Skip to content

Latest commit

 

History

305 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

substrate

The b10x execution data plane. It turns one Linux host into a governed service for confined workspaces, bounded processes and sessions, durable operations, leases, and observed state. Workloads, images, volumes, endpoints and cluster drivers remain future resource families.

The problem it removes: anything that wants to run a command on behalf of somebody else has to build confinement, quotas, durable lifecycle state and honest reporting for itself, and usually builds the reporting optimistically. Substrate runs things and reports what it observed. Where the machine cannot confine, it says so — an exec on a host with no delegated cgroup answers exec.sandbox-unavailable rather than running unconfined.

It does not decide product policy, run agent loops, or understand connector vendors.

Public handbook · Run a local daemon · System model and derivation · Run a bounded command

Source: https://github.com/beyond10x/substrate/ · Security reports: https://github.com/beyond10x/substrate/security/advisories/new

Substrate is licensed under Apache-2.0, including all beyond10x-owned material in its reachable history. Third-party material retains its own licence. Public source does not make the development wire contract stable.

Where it sits

direction what
confines harness — over the daemon socket, directly or through the Rust SDK
may govern connectors — as a first-party provider, and later to isolate an attested connector artifact
may execute for autodev — over its Executor port
may adapt flux — a remote execution adapter over the substrate API. The dependency never points back into Flux

There is no sibling-component implementation dependency. Cross-component consumers use the released native substrate-daemon artifact or b10x-substrate-sdk. The SDK's opt-in linked mode may package the daemon solely to re-execute it as a separate child; resource operations still cross the authenticated Unix socket.

The product and binary name are substrate. Rust source packages use the b10x-substrate-* prefix and are consumed from a local path or exact Git revision; every workspace package is non-publishable.

Status

Release 0.7.3 (2026-09-05) ships keyless-signed daemon and disposable MCP images plus the signed 0.16.0 development contract bundle. Signed distribution does not make the contract stable.

Area Current behavior
Workspaces Empty or authorized configured HTTPS Git source; guarded file operations, exact-commit baseline and bounded change reads
Exec and sessions Confined argv execution, raw pipes and probe-gated PTY; explicit bounds, leases, output and optional exact usage
Recovery Durable operation reservation before dispatch, stored answers, bounded events and reconciliation snapshots
Transport Personal Unix peer identity or explicit-root TLS 1.3 HTTPS/WSS with online hosted Identity admission
Rust SDK and MCP Typed clients and bounded tools over the daemon's authenticated service contract
Capability limits Execution, PTY, Git, storage quotas and metrics depend on the running daemon's verified facts
Model and derivation Explicit wire types and frozen contract bundles; authored CLI, handlers and transitions, with no whole-system ESS derivation

Read the public status page for availability and trust limits. Inspect GET /v1/machine for the facts of the daemon you will actually use.

Per-area state with the exact next proof each is waiting for is STATUS.md; ordered exit criteria are ROADMAP.md.

Build, test, run

The gate is bash scripts/gate.sh. It is the full component gate; green here is the bar for main.

The table is the gate's own order (scripts/gate.sh).

step command
tests cargo test --workspace --release --locked
format cargo fmt --all --check
lint cargo clippy --workspace --all-targets --release --locked -- -D warnings
links cargo xtask check-links — rejects machine-local and broken repository-relative links
ADRs cargo xtask check-adrs
secrets cargo xtask check-secrets — scans every reachable commit, including root trees
dependencies cargo xtask check-advisories — rejects RustSec findings and HTTP/2
licences cargo xtask check-licenses — verifies Apache-2.0 workspace metadata and deterministic third-party notices
packages cargo xtask check-packages — refuses every publishable workspace package and verifies the five runtime source packages' fixed names, exact internal versions, SPDX metadata, READMEs and public documentation targets
contract bundle 0.1.0 python3 scripts/check-contract-bundle.py
contract bundle 0.2.0 python3 scripts/check-contract-bundle-0.2.0.py
contract bundle 0.3.0 python3 scripts/check-contract-bundle-0.3.0.py
contract bundle 0.4.0 python3 scripts/check-contract-bundle-0.4.0.py
contract bundles 0.5.0–0.16.0 cargo xtask check-bundles 0.5.0 … 0.16.0 — bounded parallel fixed-point, compatibility, classification and version-addition checks; check-bundle <version> remains the focused form
contract JSON cargo xtask check-json — every JSON under contracts/ is classified by exactly one bundled schema, or it fails closed
toolchain cargo xtask check-toolchain

