Continuation tokens
Bind a delegated child to the plan, route, budget, and revocation view it runs under, and choose whether it runs once.
On this page
A delegated child runs on a continuation token: one signed object that names the plan it descends from, the route it may take, the budget slice it may spend, the revocation view it was minted under, and the parent or join that handed it authority.
The issuer signs those bindings together, so a presenter can neither retarget the token to another child nor change how often it may run. The examples follow continuation-child-a, which carries task-child-a forward from task-root in the recursive-delegation fixture.
A single-use token is spent before admission and stays spent once a tool may have run
For a single_use token, the runtime admission hook reserves the token id before the remaining admission checks run.
bbundleeevidencerrouteκauthorityccontinuationπadmissionr′revalidate
The child is admitted, then revalidated before dispatch
The runtime loads the stored bundle for the request’s artifact references and compares their ids and digests. It compares bridge, protocol target, and selected route with the stored route plan, verifies the bundle at the request’s evaluation time, and reserves the single-use continuation id. After the remaining admission checks it records the normalized route, then revalidates that record and the reloaded bundle before dispatch. One child task, admitted.
A refusal confirmed before dispatch releases the reservation, so the same token can be presented again. Once a tool may have run, the id stays consumed, and a retry cannot authorize a second effect. The states drawn here, unused, reserved, consumed, released, and unknown, explain one id’s path; they are not a stored enum.
Replay protection belongs to the admission store. The bundle verifier checks token ids and nonces within one bundle, and a resumable token skips the reservation while every other check still runs.
The issuer signs every binding, the mode included
The signature body is the canonical JSON of the whole token with signature removed. Editing any field invalidates the token.
{
"schema": "chio.swarm.continuation-token.v1",
"tokenId": "continuation-child-a",
"graphId": "swarm-graph-proof-valid",
"childTaskId": "task-child-a",
"parentTaskId": "task-root",
"parentReceiptIds": ["receipt-root"],
"graphSha256": "6233d0cf…5625",
"routePlanReceiptId": "route-child-a",
"budgetAllocationId": "budget-child-a",
"witnessChainRef": "witness-child-a",
"witnessChainSha256": "fd2971e2…fd46",
"revocationEpochRef": "revocation-epoch-swarm-valid",
"revocationEpochRootHash": "144bab85…b5b2",
"sessionAnchorRef": "session-anchor-swarm-valid",
"nonce": "nonce-task-child-a",
"mode": "single_use",
"issuedAtUnixMs": 1760000000000,
"expiresAtUnixMs": 1800000061000,
"issuer": "did:chio:43046bfe…a5de",
"signature": "2e812029…fd00"
}Every reference is checked against the bundle: the graph by id and recomputed digest, the route plan and allocation through the child’s graph node, the witness chain by digest, and the revocation epoch by id and list root. The token above comes from the fixture, with digests and the signature shortened.
Worked Rust example: Mint a continuation with all of its bindings
Mint a continuation with all of its bindings
Mint a signed single-use token with graph, route, allocation, witness, epoch and session references. Verify it twice offline, then advance the verifier's clock beyond its expiry.
Use the Rust example setup, or download the complete source project. The files below belong to that project; shared constructors and configuration are included in the download.
Run it
cargo run --locked --bin continuationImplementation
use chio_swarm_authority::verify_swarm_authority_bundle;
use swarm_authority_examples::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut bundle = build_bundle()?;
let token = &bundle.continuation_tokens[0];
println!(
"minted {} for {}: {:?}",
token.token_id, token.child_task_id, token.mode
);
verify_swarm_authority_bundle(&bundle, &trusted_witness_keys())?;
// Offline verification is repeatable: replay consumption belongs to the store.
verify_swarm_authority_bundle(&bundle, &trusted_witness_keys())?;
println!("offline verification: repeatable; run runtime-host for replay enforcement");
bundle.now_unix_ms = token.expires_at_unix_ms + 1;
println!(
"expired evidence: {}",
verify_swarm_authority_bundle(&bundle, &trusted_witness_keys())
.expect_err("expired bundle must refuse")
);
Ok(())
}pub fn continuation_token(
token_id: &str,
child_task_id: &str,
route_plan_receipt_id: &str,
graph_sha256: &str,
revocation_epoch_root_hash: &str,
witness_chain: Option<&SwarmDelegationWitnessChain>,
) -> Result<SwarmContinuationToken, Box<dyn Error>> {
let witness_chain_ref = witness_chain.map(|chain| chain.chain_id.clone());
let witness_chain_sha256 = witness_chain.map(canonical_hash).transpose()?;
let request = SwarmContinuationTokenMintRequest {
token_id: token_id.to_string(),
graph_id: "analysis-graph".to_string(),
child_task_id: child_task_id.to_string(),
parent_task_id: Some("task-root".to_string()),
join_receipt_id: None,
parent_receipt_ids: vec!["receipt-root".to_string()],
graph_sha256: graph_sha256.to_string(),
route_plan_receipt_id: route_plan_receipt_id.to_string(),
budget_allocation_id: format!("budget-{}", child_task_id.trim_start_matches("task-")),
witness_chain_ref,
witness_chain_sha256,
revocation_epoch_ref: "revocation-epoch-analysis".to_string(),
revocation_epoch_root_hash: revocation_epoch_root_hash.to_string(),
session_anchor_ref: "session-anchor-analysis".to_string(),
nonce: format!("nonce-{child_task_id}"),
mode: SwarmContinuationMode::SingleUse,
issued_at_unix_ms: now_ms() - 1_000,
expires_at_unix_ms: now_ms() + 60_000,
};
let token = mint_swarm_continuation_token(request, &witness_keypair())?;
Ok(token)
}Captured result and refusal
minted continuation-child-a for task-child-a: SingleUse
offline verification: repeatable; run runtime-host for replay enforcement
expired evidence: swarm task graph is expiredOffline verification is repeatable. Single-use consumption belongs to the configured runtime store; run the kernel example to see replay stopped before dispatch.
Exit 0 indicates a successful example run. Expected refusals shown in the output are checked by the program; invalid command inputs exit nonzero. Keys, digests, timestamps and run-directory names vary.
Read more in Revocation Epochs, which covers the list root every token commits to.
The issuer decides whether a token can be presented twice
The runtime reads mode once, when it verifies a swarm reference against the store, and only a single_use token yields an id to consume.
Ok(VerifiedSwarmAuthorityReference {
continuation_id_to_consume: match continuation.mode {
SwarmContinuationMode::SingleUse => Some(continuation.token_id.clone()),
SwarmContinuationMode::Resumable => None,
},
route_metadata,
})| Mode | Reservation | Second presentation | Receipt metadata |
|---|---|---|---|
single_use | On the token id | chio_swarm_continuation_replay while reserved or consumed | reserved_swarm_continuation_id |
resumable | None | Verified again in full | No reserved id |
A resumable token drops consume-once and keeps everything else on every presentation: expiry against the current request clock, the graph digest, the epoch root, the witness chain, the route plan, and the allocation state. Nothing counts its presentations, so mint single_use tokens and count them when a child hop must run a bounded number of times.
The hook reserves the id after every reference and the bundle check, before policy
The request names its evidence and never carries it. The hook reads governed_intent.context.chioSwarm and works in a fixed order.
- References. Seven id and digest pairs name the task graph, continuation token, route-plan receipt, delegation witness, join receipt, revocation epoch, and budget pool. A request that supplies its own trust keys is refused by name.
- Store. The hook loads the stored bundle by task-graph evidence id and overwrites its clock with the request’s, so a token that went stale after storage is stale.
- Digests and route. Each reference matches its stored artifact by id and canonical SHA-256, and the call’s route metadata matches the route plan.
- Bundle.
verify_swarm_authority_bundleruns, and the continuation’s route plan must equal the one the request named. - Consume. Only now is the single-use id reserved, before the admission evaluation that applies policy.
Presentations that share a coordinated store compete for the same marker before policy runs, and only one consume succeeds. When the admission evaluation refuses, the hook releases its reservations and strips them from the receipt metadata. Before the tool runs, the kernel calls the hook again: revalidation repeats the whole verification against the recorded route and compares the id it would consume with the recorded reservation, without consuming twice.
Read more in Route Plans, which covers the route comparison.
The prepared example releases an aborted admission and refuses its replay
The downloadable Rust project loads admission and swarm evidence into a store, pins witness keys, and evaluates a request four ways.
Worked Rust example: Release an aborted admission before dispatch
Release an aborted admission before dispatch
Load both admission and swarm evidence into the store, pin witness keys, and evaluate a request. An abort before the handler releases its reservation, allowing a later valid attempt.
Use the Rust example setup, or download the complete source project. The files below belong to that project; shared constructors and configuration are included in the download.
Run it
cargo run --locked --features runtime --bin admissionImplementation
use chio_core_types::{
capability::{
governance::GovernedTransactionIntent,
scope::{ChioScope, Operation, ToolGrant},
token::{CapabilityToken, CapabilityTokenBody},
},
crypto::Keypair,
receipt::lineage::SignedExportEnvelope,
};
use chio_kernel::{
RuntimeAdmissionContext, RuntimeAdmissionHook, RuntimeAdmissionRevalidationContext,
ToolCallRequest,
};
use chio_runtime_core::*;
use chio_swarm_authority::*;
use swarm_authority_examples::*;
type SignedPolicyInputs = (
SignedExportEnvelope<RuntimeVerifierTrustBundleV4>,
Vec<RuntimeTrustedVerifierKey>,
SignedRuntimePheromoneQueryReport,
SignedExportEnvelope<RuntimePheromonePolicy>,
SignedExportEnvelope<RuntimePeerWeights>,
);
fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = InMemoryRuntimeAdmissionStore::new();
let mut admission = bundle();
admission.schema = "chio.runtime.admission-bundle.v1".into();
admission.destructive = false;
admission.lease_id = None;
admission.governance_receipt_id = None;
let args = serde_json::json!({"record": "analysis-7", "value": "complete"});
admission.binding.tool_args_sha256 = tool_args_sha256(&args)?;
admission.binding.origin_kernel_id = None;
let admission_hash = runtime_admission_bundle_sha256(&admission)?;
store.insert_bundle(admission)?;
let swarm = build_bundle()?;
let route = &swarm.route_plan_receipts[0];
let route_metadata = serde_json::json!({"route": {
"bridge": route.bridge_id, "protocolTarget": route.protocol_target,
"selectedRoute": route.selected_route
}});
store.insert_swarm_authority_bundle(swarm.clone())?;
let request = chio_swarm_runtime_request(args, admission_hash, swarm_runtime_context(&swarm)?)?;
let hook = allowing_chio_policy_hook(store)?.with_swarm_witness_keys(trusted_witness_keys());
let context = RuntimeAdmissionContext {
request: &request,
extra_metadata: Some(&route_metadata),
now_unix_secs: now_ms() / 1000,
now_unix_ms: now_ms(),
matched_grant_index: Some(0),
local_kernel_id: "kernel.vendor-b".into(),
};
// A route mismatch must refuse without reaching the handler.
let wrong_route = serde_json::json!({"route": {"bridge":"mcp", "protocolTarget":"mcp://other", "selectedRoute":"other"}});
let denied = hook.evaluate(&RuntimeAdmissionContext {
extra_metadata: Some(&wrong_route),
request: &request,
now_unix_secs: now_ms() / 1000,
now_unix_ms: now_ms(),
matched_grant_index: Some(0),
local_kernel_id: "kernel.vendor-b".into(),
})?;
assert!(!denied.allowed);
println!(
"wrong route: {}",
denied.metadata.as_ref().unwrap()["chio_runtime"]["failure_code"]
);
// Reserve, then abort before any effect: release permits a later valid attempt.
let reserved = hook.evaluate(&context)?;
assert!(reserved.allowed, "{reserved:#?}");
hook.release_reserved(
reserved
.metadata
.as_ref()
.ok_or("missing reservation metadata")?,
)?;
println!("aborted before dispatch: reservation released");
let admitted = hook.evaluate(&context)?;
assert!(admitted.allowed, "{admitted:#?}");
hook.revalidate_before_dispatch(&RuntimeAdmissionRevalidationContext {
request: &request,
admission_metadata: admitted.metadata.as_ref(),
now_unix_secs: now_ms() / 1000,
now_unix_ms: now_ms(),
matched_grant_index: Some(0),
local_kernel_id: "kernel.vendor-b".into(),
})?;
// This local host's only effect sits after both checks. Do not release after
// this point if the outcome is uncertain: the operation may have happened.
let mut dispatch_count = 0;
std::fs::write(
"admitted-result.json",
serde_json::to_vec_pretty(&request.arguments)?,
)?;
dispatch_count += 1;
println!("handler dispatched: {dispatch_count}");
let replay = hook.evaluate(&context)?;
assert!(!replay.allowed);
assert_eq!(dispatch_count, 1);
println!(
"same continuation: {}",
replay.metadata.as_ref().unwrap()["chio_runtime"]["failure_code"]
);
Ok(())
}
fn profile() -> RuntimeAdmissionProfile {
RuntimeAdmissionProfile {
schema: CHIO_RUNTIME_ADMISSION_PROFILE_SCHEMA.to_string(),
profile_id: "profile-live-spine".to_string(),
local_kernel_id: "kernel.vendor-b".to_string(),
verifier_id: "did:chio:buyer-verifier".to_string(),
issued_at_unix_ms: (now_ms() - 1_000),
expires_at_unix_ms: (now_ms() + 3_600_000),
}
}
fn capability(capability_id: &str) -> Result<CapabilityToken, Box<dyn std::error::Error>> {
let issuer = Keypair::generate();
let subject = Keypair::generate();
Ok(CapabilityToken::sign(
CapabilityTokenBody {
id: capability_id.to_string(),
issuer: issuer.public_key(),
subject: subject.public_key(),
scope: ChioScope {
grants: vec![ToolGrant {
server_id: "analysis".to_string(),
tool_name: "record_result".to_string(),
operations: vec![Operation::Invoke],
constraints: Vec::new(),
max_invocations: None,
max_cost_per_invocation: None,
max_total_cost: None,
dpop_required: None,
}],
resource_grants: Vec::new(),
prompt_grants: Vec::new(),
},
issued_at: (now_ms() / 1000 - 1),
expires_at: (now_ms() / 1000 + 3600),
delegation_chain: Vec::new(),
aggregate_invocation_budget: None,
},
&issuer,
)?)
}
fn binding() -> RuntimeRequestBinding {
RuntimeRequestBinding {
request_id: "req-live-destructive".to_string(),
capability_id: "cap-live-1".to_string(),
server_id: "analysis".to_string(),
tool_name: "record_result".to_string(),
tool_args_sha256: "a".repeat(64),
origin_kernel_id: Some("kernel.buyer".to_string()),
host_kernel_id: "kernel.vendor-b".to_string(),
}
}
fn bundle() -> RuntimeAdmissionBundle {
RuntimeAdmissionBundle {
schema: CHIO_RUNTIME_ADMISSION_BUNDLE_SCHEMA.to_string(),
admission_id: "adm-live-1".to_string(),
binding: binding(),
workflow_id: "wf-live-1".to_string(),
workflow_grant_id: "grant-live-1".to_string(),
step_index: 1,
destructive: true,
lease_id: Some("lease-live-1".to_string()),
governance_receipt_id: Some("gov-live-1".to_string()),
trust_bundle_sha256: "b".repeat(64),
verification_context_sha256: "c".repeat(64),
}
}
fn trusted_keys(verifier: &Keypair) -> Vec<RuntimeTrustedVerifierKey> {
vec![RuntimeTrustedVerifierKey {
verifier_id: "did:chio:buyer-verifier".to_string(),
key_id: "verifier-key-1".to_string(),
public_key: verifier.public_key(),
valid_from_unix_ms: (now_ms() - 1_000),
valid_until_unix_ms: (now_ms() + 3_600_000),
status: "active".to_string(),
}]
}
fn trust_body(version: u64, previous_hash_sha256: Option<String>) -> RuntimeVerifierTrustBundleV4 {
RuntimeVerifierTrustBundleV4 {
schema: CHIO_RUNTIME_VERIFIER_TRUST_BUNDLE_SCHEMA.to_string(),
verifier_id: "did:chio:buyer-verifier".to_string(),
key_id: "verifier-key-1".to_string(),
version,
previous_hash_sha256,
trust_bundle_sha256: "b".repeat(64),
verification_context_sha256: "c".repeat(64),
revocation_checkpoint_sha256: "d".repeat(64),
revocation_authority_roots: vec!["did:chio:revocation-authority".to_string()],
issued_at_unix_ms: (now_ms() - 1_000),
expires_at_unix_ms: (now_ms() + 3_600_000),
}
}
fn advisory(strength: f64) -> RuntimePheromoneAdvisory {
RuntimePheromoneAdvisory {
source_report_sha256: "1".repeat(64),
accepted: true,
subject_class: "workflow.destructive_step".to_string(),
subject_class_namespace: "chio.runtime".to_string(),
total_strength: strength,
distinct_origin_pairs: 1,
reputation_epoch: 7,
evaluated_at_unix_ms: now_ms(),
observe_only: true,
}
}
fn policy(peer_weights_sha256: String) -> RuntimePheromonePolicy {
RuntimePheromonePolicy {
schema: CHIO_RUNTIME_PHEROMONE_POLICY_SCHEMA.to_string(),
policy_id: "policy-runtime-risk".to_string(),
verifier_id: "did:chio:buyer-verifier".to_string(),
key_id: "verifier-key-1".to_string(),
policy_version: 1,
mode: "enforce".to_string(),
issued_at_unix_ms: (now_ms() - 1_000),
expires_at_unix_ms: (now_ms() + 3_600_000),
allowed_reputation_epochs: vec![7],
max_query_report_age_ms: 60_000,
min_distinct_origin_pairs: 1,
runtime_trust_bundle_sha256: "b".repeat(64),
peer_weights_sha256,
rules: vec![RuntimePheromonePolicyRule {
rule_id: "deny-high-runtime-risk".to_string(),
subject_class: "workflow.destructive_step".to_string(),
subject_class_namespace: "chio.runtime".to_string(),
action_class_id: "*".to_string(),
direction: "deny_if_at_or_above".to_string(),
threshold_total_strength: 0.75,
effect: "deny".to_string(),
}],
}
}
fn peer_weights() -> RuntimePeerWeights {
RuntimePeerWeights {
schema: CHIO_RUNTIME_PEER_WEIGHTS_SCHEMA.to_string(),
verifier_id: "did:chio:buyer-verifier".to_string(),
key_id: "verifier-key-1".to_string(),
reputation_epoch: 7,
issued_at_unix_ms: (now_ms() - 1_000),
expires_at_unix_ms: (now_ms() + 3_600_000),
weights: vec![RuntimePeerWeight {
peer_kernel_id: "kernel.vendor-b".to_string(),
weight: 1.0,
}],
}
}
fn query_report_body(advisory: RuntimePheromoneAdvisory) -> serde_json::Value {
serde_json::json!({
"schema": "chio.pheromone.query-report.v1",
"accepted": advisory.accepted,
"concentration": {
"subjectClass": advisory.subject_class,
"subjectClassNamespace": advisory.subject_class_namespace,
"totalStrength": advisory.total_strength,
"distinctOriginPairs": advisory.distinct_origin_pairs,
"reputationEpoch": advisory.reputation_epoch,
"evaluatedAtUnixMs": advisory.evaluated_at_unix_ms
}
})
}
fn signed_query_report(
advisory: RuntimePheromoneAdvisory,
verifier: &Keypair,
) -> Result<SignedRuntimePheromoneQueryReport, Box<dyn std::error::Error>> {
Ok(SignedExportEnvelope::sign(
query_report_body(advisory),
verifier,
)?)
}
fn signed_policy_inputs(strength: f64) -> Result<SignedPolicyInputs, Box<dyn std::error::Error>> {
let verifier = Keypair::generate();
let signed_trust = SignedExportEnvelope::sign(trust_body(1, None), &verifier)?;
let weights = peer_weights();
let signed_policy =
SignedExportEnvelope::sign(policy(runtime_peer_weights_sha256(&weights)?), &verifier)?;
let signed_weights = SignedExportEnvelope::sign(weights, &verifier)?;
let signed_query_report = signed_query_report(advisory(strength), &verifier)?;
Ok((
signed_trust,
trusted_keys(&verifier),
signed_query_report,
signed_policy,
signed_weights,
))
}
fn allowing_chio_policy_hook<S>(
store: S,
) -> Result<ChioRuntimeAdmissionHook<S>, Box<dyn std::error::Error>> {
let mut profile = profile();
profile.schema = "chio.runtime.admission-profile.v1".to_string();
let (signed_trust, trusted, advisory, signed_policy, signed_weights) =
signed_policy_inputs(0.10)?;
Ok(ChioRuntimeAdmissionHook::new(profile, store)
.with_runtime_trust_input(signed_trust, trusted)
.with_pheromone_query_report(advisory)
.with_runtime_pheromone_policy(signed_policy, signed_weights))
}
fn chio_swarm_runtime_request(
args: serde_json::Value,
bundle_hash: String,
swarm_context: serde_json::Value,
) -> Result<ToolCallRequest, Box<dyn std::error::Error>> {
let cap = capability("cap-live-1")?;
Ok(ToolCallRequest {
request_id: "req-live-destructive".to_string(),
capability: cap.clone(),
tool_name: "record_result".to_string(),
server_id: "analysis".to_string(),
agent_id: cap.subject.to_hex(),
arguments: args,
dpop_proof: None,
execution_nonce: None,
governed_intent: Some(GovernedTransactionIntent {
id: "intent-live-1".to_string(),
server_id: "analysis".to_string(),
tool_name: "record_result".to_string(),
purpose: "record an analysis result".to_string(),
max_amount: None,
commerce: None,
metered_billing: None,
runtime_attestation: None,
call_chain: None,
autonomy: None,
context: Some(serde_json::json!({
"chioAdmission": {
"admissionId": "adm-live-1",
"bundleSha256": bundle_hash
},
"chioSwarm": swarm_context
})),
body: Default::default(),
}),
approval_token: None,
approval_tokens: Vec::new(),
threshold_approval_proposal: None,
supplemental_authorization: None,
model_metadata: None,
federated_origin_kernel_id: None,
})
}
fn swarm_runtime_context(
bundle: &SwarmAuthorityBundle,
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
Ok(serde_json::json!({
"taskGraph": {
"id": &bundle.task_graph.graph_id,
"sha256": canonical_hash(&bundle.task_graph)?
},
"continuationToken": {
"id": &bundle.continuation_tokens[0].token_id,
"sha256": canonical_hash(&bundle.continuation_tokens[0])?
},
"routePlanReceipt": {
"id": &bundle.route_plan_receipts[0].route_plan_id,
"sha256": canonical_hash(&bundle.route_plan_receipts[0])?
},
"delegationWitness": {
"id": &bundle.witness_chains[0].chain_id,
"sha256": canonical_hash(&bundle.witness_chains[0])?
},
"joinReceipt": {
"id": &bundle.join_receipts[0].join_id,
"sha256": canonical_hash(&bundle.join_receipts[0])?
},
"revocationEpoch": {
"id": &bundle.revocation_epoch.epoch_id,
"sha256": canonical_hash(&bundle.revocation_epoch)?
},
"budgetPool": {
"id": &bundle.budget_pool.pool_id,
"sha256": canonical_hash(&bundle.budget_pool)?
}
}))
}Captured result and refusal
wrong route: "chio_swarm_authority_rejected"
aborted before dispatch: reservation released
handler dispatched: 1
same continuation: "chio_swarm_continuation_replay"This isolates the admission-hook lifecycle. After an effect may have happened, retain consumption when the outcome is uncertain; retrying under released authority could repeat the effect.
Exit 0 indicates a successful example run. Expected refusals shown in the output are checked by the program; invalid command inputs exit nonzero. Keys, digests, timestamps and run-directory names vary.
You should see a wrong route refused with chio_swarm_authority_rejected, an abort before the handler release its reservation, one dispatch of the handler, and the same continuation then refused with chio_swarm_continuation_replay.
Stale and duplicated tokens stop offline; replays stop at the store
Two of the 11 shipped negative bundles are continuation cases: a token past its expiry, and two tokens in one bundle carrying the same nonce. The verifier refuses both from the bundle alone.
$ chio proof verify \
./swarm/negatives/stale-continuation \
./swarm/negatives/replayed-continuation-nonce \
--trust ./swarm/trust.toml --expect refuseREFUSED stale-continuation swarm continuation token is stale: continuation-child-a REFUSED replayed-continuation-nonce swarm continuation nonce replay: continuation-child-b 2 of 2 refused, as expected trust ./swarm/trust.toml (fixture keys)
A replay is a different refusal. chio_swarm_continuation_replay is the runtime refusing a second presentation of an id its store already holds, which takes two requests. The nonce case is one bundle, two tokens, one nonce, refused before any request runs.
Replay protection reaches as far as the store that records the id
A single-use id is protected within the admission store that holds it. SQLite coordinates every consumer of one database; the in-memory store coordinates callers that share its mutex, and the JSON store callers that share one opened instance.
When a consume returns an error or panics, the hook refuses, releases the treaty continuation it already took, and leaves the swarm id in place, because releasing an id of unknown state could erase a marker the store did write. After dispatch the kernel copies reserved ids into the receipt as retained. A consumed token is never restored automatically: establish whether the original action took effect before issuing replacement authority, since new authority cannot reverse an effect.
Read more in Unknown outcomes, which covers work whose effect is in doubt.
Token fields, stores, and runtime records
Try nonce, SQLite, or reservation_ownership_ambiguous.
graphId, graphSha256
The signed plan the child descends from. The id equals the bundle graph id, and the digest is a canonical SHA-256 recomputed over the bundle’s task graph, signature field included.
childTaskId
The one node the token authorizes; it must resolve in the graph node index. A node that declares a parentTaskId must carry a continuationTokenRef, and a node that carries one must be that token’s childTaskId. Additional valid tokens may name the same child.
parentTaskId or joinReceiptId
Where the authority came from. On the parent branch it must equal the child node’s own parentTaskId and follow a declared graph edge. On the join branch it names a join receipt whose nextTaskId is this child.
parentReceiptIds
The work that already happened. Entries are unique and individually non-empty. The list is non-empty on the parent branch and, on the join branch, equals the join’s actualParentReceiptIds as sorted lists.
witnessChainRef, witnessChainSha256
The proof that scope narrowed on this hop: the canonical digest of the named chain, whose parent and child task ids must match the token. Required on the parent branch and forbidden on the join branch.
routePlanReceiptId
The egress the child may take. It equals the child node’s routePlanRef and names a receipt whose taskId is this child.
budgetAllocationId
The units the child may spend. It equals the child node’s allocation ref, and the allocation’s state must be Active.
revocationEpochRef, revocationEpochRootHash
The revocation view the token was minted under: the bundle epoch’s id and its root hash, which the verifier recomputes from the epoch’s sorted revoked lists. Revoking one task changes the root, so every token minted against the old epoch fails the id or root comparison.
nonce
Freshness within the bundle, checked by a BTreeSet insert across every token in the bundle. Nonce and token-id uniqueness hold within one bundle. A second bundle that reuses a nonce under a new token id passes this check; the store catches a reused token id.
sessionAnchorRef
The session the token was minted in, by reference. It must be non-empty and is covered by the signature.
issuedAtUnixMs, expiresAtUnixMs
A validity window checked against the bundle’s now_unix_ms, which the runtime overwrites with the request clock. A token issued after that instant fails with swarm continuation token is from the future: {tokenId}; one expiring at or before it fails with swarm continuation token is stale: {tokenId}.
issuer, signature
A pinned witness key. The issuer parses to a public key that must be a member of the caller-supplied trusted slice, which then verifies the signature. Membership is the gate, and an empty slice refuses every token.
Schema and verifier
The JSON Schema sets additionalProperties: false, mirrored by deny_unknown_fields in Rust, and states one cross-field rule:
"allOf": [
{
"if": {
"required": ["parentTaskId"]
},
"then": {
"required": ["witnessChainRef", "witnessChainSha256"]
}
}
],A token naming both parentTaskId and joinReceiptId, or neither, is schema-valid; validate_continuation_parent refuses it with swarm continuation must choose parent task or join receipt or swarm continuation missing parent context. Validate against the schema to refuse malformed input early, and read authorization only from the verifier.
The schema pins signature to ^[0-9a-f]{128}$, the bare Ed25519 encoding. The Rust path parses through Signature::from_hex and PublicKey::from_hex, which also accept p256:, p384:, and hybrid: prefixes, so schema validation narrows what a token may carry. The issuer-string rules are shared across the bundle and covered on Delegation Witnesses.
Evidence references
swarm_ref_from_request requires seven id and digest pairs. A missing pair is missing_chio_swarm_evidence_ref. A present but malformed pair, meaning a non-object ref, a blank id, or a digest that is not 64 hex characters, is invalid_chio_swarm_evidence_ref, and a chioSwarm value that is not an object is invalid_chio_swarm_context.
Six keys refuse with request_smuggled_trust_root on their presence alone: trustRoot, trustRoots, trustBundle, authorityBundle, signingKey, witnessKeys. Three refuse with request_smuggled_dynamic_trust: dynamicTrust, dynamicTrustBundle, peerDiscovery. The list is fixed and screens these named keys.
Store lookup and route-plan match
After the digests and route match, verify_swarm_authority_bundle runs, and the named continuation’s routePlanReceiptId must equal the route-plan evidence id in the request, or the request refuses with chio_swarm_authority_ref_mismatch. A deployment with a stored bundle and no pinned witness key stops at the verifier with chio_swarm_authority_rejected. One with pinned keys and no stored bundle stops three steps before that, at the lookup:
let Some(mut bundle) = store.swarm_authority_bundle(&reference.task_graph.evidence_id)? else {
return rejected(
"missing_chio_swarm_authority_bundle",
"swarm-bound request referenced authority evidence that is not in the verifier-owned store",
);
};Neither touches the id. The first refusal means the keys are not pinned; the second means the named evidence is missing from the verifier-owned store. Both, with the other admission codes, are on Swarm Denial Codes, and the unpinned-key bound is on Swarm Authority.
Consume and release in the hook
The consume sits in ChioRuntimeAdmissionHook::evaluate, inside a std::panic::catch_unwind whose four arms are a clean success, a store rejection, a store error, and a panic. The three failing arms each release the treaty continuation and return a refusal. Past that match the hook builds the reservation tracker and calls evaluate_runtime_admission_tracked, wrapped the same way, whose error and panic arms release what the consume took.
Every Chio release passes the adapter-no-bypass gate before it is tagged. The gate requires at least one self.store.consume_swarm_continuation inside the hook’s evaluate, beside the route-metadata comparison and the verifier call, and requires swarm_ref_from_request to call required_swarm_evidence_ref at least seven times, so a build that deletes the consume or a required reference cannot ship. chio release verify --evidence qualification shows its result for the release you run; Qualification runs it from source.
Dispatch revalidation
When the admission evaluation accepts, the id stays consumed and the metadata carries reserved_swarm_continuation_id and the verified route metadata. The hook returns true from requires_dispatch_revalidation, and the kernel re-runs the whole verification before the tool. That pass reads verified_swarm_route_metadata from the admission receipt, so it re-proves the bundle against the request clock and the route against the recorded projection.
A changed reservation is a KernelError, with the message runtime admission reservation reserved_swarm_continuation_id changed before dispatch, and the dispatch fails closed.
Admission stores
| Store | Consume | Second consume |
|---|---|---|
Trait default | None | chio_ on the first call |
In | Insert into a mutex-guarded set | chio_ |
Json | Scan consumed_, then push, validate, persist | chio_ |
Sqlite | INSERT OR IGNORE INTO runtime_ | Zero rows, so chio_ |
Layered | Delegates to its admission store | That store’s answer |
The trait default refuses, so a store that cannot track replay refuses swarm-bound requests. Release is the inverse: remove from the set, retain the vector without the id, or DELETE ... WHERE continuation_id = ?1 AND admission_id = ?2. SQLite records which admission consumed an id and scopes the delete by it, so a release there clears only a marker the same admission wrote. The in-memory and JSON stores accept admission_id and ignore it on consume and release, so any release clears the id; treat that as a development-store property.
Separately opened JSON stores keep independent snapshots and share no file lock. SQLite’s unique insertion coordinates every consumer of the same database. The consumed-continuation table lives in the node’s SQLite state, described on Node State on Disk.
Ambiguous consume
When consume_swarm_continuation fails with a store error or panics, the refusal metadata carries failure_code: swarm_continuation_consume_error, ambiguous_swarm_continuation_id, reservation_ownership_ambiguous: true, and reservation_consumption_failure_reason. One helper writes the shape for both arms, with the reservation key as its argument, so treaty and swarm cases match under different names:
fn ambiguous_consumption_metadata(
mut metadata: serde_json::Value,
reservation_key: &str,
reservation_id: &str,
failure_reason: &str,
) -> serde_json::Value {
let runtime = metadata
.as_object_mut()
.and_then(|metadata| metadata.get_mut("chio_runtime"))
.and_then(serde_json::Value::as_object_mut);
if let Some(runtime) = runtime {
runtime.insert(
reservation_key.to_string(),
serde_json::Value::String(reservation_id.to_string()),
);
runtime.insert(
"reservation_ownership_ambiguous".to_string(),
serde_json::Value::Bool(true),
);
runtime.insert(
"reservation_consumption_failure_reason".to_string(),
serde_json::Value::String(failure_reason.to_string()),
);
}
metadata
}swarm_consume_error_releases_treaty_and_preserves_same_admission_swarm_marker and its panic twin assert one treaty release, zero swarm releases, and that a later consume of the same id returns chio_swarm_continuation_replay. The id may remain consumed, and a store error can also occur before insertion; the runtime records the ambiguity in place of a release that could erase another admission’s marker.
Retained reservations
The kernel releases reservations on a pre-dispatch refusal and on a dropped request. Once a side effect could have happened, it copies the reserved ids into the receipt under retained names and marks them:
pub(crate) fn mark_runtime_admission_reservations_retained_fail_closed(
&self,
metadata: Option<serde_json::Value>,
) -> Option<serde_json::Value> {
let mut retained = serde_json::Map::new();
{
let Some(runtime) = metadata
.as_ref()
.and_then(|value| value.get("chio_runtime"))
.and_then(serde_json::Value::as_object)
else {
return metadata;
};
// Copy across only the ids that name a REAL reservation: a present,
// non-empty reserved lease/continuation id. A `chio_runtime` route
// block that merely carries the key with no (or an empty) value had
// nothing to burn.
for (source, target) in [
(
"reserved_destructive_lease_id",
"retained_destructive_lease_id",
),
(
"reserved_treaty_continuation_id",
"retained_treaty_continuation_id",
),
(
"reserved_swarm_continuation_id",
"retained_swarm_continuation_id",
),
] {
if let Some(id) = runtime
.get(source)
.and_then(serde_json::Value::as_str)
.filter(|id| !id.is_empty())
{
retained.insert(target.to_string(), serde_json::json!(id));
}
}
// Only mark retained when at least one real reservation was actually
// retained. An observe-only admission or a metadata-only
// `chio_runtime` route block has no `reserved_*` id to recover, so
// it must not carry the fail-closed marker.
if retained.is_empty() {
return metadata;
}
retained.insert(
"reservations_retained_fail_closed".to_string(),
serde_json::Value::Bool(true),
);
}
merge_metadata_objects(
metadata,
Some(serde_json::json!({ "chio_runtime": retained })),
)
}Metadata with no chio_runtime block, or with no present and non-empty reserved id, is returned unchanged, so the marker appears only where a continuation was burned.
Release failures
A failed release records reservation_release_failed: true, the failure reason, and the reserved ids, and the marker may still be held. Retry suppression applies to the evaluator’s failed destructive-lease release path; the generic reservation-release callback ignores the flag, so inspect a failed release before retrying.
Test and fixture evidence
ChioRuntimeAdmissionHook::evaluateandrelease_reservationsinadmission_hook.rsconsume a single-use id before the admission evaluation and release it when that evaluation refuses.chio_runtime_hook_revalidates_reserved_swarm_continuation_then_denies_replayandsqlite_runtime_hook_denies_replayed_swarm_continuation_before_dispatchrefuse a replayed single-use token in memory and on disk while revalidation of the first admission passes.chio_runtime_hook_revalidates_resumable_swarm_continuation_without_replay_denialadmits the same resumable request twice and records no reserved id;continuation_id_to_consumeisNonefor that mode.chio_runtime_hook_denies_swarm_continuation_expired_since_storagerefuses a token that expired between storage and dispatch withchio_swarm_authority_rejected.- The
replayed-continuation-noncenegative backsclaim.swarm.continuation_freshwith failure codeproof-room.negative.swarm-continuation-nonce-replay; the doctor specswarm_replayed_continuation_nonceexpectsswarm continuation nonce replay. The nonce check runs after the signature and graph-digest checks, so the fixture is well signed and only the nonce repeats. mint_swarm_continuation_tokenandsign_swarm_continuation_tokenare the public construction API, exercised inchio-swarm-authority/tests/swarm_authority_stage0.rsandchio-runtime-core/tests/runtime_admission.rs.
Trusted witness keys
The operator supplies the trusted witness key slice through ChioRuntimeAdmissionHook::with_swarm_witness_keys. It starts empty, and require_trusted_witness_issuer_keys in verifier.rs refuses every swarm-bound request until it is set.
Wire contract
The token follows spec/schemas/chio-swarm/v1/continuation-token.schema.json and spec/PROTOCOL.md section 6.4.2 in the public Chio repository. The Swarm Protocol reference carries every artifact and the verification order, and 3-Vendor Walkthrough carries one delegated scenario end to end.