@adcp/sdk 6.7.0 → 6.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/bin/adcp.js +15 -3
  2. package/dist/lib/adapters/derived-account-store.d.ts +152 -0
  3. package/dist/lib/adapters/derived-account-store.d.ts.map +1 -0
  4. package/dist/lib/adapters/derived-account-store.js +135 -0
  5. package/dist/lib/adapters/derived-account-store.js.map +1 -0
  6. package/dist/lib/adapters/implicit-account-store.d.ts +3 -1
  7. package/dist/lib/adapters/implicit-account-store.d.ts.map +1 -1
  8. package/dist/lib/adapters/implicit-account-store.js +3 -1
  9. package/dist/lib/adapters/implicit-account-store.js.map +1 -1
  10. package/dist/lib/adapters/index.d.ts +1 -0
  11. package/dist/lib/adapters/index.d.ts.map +1 -1
  12. package/dist/lib/adapters/index.js +7 -1
  13. package/dist/lib/adapters/index.js.map +1 -1
  14. package/dist/lib/adapters/oauth-passthrough-resolver.d.ts +3 -1
  15. package/dist/lib/adapters/oauth-passthrough-resolver.d.ts.map +1 -1
  16. package/dist/lib/adapters/oauth-passthrough-resolver.js +3 -1
  17. package/dist/lib/adapters/oauth-passthrough-resolver.js.map +1 -1
  18. package/dist/lib/adapters/roster-account-store.d.ts +85 -24
  19. package/dist/lib/adapters/roster-account-store.d.ts.map +1 -1
  20. package/dist/lib/adapters/roster-account-store.js +52 -30
  21. package/dist/lib/adapters/roster-account-store.js.map +1 -1
  22. package/dist/lib/index.d.ts +3 -3
  23. package/dist/lib/index.d.ts.map +1 -1
  24. package/dist/lib/index.js +13 -8
  25. package/dist/lib/index.js.map +1 -1
  26. package/dist/lib/mock-server/creative-ad-server/seed-data.d.ts +81 -0
  27. package/dist/lib/mock-server/creative-ad-server/seed-data.d.ts.map +1 -0
  28. package/dist/lib/mock-server/creative-ad-server/seed-data.js +200 -0
  29. package/dist/lib/mock-server/creative-ad-server/seed-data.js.map +1 -0
  30. package/dist/lib/mock-server/creative-ad-server/server.d.ts +39 -0
  31. package/dist/lib/mock-server/creative-ad-server/server.d.ts.map +1 -0
  32. package/dist/lib/mock-server/creative-ad-server/server.js +618 -0
  33. package/dist/lib/mock-server/creative-ad-server/server.js.map +1 -0
  34. package/dist/lib/mock-server/index.d.ts.map +1 -1
  35. package/dist/lib/mock-server/index.js +180 -24
  36. package/dist/lib/mock-server/index.js.map +1 -1
  37. package/dist/lib/mock-server/sales-non-guaranteed/seed-data.d.ts +66 -0
  38. package/dist/lib/mock-server/sales-non-guaranteed/seed-data.d.ts.map +1 -0
  39. package/dist/lib/mock-server/sales-non-guaranteed/seed-data.js +193 -0
  40. package/dist/lib/mock-server/sales-non-guaranteed/seed-data.js.map +1 -0
  41. package/dist/lib/mock-server/sales-non-guaranteed/server.d.ts +33 -0
  42. package/dist/lib/mock-server/sales-non-guaranteed/server.d.ts.map +1 -0
  43. package/dist/lib/mock-server/sales-non-guaranteed/server.js +782 -0
  44. package/dist/lib/mock-server/sales-non-guaranteed/server.js.map +1 -0
  45. package/dist/lib/mock-server/sponsored-intelligence/seed-data.d.ts +50 -0
  46. package/dist/lib/mock-server/sponsored-intelligence/seed-data.d.ts.map +1 -0
  47. package/dist/lib/mock-server/sponsored-intelligence/seed-data.js +133 -0
  48. package/dist/lib/mock-server/sponsored-intelligence/seed-data.js.map +1 -0
  49. package/dist/lib/mock-server/sponsored-intelligence/server.d.ts +13 -0
  50. package/dist/lib/mock-server/sponsored-intelligence/server.d.ts.map +1 -0
  51. package/dist/lib/mock-server/sponsored-intelligence/server.js +609 -0
  52. package/dist/lib/mock-server/sponsored-intelligence/server.js.map +1 -0
  53. package/dist/lib/protocols/mcp.d.ts.map +1 -1
  54. package/dist/lib/protocols/mcp.js +1 -41
  55. package/dist/lib/protocols/mcp.js.map +1 -1
  56. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  57. package/dist/lib/server/account-mode.d.ts +113 -0
  58. package/dist/lib/server/account-mode.d.ts.map +1 -0
  59. package/dist/lib/server/account-mode.js +125 -0
  60. package/dist/lib/server/account-mode.js.map +1 -0
  61. package/dist/lib/server/adcp-server.js +41 -0
  62. package/dist/lib/server/adcp-server.js.map +1 -1
  63. package/dist/lib/server/auth.d.ts +35 -0
  64. package/dist/lib/server/auth.d.ts.map +1 -1
  65. package/dist/lib/server/auth.js.map +1 -1
  66. package/dist/lib/server/create-adcp-server.d.ts +26 -9
  67. package/dist/lib/server/create-adcp-server.d.ts.map +1 -1
  68. package/dist/lib/server/create-adcp-server.js +46 -20
  69. package/dist/lib/server/create-adcp-server.js.map +1 -1
  70. package/dist/lib/server/ctx-metadata/store.d.ts +1 -1
  71. package/dist/lib/server/ctx-metadata/store.d.ts.map +1 -1
  72. package/dist/lib/server/ctx-metadata/store.js +1 -0
  73. package/dist/lib/server/ctx-metadata/store.js.map +1 -1
  74. package/dist/lib/server/decisioning/account.d.ts +5 -0
  75. package/dist/lib/server/decisioning/account.d.ts.map +1 -1
  76. package/dist/lib/server/decisioning/account.js.map +1 -1
  77. package/dist/lib/server/decisioning/buyer-agent.d.ts +37 -4
  78. package/dist/lib/server/decisioning/buyer-agent.d.ts.map +1 -1
  79. package/dist/lib/server/decisioning/buyer-agent.js +12 -2
  80. package/dist/lib/server/decisioning/buyer-agent.js.map +1 -1
  81. package/dist/lib/server/decisioning/compose.d.ts +33 -2
  82. package/dist/lib/server/decisioning/compose.d.ts.map +1 -1
  83. package/dist/lib/server/decisioning/compose.js +13 -46
  84. package/dist/lib/server/decisioning/compose.js.map +1 -1
  85. package/dist/lib/server/decisioning/index.d.ts +2 -1
  86. package/dist/lib/server/decisioning/index.d.ts.map +1 -1
  87. package/dist/lib/server/decisioning/index.js +2 -1
  88. package/dist/lib/server/decisioning/index.js.map +1 -1
  89. package/dist/lib/server/decisioning/platform-helpers.d.ts +18 -0
  90. package/dist/lib/server/decisioning/platform-helpers.d.ts.map +1 -1
  91. package/dist/lib/server/decisioning/platform-helpers.js +20 -0
  92. package/dist/lib/server/decisioning/platform-helpers.js.map +1 -1
  93. package/dist/lib/server/decisioning/platform.d.ts +19 -21
  94. package/dist/lib/server/decisioning/platform.d.ts.map +1 -1
  95. package/dist/lib/server/decisioning/platform.js.map +1 -1
  96. package/dist/lib/server/decisioning/runtime/from-platform.d.ts.map +1 -1
  97. package/dist/lib/server/decisioning/runtime/from-platform.js +334 -44
  98. package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
  99. package/dist/lib/server/decisioning/runtime/observed-modes.d.ts +40 -0
  100. package/dist/lib/server/decisioning/runtime/observed-modes.d.ts.map +1 -0
  101. package/dist/lib/server/decisioning/runtime/observed-modes.js +82 -0
  102. package/dist/lib/server/decisioning/runtime/observed-modes.js.map +1 -0
  103. package/dist/lib/server/decisioning/runtime/protocol-for-tool.js +2 -2
  104. package/dist/lib/server/decisioning/runtime/protocol-for-tool.js.map +1 -1
  105. package/dist/lib/server/decisioning/runtime/validate-platform.d.ts.map +1 -1
  106. package/dist/lib/server/decisioning/runtime/validate-platform.js +9 -1
  107. package/dist/lib/server/decisioning/runtime/validate-platform.js.map +1 -1
  108. package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.d.ts +125 -0
  109. package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.d.ts.map +1 -0
  110. package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.js +52 -0
  111. package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.js.map +1 -0
  112. package/dist/lib/server/decisioning/tenant-registry.d.ts +16 -0
  113. package/dist/lib/server/decisioning/tenant-registry.d.ts.map +1 -1
  114. package/dist/lib/server/decisioning/tenant-registry.js.map +1 -1
  115. package/dist/lib/server/index.d.ts +4 -1
  116. package/dist/lib/server/index.d.ts.map +1 -1
  117. package/dist/lib/server/index.js +9 -3
  118. package/dist/lib/server/index.js.map +1 -1
  119. package/dist/lib/server/serve.js +20 -2
  120. package/dist/lib/server/serve.js.map +1 -1
  121. package/dist/lib/server/test-controller.d.ts +3 -0
  122. package/dist/lib/server/test-controller.d.ts.map +1 -1
  123. package/dist/lib/server/test-controller.js +23 -20
  124. package/dist/lib/server/test-controller.js.map +1 -1
  125. package/dist/lib/testing/comply-controller.d.ts +23 -2
  126. package/dist/lib/testing/comply-controller.d.ts.map +1 -1
  127. package/dist/lib/testing/comply-controller.js +19 -2
  128. package/dist/lib/testing/comply-controller.js.map +1 -1
  129. package/dist/lib/testing/index.d.ts +1 -1
  130. package/dist/lib/testing/index.d.ts.map +1 -1
  131. package/dist/lib/testing/index.js.map +1 -1
  132. package/dist/lib/testing/storyboard/validations.d.ts.map +1 -1
  133. package/dist/lib/testing/storyboard/validations.js +36 -54
  134. package/dist/lib/testing/storyboard/validations.js.map +1 -1
  135. package/dist/lib/testing/test-controller.d.ts +10 -4
  136. package/dist/lib/testing/test-controller.d.ts.map +1 -1
  137. package/dist/lib/testing/test-controller.js +9 -3
  138. package/dist/lib/testing/test-controller.js.map +1 -1
  139. package/dist/lib/types/index.d.ts +3 -2
  140. package/dist/lib/types/index.d.ts.map +1 -1
  141. package/dist/lib/types/index.js +3 -0
  142. package/dist/lib/types/index.js.map +1 -1
  143. package/dist/lib/utils/glob.d.ts +4 -2
  144. package/dist/lib/utils/glob.d.ts.map +1 -1
  145. package/dist/lib/utils/glob.js +4 -2
  146. package/dist/lib/utils/glob.js.map +1 -1
  147. package/dist/lib/version.d.ts +3 -3
  148. package/dist/lib/version.js +3 -3
  149. package/docs/llms.txt +2 -2
  150. package/examples/README.md +29 -13
  151. package/examples/hello-cluster.ts +62 -23
  152. package/examples/hello_creative_adapter_ad_server.ts +790 -0
  153. package/examples/hello_seller_adapter_guaranteed.ts +80 -22
  154. package/examples/hello_seller_adapter_non_guaranteed.ts +1020 -0
  155. package/examples/hello_si_adapter_brand.ts +572 -0
  156. package/package.json +3 -2
  157. package/skills/build-creative-agent/SKILL.md +103 -183
  158. package/skills/build-generative-seller-agent/SKILL.md +15 -9
  159. package/skills/build-governance-agent/SKILL.md +20 -11
  160. package/skills/build-retail-media-agent/SKILL.md +10 -8
  161. package/skills/build-seller-agent/SKILL.md +15 -13
  162. package/skills/build-seller-agent/specialisms/sales-non-guaranteed.md +9 -31
  163. package/skills/build-si-agent/SKILL.md +251 -196
  164. package/skills/build-signals-agent/SKILL.md +2 -0
  165. package/skills/call-adcp-agent/SKILL.md +7 -1
  166. package/skills/call-adcp-agent.previous/SKILL.md +0 -261
@@ -89,6 +89,10 @@ const DEFAULT_FRAMEWORK_LOGGER = {
89
89
  const status_changes_1 = require("../status-changes");
90
90
  const comply_controller_1 = require("../../../testing/comply-controller");
91
91
  const seed_merge_1 = require("../../../testing/seed-merge");
92
+ const adcp_server_1 = require("../../adcp-server");
93
+ const account_mode_1 = require("../../account-mode");
94
+ const test_controller_1 = require("../../test-controller");
95
+ const observed_modes_1 = require("./observed-modes");
92
96
  const normalize_errors_1 = require("../../normalize-errors");
93
97
  /**
94
98
  * Apply `normalizeErrors` to a sync_creatives row's optional `errors`
@@ -105,44 +109,63 @@ function normalizeRowErrors(row) {
105
109
  return { ...row, errors: (0, normalize_errors_1.normalizeErrors)(row.errors) };
106
110
  }
107
111
  /**
108
- * Enforce the documented `'implicit'`-resolution refusal. When a platform
109
- * declares `accounts.resolution: 'implicit'`, the framework refuses inline
110
- * `account_id` references on the wire the buyer is expected to call
111
- * `sync_accounts` first, then the framework resolves the account from the
112
- * authenticated principal on subsequent calls. Documented at
113
- * `AccountStore.resolution` in `account.ts`.
112
+ * Enforce the documented inline-`account_id` refusal for resolution modes
113
+ * that declare the field meaningless on the wire `'implicit'` (since
114
+ * #1364) and `'derived'` (since adcp-client#1468). Both modes share the
115
+ * same wire contract: the buyer does not pass `account_id` inline; the
116
+ * framework derives the tenant from the authenticated principal (after a
117
+ * `sync_accounts` step for `'implicit'`; directly for `'derived'`).
114
118
  *
115
119
  * Throws `AdcpError('INVALID_REQUEST')` before reaching the adopter's
116
120
  * `accounts.resolve`, so each adopter doesn't reimplement the same
117
121
  * `if (ref?.account_id) return null` branch and the wire response is
118
- * consistent across implicit-mode platforms. The brand+operator union arm
119
- * is permitted — the strict-reading docstring claim only refuses
120
- * `account_id`-shaped references.
122
+ * consistent across both modes. The brand+operator union arm is
123
+ * permitted — only `account_id`-shaped references are refused.
124
+ *
125
+ * Mode-specific message and suggestion: `'implicit'` adopters get the
126
+ * `sync_accounts`-first guidance; `'derived'` adopters get the single-
127
+ * tenant explanation (no `sync_accounts` step exists in derived mode —
128
+ * the auth principal alone identifies the tenant).
129
+ *
130
+ * Documented at `AccountStore.resolution` in `account.ts`.
121
131
  */
122
- function refuseImplicitAccountId(resolution, ref) {
123
- if (resolution !== 'implicit')
132
+ function refuseInlineAccountIdWhenForbidden(resolution, ref) {
133
+ if (resolution !== 'implicit' && resolution !== 'derived')
124
134
  return;
125
135
  if ((0, account_1.refAccountId)(ref) === undefined)
126
136
  return;
137
+ if (resolution === 'implicit') {
138
+ throw new async_outcome_1.AdcpError('INVALID_REQUEST', {
139
+ message: 'This platform resolves accounts from the authenticated principal — call sync_accounts first; do not pass account.account_id inline.',
140
+ field: 'account.account_id',
141
+ suggestion: 'Call sync_accounts to associate accounts with your principal, then omit account_id on subsequent calls.',
142
+ });
143
+ }
127
144
  throw new async_outcome_1.AdcpError('INVALID_REQUEST', {
128
- message: 'This platform resolves accounts from the authenticated principal — call sync_accounts first; do not pass account.account_id inline.',
145
+ message: 'This single-tenant agent identifies the tenant from the authenticated principal alone — do not pass account.account_id inline; the field is meaningless on the wire for derived-resolution agents.',
129
146
  field: 'account.account_id',
130
- suggestion: 'Call sync_accounts to associate accounts with your principal, then omit account_id on subsequent calls.',
147
+ suggestion: 'Omit the account field; the framework derives the tenant from your authenticated credential.',
131
148
  });
132
149
  }
133
150
  /**
134
151
  * Dev-mode warning when a multi-id read tool returns fewer rows than
135
152
  * the buyer requested — the canonical signal that the platform is
136
- * silently truncating to `media_buy_ids[0]` (closes #1342, follow-up
137
- * #1399). Catches the bug class where adopters write the recommended
138
- * pattern wrong on first pass; quiet in production where legitimate
139
- * misses (deleted, archived, cross-account) are routine and warning
140
- * on every miss would be noise.
153
+ * silently truncating to `<id_field>[0]` (closes #1342, follow-up
154
+ * #1399, extended in #1410 to additional read-by-id surfaces). Catches
155
+ * the bug class where adopters write the recommended pattern wrong on
156
+ * first pass; quiet in production where legitimate misses (deleted,
157
+ * archived, cross-account) are routine and warning on every miss would
158
+ * be noise.
141
159
  *
142
160
  * Suppressible via `ADCP_SUPPRESS_MULTI_ID_WARN=1` for adopters whose
143
161
  * legitimate-miss rate is high (deleted-account-rich datasets, etc.).
162
+ *
163
+ * Sync / upsert surfaces (`syncCreatives`, `syncCatalogs`, `syncPlans`)
164
+ * are intentionally out of scope — those have a different shape where
165
+ * pass-through is the obvious pattern and "did all rows roundtrip?"
166
+ * isn't a clean question.
144
167
  */
145
- function warnIfTruncatedMultiIdResponse(toolName, requestedIds, responseArray, logger) {
168
+ function warnIfTruncatedMultiIdResponse(toolName, idFieldName, requestedIds, responseArray, logger) {
146
169
  if (process.env.NODE_ENV === 'production')
147
170
  return;
148
171
  if (process.env.ADCP_SUPPRESS_MULTI_ID_WARN === '1')
@@ -152,10 +175,10 @@ function warnIfTruncatedMultiIdResponse(toolName, requestedIds, responseArray, l
152
175
  const returned = Array.isArray(responseArray) ? responseArray.length : 0;
153
176
  if (returned >= requestedIds.length)
154
177
  return;
155
- // Empty `media_buy_ids` is filtered above as paginated-mode (no truncation
178
+ // Empty `<id_field>` is filtered above as paginated-mode (no truncation
156
179
  // possible without a request to compare against).
157
- logger.warn(`[adcp/sdk] ${toolName}: platform returned ${returned} row${returned === 1 ? '' : 's'} for ${requestedIds.length} requested media_buy_ids — ` +
158
- `the platform may be silently truncating to media_buy_ids[0]. ` +
180
+ logger.warn(`[adcp/sdk] ${toolName}: platform returned ${returned} row${returned === 1 ? '' : 's'} for ${requestedIds.length} requested ${idFieldName} — ` +
181
+ `the platform may be silently truncating to ${idFieldName}[0]. ` +
159
182
  `See https://github.com/adcontextprotocol/adcp-client/issues/1342 for the multi-id pass-through contract. ` +
160
183
  `Suppress with ADCP_SUPPRESS_MULTI_ID_WARN=1 if legitimate misses (deleted / cross-account) are routine.`);
161
184
  }
@@ -515,14 +538,15 @@ function createAdcpServerFromPlatform(platform, opts) {
515
538
  let resolved = false;
516
539
  let resolvedAccountId;
517
540
  try {
518
- // Enforce the JSDoc contract documented at
519
- // `AccountStore.resolution`: implicit-mode platforms refuse inline
520
- // `account_id` references buyers call sync_accounts first, then
521
- // the framework resolves accounts from the auth principal on
522
- // subsequent calls. The brand+operator union arm is permitted
523
- // (used during the initial sync_accounts onboarding flow); only
524
- // the `{ account_id }` arm is refused. Closes adcp-client#1364.
525
- refuseImplicitAccountId(platform.accounts.resolution, ref);
541
+ // Enforce the JSDoc contract documented at `AccountStore.resolution`:
542
+ // implicit-mode and derived-mode platforms refuse inline `account_id`
543
+ // references. Implicit: buyers call sync_accounts first, then the
544
+ // framework resolves from the auth principal. Derived: single-tenant;
545
+ // the principal alone identifies the tenant. The brand+operator union
546
+ // arm is permitted (implicit's sync_accounts onboarding flow); only
547
+ // the `{ account_id }` arm is refused. Closes adcp-client#1364
548
+ // (implicit) and adcp-client#1468 (derived).
549
+ refuseInlineAccountIdWhenForbidden(platform.accounts.resolution, ref);
526
550
  const account = await platform.accounts.resolve(ref, toResolveCtx(ctx, ctx.toolName));
527
551
  resolved = account != null;
528
552
  resolvedAccountId = account?.id;
@@ -600,6 +624,7 @@ function createAdcpServerFromPlatform(platform, opts) {
600
624
  }, ctxFor), 'creative', mergeOpts),
601
625
  eventTracking: mergeHandlers(opts.eventTracking, buildEventTrackingHandlers(platform, ctxFor), 'eventTracking', mergeOpts),
602
626
  signals: mergeHandlers(opts.signals, buildSignalsHandlers(platform, ctxFor, effectiveCtxMetadata, fwLogger), 'signals', mergeOpts),
627
+ sponsoredIntelligence: mergeHandlers(opts.sponsoredIntelligence, buildSponsoredIntelligenceHandlers(platform, ctxFor, effectiveCtxMetadata, fwLogger), 'sponsoredIntelligence', mergeOpts),
603
628
  governance: mergeHandlers(opts.governance, buildGovernanceHandlers(platform, ctxFor), 'governance', mergeOpts),
604
629
  accounts: mergeHandlers(opts.accounts, buildAccountHandlers(platform, ctxFor), 'accounts', mergeOpts),
605
630
  brandRights: mergeHandlers(opts.brandRights, buildBrandRightsHandlers(platform, ctxFor, effectiveCtxMetadata, fwLogger), 'brandRights', mergeOpts),
@@ -611,10 +636,13 @@ function createAdcpServerFromPlatform(platform, opts) {
611
636
  const server = (0, create_adcp_server_1.createAdcpServer)(config);
612
637
  // Wire `comply_test_controller` if the adopter supplied adapters.
613
638
  // `createComplyController` builds the tool definition + handler + raw
614
- // dispatch; `register(server)` calls server.registerTool. Sandbox
615
- // gating is the adopter's job (per-request via complyTest.sandboxGate
616
- // or environment-level by guarding the createAdcpServerFromPlatform
617
- // call site itself).
639
+ // dispatch. The framework registers the tool itself (bypassing
640
+ // `controller.register(server)`) so the sandbox-authority gate can
641
+ // resolve the calling account through `platform.accounts.resolve`
642
+ // BEFORE dispatching — under no circumstances should the controller
643
+ // operate on a `live`-mode account, regardless of what the caller
644
+ // claims on the wire. See `docs/proposals/lifecycle-state-and-sandbox-authority.md`
645
+ // for the full three-mode design and #1435 phase 2.
618
646
  if (opts.complyTest != null) {
619
647
  let complyConfig = opts.complyTest;
620
648
  if (autoSeedStore != null) {
@@ -682,7 +710,131 @@ function createAdcpServerFromPlatform(platform, opts) {
682
710
  complyConfig = { ...opts.complyTest, seed: autoSeed };
683
711
  }
684
712
  const controller = (0, comply_controller_1.createComplyController)(complyConfig);
685
- controller.register(server);
713
+ // Manual registration with framework-side sandbox-authority gate. See
714
+ // top-of-block rationale and `docs/proposals/lifecycle-state-and-sandbox-authority.md`.
715
+ //
716
+ // Trust boundary: the gate consults the *resolved* account from
717
+ // `platform.accounts.resolve`, NOT a buyer-supplied claim like
718
+ // `account.sandbox === true` on the wire. The resolver is the only
719
+ // thing that names the account's mode; the gate refuses dispatch when
720
+ // mode is `live` (or the resolver fails to produce an account, modulo
721
+ // the env / context fallbacks below).
722
+ //
723
+ // Fallback paths (deprecated):
724
+ // - `context.sandbox === true` admits when no account resolved. Useful
725
+ // during the migration window for adopters whose wire shape carries
726
+ // sandbox routing in `context` but whose resolver isn't yet returning
727
+ // `mode: 'sandbox'`.
728
+ // - `process.env.ADCP_SANDBOX === '1'` admits unconditionally — the
729
+ // historical pattern. KEPT for back-compat so existing test platforms
730
+ // don't break on upgrade. Fails closed if the same process has ever
731
+ // resolved an explicit `mode: 'live'` account from the resolver: that
732
+ // pairing is a misconfiguration (env var should be unset on prod) and
733
+ // leaving it open re-exposes the live principal we just gated against.
734
+ //
735
+ // `list_scenarios` is exempt — it's the discovery probe used by buyer
736
+ // tooling to distinguish "controller wired but locked" from "controller
737
+ // missing entirely". Read-only and reveals nothing beyond which scenarios
738
+ // the adopter advertised in capabilities.
739
+ const mcp = (0, adcp_server_1.getSdkServer)(server);
740
+ if (mcp == null) {
741
+ // Non-MCP server — fall back to the controller's own registration so
742
+ // adopters wiring a custom transport keep the v5 behavior. The gate is
743
+ // an MCP-side concern; A2A and other transports are wired separately.
744
+ controller.register(server);
745
+ }
746
+ else {
747
+ // Permit a top-level `account` field on the wire so the gate can read
748
+ // the buyer's account ref. The canonical AdCP shape strips it (account
749
+ // routes through `context.account`), but adopters' storyboard fixtures
750
+ // commonly send it at the top level — `TOOL_INPUT_SHAPE`'s JSDoc in
751
+ // `src/lib/server/test-controller.ts` documents this extension as the
752
+ // supported escape hatch.
753
+ //
754
+ // Schema is the full canonical `AccountReference` per
755
+ // `schemas/cache/3.0.5/core/account-ref.json`: oneOf
756
+ // { account_id } — explicit accounts
757
+ // { brand, operator, sandbox? } — implicit accounts
758
+ // Modeled here as a single object with all four fields optional so
759
+ // either arm passes; resolvers narrow on the shape at dispatch.
760
+ // `brand` content is `unknown()` because the inner `brand-ref.json`
761
+ // shape is itself a oneOf and resolvers (not the gate) validate it.
762
+ // Top-level `.strict()` still blocks unknown keys at the framework
763
+ // boundary so a buyer can't stuff `__proto__` / arbitrary payloads
764
+ // into adopters' resolvers.
765
+ const gatedInputSchema = {
766
+ ...controller.toolDefinition.inputSchema,
767
+ account: zod_1.z
768
+ .object({
769
+ account_id: zod_1.z.string().min(1).optional(),
770
+ brand: zod_1.z.unknown().optional(),
771
+ operator: zod_1.z.string().optional(),
772
+ sandbox: zod_1.z.boolean().optional(),
773
+ })
774
+ .strict()
775
+ .optional(),
776
+ };
777
+ mcp.registerTool(controller.toolDefinition.name, {
778
+ description: controller.toolDefinition.description,
779
+ inputSchema: gatedInputSchema,
780
+ }, (async (input, extra) => {
781
+ // Probe exempt — capability discovery, no state mutation.
782
+ if (input.scenario === 'list_scenarios') {
783
+ return controller.handle(input);
784
+ }
785
+ // Read account ref from top-level (extended shape) or from
786
+ // `context.account` (canonical AdCP routing). First non-null wins.
787
+ const refFromTop = input.account;
788
+ const refFromContext = input.context?.account;
789
+ const accountRef = refFromTop ?? refFromContext;
790
+ let resolvedAccount = null;
791
+ try {
792
+ resolvedAccount = await platform.accounts.resolve(accountRef, {
793
+ ...(extra?.authInfo !== undefined && { authInfo: extra.authInfo }),
794
+ toolName: 'comply_test_controller',
795
+ });
796
+ }
797
+ catch {
798
+ // Resolver failures fall through to the wire-ref / env fallbacks.
799
+ // Treat as "no account resolved" — fail-closed by default unless a
800
+ // fallback admits.
801
+ }
802
+ // Record the resolved account's explicit mode (if any). Used by the
803
+ // env-fallback fail-closed guard below.
804
+ (0, observed_modes_1.recordResolvedAccountMode)(resolvedAccount);
805
+ const accountIsSandbox = resolvedAccount != null && (0, account_mode_1.isSandboxOrMockAccount)(resolvedAccount);
806
+ // Spec-defined fallback for the unresolved-account path: read
807
+ // `sandbox: true` off the wire `AccountReference` (per
808
+ // `core/account-ref.json`). Only consulted when the resolver
809
+ // returned `null` — if the resolver names the account, the
810
+ // resolver wins. The buyer's wire claim never overrides a
811
+ // resolved live account.
812
+ const refSandbox = accountRef?.sandbox === true;
813
+ const envSandbox = process.env.ADCP_SANDBOX === '1';
814
+ const wouldAdmitOnlyViaEnv = envSandbox && !accountIsSandbox && !(resolvedAccount == null && refSandbox);
815
+ // Fail-closed guard on the env fallback. If the env var is the only
816
+ // signal that would admit AND this process has ever resolved an
817
+ // explicit `mode: 'live'` account from the resolver, the env is
818
+ // misconfigured. Refuse loudly so operators notice, instead of
819
+ // silently downgrading the gate for live principals.
820
+ if (wouldAdmitOnlyViaEnv && (0, observed_modes_1.hasObservedLiveMode)()) {
821
+ throw new Error('comply_test_controller: ADCP_SANDBOX=1 is set but this process has resolved at least one ' +
822
+ 'live-mode account from platform.accounts.resolve. Remove ADCP_SANDBOX from your prod ' +
823
+ 'environment; gate the controller via mode: "sandbox" on resolved sandbox accounts instead. ' +
824
+ 'See docs/proposals/lifecycle-state-and-sandbox-authority.md.');
825
+ }
826
+ const allowed = accountIsSandbox || (resolvedAccount == null && refSandbox) || envSandbox;
827
+ if (!allowed) {
828
+ return (0, test_controller_1.toMcpResponse)({
829
+ success: false,
830
+ error: 'FORBIDDEN',
831
+ error_detail: 'comply_test_controller requires a sandbox or mock account; ' +
832
+ 'resolved account is in live mode (or no account resolved).',
833
+ });
834
+ }
835
+ return controller.handle(input);
836
+ }));
837
+ }
686
838
  }
687
839
  return Object.assign(server, {
688
840
  getTaskState: async (taskId, expectedAccountId) => {
@@ -822,7 +974,7 @@ function buildTasksGetTool(platform, taskRegistry, agentRegistry, logger) {
822
974
  };
823
975
  let resolvedAccountId;
824
976
  if (ref) {
825
- refuseImplicitAccountId(platform.accounts.resolution, ref);
977
+ refuseInlineAccountIdWhenForbidden(platform.accounts.resolution, ref);
826
978
  try {
827
979
  const resolved = await platform.accounts.resolve(ref, resolveCtx);
828
980
  if (resolved)
@@ -1907,6 +2059,10 @@ exports.ENTITY_TO_RESOURCE_KIND = {
1907
2059
  collection_list: 'collection_list',
1908
2060
  account: 'account',
1909
2061
  product: 'product',
2062
+ // SI session lifecycle, hydrated on `si_send_message` /
2063
+ // `si_terminate_session` from the entry the framework auto-stores after
2064
+ // `si_initiate_session`. See `buildSponsoredIntelligenceHandlers`.
2065
+ si_session: 'si_session',
1910
2066
  };
1911
2067
  /**
1912
2068
  * `x-entity` values the SDK does NOT hydrate today — graceful skip rather
@@ -1928,8 +2084,7 @@ exports.INTENTIONALLY_UNHYDRATED_ENTITIES = new Set([
1928
2084
  'governance_plan', // No SDK ResourceKind yet; campaign-governance follow-up.
1929
2085
  'governance_check', // Transient request envelope; not stored.
1930
2086
  'event_source', // No SDK ResourceKind yet.
1931
- 'si_session', // Session lifecycle owned by `SponsoredIntelligencePlatform`.
1932
- 'offering', // SI offering catalog; not stored as ctx-metadata.
2087
+ 'offering', // SI offering catalog; correlation handled by brand-side `offering_token`, not framework hydration.
1933
2088
  'rights_holder_brand', // Read-through `get_brand_identity`; not separately stored.
1934
2089
  'advertiser_brand', // Same as above.
1935
2090
  ]);
@@ -2354,7 +2509,7 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2354
2509
  const reqCtx = ctxFor(ctx);
2355
2510
  return projectSync(async () => {
2356
2511
  const result = await sales.getMediaBuyDelivery(params, reqCtx);
2357
- warnIfTruncatedMultiIdResponse('getMediaBuyDelivery', params.media_buy_ids, result?.media_buy_deliveries, logger);
2512
+ warnIfTruncatedMultiIdResponse('getMediaBuyDelivery', 'media_buy_ids', params.media_buy_ids, result?.media_buy_deliveries, logger);
2358
2513
  return result;
2359
2514
  }, actuals => actuals);
2360
2515
  },
@@ -2377,7 +2532,7 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2377
2532
  const reqCtx = ctxFor(ctx);
2378
2533
  return projectSync(async () => {
2379
2534
  const result = await sales.getMediaBuys(params, reqCtx);
2380
- warnIfTruncatedMultiIdResponse('getMediaBuys', params.media_buy_ids, result?.media_buys, logger);
2535
+ warnIfTruncatedMultiIdResponse('getMediaBuys', 'media_buy_ids', params.media_buy_ids, result?.media_buys, logger);
2381
2536
  await autoStoreResources(ctxMetadataStore, reqCtx.account?.id, 'media_buy', result?.media_buys, 'media_buy_id', logger);
2382
2537
  await backfillTargetingOverlay(mediaBuyStore, reqCtx.account?.id, result, logger);
2383
2538
  return result;
@@ -2408,7 +2563,11 @@ function buildMediaBuyHandlers(platform, taskRegistry, taskWebhookEmit, observab
2408
2563
  ...(sales.listCreatives && {
2409
2564
  listCreatives: async (params, ctx) => {
2410
2565
  const reqCtx = ctxFor(ctx);
2411
- return projectSync(() => sales.listCreatives(params, reqCtx), r => r);
2566
+ return projectSync(async () => {
2567
+ const result = await sales.listCreatives(params, reqCtx);
2568
+ warnIfTruncatedMultiIdResponse('listCreatives', 'creative_ids', params.creative_ids, result?.creatives, logger);
2569
+ return result;
2570
+ }, r => r);
2412
2571
  },
2413
2572
  }),
2414
2573
  };
@@ -2511,7 +2670,11 @@ function buildCreativeHandlers(platform, taskRegistry, taskWebhookEmit, observab
2511
2670
  });
2512
2671
  }
2513
2672
  const reqCtx = ctxFor(ctx);
2514
- return projectSync(() => creative.listCreatives(params, reqCtx), r => r);
2673
+ return projectSync(async () => {
2674
+ const result = await creative.listCreatives(params, reqCtx);
2675
+ warnIfTruncatedMultiIdResponse('listCreatives', 'creative_ids', params.creative_ids, result?.creatives, logger);
2676
+ return result;
2677
+ }, r => r);
2515
2678
  },
2516
2679
  getCreativeDelivery: async (params, ctx) => {
2517
2680
  if (!('getCreativeDelivery' in creative)) {
@@ -2520,7 +2683,11 @@ function buildCreativeHandlers(platform, taskRegistry, taskWebhookEmit, observab
2520
2683
  });
2521
2684
  }
2522
2685
  const reqCtx = ctxFor(ctx);
2523
- return projectSync(() => creative.getCreativeDelivery(params, reqCtx), r => r);
2686
+ return projectSync(async () => {
2687
+ const result = await creative.getCreativeDelivery(params, reqCtx);
2688
+ warnIfTruncatedMultiIdResponse('getCreativeDelivery', 'creative_ids', params.creative_ids, result?.creative_deliveries, logger);
2689
+ return result;
2690
+ }, r => r);
2524
2691
  },
2525
2692
  };
2526
2693
  }
@@ -2571,6 +2738,10 @@ function buildSignalsHandlers(platform, ctxFor, ctxMetadataStore, logger) {
2571
2738
  const reqCtx = ctxFor(ctx);
2572
2739
  return projectSync(async () => {
2573
2740
  const result = await signals.getSignals(params, reqCtx);
2741
+ // signal_ids is `SignalID[]` (`{source, data_provider_domain, id}`
2742
+ // objects), not bare strings — but the helper's truncation-detection
2743
+ // is purely length-based, so the object element type is irrelevant.
2744
+ warnIfTruncatedMultiIdResponse('getSignals', 'signal_ids', params.signal_ids, result?.signals, logger);
2574
2745
  // Auto-store signals so subsequent activate_signal can hydrate
2575
2746
  // `req.signal` from the publisher's prior catalog entry.
2576
2747
  await autoStoreResources(ctxMetadataStore, reqCtx.account?.id, 'signal', result?.signals, 'signal_agent_segment_id', logger);
@@ -2590,6 +2761,125 @@ function buildSignalsHandlers(platform, ctxFor, ctxMetadataStore, logger) {
2590
2761
  },
2591
2762
  };
2592
2763
  }
2764
+ /**
2765
+ * Adapt `SponsoredIntelligencePlatform` (v6 protocol-keyed shape) onto the v5
2766
+ * `SponsoredIntelligenceHandlers` handler-bag the dispatcher consumes.
2767
+ *
2768
+ * Auto-store on `initiateSession`: stash a session record keyed by
2769
+ * `session_id` under `ResourceKind: 'si_session'`. The framework's
2770
+ * schema-driven hydrator (TOOL_ENTITY_FIELDS for `si_send_message` /
2771
+ * `si_terminate_session`) attaches that record at `params.session` on
2772
+ * subsequent calls so the platform reads transcript context from
2773
+ * `req.session` without manual `ctx.store.get`.
2774
+ *
2775
+ * Hydrate on `sendMessage` / `terminateSession`: schema-driven
2776
+ * `hydrateForTool` reads `params.session_id`, looks up the stored
2777
+ * session, and attaches at `params.session`.
2778
+ */
2779
+ function buildSponsoredIntelligenceHandlers(platform, ctxFor, ctxMetadataStore, logger) {
2780
+ const si = platform.sponsoredIntelligence;
2781
+ if (!si)
2782
+ return undefined;
2783
+ return {
2784
+ getOffering: async (params, ctx) => {
2785
+ const reqCtx = ctxFor(ctx);
2786
+ return projectSync(() => si.getOffering(params, reqCtx), r => r);
2787
+ },
2788
+ initiateSession: async (params, ctx) => {
2789
+ const reqCtx = ctxFor(ctx);
2790
+ return projectSync(async () => {
2791
+ const result = await si.initiateSession(params, reqCtx);
2792
+ // Auto-store the session record so subsequent `sendMessage` /
2793
+ // `terminateSession` calls hydrate `req.session` without a
2794
+ // manual lookup. Stored payload preserves the bits the brand
2795
+ // engine needs to resume context across turns: identity
2796
+ // consent, offering scoping, negotiated capabilities,
2797
+ // placement provenance, and any media-buy linkage. The full
2798
+ // request intent is stored alongside so brand handlers can
2799
+ // re-read what the user originally asked for. Production
2800
+ // brand engines that own transcript state in their own
2801
+ // backend can ignore this and read from their own store —
2802
+ // see SponsoredIntelligencePlatform JSDoc.
2803
+ if (ctxMetadataStore && reqCtx.account?.id) {
2804
+ const sessionId = result?.session_id;
2805
+ if (typeof sessionId === 'string' && sessionId.length > 0) {
2806
+ const reqRec = params;
2807
+ const resRec = result;
2808
+ const sessionRecord = {
2809
+ session_id: sessionId,
2810
+ // Request-side scoping the brand engine needs across turns.
2811
+ intent: reqRec.intent,
2812
+ offering_id: reqRec.offering_id,
2813
+ offering_token: reqRec.offering_token,
2814
+ placement: reqRec.placement,
2815
+ media_buy_id: reqRec.media_buy_id,
2816
+ identity: reqRec.identity,
2817
+ supported_capabilities: reqRec.supported_capabilities,
2818
+ // Response-side state.
2819
+ negotiated_capabilities: resRec.negotiated_capabilities,
2820
+ session_status: resRec.session_status,
2821
+ session_ttl_seconds: resRec.session_ttl_seconds,
2822
+ };
2823
+ try {
2824
+ await ctxMetadataStore.setResource(reqCtx.account.id, 'si_session', sessionId, sessionRecord);
2825
+ }
2826
+ catch (err) {
2827
+ const msg = err instanceof Error ? err.message : String(err);
2828
+ logger.warn(`[adcp/decisioning] auto-store si_session ${sessionId} failed: ${msg}`);
2829
+ }
2830
+ }
2831
+ }
2832
+ return result;
2833
+ }, r => r);
2834
+ },
2835
+ sendMessage: async (params, ctx) => {
2836
+ const reqCtx = ctxFor(ctx);
2837
+ // Auto-hydrate `req.session` from the stored si_session record. The
2838
+ // schema-driven hydrator reads `params.session_id` and attaches the
2839
+ // stored session at `params.session` (via the `_id` → strip
2840
+ // convention; `session_id` → `session`).
2841
+ await hydrateForTool(ctxMetadataStore, reqCtx.account?.id, 'si_send_message', params, logger);
2842
+ return projectSync(() => si.sendMessage(params, reqCtx), r => r);
2843
+ },
2844
+ terminateSession: async (params, ctx) => {
2845
+ const reqCtx = ctxFor(ctx);
2846
+ await hydrateForTool(ctxMetadataStore, reqCtx.account?.id, 'si_terminate_session', params, logger);
2847
+ return projectSync(async () => {
2848
+ const result = await si.terminateSession(params, reqCtx);
2849
+ // Persist `acp_handoff` and terminal `session_status` onto the
2850
+ // stored session record so a re-terminate replay (which is
2851
+ // naturally idempotent on `session_id` per the spec) hydrates
2852
+ // the same handoff payload via `req.session` without the
2853
+ // adopter having to remember to write through. Honors the
2854
+ // platform interface JSDoc contract:
2855
+ // "Re-terminating a closed session MUST return the same
2856
+ // payload, including any acp_handoff block."
2857
+ if (ctxMetadataStore && reqCtx.account?.id) {
2858
+ const sessionId = params?.session_id;
2859
+ if (typeof sessionId === 'string' && sessionId.length > 0) {
2860
+ const resRec = result;
2861
+ try {
2862
+ const existing = await ctxMetadataStore.getEntry(reqCtx.account.id, 'si_session', sessionId);
2863
+ const merged = {
2864
+ ...(existing?.resource ?? { session_id: sessionId }),
2865
+ session_status: resRec.session_status,
2866
+ acp_handoff: resRec.acp_handoff,
2867
+ follow_up: resRec.follow_up,
2868
+ terminated: resRec.terminated,
2869
+ };
2870
+ await ctxMetadataStore.setResource(reqCtx.account.id, 'si_session', sessionId, merged);
2871
+ }
2872
+ catch (err) {
2873
+ const msg = err instanceof Error ? err.message : String(err);
2874
+ logger.warn(`[adcp/decisioning] auto-store si_session ${sessionId} on terminate failed: ${msg}`);
2875
+ }
2876
+ }
2877
+ }
2878
+ return result;
2879
+ }, r => r);
2880
+ },
2881
+ };
2882
+ }
2593
2883
  function buildBrandRightsHandlers(platform, ctxFor, ctxMetadataStore, logger) {
2594
2884
  const br = platform.brandRights;
2595
2885
  if (!br)
@@ -2807,7 +3097,7 @@ function buildAccountHandlers(platform, ctxFor) {
2807
3097
  // tokens / upstream IDs off `ctx.account.ctx_metadata` without
2808
3098
  // having to re-resolve from `params.account`.
2809
3099
  const resolveCtx = toResolveCtx(ctx, 'get_account_financials');
2810
- refuseImplicitAccountId(accounts.resolution, params.account);
3100
+ refuseInlineAccountIdWhenForbidden(accounts.resolution, params.account);
2811
3101
  const resolved = await accounts.resolve(params.account, resolveCtx);
2812
3102
  if (!resolved) {
2813
3103
  throw new async_outcome_1.AdcpError('ACCOUNT_NOT_FOUND', {