OpenAPI Integration
Look up how an OpenAPI document becomes tool definitions, which x-chio keys shape route policy, and what the protecting proxy answers.
On this page
An OpenAPI document is the contract Chio reads to govern an existing HTTP API. Each operation becomes a tool definition for the bridge and a route with a default policy for the protecting proxy.
A tool definition describes one operation and grants nothing. Authority comes from the capability a caller presents, which the kernel checks on each request before the upstream sees it.
Try x-chio-publish, SessionAllow, or chio_approval_required. Each entry has its own link.
Synopsis
chio-openapi parses one OpenAPI 3.x document and returns one ToolDefinition per operation. tools_from_spec is its convenience entry point: it parses the document and generates its tools with the default configuration. chio api protect loads the same document into a route table and serves it as a reverse proxy in front of the upstream.
pub fn tools_from_spec(input: &str) -> Result<Vec<chio_core_types::ToolDefinition>>chio api protect [OPTIONS] --upstream <UPSTREAM>Tool generation pipeline
κcapabilityσscopegguardsBbudget
The kernel admits the tool before the bridge sends HTTP
At startup the bridge compiles each OpenAPI operation into a named tool and a route binding. The kernel admits tools/call for createNote under the grant; the bridge then resolves the route, enforces the egress contract, and calls the HTTP dispatcher. The MCP result carries httpStatus, method, path, and body. Bridge OpenAPI to MCP.
The generator produces ToolDefinition values that conform to chio.manifest.v1: one per HTTP operation, a method and path pair. The output drives both the OpenAPI-to-MCP bridge and the chio api protect reverse proxy. The bridge resolves the route for an admitted call, applies its egress contract, and returns an MCP result carrying httpStatus, method, path, and body.
The parser resolves local references while it reads the document. The generator then merges path and operation parameters, reads extensions, skips unpublished operations when configured, and creates tool definitions: unsigned descriptions with annotations, which the caller registers. The proxy reads each operation's method and extensions separately to build its route policy, so an unpublished operation still has a route. The field mapping and the default policy give the exact rules.
Supported OpenAPI versions
The parser MUST accept specifications declaring version 3.0.x or 3.1.x in the top-level openapi field. The value MUST begin with 3..
The parser MUST reject specifications with any other version prefix with an UnsupportedVersion error. OpenAPI 2.0 (Swagger) documents fail this check.
Supported formats
The parser MUST accept both JSON and YAML input. Format detection is automatic: if the input, after leading whitespace, begins with {, the parser treats it as JSON; otherwise it treats the input as YAML. The caller passes no format flag.
Required top-level fields
A document MUST carry openapi, info, and paths. A missing field fails with MissingField("openapi"), MissingField("info"), or MissingField("paths").
When info.title is absent, the parser MUST default to "Untitled API". When info.version is absent, the parser MUST default to "0.0.0". The parser sorts paths by name, so the generated tool order is independent of the document's key order.
Operation to tool mapping
Each operation the parser extracts becomes one tool, built from its parameters, its request body, and its successful responses.
Route extraction
For each entry in paths, the parser MUST extract operations for these HTTP methods, in this order: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. The parser reads only those keys and ignores any other method key.
Parameter extraction
Each parameter MUST have a non-empty name and in field; a missing one is a MissingField("parameter.name") or MissingField("parameter.in") error. The in field MUST be one of path, query, header, or cookie. Any other value is an InvalidSpec error, as is a parameters value that is not an array or a required value that is not a boolean.
When required is absent, a path parameter is required and any other parameter is optional. Header and cookie parameters are parsed but MUST NOT appear in the generated ToolDefinition input schema. Only path and query parameters become tool input properties.
Parameter merging
Path-level parameters and operation-level parameters MUST be merged before tool generation. If an operation-level parameter has the same name and location as a path-level parameter, the operation level wins.
Request body schema
When a requestBody is present, the parser MUST look for a content["application/json"].schema entry first. If absent, the parser MUST fall back to the first available content type. If the body or the schema is a $ref, the parser resolves it before returning. The parser also records requestBody.required, which defaults to false.
Response schema selection
The output schema for a ToolDefinition comes from the response schemas. The generator MUST prefer the 200 response, then 201, then any 2xx response that includes a schema. If no successful response includes a schema, the output schema is None. Each response schema uses the same content selection as the request body: application/json first, then the first content type.
$ref resolution
The parser MUST resolve JSON Reference pointers that begin with #/. This covers #/components/schemas, #/components/parameters, and other #/components/ namespaces. The parser MUST reject, with an UnresolvedRef error:
- External references, URIs that do not begin with
#/. - Internal references that point to a path the document does not contain.
ToolDefinition derivation
Each ToolDefinition field takes its value from the operation as follows.
name:operationIdif present; otherwise"{METHOD} {path}".description:summaryif present, elsedescription, else"{METHOD} {path}".input_schema: a JSON Schema object built from path and query parameters as properties, plus abodyproperty from the request body schema.output_schema: the selected 2xx response schema, orNonewheninclude_output_schemasis off.annotations.read_only:trueif the operation has no side effects (DefaultPolicy::has_side_effects).annotations.destructive:trueif the method isDELETE.annotations.idempotent:trueif the method isGET,PUT, orDELETE.annotations.requires_approval: the value ofx-chio-approval-required, defaulting tofalse.annotations.estimated_duration_ms:None.pricing:None.
Input schema normalization
OpenAPI schemas project as JSON Schema in the generated tools. build_input_schema preserves each parameter's own schema and applies these normalizations:
- The
input_schemais one JSON Schema object withtype: "object". - Path and query parameters become top-level properties named after the parameter; header and cookie parameters are skipped.
- A parameter with no schema, or a schema that is not an object, becomes
{ "type": "string" }. A parameterdescriptionis copied into its property. - Required parameters appear in the
requiredarray, which is omitted when empty. - A request body schema appears under a top-level
bodyproperty, listed inrequiredonly whenrequestBody.requiredistrue.
The parser resolves internal $ref pointers before a schema reaches the generator, so a consumer sees expanded schemas.
Generator configuration
| Option | Type | Default | Effect |
|---|---|---|---|
server_id | string | "openapi-server" | Manifest configuration; tool definitions carry no server ID |
include_ | boolean | true | Derive output schemas from response definitions |
respect_ | boolean | true | Honor x-chio-publish: false |
x-chio-* extension vocabulary
ChioExtensions::from_operation reads these keys from the raw operation object. They are per-route policy hints that the manifest generator and the chio api protect proxy consume. Every key is optional, and a key whose value has the wrong JSON type is treated as absent.
| Extension | Scope | Type | Default | Meaning |
|---|---|---|---|---|
x- | operation | enum string | internal | Classification metadata |
x- | operation | boolean | method-driven | Overrides the side-effect annotation and method default |
x- | operation | boolean | false | Forces DenyByDefault; sets annotations. |
x- | operation | unsigned 64-bit integer | none | Budget hint in minor currency units |
x-chio-publish | operation | boolean | true | Includes the operation in the generated manifest ( should_publish) |
x-chio-side-effects overrides the method-based default policy, and x-chio-approval-required: true takes precedence over a false override. x-chio-publish: false fits internal or health-check endpoints that stay reachable through the proxy.
x-chio-sensitivity and x-chio-budget-limit are parsed metadata. The tool generator and proxy route builder carry the sensitivity label as classification, and they read the budget hint but do not turn it into a budget guard or spending limit. Spending limits come from the runtime's budget enforcement.
Sensitivity levels
x-chio-sensitivity takes one of four values: public for publicly available data, internal (the enum default), sensitive, and restricted for highly restricted data. Handling for sensitive and approval for restricted are configured separately: in the tool-generation and proxy-route paths, the label leaves approval and logging unchanged.
If the value does not match one of the four strings, the parser MUST ignore it and treat the key as absent. Sensitivity serializes in lowercase and defaults to internal; the parsed extension stays absent when the key is missing.
Approval precedence
x-chio-approval-required: true MUST take precedence over all other policy inputs. An operation that requires approval is DenyByDefault even when x-chio-side-effects is false and the method is GET.
Extension precedence summary
DefaultPolicy::for_method_with_extensions checks approval first, then the side-effects override, then the method:
| Method | x- | x- | Resulting policy |
|---|---|---|---|
GET | absent | absent | SessionAllow |
GET | absent | true | DenyByDefault |
GET | true | absent | DenyByDefault |
GET | false | true | DenyByDefault |
POST | absent | absent | DenyByDefault |
POST | false | absent | SessionAllow |
POST | false | true | DenyByDefault |
POST | absent | true | DenyByDefault |
Petstore example
The generator tests run this Petstore document, which has four operations on two paths:
openapi: "3.0.3"
info:
title: Petstore
description: A sample API for pets
version: "1.0.0"
paths:
/pets:
get:
operationId: listPets
summary: List all pets
parameters:
- name: limit
in: query
required: false
schema:
type: integer
format: int32
description: How many items to return
responses:
"200":
description: A list of pets
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Pet"
post:
operationId: createPet
summary: Create a pet
x-chio-sensitivity: internal
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
tag:
type: string
required:
- name
responses:
"201":
description: Pet created
/pets/{petId}:
get:
operationId: showPetById
summary: Info for a specific pet
parameters:
- name: petId
in: path
required: true
schema:
type: string
description: The id of the pet to retrieve
responses:
"200":
description: Expected response to a valid request
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
delete:
operationId: deletePet
summary: Delete a pet
x-chio-approval-required: true
parameters:
- name: petId
in: path
required: true
schema:
type: string
responses:
"204":
description: Pet deleted
components:
schemas:
Pet:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
required:
- id
- nameIt generates four tools. Each row is one ToolDefinition, with its annotations and the policy the route table assigns.
| Tool | Description | read_only | destructive | idempotent | requires_ | Policy |
|---|---|---|---|---|---|---|
listPets | List all pets | true | false | true | false | SessionAllow |
createPet | Create a pet | false | false | false | false | DenyByDefault |
showPetById | Info for a specific pet | true | false | true | false | SessionAllow |
deletePet | Delete a pet | false | true | true | true | DenyByDefault |
listPets takes an optional limit property and returns the array schema of the 200 response. createPet takes a required body property, because its requestBody.required is true. showPetById and deletePet take a required petId property. deletePet is DenyByDefault twice over: DELETE is a side-effect method, and the operation sets x-chio-approval-required: true.
Default deny-by-method policy
Every route in the proxy's table carries one of two PolicyDecision variants, decided by the method and the two policy extensions.
Method classification
HttpMethod::is_safe divides the methods, and DefaultPolicy::for_method maps the two classes to the two variants.
| Category | Methods | Default policy |
|---|---|---|
Safe (read-only) | GET, HEAD, OPTIONS | SessionAllow |
Side-effect (mutating) | POST, PUT, PATCH, DELETE | DenyByDefault |
SessionAllow
An operation classified as SessionAllow is permitted by default within an active session, and the caller presents no capability token. The proxy MUST still generate a signed HttpReceipt for every SessionAllow request; its evidence carries a passing DefaultPolicyGuard entry.
DenyByDefault
An operation classified as DenyByDefault requires the caller to present a valid capability token. Without a token, the proxy MUST return a structured 403 response whose verdict names CapabilityGuard.
The caller presents a capability token in the X-Chio-Capability HTTP header, matched case-insensitively, or the chio_capability query parameter; the header wins when both are present. A valid token satisfies the capability requirement, and the other configured runtime checks still apply before upstream dispatch. A token that fails validation yields a deny from the same guard, with the validation failure as the reason.
Extension overrides
- If
x-chio-approval-requiredistrue, the check returns DenyByDefault and takes highest precedence. - If
x-chio-side-effectsis explicitly set, it overrides the method default:trueforces DenyByDefault,falseforces SessionAllow. - If neither extension is set, the method classification applies.
Unmatched routes
If a request path matches no route in the loaded OpenAPI spec, the proxy MUST fall back to the method-based default policy: safe methods receive SessionAllow, and side-effect methods receive DenyByDefault. The receipt then records the raw request path as route_pattern. Route matching compares path segments one by one, and a {param} segment matches any single segment.
Caller identity and request forwarding
The proxy extracts caller identity from request headers in the order below and retains only a SHA-256 hash of the credential. The subject is the prefix plus the first 16 hex characters of the hash, and verified is false.
| Priority | Header | Identity format |
|---|---|---|
1 | Authorization: Bearer <token> | bearer:<truncated- |
2 | x-api-key (any letter case) | apikey:<truncated- |
3 | (none) | anonymous |
A credential that is empty, carries surrounding whitespace, or contains a control character is ignored, and extraction falls through to the next row.
To forward an allowed request, the proxy builds the upstream URL from the upstream base, the request path, and the query string with chio_capability removed. It forwards every request header except the hop-by-hop headers (connection, proxy-connection, keep-alive, proxy-authenticate, proxy-authorization, te, trailer, transfer-encoding, upgrade), host, content-length, x-chio-capability, and x-chio-execution-nonce. The request body is forwarded verbatim.
The proxy reads at most 10 MiB of body and answers 400 when the limit is exceeded. A request with a duplicate query key is answered 400 chio_bad_request, and a method outside the seven the route table knows is answered 405.
Error mapping
Each non-forwarded outcome has a fixed status and body. Library errors from the parser and the proxy are listed under the error catalog.
Structured 403 response
When a request is denied, a DenyByDefault route without a valid capability token, the proxy MUST return an HTTP 403 response with a JSON body conforming to this schema:
{
"error": "chio_access_denied",
"message": "<human-readable denial reason>",
"receipt_id": "<receipt ID for the denial>",
"suggestion": "provide a valid capability token in the X-Chio-Capability header or chio_capability query parameter"
}| Field | Type | Description |
|---|---|---|
error | string | Always "chio_ |
message | string | The verdict's reason |
receipt_id | string | ID of the signed receipt that records this denial |
suggestion | string | Actionable guidance for the caller |
The Content-Type on the 403 response MUST be application/json. The status is the verdict's http_status, 403 by default, and the proxy also sets X-Chio-Receipt-Id. A request that presents a revoked capability is denied before evaluation with the message "capability token has been revoked" and the suggestion "request a fresh capability token before retrying".
Cancel and incomplete verdicts
The proxy forwards only an allow verdict. For any other verdict it finalizes the receipt and answers with the EvaluateResponse as the body: 428 Precondition Required for incomplete, which a strict execution-nonce preflight produces, and 500 for cancel.
Upstream failure
If the upstream request cannot be built or the connection fails, the proxy MUST return HTTP 502 (Bad Gateway) with an X-Chio-Receipt-Id header. The finalized receipt keeps the allow verdict and records 502 as response_status, because the access-control decision stands. An upstream response with an error status is relayed with that status.
Error catalog
OpenApiError, the error type of chio-openapi:
InvalidJson: the input was detected as JSON and failed to parse.InvalidYaml: the input was detected as YAML and failed to parse.MissingField: a required top-level field (openapi,info, orpaths) or a parameter'snameorinis absent.UnsupportedVersion: theopenapiversion does not begin with3..UnresolvedRef: a$refpointer could not be resolved, an external URI or a missing internal path.InvalidSpec:parametersis not an array,parameter.innames an unsupported location, orparameter.requiredis not a boolean.
ProtectError, the error type of chio-api-protect:
SpecLoad: the OpenAPI spec file cannot be read, or auto-discovery failed.SpecParse: the loaded spec failed OpenAPI parsing; wrapsOpenApiError.Config: the durable-receipts gate, an invalid upstream URL, or a listen address that cannot be bound.Upstream: the upstream request failed.Evaluation: policy evaluation failed in caller identity hashing, content hashing, or the kernel.PendingApproval: the route is approval-gated and the decision is pending. Carriesapproval_idandkernel_receipt_idand drives the HTTP 409 approval workflow.ReceiptSign: receipt signing failed.ReceiptStore: receipt persistence failed, or a durable store could not be opened at startup.Io: an IO error during server operation.HttpClient: an HTTP client error during upstream communication.
Approval required (HTTP 409)
When evaluation of an approval-gated route returns PendingApproval, the proxy answers HTTP 409 with a body that identifies the approval workflow:
{
"error": "chio_approval_required",
"message": "request requires human approval before it can proceed",
"kernel_receipt_id": "<kernel receipt id>",
"approval_id": "<approval id>",
"resume_path": "/approvals/{approval_id}/respond"
}error, message, and kernel_receipt_id are always present. approval_id and resume_path are present when the proxy has an approval handle to resume against. A person approves at resume_path, one of the approval routes the proxy mounts behind the sidecar-control token. Any other evaluation error is answered 500 with "error": "chio_evaluation_failed" and the error text as message.
chio api protect contract
κ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.
chio api protect is a reverse proxy that places Chio capability checks between callers and an existing HTTP API, with no change to the upstream's code. The same process serves the sidecar routes under /chio that HTTP Transport specifies.
Command interface
chio api protect --upstream <URL> [--spec <path>] [--listen <addr>] \
(--receipt-store <path> | --allow-ephemeral-receipts) [--upstream-timeout-secs <n>]The flags the command adds to the global set, with the help text the binary prints:
| Flag | Required | Default | Description |
|---|---|---|---|
--upstream <UPSTREAM> | Yes | Upstream base URL to proxy to | |
--spec <SPEC> | No | auto-discovered | Optional local OpenAPI spec path. Auto-discovered when omitted |
--listen <LISTEN> | No | 127.0.0.1:9090 | Address to listen on |
--receipt-store <RECEIPT_STORE> | No | Optional SQLite receipt store path; the global --receipt-db otherwise | |
--allow- | No | false | Permit in-memory receipts, lost on every restart |
--upstream- <UPSTREAM_ | No | 20 seconds ( DEFAULT_) | Wall-clock ceiling on a single upstream hop |
--allow-ephemeral-receipts is for local development, since in-memory receipts lose their audit evidence on every restart. The --upstream-timeout-secs ceiling, in seconds, includes reading the full response; raise it for upstreams with legitimately slow calls or large bounded responses.
The global --authority-seed-file keeps the receipt signer stable across restarts. Without it, the proxy generates a fresh signer per boot and logs a warning, because receipts signed before a restart are then unverifiable against the new kernel_key. The global --revocation-db, --budget-db, --control-url, and --control-token pass through to the proxy configuration.
chio api protect refuses to start without a durable receipt store, given through --receipt-store or the global --receipt-db, unless --allow-ephemeral-receipts opts in explicitly. An in-memory SQLite path counts as no store, and the library entry point ProtectProxy::run applies the same gate to ProtectConfig.
$ chio api protect --upstream http://127.0.0.1:1error[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
Spec auto-discovery
When --spec is not provided, the proxy MUST attempt to auto-discover the OpenAPI specification from the upstream server. It probes these well-known paths in order:
/openapi.json/openapi.yaml/swagger.json/api-docs
For each path, the proxy issues an HTTP GET request to {upstream}{path} under an egress contract pinned to the upstream's scheme and authority. The first response with a successful status (2xx) and a non-empty body is used as the spec. If every probe fails, the proxy MUST fail with a SpecLoad error directing the operator to use --spec. An upstream URL without a host is a Config error.
Receipt generation
The proxy MUST generate a signed HttpReceipt for every request, allowed or denied. The full field shape is on HTTP Transport. The proxy takes each value from these sources:
id: a content-addressed receipt id, SHA-256 of the canonical receipt body withidremoved, assigned byHttpReceipt::sign.request_id: a UUIDv7 minted per request; a retry that presents an execution nonce reuses the request id the nonce is bound to.route_pattern: the matched OpenAPI path pattern, such as/pets/{petId}, or the raw path when no route matched.method: the HTTP method.caller_identity_hash: SHA-256 of the canonicalCallerIdentity.verdict:Allow, orDenywith reason and guard name;Incompleteon a strict nonce preflight.receipt_kind:mediated_decision.boundary_class:prevent.tool_origin:caller_executed.redaction_mode:none.actor_chain: empty, and therefore omitted from the wire.evidence:DefaultPolicyGuardorCapabilityGuardentries from the HTTP authority.response_status: the status the proxy answered with: the verdict'shttp_statuson deny, the upstream status on a forwarded request, 502 on upstream failure, 428 on an incomplete verdict.timestamp: the Unix timestamp when the receipt was signed.content_hash:ChioHttpRequest::content_hashover method, route pattern, path, query, and body hash.policy_hash: the SHA-256 hash of the loaded OpenAPI spec bytes.trust_level:mediated.capability_id: the id of the presented capability token, when one was presented.metadata:chio_http_status_scope(decisionat evaluation,finalonce the response status is known) andchio_decision_receipt_idon the finalized receipt.kernel_key: the public key of the proxy's signer, derived from--authority-seed-filewhen given and generated at startup otherwise.
The receipt MUST be signed with the proxy's signer keypair, and the signature MUST be verifiable using the kernel_key field embedded in the receipt. For a request that reaches a response, the proxy records the finalized receipt: a re-signed copy of the decision receipt with the answered status, a fresh timestamp, and the decision receipt's id in metadata.
Receipt header
The proxy sets an X-Chio-Receipt-Id header on every response it signs a receipt for: proxied responses, 403 denials, 428 and 500 non-allow verdicts, and 502 upstream failures. For 403 responses the receipt ID also appears in the JSON body as receipt_id.
Startup behavior
- Apply the durable-receipts gate: refuse to start without a durable receipt store unless ephemeral receipts were allowed.
- Load the OpenAPI spec, from
--specor by auto-discovery. - Parse the spec with
chio-openapiand build a route table that maps (method, path pattern) to aPolicyDecisionthroughDefaultPolicy::for_method_with_extensions. - Derive the signing keypair from the seed, or generate one.
- Compute the SHA-256 hash of the spec content for use as
policy_hashin receipts. - Open the receipt, approval, revocation, and authority stores: on the receipt store file and its sibling files when one is configured, in memory otherwise.
- Bind to the
--listenaddress and begin accepting requests.
With logging enabled, startup records the number of routes loaded, the upstream URL, and the bound address. /chio/live and /chio/health report liveness and readiness; a log line alone establishes neither.
Source and implementation references
The contract follows spec/OPENAPI-INTEGRATION.md in the Chio repository: status Normative, version 1.0, with MUST, SHOULD, and MAY as normative keywords. Where that document and the code differ, behavior follows the crates crates/protocol/chio-openapi and crates/products/chio-api-protect and the help text of chio api protect. The generator tests that run the Petstore document live in crates/protocol/chio-openapi/src/generator.rs; the document is section 5.1 of the specification, and its generated tools are section 5.2.
| Component | Crate | Entry point |
|---|---|---|
OpenAPI parser | chio-openapi | crates/ |
Manifest generator | chio-openapi | crates/ |
Extension vocabulary | chio-openapi | crates/ |
Default policy | chio-openapi | crates/ |
Reverse proxy | chio- | crates/ |
Request evaluator | chio- | crates/ |
Spec discovery | chio- | crates/ |
CLI command | chio-cli | crates/ |
Convenience function | chio-openapi | chio_ |