- Rename `par` to `pred` (predecessor) in types, serialization, tests - Remove `pol`, `pol_decision` from core payload; move to `ect_ext` - Remove `sub` from payload (not part of ECT spec) - Update `typ` from `wimse-exec+jwt` to `exec+jwt` (accept both) - Rename MaxParLength to MaxPredLength everywhere - Update testdata, demos, READMEs with migration table - All Go tests pass, all 56 Python tests pass (90% coverage)
98 lines
2.6 KiB
Python
98 lines
2.6 KiB
Python
"""Audit ledger per Section 9."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING
|
|
|
|
from ect.types import Payload
|
|
|
|
if TYPE_CHECKING:
|
|
pass
|
|
|
|
|
|
class ErrTaskIDExists(Exception):
|
|
"""Raised when appending an ECT whose tid already exists."""
|
|
|
|
|
|
@dataclass
|
|
class LedgerEntry:
|
|
ledger_sequence: int
|
|
task_id: str
|
|
agent_id: str
|
|
action: str
|
|
predecessors: list[str]
|
|
ect_jws: str
|
|
signature_verified: bool
|
|
verification_timestamp: float
|
|
stored_timestamp: float
|
|
|
|
|
|
class Ledger(ABC):
|
|
"""Append-only audit ledger; lookup by task id (jti)."""
|
|
|
|
@abstractmethod
|
|
def append(self, ect_jws: str, payload: Payload) -> int:
|
|
"""Returns new ledger sequence number."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_by_tid(self, tid: str) -> Payload | None:
|
|
pass
|
|
|
|
@abstractmethod
|
|
def contains(self, tid: str, wid: str) -> bool:
|
|
pass
|
|
|
|
|
|
class MemoryLedger(Ledger):
|
|
"""In-memory append-only ECT store implementing Ledger and ECTStore."""
|
|
|
|
def __init__(self) -> None:
|
|
self._seq = 0
|
|
self._by_tid: dict[str, "Payload"] = {}
|
|
self._entries: list[LedgerEntry] = []
|
|
self._lock = __import__("threading").Lock()
|
|
|
|
def append(self, ect_jws: str, payload: Payload) -> int:
|
|
if payload is None:
|
|
return 0
|
|
with self._lock:
|
|
wid = payload.wid or ""
|
|
if self._contains_locked(payload.jti, wid):
|
|
raise ErrTaskIDExists("ect: task ID (jti) already exists in ledger")
|
|
self._seq += 1
|
|
now = time.time()
|
|
entry = LedgerEntry(
|
|
ledger_sequence=self._seq,
|
|
task_id=payload.jti,
|
|
agent_id=payload.iss,
|
|
action=payload.exec_act,
|
|
predecessors=list(payload.pred) if payload.pred else [],
|
|
ect_jws=ect_jws,
|
|
signature_verified=True,
|
|
verification_timestamp=now,
|
|
stored_timestamp=now,
|
|
)
|
|
self._by_tid[payload.jti] = payload
|
|
self._entries.append(entry)
|
|
return self._seq
|
|
|
|
def get_by_tid(self, tid: str) -> Payload | None:
|
|
with self._lock:
|
|
return self._by_tid.get(tid)
|
|
|
|
def contains(self, tid: str, wid: str) -> bool:
|
|
with self._lock:
|
|
return self._contains_locked(tid, wid)
|
|
|
|
def _contains_locked(self, tid: str, wid: str) -> bool:
|
|
p = self._by_tid.get(tid)
|
|
if p is None:
|
|
return False
|
|
if not wid:
|
|
return True
|
|
return (p.wid or "") == wid
|