Chio/Docs
LOGIN · JOIN

Protect an API

Put Chio in front of Larkfield's pricing API, issue a quote under a grant, and revoke the grant.

Guide chio@5f63e6e

On this page

Run a Chio sidecar in front of an HTTP API you already have, and the kernel decides each request before the API sees it. Callers present a capability for writes; the sidecar forwards what the kernel admits and signs a receipt for every decision.

The API keeps its own authentication, application behavior, and storage. Starts from chio new pricing-api --example pricing-api: Larkfield's pricing functions as a plain Python HTTP API with no Chio code in it. You put the sidecar in front, issue a quote for sku-301 under a five-minute grant to Basalt's buying agent, then revoke the grant and watch the next quote stop.

Start

The sidecar decides before the upstream sees the request

The sidecar reads the API's OpenAPI document into a route table. Each request is matched to a route, then the kernel checks the capability the caller presents in the X-Chio-Capability header or the chio_capability query parameter.

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

An admitted request is forwarded without the X-Chio-Capability header, and the response returns with its receipt id. A refused one gets a structured 403 that names the missing input, and the upstream never sees it. The sidecar fails closed: when it cannot reach a decision, nothing is forwarded.

This boundary covers the requests that cross the sidecar. A caller that can reach the upstream port directly bypasses it, so close that port in the host, as the mediation boundary describes.

1. Start the pricing API

Pull the project, or download it. It holds the API, its OpenAPI document, a grant request, and a receipt reader.

Terminal 1 · the APIbash
chio new pricing-api --example pricing-api && cd pricing-api
python3 pricing_api.py
Output
Pricing API: http://127.0.0.1:8087; data: .pricing/quotes.db

Keep it running. It serves POST /v1/quote, GET /v1/quotes, and reads and withdrawals by quote ID, and it keeps issued quotes in .pricing/quotes.db.

Before you begin

You need the chio CLI, Python 3, and jq. Install the CLI with curl -fsSL https://chio.computer/install.sh | sh; Installation lists the other options.

2. Put the sidecar in front of it

In a second terminal in the same directory, start the sidecar. It reads openapi.yaml, forwards admitted requests to port 8087, and listens on port 9097.

Terminal 2 · the sidecarbash
chio --authority-seed-file .pricing/authority.hex api protect \
  --upstream http://127.0.0.1:8087 \
  --spec openapi.yaml --listen 127.0.0.1:9097 \
  --receipt-store .pricing/receipts.db

The CLI creates the signing seed in .pricing/authority.hex when it is absent and reuses it on restart. Receipts and revocations go to .pricing/receipts.db. From here on, callers talk to port 9097 only.

3. Ask for a quote without a capability

In a third terminal in the same directory, read the quotes, then ask for sku-301 at 80 percent.

Terminal 3 · read, then ask for a quotebash
curl -sS http://127.0.0.1:9097/v1/quotes
curl -sS -i http://127.0.0.1:9097/v1/quote \
  -H 'Content-Type: application/json' \
  -d '{"sku":"sku-301","percent":80}'
Output
{"quotes":[]}
HTTP/1.1 403 Forbidden
content-type: application/json
x-chio-receipt-id: 01a12ef0-bf17-7d80-ac22-c82d49abccb6

{"error":"chio_access_denied","message":"side-effect route requires a capability token","receipt_id":"01a12ef0-bf17-7d80-ac22-c82d49abccb6","suggestion":"provide a valid capability token in the X-Chio-Capability header or chio_capability query parameter"}

You should see an empty list, then 403 with chio_access_denied. POST /v1/quote is marked side-effecting, so it needs a capability. The body names the header to send and carries the receipt_id of the signed refusal; the API never saw the request.

4. Grant a capability and issue the quote

As the local operator, ask the sidecar for a five-minute grant for Basalt's buying agent. The request names a subject, a job, a lifetime, and the scope to issue.

http-grant.json · complete mint requestjson
{
  "subject": "basalt-buyer",
  "job_uid": "basalt-quotes-1",
  "ttl_seconds": 300,
  "scopes": ["tool:chio_http_authority:authorize_http_request:invoke"]
}

Mint the capability into capability.json, then send the same request with it.

Terminal 3 · mint a grant, then quotebash
curl -sS http://127.0.0.1:9097/v1/capabilities/mint \
  -H 'Content-Type: application/json' \
  --data-binary @http-grant.json -o capability.json
