Skip to content
← All projects
2026Sole engineer — design, Rust implementation, tests, deployment

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.

RustAxumPII redactionBudget enforcementCircuit breakerObservability
0 bytesof unredacted PII reach the vendor — asserted on the wire by an integration test, not assumed from a unit test

Architecture

  1. 1Parse — reject malformed JSON before anything reads it (400)
  2. 2Redact — replace PII spans with stable placeholders
  3. 3Meter — estimate tokens and cost; refuse over the ceiling (402)
  4. 4Breaker — refuse fast if the upstream is known-unhealthy (503)
  5. 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.

Live · no real model

What the vendor received

Nothing sent yet.

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.

Stack

RustAxumTokioreqwestregextracing (JSON logs)DockerCoolify