a083cc80 Andy McCall 1 month ago ↶
1
# OxygenFlow (flow) — Deterministic Execution & Provenance Pipeline Specification
2
3
4
5
| ** Format version** | 1 |
6
| ** Date** | 2026-09-28 |
7
| ** Status** | Draft 0.1 |
8
| ** Target VCS** | Oxygen (oxy) Draft 0.8i |
9
| ** Audience** | Implementers, Security Architects, and Systems Engineers |
10
11
12
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be
13
interpreted as described in RFC 2119.
14
15
## 1. Overview and Core Invariants
16
17
OxygenFlow (flow) is a content-addressed, hermetic execution engine, runner system,
18
and pipeline language designed natively for the Oxygen (oxy) version control
19
system.
20
21
Existing runner engines (GitHub Actions, GitLab CI, Forgejo Runner) suffer from
22
structural flaws:
23
1. Workspace Bloat: Full clones, index unpacks, and checkout scans dominate I/O.
24
2. String Injection: Shell interpolation within untyped YAML files produces
25
expression injection and command execution exploits.
26
3. Decoupled Supply Chain: Attestations (SLSA, in-toto, Cosign) exist outside
27
the VCS, relying on external registries rather than native repository trust.
28
4. Coarse Caching: Opaque tarballs keyed by arbitrary string hashes fail to
29
leverage commutative history or granular dependency graphs.
30
31
OxygenFlow solves these liabilities through direct integration with Oxygen:
32
- Zero-Copy Workspaces: Jobs mount workspaces directly from Oxygen's FastCDC-
33
chunked, BLAKE3-addressed Content-Addressable Store (CAS) using Copy-on-Write
34
(COW) overlays.
35
- Commutative History & Impact Trees: Pipelines evaluate over change closures
36
rather than linear snapshot sequences. Commuting changes automatically reuse
37
cached execution receipts without redundant re-execution.
38
- Micro-Engine Sandboxing: Jobs run inside isolated environments (WASM, gVisor,
39
or MicroVMs) with strict, deny-by-default capabilities and no ambient
40
network egress.
41
- Native Cryptographic Provenance: Provenance and execution receipts are
42
generated outside the untrusted guest, encoded as deterministic CBOR, and
43
signed using composite Ed25519 + ML-DSA-65 keys as native Oxygen Attest
44
objects governed by in-repository Policy rules.
45
46
## 2. Threat Model and Security Architecture
47
48
### 2.1 Adversaries
49
- Untrusted Proposal Author: Submits code to an open repository containing
50
malicious build configurations intended to escape the runner, exfiltrate
51
secrets, or corrupt shared worker caches.
52
- Malicious Dependency / Upstream: Third-party dependencies invoked during
53
build time attempting out-of-band network calls or host file modification.
54
- Compromised Runner / Malicious Worker: An execution node attempting to
55
forge successful build receipts or inject backdoors into published artifacts.
56
57
### 2.2 Security Invariants
58
- I1 Deny-by-Default Execution: Sandboxes possess no network interfaces
59
(excluding loopback when requested) and no host filesystem access unless
60
explicitly granted in policy-signed configuration.
61
- I2 Injection-Free Invocation: Configuration parsing directly yields an argv
62
string vector executed via execve(). Shell interpretation (/bin/sh -c) does
63
not exist.
64
- I3 Host-Enforced Provenance: The running payload cannot author, inspect, or
65
alter its own provenance statement. Provenance is emitted solely by the
66
isolation hypervisor boundary.
67
- I4 Cryptographic Identity & Authorization: Runners execute using an Oxygen
68
Identity whose KeyId is enrolled as a delegate in the repository's Policy.
69
Unenrolled runners cannot publish recognized attestations or advance gates.
70
71
## 3. The OxygenFlow Configuration Language (.flow)
72
73
Pipelines are declared in KDL syntax within .flow/pipeline.kdl at the
74
repository root. KDL provides strict typing, structured nesting, and rejects
75
arbitrary text macro expansion.
76
b647ab72 Andy McCall 1 month ago ↶
77
Strings follow KDL 2.0, which has no ` \033` , ` \e` or ` \x` escapes: ESC,
78
for ANSI colours in a job's output, is ` \u{1b}` (` "\u{1b}[32mok\u{1b}[0m"` ),
79
and ` flow check` says so when it meets one of the others. Inside the
80
guest, ` /dev/fd` and ` /dev/stdin|stdout|stderr` link to ` /proc/self/fd`
81
as on any Linux system, so bash process substitution works.
82
a083cc80 Andy McCall 1 month ago ↶
83
### 3.1 Pipeline Specification Schema
84
85
``` kdl
86
schema-version 1
87
88
// Define the hermetic execution boundary
89
sandbox "rust-builder" {
90
runtime "microvm" // Options: "wasm" | "gvisor" | "microvm"
91
rootfs "cas://blob/9b5c2a1e8f3d4e7a6b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a"
92
memory-limit "4GiB"
93
cpu-limit 4
ea81796b Andy McCall 19 days ago ↶
94
network "isolated" // Options: "isolated" | "loopback" | "egress" { allow "host"... } (§4.3.1)
a083cc80 Andy McCall 1 month ago ↶
95
}
96
97
// Job evaluated based on Oxygen change closure
98
job "compile-core" {
99
sandbox "rust-builder"
100
101
// Commutation boundary: runs only if change closure touches these paths
102
scope "crates/oxy-core/**" "crates/oxy-server/**" "Cargo.*"
103
104
// Zero-copy workspace mount backed by Oxygen CAS
105
mount {
106
source "oxy://workspace"
107
target "/src"
108
include "crates/oxy-core" "crates/oxy-server" "Cargo.toml" "Cargo.lock"
109
readonly #true
110
}
111
112
// Ephemeral scratch space
113
mount {
114
source "ephemeral://tmpfs"
115
target "/src/target"
116
size "8GiB"
117
}
118
119
env {
120
RUSTFLAGS "-Dwarnings"
121
CARGO_INCREMENTAL "0"
122
}
123
124
// Direct argv execution vector (no subshell)
125
exec "cargo" "build" "--release" "--package" "oxy-core"
126
127
output {
128
artifact "oxy-core-lib" {
129
path "/src/target/release/liboxy_core.rlib"
130
format "blob"
131
}
132
}
133
}
134
135
job "unit-tests" {
136
sandbox "rust-builder"
137
depends-on "compile-core"
138
139
mount {
140
source "oxy://workspace"
141
target "/src"
142
readonly #true
143
}
144
145
mount {
146
source "artifact://compile-core/oxy-core-lib"
147
target "/src/target/release/liboxy_core.rlib"
148
}
149
150
exec "cargo" "test" "--release" "--package" "oxy-core"
151
}
152
153
publish "release-provenance" {
154
depends-on "unit-tests"
155
156
attest {
157
kind "provenance"
158
bind-ref "bookmarks/main"
159
include-artifacts "compile-core/oxy-core-lib"
160
}
161
}
162
163
bf7bb1b8 Andy McCall 1 month ago ↶
164
**What starts a job.** An `on` block, at the top of the pipeline or in a job (the job's wins), names the events that run it: `push` with bookmark globs (none: any bookmark), `tag` with tag globs (none: any tag), `manual` ("Run pipeline", `oh run start`) and `schedule` with five-field cron lines on the hub's clock (UTC), run on the default bookmark. Without `on`, a job runs on every push and by hand, as before. A run has only the jobs its event starts (and what they depend on); an event that starts no job starts no run. A rerun repeats its run's event.
165
166
```kdl
167
on {
168
push "main" "feature/*"
169
manual
170
}
171
job "dist" {
172
on {
173
tag "v*"
174
manual
175
}
176
…
177
}
178
```
179
02b127d2 Andy McCall 1 month ago ↶
180
** Time limits.** A job may say how long it may run with ` timeout "90m"` (hours, minutes and seconds, as ` "2h"` , ` "1h30m"` or ` "45s"` , from a minute to a day); a sandbox's ` timeout` is the default for its jobs. Without one, the hub's default applies, and the hub caps every job at the site's maximum (both set by an administrator on Admin → Runners). The runner stops a job at its limit, and the hub ends one its runner stopped reporting a few minutes after it. A timeout is part of the compiled job, so a job without one compiles as before.
181
e87c1c13 Andy McCall 18 days ago ↶
182
** Priority.** A pipeline may lower its jobs' place in the hub's queue, never raise it (oxygenhub#244): ` priority "low"` (or ` "normal"` ) on a job, or at the top of the file for every job that does not say. A job's level is then the lower of its own and its repository's (the repository's, organisation's or site's, set on the hub), so nightly or bulk jobs can stand back for everyday ones; ` high` and ` urgent` are refused here, being the hub's to give, and a run a person expedites stays urgent whatever its jobs say. A priority is part of the compiled job; a job without one compiles as before.
183
e576e086 Andy McCall 1 month ago ↶
184
** Runner labels.** A sandbox or a job may say ` runs-on "LABEL" ...` : lower-case letters, digits, ` -` , ` _` and ` .` , at most 64 bytes each, given on one or more lines. A job needs a runner that has every label its sandbox and it name. A runner reports its labels and architecture when it registers and on each claim (` flow runner register --label` ). The hub hands a job only to a runner with all of them, within the runner's scope (OxygenHub spec 13.2), and a job no runner can take waits, saying so on the queue. Labels are part of the compiled job, so a job that asks for none compiles as before.
185
a083cc80 Andy McCall 1 month ago ↶
186
### 3.2 Canonical Compilation
187
Upon commit, .flow/pipeline.kdl is validated and compiled into a deterministic
188
CBOR job-graph blob. Runners evaluate the canonical CBOR to prevent syntax or
189
parser ambiguities across varying client platforms.
190
191
## 4. Sandbox Execution Engine
192
193
OxygenFlow implements three distinct isolation tiers depending on the workload:
194
195
### 4.1 Level 0: WASM Component Sandbox
28cc603f Andy McCall 10 days ago ↶
196
- Intended for static linters, formatters, doc generators, policy checks, and
197
test suites built for ` wasm32-wasip1` ; the tier phones and browsers run
198
(docs/handover.md).
199
- Built (oxygenflow#75): wasmtime 49 runs WASI preview 1 modules, each in a
200
fresh instance, compiled by Cranelift (tens of milliseconds a module).
201
Preview 2 components later.
202
- ` runtime "wasm"` ; no ` rootfs` and no network (` network` absent or
203
` "isolated"` ); ` memory-limit` holds the module's linear memory.
204
- ` exec` names the module, a path among the job's files (its scope or an
205
earlier job's artifact), optionally under ` /src/` , or a glob with ` *` in
206
its file name: each match runs in turn, and the job's exit code is the
207
first non-zero one after all have run. The rest of ` exec` are the
208
module's arguments. A module that traps exits 134.
209
- The job's files are the only host files it sees, preopened as ` /src` (and
210
as the current directory, for relative paths); each ` mount` is preopened at
211
its target. Secrets and identities are not offered yet.
212
- The receipt's environment names ` wasmtime` and its version, the runner
213
binary, the memory, and every module run with its BLAKE3.
a083cc80 Andy McCall 1 month ago ↶
214
215
### 4.2 Level 1: Intercepted Syscall Isolation (gVisor / Landlock + seccomp-bpf)
216
- Intended for standard compilation and hermetic test suites.
217
- Emulates or filters Linux system call interfaces in user space.
218
- Blocks access to raw network sockets, real-time hardware clocks, and device
219
nodes.
220
- Memory and execution CPU quotas are strictly bounded via cgroups v2.
221
222
### 4.3 Level 2: MicroVM Isolation (Firecracker / Cloud-Hypervisor)
223
- Intended for kernel tests, multi-container integration, and untrusted code.
224
- Ephemeral Linux microVM booting in < 80 ms.
225
- The root filesystem is presented as a read-only virtio-block device mapped
226
directly to FastCDC chunks stored in Oxygen's CAS.
227
- Guest VM memory is cleared upon termination.
228
ea81796b Andy McCall 19 days ago ↶
229
#### 4.3.1 Allow-listed egress (oxygenflow#55)
230
231
A sandbox reaches the network only by naming its hosts:
232
233
``` kdl
234
sandbox "deploy" {
235
runtime "microvm"
236
network "egress" { allow "*.googleapis.com" "registry.opentofu.org" }
237
}
238
```
239
240
The VM still has no network device. ` flow-init` serves a proxy on the
241
guest's ` 127.0.0.1:3128` (the job gets ` HTTPS_PROXY` , ` HTTP_PROXY` and
242
their lower-case forms, unless it sets them) and relays each connection,
243
unread, over Firecracker's vsock to the runner. The runner accepts only
244
` CONNECT host:443` for a host the list allows (a name exactly, or any
245
name under a ` *.` domain, never the domain itself; never an address),
246
resolves it itself (the guest does not resolve), refuses any name that
247
resolves to an address that is not public, and relays the bytes without
248
opening the TLS. Every decision, allowed or refused, is a ` flow` line in
249
the job's log. Patterns are lower-case host names of two or more labels,
250
at most 32 per sandbox, and part of the sandbox's hash.
251
252
Egress is off unless a runner turns it on (` egress = true` under
253
` [runner]` in ` flow.toml` ); such a runner offers the ` egress` label, and
254
an egress sandbox needs it, so the hub hands its jobs only to runners
255
that allow it, and a runner without it refuses one anyway. The syscall
256
runtime has no egress yet.
257
35f7a849 Andy McCall 18 days ago ↶
258
** Keyless receipts** (oxygenhub#241). With ` keyless = true` under
259
` [runner]` (or in ` runner.toml` ), a runner signs each job's receipt with a
260
fresh key instead of its own: just before signing, it asks the hub to
261
certify the key (` POST /api/v1/keyless/certify` with the job's token and
262
the key's proof of possession, scope ` seal` ), checks the certificate is
263
for that key and signed by the key the hub publishes at
264
` /.well-known/oxyhub-key` , signs, attaches the certificate and the hub's
265
identity to the receipt (Oxygen spec §14.6), and drops the key. When the
266
hub cannot certify, the runner's own key signs and the job's log says
267
why. The runner's key then only authenticates it to the hub.
268
4be5e4cd Andy McCall 15 days ago ↶
269
** Metrics** (oxygenflow#35). With ` metrics = "127.0.0.1:9464"` under
270
` [runner]` (off unless set), a runner serves Prometheus text at
271
` /metrics` on that address, from a thread of its own: its version and
272
uptime, jobs claimed and declined, finished jobs by result (` passed` ,
273
` failed` , ` error` , ` cancelled` ), the seconds they ran together, and the
274
jobs running now, each labelled with the runner's name. With
275
` metrics_token` , a scrape must send it as a bearer token; without one,
276
bind it to an address only the scraper reaches. The hub's own ` /metrics`
277
has the queue and run times of every runner's jobs.
278
a083cc80 Andy McCall 1 month ago ↶
279
## 5. Content-Addressable Storage (CAS) & Zero-Copy Mounting
280
281
OxygenFlow runners bypass traditional clone operations. Repositories are accessed
282
directly via the local object store (.oxy/objects/) or streamed on demand
283
using Oxygen's wire protocol.
284
285
### 5.1 FUSE Overlay Mounting (flow-casfs)
286
1. The runner receives a task targeting an Oxygen State ObjectRef (a sorted
287
list of maximal heads).
701dcbc5 Andy McCall 1 month ago ↶
288
2. The state's tree is read from a state index (Oxygen's flat, checksummed
289
description of a history-only state: its changes, tree view and each
290
file's deferred history with its graph key), built once from the
291
nearest checkpoint plus the changes after it and kept beside the store.
292
Listing a directory needs nothing else.
293
3. The tree is exposed to the sandbox read-only through flow-casfs (FUSE):
294
- An opaque file's bytes are its stored blob, whose chunks (FastCDC,
295
32 KiB to 256 KiB, mean 64 KiB, Oxygen spec Section 13.2) are read as
296
they are needed.
297
- A text file's bytes are a rendering of its history, not a stored blob.
298
Renderings are cached by ** graph key** (equal keys mean equal
299
renderings) as ordinary Blob and Chunk objects: a file is rendered on
300
first read or stat, and every later state in which it has the same
301
history finds it at once. A file added once and never edited is its
302
own text blob and needs no rendering at all.
303
- Missing objects are fetched lazily using the POST /objects protocol.
304
- To prevent FUSE context-switch bottlenecks on many small reads,
305
flow-casfs caches whole small files and mmaps chunk data, relying on
306
the host page cache.
307
- All guest mutations are diverted to an overlay on the job's ephemeral
308
disk (or a tmpfs); base objects remain immutable.
309
4. The same mount backs web workspaces (OxygenHub spec 13.3): a
310
` workspace` job keeps it for as long as its editor is open.
311
5. Targets, measured on the Linux tip: mount under 100 ms, a first read of
312
a file in milliseconds, and a full read of the tree no slower than a
313
checkout writing it.
a083cc80 Andy McCall 1 month ago ↶
314
315
### 5.2 Artifact Capture
316
1. At task completion, flow-casfs scans the paths declared in the output block.
317
2. Modified outputs are chunked with FastCDC, hashed via BLAKE3-256, and
318
written as canonical Blob and Chunk objects.
319
3. Output ObjectRefs are stored directly in the Oxygen repository.
9a6fac2b Andy McCall 1 month ago ↶
320
4. Outputs are read when the job passes. An artifact with ` when "always"`
321
(default ` when "passed"` ) is also read when the job fails or is stopped
322
at its time limit, so a test report survives the failure it describes.
323
From a job that did not pass it is kept and shown, but its receipt never
324
names it, and one that cannot be read is only noted in the log.
a083cc80 Andy McCall 1 month ago ↶
325
9a6fac2b Andy McCall 1 month ago ↶
326
``` kdl
327
output {
328
artifact "junit" {
329
path "/src/target/junit.xml"
330
when "always"
331
}
332
}
333
```
334
6d53fb7d Andy McCall 25 days ago ↶
335
5. A ` report` output (OxygenHub ` docs/reports.md` ) names a directory
336
holding ` report.toml` at its top. The runner reads its regular files
337
(links and anything else are left out, never followed; file names hold
338
letters, digits, ` .` , ` _` , ` -` and ` +` ; at most 2000 files and 512 MiB),
339
uploads each to ` POST /api/v1/flow/jobs/{job}/reports/{name}/files` with
340
its path in ` X-Flow-Report-Path` , then uploads a manifest,
341
` {"version":1,"files":[{"path","blob","size"}]}` , as the output under
342
the report's name, checking every blob the hub answers. The receipt
343
names the manifest. Reports and artifacts share names; a report's kind
344
is part of the job's hash (` [name, path, "report"]` ).
345
346
``` kdl
347
output {
348
report "frame-timing" {
349
path "/src/out/report"
350
when "always"
351
}
352
}
353
```
354
46fe2c81 Andy McCall 19 days ago ↶
355
### 5.3.1 Artifacts between jobs (oxygenflow#58)
356
357
A job takes an artifact of a job it depends on:
358
359
``` kdl
360
job "apply" {
361
depends-on "plan"
362
input {
363
artifact "tfplan" from "plan"
364
path "/src/tfplan"
365
}
366
}
367
```
368
369
The producer must be named in ` depends-on` and declare that name as an
370
artifact (not a report); paths are clean absolute paths under ` /src` , at
371
most 8 inputs, and they are part of the job's hash (` [artifact, from,
372
path]` , field 10). Since the producer has passed when the job is handed
373
out, the hub puts each input in the claim with the blob the producer's
374
receipt names (a passing job's receipt names exactly the artifacts it
375
uploaded). Before the job starts, the runner fetches each from
376
` GET /api/v1/flow/jobs/{job}/inputs/{index}` , checks its Oxygen blob
377
reference against that blob, refuses the job if they differ, and writes it
378
at its path in the workspace (not part of the scope hash: it belongs to the
379
run, not the repository). The job's receipt lists the blobs as its
380
` inputs` , so an apply job's receipt proves which plan it applied.
381
e2089721 Andy McCall 19 days ago ↶
382
### 5.3.2 Approval gates (oxygenflow#57)
383
384
A job can wait for a person:
385
386
``` kdl
387
job "apply" {
388
depends-on "plan"
389
approval {
390
by "maintain" // the least role: "write", "maintain" (default) or "admin"
391
others-only #true // not the person whose push started the run
392
expires "24h" // from when the jobs it depends on are done; up to 720h
2be1fe4a Andy McCall 19 days ago ↶
393
sealed #true // signed with the approver's key; default: on with an environment
e2089721 Andy McCall 19 days ago ↶
394
}
395
}
396
```
397
398
The hub hands such a job to no runner until someone with that role on the
399
repository approves it, on the run page or through the API; others-only
400
leaves out whoever pushed. Approving or rejecting is recorded on the job
401
(who, when) and in the audit log. A rejected or expired job ends as
402
cancelled and the jobs after it are skipped; a newer push to the
403
bookmark cancels the run as before, so a stale plan is never applied. The
404
approval is part of the job's hash (field 11).
405
2be1fe4a Andy McCall 19 days ago ↶
406
** Sealed approvals (oxygenhub#235).** With ` sealed` (the default for a job
407
that names an environment, §5.3.3), pressing Approve is not enough: the
408
approver signs an Oxygen ` approval` attestation with their own key
409
(` oxy seal --flow URL --job NAME` ; Oxygen spec §14.4) covering the run, the
410
job's hash, the state, the input blobs (the reviewed plan) and the
411
environment. The hub starts the job only once it verifies, the key is
412
linked to an account with the role, and it is not a delegated key; it keeps
413
the approval in the repository, and the job's identity tokens name the
414
approver and the approval's hash. Rejecting needs no seal.
415
cd03b984 Andy McCall 17 days ago ↶
416
** Agents (oxygenhub#229, #258).** A gate is there so a person decides. A
417
decision made through an agent's token (an agent-marked token, or a keyless
418
agent's grant) is refused unless the repository lets agents decide its
419
gates (its Settings › OxygenFlow, for maintainers; off by default, audited
420
when changed). Where allowed, it says so on the job, the run and the audit
421
log ("approved by carol via claude-code"). A sealed approval is a person's
422
own key, whatever carried it, and is not affected.
423
f4e51e63 Andy McCall 19 days ago ↶
424
### 5.3.3 Environments (oxygenflow#59)
425
426
` environment "prod"` on a job says where it deploys (1 to 32 of ` a-z` ,
427
` 0-9` , ` -` ; part of the job's hash, field 12). The hub keeps what each
428
environment of a repository runs, from the jobs that passed for it (state,
429
run, when, who approved), on the repository's Deployments page. The job's
430
identity tokens carry an ` environment` claim only when the job was
431
approved (§5.3.2): a job with no approval block, or one not approved, gets
432
tokens without it. So a cloud's trust condition can require
433
` environment == "prod"` , and nothing unapproved deploys there, even from
434
the deploy bookmark. The approval block is the environment's protection
435
rule for now.
436
d60aac6c Andy McCall 22 days ago ↶
437
### 5.4 The Build Cache (oxygenflow#28)
438
439
An ` ephemeral://disk` mount may name a ` cache` , and the files that key it:
440
441
``` kdl
442
mount {
443
source "ephemeral://disk"
444
target "/src/target"
445
size "24GiB"
446
cache "target"
447
key "Cargo.lock" "rust-toolchain.toml"
448
}
449
```
450
451
Such a disk is not always empty at the start. Each runner keeps a pool of
452
the disks of passing jobs, by repository, job, cache name and bookmark,
453
named by the ** key** : the BLAKE3 of the sandbox image, the job's name, the
454
cache name, and each key file's content in the job's state. A job's disk
455
starts as, in order: its bookmark's disk with the same key (moved), its
456
bookmark's newest disk (moved), another bookmark's with the same key and
457
then another's newest (copied, so a proposal never takes the default
2594278f Andy McCall 16 days ago ↶
458
bookmark's), and otherwise an empty disk. A microVM's disk must be the
459
size the mount asks for.
d60aac6c Andy McCall 22 days ago ↶
460
461
- ** Only passing jobs keep a disk.** When the job and its outputs pass,
462
its cached disks go back into the pool under their key; otherwise they
463
are discarded with the scratch directory, so nothing a failing job wrote
464
reaches a later one.
465
- ** The receipt says so.** The job's environment gains ` cache TARGET` with
466
the key and what the disk started from, so the receipt's environment
467
hash differs from a cold build's.
468
- ** Bounded.** The pool keeps under ` cache_gb` in the runner's config
469
(default 64 GiB) by dropping the disks used longest ago.
077a6524 Andy McCall 15 days ago ↶
470
- ** Pruned before it is kept** (oxygenflow#72). A kept disk would
471
otherwise carry every build's files (each crate under every hash a
472
lockfile or feature set gave it) until the job's disk is full. When a
473
passing job's cache holds more than ` cache_prune_percent` of its
474
mount's ` size` (runner config, default 60), the files the job did not
475
use are removed, used longest ago first, until it is under that cap; one
476
still over it is not kept, so the next run starts empty. A file was
477
used if it was read, written or changed since the job started, or it
478
shares a unit hash with one that was (a 16-digit hex word after a ` -`
479
in its path, as cargo names a unit's fingerprint, rlibs, binaries and
480
build-script output: a crate cargo found fresh is only looked at, but
481
its fingerprint is read), or is another link of one, or lies under one
482
of the job's outputs. A microVM's guest (` flow-init` ) prunes its disks
483
before it powers off, having mounted them ` strictatime` so every read
484
is seen, and tells the runner what it kept (` \x1eC` frames); without
485
word from it the runner only measures the image (` dumpe2fs` ) and keeps
486
it if it is under the cap. The syscall tier's runner prunes a directory
487
itself, having set each file's atime to the epoch when the job took it,
488
so even a ` relatime` file system records the job's first read of each;
489
on a ` noatime` one nothing is pruned and only the size decides. The
490
job's log says what happened: ` cache target: kept 12.3 GiB of 23.1 GiB
491
(pruned 10.8 GiB in 4113 files this run did not use; the cap is 60% of
492
its 24.0 GiB disk)` , or ` not kept:` and why.
82cf6e91 Andy McCall 22 days ago ↶
493
- ** Unchanged sources keep their times** (oxygenflow#52). Beside each kept
494
disk the pool keeps its sources: each workspace file's content hash and
495
the mtime it had, in whole seconds (what the workspace image keeps). A
496
job that takes the disk gives each file whose content is unchanged that
497
mtime back before the workspace is imaged; a changed or new file keeps
498
the time of its checkout, newer than anything on the disk. A build tool
499
that judges freshness by time, such as cargo, then rebuilds exactly the
500
crates whose sources changed, where a fresh checkout would make every
de1d7926 Andy McCall 22 days ago ↶
501
path crate look changed. Directories are kept the same way, by their
502
listing (the names in them): an unchanged one gets its old time back,
503
one that gained or lost an entry keeps its new time, so a build
504
script's ` rerun-if-changed` on a directory sees exactly what changed.
505
The log says how many files and directories kept their times. A build
506
script that reads a per-state value (the Oxygen crates' ` FLOW_HEADS` )
507
reruns on every state; those crates read it only when ` OXY_BUILD` is
508
unset, so a pipeline that sets ` OXY_BUILD` keeps them fresh.
82cf6e91 Andy McCall 22 days ago ↶
509
- ** Jobs go where their cache is** (oxygenflow#51). Each claim lists the
510
caches the runner holds a disk of (` caches` , each
511
` flow_core::api::cache_slot(repo, job)` ), and the hub ranks a runner
512
holding a job's cache above one holding its image (§11.4): a job another
513
free runner holds the cache for is held for it for up to 15 seconds, and
1e6b9fb1 Andy McCall 22 days ago ↶
514
` placed` says "holds its build cache". When several hold it, the
515
freshest copy wins (oxygenflow#54): claims also say when each slot last
516
kept a disk (` cache_times` , Unix ms), and a copy kept at least a minute
517
later ranks above an older one, before the image and speed are
518
weighed; ` placed` says how old the copy is.
2594278f Andy McCall 16 days ago ↶
519
- ** The syscall tier keeps directories** (oxygenflow#70). Its
520
` ephemeral://disk` mount is a host directory bound at the target, so
521
its pool holds directories (` <key>.tree` ) beside the microVM's images
522
(` <key>.ext4` ), taken and kept the same way: moved out of the pool for a
523
job of the same bookmark (so two jobs of one slot running at once never
524
take the same one), copied with their modes and times from another
525
bookmark's, kept only when the job and its outputs pass and otherwise
526
thrown away, and counted by the blocks of everything in them toward
527
` cache_gb` . A directory has no size, so ` size` does not choose between
528
them. Each tier takes only its own form: a syscall job never starts from
529
a microVM's image, nor a microVM from a directory. The job runs as the
530
runner's user, so what it wrote is the runner's; before a directory goes
531
into the pool nothing in it is left writable by group or others, or
532
set-id, and the pool is readable by the runner alone. Claims report the
533
slots of both forms in ` caches` and ` cache_times` , so jobs go where
534
their cache is whichever tier they run in.
d60aac6c Andy McCall 22 days ago ↶
535
- A cache is a speed-up, never a source of truth: jobs must build
536
correctly from an empty disk, which a key change or an eviction gives
537
them at any time.
538
a083cc80 Andy McCall 1 month ago ↶
539
## 6. Commutative Execution & Change Caching
540
541
In Oxygen, changes that do not share scopes (referenced atoms, placements, or
542
nodes) commute. OxygenFlow exploits this invariant to provide granular caching.
543
544
### 6.1 Execution Receipt Object
545
Every successful or failed job run emits an immutable Receipt:
546
547
``` cbor
548
Receipt {
549
version [0]: 1
550
job_name [1]: text
551
pipeline_hash [2]: ObjectRef
552
scope_tree_hash [3]: ObjectRef // BLAKE3 tree hash of the materialized scope
553
inputs [4]: [ObjectRef] // Artifacts and input files consumed
554
environment [5]: ObjectRef // Hash of rootfs, compiler flags, and argv
555
outputs [6]: [ObjectRef] // Produced artifact blobs
556
exit_code [7]: u32
557
duration_ms [8]: u64
558
}
559
```
560
561
A Receipt is hashed as an Oxygen object with type name "receipt":
562
ObjectRef = BLAKE3-256("oxy1 receipt " || decimal(len) || 0x00 || body)
563
564
### 6.2 Cache Re-use Invariant
565
Let Scope(J) be the file paths and tree nodes declared by job J.
566
Because changes that do not share scopes commute, we do not need to track lists of ChangeHashes. Instead, we hash the materialized tree of Scope(J).
567
568
If an existing Receipt R exists such that:
569
R.job_name == J.name & &
570
R.pipeline_hash == Hash(J) & &
571
R.scope_tree_hash == TreeHash(Scope(J)) & &
572
R.environment == CurrentEnvironmentHash
573
574
Then the job execution is bypassed. R.outputs are mapped into downstream
575
dependents immediately as a cached-commuted hit.
576
577
## 7. Native Provenance Specification
578
579
OxygenFlow formalizes build provenance as a native, tamper-evident Oxygen Attest
580
object (kind: "attest"). Provenance is generated exclusively by the runner
581
hypervisor, not the guest payload.
582
583
### 7.1 Provenance Object Schema
584
585
``` json
586
Attest {
587
version [0]: 1
588
kind [1]: "attest"
589
repo [2]: bytes(16) // Oxygen Repository ID
590
hlc [3]: HLC // Hybrid Logical Clock
591
body [4]: {
592
"type": "flow/provenance/v1",
593
594
// Subject artifacts produced by the build
595
"subjects": [
596
{
597
"name": text,
598
"ref": ObjectRef, // BLAKE3 hash of output blob
599
"media_type": text
600
}
601
],
602
603
// Builder platform and execution identity
604
"builder": {
605
"runner_id": KeyId, // ObjectRef of runner's Oxygen Identity
606
"isolation_level": text, // "wasm" | "gvisor" | "microvm"
607
"hypervisor_image": ObjectRef // Rootfs blob digest
608
},
609
610
// Exact input materials
611
"materials": {
612
"state_heads": [ChangeHash], // Oxygen heads of the state built
613
"scope_tree_hash": ObjectRef, // Captures exact state without O(N) array bloat
614
"inputs": [
615
{
616
"name": text,
617
"ref": ObjectRef // Content hash of locked dependencies
618
}
619
]
620
},
621
622
// Structured execution arguments
623
"invocation": {
624
"job": text,
625
"pipeline_spec": ObjectRef, // ObjectRef of .flow/pipeline.kdl
626
"argv": [text], // Unmodified execution vector
627
"env": map<text, text> // Explicitly whitelisted environment
628
},
629
630
// Environment observations
631
"metadata": {
632
"started_on": u64, // Wallclock milliseconds
633
"finished_on": u64,
634
"hermetic": bool, // Verified zero outbound network access
635
"reproducible": bool
636
}
637
}
638
}
639
```
640
641
### 7.2 Composite Signatures
642
All provenance attestations are signed using Oxygen's composite signature
643
construction (Ed25519 and ML-DSA-65):
644
645
``` cbor
646
Signature {
647
key [0]: KeyId
648
alg [1]: 1 // 1 = Ed25519 + ML-DSA-65
649
ed [2]: bytes(64) // Classical signature
650
ml [3]: bytes(3309) // Post-quantum signature
651
}
652
```
653
654
Both signatures are computed over:
655
"oxy1 sig attest\0" || KeyId || ObjectRef
656
657
## 8. Policy Integration & Bookmark Gates
658
659
Oxygen repositories enforce branch and bookmark protections via signed Policy
660
objects (Oxygen Spec Section 14.5). OxygenFlow integrates provenance directly into
661
policy evaluation during push validation.
662
663
### 8.1 Policy Gate Definition
664
A Policy rule governing a bookmark MAY demand valid OxygenFlow attestations. To mitigate a compromised authorized worker, high-security repositories can require ** Consensus Builds** by setting the threshold > 1. This requires multiple independent runner identities to arrive at the exact same output hashes and both sign it.
665
666
``` json
667
PolicyRule {
668
pattern: "bookmarks/production"
669
keys: [ KeyId ]
670
threshold: 1
671
require: "signed"
672
allow_conflicts: false
673
require_attestations: [
674
{
675
type: "flow/provenance/v1",
676
job: "release-provenance",
677
require_hermetic: true,
678
signer_roles: [ "official-worker" ]
679
}
680
]
681
}
682
683
### 8.2 Push Verification Sequence (VCS Engine Enforcement)
684
When a push request arrives at an Oxygen server attempting to update a
685
protected bookmark:
686
1. Object Integrity: Validate that all changes, chunks, and blobs exist and match
687
their BLAKE3 hashes.
688
2. Attestation Resolution: Verify that an Attest object of type
689
"flow/provenance/v1" is present, referencing the exact heads of the new
690
State.
691
3. Identity Verification: Validate that the attestation's composite signature
692
resolves to a KeyId listed as an authorized delegate of the repository
693
policy roots.
694
4. Invariant Checking: Confirm that the provenance statement records
695
"hermetic: true" and exit code 0.
696
5. Atomic Update: If validation fails at any point, the entire push is refused
697
with ok = false.
698
699
## 9. Zero-Environment Secret Brokering
700
701
OxygenFlow forbids the injection of raw secret strings into process environment
702
tables to eliminate accidental leaking via child process dumping, procfs
703
reads, and verbose compiler logging.
704
705
### 9.1 Ephemeral Secret Broker
706
1. If a job requires a credential (e.g., signing key, cloud deployment token),
707
it declares a secret mount:
708
secret "deploy-token" {
709
vault-ref "infra/tokens/prod"
710
mount-path "/run/secrets/token"
711
}
712
2. The runner negotiates a short-lived token lease from the configured vault.
713
3. The secret is mounted onto an ephemeral, in-memory ramfs location inside
714
the sandbox with access mode 0400, owned exclusively by the worker process.
715
4. Upon process termination, the memory pages are zeroized and the token is
716
marked for immediate revocation at the vault.
717
718
## 10. Conformance and Verification Requirements
719
720
A conforming OxygenFlow implementation MUST pass the following tests:
721
722
1. Injection Immunity: Attempting to supply unescaped shell metacharacters
723
(; && | ` $() \n) in any job field MUST pass those characters as literal
724
bytes to argv; subshells MUST NOT be spawned.
725
2. FastCDC Determinism: Chunks produced by artifact extraction MUST match
726
the gear table cuts and BLAKE3 hashes generated by Oxygen's core chunker.
727
3. Commutation Hit Invariance: For any two commuting changes A and B, running
728
a pipeline on State(A, B) MUST reuse cached receipts generated on State(A)
729
for any job whose scope does not intersect the touched paths of B.
730
4. Non-Downgrade Attestation: An attestation missing the ML-DSA-65 component
731
MUST be rejected by policy verification.
732
5. Hermetic Network Isolation: Jobs configured with network "isolated" MUST
fd2d4822 Andy McCall 1 month ago ↶
733
have outgoing TCP SYN and UDP packets dropped at the hypervisor boundary.
630ee660 Andy McCall 1 month ago ↶
734
## 11. Runner Capabilities and Placement (proposed 2026-10-04; §11.2 and `arch` of §11.3 done the same day; tiers of §11.1, §11.5 and the waiting reasons of §11.4 done 2026-10-05, oxygenflow#36)
fd2d4822 Andy McCall 1 month ago ↶
735
736
A runner MUST never take a job it cannot run or cannot finish well: the
737
wrong architecture, no hardware virtualisation for the MicroVM tier, too
738
little memory, an image it does not hold, or a machine too slow for the
739
job's time limit. Today a runner reports its architecture, OS, labels,
740
job slots, memory and vCPUs when it registers and on each claim, and a
741
claim carries the tiers it can run, the memory and vCPUs free now, and
742
the images it holds; the hub matches tier, room, labels, scope and image.
743
What is missing: architecture is reported but never matched (an image's
744
architecture is implicit in its bytes), a pipeline cannot say which
745
architecture it needs or offer a rootfs per architecture, speed and load
746
play no part, and a runner cannot decline a job it was handed.
747
748
### 11.1 Capability Profile
749
750
At registration and on every claim a runner sends its profile:
751
752
| Field | Meaning |
753
|---|---|
754
| `arch`, `os`, `kernel` | `x86_64`, `aarch64`, `riscv64`, …; the kernel release |
755
| `tiers` | The sandbox tiers it can honour: `microvm` only with `/dev/kvm` usable (a board without KVM, such as an OrangePi RV2, offers `syscall`, §4.2, and `wasm`) |
756
| `memory_mib`, `cpus`, `max_jobs` | What its jobs share |
757
| `images` | The rootfs images it holds, each with the architecture its manifest records (§11.2) |
758
| `labels` | As §3.1 |
62829d15 Andy McCall 24 days ago ↶
759
| `speed` | A score and a class from 1 (slow) to 5 (fast), measured by the runner at start, again every 15 minutes while it runs no job, and by `flow runner bench`: a fixed one-thread workload (BLAKE3 over 256 MiB, and a sort of 4 Mi words, where a C compiler cannot be assumed) whose time on the reference machine (Earth, about 225 ms) scores 100; class 3 is 70–139, each class a factor of two (done 2026-10-11, `facts` on the claim) |
760
| `load` | The 1-minute load average (×100), and the free disk where jobs' disks are made, sent with every claim (`facts`); memory free and jobs running as before |
fd2d4822 Andy McCall 1 month ago ↶
761
762
### 11.2 Image Manifests Carry the Architecture
763
764
`flow rootfs import` reads the ELF header of the image's `/bin/sh` and
765
records its architecture in the CAS manifest beside the hash. A runner
766
refuses to import an image for another architecture (there is no
0077c748 Andy McCall 22 days ago ↶
767
emulation tier).
fd2d4822 Andy McCall 1 month ago ↶
768
0077c748 Andy McCall 22 days ago ↶
769
An image whose manifest records no architecture (imported before this
770
section, or fetched from a hub that did not know it) is read, not
771
guessed: `jet::rootfs` follows `/bin/sh` (else busybox, env or bash)
772
through the image's ext4, symlinks included, to its ELF header, with no
773
mount and no privileges. The runner records the answer in the manifest,
774
so each image is read once, reports it with its claims, and declines a
775
job whose image is for another architecture instead of running it and
776
failing (§11.5). The hub reads the images it keeps the same way
777
(OxygenHub, oxygenhub#173), and an image nobody can read is offered only
778
to the architecture its jobs have passed on before.
779
fd2d4822 Andy McCall 1 month ago ↶
780
### 11.3 Job Requirements
781
782
Hard requirements, all of which a runner MUST satisfy to be offered the
783
job: the sandbox's tier, its rootfs (hence the image's architecture),
784
`memory-limit`, `cpus`, every `runs-on` label of the job and its sandbox,
785
and the runner's scope (OxygenHub spec §13.2). Two additions to the
786
language:
787
788
- `arch "riscv64"` on a sandbox pins the architecture; without it the
789
architecture is the rootfs image's.
790
- A rootfs per architecture: `rootfs { x86_64 "cas://blob/…"; riscv64 "cas://blob/…" }`.
791
The job runs on whichever architecture takes it; the receipt and the
792
provenance record the architecture and image used, and a check's
793
description names them.
794
795
Soft requirements guide placement but never block it: a job's `timeout`
796
against the runner's speed class. The hub keeps, per job hash and speed
797
class, the median duration of past runs; a runner whose class predicts a
798
duration over the limit is offered the job only when no better runner
799
is free.
800
801
### 11.4 Placement
802
803
The hub filters by the hard requirements, then ranks the runners that
804
remain: idle before busy (`load`), a higher speed class first, a runner
805
already holding the image before one that must fetch it (§5), fewer jobs
806
waiting on it first. A job no runner can satisfy waits and the queue
807
says what it waits for: tier, architecture, image, memory, labels or
808
scope, so the gap is visible rather than a silent stall.
809
810
### 11.5 Declining
811
812
A runner double-checks a claim before it starts: the architecture, that
813
`/dev/kvm` opens for the MicroVM tier, that the image's blob is present
814
and verifies, that the memory is free. If any check fails it answers the
815
hub with `declined` and a reason instead of running and failing; the hub
816
re-queues the job, never offers it to that runner again until its
817
profile changes, records the decline on the runner's page, and after
818
three declines by different runners marks the job `unplaceable` with the
819
reasons. A decline is never a failed check on the state.
820
630ee660 Andy McCall 1 month ago ↶
821
As built: the runner sends `POST /api/v1/flow/jobs/{id}/decline` with
822
`{"reason": …}` before it fetches anything. The profile is a hash of the
823
tiers, images, architecture, labels and capacity a runner claims with.
824
A decline also lapses after ten minutes, so a passing reason such as
825
memory in use does not bar a runner for good. An unplaceable job ends
826
with status `error` and an error beginning `unplaceable:`. The hub
827
works out why each ready job waits every 15 seconds, and the run page and
828
the API (`waiting`) show it.
829
89171fca Andy McCall 22 days ago ↶
830
### 11.5.1 Restarts (oxygenflow#49, #50)
831
832
Neither a hub restart nor a runner restart costs a job anything.
833
834
- **The hub away.** A job's uploads (artifacts, report files) and declines
835
are tried again through refused connections and server errors (5xx,
836
as a proxy answers while the hub restarts) for three minutes, waiting
837
1, 2, 4, 8 and then 15 seconds; a refusal (4xx) is final. Log lines
838
the hub could not take are kept and sent with the next batch, 200 lines
839
at a time, up to 50,000 (beyond that the oldest go, and the log says how
840
many). A job's result is kept on disk and offered for two hours, as
841
before. Claims are not retried through a lost connection, so a job is
842
never claimed twice; the runner simply asks again.
843
- **The runner restarted.** The first SIGTERM or SIGINT drains the runner:
844
it claims nothing more and stops once every job it holds has ended and
845
been reported. A second signal stops it at once. Its systemd unit says
846
`KillMode=mixed`, so only the runner is signalled and its microVMs run
847
on, and `TimeoutStopSec` longer than the longest job, after which
848
systemd kills what is left. A new binary is installed by replacing it
849
and restarting the unit (`systemctl restart --no-block`): the old runner
850
finishes its job, and `Restart=always` starts the new one.
4dd392d1 Andy McCall 9 days ago ↶
851
`flow runner drain` asks the same by a `drain.request` file in the
852
runner's home, which its loop reads: the way on Windows, which has no
853
SIGTERM (oxygenflow#83).
89171fca Andy McCall 22 days ago ↶
854
06a4c472 Andy McCall 10 days ago ↶
855
### 11.5.2 Handing back (oxygenflow#76)
856
857
A runner that leaves while it holds a job, with no time to finish it,
858
hands the job back: `POST /api/v1/flow/jobs/{id}/release` with
859
`{"reason": …}` (a phone off its allowed network, docs/handover.md §6).
860
The hub queues the job again **ahead of new work**, whatever its
861
priority, adds a line to its log saying who handed it back and why, and
862
lets the next attempt's log follow on. It is not a decline: the runner is
863
not held from the job and it does not count towards unplaceable.
864
865
**Leases.** Holding a job is a lease the runner keeps by being heard from:
866
claims, a heartbeat every 30 seconds while it claims nothing, its jobs'
867
logs. When the hub has not heard from a runner for 3 minutes, its jobs
868
are handed back the same way, so a runner that vanishes (out of every
869
network, crashed) costs a restart of its job, not the job. A job handed
870
back more than 5 times ends as an error saying so.
871
1b160d37 Andy McCall 10 days ago ↶
872
**Notice.** A departure may come with notice (a phone taken off charge
873
has a minute; a spot reclaim 30 seconds to 2 minutes). A claim carries
874
`expected_ms`, how long the job is expected to take, when the hub has
875
learned it: the median of the job's last five passing runs in that
876
repository (reused passes left out). A leaving runner finishes and
877
reports a job whose expected rest fits within the notice, and hands back
878
the others at once, and any job still running when the notice is up. A
879
job with no expected time is handed back at once.
880
f201e1bc Andy McCall 10 days ago ↶
881
**Checkpoints (#77).** A job may name directories that hold its progress:
882
`checkpoint "/src/target"` (one or more, under /src, not /src itself).
883
When its runner hands it back, the runner packs them (files with their
884
modes and modification times, which cargo's fingerprints read; at most
885
512 MiB) and keeps them with `POST /api/v1/flow/jobs/{id}/checkpoint`
886
before the release; the claim of the next attempt names the blob
887
(`checkpoint`), and that runner fetches it (`GET` on the same path),
888
checks it, and restores it over the job's cache before running the same
889
exec, so a tool that skips finished work carries on. The receipt lists
890
the checkpoint among the job's inputs. As built: the WASM and syscall
891
tiers save and restore checkpoints, under a disk mount or in the tree;
892
the MicroVM tier, whose files are in an image, starts again for now.
893
1b160d37 Andy McCall 10 days ago ↶
894
As built (flow 0.3.2, OxygenHub 0.9.4): the runner as a library has
895
`runner::leave(reason)` (no notice) and `runner::leave_within(reason,
896
notice)` beside `runner::stop()`: each claims nothing more and stops the
897
jobs it does not finish (their logs sent first), releasing them; if the
06a4c472 Andy McCall 10 days ago ↶
898
release cannot be sent, the lease does it. A host's SIGTERM still drains
1b160d37 Andy McCall 10 days ago ↶
899
by finishing what it holds. Checkpoints (#77), so the next attempt
900
resumes rather than restarts, come next.
06a4c472 Andy McCall 10 days ago ↶
901
85275e1e Andy McCall 10 days ago ↶
902
### 11.5.3 Updating a runner (oxygenflow#85)
903
904
`flow runner update --from FILE` installs a new `flow` build in place of
905
the one it runs as (`--exe` names another), the runner's side of
906
self-updating runners (docs/jetstream.md §8.2):
907
908
1. **Refused** when the home's `flow.toml` says `[runner.update] allow =
909
false`, when an earlier update is not yet proven, or when the build is
910
older than the installed one (unless `--allow-older`).
911
2. **Checked:** a 64-bit little-endian ELF for this machine; it names
912
itself (`--version`, `flow X.Y.Z …`) and finds a tier it can run here
913
(`tiers` against the runner's home), each within 30 seconds.
914
3. **Staged and swapped:** copied beside the installed binary as
915
`flow.new` and checked there; the installed one is kept as
916
`flow.prev` (a hard link, else a copy), then `flow.new` replaces it in
917
one rename, so `flow` is never missing. `--dry-run` stops before the
918
swap and changes nothing.
919
4. **The running runner leaves:** a runner writes its pid to
920
`runner.pid` in its home, and the update sends it SIGUSR1 (after
921
checking that pid is still a `flow … runner`). It then leaves with
922
`--notice` (default 600 s, §11.5.2): jobs expected to finish in time
923
finish, the rest are handed back with their checkpoints, and it exits
924
0, so its unit's `Restart=always` starts the new build. A runner from
925
before #85 is not told; restarting its unit drains it as before.
926
5. **Proven or rolled back:** the update waits in `update.json` until the
927
new build's first claim gets an answer from the hub. Each start counts;
928
on the fourth start without that answer, or after ten minutes running
929
without it, `flow.prev` is put back (and on a start, run in its place).
930
`updates.log` in the home records every install, proof and rollback.
8aa45fa6 Andy McCall 9 days ago ↶
931
The runner tells the hub at its next answer (`POST /api/v1/flow/runner/update`):
932
`runs flow X (was Y); proven`, or `rolled back from flow X to Y: why`,
933
on which the hub holds X back from every other runner until an
934
administrator lifts the hold (oxygenhub#326).
494a1b9a Andy McCall 9 days ago ↶
935
6. **On Windows** (oxygenflow#83) the same steps run with Windows' means:
936
the build is checked as a PE executable for this machine; `flow.exe`
937
is renamed to `flow.prev.exe` and `flow.new.exe` into its place (a
938
running program can be renamed but not replaced, and cannot rename
939
its own file, so a copy of it, `flow __rename`, does that); the runner
940
is told by an `update.request` file in its home, which it reads in its
941
loop, there being no SIGUSR1; and having left, it exits 75, so a Task
942
Scheduler task set to restart on failure starts the new build. A
943
rollback runs the restored build in its place and ends with it.
85275e1e Andy McCall 10 days ago ↶
944
fd2d4822 Andy McCall 1 month ago ↶
945
### 11.6 Conformance
946
947
1. Architecture Safety: a job whose image is `x86_64` MUST never be
948
offered to a `riscv64` runner, with or without the image copied there.
949
2. Tier Safety: a runner without `/dev/kvm` MUST NOT be offered a
950
MicroVM job, and MUST decline one if handed it.
951
3. Room: a job whose `memory-limit` exceeds the runner's free memory
952
MUST wait, not be killed.
953
4. Visibility: a job nobody can take MUST say why on the queue within
954
one scheduling pass.
955
5. Preference: with two idle runners that both satisfy a job, the hub
956
MUST offer it to the higher speed class, and to the one holding the
957
image when classes tie.