Chio/Docs

OpenAPI Sidecar

The smallest spec-driven HTTP example: a plain Python upstream app with no Chio coupling, fronted by chio api protect driven by the app's OpenAPI 3.x spec. Every governed route gets a capability check; every request gets a persisted, signed receipt. The example lives at examples/hello-openapi-sidecar and is the recommended first web-backend example.

Prerequisites

Python 3 for the upstream app and smoke driver. The chio binary on PATH (or built locally; the smoke flow resolves it through examples/_shared/hello-http-common.sh). See Installation if you have not built the workspace yet.

What It Shows

Four properties, all on a single OpenAPI spec:

  • Safe routes pass through. GET /hello is allowed by the sidecar and returns an x-chio-receipt-id response header.
  • Side-effect routes deny without a token. POST /echo without a capability token returns an chio_access_denied error body and a receipt id, and never reaches the upstream app.
  • Side-effect routes allow with a token. With an X-Chio-Capability header issued by chio trust serve, the same POST /echo runs the upstream app and returns a 200 with the receipt id.
  • Receipts persist locally. The sidecar writes signed receipts to a SQLite store; the smoke flow lists them with chio receipt list.

Critically, the upstream app has no Chio framework SDK or middleware. All governance happens in the sidecar.


Run It

Start the upstream app only:

terminal
cd examples/hello-openapi-sidecar
./run.sh

Run the full trust + sidecar smoke flow (boots a trust service, the upstream app, and the sidecar; runs the three governed scenarios end to end):

terminal
./smoke.sh

Artifacts land under .artifacts/<timestamp>/ as JSON response bodies, response header dumps, the issued capability token, and a summary.json with three receipt ids: one for hello, one for deny, one for allow.


The Upstream App

The app is a 100-line Python ThreadingHTTPServer with three routes: GET /healthz, GET /hello, and POST /echo. Every JSON response carries chio_sdk: false as proof that the app does not import any Chio code.

app.py (excerpt)
def do_GET(self) -> None:
    path = urlparse(self.path).path
    if path == "/healthz":
        self._json(HTTPStatus.OK, {"status": "ok"})
        return
    if path == "/hello":
        self._json(HTTPStatus.OK, {
            "message": "hello from openapi-sidecar upstream",
            "runtime": "python-http-server",
            "chio_sdk": False,
        })
        return
    self._json(HTTPStatus.NOT_FOUND, {"error": "not_found"})

The OpenAPI Spec

The spec describes the same three operations the upstream app implements. No Chio extensions are present in this minimal example; default policy applies (GET is session-allow, POST is deny-by-default until a capability token gates it).

openapi.yaml
openapi: 3.1.0
info:
  title: hello-openapi-sidecar
  version: 0.1.0
paths:
  /healthz:
    get:
      operationId: healthz
      responses:
        "200": { description: Health check }
  /hello:
    get:
      operationId: hello
      responses:
        "200": { description: Greeting }
  /echo:
    post:
      operationId: echo
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message]
              properties:
                message: { type: string }
                count: { type: integer }
      responses:
        "200": { description: Echo payload }

x-chio-* Extensions You Can Add

The OpenAPI bridge consumes a small extension vocabulary. None are required, but they let an OpenAPI spec drive policy directly.

ExtensionTypeEffect
x-chio-sensitivitypublic | internal | sensitive | restrictedAudit classification; affects log level and audit granularity. Default internal.
x-chio-side-effectsbooleanOverride the method default. Use true for a GET that mutates, false for a POST that only reads.
x-chio-approval-requiredbooleanWhen true, forces deny-by-default and sets annotations.requires_approval on the manifest. Takes precedence over everything else.
x-chio-budget-limitinteger (minor currency units)Per-invocation cost cap consumed by the budget guard.
x-chio-publishbooleanWhen false, excludes the operation from the generated manifest. Useful for health checks. Default true.

Walkthrough

Pipeline: OpenAPI 3.x to Tools to Chio

rendering…
Each operation in the OpenAPI spec becomes a tool in the Chio manifest the sidecar exposes. The sidecar mediates every request before it reaches the upstream app.

Each HTTP operation (method + path pair) becomes one entry in the generated ToolManifest. The operation id maps to the tool name; the request body schema and parameters merge into a single JSON Schema input shape; HTTP method drives the default has_side_effects flag (false for GET, HEAD, OPTIONS; true for everything else).

Trust Service and Sidecar Startup

The smoke flow boots a local trust service first, then the upstream app, then the sidecar pointed at both. The sidecar consumes the OpenAPI spec on startup; nothing rebuilds when the spec changes after that.

smoke.sh (excerpt)
"${CHIO_BIN}" trust serve \
  --listen "127.0.0.1:${TRUST_PORT}" \
  --service-token "${SERVICE_TOKEN}" \
  --receipt-db "${STATE_DIR}/trust-receipts.sqlite3" \
  --revocation-db "${STATE_DIR}/trust-revocations.sqlite3" \
  --authority-db "${STATE_DIR}/trust-authority.sqlite3" \
  --budget-db "${STATE_DIR}/trust-budgets.sqlite3" &

(
  export CHIO_TRUSTED_ISSUER_KEY="${TRUSTED_ISSUER_KEY}"
  exec "${CHIO_BIN}" \
    --control-url "${CONTROL_URL}" \
    --control-token "${SERVICE_TOKEN}" \
    api protect \
    --upstream "${APP_URL}" \
    --spec "${EXAMPLE_ROOT}/openapi.yaml" \
    --listen "127.0.0.1:${SIDECAR_PORT}" \
    --receipt-store "${RECEIPT_STORE}"
) &

