#!/usr/bin/env python3 """Apply an approved Goal patch with bounded paths and rollback-on-failure.""" from __future__ import annotations import argparse import hashlib import json import os import subprocess import tempfile import importlib.util from datetime import datetime, timezone def root() -> str: current = os.path.abspath(os.path.dirname(__file__)) while current != os.path.dirname(current): if os.path.isdir(os.path.join(current, ".specify")): return current current = os.path.dirname(current) raise SystemExit("GOAL_APPLY_ROOT_NOT_FOUND") ROOT = root() INBOX = os.path.join(ROOT, "packages", "casan-harness", "scripts", "bash", "approval-inbox.py") _MANIFEST_SPEC = importlib.util.spec_from_file_location( "casan_project_manifest", os.path.join(os.path.dirname(__file__), "project_manifest.py"), ) PROJECT_MANIFEST = importlib.util.module_from_spec(_MANIFEST_SPEC) _MANIFEST_SPEC.loader.exec_module(PROJECT_MANIFEST) def now() -> str: return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") def load(path: str) -> dict: with open(path, encoding="utf-8") as handle: return json.load(handle) def save(path: str, payload: dict) -> None: directory = os.path.dirname(path) with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=directory, delete=False) as handle: json.dump(payload, handle, indent=2, ensure_ascii=False) handle.write("\n") temporary = handle.name os.chmod(temporary, 0o600) os.replace(temporary, path) def run(command: list[str], timeout: int = 180) -> subprocess.CompletedProcess[str]: return subprocess.run(command, cwd=ROOT, capture_output=True, text=True, timeout=timeout) def verify_approval(job: dict) -> dict: result = run(["python3", INBOX, "list", "--status", "all"], 30) if result.returncode != 0: raise RuntimeError("GOAL_APPLY_APPROVAL_STORE_UNAVAILABLE") proposal = next((row for row in json.loads(result.stdout).get("proposals", []) if row.get("id") == job.get("approval", {}).get("id")), None) artifact = job.get("patch_artifact", {}) if not proposal or proposal.get("status") != "approved": raise PermissionError("GOAL_APPLY_APPROVAL_REQUIRED") if proposal.get("action") != "goal.workspace.execute" or proposal.get("payload", {}).get("goal_id") != job.get("id"): raise PermissionError("GOAL_APPLY_APPROVAL_SCOPE_MISMATCH") if proposal.get("payload", {}).get("patch_sha256") != artifact.get("sha256"): raise PermissionError("GOAL_APPLY_PATCH_HASH_MISMATCH") if sorted(proposal.get("payload", {}).get("changed_files", [])) != sorted(artifact.get("files", [])): raise PermissionError("GOAL_APPLY_FILE_SCOPE_MISMATCH") return proposal def _manifest_for_files(files: list[str]) -> dict: selected = os.environ.get("CASAN_PROJECT_MANIFEST") project = os.environ.get("CASAN_PROJECT_ID") if selected or project: return PROJECT_MANIFEST.load(ROOT, selected, project) registry = load(os.path.join(ROOT, "packages", "casan-harness", "level5", "project-registry.json")) candidates = [] for entry in registry.get("projects", []): manifest_path = entry.get("manifest") if not manifest_path or entry.get("status") != "active": continue try: manifest = PROJECT_MANIFEST.load(ROOT, manifest_path) except (OSError, ValueError): continue if any(any(path.startswith(root.rstrip("/") + "/") for root in manifest["source_roots"]) for path in files): candidates.append(manifest) if len(candidates) != 1: reason = "none" if not candidates else "multiple" raise RuntimeError(f"GOAL_APPLY_PROJECT_MANIFEST_{reason.upper()}") return candidates[0] def verification_commands(files: list[str], manifest: dict | None = None) -> list[list[str]]: project = manifest or _manifest_for_files(files) # `git apply --check --whitespace=error` already validates the exact patch # before mutation. Runtime images intentionally do not need repository # metadata, so post-apply verification is limited to manifest build/tests. return PROJECT_MANIFEST.verification_commands(project, files) def ready_for_apply(job: dict) -> bool: if not job.get("patch_artifact"): return False if job.get("status") == "requires_approval": return True return ( job.get("status") == "failed" and job.get("error") == "GOAL_APPLY_VERIFICATION_FAILED_ROLLED_BACK" ) def execute(job_path: str, actor: str) -> dict: job = load(job_path) if not ready_for_apply(job): raise RuntimeError("GOAL_APPLY_JOB_NOT_READY") proposal = verify_approval(job) job.setdefault("approval", {})["status"] = "approved" if proposal.get("approver") != actor: raise PermissionError("GOAL_APPLY_APPROVER_IDENTITY_MISMATCH") artifact = job["patch_artifact"] patch_path = os.path.realpath(os.path.join(ROOT, artifact["path"])) state_root = os.path.realpath(os.path.join(ROOT, ".specify", "state", "goals")) if not patch_path.startswith(state_root + os.sep) or not os.path.isfile(patch_path): raise PermissionError("GOAL_APPLY_ARTIFACT_PATH_DENIED") with open(patch_path, "rb") as handle: actual_hash = hashlib.sha256(handle.read()).hexdigest() if actual_hash != artifact["sha256"]: raise PermissionError("GOAL_APPLY_ARTIFACT_TAMPERED") check = run(["git", "apply", "--check", "--whitespace=error", patch_path], 30) if check.returncode != 0: raise RuntimeError("GOAL_APPLY_PRECONDITION_FAILED:" + (check.stderr or check.stdout).strip()[:200]) applied = run(["git", "apply", "--whitespace=error", patch_path], 30) if applied.returncode != 0: raise RuntimeError("GOAL_APPLY_FAILED:" + (applied.stderr or applied.stdout).strip()[:200]) checks = [] try: for command in verification_commands(artifact.get("files", [])): result = run(command, 300) checks.append({"command": " ".join(command), "exit_code": result.returncode, "output": (result.stdout + result.stderr)[-4000:]}) if result.returncode != 0: raise RuntimeError("GOAL_APPLY_VERIFICATION_FAILED") except Exception: rollback = run(["git", "apply", "--reverse", patch_path], 30) if rollback.returncode != 0: raise RuntimeError("GOAL_APPLY_ROLLBACK_FAILED") artifact["status"] = "awaiting_apply_retry" job.update(status="requires_approval", error="GOAL_APPLY_VERIFICATION_FAILED_ROLLED_BACK", patch_artifact=artifact, verification=checks, finished_at=now(), updated_at=now()) save(job_path, job) raise artifact["status"] = "applied" artifact["applied_at"] = now() artifact["applied_by"] = actor job.update(status="completed", result="Approved implementation patch applied and verified successfully.", patch_artifact=artifact, verification=checks, finished_at=now(), updated_at=now()) save(job_path, job) return job def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--job-file", required=True) parser.add_argument("--actor", required=True) args = parser.parse_args() try: print(json.dumps(execute(os.path.realpath(args.job_file), args.actor), ensure_ascii=False)) return 0 except PermissionError as error: print(str(error), file=os.sys.stderr) return 3 except Exception as error: print(str(error), file=os.sys.stderr) return 2 if __name__ == "__main__": raise SystemExit(main())