Skip to content

fix(reporting): bound each seller read in reconcile; scrub account_requests[] - #1820

Merged
bokelley merged 2 commits into
nextfrom
claude/reconcile-timeout-account-requests-scrub
Nov 2, 2026
Merged

bokelley merged 2 commits into
nextfrom
claude/reconcile-timeout-account-requests-scrub

Conversation

@bokelley

@bokelley bokelley commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Two small reviewer follow-ups for the 3.3 line. Two commits, both fix:.

1. fix(reporting): per-seller read timeout for reconcile and fleet reconcile

Nothing bounded a reconcile run, so a hung seller held its fleet concurrency slot indefinitely, and single-seller reconcile could block forever.

  • New read_timeout: timedelta | None on client.reporting.reconcile(), explain(), poll(), events(), and multi.reporting.reconcile(). It also covers the module functions reconcile_subscriptions, explain_buyer_coverage and reconcile_fleet, and ReportingEvents.poll/events. The default is DEFAULT_READ_TIMEOUT = timedelta(seconds=30). None turns the bound off. A zero, negative or non-timedelta value is refused up front as ReportingSubscriptionError(code="INVALID_REQUEST"). The fleet checks it once before fanning out, so a bad value never turns into one issue per seller. The name and the default follow SubscriptionOptions.read_timeout in managed.py. The exception type follows the sibling concurrency check and the TS SDK.
  • What is bounded (TS parity): each read call, not the whole per-seller run. The TS SDK does the same: withReadTimeout in src/lib/principal/reporting-reconcile.ts wraps the client's task methods (default readTimeoutMilliseconds 30 000), and reconcileReportingFleet passes the option through to every seller. Here _BoundedClient wraps the same set: get_principal, get_adcp_capabilities (fetch_capabilities), list_accounts, get_media_buys, get_reporting_status, and the receipts policy's sync_reporting_receipts write.
  • How a timeout surfaces: a timed-out read raises ReportingSubscriptionError(code="TIMEOUT", recovery="transient"). Its message is <task> did not answer within read_timeout (Ns).... It degrades through the same path as any other failed read:
    • A hung get_principal gives SUBSCRIPTION_PRINCIPAL_UNAVAILABLE with a "*" failed entry (error.code == "TIMEOUT"). In the fleet, it becomes that seller's FleetReconcileIssue, the same shape as other per-seller failures, and the fleet carries on.
    • A hung account read marks that account failed with error.task set to the task and error.code == "TIMEOUT".
    • explain() raises the TIMEOUT error.
  • Slot release: at the deadline the read is cancelled and abandoned without waiting for it to unwind (asyncio.wait plus cancel, not wait_for). A seller that ignores cancellation still frees the run and its fleet slot. A TimeoutError raised by the client itself, such as a transport timeout, stays the client's own error and is not relabelled as read_timeout.
  • Tests:
    • Fleet with concurrency=1 and the hung seller first: the other sellers complete and the hung one is reported as timed out.
    • Single-seller timeouts on an account read and on get_principal, plus explain.
    • None turns the bound off; non-positive and non-timedelta values are refused for reconcile, fleet and explain.
    • A read that ignores cancellation is still released.
    • A client's own TimeoutError passes through unchanged.
  • Docs: reporting-subscriptions-buyer.md and reporting-subscriptions-errors.md.

2. fix(accounts): run account_requests[] through the credential scrubber

PlatformHandler.list_accounts scrubbed the accounts[] envelope and only then attached account_requests[], so those items skipped strip_credentials_from_wire_result. request_account never went through the scrubber at all.

This is more than theoretical: a resolved AccountRequest carries a full account: Account, which can hold billing_entity.bank, governance_agents[].authentication and notification_configs[].authentication.credentials.

Fix:

  • The account_requests page is scrubbed under list_accounts before it is attached.
  • request_account's response is scrubbed.
  • request_account joins CREDENTIAL_BEARING_METHODS, so the task-registry and webhook boundaries scrub it too.

The test puts all three credential shapes into an account_requests[] item and into a request_account response. Both come back stripped, and the hook's own record is left untouched. The test fails without the handler change.

TS parity: the TS SDK has the same gap. from-platform.ts serves account_requests: requests.items raw next to projectAccountList(...), so it needs a matching follow-up there.

Checks

  • ruff check src/ packages/adcp-reporting-storage/src/ and ruff format --check on the changed files pass.
  • mypy src/adcp/ packages/adcp-reporting-storage/src/adcp_reporting_storage/ passes.
  • pytest over the reporting-subscription, account, credential-strip, handler-shim, public-API and client suites: 1308 passed, 1 skipped.
  • No changelog edit: CHANGELOG.next.md is generated by release-please.

🤖 Generated with Claude Code

bokelley and others added 2 commits November 2, 2026 22:44
reconcile(), explain(), poll()/events() and multi.reporting.reconcile() take
read_timeout (timedelta, default 30 seconds, None for no bound; a non-positive
or non-timedelta value is refused as INVALID_REQUEST before anything is read).
It bounds each read the run makes (get_principal, get_adcp_capabilities,
list_accounts, get_media_buys, get_reporting_status) and the receipts policy's
sync_reporting_receipts write, mirroring the TS SDK's withReadTimeout and
readTimeoutMilliseconds.

A read that does not answer in time fails as TIMEOUT, naming read_timeout, and
degrades the same way any other failed read does: a hung get_principal is
SUBSCRIPTION_PRINCIPAL_UNAVAILABLE (a fleet issue for that seller), a hung
account read fails that account. The read is cancelled and abandoned at the
deadline, so a hung seller frees its fleet concurrency slot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… credential scrubber

list_accounts scrubbed its accounts[] rows and then attached
account_requests[] unscrubbed, and request_account's response skipped the
scrubber entirely. A resolved AccountRequest carries a full Account, so its
billing_entity.bank, governance_agents[].authentication and
notification_configs[].authentication.credentials now get the same
defense-in-depth strip as the rows beside them. request_account joins
CREDENTIAL_BEARING_METHODS so the task registry and webhook boundaries scrub
it too.

Co-Authored-By: Claude Opus 5.5 <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 — clean two-commit follow-up with no blocking or Medium findings.

Checked:

  • fix(reporting): adds optional read_timeout (default 30s, None disables, non-positive/non-timedelta refused as INVALID_REQUEST) bounding per-seller reads via _BoundedClient; cancels-and-abandons at the deadline so a hung seller frees its fleet concurrency slot, degrades through existing failed-account/fleet-issue paths, and passes through a client's own TimeoutError unrelabelled. Covered by fleet, single-seller, explain, None-disable, bad-value, and cancellation-ignoring tests.
  • fix(accounts): routes account_requests[] and request_account responses through strip_credentials_from_wire_result, closing a real credential-leak gap (bank coords, governance, webhook auth) via the generic recursive scrubber; adds request_account to CREDENTIAL_BEARING_METHODS.
  • Additions are backward-compatible so fix: is the correct semver signal; no wire/enum/task change so DR-0032/0033 pathway requirements do not apply; no import-layering or generated-code violations.

high_risk is true only because two src/adcp/decisioning/** files were modified; with no Medium-or-higher finding on those modified files, row 5 does not fire. gated_paths is false; no no-auto-approve team match. No rows 1–8 triggered → row 9 approve.

@bokelley
bokelley merged commit f203f26 into next Nov 2, 2026
41 checks passed
@bokelley
bokelley deleted the claude/reconcile-timeout-account-requests-scrub branch November 2, 2026 23:38
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