Fail-Closed Authorization for Tool-Using AI Agents: A Capability Contract and Case Study
Andrey Lesnikov · Independent Researcher
Preprint — not peer-reviewed. Author abstract on this page; full-text PDF under CC BY 4.0.
Abstract
Abstract
For tool-using LLM agents the decisive question is whether a concrete tool call is authorized before it executes. We describe the Agent Security Contract (ASC), a deny-by-default, pre-execution gate for agent and MCP tool calls. An ASC policy pins a SHA-256 digest of the approved tool catalog, declares per-agent tool allowlists with argument constraints, marks high-impact tools that require out-of-band approval, and bounds call rates; each decision (ALLOW, BLOCK, REQUIRE_APPROVAL) is emitted as an integrity-checked (unkeyed) JSON evidence envelope, and every error path we enumerate and test resolves to BLOCK. We evaluate the open-source implementation on static, recorded tool-call traces only; no live agent is run. Bypasses of the v0.1.0 demo policy found in review were fixed in v0.2.0; on a 60-case adversarial probe, allowed attacks fall from 32 to 1 (a policy gap), at the cost of 3 new benign over-blocks. On the author-constructed 66-scenario v0.2.1 corpus, designed around these checks, ASC returns the expected verdict in 66/66 cases; tool-name RBAC (Python and OPA/Rego) admits 30 of its 33 attacks by construction, and mutation testing shows the corpus exercises few policy constraints (8 of 62 single-constraint mutants killed). As a held-out check we replay 132 AgentDojo v1.2 ground-truth traces under policies frozen beforehand. With tool-level approval policies, 25 of 26 non-empty attack traces but also 60 of 97 benign tasks are held for approval. v0.2.0’s strict-argument rule makes these unadapted policies block 94 of 97 benign tasks. Policies derived mechanically from AgentDojo’s tool signatures first hit v0.2.0’s lack of optional arguments (31 benign blocks); with v0.2.1 they reproduce the tool-level verdicts on all 132 traces, with no blocks and no newly admitted attack, so type-only declarations add no protection here. Median in-process latency is about 38 µs per trace.