Skip to content

space.yaml reference

space.yaml is the one file you write beside your Dockerfile. It declares where the built image lands, which partitions the system boots, what runs inside each one, and how frames reach the ground. space build reads it when you name no source, space run boots what it describes, and space show --sbom lists what it contains without building anything.

The tables below list every member the space.project schema decodes, with its default. A member the schema does not declare is refused by name, so a mistyped field fails the load. A row with a WIP badge is a member the decoder accepts and the rest of the product does not act on, and the row says what does not happen.

The file has two sections. build: says where the image lands, which artifact manifests it imports, and what disk geometry it is baked to. run: describes the application itself: its partitions, the services inside them, and the frame topology that carries their output. Everything that differs between machines, such as the board, the keys, and how the machine is reached, belongs to the target and is written in targets/NAME.yaml. One space.yaml therefore runs on every target, and each target completes it from its own profile.

Example

schema: space.project
version: "1"
build:
output: _build/spaceos
storage:
blocks: 256
params:
- id: 1
value: boot_count=0
- id: 2
value: mode=nominal
run:
partitions:
bus-relay:
memory: 128
apids: [0x001, 0x00F]
payload:
init: pid1
disk: true
memory: 256
apids: [0x010, 0x01F]
services:
- name: eo-classifier
image: ghcr.io/<your-org>/eo-classifier:latest
runtime: oci
isolation: runc
cpus: [0, 1]
interfaces:
- type: ip
bridges: [payload-net]
- type: frame_bus
apids: [0x010, 0x01F]
send_to: [sdls-downlink]
healthcheck:
type: command
argv: [/usr/local/bin/healthcheck]
- name: sdls-downlink
image: ghcr.io/parsimoni-labs/sdls-downlink:latest
runtime: oci
isolation: runc
depends_on: [eo-classifier]
relay:
from: bus-relay
to: [payload, ground]
ground:
port: 8080
socket: /tmp/spaceos/ground.sock

This example spells out several members that would otherwise default, so that one document shows them together.

bus-relay declares no services. It owns the APID range 0x001 to 0x00F and is the relay's source, run.relay.from, so its frames are the ones the relay forwards to payload and to the ground. A partition that is not the relay's source originates frames to no other partition and to no ground. payload runs the classifier and the sdls-downlink service beside it, which the classifier's send_to names.

space show --sbom reads the project without building it, so it is the shortest way to check that a space.yaml resolves. The header names the kernel each partition boots and the init the supervisor runs:

Terminal window
$ space show --sbom | sed -n '1,4p'
No signed SBOM at _build/spaceos/spaceos.spdx.json yet; printing the SBOM computed from space.yaml
SpaceOS Software Bill of Materials
kernel ghcr.io/parsimoni-labs/uniboot-kernel/node:6.18.39-62332cf6bcc6@sha256:21e9b2669e2347dd0a9b7703a248bf191ef91e412ae6b720f3d3768f6cfbbd8f

The package inventory follows, one row per OCI layer and OCaml package the project carries. A member the schema does not declare is refused by space show --sbom, naming the member.

space run --dry-run resolves the placement and prints the boot plan without starting anything:

Terminal window
$ space run --dry-run
Local target: macos-vmm@sha256:4c549a833d4e9801bdc6f6e55485e64b23d5d33e0190ba713a20131f114a65da+vz
Boot image: not validated (not in the local store: ghcr.io/parsimoni-labs/uniboot-kernel/node:6.18.39-62332cf6bcc6@sha256:21e9b2669e2347dd0a9b7703a248bf191ef91e412ae6b720f3d3768f6cfbbd8f); build first
image: _build/spaceos
partitions:
bus-relay 128 MB apids 0x001, 0x00f
payload 256 MB apids 0x010, 0x01f
services:
eo-classifier runc cpus 0, 1
sdls-downlink runc
relay: bus-relay -> payload, ground (sdls)
ground: port 8080 socket /tmp/spaceos/ground.sock
2 partitions, 384 MB total
Dry run complete; no workload started.

Boot image: not validated means the image is not in the local store yet. Run space build before starting the workload.

Top-level fields

FieldTypeRequiredDescription
schemastringyesMust be space.project. Another schema name is refused.
versionstringyesSchema version, "1".
buildmapnoWhere the image lands and what it imports. Every member defaults.
runmapyesThe application: its partitions, their services, and the frame topology.

Unknown fields are rejected.

Build fields

The Dockerfile says how each service image is built. build: says only where the system image lands, what it imports, and what shape it is baked to.

FieldTypeRequiredDescription
build.outputpathno; default _build/spaceosDirectory where build artifacts are written. space run boots what this directory holds, so the project states the image once.
build.artifactsmapnoImported app artifact manifests, each with a manifest path and optional digest. A service references one imported output with artifact: <name>#<output>.
build.storage.blocksintno; default 256Persistent storage, counted in 512-byte disk sectors. The partition is rounded up to a whole mebibyte and is never smaller than one, so the default asks for 256 × 512 bytes, 128 KiB, and gets 1 MiB. A signed capability grants storage in 4 KiB blocks instead, so one granted block is eight of these sectors.
build.storage.paramslistnoParameter store entries that persist across reboots.
build.storage.ab_slotsbooleanno; default falseWIP Reserves storage for a second bootable system release. The decoder accepts it and the project records it, and image assembly bakes no second slot: a node trials a new system release through its content store.

build.kernel and build.target_profile are refused by name, and so is a kernel under a partition. A kernel and a target profile describe a machine, so a project that named one would run on exactly one target. The refusal says which member it was and sends you to targets/NAME.yaml, where the target manifest carries the board.

Each item under build.storage.params:

FieldTypeRequiredDescription
idintyesInteger identifier. Must be unique within build.storage.params.
valuestringyesString in key=value format. The supervisor reads this as the wire-format representation; parsing is the workload's responsibility.

Run fields

FieldTypeRequiredDescription
run.partitionsmapyesThe partitions this project boots, keyed by name. At least one is required.
run.relaymapnoFrame route, queue limits, and authentication settings.
run.groundmapnoLocal ground-service endpoint.
run.bindingsmapnoWIP What this project supplies for each accelerator the application requires, under bindings.accelerators, keyed by the role the app declared: a content-addressed model or bitstream, and requested_limits. The decoder takes it and checks it against the app's requirements; realizing the flow through to the device is next-release work.
run.acquisitionmapnoThe imagery this project wants, and when: area as west, south, east and north in degrees, from as an RFC 3339 instant, and hours of window (default 24). A run on the local engine ignores it; a run bought from an operator that owns spacecraft is where it is read.
run.resultsstringnoDirectory a run writes its results into. Naming it empty is refused; omit it instead. It is spelled results because build.output already names where the build writes.

Partition fields

FieldTypeRequiredDescription
run.partitions.<name>.initstringno; default pid1Partition init and supervisor. It starts services, mediates frame-bus access, and reports lifecycle and health. Override only when a workload brings its own supervision.
run.partitions.<name>.diskbooleanno; default enabled when the partition declares servicesPersistent block storage for this partition, backed by the build.storage section.
run.partitions.<name>.memoryintno; default 256Memory assigned to the partition, in MiB.
run.partitions.<name>.apidstwo-item int listno; default []Inclusive CCSDS APID range, from 0x000 to 0x7FF. Example: [0x010, 0x01F].
run.partitions.<name>.fileslistnoLocal files or directories embedded into the partition initramfs. Each entry has source and image-root path.
run.partitions.<name>.requires.frame_buslist of stringno; default []Bus endpoints this partition needs the node to provide. A service's send_to names either a service of this partition or an endpoint listed here; a name in neither is refused as a misspelling.
run.partitions.<name>.serviceslistno; default []The workloads the partition supervisor manages.

Service fields

Each item under run.partitions.<name>.services:

FieldTypeRequiredDescription
namestringyesUnique identifier within the partition.
imageOCI ref or Dockerfile pathconditionalA prebuilt service image, or a local Dockerfile (or a directory holding one) that the build resolves under the project root and builds for you. Use this or artifact, not both.
artifact<manifest>#<output>conditionalReference to an imported app artifact output. Use this or image, not both.
runtimeoci, mirage, unikraft, host, or autorequired with imageService payload runtime. Artifact references derive this from the artifact manifest.
isolationstringno; default autoThe isolation backend the service runs behind. auto resolves from the runtime: runc for oci, host for host.
cpuslist of intno; default []CPU IDs the service is pinned to. An empty list leaves the placement to the runner.
memory_mbintno; default 128Memory cap for the service.
interfaceslistnoTyped service interfaces: type: ip with bridges; type: frame_bus with apids and send_to; type: host_network with bind_tcp; type: data_link with link, mbps and mode; type: supervision_page. See service interfaces.
bridgeslist of stringnoShorthand for interfaces: [{type: ip, bridges: [...]}]. A bridge is a named virtual network segment.
lifecycleprocess, bgprocess, oneshot, or internalno; default processHow the supervisor runs the service: long-running, self-daemonizing, one-shot, or a grouping node.
environmentmap of stringnoProcess environment for the service. Entries override the ones the OCI image declares in its own Config.Env.
configmap of stringnoRuntime configuration read by the supervisor, not passed to the process. The space. prefix belongs to the product: it carries the entrypoint, working directory, data paths, credential bindings, and settings the runner lowers into a launch.
depends_onlist of stringnoService names that must launch before this one. Launch-order only: this is not a health gate. Circular dependencies are rejected at build time.
healthcheckmapnoApp-declared liveness check the runner evaluates from outside the guest. Omit for services without a health requirement.

Service interfaces

interfaces is a list of tagged objects. The type field selects the arm, and there are five:

TypeFieldsGrants
ipbridgesAddresses on each named bridge, and connections to the peers the signed release lists.
frame_busapids, send_toA tenant socket on the frame bus, the APID range the service owns, and the peer services it may send frames to. ground is reserved and refused here: ground is reached through run.relay.
host_networkbind_tcpTCP ports published from the node into this service. It changes the container's network isolation, so a service may declare it once, only under runc, and never alongside an ip arm.
data_linklink, mbps, modeAttachment to one named high-throughput link, admitted against the links the signed release grants. mbps is the reserved rate; mode is stream or rmap. All three are required.
supervision_pagenoneThe shared page carrying heartbeat, mission time, health, and the safe-mode word.

bridges at the top of a service entry is shorthand for a single ip arm.

Healthcheck

healthcheck is a tagged object. The type field selects the probe:

TypeRequired fieldsDescription
tcpportHealthy when the guest accepts a TCP connection on the port.
httpport, optional path (default /)Healthy when an HTTP GET returns a 2xx response.
commandargvHealthy when the command exits successfully. argv must not be empty.

All probe types also accept interval (default 10 s), timeout (5 s), retries (3), and start_period (0 s), the defaults of Docker's HEALTHCHECK. The runtime probes from outside the guest every interval seconds, and a probe that takes longer than timeout counts as failed. Failures during the first start_period seconds do not count. After that, retries consecutive failures mark the service unhealthy. A service that stops answering is therefore marked unhealthy after about retries × interval seconds, 30 s with the defaults.

Relay fields

If run.relay is omitted, from is the first partition and to contains the remaining partitions plus ground.

FieldTypeRequiredDescription
run.relay.fromstringnoSource partition. ground is not valid here.
run.relay.tolist of stringnoDestination partitions and ground. A name may appear once. Telecommands use the reverse route.
run.relay.limits.rateintno; default 1000Frames admitted per second from the source. 0 disables the rate limit.
run.relay.limits.burstintno; default 64Capacity, in frames, of the source's token bucket: the largest burst admitted at once.
run.relay.limits.queue_depthintno; default 256Frames held in each egress queue.
run.relay.limits.drop_policyenumno; default drop-oldestBehavior when an egress queue is full: block pauses the source, drop-new discards the arriving frame, and drop-oldest discards the frame at the head of the queue.

The limits are a token bucket counted in 256-byte frames. The bucket holds burst tokens and refills at rate tokens per second, and each admitted frame spends one. A source therefore sends at most rate × 256 bytes per second sustained, 256,000 bytes per second with the defaults, and at most burst frames, 16 KiB, at once. Every relay target shares the one source, so block on any full queue pauses delivery to every target. Every discarded frame is recorded in the relay's security log.

Key material is provisioned at runtime and is never stored in space.yaml. The fields under run.relay.security select the association it is provisioned for:

FieldTypeRequiredDescription
spiintno; default 1SDLS Security Parameter Index, from 0 to 65535. Each frame's security header carries it, and the receiver uses it to select the security association: the key, the cipher suite, and the sequence counter.
scidintno; default 0Spacecraft ID.
vcidintno; default 0Virtual channel ID.
acsstringno; default aes-256-cmacAuthentication cipher suite: one of aes-256-cmac, hmac-sha-256, hmac-sha-384 or hmac-sha-512. Any other name is refused.
sn_lenintno; default 4Sequence-number length in bytes.
arsnwintno; default 128Anti-replay window size: how far below the highest sequence number received a frame's sequence number may be and still be accepted, once.
authenticationenumno; default sdlssdls is the only value, and the relay authenticates every frame.

Ground fields

The run.ground block configures local execution. space deploy uses the ground recorded for the connected target.

FieldTypeRequiredDescription
run.ground.portintno; default 8080Local ground-service TCP port.
run.ground.socketstringnoLocal ground-service Unix socket. The path is bound as written, so it must be absolute. A project that names none gets one under the run's own runtime directory.

Partition supervisor

Each partition runs a single supervisor process as PID 1, the init field, pid1 by default. Its responsibilities:

  • Start the partition's services in order, honoring depends_on.
  • Mediate each service's access to the frame bus.
  • Surface the declared per-service healthchecks.
  • Own the guest half of the supervision page.

Set init to a custom binary only when a workload brings its own supervision. Custom supervisors must implement the partition supervisor contract.

Build output

space build writes the project's artifacts and an SBOM to the build.output directory, then registers the composition in the local OCI store under the tag passed with -t:

_build/spaceos/
kernel/ # resolved partition kernels
initramfs/ # partition initramfs images
disks/ # rootfs / storage artifacts
spaceos.spdx.json # software bill of materials (SPDX)

Every build also appends one provenance row to the host build record, giving the tag, image digest, source, completion time, status, and signing fingerprint; a failed build is recorded there too.

Service image sources

Every service is an OCI image. There are two ways to produce one:

  • An app manifest. Write an app.yaml (kind: mirage, unikraft, or host) and run space build app.yaml. The build is pinned and runs in the disposable builder VM. It writes the OCI artifact and an artifact manifest that space.yaml imports with artifact:. The build needs no local toolchain and no Docker daemon.
  • Any OCI image. A service image: is a plain OCI reference. An image built and pushed with standard tools (docker build, buildah) works. A unikernel binary packaged as FROM scratch plus the binary is a valid unikernel service image.

While assembling the system image, space build detects each service's runtime, records it in the image annotations, and signs the project.

Multi-architecture images

A service image may be a standard OCI image index carrying more than one architecture. Build one with standard tooling:

Terminal window
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t ghcr.io/<your-org>/cloud-mask:latest \
--push \
.

One space build assembles a project for one architecture. Run it once per architecture you intend to ship, and publish each result under its own tag.

Validation rules

  • Partition and service names are free-form identifiers.
  • ground is reserved and cannot be used as a partition name.
  • run.relay is the only place to define the frame route. A partition declares no egress list of its own.
  • A send_to must name a service of the same partition or an endpoint the partition lists under requires.frame_bus.
  • OCI images are pulled once and cached locally. When you need a fresh artifact, use an immutable digest or publish a new tag, then build again.

Files space no longer reads

A project directory that holds build.yaml or run.yaml is refused by every command that reads a project. space run in a directory holding build.yaml prints space run: build.yaml is not read any more. This project's build and run settings live in one space.yaml. and stops.

To move such a project across, write both documents into one space.yaml and delete the old files:

  • What build.yaml declared becomes the build: section, which says where the image lands and what it imports.
  • What run.yaml declared becomes the run: section. partitions, relay, and ground keep their members and defaults, and each service carries its build source and its placement in one entry.
  • A kernel: or a target_profile: moves to the target manifest, targets/NAME.yaml, described in Supported targets. The board, the keys, and the route to a machine belong to the target.
  • An image path in run.yaml goes away: space run boots what build.output holds, and space run IMAGE names another image.

space run --dry-run then prints the placement the new file resolves to, and space show --sbom lists what a build of it contains.