Chio/Docs
LOGIN · JOIN

Serve two customers with tenant-scoped receipts

Give Harbor Goods and Ostrander their own quotes, activity views, and receipt exports from one pricing service, separated by verified identity.

Guide chio@5f63e6e

On this page

One pricing service and one kernel serve two of Larkfield's customers, and each customer's verified identity decides which account a quote lands in and which receipts they can read.

Priya Raman buys for Harbor Goods (tenant harbor-goods) and Tomás Ibarra for Ostrander (ostrander). The host validates their credentials, binds the verified tenant to the Chio session, and applies the same mapping when either of them lists, looks up, or exports receipts. A tenant value in a request body or query is never the source of authority.

Start
Tenant outcomesChoose an outcome.

Priya's identity selects Harbor Goods' account

Priya Raman's verified membership selects her session and capability, and the host adds harbor-goods to the call. The quote lands in quotes/harbor-goods, and the kernel signs tenant_id: harbor-goods into the receipt. Tenancy and read scoping.

One quote request per customer through the same host and kernel. Tenant IDs, prices, and status codes come from the captured run; receipt reads, which the same identity mapping scopes, are left out.

Both quotes take the same endpoint and kernel and end in different receipts, because identity picks the tenant before the call. The third request names a tenant in its body and never becomes a call.

1. Start the customer service

Starts from chio new pricing-desk --example pricing-desk, or the same project as a download. The desk is Larkfield's pricing functions behind small hosts that embed the kernel, so it needs no separate Chio process. Cargo selects Rust 1.94.1 from the project's toolchain file and fetches the Chio crates its manifest pins; the first build takes a few minutes.

Terminal · pricing-deskbash
chio new pricing-desk --example pricing-desk && cd pricing-desk
cargo run --locked --bin customer-service

The host keeps its state in state/customers and signs receipts into state/customers/receipts.db. It serves its interface at 127.0.0.1:4311; keep the terminal running.

Before you begin

Use a Unix-compatible system with durable file locking, Rust through rustup, and curl and jq for the client commands. Keep the selected port free. The project README lists every endpoint, credential file, and supported persistence setting.

You should see priya-token.txt, tomas-token.txt, and operator-token.txt under the state directory: one-hour JWT credentials from a local issuer whose key stays on the host. No endpoint lets a customer mint a tenant identity.

2. Request one quote as each customer

Call the same endpoint with Priya's credential, then Tomás's. In the browser, open two tabs with one credential each.

Terminal 2 · pricing-deskbash
PRIYA_TOKEN="$(cat state/customers/priya-token.txt)"
TOMAS_TOKEN="$(cat state/customers/tomas-token.txt)"
quote() {
  curl -sS http://127.0.0.1:4311/quotes -H "Authorization: Bearer $1" \
    -H 'Content-Type: application/json' -d "$2"
}
quote "$PRIYA_TOKEN" '{"sku":"sku-118","percent":10}' > priya.json
quote "$TOMAS_TOKEN" '{"sku":"sku-204","percent":25}' > tomas.json
jq '{verdict, output: (.output | {sku, price}), tenant: .receipt.tenant_id, receipt_id}' priya.json tomas.json
Output
{
  "verdict": "Allow",
  "output": { "sku": "sku-118", "price": "21.60" },
  "tenant": "harbor-goods",
  "receipt_id": "34f8a4487e5b31498166ae612138abf35e66a5c9cc06516757a3306c9120ed26"
}
{
  "verdict": "Allow",
  "output": { "sku": "sku-204", "price": "43.50" },
  "tenant": "ostrander",
  "receipt_id": "4489b06d387ff5231e04db5381688bb74bf596e487e4ef2aa900acf5487e2550"
}

You should see two allowed calls with their own receipt IDs and tenants. Priya's membership selected her session and capability, the host added harbor-goods to the call, the quote tool wrote the quote into Harbor Goods' directory, and the kernel signed tenant_id: harbor-goods into the receipt. The body accepts only sku and percent, so it cannot supply a tenant or a capability.

How the host binds the tenant to execution

The host verifies the JWT signature, algorithm, issuer, audience, expiry, and account membership before it creates a session. It sets federated_claims.tenant_id in the authenticated session and executes a ToolCallOperation; the host checks the signed attribution before returning the result.

