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
S3 REFUSED any MCP tool the user hadn't hand-added to levelcode.ai.mcp.toolPolicy.
That was the safe-but-unusable placeholder; S4 replaces it with an actual prompt.
Now a tool that isn't allow-listed shows a card — server · tool · the real
arguments — with Skip / Allow once / Always allow. "Always allow" writes the tool
to the allow-list (Global tier, matching the setting's application scope), so
future runs skip the prompt: the allow-list is still the ONE thing that grants
'allow' (G3). Autopilot does not relax any of this — an MCP tool is third-party
code.
Security properties, all verified end-to-end against the S2 fixture server (five
paths: allow-listed runs silently; un-listed prompts→runs on Allow; un-listed
prompts→NOT run on Skip; destructive prompts even when allow-listed and offers no
"always"; no-webview refuses):
- A destructive tool ALWAYS prompts (classifyMcpTool tightens on it) and the card
hides "Always allow" — offering it would be a button that does nothing, since a
destructive tool can never be allow-listed. canAllowAlways is derived from the
same annotation the classifier reads, so they can't disagree.
- The arguments are shown IN FULL on the card (only length-capped). That is the
decision — the user owns the credentials and needs to see the repo it will
touch, the row it will delete. The card is ephemeral UI, never the transcript;
the debug-log redaction (G4) is unchanged.
- No webview to ask through (headless / tests) → falls back to S3's refusal
rather than running third-party code with no way to say no.
Pieces:
- mcpConfig.describeMcpCall (pure): server/tool from the route with a
namespaced-name fallback, bounded+pretty args (never throws on circular input),
destructive/canAllowAlways. 3 new tests, mutation-checked (making a destructive
tool "always-allowable" fails).
- agent.js router: refuse → prompt via ctx.approve({kind:'mcp',…}).
- extension.js: mcpAllowAlways writes the allow-list (user-scoped read, Global
write, rejects a non-namespaced name).
- chat.html: the kind:'mcp' card. VERIFIED VISUALLY — screenshotted both the
normal and destructive variants: args wrap, buttons are distinct (two sharing
`.approve` was a selector bug, caught and fixed), destructive shows the amber
warning and no "Always allow".
Also: the MCP chips used icon:'plug', which isn't a registered codicon, so
addAgentLine rendered the literal word "plug" in the rail. Swapped to 'sparkle'
(the 🔌 emoji still marks it MCP). The identical latent issue on the auto-preview
'globe' chip (extension.js:701) is #35's, left for a follow-up. And
explainMcpRefusal's wording ("this build has no prompt") was made accurate — it's
now only the non-interactive fallback.
Deferred to S4b: the G1 trust-on-first-use LAUNCH gate for workspace-file
(.levelcode/mcp.json) servers — they're still read-and-listed but never started.
This PR is the per-call gate; the launch gate is its own reviewable slice.
Verified: 24 suites, 0 failures (mcpConfig 44 cases).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0 commit comments