feat(core): add the file contract parser and evaluator - #238
Merged
Conversation
Permission rules match on the tool, not the path. The only path-aware match is
a prefix compare against primaryInput(), and a real file_path is usually
absolute — so `Read(.env*)` matches nothing, and "never read .env" is a
sentence settings.json cannot express.
Adds the missing axis: glob × {read, write, execute} × {allow, ask, deny}.
The verdict type is PermissionVerdict, the same lattice tool rules already
produce, so composing the two needs no new vocabulary.
Nothing is wired up yet — this PR is parse and decide only, so the diff that
touches the dispatcher can be read on its own.
Shape:
- Evaluation is pure; loading is a separate module. That split is what makes
the decision table exhaustively testable.
- More specific glob wins (fewer **, then more segments, then more literals),
ties go to the later rule, so narrowing needs no reordering.
- Writes to the contract itself are denied unconditionally. A contract that can
grant itself write access is not a contract.
- Paths outside the workspace get no verdict rather than an invented one.
- A malformed contract reports `invalid`, never `absent`: falling back to "no
contract" would silently drop every deny the author wrote. The parser is
strict for the same reason — a dropped line here is a permission granted.
The glob matcher is hand-written; the repo carries no YAML or glob dependency
and this file has exactly one shape, so a strict small parser beats a permissive
general one.
Documented in docs/file-contract.md, which states plainly that this is policy
and not a security boundary: it constrains dispatcher tool calls, not what a
shell command does after Bash starts. Only the sandbox bounds that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4 tasks
oratis
added a commit
that referenced
this pull request
Aug 8, 2026
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: oratis <happyllammar@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
9 tasks
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 1 of
docs/FLOATBOAT_ADOPTION_PLAN.md§2.A. Parse and decide only — nothing is wired into the dispatcher, so the diff that touches the permission path can be reviewed on its own (that's PR 2).Zero runtime behaviour change: no caller invokes this yet.
The gap it closes
settings.jsonpermission rules match on the tool.Bash(git diff:*)works;Read(.env*)matches nothing, because the only path-aware match is a prefix compare againstprimaryInput()and a realfile_pathis absolute. So "never read.env" is a sentence the permission layer cannot express.The sandbox can express part of it via
sandbox.filesystem.denyRead, but that's inert when unconfigured (resolveSandboxModefalls todanger-full-access), its hardcoded deny list covers home-directory credential stores rather than in-project.env, and it's a flat prefix list with noaskstate.This adds the missing axis: glob × {read, write, execute} × {allow, ask, deny}.
Design decisions worth reviewing
The verdict type is
PermissionVerdict. Selfware'srequire_discussionmaps exactly onto DeepCode's existingask, so there's no new lattice and no new vocabulary — composition in PR 2 is a min over a lattice that already exists.Pure evaluation, separate loader.
file-contract.tshas nonode:fs;file-contract-loader.tsdoes the I/O. That split is what makes the decision table exhaustively testable.A malformed contract is
invalid, neverabsent. Both obvious alternatives are wrong: falling back to "no contract" silently drops everydenythe author wrote, and hard-failing turns a typo into a broken install. The parser is strict for the same reason — unknown keys, bad decision values, a rule with noglob, and a rule that decides nothing are all errors, because a silently-ignored line in this file is a permission quietly granted.Hand-written glob + YAML subset. The repo carries neither dependency by policy (see
skills/frontmatter.ts), and this file has exactly one shape.Honest limitations, stated in the docs
docs/file-contract.mdleads with a callout that this is policy, not a security boundary:cat .envis a string; statically analysing shell would be guesswork that reads as a guarantee. Only the sandbox bounds Bash.realpath. Path normalization is string math, so a symlink inside the workspace pointing out still looks inside.Test plan
pnpm test— 1224 passed, 16 skipped (+49 in this PR)pnpm typecheck·pnpm lint·pnpm format:check·pnpm build·node scripts/check-docs.mjsCoverage includes the adversarial cases AGENTS.md requires for permission changes:
a/**/bmatchinga/b, braces,?, regex metacharacters treated as literals (or**/.env*would matchaxenv)write: allowon the contract → stilldeny; broad**write: allow→ stilldeny; reading it stays allowed../escape → no verdict;src/../.envre-entry → rule still applies;/work/repo-evil/.envdoes not pass as inside/work/repoDocumentation
docs/file-contract.md— format, glob syntax, precedence, self-protection, failure behaviour, and the composition rule PR 2 will implementRelease notes label
release-notes:internal— no user-visible behaviour until PR 2 wires it inChecklist
Related
Plan §2.A. Research:
docs/research/floatboat.md§4.4(1).🤖 Generated with Claude Code