You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(mcp): MCP settings are user-authored only — close the workspace-settings RCE
PR #31 review (Copilot), and it is the serious one: RCE on clone-and-open.
The trust model is "user-authored = trusted, repo-authored = untrusted." I had it
HALF right — .levelcode/mcp.json is gated (source:'workspace', never started) —
but I missed that VS Code SETTINGS themselves have a repo-authored tier. A
committed .vscode/settings.json (or a folder entry in a .code-workspace) can set
levelcode.ai.mcp.servers, and cfg.get() returns that merged effective value. S3
treats everything from the settings read as source:'settings' and auto-starts it.
So a hostile repo could ship .vscode/settings.json naming `sh -c "curl … | sh"`
as an MCP server and have it spawn the moment the folder is opened — exactly the
RCE the whole model exists to prevent.
Fixed two ways, deliberately redundant for a spawn-on-open surface:
1. package.json — both settings are now "scope": "application", so VS Code drops
any workspace/folder value and greys them out in the workspace settings UI.
This is the idiomatic mechanism and covers the reviewer's package.json comment
(both lines).
2. extension.js — reads them via cfg.inspect() and takes ONLY the global (user)
tier, never workspaceValue/workspaceFolderValue. The spawn decision is too
dangerous to rest on a declarative manifest guard alone; this enforces the same
boundary in code, at the point of use, so it survives a future scope regression.
The trust logic is a pure helper (userScopedSetting) in mcpConfig so it is
unit-testable off the editor — the manifest scope is not.
Verified:
- 38 tests (1 new). It simulates a repo injecting a server via the workspaceValue
tier and asserts it is NOT honoured, that a real user globalValue IS, and that
we read one tier rather than merging.
- Mutation-checked: making the helper fall back to workspaceValue (the bug) fails
the suite.
- Confirmed the ONLY VS Code read of these keys is the user-scoped inspect() call;
agent.js reads the already-scoped value handed to it. The extension.js glue
(inspect() → userScopedSetting) requires vscode, so it is covered by that unit
test of the helper plus reading, not CI.
- Full gate: 18 suites, 0 failures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: extensions/levelcode-ai/package.json
+4-2Lines changed: 4 additions & 2 deletions
Original file line number
Diff line number
Diff line change
@@ -434,12 +434,14 @@
434
434
"levelcode.ai.mcp.servers": {
435
435
"type": "object",
436
436
"default": {},
437
-
"markdownDescription": "MCP servers the agent may use, as `{ \"name\": { \"command\": \"…\", \"args\": […], \"env\": {…} } }` (a `mcpServers` wrapper is also accepted). Each entry names **a process LevelCode will run with your privileges**, so add only servers you trust.\n\nExample:\n```json\n{ \"filesystem\": { \"command\": \"npx\", \"args\": [\"-y\", \"@modelcontextprotocol/server-filesystem\", \"/path\"] } }\n```\n\nServers defined in a repo's `.levelcode/mcp.json` are read and listed but **never started** — repo-authored config is untrusted, and its approval step ships in a later release."
437
+
"scope": "application",
438
+
"markdownDescription": "MCP servers the agent may use, as `{ \"name\": { \"command\": \"…\", \"args\": […], \"env\": {…} } }` (a `mcpServers` wrapper is also accepted). Each entry names **a process LevelCode will run with your privileges**, so add only servers you trust.\n\nExample:\n```json\n{ \"filesystem\": { \"command\": \"npx\", \"args\": [\"-y\", \"@modelcontextprotocol/server-filesystem\", \"/path\"] } }\n```\n\n**User-settings only** (application-scoped): a repo's committed `.vscode/settings.json` cannot set this, so opening an untrusted repo can never make LevelCode spawn a process. Servers defined in a repo's `.levelcode/mcp.json` are likewise read and listed but **never started** — its approval step ships in a later release."
438
439
},
439
440
"levelcode.ai.mcp.toolPolicy": {
440
441
"type": "object",
441
442
"default": {},
442
-
"markdownDescription": "Which MCP tools may run, as `{ \"server__tool\": \"allow\" | \"ask\" }` — use `\"*\": \"allow\"` for all of them. Tool names are namespaced `server__tool` exactly as they appear in the chat.\n\nAnything not allow-listed is **refused**, including under Autopilot: an MCP tool is third-party code, so Autopilot deliberately does not relax this. A server's own `destructiveHint` overrides an `allow` here — server hints may only ever tighten, never loosen."
443
+
"scope": "application",
444
+
"markdownDescription": "Which MCP tools may run, as `{ \"server__tool\": \"allow\" | \"ask\" }` — use `\"*\": \"allow\"` for all of them. Tool names are namespaced `server__tool` exactly as they appear in the chat.\n\n**User-settings only** (application-scoped): a repo cannot set this to auto-allow its own tools. Anything not allow-listed is **refused**, including under Autopilot: an MCP tool is third-party code, so Autopilot deliberately does not relax this. A server's own `destructiveHint` overrides an `allow` here — server hints may only ever tighten, never loosen."
0 commit comments