Skip to content

feat(adapters): add list? option to createDerivedAccountStore for spec-compliant list_accounts#1630

Closed
bokelley wants to merge 2 commits into
mainfrom
claude/issue-1628-derived-store-list-option
Closed

feat(adapters): add list? option to createDerivedAccountStore for spec-compliant list_accounts#1630
bokelley wants to merge 2 commits into
mainfrom
claude/issue-1628-derived-store-list-option

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 9, 2026

Closes #1628

Extends DerivedAccountStoreOptions with an optional list? callback so 'derived'-mode sellers can wire list_accounts without changing their resolution mode. The framework's existing if (accounts.list) gate at from-platform.ts:4576 already auto-wires the handler — the factory just needed to surface the option. Docs and llms.txt are updated to distinguish the 'derived' resolution mode from the factory default, clarify that list? is for discovery/spec-compliance only (the account_id refusal still fires unconditionally), and redirect adopters who need account_id addressing after listing to createTenantStore.

What tested

Nits (not fixed, surfaced here)

  • assert.equal(store.list, undefined) in the new describe('list callback') block is redundant with the existing assertion in the outer describe — harmless but not needed
  • ### Multi-tenant derived heading is semantically broad; a future pass could rename to ### Upstream roster enumeration to be more precise for agents scanning headings
  • Error code + field (INVALID_REQUEST, account.account_id) could be promoted into the pre-example "Key constraint" bold block rather than remaining only in the post-example prose

Pre-PR review

  • code-reviewer: approved — implementation correct (signature match, spread pattern, type assignability, AccountFilter intersection); one nit (redundant assert.equal in new test describe); changeset present and correctly typed as minor
  • docs-expert: approved after fixes — placeholder identifiers annotated as adopter-supplied; llms.txt trimmed to Shape A/B/C density with guide link; three remaining nits surfaced above

Triage-managed PR. This bot does not currently iterate on
review comments or PR conversation threads (only on the source
issue). To unblock:

  • Push fixup commits directly: gh pr checkout <num>
    fix → push.
  • Or re-trigger: comment /triage execute on the source
    issue.

See adcp#3121
for context.

Session: https://claude.ai/code/session_01JhJirRzBzWgSD98zJRpsRb


Generated by Claude Code

Extends `DerivedAccountStoreOptions` with an optional `list?` callback so
`'derived'`-mode sellers can wire `list_accounts` for spec compliance
(every seller must expose `list_accounts` OR `sync_accounts`) without
changing their resolution mode. The framework's existing `if (accounts.list)`
gate at `from-platform.ts:4576` auto-wires the handler when the option is set.

The `account_id` refusal for `'derived'` platforms is unchanged — `list?` is
for discovery only. Docs and `llms.txt` updated to distinguish the resolution
mode from the factory default, and to clarify the listing-is-discovery
constraint with an explicit `createTenantStore` redirect for adopters who
need `account_id` addressing after listing.

Closes #1628

https://claude.ai/code/session_01JhJirRzBzWgSD98zJRpsRb
@bokelley
Copy link
Copy Markdown
Contributor Author

Pausing the merge — Python-side reviewer flagged inverted rejection logic

Heads-up from the Python team that the rejection direction in this PR (and in current main) may be backwards: that derived mode should only accept `account_id` and reject the natural-key arm. Worth investigating before this lands because if true, the new `list?` option doubles down on a wrong contract.

I checked three sources before commenting:

1. AdCP 3.0.9 spec (`schemas/cache/3.0.9/core/account-ref.json`)

The spec describes only two arms and ties them to `require_operator_auth`:

Use `account_id` for explicit accounts (`require_operator_auth: true`, discovered via `list_accounts`). Use the natural key (brand + operator) for implicit accounts (`require_operator_auth: false`, declared via `sync_accounts`).

There is no `derived` mode in the spec. It's an SDK abstraction.

2. JS SDK (current main, `src/lib/server/decisioning/runtime/from-platform.ts:193`)

`refuseInlineAccountIdWhenForbidden` rejects inline `{ account_id }` for both `'implicit'` and `'derived'`; permits the brand+operator arm in both. Documented at `AccountStore.resolution` JSDoc (`account.ts:412`).

3. Python SDK (`adcontextprotocol/adcp-client-python`, `src/adcp/decisioning/accounts.py`)

`SingletonAccounts(resolution='derived')` is described as: "single-platform deployment with per-principal idempotency scoping. Synthesizes `account.id` from the verified principal." The semantics are identical to the JS `createDerivedAccountStore`: auth principal IS the tenant, no buyer-supplied `account_id`.

So the JS SDK and Python SDK are aligned today. The Python team member's assertion that derived should only accept `account_id` doesn't match the Python SDK either.

Possible interpretations of the flag:

  • They're invoking a different `derived`-shaped contract (a downstream Python platform integration, not `SingletonAccounts`)
  • They're conflating `derived` with `explicit` (which IS `account_id`-only)
  • They're proposing the SDKs should change — i.e., it's a forward-looking design objection, not a current-state correctness bug

Recommendation. Don't admin-merge #1630 yet. Two unblocks possible:

  1. Loop the Python reviewer back in: which class / file in `adcp-client-python` are they invoking? If they meant `SingletonAccounts`, the alignment evidence above should resolve it. If they meant something else, that's a real divergence worth a spec issue on `adcontextprotocol/adcp`.
  2. Independent of the rejection-direction question, this PR still has a real CI failure to fix (the `docs/llms.txt` paragraph is generated by `scripts/generate-agent-docs.ts:574`, not hand-edited; the manual edit reverts on `npm run generate-agent-docs`). That fix is straightforward but worth doing on top of a settled contract.

…regeneration

The prior PR commit manually edited docs/llms.txt to document the new
list? option on createDerivedAccountStore, but didn't update the source
template in scripts/generate-agent-docs.ts:574. Running
npm run generate-agent-docs would have reverted the manual change.

Update the template string to match the already-correct llms.txt content
so future doc regeneration preserves the list? documentation.

https://claude.ai/code/session_01S6SKqBPErPdhrJ2R5scwiG
@bokelley
Copy link
Copy Markdown
Contributor Author

CI failure fixed. Pushed a fixup commit (0ef45fe) that updates scripts/generate-agent-docs.ts:574 with the new Shape D text. The docs/llms.txt content was already correct (the manual edit), but the template hadn't been updated, so npm run generate-agent-docs would have reverted it. They now match.


On the rejection-direction question. I read the full chain before responding. @bokelley's analysis is correct:

  • from-platform.ts:193refuseInlineAccountIdWhenForbidden fires for 'implicit' and 'derived'; permits the brand+operator arm in both.
  • derived-account-store.ts:214resolve(_ref, ctx) ignores _ref (the buyer-supplied account reference) unconditionally and derives the account from ctx alone. The comment at line 193–198 says it explicitly: "Buyer-supplied account_id is refused at the framework boundary … same shape as 'implicit''s long-standing refusal."
  • The list? option is doc'd as "discovery, not addressing" — account_ids returned by the callback are informational and cannot be used on subsequent mutating calls.

The Python reviewer's assertion ("derived should only accept account_id") describes 'explicit' mode, not 'derived'. The JS and Python SDKs are aligned. I'd recommend looping them back in with a pointer to SingletonAccounts(resolution='derived') in adcp-client-python — if they're seeing different behavior, they're likely using a different class.

The contract this PR ships is sound. Holding on merge for the Python reviewer's clarification makes sense, but the logic itself doesn't need to change.


Replied by Claude Code. Session: https://claude.ai/code/session_01S6SKqBPErPdhrJ2R5scwiG


Generated by Claude Code

@bokelley
Copy link
Copy Markdown
Contributor Author

Closing — the underlying 'derived' mode semantics are inverted on main, and this PR builds on that inverted model.

Corrected three-mode taxonomy (after offline discussion):

Mode Roster owner Onboarding Wire reference
'explicit' Seller (no list) None account_id inline
'implicit' Buyer-declared sync_accounts brand + operator
'derived' Upstream platform list_accounts account_id inline

'derived' should be the upstream-managed-roster shape: Meta/Snap/Figma MCP/AudioStack/flashtalking all sit here uniformly, varying only in roster size. The current implementation has account_id rejection and brand+operator acceptance reversed for derived, refuses to refuse sync_accounts, and the spec-compliance gate accepts list_accounts OR sync_accounts instead of requiring list_accounts-only for derived.

Adding list? to derived is the right direction, but landing it on the inverted base would propagate the bug. The mode contract gets fixed first, then any derived-side helpers come back in shape.

Tracking the rework at #1647. Spec clarification (so JS and Python don't diverge again) at adcontextprotocol/adcp#4341.

Closes #1628 conceptually — the original ask (multi-tenant derived needs a path to list_accounts) gets answered by the corrected mode, not by an additive option on the inverted one.

@bokelley bokelley closed this May 10, 2026
@bokelley
Copy link
Copy Markdown
Contributor Author

Acknowledged. The closure rationale is clear — landing the list? option on the inverted derived-mode base would bake in the semantic bug rather than fix it. Will track the corrected mode contract at #1647 and watch adcontextprotocol/adcp#4341 for the spec clarification that prevents JS/Python divergence from recurring.


Generated by Claude Code

@bokelley
Copy link
Copy Markdown
Contributor Author

Noted — #1647 and adcontextprotocol/adcp#4341 are the right threads to watch. Closing the loop here.


Generated by Claude Code

@bokelley
Copy link
Copy Markdown
Contributor Author

Acknowledged — noted the forward references to #1647 and adcontextprotocol/adcp#4341.


_Generated by Claude Code


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

createDerivedAccountStore: factory + docs only cover single-tenant; multi-tenant derived needs a path to list_accounts

2 participants