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.
286 lines
12 KiB
Python
286 lines
12 KiB
Python
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, replace
|
|
from datetime import datetime
|
|
from typing import Mapping
|
|
|
|
if not __package__:
|
|
from aios_task_lineage import AcceptanceEvaluator, RelationType, TaskLineage, lineage_to_metadata, lineage_from_metadata, root_review_to_record
|
|
from aios_task_lineage_integration import (
|
|
DISPLAY_NAME_METADATA_KEY,
|
|
IntegrationError,
|
|
LINEAGE_METADATA_KEY,
|
|
NewTaskIdentityRequest,
|
|
PreparedTaskIdentity,
|
|
TaskIdentityLineageService,
|
|
TerminalReviewOutcome,
|
|
)
|
|
from aios_task_lineage_taskplane_adapter import (
|
|
AuthenticatedTaskContext,
|
|
InProcessTaskPlaneLineageAdapter,
|
|
require_context_matches_task,
|
|
)
|
|
else:
|
|
from .aios_task_lineage import AcceptanceEvaluator, RelationType, TaskLineage, lineage_to_metadata, lineage_from_metadata, root_review_to_record
|
|
from .aios_task_lineage_integration import (
|
|
DISPLAY_NAME_METADATA_KEY,
|
|
IntegrationError,
|
|
LINEAGE_METADATA_KEY,
|
|
NewTaskIdentityRequest,
|
|
PreparedTaskIdentity,
|
|
TaskIdentityLineageService,
|
|
TerminalReviewOutcome,
|
|
)
|
|
from .aios_task_lineage_taskplane_adapter import (
|
|
AuthenticatedTaskContext,
|
|
InProcessTaskPlaneLineageAdapter,
|
|
require_context_matches_task,
|
|
)
|
|
|
|
|
|
class LifecycleHookError(IntegrationError):
|
|
pass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class IdentityPersistenceOutcome:
|
|
prepared: PreparedTaskIdentity
|
|
persisted_revision_at_least: int
|
|
independently_verified: bool
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LegacyAttachOutcome:
|
|
lineage: TaskLineage
|
|
independently_verified: bool
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TerminalHookOutcome:
|
|
outcome: TerminalReviewOutcome
|
|
root_review_persisted: bool
|
|
dispatch_persisted: bool
|
|
independently_verified: bool
|
|
|
|
|
|
class TaskLineageLifecycleHooks:
|
|
"""Concrete lifecycle hook coordinator.
|
|
|
|
It does not own TaskPlane state. It orchestrates existing TaskPlane CAS,
|
|
journal/continuation and optional independent authority readback.
|
|
"""
|
|
|
|
def __init__(self, adapter: InProcessTaskPlaneLineageAdapter):
|
|
self._adapter = adapter
|
|
self._service = TaskIdentityLineageService(adapter)
|
|
|
|
def persist_new_task_identity(
|
|
self,
|
|
*,
|
|
request: NewTaskIdentityRequest,
|
|
context: AuthenticatedTaskContext,
|
|
max_cas_retries: int = 4,
|
|
require_independent_readback: bool = True,
|
|
) -> IdentityPersistenceOutcome:
|
|
if max_cas_retries < 1:
|
|
raise LifecycleHookError("max_cas_retries must be >= 1")
|
|
if require_independent_readback and not self._adapter.has_independent_readback:
|
|
raise LifecycleHookError("independent TaskPlane readback is not configured")
|
|
if request.owner_principal_id != context.principal_id:
|
|
raise LifecycleHookError("request principal does not match authenticated context")
|
|
if request.mission_id != context.mission_id:
|
|
raise LifecycleHookError("request mission does not match authenticated context")
|
|
|
|
for _ in range(max_cas_retries):
|
|
current = self._adapter.get_task_snapshot(request.task_id)
|
|
require_context_matches_task(context, current)
|
|
if current.created_at != request.created_at:
|
|
raise LifecycleHookError("request created_at does not match authority")
|
|
if (request.relation_type == RelationType.ROOT
|
|
and request.preferred_owner_alias not in (None, context.preferred_alias)):
|
|
raise LifecycleHookError("request alias does not match authenticated context")
|
|
request = replace(request, preferred_owner_alias=context.preferred_alias)
|
|
prepared = self._service.prepare_new_task_identity(request)
|
|
|
|
# Idempotent replay and legacy display-name repair are fail-closed:
|
|
# never overwrite an existing conflicting lineage record.
|
|
raw = current.metadata.get(LINEAGE_METADATA_KEY)
|
|
display = current.metadata.get(DISPLAY_NAME_METADATA_KEY)
|
|
if raw is not None and not isinstance(raw, Mapping):
|
|
raise LifecycleHookError("existing lineage metadata is malformed")
|
|
if isinstance(raw, Mapping):
|
|
# parent_revision records the creation observation, not the
|
|
# parent's current progress revision on an idempotent replay.
|
|
expected = dict(prepared.metadata_patch[LINEAGE_METADATA_KEY])
|
|
if request.parent_task_id is not None:
|
|
stored = lineage_from_metadata(dict(raw))
|
|
if (stored.parent_revision is None
|
|
or stored.parent_revision > prepared.lineage.parent_revision):
|
|
raise LifecycleHookError("existing parent revision conflicts with authority")
|
|
expected["parent_revision"] = stored.parent_revision
|
|
if raw != expected:
|
|
raise LifecycleHookError(
|
|
"existing lineage metadata conflicts with requested identity"
|
|
)
|
|
prepared = replace(prepared, lineage=lineage_from_metadata(dict(raw)),
|
|
metadata_patch={LINEAGE_METADATA_KEY: dict(raw),
|
|
DISPLAY_NAME_METADATA_KEY: prepared.display_name})
|
|
if display == prepared.display_name:
|
|
verified = False
|
|
if require_independent_readback:
|
|
self._adapter.verify_task_readback(
|
|
task_id=request.task_id,
|
|
expected_revision_at_least=current.revision,
|
|
expected_metadata=prepared.metadata_patch,
|
|
)
|
|
verified = True
|
|
return IdentityPersistenceOutcome(
|
|
prepared=prepared,
|
|
persisted_revision_at_least=current.revision,
|
|
independently_verified=verified,
|
|
)
|
|
if display not in (None, ""):
|
|
raise LifecycleHookError(
|
|
"existing display name conflicts with requested identity"
|
|
)
|
|
metadata_patch = {
|
|
DISPLAY_NAME_METADATA_KEY: prepared.display_name,
|
|
}
|
|
else:
|
|
if display not in (None, ""):
|
|
raise LifecycleHookError(
|
|
"display name exists without authoritative lineage"
|
|
)
|
|
metadata_patch = prepared.metadata_patch
|
|
|
|
ok = self._adapter.compare_and_swap_task_metadata(
|
|
task_id=request.task_id,
|
|
expected_revision=current.revision,
|
|
metadata_patch=metadata_patch,
|
|
)
|
|
if not ok:
|
|
continue
|
|
|
|
verified = False
|
|
if require_independent_readback:
|
|
self._adapter.verify_task_readback(
|
|
task_id=request.task_id,
|
|
expected_revision_at_least=current.revision + 1,
|
|
expected_metadata=prepared.metadata_patch,
|
|
)
|
|
verified = True
|
|
return IdentityPersistenceOutcome(
|
|
prepared=prepared,
|
|
persisted_revision_at_least=current.revision + 1,
|
|
independently_verified=verified,
|
|
)
|
|
|
|
raise LifecycleHookError("identity persistence CAS retry budget exhausted")
|
|
|
|
def attach_legacy_identity(
|
|
self,
|
|
*,
|
|
task_id: str,
|
|
parent_task_id: str,
|
|
relation_type: RelationType,
|
|
goal_slug: str,
|
|
context: AuthenticatedTaskContext,
|
|
max_cas_retries: int = 4,
|
|
require_independent_readback: bool = True,
|
|
) -> LegacyAttachOutcome:
|
|
if require_independent_readback and not self._adapter.has_independent_readback:
|
|
raise LifecycleHookError("independent TaskPlane readback is not configured")
|
|
before = self._adapter.get_task_snapshot(task_id)
|
|
require_context_matches_task(context, before)
|
|
lineage = self._service.attach_legacy_task(
|
|
task_id=task_id,
|
|
parent_task_id=parent_task_id,
|
|
relation_type=relation_type,
|
|
goal_slug=goal_slug,
|
|
max_cas_retries=max_cas_retries,
|
|
expected_owner_principal_id=context.principal_id,
|
|
expected_mission_id=context.mission_id,
|
|
)
|
|
verified = False
|
|
if require_independent_readback:
|
|
self._adapter.verify_task_readback(
|
|
task_id=task_id,
|
|
expected_revision_at_least=before.revision,
|
|
expected_metadata={
|
|
LINEAGE_METADATA_KEY: lineage_to_metadata(lineage),
|
|
DISPLAY_NAME_METADATA_KEY: lineage.display_name,
|
|
},
|
|
)
|
|
# Domain serialization compatibility check below avoids requiring
|
|
# TaskLineage to expose a method in older versions.
|
|
verified = True
|
|
return LegacyAttachOutcome(lineage=lineage, independently_verified=verified)
|
|
|
|
def on_proven_terminal_child(
|
|
self,
|
|
*,
|
|
child_task_id: str,
|
|
context: AuthenticatedTaskContext,
|
|
acceptance_evaluator: AcceptanceEvaluator,
|
|
reviewed_at: datetime,
|
|
max_cas_retries: int = 4,
|
|
require_independent_readback: bool = True,
|
|
) -> TerminalHookOutcome:
|
|
if require_independent_readback and not self._adapter.has_independent_readback:
|
|
raise LifecycleHookError("independent TaskPlane readback is not configured")
|
|
child = self._adapter.get_task_snapshot(child_task_id)
|
|
require_context_matches_task(context, child)
|
|
if not child.terminal_evidence_proven:
|
|
raise LifecycleHookError(
|
|
"terminal child hook requires proven terminal evidence"
|
|
)
|
|
raw = child.metadata.get(LINEAGE_METADATA_KEY)
|
|
if not isinstance(raw, Mapping):
|
|
raise LifecycleHookError("terminal child lineage missing")
|
|
lineage = lineage_from_metadata(dict(raw))
|
|
root = self._adapter.get_task_snapshot(lineage.root_task_id)
|
|
require_context_matches_task(context, root)
|
|
|
|
def authorized_acceptance(root_record, descendants):
|
|
if (root_record.lineage.owner_principal_id != context.principal_id
|
|
or root_record.lineage.mission_id != context.mission_id):
|
|
raise LifecycleHookError("root authority changed during terminal review")
|
|
return acceptance_evaluator(root_record, descendants)
|
|
|
|
outcome = self._service.on_proven_terminal_child(
|
|
child_task_id=child_task_id,
|
|
acceptance_evaluator=authorized_acceptance,
|
|
reviewed_at=reviewed_at,
|
|
max_cas_retries=max_cas_retries,
|
|
)
|
|
|
|
persisted_review = self._adapter.get_root_review_record(
|
|
outcome.review.review_id
|
|
)
|
|
if persisted_review is None:
|
|
raise LifecycleHookError("root review was not persisted")
|
|
if persisted_review != root_review_to_record(outcome.review):
|
|
raise LifecycleHookError("persisted review does not match returned review")
|
|
|
|
dispatch = self._adapter.get_root_action_dispatch(outcome.review.review_id)
|
|
if dispatch is None:
|
|
raise LifecycleHookError("root action dispatch was not persisted")
|
|
if dispatch != outcome.dispatch:
|
|
raise LifecycleHookError("persisted dispatch does not match returned receipt")
|
|
|
|
verified = False
|
|
if require_independent_readback:
|
|
self._adapter.verify_root_readback(
|
|
root_task_id=outcome.review.root_task_id,
|
|
expected_review_id=outcome.review.review_id,
|
|
)
|
|
verified = True
|
|
|
|
return TerminalHookOutcome(
|
|
outcome=outcome,
|
|
root_review_persisted=True,
|
|
dispatch_persisted=True,
|
|
independently_verified=verified,
|
|
)
|