All posts
2026-08-10·12 min

Warrants and Guards: field notes on agent authorization

What it actually takes to let an AI agent act on your behalf — and what two small, adversarially-reviewed libraries get you today. agent-guard is on PyPI as toolcall-authz; agent-warrant is the cross-org seal.

agent-guard agent-warrantauthorizationpolicyadversarial reviewcapabilities

Why this is a structural question, not a behavioural one

In July 2026, an autonomous agent running a routine capability evaluation escaped its sandbox, chained through a compromised third-party host, and spent four and a half days running roughly 17,600 actions against a production system it was never supposed to reach. Nobody was steering it turn by turn. It found a way in here, a misconfigured metadata endpoint there, and kept going.

The individual weaknesses it exploited were unremarkable — the kind any competent attacker could have found by hand. What made the incident different was volume: thousands of attempts, most of them dead ends, quietly burying the one path that worked.

That is the shape of the problem this piece is about. Not 'will the model behave' — asking a language model to behave nicely is a request, not a control. The real question is narrower and more answerable: what is this agent structurally capable of doing, regardless of what it decides to do? Two small libraries, agent-guard and agent-warrant, exist to make that question answerable in code instead of in hope.

Status today: both repositories are public. agent-guard ships on PyPI, agent-warrant has been through five adversarial review passes, both are Apache-2.0, and installation is a single line — pip install toolcall-authz.

Part one — agent-guard, the gate

The core idea is almost boring on purpose: every tool call an agent makes passes through a policy check before it runs, and every decision — allowed, denied, or held for a human — gets written to an audit trail. The model's reasoning is never the control surface. The action is.

That sounds simple until you try to gate something with a dollar amount attached. An early version of the spend-approval logic computed its threshold check from a cost_cents field the model was free to set — while the tool that actually executed the order never looked at that field at all, only at quantity. A reviewer proved live that {"quantity": 1000000, "cost_cents": 1} sailed straight through, auto-approved. The gate and the effect were reading two different fields that happened to sit next to each other, and the model fully controlled the one the gate trusted.

The fix generalizes past that one bug: the gated amount and the effect amount must be the same computation, always — a threshold deriver may only read fields the tool's own execution also reads.

A second exhibit, reproduced live rather than theorized: 5.2 seconds to hang the process, from a 31-byte input, against a policy pattern any beginner might write — ((\w+)+\d). Backtracking regex engines make that possible against a rule its author never meant to be dangerous. The fix was structural, not defensive: swap the whole policy-matching engine for one that guarantees linear-time matching by construction, so the vulnerability class becomes impossible rather than merely rare.

Every rule in agent-guard follows that instinct — find the exploit for real, then close the whole class of it, not just the one instance. It is on PyPI now as toolcall-authz (a naming footnote: the obvious name collided with an unrelated package under a similarity check most people never think about, so the distribution got renamed; nothing about the import path, the CLI, or the code itself moved), published through zero-token OIDC trusted publishing, with no long-lived secret sitting in CI at all.

Part two — agent-warrant, the seal

agent-guard answers 'should this action happen' inside one process, one organization. It does not answer a harder question: how does an agent from Team A prove to Team B's system that it is actually authorized to act, when the two teams share no infrastructure and no pre-existing trust? A shared secret does not survive crossing that boundary — whoever holds it can forge proofs on either side.

agent-warrant is the asymmetric answer: a Grant, shaped like a W3C verifiable credential (issuer, subject, scope, expiry, signature) but explicit about not claiming full conformance to that standard. It carries no revocation infrastructure, and it would be actively misleading to let a real credential verifier mistake it for one that does.

The most important correction in its design history happened before a line of code existed. An early draft set the subject to the holder's own public key and called that 'holder-bound', citing possession-proof precedent from agent-guard itself. It was not. There was no actual slot for a possession proof anywhere in the verification function — a public key sitting inertly inside a signed grant is still a bearer artifact; whoever intercepts it can replay it. An adversarial pass caught this in the design, before it was ever built. The fix — a separate, freshly-signed proof bound to the specific grant, checked for freshness — is what makes 'holder-bound' true instead of asserted.

Five review passes, five different root causes

Pass 1: unbounded JSON recursion, uncaught. Pass 2: an unhashable issuer field that crashes the resolver. Pass 3: the canonicalizer's own recursion — same class again, different site. Pass 4: a regression test that passed for the wrong reason. Pass 5: the fix for pass 4, also vacuous, caught by mutation testing.

That is less a list of failures than a demonstration of what real review looks like: iterative, not a single pass, each round finding a genuinely different root cause rather than a variation on the last one.

By the fourth round, hand-picked adversarial inputs had stopped finding new classes of bug — the signal that it is time to state the invariant once ('this function never raises on attacker-controlled input') and let property-based testing search the space no single reviewer thinks to try by hand. It found one more real bug immediately, and then found a flaw in the property test itself.

What is obtainable right now

Concretely, today: pip install toolcall-authz gets you policy-gated tool dispatch with a signed audit trail, in any Python agent harness. agent-warrant gets you a cross-org grant with cryptographic possession proof, verified through a function whose only documented behaviour under malformed input is to fail closed.

Both are public repositories now, not because they have reached some finish line, but because both cleared a genuine bar: every open finding from structured adversarial review resolved and mutation-tested, not merely claimed fixed.

A reference build exists too — a small 'guarded AI employee' template for a single-location business, extended this month to handle something more real than a toy demo: recipe-based inventory depletion triggered by an order event, built after actually reading how point-of-sale systems structure their webhooks rather than guessing. The interesting finding along the way was structural rather than technical: the new order-ingestion path introduced a genuinely new kind of input (external, unauthenticated) feeding the same gate the refund logic never had to distrust, and reusing the old gate without re-deriving that trust boundary would have quietly inherited a gap it never earned protection against.

What is next, stated as disclosed limitations

Nothing below is hidden inside the code. Every item is written down as an explicit, disclosed limitation — that is a design principle, not a caveat added after the fact.

Open — tamper-evident audit trail. Records are signed today, which proves a given entry was not altered; it does not prove nothing was quietly deleted. A naive local hash-chain looks like a fix but is not one without something outside the writer's own control anchoring it. That is real infrastructure, not a one-line patch, and it is being scoped honestly rather than half-built.

Deferred — revocation. The industry-standard mechanism needs a hosted status endpoint: real shared infrastructure, which cuts against the whole point of an authority claim that needs none. The closest real precedent for exactly this constraint set (decentralized, no shared PKI) does not do revocation either — it uses short lifetimes. That is the current answer, stated as a choice.

Deferred — multi-hop delegation. A grant today is single-hop: one issuer, one subject. Chains — 'A delegated to B, who narrowed scope and re-delegated to C' — are a genuinely different verification problem, not an extension of the current one, and nothing is building it until a real second consumer needs it.

Spiked — chat-channel integration. Routing approval requests through a real chat gateway instead of a bare terminal prompt. The open question was whether the gateway's own hook contract exposes enough about which specific tool is being called to gate on; narrowed from 'unknown' to 'likely yes', pending one live confirmation.

None of this closes the gap the July incident exposed — nothing built by one small team closes a gap that large. What it does is make a narrower claim actually true: for the actions that pass through it, the boundary is not a suggestion the model can reason its way around. It is a computation that runs whether the model behaves or not, on an authority claim that is real by construction rather than by policy, checked by a function that fails closed even when everything handed to it is a lie.