Persist review evidence and deliver audit records transactionally
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
0e48355b9f
commit
2cc32168ac
14 changed files with 1474 additions and 22 deletions
|
|
@ -97,11 +97,13 @@ The client is an internal seam, not a sufficient binding flow. Before a browser
|
|||
route can call it, T08 must connect:
|
||||
|
||||
1. The access-engine entitlement decision before rendering a named memo.
|
||||
2. A durable presentation/version, actor match and required acknowledgments.
|
||||
3. Dispositions and a transactional evidence outbox, including return/discuss.
|
||||
2. Wire the durable presentation/version, actor match and required acknowledgments.
|
||||
3. Wire persisted dispositions and the transactional outbox, including return/discuss.
|
||||
4. Independent audit custody/delivery and entry-correlation reconciliation.
|
||||
5. Native registered KeyCape login and the deployed Approval Engine proof.
|
||||
|
||||
The browser currently exposes no memo or entry route and no consume capability.
|
||||
The current `evidence.Outbox` is in memory and cannot satisfy durable evidence
|
||||
requirements. These remaining items stay in the active T08 record.
|
||||
The legacy `evidence.Outbox` is an in-memory test double. `Store` now supplies
|
||||
durable atomic state/evidence, and `audit.py` supplies delivery to the real
|
||||
receiver contract; see [durable-review-evidence.md](durable-review-evidence.md).
|
||||
Native custody, policy/controller integration and live admission stay in T08.
|
||||
|
|
|
|||
144
docs/durable-review-evidence.md
Normal file
144
docs/durable-review-evidence.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# Durable review evidence and audit delivery
|
||||
|
||||
Implemented under `INFD-WP-0001-T08`. `store.py` replaces the domain tests'
|
||||
in-memory outbox with a persistent internal store. It does not expose a browser
|
||||
route, decide entitlement, or authorize an approval entry. `web.py` still has
|
||||
no memo/entry routes and `/readyz` remains 503.
|
||||
|
||||
## Storage and recovery
|
||||
|
||||
The store uses a private SQLite file (0600), inside an existing directory owned
|
||||
by the process user (0700). Symlinks, hard-linked databases, unsafe permissions,
|
||||
unrecognized databases and unsupported schema versions are refused. WAL,
|
||||
foreign keys and FULL synchronous commits are enabled. Connections are scoped
|
||||
to operations; concurrent writers serialize through SQLite transactions.
|
||||
This is a local Unix filesystem design, not shared network storage.
|
||||
|
||||
The existing domain objects are serialized to JSON, without pickle or a second
|
||||
Decision Memo schema. Memo versions, packet documents, original presentations,
|
||||
acknowledgments, dispositions and evidence rows are immutable. SQL triggers
|
||||
reject ordinary updates/deletes; they do not protect against a database
|
||||
administrator or a compromised process with file access.
|
||||
|
||||
Packet bytes must exist in the store before a memo can reference their SHA-256
|
||||
hashes. Presentation content includes the memo and the exact binding/awareness
|
||||
documents. A presentation is produced only through `presentation.render`, the
|
||||
existing writer of `view_hash`. Packet contents are checked against their
|
||||
hashes on custody retrieval. A retrieval failure names Informed Decision as
|
||||
the custodian that cannot produce the material; it never returns a blank as
|
||||
though no evidence had been promised.
|
||||
|
||||
Every presentation, explicit acknowledgment, disposition and submission result
|
||||
commits its evidence and outbox row in the same local transaction. An outbox
|
||||
write failure rolls the state change back. Acknowledgments append separate
|
||||
records; they never overwrite the original presentation. A disposition retains
|
||||
the acknowledgment snapshot that existed at that act, so later acknowledgment
|
||||
does not retroactively strengthen its evidence.
|
||||
|
||||
Actor and presentation recipient must match. Binding also requires verified
|
||||
human provenance, the current memo version, all required acknowledgments and
|
||||
the existing step/seal guards. An accept intent requires the native approval id
|
||||
and carried digest. Return, discussion and decline remain distinct local acts;
|
||||
only accept creates an external-submission record. A dependency outage is a
|
||||
stance application, never an invented human decline.
|
||||
|
||||
`Store.backup(new_path)` uses SQLite's backup API; it refuses an existing
|
||||
destination. Restoring the resulting private database preserves evidence,
|
||||
outbox ids and submission state. Tests cover process death before commit,
|
||||
reopening after commit and restoring pending/confirmed records. An admitted
|
||||
production backup location, schedule and restoration drill are still owed.
|
||||
There is no automatic deletion/retention job in this implementation.
|
||||
|
||||
## Approval submission and uncertainty
|
||||
|
||||
These are internal delivery records, not cached approval validity:
|
||||
|
||||
1. Record the human's accept intent and its evidence before an engine POST.
|
||||
2. The controller must obtain a fresh applicable access-engine decision, verify
|
||||
the session and compare the memo's carried digest with the live approval.
|
||||
**That controller/policy integration is still pending.**
|
||||
3. `begin_submission` atomically reserves one attempt. A stale presentation
|
||||
cannot start; a memo cannot be revised while its entry attempt is in flight
|
||||
or unresolved. Network I/O happens outside the SQLite transaction.
|
||||
4. A successful non-duplicate entry response is correlated to the original
|
||||
disposition using `(approval_id, subject, approved_at)`, atomically with an
|
||||
evidence record. This confirms delivery, not permission to execute the act.
|
||||
|
||||
Operation ids make the same click idempotent; reuse with different content is
|
||||
refused. A second presentation cannot acquire another intent for the same
|
||||
approval and approver. A known confirmed correlation retrieves the original
|
||||
presentation and acknowledgment snapshot.
|
||||
|
||||
**An unknown duplicate or lost response is unresolved.** It may refer to an
|
||||
entry created before this presentation. The engine discards the POST body and
|
||||
does not store a caller's presentation/operation id, so finding an entry later
|
||||
does not prove which presentation caused it. The store does not attach it to
|
||||
the latest view or automatically POST again. A crash after reservation likewise
|
||||
does not reopen dispatch. The records remain visible for explicit controller/
|
||||
operator recovery; no recovery path in this slice fabricates that missing link.
|
||||
The remaining T08 controller must handle this state visibly before shipping.
|
||||
|
||||
## Audit Core contract
|
||||
|
||||
`audit.py` sends the envelope stored at event creation, with stable event id,
|
||||
`Idempotency-Key`, source `informed-decision` and tenant `tenant:platform`.
|
||||
The sender credential is supplied by an owner-provided callback, remains out of
|
||||
the database and is never sent to a redirected endpoint. No native credential
|
||||
is provisioned or read by this implementation task.
|
||||
|
||||
The receiver must return exactly `202 accepted` or `200 duplicate` with
|
||||
`reference=audit:<event-id>` before local delivery is marked complete. Lost
|
||||
responses replay the exact bytes; Audit Core deduplicates them. A 30-second
|
||||
local lease bounds concurrent claims; stale workers cannot overwrite a newer
|
||||
delivery result. Retryable failures back off to a five-minute maximum.
|
||||
Malformed events, conflicts and credential refusals remain visibly blocked;
|
||||
an explicit requeue after repair preserves the original event id and body.
|
||||
No failure drops the evidence or stores the upstream response body as an error.
|
||||
|
||||
Only commitments and metadata travel: no brief, packet content or discussion
|
||||
note. Native sender custody, receiver registration/application and independent
|
||||
operator access still need live proof. Component tests use Audit Core's actual
|
||||
ingestion/read APIs with a distinct SQLite fixture and synthetic scoped sender
|
||||
and auditor identities. That is contract proof, not production custody proof.
|
||||
|
||||
Per-class heartbeat generation uses the existing 86,400-second declarations.
|
||||
It does not emit “nothing to report” for a class with undelivered evidence.
|
||||
The host must schedule heartbeat generation, bounded draining and monitoring;
|
||||
no background service or cadence is installed by this source change.
|
||||
|
||||
## Reconciliation has two time bases
|
||||
|
||||
Audit Core's current `/v1/reconciliation` counts rows by **accepted_at** and
|
||||
returns `[{"class": ..., "count": ...}]`. The source counts events by
|
||||
**occurred_at**. A delayed delivery can fall into different windows even when
|
||||
nothing is missing. The client verifies source, tenant, explicit time window and
|
||||
count shape, and reports both time bases with `count_values_match`.
|
||||
|
||||
It deliberately sets `automatic_loss_finding=false`, `completeness_proven=false`
|
||||
and `reconstructability_proven=false`. Compare stable catch-up totals and inspect
|
||||
pending/blocked events and receiver references before interpreting divergence.
|
||||
A production reconciliation procedure that accounts for delayed acceptance is
|
||||
still part of T08 admission. Equal counts cannot establish equal membership,
|
||||
complete emission, truthful presentation or availability of source-held content.
|
||||
The GH-DEC-2026-014 commitment-only limitation remains.
|
||||
|
||||
## Verification and next integration
|
||||
|
||||
```sh
|
||||
INFD_APPROVAL_ENGINE_SOURCE=/home/worsch/approval-engine \
|
||||
INFD_AUDIT_CORE_SOURCE=/home/worsch/audit-core \
|
||||
uv run python -m pytest -q
|
||||
```
|
||||
|
||||
258 tests pass: 52 added tests cover transactional rollback, process death,
|
||||
restore, actor/version/ack guards, concurrent clicks, original correlation,
|
||||
ambiguous submissions, audit retry/refusal and actual receiver contracts.
|
||||
The opt-in suites use the actual Approval Engine and Audit Core implementations
|
||||
with synthetic identity/custody. They make no policy decision and spend nothing.
|
||||
|
||||
Remaining under T08: an admitted PDP package/caller and exact read/bind request,
|
||||
protected review/ack/accept/return/discuss routes, durable policy observations,
|
||||
visible unresolved-entry recovery, scheduled audit delivery/reconciliation,
|
||||
native registered human login and deployed-engine/custody proof. No Informed
|
||||
Decision policy package or registration was found in the checked Flex Auth
|
||||
examples, registry and docs at `88b3543`; do not substitute a local allow rule.
|
||||
60
docs/evidence/2026-09-10-durable-review-evidence.json
Normal file
60
docs/evidence/2026-09-10-durable-review-evidence.json
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"schema": "informed-decision.durable-review-evidence.v1",
|
||||
"observed_at": "2026-09-10T21:10:46.040050+00:00",
|
||||
"base_commit": "0e48355b9fcb28ccd0ba09398ee5ac4311e66011",
|
||||
"contract_source_commits": {
|
||||
"approval-engine": "a0a602976eef818f36dde35f76f7f2e589bd051b",
|
||||
"audit-core": "5c0ad522fb36092aa7ec2e8d72f63a5a91853b5b",
|
||||
"flex-auth": "88b354377c8e26b162f1234e673072f1c06dcd89",
|
||||
"key-cape": "139994cfac28ff97163ce4bf263f2bb035bbe0a7"
|
||||
},
|
||||
"source_task": "INFD-WP-0001-T08",
|
||||
"source_task_status": "progress",
|
||||
"verification": {
|
||||
"command": "INFD_APPROVAL_ENGINE_SOURCE=/home/worsch/approval-engine INFD_AUDIT_CORE_SOURCE=/home/worsch/audit-core python -m pytest -q",
|
||||
"tests_passed": 258,
|
||||
"tests_failed": 0,
|
||||
"tests_skipped": 0,
|
||||
"new_tests": 52,
|
||||
"actual_api_component_tests_total": 7,
|
||||
"actual_api_component_tests_new": 4,
|
||||
"identity": "locally signed synthetic fixture",
|
||||
"audit_custody": "actual Audit Core SQLite fixture, separate sender/auditor identities",
|
||||
"native_policy_exercised": false
|
||||
},
|
||||
"implemented": [
|
||||
"private persistent SQLite review/evidence store with immutable versions and packet custody",
|
||||
"atomic presentation, explicit acknowledgment, disposition, submission result and outbox writes",
|
||||
"actor-recipient, current-version and verified-human provenance checks",
|
||||
"single-attempt entry reservation and original presentation correlation",
|
||||
"explicit unresolved state after lost entry response or unknown duplicate; no automatic entry retry",
|
||||
"Audit Core exact-byte idempotent delivery with bounded retries and retained blocked records",
|
||||
"explicit backup/restore, heartbeat generation and bounded two-time-base reconciliation"
|
||||
],
|
||||
"limits": [
|
||||
"internal store only; no new public approval routes",
|
||||
"controller must obtain native PDP decision and validate session and live carried binding digest before render/dispatch",
|
||||
"unknown entry causation remains unresolved; engine stores no caller presentation/operation id",
|
||||
"heartbeats and outbox delivery require host scheduling and monitoring",
|
||||
"source counts occurred_at while Audit Core counts accepted_at; equality does not prove completeness or reconstructability",
|
||||
"no native sender custody or registration/login proof",
|
||||
"no native PDP policy package found in checked Flex Auth examples, registry and docs; owner admission still required"
|
||||
],
|
||||
"source_sha256": {
|
||||
"informed_decision/records.py": "82f696151895c51b168144a3078d05764da3499b96917c465629ce0836fa8e33",
|
||||
"informed_decision/store.py": "a26fffe647b866b061d40aec8ec4f1754d9a8df167973ccdd30ae5dfdb4d7ed4",
|
||||
"informed_decision/audit.py": "06da36a8f3451b4abc7a46da2df66d5939524c170658b879499b2a47a1b10cd3",
|
||||
"informed_decision/disposition.py": "5ddd19c2444f8e6c9bb090d04045cf1227a34554e000f07b3f677325bf8089f8",
|
||||
"informed_decision/evidence.py": "5f84997f66409c7b6cec6ab57e73ad5bba2c1b2d669604adcbf36e72a824a88d",
|
||||
"tests/test_durable_store.py": "e69b8c6c7dfe30291ed1eab2397f3740aa128a38da7c644894e7d04f3f365a29",
|
||||
"tests/test_audit_delivery.py": "c5f8a35335393d74739cd21c1a4d5b5306b30dfd16966d223119d087c18d4fe0",
|
||||
"tests/test_durable_component.py": "7f9e7b5075f7e66104b2b634d5bb14556bf4ff23e7060aae166e3697dc138c29",
|
||||
"tests/test_skeleton.py": "1b9ab97c166870dca24c34149d5f9214ec186de31787407014cbb2342acb1d92"
|
||||
},
|
||||
"browser_approval_routes_exposed": false,
|
||||
"deployed": false,
|
||||
"factory_attempts": 0,
|
||||
"paid_model_calls": 0,
|
||||
"remaining_live_task": "INFD-WP-0001-T08",
|
||||
"factory_gate_task": "HFACT-WP-0001-T03"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue