Chio/Docs
LOGIN · JOIN

PlatformFan-Out & Fan-In

Swarm

Route Plans

One signed receipt names the egress a child task may take, and the runtime refuses a dispatch that will not name it.

Bound at one end, enforced at the other

Continuation Tokens owns the artifact that binds a route plan to a child dispatch: a token’s routePlanReceiptId is inside its signature, and the runtime denies a request whose named route plan is not the one the token names. Node Egress Contract owns what actually happens on the socket. Nothing on this page reaches that code. A route plan is a signed statement about egress, checked against a claim, not a control over bytes.

Source

This page reflects spec/schemas/chio-swarm/v1/route-plan-receipt.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.


A signed claim about egress, checked twice

A route-plan receipt is one artifact family inside a swarm authority bundle, defined as SwarmRoutePlanReceipt in crates/kernel/chio-swarm-authority/src/types.rs, schema id chio.swarm.route-plan-receipt.v1, wire form pinned by spec/schemas/chio-swarm/v1/route-plan-receipt.schema.json. Sixteen fields, all required by the schema. The claim it carries, claim.swarm.route_plan_bound, is registered enforced in spec/registries/claim-registry.v1.json. There is one receipt per routed task node, and the graph’s declared route-plan refs and the receipt set must cover each other exactly.

Two different pieces of code read it, and they check different things. chio-swarm-authority checks the artifact: shape, closed vocabularies, internal prefix agreement, expiry against the bundle clock, and a signature from a pinned witness key. That pass has no I/O and no opinion about the call in flight. chio-runtime-core’s admission hook adds the part a stateless verifier cannot do: it takes the route metadata the calling edge supplied for this dispatch and compares it, field by field, against the receipt the request named.

The two halves ship differently. verify_swarm_authority_bundle runs in product binaries today: chio proof verify calls it on the swarm arm of its local family dispatch, and chio-proof-room on a bundle rebuilt from an evidence graph, both pinning keys from CHIO_SWARM_TRUSTED_WITNESS_KEYS. The runtime half is an embedder API. ChioRuntimeAdmissionHook starts with an empty witness key vector, the chio-runtime facade’s with_swarm_witness_keys is called only by tests, and an empty slice rejects every route-plan receipt, so a default-built hook denies a swarm-bound request rather than admitting it. The facade’s core_hook always forwards whatever it holds to the core hook’s method of the same name, so an embedder that never sets the keys forwards an empty vector. Swarm Authority states that bound in full.

The receipt belongs to the Swarm rung because the key that signs it is a planner’s, not the executing node’s. The node holds a pinned public key and a stored bundle; it did not pick the route and cannot re-derive it. What it can do is refuse a dispatch that will not say where it is going.


The receipt

Every field except the signature is inside the signature. route_plan_signature_body serializes the receipt and removes the signature key before hashing, leaving the other fifteen covered, so editing any binding invalidates the receipt rather than retargeting it. That includes schema and expiresAtUnixMs.

FieldPinsChecked by the verifier
schemaThe artifact familyString equality with chio.swarm.route-plan-receipt.v1, before anything else in the receipt is read
routePlanIdIdentity within the bundleNon-empty; must be a member of the graph’s routePlanRefs; a second receipt with the same id is duplicate swarm route-plan receipt; every ref must have a receipt or the bundle is rejected
graphIdThe plan it belongs torequire_same_graph against the bundle’s task graph
taskIdThe one node it routesMust resolve in the graph node index. Separately, the continuation token naming this receipt must have the same childTaskId
selectedRouteWhich candidate wonNon-empty; the text before its first : must equal bridgeId
candidateSetDigestWhat the planner claims it chose fromLowercase 64-character hex. Nothing resolves it. See Guarantees and limits
registrySnapshotHashThe registry view it chose underLowercase 64-character hex. Nothing resolves it either
bridgeIdThe protocol familyNon-empty. It is the prefix the next three fields are checked against, and the value live metadata is compared to
protocolTargetThe endpoint the hop addressesNon-empty; the text before its first :// must equal bridgeId
egressContractIdAn egress policy, by nameNon-empty; the text before its first : must equal bridgeId. Nothing resolves the name to a contract
egressConstraintsThe egress posture assertedNon-empty, entries unique, and every entry the literal deny-private-network. Anything else is unsupported swarm route-plan egress constraint
attenuationDecisionThe planner’s verdictMust be the literal accepted. A receipt recording any other decision rejects the whole bundle
policyDigestThe policy the decision was made underLowercase 64-character hex, and nothing more here. The passport family compares it to an execution lease; this crate does not
expiresAtUnixMsA validity window with no lower boundMust be strictly greater than the bundle’s now_unix_ms, a plain unsigned bundle field the caller supplies. There is no issued-at field, so a receipt cannot be too new
issuer, signatureA pinned witness keyA did:chio: issuer must be self-certifying, exactly 64 lowercase hex after the prefix; any other issuer string is parsed as a public-key hex. The resulting key must be a member of the caller-supplied trusted slice, then the signature verifies over the canonical body. An empty slice rejects before any key is compared

Three prefixes off one bridge id

validate_route_plan_target is the reason a route plan cannot quietly name one bridge and address another. It runs the same shape three times:

crates/kernel/chio-swarm-authority/src/verifier.rsrust
let selected_bridge = route
    .selected_route
    .split_once(':')
    .map(|(bridge, _)| bridge)
    .ok_or_else(|| {
        rejected(format!(
            "swarm route-plan selected route bridge mismatch: {}",
            route.route_plan_id
        ))
    })?;
if selected_bridge != route.bridge_id {
    return Err(rejected(format!(
        "swarm route-plan selected route bridge mismatch: {}",
        route.route_plan_id
    )));
}

The positive proof-room fixture shows the four fields agreeing: bridgeId is mcp, selectedRoute is mcp:task-child-a, protocolTarget is mcp://provider-a, and egressContractId is mcp:egress-contract-task-child-a (fixtures/proof-room/swarm-authority/valid-recursive-delegation/route-child-a.json). Read the check for what it is: a prefix agreement, not a URL parse. Nothing constrains the remainder, so on a reading of split_once a selectedRoute of mcp: and a protocolTarget of mcp:// both pass with empty remainders. No test in the tree constructs that case. The verifier only refuses a receipt that contradicts itself. Two of the three prefixes do have offline negatives, swarm_authority_stage0_rejects_route_plan_selected_route_bridge_mismatch and ..._protocol_target_bridge_mismatch; nothing exercises the egressContractId prefix, so treat that third branch as read, not proved.

Where the schema is stricter, and where it is not

The pinned schema carries additionalProperties: false, mirrored in Rust by deny_unknown_fields, and it is the stricter document in exactly one place: signature is pinned to ^[0-9a-f]{128}$, the bare Ed25519 encoding, while Signature::from_hex also accepts 0x, p256:, p384:, and hybrid: forms. A receipt carrying one of those parses in Rust and fails schema validation. It matches the verifier exactly on one closed vocabulary, an enum of one holding deny-private-network under minItems: 1 and uniqueItems: true.

Everywhere else the verifier is stricter, and the gap is worth knowing before you mint from the schema alone. The schema states nothing about the three bridge prefixes, accepts any non-empty attenuationDecision where the verifier demands the literal accepted, has no clock to judge expiresAtUnixMs against, and cannot see the task graph the receipt has to belong to. Validate against the schema to reject malformed input early. Do not read a schema pass as an authorization result.


The order the receipt is checked in

validate_route_plan_receipts walks the bundle’s receipts once, running per-receipt checks in a fixed sequence and returning on the first failure. Shape comes first: schema, the id, the graph id, then the remaining non-empty and digest-format checks, then the egress constraint vocabulary, then the three prefixes, then the attenuation decision, then the policy digest, then expiry. The signature is checked after all of those. Graph membership comes last: the receipt’s taskId must resolve, its routePlanId must be a declared ref, and the id must not already be indexed. Only when the loop finishes does the closure check run the other way, requiring a receipt for every ref. That closure is against the graph’s routePlanRefs list. A node’s own routePlanRef is bound separately, by the continuation token that names the node.

That ordering means a stale or self-contradictory receipt is refused before its signature is verified, so a rejection message says what is wrong with the plan rather than what is wrong with the key. The negatives are built to be honest about it anyway: swarm_authority_stage0.rs carries six route-plan mutations, and each is re-signed with sign_route_plan_receipt before the assertion runs, so nothing in the suite passes because a signature happened to break. fixtures/proof-room/swarm-authority/negatives/route-plan-mismatch.json is the same pattern in fixture form: it names the valid route-child-a.json as its base fixture, declares case RoutePlanMismatch against claim claim.swarm.route_plan_bound, and the generated receipt carries selectedRoute of a2a:task-child-a under an unchanged bridgeId of mcp, with a fresh signature.

Three of the eleven shipped negative bundles are route-plan cases: an egress constraint outside the vocabulary, the mismatch above, and an expired receipt. Each is its own directory, so the three refusals are one loop:

swarm-authority · route-plan-negativestranscript
$ source scripts/proof-room-quickstart-env.sh
$ for n in egress-constraint-unsupported route-plan-mismatch stale-route-plan; 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: unsupported swarm route-plan egress constraint: allow-private-network
exit=1
error [urn:chio:error:cli:other]: proof verify: swarm route-plan selected route bridge mismatch: route-child-a
exit=1
error [urn:chio:error:cli:other]: proof verify: swarm route-plan receipt is stale: route-child-a
exit=1
exit 0

The first names the rejected constraint string, allow-private-network; the other two name the receipt, route-child-a. All three exit 1, and none of them mentions a key or a signature, which is the ordering property the paragraph before the loop describes: a plan that contradicts itself is refused before the signature is reached.

Offline expiry is only as good as the verifying host’s clock

now_unix_ms is a field on the bundle, not on any signed artifact, and no witness key covers it. The runtime overwrites it with the request clock before verifying. chio proof verify fills it from SystemTime::now() on the machine running the command, so a wrong clock reaches a wrong staleness verdict in either direction. The proof-room fixtures carry fixed far-future expiries (1800000061000) for that reason.

Two other artifacts have to agree with the route set for the bundle to verify at all. A continuation token binds one: validate_continuation_route requires the child node’s routePlanRef to equal the token’s routePlanReceiptId, requires that receipt to exist, and requires its taskId to equal the token’s childTaskId. A terminal graph receipt binds all of them: its routePlanReceiptIds must equal the bundle’s whole route set as sorted lists, not contain it.


Live metadata against the receipt

None of this runs unless the request asks for it. swarm_ref_from_request looks for a chioSwarm object in the governed intent’s context and returns Ok(None) when it is absent; a request that never claims a swarm binding is never route-checked. When the object is present it is all or nothing: seven evidence references are required, a missing one is missing_chio_swarm_evidence_ref, a block carrying its own trust material is request_smuggled_trust_root, and a kernel with no runtime admission hook installed denies the request with runtime_admission_hook_missing rather than dispatching it.

The comparison itself lives in crates/kernel/chio-runtime-core/src/admission_hook/swarm_authority.rs. Its order matters and is not the order you would guess. All seven evidence references are matched against the stored bundle by id and canonical digest first. Then the named route plan is located. Then live route metadata is compared against it. Only after that does verify_swarm_authority_bundle run, and only after that is the continuation looked up and its routePlanReceiptId compared to the reference the request named.

rendering
Order inside verify_swarm_authority_reference_from_store. The live-metadata comparison happens before the bundle signature pass, so a routing mismatch is refused against a receipt that has been hash-matched to the store without having had its signature checked in bundle context.
sourcecrates/kernel/chio-runtime-core/src/admission_hook/swarm_authority.rs:21-43at fe56570

That comparison opens with the refusal that matters most. Metadata is not optional and its absence is not a skip:

crates/kernel/chio-runtime-core/src/admission_hook/swarm_authority.rsrust
let Some(metadata) = route_metadata else {
    return Err(ChioRuntimeError::Rejected {
        code: "chio_swarm_authority_rejected",
        detail: format!(
            "swarm route metadata missing for route plan {}",
            route_plan.route_plan_id
        ),
    });
};
let Some(route) = metadata
    .get("route")
    .or_else(|| metadata.get("route_selection"))
    .or_else(|| metadata.get("routeSelection"))
else {
    return Err(ChioRuntimeError::Rejected {
        code: "chio_swarm_authority_rejected",
        detail: format!(
            "swarm route metadata missing routed entry for route plan {}",
            route_plan.route_plan_id
        ),
    });
};

spec/PROTOCOL.md section 6.4.2 states the same rule in one line: omitting route metadata is a denial, not a way to bypass route-plan enforcement. chio_runtime_hook_denies_swarm_missing_route_metadata_before_dispatch drives it directly, passing extra_metadata: None to a hook whose store already holds a valid bundle and whose witness keys are pinned, and asserting a denial with failure code chio_swarm_authority_rejected. All eight metadata refusal strings, and every other swarm-specific one, are listed in Swarm Denial Codes.

Inside the routed entry, three values are looked for and each is compared by exact string equality. Every candidate name is tried in order and the first one carrying a JSON string wins, so a field present with a non-string value is skipped rather than fatal.

ValueAccepted names, in orderCompared toDetail when absent / when unequal
The routed entry itselfroute, route_selection, routeSelectionn/a, it is the containermissing routed entry / n/a
Bridgebridge, bridgeId, targetProtocol, selectedTargetProtocolbridgeIdmissing bridge / bridge mismatch: expected {a}, got {b}
Protocol targetprotocolTarget, protocol_targetprotocolTargetmissing protocol target / target mismatch
Selected routeselectedRoute, selected_route, selectedRouteId, selected_route_idselectedRoutemissing selected route / selected route mismatch

All eight of those details travel under one failure code, chio_swarm_authority_rejected, which is also the code a verifier rejection produces. The denial metadata records the code, not the sentence, so an operator distinguishing a routing mismatch from a stale witness signature reads the detail string, not the classification.

On success the function does not hand back what it was given. It builds a normalized projection under the canonical names and returns that:

crates/kernel/chio-runtime-core/src/admission_hook/swarm_authority.rsrust
Ok(serde_json::json!({
    "route": {
        "bridge": bridge,
        "protocolTarget": protocol_target,
        "selectedRoute": selected_route
    }
}))

An admitted request carries that projection into its receipt as chio_runtime.verified_swarm_route_metadata. Before the tool is reached the hook re-runs the entire swarm verification, and on that second pass it feeds the recorded projection back in place of fresh live metadata. So the accepted-spellings table has a second job: the canonical names are the ones the runtime feeds itself. Two consequences are worth stating plainly. Revalidation re-proves the bundle against the request clock, and re-proves the route against what admission recorded, not against the call as it stands at dispatch. And a failure on that second pass is not a deny receipt: it becomes KernelError::Internal("runtime swarm authority revalidation failed: ..."), so the classification an operator sees for a pre-dispatch swarm failure differs from the one admission produces for the same condition.

Two of these checks are held in place structurally. cargo xtask check adapter-no-bypass parses verify_swarm_authority_reference_from_store and requires at least one call to verify_route_metadata_matches and at least one to verify_swarm_authority_bundle inside it, plus a binary comparison whose text contains both continuation.route_plan_receipt_id and reference.route_plan_receipt.evidence_id. A third contract holds the other end: the tokens "routePlanReceipt" and "routePlanReceiptSha256" must appear in swarm_ref_from_request’s evidence-reference calls. Deleting the route comparison to make something else pass fails the check by name.


What the shipped edges actually send

The metadata the hook reads is extra_metadata on the kernel call, which is caller-supplied and also lands in the receipt. Every in-tree path that populates it from a route planner goes through one helper. McpTargetExecutor in chio-mcp-edge, the function execution path in chio-openai-adapter, the execution and orchestrator paths in chio-cross-protocol, and authorize_via_kernel in chio-http-core all call route_selection_metadata, which wraps a RouteSelectionEvidence under the key route_selection. That struct serializes camelCase and carries selectedRouteId and selectedTargetProtocol, which is why both spellings are in the accepted lists.

The planner evidence has no protocol target field

RouteSelectionEvidence (crates/protocol/chio-cross-protocol/src/routing.rs) has no protocolTarget or protocol_target member at all. A swarm-bound dispatch whose only metadata is that evidence therefore denies with swarm route metadata missing protocol target after clearing the bridge check. Two further mismatches sit behind that one. Both selectedRouteId and selectedTargetProtocol are Option fields with skip_serializing_if, so an evidence block recording a deny or attenuate decision omits them and fails the bridge check first. And selectedTargetProtocol serializes a DiscoveryProtocol in snake case, so the only bridge ids it can ever satisfy are native, http, mcp, a2a, acp, and open_ai. An embedder wiring swarm admission onto one of these edges supplies the routed block itself, or widens the planner evidence. Nothing in the tree currently produces all three values from one planner.

The conformance matrix at crates/tooling/chio-conformance/tests/runtime_edges/r_t03_runtime_edge_matrix.json records seven runtime edges, each asserting admission before dispatch, a deny receipt, and a downstream dispatch count of zero. Only two set routeMetadataBound true: mcp_tools_call_route_metadata and openai_function_call. The test also requires a row claiming that binding to name a routed entrypoint, which is how the matrix stays honest about which edges carry route evidence at all.


Guarantees and limits

StatusClaimEvidence
ShippedThe offline receipt validation runs in product binaries. chio proof verify and chio-proof-room both call verify_swarm_authority_bundle with keys pinned from the environment.chio-cli/src/cli/dispatch/proof.rs local family dispatch; chio-proof-room/src/source_verifier.rs
ShippedA swarm-bound dispatch that arrives with no route metadata is denied rather than admitted with the route check skipped.The None arm of verify_route_metadata_matches; spec/PROTOCOL.md 6.4.2
ShippedBridge, protocol target, and selected route are compared by exact string equality against the signed receipt, across four, two, and four accepted spellings respectively.route_metadata_string and the three comparisons in admission_hook/swarm_authority.rs
ShippedGraph refs and receipts close over each other: an undeclared receipt, a duplicate id, and a ref with no receipt all reject the bundle.validate_route_plan_receipts
Proved by testFour routing denials land before dispatch, and each asserts the failure code only. Read against the code they cover three distinct branches: absent metadata; a bridge of a2a where the receipt pins mcp; and an absent protocol target, twice, once through a route block carrying only a bridge and once through a route_selection block whose selectedTargetProtocol matches but which has no target member.chio_runtime_hook_denies_swarm_missing_route_metadata_before_dispatch, ..._route_metadata_mismatch..., ..._route_selection_id_mismatch..., ..._incomplete_swarm_route_metadata...
Proved by testThe matching path admits and survives revalidation: a routed block equal to the receipt on all three values is admitted, and revalidate_before_dispatch re-verifies from the recorded projection without denying.chio_runtime_hook_revalidates_reserved_swarm_continuation_then_denies_replay, ..._resumable_swarm_continuation_without_replay_denial
Proved by testNaming a route plan the continuation does not name is a distinct denial: chio_swarm_authority_ref_mismatch, not chio_swarm_authority_rejected.chio_runtime_hook_denies_swarm_route_plan_not_bound_to_continuation, which swaps in a second, valid receipt from the same bundle
Proved by fixtureThree offline negatives declare claim.swarm.route_plan_bound: a selected route whose bridge prefix disagrees, an expired receipt, and an unsupported egress constraint. Each is generated from the valid receipt and re-signed, and each runs in the proof-room catalog. The claim’s proof manifest cites only the first two.negatives/route-plan-mismatch.json, negatives/stale-route-plan.json, negatives/egress-constraint-unsupported.json; manifest.swarm.route_plan_bound (four Rust tests, one positive fixture, two negative fixtures, the schema); doctor spec ids swarm_route_plan_mismatch and swarm_stale_route_plan
Not provedFive of the eight route-metadata refusal branches have no test. The four runtime routing tests reach three branches between them: absent metadata, a bridge mismatch, and an absent protocol target twice. Nothing in the tree drives missing routed entry, missing bridge, missing selected route, target mismatch, or selected route mismatch to a denial, and no test in the tree asserts on any of the eight detail strings; every routing test asserts the failure code alone.The four runtime routing tests, read against verify_route_metadata_matches
LimitThe comparison binds a claim to a receipt. It does not observe a socket, resolve DNS, or read a response. What the process then does on the wire is the node egress contract’s job, and the two are not connected in code.The hook returns a JSON projection and nothing else; Node Egress Contract
LimitcandidateSetDigest and registrySnapshotHash are validated as lowercase SHA-256 and never resolved. Nothing recomputes a candidate set or a registry snapshot, so they record what the planner says it considered, with no way to check that it did.require_sha256 is the only reader in chio-swarm-authority; the only other consumers are the passport family and test fixtures
LimitegressContractId is a bridge-prefixed string. Nothing maps it to an HttpEgressContract, and deny-private-network is the only value egressConstraints may hold, so the list conveys nothing beyond being present and well formed.validate_route_plan_egress_constraints; no reference to HttpEgressContract anywhere in the crate
LimitDispatch revalidation re-checks the route against the projection recorded at admission, not against fresh metadata, so it detects tampering with the receipt record rather than a route that changed after admission. A failure there is an internal kernel error, not a deny receipt carrying a swarm failure code.revalidate_admitted_request reads runtime.get("verified_swarm_route_metadata") and maps a rejection to KernelError::Internal
LimitThe binding is caller-declared. A request whose governed intent carries no chioSwarm object is not swarm-bound and no route plan is consulted. Route-plan enforcement constrains a delegated child that claims a swarm binding; it is not a property of every call the kernel mediates.swarm_ref_from_request returns Ok(None) on an absent context key
LimitOffline staleness is judged against an unsigned number. now_unix_ms is a bundle field no witness key covers; the runtime overwrites it with the request clock, and chio proof verify fills it from the local system clock.swarm_authority_verification_time in chio-cli; bundle.now_unix_ms = now_unix_ms in the admission hook
LimitA second validator exists and is looser. chio-transaction-passport re-validates a route-plan receipt for the passport family, binding it to an execution lease’s routePlanReceiptRef and policyDigest, but applies none of the three bridge prefix rules, neither restricts the egress-constraint vocabulary nor requires the list to be non-empty, and never compares expiresAtUnixMs to anything: within that module it appears only in the deserialized struct and the signature body. It pins the issuer through runtime trust roots rather than a witness key slice.validate_route_plan_receipt in crates/platform/chio-transaction-passport/src/runtime_security/artifacts.rs
Not wiredThe runtime comparison is an embedder API, not a deployment default. No product binary calls the chio-runtime facade’s with_swarm_witness_keys, and the only callers that hand a bundle to insert_swarm_authority_bundle are tests, so a default deployment denies a swarm-bound request for a missing bundle or an empty key slice.Builder call sites in chio-runtime-core tests only; ensure_route_plan_issuer_is_pinned rejects an empty slice
Not wiredNo in-tree route planner emits all three compared values. The two edges that bind route metadata send planner evidence with no protocol target field, so a swarm-bound dispatch through them as written denies.RouteSelectionEvidence; route_selection_metadata
UnsupportedIssuance. The crate exposes sign_swarm_route_plan_receipt, which signs a receipt you already built and refuses when the signer does not match the declared issuer, and there is no mint_swarm_route_plan_receipt at all, unlike continuation tokens and join receipts. Every call site in the repo is a test or the crate’s examples/agent_os.rs.Call sites in swarm_authority_stage0.rs and runtime_admission.rs only

Next Steps

  • Continuation Tokens · the token that binds this receipt to one child dispatch, and the id burned before the tool runs
  • Node Egress Contract · what one process does with its own socket, including the private-range denial no contract can lower
  • Swarm Authority · the whole-bundle pass this receipt is validated inside, and the pinned key slice it needs
  • Task Graphs · routePlanRefs, the node ref, and the closure both directions
  • 3-Vendor Walkthrough · one delegated scenario carried end to end
Route Plans · Chio Docs