OXYGENHUB
◆ push-uses-login oxygenflow / docs / spec.md
25.3 KiB · Markdown · last changed Runners send their facts with every claim: free disk, load, speed 24 days ago renderedsource blame history raw
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
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
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
94
    network "isolated"      // Options: "isolated" | "loopback" | "egress"
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
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
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
182
**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.
183
184
### 3.2 Canonical Compilation
185
Upon commit, .flow/pipeline.kdl is validated and compiled into a deterministic
186
CBOR job-graph blob. Runners evaluate the canonical CBOR to prevent syntax or
187
parser ambiguities across varying client platforms.
188
189
## 4. Sandbox Execution Engine
190
191
OxygenFlow implements three distinct isolation tiers depending on the workload:
192
193
### 4.1 Level 0: WASM Component Sandbox
194
- Intended for static linters, formatters, doc generators, and policy checks.
195
- Runs on a standard WebAssembly System Interface (WASI) runtime.
196
- Startup latency: < 5 ms.
197
- Zero host I/O; virtualized file system presented in-memory.
198
199
### 4.2 Level 1: Intercepted Syscall Isolation (gVisor / Landlock + seccomp-bpf)
200
- Intended for standard compilation and hermetic test suites.
201
- Emulates or filters Linux system call interfaces in user space.
202
- Blocks access to raw network sockets, real-time hardware clocks, and device
203
  nodes.
204
- Memory and execution CPU quotas are strictly bounded via cgroups v2.
205
206
### 4.3 Level 2: MicroVM Isolation (Firecracker / Cloud-Hypervisor)
207
- Intended for kernel tests, multi-container integration, and untrusted code.
208
- Ephemeral Linux microVM booting in < 80 ms.
209
- The root filesystem is presented as a read-only virtio-block device mapped
210
  directly to FastCDC chunks stored in Oxygen's CAS.
211
- Guest VM memory is cleared upon termination.
212
213
## 5. Content-Addressable Storage (CAS) & Zero-Copy Mounting
214
215
OxygenFlow runners bypass traditional clone operations. Repositories are accessed
216
directly via the local object store (.oxy/objects/) or streamed on demand
217
using Oxygen's wire protocol.
218
219
### 5.1 FUSE Overlay Mounting (flow-casfs)
220
1. The runner receives a task targeting an Oxygen State ObjectRef (a sorted
221
   list of maximal heads).
222
2. The state's tree is read from a state index (Oxygen's flat, checksummed
223
   description of a history-only state: its changes, tree view and each
224
   file's deferred history with its graph key), built once from the
225
   nearest checkpoint plus the changes after it and kept beside the store.
226
   Listing a directory needs nothing else.
227
3. The tree is exposed to the sandbox read-only through flow-casfs (FUSE):
228
   - An opaque file's bytes are its stored blob, whose chunks (FastCDC,
229
     32 KiB to 256 KiB, mean 64 KiB, Oxygen spec Section 13.2) are read as
230
     they are needed.
231
   - A text file's bytes are a rendering of its history, not a stored blob.
232
     Renderings are cached by **graph key** (equal keys mean equal
233
     renderings) as ordinary Blob and Chunk objects: a file is rendered on
234
     first read or stat, and every later state in which it has the same
235
     history finds it at once. A file added once and never edited is its
236
     own text blob and needs no rendering at all.
237
   - Missing objects are fetched lazily using the POST /objects protocol.
238
   - To prevent FUSE context-switch bottlenecks on many small reads,
239
     flow-casfs caches whole small files and mmaps chunk data, relying on
240
     the host page cache.
241
   - All guest mutations are diverted to an overlay on the job's ephemeral
242
     disk (or a tmpfs); base objects remain immutable.
243
4. The same mount backs web workspaces (OxygenHub spec 13.3): a
244
   `workspace` job keeps it for as long as its editor is open.
245
5. Targets, measured on the Linux tip: mount under 100 ms, a first read of
246
   a file in milliseconds, and a full read of the tree no slower than a
247
   checkout writing it.
248
249
### 5.2 Artifact Capture
250
1. At task completion, flow-casfs scans the paths declared in the output block.
251
2. Modified outputs are chunked with FastCDC, hashed via BLAKE3-256, and
252
   written as canonical Blob and Chunk objects.
253
3. Output ObjectRefs are stored directly in the Oxygen repository.
254
4. Outputs are read when the job passes. An artifact with `when "always"`
255
   (default `when "passed"`) is also read when the job fails or is stopped
256
   at its time limit, so a test report survives the failure it describes.
257
   From a job that did not pass it is kept and shown, but its receipt never
258
   names it, and one that cannot be read is only noted in the log.
259
260
```kdl
261
output {
262
    artifact "junit" {
263
        path "/src/target/junit.xml"
264
        when "always"
265
    }
266
}
267
```
268
269
5. A `report` output (OxygenHub `docs/reports.md`) names a directory
270
   holding `report.toml` at its top. The runner reads its regular files
271
   (links and anything else are left out, never followed; file names hold
272
   letters, digits, `.`, `_`, `-` and `+`; at most 2000 files and 512 MiB),
273
   uploads each to `POST /api/v1/flow/jobs/{job}/reports/{name}/files` with
274
   its path in `X-Flow-Report-Path`, then uploads a manifest,
275
   `{"version":1,"files":[{"path","blob","size"}]}`, as the output under
276
   the report's name, checking every blob the hub answers. The receipt
277
   names the manifest. Reports and artifacts share names; a report's kind
278
   is part of the job's hash (`[name, path, "report"]`).
279
280
```kdl
281
output {
282
    report "frame-timing" {
283
        path "/src/out/report"
284
        when "always"
285
    }
286
}
287
```
288
289
## 6. Commutative Execution & Change Caching
290
291
In Oxygen, changes that do not share scopes (referenced atoms, placements, or
292
nodes) commute. OxygenFlow exploits this invariant to provide granular caching.
293
294
### 6.1 Execution Receipt Object
295
Every successful or failed job run emits an immutable Receipt:
296
297
```cbor
298
Receipt {
299
  version        [0]: 1
300
  job_name       [1]: text
301
  pipeline_hash  [2]: ObjectRef
302
  scope_tree_hash [3]: ObjectRef     // BLAKE3 tree hash of the materialized scope
303
  inputs         [4]: [ObjectRef]   // Artifacts and input files consumed
304
  environment    [5]: ObjectRef     // Hash of rootfs, compiler flags, and argv
305
  outputs        [6]: [ObjectRef]   // Produced artifact blobs
306
  exit_code      [7]: u32
307
  duration_ms    [8]: u64
308
}
309
```
310
311
A Receipt is hashed as an Oxygen object with type name "receipt":
312
ObjectRef = BLAKE3-256("oxy1 receipt " || decimal(len) || 0x00 || body)
313
314
### 6.2 Cache Re-use Invariant
315
Let Scope(J) be the file paths and tree nodes declared by job J.
316
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).
317
318
If an existing Receipt R exists such that:
319
  R.job_name == J.name &&
320
  R.pipeline_hash == Hash(J) &&
321
  R.scope_tree_hash == TreeHash(Scope(J)) &&
322
  R.environment == CurrentEnvironmentHash
323
324
Then the job execution is bypassed. R.outputs are mapped into downstream
325
dependents immediately as a cached-commuted hit.
326
327
## 7. Native Provenance Specification
328
329
OxygenFlow formalizes build provenance as a native, tamper-evident Oxygen Attest
330
object (kind: "attest"). Provenance is generated exclusively by the runner
331
hypervisor, not the guest payload.
332
333
### 7.1 Provenance Object Schema
334
335
```json
336
Attest {
337
  version [0]: 1
338
  kind    [1]: "attest"
339
  repo    [2]: bytes(16)               // Oxygen Repository ID
340
  hlc     [3]: HLC                     // Hybrid Logical Clock
341
  body    [4]: {
342
    "type": "flow/provenance/v1",
343
    
344
    // Subject artifacts produced by the build
345
    "subjects": [
346
      {
347
        "name": text,
348
        "ref": ObjectRef,              // BLAKE3 hash of output blob
349
        "media_type": text
350
      }
351
    ],
352
353
    // Builder platform and execution identity
354
    "builder": {
355
      "runner_id": KeyId,              // ObjectRef of runner's Oxygen Identity
356
      "isolation_level": text,         // "wasm" | "gvisor" | "microvm"
357
      "hypervisor_image": ObjectRef    // Rootfs blob digest
358
    },
359
360
    // Exact input materials
361
    "materials": {
362
      "state_heads": [ChangeHash],     // Oxygen heads of the state built
363
      "scope_tree_hash": ObjectRef,        // Captures exact state without O(N) array bloat
364
      "inputs": [
365
        {
366
          "name": text,
367
          "ref": ObjectRef             // Content hash of locked dependencies
368
        }
369
      ]
370
    },
371
372
    // Structured execution arguments
373
    "invocation": {
374
      "job": text,
375
      "pipeline_spec": ObjectRef,      // ObjectRef of .flow/pipeline.kdl
376
      "argv": [text],                  // Unmodified execution vector
377
      "env": map<text, text>           // Explicitly whitelisted environment
378
    },
379
380
    // Environment observations
381
    "metadata": {
382
      "started_on": u64,               // Wallclock milliseconds
383
      "finished_on": u64,
384
      "hermetic": bool,                // Verified zero outbound network access
385
      "reproducible": bool
386
    }
387
  }
388
}
389
```
390
391
### 7.2 Composite Signatures
392
All provenance attestations are signed using Oxygen's composite signature
393
construction (Ed25519 and ML-DSA-65):
394
395
```cbor
396
Signature {
397
  key [0]: KeyId
398
  alg [1]: 1                           // 1 = Ed25519 + ML-DSA-65
399
  ed  [2]: bytes(64)                   // Classical signature
400
  ml  [3]: bytes(3309)                 // Post-quantum signature
401
}
402
```
403
404
Both signatures are computed over:
405
  "oxy1 sig attest\0" || KeyId || ObjectRef
406
407
## 8. Policy Integration & Bookmark Gates
408
409
Oxygen repositories enforce branch and bookmark protections via signed Policy
410
objects (Oxygen Spec Section 14.5). OxygenFlow integrates provenance directly into
411
policy evaluation during push validation.
412
413
### 8.1 Policy Gate Definition
414
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.
415
416
```json
417
PolicyRule {
418
  pattern: "bookmarks/production"
419
  keys: [ KeyId ]
420
  threshold: 1
421
  require: "signed"
422
  allow_conflicts: false
423
  require_attestations: [
424
    {
425
      type: "flow/provenance/v1",
426
      job: "release-provenance",
427
      require_hermetic: true,
428
      signer_roles: [ "official-worker" ]
429
    }
430
  ]
431
}
432
433
### 8.2 Push Verification Sequence (VCS Engine Enforcement)
434
When a push request arrives at an Oxygen server attempting to update a
435
protected bookmark:
436
1. Object Integrity: Validate that all changes, chunks, and blobs exist and match
437
   their BLAKE3 hashes.
438
2. Attestation Resolution: Verify that an Attest object of type
439
   "flow/provenance/v1" is present, referencing the exact heads of the new
440
   State.
441
3. Identity Verification: Validate that the attestation's composite signature
442
   resolves to a KeyId listed as an authorized delegate of the repository
443
   policy roots.
444
4. Invariant Checking: Confirm that the provenance statement records
445
   "hermetic: true" and exit code 0.
446
5. Atomic Update: If validation fails at any point, the entire push is refused
447
   with ok = false.
448
449
## 9. Zero-Environment Secret Brokering
450
451
OxygenFlow forbids the injection of raw secret strings into process environment
452
tables to eliminate accidental leaking via child process dumping, procfs
453
reads, and verbose compiler logging.
454
455
### 9.1 Ephemeral Secret Broker
456
1. If a job requires a credential (e.g., signing key, cloud deployment token),
457
   it declares a secret mount:
458
     secret "deploy-token" {
459
         vault-ref "infra/tokens/prod"
460
         mount-path "/run/secrets/token"
461
     }
462
2. The runner negotiates a short-lived token lease from the configured vault.
463
3. The secret is mounted onto an ephemeral, in-memory ramfs location inside
464
   the sandbox with access mode 0400, owned exclusively by the worker process.
465
4. Upon process termination, the memory pages are zeroized and the token is
466
   marked for immediate revocation at the vault.
467
468
## 10. Conformance and Verification Requirements
469
470
A conforming OxygenFlow implementation MUST pass the following tests:
471
472
1. Injection Immunity: Attempting to supply unescaped shell metacharacters
473
   (; && | ` $() \n) in any job field MUST pass those characters as literal
474
   bytes to argv; subshells MUST NOT be spawned.
475
2. FastCDC Determinism: Chunks produced by artifact extraction MUST match
476
   the gear table cuts and BLAKE3 hashes generated by Oxygen's core chunker.
477
3. Commutation Hit Invariance: For any two commuting changes A and B, running
478
   a pipeline on State(A, B) MUST reuse cached receipts generated on State(A)
479
   for any job whose scope does not intersect the touched paths of B.
480
4. Non-Downgrade Attestation: An attestation missing the ML-DSA-65 component
481
   MUST be rejected by policy verification.
482
5. Hermetic Network Isolation: Jobs configured with network "isolated" MUST
483
   have outgoing TCP SYN and UDP packets dropped at the hypervisor boundary.
484
## 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)
485
486
A runner MUST never take a job it cannot run or cannot finish well: the
487
wrong architecture, no hardware virtualisation for the MicroVM tier, too
488
little memory, an image it does not hold, or a machine too slow for the
489
job's time limit. Today a runner reports its architecture, OS, labels,
490
job slots, memory and vCPUs when it registers and on each claim, and a
491
claim carries the tiers it can run, the memory and vCPUs free now, and
492
the images it holds; the hub matches tier, room, labels, scope and image.
493
What is missing: architecture is reported but never matched (an image's
494
architecture is implicit in its bytes), a pipeline cannot say which
495
architecture it needs or offer a rootfs per architecture, speed and load
496
play no part, and a runner cannot decline a job it was handed.
497
498
### 11.1 Capability Profile
499
500
At registration and on every claim a runner sends its profile:
501
502
| Field | Meaning |
503
|---|---|
504
| `arch`, `os`, `kernel` | `x86_64`, `aarch64`, `riscv64`, …; the kernel release |
505
| `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`) |
506
| `memory_mib`, `cpus`, `max_jobs` | What its jobs share |
507
| `images` | The rootfs images it holds, each with the architecture its manifest records (§11.2) |
508
| `labels` | As §3.1 |
509
| `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) |
510
| `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 |
511
512
### 11.2 Image Manifests Carry the Architecture
513
514
`flow rootfs import` reads the ELF header of the image's `/bin/sh` and
515
records its architecture in the CAS manifest beside the hash. A runner
516
refuses to import an image for another architecture (there is no
517
emulation tier). An image imported before this section has no recorded
518
architecture and is treated as the importing runner's.
519
520
### 11.3 Job Requirements
521
522
Hard requirements, all of which a runner MUST satisfy to be offered the
523
job: the sandbox's tier, its rootfs (hence the image's architecture),
524
`memory-limit`, `cpus`, every `runs-on` label of the job and its sandbox,
525
and the runner's scope (OxygenHub spec §13.2). Two additions to the
526
language:
527
528
- `arch "riscv64"` on a sandbox pins the architecture; without it the
529
  architecture is the rootfs image's.
530
- A rootfs per architecture: `rootfs { x86_64 "cas://blob/…"; riscv64 "cas://blob/…" }`.
531
  The job runs on whichever architecture takes it; the receipt and the
532
  provenance record the architecture and image used, and a check's
533
  description names them.
534
535
Soft requirements guide placement but never block it: a job's `timeout`
536
against the runner's speed class. The hub keeps, per job hash and speed
537
class, the median duration of past runs; a runner whose class predicts a
538
duration over the limit is offered the job only when no better runner
539
is free.
540
541
### 11.4 Placement
542
543
The hub filters by the hard requirements, then ranks the runners that
544
remain: idle before busy (`load`), a higher speed class first, a runner
545
already holding the image before one that must fetch it (§5), fewer jobs
546
waiting on it first. A job no runner can satisfy waits and the queue
547
says what it waits for: tier, architecture, image, memory, labels or
548
scope, so the gap is visible rather than a silent stall.
549
550
### 11.5 Declining
551
552
A runner double-checks a claim before it starts: the architecture, that
553
`/dev/kvm` opens for the MicroVM tier, that the image's blob is present
554
and verifies, that the memory is free. If any check fails it answers the
555
hub with `declined` and a reason instead of running and failing; the hub
556
re-queues the job, never offers it to that runner again until its
557
profile changes, records the decline on the runner's page, and after
558
three declines by different runners marks the job `unplaceable` with the
559
reasons. A decline is never a failed check on the state.
560
561
As built: the runner sends `POST /api/v1/flow/jobs/{id}/decline` with
562
`{"reason": …}` before it fetches anything. The profile is a hash of the
563
tiers, images, architecture, labels and capacity a runner claims with.
564
A decline also lapses after ten minutes, so a passing reason such as
565
memory in use does not bar a runner for good. An unplaceable job ends
566
with status `error` and an error beginning `unplaceable:`. The hub
567
works out why each ready job waits every 15 seconds, and the run page and
568
the API (`waiting`) show it.
569
570
### 11.6 Conformance
571
572
1. Architecture Safety: a job whose image is `x86_64` MUST never be
573
   offered to a `riscv64` runner, with or without the image copied there.
574
2. Tier Safety: a runner without `/dev/kvm` MUST NOT be offered a
575
   MicroVM job, and MUST decline one if handed it.
576
3. Room: a job whose `memory-limit` exceeds the runner's free memory
577
   MUST wait, not be killed.
578
4. Visibility: a job nobody can take MUST say why on the queue within
579
   one scheduling pass.
580
5. Preference: with two idle runners that both satisfy a job, the hub
581
   MUST offer it to the higher speed class, and to the one holding the
582
   image when classes tie.