Chio/Docs
LOGIN · JOIN

Connect an Application

Run the pricing service in your own stack and put the kernel in front of it, with the grants you already issued.

Guide chio@5f63e6e

On this page

Part 2 of the Quickstart published the pricing code from inside the computer. Your service may already run in your own framework, on your own hosts. Put the kernel there: the grant you gave Basalt admits the same calls, and every decision lands in the same history.

Continues from Quickstart part 2, with the quickstart project, the pricing service, and Basalt's grant for quote. You run the pricing service three ways: with a check inside its handlers, behind an HTTP sidecar, and as an MCP server behind the edge. Then you prove one allowed call and one refused call through it.

Choose an entry point

The kernel governs the routes that pass through it

Draw the service as a graph: the calling agent, the kernel, the handler that runs quote, and the price table it reads. Admission is mandatory for the price table only when every route from the agent to it crosses the kernel.

Routes from the agent to the resource, and whether every one crosses the kernelcut at Kwritable mounttnative toolinherited credentialrequestadmitteddispatchaKdRagentkerneladapterrepository/governed
mandatory(R)  ⟺  ∀ π∈Π(a,R): K∈π\text{mandatory}(R) \iff \forall\, \pi \in \Pi(a, R):\ \htmlClass{role-seal}{K} \in \pi
π0=(a,K,d,R)∋K\htmlClass{role-seal}{\pi_0 = (a, K, d, R) \ni K}
π1=(a,R)mount∌K\pi_1 = (a, R)_{\text{mount}} \not\ni K
π2=(a,t,R)∌K\pi_2 = (a, t, R) \not\ni K
π3=(a,R)cred∌K\pi_3 = (a, R)_{\text{cred}} \not\ni K
mandatory(R)=1\text{mandatory}(R) = \htmlClass{role-seal}{1}
every route to R crosses the kernelclick a dashed route to grant it
Routes from the agent to the resource, and whether every one crosses the kernelcut at Kwritable mounttnative toolinherited credentialaKdRagentkerneladapterrepository/governed
mandatory(R)  ⟺  ∀ π∈Π(a,R): K∈π\text{mandatory}(R) \iff \forall\, \pi \in \Pi(a, R):\ \htmlClass{role-seal}{K} \in \pi
π0=(a,K,d,R)∋K\htmlClass{role-seal}{\pi_0 = (a, K, d, R) \ni K}
π1=(a,R)mount∌K\pi_1 = (a, R)_{\text{mount}} \not\ni K
π2=(a,t,R)∌K\pi_2 = (a, t, R) \not\ni K
π3=(a,R)cred∌K\pi_3 = (a, R)_{\text{cred}} \not\ni K
mandatory(R)=1\text{mandatory}(R) = \htmlClass{role-seal}{1}
click a dashed route to grant it

Every route from the agent to the repository crosses the kernel, so admission is mandatory.

