Chio/Docs
LOGIN · JOIN

Rotate Keys & Revoke

Withdraw a compromised agent's authority at its next admission, rotate the operator key, and keep past receipts verifiable.

Deployment chio@5f63e6e

On this page

A revoked capability stops authorizing at its next admission on every kernel that reads your revocation store, and its delegated subtree stops with it. A rotated operator key signs everything new, and every receipt signed before the change still verifies.

The trust control plane, chio trust serve, holds the revocation set and the operator signing key. MCP edges, sidecars, and runners read both over --control-url, so one revoke or one rotation reaches every node pointed at the service.

Start
Control and data planesChoose an interface.
Trust control plane · chio trust serveData plane · each node links chio-kernelverifies with cached keys, signs receiptsTrust control planeData plane · a kernel per node

Revocation is looked up on every admission

The kernel checks the presented token and every delegation link before admission and again before finalizing an allow. It makes one lookup per id at GET /v1/revocations, with no cache, and a failed lookup denies the call. The operator revokes with POST /v1/revocations, through chio trust revoke. The state a verdict depends on.

In Rust · lib.rs:50730 lines
pub fn configure_revocation_store(
    kernel: &mut ChioKernel,
    revocation_db_path: Option<&Path>,
    control_url: Option<&str>,
    control_token: Option<&str>,
) -> Result<(), CliError> {
    match (revocation_db_path, control_url) {
        (Some(_), Some(_)) => {
            return Err(CliError::cli_other_error(
                "use either --revocation-db or --control-url for revocation state, not both"
                    .to_string(),
            ));
        }
        (Some(path), None) => {
            kernel.set_revocation_store(Box::new(chio_store_sqlite::SqliteRevocationStore::open(
                path,
            )?));
        }
        (None, Some(url)) => {
            let token = require_control_token(control_token)?;
            kernel.set_revocation_store(
                trust_control::service_runtime::remote_stores::build_remote_revocation_store(
                    url, token,
                )?,
            );
        }
        (None, None) => {}
    }
    Ok(())
}
One operator’s deployment with every node pointed at the trust control plane by --control-url. Durable admission and child receipts have remote adapters too and are left out; a node without the URL keeps the same state in local SQLite files.Architecture

1. Name the lifecycle you are changing

Chio keeps three independent lifecycles. Rotation changes the key used for new signatures; revocation withdraws authority from a credential or capability that already exists.

ObjectWhat it authorizesWhere its state lives
Operator signing keyCapability tokens, reputation credentials, trust-control writesAn authority seed file or the authority database
Agent PassportPortable reputation credentials for a did:chio subjectPassportLifecycleRecord in the passport statuses registry
Capability tokenOne tool invocation under scope, budget, and TTLRevocationRecord in the revocation store

Each lifecycle moves alone. Rotating an operator key leaves outstanding capabilities it signed valid, and revoking a passport leaves the subject's capability tokens valid, so revoke each affected type separately.

Plan rotations from credential lifetimes; revoke at once for a compromised subject, a bad deployment, or a failed policy evaluation. Revocation is visible history: the signed record still exists, and the lifecycle record and revocation store change.

2. Point every kernel at one revocation store

Start trust control with an authority database and a joint session database, which holds revocations and budget state. Then start each edge.

Terminal 1 · trust controlbash
chio trust serve \
  --listen 127.0.0.1:8787 \
  --service-token operator-admin-token \
  --receipt-db ./receipts.sqlite3 \
  --authority-db ./authority.sqlite3 \
  --session-db ./trust-session.sqlite3
Terminal 2 · the governed edgebash
chio --session-db ./session.sqlite3 \
  --control-url http://127.0.0.1:8787 \
  --control-token operator-admin-token \
  mcp serve --policy mcp-policy.yaml --server-id pricing \
  -- python3 mcp_pricing.py

The edge wraps Larkfield's pricing MCP server from the pricing desk and its quote tool. Each kernel checks the presented token and every delegation link against the store at admission, one uncached lookup per id, and a lookup that fails refuses the call. Send one tools/call quoting sku-204 at no discount and the kernel admits it.

MCP tool call response, before the revokejson
{
  "id": 3,
  "jsonrpc": "2.0",
  "result": {
    "content": [
      {
        "text": "{\"id\": \"7c2e4a90-3b1d-4f6e-9a85-d04c1e7b2f63\", \"sku\": \"sku-204\", \"percent\": 0, \"price\": \"58.00\", \"currency\": \"USD\"}",
        "type": "text"
      }
    ],
    "isError": false
  }
}

Take the capability id from that call's receipt, the newest in the store.

Terminalbash
chio --control-url http://127.0.0.1:8787 \
  --control-token operator-admin-token \
  receipt list --order newest --limit 1 \
  | jq -r '.capability_id'
Output
cap-019fbe3d-bcc9-7263-9752-68702b83f369

3. Revoke the capability and its subtree

Revoke through the service so every kernel reading it sees the same row.

Terminalbash
chio --control-url http://127.0.0.1:8787 \
  --control-token operator-admin-token \
  trust revoke \
  --capability-id cap-019fbe3d-bcc9-7263-9752-68702b83f369
Output
capability_id: cap-019fbe3d-bcc9-7263-9752-68702b83f369
revoked:       true
newly_revoked: true
backend:       http://127.0.0.1:8787

You should see backend report the service URL: the write landed in shared state. newly_revoked separates the first write from a repeat, so the step is safe to re-run. Revocation is permanent; restore a subtree by reissuing from a live ancestor.

Revoking one grant in a delegation tree: its subtree is refused from then on; its parent, its siblings, and every earlier call are unchangedselect a grant to revoke itallowedrefusedcalls before revocation standt
trev\htmlClass{role-deny}{t_{\mathrm{rev}}}
ccoordinatorCapabilityRevokedaanalysisDelegationChainRevoked(a)rreaderstests
deny(x)  ⟺  revoked(x)⏟CapabilityRevoked  ∨  ∃ ℓ∈chain(x):revoked(ℓ)⏟DelegationChainRevoked(ℓ)\mathrm{deny}(\htmlClass{role-call}{x}) \iff \underbrace{\htmlClass{role-deny}{\mathrm{revoked}}(\htmlClass{role-call}{x})}_{\texttt{CapabilityRevoked}} \;\lor\; \underbrace{\exists\, \htmlClass{cor-parent}{\ell} \in \mathrm{chain}(\htmlClass{role-call}{x}) : \htmlClass{role-deny}{\mathrm{revoked}}(\htmlClass{cor-parent}{\ell})}_{\texttt{DelegationChainRevoked}(\htmlClass{cor-parent}{\ell})}
closed(a)={ x:x=a  ∨  a∈chain(x) }={a,r}\mathrm{closed}(\htmlClass{role-deny}{a}) = \{\, \htmlClass{role-call}{x} : \htmlClass{role-call}{x} = \htmlClass{role-deny}{a} \;\lor\; \htmlClass{role-deny}{a} \in \mathrm{chain}(\htmlClass{role-call}{x}) \,\} = \{ \htmlClass{role-deny}{a}, \htmlClass{role-deny}{r} \}
Revoking one grant in a delegation tree: its subtree is refused from then on; its parent, its siblings, and every earlier call are unchangedselect a grant to revoke itallowedrefusedearlier calls standt
trev\htmlClass{role-deny}{t_{\mathrm{rev}}}
ccoordinatorCapabilityRevokedaanalysisDelegationChainRevoked(a)rreaderstests
closed(a)={a,r}\mathrm{closed}(\htmlClass{role-deny}{a}) = \{ \htmlClass{role-deny}{a}, \htmlClass{role-deny}{r} \}

Grant a (analysis) is revoked at t_rev. Its subtree, a, r, is closed: a's later calls are refused with CapabilityRevoked and its descendants' with DelegationChainRevoked(a). c and s keep their grants. Every call before t_rev stands.

deny(x)  ⟺  revoked(x)∨  ∃ ℓ∈chain(x):revoked(ℓ)closed(v)={ x:x=v  ∨  v∈chain(x) }\small \begin{aligned} & \mathrm{deny}(\htmlClass{role-call}{x}) \iff \\ & \quad \htmlClass{role-deny}{\mathrm{revoked}}(\htmlClass{role-call}{x}) \\ & \quad \lor\; \exists\, \htmlClass{cor-parent}{\ell} \in \mathrm{chain}(\htmlClass{role-call}{x}) : \htmlClass{role-deny}{\mathrm{revoked}}(\htmlClass{cor-parent}{\ell}) \\[8pt] & \mathrm{closed}(v) = \{\, \htmlClass{role-call}{x} : \\ & \quad \htmlClass{role-call}{x} = v \;\lor\; v \in \mathrm{chain}(\htmlClass{role-call}{x}) \,\} \end{aligned}
In Rust · validation.rs:71813 lines
pub(crate) fn check_revocation(&self, cap: &CapabilityToken) -> Result<(), KernelError> {
    if self.with_revocation_store(|store| Ok(store.is_revoked(&cap.id)?))? {
        return Err(KernelError::CapabilityRevoked(cap.id.clone()));
    }
    for link in &cap.delegation_chain {
        if self.with_revocation_store(|store| Ok(store.is_revoked(&link.capability_id)?))? {
            return Err(KernelError::DelegationChainRevoked(
                link.capability_id.clone(),
            ));
        }
    }
    Ok(())
}
An example team with its calls in order: the kernel refuses a grant when its own id or any ancestor id in its delegation chain is revoked. Calls admitted before the revocation stand, and the refusal holds at each verifier that has observed it.

A capability whose delegation_chain contains the revoked id is refused on presentation. A revoked leaf fails with capability has been revoked; a revoked ancestor fails with delegation chain revoked at ancestor <id>. Revoke a misbehaving child alone; revoke the parent when the agent that delegated is itself compromised.

4. Confirm the next call is refused

Repeat the identical tools/call on the live edge, without a restart or new session.

MCP tool call response, after the revokejson
{
  "id": 4,
  "jsonrpc": "2.0",
  "result": {
    "content": [
      {
        "text": "capability has been revoked: cap-019fbe3d-bcc9-7263-9752-68702b83f369",
        "type": "text"
      }
    ],
    "isError": true
  }
}

You should see an MCP tool error with isError: true. The kernel raised KernelError::CapabilityRevoked during admission, and the wrapped tool server was never reached.

5. Revoke the passport

Transition the lifecycle record where the passport was published: a registry file or trust control. The registry refuses to transition a record it never published.

Terminal · trust controlbash
chio \
  --control-url https://trust.larkfield.example \
  --control-token operator-admin-token \
  passport status revoke \
  --passport-id <passport-artifact-id> \
  --reason key-leak
The same verb against a local registry filetranscript
$ chio passport status revoke \
    --passport-id "$(cat ./passport-id.txt)" \
    --passport-statuses-file ./passport-statuses.json \
    --reason compromised
passport revoked
passport_id:   57191b1ef425678d13e2080b8447bddba4ae8363585ff0d3a69efdac414cf4cc
state:         revoked
revoked_at:    1788609711
revoked_reason:compromised
exit 0

You should see the record move from Active to Revoked with revoked_at stamped and your reason. Trust control owns its registry and returns the same fields under camelCase keys. Later resolutions return state = Revoked, and a verifier policy with requireActiveLifecycle: true stops accepting the passport as soon as that resolution is available.

revocation-runbook · resolvetranscript
$ chio passport status resolve \
    --passport-id "$(cat ./passport-id.txt)" \
    --passport-statuses-file ./passport-statuses.json
passport_id:   57191b1ef425678d13e2080b8447bddba4ae8363585ff0d3a69efdac414cf4cc
state:         revoked
subject:       did:chio:d02f9b5f38ce7746e2ccbd8fc93f6d1d9dd4ddbe02a3e206830a6950b3752590
updated_at:    1788609711
source:        registry:./passport-statuses.json
revoked_at:    1788609711
revoked_reason:compromised
exit 0

6. Rotate the operator signing key

Read the authority status to see the key you are replacing, then rotate with POST /v1/authority.

revocation-runbook · authority-statustranscript
$ curl -sS "$CHIO_CONTROL/v1/authority" \
    -H "Authorization: Bearer $CHIO_TOKEN" | jq .
{
  "configured": true,
  "backend": "sqlite",
  "publicKey": "d991d21af0635a2eacdca9e85966fd8f08bdb3580d28c72dc0fee4f334febf23",
  "generation": 1,
  "rotatedAt": 1788609712,
  "appliesToFutureSessionsOnly": true,
  "trustedPublicKeys": [
    "d991d21af0635a2eacdca9e85966fd8f08bdb3580d28c72dc0fee4f334febf23"
  ]
}
exit 0
revocation-runbook · authority-rotatetranscript
$ curl -sS -X POST "$CHIO_CONTROL/v1/authority" \
    -H "Authorization: Bearer $CHIO_TOKEN" | jq .
{
  "appliesToFutureSessionsOnly": true,
  "backend": "sqlite",
  "configured": true,
  "generation": 2,
  "publicKey": "4a3756f1650d76f57a8d7cc09a83aa95c4b14cfe339857e9db2fde282783a807",
  "rotatedAt": 1788609712,
  "trustedPublicKeys": [
    "d991d21af0635a2eacdca9e85966fd8f08bdb3580d28c72dc0fee4f334febf23",
    "4a3756f1650d76f57a8d7cc09a83aa95c4b14cfe339857e9db2fde282783a807"
  ]
}
exit 0

You should see generation go from 1 to 2 and publicKey change, the pair a replica compares. trustedPublicKeys holds both keys, so a capability the old key signed verifies until it expires or is revoked, and appliesToFutureSessionsOnly says the same of open sessions. After a compromise, remove the old key from relying verifiers' admission trust while keeping it to verify what it already signed.

Rotate through the route, which every replica reads. With keyring custody configured it runs the witnessed rotation, and a hand edit of the seed file would bypass the key log. Then reissue signed verifier policies and reputation credentials under the new key so issuer_allowlist entries keep matching.

7. Reconcile what the revoked authority did

Pull the receipts for the compromised window. A refusal writes a signed receipt on the same schema as an allow.

Terminalbash
chio --control-url http://127.0.0.1:8787 \
  --control-token operator-admin-token \
  receipt list --outcome deny --limit 5
One JSON Lines row, pretty-printed
{
  "action": {
    "parameter_hash": "88bab6d8f6dc68a877064d584cbb5b6c50e74f617ea50d81d3a53c2ee6ffbc4f",
    "parameters": {
      "percent": 0,
      "sku": "sku-204"
    }
  },
  "boundary_class": "prevent",
  "capability_id": "cap-019fbe3d-bcc9-7263-9752-68702b83f369",
  "content_hash": "74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b",
  "decision": {
    "guard": "kernel",
    "reason": "capability has been revoked: cap-019fbe3d-bcc9-7263-9752-68702b83f369",
    "verdict": "deny"
  },
  "id": "110b30ea4829d0cda2b23fa65c117906532bd0ae45a60297c4fd76c00f14382b",
  "kernel_key": "c56228c41776e96d3b266053c0cfd3e2b8439cad5b83730b37a8d2d8ec4fa226",
  "metadata": {
    "attribution": {
      "delegation_depth": 0,
      "issuer_key": "617e2042e610c87a33ba9876e6cb6d750389db1dd74010b62372aa4e6e7eb73d",
      "subject_key": "ead3b427b2954a3373dce7976618a5af04d7d4e60eccfdeb11ccc32bfd163172"
    },
    "chio_receipt_signing_nonce": "rcpt-019fbe3d-bf7e-7ab2-89b0-f5a89725b5dc",
    "receipt_context": {
      "request_id": "mcp-edge-req-a26edfd6b5b14d6aa4d8b81697e975dd"
    }
  },
  "policy_hash": "69e943b96e9ce64d0264bb56bf8930e77bd4e68adf68fcf1395790dae03e6b55",
  "receipt_kind": "mediated_decision",
  "redaction_mode": "none",
  "signature": "a1ea2190da78070fea48d91464b3756f2110b7bba5e3e3f282df3bc2df7abcbeaac386bfb8c9b534e8a365e5f147132884fc4bceb39fc3e082b687cba7687a0d",
  "timestamp": 1785603145,
  "tool_name": "quote",
  "tool_origin": "caller_executed",
  "tool_server": "pricing",
  "trust_level": "mediated"
}

Four fields carry the incident. decision.guard is kernel on every kernel-built deny, naming the layer that refused; decision.reason, or evidence[].guard_name for a guard denial, names the check. capability_id ties the denial to the allow before it, and the byte-identical action.parameter_hash shows both receipts describe one request.

Revocation leaves existing receipts unchanged: the allow stays beside the deny, both signed under kernel_key c56228c4..., and an incident review reads the pair.

Verifiers act on the revocation state published to them

Every node pointed at one trust-control service sees the same revocation set; a single-node kernel can use a local SQLite store. Passport resolutions carry an updatedAt and a cacheTtlSecs, so a consumer can tell current active state from fail-closed stale state.

