Chio/Docs
LOGIN · JOIN

Bootstrap Federated Trust

Add a partner's card, agree on a treaty, and verify offline a receipt that both kernels signed.

Guide chio@5f63e6e

On this page

Connect your kernel to a partner's, so that work crossing between your organizations is admitted under each side's own policy and leaves receipts both kernels sign.

Adding the partner's card pins its kernel key, and a signed handshake proves the partner holds that key. A treaty then names the actions you cooperate on. Every co-signed action produces a record either operator verifies offline, with the other operator's systems out of reach.

Start
Partner cardChoose what the card does.
Your operatorTheir operatorKK′Your kernelTheir kernelYour operatorTheir operatorKK′Your kernelTheir kernel

Adding the card pins their kernel key

chio partner add partner-card.json --name partner reads the card the partner sent: its org DID, did:chio:z6MkpA7…c2T, its trust-control URL, its authority key history, its receipt-signer keys, and its status resolve URL. Adding it pins the partner's kernel key, 9b07…d31c, in your trust store. Each operator adds the other's card, so each kernel keeps its own pin. Peer pins.

The partner card as chio partner add reads it: the five fields it carries and the kernel key adding it pins. The DID and key are the Quickstart's illustrative values. Treaty terms, transport, and the conformance evidence the handshake carries are left out.Peer pins

1. Add each other's partner cards

Each operator sends the other its partner card over a channel both already trust, and each adds the card it received. To try the flow on one machine, chio partner demo runs a partner with its own kernel, keys, and policy, and writes its card.

Before you begin
  • A kernel signing keypair. Each operator holds an Ed25519 keypair that signs its receipts. The card carries its public half, so the key you pin is the key that signs cross-org receipts.
  • A stable kernel identifier. A short string such as kernel.larkfield.example names your kernel to peers. Without one, the kernel uses the hex encoding of its signing public key.
  • A trusted delivery channel for cards. A shared control plane, a signed onboarding document, or a physical handoff. The card is your trust anchor; the network never supplies one.
Terminalbash
chio partner demo --out partner-card.json
chio partner add partner-card.json --name partner
Output
partner    did:chio:z6MkpA7…c2T, named partner
kernel     9b07…d31c, pinned from its card
transport  https, mutual TLS
handshake  accepted, tier Bronze, rotation due in 12 hours

You should see the partner's DID, its kernel key pinned from the card, and an accepted handshake. The card carries the organization DID, trust-control URL, authority key history, receipt-signer keys, and status resolve URL. HTTPS with mutual TLS or signed bodies is the default transport; --transport iroh adds the optional lane on self-hosted relays.

The handshake confirms the pinned key and never introduces one. Each kernel signs a challenge that binds both kernel ids, a fresh nonce, and the time, and the receiver accepts it only when all of these hold:

  1. Signature. The Ed25519 signature verifies under the declared public key.
  2. Address. The envelope names this kernel as its addressee, and its sender is the kernel id you expected.
  3. Freshness. The timestamp sits within five minutes of the local clock.
  4. Pin. The declared key equals the key the card pinned. First contact with no card stops with MissingTrustAnchor, and any other key with UnexpectedPeerKey.
  5. Tier. The peer's conformance tier meets your floor, Bronze by default.

An accepted handshake pins the peer until its rotation deadline, twelve hours later by default. Past the deadline the peer resolves as PeerStale until the kernels handshake again; schedule the next one at about half the window. A partner's new key reaches you only through a new card or the authority key history the card carries.

Read more in Peer handshake.

2. Agree on what crosses

Open the partner by the name you gave its card and propose a treaty. The treaty names the actions the two operators cooperate on, and each kernel computes the intersection of both governance ladders on its own, so the stricter requirement wins.

partners.tspartners.pysrc/main.rs · in main
import { Computer } from "@chio-protocol/sdk";

const project = await Computer.open("quickstart");
const partner = await Computer.connect("partner");
const treaty = await project.treaty({
  partner: partner,
  actions: ["workspace.read", "workspace.write", "model.invoke", "merge"],
  expiresIn: "7d",
});
console.log(treaty.describe());
Output
treaty           trt-01aa…4c19, signed by both kernels
participants     you · partner (did:chio:z6MkpA7…c2T)
action           you       them      admitted as
workspace.read   allow     allow     either kernel admits under its own policy
workspace.write  allow     allow     either kernel admits under its own policy
model.invoke     allow     allow     charged at your door
merge            co-sign   co-sign   both kernels sign
expires          in 7 days

