Implement reproducible S1 handoff contracts
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02994-7685-7940-bf34-3555b8256018
This commit is contained in:
parent
c8cb1c8edf
commit
b93af8cc78
44 changed files with 2035 additions and 342 deletions
163
scripts/s1_handoff.py
Normal file
163
scripts/s1_handoff.py
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Run the fail-closed S1 verification gate and emit a metadata-only receipt."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from baseline_contract import load_spec, profile_hostvars, validate_repo_consumers
|
||||
from inventory_contract import load_inventory
|
||||
from s1_receipt import validate_receipt
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def _git(*args: str) -> str:
|
||||
return subprocess.check_output(["git", *args], cwd=ROOT, text=True).strip()
|
||||
|
||||
|
||||
def _timestamp(value: datetime) -> str:
|
||||
return value.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def _selected_hosts(inventory: dict[str, Any], requested: list[str]) -> list[dict[str, Any]]:
|
||||
hosts = inventory["servers"]
|
||||
if not requested:
|
||||
return hosts
|
||||
known = {host["name"]: host for host in hosts}
|
||||
missing = sorted(set(requested) - set(known))
|
||||
if missing:
|
||||
raise ValueError(f"unknown hosts: {', '.join(missing)}")
|
||||
return [known[name] for name in requested]
|
||||
|
||||
|
||||
def build_receipt(
|
||||
*,
|
||||
revision: str,
|
||||
inventory_digest: str,
|
||||
hosts: list[dict[str, Any]],
|
||||
results: dict[str, int] | None,
|
||||
observed_at: datetime,
|
||||
freshness_hours: int,
|
||||
evidence: list[dict[str, str]],
|
||||
) -> dict[str, Any]:
|
||||
ran = results is not None
|
||||
passed = ran and all(results[host["name"]] == 0 for host in hosts)
|
||||
receipt: dict[str, Any] = {
|
||||
"schema_version": "1.0",
|
||||
"receipt_id": str(uuid.uuid4()),
|
||||
"event_type": "verification",
|
||||
"synthetic": False,
|
||||
"created_at": _timestamp(datetime.now(timezone.utc)),
|
||||
"source_revision": revision,
|
||||
"inventory_sha256": inventory_digest,
|
||||
"status": "pass" if passed else ("fail" if ran else "not-run"),
|
||||
"hosts": [host["name"] for host in hosts],
|
||||
"profiles": {host["name"]: host["baseline_profile"] for host in hosts},
|
||||
"observed_at": _timestamp(observed_at),
|
||||
"fresh_until": _timestamp(observed_at + timedelta(hours=freshness_hours)),
|
||||
"evidence": evidence,
|
||||
}
|
||||
if ran:
|
||||
receipt["host_exit_status"] = results
|
||||
validate_receipt(receipt)
|
||||
return receipt
|
||||
|
||||
|
||||
def _new_reports(started: float, host_names: list[str]) -> list[dict[str, str]]:
|
||||
evidence = []
|
||||
for path in sorted((ROOT / "reports").glob("goss-*.tap")):
|
||||
if path.stat().st_mtime < started:
|
||||
continue
|
||||
if not any(f"goss-{name}-" in path.name for name in host_names):
|
||||
continue
|
||||
evidence.append(
|
||||
{
|
||||
"kind": "goss-tap",
|
||||
"path": str(path.relative_to(ROOT)),
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
)
|
||||
return evidence
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--host", action="append", default=[])
|
||||
parser.add_argument("--freshness-hours", type=int, default=24)
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
parser.add_argument("--output", type=Path)
|
||||
args = parser.parse_args()
|
||||
if not 1 <= args.freshness_hours <= 168:
|
||||
parser.error("--freshness-hours must be between 1 and 168")
|
||||
|
||||
inventory_path = ROOT / "inventory" / "servers.yaml"
|
||||
spec_path = ROOT / "spec" / "server-baseline.yaml"
|
||||
inventory = load_inventory(inventory_path)
|
||||
baseline = load_spec(spec_path)
|
||||
validate_repo_consumers(ROOT)
|
||||
hosts = _selected_hosts(inventory, args.host)
|
||||
for host in hosts:
|
||||
profile_hostvars(baseline, host["baseline_profile"])
|
||||
|
||||
revision = _git("rev-parse", "HEAD")
|
||||
dirty = _git("status", "--porcelain")
|
||||
if dirty and not args.dry_run:
|
||||
print("handoff gate requires a clean checkout so the receipt pins all inputs", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
observed = datetime.now(timezone.utc)
|
||||
results: dict[str, int] | None = None
|
||||
evidence: list[dict[str, str]] = []
|
||||
if not args.dry_run:
|
||||
if shutil.which("ansible-playbook") is None:
|
||||
print("handoff gate requires ansible-playbook", file=sys.stderr)
|
||||
return 2
|
||||
started = datetime.now(timezone.utc).timestamp()
|
||||
results = {}
|
||||
for host in hosts:
|
||||
completed = subprocess.run(
|
||||
["ansible-playbook", "playbooks/verify.yaml", "--limit", host["name"]],
|
||||
cwd=ROOT / "ansible",
|
||||
check=False,
|
||||
)
|
||||
results[host["name"]] = completed.returncode
|
||||
evidence = _new_reports(started, [host["name"] for host in hosts])
|
||||
|
||||
receipt = build_receipt(
|
||||
revision=revision,
|
||||
inventory_digest=_sha256(inventory_path),
|
||||
hosts=hosts,
|
||||
results=results,
|
||||
observed_at=observed,
|
||||
freshness_hours=args.freshness_hours,
|
||||
evidence=evidence,
|
||||
)
|
||||
output = args.output
|
||||
if output is None:
|
||||
stamp = observed.strftime("%Y%m%dT%H%M%SZ")
|
||||
output = ROOT / "reports" / f"s1-handoff-{stamp}.json"
|
||||
elif not output.is_absolute():
|
||||
output = ROOT / output
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(json.dumps(receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
print(json.dumps({"receipt": str(output), "status": receipt["status"]}, sort_keys=True))
|
||||
return 0 if receipt["status"] in {"pass", "not-run"} else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue