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:
parent
0e79d69e27
commit
f3b71b1521
16 changed files with 649 additions and 23 deletions
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue