diff --git a/.agents/sdk-shim-ledger.json b/.agents/sdk-shim-ledger.json index fe51488c706..b7c53cd34a1 100644 --- a/.agents/sdk-shim-ledger.json +++ b/.agents/sdk-shim-ledger.json @@ -1 +1,21 @@ -[] +[ + { + "id": "creative-build-probe-8240", + "title": "Portable evaluator build probe preparation", + "kind": "integrity-pinned-package-patch", + "status": "temporary", + "owner": "adcp compliance maintainers / issue 8240", + "upstream": "https://github.com/adcontextprotocol/adcp-client/pull/3332", + "problem": "The pinned SDK lacks portable valid build-probe preparation and the served-catalog authority prerequisite required by the merged upstream fix.", + "localBehavior": "Patch only reviewed SDK14.0.0-rc.53 CJS/ESM loader, runner, schema-authority and declaration preimages. A local creative_build_probe ALS scope is established before preflight and pairs served catalog selection with actual JSON capability validation (builtin or external); stock14 behavior remains outside that opt-in and for other tasks. Unlike the initial candidate, authority modules include an explicit maintenance delta and do not claim whole-module identity to15.9.5. Preserve request pins, accounting, general TaskExecutor and all four main helper payloads. Authority regressions use ledgered private scope/validator interfaces and owned temporary stock copies reconstructed by exact reverse hashes; no direct installed-package edits.", + "removalCondition": "Remove after the upstream creative-build-probe runner fix is merged, available in a qualified SDK release, and both full and standalone regression suites pass after upgrading the pinned dependency. Do not release the SDK from this task.", + "paths": [ + "scripts/install-sdk-creative-build-probe.cjs", + "tests/evaluator-build-probe-authority.test.cjs" + ], + "terms": [ + "dist/lib/testing/", + "dist/lib/validation/" + ] + } +] diff --git a/.changeset/evaluator-off-list-rejection-coverage.md b/.changeset/evaluator-off-list-rejection-coverage.md new file mode 100644 index 00000000000..7840ccc238b --- /dev/null +++ b/.changeset/evaluator-off-list-rejection-coverage.md @@ -0,0 +1,24 @@ +--- +"adcontextprotocol": patch +--- + +Make both off-list evaluator rejection phases mandatory for agents that advertise evaluator support. Preserve unsupported-feature applicability, prevent later discovery from skipping the security boundary, and check all outbound methods and paths on the off-list evaluator origin. + +Bind each required rejection probe to an advertised build operation and a valid +source input. Handle non-build-first and legacy catalogs without relying on +catalog ordering or ranking features. Add an owned, integrity-checked temporary +SDK bridge for shared full/standalone runner preparation; unavailable fixtures +remain explicit coverage gaps with no passing security credit. + +Forward authored probe recipes through the manual runner, including recipe-only +kits. Malformed understood catalog declarations retain their catalog cause as +prerequisite failures (actionable dependency failures in hosted classification), +instead of being mislabeled controller setup failures. + +Resolve capability catalogs against the validated served release while preserving +the pinned build request and source-input contract, including provisioned legacy +formats when the catalog uses newer canonical vocabulary. + +Backport the merged main fix with a separately qualified, all-target guarded +SDK14.0.0-rc.53 adapter and the existing upstream served-catalog authority +prerequisite. Preserve this branch's exact dependency and lockfile. diff --git a/.github/workflows/sdk-response-conformance.yml b/.github/workflows/sdk-response-conformance.yml index 21e9c8e415a..b8ec99fdfb4 100644 --- a/.github/workflows/sdk-response-conformance.yml +++ b/.github/workflows/sdk-response-conformance.yml @@ -32,6 +32,7 @@ jobs: go-version-file: scripts/sdk-response-conformance/go/go.mod cache-dependency-path: scripts/sdk-response-conformance/go/go.sum - run: npm ci --ignore-scripts + - run: node scripts/install-sdk-creative-build-probe.cjs - run: python3 -m pip install -r scripts/sdk-response-conformance/requirements.txt - run: npm run test:sdk-response-conformance - name: Capture and validate real SDK responses diff --git a/Dockerfile b/Dockerfile index ff71d07323e..d5387a26da8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,9 +5,12 @@ WORKDIR /app # Copy package files COPY package*.json ./ +COPY scripts/install-sdk-creative-build-probe.cjs ./scripts/install-sdk-creative-build-probe.cjs +COPY patches/sdk-creative-build-probe ./patches/sdk-creative-build-probe # Install ALL dependencies (including dev dependencies for TypeScript build) -RUN npm ci --ignore-scripts +RUN npm ci --ignore-scripts \ + && node scripts/install-sdk-creative-build-probe.cjs # Copy source code COPY . . @@ -153,10 +156,13 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* COPY package*.json ./ +COPY scripts/install-sdk-creative-build-probe.cjs ./scripts/install-sdk-creative-build-probe.cjs +COPY patches/sdk-creative-build-probe ./patches/sdk-creative-build-probe # --ignore-scripts blocks arbitrary postinstall lifecycle scripts from the full # dep tree; native deps are rebuilt explicitly per-package. RUN npm ci --omit=dev --ignore-scripts \ + && node scripts/install-sdk-creative-build-probe.cjs \ && npm rebuild sharp \ && npm cache clean --force diff --git a/package.json b/package.json index 77f83686668..16e68afcfc0 100644 --- a/package.json +++ b/package.json @@ -117,10 +117,10 @@ "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 tests/compact-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/sdk-runtime-compat.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 tests/sdk-creative-build-probe-bridge.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", + "test:sdk-runner-capability-gates": "node --test --test-force-exit --test-timeout=30000 tests/sdk-runner-capability-gates.test.cjs && node --test --test-timeout=30000 tests/evaluator-allowlist-runner.test.cjs tests/evaluator-build-probe-authority.test.cjs tests/evaluator-build-probe-cleanup.test.cjs", "test:callapi-state-change": "node --test --test-force-exit --test-timeout=30000 tests/lint-callapi-state-change.test.cjs", "test:cdn-artifacts-worker": "node --test --test-force-exit --test-timeout=30000 tests/artifact-cdn-worker.test.cjs", "test:sign-protocol-tarball": "node --test --test-force-exit --test-timeout=30000 tests/sign-protocol-tarball.test.cjs", @@ -197,7 +197,8 @@ "eval:addie-fixed-traces-direct-full-suite": "tsx --import dotenv/config server/tests/manual/fixed-trace-direct-full-suite-eval.ts", "eval:addie-gemini-direct-ablation": "tsx --import dotenv/config server/tests/manual/gemini-direct-ablation-eval.ts", "eval:addie-matched-v4-plan": "tsx server/tests/manual/matched-v4-evaluation-plan.ts", - "eval:addie-matched-v4-authorized": "tsx --import dotenv/config server/tests/manual/matched-v4-authorized-execution.ts" + "eval:addie-matched-v4-authorized": "tsx --import dotenv/config server/tests/manual/matched-v4-authorized-execution.ts", + "postinstall": "node scripts/install-sdk-creative-build-probe.cjs" }, "dependencies": { "@adcp/sdk": "14.0.0-rc.53", diff --git a/patches/sdk-creative-build-probe/README.md b/patches/sdk-creative-build-probe/README.md new file mode 100644 index 00000000000..be328782df9 --- /dev/null +++ b/patches/sdk-creative-build-probe/README.md @@ -0,0 +1,57 @@ +Temporary SDK bridge for adcp#8240. Upstream source and executable tests: +https://github.com/adcontextprotocol/adcp-client/pull/3332 + +Qualified upstream source: `9f932f174e8125e00ec55d64db2a292cba2be7f9`. + +The manifest contains bounded insertions into SDK **14.0.0-rc.53** loader, runner and +matching declarations, plus the shared resolver compiled from upstream source. +It does not replace the whole newer SDK runner. Both public CJS and ESM entry +points receive the same preparation, including standalone steps. Existing global +accounting and required-phase/vacuous-pass guards remain unchanged. + +`node scripts/install-sdk-creative-build-probe.cjs` validates package identity, +all target preimages/postimages, unique insertion anchors and deterministic helper +bytes before writing. A repeated complete installation is a no-op; drift or a +mixed state fails loudly. Normal installs run this as postinstall. Docker's +`--ignore-scripts` stages explicitly invoke only this trusted installer, as does +the SDK conformance workflow. Tests reconstruct stock bytes and exercise both +public module entry points before and after explicit installation. + +The automatic source recipe relies on existing format completeness semantics; +operator recipes assert account-specific valid input for every route matched by +the existing fixture DSL. Capability catalogs follow the SDK-authorized served release; build selectors, +source manifests and dispatch retain the run pin. Missing source/preconditions yield visible coverage +gaps, not passing security credit. See the authored runner test-kit contract. + +Qualification/removal: upgrade to a published SDK containing the upstream fix, +remove this directory, installer, lifecycle/Docker/workflow calls and owned shim +ledger entry, then run the complete authored fixture and bridge/standalone +regressions with the public replacement. This task publishes no release. + +To refresh after a separately reviewed maintenance qualification, rebuild that SDK commit, copy +only creative-build-probe.{js,mjs,d.ts,d.mts}, and regenerate the bounded insertion +blocks and SHA-256 pre/postimage hashes against a pristine npm SDK 14.0.0-rc.53 install. +Review the resulting manifest and rerun integrity/public-entry regressions. + +Maintenance prerequisite: SDK 14 does not export served-release schema selection. +The manifest ports the SDK 15.9.5 selection additions with a reviewed maintenance +delta: an AsyncLocalStorage scope enabled only by the locally authored +`creative_build_probe` directive, before capability preflight. Catalog selection +and its capability response-schema backstop use that same scope. Within it, the +actual authorized JSON schema governs even for builtin roots; stale generated +Zod disagreement remains visible as a warning. Invalid JSON fails and unavailable +schema produces the existing failing `no_schema_registered` harness diagnostic. +These modules are not byte-identical to SDK 15.9.5. Unrelated OAuth or error changes +are excluded. +The four evaluator helper payloads remain byte-identical to main. All 25 target +preimages/postimages are qualified against the exact locked npm SDK 14 tarball. +The branch lockfile and dependency pin are unchanged. No newer runner is copied. + +Scope: only opted creative-probe capability preflight and capability response +checks use the added policy. Other storyboards and non-capability checks retain +stock SDK 14 semantics, including external-root authority. Explicit false scopes +shadow parent opt-in. `servedReleaseSchemaRoots` is deferred/inert for other +storyboards and outer bundle discovery; mixed bundles forward it to each opted +storyboard. A direct call of the new probe helper is itself an explicit opt-in. +SDK 14's general TaskExecutor is untouched and remains pin-only. This is not a +claim of whole-SDK parity with 15.9.5. diff --git a/patches/sdk-creative-build-probe/creative-build-probe.d.mts b/patches/sdk-creative-build-probe/creative-build-probe.d.mts new file mode 100644 index 00000000000..1857b1c65cc --- /dev/null +++ b/patches/sdk-creative-build-probe/creative-build-probe.d.mts @@ -0,0 +1,34 @@ +import type { FixtureMatchClause, StoryboardContext } from './types.mjs'; +export interface CreativeBuildProbeSpec { + context_prefix: string; + message: string; +} +export interface CreativeBuildProbeInputFixture { + match: FixtureMatchClause[]; + /** Operator-provisioned valid input for every route matched by this recipe. */ + input: Record; +} +export type CreativeBuildProbeResolution = { + status: 'resolved'; + capabilityId?: string; + formatId?: Record; + context: StoryboardContext; + source: 'generation' | 'test_kit'; + requirements: FixtureMatchClause[]; +} | { + status: 'unsatisfied'; + detail: string; +} | { + status: 'failed'; + detail: string; +}; +export declare function validateCreativeBuildProbeSpec(value: unknown): asserts value is CreativeBuildProbeSpec; +/** + * Uses only actual catalog entries and explicitly provisioned test-kit inputs. + * Global generation support does not identify a generation route in a mixed + * catalog: the automatic brief recipe is therefore used only when the catalog + * has exactly one build route (including legacy unaddressable build entries), + * and a complete, understood declaration proves there are no technical seeds. + * Other modes require a matching operator recipe, not inferred source assets. + */ +export declare function resolveCreativeBuildProbe(creative: unknown, spec: CreativeBuildProbeSpec, testKit?: unknown, adcpVersion?: string, governanceEnforcement?: unknown): CreativeBuildProbeResolution; diff --git a/patches/sdk-creative-build-probe/creative-build-probe.d.ts b/patches/sdk-creative-build-probe/creative-build-probe.d.ts new file mode 100644 index 00000000000..f7c6ecf072c --- /dev/null +++ b/patches/sdk-creative-build-probe/creative-build-probe.d.ts @@ -0,0 +1,34 @@ +import type { FixtureMatchClause, StoryboardContext } from './types'; +export interface CreativeBuildProbeSpec { + context_prefix: string; + message: string; +} +export interface CreativeBuildProbeInputFixture { + match: FixtureMatchClause[]; + /** Operator-provisioned valid input for every route matched by this recipe. */ + input: Record; +} +export type CreativeBuildProbeResolution = { + status: 'resolved'; + capabilityId?: string; + formatId?: Record; + context: StoryboardContext; + source: 'generation' | 'test_kit'; + requirements: FixtureMatchClause[]; +} | { + status: 'unsatisfied'; + detail: string; +} | { + status: 'failed'; + detail: string; +}; +export declare function validateCreativeBuildProbeSpec(value: unknown): asserts value is CreativeBuildProbeSpec; +/** + * Uses only actual catalog entries and explicitly provisioned test-kit inputs. + * Global generation support does not identify a generation route in a mixed + * catalog: the automatic brief recipe is therefore used only when the catalog + * has exactly one build route (including legacy unaddressable build entries), + * and a complete, understood declaration proves there are no technical seeds. + * Other modes require a matching operator recipe, not inferred source assets. + */ +export declare function resolveCreativeBuildProbe(creative: unknown, spec: CreativeBuildProbeSpec, testKit?: unknown, adcpVersion?: string, governanceEnforcement?: unknown): CreativeBuildProbeResolution; diff --git a/patches/sdk-creative-build-probe/creative-build-probe.js b/patches/sdk-creative-build-probe/creative-build-probe.js new file mode 100644 index 00000000000..16b8b8e3134 --- /dev/null +++ b/patches/sdk-creative-build-probe/creative-build-probe.js @@ -0,0 +1,395 @@ +"use strict"; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var creative_build_probe_exports = {}; +__export(creative_build_probe_exports, { + prepareCreativeBuildProbe: () => prepareCreativeBuildProbe, + resolveCreativeBuildProbe: () => resolveCreativeBuildProbe, + validateCreativeBuildProbeSpec: () => validateCreativeBuildProbeSpec +}); +module.exports = __toCommonJS(creative_build_probe_exports); +var import_fixture_resolution = require('./fixture-resolution.js'); +var import_schema_loader = require('../../validation/schema-loader.js'); +var import_version = require('../../version.js'); +var import_redact_secrets = require('../../utils/redact-secrets.js'); +var import_json_depth = require('../../utils/json-depth.js'); +var import_resolve_agent_properties = require('../../discovery/resolve-agent-properties.js'); +const INPUT_FIELDS = /* @__PURE__ */ new Set([ + "message", + "creative_manifest", + "creative_id", + "transformer_id", + "config", + "target_format_id" +]); +const TRUST_FIELDS = /^(auth|private[_-]?key|bearer[_-]?token|id[_-]?token|jwk|jwks|jwks[_-]?uri|x[_-]api[_-]key|signing[_-]?key|secret[_-]?key|hmac[_-]?key|client[_-]?key)$/i; +const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value); +function at(value, ...keys) { + for (const key of keys) value = isRecord(value) ? value[key] : void 0; + return value; +} +function displayDiagnostic(value, limit) { + let displayed = ""; + for (const char of value) { + const escaped = JSON.stringify(char).slice(1, -1).replace(/[\u007f-\u009f\u2028\u2029]/g, (control) => `\\u${control.charCodeAt(0).toString(16).padStart(4, "0")}`); + if (displayed.length + escaped.length > limit - 1) return `${displayed}\u2026`; + displayed += escaped; + } + return displayed; +} +function displayUnknownKind(kind) { + return `"${displayDiagnostic(kind.slice(0, 128), 768)}"`; +} +function displayValidationErrors(errors) { + const message = errors.slice(0, 8).map((error) => `${(error.instancePath || "/").slice(0, 2048)} ${(error.message ?? "").slice(0, 2048)}`).join("; "); + return displayDiagnostic(message, 2048); +} +function validateCreativeBuildProbeSpec(value) { + if (!isRecord(value) || Object.keys(value).some((key) => key !== "context_prefix" && key !== "message") || typeof value.context_prefix !== "string" || !/^[a-z][a-z0-9_]*$/.test(value.context_prefix) || typeof value.message !== "string" || value.message.trim().length === 0) { + throw new Error("creative_build_probe requires context_prefix (lowercase identifier) and a nonempty message"); + } +} +function containsSecretField(value, depth = 0, seen = /* @__PURE__ */ new Set()) { + if (depth > import_json_depth.MAX_JSON_DEPTH) throw new Error("creative_build_probe: fixture nesting exceeds the JSON depth bound"); + if (typeof value === "string" && /^https?:\/\//i.test(value)) { + let url; + try { + url = new URL(value); + } catch { + throw new Error("creative_build_probe: invalid URL in fixture"); + } + return !!(url.username || url.password) || [...url.searchParams.keys()].some((key) => import_redact_secrets.SECRET_KEY_PATTERN.test(key) || TRUST_FIELDS.test(key)); + } + if (!value || typeof value !== "object") return false; + if (seen.has(value)) throw new Error("creative_build_probe: fixture must be acyclic JSON"); + seen.add(value); + const found = Array.isArray(value) ? value.some((item) => containsSecretField(item, depth + 1, seen)) : Object.entries(value).some( + ([key, item]) => import_redact_secrets.SECRET_KEY_PATTERN.test(key) || TRUST_FIELDS.test(key) || containsSecretField(item, depth + 1, seen) + ); + seen.delete(value); + return found; +} +function validateRecipeBudget(value, location) { + let nodes = 0; + let bytes = 0; + const ancestors = /* @__PURE__ */ new Set(); + const visit = (item, depth) => { + if (++nodes > 1e5 || depth > import_json_depth.MAX_JSON_DEPTH) + throw new Error(`${location}: recipe exceeds the fixture node/depth budget`); + if (typeof item === "string") bytes += Buffer.byteLength(item, "utf8"); + if (bytes > 4 * 1024 * 1024) throw new Error(`${location}: recipe exceeds the 4 MiB fixture string budget`); + if (!item || typeof item !== "object") return; + if (ancestors.has(item)) throw new Error(`${location}: recipe must be acyclic JSON`); + ancestors.add(item); + for (const [key, child] of Object.entries(item)) { + bytes += Buffer.byteLength(key, "utf8"); + visit(child, depth + 1); + } + ancestors.delete(item); + }; + visit(value, 0); +} +function validateInput(value, location, validateManifest) { + if (!isRecord(value) || Object.keys(value).some((key) => !INPUT_FIELDS.has(key))) { + throw new Error( + `${location}: input may only contain message, creative_manifest, creative_id, transformer_id, config and target_format_id` + ); + } + if (containsSecretField(value)) + throw new Error(`${location}: input must not carry credential or trust-material fields`); + for (const key of ["message", "creative_id", "transformer_id"]) { + if (value[key] !== void 0 && (typeof value[key] !== "string" || value[key].trim().length === 0)) { + throw new Error(`${location}: ${key} must be a nonempty string`); + } + } + if (value.config !== void 0 && (!isRecord(value.config) || !value.transformer_id)) + throw new Error( + `${location}: this probe contract requires config to be an object bound to a provisioned transformer_id` + ); + if (value.creative_id && value.creative_manifest) + throw new Error( + `${location}: this probe contract requires one unambiguous source, not both creative_id and creative_manifest` + ); + if (value.creative_manifest !== void 0) { + const manifest = value.creative_manifest; + if (!validateManifest || !validateManifest(manifest)) { + throw new Error(`${location}: creative_manifest must be a complete manifest, including format and assets`); + } + } + if (!value.message && !value.creative_manifest && !value.creative_id && !value.transformer_id) { + throw new Error( + `${location}: input must identify a brief, source manifest or provisioned library creative/transformer` + ); + } +} +function resolveCreativeBuildProbe(creative, spec, testKit, adcpVersion, governanceEnforcement) { + return resolveCreativeBuildProbeWithSchemas( + creative, + spec, + testKit, + governanceEnforcement, + adcpVersion, + captureRequestSchemas(adcpVersion) + ); +} +function captureRequestSchemas(adcpVersion) { + return { + request: (0, import_schema_loader.getSchemaDocumentByRef)("media-buy/build-creative-request.json", adcpVersion)?.schema, + validateManifest: (0, import_schema_loader.getSchemaValidatorByRef)("core/creative-manifest.json", adcpVersion), + validateFormatId: (0, import_schema_loader.getSchemaValidatorByRef)("core/format-id.json", adcpVersion), + formatIdProperties: at((0, import_schema_loader.getSchemaDocumentByRef)("core/format-id.json", adcpVersion)?.schema, "properties") + }; +} +function prepareCreativeBuildProbe(capabilities, spec, testKit, adcpVersion) { + const requestSchemas = captureRequestSchemas(adcpVersion); + return (0, import_schema_loader.withServedValidationVersion)( + capabilities, + adcpVersion ?? import_version.ADCP_VERSION, + (catalogVersion) => resolveCreativeBuildProbeWithSchemas( + at(capabilities, "creative"), + spec, + testKit, + at(capabilities, "adcp", "governance_enforcement"), + catalogVersion, + requestSchemas + ) + ); +} +function resolveCreativeBuildProbeWithSchemas(creative, spec, testKit, governanceEnforcement, catalogAdcpVersion, requestSchemas) { + validateCreativeBuildProbeSpec(spec); + if (isRecord(creative) && creative.supported_formats !== void 0 && !Array.isArray(creative.supported_formats)) { + return { status: "failed", detail: "creative.supported_formats: malformed catalog (expected array)" }; + } + if (!isRecord(creative) || !Array.isArray(creative.supported_formats)) { + return { + status: "unsatisfied", + detail: `fixture_unsatisfied: creative_format:${spec.context_prefix}: no advertised creative build catalog` + }; + } + const recipes = isRecord(testKit) && testKit.creative_build_probes !== void 0 ? testKit.creative_build_probes : []; + if (!Array.isArray(recipes)) throw new Error("test_kit.creative_build_probes must be an array"); + if (recipes.some( + (recipe) => isRecord(recipe) && isRecord(recipe.input) && recipe.input.creative_manifest !== void 0 + ) && !requestSchemas.validateManifest) { + return { + status: "unsatisfied", + detail: `fixture_unsatisfied: creative_format:${spec.context_prefix}: selected-version source manifest schema validator is unavailable` + }; + } + const normalized = recipes.map((recipe, index) => { + const location = `test_kit.creative_build_probes[${index}]`; + validateRecipeBudget(recipe, location); + if (!isRecord(recipe) || Object.keys(recipe).some((key) => key !== "match" && key !== "input")) { + throw new Error(`${location}: expected only match and input`); + } + const match = (0, import_fixture_resolution.normalizeFixtureMatchExpression)(recipe.match, `${location}.match`); + if (match.length === 0) throw new Error(`${location}: at least one explicit match predicate is required`); + validateInput(recipe.input, location, requestSchemas.validateManifest); + return { match, input: recipe.input }; + }); + const catalogDocument = (0, import_schema_loader.getSchemaDocumentByRef)("protocol/get-adcp-capabilities-response.json", catalogAdcpVersion); + const catalog = catalogDocument?.schema; + const selectedVersion = catalogDocument?.resolvedVersion ?? catalogAdcpVersion ?? "selected bundle"; + const item = at(catalog, "properties", "creative", "properties", "supported_formats", "items"); + const request = requestSchemas.request; + const canonicalSelector = !!at(request, "properties", "target_capability_id"); + const legacySelector = !!at(request, "properties", "target_format_id"); + const catalogRequest = (0, import_schema_loader.getSchemaDocumentByRef)("media-buy/build-creative-request.json", catalogAdcpVersion)?.schema; + const canonicalCatalog = !!at(catalogRequest, "properties", "target_capability_id"); + const requestIdPattern = at(request, "properties", "target_capability_id", "pattern"); + const canonicalAddressable = (route) => canonicalCatalog && canonicalSelector && typeof route.capability_id === "string" && (typeof requestIdPattern !== "string" || new RegExp(requestIdPattern).test(route.capability_id)); + const ref = at(item, "properties", "format", "$ref"); + const formatRef = typeof ref === "string" ? ref.replace(/^(?:https:\/\/adcontextprotocol\.org)?\/schemas\/(?:(?:v?\d[^/]*|latest)\/)?/, "") : void 0; + const validateFormat = formatRef ? (0, import_schema_loader.getSchemaValidatorByRef)(formatRef, catalogAdcpVersion) : void 0; + const validateKind = (0, import_schema_loader.getSchemaValidatorByRef)("core/canonical-format-kind.json", catalogAdcpVersion); + if (!validateFormat || !validateKind || !catalogRequest || !canonicalSelector && !legacySelector) { + return { + status: "unsatisfied", + detail: `fixture_unsatisfied: creative_format:${spec.context_prefix}: selected-version creative catalog schema validators are unavailable` + }; + } + for (const recipe of normalized) { + if (recipe.input.target_format_id !== void 0) { + if (!legacySelector) + throw new Error( + "test_kit.creative_build_probes: target_format_id is not supported by the selected build request" + ); + const validateId = requestSchemas.validateFormatId; + if (!validateId) + return { + status: "unsatisfied", + detail: `fixture_unsatisfied: creative_format:${spec.context_prefix}: selected-version legacy FormatID schema validator is unavailable` + }; + if (!validateId(recipe.input.target_format_id)) + throw new Error( + "test_kit.creative_build_probes: target_format_id must be a selected-version valid provisioned FormatID" + ); + } + } + const idPattern = at(item, "properties", "capability_id", "pattern"); + const hasOperations = at(item, "properties", "operations") !== void 0; + const isBuildRoute = (entry) => !hasOperations || entry.operations === void 0 || Array.isArray(entry.operations) && entry.operations.includes("build"); + const selectorProperties = at( + (0, import_schema_loader.getSchemaDocumentByRef)("core/format-id.json", catalogAdcpVersion)?.schema, + "properties" + ); + const requestSelectorProperties = requestSchemas.formatIdProperties; + const requestHasPixelRatio = at(requestSelectorProperties, "pixel_ratio") !== void 0; + const legacyDimensions = ["width", "height", "duration_ms", "pixel_ratio"].filter( + (key) => at(selectorProperties, key) !== void 0 + ); + const unavailableKinds = /* @__PURE__ */ new Set(); + const ids = /* @__PURE__ */ new Set(); + const buildRoutes = []; + for (const [index, entry] of creative.supported_formats.entries()) { + const location = `creative.supported_formats[${index}]`; + const invalid = (detail) => ({ + status: "failed", + detail: `${location}: ${detail}` + }); + if (!isRecord(entry)) return invalid("malformed catalog entry (expected object)"); + if (entry.capability_id !== void 0) { + if (typeof entry.capability_id !== "string" || typeof idPattern === "string" && !new RegExp(idPattern).test(entry.capability_id) || canonicalCatalog && ids.has(entry.capability_id)) { + return invalid("capability_id must be usable and unique within the catalog"); + } + ids.add(entry.capability_id); + } + const operations = entry.operations; + if (hasOperations && operations !== void 0 && (!Array.isArray(operations) || operations.length === 0 || operations.some( + (operation, index2) => typeof operation !== "string" || !["build", "validate", "preview"].includes(operation) || operations.indexOf(operation) !== index2 + ))) { + return invalid("operations must be a nonempty unique array of build, validate or preview"); + } + if (!isRecord(entry.format) || typeof entry.format.format_kind !== "string" || !isRecord(entry.format.params)) { + return invalid("format must declare format_kind and params"); + } + if (!canonicalCatalog && !validateKind(entry.format.format_kind)) { + unavailableKinds.add(entry.format.format_kind); + if (isBuildRoute(entry)) buildRoutes.push(entry); + continue; + } + if (!validateFormat(entry.format) || !validateKind(entry.format.format_kind)) { + return invalid( + `malformed format declaration: ${displayValidationErrors([...validateFormat.errors ?? [], ...validateKind.errors ?? []])}` + ); + } + const validateParams = (0, import_schema_loader.getSchemaValidatorByRef)( + `formats/canonical/${entry.format.format_kind}.json`, + catalogAdcpVersion + ); + if (entry.format.format_kind !== "custom" && !validateParams) { + unavailableKinds.add(entry.format.format_kind); + if (isBuildRoute(entry)) buildRoutes.push(entry); + continue; + } + if (validateParams && !validateParams(entry.format.params) || validateFormat && !validateFormat(entry.format) || validateKind && !validateKind(entry.format.format_kind)) { + return invalid( + `malformed format declaration: ${displayValidationErrors([...validateParams?.errors ?? [], ...validateFormat?.errors ?? [], ...validateKind?.errors ?? []])}` + ); + } + if (isBuildRoute(entry)) buildRoutes.push(entry); + } + if (isRecord(governanceEnforcement) && Array.isArray(governanceEnforcement.tasks) && governanceEnforcement.tasks.some( + (task) => isRecord(task) && task.task === "build_creative" && Array.isArray(task.modes) && task.modes.includes("signed_context") + )) { + return { + status: "unsatisfied", + detail: `fixture_unsatisfied: creative_format:${spec.context_prefix}: payload-bound build governance authorization is not prepared by this runner contract` + }; + } + for (const route of buildRoutes) { + const knownKind = validateKind(route.format.format_kind); + if (!knownKind || route.format.format_kind !== "custom" && !(0, import_schema_loader.getSchemaValidatorByRef)( + `formats/canonical/${route.format.format_kind}.json`, + catalogAdcpVersion + )) + continue; + let source = "test_kit"; + const usable = (input2) => { + if (input2.transformer_id) return creative.supports_transformers === true; + if (input2.creative_id) return creative.has_creative_library === true; + if (input2.creative_manifest) + return creative.supports_transformation === true || creative.supports_generation === true; + return creative.supports_generation === true; + }; + const format = route.format; + const legacyBindingAgrees = (input2) => { + if (input2.target_format_id === void 0) return canonicalAddressable(route); + if (format.canonical_formats_only === true || !isRecord(input2.target_format_id)) return false; + if (!Array.isArray(format.v1_format_ref)) return true; + const target = input2.target_format_id; + const targetUrl = typeof target.agent_url === "string" ? (0, import_resolve_agent_properties.canonicalizeAgentUrl)(target.agent_url) : null; + if (targetUrl === null) return false; + return format.v1_format_ref.some( + (ref2) => isRecord(ref2) && ref2.id === target.id && typeof ref2.agent_url === "string" && typeof target.agent_url === "string" && (0, import_resolve_agent_properties.canonicalizeAgentUrl)(ref2.agent_url) === targetUrl && legacyDimensions.every( + (key) => key === "pixel_ratio" ? (ref2[key] ?? 1) === (requestHasPixelRatio ? target[key] ?? 1 : 1) : ref2[key] === void 0 || ref2[key] === target[key] + ) + ); + }; + const selectedRecipe = normalized.find( + (recipe) => (0, import_fixture_resolution.matchesFixtureRequirements)(route, recipe.match) && usable(recipe.input) && legacyBindingAgrees(recipe.input) + ); + let input = selectedRecipe?.input; + const kindValidator = (0, import_schema_loader.getSchemaValidatorByRef)("core/canonical-format-kind.json", catalogAdcpVersion); + const emptyCompleteFormat = format.technical_requirements_complete === true && format.format_kind !== "custom" && kindValidator?.(format.format_kind) === true && Object.keys(format.params).length === 0 && Object.keys(format).every( + (key) => [ + "format_kind", + "params", + "technical_requirements_complete", + "display_name", + "seller_preference", + "experimental", + "sample_render_url", + "format_option_id", + "publisher_domain", + "applies_to_channels", + "canonical_formats_only" + ].includes(key) + ) && Object.keys(route).every((key) => ["capability_id", "operations", "format"].includes(key)); + if (!input && canonicalAddressable(route) && creative.supports_generation === true && buildRoutes.length === 1 && emptyCompleteFormat) { + source = "generation"; + const manifest = { format_kind: format.format_kind, assets: {} }; + const validateManifest = requestSchemas.validateManifest; + if (!validateManifest?.(manifest)) continue; + input = { message: spec.message, creative_manifest: manifest }; + } + if (!input) continue; + const useCanonicalSelector = input.target_format_id === void 0; + const context = useCanonicalSelector ? { [`${spec.context_prefix}_capability_id`]: route.capability_id } : { [`${spec.context_prefix}_target_format_id`]: structuredClone(input.target_format_id) }; + for (const key of ["message", "creative_manifest", "creative_id", "transformer_id", "config"]) { + if (input[key] !== void 0) context[`${spec.context_prefix}_${key}`] = structuredClone(input[key]); + } + return { + status: "resolved", + ...useCanonicalSelector ? { capabilityId: route.capability_id } : { formatId: structuredClone(input.target_format_id) }, + context, + source, + requirements: selectedRecipe?.match ?? [] + }; + } + return { + status: "unsatisfied", + detail: `fixture_unsatisfied: creative_format:${spec.context_prefix}: no routable build entry has a known-valid generation input or a matching provisioned test-kit recipe${unavailableKinds.size ? `; ${unavailableKinds.size} unvalidated kind(s) in schema bundle ${selectedVersion}: ${[...unavailableKinds].slice(0, 8).map(displayUnknownKind).join(", ")}${unavailableKinds.size > 8 ? ", \u2026" : ""}` : ""}` + }; +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + prepareCreativeBuildProbe, + resolveCreativeBuildProbe, + validateCreativeBuildProbeSpec +}); diff --git a/patches/sdk-creative-build-probe/creative-build-probe.mjs b/patches/sdk-creative-build-probe/creative-build-probe.mjs new file mode 100644 index 00000000000..745cd82ff72 --- /dev/null +++ b/patches/sdk-creative-build-probe/creative-build-probe.mjs @@ -0,0 +1,373 @@ +import { matchesFixtureRequirements, normalizeFixtureMatchExpression } from "./fixture-resolution.mjs"; +import { + getSchemaDocumentByRef, + getSchemaValidatorByRef, + withServedValidationVersion +} from "../../validation/schema-loader.mjs"; +import { ADCP_VERSION } from "../../version.mjs"; +import { SECRET_KEY_PATTERN } from "../../utils/redact-secrets.mjs"; +import { MAX_JSON_DEPTH } from "../../utils/json-depth.mjs"; +import { canonicalizeAgentUrl } from "../../discovery/resolve-agent-properties.mjs"; +const INPUT_FIELDS = /* @__PURE__ */ new Set([ + "message", + "creative_manifest", + "creative_id", + "transformer_id", + "config", + "target_format_id" +]); +const TRUST_FIELDS = /^(auth|private[_-]?key|bearer[_-]?token|id[_-]?token|jwk|jwks|jwks[_-]?uri|x[_-]api[_-]key|signing[_-]?key|secret[_-]?key|hmac[_-]?key|client[_-]?key)$/i; +const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value); +function at(value, ...keys) { + for (const key of keys) value = isRecord(value) ? value[key] : void 0; + return value; +} +function displayDiagnostic(value, limit) { + let displayed = ""; + for (const char of value) { + const escaped = JSON.stringify(char).slice(1, -1).replace(/[\u007f-\u009f\u2028\u2029]/g, (control) => `\\u${control.charCodeAt(0).toString(16).padStart(4, "0")}`); + if (displayed.length + escaped.length > limit - 1) return `${displayed}\u2026`; + displayed += escaped; + } + return displayed; +} +function displayUnknownKind(kind) { + return `"${displayDiagnostic(kind.slice(0, 128), 768)}"`; +} +function displayValidationErrors(errors) { + const message = errors.slice(0, 8).map((error) => `${(error.instancePath || "/").slice(0, 2048)} ${(error.message ?? "").slice(0, 2048)}`).join("; "); + return displayDiagnostic(message, 2048); +} +function validateCreativeBuildProbeSpec(value) { + if (!isRecord(value) || Object.keys(value).some((key) => key !== "context_prefix" && key !== "message") || typeof value.context_prefix !== "string" || !/^[a-z][a-z0-9_]*$/.test(value.context_prefix) || typeof value.message !== "string" || value.message.trim().length === 0) { + throw new Error("creative_build_probe requires context_prefix (lowercase identifier) and a nonempty message"); + } +} +function containsSecretField(value, depth = 0, seen = /* @__PURE__ */ new Set()) { + if (depth > MAX_JSON_DEPTH) throw new Error("creative_build_probe: fixture nesting exceeds the JSON depth bound"); + if (typeof value === "string" && /^https?:\/\//i.test(value)) { + let url; + try { + url = new URL(value); + } catch { + throw new Error("creative_build_probe: invalid URL in fixture"); + } + return !!(url.username || url.password) || [...url.searchParams.keys()].some((key) => SECRET_KEY_PATTERN.test(key) || TRUST_FIELDS.test(key)); + } + if (!value || typeof value !== "object") return false; + if (seen.has(value)) throw new Error("creative_build_probe: fixture must be acyclic JSON"); + seen.add(value); + const found = Array.isArray(value) ? value.some((item) => containsSecretField(item, depth + 1, seen)) : Object.entries(value).some( + ([key, item]) => SECRET_KEY_PATTERN.test(key) || TRUST_FIELDS.test(key) || containsSecretField(item, depth + 1, seen) + ); + seen.delete(value); + return found; +} +function validateRecipeBudget(value, location) { + let nodes = 0; + let bytes = 0; + const ancestors = /* @__PURE__ */ new Set(); + const visit = (item, depth) => { + if (++nodes > 1e5 || depth > MAX_JSON_DEPTH) + throw new Error(`${location}: recipe exceeds the fixture node/depth budget`); + if (typeof item === "string") bytes += Buffer.byteLength(item, "utf8"); + if (bytes > 4 * 1024 * 1024) throw new Error(`${location}: recipe exceeds the 4 MiB fixture string budget`); + if (!item || typeof item !== "object") return; + if (ancestors.has(item)) throw new Error(`${location}: recipe must be acyclic JSON`); + ancestors.add(item); + for (const [key, child] of Object.entries(item)) { + bytes += Buffer.byteLength(key, "utf8"); + visit(child, depth + 1); + } + ancestors.delete(item); + }; + visit(value, 0); +} +function validateInput(value, location, validateManifest) { + if (!isRecord(value) || Object.keys(value).some((key) => !INPUT_FIELDS.has(key))) { + throw new Error( + `${location}: input may only contain message, creative_manifest, creative_id, transformer_id, config and target_format_id` + ); + } + if (containsSecretField(value)) + throw new Error(`${location}: input must not carry credential or trust-material fields`); + for (const key of ["message", "creative_id", "transformer_id"]) { + if (value[key] !== void 0 && (typeof value[key] !== "string" || value[key].trim().length === 0)) { + throw new Error(`${location}: ${key} must be a nonempty string`); + } + } + if (value.config !== void 0 && (!isRecord(value.config) || !value.transformer_id)) + throw new Error( + `${location}: this probe contract requires config to be an object bound to a provisioned transformer_id` + ); + if (value.creative_id && value.creative_manifest) + throw new Error( + `${location}: this probe contract requires one unambiguous source, not both creative_id and creative_manifest` + ); + if (value.creative_manifest !== void 0) { + const manifest = value.creative_manifest; + if (!validateManifest || !validateManifest(manifest)) { + throw new Error(`${location}: creative_manifest must be a complete manifest, including format and assets`); + } + } + if (!value.message && !value.creative_manifest && !value.creative_id && !value.transformer_id) { + throw new Error( + `${location}: input must identify a brief, source manifest or provisioned library creative/transformer` + ); + } +} +function resolveCreativeBuildProbe(creative, spec, testKit, adcpVersion, governanceEnforcement) { + return resolveCreativeBuildProbeWithSchemas( + creative, + spec, + testKit, + governanceEnforcement, + adcpVersion, + captureRequestSchemas(adcpVersion) + ); +} +function captureRequestSchemas(adcpVersion) { + return { + request: getSchemaDocumentByRef("media-buy/build-creative-request.json", adcpVersion)?.schema, + validateManifest: getSchemaValidatorByRef("core/creative-manifest.json", adcpVersion), + validateFormatId: getSchemaValidatorByRef("core/format-id.json", adcpVersion), + formatIdProperties: at(getSchemaDocumentByRef("core/format-id.json", adcpVersion)?.schema, "properties") + }; +} +function prepareCreativeBuildProbe(capabilities, spec, testKit, adcpVersion) { + const requestSchemas = captureRequestSchemas(adcpVersion); + return withServedValidationVersion( + capabilities, + adcpVersion ?? ADCP_VERSION, + (catalogVersion) => resolveCreativeBuildProbeWithSchemas( + at(capabilities, "creative"), + spec, + testKit, + at(capabilities, "adcp", "governance_enforcement"), + catalogVersion, + requestSchemas + ) + ); +} +function resolveCreativeBuildProbeWithSchemas(creative, spec, testKit, governanceEnforcement, catalogAdcpVersion, requestSchemas) { + validateCreativeBuildProbeSpec(spec); + if (isRecord(creative) && creative.supported_formats !== void 0 && !Array.isArray(creative.supported_formats)) { + return { status: "failed", detail: "creative.supported_formats: malformed catalog (expected array)" }; + } + if (!isRecord(creative) || !Array.isArray(creative.supported_formats)) { + return { + status: "unsatisfied", + detail: `fixture_unsatisfied: creative_format:${spec.context_prefix}: no advertised creative build catalog` + }; + } + const recipes = isRecord(testKit) && testKit.creative_build_probes !== void 0 ? testKit.creative_build_probes : []; + if (!Array.isArray(recipes)) throw new Error("test_kit.creative_build_probes must be an array"); + if (recipes.some( + (recipe) => isRecord(recipe) && isRecord(recipe.input) && recipe.input.creative_manifest !== void 0 + ) && !requestSchemas.validateManifest) { + return { + status: "unsatisfied", + detail: `fixture_unsatisfied: creative_format:${spec.context_prefix}: selected-version source manifest schema validator is unavailable` + }; + } + const normalized = recipes.map((recipe, index) => { + const location = `test_kit.creative_build_probes[${index}]`; + validateRecipeBudget(recipe, location); + if (!isRecord(recipe) || Object.keys(recipe).some((key) => key !== "match" && key !== "input")) { + throw new Error(`${location}: expected only match and input`); + } + const match = normalizeFixtureMatchExpression(recipe.match, `${location}.match`); + if (match.length === 0) throw new Error(`${location}: at least one explicit match predicate is required`); + validateInput(recipe.input, location, requestSchemas.validateManifest); + return { match, input: recipe.input }; + }); + const catalogDocument = getSchemaDocumentByRef("protocol/get-adcp-capabilities-response.json", catalogAdcpVersion); + const catalog = catalogDocument?.schema; + const selectedVersion = catalogDocument?.resolvedVersion ?? catalogAdcpVersion ?? "selected bundle"; + const item = at(catalog, "properties", "creative", "properties", "supported_formats", "items"); + const request = requestSchemas.request; + const canonicalSelector = !!at(request, "properties", "target_capability_id"); + const legacySelector = !!at(request, "properties", "target_format_id"); + const catalogRequest = getSchemaDocumentByRef("media-buy/build-creative-request.json", catalogAdcpVersion)?.schema; + const canonicalCatalog = !!at(catalogRequest, "properties", "target_capability_id"); + const requestIdPattern = at(request, "properties", "target_capability_id", "pattern"); + const canonicalAddressable = (route) => canonicalCatalog && canonicalSelector && typeof route.capability_id === "string" && (typeof requestIdPattern !== "string" || new RegExp(requestIdPattern).test(route.capability_id)); + const ref = at(item, "properties", "format", "$ref"); + const formatRef = typeof ref === "string" ? ref.replace(/^(?:https:\/\/adcontextprotocol\.org)?\/schemas\/(?:(?:v?\d[^/]*|latest)\/)?/, "") : void 0; + const validateFormat = formatRef ? getSchemaValidatorByRef(formatRef, catalogAdcpVersion) : void 0; + const validateKind = getSchemaValidatorByRef("core/canonical-format-kind.json", catalogAdcpVersion); + if (!validateFormat || !validateKind || !catalogRequest || !canonicalSelector && !legacySelector) { + return { + status: "unsatisfied", + detail: `fixture_unsatisfied: creative_format:${spec.context_prefix}: selected-version creative catalog schema validators are unavailable` + }; + } + for (const recipe of normalized) { + if (recipe.input.target_format_id !== void 0) { + if (!legacySelector) + throw new Error( + "test_kit.creative_build_probes: target_format_id is not supported by the selected build request" + ); + const validateId = requestSchemas.validateFormatId; + if (!validateId) + return { + status: "unsatisfied", + detail: `fixture_unsatisfied: creative_format:${spec.context_prefix}: selected-version legacy FormatID schema validator is unavailable` + }; + if (!validateId(recipe.input.target_format_id)) + throw new Error( + "test_kit.creative_build_probes: target_format_id must be a selected-version valid provisioned FormatID" + ); + } + } + const idPattern = at(item, "properties", "capability_id", "pattern"); + const hasOperations = at(item, "properties", "operations") !== void 0; + const isBuildRoute = (entry) => !hasOperations || entry.operations === void 0 || Array.isArray(entry.operations) && entry.operations.includes("build"); + const selectorProperties = at( + getSchemaDocumentByRef("core/format-id.json", catalogAdcpVersion)?.schema, + "properties" + ); + const requestSelectorProperties = requestSchemas.formatIdProperties; + const requestHasPixelRatio = at(requestSelectorProperties, "pixel_ratio") !== void 0; + const legacyDimensions = ["width", "height", "duration_ms", "pixel_ratio"].filter( + (key) => at(selectorProperties, key) !== void 0 + ); + const unavailableKinds = /* @__PURE__ */ new Set(); + const ids = /* @__PURE__ */ new Set(); + const buildRoutes = []; + for (const [index, entry] of creative.supported_formats.entries()) { + const location = `creative.supported_formats[${index}]`; + const invalid = (detail) => ({ + status: "failed", + detail: `${location}: ${detail}` + }); + if (!isRecord(entry)) return invalid("malformed catalog entry (expected object)"); + if (entry.capability_id !== void 0) { + if (typeof entry.capability_id !== "string" || typeof idPattern === "string" && !new RegExp(idPattern).test(entry.capability_id) || canonicalCatalog && ids.has(entry.capability_id)) { + return invalid("capability_id must be usable and unique within the catalog"); + } + ids.add(entry.capability_id); + } + const operations = entry.operations; + if (hasOperations && operations !== void 0 && (!Array.isArray(operations) || operations.length === 0 || operations.some( + (operation, index2) => typeof operation !== "string" || !["build", "validate", "preview"].includes(operation) || operations.indexOf(operation) !== index2 + ))) { + return invalid("operations must be a nonempty unique array of build, validate or preview"); + } + if (!isRecord(entry.format) || typeof entry.format.format_kind !== "string" || !isRecord(entry.format.params)) { + return invalid("format must declare format_kind and params"); + } + if (!canonicalCatalog && !validateKind(entry.format.format_kind)) { + unavailableKinds.add(entry.format.format_kind); + if (isBuildRoute(entry)) buildRoutes.push(entry); + continue; + } + if (!validateFormat(entry.format) || !validateKind(entry.format.format_kind)) { + return invalid( + `malformed format declaration: ${displayValidationErrors([...validateFormat.errors ?? [], ...validateKind.errors ?? []])}` + ); + } + const validateParams = getSchemaValidatorByRef( + `formats/canonical/${entry.format.format_kind}.json`, + catalogAdcpVersion + ); + if (entry.format.format_kind !== "custom" && !validateParams) { + unavailableKinds.add(entry.format.format_kind); + if (isBuildRoute(entry)) buildRoutes.push(entry); + continue; + } + if (validateParams && !validateParams(entry.format.params) || validateFormat && !validateFormat(entry.format) || validateKind && !validateKind(entry.format.format_kind)) { + return invalid( + `malformed format declaration: ${displayValidationErrors([...validateParams?.errors ?? [], ...validateFormat?.errors ?? [], ...validateKind?.errors ?? []])}` + ); + } + if (isBuildRoute(entry)) buildRoutes.push(entry); + } + if (isRecord(governanceEnforcement) && Array.isArray(governanceEnforcement.tasks) && governanceEnforcement.tasks.some( + (task) => isRecord(task) && task.task === "build_creative" && Array.isArray(task.modes) && task.modes.includes("signed_context") + )) { + return { + status: "unsatisfied", + detail: `fixture_unsatisfied: creative_format:${spec.context_prefix}: payload-bound build governance authorization is not prepared by this runner contract` + }; + } + for (const route of buildRoutes) { + const knownKind = validateKind(route.format.format_kind); + if (!knownKind || route.format.format_kind !== "custom" && !getSchemaValidatorByRef( + `formats/canonical/${route.format.format_kind}.json`, + catalogAdcpVersion + )) + continue; + let source = "test_kit"; + const usable = (input2) => { + if (input2.transformer_id) return creative.supports_transformers === true; + if (input2.creative_id) return creative.has_creative_library === true; + if (input2.creative_manifest) + return creative.supports_transformation === true || creative.supports_generation === true; + return creative.supports_generation === true; + }; + const format = route.format; + const legacyBindingAgrees = (input2) => { + if (input2.target_format_id === void 0) return canonicalAddressable(route); + if (format.canonical_formats_only === true || !isRecord(input2.target_format_id)) return false; + if (!Array.isArray(format.v1_format_ref)) return true; + const target = input2.target_format_id; + const targetUrl = typeof target.agent_url === "string" ? canonicalizeAgentUrl(target.agent_url) : null; + if (targetUrl === null) return false; + return format.v1_format_ref.some( + (ref2) => isRecord(ref2) && ref2.id === target.id && typeof ref2.agent_url === "string" && typeof target.agent_url === "string" && canonicalizeAgentUrl(ref2.agent_url) === targetUrl && legacyDimensions.every( + (key) => key === "pixel_ratio" ? (ref2[key] ?? 1) === (requestHasPixelRatio ? target[key] ?? 1 : 1) : ref2[key] === void 0 || ref2[key] === target[key] + ) + ); + }; + const selectedRecipe = normalized.find( + (recipe) => matchesFixtureRequirements(route, recipe.match) && usable(recipe.input) && legacyBindingAgrees(recipe.input) + ); + let input = selectedRecipe?.input; + const kindValidator = getSchemaValidatorByRef("core/canonical-format-kind.json", catalogAdcpVersion); + const emptyCompleteFormat = format.technical_requirements_complete === true && format.format_kind !== "custom" && kindValidator?.(format.format_kind) === true && Object.keys(format.params).length === 0 && Object.keys(format).every( + (key) => [ + "format_kind", + "params", + "technical_requirements_complete", + "display_name", + "seller_preference", + "experimental", + "sample_render_url", + "format_option_id", + "publisher_domain", + "applies_to_channels", + "canonical_formats_only" + ].includes(key) + ) && Object.keys(route).every((key) => ["capability_id", "operations", "format"].includes(key)); + if (!input && canonicalAddressable(route) && creative.supports_generation === true && buildRoutes.length === 1 && emptyCompleteFormat) { + source = "generation"; + const manifest = { format_kind: format.format_kind, assets: {} }; + const validateManifest = requestSchemas.validateManifest; + if (!validateManifest?.(manifest)) continue; + input = { message: spec.message, creative_manifest: manifest }; + } + if (!input) continue; + const useCanonicalSelector = input.target_format_id === void 0; + const context = useCanonicalSelector ? { [`${spec.context_prefix}_capability_id`]: route.capability_id } : { [`${spec.context_prefix}_target_format_id`]: structuredClone(input.target_format_id) }; + for (const key of ["message", "creative_manifest", "creative_id", "transformer_id", "config"]) { + if (input[key] !== void 0) context[`${spec.context_prefix}_${key}`] = structuredClone(input[key]); + } + return { + status: "resolved", + ...useCanonicalSelector ? { capabilityId: route.capability_id } : { formatId: structuredClone(input.target_format_id) }, + context, + source, + requirements: selectedRecipe?.match ?? [] + }; + } + return { + status: "unsatisfied", + detail: `fixture_unsatisfied: creative_format:${spec.context_prefix}: no routable build entry has a known-valid generation input or a matching provisioned test-kit recipe${unavailableKinds.size ? `; ${unavailableKinds.size} unvalidated kind(s) in schema bundle ${selectedVersion}: ${[...unavailableKinds].slice(0, 8).map(displayUnknownKind).join(", ")}${unavailableKinds.size > 8 ? ", \u2026" : ""}` : ""}` + }; +} +export { + prepareCreativeBuildProbe, + resolveCreativeBuildProbe, + validateCreativeBuildProbeSpec +}; diff --git a/patches/sdk-creative-build-probe/manifest.json b/patches/sdk-creative-build-probe/manifest.json new file mode 100644 index 00000000000..338b4374ad0 --- /dev/null +++ b/patches/sdk-creative-build-probe/manifest.json @@ -0,0 +1,434 @@ +{ + "package": "@adcp/sdk", + "version": "14.0.0-rc.53", + "provenance": { + "main_fix": "7cf1eddf8050f052fa40c830d1953eecd7db5e52", + "evaluator_source": "9f932f174e8125e00ec55d64db2a292cba2be7f9", + "served_authority_prerequisite": "@adcp/sdk@15.9.5 served-selection additions with explicit SDK14 maintenance delta: local creative_build_probe ALS opt-in before preflight, paired actual JSON catalog authority, stock semantics elsewhere; no complete module identity or whole-SDK parity claim, no OAuth/TaskExecutor changes" + }, + "files": [ + { + "path": "dist/lib/testing/storyboard/runner.js", + "before": "8a78c2f3f24a70758d6a9f244d0762b08ac169aba8098e9c90cdad4f8b4bf058", + "after": "e3a145aadb7a7fae00fd4d5d293b715e5345cab562482ab99c3c84064fdb07e6", + "replacements": [ + { + "from": "var import_node_crypto = require(\"node:crypto\");\n", + "to": "var import_creative_build_probe = require(\"./creative-build-probe.js\");\nvar import_node_crypto = require(\"node:crypto\");\n" + }, + { + "from": " let seedingFailed = false;", + "to": " let seedingFailed = false;\n let creativeBuildCatalogFailed = false;" + }, + { + "from": " let fixtureCoverageGap;\n {\n let seeding;", + "to": " let fixtureCoverageGap;\n if (storyboard.creative_build_probe && !skipControllerSeedingForPhaseGates) {\n let resolution;\n try {\n resolution = routingContext || isMultiInstance ? {\n status: \"unsatisfied\",\n detail: `fixture_unsatisfied: creative_format:${storyboard.creative_build_probe.context_prefix}: resolve an independent fixture for each routed or replicated agent in a single-agent run`\n } : (0, import_creative_build_probe.prepareCreativeBuildProbe)(\n profile?.raw_capabilities,\n storyboard.creative_build_probe,\n options.test_kit,\n options.adcpVersion\n );\n } catch (error) {\n await Promise.allSettled([\n ...webhookReceiver ? [webhookReceiver.close()] : [],\n ...!callerOwnsClients ? [(0, import_protocols.closeScopedConnections)(options.protocol)] : []\n ]);\n throw error;\n }\n const handle = storyboard.creative_build_probe.context_prefix;\n fixtureResolutionRecords = [\n {\n fixture_type: \"creative_format\",\n handle,\n requirements: resolution.status === \"resolved\" ? resolution.requirements : [],\n status: resolution.status,\n ...resolution.status === \"resolved\" ? {\n seller_ids: resolution.capabilityId !== void 0 ? { capability_id: resolution.capabilityId } : { format_id: resolution.formatId }\n } : {},\n strategy: \"discover\",\n strategies_attempted: [\n {\n strategy: \"discover\",\n disposition: resolution.status === \"unsatisfied\" ? \"unavailable\" : resolution.status,\n detail: resolution.status === \"resolved\" ? `Bound build route using ${resolution.source} input` : resolution.detail\n }\n ]\n }\n ];\n if (resolution.status !== \"resolved\") {\n const noPhasesIndex = phaseResults.findIndex((phase) => phase.phase_id === \"no_phases\");\n if (noPhasesIndex >= 0) {\n skippedCount -= phaseResults[noPhasesIndex].steps.filter((step) => step.skipped).length;\n phaseResults.splice(noPhasesIndex, 1);\n }\n }\n if (resolution.status === \"failed\") {\n seedingFailed = true;\n creativeBuildCatalogFailed = true;\n failedCount += 1;\n seedingPhaseResult = {\n phase_id: \"fixture_resolution\",\n phase_title: \"Creative build fixture resolution\",\n passed: false,\n duration_ms: 0,\n steps: [\n {\n storyboard_id: storyboard.id,\n phase_id: \"fixture_resolution\",\n step_id: \"creative_build_catalog\",\n title: \"Validate advertised creative build catalog\",\n task: \"get_adcp_capabilities\",\n passed: false,\n duration_ms: 0,\n context: {},\n extraction: { path: \"none\" },\n validations: [\n {\n check: \"creative_build_probe_catalog\",\n passed: false,\n description: \"Advertised creative catalog satisfies its format and routing contract\",\n error: resolution.detail\n }\n ]\n }\n ]\n };\n } else if (resolution.status === \"resolved\") {\n for (const key of [\n \"capability_id\",\n \"target_format_id\",\n \"message\",\n \"creative_manifest\",\n \"creative_id\",\n \"transformer_id\",\n \"config\"\n ]) {\n delete context[`${handle}_${key}`];\n }\n Object.assign(context, resolution.context);\n options = { ...options, contracts: [.../* @__PURE__ */ new Set([...options.contracts ?? [], \"creative_build_probe_runner\"])] };\n } else {\n fixtureUnsatisfied = true;\n if (preSeeded === void 0 || preSeeded.attach) skippedCount += 1;\n fixtureCoverageGap = {\n reason: \"fixture_unsatisfied\",\n detail: resolution.detail,\n fixtures: [{ fixture_type: \"creative_format\", handle, requirements: [] }]\n };\n }\n }\n {\n let seeding;" + }, + { + "from": "creativeAssetFixtureGap || skipControllerSeedingForPhaseGates ? null", + "to": "creativeAssetFixtureGap || fixtureUnsatisfied || seedingFailed || skipControllerSeedingForPhaseGates ? null" + }, + { + "from": "fixtureResolutionRecords = seeding.resolutionRecords;", + "to": "fixtureResolutionRecords = fixtureResolutionRecords ? [...fixtureResolutionRecords, ...seeding.resolutionRecords ?? []] : seeding.resolutionRecords;" + }, + { + "from": " return runMultiPass(agentUrls, storyboard, options);", + "to": " if (storyboard.creative_build_probe) return executeStoryboardPass(agentUrls, storyboard, options, 0);\n return runMultiPass(agentUrls, storyboard, options);" + }, + { + "from": " const cascadeSkip = seedingMissingController ? {\n skip_reason: \"missing_test_controller\",\n skip: { reason: \"missing_test_controller\", detail: SKIP_DETAILS.missing_test_controller }\n } : seedingUnsupported ? {\n skip_reason: \"fixture_seed_unsupported\",\n skip: { reason: \"not_applicable\", detail: FIXTURE_SEED_UNSUPPORTED_DETAIL }\n } : seedingFailed ? {\n skip_reason: \"controller_seeding_failed\",\n skip: { reason: \"prerequisite_failed\", detail: CONTROLLER_SEEDING_FAILED_DETAIL }\n } : {\n skip_reason: \"controller_seeding_failed\",\n skip: { reason: \"prerequisite_failed\", detail: CONTROLLER_SEEDING_FAILED_DETAIL }\n };", + "to": " const cascadeSkip = seedingMissingController ? {\n skip_reason: \"missing_test_controller\",\n skip: { reason: \"missing_test_controller\", detail: SKIP_DETAILS.missing_test_controller }\n } : seedingUnsupported ? {\n skip_reason: \"fixture_seed_unsupported\",\n skip: { reason: \"not_applicable\", detail: FIXTURE_SEED_UNSUPPORTED_DETAIL }\n } : creativeBuildCatalogFailed ? {\n skip_reason: \"prerequisite_failed\",\n skip: {\n reason: \"prerequisite_failed\",\n detail: \"Creative build catalog validation failed in fixture_resolution.creative_build_catalog.\"\n }\n } : seedingFailed ? {\n skip_reason: \"controller_seeding_failed\",\n skip: { reason: \"prerequisite_failed\", detail: CONTROLLER_SEEDING_FAILED_DETAIL }\n } : {\n skip_reason: \"controller_seeding_failed\",\n skip: { reason: \"prerequisite_failed\", detail: CONTROLLER_SEEDING_FAILED_DETAIL }\n };" + }, + { + "from": " const injectedReceiver = options._webhookReceiver;", + "to": " if (storyboard.creative_build_probe) {\n let resolution;\n try {\n resolution = (0, import_creative_build_probe.prepareCreativeBuildProbe)(\n profile?.raw_capabilities,\n storyboard.creative_build_probe,\n options.test_kit,\n options.adcpVersion\n );\n } catch (error) {\n if (!clientResolution.reusedShared) {\n await Promise.allSettled([(0, import_protocols.closeScopedConnections)(options.protocol)]);\n }\n throw error;\n }\n if (resolution.status === \"failed\") {\n if (!clientResolution.reusedShared) await (0, import_protocols.closeScopedConnections)(options.protocol);\n return {\n storyboard_id: storyboard.id,\n step_id: found.step.id,\n phase_id: found.phaseId,\n title: found.step.title,\n task: found.step.task,\n passed: false,\n duration_ms: 0,\n validations: [\n {\n check: \"creative_build_probe_catalog\",\n passed: false,\n description: \"Advertised creative catalog satisfies its format and routing contract\",\n error: resolution.detail\n }\n ],\n context,\n extraction: { path: \"none\" },\n contributions: Array.from(options.contributions ?? [])\n };\n }\n if (resolution.status === \"unsatisfied\") {\n if (!clientResolution.reusedShared) await (0, import_protocols.closeScopedConnections)(options.protocol);\n return {\n storyboard_id: storyboard.id,\n step_id: found.step.id,\n phase_id: found.phaseId,\n title: found.step.title,\n task: found.step.task,\n passed: true,\n skipped: true,\n skip_reason: \"fixture_unsatisfied\",\n skip: { reason: \"not_applicable\", detail: resolution.detail },\n duration_ms: 0,\n validations: [],\n context,\n extraction: { path: \"none\" },\n contributions: Array.from(options.contributions ?? [])\n };\n }\n for (const key of [\n \"capability_id\",\n \"target_format_id\",\n \"message\",\n \"creative_manifest\",\n \"creative_id\",\n \"transformer_id\",\n \"config\"\n ]) {\n delete context[`${storyboard.creative_build_probe.context_prefix}_${key}`];\n }\n Object.assign(context, resolution.context);\n options = { ...options, contracts: [.../* @__PURE__ */ new Set([...options.contracts ?? [], \"creative_build_probe_runner\"])] };\n }\n const injectedReceiver = options._webhookReceiver;" + }, + { + "from": " const schemaRoot = getRunSchemaRoot(options);\n if (schemaRoot) {\n return await (0, import_schema_loader.withExternalSchemaRoot)(\n schemaRoot.adcpVersion,\n schemaRoot.schemaRoot,\n () => runStoryboardBody(agentUrlOrUrls, storyboard, options)\n );\n }\n return await runStoryboardBody(agentUrlOrUrls, storyboard, options);", + "to": " const runBody = () => (0, import_schema_loader.withCreativeBuildProbeSchemaScope)(\n Boolean(storyboard.creative_build_probe),\n () => storyboard.creative_build_probe\n ? (0, import_schema_loader.withServedReleaseSchemaRoots)(options.servedReleaseSchemaRoots, () => runStoryboardBody(agentUrlOrUrls, storyboard, options))\n : runStoryboardBody(agentUrlOrUrls, storyboard, options)\n );\n const schemaRoot = getRunSchemaRoot(options);\n if (schemaRoot) {\n return await (0, import_schema_loader.withExternalSchemaRoot)(schemaRoot.adcpVersion, schemaRoot.schemaRoot, runBody);\n }\n return await runBody();" + }, + { + "from": " const schemaRoot = getRunSchemaRoot(options);\n if (schemaRoot) {\n return await (0, import_schema_loader.withExternalSchemaRoot)(\n schemaRoot.adcpVersion,\n schemaRoot.schemaRoot,\n () => runStoryboardStepBody(agentUrl, storyboard, stepId, options)\n );\n }\n return await runStoryboardStepBody(agentUrl, storyboard, stepId, options);", + "to": " const runBody = () => (0, import_schema_loader.withCreativeBuildProbeSchemaScope)(\n Boolean(storyboard.creative_build_probe),\n () => storyboard.creative_build_probe\n ? (0, import_schema_loader.withServedReleaseSchemaRoots)(options.servedReleaseSchemaRoots, () => runStoryboardStepBody(agentUrl, storyboard, stepId, options))\n : runStoryboardStepBody(agentUrl, storyboard, stepId, options)\n );\n const schemaRoot = getRunSchemaRoot(options);\n if (schemaRoot) {\n return await (0, import_schema_loader.withExternalSchemaRoot)(schemaRoot.adcpVersion, schemaRoot.schemaRoot, runBody);\n }\n return await runBody();" + }, + { + "from": " message: `The agent's get_adcp_capabilities response failed schema validation at ${issue.pointer || \"/\"}: ${issue.message}. Track results may share this root cause; fix it first, then re-read the failures.`,", + "to": " message: (0, import_schema_loader.isCreativeBuildProbeSchemaScopeActive)()\n ? `Cached capability-discovery diagnostic (not this run's response_schema verdict) at ${issue.pointer || \"/\"}: ${issue.message}`\n : `The agent's get_adcp_capabilities response failed schema validation at ${issue.pointer || \"/\"}: ${issue.message}. Track results may share this root cause; fix it first, then re-read the failures.`," + } + ] + }, + { + "path": "dist/lib/testing/storyboard/loader.js", + "before": "bf94284049dcae7d7072293c5182867aad01752930581ce90b40852f0fd1a9dd", + "after": "a2f6a9420f8e82bcdf8a4772e4587c914cf3cfce3b2da19e34f9678a8ce47024", + "replacements": [ + { + "from": "var import_fs = require(\"fs\");\n", + "to": "var import_creative_build_probe = require(\"./creative-build-probe.js\");\nvar import_fs = require(\"fs\");\n" + }, + { + "from": "function validateStoryboardShape(storyboard) {", + "to": "function validateStoryboardShape(storyboard) {\n if (storyboard.creative_build_probe !== undefined) (0, import_creative_build_probe.validateCreativeBuildProbeSpec)(storyboard.creative_build_probe);" + } + ] + }, + { + "path": "dist/lib/testing/storyboard/runner.mjs", + "before": "5586b950b02f9dad5fcef4d8d018a96584640289c0ce0091ad7f7e818f39dd7d", + "after": "b23e3614488ce3afdb6fcc9aede98e001f1be880656829b1ed2197d6ef95c454", + "replacements": [ + { + "from": " withExternalSchemaRoot\n} from \"../../validation/schema-loader.mjs\";\n", + "to": " withExternalSchemaRoot,\n withServedReleaseSchemaRoots,\n withCreativeBuildProbeSchemaScope,\n isCreativeBuildProbeSchemaScopeActive\n} from \"../../validation/schema-loader.mjs\";\n" + }, + { + "from": "import { createHash, createHmac } from \"node:crypto\";\n", + "to": "import { prepareCreativeBuildProbe } from \"./creative-build-probe.mjs\";\nimport { createHash, createHmac } from \"node:crypto\";\n" + }, + { + "from": " let seedingFailed = false;", + "to": " let seedingFailed = false;\n let creativeBuildCatalogFailed = false;" + }, + { + "from": " let fixtureCoverageGap;\n {\n let seeding;", + "to": " let fixtureCoverageGap;\n if (storyboard.creative_build_probe && !skipControllerSeedingForPhaseGates) {\n let resolution;\n try {\n resolution = routingContext || isMultiInstance ? {\n status: \"unsatisfied\",\n detail: `fixture_unsatisfied: creative_format:${storyboard.creative_build_probe.context_prefix}: resolve an independent fixture for each routed or replicated agent in a single-agent run`\n } : prepareCreativeBuildProbe(\n profile?.raw_capabilities,\n storyboard.creative_build_probe,\n options.test_kit,\n options.adcpVersion\n );\n } catch (error) {\n await Promise.allSettled([\n ...webhookReceiver ? [webhookReceiver.close()] : [],\n ...!callerOwnsClients ? [closeScopedConnections(options.protocol)] : []\n ]);\n throw error;\n }\n const handle = storyboard.creative_build_probe.context_prefix;\n fixtureResolutionRecords = [\n {\n fixture_type: \"creative_format\",\n handle,\n requirements: resolution.status === \"resolved\" ? resolution.requirements : [],\n status: resolution.status,\n ...resolution.status === \"resolved\" ? {\n seller_ids: resolution.capabilityId !== void 0 ? { capability_id: resolution.capabilityId } : { format_id: resolution.formatId }\n } : {},\n strategy: \"discover\",\n strategies_attempted: [\n {\n strategy: \"discover\",\n disposition: resolution.status === \"unsatisfied\" ? \"unavailable\" : resolution.status,\n detail: resolution.status === \"resolved\" ? `Bound build route using ${resolution.source} input` : resolution.detail\n }\n ]\n }\n ];\n if (resolution.status !== \"resolved\") {\n const noPhasesIndex = phaseResults.findIndex((phase) => phase.phase_id === \"no_phases\");\n if (noPhasesIndex >= 0) {\n skippedCount -= phaseResults[noPhasesIndex].steps.filter((step) => step.skipped).length;\n phaseResults.splice(noPhasesIndex, 1);\n }\n }\n if (resolution.status === \"failed\") {\n seedingFailed = true;\n creativeBuildCatalogFailed = true;\n failedCount += 1;\n seedingPhaseResult = {\n phase_id: \"fixture_resolution\",\n phase_title: \"Creative build fixture resolution\",\n passed: false,\n duration_ms: 0,\n steps: [\n {\n storyboard_id: storyboard.id,\n phase_id: \"fixture_resolution\",\n step_id: \"creative_build_catalog\",\n title: \"Validate advertised creative build catalog\",\n task: \"get_adcp_capabilities\",\n passed: false,\n duration_ms: 0,\n context: {},\n extraction: { path: \"none\" },\n validations: [\n {\n check: \"creative_build_probe_catalog\",\n passed: false,\n description: \"Advertised creative catalog satisfies its format and routing contract\",\n error: resolution.detail\n }\n ]\n }\n ]\n };\n } else if (resolution.status === \"resolved\") {\n for (const key of [\n \"capability_id\",\n \"target_format_id\",\n \"message\",\n \"creative_manifest\",\n \"creative_id\",\n \"transformer_id\",\n \"config\"\n ]) {\n delete context[`${handle}_${key}`];\n }\n Object.assign(context, resolution.context);\n options = { ...options, contracts: [.../* @__PURE__ */ new Set([...options.contracts ?? [], \"creative_build_probe_runner\"])] };\n } else {\n fixtureUnsatisfied = true;\n if (preSeeded === void 0 || preSeeded.attach) skippedCount += 1;\n fixtureCoverageGap = {\n reason: \"fixture_unsatisfied\",\n detail: resolution.detail,\n fixtures: [{ fixture_type: \"creative_format\", handle, requirements: [] }]\n };\n }\n }\n {\n let seeding;" + }, + { + "from": "creativeAssetFixtureGap || skipControllerSeedingForPhaseGates ? null", + "to": "creativeAssetFixtureGap || fixtureUnsatisfied || seedingFailed || skipControllerSeedingForPhaseGates ? null" + }, + { + "from": "fixtureResolutionRecords = seeding.resolutionRecords;", + "to": "fixtureResolutionRecords = fixtureResolutionRecords ? [...fixtureResolutionRecords, ...seeding.resolutionRecords ?? []] : seeding.resolutionRecords;" + }, + { + "from": " return runMultiPass(agentUrls, storyboard, options);", + "to": " if (storyboard.creative_build_probe) return executeStoryboardPass(agentUrls, storyboard, options, 0);\n return runMultiPass(agentUrls, storyboard, options);" + }, + { + "from": " const cascadeSkip = seedingMissingController ? {\n skip_reason: \"missing_test_controller\",\n skip: { reason: \"missing_test_controller\", detail: SKIP_DETAILS.missing_test_controller }\n } : seedingUnsupported ? {\n skip_reason: \"fixture_seed_unsupported\",\n skip: { reason: \"not_applicable\", detail: FIXTURE_SEED_UNSUPPORTED_DETAIL }\n } : seedingFailed ? {\n skip_reason: \"controller_seeding_failed\",\n skip: { reason: \"prerequisite_failed\", detail: CONTROLLER_SEEDING_FAILED_DETAIL }\n } : {\n skip_reason: \"controller_seeding_failed\",\n skip: { reason: \"prerequisite_failed\", detail: CONTROLLER_SEEDING_FAILED_DETAIL }\n };", + "to": " const cascadeSkip = seedingMissingController ? {\n skip_reason: \"missing_test_controller\",\n skip: { reason: \"missing_test_controller\", detail: SKIP_DETAILS.missing_test_controller }\n } : seedingUnsupported ? {\n skip_reason: \"fixture_seed_unsupported\",\n skip: { reason: \"not_applicable\", detail: FIXTURE_SEED_UNSUPPORTED_DETAIL }\n } : creativeBuildCatalogFailed ? {\n skip_reason: \"prerequisite_failed\",\n skip: {\n reason: \"prerequisite_failed\",\n detail: \"Creative build catalog validation failed in fixture_resolution.creative_build_catalog.\"\n }\n } : seedingFailed ? {\n skip_reason: \"controller_seeding_failed\",\n skip: { reason: \"prerequisite_failed\", detail: CONTROLLER_SEEDING_FAILED_DETAIL }\n } : {\n skip_reason: \"controller_seeding_failed\",\n skip: { reason: \"prerequisite_failed\", detail: CONTROLLER_SEEDING_FAILED_DETAIL }\n };" + }, + { + "from": " const injectedReceiver = options._webhookReceiver;", + "to": " if (storyboard.creative_build_probe) {\n let resolution;\n try {\n resolution = prepareCreativeBuildProbe(\n profile?.raw_capabilities,\n storyboard.creative_build_probe,\n options.test_kit,\n options.adcpVersion\n );\n } catch (error) {\n if (!clientResolution.reusedShared) {\n await Promise.allSettled([closeScopedConnections(options.protocol)]);\n }\n throw error;\n }\n if (resolution.status === \"failed\") {\n if (!clientResolution.reusedShared) await closeScopedConnections(options.protocol);\n return {\n storyboard_id: storyboard.id,\n step_id: found.step.id,\n phase_id: found.phaseId,\n title: found.step.title,\n task: found.step.task,\n passed: false,\n duration_ms: 0,\n validations: [\n {\n check: \"creative_build_probe_catalog\",\n passed: false,\n description: \"Advertised creative catalog satisfies its format and routing contract\",\n error: resolution.detail\n }\n ],\n context,\n extraction: { path: \"none\" },\n contributions: Array.from(options.contributions ?? [])\n };\n }\n if (resolution.status === \"unsatisfied\") {\n if (!clientResolution.reusedShared) await closeScopedConnections(options.protocol);\n return {\n storyboard_id: storyboard.id,\n step_id: found.step.id,\n phase_id: found.phaseId,\n title: found.step.title,\n task: found.step.task,\n passed: true,\n skipped: true,\n skip_reason: \"fixture_unsatisfied\",\n skip: { reason: \"not_applicable\", detail: resolution.detail },\n duration_ms: 0,\n validations: [],\n context,\n extraction: { path: \"none\" },\n contributions: Array.from(options.contributions ?? [])\n };\n }\n for (const key of [\n \"capability_id\",\n \"target_format_id\",\n \"message\",\n \"creative_manifest\",\n \"creative_id\",\n \"transformer_id\",\n \"config\"\n ]) {\n delete context[`${storyboard.creative_build_probe.context_prefix}_${key}`];\n }\n Object.assign(context, resolution.context);\n options = { ...options, contracts: [.../* @__PURE__ */ new Set([...options.contracts ?? [], \"creative_build_probe_runner\"])] };\n }\n const injectedReceiver = options._webhookReceiver;" + }, + { + "from": " const schemaRoot = getRunSchemaRoot(options);\n if (schemaRoot) {\n return await withExternalSchemaRoot(\n schemaRoot.adcpVersion,\n schemaRoot.schemaRoot,\n () => runStoryboardBody(agentUrlOrUrls, storyboard, options)\n );\n }\n return await runStoryboardBody(agentUrlOrUrls, storyboard, options);", + "to": " const runBody = () => withCreativeBuildProbeSchemaScope(\n Boolean(storyboard.creative_build_probe),\n () => storyboard.creative_build_probe\n ? withServedReleaseSchemaRoots(options.servedReleaseSchemaRoots, () => runStoryboardBody(agentUrlOrUrls, storyboard, options))\n : runStoryboardBody(agentUrlOrUrls, storyboard, options)\n );\n const schemaRoot = getRunSchemaRoot(options);\n if (schemaRoot) {\n return await withExternalSchemaRoot(schemaRoot.adcpVersion, schemaRoot.schemaRoot, runBody);\n }\n return await runBody();" + }, + { + "from": " const schemaRoot = getRunSchemaRoot(options);\n if (schemaRoot) {\n return await withExternalSchemaRoot(\n schemaRoot.adcpVersion,\n schemaRoot.schemaRoot,\n () => runStoryboardStepBody(agentUrl, storyboard, stepId, options)\n );\n }\n return await runStoryboardStepBody(agentUrl, storyboard, stepId, options);", + "to": " const runBody = () => withCreativeBuildProbeSchemaScope(\n Boolean(storyboard.creative_build_probe),\n () => storyboard.creative_build_probe\n ? withServedReleaseSchemaRoots(options.servedReleaseSchemaRoots, () => runStoryboardStepBody(agentUrl, storyboard, stepId, options))\n : runStoryboardStepBody(agentUrl, storyboard, stepId, options)\n );\n const schemaRoot = getRunSchemaRoot(options);\n if (schemaRoot) {\n return await withExternalSchemaRoot(schemaRoot.adcpVersion, schemaRoot.schemaRoot, runBody);\n }\n return await runBody();" + }, + { + "from": " message: `The agent's get_adcp_capabilities response failed schema validation at ${issue.pointer || \"/\"}: ${issue.message}. Track results may share this root cause; fix it first, then re-read the failures.`,", + "to": " message: isCreativeBuildProbeSchemaScopeActive()\n ? `Cached capability-discovery diagnostic (not this run's response_schema verdict) at ${issue.pointer || \"/\"}: ${issue.message}`\n : `The agent's get_adcp_capabilities response failed schema validation at ${issue.pointer || \"/\"}: ${issue.message}. Track results may share this root cause; fix it first, then re-read the failures.`," + } + ] + }, + { + "path": "dist/lib/testing/storyboard/loader.mjs", + "before": "ee3098c582ad0ca84610be3cf0d80984c01f12006d1e82ced64ff13e2e1337b3", + "after": "d1872a6505fdfb3005789e526667564e9b311d440129991ef9df77e33f344881", + "replacements": [ + { + "from": "import { readFileSync } from \"fs\";\n", + "to": "import { validateCreativeBuildProbeSpec } from \"./creative-build-probe.mjs\";\nimport { readFileSync } from \"fs\";\n" + }, + { + "from": "function validateStoryboardShape(storyboard) {", + "to": "function validateStoryboardShape(storyboard) {\n if (storyboard.creative_build_probe !== undefined) validateCreativeBuildProbeSpec(storyboard.creative_build_probe);" + } + ] + }, + { + "path": "dist/lib/testing/storyboard/creative-build-probe.js", + "before": null, + "after": "f38d54503cc84fba64c4d9c19bcad69eb76bb4e73f42ec95bf3c7a4eb6ccc4d2", + "contents_file": "creative-build-probe.js" + }, + { + "path": "dist/lib/testing/storyboard/creative-build-probe.mjs", + "before": null, + "after": "9c781d45603dd7017eb3462fcbd695f289feffbd2b76b67852f8b5dbf667fd3b", + "contents_file": "creative-build-probe.mjs" + }, + { + "path": "dist/lib/testing/storyboard/creative-build-probe.d.ts", + "before": null, + "after": "1ad23d5ba9f22d3c38efc77d491e9f1742e71e430e221781ab08b1f18f22c311", + "contents_file": "creative-build-probe.d.ts" + }, + { + "path": "dist/lib/testing/storyboard/creative-build-probe.d.mts", + "before": null, + "after": "d5d17d4d127dc881aef744f317a9fd37382aec20d0783f919bd8945dd5d25e25", + "contents_file": "creative-build-probe.d.mts" + }, + { + "path": "dist/lib/testing/storyboard/types.d.ts", + "before": "0b04ca12e237245e4b25c09c078cbd18f715312fc91ea7515fd7c9869549a184", + "after": "e5277b985dd3827d3565f0123b66ee5adb2b9f2b677b95f2e464f7722ddc06d1", + "replacements": [ + { + "from": "export interface Storyboard {", + "to": "export interface Storyboard {\n /** Opt-in read-only preparation of a valid evaluator build fixture. */\n creative_build_probe?: import(\"./creative-build-probe\").CreativeBuildProbeSpec;" + }, + { + "from": "fixture_type: 'product' | 'pricing_option';", + "to": "fixture_type: 'product' | 'pricing_option' | 'creative_format';" + }, + { + "from": " seller_ids?: {\n product_id: string;\n pricing_option_id?: string;\n };", + "to": " seller_ids?: {\n product_id?: string;\n pricing_option_id?: string;\n capability_id?: string;\n format_id?: Record;\n };" + } + ] + }, + { + "path": "dist/lib/testing/storyboard/types.d.mts", + "before": "86e36c5115b5fd80087b8ae5570b571718eda4c324ed9a6af64a6a003cbb94a2", + "after": "d1dd9de48016ad66efd847336c2d589e2a7b1b5920c675bcf5e7cd8ae8d43206", + "replacements": [ + { + "from": "export interface Storyboard {", + "to": "export interface Storyboard {\n /** Opt-in read-only preparation of a valid evaluator build fixture. */\n creative_build_probe?: import(\"./creative-build-probe.mjs\").CreativeBuildProbeSpec;" + }, + { + "from": "fixture_type: 'product' | 'pricing_option';", + "to": "fixture_type: 'product' | 'pricing_option' | 'creative_format';" + }, + { + "from": " seller_ids?: {\n product_id: string;\n pricing_option_id?: string;\n };", + "to": " seller_ids?: {\n product_id?: string;\n pricing_option_id?: string;\n capability_id?: string;\n format_id?: Record;\n };" + } + ] + }, + { + "path": "dist/lib/testing/index.js", + "before": "3e58ae34a1f86a2406956b77f4aafb132f05c448782da90ecbef3f5c3ef1b14c", + "after": "5ff83c7fd3408b87c0dd89ce64cb3b8dd41f486869f0437e710292ef0ba1487a", + "replacements": [ + { + "from": " testTerminalStateEnforcement,\n testValidation,\n unregisterExternalSchemaRoot,\n validateFixtureResolutionDeclarations,\n validateRateLimitTripSpec,\n withExternalSchemaRoot\n});\n", + "to": " testTerminalStateEnforcement,\n testValidation,\n unregisterExternalSchemaRoot,\n validateFixtureResolutionDeclarations,\n validateRateLimitTripSpec,\n withExternalSchemaRoot\n});\n\nvar creativeBuildProbeBridge = require(\"./storyboard/creative-build-probe.js\");\nObject.defineProperties(module.exports, { resolveCreativeBuildProbe: { enumerable: true, get: () => creativeBuildProbeBridge.resolveCreativeBuildProbe }, validateCreativeBuildProbeSpec: { enumerable: true, get: () => creativeBuildProbeBridge.validateCreativeBuildProbeSpec } });\n" + } + ] + }, + { + "path": "dist/lib/testing/index.mjs", + "before": "72628ff75d57ced764885a9c4ab418e51ddbd564fad90bad2b956a38ad417751", + "after": "48c67607ed0a4308811da152248dd7e53421093b86a06ef7d91a6d6b7f32eb0b", + "replacements": [ + { + "from": " testTerminalStateEnforcement,\n testValidation,\n unregisterExternalSchemaRoot,\n validateFixtureResolutionDeclarations,\n validateRateLimitTripSpec,\n withExternalSchemaRoot\n};\n", + "to": " testTerminalStateEnforcement,\n testValidation,\n unregisterExternalSchemaRoot,\n validateFixtureResolutionDeclarations,\n validateRateLimitTripSpec,\n withExternalSchemaRoot\n};\n\nexport { resolveCreativeBuildProbe, validateCreativeBuildProbeSpec } from \"./storyboard/creative-build-probe.mjs\";\n" + } + ] + }, + { + "path": "dist/lib/testing/index.d.ts", + "before": "333861ed3322c450de3c92eebe8bb85d6894674ff9c7554755212be5882ff783", + "after": "236bce4bb4f79173f3b90e9dbfa570bf0cdcf91123ccfacfc8e19cf1d78778c2", + "replacements": [ + { + "from": "GraphGrade, type OAuthMetadataGraphObservation, type OAuthMetadataGraphVector, type OAuthMetadataGraphVectorCorpus, BrandJsonSchema, AdagentsJsonSchema, MCP_SESSION_PROBE_TASK, } from './storyboard';\n", + "to": "GraphGrade, type OAuthMetadataGraphObservation, type OAuthMetadataGraphVector, type OAuthMetadataGraphVectorCorpus, BrandJsonSchema, AdagentsJsonSchema, MCP_SESSION_PROBE_TASK, } from './storyboard';\n\nexport { resolveCreativeBuildProbe, validateCreativeBuildProbeSpec } from \"./storyboard/creative-build-probe\";\nexport type { CreativeBuildProbeSpec, CreativeBuildProbeInputFixture, CreativeBuildProbeResolution } from \"./storyboard/creative-build-probe\";\n" + } + ] + }, + { + "path": "dist/lib/testing/index.d.mts", + "before": "c94fb41ec7bfca7c513d82f46f2d049a85f8433e8859a53738cec18a35520a00", + "after": "6f44c6faa29f6635a76b5cfdadbfc2ac72d5af3e9d82c914ae50e4f1ad914bb1", + "replacements": [ + { + "from": ", type OAuthMetadataGraphObservation, type OAuthMetadataGraphVector, type OAuthMetadataGraphVectorCorpus, BrandJsonSchema, AdagentsJsonSchema, MCP_SESSION_PROBE_TASK, } from './storyboard/index.mjs';\n", + "to": ", type OAuthMetadataGraphObservation, type OAuthMetadataGraphVector, type OAuthMetadataGraphVectorCorpus, BrandJsonSchema, AdagentsJsonSchema, MCP_SESSION_PROBE_TASK, } from './storyboard/index.mjs';\n\nexport { resolveCreativeBuildProbe, validateCreativeBuildProbeSpec } from \"./storyboard/creative-build-probe.mjs\";\nexport type { CreativeBuildProbeSpec, CreativeBuildProbeInputFixture, CreativeBuildProbeResolution } from \"./storyboard/creative-build-probe.mjs\";\n" + } + ] + }, + { + "path": "bin/adcp.js", + "before": "b8b3f32d36bb7a1894c0610b4ae47d6c338e2bc1196c798840d948790430a639", + "after": "d94f87523610c0479fcf83bf3643bfe9dd094027dc5a7addbb5f74d20c03ff92", + "replacements": [ + { + "from": "ids.pricing_option_id || ids.product_id || '?'", + "to": "ids.pricing_option_id || ids.product_id || ids.capability_id || ids.format_id?.id || '?'" + } + ] + }, + { + "path": "dist/lib/validation/schema-loader.js", + "before": "53b8cb00dcae5cb35307f8a6c40f030e4d3d59381714e4ea9fa112ef8ae1bffc", + "after": "6f8a69c7fb6cd7831d5e16ea1fa3300e42192398d76574c21a5c4b507d30cb59", + "replacements": [ + { + "from": " listValidatorKeys: () => listValidatorKeys,\n registerExternalSchemaRoot: () => registerExternalSchemaRoot,\n resolveBundleKey: () => resolveBundleKey,\n schemaAllowsTopLevelField: () => schemaAllowsTopLevelField,\n toReleasePrecisionWire: () => toReleasePrecisionWire,\n unregisterExternalSchemaRoot: () => unregisterExternalSchemaRoot,\n validateAdcpVersionWire: () => validateAdcpVersionWire,\n withExternalSchemaRoot: () => withExternalSchemaRoot\n});\nmodule.exports = __toCommonJS(schema_loader_exports);\nvar import_ajv = __toESM(require(\"ajv\"));\n", + "to": " listValidatorKeys: () => listValidatorKeys,\n registerExternalSchemaRoot: () => registerExternalSchemaRoot,\n resolveBundleKey: () => resolveBundleKey,\n resolveServedValidationVersion: () => resolveServedValidationVersion,\n schemaAllowsTopLevelField: () => schemaAllowsTopLevelField,\n toReleasePrecisionWire: () => toReleasePrecisionWire,\n unregisterExternalSchemaRoot: () => unregisterExternalSchemaRoot,\n validateAdcpVersionWire: () => validateAdcpVersionWire,\n withExternalSchemaRoot: () => withExternalSchemaRoot,\n withCreativeBuildProbeSchemaScope: () => withCreativeBuildProbeSchemaScope,\n isCreativeBuildProbeSchemaScopeActive: () => isCreativeBuildProbeSchemaScopeActive,\n withServedReleaseSchemaRoots: () => withServedReleaseSchemaRoots,\n withServedValidationVersion: () => withServedValidationVersion\n});\nmodule.exports = __toCommonJS(schema_loader_exports);\nvar import_ajv = __toESM(require(\"ajv\"));\n" + }, + { + "from": " return false;\n }\n}\nconst mcpToolSchemaCache = /* @__PURE__ */ new Map();\nconst mcpToolSummaryCache = /* @__PURE__ */ new Map();\nfunction boundedMcpToolSummary(value) {\n", + "to": " return false;\n }\n}\n// Maintenance-only boundary: producer responses cannot enable this scope.\nconst creativeBuildProbeSchemaScope = new import_async_hooks.AsyncLocalStorage();\nfunction withCreativeBuildProbeSchemaScope(enabled, fn) {\n return creativeBuildProbeSchemaScope.run(enabled === true, fn);\n}\nfunction isCreativeBuildProbeSchemaScopeActive() {\n return creativeBuildProbeSchemaScope.getStore() === true;\n}\nconst servedReleaseSchemaRootScope = new import_async_hooks.AsyncLocalStorage();\nconst servedReleaseRootAliases = /* @__PURE__ */ new Map();\nfunction withServedReleaseSchemaRoots(roots, fn) {\n if (roots === void 0) return fn();\n if (roots === null || typeof roots !== \"object\" || Array.isArray(roots)) {\n throw new TypeError(\"servedReleaseSchemaRoots must be an object mapping AdCP versions to schema roots\");\n }\n const entries = [];\n for (const [version, root] of Object.entries(roots)) {\n resolveBundleKey(version);\n if (typeof root !== \"string\" || root.length === 0) {\n throw new TypeError(`servedReleaseSchemaRoots[\"${version}\"] must be a non-empty schema root path`);\n }\n const memoKey = `${version}\\0${root}`;\n let aliases = servedReleaseRootAliases.get(memoKey);\n if (!aliases) {\n if (!hasSchemaRootShape(root)) {\n throw new Error(`Served-release AdCP schema root for version \"${version}\" not found or empty at ${root}`);\n }\n aliases = assertSchemaRootMatchesVersion(version, root);\n servedReleaseRootAliases.set(memoKey, aliases);\n }\n const conflict = entries.find((entry) => entry.root !== root && [...aliases].some((alias) => entry.aliases.has(alias)));\n if (conflict) {\n throw new Error(\n `servedReleaseSchemaRoots[\"${version}\"] and servedReleaseSchemaRoots[\"${conflict.version}\"] name the same release with different schema roots (${root}, ${conflict.root})`\n );\n }\n entries.push({ version, root, aliases });\n }\n if (entries.length === 0) return fn();\n const parent = servedReleaseSchemaRootScope.getStore() ?? [];\n return servedReleaseSchemaRootScope.run([...entries, ...parent], fn);\n}\nfunction findServedReleaseSchemaRoot(release) {\n const entries = servedReleaseSchemaRootScope.getStore();\n if (!entries?.length) return void 0;\n let key;\n try {\n key = resolveBundleKey(release);\n } catch {\n return void 0;\n }\n return entries.find((entry) => entry.aliases.has(key) || entry.aliases.has(release));\n}\nconst SERVED_RELEASE_MAJOR = /^(\\d+)\\.\\d+(?:-[a-zA-Z0-9.-]+)?$/;\nconst NUMERIC_PIN_MAJOR = /^(\\d+)\\.\\d+/;\nfunction listsSupportedRelease(response, release) {\n const adcp = response.adcp;\n if (adcp === null || typeof adcp !== \"object\") return false;\n const supported = adcp.supported_versions;\n if (!Array.isArray(supported)) return false;\n return supported.some((entry) => {\n if (typeof entry !== \"string\") return false;\n if (entry === release) return true;\n try {\n return toReleasePrecisionWire(entry) === release;\n } catch {\n return false;\n }\n });\n}\nfunction selectServedValidationVersion(response, pinnedVersion) {\n const pin = { version: pinnedVersion };\n const pinMajor = NUMERIC_PIN_MAJOR.exec(pinnedVersion)?.[1];\n if (pinMajor === void 0) return pin;\n if (response === null || typeof response !== \"object\") return pin;\n const echoed = response.adcp_version;\n if (typeof echoed !== \"string\") return pin;\n const echoMajor = SERVED_RELEASE_MAJOR.exec(echoed)?.[1];\n if (echoMajor === void 0 || echoMajor !== pinMajor) return pin;\n let pinnedRelease;\n try {\n pinnedRelease = toReleasePrecisionWire(pinnedVersion);\n } catch {\n return pin;\n }\n if (echoed === pinnedRelease) return pin;\n if (!listsSupportedRelease(response, echoed)) return pin;\n const served = findServedReleaseSchemaRoot(echoed);\n if (served) return { version: echoed, served };\n return hasSchemaBundle(echoed) ? { version: echoed } : pin;\n}\nfunction resolveServedValidationVersion(response, pinnedVersion) {\n try {\n return selectServedValidationVersion(response, pinnedVersion).version;\n } catch {\n return pinnedVersion;\n }\n}\nfunction withServedValidationVersion(response, pinnedVersion, fn) {\n let selection;\n try {\n selection = selectServedValidationVersion(response, pinnedVersion);\n } catch {\n selection = { version: pinnedVersion };\n }\n const { version, served } = selection;\n if (!served) return fn(version);\n const next = new Map(scopedExternalSchemaRoots.getStore());\n for (const alias of served.aliases) next.set(alias, served.root);\n return scopedExternalSchemaRoots.run(next, () => fn(version));\n}\nconst mcpToolSchemaCache = /* @__PURE__ */ new Map();\nconst mcpToolSummaryCache = /* @__PURE__ */ new Map();\nfunction boundedMcpToolSummary(value) {\n" + }, + { + "from": " listValidatorKeys,\n registerExternalSchemaRoot,\n resolveBundleKey,\n schemaAllowsTopLevelField,\n toReleasePrecisionWire,\n unregisterExternalSchemaRoot,\n validateAdcpVersionWire,\n withExternalSchemaRoot\n});\n", + "to": " listValidatorKeys,\n registerExternalSchemaRoot,\n resolveBundleKey,\n resolveServedValidationVersion,\n schemaAllowsTopLevelField,\n toReleasePrecisionWire,\n unregisterExternalSchemaRoot,\n validateAdcpVersionWire,\n withExternalSchemaRoot,\n withCreativeBuildProbeSchemaScope,\n isCreativeBuildProbeSchemaScopeActive,\n withServedReleaseSchemaRoots,\n withServedValidationVersion\n});\n" + } + ] + }, + { + "path": "dist/lib/validation/schema-loader.mjs", + "before": "6c81e915b9a88381df3d0fdd0a4d951d3577c9d84327be330c5c037d6c0dcbba", + "after": "32504ca15f472d82e8eb04bb2224b131a3d8a817eec9d011ac27d3d74c5426ef", + "replacements": [ + { + "from": " return false;\n }\n}\nconst mcpToolSchemaCache = /* @__PURE__ */ new Map();\nconst mcpToolSummaryCache = /* @__PURE__ */ new Map();\nfunction boundedMcpToolSummary(value) {\n", + "to": " return false;\n }\n}\n// Maintenance-only boundary: producer responses cannot enable this scope.\nconst creativeBuildProbeSchemaScope = new AsyncLocalStorage();\nfunction withCreativeBuildProbeSchemaScope(enabled, fn) {\n return creativeBuildProbeSchemaScope.run(enabled === true, fn);\n}\nfunction isCreativeBuildProbeSchemaScopeActive() {\n return creativeBuildProbeSchemaScope.getStore() === true;\n}\nconst servedReleaseSchemaRootScope = new AsyncLocalStorage();\nconst servedReleaseRootAliases = /* @__PURE__ */ new Map();\nfunction withServedReleaseSchemaRoots(roots, fn) {\n if (roots === void 0) return fn();\n if (roots === null || typeof roots !== \"object\" || Array.isArray(roots)) {\n throw new TypeError(\"servedReleaseSchemaRoots must be an object mapping AdCP versions to schema roots\");\n }\n const entries = [];\n for (const [version, root] of Object.entries(roots)) {\n resolveBundleKey(version);\n if (typeof root !== \"string\" || root.length === 0) {\n throw new TypeError(`servedReleaseSchemaRoots[\"${version}\"] must be a non-empty schema root path`);\n }\n const memoKey = `${version}\\0${root}`;\n let aliases = servedReleaseRootAliases.get(memoKey);\n if (!aliases) {\n if (!hasSchemaRootShape(root)) {\n throw new Error(`Served-release AdCP schema root for version \"${version}\" not found or empty at ${root}`);\n }\n aliases = assertSchemaRootMatchesVersion(version, root);\n servedReleaseRootAliases.set(memoKey, aliases);\n }\n const conflict = entries.find((entry) => entry.root !== root && [...aliases].some((alias) => entry.aliases.has(alias)));\n if (conflict) {\n throw new Error(\n `servedReleaseSchemaRoots[\"${version}\"] and servedReleaseSchemaRoots[\"${conflict.version}\"] name the same release with different schema roots (${root}, ${conflict.root})`\n );\n }\n entries.push({ version, root, aliases });\n }\n if (entries.length === 0) return fn();\n const parent = servedReleaseSchemaRootScope.getStore() ?? [];\n return servedReleaseSchemaRootScope.run([...entries, ...parent], fn);\n}\nfunction findServedReleaseSchemaRoot(release) {\n const entries = servedReleaseSchemaRootScope.getStore();\n if (!entries?.length) return void 0;\n let key;\n try {\n key = resolveBundleKey(release);\n } catch {\n return void 0;\n }\n return entries.find((entry) => entry.aliases.has(key) || entry.aliases.has(release));\n}\nconst SERVED_RELEASE_MAJOR = /^(\\d+)\\.\\d+(?:-[a-zA-Z0-9.-]+)?$/;\nconst NUMERIC_PIN_MAJOR = /^(\\d+)\\.\\d+/;\nfunction listsSupportedRelease(response, release) {\n const adcp = response.adcp;\n if (adcp === null || typeof adcp !== \"object\") return false;\n const supported = adcp.supported_versions;\n if (!Array.isArray(supported)) return false;\n return supported.some((entry) => {\n if (typeof entry !== \"string\") return false;\n if (entry === release) return true;\n try {\n return toReleasePrecisionWire(entry) === release;\n } catch {\n return false;\n }\n });\n}\nfunction selectServedValidationVersion(response, pinnedVersion) {\n const pin = { version: pinnedVersion };\n const pinMajor = NUMERIC_PIN_MAJOR.exec(pinnedVersion)?.[1];\n if (pinMajor === void 0) return pin;\n if (response === null || typeof response !== \"object\") return pin;\n const echoed = response.adcp_version;\n if (typeof echoed !== \"string\") return pin;\n const echoMajor = SERVED_RELEASE_MAJOR.exec(echoed)?.[1];\n if (echoMajor === void 0 || echoMajor !== pinMajor) return pin;\n let pinnedRelease;\n try {\n pinnedRelease = toReleasePrecisionWire(pinnedVersion);\n } catch {\n return pin;\n }\n if (echoed === pinnedRelease) return pin;\n if (!listsSupportedRelease(response, echoed)) return pin;\n const served = findServedReleaseSchemaRoot(echoed);\n if (served) return { version: echoed, served };\n return hasSchemaBundle(echoed) ? { version: echoed } : pin;\n}\nfunction resolveServedValidationVersion(response, pinnedVersion) {\n try {\n return selectServedValidationVersion(response, pinnedVersion).version;\n } catch {\n return pinnedVersion;\n }\n}\nfunction withServedValidationVersion(response, pinnedVersion, fn) {\n let selection;\n try {\n selection = selectServedValidationVersion(response, pinnedVersion);\n } catch {\n selection = { version: pinnedVersion };\n }\n const { version, served } = selection;\n if (!served) return fn(version);\n const next = new Map(scopedExternalSchemaRoots.getStore());\n for (const alias of served.aliases) next.set(alias, served.root);\n return scopedExternalSchemaRoots.run(next, () => fn(version));\n}\nconst mcpToolSchemaCache = /* @__PURE__ */ new Map();\nconst mcpToolSummaryCache = /* @__PURE__ */ new Map();\nfunction boundedMcpToolSummary(value) {\n" + }, + { + "from": " listValidatorKeys,\n registerExternalSchemaRoot,\n resolveBundleKey,\n schemaAllowsTopLevelField,\n toReleasePrecisionWire,\n unregisterExternalSchemaRoot,\n validateAdcpVersionWire,\n withExternalSchemaRoot\n};\n", + "to": " listValidatorKeys,\n registerExternalSchemaRoot,\n resolveBundleKey,\n resolveServedValidationVersion,\n schemaAllowsTopLevelField,\n toReleasePrecisionWire,\n unregisterExternalSchemaRoot,\n validateAdcpVersionWire,\n withExternalSchemaRoot,\n withCreativeBuildProbeSchemaScope,\n isCreativeBuildProbeSchemaScopeActive,\n withServedReleaseSchemaRoots,\n withServedValidationVersion\n};\n" + } + ] + }, + { + "path": "dist/lib/testing/storyboard/validations.js", + "before": "3db2672148177a37ff8c049ac5bf7cb874b298c67f089aa615053dda986a36a1", + "after": "9d20a7cf23a73c7159a0351ff59ef997ff2f3a98d9104908e2f92cc038970127", + "replacements": [ + { + "from": " }\n}\nfunction validateResponseSchema(validation, ctx, taskResult) {\n const taskName = ctx.taskName;\n const schemaIdentityVersion = ctx.adcpVersion ?? ctx.responseAdcpVersion ?? import_version.ADCP_VERSION;\n const { schema_id, schema_url } = resolveSchemaIdentity(ctx.responseSchemaRef, schemaIdentityVersion);\n", + "to": " }\n}\nfunction validateResponseSchema(validation, ctx, taskResult) {\n const pinnedVersion = ctx.adcpVersion ?? import_version.ADCP_VERSION;\n if ((0, import_schema_loader.isCreativeBuildProbeSchemaScopeActive)() && ctx.taskName === \"get_adcp_capabilities\" && pinnedVersion !== void 0) {\n return (0, import_schema_loader.withServedValidationVersion)(\n taskResult.data,\n pinnedVersion,\n (validationVersion) => validateResponseSchemaForVersion(\n validation,\n { ...ctx, adcpVersion: validationVersion, responseAdcpVersion: validationVersion },\n taskResult\n )\n );\n }\n return validateResponseSchemaForVersion(validation, ctx, taskResult);\n}\nfunction validateResponseSchemaForVersion(validation, ctx, taskResult) {\n const taskName = ctx.taskName;\n const schemaIdentityVersion = ctx.adcpVersion ?? ctx.responseAdcpVersion ?? import_version.ADCP_VERSION;\n const { schema_id, schema_url } = resolveSchemaIdentity(ctx.responseSchemaRef, schemaIdentityVersion);\n" + }, + { + "from": " schema_url,\n strict\n };\n const warning = buildStrictWarning(strict);\n return warning ? { ...base, warning } : base;\n }\n const issues2 = strict.issues ?? [];\n", + "to": " schema_url,\n strict\n };\n const warning = [\n buildStrictWarning(strict),\n (0, import_schema_loader.isCreativeBuildProbeSchemaScopeActive)() && taskName === \"get_adcp_capabilities\" && strict.lenient_valid === false\n ? `Authorized catalog JSON schema ${schema_url ?? schemaIdentityVersion} accepted this response; the SDK14 generated Zod schema disagrees`\n : void 0\n ].filter(Boolean).join(\"; \");\n return warning ? { ...base, warning } : base;\n }\n const issues2 = strict.issues ?? [];\n" + }, + { + "from": " return strict ? { ...failed, strict } : failed;\n}\nfunction isExternalResponseSchemaAuthoritative(ctx) {\n const validationVersion = responseSchemaValidationVersion(ctx.adcpVersion, ctx.responseAdcpVersion);\n return validationVersion !== void 0 && (0, import_schema_loader.isExternalSchemaRootActive)(validationVersion);\n}\n", + "to": " return strict ? { ...failed, strict } : failed;\n}\nfunction isExternalResponseSchemaAuthoritative(ctx) {\n // The actual selected catalog JSON schema governs this opted testing surface,\n // including builtin roots. Unavailable strict verdicts still fail no-schema.\n if ((0, import_schema_loader.isCreativeBuildProbeSchemaScopeActive)() && ctx.taskName === \"get_adcp_capabilities\") return true;\n const validationVersion = responseSchemaValidationVersion(ctx.adcpVersion, ctx.responseAdcpVersion);\n return validationVersion !== void 0 && (0, import_schema_loader.isExternalSchemaRootActive)(validationVersion);\n}\n" + } + ] + }, + { + "path": "dist/lib/testing/storyboard/validations.mjs", + "before": "544843dfe357b0cf7e58888eed21f0b3c0bef0305f1b7d6b2651a447e0d0d8a3", + "after": "da94a605da35d4d308aebdf425f96aaa63e46b93171f69fa45719622e7c683cd", + "replacements": [ + { + "from": "import { injectLegacyEnvelopeStatus } from \"../../utils/envelope-status-compat.mjs\";\nimport { TRANSPORT_SUFFIX_REGEX } from \"../../utils/a2a-discovery.mjs\";\nimport { validateResponse } from \"../../validation/schema-validator.mjs\";\nimport { hasSchemaBundle, isExternalSchemaRootActive } from \"../../validation/schema-loader.mjs\";\nimport { ADCP_VERSION, LIBRARY_VERSION } from \"../../version.mjs\";\nimport { isPre31AdcpVersion } from \"../../utils/adcp-version-config.mjs\";\nimport { gte as semverGte, valid as validSemver } from \"semver\";\n", + "to": "import { injectLegacyEnvelopeStatus } from \"../../utils/envelope-status-compat.mjs\";\nimport { TRANSPORT_SUFFIX_REGEX } from \"../../utils/a2a-discovery.mjs\";\nimport { validateResponse } from \"../../validation/schema-validator.mjs\";\nimport {\n hasSchemaBundle,\n isExternalSchemaRootActive,\n withServedValidationVersion,\n isCreativeBuildProbeSchemaScopeActive\n} from \"../../validation/schema-loader.mjs\";\nimport { ADCP_VERSION, LIBRARY_VERSION } from \"../../version.mjs\";\nimport { isPre31AdcpVersion } from \"../../utils/adcp-version-config.mjs\";\nimport { gte as semverGte, valid as validSemver } from \"semver\";\n" + }, + { + "from": " }\n}\nfunction validateResponseSchema(validation, ctx, taskResult) {\n const taskName = ctx.taskName;\n const schemaIdentityVersion = ctx.adcpVersion ?? ctx.responseAdcpVersion ?? ADCP_VERSION;\n const { schema_id, schema_url } = resolveSchemaIdentity(ctx.responseSchemaRef, schemaIdentityVersion);\n", + "to": " }\n}\nfunction validateResponseSchema(validation, ctx, taskResult) {\n const pinnedVersion = ctx.adcpVersion ?? ADCP_VERSION;\n if (isCreativeBuildProbeSchemaScopeActive() && ctx.taskName === \"get_adcp_capabilities\" && pinnedVersion !== void 0) {\n return withServedValidationVersion(\n taskResult.data,\n pinnedVersion,\n (validationVersion) => validateResponseSchemaForVersion(\n validation,\n { ...ctx, adcpVersion: validationVersion, responseAdcpVersion: validationVersion },\n taskResult\n )\n );\n }\n return validateResponseSchemaForVersion(validation, ctx, taskResult);\n}\nfunction validateResponseSchemaForVersion(validation, ctx, taskResult) {\n const taskName = ctx.taskName;\n const schemaIdentityVersion = ctx.adcpVersion ?? ctx.responseAdcpVersion ?? ADCP_VERSION;\n const { schema_id, schema_url } = resolveSchemaIdentity(ctx.responseSchemaRef, schemaIdentityVersion);\n" + }, + { + "from": " schema_url,\n strict\n };\n const warning = buildStrictWarning(strict);\n return warning ? { ...base, warning } : base;\n }\n const issues2 = strict.issues ?? [];\n", + "to": " schema_url,\n strict\n };\n const warning = [\n buildStrictWarning(strict),\n isCreativeBuildProbeSchemaScopeActive() && taskName === \"get_adcp_capabilities\" && strict.lenient_valid === false\n ? `Authorized catalog JSON schema ${schema_url ?? schemaIdentityVersion} accepted this response; the SDK14 generated Zod schema disagrees`\n : void 0\n ].filter(Boolean).join(\"; \");\n return warning ? { ...base, warning } : base;\n }\n const issues2 = strict.issues ?? [];\n" + }, + { + "from": " return strict ? { ...failed, strict } : failed;\n}\nfunction isExternalResponseSchemaAuthoritative(ctx) {\n const validationVersion = responseSchemaValidationVersion(ctx.adcpVersion, ctx.responseAdcpVersion);\n return validationVersion !== void 0 && isExternalSchemaRootActive(validationVersion);\n}\n", + "to": " return strict ? { ...failed, strict } : failed;\n}\nfunction isExternalResponseSchemaAuthoritative(ctx) {\n // The actual selected catalog JSON schema governs this opted testing surface,\n // including builtin roots. Unavailable strict verdicts still fail no-schema.\n if (isCreativeBuildProbeSchemaScopeActive() && ctx.taskName === \"get_adcp_capabilities\") return true;\n const validationVersion = responseSchemaValidationVersion(ctx.adcpVersion, ctx.responseAdcpVersion);\n return validationVersion !== void 0 && isExternalSchemaRootActive(validationVersion);\n}\n" + } + ] + }, + { + "path": "dist/lib/testing/client.js", + "before": "e1c9dce20baea6f08f896d839a566f430fedd990003fbd3eca2a1c9536786131", + "after": "ebd7d4551e3395568aa056b611667db5ec4fdf94a7815ecda15c5f6ba9c3d110", + "replacements": [ + { + "from": "var import_client_hooks = require('../validation/client-hooks.js');\n", + "to": "var import_client_hooks = require('../validation/client-hooks.js');\nvar import_schema_loader = require('../validation/schema-loader.js');\n" + }, + { + "from": " const validation = (0, import_client_hooks.validateIncomingResponse)(\n \"get_adcp_capabilities\",\n caps.data,\n \"strict\",\n void 0,\n schemaAdcpVersion ?? client.getAdcpVersion()\n );", + "to": " const validation = (0, import_schema_loader.isCreativeBuildProbeSchemaScopeActive)()\n ? (0, import_schema_loader.withServedValidationVersion)(caps.data, schemaAdcpVersion ?? client.getAdcpVersion(), (validationVersion) => ({\n ...(0, import_client_hooks.validateIncomingResponse)(\"get_adcp_capabilities\", caps.data, \"strict\", void 0, validationVersion),\n catalogSchemaAuthority: validationVersion\n }))\n : (0, import_client_hooks.validateIncomingResponse)(\n \"get_adcp_capabilities\",\n caps.data,\n \"strict\",\n void 0,\n schemaAdcpVersion ?? client.getAdcpVersion()\n );" + }, + { + "from": " message: issue.message\n }));", + "to": " message: validation.catalogSchemaAuthority !== void 0\n ? `Capability catalog JSON schema ${validation.catalogSchemaAuthority}: ${issue.message}`\n : issue.message\n }));" + } + ] + }, + { + "path": "dist/lib/testing/client.mjs", + "before": "4cb2fc25d79a7e984025dabedba992192dbd8829b983a0f33622cde5bc15d385", + "after": "b5001aaa6a1fea775f350b965b27219c0e51586c5050dfe644f63a56705c2bbb", + "replacements": [ + { + "from": "import { validateIncomingResponse } from \"../validation/client-hooks.mjs\";\n", + "to": "import { validateIncomingResponse } from \"../validation/client-hooks.mjs\";\nimport { withServedValidationVersion, isCreativeBuildProbeSchemaScopeActive } from \"../validation/schema-loader.mjs\";\n" + }, + { + "from": " const validation = validateIncomingResponse(\n \"get_adcp_capabilities\",\n caps.data,\n \"strict\",\n void 0,\n schemaAdcpVersion ?? client.getAdcpVersion()\n );", + "to": " const validation = isCreativeBuildProbeSchemaScopeActive()\n ? withServedValidationVersion(caps.data, schemaAdcpVersion ?? client.getAdcpVersion(), (validationVersion) => ({\n ...validateIncomingResponse(\"get_adcp_capabilities\", caps.data, \"strict\", void 0, validationVersion),\n catalogSchemaAuthority: validationVersion\n }))\n : validateIncomingResponse(\n \"get_adcp_capabilities\",\n caps.data,\n \"strict\",\n void 0,\n schemaAdcpVersion ?? client.getAdcpVersion()\n );" + }, + { + "from": " message: issue.message\n }));", + "to": " message: validation.catalogSchemaAuthority !== void 0\n ? `Capability catalog JSON schema ${validation.catalogSchemaAuthority}: ${issue.message}`\n : issue.message\n }));" + } + ] + }, + { + "path": "dist/lib/testing/compliance/comply.js", + "before": "9cb712b245ccb269b846028e50313eb4bd7576440a9e7d1ece3ccda1b615c0b0", + "after": "66bbd6f791db3035ff2aa0d9bc707a68ad168698014218633d77b3e6333d228d", + "replacements": [ + { + "from": "return await (0, import_schema_loader.withExternalSchemaRoot)(complianceIndex.adcp_version, scopedSchemaRoot, async () => {", + "to": "return await (0, import_schema_loader.withExternalSchemaRoot)(\n complianceIndex.adcp_version,\n scopedSchemaRoot,\n () => (0, import_schema_loader.withCreativeBuildProbeSchemaScope)(false, () => complyInSchemaScope())\n );\n async function complyInSchemaScope() {" + }, + { + "from": "\n controller_scenarios: controllerDetection.detected ? controllerDetection.scenarios : void 0,\n tested_at: (/* @__PURE__ */ new Date()).toISOString(),\n total_duration_ms: Date.now() - start,\n notices: noticesDedup\n };\n });\n}", + "to": "\n controller_scenarios: controllerDetection.detected ? controllerDetection.scenarios : void 0,\n tested_at: (/* @__PURE__ */ new Date()).toISOString(),\n total_duration_ms: Date.now() - start,\n notices: noticesDedup\n };\n }\n}" + } + ] + }, + { + "path": "dist/lib/testing/compliance/comply.mjs", + "before": "56cd87b2720db9de2618d8cb074fbb8ff08261479b828455cb053cbf6af2e962", + "after": "a96069a889e271af1e5b858a76179474f73d22ee0dceda92cfd8222bea766e60", + "replacements": [ + { + "from": "import { withExternalSchemaRoot } from \"../../validation/schema-loader.mjs\";\n", + "to": "import { withExternalSchemaRoot, withCreativeBuildProbeSchemaScope } from \"../../validation/schema-loader.mjs\";\n" + }, + { + "from": "return await withExternalSchemaRoot(complianceIndex.adcp_version, scopedSchemaRoot, async () => {", + "to": "return await withExternalSchemaRoot(\n complianceIndex.adcp_version,\n scopedSchemaRoot,\n () => withCreativeBuildProbeSchemaScope(false, () => complyInSchemaScope())\n );\n async function complyInSchemaScope() {" + }, + { + "from": "\n controller_scenarios: controllerDetection.detected ? controllerDetection.scenarios : void 0,\n tested_at: (/* @__PURE__ */ new Date()).toISOString(),\n total_duration_ms: Date.now() - start,\n notices: noticesDedup\n };\n });\n}", + "to": "\n controller_scenarios: controllerDetection.detected ? controllerDetection.scenarios : void 0,\n tested_at: (/* @__PURE__ */ new Date()).toISOString(),\n total_duration_ms: Date.now() - start,\n notices: noticesDedup\n };\n }\n}" + } + ] + }, + { + "path": "dist/lib/testing/types.d.ts", + "before": "27b26f247b230d25bd04116c08a9962e2670d858fb2bb8770d31b437e74ba2fc", + "after": "17a1f539173828f9dd23ba8215553a8fb1b934994e9384a843e1de12f1aa4f3d", + "replacements": [ + { + "from": " schemaRoot?: string;", + "to": " schemaRoot?: string;\n /** Explicit same-major served-release schema roots for creative_build_probe catalogs.\n * Identity is checked; request schemas and dispatch retain the run pin.\n * Honored only by opted creative_build_probe storyboards, including inside\n * comply(). Deferred/inert for other storyboards and bundle discovery. */\n servedReleaseSchemaRoots?: Readonly>;" + } + ] + }, + { + "path": "dist/lib/testing/types.d.mts", + "before": "12c6b39b9fe6ae32aa4c526ffb9c5d376a939183daa3fc369a8bcd25fa219932", + "after": "b533f66d73e8b22ea5a5789dab350c4054247be10a9a1ab2f506bed5d9ad015a", + "replacements": [ + { + "from": " schemaRoot?: string;", + "to": " schemaRoot?: string;\n /** Explicit same-major served-release schema roots for creative_build_probe catalogs.\n * Identity is checked; request schemas and dispatch retain the run pin.\n * Honored only by opted creative_build_probe storyboards, including inside\n * comply(). Deferred/inert for other storyboards and bundle discovery. */\n servedReleaseSchemaRoots?: Readonly>;" + } + ] + } + ] +} diff --git a/scripts/install-sdk-creative-build-probe.cjs b/scripts/install-sdk-creative-build-probe.cjs new file mode 100644 index 00000000000..baa63520ff8 --- /dev/null +++ b/scripts/install-sdk-creative-build-probe.cjs @@ -0,0 +1,56 @@ +#!/usr/bin/env node +'use strict'; + +// Owned, temporary SDK bridge; see .agents/sdk-shim-ledger.json. Apply only +// reviewed 14.0.0-rc.53 preimages, never a newer runner bundle or an approximate edit. +const fs = require('node:fs'); +const path = require('node:path'); +const { createHash } = require('node:crypto'); + +const patchRoot = path.resolve(__dirname, '../patches/sdk-creative-build-probe'); +const manifest = JSON.parse(fs.readFileSync(path.join(patchRoot, 'manifest.json'), 'utf8')); +const digest = value => createHash('sha256').update(value).digest('hex'); +const helperPayloads = new Set(['creative-build-probe.js', 'creative-build-probe.mjs', 'creative-build-probe.d.ts', 'creative-build-probe.d.mts']); + +function install(packageRoot) { + const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')); + if (pkg.name !== manifest.package || pkg.version !== manifest.version) { + throw new Error(`Creative build probe bridge requires ${manifest.package}@${manifest.version}; got ${pkg.name}@${pkg.version}. Qualify the upstream replacement before removing this bridge.`); + } + // Validate every target and every prospective output before any write. + const staged = manifest.files.map(entry => { + if (!(entry.path === 'bin/adcp.js' || entry.path.startsWith('dist/lib/testing/storyboard/') || /^dist\/lib\/testing\/(?:index\.(?:js|mjs|d\.ts|d\.mts)|types\.d\.(?:ts|mts)|client\.(?:js|mjs)|compliance\/comply\.(?:js|mjs))$/.test(entry.path) || /^dist\/lib\/validation\/schema-loader\.(?:js|mjs)$/.test(entry.path)) || entry.path.includes('..')) { + throw new Error('Invalid SDK bridge target'); + } + if (entry.contents_file !== undefined && (!helperPayloads.has(entry.contents_file) || !entry.path.endsWith('/' + entry.contents_file))) { + throw new Error('Invalid SDK bridge contents_file'); + } + const file = path.join(packageRoot, entry.path); + const before = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null; + const hash = before === null ? null : digest(before); + if (hash === entry.after) return { file, done: true }; + if (hash !== entry.before) throw new Error(`SDK bridge content drift: ${entry.path}`); + let after = entry.contents_file + ? fs.readFileSync(path.join(patchRoot, entry.contents_file), 'utf8') + : before; + for (const edit of entry.replacements ?? []) { + if (after.split(edit.from).length !== 2) throw new Error(`SDK bridge anchor drift: ${entry.path}`); + after = after.replace(edit.from, () => edit.to); + } + if (digest(after) !== entry.after) throw new Error(`SDK bridge output drift: ${entry.path}`); + return { file, done: false, after }; + }); + const done = staged.filter(entry => entry.done).length; + if (done && done !== staged.length) throw new Error('SDK bridge has mixed original/patched targets; reinstall dependencies'); + if (done === staged.length) return; + for (const entry of staged) fs.writeFileSync(entry.file, entry.after); +} + +if (require.main === module) { + // Resolve the package root through the public entry, then verify its identity. + const entry = require.resolve('@adcp/sdk/testing'); + const packageRoot = path.resolve(entry, '../../../..'); + install(packageRoot); +} + +module.exports = { install }; diff --git a/scripts/lint-storyboard-sample-request-schema.cjs b/scripts/lint-storyboard-sample-request-schema.cjs index 79e38b4fa24..dcc0675789e 100644 --- a/scripts/lint-storyboard-sample-request-schema.cjs +++ b/scripts/lint-storyboard-sample-request-schema.cjs @@ -273,6 +273,59 @@ function normalizeSubstitutions(value, schema) { return value; } +/** + * Materialize the opt-in build probe's context inputs before static schema + * checks, just as the runner does before dispatch. This synthetic catalog is + * only a representative authoring fixture, never a live producer prerequisite. + * Runtime/catalog applicability and other input modes have executable runner + * coverage; this lint must still catch broken bindings and authored payloads. + */ +function normalizeStoryboardSampleRequests(storyboard, step, schema) { + if (!storyboard.creative_build_probe || step.task !== 'build_creative' + || step.requires_contract !== 'creative_build_probe_runner') { + return [normalizeSubstitutions(step.sample_request, schema)]; + } + const { resolveCreativeBuildProbe } = require('@adcp/sdk/testing'); + // Exercise every bound source key and both selector paths, without assuming + // a particular live agent supports any of these representative modes. + const inputs = [ + undefined, // The resolver's bounded automatic generation recipe. + { creative_manifest: { format_kind: 'image', assets: {} } }, + { creative_id: 'schema-lint-creative' }, + { transformer_id: 'schema-lint-transformer', config: {} }, + { creative_id: 'schema-lint-creative', target_format_id: { agent_url: 'https://formats.example', id: 'schema-lint-format' } }, + ]; + return inputs.map(input => { + const resolution = resolveCreativeBuildProbe({ + supports_generation: true, + supports_transformation: true, + has_creative_library: true, + supports_transformers: true, + supported_formats: [{ + capability_id: 'schema-lint-build', + operations: ['build'], + format: { format_kind: 'image', params: {}, technical_requirements_complete: true }, + }], + }, storyboard.creative_build_probe, input && { + creative_build_probes: [{ match: [{ path: '/capability_id', operator: 'equals', value: 'schema-lint-build' }], input }], + }); + if (resolution.status !== 'resolved') { + throw new Error(`Cannot prepare creative build authoring fixture: ${resolution.detail}`); + } + const request = structuredClone(step.sample_request); + for (const binding of step.context_inputs || []) { + if (!Object.hasOwn(resolution.context, binding.key)) continue; + // This contract binds top-level build request fields. Do not silently + // flatten a malformed injection path or change any other lint contract. + if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(binding.inject_at)) { + throw new Error(`Invalid creative build authoring fixture path: ${binding.inject_at}`); + } + request[binding.inject_at] = structuredClone(resolution.context[binding.key]); + } + return normalizeSubstitutions(request, schema); + }); +} + // One shared ajv instance + compile cache keyed by schema_ref. Each schema // compiles once per run; a 78-storyboard suite hit ~12s when this was // per-step, ~2s with the cache. @@ -395,9 +448,13 @@ async function lintFile(file) { if (!schemaRef || schemaRef.startsWith('$test_kit.')) continue; if (!sampleRequest || typeof sampleRequest !== 'object') continue; if (isNegativeStep(step)) continue; - const result = await validateStep({ schemaRef, payload: sampleRequest }); - if (!result.ok) { - violations.push({ file, phaseId, stepId, schemaRef, errors: result.errors }); + const schema = loadSchema(schemaRef); + const payloads = schema ? normalizeStoryboardSampleRequests(doc, step, schema) : [sampleRequest]; + for (const payload of payloads) { + const result = await validateStep({ schemaRef, payload }); + if (!result.ok) { + violations.push({ file, phaseId, stepId, schemaRef, errors: result.errors }); + } } } } @@ -623,6 +680,7 @@ module.exports = { lintFile, validateStep, normalizeSubstitutions, + normalizeStoryboardSampleRequests, loadSchema, formatViolation, fingerprintError, diff --git a/server/src/compliance/storyboard-runner-options.ts b/server/src/compliance/storyboard-runner-options.ts index 0ede8a4797d..5ccbd286fb7 100644 --- a/server/src/compliance/storyboard-runner-options.ts +++ b/server/src/compliance/storyboard-runner-options.ts @@ -1,8 +1,9 @@ -import type { StoryboardRunOptions } from '@adcp/sdk/testing'; +import type { CreativeBuildProbeInputFixture, StoryboardRunOptions } from '@adcp/sdk/testing'; export interface LoadedTestKit { brand?: { house?: { domain?: string }; brand_id?: string }; commercial_relationship?: string; + creative_build_probes?: CreativeBuildProbeInputFixture[]; auth?: { api_key?: string; basic?: { username?: string; password?: string; credentials?: string }; @@ -26,21 +27,26 @@ export const PROBE_TASK_BY_TENANT: Record = { }; /** - * Thread the test-kit's auth material through to the storyboard runner so - * kit-gated auth phases execute instead of being skipped by `skip_if`. + * Thread the test-kit's allowed auth and creative-probe inputs through to the + * storyboard runner so authored fixture prerequisites remain available. */ export function testKitOptionsFromKit( kit: LoadedTestKit | undefined, tenantPath = process.env.TENANT_PATH, ): StoryboardRunOptions['test_kit'] | undefined { const auth = kit?.auth; - if (!auth?.api_key && !auth?.basic && !auth?.probe_task) return undefined; + const creativeBuildProbes = kit?.creative_build_probes; + const creativeProbeOptions = creativeBuildProbes === undefined + ? undefined + : { creative_build_probes: creativeBuildProbes }; + if (!auth?.api_key && !auth?.basic && !auth?.probe_task) return creativeProbeOptions; if (!auth.probe_task) { throw new Error('test kit declares auth credentials without auth.probe_task — required by runner'); } const probeTask = (tenantPath && PROBE_TASK_BY_TENANT[tenantPath]) ?? auth.probe_task; const commercialRelationship = kit?.commercial_relationship; return { + ...creativeProbeOptions, ...(commercialRelationship !== undefined && { commercial_relationship: commercialRelationship, }), diff --git a/server/tests/unit/storyboard-runner-options.test.ts b/server/tests/unit/storyboard-runner-options.test.ts index 5d1951348ce..aab42aece38 100644 --- a/server/tests/unit/storyboard-runner-options.test.ts +++ b/server/tests/unit/storyboard-runner-options.test.ts @@ -157,6 +157,59 @@ describe('storyboard runner option helpers', () => { }); }); + const creativeBuildProbes: NonNullable = [{ + match: [{ path: '/capability_id', operator: 'equals', value: 'sandbox-transformer-output' }], + input: { + transformer_id: 'sandbox-transformer', + config: { headline: 'Winter offer' }, + }, + }]; + + it('retains provisioned creative build recipes with auth and the tenant probe override', () => { + const kit = { + creative_build_probes: creativeBuildProbes, + auth: { api_key: 'kit-api-key', probe_task: 'list_creatives' }, + brand: { house: { domain: 'acmeoutdoor.example' } }, + unrelated: 'not a runner input', + }; + + const options = testKitOptionsFromKit(kit, 'signals'); + + expect(options).toEqual({ + creative_build_probes: creativeBuildProbes, + auth: { api_key: 'kit-api-key', probe_task: 'get_signals' }, + }); + expect(options?.creative_build_probes).toBe(creativeBuildProbes); + }); + + it('retains provisioned creative build recipes without auth and excludes unrelated kit fields', () => { + const kit = { + creative_build_probes: creativeBuildProbes, + brand: { house: { domain: 'acmeoutdoor.example' } }, + unrelated: 'not a runner input', + }; + + const options = testKitOptionsFromKit(kit, 'creative'); + + expect(options).toEqual({ creative_build_probes: creativeBuildProbes }); + expect(options?.creative_build_probes).toBe(creativeBuildProbes); + expect(options).not.toHaveProperty('auth'); + }); + + it('keeps kits without auth or creative recipes out of runner options', () => { + expect(testKitOptionsFromKit(undefined, 'creative')).toBeUndefined(); + expect(testKitOptionsFromKit({ brand: { house: { domain: 'acmeoutdoor.example' } } }, 'creative')) + .toBeUndefined(); + }); + + it.each([ + { api_key: 'kit-api-key' }, + { basic: { username: 'agent-user', password: 'agent-pass' } }, + ])('still rejects auth without a probe_task when creative recipes are present: %j', auth => { + expect(() => testKitOptionsFromKit({ creative_build_probes: creativeBuildProbes, auth }, 'creative')) + .toThrow(/without auth\.probe_task/); + }); + it('requires probe_task when a test kit declares auth credentials', () => { const kit: LoadedTestKit = { auth: { api_key: 'kit-api-key' } }; diff --git a/static/compliance/source/protocols/creative/scenarios/evaluator_auth.yaml b/static/compliance/source/protocols/creative/scenarios/evaluator_auth.yaml index 9d60da0e06e..3579b8b6fc6 100644 --- a/static/compliance/source/protocols/creative/scenarios/evaluator_auth.yaml +++ b/static/compliance/source/protocols/creative/scenarios/evaluator_auth.yaml @@ -1,5 +1,5 @@ id: creative/evaluator_auth -version: "1.0.3" +version: "1.0.5" title: "Creative evaluator authentication boundary" category: creative summary: "Verifies that build_creative evaluator pointers use the verifier allowlist, keep credentials out of payloads, and invoke accepted evaluator agents through get_creative_features." @@ -11,9 +11,17 @@ introduced_in: "3.1" required_tools: - build_creative -requires_capability: - path: creative.supports_evaluator - equals: true +# The compound gate requires an established positive profile. A single +# requires_capability permits unknown raw capabilities after a failed probe. +requires_all_capabilities: + - path: creative + present: true + - path: creative.supports_evaluator + equals: true + +creative_build_probe: + context_prefix: build + message: "Create one creative for Acme Outdoor's winter trip offer." narrative: | `build_creative.evaluator` is an experimental buyer-attached pointer for @@ -43,6 +51,18 @@ narrative: | are not cryptographic proof of transport authentication. Authentication proof remains a normative obligation on the producing agent and evaluator. + Applicability requires the established positive capability profile above; + unknown capability evidence receives no executed security coverage. For a + supporting agent, both off-list rejection phases are required independently: the + evaluator spec and build_creative reference require rejection before any + outbound call. A later discovery response cannot opt out of these checks. + The creative-build-probe runner resolves a real build route and its input as + one binding before seeding or transport. It honors explicit operations and + legacy omitted operations; an unavailable input fixture is a visible + fixture_unsatisfied coverage gap, not producer nonconformance or security + coverage. Ranking, feature vocabulary and variant support are not prerequisites + for the two required boundary probes. + agent: interaction_model: creative_agent capabilities: @@ -61,9 +81,19 @@ prerequisites: description: | The creative agent advertises `creative.supports_evaluator: true`, lists `creative.evaluator` in `experimental_features`, exposes at least one - targetable `creative.supported_formats[].capability_id`, and exposes at - least one `governance.creative_features[].feature_id` for the account under - test. Its sandbox `creative_policy.accepted_verifiers[]` includes + usable build entry in `creative.supported_formats[]`. For 3.1, provision a + matched legacy `target_format_id` recipe; do not derive it from a capability ID. The runner contract + `test-kits/creative-build-probe-runner.yaml` documents preparation of that + entry. Add operator recipes to the brand kit named below, or supply your + kit through the runner test_kit option/--test-kit. Provision account-specific transformation/library/transformer inputs + in `test_kit.creative_build_probes` when a unique generation route cannot be + inferred. The provisioned sandbox account must be authorized and entitled + to build with the selected route/input, including any required governance + authorization. This runner contract currently requires a build fixture + without per-request signed governance_context; advertised signed-context + build enforcement yields a zero-credit fixture gap. Optional ranking probes + additionally use creative feature + vocabulary. Its sandbox `creative_policy.accepted_verifiers[]` includes `https://quality-evaluator.example/adcp`. The evaluator fixture exposes the standard `get_creative_features` contract. @@ -101,8 +131,8 @@ phases: narrative: | The buyer reads capabilities before sending evaluator inputs. Agents that do not advertise evaluator support skip the remaining evaluator-specific - phases. Supporting agents also need a build capability id and a creative - feature id so later phases do not hardcode fixture-specific values. + phases. The runner binds an actual build route and valid input before execution; + optional ranking phases additionally discover a creative feature id. steps: - id: get_capabilities @@ -116,7 +146,7 @@ phases: expected: | Return the creative capability block. When `creative.supports_evaluator` is true, the following phases validate the evaluator boundary using - discovered build and feature identifiers. + the route and valid input bound by the runner. sample_request: protocols: - "creative" @@ -126,10 +156,6 @@ phases: context_outputs: - name: "supports_evaluator" path: "creative.supports_evaluator" - - name: "build_capability_id" - path: "creative.supported_formats[0].capability_id" - - name: "creative_feature_id" - path: "governance.creative_features[0].feature_id" validations: - check: response_schema description: "Response matches get-adcp-capabilities-response.json schema" @@ -170,6 +196,9 @@ phases: - "governance" context: correlation_id: "creative_evaluator_auth--feature_contract" + context_outputs: + - name: "creative_feature_id" + path: "governance.creative_features[0].feature_id" validations: - check: response_schema description: "Response matches get-adcp-capabilities-response.json schema" @@ -181,9 +210,6 @@ phases: path: "experimental_features[*]" value: "creative.evaluator" description: "Experimental evaluator feature id is declared" - - check: field_present - path: "creative.supported_formats[0].capability_id" - description: "A targetable build capability id is discoverable" - check: field_present path: "governance.creative_features[0].feature_id" description: "Creative feature vocabulary is discoverable for evaluator rank_by/eval.features" @@ -194,11 +220,9 @@ phases: - id: reject_off_list_evaluator title: "Reject an off-list evaluator URL" - optional: true requires_capability: path: "creative.supports_evaluator" equals: true - skip_if: "context.supports_evaluator != true || !context.build_capability_id || !context.creative_feature_id" narrative: | The buyer supplies a well-formed external evaluator URL that is not in the seller's accepted verifier allowlist. The creative agent must reject it @@ -208,6 +232,7 @@ phases: - id: reject_unaccepted_agent_url title: "Build with an unaccepted evaluator URL" task: build_creative + requires_contract: creative_build_probe_runner schema_ref: "media-buy/build-creative-request.json" response_schema_ref: "media-buy/build-creative-response.json" doc_ref: "/creative/task-reference/build_creative" @@ -220,17 +245,8 @@ phases: off-list URL before any outbound call to the supplied evaluator. sample_request: idempotency_key: "$generate:uuid_v4#creative_evaluator_auth_off_list" - message: "Create two best-of-N display variants for Acme Outdoor's winter trip offer." - target_capability_id: "$context.build_capability_id" - max_variants: 2 - variant_axis: - dimension: "best_of_n" - keep_mode: "keep_one" evaluator: agent_url: "https://unaccepted-evaluator.example/adcp" - rank_by: - - feature_id: "$context.creative_feature_id" - direction: "maximize" account: brand: domain: "acmeoutdoor.example" @@ -242,6 +258,21 @@ phases: domain: "acmeoutdoor.example" context: correlation_id: "creative_evaluator_auth--off_list" + context_inputs: + - key: build_capability_id + inject_at: target_capability_id + - key: build_target_format_id + inject_at: target_format_id + - key: build_message + inject_at: message + - key: build_creative_manifest + inject_at: creative_manifest + - key: build_creative_id + inject_at: creative_id + - key: build_transformer_id + inject_at: transformer_id + - key: build_config + inject_at: config validations: - check: response_schema description: "Response matches build-creative-response.json schema" @@ -251,25 +282,24 @@ phases: - check: upstream_traffic description: "Off-list direct evaluator URL is never called" min_count: 0 - endpoint_pattern: "POST https://unaccepted-evaluator.example/adcp*" + endpoint_pattern: "* https://unaccepted-evaluator.example/*" - id: reject_off_list_feature_agent title: "Reject an off-list feature_agent URL" - optional: true requires_capability: path: "creative.supports_evaluator" equals: true - skip_if: "context.supports_evaluator != true || !context.build_capability_id || !context.creative_feature_id" narrative: | - The buyer supplies a well-formed `feature_agent.agent_url` alongside an - account-arranged evaluator id. The nested feature agent uses the same - accepted-verifier allowlist and must be rejected before any outbound call + The buyer supplies a well-formed `feature_agent.agent_url` alongside the + accepted external evaluator source configured in the prerequisites. The + nested feature agent uses the same accepted-verifier allowlist and must be rejected before any outbound call when it is off-list. steps: - id: reject_unaccepted_feature_agent title: "Build with an unaccepted feature_agent URL" task: build_creative + requires_contract: creative_build_probe_runner schema_ref: "media-buy/build-creative-request.json" response_schema_ref: "media-buy/build-creative-response.json" doc_ref: "/creative/task-reference/build_creative" @@ -283,20 +313,10 @@ phases: endpoint. sample_request: idempotency_key: "$generate:uuid_v4#creative_evaluator_auth_feature_agent_off_list" - message: "Create two best-of-N display variants for Acme Outdoor's winter trip offer." - target_capability_id: "$context.build_capability_id" - max_variants: 2 - variant_axis: - dimension: "best_of_n" - keep_mode: "keep_one" evaluator: - evaluator_id: "quality-default" + agent_url: "https://quality-evaluator.example/adcp" feature_agent: agent_url: "https://unaccepted-evaluator.example/adcp" - feature_id: "$context.creative_feature_id" - rank_by: - - feature_id: "$context.creative_feature_id" - direction: "maximize" account: brand: domain: "acmeoutdoor.example" @@ -308,6 +328,21 @@ phases: domain: "acmeoutdoor.example" context: correlation_id: "creative_evaluator_auth--feature_agent_off_list" + context_inputs: + - key: build_capability_id + inject_at: target_capability_id + - key: build_target_format_id + inject_at: target_format_id + - key: build_message + inject_at: message + - key: build_creative_manifest + inject_at: creative_manifest + - key: build_creative_id + inject_at: creative_id + - key: build_transformer_id + inject_at: transformer_id + - key: build_config + inject_at: config validations: - check: response_schema description: "Response matches build-creative-response.json schema" @@ -317,7 +352,7 @@ phases: - check: upstream_traffic description: "Off-list nested feature_agent URL is never called" min_count: 0 - endpoint_pattern: "POST https://unaccepted-evaluator.example/adcp*" + endpoint_pattern: "* https://unaccepted-evaluator.example/*" - id: payload_credentials_advisory title: "Keep evaluator credentials out of the payload" @@ -325,7 +360,7 @@ phases: requires_capability: path: "creative.supports_evaluator" equals: true - skip_if: "context.supports_evaluator != true || !context.build_capability_id || !context.creative_feature_id" + skip_if: "context.supports_evaluator != true || !context.creative_feature_id" narrative: | The buyer attempts to pass an evaluator API key inside the evaluator object. The payload is schema-valid but non-conforming: evaluator @@ -339,6 +374,7 @@ phases: - id: evaluator_api_key_payload title: "Build with a credential-shaped evaluator field" task: build_creative + requires_contract: creative_build_probe_runner schema_ref: "media-buy/build-creative-request.json" response_schema_ref: "media-buy/build-creative-response.json" doc_ref: "/creative/task-reference/build_creative" @@ -351,8 +387,6 @@ phases: must not forward the smuggled credential to the evaluator. sample_request: idempotency_key: "$generate:uuid_v4#creative_evaluator_auth_payload_credential" - message: "Create two best-of-N display variants for Acme Outdoor's winter trip offer." - target_capability_id: "$context.build_capability_id" max_variants: 2 variant_axis: dimension: "best_of_n" @@ -375,6 +409,21 @@ phases: domain: "acmeoutdoor.example" context: correlation_id: "creative_evaluator_auth--payload_credential" + context_inputs: + - key: build_capability_id + inject_at: target_capability_id + - key: build_target_format_id + inject_at: target_format_id + - key: build_message + inject_at: message + - key: build_creative_manifest + inject_at: creative_manifest + - key: build_creative_id + inject_at: creative_id + - key: build_transformer_id + inject_at: transformer_id + - key: build_config + inject_at: config validations: - check: response_schema description: "Response matches build-creative-response.json schema" @@ -404,7 +453,7 @@ phases: requires_capability: path: "creative.supports_evaluator" equals: true - skip_if: "context.supports_evaluator != true || !context.build_capability_id || !context.creative_feature_id" + skip_if: "context.supports_evaluator != true || !context.creative_feature_id" narrative: | The buyer supplies an accepted external evaluator URL. The creative agent builds variants, calls the evaluator through the `get_creative_features` @@ -415,6 +464,7 @@ phases: - id: build_with_accepted_evaluator title: "Build and rank with an accepted evaluator" task: build_creative + requires_contract: creative_build_probe_runner schema_ref: "media-buy/build-creative-request.json" response_schema_ref: "media-buy/build-creative-response.json" doc_ref: "/creative/task-reference/build_creative" @@ -429,8 +479,6 @@ phases: authentication mechanism. sample_request: idempotency_key: "$generate:uuid_v4#creative_evaluator_auth_accepted" - message: "Create two best-of-N display variants for Acme Outdoor's winter trip offer." - target_capability_id: "$context.build_capability_id" max_variants: 2 variant_axis: dimension: "best_of_n" @@ -454,6 +502,21 @@ phases: domain: "acmeoutdoor.example" context: correlation_id: "creative_evaluator_auth--accepted" + context_inputs: + - key: build_capability_id + inject_at: target_capability_id + - key: build_target_format_id + inject_at: target_format_id + - key: build_message + inject_at: message + - key: build_creative_manifest + inject_at: creative_manifest + - key: build_creative_id + inject_at: creative_id + - key: build_transformer_id + inject_at: transformer_id + - key: build_config + inject_at: config validations: - check: response_schema description: "Response matches build-creative-response.json schema" @@ -485,7 +548,7 @@ phases: requires_capability: path: "creative.supports_evaluator" equals: true - skip_if: "context.supports_evaluator != true || !context.build_capability_id || !context.creative_feature_id" + skip_if: "context.supports_evaluator != true || !context.creative_feature_id" narrative: | An accepted evaluator can be unreachable or can reject the producing agent's transport authentication. The buyer-visible build should not fail @@ -498,6 +561,7 @@ phases: - id: force_evaluator_unavailable title: "Force accepted evaluator unavailable" task: comply_test_controller + requires_contract: creative_build_probe_runner schema_ref: "compliance/comply-test-controller-request.json" response_schema_ref: "compliance/comply-test-controller-response.json" doc_ref: "/building/by-layer/L3/comply-test-controller" @@ -533,6 +597,7 @@ phases: - id: build_with_unavailable_evaluator title: "Build falls back to seller-default ranking" task: build_creative + requires_contract: creative_build_probe_runner schema_ref: "media-buy/build-creative-request.json" response_schema_ref: "media-buy/build-creative-response.json" doc_ref: "/creative/task-reference/build_creative" @@ -545,8 +610,6 @@ phases: evaluator ranking was unavailable and seller-default ranking was used. sample_request: idempotency_key: "$generate:uuid_v4#creative_evaluator_auth_unavailable" - message: "Create two best-of-N display variants for Acme Outdoor's winter trip offer." - target_capability_id: "$context.build_capability_id" max_variants: 2 variant_axis: dimension: "best_of_n" @@ -567,6 +630,21 @@ phases: domain: "acmeoutdoor.example" context: correlation_id: "creative_evaluator_auth--unavailable_degrade" + context_inputs: + - key: build_capability_id + inject_at: target_capability_id + - key: build_target_format_id + inject_at: target_format_id + - key: build_message + inject_at: message + - key: build_creative_manifest + inject_at: creative_manifest + - key: build_creative_id + inject_at: creative_id + - key: build_transformer_id + inject_at: transformer_id + - key: build_config + inject_at: config validations: - check: response_schema description: "Response matches build-creative-response.json schema despite evaluator unavailability" diff --git a/static/compliance/source/test-kits/creative-build-probe-runner.yaml b/static/compliance/source/test-kits/creative-build-probe-runner.yaml new file mode 100644 index 00000000000..a1bfeeba0bc --- /dev/null +++ b/static/compliance/source/test-kits/creative-build-probe-runner.yaml @@ -0,0 +1,127 @@ +id: creative_build_probe_runner +applies_to: + storyboards: + - creative/evaluator_auth +endpoint_scope: sandbox +harness_mode: black_box +description: | + Read-only preparation of one otherwise valid build_creative request, before + fixture seeding or task dispatch. This is a runner contract, not a new agent + capability. An operator provisions an active sandbox account, build entitlement, + task authorization (including governance where applicable), and the accepted + evaluator fixture. Format completeness does not establish those prerequisites. + + The storyboard's creative_build_probe contains context_prefix and message. + Resolve route identity and input atomically from the established capability + profile's creative.supported_formats. Explicit operations must include build; + omitted operations retain the existing legacy build meaning. An omitted + capability_id is legal but cannot be used for capability-ID routing. Catalog + position never establishes whether an operation or its inputs are supported. + + Prefer an operator-provisioned test_kit.creative_build_probes recipe whose + nonempty match clauses use the existing fixture-resolution path/operator/value + DSL against the complete catalog entry. Each recipe's input may contain only + message, creative_manifest, creative_id, transformer_id, config, and (for legacy + routing) target_format_id. The + operator asserts that this input is valid for EVERY route the recipe matches, + for this account and test run. The preparer checks the selected binding; it + does not certify unselected matches or remote account entitlements. Use a specific capability_id predicate when a + canonical-kind predicate cannot establish that. Recipes do not override the + evaluator, account, authentication, correlation, or trust controls. Manifests + must validate against the selected protocol schema; credentials/trust material + must not be embedded in the recipe. For this probe contract, config requires a provisioned transformer; + this does not restrict other legal build_creative request modes. + Selected input modes must also be advertised by the creative agent. + + Follow the capabilities schema's format reference using the SDK's validated + served-release selection (same major, listed supported version, available + trusted schema root). Keep build request selectors, source manifests and + outgoing requests on the run's pin. Capability-ID routing requires both the + served catalog's routing contract and the pinned request's selector support. In 3.1, a recipe must explicitly supply a schema-valid, discovered or + provisioned target_format_id and assert its mapping to the matched route; + never synthesize a named-format ID from capability_id. A missing capability_id + is legal on that legacy path. Retain unknown 3.1 canonical kinds as unsupported + routes and continue to known routes. Do not impose 3.2 ID/operations rules on + older catalogs. Preserve advertised legacy reference constraints even when + the pinned request has older fields. This changes harness inputs, not protocol + capabilities. + + A bounded automatic recipe is available only when exactly one build route is + advertised (including unaddressable legacy entries), supports_generation is + true, capability-ID routing is supported, and its format declares technical_requirements_complete: true. The + format_kind must be a known non-custom canonical kind, params must be empty, + and other format metadata must be understood as inert: display_name, + seller_preference, experimental, sample_render_url, format_option_id, + publisher_domain, applies_to_channels, or canonical_formats_only. The route may + contain only capability_id, operations, and format. Unhandled declarations, + extensions, slots, connections, or unknown metadata require a provisioned + recipe. Supply both the authored brief and creative_manifest with the selected + format_kind and assets: {}. Do not synthesize seed assets or infer omitted + constraints without completeness. These conditions follow existing canonical + completeness and build-generation input requirements; they are not additional + requirements on producers' catalogs. + + Select a satisfiable route, continuing beyond non-build or unmatched entries. + Bind exactly one _capability_id or _target_format_id, plus any supplied _message and + _creative_manifest, _creative_id, _transformer_id, and _config. Clear + stale values in this namespace first. Only then enable this harness contract. + Emit a fixture_resolution_result with fixture_type: creative_format, the prefix + as handle, exact matched requirements, and seller_ids.capability_id or seller_ids.format_id. Record + whether the input came from the automatic generation rule or the test kit. + + No satisfiable input is fixture_unsatisfied with detail naming + creative_format:, zero executed security validations, and a coverage + gap. It is neither producer nonconformance nor security coverage. Malformed + catalogs produce structured failed validations, not an unavailable strategy; + malformed operator recipes are authoring errors during applicable preparation + with the required schemas available. Unavailable prerequisites can produce a + zero-credit gap before exhaustive recipe diagnostics. Single-step execution + uses the same resolver and fixture_unsatisfied disposition. Routed or replicated + runs currently require independent single-agent runs and report a fixture gap + before controller seeding. Engines without this contract retain their existing + unsatisfied-contract and required-phase/vacuous-pass guards; callers must report + the missing harness rather than claim passing security coverage. +references: + - /creative/canonical-formats#technical-completeness-and-file-size-units + - /creative/task-reference/build_creative + - /schemas/core/creative-operation-format-declaration.json + +# Payload-bound authorization limitation +# Current probe preparation requires a sandbox build fixture that does not need +# a per-request signed governance_context. The runner detects an advertised +# build_creative signed_context enforcement claim and reports fixture_unsatisfied +# with zero security credit; static recipes cannot pre-sign the fresh request. +# Other governed tasks do not disable this build probe. Preparing fresh governed +# probe authorizations is a separate runner integration, not a protocol decision. + +# Operator recipe authoring limits: 100,000 traversed nodes, 4 MiB of UTF-8 +# string/key bytes, and the SDK's existing JSON depth bound per recipe. Repeated +# aliases count on every visit. Exceedance is a harness configuration error, +# not a producer catalog limit or conformance verdict. Missing required +# selected-version validators are an explicit zero-credit harness gap. + +# Recipes are trusted operator-authored test inputs. Preserve their messages +# exactly; do not infer evaluator instructions from them. Key/URL checks reject +# known credential aliases but cannot prove opaque config values secret-free. +# Provision transformers with matching live output_capability_ids (3.2) or +# output_format_ids (legacy), valid config parameters, and account entitlement. +# A transformer_id plus config is a complete provisioned mode; no dummy brief +# or source manifest is required by this runner contract. + +# An explicit legacy selector recipe is also supported on newer versions that +# retain target_format_id. Bind exactly one selector. A recipe cannot override +# canonical_formats_only:true or contradict an advertised v1_format_ref pairing; +# reference agent URLs use existing canonicalization, and declared dimensions/ +# duration must agree. Incompatible route/recipe pairs are unavailable, not +# producer failures; continue to another satisfiable route. + +# Resolution certifies selected route/input preparation, not full catalog validity. +# Unknown legacy kinds remain locally unsupported/unvalidated. Keep the authored +# mandatory discovery response_schema validation to grade the entire response; +# standalone boundary steps assert their selected security probe only. + +# Operator briefs are sent as build_creative.message to the tested agent's model. +# Author creative direction without evaluator instructions or expected verdicts. +# An explicit runner test_kit replaces the declared kit as a whole; include the +# recipes in that override when supplying auth. Hosted auth defaults preserve +# explicit recipes but do not merge recipes from another declared brand kit. diff --git a/static/compliance/source/universal/storyboard-schema.yaml b/static/compliance/source/universal/storyboard-schema.yaml index 069029ea8e8..f098c3ce1dc 100644 --- a/static/compliance/source/universal/storyboard-schema.yaml +++ b/static/compliance/source/universal/storyboard-schema.yaml @@ -642,6 +642,62 @@ # See runner-output-contract.yaml > fixture_resolution_result for evidence and # `fixture_unsatisfied` grading. See governance decision DR-0010. # +# creative_build_probe: object (optional; runner preparation, not agent capability) +# context_prefix: lowercase context namespace for the atomic route/input binding +# message: brief used only by the bounded automatic generation recipe +# +# Requires the creative_build_probe_runner implementation described in +# test-kits/creative-build-probe-runner.yaml. Build steps use requires_contract: +# creative_build_probe_runner. Unsupported engines keep existing contract and +# vacuous-pass guards; discovery steps need no build-preparation contract. +# +# Operator recipes belong in the brand kit selected by prerequisites.test_kit, +# or in the runner's explicit test_kit/--test-kit override, for example: +# +# creative_build_probes: +# - match: +# - path: /capability_id +# operator: equals +# value: provisioned_image_transform +# input: +# creative_manifest: +# format_kind: image +# assets: +# image_main: +# asset_type: image +# url: https://test-assets.example/provisioned.jpg +# width: 300 +# height: 250 +# +# Match clauses use the existing fixture-resolution DSL against the complete +# supported_formats entry. Operators assert valid input for every matched route. +# Input fields: message (brief/guidance), creative_manifest (generation or +# transformation), creative_id (has_creative_library), transformer_id/config +# (supports_transformers), and a provisioned target_format_id when the selected +# request supports legacy routing. FormatID is an object, for example +# {agent_url: https://formats.example, id: sandbox_named_format}. +# For this probe contract, config is transformer-bound +# and library/inline sources are unambiguous; these are fixture authoring rules, +# not new restrictions on build_creative wire requests. Omitted recipe fields +# stay absent, including message. Never embed credentials or trust material. +# +# Automatic generation needs a unique routable build operation, generation +# support, complete understood constraints and a schema-valid minimal source +# manifest. Legacy omitted operations mean build; absent IDs remain legal but +# unaddressable through capability-ID routing. A provisioned selected-version +# target_format_id recipe can use existing legacy routing instead. Catalog order +# is not an operation/input contract. Inert metadata +# does not disable the probe. Other declared/unknown constraints need a recipe. +# +# Bind exactly one _capability_id or _target_format_id plus +# supplied _message, _creative_manifest, +# _creative_id, _transformer_id and _config; inject optional fields through +# context_inputs. Resolved evidence includes creative_format:, selected +# capability or named-format identity and exact match clauses. Missing fixtures/preconditions +# report fixture_unsatisfied with zero security credit. Invalid advertised +# catalogs produce a structured failed validation; malformed operator recipes +# are authoring errors. No global contribution/accounting rules change. +# # --- Test kit flavors --- # # Files under `static/compliance/source/test-kits/` come in two shapes. The diff --git a/tests/evaluator-allowlist-runner.test.cjs b/tests/evaluator-allowlist-runner.test.cjs new file mode 100644 index 00000000000..039bd5eebbd --- /dev/null +++ b/tests/evaluator-allowlist-runner.test.cjs @@ -0,0 +1,549 @@ +#!/usr/bin/env node +'use strict'; +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { getComplianceStoryboardById } = require('@adcp/sdk/testing'); +const { resolveDeclaredTestKit } = require(path.resolve(require.resolve('@adcp/sdk/testing'), '../storyboard/test-kit.js')); +const creativeScenariosPath = path.join(__dirname, '..', 'static', 'compliance', 'source', 'protocols', 'creative', 'scenarios'); + +// Use the complete authored fixture and the real loader/runner. In particular, +// the loader's name -> key context-output normalization and required capture +// failures must remain active; a hand-built phase cannot test applicability. +async function runEvaluatorBoundary({ + loadedStoryboard, + support = true, + discoverySupport = support, + missingCatalog, + attack, + optionalFailure = false, + preflightFailure = false, + catalog, + generation = true, + transformation = false, + transformers = false, + expectedTransformer, + expectedFormatId, + adcpVersion, + testKit, + governanceEnforcement, + capabilitiesVersion, + schemaRoot, + servedReleaseSchemaRoots, + expectedTarget = 'display-build', + expectedManifest = { format_kind: 'image', assets: {} }, + sdkApi, + cachedCapabilityIssues, +} = {}) { + const { parseStoryboard, runStoryboard, buildComplianceBundleResults, createTestClient, withExternalSchemaRoot, applyStoryboardVersionOptions } = sdkApi ?? require('@adcp/sdk/testing'); + const storyboard = loadedStoryboard ?? parseStoryboard(fs.readFileSync(path.join(creativeScenariosPath, 'evaluator_auth.yaml'), 'utf8')); + const capabilities = { + ...(capabilitiesVersion && { status: 'completed', adcp_version: capabilitiesVersion }), + adcp: { major_versions: [3], ...(capabilitiesVersion && { supported_versions: [adcpVersion.split('.').slice(0, 2).join('.'), capabilitiesVersion] }), idempotency: { supported: false }, + ...(governanceEnforcement && { governance_enforcement: governanceEnforcement }) }, + supported_protocols: ['creative', 'governance'], + creative: { + ...(support !== undefined && { supports_evaluator: support }), + supports_generation: generation, + supports_transformation: transformation, + supports_transformers: transformers, + supported_formats: catalog ?? [{ + capability_id: 'display-build', + format: { format_kind: 'image', params: {}, technical_requirements_complete: true }, + }], + }, + governance: { creative_features: [{ feature_id: 'visual-quality', type: 'quantitative' }] }, + experimental_features: ['creative.evaluator'], + }; + const previews = capabilities.creative.supported_formats.filter(route => route.operations?.includes('preview')); + if (previews.length) capabilities.creative.preview = { routes: previews.map(route => ({ capability_id: route.capability_id, rendering_origin: 'agent_approximation' })) }; + if (support === 'absent') delete capabilities.creative.supports_evaluator; + const discovery = structuredClone(capabilities); + if (discoverySupport === 'absent') delete discovery.creative.supports_evaluator; + else discovery.creative.supports_evaluator = discoverySupport; + if (missingCatalog === 'build') { capabilities.creative.supported_formats = []; discovery.creative.supported_formats = []; } + if (missingCatalog === 'feature') discovery.governance.creative_features = []; + // Unsupported agents need neither a build tool nor a controller to skip this + // optional feature; evaluate applicability before those requirements. + const tools = support === true || preflightFailure + ? ['get_adcp_capabilities', 'build_creative', 'comply_test_controller'] + : ['get_adcp_capabilities']; + const requests = []; + const trafficQueries = []; + let recordedCalls = []; + let unavailable = false; + let capabilityAttempts = 0; + const successData = context => ({ + creatives: [{ variants: [{ + build_variant_id: 'variant-boundary', + creative_manifest: { format_kind: 'image', assets: {} }, + rank: 1, + recommended: true, + eval: { features: [{ feature_id: 'visual-quality', value: 0.9 }] }, + }] }], + context, + }); + let runOptions = { + _profile: preflightFailure ? undefined : { tools, raw_capabilities: capabilities, + ...(cachedCapabilityIssues && { capabilities_schema_issues: cachedCapabilityIssues }) }, + _controllerCapabilities: { detected: true, scenarios: ['query_upstream_traffic', 'force_upstream_unavailable'] }, + agentTools: preflightFailure ? undefined : tools, + skip_controller_seeding: true, + test_kit: testKit, + adcpVersion, + schemaRoot, + servedReleaseSchemaRoots, + _client: { + resetContext() {}, + async getAgentInfo() { + return { name: 'Creative boundary fixture', tools: tools.map(name => ({ name })) }; + }, + async getAdcpCapabilities(request) { + capabilityAttempts++; + if (preflightFailure && capabilityAttempts === 1) { + throw new Error('Transient preflight capability failure'); + } + requests.push({ task: 'get_adcp_capabilities', request }); + return { success: true, data: { ...discovery, context: request.context } }; + }, + async executeTask(task, request) { + if (task === 'comply_test_controller') { + if (request.scenario === 'force_upstream_unavailable') { + unavailable = true; + return { success: true, data: { success: true, context: request.context } }; + } + assert.equal(request.scenario, 'query_upstream_traffic'); + trafficQueries.push(request); + return { success: true, data: { success: true, attestation_mode: 'raw', recorded_calls: recordedCalls } }; + } + assert.equal(task, 'build_creative'); + requests.push({ task, request }); + const form = request.evaluator.feature_agent ? 'feature_agent' : 'direct'; + if (form === 'feature_agent') { + assert.equal(request.evaluator.agent_url, 'https://quality-evaluator.example/adcp'); + assert.equal(request.evaluator.evaluator_id, undefined, 'nested probe must not depend on an unprovisioned preset'); + } + const url = request.evaluator.feature_agent?.agent_url ?? request.evaluator.agent_url; + const offList = url === 'https://unaccepted-evaluator.example/adcp'; + recordedCalls = []; + if (offList) { + assert.equal(request.target_capability_id, expectedTarget === null ? undefined : expectedTarget); + assert.deepEqual(request.target_format_id, expectedFormatId); + assert.deepEqual(request.creative_manifest, expectedManifest === null ? undefined : expectedManifest); + assert.equal(request.transformer_id, expectedTransformer); + if (testKit) assert.equal(request.message, undefined, 'operator source recipe must not acquire an unprovisioned brief'); + for (const key of ['rank_by', 'max_variants', 'keep_mode', 'variant_axis']) { + assert.equal(request.evaluator[key] ?? request[key], undefined, `${key} is not a required probe precondition`); + } + assert.equal(request.evaluator.feature_agent?.feature_id, undefined); + if (attack?.form === form && attack.endpoint) { + recordedCalls.push({ timestamp: new Date().toISOString(), endpoint: attack.endpoint }); + } + if (attack?.form === form && attack.accept) { + return { success: true, data: successData(request.context) }; + } + return { success: false, data: { + errors: [{ code: attack?.form === form && attack.wrongCode ? 'INVALID_REQUEST' : 'EVALUATOR_AGENT_NOT_ACCEPTED', message: 'Off-list evaluator' }], + context: request.context, + } }; + } + if (request.evaluator.ext?.api_key) { + return { success: true, data: { + errors: [{ code: 'CREDENTIAL_IN_ARGS', message: 'Credentials belong on transport' }], + context: request.context, + } }; + } + if (optionalFailure) return { success: false, error: 'Optional accepted-evaluator probe failed' }; + const data = successData(request.context); + if (unavailable) { + data.errors = [{ code: 'INTERNAL_ERROR', message: 'Evaluator unavailable; seller-default ranking used' }]; + } else { + recordedCalls = [{ + timestamp: new Date().toISOString(), + endpoint: 'POST https://quality-evaluator.example/adcp', + request_body: { creative_manifest: { assets: {} } }, + }]; + } + return { success: true, data }; + }, + }, + }; + // Reproduce the runner's option resolution before creating a borrowed client: + // declared kit auth must match, otherwise the SDK correctly replaces it. + runOptions = resolveDeclaredTestKit(storyboard, applyStoryboardVersionOptions(storyboard, runOptions)); + if (adcpVersion) { + // SDK14 ships prerelease3.2 bundles. Explicit stable roots must be active + // when constructing the public client, just as the runner activates them + // before it constructs an owned client. Do not waive the version guard. + const create = () => createTestClient('https://agent.example/mcp', 'mcp', runOptions); + const client = schemaRoot ? withExternalSchemaRoot(adcpVersion, schemaRoot, create) : create(); + runOptions._client = Object.assign(client, runOptions._client); + runOptions._client.buildCreativeLegacy = input => runOptions._client.executeTask('build_creative', input); + } + const callerProfile = runOptions._profile; + const profileBefore = callerProfile && structuredClone(callerProfile); + const result = await runStoryboard('https://agent.example/mcp', storyboard, runOptions); + if (callerProfile) assert.deepEqual(callerProfile, profileBefore, 'reused capability facts and discovery diagnostics remain caller-owned'); + const [bundle] = buildComplianceBundleResults([{ ref: { kind: 'protocol', id: 'creative', path: '/fixture' }, storyboards: [storyboard] }], [result]); + return { result, bundle, requests, trafficQueries, capabilityAttempts, steps: result.phases.flatMap(phase => phase.steps) }; +} + +test('supporting evaluator passes the complete authored boundary fixture with both rejection paths executed', async () => { + const { result, steps, trafficQueries } = await runEvaluatorBoundary(); + assert.equal(result.overall_passed, true, JSON.stringify(result)); + assert.equal(result.failed_count, 0); + assert.ok(steps.every(step => step.passed && !step.skipped), JSON.stringify(steps)); + for (const id of ['reject_unaccepted_agent_url', 'reject_unaccepted_feature_agent']) { + const step = steps.find(step => step.step_id === id); + assert.ok(step && step.passed && !step.skipped); + for (const check of ['response_schema', 'error_code', 'upstream_traffic']) { + assert.ok(step.validations.some(validation => validation.check === check && validation.passed && !validation.not_applicable), JSON.stringify(step)); + } + } + assert.equal(trafficQueries.length, 4, 'both rejections, credential probe and accepted invocation attest traffic'); +}); + +for (const form of ['direct', 'feature_agent']) { + for (const [label, attack, failingCheck] of [ + ['POST before correct rejection', { endpoint: 'POST https://unaccepted-evaluator.example/adcp' }, 'upstream_traffic'], + ['GET discovery before correct rejection', { endpoint: 'GET https://unaccepted-evaluator.example/.well-known/oauth-protected-resource' }, 'upstream_traffic'], + ['wrong acceptance without traffic', { accept: true }, 'error_code'], + ['wrong rejection code', { wrongCode: true }, 'error_code'], + ]) { + test(`supporting evaluator fails aggregate for ${form} ${label}`, async () => { + const { result, steps } = await runEvaluatorBoundary({ attack: { form, ...attack } }); + assert.equal(result.overall_passed, false, JSON.stringify(result)); + assert.equal(result.failed_count, 1, 'one violated rejection path must gate on its own'); + const id = form === 'direct' ? 'reject_unaccepted_agent_url' : 'reject_unaccepted_feature_agent'; + const step = steps.find(step => step.step_id === id); + assert.ok(step && !step.passed && !step.skipped, JSON.stringify(step)); + assert.ok(step.validations.some(validation => validation.check === failingCheck && !validation.passed && !validation.not_applicable), JSON.stringify(step)); + if (form === 'feature_agent') { + assert.ok(steps.some(step => step.step_id === 'reject_unaccepted_agent_url' && step.passed && !step.skipped), 'the passing direct probe must not rescue the nested failure'); + } else { + assert.ok(steps.some(step => step.step_id === 'reject_unaccepted_feature_agent' && step.skip?.reason === 'prerequisite_failed'), 'the runner preserves its hard prerequisite cascade after a required failure'); + } + }); + } +} + +for (const support of [false, 'absent']) { + test(`evaluator support ${support} skips before build or controller requirements`, async () => { + const { result, requests, trafficQueries, steps } = await runEvaluatorBoundary({ support }); + assert.equal(result.overall_passed, true, JSON.stringify(result)); + assert.equal(result.failed_count, 0); + assert.equal(result.passed_count, 0); + assert.deepEqual(requests, []); + assert.deepEqual(trafficQueries, []); + assert.equal(steps.length, 1); + assert.equal(steps[0].skip_reason, 'capability_unsupported'); + assert.equal(steps[0].skip.reason, 'not_applicable'); + }); +} + +// Exercise actual SDK profiling, including its successful tool discovery plus +// transient get-capabilities failure. An explicit storyboard run must not infer +// evaluator support from an unknown profile, whatever the next response says. +for (const discoverySupport of [false, 'absent', true]) { + test(`unknown preflight receives no evaluator coverage before later ${discoverySupport} discovery`, async () => { + const { result, requests, trafficQueries, capabilityAttempts, steps } = await runEvaluatorBoundary({ + preflightFailure: true, + discoverySupport, + }); + assert.equal(capabilityAttempts, 1, 'normal profiling attempted capability discovery'); + assert.equal(result.overall_passed, true, JSON.stringify(result)); + assert.equal(result.failed_count, 0); + assert.equal(result.passed_count, 0, 'indeterminate applicability is not passing security coverage'); + assert.deepEqual(requests, []); + assert.deepEqual(trafficQueries, []); + assert.equal(steps.length, 1); + assert.equal(steps[0].skip.reason, 'not_applicable'); + assert.match(steps[0].skip.detail, /raw capabilities unavailable/); + }); +} + +test('missing build catalog is a fixture gap with no false failure or security credit', async () => { + const { result, requests, steps } = await runEvaluatorBoundary({ missingCatalog: 'build' }); + assert.equal(result.overall_passed, true); + assert.equal(result.failed_count, 0); + assert.equal(result.passed_count, 0); + assert.match(result.coverage_gaps[0].detail, /creative_format:build/); + assert.deepEqual(requests, []); + assert.deepEqual(steps, []); +}); + +test('feature vocabulary is not a prerequisite for either required rejection probe', async () => { + const { result, steps } = await runEvaluatorBoundary({ missingCatalog: 'feature' }); + assert.equal(result.overall_passed, true, JSON.stringify(result)); + for (const id of ['reject_unaccepted_agent_url', 'reject_unaccepted_feature_agent']) { + const step = steps.find(step => step.step_id === id); + assert.ok(step?.passed && !step.skipped, JSON.stringify(steps)); + } +}); + +test('supporting evaluator profile cannot skip required rejection by returning false in later discovery', async () => { + for (const form of ['direct', 'feature_agent']) { + const { result, steps } = await runEvaluatorBoundary({ discoverySupport: false, attack: { form, accept: true } }); + assert.equal(result.overall_passed, false, JSON.stringify(result)); + assert.ok(steps.some(step => step.step_id === 'get_capabilities' && step.passed && !step.skipped), 'discovery alone passes the runner vacuous-pass guard'); + const id = form === 'direct' ? 'reject_unaccepted_agent_url' : 'reject_unaccepted_feature_agent'; + assert.ok(steps.some(step => step.step_id === id && !step.passed && !step.skipped), 'authoritative capability claim must keep rejection applicable'); + } +}); + +test('unrelated authored optional evaluator failure remains non-gating', async () => { + const { result, steps } = await runEvaluatorBoundary({ optionalFailure: true }); + assert.equal(result.overall_passed, true, JSON.stringify(result)); + assert.equal(result.failed_count, 0); + assert.ok(steps.some(step => step.step_id === 'build_with_accepted_evaluator' && !step.passed && !step.skipped)); +}); + + +const completeImageRoute = id => ({ capability_id: id, operations: ['build'], + format: { format_kind: 'image', params: {}, technical_requirements_complete: true } }); +const previewFirstCatalog = () => [ + { capability_id: 'preview-only', operations: ['preview', 'validate'], format: { format_kind: 'image', params: {} } }, + completeImageRoute('actual-build'), +]; +for (const form of ['direct', 'feature_agent']) { + for (const attack of [undefined, { form, accept: true }, { form, wrongCode: true }, + { form, endpoint: 'GET https://unaccepted-evaluator.example/.well-known/oauth-protected-resource' }]) { + test(`valid non-build-first catalog: ${form} ${JSON.stringify(attack ?? 'conformant')}`, async () => { + const { result, requests } = await runEvaluatorBoundary({ catalog: previewFirstCatalog(), expectedTarget: 'actual-build', attack }); + assert.equal(result.overall_passed, !attack, JSON.stringify(result)); + assert.ok(requests.some(x => x.task === 'build_creative')); + assert.equal(result.fixture_resolutions[0].seller_ids.capability_id, 'actual-build'); + }); + } +} +const sourceManifest = { format_kind: 'image', assets: { image_main: { asset_type: 'image', + url: 'https://test-assets.example/source.jpg', width: 300, height: 250 } } }; +const provisionedBuild = { creative_build_probes: [{ + match: [{ path: '/capability_id', operator: 'equals', value: 'transform-build' }], + input: { creative_manifest: sourceManifest }, +}] }; +for (const form of ['direct', 'feature_agent']) { + for (const malicious of [false, true]) { + test(`matched source recipe skips unsatisfiable first build: ${form} malicious=${malicious}`, async () => { + const { result } = await runEvaluatorBoundary({ generation: false, transformation: true, + catalog: [completeImageRoute('requires-other-input'), completeImageRoute('transform-build')], + testKit: provisionedBuild, expectedTarget: 'transform-build', expectedManifest: sourceManifest, + attack: malicious ? { form, endpoint: 'POST https://unaccepted-evaluator.example/adcp' } : undefined }); + assert.equal(result.overall_passed, !malicious, JSON.stringify(result)); + }); + } +} +test('generation flag without complete input constraints gets no security coverage', async () => { + const route = completeImageRoute('requires-seed'); delete route.format.technical_requirements_complete; + const { result, requests } = await runEvaluatorBoundary({ catalog: [route] }); + assert.equal(result.failed_count, 0); + assert.equal(result.passed_count, 0); + assert.equal(result.coverage_gaps[0].reason, 'fixture_unsatisfied'); + assert.deepEqual(requests, []); +}); +test('legacy omitted operations still resolves a targetable build route', async () => { + const route = completeImageRoute('legacy-build'); delete route.operations; + const { result } = await runEvaluatorBoundary({ catalog: [route], expectedTarget: 'legacy-build' }); + assert.equal(result.overall_passed, true, JSON.stringify(result)); +}); + +test('fresh payload-bound build authorization is an explicit fixture gap, not a guessed error precedence', async () => { + const { result, requests } = await runEvaluatorBoundary({ governanceEnforcement: { + tasks: [{ task: 'build_creative', modes: ['signed_context'] }], + } }); + assert.equal(result.failed_count, 0); + assert.equal(result.passed_count, 0); + assert.match(result.coverage_gaps[0].detail, /payload-bound build governance authorization/); + assert.deepEqual(requests, []); +}); + +test('real bundle grading withholds passing status for missing input and fails applicable attacks', async () => { + const good = await runEvaluatorBoundary(); + assert.equal(good.result.overall_passed, true); + assert.notEqual(good.bundle.status, 'failing'); // Existing optional advisory N/A evidence caps full-bundle coverage separately. + assert.equal((await runEvaluatorBoundary({ missingCatalog: 'build' })).bundle.status, 'partial'); + const unsupported = await runEvaluatorBoundary({ support: false }); + assert.notEqual(unsupported.bundle.status, 'failing'); + assert.equal(unsupported.result.passed_count, 0); + for (const form of ['direct', 'feature_agent']) { + assert.equal((await runEvaluatorBoundary({ attack: { form, endpoint: 'POST https://unaccepted-evaluator.example/adcp' } })).bundle.status, 'failing'); + assert.equal((await runEvaluatorBoundary({ attack: { form, accept: true } })).bundle.status, 'failing'); + } +}); + +for (const mode of ['transformer', 'legacy']) { + for (const attack of [undefined, + { form: 'direct', endpoint: 'GET https://unaccepted-evaluator.example/probe' }, + { form: 'feature_agent', endpoint: 'POST https://unaccepted-evaluator.example/adcp' }, + { form: 'direct', accept: true }, { form: 'feature_agent', accept: true }]) { + test(`authored fixture ${mode} provisioned input: ${attack ? JSON.stringify(attack) : 'good'}`, async () => { + const legacy = mode === 'legacy'; + const legacyFormat = { agent_url: 'https://formats.example', id: 'sandbox_named_format' }; + const manifest = { format_kind: 'image', assets: { image_main: { asset_type: 'image', url: 'https://test-assets.example/image.jpg', width: 300, height: 250 } } }; + const input = legacy ? { creative_manifest: manifest, target_format_id: legacyFormat } + : { transformer_id: 'sandbox-transformer', config: { headline: 'Winter offer' } }; + const entry = { ...(legacy ? {} : { capability_id: 'display-build', operations: ['build'] }), format: { format_kind: 'image', params: {} } }; + const opts = { attack, generation: false, transformation: legacy, transformers: !legacy, + expectedTransformer: legacy ? undefined : input.transformer_id, + expectedManifest: legacy ? manifest : null, + expectedTarget: legacy ? null : 'display-build', expectedFormatId: legacy ? legacyFormat : undefined, + adcpVersion: legacy ? '3.1.29' : undefined, + catalog: legacy ? [{ format: { format_kind: 'video_vast', params: {} } }, entry] : [entry], + testKit: { creative_build_probes: [{ match: [{ path: '/format/format_kind', operator: 'equals', value: 'image' }], input }] } }; + const { result } = await runEvaluatorBoundary(opts); + assert.equal(result.overall_passed, !attack); + assert.equal(result.failed_count, attack ? 1 : 0); + assert.ok(result.passed_count > 0); + assert.equal(result.fixture_resolutions[0].status, 'resolved'); + if (!attack) for (const id of ['payload_credentials_advisory', 'accepted_evaluator_invocation', 'accepted_evaluator_unavailable_degrades']) { + const phase = result.phases.find(p => p.phase_id === id); + assert.ok(phase.steps.some(step => !step.skipped), `${mode}: advisory phase ${id} must execute with either prepared selector`); + } + }); + } +} + +test('full authored discovery still grades unrelated malformed metadata on an unsupported 3.1 kind', async () => { + const target = { agent_url: 'https://formats.example', id: 'sandbox_named_format' }; + const { result } = await runEvaluatorBoundary({ adcpVersion: '3.1.29', generation: false, transformation: true, + expectedTarget: null, expectedFormatId: target, expectedManifest: { format_kind: 'image', assets: {} }, + catalog: [{ format: { format_kind: 'future_canonical', params: {}, seller_preference: 123 } }, { format: { format_kind: 'image', params: {} } }], + testKit: { creative_build_probes: [{ match: [{ path: '/format/format_kind', operator: 'equals', value: 'image' }], input: { creative_manifest: { format_kind: 'image', assets: {} }, target_format_id: target } }] } + }); + assert.equal(result.overall_passed, false); + const discovery = result.phases.flatMap(p => p.steps).find(step => step.step_id === 'get_capabilities'); + assert.equal(discovery.passed, false); + assert.ok(discovery.validations.some(v => v.check === 'response_schema' && v.passed === false)); +}); + + +// Capability catalogs follow the validated served release; build inputs and +// dispatch remain on the selected run pin, including legacy named formats. +for (const attack of [undefined, + { form: 'direct', endpoint: 'GET https://unaccepted-evaluator.example/discovery' }, + { form: 'feature_agent', endpoint: 'POST https://unaccepted-evaluator.example/adcp' }, + { form: 'direct', accept: true }, { form: 'feature_agent', accept: true }]) { + test(`served 3.2 non-build-first catalog with pinned 3.1 input: ${JSON.stringify(attack ?? 'good')}`, async () => { + const target = { agent_url: 'https://formats.example', id: 'sandbox_audio' }; + const manifest = { format_id: target, assets: {} }; + const { result, bundle, requests, steps } = await runEvaluatorBoundary({ + attack, adcpVersion: '3.1.29', capabilitiesVersion: '3.2', + schemaRoot: path.resolve(__dirname, '../dist/schemas/3.1.29'), + servedReleaseSchemaRoots: { '3.2.5': path.resolve(__dirname, '../dist/schemas/3.2.5') }, + generation: false, transformation: true, + catalog: [ + { capability_id: 'validate-only', operations: ['validate'], format: { format_kind: 'audio_vast', params: {} } }, + { capability_id: 'audio-build', operations: ['build'], format: { format_kind: 'audio_vast', params: {}, v1_format_ref: [target] } }, + ], + expectedTarget: null, expectedFormatId: target, expectedManifest: manifest, + testKit: { creative_build_probes: [{ + match: [{ path: '/capability_id', operator: 'equals', value: 'audio-build' }], + input: { creative_manifest: manifest, target_format_id: target }, + }] }, + }); + assert.equal(result.overall_passed, !attack, JSON.stringify(result)); + assert.equal(result.fixture_resolutions[0].status, 'resolved'); + assert.ok(requests.some(({ task, request }) => task === 'build_creative' && request.target_format_id?.id === target.id)); + assert.ok(steps.find(step => step.step_id === 'get_capabilities').validations.some(v => v.check === 'response_schema' && v.passed)); + if (attack) assert.equal(bundle.status, 'failing'); + else for (const id of ['reject_unaccepted_agent_url', 'reject_unaccepted_feature_agent']) { + const step = steps.find(step => step.step_id === id); + assert.ok(step.passed && !step.skipped); + assert.ok(step.validations.some(v => v.check === 'upstream_traffic' && v.passed && !v.not_applicable)); + } + }); +} + +// Both released catalog item schemas explicitly allow entry extensions. A +// provisioned recipe can supply valid input when automatic preparation cannot +// interpret an extension; do not turn that valid catalog into a producer fault. +test('served 3.2 catalog retains legal entry extensions with provisioned input', async () => { + const { result, steps } = await runEvaluatorBoundary({ + adcpVersion: '3.2.5', capabilitiesVersion: '3.2', generation: false, transformation: true, + schemaRoot: path.resolve(__dirname, '../dist/schemas/3.2.5'), + catalog: [{ ...completeImageRoute('transform-build'), seller_extension: { workflow: 'sandbox' } }], + testKit: provisionedBuild, expectedTarget: 'transform-build', expectedManifest: sourceManifest, + }); + assert.equal(result.overall_passed, true, JSON.stringify(result)); + assert.equal(result.fixture_resolutions[0].status, 'resolved'); + assert.ok(steps.find(step => step.step_id === 'get_capabilities').validations.some(v => v.check === 'response_schema' && v.passed)); + for (const id of ['reject_unaccepted_agent_url', 'reject_unaccepted_feature_agent']) { + assert.ok(steps.some(step => step.step_id === id && step.passed && !step.skipped)); + } +}); + +// Canonical CI shards run this suite in a fresh checkout independently of the +// build job. Build the mutable candidate bundle before exercising its loader; +// immutable release bundles remain untouched. +test.before(() => { + require('node:child_process').execFileSync(process.execPath, [path.resolve(__dirname, '../scripts/build-compliance.cjs')], { + cwd: path.resolve(__dirname, '..'), stdio: 'inherit', maxBuffer: 16 * 1024 * 1024, + }); +}); + +for (const [label, options] of [ + ['supporting-good', {}], + ['direct-outbound', { attack: { form: 'direct', endpoint: 'GET https://unaccepted-evaluator.example/probe' } }], + ['nested-acceptance', { attack: { form: 'feature_agent', accept: true } }], + ['unsupported', { support: false }], + ['unknown', { support: 'absent' }], +]) { + test(`actual maintenance bundle loads and grades ${label} on the locked SDK`, async () => { + const loadedStoryboard = getComplianceStoryboardById('creative/evaluator_auth', { + complianceDir: path.resolve(__dirname, '../dist/compliance/latest'), + }); + assert.ok(loadedStoryboard?.creative_build_probe, 'built bundle must retain the authored preparation contract'); + const { result, bundle, requests, steps } = await runEvaluatorBoundary({ + ...options, loadedStoryboard, adcpVersion: '3.2.5', capabilitiesVersion: '3.2', + schemaRoot: path.resolve(__dirname, '../dist/schemas/3.2.5'), + }); + if (options.attack) { + assert.equal(bundle.status, 'failing'); + const id = options.attack.form === 'direct' ? 'reject_unaccepted_agent_url' : 'reject_unaccepted_feature_agent'; + const boundary = steps.find(step => step.step_id === id); + assert.ok(boundary && !boundary.skipped && !boundary.passed, 'the actual applicable boundary must fail'); + assert.ok(boundary.validations.some(v => !v.passed && v.check === (options.attack.accept ? 'error_code' : 'upstream_traffic'))); + } + else if (options.support === false || options.support === 'absent') { + assert.equal(result.failed_count, 0); assert.equal(result.passed_count, 0); + assert.ok(!requests.some(request => request.task === 'build_creative')); + } else assert.equal(result.overall_passed, true, JSON.stringify(result)); + }); +} + +for (const [label, attack] of [ + ['supporting-good', undefined], + ['direct-outbound', { form: 'direct', endpoint: 'POST https://unaccepted-evaluator.example/probe' }], + ['feature-outbound', { form: 'feature_agent', endpoint: 'GET https://unaccepted-evaluator.example/probe' }], + ['direct-wrong-acceptance', { form: 'direct', accept: true }], + ['feature-wrong-acceptance', { form: 'feature_agent', accept: true }], +]) { + for (const mode of ['cjs', 'esm']) { + test(`actual ${mode} opted bundle uses builtin RC JSON authority despite stale generated Zod: ${label}`, async () => { + const sdkApi = mode === 'esm' ? await import('@adcp/sdk/testing') : require('@adcp/sdk/testing'); + const loadedStoryboard = sdkApi.getComplianceStoryboardById('creative/evaluator_auth', { + complianceDir: path.resolve(__dirname, '../dist/compliance/latest'), + }); + const { result, bundle, steps } = await runEvaluatorBoundary({ + sdkApi, loadedStoryboard, attack, adcpVersion: '3.2.5', capabilitiesVersion: '3.2-rc.7', + schemaRoot: path.resolve(__dirname, '../dist/schemas/3.2.5'), + governanceEnforcement: { tasks: [{ task: 'create_media_buy', modes: ['signed_context', 'online_execution_check'] }] }, + cachedCapabilityIssues: [{ pointer: '/adcp/governance_enforcement', message: 'Prior discovery schema authority was not recorded' }], + }); + const discovery = steps.find(step => step.step_id === 'get_capabilities'); + const schema = discovery.validations.find(validation => validation.check === 'response_schema'); + assert.equal(schema.passed, true); + assert.equal(schema.strict.valid, true); + assert.equal(schema.strict.lenient_valid, false); + assert.match(schema.warning, /JSON schema.*generated Zod/); + assert.ok(result.notices.some(notice => notice.code === 'capabilities_response_schema_invalid' + && notice.message.includes('Cached capability-discovery diagnostic (not this run\'s response_schema verdict)'))); + if (attack) { + assert.equal(bundle.status, 'failing'); + const id = attack.form === 'direct' ? 'reject_unaccepted_agent_url' : 'reject_unaccepted_feature_agent'; + assert.ok(steps.some(step => step.step_id === id && !step.passed && !step.skipped)); + } else assert.equal(result.overall_passed, true, JSON.stringify(result)); + }); + } +} diff --git a/tests/evaluator-build-probe-authority.test.cjs b/tests/evaluator-build-probe-authority.test.cjs new file mode 100644 index 00000000000..6e1f02872c4 --- /dev/null +++ b/tests/evaluator-build-probe-authority.test.cjs @@ -0,0 +1,861 @@ +// Maintenance integration regressions adapted from merged adcp-client#3332. +// Run against this branch's actual, unchanged SDK dependency. +const { test, before, after } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { createTestClient, parseStoryboard, runStoryboard, runStoryboardStep, withExternalSchemaRoot, buildComplianceBundleResults } = require('@adcp/sdk/testing'); + +const schemaData = path.join(__dirname, '../dist/schemas'); +const named = { agent_url: 'https://formats.example', id: 'sandbox_audio' }; +let temporaryRoot; +let hostedRoot; +before(() => { + temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'creative-probe-authority-')); + hostedRoot = path.join(temporaryRoot, 'hosted-3.2'); + fs.cpSync(path.join(schemaData, '3.2.5'), hostedRoot, { recursive: true }); + // A catalog root must never become the source-manifest authority. Poison + // its manifest validator while retaining a valid pinned 3.1 manifest. + const file = path.join(hostedRoot, 'core/creative-manifest.json'); + const schema = JSON.parse(fs.readFileSync(file, 'utf8')); + schema.not = {}; + fs.writeFileSync(file, JSON.stringify(schema)); +}); +after(() => fs.rmSync(temporaryRoot, { recursive: true, force: true })); + +function storyboard() { + return parseStoryboard(` +id: evaluator_served_catalog +version: "1.0.0" +title: Served evaluator catalog +category: creative +summary: Served catalog with pinned build inputs +creative_build_probe: + context_prefix: build + message: Build a sandbox creative +phases: +${['direct', 'nested'] + .map( + form => ` + - id: ${form} + title: ${form} + steps: + - id: reject_${form} + title: Reject ${form} + task: build_creative + requires_contract: creative_build_probe_runner + stateful: true + expect_error: true + sample_request: + evaluator: + agent_url: "https://${form === 'direct' ? 'off-list' : 'accepted'}.example/adcp" +${form === 'nested' ? ' feature_agent:\n agent_url: "https://off-list.example/adcp"' : ''} + context_inputs: + - key: build_capability_id + inject_at: target_capability_id + - key: build_target_format_id + inject_at: target_format_id + - key: build_creative_manifest + inject_at: creative_manifest + validations: + - check: error_code + value: EVALUATOR_AGENT_NOT_ACCEPTED + - check: upstream_traffic + min_count: 0 + endpoint_pattern: "* https://off-list.example/*" +` + ) + .join('')}`); +} + +async function run({ + standalone = false, + form = 'direct', + attack, + echo = '3.2', + supported = ['3.1', '3.2'], + pin = '3.1.29', + entries, + target = named, + manifest, + generation = false, + recipes = true, + servedRoot = hostedRoot, +} = {}) { + const calls = []; + let recorded = []; + const tools = ['get_adcp_capabilities', 'build_creative', 'comply_test_controller']; + const creative = { + supports_evaluator: true, + supports_generation: generation, + supports_transformation: true, + supported_formats: entries ?? [ + { + capability_id: 'audio-build', + operations: ['build'], + format: { format_kind: 'audio_vast', params: {}, v1_format_ref: [target] }, + }, + ], + }; + const inputManifest = manifest ?? { format_id: named, assets: {} }; + const caps = { + ...(echo !== null && { adcp_version: echo }), + adcp: { major_versions: [3], supported_versions: supported }, + creative, + }; + const opts = { + adcpVersion: pin, + ...(pin.includes('-') ? {} : { schemaRoot: path.join(schemaData, pin.startsWith('3.1') ? '3.1.29' : '3.2.5') }), + servedReleaseSchemaRoots: { '3.2.5': servedRoot }, + _profile: { tools, raw_capabilities: caps }, + agentTools: tools, + _controllerCapabilities: { detected: true, scenarios: ['query_upstream_traffic'] }, + test_kit: recipes + ? { + creative_build_probes: [ + { + match: [ + { + path: '/format/format_kind', + operator: 'equals', + value: entries?.[0]?.format.format_kind ?? 'audio_vast', + }, + ], + input: { target_format_id: target, creative_manifest: inputManifest }, + }, + ], + } + : undefined, + }; + const create = () => createTestClient('https://agent.example/mcp', 'mcp', opts); + const client = opts.schemaRoot ? withExternalSchemaRoot(pin, opts.schemaRoot, create) : create(); + client.resetContext = () => {}; + client.executeTask = async (task, request) => { + if (task === 'comply_test_controller') + return { success: true, data: { success: true, attestation_mode: 'raw', recorded_calls: recorded } }; + assert.equal(task, 'build_creative'); + assert.equal(request.target_capability_id, undefined, 'legacy request must retain its provisioned selector'); + assert.deepEqual(request.target_format_id, target); + assert.deepEqual(request.creative_manifest, inputManifest, 'served catalog must not rewrite pinned source input'); + calls.push(request); + const currentForm = request.evaluator.feature_agent ? 'nested' : 'direct'; + recorded = + currentForm === form && attack === 'outbound' + ? [{ timestamp: new Date().toISOString(), endpoint: 'GET https://off-list.example/probe' }] + : []; + if (currentForm === form && attack === 'accept') return { success: true, data: { creatives: [] } }; + return { success: false, data: { errors: [{ code: 'EVALUATOR_AGENT_NOT_ACCEPTED', message: 'Rejected' }] } }; + }; + client.buildCreativeLegacy = request => client.executeTask('build_creative', request); + opts._client = client; + const result = standalone + ? await runStoryboardStep('https://agent.example/mcp', storyboard(), `reject_${form}`, opts) + : await runStoryboard('https://agent.example/mcp', storyboard(), opts); + return { result, calls, caps }; +} + +for (const standalone of [false, true]) { + for (const legacyRatio of [undefined, 3]) { + for (const form of ['direct', 'nested']) { + for (const [attack, expectedPassed] of [ + [undefined, true], + ['outbound', false], + ['accept', false], + ]) { + test(`provisioned 2x target respects old unspecified parameters: standalone=${standalone}, oldRatio=${legacyRatio}, ${form}, ${attack ?? 'good'}`, async () => { + const target = { + agent_url: 'https://formats.example', + id: 'display_image', + width: 300, + height: 250, + pixel_ratio: 2, + }; + const legacyRef = { + agent_url: target.agent_url, + id: target.id, + width: 300, + height: 250, + ...(legacyRatio !== undefined && { pixel_ratio: legacyRatio }), + }; + const { result, calls } = await run({ + standalone, + form, + attack, + pin: '3.2.5', + echo: '3.1', + target, + manifest: { + format_kind: 'image', + assets: { + image_main: { + asset_type: 'image', + url: 'https://test-assets.example/source-2x.jpg', + width: 600, + height: 500, + pixel_ratio: 2, + }, + }, + }, + entries: [ + { + capability_id: 'seller.route:density', + format: { format_kind: 'image', params: {}, v1_format_ref: [legacyRef] }, + }, + ], + }); + assert.equal(standalone ? result.passed : result.overall_passed, expectedPassed, JSON.stringify(result)); + assert.ok(calls.length > 0, 'the provisioned 2x boundary must execute'); + if (!standalone) assert.equal(result.fixture_resolutions[0].status, 'resolved'); + if (attack) { + const step = standalone + ? result + : result.phases.flatMap(phase => phase.steps).find(step => step.step_id === `reject_${form}`); + assert.ok( + step.validations.some( + validation => + validation.check === (attack === 'outbound' ? 'upstream_traffic' : 'error_code') && + validation.passed === false + ) + ); + } + }); + } + } + } + for (const form of ['direct', 'nested']) { + for (const attack of [undefined, 'outbound', 'accept']) { + test(`served 3.2 audio catalog with pinned 3.1 input: standalone=${standalone}, ${form}, ${attack ?? 'good'}`, async () => { + const { result, calls } = await run({ standalone, form, attack }); + assert.equal(standalone ? result.passed : result.overall_passed, !attack, JSON.stringify(result)); + assert.ok(calls.length > 0, 'the boundary must execute rather than award unavailable coverage'); + if (attack) { + const step = standalone + ? result + : result.phases.flatMap(phase => phase.steps).find(step => step.step_id === `reject_${form}`); + assert.ok( + step.validations.some( + validation => + validation.check === (attack === 'outbound' ? 'upstream_traffic' : 'error_code') && !validation.passed + ) + ); + } + }); + } + } + for (const [label, echo, supported] of [ + ['missing echo', null, ['3.1', '3.2']], + ['patch precision is not a valid echo', '3.2.5', ['3.2.5']], + ['different major', '4.0', ['4.0']], + ['unlisted release', '3.2', ['3.1']], + ['unavailable release root', '3.9', ['3.9']], + ]) { + test(`catalog echo guard preserves pinned authority: standalone=${standalone}, ${label}`, async () => { + const { result, calls } = await run({ standalone, echo, supported }); + assert.deepEqual(calls, []); + if (standalone) assert.equal(result.skip_reason, 'fixture_unsatisfied'); + else { + assert.equal(result.failed_count, 0); + assert.equal(result.passed_count, 0); + assert.match(result.coverage_gaps[0].detail, /3\.1\.29.*audio_vast/); + } + }); + } + test(`served density is enforced even when request pin treats pixel_ratio as opaque: ${standalone}`, async () => { + const id = { ...named, width: 300, height: 250, pixel_ratio: 2 }; + const { result, calls } = await run({ standalone, target: id }); + assert.deepEqual(calls, []); + assert.equal(standalone ? result.skip_reason : result.coverage_gaps[0].reason, 'fixture_unsatisfied'); + }); + test(`old served identifiers and operations retain old meaning under a newer request: ${standalone}`, async () => { + const { result, calls } = await run({ + standalone, + pin: '3.2.5', + echo: '3.1', + supported: ['3.1', '3.2'], + manifest: { format_kind: 'image', assets: {} }, + entries: [ + { + capability_id: 'seller.route:with-dots', + operations: ['preview'], + format: { format_kind: 'image', params: {} }, + }, + ], + }); + assert.ok(calls.length > 0); + assert.equal(standalone ? result.passed : result.overall_passed, true, JSON.stringify(result)); + }); + test(`a newer request cannot infer canonical addressing from old catalog extensions: ${standalone}`, async () => { + const { result, calls } = await run({ + standalone, + pin: '3.2.5', + echo: '3.1', + generation: true, + recipes: false, + entries: [ + { + capability_id: 'legacy-build', + format: { format_kind: 'image', params: {}, technical_requirements_complete: true }, + }, + ], + }); + assert.deepEqual(calls, []); + if (standalone) assert.equal(result.skip_reason, 'fixture_unsatisfied'); + else { + assert.equal(result.failed_count, 0); + assert.equal(result.passed_count, 0); + assert.equal(result.coverage_gaps[0].reason, 'fixture_unsatisfied'); + } + }); + test(`served canonical ID uniqueness applies even under an old request pin: ${standalone}`, async () => { + const entry = { + capability_id: 'audio-build', + operations: ['build'], + format: { format_kind: 'audio_vast', params: {}, v1_format_ref: [named] }, + }; + const { result, calls } = await run({ standalone, entries: [entry, structuredClone(entry)] }); + assert.deepEqual(calls, []); + if (standalone) assert.equal(result.passed, false); + else assert.equal(result.failed_count, 1); + const step = standalone + ? result + : result.phases.flatMap(phase => phase.steps).find(step => step.step_id === 'creative_build_catalog'); + assert.match(step.validations[0].error, /unique/); + }); + test(`a served-valid canonical audio manifest cannot replace pinned legacy input validation: ${standalone}`, async () => { + await assert.rejects( + run({ + standalone, + manifest: { format_kind: 'audio_vast', assets: {} }, + servedRoot: path.join(schemaData, '3.2.5'), + }), + /creative_manifest must be a complete manifest/ + ); + }); + test(`a served canonical catalog does not inherit the pinned legacy open enum: ${standalone}`, async () => { + const { result, calls } = await run({ + standalone, + entries: [ + { capability_id: 'future', operations: ['build'], format: { format_kind: 'future_canonical', params: {} } }, + ], + }); + assert.deepEqual(calls, []); + if (standalone) assert.equal(result.passed, false); + else assert.equal(result.failed_count, 1); + const step = standalone + ? result + : result.phases.flatMap(phase => phase.steps).find(step => step.step_id === 'creative_build_catalog'); + assert.equal(step.validations[0].check, 'creative_build_probe_catalog'); + assert.equal(step.validations[0].passed, false); + }); + test(`an invalid supplied served root is rejected before dispatch: ${standalone}`, async () => { + await assert.rejects(run({ standalone, servedRoot: path.join(temporaryRoot, 'missing') }), /root|schema|ENOENT/); + }); +} + +const sdkRoot = path.resolve(require.resolve('@adcp/sdk/testing'), '../../../..'); +const schemaLoader = require(path.join(sdkRoot, 'dist/lib/validation/schema-loader.js')); +const runtimePin = require('@adcp/sdk').ADCP_VERSION; + +test('the actual RC pin selects a listed stable catalog only when its schema is available', () => { + assert.match(runtimePin, /-rc\./); + assert.doesNotThrow(() => schemaLoader.toReleasePrecisionWire(runtimePin)); + assert.equal(schemaLoader.hasSchemaBundle('3.2'), false, 'the pinned SDK does not ship stable3.2'); + const caps = { adcp_version: '3.2', adcp: { supported_versions: ['3.2'] } }; + assert.equal(schemaLoader.withServedValidationVersion(caps, runtimePin, version => version), runtimePin); + const selected = schemaLoader.withServedReleaseSchemaRoots({ '3.2.5': path.join(schemaData, '3.2.5') }, () => + schemaLoader.withServedValidationVersion(caps, runtimePin, version => version)); + assert.equal(selected, '3.2'); + assert.equal(schemaLoader.hasSchemaBundle('3.2'), false, 'the catalog root must not escape its scope'); +}); + +for (const standalone of [false, true]) { + for (const variant of ['dotted-id', 'duplicate-id', 'undefined-operations-extension']) { + test(`RC request pin respects served3.1 catalog rules: ${standalone}, ${variant}`, async () => { + const target = { agent_url: 'https://formats.example', id: 'sandbox_image', width: 300, height: 250 }; + const entry = { + capability_id: 'seller.route:with-dots', + format: { format_kind: 'image', params: {}, v1_format_ref: [target] }, + ...(variant === 'undefined-operations-extension' ? { operations: ['render'] } : {}), + }; + const { result, calls } = await run({ + standalone, pin: runtimePin, echo: '3.1', target, + entries: variant === 'duplicate-id' ? [entry, structuredClone(entry)] : [entry], + manifest: { format_kind: 'image', assets: { image_main: { + asset_type: 'image', url: 'https://test-assets.example/source.jpg', width: 300, height: 250, + } } }, + }); + assert.equal(standalone ? result.passed : result.overall_passed, true, JSON.stringify(result)); + assert.ok(calls.length > 0, 'an applicable valid probe must actually execute'); + }); + } +} + +test('served3.0 lacks a build-catalog contract and receives a zero-credit fixture gap', async () => { + const { result, calls } = await run({ pin: runtimePin, echo: '3.0', supported: ['3.0', '3.2'] }); + assert.equal(calls.length, 0); + // The raw overall_passed flag does not certify fixture coverage. Assert + // the public bundle's aggregate result and executed credit explicitly. + const [bundle] = buildComplianceBundleResults([{ ref: { kind: 'protocol', id: 'creative', path: '/fixture' }, storyboards: [storyboard()] }], [result]); + assert.equal(bundle.status, 'partial'); + assert.equal(result.passed_count, 0); + assert.equal(result.fixture_resolutions[0].status, 'unsatisfied'); + assert.equal(result.failed_count, 0, 'missing catalog authority is a fixture gap, not a producer verdict'); +}); + +test('non-opted no-echo, unlisted and unavailable-root response validation stays identical to stockSDK14', () => { + const { createHash } = require('node:crypto'); + const bridgeManifest = require('../patches/sdk-creative-build-probe/manifest.json'); + const parent = path.join(__dirname, '../.context'); + fs.mkdirSync(parent, { recursive: true }); + const dir = fs.mkdtempSync(path.join(parent, 'sdk14-authority-baseline-')); + const stock = path.join(dir, 'node_modules/@adcp/sdk'); + try { + fs.cpSync(sdkRoot, stock, { recursive: true }); + for (const entry of bridgeManifest.files) { + const file = path.join(stock, entry.path); + if (entry.before === null) { fs.rmSync(file); continue; } + let text = fs.readFileSync(file, 'utf8'); + assert.equal(createHash('sha256').update(text).digest('hex'), entry.after); + for (const edit of [...entry.replacements].reverse()) text = text.replace(edit.to, () => edit.from); + assert.equal(createHash('sha256').update(text).digest('hex'), entry.before); + fs.writeFileSync(file, text); + } + const original = require(path.join(stock, 'dist/lib/testing/storyboard/validations.js')); + const patched = require(path.join(sdkRoot, 'dist/lib/testing/storyboard/validations.js')); + for (const [echo, supported] of [[undefined, ['3.1']], ['3.1', ['3.2']], ['3.9', ['3.9']]]) { + const context = { + taskName: 'get_adcp_capabilities', adcpVersion: runtimePin, + responseSchemaRef: 'protocol/get-adcp-capabilities-response.json', + taskResult: { success: true, data: { + ...(echo && { adcp_version: echo }), adcp: { major_versions: [3], supported_versions: supported }, + supported_protocols: ['creative'], creative: { supports_evaluator: false }, + } }, + }; + const validations = [{ check: 'response_schema', description: 'Stock authority control' }]; + assert.deepEqual(patched.runValidations(validations, structuredClone(context)), + original.runValidations(validations, structuredClone(context)), `echo=${echo}`); + } + } finally { fs.rmSync(dir, { recursive: true, force: true }); } +}); + +function governanceCatalog() { + return { + status: 'completed', adcp_version: '3.2-rc.7', + adcp: { + major_versions: [3], supported_versions: ['3.2-rc.7', '3.2'], idempotency: { supported: false }, + governance_enforcement: { tasks: [ + { task: 'buy_products', modes: ['signed_context'] }, + { task: 'accept_proposal', modes: ['signed_context'] }, + { task: 'control_media_buy', modes: ['signed_context'] }, + { task: 'create_media_buy', modes: ['signed_context', 'online_execution_check'] }, + ] }, + }, + supported_protocols: ['media_buy'], experimental_features: ['governance.campaign'], + }; +} + +function reconstructStock(t) { + const { createHash } = require('node:crypto'); + const manifest = require('../patches/sdk-creative-build-probe/manifest.json'); + const dir = fs.mkdtempSync(path.join(__dirname, '../.context/sdk14-scoped-stock-')); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + const stock = path.join(dir, 'node_modules/@adcp/sdk'); + fs.cpSync(sdkRoot, stock, { recursive: true }); + for (const entry of manifest.files) { + const file = path.join(stock, entry.path); + if (entry.before === null) { fs.rmSync(file); continue; } + let text = fs.readFileSync(file, 'utf8'); + assert.equal(createHash('sha256').update(text).digest('hex'), entry.after); + for (const edit of [...entry.replacements].reverse()) { + assert.equal(text.split(edit.to).length - 1, 1); + text = text.replace(edit.to, () => edit.from); + } + assert.equal(createHash('sha256').update(text).digest('hex'), entry.before); + fs.writeFileSync(file, text); + } + return stock; +} + +// This parent compiles both real module systems and a reconstructed stock SDK; +// child assertions remain bounded and the process must exit naturally. +test('paired authority scope preserves stock14 and distinguishes actual JSON from generated Zod in CJS/ESM', { timeout: 90000 }, async t => { + const stock = reconstructStock(t); + const { pathToFileURL } = require('node:url'); + for (const ext of ['js', 'mjs']) { + const load = file => ext === 'js' ? require(file) : import(pathToFileURL(file).href); + const loader = await load(path.join(sdkRoot, `dist/lib/validation/schema-loader.${ext}`)); + const validations = await load(path.join(sdkRoot, `dist/lib/testing/storyboard/validations.${ext}`)); + const originalLoader = await load(path.join(stock, `dist/lib/validation/schema-loader.${ext}`)); + const originalValidations = await load(path.join(stock, `dist/lib/testing/storyboard/validations.${ext}`)); + const clientModule = await load(path.join(sdkRoot, `dist/lib/testing/client.${ext}`)); + const originalClientModule = await load(path.join(stock, `dist/lib/testing/client.${ext}`)); + const check = (module, data = governanceCatalog(), extra = {}) => module.runValidations( + [{ check: 'response_schema', description: 'Capability catalog' }], { + taskName: 'get_adcp_capabilities', adcpVersion: '3.2.5', + responseSchemaRef: 'protocol/get-adcp-capabilities-response.json', + taskResult: { success: true, data }, ...extra, + }, + )[0]; + const source = fn => loader.withExternalSchemaRoot('3.2.5', path.join(schemaData, '3.2.5'), fn); + const stockSource = fn => originalLoader.withExternalSchemaRoot('3.2.5', path.join(schemaData, '3.2.5'), fn); + + await t.test(`${ext}: non-opted RC/source-root mismatch exactly matches real stock14`, () => { + assert.deepEqual(source(() => check(validations)), stockSource(() => check(originalValidations))); + assert.equal(source(() => check(validations)).passed, true); + assert.equal(loader.isCreativeBuildProbeSchemaScopeActive(), false); + }); + await t.test(`${ext}: opted builtin JSON-valid catalog survives stale Zod with truthful warning`, () => { + const result = source(() => loader.withCreativeBuildProbeSchemaScope(true, () => check(validations))); + assert.equal(result.passed, true); + assert.equal(result.strict.valid, true); + assert.equal(result.strict.lenient_valid, false); + assert.match(result.schema_url, /3\.2-rc\.7/); + assert.match(result.warning, /JSON schema.*generated Zod/); + }); + await t.test(`${ext}: implicit SDK request pin uses the same served selection as the probe helper`, () => { + const data = governanceCatalog(); data.adcp_version = '3.1'; data.adcp.supported_versions = ['3.1', '3.2-rc.7']; + data.adcp.governance_enforcement.tasks = [{ task: 'create_media_buy', modes: ['online_execution_check'] }]; + const result = loader.withCreativeBuildProbeSchemaScope(true, () => check(validations, data, { + adcpVersion: undefined, responseAdcpVersion: '3.2.0-rc.7', + })); + assert.match(result.schema_url, /schemas\/3\.1\//); + assert.equal(result.strict.valid, true); + assert.equal(result.passed, true); + }); + await t.test(`${ext}: external served JSON is authoritative and actual JSON-invalid stays failed`, () => { + const poisoned = path.join(temporaryRoot, `invalid-capabilities-${ext}`); + fs.cpSync(path.join(schemaData, '3.2.5'), poisoned, { recursive: true }); + const file = path.join(poisoned, 'protocol/get-adcp-capabilities-response.json'); + const document = JSON.parse(fs.readFileSync(file, 'utf8')); document.not = {}; + fs.writeFileSync(file, JSON.stringify(document)); + // This owned root uses raw JSON rather than its precompiled bundle, whose + // independent snapshot would otherwise correctly retain the old schema. + fs.rmSync(path.join(poisoned, 'bundled'), { recursive: true, force: true }); + const data = governanceCatalog(); data.adcp_version = '3.2'; + data.adcp.governance_enforcement.tasks[3].modes = ['signed_context']; + const result = loader.withCreativeBuildProbeSchemaScope(true, () => + loader.withServedReleaseSchemaRoots({ '3.2.5': poisoned }, () => check(validations, data, { adcpVersion: runtimePin })), + ); + assert.equal(result.passed, false); + assert.equal(result.strict.valid, false); + assert.equal(result.strict.lenient_valid, true, 'actual JSON rejection must override generated-Zod acceptance'); + assert.match(result.schema_url, /schemas\/3\.2\//); + assert.ok(result.actual.some(issue => issue.keyword === 'not')); + }); + await t.test(`${ext}: actual builtin JSON-invalid catalog cannot receive passing credit`, () => { + const data = governanceCatalog(); data.adcp.idempotency = { supported: true, replay_ttl_seconds: 1 }; + const result = loader.withCreativeBuildProbeSchemaScope(true, () => check(validations, data)); + assert.equal(result.passed, false); + assert.equal(result.strict.valid, false); + assert.ok(result.actual.length > 0); + }); + await t.test(`${ext}: absent strict schema is a failing harness diagnostic, never pass/skip`, () => { + const missing = path.join(temporaryRoot, `missing-capabilities-${ext}`); + fs.cpSync(path.join(schemaData, '3.2.5'), missing, { recursive: true }); + fs.rmSync(path.join(missing, 'bundled'), { recursive: true, force: true }); + fs.rmSync(path.join(missing, 'protocol/get-adcp-capabilities-response.json')); + const data = governanceCatalog(); data.adcp_version = '3.2'; + const result = loader.withExternalSchemaRoot('3.2.5', missing, () => + loader.withCreativeBuildProbeSchemaScope(true, () => check(validations, data))); + assert.equal(result.passed, false); + assert.equal(result.not_applicable, undefined); + assert.equal(result.actual.reason, 'no_schema_registered'); + }); + await t.test(`${ext}: ordinary nested/concurrent runs shadow opt-in and scope unwinds on errors`, async () => { + const before = source(() => check(validations)); + await source(() => loader.withCreativeBuildProbeSchemaScope(true, async () => { + const results = await Promise.all([ + Promise.resolve().then(() => check(validations)), + loader.withCreativeBuildProbeSchemaScope(false, async () => { + await new Promise(resolve => setImmediate(resolve)); + return check(validations); + }), + ]); + assert.equal(results[0].strict.lenient_valid, false); + assert.deepEqual(results[1], before); + assert.equal(loader.isCreativeBuildProbeSchemaScopeActive(), true); + })); + assert.throws(() => loader.withCreativeBuildProbeSchemaScope(true, () => { throw new Error('scope cleanup'); }), /scope cleanup/); + assert.equal(loader.isCreativeBuildProbeSchemaScopeActive(), false); + assert.deepEqual(source(() => check(validations)), before); + }); + await t.test(`${ext}: actual capability preflight honors scope before validating borrowed client facts`, async () => { + const mock = { + getAgentInfo: async () => ({ name: 'Sandbox', tools: [{ name: 'get_adcp_capabilities' }] }), + getAdcpCapabilities: async () => ({ success: true, data: governanceCatalog() }), + getAdcpVersion: () => '3.2.5', + }; + const original = await stockSource(() => originalClientModule.discoverAgentProfile(mock, undefined, '3.2.5')); + const ordinary = await source(() => clientModule.discoverAgentProfile(mock, undefined, '3.2.5')); + assert.deepEqual(ordinary.profile, original.profile); + const opted = await source(() => loader.withCreativeBuildProbeSchemaScope(true, + () => clientModule.discoverAgentProfile(mock, undefined, '3.2.5'))); + assert.equal(opted.profile.capabilities_schema_issues, undefined); + assert.deepEqual(opted.profile.raw_capabilities, governanceCatalog()); + const invalid = governanceCatalog(); invalid.adcp.idempotency = { supported: true, replay_ttl_seconds: 1 }; + const rejected = await source(() => loader.withCreativeBuildProbeSchemaScope(true, + () => clientModule.discoverAgentProfile({ ...mock, getAdcpCapabilities: async () => ({ success: true, data: invalid }) }, undefined, '3.2.5'))); + assert.ok(rejected.profile.capabilities_schema_issues.length > 0); + assert.match(rejected.profile.capabilities_schema_issues[0].message, /Capability catalog JSON schema 3.2-rc.7/); + }); + await t.test(`${ext}: unauthorized echoes retain pinned authority inside opted scope`, () => { + source(() => loader.withCreativeBuildProbeSchemaScope(true, () => { + for (const echo of ['4.0', 'latest', '../../etc', '3.1.29', '3.2-..']) { + const data = governanceCatalog(); data.adcp_version = echo; data.adcp.supported_versions = [echo]; + const result = check(validations, data); + assert.match(result.schema_url, /schemas\/3\.2\.5\//, echo); + } + const data = governanceCatalog(); data.adcp.supported_versions = ['3.2']; + assert.match(check(validations, data).schema_url, /schemas\/3\.2\.5\//); + })); + }); + await t.test(`${ext}: non-capability checks preserve stock14 even inside opted scope`, () => { + const context = { taskName: 'get_products', responseSchemaRef: 'media-buy/get-products-response.json', + taskResult: { success: true, data: { status: 'completed', products: [] } } }; + const original = stockSource(() => check(originalValidations, undefined, context)); + const actual = source(() => loader.withCreativeBuildProbeSchemaScope(true, () => check(validations, undefined, context))); + assert.deepEqual(actual, original); + }); + await t.test(`${ext}: direct helper explicitly resolves served facts without scope or inside false scope`, async () => { + const helper = await load(path.join(sdkRoot, `dist/lib/testing/storyboard/creative-build-probe.${ext}`)); + const caps = { adcp_version: '3.2', adcp: { supported_versions: ['3.1', '3.2'] }, creative: { + supports_transformation: true, + supported_formats: [{ capability_id: 'audio-build', operations: ['build'], + format: { format_kind: 'audio_vast', params: {}, v1_format_ref: [named] } }], + } }; + const kit = { creative_build_probes: [{ + match: [{ path: '/format/format_kind', operator: 'equals', value: 'audio_vast' }], + input: { target_format_id: named, creative_manifest: { format_id: named, assets: {} } }, + }] }; + loader.withExternalSchemaRoot('3.1.29', path.join(schemaData, '3.1.29'), () => + loader.withServedReleaseSchemaRoots({ '3.2.5': hostedRoot }, () => { + const call = () => helper.prepareCreativeBuildProbe(caps, { context_prefix: 'build', message: 'Make a sandbox creative' }, kit, '3.1.29'); + const direct = call(); + assert.equal(direct.status, 'resolved'); + assert.deepEqual(loader.withCreativeBuildProbeSchemaScope(false, call), direct); + assert.deepEqual(loader.withCreativeBuildProbeSchemaScope(true, call), direct); + assert.equal(loader.isCreativeBuildProbeSchemaScopeActive(), false); + })); + }); + } +}); + +for (const ext of ['js', 'mjs']) { + test(`${ext}: public full/standalone preserve stock acceptance-catalog behavior and defer probe-only roots`, async t => { + const stock = reconstructStock(t); + const { pathToFileURL } = require('node:url'); + const load = file => ext === 'js' ? require(file) : import(pathToFileURL(file).href); + const api = await load(path.join(sdkRoot, `dist/lib/testing/index.${ext}`)); + const original = await load(path.join(stock, `dist/lib/testing/index.${ext}`)); + const data = governanceCatalog(); + data.media_buy = { acceptance_policy_discovery: { + catalog_url: 'https://catalog.example/policy.json', catalog_digest: `sha256:${'a'.repeat(64)}`, + } }; + const source = ` +id: media_buy_seller/acceptance_policy_discovery +version: "1.0.0" +title: Acceptance authority control +category: media_buy_seller +summary: Preserve real catalog verification after capability validation +phases: + - id: catalog + title: Catalog + steps: + - id: get_acceptance_policy_capability + title: Capability metadata + task: get_adcp_capabilities + response_schema_ref: protocol/get-adcp-capabilities-response.json + sample_request: {} + validations: + - check: response_schema + - id: products + title: Product profiles + steps: + - id: get_contextual_products + title: Product profiles + task: get_products + sample_request: {} + validations: [] +`; + async function execute(module, standalone = false) { + const storyboard = module.parseStoryboard(source); + const client = module.withExternalSchemaRoot('3.2.5', path.join(schemaData, '3.2.5'), + () => module.createTestClient('https://agent.example/mcp', 'mcp', { adcpVersion: '3.2.5' })); + client.resetContext = () => {}; + client.executeTask = async task => ({ success: true, data: task === 'get_adcp_capabilities' ? structuredClone(data) : { status: 'completed', products: [] } }); + client.getAdcpCapabilities = () => client.executeTask('get_adcp_capabilities'); + client.getProducts = () => client.executeTask('get_products'); + let resolved = 0; + const profile = { tools: ['get_adcp_capabilities', 'get_products'], raw_capabilities: data, + capabilities_schema_issues: [{ pointer: '/adcp', message: 'Capability catalog JSON schema 3.2-rc.7: prior opted discovery diagnostic' }] }; + const snapshot = structuredClone(profile); + const opts = { + adcpVersion: '3.2.5', schemaRoot: path.join(schemaData, '3.2.5'), _client: client, + _profile: profile, agentTools: profile.tools, + // Probe-only configuration must not be validated or registered here. + servedReleaseSchemaRoots: { invalid: '/nonexistent-probe-only-root' }, + _controllerCapabilities: { detected: false, scenarios: [] }, + _acceptancePolicyDiscoveryDependencies: { + resolveCatalog: async () => { resolved++; return { ok: true, catalog: { profiles: [] } }; }, + }, + }; + const result = standalone + ? await module.runStoryboardStep('https://agent.example/mcp', storyboard, 'get_acceptance_policy_capability', opts) + : await module.runStoryboard('https://agent.example/mcp', storyboard, opts); + assert.deepEqual(profile, snapshot, 'caller profile remains caller-owned'); + const steps = standalone ? [result] : result.phases.flatMap(phase => phase.steps); + assert.ok(steps.every(step => step.passed && !step.skipped), JSON.stringify(result)); + assert.equal(resolved, 1, 'actual acceptance verifier must run after valid discovery'); + assert.ok(steps[0].validations.some(validation => validation.check === 'acceptance_policy_discovery' && validation.passed)); + if (!standalone) assert.ok(steps[1].validations.some(validation => validation.check === 'acceptance_policy_discovery' && validation.passed)); + return steps.map(step => ({ passed: step.passed, validations: step.validations })); + } + for (const standalone of [false, true]) assert.deepEqual(await execute(api, standalone), await execute(original, standalone)); + }); + + test(`${ext}: real mixed comply batch defers probe roots and clears inherited opt-in before discovery`, { timeout: 60000 }, async t => { + const { pathToFileURL } = require('node:url'); + const load = file => ext === 'js' ? require(file) : import(pathToFileURL(file).href); + const api = await load(path.join(sdkRoot, `dist/lib/testing/index.${ext}`)); + const loader = await load(path.join(sdkRoot, `dist/lib/validation/schema-loader.${ext}`)); + const http = require('node:http'); + const tools = ['get_adcp_capabilities', 'build_creative', 'comply_test_controller']; + const data = governanceCatalog(); + data.supported_protocols = ['creative']; + data.creative = { supports_evaluator: true, supports_generation: true, supported_formats: [ + { capability_id: 'sandbox-build', format: { format_kind: 'image', params: {}, technical_requirements_complete: true } }, + ] }; + data.experimental_features.push('creative.evaluator'); + const server = http.createServer(async (request, response) => { + if (request.method !== 'POST') { response.writeHead(404).end(); return; } + let body = ''; for await (const chunk of request) body += chunk; + const rpc = JSON.parse(body); + if (rpc.id === undefined) { response.writeHead(202).end(); return; } + let result; + if (rpc.method === 'initialize') result = { protocolVersion: '2025-03-26', capabilities: { tools: {} }, serverInfo: { name: 'Sandbox', version: '1.0.0' } }; + else if (rpc.method === 'tools/list') result = { tools: tools.map(name => ({ name, inputSchema: { type: 'object', properties: {} } })) }; + else if (rpc.method === 'tools/call') { + const payload = rpc.params.name === 'get_adcp_capabilities' ? data : { success: true, scenarios: ['query_upstream_traffic'] }; + result = { content: [{ type: 'text', text: JSON.stringify(payload) }], structuredContent: payload }; + } else result = {}; + response.writeHead(200, { 'content-type': 'application/json' }); + response.end(JSON.stringify({ jsonrpc: '2.0', id: rpc.id, result })); + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + t.after(async () => { + server.closeAllConnections(); + await new Promise(resolve => server.close(resolve)); + }); + const endpoint = `http://127.0.0.1:${server.address().port}/mcp`; + const calls = []; + // Observe the existing SDK transport hook; requests still traverse the + // real MCP client/server and are never dispatched by a runner fallback. + const observeFetch = async (...args) => { + const request = args[0] instanceof Request ? args[0] : new Request(...args); + if (request.url.startsWith(endpoint) && request.method === 'POST') { + const body = await request.clone().text(); + if (body) { + const rpc = JSON.parse(body); + calls.push({ method: rpc.method, tool: rpc.params?.name, opted: loader.isCreativeBuildProbeSchemaScopeActive() }); + } + } + return fetch(...args); + }; + const transport = { trustedFetchFn: observeFetch, allowPrivateIp: true }; + // These use owned SDK clients and the actual MCP TaskExecutor. Inspect + // fetch at dispatch time, so server callback context cannot fake ALS proof. + for (const opted of [false, true]) { + for (const standalone of [false, true]) { + const fixture = api.parseStoryboard(` +id: scope_preflight_control +version: "1.0.0" +title: Scope preflight control +category: creative +summary: Validate the actual client preflight and response authority +${opted ? 'creative_build_probe:\n context_prefix: build\n message: Make a sandbox image' : ''} +phases: + - id: discovery + title: Discovery + steps: + - id: capabilities + title: Capability schema + task: get_adcp_capabilities + response_schema_ref: protocol/get-adcp-capabilities-response.json + sample_request: {} + validations: + - check: response_schema +`); + const start = calls.length; + const options = { allow_http: true, transport, adcpVersion: '3.2.5', + schemaRoot: path.join(schemaData, '3.2.5'), skip_controller_seeding: true, + _serverAdcpVersion: '3.1.29', + ...!opted && { servedReleaseSchemaRoots: { invalid: '/nonexistent-probe-only-root' } }, + }; + const result = standalone + ? await api.runStoryboardStep(endpoint, fixture, 'capabilities', options) + : await api.runStoryboard(endpoint, fixture, options); + const steps = standalone ? [result] : result.phases.flatMap(phase => phase.steps); + assert.ok(steps.every(step => step.passed && !step.skipped), JSON.stringify(result)); + const schema = steps[0].validations.find(validation => validation.check === 'response_schema'); + assert.equal(schema.passed, true); + assert.equal(schema.strict.valid, true); + assert.match(schema.schema_url, opted ? /3\.2-rc\.7/ : /3\.2\.5/); + const dispatched = calls.slice(start).filter(call => call.tool === 'get_adcp_capabilities'); + assert.ok(dispatched.length >= 2, 'owned client performs preflight before the authored capability step'); + assert.ok(dispatched.every(call => call.opted === opted), JSON.stringify(dispatched)); + } + } + calls.length = 0; + await loader.withCreativeBuildProbeSchemaScope(true, async () => { + await assert.rejects(api.comply(endpoint, { + allow_http: true, + transport, + complianceDir: path.join(__dirname, '../dist/compliance/latest'), + schemaRoot: path.join(schemaData, '3.2.5'), + storyboards: ['capability_discovery', 'creative/evaluator_auth'], + servedReleaseSchemaRoots: { invalid: '/nonexistent-probe-only-root' }, + }), /invalid|version|bundle|schema root/i); + assert.equal(loader.isCreativeBuildProbeSchemaScopeActive(), true, 'caller scope remains caller-owned'); + }); + const capabilities = calls.filter(call => call.tool === 'get_adcp_capabilities'); + assert.ok(capabilities.length >= 2, JSON.stringify(calls)); + assert.ok(capabilities.every(call => call.opted === false), 'outer discovery and ordinary storyboard shadow the parent opt-in'); + assert.ok(!calls.some(call => call.tool === 'build_creative'), 'invalid probe root is rejected before probe dispatch'); + assert.equal(loader.isCreativeBuildProbeSchemaScopeActive(), false); + }); +} + +test('RC selection ignores unlisted, cross-major, semver and malformed echoes even with a stable root available', () => { + schemaLoader.withServedReleaseSchemaRoots({ '3.2.5': path.join(schemaData, '3.2.5') }, () => { + for (const echo of ['4.0', '9.9', 'latest', '../../etc', '3.2-..', '3.1.29']) { + const response = { adcp_version: echo, adcp: { supported_versions: [echo] } }; + assert.equal(schemaLoader.withServedValidationVersion(response, runtimePin, version => version), runtimePin, echo); + } + const response = { adcp_version: '3.2', adcp: { supported_versions: ['3.1'] } }; + assert.equal(schemaLoader.withServedValidationVersion(response, runtimePin, version => version), runtimePin); + }); +}); + +test('validators are root-qualified and a served root never replaces the pinned manifest authority', () => { + const alternate = path.join(temporaryRoot, 'poisoned-3.1'); + fs.cpSync(path.join(schemaData, '3.1.29'), alternate, { recursive: true }); + const file = path.join(alternate, 'core/creative-manifest.json'); + const schema = JSON.parse(fs.readFileSync(file, 'utf8')); schema.not = {}; + fs.writeFileSync(file, JSON.stringify(schema)); + withExternalSchemaRoot('3.1.29', path.join(schemaData, '3.1.29'), () => { + const pinned = schemaLoader.getSchemaValidatorByRef('core/creative-manifest.json', '3.1.29'); + const input = { format_id: named, assets: {} }; + assert.equal(pinned(input), true); + schemaLoader.withServedReleaseSchemaRoots({ '3.1.29': alternate }, () => { + const response = { adcp_version: '3.1', adcp: { supported_versions: ['3.1'] } }; + schemaLoader.withServedValidationVersion(response, runtimePin, selected => { + const served = schemaLoader.getSchemaValidatorByRef('core/creative-manifest.json', selected); + assert.notEqual(served, pinned); + assert.equal(served(input), false); + assert.equal(pinned(input), true, 'already captured request validator stays pinned'); + }); + }); + assert.equal(schemaLoader.getSchemaValidatorByRef('core/creative-manifest.json', '3.1.29'), pinned); + }); +}); diff --git a/tests/evaluator-build-probe-cleanup.test.cjs b/tests/evaluator-build-probe-cleanup.test.cjs new file mode 100644 index 00000000000..43f62539c1f --- /dev/null +++ b/tests/evaluator-build-probe-cleanup.test.cjs @@ -0,0 +1,162 @@ +// Maintenance integration regressions adapted from merged adcp-client#3332. +// Run against this branch's actual, unchanged SDK dependency. +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const http = require('node:http'); +const { parseStoryboard, runStoryboard, runStoryboardStep } = require('@adcp/sdk/testing'); +const path = require('node:path'); +const { registerMCPConnectionScopeCleanup, withMCPConnectionScope } = require(path.resolve(require.resolve('@adcp/sdk/testing'), '../../protocols/mcp-scope.js')); + +function storyboard() { + return parseStoryboard(` +id: creative_probe_cleanup +version: "1.0.0" +title: Creative probe cleanup +category: creative +summary: Cleanup after fixture authoring errors +creative_build_probe: + context_prefix: build + message: Create one sandbox creative +phases: + - id: boundary + title: Evaluator boundary + steps: + - id: reject + title: Reject off-list evaluator + task: build_creative + requires_contract: creative_build_probe_runner + expect_error: true + sample_request: + evaluator: + agent_url: "https://off-list.example/adcp" + validations: + - check: error_code + value: EVALUATOR_AGENT_NOT_ACCEPTED +`); +} + +function options() { + const tools = ['get_adcp_capabilities', 'build_creative']; + return { + agentTools: tools, + _profile: { + tools, + raw_capabilities: { + creative: { + supports_generation: true, + supports_evaluator: true, + supported_formats: [ + { + capability_id: 'build', + operations: ['build'], + format: { format_kind: 'image', params: {}, technical_requirements_complete: true }, + }, + ], + }, + }, + }, + _controllerCapabilities: { detected: false, scenarios: [] }, + _client: { + resetContext() {}, + async executeTask() { + assert.fail('Fixture preparation must finish before any tool is dispatched'); + }, + }, + test_kit: { + creative_build_probes: [ + { + match: [{ path: '/capability_id', operator: 'equals', value: 'build' }], + input: { evaluator: { agent_url: 'https://injected.example/adcp' } }, + }, + ], + }, + }; +} + +async function listen(server, port) { + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(port, '127.0.0.1', resolve); + }); +} + +async function close(server) { + if (server.listening) await new Promise(resolve => server.close(resolve)); +} + +test('fixture authoring errors close the real owned listener and its isolated connection scope', async t => { + const reservation = http.createServer(); + await listen(reservation, 0); + const port = reservation.address().port; + await close(reservation); + + // Keep the failure path test safe against regressions that leave a real + // listener open: record created servers and release them after assertions. + const servers = []; + const originalCreateServer = http.createServer; + t.mock.method(http, 'createServer', (...args) => { + const server = originalCreateServer(...args); + servers.push(server); + return server; + }); + t.after(async () => { + await Promise.all(servers.map(close)); + }); + + let callerClosed = 0; + let runnerClosed = 0; + const opts = options(); + opts._client.resetContext = () => { + registerMCPConnectionScopeCleanup('modern', 'runner-owned', async () => runnerClosed++); + }; + await withMCPConnectionScope(async () => { + registerMCPConnectionScopeCleanup('modern', 'caller-owned', async () => callerClosed++); + await assert.rejects( + runStoryboard('https://agent.example/mcp', storyboard(), { + ...opts, + allow_http: true, + webhook_receiver: { host: '127.0.0.1', port }, + }), + /input may only/ + ); + assert.equal(runnerClosed, 1); + assert.equal(callerClosed, 0, 'the caller connection lifetime remains open'); + const replacement = http.createServer(); + await listen(replacement, port); + await close(replacement); + }); + assert.equal(callerClosed, 1); +}); + +test('standalone fixture authoring errors preserve the externally injected receiver', async () => { + let receiverClosed = 0; + const receiver = { + base_url: 'https://receiver.example', + async close() { + receiverClosed++; + }, + }; + await assert.rejects( + runStoryboardStep('https://agent.example/mcp', storyboard(), 'reject', { + ...options(), + _webhookReceiver: receiver, + }), + /input may only/ + ); + assert.equal(receiverClosed, 0); +}); + +test('malformed producer catalogs cascade as catalog prerequisites rather than controller failures', async () => { + const opts = options(); + delete opts.test_kit; + opts._profile.raw_capabilities.creative.supported_formats[0].format.seller_preference = 'INVALID'; + const result = await runStoryboard('https://agent.example/mcp', storyboard(), opts); + assert.equal(result.overall_passed, false); + assert.equal(result.failed_count, 1); + assert.equal(result.fixture_resolutions[0].status, 'failed'); + const rejectedProbe = result.phases.find(phase => phase.phase_id === 'boundary').steps[0]; + assert.equal(rejectedProbe.skipped, true); + assert.equal(rejectedProbe.skip_reason, 'prerequisite_failed'); + assert.equal(rejectedProbe.skip.reason, 'prerequisite_failed'); + assert.match(rejectedProbe.skip.detail, /fixture_resolution\.creative_build_catalog/); +}); diff --git a/tests/lint-sdk-shims.test.cjs b/tests/lint-sdk-shims.test.cjs index c53d28b752c..f002b3aac89 100644 --- a/tests/lint-sdk-shims.test.cjs +++ b/tests/lint-sdk-shims.test.cjs @@ -38,9 +38,10 @@ test('ledger entries include removal conditions and upstream asks', () => { } }); -test('source tree has no remaining private SDK reach-ins', () => { +test('private SDK reach-ins remain bounded to the owned installer and authority regression', () => { const findings = collectFindings(); - assert.deepEqual(findings, []); + assert.deepEqual([...new Set(findings.map(finding => finding.file))], ['scripts/install-sdk-creative-build-probe.cjs', 'tests/evaluator-build-probe-authority.test.cjs']); + assert.ok(findings.every(finding => ['dist/lib/testing/', 'dist/lib/validation/'].some(term => finding.term.includes(term)))); }); test('matchingPrivateTerm ignores comment-only lines but catches private SDK code', () => { diff --git a/tests/lint-storyboard-sample-request-schema.test.cjs b/tests/lint-storyboard-sample-request-schema.test.cjs index d4cdf5604d4..e86beca4d7c 100644 --- a/tests/lint-storyboard-sample-request-schema.test.cjs +++ b/tests/lint-storyboard-sample-request-schema.test.cjs @@ -24,6 +24,9 @@ const { entryKey, isNegativeStep, normalizeSubstitutions, + normalizeStoryboardSampleRequests, + loadSchema, + validateStep, STORYBOARD_DIR, } = require('../scripts/lint-storyboard-sample-request-schema.cjs'); @@ -376,3 +379,68 @@ test('normalizeSubstitutions synthesizes shape-valid placeholders at object-type assert.ok('data_provider_domain' in oneOfSynth); assert.ok('id' in oneOfSynth); }); + +function evaluatorBuildAuthoringFixture() { + const storyboard = yaml.load(fs.readFileSync(path.join(STORYBOARD_DIR, + 'protocols/creative/scenarios/evaluator_auth.yaml'), 'utf8')); + return { storyboard, steps: storyboard.phases.flatMap(phase => phase.steps || []) + .filter(step => step.task === 'build_creative') }; +} + +test('all authored evaluator builds materialize valid generation, source, library, transformer and legacy requests', async () => { + const { storyboard, steps } = evaluatorBuildAuthoringFixture(); + assert.equal(steps.length, 5); + for (const step of steps) { + const original = structuredClone(step.sample_request); + const schema = loadSchema(step.schema_ref); + const fixtures = normalizeStoryboardSampleRequests(storyboard, step, schema); + assert.equal(fixtures.length, 5); + for (const [index, payload] of fixtures.entries()) { + assert.equal((await validateStep({ schemaRef: step.schema_ref, payload })).ok, true, + `${step.id} mode ${index} must pass canonical call validation`); + assert.deepEqual(payload.evaluator, normalizeSubstitutions(original.evaluator, schema.properties.evaluator)); + assert.equal(Object.hasOwn(payload, 'target_capability_id'), index !== 4); + assert.equal(Object.hasOwn(payload, 'target_format_id'), index === 4); + assert.equal(Object.hasOwn(payload, 'message'), index === 0); + assert.equal(Object.hasOwn(payload, 'creative_manifest'), index === 0 || index === 1); + assert.equal(Object.hasOwn(payload, 'creative_id'), index === 2 || index === 4); + assert.equal(Object.hasOwn(payload, 'transformer_id'), index === 3); + assert.equal(Object.hasOwn(payload, 'config'), index === 3); + } + assert.deepEqual(step.sample_request, original, 'static preparation must not rewrite authored samples'); + } +}); + +test('build authoring validation rejects missing selector bindings and retains malformed evaluator negatives', async () => { + const { storyboard, steps } = evaluatorBuildAuthoringFixture(); + for (const original of steps.slice(0, 2)) { + const step = structuredClone(original); + const schema = loadSchema(step.schema_ref); + assert.equal((await validateStep({ schemaRef: step.schema_ref, payload: step.sample_request })).ok, false, + 'the incomplete pre-injection request must remain invalid'); + step.context_inputs = step.context_inputs.filter(input => !['target_capability_id', 'target_format_id'].includes(input.inject_at)); + for (const payload of normalizeStoryboardSampleRequests(storyboard, step, schema)) { + assert.equal((await validateStep({ schemaRef: step.schema_ref, payload })).ok, false, + 'the materializer must honor missing authored bindings rather than repair them'); + } + step.context_inputs = original.context_inputs; + const evaluator = step.sample_request.evaluator; + (evaluator.feature_agent || evaluator).agent_url = 42; + for (const payload of normalizeStoryboardSampleRequests(storyboard, step, schema)) { + assert.equal((await validateStep({ schemaRef: step.schema_ref, payload })).ok, false, + 'preparation must not repair malformed direct or nested evaluator fields'); + } + } +}); + +test('static build preparation requires the authored opt-in contract and rejects invalid injection paths', () => { + const { storyboard, steps: [step] } = evaluatorBuildAuthoringFixture(); + const schema = loadSchema(step.schema_ref); + const withoutContract = { ...step, requires_contract: undefined }; + assert.deepEqual(normalizeStoryboardSampleRequests(storyboard, withoutContract, schema), + [normalizeSubstitutions(step.sample_request, schema)]); + const malformed = structuredClone(step); + malformed.context_inputs.find(input => input.key === 'build_capability_id').inject_at = '__proto__.polluted'; + assert.throws(() => normalizeStoryboardSampleRequests(storyboard, malformed, schema), + /Invalid creative build authoring fixture path/); +}); diff --git a/tests/mcp-schema-projection.test.cjs b/tests/mcp-schema-projection.test.cjs index f1a8eedca0b..ea16c1301ea 100644 --- a/tests/mcp-schema-projection.test.cjs +++ b/tests/mcp-schema-projection.test.cjs @@ -14,7 +14,7 @@ const yaml = require('js-yaml'); const semver = require('semver'); const { reportingSummaryCases } = require('./helpers/reporting-summary-cases.cjs'); const { - normalizeSubstitutions, + normalizeStoryboardSampleRequests, } = require('../scripts/lint-storyboard-sample-request-schema.cjs'); const { MCP_ROLE_PROFILE_TOOLS, @@ -205,9 +205,9 @@ function collectStoryboardRequestFixtures() { const sourcePath = path.join(SOURCE_DIR, relativePath); if (!fs.existsSync(sourcePath)) continue; const sourceSchema = readJson(sourcePath); - const fixture = normalizeSubstitutions(step.sample_request, sourceSchema); + const prepared = normalizeStoryboardSampleRequests(storyboard, step, sourceSchema); if (!fixtures.has(relativePath)) fixtures.set(relativePath, []); - fixtures.get(relativePath).push(fixture); + fixtures.get(relativePath).push(...prepared); } } } diff --git a/tests/sdk-creative-build-probe-bridge.test.cjs b/tests/sdk-creative-build-probe-bridge.test.cjs new file mode 100644 index 00000000000..f3bf8bf38d7 --- /dev/null +++ b/tests/sdk-creative-build-probe-bridge.test.cjs @@ -0,0 +1,158 @@ +'use strict'; +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { execFileSync } = require('node:child_process'); +const { install } = require('../scripts/install-sdk-creative-build-probe.cjs'); + +const patchRoot = path.resolve(__dirname, '../patches/sdk-creative-build-probe'); +const manifest = JSON.parse(fs.readFileSync(path.join(patchRoot, 'manifest.json'), 'utf8')); +const sdkRoot = path.resolve(require.resolve('@adcp/sdk/testing'), '../../../..'); + +function originalPackage() { + // Stay inside this repository so public SDK entry points can resolve their + // ordinary dependencies from the parent node_modules. + fs.mkdirSync(path.resolve(__dirname, '../.context'), { recursive: true }); + const dir = fs.mkdtempSync(path.resolve(__dirname, '../.context/sdk-probe-test-')); + const root = path.join(dir, 'node_modules/@adcp/sdk'); + fs.cpSync(sdkRoot, root, { recursive: true }); + // A normal postinstall may already have patched the source package. Restore + // exact stock bytes for the compatibility and ignore-scripts install tests. + for (const entry of manifest.files) { + const file = path.join(root, entry.path); + if (entry.before === null) { fs.rmSync(file, { force: true }); continue; } + let contents = fs.readFileSync(file, 'utf8'); + for (const edit of [...entry.replacements].reverse()) contents = contents.replace(edit.to, () => edit.from); + fs.writeFileSync(file, contents); + } + return { dir, root }; +} + +const probe = String.raw` +const api = MODE === 'esm' ? await import('@adcp/sdk/testing') : require('@adcp/sdk/testing'); +if (api.resolveCreativeBuildProbe && typeof api.validateCreativeBuildProbeSpec !== 'function') throw Error('missing public resolver API'); +const sb = api.parseStoryboard( + 'id: build_probe_bridge\nversion: "1.0.0"\ntitle: Probe\ncategory: creative\nsummary: Bridge verification\n' + + 'creative_build_probe:\n context_prefix: build\n message: Build one sandbox creative\n' + + 'phases:\n - id: boundary\n title: Boundary\n steps:\n - id: reject\n title: Reject\n task: build_creative\n requires_contract: creative_build_probe_runner\n expect_error: true\n stateful: true\n' + + ' sample_request:\n target_capability_id: "$context.build_capability_id"\n message: "$context.build_message"\n' + + ' context_inputs:\n - key: build_creative_manifest\n inject_at: creative_manifest\n' + + ' validations:\n - check: error_code\n value: EVALUATOR_AGENT_NOT_ACCEPTED\n'); +let calls = 0; +const tools = ['get_adcp_capabilities', 'build_creative']; +const creative = { supports_generation: true, supported_formats: [ + { capability_id: 'validate', operations: ['validate'], format: { format_kind: 'image', params: {} } }, + { capability_id: 'build', operations: ['build'], format: { format_kind: 'image', params: {}, technical_requirements_complete: true } } +] }; +const options = { _profile: { tools, raw_capabilities: { creative } }, agentTools: tools, + _client: { resetContext() {}, async executeTask(task, input) { + calls++; + if (input.target_capability_id !== 'build' || input.creative_manifest?.format_kind !== 'image') throw Error('invalid binding'); + return { success: false, data: { errors: [{ code: 'INVALID_REQUEST', message: 'Wrong rejection' }] } }; + } } }; +const full = await api.runStoryboard('https://agent.example/mcp', sb, options); +const single = await api.runStoryboardStep('https://agent.example/mcp', sb, 'reject', options); +creative.supported_formats[1].format.technical_requirements_complete = false; +const unavailable = await api.runStoryboard('https://agent.example/mcp', sb, options); +const unavailableStep = await api.runStoryboardStep('https://agent.example/mcp', sb, 'reject', options); +const unopted = { ...sb }; delete unopted.creative_build_probe; +const unchanged = await api.runStoryboard('https://agent.example/mcp', unopted, options); +console.log(JSON.stringify({ full, single, unavailable, unavailableStep, unchanged, calls })); +`; + +function run(dir, mode) { + const script = `const MODE = ${JSON.stringify(mode)}; (async () => { ${probe} })().catch(e => { console.error(e); process.exit(1); });`; + return JSON.parse(execFileSync(process.execPath, ['-e', script], { cwd: dir, encoding: 'utf8' })); +} + +test('stock SDK gives no security credit; explicit trusted install supports public CJS and ESM full and standalone runs', () => { + const { dir, root } = originalPackage(); + try { + for (const mode of ['cjs', 'esm']) { + const stock = run(dir, mode); + assert.equal(stock.calls, 0); + assert.equal(stock.full.passed_count, 0); + assert.equal(stock.single.skipped, true); + } + install(root); // npm ci --ignore-scripts + explicit trusted invocation + install(root); // normal install / repeated postinstall is idempotent + const ts = require('typescript'); + for (const entry of manifest.files.filter(item => item.path.endsWith('.d.mts'))) { + const file = path.join(root, entry.path); + const declarations = fs.readFileSync(file, 'utf8'); + const refs = [...declarations.matchAll(/(?:from\s+|import\()(['"])([^'"]*creative-build-probe[^'"]*)\1/g)].map(match => match[2]); + for (const ref of refs) { + assert.ok(ref.endsWith('.mjs'), `ESM declaration needs explicit extension: ${ref}`); + const resolved = ts.resolveModuleName(ref, file, { + module: ts.ModuleKind.NodeNext, moduleResolution: ts.ModuleResolutionKind.NodeNext, + }, ts.sys, undefined, undefined, ts.ModuleKind.ESNext).resolvedModule; + assert.ok(resolved?.resolvedFileName.endsWith('.d.mts'), `NodeNext could not resolve ${ref}`); + } + } + + for (const mode of ['cjs', 'esm']) { + const result = run(dir, mode); + assert.equal(result.calls, 2, mode); + assert.equal(result.full.overall_passed, false); + assert.equal(result.full.failed_count, 1); + assert.equal(result.single.passed, false); + assert.equal(result.unavailable.passed_count, 0); + assert.equal(result.unavailable.failed_count, 0); + assert.equal(result.unavailable.coverage_gaps[0].reason, 'fixture_unsatisfied'); + assert.equal(result.unavailableStep.skip_reason, 'fixture_unsatisfied'); + assert.equal(result.unchanged.passed_count, 0); + assert.equal(result.unchanged.fixture_resolutions, undefined, 'unopted runner output remains unchanged'); + } + } finally { fs.rmSync(dir, { recursive: true, force: true }); } +}); + +test('bridge rejects version drift, late-file drift and mixed states before writing any target', () => { + const { dir, root } = originalPackage(); + try { + const first = path.join(root, manifest.files[0].path); + const original = fs.readFileSync(first, 'utf8'); + const last = path.join(root, manifest.files.at(-1).path); + fs.appendFileSync(last, '\n// drift\n'); + assert.throws(() => install(root), /content drift/); + assert.equal(fs.readFileSync(first, 'utf8'), original); + fs.writeFileSync(last, fs.readFileSync(last, 'utf8').replace('\n// drift\n', '')); + const firstEntry = manifest.files[0]; + let patched = original; + for (const edit of firstEntry.replacements) patched = patched.replace(edit.from, () => edit.to); + fs.writeFileSync(first, patched); + assert.throws(() => install(root), /mixed original\/patched/); + fs.writeFileSync(first, original); + const pkgFile = path.join(root, 'package.json'); + const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf8')); + for (const version of ['14.0.0-rc.52', '15.9.5', '99.0.0']) { + pkg.version = version; fs.writeFileSync(pkgFile, JSON.stringify(pkg)); + assert.throws(() => install(root), /requires.*14\.0\.0-rc\.53/); + assert.equal(fs.readFileSync(first, 'utf8'), original); + } + } finally { fs.rmSync(dir, { recursive: true, force: true }); } +}); + + +test('bridge rejects a payload traversal before writing any package target', () => { + const { dir, root } = originalPackage(); + try { + const first = path.join(root, manifest.files[0].path); + const original = fs.readFileSync(first, 'utf8'); + const harness = path.join(dir, 'owned-bridge'); + const harnessPatches = path.join(harness, 'patches/sdk-creative-build-probe'); + fs.mkdirSync(path.join(harness, 'scripts'), { recursive: true }); + fs.cpSync(patchRoot, harnessPatches, { recursive: true }); + const installerFile = path.join(harness, 'scripts/install-sdk-creative-build-probe.cjs'); + fs.copyFileSync(path.resolve(__dirname, '../scripts/install-sdk-creative-build-probe.cjs'), installerFile); + const altered = structuredClone(manifest); + const entry = altered.files.find(item => item.contents_file === 'creative-build-probe.js'); + // An outside payload with the correct bytes would satisfy output hashes. + // The installer must reject its path before reading or writing any target. + fs.copyFileSync(path.join(patchRoot, entry.contents_file), path.join(harness, 'patches/outside.js')); + entry.contents_file = '../outside.js'; + fs.writeFileSync(path.join(harnessPatches, 'manifest.json'), JSON.stringify(altered)); + assert.throws(() => require(installerFile).install(root), /Invalid SDK bridge contents_file/); + assert.equal(fs.readFileSync(first, 'utf8'), original); + } finally { fs.rmSync(dir, { recursive: true, force: true }); } +}); diff --git a/tests/sdk-runner-capability-gates.test.cjs b/tests/sdk-runner-capability-gates.test.cjs index 4bb392638c6..05b7e291f9b 100644 --- a/tests/sdk-runner-capability-gates.test.cjs +++ b/tests/sdk-runner-capability-gates.test.cjs @@ -47,6 +47,7 @@ test('evaluator capability contract grades individual experimental feature decla ...storyboard, prerequisites: undefined, fixtures: undefined, + creative_build_probe: undefined, phases: [{ ...contract, steps: contract.steps.map(step => ({ @@ -70,6 +71,7 @@ test('evaluator capability contract grades individual experimental feature decla const result = await runStoryboard('https://agent.example/mcp', executable, { _profile: { tools, raw_capabilities: capabilities }, agentTools: tools, + contracts: ['creative_build_probe_runner'], context: { supports_evaluator: true }, skip_controller_seeding: true, _client: {