Files
aios/active-runtime/app/release_approval_reopen.py
T
owner 5a4e952b39
autonomy-smoke-tests pr82 PASS evidence_sha256=c16c68f86ee0c4e8cfd0af3f76078be45441ee151395fbde5800145c08e189b5
autonomy-unit-tests pr82 FAIL evidence_sha256=44994ff4cfe08df298fe59f790f3f98df7aa29e4d239ecd185b3143818c540cd
campaign-approval-gate pr82 PASS evidence_sha256=0616d6aa11496391686dcbd658123e51d8d057e1f02661af7ef8512554e404d8
git-diff-check pr82 PASS evidence_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
py-compile pr82 PASS evidence_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
reconciliation-contract pr82 PASS evidence_sha256=a0d5c1849c776f73fa64b02300e10c193241c0f9460e6ad4e69d73ae720de90b
release-state-machine pr82 PASS evidence_sha256=1d3314562a9345a9f75b1adf32d61168822c00d316f7db85833230bba023e448
secret-scan pr82 PASS evidence_sha256=9ca1f810bf76589206704716c4f3701a6b6aca1066b1d0c24b91d5be008f52a2
signer-validator-contract pr82 PASS evidence_sha256=775b7aa27d0c17f48b3feff20c836f14b6ed8ebcb90ec7e0b76d2a13c579268a
fix(governed): pin saga CAS to current revision in approval reopen (V2R2C2)
2026-09-12 15:16:32 +08:00

360 lines
15 KiB
Python

#!/usr/bin/env python3
"""Governed, append-only reopen of one expired Owner approval cycle."""
from __future__ import annotations
import hashlib
import json
import os
import re
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Mapping
REQUEST_SCHEMA = "aios.owner-approval-reopen-request.v1"
EVENT_NAME = "owner_approval_reopen_authorized"
_IDEMPOTENCY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$")
_HEX40 = re.compile(r"^[0-9a-f]{40}$")
_HEX64 = re.compile(r"^[0-9a-f]{64}$")
class ApprovalReopenError(RuntimeError):
def __init__(self, code: str, detail: str | None = None):
self.code = code
self.detail = detail
super().__init__(code if not detail else f"{code}:{detail}")
@dataclass(frozen=True)
class ReopenRequest:
candidate_id: str
expected_approval_fingerprint: str
expected_source_commit: str
expected_tree_oid: str
expected_artifact_sha256: str
expected_manifest_sha256: str
expected_release_plan_sha256: str
expected_policy_sha256: str
reason: str
idempotency_key: str
@classmethod
def from_mapping(cls, raw: Mapping[str, Any]) -> "ReopenRequest":
if raw.get("schema") != REQUEST_SCHEMA:
raise ApprovalReopenError("APPROVAL_REOPEN_SCHEMA_INVALID")
allowed = {
"schema", "candidate_id", "expected_approval_fingerprint",
"expected_source_commit", "expected_tree_oid",
"expected_artifact_sha256", "expected_manifest_sha256",
"expected_release_plan_sha256", "expected_policy_sha256",
"reason", "idempotency_key",
}
if set(raw) != allowed:
raise ApprovalReopenError("APPROVAL_REOPEN_REQUEST_FIELDS_INVALID")
req = cls(
candidate_id=str(raw["candidate_id"]),
expected_approval_fingerprint=str(raw["expected_approval_fingerprint"]),
expected_source_commit=str(raw["expected_source_commit"]),
expected_tree_oid=str(raw["expected_tree_oid"]),
expected_artifact_sha256=str(raw["expected_artifact_sha256"]),
expected_manifest_sha256=str(raw["expected_manifest_sha256"]),
expected_release_plan_sha256=str(raw["expected_release_plan_sha256"]),
expected_policy_sha256=str(raw["expected_policy_sha256"]),
reason=str(raw["reason"]),
idempotency_key=str(raw["idempotency_key"]),
)
if not req.candidate_id or "/" in req.candidate_id or ".." in req.candidate_id:
raise ApprovalReopenError("APPROVAL_REOPEN_CANDIDATE_ID_INVALID")
if not _HEX64.fullmatch(req.expected_approval_fingerprint):
raise ApprovalReopenError("APPROVAL_REOPEN_FINGERPRINT_INVALID")
if not _HEX40.fullmatch(req.expected_source_commit):
raise ApprovalReopenError("APPROVAL_REOPEN_SOURCE_INVALID")
if not _HEX40.fullmatch(req.expected_tree_oid):
raise ApprovalReopenError("APPROVAL_REOPEN_TREE_INVALID")
for value, code in (
(req.expected_artifact_sha256, "APPROVAL_REOPEN_ARTIFACT_INVALID"),
(req.expected_manifest_sha256, "APPROVAL_REOPEN_MANIFEST_INVALID"),
(req.expected_release_plan_sha256, "APPROVAL_REOPEN_RELEASE_PLAN_INVALID"),
(req.expected_policy_sha256, "APPROVAL_REOPEN_POLICY_INVALID"),
):
if not _HEX64.fullmatch(value):
raise ApprovalReopenError(code)
if not (1 <= len(req.reason.strip()) <= 512):
raise ApprovalReopenError("APPROVAL_REOPEN_REASON_INVALID")
if not _IDEMPOTENCY_RE.fullmatch(req.idempotency_key):
raise ApprovalReopenError("APPROVAL_REOPEN_IDEMPOTENCY_KEY_INVALID")
return req
def _canon(value: Any) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()
def _sha(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def _time(value: Any, code: str) -> datetime:
if not isinstance(value, str) or not value:
raise ApprovalReopenError(code)
try:
dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError as exc:
raise ApprovalReopenError(code) from exc
if dt.tzinfo is None:
raise ApprovalReopenError(code)
return dt.astimezone(timezone.utc)
def _secure_json(path: Path) -> dict[str, Any]:
if path.is_symlink() or not path.is_file():
raise ApprovalReopenError("APPROVAL_REOPEN_STATE_FILE_INVALID")
st = path.stat()
if st.st_mode & 0o022:
raise ApprovalReopenError("APPROVAL_REOPEN_STATE_FILE_WRITABLE")
if st.st_uid not in {0, os.geteuid()}:
raise ApprovalReopenError("APPROVAL_REOPEN_STATE_FILE_OWNER_INVALID")
try:
obj = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise ApprovalReopenError("APPROVAL_REOPEN_STATE_FILE_INVALID") from exc
if not isinstance(obj, dict):
raise ApprovalReopenError("APPROVAL_REOPEN_STATE_FILE_INVALID")
return obj
def _fsync_dir(path: Path) -> None:
fd = os.open(path, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
try:
os.fsync(fd)
finally:
os.close(fd)
def _paths(root: Path, cid: str) -> dict[str, Path]:
return {
"candidate": root / "candidates" / f"{cid}.json",
"saga": root / "sagas" / f"{cid}.json",
"plan": root / "release-plans" / f"{cid}.json",
"acceptance": root / "acceptance" / f"{cid}.json",
"approval": root / "approvals" / f"{cid}.json",
}
def _history(root: Path, cid: str, fp: str) -> Path:
return root / "approval-history" / cid / f"{fp}.json"
def _archive(active: Path, history: Path) -> None:
history.parent.mkdir(parents=True, exist_ok=True, mode=0o750)
try:
os.chmod(history.parent, 0o750)
except PermissionError:
pass
original = active.read_bytes()
if history.exists():
if history.is_symlink() or not history.is_file() or history.read_bytes() != original:
raise ApprovalReopenError("APPROVAL_REOPEN_HISTORY_COLLISION")
return
try:
os.link(active, history)
except OSError:
fd = os.open(history, os.O_WRONLY | os.O_CREAT | os.O_EXCL,
active.stat().st_mode & 0o777)
try:
view = memoryview(original)
while view:
n = os.write(fd, view)
view = view[n:]
os.fsync(fd)
finally:
os.close(fd)
if history.read_bytes() != original:
raise ApprovalReopenError("APPROVAL_REOPEN_HISTORY_COPY_FAILED")
_fsync_dir(history.parent)
def _details(event: Mapping[str, Any]) -> Mapping[str, Any]:
val = event.get("details")
return val if isinstance(val, Mapping) else {}
def _cycle_event(saga: Mapping[str, Any], cycle_id: str) -> Mapping[str, Any] | None:
for event in saga.get("events") or []:
if isinstance(event, Mapping) and event.get("event") == EVENT_NAME \
and _details(event).get("approval_cycle_id") == cycle_id:
return event
return None
def _state(name: str) -> Any:
from silent_release_hardening import SagaState # lazy: avoids import cycle
return getattr(SagaState, name)
def _cycle_id(req: ReopenRequest) -> str:
return "approval-reopen-" + _sha(_canon({
"schema": REQUEST_SCHEMA,
"candidate_id": req.candidate_id,
"old_fingerprint": req.expected_approval_fingerprint,
"source_commit": req.expected_source_commit,
"tree_oid": req.expected_tree_oid,
"artifact_sha256": req.expected_artifact_sha256,
"manifest_sha256": req.expected_manifest_sha256,
"release_plan_sha256": req.expected_release_plan_sha256,
"policy_sha256": req.expected_policy_sha256,
"idempotency_key": req.idempotency_key,
}))[:24]
def _validate(req: ReopenRequest, candidate: Mapping[str, Any],
saga: Mapping[str, Any], plan: Mapping[str, Any],
acceptance: Mapping[str, Any], approval: Mapping[str, Any],
now: datetime) -> None:
if saga.get("journal_integrity") is False:
raise ApprovalReopenError("APPROVAL_REOPEN_JOURNAL_INTEGRITY_INVALID")
if saga.get("state") != "APPROVED":
raise ApprovalReopenError("APPROVAL_REOPEN_SAGA_STATE_INVALID")
if candidate.get("candidate_id") != req.candidate_id:
raise ApprovalReopenError("APPROVAL_REOPEN_CANDIDATE_MISMATCH")
if candidate.get("state") not in {"READY_FOR_APPROVAL", "APPROVED"}:
raise ApprovalReopenError("APPROVAL_REOPEN_CANDIDATE_STATE_INVALID")
if candidate.get("approval_state") not in {None, "PENDING"}:
raise ApprovalReopenError("APPROVAL_REOPEN_CANDIDATE_APPROVAL_STATE_INVALID")
# ReleaseCandidate v1 durable records do not persist side_effects_executed.
# Compatibility rule is intentionally fail-closed:
# - an absent key is accepted only together with the independent Saga
# invariant below that the accepted Owner-approval event is the final
# Saga event;
# - if a future durable schema persists the key, it must be exactly False;
# - explicit True/None/number/string values are rejected.
if "side_effects_executed" in candidate and candidate.get("side_effects_executed") is not False:
raise ApprovalReopenError("APPROVAL_REOPEN_SIDE_EFFECTS_ALREADY_EXECUTED")
checks = {
"source": candidate.get("source_commit") == req.expected_source_commit,
"tree": candidate.get("tree_oid") == req.expected_tree_oid,
"artifact": candidate.get("artifact_sha256") == req.expected_artifact_sha256,
"manifest": candidate.get("manifest_sha256") == req.expected_manifest_sha256,
"approval_fingerprint": approval.get("fingerprint") == req.expected_approval_fingerprint,
"approval_source": approval.get("source_commit") == req.expected_source_commit,
"approval_tree": approval.get("tree_oid") == req.expected_tree_oid,
"approval_artifact": approval.get("artifact_sha256") == req.expected_artifact_sha256,
"approval_manifest": approval.get("manifest_sha256") == req.expected_manifest_sha256,
"approval_release_plan": approval.get("release_plan_sha256") == req.expected_release_plan_sha256,
"approval_policy": approval.get("policy_sha256") == req.expected_policy_sha256,
"acceptance": acceptance.get("status") == "ACCEPTED",
"acceptance_release_plan": acceptance.get("release_plan_sha256") == req.expected_release_plan_sha256,
"plan_sha": (plan.get("sha256") or plan.get("release_plan_sha256")) == req.expected_release_plan_sha256,
"plan_policy": plan.get("policy_sha256") == req.expected_policy_sha256,
}
bad = sorted(k for k, ok in checks.items() if not ok)
if bad:
raise ApprovalReopenError("APPROVAL_REOPEN_BINDING_MISMATCH", ",".join(bad))
if now <= _time(approval.get("expires_at"), "APPROVAL_REOPEN_APPROVAL_EXPIRY_INVALID"):
raise ApprovalReopenError("APPROVAL_REOPEN_APPROVAL_NOT_EXPIRED")
events = [e for e in saga.get("events") or [] if isinstance(e, Mapping)]
accepted = None
for i, event in enumerate(events):
if event.get("event") == "signed_owner_approval_accepted" \
and _details(event).get("approval_fingerprint") == req.expected_approval_fingerprint:
accepted = i
if accepted is None:
raise ApprovalReopenError("APPROVAL_REOPEN_ACCEPTED_EVENT_MISSING")
if accepted != len(events) - 1:
raise ApprovalReopenError("APPROVAL_REOPEN_SAGA_ALREADY_ADVANCED")
def reopen_owner_approval(service: Any, raw: Mapping[str, Any], *,
state_root: Path | None = None,
now: datetime | None = None) -> dict[str, Any]:
req = ReopenRequest.from_mapping(raw)
raw_root = state_root or os.environ.get("AIOS_SILENT_RELEASE_STATE_ROOT", "")
if not str(raw_root):
raise ApprovalReopenError("APPROVAL_REOPEN_STATE_ROOT_MISSING")
root = Path(raw_root).expanduser().resolve()
paths = _paths(root, req.candidate_id)
cycle = _cycle_id(req)
history = _history(root, req.candidate_id, req.expected_approval_fingerprint)
saga = _secure_json(paths["saga"])
prior = _cycle_event(saga, cycle)
if prior is not None:
if saga.get("state") != "READY_FOR_APPROVAL":
raise ApprovalReopenError("APPROVAL_REOPEN_CYCLE_ALREADY_CONSUMED")
if not history.is_file():
raise ApprovalReopenError("APPROVAL_REOPEN_HISTORY_MISSING")
if paths["approval"].exists():
active = _secure_json(paths["approval"])
if active.get("fingerprint") != req.expected_approval_fingerprint:
raise ApprovalReopenError("APPROVAL_REOPEN_ACTIVE_APPROVAL_CHANGED")
paths["approval"].unlink()
_fsync_dir(paths["approval"].parent)
return {
"schema": "aios.owner-approval-reopen-result.v1",
"status": "IDEMPOTENT",
"candidate_id": req.candidate_id,
"approval_cycle_id": cycle,
"supersedes_fingerprint": req.expected_approval_fingerprint,
"saga_state": "READY_FOR_APPROVAL",
"history_ref": str(history.relative_to(root)),
}
candidate = _secure_json(paths["candidate"])
plan = _secure_json(paths["plan"])
acceptance = _secure_json(paths["acceptance"])
approval = _secure_json(paths["approval"])
_validate(req, candidate, saga, plan, acceptance, approval,
(now or datetime.now(timezone.utc)).astimezone(timezone.utc))
_archive(paths["approval"], history)
event_details = {
"schema": "aios.owner-approval-reopen-event.v1",
"approval_cycle_id": cycle,
"supersedes_approval_fingerprint": req.expected_approval_fingerprint,
"expired_at": approval.get("expires_at"),
"release_plan_sha256": req.expected_release_plan_sha256,
"source_commit": req.expected_source_commit,
"tree_oid": req.expected_tree_oid,
"artifact_sha256": req.expected_artifact_sha256,
"reason": req.reason.strip(),
"idempotency_digest": _sha(req.idempotency_key.encode()),
}
try:
service.sagas.compare_and_set(
req.candidate_id,
expected_revision=saga.get("revision"),
expected_states={_state("APPROVED")},
target=_state("READY_FOR_APPROVAL"),
event=EVENT_NAME,
details=event_details,
)
except Exception as exc:
raise ApprovalReopenError("APPROVAL_REOPEN_SAGA_CAS_FAILED") from exc
try:
active = _secure_json(paths["approval"])
if active.get("fingerprint") != req.expected_approval_fingerprint:
raise ApprovalReopenError("APPROVAL_REOPEN_ACTIVE_APPROVAL_CHANGED")
paths["approval"].unlink()
_fsync_dir(paths["approval"].parent)
except ApprovalReopenError:
raise
except OSError as exc:
raise ApprovalReopenError("APPROVAL_REOPEN_ACTIVE_APPROVAL_REMOVE_FAILED") from exc
return {
"schema": "aios.owner-approval-reopen-result.v1",
"status": "REOPENED",
"candidate_id": req.candidate_id,
"approval_cycle_id": cycle,
"supersedes_fingerprint": req.expected_approval_fingerprint,
"saga_state": "READY_FOR_APPROVAL",
"history_ref": str(history.relative_to(root)),
"release_plan_sha256": req.expected_release_plan_sha256,
"source_commit": req.expected_source_commit,
"tree_oid": req.expected_tree_oid,
}