refactor(approval): make the gate unconditional for mutating tools - #33
Merged
Merged
Conversation
The template shipped a "gate on/off" switch that disabled human approval for every mutating tool. Cameron's first design rule is that it never moves money without an explicit human decision, so a control that turns that off contradicts the product. Removed from every surface a request can reach: the toolbar toggle, the `approveAllTools` context state, the query param, its OpenAPI schema entry, and the field on `MessageOptions`. The route no longer reads such a param, and agentService no longer forwards one to the agent factory. The bypass survives for exactly one caller. The eval harness drives the factory in-process and has no human to answer an interrupt, so without it every mutating case would hang. It is renamed `bypassApprovalForEval` so nothing mistakes it for a product setting, and it is unreachable over HTTP by construction. `approvalGate.test.ts` pins that boundary at each layer. The failure it guards against is silent — a bypass wired back into the route would still typecheck, still stream, and quietly write to a real ledger — so the assertions are source-level, like capabilities.test.ts. Verified against the dev stack: a request sending `approveAllTools=true` explicitly still paused at the gate and wrote nothing, and the ordinary approve path still committed the row. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
The template shipped a gate on / gate off switch in the composer toolbar that disabled human approval for every mutating tool. Cameron's first design rule is that it never moves money without an explicit human decision — a control that turns that off contradicts the product it belongs to.
Removed from everything a request can reach
MessageInput.tsx)approveAllToolsstate inUISettingsContextapproveAllToolsquery param — the route no longer reads itMessageOptions, the request-shaped typeagentService.tsThe middleware is now installed for anything reachable over HTTP, unconditionally.
One caller keeps the bypass
The eval harness (
eval/run.mts) drives the agent factory in-process and has no human to answer an interrupt — without a bypass, every mutating eval case would hang. It passesbypassApprovalForEval: !testCase.approvaldirectly togetAgent().Renamed from
approveAllToolsso nothing mistakes it for a product setting, and documented onAgentConfigOptionsas harness-only. It is unreachable over HTTP by construction: nothing in the route, the wire protocol, or the UI can set it.Approval-gate eval cases still leave it unset — that's the behavior they grade.
Why there's a test for this
approvalGate.test.tspins the boundary at each layer. The failure it guards against is silent: a bypass wired back into the route would still typecheck, still stream, and quietly write to a real ledger. The assertions are source-level, in the same spirit ascapabilities.test.ts— what needs pinning is that a future edit doesn't reintroduce the path, which no runtime assertion observes.I verified the tests actually fail by reintroducing the param in the route: the corresponding test failed, the other seven passed.
Verification
tsc --noEmitclean on both the root and eval trees, production build succeeds, prettier clean.approveAllTools=trueexplicitly still paused at the gate (AI:PENDING), and the DB was unchanged — 12 transactions before and after, zero rows at the test amount.allowTool=allowran the tool and committed the row (12 → 13).mydb_dev:5544, port 3101). Production untouched; nothing deleted.Note on existing users
A stale
approveAllTools: truemay remain in some browsers'localStorageunderagent_model_settings. Nothing reads it any more, so it is inert — no migration needed, and it cannot re-enable a bypass.🤖 Generated with Claude Code