You should see the treaty signed by both kernels, with merge marked co-sign. Every action is admitted by the kernel that serves it, under its own policy. A grant issued under the treaty is bound to the holder's key, and your kernel requires a DPoP proof at its door before it admits a call under that grant.

Read more in Treaties and Proof of possession.

3. Run one action both kernels sign

Any action the treaty marks co-sign ends in one receipt both kernels sign. A merge of a fork the partner's agents worked is the common case.

partners.tspartners.pysrc/main.rs · in main
// fork: a fork the partner's agents worked under the treaty
const merged = await project.apply(fork.changes, { treaty });
console.log(merged.receipt.describe());
Output
merge      main 2f60a91 → 5b93d10
receipt    rcpt-01ab…f2d0, co-signed by your kernel and theirs

You should see one receipt co-signed by your kernel and theirs. The kernel that hosts the action signs its receipt and, while the origin kernel is pinned fresh, sends it to the origin's co-signer for the second signature. The exchange crosses over HTTPS, or over the iroh co-sign lane when you chose it.

The co-signed record is a DSSE envelope over an in-toto statement of the bilateral invocation: the request and outcome hashes, the ordered signer kernel ids, and, under a treaty, a treaty binding reference. The hosting kernel needs a durable receipt store and the treaty's runtime admission context; without either it refuses before dispatch.

4. Verify the co-signed receipt offline

Export the history and verify it against the two pinned kernel keys and nothing else.

Terminalbash
chio evidence verify --input audit --partner partner
Output
verified   12 receipts from 2 kernels, offline

You should see every receipt verified, the co-signed merge among them; the count is illustrative. The bilateral verifier accepts the envelope only when both signatures validate under the pinned keys, and it binds each signature to a key fingerprint. Swapping either key fails with OrgASignatureInvalid or OrgBSignatureInvalid. A receipt the partner signed before a rotation still verifies through the authority key history its card carries.

Read more in Bilateral receipts.

A refused crossing names the missing pin or co-signer

Two messages cover the common cases. A peer that is missing or past its rotation deadline produces named federation peer <id> is not pinned fresh. A pinned peer with no co-signer installed produces federation cosigner missing for request <id> bound to origin kernel <id>.

Alert on not pinned fresh and federation cosigner missing; neither message contains the word stale. A peer that fails to resolve becomes a signed Verdict::Deny written to the receipt log like any other refusal, and a failed co-signature surfaces as KernelError::Internal. Neither path returns an allow the peer did not co-sign.

Refusals from the hosting kernel

A request carrying federated_origin_kernel_id is refused before dispatch when the hosting kernel lacks a durable receipt store, with federated receipt persistence unavailable: no durable receipt store configured. Without the treaty-bound admission context it reads chio treaty-bound runtime admission context missing, and the co-signer refuses to mint an envelope with federation runtime treaty material missing; refusing treaty-bound DSSE. A co-signer alone is not enough for the call to succeed.

Embedding the trust exchange in a Rust host

Run a complete local handshake

Download the federation guide project, extract it, and run the handshake binary. It checks the expected peer key, addressed signatures, a locally consumed nonce, and the unknown-key and old-envelope refusals. Install Rust 1.94.1, Git, and a C/C++ toolchain; Cargo selects the project's pinned public dependencies. Its nonce state lasts for one process.

Terminalbash
cd federation-guide
cargo run --locked --bin handshake

Anchor the peer

The exchange object is defined in the chio-federation crate, with handshake schema chio.federation-kernel-handshake.v1 and co-signed receipt schema chio.federation-dual-signed-receipt.v1. Bind a KernelTrustExchange to your local keypair and pre-seed the expected remote key with with_trusted_peer.

src/federation.rsrust
use chio_core_types::crypto::{Keypair, PublicKey};
use chio_federation::trust_establishment::KernelTrustExchange;

// Larkfield side.
let local_keypair = load_local_kernel_keypair()?;
let halden_public_key: PublicKey = load_remote_anchor("halden")?;

let exchange = KernelTrustExchange::new(
    "kernel.larkfield.example",
    local_keypair,
)
.with_trusted_peer("kernel.halden.example", halden_public_key);

The exchange owns an InMemoryPeerStore by default, which lasts for one process. Pass any type implementing FederationPeerStore to .with_store(Box::new(my_store)) so pinned peers survive restarts. The exchange signs and checks envelopes; your host carries them over its transport.

Freshness window and clock skew

src/federation.rsrust
use chio_federation::trust_establishment::{
    KernelTrustExchange, KernelTrustExchangeConfig,
};

let exchange = KernelTrustExchange::new("kernel.larkfield.example", local_keypair)
    .with_config(KernelTrustExchangeConfig {
        // Default: 12 * 60 * 60 (twelve hours).
        rotation_window_secs: 12 * 60 * 60,
        // Default: 5 * 60 (five minutes).
        max_handshake_skew_secs: 5 * 60,
    })
    .with_trusted_peer("kernel.halden.example", halden_public_key);

The defaults, DEFAULT_ROTATION_WINDOW_SECS (12 hours) and DEFAULT_HANDSHAKE_MAX_SKEW_SECS (5 minutes), give a twelve-hour pin lifetime and refuse envelopes whose timestamps differ from the local clock by more than five minutes.

Handshake envelope

Each side builds a PeerHandshakeEnvelope addressed to its counterpart. It wraps a signed HandshakeChallenge that binds the two kernel ids, a fresh nonce, and the current timestamp, signed with the local kernel key. Both kernels do this in parallel and each processes the envelope it receives.

src/federation.rsrust
use std::time::{SystemTime, UNIX_EPOCH};

let now = SystemTime::now()
    .duration_since(UNIX_EPOCH)
    .map(|d| d.as_secs())
    .unwrap_or(0);

// Build our signed envelope addressed to the remote kernel.
let local_envelope = exchange.local_envelope(
    "kernel.halden.example",        // remote_kernel_id
    "nonce-2026-04-21-01", // caller-supplied nonce
    now,
)?;

// Serialize and send it over the authenticated transport between
// trust control planes (HTTPS with mutual TLS, or signed bodies).
let wire = serde_json::to_vec(&local_envelope)?;
transport.send("kernel.halden.example", &wire).await?;

The nonce is caller-supplied, and the transport or host consumes each challenge once inside the skew window. Any value unique across retries in that window works; UUID v7 or a counter plus a random suffix both do. Serialized, the envelope has three fields, and the challenge renders in camelCase because both structs carry rename_all = "camelCase":

  • challenge.schema: chio.federation-kernel-handshake.v1.
  • challenge.localKernelId and challenge.remoteKernelId: the directed pair for this envelope.
  • challenge.nonce and challenge.timestamp: freshness material, checked on the accepting side.
  • challenge.capabilities, challenge.conformanceTier, and challenge.ladderManifestRef: omitted from the wire at their default, so a Bronze handshake carries no conformanceTier key.
  • declaredPublicKey: the public half the signer claims, bare lowercase hex.
  • signature: Ed25519 over the canonical JSON of the challenge.

The handshake needs no second host. Build two exchanges in one process, anchor each to the other's key, pin a single now, and hand each envelope to the other. The crate's own test does exactly that, and it is the quickest place to find a mismatched anchor:

crates/trust/chio-federation/tests/trust_establishment.rs18-54rust
fn handshake_succeeds_and_pins_both_sides() {
    let kp_a = Keypair::generate();
    let kp_b = Keypair::generate();
    let now: u64 = 1_800_000_000;

    let exchange_a = KernelTrustExchange::new("kernel.org-a", kp_a.clone())
        .with_trusted_peer("kernel.org-b", kp_b.public_key());
    let exchange_b = KernelTrustExchange::new("kernel.org-b", kp_b.clone())
        .with_trusted_peer("kernel.org-a", kp_a.public_key());

    // Each side builds its own signed envelope.
    let envelope_a = exchange_a
        .local_envelope("kernel.org-b", "nonce-a", now)
        .unwrap();
    let envelope_b = exchange_b
        .local_envelope("kernel.org-a", "nonce-b", now)
        .unwrap();

    // Each side verifies and pins the remote.
    let peer_b = exchange_a
        .accept_envelope(&envelope_b, "kernel.org-b", now)
        .unwrap();
    let peer_a = exchange_b
        .accept_envelope(&envelope_a, "kernel.org-a", now)
        .unwrap();

    assert_eq!(peer_b.kernel_id, "kernel.org-b");
    assert_eq!(peer_a.kernel_id, "kernel.org-a");
    assert_eq!(peer_b.conformance_tier, ConformanceTier::Bronze);
    assert_eq!(peer_a.conformance_tier, ConformanceTier::Bronze);
    assert!(peer_b.rotation_due > now);
    assert!(peer_a.rotation_due > now);

    // Resolve while fresh succeeds.
    let resolved = exchange_a.resolve("kernel.org-b", now + 60).unwrap();
    assert_eq!(resolved.public_key.to_hex(), kp_b.public_key().to_hex());
}

Accept and pin

Call accept_envelope with the kernel id you expected the envelope from. It runs the signature, address, kernel id, skew, pin, and tier checks before pinning, so there is no confused deputy and no silent identity swap.

src/federation.rsrust
let remote_envelope: PeerHandshakeEnvelope = serde_json::from_slice(&incoming)?;

let pinned_peer = exchange.accept_envelope(
    &remote_envelope,
    "kernel.halden.example", // expected_remote_kernel_id
    now,
)?;

println!(
    "pinned {} until {} (window = {}s)",
    pinned_peer.kernel_id,
    pinned_peer.rotation_due,
    exchange.rotation_window_secs(),
);

On success it writes a FederationPeer to the peer store with established_at = now and rotation_due = now + rotation_window_secs; every failure raises a typed PeerHandshakeError. Refreshing a pin is another handshake: past rotation_due, resolve returns PeerStale. Hand the pinned peers to your running kernel so cross-org requests resolve against them:

src/federation.rsrust
use chio_kernel::ChioKernel;

let peers = exchange.peers()?; // Vec<FederationPeer>
let kernel = ChioKernel::new(config)
    .with_federation_peers(peers);

kernel.set_federation_local_kernel_id("kernel.larkfield.example");

Conformance tiers and the quorum floor

The tier is ordered Gold > Silver > Bronze, so a floor check is an ordinary comparison that fails closed. The signed challenge carries the tier and the peer record pins it. ConformanceEvidence carries threat_coverage_bps, mutation_kill_bps, and kani_trust_boundary_crates, and derive_tier() maps them to a stable band.

src/federation.rsrust
use chio_federation::trust_establishment::{ConformanceEvidence, ConformanceTier};

let evidence = ConformanceEvidence {
    threat_coverage_bps: 10_000, // 100%
    mutation_kill_bps: 8_200,    // 82%
    kani_trust_boundary_crates: 9,
};
let tier: ConformanceTier = evidence.derive_tier()?; // ConformanceTier::Gold

Advertise your tier with with_conformance_tier; the default is ConformanceTier::Bronze, skipped on serialization so the accepting side reads the same default back.

src/federation.rsrust
let exchange = KernelTrustExchange::new("kernel.larkfield.example", local_keypair)
    .with_conformance_tier(ConformanceTier::Gold)
    .with_trusted_peer("kernel.halden.example", halden_public_key);

The accepting side gates on a QuorumPolicy. accept_envelope delegates to accept_envelope_with_policy with QuorumPolicy::default(), whose min_tier is Bronze. Call the policy-aware variant to require more.

src/federation.rsrust
use chio_federation::trust_establishment::QuorumPolicy;

// Refuse any peer below Silver.
let policy = QuorumPolicy { min_tier: ConformanceTier::Silver };

let pinned_peer = exchange.accept_envelope_with_policy(
    &remote_envelope,
    "kernel.halden.example",
    now,
    &policy,
)?;

A peer below the floor raises PeerHandshakeError::ConformanceTierBelowMinimum with the kernel_id, the peer's actual tier, and the policy's minimum. The pinned peer records the tier of its most recent accepted handshake, and a later handshake may record a lower tier as long as it clears the floor in force at that moment. An optional LadderManifestRef, attached with with_ladder_manifest_ref, pins a governance-ladder manifest by manifest_id, sha256, and a validity window (issued_at_unix_ms, expires_at_unix_ms); it is validated on accept and travels with the pinned peer.

Co-signer and the federated request

Wire the tool-host kernel with a BilateralCoSigningProtocol implementation. On the iroh lane the networked co-signer is IrohBilateralCoSigner from chio-federation-transport-iroh, and a DirectoryGate resolves the authenticated EndpointId to a kernel_id through an issuer-signed transport directory before any co-signing handler runs. InProcessCoSigner serves single-host tests and integration environments.

src/federation.rsrust
use std::sync::Arc;
use chio_federation::bilateral::InProcessCoSigner;

// set_federation_cosigner takes &mut self, so the kernel binding is mutable.
let mut kernel = kernel;

// On the tool-host (Halden) kernel, install a cosigner that reaches the
// origin (Larkfield) kernel. InProcessCoSigner serves single-host tests.
kernel.set_federation_cosigner(Arc::new(InProcessCoSigner::new(
    "kernel.larkfield.example",             // origin_kernel_id
    origin_keypair.clone(),     // single-host tests only
    tool_host_public_key,       // origin verifies Halden's sig before co-signing
)));

The kernel's ToolCallRequest carries an optional federated_origin_kernel_id. When it is set and the peer is pinned fresh, the post-sign hook sends the local receipt to the co-signer and assembles two records keyed by the receipt id: the DSSE envelope through bilateral_dsse::sign_chio_bilateral_dsse_envelope_with_cosigner, and a DualSignedReceipt through co_sign_with_origin.

src/federation.rsrust
let mut request = build_tool_call_request(/* ... */);
request.federated_origin_kernel_id = Some("kernel.larkfield.example".to_string());

let response = kernel.evaluate_tool_call_blocking(&request)?;
assert_eq!(response.verdict, Verdict::Allow);

// The DSSE envelope is the authorization and audit record for the
// bilateral invocation. Verify it with the strict Chio bilateral verifier
// against both pinned peer keys (org A = origin, org B = tool host).
let envelope = kernel
    .federation_dsse_envelope(&response.receipt.id)
    .expect("federated call must produce a DSSE envelope");

let statement = chio_federation::bilateral_dsse::verify_chio_bilateral_dsse_envelope(
    &envelope,
    &origin_public_key,
    &tool_host_public_key,
)?;

// The same call records a DualSignedReceipt keyed by the same receipt id,
// with detached signatures. Use the DSSE verifier above for authorization
// and audit.
let dual = kernel
    .dual_signed_receipt(&response.receipt.id)
    .expect("federated call also produces a dual-signed receipt");

verify_chio_bilateral_dsse_envelope returns the decoded DsseStatement only when both signatures validate against the declared kernel ids.

Dual-signed receipt

A DualSignedReceipt holds the original ChioReceipt untouched plus two detached signatures over the canonical CoSigningBody: org_a_signature from the origin kernel and org_b_signature from the tool-host kernel. The base receipt still verifies alone. DualSignedReceipt::verify requires both signatures but reads the peer ids from the record it checks, so at a trust boundary use verify_pinned, where the verifier supplies the ids it expects. The DSSE envelope is the record to verify for authorization and audit.

The trust-establishment suite

The crate's suite runs the whole handshake in one process; its eighteen cases are the handshake checks, one test per refusal.

federation-handshake · suitetranscript
$ cargo test --release -p chio-federation --test trust_establishment \
    -- --test-threads 1

running 18 tests
test accept_rejects_clock_skew ... ok
test accept_rejects_tampered_signature ... ok
test accept_rejects_untrusted_first_contact ... ok
test accept_rejects_wrong_addressee ... ok
test aggregate_budget_negotiation_requires_both_peers ... ok
test conformance_evidence_derives_tiers_from_thresholds ... ok
test conformance_evidence_rejects_impossible_percentages ... ok
test conformance_tier_is_signed_and_pinned_at_handshake ... ok
test cumulative_approval_negotiation_requires_both_peers ... ok
test default_handshake_omits_v1_compatibility_fields ... ok
test explicit_t1_capabilities_are_signed_when_requested ... ok
test freshness_rotation_reissues_pin ... ok
test handshake_succeeds_and_pins_both_sides ... ok
test ladder_manifest_ref_is_signed_and_pinned_when_requested ... ok
test quorum_policy_rejects_peer_below_min_tier ... ok
test resolve_unknown_peer_fails_closed ... ok
test stale_peer_is_rejected_fail_closed ... ok
test untrusted_peer_cannot_probe_quorum_tier_floor ... ok

test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
exit 0in Chio source checkout

handshake_succeeds_and_pins_both_sides is the accepted path; the four accept_rejects_* cases are the signature, addressee, first-contact, and skew checks; stale_peer_is_rejected_fail_closed and resolve_unknown_peer_fails_closed are the two resolve refusals; default_handshake_omits_v1_compatibility_fields is why a Bronze envelope carries no tier key.