curl -sS -i http://127.0.0.1:9097/v1/quote \
  -H 'Content-Type: application/json' \
  -H "X-Chio-Capability: $(jq -c .capability capability.json)" \
  -d '{"sku":"sku-301","percent":80}'
Output
HTTP/1.1 201 Created
content-type: application/json
x-chio-receipt-id: 01a12ef0-bf64-7c12-b46e-430b507267c3

{"id":1,"sku":"sku-301","percent":80,"price":"28.00","currency":"USD"}

You should see 201 and USD 28.00, with an x-chio-receipt-id header that names the HTTP receipt. The grant covers the sidecar's HTTP operation, authorize_http_request, for five minutes; it names neither the quote operation nor POST /v1/quote.

Your own client sends the same header, with the compact JSON of capability.json's capability field as its value.

caller.tscaller.pysrc/main.rs · in main
const response = await fetch("http://127.0.0.1:9097/v1/quote", {
  method: "POST",
  headers: { "content-type": "application/json", "X-Chio-Capability": capabilityToken },
  body: JSON.stringify({ sku: "sku-301", percent: 80 }),
});
console.log(response.status, response.headers.get("x-chio-receipt-id"));

The mint endpoint accepts operator requests on loopback. Before you expose a control endpoint, configure its bearer token and put it behind its own access boundary.

5. Confirm the quote and read its receipt

Read the quotes through the sidecar, then list the allowed decisions from the receipt store.

Terminal 3 · application state and evidencebash
curl -sS http://127.0.0.1:9097/v1/quotes
python3 receipts.py --decision allow
Output
{"quotes":[{"id":1,"sku":"sku-301","percent":80.0,"price":"28.00","currency":"USD"}]}
01a12ef0-bf03-7a80-833e-f1276e427dc1  ALLOW      GET    /v1/quotes  HTTP 200
01a12ef0-bf64-7c12-b46e-430b507267c3  ALLOW      POST   /v1/quote  HTTP 201
01a12ef0-bf87-7c13-8e6f-4e5b177a5e6d  ALLOW      GET    /v1/quotes  HTTP 200

You should see the recorded quote and one allow receipt per admitted request, reads included: GET needs no capability, and the sidecar still signs a receipt for it. Pass a receipt id to receipts.py to print the complete signed object. Receipts for HTTP calls shows that lookup and its verification.

6. Revoke the grant and confirm the write stops

Release the capability by its id, then present it again.

Terminal 3 · revoke Basalt's grantbash
curl -sS http://127.0.0.1:9097/v1/capabilities/release \
  -H 'Content-Type: application/json' \
  -d "$(jq '{capability_id:.capability.id}' capability.json)"
curl -sS -i http://127.0.0.1:9097/v1/quote \
  -H 'Content-Type: application/json' \
  -H "X-Chio-Capability: $(jq -c .capability capability.json)" \
  -d '{"sku":"sku-301","percent":80}'
Output
{"released":true}
HTTP/1.1 403 Forbidden
content-type: application/json
x-chio-receipt-id: 01a12ef0-c102-7d81-870e-b35ff90b8098

{"error":"chio_access_denied","message":"capability token has been revoked","receipt_id":"01a12ef0-c102-7d81-870e-b35ff90b8098","suggestion":"request a fresh capability token before retrying"}

You should see {"released":true}, then 403 with capability token has been revoked and a receipt id. List the quotes again: the refused request recorded nothing. The store keeps revocations across sidecar restarts.

To issue new authority, change job_uid in http-grant.json before minting. An identical mint request derives the same capability id, and the revocation still applies to it.

The method decides whether a request needs a capability

The route table gives every operation a policy from its method, and the OpenAPI document can override it per operation with x-chio-side-effects.

MethodsDefault policyCapabilityReceipt
GET, HEAD, OPTIONSSessionAllowNot requiredAudit receipt
POST, PUT, PATCH, DELETEDenyByDefaultRequiredAllow or deny receipt

The sidecar signs a receipt either way. For named operation grants and a two-quote budget, serve the same API to an agent through the MCP host for the pricing API. Keep invocation accounting on a path that enforces it; the HTTP projection does not consume the presented token’s invocation or spending budget.

Next: configure the HTTP proxy