Rust 1.97, edition 2024 — the toolchain is pinned by rust-toolchain.toml, and cargo xtask check-toolchain fails when it, Cargo.toml's rust-version and the Dockerfile builder tag disagree. .github/workflows/gate.yml runs the same gate on push and pull request.

Every check the gate runs is a cargo xtask verb — anything that runs in a b10x foundation repository is Rust. Two verbs are not gate steps because cargo test --workspace --release --locked, the gate's first step, already covers them:

verb what it does
cargo xtask package-bundle <version> --out <dir> packages a released bundle as a deterministic OCI image layout, so a consumer can pin one manifest digest
cargo xtask render-bundle <version> --out <dir> renders a bundle tree from substrate-wire and the authored source at xtask/bundle-source/<version>/; this is how a successor bundle is cut, and it refuses to write anywhere under contracts/

cargo xtask check-bundles <version>... runs the 0.5.0-and-later gate checks concurrently with a bounded worker count and deterministic reporting. Its focused check-bundle <version> form runs the same check for one bundle. Re-rendering and comparing bytes catches a released tree that has stopped being the fixed point of its own source, which a hand-written checker cannot see.

The four scripts/render-contract-bundle*.py and their check-contract-bundle*.py partners are not tooling — they are the reproducibility proof of the frozen 0.1.00.4.0 bundles, which are immutable, and 0.4.0's own generator.name points at one of them. They stay in Python and are not ported. render-bundle dispatches to the frozen renderer for 0.5.00.8.0 and the versioned multi-major renderer for 0.9.0 and later; a test asserts the original renderer still reproduces the frozen 0.4.0 byte for byte.

crates/substrate-daemon/tests/runtime_vectors.rs is the clean-room runner — an independent Unix-socket HTTP lane that spawns the shipped substrate-daemon binary and asserts only on the wire, linking no implementation. It has no gate step of its own because the gate's first step, cargo test --workspace --release --locked, runs it. bash scripts/delegated-lane.sh runs that lane, and needs no privilege: it asks systemd for a delegated scope (systemd-run --user -p Delegate=yes --scope), moves itself into a child group so the delegation root stays process-free, and sets the variable. A user session's own scope is root-owned, so trying to mkdir in it fails and the lane reports itself absent, not passed (invariant 3) — which reads exactly like a green run if you only look at cargo test.

Set SUBSTRATE_VECTORS_CGROUP_ROOT=<delegated-root> by hand instead, while the test process itself is inside that delegation, to add the real no-egress, shaped-environment, pids/memory, timeout, truncation and whole-tree cancellation cases:

cargo test --workspace --release --locked -- --nocapture   # the runner prints its case inventory

The runner prints its current portable or delegated case inventory from each fresh execution; this document deliberately pins no counts that drift as adversarial coverage grows.

cargo xtask check-json fails closed on unclassified or schema-invalid contract JSON and meta-validates every Draft 2020-12 schema offline, across all sixteen released bundles. Classification used to live in a Python module the four checkers imported — shared live machinery, not any one bundle's reproducibility proof — so it moved with the rest of the tooling, and the four checkers no longer do it. They verify everything else about the bundles they froze.

cargo xtask package-bundle <version> --out <dir> packages a released contract bundle as a deterministic OCI image layout — 0.4.0 reproduces manifest sha256:3758e80bc39f1eb03b15c69410608c9ef1d2ba8095c7e707c6988dbb5894ab00. It reads contracts/ read-only and is not a gate step of its own: its cases run in cargo test. On an eligible release tag, the release workflow copies that exact layout to GHCR with ORAS, confirms the remote digest, and signs and verifies the digest before announcement. The workflow's offline write-once and ordering assertions run in xtask/tests/release_workflow.rs.

