HTTP Proxy Reference
Configure HTTP policy, authentication, receipts, and failure handling for the Chio sidecar.
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.
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:
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 20Request path
κ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.
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) getSessionAllowand pass through with an audit receipt. - Side-effect methods (
POST,PUT,PATCH,DELETE) getDenyByDefaultand require a valid capability token inX-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:
$ curl -s -i http://127.0.0.1:9090/ordersHTTP/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"]}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:
$ 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"}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:
$ 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"}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.
Try upstream-timeout-secs, x-chio-side-effects, X-Chio-Capability, or /chio/verify.
Command options
| Flag | Default | Required | Sets |
|---|---|---|---|
--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- | false | No | In-memory receipts, lost on restart |
--upstream- | 20 | No | Ceiling on one upstream hop, in seconds |
--authority- | ephemeral | With durable receipts | Hex Ed25519 seed for the signing key |
--upstreamtakes the base URL; the proxy appends each inbound request's path and query when it forwards.--specomitted, the proxy probes well-known locations on the upstream for an OpenAPI document.--receipt-storeis required at boot. Without it,chio api protectrefuses to start ("refusing to start without durable receipts") unless--allow-ephemeral-receiptsaccepts in-memory receipts for a local development run.--upstream-timeout-secsis wall-clock and includes reading the full response.--authority-seed-fileis a top-levelchioflag. 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.
$ curl -s http://127.0.0.1:9090/chio/health{"status":"healthy","version":"0.1.0","receipt_backend":"durable","revocation_backend":"durable"}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:
| Method | Default policy | Capability 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: 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.
| Field | Read by the proxy | Effect on this path |
|---|---|---|
x- | Yes | Overrides the method default |
x- | Yes | Forces DenyByDefault first |
x- | No | Parsed, then dropped |
x- | No | Parsed as minor units, then dropped |
x-chio-publish | No | Governs manifest generation |
x-chio-side-effects: trueforcesDenyByDefaulton aGET;falseforcesSessionAllowon aPOST.x-chio-approval-requiredforcesDenyByDefaultahead of everything else, includingx-chio-side-effects. A valid capability still forwards the request in the same exchange.x-chio-sensitivityparses intoSensitivity(public,internal,sensitive,restricted), changes no verdict, and reaches no receipt.x-chio-budget-limitis metadata here. Invocation and spending budgets are enforced on a kernel-mediated tool path; see Budgets & Metering.x-chio-publishleaves 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:
Authorization: Bearer <token>: the proxy hashes the token, stores only the hash, and emits a subject such asbearer:3a7c8f....X-Api-Key, in that one spelling: the proxy hashes the key and emitsapikey:plus the same sixteen hex characters.- 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:
$ 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"}}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:
$ 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"
}id: the SHA-256 of the canonical receipt body withidremoved, 64 lowercase hex characters. The proxy returns it to the caller inX-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 }. Hereguardis the individual guard,CapabilityGuardorDefaultPolicyGuard, and it repeats inevidence.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, andtrust_level: five required classification fields./chio/verifychecks three of them before it reportsok.session_id: always absent on the proxy path, which sets it toNoneand 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.
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.jsonResolve 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.
{
"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.
$ 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"}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:
chio api protect --upstream https://catalog.internal:8080 \
--spec ./catalog.openapi.yaml \
--listen 0.0.0.0:9090 \
--receipt-store ./catalog-receipts.sqliteAgents 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.
# 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.jsonReceipts 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:
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
κ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.
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/evaluatetakes aChioHttpRequest, which requiresrequest_id,method,route_pattern,path,caller, andtimestamp. It returns{ verdict, receipt, evidence }, plus anexecution_noncewhen the kernel issues one. The status is HTTP 200 for allow and deny alike: theverdictfield carries the outcome, and middleware fails closed on any other status.POST /chio/verifychecks a receipt and returns eleven booleans and strings.signature_validcovers the signature, the recomputed receipt ID, and authority semantics.okaddssigner_trusted,receipt_id_valid,parameter_hash_valid, and the semantic classification.authorizedisoknarrowed to an allow verdict, so readauthorizedto learn that a call was permitted. None of the eleven is temporal: a valid signature on an expired receipt reportssignature_valid: true.GET /chio/healthis dependency-aware readiness. It returns{ status, version, receipt_backend, revocation_backend }, and503when the status isdegradedorunhealthy.GET /chio/liveis 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
$ chio api protect \
--upstream http://127.0.0.1:8080 \
--spec ./openapi.yaml \
--listen 127.0.0.1:0error[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
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
$ 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"}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
$ 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"}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_routesat lines 485 to 513,http_receiptsat 144 to 147,policy_hashat 575). - Extension parsing:
crates/protocol/chio-openapi/src/extensions.rslines 26 to 40; approval precedence inchio-openapi/src/policy.rslines 49 to 51. - Synthetic tool and projection:
crates/platform/chio-http-core/src/authority.rslines 33 to 35 andauthority_projection.rs. - Caller identity:
crates/products/chio-api-protect/src/evaluator.rslines 540 to 575;session_idat line 363. - Capability header parsing:
crates/products/chio-api-protect/src/proxy/http.rslines 120 to 124. - Decision and final receipts:
crates/products/chio-api-protect/src/proxy/router.rslines 632 to 659; upstream failure at 661 to 676. - Receipt id and request binding:
crates/platform/chio-http-core/src/receipt.rslines 343 to 353 andrequest.rslines 168 to 188. - Verification response:
crates/platform/chio-http-core/src/evaluation.rslines 33 to 45. - Tower fail-open:
crates/protocol/chio-tower/src/service.rslines 136 to 142.