autonomy-unit-tests exact-head check passed: autonomy-unit-tests
autonomy-smoke-tests exact-head check passed: autonomy-smoke-tests
signer-validator-contract exact-head check passed: signer-validator-contract
secret-scan exact-head check passed: secret-scan
py-compile exact-head check passed: py-compile
git-diff-check exact-head check passed: git-diff-check
release-state-machine exact-head check passed: release-state-machine
reconciliation-contract exact-head check passed: reconciliation-contract
campaign-bootstrap-gate exact-head check passed: campaign-bootstrap-gate
owner-approval-gate RECOVERED_OWNER_CAMPAIGN_AUTHORIZATION_VERIFIED; bootstrap-only exact PR53/head; no review created; no new Owner interaction
280 lines
16 KiB
Python
280 lines
16 KiB
Python
"""Durable Job side-effect executor for the AIOS campaign control plane."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
from dataclasses import dataclass, replace
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any, Mapping
|
|
|
|
from aios_authority_git_executor import AuthorityGitExecutor, AuthorityGitError
|
|
from aios_campaign_recovery import RecoveryEvidenceError, load_recovered_authorization
|
|
from aios_gitea_machine_broker import CampaignCapabilityReceipt, GiteaMachineBroker, MachineBrokerError
|
|
from aios_gitops_job_controller import GitOpsJobController, JobError, JobState, JobStore
|
|
|
|
|
|
TASK_ID = "AIOS_AUTONOMOUS_RELEASE_CAMPAIGN_V1"
|
|
CAMPAIGN_ID = "AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807"
|
|
REPOSITORY = "aios/aios"
|
|
BRANCH = "agent/autonomous-release-campaign-v1-20260807"
|
|
EXPECTED_BRANCH_OID = "bba675fab3b7e0c5fba7066720252b3c91b9f20d"
|
|
AUTHORIZATION_DIGEST = "6b0bf550eb702126e967eeff2d561fa808444b924d8a71f5399accad508ed3b2"
|
|
LINEAGE_ANCHOR = "69b45e81dcd2c3707c367003b08c55fd361faae8"
|
|
|
|
|
|
class JobExecutionError(RuntimeError):
|
|
pass
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class DurableJobExecutor:
|
|
controller: GitOpsJobController
|
|
authority: AuthorityGitExecutor
|
|
broker: GiteaMachineBroker
|
|
recovery_path: Path
|
|
|
|
def _request(self, job_id: str) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
row = self.controller.store.read(job_id)
|
|
try:
|
|
request = json.loads(row["request_json"])
|
|
except (KeyError, TypeError, json.JSONDecodeError) as exc:
|
|
raise JobExecutionError("JOB_REQUEST_INVALID") from exc
|
|
return row, dict(request.get("binding") or {})
|
|
|
|
def _validate_campaign(self, request: Mapping[str, Any]) -> str:
|
|
operation = str(request.get("operation", ""))
|
|
expected_branch_oid = request.get("expected_branch_oid")
|
|
required = {
|
|
"task_id": TASK_ID,
|
|
"campaign_id": CAMPAIGN_ID,
|
|
"repository": REPOSITORY,
|
|
"branch": BRANCH,
|
|
"authorization_recovery_digest": AUTHORIZATION_DIGEST,
|
|
}
|
|
for key, expected in required.items():
|
|
if request.get(key) != expected:
|
|
raise JobExecutionError(f"JOB_{key.upper()}_MISMATCH")
|
|
new_oid = request.get("new_branch_oid")
|
|
if not isinstance(new_oid, str) or len(new_oid) != 40 or any(ch not in "0123456789abcdef" for ch in new_oid):
|
|
raise JobExecutionError("NEW_BRANCH_OID_INVALID")
|
|
if not isinstance(expected_branch_oid, str) or len(expected_branch_oid) != 40 or any(ch not in "0123456789abcdef" for ch in expected_branch_oid):
|
|
raise JobExecutionError("EXPECTED_BRANCH_OID_INVALID")
|
|
if operation == "ADVANCE_EXISTING_CAMPAIGN_BRANCH" and expected_branch_oid == new_oid:
|
|
raise JobExecutionError("EXPECTED_BRANCH_OID_INVALID")
|
|
if operation != "ADVANCE_EXISTING_CAMPAIGN_BRANCH" and expected_branch_oid != new_oid:
|
|
raise JobExecutionError("POST_ADVANCE_HEAD_BINDING_INVALID")
|
|
try:
|
|
recovery = load_recovered_authorization(self.recovery_path)
|
|
except RecoveryEvidenceError as exc:
|
|
raise JobExecutionError(str(exc)) from exc
|
|
if recovery["digest"] != AUTHORIZATION_DIGEST:
|
|
raise JobExecutionError("RECOVERY_DIGEST_MISMATCH")
|
|
for key, expected in required.items():
|
|
if key == "campaign_id":
|
|
actual = recovery.get(key)
|
|
elif key == "authorization_recovery_digest":
|
|
continue
|
|
else:
|
|
actual = recovery.get({"expected_branch_oid": "expected_branch_oid"}.get(key, key))
|
|
if actual is not None and actual != expected:
|
|
raise JobExecutionError("RECOVERY_BINDING_MISMATCH")
|
|
if recovery.get("expected_branch_oid") != EXPECTED_BRANCH_OID:
|
|
raise JobExecutionError("RECOVERY_BRANCH_OID_MISMATCH")
|
|
authority_head = self.authority.read_ref(f"refs/heads/{BRANCH}")
|
|
if authority_head not in {expected_branch_oid, new_oid}:
|
|
raise JobExecutionError("AUTHORITY_BRANCH_EXPECTATION_MISMATCH")
|
|
if not self.authority.ancestry_check(expected_branch_oid, new_oid):
|
|
raise JobExecutionError("NEW_OID_NOT_DESCENDANT")
|
|
if not self.authority.ancestry_check(EXPECTED_BRANCH_OID, new_oid):
|
|
raise JobExecutionError("NEW_OID_OUTSIDE_AUTHORIZED_LINEAGE")
|
|
identity = self.broker.verify_identity()
|
|
if identity.get("repository") != REPOSITORY or identity.get("login") == "aiman":
|
|
raise JobExecutionError("MACHINE_IDENTITY_SCOPE_INVALID")
|
|
return new_oid
|
|
|
|
def dry_run(self, job_id: str) -> dict[str, Any]:
|
|
_row, request = self._request(job_id)
|
|
new_oid = self._validate_campaign(request)
|
|
remote = self.broker.readback()
|
|
if remote is None:
|
|
action = "CREATE_REQUIRED"
|
|
elif remote == new_oid:
|
|
action = "ALREADY_IN_SYNC"
|
|
elif remote == request["expected_branch_oid"]:
|
|
action = "FAST_FORWARD_REQUIRED"
|
|
else:
|
|
action = "MANUAL_OWNER_REVIEW_REQUIRED"
|
|
if action == "MANUAL_OWNER_REVIEW_REQUIRED":
|
|
raise JobExecutionError("GITEA_BRANCH_CONFLICT")
|
|
return {
|
|
"decision": "SAFE_TO_ADVANCE",
|
|
"authority_action": "CAS_REQUIRED",
|
|
"gitea_action": action,
|
|
"previous_branch_oid": request["expected_branch_oid"],
|
|
"final_branch_oid": new_oid,
|
|
"force_push": False,
|
|
}
|
|
|
|
def _recover_requested(self, job_id: str) -> dict[str, Any]:
|
|
row = self.controller.store.read(job_id)
|
|
if JobState(row["state"]) in {JobState.RECOVERABLE_FAILURE, JobState.RECONCILIATION_REQUIRED}:
|
|
self.controller.resume(job_id)
|
|
row = self.controller.store.read(job_id)
|
|
return row
|
|
|
|
def execute(self, job_id: str, *, dry_run: bool = False) -> dict[str, Any]:
|
|
try:
|
|
return self._execute_unprotected(job_id, dry_run=dry_run)
|
|
except Exception as exc:
|
|
try:
|
|
row = self.controller.store.read(job_id)
|
|
state = JobState(row["state"])
|
|
if state not in {JobState.COMPLETED, JobState.MANUAL_OWNER_REVIEW_REQUIRED, JobState.RECOVERABLE_FAILURE, JobState.RECONCILIATION_REQUIRED}:
|
|
self.controller.advance(job_id, JobState.RECOVERABLE_FAILURE, side_effect_state="UNKNOWN", receipt={"error_code": type(exc).__name__})
|
|
except Exception:
|
|
pass
|
|
raise
|
|
|
|
def _execute_unprotected(self, job_id: str, *, dry_run: bool = False) -> dict[str, Any]:
|
|
row, request = self._request(job_id)
|
|
operation = request.get("operation")
|
|
if operation == "ADVANCE_EXISTING_CAMPAIGN_BRANCH":
|
|
if dry_run:
|
|
return self.dry_run(job_id)
|
|
return self._advance_campaign(job_id)
|
|
if operation == "COMMIT_FILE":
|
|
return self._commit_file(job_id, request)
|
|
if operation == "SYNC_TASK_BRANCH_TO_GITEA":
|
|
new_oid = self._validate_campaign(request)
|
|
final = self.broker.push_branch(new_oid)
|
|
return {"operation": operation, "remote_verified": final == new_oid, "final_network_oid": final}
|
|
if operation == "CREATE_PR":
|
|
self._validate_campaign(request)
|
|
return dict(self.broker.create_pr("AIOS autonomous release control plane", "Durable campaign continuation; exact head and recovery evidence are bound in the Job."))
|
|
if operation == "PUBLISH_CHECK":
|
|
self._validate_campaign(request)
|
|
context = request.get("check_context", "aios/durable-release")
|
|
state = request.get("check_state", "success")
|
|
description = request.get("check_description", "Durable campaign control-plane check passed")
|
|
return dict(self.broker.status(str(state), str(description), str(context)))
|
|
if operation == "PROTECTED_MERGE":
|
|
self._validate_campaign(request)
|
|
number = int(request.get("pr_number", "0"))
|
|
self.broker.transport.expected_merge_head = request["expected_branch_oid"]
|
|
expected_main = request.get("expected_main_oid")
|
|
if expected_main is not None:
|
|
self.broker.transport.expected_merge_base = expected_main
|
|
return dict(self.broker.merge(number))
|
|
if operation == "RECONCILE_AUTHORITY_MAIN":
|
|
self._validate_campaign(request)
|
|
old = request.get("expected_main_oid")
|
|
new = request.get("new_branch_oid")
|
|
if not isinstance(old, str) or not isinstance(new, str):
|
|
raise JobExecutionError("MAIN_RECONCILIATION_BINDING_INVALID")
|
|
gitea_main = self.broker.transport.read_branch("main")
|
|
if gitea_main != new:
|
|
raise JobExecutionError("GITEA_MAIN_EXPECTATION_MISMATCH")
|
|
if not self.authority.ancestry_check(old, new):
|
|
raise JobExecutionError("MAIN_OID_NOT_DESCENDANT")
|
|
return {"authority_main": self.authority.ff_CAS_main_after_verified_protected_merge(old, new)}
|
|
raise JobExecutionError("OPERATION_NOT_ALLOWED")
|
|
|
|
def _advance_campaign(self, job_id: str) -> dict[str, Any]:
|
|
self._recover_requested(job_id)
|
|
row, request = self._request(job_id)
|
|
new_oid = self._validate_campaign(request)
|
|
expected_oid = request["expected_branch_oid"]
|
|
state = JobState(row["state"])
|
|
if state == JobState.REQUESTED:
|
|
receipt = self.controller.advance(job_id, JobState.AUTHORIZED, side_effect_state="AUTHORIZED", receipt={"authorization_digest": AUTHORIZATION_DIGEST, "task_id": TASK_ID})
|
|
row = self.controller.store.read(job_id)
|
|
if JobState(row["state"]) == JobState.AUTHORIZED:
|
|
self.controller.lease_job(job_id, lease_id=f"lease-{job_id}", lineage_anchor_oid=LINEAGE_ANCHOR, current_head_oid=expected_oid, generation=1, lease_expires_at="2099-01-01T00:00:00Z", expected_revision=int(row["revision"]))
|
|
row = self.controller.store.read(job_id)
|
|
if JobState(row["state"]) == JobState.LEASED:
|
|
self.controller.advance(job_id, JobState.WORKTREE_READY, side_effect_state="WORKTREE_READY", receipt={"branch": BRANCH, "expected_oid": expected_oid})
|
|
row = self.controller.store.read(job_id)
|
|
if JobState(row["state"]) == JobState.WORKTREE_READY:
|
|
self.controller.advance(job_id, JobState.EXECUTING, side_effect_state="IN_PROGRESS")
|
|
row = self.controller.store.read(job_id)
|
|
authority_before = self.authority.read_ref(f"refs/heads/{BRANCH}")
|
|
if authority_before == expected_oid:
|
|
authority_oid = self.authority.advance_task_branch_CAS(f"refs/heads/{BRANCH}", expected_oid, new_oid)
|
|
elif authority_before == new_oid:
|
|
authority_oid = new_oid
|
|
else:
|
|
raise JobExecutionError("AUTHORITY_BRANCH_CONFLICT")
|
|
row = self.controller.store.read(job_id)
|
|
if JobState(row["state"]) == JobState.EXECUTING:
|
|
self.controller.advance(job_id, JobState.LOCAL_VERIFIED, side_effect_state="LOCAL_VERIFIED", receipt={"authority": {"branch": BRANCH, "previous_oid": expected_oid, "new_oid": new_oid, "CAS_success": True, "readback_oid": authority_oid}})
|
|
row = self.controller.store.read(job_id)
|
|
remote_before = self.broker.readback()
|
|
if remote_before != new_oid:
|
|
if remote_before not in {None, expected_oid}:
|
|
raise JobExecutionError("GITEA_BRANCH_CONFLICT")
|
|
if JobState(row["state"]) == JobState.LOCAL_VERIFIED:
|
|
if hasattr(self.broker, "receipt"):
|
|
self.broker.receipt = replace(self.broker.receipt, expected_oid=expected_oid)
|
|
self.controller.advance(job_id, JobState.REMOTE_SYNCING, side_effect_state="IN_PROGRESS", receipt={"gitea": {"previous_oid": remote_before, "desired_oid": new_oid, "force_push": False}})
|
|
final_network = self.broker.push_branch(new_oid)
|
|
else:
|
|
final_network = new_oid
|
|
final_api = self.broker.readback()
|
|
if final_api != new_oid:
|
|
raise JobExecutionError("GITEA_BRANCH_READBACK_MISMATCH")
|
|
row = self.controller.store.read(job_id)
|
|
if JobState(row["state"]) in {JobState.LOCAL_VERIFIED, JobState.REMOTE_SYNCING}:
|
|
self.controller.advance(job_id, JobState.REMOTE_VERIFIED, side_effect_state="REMOTE_VERIFIED", receipt={"gitea": {"machine_actor": "aios-autonomy-agent", "previous_oid": remote_before, "desired_oid": new_oid, "final_network_oid": final_network, "final_API_oid": final_api, "force_push": False}})
|
|
row = self.controller.store.read(job_id)
|
|
if JobState(row["state"]) == JobState.REMOTE_VERIFIED:
|
|
self.controller.advance(job_id, JobState.COMPLETED, side_effect_state="REMOTE_VERIFIED", receipt={"authority_oid": authority_oid, "network_oid": final_network, "gitea_api_oid": final_api, "force_push": False})
|
|
return self.controller.store._receipt(self.controller.store.read(job_id), duplicate=True).as_dict()
|
|
|
|
def _commit_file(self, job_id: str, request: Mapping[str, Any]) -> dict[str, Any]:
|
|
row = self.controller.store.read(job_id)
|
|
payload_ref = request.get("payload_ref")
|
|
if not payload_ref:
|
|
raise JobExecutionError("PAYLOAD_REQUIRED")
|
|
payload = self.controller.store.payloads.read_verified(payload_ref, row["payload_digest"], int(request["payload_size"]))
|
|
try:
|
|
spec = json.loads(payload.decode())
|
|
path = str(spec["path"])
|
|
content = base64.b64decode(spec["content_base64"], validate=True)
|
|
except Exception as exc:
|
|
raise JobExecutionError("COMMIT_FILE_PAYLOAD_INVALID") from exc
|
|
new_oid = self.authority.commit_file_in_isolated_worktree(base_oid=row["expected_branch_oid"], files={path: content}, message="chore(gitops): durable commit-file", worktree_name=job_id)
|
|
return {"operation": "COMMIT_FILE", "new_oid": new_oid}
|
|
|
|
|
|
def build_from_environment(job_root: Path) -> DurableJobExecutor:
|
|
from aios_gitea_machine_transport import GiteaMachineTransport
|
|
from aios_gitea_machine_broker import MachineCredentialProjection
|
|
|
|
account = __import__("os").environ.get("AIOS_GITEA_MACHINE_ACCOUNT", "aios-autonomy-agent")
|
|
api = __import__("os").environ.get("AIOS_GITEA_API", "https://g.cnnetmkt.top/git/api/v1")
|
|
remote = __import__("os").environ.get("AIOS_GITEA_REMOTE", "https://g.cnnetmkt.top/git/aios/aios.git")
|
|
authority_dir = Path(__import__("os").environ.get("AIOS_GITOPS_AUTHORITY_GIT_DIR", "/data/aios-gitops/repo.git"))
|
|
worktree_root = Path(__import__("os").environ.get("AIOS_GITOPS_WORKTREE_ROOT", "/var/lib/aios-gitops/worktrees"))
|
|
recovery = Path(__import__("os").environ.get("AIOS_RELEASE_RECOVERY_RECEIPT", "/var/lib/aios-gitops/release-campaigns/AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807/authorization-recovery.json"))
|
|
transport = GiteaMachineTransport(api, REPOSITORY, remote, authority_dir)
|
|
credential = MachineCredentialProjection.from_mapping({"scheme": "systemd-credential", "name": "gitea-machine-token", "account": account})
|
|
receipt = CampaignCapabilityReceipt(CAMPAIGN_ID, TASK_ID, REPOSITORY, BRANCH, EXPECTED_BRANCH_OID, frozenset({"exact_task_branch_push", "pr_create_update", "status_publish", "protected_merge", "remote_readback"}), frozenset({"direct_main_push", "force_push", "tag_update", "tag_delete", "baseline_write", "production_deploy"}))
|
|
return DurableJobExecutor(GitOpsJobController(JobStore(job_root)), AuthorityGitExecutor(authority_dir, worktree_root), GiteaMachineBroker(transport, credential, receipt), recovery)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--job-root", required=True, type=Path)
|
|
parser.add_argument("--job-id", required=True)
|
|
parser.add_argument("--dry-run", action="store_true")
|
|
args = parser.parse_args(argv)
|
|
result = build_from_environment(args.job_root).execute(args.job_id, dry_run=args.dry_run)
|
|
print(json.dumps(result, sort_keys=True))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|