@adcp/sdk 5.25.1 → 6.1.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.
- package/README.md +45 -7
- package/dist/lib/compliance-fixtures/index.d.ts +1 -1
- package/dist/lib/compliance-fixtures/index.js +1 -1
- package/dist/lib/core/AgentClient.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.js +27 -0
- package/dist/lib/core/SingleAgentClient.js.map +1 -1
- package/dist/lib/core/TaskExecutor.d.ts +21 -0
- package/dist/lib/core/TaskExecutor.d.ts.map +1 -1
- package/dist/lib/core/TaskExecutor.js +25 -2
- package/dist/lib/core/TaskExecutor.js.map +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +7 -8
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/schemas/index.d.ts +1 -1
- package/dist/lib/schemas/index.js +1 -1
- package/dist/lib/schemas-data/v2.5/_provenance.json +8 -0
- package/dist/lib/schemas-data/v2.5/adagents.json +494 -0
- package/dist/lib/schemas-data/v2.5/core/activation-key.json +50 -0
- package/dist/lib/schemas-data/v2.5/core/assets/audio-asset.json +32 -0
- package/dist/lib/schemas-data/v2.5/core/assets/css-asset.json +21 -0
- package/dist/lib/schemas-data/v2.5/core/assets/daast-asset.json +87 -0
- package/dist/lib/schemas-data/v2.5/core/assets/html-asset.json +21 -0
- package/dist/lib/schemas-data/v2.5/core/assets/image-asset.json +38 -0
- package/dist/lib/schemas-data/v2.5/core/assets/javascript-asset.json +21 -0
- package/dist/lib/schemas-data/v2.5/core/assets/markdown-asset.json +31 -0
- package/dist/lib/schemas-data/v2.5/core/assets/text-asset.json +21 -0
- package/dist/lib/schemas-data/v2.5/core/assets/url-asset.json +26 -0
- package/dist/lib/schemas-data/v2.5/core/assets/vast-asset.json +87 -0
- package/dist/lib/schemas-data/v2.5/core/assets/video-asset.json +44 -0
- package/dist/lib/schemas-data/v2.5/core/assets/webhook-asset.json +71 -0
- package/dist/lib/schemas-data/v2.5/core/async-response-data.json +88 -0
- package/dist/lib/schemas-data/v2.5/core/brand-manifest-ref.json +33 -0
- package/dist/lib/schemas-data/v2.5/core/brand-manifest.json +409 -0
- package/dist/lib/schemas-data/v2.5/core/context.json +8 -0
- package/dist/lib/schemas-data/v2.5/core/creative-asset.json +125 -0
- package/dist/lib/schemas-data/v2.5/core/creative-assignment.json +31 -0
- package/dist/lib/schemas-data/v2.5/core/creative-filters.json +111 -0
- package/dist/lib/schemas-data/v2.5/core/creative-manifest.json +72 -0
- package/dist/lib/schemas-data/v2.5/core/creative-policy.json +27 -0
- package/dist/lib/schemas-data/v2.5/core/delivery-metrics.json +171 -0
- package/dist/lib/schemas-data/v2.5/core/deployment.json +93 -0
- package/dist/lib/schemas-data/v2.5/core/destination.json +53 -0
- package/dist/lib/schemas-data/v2.5/core/error.json +40 -0
- package/dist/lib/schemas-data/v2.5/core/ext.json +8 -0
- package/dist/lib/schemas-data/v2.5/core/format-id.json +47 -0
- package/dist/lib/schemas-data/v2.5/core/format.json +324 -0
- package/dist/lib/schemas-data/v2.5/core/frequency-cap.json +18 -0
- package/dist/lib/schemas-data/v2.5/core/mcp-webhook-payload.json +152 -0
- package/dist/lib/schemas-data/v2.5/core/measurement.json +48 -0
- package/dist/lib/schemas-data/v2.5/core/media-buy.json +62 -0
- package/dist/lib/schemas-data/v2.5/core/package.json +72 -0
- package/dist/lib/schemas-data/v2.5/core/performance-feedback.json +90 -0
- package/dist/lib/schemas-data/v2.5/core/placement.json +34 -0
- package/dist/lib/schemas-data/v2.5/core/pricing-option.json +35 -0
- package/dist/lib/schemas-data/v2.5/core/product-filters.json +102 -0
- package/dist/lib/schemas-data/v2.5/core/product.json +153 -0
- package/dist/lib/schemas-data/v2.5/core/promoted-offerings.json +115 -0
- package/dist/lib/schemas-data/v2.5/core/promoted-products.json +67 -0
- package/dist/lib/schemas-data/v2.5/core/property-id.json +14 -0
- package/dist/lib/schemas-data/v2.5/core/property-tag.json +16 -0
- package/dist/lib/schemas-data/v2.5/core/property.json +62 -0
- package/dist/lib/schemas-data/v2.5/core/protocol-envelope.json +146 -0
- package/dist/lib/schemas-data/v2.5/core/publisher-property-selector.json +92 -0
- package/dist/lib/schemas-data/v2.5/core/push-notification-config.json +48 -0
- package/dist/lib/schemas-data/v2.5/core/reporting-capabilities.json +71 -0
- package/dist/lib/schemas-data/v2.5/core/response.json +24 -0
- package/dist/lib/schemas-data/v2.5/core/signal-filters.json +35 -0
- package/dist/lib/schemas-data/v2.5/core/start-timing.json +18 -0
- package/dist/lib/schemas-data/v2.5/core/sub-asset.json +79 -0
- package/dist/lib/schemas-data/v2.5/core/targeting.json +50 -0
- package/dist/lib/schemas-data/v2.5/core/tasks-get-request.json +43 -0
- package/dist/lib/schemas-data/v2.5/core/tasks-get-response.json +166 -0
- package/dist/lib/schemas-data/v2.5/core/tasks-list-request.json +192 -0
- package/dist/lib/schemas-data/v2.5/core/tasks-list-response.json +183 -0
- package/dist/lib/schemas-data/v2.5/creative/asset-types/index.json +101 -0
- package/dist/lib/schemas-data/v2.5/creative/list-creative-formats-request.json +73 -0
- package/dist/lib/schemas-data/v2.5/creative/list-creative-formats-response.json +61 -0
- package/dist/lib/schemas-data/v2.5/creative/preview-creative-request.json +164 -0
- package/dist/lib/schemas-data/v2.5/creative/preview-creative-response.json +245 -0
- package/dist/lib/schemas-data/v2.5/creative/preview-render.json +225 -0
- package/dist/lib/schemas-data/v2.5/enums/adcp-domain.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/asset-content-type.json +22 -0
- package/dist/lib/schemas-data/v2.5/enums/auth-scheme.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/available-metric.json +18 -0
- package/dist/lib/schemas-data/v2.5/enums/channels.json +18 -0
- package/dist/lib/schemas-data/v2.5/enums/co-branding-requirement.json +12 -0
- package/dist/lib/schemas-data/v2.5/enums/creative-action.json +14 -0
- package/dist/lib/schemas-data/v2.5/enums/creative-agent-capability.json +13 -0
- package/dist/lib/schemas-data/v2.5/enums/creative-sort-field.json +15 -0
- package/dist/lib/schemas-data/v2.5/enums/creative-status.json +19 -0
- package/dist/lib/schemas-data/v2.5/enums/daast-tracking-event.json +20 -0
- package/dist/lib/schemas-data/v2.5/enums/daast-version.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/delivery-type.json +15 -0
- package/dist/lib/schemas-data/v2.5/enums/dimension-unit.json +13 -0
- package/dist/lib/schemas-data/v2.5/enums/feed-format.json +12 -0
- package/dist/lib/schemas-data/v2.5/enums/feedback-source.json +13 -0
- package/dist/lib/schemas-data/v2.5/enums/format-category.json +16 -0
- package/dist/lib/schemas-data/v2.5/enums/format-id-parameter.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/frequency-cap-scope.json +13 -0
- package/dist/lib/schemas-data/v2.5/enums/history-entry-type.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/http-method.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/identifier-types.json +34 -0
- package/dist/lib/schemas-data/v2.5/enums/javascript-module-type.json +12 -0
- package/dist/lib/schemas-data/v2.5/enums/landing-page-requirement.json +12 -0
- package/dist/lib/schemas-data/v2.5/enums/markdown-flavor.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/media-buy-status.json +19 -0
- package/dist/lib/schemas-data/v2.5/enums/metric-type.json +17 -0
- package/dist/lib/schemas-data/v2.5/enums/notification-type.json +13 -0
- package/dist/lib/schemas-data/v2.5/enums/pacing.json +17 -0
- package/dist/lib/schemas-data/v2.5/enums/preview-output-format.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/pricing-model.json +25 -0
- package/dist/lib/schemas-data/v2.5/enums/property-type.json +16 -0
- package/dist/lib/schemas-data/v2.5/enums/publisher-identifier-types.json +19 -0
- package/dist/lib/schemas-data/v2.5/enums/reporting-frequency.json +12 -0
- package/dist/lib/schemas-data/v2.5/enums/signal-catalog-type.json +12 -0
- package/dist/lib/schemas-data/v2.5/enums/sort-direction.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/standard-format-ids.json +103 -0
- package/dist/lib/schemas-data/v2.5/enums/task-status.json +29 -0
- package/dist/lib/schemas-data/v2.5/enums/task-type.json +27 -0
- package/dist/lib/schemas-data/v2.5/enums/update-frequency.json +13 -0
- package/dist/lib/schemas-data/v2.5/enums/url-asset-type.json +12 -0
- package/dist/lib/schemas-data/v2.5/enums/validation-mode.json +11 -0
- package/dist/lib/schemas-data/v2.5/enums/vast-tracking-event.json +25 -0
- package/dist/lib/schemas-data/v2.5/enums/vast-version.json +14 -0
- package/dist/lib/schemas-data/v2.5/enums/webhook-response-type.json +13 -0
- package/dist/lib/schemas-data/v2.5/enums/webhook-security-method.json +12 -0
- package/dist/lib/schemas-data/v2.5/index.json +604 -0
- package/dist/lib/schemas-data/v2.5/media-buy/build-creative-request.json +31 -0
- package/dist/lib/schemas-data/v2.5/media-buy/build-creative-response.json +65 -0
- package/dist/lib/schemas-data/v2.5/media-buy/create-media-buy-async-response-input-required.json +31 -0
- package/dist/lib/schemas-data/v2.5/media-buy/create-media-buy-async-response-submitted.json +16 -0
- package/dist/lib/schemas-data/v2.5/media-buy/create-media-buy-async-response-working.json +36 -0
- package/dist/lib/schemas-data/v2.5/media-buy/create-media-buy-request.json +126 -0
- package/dist/lib/schemas-data/v2.5/media-buy/create-media-buy-response.json +97 -0
- package/dist/lib/schemas-data/v2.5/media-buy/get-media-buy-delivery-request.json +54 -0
- package/dist/lib/schemas-data/v2.5/media-buy/get-media-buy-delivery-response.json +285 -0
- package/dist/lib/schemas-data/v2.5/media-buy/get-products-async-response-input-required.json +38 -0
- package/dist/lib/schemas-data/v2.5/media-buy/get-products-async-response-submitted.json +21 -0
- package/dist/lib/schemas-data/v2.5/media-buy/get-products-async-response-working.json +34 -0
- package/dist/lib/schemas-data/v2.5/media-buy/get-products-request.json +28 -0
- package/dist/lib/schemas-data/v2.5/media-buy/get-products-response.json +33 -0
- package/dist/lib/schemas-data/v2.5/media-buy/list-authorized-properties-request.json +26 -0
- package/dist/lib/schemas-data/v2.5/media-buy/list-authorized-properties-response.json +70 -0
- package/dist/lib/schemas-data/v2.5/media-buy/list-creative-formats-request.json +58 -0
- package/dist/lib/schemas-data/v2.5/media-buy/list-creative-formats-response.json +61 -0
- package/dist/lib/schemas-data/v2.5/media-buy/list-creatives-request.json +137 -0
- package/dist/lib/schemas-data/v2.5/media-buy/list-creatives-response.json +437 -0
- package/dist/lib/schemas-data/v2.5/media-buy/package-request.json +80 -0
- package/dist/lib/schemas-data/v2.5/media-buy/provide-performance-feedback-request.json +88 -0
- package/dist/lib/schemas-data/v2.5/media-buy/provide-performance-feedback-response.json +66 -0
- package/dist/lib/schemas-data/v2.5/media-buy/sync-creatives-async-response-input-required.json +25 -0
- package/dist/lib/schemas-data/v2.5/media-buy/sync-creatives-async-response-submitted.json +16 -0
- package/dist/lib/schemas-data/v2.5/media-buy/sync-creatives-async-response-working.json +46 -0
- package/dist/lib/schemas-data/v2.5/media-buy/sync-creatives-request.json +178 -0
- package/dist/lib/schemas-data/v2.5/media-buy/sync-creatives-response.json +149 -0
- package/dist/lib/schemas-data/v2.5/media-buy/update-media-buy-async-response-input-required.json +24 -0
- package/dist/lib/schemas-data/v2.5/media-buy/update-media-buy-async-response-submitted.json +16 -0
- package/dist/lib/schemas-data/v2.5/media-buy/update-media-buy-async-response-working.json +36 -0
- package/dist/lib/schemas-data/v2.5/media-buy/update-media-buy-request.json +129 -0
- package/dist/lib/schemas-data/v2.5/media-buy/update-media-buy-response.json +99 -0
- package/dist/lib/schemas-data/v2.5/pricing-options/cpc-option.json +52 -0
- package/dist/lib/schemas-data/v2.5/pricing-options/cpcv-option.json +52 -0
- package/dist/lib/schemas-data/v2.5/pricing-options/cpm-auction-option.json +81 -0
- package/dist/lib/schemas-data/v2.5/pricing-options/cpm-fixed-option.json +52 -0
- package/dist/lib/schemas-data/v2.5/pricing-options/cpp-option.json +73 -0
- package/dist/lib/schemas-data/v2.5/pricing-options/cpv-option.json +88 -0
- package/dist/lib/schemas-data/v2.5/pricing-options/flat-rate-option.json +93 -0
- package/dist/lib/schemas-data/v2.5/pricing-options/vcpm-auction-option.json +81 -0
- package/dist/lib/schemas-data/v2.5/pricing-options/vcpm-fixed-option.json +52 -0
- package/dist/lib/schemas-data/v2.5/protocols/adcp-extension.json +33 -0
- package/dist/lib/schemas-data/v2.5/signals/activate-signal-request.json +32 -0
- package/dist/lib/schemas-data/v2.5/signals/activate-signal-response.json +68 -0
- package/dist/lib/schemas-data/v2.5/signals/get-signals-request.json +59 -0
- package/dist/lib/schemas-data/v2.5/signals/get-signals-response.json +100 -0
- package/dist/lib/server/create-adcp-server.d.ts +129 -11
- package/dist/lib/server/create-adcp-server.d.ts.map +1 -1
- package/dist/lib/server/create-adcp-server.js +127 -2
- package/dist/lib/server/create-adcp-server.js.map +1 -1
- package/dist/lib/server/ctx-metadata/backends/memory.d.ts +27 -0
- package/dist/lib/server/ctx-metadata/backends/memory.d.ts.map +1 -0
- package/dist/lib/server/ctx-metadata/backends/memory.js +72 -0
- package/dist/lib/server/ctx-metadata/backends/memory.js.map +1 -0
- package/dist/lib/server/ctx-metadata/backends/pg.d.ts +62 -0
- package/dist/lib/server/ctx-metadata/backends/pg.d.ts.map +1 -0
- package/dist/lib/server/ctx-metadata/backends/pg.js +145 -0
- package/dist/lib/server/ctx-metadata/backends/pg.js.map +1 -0
- package/dist/lib/server/ctx-metadata/index.d.ts +15 -0
- package/dist/lib/server/ctx-metadata/index.d.ts.map +1 -0
- package/dist/lib/server/ctx-metadata/index.js +28 -0
- package/dist/lib/server/ctx-metadata/index.js.map +1 -0
- package/dist/lib/server/ctx-metadata/store.d.ts +177 -0
- package/dist/lib/server/ctx-metadata/store.d.ts.map +1 -0
- package/dist/lib/server/ctx-metadata/store.js +327 -0
- package/dist/lib/server/ctx-metadata/store.js.map +1 -0
- package/dist/lib/server/ctx-metadata/wire-shape.d.ts +55 -0
- package/dist/lib/server/ctx-metadata/wire-shape.d.ts.map +1 -0
- package/dist/lib/server/ctx-metadata/wire-shape.js +121 -0
- package/dist/lib/server/ctx-metadata/wire-shape.js.map +1 -0
- package/dist/lib/server/decisioning/account.d.ts +309 -0
- package/dist/lib/server/decisioning/account.d.ts.map +1 -0
- package/dist/lib/server/decisioning/account.js +102 -0
- package/dist/lib/server/decisioning/account.js.map +1 -0
- package/dist/lib/server/decisioning/admin-router.d.ts +75 -0
- package/dist/lib/server/decisioning/admin-router.d.ts.map +1 -0
- package/dist/lib/server/decisioning/admin-router.js +120 -0
- package/dist/lib/server/decisioning/admin-router.js.map +1 -0
- package/dist/lib/server/decisioning/assembly-helpers.d.ts +204 -0
- package/dist/lib/server/decisioning/assembly-helpers.d.ts.map +1 -0
- package/dist/lib/server/decisioning/assembly-helpers.js +173 -0
- package/dist/lib/server/decisioning/assembly-helpers.js.map +1 -0
- package/dist/lib/server/decisioning/async-outcome.d.ts +154 -0
- package/dist/lib/server/decisioning/async-outcome.d.ts.map +1 -0
- package/dist/lib/server/decisioning/async-outcome.js +239 -0
- package/dist/lib/server/decisioning/async-outcome.js.map +1 -0
- package/dist/lib/server/decisioning/capabilities.d.ts +251 -0
- package/dist/lib/server/decisioning/capabilities.d.ts.map +1 -0
- package/dist/lib/server/decisioning/capabilities.js +16 -0
- package/dist/lib/server/decisioning/capabilities.js.map +1 -0
- package/dist/lib/server/decisioning/context.d.ts +212 -0
- package/dist/lib/server/decisioning/context.d.ts.map +1 -0
- package/dist/lib/server/decisioning/context.js +26 -0
- package/dist/lib/server/decisioning/context.js.map +1 -0
- package/dist/lib/server/decisioning/errors-typed.d.ts +104 -0
- package/dist/lib/server/decisioning/errors-typed.d.ts.map +1 -0
- package/dist/lib/server/decisioning/errors-typed.js +304 -0
- package/dist/lib/server/decisioning/errors-typed.js.map +1 -0
- package/dist/lib/server/decisioning/helpers.d.ts +131 -0
- package/dist/lib/server/decisioning/helpers.d.ts.map +1 -0
- package/dist/lib/server/decisioning/helpers.js +134 -0
- package/dist/lib/server/decisioning/helpers.js.map +1 -0
- package/dist/lib/server/decisioning/index.d.ts +46 -0
- package/dist/lib/server/decisioning/index.d.ts.map +1 -0
- package/dist/lib/server/decisioning/index.js +120 -0
- package/dist/lib/server/decisioning/index.js.map +1 -0
- package/dist/lib/server/decisioning/list-helpers.d.ts +53 -0
- package/dist/lib/server/decisioning/list-helpers.d.ts.map +1 -0
- package/dist/lib/server/decisioning/list-helpers.js +96 -0
- package/dist/lib/server/decisioning/list-helpers.js.map +1 -0
- package/dist/lib/server/decisioning/manifest-helpers.d.ts +56 -0
- package/dist/lib/server/decisioning/manifest-helpers.d.ts.map +1 -0
- package/dist/lib/server/decisioning/manifest-helpers.js +78 -0
- package/dist/lib/server/decisioning/manifest-helpers.js.map +1 -0
- package/dist/lib/server/decisioning/pagination.d.ts +21 -0
- package/dist/lib/server/decisioning/pagination.d.ts.map +1 -0
- package/dist/lib/server/decisioning/pagination.js +12 -0
- package/dist/lib/server/decisioning/pagination.js.map +1 -0
- package/dist/lib/server/decisioning/platform.d.ts +188 -0
- package/dist/lib/server/decisioning/platform.d.ts.map +1 -0
- package/dist/lib/server/decisioning/platform.js +19 -0
- package/dist/lib/server/decisioning/platform.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/from-platform.d.ts +510 -0
- package/dist/lib/server/decisioning/runtime/from-platform.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/from-platform.js +2196 -0
- package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.ts +114 -0
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.js +247 -0
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/protocol-for-tool.d.ts +32 -0
- package/dist/lib/server/decisioning/runtime/protocol-for-tool.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/protocol-for-tool.js +127 -0
- package/dist/lib/server/decisioning/runtime/protocol-for-tool.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/task-registry.d.ts +105 -0
- package/dist/lib/server/decisioning/runtime/task-registry.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/task-registry.js +96 -0
- package/dist/lib/server/decisioning/runtime/task-registry.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/to-context.d.ts +54 -0
- package/dist/lib/server/decisioning/runtime/to-context.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/to-context.js +166 -0
- package/dist/lib/server/decisioning/runtime/to-context.js.map +1 -0
- package/dist/lib/server/decisioning/runtime/validate-platform.d.ts +20 -0
- package/dist/lib/server/decisioning/runtime/validate-platform.d.ts.map +1 -0
- package/dist/lib/server/decisioning/runtime/validate-platform.js +93 -0
- package/dist/lib/server/decisioning/runtime/validate-platform.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/audiences.d.ts +72 -0
- package/dist/lib/server/decisioning/specialisms/audiences.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/audiences.js +15 -0
- package/dist/lib/server/decisioning/specialisms/audiences.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/brand-rights.d.ts +92 -0
- package/dist/lib/server/decisioning/specialisms/brand-rights.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/brand-rights.js +28 -0
- package/dist/lib/server/decisioning/specialisms/brand-rights.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/campaign-governance.d.ts +67 -0
- package/dist/lib/server/decisioning/specialisms/campaign-governance.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/campaign-governance.js +31 -0
- package/dist/lib/server/decisioning/specialisms/campaign-governance.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/content-standards.d.ts +78 -0
- package/dist/lib/server/decisioning/specialisms/content-standards.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/content-standards.js +35 -0
- package/dist/lib/server/decisioning/specialisms/content-standards.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/creative-ad-server.d.ts +81 -0
- package/dist/lib/server/decisioning/specialisms/creative-ad-server.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/creative-ad-server.js +28 -0
- package/dist/lib/server/decisioning/specialisms/creative-ad-server.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/creative.d.ts +144 -0
- package/dist/lib/server/decisioning/specialisms/creative.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/creative.js +19 -0
- package/dist/lib/server/decisioning/specialisms/creative.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/lists.d.ts +61 -0
- package/dist/lib/server/decisioning/specialisms/lists.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/lists.js +30 -0
- package/dist/lib/server/decisioning/specialisms/lists.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/sales.d.ts +163 -0
- package/dist/lib/server/decisioning/specialisms/sales.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/sales.js +64 -0
- package/dist/lib/server/decisioning/specialisms/sales.js.map +1 -0
- package/dist/lib/server/decisioning/specialisms/signals.d.ts +64 -0
- package/dist/lib/server/decisioning/specialisms/signals.d.ts.map +1 -0
- package/dist/lib/server/decisioning/specialisms/signals.js +28 -0
- package/dist/lib/server/decisioning/specialisms/signals.js.map +1 -0
- package/dist/lib/server/decisioning/start-time.d.ts +76 -0
- package/dist/lib/server/decisioning/start-time.d.ts.map +1 -0
- package/dist/lib/server/decisioning/start-time.js +81 -0
- package/dist/lib/server/decisioning/start-time.js.map +1 -0
- package/dist/lib/server/decisioning/status-changes.d.ts +165 -0
- package/dist/lib/server/decisioning/status-changes.d.ts.map +1 -0
- package/dist/lib/server/decisioning/status-changes.js +131 -0
- package/dist/lib/server/decisioning/status-changes.js.map +1 -0
- package/dist/lib/server/decisioning/status-mappers.d.ts +46 -0
- package/dist/lib/server/decisioning/status-mappers.d.ts.map +1 -0
- package/dist/lib/server/decisioning/status-mappers.js +46 -0
- package/dist/lib/server/decisioning/status-mappers.js.map +1 -0
- package/dist/lib/server/decisioning/tenant-registry.d.ts +289 -0
- package/dist/lib/server/decisioning/tenant-registry.d.ts.map +1 -0
- package/dist/lib/server/decisioning/tenant-registry.js +503 -0
- package/dist/lib/server/decisioning/tenant-registry.js.map +1 -0
- package/dist/lib/server/express-adapter.d.ts +1 -1
- package/dist/lib/server/express-adapter.js +1 -1
- package/dist/lib/server/governance.d.ts +1 -1
- package/dist/lib/server/governance.js +1 -1
- package/dist/lib/server/idempotency/store.d.ts +1 -1
- package/dist/lib/server/idempotency/store.js +1 -1
- package/dist/lib/server/index.d.ts +9 -2
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js +79 -4
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/server/legacy/v5/index.d.ts +38 -0
- package/dist/lib/server/legacy/v5/index.d.ts.map +1 -0
- package/dist/lib/server/legacy/v5/index.js +60 -0
- package/dist/lib/server/legacy/v5/index.js.map +1 -0
- package/dist/lib/server/normalize-errors.d.ts +88 -0
- package/dist/lib/server/normalize-errors.d.ts.map +1 -0
- package/dist/lib/server/normalize-errors.js +146 -0
- package/dist/lib/server/normalize-errors.js.map +1 -0
- package/dist/lib/server/pick-safe-details.d.ts +90 -0
- package/dist/lib/server/pick-safe-details.d.ts.map +1 -0
- package/dist/lib/server/pick-safe-details.js +148 -0
- package/dist/lib/server/pick-safe-details.js.map +1 -0
- package/dist/lib/server/postgres-state-store.d.ts +1 -1
- package/dist/lib/server/postgres-state-store.js +1 -1
- package/dist/lib/server/responses.d.ts +38 -0
- package/dist/lib/server/responses.d.ts.map +1 -1
- package/dist/lib/server/responses.js +38 -0
- package/dist/lib/server/responses.js.map +1 -1
- package/dist/lib/server/state-store.d.ts +1 -1
- package/dist/lib/server/state-store.js +1 -1
- package/dist/lib/server/test-controller.d.ts +10 -3
- package/dist/lib/server/test-controller.d.ts.map +1 -1
- package/dist/lib/server/test-controller.js +10 -3
- package/dist/lib/server/test-controller.js.map +1 -1
- package/dist/lib/testing/comply-controller.d.ts +47 -1
- package/dist/lib/testing/comply-controller.d.ts.map +1 -1
- package/dist/lib/testing/comply-controller.js +11 -4
- package/dist/lib/testing/comply-controller.js.map +1 -1
- package/dist/lib/testing/index.d.ts +1 -1
- package/dist/lib/testing/index.d.ts.map +1 -1
- package/dist/lib/testing/index.js.map +1 -1
- package/dist/lib/testing/personas/index.d.ts +143 -0
- package/dist/lib/testing/personas/index.d.ts.map +1 -0
- package/dist/lib/testing/personas/index.js +190 -0
- package/dist/lib/testing/personas/index.js.map +1 -0
- package/dist/lib/testing/storyboard/index.d.ts +1 -1
- package/dist/lib/testing/storyboard/index.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/index.js +3 -2
- package/dist/lib/testing/storyboard/index.js.map +1 -1
- package/dist/lib/testing/storyboard/runner.d.ts +13 -0
- package/dist/lib/testing/storyboard/runner.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/runner.js +260 -7
- package/dist/lib/testing/storyboard/runner.js.map +1 -1
- package/dist/lib/types/asset-instances.d.ts +1 -0
- package/dist/lib/types/asset-instances.d.ts.map +1 -1
- package/dist/lib/types/core.generated.d.ts +203 -98
- package/dist/lib/types/core.generated.d.ts.map +1 -1
- package/dist/lib/types/core.generated.js +1 -1
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.d.ts.map +1 -1
- package/dist/lib/types/index.js.map +1 -1
- package/dist/lib/types/schemas.generated.d.ts +599 -159
- package/dist/lib/types/schemas.generated.d.ts.map +1 -1
- package/dist/lib/types/schemas.generated.js +175 -94
- package/dist/lib/types/schemas.generated.js.map +1 -1
- package/dist/lib/types/tools.generated.d.ts +315 -46
- package/dist/lib/types/tools.generated.d.ts.map +1 -1
- package/dist/lib/utils/capabilities.d.ts +1 -1
- package/dist/lib/utils/capabilities.d.ts.map +1 -1
- package/dist/lib/utils/capabilities.js +6 -0
- package/dist/lib/utils/capabilities.js.map +1 -1
- package/dist/lib/utils/creative-adapter.d.ts +32 -2
- package/dist/lib/utils/creative-adapter.d.ts.map +1 -1
- package/dist/lib/utils/creative-adapter.js +42 -6
- package/dist/lib/utils/creative-adapter.js.map +1 -1
- package/dist/lib/validation/schema-loader.d.ts.map +1 -1
- package/dist/lib/validation/schema-loader.js +20 -2
- package/dist/lib/validation/schema-loader.js.map +1 -1
- package/dist/lib/validation/schema-validator.d.ts +13 -0
- package/dist/lib/validation/schema-validator.d.ts.map +1 -1
- package/dist/lib/validation/schema-validator.js +240 -3
- package/dist/lib/validation/schema-validator.js.map +1 -1
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.d.ts.map +1 -1
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.js.map +1 -1
- package/docs/guides/BUILD-AN-AGENT.md +30 -5
- package/docs/llms.txt +28 -17
- package/examples/README.md +3 -1
- package/examples/decisioning-platform-broadcast-tv.ts +300 -0
- package/examples/decisioning-platform-identity-graph.ts +214 -0
- package/examples/decisioning-platform-mock-seller.ts +332 -0
- package/examples/decisioning-platform-multi-tenant.ts +128 -0
- package/examples/decisioning-platform-programmatic.ts +254 -0
- package/examples/signals-agent.ts +1 -1
- package/package.json +18 -5
- package/skills/build-brand-rights-agent/SKILL.md +10 -3
- package/skills/build-creative-agent/SKILL.md +94 -64
- package/skills/build-decisioning-creative-template/SKILL.md +554 -0
- package/skills/build-decisioning-platform/SKILL.md +304 -0
- package/skills/build-decisioning-platform/advanced/BRAND-RIGHTS.md +25 -0
- package/skills/build-decisioning-platform/advanced/COMPLIANCE.md +23 -0
- package/skills/build-decisioning-platform/advanced/GOVERNANCE.md +24 -0
- package/skills/build-decisioning-platform/advanced/HITL.md +34 -0
- package/skills/build-decisioning-platform/advanced/IDEMPOTENCY.md +52 -0
- package/skills/build-decisioning-platform/advanced/MULTI-TENANT.md +47 -0
- package/skills/build-decisioning-platform/advanced/OAUTH.md +22 -0
- package/skills/build-decisioning-platform/advanced/REFERENCE.md +991 -0
- package/skills/build-decisioning-platform/advanced/SANDBOX.md +24 -0
- package/skills/build-decisioning-platform/advanced/STATE-MACHINE.md +52 -0
- package/skills/build-decisioning-signal-marketplace/SKILL.md +269 -0
- package/skills/build-generative-seller-agent/SKILL.md +89 -53
- package/skills/build-governance-agent/SKILL.md +76 -45
- package/skills/build-retail-media-agent/SKILL.md +87 -62
- package/skills/build-seller-agent/SKILL.md +384 -255
- package/skills/build-seller-agent/deployment.md +5 -3
- package/skills/build-seller-agent/specialisms/audience-sync.md +0 -2
- package/skills/build-seller-agent/specialisms/sales-broadcast-tv.md +0 -2
- package/skills/build-seller-agent/specialisms/sales-guaranteed.md +0 -2
- package/skills/build-seller-agent/specialisms/sales-non-guaranteed.md +0 -2
- package/skills/build-seller-agent/specialisms/sales-proposal-mode.md +0 -2
- package/skills/build-seller-agent/specialisms/sales-social.md +0 -2
- package/skills/build-seller-agent/specialisms/signed-requests.md +0 -2
- package/skills/build-si-agent/SKILL.md +40 -32
- package/skills/build-signals-agent/SKILL.md +139 -92
- package/skills/call-adcp-agent.previous/SKILL.md +5 -0
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-decisioning-platform
|
|
3
|
+
description: Use when building an AdCP seller, creative, or audience agent against the v6.0 DecisioningPlatform shape. One interface, four async patterns, no AsyncOutcome ceremony — just `Promise<T>` and `throw AdcpError`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build a Decisioning Platform (v6.0)
|
|
7
|
+
|
|
8
|
+
> **Status: GA.** v6.0 ships under `@adcp/sdk/server` —
|
|
9
|
+
> `createAdcpServerFromPlatform` + `DecisioningPlatform`. The lower-level
|
|
10
|
+
> `createAdcpServer({ mediaBuy: { ... } })` API remains fully supported
|
|
11
|
+
> as the substrate this framework calls into; reach for it when you need
|
|
12
|
+
> fine control over individual handlers or have custom-shaped tools the
|
|
13
|
+
> platform interface doesn't yet model. The handler-bag skill is at
|
|
14
|
+
> `skills/build-seller-agent/`.
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
18
|
+
A `DecisioningPlatform` is a single TypeScript class implementing per-specialism interfaces:
|
|
19
|
+
|
|
20
|
+
- `sales: SalesPlatform` — `sales-non-guaranteed`, `sales-guaranteed`, retail-media, etc.
|
|
21
|
+
- `creative: CreativeBuilderPlatform | CreativeAdServerPlatform` — Builder covers both `creative-template` and `creative-generative` specialisms (one merged interface; both specialism IDs map to it). AdServer adds `listCreatives` + `getCreativeDelivery`.
|
|
22
|
+
- `audiences: AudiencePlatform`
|
|
23
|
+
- `signals: SignalsPlatform` — `signal-marketplace`, `signal-owned`
|
|
24
|
+
- `brandRights: BrandRightsPlatform` — brand identity + rights licensing
|
|
25
|
+
- `campaignGovernance`, `propertyLists`, `collectionLists`, `contentStandards` — governance surfaces
|
|
26
|
+
|
|
27
|
+
The framework owns wire mapping, account resolution, idempotency, signing, async tasks, status normalization, and lifecycle state. You write the business decisions.
|
|
28
|
+
|
|
29
|
+
## The canonical adopter shape
|
|
30
|
+
|
|
31
|
+
Minimal copy-paste-runnable example. Single tenant, one product, sync `create_media_buy`. Substitute your real lookups inside the bodies.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { AdcpError, createAdcpServerFromPlatform, type SalesPlatform, type AccountStore } from '@adcp/sdk/server';
|
|
35
|
+
|
|
36
|
+
// Don't annotate `platform: DecisioningPlatform` — let TS infer the
|
|
37
|
+
// `specialisms: ['sales-non-guaranteed']` literal so RequiredPlatformsFor
|
|
38
|
+
// narrows the constraint to "must provide sales: SalesPlatform".
|
|
39
|
+
const platform = {
|
|
40
|
+
capabilities: {
|
|
41
|
+
specialisms: ['sales-non-guaranteed'] as const,
|
|
42
|
+
creative_agents: [{ agent_url: 'https://creative.example.com/mcp' }],
|
|
43
|
+
channels: ['display'] as const,
|
|
44
|
+
pricingModels: ['cpm'] as const,
|
|
45
|
+
config: {},
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// Single-tenant: one synthetic account; framework still routes everything
|
|
49
|
+
// through resolve(). See § "accounts.resolve() is mandatory".
|
|
50
|
+
accounts: {
|
|
51
|
+
resolution: 'derived',
|
|
52
|
+
resolve: async () => ({
|
|
53
|
+
id: 'tenant_singleton',
|
|
54
|
+
name: 'My Ad Network',
|
|
55
|
+
status: 'active',
|
|
56
|
+
metadata: {},
|
|
57
|
+
authInfo: { kind: 'api_key' },
|
|
58
|
+
}),
|
|
59
|
+
} satisfies AccountStore,
|
|
60
|
+
|
|
61
|
+
sales: {
|
|
62
|
+
getProducts: async (req, ctx) => ({
|
|
63
|
+
products: [
|
|
64
|
+
{
|
|
65
|
+
product_id: 'p_homepage',
|
|
66
|
+
name: 'Homepage display',
|
|
67
|
+
description: 'Above-the-fold homepage display, IAB display 300x250',
|
|
68
|
+
delivery_type: 'non_guaranteed',
|
|
69
|
+
format_ids: [{ id: 'display_300x250', agent_url: 'https://creative.example.com/mcp' }],
|
|
70
|
+
publisher_properties: [{ publisher_domain: 'publisher.example.com', selection_type: 'all' }],
|
|
71
|
+
pricing_options: [{ pricing_option_id: 'cpm_5', pricing_model: 'cpm', rate: 5, currency: 'USD' }],
|
|
72
|
+
reporting_capabilities: {
|
|
73
|
+
available_reporting_frequencies: ['hourly', 'daily'],
|
|
74
|
+
expected_delay_minutes: 30,
|
|
75
|
+
timezone: 'UTC',
|
|
76
|
+
supports_webhooks: false,
|
|
77
|
+
available_metrics: [],
|
|
78
|
+
date_range_support: 'date_range',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
}),
|
|
83
|
+
createMediaBuy: async (req, ctx) => ({
|
|
84
|
+
media_buy_id: `mb_${Date.now()}`,
|
|
85
|
+
status: 'pending_creatives',
|
|
86
|
+
confirmed_at: new Date().toISOString(),
|
|
87
|
+
packages: [],
|
|
88
|
+
}),
|
|
89
|
+
updateMediaBuy: async (mediaBuyId, patch, ctx) => ({
|
|
90
|
+
media_buy_id: mediaBuyId,
|
|
91
|
+
status: patch.paused === true ? 'paused' : 'active',
|
|
92
|
+
}),
|
|
93
|
+
syncCreatives: async (creatives, ctx) => creatives.map(c => ({ creative_id: c.creative_id, action: 'created' })),
|
|
94
|
+
getMediaBuyDelivery: async (filter, ctx) => ({
|
|
95
|
+
currency: 'USD',
|
|
96
|
+
reporting_period: { start: filter.start_date ?? '2026-04-01', end: filter.end_date ?? '2026-04-30' },
|
|
97
|
+
media_buy_deliveries: [],
|
|
98
|
+
}),
|
|
99
|
+
} satisfies SalesPlatform,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const server = createAdcpServerFromPlatform(platform, {
|
|
103
|
+
name: 'My Ad Network',
|
|
104
|
+
version: '1.0.0',
|
|
105
|
+
// Plus standard createAdcpServer options: idempotency, signedRequests,
|
|
106
|
+
// webhooks, validation, etc.
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Then mount with `serve(server)` for MCP, or createExpressAdapter for A2A.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
What the framework wires automatically when you call `createAdcpServerFromPlatform`:
|
|
113
|
+
|
|
114
|
+
- All the AdCP wire tools your declared specialisms support (e.g., `get_products`, `create_media_buy`).
|
|
115
|
+
- A `tasks_get` polling tool — buyers call it with `{ task_id, account }` to poll HITL task lifecycle. You don't write this; it's there as soon as you wire any `*Task` HITL method. See "The buyer gets terminal state two ways" below for the full lifecycle shape.
|
|
116
|
+
- Idempotency-key replay protection on every mutating tool.
|
|
117
|
+
- RFC 9421 webhook signing on terminal-task push notifications (when `serve({ webhooks })` is wired).
|
|
118
|
+
|
|
119
|
+
**Three rules**:
|
|
120
|
+
|
|
121
|
+
1. Methods return `Promise<T>` directly — no `ok()` / `submitted()` / `rejected()` wrappers.
|
|
122
|
+
2. `throw new AdcpError(code, opts)` for buyer-facing structured rejection.
|
|
123
|
+
3. For HITL on tools whose wire response defines a `Submitted` arm (`create_media_buy`, `sync_creatives`): return `ctx.handoffToTask(fn)` from inside the same method. The framework allocates `task_id`, returns the spec-defined `Submitted` envelope to the buyer, and runs `fn` in the background. Hybrid sellers branch per call.
|
|
124
|
+
|
|
125
|
+
## Persisting platform IDs (`ctx.ctxMetadata`)
|
|
126
|
+
|
|
127
|
+
The framework provides a per-account opaque-blob cache for adapter-internal state that AdCP's wire schema doesn't model — GAM `ad_unit_ids` per product, GAM `order_id` per media_buy, line item ID per package, etc. Don't re-derive on every call; stash it once, read it on subsequent calls.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// Wire a store at construction:
|
|
131
|
+
import { createCtxMetadataStore, memoryCtxMetadataStore, pgCtxMetadataStore, getCtxMetadataMigration } from '@adcp/sdk/server';
|
|
132
|
+
|
|
133
|
+
await pool.query(getCtxMetadataMigration()); // Postgres only
|
|
134
|
+
const ctxMetadata = createCtxMetadataStore({ backend: pgCtxMetadataStore(pool) });
|
|
135
|
+
|
|
136
|
+
createAdcpServerFromPlatform(myPlatform, { name: '...', version: '...', ctxMetadata });
|
|
137
|
+
|
|
138
|
+
// Stash on the way out (any returned resource — product, media_buy, package, creative):
|
|
139
|
+
getProducts: async (req, ctx) => {
|
|
140
|
+
const products = await this.gam.products.search(req.brief);
|
|
141
|
+
for (const p of products) {
|
|
142
|
+
await ctx.ctxMetadata?.set('product', p.id, { gam: { ad_unit_ids: p.adUnitIds } });
|
|
143
|
+
}
|
|
144
|
+
return { products: products.map(toAdcpProduct) };
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// Read on the way in (subsequent call referencing the same ID):
|
|
148
|
+
createMediaBuy: async (req, ctx) => {
|
|
149
|
+
for (const pkg of req.packages) {
|
|
150
|
+
const meta = await ctx.ctxMetadata?.product(pkg.product_id);
|
|
151
|
+
if (meta?.gam?.ad_unit_ids) {
|
|
152
|
+
await this.gam.lineItems.create(pkg, meta.gam.ad_unit_ids);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// ...
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Common mistake:** don't re-fetch GAM ad-unit IDs on every `create_media_buy`. Attach them on `get_products` and read from `ctx.ctxMetadata.product(id)`.
|
|
160
|
+
|
|
161
|
+
**Memory backend in production:** the SDK warns at boot when `NODE_ENV=production` and you've wired `memoryCtxMetadataStore()` — silent ctx_metadata loss after rolling restart can run for weeks producing "package not found" errors. Use `pgCtxMetadataStore(pool)` for cluster.
|
|
162
|
+
|
|
163
|
+
**Account scoping is automatic.** `ctx.ctxMetadata` binds to `ctx.account.id` per request — you never pass the account. No-account tools (`provide_performance_feedback`, `list_creative_formats`) get `ctx.ctxMetadata = undefined`.
|
|
164
|
+
|
|
165
|
+
**Same primitive across all specialisms** — sales (`product` / `media_buy` / `package`), creative-builder (refine workflow: stash on `build_creative`, read on `refine_creative`), audiences, signals, brand-rights.
|
|
166
|
+
|
|
167
|
+
See [`docs/proposals/decisioning-platform-v6-1-ctx-metadata.md`](../../docs/proposals/decisioning-platform-v6-1-ctx-metadata.md) for the full design.
|
|
168
|
+
|
|
169
|
+
## Two async patterns
|
|
170
|
+
|
|
171
|
+
### 1. Sync happy path
|
|
172
|
+
|
|
173
|
+
The 80% case. Plain async function:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
createMediaBuy: async (req, ctx) => {
|
|
177
|
+
const buy = await this.platform.createOrder(req);
|
|
178
|
+
return this.toMediaBuy(buy);
|
|
179
|
+
};
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 2. Structured rejection — `throw AdcpError`
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
createMediaBuy: async (req, ctx) => {
|
|
186
|
+
// total_budget is `number | { amount?: number; currency?: string }`
|
|
187
|
+
// depending on buyer shape; discriminate before access.
|
|
188
|
+
const budget = typeof req.total_budget === 'number' ? req.total_budget : (req.total_budget?.amount ?? 0);
|
|
189
|
+
if (budget < FLOOR_BUDGET) {
|
|
190
|
+
throw new AdcpError('BUDGET_TOO_LOW', {
|
|
191
|
+
recovery: 'correctable',
|
|
192
|
+
message: `Floor is $${FLOOR_BUDGET}`,
|
|
193
|
+
field: 'total_budget',
|
|
194
|
+
suggestion: `Raise total_budget to at least ${FLOOR_BUDGET}`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return /* ... your createMediaBuy success body ... */;
|
|
198
|
+
};
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`AdcpError` carries `code`, `recovery`, optional `field` / `suggestion` / `retry_after` / `details`. The framework projects these onto the wire `adcp_error` envelope.
|
|
202
|
+
|
|
203
|
+
**Multi-error pre-flight** (Prebid pattern): one throw, all errors in `details.errors`:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
const errors = this.preflight(req); // returns AdcpStructuredError[]
|
|
207
|
+
if (errors.length > 0) {
|
|
208
|
+
throw new AdcpError('INVALID_REQUEST', {
|
|
209
|
+
recovery: 'correctable',
|
|
210
|
+
message: errors[0].message,
|
|
211
|
+
field: errors[0].field,
|
|
212
|
+
details: { errors },
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### 3. HITL — return `ctx.handoffToTask(fn)`
|
|
218
|
+
|
|
219
|
+
For tools whose wire response defines a `Submitted` arm (today: `create_media_buy`, `sync_creatives`), adopters with human-in-the-loop workflows return `ctx.handoffToTask(fn)` from inside the same method. The framework allocates `task_id`, returns the spec-defined `Submitted` envelope to the buyer immediately, then runs `fn` in the background. `fn`'s return value becomes the task's terminal artifact; `throw new AdcpError(...)` becomes the terminal error. `fn` receives a `TaskHandoffContext` with `id` (framework-issued task id), `update(progress)`, and `heartbeat()`.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
sales: SalesPlatform<MyMeta> = {
|
|
223
|
+
// ... other methods ...
|
|
224
|
+
createMediaBuy: (req, ctx) =>
|
|
225
|
+
ctx.handoffToTask(async taskCtx => {
|
|
226
|
+
// Persist the task_id on your side first, before waiting on a human:
|
|
227
|
+
await this.queueForReview({ taskId: taskCtx.id, request: req });
|
|
228
|
+
|
|
229
|
+
// Optional: push a status message visible to the buyer's polling.
|
|
230
|
+
await taskCtx.update({ message: 'Awaiting trafficker review...' });
|
|
231
|
+
|
|
232
|
+
// Then await the operator. Hours-to-days are fine — buyer received
|
|
233
|
+
// the submitted envelope already and polls / receives webhook.
|
|
234
|
+
const decision = await this.waitForOperatorApproval(req);
|
|
235
|
+
|
|
236
|
+
if (decision.denied) {
|
|
237
|
+
throw new AdcpError('GOVERNANCE_DENIED', {
|
|
238
|
+
recovery: 'terminal',
|
|
239
|
+
message: decision.reason,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Return → task transitions to `completed` with this as `result`
|
|
244
|
+
return {
|
|
245
|
+
media_buy_id: decision.media_buy_id,
|
|
246
|
+
status: 'pending_creatives',
|
|
247
|
+
confirmed_at: new Date().toISOString(),
|
|
248
|
+
packages: [],
|
|
249
|
+
};
|
|
250
|
+
}),
|
|
251
|
+
};
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
> **`TaskHandoff` is a framework contract — never inspect or unwrap it.** `ctx.handoffToTask(fn)` returns an opaque marker. Do not `instanceof`-check it, read `._taskFn`, or call it yourself. Return it from your method and the dispatcher handles allocation, background execution, and terminal-state writes. Any code that reaches inside the marker bypasses lifecycle accounting and will break silently.
|
|
255
|
+
|
|
256
|
+
**The buyer gets terminal state two ways:**
|
|
257
|
+
|
|
258
|
+
1. **Webhook push** — buyer included `push_notification_config: { url, token }` in the original request. Framework signs (RFC 9421) + delivers to that URL with the spec's `mcp-webhook-payload.json` envelope on terminal state. URL is validated server-side: rejects RFC 1918, loopback, link-local, CGNAT, IPv6 unique-local, alternate IPv4 forms, and IPv4-mapped IPv6 before delivery (SSRF guard). Bad URLs FAIL FAST with `INVALID_REQUEST` at the request boundary — buyers see their config error immediately, not as silent webhook drops.
|
|
259
|
+
2. **Polling** — framework auto-registers a `tasks_get` custom tool. Buyers call it with `{ task_id, account }` and receive the spec-flat lifecycle shape (`task_id`, `task_type`, `status`, `created_at`, `updated_at`, `completed_at` on terminal, `result` on completed, top-level `error: { code, message, details? }` on failed). Tenant-scoped — passes `account` through `accounts.resolve(ref, ctx)` and refuses cross-tenant probes with `REFERENCE_NOT_FOUND`. You don't write this tool; it's wired in by the framework. Programmatic access for ops / cron code is via `server.getTaskState(taskId, accountId)`.
|
|
260
|
+
|
|
261
|
+
**Sync-only tools that need long-running completion** use `publishStatusChange(...)` for lifecycle updates instead of HITL. The per-tool wire response schemas don't include `Submitted` arms for `update_media_buy`, `build_creative`, `sync_catalogs`, or `get_products` (a spec inconsistency tracked as [adcp#3392](https://github.com/adcontextprotocol/adcp/issues/3392) — the Submitted schemas exist but aren't rolled into each tool's response `oneOf`). Until the spec consolidates, long-running work on those tools publishes status changes (`media_buy` → `active` → `completed`) on the event bus and buyers subscribe. When adcp#3392 lands, the SDK will widen the unified shape to `update_media_buy`, `build_creative`, and `sync_catalogs` — but NOT to `get_products` (see "Proposal generation" below).
|
|
262
|
+
|
|
263
|
+
### Proposal generation is NOT `get_products`
|
|
264
|
+
|
|
265
|
+
`get_products` is a fast catalog lookup. Proposal generation (brief-to-pitch creative workflows that produce new products tailored to the buyer's request) is a different verb. Custom-curation sellers, broadcast-TV proposal-mode systems, generative ad-network pitches — all should expose proposal generation through a separate wire surface, NOT by promoting `get_products` to HITL.
|
|
266
|
+
|
|
267
|
+
Filed upstream as [adcp#3407](https://github.com/adcontextprotocol/adcp/issues/3407) advocating a `request_proposal` tool with explicit Submitted-only semantics. Until that lands, proposal-mode adopters surface the eventual proposal via `publishStatusChange` on `resource_type: 'proposal'`; buyers subscribe and receive the brief-derived products as they're generated.
|
|
268
|
+
|
|
269
|
+
The unified hybrid shape (`Success | TaskHandoff`) is right when ONE verb has variable timing (`create_media_buy`: programmatic remnant sync, guaranteed inventory HITL). It's the wrong shape for `get_products` because catalog lookup and proposal generation are TWO verbs being squished into one tool name. Don't reach for `ctx.handoffToTask` on `getProducts` even when adcp#3392 makes it wire-legal.
|
|
270
|
+
|
|
271
|
+
## Hybrid sellers (programmatic + guaranteed in one tenant)
|
|
272
|
+
|
|
273
|
+
A real publisher commonly sells both **programmatic remnant** (sync, instant `media_buy_id`) and **guaranteed/sponsorship** (HITL, trafficker review) through the same `create_media_buy` tool. The unified shape handles this natively — branch in your method body on whatever signal determines the path (product type, buyer pre-approval, amount thresholds, etc.):
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
sales: SalesPlatform = {
|
|
277
|
+
createMediaBuy: async (req, ctx) => {
|
|
278
|
+
// Fast path: programmatic remnant, pre-approved buyer, low-risk amount.
|
|
279
|
+
// Returns Success directly — buyer gets media_buy_id on the immediate response.
|
|
280
|
+
if (this.isProgrammatic(req)) {
|
|
281
|
+
return await this.commitSync(req);
|
|
282
|
+
}
|
|
283
|
+
// Slow path: guaranteed inventory, trafficker review needed.
|
|
284
|
+
// Returns TaskHandoff — buyer gets { status: 'submitted', task_id }.
|
|
285
|
+
return ctx.handoffToTask(async taskCtx => {
|
|
286
|
+
await taskCtx.update({ message: 'Awaiting trafficker review' });
|
|
287
|
+
return await this.waitForTrafficker(req, taskCtx.id);
|
|
288
|
+
});
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Buyers pattern-match on the wire response shape. Predictable per request (deterministic given the products selected), dynamic per call. No latency tax on the 99% programmatic fast path; no awkward wire workarounds for the HITL slow path.
|
|
294
|
+
|
|
295
|
+
## Per-creative review (partial-batch)
|
|
296
|
+
|
|
297
|
+
`syncCreatives` returns per-creative `status`. Mix freely on the sync arm:
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
syncCreatives: async (creatives, ctx) => {
|
|
301
|
+
return creatives.map(c => ({
|
|
302
|
+
creative_id: c.creative_id,
|
|
303
|
+
action: 'created',
|
|
304
|
+
status: this.requiresManualReview(c) ? 'pending_review' : 'approved',
|
|
305
|
+
}));
|
|
306
|
+
};
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
When the ENTIRE batch needs background review (Innovid, broadcast TV — 4-72h SLA), return `ctx.handoffToTask(fn)` and the framework projects the spec's `Submitted` envelope:
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
syncCreatives: async (creatives, ctx) => {
|
|
313
|
+
if (creatives.some(c => this.needsBatchReview(c))) {
|
|
314
|
+
return ctx.handoffToTask(async taskCtx => {
|
|
315
|
+
await taskCtx.update({ message: 'S&P review pending' });
|
|
316
|
+
return await this.reviewAndPersist(creatives);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
// Sync arm — return rows directly.
|
|
320
|
+
return creatives.map(c => ({ creative_id: c.creative_id, action: 'created', status: 'approved' }));
|
|
321
|
+
};
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Buyer-driven approval as separate methods
|
|
325
|
+
|
|
326
|
+
Don't smush approval into `createMediaBuy` as a side-effect when the buyer can drive the workflow explicitly. AdCP has dedicated specialisms:
|
|
327
|
+
|
|
328
|
+
- `acquire_rights` — brand-rights specialism (`brand: BrandRightsPlatform`)
|
|
329
|
+
- `check_governance` — governance specialism (`governance: GovernancePlatform`, v1.1)
|
|
330
|
+
- `get_products` → `proposal_id` round-trips → `create_media_buy` commits
|
|
331
|
+
|
|
332
|
+
The buyer calls approval explicitly; `createMediaBuy` runs after the approval and is fast.
|
|
333
|
+
|
|
334
|
+
The escape hatch — `ctx.runAsync` + `ctx.startTask` — exists for the genuinely-opaque case where the buyer has no callable surface (GAM trafficker review where the operator's queue is internal).
|
|
335
|
+
|
|
336
|
+
## Error code vocabulary
|
|
337
|
+
|
|
338
|
+
`AdcpError`'s `code` field is `ErrorCode | (string & {})`. The 45 standard codes mirror `schemas/cache/3.0.0/enums/error-code.json`. Autocomplete works on the standard set; platform-specific codes are accepted (the `(string & {})` escape hatch).
|
|
339
|
+
|
|
340
|
+
**Misspellings warn at runtime.** `'BUDGET_TO_LOW'` (typo) compiles fine but the framework warns once per unknown code at construction. Set `ADCP_DECISIONING_ALLOW_CUSTOM_CODES=1` to silence the warn for platforms that intentionally mint vendor-specific codes (e.g., `'GAM_INTERNAL_QUOTA_EXCEEDED'`). Verify against the `ErrorCode` union before shipping.
|
|
341
|
+
|
|
342
|
+
Common codes:
|
|
343
|
+
|
|
344
|
+
- **Buyer-fixable** (`recovery: 'correctable'`): `INVALID_REQUEST`, `BUDGET_TOO_LOW`, `POLICY_VIOLATION`, `CREATIVE_REJECTED`, `MEDIA_BUY_NOT_FOUND`, `INVALID_STATE`, `REQUOTE_REQUIRED`
|
|
345
|
+
- **Transient** (`recovery: 'transient'`, retry with backoff): `RATE_LIMITED` (always include `retry_after`), `SERVICE_UNAVAILABLE`
|
|
346
|
+
- **Terminal** (`recovery: 'terminal'`, requires human action): `GOVERNANCE_DENIED`, `ACCOUNT_SUSPENDED`, `PERMISSION_DENIED`, `UNSUPPORTED_FEATURE`
|
|
347
|
+
|
|
348
|
+
Generic thrown errors (`Error`, `TypeError`) become `SERVICE_UNAVAILABLE` at the framework boundary.
|
|
349
|
+
|
|
350
|
+
### Sanitizing error details with `pickSafeDetails`
|
|
351
|
+
|
|
352
|
+
`AdcpError`'s optional `details` field is freeform — adopters often want to surface upstream-platform error context (request IDs, HTTP statuses, vendor codes). Raw upstream error objects almost always carry credentials, PII, or internal stack traces that MUST NOT cross the wire boundary.
|
|
353
|
+
|
|
354
|
+
`pickSafeDetails(input, allowlist, opts?)` is an explicit-allowlist sanitizer at `@adcp/sdk/server`. Only allowlisted keys survive; default caps at depth 2 + 2 KB serialized.
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
import { pickSafeDetails } from '@adcp/sdk/server';
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
await gamClient.createOrder(req);
|
|
361
|
+
} catch (upstreamErr) {
|
|
362
|
+
throw new AdcpError('UPSTREAM_REJECTED', {
|
|
363
|
+
recovery: 'transient',
|
|
364
|
+
message: 'Ad server rejected the order',
|
|
365
|
+
// Allowlist: only safe upstream fields cross the wire.
|
|
366
|
+
details: pickSafeDetails(upstreamErr, ['http_status', 'request_id', 'gam_error_code']),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
What gets dropped silently: any key not in the allowlist (no warning — the design assumes the allowlist is the contract); functions / Symbols / Date / RegExp / class instances; nested objects beyond `maxDepth`; results exceeding `maxSizeBytes`. Returns `undefined` (not `{}`) when nothing survives, so the spread into `details: ...` is a no-op rather than emitting an empty block.
|
|
372
|
+
|
|
373
|
+
### Wire-shape normalizer for `errors[]`
|
|
374
|
+
|
|
375
|
+
The wire spec for tools that surface partial-batch failures (`sync_creatives`, `sync_audiences`, `sync_accounts`, `report_usage`) requires `errors: Error[]` with the canonical `{ code, message, recovery? ... }` shape. Adopters often have errors in ad-hoc shapes — bare strings, native `Error` instances, vendor-specific objects. The framework applies `normalizeErrors` automatically at the `sync_creatives` projection seam (sales + creative dispatch); for adopter code that constructs `errors[]` directly (in custom handlers, or in tools where the framework doesn't auto-normalize yet), the helper is exported at `@adcp/sdk/server`:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import { normalizeErrors } from '@adcp/sdk/server';
|
|
379
|
+
|
|
380
|
+
return creatives.map(c => ({
|
|
381
|
+
creative_id: c.id,
|
|
382
|
+
action: c.passed ? 'created' : 'failed',
|
|
383
|
+
// Adopter passes whatever shape they have — strings, Error
|
|
384
|
+
// instances, partial wire objects. normalizeErrors coerces.
|
|
385
|
+
errors: normalizeErrors(c.upstreamErrors),
|
|
386
|
+
}));
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Coercion rules (per-entry):
|
|
390
|
+
|
|
391
|
+
- `string` → `{ code: 'GENERIC_ERROR', message: <input>, recovery: 'terminal' }`
|
|
392
|
+
- `Error` instance → `{ code: 'GENERIC_ERROR', message: err.message, recovery: 'terminal' }`
|
|
393
|
+
- Object with `code` + `message` → wire shape (vendor-specific keys dropped — use `details` for those)
|
|
394
|
+
- `null` / `undefined` → `{ code: 'GENERIC_ERROR', message: 'Unknown error', recovery: 'terminal' }`
|
|
395
|
+
|
|
396
|
+
`normalizeErrors` does NOT sanitize `details`; pair with `pickSafeDetails` on the adopter side before constructing the row.
|
|
397
|
+
|
|
398
|
+
## Account resolution
|
|
399
|
+
|
|
400
|
+
`accounts.resolve(ref, ctx?)` is the single tenant boundary. Three resolution modes:
|
|
401
|
+
|
|
402
|
+
| `resolution` | When to pick | What `resolve` receives |
|
|
403
|
+
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
404
|
+
| `'explicit'` (default) | Multi-tenant; buyer passes `account_id` on every request (Snap, Meta, GAM via Network/Company id). | `ref = { account_id }` (or `{ brand, operator }`) on every call. |
|
|
405
|
+
| `'implicit'` | Buyer pre-syncs accounts via `sync_accounts`; subsequent calls resolved by `ctx.authInfo` lookup against pre-synced linkage (LinkedIn, some retail-media operators). | `ref` may be undefined; use `ctx.authInfo.clientId` to look up. |
|
|
406
|
+
| `'derived'` | Single-tenant; one logical advertiser per agent process. Auth principal alone identifies the tenant. | `ref` typically undefined; return the singleton regardless. |
|
|
407
|
+
|
|
408
|
+
**If you have one tenant, declare `resolution: 'derived'`.** The default is `'explicit'`. A single-tenant agent that omits `resolution` falls into `'explicit'` mode where tools whose buyer omits the `account` field (`provide_performance_feedback`, `list_creative_formats`, `report_usage`, `tasks_get` without explicit account) silently fail with `ACCOUNT_NOT_FOUND` because the framework expects the buyer to pass an account on those tools too.
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
// Multi-tenant
|
|
412
|
+
accounts: {
|
|
413
|
+
resolution: 'explicit',
|
|
414
|
+
resolve: async (ref, ctx) => {
|
|
415
|
+
if (ref?.account_id) return await this.db.findById(ref.account_id);
|
|
416
|
+
if (ref?.brand) return await this.db.findByBrand(ref.brand.domain, ref.operator);
|
|
417
|
+
// ref undefined: tool without `account` field on wire — auth-derived path.
|
|
418
|
+
if (ctx?.authInfo?.clientId) return await this.db.findByClient(ctx.authInfo.clientId);
|
|
419
|
+
return null; // → ACCOUNT_NOT_FOUND
|
|
420
|
+
},
|
|
421
|
+
} satisfies AccountStore;
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Use `ctx.authInfo` to authorize, not just lookup.** Don't naively `findById(ref.account_id)` — that lets an attacker passing `{ account: { account_id: 'tenant_B' } }` get tenant B's account back from a flat lookup. Cross-check that the resolved tenant is reachable from the principal in `ctx.authInfo` (e.g., the OAuth client has been granted access to that tenant). The framework wires `ctx.authInfo` automatically from `serve({ authenticate })`.
|
|
425
|
+
|
|
426
|
+
### Explicit-mode adopters MUST handle `ref === undefined`
|
|
427
|
+
|
|
428
|
+
The framework calls `accounts.resolve(undefined, { authInfo, toolName })` for every request whose wire schema lacks an `account` field — this is universal across `'explicit'`, `'implicit'`, and `'derived'` modes. The wire tools that hit this path:
|
|
429
|
+
|
|
430
|
+
- `list_creative_formats` (universal — every buyer expects it)
|
|
431
|
+
- `provide_performance_feedback`
|
|
432
|
+
- `report_usage`
|
|
433
|
+
- `tasks_get` when called without `account` (single-tenant case)
|
|
434
|
+
- `get_account_financials` (account is implicit from auth)
|
|
435
|
+
|
|
436
|
+
If your `'explicit'`-mode resolver only handles `ref?.account_id` and falls through on `undefined`, those tools get `ctx.account === undefined` and the framework returns `ACCOUNT_NOT_FOUND`. The fix is the `if (ctx?.authInfo?.clientId)` branch in the example above. Your tenants are reachable from the OAuth client / API-key principal — that's how multi-tenant SaaS auth works — so this is a code-path you already have at the auth layer; just thread it into `resolve()`.
|
|
437
|
+
|
|
438
|
+
Throwing `AccountNotFoundError` only from `resolve()` — never from specialism methods — gets the spec's fixed `ACCOUNT_NOT_FOUND` envelope. Generic throws from inside `resolve()` map to `SERVICE_UNAVAILABLE`.
|
|
439
|
+
|
|
440
|
+
### `accounts.resolve()` is mandatory — even for "no tenant" agents
|
|
441
|
+
|
|
442
|
+
The framework calls `accounts.resolve()` on every request before dispatching to a specialism method. Single-tenant agents that historically skipped account resolution (no per-buyer scoping; the agent serves one logical advertiser) MUST still implement `resolve()` — declare `resolution: 'derived'` and return a single synthetic `Account` regardless of the input ref:
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
accounts: AccountStore<MyMeta> = {
|
|
446
|
+
resolution: 'derived', // single-tenant; auth principal alone identifies the tenant
|
|
447
|
+
resolve: async () => ({
|
|
448
|
+
id: 'singleton',
|
|
449
|
+
name: 'My Agent',
|
|
450
|
+
status: 'active',
|
|
451
|
+
metadata: {
|
|
452
|
+
/* whatever your handlers want to read off ctx.account.ctx_metadata */
|
|
453
|
+
},
|
|
454
|
+
authInfo: { kind: 'api_key' },
|
|
455
|
+
}),
|
|
456
|
+
};
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Why this is non-negotiable: the resolved account is the framework's tenant boundary for idempotency keys, status-change scoping (`account_id` on every event), workflow steps, and per-tenant capability overrides via `getCapabilitiesFor(account)`. A platform without an `account` per request can't participate in any of those. Adopters migrating from a pre-v6 codebase where `accounts.resolve()` was skipped (training-agent's posture today) need to add this wrapper as part of the migration — it's ~10 lines and unblocks the rest of the framework's invariants.
|
|
460
|
+
|
|
461
|
+
### Sandbox: `AccountReference.sandbox === true`
|
|
462
|
+
|
|
463
|
+
There is no separate "dry-run" mode in v6. When the buyer sends `account.sandbox === true`, the framework calls `accounts.resolve()` with the same flag set; your resolver routes to a sandbox account, and the platform reads/writes go through your sandbox backend by reading `account.ctx_metadata`:
|
|
464
|
+
|
|
465
|
+
```ts
|
|
466
|
+
resolve: async (ref) => {
|
|
467
|
+
if (ref.sandbox === true) {
|
|
468
|
+
return { id: 'sandbox_acc', metadata: { backend: 'sandbox' }, ... };
|
|
469
|
+
}
|
|
470
|
+
return { id: 'prod_acc', metadata: { backend: 'production' }, ... };
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Tool-specific `dry_run` flags on `sync_catalogs` and `sync_creatives` are wire fields the platform receives and honors locally — they're NOT a framework-level mode.
|
|
475
|
+
|
|
476
|
+
## OAuth provider wiring
|
|
477
|
+
|
|
478
|
+
OAuth verifiers live on `serve()`, not on the platform. The platform only sees the resolved `authInfo` via `ctx.account.authInfo` after `serve({ authenticate })` produces it:
|
|
479
|
+
|
|
480
|
+
<!-- skill-example-skip: documentation-pattern, references undeclared `server`, `parseBearerToken`, `myOAuthProvider` -->
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
import { serve } from '@adcp/sdk/server';
|
|
484
|
+
|
|
485
|
+
serve(() => server, {
|
|
486
|
+
publicUrl: 'https://my-agent.example.com',
|
|
487
|
+
authenticate: async ({ headers }) => {
|
|
488
|
+
const token = parseBearerToken(headers.authorization);
|
|
489
|
+
const principal = await myOAuthProvider.verify(token); // SnapOAuthProvider, your verifier, etc.
|
|
490
|
+
if (!principal) return null; // 401
|
|
491
|
+
return {
|
|
492
|
+
token,
|
|
493
|
+
clientId: principal.client_id,
|
|
494
|
+
scopes: principal.scopes,
|
|
495
|
+
extra: { sub: principal.sub /* whatever */ },
|
|
496
|
+
};
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
The platform's `accounts.resolve()` receives this as `extra.authInfo` on the second arg context (when `serve({ authenticate })` is wired). Use it to translate the OAuth principal into your tenant model:
|
|
502
|
+
|
|
503
|
+
```ts
|
|
504
|
+
accounts: {
|
|
505
|
+
resolve: async (ref, { authInfo }) => {
|
|
506
|
+
const platformAccountId = await myUpstream.findAccountByOAuthClient(authInfo?.clientId, ref);
|
|
507
|
+
return { id: platformAccountId, ... };
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Same pattern for stdio + http transports — `authenticate` runs at the transport boundary, the platform sees the resolved principal. There's no `auth?: AuthProvider` field on `DecisioningPlatform`; that boundary is intentionally on the surrounding `serve()` opts.
|
|
513
|
+
|
|
514
|
+
## Production task storage
|
|
515
|
+
|
|
516
|
+
The framework's default in-memory `TaskRegistry` is gated by `NODE_ENV` — refuses to construct outside `{test, development}` unless `ADCP_DECISIONING_ALLOW_INMEMORY_TASKS=1` is explicitly set. Every HITL-eligible production deployment needs a durable task registry so task state survives process restarts and load-balancer failover.
|
|
517
|
+
|
|
518
|
+
Ship `createPostgresTaskRegistry({ pool, tableName? })`:
|
|
519
|
+
|
|
520
|
+
```ts
|
|
521
|
+
import { Pool } from 'pg';
|
|
522
|
+
import {
|
|
523
|
+
createAdcpServerFromPlatform,
|
|
524
|
+
createPostgresTaskRegistry,
|
|
525
|
+
getDecisioningTaskRegistryMigration,
|
|
526
|
+
} from '@adcp/sdk/server';
|
|
527
|
+
|
|
528
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
529
|
+
|
|
530
|
+
// Once at boot — idempotent CREATE TABLE IF NOT EXISTS, safe to re-run
|
|
531
|
+
await pool.query(getDecisioningTaskRegistryMigration());
|
|
532
|
+
|
|
533
|
+
const server = createAdcpServerFromPlatform(platform, {
|
|
534
|
+
name: 'My Ad Network',
|
|
535
|
+
version: '1.0.0',
|
|
536
|
+
taskRegistry: createPostgresTaskRegistry({ pool }),
|
|
537
|
+
});
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Cross-instance reads work — process A allocates the task, process B reads the lifecycle for `tasks_get`. Terminal-state idempotency is enforced via SQL `WHERE status = 'submitted'` so concurrent webhook deliveries can't race to overwrite each other. Background-completion tracking (`_registerBackground`) is process-local — promises don't serialize, so production HITL flows that span process boundaries drive completion via webhook → an explicit `complete()` / `fail()` from the receiving process.
|
|
541
|
+
|
|
542
|
+
Custom backend? Implement the `TaskRegistry` interface (8 methods) for Redis / DynamoDB / Spanner / etc. — the framework awaits each call so all 4 mutators (`create`, `complete`, `fail`, `getTask`) can be storage-backed.
|
|
543
|
+
|
|
544
|
+
**Adopter `*Task` return size cap.** Postgres-backed registries cap `result` / `error` JSONB rows at 4MB. Returns over the cap surface via `onTaskTransition` with `errorCode: 'REGISTRY_WRITE_FAILED'` and skip webhook delivery (registry state is inconsistent, so the framework refuses to push). Offload large payloads to blob storage and return references in the result body instead. The cap protects the DB write path only — adopter code that serializes `result` for logs/metrics MUST impose its own bound.
|
|
545
|
+
|
|
546
|
+
## Multi-tenant hosting (TenantRegistry)
|
|
547
|
+
|
|
548
|
+
Running multiple tenants from one process — different sellers, or multiple variants of the same agent under different specialisms? `TenantRegistry` is the framework primitive. Each tenant gets its own `DecisioningPlatform`, its own signing key, its own per-tenant capability declaration, and its own `'pending' → 'healthy' → 'unverified' → 'disabled'` lifecycle.
|
|
549
|
+
|
|
550
|
+
```ts
|
|
551
|
+
import { createTenantRegistry } from '@adcp/sdk/server';
|
|
552
|
+
|
|
553
|
+
const registry = createTenantRegistry({
|
|
554
|
+
defaultServerOptions: { name: 'Multi-Tenant', version: '1.0.0' /* shared opts */ },
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// Subdomain routing — one tenant per subdomain
|
|
558
|
+
registry.register('snap', {
|
|
559
|
+
agentUrl: 'https://snap.example.com',
|
|
560
|
+
signingKey: { keyId: 'snap-key-1', publicJwk: snapPub, privateJwk: snapPriv },
|
|
561
|
+
platform: snapPlatform,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// Path routing — multiple tenants under one host
|
|
565
|
+
registry.register('sales', {
|
|
566
|
+
agentUrl: 'https://training.example.com/sales',
|
|
567
|
+
signingKey: { keyId: 'sales-key-1', publicJwk: salesPub, privateJwk: salesPriv },
|
|
568
|
+
platform: salesPlatform,
|
|
569
|
+
});
|
|
570
|
+
registry.register('creative', {
|
|
571
|
+
agentUrl: 'https://training.example.com/creative',
|
|
572
|
+
signingKey: { keyId: 'creative-key-1', publicJwk: creativePub, privateJwk: creativePriv },
|
|
573
|
+
platform: creativePlatform,
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// Express handler dispatches via host + path
|
|
577
|
+
app.use((req, res, next) => {
|
|
578
|
+
const resolved = registry.resolveByRequest(req.headers.host ?? '', req.path);
|
|
579
|
+
if (!resolved) return res.status(503).set('Retry-After', '5').end();
|
|
580
|
+
// Mount the resolved tenant's MCP+A2A endpoints
|
|
581
|
+
return resolved.server.handle(req, res, next);
|
|
582
|
+
});
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
**Subdomain vs. path routing.** `resolveByRequest(host, pathname)` matches both. Subdomain tenants (`https://sales.training.example.com`) have implicit `/` prefix — match any pathname. Path tenants (`https://training.example.com/sales`) match longest-prefix. Mix freely; SDK supports both shapes from the same `TenantRegistry`.
|
|
586
|
+
|
|
587
|
+
**Pending state — JWKS validation gate.** New tenants land in `'pending'` until first JWKS validation succeeds. `resolveByRequest` REFUSES traffic for pending tenants — host transport responds 503 + Retry-After. Closes the register-then-serve race window where a tenant registered with a wrong signing key would have served signed-but-unverifiable responses for ~60s. Use `register({ awaitFirstValidation: true })` to block registration on the synchronous validation outcome — useful for deploy scripts that gate on health.
|
|
588
|
+
|
|
589
|
+
**Admin-API auth.** `register()` is the privileged surface. Hosts wiring HTTP/RPC endpoints in front of `register` MUST gate them with operator-level auth — anyone who can call `register` can introduce a tenant that signs outbound webhooks. Framework doesn't ship admin-HTTP scaffolding because the right auth shape varies by deployment.
|
|
590
|
+
|
|
591
|
+
**Shared infrastructure across tenants.** Idempotency stores, task registries, and status-change buses can be shared (one Postgres for all tenants). The framework's account scoping prevents cross-tenant reads. When sharing a task registry, prefix account ids per-tenant (`tenantA:acme`) to prevent collisions if two tenants both use the literal account id `acme`.
|
|
592
|
+
|
|
593
|
+
## Capability projections (`audience_targeting` / `conversion_tracking` / `content_standards`)
|
|
594
|
+
|
|
595
|
+
Three discovery blocks live under `get_adcp_capabilities.media_buy.*` in the wire spec. Adopters declare them on `platform.capabilities` and the framework projects them onto `get_adcp_capabilities` automatically (no custom `get_adcp_capabilities` tool needed — the framework refuses one anyway).
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
class MyPlatform implements DecisioningPlatform {
|
|
599
|
+
capabilities = {
|
|
600
|
+
specialisms: ['sales-non-guaranteed', 'audience-sync'] as const,
|
|
601
|
+
creative_agents: [{ agent_url: 'https://creative.example.com/mcp' }],
|
|
602
|
+
channels: ['display', 'olv'] as const,
|
|
603
|
+
pricingModels: ['cpm'] as const,
|
|
604
|
+
config: {},
|
|
605
|
+
|
|
606
|
+
// Audience-matching capability — required for audience-sync adopters
|
|
607
|
+
audience_targeting: {
|
|
608
|
+
supported_identifier_types: ['hashed_email', 'hashed_phone'],
|
|
609
|
+
supported_uid_types: [
|
|
610
|
+
/* UID2, RampID, MAID, etc. */
|
|
611
|
+
],
|
|
612
|
+
minimum_audience_size: 100,
|
|
613
|
+
matching_latency_hours: { min: 1, max: 24 },
|
|
614
|
+
},
|
|
615
|
+
|
|
616
|
+
// Conversion-tracking capability — required if adopter accepts events
|
|
617
|
+
conversion_tracking: {
|
|
618
|
+
multi_source_event_dedup: true,
|
|
619
|
+
supported_event_types: ['purchase', 'add_to_cart', 'lead'],
|
|
620
|
+
supported_action_sources: ['website', 'app'],
|
|
621
|
+
attribution_windows: [{ event_type: 'purchase', post_click: [{ interval: 7, unit: 'days' }] }],
|
|
622
|
+
},
|
|
623
|
+
|
|
624
|
+
// Content-standards capability — required if adopter claims
|
|
625
|
+
// 'content-standards' specialism
|
|
626
|
+
content_standards: {
|
|
627
|
+
supports_local_evaluation: true,
|
|
628
|
+
supported_channels: ['display', 'olv'],
|
|
629
|
+
supports_webhook_delivery: false,
|
|
630
|
+
},
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Each block is independently optional. Wire shape lives at `core/get-adcp-capabilities-response.json#media_buy.{audience_targeting,conversion_tracking,content_standards}` — declare what your platform actually supports, omit blocks you don't.
|
|
636
|
+
|
|
637
|
+
## Brand rights (`brand-rights` specialism)
|
|
638
|
+
|
|
639
|
+
The `brand-rights` specialism covers identity discovery + licensing for branded inventory — IP holders (sports leagues, movie studios), CTV brand-rights desks, and brand-licensing marketplaces. v6.0 ships first-class platform support for the 3 wire tools that have framework dispatch infrastructure:
|
|
640
|
+
|
|
641
|
+
```ts
|
|
642
|
+
import type {
|
|
643
|
+
DecisioningPlatform,
|
|
644
|
+
BrandRightsPlatform,
|
|
645
|
+
AccountStore,
|
|
646
|
+
GetBrandIdentitySuccess,
|
|
647
|
+
AcquireRightsAcquired,
|
|
648
|
+
AcquireRightsPendingApproval,
|
|
649
|
+
AcquireRightsRejected,
|
|
650
|
+
} from '@adcp/sdk/server';
|
|
651
|
+
|
|
652
|
+
class MyBrandRightsAgent implements DecisioningPlatform {
|
|
653
|
+
capabilities = {
|
|
654
|
+
specialisms: ['brand-rights'] as const,
|
|
655
|
+
creative_agents: [],
|
|
656
|
+
channels: ['display'] as const,
|
|
657
|
+
pricingModels: ['cpm'] as const,
|
|
658
|
+
config: {},
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
statusMappers = {};
|
|
662
|
+
accounts: AccountStore = {
|
|
663
|
+
/* ... */
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
brandRights: BrandRightsPlatform = {
|
|
667
|
+
// Sync — read brand identity record
|
|
668
|
+
getBrandIdentity: async (req, ctx): Promise<GetBrandIdentitySuccess> => ({
|
|
669
|
+
brand_id: 'brand_acme_42',
|
|
670
|
+
house: { domain: 'acme-corp.example.com', name: 'Acme Corp' },
|
|
671
|
+
names: [{ en_US: 'Acme', en_GB: 'ACME Co.' }],
|
|
672
|
+
industries: ['retail'],
|
|
673
|
+
keller_type: 'master',
|
|
674
|
+
}),
|
|
675
|
+
|
|
676
|
+
// Sync — list rights matching the query. Wire field is `rights:` (NOT
|
|
677
|
+
// `offerings:`); each entry is a RightsOffering with pricing_options.
|
|
678
|
+
getRights: async (req, ctx) => ({
|
|
679
|
+
rights: [
|
|
680
|
+
{
|
|
681
|
+
rights_id: 'rights_endorsement_us',
|
|
682
|
+
brand_id: 'brand_acme_42',
|
|
683
|
+
name: 'Acme endorsement, US',
|
|
684
|
+
available_uses: ['endorsement'],
|
|
685
|
+
countries: ['US'],
|
|
686
|
+
pricing_options: [
|
|
687
|
+
{
|
|
688
|
+
pricing_option_id: 'po_flat_100k',
|
|
689
|
+
model: 'flat_rate',
|
|
690
|
+
price: 100000,
|
|
691
|
+
currency: 'USD',
|
|
692
|
+
uses: ['endorsement'],
|
|
693
|
+
period: 'one_time',
|
|
694
|
+
},
|
|
695
|
+
],
|
|
696
|
+
},
|
|
697
|
+
],
|
|
698
|
+
}),
|
|
699
|
+
|
|
700
|
+
// Three wire-spec arms — return whichever matches the request.
|
|
701
|
+
acquireRights: async (req, ctx) => {
|
|
702
|
+
if (canClearImmediately(req)) {
|
|
703
|
+
const acquired: AcquireRightsAcquired = {
|
|
704
|
+
rights_id: req.rights_id,
|
|
705
|
+
status: 'acquired',
|
|
706
|
+
brand_id: 'brand_acme_42',
|
|
707
|
+
terms: {
|
|
708
|
+
pricing_option_id: req.pricing_option_id,
|
|
709
|
+
amount: 100000,
|
|
710
|
+
currency: 'USD',
|
|
711
|
+
uses: ['endorsement'],
|
|
712
|
+
period: 'one_time',
|
|
713
|
+
start_date: '2026-05-01',
|
|
714
|
+
end_date: '2026-12-31',
|
|
715
|
+
},
|
|
716
|
+
generation_credentials: [
|
|
717
|
+
// Per-LLM-provider scoped keys for rights-cleared content gen
|
|
718
|
+
// { provider: 'midjourney', rights_key: '...', uses: ['endorsement'] },
|
|
719
|
+
],
|
|
720
|
+
rights_constraint: {
|
|
721
|
+
rights_id: req.rights_id,
|
|
722
|
+
rights_agent: { url: 'https://my-rights-agent.example.com/mcp', id: 'my-rights-agent' },
|
|
723
|
+
uses: ['endorsement'],
|
|
724
|
+
countries: ['US'],
|
|
725
|
+
valid_from: '2026-05-01T00:00:00Z',
|
|
726
|
+
valid_until: '2026-12-31T23:59:59Z',
|
|
727
|
+
},
|
|
728
|
+
};
|
|
729
|
+
return acquired;
|
|
730
|
+
}
|
|
731
|
+
if (requiresHumanReview(req)) {
|
|
732
|
+
const pending: AcquireRightsPendingApproval = {
|
|
733
|
+
rights_id: req.rights_id,
|
|
734
|
+
status: 'pending_approval',
|
|
735
|
+
brand_id: 'brand_acme_42',
|
|
736
|
+
detail: 'Awaiting rights-holder counter-signature',
|
|
737
|
+
estimated_response_time: '48h',
|
|
738
|
+
};
|
|
739
|
+
return pending;
|
|
740
|
+
}
|
|
741
|
+
const rejected: AcquireRightsRejected = {
|
|
742
|
+
rights_id: req.rights_id,
|
|
743
|
+
status: 'rejected',
|
|
744
|
+
brand_id: 'brand_acme_42',
|
|
745
|
+
reason: 'Rights unavailable in requested jurisdiction',
|
|
746
|
+
suggestions: ['Try US-only deployment'],
|
|
747
|
+
};
|
|
748
|
+
return rejected;
|
|
749
|
+
},
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
**Capability declaration is required.** Set `capabilities.brand: {}` (empty block opts in) to declare brand-protocol support — the framework auto-derives `rights: true` from the `BrandRightsPlatform` impl, and adopters declare `right_types`, `available_uses`, `generation_providers`, `description` for richer discovery:
|
|
755
|
+
|
|
756
|
+
```ts
|
|
757
|
+
capabilities = {
|
|
758
|
+
specialisms: ['brand-rights'] as const,
|
|
759
|
+
// ...
|
|
760
|
+
brand: {
|
|
761
|
+
right_types: ['talent', 'brand_ip'],
|
|
762
|
+
available_uses: ['endorsement', 'likeness'],
|
|
763
|
+
generation_providers: ['midjourney', 'elevenlabs'],
|
|
764
|
+
description: 'Acme Brand-Rights Agent',
|
|
765
|
+
},
|
|
766
|
+
};
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
`RequiredCapabilitiesFor<'brand-rights'>` enforces this at compile-time — claiming `brand-rights` without `capabilities.brand` is a TypeScript error at the `createAdcpServerFromPlatform` call site.
|
|
770
|
+
|
|
771
|
+
**No discovery/search verb in v6.0.** `getBrandIdentity` resolves a `brand_id` into the identity record; it is NOT a search/discovery surface. Real IP desks often need search ("does this league hold rights to this player?") before identity resolves. Until upstream ships `search_brands` ([adcp#3480](https://github.com/adcontextprotocol/adcp/issues/3480)), the workaround is to use `getRights({ query, uses })` for free-text discovery and project unique `brand_id` values out of the result rows — adopters then call `getBrandIdentity({ brand_id })` per unique brand to fetch full identity records.
|
|
772
|
+
|
|
773
|
+
**`acquire_rights` async delivery is webhook-only, not polling.** Unlike `create_media_buy` / `sync_creatives` which use `ctx.handoffToTask(fn)` for HITL, `acquire_rights` has its own three wire-spec arms (`Acquired` / `PendingApproval` / `Rejected`). When you return `PendingApproval`, the buyer's `push_notification_config.url` receives the eventual `Acquired` or `Rejected` outcome — the spec does NOT define a polling tool for this surface. Don't reach for `tasks_get` here.
|
|
774
|
+
|
|
775
|
+
**Two surfaces still on the merge seam (deferred to v6.1):** `update_rights` and `creative_approval` are spec-published but not yet in `AdcpToolMap`, so they don't have framework dispatch infrastructure. Wire them via `opts.brandRights.{updateRights,creativeApproval}` until v6.1:
|
|
776
|
+
|
|
777
|
+
```ts
|
|
778
|
+
createAdcpServerFromPlatform(myBrandRightsAgent, {
|
|
779
|
+
name: 'my-rights-agent',
|
|
780
|
+
version: '1.0.0',
|
|
781
|
+
// 3 wire tools auto-wire from platform.brandRights;
|
|
782
|
+
// 2 stay on the merge seam until they land in AdcpToolMap (v6.1):
|
|
783
|
+
brandRights: {
|
|
784
|
+
updateRights: async (params, ctx) => ({
|
|
785
|
+
/* UpdateRightsSuccess */
|
|
786
|
+
}),
|
|
787
|
+
// creative_approval is a webhook receiver, not an MCP tool — wire as
|
|
788
|
+
// a separate HTTP endpoint outside this seam until the spec settles.
|
|
789
|
+
},
|
|
790
|
+
});
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
## Compliance testing (`comply_test_controller`)
|
|
794
|
+
|
|
795
|
+
Conformance harnesses (the AdCP storyboard suite, in particular) drive seller-side state-machine tests via the wire-spec `comply_test_controller` tool. The framework registers it for you when you supply `complyTest` adapters on `createAdcpServerFromPlatform`.
|
|
796
|
+
|
|
797
|
+
```ts
|
|
798
|
+
import { createAdcpServerFromPlatform } from '@adcp/sdk/server';
|
|
799
|
+
|
|
800
|
+
const server = createAdcpServerFromPlatform(myPlatform, {
|
|
801
|
+
name: 'my-seller',
|
|
802
|
+
version: '1.0.0',
|
|
803
|
+
complyTest: {
|
|
804
|
+
// Per-request gate. Return true to allow; anything else (including
|
|
805
|
+
// throws) denies with FORBIDDEN. Production agents typically gate
|
|
806
|
+
// registration itself on `process.env.ADCP_SANDBOX === '1'` rather
|
|
807
|
+
// than relying on this — the helper logs a loud warning if registered
|
|
808
|
+
// ungated AND without an env flag.
|
|
809
|
+
sandboxGate: input => input.auth?.sandbox === true,
|
|
810
|
+
|
|
811
|
+
// Seed adapters — pre-populate fixtures the storyboard references by
|
|
812
|
+
// stable ID. Re-seeding the same id+fixture is idempotent; divergent
|
|
813
|
+
// fixture for the same id rejects with INVALID_PARAMS.
|
|
814
|
+
seed: {
|
|
815
|
+
product: async params => productRepo.upsert(params.product_id, params.fixture),
|
|
816
|
+
creative: async params => creativeRepo.upsert(params.creative_id, params.fixture),
|
|
817
|
+
// pricing_option, plan, media_buy similarly
|
|
818
|
+
},
|
|
819
|
+
|
|
820
|
+
// Force adapters — state transitions. Throw
|
|
821
|
+
// TestControllerError('INVALID_TRANSITION', ...) when the state
|
|
822
|
+
// machine disallows the transition (production state machine should
|
|
823
|
+
// be the source of truth; the controller doesn't enforce its own).
|
|
824
|
+
force: {
|
|
825
|
+
creative_status: async params =>
|
|
826
|
+
creativeRepo.transition(params.creative_id, params.status, params.rejection_reason),
|
|
827
|
+
media_buy_status: async params => buyRepo.transition(params.media_buy_id, params.status, params.rejection_reason),
|
|
828
|
+
// account_status, session_status similarly
|
|
829
|
+
},
|
|
830
|
+
|
|
831
|
+
// Simulate adapters — synthetic delivery / budget data, no real
|
|
832
|
+
// upstream side effects. Storyboard validates the shape downstream
|
|
833
|
+
// tools return when fed the simulated state.
|
|
834
|
+
simulate: {
|
|
835
|
+
delivery: async params => deliverySim.run(params),
|
|
836
|
+
budget_spend: async params => budgetSim.spendTo(params.spend_percentage, params),
|
|
837
|
+
},
|
|
838
|
+
},
|
|
839
|
+
});
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
**Capability declaration is required.** Set `capabilities.compliance_testing = {}` on your platform — the framework projects the discovery block to `get_adcp_capabilities` so harnesses know which scenarios you support. The framework auto-derives `scenarios` from which adapters you supplied; explicit `compliance_testing.scenarios = [...]` is optional (use it to advertise a narrower or wider list than auto-derivation).
|
|
843
|
+
|
|
844
|
+
```ts
|
|
845
|
+
class MyPlatform implements DecisioningPlatform {
|
|
846
|
+
capabilities = {
|
|
847
|
+
specialisms: ['sales-non-guaranteed'] as const,
|
|
848
|
+
// ...
|
|
849
|
+
compliance_testing: {}, // empty block declares support; framework derives scenarios
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
The framework throws `PlatformConfigError` at construction if:
|
|
855
|
+
|
|
856
|
+
- `capabilities.compliance_testing` is declared but `complyTest` is omitted (capability without implementation), OR
|
|
857
|
+
- `complyTest` is supplied but `capabilities.compliance_testing` is undeclared (implementation without discovery field).
|
|
858
|
+
|
|
859
|
+
**Sandbox-only.** The spec says `comply_test_controller` MUST NOT be exposed in production. Three guard layers:
|
|
860
|
+
|
|
861
|
+
1. **Gate registration**: only call `createAdcpServerFromPlatform({ ..., complyTest })` when `process.env.ADCP_SANDBOX === '1'` or your equivalent sandbox flag is set. Production builds never register the tool at all.
|
|
862
|
+
2. **Per-request gate**: `complyTest.sandboxGate(input)` runs on every request; return `false` to deny with `FORBIDDEN`.
|
|
863
|
+
3. **Transport-layer isolation**: production deployments often expose the sandbox endpoint on a separate URL with separate auth.
|
|
864
|
+
|
|
865
|
+
The helper warns once per construction if it's registered without a `sandboxGate` AND without `ADCP_SANDBOX=1` / `ADCP_COMPLY_CONTROLLER_UNGATED=1` to silence the warning in setups where transport isolation handles it.
|
|
866
|
+
|
|
867
|
+
## Custom webhook emitter
|
|
868
|
+
|
|
869
|
+
Default behavior: when the host wires `webhooks` on `serve()`, the framework binds the per-request `ctx.emitWebhook` to a signed RFC 9421 path. You don't need a custom emitter unless you want a different retry policy, a different signing key for task webhooks vs. status-change webhooks, or a fake for tests.
|
|
870
|
+
|
|
871
|
+
```ts
|
|
872
|
+
createAdcpServerFromPlatform(platform, {
|
|
873
|
+
name: 'My Ad Network',
|
|
874
|
+
version: '1.0.0',
|
|
875
|
+
taskWebhookEmitter: {
|
|
876
|
+
emit: async ({ url, payload, operation_id }) => {
|
|
877
|
+
// Your custom delivery — must sign per RFC 9421 if you claim
|
|
878
|
+
// signed-requests. Or delegate to ctx.emitWebhook (use the default
|
|
879
|
+
// path) if you only need to wrap with logging.
|
|
880
|
+
return await mySigningEmitter.deliver(url, payload, operation_id);
|
|
881
|
+
},
|
|
882
|
+
// Required acknowledgment when your emitter does NOT sign:
|
|
883
|
+
// unsigned: true, // for dev/test fakes
|
|
884
|
+
},
|
|
885
|
+
});
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
**Signing posture is your responsibility.** If your platform claims `signed-requests` and you wire a custom emitter without `unsigned: true`, the framework warns at construction (in non-test envs) — buyers who verify signatures will reject your unsigned webhooks. Either delegate to the framework's signing pipeline or set `unsigned: true` to acknowledge dev/test usage. Set `ADCP_DECISIONING_ALLOW_UNSIGNED_TEST_EMITTER=1` to silence the warn for staging environments where signing isn't yet wired.
|
|
889
|
+
|
|
890
|
+
### DNS rebinding — production hardening
|
|
891
|
+
|
|
892
|
+
The framework's URL validator (`validatePushNotificationUrl`) rejects RFC 1918, loopback, link-local, CGNAT, IPv6 ULA, and IPv4-mapped IPv6 addresses against the LITERAL hostname in the URL. This catches the common case but does NOT defeat DNS rebinding: a buyer registers `https://rebind.attacker.com/`, validation passes (literal host isn't in any private range), then the A-record TTL flips to `169.254.169.254` between validate and fetch — the metadata service receives the signed payload.
|
|
893
|
+
|
|
894
|
+
Two production-grade mitigations:
|
|
895
|
+
|
|
896
|
+
1. **Egress proxy with allowlist** (deployment-side, simplest). Pin all outbound webhook traffic through a forward proxy that only allows explicitly listed destinations. Standard hosting practice; framework doesn't need to know.
|
|
897
|
+
2. **Pin-and-bind custom fetch** (SDK-side). Wire a custom `fetch` on `createWebhookEmitter({ webhooks })` that resolves DNS at request time, re-validates the resolved IP against the SSRF rules, and opens the TCP/TLS connection to that specific IP with the original `Host:` header preserved. Tracking issue [adcp-client#1038](https://github.com/adcontextprotocol/adcp-client/issues/1038) — v6.1 ships this as the default.
|
|
898
|
+
|
|
899
|
+
Until v6.1, adopters running in security-sensitive environments (handling buyer-supplied URLs from untrusted principals) MUST do one of the above. The SDK's literal-hostname check is correctness, not security-against-rebinding.
|
|
900
|
+
|
|
901
|
+
## Migrating from v5.x handler-style — the merge seam
|
|
902
|
+
|
|
903
|
+
If you have a v5.x agent built on `createAdcpServer({ mediaBuy: { ... } })`, you don't need to rewrite all of it before adopting v6.0. `createAdcpServerFromPlatform` accepts the v5 handler-style domains (`mediaBuy`, `creative`, `accounts`, `eventTracking`, `signals`, `governance`, `brandRights`, `sponsoredIntelligence`) as `opts` alongside the v6 platform interface. Platform-derived handlers WIN per-key; adopter handlers fill gaps for tools the platform doesn't yet model. Migrate one specialism at a time.
|
|
904
|
+
|
|
905
|
+
**`mergeSeam: 'strict'` is the recommended default during migration.** Set it from the first commit of your migration PR and keep it on through GA — `'strict'` throws `PlatformConfigError` at construction time when the v6 platform interface and your v5 leftover handlers collide, so you find shadowed overrides in CI rather than as silent runtime UNSUPPORTED_FEATURE responses. The default is `'warn'` only for back-compat with adopters who upgraded mid-flight; `'strict'` is the right posture for new deployments. Adopters who deferred this almost always wished they hadn't — by the time a collision shows up in production logs, the affected request has already failed in some confusing way.
|
|
906
|
+
|
|
907
|
+
The seam logs a warning when an adopter handler is shadowed by a platform-derived one — the failure mode where v6.x adds a tool to a specialism interface and your prior merge-seam override silently stops running on next deploy. Pick a `mergeSeam` mode based on your environment:
|
|
908
|
+
|
|
909
|
+
| Mode | When to pick |
|
|
910
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
911
|
+
| `'warn'` (default) | Local dev, mid-migration. Logs every collision at construction. |
|
|
912
|
+
| `'log-once'` | Multi-tenant host running N constructions per process / hot-reload dev. Logs the first time each `(domain, keys)` collision is seen, then suppresses repeats. |
|
|
913
|
+
| `'strict'` | CI / new deployments. Throws `PlatformConfigError` so the build fails before silent regression ships. |
|
|
914
|
+
| `'silent'` | Intentional override — you've audited the collision and the platform-derived handler is correct; suppress the noise. |
|
|
915
|
+
|
|
916
|
+
CI vs. local-dev side-by-side:
|
|
917
|
+
|
|
918
|
+
```ts
|
|
919
|
+
// CI / new deployments — fail the build on silent migration regression.
|
|
920
|
+
createAdcpServerFromPlatform(platform, {
|
|
921
|
+
name: 'My Ad Network',
|
|
922
|
+
version: '1.0.0',
|
|
923
|
+
mergeSeam: 'strict',
|
|
924
|
+
mediaBuy: { listCreativeFormats, providePerformanceFeedback /* ... */ },
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
// Local dev / hot-reload — see every collision in the log, never crash.
|
|
928
|
+
createAdcpServerFromPlatform(platform, {
|
|
929
|
+
name: 'My Ad Network',
|
|
930
|
+
version: '1.0.0',
|
|
931
|
+
mergeSeam: process.env.NODE_ENV === 'production' ? 'log-once' : 'warn',
|
|
932
|
+
mediaBuy: { listCreativeFormats, providePerformanceFeedback /* ... */ },
|
|
933
|
+
});
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
## Observability hooks
|
|
937
|
+
|
|
938
|
+
Wire any telemetry backend (DataDog / Prometheus / OpenTelemetry / structured logger) via the framework's `observability` hooks:
|
|
939
|
+
|
|
940
|
+
```ts
|
|
941
|
+
const server = createAdcpServerFromPlatform(platform, {
|
|
942
|
+
name: 'My Ad Network',
|
|
943
|
+
version: '1.0.0',
|
|
944
|
+
observability: {
|
|
945
|
+
onAccountResolve: ({ tool, durationMs, resolved, fromAuth }) => {
|
|
946
|
+
// accountId is also present when resolved=true; pre-bucket if you forward it
|
|
947
|
+
// (high tenant counts will explode metric tag cardinality).
|
|
948
|
+
metrics.histogram('adcp.account_resolve.ms', durationMs, { tool, fromAuth, resolved: String(resolved) });
|
|
949
|
+
},
|
|
950
|
+
onTaskCreate: ({ tool, accountId, durationMs }) => {
|
|
951
|
+
metrics.histogram('adcp.task.create.ms', durationMs, { tool });
|
|
952
|
+
},
|
|
953
|
+
onTaskTransition: ({ tool, status, durationMs, errorCode }) => {
|
|
954
|
+
// errorCode is bucketed (ErrorCode enum + framework-synthetic
|
|
955
|
+
// 'REGISTRY_WRITE_FAILED'). Safe to use as a metric tag.
|
|
956
|
+
metrics.histogram('adcp.task.duration_ms', durationMs, { tool, status, errorCode: errorCode ?? 'none' });
|
|
957
|
+
},
|
|
958
|
+
onWebhookEmit: ({ tool, status, success, durationMs, errorCode }) => {
|
|
959
|
+
// errorCode is bucketed (TIMEOUT/CONNECTION_REFUSED/HTTP_4XX/HTTP_5XX/
|
|
960
|
+
// SIGNATURE_FAILURE/UNKNOWN). Don't tag on errorMessages — free-text.
|
|
961
|
+
metrics.histogram('adcp.webhook.duration_ms', durationMs, {
|
|
962
|
+
tool,
|
|
963
|
+
status,
|
|
964
|
+
success: String(success),
|
|
965
|
+
errorCode: errorCode ?? 'none',
|
|
966
|
+
});
|
|
967
|
+
},
|
|
968
|
+
onStatusChangePublish: ({ resourceType }) => {
|
|
969
|
+
metrics.increment('adcp.status_change', { resourceType });
|
|
970
|
+
},
|
|
971
|
+
},
|
|
972
|
+
});
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
Hooks are throw-safe — adopter callback exceptions are caught and logged via the framework logger; they never break dispatch. Per-tool dispatch latency hooks (`onDispatchStart` / `onDispatchEnd`) land in v6.1 with the per-handler instrumentation pass; an opt-in `@adcp/sdk/telemetry/otel` peer-dep adapter ships with AdCP-aligned span / metric names.
|
|
976
|
+
|
|
977
|
+
## Reference
|
|
978
|
+
|
|
979
|
+
- Worked example: [`examples/decisioning-platform-mock-seller.ts`](../../examples/decisioning-platform-mock-seller.ts)
|
|
980
|
+
- Integration tests: [`test/server-decisioning-mock-seller.test.js`](../../test/server-decisioning-mock-seller.test.js)
|
|
981
|
+
- Design doc: [`docs/proposals/decisioning-platform-v1.md`](../../docs/proposals/decisioning-platform-v1.md)
|
|
982
|
+
- MCP+A2A serving: [`docs/proposals/mcp-a2a-unified-serving.md`](../../docs/proposals/mcp-a2a-unified-serving.md)
|
|
983
|
+
- Migration sketches: `docs/proposals/decisioning-platform-{training-agent,gam,scope3,prebid}-migration.md`
|
|
984
|
+
|
|
985
|
+
## What's not in v6.0 alpha
|
|
986
|
+
|
|
987
|
+
- Public `./server` export — `./server/decisioning` is preview-only; subject to change before v6.0 GA
|
|
988
|
+
- Native MCP `tasks/get` method dispatch (we ship `tasks_get` snake-case as a tool today; native method dispatch via the MCP SDK's `registerToolTask` lands in v6.1, supporting both surfaces)
|
|
989
|
+
- `ctx.runAsync` `maxAutoAwaitMs` cap with AbortSignal cancellation
|
|
990
|
+
- `getCapabilitiesFor(account)` per-tenant runtime
|
|
991
|
+
- `taskRegistry.transition()` for adopter-emitted intermediate states (`working`, `input-required`, `auth-required`) — v6.0 framework writes only `submitted`/`completed`/`failed`; the Postgres registry CHECK widens in v6.1
|