Running the daemon

By default substrate-daemon serves an owner-permissioned Unix socket. Startup requires at least one explicit --allow-uid; the daemon derives local:<uid> from kernel peer credentials and never accepts a subject from HTTP data.

cargo build --workspace --locked
install -d -m 700 ./run
target/debug/substrate-daemon \
  --socket ./run/substrate.sock \
  --state ./run/state.db \
  --workspaces ./run/workspaces \
  --deployment personal \
  --event-retention 10000 \
  --allow-uid "$(id -u)"

Rust applications can instead follow the public Rust SDK guide to connect to that socket or supervise the daemon as a separate child.

The daemon image also provides /usr/local/bin/substrate-daemon-quota for explicit project-quota deployments. It is a root-owned, byte-identical copy carrying only cap_sys_admin=ep; the default executable and entrypoint have no file capabilities. Select the quota executable together with --project-quota-ids START-END, a filesystem that passes the hard byte/inode quota probe, UID/GID 65532, and only SYS_ADMIN in the container capability bounding set. Startup must allow file capability acquisition (no_new_privs disabled). Keep inheritable and ambient sets empty and never grant SYS_RESOURCE, which can bypass quotas. Deploy and roll back the image and selected executable together.

Release checks inspect the final image's bytes, ownership and file capabilities, then verify the daemon and Tokio worker masks under both startup profiles. They do not prove a served terminal backend or filesystem quotas: the image still requires a separately verified execution profile, and quota facts remain absent until the host probe succeeds. The process launcher continues to set PR_SET_NO_NEW_PRIVS before executing its trusted backend; this prevents gaining capabilities at exec, while an ordinary non-root exec with empty inheritable/ambient sets clears existing permitted/effective capabilities. PR_SET_NO_NEW_PRIVS alone does not clear capabilities at fork.

Container execution

The daemon image includes /usr/local/bin/substrate-container-exec for an explicit container execution profile. It requires Linux amd64, private PID/mount/cgroup namespaces, finite enclosing CPU and memory limits, the enforced substrate-host-exec-v1 AppArmor profile and the matching versioned seccomp profile. The application receives no host paths or host namespaces. Start the bootstrap as container PID 1 and UID/GID 0 with exactly CHOWN, SETGID, SETUID, SETPCAP and SYS_ADMIN after dropping all other capabilities. Supply a writable temporary directory and the existing private state/workspace volumes.

Pass ordinary daemon arguments after --; prepend --project-quotas when using the existing quota executable. The bootstrap verifies its enclosing resource controls, prepares only the container's private delegation, drops to UID/GID 65532 and starts the daemon beneath an init process that reaps orphaned children. Ordinary mode drops every capability; quota mode retains only the existing SYS_ADMIN file-capability path. The daemon's existing probes decide whether execution and PTY facts are present. Missing prerequisites remain startup or capability refusals.

Provision node profiles separately with the image's substrate-container-profiles executable. The installer needs only MAC_ADMIN, securityfs and a dedicated root-owned kubelet seccomp directory mounted at /node-profiles. It installs fixed versioned bytes and refuses conflicting files or an already-loaded profile without its exact retained source. --check verifies without writing; --hold supports deployment readiness and normal termination. Application pods never receive these mounts or installation authority.

The container-checker Docker build target contains a public-SDK example for final-image acceptance. Run its container-pty-check SOCKET executable as the admitted non-root UID inside the tested container's PID, mount and cgroup namespaces. It verifies file bytes, generated PTY input, resize, worker capability removal, live resource observations and whole-tree cleanup. Run both ordinary and quota variants, the existing delegated lane and image-startup checks before admitting terminals. The portable CI lane alone does not prove container execution.

Git workspace sources

