Chio/Docs
LOGIN · JOIN

Continuation tokens

Bind a delegated child to the plan, route, budget, and revocation view it runs under, and choose whether it runs once.

Concept chio@5f63e6e

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.

Child admissionChoose an outcome.
dispatchbbundleeevidencerrouteκauthorityccontinuationπadmissionr′revalidateroute metadatarecorded routerKdrequesttask-child-atool or bridgecontinuation idreleasedunusedreservedconsumedunknown: no releaseberκcπr′rKdcontinuation idreleasedunusedreservedconsumedunknown

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.

One request for task-child-a in the order the runtime admission hook checks it, with route-child-a as its route plan and revalidation immediately before dispatch. Capability, policy, and budget checks are folded into admission; resumable tokens, which skip the reservation, are left out.

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.

continuation-child-a.jsonjson
{
  "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

bash
cargo run --locked --bin continuation

Implementation

continuation.rsrust
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(())
}

Open src/bin/continuation.rs

lib.rs · continuation_tokenrust
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)
}

Open src/lib.rs

Captured result and refusal

Captured executable outputtext
minted continuation-child-a for task-child-a: SingleUse
offline verification: repeatable; run runtime-host for replay enforcement
expired evidence: swarm task graph is expired

Offline 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.

crates/kernel/chio-runtime-core/src/admission_hook/swarm_authority.rs44-50rust
Ok(VerifiedSwarmAuthorityReference {
    continuation_id_to_consume: match continuation.mode {
        SwarmContinuationMode::SingleUse => Some(continuation.token_id.clone()),
        SwarmContinuationMode::Resumable => None,
    },
    route_metadata,
})
ModeReservationSecond presentationReceipt metadata
single_useOn the token idchio_swarm_continuation_replay while reserved or consumedreserved_swarm_continuation_id
resumableNoneVerified again in fullNo 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.

  1. 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.
  2. 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.
  3. 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.
  4. Bundle. verify_swarm_authority_bundle runs, and the continuation’s route plan must equal the one the request named.
  5. 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

bash
cargo run --locked --features runtime --bin admission

Implementation

admission.rsrust
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)?
        }
    }))
}

Open src/bin/admission.rs

Captured result and refusal

Captured executable outputtext
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.

swarm-authority · continuation-negativestranscript
$ chio proof verify \
  ./swarm/negatives/stale-continuation \
  ./swarm/negatives/replayed-continuation-nonce \
  --trust ./swarm/trust.toml --expect refuse
REFUSED   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)
exit 0

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

graphId, graphSha256

StringRequired

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

StringRequired

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

StringExactly one

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

String[]Required

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

StringParent branch only

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

StringRequired

The egress the child may take. It equals the child node’s routePlanRef and names a receipt whose taskId is this child.

budgetAllocationId

StringRequired

The units the child may spend. It equals the child node’s allocation ref, and the allocation’s state must be Active.

revocationEpochRef, revocationEpochRootHash

StringRequired

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

StringRequired

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

StringRequired

The session the token was minted in, by reference. It must be non-empty and is covered by the signature.

issuedAtUnixMs, expiresAtUnixMs

u64Required

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

PublicKeyRequired

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:

spec/schemas/chio-swarm/v1/continuation-token.schema.json7-16json
"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:

crates/kernel/chio-runtime-core/src/admission_hook/swarm_authority.rs21-26rust
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

StoreConsumeSecond consume
Trait default
None
chio_swarm_continuation_store_unsupported on the first call
InMemoryRuntimeAdmissionStore
Insert into a mutex-guarded set
chio_swarm_continuation_replay
JsonRuntimeAdmissionStore
Scan consumed_swarm_continuation_ids, then push, validate, persist
chio_swarm_continuation_replay
SqliteRuntimeOrchestrationStore
INSERT OR IGNORE INTO runtime_consumed_swarm_continuations
Zero rows, so chio_swarm_continuation_replay
LayeredRuntimeAdmissionStore
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:

crates/kernel/chio-runtime-core/src/admission_hook.rs106-131rust
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:

crates/kernel/chio-kernel/src/kernel/dispatch.rs1587-1642rust
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::evaluate and release_reservations in admission_hook.rs consume a single-use id before the admission evaluation and release it when that evaluation refuses.
  • chio_runtime_hook_revalidates_reserved_swarm_continuation_then_denies_replay and sqlite_runtime_hook_denies_replayed_swarm_continuation_before_dispatch refuse 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_denial admits the same resumable request twice and records no reserved id; continuation_id_to_consume is None for that mode.
  • chio_runtime_hook_denies_swarm_continuation_expired_since_storage refuses a token that expired between storage and dispatch with chio_swarm_authority_rejected.
  • The replayed-continuation-nonce negative backs claim.swarm.continuation_fresh with failure code proof-room.negative.swarm-continuation-nonce-replay; the doctor spec swarm_replayed_continuation_nonce expects swarm 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_token and sign_swarm_continuation_token are the public construction API, exercised in chio-swarm-authority/tests/swarm_authority_stage0.rs and chio-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.

Next: see the bundle the token is checked inside