Chio/Docs
LOGIN · JOIN

Cross-Org Admission

See where a partner's call is admitted at your door, what the treaty check reads, and which refusal each failure produces.

Concept chio@5f63e6e

On this page

Another organization's agents call your tools through your kernel, which decides each call under records your side already holds. The partner runs its own kernel and keys; your runtime admission hook checks the treaty a call names against the stored scope and ladder intersection.

Your partner can present a request against the agreement, but it cannot supply a replacement trust root or mark its own evidence verified. The running example is your kernel and your partner's, independently governed, under the pricing-repair-v1 treaty: the partner's fixer-2 may read issue #417 freely, while pushing a fix to the pricing repository requires verified receipt lineage.

Your kernel admits a partner's call under records it already holds

A federated call arrives with a grant, a DPoP proof bound to that grant, and a treaty reference. Federated grants require DPoP at your door, so a copied grant alone carries no authority.

Possession at the doorChoose what arrives.
Their operatorYour operatoraK′Kdρtheir agenttheir kernelyour kerneltoolTheir operatorYour operatoraK′Kdρagenttheir kernelyour kerneltool

κgrantσpossessiongguardsτtreatyBbudget

The proof binds the grant to this call

Their agent presents the federated grant with a DPoP proof signed by the key the grant names. Your kernel verifies the grant, compares each of the proof's bindings with the request it received, then runs your guards, the treaty check, and your budget. The proof's nonce is consumed at dispatch, and your kernel signs the receipt. Proof of possession.

One federated call at your kernel's door: the grant and DPoP proof their agent presents, the proof's five bindings, and the checks your kernel runs under its own policy, in order. Scope runs inside the grant check. Output checks after dispatch and the durable replay profile are left out.

Your kernel verifies the grant and compares the proof's bindings with the received request, then runs the guards, the treaty check, and the budget; the proof's nonce is consumed at dispatch, and your kernel signs the receipt. The treaty check runs after the matched capability's pre-invocation guards and before the call is charged against budget or dispatched.

The hook loads the treaty scope and ladder intersection the request names from its own verifier-owned store, requires the presented digests to match, and evaluates them. The same store-backed check runs again, non-consuming, against a fresh snapshot immediately before dispatch.

In Rust: where the hook sits

ChioRuntimeAdmissionHook implements chio_kernel::RuntimeAdmissionHook. The kernel calls it for every ToolCallRequest through evaluate, or through evaluate_operation_owned when the hook is installed with an operation-owned binding, synchronously between the guards and the budget charge (async_evaluation_core.rs:517,564). The treaty check is evaluate_cross_boundary_admission (chio-runtime-core/src/treaty.rs:485), a separate implementation from the one the operator's CLI uses, with its own chio_treaty_* failure codes. The CLI's swarm process host installs the hook at startup (process_host/swarm.rs:320-324).

One report records what the treaty check saw

evaluate_cross_boundary_admission returns one CrossBoundaryAdmissionReport, schema chio.federation.cross-boundary-admission-report.v1.

crates/kernel/chio-runtime-core/src/types.rs284–305rust
pub struct CrossBoundaryAdmissionReport {
    pub schema: String,
    pub treaty_id: String,
    pub action_class_id: String,
    pub accepted: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub failure_code: Option<String>,
    pub mode: String,
    pub consistency_model: String,
    pub co_sign: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub co_sign_quorum: Option<GovernanceLadderQuorum>,
    pub required_evidence: Vec<String>,
    pub present_evidence: Vec<String>,
    #[serde(default)]
    pub verified_evidence: Vec<CrossBoundaryEvidenceRef>,
    pub treaty_scope_sha256: String,
    pub ladder_intersection_sha256: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub expected_ladder_intersection_sha256: Option<String>,
    pub checks: Vec<String>,
}

The report names the treaty and action class it checked and carries an accepted boolean. It records the mode, consistency model, and co-sign requirement of the matched action class, lists the evidence classes the action requires beside what was present and what verified, and pins the treaty scope and ladder intersection hashes the decision used. A rejection is a well-formed report with accepted set to false and a failure_code naming one reason.

On a rejection the hook keeps only that failure_code, which becomes the receipt's chio_runtime.failure_code; the report itself reaches neither the caller nor the receipt. A treaty scope, ladder intersection, or continuation missing from the store, or carrying a different digest, refuses with its own code before any report is built. The treaty CLI's separate implementation writes a report under the same schema to the path its --report flag names.

Evidence counts only after your verifier checks it

Each action class in the intersection lists the evidence it requires. A partner presents evidence names; the verified references come from the verifiers and stores your host configures for lineage, continuation, and bilateral evidence, and verified: true written into request JSON stands in for none of them.

In the example, read-issue requires no evidence and push-fix requires receipt_lineage. Omitting the name gives chio_treaty_missing_required_evidence; naming it without a verified reference gives chio_treaty_unverified_required_evidence.

Treaty records come from whoever builds the kernel. An embedder installs the hook with set_runtime_admission_hook and writes each scope and intersection into its store with insert_treaty_runtime_artifact; chio runtime run-loopback does the same inside one process for each scenario step that names an origin kernel. The CLI's swarm process host installs the hook in operation-owned mode for swarm authority, with admission and swarm bundles and no treaty records, so a treaty reference there ends as chio_treaty_missing_scope.

A local treaty comparison admits your partner's issue read and refuses five changed conditions

The receiving owner keeps the exact treaty, ladder intersection, and peer keys apart from the incoming request. reviewed_admission compares that retained agreement and the peer pins, then passes verifier-produced references to Chio's runtime evaluator. The synthetic hashes and local keypairs make a bounded policy comparison, with no peer signature attested and no effect dispatched. Download the complete source.

The complete comparison in Rust
security-host/treaty_boundary_cases.rsrust
use chio_core_types::{Keypair, PublicKey};
use chio_runtime_core::{
    evaluate_cross_boundary_admission, ladder_intersection_sha256, treaty_scope_sha256,
    CrossBoundaryAdmissionInput, CrossBoundaryAdmissionReport,
    CrossBoundaryEvidenceRef, LadderIntersection, LadderIntersectionActionClass, TreatyScope,
    CHIO_FEDERATION_LADDER_INTERSECTION_SCHEMA, CHIO_FEDERATION_TREATY_SCOPE_SCHEMA,
};
use std::collections::BTreeMap;

/// Owner-reviewed records and peer pins are independent of incoming evidence.
/// The caller must obtain verified references from the real evidence verifier.
pub fn reviewed_admission(
    scope: &TreatyScope,
    intersection: &LadderIntersection,
    expected_scope_sha256: &str,
    expected_intersection_sha256: &str,
    peer_pins: &BTreeMap<String, PublicKey>,
    action: &str,
    present: Vec<String>,
    verified: Vec<CrossBoundaryEvidenceRef>,
    now: u64,
) -> Result<CrossBoundaryAdmissionReport, Box<dyn std::error::Error>> {
    if treaty_scope_sha256(scope)? != expected_scope_sha256 {
        return Err("treaty differs from independently retained agreement".into());
    }
    for (id, key) in scope.participant_kernel_ids.iter().zip(&scope.participant_public_keys) {
        if peer_pins.get(id) != Some(key) {
            return Err("participant is not independently pinned".into());
        }
    }
    Ok(evaluate_cross_boundary_admission(CrossBoundaryAdmissionInput {
        treaty_scope: scope,
        ladder_intersection: intersection,
        expected_ladder_intersection_sha256: Some(expected_intersection_sha256.to_owned()),
        action_class_id: action,
        present_evidence: present,
        verified_evidence: verified,
        now_unix_ms: now,
    })?)
}

