---
title: "Trusted workers"
description: "Parked: the trusted-worker tier is accepted (ADR-0024) and not implemented. The compiled executor still speaks ADR-0014's credential hand-off; no worker exists to receive it."
---
Trusted workers are a **reserved design, not a shipped tier** (ADR-0024, parked by
ADR-0058). ADR-0024 decided that a reviewed, out-of-process MCP server in
`apps/connectors` would plan requests and normalize responses while the gateway performed
credentialed HTTP. **That protocol is not implemented.** There is no package, no image, no
deployed service, and no connector pack declares `tier: trusted_worker`. The compiled
`WorkerExecutor` still runs ADR-0014's `x-obol-upstream-authorization` hand-off. Nothing
leaks today, because no worker exists to receive the header.
The rest of this page records the decision, the unbuilt protocol, and the shipped hand-off
the executor would use if a worker were added. Do not treat either as a running product
surface.
The intended job is connector *velocity* on the messy minority of vendors, and nothing
else. No Python and no per-vendor logic goes in the gateway process ([invariant
2](/concepts/invariants)) — that decision is settled and `deny.toml` bans `pyo3` by name.
## What a worker is for
Under ADR-0024, a trusted worker would be a **request planner and a response
normalizer**. That is the whole job. The [tier decision](/connectors/tiers) would admit it
only when exactly one of four things is true:
- a mandatory 2–4 step sequence,
- cursor pagination that must be joined server-side into one result,
- a response shape needing a stable mapping, or
- a request shape OpenAPI genuinely cannot express.
Pagination the agent can page itself is not a reason. Only *mandatory* joins are.
## What is not a reason to write a worker
Older guidance — ADR-0007's own text included — predates ADRs 0020 through 0024. Each of
the following is gateway-owned by decision.
| Not a worker reason | Owner |
|---|---|
| OAuth exchange and refresh | Gateway (ADR-0023) — see [OAuth](/security/oauth) |
| Webhook receipt and signature verification | Gateway (ADR-0021, ADR-0022) |
| Credential injection, SSRF, egress policy, redirects | Gateway (ADR-0024 destination, ADR-0020). **That protocol is not implemented** |
| Idempotency keys, retries, receipts, redaction | Gateway (ADR-0019, ADR-0031) |
| Pagination the agent can do itself | Nobody — it is not a reason |
A vendor whose request signing Obol cannot express as an auth profile is a **reviewed
exception requiring a new ADR, not a secret hand-off**. AWS SigV4 is the standing
example: it signs a canonical request with the secret, so either the gateway gains an
`AwsSigV4` auth profile or the vendor is unsupported.
## The credential boundary
**On the shipped path a worker *does* receive the vendor credential.** ADR-0024 requires
that it never should — but that protocol is not implemented, and the code still runs
ADR-0014's hand-off, sending the rendered credential in `x-obol-upstream-authorization`.
See [the shipped path](#the-shipped-path-is-still-adr-0014) below for exactly what
crosses the hop today.
What holds on both paths: a worker cannot assign trust, evaluate Cedar, mint a receipt,
or choose a verification conclusion, and it never sees an inbound `ob_live_` key.
Nothing is exposed by this today, because no worker exists to receive the header:
`apps/connectors` ships no package and no image, and no deployment target defines a
connector-worker service. The boundary below is what ADR-0024 requires before one
returns.
### Target design (ADR-0024, not implemented)
ADR-0024's versioned protocol would be a bounded exchange in which the worker never holds
a credential and never dials a vendor:
The gateway would send the invocation, tool, canonical arguments, bundle and plan digest,
and idempotency key to the worker's `prepare` phase.
The worker would return a `RequestPlan`: a declared operation id, method, relative path,
selected safe headers, and a bounded body. Never an absolute arbitrary destination, and
never a credential.
The gateway would validate the plan against the pinned `ConnectorBundle` — endpoint and
method allowlists, schema, step budget, idempotency binding — then inject the Obol-vaulted
credential and perform the HTTP itself.
The gateway would retain numeric status, selected headers, safe ids, body digest, and a
temporary bounded body as **gateway-observed** transport evidence.
Where normalization is required, the gateway would send only the bounded response data the
bundle allows to `normalize`. The worker would return a `WorkerCallReport` that the
gateway converts into a `DispatchObservation`.
Multi-step connectors would use a **bounded host-call loop declared in the bundle**, each
step independently validated and receipted as part of the same invocation, with worker
deployments denying public egress and the gateway the only caller over the private service
boundary.
The split that would result: worker-derived semantic fields `connector_attested`; HTTP
status, digest, and gateway-selected response fields `gateway_observed`. On today's code
path the whole observation is `connector_attested` instead — see [Evidence a worker route
can carry](#evidence-a-worker-route-can-carry).
## The shipped path is still ADR-0014
**Treat every new worker as a credential-custody decision.** ADR-0024's
`prepare`/`normalize`/`RequestPlan`/`WorkerCallReport` protocol is not implemented in
this tree — there is no `RequestPlan` and no `WorkerCallReport` type in the workspace.
`obol-types/src/worker.rs` still carries the ADR-0014 `WorkerCallRequest` /
`WorkerCallResponse` shape, and `WorkerExecutor` sends the rendered vendor credential in
`x-obol-upstream-authorization` (`obol-mcp/src/worker/transport.rs`). The worker calls the
vendor itself, and netguard is skipped for `Worker` targets because the gateway is not
the one dialling out (`obol-gateway/src/hooks.rs`).
Do not extend the ADR-0014 shape. ADR-0024 supersedes it; the code has not caught up.
Under the shipped ADR-0014 hand-off, two credentials cross the gateway→worker hop and
neither is an Obol virtual key — workers never see inbound `ob_live_` keys:
- `Authorization: Bearer ` — minted per call by `ServiceJwtMinter` with
`aud = obol-connectors`, 60-second TTL, HS256 on the shared `OBOL_SERVICE_JWT_SECRET`.
It authenticates the *gateway*.
- `x-obol-upstream-authorization` — the rendered vendor credential, present only when the
connection has one, marked `sensitive`, never logged, and never persisted by the
worker.
The credential rides in a header rather than the body deliberately: bodies get logged,
buffered, and snapshotted by frameworks far more often than headers, and MCP handlers see
bodies but not headers. The only log line carries `worker, connection, tool,
idempotency_key, last4`, and an invariant test (`upstream_authorization_never_logged`)
holds that line.
Retry policy under ADR-0014 is exactly one retry after 200 ms, and only when the worker
declared the error retryable or the failure happened *before* the request was sent. A
post-send transport failure is never retried — the worker may have executed. ADR-0031
moves retry reservation into the gateway: each attempt is reserved before dispatch, and
a reservation failure means no second attempt is sent.
## Evidence a worker route can carry
`WorkerExecutor` stamps `EvidenceTrust::ConnectorAttested` on the observation
(`obol-mcp/src/worker/mod.rs`). Two consequences follow.
- **`connector_attested` verifies only by explicit permission.** The `EffectSpec` must
set `trust: allow_connector_attested`, or `trust_is_authoritative` rejects it. A worker
tool whose spec says `gateway_observed_only` can never verify on today's code path,
because the whole observation is attested rather than just its derived fields — the gap
ADR-0025's per-source trust exists to fix.
- **`connector_attested` may never contradict.** Only `gateway_observed` and
`vendor_signed` evidence may assert `contradicted`.
A worker's route is also the only place `connector_attested` may originate. Routing
federated evidence through an adapter in `apps/connectors` to acquire it is laundering
and is forbidden by name (ADR-0030): a federated route may never yield
`connector_attested`. See [Evidence classes](/receipts/evidence).
## Worker obligations
These are the ADR-0014 contract the compiled `WorkerExecutor` expects. No worker exists
to honour them; they would apply if one were added on the shipped path, not under
ADR-0024's unbuilt protocol.
- **An idempotency store keyed on `idempotency_key`.** The gateway reuses the same key on
a retry precisely so the worker's store collapses duplicates. This is load-bearing, not
optional.
- **No retries on non-idempotent writes** without a vendor idempotency key.
- **A stable error envelope.** Vendor errors map to `{code, retryable, message}` plus
`vendor_http_status`; `ok: false` becomes an upstream dispatch error. A missing
`structuredContent` is a transport error.
- **Never log an `Authorization` header or a PAN.**
Note that `idempotency: unsupported` forecloses redispatch entirely:
`gateway_retry_allowed` computes `vendor_retry_safe` as "the binding is not
`Unsupported`," so an ambiguous responded-but-unknown result is never re-attempted. That
is correct behavior, and a real product limitation worth recording in the connector note.
## Guardrails
- **Hand-written workers are capped at five**, each with a named reviewer, enumerated in
the connector registry. A sixth proposal is a scope conversation.
- **A worker is not a new deployable.** When `apps/connectors` is built it will be one
image hosting many connectors behind tool prefixes, scaled and circuit-broken
independently — not one deployable per vendor. A standalone compiler, registry, catalog
adapter, or verification service is prohibited without a new ADR (ADR-0005, ADR-0022).
- **Conformance fixtures are mandatory**, and 2–4 days plus a security review is the
realistic cost. Compare that against an overlay, which is hours.
## There is nothing to deploy yet
The tier is parked (ADR-0058). `apps/connectors/` contains one README and nothing else. There is no package, no lockfile,
no `obol_connectors` module, and no Dockerfile — the previous Dockerfile was a build recipe
for source that never existed, and was removed along with the `connectors` service in
Compose, `infra/fly/connectors.fly.toml`, and Helm's `connectors.yaml` with its values
block, ServiceAccount and NetworkPolicy. No deployment target sets `OBOL_CONNECTORS_URL`.
No connector pack under `packages/connectors/` declares `tier: trusted_worker` either —
all 103 bundles are `native`/`obol`. Every shipped connector executes in the gateway's
in-process OpenAPI executor or federates over remote MCP; neither path touches this
directory.
`apps/connectors/README.md` records the four preconditions for an image to return:
ADR-0024's protocol frozen in `packages/proto`, a Python package serving `/mcp/{worker}`
and `/healthz`, a Dockerfile citing ADR-0024 rather than ADR-0014, and deployment wiring
restored across all three `infra/` targets in the same change.