Skip to content

feat(reporting): deliver reporting subscriptions through production managed delivery - #1733

Merged
bokelley merged 15 commits into
nextfrom
claude/issue-1695-managed-production
Oct 31, 2026
Merged

bokelley merged 15 commits into
nextfrom
claude/issue-1695-managed-production

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Part of #1695, slice 4 (managed delivery): 4a2, production composition on the memory store, following adcontextprotocol/adcp-client#3523. It stacks on #1731 and everything below it. Review only feat(reporting): deliver reporting subscriptions through production managed delivery.

ReliableReportingService(production=..., reporting_subscriptions=ReportingSubscriptionServiceOptions(..., bind_managed_generation=...)) now delivers subscriptions through the production profile. This PR covers the memory store; PostgreSQL is 4b.

Destination binding (TS: "binding comes first")

  • Bound once: bind_managed_generation(configuration, version) is called once per (version, offering) generation, before that generation's first obligation is written. It is never called again for an admitted generation.
  • What the host does: it resolves the version's destination.destination_id to the owner's current destination generation, authorizes it for the account and returns the ReportingDestinationBinding. Per-account authorizations of a shared destination_ref are therefore the host's.
  • Checks: the SDK requires the binding to be this generation's, with the version destination's path_template. It then admits the generation and activates the account like sync_accounts admission.
  • Failure: a failed bind writes nothing, and the next turn retries.

Constituents frozen per obligation (TS: "each obligation freezes its own denominator")

  • Freezing at planning: an owed set has no generation-wide denominator. At planning, each obligation freezes the constituents of exactly its media buys from the source's current configuration_binding(configuration) (the host's authorized pool). It is committed atomically with them (commit_subscription_obligation).
  • Outside the pool: a media buy the pool lacks fails that subscription's planning for the turn, and nothing is written.
  • Dispatch and publication: the frozen constituents must still lie within the current pool. That's a subset rule instead of the account generation's exact match, and ReportingProductionSourceBinding.check skips the generation media-buy equality for owed sets.
  • Planner changes: a commit hook; the install hook receives the version's configuration; obligations are created at the planning horizon.

Production turn

  • Each production turn first plans every listed principal's owed sets, then runs each producer's owed sets after its leased work.
  • Owed sets are never leased. One owed set's failure is logged and never stops another or the turn.
  • Admission and the materializer's admission context accept an owed set through _subscription_offering. It checks the generation's resolved offering, contract, finality, definition and binding, with no grid or generation-wide denominator.
  • An owed obligation is materialized whatever the subscription's later state. The inactive parking applies only to account generations.

Authority and requests

Still refused

Tests

tests/conformance/reporting/test_reporting_subscription_production.py uses the memory production profile with the SQLite destination fixture. It uses a verifier whose row schema admits account_id (built in the test; the shipped reference definition is unchanged).

  • End to end: the generation is bound once; two owed obligations are frozen with exactly their buys, produced and materialized; delivered rows carry account_id; status reports offering_id; a second turn binds nothing.
  • Outside the pool: a buy outside the authorized pool fails that subscription's planning and writes nothing.
  • Lost grant: revoking the grant holds the next delivery under the same identity, refuses sync_reporting_status, and a re-grant delivers it.
  • Regression: all reporting suites pass locally (except wheel-building packaging tests, which need more disk than the sandbox has).

This does not adopt beta.2.

🤖 Generated with Claude Code

bokelley and others added 14 commits October 30, 2026 03:20
…ng ledger

A principal's reporting subscription is now executable by the ledger. The
engine's frozen owed sets become ordinary obligations of one configuration
generation per account, owning principal, subscription_id, version and
resolved offering_id, aligned with the TypeScript SDK.

- ReportingConfigurationGenerationKey gains an optional offering_id, set
  only for subscription generations. ledger_asdict omits it when unset, so
  every digest, work key and persisted scope of an account generation is
  byte-identical to before; a guard test forbids bare asdict in reporting.