/// Structural local policy fixture: no peer signatures or live dispatch.
pub fn compare_treaty_boundaries() -> Result<(), Box<dyn std::error::Error>> {
    let owner = Keypair::generate();
    let peer = Keypair::generate();
    let participants = vec!["larkfield-kernel".to_owned(), "halden-kernel".to_owned()];
    let manifests = vec!["11".repeat(32), "22".repeat(32)];
    let scope = TreatyScope {
        schema: CHIO_FEDERATION_TREATY_SCOPE_SCHEMA.to_owned(),
        treaty_id: "pricing-repair-v1".to_owned(),
        participant_kernel_ids: participants.clone(),
        participant_public_keys: vec![owner.public_key(), peer.public_key()],
        ladder_manifest_sha256s: manifests.clone(),
        allowed_action_classes: vec!["read-issue".to_owned(), "push-fix".to_owned()],
        issued_at_unix_ms: 1_000, expires_at_unix_ms: 5_000,
        revocation_epoch_sha256: "33".repeat(32), trust_bundle_sha256: "44".repeat(32),
    };
    let action = |id: &str, evidence: Vec<String>| LadderIntersectionActionClass {
        action_class_id: id.to_owned(), mode: "guarded".to_owned(), destructive: false,
        consistency_model: "single_kernel".to_owned(), co_sign: "none".to_owned(),
        co_sign_quorum: None, evidence_required: evidence,
        participant_modes: participants.iter().map(|id| (id.clone(), "guarded".to_owned())).collect(),
    };
    let intersection = LadderIntersection {
        schema: CHIO_FEDERATION_LADDER_INTERSECTION_SCHEMA.to_owned(),
        intersection_id: "pricing-repair-intersection-1".to_owned(), treaty_id: scope.treaty_id.clone(),
        participant_kernel_ids: participants.clone(), ladder_manifest_sha256s: manifests,
        generated_at_unix_ms: 1_000, expires_at_unix_ms: 5_000,
        action_classes: vec![action("read-issue", vec![]), action("push-fix", vec!["receipt_lineage".to_owned()])],
    };
    let pins: BTreeMap<_, _> = participants.into_iter().zip(scope.participant_public_keys.clone()).collect();
    let scope_pin = treaty_scope_sha256(&scope)?;
    let intersection_pin = ladder_intersection_sha256(&intersection)?;
    let evaluate = |candidate: &LadderIntersection, action: &str, now, present| {
        reviewed_admission(&scope, candidate, &scope_pin, &intersection_pin, &pins, action, present, vec![], now)
    };
    assert!(evaluate(&intersection, "read-issue", 2_000, vec![])?.accepted);
    let mut wrong_pins = pins.clone();
    wrong_pins.insert("halden-kernel".to_owned(), Keypair::generate().public_key());
    assert!(reviewed_admission(&scope, &intersection, &scope_pin, &intersection_pin, &wrong_pins,
                              "read-issue", vec![], vec![], 2_000).is_err());
    let mut changed_scope = scope.clone();
    changed_scope.treaty_id = "halden-general-services".to_owned();
    assert!(reviewed_admission(&changed_scope, &intersection, &scope_pin, &intersection_pin, &pins,
                              "read-issue", vec![], vec![], 2_000).is_err());
    let mut changed = intersection.clone();
    changed.action_classes[0].mode = "observation".to_owned();
    assert_eq!(evaluate(&changed, "read-issue", 2_000, vec![])?.failure_code.as_deref(),
               Some("chio_treaty_intersection_mismatch"));
    assert_eq!(evaluate(&intersection, "read-issue", 5_000, vec![])?.failure_code.as_deref(), Some("chio_treaty_stale"));
    assert_eq!(evaluate(&intersection, "push-fix", 2_000, vec![])?.failure_code.as_deref(),
               Some("chio_treaty_missing_required_evidence"));
    assert_eq!(evaluate(&intersection, "push-fix", 2_000, vec!["receipt_lineage".to_owned()])?.failure_code.as_deref(),
               Some("chio_treaty_unverified_required_evidence"));
    println!("ACCEPT local read-issue policy; no peer signature or dispatch claim");
    println!("REFUSE wrong peer pin, changed agreement/intersection and expiry");
    println!("REFUSE missing or merely named push-fix evidence");
    Ok(())
}
Executed local treaty comparisontext
ACCEPT local read-issue policy; no peer signature or dispatch claim
REFUSE wrong peer pin, changed agreement/intersection and expiry
REFUSE missing or merely named push-fix evidence

