1382 lines
57 KiB
Python
1382 lines
57 KiB
Python
"""Owner-controlled CAS-bound initial execution projection for Online Agent Tasks.
|
|
|
|
This recovery primitive is intentionally narrower than a metadata patch API. It
|
|
only projects an ``agent_runtime`` envelope onto an existing, untouched, bare
|
|
Task after resolving the Task's durable governed binding through the canonical
|
|
Bridge. It never creates Tasks, Jobs, branches, claims, heartbeats, or results.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
import hashlib
|
|
import hmac
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import sqlite3
|
|
from typing import Any, Callable, Mapping
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
from aios_online_agent_execution import (
|
|
GovernedExecutionBinding,
|
|
validate_execution_envelope,
|
|
)
|
|
from aios_agent_task_projection_recovery_http import get_projection as get_canonical_projection, recover_from_json as recover_canonical_projection
|
|
|
|
|
|
TASK_RE = re.compile(r"^[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}$")
|
|
SHA_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
JOB_RE = TASK_RE
|
|
RUNTIME_CAPABILITY_RE = re.compile(r"^agent\.runtime\.(hermes|openclaw)$")
|
|
MAX_BODY = 64 * 1024
|
|
AUTHORIZATION_ID_ENV = "AIOS_AGENT_TASK_INITIAL_EXECUTION_PROJECTION_AUTHORIZATION_ID"
|
|
|
|
|
|
class InitialExecutionProjectionError(RuntimeError):
|
|
def __init__(self, code: str, status: int = 409) -> None:
|
|
super().__init__(code)
|
|
self.code = code
|
|
self.status = status
|
|
|
|
|
|
def _canonical(value: object) -> bytes:
|
|
return json.dumps(
|
|
value, sort_keys=True, separators=(",", ":"), ensure_ascii=False
|
|
).encode("utf-8")
|
|
|
|
|
|
def _sha(value: object) -> str:
|
|
return hashlib.sha256(_canonical(value)).hexdigest()
|
|
|
|
|
|
def _now() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def _require_string(
|
|
value: object,
|
|
code: str,
|
|
pattern: re.Pattern[str] | None = None,
|
|
) -> str:
|
|
if not isinstance(value, str) or not value.strip():
|
|
raise InitialExecutionProjectionError(code, 400)
|
|
result = value.strip()
|
|
if pattern is not None and pattern.fullmatch(result) is None:
|
|
raise InitialExecutionProjectionError(code, 400)
|
|
return result
|
|
|
|
|
|
def _required_authorization_id() -> str:
|
|
value = os.environ.get(AUTHORIZATION_ID_ENV, "").strip()
|
|
if not value:
|
|
raise RuntimeError("INITIAL_EXECUTION_PROJECTION_AUTHORIZATION_ID_MISSING")
|
|
if KEY_RE.fullmatch(value) is None:
|
|
raise RuntimeError("INITIAL_EXECUTION_PROJECTION_AUTHORIZATION_ID_INVALID")
|
|
return value
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class InitialExecutionProjectionRequest:
|
|
expected_status: str
|
|
expected_revision: int
|
|
expected_attempt: int
|
|
expected_metadata_sha256: str
|
|
expected_required_capabilities_sha256: str
|
|
expected_job_id: str
|
|
expected_execution_digest: str
|
|
required_capability: str
|
|
adapter: str
|
|
authorization_id: str
|
|
idempotency_key: str
|
|
reason: str
|
|
|
|
@classmethod
|
|
def from_mapping(
|
|
cls, value: Mapping[str, Any]
|
|
) -> "InitialExecutionProjectionRequest":
|
|
allowed = {
|
|
"expected_status",
|
|
"expected_revision",
|
|
"expected_attempt",
|
|
"expected_metadata_sha256",
|
|
"expected_required_capabilities_sha256",
|
|
"expected_job_id",
|
|
"expected_execution_digest",
|
|
"required_capability",
|
|
"adapter",
|
|
"authorization_id",
|
|
"idempotency_key",
|
|
"reason",
|
|
}
|
|
if set(value) != allowed:
|
|
raise InitialExecutionProjectionError(
|
|
"INITIAL_EXECUTION_PROJECTION_FIELDS_INVALID", 400
|
|
)
|
|
status = _require_string(value["expected_status"], "EXPECTED_STATUS_INVALID")
|
|
if status != "QUEUED":
|
|
raise InitialExecutionProjectionError("EXPECTED_STATUS_INVALID", 400)
|
|
revision = value["expected_revision"]
|
|
attempt = value["expected_attempt"]
|
|
if (
|
|
not isinstance(revision, int)
|
|
or isinstance(revision, bool)
|
|
or revision != 1
|
|
or not isinstance(attempt, int)
|
|
or isinstance(attempt, bool)
|
|
or attempt != 0
|
|
):
|
|
raise InitialExecutionProjectionError(
|
|
"INITIAL_EXECUTION_PROJECTION_INITIAL_STATE_REQUIRED", 400
|
|
)
|
|
metadata_sha = _require_string(
|
|
value["expected_metadata_sha256"], "METADATA_SHA256_INVALID", SHA_RE
|
|
)
|
|
capabilities_sha = _require_string(
|
|
value["expected_required_capabilities_sha256"],
|
|
"CAPABILITIES_SHA256_INVALID",
|
|
SHA_RE,
|
|
)
|
|
job_id = _require_string(value["expected_job_id"], "JOB_ID_INVALID", JOB_RE)
|
|
digest = _require_string(
|
|
value["expected_execution_digest"], "EXECUTION_DIGEST_INVALID", SHA_RE
|
|
)
|
|
capability = _require_string(
|
|
value["required_capability"], "RUNTIME_CAPABILITY_INVALID",
|
|
RUNTIME_CAPABILITY_RE,
|
|
)
|
|
adapter = _require_string(value["adapter"], "RUNTIME_ADAPTER_INVALID")
|
|
if capability != f"agent.runtime.{adapter}" or adapter not in {"hermes", "openclaw"}:
|
|
raise InitialExecutionProjectionError("RUNTIME_CAPABILITY_MISMATCH", 400)
|
|
authorization_id = _require_string(
|
|
value["authorization_id"], "AUTHORIZATION_ID_INVALID", KEY_RE
|
|
)
|
|
idempotency_key = _require_string(
|
|
value["idempotency_key"], "IDEMPOTENCY_KEY_INVALID", KEY_RE
|
|
)
|
|
reason = _require_string(value["reason"], "PROJECTION_REASON_INVALID")
|
|
if len(reason) > 512:
|
|
raise InitialExecutionProjectionError("PROJECTION_REASON_INVALID", 400)
|
|
return cls(
|
|
status,
|
|
revision,
|
|
attempt,
|
|
metadata_sha,
|
|
capabilities_sha,
|
|
job_id,
|
|
digest,
|
|
capability,
|
|
adapter,
|
|
authorization_id,
|
|
idempotency_key,
|
|
reason,
|
|
)
|
|
|
|
def public(self) -> dict[str, Any]:
|
|
return {
|
|
"expected_status": self.expected_status,
|
|
"expected_revision": self.expected_revision,
|
|
"expected_attempt": self.expected_attempt,
|
|
"expected_metadata_sha256": self.expected_metadata_sha256,
|
|
"expected_required_capabilities_sha256": self.expected_required_capabilities_sha256,
|
|
"expected_job_id": self.expected_job_id,
|
|
"expected_execution_digest": self.expected_execution_digest,
|
|
"required_capability": self.required_capability,
|
|
"adapter": self.adapter,
|
|
"authorization_id": self.authorization_id,
|
|
"idempotency_key": self.idempotency_key,
|
|
"reason": self.reason,
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SourceCaptureProjectionRecoveryRequest:
|
|
"""CAS contract for the first durable runtime projection after source capture.
|
|
|
|
Some deployed Bridge versions materialize a binding by letting the generic
|
|
worker perform one read-only ``source_capture`` attempt. This request does
|
|
not relax the pristine initial-projection contract above. It authorizes a
|
|
separate, evidence-bound transition only after that attempt has been
|
|
officially verified as ``NEEDS_REWORK`` and returned to ``QUEUED``.
|
|
"""
|
|
|
|
expected_status: str
|
|
expected_revision: int
|
|
expected_attempt: int
|
|
expected_metadata_sha256: str
|
|
expected_required_capabilities_sha256: str
|
|
previous_verification_sha256: str
|
|
expected_source_capture_result_sha256: str
|
|
expected_source_capture_execution_digest: str
|
|
expected_job_id: str
|
|
expected_execution_digest: str
|
|
required_capability: str
|
|
adapter: str
|
|
authorization_id: str
|
|
idempotency_key: str
|
|
reason: str
|
|
|
|
@classmethod
|
|
def from_mapping(
|
|
cls, value: Mapping[str, Any]
|
|
) -> "SourceCaptureProjectionRecoveryRequest":
|
|
allowed = {
|
|
"expected_status",
|
|
"expected_revision",
|
|
"expected_attempt",
|
|
"expected_metadata_sha256",
|
|
"expected_required_capabilities_sha256",
|
|
"previous_verification_sha256",
|
|
"expected_source_capture_result_sha256",
|
|
"expected_source_capture_execution_digest",
|
|
"expected_job_id",
|
|
"expected_execution_digest",
|
|
"required_capability",
|
|
"adapter",
|
|
"authorization_id",
|
|
"idempotency_key",
|
|
"reason",
|
|
}
|
|
if set(value) != allowed:
|
|
raise InitialExecutionProjectionError(
|
|
"SOURCE_CAPTURE_PROJECTION_RECOVERY_FIELDS_INVALID", 400
|
|
)
|
|
status = _require_string(value["expected_status"], "EXPECTED_STATUS_INVALID")
|
|
revision = value["expected_revision"]
|
|
attempt = value["expected_attempt"]
|
|
if (
|
|
status != "QUEUED"
|
|
or not isinstance(revision, int)
|
|
or isinstance(revision, bool)
|
|
or revision < 2
|
|
or not isinstance(attempt, int)
|
|
or isinstance(attempt, bool)
|
|
or attempt < 1
|
|
):
|
|
raise InitialExecutionProjectionError(
|
|
"SOURCE_CAPTURE_PROJECTION_RECOVERY_STATE_INVALID", 400
|
|
)
|
|
digest_fields = {
|
|
"expected_metadata_sha256": "METADATA_SHA256_INVALID",
|
|
"expected_required_capabilities_sha256": "CAPABILITIES_SHA256_INVALID",
|
|
"previous_verification_sha256": "VERIFICATION_SHA256_INVALID",
|
|
"expected_source_capture_result_sha256": "SOURCE_CAPTURE_RESULT_SHA256_INVALID",
|
|
"expected_source_capture_execution_digest": "SOURCE_CAPTURE_EXECUTION_DIGEST_INVALID",
|
|
"expected_execution_digest": "EXECUTION_DIGEST_INVALID",
|
|
}
|
|
digests: dict[str, str] = {}
|
|
for name, code in digest_fields.items():
|
|
digests[name] = _require_string(value[name], code, SHA_RE)
|
|
job_id = _require_string(value["expected_job_id"], "JOB_ID_INVALID", JOB_RE)
|
|
capability = _require_string(
|
|
value["required_capability"],
|
|
"RUNTIME_CAPABILITY_INVALID",
|
|
RUNTIME_CAPABILITY_RE,
|
|
)
|
|
adapter = _require_string(value["adapter"], "RUNTIME_ADAPTER_INVALID")
|
|
if capability != f"agent.runtime.{adapter}" or adapter not in {"hermes", "openclaw"}:
|
|
raise InitialExecutionProjectionError("RUNTIME_CAPABILITY_MISMATCH", 400)
|
|
authorization_id = _require_string(
|
|
value["authorization_id"], "AUTHORIZATION_ID_INVALID", KEY_RE
|
|
)
|
|
idempotency_key = _require_string(
|
|
value["idempotency_key"], "IDEMPOTENCY_KEY_INVALID", KEY_RE
|
|
)
|
|
reason = _require_string(value["reason"], "PROJECTION_REASON_INVALID")
|
|
if len(reason) > 512:
|
|
raise InitialExecutionProjectionError("PROJECTION_REASON_INVALID", 400)
|
|
return cls(
|
|
status,
|
|
revision,
|
|
attempt,
|
|
digests["expected_metadata_sha256"],
|
|
digests["expected_required_capabilities_sha256"],
|
|
digests["previous_verification_sha256"],
|
|
digests["expected_source_capture_result_sha256"],
|
|
digests["expected_source_capture_execution_digest"],
|
|
job_id,
|
|
digests["expected_execution_digest"],
|
|
capability,
|
|
adapter,
|
|
authorization_id,
|
|
idempotency_key,
|
|
reason,
|
|
)
|
|
|
|
def public(self) -> dict[str, Any]:
|
|
return {
|
|
"expected_status": self.expected_status,
|
|
"expected_revision": self.expected_revision,
|
|
"expected_attempt": self.expected_attempt,
|
|
"expected_metadata_sha256": self.expected_metadata_sha256,
|
|
"expected_required_capabilities_sha256": self.expected_required_capabilities_sha256,
|
|
"previous_verification_sha256": self.previous_verification_sha256,
|
|
"expected_source_capture_result_sha256": self.expected_source_capture_result_sha256,
|
|
"expected_source_capture_execution_digest": self.expected_source_capture_execution_digest,
|
|
"expected_job_id": self.expected_job_id,
|
|
"expected_execution_digest": self.expected_execution_digest,
|
|
"required_capability": self.required_capability,
|
|
"adapter": self.adapter,
|
|
"authorization_id": self.authorization_id,
|
|
"idempotency_key": self.idempotency_key,
|
|
"reason": self.reason,
|
|
}
|
|
|
|
|
|
class CanonicalBridgeInitialExecutionResolver:
|
|
"""Resolve one agent-runtime envelope from the canonical governed Bridge."""
|
|
|
|
def __init__(self, base_url: str, control_token: str) -> None:
|
|
self.base_url = base_url.rstrip("/")
|
|
self.control_token = control_token
|
|
|
|
def __call__(
|
|
self,
|
|
task_id: str,
|
|
expected_job_id: str,
|
|
required_capability: str,
|
|
adapter: str,
|
|
runtime_input: Mapping[str, Any],
|
|
) -> dict[str, Any]:
|
|
body = {
|
|
"task_id": task_id,
|
|
"job_id": expected_job_id,
|
|
"required_capabilities": [required_capability],
|
|
"execution": {
|
|
"action": "agent_runtime",
|
|
"adapter": adapter,
|
|
"capability_scope": [required_capability],
|
|
"runtime_input": dict(runtime_input),
|
|
},
|
|
}
|
|
request = urllib.request.Request(
|
|
self.base_url + "/v2/execution-envelope",
|
|
method="POST",
|
|
data=_canonical(body),
|
|
headers={
|
|
"Authorization": "Bearer " + self.control_token,
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json",
|
|
},
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=15) as response:
|
|
raw, status = response.read(), response.status
|
|
except urllib.error.HTTPError as exc:
|
|
raw, status = exc.read(), exc.code
|
|
try:
|
|
value = json.loads(raw.decode("utf-8"))
|
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
raise InitialExecutionProjectionError(
|
|
"CANONICAL_EXECUTION_RESPONSE_INVALID", 502
|
|
) from exc
|
|
if not isinstance(value, Mapping):
|
|
raise InitialExecutionProjectionError(
|
|
"CANONICAL_EXECUTION_RESPONSE_INVALID", 502
|
|
)
|
|
if status != 200:
|
|
raise InitialExecutionProjectionError(
|
|
str(value.get("error") or "CANONICAL_EXECUTION_RESOLUTION_FAILED"),
|
|
502 if status >= 500 else 409,
|
|
)
|
|
return dict(value)
|
|
|
|
|
|
class InitialExecutionProjectionStore:
|
|
"""Apply the exact initial projection through one durable CAS transaction."""
|
|
|
|
allows_supersession_child = False
|
|
|
|
def __init__(
|
|
self,
|
|
db_path: Path,
|
|
resolver: Callable[
|
|
[str, str, str, str, Mapping[str, Any]], Mapping[str, Any]
|
|
],
|
|
) -> None:
|
|
self.db_path = Path(db_path)
|
|
self.resolver = resolver
|
|
|
|
def _connect(self) -> sqlite3.Connection:
|
|
connection = sqlite3.connect(self.db_path, timeout=30.0)
|
|
connection.row_factory = sqlite3.Row
|
|
connection.execute("PRAGMA foreign_keys=ON")
|
|
connection.execute("PRAGMA busy_timeout=30000")
|
|
return connection
|
|
|
|
def _validate_projection_entrypoint(
|
|
self, connection: sqlite3.Connection, task_id: str
|
|
) -> None:
|
|
"""Fence formal lineage children from the legacy projection entrypoint.
|
|
|
|
A supersession child carries additional append-only lineage evidence that
|
|
must be committed with its initial projection. The legacy endpoint has
|
|
no such contract, so it must not be able to project one accidentally.
|
|
"""
|
|
tables = {
|
|
str(row[0])
|
|
for row in connection.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
)
|
|
}
|
|
if "task_lineage_supersessions" not in tables:
|
|
return
|
|
lineage = connection.execute(
|
|
"SELECT 1 FROM task_lineage_supersessions WHERE child_task_id=?",
|
|
(task_id,),
|
|
).fetchone()
|
|
if lineage is not None and not self.allows_supersession_child:
|
|
raise InitialExecutionProjectionError(
|
|
"SUPERSESSION_INITIAL_PROJECTION_ENDPOINT_REQUIRED", 409
|
|
)
|
|
|
|
@staticmethod
|
|
def _columns(connection: sqlite3.Connection, table: str) -> dict[str, sqlite3.Row]:
|
|
return {
|
|
str(row["name"]): row
|
|
for row in connection.execute(f'PRAGMA table_info("{table}")')
|
|
}
|
|
|
|
@staticmethod
|
|
def _json_value(raw: object, code: str) -> Any:
|
|
if isinstance(raw, (Mapping, list)):
|
|
return dict(raw) if isinstance(raw, Mapping) else list(raw)
|
|
if not isinstance(raw, str):
|
|
raise InitialExecutionProjectionError(code, 409)
|
|
try:
|
|
return json.loads(raw)
|
|
except json.JSONDecodeError as exc:
|
|
raise InitialExecutionProjectionError(code, 409) from exc
|
|
|
|
@staticmethod
|
|
def _event_table(
|
|
connection: sqlite3.Connection,
|
|
) -> tuple[str, dict[str, sqlite3.Row]]:
|
|
tables = {
|
|
row["name"]
|
|
for row in connection.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
)
|
|
}
|
|
for name in ("task_events", "events"):
|
|
if name in tables:
|
|
columns = InitialExecutionProjectionStore._columns(connection, name)
|
|
if "task_id" in columns and "event_type" in columns:
|
|
return name, columns
|
|
raise InitialExecutionProjectionError("TASK_EVENT_TABLE_UNSUPPORTED", 503)
|
|
|
|
@classmethod
|
|
def _initial_event_chain(
|
|
cls, connection: sqlite3.Connection, task_id: str
|
|
) -> None:
|
|
table, columns = cls._event_table(connection)
|
|
ordering = "event_id" if "event_id" in columns else "rowid"
|
|
events = [
|
|
str(row[0])
|
|
for row in connection.execute(
|
|
f'SELECT event_type FROM "{table}" WHERE task_id=? ORDER BY {ordering}',
|
|
(task_id,),
|
|
)
|
|
]
|
|
if events != ["TASK_CREATED"]:
|
|
raise InitialExecutionProjectionError(
|
|
"TASK_INITIAL_EVENT_CHAIN_REQUIRED", 409
|
|
)
|
|
|
|
@classmethod
|
|
def _insert_event(
|
|
cls,
|
|
connection: sqlite3.Connection,
|
|
task_id: str,
|
|
payload: Mapping[str, Any],
|
|
) -> None:
|
|
table, columns = cls._event_table(connection)
|
|
values: dict[str, Any] = {
|
|
"task_id": task_id,
|
|
"event_type": "TASK_INITIAL_EXECUTION_PROJECTION_AUTHORIZED",
|
|
}
|
|
payload_text = json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
|
if "payload_json" in columns:
|
|
values["payload_json"] = payload_text
|
|
elif "payload" in columns:
|
|
values["payload"] = payload_text
|
|
else:
|
|
raise InitialExecutionProjectionError("TASK_EVENT_PAYLOAD_UNSUPPORTED", 503)
|
|
if "created_at" in columns:
|
|
values["created_at"] = _now()
|
|
if "agent_id" in columns:
|
|
values["agent_id"] = "owner-control"
|
|
if "actor_id" in columns:
|
|
values["actor_id"] = "owner-control"
|
|
names = list(values)
|
|
connection.execute(
|
|
f'INSERT INTO "{table}" ({",".join(chr(34)+name+chr(34) for name in names)}) '
|
|
f'VALUES ({",".join("?" for _ in names)})',
|
|
tuple(values[name] for name in names),
|
|
)
|
|
|
|
@staticmethod
|
|
def _ensure_receipt_table(connection: sqlite3.Connection) -> None:
|
|
connection.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS task_initial_execution_projections (
|
|
task_id TEXT NOT NULL,
|
|
idempotency_key TEXT NOT NULL,
|
|
request_sha256 TEXT NOT NULL,
|
|
authorization_id TEXT NOT NULL,
|
|
expected_job_id TEXT NOT NULL,
|
|
execution_digest TEXT NOT NULL,
|
|
required_capability TEXT NOT NULL,
|
|
adapter TEXT NOT NULL,
|
|
expected_revision INTEGER NOT NULL,
|
|
applied_revision INTEGER NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
PRIMARY KEY(task_id,idempotency_key)
|
|
)
|
|
"""
|
|
)
|
|
|
|
@staticmethod
|
|
def _ensure_source_capture_recovery_table(
|
|
connection: sqlite3.Connection,
|
|
) -> None:
|
|
connection.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS task_source_capture_projection_recoveries (
|
|
task_id TEXT NOT NULL,
|
|
idempotency_key TEXT NOT NULL,
|
|
request_sha256 TEXT NOT NULL,
|
|
authorization_id TEXT NOT NULL,
|
|
expected_job_id TEXT NOT NULL,
|
|
source_capture_result_sha256 TEXT NOT NULL,
|
|
source_capture_execution_digest TEXT NOT NULL,
|
|
execution_digest TEXT NOT NULL,
|
|
required_capability TEXT NOT NULL,
|
|
adapter TEXT NOT NULL,
|
|
expected_revision INTEGER NOT NULL,
|
|
applied_revision INTEGER NOT NULL,
|
|
attempt INTEGER NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
PRIMARY KEY(task_id,idempotency_key)
|
|
)
|
|
"""
|
|
)
|
|
|
|
@classmethod
|
|
def _source_capture_event_chain(
|
|
cls, connection: sqlite3.Connection, task_id: str
|
|
) -> None:
|
|
table, columns = cls._event_table(connection)
|
|
ordering = "event_id" if "event_id" in columns else "rowid"
|
|
rows = connection.execute(
|
|
f'SELECT event_type, payload_json FROM "{table}" '
|
|
f'WHERE task_id=? ORDER BY {ordering}',
|
|
(task_id,),
|
|
).fetchall()
|
|
events = [str(row[0]) for row in rows]
|
|
if (
|
|
len(events) < 5
|
|
or events[0] != "TASK_CREATED"
|
|
or events.count("TASK_CLAIMED") != 1
|
|
or events.count("RESULT_SUBMITTED") != 1
|
|
or events.count("TASK_VERIFIED") != 1
|
|
or events[-1] != "TASK_VERIFIED"
|
|
):
|
|
raise InitialExecutionProjectionError(
|
|
"SOURCE_CAPTURE_RECOVERY_EVENT_CHAIN_INVALID", 409
|
|
)
|
|
claim = events.index("TASK_CLAIMED")
|
|
result = events.index("RESULT_SUBMITTED")
|
|
verified = events.index("TASK_VERIFIED")
|
|
heartbeat_positions = [
|
|
index for index, event in enumerate(events) if event == "TASK_HEARTBEAT"
|
|
]
|
|
if (
|
|
not heartbeat_positions
|
|
or not all(claim < index < result for index in heartbeat_positions)
|
|
or not claim < result < verified
|
|
or any(
|
|
event not in {
|
|
"TASK_CREATED",
|
|
"TASK_CLAIMED",
|
|
"TASK_HEARTBEAT",
|
|
"RESULT_SUBMITTED",
|
|
"TASK_VERIFIED",
|
|
}
|
|
for event in events
|
|
)
|
|
):
|
|
raise InitialExecutionProjectionError(
|
|
"SOURCE_CAPTURE_RECOVERY_EVENT_CHAIN_INVALID", 409
|
|
)
|
|
try:
|
|
verification_event = json.loads(rows[-1][1])
|
|
except (TypeError, json.JSONDecodeError) as exc:
|
|
raise InitialExecutionProjectionError(
|
|
"SOURCE_CAPTURE_RECOVERY_VERIFICATION_EVENT_INVALID", 409
|
|
) from exc
|
|
if (
|
|
not isinstance(verification_event, Mapping)
|
|
or verification_event.get("decision") != "NEEDS_REWORK"
|
|
or verification_event.get("next_state") != "QUEUED"
|
|
):
|
|
raise InitialExecutionProjectionError(
|
|
"SOURCE_CAPTURE_RECOVERY_VERIFICATION_EVENT_INVALID", 409
|
|
)
|
|
|
|
def _load_source_capture_recovery(
|
|
self,
|
|
connection: sqlite3.Connection,
|
|
task_id: str,
|
|
request: SourceCaptureProjectionRecoveryRequest,
|
|
) -> tuple[sqlite3.Row, dict[str, Any], dict[str, Any]]:
|
|
columns = self._columns(connection, "tasks")
|
|
required = {
|
|
"task_id", "goal", "instructions", "success_criteria_json",
|
|
"constraints_json", "required_capabilities_json", "metadata_json",
|
|
"status", "attempt", "revision", "claimed_by", "lease_token_hash",
|
|
"lease_expires_at", "result_json", "verification_json",
|
|
}
|
|
if not required.issubset(columns):
|
|
raise InitialExecutionProjectionError(
|
|
"TASK_SCHEMA_SOURCE_CAPTURE_RECOVERY_UNSUPPORTED", 503
|
|
)
|
|
task = connection.execute(
|
|
"SELECT * FROM tasks WHERE task_id=?", (task_id,)
|
|
).fetchone()
|
|
if task is None:
|
|
raise InitialExecutionProjectionError("TASK_NOT_FOUND", 404)
|
|
self._validate_projection_entrypoint(connection, task_id)
|
|
if (
|
|
str(task["status"]) != request.expected_status
|
|
or int(task["revision"]) != request.expected_revision
|
|
or int(task["attempt"]) != request.expected_attempt
|
|
):
|
|
raise InitialExecutionProjectionError(
|
|
"SOURCE_CAPTURE_PROJECTION_RECOVERY_CAS_MISMATCH", 409
|
|
)
|
|
if task["claimed_by"] not in (None, ""):
|
|
raise InitialExecutionProjectionError("TASK_ALREADY_CLAIMED", 409)
|
|
if task["lease_token_hash"] not in (None, "") or task["lease_expires_at"] not in (None, ""):
|
|
raise InitialExecutionProjectionError("ACTIVE_LEASE_DENY", 409)
|
|
metadata = self._json_value(task["metadata_json"], "TASK_METADATA_INVALID")
|
|
capabilities = self._json_value(
|
|
task["required_capabilities_json"], "TASK_CAPABILITIES_INVALID"
|
|
)
|
|
result = self._json_value(
|
|
task["result_json"], "SOURCE_CAPTURE_RESULT_INVALID"
|
|
)
|
|
verification = self._json_value(
|
|
task["verification_json"], "SOURCE_CAPTURE_VERIFICATION_INVALID"
|
|
)
|
|
if (
|
|
not isinstance(metadata, Mapping)
|
|
or not isinstance(capabilities, list)
|
|
or not isinstance(result, Mapping)
|
|
or not isinstance(verification, Mapping)
|
|
):
|
|
raise InitialExecutionProjectionError(
|
|
"SOURCE_CAPTURE_RECOVERY_STATE_INVALID", 409
|
|
)
|
|
metadata = dict(metadata)
|
|
result = dict(result)
|
|
verification = dict(verification)
|
|
if "execution" in metadata or "execution_projection" in metadata:
|
|
raise InitialExecutionProjectionError("TASK_EXECUTION_ALREADY_PROJECTED", 409)
|
|
if capabilities:
|
|
raise InitialExecutionProjectionError(
|
|
"TASK_INITIAL_CAPABILITIES_MUST_BE_EMPTY", 409
|
|
)
|
|
if _sha(metadata) != request.expected_metadata_sha256:
|
|
raise InitialExecutionProjectionError("TASK_METADATA_DRIFT", 409)
|
|
if _sha(capabilities) != request.expected_required_capabilities_sha256:
|
|
raise InitialExecutionProjectionError("TASK_CAPABILITIES_DRIFT", 409)
|
|
if _sha(result) != request.expected_source_capture_result_sha256:
|
|
raise InitialExecutionProjectionError("SOURCE_CAPTURE_RESULT_DRIFT", 409)
|
|
if _sha(verification) != request.previous_verification_sha256:
|
|
raise InitialExecutionProjectionError("SOURCE_CAPTURE_VERIFICATION_DRIFT", 409)
|
|
if verification.get("decision") != "NEEDS_REWORK":
|
|
raise InitialExecutionProjectionError(
|
|
"SOURCE_CAPTURE_NEEDS_REWORK_VERIFICATION_REQUIRED", 409
|
|
)
|
|
expected_result = {
|
|
"task_id": task_id,
|
|
"job_id": request.expected_job_id,
|
|
"status": "SUCCEEDED",
|
|
"classification": "NEEDS_REWORK_SOURCE_CAPTURE",
|
|
"execution_mode": "source_capture",
|
|
"execution_digest": request.expected_source_capture_execution_digest,
|
|
"changed_files": [],
|
|
"writer_lease": "NONE",
|
|
"verification_state": "NEEDS_REWORK",
|
|
"direct_main_write": False,
|
|
"production_write": False,
|
|
"force_push": False,
|
|
"protected_ref_bypass": False,
|
|
"secret_material_exposed": False,
|
|
}
|
|
if any(result.get(name) != expected for name, expected in expected_result.items()):
|
|
raise InitialExecutionProjectionError(
|
|
"SOURCE_CAPTURE_RESULT_CONTRACT_INVALID", 409
|
|
)
|
|
self._source_capture_event_chain(connection, task_id)
|
|
return task, metadata, result
|
|
|
|
def _load_and_validate(
|
|
self,
|
|
connection: sqlite3.Connection,
|
|
task_id: str,
|
|
request: InitialExecutionProjectionRequest,
|
|
) -> tuple[sqlite3.Row, dict[str, sqlite3.Row], dict[str, Any], list[str]]:
|
|
columns = self._columns(connection, "tasks")
|
|
required = {
|
|
"task_id", "goal", "instructions", "success_criteria_json",
|
|
"constraints_json", "required_capabilities_json", "metadata_json",
|
|
"status", "attempt", "revision", "claimed_by", "lease_token_hash",
|
|
"lease_expires_at", "result_json", "verification_json",
|
|
}
|
|
if not required.issubset(columns):
|
|
raise InitialExecutionProjectionError(
|
|
"TASK_SCHEMA_INITIAL_EXECUTION_PROJECTION_UNSUPPORTED", 503
|
|
)
|
|
task = connection.execute(
|
|
"SELECT * FROM tasks WHERE task_id=?", (task_id,)
|
|
).fetchone()
|
|
if task is None:
|
|
raise InitialExecutionProjectionError("TASK_NOT_FOUND", 404)
|
|
if (
|
|
str(task["status"]) != request.expected_status
|
|
or int(task["revision"]) != request.expected_revision
|
|
or int(task["attempt"]) != request.expected_attempt
|
|
):
|
|
raise InitialExecutionProjectionError(
|
|
"INITIAL_EXECUTION_PROJECTION_CAS_MISMATCH", 409
|
|
)
|
|
if task["claimed_by"] not in (None, ""):
|
|
raise InitialExecutionProjectionError("TASK_ALREADY_CLAIMED", 409)
|
|
if task["lease_token_hash"] not in (None, "") or task["lease_expires_at"] not in (None, ""):
|
|
raise InitialExecutionProjectionError("ACTIVE_LEASE_DENY", 409)
|
|
if task["result_json"] not in (None, "") or task["verification_json"] not in (None, ""):
|
|
raise InitialExecutionProjectionError("TASK_LIFECYCLE_NOT_INITIAL", 409)
|
|
metadata = self._json_value(task["metadata_json"], "TASK_METADATA_INVALID")
|
|
capabilities = self._json_value(
|
|
task["required_capabilities_json"], "TASK_CAPABILITIES_INVALID"
|
|
)
|
|
if not isinstance(metadata, Mapping) or not isinstance(capabilities, list):
|
|
raise InitialExecutionProjectionError("TASK_INITIAL_STATE_INVALID", 409)
|
|
metadata = dict(metadata)
|
|
if "execution" in metadata or "execution_projection" in metadata:
|
|
raise InitialExecutionProjectionError("TASK_EXECUTION_ALREADY_PROJECTED", 409)
|
|
if capabilities:
|
|
raise InitialExecutionProjectionError(
|
|
"TASK_INITIAL_CAPABILITIES_MUST_BE_EMPTY", 409
|
|
)
|
|
if _sha(metadata) != request.expected_metadata_sha256:
|
|
raise InitialExecutionProjectionError("TASK_METADATA_DRIFT", 409)
|
|
if _sha(capabilities) != request.expected_required_capabilities_sha256:
|
|
raise InitialExecutionProjectionError("TASK_CAPABILITIES_DRIFT", 409)
|
|
self._initial_event_chain(connection, task_id)
|
|
return task, columns, metadata, capabilities
|
|
|
|
@staticmethod
|
|
def _runtime_input(task: sqlite3.Row) -> dict[str, Any]:
|
|
try:
|
|
criteria = json.loads(task["success_criteria_json"])
|
|
constraints = json.loads(task["constraints_json"])
|
|
except (TypeError, json.JSONDecodeError) as exc:
|
|
raise InitialExecutionProjectionError("TASK_RUNTIME_INPUT_INVALID", 409) from exc
|
|
if not isinstance(criteria, list) or not isinstance(constraints, Mapping):
|
|
raise InitialExecutionProjectionError("TASK_RUNTIME_INPUT_INVALID", 409)
|
|
return {
|
|
"goal": str(task["goal"]),
|
|
"instructions": str(task["instructions"]),
|
|
"success_criteria": criteria,
|
|
"constraints": dict(constraints),
|
|
"scope": {"scope_type": "legacy", "tenant_id": None, "project_id": None},
|
|
"trace_id": str(task["task_id"]),
|
|
}
|
|
|
|
def _projection_event_extension(
|
|
self,
|
|
connection: sqlite3.Connection,
|
|
task_id: str,
|
|
request: InitialExecutionProjectionRequest,
|
|
execution: Mapping[str, Any],
|
|
applied_revision: int,
|
|
) -> Mapping[str, Any]:
|
|
"""Return server-derived fields for a specialized projection event."""
|
|
return {}
|
|
|
|
def _insert_projection_extension(
|
|
self,
|
|
connection: sqlite3.Connection,
|
|
task_id: str,
|
|
request: InitialExecutionProjectionRequest,
|
|
execution: Mapping[str, Any],
|
|
event_payload: Mapping[str, Any],
|
|
applied_revision: int,
|
|
) -> None:
|
|
"""Persist specialized evidence in the caller's projection transaction."""
|
|
return None
|
|
|
|
def project(
|
|
self,
|
|
task_id: str,
|
|
request: InitialExecutionProjectionRequest,
|
|
) -> dict[str, Any]:
|
|
task_id = _require_string(task_id, "TASK_ID_INVALID", TASK_RE)
|
|
if request.authorization_id != _required_authorization_id():
|
|
raise InitialExecutionProjectionError("AUTHORIZATION_ID_MISMATCH", 403)
|
|
request_sha = _sha(request.public())
|
|
|
|
with self._connect() as connection:
|
|
self._validate_projection_entrypoint(connection, task_id)
|
|
self._ensure_receipt_table(connection)
|
|
replay = connection.execute(
|
|
"SELECT * FROM task_initial_execution_projections "
|
|
"WHERE task_id=? AND idempotency_key=?",
|
|
(task_id, request.idempotency_key),
|
|
).fetchone()
|
|
if replay is not None:
|
|
if replay["request_sha256"] != request_sha:
|
|
raise InitialExecutionProjectionError("IDEMPOTENCY_CONFLICT", 409)
|
|
connection.commit()
|
|
return {
|
|
"task_id": task_id,
|
|
"status": "QUEUED",
|
|
"attempt": 0,
|
|
"revision": int(replay["applied_revision"]),
|
|
"job_id": replay["expected_job_id"],
|
|
"execution_digest": replay["execution_digest"],
|
|
"required_capability": replay["required_capability"],
|
|
"adapter": replay["adapter"],
|
|
"authorization_id": replay["authorization_id"],
|
|
"idempotent": True,
|
|
}
|
|
|
|
with self._connect() as connection:
|
|
task, _, _, _ = self._load_and_validate(connection, task_id, request)
|
|
runtime_input = self._runtime_input(task)
|
|
|
|
resolved = dict(
|
|
self.resolver(
|
|
task_id,
|
|
request.expected_job_id,
|
|
request.required_capability,
|
|
request.adapter,
|
|
runtime_input,
|
|
)
|
|
)
|
|
binding = GovernedExecutionBinding.from_mapping(resolved)
|
|
if binding.task_id != task_id or binding.job_id != request.expected_job_id:
|
|
raise InitialExecutionProjectionError("GOVERNED_BINDING_CONFLICT", 409)
|
|
execution = validate_execution_envelope(resolved, binding=binding)
|
|
if (
|
|
execution.get("action") != "agent_runtime"
|
|
or execution.get("adapter") != request.adapter
|
|
or execution.get("capability_scope") != [request.required_capability]
|
|
or execution.get("change_set") != []
|
|
or execution.get("verification") != []
|
|
or execution.get("governance", {}).get("writer_lease") is not False
|
|
):
|
|
raise InitialExecutionProjectionError("AGENT_RUNTIME_ENVELOPE_INVALID", 409)
|
|
if execution["execution_digest"] != request.expected_execution_digest:
|
|
raise InitialExecutionProjectionError("EXECUTION_DIGEST_DRIFT", 409)
|
|
|
|
with self._connect() as connection:
|
|
connection.execute("BEGIN IMMEDIATE")
|
|
self._ensure_receipt_table(connection)
|
|
replay = connection.execute(
|
|
"SELECT * FROM task_initial_execution_projections "
|
|
"WHERE task_id=? AND idempotency_key=?",
|
|
(task_id, request.idempotency_key),
|
|
).fetchone()
|
|
if replay is not None:
|
|
if replay["request_sha256"] != request_sha:
|
|
raise InitialExecutionProjectionError("IDEMPOTENCY_CONFLICT", 409)
|
|
connection.commit()
|
|
return {
|
|
"task_id": task_id,
|
|
"status": "QUEUED",
|
|
"attempt": 0,
|
|
"revision": int(replay["applied_revision"]),
|
|
"job_id": replay["expected_job_id"],
|
|
"execution_digest": replay["execution_digest"],
|
|
"required_capability": replay["required_capability"],
|
|
"adapter": replay["adapter"],
|
|
"authorization_id": replay["authorization_id"],
|
|
"idempotent": True,
|
|
}
|
|
task, _, metadata, _ = self._load_and_validate(connection, task_id, request)
|
|
new_metadata = dict(metadata)
|
|
new_metadata["execution"] = execution
|
|
new_metadata["execution_projection"] = {
|
|
"schema": "aios.online-agent.task-projection.v1",
|
|
"task_id": task_id,
|
|
"job_id": request.expected_job_id,
|
|
"execution_digest": execution["execution_digest"],
|
|
}
|
|
applied_revision = request.expected_revision + 1
|
|
cursor = connection.execute(
|
|
"""
|
|
UPDATE tasks
|
|
SET metadata_json=?, required_capabilities_json=?,
|
|
revision=?, updated_at=?
|
|
WHERE task_id=? AND status=? AND revision=? AND attempt=?
|
|
AND claimed_by IS NULL AND lease_token_hash IS NULL
|
|
AND lease_expires_at IS NULL AND result_json IS NULL
|
|
AND verification_json IS NULL
|
|
AND metadata_json=? AND required_capabilities_json=?
|
|
""",
|
|
(
|
|
json.dumps(new_metadata, sort_keys=True, separators=(",", ":")),
|
|
json.dumps([request.required_capability], separators=(",", ":")),
|
|
applied_revision,
|
|
_now(),
|
|
task_id,
|
|
request.expected_status,
|
|
request.expected_revision,
|
|
request.expected_attempt,
|
|
task["metadata_json"],
|
|
task["required_capabilities_json"],
|
|
),
|
|
)
|
|
if cursor.rowcount != 1:
|
|
raise InitialExecutionProjectionError(
|
|
"INITIAL_EXECUTION_PROJECTION_CAS_MISMATCH", 409
|
|
)
|
|
event_payload = {
|
|
"schema": "aios.task-initial-execution-projection.event.v1",
|
|
"task_id": task_id,
|
|
"job_id": request.expected_job_id,
|
|
"authorization_id": request.authorization_id,
|
|
"idempotency_key": request.idempotency_key,
|
|
"reason": request.reason,
|
|
"action": "agent_runtime",
|
|
"adapter": request.adapter,
|
|
"required_capability": request.required_capability,
|
|
"execution_digest": execution["execution_digest"],
|
|
"previous_revision": request.expected_revision,
|
|
"applied_revision": applied_revision,
|
|
"attempt": request.expected_attempt,
|
|
"request_sha256": request_sha,
|
|
"writer_lease": False,
|
|
"main_write": False,
|
|
"production_write": False,
|
|
}
|
|
extension = dict(
|
|
self._projection_event_extension(
|
|
connection,
|
|
task_id,
|
|
request,
|
|
execution,
|
|
applied_revision,
|
|
)
|
|
)
|
|
if set(extension).intersection(event_payload):
|
|
raise InitialExecutionProjectionError(
|
|
"INITIAL_EXECUTION_PROJECTION_EVENT_EXTENSION_CONFLICT", 500
|
|
)
|
|
event_payload.update(extension)
|
|
self._insert_event(connection, task_id, event_payload)
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO task_initial_execution_projections(
|
|
task_id,idempotency_key,request_sha256,authorization_id,
|
|
expected_job_id,execution_digest,required_capability,adapter,
|
|
expected_revision,applied_revision,created_at
|
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
|
""",
|
|
(
|
|
task_id,
|
|
request.idempotency_key,
|
|
request_sha,
|
|
request.authorization_id,
|
|
request.expected_job_id,
|
|
execution["execution_digest"],
|
|
request.required_capability,
|
|
request.adapter,
|
|
request.expected_revision,
|
|
applied_revision,
|
|
_now(),
|
|
),
|
|
)
|
|
self._insert_projection_extension(
|
|
connection,
|
|
task_id,
|
|
request,
|
|
execution,
|
|
event_payload,
|
|
applied_revision,
|
|
)
|
|
connection.commit()
|
|
return {
|
|
"task_id": task_id,
|
|
"status": "QUEUED",
|
|
"attempt": 0,
|
|
"revision": applied_revision,
|
|
"job_id": request.expected_job_id,
|
|
"execution_digest": execution["execution_digest"],
|
|
"required_capability": request.required_capability,
|
|
"adapter": request.adapter,
|
|
"authorization_id": request.authorization_id,
|
|
"idempotent": False,
|
|
}
|
|
|
|
def project_from_source_capture(
|
|
self,
|
|
task_id: str,
|
|
request: SourceCaptureProjectionRecoveryRequest,
|
|
) -> dict[str, Any]:
|
|
task_id = _require_string(task_id, "TASK_ID_INVALID", TASK_RE)
|
|
if request.authorization_id != _required_authorization_id():
|
|
raise InitialExecutionProjectionError("AUTHORIZATION_ID_MISMATCH", 403)
|
|
request_sha = _sha(request.public())
|
|
|
|
def replay_result(row: sqlite3.Row) -> dict[str, Any]:
|
|
return {
|
|
"task_id": task_id,
|
|
"status": "QUEUED",
|
|
"attempt": int(row["attempt"]),
|
|
"revision": int(row["applied_revision"]),
|
|
"job_id": row["expected_job_id"],
|
|
"source_capture_execution_digest": row[
|
|
"source_capture_execution_digest"
|
|
],
|
|
"execution_digest": row["execution_digest"],
|
|
"required_capability": row["required_capability"],
|
|
"adapter": row["adapter"],
|
|
"authorization_id": row["authorization_id"],
|
|
"recovery_from_source_capture": True,
|
|
"idempotent": True,
|
|
}
|
|
|
|
with self._connect() as connection:
|
|
self._ensure_source_capture_recovery_table(connection)
|
|
replay = connection.execute(
|
|
"SELECT * FROM task_source_capture_projection_recoveries "
|
|
"WHERE task_id=? AND idempotency_key=?",
|
|
(task_id, request.idempotency_key),
|
|
).fetchone()
|
|
if replay is not None:
|
|
if replay["request_sha256"] != request_sha:
|
|
raise InitialExecutionProjectionError("IDEMPOTENCY_CONFLICT", 409)
|
|
connection.commit()
|
|
return replay_result(replay)
|
|
|
|
with self._connect() as connection:
|
|
task, _, result = self._load_source_capture_recovery(
|
|
connection, task_id, request
|
|
)
|
|
runtime_input = self._runtime_input(task)
|
|
|
|
resolved = dict(
|
|
self.resolver(
|
|
task_id,
|
|
request.expected_job_id,
|
|
request.required_capability,
|
|
request.adapter,
|
|
runtime_input,
|
|
)
|
|
)
|
|
binding = GovernedExecutionBinding.from_mapping(resolved)
|
|
if binding.task_id != task_id or binding.job_id != request.expected_job_id:
|
|
raise InitialExecutionProjectionError("GOVERNED_BINDING_CONFLICT", 409)
|
|
result_binding = {
|
|
"task_id": result.get("task_id"),
|
|
"job_id": result.get("job_id"),
|
|
"repository": result.get("repository"),
|
|
"base_oid": result.get("base_oid"),
|
|
"expected_branch_oid": result.get("expected_branch_oid"),
|
|
"target_branch": result.get("target_branch"),
|
|
"generation": result.get("generation"),
|
|
}
|
|
expected_binding = {
|
|
"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,
|
|
"generation": binding.generation,
|
|
}
|
|
if result_binding != expected_binding:
|
|
raise InitialExecutionProjectionError(
|
|
"SOURCE_CAPTURE_GOVERNED_BINDING_CONFLICT", 409
|
|
)
|
|
execution = validate_execution_envelope(resolved, binding=binding)
|
|
if (
|
|
execution.get("action") != "agent_runtime"
|
|
or execution.get("adapter") != request.adapter
|
|
or execution.get("capability_scope") != [request.required_capability]
|
|
or execution.get("change_set") != []
|
|
or execution.get("verification") != []
|
|
or execution.get("governance", {}).get("writer_lease") is not False
|
|
):
|
|
raise InitialExecutionProjectionError("AGENT_RUNTIME_ENVELOPE_INVALID", 409)
|
|
if execution["execution_digest"] != request.expected_execution_digest:
|
|
raise InitialExecutionProjectionError("EXECUTION_DIGEST_DRIFT", 409)
|
|
|
|
with self._connect() as connection:
|
|
connection.execute("BEGIN IMMEDIATE")
|
|
self._ensure_source_capture_recovery_table(connection)
|
|
replay = connection.execute(
|
|
"SELECT * FROM task_source_capture_projection_recoveries "
|
|
"WHERE task_id=? AND idempotency_key=?",
|
|
(task_id, request.idempotency_key),
|
|
).fetchone()
|
|
if replay is not None:
|
|
if replay["request_sha256"] != request_sha:
|
|
raise InitialExecutionProjectionError("IDEMPOTENCY_CONFLICT", 409)
|
|
connection.commit()
|
|
return replay_result(replay)
|
|
task, metadata, _ = self._load_source_capture_recovery(
|
|
connection, task_id, request
|
|
)
|
|
new_metadata = dict(metadata)
|
|
new_metadata["execution"] = execution
|
|
new_metadata["execution_projection"] = {
|
|
"schema": "aios.online-agent.task-projection.v1",
|
|
"task_id": task_id,
|
|
"job_id": request.expected_job_id,
|
|
"execution_digest": execution["execution_digest"],
|
|
}
|
|
applied_revision = request.expected_revision + 1
|
|
cursor = connection.execute(
|
|
"""
|
|
UPDATE tasks
|
|
SET metadata_json=?, required_capabilities_json=?,
|
|
revision=?, updated_at=?
|
|
WHERE task_id=? AND status=? AND revision=? AND attempt=?
|
|
AND claimed_by IS NULL AND lease_token_hash IS NULL
|
|
AND lease_expires_at IS NULL
|
|
AND metadata_json=? AND required_capabilities_json=?
|
|
AND result_json=? AND verification_json=?
|
|
""",
|
|
(
|
|
json.dumps(new_metadata, sort_keys=True, separators=(",", ":")),
|
|
json.dumps([request.required_capability], separators=(",", ":")),
|
|
applied_revision,
|
|
_now(),
|
|
task_id,
|
|
request.expected_status,
|
|
request.expected_revision,
|
|
request.expected_attempt,
|
|
task["metadata_json"],
|
|
task["required_capabilities_json"],
|
|
task["result_json"],
|
|
task["verification_json"],
|
|
),
|
|
)
|
|
if cursor.rowcount != 1:
|
|
raise InitialExecutionProjectionError(
|
|
"SOURCE_CAPTURE_PROJECTION_RECOVERY_CAS_MISMATCH", 409
|
|
)
|
|
event_payload = {
|
|
"schema": "aios.task-initial-execution-projection.event.v1",
|
|
"task_id": task_id,
|
|
"job_id": request.expected_job_id,
|
|
"authorization_id": request.authorization_id,
|
|
"idempotency_key": request.idempotency_key,
|
|
"reason": request.reason,
|
|
"action": "agent_runtime",
|
|
"adapter": request.adapter,
|
|
"required_capability": request.required_capability,
|
|
"source_capture_execution_digest": request.expected_source_capture_execution_digest,
|
|
"source_capture_result_sha256": request.expected_source_capture_result_sha256,
|
|
"previous_verification_sha256": request.previous_verification_sha256,
|
|
"execution_digest": execution["execution_digest"],
|
|
"previous_revision": request.expected_revision,
|
|
"applied_revision": applied_revision,
|
|
"attempt": request.expected_attempt,
|
|
"request_sha256": request_sha,
|
|
"recovery_from_source_capture": True,
|
|
"writer_lease": False,
|
|
"main_write": False,
|
|
"production_write": False,
|
|
}
|
|
self._insert_event(connection, task_id, event_payload)
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO task_source_capture_projection_recoveries(
|
|
task_id,idempotency_key,request_sha256,authorization_id,
|
|
expected_job_id,source_capture_result_sha256,
|
|
source_capture_execution_digest,execution_digest,
|
|
required_capability,adapter,expected_revision,applied_revision,
|
|
attempt,created_at
|
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
""",
|
|
(
|
|
task_id,
|
|
request.idempotency_key,
|
|
request_sha,
|
|
request.authorization_id,
|
|
request.expected_job_id,
|
|
request.expected_source_capture_result_sha256,
|
|
request.expected_source_capture_execution_digest,
|
|
execution["execution_digest"],
|
|
request.required_capability,
|
|
request.adapter,
|
|
request.expected_revision,
|
|
applied_revision,
|
|
request.expected_attempt,
|
|
_now(),
|
|
),
|
|
)
|
|
connection.commit()
|
|
return {
|
|
"task_id": task_id,
|
|
"status": "QUEUED",
|
|
"attempt": request.expected_attempt,
|
|
"revision": applied_revision,
|
|
"job_id": request.expected_job_id,
|
|
"source_capture_execution_digest": request.expected_source_capture_execution_digest,
|
|
"execution_digest": execution["execution_digest"],
|
|
"required_capability": request.required_capability,
|
|
"adapter": request.adapter,
|
|
"authorization_id": request.authorization_id,
|
|
"recovery_from_source_capture": True,
|
|
"idempotent": False,
|
|
}
|
|
|
|
|
|
class InitialExecutionProjectionHandler(BaseHTTPRequestHandler):
|
|
store: InitialExecutionProjectionStore | None = None
|
|
control_token: str = ""
|
|
|
|
def log_message(self, *_args: Any) -> None:
|
|
return
|
|
|
|
def _json(self, value: Mapping[str, Any], status: int) -> None:
|
|
data = json.dumps(dict(value), sort_keys=True, separators=(",", ":")).encode()
|
|
self.send_response(status)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Content-Length", str(len(data)))
|
|
self.end_headers()
|
|
self.wfile.write(data)
|
|
|
|
def _authorized(self) -> bool:
|
|
header = self.headers.get("Authorization", "")
|
|
return (
|
|
header.startswith("Bearer ")
|
|
and bool(self.control_token)
|
|
and hmac.compare_digest(header[7:], self.control_token)
|
|
)
|
|
|
|
def do_GET(self) -> None:
|
|
if self.path == "/health":
|
|
self._json(
|
|
{"status": "ok", "service": "aios-agent-task-initial-execution-projection"},
|
|
200,
|
|
)
|
|
else:
|
|
canonical_match = re.fullmatch(
|
|
r"/v1/tasks/([^/]+)/canonical-projection", self.path
|
|
)
|
|
if canonical_match is None:
|
|
self._json({"error": "NOT_FOUND"}, 404)
|
|
return
|
|
if not self._authorized():
|
|
self._json({"error": "UNAUTHORIZED"}, 401)
|
|
return
|
|
if self.store is None:
|
|
self._json({"error": "RUNTIME_UNAVAILABLE"}, 503)
|
|
return
|
|
status, result = get_canonical_projection(
|
|
self.store.db_path, canonical_match.group(1)
|
|
)
|
|
self._json(result, status)
|
|
|
|
def do_POST(self) -> None:
|
|
initial_match = re.fullmatch(
|
|
r"/v1/tasks/([^/]+)/initial-execution-projection", self.path
|
|
)
|
|
canonical_match = re.fullmatch(
|
|
r"/v1/tasks/([^/]+)/canonical-projection-recovery", self.path
|
|
)
|
|
recovery_match = re.fullmatch(
|
|
r"/v1/tasks/([^/]+)/source-capture-projection-recovery", self.path
|
|
)
|
|
if (
|
|
initial_match is None
|
|
and recovery_match is None
|
|
and canonical_match is None
|
|
):
|
|
self._json({"error": "NOT_FOUND"}, 404)
|
|
return
|
|
if not self._authorized():
|
|
self._json({"error": "UNAUTHORIZED"}, 401)
|
|
return
|
|
try:
|
|
length = int(self.headers.get("Content-Length", "0"))
|
|
except ValueError:
|
|
self._json({"error": "CONTENT_LENGTH_INVALID"}, 400)
|
|
return
|
|
if length < 1 or length > MAX_BODY:
|
|
self._json({"error": "REQUEST_SIZE_INVALID"}, 413)
|
|
return
|
|
try:
|
|
value = json.loads(self.rfile.read(length).decode("utf-8"))
|
|
if not isinstance(value, Mapping):
|
|
raise ValueError
|
|
if self.store is None:
|
|
raise InitialExecutionProjectionError("RUNTIME_UNAVAILABLE", 503)
|
|
if canonical_match is not None:
|
|
status, result = recover_canonical_projection(
|
|
self.store.db_path, canonical_match.group(1), value
|
|
)
|
|
self._json(result, status)
|
|
return
|
|
if initial_match is not None:
|
|
request = InitialExecutionProjectionRequest.from_mapping(value)
|
|
result = self.store.project(initial_match.group(1), request)
|
|
else:
|
|
recovery_request = SourceCaptureProjectionRecoveryRequest.from_mapping(
|
|
value
|
|
)
|
|
result = self.store.project_from_source_capture(
|
|
recovery_match.group(1), recovery_request
|
|
)
|
|
self._json(result, 200 if result["idempotent"] else 201)
|
|
except InitialExecutionProjectionError as exc:
|
|
self._json({"error": exc.code}, exc.status)
|
|
except (UnicodeDecodeError, json.JSONDecodeError, ValueError):
|
|
self._json({"error": "REQUEST_INVALID"}, 400)
|
|
|
|
|
|
def load_control_token(path: Path) -> str:
|
|
if path.is_symlink() or not path.is_file():
|
|
raise RuntimeError("INITIAL_EXECUTION_PROJECTION_CONTROL_CREDENTIAL_UNAVAILABLE")
|
|
value = path.read_text(encoding="utf-8").strip()
|
|
if len(value) < 32:
|
|
raise RuntimeError("INITIAL_EXECUTION_PROJECTION_CONTROL_CREDENTIAL_INVALID")
|
|
return value
|
|
|
|
|
|
def serve() -> None:
|
|
db = Path(os.environ.get("AIOS_AGENT_TASK_DB", "/var/lib/aios-agent-task/state.sqlite3"))
|
|
_required_authorization_id()
|
|
token_path = Path(
|
|
os.environ.get(
|
|
"AIOS_AGENT_TASK_CONTROL_TOKEN_FILE",
|
|
str(Path(os.environ.get("CREDENTIALS_DIRECTORY", "/run/credentials")) / "agent_task_control_token"),
|
|
)
|
|
)
|
|
token = load_control_token(token_path)
|
|
bridge = os.environ.get("AIOS_GOVERNED_WRITE_API_URL", "http://127.0.0.1:18892")
|
|
host = os.environ.get("AIOS_TASK_INITIAL_EXECUTION_PROJECTION_HOST", "127.0.0.1")
|
|
port = int(os.environ.get("AIOS_TASK_INITIAL_EXECUTION_PROJECTION_PORT", "18943"))
|
|
InitialExecutionProjectionHandler.store = InitialExecutionProjectionStore(
|
|
db, CanonicalBridgeInitialExecutionResolver(bridge, token)
|
|
)
|
|
InitialExecutionProjectionHandler.control_token = token
|
|
ThreadingHTTPServer((host, port), InitialExecutionProjectionHandler).serve_forever()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
serve()
|