Chio/Docs
LOGIN · JOIN

Task graphs

A plan signed before any child runs bounds how deep delegation recurses and how wide each task fans out.

Concept chio@5f63e6e

On this page

Before any child agent starts, a signed task graph fixes how deep delegation may recurse and how many tasks each parent may hand off.

Each task carries an identity, a delegation depth, and a scope hash. The running example is the signed fixture chio proof fixture generate recursive-runtime-swarm writes: one root, three children, and a join that returns their results to the root, the shape of the fixer splitting #417 across three helpers.

The graph fixes depth and fan-out before any child runs

Delegation edges carry authority outward. Joins carry results back. The verifier checks the two relationships separately.

Signed task graphChoose a view.
depth 0depth 13 of maxFanout 3maxDepth 2joinpool · 10,000 usd_minordelegatesnext task: task-rootκtask-roota1task-child-a2,500a2task-child-b2,500a3task-child-c2,500ρjoin-child-resultsmaxDepth 2 · maxFanout 3pool 10,000κtask-roota12,500a22,500a32,500ρjoin-child-resultsnext task: task-root

a1task-child-aa2task-child-ba3task-child-c

One signed graph bounds how far work can branch

task-root delegates to three depth-1 children. The graph signs maxDepth 2 and maxFanout 3, so every node stays at or below depth 2, each edge advances one level, and no parent has more than three children. The results meet at join-child-results, whose next task is task-root; a join adds no delegation edge. Task Graphs.

The signed task graph and budget pool from the valid-recursive-delegation fixture, with widths drawn to the pool’s units; the overdrawn pool is the budget-allocations-exceed-pool fixture and the two-hop chain is the verifier’s source test. Route plans, continuations, and the revocation epoch appear by name only.

task-root delegates to three depth-1 children. The graph signs maxDepth 2 and maxFanout 3, so every node stays at or below depth 2, each edge advances one level, and no parent has more than three children; the fixture itself uses one delegation level. The results meet at join-child-results, whose next task is task-root, without adding a delegation edge.

Read more in Swarm authority, which verifies this graph together with every record it names.

An application sets the ceilings on the grant it hands a coordinator

A coordinator receives its ceilings with its grant. Here the fixer gets room to split issue #417 two levels deep and three wide, on a fork of the pricing project, with a USD 2 pool every helper draws from.

planner.tsplanner.pysrc/main.rs · in main
import { Computer, USD } from "@chio-protocol/sdk";

const project = await Computer.open("quickstart");
const fixer = await project.agent("fixer");
const fork = await project.fork();
const authority = fork.grant({
  to: fixer,
  resources: { "/workspace": ["read", "write"] },
  delegation: { depth: 2, fanout: 3 },
  budget: USD(2),
  expiresIn: "20m",
});
console.log(authority.describe());
Output
grant      cap-01b3…6d20, signed by your kernel
to         fixer, delegating up to 2 deep and 3 wide
on         quickstart, fork 51c07ae
allows     /workspace         read, write
budget     USD 2.00 pool shared by all of its agents, charged at your door
expires    in 20 minutes

The signed graph carries the same ceilings as maxDepth and maxFanout. It holds one depth-0 root, uniquely identified tasks, directed delegation edges, and separately declared joins. A parented task references its continuation token, which agrees with the task’s route and budget allocation, and scope hashes let a witness bind parent and child at each edge.

A malformed graph stops at the first structural rule

verify_swarm_authority_bundle refuses an empty trusted key set, then runs validate_task_graph, then verify_task_graph_signature, then require_signed_swarm_delegation_evidence. Inside the first, once the header fields are checked, the structural rules run in a fixed order.

RuleChecked byRejection
Task ids unique, scope hashes well formedtask_indexduplicate swarm task id
Exactly one root; 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 duplicates, 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 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 comes before the signature, so a graph whose ceiling was edited without being re-signed is refused by the structural check. Changing a signed ceiling takes a fresh signature from a trusted issuer.

The structural sequence in Rust
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)?;

Each delegation edge advances exactly one level

The declared depth is a claim the edge set has to corroborate. The verifier compares it with the parent’s depth plus one.

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
    )));
}

Overflow is a rejection. Depth is a u32, so a wrap needs a parent declared at u32::MAX, and the depth-0 child it would produce is one validate_roots already refuses; the arithmetic refuses on its own as well. swarm_authority_stage0_rejects_edge_depth_bypass lowers max_depth to 1, reparents the second child under the first at depth 1, rebuilds the witness chain, and refreshes the continuation digests. It is refused with swarm task depth mismatch: understating depth fails on the arithmetic before it reaches the ceiling.

A planner cannot widen a ceiling it already signed

validate_graph_limits refuses any node whose depth exceeds max_depth, then counts edges per from_task_id and refuses the first source over max_fanout. The counter keys on the edge source, so the ceiling bounds outgoing edges: a dependency edge draws on the same allowance as a lineage edge.

The signature body, built by task_graph_signature_body, is the canonical JSON of the whole graph with the signature field removed, and both ceilings sit inside it. Each continuation token also carries graphSha256, which the verifier recomputes. swarm_authority_stage0_rejects_task_graph_tampering_after_continuations_are_resealed adds one to max_fanout and re-signs every continuation against the new digest; the bundle is still refused with swarm task graph signature invalid.

A ceiling is a commitment by whoever signed the graph. Any holder of a trusted key can sign a different graph with different ceilings, so the trusted set is the caller’s decision, upstream of everything here; Swarm authority states what an empty set costs. Together, one root, the parent-edge rule, and depth plus one make the lineage a tree, so a full tree at both ceilings grows exponentially in maxDepth. Swarm Scarcity carries the closed form, and the budget pool bounds what the graph may spend.

Joins gather results, and every other record hangs off the graph

Joins are declared in the graph and stay out of the edge set, so they take no part in depth arithmetic, fan-out accounting, the acyclicity walk, or witness coverage. validate_joins checks that join ids and parents are unique, parents and the next task resolve, the next task is not one of its own parents, and there are at least two parents. In the fixture the join names task-root, the depth-0 node, as its next task: the ceilings bound the delegation tree, and the join adds no edge they count.

  • Continuation token. Same graphId, recomputed graphSha256, and a child task in the graph. A parented node without a continuationTokenRef, or a token naming a different child, is refused.
  • Witness chain. Its (parentTaskId, childTaskId) pair is an edge, and each edge gets exactly one chain.
  • Route-plan receipt. routePlanRefs covers the receipts both ways, the node’s ref equals the token’s, and the receipt’s taskId is the token’s child.
  • Budget allocation. Every allocation’s taskId is a node, the node’s ref equals the token’s, each unit rollup sums to maxUnits, and the maxUnits values sum within the pool total. The allocation a token points at is Active.
  • Revocation epoch. No revoked task id names a node, and the graph issuer, the planner, and every witness-hop issuer are absent from the revoked subjects.
  • Terminal graph receipt. completedTaskIds equals the node set exactly, and the join and route id sets match the bundle.

In the fixture, task-child-a names continuation-child-a, route-child-a, and budget-child-a, witness-child-a covers its edge, and the graph names budget-pool-swarm-valid and revocation-epoch-swarm-valid. The terminal receipt lists all four tasks, the one join, and the three route plans, with a rollup of 7,500 active usd_minor units under the 10,000-unit pool. Join and terminal receipts are signed declarations checked against the graph; the verifier schedules and executes no task.

The prepared negatives refuse a lowered ceiling and a cycle

The max-depth-exceeded negative is the valid four-task graph with maxDepth lowered from 2 to 0, carrying the valid graph’s signature byte for byte. A coordinator that tried to widen its own ceiling after signing would produce exactly this file. Read the field from the graph the generator wrote:

The lowered ceilingbash
jq '.maxDepth' ./swarm/negatives/max-depth-exceeded/task-graph.json
Output
0

Verify it beside the graph-cycle negative. Both must be refused:

swarm-authority · graph-negativestranscript
$ chio proof verify \
  ./swarm/negatives/max-depth-exceeded \
  ./swarm/negatives/graph-cycle \
  --trust ./swarm/trust.toml --expect refuse
REFUSED   max-depth-exceeded  swarm task exceeds max depth: task-child-a
REFUSED   graph-cycle         swarm task graph cycle at task-child-a
2 of 2 refused, as expected
trust     ./swarm/trust.toml (fixture keys)
exit 0

You should see both refusals name the node, and the summary line confirm that each input was refused as expected. Each code is a slug of the verifier’s message: the collected bundle for the recursive-runtime-swarm stage records the same string under observed_failure_code, and the cycle case is recorded as proof-room.negative.swarm-task-graph-cycle-at-task-child-a. In the worked example below, you sign a branching graph and change its fan-out.

Worked Rust example: Sign a branching graph and change its limits

Sign a branching graph and change its limits

The constructor defines a root, two depth-one children and a join. Change the fan-out argument, sign the graph, and refresh each continuation's graph digest before verification.

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 task-graph -- 2

Implementation

task-graph.rsrust
use chio_swarm_authority::verify_swarm_authority_bundle;
use swarm_authority_examples::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let fanout: u32 = std::env::args()
        .nth(1)
        .unwrap_or_else(|| "2".into())
        .parse()?;
    let mut bundle = build_bundle()?;
    bundle.task_graph.max_fanout = fanout;
    // Re-sign the graph, then rebind and sign the continuations to its new digest.
    refresh_continuation_graph_digests(&mut bundle)?;
    let report = verify_swarm_authority_bundle(&bundle, &trusted_witness_keys())?;
    println!(
        "graph: {} tasks; max fanout {fanout}; {} join",
        report.task_count, report.join_count
    );
    bundle.task_graph.max_depth = 0;
    refresh_continuation_graph_digests(&mut bundle)?;
    let error = verify_swarm_authority_bundle(&bundle, &trusted_witness_keys())
        .expect_err("depth-1 children must refuse at depth 0");
    println!("depth mutation: {error}");
    Ok(())
}

Open src/bin/task-graph.rs

lib.rs · graph constructionrust
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)?;

Open src/lib.rs

Captured result and refusal

Captured executable outputtext
graph: 3 tasks; max fanout 2; 1 join
depth mutation: swarm task exceeds max depth: task-child-a

Run with fan-out 1 to reject two outgoing edges. The built-in depth mutation is re-signed so it exercises graph validation rather than an obsolete signature.

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.

Task graph fields and rules

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

schema

constRequired

Equals chio.swarm.task-graph.v1. Any other value is refused before a second field is read.

Example"chio.swarm.task-graph.v1"

graphId

stringRequired

Every bundle artifact with a graphId of its own matches it, checked by require_same_graph: continuation tokens, witness chains, route-plan receipts, join receipts, the budget pool, and terminal receipts. The revocation epoch carries no graphId and is bound by id.

rootTransactionRef

stringNon-empty

Names the transaction the graph descends from.

plannerSubject

stringNon-empty

Checked against the revocation epoch. A revoked planner refuses the whole bundle, as does a revoked graph issuer or a revoked witness-hop issuer.

issuer / signature

stringRequired

The issuer resolves to a chio_core_types::crypto::PublicKey. A did:chio: prefix is self-certifying: exactly 64 lowercase hex characters, which forces Ed25519. A bare issuer goes to PublicKey::from_hex, which also accepts the self-describing p256:, p384:, and hybrid: forms. The resolved key appears in the caller-supplied trusted set.

createdAtUnixMs / expiresAtUnixMs

integerRequired

Compared against now_unix_ms. A graph from the future and an expired graph are both refused.

maxDepth

u32Required

Ceiling on node depth. The schema (minimum: 0) and validate_task_graph both admit 0 for a root-only graph, and require_signed_swarm_delegation_evidence refuses any bundle without continuation tokens and witness chains. Every chain names an edge and every edge puts its target at parent depth plus one, so maxDepth is floored at 1 in any verified bundle. The root-only bundle fails with signed swarm delegation evidence missing, pinned by swarm_authority_stage0_rejects_root_only_bundle_without_signed_swarm_evidence.

Example2

maxFanout

u32Required, at least 1

Ceiling on outgoing edges per task. Zero is refused by the verifier and excluded by the schema (minimum: 1).

Example3

multiHopWitnessChains

boolDefault false

Feature gate. A witness chain carrying more than one hop is refused unless the graph sets it.

nodes / edges / joins

arraysRequired

The structure. validate_task_graph requires at least one node; the bundle verifier requires at least two nodes and one edge.

budgetPoolRef / revocationEpochRef

stringRequired

Equal to the bundle's pool id and epoch id exactly.

routePlanRefs

string[]Unique

Every ref has a receipt in the bundle, and every receipt in the bundle is declared here.

Node, edge, and join types

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 is a lowercase 64-character SHA-256 digest. The witness chain for an edge lands on it at both ends: the first hop declares the parent node’s hash, the last hop the child node’s. A node with a parent_task_id also carries a continuation_token_ref, and its route and budget refs agree with the ones inside that token. edge_type is non-empty and otherwise uninterpreted; every fixture uses delegates.

Schema and verifier

The verifier is the enforcement path, and spec/schemas/chio-swarm/v1/task-graph.schema.json describes the envelope. The schema allows a join with minItems: 1 parents and the verifier requires two, asserted by swarm_authority_stage0_rejects_single_parent_join. The schema expresses none of the single-root, depth-plus-one, or ceiling rules. spec/PROTOCOL.md section 6.4.2 states the two ceilings, the depth-plus-one rule, and overflow as a rejection; that text is the contract a third-party implementation is held to.

Each ceiling is local: maxDepth bounds depth per node and maxFanout bounds outgoing edges per edge source, and no field states a total. The acyclicity check, visit_task, is a recursive walk that runs before the ceilings in validate_task_graph, so its recursion follows the submitted node count.

Lease-path graph check

The passport path runs a narrower check in validate_execution_lease_context (crates/platform/chio-transaction-passport/src/runtime_security/artifacts.rs). It validates the graph for one leased child hop: 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. Per-edge depth arithmetic, the fan-out count, the one-root rule, and the acyclicity walk belong to the bundle verifier.

Tests and fixtures

  • swarm_authority_stage0_rejects_graph_cycle: a back edge from a child to the root is refused as a cycle, frozen as the graph-cycle negative.
  • crates/tooling/chio-conformance/tests/r_t03_recursive_swarm_conformance.rs: the signed positive fixture verifies, and a proptest draws 48 random cases over a six-mutation space, each asserting the expected rejection message.
  • sign_swarm_task_graph signs a graph; the crate’s examples/agent_os.rs binary builds and signs a demonstration bundle. chio proof fixture generate recursive-runtime-swarm (generate_recursive_runtime_swarm_fixture) writes the signed fixture built into the binary, and --with-negatives adds its eleven negatives.

Sources and wire contract

The wire schema is spec/schemas/chio-swarm/v1/task-graph.schema.json and the verification order is spec/PROTOCOL.md section 6.4.2 in the public Chio repository. The fixture graph is ./swarm/proof-room-bundle/task-graph.json after chio proof fixture generate recursive-runtime-swarm --out ./swarm. The Swarm Protocol reference carries every artifact in the bundle.

Next: give every edge its witness