Skip to content

agentHost: watch customizations on the host's own filesystem - #333683

Open
Ryan Ewen (RyanEwen) wants to merge 2 commits into
microsoft:mainfrom
RyanEwen:fix/host-local-customization-roots
Open

agentHost: watch customizations on the host's own filesystem#333683
Ryan Ewen (RyanEwen) wants to merge 2 commits into
microsoft:mainfrom
RyanEwen:fix/host-local-customization-roots

Conversation

@RyanEwen

Copy link
Copy Markdown
Contributor

Fixes #333665

Problem

In a remote window the agent host's customization watchers never attach. _watchCustomizations builds a ClaudeCustomizationWatcher and a SessionMcpDiscovery over the session's working directories, and in a dev container those arrive in the window's namespace:

CodeExpectedError: ENOPRO: No file system provider found for resource
  'vscode-remote://dev-container+.../workspace/app'
    at Fa.withProvider (…/agentHostMain.js)
    at async Fa.doWatch (…/agentHostMain.js)

The host reads its own disk through file: and registers no provider for vscode-remote, so every one of these watches throws. It fires on each _bindFreshConversation, so a session that binds a few chats logs it repeatedly. I counted 104 occurrences across four days of logs on one machine, 73 in a single day.

The watch failing is only half of it. Both watchers also match change events against the URIs they were given, and the events the host receives carry file: resources:

  • ClaudeCustomizationWatcher filters on triggers.some(t => e.affects(t)), where the triggers are built from these roots
  • SessionMcpDiscovery resolves .mcp.json under the same roots

So even where a watch did attach, nothing it produced could ever match. Project customizations and MCP config changes go unnoticed in a remote window.

The error is also invisible to callers: IFileService.watch runs its work in an async IIFE and logs the failure itself, so no try/catch at any call site sees it, and the stack in the log contains only file service frames.

Change

Reinterpret the roots as host-local paths once, at the point the watchers are created.

toHostLocalUri converts a vscode-remote: URI to the file: path it denotes on this machine and leaves everything else untouched. Applying it inside _watchCustomizations covers all three call sites, and fixes the event matching at the same time, because the triggers are derived from the roots it now returns.

It is deliberately not applied in the workingDirectories getter: that value is also handed back to the client, which must keep the window's namespace.

How to test

  1. Open a folder in a dev container and start a Claude agent host chat.
  2. Watch the agent host log (OutputAgent Host, or ~/.vscode-server/data/logs/<session>/agenthost.log).

On main each chat bind logs ENOPRO: No file system provider found for the workspace folder and its .claude directory. With this change the log is clean.

Verified on a live dev container by patching the shipped bundle with the equivalent change: from roughly two failures per chat bind to zero across four binds. The caller was identified by instrumenting doWatch to record its stack, since the error itself does not carry one.

Unit coverage adds three tests for toHostLocalUri: a window URI becomes an openable path, anything already openable is untouched, and query and fragment survive.

Note

toHostLocalUri is also introduced by #331527, which needs the same conversion for reading project customizations. Whichever lands second should drop the duplicate helper rather than adding a second one.

AI disclosure: this pull request and the related code were written with the assistance of AI.

@vs-code-engineering

Copy link
Copy Markdown
Contributor

📬 CODENOTIFY

The following users are being notified based on files changed in this PR:

TylerLeonhardt

Matched files:

  • src/vs/platform/agentHost/node/claude/claudeAgentSession.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Converts remote workspace URIs to host-local file URIs so Agent Host customization watchers work in remote environments.

Changes:

  • Adds toHostLocalUri.
  • Uses local roots for Claude customization and MCP discovery watchers.
  • Adds URI conversion tests.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
agentHostUri.ts Adds host-local URI conversion.
claudeAgentSession.ts Converts watcher roots before registration.
agentHostFileSystemProvider.test.ts Tests URI conversion behavior.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

));
store.add(watcher.onDidChange(() => this._onDidCustomizationsChange.fire()));
this._mcpDiscovery = directories?.length ? store.add(new SessionMcpDiscovery(directories, this._fileService)) : undefined;
this._mcpDiscovery = roots?.length ? store.add(new SessionMcpDiscovery(roots, this._fileService)) : undefined;
@RyanEwen

Copy link
Copy Markdown
Contributor Author

Correct, and specifically because primaryCwd is this.workingDirectory, which stays in the window's namespace by design, while SessionMcpDiscovery sets defaultCwd: this._root from the roots this change converts. In a remote window isEqual then compares a file: URI against a vscode-remote: one and never matches, so a disabled primary server is skipped by the deniedServers branch and an enabled one falls through to definitions.set(...). The first of those is the serious half: a server the user disabled would no longer be denied.

Fixed by comparing in the host-local namespace, since that is the namespace discovery reports in:

const primaryCwd = this.workingDirectory;
// Discovery roots are host-local, so compare identities in that namespace.
const primaryRoot = primaryCwd && toHostLocalUri(primaryCwd);

Both comparisons now use primaryRoot. Converting the comparison rather than reverting the roots keeps discovery reading the filesystem it can actually reach.

Added a remote primary root still gates its own workspace MCP servers, which asserts a disabled primary server reaches deniedMcpServers and an enabled one is not injected, with a vscode-remote: primary root. It fails on the previous commit and passes now, so it pins the regression rather than just covering the line.

One limitation worth stating: the test identifies the servers by the host-local .mcp.json path instead of going through getChatCustomizations. Discovery cannot read a remote root at all today, which is #331526, addressed separately in #331527. Once that lands this test can go through discovery like its neighbour.

AI disclosure: this comment and the related code were written with the assistance of AI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Agent host: file watches fail with ENOPRO in a remote window, because the host is handed vscode-remote URIs it cannot resolve

4 participants