Chio/Docs
LOGIN · JOIN

Runtime Contract

See what a kernel-hosting process owns: its session database lease, its boot order, its drain window, and its budget guarantee.

Concept chio@5f63e6e

On this page

A node is the process around the kernel. It supplies the stores, keys, clocks, and I/O that the portable decision core leaves to its host, and it owns them, so a decision the kernel admitted stays enforceable after a crash or a restart.

One process at a time serves a session database. It stamps each mutation with the store's identity, its serving lease, and an owner epoch that only grows, so a second process or a stale handle is refused before it writes. The node overview places the node among the other Platform parts.

One process owns the session database, and every boot raises its epoch

Serving owner epochsChoose a moment.
boot 2boot 3p1epoch 10..14p2epoch 214..31p3epoch 331..servingFencedp′secondAlreadyServing2201431head indexboot 2boot 3p1epoch 10..14p2epoch 214..31p3epoch 331..Fencedp′secondAlreadyServing2201431head index

Each boot closes the last lease and opens the next

open_serving takes the advisory lock, increments owner_epoch, closes the previous lease at the current authority head, and inserts a new lease row that starts there. Every mutation the process makes against the store carries { store_uuid, lease_id, owner_epoch }. Node State on Disk.

Three boots of one node against one session database, each lease drawn over the authority head index it covers. The indexes are illustrative; separate budget and revocation databases, which run without a serving owner, are left out.

open_serving canonicalizes the database path and requires the lock root and the database's parent directory to be owned by the effective user and closed to group and world writes. Then it takes an advisory lock on the per-store lock file, and a held lock maps WouldBlock to SqliteServingOwnerError::AlreadyServing. The test concurrent_process_open_is_fenced_before_serving proves it by spawning a second operating-system process.

Because a lock can be released and taken again, each successful open also increments owner_epoch, closes the previous lease at the current authority head, and inserts a new lease row that starts there.

A handle from a superseded epoch fails closed. Budget writes return BudgetStoreError::Fenced, and revocation access, reads such as is_revoked included, returns RevocationStoreError::Fenced. The lease history answers from the node's own disk which lease wrote a commit: verify_historical_revocation_commit joins chio_serving_leases against admission_authority_commits and requires the commit index to fall inside that lease's range.

  • Joint session store. The global --session-db flag (joint_authority_db_path in TrustServiceConfig) puts budget and revocation state behind the serving owner, its lock, and its epoch. For chio run and chio mcp serve-http this is the default: the policy default for durable_admission_mode is side_effecting, which requires a session database.
  • Separate stores. --budget-db and --revocation-db open through SqliteBudgetStore::open and SqliteRevocationStore::open with serving_owner: None. They take no lock, so two processes can hold the same file.

The two layouts never share a file: both separate stores refuse a database that already carries a serving-owner table, and config.validate() rejects a configuration that names both. Setting durable admission to off needs allow_unsafe_durable_admission_off and an ephemeral receipt log. Choose the layout before you make single-writer or recovery claims.

Read more in Node State on Disk.

Boot claims the disk before it binds, and any failed step aborts it

Trust control starts through serve_async, which runs six steps in order. Each returns an error that aborts the boot, so a node that serves has passed all six.

  1. Validate the config. config.validate() rejects a service token that is empty, whitespace-padded, or carries control characters; a zero cluster sync interval; a zero certification metadata TTL; a tenant read token equal to the service token; a session database configured beside --budget-db, --revocation-db, or peers; and two configured databases that resolve to the same file.
  2. Claim the disk. With a session database, the lock root is a .locks sibling of that database, created at mode 0700. provision establishes or re-verifies the store identity, then open_serving takes the lock and advances the epoch. Without one, the step is skipped and the process holds no serving lease.
  3. Bind the listener. The TCP bind comes before the remaining stores open, because build_cluster_state needs the resolved local_addr to default the advertise URL.
  4. Open the separate stores. Budget and revocation stores open only when their own paths are configured, the branch a session database excludes. A node with no budget database still serves, with no budget authority to offer.
  5. Decide whether it is clustered. build_cluster_state(&config, local_addr) returns an Option, and the sync loop is spawned only on Some.
  6. Install, wrap, and serve. ShutdownController::install fans one signal handler out over a watch channel, apply_server_hygiene wraps the router, MaxConnListener caps accepted connections, and run_until_drained bounds the drain after the signal.
Protocol edges add manifest validation

