Align lifecycle persistence with review isolation and runtime health

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a10780-6d41-7952-9718-dfbc1f41236f
This commit is contained in:
tegwick 2026-10-04 18:34:40 +02:00
parent b2a4b863a8
commit f0e9341650
8 changed files with 102 additions and 5 deletions

View file

@ -18,7 +18,9 @@ comments and event retrieval using the existing Memo and Store (schema 4). Its
local event queue is durable. `LifecycleService` now wraps these operations
with fresh exact-policy observations, expiry checks inside write transactions,
and a verified human-session adapter. HTTP exposure, workload identity composition
and external event delivery remain unimplemented. See
and external event delivery remain unimplemented. Unpublished lifecycle records
are excluded from the legacy approver overview, legacy import cannot bypass their
revision history, and pending lifecycle events close runtime readiness. See
`docs/specs/LifecycleFoundationContract.md` and
`docs/specs/LifecycleServiceContract.md`. Frictionless intake has local implementation
but its live delivery remains under `INFD-WP-0005`. The dated deployment snapshot

View file

@ -0,0 +1,35 @@
# Lifecycle and existing infrastructure alignment
Source review on 2026-10-04, INFD-WP-0007-T02/T03. No cluster inspection or
deployment occurred. Sources: access-engine `c0f3f25`, approval-engine `17f2225`,
audit-core `f1035a8`, railiance-platform `267814b`; local informed-decision
`deploy/sitting-admission.json` pins compact-sitting v11.
| Boundary | Finding and disposition |
| --- | --- |
| Store/schema | Lifecycle shares schema 4 and existing memo evidence; schema 1–3 migration/restart/backup tests pass. Legacy `save_memo` could write registered revisions without lifecycle events; it now refuses those ids. |
| Legacy UI | `memos_for` previously selected unpublished lifecycle records by named reviewer. Overview could show their ids as unavailable before authorization. Registered lifecycle records are now excluded pending explicit publication/review integration. Existing legacy review records remain eligible. |
| Runtime/audit | New pending events were invisible to readiness and admin inspection. Readiness now refuses immediately on pending lifecycle evidence; inspection reports record/event/pending counts without content. Receiver delivery remains unimplemented and pending rows are retained. |
| Access Engine | Current compact-sitting v11 contains exact-record human review and an attribute-admission branch with empty submitter/class sets. It does not admit `lifecycle.*`. Shared PolicyClient response validation is reused; a deployed review pin is not a lifecycle policy grant. |
| Approval Engine | Unbound registration creates no approval and cannot consume or accept one. Existing real-engine presentation/entry tests pass; publishing a lifecycle revision for binding still needs an explicit integration contract. |
| Identity | Human caller import reuses authenticated KeyCape session provenance. Workload lifecycle identity has only a trusted application interface; the private intake audience is not authorization for a new API. No issuer/audience/tenant claim is invented. |
| Platform | Source-phase requester composition still needs private listener/runtime configuration and CA-verified TokenReview. Platform's dependency-requirements file explicitly separates that source work from later native custody/rollout approval. Lifecycle HTTP development does not satisfy WP-0005-T05. |
Validation command:
```sh
INFD_APPROVAL_ENGINE_SOURCE=/home/worsch/approval-engine \
INFD_AUDIT_CORE_SOURCE=/home/worsch/audit-core \
INFD_STATE_HUB_SOURCE=/home/worsch/state-hub \
uv run --extra dev python -m pytest -q
```
Result before subsequent HTTP adapter development: **571 passed, 5 skipped**.
These use actual owner source APIs with synthetic identity/custody; they do not
prove native policy admission, live issuer custody or cluster routing. Lifecycle
policy admission and new event receiver/delivery contracts remain live work under
WP-0007-T02/T03; private intake runtime/admission delivery remains WP-0005.
Schema rollback requires a pre-upgrade database backup and reconciliation of any
subsequent writes. Simply deploying a schema-3 binary against schema 4 is not a
supported rollback. No production migration or image change was performed.

View file

@ -81,7 +81,9 @@ Pending lifecycle events are durable local obligations. They are not emitted as
invented Audit Core event types or reported delivered. The existing review audit
queue remains unchanged. Before exposure, T02/T03 must settle receiver admission,
payload/privacy, delivery retries and readiness accounting for the new queue.
The existing readiness check does not account for it yet.
Runtime readiness now refuses while lifecycle events remain pending, and the
admin inspection command reports content-free lifecycle counts. No events are
marked delivered without the future admitted receiver adapter.
## Remaining contracts

View file

@ -123,5 +123,6 @@ def admin():
else:
with store._connection() as db:
print(json.dumps({'schema_version': db.execute('PRAGMA user_version').fetchone()[0],
'lifecycle': store.lifecycle_counts(),
'outbox': {r[0]: r[1] for r in db.execute('SELECT state,COUNT(*) FROM outbox GROUP BY state')},
'submissions': {r[0]: r[1] for r in db.execute('SELECT state,COUNT(*) FROM submissions GROUP BY state')}}))

View file

@ -47,7 +47,11 @@ class AuditPump:
def ready(self):
with self._lock:
return self._last_ok is not None and self.clock() - self._last_ok < 90
recent = self._last_ok is not None and self.clock() - self._last_ok < 90
try:
return recent and self.store.lifecycle_counts()["pending"] == 0
except Exception:
return False
def tick(self):
try:
@ -78,6 +82,8 @@ class AuditPump:
row["state"] != "delivered" for row in self.store.outbox())
if self.intake_worker and self.store.intake_audit_pending():
healthy = False
if self.store.lifecycle_counts()["pending"]:
healthy = False
with self._lock:
self._last_ok = now if healthy else None
except Exception:

View file

@ -168,6 +168,8 @@ class Store:
def save_memo(self, memo: Memo):
with self._transaction() as db:
if db.execute("SELECT 1 FROM lifecycle_records WHERE memo_id=?", (memo.id,)).fetchone():
raise Conflict("registered lifecycle memos require lifecycle revision commands")
self._save_memo(db, memo)
def _save_memo(self, db, memo: Memo):
@ -421,13 +423,23 @@ class Store:
"SELECT body FROM dispositions WHERE presentation_id=? ORDER BY rowid", (presentation_id,))]
def memos_for(self, subject):
"""Latest version of every held memo naming ``subject`` as the person bound."""
"""Legacy review candidates; unpublished lifecycle records stay private."""
with self._connection() as db:
rows = db.execute("SELECT m.body FROM memos m WHERE m.version="
"(SELECT MAX(version) FROM memos WHERE id=m.id) ORDER BY m.id").fetchall()
"(SELECT MAX(version) FROM memos WHERE id=m.id) "
"AND NOT EXISTS (SELECT 1 FROM lifecycle_records l WHERE l.memo_id=m.id) "
"ORDER BY m.id").fetchall()
memos = (memo_from(json.loads(r[0])) for r in rows)
return [m for m in memos if m.binding.principal.id == subject and m.binding.principal.kind == "person"]
def lifecycle_counts(self):
"""Content-free operational counts, including undelivered lifecycle evidence."""
with self._connection() as db:
return {name: db.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
for name, table in (("records", "lifecycle_records"),
("events", "lifecycle_events"),
("pending", "lifecycle_pending"))}
def dispositions_by(self, subject, memo_id):
"""``subject``'s dispositions on every version of a memo, oldest first, with any submission."""
with self._connection() as db:

View file

@ -184,3 +184,31 @@ def test_foundation_imports_no_ui_or_http_runtime():
"assert not {'informed_decision.ui', 'informed_decision.web', "
"'informed_decision.runtime'} & sys.modules.keys()"], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
def test_unpublished_records_do_not_enter_legacy_review_overview(workspace):
repo, memo = workspace
legacy = replace(memo, id="legacy-review")
repo.store.save_memo(legacy)
repo.register(AGENT, memo, key="register")
assert [m.id for m in repo.store.memos_for(memo.binding.principal.id)] == [legacy.id]
def test_legacy_import_cannot_bypass_lifecycle_revision_history(workspace):
repo, memo = workspace
repo.register(AGENT, memo, key="register")
with pytest.raises(Conflict, match="lifecycle revision"):
repo.store.save_memo(memo.next_version(brief="unattributed edit"))
assert repo.retrieve(AGENT.tenant, memo.id).version == 1
assert repo.store.lifecycle_counts() == {"records": 1, "events": 1, "pending": 1}
def test_pending_lifecycle_events_close_existing_runtime_readiness_immediately(workspace):
from informed_decision.runtime import AuditPump
repo, memo = workspace
pump = AuditPump(repo.store, None, clock=lambda: 100)
pump._last_ok = 100
assert pump.ready()
repo.register(AGENT, memo, key="register")
assert not pump.ready()
assert not AuditPump(Store(repo.store.path), None).ready()

View file

@ -163,3 +163,14 @@ T02/T03 remain in progress; no listener or policy grant was deployed.
Validation: `uv run --extra dev python -m pytest -q` — 501 passed, 72 skipped;
`git diff --check` passed.
## Infrastructure alignment — 2026-10-04
Reviewed current local owner sources and shipped v11 sitting pins; findings are
in `docs/evidence/2026-10-04-lifecycle-infrastructure-alignment.md`. Fixed legacy
overview exposure of unpublished lifecycle ids, import bypass of lifecycle
revision history, and readiness/admin omission of pending lifecycle evidence.
Cross-repo suite with Approval Engine, Audit Core and State Hub source fixtures:
571 passed, 5 skipped. This is source integration evidence, not live cluster
verification or new lifecycle policy admission. External runtime/identity/policy/
receiver contracts remain on T02/T03 and WP-0005.