--- title: "Native connectors" description: "OpenAPI import plus a reviewed overlay: how a native connector is authored, how a native call executes, and why native is the only tier whose evidence is gateway-observed." --- A native connector is one where **every tool is a single bounded HTTP request**, Obol holds the vaulted vendor credential, and the gateway itself issues the call. It is the strongest observation boundary Obol has, and the only tier where the immediate observation is stamped `gateway_observed`. Native is reserved for connectors whose **receipt is the product**: prod-destructive writes, identity, money, and infrastructure mutations, and the official high-risk set named in ADR-0030. It is not a breadth exercise — long-tail coverage stays [federated](/connectors/federated). The reviewed native cut-list and its Gate 0 reasoning live in `packages/connectors/NATIVE.md`. ## What you author An OpenAPI document is an *input*, never the contract. The contract is the reviewed overlay plus the hand-authored bundle. | File | Role | |---|---| | `openapi.json` | The trimmed upstream spec. Shortlist only the operations you intend to expose — a full spec lands inline in `TargetSpec::OpenApi.spec` and is carried in the workspace snapshot to every replica. | | `overlay.json` | The only file a human authors from scratch: per-operation `EffectSpec`, `destructive`, egress, probe targets, webhook profile, `body_encoding`. | | `bundle.json` | The hand-authored `ConnectorBundle`. No compiler exists, so nothing derives it from the overlay. | | `help.json` | Operator-facing connect instructions (`packages/connectors/HELP.md`). | | `CONNECTOR.md` | The reviewer's note: vendor brief, tier decision citing it, and any blocker. | ### The `x-obol-effect` extension The importer does not guess an effect kind for mutations. | Method | Import behavior | |---|---| | `GET` | `EffectSpec::read_only()` — automatic, no extension needed | | `POST` / `PUT` / `PATCH` / `DELETE` | **Requires a complete `x-obol-effect`** | A missing extension fails the import with `"{method} operation requires x-obol-effect"` (`apps/gateway/crates/obol-httpx/src/spec.rs`). That is a stop condition, not a warning. The extension's value is deserialized and validated at import time. `destructive` is a **separate** signal that feeds Cedar and [approvals](/policy/approvals): it is `method == "delete" || x-obol-destructive == true`. It is not the effect kind, and nothing cross-checks the two. A conservative `EffectSpec` gates nothing on its own — if a `POST` mutation must require approval in prod, set `destructive: true` or write a Cedar rule. ### Request body encoding `TargetSpec::OpenApi`, the bundle, and the overlay all carry `body_encoding` (ADR-0034), and a single tool may override it under `annotations.openapi.body_encoding`. It is declared in the reviewed overlay and **never inferred from the vendor's OpenAPI document** — a spec that lists several media types, or simply gets it wrong, must not silently decide what the gateway sends. This changes only how the body is serialized. Credential injection, the host allowlist, idempotency binding, and evidence trust are unaffected: a native form-encoded call is `gateway_observed` exactly as a JSON one is. ### Dispatch annotations `annotations` on a tool is **executor dispatch config only** — never a place to park effect data. ```json { "annotations": { "openapi": { "path": "/v2/objects/{object}/records/{record_id}", "method": "get" } } } ``` ### Egress and probe targets The bundle declares its own egress policy and any probe target a readback stage names. ```json { "egress": { "schemes": ["https"], "ports": [443], "hosts": ["api.attio.com"] }, "probe_targets": [ { "probe_target_id": "pt_attio", "type": "http", "base_url": "https://api.attio.com" } ] } ``` `EgressPolicy::validate` checks very little: that hosts are non-empty and schemes are in `{https, http}`. `ports` is declared and not validated, and nothing cross-checks a probe target's `base_url` against `egress.hosts`. Author both by hand and review them deliberately. Egress and SSRF enforcement at dispatch is separate — see [Egress and SSRF](/security/egress-and-ssrf). ## How a native call executes The Obol virtual key authenticates, CEL prefilters visibility, and Cedar decides permission against the concrete tool and its arguments. Default-deny. `tools/list` never shows a tool the key cannot call. A destructive tool in prod requires approval unless policy explicitly grants it. One stable `InvocationId` and one Obol idempotency key are minted before dispatch and retained across approval, transport retries, and safe redispatch (ADR-0019). Idempotency is coordinated atomically in Redis, keyed by workspace, resolved connection, tool, and key. A request-hash conflict never dispatches. Arguments are validated against the tool's input schema **before any dispatch**. Bad arguments mean the vendor receives nothing. Path, query, header, and body are filled from the schema mapping; the body is serialized per `body_encoding`. Netguard resolves and screens the destination — loopback, private, link-local, multicast, and metadata ranges are refused — pins the resolved address, and revalidates every redirect. The vaulted vendor credential is unwrapped **inside the gateway process only** ([invariant 1](/concepts/invariants)) and injected on the outbound hop. See [Credential brokering](/security/credential-brokering). If the `EffectSpec` declares a `header` or `body_pointer` binding, the Obol key is propagated outbound under it. `unsupported` propagates nothing — and forecloses automatic redispatch after an ambiguous attempt. The executor normalizes the result into a non-serializable `DispatchObservation` stamped `EvidenceTrust::GatewayObserved` (`obol-httpx/src/lib.rs`). The numeric vendor status is preserved on every failure path, including an off-schema 2xx, which becomes `inconclusive` rather than a client-side schema error. The deterministic verifier applies the `EffectSpec`'s response binding and produces a versioned `InvocationReceipt` plus an append-only `VerificationEvent`. Raw bodies, credentials, and compared values never enter a receipt or a log — an assertion result carries a name and an outcome (`passed`, `failed`, `missing`, `invalid`) and nothing else. See [Receipts](/receipts/overview). ## Evidence a native route can carry Native is the only tier that reaches the top of the trust vocabulary on its own. - **Immediate transport facts and derived fields** are both `gateway_observed` (ADR-0025). - **An HTTP readback** against an Obol-custodied connection is `gateway_observed`. - **An MCP readback** is `broker_attested` even here, because an MCP server is by construction an authenticated intermediary rather than the originating vendor. - **A directly verified vendor webhook** is `vendor_signed`. Only `gateway_observed` and `vendor_signed` evidence may assert `contradicted`. See [Evidence classes](/receipts/evidence). ## Readback and webhook stages A native pack's overlay is where asynchronous evidence is declared, because a `VerificationPlan` may **only** live in an overlay or bundle — never on a published `ToolSnapshot` (see [EffectSpec](/connectors/effectspec) for why that rule is load-bearing). ### Readback A readback probe never carries a host, URL, or connection string. It names a `ProbeTargetId` declared in the pinned bundle, and its inputs are restricted to the invocation's safe upstream id, a bundle-authored literal, or — for HTTP path and query parameters only — a JSON Pointer into the canonical arguments resolving to a scalar (ADR-0025). ```json { "stage_id": "settled", "claim": "persisted", "kind": "readback", "after_s": 15, "probe": { "transport": "http", "probe_target_id": "pt_attio", "method": "GET", "path_template": "/v2/objects/{object}/records/{record_id}", "path_params": [ { "name": "object", "input": { "from": "request_pointer", "pointer": "/path/object" } }, { "name": "record_id", "input": { "from": "upstream_id" } } ] }, "assertions": [ { "type": "exists", "name": "record_persisted", "actual": { "source": "response_body", "pointer": "/data/id/record_id" } } ] } ``` The probe union is closed to `http` and `mcp` — the two transports the gateway already speaks. Database, message-broker, filesystem, gRPC, and object-store probes are **refused permanently, not deferred** (ADR-0025). An HTTP `path_template` must be origin-relative and start with `/`; that is what rejects an absolute URL. An MCP probe admits no request-derived argument at all. `absent` assertions are legal only on `kind: delete`, and only against a decisive negative the probe declares — `HttpProbe.decisive_negative`, restricted to 404 and 410 (ADR-0035). An errored probe yields `missing`, never a passing absence. The gateway serves readbacks at `/internal/v1/verification/readback`, driven by the control worker's scheduler with a short-lived single-use capability bound to workspace, invocation, connection, stage, plan digest, and nonce. The caller can never supply a destination or a selector. ### Webhook A webhook stage is the only route to `vendor_signed`. The bundle pins the signature profile, header, timestamp tolerance, and declared event types; only signature schemes in the closed `SignatureProfile` registry can be expressed. ```json { "signature_profile": "github_sha256", "header": "X-Hub-Signature-256", "timestamp_tolerance_s": 300, "event_types": ["issues", "issue_comment", "pull_request", "repository"], "provider_event_id": { "source": "response_header", "header": "X-GitHub-Delivery" } } ``` Vendor webhooks terminate at a generic gateway endpoint (`/webhooks/{workspace}/{connection}/{subscription}`, aliased at `/hooks/…`). The gateway verifies the original bytes, timestamp, and replay identity **before parsing**. An invalid signature produces security telemetry only — never evidence, and never a contradiction. If a vendor's scheme is not in the registry, author `"webhook": null` and say so in `CONNECTOR.md`; several shipped packs do exactly that. ## Traps worth knowing before you author - **Never publish a `VerificationPlan` on a `ToolSnapshot`.** The runtime verifier matches only `response_binding`. A published tool carries `verification: none` (reads) or a bare `response_binding` (mutations); plans stay in the overlay or bundle. - **`destructive` is set by hand.** Nothing derives `BundleTool.destructive` from `effect.kind`, so a mutation with the flag unset skips prod approval. - **Do not write tautologies.** An assertion derivable from `dispatch_statuses` — `exists` over `http_status` against `accepted: [200..=299]` — mints a green check from no evidence. Nothing enforces this rule; it is on the author and the reviewer. If a tool genuinely has no evidence path, declare it (ADR-0036) rather than fabricating one. - **Author the real idempotency binding.** Never default to `unsupported` because the vendor brief was thin; `unsupported` forecloses automatic redispatch and is a real product limitation worth writing down.