Chio/Docs

Sidecar HTTP Service

The sidecar pattern runs chio as its own process and image. The host application talks to the kernel over local HTTP at localhost:9090, and the signing key never enters the host's address space. This is the canonical production posture: it gives you an independent trust boundary, independent rollout cadence, and language portability for the host.

When to read this page

This is the operational deep dive. For the trust-boundary discussion and the high-level choice between sidecar and in-process, see Deployment Topologies.

Why Sidecar

  • Independent trust boundary. The Ed25519 signing key is mounted into the sidecar container and is unreachable from the host container. A compromised host cannot forge receipts.
  • Independent scaling. Roll the kernel forward without redeploying the host. Useful when kernel and host are owned by different teams.
  • Language portability. The host can be any language that speaks HTTP. The same sidecar image serves Rust, Python, Go, Node, .NET, and Java hosts.
  • First-class platform support. Cloud Run, ECS Fargate, and Azure Container Apps all support multi-container deployments with localhost networking and startup ordering.

The cost is a localhost-HTTP hop on every kernel call (about 100 microseconds per call). Negligible for tool-call workloads, noticeable on hot inner loops.

rendering…
The sidecar runs in its own container alongside the host. The host reaches the kernel through localhost:9090. The signing key is mounted into the sidecar only.

The Sidecar Image

Two reference Dockerfiles ship in the chio repository:

PathBaseUse
Dockerfile.sidecarAlpine 3.22 + tiniDefault sidecar image. Small, musl-linked, runs as uid/gid 10001.
deploy/sidecar/Dockerfiledistroless cc-debian12 nonroot (uid 65532)Distroless variant with a baked-in curl for HEALTHCHECK and a multi-arch shared-library layout.

Both build the same chio binary from chio-cli; the difference is the runtime layer. For a deeper Dockerfile-by-Dockerfile breakdown, see Container Images.


Build

From the chio repository root:

bash
docker build -f Dockerfile.sidecar -t chio-sidecar:local .

The builder stage installs protoc because chio-envoy-ext-authz (reachable transitively through the workspace) invokes tonic-build at compile time. CI installs protobuf-compiler for the same reason. Keeping the Docker build consistent with CI prevents a future dependency change from silently breaking the image.

The build copies the full workspace so path dependencies resolve. Specifically:

  • wit/ is consumed by chio-wasm-guards via wasmtime::component::bindgen! (reached through the chio-cli -> chio-wasm-guards path dependency).
  • examples/, formal/, and tests/ are workspace members; their absence breaks Cargo.lock resolution.
  • sdks/ is copied for forward-compatibility with non-workspace SDKs that may join the root workspace.

For the distroless variant:

bash
docker build -f deploy/sidecar/Dockerfile -t ghcr.io/backbay-labs/chio-sidecar:latest .

That command is the canonical build invocation in the sidecar deploy manifests. deploy/sidecar/Dockerfile adds a stage that bakes curl plus its shared libraries into the runtime image so the distroless layer can run a HEALTHCHECK probe without a shell.


Run

The image's zero-argument default is --help. Every chio subcommand needs operator input (policy path, wrapped server command, etc.), so a bare docker run prints usage rather than crashing. Operators always override CMD at deploy time.

bash
docker run --rm -p 8939:8939 chio-sidecar:local <subcommand> [args...]

The two production-grade subcommands are chio api protect (reverse proxy in front of an OpenAPI host) and chio mcp serve-http (HTTP-fronted MCP tool server). A typical api protect invocation:

bash
chio mcp serve-http \
  --policy /etc/chio/policy.yaml \
  --server-id my-tool

No useful zero-arg default

Both chio run and chio mcp serve-http require --policy plus additional positional input. The image falls through to --help on a bare invocation rather than exiting non-zero before the health endpoint opens.

Listen Address

The sidecar listens on 0.0.0.0:9090 by default and exposes GET /chio/health. Both values are baked into deploy/sidecar/Dockerfile as defaults:

dockerfile
ENV CHIO_LISTEN_ADDR=0.0.0.0:9090 \
    CHIO_HEALTH_PATH=/chio/health \
    CHIO_LOG_LEVEL=info \
    CHIO_KERNEL_CONFIG_PATH=/etc/chio/kernel.yaml

Override at deploy time by setting the env vars in the orchestrator manifest. The application container reaches the kernel through http://localhost:9090 because pod / task / revision containers share the network namespace.


Health Endpoints

The sidecar exposes a single health path, GET /chio/health by default. The path is governed by CHIO_HEALTH_PATH.

  • Liveness: 200 means the process is up and the kernel is constructed.
  • Readiness: 200 means the kernel has loaded its policy and the receipt sink is reachable. A failing readiness check causes the orchestrator to keep traffic off this replica.
  • Fail closed: if the kernel cannot load CHIO_KERNEL_CONFIG_PATH or the policy bundle at startup, the process exits non-zero. The orchestrator marks the container unhealthy (ECS, Azure) or fails the revision (Cloud Run).

The distroless sidecar bakes in a HEALTHCHECK that hits the same path:

dockerfile
HEALTHCHECK --interval=10s --timeout=5s --start-period=15s --retries=3 \
  CMD ["/usr/bin/curl", "-fsS", "http://localhost:9090/chio/health"]

Environment Variables

The sidecar reads a small, fixed set of environment variables. Two of them have defaults baked into the image; the rest must be provided at deploy time.

