Add protected headless lifecycle application service

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a10780-6d41-7952-9718-dfbc1f41236f
This commit is contained in:
tegwick 2026-10-04 18:25:21 +02:00
parent f7094f8313
commit b2a4b863a8
7 changed files with 439 additions and 12 deletions

View file

@ -90,8 +90,10 @@ class LifecycleRepository:
return None
@staticmethod
def _append(db, scope, digest, memo, kind, body):
def _append(db, scope, digest, memo, kind, body, policy_id=None):
event_id = str(uuid.uuid4())
if policy_id is not None:
body = {**body, "policy_observation_id": policy_id}
db.execute("INSERT INTO lifecycle_events "
"(id,tenant,memo_id,version,kind,actor,at,body) VALUES (?,?,?,?,?,?,?,?)",
(event_id, scope[0], memo.id, memo.version, kind, scope[1], _now(), dumps(body)))
@ -99,12 +101,13 @@ class LifecycleRepository:
db.execute("INSERT INTO lifecycle_operations VALUES (?,?,?,?,?,?)", (*scope, digest, event_id))
return event_id
def register(self, context: Attribution, memo: Memo, *, key: str) -> str:
def register(self, context: Attribution, memo: Memo, *, key: str, guard=None) -> str:
"""Register a complete, unbound version-one memo without creating approval."""
if memo.version != 1 or memo.sealed or memo.approval_id is not None:
raise ValueError("registration requires an unbound version-one memo")
scope, digest = self._request(context, "register", key, memo)
with self.store._transaction() as db:
policy_id = guard(db) if guard is not None else None
replay = self._replay(db, scope, digest)
if replay:
return replay
@ -113,10 +116,10 @@ class LifecycleRepository:
raise Conflict("memo identifier already exists")
self.store._save_memo(db, memo)
db.execute("INSERT INTO lifecycle_records VALUES (?,?,?)", (memo.id, context.tenant, _now()))
return self._append(db, scope, digest, memo, "registered", {"memo_version": memo.version})
return self._append(db, scope, digest, memo, "registered", {"memo_version": memo.version}, policy_id)
def revise(self, context: Attribution, memo: Memo, *, expected_version: int,
summary: str, key: str) -> str:
summary: str, key: str, guard=None) -> str:
"""Compare-and-append; retain existing Store guards on outstanding entries."""
if type(expected_version) is not int or expected_version < 1:
raise ValueError("expected version must be a positive integer")
@ -127,6 +130,7 @@ class LifecycleRepository:
scope, digest = self._request(context, "revise", key,
{"memo": memo, "expected_version": expected_version, "summary": summary})
with self.store._transaction() as db:
policy_id = guard(db) if guard is not None else None
self._record(db, context.tenant, memo.id)
replay = self._replay(db, scope, digest)
if replay:
@ -137,10 +141,10 @@ class LifecycleRepository:
if current.sealed or current.approval_id is not None:
raise Conflict("bound memo requires the review integration path")
self.store._save_memo(db, memo)
return self._append(db, scope, digest, memo, "revised", {"summary": summary})
return self._append(db, scope, digest, memo, "revised", {"summary": summary}, policy_id)
def annotate(self, context: Attribution, memo_id: str, version: int, *, kind: str,
body: dict, key: str) -> str:
body: dict, key: str, guard=None) -> str:
"""Append a source, advisory evaluation or discussion; never a disposition."""
self._validate_annotation(kind, body)
if type(version) is not int or version < 1:
@ -148,6 +152,7 @@ class LifecycleRepository:
scope, digest = self._request(context, kind, key,
{"memo_id": memo_id, "version": version, "body": body})
with self.store._transaction() as db:
policy_id = guard(db) if guard is not None else None
self._record(db, context.tenant, memo_id)
replay = self._replay(db, scope, digest)
if replay:
@ -159,7 +164,7 @@ class LifecycleRepository:
"AND memo_id=? AND version=? AND kind='source'",
(source, context.tenant, memo_id, version)).fetchone():
raise EvidenceUnavailable("evaluation source unavailable for this revision")
return self._append(db, scope, digest, memo, kind, body)
return self._append(db, scope, digest, memo, kind, body, policy_id)
@staticmethod
def _validate_annotation(kind, body):