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:
parent
b2a4b863a8
commit
f0e9341650
8 changed files with 102 additions and 5 deletions
4
SCOPE.md
4
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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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')}}))
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue