Docs · Autopilot
The gateway as a Clearance runtime provider
Status: Built (A-1, 2026-11-03): the gateway passes Clearance’s runtime conformance suite in CI, against the released core image and the plain binary. Unverified on hangar-dev until a gateway tag with A-1 is deployed and a live run’s records are read back.
The Autopilot gateway is the enforcement point for agents. Clearance decides and records; the gateway asks it first, acts only on yes, and reports what happened. Clearance calls such an enforcement point a provider and writes the contract down in its provider protocol. This page is the gateway’s side of it: what it promises, how the records name it, and how the promise is tested.
What the gateway promises
| Rule | What the gateway does |
|---|---|
| Ask first | Every tool call, model call, session open and spawn is a POST /access/v1/evaluation before anything runs |
| Act only on yes | A handler runs only on decision: true; a deny returns the rule and hint and touches no backend |
| Apply the spend | The decision’s context.spend is applied to the session the gateway holds, allow or deny (budgets, a denial, the breaker) |
| One outcome per allowed decision | POST /v1/decisions/{id}/outcome once the call ran: ok, error:<Kind>, or a handler’s refusal. Tool calls and model calls alike |
| Fail closed | No answer, a timeout or a 5xx is a deny (CORE-UNAVAILABLE). The one exception is the recorded switch GATEWAY_OUTAGE_ALLOW_READS (unscoped T0 reads, posted later as coverage-unavailable) |
| Write its own runtime records | session.open, run.launch, run.delete, run.finish, session.end, session.freeze, human.answer, trigger.hold, trigger.release through POST /v1/records |
| Identify itself | A projected service-account token, audience the core’s CLEARANCE_AUDIENCE, for the principal CLEARANCE_PROVIDERS registers |
How the records name it
Every record has a provider ({name, version}), and the core sets it, never the gateway:
-
The gateway’s own records (outcomes, runtime records) carry the gateway’s registration from the core’s
CLEARANCE_PROVIDERS:{"name": "autopilot-gateway", "version": "<gateway tag>"}. On Hangar, Apron renders the entry fromversions.autopilot.gateway, so the version is the deployed image’s tag (for examplegateway-v0.4.1):[{"name": "autopilot-gateway", "version": "gateway-v0.4.1", "principals": ["autopilot-system/clearance"], "definitions": true}] -
Decision records are the core’s own (
providerclearance, its version), withcaller{"provider": "autopilot-gateway", "principal": ...}, which the core verifies. -
Model calls follow the tool-call shape: the decision is the core’s record with
tool: model.callandcaller.provider: autopilot-gateway; its outcome is the gateway’s record (providerautopilot-gateway at the gateway tag), linked byoutcome_of {seq, hash}. The outcome’sreceiptsays whether the call streamed and what the upstream answered, so a streamed call is provable from the chain:{"tool": "model.call", "result": "ok", "outcome_of": {"seq": 812, "hash": "..."}, "provider": {"name": "autopilot-gateway", "version": "gateway-v0.4.2"}, "receipt": {"stream": true, "upstream_status": 200}, "model_version": "claude-sonnet-5-5-20261015", "usage": {"input_tokens": 1000, "output_tokens": 50, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 3000, "charged_tokens": 1350}, "subject": {"kind": "model-call", "name": "claude-sonnet-5-5", "digests": {"sha256": "<args_sha256>"}}}Results:
ok(a 2xx answer, and for a stream one that ran to its end),error:Upstream<status>,error:ModelUnreachable,error:BadUpstreamAnswer,error:StreamInterrupted(the stream ended early: the run went away or the provider dropped it),error:UnsupportedBackend, andrefused M006when the gateway has no route for a model the core allowed.receiptis claimed (the gateway says it);result’s link to the decision and the copied identity fields are verified. A denied model call has no outcome.The subject kind is
model-call, named by the model the call asked for (itstarget, the route alias), with the digest of its arguments: the core derives it fortool: model.callfrom evidence schema revision 3 (clearance#53, A-4). The gateway sends no subject; the decision and its outcome both carry it. Older rows keep thetool-callsubject they were written with. The same outcome carries A-4’smodel_version(the model that answered) and, for a call that used tokens,usage(the four token kinds andcharged_tokens, the call’s weighted charge), all claimed.
The start line (serve.describe) prints provider=autopilot-gateway/<version>: the image’s own version (the
gateway-v* tag it was built from, AUTOPILOT_GATEWAY_VERSION). It should equal the core’s registration. When it
does not, records name the registration, not the image: fix CLEARANCE_PROVIDERS (Apron versions.autopilot.gateway)
with the image pin.
Closing a session from an MCP client
session_close (MCP tool) ends one of the caller’s own sessions now instead of at its TTL: a person’s MCP client
that opened a session, or an in-cluster run closing its own. It goes through the same path as the person control
End: the session and its children close and session.end is written through the core (result
ended: <reason>, the caller as principal); then its in-cluster runs and its children’s are deleted as the expiry
sweep deletes them, each with a run.delete record. Only the session’s own caller may; anyone else, the agent’s
owner group and platform admins included, is refused (CT-FORBIDDEN) and uses End in Tower.
How it is tested: the conformance suite
clearance conformance --provider <url> --role runtime (shipped in every Clearance release) starts an in-memory core
and a fake target and drives the provider through a test-only control interface: a denied call, an allowed call, the
outcome, the spend, an unavailable and an unreachable core, and the chain at the end. The gateway serves that
interface from its own subcommand:
GATEWAY_CONFORMANCE_CONTROL=1 autopilot-gateway conformance-control --listen 127.0.0.1:9091
clearance conformance --provider http://127.0.0.1:9091 --role runtime --report conformance.json
Test only, off by default. The subcommand refuses to start unless GATEWAY_CONFORMANCE_CONTROL=1, binds a
loopback address only, on its own port, and never starts the deployable gateway: it reads no GATEWAY_DSN, core
token, model key or backend credential. Each apply runs the gateway’s own tool-call path (Gateway.call) on a
throwaway in-memory gateway against the core and target the harness names, with the bearers it hands over; the only
thing the harness controls is the evaluation the gateway sends. No chart, manifest or Apron template sets the switch
(tests/test_conformance.py checks the repo).
CI (.github/workflows/test.yaml) runs the suite twice: with the core binary built from the pinned Clearance tag,
and with the released image ghcr.io/jfillman/clearance:<tag> (docker run --network host ... conformance). The
reports are the job’s conformance-reports artifact and its step summary. A broken variant that acts on a deny must
fail the suite (runtime.deny-not-acted), so a pass is not vacuous.
Output against clearance v0.30.0:
PASS control.info the provider serves its control interface for this role: autopilot-gateway gateway-v0.4.2-test, roles [runtime]
PASS runtime.deny-not-acted a denied call is not acted on: decision 0 deny R006, state "refused", target applies 0, outcome records 0
PASS runtime.allow-acted-once an allowed call is acted on exactly once: decision 1 allow=true, state "applied", target applies 1
PASS runtime.outcome-references-decision the outcome record references the decision: outcome seq 2 -> decision seq 1, hash linked true, provider autopilot-gateway, result "ok"
PASS runtime.spend-applied the spend each decision carries is applied: deny: applied {"denial":true}, the core asked for {"denial":true}; allow: applied {"tool_calls":1,"github_calls":4,"open_prs":1}, the core asked for {"tool_calls":1,"github_calls":4,"open_prs":1}
PASS runtime.outage-fails-closed an unavailable or unreachable core fails closed: rt-outage-503: state "refused", target applies 0; rt-outage-drop: state "refused", target applies 0
PASS chain.verify the evidence chain verifies from genesis: 3 records, head ba0bf181..., 1 written as autopilot-gateway
PASS clearance-conformance/v1 runtime provider http://127.0.0.1:9091 (autopilot-gateway gateway-v0.4.2-test): 7 assertions, 0 failed
Not built
provider.conformanceon records: reserved in Clearance’s v1 schema;CLEARANCE_PROVIDERShas no field for it yet. The report’ssuite(clearance-conformance/v1) is the value to stamp.- The suite’s model-call scenario: the harness drives tool calls only; model-call outcomes are tested in this
repo (
tests/engine/test_run_identity.py).