diff --git a/SCOPE.md b/SCOPE.md index fac43f6..4a66826 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -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 diff --git a/docs/evidence/2026-10-04-lifecycle-infrastructure-alignment.md b/docs/evidence/2026-10-04-lifecycle-infrastructure-alignment.md new file mode 100644 index 0000000..f96d2b9 --- /dev/null +++ b/docs/evidence/2026-10-04-lifecycle-infrastructure-alignment.md @@ -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. diff --git a/docs/specs/LifecycleFoundationContract.md b/docs/specs/LifecycleFoundationContract.md index f15ad7d..dbfb534 100644 --- a/docs/specs/LifecycleFoundationContract.md +++ b/docs/specs/LifecycleFoundationContract.md @@ -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 diff --git a/informed_decision/container.py b/informed_decision/container.py index 6bf02b2..d83556a 100644 --- a/informed_decision/container.py +++ b/informed_decision/container.py @@ -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')}})) diff --git a/informed_decision/runtime.py b/informed_decision/runtime.py index 29db1cf..92ba87b 100644 --- a/informed_decision/runtime.py +++ b/informed_decision/runtime.py @@ -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: diff --git a/informed_decision/store.py b/informed_decision/store.py index f0cc910..4ddaaaf 100644 --- a/informed_decision/store.py +++ b/informed_decision/store.py @@ -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: diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index c55e2f3..9ea6804 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -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() diff --git a/workplans/INFD-WP-0007-decision-lifecycle-service.md b/workplans/INFD-WP-0007-decision-lifecycle-service.md index b6df56c..29c6be0 100644 --- a/workplans/INFD-WP-0007-decision-lifecycle-service.md +++ b/workplans/INFD-WP-0007-decision-lifecycle-service.md @@ -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.