304 lines
15 KiB
Python
304 lines
15 KiB
Python
"""Durable governed task binding for CREATE_NEW and CONTINUE_EXISTING."""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, asdict
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import stat
|
|
import tempfile
|
|
from typing import Any, Mapping
|
|
|
|
from aios_gitops_job_controller import GitOpsJobController, JobStore
|
|
from aios_gitops_job_identity import legacy_task_storage_key, task_storage_key
|
|
from aios_online_agent_execution import GovernedExecutionBinding, project_execution_envelope
|
|
|
|
try: # pragma: no cover - exercised on the Linux activation host
|
|
import fcntl
|
|
except ImportError: # pragma: no cover - non-POSIX execution stays fail closed
|
|
fcntl = None # type: ignore[assignment]
|
|
|
|
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
|
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
|
|
|
|
|
|
class BootstrapError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def _lexists(path: Path) -> bool:
|
|
try:
|
|
path.lstat()
|
|
return True
|
|
except FileNotFoundError:
|
|
return False
|
|
|
|
|
|
def _require_runtime_directory(path: Path) -> None:
|
|
try:
|
|
metadata = path.lstat()
|
|
except OSError as exc:
|
|
raise BootstrapError("TASK_STORAGE_PARENT_INVALID") from exc
|
|
expected_uid = getattr(os, "geteuid", lambda: metadata.st_uid)()
|
|
if (
|
|
stat.S_ISLNK(metadata.st_mode)
|
|
or not stat.S_ISDIR(metadata.st_mode)
|
|
or metadata.st_mode & 0o022
|
|
or metadata.st_uid != expected_uid
|
|
):
|
|
raise BootstrapError("TASK_STORAGE_PARENT_INVALID")
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class GovernedTaskBinding:
|
|
task_id: str
|
|
repository: str
|
|
base_oid: str
|
|
target_branch: str
|
|
risk: str
|
|
description: str
|
|
idempotency_key: str
|
|
expected_branch_oid: str
|
|
branch_mode: str
|
|
generation: int
|
|
job_id: str | None = None
|
|
|
|
def response(self, *, idempotent: bool = False) -> dict[str, Any]:
|
|
return {"task_id": self.task_id, "repository": self.repository, "base_oid": self.base_oid, "target_branch": self.target_branch, "expected_branch_oid": self.expected_branch_oid, "generation": self.generation, "branch_mode": self.branch_mode, "create_target_branch": self.branch_mode == "CREATE_NEW", "worktree_mode": "detached_at_base_oid" if self.branch_mode == "CREATE_NEW" else "detached_at_expected_head", "cas_ref_update_required": True, "idempotent": idempotent, "state": "BOUND", "job_id": self.job_id}
|
|
|
|
|
|
|
|
# ===== AIOS Formal Lineage producer-native reservation v12 =====
|
|
import re as _fl_re
|
|
|
|
_FL_PRODUCER_KEY = _fl_re.compile(r"^[0-9a-f]{64}$")
|
|
|
|
def _formal_lineage_reservation_receipt(
|
|
value,
|
|
*,
|
|
request_idempotency_key: str,
|
|
expected_side_effect_state: str,
|
|
):
|
|
if callable(getattr(value, "as_dict", None)):
|
|
raw=value.as_dict()
|
|
elif isinstance(value, dict):
|
|
raw=dict(value)
|
|
else:
|
|
raise RuntimeError("GOVERNED_TASK_SUBMIT_RECEIPT_TYPE_INVALID")
|
|
|
|
producer_key=raw.get("idempotency_key")
|
|
job_id=raw.get("job_id")
|
|
if not isinstance(job_id,str) or not job_id:
|
|
raise RuntimeError("GOVERNED_TASK_SUBMIT_RECEIPT_JOB_ID_INVALID")
|
|
if raw.get("state")!="REQUESTED":
|
|
raise RuntimeError("GOVERNED_TASK_SUBMIT_RECEIPT_STATE_INVALID")
|
|
if raw.get("side_effect_state")!=expected_side_effect_state:
|
|
raise RuntimeError("GOVERNED_TASK_SUBMIT_RECEIPT_SIDE_EFFECT_STATE_INVALID")
|
|
if not isinstance(producer_key,str) or not _FL_PRODUCER_KEY.fullmatch(producer_key):
|
|
raise RuntimeError("GOVERNED_TASK_SUBMIT_RECEIPT_PRODUCER_IDEMPOTENCY_INVALID")
|
|
if job_id!="job-"+producer_key[:32]:
|
|
raise RuntimeError("GOVERNED_TASK_SUBMIT_RECEIPT_JOB_PRODUCER_KEY_CONFLICT")
|
|
if not isinstance(request_idempotency_key,str) or not request_idempotency_key:
|
|
raise RuntimeError("GOVERNED_TASK_REQUEST_IDEMPOTENCY_INVALID")
|
|
|
|
return {
|
|
"job_id":job_id,
|
|
"state":"REQUESTED",
|
|
"side_effect_state":raw["side_effect_state"],
|
|
"producer_idempotency_key":producer_key,
|
|
"request_idempotency_key":request_idempotency_key,
|
|
}
|
|
|
|
def _formal_lineage_attach_receipt(response, receipt):
|
|
if not isinstance(response, dict):
|
|
raise RuntimeError("GOVERNED_TASK_BINDING_RESPONSE_INVALID")
|
|
if response.get("state")!="BOUND":
|
|
raise RuntimeError("GOVERNED_TASK_BINDING_STATE_INVALID")
|
|
if response.get("job_id")!=receipt.get("job_id"):
|
|
raise RuntimeError("GOVERNED_TASK_BINDING_RECEIPT_JOB_CONFLICT")
|
|
result=dict(response)
|
|
result["reservation_receipt"]=dict(receipt)
|
|
return result
|
|
|
|
class GovernedTaskBootstrap:
|
|
def __init__(self, authority: Any, root: Path, *, remote_name: str, token_path: Path, ssh_key_path: Path, api_base: str = "https://g.cnnetmkt.top/git/api/v1") -> None:
|
|
self.authority = authority; self.root = Path(root); self.remote_name = remote_name; self.token_path = Path(token_path); self.ssh_key_path = Path(ssh_key_path); self.api_base = api_base; self.task_meta_root = self.root / "task-bindings"; self.job_root = self.root / "jobs"; self.lock_root = self.root / "task-binding-locks"; self.task_meta_root.mkdir(parents=True, exist_ok=True, mode=0o700); self.job_root.mkdir(parents=True, exist_ok=True, mode=0o700); self.lock_root.mkdir(parents=True, exist_ok=True, mode=0o700); _require_runtime_directory(self.task_meta_root); _require_runtime_directory(self.job_root); _require_runtime_directory(self.lock_root)
|
|
|
|
def _store(self, task_id: str) -> tuple[JobStore, GitOpsJobController]:
|
|
root = self.job_root / task_storage_key(task_id)
|
|
legacy = self.job_root / legacy_task_storage_key(task_id)
|
|
if legacy != root and _lexists(legacy):
|
|
if _lexists(root):
|
|
raise BootstrapError("TASK_STORAGE_LEGACY_CONFLICT")
|
|
raise BootstrapError("TASK_STORAGE_LEGACY_MIGRATION_REQUIRED")
|
|
store = JobStore(root, payload_root=root / "payloads")
|
|
return store, GitOpsJobController(store)
|
|
|
|
def _metadata(self, task_id: str) -> Path:
|
|
return self.task_meta_root / (task_storage_key(task_id) + ".json")
|
|
|
|
def _legacy_metadata(self, task_id: str) -> Path:
|
|
return self.task_meta_root / (legacy_task_storage_key(task_id) + ".json")
|
|
|
|
def _read_existing(self, task_id: str) -> GovernedTaskBinding | None:
|
|
path = self._metadata(task_id)
|
|
legacy = self._legacy_metadata(task_id)
|
|
if legacy != path and _lexists(legacy):
|
|
if _lexists(path):
|
|
raise BootstrapError("TASK_BINDING_LEGACY_CONFLICT")
|
|
raise BootstrapError("TASK_BINDING_LEGACY_MIGRATION_REQUIRED")
|
|
if not _lexists(path):
|
|
return None
|
|
if not path.is_file() or path.is_symlink():
|
|
raise BootstrapError("TASK_BINDING_PATH_INVALID")
|
|
|
|
binding = GovernedTaskBinding(**json.loads(path.read_text(encoding="utf-8")))
|
|
if binding.task_id != task_id:
|
|
raise BootstrapError("TASK_BINDING_IDENTITY_CONFLICT")
|
|
return binding
|
|
def get_binding(self, task_id: str) -> GovernedTaskBinding:
|
|
"""Read the exact durable binding; never synthesize missing lineage."""
|
|
binding = self._read_existing(str(task_id))
|
|
if binding is None or not binding.job_id:
|
|
raise BootstrapError("TASK_EXECUTION_PROJECTION_MISSING")
|
|
return binding
|
|
|
|
def execution_binding(self, binding: GovernedTaskBinding) -> GovernedExecutionBinding:
|
|
return GovernedExecutionBinding.from_mapping(
|
|
{
|
|
"task_id": binding.task_id,
|
|
"job_id": binding.job_id,
|
|
"repository": binding.repository,
|
|
"base_oid": binding.base_oid,
|
|
"expected_branch_oid": binding.expected_branch_oid,
|
|
"target_branch": binding.target_branch,
|
|
"branch_mode": binding.branch_mode,
|
|
"generation": binding.generation,
|
|
"idempotency_key": binding.idempotency_key,
|
|
}
|
|
)
|
|
|
|
def execution_envelope(
|
|
self,
|
|
task_id: str,
|
|
execution_request: Mapping[str, Any] | None = None,
|
|
*,
|
|
required_capabilities: tuple[str, ...] = (),
|
|
expected_job_id: str | None = None,
|
|
) -> dict[str, Any]:
|
|
binding = self.get_binding(task_id)
|
|
if expected_job_id and binding.job_id != expected_job_id:
|
|
raise BootstrapError("JOB_BINDING_CONFLICT")
|
|
return project_execution_envelope(
|
|
self.execution_binding(binding),
|
|
execution_request,
|
|
required_capabilities=required_capabilities,
|
|
)
|
|
|
|
def _write(self, binding: GovernedTaskBinding) -> None:
|
|
path = self._metadata(binding.task_id)
|
|
descriptor, temporary_name = tempfile.mkstemp(
|
|
prefix=".task-binding-", dir=self.task_meta_root,
|
|
)
|
|
temporary = Path(temporary_name)
|
|
try:
|
|
os.fchmod(descriptor, 0o600)
|
|
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as stream:
|
|
stream.write(json.dumps(asdict(binding), sort_keys=True) + "\n")
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
os.replace(temporary, path)
|
|
os.chmod(path, 0o600)
|
|
parent = os.open(self.task_meta_root, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
|
|
try:
|
|
os.fsync(parent)
|
|
finally:
|
|
os.close(parent)
|
|
except BaseException:
|
|
temporary.unlink(missing_ok=True)
|
|
raise
|
|
|
|
def _task_lock(self, task_id: str) -> int:
|
|
if fcntl is None:
|
|
raise BootstrapError("TASK_BINDING_LOCK_UNAVAILABLE")
|
|
path = self.lock_root / (task_storage_key(task_id) + ".lock")
|
|
descriptor = os.open(
|
|
path,
|
|
os.O_RDWR | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0),
|
|
0o600,
|
|
)
|
|
metadata = os.fstat(descriptor)
|
|
expected_uid = getattr(os, "geteuid", lambda: metadata.st_uid)()
|
|
if (
|
|
not stat.S_ISREG(metadata.st_mode)
|
|
or metadata.st_nlink != 1
|
|
or metadata.st_uid != expected_uid
|
|
):
|
|
os.close(descriptor)
|
|
raise BootstrapError("TASK_BINDING_LOCK_INVALID")
|
|
os.fchmod(descriptor, 0o600)
|
|
try:
|
|
fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
except OSError as exc:
|
|
os.close(descriptor)
|
|
raise BootstrapError("TASK_BINDING_BUSY") from exc
|
|
return descriptor
|
|
|
|
@staticmethod
|
|
def _unlock_task(descriptor: int) -> None:
|
|
if fcntl is not None:
|
|
fcntl.flock(descriptor, fcntl.LOCK_UN)
|
|
os.close(descriptor)
|
|
|
|
def create(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
|
values = {key: str(request[key]) for key in ("task_id", "repository", "base_oid", "target_branch", "risk", "description", "idempotency_key")}
|
|
if values["repository"] != "aios/aios" or not OID_RE.fullmatch(values["base_oid"]): raise BootstrapError("TASK_BINDING_INVALID")
|
|
if not BRANCH_RE.fullmatch(values["target_branch"]): raise BootstrapError("TARGET_BRANCH_INVALID")
|
|
descriptor = self._task_lock(values["task_id"])
|
|
try:
|
|
existing = self._read_existing(values["task_id"])
|
|
if existing is not None:
|
|
if any(getattr(existing, key) != values[key] for key in ("repository", "base_oid", "target_branch", "risk", "description", "idempotency_key")): raise BootstrapError("TASK_BINDING_CONFLICT")
|
|
expected = str(request.get('expected_branch_oid') or values['base_oid'])
|
|
payload = json.dumps(values, sort_keys=True, separators=(',', ':')).encode()
|
|
_, controller = self._store(values['task_id'])
|
|
_fl_submit_receipt = controller.submit({'idempotency_key': values['idempotency_key'], 'campaign_id': values['task_id'], 'task_id': values['task_id'], 'repository': values['repository'], 'operation': 'GOVERNED_TASK_CREATE', 'branch': values['target_branch'], 'expected_branch_oid': expected, 'payload_digest': hashlib.sha256(payload).hexdigest(), 'lineage_anchor_oid': values['base_oid']}, payload=payload)
|
|
_fl_receipt = _formal_lineage_reservation_receipt(
|
|
_fl_submit_receipt, request_idempotency_key=request["idempotency_key"],
|
|
expected_side_effect_state='SAME_JOB'
|
|
)
|
|
_fl_response = existing.response(idempotent=True)
|
|
return _formal_lineage_attach_receipt(_fl_response,_fl_receipt)
|
|
if not self.authority.commit_exists(values["base_oid"]): raise BootstrapError("BASE_COMMIT_NOT_FOUND")
|
|
current = self.authority.read_ref("refs/heads/" + values["target_branch"])
|
|
expected = str(request.get("expected_branch_oid") or values["base_oid"])
|
|
if current is None:
|
|
mode, generation = "CREATE_NEW", 1
|
|
if expected != values["base_oid"]: raise BootstrapError("EXPECTED_BASE_CONFLICT")
|
|
else:
|
|
if expected != current: raise BootstrapError("EXISTING_BRANCH_OID_CONFLICT")
|
|
mode, generation = "CONTINUE_EXISTING", int(request.get("generation", 2))
|
|
if generation < 1: raise BootstrapError("GENERATION_INVALID")
|
|
payload = json.dumps(values, sort_keys=True, separators=(",", ":")).encode()
|
|
_, controller = self._store(values["task_id"])
|
|
expected_job_id = controller.internal_job_id_for_idempotency_key(values["idempotency_key"])
|
|
with controller.store._connect() as connection:
|
|
bootstrap_jobs = connection.execute(
|
|
"SELECT job_id FROM jobs WHERE operation='GOVERNED_TASK_CREATE' ORDER BY job_id"
|
|
).fetchall()
|
|
if bootstrap_jobs and [str(row[0]) for row in bootstrap_jobs] != [expected_job_id]:
|
|
raise BootstrapError("TASK_BINDING_RECOVERY_CONFLICT")
|
|
receipt = controller.submit({"idempotency_key": values["idempotency_key"], "campaign_id": values["task_id"], "task_id": values["task_id"], "repository": values["repository"], "operation": "GOVERNED_TASK_CREATE", "branch": values["target_branch"], "expected_branch_oid": expected, "payload_digest": hashlib.sha256(payload).hexdigest(), "lineage_anchor_oid": values["base_oid"]}, payload=payload)
|
|
binding = GovernedTaskBinding(**values, expected_branch_oid=expected, branch_mode=mode, generation=generation, job_id=receipt.job_id)
|
|
self._write(binding)
|
|
_fl_receipt = _formal_lineage_reservation_receipt(
|
|
receipt, request_idempotency_key=request["idempotency_key"],
|
|
expected_side_effect_state="QUEUED"
|
|
)
|
|
_fl_response = binding.response()
|
|
return _formal_lineage_attach_receipt(_fl_response,_fl_receipt)
|
|
finally:
|
|
self._unlock_task(descriptor)
|