Require human approval
Let an agent propose a deep discount and give Mara Okafor the final decision over exactly which price changes.
On this page
A pricing agent may propose any discount, but a price change above the quote-approval threshold waits for Mara Okafor, Larkfield's pricing lead. Her signed decision releases that exact change once, and the kernel still checks the agent's current authority before the price moves.
Part 2 of the Quickstart held Basalt's request for sku-301 at 80 percent until you approved it. This guide builds the same gate into a service you run: the agent and Mara hold different credentials, the host freezes the request before anyone reviews it, and an approval binds the hash of that request, so a changed price is a different request.
Mara's decision releases the exact request
Mara approves the preview's parameter_hash. The host consumes her signed decision once and submits the stored request, and the kernel admits publish_price, which writes price-changes/<approval-id>.json with the exact SKU, discount, and price she reviewed. Human-in-the-loop.
publish_price, and the price-change file f and receipt ρ follow. The proposal and Mara's reading of the preview are left out.Mara decides and the kernel still evaluates. A late decision stops at the host, and an approved decision is refused when the agent's grant has been revoked or has expired: consent never overrides current authority.
1. Start the approval desk
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.
chio new pricing-desk --example pricing-desk && cd pricing-desk
cargo run --locked --bin approval-deskThe host keeps its state in state/approvals and signs receipts into state/approvals/receipts.db. It serves its interface at 127.0.0.1:4310; 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 the desk's interface. The host writes agent-token.txt for the pricing agent and mara-token.txt for Mara into state/approvals, with Mara's approval key beside them. Each browser tab takes one credential.
2. Propose a deep discount as the agent
In a second terminal in the same directory, propose sku-301 at 80 percent for Basalt with the agent credential, then read the frozen preview with Mara's.
AGENT_TOKEN="$(cat state/approvals/agent-token.txt)"
MARA_TOKEN="$(cat state/approvals/mara-token.txt)"
curl -sS http://127.0.0.1:4310/proposals \
-H "Authorization: Bearer $AGENT_TOKEN" -H 'Content-Type: application/json' \
-d '{"sku":"sku-301","percent":80,"customer":"basalt"}' -o proposal.json
APPROVAL_ID="$(jq -r .approval_id proposal.json)"
curl -sS "http://127.0.0.1:4310/proposals/$APPROVAL_ID" \
-H "Authorization: Bearer $MARA_TOKEN" -o preview.json
jq '{status, change, pending: (.pending | {policy_id, tool_server, tool_name, parameter_hash, expires_at})}' preview.json{
"status": "pending",
"change": {
"change_id": "bedbf3c2-f378-4a46-8c96-ce5c03ca23d7",
"customer": "basalt",
"percent": 80.0,
"price": "28.00",
"sku": "sku-301"
},
"pending": {
"policy_id": "quote-approval",
"tool_server": "pricing",
"tool_name": "publish_price",
"parameter_hash": "cd764adc4b9f9973c7936e3ec6d1f41e923c9acf3b24be538c5eb7d94856e5ed",
"expires_at": 1791788027
}
}You should see status: pending, the price the host computed (USD 140.00 less 80 percent is USD 28.00), and the parameter_hash of the publish_price request it froze. No price has changed and no execution receipt exists yet. In the browser, the same step is the agent tab's Submit for approval.
3. Approve the exact change as Mara
Build the decision from the preview's hash. The decision names the request Mara read and carries no arguments of its own.
jq '{decision: "approve", parameter_hash: .pending.parameter_hash}' preview.json > decision.json
curl -sS "http://127.0.0.1:4310/proposals/$APPROVAL_ID/decision" \
-H "Authorization: Bearer $MARA_TOKEN" -H 'Content-Type: application/json' \
--data-binary @decision.json -o execution.json
cat "state/approvals/price-changes/$APPROVAL_ID.json"{
"change_id": "bedbf3c2-f378-4a46-8c96-ce5c03ca23d7",
"customer": "basalt",
"percent": 80.0,
"price": "28.00",
"sku": "sku-301"
}You should see the price change Mara reviewed, written once under its approval ID. The host signed the decision with Mara's key, consumed it atomically, and submitted the stored request to the kernel, which admitted publish_price under the agent's capability. In the browser, Load proposals and Approve & publish price in Mara's tab do the same.
The host's execution boundary
let outcome = chio_kernel::resume_with_decision(self.store.as_ref(), &decision, now())?;
// ... a rejection signs a deny receipt and returns here.
private_file(&proposals.join(format!("{id}.dispatch-started")), request.request_id.as_bytes())?;
let executed = self
.kernel
.evaluate_tool_call_with_metadata(
&request, // loaded from the proposal Mara saw
Some(json!({"approval_id": id, "approval_token_id": decision.token.id})),
)
.await?;Before this point the host checks the proposal's hash, subject, and expiry and signs the governed approval token. ApprovalGuard created the pending record when the agent proposed the change, and resume_with_decision resolves it exactly once.
4. Try the decisions the desk refuses
Propose a fresh change with the step 2 commands and rebuild decision.json from its preview. Then send three decisions that must not move a price: one made with the agent's credential, one for a different hash, and one that tries to swap in an 85 percent discount.
decide() {
curl -sS -w ' HTTP %{http_code}\n' "http://127.0.0.1:4310/proposals/$APPROVAL_ID/decision" \
-H "Authorization: Bearer $1" -H 'Content-Type: application/json' -d "$2"
}
HASH="$(jq -r .parameter_hash decision.json)"
decide "$AGENT_TOKEN" "{\"decision\":\"approve\",\"parameter_hash\":\"$HASH\"}"
decide "$MARA_TOKEN" '{"decision":"approve","parameter_hash":"changed"}'
decide "$MARA_TOKEN" "{\"decision\":\"approve\",\"parameter_hash\":\"$HASH\",\"arguments\":{\"percent\":85}}"{"error":"Credential is not valid for this role"} HTTP 401
{"error":"The proposed price change differs from the one reviewed. Reload and review the exact proposal"} HTTP 409
Failed to deserialize the JSON body into the target type: arguments: unknown field `arguments`, expected `decision` or `parameter_hash` at line 1 column 117 HTTP 422You should see 401, 409, and 422, and the proposal still pending with no file under price-changes/. Approve it properly, then send the same valid decision a second time:
decide "$MARA_TOKEN" "$(cat decision.json)"{"error":"Approval is unknown or already resolved; do not execute it again"} HTTP 409A consumed approval releases nothing further. Two more refusals end a review. A reject with the correct hash signs a deny receipt, “Mara Okafor rejected this price change”, and writes no file. To watch expiry, restart the desk with APPROVAL_TTL_SECONDS=10 (default 900, at most 3600): new proposals take the shorter window, a late decision is refused at the host, and publish_price never runs.
5. Reconcile an interrupted price change
A signed decision records what Mara authorized; a dispatch marker records that the host began execution. After a crash, list both under the original approval ID before anything else.
ls state/approvals/proposals/"$APPROVAL_ID".*
cat state/approvals/price-changes/"$APPROVAL_ID".jsonstate/approvals/proposals/bedbf3c2-f378-4a46-8c96-ce5c03ca23d7.decision.json
state/approvals/proposals/bedbf3c2-f378-4a46-8c96-ce5c03ca23d7.dispatch-started
state/approvals/proposals/bedbf3c2-f378-4a46-8c96-ce5c03ca23d7.execution.json
state/approvals/proposals/bedbf3c2-f378-4a46-8c96-ce5c03ca23d7.request.json
{
"change_id": "bedbf3c2-f378-4a46-8c96-ce5c03ca23d7",
"customer": "basalt",
"percent": 80.0,
"price": "28.00",
"sku": "sku-301"
}- Request only. The change is still pending, and expiry and current authority still apply. Review the original, or propose a different change as a new request.
- Execution record and price change. Match the file to the approved change and inspect its receipt.
- Decision or dispatch marker, no execution record. Look up the original request in the receipt store and check
price-changes/; keep the work unresolved until they agree. - Approved decision, kernel denial. The agent's grant was revoked or expired. A renewed grant never replays the consumed approval.
You should see all four records and the price change for a completed approval. The desk never replays an uncertain price change on its own. For the third case, ask the desk's receipt store whether the kernel signed a decision for this approval:
chio --receipt-db state/approvals/receipts.db receipt list --admin-all \
--tool-server pricing --tool-name publish_price --order newest \
| jq -c --arg id "$APPROVAL_ID" 'select(.metadata.approval_id == $id)
| {id, verdict: .decision.verdict, approval: .metadata.approval_id}'{"id":"d29f3068b76aca397a206361a06087c4e3386d9876d7c60d5e56585fe501674f","verdict":"allow","approval":"bedbf3c2-f378-4a46-8c96-ce5c03ca23d7"}You should see one allow for the completed approval. No line means the kernel never admitted the stored request, and the change stays unresolved until price-changes/ and the store agree.
6. Read the receipt behind the price change
The execution result from step 3 carries the signed receipt. Read the fields that tie it to this approval.
jq '{verdict, receipt_id, server: .receipt.tool_server, tool: .receipt.tool_name,
capability: .receipt.capability_id, approval: .receipt.metadata.approval_id}' execution.json{
"verdict": "Allow",
"receipt_id": "d29f3068b76aca397a206361a06087c4e3386d9876d7c60d5e56585fe501674f",
"server": "pricing",
"tool": "publish_price",
"capability": "cap-01a12ed5-ce98-7104-af8b-7149e337fc8e",
"approval": "bedbf3c2-f378-4a46-8c96-ce5c03ca23d7"
}You should see an allow for publish_price on the pricing server, under the agent's capability, with the approval ID in the signed metadata. Your IDs differ from these. The installed CLI explains the same record from the desk's store:
chio --receipt-db state/approvals/receipts.db receipt explain d29f3068b76aca397a206361a06087c4e3386d9876d7c60d5e56585fe501674f --admin-allreceipt: d29f3068b76aca397a206361a06087c4e3386d9876d7c60d5e56585fe501674f decision: allow tool: pricing/publish_price capability: cap-01a12ed5-ce98-7104-af8b-7149e337fc8e approval: bedbf3c2-f378-4a46-8c96-ce5c03ca23d7, signed by Mara Okafor's key parents: 0
You should see the same receipt, capability, and approval, with no parent receipts. The captured approval keeps the frozen proposal, every refusal above, the execution, and the file.
Complete source: src/approvals.rs, approvals.html, src/lib.rs, all in the project download.
Your login decides who may approve
The desk's loopback credentials stand in for authenticated roles. In your application, your login decides who signs as the approver, the approver's key stays in the trusted host, and the agent has no route to publish_price that skips the gate. Pending proposals and consumed decisions persist across restarts. When the approver should be you rather than a role in your app, the Quickstart's pricing.require rule and chio approve give the same binding with no host code.