Connect lifecycle revisions to protected human review

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a10780-6d41-7952-9718-dfbc1f41236f
This commit is contained in:
tegwick 2026-10-04 20:33:24 +02:00
parent 0e79d69e27
commit f3b71b1521
16 changed files with 649 additions and 23 deletions

View file

@ -27,7 +27,7 @@ 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 = 4
SCHEMA_VERSION = 5
_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));
@ -99,7 +99,7 @@ class Store:
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 not in (1, 2, 3, SCHEMA_VERSION):
elif version not in (1, 2, 3, 4, SCHEMA_VERSION):
raise StoreError("unsupported evidence schema version")
if version in (0, 1):
db.executescript("""BEGIN IMMEDIATE;
@ -131,6 +131,17 @@ class Store:
db.executescript("BEGIN IMMEDIATE;" + lifecycle_schema
+ "PRAGMA user_version=4; COMMIT;")
if version in (0, 1, 2, 3, 4):
# Earlier code cannot enforce source-revision guards. Refuse an
# image-only downgrade once this schema has been opened.
db.executescript("""BEGIN IMMEDIATE;
CREATE UNIQUE INDEX lifecycle_review_snapshot
ON lifecycle_events(json_extract(body,'$.review_memo_id'))
WHERE kind='review_requested';
CREATE UNIQUE INDEX lifecycle_review_revision
ON lifecycle_events(memo_id,version) WHERE kind='review_requested';
PRAGMA user_version=5; COMMIT;""")
@contextmanager
def _connection(self):
db = sqlite3.connect(self.path.as_uri() + "?mode=rw", uri=True, timeout=5, isolation_level=None)
@ -177,6 +188,15 @@ class Store:
def _save_memo(self, db, memo: Memo):
"""Shared transaction boundary for imports and lifecycle writes."""
from .lifecycle_review import review_request
if review_request(db, memo.id) is not None:
raise Conflict("review snapshots are immutable and loaded only by intake")
if db.execute("SELECT 1 FROM lifecycle_events e JOIN presentations p "
"ON p.memo_id=json_extract(e.body,'$.review_memo_id') "
"JOIN dispositions d ON d.presentation_id=p.id JOIN submissions s ON s.disposition_id=d.id "
"WHERE e.memo_id=? AND e.kind='review_requested' AND s.state IN ('in_flight','unresolved')",
(memo.id,)).fetchone():
raise Conflict("resolve the outstanding review entry before revising")
_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")
@ -210,6 +230,15 @@ class Store:
with self._connection() as db:
return self._memo(db, memo_id, version)
def review_source_current(self, memo_id):
from .lifecycle_review import require_current
with self._connection() as db:
try:
require_current(db, memo_id)
except Conflict:
return False
return True
def observe_policy(self, observation):
request = json.loads(observation.request_json)
_text(request["id"], "policy observation id")
@ -275,6 +304,8 @@ class Store:
raise ValueError("platform tenant and principal provenance are required")
with self._transaction() as db:
memo = self._memo(db, memo_id)
from .lifecycle_review import require_current
require_current(db, memo_id)
if expected_version is not None and memo.version != expected_version:
raise Conflict("memo changed after the entitlement check")
self._guard_policy(db, policy_id, memo, principal_sub, "read")
@ -284,6 +315,10 @@ class Store:
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)}
source = require_current(db, memo_id)
if source is not None:
content["lifecycle_source"] = {"memo_id": source["memo_id"], "version": source["version"],
"request_event_id": source["id"]}
if policy_id is not None:
content["policy_observation_id"] = policy_id
if approval_binding is not None:
@ -299,6 +334,8 @@ class Store:
with self._transaction() as db:
p = self._presentation(db, presentation_id)
memo = self._memo(db, p.memo_id)
from .lifecycle_review import require_current
require_current(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:
@ -333,6 +370,8 @@ class Store:
return disposition_from(json.loads(existing["body"]))
p = self._presentation(db, presentation_id)
memo = self._memo(db, p.memo_id)
from .lifecycle_review import require_current
require_current(db, p.memo_id)
self._guard_policy(db, policy_id, memo, actor.sub, verb.value)
d = record(memo, p, verb, actor, reasons=tuple(reasons), note=note)
if verb in BINDING_VERBS:
@ -377,6 +416,8 @@ class Store:
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)
from .lifecycle_review import require_current
require_current(db, p.memo_id)
if self._memo(db, p.memo_id).version != p.memo_version:
raise DispositionRefused("G_PRES", "presentation is stale")
self._guard_policy(db, policy_id, self._memo(db, p.memo_id), d.actor.sub, "accept")