---
title: "Invariants"
description: "The nine rules Obol never violates, why each exists, and the architecture decision record behind it."
---
These nine rules hold regardless of what a task, a ticket, or a customer request seems to ask for. They are enforced in code review and in CI, and a change that breaks one is wrong even when it works.
ADR numbers below refer to the decision records in `docs/adr/` in the Obol repository. Accepted ADRs are immutable: a reversal supersedes one rather than editing it.
An invariant is not a default. There is no configuration flag that turns one off, and "just for this connector" is not an exception — a genuine exception is a new ADR.
## 1. Inbound credential is never outbound credential
Obol keys authenticate to Obol. Vaulted customer credentials authenticate to vendors. A vendor secret never reaches an agent, a worker config, a log, the web app, or control. Plaintext decrypt happens only inside the gateway process.
**Why.** This is the security value proposition. An agent holding a raw `sk_live_` key means one prompt-injection leak equals full vendor blast radius. Splitting the two credentials makes revoke a product feature instead of an incident.
**Backed by** ADR-0004. See [Virtual keys](/security/virtual-keys) and [Vault](/security/vault).
## 2. No Python in the gateway process, and no product logic in it
Connectors are out-of-process MCP workers. Generic REST executes in the Rust OpenAPI executor; messy first-party vendors and catalog adapters run as separate processes.
**Why.** Embedding CPython in the proxy puts the GIL against Tokio, lets one bad connector take down the data plane, and prevents connectors from scaling or circuit-breaking independently. Keeping the Rust surface thin also keeps it stable: a feature that smells like product goes to control or to a worker by default.
**Backed by** ADR-0007 and ADR-0002. See [Trusted workers](/connectors/trusted-workers).
## 3. Postgres never sits on the streaming path
Authentication is a cached Redis point read. Usage and spend writes are batched and asynchronous. State reaches the data plane only through snapshots.
**Why.** A query per request, or an `UPDATE spend` per token stream, is how gateways fall over under load. The gateway also degrades gracefully — it keeps serving from cache during a control or Postgres blip, and `readyz` requires only Redis plus at least one loaded snapshot.
**The accepted cost** is eventual consistency: budgets lag by seconds, and revoke propagation is bounded at roughly 60 seconds.
**Backed by** ADR-0006. See [Architecture](/concepts/architecture).
## 4. Policy is compiled data evaluated in-process
CEL decides visibility. Cedar decides permission. Both run inside the Rust gateway against a compiled snapshot — never a network hop, never a database query per decision, and never business rules in CEL.
**Why.** Authorization runs on every tool call and every `tools/list`. A sidecar hop or a policy service turns the product's core control into a latency and availability dependency. Compiling to data also makes policy analyzable and auditable by a customer's security team, which a callback-based engine is not.
**The boundary rule**, enforced in review: CEL decides visibility, Cedar decides permission.
**Backed by** ADR-0003. See [Policy overview](/policy/overview), [Cedar](/policy/cedar), and [CEL prefilter](/policy/cel-prefilter).
## 5. `workspace_id` on every Postgres row and every Redis key
That is tenant isolation. Not a filter applied at the query layer, not a middleware convention — a column and a key prefix.
**Why.** Isolation that depends on every caller remembering to add a `WHERE` clause fails on the first route someone forgets. Making it structural means a missing scope is a schema error rather than a data leak.
**Backed by** ADR-0006. See [Tenancy and identity](/concepts/tenancy-and-identity).
## 6. Destructive tools in production require approval
Unless policy explicitly grants otherwise, a destructive tool in a production workspace returns `approval_required` rather than executing. Separately, `tools/list` never shows a tool the key cannot call.
**Why.** These are two halves of the same defense against prompt injection and model error. Hiding an unauthorized tool removes it as a target; holding a destructive call puts a human between the model and a refund, a delete, or an outbound SMS. Filtered listing is also why "invisible" and "does not exist" have to be one answer — otherwise the list becomes an oracle for the workspace's tool inventory.
**Backed by** ADR-0003, ADR-0015, and ADR-0039. See [Approvals](/policy/approvals).
Approval permission is separate from Cedar tool permission and never substitutes for it. A held call still passes the full Cedar path on resume.
## 7. Every tool call gets an idempotency key and a receipt
One logical invocation carries one invocation ID, one request hash, and one workspace-scoped idempotency key across approval and safe retries. Each invocation produces a receipt naming the policy id, the upstream id, and the vendor status.
**Why.** Without gateway-owned idempotency, an ambiguous mutation plus a retry equals a duplicate write, and neither Nango nor Composio offers idempotency to inherit. Without a receipt, "which agent called `create_refund` at 18:03 and under what authority" has no answer.
**Read the guarantee precisely.** The receipt is minted for every call on every tier, and the authorization it records is tier-independent. What the receipt can *prove* about the effect is tier-dependent, and the receipt names its evidence class. A native route can reach `gateway_observed`; a federated route is `untrusted` or `broker_attested` and can never be `verified`.
**Backed by** ADR-0019, ADR-0021, and ADR-0032. See [Receipts](/receipts/overview), [Evidence](/receipts/evidence), and [Idempotency](/receipts/idempotency).
## 8. No pooled vendor accounts, no full `sk_live_` storage
Outbound credentials are restricted vendor keys, OAuth tokens, or Connect — never a customer's full secret key, and never one account shared across tenants. Because this binds the vendor-facing identity, it also forbids a shared broker OAuth app: a federated connection uses the customer's own OAuth client.
**Why.** A pooled key is a single blast radius across every tenant and a vendor terms-of-service breach around resale and multi-tenant app use. The market prices it too — one broker charges 50 times more for a pooled credential than a customer-supplied one, citing shared rate limits.
**Backed by** ADR-0004 and ADR-0030. See [Credential brokering](/security/credential-brokering).
## 9. Catalog brokers are vendors, never authority
A federated catalog never mints a receipt, assigns evidence trust, or enforces a policy decision. Idempotency is always Obol's, and broker-side retry knobs are disabled on every call because they generate duplicate writes.
**Why.** Federation buys coverage without the connector treadmill, but only if the broker stays on the vendor side of the line. The moment a broker's response can set trust, the audit log records the broker's opinion rather than Obol's observation.
**How it is enforced, concretely.** A broker's connect-session response is screened at any depth for authority-shaped keys — `target`, `policy`, `evidence`, `receipt`, `idempotency`, `idempotency_key`, `retry`, `credential` — and refused outright if one is present. Other unexpected fields are ignored rather than treated as errors, so the screen tests for authority rather than novelty (ADR-0038).
**Backed by** ADR-0030, ADR-0021, ADR-0025, and ADR-0038. See [Federated connectors](/connectors/federated) and [Catalog](/routing/catalog).
## Claims that are banned
These follow directly from the invariants, and they are banned regardless of who writes them — docs, dashboard copy, or a sales deck.
| Banned claim | Why it is false |
|---|---|
| "Every tool call is verified" | Verification is tier-dependent; a federated call concludes `inconclusive` unless an independently authenticated source confirms the effect |
| "N+ verified connectors" | Connector count is not an evidence claim |
| "Your credentials never leave your control" | False when a broker holds the vendor token |
| "Revoke once and all access stops" | Revocation is two-step and propagates in up to 60 seconds |
| "We log everything your agent's credentials do" | Obol logs what goes through Obol |
How invariants 1, 5, and 8 are implemented.
The ladder invariants 7 and 9 constrain.