You should see one acceptance and two refusal lines. Each refusal has its own repair:

  • A peer key that differs from the pin. The owner rejects the unpinned participant; review the authorized peer-pin lifecycle.
  • A treaty that differs from the retained digest. The owner rejects the changed agreement; reconcile the exact jointly reviewed original.
  • A changed intersection mode. chio_treaty_intersection_mismatch; keep the expected commitment and never recompute it from the changed request.
  • Evaluation at either expiry. chio_treaty_stale; renew under the agreement's authority owners.
  • A fix push with only an evidence name. chio_treaty_unverified_required_evidence; verify the bound artifact through the configured evidence verifier.

Treaty refusals end the call before dispatch in a signed denial

A treaty rejection at admission returns Ok(ToolCallResponse { verdict: Deny, reason: Some("chio treaty-bound runtime admission denied"), receipt, .. }), with the specific code only in the signed receipt's chio_runtime.failure_code. A check that passed at admission and fails at the pre-dispatch revalidation raises KernelError::Internal, which the kernel signs into a Deny whose reason carries the code inline.

SurfaceReceipt chio_runtime.failure_codeCause
Admission Deny
chio_treaty_missing_scope, chio_treaty_scope_hash_mismatch, chio_treaty_missing_intersection
The named scope or intersection is absent from the hook's store, its digest differs, or its trust bundle hash differs from the admission bundle's
Admission Deny
chio_treaty_stale
Evaluation falls before the scope or intersection began, or at or after either expiry
Admission Deny
chio_treaty_intersection_mismatch
The stored intersection's digest differs, or scope and intersection disagree on treaty id, ladder manifests, or participants
Admission Deny
chio_treaty_action_class_not_allowed
The action class is outside the scope's allowed classes or the intersection
Admission Deny
chio_treaty_missing_continuation
The action needs lineage or bilateral evidence, and the request names no continuation or one the store lacks
Admission Deny
chio_treaty_missing_required_evidence
A required evidence class is absent from the presented evidence
Admission Deny
chio_treaty_unverified_required_evidence
A required evidence class has no verified reference, or its reference is marked unverified
"chio treaty-bound runtime admission context missing"
missing_chio_treaty_context
A request with a federated origin carries no treaty reference
"chio treaty-bound runtime admission reference invalid"
The reference's own code, such as invalid_chio_treaty_context
The treaty reference is malformed or incomplete
"internal error: runtime treaty revalidation failed: runtime admission rejected: <code>: <detail>"
None; the code is in the reason
A check that passed at admission fails at the pre-dispatch revalidation

Read chio_runtime.failure_code before choosing a repair. Missing records belong to the host's treaty provisioner, stale records need an authorized renewal, and mismatched digests, participants, or evidence need review against the independently retained agreement. The continuation, lineage, and bilateral evidence checks refuse under the same reason with their own chio_treaty_* codes.

Source reference: refusal sites
  • Store checks. treaty_evidence.rs:29,38,46,55 for the scope and intersection records, treaty_evidence.rs:64 for the stored intersection digest, and treaty_evidence.rs:96,222-225 for the continuation.
  • Evaluator. chio-runtime-core/treaty.rs: staleness at line 505, intersection agreement at 519 and 539, action classes at 553 and 567, missing evidence at 587, unverified evidence at 612.
  • Context. admission_hook/preparation.rs:37-43,192-198 for a missing reference and :201-205 for an invalid one.
  • Responses. The admission Deny is built at deny_responses.rs:521-529 and its metadata at admission_hook/metadata.rs:1-8; the revalidation error is raised at admission_hook.rs:542-546 and signed at async_evaluation_core.rs:1192-1214.

