#!/usr/bin/env python3 """Data-driven, attended ephemeral credential custody lifecycle. The command never prints or persists bearer values. Live projection requires a current Whitehat broker-readiness receipt in State Hub and an exact attended confirmation. Cleanup is scoped only by the validated projection contract and receipt. """ from __future__ import annotations import argparse import json import os import secrets import stat import subprocess import sys import tempfile import time from datetime import UTC, datetime from pathlib import Path from typing import Any, Callable SCRIPT_DIR = Path(__file__).resolve().parent if str(SCRIPT_DIR) not in sys.path: sys.path.insert(0, str(SCRIPT_DIR)) from custody_contract import ( # noqa: E402 CLEANUP_INTERFACE, PROJECTION_INTERFACE, WORKPLAN_ID, ContractError, canonical_json, contract_digest, current_broker_receipt, digest, load_json, parse_time, resource_names, validate_cleanup_receipt, validate_projection_contract, validate_projection_receipt, ) from remote_exec import RemoteExecutionError, run_remote # noqa: E402 DEFAULT_API_BASE = os.environ.get("STATE_HUB_URL", "http://127.0.0.1:8000") class ProcedureError(RuntimeError): pass def utc_now() -> datetime: return datetime.now(UTC) def rfc3339(value: datetime) -> str: return value.astimezone(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z") def safe_run( command: list[str], *, label: str, env: dict[str, str] | None = None, input_text: str | None = None, allow_missing: bool = False, runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run, ) -> subprocess.CompletedProcess[str]: completed = runner( command, text=True, input=input_text, capture_output=True, env=env, check=False, ) if completed.returncode and not allow_missing: raise ProcedureError(f"{label} failed (exit {completed.returncode})") return completed def secure_unlink(path: Path) -> None: if not path.exists(): return size = path.stat().st_size with path.open("r+b", buffering=0) as handle: handle.write(b"\0" * size) handle.flush() os.fsync(handle.fileno()) path.unlink() def write_receipt(path: Path, value: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) temporary = Path(temporary_name) try: os.fchmod(descriptor, 0o600) with os.fdopen(descriptor, "w", encoding="utf-8") as handle: handle.write(json.dumps(value, indent=2, sort_keys=True) + "\n") handle.flush() os.fsync(handle.fileno()) os.replace(temporary, path) os.chmod(path, 0o600) finally: if temporary.exists(): temporary.unlink() class Operator: def __init__(self, contract: dict[str, Any], token_file: Path) -> None: mode = stat.S_IMODE(token_file.stat().st_mode) if token_file.exists() else None if mode != 0o600: raise ProcedureError(f"OpenBao token file must exist with mode 0600: {token_file}") token = token_file.read_text(encoding="utf-8").splitlines()[0].strip() if not token: raise ProcedureError("OpenBao token file is empty") self.contract = contract self.remote = contract["authority"]["remote"] self.bao_env = dict( os.environ, BAO_ADDR=os.environ.get("BAO_ADDR", "https://bao.coulomb.social"), BAO_TOKEN=token, ) def bao( self, args: list[str], *, label: str, input_text: str | None = None, allow_missing: bool = False, ) -> subprocess.CompletedProcess[str]: return safe_run( ["bao", *args], label=label, env=self.bao_env, input_text=input_text, allow_missing=allow_missing, ) def kubectl( self, args: list[str], *, label: str, input_text: str | None = None, allow_missing: bool = False, ) -> subprocess.CompletedProcess[str]: try: return run_remote( self.remote, ["kubectl", *args], label=label, input_text=input_text, allow_missing=allow_missing, ) except RemoteExecutionError as exc: raise ProcedureError(str(exc)) from exc def bao_exists(self, args: list[str], *, label: str) -> bool: result = self.bao(args, label=label, allow_missing=True) if result.returncode == 0: return True error = result.stderr.lower() if "404" in error or "no value found" in error or "no policy named" in error: return False raise ProcedureError(f"{label} failed without a confirmed not-found response") def kubectl_exists(self, args: list[str], *, label: str) -> bool: result = self.kubectl(args, label=label, allow_missing=True) return result.returncode == 0 and bool(result.stdout.strip()) def registry(self) -> list[dict[str, Any]]: authority = self.contract["authority"] result = self.bao( ["kv", "get", f"-field={authority['registry_field']}", authority["registry_path"]], label="read sender registry", ) try: value = json.loads(result.stdout) except json.JSONDecodeError as exc: raise ProcedureError("sender registry is not valid JSON") from exc if not isinstance(value, list): raise ProcedureError("sender registry must be a list") return value def write_registry(self, registry: list[dict[str, Any]]) -> None: authority = self.contract["authority"] with tempfile.TemporaryDirectory(prefix="railiance-custody-registry-") as directory: root = Path(directory) os.chmod(root, 0o700) document = root / "registry.json" document.write_text(canonical_json(registry), encoding="utf-8") os.chmod(document, 0o600) try: self.bao( [ "kv", "patch", authority["registry_path"], f"{authority['registry_field']}=@{document}", ], label="write sender registry", ) finally: secure_unlink(document) def identity_names(contract: dict[str, Any]) -> set[str]: return {item["sender_name"] for item in contract["identities"]} def token_paths(contract: dict[str, Any]) -> dict[str, str]: authority = contract["authority"] prefix = authority["kv_prefix"].strip("/") return { item["handle"]: f"{authority['kv_mount']}/{prefix}/{item['handle']}" for item in contract["identities"] } def add_temporary_identities( registry: list[dict[str, Any]], contract: dict[str, Any], values: dict[str, str] ) -> list[dict[str, Any]]: names = identity_names(contract) present = {str(item.get("name")) for item in registry if isinstance(item, dict)} if present & names: raise ProcedureError(f"temporary identities already exist: {sorted(present & names)}") updated = list(registry) expires = contract["window"]["expires_at"] for identity in contract["identities"]: updated.append( { "name": identity["sender_name"], "tokens": [values[identity["handle"]]], "sources": ["whitehat-security"], "tenants": [identity["tenant"]], "may_write": True, "may_read": True, "expires_at": expires, } ) return updated def remove_temporary_identities( registry: list[dict[str, Any]], contract: dict[str, Any] ) -> tuple[list[dict[str, Any]], list[str]]: names = identity_names(contract) removed = sorted( str(item.get("name")) for item in registry if isinstance(item, dict) and item.get("name") in names ) updated = [ item for item in registry if not isinstance(item, dict) or item.get("name") not in names ] return updated, removed def build_policy(contract: dict[str, Any]) -> str: paths = token_paths(contract) blocks: list[str] = [] for path in paths.values(): mount, relative = path.split("/", 1) blocks.extend( [ f'path "{mount}/data/{relative}" {{', ' capabilities = ["read"]', "}", "", f'path "{mount}/metadata/{relative}" {{', ' capabilities = ["read"]', "}", "", ] ) return "\n".join(blocks).rstrip() + "\n" def build_manifest(contract: dict[str, Any], lease_id: str) -> str: names = resource_names(contract) authority = contract["authority"] runner = contract["runner"] engagement = contract["engagement_id"] annotations = { "custody.railiance/engagement": engagement, "custody.railiance/lease-id": lease_id, "custody.railiance/contract-sha256": contract_digest(contract), } document = { "apiVersion": "v1", "kind": "List", "items": [ { "apiVersion": "external-secrets.io/v1", "kind": "ClusterSecretStore", "metadata": {"name": names["store"], "annotations": annotations}, "spec": { "provider": { "vault": { "server": "http://openbao.openbao.svc:8200", "path": authority["kv_mount"], "version": "v2", "auth": { "kubernetes": { "mountPath": "kubernetes", "role": names["role"], "serviceAccountRef": { "name": authority["eso_service_account"], "namespace": authority["eso_namespace"], }, } }, } }, "conditions": [{"namespaces": [runner["namespace"]]}], }, }, { "apiVersion": "external-secrets.io/v1", "kind": "ExternalSecret", "metadata": { "name": names["external_secret"], "namespace": runner["namespace"], "annotations": annotations, }, "spec": { "refreshInterval": "1m", "secretStoreRef": {"kind": "ClusterSecretStore", "name": names["store"]}, "target": { "name": runner["secret_name"], "creationPolicy": "Owner", "deletionPolicy": "Delete", }, "data": [ { "secretKey": identity["handle"], "remoteRef": { "key": f"{authority['kv_prefix'].strip('/')}/{identity['handle']}", "property": "token", }, } for identity in sorted(contract["identities"], key=lambda item: item["handle"]) ], }, }, ], } return canonical_json(document) def time_state(contract: dict[str, Any], now: datetime) -> str: starts = parse_time(contract["window"]["starts_at"], "window.starts_at") cutoff = parse_time(contract["window"]["projection_cutoff"], "window.projection_cutoff") if now < starts: return "before-window" if now > cutoff: return "projection-cutoff-passed" return "projection-window-open" def target_ready(operator: Operator, contract: dict[str, Any]) -> str: target = contract["target"] image = operator.kubectl( [ "-n", target["namespace"], "get", "deploy", target["deployment"], "-o", f"jsonpath={{.spec.template.spec.containers[?(@.name==\"{target['container']}\")].image}}", ], label="read target image", ).stdout.strip() if not image.endswith("@" + target["image_digest"]): raise ProcedureError("target is not on the contract-approved image digest") ready = operator.kubectl( [ "-n", target["namespace"], "get", "deploy", target["deployment"], "-o", "jsonpath={.status.readyReplicas}/{.status.replicas}", ], label="read target readiness", ).stdout.strip() if ready != "1/1": raise ProcedureError(f"target is not 1/1 Ready (observed {ready or 'unknown'})") return ready def resource_presence(operator: Operator, contract: dict[str, Any]) -> dict[str, Any]: names = resource_names(contract) runner = contract["runner"] paths = token_paths(contract) identities = sorted( identity_names(contract) & { str(item.get("name")) for item in operator.registry() if isinstance(item, dict) } ) exact_paths = sorted( handle for handle, path in paths.items() if operator.bao_exists( ["kv", "metadata", "get", "-format=json", path], label=f"check exact {handle} KV metadata", ) ) resources: list[str] = [] checks = ( ("store", ["get", "clustersecretstore", names["store"], "-o", "name"]), ( "external_secret", ["-n", runner["namespace"], "get", "externalsecret", names["external_secret"], "-o", "name"], ), ( "mounted_secret", ["-n", runner["namespace"], "get", "secret", runner["secret_name"], "-o", "name"], ), ) for label, args in checks: if operator.kubectl_exists(args, label=f"check exact {label}"): resources.append(label) return { "temporary_identities_present": identities, "exact_token_paths_present": exact_paths, "projection_resources_present": resources, } def live_preflight( operator: Operator, contract: dict[str, Any], *, api_base: str = DEFAULT_API_BASE, now: datetime | None = None, ) -> dict[str, Any]: current = (now or utc_now()).astimezone(UTC) operator.bao(["status", "-format=json"], label="read OpenBao status") target = target_ready(operator, contract) runner = contract["runner"] authority = contract["authority"] operator.kubectl(["get", "ns", runner["namespace"], "-o", "name"], label="verify runner namespace") operator.kubectl( ["-n", authority["eso_namespace"], "get", "sa", authority["eso_service_account"], "-o", "name"], label="verify ESO service account", ) presence = resource_presence(operator, contract) broker_ready = True broker_reason = None try: current_broker_receipt(contract, api_base, now=current) except ContractError as exc: broker_ready = False broker_reason = str(exc) return { "interface": "railiance.custody-preflight", "version": 1, "engagement_id": contract["engagement_id"], "projection_contract_digest": contract_digest(contract), "time_state": time_state(contract, current), "broker_ready": broker_ready, "broker_gate": "pass" if broker_ready else "blocked", "broker_reason": broker_reason, "target_image_matches": True, "target_ready": target, "openbao_initialized": True, "openbao_sealed": False, **presence, "secret_values_observed": False, } def write_exact_values(operator: Operator, contract: dict[str, Any], values: dict[str, str]) -> None: with tempfile.TemporaryDirectory(prefix="railiance-custody-values-") as directory: root = Path(directory) os.chmod(root, 0o700) files: list[Path] = [] try: for handle, path in token_paths(contract).items(): document = root / handle document.write_text(values[handle], encoding="utf-8") os.chmod(document, 0o600) files.append(document) operator.bao( [ "kv", "put", path, f"token=@{document}", f"engagement_id={contract['engagement_id']}", f"expires_at={contract['window']['expires_at']}", ], label=f"write exact {handle} KV path", ) finally: for document in files: secure_unlink(document) def configure_projection(operator: Operator, contract: dict[str, Any], lease_id: str) -> None: names = resource_names(contract) authority = contract["authority"] runner = contract["runner"] operator.bao( ["policy", "write", names["policy"], "-"], label="write exact ESO policy", input_text=build_policy(contract), ) operator.bao( [ "write", f"auth/kubernetes/role/{names['role']}", f"bound_service_account_names={authority['eso_service_account']}", f"bound_service_account_namespaces={authority['eso_namespace']}", f"policies={names['policy']}", "ttl=5m", "max_ttl=15m", ], label="write exact ESO Kubernetes role", ) operator.kubectl( ["apply", "-f", "-"], label="apply exact projection manifest", input_text=build_manifest(contract, lease_id), ) operator.kubectl( ["wait", "--for=condition=Ready", f"clustersecretstore/{names['store']}", "--timeout=90s"], label="wait for exact store", ) operator.kubectl( [ "-n", runner["namespace"], "wait", "--for=condition=Ready", f"externalsecret/{names['external_secret']}", "--timeout=90s", ], label="wait for exact ExternalSecret", ) keys = operator.kubectl( [ "-n", runner["namespace"], "get", "secret", runner["secret_name"], "-o", 'go-template={{range $k, $_ := .data}}{{$k}}{{"\\n"}}{{end}}', ], label="verify projected key names", ).stdout.splitlines() expected = sorted(identity["handle"] for identity in contract["identities"]) if sorted(keys) != expected: raise ProcedureError(f"projected Secret has unexpected key names: {sorted(keys)}") def force_sender_sync_and_restart(operator: Operator, contract: dict[str, Any]) -> None: target = contract["target"] namespace = target["namespace"] external_secret = target["sender_external_secret"] before = operator.kubectl( ["-n", namespace, "get", "secret", external_secret, "-o", "jsonpath={.metadata.resourceVersion}"], label="read sender Secret resource version", ).stdout.strip() operator.kubectl( [ "-n", namespace, "annotate", "externalsecret", external_secret, f"force-sync={int(time.time())}", "--overwrite", ], label="force sender registry sync", ) deadline = time.monotonic() + 120 while time.monotonic() < deadline: current = operator.kubectl( ["-n", namespace, "get", "secret", external_secret, "-o", "jsonpath={.metadata.resourceVersion}"], label="poll sender Secret resource version", ).stdout.strip() if current and current != before: break time.sleep(2) else: raise ProcedureError("sender Secret did not refresh within 120 seconds") operator.kubectl( ["-n", namespace, "rollout", "restart", f"deploy/{target['deployment']}"], label="restart sender registry reader", ) operator.kubectl( ["-n", namespace, "rollout", "status", f"deploy/{target['deployment']}", "--timeout=120s"], label="wait for target rollout", ) def delete_projection(operator: Operator, contract: dict[str, Any]) -> None: names = resource_names(contract) runner = contract["runner"] operator.kubectl( ["-n", runner["namespace"], "delete", "externalsecret", names["external_secret"], "--ignore-not-found"], label="delete exact ExternalSecret", ) operator.kubectl( ["-n", runner["namespace"], "delete", "secret", runner["secret_name"], "--ignore-not-found"], label="delete exact mounted Secret", ) operator.kubectl( ["delete", "clustersecretstore", names["store"], "--ignore-not-found"], label="delete exact store", ) for handle, path in token_paths(contract).items(): if operator.bao_exists( ["kv", "metadata", "get", "-format=json", path], label=f"check exact {handle} metadata before delete", ): operator.bao(["kv", "metadata", "delete", path], label=f"delete exact {handle} KV path") if operator.bao_exists(["read", f"auth/kubernetes/role/{names['role']}"] , label="check exact role"): operator.bao(["delete", f"auth/kubernetes/role/{names['role']}"] , label="delete exact role") if operator.bao_exists(["policy", "read", names["policy"]], label="check exact policy"): operator.bao(["policy", "delete", names["policy"]], label="delete exact policy") def verify_cleanup_absent(operator: Operator, contract: dict[str, Any]) -> None: operator.bao(["status", "-format=json"], label="verify OpenBao after cleanup") operator.kubectl(["get", "ns", contract["runner"]["namespace"], "-o", "name"], label="verify Kubernetes after cleanup") presence = resource_presence(operator, contract) if any(presence.values()): raise ProcedureError(f"engagement resources remain after cleanup: {presence}") def cleanup_scope(operator: Operator, contract: dict[str, Any]) -> dict[str, Any]: registry = operator.registry() updated, removed = remove_temporary_identities(registry, contract) if removed: operator.write_registry(updated) force_sender_sync_and_restart(operator, contract) delete_projection(operator, contract) verify_cleanup_absent(operator, contract) target_ready(operator, contract) return { "removed_identities": removed, "removed_resources": sorted( [ *token_paths(contract).values(), *resource_names(contract).values(), ] ), } def transactional( steps: list[Callable[[], None]], rollback: Callable[[], Any] ) -> None: try: for step in steps: step() except Exception as original: try: rollback() except Exception as cleanup_error: raise ProcedureError( "projection failed and exact cleanup could not be proven" ) from cleanup_error raise original def resource_uids( operator: Operator, contract: dict[str, Any], *, require_all: bool = True ) -> dict[str, str]: names = resource_names(contract) runner = contract["runner"] requests = { "store": [ "get", "clustersecretstore", names["store"], "--ignore-not-found", "-o", "jsonpath={.metadata.uid}", ], "external_secret": [ "-n", runner["namespace"], "get", "externalsecret", names["external_secret"], "--ignore-not-found", "-o", "jsonpath={.metadata.uid}", ], "mounted_secret": [ "-n", runner["namespace"], "get", "secret", runner["secret_name"], "--ignore-not-found", "-o", "jsonpath={.metadata.uid}", ], } result = { key: operator.kubectl(args, label=f"read exact {key} UID").stdout.strip() for key, args in requests.items() } if require_all and any(not value for value in result.values()): raise ProcedureError("projection resource UID evidence is incomplete") return {key: value for key, value in result.items() if value} def verify_receipt_resource_scope( operator: Operator, contract: dict[str, Any], receipt: dict[str, Any] ) -> dict[str, str]: present = resource_uids(operator, contract, require_all=False) expected = receipt["resources"]["uids"] mismatched = { key: value for key, value in present.items() if expected.get(key) != value } if mismatched: raise ProcedureError( "live projection resource UID differs from the cleanup receipt; refusing deletion" ) return present def project( operator: Operator, contract: dict[str, Any], *, api_base: str = DEFAULT_API_BASE, now: datetime | None = None, lease_id: str | None = None, ) -> dict[str, Any]: current = (now or utc_now()).astimezone(UTC) if time_state(contract, current) != "projection-window-open": raise ProcedureError("projection is outside the approved projection window") # This is intentionally before token generation and every mutation. broker_receipt = current_broker_receipt(contract, api_base, now=current) preflight = live_preflight(operator, contract, api_base=api_base, now=current) if not preflight["broker_ready"]: raise ProcedureError("consumer broker is not ready") if any( preflight[key] for key in ( "temporary_identities_present", "exact_token_paths_present", "projection_resources_present", ) ): raise ProcedureError("cleanup is required before a new projection") active_lease = lease_id or f"custody:{secrets.token_hex(16)}" values = {identity["handle"]: secrets.token_urlsafe(48) for identity in contract["identities"]} if len(set(values.values())) != len(values): raise ProcedureError("credential generator returned duplicate values") try: transactional( [ lambda: write_exact_values(operator, contract, values), lambda: operator.write_registry( add_temporary_identities(operator.registry(), contract, values) ), lambda: configure_projection(operator, contract, active_lease), lambda: force_sender_sync_and_restart(operator, contract), ], lambda: cleanup_scope(operator, contract), ) finally: values.clear() projected_at = rfc3339(utc_now()) base = { "interface": PROJECTION_INTERFACE, "version": 1, "workplan_id": WORKPLAN_ID, "state": "projected", "lease_id": active_lease, "engagement_id": contract["engagement_id"], "target": { "id": contract["target"]["id"], "revision": contract["target"]["revision"], "image_digest": contract["target"]["image_digest"], }, "projection_contract_digest": contract_digest(contract), "broker_receipt_digest": digest(broker_receipt), "projected_at": projected_at, "expires_at": contract["window"]["expires_at"], "identities": sorted( ( { "handle": item["handle"], "role": item["role"], "sender_name": item["sender_name"], "mount_path": item["mount_path"], } for item in contract["identities"] ), key=lambda item: item["handle"], ), "resources": { "names": resource_names(contract), "uids": resource_uids(operator, contract), }, "cleanup_authority": "railiance-platform", "secret_values_observed": False, } receipt = {**base, "receipt_id": f"sha256:{digest(base)}"} validate_projection_receipt(receipt, contract) return receipt def projection_status( operator: Operator, contract: dict[str, Any], receipt: dict[str, Any] ) -> dict[str, Any]: validate_projection_receipt(receipt, contract) presence = resource_presence(operator, contract) counts = [ len(presence["temporary_identities_present"]), len(presence["exact_token_paths_present"]), len(presence["projection_resources_present"]), ] state = "active" if counts == [2, 2, 3] else "absent" if counts == [0, 0, 0] else "partial" uid_match = False present_uids = resource_uids(operator, contract, require_all=False) uid_match = bool(present_uids) and all( receipt["resources"]["uids"].get(key) == value for key, value in present_uids.items() ) if present_uids and not uid_match: state = "mismatched" return { "interface": "railiance.custody-projection-status", "version": 1, "engagement_id": contract["engagement_id"], "lease_id": receipt["lease_id"], "state": state, "resource_uids_match": uid_match, **presence, "target_ready": target_ready(operator, contract), "secret_values_observed": False, } def cleanup( operator: Operator, contract: dict[str, Any], projection: dict[str, Any] ) -> dict[str, Any]: validate_projection_receipt(projection, contract) verify_receipt_resource_scope(operator, contract, projection) result = cleanup_scope(operator, contract) receipt = { "interface": CLEANUP_INTERFACE, "version": 1, "workplan_id": WORKPLAN_ID, "state": "cleaned", "lease_id": projection["lease_id"], "engagement_id": contract["engagement_id"], "projection_receipt_id": projection["receipt_id"], "cleaned_at": rfc3339(utc_now()), "removed_resources": result["removed_resources"], "target_ready": True, "secret_values_observed": False, } validate_cleanup_receipt(receipt, projection, contract) return receipt def assert_confirm(value: str | None, expected: str) -> None: if value != expected: raise ProcedureError(f"live command requires --confirm {expected}") def assert_expired_cleanup(contract: dict[str, Any], now: datetime) -> None: expires = parse_time(contract["window"]["expires_at"], "window.expires_at") if now.astimezone(UTC) <= expires: raise ProcedureError("cleanup-expired refuses before receipt expiry") def main() -> int: parser = argparse.ArgumentParser() parser.add_argument( "command", choices=["validate", "render", "preflight", "project", "status", "cleanup", "cleanup-expired"], ) parser.add_argument("--contract", type=Path, required=True) parser.add_argument("--receipt", type=Path) parser.add_argument("--receipt-out", type=Path) parser.add_argument("--token-file", type=Path, default=Path.home() / ".local/openbao/platform-admin.token") parser.add_argument("--state-hub", default=DEFAULT_API_BASE) parser.add_argument("--confirm") args = parser.parse_args() try: contract = validate_projection_contract(load_json(args.contract)) if args.command == "validate": result = { "valid": True, "projection_contract_digest": contract_digest(contract), "resource_names": resource_names(contract), "secret_values_observed": False, } elif args.command == "render": result = { "policy": build_policy(contract), "manifest": json.loads(build_manifest(contract, "custody:" + "0" * 32)), "secret_values_observed": False, } else: projection = None if args.command in {"status", "cleanup", "cleanup-expired"}: if not args.receipt: raise ProcedureError(f"{args.command} requires --receipt") projection = validate_projection_receipt(load_json(args.receipt), contract) if args.command == "cleanup-expired": assert_expired_cleanup(contract, utc_now()) if args.command == "project": # Reject an unready consumer before reading platform-admin # custody material. project() repeats this immutable gate. current_broker_receipt(contract, args.state_hub) operator = Operator(contract, args.token_file) if args.command == "preflight": result = live_preflight(operator, contract, api_base=args.state_hub) elif args.command == "project": assert_confirm(args.confirm, f"{contract['engagement_id']}:attended") result = project(operator, contract, api_base=args.state_hub) else: assert projection is not None if args.command == "status": result = projection_status(operator, contract, projection) else: if args.command == "cleanup-expired": assert_confirm(args.confirm, f"{contract['engagement_id']}:expired") else: assert_confirm(args.confirm, f"{contract['engagement_id']}:cleanup") result = cleanup(operator, contract, projection) if args.receipt_out: write_receipt(args.receipt_out, result) print(json.dumps(result, indent=2, sort_keys=True)) return 0 except (ContractError, OSError, ProcedureError, RemoteExecutionError) as exc: print(f"custody projection failed: {exc}", file=sys.stderr) return 1 if __name__ == "__main__": raise SystemExit(main())