Route Plans
Pin each delegated task to the route its planner signed, and see where the kernel compares that route with the call.
On this page
A planner can choose a child’s route on one side of a protocol bridge while another operator executes it. The route plan turns the selection into a signed record: the bridge, the protocol target, and the selected route travel together, and the kernel compares them with the call before the child’s tool runs.
Each child names its route-plan receipt through its continuation token. The examples follow route-child-a, which sends task-child-a through MCP in the recursive-delegation fixture; the network connection itself stays under the Node Egress Contract.
The kernel compares the route at admission and again from its record before dispatch
Route data reaches the checks from two places: the caller’s metadata at admission, and the route the hook recorded when it admitted the call.
bbundleeevidencerrouteκauthorityccontinuationπadmissionr′revalidate
The child is admitted, then revalidated before dispatch
The runtime loads the stored bundle for the request’s artifact references and compares their ids and digests. It compares bridge, protocol target, and selected route with the stored route plan, verifies the bundle at the request’s evaluation time, and reserves the single-use continuation id. After the remaining admission checks it records the normalized route, then revalidates that record and the reloaded bundle before dispatch. One child task, admitted.
Admission matches every artifact’s id and digest first, then compares three exact strings in order: bridge, protocol target, selected route. It then verifies the whole bundle at the request’s evaluation time and confirms that the continuation names this route plan, so a matching route alone admits nothing. The normalized route is recorded on the admission receipt, and revalidation compares that record with the stored receipt again before dispatch.
Read more in Continuation tokens, which covers the rest of the admission order.
One signed receipt binds a child to its route
The receipt has sixteen required fields, and its signature covers all of them except signature: the schema tag, every routing field, the expiry, and the issuer.
{
"schema": "chio.swarm.route-plan-receipt.v1",
"routePlanId": "route-child-a",
"graphId": "swarm-graph-proof-valid",
"taskId": "task-child-a",
"selectedRoute": "mcp:task-child-a",
"candidateSetDigest": "0a1a64dd…ab79",
"registrySnapshotHash": "e2239b78…e2c0",
"bridgeId": "mcp",
"protocolTarget": "mcp://provider-a",
"egressContractId": "mcp:egress-contract-task-child-a",
"egressConstraints": ["deny-private-network"],
"attenuationDecision": "accepted",
"policyDigest": "58d22090…b44a",
"expiresAtUnixMs": 1800000061000,
"issuer": "did:chio:43046bfe…a5de",
"signature": "f37a7968…780e"
}The receipt’s task must exist in the graph, its id must be one of the graph’s route-plan references, and the child’s continuation must name the same receipt. Pointing the child somewhere else takes a newly signed receipt and consistent evidence references. The receipt above comes from the fixture, with digests and the signature shortened.
Three prefixes keep a route on one bridge
The selected route, the protocol target, and the egress contract each start with the receipt’s bridgeId.
| Receipt value | Compared prefix | Required value |
|---|---|---|
mcp:task-child-a | Before the first : | mcp |
mcp://provider-a | Before the first :// | mcp |
mcp:egress-contract-task-child-a | Before the first : | mcp |
These are string-prefix comparisons. They require no suffix after the prefix, leave the rest of each string unparsed, resolve no provider, and load no egress contract. The deny-private-network constraint, the one value egressConstraints accepts, records what the planner required; the configured egress implementation refuses the private-network connection.
The graph, the continuation, and the terminal receipt each commit to the route set
The route stage of verify_swarm_authority_bundle checks each receipt, then its place in the bundle. The first failure ends verification.
- Task graph. Its
routePlanRefsand the supplied receipt ids cover each other exactly, and receipt ids are unique. - Continuation. The child node’s
routePlanRefequals the token’sroutePlanReceiptId, and that receipt’staskIdequals the token’schildTaskId. - Terminal receipt. Its
routePlanReceiptIds, sorted, equal the whole validated route set. - Runtime request. Its route-plan evidence id and canonical digest match the stored receipt, and its continuation names the same route plan.
To check the artifacts offline, run chio proof verify on a transaction passport that carries the swarm bundle. A proof pass covers the supplied artifacts; runtime route metadata and replay state belong to the admission path.
Read more in 3-Vendor Walkthrough, which carries the evidence workflow around it.
The prepared example signs a route and refuses a borrowed one
The downloadable Rust project signs a route-plan receipt for mcp://analysis-provider, verifies the graph, then binds the child to its sibling’s route and signs the changed continuation.
Worked Rust example: Bind a route to its task and continuation
Bind a route to its task and continuation
Supply a protocol target, sign its route-plan receipt, and verify the graph. Then bind the child to its sibling's route and sign the changed continuation to isolate the binding failure.
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 route -- mcp://analysis-providerImplementation
use chio_swarm_authority::verify_swarm_authority_bundle;
use swarm_authority_examples::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut bundle = build_bundle()?;
let route = &mut bundle.route_plan_receipts[0];
route.protocol_target = std::env::args()
.nth(1)
.unwrap_or_else(|| "mcp://analysis-provider".into());
sign_route_plan_receipt(route)?;
verify_swarm_authority_bundle(&bundle, &trusted_witness_keys())?;
println!(
"signed route: {}",
bundle.route_plan_receipts[0].protocol_target
);
bundle.continuation_tokens[0].route_plan_receipt_id = "route-child-b".into();
sign_continuation_token(&mut bundle.continuation_tokens[0])?;
println!(
"cross-task route: {}",
verify_swarm_authority_bundle(&bundle, &trusted_witness_keys())
.expect_err("a child cannot take its sibling's route")
);
Ok(())
}pub fn route_plan_receipt(
route_plan_id: &str,
task_id: &str,
bridge_id: &str,
protocol_target: &str,
) -> Result<SwarmRoutePlanReceipt, Box<dyn Error>> {
let mut receipt = SwarmRoutePlanReceipt {
schema: CHIO_SWARM_ROUTE_PLAN_RECEIPT_SCHEMA.to_string(),
route_plan_id: route_plan_id.to_string(),
graph_id: "analysis-graph".to_string(),
task_id: task_id.to_string(),
selected_route: format!("{bridge_id}:{task_id}"),
candidate_set_digest: sha256_hex(format!("candidates-{task_id}").as_bytes()),
registry_snapshot_hash: sha256_hex(b"registry-snapshot"),
bridge_id: bridge_id.to_string(),
protocol_target: protocol_target.to_string(),
egress_contract_id: format!("{bridge_id}:egress-contract-{task_id}"),
egress_constraints: vec!["deny-private-network".to_string()],
attenuation_decision: "accepted".to_string(),
policy_digest: sha256_hex(b"swarm-route-policy"),
expires_at_unix_ms: now_ms() + 60_000,
issuer: witness_issuer(),
signature: String::new(),
};
sign_route_plan_receipt(&mut receipt)?;
Ok(receipt)
}Captured result and refusal
signed route: mcp://analysis-provider
cross-task route: swarm continuation route-plan ref mismatch: continuation-child-aThe target is signed routing metadata; this program does not contact a provider. The runtime-host example compares host-supplied route metadata before a local handler runs.
Exit 0 indicates a successful example run. Expected refusals shown in the output are checked by the program; invalid command inputs exit nonzero. Keys, digests, timestamps and run-directory names vary.
You should see the signed target printed, then the cross-task binding refused with swarm continuation route-plan ref mismatch: continuation-child-a. The program contacts no provider; the signed target is routing metadata.
A refused route names the relationship that failed
Three negative fixtures each break one route rule: an unsupported egress constraint, a selected route on the wrong bridge, and an expired receipt.
$ chio proof verify \
./swarm/negatives/egress-constraint-unsupported \
./swarm/negatives/route-plan-mismatch \
./swarm/negatives/stale-route-plan \
--trust ./swarm/trust.toml --expect refuseREFUSED egress-constraint-unsupported unsupported swarm route-plan egress constraint: allow-private-network REFUSED route-plan-mismatch swarm route-plan selected route bridge mismatch: route-child-a REFUSED stale-route-plan swarm route-plan receipt is stale: route-child-a 3 of 3 refused, as expected trust ./swarm/trust.toml (fixture keys)
Each line names the negative and the rule it breaks. The summary line and exit status 0 say every input was refused, as --expect refuse required; a negative that verified would print VERIFIED and exit 1. At runtime, ordinary refusal metadata keeps chio_swarm_authority_rejected and drops the detailed string; dispatch revalidation keeps the detail inside an internal error.
A signed route records the planner’s decision; the edge and its egress controls carry it out
A verified receipt establishes that a pinned key signed these routing fields, that they agree with the graph and continuation, and that the receipt had not expired at the evaluation time the caller supplies. The operator decides which keys may sign routes, and any pinned witness key may; the operator also stores the current authorized bundle and keeps the admission state.
The candidate set, registry snapshot, and policy are carried as signed digests; reproducing the planner’s choice takes those inputs and its selection logic. The executing edge must still use the authorized destination under the node’s egress controls, and the library sets no maximum lifetime or newest-plan sequence.
Read more in Node Egress Contract, which governs the connection itself.
Route metadata, receipt fields, and diagnostics
Try protocol_target, egressConstraints, or bridge mismatch.
Route metadata at admission and revalidation
The swarm route comparison applies when the governed intent declares context.chioSwarm. The request supplies seven artifact references, and the operator supplies the stored authority bundle and the trusted witness keys; caller-supplied trust material is refused. Requests without the swarm context pass the hook’s other checks without a route comparison.
{
"route": {
"bridge": "mcp",
"protocolTarget": "mcp://provider-a",
"selectedRoute": "mcp:task-child-a"
}
}This fragment matches the fixture’s three values; it is one part of a swarm-bound request. On admission, the normalized object is stored under chio_runtime.verified_swarm_route_metadata. Dispatch revalidation reads that recorded projection, reloads the authority bundle, and checks references, routing, and bundle validity again. It compares the recorded projection with the stored receipt and takes no fresh route metadata from the call or the socket.
Metadata keys and aliases
| Receipt field | Metadata keys, in precedence order |
|---|---|
bridgeId | bridge · bridgeId · targetProtocol · selected |
protocolTarget | protocolTarget · protocol_target |
selectedRoute | selectedRoute · selected_route · selectedRouteId · selected_ |
The outer wrapper is the first present key among route, route_selection, and routeSelection, and a present malformed value does not fall through to the next wrapper. Within it, each field takes the first alias whose value is a string; conflicting aliases are not merged. Comparisons are case-sensitive string equality with no URL normalization.
Receipt fields
| Field | What verification establishes |
|---|---|
schema | Equals chio. |
routePlanId | A nonempty id, unique among receipts and present in the graph’s routePlanRefs. Every declared route-plan ref needs a receipt. |
graphId | Equals the supplied task graph’s id. |
taskId | Names a node in that graph. A continuation adds the child-task binding. |
selectedRoute | A nonempty string whose prefix before the first colon equals bridgeId. |
candidate | A lowercase 64-character SHA-256 digest, signed as carried; the candidate set itself is not recomputed here. |
registry | The same digest shape, signed as carried; the registry snapshot is not fetched. |
bridgeId | A nonempty bridge name used by three prefix checks and the runtime comparison. |
protocolTarget | A nonempty string whose prefix before the first :// equals bridgeId. |
egress | A nonempty string whose prefix before the first colon equals bridgeId. |
egress | A nonempty, duplicate-free list containing only deny- |
attenuation | Equals the literal accepted, as the planner recorded it. |
policyDigest | A lowercase SHA-256 digest covered by the signature, carried for the planner’s policy. |
expiresAtUnixMs | Strictly later than the bundle’s evaluation time. |
issuer | Parses to a public key in the verifier- |
signature | Verifies the canonical body under that issuer key. |
The issuer comes from the shared pinned-key set, and the planner and the executing node may share a signing identity. Accepted key encodings are covered on Delegation Witnesses.
Schema and verifier
The wire schema and the Rust type both refuse unknown fields. The schema limits the egress list to its one supported value and requires a bare 128-character lowercase hexadecimal signature; the Rust signature parser also accepts its prefixed, algorithm-specific encodings. The schema has no graph and no clock, so prefix agreement, trusted signatures, graph membership, and the literal accepted are verifier checks, and a schema-valid receipt can still fail verification.
Route stage order
The route stage checks shape, the egress vocabulary, the three prefixes, the accepted attenuation, the policy-digest shape, expiry, and then the trusted signature. Task membership and receipt-id coverage follow. The runtime overwrites the stored bundle’s clock with the request’s evaluation time; CLI verification uses the host clock. The fixture receipt expires at 2027-01-15T08:01:01Z, a fixed value in example data.
Route diagnostics
| Diagnostic fragment | Inspect |
|---|---|
unsupported swarm route-plan egress constraint | A value outside the sole supported constraint, deny- |
swarm route-plan selected route bridge mismatch | The selected route prefix against bridgeId. The negative fixture changes the selected route to a2a:task-child-a while keeping mcp. |
swarm route-plan receipt is stale | Expiry must be strictly after evaluation time. |
swarm route metadata bridge mismatch | The calling edge’s bridge string against the stored receipt. |
swarm route metadata missing protocol target | The route object needs protocolTarget or protocol_target, even when it already names a selected route. |
swarm route metadata selected route mismatch | The selected-route string after the bridge and target checks have passed. |
The complete metadata-message catalogue is on Swarm Denial Codes.
Calling edges
The shared planner’s RouteSelectionEvidence records selectedRouteId and selectedTargetProtocol under route_selection. It has no protocolTarget field, so that block alone cannot satisfy the swarm comparison; a swarm-bound call adds the target, matching route strings, and every authority reference.
The MCP and OpenAI adapters propagate planner metadata and accept governed intent. The HTTP helper authorize_via_kernel supplies no governed intent, so its planner-metadata path declares no swarm binding. Install the runtime admission hook on the execution path that mediates these calls, with trusted witness keys configured and the authority store populated.
Transaction-passport route check
The runtime-security passport validator binds the route receipt to an execution lease and a policy digest, checks the accepted attenuation and runtime issuer trust, and verifies its signature. Its signature-body helper replaces the receipt schema with chio.swarm.route-plan-receipt-signature.v1, while the swarm-authority helper keeps chio.swarm.route-plan-receipt.v1. The two signatures are distinct contracts, and the passport validator leaves the prefix checks, the closed egress vocabulary, and expiry to the swarm stage.
Wire contract and source
- Route, graph, continuation, and terminal-receipt verification and the sixteen-field wire schema.
- Runtime reference checks and route normalization and admission and revalidation call sites.
- The route-child-a receipt, at
./swarm/proof-room-bundle/route-child-a.jsonafterchio proof fixture generate recursive-runtime-swarm --with-negatives --out ./swarm. - The runtime-security passport signature contract.