Chio/Docs
LOGIN · JOIN

HTTP Proxy Reference

Configure HTTP policy, authentication, receipts, and failure handling for the Chio sidecar.

Reference chio@5f63e6e

On this page

The HTTP proxy is the sidecar that chio api protect runs in front of an HTTP API. It matches each request to a route in the API's OpenAPI document, requires a capability on side-effect routes, signs an HttpReceipt for the decision, and forwards only admitted requests.

The upstream keeps its own authentication and business rules, and receives the original request with the capability stripped. Protect an API walks one service through the proxy from start to finish.

Synopsis

The proxy needs an upstream and a durable receipt store. It discovers routes from an OpenAPI document, classifies each request as safe or side-effect, checks a presented capability where one is required, signs the receipt, and proxies the request with the receipt id on the response. The upstream service is unmodified.

Command synopsistext
chio api protect --upstream <url> --receipt-store <path> [--spec <file>] [--listen <addr>]

A deployment adds a stable signing seed, an explicit spec, and a listener the callers can reach:

Terminalbash
chio --authority-seed-file ./chio-seed.hex \
    api protect \
    --upstream https://orders.internal:8443 \
    --spec ./openapi.yaml \
    --listen 0.0.0.0:9090 \
    --receipt-store ./receipts.sqlite \
    --upstream-timeout-secs 20

Request path

Reverse proxyChoose an outcome.

κcapabilityσscopegguardsBbudget

A matched write forwards with its receipt

The proxy matches the method and path against its route table, then the kernel checks the capability in X-Chio-Capability. An allowed request is forwarded with that header stripped, and the response returns with its receipt id. HTTP Proxy.

One request through the reverse proxy to the kernel and the upstream API. The proxy fails closed; a caller that can reach the upstream directly is outside this boundary, and identity extraction is left out.

Each request passes route match, caller identity, policy, the capability check, and the receipt, in that order, and the proxy fails closed. The route matcher walks the route table in order and matches both the HTTP method and a segmented path pattern with {param} placeholders. GET /pets/42 matches GET /pets/{petId} and inherits that route's policy.

Matching is case-sensitive and trailing slashes count, so /pets/ and /pets are distinct patterns. A request that matches no route falls back to a method-based default:

  • Safe methods (GET, HEAD, OPTIONS) get SessionAllow and pass through with an audit receipt.
  • Side-effect methods (POST, PUT, PATCH, DELETE) get DenyByDefault and require a valid capability token in X-Chio-Capability.

An unknown side-effect route still passes through the capability gate, and a grant that passes it can allow the request. Leaving a path out of the spec isolates nothing; the network boundary in front of the upstream is what keeps callers from reaching it directly.

Both verdicts, end to end

This captured run uses a two-route orders spec: a safe GET /orders, and a POST /orders/{orderId}/refund marked x-chio-side-effects: true. The read passes through, and the upstream response comes back with a receipt id:

api-protect · read-allowedtranscript
$ curl -s -i http://127.0.0.1:9090/orders
HTTP/1.1 200 OK
server: BaseHTTP/0.6 Python/3.13.13
date: Sat, 05 Sep 2026 06:26:37 GMT
content-type: application/json
content-length: 22
x-chio-receipt-id: 3c908e3471ee8edbf91f726b6841e004e5a28744d89df6705865776259dd2297

{"orders": ["ord-42"]}
exit 0

The write without a capability never reaches the upstream. The 403 body is a structured refusal that names the missing input and both channels that accept it:

api-protect · write-deniedtranscript
$ curl -s -i -X POST http://127.0.0.1:9090/orders/ord-42/refund \
    -H 'content-type: application/json' \
    -d '{"amount_cents": 4200}'
HTTP/1.1 403 Forbidden
content-type: application/json
x-chio-receipt-id: 99a121361c82426bdfc3063a7bce333363583cb80b5e3d9f4a399b4025abfc56
content-length: 283
date: Sat, 05 Sep 2026 06:26:37 GMT

{"error":"chio_access_denied","message":"side-effect route requires a capability token","receipt_id":"99a121361c82426bdfc3063a7bce333363583cb80b5e3d9f4a399b4025abfc56","suggestion":"provide a valid capability token in the X-Chio-Capability header or chio_capability query parameter"}
exit 0

With a valid capability in X-Chio-Capability, the same request forwards. The proxy strips the header before the hop, so the upstream never sees it:

api-protect · write-allowedtranscript
$ CAP=$(jq -c .capability capability.json)
$ curl -s -i -X POST http://127.0.0.1:9090/orders/ord-42/refund \
    -H 'content-type: application/json' \
    -H "X-Chio-Capability: $CAP" \
    -d '{"amount_cents": 4200}'
HTTP/1.1 200 OK
server: BaseHTTP/0.6 Python/3.13.13
date: Sat, 05 Sep 2026 06:26:37 GMT
content-type: application/json
content-length: 49
x-chio-receipt-id: a8fcfc7de60a1d06f66f6b9ab7fd3b788d94cd90ec1ff5b7e5239e126c355859

{"status": "refund_issued", "refund_id": "rf-99"}
exit 0

Three exchanges left three signed receipts, one of them a refusal. Caller identity and capabilities mints the capability, and HTTP receipts reads the receipts back.

Command options

FlagDefaultRequiredSets
--upstream
none
Yes
Base URL of the protected service
--spec
auto-discover
Recommended
Local OpenAPI spec, YAML or JSON
--listen
127.0.0.1:9090
No
Address for inbound proxy traffic
--receipt-store
none
Yes, at boot
SQLite path for durable receipts
--allow-ephemeral-receipts
false
No
In-memory receipts, lost on restart
--upstream-timeout-secs
20
No
Ceiling on one upstream hop, in seconds
--authority-seed-file
ephemeral
With durable receipts
Hex Ed25519 seed for the signing key
  • --upstream takes the base URL; the proxy appends each inbound request's path and query when it forwards.
  • --spec omitted, the proxy probes well-known locations on the upstream for an OpenAPI document.
  • --receipt-store is required at boot. Without it, chio api protect refuses to start ("refusing to start without durable receipts") unless --allow-ephemeral-receipts accepts in-memory receipts for a local development run.
  • --upstream-timeout-secs is wall-clock and includes reading the full response.
  • --authority-seed-file is a top-level chio flag. A stable seed keeps the receipt signing key the same across restarts, so stored receipts keep verifying.

A successful start prints nothing to stdout or stderr: the process loads the spec, builds the route table, and binds the listener. Ask the sidecar instead. GET /chio/live answers as soon as the process is up, and GET /chio/health adds the storage backends the embedded kernel resolved.

api-protect · healthtranscript
$ curl -s http://127.0.0.1:9090/chio/health
{"status":"healthy","version":"0.1.0","receipt_backend":"durable","revocation_backend":"durable"}
exit 0

receipt_backend and revocation_backend read durable when a store is attached, which catches a sidecar that came up on ephemeral state. The liveness route reports both fields as empty strings, because it is process-only and reads no storage.

chio start runs the same sidecar router with zero-config defaults for SDK quickstarts and local development: capability mint, release, and validate; receipt verify; tool-call evaluate; and the approval endpoints. It runs no upstream proxy, so its catch-all route returns 502. Durable receipts are on by default there too. Use chio api protect with --upstream and --spec to reverse-proxy an API.

Route policy

The proxy derives HTTP policy from the OpenAPI spec and its x-chio-* extension fields, so you annotate the spec instead of writing HushSpec rules per route. The default rule set follows HTTP semantics:

MethodDefault policyCapability required
GET, HEAD, OPTIONS
SessionAllow
No
POST, PUT, PATCH, DELETE
DenyByDefault
Yes

Override the default per operation with x-chio-* fields. This fragment opts a GET that returns sensitive data into deny-by-default, and gates the DELETE by capability. Keep the spec current: it sets route policy as well as client documentation, and an absent route falls back to the method default.

