872 lines
36 KiB
Python
872 lines
36 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import sqlite3
|
|
import threading
|
|
import time
|
|
import uuid
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from contextlib import closing
|
|
from dataclasses import asdict, dataclass, field
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
from typing import Any, Callable, Iterable, Mapping, Sequence
|
|
|
|
|
|
SCHEMA_RUNTIME_MANIFEST = "aios.runtime-manifest.v1"
|
|
SCHEMA_DECISION = "aios.governance-decision.v1"
|
|
|
|
|
|
def canonical_json_bytes(value: Any) -> bytes:
|
|
"""Private control-kernel encoding; not a replacement for published AIOS digests."""
|
|
return json.dumps(
|
|
value,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
ensure_ascii=False,
|
|
allow_nan=False,
|
|
).encode("utf-8")
|
|
|
|
|
|
def sha256_hex(value: bytes) -> str:
|
|
return hashlib.sha256(value).hexdigest()
|
|
|
|
|
|
class GovernanceControlError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class PreflightNotReady(GovernanceControlError):
|
|
pass
|
|
|
|
|
|
class StaleManifest(GovernanceControlError):
|
|
pass
|
|
|
|
|
|
class IdempotencyConflict(GovernanceControlError):
|
|
pass
|
|
|
|
|
|
class RetryBudgetExhausted(GovernanceControlError):
|
|
pass
|
|
|
|
|
|
class ConcurrentWriterDenied(GovernanceControlError):
|
|
pass
|
|
|
|
|
|
class InvalidStateTransition(GovernanceControlError):
|
|
pass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DiscoveryObservation:
|
|
worker: str
|
|
observed_at: float
|
|
runtime_fingerprint: str
|
|
process_start_identity: str
|
|
activation_digest: str
|
|
epoch_id: str | None
|
|
policy_digest: str | None
|
|
plan_provider_root: str | None
|
|
task_id: str | None
|
|
grant_id: str | None
|
|
facts: Mapping[str, Any] = field(default_factory=dict)
|
|
evidence_refs: tuple[str, ...] = ()
|
|
|
|
@classmethod
|
|
def build(
|
|
cls,
|
|
*,
|
|
worker: str,
|
|
runtime_fingerprint: str,
|
|
process_start_identity: str,
|
|
activation_digest: str,
|
|
epoch_id: str | None,
|
|
policy_digest: str | None,
|
|
plan_provider_root: str | None,
|
|
task_id: str | None,
|
|
grant_id: str | None,
|
|
facts: Mapping[str, Any] | None = None,
|
|
evidence_refs: Sequence[str] = (),
|
|
observed_at: float | None = None,
|
|
) -> "DiscoveryObservation":
|
|
return cls(
|
|
worker=str(worker),
|
|
observed_at=float(time.time() if observed_at is None else observed_at),
|
|
runtime_fingerprint=str(runtime_fingerprint),
|
|
process_start_identity=str(process_start_identity),
|
|
activation_digest=str(activation_digest),
|
|
epoch_id=None if epoch_id is None else str(epoch_id),
|
|
policy_digest=None if policy_digest is None else str(policy_digest),
|
|
plan_provider_root=None if plan_provider_root is None else str(plan_provider_root),
|
|
task_id=None if task_id is None else str(task_id),
|
|
grant_id=None if grant_id is None else str(grant_id),
|
|
facts=dict(facts or {}),
|
|
evidence_refs=tuple(str(x) for x in evidence_refs),
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RuntimeManifest:
|
|
observed_at: float
|
|
runtime_fingerprint: str
|
|
process_start_identity: str
|
|
activation_digest: str
|
|
epoch_id: str | None
|
|
policy_digest: str | None
|
|
plan_provider_root: str | None
|
|
task_id: str | None
|
|
grant_id: str | None
|
|
worker_facts: Mapping[str, Mapping[str, Any]]
|
|
evidence_refs: tuple[str, ...]
|
|
schema: str = SCHEMA_RUNTIME_MANIFEST
|
|
|
|
def payload(self) -> dict[str, Any]:
|
|
return {
|
|
"schema": self.schema,
|
|
"observed_at": self.observed_at,
|
|
"runtime_fingerprint": self.runtime_fingerprint,
|
|
"process_start_identity": self.process_start_identity,
|
|
"activation_digest": self.activation_digest,
|
|
"epoch_id": self.epoch_id,
|
|
"policy_digest": self.policy_digest,
|
|
"plan_provider_root": self.plan_provider_root,
|
|
"task_id": self.task_id,
|
|
"grant_id": self.grant_id,
|
|
"worker_facts": {k: dict(v) for k, v in sorted(self.worker_facts.items())},
|
|
"evidence_refs": list(self.evidence_refs),
|
|
}
|
|
|
|
@property
|
|
def manifest_id(self) -> str:
|
|
return "sha256:" + sha256_hex(canonical_json_bytes(self.payload()))
|
|
|
|
|
|
class ConsistencyBarrier:
|
|
_IDENTITY_FIELDS = (
|
|
"runtime_fingerprint",
|
|
"process_start_identity",
|
|
"activation_digest",
|
|
"epoch_id",
|
|
"policy_digest",
|
|
"plan_provider_root",
|
|
"task_id",
|
|
"grant_id",
|
|
)
|
|
|
|
def __init__(self, expected_workers: Iterable[str]):
|
|
self.expected_workers = frozenset(str(x) for x in expected_workers)
|
|
if not self.expected_workers:
|
|
raise ValueError("expected_workers must not be empty")
|
|
self._observations: dict[str, DiscoveryObservation] = {}
|
|
|
|
def submit(self, observation: DiscoveryObservation) -> None:
|
|
if observation.worker not in self.expected_workers:
|
|
raise PreflightNotReady(f"unexpected discovery worker: {observation.worker}")
|
|
if observation.worker in self._observations:
|
|
raise PreflightNotReady(f"duplicate discovery worker: {observation.worker}")
|
|
self._observations[observation.worker] = observation
|
|
|
|
def freeze(self) -> RuntimeManifest:
|
|
missing = sorted(self.expected_workers - self._observations.keys())
|
|
if missing:
|
|
raise PreflightNotReady("missing discovery workers: " + ",".join(missing))
|
|
ordered = [self._observations[k] for k in sorted(self._observations)]
|
|
anchor = ordered[0]
|
|
for other in ordered[1:]:
|
|
for name in self._IDENTITY_FIELDS:
|
|
if getattr(anchor, name) != getattr(other, name):
|
|
raise StaleManifest(
|
|
f"consistency barrier mismatch field={name} "
|
|
f"anchor={getattr(anchor, name)!r} "
|
|
f"worker={other.worker} value={getattr(other, name)!r}"
|
|
)
|
|
evidence: list[str] = []
|
|
facts: dict[str, Mapping[str, Any]] = {}
|
|
observed_at = 0.0
|
|
for item in ordered:
|
|
observed_at = max(observed_at, item.observed_at)
|
|
evidence.extend(item.evidence_refs)
|
|
facts[item.worker] = dict(item.facts)
|
|
return RuntimeManifest(
|
|
observed_at=observed_at,
|
|
runtime_fingerprint=anchor.runtime_fingerprint,
|
|
process_start_identity=anchor.process_start_identity,
|
|
activation_digest=anchor.activation_digest,
|
|
epoch_id=anchor.epoch_id,
|
|
policy_digest=anchor.policy_digest,
|
|
plan_provider_root=anchor.plan_provider_root,
|
|
task_id=anchor.task_id,
|
|
grant_id=anchor.grant_id,
|
|
worker_facts=facts,
|
|
evidence_refs=tuple(dict.fromkeys(evidence)),
|
|
)
|
|
|
|
|
|
class ParallelDiscoveryCoordinator:
|
|
def __init__(self, workers: Mapping[str, Callable[[], DiscoveryObservation]], *, max_workers: int | None = None):
|
|
if not workers:
|
|
raise ValueError("workers must not be empty")
|
|
self.workers = dict(workers)
|
|
self.max_workers = max_workers or min(8, len(self.workers))
|
|
|
|
def discover(self) -> RuntimeManifest:
|
|
barrier = ConsistencyBarrier(self.workers.keys())
|
|
errors: dict[str, str] = {}
|
|
with ThreadPoolExecutor(max_workers=self.max_workers, thread_name_prefix="aios-discovery") as pool:
|
|
futures = {pool.submit(fn): name for name, fn in self.workers.items()}
|
|
for future in as_completed(futures):
|
|
name = futures[future]
|
|
try:
|
|
observation = future.result()
|
|
if observation.worker != name:
|
|
raise PreflightNotReady(
|
|
f"worker name mismatch expected={name!r} got={observation.worker!r}"
|
|
)
|
|
barrier.submit(observation)
|
|
except Exception as exc:
|
|
errors[name] = f"{type(exc).__name__}:{exc}"
|
|
if errors:
|
|
details = ",".join(f"{k}={v}" for k, v in sorted(errors.items()))
|
|
raise PreflightNotReady(f"discovery failed: {details}")
|
|
return barrier.freeze()
|
|
|
|
|
|
class ManifestStore:
|
|
def __init__(self, db_path: str | Path):
|
|
self.db_path = str(db_path)
|
|
self._lock = threading.RLock()
|
|
self._init_schema()
|
|
|
|
def _connect(self) -> sqlite3.Connection:
|
|
conn = sqlite3.connect(self.db_path, timeout=5, isolation_level=None)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
def _init_schema(self) -> None:
|
|
with closing(self._connect()) as conn:
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
conn.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS frozen_manifests (
|
|
manifest_id TEXT PRIMARY KEY,
|
|
runtime_fingerprint TEXT NOT NULL,
|
|
process_start_identity TEXT NOT NULL,
|
|
canonical_json TEXT NOT NULL,
|
|
frozen_at REAL NOT NULL,
|
|
superseded_at REAL
|
|
)
|
|
"""
|
|
)
|
|
|
|
def freeze(self, manifest: RuntimeManifest) -> str:
|
|
manifest_id = manifest.manifest_id
|
|
canonical = canonical_json_bytes(manifest.payload()).decode("utf-8")
|
|
with self._lock, closing(self._connect()) as conn:
|
|
conn.execute("BEGIN IMMEDIATE")
|
|
row = conn.execute(
|
|
"SELECT canonical_json FROM frozen_manifests WHERE manifest_id=?",
|
|
(manifest_id,),
|
|
).fetchone()
|
|
if row is None:
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO frozen_manifests(
|
|
manifest_id,runtime_fingerprint,process_start_identity,
|
|
canonical_json,frozen_at,superseded_at
|
|
) VALUES(?,?,?,?,?,NULL)
|
|
""",
|
|
(
|
|
manifest_id,
|
|
manifest.runtime_fingerprint,
|
|
manifest.process_start_identity,
|
|
canonical,
|
|
time.time(),
|
|
),
|
|
)
|
|
elif row["canonical_json"] != canonical:
|
|
conn.rollback()
|
|
raise IdempotencyConflict("manifest_id collision with different canonical bytes")
|
|
conn.commit()
|
|
return manifest_id
|
|
|
|
def assert_current(self, *, manifest_id: str, runtime_fingerprint: str, process_start_identity: str) -> None:
|
|
with closing(self._connect()) as conn:
|
|
row = conn.execute(
|
|
"""
|
|
SELECT runtime_fingerprint,process_start_identity,superseded_at
|
|
FROM frozen_manifests WHERE manifest_id=?
|
|
""",
|
|
(manifest_id,),
|
|
).fetchone()
|
|
if row is None:
|
|
raise StaleManifest("unknown manifest_id")
|
|
if row["superseded_at"] is not None:
|
|
raise StaleManifest("manifest superseded")
|
|
if row["runtime_fingerprint"] != runtime_fingerprint:
|
|
raise StaleManifest("runtime fingerprint drift")
|
|
if row["process_start_identity"] != process_start_identity:
|
|
raise StaleManifest("process start identity drift")
|
|
|
|
def supersede(self, manifest_id: str) -> None:
|
|
with self._lock, closing(self._connect()) as conn:
|
|
conn.execute(
|
|
"UPDATE frozen_manifests SET superseded_at=? WHERE manifest_id=? AND superseded_at IS NULL",
|
|
(time.time(), manifest_id),
|
|
)
|
|
|
|
|
|
class ActionState(str, Enum):
|
|
RUNNING = "RUNNING"
|
|
SUCCEEDED = "SUCCEEDED"
|
|
FAILED = "FAILED"
|
|
EFFECT_UNKNOWN = "EFFECT_UNKNOWN"
|
|
|
|
|
|
class ActionKind(str, Enum):
|
|
READ_ONLY = "READ_ONLY"
|
|
MALFORMED_TOOL_REQUEST = "MALFORMED_TOOL_REQUEST"
|
|
ACTIVATION = "ACTIVATION"
|
|
RESTART = "RESTART"
|
|
EPOCH_ISSUANCE = "EPOCH_ISSUANCE"
|
|
ADMISSION = "ADMISSION"
|
|
CLAIM = "CLAIM"
|
|
COMMIT = "COMMIT"
|
|
GENERIC_MUTATION = "GENERIC_MUTATION"
|
|
|
|
|
|
DEFAULT_RETRY_BUDGETS: Mapping[ActionKind, int] = {
|
|
ActionKind.READ_ONLY: 2,
|
|
ActionKind.MALFORMED_TOOL_REQUEST: 1,
|
|
ActionKind.ACTIVATION: 1,
|
|
ActionKind.RESTART: 1,
|
|
ActionKind.EPOCH_ISSUANCE: 1,
|
|
ActionKind.ADMISSION: 1,
|
|
ActionKind.CLAIM: 1,
|
|
ActionKind.COMMIT: 1,
|
|
ActionKind.GENERIC_MUTATION: 1,
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ActionTicket:
|
|
action_id: str
|
|
idempotency_key: str
|
|
manifest_id: str
|
|
phase: str
|
|
kind: ActionKind
|
|
target: str
|
|
precondition_hash: str
|
|
attempt_count: int
|
|
max_attempts: int
|
|
state: ActionState
|
|
reused: bool = False
|
|
requires_reconcile: bool = False
|
|
|
|
|
|
class ActionLedger:
|
|
def __init__(self, db_path: str | Path, *, retry_budgets: Mapping[ActionKind, int] | None = None):
|
|
self.db_path = str(db_path)
|
|
self.retry_budgets = dict(DEFAULT_RETRY_BUDGETS)
|
|
if retry_budgets:
|
|
self.retry_budgets.update(retry_budgets)
|
|
self._lock = threading.RLock()
|
|
self._init_schema()
|
|
|
|
def _connect(self) -> sqlite3.Connection:
|
|
conn = sqlite3.connect(self.db_path, timeout=5, isolation_level=None)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
def _init_schema(self) -> None:
|
|
with closing(self._connect()) as conn:
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
conn.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS actions (
|
|
action_id TEXT PRIMARY KEY,
|
|
idempotency_key TEXT UNIQUE NOT NULL,
|
|
manifest_id TEXT NOT NULL,
|
|
phase TEXT NOT NULL,
|
|
kind TEXT NOT NULL,
|
|
target TEXT NOT NULL,
|
|
precondition_hash TEXT NOT NULL,
|
|
attempt_count INTEGER NOT NULL,
|
|
max_attempts INTEGER NOT NULL,
|
|
state TEXT NOT NULL,
|
|
result_json TEXT,
|
|
evidence_json TEXT,
|
|
expected_readback_json TEXT,
|
|
rollback_ref TEXT,
|
|
created_at REAL NOT NULL,
|
|
updated_at REAL NOT NULL
|
|
)
|
|
"""
|
|
)
|
|
|
|
def begin(
|
|
self,
|
|
*,
|
|
idempotency_key: str,
|
|
manifest_id: str,
|
|
phase: str,
|
|
kind: ActionKind,
|
|
target: str,
|
|
precondition_hash: str,
|
|
expected_readback: Mapping[str, Any] | None = None,
|
|
rollback_ref: str | None = None,
|
|
) -> ActionTicket:
|
|
max_attempts = int(self.retry_budgets[kind])
|
|
now = time.time()
|
|
with self._lock, closing(self._connect()) as conn:
|
|
conn.execute("BEGIN IMMEDIATE")
|
|
row = conn.execute(
|
|
"SELECT * FROM actions WHERE idempotency_key=?",
|
|
(idempotency_key,),
|
|
).fetchone()
|
|
if row is None:
|
|
action_id = "act-" + uuid.uuid4().hex
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO actions(
|
|
action_id,idempotency_key,manifest_id,phase,kind,target,
|
|
precondition_hash,attempt_count,max_attempts,state,
|
|
result_json,evidence_json,expected_readback_json,rollback_ref,
|
|
created_at,updated_at
|
|
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
""",
|
|
(
|
|
action_id,idempotency_key,manifest_id,phase,kind.value,target,
|
|
precondition_hash,1,max_attempts,ActionState.RUNNING.value,
|
|
None,"[]",json.dumps(dict(expected_readback or {}), sort_keys=True),
|
|
rollback_ref,now,now,
|
|
),
|
|
)
|
|
conn.commit()
|
|
return ActionTicket(
|
|
action_id,idempotency_key,manifest_id,phase,kind,target,
|
|
precondition_hash,1,max_attempts,ActionState.RUNNING
|
|
)
|
|
if row["manifest_id"] != manifest_id or row["precondition_hash"] != precondition_hash:
|
|
conn.rollback()
|
|
raise IdempotencyConflict(
|
|
"same idempotency key used with different manifest/precondition"
|
|
)
|
|
state = ActionState(row["state"])
|
|
if state == ActionState.SUCCEEDED:
|
|
conn.commit()
|
|
return self._ticket(row, reused=True)
|
|
if state == ActionState.RUNNING:
|
|
conn.commit()
|
|
return self._ticket(row, reused=True)
|
|
if state == ActionState.EFFECT_UNKNOWN:
|
|
conn.commit()
|
|
return self._ticket(row, reused=True, requires_reconcile=True)
|
|
attempts = int(row["attempt_count"])
|
|
if attempts >= int(row["max_attempts"]):
|
|
conn.rollback()
|
|
raise RetryBudgetExhausted(
|
|
f"retry budget exhausted key={idempotency_key} attempts={attempts}"
|
|
)
|
|
attempts += 1
|
|
conn.execute(
|
|
"UPDATE actions SET attempt_count=?,state=?,updated_at=? WHERE action_id=?",
|
|
(attempts,ActionState.RUNNING.value,now,row["action_id"]),
|
|
)
|
|
conn.commit()
|
|
updated = dict(row)
|
|
updated["attempt_count"] = attempts
|
|
updated["state"] = ActionState.RUNNING.value
|
|
return self._ticket(updated)
|
|
|
|
def _ticket(self, row: Mapping[str, Any], *, reused: bool = False, requires_reconcile: bool = False) -> ActionTicket:
|
|
return ActionTicket(
|
|
action_id=str(row["action_id"]),
|
|
idempotency_key=str(row["idempotency_key"]),
|
|
manifest_id=str(row["manifest_id"]),
|
|
phase=str(row["phase"]),
|
|
kind=ActionKind(str(row["kind"])),
|
|
target=str(row["target"]),
|
|
precondition_hash=str(row["precondition_hash"]),
|
|
attempt_count=int(row["attempt_count"]),
|
|
max_attempts=int(row["max_attempts"]),
|
|
state=ActionState(str(row["state"])),
|
|
reused=reused,
|
|
requires_reconcile=requires_reconcile,
|
|
)
|
|
|
|
def _finish(self, action_id: str, state: ActionState, *, result: Mapping[str, Any] | None = None, evidence_refs: Sequence[str] = ()) -> None:
|
|
if state == ActionState.RUNNING:
|
|
raise ValueError("finish state cannot be RUNNING")
|
|
with self._lock, closing(self._connect()) as conn:
|
|
conn.execute("BEGIN IMMEDIATE")
|
|
row = conn.execute("SELECT state FROM actions WHERE action_id=?", (action_id,)).fetchone()
|
|
if row is None:
|
|
conn.rollback()
|
|
raise GovernanceControlError("unknown action_id")
|
|
if row["state"] == ActionState.SUCCEEDED.value and state != ActionState.SUCCEEDED:
|
|
conn.rollback()
|
|
raise InvalidStateTransition("cannot move succeeded action backwards")
|
|
conn.execute(
|
|
"""
|
|
UPDATE actions SET state=?,result_json=?,evidence_json=?,updated_at=?
|
|
WHERE action_id=?
|
|
""",
|
|
(
|
|
state.value,json.dumps(dict(result or {}), sort_keys=True),
|
|
json.dumps([str(x) for x in evidence_refs], sort_keys=True),
|
|
time.time(),action_id,
|
|
),
|
|
)
|
|
conn.commit()
|
|
|
|
def mark_succeeded(self, action_id: str, *, result: Mapping[str, Any], evidence_refs: Sequence[str] = ()) -> None:
|
|
self._finish(action_id, ActionState.SUCCEEDED, result=result, evidence_refs=evidence_refs)
|
|
|
|
def mark_failed(self, action_id: str, *, result: Mapping[str, Any] | None = None, evidence_refs: Sequence[str] = ()) -> None:
|
|
self._finish(action_id, ActionState.FAILED, result=result, evidence_refs=evidence_refs)
|
|
|
|
def mark_effect_unknown(self, action_id: str, *, result: Mapping[str, Any] | None = None, evidence_refs: Sequence[str] = ()) -> None:
|
|
self._finish(action_id, ActionState.EFFECT_UNKNOWN, result=result, evidence_refs=evidence_refs)
|
|
|
|
|
|
class HypothesisStatus(str, Enum):
|
|
OPEN = "OPEN"
|
|
CONFIRMED = "CONFIRMED"
|
|
REJECTED = "REJECTED"
|
|
|
|
|
|
class HypothesisLedger:
|
|
def __init__(self, db_path: str | Path):
|
|
self.db_path = str(db_path)
|
|
self._lock = threading.RLock()
|
|
self._init_schema()
|
|
|
|
def _connect(self) -> sqlite3.Connection:
|
|
conn = sqlite3.connect(self.db_path, timeout=5, isolation_level=None)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
def _init_schema(self) -> None:
|
|
with closing(self._connect()) as conn:
|
|
conn.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS hypotheses (
|
|
hypothesis_id TEXT PRIMARY KEY,
|
|
claim TEXT NOT NULL,
|
|
evidence_for_json TEXT NOT NULL,
|
|
evidence_against_json TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
next_probe TEXT,
|
|
created_at REAL NOT NULL,
|
|
updated_at REAL NOT NULL
|
|
)
|
|
"""
|
|
)
|
|
|
|
def record(
|
|
self,
|
|
*,
|
|
hypothesis_id: str,
|
|
claim: str,
|
|
next_probe: str | None = None,
|
|
evidence_for: Sequence[str] = (),
|
|
evidence_against: Sequence[str] = (),
|
|
) -> None:
|
|
now = time.time()
|
|
with self._lock, closing(self._connect()) as conn:
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO hypotheses(
|
|
hypothesis_id,claim,evidence_for_json,evidence_against_json,
|
|
status,next_probe,created_at,updated_at
|
|
) VALUES(?,?,?,?,?,?,?,?)
|
|
ON CONFLICT(hypothesis_id) DO UPDATE SET
|
|
claim=excluded.claim,
|
|
evidence_for_json=excluded.evidence_for_json,
|
|
evidence_against_json=excluded.evidence_against_json,
|
|
next_probe=excluded.next_probe,
|
|
updated_at=excluded.updated_at
|
|
""",
|
|
(
|
|
hypothesis_id,claim,json.dumps(list(evidence_for), sort_keys=True),
|
|
json.dumps(list(evidence_against), sort_keys=True),
|
|
HypothesisStatus.OPEN.value,next_probe,now,now,
|
|
),
|
|
)
|
|
|
|
def set_status(self, hypothesis_id: str, status: HypothesisStatus) -> None:
|
|
with self._lock, closing(self._connect()) as conn:
|
|
cur = conn.execute(
|
|
"UPDATE hypotheses SET status=?,updated_at=? WHERE hypothesis_id=?",
|
|
(status.value,time.time(),hypothesis_id),
|
|
)
|
|
if cur.rowcount != 1:
|
|
raise GovernanceControlError("unknown hypothesis_id")
|
|
|
|
def assert_probe_discriminates(self, candidate_hypothesis_ids: Sequence[str]) -> None:
|
|
ids = tuple(dict.fromkeys(str(x) for x in candidate_hypothesis_ids))
|
|
if len(ids) < 2:
|
|
raise GovernanceControlError("probe must discriminate at least two candidate hypotheses")
|
|
placeholders = ",".join("?" for _ in ids)
|
|
with closing(self._connect()) as conn:
|
|
rows = conn.execute(
|
|
f"SELECT hypothesis_id,status FROM hypotheses WHERE hypothesis_id IN ({placeholders})",
|
|
ids,
|
|
).fetchall()
|
|
open_ids = {
|
|
str(r["hypothesis_id"]) for r in rows
|
|
if r["status"] == HypothesisStatus.OPEN.value
|
|
}
|
|
if len(open_ids) < 2:
|
|
raise GovernanceControlError("probe does not discriminate at least two OPEN hypotheses")
|
|
|
|
|
|
class BlockerClassification(str, Enum):
|
|
EXTERNAL_DEPENDENCY = "EXTERNAL_DEPENDENCY"
|
|
MECHANISM_DEFECT = "MECHANISM_DEFECT"
|
|
TRANSIENT = "TRANSIENT"
|
|
OWNER_BOUNDARY = "OWNER_BOUNDARY"
|
|
SELF_ACTIONABLE = "SELF_ACTIONABLE"
|
|
|
|
|
|
class ProducerKind(str, Enum):
|
|
SELF = "SELF"
|
|
AUTHORITY_SIDE = "AUTHORITY_SIDE"
|
|
IMPLEMENTER = "IMPLEMENTER"
|
|
CONTROL_PLANE_OWNER = "CONTROL_PLANE_OWNER"
|
|
OWNER = "OWNER"
|
|
|
|
|
|
class NextAction(str, Enum):
|
|
ROOT_COMPLETE = "ROOT_COMPLETE"
|
|
CREATE_OR_RESUME_NEXT_CHILD = "CREATE_OR_RESUME_NEXT_CHILD"
|
|
WAIT_DEPENDENCY = "WAIT_DEPENDENCY"
|
|
RECOVER_EXISTING_CHILD = "RECOVER_EXISTING_CHILD"
|
|
OWNER_BOUNDARY = "OWNER_BOUNDARY"
|
|
REPAIR_MECHANISM = "REPAIR_MECHANISM"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ProducerBinding:
|
|
classification: BlockerClassification
|
|
producer_kind: ProducerKind
|
|
producer_id: str
|
|
action_required: str
|
|
|
|
|
|
DEFAULT_PRODUCER_TABLE: Mapping[str, ProducerBinding] = {
|
|
"TASK_NOT_RESOLVED": ProducerBinding(BlockerClassification.EXTERNAL_DEPENDENCY, ProducerKind.AUTHORITY_SIDE, "task-mission-authority", "PROVISION_EXACT_CANDIDATE"),
|
|
"TASK_RESOLUTION_AMBIGUOUS": ProducerBinding(BlockerClassification.EXTERNAL_DEPENDENCY, ProducerKind.AUTHORITY_SIDE, "task-mission-authority", "PROVISION_EXACT_CANDIDATE"),
|
|
"LIVE_SURFACE_UNAVAILABLE": ProducerBinding(BlockerClassification.EXTERNAL_DEPENDENCY, ProducerKind.AUTHORITY_SIDE, "governed-bridge-authority-owner", "EXPOSE_READ_SURFACE"),
|
|
"LIVE_BINDING_VIOLATION": ProducerBinding(BlockerClassification.MECHANISM_DEFECT, ProducerKind.IMPLEMENTER, "agent-side-adapter-owner", "FIX_BINDING_ADAPTER"),
|
|
"GOVERNED_WRITES_DISABLED": ProducerBinding(BlockerClassification.EXTERNAL_DEPENDENCY, ProducerKind.CONTROL_PLANE_OWNER, "bridge-runtime-config", "ENABLE_GOVERNED_WRITES"),
|
|
"TASK_PRINCIPAL_WRITES_DISABLED": ProducerBinding(BlockerClassification.EXTERNAL_DEPENDENCY, ProducerKind.CONTROL_PLANE_OWNER, "bridge-runtime-config", "ENABLE_TASK_PRINCIPAL_WRITES"),
|
|
"TASK_LEASE_MISSING": ProducerBinding(BlockerClassification.SELF_ACTIONABLE, ProducerKind.SELF, "agent-recovery-executor", "ACQUIRE_LEASE_VIA_GOVERNED_PATH"),
|
|
"TASK_PRINCIPAL_MISSING": ProducerBinding(BlockerClassification.SELF_ACTIONABLE, ProducerKind.SELF, "agent-recovery-executor", "ISSUE_TASK_PRINCIPAL"),
|
|
"WRITER_LEASE_MISSING": ProducerBinding(BlockerClassification.SELF_ACTIONABLE, ProducerKind.SELF, "agent-recovery-executor", "ACQUIRE_WRITER_LEASE"),
|
|
"EXPECTED_BRANCH_OID_UNRESOLVED": ProducerBinding(BlockerClassification.EXTERNAL_DEPENDENCY, ProducerKind.AUTHORITY_SIDE, "governed-bridge-provisioner", "PROVISION_CANDIDATE_BINDING"),
|
|
"GENERATION_UNRESOLVED": ProducerBinding(BlockerClassification.EXTERNAL_DEPENDENCY, ProducerKind.AUTHORITY_SIDE, "governed-bridge-provisioner", "PROVISION_CANDIDATE_BINDING"),
|
|
"TARGET_BRANCH_UNRESOLVED": ProducerBinding(BlockerClassification.EXTERNAL_DEPENDENCY, ProducerKind.AUTHORITY_SIDE, "governed-bridge-provisioner", "PROVISION_CANDIDATE_BINDING"),
|
|
"FILE_ALLOWLIST_MISSING": ProducerBinding(BlockerClassification.OWNER_BOUNDARY, ProducerKind.OWNER, "owner-delegation-epoch", "ISSUE_OWNER_EPOCH"),
|
|
"ACCEPTANCE_REVISION_MISSING": ProducerBinding(BlockerClassification.OWNER_BOUNDARY, ProducerKind.OWNER, "owner-delegation-epoch", "ISSUE_OWNER_EPOCH"),
|
|
"RECOVERY_RESULT_UNKNOWN": ProducerBinding(BlockerClassification.SELF_ACTIONABLE, ProducerKind.SELF, "agent-recovery-executor", "RECONCILE_BEFORE_RETRY"),
|
|
"CAPABILITY_OUT_OF_POLICY": ProducerBinding(BlockerClassification.EXTERNAL_DEPENDENCY, ProducerKind.AUTHORITY_SIDE, "admission-policy-authority", "ALIGN_POLICY_AND_EPOCH"),
|
|
"EXECUTION_DIGEST_INVALID": ProducerBinding(BlockerClassification.MECHANISM_DEFECT, ProducerKind.IMPLEMENTER, "candidate-plan-builder", "REGENERATE_CANONICAL_PLAN"),
|
|
"TASK_EXPIRED_OWNER_RECOVERY_REQUIRED": ProducerBinding(BlockerClassification.OWNER_BOUNDARY, ProducerKind.OWNER, "agent-task-terminal-recovery", "AUTHORIZE_TERMINAL_RECOVERY_OR_SUCCESSOR"),
|
|
"EPOCH_EXPIRED": ProducerBinding(BlockerClassification.SELF_ACTIONABLE, ProducerKind.SELF, "trusted-grant-signer-adapter", "JIT_REISSUE_EPOCH_IF_SCOPE_UNCHANGED"),
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DecisionInput:
|
|
goal_id: str
|
|
blocker_codes: tuple[str, ...] = ()
|
|
owner_boundary_required: bool = False
|
|
surfaces_ready: bool = True
|
|
resolution_state: str = "RESOLVED"
|
|
admission_ready: bool = False
|
|
acceptance_complete: bool = False
|
|
descendant_statuses: tuple[str, ...] = ()
|
|
unchanged_cycles: int = 0
|
|
evidence_refs: tuple[str, ...] = ()
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class GovernanceDecision:
|
|
goal_id: str
|
|
next_valid_action: NextAction
|
|
owner_boundary: bool
|
|
blocker_code: str | None
|
|
classification: BlockerClassification | None
|
|
producer_kind: ProducerKind | None
|
|
producer_id: str | None
|
|
action_required: str | None
|
|
evidence_refs: tuple[str, ...]
|
|
schema: str = SCHEMA_DECISION
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
result = asdict(self)
|
|
result["next_valid_action"] = self.next_valid_action.value
|
|
result["classification"] = None if self.classification is None else self.classification.value
|
|
result["producer_kind"] = None if self.producer_kind is None else self.producer_kind.value
|
|
return result
|
|
|
|
|
|
class AutonomousDecisionEngine:
|
|
RECOVERY_STATUSES = frozenset({"FAILED_RECOVERABLE", "EXPIRED", "RECOVERY_REQUIRED"})
|
|
BLOCKED_STATUSES = frozenset({"BLOCKED", "WAIT_DEPENDENCY", "TERMINAL_UNPROVEN"})
|
|
|
|
def __init__(self, producer_table: Mapping[str, ProducerBinding] | None = None):
|
|
self.producer_table = dict(DEFAULT_PRODUCER_TABLE)
|
|
if producer_table:
|
|
self.producer_table.update(producer_table)
|
|
|
|
def _binding(self, blocker: str) -> ProducerBinding:
|
|
return self.producer_table.get(
|
|
blocker,
|
|
ProducerBinding(BlockerClassification.MECHANISM_DEFECT, ProducerKind.IMPLEMENTER, "unmapped-blocker-owner", "CLASSIFY_BLOCKER"),
|
|
)
|
|
|
|
def decide(self, inp: DecisionInput) -> GovernanceDecision:
|
|
blockers = tuple(dict.fromkeys(inp.blocker_codes))
|
|
for blocker in blockers:
|
|
binding = self._binding(blocker)
|
|
if binding.classification == BlockerClassification.MECHANISM_DEFECT:
|
|
return self._decision(inp, blocker, binding, NextAction.REPAIR_MECHANISM)
|
|
if inp.owner_boundary_required:
|
|
blocker = blockers[0] if blockers else "OWNER_BOUNDARY"
|
|
binding = self._binding(blocker) if blockers else ProducerBinding(
|
|
BlockerClassification.OWNER_BOUNDARY, ProducerKind.OWNER, "owner", "OWNER_DECISION_REQUIRED"
|
|
)
|
|
return self._decision(inp, blocker, binding, NextAction.OWNER_BOUNDARY)
|
|
for blocker in blockers:
|
|
binding = self._binding(blocker)
|
|
if binding.classification == BlockerClassification.OWNER_BOUNDARY:
|
|
return self._decision(inp, blocker, binding, NextAction.OWNER_BOUNDARY)
|
|
if not inp.surfaces_ready:
|
|
blocker = "LIVE_SURFACE_UNAVAILABLE"
|
|
return self._decision(inp, blocker, self._binding(blocker), NextAction.WAIT_DEPENDENCY)
|
|
if inp.resolution_state != "RESOLVED":
|
|
blocker = "TASK_RESOLUTION_AMBIGUOUS" if inp.resolution_state == "AMBIGUOUS" else "TASK_NOT_RESOLVED"
|
|
return self._decision(inp, blocker, self._binding(blocker), NextAction.WAIT_DEPENDENCY)
|
|
if not inp.admission_ready:
|
|
for blocker in blockers:
|
|
binding = self._binding(blocker)
|
|
if binding.classification == BlockerClassification.SELF_ACTIONABLE:
|
|
return self._decision(inp, blocker, binding, NextAction.RECOVER_EXISTING_CHILD)
|
|
if blockers:
|
|
blocker = blockers[0]
|
|
return self._decision(inp, blocker, self._binding(blocker), NextAction.WAIT_DEPENDENCY)
|
|
return GovernanceDecision(
|
|
goal_id=inp.goal_id,next_valid_action=NextAction.WAIT_DEPENDENCY,
|
|
owner_boundary=False,blocker_code=None,
|
|
classification=BlockerClassification.EXTERNAL_DEPENDENCY,
|
|
producer_kind=ProducerKind.AUTHORITY_SIDE,producer_id="admission-authority",
|
|
action_required="MAKE_ADMISSION_READY",evidence_refs=inp.evidence_refs,
|
|
)
|
|
if inp.acceptance_complete:
|
|
return GovernanceDecision(
|
|
goal_id=inp.goal_id,next_valid_action=NextAction.ROOT_COMPLETE,
|
|
owner_boundary=False,blocker_code=None,classification=None,
|
|
producer_kind=None,producer_id=None,action_required=None,evidence_refs=inp.evidence_refs,
|
|
)
|
|
statuses = {str(x).upper() for x in inp.descendant_statuses}
|
|
if statuses & self.RECOVERY_STATUSES:
|
|
return GovernanceDecision(
|
|
goal_id=inp.goal_id,next_valid_action=NextAction.RECOVER_EXISTING_CHILD,
|
|
owner_boundary=False,blocker_code=None,
|
|
classification=BlockerClassification.SELF_ACTIONABLE,
|
|
producer_kind=ProducerKind.SELF,producer_id="agent-recovery-executor",
|
|
action_required="RECOVER_EXISTING_CHILD",evidence_refs=inp.evidence_refs,
|
|
)
|
|
if statuses & self.BLOCKED_STATUSES:
|
|
return GovernanceDecision(
|
|
goal_id=inp.goal_id,next_valid_action=NextAction.WAIT_DEPENDENCY,
|
|
owner_boundary=False,blocker_code=None,
|
|
classification=BlockerClassification.EXTERNAL_DEPENDENCY,
|
|
producer_kind=ProducerKind.AUTHORITY_SIDE,producer_id="dependency-authority",
|
|
action_required="UNBLOCK_DEPENDENCY",evidence_refs=inp.evidence_refs,
|
|
)
|
|
return GovernanceDecision(
|
|
goal_id=inp.goal_id,next_valid_action=NextAction.CREATE_OR_RESUME_NEXT_CHILD,
|
|
owner_boundary=False,blocker_code=None,
|
|
classification=BlockerClassification.SELF_ACTIONABLE,
|
|
producer_kind=ProducerKind.SELF,producer_id="autonomy-controller",
|
|
action_required="CREATE_OR_RESUME_NEXT_CHILD",evidence_refs=inp.evidence_refs,
|
|
)
|
|
|
|
def _decision(self, inp: DecisionInput, blocker: str, binding: ProducerBinding, next_action: NextAction) -> GovernanceDecision:
|
|
return GovernanceDecision(
|
|
goal_id=inp.goal_id,next_valid_action=next_action,
|
|
owner_boundary=binding.classification == BlockerClassification.OWNER_BOUNDARY,
|
|
blocker_code=blocker,classification=binding.classification,
|
|
producer_kind=binding.producer_kind,producer_id=binding.producer_id,
|
|
action_required=binding.action_required,evidence_refs=inp.evidence_refs,
|
|
)
|
|
|
|
|
|
class MutationGuard:
|
|
def __init__(self, manifest_store: ManifestStore, action_ledger: ActionLedger):
|
|
self.manifest_store = manifest_store
|
|
self.action_ledger = action_ledger
|
|
self._writer_lock = threading.Lock()
|
|
|
|
def begin_mutation(
|
|
self,
|
|
*,
|
|
manifest_id: str,
|
|
runtime_fingerprint: str,
|
|
process_start_identity: str,
|
|
idempotency_key: str,
|
|
phase: str,
|
|
kind: ActionKind,
|
|
target: str,
|
|
precondition_hash: str,
|
|
expected_readback: Mapping[str, Any] | None = None,
|
|
rollback_ref: str | None = None,
|
|
) -> ActionTicket:
|
|
self.manifest_store.assert_current(
|
|
manifest_id=manifest_id,
|
|
runtime_fingerprint=runtime_fingerprint,
|
|
process_start_identity=process_start_identity,
|
|
)
|
|
if not self._writer_lock.acquire(blocking=False):
|
|
raise ConcurrentWriterDenied("another governed mutation is in progress")
|
|
try:
|
|
ticket = self.action_ledger.begin(
|
|
idempotency_key=idempotency_key,manifest_id=manifest_id,phase=phase,
|
|
kind=kind,target=target,precondition_hash=precondition_hash,
|
|
expected_readback=expected_readback,rollback_ref=rollback_ref,
|
|
)
|
|
if ticket.reused:
|
|
self._writer_lock.release()
|
|
return ticket
|
|
except Exception:
|
|
self._writer_lock.release()
|
|
raise
|
|
|
|
def finish_success(self, ticket: ActionTicket, *, result: Mapping[str, Any], evidence_refs: Sequence[str] = ()) -> None:
|
|
try:
|
|
self.action_ledger.mark_succeeded(ticket.action_id, result=result, evidence_refs=evidence_refs)
|
|
finally:
|
|
if self._writer_lock.locked():
|
|
self._writer_lock.release()
|
|
|
|
def finish_failed(self, ticket: ActionTicket, *, result: Mapping[str, Any] | None = None, evidence_refs: Sequence[str] = ()) -> None:
|
|
try:
|
|
self.action_ledger.mark_failed(ticket.action_id, result=result, evidence_refs=evidence_refs)
|
|
finally:
|
|
if self._writer_lock.locked():
|
|
self._writer_lock.release()
|
|
|
|
def finish_effect_unknown(self, ticket: ActionTicket, *, result: Mapping[str, Any] | None = None, evidence_refs: Sequence[str] = ()) -> None:
|
|
try:
|
|
self.action_ledger.mark_effect_unknown(ticket.action_id, result=result, evidence_refs=evidence_refs)
|
|
finally:
|
|
if self._writer_lock.locked():
|
|
self._writer_lock.release()
|