---
title: "Connectors overview"
description: "What a connector is in Obol: a reviewed contract that fixes who holds the credential, who executes the call, and what evidence a receipt can ever carry."
---
A connector in Obol is a **contract**, not a code path. Adding one is not "writing an
integration" — it is deciding, and recording, four things that are expensive to reverse:
- **Who holds the vendor credential** — Obol's vault, or a third party.
- **Who executes the call** — the gateway, a trusted worker planning a request the
gateway then issues, or a broker outside Obol entirely.
- **What each tool's effect is** — declared as a required `EffectSpec` on every tool
(ADR-0019). See [EffectSpec](/connectors/effectspec).
- **What evidence could ever prove that effect happened** — which fixes the ceiling on
every receipt the connector will ever mint.
Those four answers are what the tier decision produces, and the tier is what the runtime
reads back. Get it wrong and the fix is a superseding ADR, not a revert.
For workspace-owned endpoints, use [Bring your own MCP server or API](/connectors/bring-your-own). The dashboard imports and publishes reviewed revisions through the existing gateway.
## What is tier-independent
Whatever tier serves a tool, an agent reaches it the same way: one Obol virtual key
against the [MCP endpoint](/gateway/mcp). Everything Obol guarantees *before* a call is
the same on every tier, because the gateway sits upstream of all of them: one identity,
default-deny [Cedar authorization](/policy/overview) with argument conditions, CEL
visibility,
[approvals](/policy/approvals) for destructive tools, budgets, environment separation, an
Obol idempotency key per call, and one attributed invocation history. `tools/list` never
shows a tool the key cannot call, whatever tier serves it.
## What is tier-dependent
What Obol can *prove* after a call is not the same on every tier. Evidence trust is
derived from the configured route, never from what the upstream claims (ADR-0019):
| Tier | Executor | Trust on the immediate observation | Can reach `verified`? |
|---|---|---|---|
| Native | `OpenApiExecutor` (`obol-httpx`) | `gateway_observed` | Yes |
| Trusted worker | `WorkerExecutor` (`obol-mcp`) | `connector_attested` | Only if the `EffectSpec` sets `trust: allow_connector_attested` |
| Federated | `RemoteMcpExecutor` (`obol-mcp`) | `untrusted` | **No** |
Federated evidence is `untrusted` or `broker_attested`. Both fail the verifier's
authoritative-trust check, so a purely federated invocation with no separately
authenticated asynchronous stage concludes `inconclusive` — never `verified`, and never
`contradicted` (ADR-0019, ADR-0021, ADR-0025, ADR-0030). Write "receipts prove what a
tool call did" only about a native route; never unqualified.
Routing enforces the same ceiling before selection: a binding declaring an evidence floor
above its connection's tier ceiling is excluded (`obol-types/src/routing.rs`, reason
`EvidenceFloor`). See [Routing and the catalog](/routing/catalog).
## Catalog brokers are vendors, never authority
[Invariant 9](/concepts/invariants) governs every federated connector. A broker never
mints a receipt, assigns evidence trust, enforces a policy decision, chooses a target, or
supplies idempotency. Idempotency is always Obol's — neither Nango nor Composio offers
one, and their retry knobs generate duplicate writes, so **broker-side retries are
disabled on every call**. The gateway screens a broker's connect-session response for
authority-shaped keys (`target`, `policy`, `evidence`, `receipt`, `idempotency`,
`idempotency_key`, `retry`, `credential`) at any depth and refuses the call if one is
present (ADR-0038).
## The three tiers at a glance
OpenAPI import plus a reviewed overlay. Obol vaults the credential, the gateway
issues the request, evidence is gateway-observed.
An out-of-process worker that plans requests and normalizes responses. Reserved
design — no package, no image, no deployed service, and ADR-0024's
credential boundary is not yet implemented.
A catalog broker or remote MCP server holds the vendor connection. Cheap breadth,
bought with evidence.
The tier is derived from what you author, not chosen directly:
`ConnectionSnapshot::tier()` reads `custody`, `kind`, and `target`
(`apps/gateway/crates/obol-types/src/snapshot.rs`).
```rust
if self.custody == CustodyMode::Federated
|| matches!(self.kind, ConnectorKind::CatalogRemote)
{
return ConnectorTier::Federated;
}
match &self.target {
TargetSpec::OpenApi { .. } | TargetSpec::ModelProvider { .. } => ConnectorTier::Native,
TargetSpec::Worker { .. } => ConnectorTier::TrustedWorker,
TargetSpec::McpRemote { .. } => ConnectorTier::Federated,
}
```
Snapshot validation rejects federated custody on any non-MCP target
(`SnapshotValidationError::FederatedCustodyOnNativeTarget`), so a brokered route cannot
be authored as `custody: federated` over an OpenAPI target and quietly keep a
`gateway_observed` stamp. Work through the ordered table in
[Connector tiers](/connectors/tiers) before authoring anything.
## The connector pack layout
Reviewed connector packs live in `packages/connectors//`. The slug is the ADR-0015
tool prefix: dot-free, `[a-z0-9_-]`, and every tool in the pack is named
`{connector}.{tool}`.
```text
packages/connectors/
├── NATIVE.md # the reviewed native cut-list and its Gate 0 reasoning
├── HELP.md # authoring contract for help.json
├── /
│ ├── openapi.json # trimmed upstream OpenAPI document (the input)
│ ├── overlay.json # the only file a human authors: effect, idempotency,
│ │ # verification, egress, probe targets, webhook
│ ├── bundle.json # hand-authored ConnectorBundle (no compiler exists)
│ ├── help.json # operator-facing connect instructions (see HELP.md)
│ └── CONNECTOR.md # reviewer's note: vendor brief, tier decision, blockers
└── catalog/ # the frozen federated index — see below
```
`overlay.json` and `bundle.json` are both hand-authored: there is no bundle compiler and
no `obol-bundle` binary, so nothing derives one from the other and nothing derives
`BundleTool.destructive` from `effect.kind`. Set both by hand and validate by
round-tripping through the schemas in `packages/proto/`.
A bundle carries the reviewed data the runtime pins:
```json
{
"schema_version": 1,
"connector": "attio",
"tier": "native",
"custody": "obol",
"publisher": { "id": "obol", "display_name": "Obol", "reviewer": "founder" },
"egress": { "schemes": ["https"], "ports": [443], "hosts": ["api.attio.com"] },
"probe_targets": [
{ "probe_target_id": "pt_attio", "type": "http", "base_url": "https://api.attio.com" }
],
"webhook": { "signature_profile": "attio_sha256", "header": "Attio-Signature" },
"tools": [],
"source_digest": "29f47f36…",
"compiled_at": "2026-08-29T00:00:00Z"
}
```
The bundle's declared `tier` and `custody` must agree with the connection it activates.
`CONNECTOR.md` records the vendor brief the tier decision cites, and `help.json` is the
operator's note — what they must do on the vendor's own site before Obol can reach it.
Neither is remote text; both are reviewed, committed, diffable repo content, and neither
may be fetched at render time.
## The federated catalog
`packages/connectors/catalog/` is a different shape, deliberately. It is one frozen index
plus **one overlay template applied to every catalog-sourced tool** — not a pack per
vendor, because a pack per vendor is the iPaaS treadmill the PRD forecloses.
```text
packages/connectors/catalog/
├── index.json # frozen publisher index (Nango + Composio + native packs)
├── overlay.json # the generic federated template: one read op, one mutate op
├── effects/ # federated-read.json, federated-mutation.json
├── capabilities/ # capability specs the routing catalog binds against
├── webhook-profiles.json
├── logos.json # brand marks, shipped with the app, never fetched at render
├── CATALOG.md # connector note: vendor brief for both brokers
└── generate.py # regenerates index.json from vendored broker catalogs + packs
```
`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 (`generate.py::native_packs()` walks every
`packages/connectors/*/bundle.json` whose tier is `native` and custody is `obol`). Broker
catalog text is untrusted input; the index is discovery data, never live authority for
effect or verification semantics.
All structurally valid native and federated capability bindings are eligible
(ADR-0063). A workspace still needs an active matching connection, a concrete tool,
and permission. The historical broker review in `packages/connectors/catalog/CATALOG.md`
remains evidence about execution readiness: federated completion returns
`409 broker_verification_unavailable` until authenticated account verification
exists. Eligibility does not imply that a broker connector has been live-verified.
## Where to go next
The ordered decision table, and the Gate 0 risk class that runs before it.
Shape, fields, validation errors, and the traps no validator catches.
Vendor brief, contract-first order, Cedar, fixtures, probes.
What the receipt records about tier, custody, evidence, and conclusion.