Skip to content

feat(reporting): persist managed delivery reconciliation records - #1176

Open
bokelley wants to merge 4 commits into
conductor/reporting-obligation-currency-1171from
conductor/reporting-durable-records-1167a
Open

bokelley wants to merge 4 commits into
conductor/reporting-obligation-currency-1171from
conductor/reporting-durable-records-1167a

Conversation

@bokelley

@bokelley bokelley commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Refs #1167

This adds the durable storage foundation for seller reporting reconciliation. Trusted publishers and future destination writers/receipt handlers can retain immutable destination bindings, materialization evidence, readability observations, and revision/adjustment receipts with the same state machine in memory and PostgreSQL.

The audit corrections reject evidence attached to a different frozen obligation, give reconciliation its own isolated incremental feed, and enforce the exact evidence graph in PostgreSQL. The buyer selects the obligation's unique current revision first: an official revision wins when present; otherwise the unique unsuperseded snapshot is current. An unmaterialized, pending, or failed current revision never falls back to an older materialized snapshot. Multiple current candidates fail closed.

A second independent adversarial review then reproduced ten further in-scope defects and fixed each with a regression that fails on the previous head. The database now recomputes every retained payload digest under a SQL implementation of canonical_json_utf8_v1, refuses any payload field outside the frozen record types, and re-derives the financial acceptance predicate itself; the buyer selector judges snapshot topology independently of the official close; and the reference validators separate credential shapes from operational names that merely contain a credential word.

Stack

Architecture and corrected findings

  • Exact frozen ownership. Attempts, outcomes, checks, revision receipts and adjustment receipts retain the exact principal/account, configuration generation, obligation and publication identity. Matching logical content does not authorize cross-obligation fan-out. Snapshot and later official histories coexist independently, including equal attempt numbers, separate rejected replacements and terminal acceptances.
  • Independent incremental reconciliation. Optional ReportingReconciliationFeedStore supplies separate record kinds, typed snapshot/cursor/checkpoint tokens and filters. Each account/consumer has a dense append-only sequence/head. Evidence, its caller-visible sequence and its feed row commit together. Core LedgerRecordKind, LedgerPage, change feed and required public store protocols remain unchanged.
  • Scoped, resumable pagination. Caller scope and filters apply before count/limit; keyset pages freeze upper bounds and counts. Tokens bind account, consumer, feed version, filter, bounds and last sequence/key. Partial pages expose a continuation cursor; only the final page exposes a changes checkpoint. Foreign reconciliation writes do not affect Core or another consumer's snapshot ID, count, cursor, checkpoint or next sequence. Reads fail closed on mismatched/missing feed identities.
  • Database-enforced graph. Composite references, insert/immutability/ordinal guards and atomic receipt-head triggers enforce outcome → exact attempt, check → successful materialization, receipt → exact materialization/revision, adjustment receipt → exact adjustment/official revision, and current rejected-leaf replacement. Bidirectional record/feed references and a deferred head reference enforce co-commit even for direct SQL writes. All 15 denormalized identity columns are checked against decoded retained payloads on reads; referenced Core evidence stays immutable while leases/readability remain operational.
  • Database-enforced payload and financial content. reporting_canonical_json implements the canonical_json_utf8_v1 profile in SQL, so every insert recomputes its own content_sha256, and a closed key allowlist refuses any field outside the frozen record types. Ordinary direct SQL — satisfying every column constraint with no trigger disabled — can therefore no longer retain a credential or provider blob under a chosen digest, nor leave a principal's feed permanently unreadable. reporting_reconciliation_evidence re-derives the predicate that decides money: revision row count, typed totals and canonical digest; binding format, readers, method, success status, verification path and retention floor; checksum-to-object binding; receipt profile, totals, profile-specific evidence and readable window; and an adjustment digest recomputed from the retained adjustment columns. A retained payload that disagrees with its own fingerprint fails every read for that principal, including pages whose filter would have skipped the damaged row.
  • Publication topology judged independently. Snapshot leaves are counted on their own and every leaf is walked back through its predecessors, so an official close can no longer mask a forked snapshot history or a supersession cycle. ReportingRevision carries no obligation reference on the wire, so candidates exclude a revision only another obligation has materialized; revisions nobody materialized — an unmaterialized official included — are never excluded, illegal fan-out is still reported, and an unresolvable owner still fails closed. The buyer also requires immutability="native_version" for native_commit, matching the seller.
  • Achievable verification contracts. native_commit requires native_version immutability, exact native references and the exact observation path. The complete method × profile × billing matrix is tested on both stores: dataset/warehouse bindings reject manifest-only verification, and billing requires canonical digests. Success also binds typed/unit-bearing expected totals, row count, format/readers, checksums and retention floors. Managed canonical evidence remains distinct from Core's unchanged digest contract.
  • Provider-native references. Field-specific, secret-safe validators preserve wire-valid operational IDs containing spaces, slash, plus, equals and Unicode. Generated-model → retained-record → wire round trips cover GAM, FreeWheel and warehouse identifiers. Recognizable credentials, tokens, URLs and encoded forms remain rejected without echoing their contents. Credential detection now matches shapes rather than substrings: a keyword condemns a value only when it stands alone as a word and introduces something after it, so tokenized_inventory_daily, secretariat-report-v17 and authorization_metrics_v2 persist unchanged while token=… and Bearer … do not. Independently, any ?name=/&name= query-parameter pair is refused in every reference field, so presigned S3/GCS and Azure SAS material such as report.csv?sv=…&sig=… never reaches storage even though no keyword appears in it.

Exact retries return retained evidence and append nothing; changed retries conflict. Receipt IDs share one namespace, accepted leaves are terminal, and expiry/corruption does not erase acceptance. Adjustment acceptance binds the complete immutable adjustment digest and its official revision; revision and adjustment acceptance remain independent facts that completion must require together.

Migration

create_schema() applies the four bundled SQL resources in one transaction under the shared schema advisory lock. The reconciliation migration is itself atomic in autocommit mode and idempotent. Literal beta.15 and #1171 fixtures preserve existing rows, hashes, currency, leases, issue history and Core feed sequence numbers. The literal initial reconciliation upgrade also preserves records, receipt heads and legacy feed bytes/timestamps while projecting retained order into independent dense account/consumer sequences. Incomplete or inconsistent history is rejected atomically; unknown historical currency/digests/totals remain unknown. Start a new reconciliation snapshot/feed walk after upgrading; legacy Core tokens are not reconciliation tokens. Drain older writers before applying the migrations.

Validation

Python 3.12.14, real PostgreSQL 16.14. The authoritative evidence below is from head 3c405a21f978ed9d3208611bb4a7a8434a056933, tree f81e8437de09ce341b3892bb17cc3edc069c661f, with a clean checkout before and after every run. Earlier c44095e3 evidence is superseded.

  • Full make ci-local on the exact committed head: 9,076 passed, 34 skipped, 9 deselected, 1 expected failure; 85.14% coverage (80% required), pytest 1,075.97s. Lint, mypy over 1,359 source files, mypy --strict over 28 adopter fixtures with no typing suppressions, and generated-code validation all passed. All commit hooks passed, including Black, Ruff, mypy, Bandit and conventional subject validation.
  • Focused reporting gate on that head: 976 passed in 94.62s, up from 893 at c44095e3. It now carries 495 reconciliation conformance regressions (boundaries 23, changes 62, migration 20, profiles 188, SQL 104, store 50, transitions 48) plus 52 buyer-selector cases — +83 new cases from 16 new test functions.
  • Each of the ten defects was reproduced before fixing and each regression fails against the parent's src/. Replacing only src/ with c44095e3 and re-running the new tests yields: signed-query rejection 20 failed/20 passed (the 20 passes are values the old keyword list already caught), benign-name acceptance 10 failed, caller-boundary classification 4 failed, closed-payload 3 failed, payload-digest 1 failed, outcome-evidence 6 failed, receipt-evidence 4 failed, adjustment-digest 1 failed, snapshot-topology 2 failed, identical-scope ownership 1 failed, buyer native_commit immutability 1 failed.
  • Wheel and sdist built from that commit. Both fresh, non-editable installs match all 4 SQL resources byte-for-byte and ship py.typed; no workspace artifacts are packaged. Isolated python -I core import with an empty snapshot/feed/checkpoint probe passes in both installs without any PostgreSQL dependency (PG_AVAILABLE is False).
  • The built wheel with [pg] passed a real PostgreSQL 16.14 exercise across two fresh Python processes: three idempotent create_schema() calls, then a five-record walk paged [1, 2] in the seed process and resumed [3, 4, 5] from the saved cursor in a second process, with the snapshot reading all five. The isolated schema was dropped and the checkout stayed unchanged.
  • Direct feed/token probes on both stores confirmed memory/PostgreSQL parity: Core open_snapshot byte-identical before and after higher-tier writes; inter-page writes deferred past the frozen upper bound; exclusive changes_after at head; replay appending no sequence; and unknown, malformed, overlong, cross-principal, filter-mismatched and ahead tokens all failing indistinguishably as INVALID_CHECKPOINT.
  • reporting_iso_utc and reporting_canonical_json were checked byte-for-byte against iso() and canonical_json_utf8_v1, including sub-millisecond, trailing-zero and non-UTC-offset timestamps, because the recomputed adjustment digest depends on that parity.
  • Wheel SHA-256: e63c5aaa2635cfecf6411824d0a38c0d93270ec84a593c95b509f07efe08993b.
  • Sdist SHA-256: 6b89262c5e06fb4fcb6e0f8eb8f6fba1e9192ccdecc05e23be8a02382c80bea0.

The focused command was:

.venv/bin/pytest tests/conformance/reporting \
  tests/test_reporting_canonical_json.py tests/test_reporting_consumer_loop.py \
  tests/test_reporting_inline_source.py tests/test_reporting_ledger.py \
  tests/test_reporting_reconciliation.py tests/test_reporting_source_contract.py \
  tests/test_reporting_webhook_operation_id.py -q --no-cov

The full gate used the configured real PostgreSQL URL and make ci-local. Full CI on the committed head is the authoritative evidence.

Remaining boundaries

These are trusted low-level storage contracts. Future handlers must derive account/consumer scope from authenticated transport, and future publishers/writers must compute expected evidence and verify actual destination content outside retained records. Core status remains Core-only. Higher-tier handlers, destination writers, capabilities, reconciliation health, bounded wire projections and #1168's atomic notification outbox remain completion work; #1167 stays open.

The reference stores load a consumer's retained graph for transition validation. Feed pages use indexed keyset reads with scoped integrity/count scans and the existing account transaction lock. The integrity scan now recomputes one canonical digest per retained record for the calling principal, and each insert recomputes its own, so both costs grow with a principal's history. Large-history throughput and production-sized migration lock duration remain unbenchmarked. Details are in the storage architecture.

Three review findings were deliberately not changed here, with reasons:

  • Cursors and checkpoints stay unsigned, matching Core's documented design. They bind feed version, account, consumer, filter fingerprint, frozen bounds and last key, and the store re-validates all of it on use. A principal can present a boundary it built itself, but a checkpoint at the current head is byte-identical to the one a completed walk issues, so the store cannot distinguish forgery from a legitimate full walk, and the caller gains nothing it could not reach through the supported API. Foreign-principal tokens are refused. A durable per-reader issuance record would be needed to do better, and that changes the stateless-reader contract.
  • ledger_as_of is not bound into snapshot_id. Binding a clock-derived value would change the snapshot ID on every read and destroy the "foreign writes do not move my snapshot ID" property this slice is required to have. ledger_as_of is echoed-only and affects no row selection.
  • An authoritative page-local revision→obligation ownership projection belongs to the status slice. ReportingRevision has no obligation field and the response's ext extension is not yet a protocol-sanctioned alias model, so this PR narrows candidates by materialization ownership and otherwise fails closed. Two Core-only obligations that share a definition, profile, campaign set and period still read as ambiguous rather than guessing; resolving that needs the status projection planned for #1167C.

