Add direct WP-0024 owner review interface
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02669-87ee-7a31-b111-edc95a16e0fa
This commit is contained in:
codex 2026-08-22 14:15:02 +02:00
parent 3f9e4535d1
commit 0d9cebedea
6 changed files with 877 additions and 1 deletions

View file

@ -494,5 +494,16 @@ audit-database-lease-recovery-exercise: ## Run the separately approved attended
railiance01-reboot-preflight: ## Read-only gate for the coordinated single-node reboot
python3 scripts/audit-core-recovery-preflight.py node-reboot $(RAILIANCE01_REBOOT_PREFLIGHT_ARGS)
.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 apps-pg-isolation-probe
wp0024-owner-show: ## Show one owner's hash-bound review contract (set OWNER)
@test -n "$(OWNER)" || { echo 'set OWNER=audit-core|rapp-postgres|railiance-cluster|railiance-infra' >&2; exit 2; }
@python3 scripts/wp0024-owner-review.py show --owner "$(OWNER)"
wp0024-owner-verify: ## Run one owner's named read-only checks (set OWNER)
@test -n "$(OWNER)" || { echo 'set OWNER=audit-core|rapp-postgres|railiance-cluster|railiance-infra' >&2; exit 2; }
@python3 scripts/wp0024-owner-review.py verify --owner "$(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
.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

View file

@ -0,0 +1,55 @@
# WP-0024 direct owner-review interface
This interface lets each procedure owner inspect its exact review assertions,
run the applicable read-only checks, and submit an approval or change request
directly. No coding agent has to copy a to-do or translate a prose reply.
The canonical contract is
`interfaces/RAILIANCE-WP-0024-owner-reviews.json`. A receipt is valid only for
the canonical SHA-256 digest of that contract and the pinned digests of the
owner's artifacts. Any procedure change therefore makes prior approval stale.
## Owner flow
Run these commands from a current `railiance-platform` checkout, replacing the
owner and reviewer with your stable agent or operator identifier:
```bash
python3 scripts/wp0024-owner-review.py show --owner audit-core
python3 scripts/wp0024-owner-review.py verify --owner audit-core
python3 scripts/wp0024-owner-review.py approve --owner audit-core --reviewer audit-core
```
Valid owners are `audit-core`, `rapp-postgres`, `railiance-cluster`, and
`railiance-infra`. `approve` repeats verification and submits the receipt only
when artifact hashes and all named read-only checks pass. The checks may read
resource metadata, but they never read Secret data or OpenBao lease payloads.
They cannot revoke a lease, restart a workload, create a snapshot, or reboot a
node. A successful review is still not a live execution window.
To request changes instead:
```bash
python3 scripts/wp0024-owner-review.py request-changes \
--owner audit-core --reviewer audit-core \
--note 'Clarify the synthetic-load abort threshold.'
```
The note is durable State Hub metadata. Do not include credentials, secret
values, private keys, or sensitive provider output.
## Coordination status
Anyone can collect current receipts without interpreting owner messages:
```bash
python3 scripts/wp0024-owner-review.py status
```
The result reports `approve`, `request-changes`, or `missing` per owner and an
`all_approved` aggregate. Receipts for an older contract digest or different
artifact hashes are counted as stale and cannot satisfy the 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.

View file

@ -0,0 +1,102 @@
{
"interface": "railiance.owner-review",
"version": 1,
"workplan_id": "RAILIANCE-WP-0024",
"workplan_uuid": "88c4ef7f-0af8-580e-90dc-a2bae2675a4d",
"base_revision": "3f9e453",
"receipt_destination": "railiance-platform",
"decisions": [
"approve",
"request-changes"
],
"owners": {
"audit-core": {
"tasks": [
"RAILIANCE-WP-0024-T02",
"RAILIANCE-WP-0024-T03"
],
"artifacts": [
"docs/audit-core-database-lease-recovery.md",
"docs/railiance01-coordinated-reboot.md",
"scripts/audit-core-recovery-preflight.py",
"scripts/audit-core-database-lease-recovery.py"
],
"assertions": [
"The database-lease exercise uses only synthetic load and proves health 200, readiness and requests fail retryably with 503, then recover without changing the audit-core pod UID or restart count.",
"The approved load driver implements the exact start, probe, stop and summary contract and cannot expose credential values.",
"The reboot exercise proves audit-core health, readiness, database integrity and the approved synthetic E2 path after dependencies recover."
],
"checks": [
"focused-unit-tests",
"database-lease-preflight",
"node-reboot-preflight"
]
},
"rapp-postgres": {
"tasks": [
"RAILIANCE-WP-0024-T02"
],
"artifacts": [
"docs/audit-core-database-lease-recovery.md",
"scripts/audit-core-recovery-preflight.py",
"scripts/audit-core-database-lease-recovery.py",
"docs/audit-core-database-lease-approval.example.json"
],
"assertions": [
"Lease selection aborts unless exactly one live handle exists below database/creds/audit-core-runtime and its issue time coheres with the ExternalSecret refresh.",
"The action revokes only that exact lease handle; it never revokes the role prefix, an External Secrets parent lease, or another database consumer.",
"Recovery uses ordinary ExternalSecret refresh and mounted-file reread, with no pod restart, database restore or credential value in evidence."
],
"checks": [
"focused-unit-tests",
"database-lease-preflight"
]
},
"railiance-cluster": {
"tasks": [
"RAILIANCE-WP-0024-T03"
],
"artifacts": [
"docs/railiance01-coordinated-reboot.md",
"scripts/audit-core-recovery-preflight.py",
"docs/openbao-reboot-snapshot-receipt.example.json"
],
"assertions": [
"Recovery is ordered host, node/k3s, CoreDNS and operators before application readiness; application lag is not treated as host failure.",
"The exercise does not reinstall k3s, replace PVCs or alter firewall policy as a recovery shortcut.",
"External Secrets stale-provider recovery is bounded to the documented controller reconciliation path and preserves value-safe evidence."
],
"checks": [
"focused-unit-tests",
"node-reboot-preflight"
]
},
"railiance-infra": {
"tasks": [
"RAILIANCE-WP-0024-T03"
],
"artifacts": [
"docs/railiance01-coordinated-reboot.md",
"scripts/audit-core-recovery-preflight.py",
"docs/openbao-reboot-snapshot-receipt.example.json"
],
"assertions": [
"An attended operator has independently verified provider-console access before the window and performs an ordinary reboot only.",
"Host-return, access-recovery and abort/escalation ownership are explicit; no ad hoc firewall, operating-system or k3s reinstall is authorized.",
"The reboot is gated on a current verified encrypted off-host OpenBao snapshot receipt and named recovery owners."
],
"checks": [
"focused-unit-tests",
"node-reboot-preflight"
]
}
},
"artifact_sha256": {
"docs/audit-core-database-lease-approval.example.json": "360d49d0bea7ca376405974403036bff080533728f133d237f30f4ee3bb943e0",
"docs/audit-core-database-lease-recovery.md": "921b701efa14b1d59434d175f9f2df57e4e6f462ab4fbdf06d6d17c15a02fb0a",
"docs/openbao-reboot-snapshot-receipt.example.json": "b95903a3e1aacb607eecc3f0440f08e62480d09da47bd7ab96c3eb8814f2eaee",
"docs/railiance01-coordinated-reboot.md": "9969c2f498587710154e8d9a650fb8a13159e9e7b99918291dbdf577b131b7e7",
"scripts/audit-core-database-lease-recovery.py": "c87fb026c527fc17116067c5742d324340707ce4aecf440d0d4e618db9b872a1",
"scripts/audit-core-recovery-preflight.py": "802f73fa4352f64bc0972d813e3dd874f3ec2f428e162b23158c9d8d6fd8e710"
}
}

View file

