railiance-apps/tools/coulomb-image-release.py
tegwick 9c74884641
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Prepare guarded content release after inactive backup installation
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
2026-10-31 13:20:46 +01:00

198 lines
7.8 KiB
Python

"""Prepare/read-only-check a sealed release; live mode is attended founder work.
This launcher does not mint an approval or bypass gated-executor. The existing
gated-executor profile covers Kubernetes, not the root-owned backup helper refresh.
Only the founder runs --execute in their own terminal after reviewing the packet.
"""
from __future__ import annotations
import argparse
import hashlib
import io
import json
import os
import shlex
import subprocess
import sys
import tarfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PLAN = ROOT / "release/coulomb-identity-export-23ffe2c.json"
def canonical(value):
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
def prepare(plan_path=None):
plan_path = plan_path or PLAN
if plan_path.is_symlink() or not plan_path.resolve().is_relative_to(ROOT.resolve()):
raise RuntimeError("reviewed_plan_inside_owner_repository_required")
plan = json.loads(plan_path.read_text())
values = plan.get("values_file", "release/identity-export-values.yaml")
snapshot = plan.get("snapshot_file", "release/identity-export-snapshot.py")
evidence = plan.get("evidence_directory", "release/identity-export-evidence")
for name in (values, snapshot, evidence):
if (not isinstance(name, str) or Path(name).is_absolute()
or ".." in Path(name).parts or not name.startswith("release/")):
raise RuntimeError("reviewed_release_paths_required")
if not all((ROOT / name).is_file() for name in (values, snapshot)):
raise RuntimeError("reviewed_release_artifacts_missing")
paths = [
ROOT / name
for name in (
"tools/coulomb-image-release.py",
"tools/coulomb_image_release_node.py",
"tools/coulomb_image_probe.py",
values,
snapshot,
)
]
paths += sorted((ROOT / "charts/coulomb-social").rglob("*"))
paths += sorted((ROOT / evidence).glob("*.json"))
paths = [p for p in paths if p.is_file()]
if any(p.is_symlink() or not p.resolve().is_relative_to(ROOT) for p in paths):
raise RuntimeError("regular_reviewed_files_required")
content = {str(p.relative_to(ROOT)): p.read_bytes() for p in paths}
plan["files"] = {
name: hashlib.sha256(data).hexdigest() for name, data in content.items()
}
for name, expected in plan["rehearsal_evidence_sha256"].items():
if plan["files"].get(evidence + "/" + name) != expected:
raise RuntimeError("reviewed_candidate_evidence_changed")
digest = "sha256:" + hashlib.sha256(canonical(plan)).hexdigest()
content["manifest.json"] = canonical(plan)
return plan, digest, content
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--preflight", action="store_true")
parser.add_argument("--execute", action="store_true")
parser.add_argument("--expect-digest")
parser.add_argument("--output", type=Path)
parser.add_argument("--plan", type=Path, default=PLAN)
args = parser.parse_args()
if args.preflight and args.execute:
raise SystemExit("choose read-only preflight or attended execution")
plan, digest, content = prepare(args.plan)
if args.expect_digest and args.expect_digest != digest:
raise SystemExit("reviewed proposal changed; do not execute")
if not args.preflight and not args.execute:
print(
json.dumps(
{
"status": "prepared",
"proposal_digest": digest,
"file_count": len(content),
"candidate": plan["candidate_image"],
"live_changes": False,
}
)
)
return
if args.execute:
if not args.expect_digest or not sys.stdin.isatty():
raise SystemExit(
"founder must review and run the exact digest in their own terminal"
)
print("Reviewed proposal: " + digest)
print(
"Coulomb-only image update; existing database schema retained, fresh paired backup, postflight and backup image-pin refresh."
)
if (
input("Type DEPLOY followed by the full digest to proceed: ").strip()
!= "DEPLOY " + digest
):
raise SystemExit("not approved; no live change")
buffer = io.BytesIO()
with tarfile.open(fileobj=buffer, mode="w:") as archive:
for name, data in content.items():
entry = tarfile.TarInfo(name)
entry.size, entry.mode = len(data), 0o600
archive.addfile(entry, io.BytesIO(data))
remote = "/tmp/csoc-reviewed-image-" + digest.removeprefix("sha256:")
receiver = r"""
import hashlib,io,json,os,pathlib,sys,tarfile
root=pathlib.Path(sys.argv[1]);expected=sys.argv[2]
raw=sys.stdin.buffer.read(2*1024**2+1);assert len(raw)<=2*1024**2
with tarfile.open(fileobj=io.BytesIO(raw),mode='r:') as archive:
entries=archive.getmembers();assert len({e.name for e in entries})==len(entries)
assert all(e.isfile() and not pathlib.Path(e.name).is_absolute() and '..' not in pathlib.Path(e.name).parts for e in entries)
data={e.name:archive.extractfile(e).read() for e in entries}
plan=json.loads(data['manifest.json'])
canonical=json.dumps(plan,sort_keys=True,separators=(',',':')).encode()
assert 'sha256:'+hashlib.sha256(canonical).hexdigest()==expected
assert set(data)==set(plan['files'])|{'manifest.json'}
for name,digest in plan['files'].items():assert hashlib.sha256(data[name]).hexdigest()==digest
root.mkdir(mode=0o700,exist_ok=True)
assert root.lstat().st_uid==os.getuid() and root.lstat().st_mode&0o077==0
for name,value in data.items():
target=root/name;target.parent.mkdir(parents=True,exist_ok=True,mode=0o700)
if target.exists():assert not target.is_symlink() and target.read_bytes()==value;continue
fd=os.open(target,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'wb') as stream:stream.write(value)
"""
staged = subprocess.run(
[
"ssh",
"railiance01",
"python3 -c "
+ shlex.quote(receiver)
+ " "
+ shlex.quote(remote)
+ " "
+ shlex.quote(digest),
],
input=buffer.getvalue(),
capture_output=True,
timeout=60,
check=False,
)
if staged.returncode:
raise SystemExit("reviewed capsule staging refused; no live change")
node_hash = plan["files"]["tools/coulomb_image_release_node.py"]
guard = (
"import hashlib,pathlib,sys;p=pathlib.Path("
+ repr(remote + "/tools/coulomb_image_release_node.py")
+ ");"
"b=p.read_bytes();assert hashlib.sha256(b).hexdigest()=="
+ repr(node_hash)
+ ";"
"sys.argv=['reviewed-release','--capsule',"
+ repr(remote)
+ ",'--expect-digest',"
+ repr(digest)
+ ","
+ repr("--execute" if args.execute else "--preflight")
+ "];exec(compile(b,str(p),'exec'),{'__name__':'__main__'})"
)
result = subprocess.run(
["ssh", "railiance01", "sudo -n python3 -c " + shlex.quote(guard)],
capture_output=True,
timeout=3600,
check=False,
)
try:
receipt = json.loads(result.stdout)
except (ValueError, TypeError):
receipt = {
"status": "remote_execution_unconfirmed",
"proposal_digest": digest,
"journal": "/var/lib/coulomb-social-backup/releases/"
+ digest.removeprefix("sha256:")
+ "/receipt.json",
}
if args.output:
fd = os.open(args.output, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "w") as stream:
json.dump(receipt, stream, indent=2)
print(json.dumps(receipt))
if result.returncode:
raise SystemExit(1)
if __name__ == "__main__":
main()