e0918b88 Andy McCall 1 month ago
1
# OxygenFlow M0: first pipeline on Earth
2
3
4
5
| ** Status** | Plan, 2026-09-28 |
6
| ** 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
25
memory-limit "1GiB" // KiB, MiB, GiB
26
cpu-limit 2
eb5c4db6 Andy McCall 1 month ago ↶
27
network "isolated" // "isolated" or "loopback" in M0
a72abeee Andy McCall 1 month ago ↶
28
env { PATH "/opt/tool/bin:/usr/bin:/bin" } // every job's starting environment
e0918b88 Andy McCall 1 month ago
29
}
30
31
job "lint" {
32
sandbox "alpine"
33
scope "src/**" "README.md" // recorded; the job runs on every push in M0
34
env { LANG "C.UTF-8" }
35
exec "grep" "-rn" "TODO" "src"
36
}
37
38
job "test" {
39
sandbox "alpine"
40
depends-on "lint"
41
exec "sh" "-eu" "test.sh"
42
}
43
```
44
45
Rules, all checked by ` flow check` and again by the hub before anything
46
runs (spec §2.2 I2, §3.2):
47
48
- Unknown nodes, arguments or properties are errors, with the line.
49
- Every string stays a string; ` cpu-limit` is an integer from 1 to 16;
50
` memory-limit` parses to 64 MiB to 16 GiB.
51
- A job names a declared sandbox, has a non-empty ` exec` of literal
52
strings, and ` depends-on` only declared jobs; the graph has no cycle.
53
- Names are 1 to 64 characters from ` [A-Za-z0-9._-]` .
54
- ` exec` becomes ` argv` unchanged: metacharacters are bytes (spec §10.1).
55
A job that wants a shell names one, as ` sh` above; nothing adds one.
e2c12561 Andy McCall 1 month ago ↶
56
- ` publish` and a job's ` attest` are recognised and refused with "not in
57
M0", so a pipeline written for the full spec fails clearly.
58
- ` secret "token" { mount-path "/run/secrets/token" }` on a job (up to 8)
59
names a secret the hub holds for the repository (its own, its
60
organisation's or the site's; oxygenhub#107) and where the job reads it:
61
a clean path under ` /run/secrets/` . The claim carries each as ` secrets`
62
and ` flow-init` writes it there, 0400 and the job's user's, on a tmpfs
63
of its own, before the job starts; it is never in the environment, the
64
receipt or an artifact (spec §9). Locally, ` flow run --secret
65
NAME=VALUE` supplies it. ` vault-ref` is not in M0: the hub is the vault.
720eb819 Andy McCall 1 month ago ↶
66
- ` identity "sts.amazonaws.com" { mount-path "/run/secrets/aws-token" }` on
67
a job (up to 4) asks the hub for an OIDC token for that audience, naming
68
the repository, bookmark, run and job (OxygenHub spec §13.5,
69
oxygenhub#103); it comes with the claim and is mounted exactly as a
70
secret is. A local run has no hub, so ` flow run` says the file will be
71
missing.
a67b97e9 Andy McCall 1 month ago ↶
72
- ` output { artifact "oxy" { path "/src/target/release/oxy" } }` declares
73
up to 16 ** artifacts** (spec §5.2). The path is clean, absolute, under
74
` /src` , and not on a tmpfs mount, which is gone when the job ends. After
75
a job passes, the runner reads each artifact from the job's disks with
76
` debugfs` : the workspace, or the ` ephemeral://disk` mount holding it. An
77
artifact is at most 512 MiB. The runner uploads each one with
78
` POST /api/v1/flow/jobs/{id}/artifacts/{name}` (raw bytes). The hub keeps
79
it as a blob of the repository, and the receipt's ` outputs` name those
80
blobs in artifact order. A declared artifact that cannot be kept fails
81
the job, and the job gets no receipt.
fc10d9e8 Andy McCall 1 month ago ↶
82
- ` mount` is accepted for ** ephemeral scratch** only (up to 4 per job):
83
` mount { source "ephemeral://disk"; target "/src/target"; size "8GiB" }` .
84
` ephemeral://tmpfs` is memory in the guest and counts against its
85
memory-limit; ` ephemeral://disk` is a fresh sparse ext4 disk on the
86
runner, attached after the workspace, for build trees larger than
87
memory. The target is a clean absolute path strictly under ` /src` or
88
` /tmp` , mounts may not nest, sizes run from 16 MiB to 64 GiB, and any
89
other source (` oxy://workspace` and the rest) or setting (` readonly` ,
90
` include` ) is refused as not in M0. Both are empty when the job starts
91
and deleted when it ends, so nothing passes between jobs. Mounts are
92
part of the job's canonical form (field 6, present only when a job has
93
mounts, so pipelines without them keep their hashes) and of the
94
receipt's environment.
a72abeee Andy McCall 1 month ago ↶
95
96
- A sandbox's ` env` is where each of its jobs' environments starts; a
97
job's own ` env` overrides it name by name. It is part of the sandbox's
98
canonical form (field 6, present only when set). Below both is the
99
image's own environment: ` flow rootfs import --env NAME=value`
100
(repeatable) writes ` /etc/flow/env` into the image, for what a
101
Dockerfile's ` ENV` sets and ` docker export` loses, such as a toolchain's
102
` PATH` . It is part of the image, so of its hash. Under all three are
103
` PATH` (the usual directories) and ` HOME=/tmp` .
e0918b88 Andy McCall 1 month ago
104
105
The pipeline compiles to a canonical CBOR job graph (sorted keys, sandboxes
106
and jobs in name order). Its BLAKE3 is the ` pipeline_hash` used in receipts
107
and shown by the hub.
108
109
## 2. Tier 2: Firecracker (default)
110
111
Per job the runner:
112
113
1. Verifies the rootfs image: ` BLAKE3(file)` must equal the pinned hash.
114
Images live in ` $FLOW_HOME/cas/<hash>.ext4` ; an unknown hash fails the
115
job ("rootfs not on this runner").
116
2. Materializes the state at its heads with ` oxy` into a scratch
117
directory (a clone at the state), removes ` .oxy` , and builds a
118
workspace image with ` mkfs.ext4 -d` sized to the tree plus 512 MiB.
119
3. Boots Firecracker with no network device (so ` isolated` holds by
eb5c4db6 Andy McCall 1 month ago ↶
120
construction; ` loopback` only brings up the guest's ` lo` , so tests can
121
serve and connect on 127.0.0.1 while nothing leaves the guest), ` vcpu_count` and ` mem_size_mib` from the sandbox, the
e0918b88 Andy McCall 1 month ago
122
pinned guest kernel, the rootfs as a ** read-only** ` vda` , the
123
workspace as a read-write ` vdb` , and an initramfs holding ` flow-init` .
124
4. ` flow-init` (a static binary, PID 1 in the guest) mounts ` /proc` ,
125
` /sys` , ` /dev` , the rootfs at ` /newroot` read-only, ` vdb` at
fc10d9e8 Andy McCall 1 month ago ↶
126
` /newroot/src` , and tmpfs at ` /newroot/tmp` and ` /newroot/run` , then the
127
job's scratch mounts (creating their mount points, owned by the job's
128
user); it
eb5c4db6 Andy McCall 1 month ago ↶
129
switches root, then runs ` argv` (its program found in the guest's ` PATH` by
130
` flow-init` itself) with exactly the job's ` env` (plus
e0918b88 Andy McCall 1 month ago
131
` PATH` , ` HOME=/tmp` ) in ` /src` as uid 1000, with a job timeout.
c4285535 Andy McCall 1 month ago ↶
132
` /dev` gets the links devtmpfs lacks, ` fd` , ` stdin` , ` stdout` and
133
` stderr` into ` /proc/self/fd` , so shell process substitution works. A job
134
is told what it builds: ` FLOW_REPO` , ` FLOW_STATE` (the hub's state hash),
135
` FLOW_HEADS` (full hashes joined with ` +` ), ` FLOW_BOOKMARK` , ` FLOW_RUN`
136
(the run's number in its repository) and ` FLOW_JOB` ; ` flow run` sets
c58efc6a Andy McCall 1 month ago ↶
137
` FLOW_JOB` and ` FLOW_RUN=local` . After them come the ** variables** the
138
hub holds for the repository (site, organisation, repository; the
139
claim's ` env` ; ` flow run --var NAME=VALUE` locally), never secrets. A
140
variable the pipeline sets keeps its value. They are part of the job's
141
environment, so of its receipt.
e0918b88 Andy McCall 1 month ago
142
5. ` flow-init` frames the child's output on the serial console as
143
` \x1eO <line>` (stdout) and ` \x1eE <line>` (stderr), then
144
` \x1eX <code>` , and powers off. The kernel runs ` quiet` so the host sees
145
only frames; anything else is kept as ` console` records.
146
6. The host reads frames, streams them as log records, and kills the VM
147
at the timeout (the guest cannot extend it). Scratch files are deleted
148
afterwards. Guest memory is released with the process.
149
150
The guest kernel, ` flow-init` and Firecracker itself are pinned by BLAKE3
151
in the runner's configuration; their hashes and the rootfs hash make the
152
receipt's ` environment` together with ` argv` and ` env` .
153
154
## 3. Tier 1: syscall sandbox
155
156
` runtime "syscall"` : a child in new user, mount, PID, network, IPC and
157
UTS namespaces (no interfaces but loopback), the rootfs image's tree
158
(unpacked once per hash, read-only) as ` /` , the workspace directory at
159
` /src` , tmpfs at ` /tmp` ; Landlock limits writes to ` /src` and ` /tmp` ; a
160
seccomp filter refuses ` socket` for anything but ` AF_UNIX` , plus
161
` ptrace` , ` mount` , ` bpf` , ` perf_event_open` , ` keyctl` and module calls;
162
` cpu-limit` and ` memory-limit` apply through a cgroup v2 scope when the
163
runner has a delegated cgroup, and the job fails to start otherwise
164
rather than run unbounded. Same frames and receipts as tier 2. Done after
165
tier 2 works on Earth.
166
167
## 4. Receipts
168
169
Every finished job gets a receipt (spec §6.1): ` job_name` ,
170
` pipeline_hash` , ` scope_tree_hash` (BLAKE3 over the sorted
171
` (path, content hash)` of the files ` scope` matches; the whole tree when
172
there is no scope), ` inputs` (empty in M0), ` environment` , ` outputs`
173
(empty), ` exit_code` , ` duration_ms` . It is encoded as canonical CBOR and
47c1d64d Andy McCall 1 month ago ↶
174
hashed as ` BLAKE3("oxy1 receipt " || len || 0x00 || body)` , the ` receipt`
175
object of the Oxygen spec (§14.6).
176
f98a8f78 Andy McCall 1 month ago ↶
177
** Registering a runner.**
178
- ** Token:** a hub administrator creates a one-time registration token on Admin → Runners.
179
- ** 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.
180
- ** Key:** registration binds the key; the hub accepts receipts from that runner signed by no other.
181
- ** Claims:** every claim carries the runner's description (` info` : architecture, OS, version, labels, capacity) for the Runners page.
182
- ** Old configuration:** runners configured with a ` hub.toml` token still work. Their key is bound on their first signed receipt.
183
47c1d64d Andy McCall 1 month ago ↶
184
** Signed receipts.** A runner keeps a composite key in its home
185
(` runner.key` , owner-only, and ` runner.identity` ), made on first start and
186
named after the runner. After each job it binds the receipt to the state of
187
the heads it fetched (computed by the runner, not taken from the claim) and
188
to the run's URL, signs that attestation, and sends the signed receipt with
189
` finish` as ` signed_receipt` (hex). The hub checks it and keeps it in the
190
repository; anyone can then check it with ` oxy receipt verify` . Cache reuse
191
(§6.2) follows.
e0918b88 Andy McCall 1 month ago
192
193
## 5. Hub side (OxygenHub)
194
195
- Configuration: ` [flow] runner_tokens = [...]` and ` repos = ["owner/*"]` ,
196
the repositories whose pushes may start runs. No proposals trigger runs
197
in M0, so code from people without write access never runs.
198
- On a push that moves a bookmark of an allowed repository, the hub reads
199
` .flow/pipeline.kdl` from the new state, validates it with ` flow-core` ,
200
and records a ** run** (repository, bookmark, state address, pipeline
201
hash, pusher) with one ** job** per pipeline job. An invalid pipeline
202
records a failed run with the error and its line.
203
- Runner API, bearer runner token:
204
- ` POST /api/v1/flow/claim` → the oldest queued job whose dependencies
205
passed, with its run's state heads, repository clone URL, the job's
206
compiled form and its sandbox, and a ** job token** : read access to that
207
repository only, valid until the job finishes or 2 hours pass. ` 204`
208
when there is nothing.
209
- ` POST /api/v1/flow/jobs/{id}/log` appends records ` {t, stream, text}` .
210
- ` POST /api/v1/flow/jobs/{id}/finish` with the exit code, duration and
211
receipt; a job whose dependency failed becomes ` skipped` .
212
- Each job is also a check named ` flow/<job>` on the state (hub spec §13),
213
so proposals, the status bar in VS Code and required checks see it.
214
- Pages: a ** Flow** tab with runs (status, bookmark, state, pusher,
215
duration); a run page with the job graph drawn as SVG (columns by
216
dependency depth, status by colour and glyph), each job's log folded
217
by default and the failed one open, refreshed while the run is live.
218
219
## 6. Earth
220
221
Under ` /home/claude/flow` : ` bin/firecracker` (release binary, pinned),
222
` kernel/vmlinux` (pinned), ` cas/` (rootfs images), ` work/` (scratch), and
223
` runner.toml` . ` flow runner` runs as the ` flow-runner` systemd service
224
under the ` claude` user (member of ` kvm` ), polling the hub at
225
` https://earth.andymccall.xyz:7343` .
226
227
## 7. Not in M0
228
229
WASM tier; FastCDC-backed rootfs and ` casfs` mounts (M0 copies the tree
a67b97e9 Andy McCall 1 month ago ↶
230
into an image); mounts other than ephemeral scratch; artifacts as inputs of later jobs; receipt reuse; provenance
231
attestations; policy gates; secrets; proposal
e0918b88 Andy McCall 1 month ago
232
runs; scope-based skipping; re-run and cancel from the hub.