Add guarded same-job execution re-anchor repair script
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
from datetime import datetime, timezone
|
||||
import pathlib
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
UTC = timezone.utc
|
||||
|
||||
def die(msg: str) -> None:
|
||||
print(f"ERROR: {msg}", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
|
||||
def backup(path: pathlib.Path) -> pathlib.Path:
|
||||
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
|
||||
out = path.with_name(path.name + f".bak.{stamp}")
|
||||
shutil.copy2(path, out)
|
||||
return out
|
||||
|
||||
def ensure_import(path: pathlib.Path, import_line: str) -> None:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if import_line + "\n" in text:
|
||||
return
|
||||
lines = text.splitlines(keepends=True)
|
||||
at = 0
|
||||
if lines and lines[0].startswith("#!"):
|
||||
at = 1
|
||||
if at < len(lines) and lines[at].startswith("from __future__ import"):
|
||||
at += 1
|
||||
lines.insert(at, import_line + "\n")
|
||||
path.write_text("".join(lines), encoding="utf-8")
|
||||
|
||||
def replace_once(path: pathlib.Path, old: str, new: str, label: str) -> None:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if new in text:
|
||||
print(f"OK already patched: {label}")
|
||||
return
|
||||
count = text.count(old)
|
||||
if count != 1:
|
||||
die(f"{label}: expected exactly one match, found {count}")
|
||||
b = backup(path)
|
||||
path.write_text(text.replace(old, new, 1), encoding="utf-8")
|
||||
print(f"PATCHED {label}; backup={b}")
|
||||
|
||||
def syntax_check(path: pathlib.Path) -> None:
|
||||
ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
print(f"OK syntax: {path}")
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Add guarded same-job execution re-anchor support.")
|
||||
ap.add_argument("--rebind", required=True)
|
||||
args = ap.parse_args()
|
||||
path = pathlib.Path(args.rebind).resolve()
|
||||
if not path.is_file():
|
||||
die(f"missing rebind source: {path}")
|
||||
|
||||
ensure_import(path, "import subprocess")
|
||||
|
||||
old = ''' binding = GovernedExecutionBinding.from_mapping(old_execution)
|
||||
resolved = dict(self.resolver(task_id, capabilities))
|
||||
new_execution = validate_execution_envelope(resolved, binding=binding)
|
||||
if new_execution.get("action") != "deterministic":
|
||||
raise ExecutionRebindError("EXECUTION_REBIND_DETERMINISTIC_PLAN_REQUIRED", 409)
|
||||
if new_execution["execution_digest"] != request.expected_new_execution_digest:
|
||||
raise ExecutionRebindError("EXECUTION_REBIND_NEW_DIGEST_DRIFT", 409)
|
||||
if new_execution["execution_digest"] == old_execution["execution_digest"]:
|
||||
raise ExecutionRebindError("EXECUTION_REBIND_DIGEST_UNCHANGED", 409)
|
||||
'''
|
||||
new = ''' binding = GovernedExecutionBinding.from_mapping(old_execution)
|
||||
resolved = dict(self.resolver(task_id, capabilities))
|
||||
candidate_binding = GovernedExecutionBinding.from_mapping(resolved)
|
||||
|
||||
# Same-job continuation may advance only the branch head (and may
|
||||
# transition CREATE_NEW -> CONTINUE_EXISTING after the branch exists).
|
||||
immutable_fields = (
|
||||
"task_id",
|
||||
"job_id",
|
||||
"repository",
|
||||
"base_oid",
|
||||
"target_branch",
|
||||
"generation",
|
||||
"idempotency_key",
|
||||
)
|
||||
for field in immutable_fields:
|
||||
if getattr(candidate_binding, field) != getattr(binding, field):
|
||||
raise ExecutionRebindError("EXECUTION_REBIND_BINDING_CONFLICT", 409)
|
||||
|
||||
head_advanced = candidate_binding.expected_branch_oid != binding.expected_branch_oid
|
||||
if head_advanced:
|
||||
allowed_modes = {
|
||||
(binding.branch_mode, binding.branch_mode),
|
||||
("CREATE_NEW", "CONTINUE_EXISTING"),
|
||||
}
|
||||
if (binding.branch_mode, candidate_binding.branch_mode) not in allowed_modes:
|
||||
raise ExecutionRebindError("EXECUTION_REBIND_BRANCH_MODE_CONFLICT", 409)
|
||||
|
||||
authority_git_dir = "/data/aios-gitops/repo.git"
|
||||
authority_ref = f"refs/heads/{binding.target_branch}"
|
||||
try:
|
||||
authority_head = subprocess.check_output(
|
||||
[
|
||||
"/usr/bin/git",
|
||||
f"--git-dir={authority_git_dir}",
|
||||
"rev-parse",
|
||||
authority_ref,
|
||||
],
|
||||
text=True,
|
||||
timeout=5,
|
||||
).strip()
|
||||
ancestry = subprocess.run(
|
||||
[
|
||||
"/usr/bin/git",
|
||||
f"--git-dir={authority_git_dir}",
|
||||
"merge-base",
|
||||
"--is-ancestor",
|
||||
binding.expected_branch_oid,
|
||||
candidate_binding.expected_branch_oid,
|
||||
],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=5,
|
||||
check=False,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError) as exc:
|
||||
raise ExecutionRebindError("EXECUTION_REBIND_AUTHORITY_GIT_UNAVAILABLE", 503) from exc
|
||||
|
||||
if authority_head != candidate_binding.expected_branch_oid:
|
||||
raise ExecutionRebindError("EXECUTION_REBIND_CURRENT_HEAD_MISMATCH", 409)
|
||||
if ancestry.returncode != 0:
|
||||
raise ExecutionRebindError("EXECUTION_REBIND_NON_FAST_FORWARD_DENY", 409)
|
||||
elif candidate_binding.branch_mode != binding.branch_mode:
|
||||
raise ExecutionRebindError("EXECUTION_REBIND_BRANCH_MODE_CONFLICT", 409)
|
||||
|
||||
# Validate the canonical envelope on its own after the continuation
|
||||
# binding proof above. Worker-side strict equality remains unchanged.
|
||||
new_execution = validate_execution_envelope(resolved)
|
||||
|
||||
# Re-anchor may legitimately remain source_capture when no deterministic
|
||||
# plan is registered yet. A non-head-changing rebind must still advance
|
||||
# source_capture -> deterministic.
|
||||
if new_execution.get("action") not in {"source_capture", "deterministic"}:
|
||||
raise ExecutionRebindError("EXECUTION_REBIND_ACTION_INVALID", 409)
|
||||
if not head_advanced and new_execution.get("action") != "deterministic":
|
||||
raise ExecutionRebindError("EXECUTION_REBIND_DETERMINISTIC_PLAN_REQUIRED", 409)
|
||||
if new_execution["execution_digest"] != request.expected_new_execution_digest:
|
||||
raise ExecutionRebindError("EXECUTION_REBIND_NEW_DIGEST_DRIFT", 409)
|
||||
if new_execution["execution_digest"] == old_execution["execution_digest"]:
|
||||
raise ExecutionRebindError("EXECUTION_REBIND_DIGEST_UNCHANGED", 409)
|
||||
'''
|
||||
replace_once(path, old, new, "same-job fast-forward execution re-anchor")
|
||||
|
||||
old_event = ''' "old_action": old_execution.get("action"),
|
||||
"new_action": new_execution.get("action"),
|
||||
"previous_revision": request.expected_revision,
|
||||
'''
|
||||
new_event = ''' "old_action": old_execution.get("action"),
|
||||
"new_action": new_execution.get("action"),
|
||||
"old_expected_branch_oid": binding.expected_branch_oid,
|
||||
"new_expected_branch_oid": candidate_binding.expected_branch_oid,
|
||||
"head_advanced": head_advanced,
|
||||
"previous_revision": request.expected_revision,
|
||||
'''
|
||||
replace_once(path, old_event, new_event, "re-anchor audit evidence")
|
||||
syntax_check(path)
|
||||
print("RESULT: PATCH_OK")
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user