Skip to content

Commit 558ee2b

Browse files
committed
Complete Phase 2 handoff migration for release-run and sync
1 parent a8fd601 commit 558ee2b

12 files changed

Lines changed: 158 additions & 58 deletions

examples/pcs_qc_release/release/handoff/handoff_for_pf.json

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"created_at": "2026-05-17T21:22:22Z",
3+
"expected_outputs": {
4+
"signed_science_claim_bundle.json": {
5+
"artifact_type": "SignedScienceClaimBundle.v0"
6+
},
7+
"verification_result.json": {
8+
"artifact_type": "VerificationResult.v0"
9+
}
10+
},
11+
"from_component": "LabTrust-Gym",
12+
"handoff_id": "handoff-labtrust-to-pf-qc-release-v0.1",
13+
"handoff_kind": "bundle_to_verifier",
14+
"input_artifacts": {
15+
"science_claim_bundle.certified.json": {
16+
"artifact_type": "ScienceClaimBundle.v0",
17+
"sha256": "sha256:9b42d792199eb6f358d26f822699f0ed65bb4366eee306d4958d42121c656833"
18+
}
19+
},
20+
"invariants": {
21+
"certificate_id": "cert-trace-886c95f0-5d63-42d6-aa13-5891c12c5a6a",
22+
"certified_bundle_hash": "sha256:9b42d792199eb6f358d26f822699f0ed65bb4366eee306d4958d42121c656833",
23+
"trace_hash": "sha256:c3e8a3dc4ad86d533de1dfa4ae7fe2a338c2cff3c945404c96a75216524d58cd"
24+
},
25+
"schema_version": "v0",
26+
"signature_or_digest": "sha256:21421c76e5307b056f6f8b6f4ad39abdd1bbddeeba6263dd9790f6da3074adf1",
27+
"source_commit": "4c5439ae358733f9a4c4a58e33fdaed1ab0d29de",
28+
"source_repo": "https://github.com/fraware/LabTrust-Gym",
29+
"status": "Validated",
30+
"to_component": "Provability Fabric"
31+
}

examples/pcs_qc_release/scripts/ci_validate_release_fixtures.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from __future__ import annotations
55

6+
import json
67
import subprocess
78
import sys
89
from pathlib import Path
@@ -31,6 +32,13 @@ def main() -> int:
3132
subprocess.run(["pcs", "validate", str(path)], check=True, cwd=ROOT)
3233
print("OK pcs validate", artifact)
3334

