Skip to main content

A small control plane with explicit recovery

Substrate serves JSON over HTTP and bounded WebSocket channels. Resource responses carry observed state, and every response identifies the contract bundle and its digest in headers.

This page summarizes the currently implemented public families. Capability facts remain the authority for what a particular daemon can serve.

Machine​

MethodPathPurpose
GET/v1/machinedeployment, driver generation, verified capabilities, limits, contract identity

Query this first. Do not infer support from a version number or deployment label.

Workspaces​

MethodPathPurpose
POST/v1/workspacescreate a workspace through a keyed operation
GET/v1/workspaces/{workspace_id}read observed workspace state
DELETE/v1/workspaces/{workspace_id}destroy a workspace through a keyed operation
GET/v1/workspaces/{workspace_id}/files/{*path}read a bounded file range
PUT/v1/workspaces/{workspace_id}/files/{*path}atomically replace a file
DELETE/v1/workspaces/{workspace_id}/files/{*path}delete a guarded path
POST/v1/workspaces/{workspace_id}/lease/renewrenew workspace liveness

The host serves empty workspaces and conditionally serves Git materialization. A Git request names a deployment-configured HTTPS source, an authorized opaque locator, the provider branch, an exact commit and a depth from 1 through 50. It requires the workspace.git fact and transient source authority. The resulting working tree has detached HEAD at that commit. Its .git directory is usable in confined terminals but hidden from file and tree APIs.

The bounded Git routes below compare against the host-private immutable materialization baseline. They do not turn the workspace API into an arbitrary Git remote client.

Development v2 byte plane​

Bundle 0.9.0 declares the already-served v2 workspace byte plane. It adds no new resource family: it gives file operations closed request shapes for bounded directory reads, byte replacement, edits, and patches.

MethodPathPurpose
GET/v2/workspaces/{workspace_id}/files/{*path}read bounded file bytes
GET/v2/workspaces/{workspace_id}/treeread a bounded directory tree
GET/v2/workspaces/{workspace_id}/git/baseline/{*path}read one bounded file at the materialized commit
GET/v2/workspaces/{workspace_id}/git/changescompare the index/worktree with the materialized commit under item and byte bounds
PUT/v2/workspaces/{workspace_id}/files/{*path}atomically replace file bytes
POST/v2/workspaces/{workspace_id}/file-edits/{*path}apply one bounded positional edit
POST/v2/workspaces/{workspace_id}/file-patches/{*path}apply one bounded patch

A directory entry the contract cannot name is omitted from both listings: an entry that is not a regular file, directory or symbolic link (a FIFO, socket or device), or whose name is not UTF-8. The v1 directory page (mode=directory) and the v2 tree leave it out, as they leave out .git, and the tree never descends into it or counts it toward limit_items. Since bundle 0.23.0 (ADR 0043) both listings name what they left out in the x-b10x-omitted-entries response header: the count of such entries, sent only when it is at least 1. On a v1 page it is the directory's count, the same on every page; on the v2 tree it covers the part the walk reached, so it is a lower bound when the walk reached limit_items. A .git entry, and a hidden entry the request excludes, are not counted. The tree still answers truncated: true for either of two causes, as before: it reached limit_items, or it omitted at least one such entry. The header tells them apart: truncated: true without it means the limit only, so raising the limit helps; with it, at least one entry will stay omitted whatever the limit. The Rust SDK's Workspace::tree_with_omissions and Workspace::read_directory_with_omissions return the count beside the listing. Neither listing refuses because of such an entry; workspace.path-escape stays the answer for a path that escapes the workspace.

Release 0.16.1 advertises substrate-wire/0.25.0 in x-b10x-contract and its inner bundle.json SHA-256 in x-b10x-contract-bundle-sha256. The two headers are one claim. The signed outer OCI manifest has a different digest because it identifies the distribution package rather than the inner contract manifest.

Bundle 0.25.0 adds no route; bundle history lists what each bundle added. The session routes moved to /v1/sessions in bundle 0.15.0; no /v1/pipe-sessions compatibility alias is served.

Frozen compatibility declarations and route/schema details are available in the 0.25.0 bundle. See system model and derivation for which artifacts are generated and status for release maturity.

Metrics​

MethodPathPurpose
GET/v1/metricsrefresh one exec or workspace usage observation
GET/v1/metrics/streamreceive latest-wins live exec samples over WebSocket

Execs​

MethodPathPurpose
POST/v1/execsstart a bounded exec through a keyed operation
GET/v1/execs/{exec_id}read observed exec state
GET/v1/execs/{exec_id}/outputread persisted bounded output
POST/v1/execs/{exec_id}/signalsignal a running exec
POST/v1/execs/{exec_id}/lease/renewrenew exec liveness
DELETE/v1/execs/{exec_id}retire an exec

Exec start is served only when the daemon verified its complete host confinement floor.

Sessions​

MethodPathPurpose
GET/v1/sessionsread session capability information
POST/v1/sessionsstart a leased raw-pipe or PTY session
GET/v1/sessions/{session_id}read observed session state
GET/v1/sessions/{session_id}/attachattach the one bounded WebSocket channel
POST/v1/sessions/{session_id}/attachment-authoritiesmint one bounded hosted WSS attachment authority
POST/v1/sessions/{session_id}/signalsignal the underlying process
POST/v1/sessions/{session_id}/lease/renewrenew session liveness
DELETE/v1/sessions/{session_id}retire a session

Sessions are a development slice. Raw pipes remain the default; PTY mode requires a bounded initial window and a verified sessions.pty machine fact. Hosted attachment requires a short-lived, one-use key-and-channel-bound authority; Unix attachment continues to use kernel peer credentials.

Recovery and events​

MethodPathPurpose
GET/v1/ops/{operation_id}reconcile one caller-minted operation
GET/v1/eventspage through retained typed events
GET/v1/events/streamfollow the same event sequence over WebSocket
POST/v1/reconciliation-snapshotscreate a bounded state barrier
GET/v1/reconciliation-snapshots/{snapshot_id}read the barriered recovery view

Event replay is bounded by the retention advertised in machine facts. Consumers must reconcile from a snapshot after a history gap. A reconciliation snapshot contains observed resource state, not a restorable copy of workspace files.

Identity and operation IDs​

Resource IDs are opaque and server-minted. Mutation operation IDs are caller-minted and stable across retry. Resource and operation lookup stays within the authenticated subject namespace.

Read operations and observations for retry semantics and status for the implementation boundary.

Bundle history​

Earlier bundle directories remain immutable. A source contract promotion is not by itself a signed daemon release; consumers must pin and test the matching SDK/daemon revision. The SDK refuses an older daemon's contract before issuing mutations.

0.26.0​

Development source now advertises substrate-wire/0.26.0; no release advertises it yet. It adds no route. A capability snapshot may state what the serving confinement adapter proved, never its name: one optional fact per family, confinement.family.process-run through confinement.family.audit in ADR 0044's order, and one per mechanism-neutral property, such as confinement.whole-tree-kill or confinement.paths-are-host-paths. Each is true or absent, never false. An exec or session start may carry sandbox.requires, a closed object with a families list and a facts list of those names without their prefixes; an unknown name, a duplicate or an empty object is request.schema-invalid. The first required family the start's snapshot does not serve is confinement.family-unserved and the first required property it does not prove is confinement.fact-unserved, each 501 with class unserved, recorded as refused and never dispatched. The applied record echoes requires, and its cgroup is optional: present whenever a cgroup was applied, as on every host run. A host daemon publishes what its linux adapter proved, from the facts it already probes: audit always; workspace with guarded workspace I/O; and, on a host with the exec confinement floor, process-run, process-watch, output-read, wait and limits, filesystem-view where read-only roots and files are proved, network-egress where an egress aperture is declared, and the properties whole-tree-kill, ipc-socket-allowlist and ipc-signals-contained. It never publishes network-ingress, and the API never names the adapter.

0.25.0​

Release 0.16.1 advertises substrate-wire/0.25.0. It adds no route. A capability snapshot's driver may now be kubernetes as well as host, and a driver may publish the optional fact confinement.no-egress when a mechanism it verified keeps a workload off the network. Three refusals are new, each 501 with class unserved and nothing dispatched: an inline capsule on a driver without exec.inline-capsule is exec.inline-capsule-unserved, a signal outside the published exec.signals is exec.signal-unserved, and an adopt on a driver that adopts no directory is workspace.adopt-unserved. The host daemon's answers are unchanged: it still reports driver: host, publishes none of the new fact and raises none of the new codes.

The Kubernetes namespace driver reports driver: kubernetes and answers every adopt workspace.adopt-unserved. Its probe proves neither the four Linux confinement facts nor confinement.no-egress, so it refuses every exec start exec.sandbox-unavailable, and the two exec codes are not reached yet.

0.24.0​

0.24.0 added one route, the per-process event stream of ADR 0036 S12: GET /v1/processes/{process_id}/events, a WebSocket under the process scope. Its frames interleave the record's process.* events and its output lines in one order, report a gap for log bytes the bound discarded, and end with exactly one ended item naming ended_by. Each frame carries an opaque cursor to resume from. The event union gains seven process.* variants, so a client that validates events against 0.23.0 must move to 0.24.0. A ninth concurrent stream of one record, a subject's 513th or a deployment's 513th, live or ended records alike, is refused process.events-readers-exceeded (429).

