Compare commits
2
Commits
e1e438564c
...
310ab2af90
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
310ab2af90 | ||
|
|
71def340e7 |
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"artifact_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/live-activation-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.tar.gz",
|
||||
"artifact_sha256": "32bc49c207ec454b6fd81b0484f5b257bb789481b37175995029a2ec78cc62fa",
|
||||
"copied_live_db_acceptance": "PASS",
|
||||
"implementation_commit_oid": "71def340e7cc34f4f19918dcf8d3c847abbe4fbf",
|
||||
"implementation_tree_oid": "3cc61bfbf69cbe0a24d54a0e53a6c50bcc130066",
|
||||
"independent_validator": "ALLOW_CANDIDATE",
|
||||
"production_mutation": false,
|
||||
"result": "PASS",
|
||||
"rollback_artifact_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/live-activation-rollback-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.tar.gz",
|
||||
"rollback_artifact_sha256": "4efe1ea73d4fd72e5364fc91a16ad5e9d1e6ebe78d8aa466f7b11c4915b08403",
|
||||
"runtime_source_sha256": {
|
||||
"services/aios-single-owner-governance/aios_single_owner_governance/activation.py": "48846a58fa3afb2b7c8ffe6e88504a9333863932df867f33ba0321ad3bac6003",
|
||||
"services/aios-single-owner-governance/aios_single_owner_governance/gitea_protected_branch.py": "00775efe3f3d631d88379eff4ed2dcfaf386a2fa43111abe961e75109e80d768",
|
||||
"services/aios-single-owner-governance/aios_single_owner_governance/journal.py": "a8f121245e04487d99c99fb03ada83f0c907723f8eb18552872ff46dcf5a7873",
|
||||
"services/aios-single-owner-governance/aios_single_owner_governance/state_machine.py": "0b08a2c378d143cb9031a25132afc7b7d825bdc03d11efe10610341d4b70167e",
|
||||
"services/aios-single-owner-governance/aios_single_owner_governance/validator.py": "754639846cb15cb997d612a1fe0ceb0224ddef5bbc3ccac9802a3747f122dac1"
|
||||
},
|
||||
"subtests_passed": 17,
|
||||
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729",
|
||||
"test_evidence_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/test-evidence-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.json",
|
||||
"test_evidence_sha256": "c1633732767f3f051ffb01404d12d05a6168a4b01b91efdb34a64988d6dee684",
|
||||
"tests_failed": 0,
|
||||
"tests_passed": 90
|
||||
}
|
||||
@@ -17,6 +17,33 @@ a fsynced WAL before the single-row SQLite CAS, verifies the full applied row,
|
||||
and only rolls back with a target-to-previous CAS. Audit records are
|
||||
hash-chained and credentials are redacted.
|
||||
|
||||
## Hardening Contract
|
||||
|
||||
Fresh activation is authorized only before the approved `expires_at` instant.
|
||||
An already-recorded `PREPARED` transaction may recover after expiry only to
|
||||
reach a safe terminal state using the same exact bindings; `--verify` and
|
||||
`--rollback` remain available for that recorded transaction after expiry.
|
||||
`ROLLED_BACK` and `FAILED` are terminal states and must never be replayed as a
|
||||
new activation.
|
||||
|
||||
The approved execution binding must pin the implementation commit and its
|
||||
exact tree, the installed hashes of the five runtime source files
|
||||
(`activation.py`, `gitea_protected_branch.py`, `journal.py`, `validator.py`,
|
||||
and `state_machine.py`), and independent SHA-256 values for the deployment
|
||||
artifact, rollback artifact, and test evidence. It must also bind an evidence
|
||||
closure that is a commit with the exact direct parent and exactly one approved
|
||||
closure file, including that file's Git blob OID and SHA-256.
|
||||
|
||||
Every required machine status is accepted only when its exact status record,
|
||||
creator identity, producer provenance, target URL, description digest, head
|
||||
OID, state, and observation time match the approved binding. A fixture status
|
||||
or a dormant self-check is not machine-status producer provenance.
|
||||
|
||||
This contract hardens validation; it does not authorize deployment. The
|
||||
existing governed approval rules still apply, and installation remains
|
||||
disabled and inactive until the required merged approval authorizes one
|
||||
explicit execution.
|
||||
|
||||
Installation must remain disabled and inactive until a merged
|
||||
`SINGLE_OWNER_GOVERNANCE_LIVE_ACTIVATION_IMPLEMENTATION_AND_EXECUTION`
|
||||
approval authorizes one explicit execution. The approved runtime paths are
|
||||
|
||||
@@ -7,12 +7,16 @@
|
||||
"activation_manifest_blob_oid": {"pattern": "^[0-9a-f]{40,64}$", "type": "string"},
|
||||
"activation_manifest_path": {"pattern": "^approvals/.+\\.json$", "type": "string"},
|
||||
"activation_manifest_sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"},
|
||||
"artifact_path": {"pattern": "^/", "type": "string"},
|
||||
"artifact_sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"},
|
||||
"approval_db_path": {"minLength": 1, "type": "string"},
|
||||
"approval_request_head_oid": {"pattern": "^[0-9a-f]{40}$", "type": "string"},
|
||||
"backend": {"enum": ["file", "sqlite"]},
|
||||
"backend_path": {"minLength": 1, "type": "string"},
|
||||
"branch": {"const": "governance/approvals"},
|
||||
"evidence_closure_oid": {"pattern": "^[0-9a-f]{40}$", "type": "string"},
|
||||
"evidence_closure_path": {"pattern": "^(?!/)(?!.*\\.\\.).+", "type": "string"},
|
||||
"evidence_closure_sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"},
|
||||
"evidence_git_dir": {"minLength": 1, "type": "string"},
|
||||
"executor": {"not": {"const": "owner"}, "type": "string"},
|
||||
"expected_branch_protection_sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"},
|
||||
@@ -24,6 +28,10 @@
|
||||
"forbidden_operations": {"additionalProperties": {"const": false}, "type": "object"},
|
||||
"governance_git_dir": {"minLength": 1, "type": "string"},
|
||||
"governance_ref": {"const": "refs/heads/governance/approvals"},
|
||||
"implementation_commit_oid": {"pattern": "^[0-9a-f]{40}$", "type": "string"},
|
||||
"implementation_tree_oid": {"pattern": "^[0-9a-f]{40}$", "type": "string"},
|
||||
"machine_status_producer_id": {"minimum": 1, "type": "integer"},
|
||||
"machine_status_producer_name": {"minLength": 1, "type": "string"},
|
||||
"machine_checks": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -45,9 +53,28 @@
|
||||
"owner_review_oid": {"type": ["integer", "string"]},
|
||||
"repo_id": {"minimum": 1, "type": "integer"},
|
||||
"requester": {"not": {"const": "owner"}, "type": "string"},
|
||||
"rollback_artifact_path": {"pattern": "^/", "type": "string"},
|
||||
"rollback_artifact_sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"},
|
||||
"runtime_sources": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"git_path": {"minLength": 1, "type": "string"},
|
||||
"runtime_path": {"pattern": "^/", "type": "string"},
|
||||
"sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"}
|
||||
},
|
||||
"required": ["git_path", "runtime_path", "sha256"],
|
||||
"type": "object"
|
||||
},
|
||||
"maxItems": 5,
|
||||
"minItems": 5,
|
||||
"type": "array"
|
||||
},
|
||||
"state_machine_path": {"minLength": 1, "type": "string"},
|
||||
"target_branch_protection": {"type": "object"},
|
||||
"task_id": {"minLength": 1, "type": "string"},
|
||||
"test_evidence_path": {"pattern": "^/", "type": "string"},
|
||||
"test_evidence_sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"},
|
||||
"validator_path": {"minLength": 1, "type": "string"},
|
||||
"wip": {"const": false}
|
||||
},
|
||||
@@ -56,9 +83,12 @@
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"head_oid": {"pattern": "^[0-9a-f]{40}$", "type": "string"},
|
||||
"status_id": {"minimum": 1, "type": "integer"},
|
||||
"creator_id": {"minimum": 1, "type": "integer"},
|
||||
"creator_login": {"minLength": 1, "type": "string"},
|
||||
"state": {"const": "success"}
|
||||
},
|
||||
"required": ["state", "head_oid"],
|
||||
"required": ["state", "head_oid", "status_id", "creator_id", "creator_login"],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
@@ -70,6 +100,11 @@
|
||||
"activation_approval_merge_oid", "activation_approval_pr_number", "approval_request_head_oid",
|
||||
"activation_manifest_path", "activation_manifest_blob_oid", "activation_manifest_sha256",
|
||||
"evidence_closure_oid", "observed_evidence_closure_oid", "evidence_git_dir",
|
||||
"evidence_closure_path", "evidence_closure_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",
|
||||
"machine_status_producer_id", "machine_status_producer_name",
|
||||
"owner_review_oid", "observed_owner_review_oid", "approval_db_path", "expires_at",
|
||||
"governance_git_dir", "governance_ref", "machine_checks", "forbidden_operations",
|
||||
"target_branch_protection", "backend", "backend_path", "repo_id", "branch",
|
||||
|
||||
@@ -24,6 +24,8 @@ from .gitea_protected_branch import (
|
||||
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")
|
||||
@@ -64,6 +66,19 @@ REQUIRED_BINDINGS = {
|
||||
"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",
|
||||
@@ -72,6 +87,13 @@ REQUIRED_MACHINE_CONTEXTS = {
|
||||
"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):
|
||||
@@ -134,6 +156,17 @@ def _git(git_dir: Path, *args: str) -> str:
|
||||
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")
|
||||
@@ -146,6 +179,10 @@ def _parse_utc(value: object) -> datetime:
|
||||
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"],
|
||||
@@ -158,6 +195,19 @@ def _approved_execution_binding(bindings: Mapping[str, Any]) -> dict[str, Any]:
|
||||
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"],
|
||||
@@ -227,7 +277,73 @@ def _load_approved_manifest(bindings: Mapping[str, Any], git_dir: Path) -> dict[
|
||||
return manifest
|
||||
|
||||
|
||||
def _verify_external_evidence(bindings: Mapping[str, Any]) -> None:
|
||||
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"])))
|
||||
@@ -258,22 +374,32 @@ def _verify_external_evidence(bindings: Mapping[str, Any]) -> None:
|
||||
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")
|
||||
_git(evidence_git_dir, "cat-file", "-e", f"{evidence_oid}^{{object}}")
|
||||
_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 datetime.now(timezone.utc) >= expiry:
|
||||
raise RuntimeError("APPROVAL_EXPIRED")
|
||||
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,i.is_closed "
|
||||
"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"])),
|
||||
@@ -282,6 +408,8 @@ def _verify_external_evidence(bindings: Mapping[str, Any]) -> None:
|
||||
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=?",
|
||||
@@ -300,8 +428,9 @@ def _verify_external_evidence(bindings: Mapping[str, Any]) -> None:
|
||||
):
|
||||
raise RuntimeError("OWNER_APPROVAL_INVALID")
|
||||
statuses = connection.execute(
|
||||
"SELECT id,context,state,sha FROM commit_status "
|
||||
"WHERE repo_id=? AND sha=? ORDER BY id DESC",
|
||||
"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] = {}
|
||||
@@ -310,11 +439,26 @@ def _verify_external_evidence(bindings: Mapping[str, Any]) -> None:
|
||||
observed.setdefault(row["context"], row)
|
||||
if set(observed) != REQUIRED_MACHINE_CONTEXTS:
|
||||
raise RuntimeError("MACHINE_GATE_SET_MISMATCH")
|
||||
if any(row["state"] != "success" or row["sha"] != bindings["approval_request_head_oid"] for row in observed.values()):
|
||||
raise RuntimeError("MACHINE_GATE_FAILED")
|
||||
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]) -> None:
|
||||
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))
|
||||
@@ -322,6 +466,8 @@ def validate_bindings(bindings: Mapping[str, Any]) -> None:
|
||||
"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 (
|
||||
@@ -332,6 +478,10 @@ def validate_bindings(bindings: Mapping[str, Any]) -> None:
|
||||
"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))
|
||||
@@ -353,13 +503,26 @@ def validate_bindings(bindings: Mapping[str, Any]) -> None:
|
||||
raise RuntimeError("REQUESTER_SELF_APPROVAL_DENIED")
|
||||
if bindings.get("executor") == "owner":
|
||||
raise RuntimeError("EXECUTOR_SELF_APPROVAL_DENIED")
|
||||
if bindings.get("wip"):
|
||||
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 check.get("state") != "success" or check.get("head_oid") != bindings["approval_request_head_oid"]:
|
||||
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()):
|
||||
@@ -368,7 +531,7 @@ def validate_bindings(bindings: Mapping[str, Any]) -> None:
|
||||
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)
|
||||
_verify_external_evidence(bindings, require_unexpired=require_unexpired)
|
||||
|
||||
|
||||
def binding_payload_hash(bindings: Mapping[str, Any]) -> str:
|
||||
@@ -460,7 +623,6 @@ class ActivationEngine:
|
||||
def activate(self, bindings: Mapping[str, Any], supplied_plan_id: str) -> dict[str, Any]:
|
||||
with exclusive_lock(self.lock_path):
|
||||
self.journal.audit.verify()
|
||||
validate_bindings(bindings)
|
||||
operation_id = str(bindings["operation_id"])
|
||||
payload_hash = binding_payload_hash(bindings)
|
||||
record = self.journal.load(operation_id)
|
||||
@@ -469,17 +631,30 @@ class ActivationEngine:
|
||||
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,
|
||||
@@ -487,6 +662,7 @@ class ActivationEngine:
|
||||
"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),
|
||||
@@ -541,7 +717,6 @@ class ActivationEngine:
|
||||
raise
|
||||
|
||||
def verify(self, bindings: Mapping[str, Any], operation_id: str) -> dict[str, Any]:
|
||||
validate_bindings(bindings)
|
||||
record = self.journal.load(operation_id)
|
||||
if not record:
|
||||
raise RuntimeError("TRANSACTION_NOT_FOUND")
|
||||
@@ -559,7 +734,6 @@ class ActivationEngine:
|
||||
) -> dict[str, Any]:
|
||||
with exclusive_lock(self.lock_path):
|
||||
self.journal.audit.verify()
|
||||
validate_bindings(bindings)
|
||||
record = self.journal.load(operation_id)
|
||||
if not record:
|
||||
raise RuntimeError("TRANSACTION_NOT_FOUND")
|
||||
|
||||
@@ -9,14 +9,17 @@ import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
from jsonschema import Draft202012Validator, ValidationError
|
||||
|
||||
from aios_single_owner_governance.activation import (
|
||||
ActivationEngine,
|
||||
InjectedCrash,
|
||||
_verify_approved_entities,
|
||||
validate_bindings,
|
||||
)
|
||||
import aios_single_owner_governance.validator as validator_module
|
||||
import aios_single_owner_governance.state_machine as state_machine_module
|
||||
import aios_single_owner_governance.activation as activation_module
|
||||
from aios_single_owner_governance.gitea_protected_branch import (
|
||||
CASConflict,
|
||||
FileProtectedBranchAdapter,
|
||||
@@ -100,18 +103,105 @@ def git_output(git_dir: Path, *args: str, stdin: str | None = None) -> str:
|
||||
).stdout.strip()
|
||||
|
||||
|
||||
def commit_files(
|
||||
git_dir: Path, files: dict[str, str], message: str, *, parent: str | None = None
|
||||
) -> tuple[str, str]:
|
||||
root: dict[str, object] = {}
|
||||
for path, raw in files.items():
|
||||
node = root
|
||||
parts = path.split("/")
|
||||
for directory in parts[:-1]:
|
||||
node = node.setdefault(directory, {}) # type: ignore[assignment]
|
||||
node[parts[-1]] = git_output(git_dir, "hash-object", "-w", "--stdin", stdin=raw)
|
||||
|
||||
def build_tree(node: dict[str, object]) -> str:
|
||||
lines = []
|
||||
for name in sorted(node):
|
||||
value = node[name]
|
||||
if isinstance(value, dict):
|
||||
lines.append(f"040000 tree {build_tree(value)}\t{name}")
|
||||
else:
|
||||
lines.append(f"100644 blob {value}\t{name}")
|
||||
return git_output(git_dir, "mktree", stdin="\n".join(lines) + "\n")
|
||||
|
||||
tree = build_tree(root)
|
||||
args = ["commit-tree", tree]
|
||||
if parent is not None:
|
||||
args.extend(("-p", parent))
|
||||
return git_output(git_dir, *args, stdin=message + "\n"), tree
|
||||
|
||||
|
||||
def evidence_fixture(tmp_path: Path, execution_binding: dict) -> dict:
|
||||
git_dir = tmp_path / "evidence.git"
|
||||
subprocess.run(["git", "init", "--bare", str(git_dir)], capture_output=True, check=True)
|
||||
closure_oid = git_output(git_dir, "hash-object", "-w", "--stdin", stdin="closure\n")
|
||||
package_root = Path(activation_module.__file__).resolve().parent
|
||||
source_files: dict[str, str] = {}
|
||||
runtime_sources = []
|
||||
for name in (
|
||||
"activation.py", "gitea_protected_branch.py", "journal.py",
|
||||
"state_machine.py", "validator.py",
|
||||
):
|
||||
source = package_root / name
|
||||
raw = source.read_text(encoding="utf-8")
|
||||
git_path = (
|
||||
"services/aios-single-owner-governance/"
|
||||
f"aios_single_owner_governance/{name}"
|
||||
)
|
||||
source_files[git_path] = raw
|
||||
runtime_sources.append({
|
||||
"git_path": git_path,
|
||||
"runtime_path": str(source.resolve()),
|
||||
"sha256": hashlib.sha256(source.read_bytes()).hexdigest(),
|
||||
})
|
||||
implementation_oid, implementation_tree = commit_files(
|
||||
git_dir, source_files, "implementation"
|
||||
)
|
||||
closure_path = "evidence/live-activation-closure.json"
|
||||
closure_raw = '{"result":"PASS"}\n'
|
||||
closure_oid, _ = commit_files(
|
||||
git_dir, {**source_files, closure_path: closure_raw},
|
||||
"closure", parent=implementation_oid
|
||||
)
|
||||
closure_sha = hashlib.sha256(closure_raw.encode()).hexdigest()
|
||||
entities = {}
|
||||
for name, raw in {
|
||||
"artifact": b"approved-artifact\n",
|
||||
"rollback_artifact": b"approved-rollback\n",
|
||||
"test_evidence": b"approved-tests\n",
|
||||
}.items():
|
||||
path = tmp_path / "artifacts" / f"{name}.bin"
|
||||
path.parent.mkdir(exist_ok=True)
|
||||
path.write_bytes(raw)
|
||||
entities[f"{name}_path"] = str(path.resolve())
|
||||
entities[f"{name}_sha256"] = hashlib.sha256(raw).hexdigest()
|
||||
head_oid = git_output(git_dir, "hash-object", "-w", "--stdin", stdin="head\n")
|
||||
expiry = datetime.now(timezone.utc) + timedelta(hours=1)
|
||||
execution_binding = {**execution_binding, "evidence_closure_oid": closure_oid}
|
||||
producer_id = 99
|
||||
producer_name = "trusted-machine-producer"
|
||||
execution_binding = {
|
||||
**execution_binding,
|
||||
"evidence_closure_oid": closure_oid,
|
||||
"evidence_closure_path": closure_path,
|
||||
"evidence_closure_sha256": closure_sha,
|
||||
"implementation_commit_oid": implementation_oid,
|
||||
"implementation_tree_oid": implementation_tree,
|
||||
"runtime_sources": runtime_sources,
|
||||
"machine_status_producer_id": producer_id,
|
||||
"machine_status_producer_name": producer_name,
|
||||
**entities,
|
||||
}
|
||||
manifest = {
|
||||
"approval_type": "SINGLE_OWNER_GOVERNANCE_LIVE_ACTIVATION_IMPLEMENTATION_AND_EXECUTION",
|
||||
"expires_at": expiry.isoformat().replace("+00:00", "Z"),
|
||||
"execution_binding": execution_binding,
|
||||
"execution_binding_sha256": sha256_json(execution_binding),
|
||||
**{key: execution_binding[key] for key in (
|
||||
"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",
|
||||
)},
|
||||
}
|
||||
manifest_raw = json.dumps(manifest, sort_keys=True, separators=(",", ":")) + "\n"
|
||||
manifest_blob = git_output(git_dir, "hash-object", "-w", "--stdin", stdin=manifest_raw)
|
||||
@@ -125,20 +215,30 @@ def evidence_fixture(tmp_path: Path, execution_binding: dict) -> dict:
|
||||
with sqlite3.connect(db) as connection:
|
||||
connection.executescript(
|
||||
"CREATE TABLE issue(id INTEGER,repo_id INTEGER,is_closed INTEGER);"
|
||||
"CREATE TABLE pull_request(issue_id INTEGER,\"index\" INTEGER,base_branch TEXT,has_merged INTEGER,merged_commit_id TEXT);"
|
||||
"CREATE TABLE pull_request(issue_id INTEGER,\"index\" INTEGER,base_branch TEXT,has_merged INTEGER,merged_commit_id TEXT,merged_unix INTEGER);"
|
||||
"CREATE TABLE user(id INTEGER,lower_name TEXT);"
|
||||
"CREATE TABLE review(id INTEGER,type INTEGER,reviewer_id INTEGER,issue_id INTEGER,commit_id TEXT,stale INTEGER,dismissed INTEGER,created_unix INTEGER);"
|
||||
"CREATE TABLE commit_status(id INTEGER,repo_id INTEGER,state TEXT,sha TEXT,context TEXT);"
|
||||
"CREATE TABLE commit_status(id INTEGER,repo_id INTEGER,state TEXT,sha TEXT,context TEXT,creator_id INTEGER);"
|
||||
)
|
||||
connection.execute("INSERT INTO issue VALUES(10,1,1)")
|
||||
connection.execute("INSERT INTO pull_request VALUES(10,31,'governance/approvals',1,?)", (merge_oid,))
|
||||
merged_unix = int(datetime.now(timezone.utc).timestamp())
|
||||
connection.execute("INSERT INTO pull_request VALUES(10,31,'governance/approvals',1,?,?)", (merge_oid, merged_unix))
|
||||
connection.execute("INSERT INTO user VALUES(2,'owner')")
|
||||
connection.execute("INSERT INTO user VALUES(?,?)", (producer_id, producer_name))
|
||||
connection.execute(
|
||||
"INSERT INTO review VALUES(52,1,2,10,?,0,0,?)",
|
||||
(head_oid, int(datetime.now(timezone.utc).timestamp())),
|
||||
)
|
||||
for index, context in enumerate(sorted(MACHINE_CONTEXTS), 1):
|
||||
connection.execute("INSERT INTO commit_status VALUES(?,1,'success',?,?)", (index, head_oid, context))
|
||||
machine_checks = {}
|
||||
for index, context in enumerate(sorted(MACHINE_CONTEXTS), 101):
|
||||
connection.execute(
|
||||
"INSERT INTO commit_status VALUES(?,1,'success',?,?,?)",
|
||||
(index, head_oid, context, producer_id),
|
||||
)
|
||||
machine_checks[context] = {
|
||||
"state": "success", "head_oid": head_oid, "status_id": index,
|
||||
"creator_id": producer_id, "creator_login": producer_name,
|
||||
}
|
||||
return {
|
||||
"git_dir": git_dir, "merge_oid": merge_oid, "closure_oid": closure_oid,
|
||||
"head_oid": head_oid, "db": db, "expiry": expiry.isoformat().replace("+00:00", "Z"),
|
||||
@@ -146,6 +246,8 @@ def evidence_fixture(tmp_path: Path, execution_binding: dict) -> dict:
|
||||
"manifest_blob": manifest_blob,
|
||||
"manifest_sha256": hashlib.sha256(manifest_raw.encode()).hexdigest(),
|
||||
"execution_binding": execution_binding,
|
||||
"machine_checks": machine_checks,
|
||||
**execution_binding,
|
||||
}
|
||||
|
||||
|
||||
@@ -198,15 +300,19 @@ def bindings(tmp_path: Path, adapter: FileProtectedBranchAdapter, **overrides) -
|
||||
"expires_at": evidence["expiry"],
|
||||
"evidence_closure_oid": evidence["closure_oid"],
|
||||
"observed_evidence_closure_oid": evidence["closure_oid"],
|
||||
**{key: evidence[key] for key in (
|
||||
"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",
|
||||
)},
|
||||
"owner_review_oid": 52,
|
||||
"observed_owner_review_oid": 52,
|
||||
"requester": "agent",
|
||||
"executor": "activation-executor",
|
||||
"wip": False,
|
||||
"machine_checks": {
|
||||
context: {"state": "success", "head_oid": evidence["head_oid"]}
|
||||
for context in MACHINE_CONTEXTS
|
||||
},
|
||||
"machine_checks": evidence["machine_checks"],
|
||||
"forbidden_operations": {
|
||||
"main": False,
|
||||
"governance_direct": False,
|
||||
@@ -241,6 +347,87 @@ def test_live_plan_success(tmp_path: Path) -> None:
|
||||
assert len(result["plan_id"]) == 64
|
||||
|
||||
|
||||
def test_activation_bindings_schema_matches_runtime_contract(tmp_path: Path) -> None:
|
||||
_, _, _, request = harness(tmp_path)
|
||||
schema_path = Path(activation_module.__file__).resolve().parents[1] / "activation-bindings.schema.json"
|
||||
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
||||
Draft202012Validator.check_schema(schema)
|
||||
validator = Draft202012Validator(schema)
|
||||
validator.validate(request)
|
||||
invalid = json.loads(json.dumps(request))
|
||||
invalid["machine_checks"][sorted(MACHINE_CONTEXTS)[0]]["status_id"] = 0
|
||||
with pytest.raises(ValidationError):
|
||||
validator.validate(invalid)
|
||||
|
||||
|
||||
def test_runtime_source_must_be_actual_loaded_module_not_decoy(tmp_path: Path) -> None:
|
||||
_, _, engine, request = harness(tmp_path)
|
||||
source = Path(request["runtime_sources"][0]["runtime_path"])
|
||||
decoy = tmp_path / "decoy.py"
|
||||
decoy.write_bytes(source.read_bytes())
|
||||
request["runtime_sources"][0]["runtime_path"] = str(decoy.resolve())
|
||||
with pytest.raises(RuntimeError, match="RUNTIME_SOURCE_PATH_MISMATCH"):
|
||||
_verify_approved_entities(request, Path(request["evidence_git_dir"]))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path_field", ["artifact_path", "rollback_artifact_path", "test_evidence_path"])
|
||||
def test_approved_entity_digest_is_enforced(tmp_path: Path, path_field: str) -> None:
|
||||
_, _, engine, request = harness(tmp_path)
|
||||
Path(request[path_field]).write_bytes(b"tampered\n")
|
||||
with pytest.raises(RuntimeError, match="APPROVED_ENTITY_DIGEST_MISMATCH"):
|
||||
engine.plan(request)
|
||||
|
||||
|
||||
def test_evidence_closure_must_be_direct_child_commit(tmp_path: Path) -> None:
|
||||
_, _, engine, request = harness(tmp_path)
|
||||
git_dir = Path(request["evidence_git_dir"])
|
||||
unrelated, _ = commit_files(
|
||||
git_dir,
|
||||
{request["evidence_closure_path"]: '{"result":"PASS"}\n'},
|
||||
"unrelated",
|
||||
)
|
||||
request["evidence_closure_oid"] = unrelated
|
||||
request["observed_evidence_closure_oid"] = unrelated
|
||||
with pytest.raises(RuntimeError, match="APPROVED_EXECUTION_BINDING_MISMATCH"):
|
||||
engine.plan(request)
|
||||
|
||||
|
||||
def test_pr_merge_after_expiry_is_denied(tmp_path: Path) -> None:
|
||||
_, _, engine, request = harness(tmp_path)
|
||||
with sqlite3.connect(request["approval_db_path"]) as connection:
|
||||
connection.execute(
|
||||
"UPDATE pull_request SET merged_unix=?",
|
||||
(int(_parse_test_utc(request["expires_at"]).timestamp()) + 1,),
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="ACTIVATION_APPROVAL_MERGED_AFTER_EXPIRY"):
|
||||
engine.plan(request)
|
||||
|
||||
|
||||
def _parse_test_utc(value: str) -> datetime:
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
|
||||
|
||||
def test_machine_status_must_match_bound_producer_and_observed_row(tmp_path: Path) -> None:
|
||||
_, _, engine, request = harness(tmp_path)
|
||||
context = sorted(MACHINE_CONTEXTS)[0]
|
||||
request["machine_checks"][context]["status_id"] += 1000
|
||||
with pytest.raises(RuntimeError, match="MACHINE_GATE_BINDING_MISMATCH"):
|
||||
engine.plan(request)
|
||||
|
||||
|
||||
def test_latest_machine_status_from_unapproved_producer_is_denied(tmp_path: Path) -> None:
|
||||
_, _, engine, request = harness(tmp_path)
|
||||
context = sorted(MACHINE_CONTEXTS)[0]
|
||||
with sqlite3.connect(request["approval_db_path"]) as connection:
|
||||
connection.execute("INSERT INTO user VALUES(100,'untrusted-producer')")
|
||||
connection.execute(
|
||||
"INSERT INTO commit_status VALUES(999,1,'success',?,?,100)",
|
||||
(request["approval_request_head_oid"], context),
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="MACHINE_GATE_PRODUCER_MISMATCH"):
|
||||
engine.plan(request)
|
||||
|
||||
|
||||
def test_live_activate_verify_status_success(tmp_path: Path) -> None:
|
||||
_, adapter, engine, request = harness(tmp_path)
|
||||
planned = engine.plan(request)
|
||||
@@ -262,6 +449,114 @@ def test_live_rollback_success(tmp_path: Path) -> None:
|
||||
assert engine.rollback(request, request["operation_id"], planned["plan_id"])["status"] == "ALREADY_ROLLED_BACK"
|
||||
|
||||
|
||||
def test_terminal_rollback_cannot_be_reactivated(tmp_path: Path) -> None:
|
||||
_, _, engine, request = harness(tmp_path)
|
||||
planned = engine.plan(request)
|
||||
engine.activate(request, planned["plan_id"])
|
||||
engine.rollback(request, request["operation_id"], planned["plan_id"])
|
||||
with pytest.raises(RuntimeError, match="OPERATION_TERMINAL_ROLLED_BACK"):
|
||||
engine.activate(request, planned["plan_id"])
|
||||
|
||||
|
||||
def test_verify_and_rollback_remain_available_after_approval_expiry(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
_, adapter, engine, request = harness(tmp_path)
|
||||
planned = engine.plan(request)
|
||||
engine.activate(request, planned["plan_id"])
|
||||
future = datetime.now(timezone.utc) + timedelta(hours=2)
|
||||
|
||||
class FutureDateTime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return future if tz is not None else future.replace(tzinfo=None)
|
||||
|
||||
monkeypatch.setattr(activation_module, "datetime", FutureDateTime)
|
||||
assert engine.verify(request, request["operation_id"])["status"] == "VERIFIED"
|
||||
assert engine.rollback(request, request["operation_id"], planned["plan_id"])["status"] == "ROLLED_BACK"
|
||||
assert adapter.read_current() == legacy_row()
|
||||
|
||||
|
||||
def test_prepared_crash_can_recover_after_approval_expiry(tmp_path: Path, monkeypatch) -> None:
|
||||
_, adapter, engine, request = harness(tmp_path)
|
||||
request["crash_at"] = "before_apply"
|
||||
planned = engine.plan(request)
|
||||
with pytest.raises(InjectedCrash):
|
||||
engine.activate(request, planned["plan_id"])
|
||||
request.pop("crash_at")
|
||||
future = datetime.now(timezone.utc) + timedelta(hours=2)
|
||||
|
||||
class FutureDateTime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return future if tz is not None else future.replace(tzinfo=None)
|
||||
|
||||
monkeypatch.setattr(activation_module, "datetime", FutureDateTime)
|
||||
assert engine.activate(request, planned["plan_id"])["status"] == "ACTIVATED"
|
||||
assert adapter.read_current() == owner_row()
|
||||
|
||||
|
||||
def test_fresh_activation_rechecks_expiry_at_prepared_boundary(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
_, adapter, engine, request = harness(tmp_path)
|
||||
planned = engine.plan(request)
|
||||
expiry = _parse_test_utc(request["expires_at"])
|
||||
moments = iter((
|
||||
expiry - timedelta(seconds=2),
|
||||
expiry - timedelta(seconds=1),
|
||||
expiry + timedelta(seconds=1),
|
||||
))
|
||||
monkeypatch.setattr(activation_module, "_utc_now", lambda: next(moments))
|
||||
with pytest.raises(RuntimeError, match="APPROVAL_EXPIRED"):
|
||||
engine.activate(request, planned["plan_id"])
|
||||
assert adapter.read_current() == legacy_row()
|
||||
assert engine.journal.load(request["operation_id"]) is None
|
||||
|
||||
|
||||
def test_fresh_activation_rejects_snapshot_drift_at_prepared_boundary(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
_, adapter, engine, request = harness(tmp_path)
|
||||
planned = engine.plan(request)
|
||||
original_read = adapter.read_current
|
||||
reads = 0
|
||||
|
||||
def drifting_read() -> dict:
|
||||
nonlocal reads
|
||||
reads += 1
|
||||
if reads == 2:
|
||||
drifted = original_read()
|
||||
drifted["can_push"] = 1
|
||||
drifted["updated_unix"] = 777
|
||||
return drifted
|
||||
return original_read()
|
||||
|
||||
monkeypatch.setattr(adapter, "read_current", drifting_read)
|
||||
with pytest.raises(CASConflict, match="PREPARED_BOUNDARY_SNAPSHOT_CHANGED"):
|
||||
engine.activate(request, planned["plan_id"])
|
||||
assert original_read() == legacy_row()
|
||||
assert engine.journal.load(request["operation_id"]) is None
|
||||
|
||||
|
||||
def test_rollback_uses_durable_authorization_after_live_status_rerun(tmp_path: Path) -> None:
|
||||
_, adapter, engine, request = harness(tmp_path)
|
||||
planned = engine.plan(request)
|
||||
engine.activate(request, planned["plan_id"])
|
||||
context = sorted(MACHINE_CONTEXTS)[0]
|
||||
with sqlite3.connect(request["approval_db_path"]) as connection:
|
||||
connection.execute(
|
||||
"INSERT INTO commit_status VALUES(999,1,'success',?,?,?)",
|
||||
(
|
||||
request["approval_request_head_oid"], context,
|
||||
request["machine_status_producer_id"],
|
||||
),
|
||||
)
|
||||
result = engine.rollback(request, request["operation_id"], planned["plan_id"])
|
||||
assert result["status"] == "ROLLED_BACK"
|
||||
assert adapter.read_current() == legacy_row()
|
||||
|
||||
|
||||
def test_missing_approval_denied(tmp_path: Path) -> None:
|
||||
_, _, engine, request = harness(tmp_path)
|
||||
request.pop("owner_review_oid")
|
||||
|
||||
Reference in New Issue
Block a user