Workload runtime contract
The workload runtime contract is the promise a workload may rely on. It names the process a workload is started as and the filesystem and network it is given. It sets the rules by which the workload is restarted or stopped, and the bytes it exchanges on the frame bus. The frame bus is the on-board message channel between partitions, the spacecraft's flight software, and the ground link. A workload in C, C++, Python, Rust or any other language implements the contract directly, and the frame bus byte format is below.
You can check what holds today on a running node. A workload may not depend on the contract being built yet. Paths and environment-variable names become ABI the day the contract is published, so do not hard-code them before then.
What holds today
These are the parts a workload already meets. Each is a property of the runner and the launch it performs.
A workload is one service in a partition. It is declared in
space.yaml, under a partition whose supervisor
starts it, and the same entry carries its runtime placement.
The isolation boundary follows the runtime. A tenant is one project's
partition. Each workload runs in a machine no other tenant shares. That
machine is the node itself when the booted release names a single tenant.
Otherwise it is a Firecracker virtual machine the runner boots for that tenant,
as tenant machines describes. Inside
that machine the workload runs behind the isolation
backend its service names. A build that pairs a
runtime with a backend that cannot hold it is refused at build time. Host
isolation is the exception a node must opt into. A host-isolated service runs
uncontained, so the runner refuses to launch one unless it was started with
--allow-host, and the refusal names the service. That flag is meant for
development machines only.
The process comes from the image, and the composition may override it. An OCI service takes its entrypoint, arguments, and working directory from the image and runs with them. Environment entries declared on the service override entries of the same name in the image's configuration; entries the image declares and the service does not are kept.
Configuration keys beginning space. belong to the product. The
supervisor reads them and does not pass them to the process. They carry the
entrypoint and arguments, the working directory, the data paths the runner
backs with storage, credential bindings, and settings. A workload's own keys
share the same map and must not use that prefix.
Each workload gets its own input and output root. They are
/run/space-runner/inputs/NAME for what it runs over and
/run/space-runner/outputs/NAME for what it leaves, where NAME is the
workload's name. NAME must be a single path segment. It is not empty, holds
no / and no NUL byte, and is neither . nor ... Any other name is refused,
never rewritten, so one workload cannot be given another's root.
The process supervisor does not retry launch errors. For process and
bgprocess services, it retries exits except codes 1, 60, 61, 62,
63, and 64. Solo5 and Functoria use these codes for launch errors, help,
version, and argument parsing; Functoria is the MirageOS configuration
framework that generates a unikernel's entry point and command line, and its
runtime exits 63 after printing help or a version and 64 on an argument
error. Retrying cannot correct the launch. Exit code 0 is retried because
the service was declared to stay up. A oneshot service is never retried,
whatever its exit code, and a process killed by a signal is never restarted.
The n-th restart in a row, counting from n = 0, waits 2^n seconds times a random factor between 1 and 2, capped at 60 s. A process that stays up for 300 s resets the count. After 9 launches with no such 300 s window, the supervisor stops and marks the service exited. The 9 waits between those launches add up to between 243 s (1 + 2 + 4 + 8 + 16 + 32 + 3 × 60) and 302 s.
Health is evaluated from outside the guest. The runner runs a healthcheck
declared on the service, and a workload does not have to serve anything to be
supervised. A probe is a TCP connection to a guest port, an HTTP GET that
must answer 2xx, or a command run inside a container that must exit 0. The
runner waits start_period, probes every interval, counts a probe slower
than timeout as failed, and marks the service unhealthy after retries
failures in a row. The healthcheck fields
give the defaults.
A workload has only the interfaces it declares. It reaches the frame bus, an IP bridge, the node network, a high-rate data link, or the supervision page only when its service entry declares that interface. An undeclared interface is absent.
Frame bus format
A workload that declares the frame bus (interfaces: [{type: frame_bus}])
finds a Unix socket path in the SPACE_NET_SOCKET environment variable. Read
the path from the variable, and connect to it as an AF_UNIX SOCK_STREAM
client. Each tenant has its own socket.
The stream carries frames back to back, with no length prefix and no delimiter. Every frame is exactly 256 bytes: an 8-byte header and a 248-byte payload. Read and write whole 256-byte units. Multi-byte fields are big-endian.
| Offset | Width | Field | Value |
|---|---|---|---|
| 0 | 1 byte | version | 1, the only version the runtime writes and reads |
| 1 | 1 byte | msg_type | A message kind from the table below |
| 2 | 2 bytes | apid | Source APID, 0 to 2047 (0x7FF) |
| 4 | 2 bytes | payload_length | Meaningful payload bytes, 0 to 248 |
| 6 | 2 bytes | dest_apid | Destination APID, 0 to 2047; the relay routes on it |
| 8 | 248 bytes | payload | payload_length bytes, then zeros to the end of the frame |
An APID is the 11-bit application process identifier CCSDS gives each stream. A frame that breaks any rule in the table does not decode.
A telemetry frame from APID 0x010 to APID 0x001 carrying the two bytes
hi starts with the ten bytes 01 00 00 10 00 02 00 01 68 69, and 246 zero
bytes complete it.
The msg_type byte takes one of eleven codes. Any other value does not decode.
| Code | Kind | Carries |
|---|---|---|
0x00 | TM | Telemetry |
0x01 | TC | A telecommand |
0x02 | EVR | An event report |
0x03 | PRM_GET | A parameter read request |
0x04 | PRM_SET | A parameter write request |
0x05 | PRM_RSP | A parameter response |
0x06 | DP | A data product |
0x07 | HEALTH | A health report |
0x08 | LOG | A log line |
0x09 | ERROR | A refusal of an earlier frame |
0x0A | SEG | One segment of a message larger than 248 bytes |
An ERROR payload is 8 bytes. The error code is at offset 0 and the refused
frame's msg_type at offset 1. The refused frame's source APID is at offset 2
and its payload_length at offset 4, and two zero bytes follow. The error
codes are
1 unknown type, 2 unknown version, 3 message too large, 4 APID not
allocated, 5 host busy, and 6 malformed.
A SEG payload opens with a 4-byte header. Offset 0 holds the kind the
message reassembles into, and offset 1 the segment count (1 to 40). Offset 2
holds the segment index, and offset 3 the segment's payload length. The
segment's bytes
follow at offset 4. Every segment except the last carries exactly 244 bytes.
Segments arrive in index order, and a transfer with a missing segment is
refused whole.
The frame format carries no version negotiation yet. A frame whose version
byte is not 1 does not decode.
An OCaml workload can read and write these frames through the OCaml library, which binds this format.
The contract being built
WIP Publishing the contract means fixing all of the following together and versioning them as one. Until that lands, each row is a thing the runtime does in some particular way that a workload must not assume will stay that way.
| Area | What the published contract will fix |
|---|---|
| Contract version | A version the release carries and the workload can read |
| Runtime | The container, unikernel, and host execution classes it covers |
| Process | Entrypoint, arguments, working directory, user, groups, and signals |
| Environment | Every reserved variable, its producer, and whether a workload may override it |
| Filesystem | Read-only and writable paths, mount types, ownership, modes, and lifetime |
| Network | Namespace, interfaces, socket types, peer identity, and failure behavior |
| Frame ABI | Framing, byte order, field ranges, short-read handling, and version negotiation |
| Configuration | Grammar, required and optional values, provenance, and update rules |
| Credentials | Authorization, source, mount path, ownership, mode, rotation, and cleanup |
| Resources | CPU, memory, process, storage, and traffic limits |
| Lifecycle | Start ordering, readiness, health, exit handling, restart limits, and shutdown |
| Compatibility | Which older and newer contract versions the runtime accepts |
A published contract is also a tested one. The release gate has to launch representative workloads against the installed release artifact and assert each row at the process boundary. It has to assert these refusals and observations too.
- A workload that did not ask for a bus endpoint has none.
- A workload has no access to another workload's endpoint.
- Malformed and unsupported frame versions are refused.
- Missing configuration or credentials are refused before the process starts.
- Authority outside the admitted policy is refused.
- The isolation controls are observed on the running process.
- Exit and restart behavior match the declared lifecycle.
- No secret appears in a log, a process argument, or release metadata.
Limits of each control
These distinctions hold now and will hold in the published contract. Each one is a control that is easy to read as a stronger guarantee than it is.
- A Unix socket path is routing information. It is not authentication and it does not encrypt traffic.
- A host-assigned endpoint can supply workload identity to a local policy check. That property holds only where another workload cannot acquire or address the endpoint. Test that condition.
- A read-only root filesystem limits writes after launch. It does not authenticate the filesystem bytes. The image's own signed evidence and its verified root authenticate them.
- File mode
0400limits access by ordinary process credentials. It does not protect a file from the node kernel or a node administrator. - A user namespace, a seccomp filter, a capability set, and a cgroup are container controls. They are not a hardware virtual-machine boundary, which is what a Solo5 tender or Firecracker gives a unikernel. A tender is the host program that creates the sandbox and loads a Solo5 unikernel into it, and Firecracker is a virtual machine monitor on KVM that boots small virtual machines with a reduced device model.
- A network namespace isolates kernel network state. It does not authenticate or encrypt application traffic.
- Delivering a key file does not say where the key came from, how it was sealed, or whether plaintext copies were removed.
Local development execution does not admit a workload to flight. The trust root and the admission check of the released runtime admit it, and the published contract will name both. Today the runner on a node admits launch bytes through the signed metadata chain that node admission describes, from root keys pinned when the node is provisioned, and it checks the partition's signed capability envelope before it starts a service.