Files
ietf-wimse-ect/refimpl/python/ect/ledger.py
Christian Nennemann bbf557e54b Restructure refimpl into go-lang and python subdirectories
Move Go reference implementation to refimpl/go-lang/ and add new
Python reference implementation in refimpl/python/. Update build.sh
with renamed draft and simplified tool paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:11:55 +01:00

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
parents: 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,
parents=list(payload.par) if payload.par 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