35+
fragment = release / "labtrust_release_fragment.json"
36+
if fragment.is_file():
37+
from labtrust_gym.pcs.release_fragment import assert_release_fragment_valid
38+
39+
assert_release_fragment_valid(json.loads(fragment.read_text(encoding="utf-8")))
40+
print("OK labtrust_release_fragment.json")
41+
3442
subprocess.run(
3543
[
3644
sys.executable,

examples/pcs_qc_release/scripts/finalize_release_run.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def main() -> int:
5252
handoff_id = args.handoff_id.strip() or None
5353
write_run_manifests(run_dir, commits, generator=args.generator, handoff_id=handoff_id)
5454
print(f"OK {run_dir.name}/RELEASE_HANDOFF_MANIFEST.json")
55-
print(f"OK {run_dir.name}/handoff_for_pf.json")
55+
print(f"OK {run_dir.name}/handoff_to_pf.json")
5656
print(f"OK {run_dir.name}/RELEASE_FIXTURE_MANIFEST.json")
5757

5858
if not args.no_promote:

examples/pcs_qc_release/scripts/run_pcs_v01_clean_chain.ps1

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,18 +89,19 @@ Step "LabTrust-Gym: attach certificate"
8989
$PfCmd = Get-Command $PfBin -ErrorAction SilentlyContinue
9090
if (-not $PfCmd) { throw "Provability Fabric CLI not found: $PfBin" }
9191

92-
Step "LabTrust: write PF handoff guard (release-run manifests)"
92+
Step "LabTrust: write PF HandoffManifest.v0 (release-run manifests)"
9393
& $Python (Join-Path $PSScriptRoot "finalize_release_run.py") --run-dir $Work --no-promote
9494

9595
Step "Provability Fabric: verify and sign (handoff certified bundle)"
9696
& $Python -c @"
9797
from pathlib import Path
98-
from labtrust_gym.pcs.release_run import HANDOFF_FOR_PF_NAME, validate_handoff_directory
98+
from labtrust_gym.pcs.handoff_manifest import HANDOFF_TO_PF_NAME
99+
from labtrust_gym.pcs.release_run import validate_handoff_directory
99100
import json
100101
work = Path(r'$Work')
101102
validate_handoff_directory(work)
102-
guard = json.loads((work / HANDOFF_FOR_PF_NAME).read_text(encoding='utf-8'))
103-
print('OK PF input', guard['certified_bundle'], guard['expected_certificate_id'])
103+
handoff = json.loads((work / HANDOFF_TO_PF_NAME).read_text(encoding='utf-8'))
104+
print('OK PF input science_claim_bundle.certified.json', handoff['invariants']['certificate_id'])
104105
"@
105106

106107
& $PfBin verify science-claim $CertifiedJson --out $VerificationJson

examples/pcs_qc_release/scripts/run_pcs_v01_clean_chain.sh

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,20 +70,22 @@ pcs validate "$CERTIFIED_JSON"
7070

7171
pcs_require_cmd "$PF_BIN"
7272

73-
pcs_step "LabTrust: write PF handoff guard (release-run manifests)"
73+
pcs_step "LabTrust: write PF HandoffManifest.v0 (release-run manifests)"
7474
python "$SCRIPT_DIR/finalize_release_run.py" --run-dir "$WORK" --no-promote
7575

7676
pcs_step "Provability Fabric: verify and sign (handoff certified bundle)"
7777
python -c "
7878
from pathlib import Path
79-
from labtrust_gym.pcs.release_run import HANDOFF_FOR_PF_NAME, validate_handoff_directory
79+
from labtrust_gym.pcs.handoff_manifest import HANDOFF_TO_PF_NAME
80+
from labtrust_gym.pcs.release_run import validate_handoff_directory
8081
work = Path('$WORK')
8182
validate_handoff_directory(work)
82-
guard = __import__('json').loads((work / HANDOFF_FOR_PF_NAME).read_text())
83-
cert = work / guard['certified_bundle']
83+
handoff = __import__('json').loads((work / HANDOFF_TO_PF_NAME).read_text())
84+
cert_name = 'science_claim_bundle.certified.json'
85+
cert = work / cert_name
8486
if not cert.is_file():
8587
raise SystemExit(f'missing PF input bundle: {cert}')
86-
print('OK PF input', cert.name, 'certificate_id', guard['expected_certificate_id'])
88+
print('OK PF input', cert.name, 'certificate_id', handoff['invariants']['certificate_id'])
8789
"
8890

8991
"$PF_BIN" verify science-claim "$CERTIFIED_JSON" --out "$VERIFICATION_JSON"

src/labtrust_gym/pcs/release_fixtures.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@
99
from labtrust_gym.config import get_repo_root
1010
from labtrust_gym.pcs.manifest import validate_release_manifest
1111
from labtrust_gym.pcs.release_provenance import validate_release_artifact_provenance
12-
from labtrust_gym.pcs.handoff_manifest import HANDOFF_TO_PF_NAME
1312
from labtrust_gym.pcs.release_fragment import LABTRUST_RELEASE_FRAGMENT_NAME
1413
from labtrust_gym.pcs.release_protocol import assert_no_legacy_pf_handoff
1514
from labtrust_gym.pcs.release_handoff import verify_release_handoff
1615
from labtrust_gym.pcs.release_run import (
17-
HANDOFF_FOR_PF_NAME,
16+
HANDOFF_TO_PF_NAME,
1817
RELEASE_HANDOFF_MANIFEST_NAME,
1918
validate_certificate_id_chain,
2019
validate_handoff_directory,
@@ -178,10 +177,13 @@ def validate_release_fixtures(directory: Path | None = None) -> list[str]:
178177

179178
handoff_root = root / "handoff"
180179
if handoff_root.is_dir():
180+
from labtrust_gym.pcs.release_protocol import assert_no_legacy_handoff_subdir_guard
181+
182+
assert_no_legacy_handoff_subdir_guard(handoff_root)
181183
validate_handoff_directory(handoff_root)
182184
ok.append("handoff/")
183-
if (handoff_root / HANDOFF_FOR_PF_NAME).is_file():
184-
ok.append(f"handoff/{HANDOFF_FOR_PF_NAME}")
185+
if (handoff_root / HANDOFF_TO_PF_NAME).is_file():
186+
ok.append(f"handoff/{HANDOFF_TO_PF_NAME}")
185187
if (handoff_root / RELEASE_HANDOFF_MANIFEST_NAME).is_file():
186188
ok.append(f"handoff/{RELEASE_HANDOFF_MANIFEST_NAME}")
187189

src/labtrust_gym/pcs/release_protocol.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@
1515
)
1616

1717
LEGACY_PF_HANDOFF_NAME = "pf_handoff.json"
18+
LEGACY_HANDOFF_SUBDIR_GUARD = "handoff_for_pf.json"
19+
20+
21+
def assert_no_legacy_handoff_subdir_guard(handoff_root: Path) -> None:
22+
"""``release/handoff/`` must use HandoffManifest.v0, not legacy guard JSON."""
23+
legacy = handoff_root.resolve() / LEGACY_HANDOFF_SUBDIR_GUARD
24+
if legacy.is_file():
25+
raise ValueError(
26+
f"handoff/ must not contain legacy {LEGACY_HANDOFF_SUBDIR_GUARD}; "
27+
f"use {HANDOFF_TO_PF_NAME}"
28+
)
1829

1930

2031
def assert_no_legacy_pf_handoff(release_root: Path) -> None:

src/labtrust_gym/pcs/release_run.py

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
)
3535

3636
RELEASE_HANDOFF_MANIFEST_NAME = "RELEASE_HANDOFF_MANIFEST.json"
37-
HANDOFF_FOR_PF_NAME = "handoff_for_pf.json"
37+
HANDOFF_TO_PF_NAME = "handoff_to_pf.json"
38+
HANDOFF_FOR_PF_NAME = HANDOFF_TO_PF_NAME # deprecated alias
3839
RELEASE_FIXTURE_MANIFEST_NAME = "RELEASE_FIXTURE_MANIFEST.json"
3940
LEGACY_MANIFEST_NAME = "manifest.json"
4041

@@ -196,16 +197,19 @@ def build_handoff_manifest(
196197
return manifest
197198

198199

199-
def build_handoff_for_pf(run_dir: Path) -> dict[str, Any]:
200-
certified = _load_json(run_dir / "science_claim_bundle.certified.json")
201-
_, certificate_id, trace_hash = certified_bundle_ids(certified)
202-
return {
203-
"schema_version": "v0",
204-
"certified_bundle": "science_claim_bundle.certified.json",
205-
"expected_certificate_id": certificate_id,
206-
"expected_trace_hash": trace_hash,
207-
"expected_bundle_id": certified["bundle_id"],
208-
}
200+
def build_handoff_for_pf(
201+
run_dir: Path,
202+
*,
203+
source_commit: str | None = None,
204+
) -> dict[str, Any]:
205+
"""Build HandoffManifest.v0 for PF signing (legacy name retained for callers)."""
206+
from labtrust_gym.pcs.handoff_manifest import build_bundle_to_verifier_handoff
207+
208+
return build_bundle_to_verifier_handoff(
209+
run_dir / "science_claim_bundle.certified.json",
210+
release_mode=True,
211+
source_commit=source_commit,
212+
)
209213

210214

211215
def build_release_fixture_manifest(
@@ -248,7 +252,7 @@ def write_run_manifests(
248252
generator: str,
249253
handoff_id: str | None = None,
250254
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
251-
"""Write RELEASE_HANDOFF_MANIFEST, handoff_for_pf, and RELEASE_FIXTURE_MANIFEST into run_dir."""
255+
"""Write RELEASE_HANDOFF_MANIFEST, HandoffManifest.v0, and RELEASE_FIXTURE_MANIFEST into run_dir."""
252256
for name in HANDOFF_ARTIFACTS:
253257
if not (run_dir / name).is_file():
254258
raise FileNotFoundError(f"release-run missing handoff artifact: {name}")
@@ -258,7 +262,7 @@ def write_run_manifests(
258262
handoff_manifest = build_handoff_manifest(
259263
run_dir, commits, generator=generator, handoff_id=handoff_id
260264
)
261-
handoff_pf = build_handoff_for_pf(run_dir)
265+
handoff_pf = build_handoff_for_pf(run_dir, source_commit=commits["labtrust_gym_commit"])
262266
fixture_manifest = build_release_fixture_manifest(
263267
run_dir, commits, handoff_manifest, generator=generator
264268
)
@@ -309,16 +313,30 @@ def validate_handoff_directory(handoff_root: Path) -> None:
309313
path = handoff_root / name
310314
if file_content_digest(path) != expected_digest:
311315
raise ValueError(f"handoff digest mismatch for {name}")
312-
pf_guard = handoff_root / HANDOFF_FOR_PF_NAME
313-
if not pf_guard.is_file():
314-
raise FileNotFoundError(f"handoff missing: {HANDOFF_FOR_PF_NAME}")
315-
pf_doc = _load_json(pf_guard)
316-
certified = _load_json(handoff_root / pf_doc["certified_bundle"])
316+
from labtrust_gym.pcs.handoff_manifest import assert_handoff_manifest_valid
317+
318+
handoff_path = handoff_root / HANDOFF_TO_PF_NAME
319+
if not handoff_path.is_file():
320+
raise FileNotFoundError(f"handoff missing: {HANDOFF_TO_PF_NAME}")
321+
legacy_guard = handoff_root / "handoff_for_pf.json"
322+
if legacy_guard.is_file() and legacy_guard != handoff_path:
323+
raise ValueError(
324+
"handoff/ must not contain legacy handoff_for_pf.json; use handoff_to_pf.json"
325+
)
326+
pf_doc = _load_json(handoff_path)
327+
assert_handoff_manifest_valid(pf_doc)
328+
certified_name = "science_claim_bundle.certified.json"
329+
certified = _load_json(handoff_root / certified_name)
317330
_, cert_id, trace_hash = certified_bundle_ids(certified)
318-
if pf_doc["expected_certificate_id"] != cert_id:
319-
raise ValueError("handoff_for_pf.expected_certificate_id mismatch")
320-
if pf_doc["expected_trace_hash"] != trace_hash:
321-
raise ValueError("handoff_for_pf.expected_trace_hash mismatch")
331+
inv = pf_doc.get("invariants") or {}
332+
if inv.get("certificate_id") != cert_id:
333+
raise ValueError("handoff_to_pf invariants.certificate_id mismatch")
334+
if inv.get("trace_hash") != trace_hash:
335+
raise ValueError("handoff_to_pf invariants.trace_hash mismatch")
336+
entry = (pf_doc.get("input_artifacts") or {}).get(certified_name) or {}
337+
on_disk = file_content_digest(handoff_root / certified_name)
338+
if entry.get("sha256") != on_disk:
339+
raise ValueError("handoff_to_pf input_artifacts certified bundle sha256 mismatch")
322340

323341

324342
def promote_release_run_atomic(

src/labtrust_gym/pcs/sync_pcs_core_rc.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,9 @@
2323
)
2424
from labtrust_gym.pcs.release_run import (
2525
HANDOFF_ARTIFACTS,
26-
HANDOFF_FOR_PF_NAME,
2726
RELEASE_FIXTURE_MANIFEST_NAME,
2827
RELEASE_HANDOFF_MANIFEST_NAME,
2928
DOWNSTREAM_ARTIFACTS,
30-
build_handoff_for_pf,
3129
build_handoff_manifest,
3230
file_content_digest,
3331
certified_bundle_ids,
@@ -40,6 +38,26 @@
4038
RELEASE_FIXTURE_MANIFEST_NAME,
4139
)
4240

41+
_RELEASE_README_PATHS: tuple[str, ...] = ("README.md", "handoff/README.md")
42+
43+
44+
def _preserve_release_readmes(release_root: Path) -> dict[str, str]:
45+
preserved: dict[str, str] = {}
46+
release_root = release_root.resolve()
47+
for rel in _RELEASE_README_PATHS:
48+
path = release_root / rel
49+
if path.is_file():
50+
preserved[rel] = path.read_text(encoding="utf-8")
51+
return preserved
52+
53+
54+
def _restore_release_readmes(release_root: Path, preserved: dict[str, str]) -> None:
55+
release_root = release_root.resolve()
56+
for rel, text in preserved.items():
57+
path = release_root / rel
58+
path.parent.mkdir(parents=True, exist_ok=True)
59+
path.write_text(text, encoding="utf-8")
60+
4361

4462
def pcs_core_labtrust_release_dir(labtrust_root: Path | None = None) -> Path:
4563
root = labtrust_root or get_repo_root()
@@ -382,24 +400,29 @@ def sync_release_from_pcs_core_rc(
382400
json.dumps(handoff_manifest, indent=2, sort_keys=True) + "\n",
383401
encoding="utf-8",
384402
)
385-
(handoff_staging / HANDOFF_FOR_PF_NAME).write_text(
386-
json.dumps(build_handoff_for_pf(staging), indent=2, sort_keys=True) + "\n",
387-
encoding="utf-8",
388-
)
389-
390403
build_canonical_release_manifest(
391404
staging,
392405
handoff_manifest,
393406
generator=generator,
394407
certifyedge_bin="certifyedge",
395408
certifyedge_spec="CertifyEdge/templates/hospital_lab/qc_release.stl",
396409
)
397-
build_pf_handoff(staging, _load(staging / "manifest.json"))
410+
manifest = _load(staging / "manifest.json")
411+
build_pf_handoff(staging, manifest)
398412
write_trace_hash_alignment(staging)
399413

414+
from labtrust_gym.pcs.handoff_manifest import build_handoff_to_pf_from_release
415+
416+
build_handoff_to_pf_from_release(handoff_staging, manifest)
417+
legacy_handoff_guard = handoff_staging / "handoff_for_pf.json"
418+
if legacy_handoff_guard.is_file():
419+
legacy_handoff_guard.unlink()
420+
421+
readmes = _preserve_release_readmes(local)
400422
if local.exists():
401423
shutil.rmtree(local)
402424
shutil.move(str(staging), str(local))
425+
_restore_release_readmes(local, readmes)
403426

404427
assert_release_matches_pcs_core_rc(local, canon)
405428
verify_release_handoff(local)

0 commit comments

Comments
 (0)