@adcp/sdk 16.2.0 → 16.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/AGENTS.md +2 -0
  2. package/dist/lib/core/AgentClient.js +17 -1
  3. package/dist/lib/core/AgentClient.mjs +17 -1
  4. package/dist/lib/core/SingleAgentClient.js +13 -0
  5. package/dist/lib/core/SingleAgentClient.mjs +13 -0
  6. package/dist/lib/core/TaskExecutor.js +88 -13
  7. package/dist/lib/core/TaskExecutor.mjs +89 -14
  8. package/dist/lib/errors/index.d.mts +34 -0
  9. package/dist/lib/errors/index.d.ts +34 -0
  10. package/dist/lib/errors/index.js +11 -0
  11. package/dist/lib/errors/index.mjs +10 -0
  12. package/dist/lib/index.d.mts +1 -1
  13. package/dist/lib/index.d.ts +1 -1
  14. package/dist/lib/index.js +2 -0
  15. package/dist/lib/index.mjs +2 -0
  16. package/dist/lib/media-buy/compatibility.d.mts +1 -0
  17. package/dist/lib/media-buy/compatibility.d.ts +1 -0
  18. package/dist/lib/media-buy/compatibility.js +33 -10
  19. package/dist/lib/media-buy/compatibility.mjs +32 -14
  20. package/dist/lib/negotiation/buyer.js +11 -5
  21. package/dist/lib/negotiation/buyer.mjs +11 -5
  22. package/dist/lib/negotiation/types.d.mts +4 -2
  23. package/dist/lib/negotiation/types.d.ts +4 -2
  24. package/dist/lib/negotiation/verification.d.mts +15 -6
  25. package/dist/lib/negotiation/verification.d.ts +15 -6
  26. package/dist/lib/negotiation/verification.js +146 -27
  27. package/dist/lib/negotiation/verification.mjs +146 -28
  28. package/dist/lib/negotiation/version.d.mts +6 -0
  29. package/dist/lib/negotiation/version.d.ts +6 -0
  30. package/dist/lib/negotiation/version.js +60 -0
  31. package/dist/lib/negotiation/version.mjs +33 -0
  32. package/dist/lib/server/create-adcp-server.js +1 -1
  33. package/dist/lib/server/create-adcp-server.mjs +1 -1
  34. package/dist/lib/server/error-arm-tools.d.mts +10 -10
  35. package/dist/lib/server/error-arm-tools.d.ts +10 -10
  36. package/dist/lib/server/error-arm-tools.js +22 -58
  37. package/dist/lib/server/error-arm-tools.mjs +30 -65
  38. package/dist/lib/testing/compliance/comply.js +17 -0
  39. package/dist/lib/testing/compliance/comply.mjs +17 -0
  40. package/dist/lib/testing/index.d.mts +3 -0
  41. package/dist/lib/testing/index.d.ts +3 -0
  42. package/dist/lib/testing/index.js +44 -2
  43. package/dist/lib/testing/index.mjs +42 -0
  44. package/dist/lib/testing/storyboard/collection-selection.d.mts +259 -0
  45. package/dist/lib/testing/storyboard/collection-selection.d.ts +259 -0
  46. package/dist/lib/testing/storyboard/collection-selection.js +384 -0
  47. package/dist/lib/testing/storyboard/collection-selection.mjs +348 -0
  48. package/dist/lib/testing/storyboard/context.js +12 -6
  49. package/dist/lib/testing/storyboard/context.mjs +12 -6
  50. package/dist/lib/testing/storyboard/fixture-resolution.d.mts +39 -1
  51. package/dist/lib/testing/storyboard/fixture-resolution.d.ts +39 -1
  52. package/dist/lib/testing/storyboard/fixture-resolution.js +216 -12
  53. package/dist/lib/testing/storyboard/fixture-resolution.mjs +218 -12
  54. package/dist/lib/testing/storyboard/index.d.mts +9 -2
  55. package/dist/lib/testing/storyboard/index.d.ts +9 -2
  56. package/dist/lib/testing/storyboard/index.js +51 -2
  57. package/dist/lib/testing/storyboard/index.mjs +49 -1
  58. package/dist/lib/testing/storyboard/prepared-fixtures.d.mts +471 -0
  59. package/dist/lib/testing/storyboard/prepared-fixtures.d.ts +471 -0
  60. package/dist/lib/testing/storyboard/prepared-fixtures.js +434 -0
  61. package/dist/lib/testing/storyboard/prepared-fixtures.mjs +389 -0
  62. package/dist/lib/testing/storyboard/protected-context.d.mts +127 -0
  63. package/dist/lib/testing/storyboard/protected-context.d.ts +127 -0
  64. package/dist/lib/testing/storyboard/protected-context.js +301 -0
  65. package/dist/lib/testing/storyboard/protected-context.mjs +267 -0
  66. package/dist/lib/testing/storyboard/rejection-hints.js +13 -4
  67. package/dist/lib/testing/storyboard/rejection-hints.mjs +13 -4
  68. package/dist/lib/testing/storyboard/runner.d.mts +24 -0
  69. package/dist/lib/testing/storyboard/runner.d.ts +24 -0
  70. package/dist/lib/testing/storyboard/runner.js +1061 -105
  71. package/dist/lib/testing/storyboard/runner.mjs +1075 -106
  72. package/dist/lib/testing/storyboard/seeding.d.mts +15 -1
  73. package/dist/lib/testing/storyboard/seeding.d.ts +15 -1
  74. package/dist/lib/testing/storyboard/seeding.js +272 -12
  75. package/dist/lib/testing/storyboard/seeding.mjs +274 -13
  76. package/dist/lib/testing/storyboard/types.d.mts +207 -10
  77. package/dist/lib/testing/storyboard/types.d.ts +207 -10
  78. package/dist/lib/testing/storyboard/types.js +1 -1
  79. package/dist/lib/testing/storyboard/types.mjs +1 -1
  80. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  81. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  82. package/dist/lib/testing/storyboard/validations.js +95 -0
  83. package/dist/lib/testing/storyboard/validations.mjs +95 -0
  84. package/dist/lib/validation/bundled-schema-store.js +5 -0
  85. package/dist/lib/validation/bundled-schema-store.mjs +4 -0
  86. package/dist/lib/validation/schema-loader.js +19 -2
  87. package/dist/lib/validation/schema-loader.mjs +22 -3
  88. package/dist/lib/version.d.mts +3 -3
  89. package/dist/lib/version.d.ts +3 -3
  90. package/dist/lib/version.js +3 -3
  91. package/dist/lib/version.mjs +3 -3
  92. package/docs/TYPE-SUMMARY.md +1 -1
  93. package/docs/guides/COLLECTION-FIXTURE-FOUNDATION.md +850 -0
  94. package/docs/guides/PROPOSAL-TERMS-VERIFICATION.md +66 -0
  95. package/docs/llms.txt +3 -2
  96. package/docs/migration-14.x-rc-worksheet.md +4 -4
  97. package/package.json +1 -1
@@ -26,6 +26,20 @@ import {
26
26
  forwardAliasCache,
27
27
  createRunnerVariables
28
28
  } from "./context.mjs";
29
+ import {
30
+ applyProtectedContextPatch,
31
+ createProtectedContextWriter,
32
+ forwardProtectedContext,
33
+ isProtectedContextKey,
34
+ ProtectedContextViolation,
35
+ registerProtectedContext
36
+ } from "./protected-context.mjs";
37
+ import {
38
+ buildPreparedBinding,
39
+ createPreparedStoryboardFixtures,
40
+ preparedBindingRegistryFor,
41
+ resolvePreparedFixtures
42
+ } from "./prepared-fixtures.mjs";
29
43
  import { detectContextRejectionHints } from "./rejection-hints.mjs";
30
44
  import { detectShapeDriftHints } from "./shape-drift-hints.mjs";
31
45
  import { detectStrictValidationHints } from "./strict-validation-hints.mjs";
@@ -50,6 +64,7 @@ import {
50
64
  withPropertyListCredentialRedaction
51
65
  } from "./property-list-reference.mjs";
52
66
  import { ResponseSchemaValidationError } from "../../utils/response-unwrapper.mjs";
67
+ import { PreparedFixturesDeniedError } from "../../errors/index.mjs";
53
68
  import { injectLegacyEnvelopeStatus, normalizeLegacyMediaBuyStatusForReturn } from "../../utils/envelope-status-compat.mjs";
54
69
  import { queryUpstreamTraffic } from "../test-controller.mjs";
55
70
  import { IDENTIFIER_DIGEST_LIMIT } from "../../upstream-recorder/constants.mjs";
@@ -109,7 +124,9 @@ import { probeRequestSigningVector } from "./request-signing/probe-dispatch.mjs"
109
124
  import { REQUEST_SIGNING_PROBE_TASK } from "./request-signing/synthesize.mjs";
110
125
  import { createWebhookReceiver } from "./webhook-receiver.mjs";
111
126
  import { WEBHOOK_ASSERTION_TASKS, armWebhookAssertions, executeWebhookAssertionStep } from "./webhook-assertions.mjs";
112
- import { runControllerSeeding } from "./seeding.mjs";
127
+ import {
128
+ runControllerSeeding
129
+ } from "./seeding.mjs";
113
130
  import { callControllerRaw } from "../test-controller.mjs";
114
131
  import { applyFixtureBindingsToRequest } from "./fixture-resolution.mjs";
115
132
  import { getComplianceCacheDir } from "./compliance.mjs";
@@ -151,12 +168,35 @@ import {
151
168
  stepDisablesAssertion,
152
169
  validateStepInvariants
153
170
  } from "./assertions.mjs";
171
+ function tryProtectedContextPatch(context, patch, options) {
172
+ try {
173
+ applyProtectedContextPatch(context, patch, options);
174
+ return void 0;
175
+ } catch (error) {
176
+ if (!(error instanceof ProtectedContextViolation)) throw error;
177
+ return {
178
+ check: "protected_context_violation",
179
+ passed: false,
180
+ description: "Reserved fixture-preparation context keys are immutable for the rest of the run",
181
+ error: error.message,
182
+ json_pointer: null,
183
+ expected: "no write, delete or metadata change to the __adcp_fixture_ namespace",
184
+ actual: error.keys.join(", "),
185
+ // required_on_failure, and null outside `response_schema`. Omitting
186
+ // the keys is not the same as recording null — a consumer cannot
187
+ // tell "no schema applies" from "the runner forgot".
188
+ schema_id: null,
189
+ schema_url: null
190
+ };
191
+ }
192
+ }
154
193
  const SKIP_DETAILS = {
155
194
  not_applicable: "Not applicable: agent did not declare the protocol or specialism this storyboard targets.",
156
195
  no_phases: "Storyboard has no executable phases (placeholder).",
157
196
  prerequisite_failed: "Skipped: a prerequisite step or contract did not pass.",
158
197
  missing_tool: "Skipped: agent did not advertise the required tool.",
159
198
  missing_test_controller: "Skipped: deterministic_testing phase requires comply_test_controller, which the agent did not advertise.",
199
+ fixture_unsatisfied: "Skipped: every enabled strategy for a declared fixture handle was exhausted without finding or creating a qualifying entity.",
160
200
  fixture_unavailable: "Skipped: the runner test kit cannot synthesize a valid input for the seller-declared contract.",
161
201
  unsatisfied_contract: "Skipped: test-kit contract is out of scope for this grading run.",
162
202
  requirement_unmet: "Skipped: a requires: tag named a runtime requirement that is not available on this run (see RunnerSkipResult.requirement).",
@@ -518,6 +558,17 @@ function responseNotApplicableContextKeys(step, gate) {
518
558
  if (gate.context_keys?.length) return gate.context_keys;
519
559
  return (step.context_outputs ?? []).filter((o) => o.path === "pagination.cursor").map((o) => o.key).filter((key) => typeof key === "string" && key.length > 0);
520
560
  }
561
+ const FIXTURE_CAPTURE_STEP_ID = "__fixture_capture__";
562
+ function describeFixtureCaptureTask(captures, refusedKeys) {
563
+ for (const key of refusedKeys) {
564
+ const task = captures?.provenance?.[key]?.source_task;
565
+ if (typeof task === "string" && task.length > 0) return task;
566
+ }
567
+ const distinct = new Set(
568
+ Object.values(captures?.provenance ?? {}).map((entry) => entry.source_task).filter((task) => typeof task === "string" && task.length > 0)
569
+ );
570
+ return distinct.size === 1 ? [...distinct][0] : VALIDATION_ONLY_TASK;
571
+ }
521
572
  function responseDerivedContextResult(runState) {
522
573
  const entries = runState.responseDerivedNotApplicableContextKeys;
523
574
  return entries && entries.size > 0 ? { response_derived_not_applicable_context_keys: Object.fromEntries(entries) } : {};
@@ -809,27 +860,53 @@ async function runStoryboard(agentUrlOrUrls, storyboard, options = {}) {
809
860
  "Credential-isolated property-list references currently require MCP; A2A probe coverage is unavailable."
810
861
  );
811
862
  }
812
- if (hasTestClientScope() && options._client && !options.agents) {
813
- const resolution = getOrCreateClientResolution(
814
- typeof agentUrlOrUrls === "string" ? agentUrlOrUrls : agentUrlOrUrls[0],
815
- options
816
- );
817
- const discovered = await getOrDiscoverProfile(resolution.client, options);
818
- options = {
819
- ...options,
820
- _profile: discovered.step.passed ? discovered.profile : void 0,
821
- agentTools: discovered.step.passed ? discovered.profile.tools : void 0
822
- };
823
- }
863
+ storyboard = normalizeValidationOnlyTasks(storyboard);
864
+ validateStoryboardShape(storyboard);
824
865
  const schemaRoot = getRunSchemaRoot(options);
825
- const runBody = () => withServedReleaseSchemaRoots(
826
- options.servedReleaseSchemaRoots,
827
- () => runStoryboardBody(agentUrlOrUrls, storyboard, options)
828
- );
866
+ const gatedBody = async () => {
867
+ const metadataRefusal = describeReservedCallerOptions(options);
868
+ if (metadataRefusal !== void 0) {
869
+ return denyPreparedFixtures(
870
+ options,
871
+ buildPreparedRefusalRunResult(resolveBindingAgentUrls(agentUrlOrUrls, options), storyboard, {
872
+ code: "unauthorized_metadata",
873
+ detail: metadataRefusal
874
+ }),
875
+ "unauthorized_metadata"
876
+ );
877
+ }
878
+ if (options.prepared_fixtures !== void 0) {
879
+ const urls = resolveBindingAgentUrls(agentUrlOrUrls, options);
880
+ const gate = await gatePreparedFixtures(storyboard, urls, options, void 0, "run");
881
+ if (gate.used && "refusal" in gate) {
882
+ return denyPreparedFixtures(
883
+ options,
884
+ buildPreparedRefusalRunResult(urls, storyboard, gate.refusal),
885
+ gate.refusal.code
886
+ );
887
+ }
888
+ }
889
+ if (hasTestClientScope() && options._client && !options.agents) {
890
+ const resolution = getOrCreateClientResolution(
891
+ typeof agentUrlOrUrls === "string" ? agentUrlOrUrls : agentUrlOrUrls[0],
892
+ options
893
+ );
894
+ const discovered = await getOrDiscoverProfile(resolution.client, options);
895
+ options = {
896
+ ...options,
897
+ _profile: discovered.step.passed ? discovered.profile : void 0,
898
+ agentTools: discovered.step.passed ? discovered.profile.tools : void 0
899
+ };
900
+ }
901
+ return withServedReleaseSchemaRoots(
902
+ options.servedReleaseSchemaRoots,
903
+ () => runStoryboardBody(agentUrlOrUrls, storyboard, options)
904
+ );
905
+ };
829
906
  if (schemaRoot) {
830
- return await withExternalSchemaRoot(schemaRoot.adcpVersion, schemaRoot.schemaRoot, runBody);
907
+ return await withExternalSchemaRoot(schemaRoot.adcpVersion, schemaRoot.schemaRoot, gatedBody);
831
908
  }
832
- return await runBody();
909
+ return await gatedBody();
833
910
  },
834
911
  { isolate: !hasTestClientScope() }
835
912
  )
@@ -1590,10 +1667,380 @@ function runnerNoticeKey(notice) {
1590
1667
  function collectStepNotices(phases) {
1591
1668
  return phases.flatMap((phase) => phase.steps.flatMap((step) => step.notices ?? []));
1592
1669
  }
1670
+ const CREATIVE_PROBE_CONTEXT_SUFFIXES = [
1671
+ "capability_id",
1672
+ "target_format_id",
1673
+ "message",
1674
+ "creative_manifest",
1675
+ "creative_id",
1676
+ "transformer_id",
1677
+ "config"
1678
+ ];
1679
+ function createRunContext(storyboard, options) {
1680
+ const protectedWriter = createProtectedContextWriter();
1681
+ const context = {};
1682
+ registerProtectedContext(context, protectedWriter);
1683
+ applyProtectedContextPatch(
1684
+ context,
1685
+ { set: { ...storyboard.context } },
1686
+ { origin: `[${storyboard.id}] storyboard.context` }
1687
+ );
1688
+ applyProtectedContextPatch(context, { set: { ...options.context } }, { origin: "options.context" });
1689
+ if (storyboard.context) forwardAliasCache(storyboard.context, context);
1690
+ if (options.context) forwardAliasCache(options.context, context);
1691
+ return { context, protectedWriter };
1692
+ }
1693
+ function describeReservedCallerMetadata(map, location) {
1694
+ if (!map) return void 0;
1695
+ const offending = Object.keys(map).filter(
1696
+ (key) => isProtectedContextKey(key) || key === "__proto__" || key === "constructor" || key === "prototype"
1697
+ );
1698
+ if (offending.length === 0) return void 0;
1699
+ return `${location}: cannot carry reserved or prototype-sensitive key(s) ${offending.join(", ")}; fixture-preparation metadata is not caller-supplied. Rename the key to anything not starting with "__adcp_fixture_" and not named __proto__, constructor or prototype \u2014 or, if you meant to reuse a prepared graph, pass it as options.prepared_fixtures with an authorizer instead of threading its context back.`;
1700
+ }
1701
+ function describeReservedCallerOptions(options) {
1702
+ return describeReservedCallerMetadata(options.context, "options.context") ?? describeReservedCallerMetadata(options.context_provenance, "options.context_provenance") ?? describeReservedCallerMetadata(
1703
+ options.response_derived_not_applicable_context_keys,
1704
+ "options.response_derived_not_applicable_context_keys"
1705
+ );
1706
+ }
1707
+ function describeStepAuthDirective(directive) {
1708
+ if (directive === "none") return "none";
1709
+ if (typeof directive !== "object") return JSON.stringify(directive);
1710
+ const parts = [JSON.stringify(directive.type)];
1711
+ for (const [key, value] of Object.entries(directive).sort(
1712
+ ([a], [b]) => a < b ? -1 : 1
1713
+ )) {
1714
+ if (key === "type") continue;
1715
+ parts.push(JSON.stringify([key, value]));
1716
+ }
1717
+ return parts.join(";");
1718
+ }
1719
+ function resolveBindingAgentUrls(agentUrlOrUrls, options) {
1720
+ if (options.agents) return Object.values(options.agents).map((entry) => entry.url);
1721
+ return Array.isArray(agentUrlOrUrls) ? agentUrlOrUrls : [agentUrlOrUrls];
1722
+ }
1723
+ async function gatePreparedFixtures(storyboard, agentUrls, options, step, gate = "pass") {
1724
+ const outcome = await resolvePreparedFixtures({
1725
+ supplied: options.prepared_fixtures,
1726
+ // The run context is built here so the binding can report the account
1727
+ // the step's request will ACTUALLY carry. Without it the standalone
1728
+ // entry gate — the one `runStoryboardStep` uses — reported no
1729
+ // `request_account_scope` at all, and a caller-supplied or
1730
+ // context-substituted foreign account reached the seller while the
1731
+ // authorizer saw nothing to refuse. Building the context performs no
1732
+ // I/O and is what the run would build moments later anyway.
1733
+ current: buildCurrentPreparedBinding(
1734
+ storyboard,
1735
+ agentUrls,
1736
+ options,
1737
+ step,
1738
+ options,
1739
+ step !== void 0 ? createRunContext(storyboard, options).context : void 0
1740
+ ),
1741
+ ...options.prepared_fixtures_authorizer && { authorizer: options.prepared_fixtures_authorizer },
1742
+ ...step !== void 0 && { stepId: step.id },
1743
+ gate
1744
+ });
1745
+ if (outcome === void 0) return { used: false };
1746
+ if (!outcome.ok) return { used: true, refusal: outcome.refusal };
1747
+ return { used: true, state: outcome.state };
1748
+ }
1749
+ function hydratePreparedCaptures(state, context, protectedWriter, contextProvenance) {
1750
+ const captures = state.captures;
1751
+ if (Object.keys(captures).length === 0) return void 0;
1752
+ return tryProtectedContextPatch(
1753
+ context,
1754
+ { set: captures, setProvenance: state.captureProvenance },
1755
+ { writer: protectedWriter, sinks: { provenance: contextProvenance }, origin: "prepared fixture state" }
1756
+ );
1757
+ }
1758
+ async function prepareStoryboardFixtures(agentUrl, storyboard, options = {}) {
1759
+ options = applyReusableProfileOptions(options);
1760
+ return withPropertyListCredentialRedaction(
1761
+ () => withMCPConnectionScope(
1762
+ async () => {
1763
+ storyboard = normalizeValidationOnlyTasks(storyboard);
1764
+ validateStoryboardShape(storyboard);
1765
+ options = applyStoryboardVersionOptions(storyboard, options);
1766
+ options = applyFunctionalRequestSigning(options, {
1767
+ ...options.complianceDir && { complianceDir: options.complianceDir },
1768
+ ...options.adcpVersion && { version: options.adcpVersion }
1769
+ });
1770
+ options = resolveDeclaredTestKit(storyboard, options);
1771
+ options = applyNativeA2AComplianceTransportOptions(options);
1772
+ const schemaRoot = getRunSchemaRoot(options);
1773
+ const body = () => withServedReleaseSchemaRoots(
1774
+ options.servedReleaseSchemaRoots,
1775
+ () => prepareStoryboardFixturesBody(agentUrl, storyboard, options)
1776
+ );
1777
+ if (schemaRoot) {
1778
+ return await withExternalSchemaRoot(schemaRoot.adcpVersion, schemaRoot.schemaRoot, body);
1779
+ }
1780
+ return await body();
1781
+ },
1782
+ { isolate: !hasTestClientScope() }
1783
+ )
1784
+ );
1785
+ }
1786
+ function resolvePreparationRouteOptions(agentUrl, options) {
1787
+ const agents = options.agents;
1788
+ if (!agents) return options;
1789
+ const match = Object.entries(agents).find(([, entry2]) => entry2?.url === agentUrl);
1790
+ if (!match) return options;
1791
+ const [, entry] = match;
1792
+ return {
1793
+ ...options,
1794
+ ...entry?.auth !== void 0 && { auth: entry.auth },
1795
+ ...entry?.transport !== void 0 && { protocol: entry.transport }
1796
+ };
1797
+ }
1798
+ async function prepareStoryboardFixturesBody(agentUrl, storyboard, options) {
1799
+ const agentUrls = resolveBindingAgentUrls(agentUrl, options);
1800
+ const metadataRefusal = describeReservedCallerOptions(options);
1801
+ if (metadataRefusal !== void 0) {
1802
+ return buildPreparedFixtures(storyboard, agentUrls, options, void 0, {
1803
+ status: "failed",
1804
+ detail: metadataRefusal
1805
+ });
1806
+ }
1807
+ if (options.prepared_fixtures !== void 0) {
1808
+ return buildPreparedFixtures(storyboard, agentUrls, options, void 0, {
1809
+ status: "failed",
1810
+ detail: "prepareStoryboardFixtures() does not accept prepared_fixtures: it produces prepared state, it does not consume it. Passing state here authorizes nothing \u2014 the authorizer is never consulted \u2014 and preparation would re-resolve the graph against the seller anyway. Pass the state to runStoryboard() or runStoryboardStep() instead, with prepared_fixtures_authorizer. See docs/guides/COLLECTION-FIXTURE-FOUNDATION.md section 4."
1811
+ });
1812
+ }
1813
+ const callerOwnsClients = hasTestClientScope();
1814
+ const preparationOptions = resolvePreparationRouteOptions(agentUrls[0], options);
1815
+ const resolution = getOrCreateClientResolution(agentUrls[0], preparationOptions);
1816
+ const client = resolution.client;
1817
+ const bindingOptions = options;
1818
+ options = preparationOptions;
1819
+ try {
1820
+ let profile = options._profile;
1821
+ if (!profile && !resolution.reusedShared) {
1822
+ const discovered = await getOrDiscoverProfile(client, options);
1823
+ if (discovered.step.passed === false) {
1824
+ return buildPreparedFixtures(storyboard, agentUrls, bindingOptions, void 0, {
1825
+ status: "failed",
1826
+ detail: discovered.step.error ?? "agent discovery failed before fixture preparation"
1827
+ });
1828
+ }
1829
+ profile = discovered.profile;
1830
+ }
1831
+ if (profile && (!options._profile || !options.agentTools)) {
1832
+ options = {
1833
+ ...options,
1834
+ ...!options._profile && { _profile: profile },
1835
+ ...!options.agentTools && { agentTools: normalizeAgentToolNames(profile.tools) }
1836
+ };
1837
+ }
1838
+ const context = createRunContext(storyboard, options).context;
1839
+ const seeding = await runControllerSeeding(client, storyboard, options, context);
1840
+ if (!seeding) {
1841
+ return buildPreparedFixtures(storyboard, agentUrls, bindingOptions, void 0, { status: "resolved" });
1842
+ }
1843
+ const disposition = seeding.missingController ? { status: "unsupported", detail: "agent does not advertise comply_test_controller" } : seeding.seedUnsupported ? { status: "unsupported", detail: "agent did not advertise a required seed_* scenario" } : seeding.failedCount > 0 ? {
1844
+ status: "failed",
1845
+ detail: seeding.phase.steps.find((step) => step.error)?.error ?? "fixture resolution failed during preparation"
1846
+ } : seeding.fixtureUnsatisfied === true ? { status: "unsatisfied", detail: describeUnsatisfiedFixtures(seeding.resolutionRecords) } : { status: "resolved" };
1847
+ return buildPreparedFixtures(storyboard, agentUrls, bindingOptions, seeding, disposition);
1848
+ } finally {
1849
+ if (!callerOwnsClients) await closeScopedConnections(options.protocol);
1850
+ }
1851
+ }
1852
+ function describeUnsatisfiedFixtures(records) {
1853
+ const unsatisfied = (records ?? []).filter((record) => record.status === "unsatisfied");
1854
+ if (unsatisfied.length === 0) return "a declared strategy ladder exhausted without a binding";
1855
+ const names = unsatisfied.map(
1856
+ (record) => record.fixture_type === "pricing_option" && record.product_handle ? `pricing_option:${record.product_handle}/${record.handle}` : `${record.fixture_type}:${record.handle}`
1857
+ );
1858
+ return `fixture_unsatisfied: ${names.join(", ")}`;
1859
+ }
1860
+ function buildPreparedFixtures(storyboard, agentUrls, options, seeding, disposition) {
1861
+ const captures = disposition.status === "resolved" ? seeding?.preparedCaptures?.values ?? {} : {};
1862
+ const captureProvenance = disposition.status === "resolved" ? seeding?.preparedCaptures?.provenance ?? {} : {};
1863
+ const witnesses = disposition.status === "resolved" ? seeding?.preparedCaptures?.witnesses ?? {} : {};
1864
+ const unsatisfied = (seeding?.resolutionRecords ?? []).filter((record) => record.status === "unsatisfied");
1865
+ return createPreparedStoryboardFixtures({
1866
+ binding: buildCurrentPreparedBinding(storyboard, agentUrls, options),
1867
+ ...seeding?.bindings && { bindings: seeding.bindings },
1868
+ resolutions: seeding?.resolutionRecords ?? [],
1869
+ coverageGaps: unsatisfied.length > 0 ? [
1870
+ {
1871
+ reason: "fixture_unsatisfied",
1872
+ detail: "detail" in disposition ? disposition.detail : "a declared strategy ladder exhausted without a binding",
1873
+ fixtures: unsatisfied.map((record) => ({
1874
+ fixture_type: record.fixture_type,
1875
+ handle: record.handle,
1876
+ ...record.product_handle && { product_handle: record.product_handle },
1877
+ requirements: record.requirements
1878
+ }))
1879
+ }
1880
+ ] : [],
1881
+ captures,
1882
+ captureProvenance,
1883
+ witnesses,
1884
+ disposition,
1885
+ ...seeding?.phase && { phase: seeding.phase }
1886
+ });
1887
+ }
1888
+ function buildCurrentPreparedBinding(storyboard, agentUrls, options, step, runOptions, context, selectedRoute, runnerVars) {
1889
+ return buildPreparedBinding({
1890
+ storyboard,
1891
+ agentUrls,
1892
+ ...options.adcpVersion && { adcpVersion: options.adcpVersion },
1893
+ ...(runOptions ?? options).agents && {
1894
+ agentRouteMap: Object.entries((runOptions ?? options).agents ?? {}).map(
1895
+ ([key, entry]) => [key, entry?.url ?? ""]
1896
+ )
1897
+ },
1898
+ accountScope: resolveBindingAccountScope(storyboard, options),
1899
+ brandScope: resolveBindingBrandScope(storyboard, options),
1900
+ ...step !== void 0 && { ...resolveBindingRequestAccountScope(step, context, options, runnerVars) },
1901
+ sandbox: !(options.disable_sandbox === true || options.sandbox === false),
1902
+ ...options.prepared_fixtures_principal !== void 0 && { principal: options.prepared_fixtures_principal },
1903
+ // The diffed credential always comes from the RUN options. A routed
1904
+ // dispatch flattens `agents[key].auth` into a bare `auth` on its
1905
+ // per-step view, so deriving it from `assignment.options` would make an
1906
+ // unchanged routed credential read as a mismatch and refuse every step
1907
+ // of every routed run. The step's own view feeds the separate,
1908
+ // undiffed `step_credential_fingerprint` instead.
1909
+ credential: resolveBindingCredential(runOptions ?? options),
1910
+ ...(() => {
1911
+ if (step === void 0) return {};
1912
+ const stepCredential = resolveBindingCredential(options, step);
1913
+ if (stepCredential === resolveBindingCredential(runOptions ?? options)) return {};
1914
+ return {
1915
+ stepCredential,
1916
+ // The fingerprint is an HMAC under a per-process key, so a host can
1917
+ // observe that it changed but never what it is. This companion is the
1918
+ // value an authorizer can actually branch on.
1919
+ stepAuthKind: step.auth === void 0 ? "routed" : step.auth === "none" ? "none" : "override"
1920
+ };
1921
+ })(),
1922
+ ...step !== void 0 && runOptions !== void 0 && (runOptions.agents || selectedRoute) ? resolveBindingStepRoute(step, runOptions, selectedRoute) : {},
1923
+ // Never mint one here: `current.run_id` is handed to the authorizer as
1924
+ // "identity of the preparation run", and a fresh UUID per lookup is
1925
+ // noise for a host trying to correlate on it.
1926
+ runId: options.prepared_fixtures_run_id ?? ""
1927
+ });
1928
+ }
1929
+ function resolveBindingStepRoute(step, runOptions, selected) {
1930
+ if (typeof selected?.agentUrl === "string" && selected.agentUrl.length > 0) {
1931
+ return { stepRoute: `${selected.agentKey ?? "#default"}@${redactOAuthUrlForOutput(selected.agentUrl)}` };
1932
+ }
1933
+ const agents = runOptions.agents ?? {};
1934
+ const key = step.agent;
1935
+ if (typeof key !== "string" || !Object.hasOwn(agents, key)) return {};
1936
+ const url = agents[key]?.url;
1937
+ if (typeof url !== "string") return {};
1938
+ return { stepRoute: `${key}@${redactOAuthUrlForOutput(url)}` };
1939
+ }
1940
+ function resolveBindingRequestAccountScope(step, context, options, runnerVars) {
1941
+ if (!context) return {};
1942
+ let account;
1943
+ try {
1944
+ const resolvedTask = resolveTaskName(step, options) ?? step.task;
1945
+ const effectiveStep = resolvedTask === step.task ? step : { ...step, task: resolvedTask };
1946
+ account = buildStepRequest(step, effectiveStep, context, options, runnerVars).account;
1947
+ } catch {
1948
+ return {};
1949
+ }
1950
+ if (account === void 0 || account === null) return {};
1951
+ return { requestAccountScope: canonicalizeScope(account) };
1952
+ }
1953
+ function resolveBindingBrandScope(storyboard, options) {
1954
+ const merged = { ...storyboard.context, ...options.context };
1955
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1956
+ const contextAccount = record(merged.account);
1957
+ const brand = record(merged.brand) ?? record(contextAccount?.brand) ?? resolveBrand(options);
1958
+ return canonicalizeScope(brand ?? {});
1959
+ }
1960
+ function canonicalizeScope(value) {
1961
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
1962
+ if (Array.isArray(value)) return `[${value.map(canonicalizeScope).join(",")}]`;
1963
+ return `{${Object.entries(value).filter(([, nested]) => nested !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, nested]) => `${JSON.stringify(key)}:${canonicalizeScope(nested)}`).join(",")}}`;
1964
+ }
1965
+ function resolveBindingAccountScope(storyboard, options) {
1966
+ const merged = { ...storyboard.context, ...options.context };
1967
+ const contextAccount = merged.account !== null && typeof merged.account === "object" && !Array.isArray(merged.account) ? merged.account : void 0;
1968
+ const account = contextAccount ?? resolveAccount(options);
1969
+ return canonicalizeScope(account ?? {});
1970
+ }
1971
+ function resolveBindingCredential(options, step) {
1972
+ const parts = [];
1973
+ if (step?.auth !== void 0) {
1974
+ parts.push(`step_auth:${JSON.stringify(describeStepAuthDirective(step.auth))}`);
1975
+ }
1976
+ const headerEntries = Object.entries(options.headers ?? {}).sort(([a], [b]) => {
1977
+ const left = a.toLowerCase();
1978
+ const right = b.toLowerCase();
1979
+ return left < right ? -1 : left > right ? 1 : 0;
1980
+ });
1981
+ for (const [name, value] of headerEntries) {
1982
+ if (value !== void 0 && value !== null)
1983
+ parts.push(`header:${JSON.stringify([name.toLowerCase(), String(value)])}`);
1984
+ }
1985
+ const describeAuth = (auth, label) => {
1986
+ if (!auth) return;
1987
+ if (auth.type === "bearer") parts.push(`${label}:bearer:${JSON.stringify(auth.token)}`);
1988
+ else if (auth.type === "basic") parts.push(`${label}:basic:${JSON.stringify([auth.username, auth.password])}`);
1989
+ else if (auth.type === "oauth") {
1990
+ parts.push(
1991
+ `${label}:oauth:${JSON.stringify([
1992
+ auth.client?.client_id ?? null,
1993
+ typeof auth.tokens?.refresh_token === "string" ? auth.tokens.refresh_token : null,
1994
+ typeof auth.tokens?.refresh_token === "string" ? null : auth.tokens ?? null
1995
+ ])}`
1996
+ );
1997
+ } else {
1998
+ parts.push(
1999
+ `${label}:oauth_cc:${JSON.stringify([
2000
+ auth.credentials?.client_id ?? null,
2001
+ auth.credentials?.client_secret ?? null
2002
+ ])}`
2003
+ );
2004
+ }
2005
+ };
2006
+ describeAuth(options.auth, "run");
2007
+ for (const [key, entry] of Object.entries(options.agents ?? {}).sort(([a], [b]) => a < b ? -1 : 1)) {
2008
+ describeAuth(entry?.auth, `agent:${JSON.stringify([key, entry?.url ?? null])}`);
2009
+ }
2010
+ const kit = options.test_kit;
2011
+ if (kit) {
2012
+ for (const [key, value] of Object.entries(kit).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
2013
+ if (key === "auth") {
2014
+ for (const [authField, authValue] of Object.entries(value ?? {}).sort(
2015
+ ([a], [b]) => a < b ? -1 : a > b ? 1 : 0
2016
+ )) {
2017
+ if (authField === "probe_task") continue;
2018
+ if (authValue !== void 0 && authValue !== null) {
2019
+ parts.push(`test_kit:${JSON.stringify([`auth.${authField}`, authValue])}`);
2020
+ }
2021
+ }
2022
+ continue;
2023
+ }
2024
+ if (value !== void 0 && value !== null) parts.push(`test_kit:${JSON.stringify([key, value])}`);
2025
+ }
2026
+ }
2027
+ return parts.length > 0 ? parts.join("\0") : "none";
2028
+ }
1593
2029
  async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOffset, preSeeded) {
1594
2030
  const start = Date.now();
1595
2031
  const isMultiInstance = agentUrls.length > 1;
1596
2032
  const useRouting = options.agents !== void 0;
2033
+ let preparedGate = { used: false };
2034
+ if (options.prepared_fixtures !== void 0) {
2035
+ preparedGate = await gatePreparedFixtures(storyboard, agentUrls, options);
2036
+ if (preparedGate.used && "refusal" in preparedGate) {
2037
+ return denyPreparedFixtures(
2038
+ options,
2039
+ buildPreparedRefusalRunResult(agentUrls, storyboard, preparedGate.refusal),
2040
+ preparedGate.refusal.code
2041
+ );
2042
+ }
2043
+ }
1597
2044
  let clients;
1598
2045
  let routingContext;
1599
2046
  let profile;
@@ -1660,7 +2107,7 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
1660
2107
  }
1661
2108
  }
1662
2109
  }
1663
- if (preSeeded === void 0) {
2110
+ if (preSeeded === void 0 && !preparedGate.used) {
1664
2111
  const resetTargets = routingContext && options.agents ? [...routingContext.profiles.entries()].map(([key, profile2]) => ({
1665
2112
  client: routingContext.clients.get(key),
1666
2113
  options: routedAgentOptions(options.agents[key], options, profile2)
@@ -1695,7 +2142,9 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
1695
2142
  const routedControllerRoutes = /* @__PURE__ */ new Map();
1696
2143
  const routedErrors = /* @__PURE__ */ new Map();
1697
2144
  const reportedRoutingErrors = /* @__PURE__ */ new Set();
1698
- let context = { ...storyboard.context, ...options.context };
2145
+ const runContext = createRunContext(storyboard, options);
2146
+ const protectedWriter = runContext.protectedWriter;
2147
+ let context = runContext.context;
1699
2148
  const routingFailedStep = (step, phaseId, err) => {
1700
2149
  const detail = redactOAuthUrlsInText(
1701
2150
  err instanceof RoutingError ? err.message : err?.message ?? String(err)
@@ -1892,8 +2341,6 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
1892
2341
  };
1893
2342
  }
1894
2343
  }
1895
- if (storyboard.context) forwardAliasCache(storyboard.context, context);
1896
- if (options.context) forwardAliasCache(options.context, context);
1897
2344
  const contributions = /* @__PURE__ */ new Set();
1898
2345
  const contributionSources = /* @__PURE__ */ new Map();
1899
2346
  const priorStepResults = /* @__PURE__ */ new Map();
@@ -2051,6 +2498,7 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2051
2498
  skippedCount++;
2052
2499
  }
2053
2500
  let seedingPhaseResult = null;
2501
+ let probeViolationStep;
2054
2502
  let seedingFailed = false;
2055
2503
  let creativeBuildCatalogFailed = false;
2056
2504
  let seedingMissingController = false;
@@ -2137,18 +2585,38 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2137
2585
  ]
2138
2586
  };
2139
2587
  } else if (resolution.status === "resolved") {
2140
- for (const key of [
2141
- "capability_id",
2142
- "target_format_id",
2143
- "message",
2144
- "creative_manifest",
2145
- "creative_id",
2146
- "transformer_id",
2147
- "config"
2148
- ]) {
2149
- delete context[`${handle}_${key}`];
2588
+ const probeViolation = tryProtectedContextPatch(
2589
+ context,
2590
+ {
2591
+ unset: CREATIVE_PROBE_CONTEXT_SUFFIXES.map((key) => `${handle}_${key}`),
2592
+ set: resolution.context
2593
+ },
2594
+ { origin: `creative_build_probe '${handle}'` }
2595
+ );
2596
+ if (probeViolation) {
2597
+ seedingFailed = true;
2598
+ failedCount += 1;
2599
+ probeViolationStep = {
2600
+ storyboard_id: storyboard.id,
2601
+ phase_id: "fixture_resolution",
2602
+ step_id: `creative_build_probe_context.${handle}`,
2603
+ title: "Publish creative build probe context",
2604
+ task: "get_adcp_capabilities",
2605
+ passed: false,
2606
+ duration_ms: 0,
2607
+ validations: [probeViolation],
2608
+ context: withoutProtectedCaptures(context),
2609
+ extraction: { path: "none" },
2610
+ error: probeViolation.error
2611
+ };
2612
+ seedingPhaseResult = {
2613
+ phase_id: "fixture_resolution",
2614
+ phase_title: "Creative build fixture resolution",
2615
+ passed: false,
2616
+ duration_ms: 0,
2617
+ steps: [probeViolationStep]
2618
+ };
2150
2619
  }
2151
- Object.assign(context, resolution.context);
2152
2620
  options = { ...options, contracts: [.../* @__PURE__ */ new Set([...options.contracts ?? [], "creative_build_probe_runner"])] };
2153
2621
  } else {
2154
2622
  fixtureUnsatisfied = true;
@@ -2160,10 +2628,45 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2160
2628
  };
2161
2629
  }
2162
2630
  }
