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 and 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.

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.

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, Cedar, and 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.

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.
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, Evidence, and 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.

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 and 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.

Tenancy and identity

How invariants 1, 5, and 8 are implemented.

Evidence trust

The ladder invariants 7 and 9 constrain.