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