2631
+ let preparedFixtureState;
2632
+ let preparedRefused;
2633
+ {
2634
+ const prepared = preparedGate;
2635
+ if (prepared.used && "state" in prepared) {
2636
+ const hydrationViolation = hydratePreparedCaptures(prepared.state, context, protectedWriter, contextProvenance);
2637
+ if (hydrationViolation) {
2638
+ if (!callerOwnsClients) await closeScopedConnections(options.protocol);
2639
+ return buildDiscoveryFailedResult(agentUrls, storyboard, {
2640
+ step: "Publish prepared fixture captures",
2641
+ passed: false,
2642
+ duration_ms: 0,
2643
+ error: hydrationViolation.error
2644
+ });
2645
+ }
2646
+ preparedFixtureState = prepared.state;
2647
+ fixtureBindings = preparedBindingRegistryFor(prepared.state);
2648
+ fixtureResolutionRecords = prepared.state.resolutions;
2649
+ const disposition = prepared.state.disposition;
2650
+ if (disposition.status === "failed") {
2651
+ if (!callerOwnsClients) await closeScopedConnections(options.protocol);
2652
+ return buildDiscoveryFailedResult(agentUrls, storyboard, {
2653
+ step: "Reuse prepared fixture state",
2654
+ passed: false,
2655
+ duration_ms: 0,
2656
+ error: disposition.detail
2657
+ });
2658
+ }
2659
+ if (disposition.status === "unsatisfied") {
2660
+ fixtureUnsatisfied = true;
2661
+ fixtureCoverageGap = prepared.state.coverageGaps[0] ?? fixtureCoverageGap;
2662
+ }
2663
+ if (disposition.status === "unsupported") seedingMissingController = true;
2664
+ }
2665
+ }
2163
2666
  {
2164
2667
  let seeding;
2165
2668
  try {
2166
- seeding = creativeAssetFixtureGap || fixtureUnsatisfied || seedingFailed || skipControllerSeedingForPhaseGates ? null : preSeeded !== void 0 ? preSeeded.result : await runControllerSeeding(
2669
+ seeding = preparedFixtureState !== void 0 || creativeAssetFixtureGap || fixtureUnsatisfied || seedingFailed || skipControllerSeedingForPhaseGates ? null : preSeeded !== void 0 ? preSeeded.result : await runControllerSeeding(
2167
2670
  clients[0],
2168
2671
  storyboard,
2169
2672
  options,
@@ -2217,6 +2720,10 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2217
2720
  const attach = preSeeded === void 0 || preSeeded.attach;
2218
2721
  if (attach) {
2219
2722
  seedingPhaseResult = seeding.phase;
2723
+ if (probeViolationStep && !seeding.phase.steps.some((entry) => entry.step_id === probeViolationStep.step_id)) {
2724
+ seeding.phase.steps.unshift({ ...probeViolationStep, phase_id: seeding.phase.phase_id });
2725
+ seeding.phase.passed = false;
2726
+ }
2220
2727
  passedCount += seeding.passedCount;
2221
2728
  failedCount += seeding.failedCount;
2222
2729
  if (seeding.missingController || seeding.seedUnsupported || seeding.fixtureUnsatisfied) {
@@ -2224,6 +2731,56 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2224
2731
  }
2225
2732
  }
2226
2733
  fixtureBindings = seeding.bindings;
2734
+ if (seeding.preparedCaptures) {
2735
+ try {
2736
+ applyProtectedContextPatch(
2737
+ context,
2738
+ { set: seeding.preparedCaptures.values, setProvenance: seeding.preparedCaptures.provenance },
2739
+ {
2740
+ writer: protectedWriter,
2741
+ sinks: { provenance: contextProvenance },
2742
+ origin: "fixture preparation"
2743
+ }
2744
+ );
2745
+ } catch (error) {
2746
+ if (!(error instanceof ProtectedContextViolation)) throw error;
2747
+ seedingFailed = true;
2748
+ seeding.phase.passed = false;
2749
+ const alreadyReported = seeding.phase.steps.some((entry) => entry.step_id === FIXTURE_CAPTURE_STEP_ID);
2750
+ if (attach && !alreadyReported) {
2751
+ seeding.phase.steps.push({
2752
+ storyboard_id: storyboard.id,
2753
+ step_id: FIXTURE_CAPTURE_STEP_ID,
2754
+ phase_id: seeding.phase.phase_id,
2755
+ title: "Publish fixture captures",
2756
+ task: describeFixtureCaptureTask(seeding.preparedCaptures, error.keys),
2757
+ passed: false,
2758
+ duration_ms: 0,
2759
+ validations: [
2760
+ {
2761
+ check: "fixture_capture_unbuildable",
2762
+ passed: false,
2763
+ description: "Every declared fixture capture must be detachable from the pinned entity",
2764
+ error: error.message,
2765
+ json_pointer: null,
2766
+ expected: "a capture the runner can deep-clone and freeze",
2767
+ actual: error.keys.join(", "),
2768
+ schema_id: null,
2769
+ schema_url: null
2770
+ }
2771
+ ],
2772
+ // The commit failed, so nothing reserved is in `context` yet —
2773
+ // but the projection is applied unconditionally so a partially
2774
+ // applied patch can never surface a protected value in a
2775
+ // report.
2776
+ context: withoutProtectedCaptures(context),
2777
+ extraction: { path: "none" },
2778
+ error: error.message
2779
+ });
2780
+ failedCount += 1;
2781
+ }
2782
+ }
2783
+ }
2227
2784
  fixtureResolutionRecords = fixtureResolutionRecords ? [...fixtureResolutionRecords, ...seeding.resolutionRecords ?? []] : seeding.resolutionRecords;
2228
2785
  fixtureUnsatisfied = seeding.fixtureUnsatisfied === true && seeding.failedCount === 0;
2229
2786
  if (fixtureUnsatisfied) {
@@ -2242,12 +2799,13 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2242
2799
  ...record.product_handle && { product_handle: record.product_handle },
2243
2800
  requirements: record.requirements
2244
2801
  }));
2245
- const names = fixtures.map(
2246
- (fixture) => fixture.fixture_type === "pricing_option" ? `pricing_option "${fixture.product_handle}/${fixture.handle}"` : `product "${fixture.handle}"`
2247
- );
2248
2802
  fixtureCoverageGap = {
2249
2803
  reason: "fixture_unsatisfied",
2250
- detail: `fixture_unsatisfied: no seller fixture satisfied ${names.join(", ")}`,
2804
+ // The SAME spelling the disposition uses. Two renderings of one
2805
+ // reason over the same data meant a consumer parsing the
2806
+ // documented `<fixture_type>:<handle>` grammar matched on one
2807
+ // field and not the other.
2808
+ detail: describeUnsatisfiedFixtures(fixtureResolutionRecords),
2251
2809
  fixtures
2252
2810
  };
2253
2811
  }
@@ -2458,7 +3016,9 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2458
3016
  if (!routingContext && phaseContainsOauthMetadataProbe(phase) && !agentAdvertisesOauth(profile)) {
2459
3017
  phaseAbsent = true;
2460
3018
  }
3019
+ const priorPhaseContext = context;
2461
3020
  context = { ...context };
3021
+ forwardProtectedContext(priorPhaseContext, context);
2462
3022
  if (seedingMissingController || seedingUnsupported || seedingFailed) {
2463
3023
  const cascadeSkip = seedingMissingController ? {
2464
3024
  skip_reason: "missing_test_controller",
@@ -2630,7 +3190,7 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2630
3190
  );
2631
3191
  const missingKeys = /* @__PURE__ */ new Set([
2632
3192
  ...(effectiveRequest.ok ? findUnresolvedContextVars(effectiveRequest.request) : []).filter((v) => hardKeys.has(v.key)).map((v) => v.key),
2633
- ...(step.context_inputs ?? []).filter((input) => !(input.key in context) && hardKeys.has(input.key)).map((input) => input.key)
3193
+ ...(step.context_inputs ?? []).filter((input) => !Object.hasOwn(context, input.key) && hardKeys.has(input.key)).map((input) => input.key)
2634
3194
  ]);
2635
3195
  if (missingKeys.size > 0) {
2636
3196
  capabilityUnavailable = false;
@@ -2691,6 +3251,78 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2691
3251
  missingPrerequisiteContextKeysByPhase
2692
3252
  );
2693
3253
  }
3254
+ if (preparedFixtureState !== void 0) {
3255
+ const recheck = await resolvePreparedFixtures({
3256
+ supplied: options.prepared_fixtures,
3257
+ // The EFFECTIVE scope for this step: the routed options actually
3258
+ // dispatching it, plus its own auth directive.
3259
+ current: buildCurrentPreparedBinding(
3260
+ storyboard,
3261
+ agentUrls,
3262
+ assignment.options ?? options,
3263
+ step,
3264
+ options,
3265
+ context,
3266
+ {
3267
+ ...assignment.agentKey !== void 0 && { agentKey: assignment.agentKey },
3268
+ ...assignment.agentUrl !== void 0 && { agentUrl: assignment.agentUrl }
3269
+ },
3270
+ runnerVars
3271
+ ),
3272
+ ...options.prepared_fixtures_authorizer && { authorizer: options.prepared_fixtures_authorizer },
3273
+ stepId: step.id,
3274
+ gate: "step"
3275
+ });
3276
+ if (recheck !== void 0 && !recheck.ok) {
3277
+ const refusalResult = {
3278
+ storyboard_id: storyboard.id,
3279
+ step_id: step.id,
3280
+ phase_id: phase.id,
3281
+ title: step.title,
3282
+ task: step.task,
3283
+ passed: false,
3284
+ duration_ms: 0,
3285
+ validations: [
3286
+ {
3287
+ check: "prepared_fixtures_authorization",
3288
+ passed: false,
3289
+ // One description for all three gates. Three spellings of
3290
+ // the same check read to a report renderer as three kinds.
3291
+ description: "Prepared fixture state is re-validated and re-authorized on every lookup",
3292
+ error: recheck.refusal.detail,
3293
+ json_pointer: null,
3294
+ expected: "an authorized prepared state bound to this exact step scope",
3295
+ actual: recheck.refusal.code,
3296
+ schema_id: null,
3297
+ schema_url: null,
3298
+ // Explicit nulls, not omission: this is a PRE-WIRE
3299
+ // refusal, and an absent key cannot be told apart from a
3300
+ // runner that forgot to record one.
3301
+ request: null,
3302
+ response: null
3303
+ }
3304
+ ],
3305
+ // UNREDACTED. This result is never returned to the refused
3306
+ // caller — it goes to the host's authorized audit scope, where
3307
+ // withholding the evidence would defeat the point.
3308
+ context,
3309
+ extraction: { path: "none" },
3310
+ error: recheck.refusal.detail
3311
+ };
3312
+ stepResults.push(refusalResult);
3313
+ preparedRefused = recheck.refusal.code;
3314
+ failedCount++;
3315
+ phasePassed = false;
3316
+ countedAsFailed.add(refusalResult);
3317
+ priorStepResults.set(step.id, refusalResult);
3318
+ if (step.stateful) phaseStatefulCascades.set(phase.id, null);
3319
+ if (isMultiInstance || useRouting) {
3320
+ refusalResult.agent_url = redactOAuthUrlForOutput(assignment.agentUrl);
3321
+ refusalResult.agent_index = assignment.instanceIndex + 1;
3322
+ }
3323
+ continue;
3324
+ }
3325
+ }
2694
3326
  const rawResult = routedCapabilityDetail !== void 0 ? buildPhaseCapabilitySkippedSteps(
2695
3327
  storyboard,
2696
3328
  { ...phase, steps: [step] },
@@ -2734,7 +3366,10 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2734
3366
  });
2735
3367
  continue;
2736
3368
  }
2737
- const raw = await spec.onStep(assertionContexts.get(spec.id), result2);
3369
+ const raw = await spec.onStep(assertionContexts.get(spec.id), {
3370
+ ...result2,
3371
+ context: withoutProtectedCaptures(result2.context ?? {})
3372
+ });
2738
3373
  for (const r of raw) {
2739
3374
  const full = { ...r, assertion_id: spec.id, scope: "step", step_id: step.id };
2740
3375
  assertionResults.push(full);
@@ -3012,6 +3647,8 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
3012
3647
  ...assertionResults.length > 0 ? { assertions: assertionResults } : {},
3013
3648
  strict_validation_summary: strictSummary,
3014
3649
  notices,
3650
+ // `fixture_resolutions` carries `seller_ids`, so it is withheld on the
3651
+ // same condition as the context projection above.
3015
3652
  ...fixtureResolutionRecords && { fixture_resolutions: fixtureResolutionRecords },
3016
3653
  ...fixtureCoverageGap && { coverage_gaps: [fixtureCoverageGap] },
3017
3654
  ...routingContext && routingContext.discoveryFailures.length > 0 ? {
@@ -3026,14 +3663,26 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
3026
3663
  await closeScopedConnections(options.protocol);
3027
3664
  }
3028
3665
  if (webhookReceiver) await webhookReceiver.close();
3666
+ if (preparedRefused !== void 0) {
3667
+ await emitPreparedFixturesAudit(options, result);
3668
+ throw new PreparedFixturesDeniedError(preparedRefused);
3669
+ }
3029
3670
  return result;
3030
3671
  }
3031
3672
  async function runMultiPass(agentUrls, storyboard, options) {
3032
3673
  const start = Date.now();
3674
+ if (options.prepared_fixtures !== void 0) {
3675
+ const gate = await gatePreparedFixtures(storyboard, agentUrls, options, void 0, "run");
3676
+ if (gate.used && "refusal" in gate) {
3677
+ return denyPreparedFixtures(
3678
+ options,
3679
+ buildPreparedRefusalRunResult(agentUrls, storyboard, gate.refusal),
3680
+ gate.refusal.code
3681
+ );
3682
+ }
3683
+ }
3033
3684
  const preSeedClients = agentUrls.map((url) => getOrCreateClientResolution(url, options).client);
3034
- const preSeedContext = { ...storyboard.context, ...options.context };
3035
- if (storyboard.context) forwardAliasCache(storyboard.context, preSeedContext);
3036
- if (options.context) forwardAliasCache(options.context, preSeedContext);
3685
+ const preSeedContext = createRunContext(storyboard, options).context;
3037
3686
  let preSeedProfile = options._profile;
3038
3687
  if (!preSeedProfile) {
3039
3688
  const discovered = await getOrDiscoverProfile(preSeedClients[0], options);
@@ -3050,13 +3699,15 @@ async function runMultiPass(agentUrls, storyboard, options) {
3050
3699
  ...options.agentTools ? {} : { agentTools: normalizeAgentToolNames(preSeedProfile.tools) }
3051
3700
  };
3052
3701
  }
3053
- const resetFailure = await resetControllerStates(
3054
- preSeedClients.map((client) => ({ client, options })),
3055
- storyboard
3056
- );
3057
- if (resetFailure) {
3058
- await closeScopedConnections(options.protocol);
3059
- return buildDiscoveryFailedResult(agentUrls, storyboard, resetFailure);
3702
+ if (options.prepared_fixtures === void 0) {
3703
+ const resetFailure = await resetControllerStates(
3704
+ preSeedClients.map((client) => ({ client, options })),
3705
+ storyboard
3706
+ );
3707
+ if (resetFailure) {
3708
+ await closeScopedConnections(options.protocol);
3709
+ return buildDiscoveryFailedResult(agentUrls, storyboard, resetFailure);
3710
+ }
3060
3711
  }
3061
3712
  if (storyboardCapabilityPredicates(storyboard).length > 0) {
3062
3713
  const unmetDetail = evaluateStoryboardCapabilityGates(
@@ -3111,7 +3762,13 @@ async function runMultiPass(agentUrls, storyboard, options) {
3111
3762
  },
3112
3763
  preSeedExcludedPhaseIds
3113
3764
  );
3114
- const preSeededResult = preSeedFixtureGap || allExecutablePhasesCapabilitySkipped(storyboard, phaseCapabilitySkipDetails) ? null : await runControllerSeeding(preSeedClients[0], storyboard, options, preSeedContext);
3765
+ const preSeededResult = (
3766
+ // A pinned graph is authoritative across every pass. Re-seeding would
3767
+ // issue fresh `seed_*` mutations that replace the very seller state the
3768
+ // prepared captures describe, while the passes keep the original
3769
+ // bindings — the reset is guarded above for the same reason.
3770
+ options.prepared_fixtures !== void 0 || preSeedFixtureGap || allExecutablePhasesCapabilitySkipped(storyboard, phaseCapabilitySkipDetails) ? null : await runControllerSeeding(preSeedClients[0], storyboard, options, preSeedContext)
3771
+ );
3115
3772
  const passes = [];
3116
3773
  const passResults = [];
3117
3774
  for (let passIdx = 0; passIdx < agentUrls.length; passIdx++) {
@@ -3164,6 +3821,10 @@ async function runMultiPass(agentUrls, storyboard, options) {
3164
3821
  tested_at: (/* @__PURE__ */ new Date()).toISOString(),
3165
3822
  ...schemasDedup.length > 0 ? { schemas_used: schemasDedup } : {},
3166
3823
  ...assertionsAgg.length > 0 ? { assertions: assertionsAgg } : {},
3824
+ // `fixture_resolutions` carries `seller_ids`, so the aggregate withholds
3825
+ // it on the same condition the single-pass result does. Projecting
3826
+ // `phases` and `context` but republishing the pinned seller ids here
3827
+ // withheld the copies and published the original.
3167
3828
  ...first.fixture_resolutions ? { fixture_resolutions: first.fixture_resolutions } : {},
3168
3829
  ...first.coverage_gaps ? { coverage_gaps: first.coverage_gaps } : {},
3169
3830
  notices: noticesDedup
@@ -3293,8 +3954,200 @@ async function runStoryboardStep(agentUrl, storyboard, stepId, options = {}) {
3293
3954
  )
3294
3955
  );
3295
3956
  }
3957
+ function withoutProtectedCaptures(context) {
3958
+ const projected = {};
3959
+ for (const [key, value] of Object.entries(context)) {
3960
+ if (!isProtectedContextKey(key)) projected[key] = value;
3961
+ }
3962
+ return projected;
3963
+ }
3964
+ async function denyPreparedFixtures(options, record, refusalCode) {
3965
+ await emitPreparedFixturesAudit(options, record);
3966
+ throw new PreparedFixturesDeniedError(refusalCode);
3967
+ }
3968
+ async function emitPreparedFixturesAudit(options, record) {
3969
+ const sink = options.prepared_fixtures_audit_sink;
3970
+ if (!sink) return;
3971
+ try {
3972
+ await sink(record);
3973
+ } catch {
3974
+ }
3975
+ }
3976
+ function buildPreparedRefusalRunResult(agentUrls, storyboard, refusal) {
3977
+ const base = buildDiscoveryFailedResult(agentUrls, storyboard, {
3978
+ step: "Authorize prepared fixture state",
3979
+ passed: false,
3980
+ duration_ms: 0,
3981
+ error: refusal.detail
3982
+ });
3983
+ const validation = {
3984
+ check: "prepared_fixtures_authorization",
3985
+ passed: false,
3986
+ description: "Prepared fixture state is re-validated and re-authorized on every lookup",
3987
+ error: refusal.detail,
3988
+ json_pointer: null,
3989
+ expected: "an authorized prepared state bound to this exact run scope",
3990
+ actual: refusal.code,
3991
+ schema_id: null,
3992
+ schema_url: null,
3993
+ // Explicit nulls. This gate precedes all seller contact, so the
3994
+ // exchange genuinely did not happen — and an omitted key cannot be
3995
+ // told apart from a runner that forgot to record one.
3996
+ request: null,
3997
+ response: null
3998
+ };
3999
+ return {
4000
+ ...base,
4001
+ phases: base.phases.map((phase) => ({
4002
+ ...phase,
4003
+ steps: phase.steps.map((step) => ({ ...step, validations: [...step.validations, validation] }))
4004
+ }))
4005
+ };
4006
+ }
4007
+ function buildPreparedRefusalStepResult(storyboard, found, context, refusal, contributions) {
4008
+ return {
4009
+ storyboard_id: storyboard.id,
4010
+ step_id: found.step.id,
4011
+ phase_id: found.phaseId,
4012
+ title: found.step.title,
4013
+ task: found.step.task,
4014
+ passed: false,
4015
+ duration_ms: 0,
4016
+ validations: [
4017
+ {
4018
+ check: "prepared_fixtures_authorization",
4019
+ passed: false,
4020
+ description: "Prepared fixture state is re-validated and re-authorized on every lookup",
4021
+ error: refusal.detail,
4022
+ json_pointer: null,
4023
+ // `step scope`, matching the per-step gate. This builds a STEP
4024
+ // result; saying `run scope` here made the published grammar
4025
+ // disagree with itself depending on which gate refused.
4026
+ expected: "an authorized prepared state bound to this exact step scope",
4027
+ actual: refusal.code,
4028
+ schema_id: null,
4029
+ schema_url: null,
4030
+ request: null,
4031
+ response: null
4032
+ }
4033
+ ],
4034
+ // UNREDACTED: this record is the host's audit evidence, never the
4035
+ // refused caller's report.
4036
+ context,
4037
+ extraction: { path: "none" },
4038
+ contributions: Array.from(contributions),
4039
+ error: refusal.detail
4040
+ };
4041
+ }
4042
+ function buildPreparedDispositionStepResult(storyboard, found, context, disposition, contributions) {
4043
+ const detail = "detail" in disposition ? disposition.detail : "fixture preparation did not resolve";
4044
+ if (disposition.status === "failed") {
4045
+ return {
4046
+ storyboard_id: storyboard.id,
4047
+ step_id: found.step.id,
4048
+ phase_id: found.phaseId,
4049
+ title: found.step.title,
4050
+ task: found.step.task,
4051
+ passed: false,
4052
+ duration_ms: 0,
4053
+ // A failed step MUST carry at least one failing validation
4054
+ // (runner-output-contract.yaml, `step_result`). The `error`-only
4055
+ // carve-out beside that rule is for TRANSPORT failures, and a failed
4056
+ // preparation is discovery, schema validation or setup — none of
4057
+ // which is transport. Without this the caller saw a red step with no
4058
+ // `check` discriminator and had to regex the prose, which is the one
4059
+ // thing the synthesized kinds exist to avoid.
4060
+ validations: [
4061
+ {
4062
+ check: "fixture_capture_unbuildable",
4063
+ passed: false,
4064
+ description: "Fixture preparation must resolve before a step can reuse it",
4065
+ error: detail,
4066
+ json_pointer: null,
4067
+ expected: "a fixture graph that resolved during preparation",
4068
+ actual: "preparation_failed",
4069
+ schema_id: null,
4070
+ schema_url: null
4071
+ }
4072
+ ],
4073
+ context: withoutProtectedCaptures(context),
4074
+ extraction: { path: "none" },
4075
+ contributions: Array.from(contributions),
4076
+ error: detail
4077
+ };
4078
+ }
4079
+ return {
4080
+ storyboard_id: storyboard.id,
4081
+ step_id: found.step.id,
4082
+ phase_id: found.phaseId,
4083
+ title: found.step.title,
4084
+ task: found.step.task,
4085
+ passed: true,
4086
+ skipped: true,
4087
+ skip_reason: disposition.status === "unsatisfied" ? "fixture_unsatisfied" : "missing_test_controller",
4088
+ // Match the full runner: reporting a missing controller as a
4089
+ // branch-substitutable `not_applicable` would let a consumer credit a
4090
+ // peer path that does not exist.
4091
+ //
4092
+ // `fixture_unsatisfied` is now the canonical reason too, not a
4093
+ // detailed one mapped down to `not_applicable`. The published contract
4094
+ // lists it as a canonical `skip_result.reason`, and "our other code
4095
+ // path spells it differently" was SDK precedent, not a spec argument —
4096
+ // which the repo's own `spec → mock → SDK` triage order resolves
4097
+ // against the SDK. Promoting it in `RunnerSkipReason` fixed both paths
4098
+ // at once, so the two no longer disagree.
4099
+ skip: {
4100
+ reason: disposition.status === "unsatisfied" ? "fixture_unsatisfied" : "missing_test_controller",
4101
+ detail
4102
+ },
4103
+ duration_ms: 0,
4104
+ validations: [],
4105
+ context: withoutProtectedCaptures(context),
4106
+ extraction: { path: "none" },
4107
+ contributions: Array.from(contributions)
4108
+ };
4109
+ }
3296
4110
  async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
3297
4111
  validateTestKit(options.test_kit);
4112
+ const metadataRefusal = describeReservedCallerOptions(options);
4113
+ if (metadataRefusal !== void 0) {
4114
+ const target = flattenSteps(storyboard).find((entry) => entry.step.id === stepId);
4115
+ if (!target) throw new Error(`Step "${stepId}" not found in storyboard "${storyboard.id}".`);
4116
+ return denyPreparedFixtures(
4117
+ options,
4118
+ buildPreparedRefusalStepResult(
4119
+ storyboard,
4120
+ target,
4121
+ {},
4122
+ { code: "unauthorized_metadata", detail: metadataRefusal },
4123
+ options.contributions ?? []
4124
+ ),
4125
+ "unauthorized_metadata"
4126
+ );
4127
+ }
4128
+ let standaloneGate = { used: false };
4129
+ if (options.prepared_fixtures !== void 0) {
4130
+ const target = flattenSteps(storyboard).find((entry) => entry.step.id === stepId);
4131
+ if (!target) {
4132
+ throw new Error(
4133
+ `Step "${stepId}" not found in storyboard "${storyboard.id}". Available steps: ${flattenSteps(storyboard).map((entry) => entry.step.id).join(", ")}`
4134
+ );
4135
+ }
4136
+ standaloneGate = await gatePreparedFixtures(storyboard, [agentUrl], options, target.step, "step");
4137
+ if (standaloneGate.used && "refusal" in standaloneGate) {
4138
+ return denyPreparedFixtures(
4139
+ options,
4140
+ buildPreparedRefusalStepResult(
4141
+ storyboard,
4142
+ target,
4143
+ createRunContext(storyboard, options).context,
4144
+ standaloneGate.refusal,
4145
+ options.contributions ?? []
4146
+ ),
4147
+ standaloneGate.refusal.code
4148
+ );
4149
+ }
4150
+ }
3298
4151
  if (storyboard.requires?.[0] === "property_list_reference_runner" && storyboardCapabilityPredicates(storyboard).length === 0 && propertyListReferenceNeedsMcp(options)) {
3299
4152
  const found2 = flattenSteps(storyboard).find((entry) => entry.step.id === stepId);
3300
4153
  if (!found2) throw new Error(`Step "${stepId}" not found in storyboard "${storyboard.id}".`);
@@ -3314,7 +4167,9 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
3314
4167
  },
3315
4168
  duration_ms: 0,
3316
4169
  validations: [],
3317
- context: { ...storyboard.context, ...options.context },
4170
+ // Validated the same way as the executing path so a reserved key in
4171
+ // caller context fails closed on the early-return branch too.
4172
+ context: createRunContext(storyboard, options).context,
3318
4173
  extraction: { path: "none" }
3319
4174
  };
3320
4175
  }
@@ -3334,9 +4189,9 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
3334
4189
  } else {
3335
4190
  profile = options._profile;
3336
4191
  }
