Repository navigation
fix(accounts): never serve framework-internal account metadata or auth_info - #1826
Conversation
…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>
There was a problem hiding this comment.
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.
Summary
Parity with the TS SDK, which drops the framework-internal
authInfo/ctx_metadatakeys from every served account instripAccountWriteOnlyFields(src/lib/server/responses.ts).The Python equivalents are
metadataandauth_infoonadcp.decisioning.Account.to_wire_accountomits 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_accountsrowsaccount_requests[]request_accountsync_accountsresultsmetadatacan carry upstream OAuth tokens, andauth_infois the verified principal.Change
_scrub_dict(used bystrip_credentials_from_wire_result) dropsmetadata/auth_infoat account positions only: the value of anaccountkey, and each row of anaccounts[]list. It does not strip by bare name. It follows the same pattern as thedestination_billing_entitydrop from fix(accounts): never serve destination_billing_entity on account reads #1823.project_account_for_responsedrops them fromextra='allow'extras on the wireAccountmodel.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:metadatais a wire field on a scrubbed response:list_creativescreatives[].pricing_options[].metadata. It is also a wire field on the signal / property / transformerpricing_optionspaths in other methods.account,account-with-authorization,sync_accountsrow, orsync_governancerow schema definesmetadataorauth_info. Neither key appears at any.account.*or.accounts[].*path.auth_info,ctx_metadata, andauthInfoappear in no cached schema of any version.Deliberately not stripped
mode(and_mode_explicit) on the internalAccount. It is also framework-internal, but TS has no equivalent drop, it is low sensitivity, andmodeis a wire field at many non-account paths.authInfo/ctx_metadata. Python's internal type never produces them.Tests
These tests fail without the fix (6 failures) and pass with it:
_credentialed_account()fixture intests/test_account_requests_seller.pynow carriesmetadataandauth_info. That covers a plain-dict storelist_accountsrow, anaccount_requests[]embedded account (plusrequest_account), and async_accountsecho result.tests/test_credential_leak_strip.py:sync_governanceaccounts[].account;creatives[].pricing_options[].metadatasurvives whilecreatives[].account.metadatais dropped.tests/test_account_projection.pycovers the typed projection dropping the extras while keeping other extras.Local checks:
ruff check src/ packages/adcp-reporting-storage/src/,ruff format --checkon the changed files, andmypy src/adcp/ packages/adcp-reporting-storage/src/adcp_reporting_storage/are clean.🤖 Generated with Claude Code