Most agent permission systems are runtime systems. A dialog asks "may I run this command?" and a human — you, on your fourteenth approval of the hour — says yes. That's not a security boundary. It's a habit, and habits erode.
I wanted the opposite shape: decide once, declaratively, and make the wrong decisions unbuildable. On my machine that looks like this. Running pi launches the coding agent with web search, a git identity of "Max Ronner's Coding Agent agents@ronner.dev", and an agent ID baked into its environment — every commit it makes is attributable. Running pi-sandbox launches the same binary inside a bubblewrap container: a fresh tmpfs home directory, the project mounted read-only, PID and IPC namespaces unshared, and its tools whittled down to read, grep, find, and ls. Same agent, different facts about what can exist.
The model underneath is a small permission graph — capabilities grouped into scopes, scopes composed into profiles, profiles bound to clients. It is deliberately boring. The interesting part is what happens when it's wrong. If a scope references a capability that doesn't exist, I don't find out when an agent misbehaves. I find out from the evaluator:
scopeAssertions = map (scopeName: {
assertion = unknownScopeCapabilities scopeName == [ ];
message = "Agent Scope '${scopeName}' references unknown capabilities: "
+ concatStringsSep ", " (unknownScopeCapabilities scopeName);
}) (attrNames scopes);
nixos-rebuild fails, naming the offending capabilities, before a single binary is wrapped. The same check runs at every layer: profiles must reference real scopes, and clients must actually support the profiles they enable. If I try to give Pi the repo-maintainer profile — which can run nix build without asking — the build refuses: Agent Client 'pi' cannot enable Agent Profile 'repo-maintainer' … cannot enforce capabilities: rules.nixChecks. Pi's declaration doesn't list that rule as enforceable, so the combination cannot exist.
My favorite part is the test suite. It contains a deliberately broken configuration — Pi wired to repo-maintainer — and the test passes only if the evaluator rejects it with exactly that error. The repo doesn't just avoid misconfiguration; it proves, on every build, that the misconfiguration is still impossible.
There's a slogan from typed functional programming: make illegal states unrepresentable. We rarely apply it to operations. We treat trust as something you do — auditing, watching, approving — when it could be something the build proves. The Nix evaluator becomes the one party in the system that never gets tired, never auto-approves, never has a deadline. An agent is untrusted code holding your credentials; you can't make it want the right things, but you can make the wrong things unlaunchable.
The whole model is a few hundred lines in my home-manager repo, most of them assertions. That's the point, I think: anything you trust should be boring enough to check completely — and checked before it's allowed to run.