Chio/Docs
LOGIN · JOIN

PlatformFleet Operations

Cluster

Capability Leases

Three signed action classes, a receipt bound to one destructive step, and a validation library that mints nothing and stores nothing.

This is not the Governance Ladder

Governance Ladder is a different artifact with a colliding name. The ladder is a per-action-class manifest pinned at federation handshake: it declares which classes are destructive, which need a governance receipt, and which consistency model applies. Its schema lives in the Chio spec; what a crate actually validates at handshake is a LadderManifestRef, an id, a digest, and a window, not the manifest body. It also uses narrow_destructive as a partition_fallback.lease_kind string, which is a manifest field value and not the lease on this page. Read the ladder for which classes require a receipt. Read this page for what a lease and a receipt actually are, which checks run against them, and which of those checks the library declines to perform.

Three families, one crate, validation only

crates/trust/chio-governance defines three artifact families: capability leases (src/lease.rs), destructive-action governance receipts (src/authorization.rs), and the generic governance charters and cases that attach to a chio-listing identity (src/generic.rs, src/evaluation.rs). Its own architecture record states the posture plainly: no I/O, no runtime state, #![forbid(unsafe_code)], and every public function either verifies an artifact the caller already holds or builds an unsigned body for the caller to sign. It does not mint capability tokens, run guard pipelines, persist receipts, or fetch registry data.

The charter and case half of that crate is documented end to end in Listing Disputes: the four case kinds, the six lifecycle states, the effective-state mapping, and all sixteen finding codes. This page owns the other two families, plus the operator-side question neither of those covers: which authority signs these artifacts, which routes the control plane exposes, and what happens to a result once it is returned.

This is Cluster content because the issuing authority is one you run. An AuthorityProfileDocument names its lease authorities and governance authorities with their validity windows and public keys; the matching private seeds sit in a local signing-keys file on the same host. Every governance charter the control plane mints is stamped with one governing_operator_id, the node’s normalized --advertise-url. The moment these artifacts are checked by a party you do not run, the mechanism is Swarm content: Bilateral Co-Sign resolves a lease reference against a registry at step 14 and a governance receipt at step 15, and 3-Vendor Walkthrough runs both against a fixture workflow.


Four objects, three of them signed

ObjectSchema constantSigned byDefined in
CapabilityLeaseArtifactchio.capability-lease.v1A lease authority named in the authority profile, keyed by its issuer stringchio-governance/src/lease.rs
GovernanceReceiptArtifactchio.governance-receipt.v1A governance authority, keyed by its authorizing_kernel stringchio-governance/src/authorization.rs
LeaseScopeBindingArtifactchio.federation.lease-scope-binding.v1Nobody. It travels unsigned beside the leasechio-attest-buyer-core/src/claims.rs
GenericGovernanceCharterArtifact and GenericGovernanceCaseArtifactchio.registry.governance-charter.v1, chio.registry.governance-case.v1The governing operator, with the control plane’s own authority keypair on the issue routeschio-governance/src/generic.rs

The four signed types in that table (lease, receipt, charter, case) are all aliases of SignedExportEnvelope<T>: a body, the signer’s public key, and a signature over the canonical JSON of the body. That shape decides most of what follows, because verify_signature verifies against the key carried inside the envelope. A passing signature proves the body was not altered after signing. It proves nothing about who signed.