Offline treaty tooling tests an agreement before a kernel relies on it

chio-federation ships its own evaluate_cross_boundary_admission (treaty.rs:361), an independent implementation with its own chio_federation_treaty_* codes that nothing on the hook's path reaches. Its one production caller is chio federation treaty admit (chio-cli/src/cli/chio/dispatch/treaty.rs:75), which reads a treaty scope and ladder intersection from operator-supplied files and takes each --evidence pair as a verified reference, an operator assertion. intersect computes an intersection from local inputs, admit evaluates one action class against it, and verify-packet checks a buyer-attestation packet against artifacts given on the command line.

A rejected admit shows its code twice: in the --report JSON's failureCode field and in the exit error, "Chio treaty admission rejected request: <code>". An offline acceptance report installs no records in a running hook.

Report failureCodeCause
chio_federation_treaty_stale
Evaluation falls before the scope or intersection began, or at or after either expiry (chio-federation/treaty.rs:381)
chio_federation_treaty_intersection_mismatch
The digest differs from --expected-ladder-intersection-sha256, or scope and intersection disagree on treaty id, manifests, or participants (:395,415)
chio_federation_treaty_action_class_not_allowed
The action class is outside the scope's allowed classes or the intersection (:429,443)
chio_federation_treaty_missing_required_evidence
A required evidence class is absent from the --evidence pairs (:463)

A separately invoked check confirms that both tool servers behind a bilateral action attested to it. verify_chio_bilateral_invocation (bilateral_verifier/cosign.rs:12) checks a DSSE envelope of predicate type chio.bilateral-cosign-invocation.v1 against local trust material only: a pinned peer set, receipt store, lease registry, governance receipts, revocation oracle, and action-class table. An unpinned peer fails closed, as step_8_unpinned_peer_fails_closed (bilateral_verifier/tests.rs:1310) exercises. Callers reach the check through the buyer-package verifier, which wraps its error as ChioPackageError::Federation (chio-attest-buyer-core/src/report.rs:266-272), so verify_package_report returns a rejected report (report.rs:352-372).

RefusalCause
Rejected report, failure.code "federation", failure.detail "federation verification failed: peer.unpinned_or_keyid_mismatch: <detail>"
The peer's kernel identity is not in the pinned peer set, or its declared key differs from the pinned passport (VerifierError::PeerUnpinnedOrKeyidMismatch, bilateral_verifier/error.rs:40)

An unpinned peer needs an authorized peer-pin update: adding the partner's card is what pins its kernel key. Treaties and admission covers how an operator composes the scope and intersection this tooling checks.

The iroh lane refuses unadmitted endpoints before any handler runs

Cross-organization traffic travels over HTTPS with mTLS or signed bodies by default. The optional iroh lane, run on self-hosted relays, adds an accept-time gate: DirectoryGate (admission.rs:46) implements iroh's after_handshake hook (:143), which fires after iroh authenticates the remote endpoint and before any ProtocolHandler::accept runs.

The gate resolves that endpoint against a transport directory verified at load and closes the connection, before reading any application data, when the endpoint is not bound to an admitted, non-removed kernel id. The CLI mounts it in front of the relay's directed-batch signals lane when an operator runs chio pheromone relay serve --iroh-enable; the iroh transport ships in the default chio build. With the flag off, the relay's HTTP path is byte-for-byte unchanged, and tool calls never pass this gate, because the admission hook decides them.

RefusalCause
QUIC close, application code 403, reason "not admitted"
The endpoint is not bound to an admitted, non-removed kernel in the verified transport directory (AfterHandshakeOutcome::Reject, admission.rs:31,131)

Read more in Iroh transport, Transport directory, and Federation.

Next: bind a federated grant to its holder