From d0ac6fcd94852fba72715e4c5e45e6add7bb85b6 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 8 Apr 2026 05:23:46 -0400 Subject: [PATCH 1/6] fix: storyboard validation failures against training agent - Add creative and account capability blocks to get_adcp_capabilities response so capability_discovery validation passes (#1990) - Increase training agent rate limit from 60 to 300 req/min to prevent cascading failures during bulk storyboard evaluation (#1991, #1992, #1994) - Add brand_rights and capability_discovery storyboard YAMLs (#1993, #1992) - Add brand compliance track to TRACK_SCENARIOS infrastructure (#1993) - Update storyboard test to accept core/ and brand/ schema_ref prefixes Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/fix-client-validation.md | 4 + docs/storyboards/brand_rights.yaml | 135 ++++++++++++++++++ docs/storyboards/capability_discovery.yaml | 60 ++++++++ .../src/addie/services/compliance-testing.ts | 8 +- server/src/training-agent/index.ts | 6 +- server/src/training-agent/task-handlers.ts | 14 +- server/tests/unit/storyboards.test.ts | 2 +- 7 files changed, 224 insertions(+), 5 deletions(-) create mode 100644 .changeset/fix-client-validation.md create mode 100644 docs/storyboards/brand_rights.yaml create mode 100644 docs/storyboards/capability_discovery.yaml diff --git a/.changeset/fix-client-validation.md b/.changeset/fix-client-validation.md new file mode 100644 index 0000000000..a996cd4e73 --- /dev/null +++ b/.changeset/fix-client-validation.md @@ -0,0 +1,4 @@ +--- +--- + +Fix storyboard validation failures against training agent: add creative/account blocks to get_adcp_capabilities response (#1990), increase rate limit from 60 to 300 req/min to prevent cascading failures during bulk evaluation (#1991, #1992, #1994), add brand_rights and capability_discovery storyboard YAMLs (#1993), and add brand compliance track infrastructure. diff --git a/docs/storyboards/brand_rights.yaml b/docs/storyboards/brand_rights.yaml new file mode 100644 index 0000000000..b6e4c11bef --- /dev/null +++ b/docs/storyboards/brand_rights.yaml @@ -0,0 +1,135 @@ +id: brand_rights +version: "1.0.0" +title: "Brand rights management" +category: brand +summary: "Buyer discovers brand identity, acquires usage rights, and manages creative approvals." + +narrative: | + Before running campaigns with a brand's assets, buyer agents need to understand the + brand's identity guidelines and acquire proper usage rights. This storyboard covers + the brand protocol — discovering brand identity, negotiating rights, and getting + creative approval. + + Think of brand agents as representing the brand owner (e.g., Nova Motors' brand ops + team). They control identity guidelines, manage who can use the brand, and approve + creative executions before they go live. + +agent: + interaction_model: brand_guardian + capabilities: + - brand_identity + - rights_management + examples: + - "Nova Motors brand agent" + - "Brand safety platform" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The buyer has a campaign brief that requires using a specific brand's assets + and identity. The brand agent controls access to brand guidelines, logos, colors, + and creative approval workflows. + +phases: + - id: identity_discovery + title: "Discover brand identity" + narrative: | + The buyer connects to the brand agent to learn about the brand's visual identity, + tone guidelines, and available assets. This is the starting point for any + brand-compliant creative work. + + steps: + - id: get_identity + title: "Get brand identity and guidelines" + narrative: | + The buyer calls get_brand_identity to retrieve the brand's visual identity, + tone guidelines, logo assets, and color palette. This information drives + creative production and compliance checks. + task: get_brand_identity + schema_ref: "brand/get-brand-identity-request.json" + response_schema_ref: "brand/get-brand-identity-response.json" + doc_ref: "/brand/task-reference/get_brand_identity" + comply_scenario: brand_identity + stateful: false + expected: | + Return brand identity information including: + - Brand name and description + - Visual guidelines (colors, logos, typography) + - Tone and voice guidelines + - Available asset references + + validations: + - check: response_schema + description: "Response matches brand identity schema" + + - id: rights_acquisition + title: "Acquire usage rights" + narrative: | + The buyer needs permission to use the brand's assets in their campaign. They + check existing rights, acquire new ones if needed, and manage the lifecycle + of their usage agreements. + + steps: + - id: check_rights + title: "Check existing rights" + narrative: | + The buyer calls get_rights to see what usage rights they already hold + for this brand. This might return active licenses, expired agreements, + or indicate no existing relationship. + task: get_rights + schema_ref: "brand/get-rights-request.json" + response_schema_ref: "brand/get-rights-response.json" + doc_ref: "/brand/task-reference/get_rights" + comply_scenario: brand_rights_flow + stateful: false + expected: | + Return current rights status for the requesting agent, including: + - Active rights with scope and expiration + - Any restrictions or conditions + + validations: + - check: response_schema + description: "Response matches rights schema" + + - id: acquire_new_rights + title: "Acquire usage rights" + narrative: | + The buyer requests new usage rights for a campaign. The brand agent + evaluates the request against its policies and grants or denies access. + task: acquire_rights + schema_ref: "brand/acquire-rights-request.json" + response_schema_ref: "brand/acquire-rights-response.json" + doc_ref: "/brand/task-reference/acquire_rights" + comply_scenario: brand_rights_flow + stateful: true + expected: | + Return a rights grant with: + - Rights ID for future reference + - Scope of permitted usage + - Expiration or renewal terms + - Any conditions or restrictions + + validations: + - check: response_schema + description: "Response matches acquire rights schema" + + - id: update_existing_rights + title: "Update rights scope" + narrative: | + The buyer needs to modify existing rights — perhaps extending the duration, + adding channels, or changing the campaign scope. + task: update_rights + schema_ref: "brand/update-rights-request.json" + response_schema_ref: "brand/update-rights-response.json" + doc_ref: "/brand/task-reference/update_rights" + comply_scenario: brand_rights_flow + stateful: true + expected: | + Return the updated rights with new scope, reflecting the requested changes. + + validations: + - check: response_schema + description: "Response matches update rights schema" diff --git a/docs/storyboards/capability_discovery.yaml b/docs/storyboards/capability_discovery.yaml new file mode 100644 index 0000000000..4123e67585 --- /dev/null +++ b/docs/storyboards/capability_discovery.yaml @@ -0,0 +1,60 @@ +id: capability_discovery +version: "1.0.0" +title: "Capability discovery" +category: core +summary: "Buyer discovers seller capabilities, protocols, and creative features before interacting." + +narrative: | + Before a buyer agent can do anything useful, it needs to understand what you support. + Capability discovery is the first thing a buyer does when connecting to a new seller — + which protocols do you implement? What creative features? Do you require accounts? + + This storyboard walks through the v3 capability discovery flow. The buyer calls + get_adcp_capabilities and uses the response to decide which workflows to attempt. + +agent: + interaction_model: any + capabilities: [] + examples: + - "Any AdCP v3 agent" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +phases: + - id: discover + title: "Discover agent capabilities" + narrative: | + The buyer connects and calls get_adcp_capabilities to learn what the agent supports. + The response declares protocol support, creative features, account requirements, + and version information. This determines which subsequent workflows are available. + + steps: + - id: get_capabilities + title: "Get agent capabilities" + narrative: | + The buyer calls get_adcp_capabilities. The response tells the buyer which + protocols are supported (media_buy, creative, governance, signals), what + creative capabilities exist (generation, transformation, library), and whether + accounts are required before product discovery. + task: get_adcp_capabilities + schema_ref: "core/get-adcp-capabilities-request.json" + response_schema_ref: "core/get-adcp-capabilities-response.json" + doc_ref: "/protocol/task-reference/get_adcp_capabilities" + comply_scenario: capability_discovery + stateful: false + expected: | + Return a capabilities object with: + - adcp.major_versions including 3 + - supported_protocols listing implemented protocols + - creative block if creative protocol is supported + - account block describing auth and sandbox status + + validations: + - check: field_present + path: "adcp.major_versions" + description: "Response declares supported AdCP versions" + - check: field_present + path: "supported_protocols" + description: "Response lists supported protocols" diff --git a/server/src/addie/services/compliance-testing.ts b/server/src/addie/services/compliance-testing.ts index cdab0aa9e6..c8dc7a97ca 100644 --- a/server/src/addie/services/compliance-testing.ts +++ b/server/src/addie/services/compliance-testing.ts @@ -19,7 +19,8 @@ export type ComplianceTrack = | 'governance' | 'signals' | 'si' - | 'audiences'; + | 'audiences' + | 'brand'; export type PlatformType = | 'display_ad_server' @@ -114,6 +115,7 @@ const TRACK_LABELS: Record = { signals: 'Signals', si: 'Sponsored intelligence', audiences: 'Audience sync', + brand: 'Brand protocol', }; export const TRACK_SCENARIOS: Record = { @@ -133,6 +135,10 @@ export const TRACK_SCENARIOS: Record = { signals: ['signals_flow'], si: ['si_session_lifecycle', 'si_availability', 'si_handoff'], audiences: ['sync_audiences'], + // Brand scenarios (brand_identity, brand_rights_flow, creative_approval) are + // pending addition to @adcp/client — see adcontextprotocol/adcp#1993. + // Until then the brand track will always show as 'skip'. + brand: [] as TestScenario[], }; export const SAMPLE_BRIEFS: SampleBrief[] = [ diff --git a/server/src/training-agent/index.ts b/server/src/training-agent/index.ts index 880518cef0..e9c131d408 100644 --- a/server/src/training-agent/index.ts +++ b/server/src/training-agent/index.ts @@ -175,10 +175,12 @@ export function createTrainingAgentRouter(): Router { res.status(204).end(); }); - // Rate limiting: 60 requests/minute per IP (in-memory, no DB dependency) + // Rate limiting: 300 requests/minute per IP (in-memory, no DB dependency). + // The training agent is a sandbox — bulk storyboard evaluation runs ~10 MCP + // calls per storyboard, so 21 storyboards need ~210 calls within a short window. const mcpRateLimiter = rateLimit({ windowMs: 60 * 1000, - max: 60, + max: 300, standardHeaders: true, legacyHeaders: false, validate: { xForwardedForHeader: false, ip: false }, diff --git a/server/src/training-agent/task-handlers.ts b/server/src/training-agent/task-handlers.ts index 8f040d96be..e90a562368 100644 --- a/server/src/training-agent/task-handlers.ts +++ b/server/src/training-agent/task-handlers.ts @@ -1783,7 +1783,7 @@ function handleUpdateMediaBuy(args: ToolArgs, ctx: TrainingContext) { return result; } -function handleGetAdcpCapabilities(_args: ToolArgs, _ctx: TrainingContext): { adcp: { major_versions: number[] }; supported_protocols: string[]; protocol_version: string; tasks: string[]; media_buy: unknown; agent: { name: string; description: string } } { +function handleGetAdcpCapabilities(_args: ToolArgs, _ctx: TrainingContext): Record { const tasks = TOOLS .map(t => t.name) .filter(name => name !== 'get_adcp_capabilities'); @@ -1801,6 +1801,18 @@ function handleGetAdcpCapabilities(_args: ToolArgs, _ctx: TrainingContext): { ad channels, }, }, + creative: { + supports_generation: true, + supports_transformation: true, + supports_compliance: false, + has_creative_library: true, + }, + account: { + require_operator_auth: false, + required_for_products: false, + sandbox: true, + supported_billing: [], + }, agent: { name: 'AdCP Training Agent', description: 'Training agent for AdCP protocol testing and certification', diff --git a/server/tests/unit/storyboards.test.ts b/server/tests/unit/storyboards.test.ts index b5d4dac999..3c2d64f6bf 100644 --- a/server/tests/unit/storyboards.test.ts +++ b/server/tests/unit/storyboards.test.ts @@ -146,7 +146,7 @@ describe('getStoryboard', () => { it('schema_ref paths point to known schema directories', () => { const storyboards = listStoryboards(); - const validPrefixes = ['creative/', 'media-buy/', 'account/', 'governance/', 'signals/']; + const validPrefixes = ['creative/', 'media-buy/', 'account/', 'governance/', 'signals/', 'core/', 'brand/']; for (const summary of storyboards) { const sb = getStoryboard(summary.id)!; for (const phase of sb.phases) { From e304299ebff7ef4d322f14992a28cf165132a0f4 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 8 Apr 2026 05:35:24 -0400 Subject: [PATCH 2/6] fix: address code review and security review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update stale rate limit comment (60→300 req/min) in task TTL calculation - Add 'brand' to supported_protocols since training agent implements brand tools - Update test expectation for supported_protocols Co-Authored-By: Claude Opus 4.6 (1M context) --- server/src/training-agent/task-handlers.ts | 4 ++-- server/tests/unit/training-agent.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/training-agent/task-handlers.ts b/server/src/training-agent/task-handlers.ts index e90a562368..55664f6cfa 100644 --- a/server/src/training-agent/task-handlers.ts +++ b/server/src/training-agent/task-handlers.ts @@ -1790,7 +1790,7 @@ function handleGetAdcpCapabilities(_args: ToolArgs, _ctx: TrainingContext): Reco const channels = [...new Set(PUBLISHERS.flatMap(p => p.channels))].sort(); return { adcp: { major_versions: [3] }, - supported_protocols: ['media_buy', 'creative', 'governance', 'signals'], + supported_protocols: ['media_buy', 'creative', 'governance', 'signals', 'brand'], protocol_version: '3.0', tasks, media_buy: { @@ -2539,7 +2539,7 @@ export function createTrainingAgentServer(ctx: TrainingContext): Server { // Training agent tasks resolve immediately, so moderate TTLs suffice. // 15 minutes gives developers time to inspect tasks while debugging. - // With the rate limiter (60 req/min) this caps live tasks at ~900. + // With the rate limiter (300 req/min) this caps live tasks at ~4,500. const MAX_TASK_TTL = 15 * 60 * 1000; // 15 minutes const DEFAULT_TASK_TTL = 15 * 60 * 1000; // 15 minutes const clampedTtl = Math.min(taskField?.ttl ?? DEFAULT_TASK_TTL, MAX_TASK_TTL); diff --git a/server/tests/unit/training-agent.test.ts b/server/tests/unit/training-agent.test.ts index 3cbb0a9e19..24a3aab3a6 100644 --- a/server/tests/unit/training-agent.test.ts +++ b/server/tests/unit/training-agent.test.ts @@ -3464,7 +3464,7 @@ describe('get_adcp_capabilities handler', () => { expect(result.adcp).toEqual({ major_versions: [3] }); expect(result.protocol_version).toBe('3.0'); - expect(result.supported_protocols).toEqual(['media_buy', 'creative', 'governance', 'signals']); + expect(result.supported_protocols).toEqual(['media_buy', 'creative', 'governance', 'signals', 'brand']); }); it('lists protocol tasks without get_adcp_capabilities itself', async () => { From 2df3fc115041bfa922c85ac78979400cdf35e4a2 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 8 Apr 2026 05:52:48 -0400 Subject: [PATCH 3/6] fix: training agent schema compliance for creative handlers - Add created_date and updated_date to list_creatives response (required by schema) - Add include_snapshot param to list_creatives tool schema and return snapshot_unavailable_reason when requested - Fix build_creative assets format: use object map with content field instead of array with html field (matches creative-manifest.json schema) - Add generative build mode (target_format_id only, no manifest/library) since training agent declares supports_generation: true - Fix preview_creative render format: use render_id, output_format, role, preview_url/preview_html per preview-render.json schema Co-Authored-By: Claude Opus 4.6 (1M context) --- server/src/training-agent/task-handlers.ts | 73 ++++++++++++++-------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/server/src/training-agent/task-handlers.ts b/server/src/training-agent/task-handlers.ts index 55664f6cfa..b4587168a0 100644 --- a/server/src/training-agent/task-handlers.ts +++ b/server/src/training-agent/task-handlers.ts @@ -538,6 +538,7 @@ const TOOLS = [ account: ACCOUNT_REF_SCHEMA, creative_ids: { type: 'array', items: { type: 'string' } }, media_buy_id: { type: 'string' }, + include_snapshot: { type: 'boolean', description: 'Include delivery snapshot per creative' }, }, }, }, @@ -1568,7 +1569,7 @@ function handleSyncCreatives(args: ToolArgs, ctx: TrainingContext) { } function handleListCreatives(args: ToolArgs, ctx: TrainingContext) { - const req = args as unknown as ListCreativesRequest & ToolArgs & { creative_ids?: string[] }; + const req = args as unknown as ListCreativesRequest & ToolArgs & { creative_ids?: string[]; include_snapshot?: boolean }; const session = getSession(sessionKeyFromArgs(req, ctx.mode, ctx.userId, ctx.moduleId)); const filterIds = req.creative_ids || req.filters?.creative_ids; @@ -1592,6 +1593,11 @@ function handleListCreatives(args: ToolArgs, ctx: TrainingContext) { name: c.name, status: c.status, synced_at: c.syncedAt, + created_date: c.syncedAt, + updated_date: c.syncedAt, + ...(req.include_snapshot && { + snapshot_unavailable_reason: 'SNAPSHOT_UNSUPPORTED', + }), })), sandbox: true, }; @@ -2215,6 +2221,10 @@ function getDimensions(format: { renders: Array> } | und return { w: dims?.width || 300, h: dims?.height || 250 }; } +function buildHtmlAssets(html: string): Record { + return { serving_tag: { content: html } }; +} + function handleBuildCreative(args: ToolArgs, ctx: TrainingContext) { const req = args as unknown as BuildCreativeArgs; const session = getSession(sessionKeyFromArgs(req as unknown as ToolArgs, ctx.mode, ctx.userId, ctx.moduleId)); @@ -2246,13 +2256,7 @@ function handleBuildCreative(args: ToolArgs, ctx: TrainingContext) { creative_manifest: { creative_id: req.creative_id, format_id: { agent_url: agentUrl, id: formatId.id }, - assets: [ - { - asset_id: 'serving_tag', - asset_type: 'html', - html: `\n
Ad: ${escapeHtmlAttr(creative.name || req.creative_id!)}
`, - }, - ], + assets: buildHtmlAssets(`\n
Ad: ${escapeHtmlAttr(creative.name || req.creative_id!)}
`), }, sandbox: true, }; @@ -2278,13 +2282,7 @@ function handleBuildCreative(args: ToolArgs, ctx: TrainingContext) { return { creative_manifest: { format_id: { agent_url: agentUrl, id: fmtId.id }, - assets: [ - { - asset_id: 'serving_tag', - asset_type: 'html', - html: `\n
Built: ${escapeHtmlAttr(fmtId.id)} (${w}x${h})
`, - }, - ], + assets: buildHtmlAssets(`\n
Built: ${escapeHtmlAttr(fmtId.id)} (${w}x${h})
`), }, }; }); @@ -2300,20 +2298,43 @@ function handleBuildCreative(args: ToolArgs, ctx: TrainingContext) { return { creative_manifest: { format_id: { agent_url: agentUrl, id: fmtId.id }, - assets: [ - { - asset_id: 'serving_tag', - asset_type: 'html', - html: `\n
Built: ${escapeHtmlAttr(fmtId.id)} (${w}x${h})
`, + assets: buildHtmlAssets(`\n
Built: ${escapeHtmlAttr(fmtId.id)} (${w}x${h})
`), + }, + sandbox: true, + }; + } + + // Mode 3: Generative build (target_format_id + message, no manifest or library creative) + if (targetIds.length > 0) { + if (targetIds.length > 1) { + const results = targetIds.map(fmtId => { + const format = validFormatIds.get(fmtId.id); + const { w, h } = getDimensions(format); + return { + creative_manifest: { + format_id: { agent_url: agentUrl, id: fmtId.id }, + assets: buildHtmlAssets(`\n
Generated: ${escapeHtmlAttr(fmtId.id)} (${w}x${h})
`), }, - ], + }; + }); + return { results, sandbox: true }; + } + + const fmtId = targetIds[0]; + const format = validFormatIds.get(fmtId.id); + const { w, h } = getDimensions(format); + + return { + creative_manifest: { + format_id: { agent_url: agentUrl, id: fmtId.id }, + assets: buildHtmlAssets(`\n
Generated: ${escapeHtmlAttr(fmtId.id)} (${w}x${h})
`), }, sandbox: true, }; } return { - errors: [{ code: 'INVALID_REQUEST', message: 'Provide either creative_id (library mode) or creative_manifest (transformation mode).' }], + errors: [{ code: 'INVALID_REQUEST', message: 'Provide creative_id (library mode), creative_manifest (transformation mode), or target_format_id (generative mode).' }], }; } @@ -2359,15 +2380,17 @@ function handlePreviewCreative(args: ToolArgs, ctx: TrainingContext) { const previewHtml = `Preview: ${escapeHtmlAttr(fmtId)}
${escapeHtmlAttr(creativeName)}
${escapeHtmlAttr(fmtId)} (${w}x${h})
AdCP Training Agent Preview
`; const render: Record = { + render_id: `preview_${fmtId}`, + output_format: outputFormat, + role: 'primary', dimensions: { width: w, height: h }, }; if (outputFormat === 'url' || outputFormat === 'both') { - // Generate a data URI as the preview URL (the training agent doesn't host files) - render.url = `data:text/html;base64,${Buffer.from(previewHtml).toString('base64')}`; + render.preview_url = `data:text/html;base64,${Buffer.from(previewHtml).toString('base64')}`; } if (outputFormat === 'html' || outputFormat === 'both') { - render.html = previewHtml; + render.preview_html = previewHtml; } return { From 07be08cbacd065dba3d8a3432adb9a760014e14e Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 8 Apr 2026 06:08:27 -0400 Subject: [PATCH 4/6] fix: address code review and security review findings (round 2) - Fix preview_creative response: add response_type, preview_id, input fields per preview-creative-response.json schema - Fix preview_creative: reject invalid format_ids with INVALID_FORMAT error - Fix BuildCreativeArgs/PreviewCreativeArgs: type assets as Record (object map) matching creative-manifest.json schema, not Array - Cap target_format_ids at 50 to prevent response amplification - Remove non-schema synced_at field from list_creatives response Co-Authored-By: Claude Opus 4.6 (1M context) --- server/src/training-agent/task-handlers.ts | 48 ++++++++++++++++------ 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/server/src/training-agent/task-handlers.ts b/server/src/training-agent/task-handlers.ts index b4587168a0..be4e4b1e96 100644 --- a/server/src/training-agent/task-handlers.ts +++ b/server/src/training-agent/task-handlers.ts @@ -1592,7 +1592,6 @@ function handleListCreatives(args: ToolArgs, ctx: TrainingContext) { format_id: c.formatId, name: c.name, status: c.status, - synced_at: c.syncedAt, created_date: c.syncedAt, updated_date: c.syncedAt, ...(req.include_snapshot && { @@ -2206,7 +2205,7 @@ function handleGetCreativeDelivery(args: ToolArgs, ctx: TrainingContext) { interface BuildCreativeArgs { account?: unknown; creative_id?: string; - creative_manifest?: { format_id?: FormatID; assets?: Array> }; + creative_manifest?: { format_id?: FormatID; assets?: Record | Array> }; target_format_id?: FormatID; target_format_ids?: FormatID[]; brand?: { domain?: string }; @@ -2232,9 +2231,10 @@ function handleBuildCreative(args: ToolArgs, ctx: TrainingContext) { const formats = getFormats(); const validFormatIds = new Map(formats.map(f => [f.format_id.id, f])); - // Determine target formats + // Determine target formats (cap at 50 to prevent response amplification) + const MAX_TARGET_FORMATS = 50; const targetIds: FormatID[] = req.target_format_ids?.length - ? req.target_format_ids + ? req.target_format_ids.slice(0, MAX_TARGET_FORMATS) : req.target_format_id ? [req.target_format_id] : []; @@ -2264,7 +2264,8 @@ function handleBuildCreative(args: ToolArgs, ctx: TrainingContext) { // Mode 2: Stateless transformation (creative_manifest + target_format_id) if (req.creative_manifest) { - const inputAssets = req.creative_manifest.assets || []; + const rawAssets = req.creative_manifest.assets; + const inputAssetCount = Array.isArray(rawAssets) ? rawAssets.length : Object.keys(rawAssets || {}).length; if (targetIds.length === 0) { // Use the manifest's own format_id if no target specified @@ -2298,7 +2299,7 @@ function handleBuildCreative(args: ToolArgs, ctx: TrainingContext) { return { creative_manifest: { format_id: { agent_url: agentUrl, id: fmtId.id }, - assets: buildHtmlAssets(`\n
Built: ${escapeHtmlAttr(fmtId.id)} (${w}x${h})
`), + assets: buildHtmlAssets(`\n
Built: ${escapeHtmlAttr(fmtId.id)} (${w}x${h})
`), }, sandbox: true, }; @@ -2343,9 +2344,9 @@ function handleBuildCreative(args: ToolArgs, ctx: TrainingContext) { interface PreviewCreativeArgs { account?: unknown; request_type?: 'single' | 'batch'; - creative_manifest?: { format_id?: FormatID; creative_id?: string; assets?: Array> }; + creative_manifest?: { format_id?: FormatID; creative_id?: string; assets?: Record }; creative_id?: string; - creatives?: Array<{ format_id?: FormatID; creative_id?: string; assets?: Array> }>; + creatives?: Array<{ format_id?: FormatID; creative_id?: string; assets?: Record }>; output_format?: 'url' | 'html' | 'both'; quality?: 'draft' | 'production'; } @@ -2359,7 +2360,7 @@ function handlePreviewCreative(args: ToolArgs, ctx: TrainingContext) { const outputFormat = req.output_format || 'url'; const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); - function buildPreview(manifest: { format_id?: FormatID; creative_id?: string; assets?: Array> }) { + function buildPreview(manifest: { format_id?: FormatID; creative_id?: string; assets?: Record }) { // Resolve format let formatId = manifest.format_id; let creativeName = 'Preview'; @@ -2375,6 +2376,9 @@ function handlePreviewCreative(args: ToolArgs, ctx: TrainingContext) { const fmtId = formatId?.id || 'display_300x250'; const format = validFormatIds.get(fmtId); + if (!format && formatId?.id) { + return null; // Signal invalid format to caller + } const { w, h } = getDimensions(format); const previewHtml = `Preview: ${escapeHtmlAttr(fmtId)}
${escapeHtmlAttr(creativeName)}
${escapeHtmlAttr(fmtId)} (${w}x${h})
AdCP Training Agent Preview
`; @@ -2394,16 +2398,24 @@ function handlePreviewCreative(args: ToolArgs, ctx: TrainingContext) { } return { - format_id: { agent_url: agentUrl, id: fmtId }, + preview_id: `preview_${fmtId}`, renders: [render], - expires_at: expiresAt, + input: { name: creativeName }, }; } // Batch mode if (req.request_type === 'batch' && req.creatives?.length) { return { - previews: req.creatives.map(buildPreview), + response_type: 'batch', + results: req.creatives.map(c => ({ + success: true, + creative_id: c.creative_id || 'unknown', + response: { + previews: [buildPreview(c)], + expires_at: expiresAt, + }, + })), sandbox: true, }; } @@ -2416,8 +2428,18 @@ function handlePreviewCreative(args: ToolArgs, ctx: TrainingContext) { }; } + const preview = buildPreview(manifest); + if (!preview) { + const fmtId = manifest.format_id?.id || 'unknown'; + return { + errors: [{ code: 'INVALID_FORMAT', message: `Format "${fmtId}" is not supported. Use list_creative_formats to discover available formats.` }], + }; + } + return { - previews: [buildPreview(manifest)], + response_type: 'single', + previews: [preview], + expires_at: expiresAt, sandbox: true, }; } From 96823fe247bc10bc575292e7e1c5ad7f64f49f0d Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 8 Apr 2026 06:14:36 -0400 Subject: [PATCH 5/6] refactor: scope PR to training agent fixes only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove storyboard YAMLs (brand_rights, capability_discovery) and brand compliance track — these belong in #1985 which covers full storyboard coverage and comply() migration. Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/fix-client-validation.md | 2 +- docs/storyboards/brand_rights.yaml | 135 ------------------ docs/storyboards/capability_discovery.yaml | 60 -------- .../src/addie/services/compliance-testing.ts | 8 +- server/tests/unit/storyboards.test.ts | 2 +- 5 files changed, 3 insertions(+), 204 deletions(-) delete mode 100644 docs/storyboards/brand_rights.yaml delete mode 100644 docs/storyboards/capability_discovery.yaml diff --git a/.changeset/fix-client-validation.md b/.changeset/fix-client-validation.md index a996cd4e73..81497e8665 100644 --- a/.changeset/fix-client-validation.md +++ b/.changeset/fix-client-validation.md @@ -1,4 +1,4 @@ --- --- -Fix storyboard validation failures against training agent: add creative/account blocks to get_adcp_capabilities response (#1990), increase rate limit from 60 to 300 req/min to prevent cascading failures during bulk evaluation (#1991, #1992, #1994), add brand_rights and capability_discovery storyboard YAMLs (#1993), and add brand compliance track infrastructure. +Training agent fixes: add creative/account/brand blocks to get_adcp_capabilities (#1990), increase rate limit 60→300 req/min (#1991, #1994), fix creative handler schema compliance (assets object map, generative build mode, preview response fields, list_creatives dates/snapshots). diff --git a/docs/storyboards/brand_rights.yaml b/docs/storyboards/brand_rights.yaml deleted file mode 100644 index b6e4c11bef..0000000000 --- a/docs/storyboards/brand_rights.yaml +++ /dev/null @@ -1,135 +0,0 @@ -id: brand_rights -version: "1.0.0" -title: "Brand rights management" -category: brand -summary: "Buyer discovers brand identity, acquires usage rights, and manages creative approvals." - -narrative: | - Before running campaigns with a brand's assets, buyer agents need to understand the - brand's identity guidelines and acquire proper usage rights. This storyboard covers - the brand protocol — discovering brand identity, negotiating rights, and getting - creative approval. - - Think of brand agents as representing the brand owner (e.g., Nova Motors' brand ops - team). They control identity guidelines, manage who can use the brand, and approve - creative executions before they go live. - -agent: - interaction_model: brand_guardian - capabilities: - - brand_identity - - rights_management - examples: - - "Nova Motors brand agent" - - "Brand safety platform" - -caller: - role: buyer_agent - example: "Pinnacle Agency (buyer)" - -prerequisites: - description: | - The buyer has a campaign brief that requires using a specific brand's assets - and identity. The brand agent controls access to brand guidelines, logos, colors, - and creative approval workflows. - -phases: - - id: identity_discovery - title: "Discover brand identity" - narrative: | - The buyer connects to the brand agent to learn about the brand's visual identity, - tone guidelines, and available assets. This is the starting point for any - brand-compliant creative work. - - steps: - - id: get_identity - title: "Get brand identity and guidelines" - narrative: | - The buyer calls get_brand_identity to retrieve the brand's visual identity, - tone guidelines, logo assets, and color palette. This information drives - creative production and compliance checks. - task: get_brand_identity - schema_ref: "brand/get-brand-identity-request.json" - response_schema_ref: "brand/get-brand-identity-response.json" - doc_ref: "/brand/task-reference/get_brand_identity" - comply_scenario: brand_identity - stateful: false - expected: | - Return brand identity information including: - - Brand name and description - - Visual guidelines (colors, logos, typography) - - Tone and voice guidelines - - Available asset references - - validations: - - check: response_schema - description: "Response matches brand identity schema" - - - id: rights_acquisition - title: "Acquire usage rights" - narrative: | - The buyer needs permission to use the brand's assets in their campaign. They - check existing rights, acquire new ones if needed, and manage the lifecycle - of their usage agreements. - - steps: - - id: check_rights - title: "Check existing rights" - narrative: | - The buyer calls get_rights to see what usage rights they already hold - for this brand. This might return active licenses, expired agreements, - or indicate no existing relationship. - task: get_rights - schema_ref: "brand/get-rights-request.json" - response_schema_ref: "brand/get-rights-response.json" - doc_ref: "/brand/task-reference/get_rights" - comply_scenario: brand_rights_flow - stateful: false - expected: | - Return current rights status for the requesting agent, including: - - Active rights with scope and expiration - - Any restrictions or conditions - - validations: - - check: response_schema - description: "Response matches rights schema" - - - id: acquire_new_rights - title: "Acquire usage rights" - narrative: | - The buyer requests new usage rights for a campaign. The brand agent - evaluates the request against its policies and grants or denies access. - task: acquire_rights - schema_ref: "brand/acquire-rights-request.json" - response_schema_ref: "brand/acquire-rights-response.json" - doc_ref: "/brand/task-reference/acquire_rights" - comply_scenario: brand_rights_flow - stateful: true - expected: | - Return a rights grant with: - - Rights ID for future reference - - Scope of permitted usage - - Expiration or renewal terms - - Any conditions or restrictions - - validations: - - check: response_schema - description: "Response matches acquire rights schema" - - - id: update_existing_rights - title: "Update rights scope" - narrative: | - The buyer needs to modify existing rights — perhaps extending the duration, - adding channels, or changing the campaign scope. - task: update_rights - schema_ref: "brand/update-rights-request.json" - response_schema_ref: "brand/update-rights-response.json" - doc_ref: "/brand/task-reference/update_rights" - comply_scenario: brand_rights_flow - stateful: true - expected: | - Return the updated rights with new scope, reflecting the requested changes. - - validations: - - check: response_schema - description: "Response matches update rights schema" diff --git a/docs/storyboards/capability_discovery.yaml b/docs/storyboards/capability_discovery.yaml deleted file mode 100644 index 4123e67585..0000000000 --- a/docs/storyboards/capability_discovery.yaml +++ /dev/null @@ -1,60 +0,0 @@ -id: capability_discovery -version: "1.0.0" -title: "Capability discovery" -category: core -summary: "Buyer discovers seller capabilities, protocols, and creative features before interacting." - -narrative: | - Before a buyer agent can do anything useful, it needs to understand what you support. - Capability discovery is the first thing a buyer does when connecting to a new seller — - which protocols do you implement? What creative features? Do you require accounts? - - This storyboard walks through the v3 capability discovery flow. The buyer calls - get_adcp_capabilities and uses the response to decide which workflows to attempt. - -agent: - interaction_model: any - capabilities: [] - examples: - - "Any AdCP v3 agent" - -caller: - role: buyer_agent - example: "Pinnacle Agency (buyer)" - -phases: - - id: discover - title: "Discover agent capabilities" - narrative: | - The buyer connects and calls get_adcp_capabilities to learn what the agent supports. - The response declares protocol support, creative features, account requirements, - and version information. This determines which subsequent workflows are available. - - steps: - - id: get_capabilities - title: "Get agent capabilities" - narrative: | - The buyer calls get_adcp_capabilities. The response tells the buyer which - protocols are supported (media_buy, creative, governance, signals), what - creative capabilities exist (generation, transformation, library), and whether - accounts are required before product discovery. - task: get_adcp_capabilities - schema_ref: "core/get-adcp-capabilities-request.json" - response_schema_ref: "core/get-adcp-capabilities-response.json" - doc_ref: "/protocol/task-reference/get_adcp_capabilities" - comply_scenario: capability_discovery - stateful: false - expected: | - Return a capabilities object with: - - adcp.major_versions including 3 - - supported_protocols listing implemented protocols - - creative block if creative protocol is supported - - account block describing auth and sandbox status - - validations: - - check: field_present - path: "adcp.major_versions" - description: "Response declares supported AdCP versions" - - check: field_present - path: "supported_protocols" - description: "Response lists supported protocols" diff --git a/server/src/addie/services/compliance-testing.ts b/server/src/addie/services/compliance-testing.ts index c8dc7a97ca..cdab0aa9e6 100644 --- a/server/src/addie/services/compliance-testing.ts +++ b/server/src/addie/services/compliance-testing.ts @@ -19,8 +19,7 @@ export type ComplianceTrack = | 'governance' | 'signals' | 'si' - | 'audiences' - | 'brand'; + | 'audiences'; export type PlatformType = | 'display_ad_server' @@ -115,7 +114,6 @@ const TRACK_LABELS: Record = { signals: 'Signals', si: 'Sponsored intelligence', audiences: 'Audience sync', - brand: 'Brand protocol', }; export const TRACK_SCENARIOS: Record = { @@ -135,10 +133,6 @@ export const TRACK_SCENARIOS: Record = { signals: ['signals_flow'], si: ['si_session_lifecycle', 'si_availability', 'si_handoff'], audiences: ['sync_audiences'], - // Brand scenarios (brand_identity, brand_rights_flow, creative_approval) are - // pending addition to @adcp/client — see adcontextprotocol/adcp#1993. - // Until then the brand track will always show as 'skip'. - brand: [] as TestScenario[], }; export const SAMPLE_BRIEFS: SampleBrief[] = [ diff --git a/server/tests/unit/storyboards.test.ts b/server/tests/unit/storyboards.test.ts index 3c2d64f6bf..b5d4dac999 100644 --- a/server/tests/unit/storyboards.test.ts +++ b/server/tests/unit/storyboards.test.ts @@ -146,7 +146,7 @@ describe('getStoryboard', () => { it('schema_ref paths point to known schema directories', () => { const storyboards = listStoryboards(); - const validPrefixes = ['creative/', 'media-buy/', 'account/', 'governance/', 'signals/', 'core/', 'brand/']; + const validPrefixes = ['creative/', 'media-buy/', 'account/', 'governance/', 'signals/']; for (const summary of storyboards) { const sb = getStoryboard(summary.id)!; for (const phase of sb.phases) { From 8edec583d389d197ae171ab401d857c1197bebf4 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 8 Apr 2026 06:30:38 -0400 Subject: [PATCH 6/6] fix: type training agent responses with @adcp/client types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Import BuildCreativeResponse, ListCreativesResponse, PreviewCreativeResponse, and CreativeManifest from @adcp/client and use them as return types. The compiler now catches shape mismatches at build time: - Removed creative_id from CreativeManifest (not in schema) - Changed multi-format response from { results } to { creative_manifests } - buildHtmlAssets returns AdcpCreativeManifest['assets'] type Previously, handlers returned untyped object literals — the MCP SDK wraps them in content[0].text as opaque strings, so TypeScript couldn't validate the domain payload shape. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/src/training-agent/task-handlers.ts | 30 ++++++++++------------ 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/server/src/training-agent/task-handlers.ts b/server/src/training-agent/task-handlers.ts index be4e4b1e96..2943e51f98 100644 --- a/server/src/training-agent/task-handlers.ts +++ b/server/src/training-agent/task-handlers.ts @@ -32,6 +32,10 @@ import type { ActivateSignalRequest, GetCreativeDeliveryRequest, GetAdCPCapabilitiesRequest, + BuildCreativeResponse, + ListCreativesResponse, + PreviewCreativeResponse, + CreativeManifest as AdcpCreativeManifest, } from '@adcp/client'; /** Escape HTML special characters to prevent injection in generated HTML responses. */ function escapeHtmlAttr(s: string): string { @@ -2220,11 +2224,11 @@ function getDimensions(format: { renders: Array> } | und return { w: dims?.width || 300, h: dims?.height || 250 }; } -function buildHtmlAssets(html: string): Record { +function buildHtmlAssets(html: string): AdcpCreativeManifest['assets'] { return { serving_tag: { content: html } }; } -function handleBuildCreative(args: ToolArgs, ctx: TrainingContext) { +function handleBuildCreative(args: ToolArgs, ctx: TrainingContext): BuildCreativeResponse & { sandbox?: boolean } { const req = args as unknown as BuildCreativeArgs; const session = getSession(sessionKeyFromArgs(req as unknown as ToolArgs, ctx.mode, ctx.userId, ctx.moduleId)); const agentUrl = getAgentUrl(); @@ -2254,7 +2258,6 @@ function handleBuildCreative(args: ToolArgs, ctx: TrainingContext) { return { creative_manifest: { - creative_id: req.creative_id, format_id: { agent_url: agentUrl, id: formatId.id }, assets: buildHtmlAssets(`\n
Ad: ${escapeHtmlAttr(creative.name || req.creative_id!)}
`), }, @@ -2276,19 +2279,16 @@ function handleBuildCreative(args: ToolArgs, ctx: TrainingContext) { // Generate output for each target format if (targetIds.length > 1) { // Multi-format response - const results = targetIds.map(fmtId => { + const creative_manifests = targetIds.map(fmtId => { const format = validFormatIds.get(fmtId.id); const { w, h } = getDimensions(format); - return { - creative_manifest: { - format_id: { agent_url: agentUrl, id: fmtId.id }, - assets: buildHtmlAssets(`\n
Built: ${escapeHtmlAttr(fmtId.id)} (${w}x${h})
`), - }, + format_id: { agent_url: agentUrl, id: fmtId.id }, + assets: buildHtmlAssets(`\n
Built: ${escapeHtmlAttr(fmtId.id)} (${w}x${h})
`), }; }); - return { results, sandbox: true }; + return { creative_manifests, sandbox: true }; } // Single format response @@ -2308,17 +2308,15 @@ function handleBuildCreative(args: ToolArgs, ctx: TrainingContext) { // Mode 3: Generative build (target_format_id + message, no manifest or library creative) if (targetIds.length > 0) { if (targetIds.length > 1) { - const results = targetIds.map(fmtId => { + const creative_manifests = targetIds.map(fmtId => { const format = validFormatIds.get(fmtId.id); const { w, h } = getDimensions(format); return { - creative_manifest: { - format_id: { agent_url: agentUrl, id: fmtId.id }, - assets: buildHtmlAssets(`\n
Generated: ${escapeHtmlAttr(fmtId.id)} (${w}x${h})
`), - }, + format_id: { agent_url: agentUrl, id: fmtId.id }, + assets: buildHtmlAssets(`\n
Generated: ${escapeHtmlAttr(fmtId.id)} (${w}x${h})
`), }; }); - return { results, sandbox: true }; + return { creative_manifests, sandbox: true }; } const fmtId = targetIds[0];