0.23.0​

0.23.0 adds no route and publishes the read-only roots the wire has always admitted (ADR 0010): an exec or session start may name read_only_roots, at most four {"host_path": <path>, "mount": <path>} entries, each path absolute and canonical. A host publishes exec.read-only-roots, {"max_roots": 4}, only when it is confined and resolves paths without following a symlink. The applied record lists each bound root as declared. No refusal is new: exec.read-only-root-invalid, exec.read-only-root-absent and exec.read-only-root-not-a-directory (422) are now published, each with a refusal vector. A start naming a root on a host without the fact is refused exec.sandbox-unavailable (501) before dispatch, after the checks of the start's other confinement members. The exec applied record now also admits secret_slots, which 0.5.0 to 0.22.0 wrongly reject, and read_only_files, which 0.18.0 to 0.22.0 wrongly reject. A client that validates capability snapshots or resources against 0.22.0 must move to 0.23.0 to accept the new fact and record members. The session start's exec object now also publishes scratch, measurements and workspace_access, each the same schema as on exec start; a session has always accepted and applied them. Both starts now publish the refusals those three members have always raised, each held to its substrate_wire constant, and the daemon refuses a null scratch, a storage max_bytes that is not a multiple of 1024 and a repeated measurement as request.schema-invalid (422), as the schemas always did.

0.22.0​

0.22.0 added ADR 0038. An exec or session start may name writable_roots, at most eight writable mounts outside /workspace (ADR 0038). Each entry is exactly one of {"host": <name>, "mount": <path>}, a directory the operator declared by name, or {"workspace": <workspace id>, "mount": <path>}, a Git checkout workspace of the caller; a request never carries a host path. A mount is absolute and canonical, never /, and neither is, lies beneath nor contains a system path, /workspace, /runtime, /scratch, another writable root or a read-only root or file; each of those rules has its own named refusal (422), checked before any capability. A host publishes exec.writable-roots.host, the declared names, and exec.writable-roots.git only after it proved a root bound by descriptor is writable; a start naming a kind or name no fact serves is exec.writable-roots-host-unserved or exec.writable-roots-git-unserved (501), and the message names the root. The applied record lists each bound root with its kind, name or workspace id, host path and mount, and a workdir may lie beneath a writable root. A client that validates capability snapshots or resources against 0.21.0 must move to 0.22.0 to accept the new facts and record member.

0.21.0​

0.21.0 added design 26. An exec or session start may name workdir, the directory the program starts in: an absolute, lexically normal path of at most 4096 bytes at or beneath /workspace, a declared read-only root mount, /scratch when scratch is requested, or /runtime when a capsule is carried. A host that has proved it can apply one publishes the exec.workdir fact and refuses an absent or symlinked directory as exec.workdir-missing before anything runs; without the fact a start that names one is exec.workdir-unserved. A sandbox may name apertures, a list of two to four distinct declared apertures, instead of one aperture; an applied list is an array whose elements state the loopback address each aperture listens on. A Linux host refuses a list in which two apertures declare one host name, or any but the first declares localhost, as exec.aperture-host-listed-twice, because the child could reach only one of them by that name. A client that validates capability snapshots or resources against 0.20.0 must move to 0.21.0 to accept the new fact and record forms.

0.20.0​

0.20.0 added the host-process family (ADR 0036): seven routes under /v1/processes to start, list, read, stop, stop by owner session, renew the lease of and tail the log of an unconfined process the daemon supervises in a cgroup of its own. Every record says profile: host and confinement: none. A daemon publishes the optional process.host fact only when its delegated cgroup subtree can hold process cgroups, and serves these routes only on the personal Unix socket to a peer of its own uid. A client that validates capability snapshots or resources against 0.19.0 must move to 0.20.0 to accept the new fact and record.

0.19.0​

0.19.0 added the explicit workspace.adopt operation at POST /v1/workspaces/adopt, which serves a directory the operator placed under the workspace root, and two required read-only members on the workspace resource: origin and root_name. Workspace ids keep their ws_ form. A client that validates workspaces against 0.18.0 must move to 0.19.0 to accept the new members.

0.18.0​

0.18.0 made the exec.resource-usage.block_io fact a boolean: a host without the io controller publishes it false and omits io_read_bytes and io_write_bytes from every usage record instead of refusing execution usage. It also added the optional read_only_files start field, which binds single host files read-only, together with the exec.read-only-files fact and that field's named refusals.

0.17.0​

0.17.0 added explicit session capture selection, content-free output counters, capture-policy refusals and attachment ceilings; existing requests retain recorded output.