--git-source <name>=<https-prefix>/ (repeatable) declares a segment-bounded Connector byte-plane source. A Git workspace request names that source and a broker-issued locator below the prefix, plus the provider's branch, exact 40-character commit and a depth from 1 through 50. The source authority arrives only in X-B10X-Workspace-Source-Authorization; it is never JSON, ledger state, argv, environment or an error. The configured prefix must end in / so an adjacent path cannot be mistaken for a child path.

The SDK's Workspace::read_git_file reads a complete baseline blob under a caller-declared byte bound, while Workspace::git_changes returns a path-sorted item/patch-byte-bounded comparison with the exact materialization commit. Current bytes still use read_file_v2; .git is deliberately absent from every file/tree API and remains available only inside the confined terminal.

Secret slots

--secret-slot <name>=<path> (repeatable) declares a secret the daemon holds for a run. The value reaches a child only as a sealed memfd at a declared descriptor — never in argv, never in the environment, never in an event, the ledger or an error body. The child gets the mapping and nothing else, through SUBSTRATE_SECRET_SLOTS=<name>=<fd>,…; the daemon closes its own copy immediately after spawn, and the seal set is F_SEAL_WRITE|F_SEAL_SHRINK|F_SEAL_GROW|F_SEAL_SEAL, which a child can confirm with fcntl(fd, F_GET_SEALS).

target/debug/substrate-daemon \
  --socket ./run/substrate.sock \
  --state ./run/state.db \
  --workspaces ./run/workspaces \
  --deployment personal \
  --allow-uid "$(id -u)" \
  --secret-slot model_key=/etc/substrate/model-key

A slot name is lowercase ASCII, digits and _, first character a letter, at most 64 bytes (crates/substrate-wire/src/lib.rs:1766) — a hyphen is refused. The path never leaves the daemon process — it is not a capability fact, not an event field and not an error message. An error may name a slot; it never names a value. Rotating the file behind a declared name needs no restart and invalidates no admitted operation. The ledger request hash covers slot names only, so two requests differing only in a slot's value hash identically. Where sealing is unavailable the capability fact secrets.slots is absent and the operation is refused by name — it never degrades to passing the value some other way (invariant 3). ADR 0012.

Egress apertures

Ordinary execution has no egress and that does not move: every run is under --unshare-net in a namespace with loopback and nothing else. An aperture is a separate, operator-declared authority to reach exactly one destination — --egress-aperture <name>=<host>:<port>/tcp[/max=<size>], repeatable. A request selects one by name and can never carry a destination or a ceiling:

target/debug/substrate-daemon \
  --socket ./run/substrate.sock \
  --state ./run/state.db \
  --workspaces ./run/workspaces \
  --deployment personal \
  --allow-uid "$(id -u)" \
  --cgroup-root /sys/fs/cgroup/…/substrate \
  --egress-aperture model=api.example.com:443/tcp \
  --ca-bundle /etc/ssl/certs/ca-certificates.crt
{ "sandbox": { "network": "aperture", "aperture": "model", "profile": "workspace", "…": "" } }

The host is resolved once, at declaration, and pinned; the sandbox gets no resolver and performs no lookup. Inside the run, the declared name maps to loopback through a generated read-only /etc/hosts and the forwarder listens on the declared port, so https://api.example.com/… is the URL a child uses unchanged — and, where --ca-bundle is configured, verifies against a private per-run snapshot of that anchor. The pinned address itself is not reachable directly: the aperture is the only peer in the namespace.

What was installed is reported rather than inferred — applied.network becomes {mode, name, destination, mechanism, bytes, max_bytes}, with the address the forwarder actually dialled and the bytes counted where they crossed. An aperture nobody declared is unserved with the name in the message; where the mechanism did not verify in a throwaway sandbox at startup, the capability fact exec.egress-apertures is absent and every aperture request is unserved — never a run that quietly got no network instead (invariant 3). ADR 0013.

The optional /max=<size> term bounds how much may cross, over both directions summed, for one run — 1048576, 512KiB, 64MiB, 2GiB, and never a decimal-power unit such as MB. An unrecognised term is a startup error, not an ignored one, and an aperture declared without the term passes exactly what it passed before. The relay stops relaying at the ceiling, so the total may exceed it by at most one 16 KiB relay buffer per live connection; the run is then ended and the observation carries refusal: {class: "exhausted", code: "exec.aperture-byte-limit", …} beside a state of cancelled. The child is told nothing — its socket closes mid-stream and the tree is killed. A ceiling in a request is refused exec.aperture-ceiling-in-request. ADR 0014.

Grant attribution

--delegated-context-key <kid>=<issuer>=<base64url> (repeatable) declares a key substrate will verify delegated-context documents against. Substrate holds a verifying key and never a signing key: which service signs is a configuration of the trusted key and changes no substrate code.

A start may then carry delegated_context, a compact JWS, alongside op and input — never inside input, so it stays outside the canonical request hash. Replaying the same op with a fresh context is the same operation and returns the original outcome, and a request without one serializes exactly as a 0.6.0 client's did.

substrate-daemon \
  --delegated-context-key k1=https://identity.example.com=g5Iv…A0

What a verified document contributes is two columns and nothing else: grant_ref and platform_principal, on the ledger row and on the operation.* events. Substrate verifies signature, issuer, exact audience, time window and binding to the authenticated subject; it never evaluates the grant — connectors decides, substrate records. Identity-shaped strings a caller may legitimately write reach the resource and never the attribution; writing one into the envelope is request.schema-invalid, not a quiet drop. Every failure is a named refusal, never a weaker run: delegated-context.absent, .malformed, .unknown-key, .signature-invalid, .audience-mismatch, .subject-mismatch, .expired, .grant-conflict (crates/substrate-daemon/src/delegation.rs:209-256). ADR 0011.

Serving exec

Without a delegated cgroup root, workspace operations are still served, exec confinement facts are absent, and exec admission answers exec.sandbox-unavailable. A Linux deployment that serves exec must:

  1. place the daemon in a delegated cgroup subtree carrying cpu, memory and pids;
  2. keep the delegation root itself process-free — for example systemd Delegate=yes plus DelegateSubgroup=daemon;
  3. provide the configured bubblewrap binary and /usr/bin/socat, which the runtime probe uses to prove that the seccomp profile denies host Unix-socket access. That bubblewrap must accept --disable-userns and --assert-userns-disabled, because the probe asks for a non-nestable user namespace and then makes bubblewrap assert it rather than trusting the option took. A backend without those options fails the probe, so exec confinement facts are absent and every exec is refused exec.sandbox-unavailable, exactly as with a missing delegated cgroup. Check with bwrap --help; 0.11.2 is the version this was measured against, and no lower bound has been established here;
  4. pass that root through --cgroup-root.

The runtime probe enables and tests the controllers, bubblewrap namespaces, cgroup kill and the swap-inclusive memory bound before it advertises exec.

The TCP transport is development-only

The static-bearer TCP transport is enabled only as an explicitly acknowledged development profile (--tcp-development-only --tcp-private-overlay) and now refuses every non-loopback bind. It requires a bounded bearer file plus deployment-owned --tcp-subject and --tcp-actor bindings. The daemon opens that file once, bounds it to 512 bytes, and admits either an owner-private workload file or a root-owned, group-readable projected Secret with no group write/execute and no world access.

This static bearer does not satisfy the accepted scoped, expiring, rotating hosted trust-envelope profile, and must not be published through external or shared ingress. A hosted container without a delegated cgroup or bubblewrap environment continues to report execution sandbox unavailability rather than weakening confinement.

Production TLS transport

Current source can bind a distinct TLS 1.3 HTTPS/WSS listener:

substrate-daemon \
  --socket /run/substrate/local.sock \
  --state /var/lib/substrate/state.sqlite \
  --workspaces /var/lib/substrate/workspaces \
  --deployment edge-01 \
  --tls-listen 0.0.0.0:8443 \
  --tls-certificate-chain /run/substrate-tls/chain.pem \
  --tls-private-key /run/substrate-tls/key.pem \
  --hosted-identity-origin https://identity.example.com \
  --hosted-identity-ca-bundle /run/substrate-identity/ca.pem

