What
The RequestContext docstring (adcp/decisioning/context.py:476-509) describes auth_principal as "who's calling" and caller_identity as "what's the cache scope key" / opaque. In practice for bearer-token adopters:
BearerTokenAuthMiddleware populates current_principal ContextVar with the bare principal_id (whatever the adopter's validate_token returns on Principal.caller_identity)
auth_context_factory reads that contextvar and sets ToolContext.caller_identity from it
- BUT — by the time downstream
_impl code reads ctx.caller_identity, it's been mutated to the composite scope key <store_module>.<store_qualname>:<account_id> (e.g., core.stores.accounts.SalesagentAccountStore:wonderstruck:default)
ctx.auth_principal stays None for bearer flows — that field only populates on signed-request flows where AuthInfo.principal is set
Net: a bearer-token adopter trying to read "who's calling?" from ctx.caller_identity gets the composite, not the bare id. salesagent's _create_media_buy_impl uses identity.principal_id for a Principal-table lookup, which 404'd on the composite key.
Repro
# core/platforms/_delegate.py
def _build_identity(ctx):
# WRONG — returns composite scope-key in bearer flows
principal_id = ctx.caller_identity # → "core.stores.accounts.SalesagentAccountStore:wonderstruck:default"
return ResolvedIdentity(principal_id=principal_id, ...)
The fix in salesagent was to import the framework's contextvar directly:
from adcp.server.auth import current_principal
principal_id = current_principal.get() # bare value, e.g. "wonderstruck_buyer"
That works but requires reaching into module-private state.
What we'd want
Either:
- Docs clarify that
caller_identity is mutated to composite by downstream framework code, and document the correct read path for bearer adopters (adcp.server.auth.current_principal.get() or a typed helper)
- Populate
ctx.auth_principal from the bearer flow too — give bearer adopters a typed read for "who's calling" without reaching into ContextVars
- Stop mutating
ctx.caller_identity from its initial bare-id value; do composite-scoping inside the idempotency middleware via a separate field
(2) is probably the cleanest — auth_principal already exists as the "typed convenience field"; populating it on bearer flows matches its docstring intent.
Filed by
salesagent live GAM CRUD probe — surfaced when _create_media_buy_impl Principal lookup failed because identity.principal_id was the composite. Tracked as salesagent #42.
What
The
RequestContextdocstring (adcp/decisioning/context.py:476-509) describesauth_principalas "who's calling" andcaller_identityas "what's the cache scope key" / opaque. In practice for bearer-token adopters:BearerTokenAuthMiddlewarepopulatescurrent_principalContextVar with the bare principal_id (whatever the adopter'svalidate_tokenreturns onPrincipal.caller_identity)auth_context_factoryreads that contextvar and setsToolContext.caller_identityfrom it_implcode readsctx.caller_identity, it's been mutated to the composite scope key<store_module>.<store_qualname>:<account_id>(e.g.,core.stores.accounts.SalesagentAccountStore:wonderstruck:default)ctx.auth_principalstaysNonefor bearer flows — that field only populates on signed-request flows where AuthInfo.principal is setNet: a bearer-token adopter trying to read "who's calling?" from
ctx.caller_identitygets the composite, not the bare id. salesagent's_create_media_buy_implusesidentity.principal_idfor a Principal-table lookup, which 404'd on the composite key.Repro
The fix in salesagent was to import the framework's contextvar directly:
That works but requires reaching into module-private state.
What we'd want
Either:
caller_identityis mutated to composite by downstream framework code, and document the correct read path for bearer adopters (adcp.server.auth.current_principal.get()or a typed helper)ctx.auth_principalfrom the bearer flow too — give bearer adopters a typed read for "who's calling" without reaching into ContextVarsctx.caller_identityfrom its initial bare-id value; do composite-scoping inside the idempotency middleware via a separate field(2) is probably the cleanest —
auth_principalalready exists as the "typed convenience field"; populating it on bearer flows matches its docstring intent.Filed by
salesagent live GAM CRUD probe — surfaced when
_create_media_buy_implPrincipal lookup failed because identity.principal_id was the composite. Tracked as salesagent #42.