861 lines
41 KiB
Python
861 lines
41 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
from contextlib import contextmanager
|
|
from datetime import datetime, timezone
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import sqlite3
|
|
import subprocess
|
|
from typing import Any, Iterator, Mapping, Protocol
|
|
|
|
try:
|
|
import fcntl
|
|
except ImportError: # pragma: no cover
|
|
fcntl = None
|
|
|
|
from .gitea_protected_branch import (
|
|
CASConflict,
|
|
FileProtectedBranchAdapter,
|
|
SQLiteProtectedBranchAdapter,
|
|
calculate_snapshot_sha256,
|
|
)
|
|
from .journal import JournalStore, atomic_write_json, read_json, redact, sha256_json
|
|
from . import state_machine as state_machine_module
|
|
from . import validator as validator_module
|
|
|
|
|
|
DEFAULT_STATE_ROOT = Path("/var/lib/aios-gitops/governance-activation")
|
|
DEFAULT_LOCK_PATH = Path("/run/aios-gitops/governance-activation.lock")
|
|
LIVE_GITEA_DB = Path("/data/aios-gitops/runtime/gitea-bin/data/gitea.db")
|
|
LIVE_AUTHORITATIVE_GIT = Path("/data/aios-gitops/repo.git")
|
|
HEX40 = re.compile(r"^[0-9a-f]{40}$")
|
|
HEX64 = re.compile(r"^[0-9a-f]{64}$")
|
|
REQUIRED_BINDINGS = {
|
|
"task_id",
|
|
"operation_id",
|
|
"expected_governance_oid",
|
|
"expected_validator_sha256",
|
|
"expected_state_machine_sha256",
|
|
"expected_branch_protection_sha256",
|
|
"activation_approval_merge_oid",
|
|
"evidence_closure_oid",
|
|
"owner_review_oid",
|
|
"observed_governance_oid",
|
|
"observed_validator_sha256",
|
|
"observed_state_machine_sha256",
|
|
"observed_evidence_closure_oid",
|
|
"observed_owner_review_oid",
|
|
"approval_request_head_oid",
|
|
"activation_approval_pr_number",
|
|
"expires_at",
|
|
"validator_path",
|
|
"state_machine_path",
|
|
"governance_git_dir",
|
|
"governance_ref",
|
|
"evidence_git_dir",
|
|
"approval_db_path",
|
|
"expected_gitea_schema_sha256",
|
|
"backend",
|
|
"backend_path",
|
|
"repo_id",
|
|
"branch",
|
|
"activation_manifest_path",
|
|
"activation_manifest_blob_oid",
|
|
"activation_manifest_sha256",
|
|
"implementation_commit_oid",
|
|
"implementation_tree_oid",
|
|
"runtime_sources",
|
|
"artifact_path",
|
|
"artifact_sha256",
|
|
"rollback_artifact_path",
|
|
"rollback_artifact_sha256",
|
|
"test_evidence_path",
|
|
"test_evidence_sha256",
|
|
"evidence_closure_path",
|
|
"evidence_closure_sha256",
|
|
"machine_status_producer_id",
|
|
"machine_status_producer_name",
|
|
}
|
|
REQUIRED_MACHINE_CONTEXTS = {
|
|
"aios/validator",
|
|
"aios/state-machine",
|
|
"aios/branch-protection",
|
|
"aios/secret-scan",
|
|
"aios/evidence-closure",
|
|
}
|
|
REQUIRED_RUNTIME_GIT_PATHS = {
|
|
"services/aios-single-owner-governance/aios_single_owner_governance/activation.py",
|
|
"services/aios-single-owner-governance/aios_single_owner_governance/gitea_protected_branch.py",
|
|
"services/aios-single-owner-governance/aios_single_owner_governance/journal.py",
|
|
"services/aios-single-owner-governance/aios_single_owner_governance/state_machine.py",
|
|
"services/aios-single-owner-governance/aios_single_owner_governance/validator.py",
|
|
}
|
|
|
|
|
|
class InjectedCrash(BaseException):
|
|
pass
|
|
|
|
|
|
class Adapter(Protocol):
|
|
def read_current(self) -> dict[str, Any]: ...
|
|
def calculate_snapshot_sha256(self, snapshot: Mapping[str, Any]) -> str: ...
|
|
def apply_atomic(self, expected_sha256: str, target: Mapping[str, Any]) -> dict[str, Any]: ...
|
|
def verify_applied(self, expected_sha256: str) -> bool: ...
|
|
def rollback_atomic(self, expected_target_sha256: str, previous: Mapping[str, Any]) -> dict[str, Any]: ...
|
|
def verify_rollback(self, expected_sha256: str) -> bool: ...
|
|
|
|
|
|
@contextmanager
|
|
def exclusive_lock(path: Path) -> Iterator[None]:
|
|
path.parent.mkdir(parents=True, exist_ok=True, mode=0o750)
|
|
flags = os.O_RDWR | os.O_CREAT
|
|
if hasattr(os, "O_NOFOLLOW"):
|
|
flags |= os.O_NOFOLLOW
|
|
fd = os.open(path, flags, 0o640)
|
|
try:
|
|
os.fchmod(fd, 0o640)
|
|
if fcntl is not None:
|
|
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
yield
|
|
finally:
|
|
if fcntl is not None:
|
|
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
os.close(fd)
|
|
|
|
|
|
def _require_hex(name: str, value: object, lengths: tuple[int, ...]) -> None:
|
|
if not isinstance(value, str) or len(value) not in lengths or not re.fullmatch(r"[0-9a-f]+", value):
|
|
raise RuntimeError(f"{name.upper()}_INVALID")
|
|
|
|
|
|
def _sha256_file(path: Path) -> str:
|
|
if not path.is_absolute() or not path.is_file() or path.is_symlink():
|
|
raise RuntimeError("VERIFIED_COMPONENT_PATH_INVALID")
|
|
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 _git(git_dir: Path, *args: str) -> str:
|
|
if not git_dir.is_absolute() or not git_dir.exists() or git_dir.is_symlink():
|
|
raise RuntimeError("GIT_OBJECT_STORE_INVALID")
|
|
result = subprocess.run(
|
|
["git", f"--git-dir={git_dir}", *args],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
if result.returncode != 0:
|
|
raise RuntimeError("GIT_EVIDENCE_VERIFICATION_FAILED")
|
|
return result.stdout.strip()
|
|
|
|
|
|
def _git_raw(git_dir: Path, *args: str) -> bytes:
|
|
if not git_dir.is_absolute() or not git_dir.exists() or git_dir.is_symlink():
|
|
raise RuntimeError("GIT_OBJECT_STORE_INVALID")
|
|
result = subprocess.run(
|
|
["git", f"--git-dir={git_dir}", *args], capture_output=True, check=False
|
|
)
|
|
if result.returncode != 0:
|
|
raise RuntimeError("GIT_EVIDENCE_VERIFICATION_FAILED")
|
|
return result.stdout
|
|
|
|
|
|
def _parse_utc(value: object) -> datetime:
|
|
if not isinstance(value, str):
|
|
raise RuntimeError("EXPIRY_INVALID")
|
|
try:
|
|
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
except ValueError as exc:
|
|
raise RuntimeError("EXPIRY_INVALID") from exc
|
|
if parsed.tzinfo is None:
|
|
raise RuntimeError("EXPIRY_INVALID")
|
|
return parsed.astimezone(timezone.utc)
|
|
|
|
|
|
def _utc_now() -> datetime:
|
|
return datetime.now(timezone.utc)
|
|
|
|
|
|
def _approved_execution_binding(bindings: Mapping[str, Any]) -> dict[str, Any]:
|
|
return {
|
|
"task_id": bindings["task_id"],
|
|
"operation_id": bindings["operation_id"],
|
|
"expected_validator_sha256": bindings["expected_validator_sha256"],
|
|
"expected_state_machine_sha256": bindings["expected_state_machine_sha256"],
|
|
"expected_branch_protection_sha256": bindings["expected_branch_protection_sha256"],
|
|
"expected_gitea_schema_sha256": bindings["expected_gitea_schema_sha256"],
|
|
"target_branch_protection_sha256": calculate_snapshot_sha256(
|
|
bindings["target_branch_protection"]
|
|
),
|
|
"evidence_closure_oid": bindings["evidence_closure_oid"],
|
|
"evidence_closure_path": bindings["evidence_closure_path"],
|
|
"evidence_closure_sha256": bindings["evidence_closure_sha256"],
|
|
"implementation_commit_oid": bindings["implementation_commit_oid"],
|
|
"implementation_tree_oid": bindings["implementation_tree_oid"],
|
|
"runtime_sources": bindings["runtime_sources"],
|
|
"artifact_path": bindings["artifact_path"],
|
|
"artifact_sha256": bindings["artifact_sha256"],
|
|
"rollback_artifact_path": bindings["rollback_artifact_path"],
|
|
"rollback_artifact_sha256": bindings["rollback_artifact_sha256"],
|
|
"test_evidence_path": bindings["test_evidence_path"],
|
|
"test_evidence_sha256": bindings["test_evidence_sha256"],
|
|
"machine_status_producer_id": bindings["machine_status_producer_id"],
|
|
"machine_status_producer_name": bindings["machine_status_producer_name"],
|
|
"backend": bindings["backend"],
|
|
"repo_id": bindings["repo_id"],
|
|
"branch": bindings["branch"],
|
|
"requester": bindings["requester"],
|
|
"executor": bindings["executor"],
|
|
"forbidden_operations": bindings["forbidden_operations"],
|
|
}
|
|
|
|
|
|
def _same_path(left: Path, right: Path) -> bool:
|
|
try:
|
|
return left.resolve(strict=True) == right.resolve(strict=True) and os.path.samefile(left, right)
|
|
except (FileNotFoundError, OSError):
|
|
return False
|
|
|
|
|
|
def _verify_authority_paths(bindings: Mapping[str, Any]) -> None:
|
|
if bindings["backend"] != "sqlite":
|
|
return
|
|
approval_db = Path(str(bindings["approval_db_path"]))
|
|
backend_db = Path(str(bindings["backend_path"]))
|
|
if not _same_path(approval_db, backend_db):
|
|
raise RuntimeError("APPROVAL_DB_BACKEND_DB_IDENTITY_MISMATCH")
|
|
live = _same_path(backend_db, LIVE_GITEA_DB)
|
|
if live:
|
|
if not _same_path(Path(str(bindings["governance_git_dir"])), LIVE_AUTHORITATIVE_GIT):
|
|
raise RuntimeError("AUTHORITATIVE_GOVERNANCE_GIT_REQUIRED")
|
|
if not _same_path(Path(str(bindings["evidence_git_dir"])), LIVE_AUTHORITATIVE_GIT):
|
|
raise RuntimeError("AUTHORITATIVE_EVIDENCE_GIT_REQUIRED")
|
|
return
|
|
acceptance_root = os.environ.get("AIOS_GOVERNANCE_ACTIVATION_ACCEPTANCE_ROOT")
|
|
if not acceptance_root:
|
|
raise RuntimeError("NON_LIVE_SQLITE_BACKEND_DENIED")
|
|
root = Path(acceptance_root).resolve(strict=True)
|
|
if not str(root).startswith("/run/aios-live-activation-acceptance-"):
|
|
raise RuntimeError("ACCEPTANCE_ROOT_INVALID")
|
|
for path in (approval_db, backend_db, Path(str(bindings["governance_git_dir"])), Path(str(bindings["evidence_git_dir"]))):
|
|
try:
|
|
path.resolve(strict=True).relative_to(root)
|
|
except (FileNotFoundError, ValueError) as exc:
|
|
raise RuntimeError("ACCEPTANCE_AUTHORITY_ESCAPE_DENIED") from exc
|
|
|
|
|
|
def _load_approved_manifest(bindings: Mapping[str, Any], git_dir: Path) -> dict[str, Any]:
|
|
path = str(bindings["activation_manifest_path"])
|
|
if not path.startswith("approvals/") or not path.endswith(".json") or ".." in Path(path).parts:
|
|
raise RuntimeError("ACTIVATION_MANIFEST_PATH_INVALID")
|
|
merge_oid = str(bindings["activation_approval_merge_oid"])
|
|
blob_oid = _git(git_dir, "rev-parse", f"{merge_oid}:{path}")
|
|
if blob_oid != bindings["activation_manifest_blob_oid"]:
|
|
raise RuntimeError("ACTIVATION_MANIFEST_BLOB_MISMATCH")
|
|
raw_result = subprocess.run(
|
|
["git", f"--git-dir={git_dir}", "show", f"{merge_oid}:{path}"],
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
if raw_result.returncode != 0:
|
|
raise RuntimeError("ACTIVATION_MANIFEST_READ_FAILED")
|
|
if hashlib.sha256(raw_result.stdout).hexdigest() != bindings["activation_manifest_sha256"]:
|
|
raise RuntimeError("ACTIVATION_MANIFEST_SHA256_MISMATCH")
|
|
try:
|
|
manifest = json.loads(raw_result.stdout)
|
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
raise RuntimeError("ACTIVATION_MANIFEST_INVALID") from exc
|
|
if not isinstance(manifest, dict):
|
|
raise RuntimeError("ACTIVATION_MANIFEST_INVALID")
|
|
return manifest
|
|
|
|
|
|
def _require_approval_unexpired(bindings: Mapping[str, Any]) -> None:
|
|
if _utc_now() >= _parse_utc(bindings["expires_at"]):
|
|
raise RuntimeError("APPROVAL_EXPIRED")
|
|
|
|
|
|
def _safe_git_path(value: object) -> str:
|
|
path = str(value)
|
|
if not path or Path(path).is_absolute() or ".." in Path(path).parts:
|
|
raise RuntimeError("APPROVED_GIT_PATH_INVALID")
|
|
return path
|
|
|
|
|
|
def _verify_approved_entities(bindings: Mapping[str, Any], git_dir: Path) -> None:
|
|
implementation_oid = str(bindings["implementation_commit_oid"])
|
|
_git(git_dir, "cat-file", "-e", f"{implementation_oid}^{{commit}}")
|
|
if _git(git_dir, "rev-parse", f"{implementation_oid}^{{tree}}") != bindings["implementation_tree_oid"]:
|
|
raise RuntimeError("IMPLEMENTATION_TREE_MISMATCH")
|
|
sources = bindings["runtime_sources"]
|
|
if not isinstance(sources, list) or not sources:
|
|
raise RuntimeError("RUNTIME_SOURCES_INVALID")
|
|
seen_git: set[str] = set()
|
|
seen_runtime: set[str] = set()
|
|
loaded_runtime_paths = {
|
|
"services/aios-single-owner-governance/aios_single_owner_governance/activation.py": Path(__file__).resolve(),
|
|
"services/aios-single-owner-governance/aios_single_owner_governance/gitea_protected_branch.py": Path(__import__(SQLiteProtectedBranchAdapter.__module__, fromlist=["x"]).__file__).resolve(),
|
|
"services/aios-single-owner-governance/aios_single_owner_governance/journal.py": Path(__import__(JournalStore.__module__, fromlist=["x"]).__file__).resolve(),
|
|
"services/aios-single-owner-governance/aios_single_owner_governance/state_machine.py": Path(state_machine_module.__file__).resolve(),
|
|
"services/aios-single-owner-governance/aios_single_owner_governance/validator.py": Path(validator_module.__file__).resolve(),
|
|
}
|
|
for source in sources:
|
|
if not isinstance(source, dict) or set(source) != {"git_path", "runtime_path", "sha256"}:
|
|
raise RuntimeError("RUNTIME_SOURCE_BINDING_INVALID")
|
|
git_path = _safe_git_path(source["git_path"])
|
|
runtime_path = str(source["runtime_path"])
|
|
_require_hex("runtime_source_sha256", source["sha256"], (64,))
|
|
if git_path in seen_git or runtime_path in seen_runtime:
|
|
raise RuntimeError("RUNTIME_SOURCE_BINDING_DUPLICATE")
|
|
seen_git.add(git_path)
|
|
seen_runtime.add(runtime_path)
|
|
if Path(runtime_path).resolve() != loaded_runtime_paths.get(git_path):
|
|
raise RuntimeError("RUNTIME_SOURCE_PATH_MISMATCH")
|
|
if hashlib.sha256(_git_raw(git_dir, "show", f"{implementation_oid}:{git_path}")).hexdigest() != source["sha256"]:
|
|
raise RuntimeError("IMPLEMENTATION_SOURCE_DIGEST_MISMATCH")
|
|
if _sha256_file(Path(runtime_path)) != source["sha256"]:
|
|
raise RuntimeError("RUNTIME_SOURCE_DIGEST_MISMATCH")
|
|
if seen_git != REQUIRED_RUNTIME_GIT_PATHS:
|
|
raise RuntimeError("RUNTIME_SOURCE_SET_MISMATCH")
|
|
for prefix in ("artifact", "rollback_artifact", "test_evidence"):
|
|
if _sha256_file(Path(str(bindings[f"{prefix}_path"]))) != bindings[f"{prefix}_sha256"]:
|
|
raise RuntimeError("APPROVED_ENTITY_DIGEST_MISMATCH")
|
|
|
|
closure_oid = str(bindings["evidence_closure_oid"])
|
|
if _git(git_dir, "cat-file", "-t", closure_oid) != "commit":
|
|
raise RuntimeError("EVIDENCE_CLOSURE_NOT_COMMIT")
|
|
parents = _git(git_dir, "rev-list", "--parents", "-n", "1", closure_oid).split()
|
|
if parents != [closure_oid, implementation_oid]:
|
|
raise RuntimeError("EVIDENCE_CLOSURE_PARENT_MISMATCH")
|
|
closure_path = _safe_git_path(bindings["evidence_closure_path"])
|
|
if hashlib.sha256(_git_raw(git_dir, "show", f"{closure_oid}:{closure_path}")).hexdigest() != bindings["evidence_closure_sha256"]:
|
|
raise RuntimeError("EVIDENCE_CLOSURE_SHA256_MISMATCH")
|
|
if _git(git_dir, "diff", "--name-status", implementation_oid, closure_oid) != f"A\t{closure_path}":
|
|
raise RuntimeError("EVIDENCE_CLOSURE_DIFF_INVALID")
|
|
|
|
|
|
def _verify_external_evidence(
|
|
bindings: Mapping[str, Any], *, require_unexpired: bool = True
|
|
) -> None:
|
|
_verify_authority_paths(bindings)
|
|
validator_sha = _sha256_file(Path(str(bindings["validator_path"])))
|
|
state_machine_sha = _sha256_file(Path(str(bindings["state_machine_path"])))
|
|
if validator_sha != bindings["expected_validator_sha256"] or validator_sha != bindings["observed_validator_sha256"]:
|
|
raise RuntimeError("VALIDATOR_DIGEST_MISMATCH")
|
|
if state_machine_sha != bindings["expected_state_machine_sha256"] or state_machine_sha != bindings["observed_state_machine_sha256"]:
|
|
raise RuntimeError("STATE_MACHINE_DIGEST_MISMATCH")
|
|
|
|
governance_git_dir = Path(str(bindings["governance_git_dir"]))
|
|
governance_oid = _git(governance_git_dir, "rev-parse", "--verify", str(bindings["governance_ref"]))
|
|
if governance_oid != bindings["expected_governance_oid"] or governance_oid != bindings["observed_governance_oid"]:
|
|
raise RuntimeError("WRONG_GOVERNANCE_OID_DENIED")
|
|
merge_oid = str(bindings["activation_approval_merge_oid"])
|
|
_git(governance_git_dir, "cat-file", "-e", f"{merge_oid}^{{commit}}")
|
|
if subprocess.run(
|
|
["git", f"--git-dir={governance_git_dir}", "merge-base", "--is-ancestor", merge_oid, governance_oid],
|
|
capture_output=True,
|
|
check=False,
|
|
).returncode != 0:
|
|
raise RuntimeError("ACTIVATION_APPROVAL_NOT_IN_GOVERNANCE_REF")
|
|
manifest = _load_approved_manifest(bindings, governance_git_dir)
|
|
if manifest.get("approval_type") != "SINGLE_OWNER_GOVERNANCE_LIVE_ACTIVATION_IMPLEMENTATION_AND_EXECUTION":
|
|
raise RuntimeError("ACTIVATION_APPROVAL_TYPE_MISMATCH")
|
|
if manifest.get("execution_binding") != _approved_execution_binding(bindings):
|
|
raise RuntimeError("APPROVED_EXECUTION_BINDING_MISMATCH")
|
|
if manifest.get("execution_binding_sha256") != sha256_json(manifest["execution_binding"]):
|
|
raise RuntimeError("APPROVED_EXECUTION_BINDING_SHA256_MISMATCH")
|
|
if manifest.get("expires_at") != bindings["expires_at"]:
|
|
raise RuntimeError("APPROVAL_EXPIRY_BINDING_MISMATCH")
|
|
|
|
manifest_bound_fields = (
|
|
"implementation_commit_oid", "implementation_tree_oid", "runtime_sources",
|
|
"artifact_path", "artifact_sha256", "rollback_artifact_path",
|
|
"rollback_artifact_sha256", "test_evidence_path", "test_evidence_sha256",
|
|
"evidence_closure_oid", "evidence_closure_path", "evidence_closure_sha256",
|
|
"machine_status_producer_id", "machine_status_producer_name",
|
|
)
|
|
if any(manifest.get(field) != bindings[field] for field in manifest_bound_fields):
|
|
raise RuntimeError("APPROVED_ARTIFACT_BINDING_MISMATCH")
|
|
|
|
evidence_git_dir = Path(str(bindings["evidence_git_dir"]))
|
|
evidence_oid = str(bindings["evidence_closure_oid"])
|
|
if evidence_oid != bindings["observed_evidence_closure_oid"]:
|
|
raise RuntimeError("WRONG_EVIDENCE_CLOSURE_DENIED")
|
|
_verify_approved_entities(bindings, evidence_git_dir)
|
|
|
|
db_path = Path(str(bindings["approval_db_path"]))
|
|
if not db_path.is_absolute() or not db_path.is_file() or db_path.is_symlink():
|
|
raise RuntimeError("APPROVAL_DATABASE_INVALID")
|
|
expiry = _parse_utc(bindings["expires_at"])
|
|
if require_unexpired:
|
|
_require_approval_unexpired(bindings)
|
|
with sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) as connection:
|
|
connection.row_factory = sqlite3.Row
|
|
pr = connection.execute(
|
|
"SELECT pr.issue_id,pr.has_merged,pr.merged_commit_id,pr.merged_unix,i.is_closed "
|
|
"FROM pull_request pr JOIN issue i ON i.id=pr.issue_id "
|
|
"WHERE i.repo_id=? AND pr.\"index\"=? AND pr.base_branch=?",
|
|
(int(bindings["repo_id"]), int(bindings["activation_approval_pr_number"]), str(bindings["branch"])),
|
|
).fetchone()
|
|
if pr is None or int(pr["has_merged"]) != 1 or int(pr["is_closed"]) != 1:
|
|
raise RuntimeError("ACTIVATION_APPROVAL_PR_NOT_MERGED")
|
|
if pr["merged_commit_id"] != bindings["activation_approval_merge_oid"]:
|
|
raise RuntimeError("ACTIVATION_APPROVAL_MERGE_MISMATCH")
|
|
if int(pr["merged_unix"] or 0) <= 0 or datetime.fromtimestamp(int(pr["merged_unix"]), timezone.utc) > expiry:
|
|
raise RuntimeError("ACTIVATION_APPROVAL_MERGED_AFTER_EXPIRY")
|
|
review = connection.execute(
|
|
"SELECT r.id,r.type,r.reviewer_id,r.commit_id,r.stale,r.dismissed,r.created_unix,u.lower_name "
|
|
"FROM review r JOIN user u ON u.id=r.reviewer_id WHERE r.id=? AND r.issue_id=?",
|
|
(int(bindings["owner_review_oid"]), int(pr["issue_id"])),
|
|
).fetchone()
|
|
if review is None or int(review["id"]) != int(bindings["observed_owner_review_oid"]):
|
|
raise RuntimeError("WRONG_APPROVAL_OID_DENIED")
|
|
if (
|
|
int(review["type"]) != 1
|
|
or int(review["reviewer_id"]) != 2
|
|
or review["lower_name"] != "owner"
|
|
or review["commit_id"] != bindings["approval_request_head_oid"]
|
|
or int(review["stale"]) != 0
|
|
or int(review["dismissed"]) != 0
|
|
or datetime.fromtimestamp(int(review["created_unix"]), timezone.utc) >= expiry
|
|
):
|
|
raise RuntimeError("OWNER_APPROVAL_INVALID")
|
|
statuses = connection.execute(
|
|
"SELECT cs.id,cs.context,cs.state,cs.sha,cs.creator_id,u.lower_name AS creator_login "
|
|
"FROM commit_status cs LEFT JOIN user u ON u.id=cs.creator_id "
|
|
"WHERE cs.repo_id=? AND cs.sha=? ORDER BY cs.id DESC",
|
|
(int(bindings["repo_id"]), str(bindings["approval_request_head_oid"])),
|
|
).fetchall()
|
|
observed: dict[str, sqlite3.Row] = {}
|
|
for row in statuses:
|
|
if row["context"] in REQUIRED_MACHINE_CONTEXTS:
|
|
observed.setdefault(row["context"], row)
|
|
if set(observed) != REQUIRED_MACHINE_CONTEXTS:
|
|
raise RuntimeError("MACHINE_GATE_SET_MISMATCH")
|
|
for context, row in observed.items():
|
|
if row["state"] != "success" or row["sha"] != bindings["approval_request_head_oid"]:
|
|
raise RuntimeError("MACHINE_GATE_FAILED")
|
|
if (
|
|
int(row["creator_id"] or 0) != int(bindings["machine_status_producer_id"])
|
|
or row["creator_login"] != bindings["machine_status_producer_name"]
|
|
):
|
|
raise RuntimeError("MACHINE_GATE_PRODUCER_MISMATCH")
|
|
expected = bindings["machine_checks"][context]
|
|
actual = {
|
|
"state": row["state"], "head_oid": row["sha"], "status_id": int(row["id"]),
|
|
"creator_id": int(row["creator_id"]), "creator_login": row["creator_login"],
|
|
}
|
|
if expected != actual:
|
|
raise RuntimeError("MACHINE_GATE_BINDING_MISMATCH")
|
|
|
|
|
|
def validate_bindings(
|
|
bindings: Mapping[str, Any], *, require_unexpired: bool = True
|
|
) -> None:
|
|
missing = sorted(REQUIRED_BINDINGS - set(bindings))
|
|
if missing:
|
|
raise RuntimeError("MISSING_BINDINGS:" + ",".join(missing))
|
|
for name in (
|
|
"expected_governance_oid",
|
|
"activation_approval_merge_oid",
|
|
"evidence_closure_oid",
|
|
"implementation_commit_oid",
|
|
"implementation_tree_oid",
|
|
):
|
|
_require_hex(name, bindings[name], (40,))
|
|
for name in (
|
|
"expected_validator_sha256",
|
|
"expected_state_machine_sha256",
|
|
"expected_branch_protection_sha256",
|
|
"observed_validator_sha256",
|
|
"observed_state_machine_sha256",
|
|
"expected_gitea_schema_sha256",
|
|
"activation_manifest_sha256",
|
|
"artifact_sha256",
|
|
"rollback_artifact_sha256",
|
|
"test_evidence_sha256",
|
|
"evidence_closure_sha256",
|
|
):
|
|
_require_hex(name, bindings[name], (64,))
|
|
_require_hex("activation_manifest_blob_oid", bindings["activation_manifest_blob_oid"], (40, 64))
|
|
if not isinstance(bindings["owner_review_oid"], (int, str)) or str(bindings["owner_review_oid"]).strip() == "":
|
|
raise RuntimeError("OWNER_REVIEW_OID_INVALID")
|
|
if bindings["governance_ref"] != "refs/heads/governance/approvals":
|
|
raise RuntimeError("GOVERNANCE_REF_INVALID")
|
|
if bindings["branch"] != "governance/approvals":
|
|
raise RuntimeError("BRANCH_BINDING_DENIED")
|
|
if bindings["backend"] not in {"file", "sqlite"}:
|
|
raise RuntimeError("UNKNOWN_PROTECTION_BACKEND")
|
|
if bindings["observed_governance_oid"] != bindings["expected_governance_oid"]:
|
|
raise RuntimeError("WRONG_GOVERNANCE_OID_DENIED")
|
|
if bindings["observed_evidence_closure_oid"] != bindings["evidence_closure_oid"]:
|
|
raise RuntimeError("WRONG_EVIDENCE_CLOSURE_DENIED")
|
|
if str(bindings["observed_owner_review_oid"]) != str(bindings["owner_review_oid"]):
|
|
raise RuntimeError("WRONG_APPROVAL_OID_DENIED")
|
|
if bindings.get("requester") == "owner":
|
|
raise RuntimeError("REQUESTER_SELF_APPROVAL_DENIED")
|
|
if bindings.get("executor") == "owner":
|
|
raise RuntimeError("EXECUTOR_SELF_APPROVAL_DENIED")
|
|
if bindings.get("wip") is not False:
|
|
raise RuntimeError("WIP_BLOCKED")
|
|
checks = bindings.get("machine_checks")
|
|
if not isinstance(checks, dict) or set(checks) != REQUIRED_MACHINE_CONTEXTS:
|
|
raise RuntimeError("MACHINE_GATE_SET_MISMATCH")
|
|
if not isinstance(bindings["machine_status_producer_id"], int) or bindings["machine_status_producer_id"] < 1:
|
|
raise RuntimeError("MACHINE_STATUS_PRODUCER_INVALID")
|
|
if not isinstance(bindings["machine_status_producer_name"], str) or not bindings["machine_status_producer_name"]:
|
|
raise RuntimeError("MACHINE_STATUS_PRODUCER_INVALID")
|
|
for context, check in checks.items():
|
|
if (
|
|
not isinstance(check, dict)
|
|
or set(check) != {"state", "head_oid", "status_id", "creator_id", "creator_login"}
|
|
or check.get("state") != "success"
|
|
or check.get("head_oid") != bindings["approval_request_head_oid"]
|
|
or not isinstance(check.get("status_id"), int)
|
|
or check.get("status_id") < 1
|
|
or check.get("creator_id") != bindings["machine_status_producer_id"]
|
|
or check.get("creator_login") != bindings["machine_status_producer_name"]
|
|
):
|
|
raise RuntimeError(f"MACHINE_GATE_FAILED:{context}")
|
|
forbidden = bindings.get("forbidden_operations", {})
|
|
if any(bool(value) for value in forbidden.values()):
|
|
raise RuntimeError("FORBIDDEN_OPERATION_REQUESTED")
|
|
if os.environ.get("AIOS_GITOPS_WRITES_ENABLED", "false").lower() != "false":
|
|
raise RuntimeError("GLOBAL_GITOPS_WRITES_MUST_REMAIN_FALSE")
|
|
if not isinstance(bindings.get("target_branch_protection"), dict):
|
|
raise RuntimeError("TARGET_BRANCH_PROTECTION_REQUIRED")
|
|
_verify_external_evidence(bindings, require_unexpired=require_unexpired)
|
|
|
|
|
|
def binding_payload_hash(bindings: Mapping[str, Any]) -> str:
|
|
stable = dict(bindings)
|
|
stable.pop("crash_at", None)
|
|
return sha256_json(redact(stable))
|
|
|
|
|
|
def plan_id(bindings: Mapping[str, Any], previous: Mapping[str, Any], target: Mapping[str, Any]) -> str:
|
|
stable = dict(bindings)
|
|
stable.pop("crash_at", None)
|
|
return sha256_json(
|
|
{
|
|
"bindings": redact(stable),
|
|
"previous": dict(previous),
|
|
"target": dict(target),
|
|
}
|
|
)
|
|
|
|
|
|
class ActivationEngine:
|
|
def __init__(self, adapter: Adapter, state_root: Path, lock_path: Path) -> None:
|
|
self.adapter = adapter
|
|
self.journal = JournalStore(state_root)
|
|
self.lock_path = lock_path
|
|
|
|
def plan(self, bindings: Mapping[str, Any]) -> dict[str, Any]:
|
|
validate_bindings(bindings)
|
|
previous = self.adapter.read_current()
|
|
previous_sha = calculate_snapshot_sha256(previous)
|
|
if previous_sha != bindings["expected_branch_protection_sha256"]:
|
|
raise CASConflict("EXPECTED_BRANCH_PROTECTION_SHA256_MISMATCH")
|
|
target = dict(bindings["target_branch_protection"])
|
|
# Validate without writing by using the public target validator through a
|
|
# disposable comparison: adapters enforce the same checks again in CAS.
|
|
from .gitea_protected_branch import validate_target
|
|
|
|
validate_target(previous, target)
|
|
result = {
|
|
"status": "PLAN_READY",
|
|
"task_id": bindings["task_id"],
|
|
"operation_id": bindings["operation_id"],
|
|
"plan_id": plan_id(bindings, previous, target),
|
|
"payload_hash": binding_payload_hash(bindings),
|
|
"previous_sha256": previous_sha,
|
|
"target_sha256": calculate_snapshot_sha256(target),
|
|
}
|
|
return result
|
|
|
|
def _recover(self, record: dict[str, Any]) -> dict[str, Any]:
|
|
current = self.adapter.read_current()
|
|
current_sha = calculate_snapshot_sha256(current)
|
|
previous_sha = record["previous_sha256"]
|
|
target_sha = record["target_sha256"]
|
|
if record["phase"] == "PREPARED":
|
|
if current_sha == previous_sha:
|
|
return {**record, "recovery": "RESUME_BEFORE_APPLY"}
|
|
if current_sha == target_sha:
|
|
record = {**record, "phase": "APPLIED", "recovery": "RESUME_AFTER_APPLY"}
|
|
self.journal.write(record["operation_id"], record)
|
|
return record
|
|
raise RuntimeError("MANUAL_INTERVENTION_REQUIRED")
|
|
if record["phase"] in {"APPLIED", "COMMITTED"} and current_sha not in {target_sha, previous_sha}:
|
|
raise RuntimeError("MANUAL_INTERVENTION_REQUIRED")
|
|
return record
|
|
|
|
def _ensure_closure(self, record: dict[str, Any], event: str) -> None:
|
|
expected_sha = (
|
|
record["target_sha256"] if record["phase"] == "COMMITTED" else record["previous_sha256"]
|
|
)
|
|
if calculate_snapshot_sha256(self.adapter.read_current()) != expected_sha:
|
|
raise RuntimeError("MANUAL_INTERVENTION_REQUIRED")
|
|
atomic_write_json(
|
|
self.journal.root / "current-snapshot.json",
|
|
record["target"] if record["phase"] == "COMMITTED" else record["previous"],
|
|
)
|
|
atomic_write_json(self.journal.root / "rollback-snapshot.json", record["previous"])
|
|
self.journal.write_receipt(record["operation_id"], record)
|
|
if not self.journal.audit.has_event(record["operation_id"], event):
|
|
payload = {"event": event}
|
|
if event == "SINGLE_OWNER_ACTIVATION_COMMITTED":
|
|
payload.update(
|
|
payload_hash=record["payload_hash"],
|
|
target_sha256=record["target_sha256"],
|
|
)
|
|
self.journal.audit.append(record["operation_id"], payload)
|
|
atomic_write_json(self.journal.root / "state.json", record)
|
|
|
|
def activate(self, bindings: Mapping[str, Any], supplied_plan_id: str) -> dict[str, Any]:
|
|
with exclusive_lock(self.lock_path):
|
|
self.journal.audit.verify()
|
|
operation_id = str(bindings["operation_id"])
|
|
payload_hash = binding_payload_hash(bindings)
|
|
record = self.journal.load(operation_id)
|
|
if record:
|
|
if record.get("payload_hash") != payload_hash:
|
|
raise RuntimeError("OPERATION_ID_PAYLOAD_MISMATCH_DENIED")
|
|
if record.get("plan_id") != supplied_plan_id:
|
|
raise RuntimeError("PLAN_ID_MISMATCH")
|
|
if record.get("phase") == "ROLLED_BACK":
|
|
raise RuntimeError("OPERATION_TERMINAL_ROLLED_BACK")
|
|
if record.get("phase") == "FAILED":
|
|
raise RuntimeError("OPERATION_TERMINAL_FAILED")
|
|
prepared_at = _parse_utc(record.get("prepared_at"))
|
|
if prepared_at >= _parse_utc(bindings["expires_at"]):
|
|
raise RuntimeError("RECOVERY_AUTHORIZATION_INVALID")
|
|
record = self._recover(record)
|
|
if record["phase"] == "COMMITTED":
|
|
self._ensure_closure(record, "SINGLE_OWNER_ACTIVATION_COMMITTED")
|
|
return {"status": "ALREADY_COMMITTED", **record}
|
|
else:
|
|
validate_bindings(bindings)
|
|
planned = self.plan(bindings)
|
|
if supplied_plan_id != planned["plan_id"]:
|
|
raise RuntimeError("PLAN_ID_MISMATCH")
|
|
previous = self.adapter.read_current()
|
|
target = dict(bindings["target_branch_protection"])
|
|
if record is None:
|
|
if calculate_snapshot_sha256(previous) != planned["previous_sha256"]:
|
|
raise CASConflict("PREPARED_BOUNDARY_SNAPSHOT_CHANGED")
|
|
prepared_at = _utc_now()
|
|
if prepared_at >= _parse_utc(bindings["expires_at"]):
|
|
raise RuntimeError("APPROVAL_EXPIRED")
|
|
record = {
|
|
"task_id": bindings["task_id"],
|
|
"operation_id": operation_id,
|
|
"transaction_id": operation_id,
|
|
"payload_hash": payload_hash,
|
|
"plan_id": supplied_plan_id,
|
|
"phase": "PREPARED",
|
|
"prepared_at": prepared_at.isoformat().replace("+00:00", "Z"),
|
|
"previous": previous,
|
|
"target": target,
|
|
"previous_sha256": calculate_snapshot_sha256(previous),
|
|
"target_sha256": calculate_snapshot_sha256(target),
|
|
"activation_approval_merge_oid": bindings["activation_approval_merge_oid"],
|
|
"evidence_closure_oid": bindings["evidence_closure_oid"],
|
|
"owner_review_oid": bindings["owner_review_oid"],
|
|
}
|
|
self.journal.write(operation_id, record)
|
|
atomic_write_json(self.journal.root / "current-snapshot.json", previous)
|
|
atomic_write_json(self.journal.root / "rollback-snapshot.json", previous)
|
|
crash_at = bindings.get("crash_at")
|
|
if crash_at == "before_apply":
|
|
raise InjectedCrash("before_apply")
|
|
try:
|
|
current_sha = calculate_snapshot_sha256(self.adapter.read_current())
|
|
if current_sha == record["previous_sha256"]:
|
|
applied = self.adapter.apply_atomic(record["previous_sha256"], record["target"])
|
|
record["target"] = applied
|
|
record["target_sha256"] = calculate_snapshot_sha256(applied)
|
|
elif current_sha != record["target_sha256"]:
|
|
raise CASConflict("CAS_CONFLICT")
|
|
if crash_at == "during_apply":
|
|
raise InjectedCrash("during_apply")
|
|
record["phase"] = "APPLIED"
|
|
self.journal.write(operation_id, record)
|
|
if crash_at == "after_apply_before_receipt":
|
|
raise InjectedCrash("after_apply_before_receipt")
|
|
if not self.adapter.verify_applied(record["target_sha256"]):
|
|
raise RuntimeError("POST_ACTIVATION_VERIFY_FAILED")
|
|
record["phase"] = "COMMITTED"
|
|
record["single_owner_policy_active"] = True
|
|
record["break_glass_active"] = False
|
|
self.journal.write(operation_id, record)
|
|
if crash_at == "after_commit_wal":
|
|
raise InjectedCrash("after_commit_wal")
|
|
self._ensure_closure(record, "SINGLE_OWNER_ACTIVATION_COMMITTED")
|
|
return {"status": "ACTIVATED", **record}
|
|
except InjectedCrash:
|
|
raise
|
|
except Exception as exc:
|
|
failure = {**record, "phase": "FAILED", "error": type(exc).__name__}
|
|
current_sha = calculate_snapshot_sha256(self.adapter.read_current())
|
|
if current_sha == record["target_sha256"]:
|
|
self.adapter.rollback_atomic(record["target_sha256"], record["previous"])
|
|
if not self.adapter.verify_rollback(record["previous_sha256"]):
|
|
raise RuntimeError("ROLLBACK_VERIFY_FAILED") from exc
|
|
failure["phase"] = "ROLLED_BACK"
|
|
self.journal.write(operation_id, failure)
|
|
self.journal.write_receipt(operation_id, failure)
|
|
self.journal.audit.append(operation_id, {"event": failure["phase"], "error": type(exc).__name__})
|
|
raise
|
|
|
|
def verify(self, bindings: Mapping[str, Any], operation_id: str) -> dict[str, Any]:
|
|
record = self.journal.load(operation_id)
|
|
if not record:
|
|
raise RuntimeError("TRANSACTION_NOT_FOUND")
|
|
if record.get("payload_hash") != binding_payload_hash(bindings):
|
|
raise RuntimeError("OPERATION_ID_PAYLOAD_MISMATCH_DENIED")
|
|
self.journal.audit.verify()
|
|
current_sha = calculate_snapshot_sha256(self.adapter.read_current())
|
|
passed = record.get("phase") == "COMMITTED" and current_sha == record.get("target_sha256")
|
|
if not passed:
|
|
raise RuntimeError("ACTIVATION_VERIFY_FAILED")
|
|
return {"status": "VERIFIED", "operation_id": operation_id, "target_sha256": current_sha}
|
|
|
|
def rollback(
|
|
self, bindings: Mapping[str, Any], operation_id: str, supplied_plan_id: str
|
|
) -> dict[str, Any]:
|
|
with exclusive_lock(self.lock_path):
|
|
self.journal.audit.verify()
|
|
record = self.journal.load(operation_id)
|
|
if not record:
|
|
raise RuntimeError("TRANSACTION_NOT_FOUND")
|
|
if record.get("payload_hash") != binding_payload_hash(bindings):
|
|
raise RuntimeError("OPERATION_ID_PAYLOAD_MISMATCH_DENIED")
|
|
if record.get("plan_id") != supplied_plan_id:
|
|
raise RuntimeError("PLAN_ID_MISMATCH")
|
|
for field in (
|
|
"activation_approval_merge_oid",
|
|
"evidence_closure_oid",
|
|
"owner_review_oid",
|
|
):
|
|
if str(record.get(field)) != str(bindings[field]):
|
|
raise RuntimeError("ROLLBACK_AUTHORIZATION_MISMATCH_DENIED")
|
|
current_sha = calculate_snapshot_sha256(self.adapter.read_current())
|
|
if current_sha == record["previous_sha256"]:
|
|
record["phase"] = "ROLLED_BACK"
|
|
record["single_owner_policy_active"] = False
|
|
self.journal.write(operation_id, record)
|
|
self._ensure_closure(record, "ACTIVATION_ROLLED_BACK")
|
|
return {"status": "ALREADY_ROLLED_BACK", "operation_id": operation_id}
|
|
if current_sha != record["target_sha256"]:
|
|
raise RuntimeError("MANUAL_INTERVENTION_REQUIRED")
|
|
self.adapter.rollback_atomic(record["target_sha256"], record["previous"])
|
|
if not self.adapter.verify_rollback(record["previous_sha256"]):
|
|
raise RuntimeError("ROLLBACK_VERIFY_FAILED")
|
|
if bindings.get("crash_at") == "after_rollback_cas":
|
|
raise InjectedCrash("after_rollback_cas")
|
|
record["phase"] = "ROLLED_BACK"
|
|
record["single_owner_policy_active"] = False
|
|
self.journal.write(operation_id, record)
|
|
if bindings.get("crash_at") == "after_rollback_wal":
|
|
raise InjectedCrash("after_rollback_wal")
|
|
self._ensure_closure(record, "ACTIVATION_ROLLED_BACK")
|
|
return {"status": "ROLLED_BACK", "operation_id": operation_id}
|
|
|
|
def status(self, operation_id: str | None = None) -> dict[str, Any]:
|
|
audit_head = self.journal.audit.verify()
|
|
if operation_id:
|
|
record = self.journal.load(operation_id)
|
|
if not record:
|
|
raise RuntimeError("TRANSACTION_NOT_FOUND")
|
|
return {"status": "OK", "audit_head": audit_head, "transaction": record}
|
|
state_path = self.journal.root / "state.json"
|
|
return {
|
|
"status": "OK",
|
|
"audit_head": audit_head,
|
|
"state": read_json(state_path) if state_path.exists() else None,
|
|
"transactions": list(self.journal.iter_transactions()),
|
|
}
|
|
|
|
|
|
def load_bindings(path: Path) -> dict[str, Any]:
|
|
return read_json(path)
|
|
|
|
|
|
def adapter_from_bindings(bindings: Mapping[str, Any]) -> Adapter:
|
|
backend = bindings.get("backend", "file")
|
|
backend_path = Path(str(bindings.get("backend_path", "")))
|
|
if not backend_path.is_absolute():
|
|
raise RuntimeError("BACKEND_PATH_MUST_BE_ABSOLUTE")
|
|
if backend == "file":
|
|
return FileProtectedBranchAdapter(backend_path)
|
|
if backend == "sqlite":
|
|
return SQLiteProtectedBranchAdapter(
|
|
backend_path,
|
|
repo_id=int(bindings.get("repo_id", 1)),
|
|
branch=str(bindings.get("branch", "governance/approvals")),
|
|
expected_schema_sha256=str(bindings["expected_gitea_schema_sha256"]),
|
|
)
|
|
raise RuntimeError("UNKNOWN_PROTECTION_BACKEND")
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser()
|
|
action = parser.add_mutually_exclusive_group(required=True)
|
|
for name in ("plan", "activate", "verify", "rollback", "status"):
|
|
action.add_argument(f"--{name}", action="store_true")
|
|
parser.add_argument("--bindings", type=Path)
|
|
parser.add_argument("--plan-id")
|
|
parser.add_argument("--transaction")
|
|
parser.add_argument("--state-root", type=Path, default=DEFAULT_STATE_ROOT)
|
|
parser.add_argument("--lock-path", type=Path, default=DEFAULT_LOCK_PATH)
|
|
return parser
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = build_parser().parse_args(argv)
|
|
if args.status and args.bindings is None:
|
|
# Status without bindings is intentionally limited to durable local state.
|
|
journal = JournalStore(args.state_root)
|
|
result = {
|
|
"status": "OK",
|
|
"audit_head": journal.audit.verify(),
|
|
"transactions": list(journal.iter_transactions()),
|
|
}
|
|
print(json.dumps(result, sort_keys=True))
|
|
return 0
|
|
if args.bindings is None:
|
|
raise SystemExit("--bindings is required")
|
|
bindings = load_bindings(args.bindings)
|
|
engine = ActivationEngine(adapter_from_bindings(bindings), args.state_root, args.lock_path)
|
|
if args.plan:
|
|
result = engine.plan(bindings)
|
|
elif args.activate:
|
|
if not args.plan_id:
|
|
raise SystemExit("--plan-id is required")
|
|
result = engine.activate(bindings, args.plan_id)
|
|
elif args.verify:
|
|
result = engine.verify(bindings, args.transaction or str(bindings["operation_id"]))
|
|
elif args.rollback:
|
|
if not args.plan_id:
|
|
raise SystemExit("--plan-id is required")
|
|
result = engine.rollback(
|
|
bindings, args.transaction or str(bindings["operation_id"]), args.plan_id
|
|
)
|
|
else:
|
|
result = engine.status(args.transaction)
|
|
print(json.dumps(redact(result), sort_keys=True))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|