agentHost: configure plugins for remote agent hosts#312225
agentHost: configure plugins for remote agent hosts#312225joshspicer wants to merge 38 commits intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR enables remote Agent Host Protocol (AHP) servers to own and persist plugin customization configuration (rather than relying on client-side sync), and wires those host-owned plugins through the agent host state/actions pipeline into the Sessions AI customization UI.
Changes:
- Persist host-owned plugin customizations in agent host root config (
agent-host-config.json) and plumbroot/configChangedthrough server dispatch + side effects to republish live customization state. - Extend customization harness descriptors and the Plugins UI to support remote plugin items plus harness-provided toolbar/actions (add/remove/manage flows for remote hosts).
- Update protocol/state typings and add targeted agentHost + sessions tests for root config and customization republishing behavior.
Show a summary per file
| File | Description |
|---|---|
| src/vs/workbench/contrib/chat/common/customizationHarnessService.ts | Adds pluginActions, per-item actions, and itemKey to support remote/plugin management UI. |
| src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts | Renders remote plugin entries, groups them, and allows harness-driven toolbar + item context actions. |
| src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/loggingAgentConnection.ts | Updates dispatch typing to StateAction for broader action plumbing. |
| src/vs/sessions/contrib/remoteAgentHost/test/browser/remoteAgentHostCustomizationHarness.test.ts | New browser tests for remote host plugin item identity and removal behavior. |
| src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts | Implements remote host plugin controller + provider; maps root/session state to plugin list items & actions. |
| src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHost.contribution.ts | Wires remote harness registration with plugin controller, provider, and required services. |
| src/vs/sessions/AI_CUSTOMIZATIONS.md | Documents external harnesses + pluginActions and how plugins are surfaced for remote harnesses. |
| src/vs/platform/agentHost/test/node/mockAgent.ts | Extends mock agent to support host/session customization APIs used by new side effects + tests. |
| src/vs/platform/agentHost/test/node/agentSideEffects.test.ts | Adds coverage for republishing customizations on root config changes + clearing client customizations. |
| src/vs/platform/agentHost/test/node/agentService.test.ts | Adds coverage ensuring root config changes are persisted to agent-host-config.json. |
| src/vs/platform/agentHost/node/protocolServerHandler.ts | Validates client-dispatchable actions (incl root actions) before forwarding to agent service. |
| src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts | Adds customizationDirectory so workspace-scoped host plugins resolve against the intended root. |
| src/vs/platform/agentHost/node/copilot/copilotAgent.ts | Implements host-owned + session-effective customization resolution and plugin parsing flow updates. |
| src/vs/platform/agentHost/node/agentSideEffects.ts | Applies host config to agents and republishes agent/session customizations when root config changes. |
| src/vs/platform/agentHost/node/agentService.ts | Accepts root config resource, persists root config on RootConfigChanged, and initializes providers with host customizations. |
| src/vs/platform/agentHost/node/agentHostStateManager.ts | Broadens client dispatch to StateAction and exposes session URIs for republish flows. |
| src/vs/platform/agentHost/node/agentHostServerMain.ts | Provides default persisted root-config path when starting standalone agent host server. |
| src/vs/platform/agentHost/node/agentHostMain.ts | Provides default persisted root-config path when starting agent host utility process. |
| src/vs/platform/agentHost/node/agentConfigurationService.ts | Introduces root config load/validate/persist support and initializes root config state. |
| src/vs/platform/agentHost/electron-browser/agentHostService.ts | Updates action dispatch typing to StateAction. |
| src/vs/platform/agentHost/common/state/protocol/version/registry.ts | Registers new action version mapping (incl SessionActivityChanged). |
| src/vs/platform/agentHost/common/state/protocol/state.ts | Adds customization scoping + source metadata and session activity field. |
| src/vs/platform/agentHost/common/state/protocol/reducers.ts | Updates reducer and isClientDispatchable to cover root actions; adds session activity reducer case. |
| src/vs/platform/agentHost/common/state/protocol/actions.ts | Adds SessionActivityChanged action and extends StateAction union. |
| src/vs/platform/agentHost/common/state/protocol/action-origin.generated.ts | Regenerates action origin unions and dispatchability map for new root/session actions. |
| src/vs/platform/agentHost/common/state/protocol/.ahp-version | Bumps synced protocol version hash. |
| src/vs/platform/agentHost/common/state/agentSubscription.ts | Broadens optimistic dispatch signature to StateAction. |
| src/vs/platform/agentHost/common/agentService.ts | Extends agent interfaces for host/session customizations and updates dispatch typing to StateAction. |
| src/vs/platform/agentHost/common/agentHostCustomizationConfig.ts | New schema + helpers for persisted host customization config and scope matching. |
| src/vs/platform/agentHost/browser/remoteAgentHostProtocolClient.ts | Updates dispatch typing to StateAction. |
| src/vs/platform/agentHost/browser/nullAgentHostService.ts | Updates dispatch typing to StateAction. |
Copilot's findings
Comments suppressed due to low confidence (1)
src/vs/platform/agentHost/common/state/agentSubscription.ts:464
dispatchOptimisticnow acceptsStateAction(so clients can send root actions likeroot/configChanged), but it still only applies optimistic updates for session actions. This means root-config updates won’t be reflected locally until the server echo arrives, and it also conflicts with the comment that root state is “server-only mutations”. Either implement write-ahead/reconciliation for client-dispatchable root actions (at leastRootConfigChanged) or explicitly document/handle why root actions are excluded here.
dispatchOptimistic(action: StateAction): number {
if (isSessionAction(action)) {
const entry = this._subscriptions.get(URI.parse(action.session));
if (entry && entry.sub instanceof SessionStateSubscription) {
return entry.sub.applyOptimistic(action);
}
}
return this._seqAllocator();
}
- Files reviewed: 31/31 changed files
- Comments generated: 1
655622b to
5266a5c
Compare
Sync CustomizationRef.scope, SessionCustomization.source, and RootConfigChanged action from the agent-host-protocol spec branch (8d19730). Add agentHostCustomizationConfig.ts with the JSON schema and validation helpers for host-owned plugin configuration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add AgentConfigurationService for root config read/write/persist to agent-host-config.json. Wire root action dispatch and side effects so that host plugin changes are persisted and republished as session customizations. Implement PluginController in copilotAgent.ts to merge host-owned and client-synced plugins, resolve them into IParsedPlugin snapshots, and feed them into the SDK via _buildSessionConfig(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add RemoteAgentPluginController for add/remove plugin commands and RemoteAgentCustomizationItemProvider that surfaces host-owned and session-synced plugins. Expand each plugin directory via IFileService to discover individual skills, agents, instructions, and prompts for per-type sections. Extend pluginListWidget with remote item rendering, toolbar actions, group headers, and live refresh on itemProvider changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add tests for host config dispatch, side effect republishing, scope filtering, and the remote harness item provider (distinct item keys for scope-distinct plugins, client-synced vs host-owned separation). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add ICustomizationDisableProvider interface (opt-out model) alongside the existing ICustomizationSyncProvider. Add AgentCustomizationDisableProvider implementation and LocalAgentHostCustomizationItemProvider with shared enumerateLocalCustomizationsForHarness() and resolveCustomizationRefs() helpers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace ICustomizationSyncProvider usage with ICustomizationDisableProvider across all consumers. Simplify ProviderCustomizationItemSource to a single required itemProvider (no more dual local/remote path). Update harness descriptors to use disableProvider. Listen to both disable provider and prompts service change events for auto-sync. Add throttling for bundler re-resolves. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Delete AgentCustomizationSyncProvider and its tests. The opt-out ICustomizationDisableProvider is now the only sync model. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix synthetic bundle URI scheme: synced-customization:// URIs must not be wrapped as agent-host:// since the server lacks that scheme; expansion now reads the client in-memory FS directly - Suppress client-synced plugin entries from the Plugins list (they are already shown under 'Enabled Locally'); their contents still expand into per-type tabs (Skills, Agents, etc.) - Propagate childGroupKey through expansion so host-originated items land in the 'Remote' group and client-synced items land in 'Client' group - Rename groups to 'Remote' / 'Client' in both the Plugins view and the per-type tabs - Remove redundant 'Remote Host' badge from host-configured plugin items - Add dotfile filter (.DS_Store etc.) in _collectFromTypeDir - Include PromptsStorage.extension in SYNCABLE_STORAGE_SOURCES so built-in extension skills reach the agent host SDK - Remove checkbox column from Agent Customizations list widget - Enhance debug report with installed harnesses and plugins (Stages 5-6) - Fix test: add IPromptsService stub to agentHostChatContribution test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
5266a5c to
b55a44d
Compare
Skills are conventionally directories containing SKILL.md, so the file
locator returns URIs like `/skills/skill-a/SKILL.md`. The bundler used
`basename(uri)` for the destination filename, causing every skill to
overwrite the same `skills/SKILL. only the last one survived.md`
Fix: detect SKILL.md filenames and preserve the parent directory
structure (`skills/{skillName}/SKILL.md`), matching the Open Plugin
convention that the expansion code already handles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reverts the broad StateAction widening on the dispatchAction/dispatch interface boundary. Instead of accepting any StateAction (which includes server-only emissions like SessionReady, SessionDelta, etc.), the API now accepts the precise ClientAction union: ClientAction = ClientRootAction | ClientSessionAction | ClientTerminalAction This restores compile-time safety: passing a server-only action to dispatch() is now a type error. The new type is exported from sessionActions.ts alongside the existing SessionAction/TerminalAction aliases. Internal server-side state management (reducers, subscriptions, dispatchServerAction, dispatchClientAction) continues to use the broad StateAction type as appropriate. The one root action clients legitimately dispatch (RootConfigChanged, used to push plugin configuration changes) is included in ClientRootAction, so the remote harness's dispatchCustomizations() call compiles correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The provideChatSessionCustomizations method was skipping client-synced
items from the top-level items map with the comment that the local
'Enabled Locally' section already shows them. This is wrong for the
remote harness there is no local section, and client-syncedcontext
plugins should appear as distinct 'Client' group entries alongside
host-owned 'Remote' group entries.
Fixes the failing test:
RemoteAgentHostCustomizationHarness
provider keeps client-synced entries distinct from host-owned entries
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Covers: - Client-synced vs host-owned group assignment (remote-client/remote-host) - Synthetic bundle hidden from top-level but expanded for children - Synced-customization scheme URI preserved (not wrapped as agent-host://) - Status/statusMessage propagation from session customizations - Change event firing on SessionCustomizationsChanged action - Remove action only on host items, not client-synced items - Multi-entry remove dispatch - Multiple client-synced entries with distinct keys - SKILL.md collision prevention with subdirectory layout - Mixed SKILL.md and non-SKILL.md skill coexistence - Nonce includes subdirectory path for SKILL.md files - Rebundle clears previous tree - Bundle description includes file count Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Dynamic groups (e.g. remote-host, remote-client) were appended after the built-in group, causing built-in to appear above them. Insert dynamic groups before the built-in entry so it always stays last. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the 'Add Workspace Plugin' button and its backing methods (addPluginForWorkspace, getWorkspaceScope). Plugins are now always added at the host scope. Simplifies addConfiguredPlugin accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The 'Client' group header already communicates that entries are synced from the client. The full-width 'Synced' badge was redundant and visually inconsistent with inline badges elsewhere. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Copilot's findings
Comments suppressed due to low confidence (1)
src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts:963
filterPluginsis nowasync(awaiting remote provider results). Please ensure all call sites treat it as such (useawait/return/voidconsistently). At leastrefresh()currently calls it without awaiting, which can leave a floating promise and make refresh complete before the list state is updated.
- Files reviewed: 49/49 changed files
- Comments generated: 2
Use SessionAction | TerminalAction | RootAction inline everywhere instead of introducing a ClientAction alias type. The alias was an unnecessary abstraction since the underlying union is already descriptive and matches the pattern used throughout the codebase. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Only RootConfigChangedAction can be dispatched by clients, not the entire RootAction union. Use the specific action type in dispatch signatures to make the contract explicit and prevent clients from accidentally sending server-only root actions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file deletion was unrelated to the remote plugin feature. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
dispatchOptimistic and dispatchClientAction accepted the broad StateAction union, but clients can only send SessionAction, TerminalAction, or RootConfigChangedAction. Narrow the signatures to match the established dispatch contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Client-synced items are already shown in the 'Enabled Locally' section. Showing them again in a separate 'Client' group is redundant. Skip remote-client items when building plugin page groups. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolve three merge conflicts from main: - remoteAgentHostCustomizationHarness.ts: keep our enhanced provider with session source tracking and plugin expansion - aiCustomizationItemSource.ts: drop old fetchLocalSyncableItems (replaced by disable model) - Add extensionId/pluginUri fields required by updated ICustomizationItem Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
AgentConfigurationService constructor was overwriting the root config that AgentHostStateManager had already seeded with platformRootSchema (permissions). Now merges both schema properties and values so platform-owned keys like permissions are preserved alongside our customizations key. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The _publishSessionCustomizationsSoon call before setClientCustomizations would publish stale/previous client customization state. The progress callback and .then() already handle publishing after the agent updates its internal state, which includes the merged host+client view. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The class/interface/file were unnecessarily renamed from SyncProvider to DisableProvider in our branch. SyncProvider is a better name since the class controls which customizations get synced, with disable being just the opt-out mechanism. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
After the root config merge fix, the persisted config now contains both permissions and customizations keys. The test should assert that customizations was persisted correctly rather than expecting the full bag to contain only customizations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add void to unhandled this.refresh() in pluginListWidget itemProvider onChange handler (consistent with all other call sites) - Update comment in remoteAgentHostCustomizationHarness to reflect that synced bundles now preserve per-skill subdirectories Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
connor4312
left a comment
There was a problem hiding this comment.
couple high level comments
| return []; | ||
| } | ||
|
|
||
| return getAgentHostConfiguredCustomizations(rootState.config?.values); |
There was a problem hiding this comment.
Rather than manually reading config in this, there is an IAgentConfigurationService with getRootValue
|
|
||
| public setEnabled(pluginProtocolUri: string, enabled: boolean) { | ||
| this._enablement.set(pluginProtocolUri, enabled); | ||
| } |
There was a problem hiding this comment.
This is called any time config is updated. Most of the time, this probably won't mean customizations are necessarily updated and we'll fire events unnecessarily
There was a problem hiding this comment.
i believe this fixes and follows the existing pattern 1060c44
| * for the host customizations changes so callers can republish | ||
| * `SessionCustomization` state. | ||
| */ | ||
| setHostCustomizations?(customizations: CustomizationRef[], progress?: () => void): void; |
There was a problem hiding this comment.
Internally in the agent, plugins are managed by the PluginController. I wonder if this should have the AgentHostStateManager injected (note: we should probably make this @Injectable rather than getting passed around everywhere) and handle it internally. Then the individual agent doesn't need to know about these customizations, it just checks with the PluginController at each turn to see if it needs to wait for something to happen.
We could also apply that treatment to setClientCustomizations.
Add equality check in _applyHostCustomizationsToAgents to avoid firing setHostCustomizations and downstream publishes when root config changes for non-customization keys (e.g. permissions). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move host customization management into PluginController so the IAgent interface no longer needs setHostCustomizations. PluginController now: - injects IAgentConfigurationService - subscribes to onDidRootConfigChange (new event) - reads + resolves customizations internally - fires onDidChange so AgentSideEffects can republish This removes the external poke from AgentSideEffects and the seed call from AgentService.registerProvider, addressing Connor's architectural feedback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Verify that firing onDidCustomizationsChange on an agent triggers republishing of agent info and session customizations, and that disposing the progress listener stops republishing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address Connor's spec PR feedback: - Remove SessionCustomizationSource enum; use clientId field on SessionCustomization to distinguish host vs client origin - Remove CustomizationScopeKind, CustomizationScope, and scope field from CustomizationRef; all host plugins apply to all sessions (workspace scoping was plumbed but never user-facing) - Remove customizationMatchesDirectory() (dead code after scope removal) - Stamp clientId onto client-sourced customizations in PluginController.sync() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PluginController now reads root config from IAgentConfigurationService in its constructor. The integration test setup didn't register the service, causing all CopilotAgent tests to fail with 'Cannot read properties of undefined (reading getRootValue)'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PluginController now requires IAgentConfigurationService in its constructor. agentHostServerMain.ts and agentHostMain.ts both construct CopilotAgent via DI but didn't register the service, causing the integration test 'Agent Host Server starts with production agent services registered' to fail because the server exited prematurely on startup. Expose configurationService getter on AgentService and register it in the downstream DI containers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…te-agent-plugins # Conflicts: # src/vs/platform/agentHost/common/agentService.ts # src/vs/platform/agentHost/common/state/protocol/.ahp-version # src/vs/platform/agentHost/common/state/protocol/action-origin.generated.ts # src/vs/platform/agentHost/common/state/protocol/state.ts # src/vs/platform/agentHost/node/agentHostMain.ts # src/vs/platform/agentHost/node/agentHostServerMain.ts # src/vs/platform/agentHost/node/agentService.ts # src/vs/platform/agentHost/test/node/agentService.test.ts # src/vs/sessions/contrib/agentHost/test/browser/localAgentHostSessionsProvider.test.ts # src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts
Summary
Why
When a user connects from a browser or phone, they may not have any local customization state to sync into the remote host. This change lets the remote AHP host configure plugins directly, then feeds those resolved plugins into the Copilot-backed runtime so sessions can use them without relying on local client state.
How it works
There are two categories of customizations surfaced when connected to a remote host:
Client customizations ("Client" group in UI)
These are your local VS Code customizations (skills, instructions, agents, prompts) synced to the remote host at connection time.
SyncedCustomizationBundlercollects local files and packages them into a single synthetic Open Plugin (vscode-synced-customization://in-memory bundle)AgentHostContributiondispatchesRootConfigChangedto the remote server with the bundle included in the customization listagent.setHostCustomizations(), and the SDK loads the bundle's files into the sessionSessionCustomizationsChangedactions and surfaces each file as an individual item under a Client groupRemote customizations ("Remote" group in UI)
These are Open Plugin Format folders explicitly configured on the remote host's filesystem.
RemoteAgentPluginControllerlets you browse the remote filesystem and pick a plugin folderRootConfigChangedwith the folder URI added toagentHost.customizationsin the host's root configagentHostConfig.json), callsagent.setHostCustomizations(), and the SDK loads the pluginSessionCustomizationsChangedfires with status updates (loading → loaded/error), surfaced under a Remote group with remove affordancesThe flow that makes something appear in the UI
The server is the source of truth for what's actually loaded and its status. The client sends "here's what I want", and the server echoes back "here's what actually loaded" via
SessionCustomizationsChanged.Protocol / Spec
Paired draft PR: microsoft/agent-host-protocol#77
Notes for reviewers
agent-host-config.jsonvia AHP root configRootConfigChangedActionis the only root action clients may dispatch (narrowed from the fullRootActionunion)Validation
npm run compile-check-ts-nativenode ./build/hygiene.ts $(git diff --name-only)npm run valid-layers-checknpm run transpile-clientnpm run test-node -- --run src/vs/platform/agentHost/test/node/agentService.test.js src/vs/platform/agentHost/test/node/agentSideEffects.test.js src/vs/sessions/contrib/remoteAgentHost/test/browser/remoteAgentHostCustomizationHarness.test.js./scripts/test.shstill does not boot Electron correctly in this shell (appis undefined during startup), so the focused node runner remains the reliable validation path here.