ReferenceSpec
OpenAPI Integration
How chio-openapi turns an OpenAPI 3.x document into ToolDefinition values, the x-chio-* keys, the deny-by-method policy, and the chio api protect contract.
Source
This page normatively reflects spec/OPENAPI-INTEGRATION.md in the chio repository. Status: Normative. Version 1.0. The keywords MUST, SHOULD, and MAY are normative.
Where that document and the code differ, the page follows the crates crates/protocol/chio-openapi and crates/products/chio-api-protect and the help text of chio api protect, which are the normative source for behavior.
Synopsis
pub fn tools_from_spec(input: &str) -> Result<Vec<chio_core_types::ToolDefinition>>chio api protect [OPTIONS] --upstream <UPSTREAM>The convenience entry point of chio-openapi, which parses one document and generates its tools with the default configuration, and the usage line the chio binary prints for the proxy.
From OpenAPI document to ToolDefinition
The chio-openapi crate parses OpenAPI specifications and produces Chio ToolDefinition values conforming to chio.manifest.v1. Each HTTP operation (method + path pair) becomes one tool definition. The output drives both the OpenAPI-to-MCP bridge and the chio api protect reverse proxy.
- Parse the OpenAPI document (JSON or YAML) with
OpenApiSpec::parse. - Extract operations from each path entry, in path order.
- Resolve
$refpointers and merge path-level parameters into each operation. - Read
x-chio-*keys for per-route policy hints. - Generate
ToolDefinitionvalues withManifestGenerator::generate_toolsand assign default policies withDefaultPolicy.
spec/OPENAPI-INTEGRATION.md:149-159at fe56570Supported 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) is not supported.
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. No explicit format flag is required from the caller.
Required top-level fields
| Field | Error if absent |
|---|---|
openapi | MissingField("openapi") |
info | MissingField("info") |
paths | 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 does not depend on the document's key order.
Operation to tool mapping
Route extraction
For each entry in paths, the parser MUST extract operations for the following HTTP methods, in this order: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. The parser reads only those keys, so any other method key is ignored.
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 not. Header and cookie parameters are parsed but MUST NOT appear in the generated ToolDefinition input schema. Only path and query parameters are promoted to 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 is derived from 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 comes from 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:
- External references (URIs not beginning with
#/) with anUnresolvedReferror. - Internal references that point to nonexistent paths within the document with an
UnresolvedReferror.
ToolDefinition derivation
| ToolDefinition field | Source |
|---|---|
name | operationId if present; otherwise "{METHOD} {path}" |
description | summary if present, else description, else "{METHOD} {path}" |
input_schema | JSON Schema object built from path + query parameters as properties, plus a body property from the request body schema |
output_schema | Selected 2xx response schema, or None when include_output_schemas is off |
annotations.read_only | true if the operation has no side effects (DefaultPolicy::has_side_effects) |
annotations.destructive | true if method is DELETE |
annotations.idempotent | true if method is GET, PUT, or DELETE |
annotations.requires_approval | Value of x-chio-approval-required, defaulting to false |
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.
Internal $ref pointers are resolved by the parser before a schema reaches the generator, so a consumer sees expanded schemas.
Generator configuration
| Option | Type | Default | Effect |
|---|---|---|---|
server_id | string | "openapi-server" | Identifier for the generated manifest body |
include_output_schemas | boolean | true | Whether to derive output schemas from response definitions |
respect_publish_flag | boolean | true | Whether to honor x-chio-publish: false |
x-chio-* extension vocabulary
ChioExtensions::from_operation reads these keys from the raw operation object. They provide per-route policy hints that the manifest generator and the chio api protect proxy consume. Every key is optional; a key whose value has the wrong JSON type is treated as absent.
| Extension | Scope | Type | Default | Meaning |
|---|---|---|---|---|
x-chio-sensitivity | operation | enum string | internal | One of public, internal, sensitive, restricted. Metadata classification consumed by the guard pipeline for logging level and audit granularity. Does not change policy directly. |
x-chio-side-effects | operation | boolean | method-driven | Overrides the method-based side-effect default. true forces deny-by-default; false forces session-scoped allow. |
x-chio-approval-required | operation | boolean | false | When true, forces deny-by-default regardless of method or x-chio-side-effects. Sets annotations.requires_approval. |
x-chio-budget-limit | operation | unsigned 64-bit integer | none | Per-invocation cost cap in minor currency units. Consumed by the budget guard. |
x-chio-publish | operation | boolean | true | Controls whether the operation appears in the generated manifest (should_publish). Useful for internal or health-check endpoints that stay reachable through the proxy. |
Sensitivity levels
| Level | Meaning |
|---|---|
public | Publicly available data, no special handling |
internal | Internal data, logged but not restricted beyond defaults |
sensitive | Sensitive data, may require additional approval |
restricted | Highly restricted data, always requires approval |
If the value does not match one of the four allowed strings, the parser MUST ignore it (treat as absent). Sensitivity serializes in lowercase and defaults to internal.
Approval precedence
Approval wins
x-chio-approval-required: true MUST take precedence over all other policy inputs. Even if x-chio-side-effects is false and the method is GET, the operation is deny-by-default when approval is required.Extension precedence summary
DefaultPolicy::for_method_with_extensions checks approval first, then the side-effects override, then the method:
| Method | x-chio-side-effects | x-chio-approval-required | 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
Section 5.1 of the specification gives this input document; the generator tests in crates/protocol/chio-openapi/src/generator.rs run the same document.
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
- nameSection 5.2 lists the generated tools. One row per ToolDefinition, with the annotations and the policy the route table assigns:
| Tool | Description | read_only | destructive | idempotent | requires_approval | 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 deny-by-default both because DELETE is a side-effect method and because it sets x-chio-approval-required: true.
Default deny-by-method policy
Method classification
HttpMethod::is_safe divides the methods, and DefaultPolicy::for_method maps the two classes to the two PolicyDecision variants.
| Category | Methods | Default policy |
|---|---|---|
| Safe (read-only) | GET, HEAD, OPTIONS | SessionAllow |
| Side-effect (mutating) | POST, PUT, PATCH, DELETE | DenyByDefault |
SessionAllow
Operations classified as SessionAllow are permitted by default within an active session. No capability token is required. The proxy MUST still generate a signed HttpReceipt for every SessionAllow request; its evidence carries a passing DefaultPolicyGuard entry.
DenyByDefault
Operations classified as DenyByDefault require 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 via the X-Chio-Capability HTTP header (matched case-insensitively) or the chio_capability query parameter; the header wins when both are present. When a valid token is present, the request proceeds to the upstream. 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 does not match any route in the loaded OpenAPI spec, the proxy MUST fall back to method-based default policy. Safe methods receive SessionAllow; 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 using the following precedence; only SHA-256 hashes are retained, never the raw 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-sha256> |
| 2 | x-api-key (any letter case) | apikey:<truncated-sha256> |
| 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.
When forwarding 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
Structured 403 response
When a request is denied (DenyByDefault policy 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_access_denied" |
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
A verdict that is neither allow nor deny is not forwarded. The proxy 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 for that request keeps the allow verdict and records 502 as response_status; the upstream failure does not change the access-control decision. An upstream response with an error status is relayed with that status.
Error catalog
OpenApiError, the error type of chio-openapi:
| Error | Condition |
|---|---|
InvalidJson | Input detected as JSON but failed to parse |
InvalidYaml | Input detected as YAML but failed to parse |
MissingField | A required top-level field (openapi, info, or paths) or a parameter's name or in is absent |
UnsupportedVersion | The openapi version does not begin with 3. |
UnresolvedRef | A $ref pointer could not be resolved (external URI or nonexistent internal path) |
InvalidSpec | parameters is not an array, parameter.in names an unsupported location, or parameter.required is not a boolean |
ProtectError, the error type of chio-api-protect:
| Error | Condition |
|---|---|
SpecLoad | The OpenAPI spec file cannot be read or auto-discovery failed |
SpecParse | The loaded spec failed OpenAPI parsing (wraps OpenApiError) |
Config | Configuration error: 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: caller identity hashing, content hashing, or the kernel |
PendingApproval | The route is approval-gated and the decision is pending. Carries approval_id and kernel_receipt_id and 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 | IO error during server operation |
HttpClient | HTTP client error during upstream communication |
Approval required (HTTP 409)
Approval-gated routes return neither an allow nor a deny. When evaluation 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; the approver responds 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
chio api protect is a zero-code reverse proxy that interposes Chio capability-based access control between callers and an existing HTTP API. It requires no code changes to the upstream. 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 is used when this flag is absent. | |
--allow-ephemeral-receipts | No | false | Permit in-memory receipts, whose audit evidence is lost on every restart. Required to boot without --receipt-store. For local development only |
--upstream-timeout-secs <UPSTREAM_TIMEOUT_SECS> | No | 20 seconds (DEFAULT_UPSTREAM_REQUEST_TIMEOUT) | Wall-clock ceiling in seconds on a single upstream hop, including 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 are passed through to the proxy configuration.
Durable receipts are required at boot
chio api protect refuses to start unless it has a durable receipt store (through --receipt-store or the global --receipt-db) or an explicit --allow-ephemeral-receipts opt-in. An in-memory SQLite path counts as no store. The library entry point ProtectProxy::run applies the same gate to ProtectConfig.$ chio api protect --upstream http://127.0.0.1:1error [urn:chio:error:cli:other]: refusing to start without durable receipts: pass --receipt-store <path> for a durable audit log on a filesystem path, or --allow-ephemeral-receipts to run with in-memory receipts that are lost on every restart
context: {"domain":"cli","severity":"error","stability":"deprecated","string_code":"CHIO-CLI-OTHER"}
suggested fix: Preserve the original message and migrate the call site to a specific registry code when touched.Spec auto-discovery
When --spec is not provided, the proxy MUST attempt to auto-discover the OpenAPI specification from the upstream server. The proxy probes the following well-known paths in order:
| Priority | Path |
|---|---|
| 1 | /openapi.json |
| 2 | /openapi.yaml |
| 3 | /swagger.json |
| 4 | /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 that returns a successful HTTP status (2xx) with a non-empty body is used as the spec. If none of the probes succeed, 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, regardless of whether the request is allowed or denied. The full field shape is on HTTP Transport; this table names where the proxy takes each value from.
| Receipt field | Source |
|---|---|
id | Content-addressed receipt id (SHA-256 of the canonical receipt body with id removed), assigned by HttpReceipt::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 | Matched OpenAPI path pattern (e.g., /pets/{petId}), or the raw path when no route matched |
method | HTTP method |
caller_identity_hash | SHA-256 of the canonical CallerIdentity |
verdict | Allow, or Deny with reason and guard name; Incomplete on 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 | DefaultPolicyGuard or CapabilityGuard entries from the HTTP authority |
response_status | The status the proxy answered with: the verdict's http_status on deny, the upstream status on a forwarded request, 502 on upstream failure, 428 on an incomplete verdict |
timestamp | Unix timestamp when the receipt was signed |
content_hash | ChioHttpRequest::content_hash over method, route pattern, path, query, and body hash |
policy_hash | 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 (decision at evaluation, final after the response status is known) and chio_decision_receipt_id on the finalized receipt |
kernel_key | Public key of the proxy's signer: derived from --authority-seed-file when given, otherwise generated at startup |
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 via auto-discovery). - Parse the spec using
chio-openapiand build a route table mapping (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.
The proxy logs the number of routes loaded, the upstream URL, and the bound address at startup.
Implementation references
| Component | Crate | Entry point |
|---|---|---|
| OpenAPI parser | chio-openapi | crates/protocol/chio-openapi/src/parser.rs |
| Manifest generator | chio-openapi | crates/protocol/chio-openapi/src/generator.rs |
| Extension vocabulary | chio-openapi | crates/protocol/chio-openapi/src/extensions.rs |
| Default policy | chio-openapi | crates/protocol/chio-openapi/src/policy.rs |
| Reverse proxy | chio-api-protect | crates/products/chio-api-protect/src/proxy.rs |
| Request evaluator | chio-api-protect | crates/products/chio-api-protect/src/evaluator.rs |
| Spec discovery | chio-api-protect | crates/products/chio-api-protect/src/spec_discovery.rs |
| CLI command | chio-cli | crates/products/chio-cli/src/cli/runtime.rs |
| Convenience function | chio-openapi | chio_openapi::tools_from_spec() |
Related
- HTTP Transport: the sidecar routes,
ChioHttpRequest,Verdict, and theHttpReceiptfield shape. - Bridges: the OpenAPI bridge alongside A2A, ACP, and OpenAI integrations.
- Bridge OpenAPI to MCP: walkthrough of the conversion.
- OpenAPI sidecar example.
- Protocol:
chio.manifest.v1contract. - CLI reference:
chiocommands and flags.