Runtime Contract
See what a kernel-hosting process owns: its session database lease, its boot order, its drain window, and its budget guarantee.
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
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.
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-dbflag (joint_authority_db_pathinTrustServiceConfig) puts budget and revocation state behind the serving owner, its lock, and its epoch. Forchio runandchio mcp serve-httpthis is the default: the policy default fordurable_admission_modeisside_effecting, which requires a session database. - Separate stores.
--budget-dband--revocation-dbopen throughSqliteBudgetStore::openandSqliteRevocationStore::openwithserving_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.
- 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. - Claim the disk. With a session database, the lock root is a
.lockssibling of that database, created at mode0700.provisionestablishes or re-verifies the store identity, thenopen_servingtakes the lock and advances the epoch. Without one, the step is skipped and the process holds no serving lease. - Bind the listener. The TCP bind comes before the remaining stores open, because
build_cluster_stateneeds the resolvedlocal_addrto default the advertise URL. - 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.
- Decide whether it is clustered.
build_cluster_state(&config, local_addr)returns anOption, and the sync loop is spawned only onSome. - Install, wrap, and serve.
ShutdownController::installfans one signal handler out over a watch channel,apply_server_hygienewraps the router,MaxConnListenercaps accepted connections, andrun_until_drainedbounds 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.
| Limit | Default | Trust control | Past the limit |
|---|---|---|---|
| Drain window | 25 s | 25 s | DrainOutcome::Forced |
| Request timeout | 20 s | off | 408 |
| Concurrent requests | 1024 | 1024 | 503, load shed |
| Accepted connections | 2048 | 2048 | held at the cap |
| Body size | None | 1 MiB | 413 |
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.
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
Try owner_epoch, --peer-url, or budget_ack_heads.
Runtime objects
| Object | What it is | Where 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. | Sqlite |
Store mutation fence | { store_uuid, lease_id, owner_epoch }, stamped into every mutation this process makes against that store. | crates/ |
Serving lease | A row spanning start_ to end_head_index, opened on boot and closed by the next boot. | chio_ |
Cluster runtime state | An Option; None is the ordinary standalone value. | build_ |
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.
| Obligation | What the node supplies | Refused 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- 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-. Clustering requires the SQLite --authority-db, the capability- |
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 | Cluster: 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_ 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_ 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".