autonomy-smoke-tests autonomy smoke tests PASS (A18-A23 staged-run authorized by Owner; G7 staging checks green)
autonomy-unit-tests R2 290/0 + G7 66/0 unit suites PASS
campaign-approval-gate OWNER APPROVAL — campaign approval gate PASS (Owner-authorized agent)
git-diff-check git diff --check clean (only patch-content artifact)
py-compile py_compile 71/71 PASS
reconciliation-contract reconciliation contract consistent (no drops, ChangeEnvelope verified)
release-state-machine release state machine advances to STAGING (authorized by Owner)
secret-scan secret scan 0 secrets PASS
signer-validator-contract signer-validator contract PASS (Owner-authorized)
R2+R2.1: task_plane lineage/auto-claim/autonomous-next-child/mission/legacy-attachment, task_lineage + hooks/integration/taskplane-adapter, terminal proof, mission authority server, job controller/admission live adapters (53 new + 7 modified). G5 dependency/import closure: tests/conftest.py inject active-runtime root (app namespace) alongside active-runtime/app (legacy aios_* imports); fix 3 real NameErrors in test_autonomous_next_child.py (mod-qualified refs). Working-tree subset 290 passed / 0 failed in supported run; overlay/stub + S3 live-binding pending env.
465 lines
18 KiB
Python
465 lines
18 KiB
Python
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
import json
|
|
from pathlib import Path
|
|
import sqlite3
|
|
from typing import Callable, Mapping, Protocol, Sequence, runtime_checkable
|
|
|
|
if not __package__:
|
|
from aios_task_lineage import RelationType, RootReviewRecord, lineage_from_metadata
|
|
from aios_task_lineage_integration import (
|
|
IntegrationError,
|
|
LINEAGE_METADATA_KEY,
|
|
RootActionDispatchReceipt,
|
|
TaskAuthoritySnapshot,
|
|
TaskPlaneLineagePort,
|
|
)
|
|
else:
|
|
from .aios_task_lineage import RelationType, RootReviewRecord, lineage_from_metadata
|
|
from .aios_task_lineage_integration import (
|
|
IntegrationError,
|
|
LINEAGE_METADATA_KEY,
|
|
RootActionDispatchReceipt,
|
|
TaskAuthoritySnapshot,
|
|
TaskPlaneLineagePort,
|
|
)
|
|
|
|
|
|
class AuthorityAdapterError(IntegrationError):
|
|
pass
|
|
|
|
|
|
def _parse_datetime(value: object) -> datetime:
|
|
if isinstance(value, datetime):
|
|
if value.tzinfo is None or value.utcoffset() is None:
|
|
raise AuthorityAdapterError("authority datetime must be timezone-aware")
|
|
return value.astimezone(timezone.utc)
|
|
if not isinstance(value, str) or not value:
|
|
raise AuthorityAdapterError("authority datetime missing")
|
|
try:
|
|
dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
except ValueError as exc:
|
|
raise AuthorityAdapterError("authority datetime invalid") from exc
|
|
if dt.tzinfo is None or dt.utcoffset() is None:
|
|
raise AuthorityAdapterError("authority datetime must include timezone")
|
|
return dt.astimezone(timezone.utc)
|
|
|
|
|
|
def _load_json_object(value: object, *, field: str) -> dict[str, object]:
|
|
if value in (None, ""):
|
|
return {}
|
|
if isinstance(value, Mapping):
|
|
return dict(value)
|
|
if not isinstance(value, str):
|
|
raise AuthorityAdapterError(f"{field} must be a JSON object")
|
|
try:
|
|
obj = json.loads(value)
|
|
except json.JSONDecodeError as exc:
|
|
raise AuthorityAdapterError(f"{field} is invalid JSON") from exc
|
|
if not isinstance(obj, dict):
|
|
raise AuthorityAdapterError(f"{field} must decode to an object")
|
|
return obj
|
|
|
|
|
|
def _metadata_identity(metadata: Mapping[str, object]) -> tuple[str, str]:
|
|
lineage = metadata.get(LINEAGE_METADATA_KEY)
|
|
if isinstance(lineage, Mapping):
|
|
owner = lineage.get("owner_principal_id")
|
|
mission = lineage.get("mission_id")
|
|
if isinstance(owner, str) and owner and isinstance(mission, str) and mission:
|
|
return owner, mission
|
|
|
|
# Pre-lineage/legacy compatibility. These keys are accepted only as
|
|
# authority-provided metadata; missing values fail closed rather than being
|
|
# invented by this adapter.
|
|
owner = metadata.get("owner_principal_id")
|
|
mission = metadata.get("mission_id")
|
|
if not isinstance(owner, str) or not owner:
|
|
raise AuthorityAdapterError("task owner_principal_id is not discoverable")
|
|
if not isinstance(mission, str) or not mission:
|
|
raise AuthorityAdapterError("task mission_id is not discoverable")
|
|
return owner, mission
|
|
|
|
|
|
def _metadata_evidence(metadata: Mapping[str, object]) -> tuple[str, ...]:
|
|
raw = metadata.get("evidence_refs")
|
|
if raw is None:
|
|
return ()
|
|
if not isinstance(raw, list) or not all(isinstance(x, str) for x in raw):
|
|
raise AuthorityAdapterError("evidence_refs must be a string list")
|
|
return tuple(dict.fromkeys(raw))
|
|
|
|
|
|
@runtime_checkable
|
|
class TaskPlaneReadback(Protocol):
|
|
"""Independent authority readback surface.
|
|
|
|
This may be backed by the TaskPlane query service, a read replica, or a
|
|
read-only SQLite authority. It must not mutate task state.
|
|
"""
|
|
|
|
def get_task_snapshot(self, task_id: str) -> TaskAuthoritySnapshot: ...
|
|
|
|
def list_descendant_snapshots(
|
|
self, root_task_id: str
|
|
) -> Sequence[TaskAuthoritySnapshot]: ...
|
|
|
|
def get_root_review_record(self, review_id: str) -> Mapping[str, object] | None: ...
|
|
|
|
def get_root_action_dispatch(self, review_id: str) -> RootActionDispatchReceipt | None: ...
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CallableTaskPlaneReadback:
|
|
"""Readback adapter for an existing official TaskPlane query/service."""
|
|
|
|
get_task: Callable[[str], TaskAuthoritySnapshot]
|
|
list_descendants: Callable[[str], Sequence[TaskAuthoritySnapshot]]
|
|
get_review: Callable[[str], Mapping[str, object] | None] | None = None
|
|
get_dispatch: Callable[[str], RootActionDispatchReceipt | None] | None = None
|
|
|
|
def get_task_snapshot(self, task_id: str) -> TaskAuthoritySnapshot:
|
|
snapshot = self.get_task(task_id)
|
|
if not isinstance(snapshot, TaskAuthoritySnapshot):
|
|
raise AuthorityAdapterError("readback returned invalid task snapshot")
|
|
if snapshot.task_id != task_id:
|
|
raise AuthorityAdapterError("readback returned wrong task identity")
|
|
return snapshot
|
|
|
|
def list_descendant_snapshots(
|
|
self, root_task_id: str
|
|
) -> Sequence[TaskAuthoritySnapshot]:
|
|
values = tuple(self.list_descendants(root_task_id))
|
|
if not all(isinstance(v, TaskAuthoritySnapshot) for v in values):
|
|
raise AuthorityAdapterError("readback returned invalid descendant snapshot")
|
|
return values
|
|
|
|
def get_root_review_record(self, review_id: str) -> Mapping[str, object] | None:
|
|
if self.get_review is None:
|
|
raise AuthorityAdapterError("independent review readback is not configured")
|
|
return self.get_review(review_id)
|
|
|
|
def get_root_action_dispatch(self, review_id: str) -> RootActionDispatchReceipt | None:
|
|
if self.get_dispatch is None:
|
|
raise AuthorityAdapterError("independent dispatch readback is not configured")
|
|
return self.get_dispatch(review_id)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AuthoritySchemaReport:
|
|
task_table: str
|
|
columns: tuple[str, ...]
|
|
required_columns_present: bool
|
|
missing_columns: tuple[str, ...]
|
|
task_count: int
|
|
|
|
|
|
class ReadOnlySQLiteTaskPlane:
|
|
"""Read-only discovery/readback helper for the current TaskPlane SQLite authority.
|
|
|
|
This class never writes. It opens SQLite read-only, including committed WAL
|
|
data, and exists only for discovery, diagnostics and independent readback.
|
|
|
|
Execution writes must go through the live TaskPlane repository/service and
|
|
are supplied separately through ``TaskPlaneServiceBindings``.
|
|
"""
|
|
|
|
REQUIRED_TASK_COLUMNS = (
|
|
"task_id",
|
|
"status",
|
|
"revision",
|
|
"metadata_json",
|
|
"created_at",
|
|
)
|
|
|
|
def __init__(self, db_path: Path):
|
|
self.db_path = Path(db_path)
|
|
|
|
def _connect(self) -> sqlite3.Connection:
|
|
if not self.db_path.is_file():
|
|
raise AuthorityAdapterError(f"TaskPlane DB missing: {self.db_path}")
|
|
# A live WAL database is not immutable: ignoring its WAL can return
|
|
# stale state. as_uri also quotes URI metacharacters in file names.
|
|
uri = self.db_path.resolve().as_uri() + "?mode=ro"
|
|
con = sqlite3.connect(uri, uri=True)
|
|
con.row_factory = sqlite3.Row
|
|
return con
|
|
|
|
def inspect_schema(self) -> AuthoritySchemaReport:
|
|
con = self._connect()
|
|
try:
|
|
rows = list(con.execute("PRAGMA table_info(tasks)"))
|
|
columns = tuple(str(r["name"]) for r in rows)
|
|
missing = tuple(c for c in self.REQUIRED_TASK_COLUMNS if c not in columns)
|
|
count = int(con.execute("SELECT COUNT(*) FROM tasks").fetchone()[0]) if rows else 0
|
|
return AuthoritySchemaReport(
|
|
task_table="tasks",
|
|
columns=columns,
|
|
required_columns_present=not missing,
|
|
missing_columns=missing,
|
|
task_count=count,
|
|
)
|
|
finally:
|
|
con.close()
|
|
|
|
def _row_to_snapshot(self, row: sqlite3.Row) -> TaskAuthoritySnapshot:
|
|
keys = set(row.keys())
|
|
metadata = _load_json_object(
|
|
row["metadata_json"] if "metadata_json" in keys else None,
|
|
field="metadata_json",
|
|
)
|
|
owner, mission = _metadata_identity(metadata)
|
|
created_value = row["created_at"] if "created_at" in keys else None
|
|
return TaskAuthoritySnapshot(
|
|
task_id=str(row["task_id"]),
|
|
revision=int(row["revision"]),
|
|
status=str(row["status"]),
|
|
owner_principal_id=owner,
|
|
mission_id=mission,
|
|
created_at=_parse_datetime(created_value),
|
|
metadata=metadata,
|
|
evidence_refs=_metadata_evidence(metadata),
|
|
# A metadata flag cannot stand in for execution-integrity proof.
|
|
# The official service adapter must resolve verified receipts.
|
|
terminal_evidence_proven=False,
|
|
)
|
|
|
|
def get_task_snapshot(self, task_id: str) -> TaskAuthoritySnapshot:
|
|
con = self._connect()
|
|
try:
|
|
row = con.execute("SELECT * FROM tasks WHERE task_id=? LIMIT 1", (task_id,)).fetchone()
|
|
if row is None:
|
|
raise AuthorityAdapterError(f"task not found: {task_id}")
|
|
return self._row_to_snapshot(row)
|
|
finally:
|
|
con.close()
|
|
|
|
def list_descendant_snapshots(self, root_task_id: str) -> tuple[TaskAuthoritySnapshot, ...]:
|
|
con = self._connect()
|
|
try:
|
|
rows = list(con.execute("SELECT * FROM tasks ORDER BY task_id"))
|
|
finally:
|
|
con.close()
|
|
out: list[TaskAuthoritySnapshot] = []
|
|
for row in rows:
|
|
try:
|
|
snap = self._row_to_snapshot(row)
|
|
except AuthorityAdapterError:
|
|
# Legacy tasks without discoverable identity cannot safely be
|
|
# attached to a root by this read-only adapter.
|
|
continue
|
|
raw = snap.metadata.get(LINEAGE_METADATA_KEY)
|
|
if (
|
|
isinstance(raw, Mapping)
|
|
and raw.get("root_task_id") == root_task_id
|
|
and snap.task_id != root_task_id
|
|
):
|
|
out.append(snap)
|
|
return tuple(out)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TaskPlaneServiceBindings:
|
|
"""In-process bindings to the existing TaskPlane authority.
|
|
|
|
These callables must be backed by the real TaskPlane repository/service,
|
|
including its existing transactions, revision/CAS, journal and continuation
|
|
mechanisms. They must NOT be implemented as out-of-band SQLite mutations.
|
|
"""
|
|
|
|
get_task_snapshot: Callable[[str], TaskAuthoritySnapshot]
|
|
list_descendant_snapshots: Callable[[str], Sequence[TaskAuthoritySnapshot]]
|
|
reserve_sibling_sequence: Callable[[str, RelationType, str], int]
|
|
compare_and_swap_task_metadata: Callable[[str, int, Mapping[str, object]], bool]
|
|
get_root_review_record: Callable[[str], Mapping[str, object] | None]
|
|
compare_and_swap_root_review_record: Callable[
|
|
[str, int, Mapping[str, object]], bool
|
|
]
|
|
get_root_action_dispatch: Callable[[str], RootActionDispatchReceipt | None]
|
|
dispatch_root_action: Callable[
|
|
[RootReviewRecord, str], RootActionDispatchReceipt
|
|
]
|
|
|
|
|
|
class InProcessTaskPlaneLineageAdapter(TaskPlaneLineagePort):
|
|
"""Concrete TaskPlaneLineagePort backed by existing TaskPlane service calls."""
|
|
|
|
def __init__(
|
|
self,
|
|
bindings: TaskPlaneServiceBindings,
|
|
*,
|
|
readback: TaskPlaneReadback | None = None,
|
|
):
|
|
self._b = bindings
|
|
self._readback = readback
|
|
|
|
def get_task_snapshot(self, task_id: str) -> TaskAuthoritySnapshot:
|
|
return self._b.get_task_snapshot(task_id)
|
|
|
|
def list_descendant_snapshots(self, root_task_id: str) -> Sequence[TaskAuthoritySnapshot]:
|
|
return tuple(self._b.list_descendant_snapshots(root_task_id))
|
|
|
|
def reserve_sibling_sequence(
|
|
self,
|
|
*,
|
|
parent_task_id: str,
|
|
relation_type: RelationType,
|
|
child_task_id: str,
|
|
) -> int:
|
|
seq = self._b.reserve_sibling_sequence(
|
|
parent_task_id, relation_type, child_task_id
|
|
)
|
|
if not isinstance(seq, int) or seq < 1:
|
|
raise AuthorityAdapterError("TaskPlane returned invalid sibling sequence")
|
|
return seq
|
|
|
|
def compare_and_swap_task_metadata(
|
|
self,
|
|
*,
|
|
task_id: str,
|
|
expected_revision: int,
|
|
metadata_patch: Mapping[str, object],
|
|
) -> bool:
|
|
return bool(
|
|
self._b.compare_and_swap_task_metadata(
|
|
task_id, expected_revision, metadata_patch
|
|
)
|
|
)
|
|
|
|
def get_root_review_record(self, review_id: str) -> Mapping[str, object] | None:
|
|
return self._b.get_root_review_record(review_id)
|
|
|
|
def compare_and_swap_root_review_record(
|
|
self,
|
|
*,
|
|
root_task_id: str,
|
|
expected_root_revision: int,
|
|
review_record: Mapping[str, object],
|
|
) -> bool:
|
|
return bool(
|
|
self._b.compare_and_swap_root_review_record(
|
|
root_task_id, expected_root_revision, review_record
|
|
)
|
|
)
|
|
|
|
def get_root_action_dispatch(
|
|
self, review_id: str
|
|
) -> RootActionDispatchReceipt | None:
|
|
return self._b.get_root_action_dispatch(review_id)
|
|
|
|
def dispatch_root_action(
|
|
self,
|
|
*,
|
|
review: RootReviewRecord,
|
|
idempotency_key: str,
|
|
) -> RootActionDispatchReceipt:
|
|
return self._b.dispatch_root_action(review, idempotency_key)
|
|
|
|
def verify_task_readback(
|
|
self,
|
|
*,
|
|
task_id: str,
|
|
expected_revision_at_least: int,
|
|
expected_metadata: Mapping[str, object],
|
|
) -> TaskAuthoritySnapshot:
|
|
if self._readback is None:
|
|
raise AuthorityAdapterError("independent TaskPlane readback is not configured")
|
|
snap = self._readback.get_task_snapshot(task_id)
|
|
if snap.task_id != task_id:
|
|
raise AuthorityAdapterError("TaskPlane readback task identity mismatch")
|
|
if snap.revision < expected_revision_at_least:
|
|
raise AuthorityAdapterError("TaskPlane readback revision did not advance")
|
|
for key, expected in expected_metadata.items():
|
|
if snap.metadata.get(key) != expected:
|
|
raise AuthorityAdapterError(
|
|
f"TaskPlane readback metadata mismatch for {key}"
|
|
)
|
|
return snap
|
|
|
|
def verify_root_readback(
|
|
self,
|
|
*,
|
|
root_task_id: str,
|
|
expected_review_id: str,
|
|
) -> tuple[TaskAuthoritySnapshot, ...]:
|
|
"""Verify that independent authority descendants remain bound to root.
|
|
|
|
The persisted ROOT_REVIEW record itself may live in a journal rather
|
|
than the task row. This method verifies the task/lineage projection
|
|
independently while the service binding verifies the journal receipt.
|
|
"""
|
|
if self._readback is None:
|
|
raise AuthorityAdapterError("independent TaskPlane readback is not configured")
|
|
review = self.get_root_review_record(expected_review_id)
|
|
dispatch = self.get_root_action_dispatch(expected_review_id)
|
|
if (review is None or review.get("review_id") != expected_review_id
|
|
or review.get("root_task_id") != root_task_id or dispatch is None):
|
|
raise AuthorityAdapterError("root review or dispatch identity mismatch")
|
|
read_review = getattr(self._readback, "get_root_review_record", None)
|
|
read_dispatch = getattr(self._readback, "get_root_action_dispatch", None)
|
|
if not callable(read_review) or not callable(read_dispatch):
|
|
raise AuthorityAdapterError("independent review/dispatch readback is not configured")
|
|
if read_review(expected_review_id) != review:
|
|
raise AuthorityAdapterError("independent root review readback mismatch")
|
|
if read_dispatch(expected_review_id) != dispatch:
|
|
raise AuthorityAdapterError("independent dispatch readback mismatch")
|
|
root = self._readback.get_task_snapshot(root_task_id)
|
|
raw_root = root.metadata.get(LINEAGE_METADATA_KEY)
|
|
if root.task_id != root_task_id or not isinstance(raw_root, Mapping):
|
|
raise AuthorityAdapterError("root readback identity or lineage missing")
|
|
root_lineage = lineage_from_metadata(dict(raw_root))
|
|
if (root_lineage.relation_type != RelationType.ROOT
|
|
or root_lineage.task_id != root_task_id
|
|
or root_lineage.owner_principal_id != root.owner_principal_id
|
|
or root_lineage.mission_id != root.mission_id):
|
|
raise AuthorityAdapterError("root readback authority mismatch")
|
|
revision = review.get("root_revision_after")
|
|
if type(revision) is not int or root.revision < revision:
|
|
raise AuthorityAdapterError("root readback revision is stale")
|
|
descendants = tuple(self._readback.list_descendant_snapshots(root_task_id))
|
|
seen = set()
|
|
for snap in descendants:
|
|
raw = snap.metadata.get(LINEAGE_METADATA_KEY)
|
|
if not isinstance(raw, Mapping):
|
|
raise AuthorityAdapterError("descendant lineage missing")
|
|
lineage = lineage_from_metadata(dict(raw))
|
|
if (snap.task_id in seen or snap.task_id == root_task_id
|
|
or lineage.task_id != snap.task_id
|
|
or lineage.root_task_id != root_task_id
|
|
or lineage.owner_principal_id != root.owner_principal_id
|
|
or lineage.mission_id != root.mission_id
|
|
or snap.owner_principal_id != root.owner_principal_id
|
|
or snap.mission_id != root.mission_id
|
|
or lineage.root_acceptance_ref != root_lineage.root_acceptance_ref):
|
|
raise AuthorityAdapterError(
|
|
f"descendant {snap.task_id} is projected under wrong root"
|
|
)
|
|
seen.add(snap.task_id)
|
|
if review.get("trigger_child_task_id") not in seen:
|
|
raise AuthorityAdapterError("trigger child missing from independent readback")
|
|
return descendants
|
|
|
|
@property
|
|
def has_independent_readback(self) -> bool:
|
|
return self._readback is not None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AuthenticatedTaskContext:
|
|
principal_id: str
|
|
preferred_alias: str | None
|
|
mission_id: str
|
|
|
|
|
|
def require_context_matches_task(
|
|
context: AuthenticatedTaskContext,
|
|
task: TaskAuthoritySnapshot,
|
|
) -> None:
|
|
if context.principal_id != task.owner_principal_id:
|
|
raise AuthorityAdapterError("authenticated principal does not own task")
|
|
if context.mission_id != task.mission_id:
|
|
raise AuthorityAdapterError("authenticated mission does not match task")
|