VariableDefaultPurpose
CHIO_LISTEN_ADDR0.0.0.0:9090Bind address for the sidecar HTTP listener.
CHIO_HEALTH_PATH/chio/healthPath the orchestrator probes for liveness and readiness.
CHIO_KERNEL_CONFIG_PATH/etc/chio/kernel.yamlPath to the kernel YAML config inside the container.
CHIO_POLICY_SOURCE(none)Policy bundle URL (gs://, s3://, https://, or a bundled path).
CHIO_RECEIPT_SINK(none)Receipt destination (BigQuery, DynamoDB, Cosmos DB, stdout, etc.).
CHIO_SIGNING_KEY(none)Ed25519 signing seed for receipts. MUST come from a managed secret backend.
CHIO_CAPABILITY_AUTHORITY_URL(none)URL of the capability authority issuing tokens.
CHIO_LOG_LEVELinfoTracing log level (info / warn / error).
CHIO_TRUSTED_ISSUER_KEY(none)Single trusted issuer public key (hex-encoded Ed25519). Read by chio api protect.
CHIO_TRUSTED_ISSUER_KEYS(none)Multiple trusted issuer keys, comma-separated. Read alongside CHIO_TRUSTED_ISSUER_KEY; the union is used.
CHIO_SIDECAR_CONTROL_TOKEN(none)Bearer token for the sidecar admin surface. Falls back to CHIO_API_PROTECT_CONTROL_TOKEN.

Verified against crates/chio-cli/src/cli/runtime.rs

CHIO_TRUSTED_ISSUER_KEY, CHIO_TRUSTED_ISSUER_KEYS, and CHIO_SIDECAR_CONTROL_TOKEN are read directly by chio api protect at startup. CHIO_API_PROTECT_CONTROL_TOKEN is the legacy alias.

Wire Protocol

The sidecar exposes the kernel surface over HTTP. Hosts call endpoints for capability validation, tool dispatch, and receipt retrieval; the wire format is documented in Wire Protocol. Every request is serialized as JSON; binary payloads are base64-encoded.

For the chio api protect mode the sidecar is itself the front door: it terminates the inbound request, validates capabilities, runs guards, forwards to the upstream host, and signs a receipt on the way out. The host never sees the original capability token.


Graceful Shutdown

On SIGTERM the sidecar runs a drain sequence:

  1. Stop accepting new connections on the listen address.
  2. Let in-flight requests finish. A request that has already passed the guard pipeline is allowed to complete and emit a receipt.
  3. Flush the receipt log to the configured sink (CHIO_RECEIPT_SINK).
  4. Drain the signing task and emit a final checkpoint if a checkpoint boundary is crossed.
  5. Exit 0.

tini is PID 1 in both Dockerfiles, so signals reach the chio binary correctly. If the drain takes longer than the orchestrator's grace period, the platform escalates to SIGKILL; in-flight receipts buffered in memory at that point may be lost.

Set a grace period that fits the receipt sink

The drain time is dominated by the receipt-sink flush. If the sink is BigQuery or DynamoDB, expect a tail of a few seconds. Set the orchestrator's termination grace period to at least 30 seconds.

Process Supervision

systemd

For bare-metal or VM hosts, run the sidecar under systemd with a unit that pins the binary, mounts the secret directory, and restarts on failure.

/etc/systemd/system/chio-sidecar.service
[Unit]
Description=Chio sidecar HTTP service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=chio
Group=chio
ExecStart=/usr/local/bin/chio mcp serve-http \
    --policy /etc/chio/policy.yaml \
    --server-id my-tool \
    --listen 0.0.0.0:9090
Environment=CHIO_KERNEL_CONFIG_PATH=/etc/chio/kernel.yaml
Environment=CHIO_LOG_LEVEL=info
EnvironmentFile=/run/secrets/chio.env
Restart=on-failure
RestartSec=2s
TimeoutStopSec=30s
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/chio

[Install]
WantedBy=multi-user.target

Container orchestrators

For multi-container platforms, the sidecar ships next to the host and the platform handles startup ordering and health gating. The orchestrator-specific manifests live under deploy/:

  • Cloud Run uses run.googleapis.com/container-dependencies plus an httpGet startup probe.
  • ECS Fargate uses dependsOn with condition: HEALTHY and a curl-based healthCheck.
  • Azure Container Apps uses Bicep multi-container probe sequencing.

Worked Example

Run the sidecar locally, send a request, observe the receipt.

bash
# 1. Build the image from the chio repo root.
$ docker build -f Dockerfile.sidecar -t chio-sidecar:local .

# 2. Run the sidecar with a local policy and a stdout receipt sink.
$ docker run --rm \
    -p 9090:9090 \
    -v "$(pwd)/policy.yaml:/etc/chio/policy.yaml:ro" \
    -v "$(pwd)/kernel.yaml:/etc/chio/kernel.yaml:ro" \
    -e CHIO_KERNEL_CONFIG_PATH=/etc/chio/kernel.yaml \
    -e CHIO_RECEIPT_SINK=stdout \
    -e CHIO_SIGNING_KEY="$(cat ./secrets/signing-seed.hex)" \
    chio-sidecar:local \
    mcp serve-http \
      --policy /etc/chio/policy.yaml \
      --server-id local-mock \
      --server-name "Local Mock" \
      --listen 0.0.0.0:9090

# 3. From another shell: hit the health endpoint.
$ curl -fsS http://localhost:9090/chio/health
{"status":"ok"}

# 4. Issue a tool call (shape depends on the wrapped server).
$ curl -fsS -X POST http://localhost:9090/invoke \
    -H "Authorization: Bearer $CAPABILITY_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"tool":"echo","arguments":{"message":"hello"}}'

# 5. Watch the receipt arrive in the stdout sink.
$ docker logs <container> 2>&1 | grep '"kind":"chio_receipt"'

For an end-to-end example with a real upstream, see Hello Tool.


Sidecar vs In-Process

PropertyIn-ProcessSidecar
Trust boundaryHost process holds the signing keySidecar container holds the signing key
Latency per kernel callNo IPC. Function call cost.Localhost HTTP. About 100 microseconds.
Independent scalingNo. Kernel scales with the host.Yes. Kernel and host scale separately.
Hot-reload policyNo. Restart the host.Yes. Roll the sidecar revision.
Host languageRust onlyAny language with an HTTP client
Recommended forTrusted single-tenant binariesUntrusted code, multi-tenant, third-party agents

Next Steps

Sidecar HTTP Service · Chio Docs