Chio/Docs
LOGIN · JOIN

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

rust
pub fn tools_from_spec(input: &str) -> Result<Vec<chio_core_types::ToolDefinition>>
bash
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.

  1. Parse the OpenAPI document (JSON or YAML) with OpenApiSpec::parse.
  2. Extract operations from each path entry, in path order.
  3. Resolve $ref pointers and merge path-level parameters into each operation.
  4. Read x-chio-* keys for per-route policy hints.
  5. Generate ToolDefinition values with ManifestGenerator::generate_tools and assign default policies with DefaultPolicy.
rendering
The conversion stages as the specification draws them, from the parsed document through extension extraction and reference resolution to the generated tools and their policies. The reverse proxy reads the same parsed document to build its route table.
sourcespec/OPENAPI-INTEGRATION.md:149-159at fe56570

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) 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

FieldError if absent
openapiMissingField("openapi")
infoMissingField("info")
pathsMissingField("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 an UnresolvedRef error.
  • Internal references that point to nonexistent paths within the document with an UnresolvedRef error.

ToolDefinition derivation

ToolDefinition fieldSource
nameoperationId if present; otherwise "{METHOD} {path}"
descriptionsummary if present, else description, else "{METHOD} {path}"
input_schemaJSON Schema object built from path + query parameters as properties, plus a body property from the request body schema
output_schemaSelected 2xx response schema, or None when include_output_schemas is off
annotations.read_onlytrue if the operation has no side effects (DefaultPolicy::has_side_effects)
annotations.destructivetrue if method is DELETE
annotations.idempotenttrue if method is GET, PUT, or DELETE
annotations.requires_approvalValue of x-chio-approval-required, defaulting to false
annotations.estimated_duration_msNone
pricingNone

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_schema is one JSON Schema object with type: "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 parameter description is copied into its property.
  • Required parameters appear in the required array, which is omitted when empty.
  • A request body schema appears under a top-level body property, listed in required only when requestBody.required is true.

Internal $ref pointers are resolved by the parser before a schema reaches the generator, so a consumer sees expanded schemas.

Generator configuration

OptionTypeDefaultEffect
server_idstring"openapi-server"Identifier for the generated manifest body
include_output_schemasbooleantrueWhether to derive output schemas from response definitions
respect_publish_flagbooleantrueWhether 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.

ExtensionScopeTypeDefaultMeaning
x-chio-sensitivityoperationenum stringinternalOne 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-effectsoperationbooleanmethod-drivenOverrides the method-based side-effect default. true forces deny-by-default; false forces session-scoped allow.
x-chio-approval-requiredoperationbooleanfalseWhen true, forces deny-by-default regardless of method or x-chio-side-effects. Sets annotations.requires_approval.
x-chio-budget-limitoperationunsigned 64-bit integernonePer-invocation cost cap in minor currency units. Consumed by the budget guard.
x-chio-publishoperationbooleantrueControls 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

LevelMeaning
publicPublicly available data, no special handling
internalInternal data, logged but not restricted beyond defaults
sensitiveSensitive data, may require additional approval
restrictedHighly 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:

Methodx-chio-side-effectsx-chio-approval-requiredResulting policy
GETabsentabsentSessionAllow
GETabsenttrueDenyByDefault
GETtrueabsentDenyByDefault
GETfalsetrueDenyByDefault
POSTabsentabsentDenyByDefault
POSTfalseabsentSessionAllow
POSTfalsetrueDenyByDefault
POSTabsenttrueDenyByDefault

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.

spec/OPENAPI-INTEGRATION.mdyaml
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
        - name

Section 5.2 lists the generated tools. One row per ToolDefinition, with the annotations and the policy the route table assigns:

ToolDescriptionread_onlydestructiveidempotentrequires_approvalPolicy
listPetsList all petstruefalsetruefalseSessionAllow
createPetCreate a petfalsefalsefalsefalseDenyByDefault
showPetByIdInfo for a specific pettruefalsetruefalseSessionAllow
deletePetDelete a petfalsetruetruetrueDenyByDefault

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.

CategoryMethodsDefault policy
Safe (read-only)GET, HEAD, OPTIONSSessionAllow
Side-effect (mutating)POST, PUT, PATCH, DELETEDenyByDefault

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

  1. If x-chio-approval-required is true, the check returns DenyByDefault and takes highest precedence.
  2. If x-chio-side-effects is explicitly set, it overrides the method default: true forces DenyByDefault, false forces SessionAllow.
  3. 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.

PriorityHeaderIdentity format
1Authorization: Bearer <token>bearer:<truncated-sha256>
2x-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:

json
{
  "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"
}
FieldTypeDescription
errorstringAlways "chio_access_denied"
messagestringThe verdict's reason
receipt_idstringID of the signed receipt that records this denial
suggestionstringActionable 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:

ErrorCondition
InvalidJsonInput detected as JSON but failed to parse
InvalidYamlInput detected as YAML but failed to parse
MissingFieldA required top-level field (openapi, info, or paths) or a parameter's name or in is absent
UnsupportedVersionThe openapi version does not begin with 3.
UnresolvedRefA $ref pointer could not be resolved (external URI or nonexistent internal path)
InvalidSpecparameters 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:

ErrorCondition
SpecLoadThe OpenAPI spec file cannot be read or auto-discovery failed
SpecParseThe loaded spec failed OpenAPI parsing (wraps OpenApiError)
ConfigConfiguration error: the durable-receipts gate, an invalid upstream URL, or a listen address that cannot be bound
UpstreamThe upstream request failed
EvaluationPolicy evaluation failed: caller identity hashing, content hashing, or the kernel
PendingApprovalThe route is approval-gated and the decision is pending. Carries approval_id and kernel_receipt_id and drives the HTTP 409 approval workflow.
ReceiptSignReceipt signing failed
ReceiptStoreReceipt persistence failed, or a durable store could not be opened at startup
IoIO error during server operation
HttpClientHTTP 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:

json
{
  "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

bash
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:

FlagRequiredDefaultDescription
--upstream <UPSTREAM>YesUpstream base URL to proxy to
--spec <SPEC>Noauto-discoveredOptional local OpenAPI spec path. Auto-discovered when omitted
--listen <LISTEN>No127.0.0.1:9090Address to listen on
--receipt-store <RECEIPT_STORE>NoOptional SQLite receipt store path. The global --receipt-db is used when this flag is absent.
--allow-ephemeral-receiptsNofalsePermit 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>No20 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.
reference · api-protect-boot-gatetranscript
$ chio api protect --upstream http://127.0.0.1:1
error [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.
exit 1

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:

PriorityPath
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 fieldSource
idContent-addressed receipt id (SHA-256 of the canonical receipt body with id removed), assigned by HttpReceipt::sign
request_idA UUIDv7 minted per request; a retry that presents an execution nonce reuses the request id the nonce is bound to
route_patternMatched OpenAPI path pattern (e.g., /pets/{petId}), or the raw path when no route matched
methodHTTP method
caller_identity_hashSHA-256 of the canonical CallerIdentity
verdictAllow, or Deny with reason and guard name; Incomplete on a strict nonce preflight
receipt_kindmediated_decision
boundary_classprevent
tool_origincaller_executed
redaction_modenone
actor_chainEmpty, and therefore omitted from the wire
evidenceDefaultPolicyGuard or CapabilityGuard entries from the HTTP authority
response_statusThe 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
timestampUnix timestamp when the receipt was signed
content_hashChioHttpRequest::content_hash over method, route pattern, path, query, and body hash
policy_hashSHA-256 hash of the loaded OpenAPI spec bytes
trust_levelmediated
capability_idThe id of the presented capability token, when one was presented
metadatachio_http_status_scope (decision at evaluation, final after the response status is known) and chio_decision_receipt_id on the finalized receipt
kernel_keyPublic 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

  1. Apply the durable-receipts gate: refuse to start without a durable receipt store unless ephemeral receipts were allowed.
  2. Load the OpenAPI spec (from --spec or via auto-discovery).
  3. Parse the spec using chio-openapi and build a route table mapping (method, path pattern) to a PolicyDecision through DefaultPolicy::for_method_with_extensions.
  4. Derive the signing keypair from the seed, or generate one.
  5. Compute the SHA-256 hash of the spec content for use as policy_hash in receipts.
  6. Open the receipt, approval, revocation, and authority stores: on the receipt store file and its sibling files when one is configured, in memory otherwise.
  7. Bind to the --listen address and begin accepting requests.

The proxy logs the number of routes loaded, the upstream URL, and the bound address at startup.


Implementation references

ComponentCrateEntry point
OpenAPI parserchio-openapicrates/protocol/chio-openapi/src/parser.rs
Manifest generatorchio-openapicrates/protocol/chio-openapi/src/generator.rs
Extension vocabularychio-openapicrates/protocol/chio-openapi/src/extensions.rs
Default policychio-openapicrates/protocol/chio-openapi/src/policy.rs
Reverse proxychio-api-protectcrates/products/chio-api-protect/src/proxy.rs
Request evaluatorchio-api-protectcrates/products/chio-api-protect/src/evaluator.rs
Spec discoverychio-api-protectcrates/products/chio-api-protect/src/spec_discovery.rs
CLI commandchio-clicrates/products/chio-cli/src/cli/runtime.rs
Convenience functionchio-openapichio_openapi::tools_from_spec()