**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.
**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.
**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.
"hermetic": bool, // Verified zero outbound network access
334
"reproducible": bool
335
}
336
}
337
}
338
```
339
340
###7.2 Composite Signatures
341
All provenance attestations are signed using Oxygen's composite signature
342
construction (Ed25519 and ML-DSA-65):
343
344
```cbor
345
Signature {
346
key [0]: KeyId
347
alg [1]: 1 // 1 = Ed25519 + ML-DSA-65
348
ed [2]: bytes(64) // Classical signature
349
ml [3]: bytes(3309) // Post-quantum signature
350
}
351
```
352
353
Both signatures are computed over:
354
"oxy1 sig attest\0" || KeyId || ObjectRef
355
356
##8. Policy Integration & Bookmark Gates
357
358
Oxygen repositories enforce branch and bookmark protections via signed Policy
359
objects (Oxygen Spec Section 14.5). OxygenFlow integrates provenance directly into
360
policy evaluation during push validation.
361
362
###8.1 Policy Gate Definition
363
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.
## 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)
| `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`) |
455
| `memory_mib`, `cpus`, `max_jobs` | What its jobs share |
456
| `images` | The rootfs images it holds, each with the architecture its manifest records (§11.2) |
457
| `labels` | As §3.1 |
458
| `speed` | A class from 1 (slow) to 5 (fast), measured at `flow runner register` and `flow runner bench` by a fixed workload (BLAKE3 over 256 MiB and the compilation of a fixed C file), bucketed so that two machines of the same class finish a job within a factor of two of each other |
459
| `load` | The 1-minute load average divided by `cpus`, memory free now, jobs running |
460
461
### 11.2 Image Manifests Carry the Architecture
462
463
`flow rootfs import` reads the ELF header of the image's `/bin/sh` and
464
records its architecture in the CAS manifest beside the hash. A runner
465
refuses to import an image for another architecture (there is no
466
emulation tier). An image imported before this section has no recorded
467
architecture and is treated as the importing runner's.
468
469
### 11.3 Job Requirements
470
471
Hard requirements, all of which a runner MUST satisfy to be offered the
472
job: the sandbox's tier, its rootfs (hence the image's architecture),
473
`memory-limit`, `cpus`, every `runs-on` label of the job and its sandbox,
474
and the runner's scope (OxygenHub spec §13.2). Two additions to the
475
language:
476
477
- `arch "riscv64"` on a sandbox pins the architecture; without it the
478
architecture is the rootfs image's.
479
- A rootfs per architecture: `rootfs { x86_64 "cas://blob/…"; riscv64 "cas://blob/…" }`.
480
The job runs on whichever architecture takes it; the receipt and the
481
provenance record the architecture and image used, and a check's
482
description names them.
483
484
Soft requirements guide placement but never block it: a job's `timeout`
485
against the runner's speed class. The hub keeps, per job hash and speed
486
class, the median duration of past runs; a runner whose class predicts a
487
duration over the limit is offered the job only when no better runner
488
is free.
489
490
### 11.4 Placement
491
492
The hub filters by the hard requirements, then ranks the runners that
493
remain: idle before busy (`load`), a higher speed class first, a runner
494
already holding the image before one that must fetch it (§5), fewer jobs
495
waiting on it first. A job no runner can satisfy waits and the queue
496
says what it waits for: tier, architecture, image, memory, labels or
497
scope, so the gap is visible rather than a silent stall.
498
499
### 11.5 Declining
500
501
A runner double-checks a claim before it starts: the architecture, that
502
`/dev/kvm` opens for the MicroVM tier, that the image's blob is present
503
and verifies, that the memory is free. If any check fails it answers the
504
hub with `declined` and a reason instead of running and failing; the hub
505
re-queues the job, never offers it to that runner again until its
506
profile changes, records the decline on the runner's page, and after
507
three declines by different runners marks the job `unplaceable` with the
508
reasons. A decline is never a failed check on the state.