Smoke checks for an embedded kernel

  • Peer snapshot. kernel.federation_peers_snapshot() returns the pinned peer with a rotation_due in the future.
  • Fresh lookup. On the tool-host kernel the peer that gates a federated call is the origin, so check the id you put in federated_origin_kernel_id: kernel.federation_peer("kernel.larkfield.example", now) returns Some(_) while fresh and None past the deadline.
  • Federated tool call. With federated_origin_kernel_id set, the verdict follows local policy and federation_dsse_envelope(&receipt.id) returns a DsseEnvelope, with dual_signed_receipt(&receipt.id) beside it.
  • Mutual verification. Send the DSSE envelope to the other side and verify it there with verify_chio_bilateral_dsse_envelope against that side's copy of both pinned keys.
  • Fail-closed check. Repeat the call on a kernel with no pin for the origin. The response is a signed, persisted Verdict::Deny, not an Err, whose reason contains named federation peer kernel.larkfield.example is not pinned fresh. An allow here means the federation is misconfigured.

Handshake and co-signing errors

Handshake errors are typed PeerHandshakeError, every variant fail-closed; co-signing errors are typed BilateralCoSigningError.

crates/trust/chio-federation/src/trust_establishment.rs462-528rust
/// Errors raised by the trust-establishment primitives. Every variant is
/// fail-closed: callers MUST refuse to pin a peer when any step fails.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum PeerHandshakeError {
    #[error("unsupported handshake schema: {0}")]
    UnsupportedSchema(String),

    #[error("canonical JSON encoding failed: {0}")]
    CanonicalJson(String),

    #[error("handshake signing failed: {0}")]
    SigningFailed(String),

    #[error("remote handshake signature is invalid")]
    InvalidSignature,

    #[error("capability negotiation failed closed: {0}")]
    CapabilityNegotiation(String),

    #[error("remote envelope is addressed to kernel_id {addressed_to} but we are {actual}")]
    AddressMismatch {
        addressed_to: String,
        actual: String,
    },

    #[error("remote envelope declares self as kernel_id {declared} but we expected {expected}")]
    KernelIdMismatch { declared: String, expected: String },

    #[error("remote envelope timestamp {envelope} drifts from local clock {local} beyond {skew}s")]
    ClockSkewExceeded {
        envelope: u64,
        local: u64,
        skew: u64,
    },

    #[error("peer {0} is not pinned; run a handshake before resolving")]
    PeerNotPinned(String),

    #[error("peer {0} is stale and must be re-handshaked before use")]
    PeerStale(String),

    #[error("peer {0} is not trusted for first contact; configure a trust anchor before accepting handshakes")]
    MissingTrustAnchor(String),

    #[error("peer {kernel_id} declared unexpected public key; expected {expected}, got {actual}")]
    UnexpectedPeerKey {
        kernel_id: String,
        expected: String,
        actual: String,
    },

    #[error("peer {kernel_id} conformance tier {actual:?} is below required {minimum:?}")]
    ConformanceTierBelowMinimum {
        kernel_id: String,
        actual: ConformanceTier,
        minimum: ConformanceTier,
    },

    #[error("invalid conformance evidence: {0}")]
    InvalidConformanceEvidence(String),

    #[error("invalid ladder manifest reference: {0}")]
    InvalidLadderManifestRef(String),

    #[error("trust store is poisoned and cannot service requests")]
    StorePoisoned,
}
ErrorMeaningRepair
MissingTrustAnchor
First contact with no anchor or prior pin
Add the partner's card, or call with_trusted_peer, before accept_envelope
UnexpectedPeerKey
The remote declared a key other than the anchor or pin
Confirm the partner's rotation through its key history or a new card, or refuse
InvalidSignature
The signature fails under the declared key
Tampered or truncated transport; re-request the envelope and never retry a partial automatically
AddressMismatch
The envelope is addressed to another kernel
Check local_kernel_id on both sides
KernelIdMismatch
The declared sender id differs from the expected one
Confirm both sides use the same peer id
ClockSkewExceeded
The timestamp drifts beyond max_handshake_skew_secs
Synchronize NTP on both hosts; keep the skew window as configured
PeerStale
KernelTrustExchange::resolve found a pin past its rotation_due
Handshake again for a later deadline
PeerNotPinned
resolve was asked for a peer the store lacks
Complete the handshake, or refuse the inbound call at the edge. The kernel's own miss is a deny reading is not pinned fresh, which covers missing and stale alike
OrgASignatureInvalid
The origin kernel's signature failed
Confirm the pinned origin key matches the key the origin signs with
OrgBSignatureInvalid
The tool-host signature failed, or a forged body was offered for co-signing
InProcessCoSigner refuses this; for networked co-signers, confirm the tool-host key the origin holds
UnsupportedSchema
The schema is not chio.federation-kernel-handshake.v1
Upgrade the lagging side; the v1 schema is frozen and mismatches fail closed

Next: give the partner's agents a grant