The certificate and key paths must be non-empty regular files rather than symlinks. The key must belong to the daemon's effective user and have no group or other permission bits. The daemon checks certificate validity and certificate/key agreement before binding, negotiates only TLS 1.3 and HTTP/1.1, and never trusts Forwarded, X-Forwarded-*, or caller-written identity headers.

Replace both files completely and send SIGHUP to rotate them. A complete valid pair becomes the snapshot for new connections; existing connections retain their admitted snapshot. An invalid replacement is logged only as tls.reload-invalid, and the last valid identity keeps serving.

The production listener also authenticates every caller by resolving an opaque five-minute identity_access_v1_… bearer at Identity's GET /v1/access-authority endpoint. Resolution uses the exact urn:b10x:substrate audience, the explicit CA roots above, direct HTTPS with no redirects or proxy, a five-second deadline and a 64 KiB response bound. observe, workspaces and exec are checked against the addressed route before any handler can reserve a durable operation. Missing, invalid, under-scoped and temporarily unresolvable authority answers auth.credential-absent, auth.authority-invalid, auth.scope-denied or auth.authority-unavailable; there is no cached or caller-written fallback. There is no production plaintext fallback and no verification-disable flag.

The Rust SDK addresses this listener only when the caller supplies the exact HTTPS origin, PEM trust roots, expected DNS identity and an asynchronous Identity access-token provider. It uses the same TLS 1.3 configuration for HTTP and WSS, refreshes once only after a named authentication 401, and mints a fresh one-use attachment authority for every hosted session connection. See the public Rust SDK guide for a complete builder example.

What is enforced

area enforced
filesystem openat2 beneath / no-link / no-mount I/O, atomic replacement, symlink escape refusal
process cleared and shaped environment, namespace no-egress, pids and memory-plus-swap bounds, cumulatively observed CPU bounds, timeout, whole-tree kill
capsules exact capsule-byte verification, read-only /runtime, separate writable /workspace, bounded normal and restart cleanup
output both stdout and stderr drained continuously while a process runs, bounded captures retained, persisted when the exec is observed, ranged reads exposed
durability terminal observations and output stay in memory until the durable store acknowledges them; maintenance cannot regress a durable terminal state
concurrency blocking filesystem and SQLite work runs in separate bounded lanes, so saturation backpressures callers without starving asynchronous service

Substrate reports the applied capsule identity. It does not claim the host interpreter, libraries or base system as part of that closure.

Git materialization is conditional on a configured HTTPS source and transient Connector authority. It checks out the exact provider commit and exposes bounded baseline and change reads. General workspace backup/restore snapshots remain absent.

Layout

crate owns
crates/substrate-wire the closed Rust representation of the wire; subordinate to the contract bundle, never the other way round
crates/substrate-store durable operation and resource state
crates/substrate-host the Linux host driver
crates/substrate-daemon the standalone HTTP daemon: DaemonConfig plus the async serve entrypoint
crates/substrate-contract-check the offline contract checker
path holds
contracts/substrate-wire/ the canonical wire bundles, one directory per version; earlier bundles are immutable
architecture/ the accepted system boundary and dependency direction
docs/design/ wire, driver, lifecycle, security, session and trust design; each document states whether it is accepted or under review
docs/plan/ design turned into review gates and implementation slices, without implementation
.engineering/planning/ the plan: epics and stories as governed artifacts, read with protocol artifact list / board
adr/ accepted component decisions, with YAML frontmatter
scripts/ gate.sh and the checks it runs

Read more

Start here, in order:

  1. Vision
  2. Architecture overview
  3. Domain model
  4. Stack integration
  5. API contract
  6. Specification bundle and minimum wire
  7. Roadmap

Also: glossary.md, STATUS.md, CHANGELOG.md, and AGENTS.md for the working agreements and invariants.

Documentation

Substrate documentation · Start · Ecosystem · Impact · Releases

About

Confined execution with explicit capabilities, durable operations, and observed outcomes.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages