Agent Egress Proxy — a fail-closed perimeter for outbound AI traffic
A small Rust service that sits between an agent and any OpenAI-compatible model API. It redacts PII — email addresses, UK National Insurance numbers, UK phone numbers, Luhn-checked card numbers, IP addresses, vendor API keys — refuses requests over a per-request token or cost ceiling, and records every decision as structured JSON. An upstream circuit breaker turns a vendor outage into fast refusals rather than a queue of hanging connections.
Architecture
- 1Parse — reject malformed JSON before anything reads it (400)
- 2Redact — replace PII spans with stable placeholders
- 3Meter — estimate tokens and cost; refuse over the ceiling (402)
- 4Breaker — refuse fast if the upstream is known-unhealthy (503)
- 5Forward — send only the redacted body; surface the decision in response headers
The problem
Three properties are easy to claim and hard to demonstrate about an agent platform: that it does not leak, that it cannot run away with spend, and that you can say what a request cost and how long it took. Prompts are assembled from whatever context an agent has to hand — support threads, scanned documents, receipts — so the realistic leak is accidental, not malicious. The only place all three properties can be enforced without trusting the caller is the network boundary.
Approach & decisions
- The order of operations is the security property: parse → redact → meter → check the breaker → forward the redacted body. Redaction runs before metering on purpose — placeholders are shorter than what they replace, so metering the redacted body is both more accurate and guarantees no unredacted bytes are ever measured or logged.
- Refusals are not faults. A budget refusal or a malformed request says nothing about upstream health, so the error type carries an is_upstream_fault() distinction and only genuine transport faults count toward opening the breaker. Without that, one caller sending oversized prompts could take the vendor offline for everyone — a trivially exploitable denial of service, closed by the type system and locked in by a test.
- Card numbers are Luhn-checked before redaction. A bare 13–19 digit pattern also matches order numbers, timestamps and hashes; redacting those corrupts legitimate prompts and trains people to disable the gate.
- Money is compared in integer cents, never floats. The estimate is computed in floating point because token pricing is fractional, then rounded up to whole cents before any comparison — ties go against the caller, never against the budget.
- Configuration is validated once, at boot. A bad limit kills the process with a legible message rather than surfacing on the thousandth request; by the time a handler runs, the config is known good.
- The claim is checkable rather than merely stated: a standard-library-only mock vendor echoes back exactly the body it received, so anyone can send a prompt with a card number through the proxy and read what the “vendor” got. That is what the live demo on this page does.
Results
- 31 unit tests and 7 integration tests. The integration suite starts a real mock upstream on an ephemeral port and asserts on the bytes it actually received — the email, card and NI number never arrive, the placeholders do, and the rest of the request survives intact.
- Deployed on a Hetzner box via Coolify in front of the echo-only mock upstream. The demo below sends your text through that live instance.
- Runs identically against a frontier vendor or a fully local model with no external network path at all — the perimeter does not care where the model lives.
Live demo
Try it against the live proxy
Your text goes from this page's server to a deployed instance of the proxy, which forwards it to an echo-only mock vendor. What you see on the right is what that vendor actually received. Nothing is stored; the route is rate-limited.
What the vendor received
What this honestly is not
- Not authentication. The caller's Authorization header is relayed untouched — this governs content, not identity, and deliberately holds nobody's credentials.
- Not a guarantee. Redaction is pattern-based; it will not catch a name or a postal address. It raises the cost of an accidental leak; it does not prevent a determined one.
- Not streaming. Responses are buffered, which is fine for chat completions and wrong for server-sent events — the first real gap to close.