The catalog is a frozen, content-reviewed index of connectors that Obol will list, shipped with the image at packages/connectors/catalog/index.json. It is discovery data, not live authority.
Catalog brokers are vendors, never authority (invariant 9). A broker never mints a receipt, assigns evidence trust, enforces a policy decision, or supplies idempotency. Idempotency is always Obol’s, and broker-side retries are disabled on every call — neither Nango nor Composio offers an idempotency primitive, and their retry knobs generate duplicate writes.

What is in the freeze

Lane counts overlap and do not sum to the catalog size: many vendors are carried by both brokers. The browse read publishes lane_overlap so a rail cannot be drawn as a partition it is not. Each row is a slug, a display name, a description, categories, a default tier, and one entry per source:
One catalog row
allowed_mcp_origins is the reviewed origin allowlist a federated completion is checked against. It is control’s input, never a browser’s, and the browse projection drops it along with upstream_id and mcp. Slugs are validated on load: lowercase alphanumerics, _, and -, no dots, and unique. Uniqueness is what makes the browse ordering a total order — two rows sharing a slug would tie on every key, and a tie in a paged read is how a row gets dropped from one page and repeated on the next.

How it is generated

packages/connectors/catalog/generate.py builds index.json from two inputs:
  • Vendored public broker catalogs in sources/ — Nango’s api-catalog.txt and Composio’s toolkits.md. Rows whose auth mode does not map onto Obol’s closed oauth2 | api_key pair are dropped, as are NONE and NO_AUTH rows.
  • Obol’s own reviewed native packs. native_packs() walks packages/connectors/*/bundle.json and adds a native source for every pack whose tier is native and custody is obol, keyed by the pack directory name — which is the slug control resolves a bundle by. Auth mode comes from the pack’s reviewed help.json.
Regeneration and drift checking:
Regenerate the freeze
Detected drift quarantines the index. It is never a hot swap from a live broker API. That matters more here than elsewhere: Composio’s catalog is machine-generated from scraped OpenAPI specs, its slugs have been renamed outside its own version system, and pinning a toolkit version does not pin the vendor API behind it. Connector brand marks for the dashboard gallery are a separate reviewed artifact (logos.json), and ship with the app rather than being fetched from a broker at render time.

Reading the catalog

GET /api/v1/workspaces/{id}/catalog answers in two shapes, discriminated by the presence of page. With no query parameter at all it returns every row verbatim with the workspace’s connections attached — the shape the connect dialog reads. Supply any of q, category, source, limit, offset, or cursor and it returns a page of gallery cards plus facets:
Browse the catalog
Every bound is refused rather than clamped: a silently narrowed page or truncated query is a different answer to a different question wearing a 200. The one exception is a category nobody uses, which is an empty page — “nothing is filed under that” is a real answer. A source is not that exception, because the lanes are a closed enum. Ranking is exact slug, then slug prefix, then name prefix, then substring in slug, then substring in name; ties break on slug, which is unique, so the same offset names the same row on every call. Both facet lists are computed over the unfiltered catalog so neither rail reshuffles while an operator types. A cursor is an offset plus a digest of the schema version, the freeze timestamp, and every filter field. Replaying a cursor issued for one query against another is refused rather than followed. It is not a signature and confers no authority — every offset it can name is one the caller could pass directly. GET /api/v1/workspaces/{id}/catalog/{slug} returns one row whole, or 404 for an unknown slug. Both reads require workspace.read.

From a catalog row to a routable capability

1

Register the broker (federated only)

POST /api/v1/workspaces/{id}/catalog-providers with {"provider": "nango" | "composio", "api_key": "…"} creates one broker connection per workspace. The broker’s own API key is sealed by the gateway and the connection goes active. A second registration for the same provider is refused.
2

Start the connection

POST /api/v1/workspaces/{id}/connections with {"slug", "source"} resolves the row and its source, takes the workspace lock, and either reuses an abandoned credential-less pending row or creates one. What comes back in next depends on the source’s auth mode: restricted_key, redirect (native OAuth), or catalog_redirect (federated).
3

Finish the credential

For a native API-key source, POST .../restricted-key. For native OAuth, follow the returned authorize URL to the callback. For a federated source, follow the broker’s session URL and then call POST .../federated-complete — which always fails today with 409 broker_verification_unavailable, so no federated connection can be finished. See the warning below.
4

Publish

Every mutation stages a snapshot publication inside its transaction and publishes after commit. Nothing is routable until the published version covers the workspace’s current snapshot version.
5

Bind

Once a native or federated connection is active, tools_from_graph publishes its concrete tools. bindings_for_routes includes active capability bindings whose target resolves to exactly one tool and whose source, custody, tier, surface, and evidence ceiling match the connection. ADR-0063 makes all structurally valid catalog bindings eligible without native-only or curation gates. Inferred broker target names still need a matching concrete tool contract; eligibility does not certify their mapping. See Capabilities.
A workspace holds at most one active connection per connector. A second is refused with a conflict rather than added, because two active connections on one connector publish two tool snapshots with the same name and the gateway resolves whichever sorts first — leaving the other credential silently unreachable.

Federated connect sessions

Starting a federated connection creates a CatalogConnectSession (cxs_…) that is:
  • Bound to the workspace, the connection, the initiating operator, and the provider.
  • Frozen with the reviewed allowed_mcp_origins for that source at handoff time, so a later catalog or settings change cannot widen what completion will accept.
  • Valid for 10 minutes, single use.
Control then asks the gateway to mint the broker session under a capability token and returns the broker’s redirect URL along with the cxs_ id. Because the broker has no return leg to Obol, the connections and catalog reads hand a resumable handle back — but only to a caller who holds connection.manage, only to the operator who initiated it, only while unconsumed and unexpired, only while the session’s provider still matches the connection’s, and only once the snapshot is published. Those are exactly the grounds completion itself refuses on, so a surface never offers a handle that completion would reject.

Completion

POST /api/v1/workspaces/{id}/connections/{connection_id}/federated-complete takes the cxs_ id, the broker’s upstream connection id, and the MCP URL. It runs in two short transactions with no transaction open across the network:
  1. Phase A locks Membership → Workspace → Connection → Connector → CatalogConnectSession → SnapshotPublication, validates eligibility and the upstream id, captures the frozen origins, and commits — releasing every lock before a packet leaves.
  2. The probe normalizes and validates the URL against those origins with no lock held. See Capabilities for what that validator does and does not guarantee.
  3. Phase B retakes the same locks in the same order, repeats every check on fresh rows, and mutates.
The upstream connection id is broker-supplied and lands verbatim in an audit row, so it is refused if it is empty, padded, over 256 bytes, carries control characters, or is URL-shaped. Only the bare reviewed origin reaches the audit detail — never the path or the full URL.
Federated completion currently always fails. complete_federated_session in apps/control/app/services/connections.py raises broker_verification_unavailable (409) at the mutation boundary, leaving the connection pending and the handoff unconsumed. An unauthenticated HEAD probe proves a reviewed, reachable origin — not who owns the connected account, whether authorization is active, or whose OAuth client it uses, and an operator-supplied identifier cannot be authority for a credentialed route. No authenticated broker-account verifier exists yet.

Managing a connection

A connection row is otherwise kept forever, because it is the audit trail of a credential this workspace held.

Revocation is two-step

Disabling or purging at Obol covers the Obol hop only. The broker still holds the vendor credential, and a broker-held token can act outside Obol entirely — through the customer’s own scripts or another connected agent. Obol’s audit log is a record of Obol-mediated traffic, not of everything the credential did. Composio cannot revoke API-key-scheme connections at all, leaving revocation to the end user at the vendor.

Broker asymmetry

The two brokers are adopted in different roles, and the difference is enforced in the snapshot rather than in a runbook.
The credential substrate. Its catalog is configuration data across roughly 980 providers, its raw proxy preserves the vendor’s numeric HTTP status, and it permits extracting the token — which is the precondition for hybrid custody, and therefore the only path by which a federated write could ever reach verified.Obol must use the raw proxy rather than the normalized action endpoint, and the proxy-forward-all-response-headers account flag is a contractual precondition: without it the vendor’s request id never reaches a receipt.Rate limits are per Nango account, not per Obol workspace.
Official and high-risk connectors stay native or trusted-worker regardless. github has default_tier: native and stripe has default_tier: trusted_worker; federation is listed as an additional source, not the default.

Broker content is data, never 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 instructing them how to sign up. Catalog-sourced bundles are therefore frozen, content-addressed, workspace-qualified, and conformance-tested, and broker runtime metadata is never authority.

Current state

All structurally valid catalog capabilities and bindings are eligible across native and federated sources (ADR-0063). Missing conformance, curation, or additional providers does not exclude a candidate. Workspace access and concrete operation contracts still constrain publication and execution. Federated completion still refuses with 409 broker_verification_unavailable until authenticated broker-account verification exists; eligibility does not complete a connection. The historical review and source ledger live at docs/reviews/2026-09-04-connectors-federated.md.

Federated connectors

What the federated tier is and what it can prove.

Connector tiers

The gate that decides native, trusted worker, or federated.

OAuth

Gateway-owned exchange and refresh for native connections.

Catalog API

Full request and response shapes.