Terminalbash
# Capability status
chio --json \
  --revocation-db revocations.sqlite3 \
  trust status \
  --capability-id cap-019fbe3d-bcc9-7263-9752-68702b83f369

# Passport lifecycle
chio passport status resolve \
  --passport-id <passport-artifact-id> \
  --passport-statuses-file passport-statuses.json

# Passport lifecycle via the public holder route (read-only, no admin token)
chio passport status resolve \
  --passport-id <passport-artifact-id> \
  --control-url https://trust.larkfield.example
revocation-runbook · statustranscript
$ chio --json --revocation-db ./revocations.sqlite3 \
    trust status --capability-id cap-test-123
{
  "capability_id": "cap-test-123",
  "revocation_backend": "./revocations.sqlite3",
  "revoked": true
}
exit 0

An id the store has never seen returns revoked: false, so a status poll treats "never issued" and "issued and live" alike.

revocation-runbook · status-unknowntranscript
$ chio --json --revocation-db ./revocations.sqlite3 \
    trust status --capability-id cap-never-issued
{
  "capability_id": "cap-never-issued",
  "revocation_backend": "./revocations.sqlite3",
  "revoked": false
}
exit 0

One script runs the incident in order

Revoke capabilities, then the passport, then rotate the key. Revocation is safe to repeat; rotation and publication create new state, so reconcile the current key, generation, or publication before retrying an uncertain response.

incident.shbash
# Emergency: one agent compromised, one operator key leaked
export CHIO_CONTROL=https://trust.larkfield.example
export CHIO_TOKEN=operator-admin-token

# 1. Revoke the agent's capability subtree
chio --control-url $CHIO_CONTROL --control-token $CHIO_TOKEN \
  trust revoke --capability-id cap-019fbe3d-bcc9-7263-9752-68702b83f369

# 2. Revoke the passport
chio --control-url $CHIO_CONTROL --control-token $CHIO_TOKEN \
  passport status revoke \
  --passport-id passport-fixer-v3 \
  --reason compromised

# 3. Rotate the authority key through trust control, which every replica reads
curl -X POST "$CHIO_CONTROL/v1/authority" \
  -H "Authorization: Bearer $CHIO_TOKEN"

# 4. Confirm the new authority public key
curl "$CHIO_CONTROL/v1/authority" \
  -H "Authorization: Bearer $CHIO_TOKEN"

In an incident, revoke the token rather than waiting for its TTL; expiry supplements revocation.

An application host can revoke and rotate its own agent

The approval desk in pricing-desk runs its host on port 4310 with state in state/approvals. After Mara approves one price change, revoke the agent's capability:

Terminalbash
curl --fail -X POST http://127.0.0.1:4310/agent/revoke \
  -H "Authorization: Bearer $(cat state/approvals/mara-token.txt)"

Mara can still approve, but the kernel refuses execution under the revoked grant and no file appears in price-changes: approval and execution authority are separate. Rotating issues new authority:

Terminalbash
curl --fail -X POST http://127.0.0.1:4310/agent/rotate \
  -H "Authorization: Bearer $(cat state/approvals/mara-token.txt)"

The response names the old capability, the new capability, and the new agent subject key, which the host generates and retains. The host issues a grant with the same scope, revokes the previous one, and publishes the next approved price change under it. Pending proposals keep their original capability, existing receipts verify under the unchanged kernel key, and the kernel issuer and Mara's login stay as they were. If the response is lost, inspect the retained capability before retrying; another rotation creates another identity. Read the host implementation.

Each failure names the token, flag, or record it needs

missing or invalid control bearer token, HTTP 401

The control token differs from the service's --service-token. Check --control-token; reads of the public passport route need no token.

revocation-runbook · authority-unauthorizedtranscript
$ curl -sS -w '\nHTTP %{http_code}\n' "$CHIO_CONTROL/v1/authority" \
    -H "Authorization: Bearer wrong-token"
{"error":"missing or invalid control bearer token"}
HTTP 401
exit 0

trust control service requires --authority-seed-file or --authority-db, HTTP 409

The service has no authority backend to rotate. Restart it with --authority-db and rotate again.

trust control service requires --revocation-db, HTTP 409

The service holds no revocation store. Restart with a joint --session-db or a standalone --revocation-db.

a joint authority database replaces separate budget and revocation databases

Both --session-db and --revocation-db were passed. Drop --revocation-db; the joint database owns the revocation store.

