Skip to content

Commit 872a96c

Browse files
solomonneascodex
andcommitted
feat(receipts): bind receipts to patch identity
Co-Authored-By: Codex <codex@openai.com>
1 parent 4fadf50 commit 872a96c

2 files changed

Lines changed: 350 additions & 32 deletions

File tree

src/brigade/work_cmd/verification.py

Lines changed: 130 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@
1010
import shutil
1111
import subprocess
1212
import sys
13+
import tempfile
1314
from pathlib import Path
1415
from typing import Any
1516
from uuid import uuid4
16-
from .. import config, graphtrail_delta, localio, proc, receipt_signing
17+
from .. import config, graphtrail_delta, localio, proc, receipt_signing, runguard
1718
from . import constants, helpers, ledger as ledger_mod
1819
from . import reviews as reviews_mod
1920
from . import scanners as scanners_mod
@@ -132,6 +133,7 @@ def _verify_execution_argv(argv: list[str], target: Path) -> list[str]:
132133
_VERIFY_CANCELED_RC = 130
133134
_VERIFY_INTERRUPTED_COMMAND_STATUS = "interrupted"
134135
_VERIFY_CANCELED_RECEIPT_STATUS = "canceled"
136+
_VERIFY_RECEIPT_SCHEMA_VERSION = 2
135137

136138

137139
def _verify_child_popen_kwargs() -> dict[str, Any]:
@@ -576,9 +578,16 @@ def _write_verify_markdown(run_dir: Path, receipt: dict[str, Any]) -> None:
576578
f"- Started: {receipt.get('started_at')}",
577579
f"- Completed: {receipt.get('completed_at')}",
578580
"",
579-
"## Commands",
580-
"",
581581
]
582+
lines.extend(_verify_identity_lines(receipt))
583+
if lines[-1] != "":
584+
lines.append("")
585+
lines.extend(
586+
[
587+
"## Commands",
588+
"",
589+
]
590+
)
582591
for command in receipt.get("commands", []):
583592
if not isinstance(command, dict):
584593
continue
@@ -597,10 +606,56 @@ def _write_verify_markdown(run_dir: Path, receipt: dict[str, Any]) -> None:
597606
(run_dir / "summary.md").write_text("\n".join(lines) + "\n")
598607

599608

600-
def _fingerprint_segment(hasher, label: str, data: bytes) -> None:
601-
encoded_label = label.encode()
602-
hasher.update(str(len(encoded_label)).encode() + b":" + encoded_label)
603-
hasher.update(str(len(data)).encode() + b":" + data)
609+
def _verify_identity_binding(receipt: dict[str, Any]) -> str | None:
610+
baseline = receipt.get("baseline_commit")
611+
fingerprint = receipt.get("tree_fingerprint")
612+
patch_hash = receipt.get("changes_patch_sha256")
613+
if not isinstance(baseline, str) or not baseline:
614+
return None
615+
if not isinstance(fingerprint, str) or not fingerprint:
616+
return None
617+
if not isinstance(patch_hash, str) or not patch_hash:
618+
return None
619+
return f"verified tree {fingerprint} = baseline {baseline} + patch {patch_hash}"
620+
621+
622+
def _verify_identity_lines(receipt: dict[str, Any]) -> list[str]:
623+
baseline = receipt.get("baseline_commit")
624+
fingerprint = receipt.get("tree_fingerprint")
625+
patch_hash = receipt.get("changes_patch_sha256")
626+
if not isinstance(baseline, str) or not baseline:
627+
return []
628+
if not isinstance(fingerprint, str) or not fingerprint:
629+
return []
630+
if not isinstance(patch_hash, str) or not patch_hash:
631+
return []
632+
binding = _verify_identity_binding(receipt)
633+
assert binding is not None
634+
return [
635+
f"- Baseline commit: `{baseline}`",
636+
f"- Tree fingerprint: `{fingerprint}`",
637+
f"- Patch hash: `{patch_hash}`",
638+
f"- {binding}",
639+
]
640+
641+
642+
def _git_with_index(target: Path, index_file: Path, *args: str) -> subprocess.CompletedProcess[str]:
643+
env = {**os.environ, "GIT_INDEX_FILE": str(index_file)}
644+
try:
645+
return subprocess.run(
646+
["git", "-C", str(target), *args],
647+
check=False,
648+
stdout=subprocess.PIPE,
649+
stderr=subprocess.PIPE,
650+
text=True,
651+
stdin=subprocess.DEVNULL,
652+
timeout=30,
653+
env=env,
654+
)
655+
except subprocess.TimeoutExpired as exc:
656+
return subprocess.CompletedProcess(exc.cmd, 124, stdout="", stderr=f"git timed out after {exc.timeout:g}s")
657+
except OSError:
658+
return subprocess.CompletedProcess(["git"], 127, stdout="", stderr="git unavailable")
604659

605660

606661
def _stamp_harness_session(receipt: dict[str, Any]) -> None:
@@ -611,30 +666,55 @@ def _stamp_harness_session(receipt: dict[str, Any]) -> None:
611666

612667

613668
def _tree_fingerprint(target: Path) -> str | None:
614-
"""Content hash of HEAD + tracked diff + untracked files. None outside git."""
669+
"""Git tree object for HEAD plus tracked and non-ignored untracked files."""
615670
try:
616-
head = helpers._git(target, "rev-parse", "HEAD")
617-
if head.returncode != 0:
618-
return None
619-
diff = helpers._git(target, "diff", "HEAD")
620-
untracked = helpers._git(target, "ls-files", "--others", "--exclude-standard")
621-
if diff.returncode != 0 or untracked.returncode != 0:
622-
return None
671+
with tempfile.TemporaryDirectory() as tmpdir:
672+
index_file = Path(tmpdir) / "index"
673+
read_tree = _git_with_index(target, index_file, "read-tree", "HEAD")
674+
if read_tree.returncode != 0:
675+
return None
676+
add_all = _git_with_index(target, index_file, "add", "-A")
677+
if add_all.returncode != 0:
678+
return None
679+
write_tree = _git_with_index(target, index_file, "write-tree")
680+
if write_tree.returncode != 0:
681+
return None
682+
value = write_tree.stdout.strip()
683+
return value or None
623684
except OSError:
624-
# helpers._git only catches TimeoutExpired; a missing git binary (e.g. a
625-
# test that restricts PATH) raises FileNotFoundError, an OSError subclass.
626685
return None
627-
hasher = hashlib.sha256()
628-
_fingerprint_segment(hasher, "head", head.stdout.encode())
629-
_fingerprint_segment(hasher, "diff", diff.stdout.encode())
630-
for name in sorted(untracked.stdout.splitlines()):
631-
path = target / name
632-
try:
633-
data = path.read_bytes()
634-
except OSError:
635-
return None
636-
_fingerprint_segment(hasher, f"untracked:{name}", data)
637-
return hasher.hexdigest()
686+
687+
688+
def _capture_verify_identity(target: Path, run_dir: Path) -> dict[str, Any]:
689+
unavailable: dict[str, Any] = {
690+
"schema_version": _VERIFY_RECEIPT_SCHEMA_VERSION,
691+
"baseline_commit": None,
692+
"tree_fingerprint": None,
693+
"changes_patch_sha256": None,
694+
}
695+
patch_path = run_dir / "changes.patch"
696+
try:
697+
baseline_commit = helpers._git_value(target, "rev-parse", "HEAD")
698+
tree_fingerprint = _tree_fingerprint(target)
699+
if baseline_commit is None or tree_fingerprint is None:
700+
return unavailable
701+
runguard.collect_changes_patch(target, patch_path)
702+
patch_bytes = patch_path.read_bytes()
703+
if (
704+
helpers._git_value(target, "rev-parse", "HEAD") != baseline_commit
705+
or _tree_fingerprint(target) != tree_fingerprint
706+
):
707+
patch_path.unlink(missing_ok=True)
708+
return unavailable
709+
except (OSError, runguard.RunGuardError):
710+
patch_path.unlink(missing_ok=True)
711+
return unavailable
712+
return {
713+
"schema_version": _VERIFY_RECEIPT_SCHEMA_VERSION,
714+
"baseline_commit": baseline_commit,
715+
"tree_fingerprint": tree_fingerprint,
716+
"changes_patch_sha256": hashlib.sha256(patch_bytes).hexdigest(),
717+
}
638718

