From f9f33c9b29163b78d4f2d65bc2055eff4db397b2 Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Tue, 14 Jul 2026 12:57:00 +0800 Subject: [PATCH 1/2] Delegate sub-agent enablement to language server Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CONTEXT.md | 29 +++++ .../copilot/eclipse/core/Constants.java | 1 - .../copilot/eclipse/core/FeatureFlags.java | 68 ----------- .../core/events/CopilotEventConstants.java | 5 - .../core/lsp/CopilotLanguageClient.java | 4 - .../core/lsp/LsStreamConnectionProvider.java | 2 +- .../policy/DidChangePolicyParams.java | 15 +-- .../ui/preferences/ChatPreferencesPage.java | 112 ------------------ .../CopilotPreferenceInitializer.java | 1 - .../eclipse/ui/preferences/Messages.java | 2 - .../ui/preferences/messages.properties | 2 - ...sub-agent-enablement-is-server-governed.md | 75 ++++++++++++ 12 files changed, 106 insertions(+), 210 deletions(-) create mode 100644 CONTEXT.md create mode 100644 docs/adr/0001-sub-agent-enablement-is-server-governed.md diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 00000000..78e6cee3 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,29 @@ +# Context & Glossary + +This file is a glossary of the ubiquitous language used in this codebase. It is +intentionally free of implementation detail — it defines *what words mean*, not +*how things are built*. + +## Terms + +### Sub-agent +A delegated agent that the main agent spawns to carry out a scoped sub-task on +its behalf. The main agent invokes it through the `run_subagent` tool and +receives the sub-agent's result back as part of its own turn. + +### Sub-agent policy +Organization-level governance that decides whether sub-agents are permitted for +a user. It is authoritative and is **enforced by the language server**, not by +the editor client: when the policy forbids sub-agents, the server refuses to +offer the `run_subagent` tool regardless of what the client advertises. + +### Capability +A feature the editor client declares support for when it initializes its +connection to the language server. A capability is an *offer of support*, not a +*grant of permission* — the server may still withhold the corresponding feature +(for example, because the sub-agent policy forbids it). + +### Client preview feature +A gate that exposes in-development features to a subset of users. It is distinct +from the sub-agent policy: after this change, the availability of sub-agents no +longer depends on the client preview feature. diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/Constants.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/Constants.java index 176bc245..101fa319 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/Constants.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/Constants.java @@ -26,7 +26,6 @@ private Constants() { public static final String PROXY_KERBEROS_SP = "proxyKerberosSp"; public static final String GITHUB_ENTERPRISE = "githubEnterprise"; public static final String WORKSPACE_CONTEXT_ENABLED = "workspaceContextEnabled"; - public static final String SUB_AGENT_ENABLED = "subAgentEnabled"; public static final String AGENT_MAX_REQUESTS = "agentMaxRequests"; public static final String ENABLE_SKILLS = "enableSkills"; public static final String TRANSCRIPT_SUBDIR = ".copilot/eclipse"; diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/FeatureFlags.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/FeatureFlags.java index f54e0bfa..e3414736 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/FeatureFlags.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/FeatureFlags.java @@ -5,7 +5,6 @@ import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.InstanceScope; -import org.osgi.service.prefs.BackingStoreException; /** * Class to manage feature flags for the Copilot plugin. This class allows enabling or disabling features. @@ -21,8 +20,6 @@ public class FeatureFlags { private boolean mcpContributionPointEnabled = false; - private boolean subAgentPolicyEnabled = true; - private boolean customAgentPolicyEnabled = true; private boolean autoApprovalTokenEnabled = true; @@ -61,25 +58,6 @@ public void setMcpContributionPointEnabled(boolean mcpContributionPointEnabled) this.mcpContributionPointEnabled = mcpContributionPointEnabled; } - public boolean isSubAgentPolicyEnabled() { - return subAgentPolicyEnabled; - } - - /** - * Sets whether the sub-agent policy is enabled. - * When the policy is disabled, it also disables the user preference for sub-agent. - * - * @param subAgentPolicyEnabled true to enable the sub-agent policy, false to disable it - */ - public void setSubAgentPolicyEnabled(boolean subAgentPolicyEnabled) { - this.subAgentPolicyEnabled = subAgentPolicyEnabled; - - // When policy disables subagent, also disable the user preference - if (!subAgentPolicyEnabled) { - disableSubAgentPreference(); - } - } - public boolean isCustomAgentPolicyEnabled() { return customAgentPolicyEnabled; } @@ -113,17 +91,11 @@ public boolean isClientPreviewFeatureEnabled() { /** * Sets whether the client preview feature is enabled. - * When the feature is disabled, it also disables the user preference for sub-agent. * * @param clientPreviewFeatureEnabled true to enable the client preview feature, false to disable it */ public void setClientPreviewFeatureEnabled(boolean clientPreviewFeatureEnabled) { this.clientPreviewFeatureEnabled = clientPreviewFeatureEnabled; - - // When client preview feature is disabled, also disable the user preference for subagent - if (!clientPreviewFeatureEnabled) { - disableSubAgentPreference(); - } } /** @@ -143,46 +115,6 @@ public static boolean isWorkspaceContextEnabled() { return false; } - /** - * Disables the user preference for sub-agent. - * This method accesses the UI plugin's preference store to set the sub-agent preference to false. - * - *