3337
- const context = { ...storyboard.context, ...options.context };
3338
- if (storyboard.context) forwardAliasCache(storyboard.context, context);
3339
- if (options.context) forwardAliasCache(options.context, context);
4192
+ const standaloneRunContext = createRunContext(storyboard, options);
4193
+ const protectedWriter = standaloneRunContext.protectedWriter;
4194
+ const context = standaloneRunContext.context;
3340
4195
  const allSteps = flattenSteps(storyboard);
3341
4196
  const found = allSteps.find((s) => s.step.id === stepId);
3342
4197
  if (!found) {
@@ -3417,7 +4272,11 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
3417
4272
  passed: true,
3418
4273
  skipped: true,
3419
4274
  skip_reason: "fixture_unsatisfied",
3420
- skip: { reason: "not_applicable", detail: resolution.detail },
4275
+ // Canonical too, now that `fixture_unsatisfied` is in
4276
+ // `RunnerSkipReason`. Both unsatisfied paths spell it the same way,
4277
+ // which is what the contract asks for and what the previous
4278
+ // `not_applicable` mapping obscured.
4279
+ skip: { reason: "fixture_unsatisfied", detail: resolution.detail },
3421
4280
  duration_ms: 0,
3422
4281
  validations: [],
3423
4282
  context,
@@ -3425,18 +4284,30 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
3425
4284
  contributions: Array.from(options.contributions ?? [])
3426
4285
  };
3427
4286
  }
3428
- for (const key of [
3429
- "capability_id",
3430
- "target_format_id",
3431
- "message",
3432
- "creative_manifest",
3433
- "creative_id",
3434
- "transformer_id",
3435
- "config"
3436
- ]) {
3437
- delete context[`${storyboard.creative_build_probe.context_prefix}_${key}`];
3438
- }
3439
- Object.assign(context, resolution.context);
4287
+ const probeViolation = tryProtectedContextPatch(
4288
+ context,
4289
+ {
4290
+ unset: CREATIVE_PROBE_CONTEXT_SUFFIXES.map((key) => `${storyboard.creative_build_probe.context_prefix}_${key}`),
4291
+ set: resolution.context
4292
+ },
4293
+ { origin: `creative_build_probe '${storyboard.creative_build_probe.context_prefix}'` }
4294
+ );
4295
+ if (probeViolation) {
4296
+ return {
4297
+ storyboard_id: storyboard.id,
4298
+ step_id: found.step.id,
4299
+ phase_id: found.phaseId,
4300
+ title: found.step.title,
4301
+ task: found.step.task,
4302
+ passed: false,
4303
+ duration_ms: 0,
4304
+ validations: [probeViolation],
4305
+ context: withoutProtectedCaptures(context),
4306
+ extraction: { path: "none" },
4307
+ contributions: Array.from(options.contributions ?? []),
4308
+ error: probeViolation.error
4309
+ };
4310
+ }
3440
4311
  options = { ...options, contracts: [.../* @__PURE__ */ new Set([...options.contracts ?? [], "creative_build_probe_runner"])] };
3441
4312
  }
3442
4313
  const injectedReceiver = options._webhookReceiver;
@@ -3476,8 +4347,35 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
3476
4347
  agentProfile: profile,
3477
4348
  agentLibraryVersion: profile?.library_version,
3478
4349
  storyboardRequiresRequestSigner: resolveStoryboardRequires(storyboard, options).includes("request_signer"),
3479
- storyboardRequiresPublisherAuthRunner: storyboard.requires?.includes("trusted_match_publisher_auth_runner") === true
4350
+ storyboardRequiresPublisherAuthRunner: storyboard.requires?.includes("trusted_match_publisher_auth_runner") === true,
4351
+ protectedWriter
3480
4352
  };
4353
+ const prepared = standaloneGate;
4354
+ if (prepared.used && "state" in prepared) {
4355
+ const disposition = prepared.state.disposition;
4356
+ if (disposition.status !== "resolved") {
4357
+ return buildPreparedDispositionStepResult(storyboard, found, context, disposition, contributions);
4358
+ }
4359
+ const hydrationViolation = hydratePreparedCaptures(prepared.state, context, protectedWriter, contextProvenance);
4360
+ if (hydrationViolation) {
4361
+ return {
4362
+ storyboard_id: storyboard.id,
4363
+ step_id: found.step.id,
4364
+ phase_id: found.phaseId,
4365
+ title: found.step.title,
4366
+ task: found.step.task,
4367
+ passed: false,
4368
+ duration_ms: 0,
4369
+ validations: [hydrationViolation],
4370
+ context: withoutProtectedCaptures(context),
4371
+ extraction: { path: "none" },
4372
+ contributions: Array.from(contributions),
4373
+ error: hydrationViolation.error
4374
+ };
4375
+ }
4376
+ runState.fixtureBindings = preparedBindingRegistryFor(prepared.state);
4377
+ runState.preparedFixtures = prepared.state;
4378
+ }
3481
4379
  let prerequisiteFailure;
3482
4380
  if (storyboard.id === "media_buy_seller/acceptance_policy_discovery" && found.step.id === "get_contextual_products" && options.acceptancePolicyDiscovery?.enabled !== false) {
3483
4381
  const capabilityStep = allSteps.find((value) => value.step.id === "get_acceptance_policy_capability");
@@ -3533,7 +4431,11 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
3533
4431
  contributions.add(found.step.contributes_to);
3534
4432
  }
3535
4433
  }