A residual, unchanged boundary: no syntax check can decide whether an arbitrary opaque string is a secret. A high-entropy value with no keyword, scheme, userinfo, PEM header, AWS key shape, JWT shape or query-parameter pair still persists, and provenance remains the trusted adapter's responsibility. One cosmetic nit is also left alone: a ledger with both multiple officials and multiple snapshot leaves reports AMBIGUOUS_REVISION_CHAIN twice, which changes no decision.

@gitguardian

gitguardian Bot commented Sep 16, 2026

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@bokelley
bokelley force-pushed the conductor/reporting-durable-records-1167a branch from ac2bd13 to 6fb8936 Compare September 16, 2026 03:13
@aao-secretariat

Copy link
Copy Markdown

Ladon cannot review this PR until merge conflicts are resolved.

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — additive seller reconciliation storage contracts (#1167 storage half).

Checked:

  • No no-auto-approve team match; gated_paths false; high_risk false — no hard gates apply.
  • Reviewer found no blocking or Medium findings across all 26 files.
  • Verified via reviewer notes: multi-tenant isolation, credential/echo boundary (no credentials in ctx_metadata), append-only immutability, transaction atomicity, forward-compat on discriminated unions, and type-system import layering all clean.
  • Semver signal correctly feat: — the change is purely additive (new opt-in stores, managed evidence fields, a bundled migration, DB triggers, wire projections), no public-surface removals/renames/required-flips.

No blocking findings, no medium findings, no escalation triggers — falls through to row 9 (approve).

@bokelley
bokelley force-pushed the conductor/reporting-durable-records-1167a branch from 6fb8936 to c44095e Compare September 16, 2026 04:49

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — reconciliation storage foundation (second pass).

No blocking findings. No medium findings. Reviewer verified memory/PG store parity, the fail-closed transition contract, scoped independent feeds, official-wins revision selection, and an additive public surface carrying the correct feat(reporting) semver signal. Full CI green (8,993 passed).

Gate checks:

  • No no-auto-approve team match.
  • gated_paths: false; review_decision: APPROVED.
  • high_risk: false (no deleted/modified high-risk trigger).
  • Prior decision was approve — this pass is consistent, no new findings.

None of rows 1–8 fire (zero findings), so the outcome falls through to row 9: approve.

…e snapshot topology

Independent adversarial review of the #1167A storage slice reproduced ten
in-scope defects. Each is fixed with a regression that fails on the previous
head.

Buyer selector (`_reconcile._select_current`):

- A unique official close masked a broken snapshot history. `current = official
  or leaves` never inspected snapshot topology, so two unsuperseded snapshot
  roots, or an A<->B supersession cycle, plus one official evaluated
  `definitive=True` with no reasons. Snapshot leaves are now counted on their
  own, and every leaf is walked back through its predecessors so a cycle's
  members stay unreachable and fail closed.
- `native_commit` accepted a resource whose retained descriptor declared
  `immutability="immutable_location"` whenever the native reference and
  observation path matched. The seller already refused that; the buyer now
  mirrors the predicate.
- `ReportingRevision` carries no obligation reference, so scanning semantic
  scope made two legitimate obligations that share a definition, profile,
  campaign set and period see each other's revisions and both go
  non-definitive. Candidates now exclude a revision only another obligation has
  materialized. Revisions nobody materialized -- an unmaterialized official
  included -- are never excluded, illegal fan-out is still reported, and an
  unresolvable owner still fails closed rather than guessing.

Reference validators (`reporting.evidence`):

- `resource_location` accepted Azure-SAS-shaped signed query material because no
  keyword appeared in it. Any `?name=` / `&name=` query-parameter pair is now
  refused in every reference field.
- Unbounded credential substring matching rejected benign operational names such
  as `tokenized_inventory_daily`, `secretariat-report-v17` and
  `authorization_metrics_v2`. A keyword now condemns a value only when it stands
  alone as a word and introduces something after it.

PostgreSQL evidence graph (`reporting_ledger_reconciliation.sql`):

- `content_sha256` was only format-checked and the payload shape was open, so
  ordinary direct SQL -- with no trigger disabled -- retained a credential under
  an attacker-chosen digest and left that principal's whole feed unreadable.
  `reporting_canonical_json` now implements the `canonical_json_utf8_v1` profile
  in SQL, every insert recomputes its own fingerprint, and a closed key
  allowlist refuses any field outside the frozen record types.
- No SQL predicate compared a successful materialization or an accepted receipt
  against the Core revision it named, so a coherent payload with a true
  fingerprint committed `row_count=999` and auto-created an accepted terminal
  head. `reporting_reconciliation_evidence` re-derives the predicate that
  decides money: revision row count, typed totals and canonical digest; binding
  format, readers, method, success status, verification path and retention
  floor; checksum-to-object binding; receipt profile, totals, profile-specific
  evidence and readable window; and an adjustment digest recomputed from the
  retained adjustment columns.
- A filtered page succeeded while a payload-corrupt row sat inside the frozen
  boundary but outside the filter. The read integrity scan now covers the
  caller's whole retained graph, so any mismatch fails every page.

Feed boundaries:

- A caller's stale or hand-built boundary was reported as
  `REPORTING_HISTORY_CORRUPT`. Only a boundary this store opened can accuse
  itself; caller tokens now fail as `INVALID_CHECKPOINT`, indistinguishably from
  unknown, malformed, overlong, cross-principal, filter-mismatched and ahead
  tokens.

Also adds the missing true concurrent-allocation probe: four order-independent
records for one principal commit at once and must take dense unique sequences
with a matching caller-local head.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve.

Delta pass on adversarial-hardening commit 3c405a2 (subsequent review; prior decision was approve with no findings). The reviewer examined all five source deltas — _reconcile._select_current cycle/fork detection, evidence.py credential-shape validators, delivery/delivery_pg boundary attribution, and the +262-line reconciliation SQL evidence graph — plus operational readiness and public-surface/layering. No Critical/High/Medium findings; changes are fail-closed and covered by new regression tests.

Gate checks:

  • No no-auto-approve team match.
  • gated_paths: false (row 2 N/A); review_decision: APPROVED regardless.
  • high_risk: false — no (deleted)/(modified) escalation triggers (rows 3, 5 N/A).
  • No medium findings (rows 4, 8 N/A).
  • Prior decision was approve, not escalate (row 6 N/A).

None of rows 1–8 fire; falls through to row 9 → approve.

bokelley added a commit that referenced this pull request Sep 16, 2026
A cell declaring a metric `missing`/`delayed`/`unsupported` withdrew that
metric's control total unconditionally, including when the declaring
constituent staged no rows at all. `validate_monetary_content` requires the
total for a monetary column every retained row carries, so the composition of
#1173's per-metric evidence with #1175's obligation currency sealed an
immutable publication the ledger then refused with `MONETARY_TOTAL_MISMATCH`
on the replay of every retry -- permanently wedging the obligation for the
answer per-metric evidence exists to give: a buy that delivered nothing and
whose billing is pending.

Distinguish the two shapes rather than exempting money from the withdrawal:

- A withdrawal by a constituent that staged rows still removes the total.
  Those rows carry values the adapter disclaimed, so summing them would
  contradict its own evidence, and the payload stays staged byte-for-byte.
- A withdrawal by a constituent that staged no rows reaches no sum and removes
  nothing. The checksum over the rows that were measured is retained.

A row-empty batch still withdraws, so unavailability is never published as an
observed zero. No total ever sums a disclaimed value.

Monetary columns have no third option: dropping the total is refused by the
ledger and keeping it would falsify the evidence. A withdrawn monetary cell
whose own constituent's rows report that metric now raises before anything is
staged or sealed. `ReportingDefinitionBinding.to_wire()` deliberately keeps
the frozen unit declarations off the wire, so the frozen slice request cannot
carry them; `InlineReportingSource(monetary_metrics=...)` lets a trusted caller
declare #1171's custom `monetary_metric_units` columns, with `spend` always
included. An undeclared custom money column keeps the non-monetary behavior.

Manifest and control-total bytes for a result with no `cell_availability` are
unchanged, byte-identical to the #1176 base.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant