diff --git a/scripts/rpf-wp-0015-e2-receipt.py b/scripts/rpf-wp-0015-e2-receipt.py new file mode 100755 index 0000000..cf3594e --- /dev/null +++ b/scripts/rpf-wp-0015-e2-receipt.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Write the RPF-WP-0015-T03 synthetic E2 approval receipt for one founder window. + +Usage: scripts/rpf-wp-0015-e2-receipt.py OUTPUT.json [--start-in-min N] + +The window is 15 minutes and starts now, or N minutes from now. Writing the +receipt and running the window is the founder GO for that window; D5 +(2fd5849c) authorizes the exercise class only. The owner acknowledgements are +the round-9 approvals of review contract 014627d6 (audit-core 0d0da0d2, +rapp-postgres 6d047a08, railiance-platform c4a80a60). They stay valid only +while the pinned artifacts in interfaces/RAILIANCE-WP-0024-owner-reviews.json +are unchanged; the script refuses otherwise. The receipt holds no credential +values. It refuses to overwrite an existing file. +""" +import hashlib +import json +import sys +from datetime import datetime, timedelta, timezone +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +pins = json.loads((ROOT / "interfaces/RAILIANCE-WP-0024-owner-reviews.json").read_text())["artifact_sha256"] +drift = [p for p, h in pins.items() if hashlib.sha256((ROOT / p).read_bytes()).hexdigest() != h] +if drift: + sys.exit("owner-reviewed artifacts changed since contract 014627d6; new owner approvals needed: " + ", ".join(drift)) +if len(sys.argv) < 2: + sys.exit(__doc__) +out = sys.argv[1] +delay = int(sys.argv[sys.argv.index('--start-in-min') + 1]) if '--start-in-min' in sys.argv else 0 +start = datetime.now(timezone.utc).replace(second=0, microsecond=0) + timedelta(minutes=delay) +end = start + timedelta(minutes=15) +tag = start.strftime('%Y%m%dt%H%M') +z = lambda t: t.strftime('%Y-%m-%dT%H:%M:%SZ') +receipt = { + "procedure": "audit-core-database-lease-recovery", + "task_id": "RAILIANCE-WP-0024-T03", + "status": "approved", + "approval_id": f"rpf-wp-0015-t03-e2-{tag}z-founder-go", + "window": {"start": z(start), "end": z(end)}, + "abort_operator": "founder (platform-root), attended", + "owners": { + "audit-core": {"acknowledged": True, "message_id": "0d0da0d2-36a1-4ffc-b7bf-7896fad691f6"}, + "rapp-postgres": {"acknowledged": True, "message_id": "6d047a08-e9c8-4dbf-95b9-dca039791329"}, + "railiance-platform": {"acknowledged": True, "message_id": "c4a80a60-a415-4f2b-8928-751407473641"}, + }, + "synthetic_load": {"contract_id": "RAILIANCE-WP-0024-T02-SYNTHETIC-01", + "driver_revision": "sha256:941ba251f638869626b06e9cbf430c70188c0bdf4715e3eff9c7610366f68662"}, + "synthetic_sender": {"identity": f"t03-e2-synthetic-sender-{start:%Y%m%d}", + "source": f"t03-e2-synthetic-{start:%Y%m%d}", + "tenant": f"tenant:trial:t03-e2-synthetic-{start:%Y%m%d}", + "expires_at": z(end)}, +} +with open(out, 'x') as f: + json.dump(receipt, f, indent=2); f.write('\n') +print(json.dumps({"approval_id": receipt["approval_id"], "window": receipt["window"], + "confirm": f"audit-core-synthetic-sender:{receipt['approval_id']}:attended"})) diff --git a/tests/test_rpf_wp_0015_e2_receipt.py b/tests/test_rpf_wp_0015_e2_receipt.py new file mode 100644 index 0000000..8dd2f23 --- /dev/null +++ b/tests/test_rpf_wp_0015_e2_receipt.py @@ -0,0 +1,35 @@ +"""The E2 receipt generator emits a receipt the sender lane accepts as T03.""" +import importlib.util +import json +import subprocess +import sys +from datetime import UTC, datetime +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def lane(): + sys.path.insert(0, str(ROOT / "scripts")) + spec = importlib.util.spec_from_file_location("lane_e2", ROOT / "scripts/audit_core_synthetic_sender_lane.py") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_receipt_is_accepted_by_the_lane_as_t03(tmp_path): + out = tmp_path / "approval.json" + printed = json.loads(subprocess.run([sys.executable, str(ROOT / "scripts/rpf-wp-0015-e2-receipt.py"), str(out)], + check=True, capture_output=True, text=True).stdout) + m = lane() + approval, spec = m.load_receipt(out, datetime.now(UTC), require_open_window=True) + assert approval["task_id"] == "RAILIANCE-WP-0024-T03" + assert printed["confirm"] == m.confirm_token(approval) + + +def test_refuses_to_overwrite(tmp_path): + out = tmp_path / "approval.json" + out.write_text("{}") + result = subprocess.run([sys.executable, str(ROOT / "scripts/rpf-wp-0015-e2-receipt.py"), str(out)], + capture_output=True, text=True) + assert result.returncode != 0 and out.read_text() == "{}" diff --git a/workplans/RPF-WP-0015-audit-core-custody-and-recovery-coordination.md b/workplans/RPF-WP-0015-audit-core-custody-and-recovery-coordination.md index 4adc132..a952d59 100644 --- a/workplans/RPF-WP-0015-audit-core-custody-and-recovery-coordination.md +++ b/workplans/RPF-WP-0015-audit-core-custody-and-recovery-coordination.md @@ -9,7 +9,7 @@ flavor: residual owner: codex topic_slug: railiance created: "2026-08-22" -updated: "2026-10-14" +updated: "2026-11-03" related: - AUDIT-WP-0008 - WH-ENG-20260822-AUDIT-E2-01 @@ -535,3 +535,18 @@ and warden failed closed with self-revocation. Nothing was projected and audit-c untouched. Next: sign out of KeyCape (or use a fresh private window), complete password+OTP (aal2), reissue a near E2 receipt and run the launcher once. T03 stays `wait` until that E2 evidence exists. + +E2 window prepared 2026-11-03. No new owner round is needed. Receipt +acknowledgements are the owners' approval receipts for the review contract, +not per-window sign-offs; T02 used the `ea3f53d6` approvals the same way. All +pinned artifacts in `interfaces/RAILIANCE-WP-0024-owner-reviews.json` still +match round 9 (contract `014627d6`: audit-core `0d0da0d2`, rapp-postgres +`6d047a08`, railiance-platform `c4a80a60`). The load driver +(`audit-core/scripts/t02_synthetic_load_driver.py`) still has revision +`sha256:941ba251…`. The installed ops-warden contains `6e14300`. +`scripts/rpf-wp-0015-e2-receipt.py` writes the 15-minute receipt and prints +the confirm string. It refuses if any pinned artifact has changed, and the +sender lane accepts its receipt as T03 +(`tests/test_rpf_wp_0015_e2_receipt.py`). The 2026-10-14/15 blocker was the +KeyCape session at `aal1`. The window needs a fresh password+OTP (`aal2`) +login, and its start must not fall within 20 minutes of 03:17 UTC.