Note: The sub-agent preference is used to initialize capabilities which happens before policies - * are sent to us. Therefore, we need to flush the preference immediately when policy changes occur - * to ensure the next capability initialization reflects the updated policy state. - */ - private void disableSubAgentPreference() { - // The preference is stored in the UI plugin's preference store, which uses InstanceScope internally - IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode("com.microsoft.copilot.eclipse.ui"); - if (prefs != null) { - // Only update and flush if the current setting is true - boolean currentValue = prefs.getBoolean(Constants.SUB_AGENT_ENABLED, false); - if (currentValue) { - prefs.putBoolean(Constants.SUB_AGENT_ENABLED, false); - try { - prefs.flush(); - } catch (BackingStoreException e) { - CopilotCore.LOGGER.error("Failed to save subagent preference when disabled by policy", e); - } - } - } - } - - /** - * Checks if the sub-agent is enabled. - * Sub-agent is enabled only if both the user preference is enabled AND the organization policy allows it. - * - * @return true if the sub-agent is enabled, false otherwise. - */ - public static boolean isSubAgentEnabled() { - IEclipsePreferences uiPrefs = InstanceScope.INSTANCE.getNode("com.microsoft.copilot.eclipse.ui"); - if (uiPrefs != null) { - return uiPrefs.getBoolean(Constants.SUB_AGENT_ENABLED, false); - } - - return false; - } - /** * Checks if the custom agent is enabled. * Custom agent is enabled only if the organization policy allows it. diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/events/CopilotEventConstants.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/events/CopilotEventConstants.java index 1a2d3c9e..8b5b6fa3 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/events/CopilotEventConstants.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/events/CopilotEventConstants.java @@ -86,11 +86,6 @@ public class CopilotEventConstants { public static final String TOPIC_DID_CHANGE_MCP_CONTRIBUTION_POINT_POLICY = TOPIC_POLICY + "MCP_CONTRIBUTION_POINT_ENABLED"; - /** - * Event when the sub-agent policy flag is updated. - */ - public static final String TOPIC_DID_CHANGE_SUB_AGENT_POLICY = TOPIC_POLICY + "SUB_AGENT_ENABLED"; - /** * Event when the custom agent policy flag is updated. */ diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java index cf0e81ad..e7008691 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java @@ -351,10 +351,6 @@ public void onDidChangePolicy(DidChangePolicyParams params) { eventBroker.post(CopilotEventConstants.TOPIC_DID_CHANGE_MCP_CONTRIBUTION_POINT_POLICY, params.isMcpContributionPointEnabled()); } - if (flags.isSubAgentPolicyEnabled() != params.isSubAgentEnabled()) { - flags.setSubAgentPolicyEnabled(params.isSubAgentEnabled()); - eventBroker.post(CopilotEventConstants.TOPIC_DID_CHANGE_SUB_AGENT_POLICY, params.isSubAgentEnabled()); - } if (flags.isCustomAgentPolicyEnabled() != params.isCustomAgentEnabled()) { flags.setCustomAgentPolicyEnabled(params.isCustomAgentEnabled()); eventBroker.post(CopilotEventConstants.TOPIC_DID_CHANGE_CUSTOM_AGENT_POLICY, params.isCustomAgentEnabled()); diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java index 6f637137..4c775f84 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/LsStreamConnectionProvider.java @@ -55,7 +55,7 @@ public Object getInitializationOptions(@Nullable URI rootUri) { NameAndVersion editorPluginInfo = new NameAndVersion(EDITOR_PLUGIN_NAME, bundleVersion); List supportedUriSchemes = PlatformUtils.getSupportedUriSchemes(); CopilotCapabilities capabilities = new CopilotCapabilities(false, FeatureFlags.isWorkspaceContextEnabled(), - FeatureFlags.isSubAgentEnabled(), supportedUriSchemes); + true /*isSubAgentEnabled*/, supportedUriSchemes); return new InitializationOptions(editorInfo, editorPluginInfo, capabilities); } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/policy/DidChangePolicyParams.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/policy/DidChangePolicyParams.java index 3d21772a..1f194a50 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/policy/DidChangePolicyParams.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/policy/DidChangePolicyParams.java @@ -16,9 +16,6 @@ public class DidChangePolicyParams { @SerializedName("mcp.contributionPoint.enabled") private boolean mcpContributionPointEnabled; - @SerializedName("subAgent.enabled") - private boolean subAgentEnabled = true; - @SerializedName("customAgent.enabled") private boolean customAgentEnabled = true; @@ -33,14 +30,6 @@ public void setMcpContributionPointEnabled(boolean mcpContributionPointEnabled) this.mcpContributionPointEnabled = mcpContributionPointEnabled; } - public boolean isSubAgentEnabled() { - return subAgentEnabled; - } - - public void setSubAgentEnabled(boolean subAgentEnabled) { - this.subAgentEnabled = subAgentEnabled; - } - public boolean isCustomAgentEnabled() { return customAgentEnabled; } @@ -59,7 +48,7 @@ public void setAutoApprovalPolicyEnabled(boolean autoApprovalPolicyEnabled) { @Override public int hashCode() { - return Objects.hash(mcpContributionPointEnabled, subAgentEnabled, customAgentEnabled, + return Objects.hash(mcpContributionPointEnabled, customAgentEnabled, autoApprovalPolicyEnabled); } @@ -76,7 +65,6 @@ public boolean equals(Object obj) { } DidChangePolicyParams other = (DidChangePolicyParams) obj; return mcpContributionPointEnabled == other.mcpContributionPointEnabled - && subAgentEnabled == other.subAgentEnabled && customAgentEnabled == other.customAgentEnabled && autoApprovalPolicyEnabled == other.autoApprovalPolicyEnabled; } @@ -85,7 +73,6 @@ public boolean equals(Object obj) { public String toString() { ToStringBuilder builder = new ToStringBuilder(this); builder.append("mcpContributionPointEnabled", mcpContributionPointEnabled); - builder.append("subAgentEnabled", subAgentEnabled); builder.append("customAgentEnabled", customAgentEnabled); builder.append("autoApprovalPolicyEnabled", autoApprovalPolicyEnabled); return builder.toString(); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/ChatPreferencesPage.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/ChatPreferencesPage.java index 11a791ef..0b0ce6ff 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/ChatPreferencesPage.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/ChatPreferencesPage.java @@ -3,12 +3,6 @@ package com.microsoft.copilot.eclipse.ui.preferences; -import java.lang.reflect.Type; -import java.util.HashMap; -import java.util.Map; - -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.layout.GridDataFactory; @@ -60,29 +54,6 @@ public void createFieldEditors() { addNote(parent, Messages.preferences_page_watched_files_note_content); addSeparator(parent); - // Add sub-agent toggle - Composite subAgentComposite = createSectionComposite(parent, gdf); - boolean policyAllowsSubAgent = isPolicyAllowsSubAgent(); - if (!policyAllowsSubAgent) { - Composite disabledComposite = new Composite(subAgentComposite, SWT.NONE); - GridLayout disabledCompositeLayout = new GridLayout(1, false); - disabledCompositeLayout.marginWidth = 0; - disabledCompositeLayout.marginHeight = 0; - disabledComposite.setLayout(disabledCompositeLayout); - disabledComposite.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); - WrappableIconLink.createWithCustomizedImage(disabledComposite, "/icons/information.png", - Messages.setting_managed_by_organization); - } - - BooleanFieldEditor subAgentField = new BooleanFieldEditor(Constants.SUB_AGENT_ENABLED, - Messages.preferences_page_sub_agent, SWT.WRAP, subAgentComposite); - subAgentField.setEnabled(policyAllowsSubAgent, subAgentComposite); - applyFieldWidthHint(subAgentField, subAgentComposite); - addField(subAgentField); - - addNote(parent, Messages.preferences_page_sub_agent_note_content); - addSeparator(parent); - if (isClientPreviewFeatureEnabled()) { // Add Enable Skills toggle Composite skillsComposite = createSectionComposite(parent, gdf); @@ -111,41 +82,15 @@ public void createFieldEditors() { @Override public void init(IWorkbench workbench) { setPreferenceStore(CopilotUi.getPlugin().getPreferenceStore()); - - // Ensure run_subagent tool configuration is consistent with sub-agent preference - // Only check if sub-agent is policy-enabled - if (isPolicyAllowsSubAgent()) { - boolean subAgentEnabled = getPreferenceStore().getBoolean(Constants.SUB_AGENT_ENABLED); - updateSubAgentToolConfiguration(subAgentEnabled); - } } @Override public boolean performOk() { final boolean oldWorkspaceContextValue = getPreferenceStore().getBoolean(Constants.WORKSPACE_CONTEXT_ENABLED); - // Check if sub-agent is policy-enabled before handling sub-agent preferences - boolean policyAllowsSubAgent = isPolicyAllowsSubAgent(); - - boolean oldSubAgentValue = false; - if (policyAllowsSubAgent) { - oldSubAgentValue = getPreferenceStore().getBoolean(Constants.SUB_AGENT_ENABLED); - } - final boolean result = super.performOk(); boolean newWorkspaceContextValue = getPreferenceStore().getBoolean(Constants.WORKSPACE_CONTEXT_ENABLED); - boolean newSubAgentValue = false; - if (policyAllowsSubAgent) { - newSubAgentValue = getPreferenceStore().getBoolean(Constants.SUB_AGENT_ENABLED); - } - - // Handle sub-agent preference change - boolean isSubAgentChanged = policyAllowsSubAgent && (oldSubAgentValue ^ newSubAgentValue); - if (isSubAgentChanged) { - updateSubAgentToolConfiguration(newSubAgentValue); - } - boolean isWorkspaceContextChanged = oldWorkspaceContextValue ^ newWorkspaceContextValue; if (isWorkspaceContextChanged) { try { @@ -153,9 +98,7 @@ public boolean performOk() { } catch (BackingStoreException e) { CopilotCore.LOGGER.error("Failed to save preference 'Enable workspace context'", e); } - } - if (isSubAgentChanged || isWorkspaceContextChanged) { boolean restart = MessageDialog.openQuestion(getShell(), Messages.preferences_page_restart_required, Messages.preferences_page_restart_question); @@ -196,63 +139,8 @@ private void addSeparator(Composite parent) { separator.setLayoutData(gridData); } - private boolean isPolicyAllowsSubAgent() { - FeatureFlags flags = CopilotCore.getPlugin().getFeatureFlags(); - return isClientPreviewFeatureEnabled() && flags != null && flags.isSubAgentPolicyEnabled(); - } - private boolean isClientPreviewFeatureEnabled() { FeatureFlags flags = CopilotCore.getPlugin().getFeatureFlags(); return flags != null && flags.isClientPreviewFeatureEnabled(); } - - /** - * Updates the MCP tool configuration to include or exclude the run_subagent tool for agent mode based on the - * sub-agent preference setting. - * - * @param subAgentEnabled true if sub-agent is enabled, false otherwise - */ - private void updateSubAgentToolConfiguration(boolean subAgentEnabled) { - try { - // Load existing MCP tools mode status - String existingJson = getPreferenceStore().getString(Constants.MCP_TOOLS_MODE_STATUS); - - // Parse existing configuration or create new one - Map>> modeToolStatus; - if (existingJson != null && !existingJson.trim().isEmpty()) { - Type type = new TypeToken>>>() { - }.getType(); - modeToolStatus = new Gson().fromJson(existingJson, type); - } else { - modeToolStatus = new HashMap<>(); - } - - // Ensure agent-mode map exists - if (!modeToolStatus.containsKey("agent-mode")) { - modeToolStatus.put("agent-mode", new HashMap<>()); - } - - // Get or create the Built-in Tools server map - Map> agentModeTools = modeToolStatus.get("agent-mode"); - String builtInToolsKey = Messages.preferences_page_mcp_tools_builtin; - if (!agentModeTools.containsKey(builtInToolsKey)) { - agentModeTools.put(builtInToolsKey, new HashMap<>()); - } - - // Update the run_subagent tool status - Map builtInTools = agentModeTools.get(builtInToolsKey); - if (subAgentEnabled) { - builtInTools.put("run_subagent", true); - } else { - builtInTools.remove("run_subagent"); - } - - // Save back to preferences - String updatedJson = new Gson().toJson(modeToolStatus); - getPreferenceStore().setValue(Constants.MCP_TOOLS_MODE_STATUS, updatedJson); - - } catch (Exception e) { - CopilotCore.LOGGER.error("Failed to update sub-agent tool configuration", e); - } - } } \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/CopilotPreferenceInitializer.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/CopilotPreferenceInitializer.java index 6b43717c..2e0f33af 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/CopilotPreferenceInitializer.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/CopilotPreferenceInitializer.java @@ -31,7 +31,6 @@ public void initializeDefaultPreferences() { pref.setDefault(Constants.PROXY_KERBEROS_SP, ""); pref.setDefault(Constants.GITHUB_ENTERPRISE, ""); pref.setDefault(Constants.WORKSPACE_CONTEXT_ENABLED, false); - pref.setDefault(Constants.SUB_AGENT_ENABLED, true); pref.setDefault(Constants.AGENT_MAX_REQUESTS, 25); pref.setDefault(Constants.ENABLE_SKILLS, true); pref.setDefault(Constants.CUSTOM_INSTRUCTIONS_WORKSPACE_ENABLED, false); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/Messages.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/Messages.java index 7582fd7a..d8053973 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/Messages.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/Messages.java @@ -58,8 +58,6 @@ public class Messages extends NLS { public static String preferences_page_watched_files; public static String preferences_page_watched_files_note_content; public static String preferences_page_restart_question; - public static String preferences_page_sub_agent; - public static String preferences_page_sub_agent_note_content; public static String preferences_page_mcp; public static String preferences_page_proxy_config_link; public static String preferences_page_proxy_settings; diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/messages.properties b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/messages.properties index f041db59..bd10669e 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/messages.properties +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/messages.properties @@ -97,8 +97,6 @@ preferences_page_custom_instructions_git_commit_desc=Set custom instructions for preferences_page_custom_instructions_git_commit_note= Access this feature in the Git Staging view by clicking the Copilot icon. You can find this view in the Git perspective or add it via the 'Window' > 'Show View' menu. preferences_page_watched_files_note_content= Allow the use of @workspace in Ask Mode. Enabling this feature may affect startup performance. preferences_page_restart_question=You need to restart Eclipse to apply the changes. Would you like to restart now? -preferences_page_sub_agent= Enable sub-agent -preferences_page_sub_agent_note_content= Allow Copilot to use sub-agents for complex multi-step tasks. preferences_page_restart_required= Restart Required # CustomModesPreferencePage customModes_page_description=Configure custom agents stored as .agent.md files in .github/agents directory. diff --git a/docs/adr/0001-sub-agent-enablement-is-server-governed.md b/docs/adr/0001-sub-agent-enablement-is-server-governed.md new file mode 100644 index 00000000..3afb94d1 --- /dev/null +++ b/docs/adr/0001-sub-agent-enablement-is-server-governed.md @@ -0,0 +1,75 @@ +# 1. Sub-agent enablement is governed by the language server + +Date: 2026-07-01 + +## Status + +Accepted + +## Context + +Historically the Eclipse client tried to gate sub-agents (the `run_subagent` +tool) itself, through three overlapping mechanisms: + +- a user preference (`subAgentEnabled`) with a toggle on the Chat preference + page, +- an in-memory organization-policy flag (`subAgentPolicyEnabled`) updated from + the server's `policy/didChange` notification, and +- the client-preview feature flag. + +Because the `subAgent` capability is read only once, at LSP initialization — +which always happens *before* the first `policy/didChange` arrives — the client +persisted policy state by force-writing the user preference to `false`. This +coupling was intricate and had two defects we confirmed while investigating: + +1. **The client-side policy gate never actually worked.** The language server + emits the policy key `subagent.enabled` (all lowercase), but the Eclipse DTO + deserializes `@SerializedName("subAgent.enabled")`. Gson never matched them, + so `subAgentPolicyEnabled` stayed at its default `true` forever. +2. **Fresh users advertised the wrong value.** `FeatureFlags.isSubAgentEnabled()` + reads the raw `InstanceScope` node with a `false` fallback and does not + consult the default scope (`true`), so a user who never opened the Chat + preference page advertised `subAgent=false` at init. + +Meanwhile the language server already enforces the policy authoritatively: + +- `run_subagent` registration is gated on `subagent.enabled` **before** the + client capability is consulted (policy wins), and +- when the policy flips to disabled at runtime, the server forces + `subAgent: false` into its own capabilities provider and re-evaluates, + unregistering the tool live. + +So the client capability is an *offer of support*, and the server is free to +override it. The elaborate client-side gating and persistence were redundant +with — and weaker than — the server's own enforcement. + +## Decision + +The Eclipse client will **always advertise the `subAgent` capability at +initialization** and delegate all sub-agent enablement and policy enforcement to +the language server. Concretely: + +- Remove the `subAgentEnabled` user preference and its toggle on the Chat + preference page. +- Remove the client-preview coupling for sub-agents. +- Remove the now-dead client-side policy plumbing: the `subAgentPolicyEnabled` + flag, the `TOPIC_DID_CHANGE_SUB_AGENT_POLICY` event, the `subAgentEnabled` + field on the policy DTO, and the preference force-write bridge. +- Do **not** persist any sub-agent policy state on the client. + +## Consequences + +- **Simpler, correct behavior.** Sub-agents are available to every user unless + the organization policy forbids them, enforced in one authoritative place. + This also fixes the fresh-user "advertised `false`" bug. +- **No client-side opt-out.** A user who previously disabled sub-agents via the + toggle will now have them enabled (subject to org policy). This is the intended + outcome of "always enable". +- **Server dependency.** Correct behavior now relies on the server continuing to + gate `run_subagent` on `subagent.enabled`. If a future server regressed that + gate, the client would no longer provide a second line of defense. +- **Orphaned preference key.** The `subAgentEnabled` instance-scope value left on + existing installs is inert; no migration is performed. +- **Reversal cost.** Re-introducing a client-side toggle would mean rebuilding + the preference, the tool-picker seeding, and the init-vs-policy persistence + bridge — non-trivial, which is why this is recorded here. From 6530eda03638c5f1632ff7fea34706d4a4f4d6fa Mon Sep 17 00:00:00 2001 From: Sheng Chen Date: Tue, 14 Jul 2026 13:04:50 +0800 Subject: [PATCH 2/2] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/adr/0001-sub-agent-enablement-is-server-governed.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/adr/0001-sub-agent-enablement-is-server-governed.md b/docs/adr/0001-sub-agent-enablement-is-server-governed.md index 3afb94d1..12133223 100644 --- a/docs/adr/0001-sub-agent-enablement-is-server-governed.md +++ b/docs/adr/0001-sub-agent-enablement-is-server-governed.md @@ -62,9 +62,9 @@ the language server. Concretely: - **Simpler, correct behavior.** Sub-agents are available to every user unless the organization policy forbids them, enforced in one authoritative place. This also fixes the fresh-user "advertised `false`" bug. -- **No client-side opt-out.** A user who previously disabled sub-agents via the - toggle will now have them enabled (subject to org policy). This is the intended - outcome of "always enable". +- **No dedicated client-side opt-out.** The `subAgentEnabled` toggle is removed; the client always advertises `subAgent`. + Users who previously disabled sub-agents via the toggle may still have a persisted `mcpToolsModeStatus` override for + `run_subagent` and may need to reset their MCP tool settings to re-enable it (subject to org policy). - **Server dependency.** Correct behavior now relies on the server continuing to gate `run_subagent` on `subagent.enabled`. If a future server regressed that gate, the client would no longer provide a second line of defense.