voyage/docs/T2-bakeoff-results.md
Kjell Tore Guttormsen b208e4ee04 fix(voyage): S21 — close IPv4-mapped IPv6 SSRF bypass + security/safety audit (blind spot #2)
S21 = devil's-advocate blind spot #2 (security/safety), operator scope
"security-core": fix genuine defects, honestly record the rest.

SSRF fix (CWE-918). validateOtlpEndpoint classified the host by literal
string match. Decimal/hex/octal/trailing-dot encodings are already caught
(the WHATWG URL parser canonicalizes them), but IPv4-mapped IPv6 literals
(::ffff:127.0.0.1, ::ffff:192.168.x, ::ffff:169.254.169.254) render as
::ffff:HHHH:HHHH and matched neither the loopback set, the RFC-1918 regex,
link-local, nor HARD_BLOCKED_HOSTS — they passed over https and would reach
loopback / private / cloud-metadata, defeating the comment's "PERMANENTLY
block metadata" promise. Added mappedV4() to decode the embedded IPv4
(dotted + hex-pair forms) and classify on it. TDD: 4 tests failing-first —
mapped loopback/RFC-1918/metadata rejected (metadata stays HARD_BLOCKED even
with VOYAGE_OTEL_ALLOW_PRIVATE=1), mapped public ::ffff:8.8.8.8 still valid
(no over-block). Threat model narrow (endpoint is operator-set env; export
opt-in; a brief cannot set env).

Survivor #18 honest-residual. T2-bakeoff §3 "Classifier interference: 0"
read as satisfied, but the auto/bypass-mode re-run that matters for headless
trekreview was never run (mode is operator-set, not settable in-session) —
footnoted, not gating. Per recommendation #9, moved to an OPEN RESIDUAL
(untested), guarded by a new doc-consistency pin.

Audit record. ## S21 resolution block in devils-advocate-results.md
dispositions all four sub-questions: SSRF (fixed), hooks-block (verified;
advisory-rail residuals: Write-only matcher, Bash-redirect, regex gaps — by
design), malicious-brief-headless (catastrophe-blocked, exfil NOT blocked —
inherent limit). S21b flagged (NOT done): operations.md:15 mis-describes
autonomy-gate.mjs state machine + --gates table.

Test count: real baseline 700 (698 pass / 2 skip), NOT 715 — the node:test
headline carried in S20 commit msgs was stale/miscounted (census figures
were correct). Now 705 (703 pass / 2 skip / 0 fail); census behavior
601->605, doc-pins 71->72, total 672->677.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqBYc8Ltrk7LipyJmGxXiB
2026-06-19 20:02:56 +02:00

13 KiB
Raw Blame History

T2 / NW2 — prose-vs-Workflow bake-off results

Status (S10): Part A (build + smoke) and part B (full ≥3-runs/arm measurement) COMPLETE. Operator GO 2026-06-18 (choice "a" — full ≥3× run). T2 §5 verdict: opt-in-defensible (POSITIVE on the measured axes, on a single un-archived run — see §Reproducibility caveat) — the Workflow substrate is fidelity-neutral on what was measured; proceed to the opt-in --workflow flag at S11. Full evidence in §Full run (S10 part B) at the end of this doc. The smoke section below is retained for history.

Resolves: docs/W1-narrow-wins-plan.md §S10 (build + de-risk and full measurement).


Setup

Item Value
Fixture tests/fixtures/bakeoff/ — real diff of commit b149538 (NW1) + brief reconstructed from docs/W1-narrow-wins-plan.md §S9
Delivered diff 3 files, +438/-3 (commands/trekreview.md, lib/review/findings-schema.mjs, tests/lib/findings-schema.test.mjs)
Triage all 3 files summary-only (deterministic Phase-4 classifier)
Model opus (reviewer + coordinator agent defaults)
Arm A (prose) reviewers spawned via Agent tool, prose trailing-JSON contract (validated by NW1 findings-schema)
Arm B (Workflow) scripts/trekreview-armB.workflow.mjs via Workflow tool: parallel([conformance, correctness]) schema-forced → JS dedup-by-triplet → agent(review-coordinator) verdict schema
Fidelity metric lib/review/fidelity-diff.mjs fidelityDiffStructured (verdict + jaccard over (file,line,rule_key)-IDs + severity/rule cross-check)

Smoke results (single run per arm)

Arm A (prose) Arm B (Workflow)
Reviewers run 2 2
Raw findings 0 1
After coordinator 0 (coordinator moot — 0 findings) 0 (coordinator dropped the 1 finding)
Verdict ALLOW ALLOW
Agents 2 3 (2 reviewers + coordinator)
Subagent tokens ~72.3k (34.3k + 38.0k; no coordinator) ~100.9k (incl. coordinator)
Wall-time ~70 s (parallel reviewers) ~157 s (full pipeline)

PRIMARY metric — output fidelity: EQUIVALENT

fidelityDiffStructured(ArmA, ArmB) =
  { verdictMatch: true, jaccard: 1, countA: 0, countB: 0,
    severityMismatches: [], ruleKeyMismatches: [], equivalent: true }

Caveat — thin finding surface. Both arms returned 0 final findings on this clean, TDD'd fixture, so fidelity is confirmed only at the verdict level (ALLOW ≡ ALLOW); the finding-set fidelity is trivially equal at zero and was not stressed. A reviewer-level divergence did appear (Arm B raised 1 raw finding, its coordinator filtered it; Arm A raised 0) — masked at the verdict level. Quantifying that divergence is exactly what the full run on a richer-finding-surface fixture must do.

Secondary metrics (smoke, single-run — not medians)

  • JSON-robustness (the F2 win): Arm B's reviewers were schema-forced (StructuredOutput) — typed findings, zero JSON.parse; the 1 raw finding + the coordinator verdict both conformed with no re-ask. Arm A's trailing-JSON validated clean via NW1 findings-schema. Win demonstrated structurally; the parse-error/re-ask delta needs a fixture that actually provokes malformed JSON.
  • Classifier interference: 0. Arm B's 2-agent fan-out + coordinator (3 agents) ran with no denied/missing spawns. Confirms S8 F4 for trekreview's small fan-out under the default mode. (auto/bypass still to be checked in the full run.)
  • Token cost: preliminary and not yet comparable — Arm B ran a coordinator (on its 1 finding) that Arm A did not. Single run; no medians.
  • Control/visibility: Arm B runs in the background; intermediate findings are visible in the workflow transcript + /workflows. Operator-gate (the review.md write) is unaffected — both arms return structured {verdict, findings} and Phase 7 rendering stays shared/prose.

Smoke verdict

SMOKE PASS — machinery validated. Arm B (Workflow substrate) runs the full Phase 56 pipeline end-to-end, fidelity-equivalent to Arm A at the verdict level, with zero classifier interference. The build is sound: NW1 schema, fidelity-diff, fixture, and the Arm B port all work together.

This is not the T2 §5 POSITIVE/NEGATIVE verdict — that needs the full ≥3-runs/arm measurement with a finding-rich fixture.

Go / no-go recommendation (operator decides)

Recommend: proceed to the full ≥3-runs/arm run (S10 part B) with two changes:

  1. Use a richer-finding-surface fixture (a larger real voyage commit, or seed the fixture with a few genuine issues) so finding-set fidelity is actually stressed — the smoke only proved verdict fidelity at 0 findings.
  2. Match the arms' coordinator path (run Arm A's coordinator too, even at low finding counts) so the token/wall-time comparison is apples-to-apples, and add the auto/bypass classifier-interference check (F4).

If the operator prefers, S11 can instead record "port built + smoke-validated; full measurement deferred" and integrate behind the opt-in --workflow flag on the smoke evidence alone — weaker, but the substrate is demonstrably functional.


Full run (S10 part B) — ≥3 runs/arm, rich-finding fixture

This is the T2 §5 verdict. Operator GO 2026-06-18 (choice "a"). Resolves the measurement half of docs/W1-narrow-wins-plan.md §S10.

Setup (vs smoke)

Item Value
Fixture tests/fixtures/bakeoff-rich/ — JWT-auth brief + diff, 5 seeded blatant issues (varied severity/rule_key, one dual-flaggable). Live reviewers surface 1118 findings/run — the smoke's 0-finding limitation is resolved.
Triage all 3 files deep-review (auth/security surface) — pinned, passed to both arms
Both arms run the coordinator yes (token now comparable; smoke ran A's coordinator only on its 1 finding)
Arm A (prose) foreground reviewers (Agent tool, no name) → validateReviewerOutput (NW1) → scripts/bakeoff-armA-merge.mjs triplet-dedup → foreground review-coordinator. Runs ×3.
Arm B (Workflow) scripts/trekreview-armB.workflow.mjs ×3 (StructuredOutput findings → JS triplet-dedup → coordinator verdict schema)
Analysis scripts/bakeoff-fidelity.mjs — cross-arm + within-arm + granularity ladder

Raw results

Run Arm A (prose) Arm B (Workflow)
1 BLOCK · 13 findings · 86.3k tok · ~119s BLOCK · 11 findings · 96.9k tok · ~230s
2 BLOCK · 12 findings · 92.8k tok · ~150s BLOCK · 14 findings · 91.9k tok · ~201s
3 BLOCK · 16 findings · 95.8k tok · ~188s BLOCK · 18 findings · 97.2k tok · ~249s

All 6 runs → BLOCK. (Arm A tokens = 2 reviewers + coordinator subagent-tokens; wall = reviewer-phase max + coordinator.)

Metrics vs T2 §5

1. PRIMARY — output fidelity

  • Verdict: EQUIVALENT. Verdict-match rate 1.0 — all 9 cross-arm (Aᵢ×Bⱼ) pairs agree, all 6 runs BLOCK. This is the operator-meaningful gate decision.

  • Finding-set — granularity ladder (cross-arm median jaccard):

    Granularity Cross-arm median [min,max] Within-arm median (A / B)
    (file,line,rule_key) triplet 0.41 [0.29, 0.64] 0.40 / 0.32
    (file,rule_key) (ignore line) 0.71 [0.58, 0.91] 0.69 / 0.67
    rule_key set 0.86 [0.86, 1.00]
    file set 1.00
  • Underlying-issue coverage: 5/5 core issues flagged in 6/6 runs (100%) — alg-from-header, soft-fail-200, bcrypt-drift, missing-test, refresh error-handling.

  • rule_key mismatches: 0. severity mismatches: 6/9 pairs × 1 — traced to MISSING_ERROR_HANDLING rated MINOR (catalogue tier) in Arm A vs MAJOR (brief-Constraint framing) in Arm B. Does not change the verdict.

  • Read: the substrate is fidelity-neutral — cross-arm divergence ≤ each arm's own run-to-run nondeterminism at every granularity (cross 0.71 ≥ within 0.670.69 at (file,rule_key)). The low triplet jaccard is line-citation noise shared by both arms (reviewers cite the same defect at line 24/25/26 or 44/46/50), not a substrate effect.

2. JSON-robustness (the F2 win)

  • Arm B: StructuredOutput schema-forced — typed findings, zero JSON.parse, rule_key enum enforced at the tool layer (the agent literally cannot emit an out-of-catalogue key — stronger than NW1's post-hoc check).
  • Arm A: 6/6 reviewer outputs valid via NW1 validateReviewerOutput (0 parse errors, 0 schema errors, 0 re-asks); 3/3 coordinator trailing-json parsed clean.
  • Both arms 0 re-asks this run ⇒ Arm B's win is structural, not a measured re-ask delta (the rich fixture did not provoke malformed JSON; a delta needs a JSON-hostile fixture).

3. Classifier interference: 0 under the session's active mode — auto/bypass UNTESTED (open residual)

Arm B ran 3 concurrent workflows = 9 concurrent agents; Arm A ran 6 concurrent reviewers + 3 concurrent coordinators. No denied/missing spawns in any arm under the session's active mode. Confirms S8 F4 at higher (9-agent) concurrency. Open residual (Survivor #18 / S21, 2026-06-19). The W1 charter made a re-run under auto/bypass permission mode an explicit guard, because trekreview can run headless under exactly those modes. That re-run was never performed — the permission mode is operator-set, not settable from within an interactive session — and was originally footnoted rather than gating. Per the devil's-advocate audit (Survivor #18, recommendation #9) this result is therefore not "metric satisfied": it holds only under the session's active mode. The mode that matters for headless trekreview (auto/bypass, ≥9-agent fan-out) remains untested and stays an open residual until measured from a genuinely headless run. trekreview's small fan-out showed 0 interference in S8 and here; the large fan-out (trekplan swarm) is explicitly out of narrow-wins scope.

4. Token cost (comparable — both ran coordinator)

Arm A median 92.8k subagent-tokens/run; Arm B median 96.9k+4.4% (≤ +15% POSITIVE bar; far below +30% NEGATIVE). Arm A additionally burdens the main context with hand-orchestration (validate/dedup/prompt-build) that Arm B offloads to the workflow runtime — an uncounted Arm-A cost, i.e. a further point for Arm B.

5. Wall-time

Arm A median ~150s/run; Arm B ~230s ⇒ +54%. Caveat: not a controlled per-run comparison — Arm A's reviewers+coordinators were batch-parallelized across the 3 runs, Arm B ran 3 full pipelines concurrently. Arm B is non-blocking (background) and frees the main context for the duration.

6. Control / visibility

Arm B runs in the background; intermediate findings visible in the workflow transcript + /workflows; returns structured {verdict, findings}. Phase 7 rendering stays shared/prose; the operator-gate (review.md write) is unaffected. Arm B frees the main context during the review (the 3 workflows ran while main did other work) — Arm A occupies it end-to-end.

VERDICT: opt-in-defensible (POSITIVE on measured axes; single un-archived run)

The Workflow substrate (Arm B) is fidelity-equivalent to the prose path (Arm A) on the operator-meaningful axes — verdict 1.0, file-set 1.0, issue-coverage 100%, (file,rule_key) jaccard ≥ within-arm — with comparable tokens (+4.4%), zero classifier interference, structurally stronger JSON robustness, and better control/visibility (background + /workflows + frees main context). There is no substrate-attributable divergence: cross-arm ≤ within-arm at every granularity.

Caveat (surfaced per plan §Posture, not hidden): the strict fidelityDiffStructured equivalent flag (triplet-jaccard ≥ 0.7) is 0/9. This is a metric-calibration artifactboth arms score sub-0.7 against themselves at triplet granularity because live reviewers vary line citations and rule_key choice per semantically-identical issue. It is not a regression: the granularity ladder and within-vs-cross comparison isolate the divergence as intrinsic LLM nondeterminism, equal in both arms. Arm B's wall-time is ~+54% but non-blocking.

Reproducibility caveat (Survivor #3, added S17)

The per-run arm outputs (a1.json..b3.json) were never committedgit log --diff-filter=A finds no such file in any ref, and scripts/bakeoff-fidelity.mjs requires them as input. The medians and the jaccard ladder above therefore cannot be regenerated, audited, or falsified from the repo. This is why the verdict is labeled opt-in-defensible rather than a clean POSITIVE: it justifies shipping --workflow behind a flag (the measured axes did come out fidelity-neutral), but it rests on a single un-archived run and is not a reproducible result. To upgrade it, a future session must re-run the bake-off and commit the raw per-run JSON under tests/fixtures/bakeoff-rich/runs/ with a test that re-derives these numbers.

→ S11: proceed with the opt-in --workflow flag (prose stays default — preserves the 2.1.154+ portability floor; see §Open decisions in the narrow-wins plan). The bake-off supports making the Workflow path reachable; residuals are (a) the F4 auto/bypass explicit-mode check and (b) wiring the flag to reuse the NW1 schemas. A future fidelity metric should score at (file,rule_key) granularity (line-noise-robust) rather than the exact triplet.