diff --git a/Makefile b/Makefile index 8d9a4ed..c9b5987 100644 --- a/Makefile +++ b/Makefile @@ -505,5 +505,17 @@ wp0024-owner-verify: ## Run one owner's named read-only checks (set OWNER) wp0024-owner-status: ## Collect current hash-bound owner receipts from State Hub @python3 scripts/wp0024-owner-review.py status -.PHONY: audit-whitehat-e2-preflight audit-whitehat-e2-project audit-whitehat-e2-cleanup audit-database-lease-recovery-preflight audit-database-lease-approval-check audit-database-lease-recovery-exercise railiance01-reboot-preflight wp0024-owner-show wp0024-owner-verify wp0024-owner-status apps-pg-isolation-probe +wp0024-t02-driver-show: ## Show the exact T02 synthetic-load driver interface + @python3 scripts/wp0024-t02-driver-candidate.py show + +wp0024-t02-driver-status: ## Collect a revision-pinned audit-core driver candidate + @python3 scripts/wp0024-t02-driver-candidate.py status + +wp0024-t02-driver-register: ## Register committed audit-core driver source without executing it + @test -n "$(DRIVER)" || { echo 'set DRIVER to a path relative to the audit-core repo' >&2; exit 2; } + @test -n "$(REVIEWER)" || { echo 'set REVIEWER to the audit-core reviewer identifier' >&2; exit 2; } + @python3 scripts/wp0024-t02-driver-candidate.py register \ + --repo "$(abspath ../audit-core)" --driver "$(DRIVER)" --reviewer "$(REVIEWER)" + +.PHONY: audit-whitehat-e2-preflight audit-whitehat-e2-project audit-whitehat-e2-cleanup audit-database-lease-recovery-preflight audit-database-lease-approval-check audit-database-lease-recovery-exercise railiance01-reboot-preflight wp0024-owner-show wp0024-owner-verify wp0024-owner-status wp0024-t02-driver-show wp0024-t02-driver-status wp0024-t02-driver-register apps-pg-isolation-probe .PHONY: db-deploy db-status db-shell db-logs forgejo-db-deploy forgejo-db-status forgejo-db-shell apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-overlay-apply openbao-verify-login-overlay openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-configure-ssh openbao-verify-ssh openbao-verify-authenticated openbao-configure-external-secrets-issue-core openbao-configure-external-secrets-activity-core openbao-configure-external-secrets-forgejo openbao-validate-restore-evidence openbao-validate-emergency-evidence credential-grants-validate credential-change-applier-dry-run credential-change-applier-apply-plan credential-change-applier-apply credential-change-runbook credential-change-record-evidence credential-change-lifecycle-plan credential-change-lifecycle-event credential-change-import-inventory agent-high-risk-boundary-check openbao-credential-change-appliers-dry-run openbao-configure-credential-change-appliers openbao-token-grants-dry-run openbao-configure-token-grants openbao-verify-token-grants-dry-run openbao-verify-token-grants openbao-verify-token-grants-smoke credential-helper-dry-run credential-tests credential-exec-ops-warden-smoke argocd-bootstrap-dry-run argocd-bootstrap-deploy argocd-repo-apply argocd-status backup forgejo-backup forgejo-backup-dry-run forgejo-backup-status forgejo-package-prune forgejo-package-prune-dry-run reef-deployables apps-pg-evidence consumption-preflight help diff --git a/docs/wp0024-owner-review-interface.md b/docs/wp0024-owner-review-interface.md index c4e4652..def5ba1 100644 --- a/docs/wp0024-owner-review-interface.md +++ b/docs/wp0024-owner-review-interface.md @@ -63,3 +63,28 @@ are counted as stale and cannot satisfy an aggregate. The interface uses `STATE_HUB_URL` when set and otherwise connects to `http://127.0.0.1:8000`. The same value can be supplied explicitly with the top-level `--api-base` option before the subcommand. + +## T02 synthetic-load candidate + +After procedure receipts are complete, audit-core can implement directly +against `interfaces/RAILIANCE-WP-0024-T02-synthetic-load-driver.json`. The +contract fixes all four phases, exact JSON keys, accepted statuses, retry +semantics, value-safety requirements, and custody boundary. + +The owner registers committed source without executing it: + +```bash +python3 scripts/wp0024-t02-driver-candidate.py register \ + --repo ../audit-core \ + --driver scripts/ \ + --reviewer audit-core +``` + +Registration refuses untracked, modified, non-executable, or out-of-repository +source. Its State Hub receipt contains the full source commit and SHA-256 driver +revision. It neither runs synthetic traffic nor authorizes a lease revocation. +Coordination collects the candidate with: + +```bash +python3 scripts/wp0024-t02-driver-candidate.py status +``` diff --git a/interfaces/RAILIANCE-WP-0024-T02-synthetic-load-driver.json b/interfaces/RAILIANCE-WP-0024-T02-synthetic-load-driver.json new file mode 100644 index 0000000..3012dbd --- /dev/null +++ b/interfaces/RAILIANCE-WP-0024-T02-synthetic-load-driver.json @@ -0,0 +1,97 @@ +{ + "interface": "railiance.synthetic-load-driver", + "version": 1, + "workplan_id": "RAILIANCE-WP-0024", + "task_id": "RAILIANCE-WP-0024-T02", + "contract_id": "RAILIANCE-WP-0024-T02-SYNTHETIC-01", + "owner": "audit-core", + "invocation": [ + "", + "", + "--contract-id", + "RAILIANCE-WP-0024-T02-SYNTHETIC-01" + ], + "stdout": "Exactly one JSON object containing exactly the keys declared for the phase.", + "stderr": "Must not contain a bearer, payload, response body, database credential, Secret data or OpenBao output.", + "phases": { + "baseline": { + "keys": [ + "contract_id", + "fixture_id", + "status", + "secret_values_observed" + ], + "status": [ + "ready" + ] + }, + "expect-unavailable": { + "keys": [ + "contract_id", + "fixture_id", + "status", + "http_status", + "attempts", + "secret_values_observed" + ], + "status": [ + "retryable_unavailable" + ], + "http_status": [ + 503 + ], + "minimum_attempts": 1 + }, + "expect-recovered": { + "keys": [ + "contract_id", + "fixture_id", + "status", + "http_status", + "attempts", + "secret_values_observed" + ], + "status": [ + "accepted", + "duplicate" + ], + "http_status": [ + 200, + 202 + ], + "minimum_attempts": 1 + }, + "cleanup": { + "keys": [ + "contract_id", + "fixture_id", + "status", + "secret_values_observed" + ], + "status": [ + "clean" + ] + } + }, + "invariants": [ + "contract_id equals the requested contract on every phase", + "fixture_id is identical across every phase", + "secret_values_observed is false on every phase", + "only a separately approved synthetic sender and tenant are used", + "the bearer is read only from an approved mounted file and never reaches the parent shell", + "expect-unavailable treats HTTP 503 as retryable evidence and never retries 400, 401, 403 or 409", + "cleanup stops and removes driver-owned ephemeral runtime; an accepted immutable audit fixture is retained under the audit evidence contract", + "temporary sender identity cleanup remains an attended railiance-platform custody action" + ], + "candidate_receipt": { + "source_repo": "audit-core", + "required_fields": [ + "source_revision", + "source_path", + "driver_revision", + "reviewer", + "created_at" + ], + "registration_executes_driver": false + } +} diff --git a/scripts/wp0024-t02-driver-candidate.py b/scripts/wp0024-t02-driver-candidate.py new file mode 100644 index 0000000..d0b12f2 --- /dev/null +++ b/scripts/wp0024-t02-driver-candidate.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +"""Register or inspect a revision-pinned T02 synthetic-load driver candidate. + +Registration reads and hashes committed source. It never executes the driver, +reads a bearer, or authorizes the live database-lease exercise. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import subprocess +import sys +import urllib.error +import urllib.parse +import urllib.request +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_CONTRACT = ROOT / "interfaces" / "RAILIANCE-WP-0024-T02-synthetic-load-driver.json" +DEFAULT_AUDIT_REPO = ROOT.parent / "audit-core" +DEFAULT_API_BASE = os.environ.get("STATE_HUB_URL", "http://127.0.0.1:8000") +PREFIX = "WP0024-T02-DRIVER-CANDIDATE" + + +class CandidateError(RuntimeError): + pass + + +def canonical(value: Any) -> str: + return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + +def digest(value: bytes) -> str: + return hashlib.sha256(value).hexdigest() + + +def load_contract(path: Path = DEFAULT_CONTRACT) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise CandidateError("synthetic-load contract is unavailable or invalid") from exc + if ( + not isinstance(value, dict) + or value.get("interface") != "railiance.synthetic-load-driver" + or value.get("version") != 1 + or value.get("task_id") != "RAILIANCE-WP-0024-T02" + or value.get("owner") != "audit-core" + ): + raise CandidateError("unsupported synthetic-load contract") + phases = value.get("phases") + if not isinstance(phases, dict) or set(phases) != { + "baseline", "expect-unavailable", "expect-recovered", "cleanup" + }: + raise CandidateError("synthetic-load contract has the wrong phase set") + for phase, record in phases.items(): + if not isinstance(record, dict) or not record.get("keys") or not record.get("status"): + raise CandidateError(f"synthetic-load phase is incomplete: {phase}") + return value + + +def contract_digest(contract: dict[str, Any]) -> str: + return digest(canonical(contract).encode("utf-8")) + + +def run_git(repo: Path, args: list[str]) -> str: + completed = subprocess.run( + ["git", "-C", str(repo), *args], text=True, capture_output=True, check=False + ) + if completed.returncode != 0: + raise CandidateError("cannot resolve committed driver source") + return completed.stdout.strip() + + +def inspect_driver(repo: Path, source_path: str) -> dict[str, str]: + repo = repo.resolve() + target = (repo / source_path).resolve() + try: + relative = target.relative_to(repo).as_posix() + except ValueError as exc: + raise CandidateError("driver path escapes the audit-core repository") from exc + if not target.is_file() or not os.access(target, os.X_OK): + raise CandidateError("driver candidate must be an executable file") + if run_git(repo, ["status", "--porcelain", "--", relative]): + raise CandidateError("driver candidate must be committed with no path-local changes") + tracked = run_git(repo, ["ls-files", "--error-unmatch", "--", relative]) + if tracked != relative: + raise CandidateError("driver candidate is not tracked at the requested path") + revision = run_git(repo, ["log", "-1", "--format=%H", "--", relative]) + if not re.fullmatch(r"[0-9a-f]{40}", revision): + raise CandidateError("driver source revision is not a full Git commit") + return { + "source_repo": "audit-core", + "source_revision": revision, + "source_path": relative, + "driver_revision": "sha256:" + digest(target.read_bytes()), + } + + +def reviewer(value: str) -> str: + if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:@/+\-]{1,127}", value): + raise CandidateError("reviewer must be a stable 2-128 character identifier") + return value + + +def build_receipt( + contract: dict[str, Any], source: dict[str, str], reviewer_id: str, + *, now: datetime | None = None, +) -> dict[str, Any]: + return { + "interface": "railiance.synthetic-load-driver-candidate", + "version": 1, + "workplan_id": contract["workplan_id"], + "task_id": contract["task_id"], + "contract_id": contract["contract_id"], + "contract_digest": contract_digest(contract), + "owner": "audit-core", + "reviewer": reviewer(reviewer_id), + **source, + "created_at": (now or datetime.now(UTC)).astimezone(UTC).isoformat().replace("+00:00", "Z"), + "registration_executed_driver": False, + "secret_values_observed": False, + } + + +def subject(receipt: dict[str, Any]) -> str: + return "/".join((PREFIX, "v1", receipt["contract_digest"], receipt["driver_revision"][7:])) + + +def http_json(method: str, url: str, payload: dict[str, Any] | None = None) -> Any: + data = canonical(payload).encode("utf-8") if payload is not None else None + request = urllib.request.Request( + url, data=data, method=method, + headers={"Content-Type": "application/json"} if data else {}, + ) + try: + with urllib.request.urlopen(request, timeout=10) as response: + return json.loads(response.read().decode("utf-8")) + except (OSError, urllib.error.URLError, json.JSONDecodeError) as exc: + raise CandidateError(f"State Hub request failed: {method} {url}") from exc + + +def post(receipt: dict[str, Any], api_base: str) -> dict[str, Any]: + value = http_json( + "POST", + f"{api_base.rstrip('/')}/messages/", + { + "from_agent": "audit-core", + "to_agent": "railiance-platform", + "subject": subject(receipt), + "body": canonical(receipt), + }, + ) + if not isinstance(value, dict): + raise CandidateError("State Hub returned an invalid candidate response") + return value + + +def parse_message(message: dict[str, Any], contract: dict[str, Any]) -> dict[str, Any] | None: + if message.get("from_agent") != "audit-core": + return None + try: + receipt = json.loads(message["body"]) + except (KeyError, TypeError, json.JSONDecodeError): + return None + if not isinstance(receipt, dict): + return None + try: + valid = ( + message.get("subject") == subject(receipt) + and receipt.get("interface") == "railiance.synthetic-load-driver-candidate" + and receipt.get("version") == 1 + and receipt.get("task_id") == contract["task_id"] + and receipt.get("contract_id") == contract["contract_id"] + and receipt.get("contract_digest") == contract_digest(contract) + and receipt.get("owner") == "audit-core" + and receipt.get("source_repo") == "audit-core" + and re.fullmatch(r"[0-9a-f]{40}", receipt.get("source_revision", "")) + and re.fullmatch(r"sha256:[0-9a-f]{64}", receipt.get("driver_revision", "")) + and receipt.get("registration_executed_driver") is False + and receipt.get("secret_values_observed") is False + ) + except (KeyError, TypeError): + return None + return receipt if valid else None + + +def status(contract: dict[str, Any], api_base: str) -> dict[str, Any]: + query = urllib.parse.urlencode({"to_agent": "railiance-platform", "limit": 500}) + value = http_json("GET", f"{api_base.rstrip('/')}/messages/?{query}") + if not isinstance(value, list): + raise CandidateError("State Hub messages response is not a list") + candidates = [item for message in value if (item := parse_message(message, contract))] + latest = max(candidates, key=lambda item: item.get("created_at", ""), default=None) + return { + "interface": contract["interface"], + "version": contract["version"], + "task_id": contract["task_id"], + "contract_id": contract["contract_id"], + "contract_digest": contract_digest(contract), + "candidate_registered": latest is not None, + "candidate": latest, + } + + +def parser() -> argparse.ArgumentParser: + result = argparse.ArgumentParser(description=__doc__) + result.add_argument("--contract", type=Path, default=DEFAULT_CONTRACT) + result.add_argument("--api-base", default=DEFAULT_API_BASE) + sub = result.add_subparsers(dest="command", required=True) + sub.add_parser("show") + register = sub.add_parser("register") + register.add_argument("--repo", type=Path, default=DEFAULT_AUDIT_REPO) + register.add_argument("--driver", required=True, help="path relative to the audit-core repo") + register.add_argument("--reviewer", required=True) + sub.add_parser("status") + return result + + +def main(argv: list[str] | None = None) -> int: + args = parser().parse_args(argv) + try: + contract = load_contract(args.contract) + if args.command == "show": + output = {**contract, "contract_digest": contract_digest(contract)} + elif args.command == "register": + source = inspect_driver(args.repo, args.driver) + receipt = build_receipt(contract, source, args.reviewer) + response = post(receipt, args.api_base) + output = {"submitted": True, "message_id": response.get("id"), "receipt": receipt} + else: + output = status(contract, args.api_base) + except CandidateError as exc: + print(canonical({"ok": False, "error": str(exc)}), file=sys.stderr) + return 2 + print(json.dumps(output, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_wp0024_t02_driver_candidate.py b/tests/test_wp0024_t02_driver_candidate.py new file mode 100644 index 0000000..ee9b69b --- /dev/null +++ b/tests/test_wp0024_t02_driver_candidate.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +import importlib.util +import json +import unittest +from datetime import UTC, datetime +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def load(name: str, path: Path): + spec = importlib.util.spec_from_file_location(name, path) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +candidate = load( + "wp0024_t02_driver_candidate", + ROOT / "scripts" / "wp0024-t02-driver-candidate.py", +) +harness = load( + "audit_core_database_lease_recovery_for_driver_contract", + ROOT / "scripts" / "audit-core-database-lease-recovery.py", +) + + +class DriverCandidateTests(unittest.TestCase): + def setUp(self) -> None: + self.contract = candidate.load_contract() + + def test_contract_exactly_matches_harness_evidence_keys(self) -> None: + phase_keys = { + phase: set(record["keys"]) + for phase, record in self.contract["phases"].items() + } + self.assertEqual(harness.EXPECTED_DRIVER_KEYS, phase_keys) + self.assertEqual( + "RAILIANCE-WP-0024-T02-SYNTHETIC-01", + self.contract["contract_id"], + ) + + def test_candidate_receipt_round_trip(self) -> None: + source = { + "source_repo": "audit-core", + "source_revision": "a" * 40, + "source_path": "scripts/lease-load.py", + "driver_revision": "sha256:" + "b" * 64, + } + receipt = candidate.build_receipt( + self.contract, + source, + "audit-core", + now=datetime(2026, 8, 22, 14, 0, tzinfo=UTC), + ) + message = { + "from_agent": "audit-core", + "subject": candidate.subject(receipt), + "body": candidate.canonical(receipt), + } + self.assertEqual(receipt, candidate.parse_message(message, self.contract)) + self.assertFalse(receipt["registration_executed_driver"]) + self.assertFalse(receipt["secret_values_observed"]) + + def test_wrong_contract_digest_is_rejected(self) -> None: + receipt = candidate.build_receipt( + self.contract, + { + "source_repo": "audit-core", + "source_revision": "a" * 40, + "source_path": "scripts/lease-load.py", + "driver_revision": "sha256:" + "b" * 64, + }, + "audit-core", + ) + receipt["contract_digest"] = "0" * 64 + message = { + "from_agent": "audit-core", + "subject": candidate.subject(receipt), + "body": json.dumps(receipt), + } + self.assertIsNone(candidate.parse_message(message, self.contract)) + + +if __name__ == "__main__": + unittest.main() diff --git a/workplans/RAILIANCE-WP-0024-audit-core-custody-and-recovery-coordination.md b/workplans/RAILIANCE-WP-0024-audit-core-custody-and-recovery-coordination.md index 62e838f..fb33f38 100644 --- a/workplans/RAILIANCE-WP-0024-audit-core-custody-and-recovery-coordination.md +++ b/workplans/RAILIANCE-WP-0024-audit-core-custody-and-recovery-coordination.md @@ -130,6 +130,15 @@ The procedure's responsibility table now uses the same metadata-only selection rule; it no longer claims lease metadata exposes or matches a mounted database username. +**Owner receipts complete (2026-08-22):** the canonical direct-review contract +`f86d418f951f829f075de04dd825c6e2e185e577019ee23d6da1fa9040302d62` +is approved by audit-core (`0c0e3dff-c7fd-4fac-898b-1aec160f4753`), +rapp-postgres (`e958f75a-4e41-4103-9261-a4393eb6f1ef`), and +railiance-platform (`52ab4c51-0950-45e8-a592-564dc2a450bb`). T02 no longer +waits on procedure review. Its remaining gates are a revision-pinned approved +synthetic-load driver, a named abort operator, and a non-overlapping ≤15-minute +live window after the Whitehat engagement cleanup. + ## T03 — Define the coordinated railiance01 reboot exercise ```task @@ -180,6 +189,16 @@ did not measure the complete post-unseal ESO, audit-chain, and accepted-event recovery path required here. T03 therefore still needs one deliberate run, but does not need to rediscover the expected boot order. +**Owner receipts complete (2026-08-22):** the same canonical contract is +approved by audit-core (`0c0e3dff-c7fd-4fac-898b-1aec160f4753`), +rapp-postgres (`e958f75a-4e41-4103-9261-a4393eb6f1ef`), +railiance-cluster (`f5919864-b7f1-40b5-b07e-d19055dffca8`), +railiance-infra (`9bbf986c-7385-42de-964f-9040eebdfee4`), and +railiance-platform (`52ab4c51-0950-45e8-a592-564dc2a450bb`). T03 no longer +waits on procedure review. Its remaining gates are the approved outage window, +fresh encrypted off-host OpenBao snapshot receipt, provider-console access, +2-of-3 Shamir quorum, and named abort operator. + ## T04 — Route the contracts and approval gates ```task