WT-2-metric-extract: eval-metrics extractor (Phase 2 D2)#68
Merged
Conversation
Covers CLI, 36 metric catalog from 12号 §A/§B/§C/§D + 08号 §Lifecycle/Contracted/User-promoted/Semantic/Overhead, 3+ golden fixtures with hand-computed values, security (a)-(h) with SQL injection defense-in-depth. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P0: - Add StateContinuityRate (#9), TimeToFirstTask (#38), SetupFailureRate (#39); catalog now 39 metrics. P1: - Scope line: CSV single row or JSON single object, not JSON-Lines. - Aggregation contract: one output record per invocation, even for empty cohort (row_count=0); §5.1 + §4.4 aligned. - RoutingLatencyP50P95 join: time window, not conversation_id (runs has no conversation_id column). - Fixture 3 null-count arithmetic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Scope §1 acknowledges some data sources not landed. - Metric counts 36 -> 39 propagated everywhere; §8 file manifest corrected (lifecycle 9, overhead 9). - §7 (d) --out policy fully rewritten (parent-exists, overwrite/symlink refusal, no /dev/stdout). - Fixture 3 route_reasons keying uses time-window timestamps. - Fixture 3 null-count reconciled to 31 cells (was drifting). - §2.2 #10 ContractCompleteness cohort projection added with fallback warning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- §2.2 #11/#12/#13 reclassified as upstream-missing (12号 §A3/§A4 not landed); fixture 2 values now null. - §2.1 #9 StateContinuityRate denominator = count(runs) per 08:39; numerator closed-form, populated today. - Fixture 1 StateContinuityRate = 6/10. - Fixture 3 rewritten: 9 populated + 30 null = 39 arithmetic. - '36-column' → '39-metric' in §7 (d). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- §2.1: metric-set membership override for #7 #8 (both lifecycle+overhead). - §2.2 #10 ContractCompleteness: fallback removed; null when join key missing (was silently aggregating over all rows). - §2.2 #15 RecoverySuccessRate: null (needs 12号 §A6). - §2.1 #9 StateContinuityRate: null (needs 12号 §A6). - §3.3 empty-DB: count-metrics with landed upstream = 0, others = null. - §3.1 filter grammar: dropped col = ? (no user placeholders). - Fixtures updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- WrongContextFailureRate: forbidden-cred + stale-capability added. - LifecycleClosureRate: capability_snapshot_hash != '' added. - Metric-set membership: CapabilityReuseRate/RepeatedGenerationRate are lifecycle+user-promoted (08:37-38). - Empty-cohort contract harmonized: count with landed upstream = 0, count with unlanded = null. - Fixture 3 ContractCompleteness null-reason aligned. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- §3.2 metric-set subset table added; §5.1 acceptance criterion #3 aligned so cross-listed metrics don't contradict each other. - §7 (c) AST parse tightened: every Comparison/In/Like/Between/Is node MUST reference a whitelisted column (closes OR-broadening attack via 'run_id = x OR 1 = 1'). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- §2.1 membership + §3.2 subset table: #4 HumanContextSelectionCount and #5 WrongContextFailureRate cross-listed into semantic (08:140). - Scope §1: carves out ContractCompleteness (upstream landed but cohort join key missing → null). - §7 (d) --out: atomic O_CREAT|O_EXCL|O_NOFOLLOW open (mode 0600) as the actual TOCTOU-close; earlier checks are the belt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- §3.3 _notes: two-value closed set (upstream-missing OR denominator-zero); §5.1 + §7 (f) aligned. - §4.4 empty-DB fixture: applies FULL observer schema (was: only WT-1-run-schema DDL, would fail on route_reasons); missing-table branch = null + upstream-missing. - §7 (d) --out step 4: anchor O_CREAT|O_EXCL|O_NOFOLLOW on dir_fd opened O_DIRECTORY|O_NOFOLLOW from resolved parent. - ConfigTouchCount cite 08:237 not 08:91. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- P0: HumanEditCount added as #40 (08:185 / 11号 §6); catalog 39 → 40. - P1: §7 (g) notes emit for both upstream-missing AND denominator-zero. - P1: §4.4 empty-DB fixture: missing-table (upstream) vs empty-table (denominator zero) semantics distinct. - P1: RegistryLookupHitRate denominator = count(runs) per 08:70. - P1: GeneratedCapabilityDefectRate narrowed to reuse+tool-defect per 08:76. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- P0: TokenUsage added as #41 (08:185 token collection line); catalog 40 → 41. - P1: --metric-set user-promoted emits #40 + #41 too. - P1: §2.5 all 6 non-routing overhead metrics carry {p50, p95, count} structured shape per 12号 §D7; §3.3 flatten list lists all 10 structured. - P1: fixture 3 arithmetic: 33 nulls + 8 populated = 41. - P1: empty-DB JSON example shows both null-note reasons. - P1: fixture short IDs canonicalized to sha256 by fixture-builder (evalrun/schema.go:42 validator). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Stale 40 refs corrected to 41 (§3.2, §5.1, §8 manifest, fixture 3). - Empty-DB JSON example cites both null-note reasons. - §7 (b) --observer-db TOCTOU tightened: fd-based /proc/self/fd handoff on Linux; documented plain-path fallback on other OSes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Scope carve-out: NewSQLWriter Go-wiring (14号 §5 item 1) reassigned to follow-up Go-only worktree; this Python worktree stays Python-only. - Metric-set: #1 TaskSuccessRate + #3 TimeToCompletion cross-listed into user-promoted per 08:185-186; §3.2 subset table updated. - §2.5 structured shapes: #35 ObserverOverhead = 5-key (latency p50/p95 + CPU delta + mem delta + count per 08:88); #36 ModelProxyOverhead = 6-key (first-token p50/p95 + tokens/sec p50/p95 + e2e p95 + count per 08:89). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- §2.5 structured shapes acknowledge per-metric variable sub-column counts (3/3/3/3/5/6/3 = 26 total). - §7 (c) tautology guard: column-column comparisons rejected (col1 = col2 → ErrRunsFilterColumnCompare); only col = literal, col IN (literals), col LIKE 'pat' accepted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Test matrix covering §7 (a)-(h) security items 1:1, plus §2 metric catalog coverage on 3 golden fixtures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Plan §2 explicit test-file list including conftest.py + all splits. - Spec §8 two-directory rule (multi-agent/tools/eval/metrics/ + docs/specs/) — the wt1-* precedent already places specs there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…elpers) Package skeleton for the WT-2-metric-extract paper-metric extractor. Establishes: - pyproject.toml with sqlglot<27 pin + pytest markers (perf, linux_only) per plan §4. - paths.py — spec §7 (b) --observer-db validators (realpath scrub, forbidden-prefix reject, magic-bytes probe, O_NOFOLLOW handoff, /proc/self/fd Linux TOCTOU closure) + spec §7 (d) --out validators (parent-realpath, overwrite refuse, symlink refuse, dir_fd-anchored O_CREAT|O_EXCL|O_NOFOLLOW). - db.py — read-only sqlite open helper (PRAGMA query_only belt) + companion_table_status three-way MISSING/EMPTY/PRESENT branch for spec §4.4. - filter.py — spec §7 (c) sanitizer (denylist + parametric extraction + sqlglot AST walk + tautology guard + column-compare guard). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the metric registry per spec §2 (41 metrics) with §3.2 subset- selection helper: - metrics/__init__.py — Metric / MetricResult / Context dataclasses; ALL_METRICS list; metrics_for_set() cross-list resolver. - lifecycle.py — §2.1 (#1..#9): TaskSuccessRate, LifecycleClosureRate, TimeToCompletion (numpy-compat linear percentile), HumanContext- SelectionCount, WrongContextFailureRate (5-tag D4 taxonomy), ArtifactCorrectnessRate populate today; ManualSetupStepCount / ConfigTouchCount / StateContinuityRate emit null + upstream-missing. - contracted.py — §2.2 (#10..#16) all emit null + upstream-missing. - user_promoted.py — §2.3 (#17..#27, #40, #41) all emit null; #40/#41 probe PRAGMA table_info so a future §D8 column landing flips them automatically. - semantic.py — §2.4 (#28..#30): RoutingAccuracy populates from runs; CapabilityRecall / CapabilityPrecision emit null. - overhead.py — §2.5 (#31..#39): only RoutingLatencyP50P95 (#37) populates today (route_reasons time-window join per §2.5 clause). Distinguishes MISSING (upstream-missing) vs EMPTY (denominator-zero) companion-table branch per §4.4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- csv_out.py — spec §3.3 CSV shape (header + 1 data row per invocation,
structured metrics flattened to <metric>.<subkey>); spec §7 (d)
formula-injection escape for cells starting with = + - @ tab CR LF;
_notes companion column with semicolon-separated <metric>: <reason>
pairs from the two-value closed set.
- json_out.py — spec §3.3 single JSON object; fixed key order
{metric_set, row_count, metrics, notes}; notes ALWAYS present
(even when empty {}).
- cli.py — spec §3 extract subcommand; exit codes 0/2/3; per-metric
stderr warnings per spec §7 (g) template; owner-hint table for
upstream-missing warnings; --format required with no default
(silent-typo hazard).
Empty-DB smoke: header row (71 cols) + 1 data row with row_count=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three golden fixtures per spec §4:
- fixture_1.db (§4.1): 10 lifecycle runs, experiment E1/FullLoom;
hand-computed TaskSuccessRate=0.6, LifecycleClosureRate=0.6,
TimeToCompletion.p50=19.0/p95=76.5/mean=29.2/count=10,
HumanContextSelectionCount=8, WrongContextFailureRate=0.3,
ArtifactCorrectnessRate=1.0.
- fixture_2.db (§4.2): 6 E3 runs + 6 task_contracts with 7-field
bitmap; all §2.2 metrics null today.
- fixture_3.db (§4.3): 5 E2 runs + 4 route_reasons with time-window
alignment so 4 dispatched runs each contain one decision; hand-
computed RoutingAccuracy=0.75, RoutingLatencyP50P95={p50:1.5M,
p95:3.7M, count:4}.
Fixture builders apply the FULL observer schema.sql (spec §4.4
rationale — WT-1-run-schema DDL alone would leave route_reasons
absent). Short artifact/contract IDs are canonicalized to sha256
before insertion per spec §4.1 hash-format note. Timestamps derive
from a fixed BASE_TS + offset so rebuilds are deterministic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Test files map 1:1 to plan §2 test matrix, covering the 41-metric catalog (§2.1..§2.5) and every spec §7 (a)-(h) security item: - test_cli_help.py, test_cli.py — CLI help / subcommand / --format / --metric-set (cross-listed via §3.2 subset table). - test_paths.py — §7 (b) --observer-db (forbidden prefixes, magic bytes, TOCTOU) + §7 (d) --out (parent-must-exist, overwrite refuse, symlink refuse, O_EXCL / dir_fd binding). - test_db_readonly.py — §7 (a) read-only pragma + §4.4 companion- table three-way branch (MISSING / EMPTY / PRESENT). - test_filter.py — §7 (c) denylist, whitelist, tautology, column- compare, parametric binding, accepted grammar. - test_metrics_lifecycle.py — fixture 1 golden (6 populated + 3 null). - test_metrics_contracted.py — fixture 2: all 7 §2.2 metrics null. - test_metrics_semantic.py — fixture 3: RoutingAccuracy + 2 null. - test_metrics_user_promoted.py — fixture 3: all 13 §2.3 metrics null. - test_metrics_overhead.py — fixture 3: RoutingLatencyP50P95 populated; #35/#36 subkey-count sanity; #38/#39 null. - test_metrics_golden_fixture_3.py — 8 populated + 33 null = 41 total. - test_csv_out.py — formula-injection escape (=/+/-/@/tab/CR/LF); header-order stability; _notes shape. - test_json_out.py — key order + notes always present. - test_metrics_null_contract.py — §7 (f) null-not-NaN + closed-set reasons + count-zero-vs-null. - test_empty_db.py — §5.1 empty-DB contract (header + 1 data row, landed count = 0, unlanded count = null). - test_integration.py — end-to-end fixture 1/2/3 + --runs-filter cohort narrowing + stderr warning template. - test_perf_gated.py — @Perf mark, skipped by default (§7 (h)). Test count: 100 passing + 1 perf-skipped. Empty-DB smoke: `python -m eval_metrics extract --observer-db <empty> --format csv` emits header + 1 data row with row_count=0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round-11 spec change gave the wrong hex expansion for the illustrative sha256(b'a1'); the formula-of-record (hashlib.sha256(short.encode()).hexdigest()) was correct, only the example literal was off. Fixture-builder used the formula, so no code change needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P0: --runs-filter tautology guard bypassed by bare TRUE / FALSE and 'run_id = x OR TRUE'. Fixed by adding _reject_non_predicate_leaves tree-shape walker: every leaf of the AND/OR/NOT/Paren boolean tree must be a predicate node (EQ/NEQ/GT/GTE/LT/LTE/In/Like/ILike/Between/ Is). Boolean literals and bare numeric literals at boolean positions now raise ErrRunsFilterTautology. Added 6 regression tests. P1: linear_percentile emitted binary-float dust (e.g. 76.499999... instead of 76.5); did the math in Fraction-space and round the final float to 9 decimals. Emitted values now match spec §4 exact decimals. P1: TOCTOU tests test_observer_db_symlink_swap_after_resolve and test_out_dir_fd_bind were happy-path only. Rewrote to actually swap inodes / rename parents between resolve and open; verify the O_NOFOLLOW / dir_fd rejection actually fires (OSError / ELOOP). Added test_out_dir_fd_bind_happy_path for the non-race path. 107 passed, 1 skipped (perf mark). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…grammar workload_id IS NOT NULL was a schema-tautology bypass (the runs DDL declares workload_id NOT NULL, so IS NOT NULL selects the full cohort while looking like a narrowing filter). Spec §3.1 grammar only names =, IN, LIKE, AND/OR — IS was never part of it. Removed exp.Is from _PREDICATE_TYPES; the tautology walker rejects any IS predicate at the tree-shape check. Added 2 regression tests. 109 passed, 1 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- test_perf_gated: -m perf now enables via os.sys.argv sniff (not just CI=true). - cli.py: --out open failure returns exit 2 (validation) not exit 3 (write); spec §3 exit codes. - metrics/__init__.py: catalog order = §2 section-order (2.1, 2.2, 2.3 incl #40+#41, 2.4, 2.5) not tail-appended. - filter.py: added rationale for accepting !=/BETWEEN/ILIKE beyond spec §3.1 grammar (convenience; tautology/column-compare walkers still apply uniformly). 109 passed (default), 1 passed (-m perf), 1 skipped (-m perf default). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bypass class: 'run_id = run_id || ""' / 'run_id = COALESCE(run_id, "")' / 'run_id = LOWER(run_id)' all evaluate to schema-tautologies while looking like narrowing filters. Prior tautology walker only rejected Column-Column direct comparisons and let function-wrapped columns slip through. Fix: added _enforce_predicate_shape — LHS must be a bare Column, RHS must be a bare Literal/Placeholder/Null/Boolean/Neg (or list of these for IN). No functions, no concatenation, no expressions. Applied to EQ/NEQ/GT/GTE/LT/LTE + In + Like/ILike + Between. 7 regression tests added covering: || concat, COALESCE, LOWER(col), LOWER(literal), IN subquery, BETWEEN with column bound, LHS function. 116 passed, 1 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bypass: 'run_id = -workload_id' / 'run_id = -(-run_id)' — the round-4
_RHS_ATOMIC_TYPES allowed exp.Neg as atomic without recursing into
its child, so Neg(Column) slipped through. For numeric-only run_ids
this becomes a self-tautology on the negated pair.
Fix: replaced isinstance(rhs, _RHS_ATOMIC_TYPES) with _is_bare_rhs
helper that recurses through Neg and Paren wrappers and only accepts
if the innermost node is a bare Literal/Placeholder/Null/Boolean.
Neg-of-literal ('run_id = -1', 'run_id = -(1)') still works;
Neg-of-column / Paren-of-column rejected.
4 regression tests added.
120 passed, 1 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P0-1: PRAGMA query_only was set but never verified. A silent PRAGMA failure would leave the connection writable. open_observer_db now reads PRAGMA query_only after setting it and refuses to return a connection where the value is not 1. Regression test added. P0-2: --out parent-inode was resolved at validate time but re-opened by pathname at open time — a regular-directory parent swap between validate and open would land the file in the attacker's replacement dir. Refactored: validate_out_path now returns an OutTarget carrying a pinned dir_fd (opened O_RDONLY|O_DIRECTORY|O_NOFOLLOW during validation); open_out_file uses target.dir_fd directly. Any parent swap after validate can only redirect the pathname — the dir_fd still points at the original inode, so the file lands where we validated. lstat is also now dir_fd-relative (os.stat with dir_fd and follow_symlinks=False), pinning the symlink check too. Added tests: test_out_dir_fd_bind_rejects_regular_dir_parent_swap (the exact bypass class), refactored test_out_dir_fd_bind_rejects _symlink_parent_swap to assert the file lands in the RENAMED original (not the replacement), and test_out_atomic_create_race for the O_EXCL between-check-and-open race. 122 passed, 1 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- filter.py: error message no longer mentions 'or IS' (IS is intentionally rejected; the message would confuse operators). - lifecycle.py: hoisted Fraction import to module scope (was inside linear_percentile call — trivial style). - spec §3.1: grammar acknowledges NOT and parenthesised combinations (implementation already accepted them; spec caught up). - spec §3.1: added 'Denylist-inside-literal' trade-off paragraph documenting that literals containing SQL keywords / comment tokens are rejected too, and operators should avoid the 20 keywords when naming workload / experiment / claim IDs. 122 passed, 1 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P1: --runs-filter denylist now rejects user-supplied bind markers (?, :name, @name, $name) pre-parse. Without this, 'run_id = ?' compiled cleanly with 0 params then crashed at cursor.execute — violating spec §3.1 'users cannot supply ?'. P1: compile_runs_filter wraps RecursionError as ErrRunsFilterParseError. Deep-nested fragments (e.g. 5000 parens) now exit 2 cleanly instead of exit 1 with a Python stack trace leaking sqlglot module paths. P2: _reject_non_predicate_leaves caps recursion at _MAX_TREE_DEPTH=32 (NOT-chain depth guard). P2: IN () empty list rejected explicitly (was silent zero-cohort). P2: cli.py compute loop distinguishes sqlite3.Error from generic exceptions — schema drift surfaces distinctly instead of being buried under upstream-missing. P2: spec §3.1 grammar acknowledges !=, <, <=, >, >=, ILIKE, BETWEEN; corrects misleading 'AND after its own filter' — no built-in filter exists. 7 new regression tests. 129 pass, 1 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- filter.py: drop exp.Boolean and exp.Null from _RHS_ATOMIC_TYPES. Spec §3.1 grammar never enumerated them; run_id = TRUE / NULL silently returned 0 rows (implementation drift from spec). - filter.py: reject qualified column references (col.table or col.db non-empty) at compile-time. runs.run_id / route_reasons. run_id / main.runs.run_id previously slipped past the whitelist (leaf name matched) then failed at sqlite3 execute-time with 'no such column'. Now cleanly rejected as ErrRunsFilterFieldNotAllowed. - spec §3.1 denylist paragraph aligned with §7 (c) full 20-token list. - test_field_whitelist_sqlite_master_reject pinned to ErrRunsFilterDenylist (was accepting either exception). 7 new regression tests: TRUE/FALSE/NULL RHS reject; NULL in IN reject; runs./route_reasons./main.runs. qualified reject. 136 pass, 1 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 2 #6 · 12号 §D2 · Owner: WT-2-metric-extract
Scope
multi-agent/tools/eval/metrics/(moduleeval_metrics).runs,route_reasons,capability_snapshots,task_contracts, …) → emits paper-metric CSV or JSON.multi-agent/tools/eval/metrics/+docs/specs/wt2-metric-extract.{spec,plan}.md.Metric coverage (41)
Union of 08 号 §Metrics + §Experiments E1–E6 + 12 号 §A/§B/§C/§D + 11 号 §6 (E4 Stage-A
HumanEditCount/token):HumanEditCount+ feat(eval): §F4 ground-truth labels + JSON Schema validation #41TokenUsageCross-section membership (per spec §3.2 subset table):
TaskSuccessRate/TimeToCompletion/HumanContextSelectionCount/WrongContextFailureRate/ManualSetupStepCount/ConfigTouchCount/CapabilityReuseRate/RepeatedGenerationRateare cross-listed so--metric-set semantic|overhead|user-promoted|lifecycleall pick them up per 08:140 / 08:87 / 08:37-38.Metrics whose upstream data source has not landed (12号 §A3 / §A4 / §A6 / §B / §C4 / §D6c / §D7 / §D8) emit
nullwith_notes: \"upstream data missing\"and a stderr warning naming the owner. Metrics with landed upstream but empty cohort emitnullwith\"denominator zero\". The two reasons are a closed set — everynullcell has exactly one.CLI
Exit codes: 0 success / 2 validation / 3 write-time I/O.
Security (spec §7 (a)–(h))
Defense-in-depth on
--runs-filter(the flagship attack:--runs-filter \"1=1; UPDATE runs SET success_oracle_result='pass'\"would silently corrupt the paper's evidence base):;,--,/*,*/, 20 DDL/DML keywords case-insensitive?placeholder<27)run_id,workload_id,claim_id,experiment_id,baseline_or_ablationTRUE,run_id='x' OR TRUE)1=1)workload_id IS NOT NULL(schema-tautology on NOT NULL columns)col = LOWER(col),col = col || '',col = COALESCE(col,''),col = -workload_id,col = -(-col), function-wrapped LHS--observer-db: realpath scrub →/etc /proc /sys /devreject →O_NOFOLLOWmagic-byte probe →/proc/self/fd/<fd>handoff to sqlite3 on Linux (Codex round-6 P0 tripwire) →PRAGMA query_only=ON+ read-back verification.--out: parent realpath (must exist, nomkdir -p) → forbidden-subtree reject →O_RDONLY|O_DIRECTORY|O_NOFOLLOWdir_fd pin → dir_fd-relativefstatat/lstatsymlink/overwrite refusal → dir_fd-relativeO_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOWopen at mode 0600. Parent swap between validate and open lands the file in the ORIGINAL inode, not the attacker's replacement.CSV formula-injection escape covers 7 prefix chars (
=+-@\\t\\r\\n), matchingcmd/evalrun-export/main.go:283.Fixtures + tests
fixture_{1,2,3}.db+expected_{1,2,3}.json+build_fixture_{1,2,3}.py) exercise the full 41-metric matrix. Fixture-builder canonicalises short IDs to sha256 to satisfyevalrun/schema.go:42^[a-f0-9]{64}$.-m perforCI=true).Fractionto avoid IEEE-754 dust (76.5not76.49999999999997;3700000.0not3699999.999999999).Handoff carve-out
14号 §5 handoff item 1 (
internal/evalrun.NewSQLWriterGo-side wiring) is reassigned to a follow-up Go-only worktree; this Python-only worktree consumes whatever the SQLWriter writes toruns, whether via PR #53's stub CSV or the future real SQL writer.Codex review trajectory
Every code-review P0 covered a real security bypass in the
--runs-filterguard or--observer-db/--outTOCTOU handling; each was fixed with a targeted regression test.Verification
🤖 Generated with Claude Code