gates: close the audit findings — enforce the gates the docs claimed

An adversarial audit found the enforcement story ahead of the code: gates
that were documented as hard gates were advisory, manual, or absent, and
several human-facing claims outran the machine record. This makes the
claims true.

High findings

- Differential fuzz was documentation, not enforcement: verdict hardcoded
  fuzz_iters=0 and diff-test hardcoded the gate to Skipped, while the case
  studies claimed 10k-iteration fuzz on the same wins. Fuzz is now a real
  pipeline gate (strict FUZZ-RESULT contract, executed count recorded), and
  it is inherently baseline-vs-candidate, so it runs on the accept path and
  reports Skipped in the standalone `just gates` flow. New hard rule: a
  machine `accepted` verdict is impossible unless the fuzz gate actually
  passed — otherwise it caps at needs-human-review.
- The agent's system prompt claimed FP/concurrency/unsafe patches were
  "routed to human review automatically". No such mechanism existed. Added
  a conservative lexical risk classifier (crates/verdict/src/risk.rs) that
  forces needs-human-review on any would-be accept carrying those signals;
  fp-tolerance equivalence always signals.
- The local verdict path ran agent-influenced code unsandboxed. It now runs
  under scripts/no-net.sh (network namespace) and fails closed. Upstream
  test suites are hash-verified against corpora/<t>/TESTSUITE.sha256 when
  present (scripts/pin-testsuite.sh generates them).

Also fixed, found while fixing the above

- The pipeline script's `set -e` was inert (brace group as the left operand
  of `&&`), so a failing candidate build did not abort the run and verdict
  could bench a stale binary. Now a scoped subshell whose status is branched
  on at top level.
- A wrapper that exits before exec'ing (no-net.sh refusing, bad wrapper path)
  stranded RUNNING.lock and left read_verdict polling forever. Failure
  accounting now lives outside the wrapper.
- harnessd's patch allowlist ignored git rename/copy headers and --no-prefix
  diffs; the rename-escape case is now a committed regression test.
- The workspace did not compile on Windows (ungated Unix APIs). cfg-gated;
  71 portable tests now run on both platforms.
- pin-check regenerated the corpus manifest instead of verifying it; it is
  now verify-only, with a deliberate `just pin-corpus` to re-pin.
- Max RSS is captured per run on Unix; PMU counters are opt-in diagnostics
  (compare --perf-stat), never part of the accept decision.

Docs now match the code, including what is NOT yet enforced: no target ships
a TESTSUITE.sha256 pin, no target declares [build].tsan, and matmul is not
wired to the fp-tolerance gate — each is stated as mechanized-but-inert
rather than done. Corrected the stale "33 ledger rows" (34), the playbook's
class-3 cJSON win (rejected), adversarial-review.md's sanitizer-separation
claim (five accepted rows carry sanitizers_clean=false), the unsupported
"40,000 requests" figure, and the blanket "historical rows record
fuzz_iters=0" (8 of 10; phase0-tokei-002/-003 record 4,332).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 13:10:14 -04:00
co-authored by Claude Opus 4.8
parent 21352efe6c
commit 899d55f4b6
57 changed files with 3308 additions and 692 deletions
+4
View File
@@ -0,0 +1,4 @@
# Hash manifests and shell scripts must stay LF even on Windows checkouts —
# sha256sum -c and POSIX sh both break on CRLF working-tree copies.
*.sha256 text eol=lf
*.sh text eol=lf
+3
View File
@@ -234,7 +234,9 @@ targets/comrak/asan*/
targets/tokei/candidate-*/ targets/tokei/candidate-*/
targets/tokei/asan/ targets/tokei/asan/
!results/calibration/*.json !results/calibration/*.json
!results/phase4/*.json
targets/*/baseline/ targets/*/baseline/
targets/*/fuzz-work/
# Phase 2 agent artifacts (transcripts kept; pending logs are scratch) # Phase 2 agent artifacts (transcripts kept; pending logs are scratch)
results/pending/ results/pending/
@@ -250,3 +252,4 @@ targets/cjson/asan*/
targets/cjson/svc-baseline/ targets/cjson/svc-baseline/
targets/cjson/svc-candidate/ targets/cjson/svc-candidate/
docs/book/ docs/book/
+33 -22
View File
@@ -16,13 +16,13 @@ Full engineering spec: `SPEC.md`. Read it before starting any phase.
4. **Equivalence gates are hard gates.** Changes touching floating-point ordering, concurrency primitives, or anything UB-adjacent are NEVER auto-accepted — verdict is `needs-human-review`. 4. **Equivalence gates are hard gates.** Changes touching floating-point ordering, concurrency primitives, or anything UB-adjacent are NEVER auto-accepted — verdict is `needs-human-review`.
5. **Every attempt goes in the ledger**, including failures and rejections. Failures are future training data. 5. **Every attempt goes in the ledger**, including failures and rejections. Failures are future training data.
6. **The agent never modifies the trust layer.** `crates/`, `config/`, `corpora/`, and upstream test suites are read-only to the agent. Patches may touch only allowlisted paths under `targets/<name>/`. See SPEC.md §10. 6. **The agent never modifies the trust layer.** `crates/`, `config/`, `corpora/`, and upstream test suites are read-only to the agent. Patches may touch only allowlisted paths under `targets/<name>/`. See SPEC.md §10.
7. **Sandbox everything.** Target code runs only inside no-network containers. Never on the host. 7. **Sandbox everything.** Target code must never run unisolated on the host. *As built:* the harnessd-launched verdict pipeline execs through `scripts/no-net.sh` (Linux network namespace; **fails closed** — exit 97 — when namespaces are unavailable, unless `HOTPATH_ALLOW_UNISOLATED=1`, which runs it **with** network and stamps that fact into the ledger's isolation note). CI runs the bench workload under `docker run --network=none`. Two honest limits: a human invoking `just verdict` directly is **not** wrapped, and full-container (seccomp-restricted) isolation of the whole pipeline is still an **open gap**. See SPEC §10.
8. **Statistical bar:** accept only if the bootstrap 95% CI lower bound of speedup ≥ threshold in `config/accept.toml` (default 2%), from interleaved A/B runs on the same pinned hardware. 8. **Statistical bar:** accept only if the bootstrap 95% CI lower bound of speedup ≥ threshold in `config/accept.toml` (default 2%), from interleaved A/B runs on the same pinned hardware.
## Architecture map ## Architecture map
- `crates/bench-runner` — trust layer: containerized, CPU-pinned, interleaved A/B benchmarking with bootstrap confidence intervals and A/A self-tests - `crates/bench-runner` — trust layer: CPU-pinned, interleaved A/B benchmarking with bootstrap confidence intervals and A/A self-tests (it does **not** containerize anything itself — isolation is the wrapper's job, non-negotiable 7)
- `crates/diff-test` — behavioral equivalence: upstream test suite + golden I/O replay + differential fuzzing of changed functions + sanitizers - `crates/diff-test` — behavioral equivalence: corpus/test-suite pin checks + upstream test suite + golden I/O replay + differential fuzz (a baseline-vs-candidate gate; sanitizers run on the accept path in `crates/verdict`, and `diff-test` reports them as skipped-with-reason)
- `crates/ledger` — append-only SQLite record of every attempt: hypothesis, patch, gate results, bench deltas, verdict, cost - `crates/ledger` — append-only SQLite record of every attempt: hypothesis, patch, gate results, bench deltas, verdict, cost
- `crates/report` — ROI generator: cores saved × $/core-hr, latency percentile deltas, CIs, workload caveats printed on every number - `crates/report` — ROI generator: cores saved × $/core-hr, latency percentile deltas, CIs, workload caveats printed on every number
- `agent/` — Claude Agent SDK (Python) loop, prompts, tool definitions - `agent/` — Claude Agent SDK (Python) loop, prompts, tool definitions
@@ -30,44 +30,55 @@ Full engineering spec: `SPEC.md`. Read it before starting any phase.
- `playbook/` — optimization classes, ordered, with preconditions and known risks - `playbook/` — optimization classes, ordered, with preconditions and known risks
- `results/` — generated reports and flamegraphs - `results/` — generated reports and flamegraphs
**Phase 4 COMPLETE — all SPEC §5 exit criteria met. Phases 0–4 are all done.** Service-mode latency benchmarking shipped: a coordinated-omission-correct, open-loop, fixed-rate load generator (`crates/bench-runner/src/service.rs`) measuring latency from each request's intended send time (queueing counted, not hidden), interleaved A/B, exact percentiles, bootstrap p50/p99 CIs. A real service target — `targets/cjson/service.c`, a minimal HTTP server wrapping the patched `cJSON.c` (trust-layer, outside the workspace allowlist). Service calibration PASS (0/10 A/A false-positive, 10/10 injected-5%-latency-regression detection; `results/calibration/cjson-service-aa.json`). **The verified batch win phase3-cjson-002 measured under a 150 rps replay (20 rounds, 40k requests, 0 drops): p50 latency +6.2%, 95% CI [+5.8%, +7.2%] — accepted (`phase4-cjson-service` ledger row); p99 NOT claimed (CI [0.07, 4.97], single-worker loopback tail jitter) — harness correctly rejected it.** Mechanical ROI report generator (`crates/report` bin, `just report`, SPEC §9): reads a ledger row + service-latency JSON → throughput→cores→dollars (27.5 cores / $9,621-per-year CI lower bound on a 500-core fleet) *and* latency percentiles, every figure with CI + workload + methodology inline; flags any non-accepted row. Case study: `results/phase4/`. **The roadmap is complete — the profile→hypothesize→patch→gated-verdict→ROI loop runs end to end across Rust and C, batch and service, with zero shipped false accepts across all phases.** **Phase 4 COMPLETE — all SPEC §5 exit criteria met. Phases 0–4 are all done.** Service-mode latency benchmarking shipped: a coordinated-omission-correct, open-loop, fixed-rate load generator (`crates/bench-runner/src/service.rs`) measuring latency from each request's intended send time (queueing counted, not hidden), interleaved A/B, exact percentiles, bootstrap p50/p99 CIs. A real service target — `targets/cjson/service.c`, a minimal HTTP server wrapping the patched `cJSON.c` (trust-layer, outside the workspace allowlist). Service calibration PASS (0/10 A/A false-positive, 10/10 injected-5%-latency-regression detection; `results/calibration/cjson-service-aa.json`). **The verified batch win phase3-cjson-002 measured under a 150 rps-target replay (20 interleaved rounds, 0 drops — the `phase4-cjson-service` ledger row records sessions=20, rate_rps=150, drop_rate=0.0; total request count is not machine-recorded): p50 latency +6.2%, 95% CI [+5.8%, +7.2%] — accepted; p99 NOT claimed (CI [0.07, 4.97], single-worker loopback tail jitter) — harness correctly rejected it.** Mechanical ROI report generator (`crates/report` bin, `just report`, SPEC §9): reads a ledger row + service-latency JSON → throughput→cores→dollars (27.5 cores / $9,621-per-year CI lower bound on a 500-core fleet) *and* latency percentiles, every figure with CI + workload + methodology inline; flags any non-accepted row. Case study: `results/phase4/`. **The roadmap is complete — the profile→hypothesize→patch→gated-verdict→ROI loop runs end to end across Rust and C, batch and service, with zero shipped false accepts across all phases.**
**Phase 5 COMPLETE — the two SPEC §13 research forks, both built and both routed to human review.** (a) *Learned class-selection policy* (`crates/policy`, `cargo run -p policy`): reads the append-only ledger and ranks optimization classes by the Wilson lower bound of their shippable-win rate — a learned prior over the fixed cheapest-first ordering, advisory only (gates still decide). A "win" counts only machine-sanitizer-verified accepts; the overturned comrak-010 and tier-gated mimalloc are excluded/held. On the current 34-row ledger it recommends algorithmic first (Wilson lb 0.066). (b) *Kernel lane* (`targets/matmul/kernel.c`, `crates/diff-test` FP-tolerance policy + `fp-compare` bin, `scripts/kernel-lane-demo.sh`): a matmul optimization (transpose-B cache locality + eight-accumulator ILP) that **reorders FP accumulation**, so byte-identical golden replay is the wrong gate — 244,901/262,144 values differ. The FP-tolerance gate (`abs 1e-4 + rel 1e-3`) accepts it, still REJECTS a +0.5 perturbation, and the bench measures 3.23× [3.16, 3.26] with the same interleaved A/B + bootstrap CI. Ledger row `phase5-matmul-opt` = needs-human-review (using the tolerance tier is a §8 signal, same posture as mimalloc). No GPU in this environment, so the GPU fork is the identical shape shown on CPU — only the timer and hardware change. Case study: `results/phase5/`; docs chapter `docs/src/research-forks.md`. Open follow-up: the standing accept-scoped ruling on `phase0-comrak-002` (mimalloc) is resolved (`results/rulings/`).** **Hardening pass (2026-07-13, `audit/resolve-findings`) — what is *enforced* vs. what is only *mechanized*.** Five gates were addressed in code. **Two are live on the verdict path (a, b); three are wired but currently inert, because no target has opted in (c, d, e).** The distinction is stated precisely because collapsing it is exactly the overclaim this project exists to prevent.
- **(a) Differential fuzz — live, and now blocks accepts.** `diff-test` runs the target's declared `[gates].fuzz` command and grades it strictly from a `FUZZ-RESULT iters=<n> divergences=<m>` line (no line = Failed, not a pass by silence); the ledger records the iteration count **actually executed**. It is a **baseline-vs-candidate** gate, so it runs *only* on the accept path (`crates/verdict`, the one flow that rebuilds a pristine baseline). The standalone `just gates` flow has no baseline, so the gate reports **Skipped with that reason** — `just gates` is a fast equivalence check, **not** a fuzz run. **Hard rule:** a machine `accepted` verdict is **impossible** unless the DifferentialFuzz gate actually *Passed*; skipped or failed caps the verdict at `needs-human-review`. `cjson`/`comrak`/`tokei` declare `[gates].fuzz`; `matmul` does not. *Ledger history:* fuzz ran out-of-band via `scripts/diff-fuzz-*.py` through Phase 5, so **8 of the 10 accepted rows record `fuzz_iters=0`** — the two exceptions, `phase0-tokei-002` and `phase0-tokei-003`, record `fuzz_iters=4332`.
- **(b) Lexical risk classifier — live.** `crates/verdict/src/risk.rs` scans the patch's **changed lines** for concurrency / `unsafe` / floating-point tokens; any signal on a would-be accept forces `needs-human-review`, and using fp-tolerance equivalence mode is itself always a signal. It is **lexical and deliberately over-triggering, not semantic** — it detects the presence of risk markers; it does not understand the code and cannot prove their absence. `harnessd` always passes `--patch-file`, so every agent-proposed patch is classified.
- **(c) Test-suite pinning — mechanized, INERT.** `diff-test` verifies `corpora/<t>/TESTSUITE.sha256` **when present** (mismatch = hard refusal, same posture as the corpus pin) and prints an explicit "suite unpinned" warning when absent. **No target ships a pin today — there are zero `TESTSUITE.sha256` files in the repo** — so the gate constrains nothing yet. Pins must be generated per target, post-fetch, by a deliberate `scripts/pin-testsuite.sh` run.
- **(d) TSan lane — mechanized, INERT.** `crates/verdict` builds and runs a TSan lane on the accept path **only when a target declares `[build].tsan`**. **No target declares it today, so no TSan coverage exists.** (ASan/UBSan *is* live on the accept path: a flagged run — or a target with no sanitizer build at all — caps the accept at `needs-human-review`.)
- **(e) FP-tolerance equivalence — mechanized in the pipeline, target NOT wired.** The gate pipeline honors an fp-tolerance policy (`targets/<t>/equivalence.toml`; requires `[corpus].golden_reference`), and any fp-tolerance run auto-routes to `needs-human-review`. But the only FP target, `matmul`, has an `equivalence.toml` and **no `target.toml`** — so it is not a pipeline target at all, and the kernel lane stays script-driven (`scripts/kernel-lane-demo.sh`).
**Phase 5 COMPLETE — the two SPEC §13 research forks, both built and both routed to human review.** (a) *Learned class-selection policy* (`crates/policy`, `cargo run -p policy`): reads the append-only ledger and ranks optimization classes by the Wilson lower bound of their shippable-win rate — a learned prior over the fixed cheapest-first ordering, advisory only (gates still decide). A "win" counts only machine-sanitizer-verified accepts; the overturned comrak-010 and tier-gated mimalloc are excluded/held. On the current 34-row ledger it recommends algorithmic first (Wilson lb 0.066). (b) *Kernel lane* (`targets/matmul/kernel.c`, `crates/diff-test` FP-tolerance policy + `fp-compare` bin, `scripts/kernel-lane-demo.sh`): a matmul optimization (transpose-B cache locality + eight-accumulator ILP) that **reorders FP accumulation**, so byte-identical golden replay is the wrong gate — 244,901/262,144 values differ. The FP-tolerance policy (`abs 1e-4 + rel 1e-3`) accepts it, still REJECTS a +0.5 perturbation, and the bench measures 3.23× [3.16, 3.26] with the same interleaved A/B + bootstrap CI. Ledger row `phase5-matmul-opt` = needs-human-review (using the tolerance tier is a §8 signal, same posture as mimalloc). **Honest scope:** the FP-tolerance *mechanism* lives in the gate pipeline, but `matmul` is **not wired into it** — it has an `equivalence.toml` and no `target.toml`, so `diff-test`/`verdict` cannot load it. The lane is demonstrated script-driven (`fp-compare` + `bench-runner compare`), and that ledger row was recorded outside the automated verdict path. Wiring `matmul` as a real pipeline target is open work. No GPU in this environment, so the GPU fork is the identical shape shown on CPU — only the timer and hardware change. Case study: `results/phase5/`; docs chapter `docs/src/research-forks.md`. Open follow-up: the standing accept-scoped ruling on `phase0-comrak-002` (mimalloc) is resolved (`results/rulings/`).**
## Commands ## Commands
Fill these in as the harness is built; keep them current: The real recipe list (`justfile`); keep it current:
```
just profile <target> # perf record + flamegraph for a target's benchmark workload
just bench <target> <ref> # interleaved A/B: pristine baseline vs working tree
just gates <target> # test suite + golden replay + diff-fuzz + sanitizers
just verdict <target> # runs gates + bench, writes ledger row, prints verdict
just report <run-id> # ROI report from ledger
just aa <target> # A/A calibration run (must show null result)
```
Built so far (command-level, pre-target-integration):
``` ```
just build / test / lint # trust-layer workspace: cargo build/test/clippy+fmt just build / test / lint # trust-layer workspace: cargo build/test/clippy+fmt
just aa [cmd] # A/A self-test of bench-runner on a shell command just aa [cmd] # A/A self-test of bench-runner on a shell command
just compare <a> <b> # interleaved A/B of two shell commands, bootstrap CI just compare <a> <b> # interleaved A/B of two shell commands, bootstrap CI
just gates <target> # corpus pin + upstream tests + golden replay just gates <target> # corpus pin (+ suite pin if present) + upstream tests + golden replay.
just pin-check <target> # verify corpus MANIFEST.sha256 # diff-fuzz SKIPS here (needs a baseline) — it runs in `just verdict`
just pin-check <target> # verify corpus MANIFEST.sha256 (verify-only, never re-pins)
just pin-corpus <target> # deliberate corpus re-pin (writes MANIFEST.sha256)
just calibrate <cmd> <out> # automated A/A + regression-injection calibration just calibrate <cmd> <out> # automated A/A + regression-injection calibration
just verdict <t> <bin> ... # gates + bench vs pristine-rebuilt baseline + ledger row just verdict <t> <bin> ... # gates + bench vs pristine-rebuilt baseline + ledger row
cargo run -p harnessd # agent IPC daemon: six-tool JSON surface (SPEC §3.5) just report <run-id> # ROI report from a ledger row (CIs + methodology inline)
just agent-attempt <t> <id> # one unattended agent attempt behind the OS boundary
just isolation-check # verify the SPEC §10 OS boundary (19 checks, both modes)
just service <b> <c> <doc> # service-mode latency A/B, CO-correct, p50/p99 CIs
just service-calibrate ... # latency A/A + injected-regression calibration
just coz <target> # causal profile of a C/C++ target
cargo run -p harnessd # agent IPC daemon: seven-op JSON surface (SPEC §3.5)
cargo run -p policy # learned class-ranking prior from the ledger (advisory)
``` ```
**Platform note:** the harness is Linux/POSIX-only at runtime — bench-runner
service mode, harnessd (Unix sockets, `setsid`), and every sh-based gate and
isolation script assume a Unix host. Windows compiles the workspace and runs
the portable unit tests, but cannot run the pipeline.
## Definition of done — one optimization attempt ## Definition of done — one optimization attempt
- [ ] Hypothesis logged in ledger *before* patching - [ ] Hypothesis logged in ledger *before* patching
- [ ] Patch touches only allowlisted paths under `targets/<name>/` - [ ] Patch touches only allowlisted paths under `targets/<name>/`
- [ ] Upstream test suite green - [ ] Upstream test suite green
- [ ] Golden replay byte-identical (or within the target's explicit FP tolerance policy) - [ ] Golden replay byte-identical (or within the target's explicit FP tolerance policy)
- [ ] Differential fuzz on changed functions: no divergence (60s or 10k iterations minimum) - [ ] Differential fuzz on changed functions: no divergence (60s or 10k iterations minimum). Requires `[gates].fuzz` in the target's `target.toml` **and** the accept path (`just verdict`) — an accept is impossible without a Passed fuzz gate
- [ ] ASan + UBSan clean; TSan if the patch touches anything threaded - [ ] ASan + UBSan clean (machine-enforced on accept). TSan if the patch touches anything threaded — note the lane only runs when the target declares `[build].tsan`, and **no target declares one today**, so a threaded patch needs that opt-in added first
- [ ] Interleaved A/B bench passes the CI-lower-bound threshold - [ ] Interleaved A/B bench passes the CI-lower-bound threshold
- [ ] Ledger row written with verdict and full evidence - [ ] Ledger row written with verdict and full evidence
- [ ] If FP-ordering / concurrency / UB-adjacent: verdict `needs-human-review`, never auto-accept - [ ] If FP-ordering / concurrency / UB-adjacent: verdict `needs-human-review`, never auto-accept
+7 -2
View File
@@ -21,7 +21,7 @@ hostile review.
## Verified results ## Verified results
Every number carries its 95% CI and its workload, and every one survived the Every number carries its 95% CI and its workload, and every one survived the
gates above. Across **five phases and 33 ledger rows, zero shipped false gates above. Across **five phases and 34 ledger rows, zero shipped false
accepts** — including two pipeline over-accepts the audit caught before accepts** — including two pipeline over-accepts the audit caught before
anything shipped, each becoming a permanent new gate. anything shipped, each becoming a permanent new gate.
@@ -58,11 +58,16 @@ docs/ The mdBook site (deployed to GitHub Pages)
just build / test / lint # trust-layer workspace just build / test / lint # trust-layer workspace
just aa # A/A self-test — must yield a null verdict just aa # A/A self-test — must yield a null verdict
just gates <target> # corpus pin + upstream tests + golden replay just gates <target> # corpus pin + upstream tests + golden replay
just verdict <t> <bin> ... # gates + bench vs pristine baseline + ledger row # (fuzz needs a baseline — it runs in `just verdict`)
just verdict <t> <bin> ... # gates + fuzz + sanitizers + bench vs pristine baseline
just report <run-id> # ROI report from a ledger row just report <run-id> # ROI report from a ledger row
just isolation-check # 19 OS-boundary checks (both modes) just isolation-check # 19 OS-boundary checks (both modes)
``` ```
The pipeline is **Linux/POSIX-only** at runtime (Unix sockets, `setsid`,
sh-based gates). Windows builds the workspace and runs the portable unit
tests, but cannot run the pipeline.
Full command reference and reproduction steps: Full command reference and reproduction steps:
[Reproduce it yourself](https://bdbrown4.github.io/peltier.io/reproduce.html). [Reproduce it yourself](https://bdbrown4.github.io/peltier.io/reproduce.html).
+26 -2
View File
@@ -43,6 +43,8 @@ Requirements:
- **A/A self-test mode**: run the same binary as both sides; must produce a null verdict. Scheduled automatically before any measurement session. - **A/A self-test mode**: run the same binary as both sides; must produce a null verdict. Scheduled automatically before any measurement session.
- Regression-injection self-test: a synthetic 5% slowdown must be detected ≥95% of the time. - Regression-injection self-test: a synthetic 5% slowdown must be detected ≥95% of the time.
- Metrics captured per run: wall time, cycles, instructions, cache misses, branch misses (via `perf stat`), max RSS, and RAPL energy where available. - Metrics captured per run: wall time, cycles, instructions, cache misses, branch misses (via `perf stat`), max RSS, and RAPL energy where available.
> **Amended 2026-07-13.** As built: **wall time is the only metric a verdict is decided on.** Max RSS is captured per timed run on Unix (`wait4` / `ru_maxrss`) and recorded as baseline/candidate medians in the env fingerprint — it is **context, never an accept metric**. PMU counters (cycles, instructions, cache misses, branch misses) are opt-in diagnostics via `bench-runner compare --perf-stat` — printed with the workload caveat, never part of the accept decision, and unavailable in most VMs. RAPL energy capture was never implemented. The **"no-network container"** named in the environment-control bullet above is *not* what ships — see the §10 amendment for the isolation actually built (a network namespace on the agent path only).
- Modes: (a) whole-program via hyperfine-style CLI timing; (b) microbench via criterion harness hooks; (c) service mode (Phase 4): wrk2 / vegeta load replay with **coordinated-omission-aware** latency recording (HdrHistogram). - Modes: (a) whole-program via hyperfine-style CLI timing; (b) microbench via criterion harness hooks; (c) service mode (Phase 4): wrk2 / vegeta load replay with **coordinated-omission-aware** latency recording (HdrHistogram).
Acceptance: passes A/A calibration at <5% false-positive rate and injected-regression detection at ≥95%, documented in `results/calibration/`. Acceptance: passes A/A calibration at <5% false-positive rate and injected-regression detection at ≥95%, documented in `results/calibration/`.
@@ -57,6 +59,12 @@ Layered, all must pass:
Corpus and test files live outside agent-writable paths and are hash-pinned; `diff-test` refuses to run if hashes mismatch. Corpus and test files live outside agent-writable paths and are hash-pinned; `diff-test` refuses to run if hashes mismatch.
> **Amended 2026-07-13 — layer 3 (differential fuzz) as actually built.** Fuzz is a **baseline-vs-candidate** gate: it needs both binaries, so it runs *only* on the accept path (`verdict`, the one flow that rebuilds a pristine baseline). The standalone `diff-test` / `just gates` flow has no baseline and records the gate as **Skipped, with that reason** — it is never faked by comparing the candidate against itself. `diff-test` executes the target's declared `[gates].fuzz` command and grades it strictly from a `FUZZ-RESULT iters=<n> divergences=<m>` line; a run that never prints the line is a **Failed** gate, not a pass by silence. The ledger records the iteration count actually executed. **Hard rule:** a machine `accepted` verdict is impossible unless this gate *Passed* — skipped or failed caps the verdict at `needs-human-review`. `cjson`, `comrak`, and `tokei` declare `[gates].fuzz`; `matmul` does not. The per-changed-function scoping and the 60s/10k minimum remain aspirational: the shipped fuzzers are per-target whole-binary differential drivers (`scripts/diff-fuzz-*.py`) with a configurable iteration budget.
> **Amended 2026-07-13 — layer 4 (sanitizers) as actually built.** ASan+UBSan is machine-enforced on the accept path: `verdict` builds the target's `[build].sanitizer` binary, runs the pinned workload under it, and caps a flagged run — or a target that declares **no** sanitizer build — at `needs-human-review`. The **TSan lane is opt-in per target** (`[build].tsan` + `[build].tsan_binary`) rather than diff-triggered, and **no target declares it today**, so the lane is wired but **inert and no TSan coverage currently exists**; a threaded patch requires adding that opt-in first. **MIRI was never implemented.**
> **Amended 2026-07-13 — test-suite pinning: mechanized, not yet active.** As built, upstream test suites are vendored inside `targets/<t>/workspace/` — an agent-writable path — so "unmodified" is enforced by pinning, not placement: `corpora/<t>/TESTSUITE.sha256` (generated only by a deliberate human run of `scripts/pin-testsuite.sh`) is verified by `diff-test` before the suite runs; a hash mismatch is a hard refusal, same posture as the corpus pin. Targets without a pin file run with an explicit "suite unpinned" warning, and `targets/fetch.sh` verifies the pin after every fetch. **Status: no target ships a pin — there are zero `TESTSUITE.sha256` files in the repo — so this gate is currently inert and constrains nothing.** It becomes real only when pins are generated per target after a fetch. (The *corpus* pin, `MANIFEST.sha256`, ships for every target and **is** enforced today.)
### 3.3 `ledger` ### 3.3 `ledger`
Append-only SQLite. One row per attempt: Append-only SQLite. One row per attempt:
@@ -70,7 +78,7 @@ Phase 0–2: `perf record` + flamegraphs (inferno) + `perf stat`; hotspot rankin
### 3.5 `agent/` ### 3.5 `agent/`
Claude Agent SDK (Python). Tools exposed to the model: `read_profile`, `read_ledger`, `read_playbook`, `propose_patch(diff, hypothesis)`, `run_verdict`, `read_target_source` (read-only). Deliberately **not** exposed: shell on the host, writes outside `targets/<name>/`, any access to `crates/`, `config/`, `corpora/`. Claude Agent SDK (Python). Tools exposed to the model — seven: `read_profile`, `read_ledger`, `read_playbook`, `read_target_source` (read-only), `propose_patch(diff, hypothesis)`, `run_verdict`, `read_verdict`. Deliberately **not** exposed: shell on the host, writes outside `targets/<name>/`, any access to `crates/`, `config/`, `corpora/`. *(Amended 2026-07-13: `run_verdict` launches the pipeline detached and returns immediately; the pollable `read_verdict` — added in Phase 2 when the pipeline outlived the MCP transport timeout — reads the result, making seven operations total.)*
Prompting spine: (1) state hypothesis before patching; (2) prefer the cheapest untried playbook class; (3) max two iterations per rejected hypothesis; (4) a rejection with a clean ledger row is a successful outcome. Prompting spine: (1) state hypothesis before patching; (2) prefer the cheapest untried playbook class; (3) max two iterations per rejected hypothesis; (4) a rejection with a clean ledger row is a successful outcome.
@@ -79,7 +87,7 @@ Prompting spine: (1) state hypothesis before patching; (2) prefer the cheapest u
Consumes ledger rows, emits per-engagement ROI report: Consumes ledger rows, emits per-engagement ROI report:
- Throughput jobs: cores saved = fleet_cores × (1 − 1/speedup); $ = cores × $/core-hr × hours/yr (rates in `config/pricing.toml`). - Throughput jobs: cores saved = fleet_cores × (1 − 1/speedup); $ = cores × $/core-hr × hours/yr (rates in `config/pricing.toml`).
- Services: p50/p95/p99 deltas under replayed traffic, with the business-value mapping left as a customer-supplied parameter and one worked example. - Services: p50/p95/p99 deltas under replayed traffic, with the business-value mapping left as a customer-supplied parameter and one worked example.
- Optional: joules saved (RAPL) for the sustainability line. - Optional: joules saved (RAPL) for the sustainability line. *(Amended 2026-07-13: never implemented — RAPL energy is not captured anywhere in the harness, so no report emits a joules figure. See the §3.1 amendment.)*
- Every figure carries CI + workload description + environment fingerprint. Caveats are printed on the report, not in an appendix. - Every figure carries CI + workload description + environment fingerprint. Caveats are printed on the report, not in an appendix.
## 4. Optimization playbook v0 (strict order) ## 4. Optimization playbook v0 (strict order)
@@ -106,6 +114,8 @@ Each class ships as `playbook/NN-name.md`: preconditions (profile signature that
Language order: Rust first (criterion/proptest/MIRI make Phases 1–2 cheaper), C/C++ second (the richer legacy hunting ground), services third. Language order: Rust first (criterion/proptest/MIRI make Phases 1–2 cheaper), C/C++ second (the richer legacy hunting ground), services third.
> **Amended 2026-07-13.** The Phase 2 criterion as written — "zero false accepts across ≥20 audited attempts" — was violated once *in-pipeline*: `phase2-comrak-010` was falsely accepted by the pipeline (no machine sanitizer gate existed yet), caught by the 100% human audit, overturned, and never shipped. The criterion was met in the form **zero *shipped* false accepts**; the overturn drove the machine-enforced sanitizer gate and stands documented in the Phase 2 case study.
## 6. Target selection criteria (Phase 0–3) ## 6. Target selection criteria (Phase 0–3)
CPU-bound (>70% user time on profile); meaningful test suite; existing benchmark or easily scripted workload; permissive license; active but not hyper-optimized (skip anything with a dedicated perf team); builds cleanly in container. Good hunting: codecs, parsers, serializers, image processing, compression, static-site generators, linters. CPU-bound (>70% user time on profile); meaningful test suite; existing benchmark or easily scripted workload; permissive license; active but not hyper-optimized (skip anything with a dedicated perf team); builds cleanly in container. Good hunting: codecs, parsers, serializers, image processing, compression, static-site generators, linters.
@@ -123,6 +133,8 @@ CPU-bound (>70% user time on profile); meaningful test suite; existing benchmark
- **needs-human-review** (never auto-accepted): FP reassociation/contraction or `-ffast-math`-class flags; any diff touching atomics, locks, memory ordering, or thread counts; unsafe blocks / raw pointer arithmetic introduced; anything the sanitizers flag even as warnings. - **needs-human-review** (never auto-accepted): FP reassociation/contraction or `-ffast-math`-class flags; any diff touching atomics, locks, memory ordering, or thread counts; unsafe blocks / raw pointer arithmetic introduced; anything the sanitizers flag even as warnings.
- **Auto-rejected**: any gate failure, any divergence under differential fuzz, patch outside allowlist. - **Auto-rejected**: any gate failure, any divergence under differential fuzz, patch outside allowlist.
> **Amended 2026-07-13.** The needs-human-review tier is machine-routed, not honor-system: `verdict` runs a **conservative lexical risk classifier** over the patch's changed lines (substring token lists for concurrency, unsafe, and floating-point signals) and forces any would-be accept carrying a signal to `needs-human-review`; using **fp-tolerance equivalence mode** at all also auto-routes to review; the manual `--needs-human-review` flag remains. The classifier is lexical and deliberately over-triggering — it cheaply catches the presence of risk markers, it does not prove their absence.
## 9. ROI methodology (the pitch artifact) ## 9. ROI methodology (the pitch artifact)
The report answers one question: *what did the stopwatch say, and what does that cost or buy?* Throughput → cores → dollars via public cloud pricing; latency → percentile deltas under recorded production-shaped load, priced by the customer's own latency-value number; batch windows → wall-clock hours returned to the schedule. All three come with CIs and a workload statement. The methodology section ships *inside* every report so the number survives hostile review — that is the differentiation. The report answers one question: *what did the stopwatch say, and what does that cost or buy?* Throughput → cores → dollars via public cloud pricing; latency → percentile deltas under recorded production-shaped load, priced by the customer's own latency-value number; batch windows → wall-clock hours returned to the schedule. All three come with CIs and a workload statement. The methodology section ships *inside* every report so the number survives hostile review — that is the differentiation.
@@ -136,6 +148,14 @@ The agent's incentive is "make the number go up"; the design must make cheating
- Target code executes only in no-network containers (bench containers additionally seccomp-restricted). - Target code executes only in no-network containers (bench containers additionally seccomp-restricted).
- Periodic human audit of accepted wins (100% in Phase 2, sampled thereafter); any false accept is a stop-the-line event. - Periodic human audit of accepted wins (100% in Phase 2, sampled thereafter); any false accept is a stop-the-line event.
> **Amended 2026-07-13 — isolation as actually built.** The bullet above ("target code executes only in no-network containers, seccomp-restricted") is **not** what ships. What ships:
>
> - **The `harnessd`-launched verdict pipeline** — the agent path — execs through `scripts/no-net.sh`, a **network-namespace** wrapper (`unshare --net --map-current-user`). `HOTPATH_VERDICT_WRAPPER` overrides the wrapper.
> - **It fails closed.** Where namespaces are unavailable the wrapper **exits 97 without running the pipeline**, rather than degrading silently. The only bypass is the explicit `HOTPATH_ALLOW_UNISOLATED=1` override — which runs the pipeline **with full network access**, warns loudly on stderr, and is recorded verbatim in the ledger's isolation note (`"no-net.sh (HOTPATH_ALLOW_UNISOLATED=1 — network NOT isolated)"`), so a run can never *claim* an isolation it did not have.
> - **In CI**, the bench workload runs under `docker run --network=none`.
>
> **Two gaps, stated plainly.** (1) **Only the harnessd (agent) path is wrapped.** A human invoking `just verdict` directly runs **unwrapped** on the host; such rows record `isolation: "unwrapped-host"`. (2) **Full-container isolation of the whole pipeline remains open** — the wrapper isolates the *network*, not the filesystem or the syscall surface; the seccomp-restricted bench container of this section is not built. The agent-side OS boundary (mount-namespace / unprivileged-uid isolation, `just isolation-check`) is a separate mechanism and *is* shipped.
## 11. Risk register ## 11. Risk register
| Risk | Mitigation | | Risk | Mitigation |
@@ -149,10 +169,14 @@ The agent's incentive is "make the number go up"; the design must make cheating
| License/IP hygiene | Permissive-license targets only for public case studies | | License/IP hygiene | Permissive-license targets only for public case studies |
| Wins too small to matter | Cheap-wins-first ordering keeps cost-per-attempt low; ledger prevents re-grinding dead ends | | Wins too small to matter | Cheap-wins-first ordering keeps cost-per-attempt low; ledger prevents re-grinding dead ends |
> **Amended 2026-07-13 — which mitigations above are actually live.** The "Concurrency bugs introduced" and "UB introduced" rows overstate the machine coverage. Live today: **UBSan** (bundled with ASan on the accept path, machine-enforced) and the **human-review tier**, which is machine-routed by the lexical risk classifier of the §8 amendment — so a concurrency-token or `unsafe`-token patch *is* forced to `needs-human-review`, conservatively and lexically. **Not live: the TSan gate** (opt-in via `[build].tsan`; no target declares it) and **MIRI** (never implemented). The concurrency and UB rows therefore rest on the review tier and UBSan, not on TSan/MIRI.
## 12. Tech stack ## 12. Tech stack
Rust workspace (`bench-runner`, `diff-test`, `ledger`, `report`) · `just` · hyperfine-style timing + criterion hooks · perf, inferno flamegraphs, coz · proptest, cargo-fuzz, libFuzzer · ASan/UBSan/TSan, MIRI · podman/docker (no-net) · wrk2 + HdrHistogram, vegeta · SQLite · Python 3.11+ with Claude Agent SDK (`agent/`) — confirm current SDK package name and auth at https://docs.claude.com/en/docs/claude-code/overview. Rust workspace (`bench-runner`, `diff-test`, `ledger`, `report`) · `just` · hyperfine-style timing + criterion hooks · perf, inferno flamegraphs, coz · proptest, cargo-fuzz, libFuzzer · ASan/UBSan/TSan, MIRI · podman/docker (no-net) · wrk2 + HdrHistogram, vegeta · SQLite · Python 3.11+ with Claude Agent SDK (`agent/`) — confirm current SDK package name and auth at https://docs.claude.com/en/docs/claude-code/overview.
> **Amended 2026-07-13 — the stack as actually used.** This list is the *planned* stack, not an inventory. In use: the Rust workspace (plus `verdict`, `harnessd`, `policy`), `just`, in-house interleaved timing and an in-house open-loop load generator (**not** hyperfine/criterion/wrk2/vegeta), valgrind-callgrind and coz for profiling (`perf` is often unavailable in the container), Python differential fuzzers (`scripts/diff-fuzz-*.py` — **not** proptest/cargo-fuzz/libFuzzer), ASan+UBSan, SQLite, and the Agent SDK. **Not used: MIRI (never implemented), TSan (wired but no target opts in), and podman/docker for the local verdict path** — local isolation is a network namespace (`scripts/no-net.sh`); Docker `--network=none` is used in CI.
## 13. Later / research fork (explicitly out of scope now) ## 13. Later / research fork (explicitly out of scope now)
- GPU kernel lane (where 2–10x gaps live): Triton/CUDA targets, correctness via reference-kernel differential testing, reward = measured kernel time. - GPU kernel lane (where 2–10x gaps live): Triton/CUDA targets, correctness via reference-kernel differential testing, reward = measured kernel time.
+2 -2
View File
@@ -1,7 +1,7 @@
"""Phase 2 agent loop (SPEC §3.5): Claude Agent SDK driving the six """Phase 2 agent loop (SPEC §3.5): Claude Agent SDK driving the seven
harness tools over a standalone stdio MCP server. The agent gets NO harness tools over a standalone stdio MCP server. The agent gets NO
built-in tools — no shell, no file writes; every capability is one of built-in tools — no shell, no file writes; every capability is one of
the six IPC operations, and the trust layer's allowlist/gates remain the seven IPC operations, and the trust layer's allowlist/gates remain
the hard boundary (SPEC §10). the hard boundary (SPEC §10).
Run one attempt: Run one attempt:
+1 -1
View File
@@ -1,4 +1,4 @@
"""Standalone MCP stdio server exposing the six harness tools (SPEC §3.5). """Standalone MCP stdio server exposing the seven harness tools (SPEC §3.5).
Spawned as a subprocess by the agent loop; speaks MCP over stdio. Wraps Spawned as a subprocess by the agent loop; speaks MCP over stdio. Wraps
the harnessd IPC client — the trust-layer allowlist and gates remain the the harnessd IPC client — the trust-layer allowlist and gates remain the
+8 -4
View File
@@ -24,8 +24,12 @@ Rules, in priority order:
Never restructure a patch to influence measurement rather than to Never restructure a patch to influence measurement rather than to
express the hypothesis. express the hypothesis.
5. Equivalence is not yours to relax. Floating-point flag changes, 5. Equivalence is not yours to relax. The trust layer (crates/verdict)
concurrency-primitive changes, and new unsafe code are routed to runs a conservative lexical risk classifier over every patch's
human review automatically; do not attempt to phrase a patch to changed lines: concurrency, unsafe, and floating-point signals force
avoid that routing. any would-be accept to needs-human-review, and fp-tolerance
equivalence mode always routes to human review. The classifier is
lexical and deliberately over-triggering, not a semantic analysis;
a human --needs-human-review flag also exists. Do not attempt to
phrase a patch to slip past that routing.
""" """
+1 -1
View File
@@ -1,6 +1,6 @@
"""Tool surface exposed to the model (SPEC §3.5). """Tool surface exposed to the model (SPEC §3.5).
Exactly six tools, all mediated by harnessd over line-delimited JSON — Exactly seven tools, all mediated by harnessd over line-delimited JSON —
the agent process has no direct filesystem or shell capability beyond the agent process has no direct filesystem or shell capability beyond
what these return. Deliberately NOT exposed: shell on the host, writes what these return. Deliberately NOT exposed: shell on the host, writes
outside targets/<name>/, any access to crates/, config/, corpora/. outside targets/<name>/, any access to crates/, config/, corpora/.
+3
View File
@@ -23,3 +23,6 @@ bootstrap_seed = 1887343589
# Calibration evidence for this machine shows pinning tightens the A/A # Calibration evidence for this machine shows pinning tightens the A/A
# CI ~5x (results/calibration/). Empty string disables. # CI ~5x (results/calibration/). Empty string disables.
pin_prefix = "taskset -c 2" pin_prefix = "taskset -c 2"
# Differential-fuzz iterations requested per gate run (SPEC §3.2).
# fuzz_iters = 10000
+9 -2
View File
@@ -8,8 +8,15 @@ Every corpus directory carries a `MANIFEST.sha256` (`sha256sum` format:
before every run and refuses to run on any mismatch — a tampered corpus before every run and refuses to run on any mismatch — a tampered corpus
is a stop-the-line event. is a stop-the-line event.
Regenerate a manifest only as a deliberate human action: `just pin-check <target>` regenerates the corpus and verifies it against
the manifest; it never rewrites the manifest. Regenerate a manifest only
as a deliberate human action:
```sh ```sh
cd corpora/<target> && find . -type f ! -name MANIFEST.sha256 -exec sha256sum {} + > MANIFEST.sha256 just pin-corpus <target> # gen-corpus.sh --pin
``` ```
A directory may also carry a `TESTSUITE.sha256` pinning the upstream test
suite (repo-root-relative paths). `diff-test` verifies it before running
upstream tests and `targets/fetch.sh` verifies it after every fetch;
generate it with `scripts/pin-testsuite.sh <target> <path>...`.
+19 -3
View File
@@ -4,8 +4,16 @@
# with escapes and unicode, ints, floats (the strtod/sprintf hot path), # with escapes and unicode, ints, floats (the strtod/sprintf hot path),
# booleans, nulls. No randomness — byte-identical on every machine, so # booleans, nulls. No randomness — byte-identical on every machine, so
# the MANIFEST.sha256 pin is reproducible. Run from repo root: # the MANIFEST.sha256 pin is reproducible. Run from repo root:
# sh corpora/cjson/gen-corpus.sh # sh corpora/cjson/gen-corpus.sh [--check|--pin]
# Default/--check: regenerate and verify against MANIFEST.sha256 (never
# rewrites it). --pin: deliberately rewrite the manifest.
set -eu set -eu
mode="check"
case "${1:-}" in
"" | --check) ;;
--pin) mode="pin" ;;
*) echo "usage: gen-corpus.sh [--check|--pin]" >&2; exit 2 ;;
esac
DIR="$(cd "$(dirname "$0")" && pwd)" DIR="$(cd "$(dirname "$0")" && pwd)"
OUT="$DIR/input/big.json" OUT="$DIR/input/big.json"
mkdir -p "$DIR/input" mkdir -p "$DIR/input"
@@ -57,6 +65,14 @@ with open(sys.argv[1], "w") as f:
PY PY
echo "wrote $OUT ($(wc -c < "$OUT") bytes)" echo "wrote $OUT ($(wc -c < "$OUT") bytes)"
# Pin the corpus and regenerate the manifest. if [ "$mode" = "pin" ]; then
( cd "$DIR/input" && find . -type f -print0 | sort -z | xargs -0 sha256sum ) > "$DIR/MANIFEST.sha256" ( cd "$DIR/input" && find . -type f -print0 | sort -z | xargs -0 sha256sum ) > "$DIR/MANIFEST.sha256"
echo "manifest: $DIR/MANIFEST.sha256" echo "re-pinned $DIR/MANIFEST.sha256 — a deliberate human action (corpora/README.md);"
echo "commit the new manifest with justification."
else
if [ ! -f "$DIR/MANIFEST.sha256" ]; then
echo "no MANIFEST.sha256 — pin the corpus with: sh corpora/cjson/gen-corpus.sh --pin" >&2
exit 1
fi
( cd "$DIR/input" && sha256sum -c ../MANIFEST.sha256 )
fi
+14
View File
@@ -1,7 +1,21 @@
#!/bin/sh #!/bin/sh
# Regenerate the comrak bench corpus from the pinned progit checkout # Regenerate the comrak bench corpus from the pinned progit checkout
# (targets/comrak/target.toml [source.submodules]), then verify the pin. # (targets/comrak/target.toml [source.submodules]), then verify the pin.
# Default/--check verifies MANIFEST.sha256 (never rewrites it); --pin
# deliberately rewrites it.
set -eu set -eu
mode="check"
case "${1:-}" in
"" | --check) ;;
--pin) mode="pin" ;;
*) echo "usage: gen-corpus.sh [--check|--pin]" >&2; exit 2 ;;
esac
cd "$(dirname "$0")" cd "$(dirname "$0")"
cat ../../targets/comrak/workspace/vendor/progit/*/*/*.markdown > progit.md cat ../../targets/comrak/workspace/vendor/progit/*/*/*.markdown > progit.md
if [ "$mode" = "pin" ]; then
sha256sum progit.md > MANIFEST.sha256
echo "re-pinned MANIFEST.sha256 — a deliberate human action (corpora/README.md);"
echo "commit the new manifest with justification."
else
sha256sum -c MANIFEST.sha256 sha256sum -c MANIFEST.sha256
fi
+14
View File
@@ -2,12 +2,26 @@
# Regenerate the tokei bench input: 8 replicas of two pinned trees # Regenerate the tokei bench input: 8 replicas of two pinned trees
# (progit @ 61833a5, comrak src/ @ 45c1995), .git removed. Verifies the # (progit @ 61833a5, comrak src/ @ 45c1995), .git removed. Verifies the
# manifest for copy1; copies 2-8 are byte-identical replicas of copy1. # manifest for copy1; copies 2-8 are byte-identical replicas of copy1.
# Default/--check verifies MANIFEST.sha256 (never rewrites it); --pin
# deliberately rewrites it.
set -eu set -eu
mode="check"
case "${1:-}" in
"" | --check) ;;
--pin) mode="pin" ;;
*) echo "usage: gen-corpus.sh [--check|--pin]" >&2; exit 2 ;;
esac
cd "$(dirname "$0")" cd "$(dirname "$0")"
rm -rf input rm -rf input
mkdir -p input/copy1 mkdir -p input/copy1
cp -r ../../targets/comrak/workspace/vendor/progit input/copy1/progit cp -r ../../targets/comrak/workspace/vendor/progit input/copy1/progit
cp -r ../../targets/comrak/workspace/src input/copy1/src cp -r ../../targets/comrak/workspace/src input/copy1/src
rm -rf input/copy1/progit/.git rm -rf input/copy1/progit/.git
if [ "$mode" = "pin" ]; then
(cd input/copy1 && find . -type f -print0 | sort -z | xargs -0 sha256sum) > MANIFEST.sha256
echo "re-pinned MANIFEST.sha256 — a deliberate human action (corpora/README.md);"
echo "commit the new manifest with justification."
else
(cd input/copy1 && sha256sum -c ../../MANIFEST.sha256 --quiet) || { echo "corpus pin FAILED"; exit 1; } (cd input/copy1 && sha256sum -c ../../MANIFEST.sha256 --quiet) || { echo "corpus pin FAILED"; exit 1; }
fi
for i in 2 3 4 5 6 7 8; do cp -r input/copy1 "input/copy$i"; done for i in 2 3 4 5 6 7 8; do cp -r input/copy1 "input/copy$i"; done
+3 -1
View File
@@ -10,7 +10,9 @@ repository.workspace = true
anyhow.workspace = true anyhow.workspace = true
clap.workspace = true clap.workspace = true
ledger = { path = "../ledger" } ledger = { path = "../ledger" }
libc = "0.2"
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
toml.workspace = true toml.workspace = true
[target.'cfg(unix)'.dependencies]
libc = "0.2"
+36
View File
@@ -23,6 +23,13 @@ pub struct AcceptConfig {
/// both sides identically. /// both sides identically.
#[serde(default)] #[serde(default)]
pub pin_prefix: String, pub pin_prefix: String,
/// Differential-fuzz iterations requested per gate run (SPEC §3.2).
#[serde(default = "default_fuzz_iters")]
pub fuzz_iters: u64,
}
fn default_fuzz_iters() -> u64 {
10_000
} }
impl Default for AcceptConfig { impl Default for AcceptConfig {
@@ -35,6 +42,7 @@ impl Default for AcceptConfig {
confidence: 0.95, confidence: 0.95,
bootstrap_seed: 0x707E_17E5, bootstrap_seed: 0x707E_17E5,
pin_prefix: String::new(), pin_prefix: String::new(),
fuzz_iters: default_fuzz_iters(),
} }
} }
} }
@@ -52,3 +60,31 @@ impl AcceptConfig {
Ok(cfg) Ok(cfg)
} }
} }
#[cfg(test)]
mod tests {
use super::*;
const MINIMAL: &str = "threshold = 0.02\nruns_per_side = 30\nwarmup_runs = 3\n\
bootstrap_iters = 10000\nconfidence = 0.95\nbootstrap_seed = 7\n";
#[test]
fn fuzz_iters_defaults_when_absent() {
let cfg: AcceptConfig = toml::from_str(MINIMAL).unwrap();
assert_eq!(cfg.fuzz_iters, 10_000);
assert_eq!(AcceptConfig::default().fuzz_iters, 10_000);
}
#[test]
fn fuzz_iters_overridable() {
let raw = format!("{MINIMAL}fuzz_iters = 500\n");
let cfg: AcceptConfig = toml::from_str(&raw).unwrap();
assert_eq!(cfg.fuzz_iters, 500);
}
#[test]
fn unknown_fields_still_rejected() {
let raw = format!("{MINIMAL}fuz_iters = 500\n");
assert!(toml::from_str::<AcceptConfig>(&raw).is_err());
}
}
+218
View File
@@ -0,0 +1,218 @@
//! Opt-in `perf stat` PMU counter capture. Diagnostics only — counters
//! never feed the accept decision (wall time does, SPEC §3.1). Each
//! timed run is wrapped `perf stat -x, -e <events> -o <tmpfile> -- sh -c
//! <cmd>`; the CSV file is parsed after every run and per-side medians
//! are printed with the standard caveats. Unix-only at runtime; the
//! parser is portable and unit-tested.
use crate::exec::MeasuredSamples;
use crate::schedule::{interleaved, Side};
use crate::stats;
use anyhow::Context;
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::Instant;
pub const PERF_EVENTS: [&str; 4] = ["cycles", "instructions", "cache-misses", "branch-misses"];
/// One side's counter samples: `samples[i]` holds one value per measured
/// run for `PERF_EVENTS[i]`; runs where perf reported `<not counted>` or
/// `<not supported>` contribute nothing.
#[derive(Debug, Clone, Default)]
pub struct PerfSide {
pub samples: [Vec<u64>; 4],
}
#[derive(Debug, Clone, Default)]
pub struct PerfReport {
pub baseline: PerfSide,
pub candidate: PerfSide,
}
/// Parse `perf stat -x,` CSV: field 0 is the counter value, field 2 the
/// event name (perf may append a modifier, e.g. `cycles:u`). Comment
/// lines, blank lines, and non-numeric values (`<not counted>`,
/// `<not supported>`) are skipped. Returns (event, count) in file order.
pub fn parse_perf_csv(text: &str) -> Vec<(String, u64)> {
let mut out = Vec::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut fields = line.split(',');
let value = fields.next().unwrap_or("");
let _unit = fields.next();
let Some(event) = fields.next() else {
continue;
};
let Ok(v) = value.trim().parse::<u64>() else {
continue;
};
out.push((event.trim().to_string(), v));
}
out
}
/// Index into PERF_EVENTS, tolerating a perf modifier suffix
/// (`cycles:u` when perf_event_paranoid restricts kernel counting).
fn event_index(event: &str) -> Option<usize> {
PERF_EVENTS.iter().position(|name| {
event == *name || (event.starts_with(name) && event[name.len()..].starts_with(':'))
})
}
impl PerfSide {
fn absorb(&mut self, parsed: &[(String, u64)]) {
for (event, value) in parsed {
if let Some(i) = event_index(event) {
self.samples[i].push(*value);
}
}
}
}
impl PerfReport {
/// Per-side medians, each with its command (workload) and run count.
pub fn print(&self, baseline_cmd: &str, candidate_cmd: &str) {
println!("perf-stat per-side medians:");
for (label, cmd, side) in [
("baseline ", baseline_cmd, &self.baseline),
("candidate", candidate_cmd, &self.candidate),
] {
let cols: Vec<String> = PERF_EVENTS
.iter()
.zip(&side.samples)
.map(|(name, xs)| {
if xs.is_empty() {
format!("{name}=n/a")
} else {
let v: Vec<f64> = xs.iter().map(|&x| x as f64).collect();
format!(
"{name}={:.0} (median of {} runs)",
stats::median(&v),
xs.len()
)
}
})
.collect();
println!(" {label} `{cmd}`: {}", cols.join(", "));
}
println!(
" note: PMU counters unavailable in most VMs — flag is opt-in diagnostics, \
not part of the accept decision"
);
}
}
fn time_once_perf(shell_cmd: &str, out_file: &Path) -> anyhow::Result<(f64, Vec<(String, u64)>)> {
let start = Instant::now();
let status = Command::new("perf")
.args(["stat", "-x,", "-e", &PERF_EVENTS.join(",")])
.arg("-o")
.arg(out_file)
.args(["--", "sh", "-c", shell_cmd])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.with_context(|| format!("failed to spawn perf stat for: {shell_cmd}"))?;
let elapsed = start.elapsed().as_secs_f64();
anyhow::ensure!(
status.success(),
"perf stat run exited nonzero: {shell_cmd}"
);
let text = std::fs::read_to_string(out_file)
.with_context(|| format!("perf stat output missing: {}", out_file.display()))?;
Ok((elapsed, parse_perf_csv(&text)))
}
/// Interleaved A/B with each timed run wrapped in `perf stat`. The perf
/// wrapper overhead lands on both sides identically, but this path is
/// diagnostics only — verdicts always measure through the unwrapped
/// `exec::run_interleaved`.
pub fn run_interleaved_perf(
baseline_cmd: &str,
candidate_cmd: &str,
runs_per_side: usize,
warmup: usize,
) -> anyhow::Result<(MeasuredSamples, PerfReport)> {
anyhow::ensure!(cfg!(unix), "--perf-stat requires a Unix host with perf(1)");
let tmp = std::env::temp_dir().join(format!("bench-runner-perf-{}.csv", std::process::id()));
let mut samples = MeasuredSamples {
baseline_s: Vec::with_capacity(runs_per_side),
candidate_s: Vec::with_capacity(runs_per_side),
baseline_max_rss_kib: Vec::new(),
candidate_max_rss_kib: Vec::new(),
};
let mut report = PerfReport::default();
for (side, measured) in interleaved(runs_per_side, warmup) {
let cmd = match side {
Side::Baseline => baseline_cmd,
Side::Candidate => candidate_cmd,
};
let (elapsed, parsed) = time_once_perf(cmd, &tmp)?;
if measured {
match side {
Side::Baseline => {
samples.baseline_s.push(elapsed);
report.baseline.absorb(&parsed);
}
Side::Candidate => {
samples.candidate_s.push(elapsed);
report.candidate.absorb(&parsed);
}
}
}
}
let _ = std::fs::remove_file(&tmp);
Ok((samples, report))
}
#[cfg(test)]
mod tests {
use super::*;
const CANNED: &str = "\
# started on Mon Jul 13 12:00:00 2026
123456789,,cycles,401000000,100.00,,
234567890,,instructions:u,401000000,100.00,1.90,insn per cycle
<not supported>,,cache-misses,0,100.00,,
<not counted>,,branch-misses,0,0.00,,
garbage line without commas
999,,unrelated-event,1,100.00,,
";
#[test]
fn parses_values_and_skips_junk() {
let parsed = parse_perf_csv(CANNED);
assert_eq!(
parsed,
vec![
("cycles".to_string(), 123_456_789),
("instructions:u".to_string(), 234_567_890),
("unrelated-event".to_string(), 999),
]
);
}
#[test]
fn event_index_tolerates_modifier_suffix_only() {
assert_eq!(event_index("cycles"), Some(0));
assert_eq!(event_index("instructions:u"), Some(1));
assert_eq!(event_index("cache-misses"), Some(2));
assert_eq!(event_index("cycle"), None);
assert_eq!(event_index("cycles_total"), None);
assert_eq!(event_index("unrelated-event"), None);
}
#[test]
fn absorb_buckets_by_event() {
let mut side = PerfSide::default();
side.absorb(&parse_perf_csv(CANNED));
assert_eq!(side.samples[0], vec![123_456_789]);
assert_eq!(side.samples[1], vec![234_567_890]);
assert!(side.samples[2].is_empty(), "<not supported> is skipped");
assert!(side.samples[3].is_empty(), "<not counted> is skipped");
}
}
+106 -11
View File
@@ -1,29 +1,87 @@
//! Command execution and timing. Whole-program mode (hyperfine-style): //! Command execution and timing. Whole-program mode (hyperfine-style):
//! spawn the command, measure wall time, discard warm-ups per the //! spawn the command, measure wall time, discard warm-ups per the
//! schedule. `perf stat` counters and RAPL energy are Phase 1 follow-ups //! schedule. Max RSS is captured per run via `wait4` on Unix; opt-in
//! (tracked in README roadmap); wall time alone is enough to bring up //! `perf stat` counters live in `counters` (diagnostics only).
//! A/A calibration.
use crate::schedule::{interleaved, Side}; use crate::schedule::{interleaved, Side};
use anyhow::Context; use anyhow::Context;
use std::process::{Command, Stdio}; use std::process::{Child, Command, ExitStatus, Stdio};
use std::time::Instant; use std::time::Instant;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct MeasuredSamples { pub struct MeasuredSamples {
pub baseline_s: Vec<f64>, pub baseline_s: Vec<f64>,
pub candidate_s: Vec<f64>, pub candidate_s: Vec<f64>,
/// Per-run max resident set size, normalized to KiB from `wait4`
/// ru_maxrss (whose own unit is platform-defined — see
/// `maxrss_to_kib`). Empty on non-Unix hosts.
pub baseline_max_rss_kib: Vec<u64>,
pub candidate_max_rss_kib: Vec<u64>,
} }
fn time_once(shell_cmd: &str, inject_spin_s: f64) -> anyhow::Result<f64> { /// Raw `ru_maxrss` → KiB.
///
/// The unit of `ru_maxrss` is *platform-defined*, and the platforms
/// disagree: Linux and the BSDs report kilobytes, while Apple platforms
/// (macOS et al.) report **bytes**. Taking the raw value as KiB everywhere
/// — as this did — silently inflates every macOS reading by 1024×, and the
/// number is not cosmetic: verdict records its median as
/// `env_fingerprint.max_rss_kib` in the ledger, where an allocator-swap
/// attempt is judged partly on memory. Everything downstream is KiB, so the
/// raw value is normalized here, at the syscall edge.
///
/// `raw_is_bytes` is the platform predicate (passed at the one call site, so
/// this stays a pure function testable on every host). Bytes round *up*: a
/// nonzero RSS must never record as 0 KiB. Negative values — a kernel that
/// reports nothing — clamp to 0 rather than wrapping into the exabytes.
#[cfg(any(unix, test))]
fn maxrss_to_kib(raw: i64, raw_is_bytes: bool) -> u64 {
let raw = raw.max(0) as u64;
if raw_is_bytes {
raw.div_ceil(1024)
} else {
raw
}
}
/// Reap the child, returning its exit status and max RSS in KiB when the
/// platform reports it.
#[cfg(unix)]
fn wait_with_rusage(child: &mut Child) -> anyhow::Result<(ExitStatus, Option<u64>)> {
use std::os::unix::process::ExitStatusExt;
let pid = child.id() as libc::pid_t;
let mut status: libc::c_int = 0;
let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
loop {
let r = unsafe { libc::wait4(pid, &mut status, 0, &mut usage) };
if r == pid {
break;
}
let err = std::io::Error::last_os_error();
if err.raw_os_error() != Some(libc::EINTR) {
return Err(err).context("wait4 failed");
}
}
// Apple's getrusage reports ru_maxrss in bytes; Linux and the BSDs in KiB.
let max_rss_kib = maxrss_to_kib(usage.ru_maxrss as i64, cfg!(target_vendor = "apple"));
Ok((ExitStatus::from_raw(status), Some(max_rss_kib)))
}
#[cfg(not(unix))]
fn wait_with_rusage(child: &mut Child) -> anyhow::Result<(ExitStatus, Option<u64>)> {
Ok((child.wait()?, None))
}
fn time_once(shell_cmd: &str, inject_spin_s: f64) -> anyhow::Result<(f64, Option<u64>)> {
let start = Instant::now(); let start = Instant::now();
let status = Command::new("sh") let mut child = Command::new("sh")
.arg("-c") .arg("-c")
.arg(shell_cmd) .arg(shell_cmd)
.stdout(Stdio::null()) .stdout(Stdio::null())
.stderr(Stdio::null()) .stderr(Stdio::null())
.status() .spawn()
.with_context(|| format!("failed to spawn: {shell_cmd}"))?; .with_context(|| format!("failed to spawn: {shell_cmd}"))?;
let (status, max_rss_kib) = wait_with_rusage(&mut child)?;
if inject_spin_s > 0.0 { if inject_spin_s > 0.0 {
// Calibration-only: a real busy-wait inside the timed window, so an // Calibration-only: a real busy-wait inside the timed window, so an
// injected slowdown flows through the entire measurement pipeline // injected slowdown flows through the entire measurement pipeline
@@ -35,7 +93,7 @@ fn time_once(shell_cmd: &str, inject_spin_s: f64) -> anyhow::Result<f64> {
} }
let elapsed = start.elapsed().as_secs_f64(); let elapsed = start.elapsed().as_secs_f64();
anyhow::ensure!(status.success(), "command exited nonzero: {shell_cmd}"); anyhow::ensure!(status.success(), "command exited nonzero: {shell_cmd}");
Ok(elapsed) Ok((elapsed, max_rss_kib))
} }
/// Run baseline and candidate commands on the interleaved schedule and /// Run baseline and candidate commands on the interleaved schedule and
@@ -53,19 +111,56 @@ pub fn run_interleaved(
let mut out = MeasuredSamples { let mut out = MeasuredSamples {
baseline_s: Vec::with_capacity(runs_per_side), baseline_s: Vec::with_capacity(runs_per_side),
candidate_s: Vec::with_capacity(runs_per_side), candidate_s: Vec::with_capacity(runs_per_side),
baseline_max_rss_kib: Vec::new(),
candidate_max_rss_kib: Vec::new(),
}; };
for (side, measured) in interleaved(runs_per_side, warmup) { for (side, measured) in interleaved(runs_per_side, warmup) {
let (cmd, spin) = match side { let (cmd, spin) = match side {
Side::Baseline => (baseline_cmd, 0.0), Side::Baseline => (baseline_cmd, 0.0),
Side::Candidate => (candidate_cmd, inject_candidate_spin_s), Side::Candidate => (candidate_cmd, inject_candidate_spin_s),
}; };
let elapsed = time_once(cmd, spin)?; let (elapsed, max_rss_kib) = time_once(cmd, spin)?;
if measured { if measured {
match side { match side {
Side::Baseline => out.baseline_s.push(elapsed), Side::Baseline => {
Side::Candidate => out.candidate_s.push(elapsed), out.baseline_s.push(elapsed);
out.baseline_max_rss_kib.extend(max_rss_kib);
}
Side::Candidate => {
out.candidate_s.push(elapsed);
out.candidate_max_rss_kib.extend(max_rss_kib);
}
} }
} }
} }
Ok(out) Ok(out)
} }
#[cfg(test)]
mod tests {
use super::maxrss_to_kib;
#[test]
fn maxrss_kib_passthrough_when_platform_reports_kib() {
// Linux / BSD: already KiB.
assert_eq!(maxrss_to_kib(2048, false), 2048);
assert_eq!(maxrss_to_kib(0, false), 0);
}
#[test]
fn maxrss_bytes_are_converted_not_recorded_raw() {
// Apple: bytes. 2 MiB is 2048 KiB, not 2_097_152 — the 1024×
// inflation this guards against.
assert_eq!(maxrss_to_kib(2_097_152, true), 2048);
// Rounds up: a nonzero RSS never records as 0 KiB.
assert_eq!(maxrss_to_kib(1, true), 1);
assert_eq!(maxrss_to_kib(1025, true), 2);
assert_eq!(maxrss_to_kib(0, true), 0);
}
#[test]
fn maxrss_negative_clamps_instead_of_wrapping() {
assert_eq!(maxrss_to_kib(-1, false), 0);
assert_eq!(maxrss_to_kib(-1, true), 0);
}
}
+3 -1
View File
@@ -3,9 +3,11 @@
//! The product is this crate: interleaved A/B measurement, bootstrap CIs //! The product is this crate: interleaved A/B measurement, bootstrap CIs
//! over the ratio of medians, threshold-gated accept decisions, and A/A //! over the ratio of medians, threshold-gated accept decisions, and A/A
//! self-calibration. Pure statistics live here and are unit-tested; //! self-calibration. Pure statistics live here and are unit-tested;
//! process execution and `perf stat` integration live in `exec`. //! process execution lives in `exec`; opt-in `perf stat` diagnostics
//! live in `counters`.
pub mod config; pub mod config;
pub mod counters;
pub mod exec; pub mod exec;
pub mod fingerprint; pub mod fingerprint;
pub mod schedule; pub mod schedule;
+26 -4
View File
@@ -1,5 +1,7 @@
use anyhow::Result; use anyhow::Result;
use bench_runner::{config::AcceptConfig, decide, exec, fingerprint::EnvFingerprint, stats}; use bench_runner::{
config::AcceptConfig, counters, decide, exec, fingerprint::EnvFingerprint, stats,
};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use ledger::Verdict; use ledger::Verdict;
use std::path::PathBuf; use std::path::PathBuf;
@@ -25,6 +27,11 @@ enum Cmd {
baseline: String, baseline: String,
#[arg(long)] #[arg(long)]
candidate: String, candidate: String,
/// Wrap each timed run in `perf stat` and print per-side counter
/// medians (Unix only; opt-in diagnostics, never part of the
/// accept decision).
#[arg(long, default_value_t = false)]
perf_stat: bool,
}, },
/// A/A self-test: same command both sides; must yield a null verdict. /// A/A self-test: same command both sides; must yield a null verdict.
Aa { Aa {
@@ -429,12 +436,13 @@ fn main() -> Result<()> {
None => AcceptConfig::default(), None => AcceptConfig::default(),
}; };
let (baseline, candidate, aa_mode) = match &cli.command { let (baseline, candidate, aa_mode, perf_stat) = match &cli.command {
Cmd::Compare { Cmd::Compare {
baseline, baseline,
candidate, candidate,
} => (baseline.clone(), candidate.clone(), false), perf_stat,
Cmd::Aa { cmd } => (cmd.clone(), cmd.clone(), true), } => (baseline.clone(), candidate.clone(), false, *perf_stat),
Cmd::Aa { cmd } => (cmd.clone(), cmd.clone(), true, false),
Cmd::Calibrate { Cmd::Calibrate {
cmd, cmd,
sessions, sessions,
@@ -502,6 +510,15 @@ fn main() -> Result<()> {
cfg.runs_per_side, cfg.warmup_runs cfg.runs_per_side, cfg.warmup_runs
); );
let (samples, perf_report) = if perf_stat {
let (samples, report) = counters::run_interleaved_perf(
&baseline,
&candidate,
cfg.runs_per_side,
cfg.warmup_runs,
)?;
(samples, Some(report))
} else {
let samples = exec::run_interleaved( let samples = exec::run_interleaved(
&baseline, &baseline,
&candidate, &candidate,
@@ -509,6 +526,8 @@ fn main() -> Result<()> {
cfg.warmup_runs, cfg.warmup_runs,
0.0, 0.0,
)?; )?;
(samples, None)
};
let ci = stats::bootstrap_ratio_ci( let ci = stats::bootstrap_ratio_ci(
&samples.baseline_s, &samples.baseline_s,
&samples.candidate_s, &samples.candidate_s,
@@ -524,6 +543,9 @@ fn main() -> Result<()> {
ci.lo, ci.lo,
ci.hi ci.hi
); );
if let Some(report) = &perf_report {
report.print(&baseline, &candidate);
}
if aa_mode { if aa_mode {
// Null verdict required: an "accept" here is a calibration failure. // Null verdict required: an "accept" here is a calibration failure.
+7
View File
@@ -147,6 +147,7 @@ fn spawn_server(cfg: &ServiceCfg, port: u16) -> Result<Child> {
Ok(child) Ok(child)
} }
#[cfg(unix)]
fn set_linger_zero(s: &TcpStream) { fn set_linger_zero(s: &TcpStream) {
// SO_LINGER with a 0 timeout → RST on close instead of FIN, so the // SO_LINGER with a 0 timeout → RST on close instead of FIN, so the
// tens of thousands of short client connections a load session opens // tens of thousands of short client connections a load session opens
@@ -170,6 +171,12 @@ fn set_linger_zero(s: &TcpStream) {
} }
} }
#[cfg(not(unix))]
fn set_linger_zero(_s: &TcpStream) {
// Service mode is POSIX-only at runtime (`sh`, taskset); this stub
// only keeps the crate compiling on non-Unix hosts.
}
fn one_request(port: u16) -> std::io::Result<()> { fn one_request(port: u16) -> std::io::Result<()> {
let mut s = TcpStream::connect((Ipv4Addr::LOCALHOST, port))?; let mut s = TcpStream::connect((Ipv4Addr::LOCALHOST, port))?;
s.set_nodelay(true)?; s.set_nodelay(true)?;
+71
View File
@@ -43,6 +43,49 @@ pub fn all_passed(results: &[(GateLayer, GateOutcome)]) -> bool {
.any(|(_, o)| matches!(o, GateOutcome::Passed)) .any(|(_, o)| matches!(o, GateOutcome::Passed))
} }
/// Inputs to one gate run: the candidate binary under test, the pristine
/// baseline to differ it against, and the differential-fuzz iteration
/// budget (from AcceptConfig, default 10_000).
#[derive(Debug, Clone, Copy)]
pub struct GateInputs<'a> {
/// Patched binary (repo-root-relative): golden replay runs it, and it
/// is the `{candidate}` side of differential fuzz.
pub candidate_binary: &'a str,
/// Pristine baseline binary (repo-root-relative), the `{baseline}` side
/// of differential fuzz. `None` when the caller has no baseline to
/// differ against — the standalone `just gates` flow builds only the
/// working tree — and the fuzz gate is then skipped with that reason
/// rather than silently comparing the candidate to itself. The accept
/// path (`just verdict`) rebuilds a pristine baseline and passes it,
/// and only a fuzz gate that actually Passed can mint an accept.
pub baseline_binary: Option<&'a str>,
pub fuzz_iters: u64,
}
/// Everything one gate run produced: the four layers in fixed order
/// (UpstreamTests, GoldenReplay, DifferentialFuzz, Sanitizers) plus the
/// fuzz counts and equivalence mode that verdict records in the ledger.
#[derive(Debug, Clone)]
pub struct GateReport {
pub gates: Vec<(GateLayer, GateOutcome)>,
/// Fuzz iterations actually executed (parsed from the FUZZ-RESULT
/// line); 0 when the fuzz gate was skipped or never reported.
pub fuzz_iters: u64,
pub fuzz_divergence: bool,
/// "byte-identical" | "fp-tolerance"
pub equivalence_mode: String,
}
impl GateReport {
pub fn outcome(&self, layer: GateLayer) -> Option<&GateOutcome> {
self.gates.iter().find(|(l, _)| *l == layer).map(|(_, o)| o)
}
pub fn all_passed(&self) -> bool {
all_passed(&self.gates)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -71,4 +114,32 @@ mod tests {
)]; )];
assert!(!all_passed(&results)); assert!(!all_passed(&results));
} }
#[test]
fn report_outcome_looks_up_by_layer() {
let report = GateReport {
gates: vec![
(GateLayer::UpstreamTests, GateOutcome::Passed),
(
GateLayer::GoldenReplay,
GateOutcome::Failed {
detail: "stdout diverged".into(),
},
),
],
fuzz_iters: 0,
fuzz_divergence: false,
equivalence_mode: "byte-identical".into(),
};
assert_eq!(
report.outcome(GateLayer::UpstreamTests),
Some(&GateOutcome::Passed)
);
assert!(matches!(
report.outcome(GateLayer::GoldenReplay),
Some(GateOutcome::Failed { .. })
));
assert_eq!(report.outcome(GateLayer::Sanitizers), None);
assert!(!report.all_passed());
}
} }
+47 -7
View File
@@ -1,12 +1,18 @@
//! Gate orchestration CLI: corpus pin → upstream tests → golden replay. //! Gate orchestration CLI: corpus + test-suite pins → upstream tests →
//! Differential fuzz and sanitizers remain per-attempt manual steps in //! policy-aware golden replay → differential fuzz. Sanitizers run on the
//! Phase 1 and are reported as skipped with a reason. //! accept path in verdict and are reported as skipped with a reason.
//!
//! Differential fuzz is skipped here too, and for the same structural
//! reason: it differs a *pristine baseline* against the candidate, and this
//! flow builds only the working tree — there is nothing to differ against.
//! The accept path (`just verdict`) rebuilds a pristine baseline, passes it
//! in, and refuses to mint an accept unless the fuzz gate actually passed.
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::Parser;
use diff_test::orchestrate::run_core_gates; use diff_test::orchestrate::run_core_gates;
use diff_test::target::{subst_out, TargetSpec}; use diff_test::target::{subst_out, TargetSpec};
use diff_test::{all_passed, GateOutcome}; use diff_test::{GateInputs, GateLayer, GateOutcome};
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Parser)] #[derive(Parser)]
@@ -21,6 +27,13 @@ struct Cli {
/// tree is built to targets/<name>/gates-build and that is tested. /// tree is built to targets/<name>/gates-build and that is tested.
#[arg(long)] #[arg(long)]
binary: Option<String>, binary: Option<String>,
/// Differential-fuzz iteration budget, substituted for {iters} in the
/// target's [gates].fuzz command. Inert on this flow: with no pristine
/// baseline to differ against, the fuzz gate is skipped. The accept path
/// (`just verdict`) rebuilds one and takes its budget from
/// config/accept.toml.
#[arg(long, default_value_t = 10_000)]
fuzz_iters: u64,
} }
fn main() -> Result<()> { fn main() -> Result<()> {
@@ -47,15 +60,42 @@ fn main() -> Result<()> {
subst_out(&spec.build.binary, &out) subst_out(&spec.build.binary, &out)
} }
}; };
let results = run_core_gates(&root, &spec, &binary)?; let report = run_core_gates(
for (layer, outcome) in &results { &root,
&spec,
&GateInputs {
candidate_binary: &binary,
// No pristine baseline on this flow — see the module doc. The
// fuzz gate reports Skipped with that reason rather than
// comparing the candidate against itself.
baseline_binary: None,
fuzz_iters: cli.fuzz_iters,
},
)?;
for (layer, outcome) in &report.gates {
match outcome { match outcome {
GateOutcome::Passed if *layer == GateLayer::DifferentialFuzz => {
println!("{layer:?}: PASS ({} iters)", report.fuzz_iters)
}
GateOutcome::Passed => println!("{layer:?}: PASS"), GateOutcome::Passed => println!("{layer:?}: PASS"),
GateOutcome::Failed { detail } => println!("{layer:?}: FAIL — {detail}"), GateOutcome::Failed { detail } => println!("{layer:?}: FAIL — {detail}"),
GateOutcome::Skipped { reason } => println!("{layer:?}: skipped — {reason}"), GateOutcome::Skipped { reason } => println!("{layer:?}: skipped — {reason}"),
} }
} }
if all_passed(&results) { if report.all_passed() {
// Inside all_passed(), the fuzz gate is Passed or Skipped — never
// Failed. Say so plainly: a PASS here must not read as a verified
// differential-fuzz run, which is exactly the confusion that let
// accepted rows carry fuzz_iters=0.
if !matches!(
report.outcome(GateLayer::DifferentialFuzz),
Some(GateOutcome::Passed)
) {
println!(
"note: differential fuzz did not run — `just gates` is not an accept path; only \
`just verdict` (pristine baseline + fuzz gate) can accept a change"
);
}
println!("gates: PASS ({})", cli.target); println!("gates: PASS ({})", cli.target);
Ok(()) Ok(())
} else { } else {
+439 -19
View File
@@ -1,10 +1,20 @@
//! Core gate sequence, callable from the diff-test CLI and from verdict: //! Core gate sequence, callable from the diff-test CLI and from verdict:
//! corpus pin (refuses on mismatch) → upstream tests → golden replay. //! corpus + test-suite pins (refuse on mismatch) → upstream tests →
//! policy-aware golden replay → differential fuzz. Sanitizers run on the
//! accept path in verdict and are reported here as skipped with a reason.
//!
//! Differential fuzz is the one gate that is not purely a property of the
//! candidate: it differs the candidate against a *pristine baseline*, so it
//! needs both binaries. Callers that have only a candidate (the standalone
//! `just gates` flow) pass `baseline_binary: None` and the gate is skipped
//! with that reason recorded — it is never faked by comparing the candidate
//! against itself, and verdict refuses to mint an accept without it.
use crate::policy::EquivalencePolicy;
use crate::target::{expected_golden_hash, TargetSpec}; use crate::target::{expected_golden_hash, TargetSpec};
use crate::{pin, GateLayer, GateOutcome}; use crate::{pin, GateInputs, GateLayer, GateOutcome, GateReport};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::path::Path; use std::path::{Path, PathBuf};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
fn sh(root: &Path, cmd: &str) -> anyhow::Result<std::process::Output> { fn sh(root: &Path, cmd: &str) -> anyhow::Result<std::process::Output> {
@@ -17,14 +27,155 @@ fn sh(root: &Path, cmd: &str) -> anyhow::Result<std::process::Output> {
.output()?) .output()?)
} }
/// Run the mechanical gates for `target`, testing `binary` (repo-root- /// TESTSUITE.sha256 lives beside the corpus MANIFEST.sha256 (both under
/// relative) in the golden replay. Corpus-pin mismatch is an error, not /// corpora/<target>/), so its path derives from the manifest's.
/// a gate failure: nothing runs on a tampered corpus. fn suite_manifest_path(corpus_manifest: &Path) -> PathBuf {
corpus_manifest
.parent()
.unwrap_or_else(|| Path::new(""))
.join("TESTSUITE.sha256")
}
fn parse_u64_strict(s: &str) -> Option<u64> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
s.parse().ok()
}
/// Parse the last `FUZZ-RESULT iters=<u64> divergences=<u64>` stdout line
/// of a fuzz command (exact ASCII form, single spaces). Returns
/// `(iters, divergences)`, or None when no line matches.
pub fn parse_fuzz_result(stdout: &str) -> Option<(u64, u64)> {
stdout.lines().rev().find_map(|line| {
let line = line.strip_suffix('\r').unwrap_or(line);
let rest = line.strip_prefix("FUZZ-RESULT iters=")?;
let (iters, rest) = rest.split_once(' ')?;
let divergences = rest.strip_prefix("divergences=")?;
Some((parse_u64_strict(iters)?, parse_u64_strict(divergences)?))
})
}
/// Recorded verbatim when the target declares no fuzz command at all.
const FUZZ_SKIP_NO_COMMAND: &str = "no fuzz command declared in target.toml [gates].fuzz";
/// Recorded verbatim when a fuzz command exists but the caller has no
/// pristine baseline: differential fuzz differs two binaries, and comparing
/// the candidate against itself would be a gate that cannot fail.
const FUZZ_SKIP_NO_BASELINE: &str =
"differential fuzz needs a pristine baseline binary; run the accept path (`just verdict`), \
which rebuilds one";
/// Substitute the three `[gates].fuzz` template placeholders. The contract
/// with `targets/<t>/target.toml` and `scripts/diff-fuzz-*.py`: `{iters}`
/// is the iteration budget, `{baseline}` the pristine baseline binary and
/// `{candidate}` the patched one (both repo-root-relative paths, and the
/// command runs from the repo root). A fuzz script must never hardcode a
/// baseline path — the harness owns where the pristine build lands.
fn fuzz_command(tmpl: &str, iters: u64, baseline: &str, candidate: &str) -> String {
tmpl.replace("{iters}", &iters.to_string())
.replace("{baseline}", baseline)
.replace("{candidate}", candidate)
}
/// What the differential-fuzz gate will do, decided before any process runs.
#[derive(Debug, PartialEq, Eq)]
enum FuzzPlan {
Skip(&'static str),
Run(String),
}
/// Differential fuzz is a baseline-vs-candidate gate: it needs both sides.
/// Missing either one is a Skip with the reason recorded, never a silent
/// self-comparison and never a hard error (the standalone gates flow has
/// no baseline by construction).
fn plan_fuzz(fuzz: Option<&str>, inputs: &GateInputs) -> FuzzPlan {
match (fuzz, inputs.baseline_binary) {
(None, _) => FuzzPlan::Skip(FUZZ_SKIP_NO_COMMAND),
(Some(_), None) => FuzzPlan::Skip(FUZZ_SKIP_NO_BASELINE),
(Some(tmpl), Some(baseline)) => FuzzPlan::Run(fuzz_command(
tmpl,
inputs.fuzz_iters,
baseline,
inputs.candidate_binary,
)),
}
}
/// Grade one executed fuzz run. Passed iff the command exited 0, printed a
/// well-formed `FUZZ-RESULT` line, and reported zero divergences — a run
/// that never reports the line is a Failed gate, not a pass by silence.
/// Returns `(outcome, iters_actually_executed, divergence)`.
fn fuzz_gate_outcome(
stdout: &str,
success: bool,
exit_code: Option<i32>,
) -> (GateOutcome, u64, bool) {
match parse_fuzz_result(stdout) {
None => (
GateOutcome::Failed {
detail: "fuzz command did not report FUZZ-RESULT".into(),
},
0,
false,
),
Some((iters, divergences)) => (
if success && divergences == 0 {
GateOutcome::Passed
} else {
GateOutcome::Failed {
detail: format!("exit={exit_code:?} iters={iters} divergences={divergences}"),
}
},
iters,
divergences > 0,
),
}
}
/// The fp-tolerance policy fails closed without a committed reference:
/// there is no hash to fall back to that a tolerance could compare against.
fn fp_reference(golden_reference: Option<&PathBuf>) -> Result<&PathBuf, GateOutcome> {
golden_reference.ok_or_else(|| GateOutcome::Failed {
detail: "fp-tolerance policy requires corpus.golden_reference in target.toml".into(),
})
}
/// GoldenReplay verdict under an fp-tolerance policy: the replay stdout is
/// compared token-wise against the committed reference output.
fn fp_golden_outcome(
policy: &EquivalencePolicy,
reference: &str,
stdout: &str,
success: bool,
exit_code: Option<i32>,
) -> GateOutcome {
if !success {
return GateOutcome::Failed {
detail: format!("golden replay command failed (exit={exit_code:?})"),
};
}
match policy.compare(reference, stdout) {
Ok(()) => GateOutcome::Passed,
Err(d) => GateOutcome::Failed {
detail: format!(
"diverged at token/offset {}: baseline={:?} candidate={:?} — {}",
d.token, d.baseline, d.candidate, d.reason
),
},
}
}
/// Run the mechanical gates for `target`, testing `inputs.candidate_binary`
/// (repo-root-relative) in golden replay and differential fuzz. Differential
/// fuzz additionally needs `inputs.baseline_binary` to differ against and is
/// skipped-with-reason without one. Pin mismatches (corpus or test suite)
/// are errors, not gate failures: nothing runs on tampered inputs.
pub fn run_core_gates( pub fn run_core_gates(
root: &Path, root: &Path,
spec: &TargetSpec, spec: &TargetSpec,
binary: &str, inputs: &GateInputs,
) -> anyhow::Result<Vec<(GateLayer, GateOutcome)>> { ) -> anyhow::Result<GateReport> {
let verified = pin::verify_manifest( let verified = pin::verify_manifest(
&root.join(&spec.corpus.manifest), &root.join(&spec.corpus.manifest),
&root.join(&spec.corpus.root), &root.join(&spec.corpus.root),
@@ -34,6 +185,24 @@ pub fn run_core_gates(
spec.corpus.manifest.display() spec.corpus.manifest.display()
); );
// Test-suite pin: same refuse-on-mismatch posture as the corpus pin,
// but the manifest is optional until a human deliberately generates it.
let suite_manifest = suite_manifest_path(&spec.corpus.manifest);
if root.join(&suite_manifest).exists() {
let verified = pin::verify_manifest(&root.join(&suite_manifest), root).map_err(|e| {
anyhow::anyhow!("upstream test suite hash mismatch — refusing to run: {e}")
})?;
eprintln!(
"test-suite pin: {verified} files verified against {}",
suite_manifest.display()
);
} else {
eprintln!(
"test-suite pin: {} not found — suite unpinned (generate with scripts/pin-testsuite.sh)",
suite_manifest.display()
);
}
let mut results = Vec::new(); let mut results = Vec::new();
let t = sh(root, &spec.gates.tests)?; let t = sh(root, &spec.gates.tests)?;
@@ -51,11 +220,20 @@ pub fn run_core_gates(
}, },
)); ));
let g = sh(root, &spec.gates.golden.replace("{binary}", binary))?; let policy = EquivalencePolicy::load(&spec.target_dir(root))?;
let equivalence_mode = match &policy {
EquivalencePolicy::ByteIdentical => "byte-identical",
EquivalencePolicy::FpTolerance { .. } => "fp-tolerance",
};
let golden_cmd = spec
.gates
.golden
.replace("{binary}", inputs.candidate_binary);
let golden = match &policy {
EquivalencePolicy::ByteIdentical => {
let g = sh(root, &golden_cmd)?;
let expected = expected_golden_hash(&root.join(&spec.corpus.golden_sha256))?; let expected = expected_golden_hash(&root.join(&spec.corpus.golden_sha256))?;
let actual = format!("{:x}", Sha256::digest(&g.stdout)); let actual = format!("{:x}", Sha256::digest(&g.stdout));
results.push((
GateLayer::GoldenReplay,
if g.status.success() && actual == expected { if g.status.success() && actual == expected {
GateOutcome::Passed GateOutcome::Passed
} else { } else {
@@ -65,20 +243,262 @@ pub fn run_core_gates(
g.status.code() g.status.code()
), ),
} }
}, }
)); }
EquivalencePolicy::FpTolerance { .. } => {
match fp_reference(spec.corpus.golden_reference.as_ref()) {
Err(outcome) => outcome,
Ok(reference_path) => {
let reference =
std::fs::read_to_string(root.join(reference_path)).map_err(|e| {
anyhow::anyhow!(
"cannot read corpus.golden_reference {}: {e}",
reference_path.display()
)
})?;
let g = sh(root, &golden_cmd)?;
fp_golden_outcome(
&policy,
&reference,
&String::from_utf8_lossy(&g.stdout),
g.status.success(),
g.status.code(),
)
}
}
}
};
results.push((GateLayer::GoldenReplay, golden));
results.push(( let (fuzz_outcome, fuzz_iters, fuzz_divergence) =
GateLayer::DifferentialFuzz, match plan_fuzz(spec.gates.fuzz.as_deref(), inputs) {
FuzzPlan::Skip(reason) => (
GateOutcome::Skipped { GateOutcome::Skipped {
reason: "per-attempt manual step in Phase 1 (needs old/new pair)".into(), reason: reason.to_string(),
}, },
)); 0,
false,
),
FuzzPlan::Run(cmd) => {
let f = sh(root, &cmd)?;
fuzz_gate_outcome(
&String::from_utf8_lossy(&f.stdout),
f.status.success(),
f.status.code(),
)
}
};
results.push((GateLayer::DifferentialFuzz, fuzz_outcome));
results.push(( results.push((
GateLayer::Sanitizers, GateLayer::Sanitizers,
GateOutcome::Skipped { GateOutcome::Skipped {
reason: "per-attempt manual step in Phase 1 (nightly toolchain)".into(), reason:
"sanitizers run on the accept path in verdict (ASan/UBSan, TSan when configured)"
.into(),
}, },
)); ));
Ok(results)
Ok(GateReport {
gates: results,
fuzz_iters,
fuzz_divergence,
equivalence_mode: equivalence_mode.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn suite_manifest_derives_from_corpus_manifest() {
assert_eq!(
suite_manifest_path(Path::new("corpora/cjson/MANIFEST.sha256")),
Path::new("corpora/cjson").join("TESTSUITE.sha256")
);
assert_eq!(
suite_manifest_path(Path::new("MANIFEST.sha256")),
Path::new("TESTSUITE.sha256")
);
}
#[test]
fn fuzz_result_parses_last_exact_line() {
assert_eq!(
parse_fuzz_result("FUZZ-RESULT iters=10000 divergences=0\n"),
Some((10000, 0))
);
// Last matching line wins; trailing non-matching output is ignored.
let out = "FUZZ-RESULT iters=1 divergences=1\nprogress...\nFUZZ-RESULT iters=9999 divergences=2\ndone\n";
assert_eq!(parse_fuzz_result(out), Some((9999, 2)));
// CRLF-terminated capture still parses.
assert_eq!(
parse_fuzz_result("FUZZ-RESULT iters=42 divergences=0\r\n"),
Some((42, 0))
);
}
#[test]
fn fuzz_result_rejects_malformed_lines() {
for bad in [
"",
"no result here",
" FUZZ-RESULT iters=1 divergences=0", // leading space
"FUZZ-RESULT iters=1 divergences=0", // double space
"FUZZ-RESULT iters=abc divergences=0", // non-numeric
"FUZZ-RESULT iters=1 divergences=-1", // sign not allowed
"FUZZ-RESULT iters=1 divergences=0 xx", // trailing junk
"FUZZ-RESULT divergences=0 iters=1", // wrong field order
"FUZZ-RESULT iters=1", // missing field
] {
assert_eq!(parse_fuzz_result(bad), None, "should reject: {bad:?}");
}
}
fn inputs(baseline: Option<&'static str>) -> GateInputs<'static> {
GateInputs {
candidate_binary: "targets/cjson/cand/cjson-bench",
baseline_binary: baseline,
fuzz_iters: 10_000,
}
}
const TMPL: &str = "python3 scripts/diff-fuzz-cjson.py targets/cjson/fuzz-work \
{iters} {baseline} {candidate}";
#[test]
fn fuzz_command_substitutes_all_three_placeholders() {
assert_eq!(
fuzz_command(TMPL, 500, "base/bin", "cand/bin"),
"python3 scripts/diff-fuzz-cjson.py targets/cjson/fuzz-work 500 base/bin cand/bin"
);
// Repeated placeholders all substitute; unknown braces are left alone.
assert_eq!(
fuzz_command("{candidate} {candidate} {iters} {other}", 2, "b", "c"),
"c c 2 {other}"
);
}
#[test]
fn fuzz_skipped_when_no_command_declared() {
assert_eq!(
plan_fuzz(None, &inputs(Some("targets/cjson/baseline/cjson-bench"))),
FuzzPlan::Skip("no fuzz command declared in target.toml [gates].fuzz")
);
// Absent command wins even without a baseline: nothing to run either way.
assert_eq!(
plan_fuzz(None, &inputs(None)),
FuzzPlan::Skip(FUZZ_SKIP_NO_COMMAND)
);
}
/// The regression this branch exists for: `just gates` (and CI) build only
/// the working tree, so there is no pristine baseline. Before the fix the
/// fuzz script silently hardcoded targets/<t>/baseline/… — a path nothing
/// on that flow builds — and the gate hard-failed. Now it skips, loudly.
#[test]
fn fuzz_skipped_when_declared_but_no_baseline() {
assert_eq!(
plan_fuzz(Some(TMPL), &inputs(None)),
FuzzPlan::Skip(
"differential fuzz needs a pristine baseline binary; run the accept path \
(`just verdict`), which rebuilds one"
)
);
}
#[test]
fn fuzz_runs_with_both_sides() {
assert_eq!(
plan_fuzz(
Some(TMPL),
&inputs(Some("targets/cjson/baseline/cjson-bench"))
),
FuzzPlan::Run(
"python3 scripts/diff-fuzz-cjson.py targets/cjson/fuzz-work 10000 \
targets/cjson/baseline/cjson-bench targets/cjson/cand/cjson-bench"
.to_string()
)
);
}
#[test]
fn fuzz_gate_grades_clean_run_as_pass() {
let (outcome, iters, diverged) =
fuzz_gate_outcome("FUZZ-RESULT iters=10000 divergences=0\n", true, Some(0));
assert_eq!(outcome, GateOutcome::Passed);
// The ledger records iterations *actually executed*, not the budget.
assert_eq!(iters, 10_000);
assert!(!diverged);
}
#[test]
fn fuzz_gate_fails_on_divergence_nonzero_exit_or_no_result_line() {
// Divergence reported: fail, and the count is still recorded.
let (outcome, iters, diverged) =
fuzz_gate_outcome("FUZZ-RESULT iters=900 divergences=3\n", false, Some(1));
assert!(
matches!(outcome, GateOutcome::Failed { detail } if detail.contains("divergences=3"))
);
assert_eq!(iters, 900);
assert!(diverged);
// Zero divergences but a nonzero exit (crash/timeout) is NOT a pass.
let (outcome, ..) =
fuzz_gate_outcome("FUZZ-RESULT iters=10 divergences=0\n", false, Some(2));
assert!(
matches!(outcome, GateOutcome::Failed { detail } if detail.contains("exit=Some(2)"))
);
// A run that never reports the contract line fails closed (e.g. the
// script died before printing — the old hardcoded-baseline breakage).
let (outcome, iters, diverged) =
fuzz_gate_outcome("Traceback (most recent call last):\n", false, Some(1));
assert_eq!(
outcome,
GateOutcome::Failed {
detail: "fuzz command did not report FUZZ-RESULT".into()
}
);
assert_eq!(iters, 0);
assert!(!diverged);
}
#[test]
fn fp_tolerance_requires_golden_reference() {
let err = fp_reference(None).unwrap_err();
assert_eq!(
err,
GateOutcome::Failed {
detail: "fp-tolerance policy requires corpus.golden_reference in target.toml"
.into()
}
);
let path = PathBuf::from("corpora/matmul/GOLDEN.reference");
assert_eq!(fp_reference(Some(&path)).unwrap(), &path);
}
#[test]
fn fp_golden_outcome_applies_policy() {
let policy = EquivalencePolicy::FpTolerance {
abs: 1e-6,
rel: 1e-5,
};
// Last-ULP reordering within tolerance passes.
assert_eq!(
fp_golden_outcome(&policy, "1.0000000 2.0", "1.0000001 2.0", true, Some(0)),
GateOutcome::Passed
);
// A genuine wrong value fails with the divergence in the detail.
assert!(matches!(
fp_golden_outcome(&policy, "1.0 2.0", "1.0 2.5", true, Some(0)),
GateOutcome::Failed { detail } if detail.contains("token/offset 1")
));
// A failing replay command fails regardless of output.
assert!(matches!(
fp_golden_outcome(&policy, "1.0", "1.0", false, Some(3)),
GateOutcome::Failed { detail } if detail.contains("exit=Some(3)")
));
}
} }
+76 -1
View File
@@ -12,6 +12,10 @@ pub struct TargetSpec {
pub bench: Bench, pub bench: Bench,
pub gates: Gates, pub gates: Gates,
pub corpus: Corpus, pub corpus: Corpus,
/// Target name (the directory under `targets/`), captured by `load`;
/// not part of target.toml itself.
#[serde(skip)]
name: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -52,6 +56,14 @@ pub struct Build {
/// Sanitizer binary path with `{out}` substituted. /// Sanitizer binary path with `{out}` substituted.
#[serde(default)] #[serde(default)]
pub sanitizer_binary: Option<String>, pub sanitizer_binary: Option<String>,
/// TSan build command; `{out}` substituted. Run on the accept path
/// when configured — a patch touching concurrency without a TSan
/// lane cannot be machine-accepted.
#[serde(default)]
pub tsan: Option<String>,
/// TSan binary path with `{out}` substituted.
#[serde(default)]
pub tsan_binary: Option<String>,
} }
/// Substitute the `{out}` isolation-dir placeholder in a build command or /// Substitute the `{out}` isolation-dir placeholder in a build command or
@@ -66,6 +78,11 @@ pub struct Gates {
pub tests: String, pub tests: String,
/// Golden replay command; stdout is hashed. `{binary}` substituted. /// Golden replay command; stdout is hashed. `{binary}` substituted.
pub golden: String, pub golden: String,
/// Differential-fuzz command; `{candidate}` and `{iters}` substituted.
/// Must print `FUZZ-RESULT iters=<n> divergences=<m>` as its final
/// stdout line. Absent → the fuzz gate is skipped with a reason.
#[serde(default)]
pub fuzz: Option<String>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -77,6 +94,11 @@ pub struct Corpus {
/// File whose last whitespace-delimited-first-field line is the /// File whose last whitespace-delimited-first-field line is the
/// expected sha256 of the golden command's stdout. /// expected sha256 of the golden command's stdout.
pub golden_sha256: PathBuf, pub golden_sha256: PathBuf,
/// Committed reference stdout of the golden command, repo-root-
/// relative. Required by fp-tolerance targets: tolerance comparison
/// needs the actual values, not a hash.
#[serde(default)]
pub golden_reference: Option<PathBuf>,
} }
impl TargetSpec { impl TargetSpec {
@@ -84,7 +106,15 @@ impl TargetSpec {
let path = repo_root.join("targets").join(name).join("target.toml"); let path = repo_root.join("targets").join(name).join("target.toml");
let raw = std::fs::read_to_string(&path) let raw = std::fs::read_to_string(&path)
.map_err(|e| anyhow::anyhow!("cannot read {}: {e}", path.display()))?; .map_err(|e| anyhow::anyhow!("cannot read {}: {e}", path.display()))?;
Ok(toml::from_str(&raw)?) let mut spec: Self = toml::from_str(&raw)?;
spec.name = name.to_string();
Ok(spec)
}
/// This target's directory under the repo root (`targets/<name>`),
/// where per-target policy files like equivalence.toml live.
pub fn target_dir(&self, root: &Path) -> PathBuf {
root.join("targets").join(&self.name)
} }
} }
@@ -100,3 +130,48 @@ pub fn expected_golden_hash(path: &Path) -> anyhow::Result<String> {
.map(str::to_string) .map(str::to_string)
.ok_or_else(|| anyhow::anyhow!("no hash entry in {}", path.display())) .ok_or_else(|| anyhow::anyhow!("no hash entry in {}", path.display()))
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn load_captures_name_and_defaults_new_fields() {
let root = std::env::temp_dir().join("hotpath-target-load");
let dir = root.join("targets").join("demo");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("target.toml"),
r#"
[source]
repo = "https://example.invalid/demo.git"
commit = "deadbeef"
license = "MIT"
[build]
baseline = "make -C targets/demo OUT={out}"
binary = "{out}/demo"
[bench]
command = "{binary} bench"
workload = "demo workload"
[gates]
tests = "make -C targets/demo test"
golden = "{binary} replay"
[corpus]
manifest = "corpora/demo/MANIFEST.sha256"
root = "corpora/demo"
golden_sha256 = "corpora/demo/GOLDEN.sha256"
"#,
)
.unwrap();
let spec = TargetSpec::load(&root, "demo").unwrap();
assert_eq!(spec.target_dir(&root), root.join("targets").join("demo"));
assert!(spec.gates.fuzz.is_none());
assert!(spec.corpus.golden_reference.is_none());
assert!(spec.build.tsan.is_none());
assert!(spec.build.tsan_binary.is_none());
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "harnessd" name = "harnessd"
description = "Agent-facing IPC daemon: the six-tool surface (SPEC §3.5) over line-delimited JSON on stdio; patches applied only after path-allowlist checks" description = "Agent-facing IPC daemon: the seven-op JSON surface (SPEC §3.5) over line-delimited JSON on stdio; patches applied only after path-allowlist checks"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
+309
View File
@@ -0,0 +1,309 @@
//! Diff-header path vetting: the allowlist between agent-authored diffs
//! and `git apply` in the target workspace. Portable (no Unix
//! dependencies) so the escape-regression tests run on every development
//! platform, not just the daemon's POSIX hosts.
use anyhow::{anyhow, ensure, Result};
use std::path::{Component, Path};
/// Relative paths only; no parent components, no .git, no absolutes.
pub fn check_rel_path(p: &str) -> Result<()> {
let path = Path::new(p);
ensure!(!path.is_absolute(), "absolute paths forbidden");
for c in path.components() {
match c {
Component::Normal(seg) => ensure!(seg != ".git", ".git access forbidden"),
Component::CurDir => {}
_ => anyhow::bail!("path escapes workspace: {p}"),
}
}
Ok(())
}
const RENAME_COPY_HEADERS: [&str; 4] = ["rename from ", "rename to ", "copy from ", "copy to "];
/// Appended to every `---`/`+++` rejection. That rule is the one place where a
/// hunk BODY line can be taken for a header — `-` and `+` are the body markers
/// themselves — and the bare error ("lacks the a/ or b/ prefix", "path escapes
/// workspace") is baffling if all you did was delete a comment. Say the cause
/// out loud. No other header rule can collide: a body line always carries a
/// `-`, `+` or space marker, so it can never begin with `diff --git ` or
/// `rename `.
const HEADER_COLLISION_NOTE: &str = "\
note: this line was vetted as a `---`/`+++` file header. The scanner does not track hunk \
boundaries — deliberate, and it stays that way: a real header must never slip past it as body \
text — so a hunk body line reaches this rule too. REMOVING a line whose own text begins with \
`-- ` renders at column 0 as `--- ...` (an unindented SQL/Lua `-- comment`), and ADDING one \
that begins with `++ ` renders as `+++ ...`. If that is what this line is, the patch is being \
rejected on a false positive. Workaround: leave such lines untouched as context and edit \
around them. Rewording or indenting the line does NOT help — the removal itself is what \
renders as `--- `. Mind the space: `--x`/`++x` (C decrement/increment) never collide.";
/// Vet every path-bearing header line of a unified git diff: `diff --git`,
/// `---`, `+++`, and the rename/copy extended headers. The whole diff is
/// rejected if any extracted path is absolute, contains a `..` component,
/// is quoted (git's C-style quoting can encode `..` invisibly), or — for
/// `---`/`+++` lines — lacks the standard `a/`/`b/` prefix (`--no-prefix`
/// output). Every line is scanned without tracking hunk boundaries, so a
/// content line colliding with a header prefix is vetted too: over-rejection
/// is the safe direction, and `git apply` remains the backstop.
pub fn check_diff_paths(diff: &str) -> Result<()> {
for line in diff.lines() {
if let Some(rest) = line.strip_prefix("diff --git ") {
let (a, b) = split_git_header_paths(rest).ok_or_else(|| {
anyhow!(
"cannot parse diff header {line:?}: expected \
`diff --git a/<path> b/<path>` with plain unquoted paths"
)
})?;
vet_extracted(a)?;
vet_extracted(b)?;
} else if line.starts_with("--- ") || line.starts_with("+++ ") {
vet_file_header(&line[4..]).map_err(|e| {
anyhow!("{e}\n offending line: {line:?}\n {HEADER_COLLISION_NOTE}")
})?;
} else if let Some(rest) = RENAME_COPY_HEADERS
.iter()
.find_map(|pre| line.strip_prefix(pre))
{
vet_extracted(header_path(rest))?;
}
}
Ok(())
}
/// Vet the path on a `---`/`+++` file-header line; `rest` is the text after
/// the four-byte marker. Errors are wrapped by the caller with
/// `HEADER_COLLISION_NOTE`, since any line reaching here *might* be a hunk
/// body line rather than a header.
fn vet_file_header(rest: &str) -> Result<()> {
let p = header_path(rest);
if p == "/dev/null" {
return Ok(());
}
ensure!(
!p.starts_with('"'),
"quoted diff paths forbidden: {p} (rewrite the patch with plain ASCII paths)"
);
let stripped = p
.strip_prefix("a/")
.or_else(|| p.strip_prefix("b/"))
.ok_or_else(|| {
anyhow!(
"diff path {p:?} lacks the a/ or b/ prefix — regenerate with \
standard `git diff` output (--no-prefix diffs are rejected)"
)
})?;
vet_extracted(stripped)
}
/// Header paths may carry a traditional-diff `\t<timestamp>` suffix.
fn header_path(rest: &str) -> &str {
rest.split('\t').next().unwrap_or(rest).trim_end()
}
/// `diff --git a/<x> b/<y>`: split at the LAST ` b/` so a ` b/` embedded
/// in <x> cannot truncate <y>; splitting only ever removes the three
/// bytes ` b/`, so a `..` component in either real path always survives
/// intact into one of the two fragments.
fn split_git_header_paths(rest: &str) -> Option<(&str, &str)> {
let a_rest = rest.strip_prefix("a/")?;
let idx = a_rest.rfind(" b/")?;
Some((&a_rest[..idx], a_rest[idx + 3..].trim_end()))
}
fn vet_extracted(p: &str) -> Result<()> {
if p == "/dev/null" {
return Ok(());
}
ensure!(!p.is_empty(), "empty path in diff header");
ensure!(
!p.starts_with('"'),
"quoted diff paths forbidden: {p} (rewrite the patch with plain ASCII paths)"
);
ensure!(
!p.starts_with('/'),
"absolute diff paths forbidden (only /dev/null is allowed, for new/deleted files): {p}"
);
check_rel_path(p)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rel_path_rules() {
assert!(check_rel_path("src/lib.rs").is_ok());
assert!(check_rel_path("./src/lib.rs").is_ok());
assert!(check_rel_path("../outside").is_err());
assert!(check_rel_path("a/../b").is_err());
assert!(check_rel_path("/etc/passwd").is_err());
assert!(check_rel_path(".git/config").is_err());
assert!(check_rel_path("a/.git/hooks").is_err());
}
#[test]
fn normal_diff_passes() {
let diff = "diff --git a/src/lib.rs b/src/lib.rs\n\
index 1111111..2222222 100644\n\
--- a/src/lib.rs\n\
+++ b/src/lib.rs\n\
@@ -1,2 +1,2 @@\n\
-old\n\
+new\n";
assert!(check_diff_paths(diff).is_ok());
}
#[test]
fn new_and_deleted_file_dev_null_passes() {
let diff = "diff --git a/newfile.rs b/newfile.rs\n\
new file mode 100644\n\
--- /dev/null\n\
+++ b/newfile.rs\n\
@@ -0,0 +1 @@\n\
+hello\n\
diff --git a/gone.rs b/gone.rs\n\
deleted file mode 100644\n\
--- a/gone.rs\n\
+++ /dev/null\n\
@@ -1 +0,0 @@\n\
-bye\n";
assert!(check_diff_paths(diff).is_ok());
}
#[test]
fn in_workspace_rename_passes() {
let diff = "diff --git a/old.rs b/nested/new.rs\n\
similarity index 100%\n\
rename from old.rs\n\
rename to nested/new.rs\n";
assert!(check_diff_paths(diff).is_ok());
}
/// The exact fixture from the apply-escape audit: a rename whose
/// destination climbs out of the workspace. `---`/`+++` scanning alone
/// never sees it — the paths live only in the `diff --git` and
/// rename headers.
#[test]
fn rename_escape_fixture_rejected() {
let diff = "diff --git a/src.txt b/../outside/escaped.txt\n\
similarity index 100%\n\
rename from src.txt\n\
rename to ../outside/escaped.txt\n";
assert!(check_diff_paths(diff).is_err());
}
#[test]
fn rename_headers_rejected_without_git_header_line() {
let diff = "similarity index 100%\n\
rename from src.txt\n\
rename to ../outside/escaped.txt\n";
assert!(check_diff_paths(diff).is_err());
}
#[test]
fn copy_escape_rejected() {
let diff = "diff --git a/src.txt b/src2.txt\n\
copy from src.txt\n\
copy to ../outside/copied.txt\n";
assert!(check_diff_paths(diff).is_err());
}
#[test]
fn no_prefix_diff_rejected() {
let diff = "diff --git src/lib.rs src/lib.rs\n\
index 1111111..2222222 100644\n\
--- src/lib.rs\n\
+++ src/lib.rs\n\
@@ -1 +1 @@\n\
-old\n\
+new\n";
let err = check_diff_paths(diff).unwrap_err().to_string();
assert!(
err.contains("diff --git") || err.contains("a/ or b/ prefix"),
"{err}"
);
}
#[test]
fn absolute_path_diff_rejected() {
let diff = "diff --git a/x b/x\n\
rename from x\n\
rename to /etc/passwd\n";
assert!(check_diff_paths(diff).is_err());
let plus = "--- a/x\n\
+++ /etc/passwd\n";
assert!(check_diff_paths(plus).is_err());
}
#[test]
fn parent_component_in_file_headers_rejected() {
assert!(check_diff_paths("--- a/../escape.rs\n+++ b/escape.rs\n").is_err());
assert!(check_diff_paths("--- a/ok.rs\n+++ b/nested/../../escape.rs\n").is_err());
assert!(check_diff_paths("diff --git a/ok.rs b/../escape.rs\n").is_err());
}
#[test]
fn dot_git_path_rejected() {
let diff = "diff --git a/.git/hooks/pre-commit b/.git/hooks/pre-commit\n\
--- a/.git/hooks/pre-commit\n\
+++ b/.git/hooks/pre-commit\n";
assert!(check_diff_paths(diff).is_err());
}
#[test]
fn quoted_paths_rejected() {
assert!(check_diff_paths("diff --git \"a/x\" \"b/y\"\n").is_err());
assert!(check_diff_paths("--- \"a/x\"\n+++ b/y\n").is_err());
assert!(check_diff_paths("rename to \"\\056\\056/evil\"\n").is_err());
}
#[test]
fn embedded_b_slash_cannot_hide_escape() {
// Whatever way ` b/` splits this header, the `..` lands in a fragment.
assert!(check_diff_paths("diff --git a/x b/y b/../evil\n").is_err());
assert!(check_diff_paths("diff --git a/../evil b/y b/z\n").is_err());
}
/// A removed body line that renders as `--- ` at column 0 — deleting an
/// unindented SQL/Lua `-- comment` — is still REJECTED (fail-closed is the
/// point; tracking hunk boundaries here would risk desyncing from git's own
/// parser). But the error must name that cause instead of leaving an agent
/// staring at a complaint about a/ and b/ prefixes it never wrote.
#[test]
fn body_line_colliding_with_header_prefix_explains_itself() {
let diff = "diff --git a/q.sql b/q.sql\n\
--- a/q.sql\n\
+++ b/q.sql\n\
@@ -1,2 +1 @@\n\
--- drop this comment\n\
\x20SELECT 1;\n";
let err = check_diff_paths(diff).unwrap_err().to_string();
assert!(err.contains("--- drop this comment"), "{err}");
assert!(err.contains("hunk body line"), "{err}");
assert!(err.contains("context"), "{err}");
}
/// The collision needs the SPACE. A removed `--x;` renders as `---x;` and an
/// added `++x;` as `+++x;` — neither is a `--- `/`+++ ` header, and both
/// must sail through. Guards against anyone "fixing" the note above by
/// widening the prefix match.
#[test]
fn body_line_without_the_space_does_not_collide() {
let diff = "diff --git a/x.c b/x.c\n\
--- a/x.c\n\
+++ b/x.c\n\
@@ -1,2 +1,2 @@\n\
---x;\n\
+++x;\n";
assert!(check_diff_paths(diff).is_ok());
}
#[test]
fn timestamp_suffix_tolerated() {
let diff = "--- a/src/lib.rs\t2026-07-13 00:00:00\n\
+++ b/src/lib.rs\t2026-07-13 00:00:00\n";
assert!(check_diff_paths(diff).is_ok());
}
}
+556
View File
@@ -0,0 +1,556 @@
//! The Unix daemon proper: stdio/socket serving, op dispatch, and the
//! detached verdict pipeline (setsid + Unix sockets keep this POSIX-only;
//! path vetting lives in the portable `allowlist` module).
use crate::allowlist;
use anyhow::{anyhow, ensure, Result};
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use std::io::{BufRead, Write};
use std::os::unix::net::UnixListener;
use std::path::Path;
use std::process::Command;
use std::sync::Mutex;
/// Serializes mutating ops (git apply, pending writes) across socket
/// connections; reads don't need it but the cost is nil at one agent.
static HANDLE_LOCK: Mutex<()> = Mutex::new(());
pub fn run() -> Result<()> {
let root = std::env::current_dir()?.canonicalize()?;
let args: Vec<String> = std::env::args().collect();
if let Some(i) = args.iter().position(|a| a == "--socket") {
let path = args
.get(i + 1)
.ok_or_else(|| anyhow!("--socket requires a path"))?;
return serve_socket(&root, Path::new(path));
}
let stdin = std::io::stdin();
let mut stdout = std::io::stdout().lock();
for line in stdin.lock().lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let resp = respond(&root, &line);
writeln!(stdout, "{resp}")?;
stdout.flush()?;
}
Ok(())
}
/// SPEC §10 socket mode: harnessd runs as the trusted uid and listens on a
/// Unix socket; the agent process runs as an unprivileged user whose only
/// write path into the repo is this daemon. Socket permissions (owner/group/
/// mode) are set by the supervisor script that starts us.
fn serve_socket(root: &Path, sock: &Path) -> Result<()> {
if let Some(dir) = sock.parent() {
std::fs::create_dir_all(dir)?;
}
let _ = std::fs::remove_file(sock);
let listener = UnixListener::bind(sock)?;
eprintln!("harnessd listening on {}", sock.display());
for conn in listener.incoming() {
let stream = match conn {
Ok(s) => s,
Err(e) => {
eprintln!("accept error: {e}");
continue;
}
};
let root = root.to_path_buf();
std::thread::spawn(move || {
let reader = match stream.try_clone() {
Ok(s) => std::io::BufReader::new(s),
Err(_) => return,
};
let mut writer = stream;
for line in reader.lines() {
let Ok(line) = line else { break };
if line.trim().is_empty() {
continue;
}
let resp = respond(&root, &line);
if writeln!(writer, "{resp}").is_err() {
break;
}
}
});
}
Ok(())
}
fn respond(root: &Path, line: &str) -> Value {
// read_verdict long-polls the ledger (read-only) and must not hold
// the lock that serializes mutating ops while it sleeps.
let is_read_verdict = serde_json::from_str::<Value>(line)
.ok()
.and_then(|v| {
v.get("op")
.and_then(Value::as_str)
.map(|o| o == "read_verdict")
})
.unwrap_or(false);
let _guard = if is_read_verdict {
None
} else {
Some(HANDLE_LOCK.lock().unwrap_or_else(|p| p.into_inner()))
};
match handle(root, line) {
Ok(v) => json!({"ok": true, "result": v}),
Err(e) => json!({"ok": false, "error": e.to_string()}),
}
}
/// Single-quote a string for safe embedding in a generated shell script.
fn shq(s: &str) -> String {
format!("'{}'", s.replace('\'', "'\\''"))
}
/// The exact probe `scripts/no-net.sh` runs before it decides whether to
/// isolate: can this host open an unprivileged network namespace? Run it here
/// too — same uid, same environment, same host as the wrapper we are about to
/// spawn — so the isolation note we record describes the run that actually
/// happened instead of the one we hoped for.
fn netns_available() -> bool {
Command::new("unshare")
.args(["--net", "--map-current-user", "true"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn field<'a>(req: &'a Value, key: &str) -> Result<&'a str> {
req.get(key)
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("missing string field: {key}"))
}
/// A target name must be a plain directory name — no separators, no dots.
fn check_target(name: &str) -> Result<()> {
ensure!(
!name.is_empty()
&& name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
"invalid target name"
);
Ok(())
}
const RUNNING_LOCK: &str = "results/pending/RUNNING.lock";
/// The detached verdict pipeline benches the workspace tree it was launched
/// against; HANDLE_LOCK cannot cover it, so a lock file does. Refuse any op
/// that would swap that tree while a pipeline is in flight.
fn ensure_no_running_pipeline(root: &Path) -> Result<()> {
let lock = root.join(RUNNING_LOCK);
if lock.exists() {
let rid = std::fs::read_to_string(&lock).unwrap_or_default();
let rid = rid.trim();
let rid = if rid.is_empty() { "unknown" } else { rid };
anyhow::bail!(
"verdict pipeline {rid} in flight ({RUNNING_LOCK} present — wait for \
read_verdict to resolve it, or remove the lock if the pipeline is dead)"
);
}
Ok(())
}
fn handle(root: &Path, line: &str) -> Result<Value> {
let req: Value = serde_json::from_str(line)?;
match field(&req, "op")? {
"read_profile" => {
let t = field(&req, "target")?;
check_target(t)?;
let text = std::fs::read_to_string(root.join(format!("results/{t}/hotspots.txt")))?;
Ok(json!({"hotspots_text": text}))
}
"read_ledger" => {
let t = field(&req, "target")?;
check_target(t)?;
// Read-only surface over the append-only DB.
let ledger = ledger::Ledger::open(&root.join("results/ledger.sqlite"))?;
let classes = ledger.attempted_classes(t)?;
let history = ledger.attempt_history(t)?;
Ok(json!({
"attempted_playbook_classes": classes,
"attempts": history,
"total_attempts": ledger.count()?,
"note": "a class may be re-entered with a materially NEW hypothesis; \
never duplicate a (hotspot, class, hypothesis) that has a verdict",
}))
}
"read_playbook" => {
let class = match req.get("class") {
Some(Value::Number(n)) => n.as_u64().ok_or_else(|| anyhow!("bad class"))? as u8,
Some(Value::String(s)) => s.parse()?,
_ => anyhow::bail!("missing class"),
};
ensure!((1..=7).contains(&class), "playbook class must be 1-7");
let dir = root.join("playbook");
let entry = std::fs::read_dir(&dir)?
.filter_map(|e| e.ok())
.find(|e| {
e.file_name()
.to_string_lossy()
.starts_with(&format!("{class:02}-"))
})
.ok_or_else(|| anyhow!("no playbook file for class {class}"))?;
Ok(json!({"markdown": std::fs::read_to_string(entry.path())?}))
}
"read_target_source" => {
let t = field(&req, "target")?;
let p = field(&req, "path")?;
check_target(t)?;
allowlist::check_rel_path(p)?;
// Serve the PRISTINE (HEAD) content, not the working tree:
// propose_patch applies each proposal against a reset
// workspace, so reads must show the same base the diff needs
// — otherwise reads after a proposal show the patched tree
// and the next diff's context lines drift (phase2-comrak-005
// burned turns rediscovering this).
let ws = root.join(format!("targets/{t}/workspace"));
let shown = Command::new("git")
.args(["-C", ws.to_str().unwrap(), "show", &format!("HEAD:{p}")])
.output()?;
ensure!(
shown.status.success(),
"cannot read {p} at HEAD: {}",
String::from_utf8_lossy(&shown.stderr).trim()
);
let text = String::from_utf8_lossy(&shown.stdout).into_owned();
// Optional line window so large source files (100KB+) can be read in
// chunks instead of overflowing one response — otherwise the agent
// is tempted to reach for a shell it must not have.
let lines: Vec<&str> = text.lines().collect();
let total = lines.len();
let offset = req.get("offset").and_then(Value::as_u64).unwrap_or(0) as usize;
let limit = req
.get("limit")
.and_then(Value::as_u64)
.map(|v| v as usize)
.unwrap_or(usize::MAX);
let end = offset.saturating_add(limit).min(total);
let start = offset.min(total);
let slice = lines[start..end]
.iter()
.enumerate()
.map(|(i, l)| format!("{}\t{}", start + i + 1, l))
.collect::<Vec<_>>()
.join("\n");
Ok(json!({
"content": slice,
"total_lines": total,
"returned_lines": [start + 1, end],
"truncated": end < total,
}))
}
"propose_patch" => {
let t = field(&req, "target")?;
let diff = field(&req, "diff")?;
let hypothesis = field(&req, "hypothesis")?;
check_target(t)?;
ensure_no_running_pipeline(root)?;
// git rejects a diff without a final newline with an opaque
// "corrupt patch" — say it plainly (phase2-comrak-004 burned
// several turns rediscovering this).
ensure!(
diff.ends_with('\n'),
"diff must end with a trailing newline (git would report 'corrupt patch' at EOF)"
);
// Allowlist: every path named by any diff header must be a safe
// relative path (the git -C below roots them in the target
// workspace; nothing outside it is reachable).
allowlist::check_diff_paths(diff)?;
let ws = root.join(format!("targets/{t}/workspace"));
// Every proposal stands alone: reset the workspace to HEAD
// first so probe/iteration patches cannot accumulate into a
// tested tree the ledger's patch field doesn't describe
// (phase2-comrak-004 audit finding).
ensure!(
Command::new("git")
.args(["-C", ws.to_str().unwrap(), "checkout", "--", "."])
.status()?
.success(),
"workspace reset failed"
);
ensure!(
Command::new("git")
.args(["-C", ws.to_str().unwrap(), "clean", "-fdq"])
.status()?
.success(),
"workspace clean failed"
);
let run_git_apply = |check: bool| -> Result<std::process::Output> {
let mut args = vec!["-C", ws.to_str().unwrap(), "apply"];
if check {
args.push("--check");
}
args.push("-");
let mut child = Command::new("git")
.args(&args)
.stdin(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
child.stdin.take().unwrap().write_all(diff.as_bytes())?;
Ok(child.wait_with_output()?)
};
let checked = run_git_apply(true)?;
ensure!(
checked.status.success(),
"diff does not apply cleanly against the pristine workspace \
(proposals are standalone; prior proposals were reset): {}",
String::from_utf8_lossy(&checked.stderr).trim()
);
let applied = run_git_apply(false)?;
ensure!(
applied.status.success(),
"git apply failed: {}",
String::from_utf8_lossy(&applied.stderr).trim()
);
let patch_id = format!("{:x}", Sha256::digest(diff.as_bytes()))[..12].to_string();
let pending = root.join("results/pending");
std::fs::create_dir_all(&pending)?;
std::fs::write(
pending.join(format!("{patch_id}.json")),
serde_json::to_string_pretty(
&json!({"target": t, "hypothesis": hypothesis, "diff": diff}),
)?,
)?;
Ok(json!({"patch_id": patch_id}))
}
"run_verdict" => {
let patch_id = field(&req, "patch_id")?;
ensure!(
patch_id.chars().all(|c| c.is_ascii_hexdigit()) && patch_id.len() == 12,
"bad patch id"
);
let run_id = field(&req, "run_id")?;
ensure!(
run_id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
"bad run id"
);
let class = field(&req, "playbook_class")?;
let hotspot = field(&req, "hotspot")?;
ensure_no_running_pipeline(root)?;
let pending: Value = serde_json::from_str(&std::fs::read_to_string(
root.join(format!("results/pending/{patch_id}.json")),
)?)?;
let t = pending["target"].as_str().unwrap();
let diff = pending["diff"].as_str().unwrap();
// Defense in depth: the pending file sat on disk since
// propose_patch vetted it — vet again before applying.
allowlist::check_diff_paths(diff)?;
let spec = diff_test::target::TargetSpec::load(root, t)?;
let diff_path = root.join(format!("results/pending/{patch_id}.json.diff"));
std::fs::write(&diff_path, diff)?;
// Re-establish the exact tree the pipeline must test: proposals
// after this one may have reset/repatched the workspace.
let ws = root.join(format!("targets/{t}/workspace"));
let git_ws = |args: &[&str]| -> Result<()> {
let out = Command::new("git").arg("-C").arg(&ws).args(args).output()?;
ensure!(
out.status.success(),
"git {} failed in target workspace: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr).trim()
);
Ok(())
};
git_ws(&["checkout", "--", "."])?;
git_ws(&["clean", "-fdq"])?;
let dp = diff_path.to_str().unwrap();
git_ws(&["apply", "--check", dp]).map_err(|e| {
anyhow!(
"pending diff {patch_id} no longer applies to the pristine \
workspace — re-propose it: {e}"
)
})?;
git_ws(&["apply", dp])?;
// The build + verdict pipeline runs for minutes — far past the MCP
// transport's per-call cap. Launch it detached, writing progress to
// a log; the agent observes the result via the read_verdict op,
// which reads the append-only ledger row once written.
// {out} isolates the candidate build (cargo: CARGO_TARGET_DIR;
// make: build+copy into it) — language-agnostic, mirrors the
// baseline rebuild in verdict.
let cand_dir = root
.join(format!("targets/{t}/candidate-{patch_id}"))
.to_string_lossy()
.into_owned();
let cand_build = diff_test::target::subst_out(&spec.build.baseline, &cand_dir);
let cand_bin = diff_test::target::subst_out(&spec.build.binary, &cand_dir);
let log = format!("results/pending/{run_id}.log");
// Target code must not reach the network: the pipeline launches
// through a wrapper (default scripts/no-net.sh, a network-namespace
// exec); HOTPATH_VERDICT_WRAPPER overrides with a single executable
// path for hosts where unshare is unavailable.
let (wrapper, isolation_note) = match std::env::var("HOTPATH_VERDICT_WRAPPER") {
Ok(w) => {
let note = w.clone();
(w, note)
}
Err(_) => {
let w = root
.join("scripts/no-net.sh")
.to_string_lossy()
.into_owned();
// Honesty: this note is echoed to the agent AND stamped into
// the ledger's env fingerprint. With HOTPATH_ALLOW_UNISOLATED=1
// set, no-net.sh execs the pipeline with FULL network access on
// any host where it cannot open a netns — recording a bare
// "no-net.sh" there would claim an isolation the run never had.
// Probe the same condition the wrapper will test, and say which
// of the two actually applies.
let allow_unisolated = std::env::var("HOTPATH_ALLOW_UNISOLATED")
.map(|v| v == "1")
.unwrap_or(false);
let note = if allow_unisolated && !netns_available() {
"no-net.sh (HOTPATH_ALLOW_UNISOLATED=1 — network NOT isolated)"
} else {
"no-net.sh"
};
(w, note.to_string())
}
};
// Two scripts, because failure accounting has to survive a wrapper
// that never execs its argv.
//
// INNER — runs inside the isolation wrapper. The guarded section is
// a SUBSHELL, not the left operand of an `&&`: POSIX sh ignores
// `set -e` inside any compound command that is an AND-OR operand
// other than the last, so the previous
// { set -e; <build>; verdict; } && rm -f lock || { ...; }
// shape left errexit inert. A failing candidate build fell straight
// through to `cargo run -p verdict`, which then benched — and wrote
// a ledger row for — a STALE or missing candidate binary. That is
// the exact class of false evidence this project exists to prevent.
// A standalone subshell scopes errexit and makes it live again, so a
// failed build aborts before verdict can measure anything. The
// subshell is the script's last command, so its status is the
// script's status.
let inner = format!(
"printf %s {rid} > {lock}\n\
( set -e\n{build}\ncargo run -q -p verdict -- {tgt} \
--rebuild-baseline --candidate-bin {cbin} --run-id {rid} --playbook-class {cls} \
--hypothesis {hyp} --hotspot {hs} --patch-file {pf} --isolation-note {iso}\n\
)\n",
rid = shq(run_id),
lock = shq(RUNNING_LOCK),
build = cand_build,
tgt = shq(t),
cbin = shq(&cand_bin),
cls = shq(class),
hyp = shq(pending["hypothesis"].as_str().unwrap()),
hs = shq(hotspot),
pf = shq(diff_path.to_str().unwrap()),
iso = shq(&isolation_note),
);
// OUTER — runs OUTSIDE the wrapper: invoke the wrapper, then account
// for its exit status no matter what it did. scripts/no-net.sh exits
// 97 WITHOUT exec'ing its argv when unprivileged netns are
// unavailable, and a missing or non-executable HOTPATH_VERDICT_WRAPPER
// exits 127/126 — in all three cases the inner script never runs at
// all. While this accounting lived inside the wrapper, those cases
// removed no lock and wrote no marker: read_verdict polled "running"
// forever and every later propose_patch/run_verdict was refused with
// "verdict pipeline in flight" until a human deleted the lock by hand.
// Out here it always runs, so the run is reported failed and the
// daemon stays usable. The wrapper's own stderr (no-net.sh's refusal
// text) is already captured in the run log.
let script = format!(
"{wrapper} sh -c {inner}\n\
rc=$?\n\
rm -f {lock}\n\
[ \"$rc\" -eq 0 ] || echo \"HOTPATH-PIPELINE-FAILED (exit $rc)\"\n",
wrapper = shq(&wrapper),
inner = shq(&inner),
lock = shq(RUNNING_LOCK),
);
let logf = std::fs::File::create(root.join(&log))?;
let errf = logf.try_clone()?;
// Write the lock before spawning: the detached script's own
// write lands only after exec latency, and a propose_patch in
// that window would swap the tree under the pipeline.
let lock_path = root.join(RUNNING_LOCK);
std::fs::write(&lock_path, run_id)?;
// The wrapper is invoked BY the outer script (and is shq'd into it),
// not by setsid — that is what puts the accounting outside it.
let spawned = Command::new("setsid")
.arg("sh")
.arg("-c")
.arg(&script)
.current_dir(root)
.stdout(std::process::Stdio::from(logf))
.stderr(std::process::Stdio::from(errf))
.spawn();
if let Err(e) = spawned {
let _ = std::fs::remove_file(&lock_path);
return Err(anyhow!("failed to launch verdict pipeline: {e}"));
}
Ok(json!({"status": "started", "run_id": run_id, "log": log,
"isolation": isolation_note,
"note": "pipeline runs detached; poll read_verdict with this run_id"}))
}
"read_verdict" => {
let run_id = field(&req, "run_id")?;
// Long-poll: the pipeline runs for minutes and the MCP transport
// caps a tool call at 60s. Waiting ~45s server-side per poll cuts
// the agent's turn burn ~15x versus instant "running" replies.
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(45);
loop {
let ledger = ledger::Ledger::open(&root.join("results/ledger.sqlite"))?;
if let Some(v) = ledger.verdict_summary(run_id)? {
return Ok(v);
}
drop(ledger);
// A pipeline that died before the verdict binary ran never
// writes a row; the launcher's failure accounting — which runs
// outside the isolation wrapper, so it survives a wrapper that
// refuses to exec — marks the log instead.
if run_id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
let log = root.join(format!("results/pending/{run_id}.log"));
if let Ok(text) = std::fs::read_to_string(&log) {
if text.contains("HOTPATH-PIPELINE-FAILED") {
let tail: String = text
.lines()
.rev()
.take(12)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<Vec<_>>()
.join("\n");
return Ok(json!({
"status": "failed",
"note": "the pipeline died before producing a verdict — usually a \
candidate build failure, otherwise a crash or the isolation \
wrapper refusing to run (scripts/no-net.sh exits 97 when it \
cannot open a network namespace). The marker line carries the \
exit status. No ledger row was or will be written for this \
run_id, and the run lock has been released. Log tail follows.",
"log_tail": tail,
}));
}
}
}
if std::time::Instant::now() >= deadline {
return Ok(json!({"status": "running",
"note": "no ledger row yet; the pipeline is still building/benching — poll again"}));
}
std::thread::sleep(std::time::Duration::from_secs(3));
}
}
other => Err(anyhow!(
"unknown op: {other} (seven ops, SPEC §3.5 + async read_verdict)"
)),
}
}
+22 -431
View File
@@ -1,438 +1,29 @@
//! harnessd — the only door between the agent and the trust layer //! harnessd — the only door between the agent and the trust layer
//! (SPEC §3.5, §10). One JSON request per stdin line, one JSON response //! (SPEC §3.5, §10). One JSON request per stdin line, one JSON response
//! per stdout line. The agent gets exactly six operations; everything //! per stdout line. The agent gets exactly seven operations (the six
//! else (shell, trust-layer writes, patches outside the target //! synchronous ops plus the async read_verdict poll); everything else
//! workspace) simply has no code path here. Phase 1 caveat, recorded: //! (shell, trust-layer writes, patches outside the target workspace)
//! same-uid filesystem read-only enforcement still requires the //! simply has no code path here. Phase 1 caveat, recorded: same-uid
//! separate-user/container setup (Phase 1 infra gap). //! filesystem read-only enforcement still requires the separate-user/
//! container setup (Phase 1 infra gap).
//!
//! The daemon itself is POSIX-only (Unix sockets, setsid); the diff
//! allowlist is a portable module so its escape-regression tests run on
//! every development platform.
use anyhow::{anyhow, ensure, Result}; // On non-Unix only the allowlist tests compile against this module.
use serde_json::{json, Value}; #[cfg_attr(not(unix), allow(dead_code))]
use sha2::{Digest, Sha256}; mod allowlist;
use std::io::{BufRead, Write}; #[cfg(unix)]
use std::os::unix::net::UnixListener; mod daemon;
use std::path::{Component, Path};
use std::process::Command;
use std::sync::Mutex;
/// Serializes mutating ops (git apply, pending writes) across socket #[cfg(unix)]
/// connections; reads don't need it but the cost is nil at one agent. fn main() -> anyhow::Result<()> {
static HANDLE_LOCK: Mutex<()> = Mutex::new(()); daemon::run()
fn main() -> Result<()> {
let root = std::env::current_dir()?.canonicalize()?;
let args: Vec<String> = std::env::args().collect();
if let Some(i) = args.iter().position(|a| a == "--socket") {
let path = args
.get(i + 1)
.ok_or_else(|| anyhow!("--socket requires a path"))?;
return serve_socket(&root, Path::new(path));
}
let stdin = std::io::stdin();
let mut stdout = std::io::stdout().lock();
for line in stdin.lock().lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let resp = respond(&root, &line);
writeln!(stdout, "{resp}")?;
stdout.flush()?;
}
Ok(())
} }
/// SPEC §10 socket mode: harnessd runs as the trusted uid and listens on a #[cfg(not(unix))]
/// Unix socket; the agent process runs as an unprivileged user whose only fn main() {
/// write path into the repo is this daemon. Socket permissions (owner/group/ eprintln!("harnessd requires a Unix host (Unix sockets, setsid)");
/// mode) are set by the supervisor script that starts us. std::process::exit(1);
fn serve_socket(root: &Path, sock: &Path) -> Result<()> {
if let Some(dir) = sock.parent() {
std::fs::create_dir_all(dir)?;
}
let _ = std::fs::remove_file(sock);
let listener = UnixListener::bind(sock)?;
eprintln!("harnessd listening on {}", sock.display());
for conn in listener.incoming() {
let stream = match conn {
Ok(s) => s,
Err(e) => {
eprintln!("accept error: {e}");
continue;
}
};
let root = root.to_path_buf();
std::thread::spawn(move || {
let reader = match stream.try_clone() {
Ok(s) => std::io::BufReader::new(s),
Err(_) => return,
};
let mut writer = stream;
for line in reader.lines() {
let Ok(line) = line else { break };
if line.trim().is_empty() {
continue;
}
let resp = respond(&root, &line);
if writeln!(writer, "{resp}").is_err() {
break;
}
}
});
}
Ok(())
}
fn respond(root: &Path, line: &str) -> Value {
// read_verdict long-polls the ledger (read-only) and must not hold
// the lock that serializes mutating ops while it sleeps.
let is_read_verdict = serde_json::from_str::<Value>(line)
.ok()
.and_then(|v| {
v.get("op")
.and_then(Value::as_str)
.map(|o| o == "read_verdict")
})
.unwrap_or(false);
let _guard = if is_read_verdict {
None
} else {
Some(HANDLE_LOCK.lock().unwrap_or_else(|p| p.into_inner()))
};
match handle(root, line) {
Ok(v) => json!({"ok": true, "result": v}),
Err(e) => json!({"ok": false, "error": e.to_string()}),
}
}
/// Single-quote a string for safe embedding in a generated shell script.
fn shq(s: &str) -> String {
format!("'{}'", s.replace('\'', "'\\''"))
}
fn field<'a>(req: &'a Value, key: &str) -> Result<&'a str> {
req.get(key)
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("missing string field: {key}"))
}
/// A target name must be a plain directory name — no separators, no dots.
fn check_target(name: &str) -> Result<()> {
ensure!(
!name.is_empty()
&& name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
"invalid target name"
);
Ok(())
}
/// Relative paths only; no parent components, no .git, no absolutes.
fn check_rel_path(p: &str) -> Result<()> {
let path = Path::new(p);
ensure!(!path.is_absolute(), "absolute paths forbidden");
for c in path.components() {
match c {
Component::Normal(seg) => ensure!(seg != ".git", ".git access forbidden"),
Component::CurDir => {}
_ => anyhow::bail!("path escapes workspace: {p}"),
}
}
Ok(())
}
fn handle(root: &Path, line: &str) -> Result<Value> {
let req: Value = serde_json::from_str(line)?;
match field(&req, "op")? {
"read_profile" => {
let t = field(&req, "target")?;
check_target(t)?;
let text = std::fs::read_to_string(root.join(format!("results/{t}/hotspots.txt")))?;
Ok(json!({"hotspots_text": text}))
}
"read_ledger" => {
let t = field(&req, "target")?;
check_target(t)?;
// Read-only surface over the append-only DB.
let ledger = ledger::Ledger::open(&root.join("results/ledger.sqlite"))?;
let classes = ledger.attempted_classes(t)?;
let history = ledger.attempt_history(t)?;
Ok(json!({
"attempted_playbook_classes": classes,
"attempts": history,
"total_attempts": ledger.count()?,
"note": "a class may be re-entered with a materially NEW hypothesis; \
never duplicate a (hotspot, class, hypothesis) that has a verdict",
}))
}
"read_playbook" => {
let class = match req.get("class") {
Some(Value::Number(n)) => n.as_u64().ok_or_else(|| anyhow!("bad class"))? as u8,
Some(Value::String(s)) => s.parse()?,
_ => anyhow::bail!("missing class"),
};
ensure!((1..=7).contains(&class), "playbook class must be 1-7");
let dir = root.join("playbook");
let entry = std::fs::read_dir(&dir)?
.filter_map(|e| e.ok())
.find(|e| {
e.file_name()
.to_string_lossy()
.starts_with(&format!("{class:02}-"))
})
.ok_or_else(|| anyhow!("no playbook file for class {class}"))?;
Ok(json!({"markdown": std::fs::read_to_string(entry.path())?}))
}
"read_target_source" => {
let t = field(&req, "target")?;
let p = field(&req, "path")?;
check_target(t)?;
check_rel_path(p)?;
// Serve the PRISTINE (HEAD) content, not the working tree:
// propose_patch applies each proposal against a reset
// workspace, so reads must show the same base the diff needs
// — otherwise reads after a proposal show the patched tree
// and the next diff's context lines drift (phase2-comrak-005
// burned turns rediscovering this).
let ws = root.join(format!("targets/{t}/workspace"));
let shown = Command::new("git")
.args(["-C", ws.to_str().unwrap(), "show", &format!("HEAD:{p}")])
.output()?;
ensure!(
shown.status.success(),
"cannot read {p} at HEAD: {}",
String::from_utf8_lossy(&shown.stderr).trim()
);
let text = String::from_utf8_lossy(&shown.stdout).into_owned();
// Optional line window so large source files (100KB+) can be read in
// chunks instead of overflowing one response — otherwise the agent
// is tempted to reach for a shell it must not have.
let lines: Vec<&str> = text.lines().collect();
let total = lines.len();
let offset = req.get("offset").and_then(Value::as_u64).unwrap_or(0) as usize;
let limit = req
.get("limit")
.and_then(Value::as_u64)
.map(|v| v as usize)
.unwrap_or(usize::MAX);
let end = offset.saturating_add(limit).min(total);
let start = offset.min(total);
let slice = lines[start..end]
.iter()
.enumerate()
.map(|(i, l)| format!("{}\t{}", start + i + 1, l))
.collect::<Vec<_>>()
.join("\n");
Ok(json!({
"content": slice,
"total_lines": total,
"returned_lines": [start + 1, end],
"truncated": end < total,
}))
}
"propose_patch" => {
let t = field(&req, "target")?;
let diff = field(&req, "diff")?;
let hypothesis = field(&req, "hypothesis")?;
check_target(t)?;
// git rejects a diff without a final newline with an opaque
// "corrupt patch" — say it plainly (phase2-comrak-004 burned
// several turns rediscovering this).
ensure!(
diff.ends_with('\n'),
"diff must end with a trailing newline (git would report 'corrupt patch' at EOF)"
);
// Allowlist: every path named by the diff must be a safe
// relative path (the git -C below roots them in the target
// workspace; nothing outside it is reachable). `/dev/null`
// is the one absolute path git legitimately emits, for
// new/deleted files.
for l in diff.lines() {
if let Some(p) = l
.strip_prefix("--- a/")
.or_else(|| l.strip_prefix("+++ b/"))
{
check_rel_path(p.trim())?;
}
ensure!(
(!l.starts_with("--- /") && !l.starts_with("+++ /"))
|| l == "--- /dev/null"
|| l == "+++ /dev/null",
"absolute diff paths forbidden (only /dev/null is allowed, for new/deleted files)"
);
}
let ws = root.join(format!("targets/{t}/workspace"));
// Every proposal stands alone: reset the workspace to HEAD
// first so probe/iteration patches cannot accumulate into a
// tested tree the ledger's patch field doesn't describe
// (phase2-comrak-004 audit finding).
ensure!(
Command::new("git")
.args(["-C", ws.to_str().unwrap(), "checkout", "--", "."])
.status()?
.success(),
"workspace reset failed"
);
ensure!(
Command::new("git")
.args(["-C", ws.to_str().unwrap(), "clean", "-fdq"])
.status()?
.success(),
"workspace clean failed"
);
let run_git_apply = |check: bool| -> Result<std::process::Output> {
let mut args = vec!["-C", ws.to_str().unwrap(), "apply"];
if check {
args.push("--check");
}
args.push("-");
let mut child = Command::new("git")
.args(&args)
.stdin(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
child.stdin.take().unwrap().write_all(diff.as_bytes())?;
Ok(child.wait_with_output()?)
};
let checked = run_git_apply(true)?;
ensure!(
checked.status.success(),
"diff does not apply cleanly against the pristine workspace \
(proposals are standalone; prior proposals were reset): {}",
String::from_utf8_lossy(&checked.stderr).trim()
);
let applied = run_git_apply(false)?;
ensure!(
applied.status.success(),
"git apply failed: {}",
String::from_utf8_lossy(&applied.stderr).trim()
);
let patch_id = format!("{:x}", Sha256::digest(diff.as_bytes()))[..12].to_string();
let pending = root.join("results/pending");
std::fs::create_dir_all(&pending)?;
std::fs::write(
pending.join(format!("{patch_id}.json")),
serde_json::to_string_pretty(
&json!({"target": t, "hypothesis": hypothesis, "diff": diff}),
)?,
)?;
Ok(json!({"patch_id": patch_id}))
}
"run_verdict" => {
let patch_id = field(&req, "patch_id")?;
ensure!(
patch_id.chars().all(|c| c.is_ascii_hexdigit()) && patch_id.len() == 12,
"bad patch id"
);
let run_id = field(&req, "run_id")?;
ensure!(
run_id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
"bad run id"
);
let class = field(&req, "playbook_class")?;
let hotspot = field(&req, "hotspot")?;
let pending: Value = serde_json::from_str(&std::fs::read_to_string(
root.join(format!("results/pending/{patch_id}.json")),
)?)?;
let t = pending["target"].as_str().unwrap();
let spec = diff_test::target::TargetSpec::load(root, t)?;
let diff_path = root.join(format!("results/pending/{patch_id}.json.diff"));
std::fs::write(&diff_path, pending["diff"].as_str().unwrap())?;
// The build + verdict pipeline runs for minutes — far past the MCP
// transport's per-call cap. Launch it detached, writing progress to
// a log; the agent observes the result via the read_verdict op,
// which reads the append-only ledger row once written.
// {out} isolates the candidate build (cargo: CARGO_TARGET_DIR;
// make: build+copy into it) — language-agnostic, mirrors the
// baseline rebuild in verdict.
let cand_dir = root
.join(format!("targets/{t}/candidate-{patch_id}"))
.to_string_lossy()
.into_owned();
let cand_build = diff_test::target::subst_out(&spec.build.baseline, &cand_dir);
let cand_bin = diff_test::target::subst_out(&spec.build.binary, &cand_dir);
let log = format!("results/pending/{run_id}.log");
// On any failure (candidate build, gates crash) the trap line
// marks the log so read_verdict can report "failed" instead of
// leaving the agent polling a verdict that will never exist.
let script = format!(
"{{ set -e\n{build}\ncargo run -q -p verdict -- {tgt} \
--rebuild-baseline --candidate-bin {cbin} --run-id {rid} --playbook-class {cls} \
--hypothesis {hyp} --hotspot {hs} --patch-file {pf}\n}} || echo HOTPATH-PIPELINE-FAILED\n",
build = cand_build,
tgt = shq(t),
cbin = shq(&cand_bin),
rid = shq(run_id),
cls = shq(class),
hyp = shq(pending["hypothesis"].as_str().unwrap()),
hs = shq(hotspot),
pf = shq(diff_path.to_str().unwrap()),
);
let logf = std::fs::File::create(root.join(&log))?;
let errf = logf.try_clone()?;
Command::new("setsid")
.arg("sh")
.arg("-c")
.arg(&script)
.current_dir(root)
.stdout(std::process::Stdio::from(logf))
.stderr(std::process::Stdio::from(errf))
.spawn()?;
Ok(json!({"status": "started", "run_id": run_id, "log": log,
"note": "pipeline runs detached; poll read_verdict with this run_id"}))
}
"read_verdict" => {
let run_id = field(&req, "run_id")?;
// Long-poll: the pipeline runs for minutes and the MCP transport
// caps a tool call at 60s. Waiting ~45s server-side per poll cuts
// the agent's turn burn ~15x versus instant "running" replies.
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(45);
loop {
let ledger = ledger::Ledger::open(&root.join("results/ledger.sqlite"))?;
if let Some(v) = ledger.verdict_summary(run_id)? {
return Ok(v);
}
drop(ledger);
// A pipeline that died before the verdict binary ran never
// writes a row; its trap line marks the log instead.
if run_id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
let log = root.join(format!("results/pending/{run_id}.log"));
if let Ok(text) = std::fs::read_to_string(&log) {
if text.contains("HOTPATH-PIPELINE-FAILED") {
let tail: String = text
.lines()
.rev()
.take(12)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<Vec<_>>()
.join("\n");
return Ok(json!({
"status": "failed",
"note": "the pipeline died before producing a verdict (usually a \
candidate build failure); no ledger row was or will be \
written for this run_id. Failure log tail follows.",
"log_tail": tail,
}));
}
}
}
if std::time::Instant::now() >= deadline {
return Ok(json!({"status": "running",
"note": "no ledger row yet; the pipeline is still building/benching — poll again"}));
}
std::thread::sleep(std::time::Duration::from_secs(3));
}
}
other => Err(anyhow!(
"unknown op: {other} (seven ops, SPEC §3.5 + async read_verdict)"
)),
}
} }
+49 -1
View File
@@ -36,7 +36,10 @@ impl Verdict {
} }
} }
/// Gate outcomes for one attempt (SPEC §3.2). /// Gate outcomes for one attempt (SPEC §3.2). The trailing fields are
/// defaulted so historical rows (written before they existed) keep
/// deserializing; they live only in the `gates` JSON column — no schema
/// change, append-only doctrine untouched.
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GateResults { pub struct GateResults {
pub upstream_tests: bool, pub upstream_tests: bool,
@@ -44,6 +47,15 @@ pub struct GateResults {
pub fuzz_iters: u64, pub fuzz_iters: u64,
pub fuzz_divergence: bool, pub fuzz_divergence: bool,
pub sanitizers_clean: bool, pub sanitizers_clean: bool,
/// None = no TSan lane configured/run for this attempt.
#[serde(default)]
pub tsan_clean: Option<bool>,
/// Lexical NHR-classifier hits (crates/verdict src/risk.rs).
#[serde(default)]
pub risk_signals: Vec<String>,
/// "byte-identical" | "fp-tolerance"; None for historical rows.
#[serde(default)]
pub equivalence_mode: Option<String>,
} }
/// Benchmark evidence for one attempt. All times in seconds; CIs are /// Benchmark evidence for one attempt. All times in seconds; CIs are
@@ -334,6 +346,42 @@ mod tests {
assert_eq!(ledger.attempted_classes("example").unwrap(), vec![2]); assert_eq!(ledger.attempted_classes("example").unwrap(), vec![2]);
} }
#[test]
fn historical_gates_json_still_deserializes() {
// Exact shape of the 34 pre-existing rows' gates column: no
// tsan_clean / risk_signals / equivalence_mode keys.
let old = r#"{"upstream_tests":true,"golden_replay":true,"fuzz_iters":4332,
"fuzz_divergence":false,"sanitizers_clean":true}"#;
let g: GateResults = serde_json::from_str(old).unwrap();
assert!(g.upstream_tests && g.golden_replay && g.sanitizers_clean);
assert_eq!(g.fuzz_iters, 4332);
assert_eq!(g.tsan_clean, None);
assert!(g.risk_signals.is_empty());
assert_eq!(g.equivalence_mode, None);
}
#[test]
fn extended_gates_round_trip() {
let g = GateResults {
upstream_tests: true,
golden_replay: true,
fuzz_iters: 10_000,
fuzz_divergence: false,
sanitizers_clean: true,
tsan_clean: Some(false),
risk_signals: vec!["concurrency".into()],
equivalence_mode: Some("fp-tolerance".into()),
};
let json = serde_json::to_string(&g).unwrap();
let back: GateResults = serde_json::from_str(&json).unwrap();
assert_eq!(back.tsan_clean, Some(false));
assert_eq!(back.risk_signals, vec!["concurrency".to_string()]);
assert_eq!(back.equivalence_mode.as_deref(), Some("fp-tolerance"));
// The keys the defensive Value readers depend on must survive.
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(v.get("sanitizers_clean"), Some(&serde_json::json!(true)));
}
#[test] #[test]
fn append_only_is_enforced() { fn append_only_is_enforced() {
let ledger = Ledger::open_in_memory().unwrap(); let ledger = Ledger::open_in_memory().unwrap();
+279 -62
View File
@@ -1,15 +1,25 @@
//! `just verdict`: the whole attempt pipeline in one command. //! `just verdict`: the whole attempt pipeline in one command.
//! Gates on the candidate → interleaved A/B bench vs the baseline → //! Gates on the candidate → interleaved A/B bench vs the baseline →
//! verdict by the CI-lower-bound rule → append-only ledger row. //! verdict by the CI-lower-bound rule → append-only ledger row.
//! Any gate failure is rejected-gate and the bench is skipped; the //! Any gate failure is rejected-gate and the bench is skipped. A verdict
//! --needs-human-review flag caps the verdict for FP/concurrency/ //! is only ever capped, never upgraded: the lexical risk classifier
//! sanitizer-flagged attempts (SPEC §8) — it never upgrades one. //! (src/risk.rs) and fp-tolerance equivalence route any would-be accept
//! to needs-human-review, and the --needs-human-review flag forces the
//! same cap by hand (SPEC §8).
//!
//! This is also the only flow that can run differential fuzz, because it is
//! the only one holding both sides of the comparison: it rebuilds a pristine
//! baseline and passes it to the gates alongside the candidate. Hard rule —
//! a machine `accepted` requires that fuzz gate to have actually Passed; a
//! skipped or failed one caps the verdict at needs-human-review.
mod risk;
use anyhow::Result; use anyhow::Result;
use bench_runner::{config::AcceptConfig, decide, exec, fingerprint::EnvFingerprint, stats}; use bench_runner::{config::AcceptConfig, decide, exec, fingerprint::EnvFingerprint, stats};
use clap::Parser; use clap::Parser;
use diff_test::{ use diff_test::{
all_passed, orchestrate::run_core_gates, target::TargetSpec, GateLayer, GateOutcome, orchestrate::run_core_gates, target::TargetSpec, GateInputs, GateLayer, GateOutcome,
}; };
use ledger::{Attempt, BenchEvidence, GateResults, Ledger, Verdict}; use ledger::{Attempt, BenchEvidence, GateResults, Ledger, Verdict};
use std::path::PathBuf; use std::path::PathBuf;
@@ -48,6 +58,10 @@ struct Cli {
/// Force the needs-human-review verdict tier (FP/concurrency/UB-adjacent). /// Force the needs-human-review verdict tier (FP/concurrency/UB-adjacent).
#[arg(long)] #[arg(long)]
needs_human_review: bool, needs_human_review: bool,
/// How this run was isolated, recorded verbatim in the env
/// fingerprint (e.g. "no-net.sh"); absent = "unwrapped-host".
#[arg(long)]
isolation_note: Option<String>,
#[arg(long, default_value = "results/ledger.sqlite")] #[arg(long, default_value = "results/ledger.sqlite")]
db: PathBuf, db: PathBuf,
#[arg(long, default_value = "config/accept.toml")] #[arg(long, default_value = "config/accept.toml")]
@@ -107,6 +121,78 @@ fn rebuild_pristine_baseline(
Ok(baseline) Ok(baseline)
} }
/// A run is flagged on nonzero exit OR any of these stderr markers.
/// halt_on_error makes sanitizer hits exit nonzero; the markers catch
/// builds whose runtime still exits 0 (and never a bare "ERROR:" from
/// the target's own logging, the old false-flag risk).
const SANITIZER_MARKERS: &[&str] = &[
"ERROR: AddressSanitizer",
"ERROR: LeakSanitizer",
"ERROR: ThreadSanitizer",
"WARNING: ThreadSanitizer",
"runtime error:",
];
/// One instrumented build+run lane: templates, isolation subdir, and the
/// halt-on-error runtime options for its sanitizer family.
struct SanLane<'a> {
label: &'a str,
out_subdir: &'a str,
build_tpl: &'a str,
bin_tpl: &'a str,
envs: &'a [(&'a str, &'a str)],
}
/// Build one instrumented lane and run the pinned bench workload under
/// it. Returns Some(clean), or None when the instrumented binary cannot
/// be built — an infrastructure failure, not a code defect: the caller
/// caps the accept at needs-human-review instead of crashing a verdict
/// that already carries a measured bench.
fn run_san_lane(
root: &std::path::Path,
target: &str,
spec: &TargetSpec,
lane: &SanLane,
) -> Result<Option<bool>> {
let label = lane.label;
let out_dir = root
.join(format!("targets/{target}/{}", lane.out_subdir))
.to_string_lossy()
.into_owned();
println!("{label}: instrumented build of the patched tree…");
let status = std::process::Command::new("sh")
.arg("-c")
.arg(diff_test::target::subst_out(lane.build_tpl, &out_dir))
.current_dir(root)
.status()?;
if !status.success() {
println!("{label}: BUILD FAILED — cannot verify; capping accept at needs-human-review");
return Ok(None);
}
let bin = diff_test::target::subst_out(lane.bin_tpl, &out_dir);
let mut run = std::process::Command::new("sh");
run.arg("-c")
.arg(spec.bench.command.replace("{binary}", &bin))
.current_dir(root);
for (k, v) in lane.envs {
run.env(k, v);
}
let out = run.output()?;
let stderr = String::from_utf8_lossy(&out.stderr);
let clean = out.status.success() && !SANITIZER_MARKERS.iter().any(|m| stderr.contains(m));
if clean {
println!("{label}: clean");
} else {
let lines: Vec<&str> = stderr.lines().filter(|l| !l.is_empty()).collect();
let start = lines.len().saturating_sub(8);
println!("{label}: FLAGGED (rc={:?})", out.status.code());
for l in &lines[start..] {
println!(" {l}");
}
}
Ok(Some(clean))
}
/// SPEC §8: an accept is final only if ASan+LSan are clean on the patched /// SPEC §8: an accept is final only if ASan+LSan are clean on the patched
/// tree over the pinned workload. Stop-the-line fix after phase2-comrak-010: /// tree over the pinned workload. Stop-the-line fix after phase2-comrak-010:
/// the pipeline auto-accepted an LSan-flagged teardown patch (sanitizers /// the pipeline auto-accepted an LSan-flagged teardown patch (sanitizers
@@ -126,50 +212,68 @@ fn sanitizer_check(
println!("sanitizers: target declares no sanitizer build — cannot verify an accept"); println!("sanitizers: target declares no sanitizer build — cannot verify an accept");
return Ok(None); return Ok(None);
}; };
let out_dir = root run_san_lane(
.join(format!("targets/{target}/asan")) root,
.to_string_lossy() target,
.into_owned(); spec,
println!("sanitizers: ASan build of the patched tree…"); &SanLane {
let status = std::process::Command::new("sh") label: "sanitizers",
.arg("-c") out_subdir: "asan",
.arg(diff_test::target::subst_out(build_tpl, &out_dir)) build_tpl,
.current_dir(root) bin_tpl,
.status()?; envs: &[
if !status.success() { ("ASAN_OPTIONS", "halt_on_error=1:abort_on_error=1"),
// Infrastructure failure (missing runtime, toolchain), not a code ("UBSAN_OPTIONS", "halt_on_error=1:print_stacktrace=1"),
// defect: don't crash the verdict and lose a measured bench — ],
// treat "cannot build the sanitizer binary" like "no sanitizer },
// declared" so the accept caps at needs-human-review (None), with )
// the failure logged for the operator to fix and re-run. }
println!("sanitizers: BUILD FAILED — cannot verify; capping accept at needs-human-review");
/// TSan lane (SPEC §8), run on the accept path when the target declares
/// a TSan build — call only when spec.build.tsan is Some. Returns None
/// when the lane cannot run (missing tsan_binary, build failure); the
/// caller caps the accept, mirroring the ASan posture.
fn tsan_check(root: &std::path::Path, target: &str, spec: &TargetSpec) -> Result<Option<bool>> {
let (Some(build_tpl), Some(bin_tpl)) = (&spec.build.tsan, &spec.build.tsan_binary) else {
println!("tsan: build.tsan declared without build.tsan_binary — cannot verify");
return Ok(None); return Ok(None);
};
run_san_lane(
root,
target,
spec,
&SanLane {
label: "tsan",
out_subdir: "tsan",
build_tpl,
bin_tpl,
envs: &[("TSAN_OPTIONS", "halt_on_error=1")],
},
)
} }
let asan_bin = diff_test::target::subst_out(bin_tpl, &out_dir);
let cmd = spec.bench.command.replace("{binary}", &asan_bin); /// One word for the state of the DifferentialFuzz gate, for the cap message.
let out = std::process::Command::new("sh") fn fuzz_state(outcome: Option<&GateOutcome>) -> &'static str {
.arg("-c") match outcome {
.arg(&cmd) Some(GateOutcome::Passed) => "passed",
// detect_leaks is a no-op for the C/C++ UBSan combo but harmless. Some(GateOutcome::Failed { .. }) => "failed",
.env("ASAN_OPTIONS", "detect_leaks=1") Some(GateOutcome::Skipped { .. }) => "skipped",
.env("UBSAN_OPTIONS", "halt_on_error=1:print_stacktrace=1") None => "not run",
.current_dir(root) }
.output()?; }
let stderr = String::from_utf8_lossy(&out.stderr);
let clean = /// Median of per-run max-RSS samples (KiB); an even count averages the
out.status.success() && !stderr.contains("ERROR:") && !stderr.contains("runtime error:"); /// two middle values.
if clean { fn median_u64(xs: &[u64]) -> u64 {
println!("sanitizers: clean"); let mut v = xs.to_vec();
v.sort_unstable();
let n = v.len();
if n % 2 == 1 {
v[n / 2]
} else { } else {
let lines: Vec<&str> = stderr.lines().filter(|l| !l.is_empty()).collect(); (v[n / 2 - 1] + v[n / 2]) / 2
let start = lines.len().saturating_sub(8);
println!("sanitizers: FLAGGED (rc={:?})", out.status.code());
for l in &lines[start..] {
println!(" {l}");
} }
} }
Ok(Some(clean))
}
fn now_utc() -> String { fn now_utc() -> String {
let out = std::process::Command::new("date") let out = std::process::Command::new("date")
@@ -197,23 +301,58 @@ fn main() -> Result<()> {
})? })?
}; };
// Gates run on the candidate binary. let patch = match &cli.patch_file {
let gates = run_core_gates(&root, &spec, &cli.candidate_bin)?; Some(p) => std::fs::read_to_string(p)?,
for (layer, outcome) in &gates { None => "(no source patch)".into(),
};
// Gates run on the candidate binary — except differential fuzz, which
// differs it against the pristine baseline built just above. This is the
// only flow that has both sides, and therefore the only flow that can
// run the fuzz gate at all (hence the accept rule enforced below).
let report = run_core_gates(
&root,
&spec,
&GateInputs {
candidate_binary: &cli.candidate_bin,
baseline_binary: Some(baseline_bin.as_str()),
fuzz_iters: cfg.fuzz_iters,
},
)?;
for (layer, outcome) in &report.gates {
match outcome { match outcome {
GateOutcome::Passed => println!("{layer:?}: PASS"), GateOutcome::Passed => println!("{layer:?}: PASS"),
GateOutcome::Failed { detail } => println!("{layer:?}: FAIL — {detail}"), GateOutcome::Failed { detail } => println!("{layer:?}: FAIL — {detail}"),
GateOutcome::Skipped { reason } => println!("{layer:?}: skipped — {reason}"), GateOutcome::Skipped { reason } => println!("{layer:?}: skipped — {reason}"),
} }
} }
let passed = |layer| matches!(report.outcome(layer), Some(GateOutcome::Passed));
let mut gate_results = GateResults { let mut gate_results = GateResults {
upstream_tests: matches!(gates[0].1, GateOutcome::Passed), upstream_tests: passed(GateLayer::UpstreamTests),
golden_replay: matches!(gates[1].1, GateOutcome::Passed), golden_replay: passed(GateLayer::GoldenReplay),
fuzz_iters: 0, fuzz_iters: report.fuzz_iters,
fuzz_divergence: false, fuzz_divergence: report.fuzz_divergence,
sanitizers_clean: false, sanitizers_clean: false,
tsan_clean: None,
risk_signals: risk::classify(&patch, report.equivalence_mode == "fp-tolerance"),
equivalence_mode: Some(report.equivalence_mode.clone()),
}; };
debug_assert!(matches!(gates[0].0, GateLayer::UpstreamTests)); if !gate_results.risk_signals.is_empty() {
println!(
"risk: lexical signals [{}] — a would-be accept routes to needs-human-review (SPEC §8)",
gate_results.risk_signals.join(", ")
);
}
// Announced up front, like the risk signals, so the rule is in the
// transcript even when another cap fires first below.
let fuzz_passed = passed(GateLayer::DifferentialFuzz);
if !fuzz_passed {
println!(
"fuzz: differential-fuzz gate {} — a would-be accept routes to needs-human-review; \
no machine accept without a real differential-fuzz run (SPEC §8)",
fuzz_state(report.outcome(GateLayer::DifferentialFuzz))
);
}
let pin = &cfg.pin_prefix; let pin = &cfg.pin_prefix;
let wrap = |bin: &str| { let wrap = |bin: &str| {
@@ -225,7 +364,7 @@ fn main() -> Result<()> {
} }
}; };
let (verdict, bench) = if !all_passed(&gates) { let (verdict, bench) = if !report.all_passed() {
println!("verdict: rejected-gate (bench skipped)"); println!("verdict: rejected-gate (bench skipped)");
(Verdict::RejectedGate, None) (Verdict::RejectedGate, None)
} else { } else {
@@ -285,8 +424,62 @@ fn main() -> Result<()> {
v = Verdict::NeedsHumanReview; v = Verdict::NeedsHumanReview;
} }
} }
if spec.build.tsan.is_some() {
match tsan_check(&root, &cli.target, &spec)? {
Some(clean) => {
gate_results.tsan_clean = Some(clean);
if !clean {
println!(
"verdict: TSan flag caps the accept at needs-human-review (SPEC §8)"
);
v = Verdict::NeedsHumanReview;
}
}
None => {
println!("verdict: TSan lane declared but not verifiable; capping accept at needs-human-review (SPEC §8)");
v = Verdict::NeedsHumanReview;
}
}
}
if v == Verdict::Accepted && !gate_results.risk_signals.is_empty() {
println!(
"verdict: risk signals [{}] route the accept to needs-human-review (SPEC §8)",
gate_results.risk_signals.join(", ")
);
v = Verdict::NeedsHumanReview;
}
// Hard rule (audit remediation): differential fuzz is the gate
// that catches behavioral divergence the fixed corpus misses, so
// an accept minted without it is an unverified claim. The audit
// found accepted rows carrying fuzz_iters=0 — fuzz had run
// out-of-band, or not at all. A skipped or failed fuzz gate can
// no longer produce a machine accept, only needs-human-review.
if v == Verdict::Accepted && !fuzz_passed {
println!(
"verdict: differential fuzz {} — no accept without a real differential-fuzz \
run against the pristine baseline; capping at needs-human-review. The target \
must declare [gates].fuzz in targets/{}/target.toml, and the run must report \
FUZZ-RESULT with zero divergences (SPEC §8)",
fuzz_state(report.outcome(GateLayer::DifferentialFuzz)),
cli.target
);
v = Verdict::NeedsHumanReview;
}
} }
let fp = EnvFingerprint::collect(pin, "system-default"); let fp = EnvFingerprint::collect(pin, "system-default");
let mut env_fingerprint = serde_json::json!({
"fingerprint": fp,
"workload": spec.bench.workload,
"target_commit": spec.source.commit,
"isolation": cli.isolation_note.as_deref().unwrap_or("unwrapped-host"),
"gates_detail": "diff-fuzz gated in-pipeline against the pristine baseline (an accept requires it to have passed); ASan+UBSan enforced on the accept path, TSan when configured",
});
if !samples.baseline_max_rss_kib.is_empty() && !samples.candidate_max_rss_kib.is_empty() {
env_fingerprint["max_rss_kib"] = serde_json::json!({
"baseline_median": median_u64(&samples.baseline_max_rss_kib),
"candidate_median": median_u64(&samples.candidate_max_rss_kib),
});
}
( (
v, v,
Some(BenchEvidence { Some(BenchEvidence {
@@ -296,20 +489,11 @@ fn main() -> Result<()> {
candidate_ci: (clo, chi), candidate_ci: (clo, chi),
speedup_median: ratio.median, speedup_median: ratio.median,
speedup_ci: (ratio.lo, ratio.hi), speedup_ci: (ratio.lo, ratio.hi),
env_fingerprint: serde_json::json!({ env_fingerprint,
"fingerprint": fp,
"workload": spec.bench.workload,
"target_commit": spec.source.commit,
"gates_detail": "diff-fuzz per-attempt manual; ASan+LSan enforced on the accept path",
}),
}), }),
) )
}; };
let patch = match &cli.patch_file {
Some(p) => std::fs::read_to_string(p)?,
None => "(no source patch)".into(),
};
// Derive the phase from the run_id namespace (phaseN-*) so the column // Derive the phase from the run_id namespace (phaseN-*) so the column
// matches the run_id prefix; the historical hardcoded `1` left every // matches the run_id prefix; the historical hardcoded `1` left every
// verdict-written row tagged phase 1 regardless of namespace (found in // verdict-written row tagged phase 1 regardless of namespace (found in
@@ -344,3 +528,36 @@ fn main() -> Result<()> {
); );
Ok(()) Ok(())
} }
#[cfg(test)]
mod tests {
use super::{fuzz_state, median_u64};
use diff_test::GateOutcome;
#[test]
fn median_u64_odd_and_even() {
assert_eq!(median_u64(&[5]), 5);
assert_eq!(median_u64(&[9, 1, 5]), 5);
assert_eq!(median_u64(&[4, 2, 8, 6]), 5);
}
/// Only `Passed` clears the accept bar; every other state (including a
/// gate that never ran) is a cap to needs-human-review.
#[test]
fn fuzz_state_names_every_outcome() {
assert_eq!(fuzz_state(Some(&GateOutcome::Passed)), "passed");
assert_eq!(
fuzz_state(Some(&GateOutcome::Failed {
detail: "divergences=3".into()
})),
"failed"
);
assert_eq!(
fuzz_state(Some(&GateOutcome::Skipped {
reason: "no baseline".into()
})),
"skipped"
);
assert_eq!(fuzz_state(None), "not run");
}
}
+168
View File
@@ -0,0 +1,168 @@
//! Lexical NHR risk classifier (SPEC §8). Scans only the changed lines
//! of a unified diff for concurrency / unsafe / floating-point tokens
//! and routes any would-be accept that hits one to needs-human-review.
//! Deliberately lexical and conservative: over-triggering costs a human
//! a look, under-triggering ships an unreviewed risk — so plain token
//! presence decides, no semantic analysis.
const CONCURRENCY: &[&str] = &[
"pthread_",
"std::sync",
"std::thread",
"Atomic",
"atomic_",
"__atomic_",
"__sync_",
"Mutex",
"RwLock",
"Condvar",
"mpsc",
"rayon::",
"memory_order",
"volatile",
];
const UNSAFE: &[&str] = &[
"unsafe ",
"transmute",
"from_raw",
"MaybeUninit",
"UnsafeCell",
];
/// The spaced tokens carry their own trailing boundary; `f32`/`f64` get
/// hand boundary checks below.
const FP_SPACED: &[&str] = &["float ", "double "];
const FP_TYPES: &[&str] = &["f32", "f64"];
fn is_ident_letter(b: u8) -> bool {
b.is_ascii_alphabetic() || b == b'_'
}
/// True when `line` contains `token` not glued to an identifier letter on
/// either side. Digits are allowed neighbors so literal suffixes like
/// `1.0f32` still trigger; letter neighbors (identifiers, most hex
/// strings, e.g. `buf32`, `deadf64b`) do not.
fn has_fp_token(line: &str, token: &str) -> bool {
let bytes = line.as_bytes();
let mut start = 0;
while let Some(pos) = line[start..].find(token) {
let i = start + pos;
let before_ok = i == 0 || !is_ident_letter(bytes[i - 1]);
let after_ok = i + token.len() >= bytes.len() || !is_ident_letter(bytes[i + token.len()]);
if before_ok && after_ok {
return true;
}
start = i + 1;
}
false
}
fn is_changed_line(line: &str) -> bool {
(line.starts_with('+') && !line.starts_with("+++"))
|| (line.starts_with('-') && !line.starts_with("---"))
}
/// Classify a unified diff into risk-signal names. `fp_tolerance` is true
/// when the target's equivalence mode is fp-tolerance, itself a §8 signal.
/// Returned names are unique; empty means no signal.
pub fn classify(diff: &str, fp_tolerance: bool) -> Vec<String> {
let (mut concurrency, mut unsafe_sig, mut fp) = (false, false, false);
for line in diff.lines().filter(|l| is_changed_line(l)) {
concurrency = concurrency || CONCURRENCY.iter().any(|t| line.contains(t));
unsafe_sig = unsafe_sig || UNSAFE.iter().any(|t| line.contains(t));
fp = fp
|| FP_SPACED.iter().any(|t| line.contains(t))
|| FP_TYPES.iter().any(|t| has_fp_token(line, t));
}
let mut signals = Vec::new();
if concurrency {
signals.push("concurrency".to_string());
}
if unsafe_sig {
signals.push("unsafe".to_string());
}
if fp {
signals.push("floating-point".to_string());
}
if fp_tolerance {
signals.push("fp-tolerance-equivalence".to_string());
}
signals
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn concurrency_tokens_trigger() {
let diff = "--- a/src/lib.rs\n+++ b/src/lib.rs\n+ let m = Mutex::new(0);\n";
assert_eq!(classify(diff, false), vec!["concurrency"]);
let diff = "- pthread_create(&t, NULL, worker, NULL);\n";
assert_eq!(classify(diff, false), vec!["concurrency"]);
}
#[test]
fn unsafe_tokens_trigger() {
let diff = "+ unsafe { ptr.read() }\n";
assert_eq!(classify(diff, false), vec!["unsafe"]);
let diff = "+ let v: Vec<u8> = Vec::from_raw_parts(p, n, n);\n";
assert_eq!(classify(diff, false), vec!["unsafe"]);
}
#[test]
fn floating_point_tokens_trigger_with_boundaries() {
assert_eq!(
classify("+ let x: f64 = 1.0;\n", false),
vec!["floating-point"]
);
// Literal suffixes have a digit neighbor and must still trigger.
assert_eq!(
classify("+ sum += 1.0f32;\n", false),
vec!["floating-point"]
);
assert_eq!(
classify("+ double acc = 0.0;\n", false),
vec!["floating-point"]
);
// Glued to identifier letters: not an FP token.
assert!(classify("+ let buf32 = [0u8; 4];\n", false).is_empty());
assert!(classify("+ hash = \"deadf64beef\";\n", false).is_empty());
}
#[test]
fn fp_tolerance_flag_is_its_own_signal() {
assert_eq!(classify("", true), vec!["fp-tolerance-equivalence"]);
assert_eq!(
classify("+ let x: f32 = 0.5;\n", true),
vec!["floating-point", "fp-tolerance-equivalence"]
);
}
#[test]
fn clean_diff_yields_no_signals() {
let diff = "--- a/src/lib.rs\n+++ b/src/lib.rs\n@@ -1,3 +1,3 @@\n\
- let n = items.len();\n+ let n = count;\n let done = true;\n";
assert!(classify(diff, false).is_empty());
}
#[test]
fn only_changed_lines_are_scanned() {
// Tokens on context lines and in file headers must not trigger.
let diff = "--- a/src/mutex_wrapper.rs\n+++ b/src/mutex_wrapper.rs\n\
@@ -10,4 +10,4 @@\n let guard = lock.mutex();\n\
+ let n = count;\n";
assert!(classify(diff, false).is_empty());
}
#[test]
fn signals_are_deduped() {
let diff = "+ let a = Mutex::new(0);\n+ let b = RwLock::new(0);\n\
+ unsafe { transmute::<u32, f32>(x) };\n";
assert_eq!(
classify(diff, false),
vec!["concurrency", "unsafe", "floating-point"]
);
}
}
+63 -9
View File
@@ -7,6 +7,22 @@ candidate must prove it does the same thing the baseline does. These are
## The layers ## The layers
0. **Pin checks.** Before any gate runs, the corpus is verified against its
committed `MANIFEST.sha256` and the harness **refuses to run** on a
mismatch — nothing is measured on tampered inputs. This ships and is
enforced for every target.
The *test suite* has the same machinery but is **not yet active**:
`diff-test` verifies `corpora/<t>/TESTSUITE.sha256` when the file is
present (mismatch = hard refusal) and prints an explicit "suite unpinned"
warning when it is absent. **No target ships a suite pin today**, so this
check currently constrains nothing. It matters because upstream suites
are vendored under `targets/<t>/workspace/` — an agent-writable path — so
"the agent cannot edit the tests" rests on a pin that still has to be
generated (`scripts/pin-testsuite.sh`, a deliberate human action, run
after a fetch). Until then, that specific guarantee is a mechanism, not
a fact.
1. **Upstream test suite.** The target's own tests, run against the patched 1. **Upstream test suite.** The target's own tests, run against the patched
code. For cJSON that is the vendored Unity/CTest suite (19 tests: code. For cJSON that is the vendored Unity/CTest suite (19 tests:
`parse_number`, `parse_string`, `print_*`, `misc_tests`, …); for the Rust `parse_number`, `parse_string`, `print_*`, `misc_tests`, …); for the Rust
@@ -20,15 +36,49 @@ candidate must prove it does the same thing the baseline does. These are
mismatch. mismatch.
3. **Differential fuzzing.** Pristine vs candidate on thousands of mutated 3. **Differential fuzzing.** Pristine vs candidate on thousands of mutated
inputs, comparing output. Every accepted win cleared **10,000 mutated inputs, comparing output.
inputs with 0 divergences** — the tokei wins after canonicalizing benign
parallel-walker output ordering (a real finding: differential gates must
compare *semantics*, not bytes, where the target is legitimately
nondeterministic in presentation), the cJSON win on quote/number/escape
edge cases.
4. **Sanitizers.** ASan/LSan for Rust, ASan+UBSan for C. Run on **every *Where it runs.* Fuzz is the one gate that is **not** a property of the
would-be accept** — this is machine-enforced, not manual (see below). candidate alone: it differs the candidate against a **pristine baseline**,
so it needs both binaries. That means it runs **only on the accept path**
(`just verdict`, the one flow that rebuilds a pristine baseline). The
standalone `just gates` flow has no baseline, so the gate reports
**Skipped, with that reason recorded** — it is never faked by comparing
the candidate against itself. `just gates` is a fast equivalence check,
**not** a fuzz run. The gate is graded strictly: `diff-test` runs the
target's declared `[gates].fuzz` command and parses a
`FUZZ-RESULT iters=<n> divergences=<m>` line; a run that never prints one
**fails**, rather than passing by silence.
*The hard rule.* A machine `accepted` verdict is now **impossible**
without this gate having actually passed. A skipped or failed fuzz gate
caps the verdict at `needs-human-review` — because an accept minted
without differential fuzz is an unverified claim.
*Honest history.* Through Phase 5 this ran as an **out-of-band
per-attempt script** (`scripts/diff-fuzz-<target>.py`), not inside the
pipeline — so **8 of the 10 accepted ledger rows record `fuzz_iters=0`**
even where a 10,000-input audit run is documented in the case study. The
two exceptions are the Phase 0 tokei rows, which record 4,332. Those
out-of-band runs produced real findings — the tokei wins passed only
after canonicalizing benign parallel-walker output ordering (differential
gates must compare *semantics*, not bytes, where the target is
legitimately nondeterministic in presentation), and the cJSON win cleared
quote/number/escape edge cases with 0 divergences.
4. **Sanitizers.** ASan+UBSan (ASan/LSan on the Rust targets) is built and
run against the pinned workload on **every would-be accept** — the
sanitizer gate is machine-enforced, not manual (see below). A flagged
run, *or* a target that declares no sanitizer build at all, caps the
accept at `needs-human-review`.
A **TSan lane** exists on the same accept path, but it only runs when a
target declares `[build].tsan`. **No target declares it today, so there
is currently no TSan coverage** — the lane is wired and dormant, and a
patch touching threads needs that opt-in added before it can be checked.
(MIRI, named in the spec, was never implemented.) The concurrency safety
net that *is* live is the review tier below, which routes any
concurrency-token patch to a human.
## The tier rule (SPEC §8) ## The tier rule (SPEC §8)
@@ -36,7 +86,11 @@ Some changes can pass every gate and still be unsafe to auto-ship. A change
that touches floating-point ordering, concurrency primitives, `unsafe`, or that touches floating-point ordering, concurrency primitives, `unsafe`, or
carries a sanitizer flag is **never** auto-accepted — its verdict is capped carries a sanitizer flag is **never** auto-accepted — its verdict is capped
at `needs-human-review`. The tier is decided by the *nature* of the change, at `needs-human-review`. The tier is decided by the *nature* of the change,
never by the size of the number. The mimalloc allocator swap never by the size of the number. The routing is machine-enforced: `verdict`
runs a conservative **lexical risk classifier** over the patch's changed
lines (concurrency / unsafe / floating-point token lists — deliberately
over-triggering, not a semantic analysis) and any signal, or the use of
fp-tolerance equivalence mode at all, forces a would-be accept to review. The mimalloc allocator swap
([case study](./case-studies/build-flags.md)) is the canonical example: a ([case study](./case-studies/build-flags.md)) is the canonical example: a
real +4.6% win, correctly held for a human ruling because a global real +4.6% win, correctly held for a human ruling because a global
allocator change's safety is not something one benchmark can clear. allocator change's safety is not something one benchmark can clear.
+1 -1
View File
@@ -29,7 +29,7 @@ and every one survived the gates above. These are not projections.
| cJSON HTTP service — p50 latency under replayed load | +6.2%, CI [+5.8%, +7.2%] | | cJSON HTTP service — p50 latency under replayed load | +6.2%, CI [+5.8%, +7.2%] |
| comrak (Rust) — mimalloc allocator swap (class 2) | +4.6%, CI [+3.3%, +5.8%], human-ruled | | comrak (Rust) — mimalloc allocator swap (class 2) | +4.6%, CI [+3.3%, +5.8%], human-ruled |
Across **five phases and 33 ledger rows**, with **zero shipped false Across **five phases and 34 ledger rows**, with **zero shipped false
accepts** — including two occasions where the pipeline itself over-accepted accepts** — including two occasions where the pipeline itself over-accepted
and the audit caught it before anything shipped, each becoming a permanent and the audit caught it before anything shipped, each becoming a permanent
new gate. new gate.
+29
View File
@@ -34,6 +34,35 @@ The SDK's tool allow/deny list is defense-in-depth, not the boundary —
path into the repo is the harnessd Unix socket; the trust layer is owned path into the repo is the harnessd Unix socket; the trust layer is owned
by another uid. This is the production shape (`docs/infra.md`). by another uid. This is the production shape (`docs/infra.md`).
## Network isolation on the verdict path
Target code should not be able to phone home during a measurement. What
is actually enforced, stated honestly:
- **The agent path is wrapped.** When `harnessd` launches the verdict
pipeline, it execs through `scripts/no-net.sh`, which runs it inside an
unprivileged **network namespace** (`unshare --net --map-current-user`).
`HOTPATH_VERDICT_WRAPPER` overrides the wrapper.
- **It fails closed.** Where namespaces are unavailable the wrapper **exits
97 without running the pipeline** — it does not degrade silently. The one
bypass, `HOTPATH_ALLOW_UNISOLATED=1`, runs the pipeline **with full
network access**; it warns loudly on stderr *and* the resulting ledger row
records the isolation note `no-net.sh (HOTPATH_ALLOW_UNISOLATED=1 —
network NOT isolated)`. A run cannot claim an isolation it did not have.
- **In CI**, the bench workload runs under `docker run --network=none`.
Two limits, stated plainly rather than buried:
- **Only the harnessd (agent) path is wrapped.** A human who invokes
`just verdict` directly runs it **unwrapped on the host**; those rows are
stamped `isolation: "unwrapped-host"` in the ledger. The wrapper is a
property of the agent pipeline, not of the `verdict` binary.
- **Full-container isolation is still an open gap.** The namespace isolates
the *network* — not the filesystem, not the syscall surface. The
fully containerized, seccomp-restricted bench container of SPEC §10 is
**not built**. The agent-side OS boundary above is a separate mechanism
and *is* shipped.
## The two times the pipeline was wrong ## The two times the pipeline was wrong
Anti-reward-hacking is not "it never fails" — it is "when it fails, the Anti-reward-hacking is not "it never fails" — it is "when it fails, the
+16
View File
@@ -27,6 +27,22 @@ reproduces). A change is accepted only if the **CI lower bound** clears the
threshold in `config/accept.toml` (default 2%) — not the median. The number threshold in `config/accept.toml` (default 2%) — not the median. The number
you commit to is the one that survives the interval. you commit to is the one that survives the interval.
## Beyond wall time: max RSS and PMU counters
Wall time is the **only** metric a verdict is ever decided on. Two
supporting measurements exist alongside it:
- **Max RSS** is captured for every timed run on Unix (`wait4`'s
`ru_maxrss`) and recorded in the run's environment fingerprint as
baseline/candidate medians — so a speedup that quietly pays for itself
in memory is visible next to the number it bought.
- **PMU counters** (cycles, instructions, cache misses, branch misses)
are opt-in diagnostics: `bench-runner compare --perf-stat` wraps each
timed run in `perf stat` and prints per-side medians with the workload
caveat attached. PMU access is unavailable in most VMs, and the
counters never participate in the accept decision — they explain a
wall-time delta, they do not replace it.
## The hardware has to earn trust first ## The hardware has to earn trust first
Before any measurement session, the harness runs two self-tests, recorded Before any measurement session, the harness runs two self-tests, recorded
+4 -2
View File
@@ -8,14 +8,16 @@ the ledger stops you from re-grinding a class that already has a verdict.
|---|---|---|---| |---|---|---|---|
| 1 | Build config | LTO, codegen-units, target-cpu | tokei ✅ +10.4% | | 1 | Build config | LTO, codegen-units, target-cpu | tokei ✅ +10.4% |
| 2 | Allocator | Global allocator swap (mimalloc) | comrak ✅ +4.6% (human-ruled) | | 2 | Allocator | Global allocator swap (mimalloc) | comrak ✅ +4.6% (human-ruled) |
| 3 | Allocation churn | Remove per-item malloc/free | tokei ✅, cJSON ✅ (+ one overturned) | | 3 | Allocation churn | Remove per-item malloc/free | tokei ✅; cJSON rejected-bench (+ one overturned) |
| 4 | Data layout | Make the cache hierarchy fit the access pattern | comrak (attempted) | | 4 | Data layout | Make the cache hierarchy fit the access pattern | comrak (attempted) |
| 5 | Algorithmic | Eliminate redundant work | tokei ×3 ✅, cJSON ✅ | | 5 | Algorithmic | Eliminate redundant work | tokei ×3 ✅, cJSON ✅ |
| 6 | SIMD | Autovectorization enablement | comrak, tokei (attempted) | | 6 | SIMD | Autovectorization enablement | comrak, tokei (attempted) |
| 7 | Concurrency | Parallelism — always `needs-human-review` | — | | 7 | Concurrency | Parallelism — always `needs-human-review` | — |
Six classes were exercised across the case studies, with verified wins on Six classes were exercised across the case studies, with verified wins on
classes 1, 2, 3, and 5 across **both a Rust and a C target**. classes 1, 2, 3, and 5 — spanning **both a Rust and a C target** (the C
win is the class-5 cJSON rewrite; cJSON's class-3 attempt was an honest
`rejected-bench`, its CI lower bound short of the 2% bar).
## Why the ordering is load-bearing ## Why the ordering is load-bearing
+13 -1
View File
@@ -5,6 +5,10 @@ through the same commands.
## Prerequisites ## Prerequisites
- **A Linux/POSIX host.** The pipeline is Unix-only at runtime: `harnessd`
(Unix sockets, `setsid`), service mode, the isolation wrapper, and every
sh-based gate assume it. Windows compiles the workspace and runs the
portable unit tests, but **cannot run the pipeline**.
- Rust (stable) + `just`; `clang`/`cmake` for the C target; `valgrind` for - Rust (stable) + `just`; `clang`/`cmake` for the C target; `valgrind` for
callgrind profiling. Nightly Rust for the sanitizer gate. callgrind profiling. Nightly Rust for the sanitizer gate.
- Targets are vendored fresh (their `workspace/` is gitignored); each - Targets are vendored fresh (their `workspace/` is gitignored); each
@@ -21,6 +25,14 @@ just verdict <t> <bin> ... # gates + bench vs pristine baseline + ledger row
just report <run-id> # ROI report from a ledger row just report <run-id> # ROI report from a ledger row
``` ```
> **`just gates` does not fuzz.** Differential fuzz differs the candidate
> against a *pristine baseline*, and only `just verdict` builds one — so on
> the `gates` flow the fuzz layer reports **Skipped**, with the reason
> recorded, rather than comparing the candidate against itself. Use `gates`
> as a fast equivalence check; the fuzz gate (and the sanitizer lanes) run
> on the accept path, where an accept is impossible without a passed fuzz
> gate.
Service mode (Phase 4): Service mode (Phase 4):
``` ```
@@ -42,7 +54,7 @@ Every accepted win can be re-audited independently:
``` ```
python3 scripts/audit-attempt.py <run-id> # mechanical re-check python3 scripts/audit-attempt.py <run-id> # mechanical re-check
python3 scripts/diff-fuzz-<target>.py <dir> 10000 <candidate-bin> # 10k fuzz python3 scripts/diff-fuzz-<target>.py <dir> 10000 <baseline-bin> <candidate-bin>
``` ```
The ledger itself is the record: `results/ledger.sqlite`, append-only, The ledger itself is the record: `results/ledger.sqlite`, append-only,
+13
View File
@@ -52,6 +52,19 @@ tolerance is defensible for the workload, exactly as with the mimalloc
allocator swap. The machine measures and gates; the human ratifies the allocator swap. The machine measures and gates; the human ratifies the
tolerance. tolerance.
> **Scope, stated honestly.** The FP-tolerance *mechanism* is in the gate
> pipeline — `diff-test` honors an `equivalence.toml` policy (it needs a
> committed `[corpus].golden_reference`, and fails closed without one), and
> `verdict` auto-routes any fp-tolerance run to `needs-human-review`. But
> **`matmul` is not wired into that pipeline**: it has an `equivalence.toml`
> and **no `target.toml`**, which is the file `diff-test`/`verdict` load a
> target from. So the kernel lane is demonstrated **script-driven**
> (`scripts/kernel-lane-demo.sh`, using the `fp-compare` binary and
> `bench-runner compare` directly), and the `phase5-matmul-opt` ledger row
> was recorded outside the automated verdict path. Promoting `matmul` to a
> real pipeline target is open work — the policy is closed; that target's
> wiring is not.
**This is the GPU lane in miniature.** The environment has no GPU, so the **This is the GPU lane in miniature.** The environment has no GPU, so the
GPU fork is shown on the CPU as what it actually is — the same trust GPU fork is shown on the CPU as what it actually is — the same trust
machinery with a different timer. A Triton or CUDA kernel versus a reference machinery with a different timer. A Triton or CUDA kernel versus a reference
+20 -2
View File
@@ -44,5 +44,23 @@ Full write-up: [Research forks](./research-forks.md).
The end goal is not vendored OSS case studies — it is a **real production The end goal is not vendored OSS case studies — it is a **real production
system**. The same gates that matter for a JSON parser matter far more for system**. The same gates that matter for a JSON parser matter far more for
a live CMS: an equivalence failure there is a customer-facing bug, and the a live CMS: an equivalence failure there is a customer-facing bug, and the
no-network sandbox and append-only audit trail are exactly what make it network-isolated verdict path and append-only audit trail are what would
safe to point an optimization agent at code that a business depends on. make it safe to point an optimization agent at code that a business depends
on.
## Known gaps (what would have to close first)
Honesty about the remaining edges, since the product is trust:
- **Full-container isolation.** The verdict pipeline is network-isolated on
the agent path (`scripts/no-net.sh`); the seccomp-restricted bench
container of SPEC §10 is **not built**, and a human running `just verdict`
directly is unwrapped.
- **Test-suite pins.** The verify-or-refuse machinery exists, but **no
target ships a `TESTSUITE.sha256` yet** — so "the agent cannot edit the
upstream tests" is currently a mechanism awaiting its pins.
- **TSan.** The lane runs on the accept path only when a target declares
`[build].tsan`; **none does today**, so there is no TSan coverage. Class-7
(concurrency) work depends on that opt-in landing.
- **Kernel lane wiring.** `matmul` demonstrates FP-tolerance equivalence
script-driven; it is not yet a `target.toml` pipeline target.
+12 -5
View File
@@ -23,10 +23,15 @@ compare baseline candidate:
cargo run -p bench-runner -- --config config/accept.toml compare \ cargo run -p bench-runner -- --config config/accept.toml compare \
--baseline "{{baseline}}" --candidate "{{candidate}}" --baseline "{{baseline}}" --candidate "{{candidate}}"
# Verify a target corpus against its pinned manifest # Verify a target corpus against its pinned manifest (never rewrites it)
# (wired through diff-test CLI once it exists; manual check for now)
pin-check target: pin-check target:
cd corpora/{{target}} && sh gen-corpus.sh cd corpora/{{target}} && sh gen-corpus.sh --check
# Deliberately re-pin a target corpus manifest — a trust-layer change;
# review and commit the new MANIFEST.sha256 with justification
pin-corpus target:
@echo "WARNING: rewriting corpora/{{target}}/MANIFEST.sha256 — deliberate human action (corpora/README.md)"
cd corpora/{{target}} && sh gen-corpus.sh --pin
# Automated calibration: N A/A sessions (<5% false-positive required) + # Automated calibration: N A/A sessions (<5% false-positive required) +
# N injected-slowdown sessions (>=95% detection required), JSON evidence # N injected-slowdown sessions (>=95% detection required), JSON evidence
@@ -34,8 +39,10 @@ calibrate cmd out sessions="20":
cargo run -p bench-runner -- --config config/accept.toml calibrate \ cargo run -p bench-runner -- --config config/accept.toml calibrate \
--cmd "{{cmd}}" --sessions {{sessions}} --out "{{out}}" --cmd "{{cmd}}" --sessions {{sessions}} --out "{{out}}"
# Equivalence gates for a target: corpus pin (refuses on mismatch) -> # Equivalence gates for a target: corpus + test-suite pins (refuse on
# upstream tests -> golden replay. Fuzz/sanitizers stay per-attempt. # mismatch) -> upstream tests -> golden replay. Differential fuzz needs a
# pristine baseline to differ against, so it reports Skipped here and runs
# for real on the accept path; sanitizers likewise run in verdict.
gates target: gates target:
cargo run -p diff-test -- {{target}} cargo run -p diff-test -- {{target}}
+16
View File
@@ -57,3 +57,19 @@ consistent, with the compound tokei figure stated as an approximation
the case study and is now enforced by the report generator too. the case study and is now enforced by the report generator too.
- `phase0-comrak-002` (mimalloc) is `needs-human-review`, awaiting a ruling - `phase0-comrak-002` (mimalloc) is `needs-human-review`, awaiting a ruling
(see `results/rulings/`). (see `results/rulings/`).
## Erratum (2026-07-13)
Finding 1's separation claim above — "only the historical overturned row
is `accepted + false`, so the rule cleanly separates them" — was too
strong. **Five** accepted rows carry `sanitizers_clean: false`:
`phase1-verdict-smoke-002`, `phase1-ipc-smoke-001`, `phase2-tokei-001`,
`phase2-tokei-003`, and `phase2-comrak-010`. All five pre-date the
machine-enforced sanitizer lane — the Phase 1 rows are harness smoke
tests, and the two Phase 2 tokei wins were sanitizer-audited by hand
during the 100% Phase 2 audit (see the Phase 2 case study) — but the
ledger's machine record cannot distinguish "manually audited clean" from
"never run", so the report generator's not-shippable rule flags all five,
not just the overturned row. That is the correct conservative behavior;
only the description of *why* the rule separates cleanly was wrong. The
original text stands unedited above; this note is the correction.
+11
View File
@@ -230,3 +230,14 @@ remains the deployment shape.
pipeline into machine-enforced sanitizer gating. pipeline into machine-enforced sanitizer gating.
- **OS-level tool-boundary isolation:** met — was the tracked hardening - **OS-level tool-boundary isolation:** met — was the tracked hardening
gap; shipped and verified this phase. gap; shipped and verified this phase.
---
> **Note (2026-07-13).** The differential-fuzz and ASan/LSan runs
> described above ran **out-of-band** — per-attempt scripts during the
> 100% human audit, exactly as this case study records ("fuzz and
> sanitizers are per-attempt manual in this phase") — so the Phase 2
> ledger rows carry `fuzz_iters=0` in their machine record. The fuzz
> gate is now pipeline-integrated (`diff-test` runs the target's
> declared fuzz command); rows written after this date record the
> iteration count actually executed.
@@ -62,3 +62,12 @@ increments, all on the same 40%-of-runtime function — and one honest
rejection in between (`phase2-tokei-009`, a real +5.7% median whose rejection in between (`phase2-tokei-009`, a real +5.7% median whose
noisy CI lower bound of +0.2% could not clear the bar, so it does not noisy CI lower bound of +0.2% could not clear the bar, so it does not
count). count).
---
> **Note (2026-07-13).** The 10,000-input differential fuzz cited above
> ran **out-of-band** (`scripts/diff-fuzz-tokei.py`, per-attempt), not
> inside the verdict pipeline — so these ledger rows record
> `fuzz_iters=0` in their machine record. The fuzz gate is now
> pipeline-integrated; rows written after this date record the
> iteration count actually executed.
+9
View File
@@ -84,3 +84,12 @@ about the trust story is Rust-specific. The cheapest safe lever (skip a
per-number allocation) wasn't enough; the algorithmic lever (stop doing a per-number allocation) wasn't enough; the algorithmic lever (stop doing a
redundant heavyweight parse) was — and the pipeline proved it the same redundant heavyweight parse) was — and the pipeline proved it the same
way it proves everything else. way it proves everything else.
---
> **Note (2026-07-13).** The 10,000-input differential fuzz cited above
> ran **out-of-band** (`scripts/diff-fuzz-cjson.py`, per-attempt), not
> inside the verdict pipeline — so the `phase3-cjson-002` ledger row
> records `fuzz_iters=0` in its machine record. The fuzz gate is now
> pipeline-integrated; rows written after this date record the
> iteration count actually executed.
+10
View File
@@ -66,3 +66,13 @@ fuzz, and sanitizers. Every rejection is a real mechanism that simply
didn't clear the 2% CI-lower-bound bar — and it is in the ledger too, didn't clear the 2% CI-lower-bound bar — and it is in the ledger too,
because a rejection is a complete, valid outcome. Nothing shipped that because a rejection is a complete, valid outcome. Nothing shipped that
the pipeline couldn't prove. the pipeline couldn't prove.
> **Note (2026-07-13).** "Every win survived … a 10k-input differential
> fuzz, and sanitizers" is true of the **evidence**, not of the machine
> record at the time: through Phase 5 the fuzz ran **out-of-band**
> (`scripts/diff-fuzz-<target>.py`, per-attempt) and the Phase 3 rows
> therefore carry `fuzz_iters=0`. The fuzz gate is now integrated into the
> accept path, where a passed fuzz gate is a **precondition** for a machine
> `accepted` verdict; rows written after this date record the iteration
> count actually executed. See the per-case-study notes in
> `01-tokei-algorithmic-rust.md` and `04-cjson-c-target.md`.
+25 -3
View File
@@ -36,9 +36,11 @@ numbers.
## The result (`phase4-cjson-service`, ledger) ## The result (`phase4-cjson-service`, ledger)
The accepted batch win `phase3-cjson-002` (+8.85% throughput) was The accepted batch win `phase3-cjson-002` (+8.85% throughput) was
measured as a **service under a 150 rps coordinated-omission-correct measured as a **service under a 150 rps-target coordinated-omission-correct
replay**, baseline (pristine cJSON v1.7.19) vs candidate (the banked replay**, baseline (pristine cJSON v1.7.19) vs candidate (the banked
win), 20 interleaved rounds, server pinned, 40,000 requests, **0 drops**: win), 20 interleaved rounds, server pinned, **0 drops** — the ledger row
records `sessions=20`, `rate_rps=150`, `drop_rate=0.0`; the total request
count was **not machine-recorded** (see errata below):
| metric | baseline | candidate | speedup | 95% CI | | metric | baseline | candidate | speedup | 95% CI |
|---|---|---|---|---| |---|---|---|---|---|
@@ -63,7 +65,10 @@ project in one line.
## The ROI, generated mechanically ## The ROI, generated mechanically
`just report phase4-cjson-service --service-json results/phase4/cjson-service.json` `just report phase4-cjson-service --service-json results/phase4/cjson-service.json`
→ [`cjson-service-roi.md`](cjson-service-roi.md). On a 500-core fleet at → [`cjson-service-roi.md`](cjson-service-roi.md). *(Note, 2026-07-13: the
raw `cjson-service.json` was not preserved — see errata below — so this
command is not reproducible as written; the generated report and the
ledger row are the surviving evidence.)* On a 500-core fleet at
$0.04/core-hour, the +6.2% p50 win returns **27.5 cores / $9,621 per $0.04/core-hour, the +6.2% p50 win returns **27.5 cores / $9,621 per
year at the CI lower bound** (median 29.4 cores / $10,290) — and the year at the CI lower bound** (median 29.4 cores / $10,290) — and the
report quotes the lower bound, because the point estimate is not a report quotes the lower bound, because the point estimate is not a
@@ -79,3 +84,20 @@ that produced it, so the number survives hostile review.
ledger row and the calibrated service JSON with no hand-editing; ledger row and the calibrated service JSON with no hand-editing;
throughput→cores→dollars and latency percentiles, CIs and methodology throughput→cores→dollars and latency percentiles, CIs and methodology
inline. inline.
## Errata (2026-07-13)
- **"40,000 requests" retracted.** This report originally stated the
replay comprised 40,000 requests. The `phase4-cjson-service` ledger row
records only `sessions=20`, `rate_rps=150`, and `drop_rate=0.0` — the
per-run request count was never machine-recorded, so the 40,000 figure
(an arithmetic reconstruction, not evidence) is withdrawn. The prose
above now states the ledger-backed facts only. The claimed result is
unaffected: p50 latency +6.2%, 95% CI [+5.8%, +7.2%], under the 150
rps-target CO-correct replay, 20 interleaved rounds, 0 drops.
- **`results/phase4/cjson-service.json` was not preserved.** The
`.gitignore` rule `results/**/*.json` swept the raw service-latency
JSON that the report command consumes, so it no longer exists in the
repository. A `!results/phase4/*.json` carve-out has been added so
future service-mode evidence JSONs are committed alongside the
calibration JSONs.
+28
View File
@@ -137,3 +137,31 @@ it generalize?" objection with the two hardest cases the spec named:
Zero shipped false accepts, still. The product is trust, and trust Zero shipped false accepts, still. The product is trust, and trust
transferred. transferred.
---
## Erratum (2026-07-13) — how far the kernel lane is actually wired
The write-up above describes the FP-tolerance gate as though `matmul` runs
through the standard pipeline. It does not, and the distinction matters:
- **The policy is in the pipeline.** `EquivalencePolicy` is honored by
`diff-test`'s gate sequence (an fp-tolerance target needs a committed
`[corpus].golden_reference`; the gate fails closed without one), and
`crates/verdict` treats fp-tolerance equivalence as an automatic
`needs-human-review` signal. That machinery is real and unit-tested.
- **`matmul` is not a pipeline target.** It ships `targets/matmul/kernel.c`
and `targets/matmul/equivalence.toml` but **no `target.toml`** — the file
`diff-test`/`verdict` load a target from. So the lane cannot currently be
run via `just gates` or `just verdict` at all.
- **Therefore the demonstration is script-driven.**
`scripts/kernel-lane-demo.sh` invokes the `fp-compare` binary and
`bench-runner compare` directly, and the `phase5-matmul-opt` ledger row
was recorded **outside** the automated verdict path.
Nothing in the measured result changes — the 3.23× [3.16, 3.26] speedup, the
244,901/262,144 differing values, the rejected +0.5 perturbation, and the
`needs-human-review` verdict all stand as printed. What was overstated is the
*integration*: the equivalence policy is closed; wiring this target into the
pipeline is open work. The original text stands unedited above; this note is
the correction.
+103 -19
View File
@@ -4,7 +4,21 @@ produce byte-identical stdout (serialization + checksum) AND identical exit
code on mutated JSON. cJSON is a deterministic single-threaded parser, so code on mutated JSON. cJSON is a deterministic single-threaded parser, so
raw byte comparison needs no canonicalization. raw byte comparison needs no canonicalization.
python3 scripts/diff-fuzz-cjson.py <workdir> <iters> <candidate-bin> python3 scripts/diff-fuzz-cjson.py <workdir> <iters> <baseline-bin> <candidate-bin>
All four arguments are required and are supplied by the harness: diff-test
substitutes {iters}, {baseline} and {candidate} into the target's
[gates].fuzz template and runs it from the repo root. Both binary paths may
be repo-root-relative OR absolute (the accept path rebuilds the pristine
baseline to an absolute path) — relative ones resolve against the repo root.
Nothing here may hardcode a baseline location: the harness owns where the
pristine build lands, and a hardcoded path is a gate that always hard-fails.
Final stdout line is the machine contract diff-test parses:
FUZZ-RESULT iters=<n> divergences=<m>. Exit 0 iff the run completed with
zero divergences, 1 on divergence (the line is still printed, with the real
count), 2 on a usage/setup error — which prints no FUZZ-RESULT and is
therefore graded a hard gate FAILURE, never a pass.
Seeds: slices of the pinned corpus plus hand JSON fragments. Mutations Seeds: slices of the pinned corpus plus hand JSON fragments. Mutations
target parser edges: structural tokens, string escapes, number forms target parser edges: structural tokens, string escapes, number forms
@@ -15,13 +29,12 @@ import random
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from typing import NoReturn
ROOT = Path("/home/user/peltier.io") ROOT = Path(__file__).resolve().parents[1]
BASE = ROOT / "targets/cjson/baseline/cjson-bench"
CAND = ROOT / sys.argv[3] if len(sys.argv) > 3 else ROOT / "targets/cjson/baseline/cjson-bench"
CORPUS = ROOT / "corpora/cjson/input/big.json" CORPUS = ROOT / "corpora/cjson/input/big.json"
WORK = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("/tmp/difffuzz-cjson")
ITERS = int(sys.argv[2]) if len(sys.argv) > 2 else 10000 USAGE = "usage: diff-fuzz-cjson.py <workdir> <iters> <baseline-bin> <candidate-bin>"
TOKENS = [ TOKENS = [
b"{", b"}", b"[", b"]", b'"', b":", b",", b"\\", b"\\n", b"\\u00e9", b"\\uD83D", b"{", b"}", b"[", b"]", b'"', b":", b",", b"\\", b"\\n", b"\\u00e9", b"\\uD83D",
@@ -38,6 +51,59 @@ SEEDS_STATIC = [
] ]
def die(msg: str) -> NoReturn:
"""Fatal setup error: exit 2 having printed NO FUZZ-RESULT line. diff-test
grades a fuzz command that never reports the line as a hard gate failure,
which is the right posture for a gate that could not run at all — it must
never be gradeable as a pass."""
print(f"diff-fuzz-cjson: {msg}", file=sys.stderr)
raise SystemExit(2)
def parse_args(argv: list[str]) -> tuple[Path, int, Path, Path]:
"""<workdir> <iters> <baseline-bin> <candidate-bin>, all positional."""
if len(argv) != 5:
die(f"expected 4 arguments, got {len(argv) - 1}\n{USAGE}")
work, raw_iters, baseline, candidate = argv[1:5]
if not raw_iters.isdigit():
die(f"iters must be a non-negative integer, got {raw_iters!r}\n{USAGE}")
iters = int(raw_iters)
if iters < 1:
# A zero-iteration run would print FUZZ-RESULT iters=0 divergences=0
# and exit 0 — a fuzz gate that passes without fuzzing anything.
die(f"iters must be >= 1, got {iters}\n{USAGE}")
# `ROOT / p` keeps a repo-root-relative path anchored at the repo root and
# leaves an absolute path untouched (pathlib drops the left operand when
# the right side is absolute). The harness supplies either form.
return ROOT / work, iters, ROOT / baseline, ROOT / candidate
def need_binary(path: Path, label: str) -> Path:
if not path.is_file():
die(
f"{label} binary not found: {path}\nthe harness substitutes "
"{baseline}/{candidate} in [gates].fuzz — check targets/cjson/target.toml"
)
return path
def keep_dir(work: Path, tag: str) -> Path:
"""First unused divergence-<tag> directory under `work`.
The RNG is seeded, so re-running in the same work dir re-diverges at the
same iteration: the kept-divergence directory already exists. The old
plain `mkdir()` raised FileExistsError there and killed the run *before*
the FUZZ-RESULT line, so diff-test reported a genuine divergence — the
whole point of the gate — as "fuzz command did not report FUZZ-RESULT".
This never raises; it picks the next free name instead.
"""
path, n = work / f"divergence-{tag}", 2
while path.exists():
path = work / f"divergence-{tag}-{n}"
n += 1
return path
def mutate(data: bytes, rng: random.Random) -> bytes: def mutate(data: bytes, rng: random.Random) -> bytes:
b = bytearray(data) b = bytearray(data)
for _ in range(rng.randrange(1, 8)): for _ in range(rng.randrange(1, 8)):
@@ -65,36 +131,54 @@ def run(binary: Path, path: Path) -> tuple[bytes, int]:
return r.stdout, r.returncode return r.stdout, r.returncode
def main() -> int: def main(argv: list[str]) -> int:
work, iters, baseline, candidate = parse_args(argv)
need_binary(baseline, "baseline")
need_binary(candidate, "candidate")
if not CORPUS.is_file():
die(f"corpus not found: {CORPUS} (generate it with corpora/cjson/gen-corpus.sh)")
rng = random.Random(0x1CE_B00C) rng = random.Random(0x1CE_B00C)
corpus = CORPUS.read_bytes() corpus = CORPUS.read_bytes()
seeds = list(SEEDS_STATIC) seeds = list(SEEDS_STATIC)
# windows of the real corpus (often invalid mid-slice — exercises the # windows of the real corpus (often invalid mid-slice — exercises the
# error path, which must diverge identically or not at all) # error path, which must diverge identically or not at all). The max(1, …)
for o in range(0, len(corpus) - 4096, len(corpus) // 200): # guards are no-ops on the pinned 5.6 MB corpus (identical seed set, so
# the fuzz stays reproducible) and stop a degenerate tiny corpus from
# raising a zero-step ValueError before FUZZ-RESULT is ever printed.
for o in range(0, max(1, len(corpus) - 4096), max(1, len(corpus) // 200)):
for n in (64, 512, 4096): for n in (64, 512, 4096):
seeds.append(corpus[o : o + n]) seeds.append(corpus[o : o + n])
WORK.mkdir(parents=True, exist_ok=True) work.mkdir(parents=True, exist_ok=True)
case = WORK / "case.json" case = work / "case.json"
divergences = 0 divergences = 0
for i in range(ITERS): for i in range(iters):
payload = mutate(rng.choice(seeds), rng) payload = mutate(rng.choice(seeds), rng)
case.write_bytes(payload) case.write_bytes(payload)
a_out, a_rc = run(BASE, case) a_out, a_rc = run(baseline, case)
b_out, b_rc = run(CAND, case) b_out, b_rc = run(candidate, case)
if a_out != b_out or a_rc != b_rc: if a_out != b_out or a_rc != b_rc:
divergences += 1 divergences += 1
keep = WORK / f"divergence-{i}" # Keeping the reproducer is best-effort: an I/O failure here must
keep.mkdir(exist_ok=True) # never cost the FUZZ-RESULT line and turn a real divergence into
# an unparseable gate error (see keep_dir).
try:
keep = keep_dir(work, str(i))
keep.mkdir(parents=True, exist_ok=True)
(keep / "input.json").write_bytes(payload) (keep / "input.json").write_bytes(payload)
(keep / "baseline.out").write_bytes(a_out + f"\nrc={a_rc}".encode()) (keep / "baseline.out").write_bytes(a_out + f"\nrc={a_rc}".encode())
(keep / "candidate.out").write_bytes(b_out + f"\nrc={b_rc}".encode()) (keep / "candidate.out").write_bytes(b_out + f"\nrc={b_rc}".encode())
print(f"DIVERGENCE at iter {i} (rc {a_rc} vs {b_rc}); kept in {keep}") print(f"DIVERGENCE at iter {i} (rc {a_rc} vs {b_rc}); kept in {keep}")
except OSError as e:
print(f"DIVERGENCE at iter {i} (rc {a_rc} vs {b_rc}); "
f"could not keep artifacts: {e}")
if (i + 1) % 1000 == 0: if (i + 1) % 1000 == 0:
print(f"{i + 1}/{ITERS}, {divergences} divergences", flush=True) print(f"{i + 1}/{iters}, {divergences} divergences", flush=True)
print(f"done: {ITERS} inputs, {divergences} divergences") print(f"done: {iters} inputs, {divergences} divergences")
# Machine contract, always the final stdout line on any completing run.
print(f"FUZZ-RESULT iters={iters} divergences={divergences}", flush=True)
return 1 if divergences else 0 return 1 if divergences else 0
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(main()) sys.exit(main(sys.argv))
+104 -18
View File
@@ -4,7 +4,21 @@ produce byte-identical HTML (stdout), stderr, and exit code on mutated
markdown. comrak is a single-threaded stdin->stdout filter, so raw byte markdown. comrak is a single-threaded stdin->stdout filter, so raw byte
comparison needs no canonicalization. comparison needs no canonicalization.
python3 scripts/diff-fuzz-comrak.py <workdir> <iters> <candidate-bin> python3 scripts/diff-fuzz-comrak.py <workdir> <iters> <baseline-bin> <candidate-bin>
All four arguments are required and are supplied by the harness: diff-test
substitutes {iters}, {baseline} and {candidate} into the target's
[gates].fuzz template and runs it from the repo root. Both binary paths may
be repo-root-relative OR absolute (the accept path rebuilds the pristine
baseline to an absolute path) — relative ones resolve against the repo root.
Nothing here may hardcode a baseline location: the harness owns where the
pristine build lands, and a hardcoded path is a gate that always hard-fails.
Final stdout line is the machine contract diff-test parses:
FUZZ-RESULT iters=<n> divergences=<m>. Exit 0 iff the run completed with
zero divergences, 1 on divergence (the line is still printed, with the real
count), 2 on a usage/setup error — which prints no FUZZ-RESULT and is
therefore graded a hard gate FAILURE, never a pass.
Seeds: windows of the pinned Pro Git corpus. Mutations focus on inline/ Seeds: windows of the pinned Pro Git corpus. Mutations focus on inline/
block structure the parser and teardown paths exercise: emphasis, code block structure the parser and teardown paths exercise: emphasis, code
@@ -15,13 +29,12 @@ import random
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from typing import NoReturn
ROOT = Path("/home/user/peltier.io") ROOT = Path(__file__).resolve().parents[1]
BASE = ROOT / "targets/comrak/baseline/release/comrak"
CAND = ROOT / sys.argv[3]
CORPUS = ROOT / "corpora/comrak/progit.md" CORPUS = ROOT / "corpora/comrak/progit.md"
WORK = Path(sys.argv[1])
ITERS = int(sys.argv[2]) USAGE = "usage: diff-fuzz-comrak.py <workdir> <iters> <baseline-bin> <candidate-bin>"
TOKENS = [ TOKENS = [
b"**", b"*", b"__", b"_", b"`", b"```", b"~~~", b"~~", b"[", b"](", b")", b"**", b"*", b"__", b"_", b"`", b"```", b"~~~", b"~~", b"[", b"](", b")",
@@ -30,6 +43,59 @@ TOKENS = [
] ]
def die(msg: str) -> NoReturn:
"""Fatal setup error: exit 2 having printed NO FUZZ-RESULT line. diff-test
grades a fuzz command that never reports the line as a hard gate failure,
which is the right posture for a gate that could not run at all — it must
never be gradeable as a pass."""
print(f"diff-fuzz-comrak: {msg}", file=sys.stderr)
raise SystemExit(2)
def parse_args(argv: list[str]) -> tuple[Path, int, Path, Path]:
"""<workdir> <iters> <baseline-bin> <candidate-bin>, all positional."""
if len(argv) != 5:
die(f"expected 4 arguments, got {len(argv) - 1}\n{USAGE}")
work, raw_iters, baseline, candidate = argv[1:5]
if not raw_iters.isdigit():
die(f"iters must be a non-negative integer, got {raw_iters!r}\n{USAGE}")
iters = int(raw_iters)
if iters < 1:
# A zero-iteration run would print FUZZ-RESULT iters=0 divergences=0
# and exit 0 — a fuzz gate that passes without fuzzing anything.
die(f"iters must be >= 1, got {iters}\n{USAGE}")
# `ROOT / p` keeps a repo-root-relative path anchored at the repo root and
# leaves an absolute path untouched (pathlib drops the left operand when
# the right side is absolute). The harness supplies either form.
return ROOT / work, iters, ROOT / baseline, ROOT / candidate
def need_binary(path: Path, label: str) -> Path:
if not path.is_file():
die(
f"{label} binary not found: {path}\nthe harness substitutes "
"{baseline}/{candidate} in [gates].fuzz — check targets/comrak/target.toml"
)
return path
def keep_dir(work: Path, tag: str) -> Path:
"""First unused divergence-<tag> directory under `work`.
The RNG is seeded, so re-running in the same work dir re-diverges at the
same iteration: the kept-divergence directory already exists. The old
plain `mkdir()` raised FileExistsError there and killed the run *before*
the FUZZ-RESULT line, so diff-test reported a genuine divergence — the
whole point of the gate — as "fuzz command did not report FUZZ-RESULT".
This never raises; it picks the next free name instead.
"""
path, n = work / f"divergence-{tag}", 2
while path.exists():
path = work / f"divergence-{tag}-{n}"
n += 1
return path
def mutate(data: bytes, rng: random.Random) -> bytes: def mutate(data: bytes, rng: random.Random) -> bytes:
b = bytearray(data) b = bytearray(data)
for _ in range(rng.randrange(1, 8)): for _ in range(rng.randrange(1, 8)):
@@ -57,34 +123,54 @@ def run(binary: Path, payload: bytes) -> tuple[bytes, bytes, int]:
return r.stdout, r.stderr, r.returncode return r.stdout, r.stderr, r.returncode
def main() -> int: def main(argv: list[str]) -> int:
work, iters, baseline, candidate = parse_args(argv)
need_binary(baseline, "baseline")
need_binary(candidate, "candidate")
if not CORPUS.is_file():
die(f"corpus not found: {CORPUS} (generate it with corpora/comrak/gen-corpus.sh)")
rng = random.Random(0xC0FFEE) rng = random.Random(0xC0FFEE)
corpus = CORPUS.read_bytes() corpus = CORPUS.read_bytes()
# Seed windows across the whole book, varied sizes. # Seed windows across the whole book, varied sizes. The max(1, …) guards
# are no-ops on the pinned 11 MB corpus (identical seed set, so the fuzz
# stays reproducible); they stop a degenerate tiny corpus from raising a
# zero-step ValueError, or yielding no seeds at all, before FUZZ-RESULT
# is ever printed.
seeds = [ seeds = [
corpus[o : o + n] corpus[o : o + n]
for o in range(0, len(corpus) - 65536, len(corpus) // 300) for o in range(0, max(1, len(corpus) - 65536), max(1, len(corpus) // 300))
for n in (512, 4096, 32768) for n in (512, 4096, 32768)
] ]
WORK.mkdir(parents=True, exist_ok=True) if not seeds:
die(f"corpus is empty, no seeds to fuzz from: {CORPUS}")
work.mkdir(parents=True, exist_ok=True)
divergences = 0 divergences = 0
for i in range(ITERS): for i in range(iters):
payload = mutate(rng.choice(seeds), rng) payload = mutate(rng.choice(seeds), rng)
a = run(BASE, payload) a = run(baseline, payload)
b = run(CAND, payload) b = run(candidate, payload)
if a != b: if a != b:
divergences += 1 divergences += 1
keep = WORK / f"divergence-{i}" # Keeping the reproducer is best-effort: an I/O failure here must
keep.mkdir() # never cost the FUZZ-RESULT line and turn a real divergence into
# an unparseable gate error (see keep_dir).
try:
keep = keep_dir(work, str(i))
keep.mkdir(parents=True, exist_ok=True)
(keep / "input.md").write_bytes(payload) (keep / "input.md").write_bytes(payload)
(keep / "baseline.out").write_bytes(a[0] + b"\n--stderr--\n" + a[1] + f"\nrc={a[2]}".encode()) (keep / "baseline.out").write_bytes(a[0] + b"\n--stderr--\n" + a[1] + f"\nrc={a[2]}".encode())
(keep / "candidate.out").write_bytes(b[0] + b"\n--stderr--\n" + b[1] + f"\nrc={b[2]}".encode()) (keep / "candidate.out").write_bytes(b[0] + b"\n--stderr--\n" + b[1] + f"\nrc={b[2]}".encode())
print(f"DIVERGENCE at iter {i}; kept in {keep}") print(f"DIVERGENCE at iter {i}; kept in {keep}")
except OSError as e:
print(f"DIVERGENCE at iter {i}; could not keep artifacts: {e}")
if (i + 1) % 1000 == 0: if (i + 1) % 1000 == 0:
print(f"{i + 1}/{ITERS}, {divergences} divergences", flush=True) print(f"{i + 1}/{iters}, {divergences} divergences", flush=True)
print(f"done: {ITERS} inputs, {divergences} divergences") print(f"done: {iters} inputs, {divergences} divergences")
# Machine contract, always the final stdout line on any completing run.
print(f"FUZZ-RESULT iters={iters} divergences={divergences}", flush=True)
return 1 if divergences else 0 return 1 if divergences else 0
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(main()) sys.exit(main(sys.argv))
+110 -20
View File
@@ -1,8 +1,25 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Differential fuzz for phase2-tokei-001: pristine vs candidate tokei """Differential fuzz for tokei attempts: pristine vs candidate tokei
must produce byte-identical output on mutated source inputs targeting must produce byte-identical output on mutated source inputs targeting
the patched code path (quote/comment token scanning). the patched code path (quote/comment token scanning).
python3 scripts/diff-fuzz-tokei.py <workdir> <iters> <baseline-bin> <candidate-bin>
All four arguments are required and are supplied by the harness: diff-test
substitutes {iters}, {baseline} and {candidate} into the target's
[gates].fuzz template and runs it from the repo root. Both binary paths may
be repo-root-relative OR absolute (the accept path rebuilds the pristine
baseline to an absolute path) — relative ones resolve against the repo root.
Nothing here may hardcode a baseline location: the harness owns where the
pristine build lands, and a hardcoded path is a gate that always hard-fails.
Final stdout line is the machine contract diff-test parses:
FUZZ-RESULT iters=<n> divergences=<m>. Exit 0 iff the run completed with
zero divergences, 1 on divergence (the line is still printed, with the real
count), 2 on a usage/setup error — which prints no FUZZ-RESULT and is
therefore graded a hard gate FAILURE, never a pass. `iters` counts mutated
files (tokei scans a directory, so files are batched per invocation).
Seeds: files from the pinned corpus (many languages). Mutations focus on Seeds: files from the pinned corpus (many languages). Mutations focus on
quote/comment structure: inserting/deleting/duplicating quote chars, quote/comment structure: inserting/deleting/duplicating quote chars,
comment openers/closers, backslashes, and random bytes; plus boundary comment openers/closers, backslashes, and random bytes; plus boundary
@@ -13,13 +30,12 @@ import random
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from typing import NoReturn
ROOT = Path("/home/user/peltier.io") ROOT = Path(__file__).resolve().parents[1]
BASE = ROOT / "targets/tokei/baseline/release/tokei"
CAND = ROOT / "targets/tokei/candidate-4e3da7e6bd1c/release/tokei"
CORPUS = ROOT / "corpora/tokei/input/copy1" CORPUS = ROOT / "corpora/tokei/input/copy1"
WORK = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("/tmp/difffuzz")
ITERS = int(sys.argv[2]) if len(sys.argv) > 2 else 10000 USAGE = "usage: diff-fuzz-tokei.py <workdir> <iters> <baseline-bin> <candidate-bin>"
TOKENS = [ TOKENS = [
b'"', b"'", b'`', b'"""', b"'''", b"/*", b"*/", b"//", b"#", b"--", b'"', b"'", b'`', b'"""', b"'''", b"/*", b"*/", b"//", b"#", b"--",
@@ -27,6 +43,60 @@ TOKENS = [
b"r#\"", b"\"#", b"=begin", b"=end", b"'''", b'f"""', b"%{", b"}", b"r#\"", b"\"#", b"=begin", b"=end", b"'''", b'f"""', b"%{", b"}",
] ]
def die(msg: str) -> NoReturn:
"""Fatal setup error: exit 2 having printed NO FUZZ-RESULT line. diff-test
grades a fuzz command that never reports the line as a hard gate failure,
which is the right posture for a gate that could not run at all — it must
never be gradeable as a pass."""
print(f"diff-fuzz-tokei: {msg}", file=sys.stderr)
raise SystemExit(2)
def parse_args(argv: list[str]) -> tuple[Path, int, Path, Path]:
"""<workdir> <iters> <baseline-bin> <candidate-bin>, all positional."""
if len(argv) != 5:
die(f"expected 4 arguments, got {len(argv) - 1}\n{USAGE}")
work, raw_iters, baseline, candidate = argv[1:5]
if not raw_iters.isdigit():
die(f"iters must be a non-negative integer, got {raw_iters!r}\n{USAGE}")
iters = int(raw_iters)
if iters < 1:
# A zero-iteration run would print FUZZ-RESULT iters=0 divergences=0
# and exit 0 — a fuzz gate that passes without fuzzing anything.
die(f"iters must be >= 1, got {iters}\n{USAGE}")
# `ROOT / p` keeps a repo-root-relative path anchored at the repo root and
# leaves an absolute path untouched (pathlib drops the left operand when
# the right side is absolute). The harness supplies either form.
return ROOT / work, iters, ROOT / baseline, ROOT / candidate
def need_binary(path: Path, label: str) -> Path:
if not path.is_file():
die(
f"{label} binary not found: {path}\nthe harness substitutes "
"{baseline}/{candidate} in [gates].fuzz — check targets/tokei/target.toml"
)
return path
def keep_dir(work: Path, tag: str) -> Path:
"""First unused divergence-<tag> directory under `work`.
The RNG is seeded, so re-running in the same work dir re-diverges at the
same batch: the kept-divergence directory already exists. The old plain
`rename()` raised (the destination is a non-empty directory) and killed
the run *before* the FUZZ-RESULT line, so diff-test reported a genuine
divergence — the whole point of the gate — as "fuzz command did not
report FUZZ-RESULT". This never raises; it picks the next free name.
"""
path, n = work / f"divergence-{tag}", 2
while path.exists():
path = work / f"divergence-{tag}-{n}"
n += 1
return path
def mutate(data: bytes, rng: random.Random) -> bytes: def mutate(data: bytes, rng: random.Random) -> bytes:
ops = rng.randrange(1, 6) ops = rng.randrange(1, 6)
b = bytearray(data) b = bytearray(data)
@@ -51,6 +121,7 @@ def mutate(data: bytes, rng: random.Random) -> bytes:
b[pos:pos] = b"\n" b[pos:pos] = b"\n"
return bytes(b) return bytes(b)
def canonicalize(payload: bytes) -> str: def canonicalize(payload: bytes) -> str:
"""Parse tokei JSON and sort every list so benign output-order """Parse tokei JSON and sort every list so benign output-order
nondeterminism (parallel walker arrival order) doesn't count as a nondeterminism (parallel walker arrival order) doesn't count as a
@@ -78,43 +149,62 @@ def run(binary: Path, d: Path) -> tuple[str, bytes, int]:
canon = f"UNPARSEABLE:{e}:{r.stdout[:200]!r}" canon = f"UNPARSEABLE:{e}:{r.stdout[:200]!r}"
return canon + f"||rc={r.returncode}", r.stdout, r.returncode return canon + f"||rc={r.returncode}", r.stdout, r.returncode
def main() -> int:
def main(argv: list[str]) -> int:
work, iters, baseline, candidate = parse_args(argv)
need_binary(baseline, "baseline")
need_binary(candidate, "candidate")
if not CORPUS.is_dir():
die(f"corpus not found: {CORPUS} (generate it with corpora/tokei/gen-corpus.sh)")
rng = random.Random(0xB16B00B5) rng = random.Random(0xB16B00B5)
seeds = sorted(CORPUS.rglob("*")) seeds = sorted(CORPUS.rglob("*"))
seeds = [p for p in seeds if p.is_file() and p.stat().st_size < 200_000][:400] seeds = [p for p in seeds if p.is_file() and p.stat().st_size < 200_000][:400]
if not seeds: if not seeds:
print("no seeds found", file=sys.stderr) die(f"no seeds found under {CORPUS}")
return 2 work.mkdir(parents=True, exist_ok=True)
WORK.mkdir(parents=True, exist_ok=True) fdir = work / "case"
fdir = WORK / "case"
divergences = 0 divergences = 0
done = 0 done = 0
runs = 0
# Batch: mutate BATCH files per tokei invocation (tokei scans a dir), # Batch: mutate BATCH files per tokei invocation (tokei scans a dir),
# keeping each seed's extension so language detection stays realistic. # keeping each seed's extension so language detection stays realistic.
BATCH = 50 BATCH = 50
while done < ITERS: while done < iters:
if fdir.exists(): if fdir.exists():
for f in fdir.iterdir(): for f in fdir.iterdir():
f.unlink() f.unlink()
fdir.mkdir(exist_ok=True) fdir.mkdir(exist_ok=True)
for j in range(min(BATCH, ITERS - done)): batch = min(BATCH, iters - done)
for j in range(batch):
seed = rng.choice(seeds) seed = rng.choice(seeds)
name = f"m{done + j}{seed.suffix or '.txt'}" name = f"m{done + j}{seed.suffix or '.txt'}"
(fdir / name).write_bytes(mutate(seed.read_bytes(), rng)) (fdir / name).write_bytes(mutate(seed.read_bytes(), rng))
a_canon, a_raw, a_rc = run(BASE, fdir) a_canon, a_raw, a_rc = run(baseline, fdir)
b_canon, b_raw, b_rc = run(CAND, fdir) b_canon, b_raw, b_rc = run(candidate, fdir)
if a_canon != b_canon: if a_canon != b_canon:
divergences += 1 divergences += 1
keep = WORK / f"divergence-{done}" # Keeping the reproducer is best-effort: an I/O failure here must
# never cost the FUZZ-RESULT line and turn a real divergence into
# an unparseable gate error (see keep_dir). If the rename fails,
# fdir survives and the next batch cleans it out.
try:
keep = keep_dir(work, str(done))
fdir.rename(keep) fdir.rename(keep)
(keep / "_baseline.out").write_bytes(a_raw + f"\nrc={a_rc}".encode()) (keep / "_baseline.out").write_bytes(a_raw + f"\nrc={a_rc}".encode())
(keep / "_candidate.out").write_bytes(b_raw + f"\nrc={b_rc}".encode()) (keep / "_candidate.out").write_bytes(b_raw + f"\nrc={b_rc}".encode())
print(f"DIVERGENCE at batch starting {done}; inputs+outputs kept in {keep}") print(f"DIVERGENCE at batch starting {done}; inputs+outputs kept in {keep}")
done += BATCH except OSError as e:
print(f"DIVERGENCE at batch starting {done}; could not keep artifacts: {e}")
done += batch
runs += 1
if done % 1000 == 0: if done % 1000 == 0:
print(f"{done}/{ITERS} mutated files, {divergences} divergences", flush=True) print(f"{done}/{iters} mutated files, {divergences} divergences", flush=True)
print(f"done: {done} mutated files across {done // BATCH} runs, {divergences} divergences") print(f"done: {done} mutated files across {runs} runs, {divergences} divergences")
# Machine contract, always the final stdout line on any completing run.
print(f"FUZZ-RESULT iters={done} divergences={divergences}", flush=True)
return 1 if divergences else 0 return 1 if divergences else 0
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(main()) sys.exit(main(sys.argv))
+25
View File
@@ -0,0 +1,25 @@
#!/bin/sh
# Network-isolation wrapper for the local verdict path (SPEC §10): exec the
# given command inside a fresh network namespace so target code built and
# run by the pipeline cannot reach the network. harnessd invokes it as
# `no-net.sh sh -c <pipeline-script>` (HOTPATH_VERDICT_WRAPPER overrides).
# CI gets the same guarantee from `docker --network=none` instead.
set -eu
if [ $# -eq 0 ]; then
echo "usage: scripts/no-net.sh <command> [args...]" >&2
exit 2
fi
if unshare --net --map-current-user true 2>/dev/null; then
exec unshare --net --map-current-user -- "$@"
fi
if [ "${HOTPATH_ALLOW_UNISOLATED:-}" = "1" ]; then
echo "no-net.sh: WARNING: unshare --net unavailable and HOTPATH_ALLOW_UNISOLATED=1 —" >&2
echo "no-net.sh: WARNING: running WITHOUT network isolation. Target code can reach the network." >&2
exec "$@"
fi
echo "no-net.sh: cannot create a network namespace (unshare --net --map-current-user failed)." >&2
echo "no-net.sh: refusing to run target code unisolated. Options:" >&2
echo "no-net.sh: - run on a host with unprivileged user namespaces enabled" >&2
echo "no-net.sh: - in CI, run the whole pipeline under docker --network=none" >&2
echo "no-net.sh: - set HOTPATH_ALLOW_UNISOLATED=1 to override (loud warning, not for accepts)" >&2
exit 97
+63
View File
@@ -0,0 +1,63 @@
#!/bin/sh
# Pin an upstream test suite: hash every file under the given repo-root-
# relative paths (typically targets/<t>/workspace/tests/...) into
# corpora/<target>/TESTSUITE.sha256, sha256sum two-space format. diff-test
# verifies this pin before running upstream tests; targets/fetch.sh
# verifies it after every fetch. Run from repo root:
# scripts/pin-testsuite.sh <target> <path>...
set -eu
if [ $# -lt 2 ]; then
echo "usage: scripts/pin-testsuite.sh <target> <path>..." >&2
exit 2
fi
target="$1"
shift
out="corpora/$target/TESTSUITE.sha256"
if [ ! -d "corpora/$target" ]; then
echo "pin-testsuite: corpora/$target does not exist (run from repo root?)" >&2
exit 1
fi
for p in "$@"; do
if [ ! -e "$p" ]; then
echo "pin-testsuite: $p not found (paths are repo-root-relative)" >&2
exit 1
fi
done
# Build the pin out-of-place and install it only on success: a run that fails
# anywhere below must not truncate, replace, or half-write an existing pin —
# that pin is what diff-test refuses to run against on mismatch.
list="$out.files.$$"
tmp="$out.new.$$"
trap 'rm -f "$list" "$tmp"' EXIT INT TERM
# LC_ALL=C: byte-order sort, so the same tree pins identically on every
# machine. -print0/-z: filenames with spaces or newlines survive intact.
find "$@" -type f -print0 | LC_ALL=C sort -z > "$list"
count=$(tr -dc '\0' < "$list" | wc -c | tr -d ' ')
if [ "$count" -eq 0 ]; then
# Was: `find ... | xargs -0 sha256sum > "$out"` — with no matches, xargs
# still ran sha256sum with zero file arguments, so it hashed STDIN (the
# empty pipe) and wrote a bogus "<sha-of-empty> -" entry, then reported
# SUCCESS. That is a pin of nothing that later "verifies" by reading
# stdin. Refuse instead, and leave any existing pin untouched.
echo "pin-testsuite: no regular files under: $* — refusing to write $out" >&2
exit 1
fi
# --text is GNU's default on Linux (a no-op there); it is spelled out because
# sha256sum defaults to *binary* mode on some platforms, emitting "<hash> *path"
# — a one-space-plus-asterisk form that diff-test's pin verifier (which splits
# on the two-space separator) does not parse. Paths stay repo-root-relative:
# find was handed repo-root-relative paths and we run from the repo root.
xargs -0 sha256sum --text -- < "$list" > "$tmp"
lines=$(wc -l < "$tmp" | tr -d ' ')
if [ "$lines" -ne "$count" ]; then
echo "pin-testsuite: hashed $lines of $count files — refusing to write a partial pin" >&2
exit 1
fi
mv "$tmp" "$out"
echo "wrote $out ($count files)"
echo "pinning a test suite is a deliberate human action (corpora/README.md):"
echo "commit the new pin with justification."
+6
View File
@@ -37,6 +37,12 @@ workload = "5.6 MB synthetic JSON (20k records: nested objects/arrays, escaped/u
tests = "cmake -S targets/cjson/workspace -B targets/cjson/testbuild -DENABLE_CJSON_TEST=On -DCMAKE_BUILD_TYPE=Debug >/dev/null 2>&1 && cmake --build targets/cjson/testbuild -j >/dev/null 2>&1 && ctest --test-dir targets/cjson/testbuild --output-on-failure" tests = "cmake -S targets/cjson/workspace -B targets/cjson/testbuild -DENABLE_CJSON_TEST=On -DCMAKE_BUILD_TYPE=Debug >/dev/null 2>&1 && cmake --build targets/cjson/testbuild -j >/dev/null 2>&1 && ctest --test-dir targets/cjson/testbuild --output-on-failure"
# Golden: full re-serialization + checksum of the corpus, byte-hashed. # Golden: full re-serialization + checksum of the corpus, byte-hashed.
golden = "{binary} corpora/cjson/input/big.json 1" golden = "{binary} corpora/cjson/input/big.json 1"
# Differential fuzz, pipeline-integrated: diff-test substitutes {iters},
# {baseline} (the pristine binary the accept path rebuilds) and {candidate},
# then parses the script's final FUZZ-RESULT line. The harness owns where the
# pristine build lands, so the script takes the baseline from argv and never
# hardcodes a path; paths may be repo-root-relative or absolute.
fuzz = "python3 scripts/diff-fuzz-cjson.py targets/cjson/fuzz-work {iters} {baseline} {candidate}"
[corpus] [corpus]
manifest = "corpora/cjson/MANIFEST.sha256" manifest = "corpora/cjson/MANIFEST.sha256"
+6
View File
@@ -33,6 +33,12 @@ workload = "Pro Git book, all translations, 11.0 MB concatenated markdown, singl
tests = "cargo test --release --manifest-path targets/comrak/workspace/Cargo.toml" tests = "cargo test --release --manifest-path targets/comrak/workspace/Cargo.toml"
# Golden replay: corpus through the binary, stdout must be byte-identical. # Golden replay: corpus through the binary, stdout must be byte-identical.
golden = "cat corpora/comrak/progit.md | {binary}" golden = "cat corpora/comrak/progit.md | {binary}"
# Differential fuzz, pipeline-integrated: diff-test substitutes {iters},
# {baseline} (the pristine binary the accept path rebuilds) and {candidate},
# then parses the script's final FUZZ-RESULT line. The harness owns where the
# pristine build lands, so the script takes the baseline from argv and never
# hardcodes a path; paths may be repo-root-relative or absolute.
fuzz = "python3 scripts/diff-fuzz-comrak.py targets/comrak/fuzz-work {iters} {baseline} {candidate}"
[corpus] [corpus]
manifest = "corpora/comrak/MANIFEST.sha256" manifest = "corpora/comrak/MANIFEST.sha256"
+12
View File
@@ -21,4 +21,16 @@ grep -A0 '^"vendor/' "$toml" | while IFS= read -r line; do
git -C "$ws/$sub" fetch -q origin "$scommit" 2>/dev/null || true git -C "$ws/$sub" fetch -q origin "$scommit" 2>/dev/null || true
git -C "$ws/$sub" checkout -q "$scommit" git -C "$ws/$sub" checkout -q "$scommit"
done done
# Upstream test-suite pin: verify against repo root when present; a
# mismatch means the fetched suite is not the one that was audited.
suite="corpora/$name/TESTSUITE.sha256"
if [ -f "$suite" ]; then
if ! sha256sum -c --quiet "$suite"; then
echo "fetch: upstream test-suite pin MISMATCH for $name ($suite) — refusing" >&2
exit 1
fi
echo "$name test suite verified against $suite"
else
echo "note: no $suite — pin the suite with: scripts/pin-testsuite.sh $name <paths...>"
fi
echo "$name pinned at $commit" echo "$name pinned at $commit"
+6
View File
@@ -23,6 +23,12 @@ workload = "8 replicas of pinned progit + comrak/src trees, 221 MB, 4360 files,
[gates] [gates]
tests = "cargo test --release --manifest-path targets/tokei/workspace/Cargo.toml" tests = "cargo test --release --manifest-path targets/tokei/workspace/Cargo.toml"
golden = "RAYON_NUM_THREADS=1 {binary} --sort code corpora/tokei/input" golden = "RAYON_NUM_THREADS=1 {binary} --sort code corpora/tokei/input"
# Differential fuzz, pipeline-integrated: diff-test substitutes {iters},
# {baseline} (the pristine binary the accept path rebuilds) and {candidate},
# then parses the script's final FUZZ-RESULT line. The harness owns where the
# pristine build lands, so the script takes the baseline from argv and never
# hardcodes a path; paths may be repo-root-relative or absolute.
fuzz = "python3 scripts/diff-fuzz-tokei.py targets/tokei/fuzz-work {iters} {baseline} {candidate}"
[corpus] [corpus]
manifest = "corpora/tokei/MANIFEST.sha256" manifest = "corpora/tokei/MANIFEST.sha256"