crates/trust/chio-governance/src/lease.rsrust
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CapabilityLeaseActionClass {
    ScopedObservation,
    DelegatedAction,
    NarrowDestructive,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CapabilityLeaseArtifact {
    pub schema: String,
    pub lease_id: String,
    pub issuer: String,
    pub subject: String,
    pub scope_digest: String,
    pub action_class: CapabilityLeaseActionClass,
    pub issued_at_unix_ms: u64,
    pub expires_at_unix_ms: u64,
}

The action class is the whole ladder the lease carries. There is no fourth class, no numeric level, and no ordering relation defined anywhere in the crate: the classes are compared by equality against an allowlist, never by rank. Wire form is snake_case (scoped_observation, delegated_action, narrow_destructive) inside a camelCase body.

scope_digest is where the lease says what it is for, and the crate deliberately cannot read it. The digest is a canonical-JSON SHA-256 over a thirteen-field preimage held in chio-attest-buyer-core: lease id, workflow id, workflow grant id, step index, tool name, peer kernel id, action class id, subject, action class, tool args hash, destructive flag, and the two timestamps. The schema field is not in the preimage. To chio-governance the digest is an opaque 64-character string it compares for equality against one the caller supplies.

crates/trust/chio-governance/src/authorization.rs17-27rust
pub struct GovernanceReceiptArtifact {
    pub schema: String,
    pub receipt_id: String,
    pub authorizing_kernel: String,
    pub case_kind: GovernanceReceiptCaseKind,
    pub authorized_lease_id: String,
    pub workflow_id: String,
    pub step_sha256: String,
    pub issued_at_unix_ms: u64,
    pub expires_at_unix_ms: u64,
}

GovernanceReceiptCaseKind has exactly one variant today, DestructiveAuthorization. It is still checked against an authority’s allowed_case_kinds allowlist, so the extension point is wired even though the set is a singleton. That check is entirely caller-side. No function in chio-governance reads case_kind, and none reads a lease’s action_class either: both are validated as enum variants on deserialization and then compared to an allowlist by whoever holds the authority profile.


Issuance binds the windows together

Leases and receipts are issued offline by chio-federation-authority, not by the control plane. One command, chio federation authority issue --profile P --request R --signing-keys K --out-dir D, reads three JSON documents and writes five: the issuance bundle plus the leases, the scope bindings, the receipts, and the verification context split out for convenience. One request names exactly one lease authority and one governance authority, and every step in it is signed by that pair. Issuing under two lease authorities means two runs.

The profile is validated before a single signature is produced. Every lease and governance authority must carry a key_id equal to the id derived from its own public key, a validity window with a strictly positive span, an explicit status, and a non-empty allowlist (allowed_action_classes for a lease authority, allowed_case_kinds for a governance authority). Runtime policy issuer keys must be distinct from every lease, governance, and revocation authority key, which is the one role-separation rule the profile enforces by construction. Duplicate issuers and duplicate authorizing kernels are rejected, and all four required sections (BBS issuers, lease authorities, governance authorities, runtime policy issuers) must be non-empty. Each named authority must then be Active at issue time, and its seed must derive the public key the profile publishes.

Two allowlist gates follow. Before any step is processed, the governance authority must list DestructiveAuthorization in allowed_case_kinds, even for a request with no destructive step at all. Then, per step, the lease authority must list that step’s action class in allowed_action_classes.

Per step, issuance is a chain of interval nesting. The lease window must sit inside the lease authority’s window. A destructive step must declare NarrowDestructive, and its governance receipt window must sit inside both the governance authority’s window and the lease’s own window. A non-destructive step that carries any governance receipt field at all is rejected outright, which is the symmetric half of the rule most systems only enforce in one direction. Duplicate lease ids and duplicate step indices are rejected at the request level before any of this runs.

The scope binding is built first, its digest computed, and the digest embedded in the lease body before signing. The binding itself is never signed, and it does not need to be: altering any of its thirteen fields changes the digest, and the digest is inside a signed lease. A verifier that receives both recomputes the digest and compares it to the lease. verify_lease_scope_bindings does that and considerably more: it requires one binding per lease with no duplicates, then cross-checks eleven of the thirteen fields against the workflow receipt step, the tool receipt, the step class binding, and the lease body before recomputing anything (lease id and step index serve as lookup keys rather than compared values). That function lives in chio-attest-buyer-core and runs on the proof-package path only. Nothing in chio-governance knows the binding exists.


Three entry points, three different strictnesses

FunctionChecksDoes not check
verify_capability_leaseSchema constant, non-empty lease_id / issuer / subject, digest shape, window ordering, signature, not-yet-valid, expired, and an exact scope_digest match when one is suppliedWhether the issuer is anyone in particular, whether the key is trusted, whether the action class is one that issuer may sign, whether the lease was ever revoked
verify_destructive_authorizationEverything the receipt body validates, plus signature, window, and equality against an expected lease id, workflow id, and step hashThe lease itself. It compares an id string; it never sees the lease artifact. It also ignores case_kind
verify_step_governance_boundaryThat a step marked destructive carries a receipt at all, and that the receipt validates, is unexpired, is signed, and is not future-datedLease id, workflow id, and step hash. It is strictly weaker than the function above

The scope-digest parameter is an Option, so passing None verifies a lease as a bearer credential with no binding to a step. Every non-test caller in the tree passes Some.

One correction to the crate’s architecture record, because the code is the authority: it states that all three helpers verify the signature before enforcing the validity window. verify_step_governance_boundary does the reverse, checking expires_at_unix_ms before it calls verify_signature. Both orders fail closed, so this changes which error a caller sees on a receipt that is both expired and unsigned, not whether it is admitted. Match on the variant, not on the order you expected.

GovernanceAuthorizationErrorRaised when
UnsupportedSchemaThe body’s schema string is not the expected constant. Checked first, before any field.
InvalidArtifactA required string is empty after trimming, a digest field is not 64 hex characters, or expires_at_unix_ms <= issued_at_unix_ms. The caller-supplied expected digest and step hash run through the same shape check, so a malformed expectation raises this rather than a mismatch variant.
InvalidSignatureThe signature does not verify against the envelope’s own signer_key.
LeaseNotYetValid / GovernanceReceiptNotYetValidissued_at_unix_ms > now. There is no skew tolerance.
LeaseExpiredOrUnknownexpires_at_unix_ms <= now. Expiry is exclusive at the boundary. The same variant carries a receipt id when a receipt expires.
ScopeDigestMismatchAn expected digest was supplied and does not equal the lease’s, byte for byte.
GovernanceReceiptRequiredA step marked destructive arrived with no receipt.
LeaseMismatch / WorkflowMismatch / StepHashMismatchThe receipt authorizes a different lease, a different workflow, or a different step body.
CryptoCanonicalization or signature machinery failed. Distinct from a signature that verified as false.

The check the crate leaves to you

Because the envelope carries its own signer key, a caller that stops at verify_capability_lease accepts a lease anyone could have minted. The pinning is a separate layer, and chio-attest-buyer-core shows the shape it has to take:

crates/trust/chio-attest-buyer-core/src/report.rsrust
    if lease.signer_key != authority.public_key {
        return Err(ChioPackageError::Governance(format!(
            "lease authority {} signer key mismatch",
            lease.body.issuer
        )));
    }
    if !authority
        .allowed_action_classes
        .contains(&lease.body.action_class)
    {
        return Err(ChioPackageError::Governance(format!(
            "lease authority {} is not trusted for action class {:?}",
            lease.body.issuer, lease.body.action_class
        )));
    }
    verify_capability_lease(lease, now_unix_ms, Some(scope_digest.to_string()))
        .map_err(|error| ChioPackageError::Governance(error.to_string()))

Seven checks run before the library is called at all: the issuer resolves to a trusted lease authority, that authority’s key is absent from the pinned revocation checkpoint, the authority is inside its window at the verifier epoch, the lease is not issued in the future, the lease’s own issue time falls inside the authority window, the envelope’s signer key equals the authority’s published key, and the action class is one the authority is trusted to sign. Both window tests are half-open, so an authority is already inactive at valid_until_unix_ms. The governance receipt gets the same seven, with allowed_case_kinds in place of the action-class allowlist. Revocation is at the key level: the checkpoint lists revoked key fingerprints, there is no revocation list for lease ids, and LeaseExpiredOrUnknown earns the second half of its name only from callers that keep a registry.

The step hash has the same property from the other direction. Nothing in the receipt tells a verifier what step_sha256 should be; verify_destructive_steps recomputes it as the canonical SHA-256 of the tool receipt body for that step and passes the result in. The receipt cannot name its own expected value. That same function re-runs the receipt-inside-lease window nesting that issuance already enforced, so the interval chain is checked twice by two crates that do not trust each other’s output.


What the control plane exposes

RouteDoesSigns with
POST /v1/registry/governance/charters/issueBuilds an unsigned charter body from the request, stamps it with the local operator id, signs, returns itThe node’s authority keypair
POST /v1/registry/governance/cases/issueVerifies the supplied charter, listing, and optional activation signatures, refuses a charter governed by another operator, then mints and signs the caseThe node’s authority keypair
POST /v1/registry/governance/cases/evaluateRuns evaluate_generic_governance_case over artifacts the caller supplies and returns the evaluationNothing. It signs no output

Read that table for what is absent as much as what is present. There is no lease route and no governance-receipt route: those artifacts have a CLI and a library path only. POST /v1/capabilities/issue is not one, whatever the name suggests. It mints a CapabilityToken under the cluster authority keypair, an unrelated artifact family, and unlike the three above it does forward to the leader and does pass the term fence. Nothing on any of the three governance routes is persisted, so a charter or a case exists exactly as long as the caller keeps the JSON. And all three run locally. Each one checks the bearer service token and calls straight into service_runtime::issuance without touching a forwarding helper, so on a clustered node they behave the way Leader & Failover describes for any handler that does not forward: they answer wherever the request lands, and no delta stream carries the result to a peer.

Both issue routes also need --advertise-url: the operator id is derived from it, and without one the route answers 400 with generic registry listings require --advertise-url on the trust-control service. They need a signing source too. Exactly one of --authority-db or --authority-seed-file must be set; both together, or neither, is a 400.

Which key signs matters more than it looks. load_behavioral_feed_signing_keypair resolves to SqliteCapabilityAuthority::local_keypair() when --authority-db is the one configured. That is the unconditional accessor: it returns whatever seed this node holds without comparing it to the replicated authority public key, unlike current_keypair(), which fences on exactly that comparison. Two nodes of one cluster therefore sign governance artifacts with whatever seeds they each hold, under whatever --advertise-url they each advertise, and nothing reconciles the two. Authority & Rotation owns that seam in full.

A clear evaluation is not a pass

One opening shape check on the listing body and the current publisher is a hard Err. Every failure after it short-circuits into an Ok result instead, and the failure constructor sets effective_state: Clear with blocks_admission: false and one finding. An unverifiable case and a resolved case are byte-identical on those two fields. Gate on findings.is_empty() first and on blocks_admission second; a clear result with a finding means "could not enforce", and treating it as "nothing wrong" is how an enforced freeze gets dropped. The sixteen finding codes are tabulated in Listing Disputes.

Guarantees and limits

StatusClaimEvidence
ShippedEvery artifact checks its schema constant before any field, and expiry is exclusive at the boundary in all three verifiers: expires_at <= now is refused, not just <.lease.rs, authorization.rs
Proved by testA lease verifies only strictly inside its window and only against an exactly equal scope digest: 999 is not-yet-valid, 1500 passes, 2000 is expired, and a different digest at 1500 is a mismatch.capability_lease_validates_signature_lifetime_and_scope
Proved by testA governance receipt is refused when the workflow id differs, and refused before its own issue time by both the strict and the boundary check.destructive_governance_receipt_binds_lease_workflow_and_step_hash
Proved by testA non-destructive step needs no receipt; a destructive step with none is GovernanceReceiptRequired.non_destructive_steps_do_not_require_governance_receipts
Proved by testIssuance emits one scope binding per lease with the digest the lease carries, one receipt for the single destructive step, and refuses to sign anything when a named authority is not active.issuer_outputs_verifier_compatible_lease_and_governance_artifacts, inactive_authority_fails_before_signing
LimitA valid signature is an integrity proof, not an authority proof, because the envelope carries its own signer key. Pinning the issuer to a trusted key, checking that key against a revocation checkpoint, and constraining the action class are all caller work.SignedExportEnvelope::verify_signature; verify_trusted_capability_lease as the reference implementation
Limitverify_step_governance_boundary confirms a receipt exists and is live, nothing more. A receipt for a different lease, workflow, or step passes it. Use it as a presence gate and verify_destructive_authorization as the binding check.authorization.rs; both are called, in that order, on the proof-package path
LimitDigest shape validation accepts uppercase hex while every comparison is byte equality, so an uppercase digest passes the shape check and then fails the match. The issuance side is stricter and requires lowercase, so this only bites artifacts minted elsewhere.is_sha256_hex and its test; validate_lowercase_hex in chio-federation-authority
Not claimedOne-shot semantics. Nothing here consumes a receipt or a lease. The step hash binds a receipt to one step body and the window bounds it in time; inside that window the same receipt verifies against the same step as many times as it is presented.No nonce, counter, or store in chio-governance
Not wiredKernel enforcement. chio-kernel re-exports evaluate_generic_governance_case, and the pure core’s exclusion list names governed-transaction policy evaluation as fenced out of it, but no kernel crate and no guard in the seven-guard pipeline calls a lease or receipt verifier. These artifacts are checked at issuance and at proof verification, not on the mediated call. The kernel’s own governed-transaction path is a third artifact family under a third colliding name: chio_core::capability::governance (GovernedApprovalToken, ThresholdApprovalProposal), unrelated to chio-governance.The only verify_capability_lease and verify_destructive_authorization call sites in crates/ are in chio-attest-buyer-core
UnsupportedCluster agreement on governance state. The three routes neither forward to the leader nor replicate, and each node signs with its own local seed under its own advertised operator id. A charter issued on one node is invisible to its peers until somebody carries the JSON across.certification_handlers.rs (no forwarding helper); local_keypair() in chio-store-sqlite/src/authority.rs

Next Steps

  • Listing Disputes · the other half of the crate: case kinds, lifecycle states, the effective-state map, and every finding code
  • Governance Ladder · the manifest that decides which action classes need one of these receipts in the first place
  • Bilateral Co-Sign · lease resolution and receipt digest matching as a verifier step, once the artifact crosses an organization boundary
  • 3-Vendor Walkthrough · one destructive step, end to end, with the lease and receipt in place
  • Authority & Rotation · the keypair the issue routes sign with, and why the unfenced accessor is the one they use
Capability Leases · Chio Docs