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.
fc10d9e8 Andy McCall 1 month ago ↶
56
- ` output` , ` publish` and ` secret` are recognised and refused with
e0918b88 Andy McCall 1 month ago
57
"not in M0", so a pipeline written for the full spec fails clearly.
fc10d9e8 Andy McCall 1 month ago ↶
58
- ` mount` is accepted for ** ephemeral scratch** only (up to 4 per job):
59
` mount { source "ephemeral://disk"; target "/src/target"; size "8GiB" }` .
60
` ephemeral://tmpfs` is memory in the guest and counts against its
61
memory-limit; ` ephemeral://disk` is a fresh sparse ext4 disk on the
62
runner, attached after the workspace, for build trees larger than
63
memory. The target is a clean absolute path strictly under ` /src` or
64
` /tmp` , mounts may not nest, sizes run from 16 MiB to 64 GiB, and any
65
other source (` oxy://workspace` and the rest) or setting (` readonly` ,
66
` include` ) is refused as not in M0. Both are empty when the job starts
67
and deleted when it ends, so nothing passes between jobs. Mounts are
68
part of the job's canonical form (field 6, present only when a job has
69
mounts, so pipelines without them keep their hashes) and of the
70
receipt's environment.
a72abeee Andy McCall 1 month ago ↶
71
72
- A sandbox's ` env` is where each of its jobs' environments starts; a
73
job's own ` env` overrides it name by name. It is part of the sandbox's
74
canonical form (field 6, present only when set). Below both is the
75
image's own environment: ` flow rootfs import --env NAME=value`
76
(repeatable) writes ` /etc/flow/env` into the image, for what a
77
Dockerfile's ` ENV` sets and ` docker export` loses, such as a toolchain's
78
` PATH` . It is part of the image, so of its hash. Under all three are
79
` PATH` (the usual directories) and ` HOME=/tmp` .
e0918b88 Andy McCall 1 month ago
80
81
The pipeline compiles to a canonical CBOR job graph (sorted keys, sandboxes
82
and jobs in name order). Its BLAKE3 is the ` pipeline_hash` used in receipts
83
and shown by the hub.
84
85
## 2. Tier 2: Firecracker (default)
86
87
Per job the runner:
88
89
1. Verifies the rootfs image: ` BLAKE3(file)` must equal the pinned hash.
90
Images live in ` $FLOW_HOME/cas/<hash>.ext4` ; an unknown hash fails the
91
job ("rootfs not on this runner").
92
2. Materializes the state at its heads with ` oxy` into a scratch
93
directory (a clone at the state), removes ` .oxy` , and builds a
94
workspace image with ` mkfs.ext4 -d` sized to the tree plus 512 MiB.
95
3. Boots Firecracker with no network device (so ` isolated` holds by
eb5c4db6 Andy McCall 1 month ago ↶
96
construction; ` loopback` only brings up the guest's ` lo` , so tests can
97
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
98
pinned guest kernel, the rootfs as a ** read-only** ` vda` , the
99
workspace as a read-write ` vdb` , and an initramfs holding ` flow-init` .
100
4. ` flow-init` (a static binary, PID 1 in the guest) mounts ` /proc` ,
101
` /sys` , ` /dev` , the rootfs at ` /newroot` read-only, ` vdb` at
fc10d9e8 Andy McCall 1 month ago ↶
102
` /newroot/src` , and tmpfs at ` /newroot/tmp` and ` /newroot/run` , then the
103
job's scratch mounts (creating their mount points, owned by the job's
104
user); it
eb5c4db6 Andy McCall 1 month ago ↶
105
switches root, then runs ` argv` (its program found in the guest's ` PATH` by
106
` flow-init` itself) with exactly the job's ` env` (plus
e0918b88 Andy McCall 1 month ago
107
` PATH` , ` HOME=/tmp` ) in ` /src` as uid 1000, with a job timeout.
c4285535 Andy McCall 1 month ago ↶
108
` /dev` gets the links devtmpfs lacks, ` fd` , ` stdin` , ` stdout` and
109
` stderr` into ` /proc/self/fd` , so shell process substitution works. A job
110
is told what it builds: ` FLOW_REPO` , ` FLOW_STATE` (the hub's state hash),
111
` FLOW_HEADS` (full hashes joined with ` +` ), ` FLOW_BOOKMARK` , ` FLOW_RUN`
112
(the run's number in its repository) and ` FLOW_JOB` ; ` flow run` sets
113
` FLOW_JOB` and ` FLOW_RUN=local` . A variable the pipeline sets keeps its
114
value. They are part of the job's environment, so of its receipt.
e0918b88 Andy McCall 1 month ago
115
5. ` flow-init` frames the child's output on the serial console as
116
` \x1eO <line>` (stdout) and ` \x1eE <line>` (stderr), then
117
` \x1eX <code>` , and powers off. The kernel runs ` quiet` so the host sees
118
only frames; anything else is kept as ` console` records.
119
6. The host reads frames, streams them as log records, and kills the VM
120
at the timeout (the guest cannot extend it). Scratch files are deleted
121
afterwards. Guest memory is released with the process.
122
123
The guest kernel, ` flow-init` and Firecracker itself are pinned by BLAKE3
124
in the runner's configuration; their hashes and the rootfs hash make the
125
receipt's ` environment` together with ` argv` and ` env` .
126
127
## 3. Tier 1: syscall sandbox
128
129
` runtime "syscall"` : a child in new user, mount, PID, network, IPC and
130
UTS namespaces (no interfaces but loopback), the rootfs image's tree
131
(unpacked once per hash, read-only) as ` /` , the workspace directory at
132
` /src` , tmpfs at ` /tmp` ; Landlock limits writes to ` /src` and ` /tmp` ; a
133
seccomp filter refuses ` socket` for anything but ` AF_UNIX` , plus
134
` ptrace` , ` mount` , ` bpf` , ` perf_event_open` , ` keyctl` and module calls;
135
` cpu-limit` and ` memory-limit` apply through a cgroup v2 scope when the
136
runner has a delegated cgroup, and the job fails to start otherwise
137
rather than run unbounded. Same frames and receipts as tier 2. Done after
138
tier 2 works on Earth.
139
140
## 4. Receipts
141
142
Every finished job gets a receipt (spec §6.1): ` job_name` ,
143
` pipeline_hash` , ` scope_tree_hash` (BLAKE3 over the sorted
144
` (path, content hash)` of the files ` scope` matches; the whole tree when
145
there is no scope), ` inputs` (empty in M0), ` environment` , ` outputs`
146
(empty), ` exit_code` , ` duration_ms` . It is encoded as canonical CBOR and
147
hashed as ` BLAKE3("oxy1 receipt " || len || 0x00 || body)` . M0 stores it on
148
the hub with the job; storing receipts as Oxygen objects needs the
149
` receipt` object type in the Oxygen spec, and cache reuse (§6.2) follows.
150
151
## 5. Hub side (OxygenHub)
152
153
- Configuration: ` [flow] runner_tokens = [...]` and ` repos = ["owner/*"]` ,
154
the repositories whose pushes may start runs. No proposals trigger runs
155
in M0, so code from people without write access never runs.
156
- On a push that moves a bookmark of an allowed repository, the hub reads
157
` .flow/pipeline.kdl` from the new state, validates it with ` flow-core` ,
158
and records a ** run** (repository, bookmark, state address, pipeline
159
hash, pusher) with one ** job** per pipeline job. An invalid pipeline
160
records a failed run with the error and its line.
161
- Runner API, bearer runner token:
162
- ` POST /api/v1/flow/claim` → the oldest queued job whose dependencies
163
passed, with its run's state heads, repository clone URL, the job's
164
compiled form and its sandbox, and a ** job token** : read access to that
165
repository only, valid until the job finishes or 2 hours pass. ` 204`
166
when there is nothing.
167
- ` POST /api/v1/flow/jobs/{id}/log` appends records ` {t, stream, text}` .
168
- ` POST /api/v1/flow/jobs/{id}/finish` with the exit code, duration and
169
receipt; a job whose dependency failed becomes ` skipped` .
170
- Each job is also a check named ` flow/<job>` on the state (hub spec §13),
171
so proposals, the status bar in VS Code and required checks see it.
172
- Pages: a ** Flow** tab with runs (status, bookmark, state, pusher,
173
duration); a run page with the job graph drawn as SVG (columns by
174
dependency depth, status by colour and glyph), each job's log folded
175
by default and the failed one open, refreshed while the run is live.
176
177
## 6. Earth
178
179
Under ` /home/claude/flow` : ` bin/firecracker` (release binary, pinned),
180
` kernel/vmlinux` (pinned), ` cas/` (rootfs images), ` work/` (scratch), and
181
` runner.toml` . ` flow runner` runs as the ` flow-runner` systemd service
182
under the ` claude` user (member of ` kvm` ), polling the hub at
183
` https://earth.andymccall.xyz:7343` .
184
185
## 7. Not in M0
186
187
WASM tier; FastCDC-backed rootfs and ` casfs` mounts (M0 copies the tree
fc10d9e8 Andy McCall 1 month ago ↶
188
into an image); mounts other than ephemeral scratch, ` output` and artifacts; receipt reuse; provenance
e0918b88 Andy McCall 1 month ago
189
attestations and composite signatures; policy gates; secrets; proposal
190
runs; scope-based skipping; re-run and cancel from the hub.