A protocol edge adds one step, and a bare trust-control process validates no manifest. The edge exists only once every manifest it was handed passes chio_manifest::validate_manifest, which runs inside the constructor and returns an error in place of a serving edge. Validation is structural: schema identifier, identity fields, unique tool names, JSON-object schemas, pricing completeness, and duplicate-free permissions.

ChioMcpEdge::new routes through build_exposed_tool_bindings and refuses to construct when two manifests claim one tool name, with ManifestError::DuplicateToolName. ChioAcpEdge::new and ChioA2aEdge::new run the same validation loop, so a malformed manifest fails all three edges. On a duplicate name, ACP marks the capability BridgeFidelity::Unsupported, drops it from the bindings, and continues; A2A counts the duplicate, flags the skill as ambiguous, and continues. The fullest edge, with Streamable HTTP, bearer and sender-constrained admission, and one kernel per session, is Remote MCP Edge.

Signature checks are a separate operation. chio_manifest::verify_manifest checks a SignedManifest against the registered server key and requires the embedded public_key field to match the signer; the confined launcher calls it at admission. The FFI entry point chio_verify_manifest_json calls chio_binding_helpers::verify_signed_manifest_json, which returns a ManifestVerification report of four booleans for the caller to check.

Stopping is bounded: 25 seconds to drain, then the serve future is dropped

Every Chio serve site takes its stop path from chio-http-serve. It notices the stop signal, stops accepting and drains inside a bounded window, and answers excess load with an explicit status.

LimitDefaultTrust controlPast the limit
Drain window25 s25 sDrainOutcome::Forced
Request timeout20 soff408
Concurrent requests10241024503, load shed
Accepted connections20482048held at the cap
Body sizeNone1 MiB413

The request timeout sits below the drain, so a request admitted late reaches its own 408 before the drain severs it. The body cap stays unset by default, so a site with a large route-local upload limit keeps it. Trust control disables the request timeout because a clustered budget authorize parks in the quorum wait. A timeout firing after the local exposure write, before that wait returned, would drop the handler before its rollback branch and leave a charged, leader-visible write the client saw fail.

The post-drain join of the cluster sync loop gets only what remains of the same window. A drain that reaches its deadline is logged and reported as DrainOutcome::Forced, and the serve future is dropped to release the accept loop. Axum runs each connection on a detached task that a bounded drain can neither join nor cancel, so a handler can still be finishing after a forced report.

Trust control passes a no-op flush hook, since its handlers write budget and revocation state synchronously. ServeError::Flush covers the sites whose receipts are buffered.

Read more in Node Lifecycle.

A standalone node carries the stronger budget guarantee

With no peers configured, trust control runs as one node with one SQLite joint authority store. That shape provides the single_node_atomic budget guarantee durable admission requires. It is one failure domain, so its availability comes from a supervisor, a restart, and a backup.

