From b2c1267fb458ad1fed43e052dde885411dcc767d Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 16 Aug 2026 01:18:55 +0200 Subject: [PATCH 1/6] test(compliance): prove signal governance denial --- .changeset/prove-signal-governance-denial.md | 5 ++ package.json | 3 +- .../scenarios/governance_denied.yaml | 46 ++++++++---- tests/signal-governance-storyboard.test.cjs | 71 +++++++++++++++++++ 4 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 .changeset/prove-signal-governance-denial.md create mode 100644 tests/signal-governance-storyboard.test.cjs diff --git a/.changeset/prove-signal-governance-denial.md b/.changeset/prove-signal-governance-denial.md new file mode 100644 index 0000000000..3c126d1526 --- /dev/null +++ b/.changeset/prove-signal-governance-denial.md @@ -0,0 +1,5 @@ +--- +"adcontextprotocol": minor +--- + +Gate signal-activation governance conformance on the task-scoped `adcp.governance_enforcement` claim. The denied activation scenario now exercises an explicit paid activation, grades missing signed authorization, and verifies that rejection caused no platform-primary deployment call instead of treating a response echo as governance evidence. diff --git a/package.json b/package.json index f8b4d47122..f817d1d730 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "test:rights-attestations": "node --test --test-force-exit --test-timeout=30000 tests/rights-attestations.test.cjs", "test:governance-runtime-attestations": "node --test --test-force-exit --test-timeout=30000 tests/governance-runtime-attestations.test.cjs", "test:governance-conditions-storyboard": "node --test --test-force-exit --test-timeout=30000 tests/governance-conditions-storyboard.test.cjs", + "test:signal-governance-storyboard": "node --test --test-force-exit --test-timeout=30000 tests/signal-governance-storyboard.test.cjs", "test:audience-evidence": "node --test --test-force-exit --test-timeout=30000 tests/audience-evidence.test.cjs", "test:status-as-of-vectors": "node --test --test-force-exit --test-timeout=30000 tests/media-buy-status-as-of-vectors.test.cjs", "test:storyboard-scoping": "node --test --test-force-exit --test-timeout=30000 tests/lint-storyboard-scoping.test.cjs", @@ -129,7 +130,7 @@ "audit:oneof": "node scripts/audit-oneof.mjs", "test:schema-utf8": "node scripts/normalize-schema-utf8.mjs --check", "fix:schema-utf8": "node scripts/normalize-schema-utf8.mjs", - "test": "npm run test:docs-nav && npm run test:owned-links && npm run test:release-docs-nav && npm run test:rewrite-dist-redirect-links && npm run test:rewrite-dist-links-idempotency && npm run test:docs-error-handling-copy && npm run test:schemas && npm run test:performance-feedback && npm run test:mcp-schema-projection && npm run test:dist-schema-version-ids && npm run test:examples && npm run test:extensions && npm run test:extension-schemas && npm run test:error-handling && npm run test:json-schema && npm run test:audio-radio && npm run test:adagents-catalog-only && npm run test:canonical-reference-resolver && npm run test:composed && npm run test:rejection-arm-mutex && npm run test:migrations && npm run test:hmac-vectors && npm run test:hmac-signer-conformance && npm run test:webhook-signing-vectors && npm run test:webhook-receiver-envelope && npm run test:oauth-setup-vectors && npm run test:transport-errors && npm run test:targeting-overlay-vectors && npm run test:targeting-aware-discovery && npm run test:demographic-targeting && npm run test:attestation-vectors && npm run test:rights-attestations && npm run test:governance-runtime-attestations && npm run test:governance-conditions-storyboard && npm run test:audience-evidence && npm run test:status-as-of-vectors && npm run test:storyboard-scoping && npm run test:storyboard-branch-sets && npm run test:storyboard-provides-state-for && npm run test:storyboard-fixture-resolution && npm run test:storyboard-contradictions && npm run test:storyboard-context-entity && npm run test:storyboard-auth-shape && npm run test:storyboard-test-kits && npm run test:compliance-packaged-refs && npm run test:compliance-source-authority && npm run test:storyboard-sample-request-schema && npm run test:storyboard-response-schema && npm run test:storyboard-context-output-paths && npm run test:storyboard-validations-paths && npm run test:storyboard-check-enum && npm run test:update-media-buy-affected-packages && npm run test:storyboard-advisory-expiry && npm run test:storyboard-raw-mode-required && npm run test:storyboard-upstream-traffic-paths && npm run test:refine-finalize-validation-ids && npm run test:run-storyboards-schema-root && npm run test:storyboard-doc-parity && npm run test:pagination-invariant && npm run test:version-envelope && npm run test:test-dynamic-imports && npm run test:sdk-shims && npm run test:format-identity-boundaries && npm run test:sdk-runner-capability-gates && npm run test:callapi-state-change && npm run test:sign-protocol-tarball && npm run test:chat-streaming-code-fences && npm run test:certification-demo-formatting && npm run test:build-schemas-hoist-enums && npm run test:build-schemas-hoist-marked && npm run test:build-schemas-async-response-refs && npm run test:release-workflow && npm run test:immutable-release-artifacts && npm run test:patch-3-0-compat-bundle && npm run test:error-codes && npm run test:compliance-snippets && npm run test:doc-compliance-drift && npm run test:substitution-vector-names && npm run test:platform-agnostic && npm run test:oneof-discriminators && npm run test:schema-utf8 && npm run test:unit && npm run test:server-unit && npm run test:openapi && npm run typecheck", + "test": "npm run test:docs-nav && npm run test:owned-links && npm run test:release-docs-nav && npm run test:rewrite-dist-redirect-links && npm run test:rewrite-dist-links-idempotency && npm run test:docs-error-handling-copy && npm run test:schemas && npm run test:performance-feedback && npm run test:mcp-schema-projection && npm run test:dist-schema-version-ids && npm run test:examples && npm run test:extensions && npm run test:extension-schemas && npm run test:error-handling && npm run test:json-schema && npm run test:audio-radio && npm run test:adagents-catalog-only && npm run test:canonical-reference-resolver && npm run test:composed && npm run test:rejection-arm-mutex && npm run test:migrations && npm run test:hmac-vectors && npm run test:hmac-signer-conformance && npm run test:webhook-signing-vectors && npm run test:webhook-receiver-envelope && npm run test:oauth-setup-vectors && npm run test:transport-errors && npm run test:targeting-overlay-vectors && npm run test:targeting-aware-discovery && npm run test:demographic-targeting && npm run test:attestation-vectors && npm run test:rights-attestations && npm run test:governance-runtime-attestations && npm run test:governance-conditions-storyboard && npm run test:signal-governance-storyboard && npm run test:audience-evidence && npm run test:status-as-of-vectors && npm run test:storyboard-scoping && npm run test:storyboard-branch-sets && npm run test:storyboard-provides-state-for && npm run test:storyboard-fixture-resolution && npm run test:storyboard-contradictions && npm run test:storyboard-context-entity && npm run test:storyboard-auth-shape && npm run test:storyboard-test-kits && npm run test:compliance-packaged-refs && npm run test:compliance-source-authority && npm run test:storyboard-sample-request-schema && npm run test:storyboard-response-schema && npm run test:storyboard-context-output-paths && npm run test:storyboard-validations-paths && npm run test:storyboard-check-enum && npm run test:update-media-buy-affected-packages && npm run test:storyboard-advisory-expiry && npm run test:storyboard-raw-mode-required && npm run test:storyboard-upstream-traffic-paths && npm run test:refine-finalize-validation-ids && npm run test:run-storyboards-schema-root && npm run test:storyboard-doc-parity && npm run test:pagination-invariant && npm run test:version-envelope && npm run test:test-dynamic-imports && npm run test:sdk-shims && npm run test:format-identity-boundaries && npm run test:sdk-runner-capability-gates && npm run test:callapi-state-change && npm run test:sign-protocol-tarball && npm run test:chat-streaming-code-fences && npm run test:certification-demo-formatting && npm run test:build-schemas-hoist-enums && npm run test:build-schemas-hoist-marked && npm run test:build-schemas-async-response-refs && npm run test:release-workflow && npm run test:immutable-release-artifacts && npm run test:patch-3-0-compat-bundle && npm run test:error-codes && npm run test:compliance-snippets && npm run test:doc-compliance-drift && npm run test:substitution-vector-names && npm run test:platform-agnostic && npm run test:oneof-discriminators && npm run test:schema-utf8 && npm run test:unit && npm run test:server-unit && npm run test:openapi && npm run typecheck", "test:all": "npm run test:schemas && npm run test:examples && npm run test:extensions && npm run test:error-handling && npm run test:snippets && npm run typecheck", "precommit:server-unit": "node scripts/precommit-server-unit.cjs", "precommit": "bash scripts/with-timeout.sh 180 npm run test:unit && npm run test:test-dynamic-imports && npm run test:format-identity-boundaries && npm run test:callapi-state-change && bash scripts/with-timeout.sh 240 npm run precommit:server-unit && npm run typecheck", diff --git a/static/compliance/source/specialisms/signal-marketplace/scenarios/governance_denied.yaml b/static/compliance/source/specialisms/signal-marketplace/scenarios/governance_denied.yaml index 1026377e57..8b24d2c4ed 100644 --- a/static/compliance/source/specialisms/signal-marketplace/scenarios/governance_denied.yaml +++ b/static/compliance/source/specialisms/signal-marketplace/scenarios/governance_denied.yaml @@ -2,23 +2,33 @@ id: signal_marketplace/governance_denied version: "1.0.0" title: "Signal agent rejects activation without governance approval" category: signal_marketplace -summary: "Verifies that a signal agent requires a governance approval context before activating signals on governed accounts." +summary: "Verifies that a signal agent claiming activate_signal governance enforcement rejects missing authorization without calling a deployment platform." track: signals required_tools: + - sync_accounts - sync_governance - get_signals - activate_signal +requires_capability: + path: adcp.governance_enforcement.tasks + contains: + task: activate_signal + modes: + - signed_context + narrative: | Signal activation is a spending event: activating a third-party segment costs per-user - or per-activation fees that must be authorized against the buyer's governance plan. The - buyer-side orchestrator must call check_governance before sending a governed activation - request. The signal agent enforces that contract by rejecting activations for governed - accounts when the request does not carry a valid governance_context. + or per-activation fees that must be authorized against the buyer's governance plan. An + agent that advertises `adcp.governance_enforcement.tasks[]` for `activate_signal` commits + to validating the approved signed intent before performing that task. Non-claiming + agents grade this scenario `not_applicable`. This scenario registers a governance agent with the signal agent via sync_governance, - then attempts to activate a signal without the prior check_governance approval context. - The signal agent must fail closed with PERMISSION_DENIED. + then attempts a paid activation without the required `governance_context`. The signal + agent must fail closed with PERMISSION_DENIED. The denial response is not treated as + proof by itself: the storyboard also verifies that the rejected request caused no + platform-primary outbound activation call. A response echo is never governance evidence. By default, the governance agent is the training agent at test-agent.adcontextprotocol.org. Override by supplying a different `governance_agent_url` in the run's initial context @@ -41,7 +51,7 @@ caller: prerequisites: description: | A governance agent URL to register, and a signal agent that supports - sync_governance + activate_signal. + account setup, governance registration, signal discovery, and activation. test_kit: "test-kits/acme-outdoor.yaml" phases: @@ -105,7 +115,7 @@ phases: task: get_signals schema_ref: "signals/get-signals-request.json" response_schema_ref: "signals/get-signals-response.json" - doc_ref: "/signals/task-reference/get_signals" + doc_ref: "/signals/tasks/get_signals" comply_scenario: signal_discovery stateful: false expected: | @@ -119,19 +129,24 @@ phases: context_outputs: - path: "signals[0].signal_agent_segment_id" key: "signal_agent_segment_id" + - path: "signals[0].pricing_options[0].pricing_option_id" + key: "pricing_option_id" validations: - check: response_schema description: "Response matches get-signals-response.json schema" - check: field_present path: "signals[0].signal_agent_segment_id" description: "Signal has an activation identifier" + - check: field_present + path: "signals[0].pricing_options[0].pricing_option_id" + description: "Marketplace signal has a pricing option for the governed commitment" - id: activate_signal_denied title: "activate_signal — missing governance approval" task: activate_signal schema_ref: "signals/activate-signal-request.json" response_schema_ref: "signals/activate-signal-response.json" - doc_ref: "/signals/task-reference/activate_signal" + doc_ref: "/signals/tasks/activate_signal" comply_scenario: signal_activation expect_error: true negative_path: payload_well_formed @@ -142,7 +157,9 @@ phases: - findings explaining that check_governance must run first sample_request: + action: "activate" signal_agent_segment_id: "$context.signal_agent_segment_id" + pricing_option_id: "$context.pricing_option_id" destinations: - type: "platform" platform: "the-trade-desk" @@ -169,6 +186,9 @@ phases: - check: error_code value: "PERMISSION_DENIED" description: "Error code is PERMISSION_DENIED" - - check: field_present - path: "context" - description: "Response echoes back the context object even on errors" + - check: upstream_traffic + description: "Rejected activation causes no platform-primary outbound deployment call" + min_count: 0 + endpoint_pattern: "POST *" + purpose_filter: + - platform_primary diff --git a/tests/signal-governance-storyboard.test.cjs b/tests/signal-governance-storyboard.test.cjs new file mode 100644 index 0000000000..3ea37827d3 --- /dev/null +++ b/tests/signal-governance-storyboard.test.cjs @@ -0,0 +1,71 @@ +'use strict'; + +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const yaml = require('js-yaml'); + +const ROOT = path.resolve(__dirname, '..'); +const STORYBOARD_PATH = path.join( + ROOT, + 'static/compliance/source/specialisms/signal-marketplace/scenarios/governance_denied.yaml', +); + +function loadStoryboard() { + return yaml.load(fs.readFileSync(STORYBOARD_PATH, 'utf8')); +} + +function stepsById(doc) { + return new Map( + (doc.phases || []).flatMap((phase) => phase.steps || []).map((step) => [step.id, step]), + ); +} + +test('signal governance denial is task-gated and proves no deployment side effect', () => { + const doc = loadStoryboard(); + const steps = stepsById(doc); + const discovery = steps.get('get_signals_list'); + const denied = steps.get('activate_signal_denied'); + + assert.deepEqual(doc.requires_capability, { + path: 'adcp.governance_enforcement.tasks', + contains: { + task: 'activate_signal', + modes: ['signed_context'], + }, + }); + for (const tool of ['sync_accounts', 'sync_governance', 'get_signals', 'activate_signal']) { + assert.ok(doc.required_tools.includes(tool), `${tool} must be declared`); + } + + assert.equal( + discovery.context_outputs.find((capture) => capture.key === 'pricing_option_id')?.path, + 'signals[0].pricing_options[0].pricing_option_id', + ); + assert.equal(denied.sample_request.action, 'activate'); + assert.equal(denied.sample_request.pricing_option_id, '$context.pricing_option_id'); + assert.equal(denied.sample_request.governance_context, undefined); + assert.equal( + denied.validations.find((validation) => validation.check === 'error_code')?.value, + 'PERMISSION_DENIED', + ); + + const noDeployment = denied.validations.find( + (validation) => validation.check === 'upstream_traffic', + ); + assert.deepEqual(noDeployment, { + check: 'upstream_traffic', + description: 'Rejected activation causes no platform-primary outbound deployment call', + min_count: 0, + endpoint_pattern: 'POST *', + purpose_filter: ['platform_primary'], + }); + assert.equal( + denied.validations.some( + (validation) => validation.check === 'field_present' && validation.path === 'context', + ), + false, + 'response context echo must not be graded as governance evidence', + ); +}); From a2691c6d8722b933ceecbcba56a6259b1cf7e2fb Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 16 Aug 2026 01:41:16 +0200 Subject: [PATCH 2/6] test(compliance): require priced signal discovery --- .../signal-marketplace/scenarios/governance_denied.yaml | 6 +++++- tests/signal-governance-storyboard.test.cjs | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/static/compliance/source/specialisms/signal-marketplace/scenarios/governance_denied.yaml b/static/compliance/source/specialisms/signal-marketplace/scenarios/governance_denied.yaml index 8b24d2c4ed..9625eea4ce 100644 --- a/static/compliance/source/specialisms/signal-marketplace/scenarios/governance_denied.yaml +++ b/static/compliance/source/specialisms/signal-marketplace/scenarios/governance_denied.yaml @@ -121,7 +121,11 @@ phases: expected: | Return at least one signal with a signal_agent_segment_id and pricing. sample_request: - signal_spec: "outdoor enthusiasts, age 25-54, US" + signal_spec: >- + a paid, standalone marketplace signal for outdoor enthusiasts age 25-54 in + the US, with at least one pricing option available for immediate activation + fields: + - pricing_options account: brand: domain: "acmeoutdoor.example" diff --git a/tests/signal-governance-storyboard.test.cjs b/tests/signal-governance-storyboard.test.cjs index 3ea37827d3..98dc77187d 100644 --- a/tests/signal-governance-storyboard.test.cjs +++ b/tests/signal-governance-storyboard.test.cjs @@ -43,6 +43,9 @@ test('signal governance denial is task-gated and proves no deployment side effec discovery.context_outputs.find((capture) => capture.key === 'pricing_option_id')?.path, 'signals[0].pricing_options[0].pricing_option_id', ); + assert.deepEqual(discovery.sample_request.fields, ['pricing_options']); + assert.match(discovery.sample_request.signal_spec, /paid, standalone marketplace signal/); + assert.match(discovery.sample_request.signal_spec, /at least one pricing option/); assert.equal(denied.sample_request.action, 'activate'); assert.equal(denied.sample_request.pricing_option_id, '$context.pricing_option_id'); assert.equal(denied.sample_request.governance_context, undefined); From 1779124d206a02bf2bb019c23881b4756a1d3188 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 16 Aug 2026 02:02:37 +0200 Subject: [PATCH 3/6] test(compliance): exercise object capability gates --- package-lock.json | 8 ++--- package.json | 2 +- tests/sdk-runner-capability-gates.test.cjs | 40 ++++++++++++++++++++++ 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 12fa2e8f52..70912c0630 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "3.1.1", "hasInstallScript": true, "dependencies": { - "@adcp/sdk": "13.0.0-rc.21", + "@adcp/sdk": "13.0.0-rc.23", "@anthropic-ai/sdk": "^0.115.0", "@asteasolutions/zod-to-openapi": "^8.5.0", "@contentauth/c2pa-node": "^0.8.3", @@ -132,9 +132,9 @@ } }, "node_modules/@adcp/sdk": { - "version": "13.0.0-rc.21", - "resolved": "https://registry.npmjs.org/@adcp/sdk/-/sdk-13.0.0-rc.21.tgz", - "integrity": "sha512-qruzhVkygM+KSgyXdxCx/4y3+QVBIq+IcrcuXqoq7q1qrJyhxxlF1U2nzPMjG95FOc02Xdbppj9jxDYA3toQ5Q==", + "version": "13.0.0-rc.23", + "resolved": "https://registry.npmjs.org/@adcp/sdk/-/sdk-13.0.0-rc.23.tgz", + "integrity": "sha512-1et5fLEPwVOFo76SAvXVfpsIwJMZAyp5TCCk+aLfzV8M2iKh43Ocl3sV2PzRIlc8+XF3s9hBfkkxzD6oyk850Q==", "license": "Apache-2.0", "workspaces": [ ".", diff --git a/package.json b/package.json index f817d1d730..4f3f75ad12 100644 --- a/package.json +++ b/package.json @@ -159,7 +159,7 @@ "docs:json-field-audit": "node scripts/docs-json-field-audit.cjs" }, "dependencies": { - "@adcp/sdk": "13.0.0-rc.21", + "@adcp/sdk": "13.0.0-rc.23", "@anthropic-ai/sdk": "^0.115.0", "@asteasolutions/zod-to-openapi": "^8.5.0", "@contentauth/c2pa-node": "^0.8.3", diff --git a/tests/sdk-runner-capability-gates.test.cjs b/tests/sdk-runner-capability-gates.test.cjs index 9ccf40ddda..18c84ed57f 100644 --- a/tests/sdk-runner-capability-gates.test.cjs +++ b/tests/sdk-runner-capability-gates.test.cjs @@ -142,6 +142,46 @@ test('runStoryboard skips a not_contains-gated phase when the array contains the assert.match(result.phases[0].steps[0].error, /must not contain "operator"/); }); +test('runStoryboard structurally matches object-valued governance task gates', async () => { + const storyboardPath = path.join( + __dirname, + '..', + 'static', + 'compliance', + 'source', + 'specialisms', + 'signal-marketplace', + 'scenarios', + 'governance_denied.yaml' + ); + const storyboard = YAML.parse(fs.readFileSync(storyboardPath, 'utf8')); + const profile = { + tools: ['get_adcp_capabilities', ...storyboard.required_tools], + raw_capabilities: { + adcp: { + governance_enforcement: { + tasks: [{ modes: ['signed_context'], task: 'activate_signal' }], + }, + }, + }, + }; + + // Strip executable work to isolate the affirmative gate path without + // dispatching any storyboard steps over the network. + const result = await runStoryboard( + 'https://agent.example/mcp', + { ...storyboard, prerequisites: undefined, phases: [] }, + { + _profile: profile, + agentTools: profile.tools, + } + ); + + assert.equal(result.overall_passed, true); + assert.equal(result.phases[0].phase_id, 'no_phases'); + assert.equal(result.phases[0].steps[0].skip_reason, 'no_phases'); +}); + test('billing gate skips per-agent phases when agent billing is not supported', () => { const storyboardPath = path.join( __dirname, From e48dfb59dec1726c24e40731efe8add16e9afa48 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 16 Aug 2026 02:52:35 +0200 Subject: [PATCH 4/6] fix(compliance): use published SDK storyboard routing --- .agents/sdk-shim-ledger.json | 23 +- .husky/pre-push | 2 +- package-lock.json | 9 +- package.json | 6 +- scripts/patch-sdk-rc15.mjs | 126 ------- .../scenarios/canonical_formats.yaml | 7 + .../source/universal/storyboard-schema.yaml | 1 + tests/lint-sdk-shims.test.cjs | 11 +- tests/patch-sdk-rc15.test.cjs | 346 ------------------ tests/sdk-runtime-compat.test.cjs | 118 ++++++ 10 files changed, 135 insertions(+), 514 deletions(-) delete mode 100644 scripts/patch-sdk-rc15.mjs delete mode 100644 tests/patch-sdk-rc15.test.cjs create mode 100644 tests/sdk-runtime-compat.test.cjs diff --git a/.agents/sdk-shim-ledger.json b/.agents/sdk-shim-ledger.json index a65870e564..d2103cc31b 100644 --- a/.agents/sdk-shim-ledger.json +++ b/.agents/sdk-shim-ledger.json @@ -30,7 +30,7 @@ "paths": [ "scripts/overlay-compliance-cache.sh", "scripts/run-storyboards-matrix.sh", - "tests/patch-sdk-rc15.test.cjs" + "tests/sdk-runtime-compat.test.cjs" ], "terms": [ "node_modules/@adcp/sdk/compliance/cache", @@ -39,26 +39,5 @@ "node_modules/@adcp/sdk/dist/lib/types/schemas.generated.mjs", "schemas.generated.js" ] - }, - { - "id": "sdk-storyboard-product-wire", - "title": "SDK storyboard product-wire routing fix", - "kind": "installed_sdk_runtime_patch", - "status": "temporary", - "owner": "training-agent", - "upstream": "adcontextprotocol/adcp-client#2495, adcontextprotocol/adcp-client#2527", - "problem": "The rc.18 storyboard task map forces get_products onto a legacy-only wire, and scoped transports discover capabilities without carrying them into request adaptation. The latter makes 3.1 sellers look pre-3.1 and strips filters.pricing_currencies.", - "localBehavior": "Patch both compiled SDK module formats after installation so the storyboard runner leaves get_products wire selection unspecified and request-local capabilities drive version adaptation without cross-tenant caching.", - "removalCondition": "Remove the patch script, Docker invocations, and ledger entry after an SDK release fixes storyboard get_products routing and carries scoped capabilities into request adaptation, and both current and released-3.0 storyboard matrices pass without the patch.", - "paths": [ - "scripts/patch-sdk-rc15.mjs", - "tests/patch-sdk-rc15.test.cjs" - ], - "terms": [ - "node_modules/@adcp/sdk/dist/lib/testing/storyboard/task-map.js", - "node_modules/@adcp/sdk/dist/lib/testing/storyboard/task-map.mjs", - "node_modules/@adcp/sdk/dist/lib/core/SingleAgentClient.js", - "node_modules/@adcp/sdk/dist/lib/core/SingleAgentClient.mjs" - ] } ] diff --git a/.husky/pre-push b/.husky/pre-push index af54b378a9..5110e9fda2 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -46,7 +46,7 @@ DOC_PATHS='^(docs/|dist/docs/|mintlify-docs/|docs\.json|mint\.json|README\.md|se CHANGED=$(git diff --name-only "$RANGE" 2>/dev/null || echo "") -STORYBOARD_PATHS='^(server/src/training-agent/|server/src/services/hosted-compliance-version\.ts|server/tests/manual/run-storyboards\.ts|static/compliance/source/|dist/compliance/|scripts/build-compliance\.cjs|scripts/overlay-compliance-cache\.sh|scripts/patch-sdk-rc15\.mjs|scripts/run-storyboards-matrix\.sh|scripts/run-storyboards-3-0-compat\.sh|package\.json|package-lock\.json)' +STORYBOARD_PATHS='^(server/src/training-agent/|server/src/services/hosted-compliance-version\.ts|server/tests/manual/run-storyboards\.ts|static/compliance/source/|dist/compliance/|scripts/build-compliance\.cjs|scripts/overlay-compliance-cache\.sh|scripts/run-storyboards-matrix\.sh|scripts/run-storyboards-3-0-compat\.sh|package\.json|package-lock\.json)' if echo "$CHANGED" | grep -Eq "$STORYBOARD_PATHS"; then echo "🔍 Training-agent or compliance source changed — running storyboard matrix (~3min)..." diff --git a/package-lock.json b/package-lock.json index 70912c0630..3308f89711 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,8 @@ "": { "name": "adcontextprotocol", "version": "3.1.1", - "hasInstallScript": true, "dependencies": { - "@adcp/sdk": "13.0.0-rc.23", + "@adcp/sdk": "13.0.0-rc.24", "@anthropic-ai/sdk": "^0.115.0", "@asteasolutions/zod-to-openapi": "^8.5.0", "@contentauth/c2pa-node": "^0.8.3", @@ -132,9 +131,9 @@ } }, "node_modules/@adcp/sdk": { - "version": "13.0.0-rc.23", - "resolved": "https://registry.npmjs.org/@adcp/sdk/-/sdk-13.0.0-rc.23.tgz", - "integrity": "sha512-1et5fLEPwVOFo76SAvXVfpsIwJMZAyp5TCCk+aLfzV8M2iKh43Ocl3sV2PzRIlc8+XF3s9hBfkkxzD6oyk850Q==", + "version": "13.0.0-rc.24", + "resolved": "https://registry.npmjs.org/@adcp/sdk/-/sdk-13.0.0-rc.24.tgz", + "integrity": "sha512-wmOdwRiwj7yLazm2R66DMhDT9LlwHsIOcbM/KGa5V+s2bTS6JEJo41q4eZ7JZkGReUuvJe8QsXcoCaLteBFo8A==", "license": "Apache-2.0", "workspaces": [ ".", diff --git a/package.json b/package.json index 4f3f75ad12..c83f02f97f 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "test:pagination-invariant": "node --test --test-force-exit --test-timeout=30000 tests/lint-pagination-invariant.test.cjs", "test:version-envelope": "node --test --test-force-exit --test-timeout=30000 tests/lint-version-envelope.test.cjs", "test:test-dynamic-imports": "node --test --test-force-exit --test-timeout=30000 tests/lint-test-dynamic-imports.test.cjs", - "test:sdk-shims": "node --test --test-force-exit --test-timeout=30000 tests/lint-sdk-shims.test.cjs tests/patch-sdk-rc15.test.cjs", + "test:sdk-shims": "node --test --test-force-exit --test-timeout=30000 tests/lint-sdk-shims.test.cjs tests/sdk-runtime-compat.test.cjs", "test:tenant-routing": "vitest run --config server/vitest.config.ts server/src/training-agent/tenants/tenant-smoke.test.ts", "test:format-identity-boundaries": "node --test --test-force-exit --test-timeout=30000 tests/lint-format-identity-boundaries.test.cjs", "test:sdk-runner-capability-gates": "node --test --test-force-exit --test-timeout=30000 tests/sdk-runner-capability-gates.test.cjs", @@ -137,8 +137,6 @@ "test:storyboards": "bash scripts/run-storyboards-matrix.sh", "test:storyboards:3.0-compat": "bash scripts/run-storyboards-3-0-compat.sh", "prepare": "husky", - "postinstall": "node scripts/patch-sdk-rc15.mjs", - "patch:sdk-rc15": "node scripts/patch-sdk-rc15.mjs", "changeset": "changeset", "version": "changeset version && npm install --package-lock-only --ignore-scripts && npm run build:schemas -- --release && npm run build:compliance -- --release && npm run build:protocol-tarball -- --release && npm run sign:protocol-tarball", "sign:protocol-tarball": "bash scripts/sign-protocol-tarball.sh", @@ -159,7 +157,7 @@ "docs:json-field-audit": "node scripts/docs-json-field-audit.cjs" }, "dependencies": { - "@adcp/sdk": "13.0.0-rc.23", + "@adcp/sdk": "13.0.0-rc.24", "@anthropic-ai/sdk": "^0.115.0", "@asteasolutions/zod-to-openapi": "^8.5.0", "@contentauth/c2pa-node": "^0.8.3", diff --git a/scripts/patch-sdk-rc15.mjs b/scripts/patch-sdk-rc15.mjs deleted file mode 100644 index b2abf7ab00..0000000000 --- a/scripts/patch-sdk-rc15.mjs +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env node - -import fs from 'node:fs'; -import path from 'node:path'; - -// This temporary patch remains version-locked even though its filename dates -// from the release where it was introduced. Remove it when the upstream -// get_products routing and scoped capability propagation fixes ship. -const EXPECTED_VERSION = '13.0.0-rc.21'; -const packageJson = JSON.parse(fs.readFileSync(path.resolve('node_modules/@adcp/sdk/package.json'), 'utf8')); - -if (packageJson.version !== EXPECTED_VERSION) { - throw new Error( - `Refusing to patch @adcp/sdk ${packageJson.version}; expected ${EXPECTED_VERSION}. ` + - 'Remove this patcher when the upstream storyboard routing and scoped capability fixes ship.', - ); -} - -const storyboardTaskMapFiles = [ - 'node_modules/@adcp/sdk/dist/lib/testing/storyboard/task-map.js', - 'node_modules/@adcp/sdk/dist/lib/testing/storyboard/task-map.mjs', -]; - -const singleAgentClientFiles = [ - { - relative: 'node_modules/@adcp/sdk/dist/lib/core/SingleAgentClient.js', - resolveAdapterOriginal: - ' const adapterKey = (0, import_version2.resolveAdapterKey)(this.resolvedAdcpVersion, this.cachedCapabilities);', - resolveAdapterPatched: - ' const adapterKey = (0, import_version2.resolveAdapterKey)(this.resolvedAdcpVersion, perCallCapabilities ?? this.cachedCapabilities);', - }, - { - relative: 'node_modules/@adcp/sdk/dist/lib/core/SingleAgentClient.mjs', - resolveAdapterOriginal: - ' const adapterKey = resolveAdapterKey(this.resolvedAdcpVersion, this.cachedCapabilities);', - resolveAdapterPatched: - ' const adapterKey = resolveAdapterKey(this.resolvedAdcpVersion, perCallCapabilities ?? this.cachedCapabilities);', - }, -]; - -const originalCallParams = ' const callParams = legacyMethodName ? withLegacyCreativeWireHint(params) : params;'; -const patchedCallParams = ' const callParams = legacyMethodName && taskName !== "get_products" ? withLegacyCreativeWireHint(params) : params;'; - -function countOccurrences(source, token) { - return source.split(token).length - 1; -} - -function preparePatch(relative, replacements) { - const file = path.resolve(relative); - const source = fs.readFileSync(file, 'utf8'); - const states = replacements.map(({ original, patched, expectedCount = 1 }) => { - const originalCount = countOccurrences(source, original); - const patchedCount = countOccurrences(source, patched); - const needsPatch = originalCount === expectedCount && patchedCount === 0; - const alreadyPatched = originalCount === 0 && patchedCount === expectedCount; - if (!needsPatch && !alreadyPatched) { - throw new Error(`Unexpected ${EXPECTED_VERSION} SDK shape in ${relative}`); - } - return needsPatch ? 'original' : 'patched'; - }); - if (new Set(states).size !== 1) { - throw new Error(`Mixed ${EXPECTED_VERSION} SDK patch state in ${relative}`); - } - const output = states[0] === 'original' - ? replacements.reduce((current, { original, patched }) => current.replaceAll(original, patched), source) - : source; - return { file, source, output, replacements }; -} - -const storyboardTaskMaps = storyboardTaskMapFiles.map(relative => preparePatch(relative, [{ - original: originalCallParams, - patched: patchedCallParams, -}])); - -const adaptRequestSignature = - ' adaptRequest(taskType, params, serverVersion, debugLogs, perCallToolSchemas) {'; -const patchedAdaptRequestSignature = - ' adaptRequest(taskType, params, serverVersion, debugLogs, perCallToolSchemas, perCallCapabilities) {'; -const perCallToolSchemasArgument = 'capabilityDiscoveryContext.toolSchemas\n'; -const patchedPerCallToolSchemasArgument = - 'capabilityDiscoveryContext.toolSchemas, capabilityDiscoveryContext.capabilities\n'; -const detectServerVersion = ` async detectServerVersion(options) { - const capabilities = await this.getCapabilities(options); - return capabilities.version; - }`; -const patchedDetectServerVersion = ` async detectServerVersion(options) { - const capabilities = await this.getCapabilities(options); - const discoveryContext = options?.[CAPABILITY_DISCOVERY_CONTEXT]; - if (discoveryContext) discoveryContext.capabilities = capabilities; - return capabilities.version; - }`; - -const singleAgentClients = singleAgentClientFiles.map(({ - relative, - resolveAdapterOriginal, - resolveAdapterPatched, -}) => preparePatch(relative, [ - { - original: perCallToolSchemasArgument, - patched: patchedPerCallToolSchemasArgument, - expectedCount: 2, - }, - { original: adaptRequestSignature, patched: patchedAdaptRequestSignature }, - { original: resolveAdapterOriginal, patched: resolveAdapterPatched }, - { original: detectServerVersion, patched: patchedDetectServerVersion }, -])); - -// Validate both module formats before writing either one so a corrupt or -// unexpected SDK artifact cannot leave the install only partially patched. -for (const { file, source, output } of [...storyboardTaskMaps, ...singleAgentClients]) { - if (output !== source) { - fs.writeFileSync(file, output); - } -} - -for (const { file, replacements } of [...storyboardTaskMaps, ...singleAgentClients]) { - const source = fs.readFileSync(file, 'utf8'); - for (const { original, patched, expectedCount = 1 } of replacements) { - if ( - countOccurrences(source, original) !== 0 || - countOccurrences(source, patched) !== expectedCount - ) { - throw new Error(`@adcp/sdk ${EXPECTED_VERSION} patch verification failed for ${file}`); - } - } -} diff --git a/static/compliance/source/protocols/media-buy/scenarios/canonical_formats.yaml b/static/compliance/source/protocols/media-buy/scenarios/canonical_formats.yaml index 731262122f..0740567813 100644 --- a/static/compliance/source/protocols/media-buy/scenarios/canonical_formats.yaml +++ b/static/compliance/source/protocols/media-buy/scenarios/canonical_formats.yaml @@ -340,6 +340,7 @@ phases: response_schema_ref: "media-buy/get-products-response.json" doc_ref: "/media-buy/task-reference/get_products" comply_scenario: schema_compliance + response_projection: raw stateful: true expected: | Return products including the seeded canonical_formats_mrec_display @@ -445,6 +446,7 @@ phases: response_schema_ref: "media-buy/get-products-response.json" doc_ref: "/media-buy/task-reference/get_products" comply_scenario: schema_compliance + response_projection: raw stateful: true expected: | Return the seeded canonical_formats_ids_only_display product with a @@ -495,6 +497,7 @@ phases: response_schema_ref: "media-buy/get-products-response.json" doc_ref: "/media-buy/task-reference/get_products" comply_scenario: schema_compliance + response_projection: raw stateful: true expected: | Return the seeded canonical_formats_options_only_display product with @@ -553,6 +556,7 @@ phases: response_schema_ref: "media-buy/get-products-response.json" doc_ref: "/media-buy/task-reference/get_products" comply_scenario: schema_compliance + response_projection: raw stateful: true expected: | Return the seeded canonical_formats_custom_takeover product with a @@ -619,6 +623,7 @@ phases: response_schema_ref: "media-buy/get-products-response.json" doc_ref: "/media-buy/task-reference/get_products" comply_scenario: schema_compliance + response_projection: raw stateful: true expected: | Return the seeded canonical_formats_experimental_display product with @@ -683,6 +688,7 @@ phases: response_schema_ref: "media-buy/get-products-response.json" doc_ref: "/media-buy/task-reference/get_products" comply_scenario: schema_compliance + response_projection: raw auth: type: api_key from_test_kit: true @@ -744,6 +750,7 @@ phases: response_schema_ref: "media-buy/get-products-response.json" doc_ref: "/media-buy/task-reference/get_products" comply_scenario: schema_compliance + response_projection: raw stateful: true expected: | Return the seeded canonical_formats_mrec_display product so create-time diff --git a/static/compliance/source/universal/storyboard-schema.yaml b/static/compliance/source/universal/storyboard-schema.yaml index 4e2d21f81a..a6706d76b7 100644 --- a/static/compliance/source/universal/storyboard-schema.yaml +++ b/static/compliance/source/universal/storyboard-schema.yaml @@ -812,6 +812,7 @@ # response_schema_ref: string (path to response schema) # doc_ref: string (path to documentation page) # comply_scenario: string (maps to @adcp/client testing scenario, e.g., "creative_sync") +# response_projection: raw (optional — grade the SDK's unprojected response without changing the authored request wire; use only for compatibility storyboards that assert legacy and canonical fields together) # expected: string (human-readable description of expected behavior) # stateful: boolean (does this step depend on state from a previous step?) # diff --git a/tests/lint-sdk-shims.test.cjs b/tests/lint-sdk-shims.test.cjs index 7422ed1214..5359bdf3a9 100644 --- a/tests/lint-sdk-shims.test.cjs +++ b/tests/lint-sdk-shims.test.cjs @@ -40,19 +40,10 @@ test('ledger entries include removal conditions and upstream asks', () => { }); test('scanner finds the expected high-risk SDK reach-ins', () => { - const ledger = loadLedger(); const findings = collectFindings(); assert.ok(findings.some((f) => f.file === 'scripts/stage-sdk-schema-bundle.sh' && f.term.includes('schemas-data'))); assert.ok(findings.some((f) => f.file === 'scripts/overlay-compliance-cache.sh' && f.term.includes('schemas.generated.js'))); - assert.ok(findings.some((f) => f.file === 'scripts/patch-sdk-rc15.mjs' && f.term.includes('task-map'))); - const patchEntry = ledger.find(entry => entry.id === 'sdk-storyboard-product-wire'); - assert.ok(patchEntry, 'missing storyboard product-wire patch ledger entry'); - for (const term of patchEntry.terms) { - assert.ok( - findings.some(finding => finding.file === 'scripts/patch-sdk-rc15.mjs' && finding.term.includes(term)), - `SDK patch no longer references required artifact: ${term}`, - ); - } + assert.ok(findings.some((f) => f.file === 'tests/sdk-runtime-compat.test.cjs' && f.term.includes('schemas.generated.js'))); }); test('matchingPrivateTerm ignores comment-only lines but catches private SDK code', () => { diff --git a/tests/patch-sdk-rc15.test.cjs b/tests/patch-sdk-rc15.test.cjs deleted file mode 100644 index a282def0b9..0000000000 --- a/tests/patch-sdk-rc15.test.cjs +++ /dev/null @@ -1,346 +0,0 @@ -const assert = require('node:assert/strict'); -const fs = require('node:fs'); -const os = require('node:os'); -const path = require('node:path'); -const { spawnSync } = require('node:child_process'); -const test = require('node:test'); -const { pathToFileURL } = require('node:url'); - -const PATCHER = path.resolve(__dirname, '..', 'scripts', 'patch-sdk-rc15.mjs'); -const TASK_MAP_FILES = [ - 'node_modules/@adcp/sdk/dist/lib/testing/storyboard/task-map.js', - 'node_modules/@adcp/sdk/dist/lib/testing/storyboard/task-map.mjs', -]; -const SINGLE_AGENT_CLIENT_FILES = [ - 'node_modules/@adcp/sdk/dist/lib/core/SingleAgentClient.js', - 'node_modules/@adcp/sdk/dist/lib/core/SingleAgentClient.mjs', -]; - -const TASK_MAP_SOURCE = `function withLegacyCreativeWireHint(params) { - return { ...params, ext: { adcp: { creative_wire: "legacy" } } }; -} -async function executeStoryboardTask(client, taskName, params) { - const legacyMethodName = taskName === "get_products" ? "getProductsLegacy" : "syncCreativesLegacy"; - const callParams = legacyMethodName ? withLegacyCreativeWireHint(params) : params; - return await client[legacyMethodName](callParams); -} -module.exports = { executeStoryboardTask }; -`; - -const SINGLE_AGENT_CLIENT_SOURCE = `const CAPABILITY_DISCOVERY_CONTEXT = Symbol("capabilityDiscoveryContext"); -function resolveAdapterKey() {} -const import_version2 = { resolveAdapterKey }; -class SingleAgentClient { - executeCanonical(capabilityDiscoveryContext) { - return this.adaptRequest("get_products", {}, "v3", [], capabilityDiscoveryContext.toolSchemas - ); - } - executeUnprojected(capabilityDiscoveryContext) { - return this.adaptRequest("get_products", {}, "v3", [], capabilityDiscoveryContext.toolSchemas - ); - } - adaptRequest(taskType, params, serverVersion, debugLogs, perCallToolSchemas) { - const adapterKey = resolveAdapterKey(this.resolvedAdcpVersion, this.cachedCapabilities); - return adapterKey; - } - async getCapabilities() { return { version: "v3" }; } - async detectServerVersion(options) { - const capabilities = await this.getCapabilities(options); - return capabilities.version; - } -} -module.exports = { SingleAgentClient }; -`; - -function writeFixture(version = '13.0.0-rc.21') { - const root = fs.mkdtempSync(path.join(os.tmpdir(), 'sdk-rc15-patch-')); - const sdkRoot = path.join(root, 'node_modules', '@adcp', 'sdk'); - fs.mkdirSync(sdkRoot, { recursive: true }); - fs.writeFileSync(path.join(sdkRoot, 'package.json'), JSON.stringify({ version })); - for (const relative of TASK_MAP_FILES) { - const file = path.join(root, relative); - fs.mkdirSync(path.dirname(file), { recursive: true }); - fs.writeFileSync(file, relative.endsWith('.mjs') - ? TASK_MAP_SOURCE.replace('module.exports = { executeStoryboardTask };', 'export { executeStoryboardTask };') - : TASK_MAP_SOURCE); - } - for (const relative of SINGLE_AGENT_CLIENT_FILES) { - const file = path.join(root, relative); - fs.mkdirSync(path.dirname(file), { recursive: true }); - const source = relative.endsWith('.mjs') - ? SINGLE_AGENT_CLIENT_SOURCE.replace('module.exports = { SingleAgentClient };', 'export { SingleAgentClient };') - : SINGLE_AGENT_CLIENT_SOURCE.replace( - ' const adapterKey = resolveAdapterKey(this.resolvedAdcpVersion, this.cachedCapabilities);', - ' const adapterKey = (0, import_version2.resolveAdapterKey)(this.resolvedAdcpVersion, this.cachedCapabilities);', - ); - fs.writeFileSync(file, source); - } - return root; -} - -function runPatcher(root) { - return spawnSync(process.execPath, [PATCHER], { cwd: root, encoding: 'utf8' }); -} - -test('hosted SDK patch fixes get_products routing in CJS and ESM idempotently', async (t) => { - const root = writeFixture(); - t.after(() => fs.rmSync(root, { recursive: true, force: true })); - - const first = runPatcher(root); - assert.equal(first.status, 0, first.stderr); - const files = TASK_MAP_FILES.map(relative => path.join(root, relative)); - const clientFiles = SINGLE_AGENT_CLIENT_FILES.map(relative => path.join(root, relative)); - const once = files.map(file => fs.readFileSync(file, 'utf8')); - const clientsOnce = clientFiles.map(file => fs.readFileSync(file, 'utf8')); - for (const source of once) assert.ok(source.includes('taskName !== "get_products"')); - for (const source of clientsOnce) { - assert.ok(source.includes('perCallCapabilities ?? this.cachedCapabilities')); - assert.equal((source.match(/capabilityDiscoveryContext\.capabilities/g) ?? []).length, 2); - assert.ok(source.includes('discoveryContext.capabilities = capabilities')); - } - for (const file of [...files, ...clientFiles]) { - const checked = spawnSync(process.execPath, ['--check', file], { encoding: 'utf8' }); - assert.equal(checked.status, 0, checked.stderr); - } - - const cjsTaskMap = require(files[0]); - const esmTaskMap = await import(pathToFileURL(files[1]).href); - const client = { - getProductsLegacy: async params => params, - syncCreativesLegacy: async params => params, - }; - for (const taskMap of [cjsTaskMap, esmTaskMap]) { - const request = { - buying_mode: 'wholesale', - filters: { pricing_currencies: ['USD'] }, - fields: ['product_id', 'pricing_options'], - account: { account_id: 'account-1' }, - context: { correlation_id: 'pricing-filter-test' }, - }; - assert.deepEqual( - await taskMap.executeStoryboardTask(client, 'get_products', request), - request, - ); - assert.deepEqual( - await taskMap.executeStoryboardTask(client, 'sync_creatives', { creatives: [] }), - { creatives: [], ext: { adcp: { creative_wire: 'legacy' } } }, - ); - } - - const second = runPatcher(root); - assert.equal(second.status, 0, second.stderr); - assert.deepEqual(files.map(file => fs.readFileSync(file, 'utf8')), once); - assert.deepEqual(clientFiles.map(file => fs.readFileSync(file, 'utf8')), clientsOnce); -}); - -test('hosted SDK patch refuses an unreviewed SDK version before modifying artifacts', (t) => { - const root = writeFixture('13.0.0-rc.19'); - t.after(() => fs.rmSync(root, { recursive: true, force: true })); - const result = runPatcher(root); - assert.notEqual(result.status, 0); - assert.match(result.stderr, /Refusing to patch @adcp\/sdk 13\.0\.0-rc\.19/); -}); - -test('hosted SDK patch rejects an unexpected task-map source shape', (t) => { - const root = writeFixture(); - t.after(() => fs.rmSync(root, { recursive: true, force: true })); - for (const relative of TASK_MAP_FILES) { - const file = path.join(root, relative); - const source = fs.readFileSync(file, 'utf8').replace( - ' const callParams = legacyMethodName ? withLegacyCreativeWireHint(params) : params;', - ' const callParams = params;', - ); - fs.writeFileSync(file, source); - } - - const result = runPatcher(root); - assert.notEqual(result.status, 0); - assert.match(result.stderr, /Unexpected 13\.0\.0-rc\.21 SDK shape/); -}); - -test('hosted SDK patch rejects mixed original and patched call sites before writing either format', (t) => { - const root = writeFixture(); - t.after(() => fs.rmSync(root, { recursive: true, force: true })); - const cjsFile = path.join(root, TASK_MAP_FILES[0]); - const esmFile = path.join(root, TASK_MAP_FILES[1]); - const cjsBefore = fs.readFileSync(cjsFile, 'utf8'); - const original = ' const callParams = legacyMethodName ? withLegacyCreativeWireHint(params) : params;'; - const patched = ' const callParams = legacyMethodName && taskName !== "get_products" ? withLegacyCreativeWireHint(params) : params;'; - fs.writeFileSync(esmFile, fs.readFileSync(esmFile, 'utf8').replace(original, `${original}\n${patched}`)); - - const result = runPatcher(root); - assert.notEqual(result.status, 0); - assert.match(result.stderr, /Unexpected 13\.0\.0-rc\.21 SDK shape/); - assert.equal(fs.readFileSync(cjsFile, 'utf8'), cjsBefore); -}); - -test('hosted SDK patch rejects duplicate original call sites before writing either format', (t) => { - const root = writeFixture(); - t.after(() => fs.rmSync(root, { recursive: true, force: true })); - const cjsFile = path.join(root, TASK_MAP_FILES[0]); - const esmFile = path.join(root, TASK_MAP_FILES[1]); - const cjsBefore = fs.readFileSync(cjsFile, 'utf8'); - const original = ' const callParams = legacyMethodName ? withLegacyCreativeWireHint(params) : params;'; - fs.writeFileSync(esmFile, fs.readFileSync(esmFile, 'utf8').replace(original, `${original}\n${original}`)); - - const result = runPatcher(root); - assert.notEqual(result.status, 0); - assert.match(result.stderr, /Unexpected 13\.0\.0-rc\.21 SDK shape/); - assert.equal(fs.readFileSync(cjsFile, 'utf8'), cjsBefore); -}); - -test('hosted SDK patch preflights both module formats before writing either one', (t) => { - const root = writeFixture(); - t.after(() => fs.rmSync(root, { recursive: true, force: true })); - const cjsFile = path.join(root, TASK_MAP_FILES[0]); - const esmFile = path.join(root, TASK_MAP_FILES[1]); - const cjsBefore = fs.readFileSync(cjsFile, 'utf8'); - fs.writeFileSync( - esmFile, - fs.readFileSync(esmFile, 'utf8').replace( - ' const callParams = legacyMethodName ? withLegacyCreativeWireHint(params) : params;', - ' const callParams = params;', - ), - ); - - const result = runPatcher(root); - assert.notEqual(result.status, 0); - assert.match(result.stderr, /Unexpected 13\.0\.0-rc\.21 SDK shape/); - assert.equal(fs.readFileSync(cjsFile, 'utf8'), cjsBefore); -}); - -test('hosted SDK patch preflights core clients before writing any artifact', (t) => { - const root = writeFixture(); - t.after(() => fs.rmSync(root, { recursive: true, force: true })); - const taskMapFile = path.join(root, TASK_MAP_FILES[0]); - const clientFile = path.join(root, SINGLE_AGENT_CLIENT_FILES[1]); - const taskMapBefore = fs.readFileSync(taskMapFile, 'utf8'); - fs.writeFileSync( - clientFile, - fs.readFileSync(clientFile, 'utf8').replace( - ' adaptRequest(taskType, params, serverVersion, debugLogs, perCallToolSchemas) {', - ' adaptRequest(taskType, params, serverVersion, debugLogs) {', - ), - ); - - const result = runPatcher(root); - assert.notEqual(result.status, 0); - assert.match(result.stderr, /Unexpected 13\.0\.0-rc\.21 SDK shape/); - assert.equal(fs.readFileSync(taskMapFile, 'utf8'), taskMapBefore); -}); - -async function loadInstalledSingleAgentClients() { - const cjsPath = path.resolve(__dirname, '..', SINGLE_AGENT_CLIENT_FILES[0]); - const esmPath = path.resolve(__dirname, '..', SINGLE_AGENT_CLIENT_FILES[1]); - return [ - require(cjsPath).SingleAgentClient, - (await import(pathToFileURL(esmPath).href)).SingleAgentClient, - ]; -} - -async function runScopedCapabilityCase(SingleAgentClient, supportedVersion, methodName) { - const client = new SingleAgentClient({ - id: `scoped-${supportedVersion}`, - name: `Scoped ${supportedVersion}`, - agent_uri: 'https://agent.example/mcp', - protocol: 'mcp', - }, { - transport: { fetchFn: async () => { throw new Error('unexpected network call'); } }, - validation: { requests: 'off', responses: 'off' }, - validateFeatures: false, - }); - client.ensureEndpointDiscovered = async () => client.normalizedAgent; - client.getAgentInfo = async () => ({ - tools: [ - { name: 'get_adcp_capabilities', inputSchema: { type: 'object', properties: {} } }, - { - name: 'get_products', - inputSchema: { - type: 'object', - properties: { buying_mode: { type: 'string' }, filters: { type: 'object' } }, - }, - }, - ], - }); - let outbound; - client.executor.executeTask = async (_agent, taskName, params) => { - if (taskName === 'get_adcp_capabilities') { - return { - success: true, - status: 'completed', - data: { - adcp: { - major_versions: [3], - supported_versions: [supportedVersion], - build_version: supportedVersion === '3.1' ? '3.1.13' : '3.0.14', - }, - supported_protocols: ['media_buy'], - }, - metadata: { status: 'completed', taskName }, - debug_logs: [], - }; - } - outbound = params; - return { - success: true, - status: 'completed', - data: { products: [] }, - metadata: { status: 'completed', taskName }, - debug_logs: [], - }; - }; - const result = await client[methodName]({ - buying_mode: 'wholesale', - filters: { pricing_currencies: ['USD'] }, - }); - return { client, outbound, result }; -} - -test('installed SDK uses request-local scoped capabilities for get_products adaptation', async () => { - for (const SingleAgentClient of await loadInstalledSingleAgentClients()) { - for (const methodName of ['getProducts', 'getProductsLegacy']) { - const v31 = await runScopedCapabilityCase(SingleAgentClient, '3.1', methodName); - assert.deepEqual(v31.outbound.filters, { pricing_currencies: ['USD'] }); - assert.equal( - v31.result.debug_logs?.some(log => log.type === 'pre31_pricing_currencies_stripped'), - false, - ); - assert.equal(v31.client.cachedCapabilities, undefined, 'scoped capabilities must remain request-local'); - assert.equal(v31.client.cachedToolSchemas, undefined, 'scoped tool schemas must remain request-local'); - - const v30 = await runScopedCapabilityCase(SingleAgentClient, '3.0', methodName); - assert.deepEqual(v30.outbound.filters, {}); - assert.equal( - v30.result.debug_logs?.some(log => log.type === 'pre31_pricing_currencies_stripped'), - true, - ); - assert.equal(v30.client.cachedCapabilities, undefined, 'scoped capabilities must remain request-local'); - assert.equal(v30.client.cachedToolSchemas, undefined, 'scoped tool schemas must remain request-local'); - } - } -}); - -test('installed 3.1 SDK accepts the additive flat advertiser natural-key response', async () => { - const schemasPath = path.resolve( - __dirname, - '..', - 'node_modules/@adcp/sdk/dist/lib/types/schemas.generated.js', - ); - const { SyncAccountsResponseSchema } = await import(pathToFileURL(schemasPath).href); - const parsed = SyncAccountsResponseSchema.safeParse({ - status: 'completed', - accounts: [{ - account_id: 'acc_nova_nl', - brand: { domain: 'nova-athletics.example', countries: ['NL'] }, - operator: 'pinnacle-agency.example', - operator_unit: { id: '234284238', name: 'EMEA' }, - currency: 'EUR', - sandbox: true, - action: 'created', - status: 'active', - billing: 'operator', - }], - }); - - assert.equal(parsed.success, true, parsed.success ? undefined : parsed.error.toString()); -}); diff --git a/tests/sdk-runtime-compat.test.cjs b/tests/sdk-runtime-compat.test.cjs new file mode 100644 index 0000000000..4b30ccfb84 --- /dev/null +++ b/tests/sdk-runtime-compat.test.cjs @@ -0,0 +1,118 @@ +const assert = require('node:assert/strict'); +const path = require('node:path'); +const test = require('node:test'); +const { pathToFileURL } = require('node:url'); + +async function loadInstalledSingleAgentClients() { + return [ + require('@adcp/sdk').SingleAgentClient, + (await import('@adcp/sdk')).SingleAgentClient, + ]; +} + +async function runScopedCapabilityCase(SingleAgentClient, supportedVersion, methodName) { + const client = new SingleAgentClient({ + id: `scoped-${supportedVersion}`, + name: `Scoped ${supportedVersion}`, + agent_uri: 'https://agent.example/mcp', + protocol: 'mcp', + }, { + transport: { fetchFn: async () => { throw new Error('unexpected network call'); } }, + validation: { requests: 'off', responses: 'off' }, + validateFeatures: false, + }); + client.ensureEndpointDiscovered = async () => client.normalizedAgent; + client.getAgentInfo = async () => ({ + tools: [ + { name: 'get_adcp_capabilities', inputSchema: { type: 'object', properties: {} } }, + { + name: 'get_products', + inputSchema: { + type: 'object', + properties: { buying_mode: { type: 'string' }, filters: { type: 'object' } }, + }, + }, + ], + }); + let outbound; + client.executor.executeTask = async (_agent, taskName, params) => { + if (taskName === 'get_adcp_capabilities') { + return { + success: true, + status: 'completed', + data: { + adcp: { + major_versions: [3], + supported_versions: [supportedVersion], + build_version: supportedVersion === '3.1' ? '3.1.13' : '3.0.14', + }, + supported_protocols: ['media_buy'], + }, + metadata: { status: 'completed', taskName }, + debug_logs: [], + }; + } + outbound = params; + return { + success: true, + status: 'completed', + data: { products: [] }, + metadata: { status: 'completed', taskName }, + debug_logs: [], + }; + }; + const result = await client[methodName]({ + buying_mode: 'wholesale', + filters: { pricing_currencies: ['USD'] }, + }); + return { client, outbound, result }; +} + +test('installed SDK uses request-local scoped capabilities for get_products adaptation', async () => { + for (const SingleAgentClient of await loadInstalledSingleAgentClients()) { + for (const methodName of ['getProducts', 'getProductsLegacy']) { + const v31 = await runScopedCapabilityCase(SingleAgentClient, '3.1', methodName); + assert.deepEqual(v31.outbound.filters, { pricing_currencies: ['USD'] }); + assert.equal( + v31.result.debug_logs?.some(log => log.type === 'pre31_pricing_currencies_stripped'), + false, + ); + assert.equal(v31.client.cachedCapabilities, undefined, 'scoped capabilities must remain request-local'); + assert.equal(v31.client.cachedToolSchemas, undefined, 'scoped tool schemas must remain request-local'); + + const v30 = await runScopedCapabilityCase(SingleAgentClient, '3.0', methodName); + assert.deepEqual(v30.outbound.filters, {}); + assert.equal( + v30.result.debug_logs?.some(log => log.type === 'pre31_pricing_currencies_stripped'), + true, + ); + assert.equal(v30.client.cachedCapabilities, undefined, 'scoped capabilities must remain request-local'); + assert.equal(v30.client.cachedToolSchemas, undefined, 'scoped tool schemas must remain request-local'); + } + } +}); + +test('installed 3.1 SDK accepts the additive flat advertiser natural-key response', async () => { + const schemasPath = path.resolve( + __dirname, + '..', + 'node_modules/@adcp/sdk/dist/lib/types/schemas.generated.js', + ); + const { SyncAccountsResponseSchema } = await import(pathToFileURL(schemasPath).href); + const parsed = SyncAccountsResponseSchema.safeParse({ + status: 'completed', + accounts: [{ + account_id: 'acc_nova_nl', + brand: { domain: 'nova-athletics.example', countries: ['NL'] }, + operator: 'pinnacle-agency.example', + operator_unit: { id: '234284238', name: 'EMEA' }, + currency: 'EUR', + sandbox: true, + action: 'created', + status: 'active', + billing: 'operator', + }], + }); + + assert.equal(parsed.success, true, parsed.success ? undefined : parsed.error.toString()); +}); From addaa2c10352d8b0f4afd12618e0a4727fe56cd5 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 16 Aug 2026 03:12:47 +0200 Subject: [PATCH 5/6] fix(build): remove retired SDK patch steps --- .github/workflows/migration-smoke-test.yml | 2 -- .github/workflows/release.yml | 2 +- .github/workflows/training-agent-storyboards.yml | 2 -- Dockerfile | 8 -------- 4 files changed, 1 insertion(+), 13 deletions(-) diff --git a/.github/workflows/migration-smoke-test.yml b/.github/workflows/migration-smoke-test.yml index 29affe7048..8e7b587399 100644 --- a/.github/workflows/migration-smoke-test.yml +++ b/.github/workflows/migration-smoke-test.yml @@ -11,7 +11,6 @@ on: - 'server/src/config.ts' - 'server/tsconfig.json' - 'Dockerfile' - - 'scripts/patch-sdk-rc15.mjs' push: branches: [main] paths: @@ -22,7 +21,6 @@ on: - 'server/src/config.ts' - 'server/tsconfig.json' - 'Dockerfile' - - 'scripts/patch-sdk-rc15.mjs' jobs: migrate: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01b6d750a3..dd17399878 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,7 +59,7 @@ jobs: echo "version=${VERSION}" >> "$GITHUB_OUTPUT" changed_files="$(git show --format= --name-only --no-renames "${GITHUB_SHA}")" - if grep -Eq '^(\.changeset/|package(-lock)?[.]json$|dist/(schemas|compliance|protocol)/|static/(schemas|compliance)/source/|server/src/training-agent/|scripts/(build-schemas[.]cjs|build-compliance[.]cjs|build-protocol-tarball[.]cjs|sign-protocol-tarball[.]sh|run-storyboards-[^/]+[.]sh|run-storyboards-matrix[.]sh|stage-sdk-schema-bundle[.]sh|overlay-compliance-cache[.]sh|patch-sdk-rc15[.]mjs)|[.]github/workflows/(release|training-agent-storyboards)[.]yml$)' <<< "${changed_files}"; then + if grep -Eq '^(\.changeset/|package(-lock)?[.]json$|dist/(schemas|compliance|protocol)/|static/(schemas|compliance)/source/|server/src/training-agent/|scripts/(build-schemas[.]cjs|build-compliance[.]cjs|build-protocol-tarball[.]cjs|sign-protocol-tarball[.]sh|run-storyboards-[^/]+[.]sh|run-storyboards-matrix[.]sh|stage-sdk-schema-bundle[.]sh|overlay-compliance-cache[.]sh)|[.]github/workflows/(release|training-agent-storyboards)[.]yml$)' <<< "${changed_files}"; then echo "relevant=true" >> "$GITHUB_OUTPUT" echo "Release-relevant files changed." exit 0 diff --git a/.github/workflows/training-agent-storyboards.yml b/.github/workflows/training-agent-storyboards.yml index b30eaf21f9..abbf9a5958 100644 --- a/.github/workflows/training-agent-storyboards.yml +++ b/.github/workflows/training-agent-storyboards.yml @@ -11,7 +11,6 @@ on: - 'package-lock.json' - 'scripts/build-compliance.cjs' - 'scripts/overlay-compliance-cache.sh' - - 'scripts/patch-sdk-rc15.mjs' - 'scripts/run-storyboards-matrix.sh' - 'scripts/run-storyboards-3-0-compat.sh' - 'scripts/stage-sdk-schema-bundle.sh' @@ -30,7 +29,6 @@ on: - 'package-lock.json' - 'scripts/build-compliance.cjs' - 'scripts/overlay-compliance-cache.sh' - - 'scripts/patch-sdk-rc15.mjs' - 'scripts/run-storyboards-matrix.sh' - 'scripts/run-storyboards-3-0-compat.sh' - 'scripts/stage-sdk-schema-bundle.sh' diff --git a/Dockerfile b/Dockerfile index 9bbcb7a80a..d0a9d8a43a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,11 +12,6 @@ RUN npm ci --ignore-scripts # Copy source code COPY . . -# rc.15's storyboard runner forces get_products onto the legacy-only wire, -# hiding the transitional 3.1 dual response. Apply the exact-version patch -# explicitly because installs disable scripts. -RUN npm run patch:sdk-rc15 - # Build the TypeScript server (increase heap for large tsc compilation) RUN NODE_OPTIONS=--max-old-space-size=4096 npm run build @@ -161,9 +156,6 @@ RUN npm ci --omit=dev --ignore-scripts \ && npm rebuild sharp \ && npm cache clean --force -COPY --from=builder /app/scripts/patch-sdk-rc15.mjs ./scripts/patch-sdk-rc15.mjs -RUN node scripts/patch-sdk-rc15.mjs - # Copy built files from builder. Runtime assets under server/src/** (JSON # format catalogs, SQL migrations, Addie rule markdown, etc.) are mirrored # into dist/ by scripts/copy-server-assets.cjs during `npm run build`, so From 9b6ddf29ed0a449fb7b350b609fe2886b3a684d8 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 16 Aug 2026 06:51:30 +0200 Subject: [PATCH 6/6] fix(compliance): rely on SDK schema grading --- package-lock.json | 8 ++++---- package.json | 2 +- .../protocols/media-buy/scenarios/canonical_formats.yaml | 7 ------- static/compliance/source/universal/storyboard-schema.yaml | 1 - 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3308f89711..6539f218f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "adcontextprotocol", "version": "3.1.1", "dependencies": { - "@adcp/sdk": "13.0.0-rc.24", + "@adcp/sdk": "13.0.0-rc.25", "@anthropic-ai/sdk": "^0.115.0", "@asteasolutions/zod-to-openapi": "^8.5.0", "@contentauth/c2pa-node": "^0.8.3", @@ -131,9 +131,9 @@ } }, "node_modules/@adcp/sdk": { - "version": "13.0.0-rc.24", - "resolved": "https://registry.npmjs.org/@adcp/sdk/-/sdk-13.0.0-rc.24.tgz", - "integrity": "sha512-wmOdwRiwj7yLazm2R66DMhDT9LlwHsIOcbM/KGa5V+s2bTS6JEJo41q4eZ7JZkGReUuvJe8QsXcoCaLteBFo8A==", + "version": "13.0.0-rc.25", + "resolved": "https://registry.npmjs.org/@adcp/sdk/-/sdk-13.0.0-rc.25.tgz", + "integrity": "sha512-FTU2K1GOCfAuKCHW25/Q3Not4g7fj2P2dok+vNBmZ9d93V+d+OkPRgZk4B4rzGwQVg+2jMqAzvYCZbS8CgFi+A==", "license": "Apache-2.0", "workspaces": [ ".", diff --git a/package.json b/package.json index c83f02f97f..818b635557 100644 --- a/package.json +++ b/package.json @@ -157,7 +157,7 @@ "docs:json-field-audit": "node scripts/docs-json-field-audit.cjs" }, "dependencies": { - "@adcp/sdk": "13.0.0-rc.24", + "@adcp/sdk": "13.0.0-rc.25", "@anthropic-ai/sdk": "^0.115.0", "@asteasolutions/zod-to-openapi": "^8.5.0", "@contentauth/c2pa-node": "^0.8.3", diff --git a/static/compliance/source/protocols/media-buy/scenarios/canonical_formats.yaml b/static/compliance/source/protocols/media-buy/scenarios/canonical_formats.yaml index 0740567813..731262122f 100644 --- a/static/compliance/source/protocols/media-buy/scenarios/canonical_formats.yaml +++ b/static/compliance/source/protocols/media-buy/scenarios/canonical_formats.yaml @@ -340,7 +340,6 @@ phases: response_schema_ref: "media-buy/get-products-response.json" doc_ref: "/media-buy/task-reference/get_products" comply_scenario: schema_compliance - response_projection: raw stateful: true expected: | Return products including the seeded canonical_formats_mrec_display @@ -446,7 +445,6 @@ phases: response_schema_ref: "media-buy/get-products-response.json" doc_ref: "/media-buy/task-reference/get_products" comply_scenario: schema_compliance - response_projection: raw stateful: true expected: | Return the seeded canonical_formats_ids_only_display product with a @@ -497,7 +495,6 @@ phases: response_schema_ref: "media-buy/get-products-response.json" doc_ref: "/media-buy/task-reference/get_products" comply_scenario: schema_compliance - response_projection: raw stateful: true expected: | Return the seeded canonical_formats_options_only_display product with @@ -556,7 +553,6 @@ phases: response_schema_ref: "media-buy/get-products-response.json" doc_ref: "/media-buy/task-reference/get_products" comply_scenario: schema_compliance - response_projection: raw stateful: true expected: | Return the seeded canonical_formats_custom_takeover product with a @@ -623,7 +619,6 @@ phases: response_schema_ref: "media-buy/get-products-response.json" doc_ref: "/media-buy/task-reference/get_products" comply_scenario: schema_compliance - response_projection: raw stateful: true expected: | Return the seeded canonical_formats_experimental_display product with @@ -688,7 +683,6 @@ phases: response_schema_ref: "media-buy/get-products-response.json" doc_ref: "/media-buy/task-reference/get_products" comply_scenario: schema_compliance - response_projection: raw auth: type: api_key from_test_kit: true @@ -750,7 +744,6 @@ phases: response_schema_ref: "media-buy/get-products-response.json" doc_ref: "/media-buy/task-reference/get_products" comply_scenario: schema_compliance - response_projection: raw stateful: true expected: | Return the seeded canonical_formats_mrec_display product so create-time diff --git a/static/compliance/source/universal/storyboard-schema.yaml b/static/compliance/source/universal/storyboard-schema.yaml index a6706d76b7..4e2d21f81a 100644 --- a/static/compliance/source/universal/storyboard-schema.yaml +++ b/static/compliance/source/universal/storyboard-schema.yaml @@ -812,7 +812,6 @@ # response_schema_ref: string (path to response schema) # doc_ref: string (path to documentation page) # comply_scenario: string (maps to @adcp/client testing scenario, e.g., "creative_sync") -# response_projection: raw (optional — grade the SDK's unprojected response without changing the authored request wire; use only for compatibility storyboards that assert legacy and canonical fields together) # expected: string (human-readable description of expected behavior) # stateful: boolean (does this step depend on state from a previous step?) #