feat(core): enforce the file contract at the tool gate - #239
Merged
Conversation
Wires the path-axis contract from #238 into dispatchToolCall, the one central gate, so all four clients get it without touching a line of client code. Composition is most-restrictive-wins over the existing PermissionVerdict lattice. `no-match` means "no opinion" and never wins, so an absent contract collapses to the tool verdict exactly — the no-op property is exact, not approximate, and the 16-cell table is enumerated in tests rather than sampled. A contract `deny` is checked first and cannot be waived, including by bypassPermissions. It states something standing about a path rather than prompting about one call, so the mode that exists to skip prompts has no business clearing it — otherwise the contract's strongest sentence would be its easiest to disable. Contract `ask` stays ordinary: mode and hooks still apply. RuntimeHost loads the contract itself instead of each client passing one in. Four hosts each remembering an optional argument is the shape AGENTS.md rules out for anything gating tool execution. Also closed while wiring: the plugin capability bridge in apps/server called dispatchToolCall without a contract, so a plugin subprocess would have been a way around path rules. Honest reach, said out loud rather than only in docs: when a contract denies reads while the sandbox is off, the REPL, headless, and `contract show` all warn that the denial covers Read/Grep/Glob and not Bash. Write-only contracts stay quiet — there is no false-enforcement risk there, and a warning nobody needs is one users learn to ignore. Adds `deepcode contract <show|init|check>`; RECOMMENDED_FILE_CONTRACT was unreachable without a way to install it. RuntimeHost.run stays non-async on purpose. Making it async would have turned its existing synchronous missing-cwd throw into a rejection and changed what callers catch; the async work sits behind a synchronous guard instead. An existing test caught that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 8, 2026
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.
Summary
PR 2 of
docs/FLOATBOAT_ADOPTION_PLAN.md§2.A — wires the evaluator from #238 intodispatchToolCall, the one central gate. This is the step the plan flagged as the only one needing careful review.RuntimeHostloads the contract itself, so all four clients (CLI, server, VS Code, LSP) get it without a line of client change.The safety argument
no-matchmeans "no opinion" and never wins. So an absent contract collapses to the tool verdict exactly — the no-op property is exact, not approximate. The 16-cell table is enumerated in tests, not sampled, plus symmetry and a "settings deny survives any contract verdict" property.A contract can only tighten. Adding one cannot reduce existing safety.
A contract
denysurvivesbypassPermissionsThe one deliberate asymmetry, and worth arguing about in review.
A
denystates something standing about a path ("never read.env"); it is not a prompt.bypassPermissionsexists to skip prompts. If it also cleared denials, the contract's strongest sentence would be its easiest to disable. Sodenyis checked before mode and cannot be waived — bybypassPermissions, bydontAsk, or by an explicitallowin settings.Contract
askstays ordinary: mode applies, and aPreToolUsehook is still the last word.Found and closed while wiring
The plugin capability bridge in
apps/server/src/runtime-composition.tscallsdispatchToolCalldirectly and was not passed a contract — a plugin subprocess would have been a way around every path rule. It now loads and passes the same contract, and reports aninvalidcontract as a composition diagnostic.Saying what it actually covers
The plan's stated top risk for this feature is false confidence, so the warning is code, not just prose. When a contract denies reads while the sandbox resolves to
danger-full-access, the REPL, headless runs, andcontract showall print that the denial coversRead/Grep/Globand notBash.Write-only contracts stay silent — there's no false-enforcement risk there, and a warning nobody needs is one users learn to ignore.
New command
RECOMMENDED_FILE_CONTRACTshipped in #238 with no way to install it:initrefuses to clobber an existing contract without--force— silently dropping rules a user wrote is the exact failure this feature exists to prevent.One regression caught by an existing test
Making
RuntimeHost.runasyncturned its synchronous missing-cwdthrowinto a rejection, changing what callers catch.runstays non-async with the await work behind a synchronous guard. Noting it because it's the kind of change that passes review by looking like a no-op.Test plan
pnpm test— 1289 passed, 16 skipped (+65 here: 855 core, 216 cli)pnpm typecheck·pnpm lint·pnpm format:check·pnpm build·node scripts/check-docs.mjsbypassPermissions,dontAsk, and an explicit settingsallowsource: 'contract', and the author'sreasonreaches the userNotebookEditreadsnotebook_pathnotfile_path; Grep/Glob search roots; Bash gets no verdictRuntimeHostwith only acwdblocks a realWrite— proves loading isn't client-dependentaskis still overridable byPreToolUseinitclobber-refusal and invalid-contract reportingDocumentation
docs/file-contract.md— added Getting started, the governed-tools list with Bash called out, and the non-waivable-deny ruleRelease notes label
release-notes:feature— opt-in; no contract file means no behaviour changeRelated
Plan §2.A (PR 2). Builds on #238.
🤖 Generated with Claude Code