Repository navigation
feat(host-core): decide contract-mode admission by effect - #1720
chengzhi-c wants to merge 7 commits into
Conversation
…g safe ones Plan and Goal decided what they allowed by naming tools. That list answered the wrong question: it asked where a tool came from rather than what the call would do, so a read-only MCP search was refused for being an MCP tool while the mode prompt — guidance, not a boundary — told the model not to mutate anything. The gate now classifies by effect. Read-only builtins reach the ordinary permission path; a builtin outside that set is denied outright, so a tool added later is denied until someone classifies it. Bash and an unclassifiable MCP tool take a permission card in every permission mode, because neither carries a signal the host can read: what a command does is decided by the shell that runs it, and an MCP tool's name means nothing while its server's own risk annotation is not evidence. A plugin tool is admitted when it declares a non-empty planSafeActions list, which the plugin runtime also enforces, and refused otherwise — an approval the callee would reject is not a gate. A trusted-extension tool is withheld in a contract mode. It executes in the sidecar, so no host call is made for it and the permission path cannot see it. The runtime's mode filter moves to the same shape so the catalog and the host agree, and a refused card now reports an ordinary denial rather than a mode denial.
Plan and Goal gate Bash and user MCP tools behind an approval card, but the card came back on every call: the gate answered who decides and left the repeated question in place. An exact-tool `allow-session` grant now suppresses later cards for that tool in that session, which is what the card is for, and it does not carry to another tool or another session. The same change closes the gap the ADR left open for plugin-contributed MCP tools. That registration path carries no enforced plan-safe action list, so the host could only take the caller's word for it. The execution callback now refuses Plan and Goal itself, before the server is called, instead of trusting admission metadata the caller supplies. MCP tools stay discoverable through ToolSearch rather than being declared core, so a server's schemas do not enter every contract-mode request. The catalog and the execution guard keep one predicate for what a contract mode permits, and the extension and retained-reference denials are covered by tests that fail without the guard. closes vastsa#1112
…owlist Plan and Goal classified builtins from a read-only allowlist, so a name nobody classified was refused and a read-only local tool was refused with it. Ask the question from the effect side instead: the contract modes deny the builtins whose purpose is to change files, session state, or spawned work, and every other name reaches the ordinary permission path, which still applies risk, the permission mode, session grants, and the external-path rule. Bash and user MCP tools keep taking a card in every mode. The list exists in Rust and TypeScript, so a test now fails when the two copies disagree. The spec, ADR 0330, the decision log, the RPC authorization gate, and the plan E2E scenario record the new posture.
`context-compaction.test.mjs` asserted that `new_context` appears in `crates/host-core/src/permissions.rs`. A contract mode keeps a deny list instead of a read-only allowlist, so the name is no longer listed there. The guard now asserts the name stays off that list, which is the same wiring requirement ADR 0064 states.
|
Read the core gate in an isolated worktree (head The direction is right — deciding by effect beats the old name allowlist, the cross-language mirror test is the correct guard, and the three-tier 1. 2. The Minor: the plugin carve-out still keys off |
…-goal Only docs/adr/README.md conflicted. Main merged its own ADR 0330 while this branch was open, so the contract-modes ADR moves to 0331 and every reference to it follows; the index keeps both rows.
Review asked for the consequence of replacing the read-only allowlist with a deny list to be visible where the posture is defined: the gate admits every name it does not deny, so a builtin nobody classified reaches the ordinary permission path. ADR 0331 and the security spec now say so, name the builtins admitted by omission, and put network access in the deny-list criterion so a future egress builtin is classified instead of inherited. The host comment on the plugin carve-out points at the plugin-runtime execute path that enforces the declared actions.
Plan and Goal decided tool admission from a read-only allowlist: a builtin that was not on the list was refused until someone classified it, so a read-only local tool was refused with it and every new builtin became a contract-mode regression. The gate now answers by effect. It refuses the builtins that act on state: the ones that write files or session state (
Write,Edit,TodoWrite,GenerateImages,PluginScaffold,PluginPack,ScheduledTask*), the delegation family that spawns and controls work (Task*), andJevClassify. Every other name is admitted by the gate;Read,Glob,Grep,BrowserPreview, and an unclassified builtin then fall through to the ordinary permission path, which still applies risk, the effective permission mode, session grants, and the external-path rule.Bashand user MCP tools keep taking a permission card in every mode,autoincluded. A plugin tool still needs a non-emptyplanSafeActionslist, a plugin-contributed MCP tool is still refused in both modes, and a trusted-extension tool stays unavailable. What a contract mode offers the model is unchanged; this changes the gate's answer, not the catalog.crates/host-core/src/permissions.rsandpackages/agent-runtime/src/mode-tool-access.tshold the same names, andmode-tool-access.test.tsfails when the two copies drift apart.A state-changing builtin added later is admitted until someone classifies it, because the gate no longer refuses unknown names. ADR 0330 records that consequence: deciding what a new builtin does to state is part of adding it.
Checks:
cargo test -p host-core -- permissions::22 passed;-- scheduled_tools:: todos::26 passednode scripts/e2e-plan.mjs14 passed, 0 failed, 3 skippedcargo test -p host-corefull 889 passed, 38 failed: the same shell-spawn (os error 5) and git-worktree failures the pristine base shows on this machine, and the two RPC authorization tests fail only where they execute an approved Bash commandcargo clippy -p host-core --all-targets,cargo fmt --check,biome lint,scripts/check-architecture.mjs,check:pr-basecleanSpecs, ADR 0330, and the decision log carry the new posture.
Plan with Auto still asks before Bash; the card is the decision, not the mode.