OXYGENHUB
# 2814f4213e7c oxygenflow / docs / m0.md
7.3 KiB · Markdown · last changed OxygenFlow M0: pipeline parser and canonical job graph, receipts, the runner API types, flow-init, the Firecracker tier, flow check, run, rootfs import and runner 1 month ago renderedsource blame history raw
e0918b88 Andy McCall 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
27
    network "isolated"           // M0 accepts only "isolated"
28
}
29
30
job "lint" {
31
    sandbox "alpine"
32
    scope "src/**" "README.md"   // recorded; the job runs on every push in M0
33
    env { LANG "C.UTF-8" }
34
    exec "grep" "-rn" "TODO" "src"
35
}
36
37
job "test" {
38
    sandbox "alpine"
39
    depends-on "lint"
40
    exec "sh" "-eu" "test.sh"
41
}
42
```
43
44
Rules, all checked by `flow check` and again by the hub before anything
45
runs (spec §2.2 I2, §3.2):
46
47
- Unknown nodes, arguments or properties are errors, with the line.
48
- Every string stays a string; `cpu-limit` is an integer from 1 to 16;
49
  `memory-limit` parses to 64 MiB to 16 GiB.
50
- A job names a declared sandbox, has a non-empty `exec` of literal
51
  strings, and `depends-on` only declared jobs; the graph has no cycle.
52
- Names are 1 to 64 characters from `[A-Za-z0-9._-]`.
53
- `exec` becomes `argv` unchanged: metacharacters are bytes (spec §10.1).
54
  A job that wants a shell names one, as `sh` above; nothing adds one.
55
- `mount`, `output`, `publish` and `secret` are recognised and refused with
56
  "not in M0", so a pipeline written for the full spec fails clearly.
57
58
The pipeline compiles to a canonical CBOR job graph (sorted keys, sandboxes
59
and jobs in name order). Its BLAKE3 is the `pipeline_hash` used in receipts
60
and shown by the hub.
61
62
## 2. Tier 2: Firecracker (default)
63
64
Per job the runner:
65
66
1. Verifies the rootfs image: `BLAKE3(file)` must equal the pinned hash.
67
   Images live in `$FLOW_HOME/cas/<hash>.ext4`; an unknown hash fails the
68
   job ("rootfs not on this runner").
69
2. Materializes the state at its heads with `oxy` into a scratch
70
   directory (a clone at the state), removes `.oxy`, and builds a
71
   workspace image with `mkfs.ext4 -d` sized to the tree plus 512 MiB.
72
3. Boots Firecracker with no network device (so `isolated` holds by
73
   construction), `vcpu_count` and `mem_size_mib` from the sandbox, the
74
   pinned guest kernel, the rootfs as a **read-only** `vda`, the
75
   workspace as a read-write `vdb`, and an initramfs holding `flow-init`.
76
4. `flow-init` (a static binary, PID 1 in the guest) mounts `/proc`,
77
   `/sys`, `/dev`, the rootfs at `/newroot` read-only, `vdb` at
78
   `/newroot/src`, and tmpfs at `/newroot/tmp` and `/newroot/run`; it
79
   switches root, then runs `argv` with exactly the job's `env` (plus
80
   `PATH`, `HOME=/tmp`) in `/src` as uid 1000, with a job timeout.
81
5. `flow-init` frames the child's output on the serial console as
82
   `\x1eO <line>` (stdout) and `\x1eE <line>` (stderr), then
83
   `\x1eX <code>`, and powers off. The kernel runs `quiet` so the host sees
84
   only frames; anything else is kept as `console` records.
85
6. The host reads frames, streams them as log records, and kills the VM
86
   at the timeout (the guest cannot extend it). Scratch files are deleted
87
   afterwards. Guest memory is released with the process.
88
89
The guest kernel, `flow-init` and Firecracker itself are pinned by BLAKE3
90
in the runner's configuration; their hashes and the rootfs hash make the
91
receipt's `environment` together with `argv` and `env`.
92
93
## 3. Tier 1: syscall sandbox
94
95
`runtime "syscall"`: a child in new user, mount, PID, network, IPC and
96
UTS namespaces (no interfaces but loopback), the rootfs image's tree
97
(unpacked once per hash, read-only) as `/`, the workspace directory at
98
`/src`, tmpfs at `/tmp`; Landlock limits writes to `/src` and `/tmp`; a
99
seccomp filter refuses `socket` for anything but `AF_UNIX`, plus
100
`ptrace`, `mount`, `bpf`, `perf_event_open`, `keyctl` and module calls;
101
`cpu-limit` and `memory-limit` apply through a cgroup v2 scope when the
102
runner has a delegated cgroup, and the job fails to start otherwise
103
rather than run unbounded. Same frames and receipts as tier 2. Done after
104
tier 2 works on Earth.
105
106
## 4. Receipts
107
108
Every finished job gets a receipt (spec §6.1): `job_name`,
109
`pipeline_hash`, `scope_tree_hash` (BLAKE3 over the sorted
110
`(path, content hash)` of the files `scope` matches; the whole tree when
111
there is no scope), `inputs` (empty in M0), `environment`, `outputs`
112
(empty), `exit_code`, `duration_ms`. It is encoded as canonical CBOR and
113
hashed as `BLAKE3("oxy1 receipt " || len || 0x00 || body)`. M0 stores it on
114
the hub with the job; storing receipts as Oxygen objects needs the
115
`receipt` object type in the Oxygen spec, and cache reuse (§6.2) follows.
116
117
## 5. Hub side (OxygenHub)
118
119
- Configuration: `[flow] runner_tokens = [...]` and `repos = ["owner/*"]`,
120
  the repositories whose pushes may start runs. No proposals trigger runs
121
  in M0, so code from people without write access never runs.
122
- On a push that moves a bookmark of an allowed repository, the hub reads
123
  `.flow/pipeline.kdl` from the new state, validates it with `flow-core`,
124
  and records a **run** (repository, bookmark, state address, pipeline
125
  hash, pusher) with one **job** per pipeline job. An invalid pipeline
126
  records a failed run with the error and its line.
127
- Runner API, bearer runner token:
128
  - `POST /api/v1/flow/claim` → the oldest queued job whose dependencies
129
    passed, with its run's state heads, repository clone URL, the job's
130
    compiled form and its sandbox, and a **job token**: read access to that
131
    repository only, valid until the job finishes or 2 hours pass. `204`
132
    when there is nothing.
133
  - `POST /api/v1/flow/jobs/{id}/log` appends records `{t, stream, text}`.
134
  - `POST /api/v1/flow/jobs/{id}/finish` with the exit code, duration and
135
    receipt; a job whose dependency failed becomes `skipped`.
136
- Each job is also a check named `flow/<job>` on the state (hub spec §13),
137
  so proposals, the status bar in VS Code and required checks see it.
138
- Pages: a **Flow** tab with runs (status, bookmark, state, pusher,
139
  duration); a run page with the job graph drawn as SVG (columns by
140
  dependency depth, status by colour and glyph), each job's log folded
141
  by default and the failed one open, refreshed while the run is live.
142
143
## 6. Earth
144
145
Under `/home/claude/flow`: `bin/firecracker` (release binary, pinned),
146
`kernel/vmlinux` (pinned), `cas/` (rootfs images), `work/` (scratch), and
147
`runner.toml`. `flow runner` runs as the `flow-runner` systemd service
148
under the `claude` user (member of `kvm`), polling the hub at
149
`https://earth.andymccall.xyz:7343`.
150
151
## 7. Not in M0
152
153
WASM tier; FastCDC-backed rootfs and `casfs` mounts (M0 copies the tree
154
into an image); `mount`, `output` and artifacts; receipt reuse; provenance
155
attestations and composite signatures; policy gates; secrets; proposal
156
runs; scope-based skipping; re-run and cancel from the hub.