The Three Scenarios

The smoke flow exercises each policy outcome with one curl call. All three responses carry an x-chio-receipt-id header, which the smoke flow extracts for the summary.

smoke.sh (excerpt)
# 1. Safe GET allowed without a token.
curl -sS -D hello.headers "${SIDECAR_URL}/hello" > hello.json

# 2. POST denied without a capability token.
curl -sS -D deny.headers \
  -H "content-type: application/json" \
  --data '{"message":"denied","count":1}' \
  "${SIDECAR_URL}/echo" > deny.json

# 3. POST allowed with an issued capability token.
curl -sS -D allow.headers \
  -H "content-type: application/json" \
  -H "X-Chio-Capability: $(tr -d '\n' < capability.token)" \
  --data '{"message":"hello","count":2}' \
  "${SIDECAR_URL}/echo" > allow.json

Assertions on each response:

  • hello.json has message: "hello from openapi-sidecar upstream" and chio_sdk: false.
  • deny.json has error: "chio_access_denied" and a non-empty receipt_id plus a suggestion field. The upstream app is never reached.
  • allow.json has handled_by: "plain-upstream-app" and chio_sdk: false: the sidecar mediated, the app ran the request unmodified.

Full Request and Response

The capability-gated POST in the third scenario, in full. Header names are normalized lower-case by the sidecar.

request
POST /echo HTTP/1.1
Host: 127.0.0.1:{SIDECAR_PORT}
Content-Type: application/json
X-Chio-Capability: eyJhbGciOiJFZERTQSIsInR5cCI6IkNoaW9DYXBhYmlsaXR5In0...

{"message":"hello","count":2}
response
HTTP/1.1 200 OK
Content-Type: application/json
x-chio-receipt-id: 01J9D7K6E3R5V8YQ2X1N0M4F7C

{
  "message": "hello",
  "count": 2,
  "handled_by": "plain-upstream-app",
  "chio_sdk": false
}

The deny variant returns 403 with the same x-chio-receipt-id header plus a structured error body:

deny response
HTTP/1.1 403 Forbidden
Content-Type: application/json
x-chio-receipt-id: 01J9D7K8X2A0B5T9R7P1M3W6V

{
  "error": "chio_access_denied",
  "receipt_id": "01J9D7K8X2A0B5T9R7P1M3W6V",
  "suggestion": "issue a capability token via chio trust serve"
}

Smoke assertions

examples/hello-openapi-sidecar/smoke.sh
# /hello: upstream answered, no SDK linked
assert body["message"] == "hello from openapi-sidecar upstream", body
assert body["chio_sdk"] is False, body

# /echo deny: structured error, receipt id present, suggestion field
assert body["error"] == "chio_access_denied", body
assert body["receipt_id"], body
assert body["suggestion"], body

# /echo allow: upstream ran, still no SDK
assert body["message"] == "hello", body
assert body["count"] == 2, body
assert body["handled_by"] == "plain-upstream-app", body
assert body["chio_sdk"] is False, body

Listing Persisted Receipts

The sidecar writes every signed receipt to RECEIPT_STORE (SQLite). The smoke flow lists them with the chio receipt CLI:

smoke.sh (excerpt)
"${CHIO_BIN}" receipt list --receipt-db "${RECEIPT_STORE}" --limit 20 \
  > "${ARTIFACT_ROOT}/receipts.ndjson"

Three receipts are produced: one for GET /hello (allow), one for the unauthorized POST /echo (deny), one for the capability-gated POST /echo (allow). Receipt ids match the values returned in the response headers.

Inspect after

bash
cd .artifacts/$(ls -t .artifacts | head -1)

# Three receipt ids, captured into summary.json
jq '.receipt_ids' summary.json
# {
#   "hello": "01J9D7K5...",
#   "deny":  "01J9D7K8...",
#   "allow": "01J9D7K6..."
# }

# Headers carry the same ids
grep -i x-chio-receipt-id hello.headers deny.headers allow.headers

# Persisted receipt store: 3 records, 2 allow + 1 deny
wc -l receipts.ndjson
jq -r '.verdict.outcome' receipts.ndjson | sort | uniq -c

# Confirm the deny receipt never reached upstream
jq -r '.verdict.guard, .verdict.reason' receipts.ndjson | head -6

# SQLite peek
sqlite3 state/sidecar-receipts.sqlite3 \
  "select id, route_method || ' ' || route_path as op, verdict_outcome
   from receipts order by created_at;"

Decision rule

Use this when: the upstream app is already a working HTTP service, the team has an OpenAPI 3.x spec, and you want governance with no code change to the app. Don't use this if you need receipts surfaced inside the request scope (for instance to log the receipt id alongside business events): use a framework SDK instead. See Python, Node, JVM and .NET, or Go and C++.

Why start here

This example is the cleanest separation of concerns Chio offers for HTTP services. The app stays a plain HTTP server. Routing, capability validation, guards, and receipt persistence are all in the sidecar. When you outgrow this and want framework-native interception (for instance to surface receipts in a request scope), the framework integrations are drop-in replacements for the sidecar.

Manifest Signing and Sidecar Deployment

The sidecar consumes the OpenAPI spec at startup and produces an in-memory manifest signed with the configured trusted issuer key (passed via CHIO_TRUSTED_ISSUER_KEY in this example). For production deployments where the manifest is shared with other services, sign the manifest once with chio and distribute the signed artifact, rather than regenerating per-process. See the Bridge OpenAPI to MCP guide for the manifest-first variant.


Next Steps

OpenAPI Sidecar · Chio Docs