In Rust · lib.rs:25913 lines
/// Invoke a bridged tool. A dispatcher is required so the kernel cannot
/// sign successful receipts for simulated side effects.
pub fn invoke_tool(&self, tool_name: &str, arguments: Value) -> Result<Value, BridgeError> {
    // …
    if let Some(dispatcher) = &self.dispatcher {
        // … egress contract checks
        let response = dispatcher(&binding.method, &url, &arguments)?;
        // …
    } else {
        Err(BridgeError::Kernel(
            "OpenAPI bridge requires a dispatcher for live tool invocation".to_string(),
        ))
    }
An illustrative deployment. The host, not Chio, enforces the cut: it decides which processes run and which credentials, handles, and mounts they receive. An embedded library cannot isolate itself from a compromised host.Architecture

Each extra route is a second source of authority. A caller refused at the sidecar can still reach the handler on port 8000 if that port is open to it. Bind the service to localhost or a private network, and test the other routes in step 7.

1. Choose where the call enters the kernel

The kernel is a library, linked into whichever process serves the entry point. Each entry point below is such a process, and each checks a call before the pricing code sees it.

Entry pointsInspect a process.
--control-urlaagent workerRresourceK₁process hostchio process serveσ₁local filescMCP clientsMCP serverK₂MCP edgechio mcp serveσ₂hHTTP clientuupstream APIK₃HTTP sidecarchio api protectσ₃σshared statechio trust serveaworkerRresourceK₁process hostchio process serveσ₁local filescMCP clientsMCP serverK₂MCP edgechio mcp serveσ₂hHTTP clientuupstreamK₃HTTP sidecarchio api protectσ₃σsharedstate

Processes share state only through a configured store

Point every process at one trust-control service, chio trust serve, with --control-url. It issues capabilities and holds revocations, budget holds, and receipts, so a revocation recorded through one process is visible to the others. Architecture.

A deployment that uses all three entry points; most use one or two. Each kernel node is the same library linked into a different process. The operator’s CLI, cluster peers, and offline verifiers attach to the trust-control service.Architecture

Choose by the operation you govern. Middleware fits when you own the service and want the check beside the handler. A sidecar fits an API you cannot change. The MCP edge fits callers that speak MCP. A deployment can use all three; framework adapters cover the agent side of the same boundary.

--computer quickstart attaches each process to the project's kernel state. The process verifies grants your kernel issued, reads the policy in force, consults the project's revocations and budgets, and appends receipts to the project's history. A revocation recorded through one process is visible to the others.

2. Get the service and start the kernel beside it

Pull the pricing service, or download the same bundle. It carries the pricing functions from main after part 3, served as a FastAPI app, an Express app, an axum app, and an MCP server, with the OpenAPI document for its three routes.

Terminal 1bash
chio new pricing-service --example pricing-service
cd pricing-service && pip install .
uvicorn app:app --port 8000

Start the kernel beside it. The sidecar maps each request to an operation by its operationId in openapi.yaml, so POST /v1/quote is the quote operation your grants name.

Terminal 2bash
chio api protect --computer quickstart --service pricing \
  --spec openapi.yaml --upstream http://127.0.0.1:8000 \
  --listen 127.0.0.1:9090
Output
service    pricing, operations quote · discount · refund from openapi.yaml
grants     issued by kernel 4f1c…a2e9 for quickstart
policy     sha256 61d0…a7f3
receipts   quickstart history
listening  127.0.0.1:9090, forwarding admitted requests to 127.0.0.1:8000

3. Check calls inside your handlers

Middleware puts the check in front of each handler. The Express and FastAPI middleware ask the kernel in the sidecar for each verdict. The Rust layer links the kernel into your service, attached to the same project.

node/server.tsapp.pyrust/src/main.rs
import express from "express";
import { chio, chioErrorHandler } from "@chio-protocol/express";
import { quote } from "./pricing.ts";

const app = express();
app.use(chio({ sidecarUrl: "http://127.0.0.1:9090", skip: ["/healthz"] }));
app.use(express.json());

app.post("/v1/quote", (req, res) => {
  res.json(quote(req.body.sku, req.body.percent));
});

app.use(chioErrorHandler);
app.listen(8000);

A refused call never reaches quote. An admitted one carries its receipt ID in the x-chio-receipt-id response header, and the Express handler can read it from req.chioResult. In Rust, policy_hash is the hash every receipt records, so a receipt written by this process names the same policy as one written by the sidecar.

Read more in Add API Middleware.

4. Call the service through the sidecar

The sidecar is also a reverse proxy, so it governs an API with no middleware at all. Callers send requests to port 9090 with their grant in the X-Chio-Capability header. The demo caller from part 2 holds Basalt's grant; export it for the calls below.

Terminal 3bash
export BASALT_GRANT="$(chio agent token caller.json)"
Output
grant      cap-01ab…77d1, pricing.quote, signed by kernel 4f1c…a2e9
caller.tscaller.pysrc/main.rs · in main
const response = await fetch("http://127.0.0.1:9090/v1/quote", {
  method: "POST",
  headers: {
    "content-type": "application/json",
    "X-Chio-Capability": process.env.BASALT_GRANT!,
  },
  body: JSON.stringify({ sku: "sku-118", percent: 10 }),
});
console.log(response.status, await response.text());
Output
200 {"sku":"sku-118","percent":10.0,"price":21.6,"currency":"USD"}

The sidecar checked the grant, the hourly limit, and the budget before it forwarded the request, then charged USD 0.01 to Basalt's budget.

5. Serve the MCP tools through the edge

The MCP edge starts mcp_server.py as a subprocess and checks each tools/call before the server sees it. It serves Streamable HTTP and accepts a grant from your kernel as the session credential.

Terminal 4bash
chio mcp serve-http --computer quickstart --server-id pricing \
  --listen 127.0.0.1:8931 -- python mcp_server.py

Connect as Basalt's agent, list the tools, and request a quote.

client.tsclient.py
import { ChioClient } from "@chio-protocol/sdk";

type Quote = { sku: string; percent: number; price: number; currency: string };

const client = ChioClient.withStaticBearer("http://127.0.0.1:8931", process.env.BASALT_GRANT!);
const session = await client.initialize();
try {
  const { tools } = (await session.listTools()) as { tools: { name: string }[] };
  console.log(tools.map((tool) => tool.name).join(" · "));
  const result = (await session.callTool("quote", { sku: "sku-204", percent: 25 })) as { structuredContent: Quote };
  const quote = result.structuredContent;
  console.log(`${quote.sku} at ${quote.percent}%: ${quote.currency} ${quote.price.toFixed(2)}`);
} finally {
  await session.close();
}
Output
quote
sku-204 at 25%: USD 43.50

The server exposes quote, discount, and refund; the edge lists only quote, because that is all the grant covers. A client that starts its servers over stdio uses chio mcp serve in its server command instead, as Govern a coding agent does for file tools.

6. Prove one allowed call and one refused call

Send the same request twice and change one thing, so the refusal can only come from that change. First with Basalt's grant:

Terminal 3bash
curl -s -X POST http://127.0.0.1:9090/v1/quote \
  -H "X-Chio-Capability: $BASALT_GRANT" \
  -H "content-type: application/json" \
  -d '{"sku":"sku-118","percent":10}'
Output
{"sku":"sku-118","percent":10.0,"price":21.6,"currency":"USD"}

Then as an agent with no grant from your kernel:

Terminal 3bash
curl -s -i -X POST http://127.0.0.1:9090/v1/quote \
  -H "content-type: application/json" \
  -d '{"sku":"sku-118","percent":10}'
Output
HTTP/1.1 403 Forbidden
content-type: application/json

{"error":"chio_access_denied","message":"side-effect route requires a capability token","receipt_id":"rcpt-01ad…3c14","suggestion":"provide a valid capability token in the X-Chio-Capability header or chio_capability query parameter"}

You should see a 403 with a machine-readable error and the ID of the receipt that records the refusal. Read both decisions from the project, beside the ones the edge recorded:

receipts.tsreceipts.pysrc/main.rs · in main
import { Computer } from "@chio-protocol/sdk";

const project = await Computer.open("quickstart");
const calls = await project.history.receipts({ service: "pricing", since: "10m" });
for (const receipt of calls) console.log(receipt.summary);
Output
rcpt-01ad…3a02  allow  z6MkbY4…e1R  quote  sku-118 at 10%  sidecar   USD 0.01
rcpt-01ad…3b70  allow  z6MkbY4…e1R  quote  sku-204 at 25%  mcp edge  USD 0.01
rcpt-01ad…3bd5  allow  z6MkbY4…e1R  quote  sku-118 at 10%  sidecar   USD 0.01
rcpt-01ad…3c14  deny   no grant     quote  sku-118 at 10%  sidecar   side-effect route requires a capability token

Calls through your own stack land in the same history as the calls part 2 served, signed by the same kernel, so Inspect Your Receipts and an evidence export cover both. Then check the price table itself. A receipt records the kernel's decision; the allowed call should return a quote, and the refused one should leave no trace in the service's logs.

7. Test what happens around the kernel

Stop the sidecar and send the allowed request to port 8000. The Express and FastAPI middleware fail closed: with no kernel to answer, they refuse. The Tower evaluator fails closed by default as well; its with_fail_open option forwards unenforced requests and counts them in chio_fail_open_suspected_total.

Then try to reach the price table without the kernel: the service port from another host, a database credential, a second process that imports pricing. If one works, close it in the host. chio check shows how the kernel would decide a request, but it does not exercise this dispatch path, so it cannot replace the two runs in step 6.

Read more in How a Call Enters the Kernel.

Next: follow the call through the kernel