398 lines
16 KiB
Python
398 lines
16 KiB
Python
"""Canonical governed execution projection for Online Agent Tasks.
|
|
|
|
The durable governed binding is authoritative. Natural-language task fields and
|
|
worker configuration are never allowed to invent repository, ref, generation, or
|
|
idempotency coordinates.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import asdict, dataclass
|
|
import hashlib
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any, Mapping, Sequence
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
|
|
EXECUTION_SCHEMA = "aios.online-agent.execution.v2"
|
|
PROJECTION_SCHEMA = "aios.online-agent.task-projection.v1"
|
|
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
|
TASK_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{2,179}$")
|
|
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
|
|
KEY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{7,159}$")
|
|
PROTECTED_PREFIXES = ("main", "master", "refs/", "tags/", "tag/", "release/", "baseline/", "production/")
|
|
MUTATION_ACTIONS = frozenset({"deterministic"})
|
|
SUPPORTED_ACTIONS = frozenset({"deterministic", "source_capture"})
|
|
|
|
|
|
class ExecutionProjectionError(RuntimeError):
|
|
"""A canonical fail-closed projection or validation error."""
|
|
|
|
|
|
class OnlineTaskProjectionClient:
|
|
"""Official HTTP projection client using only a control credential reference."""
|
|
|
|
def __init__(self, api_base: str, control_token_path: Path) -> None:
|
|
self.api_base = api_base.rstrip("/")
|
|
self.control_token_path = Path(control_token_path)
|
|
|
|
def _token(self) -> str:
|
|
path = self.control_token_path
|
|
if path.is_symlink() or not path.is_file():
|
|
_error("ONLINE_TASK_PROJECTION_CONTROL_UNAVAILABLE")
|
|
value = path.read_text(encoding="utf-8").strip()
|
|
if len(value) < 32:
|
|
_error("ONLINE_TASK_PROJECTION_CONTROL_INVALID")
|
|
return value
|
|
|
|
def create(
|
|
self,
|
|
projected_task: Mapping[str, Any],
|
|
*,
|
|
binding: "GovernedExecutionBinding",
|
|
) -> dict[str, Any]:
|
|
body = dict(projected_task)
|
|
payload = _canonical(body).encode("utf-8")
|
|
request = urllib.request.Request(
|
|
self.api_base + "/v1/tasks",
|
|
method="POST",
|
|
data=payload,
|
|
headers={
|
|
"Authorization": "Bearer " + self._token(),
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json",
|
|
},
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=15) as response:
|
|
result = json.loads(response.read().decode("utf-8"))
|
|
status = response.status
|
|
except urllib.error.HTTPError as exc:
|
|
raw = exc.read(8192).decode("utf-8", "replace")
|
|
try:
|
|
error = json.loads(raw).get("error")
|
|
except (AttributeError, json.JSONDecodeError):
|
|
error = "HTTP_ERROR"
|
|
raise ExecutionProjectionError(f"ONLINE_TASK_PROJECTION_FAILED:{exc.code}:{error}") from exc
|
|
except (OSError, urllib.error.URLError, TimeoutError) as exc:
|
|
raise ExecutionProjectionError("ONLINE_TASK_PROJECTION_UNAVAILABLE") from exc
|
|
if status not in {200, 201} or not isinstance(result, dict):
|
|
_error("ONLINE_TASK_PROJECTION_RESPONSE_INVALID")
|
|
if result.get("task_id") != binding.task_id or result.get("status") not in {"QUEUED", "VERIFYING", "SUCCEEDED"}:
|
|
_error("ONLINE_TASK_PROJECTION_RESPONSE_INVALID")
|
|
metadata = result.get("metadata")
|
|
if not isinstance(metadata, Mapping):
|
|
_error("ONLINE_TASK_PROJECTION_RESPONSE_INVALID")
|
|
returned_execution = validate_execution_envelope(
|
|
_object(metadata.get("execution"), "ONLINE_TASK_PROJECTION_RESPONSE_INVALID"),
|
|
binding=binding,
|
|
)
|
|
submitted_metadata = _object(body.get("metadata"), "ONLINE_TASK_PROJECTION_REQUEST_INVALID")
|
|
submitted_execution = validate_execution_envelope(
|
|
_object(submitted_metadata.get("execution"), "ONLINE_TASK_PROJECTION_REQUEST_INVALID"),
|
|
binding=binding,
|
|
)
|
|
if _canonical(returned_execution) != _canonical(submitted_execution):
|
|
_error("ONLINE_TASK_PROJECTION_RESPONSE_BINDING_INVALID")
|
|
return result
|
|
|
|
|
|
def _error(code: str) -> None:
|
|
raise ExecutionProjectionError(code)
|
|
|
|
|
|
def _object(value: object, code: str) -> dict[str, Any]:
|
|
if not isinstance(value, Mapping):
|
|
_error(code)
|
|
return dict(value)
|
|
|
|
|
|
def _string(value: object, code: str) -> str:
|
|
if not isinstance(value, str) or not value.strip():
|
|
_error(code)
|
|
return value.strip()
|
|
|
|
|
|
def _task_id(value: object) -> str:
|
|
value = _string(value, "TASK_ID_INVALID")
|
|
if TASK_RE.fullmatch(value) is None:
|
|
_error("TASK_ID_INVALID")
|
|
return value
|
|
|
|
|
|
def _oid(value: object, code: str) -> str:
|
|
value = _string(value, code)
|
|
if OID_RE.fullmatch(value) is None:
|
|
_error(code)
|
|
return value
|
|
|
|
|
|
def _branch(value: object) -> str:
|
|
value = _string(value, "TARGET_BRANCH_INVALID")
|
|
if BRANCH_RE.fullmatch(value) is None or value.startswith(PROTECTED_PREFIXES):
|
|
_error("TARGET_BRANCH_INVALID")
|
|
return value
|
|
|
|
|
|
def _generation(value: object) -> int:
|
|
if not isinstance(value, int) or isinstance(value, bool) or value < 1:
|
|
_error("GENERATION_INVALID")
|
|
return value
|
|
|
|
|
|
def _canonical(value: object) -> str:
|
|
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
|
|
|
|
def _digest(value: object) -> str:
|
|
return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
|
|
|
|
|
|
def _source_signals(value: object) -> tuple[str, ...]:
|
|
"""Normalize untrusted natural-language hints before they enter a signed envelope."""
|
|
if value is None:
|
|
return ()
|
|
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
|
|
_error("SOURCE_CAPTURE_SIGNALS_INVALID")
|
|
result: list[str] = []
|
|
for item in value:
|
|
if not isinstance(item, str):
|
|
_error("SOURCE_CAPTURE_SIGNALS_INVALID")
|
|
text = " ".join(item.split()).strip()
|
|
if not text:
|
|
continue
|
|
# Projection may accept long task prose, but execution envelopes stay bounded.
|
|
text = text[:512].rstrip()
|
|
if text not in result:
|
|
result.append(text)
|
|
if len(result) == 16:
|
|
break
|
|
return tuple(result)
|
|
|
|
|
|
def _capabilities(value: object) -> tuple[str, ...]:
|
|
if value is None:
|
|
return ()
|
|
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
|
|
_error("CAPABILITY_SCOPE_INVALID")
|
|
result = tuple(sorted({_string(item, "CAPABILITY_SCOPE_INVALID") for item in value}))
|
|
if any(len(item) > 120 for item in result):
|
|
_error("CAPABILITY_SCOPE_INVALID")
|
|
return result
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class GovernedExecutionBinding:
|
|
task_id: str
|
|
job_id: str
|
|
repository: str
|
|
base_oid: str
|
|
expected_branch_oid: str
|
|
target_branch: str
|
|
branch_mode: str
|
|
generation: int
|
|
idempotency_key: str
|
|
|
|
@classmethod
|
|
def from_mapping(cls, value: Mapping[str, Any]) -> "GovernedExecutionBinding":
|
|
data = dict(value)
|
|
task_id = _task_id(data.get("task_id"))
|
|
job_id = _string(data.get("job_id"), "JOB_ID_REQUIRED")
|
|
if TASK_RE.fullmatch(job_id) is None:
|
|
_error("JOB_ID_INVALID")
|
|
repository = _string(data.get("repository"), "REPOSITORY_INVALID")
|
|
if repository != "aios/aios":
|
|
_error("WRONG_REPOSITORY_ENVELOPE_DENY")
|
|
base_oid = _oid(data.get("base_oid"), "BASE_OID_INVALID")
|
|
expected = _oid(data.get("expected_branch_oid"), "EXPECTED_BRANCH_OID_INVALID")
|
|
target = _branch(data.get("target_branch"))
|
|
mode = _string(data.get("branch_mode"), "BRANCH_MODE_MISSING")
|
|
if mode not in {"CREATE_NEW", "CONTINUE_EXISTING"}:
|
|
_error("BRANCH_MODE_INVALID")
|
|
generation = _generation(data.get("generation"))
|
|
if mode == "CREATE_NEW" and expected != base_oid:
|
|
_error("EXPECTED_BASE_CONFLICT")
|
|
key = _string(data.get("idempotency_key"), "IDEMPOTENCY_KEY_INVALID")
|
|
if KEY_RE.fullmatch(key) is None:
|
|
_error("IDEMPOTENCY_KEY_INVALID")
|
|
return cls(task_id, job_id, repository, base_oid, expected, target, mode, generation, key)
|
|
|
|
def public(self) -> dict[str, Any]:
|
|
return asdict(self)
|
|
|
|
|
|
def binding_from_bootstrap(value: Mapping[str, Any]) -> GovernedExecutionBinding:
|
|
return GovernedExecutionBinding.from_mapping(value)
|
|
|
|
|
|
def validate_execution_envelope(
|
|
value: Mapping[str, Any],
|
|
*,
|
|
binding: GovernedExecutionBinding | None = None,
|
|
worker_capabilities: Sequence[str] | None = None,
|
|
) -> dict[str, Any]:
|
|
envelope = dict(value)
|
|
if envelope.get("schema") != EXECUTION_SCHEMA:
|
|
_error("EXECUTION_ENVELOPE_SCHEMA_VALIDATION")
|
|
candidate = GovernedExecutionBinding.from_mapping(envelope)
|
|
if binding is not None and candidate != binding:
|
|
comparisons = (
|
|
(candidate.task_id, binding.task_id, "TASK_BINDING_CONFLICT"),
|
|
(candidate.job_id, binding.job_id, "JOB_BINDING_CONFLICT"),
|
|
(candidate.repository, binding.repository, "WRONG_REPOSITORY_ENVELOPE_DENY"),
|
|
(candidate.base_oid, binding.base_oid, "WRONG_BASE_ENVELOPE_DENY"),
|
|
(candidate.expected_branch_oid, binding.expected_branch_oid, "WRONG_EXPECTED_HEAD_ENVELOPE_DENY"),
|
|
(candidate.target_branch, binding.target_branch, "WRONG_BRANCH_ENVELOPE_DENY"),
|
|
(candidate.generation, binding.generation, "WRONG_GENERATION_ENVELOPE_DENY"),
|
|
(candidate.branch_mode, binding.branch_mode, "BRANCH_MODE_BINDING_CONFLICT"),
|
|
(candidate.idempotency_key, binding.idempotency_key, "IDEMPOTENCY_BINDING_CONFLICT"),
|
|
)
|
|
for actual, expected, code in comparisons:
|
|
if actual != expected:
|
|
_error(code)
|
|
action = _string(envelope.get("action"), "EXECUTION_ACTION_MISSING")
|
|
if action not in SUPPORTED_ACTIONS:
|
|
_error("EXECUTION_ACTION_UNSUPPORTED")
|
|
capability_scope = _capabilities(envelope.get("capability_scope"))
|
|
if worker_capabilities is not None and not set(capability_scope).issubset(set(worker_capabilities)):
|
|
_error("CAPABILITY_MISMATCH_DENY")
|
|
workspace = _object(envelope.get("workspace"), "WORKSPACE_POLICY_REQUIRED")
|
|
if workspace.get("authority_git_dir") != "/data/aios-gitops/repo.git" or workspace.get("read_only_repository") is not True:
|
|
_error("WORKSPACE_POLICY_DENY")
|
|
governance = _object(envelope.get("governance"), "GOVERNANCE_POLICY_REQUIRED")
|
|
governed_coordinates = (
|
|
(governance.get("task_id"), candidate.task_id, "TASK_BINDING_CONFLICT"),
|
|
(governance.get("job_id"), candidate.job_id, "JOB_BINDING_CONFLICT"),
|
|
(governance.get("idempotency_key"), candidate.idempotency_key, "IDEMPOTENCY_BINDING_CONFLICT"),
|
|
)
|
|
for actual_coordinate, expected_coordinate, code in governed_coordinates:
|
|
if actual_coordinate != expected_coordinate:
|
|
_error(code)
|
|
commit_key = _string(governance.get("commit_idempotency_key"), "COMMIT_IDEMPOTENCY_KEY_INVALID")
|
|
if KEY_RE.fullmatch(commit_key) is None:
|
|
_error("COMMIT_IDEMPOTENCY_KEY_INVALID")
|
|
if governance.get("writer_lease") is not (action in MUTATION_ACTIONS):
|
|
_error("WRITER_LEASE_POLICY_DENY")
|
|
for field in ("main_write", "production_write", "tag_write", "baseline_write", "force_push", "protected_ref_bypass"):
|
|
if governance.get(field, False) is not False:
|
|
_error("POLICY_DENY")
|
|
changes = envelope.get("change_set")
|
|
verification = envelope.get("verification")
|
|
if not isinstance(changes, list) or not isinstance(verification, list):
|
|
_error("EXECUTION_OPERATIONS_INVALID")
|
|
if action == "source_capture":
|
|
if changes or verification:
|
|
_error("SOURCE_CAPTURE_MUTATION_DENY")
|
|
signals = list(_source_signals(envelope.get("source_signals") or []))
|
|
if envelope.get("source_signals") != signals:
|
|
_error("SOURCE_CAPTURE_SIGNALS_INVALID")
|
|
elif not changes:
|
|
_error("CHANGE_SET_INVALID")
|
|
declared = str(envelope.get("execution_digest") or "")
|
|
material = {key: item for key, item in envelope.items() if key != "execution_digest"}
|
|
actual = _digest(material)
|
|
if declared != actual:
|
|
_error("EXECUTION_DIGEST_INVALID")
|
|
return {**material, "execution_digest": actual}
|
|
|
|
|
|
def project_execution_envelope(
|
|
binding: GovernedExecutionBinding,
|
|
execution_request: Mapping[str, Any] | None = None,
|
|
*,
|
|
required_capabilities: Sequence[str] = (),
|
|
) -> dict[str, Any]:
|
|
request = dict(execution_request or {})
|
|
action = str(request.get("action") or "source_capture")
|
|
if action not in SUPPORTED_ACTIONS:
|
|
_error("EXECUTION_ACTION_UNSUPPORTED")
|
|
capabilities = _capabilities(request.get("capability_scope", required_capabilities))
|
|
governance = {
|
|
"task_id": binding.task_id,
|
|
"job_id": binding.job_id,
|
|
"idempotency_key": binding.idempotency_key,
|
|
"commit_idempotency_key": str(request.get("commit_idempotency_key") or f"{binding.task_id}-commit-g{binding.generation}"),
|
|
"writer_lease": action in MUTATION_ACTIONS,
|
|
"main_write": False,
|
|
"production_write": False,
|
|
"tag_write": False,
|
|
"baseline_write": False,
|
|
"force_push": False,
|
|
"protected_ref_bypass": False,
|
|
}
|
|
supplied_governance = request.get("governance")
|
|
if supplied_governance is not None:
|
|
governance.update(_object(supplied_governance, "GOVERNANCE_POLICY_REQUIRED"))
|
|
material: dict[str, Any] = {
|
|
"schema": EXECUTION_SCHEMA,
|
|
**binding.public(),
|
|
"action": action,
|
|
"adapter": str(request.get("adapter") or ("deterministic" if action == "deterministic" else "source_capture")),
|
|
"capability_scope": list(capabilities),
|
|
"change_set": list(request.get("change_set") or []),
|
|
"verification": list(request.get("verification") or []),
|
|
"source_signals": list(_source_signals(request.get("source_signals") or [])) if action == "source_capture" else [],
|
|
"governance": governance,
|
|
"workspace": {
|
|
"authority_git_dir": "/data/aios-gitops/repo.git",
|
|
"read_only_repository": True,
|
|
"cleanup_policy": "always",
|
|
},
|
|
}
|
|
envelope = {**material, "execution_digest": _digest(material)}
|
|
return validate_execution_envelope(envelope, binding=binding)
|
|
|
|
|
|
def project_online_task(
|
|
binding: GovernedExecutionBinding,
|
|
task_request: Mapping[str, Any],
|
|
execution_request: Mapping[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
request = dict(task_request)
|
|
required = _capabilities(request.get("required_capabilities"))
|
|
execution = project_execution_envelope(binding, execution_request, required_capabilities=required)
|
|
metadata = dict(request.get("metadata") or {})
|
|
if "execution" in metadata:
|
|
validate_execution_envelope(_object(metadata["execution"], "EXECUTION_ENVELOPE_INVALID"), binding=binding)
|
|
if metadata["execution"] != execution:
|
|
_error("EXECUTION_PROJECTION_CONFLICT")
|
|
metadata["execution"] = execution
|
|
metadata["execution_projection"] = {
|
|
"schema": PROJECTION_SCHEMA,
|
|
"task_id": binding.task_id,
|
|
"job_id": binding.job_id,
|
|
"execution_digest": execution["execution_digest"],
|
|
}
|
|
return {
|
|
**request,
|
|
"task_id": binding.task_id,
|
|
"metadata": metadata,
|
|
"required_capabilities": list(required),
|
|
"idempotency_key": str(request.get("idempotency_key") or f"online-task:{binding.idempotency_key}"),
|
|
}
|
|
|
|
|
|
def resolve_task_execution(
|
|
task: Mapping[str, Any],
|
|
binding: GovernedExecutionBinding,
|
|
*,
|
|
worker_capabilities: Sequence[str] | None = None,
|
|
) -> tuple[dict[str, Any], str]:
|
|
metadata = task.get("metadata")
|
|
if isinstance(metadata, Mapping) and "execution" in metadata:
|
|
value = validate_execution_envelope(
|
|
_object(metadata["execution"], "EXECUTION_ENVELOPE_INVALID"),
|
|
binding=binding,
|
|
worker_capabilities=worker_capabilities,
|
|
)
|
|
return value, "persisted"
|
|
projected = project_execution_envelope(
|
|
binding,
|
|
{"action": "source_capture", "capability_scope": task.get("required_capabilities") or []},
|
|
required_capabilities=task.get("required_capabilities") or [],
|
|
)
|
|
validate_execution_envelope(projected, binding=binding, worker_capabilities=worker_capabilities)
|
|
return projected, "recovered_projection"
|