apps/gateway, crate obol-gateway) is the only component your
agents talk to. It is an axum service that terminates agent traffic on /v1/*
and /mcp, decides whether the call is allowed, injects your vendor credential
on the outbound hop, and records what happened.
Everything except main.rs lives in the library, so integration tests drive the
same router production serves.
Surfaces
The
/v1/* LLM surface is documented in LLM routes; the
MCP surface in MCP surface.
The five hot-path rules
The gateway is deliberately narrow. Five constraints shape every route.1
Authentication is a cached Redis read, never Postgres
A virtual key is hashed with SHA-256 and looked up at
key:{hash} in Redis
(60-second TTL by default, OBOL_KEY_TTL_S). On a miss the gateway calls the
control plane’s internal snapshot API with a short-lived service JWT and writes
the result back. The gateway holds no Postgres pool on the request path
(ADR-0006). A store failure fails closed as not_ready — never as
unauthorized, which a client would treat as final.2
Policy is compiled data evaluated in-process
CEL decides visibility, Cedar decides permission, and both run inside the
gateway process against a snapshot already in memory. There is no network hop
and no database query per decision (ADR-0003). See
CEL prefilter and Cedar.
3
Vendor credentials are unwrapped at the outbound hop
The encrypted credential travels in the workspace snapshot.
obol-vault
decrypts it (AES-256-GCM under a KEK) immediately before the outbound request
and the plaintext exists only between that call and the socket. It is never
serialized, cached, or logged. See Vault.4
No product logic, and no Python, in the gateway process
OAuth exchange orchestration, billing, approvals inboxes, and connector adapters
live elsewhere. Python connectors are out-of-process MCP workers (ADR-0002,
ADR-0007).
5
Usage emission is asynchronous and lossy; invocation evidence is not
Usage events go through a bounded in-memory queue and a batching flusher onto a
Redis stream. If that queue is full, the event is dropped and a counter
increments — a response is never blocked on metering. Invocation receipts and
verification events do not use that queue: they are committed durably through
the idempotency store before the gateway exposes a result (ADR-0032).
Request lifecycle
Both surfaces share the same prelude: request id, authentication, workspace snapshot, environment and freeze checks, scope check. They diverge after that.What the gateway proves
The guarantees the gateway makes before a call are the same everywhere: the key is authenticated, the tool or model is visible to it under CEL, Cedar allowed the action under the pinned policy publication, admission limits held, and an idempotency key was assigned. What the gateway can prove after the call is not uniform. A native route executes through the gateway’s own OpenAPI executor, so the receipt can record a gateway-observed outcome. A federated catalog route executes at a broker the gateway did not observe, so its evidence isuntrusted or broker_attested and
never verified. The receipt always names which class it is — read
Receipts before relying on one.
Error envelopes
Every error is oneErrorEnvelope rendered in the client’s dialect. /v1/*
OpenAI-format routes get an error object with message, type, code, and
param; /v1/messages gets the Anthropic type: "error" shape; /mcp gets a
JSON-RPC error object carrying the envelope as error.data. Every error
response carries x-request-id, and retry-after when the code supplies one.
Configuration
Configuration is environment-only, parsed once at startup.Debug redacts every
secret.
OBOL_IDEMPOTENCY_TTL_S is validated at startup: it must exceed the upstream
plus drain timeouts, and must not exceed the store maximum. A bad value fails
the process at boot rather than at the first call.
Startup and drain
run connects Redis and fails fast if it is unreachable, preloads up to 1000
workspace snapshots, spawns the invalidation subscriber, then binds. Shutdown on
SIGTERM or SIGINT flips /readyz and /startupz to draining, stops accepting connections,
waits for in-flight requests and metering reconciliation, and closes the meter
queue — all inside one OBOL_DRAIN_TIMEOUT_S deadline. See
Sessions and drain.
Metrics
The gateway registers these Prometheus series:
JSON-RPC method labels are drawn from a fixed allowlist; an unrecognized method
name is labeled
unknown so an attacker cannot grow the metric cardinality. See
Telemetry.
Next
LLM routes
The OpenAI-compatible surface, translation, streaming, and error mapping.
MCP surface
Tool namespacing, list filtering, invocation, and approvals.
Sessions and drain
Opaque session ids, snapshot consumption, graceful shutdown.
Crate reference
What every crate in
apps/gateway/crates is responsible for.