HOMELAB-2682: fix(sbx-gits): read both ledger formats (forward-compatible suite_relpath) #869

Merged
pi-landing merged 2 commits from plane/HOMELAB-2682-suite-relpath into live 2026-11-06 04:48:00 +00:00
Owner

Root cause (measured read-only on pod sbx-gits-0)

/home/ld/work/.emu-proof/ledger.jsonl line 33 was written by the #859 proof tool while it was live (ArgoCD auto-sync, 03:43Z…03:46Z) and carries a suite_relpath field:

"boot_id":"d2441774-…","suite_relpath":"docs/rung2-herdr-attach-suite.sh","prev":"80e0e08e…","hash":"1fcdfcfc…"

The revert #866 put back the pre-#859 tool, whose chain_text does not know that field. It recomputed the chain without it, got 9297a7fc… instead of the recorded 1fcdfcfc…, and check refused:

- linea 33: hash no cuadra (recalculado 9297a7fc…, escrito 1fcdfcfc…): algun campo de esa linea se edito a mano

A broken chain is the gate itself ("si una linea no encadena, lo que diga esa linea no prueba nada"), so every head failed → no GITS merge could land. Nobody edited the ledger. The line is exactly what the #859 writer produced.

The fix: the same optional-field pattern #853 established

suite_relpath is now an OPTIONAL, RECORDED, HASHED, otherwise inert field, exactly as class and results_sha256 are:

where what
chain_text suite_relpath is argument 21, appended after prev, and after class (19) and results_sha256 (20)
check reads the field off each line and passes it as the 21st chain_text argument; empty/absent → the hashed text is byte-identical to before
run records it (path of the executed suite relative to the tree, new rel_under helper) when the suite lives inside the checkout; omits the field when it does not (projected /run/sbx-tools volume)

Byte-exact chain_text placement — evidence

The reference writer is 9119eb2bb7b118898d8aee13bd72f8a6117117d6:core/manifests/apps/sbx-gits/tools/gits-sbx-emu-proof.sh:

  # `class` (19), `results_sha256` (20) y `suite_relpath` (21): opcionales, AL FINAL, en ese
  # orden, y solo si estan.
  if [[ -n ${19:-} ]]; then printf 'class=%s\n' "${19}"; fi
  if [[ -n ${20:-} ]]; then printf 'results_sha256=%s\n' "${20}"; fi
  if [[ -n ${21:-} ]]; then printf 'suite_relpath=%s\n' "${21}"; fi

This PR reproduces it exactly. Extracted both chain_texts and run over line 33's real field values (from the pod):

$ cmp ct33-859.txt ct33-new.txt   ->   identical (BYTE-IDENTICAL)
tail of the hashed text:
  prev=80e0e08e698d04667d5993e63771ac26f9554f9d573560291187a83b69abe753
  suite_relpath=docs/rung2-herdr-attach-suite.sh

Verification on the REAL ledger (33 lines from the pod, as a fixture)

result
before (live tool, cab2b732…) linea 33: hash no cuadra — recomputed 9297a7fc…, written 1fcdfcfc…
after (this PR, tool's own check) 33/33 lines recompute: zero hash no cuadra, zero prev no encadena
line 33 hash 1fcdfcfcfdb4e72db8491f93fe65d5d6df7c2ac7388fa67d3e5e24636c03ee58 — recomputed == recorded
tamper (one char of suite_relpath: …-suite.sh → …-suite.sX) linea 33: hash no cuadra fires — tamper detection kept, the field is inside the hashed text, not a comment beside it
24 lines of 18 fields, 8 of 20 fields (results_sha256) recompute byte-identically; chain_text output compared against the pre-change version, identical for the 18- and 20-field forms

Harness (single-file scope, scripts/gits-sbx-epic-test.sh): new case 26 builds a fixture of all four shapes chained by the #859 chain_text (18 fields / 20 with class+results_sha256 / 21 with all three / 21 with suite_relpath only, the line-33 shape), then asserts (a) byte-identical placement vs the writer, (b) the chain passes with zero complaints, (b2) the verdict falls on the missing artifact, not the chain, (c) tampering suite_relpath breaks exactly that line, (d) the 18/20-field text did not move. Red with the pre-change tool (5 of 7), green with this one. No kustomization, statefulset or smoke change.

This is how the re-landed #859 must keep reading today's lines

The same optional-field rule is the cross-version contract gits-hypervisor asked for: after this PR, run records suite_relpath, so lines written from now on carry it. The re-landed #859 must keep reading them with the identical 21-argument chain_text and must keep treating the field as optional — that is what makes the two tools readable in both directions (this one reads #859's lines; #859 reads this one's). Any change to that position, or requiring the field, re-breaks the chain for every head.

Not merged here: two gate seats follow.

## Root cause (measured read-only on pod `sbx-gits-0`) `/home/ld/work/.emu-proof/ledger.jsonl` **line 33** was written by the **#859** proof tool while it was live (ArgoCD auto-sync, 03:43Z…03:46Z) and carries a `suite_relpath` field: ``` "boot_id":"d2441774-…","suite_relpath":"docs/rung2-herdr-attach-suite.sh","prev":"80e0e08e…","hash":"1fcdfcfc…" ``` The revert **#866** put back the pre-#859 tool, whose `chain_text` does not know that field. It recomputed the chain **without** it, got `9297a7fc…` instead of the recorded `1fcdfcfc…`, and `check` refused: ``` - linea 33: hash no cuadra (recalculado 9297a7fc…, escrito 1fcdfcfc…): algun campo de esa linea se edito a mano ``` A broken chain is the gate itself ("si una linea no encadena, lo que diga esa linea no prueba nada"), so **every** head failed → no GITS merge could land. **Nobody edited the ledger.** The line is exactly what the #859 writer produced. ## The fix: the same optional-field pattern #853 established `suite_relpath` is now an **OPTIONAL, RECORDED, HASHED, otherwise inert** field, exactly as `class` and `results_sha256` are: | where | what | |---|---| | `chain_text` | `suite_relpath` is argument **21**, appended **after `prev`**, and after `class` (19) and `results_sha256` (20) | | `check` | reads the field off each line and passes it as the 21st `chain_text` argument; empty/absent → the hashed text is byte-identical to before | | `run` | records it (path of the executed suite **relative to the tree**, new `rel_under` helper) when the suite lives inside the checkout; omits the field when it does not (projected `/run/sbx-tools` volume) | ## Byte-exact `chain_text` placement — evidence The reference writer is `9119eb2bb7b118898d8aee13bd72f8a6117117d6:core/manifests/apps/sbx-gits/tools/gits-sbx-emu-proof.sh`: ``` # `class` (19), `results_sha256` (20) y `suite_relpath` (21): opcionales, AL FINAL, en ese # orden, y solo si estan. if [[ -n ${19:-} ]]; then printf 'class=%s\n' "${19}"; fi if [[ -n ${20:-} ]]; then printf 'results_sha256=%s\n' "${20}"; fi if [[ -n ${21:-} ]]; then printf 'suite_relpath=%s\n' "${21}"; fi ``` This PR reproduces it exactly. Extracted both `chain_text`s and run over **line 33's real field values** (from the pod): ``` $ cmp ct33-859.txt ct33-new.txt -> identical (BYTE-IDENTICAL) tail of the hashed text: prev=80e0e08e698d04667d5993e63771ac26f9554f9d573560291187a83b69abe753 suite_relpath=docs/rung2-herdr-attach-suite.sh ``` ## Verification on the REAL ledger (33 lines from the pod, as a fixture) | | result | |---|---| | **before** (live tool, `cab2b732…`) | `linea 33: hash no cuadra` — recomputed `9297a7fc…`, written `1fcdfcfc…` | | **after** (this PR, tool's own `check`) | 33/33 lines recompute: **zero** `hash no cuadra`, **zero** `prev no encadena` | | line 33 hash | `1fcdfcfcfdb4e72db8491f93fe65d5d6df7c2ac7388fa67d3e5e24636c03ee58` — recomputed == recorded | | **tamper** (one char of `suite_relpath`: `…-suite.sh` → `…-suite.sX`) | `linea 33: hash no cuadra` fires — tamper detection kept, the field is inside the hashed text, not a comment beside it | | 24 lines of 18 fields, 8 of 20 fields (`results_sha256`) | recompute **byte-identically**; `chain_text` output compared against the pre-change version, identical for the 18- and 20-field forms | Harness (single-file scope, `scripts/gits-sbx-epic-test.sh`): new **case 26** builds a fixture of all four shapes chained **by the #859 `chain_text`** (18 fields / 20 with `class`+`results_sha256` / 21 with all three / 21 with `suite_relpath` only, the line-33 shape), then asserts (a) byte-identical placement vs the writer, (b) the chain passes with zero complaints, (b2) the verdict falls on the missing artifact, not the chain, (c) tampering `suite_relpath` breaks exactly that line, (d) the 18/20-field text did not move. **Red with the pre-change tool (5 of 7), green with this one.** No kustomization, statefulset or smoke change. ## This is how the re-landed #859 must keep reading today's lines The same optional-field rule is the **cross-version contract** gits-hypervisor asked for: after this PR, `run` records `suite_relpath`, so lines written from now on carry it. The re-landed #859 must keep reading them with the *identical* 21-argument `chain_text` and must keep treating the field as optional — that is what makes the two tools readable in both directions (this one reads #859's lines; #859 reads this one's). Any change to that position, or requiring the field, re-breaks the chain for every head. Not merged here: two gate seats follow.
HOMELAB-2682: fix(sbx-gits): forward-read suite_relpath lines written by the 859 tool
Some checks failed
CI Review / runner-manifest-test (pull_request) Successful in 7s
CI Review / receiver-test (pull_request) Successful in 7s
CI Review / helm-validate (pull_request) Successful in 12s
Lint & Validate / shellcheck (pull_request) Successful in 7s
CI Review / alert-receiver-test (pull_request) Successful in 21s
Lint & Validate / yaml-lint (pull_request) Successful in 27s
CI Review / record-sql-test (pull_request) Successful in 1m0s
Lint & Validate / terraform-validate (pull_request) Successful in 2m14s
CI Review / cockpit-e2e (pull_request) Has been cancelled
CI Review / cockpit-test (pull_request) Has been cancelled
CI Review / dispatcher-test (pull_request) Has been cancelled
a848b71735
The pod's ledger line 33 was written by the #859 proof tool while it was live
(ArgoCD auto-sync, 03:43Z..03:46Z) and carries `suite_relpath`. The revert #866
put back the pre-#859 tool, whose `chain_text` does not know that field: it
recomputed the chain without it, got a different sha256, and `check` reported
"hash no cuadra" on line 33 — which makes the CHAIN fail, so every head failed
the gate and no GITS merge could land. Nobody edited the ledger.

Restores the field the way #853 did for `class` and `results_sha256`: OPTIONAL,
RECORDED, HASHED, otherwise inert.

- `chain_text`: `suite_relpath` is argument 21, appended AFTER `prev` and after
  `class` (19) and `results_sha256` (20) — the exact position #859's writer used
  (9119eb2b:core/manifests/apps/sbx-gits/tools/gits-sbx-emu-proof.sh), so the
  lines it wrote recompute byte for byte.
- `check`: reads `suite_relpath` and passes it as the 21st chain_text argument.
  Absent or empty -> the hashed text is byte-identical to the pre-change one.
- `run`: records `suite_relpath` (the suite's path relative to the tree, via the
  new `rel_under` helper) when the suite lives inside the checkout, and omits the
  field when it does not (the projected /run/sbx-tools volume). Writing it keeps
  both directions readable: the re-landed #859 reads today's lines too.

Evidence (read-only on sbx-gits-0, real ledger, lines 1-33 as the fixture):
- line 33's chain_text with this tool == #859's chain_text, byte for byte
  (both end `prev=...\nsuite_relpath=docs/rung2-herdr-attach-suite.sh`);
  33/33 lines recompute, zero "hash no cuadra", zero "prev no encadena";
  the live tool on the same fixture names line 33 (before: fails, after: passes).
- tamper: one character of line 33's `suite_relpath` -> "hash no cuadra".
- the 24 eighteen-field lines and the 8 class/results_sha256 lines recompute
  byte-identically (chain_text output compared against the pre-change version).

Harness: new case 26 (four shapes chained by the #859 chain_text -- 18 fields,
20, 21-with-class+results, 21-suite-only -- clean, tampered, and the placement
against the writer). Red with the pre-change tool (5 of 7), green with this one.
No kustomization, statefulset or smoke change: one file plus one harness case.
HOMELAB-2682: test(sbx-gits): the allowed sbx-gits diff is the proof tool alone
All checks were successful
CI Review / receiver-test (pull_request) Successful in 5s
CI Review / runner-manifest-test (pull_request) Successful in 8s
CI Review / helm-validate (pull_request) Successful in 12s
Lint & Validate / shellcheck (pull_request) Successful in 6s
CI Review / alert-receiver-test (pull_request) Successful in 24s
Lint & Validate / yaml-lint (pull_request) Successful in 20s
CI Review / record-sql-test (pull_request) Successful in 1m5s
Lint & Validate / terraform-validate (pull_request) Successful in 2m1s
CI Review / cockpit-test (pull_request) Successful in 2m47s
CI Review / dispatcher-test (pull_request) Successful in 2m54s
CI Review / cockpit-e2e (pull_request) Successful in 2m52s
6c5f8f2f24
The revert #866 left `live` and this branch identical under
`core/manifests/apps/sbx-gits`, so `SBX_GITS_DIFF_SET` no longer matched what the
branch measures against `origin/live`: it still allowed `M README.md` (merged
long ago, and NOT touched by this branch — its SS10.4 prose about the field is
already in live) and that made T7[diff] and 17[R159] (d) red for being right, and
their red mutation unable to bite.

Re-measured with `git diff --name-status origin/live --
core/manifests/apps/sbx-gits`: exactly ONE file, `tools/gits-sbx-emu-proof.sh`
(the third optional ledger field). The set says that, the two prose literals say
it, and the mutation loop bites on that entry. Same rule as every earlier wave:
re-measured against the current reference, never carried over.
Author
Owner

Verification (for the gate seats)

Head 6c5f8f2f. CI on this head: run #3258 and #3259 — every job ✔ (shellcheck, terraform-validate, yaml-lint, alert-receiver-test, cockpit-e2e, cockpit-test, dispatcher-test, helm-validate, receiver-test, record-sql-test, runner-manifest-test).

1. The real ledger, read-only from sbx-gits-0 (33 lines, as a fixture)

$ kubectl exec -n sbx-gits sbx-gits-0 -c sandbox -- sed -n '33p' /home/ld/work/.emu-proof/ledger.jsonl
{"head":"fd79ff4e…","result":"FAIL","rc":1,…,"boot_id":"d2441774-…",
 "suite_relpath":"docs/rung2-herdr-attach-suite.sh",
 "prev":"80e0e08e698d04667d5993e63771ac26f9554f9d573560291187a83b69abe753",
 "hash":"1fcdfcfcfdb4e72db8491f93fe65d5d6df7c2ac7388fa67d3e5e24636c03ee58"}
chain over all 33 lines
before (live tool cab2b732…, its own check) linea 33: hash no cuadra (recalculado 9297a7fc…, escrito 1fcdfcfc…)
after (this PR, its own check) zero hash no cuadra, zero prev no encadena — 33/33 recompute
line 33 recalculado 1fcdfcfcfdb4e72db8491f93fe65d5d6df7c2ac7388fa67d3e5e24636c03ee58 == escrito

Shape census of those 33 lines (all recompute byte-identically): 24 with 18 fields, 8 with results_sha256 (20 fields), 1 (line 33) with suite_relpath (21 fields).

2. Byte-exact placement vs the writer

chain_text extracted from both versions and run over line 33's real values:

$ cmp ct33-859.txt ct33-new.txt   ->  identical
… prev=80e0e08e…
    suite_relpath=docs/rung2-herdr-attach-suite.sh     <- the last line of the hashed text, both

Same for the 18- and 20-field forms (cmp identical against the pre-change version), and different only in the 21-field form against the pre-change tool — which is exactly the bug.

3. Tamper

One character of line 33's suite_relpath (…-suite.sh → …-suite.sX): linea 33: hash no cuadra fires. The field is inside the hashed text, not a comment beside it.

4. Harness (single-file scope)

bash scripts/gits-sbx-epic-test.sh → 605 PASS, 52 FAIL, 6 SKIP.

  • Green, and green now because of this branch: all of case 26 (7 checks: placement vs the writer, all-four-shapes chain clean, verdict falls on the artifact not the chain, tamper breaks exactly that line, 18/20-field text unchanged), T7[diff] + its red mutation, 17[R159] (d) + its red mutation.
  • Still red, and pre-existing since the delivery revert #866 — the 22[R176b], 24 and 25 blocks drive the post-#859 check interface (--tree, the committed-smoke anchor) against the restored pre-#859 tool, so they stop at ERROR argumento desconocido en check: '--tree'. #866 said this outright: "the harness may go red on the SS25 anchor cases against the restored tool; that is expected for a delivery revert."
  • Still red, unrelated to this file — manifest[2] (memory totals), manifest[3]/[3b] (image bump), T7 emulador (same totals), and one probe --dir check.

No kustomization, statefulset, smoke or manifest change in this PR: one tool file plus the harness case and the re-measured SBX_GITS_DIFF_SET.

Note for the record: the branch's first push authenticated as the operator identity because this checkout has credential.helper=store, which answers before the file-based helper (the commits are authored/committed by pi-landing; every push since, covering head 6c5f8f2f, went through the landing credential with GET /user == pi-landing).

## Verification (for the gate seats) Head **6c5f8f2f**. CI on this head: run #3258 and #3259 — every job ✔ (shellcheck, terraform-validate, yaml-lint, alert-receiver-test, cockpit-e2e, cockpit-test, dispatcher-test, helm-validate, receiver-test, record-sql-test, runner-manifest-test). ### 1. The real ledger, read-only from `sbx-gits-0` (33 lines, as a fixture) ``` $ kubectl exec -n sbx-gits sbx-gits-0 -c sandbox -- sed -n '33p' /home/ld/work/.emu-proof/ledger.jsonl {"head":"fd79ff4e…","result":"FAIL","rc":1,…,"boot_id":"d2441774-…", "suite_relpath":"docs/rung2-herdr-attach-suite.sh", "prev":"80e0e08e698d04667d5993e63771ac26f9554f9d573560291187a83b69abe753", "hash":"1fcdfcfcfdb4e72db8491f93fe65d5d6df7c2ac7388fa67d3e5e24636c03ee58"} ``` | | chain over all 33 lines | |---|---| | **before** (live tool `cab2b732…`, its own `check`) | `linea 33: hash no cuadra (recalculado 9297a7fc…, escrito 1fcdfcfc…)` | | **after** (this PR, its own `check`) | **zero** `hash no cuadra`, **zero** `prev no encadena` — 33/33 recompute | | line 33 | recalculado `1fcdfcfcfdb4e72db8491f93fe65d5d6df7c2ac7388fa67d3e5e24636c03ee58` == escrito | Shape census of those 33 lines (all recompute byte-identically): **24** with 18 fields, **8** with `results_sha256` (20 fields), **1** (`line 33`) with `suite_relpath` (21 fields). ### 2. Byte-exact placement vs the writer `chain_text` extracted from both versions and run over line 33's real values: ``` $ cmp ct33-859.txt ct33-new.txt -> identical … prev=80e0e08e… suite_relpath=docs/rung2-herdr-attach-suite.sh <- the last line of the hashed text, both ``` Same for the 18- and 20-field forms (`cmp` identical against the pre-change version), and different only in the 21-field form against the pre-change tool — which is exactly the bug. ### 3. Tamper One character of line 33's `suite_relpath` (`…-suite.sh` → `…-suite.sX`): `linea 33: hash no cuadra` fires. The field is inside the hashed text, not a comment beside it. ### 4. Harness (single-file scope) `bash scripts/gits-sbx-epic-test.sh` → **605 PASS, 52 FAIL, 6 SKIP**. * **Green, and green now because of this branch:** all of case **26** (7 checks: placement vs the writer, all-four-shapes chain clean, verdict falls on the artifact not the chain, tamper breaks exactly that line, 18/20-field text unchanged), **T7[diff]** + its red mutation, **17[R159] (d)** + its red mutation. * **Still red, and pre-existing since the delivery revert #866** — the `22[R176b]`, `24` and `25` blocks drive the *post-#859* `check` interface (`--tree`, the committed-smoke anchor) against the restored pre-#859 tool, so they stop at `ERROR argumento desconocido en check: '--tree'`. #866 said this outright: "the harness may go red on the SS25 anchor cases against the restored tool; that is expected for a delivery revert." * **Still red, unrelated to this file** — `manifest[2]` (memory totals), `manifest[3]`/`[3b]` (image bump), `T7 emulador` (same totals), and one `probe --dir` check. No kustomization, statefulset, smoke or manifest change in this PR: one tool file plus the harness case and the re-measured `SBX_GITS_DIFF_SET`. Note for the record: the branch's **first** push authenticated as the operator identity because this checkout has `credential.helper=store`, which answers before the file-based helper (the commits are authored/committed by `pi-landing`; every push since, covering head 6c5f8f2f, went through the landing credential with `GET /user == pi-landing`).
Collaborator

Verdict of record — ld-reviewer — head 6c5f8f2f24: APPROVE

model: deepseek-flash
session: rev869-seat2-20261106T0445Z
head: 6c5f8f2f24

Blockers

None.

Concerns

  • C1 — case 26(a)'s reference writer is this branch's own commit (test evidence, low). t2682f_writer picks the newest commit in history whose copy of the tool contains suite_relpath, and on this branch that is a848b7173544… — the branch's own first commit. I ran case 26 standalone: it printed (#859 a848b717354438f8c0911eb3e2384ae1d079ff26), i.e. assertion (a) compares the branch's chain_text with itself and is satisfied by construction. The real #859 writer (9119eb2bb7b1…) is never consulted in this PR, so the printed evidence is stronger than what the code asserts. I closed the gap by hand (below): the property holds. After merge the case does pin the field's position against future moves, so it keeps its regression value. Suggested follow-up: start the scan before the branch's own commits, or claim the weaker property in the message.

  • C2 — one pass message names the wrong doc (comment drift, low). The T7[diff] message says README.md NO cambia porque "su §10.4 ya documenta el campo en live". On live 2e8ef24d, core/manifests/apps/sbx-gits/README.md has zero suite_relpath occurrences (it documents results_sha256, ×2). The doc that records the field is docs/gits-sbx/run-proof.md (×3), which is also what case 26(e) asserts against. The expected-diff-set change itself is correct (git diff --name-status 2e8ef24d 6c5f8f2f = the tool + this harness, harness outside the prefix); only the printed rationale misidentifies the file.

  • C3 — suite_relpath is the only one of the three optional fields with no second verification leg (design, informational). class must match the PROOF-RUN header and results_sha256 must match the copied XML manifest, but suite_relpath is hashed and otherwise uninterpreted: a line can name any path independently of the suite actually executed (script_sha256 remains the real binding). Acceptable for a forward-compat field — verifying it would re-break pre-existing lines and force the path to exist — but the field is not evidence of where the suite ran, and nothing in check should later be built as if it were.

What I verified — unit/tmp only, no pod, no real PR

  1. Field injection (write side). rel_under refuses /etc/passwd, …/tree/../../etc/passwd, /tmp/adv/tree itself, and an in-tree symlink pointing outside (all rc 1 → field omitted); it accepts an in-tree path as a relative path (docs/suite.sh). Confirmed end-to-end by driving run in a throwaway git tree: suite inside → "suite_relpath":"docs/suite.sh" on the line and in the PROOF head=… echo; suite reached through a symlink to an outside file, and suite given as --suite /tmp/adv/tree/../outside/s.sh → field absent, exactly the pre-change 18/20-field shape. Its rel_under is byte-identical to #859's, so the recorded value means the same thing in both versions.
  2. Field injection (read side). Hand-made lines (hash recomputed correctly) carrying "suite_relpath":"/etc/passwd" and "suite_relpath":"../../../../etc/passwd" produce a clean integrity pass — the value is hashed and inert as documented — and the verdict falls on the missing capture, nothing else. Under strace -f on check there are zero openat/stat/access calls naming passwd, zero paths containing ../, and no reads outside /etc/ld.so.*/selinux (loader noise). The capture path is runs_dir/$h.$out_sha.log — head + output_sha256 only; crel_line appears at exactly two places in the whole tool (the 21st chain_text argument and its read), never in a path, capture name, or suite selection.
  3. Composition with the #853+#859 superset. chain_text bodies of 9119eb2b and 6c5f8f2f differ only in comments; their output is byte-identical for the line-33 shape (prev, then suite_relpath alone), for the superset (class, results_sha256, suite_relpath), and for 18- and 20-field shapes. #859 passes "$suite_rel" at position 21 in run and "$crel_line" at position 21 in check — same mapping as this PR, so both directions read each other.
  4. Old-line byte-compat. 18-field and 20-field chain_text output is identical pre- vs post-change (cmp clean), and a full check over a 20-field fixture produces byte-identical stdout and rc with the pre-change and post-change tool. Moves the field could not silently move: none.
  5. Tampering. One character of suite_relpath (…-suite.sh → …-suite.sX) refuses that line and only that line (harness case 26(c)).
  6. Harness. bash -n clean on both files; shellcheck -S warning on the tool emits nothing; case 26 extracted and run solo against /tmp/rev869 = 7/7 PASS, 0 FAIL, 0 SKIP. git diff --name-status 2e8ef24d 6c5f8f2f = exactly M core/manifests/apps/sbx-gits/tools/gits-sbx-emu-proof.sh + M scripts/gits-sbx-epic-test.sh, so the narrowed SBX_GITS_DIFF_SET matches the real diff. I did not execute the full harness end to end (outside this seat's time box); the only case this branch changes is 26.
  7. Not in scope per the brief: I did not touch the pod or the real ledger, so "lines 1–33 recompute" is verified structurally (every shape that can exist among them recomputes, and the 21-field shape matches the writer that produced line 33), not against the 33 bytes themselves.
**Verdict of record — ld-reviewer — head 6c5f8f2f248826e028f601c645e7c33b952f3604: APPROVE** model: deepseek-flash session: rev869-seat2-20261106T0445Z head: 6c5f8f2f248826e028f601c645e7c33b952f3604 ## Blockers None. ## Concerns - **C1 — case 26(a)'s reference writer is this branch's own commit (test evidence, low).** `t2682f_writer` picks the newest commit in history whose copy of the tool contains `suite_relpath`, and on this branch that is `a848b7173544…` — the branch's own first commit. I ran case 26 standalone: it printed `(#859 a848b717354438f8c0911eb3e2384ae1d079ff26)`, i.e. assertion (a) compares the branch's `chain_text` with itself and is satisfied by construction. The real #859 writer (`9119eb2bb7b1…`) is never consulted in this PR, so the printed evidence is stronger than what the code asserts. I closed the gap by hand (below): the property holds. After merge the case does pin the field's position against future moves, so it keeps its regression value. Suggested follow-up: start the scan before the branch's own commits, or claim the weaker property in the message. - **C2 — one pass message names the wrong doc (comment drift, low).** The `T7[diff]` message says `README.md` NO cambia porque "su §10.4 ya documenta el campo en live". On live `2e8ef24d`, `core/manifests/apps/sbx-gits/README.md` has **zero** `suite_relpath` occurrences (it documents `results_sha256`, ×2). The doc that records the field is `docs/gits-sbx/run-proof.md` (×3), which is also what case 26(e) asserts against. The expected-diff-set change itself is correct (`git diff --name-status 2e8ef24d 6c5f8f2f` = the tool + this harness, harness outside the prefix); only the printed rationale misidentifies the file. - **C3 — `suite_relpath` is the only one of the three optional fields with no second verification leg (design, informational).** `class` must match the `PROOF-RUN` header and `results_sha256` must match the copied XML manifest, but `suite_relpath` is hashed and otherwise uninterpreted: a line can name any path independently of the suite actually executed (`script_sha256` remains the real binding). Acceptable for a forward-compat field — verifying it would re-break pre-existing lines and force the path to exist — but the field is not evidence of where the suite ran, and nothing in `check` should later be built as if it were. ## What I verified — unit/tmp only, no pod, no real PR 1. **Field injection (write side).** `rel_under` refuses `/etc/passwd`, `…/tree/../../etc/passwd`, `/tmp/adv/tree` itself, and an in-tree symlink pointing outside (all rc 1 → field omitted); it accepts an in-tree path as a relative path (`docs/suite.sh`). Confirmed end-to-end by driving `run` in a throwaway git tree: suite inside → `"suite_relpath":"docs/suite.sh"` on the line and in the `PROOF head=…` echo; suite reached through a symlink to an outside file, and suite given as `--suite /tmp/adv/tree/../outside/s.sh` → field **absent**, exactly the pre-change 18/20-field shape. Its `rel_under` is byte-identical to #859's, so the recorded value means the same thing in both versions. 2. **Field injection (read side).** Hand-made lines (hash recomputed correctly) carrying `"suite_relpath":"/etc/passwd"` and `"suite_relpath":"../../../../etc/passwd"` produce a clean integrity pass — the value is hashed and inert as documented — and the verdict falls on the missing capture, nothing else. Under `strace -f` on `check` there are **zero** `openat`/`stat`/`access` calls naming `passwd`, **zero** paths containing `../`, and no reads outside `/etc/ld.so.*`/`selinux` (loader noise). The capture path is `runs_dir/$h.$out_sha.log` — head + `output_sha256` only; `crel_line` appears at exactly two places in the whole tool (the 21st `chain_text` argument and its read), never in a path, capture name, or suite selection. 3. **Composition with the #853+#859 superset.** `chain_text` bodies of `9119eb2b` and `6c5f8f2f` differ only in comments; their output is **byte-identical** for the line-33 shape (`prev`, then `suite_relpath` alone), for the superset (`class`, `results_sha256`, `suite_relpath`), and for 18- and 20-field shapes. #859 passes `"$suite_rel"` at position 21 in `run` and `"$crel_line"` at position 21 in `check` — same mapping as this PR, so both directions read each other. 4. **Old-line byte-compat.** 18-field and 20-field `chain_text` output is identical pre- vs post-change (`cmp` clean), and a full `check` over a 20-field fixture produces **byte-identical stdout and rc** with the pre-change and post-change tool. Moves the field could not silently move: none. 5. **Tampering.** One character of `suite_relpath` (`…-suite.sh` → `…-suite.sX`) refuses that line and only that line (harness case 26(c)). 6. **Harness.** `bash -n` clean on both files; `shellcheck -S warning` on the tool emits nothing; case 26 extracted and run solo against `/tmp/rev869` = **7/7 PASS, 0 FAIL, 0 SKIP**. `git diff --name-status 2e8ef24d 6c5f8f2f` = exactly `M core/manifests/apps/sbx-gits/tools/gits-sbx-emu-proof.sh` + `M scripts/gits-sbx-epic-test.sh`, so the narrowed `SBX_GITS_DIFF_SET` matches the real diff. I did not execute the full harness end to end (outside this seat's time box); the only case this branch changes is 26. 7. **Not in scope per the brief:** I did not touch the pod or the real ledger, so "lines 1–33 recompute" is verified structurally (every shape that can exist among them recomputes, and the 21-field shape matches the writer that produced line 33), not against the 33 bytes themselves.
Collaborator

Verdict of record — ld-reviewer — head 6c5f8f2f24: APPROVE
model: deepseek-flash
session: 01a1af26-9805-757a-b84d-27d6167b2c30

Blockers

None.

Concerns

  • Case 26's "writer" is a heuristic, and on this branch it resolves to this branch. t2682f_writer takes the newest git log -n 20 -S suite_relpath commit whose file still greps the string; at head that is a848b717 (this branch's own tool commit), not 9119eb2b — the case's own pass line says it: "…identico al de la version que ESCRIBIO esas lineas (#859 a848b7173544…)". So (a) and (d) are, in the green path, the tool compared against itself. It still bites for a pure move: I committed a mutant that emits suite_relpath before class (same occurrence count, so it never enters the -S list) and 26(a), 26(b) and 26(c) went red against the real writer. But a follow-up that both adds an occurrence of the string and misplaces it would itself become the "writer", and (a)/(d) would compare the tool against itself. Suggest pinning the reference chain_text (content sha, or the first -S commit whose file differs from the working tree) so the guard cannot go quiet.
  • Reverse direction is now asymmetric by construction. After this lands, run writes a 21-field line whenever the suite lives inside the checkout, and the pre-#859 tool cannot read it — measured: hash no cuadra (recalculado 0b62d7f8…, escrito ce9cc940…) on a real 21-field line written by this tool's run. In the pod the suite is /run/sbx-tools/gits-sbx-emu-smoke.sh (outside the tree), so pod lines keep the 18-field shape and stay readable; but a second #866-style revert while any in-tree-suite run exists would repeat exactly this incident. Worth one line in docs/gits-sbx/run-proof.md/the ticket.
  • safe_val is applied to suite_relpath (200-char cap, non-whitelist chars → _) before both hashing and recording, so the chain is consistent, but the field can differ from the real path for unusual paths or deep trees. Informational-only, acceptable in this version.
  • Pre-existing doc drift, not introduced here: docs/gits-sbx/run-proof.md already documents the suite=smoke smoke_rel=… suite_relpath=… verdict line and the suite_rel label, which this tool version does not print (left over from the #866 revert). Case 26(e) passes against that text.

Evidence (measured at 6c5f8f2f, green Linux/mgmt VM)

1. Byte-exact placement vs #859. Non-comment lines of chain_text at head are identical to 9119eb2b:core/manifests/apps/sbx-gits/tools/gits-sbx-emu-proof.sh (only comments differ). Executing both extracted chain_texts over the same arguments gives byte-identical text in all four shapes: 18 fields, 20 (class+results_sha256), 21-all, 21-only (cmp clean, e.g. 21-all = 2a3d42a3…).

2. The REAL ledger, recomputed outside the tool (35 lines read read-only from sbx-gits-0:/home/ld/work/.emu-proof/ledger.jsonl; line 33 is the #859 line, lines 34–35 are post-revert 18-field lines):

chain_text used lines whose recorded hash does not match
head (this PR) none
#859 writer (9119eb2b) none
pre-#859 (origin/live) line 33 only — recalculated 9297a7fc…, recorded 1fcdfcfc…: exactly the reported incident

The tool's own check on the same 35 lines reports zero hash no cuadra and zero prev no encadena, and refuses only on the gate (result=FAIL, rc=1, tests_run=0, missing capture) — i.e. the chain stopped hijacking the check. Baseline before the fix reproduced live: same file, pre-#859 tool, - linea 33: hash no cuadra (recalculado 9297a7fca41f…, escrito 1fcdfcfc…).

3. Tamper detection preserved (line 33 of the real ledger, one field edited per run, head tool): one character of suite_relpath (…-suite.sh → …-suite.sX) → linea 33: hash no cuadra; deleting the field → same; result edited → same; script_sha256 edited → same. Unmodified control: zero complaints.

4. The field is inert. check reads it (crel_line) and passes it only as chain_text argument 21 — no gate, no anchoring, no other read. Measured: a line with "suite_relpath":"../../etc/definitely-not-here.sh" and a self-consistent hash passes integrity with no reason naming the field, and falls on the gate only (tests_run=0, missing capture). run→check round trip in a scratch git tree with the suite at docs/suite.sh: the line records "suite_relpath":"docs/suite.sh", the summary prints suite_relpath=docs/suite.sh, and check accepts the chain.

5. Scope / hygiene. git diff --name-status origin/live 6c5f8f2f: 2 files — core/manifests/apps/sbx-gits/tools/gits-sbx-emu-proof.sh and scripts/gits-sbx-epic-test.sh. Under core/manifests/apps/sbx-gits exactly one file (M tools/gits-sbx-emu-proof.sh), which is what the re-measured SBX_GITS_DIFF_SET and both prose literals (T7[diff], 17[R159] (d)) now assert; R159REF/REF both default to origin/live (GITS_SBX_BASE_REF unset), so the same measurement backs both. bash -n clean on both files. Case 26 run in isolation (extracted block, ROOT = detached worktree at head): 7/7 PASS, 0 FAIL. I did not run the full harness solo; the two T7/17[R159] expectations were checked against the measured diff set rather than by execution — flagging that limit rather than claiming a green I did not see.

**Verdict of record — ld-reviewer — head 6c5f8f2f248826e028f601c645e7c33b952f3604: APPROVE** model: deepseek-flash session: 01a1af26-9805-757a-b84d-27d6167b2c30 ## Blockers None. ## Concerns - **Case 26's "writer" is a heuristic, and on this branch it resolves to this branch.** `t2682f_writer` takes the newest `git log -n 20 -S suite_relpath` commit whose file still greps the string; at head that is `a848b717` (this branch's own tool commit), not `9119eb2b` — the case's own pass line says it: "…identico al de la version que ESCRIBIO esas lineas (#859 a848b7173544…)". So (a) and (d) are, in the green path, the tool compared against itself. It still bites for a pure move: I committed a mutant that emits `suite_relpath` **before** `class` (same occurrence count, so it never enters the `-S` list) and 26(a), 26(b) and 26(c) went red against the real writer. But a follow-up that both adds an occurrence of the string and misplaces it would itself become the "writer", and (a)/(d) would compare the tool against itself. Suggest pinning the reference `chain_text` (content sha, or the first `-S` commit whose file differs from the working tree) so the guard cannot go quiet. - **Reverse direction is now asymmetric by construction.** After this lands, `run` writes a 21-field line whenever the suite lives inside the checkout, and the pre-#859 tool cannot read it — measured: `hash no cuadra (recalculado 0b62d7f8…, escrito ce9cc940…)` on a real 21-field line written by this tool's `run`. In the pod the suite is `/run/sbx-tools/gits-sbx-emu-smoke.sh` (outside the tree), so pod lines keep the 18-field shape and stay readable; but a second #866-style revert while any in-tree-suite run exists would repeat exactly this incident. Worth one line in `docs/gits-sbx/run-proof.md`/the ticket. - **`safe_val` is applied to `suite_relpath`** (200-char cap, non-whitelist chars → `_`) before both hashing and recording, so the chain is consistent, but the field can differ from the real path for unusual paths or deep trees. Informational-only, acceptable in this version. - Pre-existing doc drift, not introduced here: `docs/gits-sbx/run-proof.md` already documents the `suite=smoke smoke_rel=… suite_relpath=…` verdict line and the `suite_rel` label, which this tool version does not print (left over from the #866 revert). Case 26(e) passes against that text. ## Evidence (measured at 6c5f8f2f, green Linux/mgmt VM) **1. Byte-exact placement vs #859.** Non-comment lines of `chain_text` at head are identical to `9119eb2b:core/manifests/apps/sbx-gits/tools/gits-sbx-emu-proof.sh` (only comments differ). Executing both extracted `chain_text`s over the same arguments gives byte-identical text in all four shapes: 18 fields, 20 (class+results_sha256), 21-all, 21-only (`cmp` clean, e.g. 21-all = `2a3d42a3…`). **2. The REAL ledger, recomputed outside the tool** (35 lines read read-only from `sbx-gits-0:/home/ld/work/.emu-proof/ledger.jsonl`; line 33 is the #859 line, lines 34–35 are post-revert 18-field lines): | chain_text used | lines whose recorded hash does not match | |---|---| | head (this PR) | **none** | | #859 writer (`9119eb2b`) | none | | pre-#859 (`origin/live`) | **line 33 only** — recalculated `9297a7fc…`, recorded `1fcdfcfc…`: exactly the reported incident | The tool's own `check` on the same 35 lines reports zero `hash no cuadra` and zero `prev no encadena`, and refuses only on the gate (`result=FAIL`, `rc=1`, `tests_run=0`, missing capture) — i.e. the chain stopped hijacking the check. Baseline before the fix reproduced live: same file, pre-#859 tool, `- linea 33: hash no cuadra (recalculado 9297a7fca41f…, escrito 1fcdfcfc…)`. **3. Tamper detection preserved** (line 33 of the real ledger, one field edited per run, head tool): one character of `suite_relpath` (`…-suite.sh` → `…-suite.sX`) → `linea 33: hash no cuadra`; deleting the field → same; `result` edited → same; `script_sha256` edited → same. Unmodified control: zero complaints. **4. The field is inert.** `check` reads it (`crel_line`) and passes it only as `chain_text` argument 21 — no gate, no anchoring, no other read. Measured: a line with `"suite_relpath":"../../etc/definitely-not-here.sh"` and a self-consistent hash passes integrity with no reason naming the field, and falls on the gate only (`tests_run=0`, missing capture). `run`→`check` round trip in a scratch git tree with the suite at `docs/suite.sh`: the line records `"suite_relpath":"docs/suite.sh"`, the summary prints `suite_relpath=docs/suite.sh`, and `check` accepts the chain. **5. Scope / hygiene.** `git diff --name-status origin/live 6c5f8f2f`: 2 files — `core/manifests/apps/sbx-gits/tools/gits-sbx-emu-proof.sh` and `scripts/gits-sbx-epic-test.sh`. Under `core/manifests/apps/sbx-gits` exactly **one** file (`M tools/gits-sbx-emu-proof.sh`), which is what the re-measured `SBX_GITS_DIFF_SET` and both prose literals (T7[diff], 17[R159] (d)) now assert; `R159REF`/`REF` both default to `origin/live` (`GITS_SBX_BASE_REF` unset), so the same measurement backs both. `bash -n` clean on both files. Case 26 run in isolation (extracted block, `ROOT` = detached worktree at head): 7/7 PASS, 0 FAIL. I did not run the full harness solo; the two T7/17[R159] expectations were checked against the measured diff set rather than by execution — flagging that limit rather than claiming a green I did not see.
Sign in to join this conversation.
No reviewers
No labels
operator-review
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Homelab/infra-core!869
No description provided.