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