Skip to content
← All projects
2026Sole engineer — design, Rust implementation, tests

Grit — a policy gateway for MCP tool calls

Grit sits between an MCP client and the tools it calls. Policy is keyed by (server, tool). Path arguments are fully resolved — following symlinks — and checked component-wise against allowed and denied roots. A workspace snapshot is taken before any tool that may mutate runs, so a bad call can be undone from what the filesystem actually shows. Credentials are AES-256-GCM encrypted at rest and decrypted only for the tool that declared them. Every decision lands in an append-only audit log.

RustMCPPolicy isolationPath containmentAES-256-GCMRollback
49 testsacross policy, containment, snapshot-and-rollback, and a secret store that fails closed

Architecture

  1. 1Authorise — look up the (server, tool) policy; undeclared means denied
  2. 2Contain — resolve every path argument, check it against allowed and denied roots
  3. 3Snapshot — capture the workspace if the tool may mutate
  4. 4The host runs the tool — not Grit's business
  5. 5Complete — size cap, injection scan, rollback on refusal, audit either way

The problem

An agent talked into passing a bad path, a tool that writes more than it claimed, an injected instruction that redirects a file operation — these are the realistic failures of tool-using agents, and none of them are caught by asking the agent to behave. They need a layer that decides before the call and measures after it, from outside the agent.

Approach & decisions

  • A two-phase API — authorise before, complete after — because the interesting moment belongs to somebody else: Grit never executes the tool, the host does. A single function would have to snapshot too late or never roll back.
  • Paths are resolved first, then compared. Checking the string the tool was handed, rather than where it actually points, is exactly how a symlink walks a permitted call out of its root.
  • mutates defaults to true because the cost of being wrong is asymmetric: a read marked mutating wastes a snapshot; a write marked read-only means no rollback exists when it matters.
  • Snapshots are bounded — 5,000 files or 64 MB — and exceeding the bound is an error, never a partial capture, because a rollback from half a snapshot would appear to succeed.
  • In the secret store, a secret's name is authenticated as associated data under GCM, so swapping two ciphertexts in the store file fails closed instead of quietly handing a tool the production token it was not meant to have. Cryptographic failures return one opaque error: distinguishing a bad key from a bad tag is how oracles get built.
  • A real race surfaced only once CI was actually executing the suite: one test mutated the process-wide master-key variable while others were decrypting. Fixed by serialising those tests behind a mutex, and reproduced clean over eight consecutive runs before the fix was called done.

Results

  • 49 tests across policy, containment, snapshot-and-rollback and the crypto store, green in CI with secret and dependency scanning on every push.
  • Rollback is measured from the filesystem, not from anything the tool says about itself, so it holds regardless of how well-behaved the tool turns out to be.
  • The injection heuristics are documented as a signal, not a boundary: pattern-matching on prose is evaded by rephrasing. The containment is the policy layer; the patterns raise an alarm.

What this honestly is not

  • Not kernel sandboxing. No namespaces, no seccomp, no cgroups — a tool Grit permits could ignore the path it was handed and open something else directly. It checks arguments, not syscalls, and is built to sit inside a real sandbox rather than replace one.
  • Not network policy. allow_network is declared and recorded, not enforced.

Stack

Rustaes-gcmsha2walkdirzeroizeserdetracinggitleaks · osv-scanner · cargo-audit in CI