Skip to content

fix(accounts): scrub account_requests[] and request_account like account rows - #3604

Merged
bokelley merged 1 commit into
mainfrom
claude/scrub-account-requests
Nov 3, 2026
Merged

bokelley merged 1 commit into
mainfrom
claude/scrub-account-requests

Conversation

@bokelley

@bokelley bokelley commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Security fix

A resolved AdCP 3.3 AccountRequest embeds a full Account. The SDK scrubbed list_accounts accounts[] rows but served account_requests[] raw, and request_account had no response builder at all (wrap: null → genericResponse). Whatever the seller kept on the account went to the buyer and into the idempotency replay cache:

  • billing_entity.bank
  • governance_agents[].authentication (the seller's bearer for check_governance)
  • notification_configs[].authentication.credentials (webhook signing secrets)
  • destination_billing_entity (request-only; must not appear in account read models)
  • plus requested, if a handler spread the inbound request into it

Affected

Path Since
request_account, createAdcpServer and createAdcpServerFromPlatform 17.0.0-beta.10 (eec99e4, 3.3.0-beta.1 adoption)
Raw listAccounts handler returning account_requests 17.0.0-beta.10
Platform accounts.listRequests → account_requests: requests.items 17.0.0-beta.12 (a831111)

Stable 16.x does not serve account requests.

Fix (wire-emit boundary, src/lib/server/responses.ts)

  • listAccountsResponse now scrubs each account_requests[] item.
  • New internal requestAccountResponse wrap for request_account (TOOL_META) scrubs request on both the completed and the status: 'failed' arm.
  • A request's account goes through stripAccountWriteOnlyFields, the same scrubber accounts[] rows use. requested drops the same credential fields (no identity projection).
  • stripAccountWriteOnlyFields now also drops governance_agents[].authentication. The v6 platform path already projected rows to { url }, but raw createAdcpServer list_accounts / sync_accounts / get_media_buys handlers could still echo it.
  • Everything is copy-on-write, so store and handler records are not mutated.

I put the scrub in the response builders, not in from-platform.ts. That way one change covers both the v5 handler-bag path and the v6 platform path, and the cached replay payload is the scrubbed one. This mirrors adcontextprotocol/adcp-client-python#1820. The TS SDK has no CREDENTIAL_BEARING_METHODS-style list (the task-registry and webhook paths don't key scrubbing off tool names), so there was nothing to add request_account to.

Tests

test/lib/account-requests-seller.test.js › account requests never echo write-only account fields:

  • Platform path: one account_requests[] item and one request_account response, each carrying a bank account, a governance credential, a webhook credential and destination_billing_entity on account and requested. All four are stripped, the idempotent replay is clean, and the store record is unchanged.
  • createAdcpServer raw handlers: same checks for an accounts[] row, an account_requests[] item, and the failed request_account arm.
  • Response validation is off in both tests (the production default), so the scrub can't be masked by schema validation.
  • Both tests fail on main without the src/ change (verified by stash + rebuild).

Local checks

  • tsc --noEmit, npm run lint, npm run lint:boundaries: pass. Prettier on changed files: pass.
  • npm run test:lib:fast: everything passes except the live-Postgres suites (ECONNREFUSED, no DB locally).
  • check:package-size: within budget.

🤖 Generated with Claude Code

…unt rows

A resolved AdCP 3.3 account request embeds a full Account. list_accounts
served account_requests[] raw next to the scrubbed account rows, and
request_account had no response builder at all, so billing_entity.bank,
governance_agents[].authentication, notification_configs[].authentication
.credentials and destination_billing_entity reached the buyer and the
idempotency replay cache.

listAccountsResponse now scrubs each account_requests[] item, and
request_account gets a framework wrap that scrubs `request` on both arms.
The embedded account goes through stripAccountWriteOnlyFields; `requested`
loses the same credential fields. The account scrubber also drops
governance_agents[].authentication, which raw list_accounts / sync_accounts /
get_media_buys handlers could otherwise echo. Inputs are copied, not mutated.

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 — security fix scrubs write-only account credentials from list_accounts and request_account responses.

What I checked:

  • Credential scrub (billing_entity.bank, governance_agents[].authentication, notification_configs[].authentication.credentials, destination_billing_entity) applied to account_requests[] and request_account responses, consistent with the existing account-row scrub precedent.
  • Both raw and platform paths funnel through one TOOL_META wrap (create-adcp-server.ts:3573); stripAccountCredentialFields (responses.ts:75) copies and never mutates.
  • Replay path caches the wrapped/scrubbed payload; test asserts no secrets leak on replay with validation off.
  • candidates[] uncovered but AccountCandidate carries no credential fields — not a gap.
  • Changeset present (patch) — defensible for a leak bugfix matching the prior account-row scrub precedent.

No Critical/High/Medium findings. high_risk false, gated_paths false, no author team gate. Rows 1–8 do not fire → row 9 approve.

@bokelley
bokelley merged commit 047f45c into main Nov 3, 2026
45 checks passed
@bokelley
bokelley deleted the claude/scrub-account-requests branch November 3, 2026 00:17
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