- SubscriptionObligationPlanner writes sparse, immutable obligations for
  periods ending by the engine's horizon, then advances the owed set's
  completeness horizon. Stores opt in through ReportingSubscriptionLedger;
  the in-memory store implements it, and the planner refuses other stores.
- The producer never grid-fills an owed set and reads an owed obligation
  only once its period has ended. Owed sets are not leased; the service
  runs them through run_configuration.
- get_reporting_status reports offering_id per period, keeps one wire
  generation per id, version and purpose, and leaves scope_closed false
  while the scope ends after the owed set's horizon.
- sync_reporting_status resolves a statement through the caller's owed
  obligation for its period and report definition; a period the owed set
  does not hold fails with INVALID_STATUS_PERIOD.

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

The closed-payload allowlist in reporting_reconciliation_validate now names
offering_id, so the required-schema manifest carries the function's new
fingerprint, computed from a clean migration chain. The public API snapshot
records ReportingSubscriptionGeneration and ReportingSubscriptionLedger.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The engine now persists its state through a compare-and-swap store, so
frozen_owed_sets reads one snapshot of the principal's state, as
owed_periods and subscription_coverage do, instead of the removed
process-local lock.

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

ReliableReportingService(reporting_subscriptions=...) now owes and serves a
principal's reporting subscriptions. Without the option, capabilities,
responses and worker turns are unchanged.

- Each worker turn plans every listed principal's frozen owed set, then
  executes it with the account configurations. A subscription generation is
  installed through the same trusted configure path: account_context
  receives it with configuration.subscription set, and the service validates
  the source route, currency and advertised offering as for an account
  configuration. The horizon is the earlier of the engine's and the turn
  clock.
- A subscription that cannot be planned is isolated, recorded in
  turn.subscription_errors and reported to planning_error_handler or the
  worker error handler; it never stops another subscription or an account
  configuration.
- Capabilities gain configuration_tasks, the subscriptions block,
  standard_dimensions, the feature id and the principal section at the
  served release; callers below 3.3 see the pre-3.3 block, and adcp#8585
  offering fields reach only 3.3.0-beta.2 callers.
- get_reporting_status summary and periods views gain
  subscription_coverage. Ledger reads stay scoped to the caller, and
  resolve_principal is checked against the caller resolver.
- The planner takes an install hook and resolves currency per generation;
  the engine exposes its offerings and status_retention_days.

Production options, managed or receipt-reconciled subscription offerings
and PostgreSQL owed sets are refused or not yet served.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…on grant

Access means access. A loss of authority opens no window: the account is
unknown to every task, sync_reporting_receipts and sync_reporting_status
included. A seller that wants a buyer to finish reconciliation keeps a
reduced grant instead of revoking.

- AuthorizedAccount carries the grant's allowed_tasks, persisted in the
  engine state document only when set, so unscoped documents are unchanged.
- Coverage continues only while the grant lists get_reporting_status; a
  grant reduced below that ends coverage as a revocation does.
- ReportingSubscriptionEngine.grants_task answers from the last reconcile.
- For a caller holding subscription generations on the account,
  ReliableReportingService checks get_reporting_status, exact revision
  reads, sync_reporting_status and sync_reporting_receipts against the
  grant, and refuses an absent task with ACCOUNT_NOT_FOUND.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…SQL ledger

The PostgreSQL generation key widens to account, consumer,
delivery_config_id, delivery_config_version and offering_id, matching the
TypeScript ledger, so several offerings of one subscription version coexist
with account generations, which keep offering_id ''.

This is a stopped-worker maintenance upgrade. Stop every reporting worker,
run create_schema once with this release, then restart: workers of earlier
releases refuse the widened schema. Under the schema lock the upgrade adds
offering_id to configurations, obligations and consumer statuses, widens
the primary key and the obligation period index, and re-points the SDK's
own references at the widened key. Those referencing tables hold
account-level generations only, enforced by a CHECK. A foreign key outside
the SDK refuses the upgrade instead of being dropped. The upgrade is
lossless and idempotent, and a fresh schema ends identical to an upgraded
one.

- PgReportingLedgerStore implements ReportingSubscriptionLedger: owed-set
  obligations, a monotonic completeness horizon in the private
  adcp_reporting_owed_set_progress table, and horizons in status snapshots.
- Every configuration, obligation and status query carries offering_id;
  an owed set is never leased; production, delivery and materializer
  lookups stay on account generations.
- A store with status notifications enabled refuses an owed set until the
  status checkpoints hold one per offering.
- The required-schema manifests and the pinned object counts are
  regenerated from a clean migration chain.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The materializer manifest now lists the discovery table's offering_id
column, CHECK and foreign key, so the installed and exact catalog counts
grow by those objects. The PostgreSQL owed-set test imports psycopg only
after the pool fixture, so it skips cleanly where psycopg is absent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A deactivated subscription stays in subscription_coverage only while it
still has retained obligations for the account. It reads status inactive
with its deactivated_at, which takes precedence over the account-level
status; once no owed period is within status retention its entry is
omitted.

The inactive status and deactivated_at are drafted for 3.3.0-beta.3 and
hand-modelled on the pinned coverage model. They are served only to callers
at 3.3.0-beta.3 or later; earlier callers keep the account-level status
their schema defines. The omission rule applies at every release.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The retention check that decides whether a deactivated subscription stays
listed keeps the entry when its owed periods cannot be computed, and now
logs a warning, as the module's other isolated evaluations do.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hecks authority

The delivery contract for reporting-subscription obligations, ahead of
production managed delivery.

- Every row of a subscription revision carries its account_id, as AdCP 3.3
  requires of delivered rows. The producer stamps it before the revision is
  bound, so revision_content_sha256 and, for managed delivery, the canonical
  content digest cover it; a row naming another account is refused with
  ROW_ACCOUNT_MISMATCH. The report definition's row schema must admit an
  account_id string, or managed publication fails closed. Account-level
  obligations are untouched, so their rows and digests are byte-for-byte
  unchanged.
- ReportingMaterializerService.authorize_subscription_delivery re-checks
  the owning principal's authority before preparing, before writing and
  after writing. A loss holds the materialization: AUTHORIZATION_DENIED
  with same_identity keeps it pending under the same identity for
  authority_hold_seconds, and it is never failed.
  ReportingSubscriptionEngine.authorizes_delivery answers from the grant.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ts binding

A reporting subscription's managed delivery binding carries the version's
destination path_template, validated as a relative path with only the
spec's placeholders. ReportingDestinationBinding.delivery_path expands it
per delivery, one path segment per value, and ReportingPreparedRevision
exposes it so writers place a subscription's objects under it; the
reference writer does, below its collision-free attempt prefix. Only a
subscription generation's binding may carry one. ledger_asdict omits an
unset path_template, so account bindings keep their fingerprints and
stored payloads byte for byte.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A subscription destination binding may carry path_template, so the
PostgreSQL closed-payload allowlist names it and the required-schema
manifest carries the guard function's new fingerprint, computed from a
clean migration chain. Account bindings never write the key.

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

authorize_subscription_delivery runs through the attempt's I/O context,
like every destination call, so a hung hook ends in DEADLINE_EXCEEDED
under the same identity and the materialization is held, rather than the
heartbeat renewing its lease indefinitely.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…anaged delivery

ReliableReportingService(production=..., reporting_subscriptions=...) now
serves subscriptions through the memory production profile, following
adcp-client 3523. A host supplies bind_managed_generation.

- Each managed generation is bound once, before its first obligation: the
  hook resolves the version's destination to the owner's current
  generation and authorizes it for the account; its path_template must
  equal the version's. A failure writes nothing and the next turn retries.
- Each owed obligation freezes the constituents of exactly its media buys
  from the source's current authorized pool, atomically with the
  obligation. A buy outside the pool fails that subscription's planning;
  at dispatch and publication the frozen constituents must still lie
  within the pool. The planner gains a commit hook, passes each version's
  configuration to its install hook, and plans obligations at the horizon.
- Production plans subscriptions at the start of each turn and runs owed
  sets, which are never leased, isolating each one's failure. Admission,
  the materializer's admission context and the source checks treat an
  owed set as having no generation-wide denominator, and an owed
  obligation is delivered whatever the subscription's later state.
- The materializer re-checks engine.authorizes_delivery at each attempt,
  and the mounted production handler checks each task against the grant
  and adds subscription_coverage.

Receipt-reconciled subscription offerings and PostgreSQL production stores
remain refused.

Co-Authored-By: Claude Opus 5.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 — new production composition for reporting-subscription managed delivery on the memory store, with a 445-line conformance test backing the load-bearing invariants.

Decision trace (top-down):

  • Row 1 (critical/high): no blocking findings. Reviewer verified binding-once + idempotent admit, per-obligation constituent freeze (buy-outside-pool fails planning and writes nothing, tested L411), owed-set exemption from the generation-wide denominator at both the lenient store check and strict dispatch re-check, lost-grant holds delivery under the same identity with re-grant delivering (tested L420), grant checks on all four production tasks, coverage gated by serves_subscriptions. PostgreSQL and receipt-reconciled offerings correctly refused.
  • Row 2 (gated_paths): false, N/A.
  • Row 3/5 (high_risk deleted/modified+medium): high_risk is true via src/adcp/decisioning/handler.py (modified), but the single medium finding lands in src/adcp/reporting/production/service.py — NOT on the modified high-risk file. Row 5 requires the medium to coincide with the modified high-risk file; it does not. No deletions. Neither row fires.
  • Row 4 (medium + data-loss/schema/infra): the one medium is an observability/diagnostics gap (owed-set run failure logs only the exception type, bypassing the worker's structured diagnostics signals) — not data-loss/schema/infra category. Does not fire.
  • Row 6: no prior decision.
  • Row 7: no no-auto-approve team match.
  • Row 8: only 1 medium finding (< 3).
  • Row 9: approve.

Medium finding (non-blocking):

  • src/adcp/reporting/production/service.py:766 — Owed-set run failure logs only the exception type via the plain module logger, bypassing the worker's _diagnostics closed signals; a persistently failing subscription delivers nothing with no structured operator signal. Worth a follow-up to route through _diagnostics, but a single observability gap does not block.

Medium findings

  • src/adcp/reporting/production/service.py:766 — Owed-set run failure logs only the exception type, bypassing the worker's diagnostics signals

A reporting-subscription owed set that failed run_configuration was logged
only as a module warning carrying the exception type. A subscription that
never delivered was invisible to operators watching the production
diagnostics codes.

Failures now raise the closed REPORTING_PRODUCTION_OWED_SET_FAILED warning
once per turn on the adcp.reporting.production logger, with the integer
failed and total counters only: no identifier, exception or provider
message. Each owed set stays isolated, and the next turn retries.

Co-Authored-By: Claude Opus 5.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.

This is a subsequent review following a prior approve. The developer's delta resolves the single prior Medium finding (service.py:766 — owed-set run failures logged only the exception type, bypassing the worker's diagnostics signals). _run_owed_sets now aggregates per-turn owed-set failures into one closed REPORTING_PRODUCTION_OWED_SET_FAILED diagnostics signal via _owed_sets_failed, mirroring _maintenance_failed exactly (closed code, two integer counters, type() is int guards, no identifier/exception/message leak, broad except around handle). A test covers isolation, no-secret-leak (exc_info is None), and the quiet healthy path.

Gate checks:

  • No no-auto-approve team match (HARD RULE N/A).
  • gated_paths: false, and review_decision: APPROVED anyway (HARD RULE N/A).
  • No critical/high findings (row 1 N/A).
  • high_risk: true but the only high-risk reason is src/adcp/decisioning/handler.py (modified) with no medium-or-higher finding on it (rows 3, 5 N/A).
  • No medium findings at all in this run (rows 4, 8 N/A).
  • Prior decision was approve, not escalate (row 6 N/A).

No current findings. Falls through to row 9 → approve.

@bokelley
bokelley merged commit 6015c72 into next Oct 31, 2026
41 checks passed
@bokelley
bokelley deleted the claude/issue-1695-managed-production branch October 31, 2026 14:24
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