BuildProtocols
OpenAPI Sidecar
Use an OpenAPI 3.x spec to gate side effects and persist signed receipts for a plain HTTP app.
Prerequisites
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
This example uses one OpenAPI spec to show four behaviors:
- Safe routes pass through.
GET /hellois allowed by the sidecar and returns anx-chio-receipt-idresponse header. - Side-effect routes deny without a token.
POST /echowithout a capability token returns anchio_access_deniederror body and a receipt ID; the sidecar does not dispatch it to the upstream app. - Side-effect routes allow with a token. With an
X-Chio-Capabilityheader issued bychio trust serve, the samePOST /echoruns 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 reads them back from the store's
http_receiptstable and writes them out as NDJSON.
The upstream app has no Chio framework SDK or middleware. The sidecar applies the policy and stores receipts.
Run It
Start the upstream app only:
cd examples/hello-openapi-sidecar
./run.shRun the full trust + sidecar smoke flow (boots a trust service, the upstream app, and sidecar; then runs three governed scenarios):
./smoke.shRun output is written 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.
Zero-config alias: chio start
chio start stands up the same sidecar router with zero configuration: it binds 127.0.0.1:9090, keeps receipts durable by default, and configures no upstream, so the catch-all route returns a loud 502. It is the SDK-quickstart and chio-hermes entry point and takes the same store flag this example uses: chio start --receipt-store PATH [--listen ADDR] [--allow-ephemeral-receipts] [--print-config]. Use chio api protect when you need --upstream and --spec.The Upstream App
The app is a 160-line Python ThreadingHTTPServer with three routes: GET /healthz, GET /hello, and POST /echo. JSON responses carry chio_sdk: false as proof that the app does not import any Chio code.
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: 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.
| Extension | Type | Effect |
|---|---|---|
x-chio-sensitivity | public | internal | sensitive | restricted | Audit classification; affects log level and audit granularity. Default internal. |
x-chio-side-effects | boolean | Override the method default. Use true for a GET that mutates, false for a POST that only reads. |
x-chio-approval-required | boolean | When true, forces deny-by-default and sets annotations.requires_approval on the manifest. Takes precedence over everything else. |
x-chio-budget-limit | integer (minor currency units) | Per-invocation cost cap consumed by the budget guard. |
x-chio-publish | boolean | When false, excludes the operation from the generated manifest. Useful for health checks. Default true. |
Walkthrough
Pipeline: OpenAPI 3.x to Tools to Chio
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 reads the OpenAPI spec at startup. Restart the sidecar after changing the spec.
"${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.
# 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.jsonAssertions on each response:
hello.jsonhasmessage: "hello from openapi-sidecar upstream"andchio_sdk: false.deny.jsonhaserror: "chio_access_denied"and a non-emptyreceipt_idplus asuggestionfield. The upstream app is never reached.allow.jsonhashandled_by: "plain-upstream-app"andchio_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.
POST /echo HTTP/1.1
Host: 127.0.0.1:{SIDECAR_PORT}
Content-Type: application/json
X-Chio-Capability: eyJhbGciOiJFZERTQSIsInR5cCI6IkNoaW9DYXBhYmlsaXR5In0...
{"message":"hello","count":2}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:
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
# /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, bodyListing Persisted Receipts
The sidecar writes each signed receipt to RECEIPT_STORE (SQLite), one row per receipt in the http_receipts table. The smoke flow reads them back directly with a small python3/sqlite3 query and writes each receipt as one JSON line to receipts.ndjson:
python3 - "${RECEIPT_STORE}" "${ARTIFACT_ROOT}/receipts.ndjson" <<'PY'
import json
import sqlite3
import sys
from pathlib import Path
with sqlite3.connect(Path(sys.argv[1])) as db:
rows = db.execute(
"SELECT receipt_json FROM http_receipts ORDER BY rowid ASC"
).fetchall()
receipts = [json.loads(row[0]) for row in rows]
Path(sys.argv[2]).write_text(
"".join(json.dumps(r, separators=(",", ":")) + "\n" for r in receipts),
encoding="utf-8",
)
PYThree 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
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.verdict' receipts.ndjson | sort | uniq -c
# Confirm the deny receipt never reached upstream
jq -r '.verdict.guard, .verdict.reason' receipts.ndjson | head -6
# SQLite peek: raw rows (route, method, and verdict live inside receipt_json)
sqlite3 state/sidecar-receipts.sqlite3 \
"select id, receipt_json from http_receipts order by rowid;"Decision rule
Why start here
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 manifest instead of regenerating it for each process. See the Bridge OpenAPI to MCP guide for the manifest-first variant.
Next Steps
- Bridge OpenAPI to MCP · the manifest-first variant that exposes OpenAPI operations as MCP tools
- OpenAPI integration reference · normative spec for the parser, the
x-chio-*vocabulary, and the default policy table - Protect an API · the operator guide for
chio api protect