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.
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.
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.
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(),
))
}
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.
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.
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.
chio new pricing-service --example pricing-service
cd pricing-service && pip install .
uvicorn app:app --port 8000Start 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.
chio api protect --computer quickstart --service pricing \
--spec openapi.yaml --upstream http://127.0.0.1:8000 \
--listen 127.0.0.1:9090service 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.
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);from chio_asgi import ChioASGIMiddleware
from chio_asgi.config import ChioASGIConfig
from fastapi import FastAPI
from pydantic import BaseModel, Field
from pricing import quote
app = FastAPI(title="pricing")
app.add_middleware(
ChioASGIMiddleware,
config=ChioASGIConfig(sidecar_url="http://127.0.0.1:9090",
exclude_paths=frozenset({"/healthz"})),
)
class QuoteRequest(BaseModel):
sku: str
percent: float = Field(ge=0, le=100)
@app.post("/v1/quote")
async def quote_route(body: QuoteRequest) -> dict[str, object]:
return quote(body.sku, body.percent)mod pricing;
use axum::{http::StatusCode, routing::post, Json, Router};
use chio::Computer;
use chio_tower::ChioLayer;
use pricing::{Quote, QuoteRequest};
async fn quote(Json(body): Json<QuoteRequest>) -> Result<Json<Quote>, (StatusCode, String)> {
pricing::quote(&body.sku, body.percent).map(Json).map_err(|e| (StatusCode::NOT_FOUND, e))
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let project = Computer::open("quickstart").await?;
// A serving key your kernel certifies, and the hash of the policy in force.
let node_key = project.node_key("pricing-http").await?;
let policy_hash = project.policy().hash().await?;
let layer = ChioLayer::builder(node_key, policy_hash)
.receipt_store(project.history().receipt_store())
.revocation_store(project.revocation_store())
.build()?;
let app = Router::new().route("/v1/quote", post(quote)).layer(layer);
let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await?;
axum::serve(listener, app).await?;
Ok(())
}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.
export BASALT_GRANT="$(chio agent token caller.json)"grant cap-01ab…77d1, pricing.quote, signed by kernel 4f1c…a2e9
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());200 {"sku":"sku-118","percent":10.0,"price":21.6,"currency":"USD"}import os
import httpx
response = httpx.post(
"http://127.0.0.1:9090/v1/quote",
headers={"X-Chio-Capability": os.environ["BASALT_GRANT"]},
json={"sku": "sku-118", "percent": 10},
)
print(response.status_code, response.text)200 {"sku":"sku-118","percent":10.0,"price":21.6,"currency":"USD"}let grant = std::env::var("BASALT_GRANT")?;
let response = reqwest::Client::new()
.post("http://127.0.0.1:9090/v1/quote")
.header("X-Chio-Capability", grant)
.json(&serde_json::json!({ "sku": "sku-118", "percent": 10 }))
.send()
.await?;
println!("{} {}", response.status().as_u16(), response.text().await?);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.
chio mcp serve-http --computer quickstart --server-id pricing \
--listen 127.0.0.1:8931 -- python mcp_server.pyConnect as Basalt's agent, list the tools, and request a quote.
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();
}quote sku-204 at 25%: USD 43.50
import os
from chio import ChioClient
client = ChioClient.with_static_bearer("http://127.0.0.1:8931", os.environ["BASALT_GRANT"])
session = client.initialize()
try:
tools = session.list_tools()["result"]["tools"]
print(" · ".join(tool["name"] for tool in tools))
result = session.call_tool("quote", {"sku": "sku-204", "percent": 25})["result"]
quote = result["structuredContent"]
print(f"{quote['sku']} at {quote['percent']:g}%: {quote['currency']} {quote['price']:.2f}")
finally:
session.close()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:
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}'{"sku":"sku-118","percent":10.0,"price":21.6,"currency":"USD"}Then as an agent with no grant from your kernel:
curl -s -i -X POST http://127.0.0.1:9090/v1/quote \
-H "content-type: application/json" \
-d '{"sku":"sku-118","percent":10}'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:
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);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
from chio import Computer
project = Computer.open("quickstart")
calls = project.history.receipts(service="pricing", since="10m")
for receipt in calls:
print(receipt.summary)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
use chio::Computer;
let project = Computer::open("quickstart").await?;
let calls = project.history().receipts().service("pricing").since("10m").all().await?;
for receipt in &calls {
println!("{}", receipt.summary());
}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.