crates/platform/chio-control-plane/src/trust_control/cluster/consensus.rsrust
pub(crate) fn build_cluster_state(
    config: &TrustServiceConfig,
    local_addr: SocketAddr,
) -> Result<Option<Arc<Mutex<ClusterRuntimeState>>>, CliError> {
    config.validate()?;
    // ... rejects peers configured alongside --authority-seed-file
    if config.peer_urls.is_empty() {
        return Ok(None);
    }

No peers means Ok(None), and so does a peer list that normalizes to nothing once the node's own advertise URL is filtered out. init.rs spawns the sync task through state.cluster.is_some().then(...), so an unclustered node runs no replication loop, opens no peer connections, and has no background work to fail. Its health response reports clustered as that boolean; Health & Readiness covers the rest of the response.

budget_authority_guarantee_level returns single_node_atomic when state.cluster is None and advisory_posthoc otherwise. AdmissionCombinedCaptureCommit::validate and verify_historical_revocation_commit accept only SingleNodeAtomic. On the write path, wait_for_budget_write_quorum_commit returns Ok(None) at once when the node has no cluster progress handle, so there is no quorum wait to time out into a 503.

Clustered trust control and its overrun bound

A clustered deployment supplies an advertise URL, a peer set, replication cursors, authority leases, and budget-write acknowledgments, listed in the reference below. Trust Control Plane covers what the cluster does with them. These settings are separate from cross-operator federation and swarms.

The clustered coordinator reports advisory_posthoc. Its overrun is bounded at max_cost_per_invocation * node_count, and the concurrent_charge_overrun_bound test in budget_store exercises that bound. Replication and a shared address leave that contract short of distributed linearizability.

In trust control the fence and the cluster are mutually exclusive. config.validate() rejects a session database configured beside any peer, and optional_budget_store answers 503 with the same refusal if the pair is reached at runtime. A clustered node has no serving owner, no serving lease, and no cross-process write fence; a fenced node has no peers.

Each layer of a deployment establishes one thing

The process hosts transports, runtimes, state, and lifecycle. One process may host several kernels, and trust control is a separate service composition. The joint session store gives one serving owner and epoch-fenced local authority mutations, which separate stores and a clustered node run without.

A protocol edge carries a governed request to the kernel along its configured path; transport errors and compatibility passthrough produce no governed receipt. Admission can refuse dispatch, and a later failure leaves an external effect in place, a limit shared by every kernel and stated at the mediation boundary.

An application that connects through an SDK supplies the node URL and credentials, while the operator manages serving locks, state, health, and shutdown. The TypeScript, Python, and Go SDKs also check canonical JSON, signatures, capabilities, and receipts locally, with no live node. The Quickstart shows an allowed call and its receipt.

Runtime objects and cluster obligations

Runtime objects

ObjectWhat it isWhere it lives
Node
A running process that owns its listeners, runtime services, stores, and locks.
A chio trust serve or chio mcp serve-http process.
Serving owner
The single writer identity for one session database, held by advisory lock on a private lock file.
SqliteAuthorityStore::open_serving
Store mutation fence
{ store_uuid, lease_id, owner_epoch }, stamped into every mutation this process makes against that store.
crates/core/chio-core-types/src/store_fence.rs
Serving lease
A row spanning start_head_index to end_head_index, opened on boot and closed by the next boot.
chio_serving_leases
Cluster runtime state
An Option; None is the ordinary standalone value.
build_cluster_state

Startup sequence source

The six boot steps are serve_async in crates/platform/chio-control-plane/src/trust_control/service_runtime/init.rs, the reference sequence for a serving node.

Cluster obligations

Adding peers leaves the node the same process and adds five obligations it supplies and the cluster consumes.

ObligationWhat the node suppliesRefused when
Advertise URL
--advertise-url, defaulting to http:// plus the bound local address. Normalized once, at boot, into self_url.
The scheme is other than http or https, or the URL carries credentials, a query string, or a fragment. A loopback or private host is rejected too, unless --allow-local-peer-urls is set; it defaults off.
Peer set
Repeated --peer-url values, normalized the same way, with the node's own URL filtered out.
Peers are configured alongside --authority-seed-file. Clustering requires the SQLite --authority-db, the capability-authority store, which is distinct from the session database.
Replication cursors
tool_seq, child_seq, lineage_seq, budget_seq, and a revocation cursor of (revoked_at, capability_id). An unconfigured store publishes a zero, or null for the revocation cursor, in place of an error.
A peer page that fails to advance past the cursor, or is not contiguous from the expected next seq, is a wire-contract violation and demotes the peer to Unhealthy.
Fenced authority lease
ClusterAuthorityLeaseView: a lease ID of {leader_url}#term-{epoch}, the election term, and an expiry. The stored TTL is three sync intervals clamped to 500 ms through 5 s, so the 500 ms default interval yields 1.5 s; consensus truncates it to whole seconds with a 1 s floor when computing expiry and contact freshness.
current_budget_event_authority answers 503 when the lease is unavailable, including when no leader exists for want of quorum, and again when it expired before the write could start.
Budget write quorum acks
Per-origin contiguous ack heads, published as budget_ack_heads and consumed as witnesses for another node's write.
The ack head for that exact origin has yet to reach the write's event_seq, or the peer is unreachable, partitioned, or pending a forced snapshot.

Election term across a restart

The lease pins the cluster's shared authority to a term. The election term is persisted in the capability-authority store. On boot, build_cluster_state reads it only when --authority-db is configured, and adopts it only when the recorded generation and rotation timestamp still match the live authority. A rotated authority makes that fence stale: the node logs the discard and starts from term zero, so no lease carries across a key change. A persisted leader URL is kept only if it still normalizes and still names this node or a configured peer.

Session database with peers

The refusal from config.validate(), and from optional_budget_store at runtime, reads "a local joint authority database cannot run with the legacy cluster coordinator".

Next: see what the node keeps on disk