3536
- return { ...result, contributions: Array.from(contributions) };
4434
+ return {
4435
+ ...result,
4436
+ context: withoutProtectedCaptures(result.context ?? {}),
4437
+ contributions: Array.from(contributions)
4438
+ };
3537
4439
  } finally {
3538
4440
  await Promise.all([
3539
4441
  ...!clientResolution.reusedShared ? [closeScopedConnections(options.protocol)] : [],
@@ -3543,7 +4445,7 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
3543
4445
  }
3544
4446
  function unresolvedStepContextVars(step, request, context, runState) {
3545
4447
  const unavailableInputs = (step.context_inputs ?? []).filter(
3546
- (input) => !(input.key in context) && (runState.capabilityUnavailableContextKeys?.has(input.key) === true || runState.missingPrerequisiteContextKeys?.has(input.key) === true)
4448
+ (input) => !Object.hasOwn(context, input.key) && (runState.capabilityUnavailableContextKeys?.has(input.key) === true || runState.missingPrerequisiteContextKeys?.has(input.key) === true)
3547
4449
  );
3548
4450
  return [
3549
4451
  ...findUnresolvedContextVars(request),
@@ -3836,7 +4738,7 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
3836
4738
  synthesized.push({
3837
4739
  check: "unresolved_substitution",
3838
4740
  passed: false,
3839
- description: v.token === BUILD_ASSETS_FROM_FORMAT_DIRECTIVE ? `request directive "${v.token}" did not resolve \u2014 the test kit could not populate every seller-required asset at ${v.key}` : `request token "${v.token}" did not resolve \u2014 prior step did not populate context.${v.key}`,
4741
+ description: v.token === BUILD_ASSETS_FROM_FORMAT_DIRECTIVE ? `request directive "${v.token}" did not resolve \u2014 the test kit could not populate every seller-required asset at ${v.key}` : DOTTED_CONTEXT_TOKEN_RE.test(v.token) ? `request token "${v.token}" is not a supported substitution \u2014 $context.<key> matches a whole key only and cannot drill into a captured object. Capture the field directly with context_outputs: [{ key: ..., path: ${suggestedCapturePath(v.token)} }], or branch on the snapshot with a phase skip_if.` : `request token "${v.token}" did not resolve \u2014 prior step did not populate context.${v.key}`,
3840
4742
  json_pointer: null,
3841
4743
  expected: v.token,
3842
4744
  actual: null,
@@ -4355,21 +5257,38 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
4355
5257
  allSteps
4356
5258
  );
4357
5259
  if (responseDerivedSkip && !step.expect_error) {
4358
- for (const key of responseDerivedSkip.contextKeys) {
4359
- runState.responseDerivedNotApplicableContextKeys?.set(key, responseDerivedSkip.detail);
4360
- }
5260
+ const unavailableViolation = tryProtectedContextPatch(
5261
+ context,
5262
+ {
5263
+ markUnavailable: Object.fromEntries(
5264
+ responseDerivedSkip.contextKeys.map((key) => [key, responseDerivedSkip.detail])
5265
+ )
5266
+ },
5267
+ {
5268
+ ...runState.responseDerivedNotApplicableContextKeys && {
5269
+ sinks: { unavailable: runState.responseDerivedNotApplicableContextKeys }
5270
+ },
5271
+ origin: `step '${step.id}' not_applicable_if response gate`
5272
+ }
5273
+ );
4361
5274
  const next2 = getNextStepPreview(step.id, allSteps, context, runState.runnerVars);
4362
5275
  return {
4363
5276
  step_id: step.id,
4364
5277
  phase_id: phaseId,
4365
5278
  title: step.title,
4366
5279
  task: step.task,
4367
- passed: true,
4368
- skipped: true,
4369
- skip_reason: "not_applicable",
4370
- skip: buildSkip("not_applicable", responseDerivedSkip.detail),
5280
+ // A refused metadata write is a failure, not a skip: the gate did not
5281
+ // actually mark anything unavailable, so reporting `not_applicable`
5282
+ // would claim a disposition the runner never reached.
5283
+ passed: unavailableViolation === void 0,
5284
+ ...unavailableViolation === void 0 && {
5285
+ skipped: true,
5286
+ skip_reason: "not_applicable",
5287
+ skip: buildSkip("not_applicable", responseDerivedSkip.detail)
5288
+ },
4371
5289
  duration_ms: stepResult.duration_ms,
4372
- validations: [],
5290
+ validations: unavailableViolation ? [unavailableViolation] : [],
5291
+ ...unavailableViolation && { error: unavailableViolation.error },
4373
5292
  context,
4374
5293
  ...responseDerivedContextResult(runState),
4375
5294
  response: redactSecrets(taskResult?.data),
@@ -4609,33 +5528,60 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
4609
5528
  }
4610
5529
  const updatedContext = { ...context };
4611
5530
  forwardAliasCache(context, updatedContext);
5531
+ forwardProtectedContext(context, updatedContext);
5532
+ const protectedSinks = {
5533
+ ...runState.contextProvenance && { provenance: runState.contextProvenance },
5534
+ ...runState.responseDerivedNotApplicableContextKeys && {
5535
+ unavailable: runState.responseDerivedNotApplicableContextKeys
5536
+ }
5537
+ };
5538
+ const protectedViolations = [];
5539
+ const commitContextPatch = (patch, origin) => {
5540
+ try {
5541
+ applyProtectedContextPatch(updatedContext, patch, { sinks: protectedSinks, origin });
5542
+ } catch (error) {
5543
+ if (!(error instanceof ProtectedContextViolation)) throw error;
5544
+ protectedViolations.push({
5545
+ check: "protected_context_violation",
5546
+ passed: false,
5547
+ description: "Reserved fixture-preparation context keys are immutable for the rest of the run",
5548
+ error: error.message,
5549
+ json_pointer: null,
5550
+ expected: "no write, delete or metadata change to the __adcp_fixture_ namespace",
5551
+ actual: error.keys.join(", "),
5552
+ schema_id: null,
5553
+ schema_url: null
5554
+ });
5555
+ }
5556
+ };
4612
5557
  const hasData = taskResult?.data !== void 0 && taskResult?.data !== null;
4613
5558
  if (passed && hasData && taskResult && !(effectiveStep.task === "sync_accounts" && request.dry_run === true)) {
4614
5559
  const extracted = extractContextWithProvenance(effectiveStep.task, taskResult.data, step.id);
5560
+ const conventionUnset = [];
4615
5561
  for (const group of extracted.clearGroups ?? []) {
4616
5562
  if (group.when && !group.when.values.includes(updatedContext[group.when.key])) continue;
4617
5563
  for (const key of group.keys) {
4618
5564
  const provenance = runState.contextProvenance?.get(key);
4619
5565
  if (provenance && provenance.source_kind !== "convention") continue;
4620
- delete updatedContext[key];
4621
- runState.responseDerivedNotApplicableContextKeys?.delete(key);
4622
- runState.contextProvenance?.delete(key);
4623
- }
4624
- }
4625
- Object.assign(updatedContext, extracted.values);
4626
- for (const key of Object.keys(extracted.values)) {
4627
- runState.responseDerivedNotApplicableContextKeys?.delete(key);
4628
- }
4629
- if (runState.contextProvenance) {
4630
- for (const [key, entry] of Object.entries(extracted.provenance)) {
4631
- runState.contextProvenance.set(key, entry);
5566
+ conventionUnset.push(key);
4632
5567
  }
4633
5568
  }
5569
+ commitContextPatch(
5570
+ {
5571
+ set: extracted.values,
5572
+ unset: conventionUnset,
5573
+ setProvenance: extracted.provenance,
5574
+ unsetProvenance: conventionUnset,
5575
+ clearUnavailable: [...conventionUnset, ...Object.keys(extracted.values)]
5576
+ },
5577
+ `step '${step.id}' convention extraction for ${effectiveStep.task}`
5578
+ );
4634
5579
  }
4635
5580
  if (step.context_outputs?.length) {
4636
- for (const output of step.context_outputs) {
4637
- if (output.key) runState.responseDerivedNotApplicableContextKeys?.delete(output.key);
4638
- }
5581
+ commitContextPatch(
5582
+ { clearUnavailable: step.context_outputs.map((output) => output.key).filter((key) => typeof key === "string") },
5583
+ `step '${step.id}' context_outputs availability reset`
5584
+ );
4639
5585
  const taskCompletionResolution = await resolveTaskCompletionOutputs(
4640
5586
  taskResult,
4641
5587
  step.context_outputs,
@@ -4652,15 +5598,14 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
4652
5598
  effectiveStep.task,
4653
5599
  updatedContext
4654
5600
  );
4655
- Object.assign(updatedContext, explicit.values);
4656
- for (const key of Object.keys(explicit.values)) {
4657
- runState.responseDerivedNotApplicableContextKeys?.delete(key);
4658
- }
4659
- if (runState.contextProvenance) {
4660
- for (const [key, entry] of Object.entries(explicit.provenance)) {
4661
- runState.contextProvenance.set(key, entry);
4662
- }
4663
- }
5601
+ commitContextPatch(
5602
+ {
5603
+ set: explicit.values,
5604
+ setProvenance: explicit.provenance,
5605
+ clearUnavailable: Object.keys(explicit.values)
5606
+ },
5607
+ `step '${step.id}' context_outputs capture`
5608
+ );
4664
5609
  if (explicit.failures && explicit.failures.length > 0) {
4665
5610
  for (const failure of explicit.failures) {
4666
5611
  const wasTaskCompletion = step.context_outputs.some(
@@ -4697,6 +5642,7 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
4697
5642
  }
4698
5643
  }
4699
5644
  }
5645
+ if (protectedViolations.length > 0) validations.push(...protectedViolations);
4700
5646
  const allValidationsPassedFinal = validations.every((v) => !validationFailsStep(v));
4701
5647
  const stepFailed = !(passed && allValidationsPassedFinal);
4702
5648
  const contextRejectionHints = stepFailed && runState.contextProvenance ? detectContextRejectionHints(taskResult, request, context, runState.contextProvenance, effectiveStep.task) : [];
@@ -4727,9 +5673,21 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
4727
5673
  // modern `response_record.payload` path is already redacted.
4728
5674
  response: redactSecrets(taskResult?.data),
4729
5675
  validations,
5676
+ // NOT projected here: the full runner threads this map from step to
5677
+ // step, so stripping the reserved namespace would break the very
5678
+ // substitution the captures exist for. The STANDALONE entry point
5679
+ // projects it on the way out — see `runStoryboardStepBody` — because
5680
+ // there the map is a thread-back channel the next call refuses.
4730
5681
  context: updatedContext,
4731
5682
  ...runState.contextProvenance && runState.contextProvenance.size > 0 && {
4732
- context_provenance: Object.fromEntries(runState.contextProvenance)
5683
+ // Reserved-namespace provenance is withheld from the echo: the
5684
+ // standalone contract threads this map back in on the NEXT call,
5685
+ // where `assertUnreservedCallerMetadata` refuses it. Emitting
5686
+ // something we then reject would break the hosted step loop this
5687
+ // feature exists for.
5688
+ context_provenance: Object.fromEntries(
5689
+ [...runState.contextProvenance].filter(([key]) => !isProtectedContextKey(key))
5690
+ )
4733
5691
  },
4734
5692
  ...responseDerivedContextResult(runState),
4735
5693
  error: step.expect_error || schemaRejectionIsAdvisory ? void 0 : truncateError(stepResult.error || taskResult?.error),
@@ -6743,12 +7701,22 @@ function collectUpstreamIdentifierDigests(validations, requestPayload, sampleReq
6743
7701
  function sha256Hex(value) {
6744
7702
  return createHash("sha256").update(value, "utf8").digest("hex");
6745
7703
  }
7704
+ const DOTTED_CONTEXT_TOKEN_RE = /^\$context\.(\w+)((?:\.\w+|\[\d+\])+)$/;
7705
+ function suggestedCapturePath(token) {
7706
+ const match = token.match(DOTTED_CONTEXT_TOKEN_RE);
7707
+ const accessor = match?.[2] ?? "";
7708
+ return accessor.startsWith(".") ? accessor.slice(1) : accessor;
7709
+ }
6746
7710
  function findUnresolvedContextVars(obj) {
6747
7711
  const vars = [];
6748
7712
  const walk = (val) => {
6749
7713
  if (typeof val === "string") {
6750
7714
  const match = val.match(/^\$context\.(\w+)$/);
6751
7715
  if (match?.[1]) vars.push({ key: match[1], token: val });
7716
+ else {
7717
+ const dotted = val.match(DOTTED_CONTEXT_TOKEN_RE);
7718
+ if (dotted?.[1]) vars.push({ key: dotted[1], token: val });
7719
+ }
6752
7720
  } else if (Array.isArray(val)) {
6753
7721
  val.forEach(walk);
6754
7722
  } else if (val !== null && typeof val === "object") {
@@ -6948,6 +7916,7 @@ export {
6948
7916
  getFirstStepPreview,
6949
7917
  listStrictOnlyFailures,
6950
7918
  prepareGovernedStoryboardWireArgs,
7919
+ prepareStoryboardFixtures,
6951
7920
  resolveCapabilityPath,
6952
7921
  runStoryboard,
6953
7922
  runStoryboardStep,