src/customers.rs · session-bound executionrust
kernel.set_session_auth_context(&session, authenticated_context)?;
kernel.activate_session(&session)?;
// The host adds the tenant from verified claims; the body cannot supply it.
let response = kernel.evaluate_session_operation(&context, &operation)?;

3. Try to widen Priya's authority

Ask for Ostrander's receipts with Priya's credential, then request a quote that names Ostrander in its body.

Terminal 2 · pricing-deskbash
curl -sS -w ' HTTP %{http_code}\n' 'http://127.0.0.1:4311/receipts?tenant=ostrander' \
  -H "Authorization: Bearer $PRIYA_TOKEN"
curl -sS -w ' HTTP %{http_code}\n' http://127.0.0.1:4311/quotes \
  -H "Authorization: Bearer $PRIYA_TOKEN" -H 'Content-Type: application/json' \
  -d '{"sku":"sku-118","percent":10,"tenant":"ostrander"}'
Output
{"error":"receipt read boundary error: receipt query tenant filter cannot widen authenticated tenant scope"} HTTP 409
Failed to deserialize the JSON body into the target type: tenant: unknown field `tenant`, expected `sku` or `percent` at line 1 column 38 HTTP 422

You should see HTTP 409 for the widened receipt filter and HTTP 422 for the extra field. Both are application boundary checks, so no tool runs and no receipt is signed. The operator credential has the mirror-image limits:

OperationPriya or TomásOperator
Request or list quotesOwn tenant onlyRefused
List or export receiptsOwn tenant’s signed recordsBoth tenants, administrative view
Exact receipt lookupOwn receipt; others return 404Both tenants
Tenant in a quote bodyRejected as unknown fieldNo invocation route

Administrative evidence access grants no tool authority: the operator reads both tenants’ receipts through an explicit administrative read context and has no route that requests a quote for a customer.

4. Look up Tomás's receipt as Priya

Put Tomás's receipt ID from step 2 into an exact lookup with Priya's credential.

Terminal 2 · pricing-deskbash
curl -sS -w ' HTTP %{http_code}\n' "http://127.0.0.1:4311/receipts/$(jq -r .receipt_id tomas.json)" \
  -H "Authorization: Bearer $PRIYA_TOKEN"
Output
{"error":"Receipt not found in your authorized view"} HTTP 404

You should see 404: the receipt is outside Priya's authorized view. Tomás's credential returns his complete signed receipt. In Priya's tab, Load my activity shows only Harbor Goods' receipts.

The receipt read boundary
src/customers.rs · scoped receipt queryrust
let context = ReceiptReadContext::authenticated_tenant(tenant);
let rows = store.query_receipts(&ReceiptQuery {
    read_context: Some(context),
    limit: 50,
    ..Default::default()
})?;

5. Export and verify each customer’s receipts

Export Priya's receipts and count the tenants they carry.

Terminal 2 · pricing-deskbash
curl -sS http://127.0.0.1:4311/export -H "Authorization: Bearer $PRIYA_TOKEN" \
  -o harbor-goods-receipts.ndjson
jq -r .tenant_id harbor-goods-receipts.ndjson | sort | uniq -c
Output
   1 harbor-goods

You should see one tenant. The export walks every scoped page and writes complete signed records as NDJSON; Tomás's export carries only ostrander. Download my receipts in each tab produces the same files. The captured run holds both attributed quotes and every refusal above.

Complete source: src/customers.rs, customers.html, src/lib.rs, all in the project download.

The resource boundary matches the receipt boundary

Receipt scoping protects evidence; the quotes need their own boundary. The quote tool files each quote under the tenant the host added from verified claims, state/customers/quotes/harbor-goods or ostrander. Neither the request body nor a URL parameter chooses it. Request distinguishable quotes and compare the files in both directories; the application CI exercises both boundaries.

In your service, validate the identity provider you trust, map verified organization membership on every request, and apply that mapping to lists, exact lookups, pagination, exports, and resource reads. A fresh login never renews tool authority: restarting the example issues new one-hour logins while the saved one-day tool capabilities keep their expiry. Renew tool authority through an explicit, trusted provisioning path.

Next: run the boundaries in CI

Serve two customers with tenant-scoped receipts · Chio Docs