--- title: "Federated connectors" description: "Catalog brokers and remote MCP servers: the customer's own OAuth client, the generic federated EffectSpec, the evidence ceiling, and everything a broker is not allowed to do." --- A federated connector is one where a **third party holds the vendor credential and executes the call** — a catalog broker such as Nango or Composio, a remote MCP server, or a customer-run server. Obol stores only a workspace-scoped upstream credential or an account-scoped connection reference (`FederatedBrokerRef`), never the underlying vendor secret. Federation is how coverage arrives without the iPaaS treadmill (ADR-0030, PRD §6). It is cheap breadth **bought with evidence**, and the price is exact. For a purely federated invocation with no separately authenticated asynchronous stage, the fraction of receipts that can ever read `verified` is exactly zero — enforced at validation time (ADR-0030). Do not federate anything whose value is the receipt. ## When a connector is federated Either of the first two rows of the [tier decision](/connectors/tiers) fires: 1. A third party holds the vendor credential and executes the call → `custody: federated`, `kind: catalog_remote`, `broker_ref` set. 2. The surface is an MCP server rather than an HTTP API → `target: mcp_remote`, **even when Obol vaults the MCP bearer token**, because the upstream still owns the vendor connection. Gate 0 runs first and closes federation for prod destructive tools and for official/high-risk connectors, which stay native or trusted-worker. Snapshot validation enforces the shape: `SnapshotValidationError::FederatedCustodyOnNativeTarget` rejects a connection with `custody: federated` on any target other than `mcp_remote`, so a brokered route cannot be authored over an OpenAPI target and quietly keep a `gateway_observed` stamp. ## Custody: the customer's own OAuth client [Invariant 8](/concepts/invariants) binds the *vendor-facing identity*, so it forbids a shared broker OAuth app across tenants just as it forbids a pooled vendor account. - **No pooled broker account and no pooled broker OAuth application.** Activation fails if a broker account identifier appears under more than one `workspace_id`. - **Production destructive tools require the customer's own OAuth client at the broker.** - **No vendor secret crosses either boundary** — none imported broker→Obol, none exported Obol→broker. - Custody mode, connector tier, achieved evidence ceiling, and idempotency-propagation mode are non-nullable on every receipt and snapshot tool entry, and are Cedar-visible. **Revocation is two-step, and must be disclosed as such.** Revoking the Obol key covers the Obol hop only; control must call the broker's disconnect API, and the dashboard renders "revoked at Obol, still live at broker." A broker-held token can also act outside Obol entirely — through the customer's own scripts or another connected agent — so Obol's audit log is a record of Obol-mediated traffic, not of everything the credential did. There is no technical fix; the mitigation is a connection-exclusivity attestation surfaced in the dashboard and deniable by policy for production destructive use. ## The frozen catalog `packages/connectors/catalog/` holds one frozen index and **one overlay template applied to every catalog-sourced tool** — deliberately not a pack per vendor. `index.json` is generated by `generate.py` from two inputs: vendored public broker catalogs for the federated long tail, and the reviewed native packs on disk for the routes Obol custodies itself. Each entry names its sources and their auth modes: ```json { "slug": "1password-events", "display_name": "1Password (Events API)", "categories": ["iam"], "default_tier": "federated", "sources": [ { "provider": "nango", "auth_modes": ["api_key"], "mcp": true, "allowed_mcp_origins": ["https://api.nango.dev"], "upstream_id": "1password-events" } ] } ``` The freeze is 2,147 connectors: 980 Nango, 1,380 Composio, and 103 native sources read back from Obol's own packs. `github` and `stripe` carry a `default_tier` that is not federated — federation is listed for them as an additional source, never the default. **Catalog bundles are reviewed data, not live authority.** Every catalog-sourced bundle is frozen, content-addressed, and workspace-qualified. Conformance records describe the mappings actually tested; eligible status does not certify conformance. Broker runtime metadata is never authority; detected drift **quarantines** the binding and never silently upgrades it. Broker-side schema and slug churn must be detected by digest comparison on every snapshot build, because neither broker offers a deprecation window or a schema-stability commitment. All structurally valid Nango and Composio capability bindings are eligible under `cap.*` (ADR-0063). Eligibility does not certify their operation mapping or connect an account. Federated completion still returns `409 broker_verification_unavailable` until authenticated broker-account verification exists; the connection stays pending and its handoff unconsumed. ## The generic federated EffectSpec Because there is no per-vendor federated pack, there is no per-vendor `EffectSpec` either. `packages/connectors/catalog/effects/` holds two, applied as a template. **Read** — `federated-read.json`. No dispatch classification, no upstream id, no verification. A read concludes `not_required`. ```json { "schema_version": 1, "kind": "read", "idempotency": { "type": "unsupported" }, "dispatch_statuses": { "accepted": [], "definitely_rejected": [] }, "upstream_id": null, "verification": { "type": "none" } } ``` **Mutation** — `federated-mutation.json`. Conservative status classification, one existence assertion on the broker's object id, and `idempotency: unsupported`, which is the honest binding because neither broker offers an idempotency primitive. ```json { "schema_version": 1, "kind": "create", "idempotency": { "type": "unsupported" }, "dispatch_statuses": { "accepted": [{ "type": "range", "start": 200, "end": 299 }], "definitely_rejected": [ { "type": "exact", "status": 400 }, { "type": "exact", "status": 401 }, { "type": "exact", "status": 403 }, { "type": "exact", "status": 404 } ] }, "upstream_id": { "source": "response_body", "pointer": "/id" }, "verification": { "type": "response_binding", "config": { "trust": "gateway_observed_only", "assertions": [ { "type": "exists", "name": "broker_object_id", "actual": { "source": "response_body", "pointer": "/id" } } ] } } } ``` The `trust` field on a response binding says what the *spec* is willing to accept; it does not raise what the *route* stamps. A federated observation is `untrusted`, so the binding's assertions cannot reach `verified` however the tool self-reports — proven by `remote_mcp_false_proof_remains_inconclusive` in the gateway's verification tests. ## Capability routing vs concrete federated tools Federated tools can be called by their concrete names and can participate in `cap.*` routing (ADR-0063). A binding must resolve to an actual tool on a matching active connection and pass workspace and request controls. Generated broker operations may use inferred `{slug}.read` / `{slug}.mutate` names; the frozen catalog does not certify their per-vendor request or response shapes. Native-only activation, mandatory curation, and mandatory native conformance fixtures no longer restrict the candidate pool. Declared feature coverage remains accurate, concrete tools remain the authorization subject, and invalid requests or responses still fail validation. Broker metadata never assigns receipt trust or authorizes execution. The evidence floor is unchanged: a purely federated invocation with no separately authenticated asynchronous stage can never read `verified`. ## Evidence classes on a federated route Per-source trust (ADR-0025), for a federated connection: | Evidence source | Trust | |---|---| | Immediate transport facts | `untrusted` — the gateway's socket terminated at the broker, not the vendor | | Immediate derived fields | `untrusted` | | Readback | `untrusted`, unless Obol authenticates a workspace-scoped identity at the far end → `broker_attested` | | Broker-forwarded webhook | `untrusted`, unless the same authentication holds → `broker_attested`; never `vendor_signed` unless Obol validates the original provider signature | `broker_attested` requires a pinned, workspace-scoped, versioned broker signing identity — signing secret, mTLS identity, or signed assertion — with rotation overlap, timestamp tolerance, and replay identity checked before parsing. **Obol authenticating *to* a broker never constitutes attestation**; the direction that matters is broker→Obol. Where the identity is channel-level only, the receipt records `attestation: channel` rather than `attestation: signed_assertion`, because only the latter is non-repudiable. And a signed assertion proves non-repudiation, not correctness. If a broker signs only its own normalized output, it has restated its claim under a key. To mean more, the assertion must bind workspace, broker connection, reflected invocation id, a digest of the vendor request actually issued, the vendor's numeric status, the vendor object id, and a nonce. Both `broker_attested` and `untrusted` fail the verifier's authoritative-trust check, so a federated mutation concludes `inconclusive`. Neither may assert `contradicted` — a hostile federated upstream must not be able to raise a customer-visible alarm on a real effect. See [Evidence classes](/receipts/evidence). ### The two paths to a verified federated write Neither is reachable today, but for different reasons — and the difference matters. Hybrid custody is genuinely unbuilt and is deferred to phase 2 (`docs/PRD.md` §12). Signed vendor-webhook ingress, by contrast, **does ship**: the `/hooks/...` endpoint, its reviewed signature profiles, correlation and receipt revision are all implemented. What blocks path 2 is upstream of the evidence: no federated connection can be completed at all while `federated-complete` returns `409 broker_verification_unavailable`. Do not promise either in a bundle you ship today. 1. **Hybrid custody** — Obol vaults its own read-only credential for the same account while the broker holds the write credential, yielding a `gateway_observed` readback. Available through Nango, **not** through Composio, which redacts tokens universally. It requires amending the connection contract's exclusive *either an Obol-encrypted credential or a federated upstream reference* into a pair. 2. **Direct vendor webhooks** — `vendor_signed`, needing only that the customer register Obol's endpoint at the vendor, which most vendors permit alongside the broker's. ## What a broker is not allowed to do [Invariant 9](/concepts/invariants): catalog brokers are vendors, never authority. A federated catalog never mints a receipt, assigns evidence trust, enforces a policy decision, chooses a target, or supplies idempotency. Concretely: - **Never delegate a Cedar decision to broker-side filtering.** Composio's execution allowlist is documented as bypassable and its schema-modifier hooks do not run over MCP. Authorization is Obol's, on every tier. - **Never launder provenance through a catalog adapter.** Routing federated evidence through an adapter in `apps/connectors` to acquire `connector_attested` is laundering and is rejected by name: a federated route may never yield `connector_attested`. That class means a reviewed first-party worker under Obol's control. - **Never treat broker catalog text as instruction.** Any surface a broker controls can carry text aimed at a model — Composio's own pricing page ships a block addressed to AI agents. A bundle is frozen, content-addressed, reviewed data. - **Never accept broker-asserted authority in a response.** The gateway screens a connect-session response for authority-shaped keys — `target`, `policy`, `evidence`, `receipt`, `idempotency`, `idempotency_key`, `retry`, `credential` — at any depth and fails the call if one is present (ADR-0038). Fields Obol simply ignores travel no further than the parse; a broker *asserting authority* is a fact worth failing on. - **Never let a broker retry.** Gateway-side idempotency is unconditional and broker-side retries are **disabled on every call**. Propagation is declared honestly per binding, and `propagation: none` is a policy-deniable attribute. An ambiguous federated mutation terminates the invocation `inconclusive` and is never auto-retried or failed over. ## Operating a federated connector - **Use the raw proxy, never the normalized action endpoint**, where a broker offers both — it is the only shape that preserves the vendor's numeric status. Nango's `proxy-forward-all-response-headers` account flag is a contractual precondition: without it the vendor's request id never reaches a receipt. - **Federated capacity is not the customer's capacity.** Broker rate limits pool across tenants, so a federated candidate degrades based on strangers' behavior. `shared_quota` is a declared binding attribute and a [routing](/routing/catalog) filter input, not an operational footnote. - **Federated cost is a stack, and often unknowable.** The broker component is computed from workspace configuration rather than looked up, and the vendor component may be genuinely unavailable — so cost attribution carries `bundled_in_broker` as a distinct value rather than a fabricated number. - **Independence is a requirement.** No catalog is a hard dependency of authentication, authorization, receipt minting, metering, or the vault. A broker outage degrades the connectors it serves and nothing else, and every catalog-sourced connector has a documented graduation path to native or trusted-worker custody. - **Self-hosting a broker is closed.** ADR-0030 rejects self-hosting Nango on licence grounds, and Composio's platform is enterprise-gated. An [Enterprise self-host](/get-started/self-hosting) deployment using federation requires the customer's own broker account. - **State the ceiling at connect time, not at audit time.** If a dashboard collapses five evidence classes into one green check, the distinction has made verification weaker rather than stronger. Render a federated write as *executed — not verified*.