openapi.yamlyaml
openapi: 3.1.0
info:
  title: Orders API
  version: 1.0.0

paths:
  /orders:
    get:
      operationId: listOrders
      summary: List orders in the current tenant.
      responses:
        "200": { description: OK }

  /orders/{orderId}:
    get:
      operationId: getOrder
      x-chio-sensitivity: sensitive
      x-chio-side-effects: true
      responses:
        "200": { description: OK }

    delete:
      operationId: deleteOrder
      x-chio-approval-required: true
      x-chio-budget-limit: 10000  # metadata only on this HTTP path
      responses:
        "204": { description: Deleted }

  /orders/{orderId}/refund:
    post:
      operationId: refundOrder
      x-chio-side-effects: true
      x-chio-budget-limit: 50000  # metadata only on this HTTP path
      responses:
        "200": { description: Refund issued }

chio-openapi parses five x-chio-* fields off an operation. Two reach the sidecar route table, because the proxy turns each operation into a pattern, a method, and one policy decision.

FieldRead by the proxyEffect on this path
x-chio-side-effects
Yes
Overrides the method default
x-chio-approval-required
Yes
Forces DenyByDefault first
x-chio-sensitivity
No
Parsed, then dropped
x-chio-budget-limit
No
Parsed as minor units, then dropped
x-chio-publish
No
Governs manifest generation
  • x-chio-side-effects: true forces DenyByDefault on a GET; false forces SessionAllow on a POST.
  • x-chio-approval-required forces DenyByDefault ahead of everything else, including x-chio-side-effects. A valid capability still forwards the request in the same exchange.
  • x-chio-sensitivity parses into Sensitivity (public, internal, sensitive, restricted), changes no verdict, and reaches no receipt.
  • x-chio-budget-limit is metadata here. Invocation and spending budgets are enforced on a kernel-mediated tool path; see Budgets & Metering.
  • x-chio-publish leaves the route matched and governed. Bridge OpenAPI to MCP honors it when it generates manifests.

Every governed request projects onto one synthetic tool call, so a capability names the tool authorize_http_request on the server chio_http_authority, whatever the route. A token whose grant names anything else is refused with capability does not authorize tool authorize_http_request on server chio_http_authority.

The proxy checks the presented token's issuer, signature, time window, revocation, and matching scope. The projection consumes none of the token's invocation or spending budget, so bound an HTTP capability with a short lifetime and revoke it explicitly when its work ends.

Caller identity and capabilities

The proxy layers capability authorization over the upstream's own authentication. Authentication answers who the caller is, through Authorization: Bearer ..., API keys, mTLS, or whatever the service already requires. Capability authorization answers whether this caller holds a signed grant for this action now, carried in the X-Chio-Capability header or the chio_capability query parameter.

The proxy derives a caller subject from standard auth signals, so each receipt binds the caller to the capability it presented. The precedence is:

  1. Authorization: Bearer <token>: the proxy hashes the token, stores only the hash, and emits a subject such as bearer:3a7c8f....
  2. X-Api-Key, in that one spelling: the proxy hashes the key and emits apikey: plus the same sixteen hex characters.
  3. Neither present, or the value is malformed: the subject is anonymous.

The subject is a prefix plus the first sixteen characters of the SHA-256 hex of the credential. The receipt carries caller_identity_hash, a SHA-256 over the whole identity record, so an anonymous caller still hashes to a stable value.

Auth headers flow through unchanged, and the upstream validates its own Authorization. The proxy strips only the capability: X-Chio-Capability from the headers and chio_capability from the query string.

The capability travels as the token document itself, compact JSON, which the proxy parses straight into a CapabilityToken. Mint one against the sidecar and it comes back ready to present:

api-protect · minttranscript
$ curl -s -X POST http://127.0.0.1:9090/v1/capabilities/mint \
    -H 'content-type: application/json' \
    -d '{"subject": "agent-refunds", "scopes": ["tool:chio_http_authority:authorize_http_request:invoke"],
$          "ttl_seconds": 300, "job_uid": "refund-batch-1"}' \
  | tee capability.json
{"capability":{"schema":"chio.capability.v1","id":"sidecar-f80ee84531c5d4ad1b7f068f3187c2fcf7eb4f0d0ac75a5e83ba01397b9525f0","issuer":"04c8e7aadf96a0af7d148b8b25e0cc0e2509bbdce3e1266661168c262c822b7e","subject":"b16122b44a030ccf12519713adec3b232a3482775134765081dff99043ae8575","scope":{"grants":[{"server_id":"chio_http_authority","tool_name":"authorize_http_request","operations":["invoke"]}]},"issued_at":1788589597,"expires_at":1788589897,"signature":"5be928657bd06d26eecf43d4e9af3ecd8aea4d8d306523f95dd2a997d42bec9a0842d8f8b86b187aad32b408678b0cf7d0c71e39ad0dea75583ad3b0fb1e3f06"}}
exit 0

issuer, subject, and signature are bare lowercase hex, with no ed25519: or 0x prefix. The grant names chio_http_authority and authorize_http_request, the pair the proxy checks on every governed route.

HTTP receipts

Every evaluated request produces at least one HttpReceipt, signed with Ed25519. The proxy signs a decision receipt before the upstream call and a final receipt once the status is known, and persists the final one. The final receipt names the decision it finalized in metadata.chio_decision_receipt_id. A denial is finalized the same way, so a refusal is a stored receipt as well as a status code.

This is the receipt the refused refund wrote, with its route_selection metadata block dropped for length and nothing else edited:

api-protect · http-receipttranscript
$ python3 -c 'import json, sqlite3, sys
$ rows = sqlite3.connect("receipts.sqlite").execute(
$     "SELECT receipt_json FROM http_receipts").fetchall()
$ deny = [json.loads(r[0]) for r in rows
$         if json.loads(r[0])["verdict"]["verdict"] == "deny"][0]
$ deny["metadata"].pop("route_selection", None)
$ json.dump(deny, sys.stdout, indent=2)'
{
  "id": "99a121361c82426bdfc3063a7bce333363583cb80b5e3d9f4a399b4025abfc56",
  "request_id": "01a0703f-6162-76e1-a2b9-c95d7b5e3ffc",
  "route_pattern": "/orders/{orderId}/refund",
  "method": "POST",
  "caller_identity_hash": "5a45e80d1ab1a73e8c2644f3f6c6e53974d0a986c2fc86692372d3f130248c9c",
  "verdict": {
    "verdict": "deny",
    "reason": "side-effect route requires a capability token",
    "guard": "CapabilityGuard",
    "http_status": 403
  },
  "receipt_kind": "mediated_decision",
  "boundary_class": "prevent",
  "tool_origin": "caller_executed",
  "redaction_mode": "none",
  "evidence": [
    {
      "guard_name": "CapabilityGuard",
      "verdict": false,
      "details": "side-effect route requires a valid capability token"
    }
  ],
  "response_status": 403,
  "timestamp": 1788589597,
  "content_hash": "37458db593fb679bf8629b8f345d39c9f32c264e9b045340dc4d11248ccf8433",
  "policy_hash": "215a1eecb13caf8340c4905407b0c03f2a6309866ff2ff0a5a5f9735ea3b3566",
  "trust_level": "mediated",
  "metadata": {
    "chio_decision_receipt_id": "ce578445a868ef2b0575a99adc04da2322455c258f019d0d976feff7b11573bc",
    "chio_http_status_scope": "final",
    "chio_kernel_receipt_id": "7ff5f3b5fed6b5d74c6048eae04a6e0ed26ec2c02016d7c13689f4401009ec0f"
  },
  "kernel_key": "04c8e7aadf96a0af7d148b8b25e0cc0e2509bbdce3e1266661168c262c822b7e",
  "signature": "59f7b28344c224e37816ef87ef36a12971cf6880e24f731ded1d9a4478850103942b62c9344c044741c8d41917b949698c82bb83d25a7990dccf0b3f18406b09"
}
exit 0deny
  • id: the SHA-256 of the canonical receipt body with id removed, 64 lowercase hex characters. The proxy returns it to the caller in X-Chio-Receipt-Id.
  • request_id: a dashed UUIDv7, minted per request unless a signed execution nonce carries one in.
  • route_pattern: the matched OpenAPI pattern, which keeps cardinality bounded for analytics. An unmatched path records the literal path.
  • verdict: on a deny, { verdict, reason, guard, http_status }. Here guard is the individual guard, CapabilityGuard or DefaultPolicyGuard, and it repeats in evidence.
  • caller_identity_hash: SHA-256 over the canonical JSON of the whole caller identity record.
  • content_hash: SHA-256 over the canonical request binding of method, route pattern, path, query, and body hash.
  • policy_hash: SHA-256 of the spec bytes the proxy loaded at start. A rotated spec changes the hash, which a downstream verifier sees.
  • receipt_kind, boundary_class, tool_origin, redaction_mode, and trust_level: five required classification fields. /chio/verify checks three of them before it reports ok.
  • session_id: always absent on the proxy path, which sets it to None and skips it when serializing.

The proxy writes HTTP receipts to the http_receipts table in its SQLite store, and exported evidence embeds them as chio_http_receipt_v1. With local access to that database, select the complete signed object with SQLite and jq and post it to the verifier. Set the database path and receipt ID from your deployment; this example sidecar listens on port 9097.

Terminalbash
HTTP_RECEIPT_DB=/path/to/sidecar-receipts.db
RECEIPT_ID=replace-with-receipt-id
sqlite3 -readonly "$HTTP_RECEIPT_DB" \
  'SELECT receipt_json FROM http_receipts ORDER BY rowid' \
  | jq -s -e --arg id "$RECEIPT_ID" \
      'map(select(.id == $id)) | if length == 1 then .[0] else error("receipt not found uniquely") end' \
  > http-receipt.json
curl --fail-with-body -sS http://127.0.0.1:9097/chio/verify \
  -H 'Content-Type: application/json' --data-binary @http-receipt.json

Resolve a missing database, missing table, or unknown receipt ID before verifying, and keep the complete JSON document: the verifier needs every signed field. The query reads the local storage schema directly; the receipt-list route is on the trust-control service.

Captured verification of the complete denied-write receiptjson
{
  "signature_valid": true,
  "signer_trusted": true,
  "receipt_id_valid": true,
  "parameter_hash_valid": true,
  "receipt_kind": "mediated_decision",
  "boundary_class": "prevent",
  "trust_level": "mediated",
  "result": "deny",
  "authorized": false,
  "signer_key_hex": "7b70264765072667013a47099931db64653384951257575aa8aed21694614c83",
  "ok": true
}

ok is true because the recorded receipt verifies; authorized is false because its decision is a denial. The endpoint trusts the running sidecar's signing key, so verify a remote operator's receipts against keys you distribute yourself. The complete captured denial and the successful-write record retain every signed field.

Receipts hold hashes of Authorization and X-Api-Key, never the raw values. The upstream's own logs are outside the proxy: if they record request bodies that carry secrets or personal data, hash those fields before the proxy so the plaintext never reaches disk.

Kernel receipts for the same decision

chio receipt list reads the kernel receipt for each HTTP decision, which sits beside the HttpReceipt in the store. Its tool server is always chio_http_authority and its tool name always authorize_http_request. The route is one level down, in action.parameters.route_pattern, so filter there; a top-level route_pattern filter matches nothing.

api-protect · kernel-receipttranscript
$ chio --receipt-db ./receipts.sqlite receipt list --admin-all \
  | jq -c 'select(.action.parameters.route_pattern == "/orders/{orderId}/refund")
$            | {id, tool_server, tool_name, decision,
$               route: .action.parameters.route_pattern}'
{"id":"7ff5f3b5fed6b5d74c6048eae04a6e0ed26ec2c02016d7c13689f4401009ec0f","tool_server":"chio_http_authority","tool_name":"authorize_http_request","decision":{"verdict":"deny","reason":"guard denied the request: guard \"http_projection_policy\" error (fail-closed): guard denied the request: side-effect route requires a capability token","guard":"kernel"},"route":"/orders/{orderId}/refund"}
{"id":"1b409feb6b244d7758d988fc4d6c2fcb2df201782ac725be67b2cfc97806a03c","tool_server":"chio_http_authority","tool_name":"authorize_http_request","decision":{"verdict":"allow"},"route":"/orders/{orderId}/refund"}
{"id":"6fc6f7ad5e142d248a4b486a2f9e1d23eca9b0217c8e0c345d811f0e2b9b1dd9","tool_server":"chio_http_authority","tool_name":"authorize_http_request","decision":{"verdict":"deny","reason":"guard denied the request: guard \"http_projection_policy\" error (fail-closed): guard denied the request: capability signature verification failed","guard":"kernel"},"route":"/orders/{orderId}/refund"}
{"id":"077f7145b73f795a514594e2124513921b9089e39c5e3c89a2856fe0b0a668c5","tool_server":"chio_http_authority","tool_name":"authorize_http_request","decision":{"verdict":"deny","reason":"guard denied the request: guard \"http_projection_policy\" error (fail-closed): guard denied the request: invalid capability token: capability expired at 1788589598","guard":"kernel"},"route":"/orders/{orderId}/refund"}
exit 0deny

The HTTP receipt names its kernel receipt in metadata.chio_kernel_receipt_id. The kernel receipt's decision.guard reads kernel, because the HTTP verdict reaches the kernel as one projected guard result.

chio receipt list filters on --capability, --tool-server, --tool-name, --outcome, --since, --until, --min-cost, --max-cost, and --cost-currency, and pages with --limit and --cursor. A local read names its tenant scope with --tenant <id> or --admin-all and refuses without one. Shape a route query by server and tool, then refine with jq; the Receipt Query Cookbook has the recipes.

Deployment patterns

Public read, authorized write

Reads get audit receipts and writes require a capability. That is the default, so the spec needs no annotations:

Terminalbash
chio api protect --upstream https://catalog.internal:8080 \
    --spec ./catalog.openapi.yaml \
    --listen 0.0.0.0:9090 \
    --receipt-store ./catalog-receipts.sqlite

Agents call GET /products and GET /products/{sku} freely, and each completed decision leaves an audit receipt. Any POST, PATCH, or DELETE requires a signed capability and fails closed with a 403 receipt without one.

One sidecar per authority domain

On the proxy path, server_id and tool_name are pinned to chio_http_authority and authorize_http_request, so an unconstrained grant authorizes every governed route on that sidecar. The proxy checks the token's lifetime and supported scope constraints; the other ToolGrant fields apply on runtime paths that enforce them.

Give each authority domain its own sidecar, with separate tenant services, upstream credentials, network reachability, signer keys, and stores. A tenant-specific spec documents its routes, and unmatched routes keep the method-based fallback. Separate signers reject each other's tokens unless an operator configures shared trust. Treat minting as operator authority: a minted HTTP capability grants the synthetic operation on that sidecar, so keep its lifetime short.

Webhook and egress control

The proxy works in the outbound direction too. An agent that posts to a third-party webhook calls a protected egress proxy, which applies the same controls. Choose an upstream and a network boundary limited to the intended service: unmatched side-effect paths still accept a suitable capability, so a reduced spec is a route policy, and the network boundary is the destination allowlist.

Terminalbash
# Egress proxy in front of a third-party webhook target
chio --authority-seed-file ./egress-seed.hex \
    api protect --upstream https://api.thirdparty.example \
    --spec ./thirdparty-webhook.openapi.yaml \
    --listen 127.0.0.1:9191 \
    --receipt-store ./egress-receipts.sqlite

# The agent calls localhost; the proxy checks the capability and forwards outbound
curl -X POST http://127.0.0.1:9191/v1/webhooks/orders \
    -H "X-Chio-Capability: $(jq -c .capability ./webhook-cap.json)" \
    -H "Content-Type: application/json" \
    -d @event.json

Receipts bind the recorded request context of each call that crossed the proxy. Reachability of other network paths is a property of the host and network.

Sensitive reads require a capability

Annotate a GET that returns sensitive data so it stops being treated as safe:

openapi-sensitive.yamlyaml
paths:
  /users/{userId}/ssn:
    get:
      operationId: getUserSsn
      x-chio-sensitivity: restricted
      x-chio-side-effects: true
      x-chio-approval-required: true
      responses:
        "200": { description: OK }

The route now requires a capability, and because x-chio-approval-required is set it would deny by default even with x-chio-side-effects: false. A valid capability forwards the request in the same exchange. To hold a request for a person, use the approval endpoints on the sidecar listener, or chio_approval in chio-fastapi.

Sidecar wire protocol

Sidecar evaluationChoose an outcome.

κcapabilityσscopegguardsBbudget

The verdict comes back before the operation runs

The SDK posts a ChioHttpRequest to the sidecar’s /chio/evaluate route on 127.0.0.1:9090. The response carries the verdict, the signed receipt, and evidence; with allow, the application runs the operation. Sidecar HTTP Service.

One evaluation from an application’s SDK to the kernel in a sidecar process, which alone holds the signing key. The /chio/verify route and the health routes are left out.Sidecar HTTP Service

The same binary speaks a small JSON wire protocol on the sidecar listener, which SDK middleware uses to evaluate a request before its handler runs. Four /chio/* routes serve JSON on the default 127.0.0.1:9090 listener; override the address with the SDK's sidecarUrl, or the CHIO_SIDECAR_URL environment variable where the SDK reads one.

  • POST /chio/evaluate takes a ChioHttpRequest, which requires request_id, method, route_pattern, path, caller, and timestamp. It returns { verdict, receipt, evidence }, plus an execution_nonce when the kernel issues one. The status is HTTP 200 for allow and deny alike: the verdict field carries the outcome, and middleware fails closed on any other status.
  • POST /chio/verify checks a receipt and returns eleven booleans and strings. signature_valid covers the signature, the recomputed receipt ID, and authority semantics. ok adds signer_trusted, receipt_id_valid, parameter_hash_valid, and the semantic classification. authorized is ok narrowed to an allow verdict, so read authorized to learn that a call was permitted. None of the eleven is temporal: a valid signature on an expired receipt reports signature_valid: true.
  • GET /chio/health is dependency-aware readiness. It returns { status, version, receipt_backend, revocation_backend }, and 503 when the status is degraded or unhealthy.
  • GET /chio/live is process-only liveness, with the same body and empty backend fields. Point a liveness probe here and a readiness probe at /chio/health, so a dependency blip pulls the instance from rotation without restarting it.

SDK middleware calls these routes with a 5000 ms timeout and fails closed when the sidecar is unreachable or times out. The TypeScript and .NET middleware fail closed whatever their onSidecarError setting says, onSidecarError: "allow" included. Rust's chio-tower honors fail-open: with with_fail_open(true) the service logs and forwards the request unenforced.

Failure responses

The sidecar refuses instead of degrading, and names the input it was missing. Every chio error is a three-line envelope: the code and message, a context object, and a suggested fix.

No receipt store at startup

api-protect · no-receipt-storetranscript
$ chio api protect \
    --upstream http://127.0.0.1:8080 \
    --spec ./openapi.yaml \
    --listen 127.0.0.1:0
error[CHIO-CLI-RECEIPT-STORE-REQUIRED]: refusing to start without a durable receipt store
  help: pass --receipt-store <path> for a durable audit log, or --allow-ephemeral-receipts to keep receipts in memory, where a restart loses them
  code: urn:chio:error:cli:receipt-store-required
  docs: https://chio.computer/docs/reference/errors#error-cli-receipt-store-required
exit 1

Pass --receipt-store <path>, or --allow-ephemeral-receipts for a local run whose audit trail you accept losing on restart. A bad --spec path fails the same way, with urn:chio:error:transport:http-failed and failed to load OpenAPI spec.

A token whose bytes changed

api-protect · capability-tamperedtranscript
$ BAD=$(jq -c '.capability.subject = "00000000..."' capability.json)
$ curl -s -X POST http://127.0.0.1:9090/orders/ord-42/refund \
    -H "X-Chio-Capability: $BAD" -d '{}'
{"error":"chio_access_denied","message":"capability signature verification failed","receipt_id":"c751fa3734bff4c2716cc851363402515490fd5edbccbd4730a345f0a0ba88e6","suggestion":"provide a valid capability token in the X-Chio-Capability header or chio_capability query parameter"}
exit 0

The signature covers the whole token, so editing any field, the subject included, invalidates it. A token minted by a sidecar whose public key is outside this one's trusted issuer set gets the same message, which is what two instances without a shared --authority-seed-file produce.

A token past its expiry

api-protect · capability-expiredtranscript
$ EXPIRED=$(curl -s -X POST http://127.0.0.1:9090/v1/capabilities/mint \
    -H 'content-type: application/json' \
    -d '{"subject": "agent-refunds", "ttl_seconds": 1,
$          "scopes": ["tool:chio_http_authority:authorize_http_request:invoke"],
$          "job_uid": "short"}' | jq -c .capability)
$ sleep 3
$ curl -s -X POST http://127.0.0.1:9090/orders/ord-42/refund \
    -H "X-Chio-Capability: $EXPIRED" -d '{}'
{"error":"chio_access_denied","message":"invalid capability token: capability expired at 1788589598","receipt_id":"33b7fe82430c395b6c38cbe823ba106005139cc81cc54fbe7d41b4f7fff8d579","suggestion":"provide a valid capability token in the X-Chio-Capability header or chio_capability query parameter"}
exit 0

The refusal names the epoch second the token expired, so clock skew between the minting host and the sidecar shows up as a token that was never valid. Mint a fresh token; tokens have no refresh.

An unreachable or slow upstream

An allowed request whose upstream hop fails gets a 502, and the proxy still finalizes and stores the receipt with response_status: 502. A hop that runs past --upstream-timeout-secs (20 by default, including reading the whole response) lands the same way. An allow records authorization, and the 502 records that delivery failed: upstream reachability, a timeout, or an egress or transport restriction. Some failures occur before a final receipt exists, so read the error body with the receipt.

Source files for this reference
  • Route table, store tables, and spec hash: crates/products/chio-api-protect/src/proxy/state.rs (ProxyState::build_routes at lines 485 to 513, http_receipts at 144 to 147, policy_hash at 575).
  • Extension parsing: crates/protocol/chio-openapi/src/extensions.rs lines 26 to 40; approval precedence in chio-openapi/src/policy.rs lines 49 to 51.
  • Synthetic tool and projection: crates/platform/chio-http-core/src/authority.rs lines 33 to 35 and authority_projection.rs.
  • Caller identity: crates/products/chio-api-protect/src/evaluator.rs lines 540 to 575; session_id at line 363.
  • Capability header parsing: crates/products/chio-api-protect/src/proxy/http.rs lines 120 to 124.
  • Decision and final receipts: crates/products/chio-api-protect/src/proxy/router.rs lines 632 to 659; upstream failure at 661 to 676.
  • Receipt id and request binding: crates/platform/chio-http-core/src/receipt.rs lines 343 to 353 and request.rs lines 168 to 188.
  • Verification response: crates/platform/chio-http-core/src/evaluation.rs lines 33 to 45.
  • Tower fail-open: crates/protocol/chio-tower/src/service.rs lines 136 to 142.

Next: put the proxy in front of a service