railiance-platform/scripts/rpf-wp-0015-e2-receipt.py
codex 17fdc27465 Let the audit-core recovery scripts use the attended admin kubeconfig.
Since RCLUSTER-WP-0019 the node account cannot read the k3s admin kubeconfig,
so ssh railiance01 kubectl fails at read node state. With AUDIT_KUBECONFIG
set, cluster reads and the port-forward run locally against that file; host
reads stay on ssh. Unset keeps the earlier path. The E2 receipt script takes
owner approvals for the current contract digest from the hub.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 236506@bnt-lap001
Assistant-Session: 09087018-91e1-48c5-ab2f-acfbb62c92e4
2026-11-03 15:05:45 +01:00

93 lines
4.1 KiB
Python
Executable file

#!/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 owners' latest State Hub `approve` receipts
for the *current* review contract
(interfaces/RAILIANCE-WP-0024-owner-reviews.json). They are found by subject
`WP0024-OWNER-RECEIPT/v1/<owner>/approve/<contract digest>`. The script
refuses when any pinned artifact differs from the contract, or when a
required owner has no approval for this digest. The receipt holds no
credential values, and the script never overwrites an existing file.
"""
import importlib.util
import json
import os
import sys
import urllib.parse
import urllib.request
from datetime import datetime, timedelta, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OWNERS = ("audit-core", "rapp-postgres", "railiance-platform")
def review_module():
spec = importlib.util.spec_from_file_location("wp0024_review", ROOT / "scripts/wp0024-owner-review.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def approvals(review, contract, api_base):
digest = review.contract_digest(contract)
query = urllib.parse.urlencode({"to_agent": "railiance-platform", "limit": 1000})
with urllib.request.urlopen(f"{api_base}/messages/?{query}", timeout=20) as response:
messages = json.load(response)
found = {}
for owner in OWNERS:
subject = f"{review.RECEIPT_PREFIX}/v1/{owner}/approve/{digest}"
hits = sorted((m for m in messages if m.get("subject") == subject and m.get("from_agent") == owner),
key=lambda m: m["created_at"])
if not hits:
sys.exit(f"no {owner} approval for contract {digest[:8]}; run the owner round first")
found[owner] = {"acknowledged": True, "message_id": hits[-1]["id"]}
return digest, found
def main(argv):
if len(argv) < 2:
sys.exit(__doc__)
review = review_module()
contract = review.load_contract()
drift = [p for p, h in contract["artifact_sha256"].items() if review.file_sha256(ROOT / p) != h]
if drift:
sys.exit("reviewed artifacts differ from the owner-review contract: " + ", ".join(drift))
digest, owners = approvals(review, contract, os.environ.get("STATE_HUB_URL", "http://127.0.0.1:8000"))
out = argv[1]
delay = int(argv[argv.index("--start-in-min") + 1]) if "--start-in-min" in 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") # noqa: E731
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": owners,
"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 handle:
json.dump(receipt, handle, indent=2)
handle.write("\n")
print(json.dumps({"approval_id": receipt["approval_id"], "window": receipt["window"],
"contract_digest": digest,
"confirm": f"audit-core-synthetic-sender:{receipt['approval_id']}:attended"}))
if __name__ == "__main__":
main(sys.argv)