340 lines
16 KiB
Python
340 lines
16 KiB
Python
"""One-time, Owner-signed, append-only supersession for one exact Task lineage."""
|
|
from __future__ import annotations
|
|
|
|
from contextlib import closing
|
|
from datetime import datetime, timedelta, timezone
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import sqlite3
|
|
from typing import Any, Callable, Mapping, Protocol
|
|
|
|
DECISION_SCHEMA = "aios.task-lineage-supersession-decision.v1"
|
|
RECEIPT_SCHEMA = "aios.task-lineage-supersession-receipt.v1"
|
|
OWNER_NAMESPACE = "aios-owner-approval-v1"
|
|
OWNER_PRINCIPAL = "aios-task-grant-signer"
|
|
OWNER_KEY_ID = "SHA256:rPqpxfPHtaZPM3zSWojX8Q7VxzCknsYqOokyo/aAM3A"
|
|
SOURCE_TASK_ID = "TASK-SINGLEHOST-TRUSTROOT-P0-20260904"
|
|
CHILD_TASK_ID = SOURCE_TASK_ID + "-S1"
|
|
REPOSITORY = "aios/aios"
|
|
TARGET_BRANCH = "agent/single-host-trust-root-p0-20260904"
|
|
GENERATION = 1
|
|
EXECUTION_DIGEST = "e99bda6a251832eb3865271eb5687af7eea44dd261144cdcdfa57104c0780536"
|
|
EXTERNAL_JOB_ID = "job-4e10f92a6a5742518a9f1d8745c845ab"
|
|
SUPERSESSION_ORDINAL = 1
|
|
CANONICAL_PROFILE = "aios.task-initial-execution.v1"
|
|
CANONICAL_PROFILE_VERSION = 1
|
|
SUPERSESSION_REASON = "formal-lineage-supersession-after-expired-unclaimed-source"
|
|
MAX_TTL = timedelta(minutes=5)
|
|
HEX64 = re.compile(r"^[0-9a-f]{64}$")
|
|
SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/+:-]{2,511}$")
|
|
|
|
|
|
class TaskLineageSupersessionError(RuntimeError):
|
|
def __init__(self, code: str, http_status: int = 409):
|
|
super().__init__(code)
|
|
self.code = code
|
|
self.http_status = int(http_status)
|
|
|
|
|
|
class SignatureVerifier(Protocol):
|
|
def verify(self, *, namespace: str, payload: bytes, signature: str, key_id: str) -> bool: ...
|
|
|
|
|
|
def canonical_json(value: Any) -> str:
|
|
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
|
|
|
|
|
def sha256_text(value: str) -> str:
|
|
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def _fail(code: str, status: int = 409) -> None:
|
|
raise TaskLineageSupersessionError(code, status)
|
|
|
|
|
|
def _utc(value: Any, field: str) -> datetime:
|
|
if not isinstance(value, str) or not value or len(value) > 80:
|
|
_fail(field.upper() + "_INVALID", 400)
|
|
try:
|
|
result = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
except ValueError as exc:
|
|
raise TaskLineageSupersessionError(field.upper() + "_INVALID", 400) from exc
|
|
if result.tzinfo is None:
|
|
_fail(field.upper() + "_INVALID", 400)
|
|
return result.astimezone(timezone.utc)
|
|
|
|
|
|
def _text(value: Any, field: str, *, maximum: int = 1024) -> str:
|
|
if not isinstance(value, str) or not value or len(value) > maximum:
|
|
_fail(field.upper() + "_INVALID", 400)
|
|
return value
|
|
|
|
|
|
def decision_payload(raw: Mapping[str, Any]) -> dict[str, Any]:
|
|
return {key: raw[key] for key in raw if key != "signature"}
|
|
|
|
|
|
DECISION_FIELDS = {
|
|
"schema", "decision_id", "nonce", "source_task_id",
|
|
"source_task_identity_sha256", "source_metadata_sha256",
|
|
"source_expected_status", "source_expected_revision", "source_expected_attempt",
|
|
"source_expected_claimed_by", "source_expected_lease_token_hash",
|
|
"source_expected_lease_expires_at", "repository", "target_branch", "generation",
|
|
"execution_digest", "external_job_id", "child_task_id", "supersession_ordinal",
|
|
"canonical_projection_profile", "canonical_projection_version", "supersession_reason",
|
|
"owner_identity", "signature_namespace", "signature_principal", "key_id",
|
|
"issued_at", "not_before", "expires_at", "one_time_use", "signature",
|
|
}
|
|
|
|
|
|
def validate_decision(raw: Mapping[str, Any], *, now: datetime) -> dict[str, Any]:
|
|
if not isinstance(raw, Mapping) or set(raw) != DECISION_FIELDS:
|
|
_fail("DECISION_FIELDS_INVALID", 400)
|
|
exact = {
|
|
"schema": DECISION_SCHEMA,
|
|
"source_task_id": SOURCE_TASK_ID,
|
|
"source_expected_status": "EXPIRED",
|
|
"source_expected_attempt": 0,
|
|
"source_expected_claimed_by": None,
|
|
"source_expected_lease_token_hash": None,
|
|
"source_expected_lease_expires_at": None,
|
|
"repository": REPOSITORY,
|
|
"target_branch": TARGET_BRANCH,
|
|
"generation": GENERATION,
|
|
"execution_digest": EXECUTION_DIGEST,
|
|
"external_job_id": EXTERNAL_JOB_ID,
|
|
"child_task_id": CHILD_TASK_ID,
|
|
"supersession_ordinal": SUPERSESSION_ORDINAL,
|
|
"canonical_projection_profile": CANONICAL_PROFILE,
|
|
"canonical_projection_version": CANONICAL_PROFILE_VERSION,
|
|
"supersession_reason": SUPERSESSION_REASON,
|
|
"owner_identity": OWNER_PRINCIPAL,
|
|
"signature_namespace": OWNER_NAMESPACE,
|
|
"signature_principal": OWNER_PRINCIPAL,
|
|
"key_id": OWNER_KEY_ID,
|
|
"one_time_use": True,
|
|
}
|
|
for field, expected in exact.items():
|
|
if raw[field] != expected:
|
|
_fail(field.upper() + "_MISMATCH", 403 if field.startswith(("owner", "signature", "key")) else 409)
|
|
for field in ("decision_id", "nonce"):
|
|
value = _text(raw[field], field, maximum=256)
|
|
if SAFE_ID.fullmatch(value) is None:
|
|
_fail(field.upper() + "_INVALID", 400)
|
|
for field in ("source_task_identity_sha256", "source_metadata_sha256"):
|
|
if not isinstance(raw[field], str) or HEX64.fullmatch(raw[field]) is None:
|
|
_fail(field.upper() + "_INVALID", 400)
|
|
if type(raw["source_expected_revision"]) is not int or raw["source_expected_revision"] < 1:
|
|
_fail("SOURCE_EXPECTED_REVISION_INVALID", 400)
|
|
_text(raw["signature"], "signature", maximum=32768)
|
|
issued_at = _utc(raw["issued_at"], "issued_at")
|
|
not_before = _utc(raw["not_before"], "not_before")
|
|
expires_at = _utc(raw["expires_at"], "expires_at")
|
|
if issued_at > expires_at or not_before > expires_at:
|
|
_fail("DECISION_TIME_ORDER_INVALID", 403)
|
|
if expires_at - issued_at > MAX_TTL or expires_at <= issued_at:
|
|
_fail("DECISION_TTL_INVALID", 403)
|
|
now = now.astimezone(timezone.utc)
|
|
if now < not_before or now >= expires_at:
|
|
_fail("DECISION_EXPIRED_OR_NOT_YET_VALID", 403)
|
|
if issued_at > now + timedelta(seconds=5) or now - issued_at > MAX_TTL:
|
|
_fail("DECISION_ISSUED_AT_INVALID", 403)
|
|
return {**dict(raw), "issued_at_dt": issued_at, "not_before_dt": not_before, "expires_at_dt": expires_at}
|
|
|
|
|
|
def _default_identity_digest(row: sqlite3.Row) -> str:
|
|
try:
|
|
from aios_expired_task_recovery import task_identity_digest
|
|
except ImportError as exc:
|
|
raise TaskLineageSupersessionError("FORMAL_TASK_IDENTITY_HELPER_UNAVAILABLE", 503) from exc
|
|
return str(task_identity_digest(row))
|
|
|
|
|
|
def _json_object(row: sqlite3.Row, field: str) -> dict[str, Any]:
|
|
try:
|
|
value = json.loads(row[field])
|
|
except (TypeError, ValueError, KeyError) as exc:
|
|
raise TaskLineageSupersessionError("SOURCE_TASK_JSON_INVALID", 409) from exc
|
|
if not isinstance(value, Mapping):
|
|
_fail("SOURCE_TASK_JSON_INVALID")
|
|
return dict(value)
|
|
|
|
|
|
def _verify_source(row: sqlite3.Row, d: Mapping[str, Any], identity_digest: Callable[[sqlite3.Row], str]) -> None:
|
|
if row["task_id"] != SOURCE_TASK_ID:
|
|
_fail("SOURCE_TASK_ID_MISMATCH")
|
|
cas = (
|
|
row["status"] == "EXPIRED"
|
|
and int(row["revision"]) == d["source_expected_revision"]
|
|
and int(row["attempt"]) == 0
|
|
and row["claimed_by"] is None
|
|
and row["lease_token_hash"] is None
|
|
and row["lease_expires_at"] is None
|
|
)
|
|
if not cas:
|
|
_fail("SOURCE_TASK_EXACT_CAS_MISMATCH")
|
|
metadata_raw = row["metadata_json"]
|
|
if not isinstance(metadata_raw, str) or sha256_text(metadata_raw) != d["source_metadata_sha256"]:
|
|
_fail("SOURCE_METADATA_DIGEST_MISMATCH")
|
|
if identity_digest(row) != d["source_task_identity_sha256"]:
|
|
_fail("SOURCE_TASK_IDENTITY_DIGEST_MISMATCH")
|
|
metadata = _json_object(row, "metadata_json")
|
|
execution = metadata.get("execution")
|
|
constraints = _json_object(row, "constraints_json")
|
|
if not isinstance(execution, Mapping):
|
|
_fail("SOURCE_EXECUTION_BINDING_INVALID")
|
|
expected = {
|
|
"repository": REPOSITORY,
|
|
"target_branch": TARGET_BRANCH,
|
|
"generation": GENERATION,
|
|
"execution_digest": EXECUTION_DIGEST,
|
|
"external_job_id": EXTERNAL_JOB_ID,
|
|
"task_id": SOURCE_TASK_ID,
|
|
}
|
|
if any(execution.get(key) != value for key, value in expected.items()):
|
|
_fail("SOURCE_EXECUTION_BINDING_MISMATCH")
|
|
if constraints.get("generation") != GENERATION:
|
|
_fail("SOURCE_GENERATION_CONSTRAINT_MISMATCH")
|
|
|
|
|
|
class TaskLineageSupersessionController:
|
|
def __init__(self, db_path: str | Path, verifier: SignatureVerifier, *, clock=None, identity_digest=None):
|
|
self.db_path = Path(db_path)
|
|
self.verifier = verifier
|
|
self.clock = clock or (lambda: datetime.now(timezone.utc))
|
|
self.identity_digest = identity_digest or _default_identity_digest
|
|
|
|
def _connect(self) -> sqlite3.Connection:
|
|
connection = sqlite3.connect(self.db_path, timeout=30, isolation_level=None)
|
|
connection.row_factory = sqlite3.Row
|
|
connection.execute("PRAGMA foreign_keys=ON")
|
|
connection.execute("PRAGMA busy_timeout=30000")
|
|
return connection
|
|
|
|
def _now(self) -> datetime:
|
|
value = self.clock()
|
|
if value.tzinfo is None:
|
|
value = value.replace(tzinfo=timezone.utc)
|
|
return value.astimezone(timezone.utc)
|
|
|
|
@staticmethod
|
|
def _assert_schema(connection: sqlite3.Connection) -> None:
|
|
required = {"tasks", "events", "task_lineage_supersessions", "task_lineage_supersession_receipts"}
|
|
actual = {row[0] for row in connection.execute("SELECT name FROM sqlite_master WHERE type='table'")}
|
|
if not required.issubset(actual):
|
|
_fail("SUPERSESSION_SCHEMA_UNAVAILABLE", 503)
|
|
columns = {row[1] for row in connection.execute("PRAGMA table_info(tasks)")}
|
|
task_columns = {
|
|
"task_id", "goal", "instructions", "success_criteria_json",
|
|
"required_capabilities_json", "constraints_json", "metadata_json", "deadline",
|
|
"priority", "max_attempts", "attempt", "status", "claimed_by",
|
|
"lease_token_hash", "lease_expires_at", "result_json", "result_digest",
|
|
"result_idempotency_key", "verification_json", "revision", "created_at", "updated_at",
|
|
}
|
|
if not task_columns.issubset(columns):
|
|
_fail("TASK_SCHEMA_UNSUPPORTED", 503)
|
|
|
|
def supersede(self, raw: Mapping[str, Any]) -> dict[str, Any]:
|
|
now = self._now()
|
|
decision = validate_decision(raw, now=now)
|
|
payload_bytes = canonical_json(decision_payload(raw)).encode("utf-8")
|
|
if not self.verifier.verify(
|
|
namespace=OWNER_NAMESPACE, payload=payload_bytes,
|
|
signature=decision["signature"], key_id=decision["key_id"],
|
|
):
|
|
_fail("DECISION_SIGNATURE_INVALID", 403)
|
|
decision_sha = hashlib.sha256(payload_bytes).hexdigest()
|
|
now_text = now.isoformat()
|
|
with closing(self._connect()) as connection:
|
|
connection.execute("BEGIN IMMEDIATE")
|
|
try:
|
|
self._assert_schema(connection)
|
|
replay = connection.execute(
|
|
"SELECT owner_decision_sha256,receipt_json FROM task_lineage_supersession_receipts WHERE decision_id=? OR nonce=?",
|
|
(decision["decision_id"], decision["nonce"]),
|
|
).fetchone()
|
|
if replay is not None:
|
|
if replay["owner_decision_sha256"] != decision_sha:
|
|
_fail("DECISION_REPLAY_MISMATCH")
|
|
result = json.loads(replay["receipt_json"])
|
|
connection.commit()
|
|
return {**result, "idempotent": True}
|
|
source = connection.execute("SELECT * FROM tasks WHERE task_id=?", (SOURCE_TASK_ID,)).fetchone()
|
|
if source is None:
|
|
_fail("SOURCE_TASK_NOT_FOUND", 404)
|
|
_verify_source(source, decision, self.identity_digest)
|
|
if connection.execute(
|
|
"SELECT 1 FROM task_lineage_supersessions WHERE source_task_id=? AND supersession_ordinal=?",
|
|
(SOURCE_TASK_ID, SUPERSESSION_ORDINAL),
|
|
).fetchone():
|
|
_fail("SUPERSESSION_ALREADY_EXISTS")
|
|
if connection.execute("SELECT 1 FROM tasks WHERE task_id=?", (CHILD_TASK_ID,)).fetchone():
|
|
_fail("CHILD_TASK_ALREADY_EXISTS")
|
|
connection.execute(
|
|
"""INSERT INTO tasks(
|
|
task_id,goal,instructions,success_criteria_json,required_capabilities_json,
|
|
constraints_json,metadata_json,deadline,priority,max_attempts,attempt,status,
|
|
claimed_by,lease_token_hash,lease_expires_at,result_json,result_digest,
|
|
result_idempotency_key,verification_json,revision,created_at,updated_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
(
|
|
CHILD_TASK_ID, source["goal"], source["instructions"], source["success_criteria_json"],
|
|
"[]", source["constraints_json"], "{}", None, source["priority"], source["max_attempts"],
|
|
0, "QUEUED", None, None, None, None, None, None, None, 1, now_text, now_text,
|
|
),
|
|
)
|
|
connection.execute(
|
|
"""INSERT INTO task_lineage_supersessions(
|
|
source_task_id,supersession_ordinal,child_task_id,source_task_identity_sha256,
|
|
source_metadata_sha256,owner_decision_sha256,decision_id,created_at)
|
|
VALUES(?,?,?,?,?,?,?,?)""",
|
|
(SOURCE_TASK_ID, SUPERSESSION_ORDINAL, CHILD_TASK_ID,
|
|
decision["source_task_identity_sha256"], decision["source_metadata_sha256"],
|
|
decision_sha, decision["decision_id"], now_text),
|
|
)
|
|
event_payload = {
|
|
"task_id": CHILD_TASK_ID, "supersedes_task_id": SOURCE_TASK_ID,
|
|
"supersession_ordinal": SUPERSESSION_ORDINAL,
|
|
"source_task_identity_sha256": decision["source_task_identity_sha256"],
|
|
"source_metadata_sha256": decision["source_metadata_sha256"],
|
|
"owner_supersession_decision_sha256": decision_sha,
|
|
"canonical_projection_profile": CANONICAL_PROFILE,
|
|
"canonical_projection_version": CANONICAL_PROFILE_VERSION,
|
|
}
|
|
connection.execute(
|
|
"INSERT INTO events(task_id,agent_id,event_type,payload_json,created_at) VALUES(?,?,?,?,?)",
|
|
(CHILD_TASK_ID, None, "TASK_CREATED", canonical_json(event_payload), now_text),
|
|
)
|
|
receipt = {
|
|
"schema": RECEIPT_SCHEMA, "status": "SUPERSEDED",
|
|
"decision_id": decision["decision_id"], "owner_decision_sha256": decision_sha,
|
|
"source_task_id": SOURCE_TASK_ID, "source_task_identity_sha256": decision["source_task_identity_sha256"],
|
|
"source_metadata_sha256": decision["source_metadata_sha256"],
|
|
"source_status": "EXPIRED", "source_revision": decision["source_expected_revision"],
|
|
"source_attempt": 0, "source_history_preserved": True,
|
|
"child_task_id": CHILD_TASK_ID, "supersession_ordinal": SUPERSESSION_ORDINAL,
|
|
"child_status": "QUEUED", "child_revision": 1, "child_attempt": 0,
|
|
"canonical_projection_profile": CANONICAL_PROFILE,
|
|
"canonical_projection_version": CANONICAL_PROFILE_VERSION,
|
|
"exact_identity_cas": True, "one_time_use": True,
|
|
"owner_signature_namespace": OWNER_NAMESPACE,
|
|
"owner_signature_principal": OWNER_PRINCIPAL,
|
|
"owner_signature_key_id": OWNER_KEY_ID,
|
|
"owner_signature_sha256": sha256_text(decision["signature"]),
|
|
}
|
|
connection.execute(
|
|
"""INSERT INTO task_lineage_supersession_receipts(
|
|
decision_id,nonce,source_task_id,supersession_ordinal,child_task_id,
|
|
owner_decision_sha256,decision_json,receipt_json,consumed_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?)""",
|
|
(decision["decision_id"], decision["nonce"], SOURCE_TASK_ID, SUPERSESSION_ORDINAL,
|
|
CHILD_TASK_ID, decision_sha, canonical_json(dict(raw)), canonical_json(receipt), now_text),
|
|
)
|
|
connection.commit()
|
|
return {**receipt, "idempotent": False}
|
|
except Exception:
|
|
connection.rollback()
|
|
raise
|