Swarm authority
Every child task's authority traces back to a signed plan, and the kernel admits or refuses the child on that trace.
On this page
A coordinator can split work across many agents, and any kernel that serves a child task can check that the child’s authority descends, hop by hop, from a plan its issuer signed.
Chio checks the delegation as one signed bundle: the task graph, a witness for every edge, the selected routes, the budget pool, the revocation epoch, and the receipts that close the graph. The running example is the bundle chio proof fixture generate recursive-runtime-swarm writes: one root task delegating to three children, the shape the fixer builds when it splits issue #417 across three helpers. The fixture names its tasks generically, so the outputs below read task-root and task-child-a.
A delegation is checked as one bundle, accepted whole or refused
Every record in the bundle points at the graph, and the graph names every record. A child is admitted only when all of them agree.
a1task-child-aa2task-child-ba3task-child-c
Every edge carries a signed narrowing
Each delegation edge has exactly one witness chain. The verifier anchors the first parent scope to task-root’s scope hash 07bedb… and the last child scope to the child’s 582bb5…, then recomputes the subset relation from the scope text. The task-child-b witness keeps commerce/reserve_budget and cuts max_invocations from 3 to 1. Delegation Witnesses.
In the fixture, task-root delegates to task-child-a, task-child-b, and task-child-c under a graph that signs maxDepth 2 and maxFanout 3. Their results meet at join-child-results, whose next task is task-root. Each edge carries one witness chain anchored at the root’s scope hash 07bedb… and the child’s 582bb5…; the task-child-b witness keeps commerce/reserve_budget and cuts max_invocations from 3 to 1.
The graph indexes everything else. task-child-a names continuation-child-a, route-child-a, and budget-child-a, and witness-child-a covers its edge. The graph itself names budget-pool-swarm-valid and revocation-epoch-swarm-valid. Delegated scope is evidence about authority; the runtime process tree separately decides which descendants a cancellation reaches, as the process authority tree shows.
Read more in Swarm Protocol, which defines every artifact in the bundle.
One pure function decides whether a delegation is valid
verify_swarm_authority_bundle takes the bundle and the trusted issuer keys and returns a complete report or an error. The caller supplies the evaluation time as now_unix_ms.
pub fn verify_swarm_authority_bundle(
bundle: &SwarmAuthorityBundle,
trusted_witness_issuer_keys: &[PublicKey],
) -> Result<SwarmAuthorityVerifierReport, SwarmAuthorityError> {The function reads only its arguments, with no filesystem, network, clock, or interior mutability. A successful report carries verdict: verified; a failed check returns SwarmAuthorityError with no partial report.
Four callers share it. Runtime admission in chio-runtime-core calls it before dispatch. chio proof verify calls it from the swarm arm of its family dispatch, chio-proof-room calls it on a bundle reconstructed from an evidence graph, and chio-conformance calls it in r_t03_recursive_swarm_conformance. Every path applies one definition of a valid delegation.
Trust comes first, and each later check reads an index already verified
The entry point is a straight sequence of fallible calls, up to the point where the claim list is assembled.
require_trusted_witness_issuer_keys(trusted_witness_issuer_keys)?;
validate_task_graph(&bundle.task_graph, bundle.now_unix_ms)?;
verify_task_graph_signature(&bundle.task_graph, trusted_witness_issuer_keys)?;
require_signed_swarm_delegation_evidence(bundle)?;
let graph_sha256 = canonical_sha256(&bundle.task_graph)?;
let task_by_id = task_index(&bundle.task_graph)?;
let edge_set = edge_set(&bundle.task_graph.edges);
let route_by_id =
validate_route_plan_receipts(bundle, &task_by_id, trusted_witness_issuer_keys)?;
let join_by_id = validate_join_receipts(bundle, &task_by_id, trusted_witness_issuer_keys)?;
let allocation_by_id = validate_budget_pool(bundle, &task_by_id)?;
validate_revocation_epoch(bundle, &task_by_id, trusted_witness_issuer_keys)?;
validate_terminal_graph_receipts(
bundle,
&task_by_id,
&route_by_id,
&join_by_id,
&allocation_by_id,
trusted_witness_issuer_keys,
)?;
validate_continuation_tokens(&ContinuationValidationContext {
bundle,
graph_sha256: &graph_sha256,
task_by_id: &task_by_id,
edge_set: &edge_set,
route_by_id: &route_by_id,
join_by_id: &join_by_id,
allocation_by_id: &allocation_by_id,
trusted_witness_issuer_keys,
})?;
validate_witness_chains(bundle, &task_by_id, &edge_set, trusted_witness_issuer_keys)?;An unconfigured verifier stops before it reads a single artifact. Graph structure and the graph signature come next, so every later check indexes into a graph already known to be acyclic, single-rooted, depth-consistent, and signed by a pinned issuer. Routes, joins, allocations, and the epoch are validated and indexed before continuations, because a continuation token names all four. Witness chains run last, over the edge set, as a cover check.
Routes, joins, witness chains, and terminal receipts are checked in both directions: every declared reference is backed, and every supplied artifact is declared. That symmetry makes the bundle atomic. A tampered graph also breaks every continuation’s graph_sha256, and the two digests cover different bytes, so repairing one leaves the other broken.
Read more in Totality rules and recomputed digests.
The kernel admits a swarm-bound call only after its bundle verifies
Runtime admission runs before dispatch, for a request whose governed intent carries a chioSwarm context naming seven evidence references. The host installs ChioRuntimeAdmissionHook with the operator’s trusted keys through with_swarm_witness_keys and stores signed bundles with insert_swarm_authority_bundle.
- Load.
chio-runtime-coreloads the bundle from its admission store by task-graph evidence id. - Clock. It overwrites
now_unix_mswith the request clock. - Match. Each referenced evidence id and its canonical SHA-256 must match the stored artifact, or the call is refused with
chio_swarm_authority_ref_mismatch. - Route. Live route metadata is compared against the route-plan receipt.
- Verify. The verifier runs; a rejection becomes
chio_swarm_authority_rejected, carrying the verifier’s message as detail. - Consume. A
single_usecontinuation id is consumed, and a second presentation is refused withchio_swarm_continuation_replay. Aresumabletoken is presented again without replay accounting.
The proof paths run after the fact, on untrusted input. chio proof verify and the Proof Room read the keys pinned for the swarm.witness role, from --trust or the default trust file. Reference hashing, route comparison, and continuation consumption belong to the runtime path alone.
Replay protection has two layers. Token ids and nonces are unique within one bundle, and the admission store refuses a second presentation of a consumed single_use token across bundles.
Worked Rust example: Install swarm admission on a working kernel
Install swarm admission on a working kernel
Register a local tool handler, issue its capability, populate the evidence store, and install ChioRuntimeAdmissionHook with trusted witness keys. Calls go through ChioKernel::evaluate_tool_call_with_metadata.
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 runtime-hostImplementation
#[path = "../admission_support.rs"]
mod admission_support;
use admission_support::*;
use chio_core_types::{capability::scope::ChioScope, crypto::Keypair};
use chio_kernel::*;
use chio_runtime_core::*;
use serde_json::{json, Value};
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
use swarm_authority_examples::*;
struct Analysis {
calls: Arc<AtomicUsize>,
}
#[async_trait::async_trait]
impl ToolServerConnection for Analysis {
fn server_id(&self) -> &str {
"analysis"
}
fn tool_names(&self) -> Vec<String> {
vec!["record_result".into()]
}
fn tool_is_read_only(&self, _tool: &str) -> bool {
false
}
async fn invoke(
&self,
tool: &str,
args: Value,
_bridge: Option<&mut dyn NestedFlowBridge>,
) -> Result<Value, KernelError> {
if tool != "record_result" {
return Err(KernelError::ToolServerError("unknown analysis tool".into()));
}
self.calls.fetch_add(1, Ordering::SeqCst);
Ok(json!({"recorded": args}))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let kernel_key = Keypair::generate();
let mut kernel = ChioKernel::new(KernelConfig {
keypair: kernel_key.clone(),
ca_public_keys: Vec::new(),
max_delegation_depth: 3,
policy_hash: chio_core_types::sha256_hex(include_bytes!("runtime-host.rs")),
allow_sampling: false,
allow_sampling_tool_use: false,
allow_elicitation: false,
max_stream_duration_secs: DEFAULT_MAX_STREAM_DURATION_SECS,
max_stream_total_bytes: DEFAULT_MAX_STREAM_TOTAL_BYTES,
require_web3_evidence: false,
// Keys and budget counters live for one process. A SQLite store
// retains parent capabilities and receipts for this run.
allow_ephemeral_receipt_log: true,
allow_ephemeral_revocation_store: true,
checkpoint_batch_size: DEFAULT_CHECKPOINT_BATCH_SIZE,
retention_config: None,
memory_budget: chio_kernel::MemoryBudgetConfig::defaults(),
deadlines: chio_kernel::HotPathDeadlineConfig::default(),
});
kernel.set_federation_local_kernel_id("kernel.vendor-b");
let calls = Arc::new(AtomicUsize::new(0));
kernel.register_tool_server(Box::new(Analysis {
calls: calls.clone(),
}));
let worker = Keypair::generate();
let capability = kernel.issue_capability(
&worker.public_key(),
ChioScope {
grants: scope_for("analysis", "record_result", 10).grants,
..ChioScope::default()
},
300,
)?;
kernel.set_capability_trust_root(
kernel_key.public_key(),
chio_core_types::capability::attenuation::scope_hash(&capability.scope)?,
);
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 = json!({"record": "analysis-7", "value": "complete"});
admission.binding.capability_id = capability.id.clone();
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 metadata = json!({"route": {"bridge":route.bridge_id, "protocolTarget":route.protocol_target, "selectedRoute":route.selected_route}});
store.insert_swarm_authority_bundle(swarm.clone())?;
let mut request =
chio_swarm_runtime_request(args, admission_hash, swarm_runtime_context(&swarm)?)?;
request.capability = capability;
request.agent_id = worker.public_key().to_hex();
let hook = allowing_chio_policy_hook(store)?.with_swarm_witness_keys(trusted_witness_keys());
kernel.set_runtime_admission_hook(Arc::new(hook));
let wrong =
json!({"route":{"bridge":"mcp", "protocolTarget":"mcp://other", "selectedRoute":"other"}});
let denied = kernel
.evaluate_tool_call_with_metadata(&request, Some(wrong))
.await?;
assert!(denied.verdict != Verdict::Allow);
assert_eq!(calls.load(Ordering::SeqCst), 0);
println!("wrong route: DENY; handler calls 0");
let allowed = kernel
.evaluate_tool_call_with_metadata(&request, Some(metadata.clone()))
.await?;
assert!(allowed.verdict == Verdict::Allow, "{allowed:#?}");
assert_eq!(calls.load(Ordering::SeqCst), 1);
println!("valid continuation: ALLOW; handler calls 1");
let replay = kernel
.evaluate_tool_call_with_metadata(&request, Some(metadata))
.await?;
assert!(replay.verdict != Verdict::Allow);
assert_eq!(calls.load(Ordering::SeqCst), 1);
assert_eq!(
replay
.receipt
.metadata
.as_ref()
.and_then(|m| m.get("chio_runtime"))
.and_then(|m| m.get("failure_code"))
.and_then(Value::as_str),
Some("chio_swarm_continuation_replay")
);
for (name, response) in [
("wrong-route", denied),
("allowed", allowed),
("replay", replay),
] {
assert!(response.receipt.verify_signature()?);
std::fs::write(
format!("{name}-receipt.json"),
serde_json::to_vec_pretty(&response.receipt)?,
)?;
}
println!("same continuation: DENY; handler calls 1; three receipt signatures verified");
Ok(())
}pub 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))
}pub 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: DENY; handler calls 0
valid continuation: ALLOW; handler calls 1
same continuation: DENY; handler calls 1; three receipt signatures verifiedThe wrong route and replay leave the handler count unchanged. All three returned receipt signatures are verified and saved. This is one local kernel with process-local admission state; a persistent distributed host needs durable consumption and its own evidence acquisition.
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.
The prepared fixture verifies with eight claims, and a revoked task refuses it
The fixture bundle exercises the whole order of checks: a four-task graph, three witness chains, three route plans, one join, one pool, and one epoch. Generate it with its eleven negative variants. The generator also writes trust.toml, the fixture’s signing keys marked as a fixture profile.
$ chio proof fixture generate recursive-runtime-swarm --with-negatives --out ./swarmgenerated recursive-runtime-swarm ./swarm trust ./swarm/trust.toml (fixture profile, 4 roles) negatives 11 under ./swarm/negatives
chio proof verify prints the verdict and every claim it established. Add --json for the full report.
$ chio proof verify ./swarm/proof-room-bundle --trust ./swarm/trust.tomlVERIFIED passport-swarm-valid 8 claims claim.swarm.task_graph_bound claim.swarm.continuation_fresh claim.swarm.attenuation_witness_chain_bound claim.swarm.route_plan_bound claim.swarm.join_receipt_bound claim.swarm.budget_pool_bound claim.swarm.revocation_epoch_bound claim.swarm.terminal_graph_receipt_bound trust ./swarm/trust.toml (fixture keys)
You should see eight claims, one per artifact family. The full report also carries a hopReports array under family_reports, with attenuationVerified, routeVerified, budgetVerified, and lineageVerified per child task, plus the SHA-256 of the claim set, the evidence graph, and the verifier policy.
The revoked-task negative that chio proof fixture generate recursive-runtime-swarm --with-negatives writes to ./swarm/negatives/revoked-task/ is signed and internally consistent, but its epoch carries "revokedTaskIds":["task-child-a"]. It targets claim.swarm.revocation_epoch_bound: the coordinator revoked one helper after delegating to it, and every kernel that reads the epoch must now refuse that helper’s work.
$ chio proof verify ./swarm/negatives/revoked-task --trust ./swarm/trust.tomlREFUSED revoked-task swarm task is revoked: task-child-a trust ./swarm/trust.toml (fixture keys)
You should see the refusal name the task, with exit status 1 and no report. The seven claims that would have verified are absent too, because the verifier returns on the first failing check. The worked example below builds and verifies the same kind of bundle from Rust.
Worked Rust example: Build and verify an authority bundle
Build and verify an authority bundle
Construct a signed graph and every referenced artifact, then choose the verification key and evaluation time independently. The program writes bundle.json for inspection or a second verifier.
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 authorityImplementation
use chio_core_types::crypto::PublicKey;
use chio_swarm_authority::{verify_swarm_authority_bundle, SwarmAuthorityBundle};
use swarm_authority_examples::{build_bundle, trusted_witness_keys};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
if args.get(1).map(String::as_str) == Some("verify") {
if args.len() != 5 {
return Err("usage: authority verify bundle.json trusted-key-hex now-unix-ms".into());
}
let mut bundle: SwarmAuthorityBundle = serde_json::from_slice(&std::fs::read(&args[2])?)?;
// The verifier chooses time and trust; neither comes from the submitted bundle.
bundle.now_unix_ms = args[4].parse()?;
let trusted = vec![PublicKey::from_hex(&args[3])?];
let report = verify_swarm_authority_bundle(&bundle, &trusted)?;
println!("{}", serde_json::to_string_pretty(&report)?);
return Ok(());
}
let bundle = build_bundle()?;
let trusted = trusted_witness_keys();
let report = verify_swarm_authority_bundle(&bundle, &trusted)?;
assert_eq!(
(
report.task_count,
report.continuation_count,
report.join_count
),
(3, 2, 1)
);
std::fs::write("bundle.json", serde_json::to_vec_pretty(&bundle)?)?;
println!(
"verified: {} tasks, {} continuations, {} join",
report.task_count, report.continuation_count, report.join_count
);
println!("trusted key: {}", trusted[0].to_hex());
println!("evaluation time: {}", bundle.now_unix_ms);
let error =
verify_swarm_authority_bundle(&bundle, &[]).expect_err("an unpinned signer must refuse");
println!("untrusted signer: {error}");
Ok(())
}pub fn build_bundle() -> Result<SwarmAuthorityBundle, Box<dyn Error>> {
let parent_scope = scope_for("analysis", "record_result", 3);
let child_scope = scope_for("analysis", "record_result", 1);
let parent_scope_hash = scope_hash(&parent_scope)?;
let child_scope_hash = scope_hash(&child_scope)?;
let witness = compute_attenuation_witness(&parent_scope, &child_scope)?;
let mut task_graph = SwarmTaskGraph {
schema: CHIO_SWARM_TASK_GRAPH_SCHEMA.to_string(),
graph_id: "analysis-graph".to_string(),
root_transaction_ref: "passport-analysis".to_string(),
planner_subject: "did:chio:planner".to_string(),
issuer: witness_issuer(),
signature: String::new(),
created_at_unix_ms: now_ms() - 1_000,
expires_at_unix_ms: now_ms() + 60_000,
max_depth: 2,
max_fanout: 2,
multi_hop_witness_chains: false,
nodes: vec![
SwarmGraphNode {
task_id: "task-root".to_string(),
parent_task_id: None,
route_plan_ref: None,
continuation_token_ref: None,
budget_allocation_ref: None,
scope_hash: parent_scope_hash.clone(),
depth: 0,
},
SwarmGraphNode {
task_id: "task-child-a".to_string(),
parent_task_id: Some("task-root".to_string()),
route_plan_ref: Some("route-child-a".to_string()),
continuation_token_ref: Some("continuation-child-a".to_string()),
budget_allocation_ref: Some("budget-child-a".to_string()),
scope_hash: child_scope_hash.clone(),
depth: 1,
},
SwarmGraphNode {
task_id: "task-child-b".to_string(),
parent_task_id: Some("task-root".to_string()),
route_plan_ref: Some("route-child-b".to_string()),
continuation_token_ref: Some("continuation-child-b".to_string()),
budget_allocation_ref: Some("budget-child-b".to_string()),
scope_hash: child_scope_hash.clone(),
depth: 1,
},
],
edges: vec![
SwarmGraphEdge {
from_task_id: "task-root".to_string(),
to_task_id: "task-child-a".to_string(),
edge_type: "delegates".to_string(),
},
SwarmGraphEdge {
from_task_id: "task-root".to_string(),
to_task_id: "task-child-b".to_string(),
edge_type: "delegates".to_string(),
},
],
joins: vec![SwarmGraphJoin {
join_id: "join-child-results".to_string(),
parent_task_ids: vec!["task-child-a".to_string(), "task-child-b".to_string()],
next_task_id: "task-root".to_string(),
}],
budget_pool_ref: "budget-pool-analysis".to_string(),
revocation_epoch_ref: "revocation-epoch-analysis".to_string(),
route_plan_refs: vec!["route-child-a".to_string(), "route-child-b".to_string()],
};
sign_task_graph(&mut task_graph)?;
let graph_sha256 = canonical_hash(&task_graph)?;
let empty_revoked_subjects = Vec::<String>::new();
let empty_revoked_task_ids = Vec::<String>::new();
let revocation_epoch_root_hash =
revocation_epoch_root_hash(&empty_revoked_subjects, &empty_revoked_task_ids)?;
let witness_chains = vec![
witness_chain(
"witness-child-a",
"task-root",
"task-child-a",
&parent_scope_hash,
&child_scope_hash,
witness.clone(),
)?,
witness_chain(
"witness-child-b",
"task-root",
"task-child-b",
&parent_scope_hash,
&child_scope_hash,
witness,
)?,
];
let continuation_tokens = vec![
continuation_token(
"continuation-child-a",
"task-child-a",
"route-child-a",
&graph_sha256,
&revocation_epoch_root_hash,
Some(&witness_chains[0]),
)?,
continuation_token(
"continuation-child-b",
"task-child-b",
"route-child-b",
&graph_sha256,
&revocation_epoch_root_hash,
Some(&witness_chains[1]),
)?,
];
task_graph.nodes[1].continuation_token_ref = Some(continuation_tokens[0].token_id.clone());
task_graph.nodes[2].continuation_token_ref = Some(continuation_tokens[1].token_id.clone());
let join_request = SwarmJoinReceiptMintRequest {
join_id: "join-child-results".to_string(),
graph_id: "analysis-graph".to_string(),
chain_id: "analysis-chain".to_string(),
dag_ordinal: 2,
hlc_unix_ms: now_ms() - 500,
parent_task_receipts: vec![
SwarmJoinParentReceipt {
task_id: "task-child-a".to_string(),
receipt_id: "receipt-child-a".to_string(),
},
SwarmJoinParentReceipt {
task_id: "task-child-b".to_string(),
receipt_id: "receipt-child-b".to_string(),
},
],
expected_parent_receipt_ids: vec![
"receipt-child-a".to_string(),
"receipt-child-b".to_string(),
],
actual_parent_receipt_ids: vec![
"receipt-child-a".to_string(),
"receipt-child-b".to_string(),
],
join_predicate: "all_success".to_string(),
result_digest: sha256_hex(b"joined-child-results"),
next_task_id: "task-root".to_string(),
};
let join_receipts = vec![mint_swarm_join_receipt(join_request, &witness_keypair())?];
let mut bundle = SwarmAuthorityBundle {
task_graph,
continuation_tokens,
witness_chains,
join_receipts,
route_plan_receipts: vec![
route_plan_receipt("route-child-a", "task-child-a", "mcp", "mcp://provider-a")?,
route_plan_receipt("route-child-b", "task-child-b", "a2a", "a2a://provider-b")?,
],
budget_pool: SwarmBudgetPool {
schema: CHIO_SWARM_BUDGET_POOL_SCHEMA.to_string(),
pool_id: "budget-pool-analysis".to_string(),
graph_id: "analysis-graph".to_string(),
currency: "USD".to_string(),
total_units: 10_000,
allocations: vec![
SwarmBudgetAllocation {
allocation_id: "budget-child-a".to_string(),
task_id: "task-child-a".to_string(),
dimension_id: "usd_minor".to_string(),
state: SwarmBudgetAllocationState::Active,
max_units: 2_500,
reserved_units: 0,
active_units: 2_500,
consumed_units: 0,
released_units: 0,
reversed_units: 0,
},
SwarmBudgetAllocation {
allocation_id: "budget-child-b".to_string(),
task_id: "task-child-b".to_string(),
dimension_id: "usd_minor".to_string(),
state: SwarmBudgetAllocationState::Active,
max_units: 2_500,
reserved_units: 0,
active_units: 2_500,
consumed_units: 0,
released_units: 0,
reversed_units: 0,
},
],
},
revocation_epoch: SwarmRevocationEpoch {
schema: CHIO_SWARM_REVOCATION_EPOCH_SCHEMA.to_string(),
epoch_id: "revocation-epoch-analysis".to_string(),
root_hash: revocation_epoch_root_hash,
issued_at_unix_ms: now_ms() - 1_000,
valid_until_unix_ms: now_ms() + 60_000,
revoked_subjects: empty_revoked_subjects,
revoked_task_ids: empty_revoked_task_ids,
issuer: witness_issuer(),
signature: String::new(),
},
terminal_receipts: vec![terminal_graph_receipt()?],
now_unix_ms: now_ms(),
};
sign_revocation_epoch(&mut bundle.revocation_epoch)?;
Ok(bundle)
}Captured result and refusal
verified: 3 tasks, 2 continuations, 1 join
trusted key: e5a17d66cccd612fc7cd09d2db256b67fccf9ac0082fffef16eff9e6cdb0b930
evaluation time: 1789230049403
untrusted signer: trusted swarm witness keys missing: CHIO_SWARM_TRUSTED_WITNESS_KEYS must pin trusted swarm witness keysThis verifies signed declarations. It does not dispatch the graph's tasks. The configured kernel example below covers admission before a tool handler.
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.
A bundle that fails any check is refused before a report exists
An empty trusted-key slice refuses the bundle on the first line, before any artifact validation or structural check. One slice pins the graph, the routes, the joins, the epoch, the terminal receipts, the continuations, and every witness hop alike; the caller decides which keys it holds.
fn require_signed_swarm_delegation_evidence(
bundle: &SwarmAuthorityBundle,
) -> Result<(), SwarmAuthorityError> {
if bundle.continuation_tokens.is_empty() || bundle.witness_chains.is_empty() {
return Err(rejected(
"signed swarm delegation evidence missing: continuation tokens and witness chains are required",
));
}
Ok(())
}A graph with one task and no edges is structurally valid and still refused. Four of the eight claims are pushed only when their collection is non-empty, so this check keeps a report from reading verified without signed delegation evidence. Both proof paths also compare verified_claims against the required_claims in the verifier policy shipped with the bundle.
SwarmAuthorityHopReport flags are a per-hop rendering of the one bundle-wide verdict, written only after the whole bundle verified. On any failure there is no report to read. The Denial Codes page verifies all 11 negative fixtures in one command.
Verification establishes consistency with the supplied keys and time. Join and terminal receipts are signed declarations: the verifier checks their references, parent sets, and accounting, and it never observes the child tasks running or reads parent execution receipt bodies. What a receipt proves about an external effect follows the mediation boundary.
Bundle fields and verifier rules
Search for now_unix_ms, quorum, deny-private-network, or graph_sha256.
Supply all nine fields, with matching graph references and an explicit evaluation time. Unknown JSON fields are rejected, including inside nested witnesses.
task_graph
The signed plan: one root, tasks with depth and scope hash, delegation edges, joins, and the max_depth and max_fanout ceilings.
PageTask graphs
continuation_tokens
Every parented task references a token. Additional valid tokens for a child are permitted.
witness_chains
One per graph edge, each a sequence of hops carrying a scope-subset proof.
join_receipts
One per declared join: parent set hash, DAG ordinal, and join predicate.
route_plan_receipts
Each receipt matches a route_plan_refs entry and carries the selected route, candidate-set digest, registry snapshot hash, bridge, protocol target, egress contract, and constraints.
budget_pool
Its id matches budget_pool_ref. Carries the pool total plus per-task allocations across five states.
revocation_epoch
Its id matches revocation_epoch_ref. Carries revoked subjects and revoked task ids inside a validity window.
terminal_receipts
Graph closure: completed tasks, joins, routes, and per-dimension budget rollups. Each receipt closes the whole graph.
now_unix_ms
The evaluation instant every freshness check runs against. Runtime admission overwrites it with the request clock.
Issuer keys
Issuers are strings. An issuer prefixed did:chio: is self-certifying: exactly 64 lowercase hex characters after the prefix, parsed as the Ed25519 public key that signed the artifact. A bare hex key is also accepted. Either way the resulting key must appear in trusted_witness_issuer_keys, which makes the DID a pin.
require_trusted_witness_issuer_keys refuses an empty slice before any check; the caller has already deserialized the bundle. Most artifact families repeat the emptiness check ahead of their own pin comparison, and the revocation epoch fails the comparison instead, because no key matches an empty set. The tests are swarm_authority_stage0_rejects_unpinned_witness_issuer and swarm_authority_stage0_rejects_root_only_bundle_without_trusted_witness_keys. The CLI reads the slice from the keys pinned for swarm.witness and stops with trust:role-unpinned when none is pinned. The crate resolves, rotates, and expires no keys and assigns no issuer to an artifact class.
Closed vocabularies and one gate
- A route-plan receipt’s
egress_constraintsis non-empty, and every entry is the literaldeny-private-network; any other value isunsupported swarm route-plan egress constraint. Itsattenuation_decisionis the literalaccepted. - A route-plan receipt’s
selected_routeandegress_contract_ideach start with its ownbridge_idfollowed by:, andprotocol_targetstarts with the same id followed by://. join_predicateisall_success,any_success, orquorum:NwithNbetween one and the expected parent count. A graph join needs at least two parents.- A witness chain of more than one hop is refused unless the signed graph sets
multiHopWitnessChains, a per-graph gate carried inside the signed artifact. - The allocation a continuation names is in state
active, and the pool’s summedmax_unitsstays within itstotal_units.
Totality rules
- Every
route_plan_refsentry has a receipt, and every receipt matches a ref. Duplicates are refused on sight. - Every graph join has a receipt whose
next_task_idequals the join’s and whose expected-parent count equals the join’s parent count. - Every edge is covered by exactly one witness chain. A second chain over the same edge is a duplicate; an uncovered edge is
missing swarm delegation witness chain. - Every task with a parent names a continuation token whose
child_task_idis the naming task. Token ids and nonces are unique within the bundle. Each token binds exactly one ofparent_task_idandjoin_receipt_id. - Continuations are the one asymmetric family.
validate_continuation_tokenswalks tasks to find missing tokens and accepts a second valid token for a child, which only raisescontinuation_countin the report. The runtime selects the token by naming one token id in the request. - The terminal receipt’s completed-task set equals the graph’s entire task set, its join set every join, and its route set every route. A terminal receipt closes the whole graph or fails.
Recomputed digests and rollups
Four digest fields and the terminal budget rollups are recomputed from the bundle’s own contents and compared.
continuation.graph_sha256: canonical JSON of the whole task graph, signature field included.continuation.witness_chain_sha256: canonical JSON of the referenced chain, whose parent and child task ids also match the token.revocation_epoch.root_hash: canonical JSON of the sorted revoked-subject and revoked-task lists.join.parent_set_hash: canonical JSON of the chain id plus the sorted actual parent receipt ids.terminal.budget_rollups: per-dimension sums over every allocation in the pool, compared as whole maps. Each rollup’s five state counts sum to its declaredtotal_units, and the sum across dimensions stays within the pool total.
The graph signature body drops the signature field and the token digest keeps it. swarm_authority_stage0_rejects_task_graph_tampering_after_continuations_are_resealed widens max_fanout, reseals every continuation against the new digest, and still lands on swarm task graph signature invalid.
Claims and per-hop flags
swarm_authority_stage0_rejects_root_only_bundle_without_signed_swarm_evidence pins the root-only refusal. The required_claims list is set per verifier policy: the recursive-delegation fixture requires seven of the eight swarm claims and omits claim.swarm.terminal_graph_receipt_bound. spec/registries/proof-manifest.v1.json registers all eight against test, fixture, and schema references, as a registry checked for liveness.
SwarmAuthorityHopReport carries authority_verified, attenuation_verified, lineage_verified, route_verified, and budget_verified, each written as the literal true while the report is built.
Error type
SwarmAuthorityError has two variants, Rejected(String) and Canonical(String). Callers that need a code map the whole error: the runtime to one denial code, the proof room to a slug. Match on the message only where a fixture already pins it.
Tests and negative fixtures
swarm_authority_stage0_rejects_generated_recursive_scope_widening is a 32-case proptest asserting swarm attenuation witness invalid for a widened child scope inside a signed witness hop. The stage-0 suite in crates/kernel/chio-swarm-authority/tests/swarm_authority_stage0.rs covers the rejection paths one at a time: tampered or resealed signatures on the graph, continuations, join receipts, route plans, witness hops, and the epoch; stale and future timestamps; nonce replay; ungated multi-hop chains; released and oversubscribed allocations; revoked tasks and revoked subjects.
11 negative fixture bundles are replayed on every run of chio proof doctor --scenario proof-package. Each directory fails verification, and the failure matches the expected_failure_code in its paired descriptor: the raw error text contains the code, or the slug derived from the error carries it at a : or whitespace boundary. A reworded rejection reads as a mismatch. The replay lives in package_negative_expected_failure and check_transaction_passport_rejects, with the fixture catalog built into the binary and proof_doctor_accepts_proof_package_evidence.
The descriptors in negatives/ are unsigned JSON: schema chio.swarm-authority.negative-fixture.v1, a claim_ref, the base_fixture the case mutates, a case name, and the expected code. The signed material lives in the fixture directory beside them. Nine of the 11 are also registered as claim evidence in the proof manifest; egress-constraint-unsupported and max-depth-exceeded are the two outside it.
Sources and wire contract
The report schema is spec/schemas/chio-swarm/v1/authority-verifier-report.schema.json, and the verification order is spec/PROTOCOL.md section 6.4.2 in the public Chio repository. The verifier is crates/kernel/chio-swarm-authority/src/verifier.rs and the error type crates/kernel/chio-swarm-authority/src/error.rs.