was not found in the lifecycle registry

The passport was never published. Publish it with chio passport status publish, then revoke.

cannot include an empty revoked_reason

A lifecycle record with an empty revoked_reason fails PassportLifecycleRecord validation. Pass a non-empty --reason, or omit the flag.

delegation chain revoked at ancestor

A capability was presented under a revoked parent, as expected after a cascade. Reissue the subtree from a live ancestor.

delegation admission failed: missing capability snapshot for delegation ancestor

An ancestor snapshot is absent, or its depth or parent linkage disagrees with the presented chain. This lineage failure is independent of revocation; reissue the chain from an ancestor the kernel holds.

private directory ancestry must not be group or world writable unless sticky

The directory holding the SQLite files has permissive parents. Run chmod 700 on the working directory before starting the service.

Commands, flags, and records

Commands and the state they write

ActionCommandState it writes
Rotate authority key
POST /v1/authority on trust control
TrustAuthorityStatus.rotated_at
Revoke a passport
chio passport status revoke
PassportLifecycleRecord.status = Revoked
Supersede a passport
chio passport status publish
superseded_by = <new-id>
Revoke a capability
chio trust revoke --capability-id ...
RevocationRecord in the revocation store
Check capability status
chio trust status --capability-id ...
Read-only query
Check passport lifecycle
chio passport status resolve
Returns PassportLifecycleResolution

Revocation and authority backends

  • --revocation-db names a local SQLite revocation store; --control-url names a trust-control service instead. Every revoke and status command targets one or the other.
  • --control-token is sent as an Authorization: Bearer header. Revoking and rotating need it; reading a passport lifecycle record does not.
  • chio trust serve takes a joint authority database (--session-db, which supplies the revocation store with budget state) or a standalone --revocation-db. The two are mutually exclusive, and with neither a revoke answers 409 naming the flag it wants.
  • On --authority-db the authority status carries a monotonic generation and a rotatedAt stamp. On --authority-seed-file the rotation happens and both come back null, so a replica diffs the key itself. With neither, the rotate call answers 409.
  • --passport-statuses-file names a JSON passport statuses registry; trust control can host the same registry. A record must be published before it can be revoked.
  • With --advertise-url, published passport records inherit https://.../v1/public/passport/statuses/resolve as the default holder resolution endpoint, with a cacheTtlSecs of 300.

Authority key and routes

An operator signing key is an Ed25519 keypair held as a seed file. chio-control-plane exports load_or_create_authority_keypair to read it and rotate_authority_keypair to roll it atomically: a new keypair is generated, written to the seed path, and its public key returned.

rotate_authority_keypairrust
pub fn rotate_authority_keypair(path: &Path) -> Result<chio_core::PublicKey, CliError> {
    let keypair = Keypair::generate();
    write_authority_seed_file(path, &keypair)?;
    Ok(keypair.public_key())
}

POST /v1/authority performs the rotation on the service and returns a TrustAuthorityStatus carrying the new public key; GET /v1/authority reads it back. The status fields are configured, backend, publicKey, generation, rotatedAt, appliesToFutureSessionsOnly, and trustedPublicKeys. Verifiers query the route and pin the new key before the old key stops signing new capabilities. Policies signed before the rotation still verify against their signer but no longer name the current authority.

Revocation store

crates/kernel/chio-kernel/src/revocation_runtime.rs16-39rust
pub trait RevocationStore: Send + Sync {
    /// Check if a capability ID has been revoked.
    fn is_revoked(&self, capability_id: &str) -> Result<bool, RevocationStoreError>;

    /// Revoke a capability. Returns `true` if it was newly revoked.
    fn revoke(&self, capability_id: &str) -> Result<bool, RevocationStoreError>;

    fn observe_revocation(
        &self,
        capability_id: &str,
    ) -> Result<RevocationObservation, RevocationStoreError> {
        Ok(RevocationObservation {
            revoked: self.is_revoked(capability_id)?,
            commit: None,
        })
    }

    /// Whether this store loses its revocation set on process restart. The
    /// default is the safe (loud) assumption so an unknown store is treated as
    /// ephemeral; durable and remote stores override to `false`.
    fn is_ephemeral(&self) -> bool {
        true
    }
}

Every method takes &self, so a store is shared behind an Arc and mutates through interior mutability. observe_revocation returns the boolean plus the commit metadata a distributed store attaches; its default answers from is_revoked with no commit. The SQLite store persists a two-field row:

crates/kernel/chio-kernel/src/revocation_store.rsrust
pub struct RevocationRecord {
    pub capability_id: String,
    pub revoked_at: i64,
}

SqliteRevocationStore exposes list_revocations_after for cursor-based replication and suits single-node kernels and offline proofs. Trust control exposes a revocation query API with cursor pagination and a revoke admin endpoint.

Revocation inside the kernel

Each admission through ChioKernel consults the RevocationStore. The write is one row, committed inside a runtime trace transition; with a trace observer installed, the commit becomes a RevocationCommitted event whose newly_revoked field separates a new revocation from a repeat.

crates/kernel/chio-kernel/src/kernel/validation/revocation_trace.rs10-29rust
pub fn revoke_capability(&self, capability_id: &CapabilityId) -> Result<(), KernelError> {
    info!(capability_id = %capability_id, "revoking capability");
    let trace_transition = self.lock_runtime_trace_transition()?;
    let newly_revoked = self.with_revocation_store(|store| Ok(store.revoke(capability_id)?))?;
    let trace_event = if self.runtime_trace_observer.is_some() {
        Some(RuntimeTraceEvent::RevocationCommitted {
            source_sequence: self.allocate_runtime_trace_source_sequence()?,
            capability_id: capability_id.clone(),
            newly_revoked,
            delegation_depth_limit: self.config.max_delegation_depth,
        })
    } else {
        None
    };
    drop(trace_transition);
    if let Some(event) = trace_event {
        self.observe_runtime_trace(event);
    }
    Ok(())
}

Revoke and check against a local store

Both backends call the same revocation operation. Against a local store the command prints the capability, whether it was newly revoked, and the backend that committed the write.

Terminalbash
# Local single-node kernel, persistent sqlite revocation store
chio \
  --revocation-db revocations.sqlite3 \
  trust revoke \
  --capability-id cap-019fbe3d-bcc9-7263-9752-68702b83f369

# Trust-control service, cluster-wide visibility
chio \
  --control-url https://trust.larkfield.example \
  --control-token operator-admin-token \
  trust revoke \
  --capability-id cap-019fbe3d-bcc9-7263-9752-68702b83f369
revocation-runbook · revoketranscript
$ chio --revocation-db ./revocations.sqlite3 \
    trust revoke --capability-id cap-test-123
capability_id: cap-test-123
revoked:       true
newly_revoked: true
backend:       ./revocations.sqlite3
exit 0

In JSON mode the result suits automated runbooks. The capture is a second run against the same store: revoked is still true and newly_revoked is false. The alphabetical key order is the canonical JSON the command-result envelope serializes in.

revocation-runbook · revoke-jsontranscript
$ chio --json --revocation-db ./revocations.sqlite3 \
    trust revoke --capability-id cap-test-123
{
  "capability_id": "cap-test-123",
  "newly_revoked": false,
  "revocation_backend": "./revocations.sqlite3",
  "revoked": true
}
exit 0

Passport lifecycle record

An Agent Passport is a bundle of signed credentials. Revocation operates on its published PassportLifecycleRecord, never the signed passport: the record moves from Active to Revoked, stamps revoked_at, and may carry a revoked_reason that later resolutions return. A new passport for the same subject replaces the old one through a record whose superseded_by names the new passport: a lifecycle transition relying parties honor, distinct from a security revocation.

crates/trust/chio-credentials/src/passport.rs88-107rust
pub struct PassportLifecycleRecord {
    pub passport_id: String,
    pub subject: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub issuers: Vec<String>,
    pub issuer_count: usize,
    pub published_at: u64,
    #[serde(default)]
    pub updated_at: u64,
    pub status: PassportLifecycleState,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub superseded_by: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub revoked_at: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub revoked_reason: Option<String>,
    #[serde(default, skip_serializing_if = "PassportStatusDistribution::is_empty")]
    pub distribution: PassportStatusDistribution,
    pub valid_until: String,
}

Passport revocations resolve through /v1/public/passport/statuses/resolve/{passport_id}. PassportStatusDistribution validation rejects a distribution that publishes resolve URLs without a cacheTtlSecs, and rejects a TTL of zero. chio did resolve --passport-status-url ... advertises the resolve URL in the subject's DID document as a ChioPassportStatusService service entry.

Next: run trust control for every node