From fb2f544cae19a39a9def2aba866a586184438036 Mon Sep 17 00:00:00 2001 From: kanywst Date: Sun, 24 May 2026 20:20:36 +0900 Subject: [PATCH 1/6] feat(mcp-client): scaffold built-in MCP client contribution This PR reserves the workbench slot at 'src/vs/workbench/contrib/mcpClient/' and lays out the design in 'docs/zeus-mcp-client.md'. When the implementation lands it will: - read '.zeus/mcp.json' from the workspace root - spawn stdio servers and open SSE connections - aggregate tool definitions for the agent runtime - reload on config change without restarting unaffected servers Depends on 'feat/zeus-conventions' (#23) for the '.zeus/mcp.json' schema. Implementation depends on '@modelcontextprotocol/sdk', which lands with the agent SDK PR. --- docs/zeus-mcp-client.md | 58 ++++++++++++++++++++ src/vs/workbench/contrib/mcpClient/README.md | 14 +++++ 2 files changed, 72 insertions(+) create mode 100644 docs/zeus-mcp-client.md create mode 100644 src/vs/workbench/contrib/mcpClient/README.md diff --git a/docs/zeus-mcp-client.md b/docs/zeus-mcp-client.md new file mode 100644 index 00000000..70224fae --- /dev/null +++ b/docs/zeus-mcp-client.md @@ -0,0 +1,58 @@ +# MCP client (built-in) + +Zeus consumes MCP servers listed in `.zeus/mcp.json` and exposes their tools to the editor's AI features. This is the **client** half of the MCP-first design; the **server** half lives in `feat/mcp-server`. + +## Where this lives + +`src/vs/workbench/contrib/mcpClient/` — a workbench contribution that: + +- Reads `.zeus/mcp.json` from the workspace root +- Spawns stdio MCP servers as subprocesses, or opens SSE connections +- Aggregates the tool list and exposes it to the agent runtime +- Reloads on `.zeus/mcp.json` change + +This is intentionally a built-in contribution rather than a VS Code extension. MCP server lifecycles are too important to let users disable accidentally; we want them tied to the workspace lifecycle. + +## Why not [VS Code's `vscode.lm` tool API](https://code.visualstudio.com/api/extension-guides/tools)? + +We want the MCP-first stance to be honest. VS Code's `vscode.lm.registerTool` is a fine API but it's vscode-specific. By going through `@modelcontextprotocol/sdk` directly, the same `.zeus/mcp.json` works in: + +- Claude Code CLI (already MCP-native) +- ChatGPT desktop / Codex (MCP support shipping) +- Future agents (MCP is an open spec) + +VS Code extensions can still register `lm` tools — those continue to work — but Zeus's first-class story is MCP. + +## Loader + +```text +.zeus/mcp.json + ↓ +McpConfigLoader (watches file, validates schema) + ↓ +McpClientRegistry (one McpClient per server entry) + ↓ +McpToolAggregator (combined tool list, dispatches calls) + ↓ +IAgentRuntime (Agent SDK PR consumes this) +``` + +## Sub-PRs needed before this can land + +1. `feat/zeus-conventions` (`.zeus/mcp.json` schema) — PR #23 +2. This PR — scaffold + design +3. Follow-up — `@modelcontextprotocol/sdk` dep + real implementation + +## Acceptance criteria (real impl) + +- [ ] Loads `.zeus/mcp.json` at workbench startup +- [ ] Spawns each `stdio` server as a child process +- [ ] Connects to each `sse` server with bearer auth +- [ ] Aggregates all tool definitions into a single registry +- [ ] Reloads on file change without restarting unaffected servers +- [ ] Surfaces server connection errors in the status bar +- [ ] Refuses servers that try to register tools with reserved name prefixes (`buffer_`, `agent_`, `editor_` — those belong to the MCP **server** half) + +## Status + +Scaffold only. Slot reserved at `src/vs/workbench/contrib/mcpClient/`. diff --git a/src/vs/workbench/contrib/mcpClient/README.md b/src/vs/workbench/contrib/mcpClient/README.md new file mode 100644 index 00000000..1b2992cc --- /dev/null +++ b/src/vs/workbench/contrib/mcpClient/README.md @@ -0,0 +1,14 @@ +# `mcpClient` contribution + +Slot for the built-in MCP client. Design lives at [`docs/zeus-mcp-client.md`](../../../../../../docs/zeus-mcp-client.md). + +When the real implementation lands, this directory will contain: + +- `browser/mcpClient.contribution.ts` — workbench registration +- `browser/mcpConfigLoader.ts` — `.zeus/mcp.json` watcher +- `browser/mcpClientRegistry.ts` — per-server clients +- `browser/mcpToolAggregator.ts` — unified tool registry +- `common/mcpTypes.ts` — shared types +- `test/browser/*.test.ts` — unit tests + +Until then, this README is the placeholder so other PRs can reference the path without merge conflicts. From 08165da52ac66617ab990f8633d549296846e74c Mon Sep 17 00:00:00 2001 From: kanywst Date: Sun, 24 May 2026 21:06:56 +0900 Subject: [PATCH 2/6] docs: fix relative path to design doc (5 levels, not 6) --- src/vs/workbench/contrib/mcpClient/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/mcpClient/README.md b/src/vs/workbench/contrib/mcpClient/README.md index 1b2992cc..7ce60708 100644 --- a/src/vs/workbench/contrib/mcpClient/README.md +++ b/src/vs/workbench/contrib/mcpClient/README.md @@ -1,6 +1,6 @@ # `mcpClient` contribution -Slot for the built-in MCP client. Design lives at [`docs/zeus-mcp-client.md`](../../../../../../docs/zeus-mcp-client.md). +Slot for the built-in MCP client. Design lives at [`docs/zeus-mcp-client.md`](../../../../../docs/zeus-mcp-client.md). When the real implementation lands, this directory will contain: From 7c82c69987dbcc89c1ce19e024ad8cbdac651da1 Mon Sep 17 00:00:00 2001 From: kanywst Date: Sun, 24 May 2026 23:35:12 +0900 Subject: [PATCH 3/6] docs(mcp-client): trust prompt for RCE, secret refs, tool namespacing, layering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Gemini review: - Trust model section: .zeus/mcp.json lists commands to execute, which is an RCE vector if someone commits a malicious entry and a teammate pulls + opens the workspace. Spell out: first-encounter trust prompt per server, inherit Workspace Trust, refuse shell-form commands. - Secret storage: ${env:NAME} and ${secret:keychain:NAME} references only; warn on plain values for *_TOKEN/*_KEY/*_SECRET/PASSWORD field names; loader is read-only against mcp.json. - Tool name collision across servers: namespace as '__' in the aggregated registry; UI shows the short name with server as secondary text. - Directory layout: split common/ (platform-agnostic), browser/ (UI, workspace registration), node/ (stdio child processes — cannot live in browser/ per vscode's sandbox). --- docs/zeus-mcp-client.md | 31 ++++++++++++++++++-- src/vs/workbench/contrib/mcpClient/README.md | 14 +++++---- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/docs/zeus-mcp-client.md b/docs/zeus-mcp-client.md index 70224fae..0bf8e199 100644 --- a/docs/zeus-mcp-client.md +++ b/docs/zeus-mcp-client.md @@ -4,15 +4,41 @@ Zeus consumes MCP servers listed in `.zeus/mcp.json` and exposes their tools to ## Where this lives -`src/vs/workbench/contrib/mcpClient/` — a workbench contribution that: +`src/vs/workbench/contrib/mcpClient/` — a workbench contribution split across the `common/`, `browser/`, and `node/` layers per vscode's architecture: + +- `common/` — config schema, types, the workspace-side `McpToolAggregator` that hands a unified registry to the agent runtime +- `browser/` — UI surfaces (status bar entries, error notifications, the trust-prompt for newly-added stdio entries) +- `node/` — process spawning. `stdio` MCP servers must be launched from the main / node side; `browser/` cannot spawn child processes in vscode's sandbox. SSE connections can live in either layer + +Behaviour: - Reads `.zeus/mcp.json` from the workspace root -- Spawns stdio MCP servers as subprocesses, or opens SSE connections +- Spawns stdio MCP servers (`node/`) as subprocesses, or opens SSE connections - Aggregates the tool list and exposes it to the agent runtime - Reloads on `.zeus/mcp.json` change This is intentionally a built-in contribution rather than a VS Code extension. MCP server lifecycles are too important to let users disable accidentally; we want them tied to the workspace lifecycle. +## Trust model — RCE risk + +`.zeus/mcp.json` lists *commands to execute*. Anyone with commit rights can add an arbitrary command, and a colleague who pulls and opens the workspace would silently spawn it. That's a real RCE vector. + +Mitigations: + +- **Trust prompt** — the first time a workspace is opened with a non-empty `mcp.json`, or whenever a new server entry is added in a subsequent pull, Zeus blocks startup of those servers and shows a per-server confirmation pane (similar to vscode's "Restricted Mode" workspace trust). Accepting writes a fingerprint into per-user (not in-git) state so the prompt doesn't re-fire on every edit. +- **Inherit Workspace Trust** — if the workspace is in Restricted Mode, refuse to spawn any stdio server. SSE-only entries can be allowed because they don't execute local code. +- **Hard refuse on `command: bash -c ""` patterns** — block shell-form commands in `command:`; require `args:` arrays for argument vectors. + +## Secret storage + +`mcp.json` lives in git. We only allow `${env:NAME}` and `${secret::NAME}` references in `env` blocks for credentials: + +- `${env:NAME}` resolves at spawn time from the user's environment +- `${secret:keychain:NAME}` reads from `vscode.SecretStorage` (per-user, OS-keychain backed) +- Plain string values are accepted only for non-secret config; the loader warns when a field name matches a heuristic list (`*_TOKEN`, `*_KEY`, `*_SECRET`, `PASSWORD`) and the value isn't a reference + +Never write secret values back into the file. The loader is read-only against `mcp.json`. + ## Why not [VS Code's `vscode.lm` tool API](https://code.visualstudio.com/api/extension-guides/tools)? We want the MCP-first stance to be honest. VS Code's `vscode.lm.registerTool` is a fine API but it's vscode-specific. By going through `@modelcontextprotocol/sdk` directly, the same `.zeus/mcp.json` works in: @@ -52,6 +78,7 @@ IAgentRuntime (Agent SDK PR consumes this) - [ ] Reloads on file change without restarting unaffected servers - [ ] Surfaces server connection errors in the status bar - [ ] Refuses servers that try to register tools with reserved name prefixes (`buffer_`, `agent_`, `editor_` — those belong to the MCP **server** half) +- [ ] Collisions across servers are resolved by namespacing exposed tools as `__` in the aggregated registry; the underlying call still goes to the originally-named tool on the right server. UI surfaces show the short tool name with the server name as secondary text. ## Status diff --git a/src/vs/workbench/contrib/mcpClient/README.md b/src/vs/workbench/contrib/mcpClient/README.md index 7ce60708..92fbc2da 100644 --- a/src/vs/workbench/contrib/mcpClient/README.md +++ b/src/vs/workbench/contrib/mcpClient/README.md @@ -2,13 +2,15 @@ Slot for the built-in MCP client. Design lives at [`docs/zeus-mcp-client.md`](../../../../../docs/zeus-mcp-client.md). -When the real implementation lands, this directory will contain: +When the real implementation lands, this directory will contain (vscode layering: `common/` is platform-agnostic, `browser/` is renderer, `node/` is the Node-only half that can spawn subprocesses): -- `browser/mcpClient.contribution.ts` — workbench registration -- `browser/mcpConfigLoader.ts` — `.zeus/mcp.json` watcher -- `browser/mcpClientRegistry.ts` — per-server clients -- `browser/mcpToolAggregator.ts` — unified tool registry - `common/mcpTypes.ts` — shared types -- `test/browser/*.test.ts` — unit tests +- `common/mcpToolAggregator.ts` — unified, namespaced tool registry +- `browser/mcpClient.contribution.ts` — workbench registration + status bar +- `browser/mcpConfigLoader.ts` — `.zeus/mcp.json` watcher + schema validation +- `browser/mcpTrustPrompt.ts` — user confirmation before spawning new stdio servers +- `node/mcpStdioRegistry.ts` — per-server stdio child processes (cannot live in `browser/`) +- `node/mcpSseRegistry.ts` — SSE connections (can also be browser; placed here for symmetry) +- `test/node/*.test.ts` — unit tests for the spawn paths Until then, this README is the placeholder so other PRs can reference the path without merge conflicts. From e800853d957059fb40c131c38a47e75bd023d9dc Mon Sep 17 00:00:00 2001 From: kanywst Date: Mon, 25 May 2026 00:03:39 +0900 Subject: [PATCH 4/6] docs(mcp-client): full-config fingerprint, no SSE trust bypass, refuse shell wrappers, zeus_ prefix --- docs/zeus-mcp-client.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/zeus-mcp-client.md b/docs/zeus-mcp-client.md index 0bf8e199..6f5fb70d 100644 --- a/docs/zeus-mcp-client.md +++ b/docs/zeus-mcp-client.md @@ -25,9 +25,9 @@ This is intentionally a built-in contribution rather than a VS Code extension. M Mitigations: -- **Trust prompt** — the first time a workspace is opened with a non-empty `mcp.json`, or whenever a new server entry is added in a subsequent pull, Zeus blocks startup of those servers and shows a per-server confirmation pane (similar to vscode's "Restricted Mode" workspace trust). Accepting writes a fingerprint into per-user (not in-git) state so the prompt doesn't re-fire on every edit. -- **Inherit Workspace Trust** — if the workspace is in Restricted Mode, refuse to spawn any stdio server. SSE-only entries can be allowed because they don't execute local code. -- **Hard refuse on `command: bash -c ""` patterns** — block shell-form commands in `command:`; require `args:` arrays for argument vectors. +- **Trust prompt** — the first time a workspace is opened with a non-empty `mcp.json`, *or* whenever a server entry is added **or modified** (any change to `command`, `args`, `env`, `url`, or `transport`), Zeus blocks startup of those servers and shows a per-server confirmation pane (similar to vscode's "Restricted Mode" workspace trust). The fingerprint stored in per-user (not in-git) state is a hash of the entire normalised server-config object — any tweak invalidates the prior consent so a colleague editing in `args:` re-prompts the user. +- **Inherit Workspace Trust** — if the workspace is in Restricted Mode, refuse to spawn any server (stdio *and* SSE). A remote SSE endpoint never executes local code itself, but the tools it exposes can still cause file writes, shell calls, or prompt-injection via the agent, so the trust prompt covers it too. +- **Refuse shell wrappers, not just `bash -c`** — `command:` must resolve to an actual executable path; argument vectors must go through `args:`. Reject `command:` values whose basename matches any shell (`sh`, `bash`, `zsh`, `ksh`, `fish`, `pwsh`, `cmd`, `cmd.exe`, `powershell`, `powershell.exe`) when paired with a `-c` / `/c` / `-Command` flag in `args:`. The point is to make the executable + argv structurally visible, not to chase shell-specific bypasses. ## Secret storage @@ -77,7 +77,7 @@ IAgentRuntime (Agent SDK PR consumes this) - [ ] Aggregates all tool definitions into a single registry - [ ] Reloads on file change without restarting unaffected servers - [ ] Surfaces server connection errors in the status bar -- [ ] Refuses servers that try to register tools with reserved name prefixes (`buffer_`, `agent_`, `editor_` — those belong to the MCP **server** half) +- [ ] Zeus's own MCP **server** half publishes its tools under a `zeus_` prefix (e.g. `zeus_buffer_read`, `zeus_editor_open`). Third-party servers are free to use any name they like — including `buffer_` or `editor_` — because tool name conflicts across servers are resolved by the `__` namespacing rule below, not by reserving a global prefix - [ ] Collisions across servers are resolved by namespacing exposed tools as `__` in the aggregated registry; the underlying call still goes to the originally-named tool on the right server. UI surfaces show the short tool name with the server name as secondary text. ## Status From 052ad90da6d905589c524bd14a1fb729e4a05b53 Mon Sep 17 00:00:00 2001 From: kanywst Date: Mon, 25 May 2026 00:26:00 +0900 Subject: [PATCH 5/6] docs(mcp-client): canonical-JSON fingerprint, dash/EncodedCommand, refuse plain secrets, zeus__ prefix, always-namespace, VS Code spelling --- docs/zeus-mcp-client.md | 13 +++++++------ src/vs/workbench/contrib/mcpClient/README.md | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/zeus-mcp-client.md b/docs/zeus-mcp-client.md index 6f5fb70d..126c2888 100644 --- a/docs/zeus-mcp-client.md +++ b/docs/zeus-mcp-client.md @@ -25,9 +25,9 @@ This is intentionally a built-in contribution rather than a VS Code extension. M Mitigations: -- **Trust prompt** — the first time a workspace is opened with a non-empty `mcp.json`, *or* whenever a server entry is added **or modified** (any change to `command`, `args`, `env`, `url`, or `transport`), Zeus blocks startup of those servers and shows a per-server confirmation pane (similar to vscode's "Restricted Mode" workspace trust). The fingerprint stored in per-user (not in-git) state is a hash of the entire normalised server-config object — any tweak invalidates the prior consent so a colleague editing in `args:` re-prompts the user. +- **Trust prompt** — the first time a workspace is opened with a non-empty `mcp.json`, *or* whenever a server entry is added **or modified** (any change to `command`, `args`, `env`, `url`, or `transport`), Zeus blocks startup of those servers and shows a per-server confirmation pane (similar to VS Code's "Restricted Mode" workspace trust). The fingerprint stored in per-user (not in-git) state is `sha256(canonical_json(serverConfig))`, where `canonical_json` (a) sorts object keys lexicographically at every depth, (b) resolves relative `command:` paths against the workspace root to absolute, (c) drops trailing whitespace in env values, and (d) uses LF line endings. Equal-meaning configurations therefore hash identically across platforms and JSON serializers, and any tweak invalidates the prior consent. - **Inherit Workspace Trust** — if the workspace is in Restricted Mode, refuse to spawn any server (stdio *and* SSE). A remote SSE endpoint never executes local code itself, but the tools it exposes can still cause file writes, shell calls, or prompt-injection via the agent, so the trust prompt covers it too. -- **Refuse shell wrappers, not just `bash -c`** — `command:` must resolve to an actual executable path; argument vectors must go through `args:`. Reject `command:` values whose basename matches any shell (`sh`, `bash`, `zsh`, `ksh`, `fish`, `pwsh`, `cmd`, `cmd.exe`, `powershell`, `powershell.exe`) when paired with a `-c` / `/c` / `-Command` flag in `args:`. The point is to make the executable + argv structurally visible, not to chase shell-specific bypasses. +- **Refuse shell wrappers, not just `bash -c`** — `command:` must resolve to an actual executable path; argument vectors must go through `args:`. Reject `command:` values whose basename matches any shell (`sh`, `dash`, `bash`, `zsh`, `ksh`, `fish`, `pwsh`, `cmd`, `cmd.exe`, `powershell`, `powershell.exe`) when paired with an execution flag in `args:`: `-c` / `/c` / `-Command` / `-EncodedCommand`. (PowerShell's `-EncodedCommand` is a common obfuscation vector and gets the same treatment as `-Command`.) The point is to make the executable + argv structurally visible, not to chase shell-specific bypasses. ## Secret storage @@ -35,7 +35,8 @@ Mitigations: - `${env:NAME}` resolves at spawn time from the user's environment - `${secret:keychain:NAME}` reads from `vscode.SecretStorage` (per-user, OS-keychain backed) -- Plain string values are accepted only for non-secret config; the loader warns when a field name matches a heuristic list (`*_TOKEN`, `*_KEY`, `*_SECRET`, `PASSWORD`) and the value isn't a reference +- Plain string values are accepted only for non-secret config. The loader **refuses to start** a server (rather than just warning) when a field name matches the heuristic list (`*_TOKEN`, `*_KEY`, `*_SECRET`, `PASSWORD`) and the value is not a `${env:...}` / `${secret:...}` reference. The user sees a per-server error in the status bar with a "Move to keychain" quick-fix that creates the secret and rewrites the reference for them. +- A separate high-entropy heuristic (≥ 32 chars, base64 / hex-ish) on **any** plain `env` value triggers the same refuse-and-prompt path, so secrets that don't match the field-name list still get caught. Never write secret values back into the file. The loader is read-only against `mcp.json`. @@ -73,12 +74,12 @@ IAgentRuntime (Agent SDK PR consumes this) - [ ] Loads `.zeus/mcp.json` at workbench startup - [ ] Spawns each `stdio` server as a child process -- [ ] Connects to each `sse` server with bearer auth +- [ ] Connects to each `sse` server with bearer auth, where the token **must** come from a `${secret:keychain:...}` (or `${env:...}`) reference in the server entry — hard-coded bearer tokens in `mcp.json` are refused by the same secret-storage rule above - [ ] Aggregates all tool definitions into a single registry - [ ] Reloads on file change without restarting unaffected servers - [ ] Surfaces server connection errors in the status bar -- [ ] Zeus's own MCP **server** half publishes its tools under a `zeus_` prefix (e.g. `zeus_buffer_read`, `zeus_editor_open`). Third-party servers are free to use any name they like — including `buffer_` or `editor_` — because tool name conflicts across servers are resolved by the `__` namespacing rule below, not by reserving a global prefix -- [ ] Collisions across servers are resolved by namespacing exposed tools as `__` in the aggregated registry; the underlying call still goes to the originally-named tool on the right server. UI surfaces show the short tool name with the server name as secondary text. +- [ ] Zeus's own MCP **server** half publishes its tools under the `zeus__` prefix (double underscore, matching the third-party namespacing rule below — e.g. `zeus__buffer_read`, `zeus__editor_open`). The double-underscore separator means a third-party server *literally* named `zeus` does not collide with our internal surface +- [ ] **All** third-party tools are always exposed as `__` in the aggregated registry, regardless of whether another server has registered the same short name. Always-namespacing (rather than namespacing-on-collision) means the tool name the agent sees is stable: it does not change when a new server is added later. UI surfaces still show the short tool name with the server name as secondary text. The underlying call dispatches to the originally-named tool on the right server. ## Status diff --git a/src/vs/workbench/contrib/mcpClient/README.md b/src/vs/workbench/contrib/mcpClient/README.md index 92fbc2da..a389a065 100644 --- a/src/vs/workbench/contrib/mcpClient/README.md +++ b/src/vs/workbench/contrib/mcpClient/README.md @@ -2,7 +2,7 @@ Slot for the built-in MCP client. Design lives at [`docs/zeus-mcp-client.md`](../../../../../docs/zeus-mcp-client.md). -When the real implementation lands, this directory will contain (vscode layering: `common/` is platform-agnostic, `browser/` is renderer, `node/` is the Node-only half that can spawn subprocesses): +When the real implementation lands, this directory will contain (VS Code layering: `common/` is platform-agnostic, `browser/` is renderer, `node/` is the Node-only half that can spawn subprocesses): - `common/mcpTypes.ts` — shared types - `common/mcpToolAggregator.ts` — unified, namespaced tool registry From b4b64142b7158dca4bdc2a546e57ce621af019a8 Mon Sep 17 00:00:00 2001 From: kanywst Date: Mon, 25 May 2026 22:46:11 +0900 Subject: [PATCH 6/6] docs(mcp-client): reserve 'zeus' server name, clarify fingerprint is per-machine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues raised by reviewer: 1. Reserve 'zeus' as a server name in .zeus/mcp.json. The original acceptance criterion claimed the zeus__ prefix on internal tools prevented collision with a third-party server literally named zeus, but the always-namespace rule (__) would still map such a server's tools into the same zeus__ namespace. Refuse the configuration outright instead. 2. The trust-prompt fingerprint says it resolves relative command: paths to absolute, which is intentional (we want re-consent if a different binary would be launched), but the doc also claimed configs would hash identically across platforms. Those two are inconsistent — the fingerprint is per-user / per-machine on purpose; cross-machine identity was never the goal. Clarified. --- docs/zeus-mcp-client.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/zeus-mcp-client.md b/docs/zeus-mcp-client.md index 126c2888..c1c03e80 100644 --- a/docs/zeus-mcp-client.md +++ b/docs/zeus-mcp-client.md @@ -25,7 +25,7 @@ This is intentionally a built-in contribution rather than a VS Code extension. M Mitigations: -- **Trust prompt** — the first time a workspace is opened with a non-empty `mcp.json`, *or* whenever a server entry is added **or modified** (any change to `command`, `args`, `env`, `url`, or `transport`), Zeus blocks startup of those servers and shows a per-server confirmation pane (similar to VS Code's "Restricted Mode" workspace trust). The fingerprint stored in per-user (not in-git) state is `sha256(canonical_json(serverConfig))`, where `canonical_json` (a) sorts object keys lexicographically at every depth, (b) resolves relative `command:` paths against the workspace root to absolute, (c) drops trailing whitespace in env values, and (d) uses LF line endings. Equal-meaning configurations therefore hash identically across platforms and JSON serializers, and any tweak invalidates the prior consent. +- **Trust prompt** — the first time a workspace is opened with a non-empty `mcp.json`, *or* whenever a server entry is added **or modified** (any change to `command`, `args`, `env`, `url`, or `transport`), Zeus blocks startup of those servers and shows a per-server confirmation pane (similar to VS Code's "Restricted Mode" workspace trust). The fingerprint stored in per-user (not in-git) state is `sha256(canonical_json(serverConfig))`, where `canonical_json` (a) sorts object keys lexicographically at every depth, (b) resolves relative `command:` paths against the workspace root to absolute, (c) drops trailing whitespace in env values, and (d) uses LF line endings. The fingerprint is intentionally **per-user / per-machine**, not cross-platform — that's the point. Two machines that would launch a different binary for the same `command:` value (e.g. `/usr/local/bin/node` vs `/opt/homebrew/bin/node`) deserve to re-consent, because the actual code being executed differs. Trust travels with the user, never with the repository. Within a single machine, equal-meaning configs (key order, JSON whitespace, line-ending variance) hash identically, so a benign reformat doesn't re-trigger the prompt. - **Inherit Workspace Trust** — if the workspace is in Restricted Mode, refuse to spawn any server (stdio *and* SSE). A remote SSE endpoint never executes local code itself, but the tools it exposes can still cause file writes, shell calls, or prompt-injection via the agent, so the trust prompt covers it too. - **Refuse shell wrappers, not just `bash -c`** — `command:` must resolve to an actual executable path; argument vectors must go through `args:`. Reject `command:` values whose basename matches any shell (`sh`, `dash`, `bash`, `zsh`, `ksh`, `fish`, `pwsh`, `cmd`, `cmd.exe`, `powershell`, `powershell.exe`) when paired with an execution flag in `args:`: `-c` / `/c` / `-Command` / `-EncodedCommand`. (PowerShell's `-EncodedCommand` is a common obfuscation vector and gets the same treatment as `-Command`.) The point is to make the executable + argv structurally visible, not to chase shell-specific bypasses. @@ -78,7 +78,8 @@ IAgentRuntime (Agent SDK PR consumes this) - [ ] Aggregates all tool definitions into a single registry - [ ] Reloads on file change without restarting unaffected servers - [ ] Surfaces server connection errors in the status bar -- [ ] Zeus's own MCP **server** half publishes its tools under the `zeus__` prefix (double underscore, matching the third-party namespacing rule below — e.g. `zeus__buffer_read`, `zeus__editor_open`). The double-underscore separator means a third-party server *literally* named `zeus` does not collide with our internal surface +- [ ] Zeus's own MCP **server** half publishes its tools under the `zeus__` prefix (double underscore, e.g. `zeus__buffer_read`, `zeus__editor_open`) +- [ ] **`zeus` is a reserved server name** in `.zeus/mcp.json`. The loader refuses any third-party entry whose `name` (after lowercasing) is `zeus`, with a clear error message pointing at the offending entry. This is what actually prevents collision with the internal `zeus__` namespace — a double-underscore separator alone isn't enough, because a third-party server *literally* named `zeus` would still get mapped to `zeus__` by the namespacing rule and collide - [ ] **All** third-party tools are always exposed as `__` in the aggregated registry, regardless of whether another server has registered the same short name. Always-namespacing (rather than namespacing-on-collision) means the tool name the agent sees is stable: it does not change when a new server is added later. UI surfaces still show the short tool name with the server name as secondary text. The underlying call dispatches to the originally-named tool on the right server. ## Status