The resolved account (typically ctx.account inside
a tool dispatch). Pass undefined if no account resolved — the
helper fails closed.
Optionaltool?: stringOptional tool name to surface in the error details
(e.g., 'comply_test_controller').
Optionalmessage?: stringOptional override for the user-facing message. MUST be a static string literal — see "opts.message" note above.
Throws an
AdcpError('PERMISSION_DENIED')if the account is not in a non-production mode. Use to gate dispatch of test-only surfaces.Fail-closed semantics:
account === undefined(no resolved account): throws.account.mode === 'live'or unspecified + nosandbox: true: throws.account.mode === 'sandbox' | 'mock'(or legacysandbox: true): no-op, dispatch proceeds.The
detailspayload carries{ scope: 'sandbox-gate', tool? }so dashboards can distinguish gate-rejections from other permission denials.Resolver discipline. The strength of this gate depends entirely on how the adopter's
AccountStore.resolveconstructs its return value. Resolvers MUST NOT spread untrusted input (request body, headers,ctx_metadata, query params) into the resolved account — doing so lets a buyer self-promote tomode: 'sandbox'and unlock test-only surfaces on a live principal. Sourcemode(andsandbox) from a trusted store keyed by the authenticated principal; never from request data.opts.message must be a static string literal. The message is echoed on the wire inside the error envelope. Interpolating user-controlled values into it creates a reflection sink (PII leakage, log injection, downstream HTML rendering). Pick from a fixed set of messages keyed by
toolif you need variants.