Add direct WP-0024 owner review interface
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02669-87ee-7a31-b111-edc95a16e0fa
This commit is contained in:
parent
3f9e4535d1
commit
0d9cebedea
6 changed files with 877 additions and 1 deletions
13
Makefile
13
Makefile
|
|
@ -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
|
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)
|
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
|
.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
|
||||||
|
|
|
||||||
55
docs/wp0024-owner-review-interface.md
Normal file
55
docs/wp0024-owner-review-interface.md
Normal 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.
|
||||||
102
interfaces/RAILIANCE-WP-0024-owner-reviews.json
Normal file
102
interfaces/RAILIANCE-WP-0024-owner-reviews.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
523
scripts/wp0024-owner-review.py
Normal file
523
scripts/wp0024-owner-review.py
Normal 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())
|
||||||
176
tests/test_wp0024_owner_review.py
Normal file
176
tests/test_wp0024_owner_review.py
Normal 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()
|
||||||
|
|
@ -201,6 +201,15 @@ Routed from immutable revision `bd25f7f` to audit-core
|
||||||
procedure paths, responsibility boundary, stop conditions, passing automated
|
procedure paths, responsibility boundary, stop conditions, passing automated
|
||||||
preflight state, and explicit statement that no live mutation was requested.
|
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
|
## Acceptance
|
||||||
|
|
||||||
- [ ] E2 cannot proceed without enforced sender expiry and exact-path projection.
|
- [ ] E2 cannot proceed without enforced sender expiry and exact-path projection.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue