Agentic toolkit

A cost and safety governor for the Claude API

agent-governor sits between agents and the Claude API. Agents keep their SDK and change one line; the real key stays in the governor. Each agent gets a policy: which models, which tools, how many dollars a day, how many requests a minute, and what is redacted from the data it sends. Every request is a span, an audit line and a row in a budget ledger that survives restarts.

Read the source and decision record, or the FleetAlert write-up, whose cost guard this generalises.

Between the agents and the key

FleetAlert agentgovernor key · deny execute_fixCI summarisergovernor key · Haiku only, no toolsOperatorgovernor key · wide, still meteredPOST /v1/messagesagent-governor — a reverse proxy speaking the Messages API1 governor key → principal and its policy (constant-time digest compare)2 model allowlist · max_tokens cap · denied tools stripped, or the call rejected3 PII and secrets redacted from tool results before anything leaves4 request rate · daily and monthly USD, checked on the worst case this call could cost5 forward with the one real key · streams pass through byte for bytethe one real keysettle on real usageClaude APIapi.anthropic.comLedger — SQLitepriced per model · survives restartsOpenTelemetry span per requestgen_ai.* and governor.* attributesOTLP when configured, free otherwise/metrics · /budgets · audit logJSON or Prometheus · remaining spendone JSON line per request, with trace idRefusals: 429 rate_limit_error with Retry-After · 403 permission_error · 401 authentication_errorThe API’s own envelope, so the SDK raises RateLimitError, PermissionDeniedError, AuthenticationError.

The numbered steps happen on every request, in that order. The ledger and the Claude API are the only things the governor talks to; everything in the dashed boxes is what it emits about itself.

The five-minute version

01

Agents change one line; the real key moves out of them

The governor speaks the Messages API. An agent sets its base URL to the governor and its API key to a governor key, or changes nothing but two environment variables. The one real Anthropic key lives in the governor's process. A leaked agent key is revoked by deleting a line of policy, and its blast radius was its budget. Keys are compared by SHA-256 digest in constant time. FleetAlert had built a daily cost guard into its own code; this is that guard made shareable.

02

Budgets checked before the call, settled after

Before forwarding, the governor computes the worst case this call could cost: the request's max_tokens at the model's output price plus a rough input estimate. If today's spend plus that exceeds the daily cap, or the month's exceeds the monthly cap, the call is refused with a 429 and a Retry-After to the next UTC midnight. After the response, the real usage block is priced per model, cache reads and writes included, and written to a SQLite ledger, so a restart does not reset a cap. A stream that produced no parsable usage is charged the worst case, not nothing.

03

Tool policy at the request edge

FleetAlert keeps its executes-action capability unreachable by never offering it to the model. The governor gives any agent that property: tool definitions on a principal's deny list are stripped from the request before the API sees them, so the model cannot call what it cannot see. A tool_choice that forces a denied tool is always refused, and a reject mode refuses the whole request for authors who want to know. Responses are never rewritten; editing model output would break streaming accumulators and usage accounting and leave the model believing it called a tool nobody ran.

04

What agents send gets scrubbed

Tool results are where customer data and credentials leak into prompts: CRM rows, logs, config. By default the governor redacts emails, phone numbers, SSNs, Luhn-valid card numbers and API keys in tool results before they leave, replacing each with a labelled marker; a per-agent setting extends that to everything. Counts per category go to the audit line, the span and the metrics, which is how you notice an agent pulling more than it should.

05

Refusals the SDK already understands

A governor refusal uses the Claude API's own error envelope and status codes: rate_limit_error with 429, permission_error with 403, authentication_error with 401. The agent's SDK raises its normal typed exception and existing retry logic keeps working. The end-to-end test runs the real anthropic SDK through the governor against a fake upstream on real sockets and asserts exactly those exceptions, for a disallowed model, an exhausted budget and a bad key.

06

Observability without a sidecar for the sidecar

Every request is one OpenTelemetry span with GenAI semantic-convention attributes (request and response model, input and output tokens, finish reason) and the governor's decisions (principal, cost, tools removed, redactions, budget state), exported over OTLP when a collector is configured and free when not. Streams settle in a child span. Counters are served at /metrics in JSON or Prometheus text, remaining spend at /budgets, and a JSON audit line per request carries the trace id so logs join to traces.

07

Verified as a process, not just in tests

Beyond the test suite, the governor ran as a real process in front of a fake upstream: a request with a denied tool and an email inside a tool result reached the upstream with the tool stripped, the email redacted and max_tokens clamped, priced and recorded. The governor was then killed and restarted, and /budgets reported the same spend. A Dockerfile packages it as a sidecar, and CI builds the image and probes its health endpoint.

Key decisions, from the record

ADR-0001
A reverse proxy speaking the Messages API, not an SDK wrapper. Zero code change for agents, language-agnostic, one place for the real key, one ledger across every agent. The cost: tracking the API surface it fronts.
ADR-0002
Worst case before, real usage after, in SQLite. Pessimistic pre-checks with Retry-After, settlement from the usage block including cache tokens, a ledger that survives restarts, unknown models priced at the most expensive known rate.
ADR-0003
Tool and model policy applied to the request. Denied definitions stripped or the call rejected; forced denied tools always refused; max_tokens clamped and reported. Responses pass through byte for byte.
ADR-0004
Redaction by default, API-shaped refusals, GenAI spans. Tool results scrubbed with Luhn-checked card detection and secret patterns; errors in the API's envelope so typed SDK exceptions fire; spans and counters where operators already look.

By the numbers

1
Real API key in the whole system; every agent holds a governor key instead
5
Redaction categories on by default: email, phone, SSN, card (Luhn), secrets
429
What a spent budget looks like to the SDK: a RateLimitError with Retry-After
0
Bytes of a model response the governor changes, streaming or not
19
Tests, including the official SDK driven through the governor on real sockets
4
Architecture decision records

Production habits, applied to model calls

Budgets enforced before the spend, credentials isolated to one process, allowlists at the edge, redaction before egress, typed errors, traces and metrics: none of this is new in backend engineering. The point of the governor is that model calls are production traffic and deserve the same treatment, and that an agent should not have to re-implement it.

Built with Claude Code as a pair, with the Claude API reference open for the error envelope, the streaming event shapes and the current price table. The judgment calls stayed with a person: that budgets fail closed, that tool policy never touches a response, and that a stream with no usage is charged the worst case rather than trusted.