Chio/Docs
LOGIN · JOIN

Threshold approvals

Require independent authority for a consequential operation, then preserve exactly what was approved through collection and execution.

Guide chio@5f63e6e

On this page

Hold a consequential operation until two of three independent approvers sign it, then run exactly the request they approved.

An 80 percent price cut on sku-301, a refund above the usual ceiling, or a key rotation can depend on human judgment about context. A threshold policy keeps the original operation waiting while eligible principals sign one bound proposal, and the kernel resumes that request only when its current checks permit. Independence comes from keys: two names backed by one key supply one approval, and a vote for another subject, intent, or proposal counts toward nothing here.

Start
Approval quorumChoose a moment.
proposalvotesame keycollector2 of 2 · three eligiblereadyexact setdispatchrKτ1τ2τ3dρrequestreceipttoolreviewerrelease operatordata stewardproposalsame keycollector2 of 2 · three eligiblereadydispatchrKτ1τ2τ3dρrequestreceipttoolreviewerreleasesteward

Two distinct eligible keys meet the threshold

The release operator's vote makes the proposal ready. Delivery persists its transition before returning the original proposal and the exact token set, and repeating delivery returns that same set. No tool has run.

A two-of-three approval for one retained publication request: one signed proposal, three policy-pinned keys, and the durable collector's tally. Vote expiry, directory changes, and the signature algorithm floor are left out.

1. Pin the policy and the eligible keys

The example uses three keys: reviewer, the reviewer agent; release, your release operator; and steward, held by your pricing lead as the pricing data steward. In the run below the reviewer and the steward approve. The host requires two of three for publication/append under one policy hash and a 900-second collection window. These are host configuration values, so the threshold is fixed outside the caller's request.

security-execution/src/approvals.rsrust
let requirement = ThresholdApprovalRequirement::new(
    policy(),
    2,
    eligible,
    "publication-directory-v1".into(),
    900,
)
.map_err(anyhow::Error::msg)?;
kernel.set_threshold_approval_requirement_resolver(Arc::new(
    move |hash: &str,
          server: &str,
          tool: &str|
          -> Result<Option<ThresholdApprovalRequirement>, String> {
        Ok(
            (hash == requirement.policy_hash && server == "publication" && tool == "append")
                .then(|| requirement.clone()),
        )
    },
));

The constructor canonicalizes the eligible set and rejects duplicate identifiers, duplicate keys, and invalid thresholds or time bounds. Install the resolver before startup reconciliation, so a reopened host reads its original pending admissions under the current policy.

In Rust: the requirement and route types
crates/core/chio-core-types/src/capability/threshold_approval.rs77–84rust
pub struct ThresholdApprovalRequirement {
    pub policy_hash: String,
    pub threshold: u32,
    pub eligible_approvers: Vec<ThresholdApproverIdentity>,
    pub eligible_set_digest: String,
    pub directory_version: String,
    pub timeout_seconds: u64,
}
crates/core/chio-core-types/src/capability/threshold_approval.rs19–23rust
pub struct ThresholdApprovalRequest {
    request_id: String,
    server_id: String,
    tool_name: String,
}

2. Prepare an operation that waits for approval

Finalize the tool, arguments, subject, and governing capability before asking. The approvals program records the reviewed price change for sku-301, with a spending authorization of 100 USD minor units, in a local publication journal; the adapter records the authorization and note, settles no payment, and reports no tool charge.

Run the steps from the execution lab directory with jq installed, and finish collection within 15 minutes. Each command starts a new process and reopens the original SQLite stores. The disposable exercise keeps all three keys under one operator account; deployed approvers each hold their own key and authenticate separately.

Terminalbash
approvals init .approvals
printf 'Reviewed: sku-301 at 80 percent for the spring clearance. Publish the price change.\n' > price-change.txt
approvals prepare .approvals price-change.txt > proposal.json
jq '{request_id, threshold, proposal_created_at, proposal_deadline}' proposal.json
test ! -e .approvals/publications.jsonl

You should see request review-publication with threshold 2 and a deadline 900 seconds after creation, and the final test succeed because no journal entry exists. Preparation invokes the kernel and requires a pending-approval verdict. It retains the original request, a signed proposal whose commitments name the request, intent, authorizing capability, policy, eligible set, and deadline, and a pending receipt; the one-invocation capability and the cumulative approval requirement stay attached while it waits.

Show approvers a readable rendering of that same request, with the signed proposal kept beside it. A title such as "approve price change" cannot distinguish an 80 percent cut on sku-301 from a changed SKU or percentage, and approve or deny booleans detached from the proposal carry no authority.

3. Reopen the collector against the original admission

The program opens SqliteApprovalStore and builds the collector with the kernel-owned constructor. Persistence keeps votes across restarts, and authorization still comes from the kernel's original admission.

security-execution/src/approvals.rsrust
let collector = kernel.create_threshold_approval_collector(
    store,
    ThresholdApprovalCollectionPolicy::new(policy(), true)?,
)?;
collector.create_proposal(proposal.clone(), now()?)?;

The kernel resolves the proposal against its durable cumulative admission, revalidating the original subject, capability ancestry, current authority, route, intent, and threshold requirement, and the policy requires separation from the submitter. An arbitrary request body or a correctly signed proposal alone cannot supply that context. Keep request identity unambiguous across subjects and admission kinds: if several retained operations matched one request ID, picking the newest would pick a different authorization target, so a host that cannot resolve the eligible original operation refuses collection.

4. Sign one vote and watch a duplicate refuse

Each approver signs the original subject, intent hash, request ID, and proposal digest, with its own decision and a validity window that ends at the proposal deadline. A retried token renews neither the time nor the deadline.

security-execution/src/approvals.rsrust
let signer = key(&state.join(format!("{who}.key")))?;
let token = GovernedApprovalToken::sign(
    GovernedApprovalTokenBody {
        id,
        approver: signer.public_key(),
        subject: proposal.body.subject.clone(),
        governed_intent_hash: proposal.body.governed_intent_hash.clone(),
        request_id: proposal.body.request_id.clone(),
        threshold_proposal_hash: Some(proposal.artifact_digest()?),
        issued_at: now()?,
        expires_at: proposal.body.proposal_deadline,
        decision: GovernedApprovalDecision::Approved,
    },
    &signer,
)?;
Terminalbash
approvals vote .approvals reviewer > first-vote.json
jq '{state, votes: (.tokens | length)}' first-vote.json
approvals vote .approvals reviewer > repeated-vote.json
cmp first-vote.json repeated-vote.json

# Both commands must refuse and exit nonzero.
if approvals vote .approvals reviewer reviewer-second-token; then
  echo 'Unexpected duplicate-signer acceptance' >&2; exit 1
fi
if approvals deliver .approvals; then
  echo 'Unexpected delivery below threshold' >&2; exit 1
fi
test ! -e .approvals/publications.jsonl

You should see collecting with one token, and an identical acknowledgement after the process restart. A fresh token ID from the same key refuses, as does delivery with one vote, and neither refusal touches the pending proposal or its valid first vote. Retain each token before submitting it: if the acknowledgement is lost, resubmit those exact bytes, because a new token ID from the same signer is another decision from the same principal.

5. Collect the second vote and deliver the exact set

Terminalbash
approvals vote .approvals steward > ready.json
jq '{state, votes: (.tokens | length)}' ready.json
approvals deliver .approvals > delivered.json
approvals deliver .approvals > delivered-again.json
cmp delivered.json delivered-again.json
test ! -e .approvals/publications.jsonl

You should see ready with two votes and two identical delivery records. Delivery persists its transition before returning the original proposal and token set, and repeating it while the original admission stays eligible returns the same set. The journal is still absent: collection has authorized a continuation that the kernel has yet to execute.

StateWhat existsWho advances it
Pending admissionOriginal request, reservation, signed proposalKernel admission
CollectingDistinct eligible signed decisionsCollector under its current resolver
Ready, deliveredThreshold met; exact delivered set retainedKernel continuation using that set
CompletedOriginal result and signed completion receiptRetained-operation replay

6. Return to admission and verify the receipt

The application reloads the original request and attaches the delivered proposal and votes, never rebuilding a call from display text or turning approval into a reusable grant.

security-execution/src/approvals.rsrust
let set: CollectedThresholdApprovalSet = read(&state.join("delivered.json"))?;
let mut request: ToolCallRequest = read(&state.join("request.json"))?;
request.threshold_approval_proposal = Some(set.proposal);
request.approval_tokens = set.tokens;
let response = kernel.evaluate_tool_call_blocking(&request)?;
Terminalbash
approvals execute .approvals > result.json
result.json
{
  "authorized_amount": {
    "currency": "USD",
    "units": 100
  },
  "note": "Reviewed: sku-301 at 80 percent for the spring clearance. Publish the price change.\n",
  "sha256": "d5bd90015e06c56d5f26a41802909c1bc871da41ea58df17303b3a8a86aeee9c"
}

You should see the authorized amount of 100 USD minor units, the reviewed price change, and its SHA-256 digest. The kernel still evaluated current authority, effective scope, budgets, and flow checks, so a withdrawal or an expired approval would have stopped an operation with enough votes while leaving the pending operation identifiable. Save the receipt and request the same completion again:

Terminalbash
cp .approvals/completed.receipt.json original.receipt.json
approvals execute .approvals > repeated-result.json
cmp result.json repeated-result.json
cmp original.receipt.json .approvals/completed.receipt.json
test "$(wc -l < .approvals/publications.jsonl | tr -d ' ')" = 1
chio receipt verify --input original.receipt.json \
  --trusted-kernel-pubkey .approvals/kernel.pub

You should see identical results, a byte-identical completion receipt, one journal entry, and the receipt's signature verified against the pinned kernel key. This is a retry after completed execution; an interruption inside the write is an unknown outcome. Keys, signatures, and proposal IDs differ on each fresh run.

An interrupted approval resumes from the state that exists

  • Vote acknowledgement lost. Resubmit the exact original signed token, keeping its identity and expiry.
  • Delivery reply lost. Deliver again under the current original-operation context; the delivered membership stays the same.
  • Execution reply lost. Read the original operation's retained outcome, with its request binding and completion receipt.
  • A delivered token expires. Follow the expiry and cancellation lifecycle and keep the expired set; its members are never silently replaced.
  • Policy, directory, or payload changes. Validate under current policy and admit the changed work as distinct from the action already approved.

Before delivery, a new unique token can replace an expired vote from the same eligible signer; a terminal delivery is never rewritten. After execution completes, recover through the operation's completion state, which the collector's resolver need no longer accept as a pending request.

The proposal and each vote are separate signed envelopes, and the verifier's trusted policy selects their allowed algorithms: a hybrid capability leaves classical approval signatures unacceptable when the floor requires hybrid signatures, as Post-quantum signatures explains. On restart, restore the still-trusted authority the retained proposal needs; a newly installed signer rewrites no old proposal, and the example refuses missing original keys and stores instead of provisioning replacements.

The run exercises kernel admission, durable collection, duplicate-signer refusal, insufficient votes, delivery replay, and completed replay. A deployed approval service adds tests for changed payloads, directory changes, expired votes, revocation before dispatch, and authenticated approver routing, and inspects the protected resource after each refused continuation, because an HTTP acknowledgement alone cannot show that an effect was withheld.

Next: apply approvals to restricted data