fix(tool_guard): gate unknown/external WRITE tools by side-effect (close MCP bypass) - #3
Merged
Merged
Conversation
…ose MCP bypass) gate() short-circuited any tool name outside a hardcoded 6-set to AUTO, so an MCP write tool (e.g. gmail.send_email) reaching the approval gate was auto-approved — the unknown-WRITE / tool-poisoning bypass (CVE-2025-54136 class). This is the keystone for MCP integration. confirmation_callback now passes spec.side_effect + requires_confirmation into gate(); gate() decides by side-effect first: - name-keyed native tools keep their curated policy (shell danger-matched; the always-confirm writers gate every call) - requires_confirmation=True always gates (native or not) - side_effect=write AND not a native tool (external/MCP) gates by default - native low-risk writes (reminders/todos/notes/voice) stay AUTO via a set_native_tools() snapshot taken at startup before any connect_mcp() — fixes the over-gating regression an adversarial review caught (all 8 in-repo writes would have modal-spammed in CONFIRM mode, with no allowlist escape) - malformed spec fails closed (treated as write) Adjacent bugs the review surfaced (latent on main): - _ALWAYS_CONFIRM had a dead "create_session" entry; the real tool is create_claude_session, so iTerm session-creation was never actually gated. Fixed the name + the _primary_text extractor. - enroll_face declared neither side_effect nor requires_confirmation, so it never reached the gate despite docs/CLAUDE.md claiming it is gated. Added side_effect="write". Verified against the real Arcana 1.0 contract: gateway._confirm_execution only invokes the callback for WRITE/requires_confirmation tools; MCP classifies side-effect via a name/desc keyword heuristic (default READ) — residual noted in CLAUDE.md (a write tool whose name dodges the keywords classifies READ and never reaches the gate; Arcana-level, fix later). 528 pytest green (+15), incl. an integration test that dispatches a WRITE tool through the real gateway to pin Arcana's routing contract. Reviewed by a 3-lens adversarial workflow (security-bypass / regression / correctness); the over-gating regression and adjacent bugs are fixed here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Phase 0 keystone for the MCP integration roadmap.
tool_guard.gate()short-circuited any tool name outside a hardcoded 6-set toDecision.AUTO, so an MCP write tool (e.g.gmail.send_email) reaching the approval gate was auto-approved — the unknown-WRITE / tool-poisoning bypass (CVE-2025-54136 class). This change makes gating side-effect-first instead of name-allowlisted, which:How it works now
confirmation_callbackpassesspec.side_effect+spec.requires_confirmationintogate(), which decides by precedence:shell(danger-matched: only dangerous commands gate), and the always-confirm writerssend_to_session/create_claude_session/enroll_face/write_file/edit_filerequires_confirmation=True→ always gated (native or not)side_effect=writeAND not a native tool (external/MCP) → gated by defaultset_native_tools()snapshot taken at startup (from the runtime's tool registry, before anyconnect_mcp())Adversarial review → fixes folded in
A 3-lens adversarial workflow (security-bypass / regression / correctness) reviewed the first cut. Security verdict was sound, but it caught a real MEDIUM regression + adjacent latent bugs, all fixed here:
schedule_reminder,add_todo,switch_tts_voice,update_self, …) in CONFIRM mode, with no allowlist escape. → fixed via the native-vs-external trust boundary (set_native_tools()); native writes stay AUTO, only unknown/external writes gate.create_sessionwas a dead_ALWAYS_CONFIRMentry — the real tool iscreate_claude_session, so iTerm session-creation was never actually gated. Fixed the name +_primary_textextractor.enroll_facedeclared neitherside_effectnorrequires_confirmation, so it never reached the gate despite docs claiming it's gated. Addedside_effect="write".Verification
Verified against the real Arcana 1.0 source:
gateway._confirm_executiononly invokesconfirmation_callbackfor WRITE/requires_confirmation tools; MCP classifies side-effect via a name/desc keyword heuristic (default READ).Residual (Arcana-level, noted in CLAUDE.md): a write tool whose name dodges the keyword heuristic classifies READ and never reaches the gate — fix later by registering MCP servers with explicit side-effects or the HEAD guardrail API.
uv run pytest -q→ 528 passed (+15), including an integration test that dispatches a WRITE tool through the real gateway to pin Arcana's routing contract (so a future Arcana bump that changes the contract fails loudly).Follow-up (deferred to Phase 1, when MCP lands)
The modal currently titles external-write confirmations as "危险命令确认" (dangerous-command). No external/MCP tools exist yet, so this never fires today — but when MCP lands, split
danger_reason(real danger only) from anapproval_reasonso benign external writes don't read as "dangerous" (alarm-fatigue). LOW.