Persist review evidence and deliver audit records transactionally

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-10 23:27:19 +02:00
parent 0e48355b9f
commit 2cc32168ac
14 changed files with 1474 additions and 22 deletions

459
informed_decision/store.py Normal file
View file

@ -0,0 +1,459 @@
"""Durable source-held evidence and transactional outbox for the review domain.
This is an internal store, not an authorization endpoint. Callers owe an
access-engine decision before exposing its content or dispatching an entry.
Submission state describes local delivery, never approval validity.
"""
from contextlib import contextmanager
from dataclasses import replace
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import re
import sqlite3
import stat
import time
import uuid
from .approval_client import EntryResult
from .disposition import Actor, BINDING_VERBS, DispositionRefused, Verb, record
from .evidence import EventClass, commit_disposition, commit_presentation, commit_stance_application, heartbeat, HEARTBEAT_CLASSES
from .memo import Memo
from .presentation import render
from .provenance import Claim, Route, assert_human_control_dischargeable
from .stance import resolve
from .records import disposition_from, dumps, memo_from, presentation_from
SCHEMA_VERSION = 1
_SCHEMA = """
CREATE TABLE documents (digest TEXT PRIMARY KEY, media_type TEXT NOT NULL, content BLOB NOT NULL);
CREATE TABLE memos (id TEXT NOT NULL, version INTEGER NOT NULL, body TEXT NOT NULL, PRIMARY KEY(id,version));
CREATE TABLE presentations (id TEXT PRIMARY KEY, memo_id TEXT NOT NULL, version INTEGER NOT NULL, body TEXT NOT NULL,
FOREIGN KEY(memo_id,version) REFERENCES memos(id,version));
CREATE TABLE acknowledgments (presentation_id TEXT NOT NULL REFERENCES presentations(id),
highlight_id TEXT NOT NULL, at TEXT NOT NULL, PRIMARY KEY(presentation_id,highlight_id));
CREATE TABLE dispositions (id TEXT PRIMARY KEY, operation_id TEXT UNIQUE NOT NULL, request TEXT NOT NULL,
presentation_id TEXT NOT NULL REFERENCES presentations(id), body TEXT NOT NULL);
CREATE TABLE evidence (id TEXT PRIMARY KEY, class TEXT NOT NULL, at TEXT NOT NULL,
envelope TEXT NOT NULL, content TEXT NOT NULL);
CREATE TABLE outbox (id TEXT PRIMARY KEY REFERENCES evidence(id), state TEXT NOT NULL DEFAULT 'pending',
attempts INTEGER NOT NULL DEFAULT 0, next_attempt REAL NOT NULL DEFAULT 0,
lease TEXT, lease_until REAL, last_error TEXT, receiver_reference TEXT);
CREATE TABLE submissions (disposition_id TEXT PRIMARY KEY REFERENCES dispositions(id),
approval_id TEXT NOT NULL, subject TEXT NOT NULL, state TEXT NOT NULL DEFAULT 'prepared',
attempt TEXT, approved_at TEXT, UNIQUE(approval_id,subject));
"""
class StoreError(RuntimeError):
pass
class Conflict(StoreError):
pass
class EvidenceUnavailable(StoreError):
"""Retrieval failure attributable to informed-decision, not an empty result."""
def _text(value, name):
if not isinstance(value, str) or not value or len(value) > 256:
raise ValueError(f"invalid {name}")
def _now():
return datetime.now(timezone.utc).isoformat(timespec="microseconds")
class Store:
def __init__(self, path: str | Path):
self.path = Path(path).absolute()
parent = self.path.parent
info = parent.lstat()
if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid()
or stat.S_IMODE(info.st_mode) != 0o700):
raise StoreError("evidence directory must be owned by this user with mode 0700")
try:
fd = os.open(self.path, os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_NOFOLLOW, 0o600)
except FileExistsError:
pass
else:
os.close(fd)
info = self.path.lstat()
if (not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_uid != os.getuid()
or stat.S_IMODE(info.st_mode) != 0o600):
raise StoreError("evidence database must be a private, owned, non-linked 0600 file")
with self._connection() as db:
db.execute("PRAGMA journal_mode=WAL")
version = db.execute("PRAGMA user_version").fetchone()[0]
if version == 0:
if db.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchone():
raise StoreError("unrecognized evidence database")
triggers = ""
for table in ("documents", "memos", "presentations", "acknowledgments", "dispositions", "evidence"):
for operation in ("UPDATE", "DELETE"):
triggers += (f"CREATE TRIGGER immutable_{table}_{operation} BEFORE {operation} ON {table} "
"BEGIN SELECT RAISE(ABORT,'immutable evidence'); END;\n")
db.executescript("BEGIN IMMEDIATE;" + _SCHEMA + triggers + "PRAGMA user_version=1; COMMIT;")
elif version != SCHEMA_VERSION:
raise StoreError("unsupported evidence schema version")
@contextmanager
def _connection(self):
db = sqlite3.connect(self.path.as_uri() + "?mode=rw", uri=True, timeout=5, isolation_level=None)
db.row_factory = sqlite3.Row
db.execute("PRAGMA foreign_keys=ON")
db.execute("PRAGMA synchronous=FULL")
try:
yield db
finally:
db.close()
@contextmanager
def _transaction(self):
with self._connection() as db:
db.execute("BEGIN IMMEDIATE")
try:
yield db
db.commit()
except BaseException:
db.rollback()
raise
def put_document(self, content: bytes, media_type="text/plain") -> str:
if not isinstance(content, bytes) or len(content) > 16 * 1024 * 1024:
raise ValueError("packet document must be bytes, at most 16 MiB")
_text(media_type, "media type")
digest = "sha256:" + hashlib.sha256(content).hexdigest()
with self._transaction() as db:
existing = db.execute("SELECT media_type,content FROM documents WHERE digest=?", (digest,)).fetchone()
if existing and (existing["media_type"] != media_type or existing["content"] != content):
raise Conflict("document digest already has different content or media type")
if not existing:
db.execute("INSERT INTO documents VALUES (?,?,?)", (digest, media_type, content))
return digest
def save_memo(self, memo: Memo):
_text(memo.id, "memo id")
if type(memo.version) is not int or memo.version < 1:
raise ValueError("memo version must be a positive integer")
body = dumps(memo)
if len(body.encode()) > 256 * 1024:
raise ValueError("memo is too large")
with self._transaction() as db:
previous = db.execute("SELECT version,body FROM memos WHERE id=? ORDER BY version DESC LIMIT 1", (memo.id,)).fetchone()
if previous and previous["version"] == memo.version and previous["body"] == body:
return
if memo.version != (previous["version"] + 1 if previous else 1):
raise Conflict("memo version must advance by one; existing versions are immutable")
if db.execute("SELECT 1 FROM submissions s JOIN dispositions d ON d.id=s.disposition_id "
"JOIN presentations p ON p.id=d.presentation_id WHERE p.memo_id=? "
"AND s.state IN ('in_flight','unresolved') LIMIT 1", (memo.id,)).fetchone():
raise Conflict("resolve the outstanding entry attempt before revising this memo")
for item in memo.packet:
if not db.execute("SELECT 1 FROM documents WHERE digest=?", (item.hash,)).fetchone():
raise EvidenceUnavailable("informed-decision does not hold the referenced packet content")
db.execute("INSERT INTO memos VALUES (?,?,?)", (memo.id, memo.version, body))
def _memo(self, db, memo_id, version=None):
if version is None:
row = db.execute("SELECT body FROM memos WHERE id=? ORDER BY version DESC LIMIT 1", (memo_id,)).fetchone()
else:
row = db.execute("SELECT body FROM memos WHERE id=? AND version=?", (memo_id, version)).fetchone()
if row is None:
raise EvidenceUnavailable("informed-decision cannot produce the named memo version")
return memo_from(json.loads(row["body"]))
def memo(self, memo_id):
with self._connection() as db:
return self._memo(db, memo_id)
def _presentation(self, db, presentation_id):
row = db.execute("SELECT body FROM presentations WHERE id=?", (presentation_id,)).fetchone()
if row is None:
raise EvidenceUnavailable("informed-decision cannot produce the named presentation")
original = presentation_from(json.loads(row["body"]))
acks = db.execute("SELECT highlight_id FROM acknowledgments WHERE presentation_id=?", (presentation_id,))
return replace(original, acked_highlight_ids=frozenset(r[0] for r in acks))
def presentation(self, presentation_id):
with self._connection() as db:
return self._presentation(db, presentation_id)
def _event(self, db, commitment, content, extra=None):
# Construct the stable envelope once, in the same transaction as content
# and state. Retries send these exact bytes and the same idempotency key.
data = {**commitment.data, **(extra or {})}
at = datetime.fromisoformat(commitment.at.replace("Z", "+00:00")).astimezone(timezone.utc).isoformat(timespec="microseconds")
envelope = {"id": commitment.id, "type": commitment.event_class.value,
"source": "informed-decision", "tenant": "tenant:platform",
"subject": data.get("memo_id", "informed-decision"),
"correlation_id": data.get("approval_id") or commitment.id,
"occurred_at": at, "data": data}
encoded = dumps(envelope)
if len(encoded.encode()) > 256 * 1024:
raise StoreError("audit commitment exceeds the receiver limit")
db.execute("INSERT INTO evidence VALUES (?,?,?,?,?)",
(commitment.id, commitment.event_class.value, at, encoded, dumps(content)))
db.execute("INSERT INTO outbox(id) VALUES (?)", (commitment.id,))
return commitment.id
def present(self, memo_id, *, principal_sub, tenant, principal_type, awareness=None):
_text(principal_sub, "principal")
if (not isinstance(tenant, Claim) or tenant.value != "tenant:platform"
or tenant.route not in (Route.DIRECTORY, Route.REGISTRATION)
or not isinstance(principal_type, Claim)):
raise ValueError("platform tenant and principal provenance are required")
with self._transaction() as db:
memo = self._memo(db, memo_id)
presentation = render(memo, principal_sub=principal_sub, tenant=tenant,
principal_type=principal_type, awareness=awareness)
content = {"memo": json.loads(dumps(memo)), "presentation": json.loads(dumps(presentation)),
"binding_document": memo.binding_document(), "awareness_document": memo.awareness_document(awareness)}
db.execute("INSERT INTO presentations VALUES (?,?,?,?)",
(presentation.id, memo.id, memo.version, dumps(presentation)))
commitment = commit_presentation(presentation, custody="informed-decision:presentations:" + presentation.id)
self._event(db, commitment, content)
return presentation
def acknowledge(self, presentation_id, actor: Actor, highlight_ids):
ids = frozenset(highlight_ids)
with self._transaction() as db:
p = self._presentation(db, presentation_id)
memo = self._memo(db, p.memo_id)
if actor.sub != p.principal_sub:
raise DispositionRefused("G_ACTOR", "actor did not receive this presentation")
if memo.version != p.memo_version:
raise DispositionRefused("G_PRES", "presentation is stale")
if ids - {h.id for h in memo.highlights}:
raise DispositionRefused("G_ACK", "unknown highlight")
fresh = ids - p.acked_highlight_ids
if not fresh:
return p
at = _now()
for highlight_id in sorted(fresh):
db.execute("INSERT INTO acknowledgments VALUES (?,?,?)", (p.id, highlight_id, at))
p = replace(p, acked_highlight_ids=p.acked_highlight_ids | fresh)
c = commit_presentation(p, custody="informed-decision:presentations:" + p.id)
self._event(db, c, {"presentation": json.loads(dumps(p))},
{"event_kind": "acknowledgment", "acknowledged_at": at})
return p
def record_disposition(self, presentation_id, actor, verb, *, operation_id, reasons=(), note=None):
_text(operation_id, "operation id")
if note is not None and (not isinstance(note, str) or len(note) > 8192):
raise ValueError("disposition note is too large or invalid")
if any(not isinstance(r, str) or not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", r) for r in reasons):
raise ValueError("reasons must be bounded codes, not free text")
request = dumps({"presentation": presentation_id, "actor": actor, "verb": verb, "reasons": reasons, "note": note})
with self._transaction() as db:
existing = db.execute("SELECT request,body FROM dispositions WHERE operation_id=?", (operation_id,)).fetchone()
if existing:
if existing["request"] != request:
raise Conflict("operation id already names a different disposition")
return disposition_from(json.loads(existing["body"]))
p = self._presentation(db, presentation_id)
memo = self._memo(db, p.memo_id)
d = record(memo, p, verb, actor, reasons=tuple(reasons), note=note)
if verb in BINDING_VERBS:
if p.principal_type is None:
raise DispositionRefused("G_IDENTITY", "verified human provenance required")
assert_human_control_dischargeable(p.principal_type)
if verb is Verb.ACCEPT:
if not memo.approval_id or not memo.approval_binding_digest:
raise DispositionRefused("G_BINDING", "the native approval id and carried digest are required")
if db.execute("SELECT 1 FROM submissions WHERE approval_id=? AND subject=?", (memo.approval_id, actor.sub)).fetchone():
raise Conflict("this approver already has an intent for the approval; recover the original")
db.execute("INSERT INTO dispositions VALUES (?,?,?,?,?)", (d.id, operation_id, request, p.id, dumps(d)))
if verb is Verb.ACCEPT:
db.execute("INSERT INTO submissions(disposition_id,approval_id,subject) VALUES (?,?,?)", (d.id, memo.approval_id, actor.sub))
c = commit_disposition(d, custody="informed-decision:dispositions:" + d.id)
self._event(db, c, {"disposition": json.loads(dumps(d)), "presentation": json.loads(dumps(p))},
{"submission_state": "prepared"} if verb is Verb.ACCEPT else None)
return d
def begin_submission(self, disposition_id):
"""Reserve one external attempt, AFTER the caller's fresh policy check.
No network occurs in this store. A crash after this reservation stays
unresolved; restart never silently permits another POST.
"""
with self._transaction() as db:
row = db.execute("SELECT * FROM submissions WHERE disposition_id=?", (disposition_id,)).fetchone()
if row is None or row["state"] != "prepared":
raise Conflict("submission is absent, already attempted or unresolved")
d = disposition_from(json.loads(db.execute("SELECT body FROM dispositions WHERE id=?", (disposition_id,)).fetchone()[0]))
p = self._presentation(db, d.presentation_id)
if self._memo(db, p.memo_id).version != p.memo_version:
raise DispositionRefused("G_PRES", "presentation is stale")
attempt = str(uuid.uuid4())
db.execute("UPDATE submissions SET state='in_flight',attempt=? WHERE disposition_id=?", (attempt, disposition_id))
return attempt
def finish_submission(self, disposition_id, attempt, result: EntryResult | None = None):
with self._transaction() as db:
row = db.execute("SELECT * FROM submissions WHERE disposition_id=?", (disposition_id,)).fetchone()
if row is None or row["state"] != "in_flight" or row["attempt"] != attempt:
raise Conflict("submission attempt does not match")
if result is not None and (result.approval_id != row["approval_id"] or result.subject != row["subject"]):
raise Conflict("approval entry does not match the original intent")
if result is not None:
at = datetime.fromisoformat(result.approved_at.replace("Z", "+00:00"))
if at.tzinfo is None:
raise ValueError("approval timestamp must carry its timezone")
# An unknown duplicate may predate this presentation. Never attach it
# to a new one. A lost response likewise cannot prove causation.
state = "confirmed" if result is not None and not result.duplicate else "unresolved"
db.execute("UPDATE submissions SET state=?,approved_at=? WHERE disposition_id=?",
(state, result.approved_at if state == "confirmed" else None, disposition_id))
d = disposition_from(json.loads(db.execute("SELECT body FROM dispositions WHERE id=?", (disposition_id,)).fetchone()[0]))
c = commit_disposition(d, custody="informed-decision:dispositions:" + d.id)
extra = {"event_kind": "submission_result", "submission_state": state}
if state == "confirmed":
extra["entry_correlation"] = list(result.correlation)
self._event(db, c, {"disposition": json.loads(dumps(d)), **extra}, extra)
return state
def submission(self, disposition_id):
with self._connection() as db:
row = db.execute("SELECT * FROM submissions WHERE disposition_id=?", (disposition_id,)).fetchone()
return dict(row) if row else None
def retrieve_presentation(self, presentation_id):
"""Internal custody retrieval, not a browser export/entitlement route."""
with self._connection() as db:
p = self._presentation(db, presentation_id)
memo = self._memo(db, p.memo_id, p.memo_version)
docs = {}
for item in memo.packet:
row = db.execute("SELECT content FROM documents WHERE digest=?", (item.hash,)).fetchone()
if row is None or "sha256:" + hashlib.sha256(row[0]).hexdigest() != item.hash:
raise EvidenceUnavailable("informed-decision cannot produce intact packet content")
docs[item.item_id] = row[0]
return memo, p, docs
def evidence(self):
with self._connection() as db:
return [dict(r) for r in db.execute("SELECT * FROM evidence ORDER BY rowid")]
def record_unreachable(self, memo_id, dependency):
if dependency not in {"access-engine", "approval-engine", "key-cape", "audit-core"}:
raise ValueError("unknown dependency")
with self._transaction() as db:
memo = self._memo(db, memo_id)
stance, state = resolve(memo.binding_level.value)
c = commit_stance_application(memo_id=memo.id, memo_version=memo.version,
binding_level=memo.binding_level.value, binding_level_state=state.value,
stance=stance, dependency=dependency, custody="informed-decision:memos:" + memo.id)
return self._event(db, c, {"memo": json.loads(dumps(memo)), "dependency": dependency})
def retrieve_disposition(self, disposition_id):
with self._connection() as db:
row = db.execute("SELECT body FROM dispositions WHERE id=?", (disposition_id,)).fetchone()
snapshot = db.execute("SELECT content FROM evidence WHERE class=? AND "
"json_extract(envelope,'$.data.disposition_id')=? ORDER BY rowid LIMIT 1",
(EventClass.DISPOSITION.value, disposition_id)).fetchone()
if row is None or snapshot is None:
raise EvidenceUnavailable("informed-decision cannot produce the named disposition")
d = disposition_from(json.loads(row[0]))
original_presentation = presentation_from(json.loads(snapshot[0])["presentation"])
memo, _, documents = self.retrieve_presentation(d.presentation_id)
return d, original_presentation, memo, documents
def presentation_for_entry(self, approval_id, subject, approved_at):
with self._connection() as db:
row = db.execute("SELECT disposition_id FROM submissions WHERE state='confirmed' "
"AND approval_id=? AND subject=? AND approved_at=?", (approval_id, subject, approved_at)).fetchone()
if row is None:
raise EvidenceUnavailable("informed-decision has no confirmed presentation correlation for this entry")
return self.retrieve_disposition(row[0])
def outbox(self):
with self._connection() as db:
return [dict(r) for r in db.execute("SELECT * FROM outbox ORDER BY rowid")]
def queue_heartbeats(self, now=None):
now = time.time() if now is None else now
count = 0
with self._transaction() as db:
for event_class, interval in HEARTBEAT_CLASSES.items():
if db.execute("SELECT 1 FROM outbox o JOIN evidence e ON e.id=o.id WHERE o.state!='delivered' "
"AND (e.class=? OR (e.class=? AND json_extract(e.envelope,'$.data.class')=?)) LIMIT 1",
(event_class, EventClass.HEARTBEAT.value, event_class)).fetchone():
continue # Never let a nothing-to-report assertion mask undelivered evidence.
recent = db.execute("SELECT MAX(at) FROM evidence WHERE class=? OR "
"(class=? AND json_extract(envelope,'$.data.class')=?)",
(event_class, EventClass.HEARTBEAT.value, event_class)).fetchone()[0]
if recent and now - datetime.fromisoformat(recent.replace("Z", "+00:00")).timestamp() < interval:
continue
c = heartbeat(EventClass(event_class))
c = replace(c, at=datetime.fromtimestamp(now, timezone.utc).isoformat(timespec="microseconds"))
self._event(db, c, {"class": event_class})
count += 1
return count
def claim_delivery(self, now=None):
now = time.time() if now is None else now
with self._transaction() as db:
row = db.execute("SELECT o.id,e.envelope FROM outbox o JOIN evidence e ON e.id=o.id "
"WHERE o.state='pending' AND o.next_attempt<=? AND (o.lease IS NULL OR o.lease_until<=?) ORDER BY o.rowid LIMIT 1",
(now, now)).fetchone()
if row is None:
return None
lease = str(uuid.uuid4())
db.execute("UPDATE outbox SET lease=?,lease_until=?,attempts=attempts+1 WHERE id=?", (lease, now + 30, row["id"]))
return row["id"], lease, row["envelope"]
def finish_delivery(self, event_id, lease, *, reference=None, error=None, permanent=False, now=None):
now = time.time() if now is None else now
with self._transaction() as db:
row = db.execute("SELECT attempts FROM outbox WHERE id=? AND state='pending' AND lease=?", (event_id, lease)).fetchone()
if row is None:
raise Conflict("delivery lease does not match")
if reference:
db.execute("UPDATE outbox SET state='delivered',receiver_reference=?,lease=NULL,lease_until=NULL,last_error=NULL WHERE id=?", (reference, event_id))
else:
if error not in {"unavailable", "unauthorized", "rejected", "conflict", "invalid_receipt"}:
raise ValueError("a bounded delivery failure code is required")
delay = min(300, 2 ** min(row["attempts"], 8))
db.execute("UPDATE outbox SET state=?,last_error=?,next_attempt=?,lease=NULL,lease_until=NULL WHERE id=?",
("blocked" if permanent else "pending", error, now + delay, event_id))
def requeue_blocked(self, event_id):
"""Explicit operator retry after credential/config repair; same event bytes."""
with self._transaction() as db:
db.execute("UPDATE outbox SET state='pending',next_attempt=0 WHERE id=? AND state='blocked'", (event_id,))
def counts_by_class(self, since=None, until=None):
with self._connection() as db:
if since is None and until is None:
rows = db.execute("SELECT class,COUNT(*) FROM evidence GROUP BY class")
else:
start, end = bounded_window(since, until)
rows = db.execute("SELECT class,COUNT(*) FROM evidence WHERE at>=? AND at<? GROUP BY class", (start, end))
return {row[0]: row[1] for row in rows}
def backup(self, destination: str | Path):
if Path(destination).exists() or Path(destination).is_symlink():
raise Conflict("backup destination must not already exist")
target = Store(destination)
with self._connection() as source, target._connection() as dest:
source.backup(dest)
def bounded_window(since, until):
result = []
for value in (since, until):
if not isinstance(value, str):
raise ValueError("an explicit bounded time window is required")
at = datetime.fromisoformat(value.replace("Z", "+00:00"))
if at.tzinfo is None:
raise ValueError("window must carry its timezone")
result.append(at.astimezone(timezone.utc).isoformat(timespec="microseconds"))
if result[0] >= result[1]:
raise ValueError("window must be increasing")
return tuple(result)