Skip to content

fix(tool_guard): gate unknown/external WRITE tools by side-effect (close MCP bypass) - #3

Merged
tyxben merged 1 commit into
mainfrom
feat/gate-by-side-effect
Jun 13, 2026
Merged

fix(tool_guard): gate unknown/external WRITE tools by side-effect (close MCP bypass)#3
tyxben merged 1 commit into
mainfrom
feat/gate-by-side-effect

Conversation

@tyxben

@tyxben tyxben commented Jun 13, 2026

Copy link
Copy Markdown
Owner

What & why

Phase 0 keystone for the MCP integration roadmap. tool_guard.gate() short-circuited any tool name outside a hardcoded 6-set to Decision.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:

  • unblocks MCP (Gmail/Calendar/messageinfra) — the agent can consume external write tools safely
  • closes the documented unknown-WRITE-bypass class
  • is the prerequisite for Programmatic Tool Calling

How it works now

confirmation_callback passes spec.side_effect + spec.requires_confirmation into gate(), which decides by precedence:

  1. name-keyed native tools keep their curated policy — shell (danger-matched: only dangerous commands gate), and the always-confirm writers send_to_session / create_claude_session / enroll_face / write_file / edit_file
  2. requires_confirmation=True → always gated (native or not)
  3. side_effect=write AND not a native tool (external/MCP) → gated by default
  4. native low-risk writes (reminders/todos/notes/voice) → AUTO, via a set_native_tools() snapshot taken at startup (from the runtime's tool registry, before any connect_mcp())
  5. else (unknown READ/none, unflagged) → AUTO
  6. malformed spec → fails closed (treated as write)

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:

  • Over-gating regression: gating all writes would have modal-spammed every benign in-repo write (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_session was a dead _ALWAYS_CONFIRM entry — the real tool is create_claude_session, so iTerm session-creation was never actually gated. Fixed the name + _primary_text extractor.
  • enroll_face declared neither side_effect nor requires_confirmation, so it never reached the gate despite docs claiming it's gated. Added side_effect="write".

Verification

Verified against the real Arcana 1.0 source: gateway._confirm_execution only invokes confirmation_callback for 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 -q528 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 an approval_reason so benign external writes don't read as "dangerous" (alarm-fatigue). LOW.

…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.
@tyxben
tyxben merged commit 2f86857 into main Jun 13, 2026
1 check passed
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.

1 participant