@ -0,0 +1,523 @@
#!/usr/bin/env python3
"""Direct, value-safe owner review interface for RAILIANCE-WP-0024.
The executable surface is intentionally closed: contract files select named
read-only checks, never shell commands. Approvals are bound to the canonical
contract digest and the SHA-256 digests of the owner's reviewed artifacts.
"""
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, Callable
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_CONTRACT = ROOT / "interfaces" / "RAILIANCE-WP-0024-owner-reviews.json"
DEFAULT_API_BASE = os.environ.get("STATE_HUB_URL", "http://127.0.0.1:8000")
RECEIPT_PREFIX = "WP0024-OWNER-RECEIPT"
KNOWN_CHECKS = {
"focused-unit-tests",
"database-lease-preflight",
"node-reboot-preflight",
}
class ReviewError(RuntimeError):
pass
def canonical_json(value: Any) -> str:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
def sha256_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.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 ReviewError(f"review contract is unavailable or invalid: {path}") from exc
if not isinstance(value, dict):
raise ReviewError("review contract must be a JSON object")
if value.get("interface") != "railiance.owner-review" or value.get("version") != 1:
raise ReviewError("unsupported owner-review interface/version")
owners = value.get("owners")
hashes = value.get("artifact_sha256")
if not isinstance(owners, dict) or not owners or not isinstance(hashes, dict):
raise ReviewError("review contract requires owners and artifact_sha256 maps")
for owner, review in owners.items():
if not isinstance(owner, str) or not isinstance(review, dict):
raise ReviewError("invalid owner review entry")
artifacts = review.get("artifacts")
checks = review.get("checks")
assertions = review.get("assertions")
if not all(isinstance(item, str) for item in artifacts or []):
raise ReviewError(f"{owner}: artifacts must be strings")
if not artifacts or any(item not in hashes for item in artifacts):
raise ReviewError(f"{owner}: every artifact requires a pinned digest")
if not checks or any(item not in KNOWN_CHECKS for item in checks):
raise ReviewError(f"{owner}: unknown or missing read-only check")
if not assertions or not all(isinstance(item, str) for item in assertions):
raise ReviewError(f"{owner}: assertions must be non-empty strings")
return value
def contract_digest(contract: dict[str, Any]) -> str:
return sha256_bytes(canonical_json(contract).encode("utf-8"))
def owner_review(contract: dict[str, Any], owner: str) -> dict[str, Any]:
review = contract["owners"].get(owner)
if not isinstance(review, dict):
choices = ", ".join(sorted(contract["owners"]))
raise ReviewError(f"unknown owner {owner!r}; choose one of: {choices}")
return review
def artifact_state(
contract: dict[str, Any], owner: str, *, root: Path = ROOT
) -> dict[str, dict[str, Any]]:
review = owner_review(contract, owner)
result: dict[str, dict[str, Any]] = {}
for relative in review["artifacts"]:
expected = contract["artifact_sha256"][relative]
target = (root / relative).resolve()
try:
target.relative_to(root.resolve())
except ValueError as exc:
raise ReviewError(f"artifact escapes repository root: {relative}") from exc
try:
observed = file_sha256(target)
except OSError:
observed = None
result[relative] = {
"expected_sha256": expected,
"observed_sha256": observed,
"matches": observed == expected,
}
return result
def check_command(check_id: str) -> list[str]:
if check_id == "focused-unit-tests":
return [
sys.executable,
"-m",
"unittest",
"tests/test_audit_core_recovery_preflight.py",
"tests/test_audit_core_database_lease_recovery.py",
"tests/test_wp0024_owner_review.py",
]
if check_id == "database-lease-preflight":
return [sys.executable, "scripts/audit-core-recovery-preflight.py", "database-lease"]
if check_id == "node-reboot-preflight":
return [sys.executable, "scripts/audit-core-recovery-preflight.py", "node-reboot"]
raise ReviewError(f"unknown check id: {check_id}")
def run_named_check(
check_id: str,
*,
runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
) -> dict[str, Any]:
completed = runner(
check_command(check_id),
cwd=ROOT,
text=True,
capture_output=True,
check=False,
)
result: dict[str, Any] = {
"id": check_id,
"passed": completed.returncode == 0,
"exit_code": completed.returncode,
"read_only": True,
}
if check_id.endswith("-preflight") and completed.returncode == 0:
try:
payload = json.loads(completed.stdout)
except json.JSONDecodeError:
result["passed"] = False
result["detail"] = "preflight returned invalid JSON"
else:
safe = (
isinstance(payload, dict)
and payload.get("automated_checks_passed") is True
and payload.get("secret_values_observed") is False
)
result["passed"] = bool(safe)
result["automated_checks_passed"] = payload.get("automated_checks_passed")
result["ready_for_live_execution"] = payload.get("ready_for_live_execution")
result["secret_values_observed"] = payload.get("secret_values_observed")
if not safe:
result["detail"] = "preflight safety/automated gate did not pass"
elif completed.returncode != 0:
result["detail"] = "check failed; run the named interface verify command locally for diagnosis"
return result
def verify(
contract: dict[str, Any],
owner: str,
*,
root: Path = ROOT,
check_runner: Callable[[str], dict[str, Any]] = run_named_check,
) -> dict[str, Any]:
artifacts = artifact_state(contract, owner, root=root)
checks: list[dict[str, Any]] = []
if all(item["matches"] for item in artifacts.values()):
checks = [check_runner(item) for item in owner_review(contract, owner)["checks"]]
return {
"interface": contract["interface"],
"version": contract["version"],
"workplan_id": contract["workplan_id"],
"owner": owner,
"contract_digest": contract_digest(contract),
"artifacts": artifacts,
"checks": checks,
"passed": bool(checks)
and all(item["matches"] for item in artifacts.values())
and all(item["passed"] for item in checks),
"secret_values_observed": False,
}
def validate_reviewer(value: str) -> str:
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:@/+\-]{1,127}", value):
raise ReviewError("reviewer must be a stable 2-128 character identifier")
return value
def validate_note(value: str) -> str:
note = value.strip()
if not note or len(note) > 2000:
raise ReviewError("request-changes requires a note of 1-2000 characters")
forbidden = ("BEGIN PRIVATE KEY", "BEGIN OPENSSH PRIVATE KEY", "AGE-SECRET-KEY-1", "hvs.")
if any(marker in note for marker in forbidden):
raise ReviewError("note appears to contain credential material; do not submit it")
return note
def receipt_subject(receipt: dict[str, Any]) -> str:
return "/".join(
(
RECEIPT_PREFIX,
f"v{receipt['version']}",
receipt["owner"],
receipt["decision"],
receipt["contract_digest"],
)
)
def build_receipt(
contract: dict[str, Any],
owner: str,
decision: str,
reviewer: str,
*,
verification: dict[str, Any] | None = None,
note: str | None = None,
now: datetime | None = None,
) -> dict[str, Any]:
if decision not in contract["decisions"]:
raise ReviewError(f"unsupported decision: {decision}")
if decision == "approve" and (not verification or verification.get("passed") is not True):
raise ReviewError("approval requires a passing direct verification")
if decision == "request-changes":
note = validate_note(note or "")
review = owner_review(contract, owner)
timestamp = (now or datetime.now(UTC)).astimezone(UTC).isoformat().replace("+00:00", "Z")
receipt: dict[str, Any] = {
"interface": contract["interface"],
"version": contract["version"],
"workplan_id": contract["workplan_id"],
"workplan_uuid": contract["workplan_uuid"],
"owner": owner,
"reviewer": validate_reviewer(reviewer),
"decision": decision,
"created_at": timestamp,
"contract_digest": contract_digest(contract),
"artifact_sha256": {
path: contract["artifact_sha256"][path] for path in review["artifacts"]
},
"assertion_count": len(review["assertions"]),
"secret_values_observed": False,
}
if verification:
receipt["checks"] = [
{"id": item["id"], "passed": item["passed"], "read_only": True}
for item in verification["checks"]
]
if note is not None:
receipt["note"] = note
return receipt
def http_json(
method: str,
url: str,
payload: dict[str, Any] | None = None,
*,
opener: Callable[..., Any] = urllib.request.urlopen,
) -> Any:
data = canonical_json(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 opener(request, timeout=10) as response:
return json.loads(response.read().decode("utf-8"))
except (OSError, urllib.error.URLError, json.JSONDecodeError) as exc:
raise ReviewError(f"State Hub request failed: {method} {url}") from exc
def post_receipt(receipt: dict[str, Any], api_base: str) -> dict[str, Any]:
payload = {
"from_agent": receipt["owner"],
"to_agent": "railiance-platform",
"subject": receipt_subject(receipt),
"body": canonical_json(receipt),
}
response = http_json("POST", f"{api_base.rstrip('/')}/messages/", payload)
if not isinstance(response, dict):
raise ReviewError("State Hub returned an invalid receipt response")
return response
def parse_receipt_message(message: dict[str, Any]) -> dict[str, Any] | None:
subject = message.get("subject")
body = message.get("body")
if not isinstance(subject, str) or not subject.startswith(RECEIPT_PREFIX + "/"):
return None
if not isinstance(body, str):
return None
try:
receipt = json.loads(body)
except json.JSONDecodeError:
return None
if not isinstance(receipt, dict):
return None
required = {
"interface": str,
"version": int,
"workplan_id": str,
"workplan_uuid": str,
"owner": str,
"reviewer": str,
"decision": str,
"created_at": str,
"contract_digest": str,
"artifact_sha256": dict,
"assertion_count": int,
"secret_values_observed": bool,
}
if any(not isinstance(receipt.get(key), kind) for key, kind in required.items()):
return None
if (
receipt["interface"] != "railiance.owner-review"
or receipt["version"] != 1
or receipt["workplan_id"] != "RAILIANCE-WP-0024"
or receipt["decision"] not in {"approve", "request-changes"}
or receipt["secret_values_observed"] is not False
or not re.fullmatch(r"[0-9a-f]{64}", receipt["contract_digest"])
):
return None
try:
datetime.fromisoformat(receipt["created_at"].replace("Z", "+00:00"))
expected_subject = receipt_subject(receipt)
except (KeyError, TypeError, ValueError):
return None
if expected_subject != subject:
return None
if message.get("from_agent") != receipt.get("owner"):
return None
return receipt
def aggregate_status(
contract: dict[str, Any], messages: list[dict[str, Any]]
) -> dict[str, Any]:
digest = contract_digest(contract)
candidates: dict[str, list[dict[str, Any]]] = {
owner: [] for owner in contract["owners"]
}
stale: dict[str, int] = {owner: 0 for owner in contract["owners"]}
for message in messages:
receipt = parse_receipt_message(message)
if not receipt or receipt.get("owner") not in candidates:
continue
owner = receipt["owner"]
if receipt.get("contract_digest") != digest:
stale[owner] += 1
continue
expected = {
path: contract["artifact_sha256"][path]
for path in contract["owners"][owner]["artifacts"]
}
review = contract["owners"][owner]
try:
validate_reviewer(receipt["reviewer"])
except ReviewError:
stale[owner] += 1
continue
if (
receipt.get("interface") != contract["interface"]
or receipt.get("version") != contract["version"]
or receipt.get("workplan_id") != contract["workplan_id"]
or receipt.get("workplan_uuid") != contract["workplan_uuid"]
or receipt.get("artifact_sha256") != expected
or receipt.get("assertion_count") != len(review["assertions"])
):
stale[owner] += 1
continue
if receipt["decision"] == "approve":
checks = receipt.get("checks")
if (
not isinstance(checks, list)
or [item.get("id") for item in checks if isinstance(item, dict)]
!= review["checks"]
or any(
not isinstance(item, dict)
or item.get("passed") is not True
or item.get("read_only") is not True
for item in checks
)
):
stale[owner] += 1
continue
candidates[owner].append(receipt)
owners: dict[str, Any] = {}
for owner, receipts in candidates.items():
latest = max(receipts, key=lambda item: item.get("created_at", ""), default=None)
owners[owner] = {
"decision": latest.get("decision") if latest else "missing",
"reviewer": latest.get("reviewer") if latest else None,
"created_at": latest.get("created_at") if latest else None,
"stale_receipt_count": stale[owner],
}
try:
artifact_current = all(
file_sha256(ROOT / path) == expected
for path, expected in contract["artifact_sha256"].items()
)
except OSError:
artifact_current = False
return {
"interface": contract["interface"],
"version": contract["version"],
"workplan_id": contract["workplan_id"],
"contract_digest": digest,
"contract_artifacts_current": artifact_current,
"owners": owners,
"all_approved": artifact_current
and all(item["decision"] == "approve" for item in owners.values()),
}
def fetch_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 ReviewError("State Hub messages response is not a list")
return aggregate_status(contract, value)
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)
subparsers = result.add_subparsers(dest="command", required=True)
for command in ("show", "verify"):
child = subparsers.add_parser(command)
child.add_argument("--owner", required=True)
approve = subparsers.add_parser("approve")
approve.add_argument("--owner", required=True)
approve.add_argument("--reviewer", required=True)
changes = subparsers.add_parser("request-changes")
changes.add_argument("--owner", required=True)
changes.add_argument("--reviewer", required=True)
changes.add_argument("--note", required=True)
subparsers.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":
review = owner_review(contract, args.owner)
output = {
"interface": contract["interface"],
"version": contract["version"],
"workplan_id": contract["workplan_id"],
"owner": args.owner,
"contract_digest": contract_digest(contract),
**review,
"artifacts_state": artifact_state(contract, args.owner),
"decisions": contract["decisions"],
}
elif args.command == "verify":
output = verify(contract, args.owner)
elif args.command == "approve":
verification = verify(contract, args.owner)
receipt = build_receipt(
contract, args.owner, "approve", args.reviewer, verification=verification
)
posted = post_receipt(receipt, args.api_base)
output = {
"submitted": True,
"message_id": posted.get("id"),
"receipt": receipt,
}
elif args.command == "request-changes":
receipt = build_receipt(
contract,
args.owner,
"request-changes",
args.reviewer,
note=args.note,
)
posted = post_receipt(receipt, args.api_base)
output = {
"submitted": True,
"message_id": posted.get("id"),
"receipt": receipt,
}
else:
output = fetch_status(contract, args.api_base)
except ReviewError as exc:
print(canonical_json({"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())

View file

@ -0,0 +1,176 @@
from __future__ import annotations
import importlib.util
import json
import subprocess
import tempfile
import unittest
from datetime import UTC, datetime
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location(
"wp0024_owner_review", ROOT / "scripts" / "wp0024-owner-review.py"
)
assert SPEC and SPEC.loader
module = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(module)
class OwnerReviewTests(unittest.TestCase):
def setUp(self) -> None:
self.contract = module.load_contract()
def test_contract_has_four_owners_and_only_known_checks(self) -> None:
self.assertEqual(
{"audit-core", "rapp-postgres", "railiance-cluster", "railiance-infra"},
set(self.contract["owners"]),
)
checks = {
check
for review in self.contract["owners"].values()
for check in review["checks"]
}
self.assertLessEqual(checks, module.KNOWN_CHECKS)
self.assertEqual(64, len(module.contract_digest(self.contract)))
def test_current_artifacts_match_contract(self) -> None:
for owner in self.contract["owners"]:
state = module.artifact_state(self.contract, owner)
self.assertTrue(all(item["matches"] for item in state.values()))
def test_artifact_change_prevents_checks_and_approval(self) -> None:
contract = json.loads(json.dumps(self.contract))
owner = "rapp-postgres"
review = contract["owners"][owner]
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
for relative in review["artifacts"]:
target = root / relative
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes((ROOT / relative).read_bytes())
(root / review["artifacts"][0]).write_text("changed", encoding="utf-8")
result = module.verify(
contract,
owner,
root=root,
check_runner=lambda _: self.fail("checks must not run after hash mismatch"),
)
self.assertFalse(result["passed"])
self.assertEqual([], result["checks"])
with self.assertRaises(module.ReviewError):
module.build_receipt(
contract, owner, "approve", owner, verification=result
)
def test_named_preflight_requires_safe_machine_result(self) -> None:
def runner(*args, **kwargs):
return subprocess.CompletedProcess(
args[0],
0,
stdout=json.dumps(
{
"automated_checks_passed": True,
"ready_for_live_execution": False,
"secret_values_observed": False,
}
),
stderr="",
)
result = module.run_named_check("database-lease-preflight", runner=runner)
self.assertTrue(result["passed"])
self.assertFalse(result["ready_for_live_execution"])
self.assertFalse(result["secret_values_observed"])
def test_receipt_round_trip_and_stale_digest_invalidation(self) -> None:
owner = "audit-core"
verification = {
"passed": True,
"checks": [
{"id": check, "passed": True}
for check in self.contract["owners"][owner]["checks"]
],
}
receipt = module.build_receipt(
self.contract,
owner,
"approve",
owner,
verification=verification,
now=datetime(2026, 8, 22, 12, 0, tzinfo=UTC),
)
message = {
"from_agent": owner,
"subject": module.receipt_subject(receipt),
"body": module.canonical_json(receipt),
}
self.assertEqual(receipt, module.parse_receipt_message(message))
current = module.aggregate_status(self.contract, [message])
self.assertEqual("approve", current["owners"][owner]["decision"])
self.assertTrue(current["contract_artifacts_current"])
self.assertFalse(current["all_approved"])
stale = json.loads(json.dumps(receipt))
stale["contract_digest"] = "0" * 64
stale_message = {
"from_agent": owner,
"subject": module.receipt_subject(stale),
"body": module.canonical_json(stale),
}
status = module.aggregate_status(self.contract, [stale_message])
self.assertEqual("missing", status["owners"][owner]["decision"])
self.assertEqual(1, status["owners"][owner]["stale_receipt_count"])
def test_incomplete_approval_is_not_counted(self) -> None:
owner = "rapp-postgres"
verification = {
"passed": True,
"checks": [
{"id": check, "passed": True}
for check in self.contract["owners"][owner]["checks"]
],
}
receipt = module.build_receipt(
self.contract, owner, "approve", owner, verification=verification
)
receipt["checks"].pop()
message = {
"from_agent": owner,
"subject": module.receipt_subject(receipt),
"body": module.canonical_json(receipt),
}
status = module.aggregate_status(self.contract, [message])
self.assertEqual("missing", status["owners"][owner]["decision"])
self.assertEqual(1, status["owners"][owner]["stale_receipt_count"])
def test_malformed_receipt_message_is_ignored(self) -> None:
message = {
"from_agent": "audit-core",
"subject": module.RECEIPT_PREFIX + "/invalid",
"body": "{}",
}
self.assertIsNone(module.parse_receipt_message(message))
def test_request_changes_requires_safe_note(self) -> None:
receipt = module.build_receipt(
self.contract,
"railiance-infra",
"request-changes",
"infra-owner",
note="Name the provider-console operator.",
)
self.assertEqual("request-changes", receipt["decision"])
with self.assertRaises(module.ReviewError):
module.build_receipt(
self.contract,
"railiance-infra",
"request-changes",
"infra-owner",
note="",
)
if __name__ == "__main__":
unittest.main()

View file

@ -201,6 +201,15 @@ Routed from immutable revision `bd25f7f` to audit-core
procedure paths, responsibility boundary, stop conditions, passing automated
preflight state, and explicit statement that no live mutation was requested.
**Direct review interface (2026-08-22):**
`interfaces/RAILIANCE-WP-0024-owner-reviews.json` and
`scripts/wp0024-owner-review.py` replace follow-up prose-to-task translation
with owner-executable `show`, `verify`, `approve`, `request-changes`, and
aggregate `status` operations. Approvals are valid only for the canonical
contract digest and the owner's pinned artifact hashes, so a changed procedure
invalidates the old receipt. The interface runs only closed-set, read-only
checks and cannot perform any T02/T03 live mutation.
## Acceptance
- [ ] E2 cannot proceed without enforced sender expiry and exact-path projection.