Chio/Docs
LOGIN · JOIN

PlatformThe Delegation Graph

Swarm

Task Graphs

The signed graph that bounds recursive delegation: one root, exact depth arithmetic, and ceilings a planner cannot widen after issuance.

The rung page next door

Swarm Overview owns the rung: the filing test, the fail-closed verifier entry point, and the eight artifact roles a delegation bundle carries. This page owns the first of them and stops where the graph stops. The other seven appear below only where the graph binds them.

Source

This page reflects spec/schemas/chio-swarm/v1/task-graph.schema.json and spec/PROTOCOL.md section 6.4.2 in the chio repository. The Swarm Protocol reference carries the full set of artifacts and the verification order.


The first object on the ladder with a depth

A task graph is one signed artifact naming every task in a recursive delegation, the edges between them, the joins that fan results back together, and two integer ceilings on the shape those edges may take. Its schema tag is chio.swarm.task-graph.v1, its Rust type is SwarmTaskGraph in crates/kernel/chio-swarm-authority/src/types.rs, and its published wire shape is spec/schemas/chio-swarm/v1/task-graph.schema.json. That JSON Schema is a spec artifact: no crate loads it to validate a graph. Parsing is what rejects a malformed one.

Depth is what makes this artifact different from anything on the rungs below. Delegation depth is not new there: TierScopeCeiling in crates/platform/chio-control-plane/src/policy/types.rs carries an optional max_delegation_depth. What is new is the record. No artifact at the cluster rung carries a depth field, because a node under one operator does not have to prove to anyone how far a call had already been delegated. A swarm graph declares that number per node and the verifier corroborates it against the edge set, and every check on this page follows from that.

Status. Verification is shipped and sits on a runtime path. verify_swarm_authority_bundle is a pure function with no I/O and no clock of its own: the current time arrives as bundle.now_unix_ms and the trusted issuer keys arrive as an argument. The pre-dispatch admission hook at crates/kernel/chio-runtime-core/src/admission_hook/swarm_authority.rs calls it before a swarm-bound child action runs. Issuance is not shipped: sign_swarm_task_graph is exported, and every call site in the workspace is a test helper or the crate’s own examples/agent_os.rs.


The object

Field names below are the wire spellings. The Rust struct uses snake case and deserializes with deny_unknown_fields, so an unrecognized key is a parse failure rather than a silently ignored extension.

FieldTypeWhat the verifier does with it
schemaconstMust equal chio.swarm.task-graph.v1. Any other value rejects before a second field is read.
graphIdstringEvery bundle artifact that carries a graphId of its own must match it, checked by require_same_graph: continuation tokens, witness chains, route-plan receipts, join receipts, the budget pool, and terminal receipts. The revocation epoch has no graphId and is bound by id instead.
rootTransactionRefstringNon-empty. Names the transaction the graph descends from.
plannerSubjectstringNon-empty, and checked against the revocation epoch: a revoked planner rejects the whole bundle, as does a revoked graph issuer or a revoked witness-hop issuer.
issuer / signaturestringThe issuer resolves to a chio_core_types::crypto::PublicKey. A did:chio: prefix must be self-certifying: exactly 64 lowercase hex characters, which forces Ed25519. A bare issuer goes straight to PublicKey::from_hex, which also accepts the self-describing p256:, p384:, and hybrid: forms. The resolved key must appear in the caller-supplied trusted set.
createdAtUnixMs / expiresAtUnixMsintegerCompared against now_unix_ms. A graph from the future and an expired graph both reject.
maxDepthu32Ceiling on node depth. The schema and validate_task_graph both admit 0, but no bundle with maxDepth: 0 can verify. See the limit below.
maxFanoutu32Ceiling on outgoing edges per task. Zero is rejected by the verifier and excluded by the schema (minimum: 1).
multiHopWitnessChainsbool, default falseFeature gate. A witness chain carrying more than one hop rejects unless this is set on the graph.
nodes / edges / joinsarraysThe structure. validate_task_graph requires at least one node; the bundle verifier effectively requires at least two nodes and one edge. The checks are the subject of the next section.
budgetPoolRef / revocationEpochRefstringMust equal the bundle’s pool id and epoch id exactly.
routePlanRefsstring[]Unique. Every ref must have a receipt in the bundle, and every receipt in the bundle must be declared here.
crates/kernel/chio-swarm-authority/src/types.rs67-79rust
pub struct SwarmGraphNode {
    pub task_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent_task_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub route_plan_ref: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub continuation_token_ref: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub budget_allocation_ref: Option<String>,
    pub scope_hash: String,
    pub depth: u32,
}
crates/kernel/chio-swarm-authority/src/types.rs83-87rust
pub struct SwarmGraphEdge {
    pub from_task_id: String,
    pub to_task_id: String,
    pub edge_type: String,
}
crates/kernel/chio-swarm-authority/src/types.rs91-95rust
pub struct SwarmGraphJoin {
    pub join_id: String,
    pub parent_task_ids: Vec<String>,
    pub next_task_id: String,
}

task_id is unique across the graph, and scope_hash must be a lowercase 64-character SHA-256 digest. It is what the witness chain for the edge has to land on at both ends: the chain’s first hop must declare the parent node’s hash, its last hop the child node’s. The four optional node fields are less optional than the type suggests: a node with a parent_task_id must also carry a continuation_token_ref, and its route and budget refs must agree with the ones inside that token. edge_type is required to be non-empty and is otherwise uninterpreted; every fixture in the Chio repo uses delegates.

maxDepth 0 never verifies as a bundle

validate_task_graph accepts a root-only graph with maxDepth: 0, and the schema sets minimum: 0. The bundle verifier does not. require_signed_swarm_delegation_evidence rejects any bundle with no continuation tokens or no witness chains. Every witness chain must name an edge, and every edge forces a target at parent depth plus one, so maxDepth is effectively floored at 1 in anything the verifier will admit. swarm_authority_stage0_rejects_root_only_bundle_without_signed_swarm_evidence is the test: the root-only bundle fails with signed swarm delegation evidence missing rather than on any graph rule.

The checks, in order

verify_swarm_authority_bundle refuses an empty trusted key set first, then validates the graph’s shape, then verifies the graph’s signature, then refuses a bundle that carries no continuation tokens or no witness chains: validate_task_graph runs before verify_task_graph_signature, which runs before require_signed_swarm_delegation_evidence. Inside the first of those, once the header fields are checked, the task index and the edge set are built and the structural checks run in a fixed order.

crates/kernel/chio-swarm-authority/src/verifier.rsrust
let task_by_id = task_index(graph)?;
let edge_set = edge_set(&graph.edges);
validate_roots(graph)?;
validate_edges(graph, &task_by_id, &edge_set)?;
validate_joins(graph, &task_by_id)?;
validate_route_refs(graph)?;
validate_acyclic(graph, &task_by_id)?;
validate_edge_depths(graph, &task_by_id)?;
validate_graph_limits(graph)?;
RuleEnforced byRejection
Task ids unique, scope hashes well formedtask_indexduplicate swarm task id
Exactly one root, and depth 0 means no parentvalidate_rootsswarm task graph requires exactly one root task, swarm root-depth task has parent, swarm non-root task missing parent
Edges resolve, no duplicate edge, every lineage edge presentvalidate_edgesswarm task parent edge missing: A -> B
Joins well formedvalidate_joinsswarm join requires at least two parents
Route-plan refs unique and non-emptyvalidate_route_refsduplicate swarm route plan ref: {value}, swarm route plan ref must not be empty
Acyclic over the edge setvalidate_acyclicswarm task graph cycle at <task>
Every edge target at exactly parent depth plus onevalidate_edge_depthsswarm task depth mismatch: A -> B, swarm task depth overflow
Both ceilingsvalidate_graph_limitsswarm task exceeds max depth, swarm task exceeds max fanout

