Commit 18ba057
authored
* fix(proof): close four evidence-integrity holes (#952)
Waiver expiry: compared local-date isoformat with <=, so a waiver expiring
today was already expired and spuriously blocked the #731 merge gate on its
last valid day. Now UTC, compared as dates, and < — the expiry date is the
last VALID day.
run_id: str(uuid4())[:8] is 32 bits, so runs collide around 600 by the
birthday bound and a passing run can absorb a failing run's artifacts. Both
generation sites now use a shared _new_run_id() returning a full UUID.
Checksums: computed and stored but never verified on read, so the integrity
claim was unenforced. Added verify_evidence + EvidenceTamperError, called from
check_obligation_satisfied so a tampered artifact cannot satisfy a gate. Per
the issue's design note the digest binds run id, gate and canonical path — not
bytes alone — so a genuinely passing artifact cannot be transplanted into
another run. Verification also accepts the pre-#952 bytes-only digest so
existing evidence does not all become 'tampered' at once; new writes are always
bound, so that path drains.
Stubs: title/description were interpolated raw. A newline escaped whatever
line context held them and the next line landed as code; an apostrophe broke
the E2E test('...') literal open. Now collapsed to one line with docstring
delimiters neutralized, and the JS literal built with json.dumps.
30 tests, RED first: a waiver expiring today, two runs' evidence staying
separable, a mutated/deleted/transplanted artifact, and a title carrying a
quote and a newline through every gate's template (Python stubs must still
ast.parse).
* fix(proof): enforce checksum verification on the path that gates merges (#952)
codex review P1, and correct: the first cut wired verify_evidence into
check_obligation_satisfied, which has NO production callers. The #731 merge
gate asks only for OPEN requirements, so a requirement already recorded
SATISFIED kept that status after its artifact was edited or deleted, and the
merge went through until someone happened to run a full proof again.
Added evidence.list_blocking_requirements: OPEN requirements plus SATISFIED
ones whose evidence fails verification. Both merge gates (API pr_v2 and CLI
pr_commands) now use it instead of the raw status query, with a test asserting
neither goes back.
A SATISFIED requirement with no evidence rows deliberately does not block —
that is a pre-existing state in older ledgers, not a tamper signal, and
treating it as one would wedge every workspace that has it.
339 proof/PR tests pass.
* fix(proof): judge the merge gate on the latest evidence per gate (#952)
codex review P2, and a worse failure than the one it guards: blocking on any
historical evidence row meant deleting last month's superseded artifacts —
routine housekeeping — would wedge the merge gate permanently.
list_blocking_requirements now takes the newest passing row per gate
(list_evidence returns newest first) and verifies only those. Judged per gate,
so a stale-but-intact UNIT artifact cannot mask a tampered SEC one.
Three tests: cleanup of a superseded artifact does not block, tampering with
the current one still does, and a tampered SEC artifact blocks even when UNIT
is fine.
* fix(proof): handle text ending at a docstring delimiter, and say why a req blocks (#952)
CI review on #1080.
A description ending in a single quote is not a """ run, so _inline did not
touch it — and six templates butt {description} straight against their own
closing delimiter. Four quotes in a row: Python closes the docstring on the
first three and the fourth opens an unterminated literal, i.e. a SyntaxError
in the generated stub, the exact failure AC4 exists to prevent. A trailing
backslash escapes the delimiter's first quote for the same result. Both now
get one separating space. Reproduced before fixing; 24 new parametrized cases
across the six affected gates, for title as well as description.
Also: both merge gates kept saying 'N open requirement(s) block this merge'
after switching to list_blocking_requirements, which now also returns
requirements recorded SATISFIED whose evidence fails verification — so a user
blocked by a tampered artifact saw a status that did not match the ledger.
Renamed the variable and reworded both messages to name either cause.
* fix(proof): verify evidence on the public read paths too (#952)
codex review, and AC3 says reads specifically. Verification reached the merge
gate but not GET /api/v2/proof/requirements/{id}/evidence or
/runs/{id}/evidence — both serialized satisfied straight from the ledger and
served the artifact's CURRENT bytes beside it. Editing a file after a passing
run therefore showed a pass in the UI next to forged text.
EvidenceResponse gains verified + tamper_detail, computed per record in both
endpoints. When a record fails verification its artifact_text is withheld
rather than rendered: the bytes on disk are not the ones the record attests
to, so showing them is the forgery.
Reported per record rather than raised, so one bad artifact does not 404 a
whole run's evidence list — pinned by a test where one gate is tampered and
the other still reads clean.
6 new tests; tests/ -k 'proof or pr_v2 or pr_commands': 357 passed.
* fix(proof): stop serving tampered evidence as a pass, and surface it in the UI (#952)
codex review, and the sharpest finding of the set: adding verified:false beside
an unchanged satisfied:true fixed nothing a user sees. Every existing client
renders green from satisfied/status and knows nothing about the new fields, so
tampered evidence still displayed as passing proof — just without its text.
Backend: a record failing verification now serializes satisfied=false and
status='unverifiable' — the existing vocabulary for 'this obligation could not
be checked', which every client already renders as not-green — while
verified/tamper_detail carry the precise reason for clients that look. So even
an un-updated client can no longer show it as proof.
Web UI: ProofEvidence gains verified/tamper_detail; GateEvidencePanel shows a
distinct red 'tampered' badge (not the amber 'cannot verify', which means
something else) and, when expanded, explains that the contents are withheld
because the artifact no longer matches its checksum.
Backend: 360 proof/PR tests pass. Web UI: 1103 tests pass, production build
succeeds. The 25 pre-existing tsc errors are all in unrelated test files.
* fix(proof): treat an unreadable artifact as tamper, not a crash (#952)
codex review. FileNotFoundError is only one kind of OSError. An artifact
replaced by a directory, or chmodded unreadable, still exists — read_bytes then
raises IsADirectoryError/PermissionError, which escaped verify_evidence. That
500'd the evidence endpoints and took down the merge gate before it could block
the specific requirement or honor an override.
Catching OSError covers all three (FileNotFoundError is a subclass), and an
artifact we cannot read is exactly an artifact we cannot verify.
Three tests: directory-in-place-of-file, an unreadable file (skipped where the
environment ignores mode), and the merge gate naming the requirement rather
than raising.
* fix(proof): escape backslashes in stub text instead of padding them (#952)
CI caught what my local run did not: appending a space after a trailing
backslash produced '\ ' inside a non-raw docstring — an invalid escape
sequence, which this repo's pytest config escalates from SyntaxWarning to an
error. My fix for an escaping bug introduced a different escaping bug.
Padding a dangerous character is not escaping it. _inline now escapes every
backslash ('\\' renders identically when the docstring is read), which also
covers Windows paths and regexes anywhere in the text, not just at the
delimiter boundary. The trailing-quote case keeps its separating space, which
is safe.
The tests were too weak to catch it: ast.parse succeeds on source that merely
warns. They now assert under warnings.simplefilter('error') and cover
'C:\\path', 're: \\d+' and '\\n' as well as the boundary tails.
Also records the lesson, including: run the CI invocation locally, not a
single-file subset — warning behaviour differs.
* fix(proof): apply stub escaping per context, exactly once (#952)
CI review, and a precise catch: my backslash-doubling commit made _js_string
run on _inline's already-doubled output, so json.dumps escaped those doubles
again. A title of '\d+' reached the .ts source as four backslashes and read
back as '\\d+'. The markdown templates had the same problem in reverse —
they got Python-docstring escaping they have no use for.
Split the escaping: _collapse is the context-neutral half (one line, no
docstring-closing sequence), and each context applies its own escaper to that,
once. Python templates get _inline (backslash-doubled, trailing quote padded),
the E2E literal gets _js_string(raw), markdown gets _collapse.
Tests assert round-trip fidelity rather than absence of a bad character: the
E2E literal must json.loads back to exactly the original title, the Python
docstring must ast.get_docstring back to exactly the original description, and
markdown must contain the original verbatim.
* fix(proof): E2E comments are not Python either (#952)
CI review follow-up. The E2E template's {title}/{description} sit in '//'
comments, but they were still getting _inline's backslash-doubling — which a
JS comment shows verbatim. My own docstring said 'per context, applied once',
and that was the one context still getting the wrong escaper.
_MARKDOWN_GATES becomes _NON_PYTHON_GATES and includes E2E. Only the Python
templates need doubling; {title_js} stays on _js_string, being the one field
that really is a string literal.
1 parent 6663367 commit 18ba057
12 files changed
Lines changed: 1291 additions & 59 deletions
File tree
- codeframe
- cli
- core/proof
- ui/routers
- tests
- core
- ui
- web-ui/src
- __tests__/components/proof
- components/proof
- types
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
432 | 432 | | |
433 | 433 | | |
434 | 434 | | |
435 | | - | |
436 | | - | |
437 | 435 | | |
438 | 436 | | |
439 | 437 | | |
| |||
455 | 453 | | |
456 | 454 | | |
457 | 455 | | |
458 | | - | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
459 | 461 | | |
460 | 462 | | |
461 | 463 | | |
462 | 464 | | |
463 | | - | |
| 465 | + | |
464 | 466 | | |
465 | 467 | | |
466 | 468 | | |
467 | 469 | | |
468 | | - | |
| 470 | + | |
469 | 471 | | |
470 | | - | |
| 472 | + | |
471 | 473 | | |
472 | | - | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
473 | 479 | | |
474 | 480 | | |
475 | 481 | | |
476 | | - | |
| 482 | + | |
477 | 483 | | |
478 | | - | |
| 484 | + | |
479 | 485 | | |
480 | 486 | | |
481 | 487 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
15 | 27 | | |
16 | 28 | | |
17 | | - | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
18 | 34 | | |
19 | 35 | | |
20 | 36 | | |
| |||
23 | 39 | | |
24 | 40 | | |
25 | 41 | | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
26 | 106 | | |
27 | 107 | | |
28 | 108 | | |
| |||
41 | 121 | | |
42 | 122 | | |
43 | 123 | | |
44 | | - | |
| 124 | + | |
45 | 125 | | |
46 | 126 | | |
47 | 127 | | |
| |||
53 | 133 | | |
54 | 134 | | |
55 | 135 | | |
56 | | - | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
57 | 144 | | |
58 | 145 | | |
59 | | - | |
60 | | - | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
61 | 154 | | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
703 | 703 | | |
704 | 704 | | |
705 | 705 | | |
706 | | - | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
707 | 709 | | |
708 | 710 | | |
709 | 711 | | |
| |||
720 | 722 | | |
721 | 723 | | |
722 | 724 | | |
723 | | - | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
724 | 731 | | |
725 | 732 | | |
726 | 733 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
30 | 43 | | |
31 | 44 | | |
32 | 45 | | |
| |||
270 | 283 | | |
271 | 284 | | |
272 | 285 | | |
273 | | - | |
| 286 | + | |
274 | 287 | | |
275 | 288 | | |
276 | 289 | | |
| |||
0 commit comments