Skip to content

fix(accounts): never serve framework-internal account metadata or auth_info - #1826

Merged
bokelley merged 1 commit into
nextfrom
claude/scrub-framework-internal-keys
Nov 3, 2026
Merged

bokelley merged 1 commit into
nextfrom
claude/scrub-framework-internal-keys

Conversation

@bokelley

@bokelley bokelley commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Parity with the TS SDK, which drops the framework-internal authInfo / ctx_metadata keys from every served account in stripAccountWriteOnlyFields (src/lib/server/responses.ts).

The Python equivalents are metadata and auth_info on adcp.decisioning.Account. to_wire_account omits them by construction, but the defense-in-depth scrubber relied on that typed projection. So a store returning plain dicts, or a handler that spread the internal object (vars(account), {**row}), served them on:

  • list_accounts rows
  • account_requests[]
  • request_account
  • sync_accounts results
  • accounts embedded in media-buy and creative responses

metadata can carry upstream OAuth tokens, and auth_info is the verified principal.

Change

  • _scrub_dict (used by strip_credentials_from_wire_result) drops metadata / auth_info at account positions only: the value of an account key, and each row of an accounts[] list. It does not strip by bare name. It follows the same pattern as the destination_billing_entity drop from fix(accounts): never serve destination_billing_entity on account reads #1823.
  • project_account_for_response drops them from extra='allow' extras on the wire Account model.
  • Requests are untouched, because the scrubber only runs on responses.

Why positional, not by bare name

I walked the ref-resolved 3.3.0-beta.3 response schemas for every method in CREDENTIAL_BEARING_METHODS:

  • metadata is a wire field on a scrubbed response: list_creatives creatives[].pricing_options[].metadata. It is also a wire field on the signal / property / transformer pricing_options paths in other methods.
  • No account, account-with-authorization, sync_accounts row, or sync_governance row schema defines metadata or auth_info. Neither key appears at any .account.* or .accounts[].* path.
  • auth_info, ctx_metadata, and authInfo appear in no cached schema of any version.

Deliberately not stripped

  • mode (and _mode_explicit) on the internal Account. It is also framework-internal, but TS has no equivalent drop, it is low sensitivity, and mode is a wire field at many non-account paths.
  • The TS key names authInfo / ctx_metadata. Python's internal type never produces them.

Tests

These tests fail without the fix (6 failures) and pass with it:

  • The _credentialed_account() fixture in tests/test_account_requests_seller.py now carries metadata and auth_info. That covers a plain-dict store list_accounts row, an account_requests[] embedded account (plus request_account), and a sync_accounts echo result.
  • tests/test_credential_leak_strip.py:
    • covers a media-buy embedded account and a sync_governance accounts[].account;
    • checks that the wire creatives[].pricing_options[].metadata survives while creatives[].account.metadata is dropped.
  • tests/test_account_projection.py covers the typed projection dropping the extras while keeping other extras.

Local checks:

  • ruff check src/ packages/adcp-reporting-storage/src/, ruff format --check on the changed files, and mypy src/adcp/ packages/adcp-reporting-storage/src/adcp_reporting_storage/ are clean.
  • The account, decisioning, credential, webhook, media-buy, creative, governance and handler suites pass (2369 passed).

🤖 Generated with Claude Code

…h_info

adcp.decisioning.Account carries two framework-internal fields, metadata
(adopter state: upstream adapters, OAuth tokens, network ids) and auth_info
(the verified principal). to_wire_account omits them by construction, but
the defense-in-depth scrubber relied on that typed projection: a store
returning plain dicts, or a handler spreading the internal object, served
them on list_accounts rows, account_requests[], request_account,
sync_accounts results and accounts embedded in media-buy / creative
responses. The TS SDK drops the equivalent authInfo / ctx_metadata.

strip_credentials_from_wire_result now drops both keys from account
positions (an `account` object, an `accounts[]` row), not by bare name:
metadata IS a wire field elsewhere on scrubbed responses
(list_creatives creatives[].pricing_options[].metadata), and no account,
account-with-authorization, sync_accounts or sync_governance row schema
defines either key. project_account_for_response drops them from
extra='allow' extras too. Requests are untouched: the scrubber only runs
on responses.

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 — defense-in-depth scrub of framework-internal metadata/auth_info from wire account positions.

No blocking or Medium findings. The reviewer verified: no AdCP account-position schema defines metadata/auth_info (so no real wire field is stripped), full coverage of account positions via the account/accounts[] branches plus existing recursion, non-mutation of the source model's extras, and that no public surface or semver signal is affected.

Decision table trace: no critical/high findings (row 1 n/a); gated_paths false (row 2 n/a); no deletions in high-risk reasons (row 3 n/a); no medium findings in data-loss/schema/infra (row 4 n/a); high_risk is true on a (modified) file but there are no medium findings (row 5 n/a); no prior decision (row 6 n/a); no no-auto-approve team match (row 7 n/a); fewer than three medium findings (row 8 n/a). Falls through to row 9 → approve. The high_risk flag on account_projection.py (modified) is a look-closer signal only; the reviewer found no medium-or-higher concern, so the modification is presumed safe.

@bokelley
bokelley merged commit 2e79ea9 into next Nov 3, 2026
77 of 78 checks passed
@bokelley
bokelley deleted the claude/scrub-framework-internal-keys branch November 3, 2026 03:19
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