Delegation Witnesses
At every hand-off you can check that a child task received a narrower scope than its parent, under a key you trust.
On this page
Each hand-off in a delegation carries its own signed proof that the child’s scope is a subset of the parent’s, so a receiving operator can check the derivation one hop at a time.
A witness chain is one record in the swarm authority bundle. It covers the derivation of scope; routes, pooled budgets, revocation, and continuation admission have their own checks in the same bundle.
Every delegation edge carries exactly one witness chain
A chain names the graph, the parent task, and the child task, then carries one or more signed hops. Each hop holds the parent and child scope text, their hashes, capability digests, an expiry, and an issuer.
a1task-child-aa2task-child-ba3task-child-c
A chain hands authority across an interior scope
In the two-hop example, commerce/reserve_budget narrows from three invocations at scope A to two at the interior scope B, then one at C. The second hop starts from the scope hash and capability digest D that the first hop handed over, and each hop signs both scopes with the graph and chain identity. Delegation Witnesses.
The verifier anchors the first parent scope to the parent graph node and the last child scope to the child node. Between them, each hop starts from the scope hash and capability digest the previous hop handed over. In the two-hop test example, commerce/reserve_budget narrows from three invocations at scope A to two at the interior scope B, then one at C. A and C are graph-node anchors; B is an interior scope with the same capability digest D on both sides, and it adds no third node to the graph.
Each hop’s signature covers its complete signed body, including the chain and graph identity. The in-token delegation chain is a separate contract with a one-link limit; both paths apply the same scope-subset rules.
One hop narrows three invocations to one, and the verifier recomputes the subset
The fixture witness-child-b.json is a single hop from task-root to task-child-b. Both scopes name server commerce, tool reserve_budget, and operation invoke; max_invocations drops from 3 to 1. The chain is anchored at the root’s scope hash 07bedb… and the child’s 582bb5….
The hop carries both normalized JSON strings in scopeSubsetProof, with a declared relation that child tool grant 0 is a subset of parent tool grant 0. validate_attenuation_proof rebuilds that relation from the text:
- Hash. It hashes the exact supplied parent and child strings and compares them with the hop’s scope hashes.
- Declared relations. It refuses any declared relation whose
subsetis false. - Marker. It validates any cumulative-approval marker.
- Subset. It parses both scopes and checks
child.is_subset_of(parent), then checks that the marker agrees with the child scope.
Each child tool, resource, or prompt grant is covered by a parent grant. For a tool grant, coverage spans the target, operations, invocation and monetary caps, required constraints, and the DPoP requirement. Coverage is per grant; Swarm Budgets debits the shared pool across sibling tasks.
Read more in Delegation & Attenuation, which defines the underlying subset rules.
Graph depth and witness length are separate limits
multiHopWitnessChains belongs to the signed task graph. A chain with more than one hop is refused when that field is false, and changing it takes a new valid graph signature.
Once enabled, the permission sets no maximum hop count. The graph’s maxDepth limits task-node depth, and interior witness scopes on one edge sit outside that count. The transaction-passport claim path also requires maxDepth of at least two when multi-hop is enabled, and that check leaves chain length open as well.
The trusted key set decides who may sign a hop
The hop’s canonical signature body uses chio.swarm.delegation-witness-hop-signature.v1. It includes graphId, chainId, both task ids, and all nine hop fields other than the signature, which binds the hop to its named edge and chain. The hop’s index and the chain length are unsigned; scope and capability continuity fix the order.
An issuer of the form did:chio:<64 lowercase hex characters> identifies an Ed25519 public key directly, with no DID-document resolution. PublicKey::from_hex parses the other accepted encodings, including the p256, p384, and hybrid forms. The parsed key must be in trusted_witness_issuer_keys, the same set that pins the graph, route and join receipts, revocation epoch, terminal receipts, and continuation tokens; a key pinned as a witness can sign any of those classes.
Revocation compares the raw issuer string. A did:chio issuer and the same public key as bare hex are different strings, so publish revocations in the exact representation the issuer uses. Continuation Tokens explains the remaining admission bindings.
The prepared fixture stops at the child anchor, and a missing key list stops sooner
Both runs read the negatives that chio proof fixture generate recursive-runtime-swarm --with-negatives --out ./swarm writes. The first passes the fixture’s trust file and reaches the child-scope anchor check.
$ chio proof verify ./swarm/negatives/witness-child-scope-mismatch \
--trust ./swarm/trust.tomlREFUSED witness-child-scope-mismatch swarm witness child scope mismatch: witness-child-a trust ./swarm/trust.toml (fixture keys)
The second runs with no key pinned for swarm.witness. The CLI loads the bundle, then stops before any check with trust:role-unpinned, a different refusal from the library’s empty-slice error, trusted swarm witness keys missing.
$ chio proof verify ./swarm/negatives/witness-child-scope-mismatcherror[CHIO-TRUST-ROLE-UNPINNED]: no key is pinned for swarm.witness, so the witness chain cannot be checked help: pass --trust ./swarm/trust.toml, or pin the witness key with chio trust pin swarm.witness <KEY> code: urn:chio:error:trust:role-unpinned docs: https://chio.computer/docs/reference/errors#error-trust-role-unpinned
You should see swarm witness child scope mismatch: witness-child-a with exit status 1 on the first: a result about the bundle. The second exits 2, an error about your configuration, and its help line names the fix. The worked example issues a narrowing witness from Rust.
Worked Rust example: Carry a signed narrowing witness
Carry a signed narrowing witness
Give a child one invocation from a parent scope allowing three. The complete constructor binds the witness to the graph edge, signs its hop, and binds the chain digest into the continuation.
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 witnessImplementation
use chio_core_types::capability::attenuation::compute_attenuation_witness;
use chio_swarm_authority::verify_swarm_authority_bundle;
use swarm_authority_examples::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let parent = scope_for("commerce", "reserve_budget", 3);
let child = scope_for("commerce", "reserve_budget", 1);
let proof = compute_attenuation_witness(&parent, &child)?;
println!("narrowing witness: {}", serde_json::to_string(&proof)?);
let bundle = build_bundle()?;
let report = verify_swarm_authority_bundle(&bundle, &trusted_witness_keys())?;
assert!(report
.hop_reports
.iter()
.all(|hop| hop.attenuation_verified));
println!("signed hops verified: {}", report.hop_reports.len());
let wider = scope_for("commerce", "reserve_budget", 4);
println!(
"widening: {}",
compute_attenuation_witness(&parent, &wider).expect_err("child cannot increase quota")
);
Ok(())
}pub fn scope_for(server_id: &str, tool_name: &str, max_invocations: u32) -> ChioScope {
ChioScope {
grants: vec![ToolGrant {
server_id: server_id.to_string(),
tool_name: tool_name.to_string(),
operations: vec![Operation::Invoke],
constraints: Vec::new(),
max_invocations: Some(max_invocations),
max_cost_per_invocation: None,
max_total_cost: None,
dpop_required: None,
}],
..ChioScope::default()
}
}pub fn witness_chain(
chain_id: &str,
parent_task_id: &str,
child_task_id: &str,
parent_scope_hash: &str,
child_scope_hash: &str,
scope_subset_proof: chio_core_types::capability::attenuation::AttenuationWitness,
) -> Result<SwarmDelegationWitnessChain, Box<dyn Error>> {
let mut chain = SwarmDelegationWitnessChain {
schema: CHIO_SWARM_DELEGATION_WITNESS_CHAIN_SCHEMA.to_string(),
chain_id: chain_id.to_string(),
graph_id: "analysis-graph".to_string(),
parent_task_id: parent_task_id.to_string(),
child_task_id: child_task_id.to_string(),
hops: vec![SwarmDelegationWitnessHop {
parent_capability_digest: sha256_hex(b"parent-capability"),
child_capability_digest: sha256_hex(child_task_id.as_bytes()),
parent_scope_hash: parent_scope_hash.to_string(),
child_scope_hash: child_scope_hash.to_string(),
attenuation_rule_id: "rule-subset-tool-invocation".to_string(),
scope_subset_proof,
expires_at_unix_ms: now_ms() + 60_000,
issuer: witness_issuer(),
policy_digest: sha256_hex(b"swarm-policy"),
witness_signature: String::new(),
}],
};
sign_witness_chain(&mut chain)?;
Ok(chain)
}pub fn sign_witness_chain(chain: &mut SwarmDelegationWitnessChain) -> Result<(), Box<dyn Error>> {
let keypair = witness_keypair();
for index in 0..chain.hops.len() {
let signature = sign_swarm_delegation_witness_hop(chain, &chain.hops[index], &keypair)?;
chain.hops[index].witness_signature = signature;
}
Ok(())
}Captured result and refusal
narrowing witness: {"normalizedParentScope":"{\"grants\":[{\"max_invocations\":3,\"operations\":[\"invoke\"],\"server_id\":\"commerce\",\"tool_name\":\"reserve_budget\"}]}","normalizedChildScope":"{\"grants\":[{\"max_invocations\":1,\"operations\":[\"invoke\"],\"server_id\":\"commerce\",\"tool_name\":\"reserve_budget\"}]}","subsetRelations":[{"grantKind":"tool","childIndex":0,"parentIndex":0,"subset":true}]}
signed hops verified: 2
widening: attenuation violation: child scope is not a subset of parent scopeThe final call asks for four invocations and must fail before a witness can be issued. A signature cannot make a wider scope an attenuation.
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 broken hop refuses the whole bundle at the first failing contract
Witness checking runs inside verify_swarm_authority_bundle, after the other bundle checks, continuation bindings included. Each chain is checked in this order:
- The schema, a non-empty chain id, a matching graph id, and a parent and child pair that exists as a graph edge.
- At least one hop, with the first parent scope and last child scope anchored to their graph nodes.
- The graph’s signed multi-hop permission, when the chain has more than one hop.
- For each hop, field shapes, expiry, the scope-subset proof, issuer trust, and signature, then its input hashes against the preceding hop’s output hashes.
- A second chain for the same edge is refused; after all chains, so is any graph edge left without one.
The verifier returns its first error, never a partial success report, so a malformed continuation binding can stop the bundle before its chain reaches this stage. Each diagnostic names the relationship that failed, and most append the chain id; the diagnostic lookup lists them.
A valid witness proves the scope narrowed at every hop
The verifier is a pure library. The caller supplies the bundle, the evaluation time, and the trusted keys, and it reads neither a clock nor an environment variable. On the serving path, the runtime admission hook loads the bundle from its store and runs the same verification before dispatch.
Capability and policy digests are checked for shape, continuity, and inclusion in the signature; the owning paths resolve and enforce the tokens and policies they name. What a child task did afterwards is recorded in its own receipts, within the mediation boundary.
Witness hop fields and diagnostics
Search for scope discontinuity, policyDigest, or witnessChainSha256.
The wire schema is chio.swarm.delegation-witness-chain.v1. Chain and hop types reject unknown fields. The JSON Schema describes the envelope and leaves scopeSubsetProof as an object, so narrowing is established by the verifier.
Fixture scope comparison
| Scope field | Parent | Child |
|---|---|---|
server_id | commerce | commerce |
tool_name | reserve_budget | reserve_budget |
operations | invoke | invoke |
max_invocations | 3 | 1 |
parentCapabilityDigest / childCapabilityDigest
Lowercase SHA-256 hex shape and continuity between adjacent hops. The witness verifier treats them as digests and leaves token resolution to the capability path.
parentScopeHash / childScopeHash
Hex shape, endpoint anchors, hop continuity, and hashes recomputed from the supplied scope strings.
attenuationRuleId
Carried and signed as an identifier. Narrowing is decided by recomputation from the scope text.
scopeSubsetProof
Scope hashes, parsed scope narrowing, declared non-subset refusals, and cumulative-approval marker preservation. The relation list never substitutes for recomputation. The witness's restrictedPredicates and aggregateBudget fields are signed, and the witness-hop validation path reads neither; the cumulative-approval marker is checked and preserved.
expiresAtUnixMs
Strictly after the caller-supplied bundle time.
issuer
A parseable public key that is a member of the caller's trusted key set.
policyDigest
Lowercase SHA-256 hex shape and inclusion in the signature. The policy itself is resolved and executed by the policy path.
witnessSignature
A valid signature over the canonical hop body, including graph, chain, and task identity.
Continuation and revocation bindings
- Continuation with a parent task. Carries
witnessChainRefandwitnessChainSha256; the referenced chain matches both task ids and the token’s canonical chain digest. - Continuation without a parent task. Carries neither witness-chain field.
- Bundle evidence. The full authority verifier refuses an empty witness-chain or continuation-token list.
- Revoked hop issuer. A match in the supplied epoch’s
revokedSubjectsrefuses the entire bundle.
Diagnostic fragments
Each fragment names the relationship to inspect. Most append the chain id. They are verifier diagnostics, distinct from the stable CLI registry codes.
missing swarm delegation witness chain: a graph edge with no chain.duplicate swarm delegation witness chain: more than one chain covering the same edge.swarm witness chain edge mismatch: the parent and child pair against the graph’s edges.swarm witness parent scope mismatch: the first hop’s parent hash against the parent node.swarm witness child scope mismatch: the last hop’s child hash against the child node.swarm multi-hop witness chain feature gate missing: the signed graph’smultiHopWitnessChainsfield.swarm witness hop scope discontinuity: the preceding child scope hash and the current parent scope hash.swarm witness hop capability discontinuity: the corresponding capability digests at that junction.swarm witness hop is stale: expiry againstbundle.now_unix_ms.swarm attenuation witness invalid: scope text and hashes, parsed subset rules, declared relations, and cumulative-approval markers.swarm witness issuer is not trusted: the parsed issuer key against the caller’s pins.swarm witness signature invalid: signature encoding and the complete canonical signed body.trusted swarm witness keys missing: an empty key slice supplied directly to the verifier.
Issuing a witness
An issuer builds normalized scopes, keeps chain continuity, and signs each hop with a trusted key. The worked example’s witness_chain and sign_witness_chain constructors bind the witness to its graph edge, sign the hop, and bind the chain digest into the continuation. A request for a wider scope fails before a witness exists: attenuation violation: child scope is not a subset of parent scope.
What the source tests exercise
The suite covers child-anchor mismatch, a broken continuation-to-chain binding, ungated and enabled two-hop chains, a tampered signature, and disconnected hops. The enabled two-hop test checks a reported witnessHopCount of two.
The generated widening test runs 32 cases. It re-signs a graph and chain with a widened child hash and keeps the proof for the permitted scope; the expected attenuation refusal shows that signed but inconsistent scope text fails. Its coverage is the generated widening space.
Sources and wire contract
- Witness-chain verifier and signature body
- Scope proof validation and grant subset rules
- Delegation witness wire schema, and the complete one-hop witness at
./swarm/proof-room-bundle/witness-child-b.json - Authority verifier tests and two-hop fixture construction