Structure before signature has a consequence worth stating plainly: a graph whose ceiling was edited without being re-signed is refused by the structural check, and the negative fixture is built that way on purpose. fixtures/proof-room/swarm-authority/max-depth-exceeded/task-graph.json is the valid four-task fixture with maxDepth lowered from 2 to 0 while carrying the valid graph’s signature byte for byte. Its descriptor names the failure the proof room must produce, and the collected bundle for the recursive-runtime-swarm stage records the same string under observed_failure_code. The codes are slugs of the verifier’s rejection message: the neighbouring cycle case is recorded as proof-room.negative.swarm-task-graph-cycle-at-task-child-a.

fixtures/proof-room/swarm-authority/negatives/max-depth-exceeded.jsonjson
{
  "schema": "chio.swarm-authority.negative-fixture.v1",
  "id": "max-depth-exceeded",
  "claim_ref": "claim.swarm.task_graph_bound",
  "base_fixture": "fixtures/proof-room/swarm-authority/valid-recursive-delegation/task-graph.json",
  "case": "MaxDepthExceeded",
  "expected_failure_code": "proof-room.negative.swarm-task-exceeds-max-depth"
}

Both bundles are on disk beside the descriptors, so the two refusals are two commands from a Chio checkout:

swarm-authority · graph-negativestranscript
$ source scripts/proof-room-quickstart-env.sh
$ for n in max-depth-exceeded graph-cycle; do
$   chio proof verify \
    "fixtures/proof-room/swarm-authority/$n/transaction-passport.json" 2>&1 | head -1
$   echo "exit=${PIPESTATUS[0]}"
$ done
error [urn:chio:error:cli:other]: proof verify: swarm task exceeds max depth: task-child-a
exit=1
error [urn:chio:error:cli:other]: proof verify: swarm task graph cycle at task-child-a
exit=1
exit 0

Both name the node, and both exit 1. Slugify either sentence and the descriptor’s expected_failure_code falls out of it, which is the whole binding between the two artifacts.

Depth arithmetic

crates/kernel/chio-swarm-authority/src/verifier.rsrust
let expected_child_depth = parent
    .depth
    .checked_add(1)
    .ok_or_else(|| rejected("swarm task depth overflow"))?;
if child.depth != expected_child_depth {
    return Err(rejected(format!(
        "swarm task depth mismatch: {} -> {}",
        edge.from_task_id, edge.to_task_id
    )));
}

Exactly parent depth plus one, not at most. The declared depth is not a hint the verifier recomputes and repairs; it is a claim the edge set has to corroborate. checked_add is the second half: overflow is a rejection, not a wraparound. Depth is a u32, so the wrap needs a parent declared at u32::MAX, and the depth-0 child it would produce is something validate_roots already refuses, because a depth-0 node with a parent rejects. The arithmetic refuses on its own rather than leaning on that ordering.

swarm_authority_stage0_rejects_edge_depth_bypass is the interesting negative, because it satisfies the ceiling by lying about the shape rather than by editing the ceiling. It lowers max_depth to 1, reparents the second child under the first, leaves that child’s depth at 1, repoints the edge, rebuilds the witness chain for the new pair, and refreshes the continuation digests so the downstream bindings stay consistent. The bundle rejects with swarm task depth mismatch: understating depth fails on the arithmetic before it ever reaches the ceiling.

The two ceilings

validate_graph_limits does two things. It walks the nodes and rejects any whose depth exceeds max_depth. Then it walks the edges, counting per from_task_id, and rejects the first source whose count exceeds max_fanout. Because the counter keys on the edge source, the ceiling bounds outgoing edges rather than children: an edge that expresses a dependency rather than lineage draws on the same allowance.

rendering
In the shipped positive fixture, three delegate edges each land at exactly parent depth plus one and each consume one of the three fan-out allowances. The join is declared in the graph but is not an edge, so it neither raises a depth nor spends fan-out.
sourcefixtures/proof-room/swarm-authority/valid-recursive-delegation/task-graph.jsonat fe56570

Why a planner cannot widen them

The signature body is the canonical JSON serialization of the whole graph with the signature field removed, built by task_graph_signature_body. Both ceilings are inside it, alongside every node, edge, and join. Widening a ceiling after issuance changes the signed body, and the graph is also bound downstream: each continuation token carries graphSha256, the canonical SHA-256 of the graph, which the verifier recomputes rather than trusts.

swarm_authority_stage0_rejects_task_graph_tampering_after_continuations_are_resealed closes the obvious escape. It adds one to max_fanout, recomputes the graph digest, and re-signs every continuation token against the new digest, so the digest binding is consistent again and the widened ceiling is trivially satisfied by a graph that was already inside the old one. The bundle still rejects, with swarm task graph signature invalid. Re-sealing downstream evidence does not help: max_fanout is inside the signed body, so the old signature no longer verifies over it, and minting a new one needs a key in the caller’s pinned set.

The ceilings bind a planner, not a key holder

A ceiling is a commitment by whoever signed the graph. Anyone holding a key in the trusted set can sign a different graph with different ceilings, and the verifier will accept it. What the mechanism rules out is a planner widening recursion or fan-out on a graph it has already issued. Which keys go in the trusted set is the caller’s decision, and it is upstream of everything here: Swarm Authority states what an empty set costs.

What the ceilings do not cover

Joins are declared in the graph and are not edges. The edge set the verifier builds comes from graph.edges alone, so a join takes no part in depth arithmetic, fan-out accounting, the acyclicity walk, or the requirement that every edge carry exactly one delegation witness chain. What validate_joins does check: join ids are unique, parents are unique and resolve to real tasks, the next task resolves, the next task is not one of its own parents, and there are at least two parents.

The join target is not required to sit below its parents. In the shipped positive fixture the join over three depth-1 children names task-root, the depth-0 node, as its next task, and the edge set stays acyclic because the join is not in it. That is the boundary of the containment on this page: it bounds the delegation tree, not the convergence topology drawn over it.

Schema and verifier also disagree here, in the safe direction. The schema allows a join with minItems: 1 parents; the verifier requires two, asserted by swarm_authority_stage0_rejects_single_parent_join. The schema likewise expresses neither the single root, nor depth plus one, nor either ceiling being enforced.


How the rest of the bundle hangs off the graph

The graph is the index every other artifact is checked against, in both directions. That is what keeps it from being metadata attached to work already authorized elsewhere.

ArtifactBound to the graph byRejected when
Continuation tokenSame graphId, recomputed graphSha256, and a child task that is a node in the graphA node with a parent carries no continuationTokenRef, or the referenced token names a different child
Delegation witness chainIts (parentTaskId, childTaskId) pair must be an edge, and each edge gets exactly one chainTwo chains cover one edge, or an edge has no chain
Route-plan receiptroutePlanRefs covers the receipts both ways; the node’s ref must equal the continuation token’sAn undeclared receipt, a declared ref with no receipt, or a receipt whose taskId is not the token’s child
Budget allocationEvery allocation’s taskId must be a node; the node’s ref must equal the token’sAn allocation for an unknown task; a unit rollup that does not sum to maxUnits; allocations whose maxUnits sum past the pool total. State is checked only where a continuation token points: that allocation must be Active
Revocation epochIts revoked task ids are matched against the node set; its revoked subjects against the graph issuer, the planner, and every witness-hop issuerAny revoked task id names a node in this graph, or any of those three subject roles is revoked
Terminal graph receiptSet equality, not containmentcompletedTaskIds is not exactly the node set, or the join and route id sets do not match the bundle

Guarantees and limits

StatusClaimEvidence
ShippedBoth ceilings sit inside the signed body, so widening either one requires a fresh signature from a key in the caller’s trusted set.task_graph_signature_body, ensure_task_graph_issuer_is_pinned
ShippedDepth arithmetic rejects overflow rather than wrapping, and demands exact equality on every edge rather than an upper bound.validate_edge_depths
Proved by testWidening maxFanout and re-sealing every continuation token against the new graph digest does not rescue the bundle.swarm_authority_stage0_rejects_task_graph_tampering_after_continuations_are_resealed
Proved by testUnderstating a child’s depth to fit a lowered ceiling rejects on the arithmetic, with the witness chain and continuation digests rebuilt to match.swarm_authority_stage0_rejects_edge_depth_bypass
Proved by testA back edge from a child to the root rejects as a cycle, and the same case is frozen as a proof-room negative fixture.swarm_authority_stage0_rejects_graph_cycle, fixtures/proof-room/swarm-authority/graph-cycle/
Proved by testThe signed positive fixture verifies. A proptest draws 48 cases over a six-mutation space, each asserting the expected rejection message. Random draws, not a swept matrix.crates/tooling/chio-conformance/tests/r_t03_recursive_swarm_conformance.rs
SpecThe protocol text states the same two ceilings, the depth-plus-one rule, and overflow as a rejection. Code and text agree today; the text is the contract a third-party implementation is held to.spec/PROTOCOL.md section 6.4.2
Not claimedThat schema validation is sufficient, or that it happens. No crate loads the schema. It also admits a one-parent join the verifier refuses, and expresses none of the one-root, depth-plus-one, or ceiling rules.spec/schemas/chio-swarm/v1/task-graph.schema.json against validate_joins
Not claimedThat either ceiling bounds graph size on its own. Each is local: maxDepth bounds depth per node, maxFanout bounds outgoing edges per edge source, and no field states a total. Together they do bound it, because exactly one root plus the parent-edge and depth-plus-one rules make the lineage a tree, so a full tree at both ceilings is well formed and grows exponentially in maxDepth. Swarm Scarcity carries the closed form. The only quantitative bound on what a graph may spend is the budget pool.validate_graph_limits, validate_roots, validate_edges, validate_edge_depths, validate_budget_pool
Not claimedThat the verifier bounds its own work by the signed ceilings. The acyclicity check is a recursive walk and it runs before the ceilings are enforced, so recursion follows the submitted node count rather than maxDepth. The crate’s tests exercise three- and four-task graphs; nothing in the workspace characterises a very large one.visit_task, and the call order in validate_task_graph
BoundedA graph is not a whole bundle. verify_swarm_authority_bundle refuses a bundle with no continuation tokens or no witness chains, so a well formed graph with no delegation in it never verifies, and maxDepth: 0 is unreachable in practice.require_signed_swarm_delegation_evidence, swarm_authority_stage0_rejects_root_only_bundle_without_signed_swarm_evidence
BoundedA second, narrower check exists on the passport path, in validate_execution_lease_context. It validates the graph for one leased child hop rather than the whole graph: schema tag, expiry ordering, positive fan-out, a zero maxDepth against child depths, multi-hop witness chains requiring maxDepth of at least 2, the graph digest bound to the lease, the graph signature against a trusted runtime root, and the leased child’s own depth against maxDepth. It runs neither the per-edge depth arithmetic, the fan-out count, the one-root rule, nor the acyclicity walk.crates/platform/chio-transaction-passport/src/runtime_security/artifacts.rs
UnsupportedIssuance. Every call site of sign_swarm_task_graph in the workspace lives in a tests/ directory or in the crate’s examples/ binary, which builds and signs a demonstration bundle. chio proof fixture generate recursive-runtime-swarm looks like a counterexample and is not: it copies the checked-in signed fixture rather than minting a graph.runtime_admission.rs, swarm_authority_stage0.rs, proof_cli_contract/verify.rs, chio-swarm-authority/examples/agent_os.rs; generate_recursive_runtime_swarm_fixture

Next Steps

  • Swarm Authority · the one function that verifies this graph together with the other seven artifacts
  • Swarm Overview · the rung, the verifier entry point, and the other seven artifact roles in the bundle
  • 3-Vendor Walkthrough · one delegation carried end to end, graph included
  • Cluster Overview · the rung below, where authority is held rather than derived and nothing nests
  • Capabilities · attenuation itself, the Kernel mechanism each edge’s witness chain proves a hop obeyed
  • Federation · the standing relationship a derived authority travels across