|**Implements**| a subset of `docs/spec.md` (Draft 0.1) |
7
|**Goal**| A push to a repository on OxygenHub (Earth) runs `.flow/pipeline.kdl` in Firecracker microVMs, and the hub shows the run as a graph of jobs with live status and logs. |
8
9
M0 is the smallest slice that exercises the real architecture: a typed
10
pipeline compiled to a canonical form, host-side execution in the default
11
isolation tier, receipts computed by the host, and results reported to the
12
hub as checks. Everything M0 leaves out is listed at the end.
13
14
##1. Pipeline subset
15
16
The file is `.flow/pipeline.kdl` in the state being built (KDL 2.0). M0
17
accepts:
18
19
```kdl
20
schema-version 1
21
22
sandbox "alpine" {
23
runtime "microvm" // "microvm" (default) | "syscall"; "wasm" is refused in M0
24
rootfs "cas://blob/<64 hex>" // BLAKE3 of an ext4 image the runner holds
-**Helper:** for each job the runner starts `flow __sandbox <spec>` (its own binary, through `/proc/self/exe`). The helper joins the job's cgroup, makes the namespaces with the job's user (uid and gid 1000) mapped to the runner's, and forks the sandbox's PID 1.
-**Root:** PID 1 binds the unpacked tree read-only as the new root, the workspace at `/src`, tmpfs at `/tmp`, `/run` and `/run/secrets`, a fresh `/proc`, and a `/dev` of `null`, `zero`, `full`, `random` and `urandom` with `/dev/shm`. It brings `lo` up for `network "loopback"` and `"egress"` (with the vsock relay of spec §4.3.1 for the latter), pivots into the root and detaches the old one, so the host's files, the spec and the scratch directory are out of reach.
-**Job:** the job gets no new privileges, then Landlock (read and execute everywhere, write beneath `/src`, `/tmp`, `/dev` and its mounts), then the seccomp filter, then `execve` with exactly the job's environment, found the way `flow-init` finds it (the two share that code). The filter also refuses io_uring (which could open sockets past it), the new mount calls, kexec, swap and reboot.
177
-**Unpacking:**`debugfs rdump` unpacks the image into `trees/<hash>` in the home once, without mount rights. Set-id bits and device nodes are dropped.
-**Ephemeral mounts:** a `tmpfs` mount is a tmpfs of its size. An `ephemeral://disk` mount is a scratch directory, so its size is not enforced in this tier. One that names a `cache` starts from a directory a passing job of the slot kept, and goes back into the runner's cache pool when the job passes (spec §5.4, oxygenflow#70).
-**Artifacts:** they are read from the workspace and mount directories without following links.
180
-**Receipts:** the environment's runtime is `syscall`. Its parts are the `flow` binary's BLAKE3, the host kernel's release, memory, CPUs and mounts.
181
-**Limits:** the job's cgroup gets `memory.max`, `memory.swap.max 0` and `cpu.max`. A job killed for memory is told so in its log. At the end the group is killed and removed.
182
183
**Where it is offered.**`flow tiers` prints the tiers this machine can run and why the others cannot; a runner offers only those (spec §11.1). The syscall tier needs:
184
-**Namespaces:** unprivileged user namespaces for the `flow` binary. Ubuntu's `kernel.apparmor_restrict_unprivileged_userns=1` can deny them to a program without an AppArmor profile.
185
-**Kernel features:** Landlock in the kernel's LSM list, and seccomp filters.
186
-**Cgroup:** a cgroup v2 group the runner may manage, with the cpu and memory controllers and no other process in it. That is a systemd service with `Delegate=yes`, or a user service under `systemd --user`. The runner moves itself into a `flow-main` leaf of that group and makes one group per job beside it. For `flow run`, use `systemd-run --user --scope -p Delegate=yes flow run`.
-**Token:** a hub administrator creates a one-time registration token on Admin → Runners.
200
-**Registering:**`flow runner register https://hub --token … [--name N] [--ca PATH] [--label L]…` sends the runner's signing identity, architecture, OS, version and labels (`POST /api/v1/flow/runners/register`). It gets back the runner's own credential and writes hub, credential, name, CA and labels to `runner.toml` (owner-only). That file stands in for a `[runner]` section `flow.toml` lacks, and `flow runner` then starts from it.
201
-**Key:** registration binds the key; the hub accepts receipts from that runner signed by no other.
202
-**Claims:** every claim carries the runner's description (`info`: architecture, OS, version, labels, capacity) for the Runners page.
203
-**Old configuration:** runners configured with a `hub.toml` token still work. Their key is bound on their first signed receipt.
-`POST /api/v1/flow/jobs/{id}/log` appends records `{t, stream, text}`. An empty batch only asks whether the job still runs; the runner sends one every 5 s while a job is quiet, and stops the job's microVM when the hub answers `409` (the run was cancelled, oxygenflow#32). A cancelled job's end is not reported.