639719

640720
def _run_verify_commands(
@@ -654,6 +734,7 @@ def _run_verify_commands(
654734
finalized = False
655735
try:
656736
run_dir.mkdir(parents=True, exist_ok=False)
737+
identity = _capture_verify_identity(target, run_dir)
657738
receipt = {
658739
"run_id": run_id,
659740
"target": str(target),
@@ -663,9 +744,9 @@ def _run_verify_commands(
663744
"path": str(run_dir),
664745
"evidence": _verification_evidence_payload(target),
665746
"commands": [],
666-
"tree_fingerprint": _tree_fingerprint(target),
667747
"planned_commands": _planned_commands_display(commands),
668748
}
749+
receipt.update(identity)
669750
_stamp_harness_session(receipt)
670751
try:
671752
graph_delta_before = graphtrail_delta.capture_before(target, run_dir, timeout=graphtrail_timeout)
@@ -796,7 +877,6 @@ def _run_verify_commands(
796877
def _write_reused_receipt(
797878
target: Path,
798879
latest: dict[str, Any],
799-
fingerprint: str | None,
800880
planned_display: list[str],
801881
timeout: int,
802882
) -> tuple[dict[str, Any], int]:
@@ -805,6 +885,7 @@ def _write_reused_receipt(
805885
run_id = f"{started.strftime('%Y%m%d-%H%M%S')}-work-verify-{uuid4().hex[:6]}"
806886
run_dir = helpers._verify_runs_root(target) / run_id
807887
run_dir.mkdir(parents=True, exist_ok=False)
888+
identity = _capture_verify_identity(target, run_dir)
808889
completed_at = helpers._now()
809890
receipt: dict[str, Any] = {
810891
"run_id": run_id,
@@ -817,9 +898,9 @@ def _write_reused_receipt(
817898
"path": str(run_dir),
818899
"commands": copy.deepcopy(latest.get("commands", [])),
819900
"reused_from": latest.get("run_id"),
820-
"tree_fingerprint": fingerprint,
821901
"planned_commands": planned_display,
822902
}
903+
receipt.update(identity)
823904
_stamp_harness_session(receipt)
824905
git = _receipt_git_snapshot(target)
825906
if git is not None:
@@ -1082,7 +1163,7 @@ def verify_run(
10821163
and latest.get("tree_fingerprint") == fingerprint
10831164
and latest.get("planned_commands") == planned_display
10841165
):
1085-
receipt, rc = _write_reused_receipt(target, latest, fingerprint, planned_display, timeout)
1166+
receipt, rc = _write_reused_receipt(target, latest, planned_display, timeout)
10861167
if receipt is None:
10871168
receipt, rc = _run_verify_commands(
10881169
target, planned, timeout, graphtrail_timeout=effective_graphtrail_timeout
@@ -1175,6 +1256,23 @@ def verify_show(*, target: Path, run_id: str, json_output: bool = False) -> int:
11751256
print(f"target: {run.get('target')}")
11761257
print(f"started: {run.get('started_at')}")
11771258
print(f"completed: {run.get('completed_at')}")
1259+
baseline = run.get("baseline_commit")
1260+
fingerprint = run.get("tree_fingerprint")
1261+
patch_hash = run.get("changes_patch_sha256")
1262+
if (
1263+
isinstance(baseline, str)
1264+
and baseline
1265+
and isinstance(fingerprint, str)
1266+
and fingerprint
1267+
and isinstance(patch_hash, str)
1268+
and patch_hash
1269+
):
1270+
print(f"baseline commit: {baseline}")
1271+
print(f"tree fingerprint: {fingerprint}")
1272+
print(f"patch hash: {patch_hash}")
1273+
binding = _verify_identity_binding(run)
1274+
if binding:
1275+
print(binding)
11781276
for command in run.get("commands", []):
11791277
if isinstance(command, dict):
11801278
print(f"- {command.get('command')} [{command.get('status')}] exit={command.get('exit_code')}")

0 commit comments

Comments
 (0)