@adcp/sdk 16.2.0 → 16.4.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 (161) hide show
  1. package/AGENTS.md +2 -0
  2. package/dist/lib/adapters/version/3.1/assignments.d.mts +12 -0
  3. package/dist/lib/adapters/version/3.1/assignments.d.ts +12 -0
  4. package/dist/lib/adapters/version/3.1/assignments.js +67 -0
  5. package/dist/lib/adapters/version/3.1/assignments.mjs +43 -0
  6. package/dist/lib/adapters/version/3.1/bidding.d.mts +3 -0
  7. package/dist/lib/adapters/version/3.1/bidding.d.ts +3 -0
  8. package/dist/lib/adapters/version/3.1/bidding.js +115 -0
  9. package/dist/lib/adapters/version/3.1/bidding.mjs +90 -0
  10. package/dist/lib/adapters/version/3.1/delivery.d.mts +7 -0
  11. package/dist/lib/adapters/version/3.1/delivery.d.ts +7 -0
  12. package/dist/lib/adapters/version/3.1/delivery.js +78 -0
  13. package/dist/lib/adapters/version/3.1/delivery.mjs +54 -0
  14. package/dist/lib/adapters/version/3.1/index.d.mts +3 -0
  15. package/dist/lib/adapters/version/3.1/index.d.ts +3 -0
  16. package/dist/lib/adapters/version/3.1/index.js +36 -0
  17. package/dist/lib/adapters/version/3.1/index.mjs +12 -0
  18. package/dist/lib/adapters/version/index.d.mts +59 -7
  19. package/dist/lib/adapters/version/index.d.ts +59 -7
  20. package/dist/lib/adapters/version/index.js +51 -2
  21. package/dist/lib/adapters/version/index.mjs +49 -1
  22. package/dist/lib/adapters/version/negotiate.d.mts +71 -0
  23. package/dist/lib/adapters/version/negotiate.d.ts +71 -0
  24. package/dist/lib/adapters/version/negotiate.js +101 -0
  25. package/dist/lib/adapters/version/negotiate.mjs +73 -0
  26. package/dist/lib/adapters/version/schema-projection.d.mts +33 -0
  27. package/dist/lib/adapters/version/schema-projection.d.ts +33 -0
  28. package/dist/lib/adapters/version/schema-projection.js +294 -0
  29. package/dist/lib/adapters/version/schema-projection.mjs +269 -0
  30. package/dist/lib/adapters/version/translate.d.mts +122 -0
  31. package/dist/lib/adapters/version/translate.d.ts +122 -0
  32. package/dist/lib/adapters/version/translate.js +169 -0
  33. package/dist/lib/adapters/version/translate.mjs +141 -0
  34. package/dist/lib/adapters/version/types.d.mts +51 -10
  35. package/dist/lib/adapters/version/types.d.ts +51 -10
  36. package/dist/lib/core/AgentClient.d.mts +1 -1
  37. package/dist/lib/core/AgentClient.d.ts +1 -1
  38. package/dist/lib/core/AgentClient.js +17 -1
  39. package/dist/lib/core/AgentClient.mjs +17 -1
  40. package/dist/lib/core/ConversationTypes.d.mts +7 -0
  41. package/dist/lib/core/ConversationTypes.d.ts +7 -0
  42. package/dist/lib/core/SingleAgentClient.d.mts +82 -0
  43. package/dist/lib/core/SingleAgentClient.d.ts +82 -0
  44. package/dist/lib/core/SingleAgentClient.js +336 -15
  45. package/dist/lib/core/SingleAgentClient.mjs +350 -17
  46. package/dist/lib/core/TaskExecutor.d.mts +1 -1
  47. package/dist/lib/core/TaskExecutor.d.ts +1 -1
  48. package/dist/lib/core/TaskExecutor.js +182 -31
  49. package/dist/lib/core/TaskExecutor.mjs +183 -32
  50. package/dist/lib/core/account-reduction.d.mts +36 -0
  51. package/dist/lib/core/account-reduction.d.ts +36 -0
  52. package/dist/lib/core/account-reduction.js +127 -0
  53. package/dist/lib/core/account-reduction.mjs +98 -0
  54. package/dist/lib/core/buyer-account-registry.d.mts +52 -0
  55. package/dist/lib/core/buyer-account-registry.d.ts +52 -0
  56. package/dist/lib/core/buyer-account-registry.js +142 -6
  57. package/dist/lib/core/buyer-account-registry.mjs +142 -6
  58. package/dist/lib/errors/index.d.mts +34 -0
  59. package/dist/lib/errors/index.d.ts +34 -0
  60. package/dist/lib/errors/index.js +11 -0
  61. package/dist/lib/errors/index.mjs +10 -0
  62. package/dist/lib/index.d.mts +1 -1
  63. package/dist/lib/index.d.ts +1 -1
  64. package/dist/lib/index.js +2 -0
  65. package/dist/lib/index.mjs +2 -0
  66. package/dist/lib/media-buy/action-contracts.js +4 -4
  67. package/dist/lib/media-buy/action-contracts.mjs +4 -4
  68. package/dist/lib/media-buy/action-types.d.mts +3 -2
  69. package/dist/lib/media-buy/action-types.d.ts +3 -2
  70. package/dist/lib/media-buy/compatibility.d.mts +1 -0
  71. package/dist/lib/media-buy/compatibility.d.ts +1 -0
  72. package/dist/lib/media-buy/compatibility.js +33 -10
  73. package/dist/lib/media-buy/compatibility.mjs +32 -14
  74. package/dist/lib/media-buy/update-fields.generated.d.mts +3 -2
  75. package/dist/lib/media-buy/update-fields.generated.d.ts +3 -2
  76. package/dist/lib/negotiation/buyer.js +11 -5
  77. package/dist/lib/negotiation/buyer.mjs +11 -5
  78. package/dist/lib/negotiation/types.d.mts +4 -2
  79. package/dist/lib/negotiation/types.d.ts +4 -2
  80. package/dist/lib/negotiation/verification.d.mts +15 -6
  81. package/dist/lib/negotiation/verification.d.ts +15 -6
  82. package/dist/lib/negotiation/verification.js +146 -27
  83. package/dist/lib/negotiation/verification.mjs +146 -28
  84. package/dist/lib/negotiation/version.d.mts +6 -0
  85. package/dist/lib/negotiation/version.d.ts +6 -0
  86. package/dist/lib/negotiation/version.js +60 -0
  87. package/dist/lib/negotiation/version.mjs +33 -0
  88. package/dist/lib/registry/types.generated.d.mts +1 -1
  89. package/dist/lib/registry/types.generated.d.ts +1 -1
  90. package/dist/lib/server/create-adcp-server.d.mts +21 -0
  91. package/dist/lib/server/create-adcp-server.d.ts +21 -0
  92. package/dist/lib/server/create-adcp-server.js +98 -16
  93. package/dist/lib/server/create-adcp-server.mjs +84 -2
  94. package/dist/lib/server/error-arm-tools.d.mts +10 -10
  95. package/dist/lib/server/error-arm-tools.d.ts +10 -10
  96. package/dist/lib/server/error-arm-tools.js +22 -58
  97. package/dist/lib/server/error-arm-tools.mjs +30 -65
  98. package/dist/lib/server/index.d.mts +1 -1
  99. package/dist/lib/server/index.d.ts +1 -1
  100. package/dist/lib/storage/interfaces.d.mts +2 -0
  101. package/dist/lib/storage/interfaces.d.ts +2 -0
  102. package/dist/lib/testing/compliance/comply.js +17 -0
  103. package/dist/lib/testing/compliance/comply.mjs +17 -0
  104. package/dist/lib/testing/index.d.mts +3 -0
  105. package/dist/lib/testing/index.d.ts +3 -0
  106. package/dist/lib/testing/index.js +44 -2
  107. package/dist/lib/testing/index.mjs +42 -0
  108. package/dist/lib/testing/storyboard/collection-selection.d.mts +259 -0
  109. package/dist/lib/testing/storyboard/collection-selection.d.ts +259 -0
  110. package/dist/lib/testing/storyboard/collection-selection.js +384 -0
  111. package/dist/lib/testing/storyboard/collection-selection.mjs +348 -0
  112. package/dist/lib/testing/storyboard/context.js +12 -6
  113. package/dist/lib/testing/storyboard/context.mjs +12 -6
  114. package/dist/lib/testing/storyboard/fixture-resolution.d.mts +39 -1
  115. package/dist/lib/testing/storyboard/fixture-resolution.d.ts +39 -1
  116. package/dist/lib/testing/storyboard/fixture-resolution.js +216 -12
  117. package/dist/lib/testing/storyboard/fixture-resolution.mjs +218 -12
  118. package/dist/lib/testing/storyboard/index.d.mts +9 -2
  119. package/dist/lib/testing/storyboard/index.d.ts +9 -2
  120. package/dist/lib/testing/storyboard/index.js +51 -2
  121. package/dist/lib/testing/storyboard/index.mjs +49 -1
  122. package/dist/lib/testing/storyboard/prepared-fixtures.d.mts +471 -0
  123. package/dist/lib/testing/storyboard/prepared-fixtures.d.ts +471 -0
  124. package/dist/lib/testing/storyboard/prepared-fixtures.js +434 -0
  125. package/dist/lib/testing/storyboard/prepared-fixtures.mjs +389 -0
  126. package/dist/lib/testing/storyboard/protected-context.d.mts +127 -0
  127. package/dist/lib/testing/storyboard/protected-context.d.ts +127 -0
  128. package/dist/lib/testing/storyboard/protected-context.js +301 -0
  129. package/dist/lib/testing/storyboard/protected-context.mjs +267 -0
  130. package/dist/lib/testing/storyboard/rejection-hints.js +13 -4
  131. package/dist/lib/testing/storyboard/rejection-hints.mjs +13 -4
  132. package/dist/lib/testing/storyboard/runner.d.mts +24 -0
  133. package/dist/lib/testing/storyboard/runner.d.ts +24 -0
  134. package/dist/lib/testing/storyboard/runner.js +1061 -105
  135. package/dist/lib/testing/storyboard/runner.mjs +1075 -106
  136. package/dist/lib/testing/storyboard/seeding.d.mts +15 -1
  137. package/dist/lib/testing/storyboard/seeding.d.ts +15 -1
  138. package/dist/lib/testing/storyboard/seeding.js +272 -12
  139. package/dist/lib/testing/storyboard/seeding.mjs +274 -13
  140. package/dist/lib/testing/storyboard/types.d.mts +207 -10
  141. package/dist/lib/testing/storyboard/types.d.ts +207 -10
  142. package/dist/lib/testing/storyboard/types.js +1 -1
  143. package/dist/lib/testing/storyboard/types.mjs +1 -1
  144. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  145. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  146. package/dist/lib/testing/storyboard/validations.js +95 -0
  147. package/dist/lib/testing/storyboard/validations.mjs +95 -0
  148. package/dist/lib/validation/bundled-schema-store.js +5 -0
  149. package/dist/lib/validation/bundled-schema-store.mjs +4 -0
  150. package/dist/lib/validation/schema-loader.js +19 -2
  151. package/dist/lib/validation/schema-loader.mjs +22 -3
  152. package/dist/lib/version.d.mts +3 -3
  153. package/dist/lib/version.d.ts +3 -3
  154. package/dist/lib/version.js +3 -3
  155. package/dist/lib/version.mjs +3 -3
  156. package/docs/TYPE-SUMMARY.md +2 -2
  157. package/docs/guides/COLLECTION-FIXTURE-FOUNDATION.md +850 -0
  158. package/docs/guides/PROPOSAL-TERMS-VERIFICATION.md +66 -0
  159. package/docs/llms.txt +4 -3
  160. package/docs/migration-14.x-rc-worksheet.md +4 -4
  161. package/package.json +1 -1
@@ -48,6 +48,7 @@ __export(runner_exports, {
48
48
  getFirstStepPreview: () => getFirstStepPreview,
49
49
  listStrictOnlyFailures: () => listStrictOnlyFailures,
50
50
  prepareGovernedStoryboardWireArgs: () => prepareGovernedStoryboardWireArgs,
51
+ prepareStoryboardFixtures: () => prepareStoryboardFixtures,
51
52
  resolveCapabilityPath: () => resolveCapabilityPath,
52
53
  runStoryboard: () => runStoryboard,
53
54
  runStoryboardStep: () => runStoryboardStep,
@@ -69,6 +70,8 @@ var import_rawResponseCapture = require('../../protocols/rawResponseCapture.js')
69
70
  var import_task_map = require('./task-map.js');
70
71
  var import_functional_dispatch = require('./request-signing/functional-dispatch.js');
71
72
  var import_context = require('./context.js');
73
+ var import_protected_context = require('./protected-context.js');
74
+ var import_prepared_fixtures = require('./prepared-fixtures.js');
72
75
  var import_rejection_hints = require('./rejection-hints.js');
73
76
  var import_shape_drift_hints = require('./shape-drift-hints.js');
74
77
  var import_strict_validation_hints = require('./strict-validation-hints.js');
@@ -79,6 +82,7 @@ var import_rate_limit_trip = require('./rate-limit-trip.js');
79
82
  var import_redact_secrets = require('../../utils/redact-secrets.js');
80
83
  var import_property_list_reference = require('./property-list-reference.js');
81
84
  var import_response_unwrapper = require('../../utils/response-unwrapper.js');
85
+ var import_errors = require('../../errors/index.js');
82
86
  var import_envelope_status_compat = require('../../utils/envelope-status-compat.js');
83
87
  var import_test_controller = require('../test-controller.js');
84
88
  var import_constants = require('../../upstream-recorder/constants.js');
@@ -123,12 +127,35 @@ function getPreparedTrustedMatchPublisherAuth(options) {
123
127
  function withPreparedTrustedMatchPublisherAuth(options, prepared) {
124
128
  return { ...options, [PREPARED_TRUSTED_MATCH_PUBLISHER_AUTH]: prepared };
125
129
  }
130
+ function tryProtectedContextPatch(context, patch, options) {
131
+ try {
132
+ (0, import_protected_context.applyProtectedContextPatch)(context, patch, options);
133
+ return void 0;
134
+ } catch (error) {
135
+ if (!(error instanceof import_protected_context.ProtectedContextViolation)) throw error;
136
+ return {
137
+ check: "protected_context_violation",
138
+ passed: false,
139
+ description: "Reserved fixture-preparation context keys are immutable for the rest of the run",
140
+ error: error.message,
141
+ json_pointer: null,
142
+ expected: "no write, delete or metadata change to the __adcp_fixture_ namespace",
143
+ actual: error.keys.join(", "),
144
+ // required_on_failure, and null outside `response_schema`. Omitting
145
+ // the keys is not the same as recording null — a consumer cannot
146
+ // tell "no schema applies" from "the runner forgot".
147
+ schema_id: null,
148
+ schema_url: null
149
+ };
150
+ }
151
+ }
126
152
  const SKIP_DETAILS = {
127
153
  not_applicable: "Not applicable: agent did not declare the protocol or specialism this storyboard targets.",
128
154
  no_phases: "Storyboard has no executable phases (placeholder).",
129
155
  prerequisite_failed: "Skipped: a prerequisite step or contract did not pass.",
130
156
  missing_tool: "Skipped: agent did not advertise the required tool.",
131
157
  missing_test_controller: "Skipped: deterministic_testing phase requires comply_test_controller, which the agent did not advertise.",
158
+ fixture_unsatisfied: "Skipped: every enabled strategy for a declared fixture handle was exhausted without finding or creating a qualifying entity.",
132
159
  fixture_unavailable: "Skipped: the runner test kit cannot synthesize a valid input for the seller-declared contract.",
133
160
  unsatisfied_contract: "Skipped: test-kit contract is out of scope for this grading run.",
134
161
  requirement_unmet: "Skipped: a requires: tag named a runtime requirement that is not available on this run (see RunnerSkipResult.requirement).",
@@ -490,6 +517,17 @@ function responseNotApplicableContextKeys(step, gate) {
490
517
  if (gate.context_keys?.length) return gate.context_keys;
491
518
  return (step.context_outputs ?? []).filter((o) => o.path === "pagination.cursor").map((o) => o.key).filter((key) => typeof key === "string" && key.length > 0);
492
519
  }
520
+ const FIXTURE_CAPTURE_STEP_ID = "__fixture_capture__";
521
+ function describeFixtureCaptureTask(captures, refusedKeys) {
522
+ for (const key of refusedKeys) {
523
+ const task = captures?.provenance?.[key]?.source_task;
524
+ if (typeof task === "string" && task.length > 0) return task;
525
+ }
526
+ const distinct = new Set(
527
+ Object.values(captures?.provenance ?? {}).map((entry) => entry.source_task).filter((task) => typeof task === "string" && task.length > 0)
528
+ );
529
+ return distinct.size === 1 ? [...distinct][0] : import_loader.VALIDATION_ONLY_TASK;
530
+ }
493
531
  function responseDerivedContextResult(runState) {
494
532
  const entries = runState.responseDerivedNotApplicableContextKeys;
495
533
  return entries && entries.size > 0 ? { response_derived_not_applicable_context_keys: Object.fromEntries(entries) } : {};
@@ -781,27 +819,53 @@ async function runStoryboard(agentUrlOrUrls, storyboard, options = {}) {
781
819
  "Credential-isolated property-list references currently require MCP; A2A probe coverage is unavailable."
782
820
  );
783
821
  }
784
- if ((0, import_client.hasTestClientScope)() && options._client && !options.agents) {
785
- const resolution = (0, import_client.getOrCreateClientResolution)(
786
- typeof agentUrlOrUrls === "string" ? agentUrlOrUrls : agentUrlOrUrls[0],
787
- options
788
- );
789
- const discovered = await (0, import_client.getOrDiscoverProfile)(resolution.client, options);
790
- options = {
791
- ...options,
792
- _profile: discovered.step.passed ? discovered.profile : void 0,
793
- agentTools: discovered.step.passed ? discovered.profile.tools : void 0
794
- };
795
- }
822
+ storyboard = (0, import_loader.normalizeValidationOnlyTasks)(storyboard);
823
+ (0, import_loader.validateStoryboardShape)(storyboard);
796
824
  const schemaRoot = getRunSchemaRoot(options);
797
- const runBody = () => (0, import_schema_loader.withServedReleaseSchemaRoots)(
798
- options.servedReleaseSchemaRoots,
799
- () => runStoryboardBody(agentUrlOrUrls, storyboard, options)
800
- );
825
+ const gatedBody = async () => {
826
+ const metadataRefusal = describeReservedCallerOptions(options);
827
+ if (metadataRefusal !== void 0) {
828
+ return denyPreparedFixtures(
829
+ options,
830
+ buildPreparedRefusalRunResult(resolveBindingAgentUrls(agentUrlOrUrls, options), storyboard, {
831
+ code: "unauthorized_metadata",
832
+ detail: metadataRefusal
833
+ }),
834
+ "unauthorized_metadata"
835
+ );
836
+ }
837
+ if (options.prepared_fixtures !== void 0) {
838
+ const urls = resolveBindingAgentUrls(agentUrlOrUrls, options);
839
+ const gate = await gatePreparedFixtures(storyboard, urls, options, void 0, "run");
840
+ if (gate.used && "refusal" in gate) {
841
+ return denyPreparedFixtures(
842
+ options,
843
+ buildPreparedRefusalRunResult(urls, storyboard, gate.refusal),
844
+ gate.refusal.code
845
+ );
846
+ }
847
+ }
848
+ if ((0, import_client.hasTestClientScope)() && options._client && !options.agents) {
849
+ const resolution = (0, import_client.getOrCreateClientResolution)(
850
+ typeof agentUrlOrUrls === "string" ? agentUrlOrUrls : agentUrlOrUrls[0],
851
+ options
852
+ );
853
+ const discovered = await (0, import_client.getOrDiscoverProfile)(resolution.client, options);
854
+ options = {
855
+ ...options,
856
+ _profile: discovered.step.passed ? discovered.profile : void 0,
857
+ agentTools: discovered.step.passed ? discovered.profile.tools : void 0
858
+ };
859
+ }
860
+ return (0, import_schema_loader.withServedReleaseSchemaRoots)(
861
+ options.servedReleaseSchemaRoots,
862
+ () => runStoryboardBody(agentUrlOrUrls, storyboard, options)
863
+ );
864
+ };
801
865
  if (schemaRoot) {
802
- return await (0, import_schema_loader.withExternalSchemaRoot)(schemaRoot.adcpVersion, schemaRoot.schemaRoot, runBody);
866
+ return await (0, import_schema_loader.withExternalSchemaRoot)(schemaRoot.adcpVersion, schemaRoot.schemaRoot, gatedBody);
803
867
  }
804
- return await runBody();
868
+ return await gatedBody();
805
869
  },
806
870
  { isolate: !(0, import_client.hasTestClientScope)() }
807
871
  )
@@ -1562,10 +1626,380 @@ function runnerNoticeKey(notice) {
1562
1626
  function collectStepNotices(phases) {
1563
1627
  return phases.flatMap((phase) => phase.steps.flatMap((step) => step.notices ?? []));
1564
1628
  }
1629
+ const CREATIVE_PROBE_CONTEXT_SUFFIXES = [
1630
+ "capability_id",
1631
+ "target_format_id",
1632
+ "message",
1633
+ "creative_manifest",
1634
+ "creative_id",
1635
+ "transformer_id",
1636
+ "config"
1637
+ ];
1638
+ function createRunContext(storyboard, options) {
1639
+ const protectedWriter = (0, import_protected_context.createProtectedContextWriter)();
1640
+ const context = {};
1641
+ (0, import_protected_context.registerProtectedContext)(context, protectedWriter);
1642
+ (0, import_protected_context.applyProtectedContextPatch)(
1643
+ context,
1644
+ { set: { ...storyboard.context } },
1645
+ { origin: `[${storyboard.id}] storyboard.context` }
1646
+ );
1647
+ (0, import_protected_context.applyProtectedContextPatch)(context, { set: { ...options.context } }, { origin: "options.context" });
1648
+ if (storyboard.context) (0, import_context.forwardAliasCache)(storyboard.context, context);
1649
+ if (options.context) (0, import_context.forwardAliasCache)(options.context, context);
1650
+ return { context, protectedWriter };
1651
+ }
1652
+ function describeReservedCallerMetadata(map, location) {
1653
+ if (!map) return void 0;
1654
+ const offending = Object.keys(map).filter(
1655
+ (key) => (0, import_protected_context.isProtectedContextKey)(key) || key === "__proto__" || key === "constructor" || key === "prototype"
1656
+ );
1657
+ if (offending.length === 0) return void 0;
1658
+ 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.`;
1659
+ }
1660
+ function describeReservedCallerOptions(options) {
1661
+ return describeReservedCallerMetadata(options.context, "options.context") ?? describeReservedCallerMetadata(options.context_provenance, "options.context_provenance") ?? describeReservedCallerMetadata(
1662
+ options.response_derived_not_applicable_context_keys,
1663
+ "options.response_derived_not_applicable_context_keys"
1664
+ );
1665
+ }
1666
+ function describeStepAuthDirective(directive) {
1667
+ if (directive === "none") return "none";
1668
+ if (typeof directive !== "object") return JSON.stringify(directive);
1669
+ const parts = [JSON.stringify(directive.type)];
1670
+ for (const [key, value] of Object.entries(directive).sort(
1671
+ ([a], [b]) => a < b ? -1 : 1
1672
+ )) {
1673
+ if (key === "type") continue;
1674
+ parts.push(JSON.stringify([key, value]));
1675
+ }
1676
+ return parts.join(";");
1677
+ }
1678
+ function resolveBindingAgentUrls(agentUrlOrUrls, options) {
1679
+ if (options.agents) return Object.values(options.agents).map((entry) => entry.url);
1680
+ return Array.isArray(agentUrlOrUrls) ? agentUrlOrUrls : [agentUrlOrUrls];
1681
+ }
1682
+ async function gatePreparedFixtures(storyboard, agentUrls, options, step, gate = "pass") {
1683
+ const outcome = await (0, import_prepared_fixtures.resolvePreparedFixtures)({
1684
+ supplied: options.prepared_fixtures,
1685
+ // The run context is built here so the binding can report the account
1686
+ // the step's request will ACTUALLY carry. Without it the standalone
1687
+ // entry gate — the one `runStoryboardStep` uses — reported no
1688
+ // `request_account_scope` at all, and a caller-supplied or
1689
+ // context-substituted foreign account reached the seller while the
1690
+ // authorizer saw nothing to refuse. Building the context performs no
1691
+ // I/O and is what the run would build moments later anyway.
1692
+ current: buildCurrentPreparedBinding(
1693
+ storyboard,
1694
+ agentUrls,
1695
+ options,
1696
+ step,
1697
+ options,
1698
+ step !== void 0 ? createRunContext(storyboard, options).context : void 0
1699
+ ),
1700
+ ...options.prepared_fixtures_authorizer && { authorizer: options.prepared_fixtures_authorizer },
1701
+ ...step !== void 0 && { stepId: step.id },
1702
+ gate
1703
+ });
1704
+ if (outcome === void 0) return { used: false };
1705
+ if (!outcome.ok) return { used: true, refusal: outcome.refusal };
1706
+ return { used: true, state: outcome.state };
1707
+ }
1708
+ function hydratePreparedCaptures(state, context, protectedWriter, contextProvenance) {
1709
+ const captures = state.captures;
1710
+ if (Object.keys(captures).length === 0) return void 0;
1711
+ return tryProtectedContextPatch(
1712
+ context,
1713
+ { set: captures, setProvenance: state.captureProvenance },
1714
+ { writer: protectedWriter, sinks: { provenance: contextProvenance }, origin: "prepared fixture state" }
1715
+ );
1716
+ }
1717
+ async function prepareStoryboardFixtures(agentUrl, storyboard, options = {}) {
1718
+ options = applyReusableProfileOptions(options);
1719
+ return (0, import_property_list_reference.withPropertyListCredentialRedaction)(
1720
+ () => (0, import_protocols.withMCPConnectionScope)(
1721
+ async () => {
1722
+ storyboard = (0, import_loader.normalizeValidationOnlyTasks)(storyboard);
1723
+ (0, import_loader.validateStoryboardShape)(storyboard);
1724
+ options = applyStoryboardVersionOptions(storyboard, options);
1725
+ options = (0, import_functional_dispatch.applyFunctionalRequestSigning)(options, {
1726
+ ...options.complianceDir && { complianceDir: options.complianceDir },
1727
+ ...options.adcpVersion && { version: options.adcpVersion }
1728
+ });
1729
+ options = (0, import_test_kit.resolveDeclaredTestKit)(storyboard, options);
1730
+ options = (0, import_native_a2a_compliance.applyNativeA2AComplianceTransportOptions)(options);
1731
+ const schemaRoot = getRunSchemaRoot(options);
1732
+ const body = () => (0, import_schema_loader.withServedReleaseSchemaRoots)(
1733
+ options.servedReleaseSchemaRoots,
1734
+ () => prepareStoryboardFixturesBody(agentUrl, storyboard, options)
1735
+ );
1736
+ if (schemaRoot) {
1737
+ return await (0, import_schema_loader.withExternalSchemaRoot)(schemaRoot.adcpVersion, schemaRoot.schemaRoot, body);
1738
+ }
1739
+ return await body();
1740
+ },
1741
+ { isolate: !(0, import_client.hasTestClientScope)() }
1742
+ )
1743
+ );
1744
+ }
1745
+ function resolvePreparationRouteOptions(agentUrl, options) {
1746
+ const agents = options.agents;
1747
+ if (!agents) return options;
1748
+ const match = Object.entries(agents).find(([, entry2]) => entry2?.url === agentUrl);
1749
+ if (!match) return options;
1750
+ const [, entry] = match;
1751
+ return {
1752
+ ...options,
1753
+ ...entry?.auth !== void 0 && { auth: entry.auth },
1754
+ ...entry?.transport !== void 0 && { protocol: entry.transport }
1755
+ };
1756
+ }
1757
+ async function prepareStoryboardFixturesBody(agentUrl, storyboard, options) {
1758
+ const agentUrls = resolveBindingAgentUrls(agentUrl, options);
1759
+ const metadataRefusal = describeReservedCallerOptions(options);
1760
+ if (metadataRefusal !== void 0) {
1761
+ return buildPreparedFixtures(storyboard, agentUrls, options, void 0, {
1762
+ status: "failed",
1763
+ detail: metadataRefusal
1764
+ });
1765
+ }
1766
+ if (options.prepared_fixtures !== void 0) {
1767
+ return buildPreparedFixtures(storyboard, agentUrls, options, void 0, {
1768
+ status: "failed",
1769
+ 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."
1770
+ });
1771
+ }
1772
+ const callerOwnsClients = (0, import_client.hasTestClientScope)();
1773
+ const preparationOptions = resolvePreparationRouteOptions(agentUrls[0], options);
1774
+ const resolution = (0, import_client.getOrCreateClientResolution)(agentUrls[0], preparationOptions);
1775
+ const client = resolution.client;
1776
+ const bindingOptions = options;
1777
+ options = preparationOptions;
1778
+ try {
1779
+ let profile = options._profile;
1780
+ if (!profile && !resolution.reusedShared) {
1781
+ const discovered = await (0, import_client.getOrDiscoverProfile)(client, options);
1782
+ if (discovered.step.passed === false) {
1783
+ return buildPreparedFixtures(storyboard, agentUrls, bindingOptions, void 0, {
1784
+ status: "failed",
1785
+ detail: discovered.step.error ?? "agent discovery failed before fixture preparation"
1786
+ });
1787
+ }
1788
+ profile = discovered.profile;
1789
+ }
1790
+ if (profile && (!options._profile || !options.agentTools)) {
1791
+ options = {
1792
+ ...options,
1793
+ ...!options._profile && { _profile: profile },
1794
+ ...!options.agentTools && { agentTools: (0, import_agent_routing.normalizeAgentToolNames)(profile.tools) }
1795
+ };
1796
+ }
1797
+ const context = createRunContext(storyboard, options).context;
1798
+ const seeding = await (0, import_seeding.runControllerSeeding)(client, storyboard, options, context);
1799
+ if (!seeding) {
1800
+ return buildPreparedFixtures(storyboard, agentUrls, bindingOptions, void 0, { status: "resolved" });
1801
+ }
1802
+ 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 ? {
1803
+ status: "failed",
1804
+ detail: seeding.phase.steps.find((step) => step.error)?.error ?? "fixture resolution failed during preparation"
1805
+ } : seeding.fixtureUnsatisfied === true ? { status: "unsatisfied", detail: describeUnsatisfiedFixtures(seeding.resolutionRecords) } : { status: "resolved" };
1806
+ return buildPreparedFixtures(storyboard, agentUrls, bindingOptions, seeding, disposition);
1807
+ } finally {
1808
+ if (!callerOwnsClients) await (0, import_protocols.closeScopedConnections)(options.protocol);
1809
+ }
1810
+ }
1811
+ function describeUnsatisfiedFixtures(records) {
1812
+ const unsatisfied = (records ?? []).filter((record) => record.status === "unsatisfied");
1813
+ if (unsatisfied.length === 0) return "a declared strategy ladder exhausted without a binding";
1814
+ const names = unsatisfied.map(
1815
+ (record) => record.fixture_type === "pricing_option" && record.product_handle ? `pricing_option:${record.product_handle}/${record.handle}` : `${record.fixture_type}:${record.handle}`
1816
+ );
1817
+ return `fixture_unsatisfied: ${names.join(", ")}`;
1818
+ }
1819
+ function buildPreparedFixtures(storyboard, agentUrls, options, seeding, disposition) {
1820
+ const captures = disposition.status === "resolved" ? seeding?.preparedCaptures?.values ?? {} : {};
1821
+ const captureProvenance = disposition.status === "resolved" ? seeding?.preparedCaptures?.provenance ?? {} : {};
1822
+ const witnesses = disposition.status === "resolved" ? seeding?.preparedCaptures?.witnesses ?? {} : {};
1823
+ const unsatisfied = (seeding?.resolutionRecords ?? []).filter((record) => record.status === "unsatisfied");
1824
+ return (0, import_prepared_fixtures.createPreparedStoryboardFixtures)({
1825
+ binding: buildCurrentPreparedBinding(storyboard, agentUrls, options),
1826
+ ...seeding?.bindings && { bindings: seeding.bindings },
1827
+ resolutions: seeding?.resolutionRecords ?? [],
1828
+ coverageGaps: unsatisfied.length > 0 ? [
1829
+ {
1830
+ reason: "fixture_unsatisfied",
1831
+ detail: "detail" in disposition ? disposition.detail : "a declared strategy ladder exhausted without a binding",
1832
+ fixtures: unsatisfied.map((record) => ({
1833
+ fixture_type: record.fixture_type,
1834
+ handle: record.handle,
1835
+ ...record.product_handle && { product_handle: record.product_handle },
1836
+ requirements: record.requirements
1837
+ }))
1838
+ }
1839
+ ] : [],
1840
+ captures,
1841
+ captureProvenance,
1842
+ witnesses,
1843
+ disposition,
1844
+ ...seeding?.phase && { phase: seeding.phase }
1845
+ });
1846
+ }
1847
+ function buildCurrentPreparedBinding(storyboard, agentUrls, options, step, runOptions, context, selectedRoute, runnerVars) {
1848
+ return (0, import_prepared_fixtures.buildPreparedBinding)({
1849
+ storyboard,
1850
+ agentUrls,
1851
+ ...options.adcpVersion && { adcpVersion: options.adcpVersion },
1852
+ ...(runOptions ?? options).agents && {
1853
+ agentRouteMap: Object.entries((runOptions ?? options).agents ?? {}).map(
1854
+ ([key, entry]) => [key, entry?.url ?? ""]
1855
+ )
1856
+ },
1857
+ accountScope: resolveBindingAccountScope(storyboard, options),
1858
+ brandScope: resolveBindingBrandScope(storyboard, options),
1859
+ ...step !== void 0 && { ...resolveBindingRequestAccountScope(step, context, options, runnerVars) },
1860
+ sandbox: !(options.disable_sandbox === true || options.sandbox === false),
1861
+ ...options.prepared_fixtures_principal !== void 0 && { principal: options.prepared_fixtures_principal },
1862
+ // The diffed credential always comes from the RUN options. A routed
1863
+ // dispatch flattens `agents[key].auth` into a bare `auth` on its
1864
+ // per-step view, so deriving it from `assignment.options` would make an
1865
+ // unchanged routed credential read as a mismatch and refuse every step
1866
+ // of every routed run. The step's own view feeds the separate,
1867
+ // undiffed `step_credential_fingerprint` instead.
1868
+ credential: resolveBindingCredential(runOptions ?? options),
1869
+ ...(() => {
1870
+ if (step === void 0) return {};
1871
+ const stepCredential = resolveBindingCredential(options, step);
1872
+ if (stepCredential === resolveBindingCredential(runOptions ?? options)) return {};
1873
+ return {
1874
+ stepCredential,
1875
+ // The fingerprint is an HMAC under a per-process key, so a host can
1876
+ // observe that it changed but never what it is. This companion is the
1877
+ // value an authorizer can actually branch on.
1878
+ stepAuthKind: step.auth === void 0 ? "routed" : step.auth === "none" ? "none" : "override"
1879
+ };
1880
+ })(),
1881
+ ...step !== void 0 && runOptions !== void 0 && (runOptions.agents || selectedRoute) ? resolveBindingStepRoute(step, runOptions, selectedRoute) : {},
1882
+ // Never mint one here: `current.run_id` is handed to the authorizer as
1883
+ // "identity of the preparation run", and a fresh UUID per lookup is
1884
+ // noise for a host trying to correlate on it.
1885
+ runId: options.prepared_fixtures_run_id ?? ""
1886
+ });
1887
+ }
1888
+ function resolveBindingStepRoute(step, runOptions, selected) {
1889
+ if (typeof selected?.agentUrl === "string" && selected.agentUrl.length > 0) {
1890
+ return { stepRoute: `${selected.agentKey ?? "#default"}@${(0, import_oauth_metadata_graph.redactOAuthUrlForOutput)(selected.agentUrl)}` };
1891
+ }
1892
+ const agents = runOptions.agents ?? {};
1893
+ const key = step.agent;
1894
+ if (typeof key !== "string" || !Object.hasOwn(agents, key)) return {};
1895
+ const url = agents[key]?.url;
1896
+ if (typeof url !== "string") return {};
1897
+ return { stepRoute: `${key}@${(0, import_oauth_metadata_graph.redactOAuthUrlForOutput)(url)}` };
1898
+ }
1899
+ function resolveBindingRequestAccountScope(step, context, options, runnerVars) {
1900
+ if (!context) return {};
1901
+ let account;
1902
+ try {
1903
+ const resolvedTask = (0, import_agent_routing.resolveTaskName)(step, options) ?? step.task;
1904
+ const effectiveStep = resolvedTask === step.task ? step : { ...step, task: resolvedTask };
1905
+ account = buildStepRequest(step, effectiveStep, context, options, runnerVars).account;
1906
+ } catch {
1907
+ return {};
1908
+ }
1909
+ if (account === void 0 || account === null) return {};
1910
+ return { requestAccountScope: canonicalizeScope(account) };
1911
+ }
1912
+ function resolveBindingBrandScope(storyboard, options) {
1913
+ const merged = { ...storyboard.context, ...options.context };
1914
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1915
+ const contextAccount = record(merged.account);
1916
+ const brand = record(merged.brand) ?? record(contextAccount?.brand) ?? (0, import_client2.resolveBrand)(options);
1917
+ return canonicalizeScope(brand ?? {});
1918
+ }
1919
+ function canonicalizeScope(value) {
1920
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
1921
+ if (Array.isArray(value)) return `[${value.map(canonicalizeScope).join(",")}]`;
1922
+ 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(",")}}`;
1923
+ }
1924
+ function resolveBindingAccountScope(storyboard, options) {
1925
+ const merged = { ...storyboard.context, ...options.context };
1926
+ const contextAccount = merged.account !== null && typeof merged.account === "object" && !Array.isArray(merged.account) ? merged.account : void 0;
1927
+ const account = contextAccount ?? (0, import_client2.resolveAccount)(options);
1928
+ return canonicalizeScope(account ?? {});
1929
+ }
1930
+ function resolveBindingCredential(options, step) {
1931
+ const parts = [];
1932
+ if (step?.auth !== void 0) {
1933
+ parts.push(`step_auth:${JSON.stringify(describeStepAuthDirective(step.auth))}`);
1934
+ }
1935
+ const headerEntries = Object.entries(options.headers ?? {}).sort(([a], [b]) => {
1936
+ const left = a.toLowerCase();
1937
+ const right = b.toLowerCase();
1938
+ return left < right ? -1 : left > right ? 1 : 0;
1939
+ });
1940
+ for (const [name, value] of headerEntries) {
1941
+ if (value !== void 0 && value !== null)
1942
+ parts.push(`header:${JSON.stringify([name.toLowerCase(), String(value)])}`);
1943
+ }
1944
+ const describeAuth = (auth, label) => {
1945
+ if (!auth) return;
1946
+ if (auth.type === "bearer") parts.push(`${label}:bearer:${JSON.stringify(auth.token)}`);
1947
+ else if (auth.type === "basic") parts.push(`${label}:basic:${JSON.stringify([auth.username, auth.password])}`);
1948
+ else if (auth.type === "oauth") {
1949
+ parts.push(
1950
+ `${label}:oauth:${JSON.stringify([
1951
+ auth.client?.client_id ?? null,
1952
+ typeof auth.tokens?.refresh_token === "string" ? auth.tokens.refresh_token : null,
1953
+ typeof auth.tokens?.refresh_token === "string" ? null : auth.tokens ?? null
1954
+ ])}`
1955
+ );
1956
+ } else {
1957
+ parts.push(
1958
+ `${label}:oauth_cc:${JSON.stringify([
1959
+ auth.credentials?.client_id ?? null,
1960
+ auth.credentials?.client_secret ?? null
1961
+ ])}`
1962
+ );
1963
+ }
1964
+ };
1965
+ describeAuth(options.auth, "run");
1966
+ for (const [key, entry] of Object.entries(options.agents ?? {}).sort(([a], [b]) => a < b ? -1 : 1)) {
1967
+ describeAuth(entry?.auth, `agent:${JSON.stringify([key, entry?.url ?? null])}`);
1968
+ }
1969
+ const kit = options.test_kit;
1970
+ if (kit) {
1971
+ for (const [key, value] of Object.entries(kit).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
1972
+ if (key === "auth") {
1973
+ for (const [authField, authValue] of Object.entries(value ?? {}).sort(
1974
+ ([a], [b]) => a < b ? -1 : a > b ? 1 : 0
1975
+ )) {
1976
+ if (authField === "probe_task") continue;
1977
+ if (authValue !== void 0 && authValue !== null) {
1978
+ parts.push(`test_kit:${JSON.stringify([`auth.${authField}`, authValue])}`);
1979
+ }
1980
+ }
1981
+ continue;
1982
+ }
1983
+ if (value !== void 0 && value !== null) parts.push(`test_kit:${JSON.stringify([key, value])}`);
1984
+ }
1985
+ }
1986
+ return parts.length > 0 ? parts.join("\0") : "none";
1987
+ }
1565
1988
  async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOffset, preSeeded) {
1566
1989
  const start = Date.now();
1567
1990
  const isMultiInstance = agentUrls.length > 1;
1568
1991
  const useRouting = options.agents !== void 0;
1992
+ let preparedGate = { used: false };
1993
+ if (options.prepared_fixtures !== void 0) {
1994
+ preparedGate = await gatePreparedFixtures(storyboard, agentUrls, options);
1995
+ if (preparedGate.used && "refusal" in preparedGate) {
1996
+ return denyPreparedFixtures(
1997
+ options,
1998
+ buildPreparedRefusalRunResult(agentUrls, storyboard, preparedGate.refusal),
1999
+ preparedGate.refusal.code
2000
+ );
2001
+ }
2002
+ }
1569
2003
  let clients;
1570
2004
  let routingContext;
1571
2005
  let profile;
@@ -1632,7 +2066,7 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
1632
2066
  }
1633
2067
  }
1634
2068
  }
1635
- if (preSeeded === void 0) {
2069
+ if (preSeeded === void 0 && !preparedGate.used) {
1636
2070
  const resetTargets = routingContext && options.agents ? [...routingContext.profiles.entries()].map(([key, profile2]) => ({
1637
2071
  client: routingContext.clients.get(key),
1638
2072
  options: (0, import_agent_routing.routedAgentOptions)(options.agents[key], options, profile2)
@@ -1667,7 +2101,9 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
1667
2101
  const routedControllerRoutes = /* @__PURE__ */ new Map();
1668
2102
  const routedErrors = /* @__PURE__ */ new Map();
1669
2103
  const reportedRoutingErrors = /* @__PURE__ */ new Set();
1670
- let context = { ...storyboard.context, ...options.context };
2104
+ const runContext = createRunContext(storyboard, options);
2105
+ const protectedWriter = runContext.protectedWriter;
2106
+ let context = runContext.context;
1671
2107
  const routingFailedStep = (step, phaseId, err) => {
1672
2108
  const detail = (0, import_oauth_metadata_graph.redactOAuthUrlsInText)(
1673
2109
  err instanceof import_agent_routing.RoutingError ? err.message : err?.message ?? String(err)
@@ -1864,8 +2300,6 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
1864
2300
  };
1865
2301
  }
1866
2302
  }
1867
- if (storyboard.context) (0, import_context.forwardAliasCache)(storyboard.context, context);
1868
- if (options.context) (0, import_context.forwardAliasCache)(options.context, context);
1869
2303
  const contributions = /* @__PURE__ */ new Set();
1870
2304
  const contributionSources = /* @__PURE__ */ new Map();
1871
2305
  const priorStepResults = /* @__PURE__ */ new Map();
@@ -2023,6 +2457,7 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2023
2457
  skippedCount++;
2024
2458
  }
2025
2459
  let seedingPhaseResult = null;
2460
+ let probeViolationStep;
2026
2461
  let seedingFailed = false;
2027
2462
  let creativeBuildCatalogFailed = false;
2028
2463
  let seedingMissingController = false;
@@ -2109,18 +2544,38 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2109
2544
  ]
2110
2545
  };
2111
2546
  } else if (resolution.status === "resolved") {
2112
- for (const key of [
2113
- "capability_id",
2114
- "target_format_id",
2115
- "message",
2116
- "creative_manifest",
2117
- "creative_id",
2118
- "transformer_id",
2119
- "config"
2120
- ]) {
2121
- delete context[`${handle}_${key}`];
2547
+ const probeViolation = tryProtectedContextPatch(
2548
+ context,
2549
+ {
2550
+ unset: CREATIVE_PROBE_CONTEXT_SUFFIXES.map((key) => `${handle}_${key}`),
2551
+ set: resolution.context
2552
+ },
2553
+ { origin: `creative_build_probe '${handle}'` }
2554
+ );
2555
+ if (probeViolation) {
2556
+ seedingFailed = true;
2557
+ failedCount += 1;
2558
+ probeViolationStep = {
2559
+ storyboard_id: storyboard.id,
2560
+ phase_id: "fixture_resolution",
2561
+ step_id: `creative_build_probe_context.${handle}`,
2562
+ title: "Publish creative build probe context",
2563
+ task: "get_adcp_capabilities",
2564
+ passed: false,
2565
+ duration_ms: 0,
2566
+ validations: [probeViolation],
2567
+ context: withoutProtectedCaptures(context),
2568
+ extraction: { path: "none" },
2569
+ error: probeViolation.error
2570
+ };
2571
+ seedingPhaseResult = {
2572
+ phase_id: "fixture_resolution",
2573
+ phase_title: "Creative build fixture resolution",
2574
+ passed: false,
2575
+ duration_ms: 0,
2576
+ steps: [probeViolationStep]
2577
+ };
2122
2578
  }
2123
- Object.assign(context, resolution.context);
2124
2579
  options = { ...options, contracts: [.../* @__PURE__ */ new Set([...options.contracts ?? [], "creative_build_probe_runner"])] };
2125
2580
  } else {
2126
2581
  fixtureUnsatisfied = true;
@@ -2132,10 +2587,45 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2132
2587
  };
2133
2588
  }
2134
2589
  }
2590
+ let preparedFixtureState;
2591
+ let preparedRefused;
2592
+ {
2593
+ const prepared = preparedGate;
2594
+ if (prepared.used && "state" in prepared) {
2595
+ const hydrationViolation = hydratePreparedCaptures(prepared.state, context, protectedWriter, contextProvenance);
2596
+ if (hydrationViolation) {
2597
+ if (!callerOwnsClients) await (0, import_protocols.closeScopedConnections)(options.protocol);
2598
+ return buildDiscoveryFailedResult(agentUrls, storyboard, {
2599
+ step: "Publish prepared fixture captures",
2600
+ passed: false,
2601
+ duration_ms: 0,
2602
+ error: hydrationViolation.error
2603
+ });
2604
+ }
2605
+ preparedFixtureState = prepared.state;
2606
+ fixtureBindings = (0, import_prepared_fixtures.preparedBindingRegistryFor)(prepared.state);
2607
+ fixtureResolutionRecords = prepared.state.resolutions;
2608
+ const disposition = prepared.state.disposition;
2609
+ if (disposition.status === "failed") {
2610
+ if (!callerOwnsClients) await (0, import_protocols.closeScopedConnections)(options.protocol);
2611
+ return buildDiscoveryFailedResult(agentUrls, storyboard, {
2612
+ step: "Reuse prepared fixture state",
2613
+ passed: false,
2614
+ duration_ms: 0,
2615
+ error: disposition.detail
2616
+ });
2617
+ }
2618
+ if (disposition.status === "unsatisfied") {
2619
+ fixtureUnsatisfied = true;
2620
+ fixtureCoverageGap = prepared.state.coverageGaps[0] ?? fixtureCoverageGap;
2621
+ }
2622
+ if (disposition.status === "unsupported") seedingMissingController = true;
2623
+ }
2624
+ }
2135
2625
  {
2136
2626
  let seeding;
2137
2627
  try {
2138
- seeding = creativeAssetFixtureGap || fixtureUnsatisfied || seedingFailed || skipControllerSeedingForPhaseGates ? null : preSeeded !== void 0 ? preSeeded.result : await (0, import_seeding.runControllerSeeding)(
2628
+ seeding = preparedFixtureState !== void 0 || creativeAssetFixtureGap || fixtureUnsatisfied || seedingFailed || skipControllerSeedingForPhaseGates ? null : preSeeded !== void 0 ? preSeeded.result : await (0, import_seeding.runControllerSeeding)(
2139
2629
  clients[0],
2140
2630
  storyboard,
2141
2631
  options,
@@ -2189,6 +2679,10 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2189
2679
  const attach = preSeeded === void 0 || preSeeded.attach;
2190
2680
  if (attach) {
2191
2681
  seedingPhaseResult = seeding.phase;
2682
+ if (probeViolationStep && !seeding.phase.steps.some((entry) => entry.step_id === probeViolationStep.step_id)) {
2683
+ seeding.phase.steps.unshift({ ...probeViolationStep, phase_id: seeding.phase.phase_id });
2684
+ seeding.phase.passed = false;
2685
+ }
2192
2686
  passedCount += seeding.passedCount;
2193
2687
  failedCount += seeding.failedCount;
2194
2688
  if (seeding.missingController || seeding.seedUnsupported || seeding.fixtureUnsatisfied) {
@@ -2196,6 +2690,56 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2196
2690
  }
2197
2691
  }
2198
2692
  fixtureBindings = seeding.bindings;
2693
+ if (seeding.preparedCaptures) {
2694
+ try {
2695
+ (0, import_protected_context.applyProtectedContextPatch)(
2696
+ context,
2697
+ { set: seeding.preparedCaptures.values, setProvenance: seeding.preparedCaptures.provenance },
2698
+ {
2699
+ writer: protectedWriter,
2700
+ sinks: { provenance: contextProvenance },
2701
+ origin: "fixture preparation"
2702
+ }
2703
+ );
2704
+ } catch (error) {
2705
+ if (!(error instanceof import_protected_context.ProtectedContextViolation)) throw error;
2706
+ seedingFailed = true;
2707
+ seeding.phase.passed = false;
2708
+ const alreadyReported = seeding.phase.steps.some((entry) => entry.step_id === FIXTURE_CAPTURE_STEP_ID);
2709
+ if (attach && !alreadyReported) {
2710
+ seeding.phase.steps.push({
2711
+ storyboard_id: storyboard.id,
2712
+ step_id: FIXTURE_CAPTURE_STEP_ID,
2713
+ phase_id: seeding.phase.phase_id,
2714
+ title: "Publish fixture captures",
2715
+ task: describeFixtureCaptureTask(seeding.preparedCaptures, error.keys),
2716
+ passed: false,
2717
+ duration_ms: 0,
2718
+ validations: [
2719
+ {
2720
+ check: "fixture_capture_unbuildable",
2721
+ passed: false,
2722
+ description: "Every declared fixture capture must be detachable from the pinned entity",
2723
+ error: error.message,
2724
+ json_pointer: null,
2725
+ expected: "a capture the runner can deep-clone and freeze",
2726
+ actual: error.keys.join(", "),
2727
+ schema_id: null,
2728
+ schema_url: null
2729
+ }
2730
+ ],
2731
+ // The commit failed, so nothing reserved is in `context` yet —
2732
+ // but the projection is applied unconditionally so a partially
2733
+ // applied patch can never surface a protected value in a
2734
+ // report.
2735
+ context: withoutProtectedCaptures(context),
2736
+ extraction: { path: "none" },
2737
+ error: error.message
2738
+ });
2739
+ failedCount += 1;
2740
+ }
2741
+ }
2742
+ }
2199
2743
  fixtureResolutionRecords = fixtureResolutionRecords ? [...fixtureResolutionRecords, ...seeding.resolutionRecords ?? []] : seeding.resolutionRecords;
2200
2744
  fixtureUnsatisfied = seeding.fixtureUnsatisfied === true && seeding.failedCount === 0;
2201
2745
  if (fixtureUnsatisfied) {
@@ -2214,12 +2758,13 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2214
2758
  ...record.product_handle && { product_handle: record.product_handle },
2215
2759
  requirements: record.requirements
2216
2760
  }));
2217
- const names = fixtures.map(
2218
- (fixture) => fixture.fixture_type === "pricing_option" ? `pricing_option "${fixture.product_handle}/${fixture.handle}"` : `product "${fixture.handle}"`
2219
- );
2220
2761
  fixtureCoverageGap = {
2221
2762
  reason: "fixture_unsatisfied",
2222
- detail: `fixture_unsatisfied: no seller fixture satisfied ${names.join(", ")}`,
2763
+ // The SAME spelling the disposition uses. Two renderings of one
2764
+ // reason over the same data meant a consumer parsing the
2765
+ // documented `<fixture_type>:<handle>` grammar matched on one
2766
+ // field and not the other.
2767
+ detail: describeUnsatisfiedFixtures(fixtureResolutionRecords),
2223
2768
  fixtures
2224
2769
  };
2225
2770
  }
@@ -2430,7 +2975,9 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2430
2975
  if (!routingContext && phaseContainsOauthMetadataProbe(phase) && !agentAdvertisesOauth(profile)) {
2431
2976
  phaseAbsent = true;
2432
2977
  }
2978
+ const priorPhaseContext = context;
2433
2979
  context = { ...context };
2980
+ (0, import_protected_context.forwardProtectedContext)(priorPhaseContext, context);
2434
2981
  if (seedingMissingController || seedingUnsupported || seedingFailed) {
2435
2982
  const cascadeSkip = seedingMissingController ? {
2436
2983
  skip_reason: "missing_test_controller",
@@ -2602,7 +3149,7 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2602
3149
  );
2603
3150
  const missingKeys = /* @__PURE__ */ new Set([
2604
3151
  ...(effectiveRequest.ok ? findUnresolvedContextVars(effectiveRequest.request) : []).filter((v) => hardKeys.has(v.key)).map((v) => v.key),
2605
- ...(step.context_inputs ?? []).filter((input) => !(input.key in context) && hardKeys.has(input.key)).map((input) => input.key)
3152
+ ...(step.context_inputs ?? []).filter((input) => !Object.hasOwn(context, input.key) && hardKeys.has(input.key)).map((input) => input.key)
2606
3153
  ]);
2607
3154
  if (missingKeys.size > 0) {
2608
3155
  capabilityUnavailable = false;
@@ -2663,6 +3210,78 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2663
3210
  missingPrerequisiteContextKeysByPhase
2664
3211
  );
2665
3212
  }
3213
+ if (preparedFixtureState !== void 0) {
3214
+ const recheck = await (0, import_prepared_fixtures.resolvePreparedFixtures)({
3215
+ supplied: options.prepared_fixtures,
3216
+ // The EFFECTIVE scope for this step: the routed options actually
3217
+ // dispatching it, plus its own auth directive.
3218
+ current: buildCurrentPreparedBinding(
3219
+ storyboard,
3220
+ agentUrls,
3221
+ assignment.options ?? options,
3222
+ step,
3223
+ options,
3224
+ context,
3225
+ {
3226
+ ...assignment.agentKey !== void 0 && { agentKey: assignment.agentKey },
3227
+ ...assignment.agentUrl !== void 0 && { agentUrl: assignment.agentUrl }
3228
+ },
3229
+ runnerVars
3230
+ ),
3231
+ ...options.prepared_fixtures_authorizer && { authorizer: options.prepared_fixtures_authorizer },
3232
+ stepId: step.id,
3233
+ gate: "step"
3234
+ });
3235
+ if (recheck !== void 0 && !recheck.ok) {
3236
+ const refusalResult = {
3237
+ storyboard_id: storyboard.id,
3238
+ step_id: step.id,
3239
+ phase_id: phase.id,
3240
+ title: step.title,
3241
+ task: step.task,
3242
+ passed: false,
3243
+ duration_ms: 0,
3244
+ validations: [
3245
+ {
3246
+ check: "prepared_fixtures_authorization",
3247
+ passed: false,
3248
+ // One description for all three gates. Three spellings of
3249
+ // the same check read to a report renderer as three kinds.
3250
+ description: "Prepared fixture state is re-validated and re-authorized on every lookup",
3251
+ error: recheck.refusal.detail,
3252
+ json_pointer: null,
3253
+ expected: "an authorized prepared state bound to this exact step scope",
3254
+ actual: recheck.refusal.code,
3255
+ schema_id: null,
3256
+ schema_url: null,
3257
+ // Explicit nulls, not omission: this is a PRE-WIRE
3258
+ // refusal, and an absent key cannot be told apart from a
3259
+ // runner that forgot to record one.
3260
+ request: null,
3261
+ response: null
3262
+ }
3263
+ ],
3264
+ // UNREDACTED. This result is never returned to the refused
3265
+ // caller — it goes to the host's authorized audit scope, where
3266
+ // withholding the evidence would defeat the point.
3267
+ context,
3268
+ extraction: { path: "none" },
3269
+ error: recheck.refusal.detail
3270
+ };
3271
+ stepResults.push(refusalResult);
3272
+ preparedRefused = recheck.refusal.code;
3273
+ failedCount++;
3274
+ phasePassed = false;
3275
+ countedAsFailed.add(refusalResult);
3276
+ priorStepResults.set(step.id, refusalResult);
3277
+ if (step.stateful) phaseStatefulCascades.set(phase.id, null);
3278
+ if (isMultiInstance || useRouting) {
3279
+ refusalResult.agent_url = (0, import_oauth_metadata_graph.redactOAuthUrlForOutput)(assignment.agentUrl);
3280
+ refusalResult.agent_index = assignment.instanceIndex + 1;
3281
+ }
3282
+ continue;
3283
+ }
3284
+ }
2666
3285
  const rawResult = routedCapabilityDetail !== void 0 ? buildPhaseCapabilitySkippedSteps(
2667
3286
  storyboard,
2668
3287
  { ...phase, steps: [step] },
@@ -2706,7 +3325,10 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2706
3325
  });
2707
3326
  continue;
2708
3327
  }
2709
- const raw = await spec.onStep(assertionContexts.get(spec.id), result2);
3328
+ const raw = await spec.onStep(assertionContexts.get(spec.id), {
3329
+ ...result2,
3330
+ context: withoutProtectedCaptures(result2.context ?? {})
3331
+ });
2710
3332
  for (const r of raw) {
2711
3333
  const full = { ...r, assertion_id: spec.id, scope: "step", step_id: step.id };
2712
3334
  assertionResults.push(full);
@@ -2984,6 +3606,8 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2984
3606
  ...assertionResults.length > 0 ? { assertions: assertionResults } : {},
2985
3607
  strict_validation_summary: strictSummary,
2986
3608
  notices,
3609
+ // `fixture_resolutions` carries `seller_ids`, so it is withheld on the
3610
+ // same condition as the context projection above.
2987
3611
  ...fixtureResolutionRecords && { fixture_resolutions: fixtureResolutionRecords },
2988
3612
  ...fixtureCoverageGap && { coverage_gaps: [fixtureCoverageGap] },
2989
3613
  ...routingContext && routingContext.discoveryFailures.length > 0 ? {
@@ -2998,14 +3622,26 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2998
3622
  await (0, import_protocols.closeScopedConnections)(options.protocol);
2999
3623
  }
3000
3624
  if (webhookReceiver) await webhookReceiver.close();
3625
+ if (preparedRefused !== void 0) {
3626
+ await emitPreparedFixturesAudit(options, result);
3627
+ throw new import_errors.PreparedFixturesDeniedError(preparedRefused);
3628
+ }
3001
3629
  return result;
3002
3630
  }
3003
3631
  async function runMultiPass(agentUrls, storyboard, options) {
3004
3632
  const start = Date.now();
3633
+ if (options.prepared_fixtures !== void 0) {
3634
+ const gate = await gatePreparedFixtures(storyboard, agentUrls, options, void 0, "run");
3635
+ if (gate.used && "refusal" in gate) {
3636
+ return denyPreparedFixtures(
3637
+ options,
3638
+ buildPreparedRefusalRunResult(agentUrls, storyboard, gate.refusal),
3639
+ gate.refusal.code
3640
+ );
3641
+ }
3642
+ }
3005
3643
  const preSeedClients = agentUrls.map((url) => (0, import_client.getOrCreateClientResolution)(url, options).client);
3006
- const preSeedContext = { ...storyboard.context, ...options.context };
3007
- if (storyboard.context) (0, import_context.forwardAliasCache)(storyboard.context, preSeedContext);
3008
- if (options.context) (0, import_context.forwardAliasCache)(options.context, preSeedContext);
3644
+ const preSeedContext = createRunContext(storyboard, options).context;
3009
3645
  let preSeedProfile = options._profile;
3010
3646
  if (!preSeedProfile) {
3011
3647
  const discovered = await (0, import_client.getOrDiscoverProfile)(preSeedClients[0], options);
@@ -3022,13 +3658,15 @@ async function runMultiPass(agentUrls, storyboard, options) {
3022
3658
  ...options.agentTools ? {} : { agentTools: (0, import_agent_routing.normalizeAgentToolNames)(preSeedProfile.tools) }
3023
3659
  };
3024
3660
  }
3025
- const resetFailure = await resetControllerStates(
3026
- preSeedClients.map((client) => ({ client, options })),
3027
- storyboard
3028
- );
3029
- if (resetFailure) {
3030
- await (0, import_protocols.closeScopedConnections)(options.protocol);
3031
- return buildDiscoveryFailedResult(agentUrls, storyboard, resetFailure);
3661
+ if (options.prepared_fixtures === void 0) {
3662
+ const resetFailure = await resetControllerStates(
3663
+ preSeedClients.map((client) => ({ client, options })),
3664
+ storyboard
3665
+ );
3666
+ if (resetFailure) {
3667
+ await (0, import_protocols.closeScopedConnections)(options.protocol);
3668
+ return buildDiscoveryFailedResult(agentUrls, storyboard, resetFailure);
3669
+ }
3032
3670
  }
3033
3671
  if (storyboardCapabilityPredicates(storyboard).length > 0) {
3034
3672
  const unmetDetail = evaluateStoryboardCapabilityGates(
@@ -3083,7 +3721,13 @@ async function runMultiPass(agentUrls, storyboard, options) {
3083
3721
  },
3084
3722
  preSeedExcludedPhaseIds
3085
3723
  );
3086
- const preSeededResult = preSeedFixtureGap || allExecutablePhasesCapabilitySkipped(storyboard, phaseCapabilitySkipDetails) ? null : await (0, import_seeding.runControllerSeeding)(preSeedClients[0], storyboard, options, preSeedContext);
3724
+ const preSeededResult = (
3725
+ // A pinned graph is authoritative across every pass. Re-seeding would
3726
+ // issue fresh `seed_*` mutations that replace the very seller state the
3727
+ // prepared captures describe, while the passes keep the original
3728
+ // bindings — the reset is guarded above for the same reason.
3729
+ options.prepared_fixtures !== void 0 || preSeedFixtureGap || allExecutablePhasesCapabilitySkipped(storyboard, phaseCapabilitySkipDetails) ? null : await (0, import_seeding.runControllerSeeding)(preSeedClients[0], storyboard, options, preSeedContext)
3730
+ );
3087
3731
  const passes = [];
3088
3732
  const passResults = [];
3089
3733
  for (let passIdx = 0; passIdx < agentUrls.length; passIdx++) {
@@ -3136,6 +3780,10 @@ async function runMultiPass(agentUrls, storyboard, options) {
3136
3780
  tested_at: (/* @__PURE__ */ new Date()).toISOString(),
3137
3781
  ...schemasDedup.length > 0 ? { schemas_used: schemasDedup } : {},
3138
3782
  ...assertionsAgg.length > 0 ? { assertions: assertionsAgg } : {},
3783
+ // `fixture_resolutions` carries `seller_ids`, so the aggregate withholds
3784
+ // it on the same condition the single-pass result does. Projecting
3785
+ // `phases` and `context` but republishing the pinned seller ids here
3786
+ // withheld the copies and published the original.
3139
3787
  ...first.fixture_resolutions ? { fixture_resolutions: first.fixture_resolutions } : {},
3140
3788
  ...first.coverage_gaps ? { coverage_gaps: first.coverage_gaps } : {},
3141
3789
  notices: noticesDedup
@@ -3265,8 +3913,200 @@ async function runStoryboardStep(agentUrl, storyboard, stepId, options = {}) {
3265
3913
  )
3266
3914
  );
3267
3915
  }
3916
+ function withoutProtectedCaptures(context) {
3917
+ const projected = {};
3918
+ for (const [key, value] of Object.entries(context)) {
3919
+ if (!(0, import_protected_context.isProtectedContextKey)(key)) projected[key] = value;
3920
+ }
3921
+ return projected;
3922
+ }
3923
+ async function denyPreparedFixtures(options, record, refusalCode) {
3924
+ await emitPreparedFixturesAudit(options, record);
3925
+ throw new import_errors.PreparedFixturesDeniedError(refusalCode);
3926
+ }
3927
+ async function emitPreparedFixturesAudit(options, record) {
3928
+ const sink = options.prepared_fixtures_audit_sink;
3929
+ if (!sink) return;
3930
+ try {
3931
+ await sink(record);
3932
+ } catch {
3933
+ }
3934
+ }
3935
+ function buildPreparedRefusalRunResult(agentUrls, storyboard, refusal) {
3936
+ const base = buildDiscoveryFailedResult(agentUrls, storyboard, {
3937
+ step: "Authorize prepared fixture state",
3938
+ passed: false,
3939
+ duration_ms: 0,
3940
+ error: refusal.detail
3941
+ });
3942
+ const validation = {
3943
+ check: "prepared_fixtures_authorization",
3944
+ passed: false,
3945
+ description: "Prepared fixture state is re-validated and re-authorized on every lookup",
3946
+ error: refusal.detail,
3947
+ json_pointer: null,
3948
+ expected: "an authorized prepared state bound to this exact run scope",
3949
+ actual: refusal.code,
3950
+ schema_id: null,
3951
+ schema_url: null,
3952
+ // Explicit nulls. This gate precedes all seller contact, so the
3953
+ // exchange genuinely did not happen — and an omitted key cannot be
3954
+ // told apart from a runner that forgot to record one.
3955
+ request: null,
3956
+ response: null
3957
+ };
3958
+ return {
3959
+ ...base,
3960
+ phases: base.phases.map((phase) => ({
3961
+ ...phase,
3962
+ steps: phase.steps.map((step) => ({ ...step, validations: [...step.validations, validation] }))
3963
+ }))
3964
+ };
3965
+ }
3966
+ function buildPreparedRefusalStepResult(storyboard, found, context, refusal, contributions) {
3967
+ return {
3968
+ storyboard_id: storyboard.id,
3969
+ step_id: found.step.id,
3970
+ phase_id: found.phaseId,
3971
+ title: found.step.title,
3972
+ task: found.step.task,
3973
+ passed: false,
3974
+ duration_ms: 0,
3975
+ validations: [
3976
+ {
3977
+ check: "prepared_fixtures_authorization",
3978
+ passed: false,
3979
+ description: "Prepared fixture state is re-validated and re-authorized on every lookup",
3980
+ error: refusal.detail,
3981
+ json_pointer: null,
3982
+ // `step scope`, matching the per-step gate. This builds a STEP
3983
+ // result; saying `run scope` here made the published grammar
3984
+ // disagree with itself depending on which gate refused.
3985
+ expected: "an authorized prepared state bound to this exact step scope",
3986
+ actual: refusal.code,
3987
+ schema_id: null,
3988
+ schema_url: null,
3989
+ request: null,
3990
+ response: null
3991
+ }
3992
+ ],
3993
+ // UNREDACTED: this record is the host's audit evidence, never the
3994
+ // refused caller's report.
3995
+ context,
3996
+ extraction: { path: "none" },
3997
+ contributions: Array.from(contributions),
3998
+ error: refusal.detail
3999
+ };
4000
+ }
4001
+ function buildPreparedDispositionStepResult(storyboard, found, context, disposition, contributions) {
4002
+ const detail = "detail" in disposition ? disposition.detail : "fixture preparation did not resolve";
4003
+ if (disposition.status === "failed") {
4004
+ return {
4005
+ storyboard_id: storyboard.id,
4006
+ step_id: found.step.id,
4007
+ phase_id: found.phaseId,
4008
+ title: found.step.title,
4009
+ task: found.step.task,
4010
+ passed: false,
4011
+ duration_ms: 0,
4012
+ // A failed step MUST carry at least one failing validation
4013
+ // (runner-output-contract.yaml, `step_result`). The `error`-only
4014
+ // carve-out beside that rule is for TRANSPORT failures, and a failed
4015
+ // preparation is discovery, schema validation or setup — none of
4016
+ // which is transport. Without this the caller saw a red step with no
4017
+ // `check` discriminator and had to regex the prose, which is the one
4018
+ // thing the synthesized kinds exist to avoid.
4019
+ validations: [
4020
+ {
4021
+ check: "fixture_capture_unbuildable",
4022
+ passed: false,
4023
+ description: "Fixture preparation must resolve before a step can reuse it",
4024
+ error: detail,
4025
+ json_pointer: null,
4026
+ expected: "a fixture graph that resolved during preparation",
4027
+ actual: "preparation_failed",
4028
+ schema_id: null,
4029
+ schema_url: null
4030
+ }
4031
+ ],
4032
+ context: withoutProtectedCaptures(context),
4033
+ extraction: { path: "none" },
4034
+ contributions: Array.from(contributions),
4035
+ error: detail
4036
+ };
4037
+ }
4038
+ return {
4039
+ storyboard_id: storyboard.id,
4040
+ step_id: found.step.id,
4041
+ phase_id: found.phaseId,
4042
+ title: found.step.title,
4043
+ task: found.step.task,
4044
+ passed: true,
4045
+ skipped: true,
4046
+ skip_reason: disposition.status === "unsatisfied" ? "fixture_unsatisfied" : "missing_test_controller",
4047
+ // Match the full runner: reporting a missing controller as a
4048
+ // branch-substitutable `not_applicable` would let a consumer credit a
4049
+ // peer path that does not exist.
4050
+ //
4051
+ // `fixture_unsatisfied` is now the canonical reason too, not a
4052
+ // detailed one mapped down to `not_applicable`. The published contract
4053
+ // lists it as a canonical `skip_result.reason`, and "our other code
4054
+ // path spells it differently" was SDK precedent, not a spec argument —
4055
+ // which the repo's own `spec → mock → SDK` triage order resolves
4056
+ // against the SDK. Promoting it in `RunnerSkipReason` fixed both paths
4057
+ // at once, so the two no longer disagree.
4058
+ skip: {
4059
+ reason: disposition.status === "unsatisfied" ? "fixture_unsatisfied" : "missing_test_controller",
4060
+ detail
4061
+ },
4062
+ duration_ms: 0,
4063
+ validations: [],
4064
+ context: withoutProtectedCaptures(context),
4065
+ extraction: { path: "none" },
4066
+ contributions: Array.from(contributions)
4067
+ };
4068
+ }
3268
4069
  async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
3269
4070
  (0, import_test_kit.validateTestKit)(options.test_kit);
4071
+ const metadataRefusal = describeReservedCallerOptions(options);
4072
+ if (metadataRefusal !== void 0) {
4073
+ const target = flattenSteps(storyboard).find((entry) => entry.step.id === stepId);
4074
+ if (!target) throw new Error(`Step "${stepId}" not found in storyboard "${storyboard.id}".`);
4075
+ return denyPreparedFixtures(
4076
+ options,
4077
+ buildPreparedRefusalStepResult(
4078
+ storyboard,
4079
+ target,
4080
+ {},
4081
+ { code: "unauthorized_metadata", detail: metadataRefusal },
4082
+ options.contributions ?? []
4083
+ ),
4084
+ "unauthorized_metadata"
4085
+ );
4086
+ }
4087
+ let standaloneGate = { used: false };
4088
+ if (options.prepared_fixtures !== void 0) {
4089
+ const target = flattenSteps(storyboard).find((entry) => entry.step.id === stepId);
4090
+ if (!target) {
4091
+ throw new Error(
4092
+ `Step "${stepId}" not found in storyboard "${storyboard.id}". Available steps: ${flattenSteps(storyboard).map((entry) => entry.step.id).join(", ")}`
4093
+ );
4094
+ }
4095
+ standaloneGate = await gatePreparedFixtures(storyboard, [agentUrl], options, target.step, "step");
4096
+ if (standaloneGate.used && "refusal" in standaloneGate) {
4097
+ return denyPreparedFixtures(
4098
+ options,
4099
+ buildPreparedRefusalStepResult(
4100
+ storyboard,
4101
+ target,
4102
+ createRunContext(storyboard, options).context,
4103
+ standaloneGate.refusal,
4104
+ options.contributions ?? []
4105
+ ),
4106
+ standaloneGate.refusal.code
4107
+ );
4108
+ }
4109
+ }
3270
4110
  if (storyboard.requires?.[0] === "property_list_reference_runner" && storyboardCapabilityPredicates(storyboard).length === 0 && propertyListReferenceNeedsMcp(options)) {
3271
4111
  const found2 = flattenSteps(storyboard).find((entry) => entry.step.id === stepId);
3272
4112
  if (!found2) throw new Error(`Step "${stepId}" not found in storyboard "${storyboard.id}".`);
@@ -3286,7 +4126,9 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
3286
4126
  },
3287
4127
  duration_ms: 0,
3288
4128
  validations: [],
3289
- context: { ...storyboard.context, ...options.context },
4129
+ // Validated the same way as the executing path so a reserved key in
4130
+ // caller context fails closed on the early-return branch too.
4131
+ context: createRunContext(storyboard, options).context,
3290
4132
  extraction: { path: "none" }
3291
4133
  };
3292
4134
  }
@@ -3306,9 +4148,9 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
3306
4148
  } else {
3307
4149
  profile = options._profile;
3308
4150
  }
3309
- const context = { ...storyboard.context, ...options.context };
3310
- if (storyboard.context) (0, import_context.forwardAliasCache)(storyboard.context, context);
3311
- if (options.context) (0, import_context.forwardAliasCache)(options.context, context);
4151
+ const standaloneRunContext = createRunContext(storyboard, options);
4152
+ const protectedWriter = standaloneRunContext.protectedWriter;
4153
+ const context = standaloneRunContext.context;
3312
4154
  const allSteps = flattenSteps(storyboard);
3313
4155
  const found = allSteps.find((s) => s.step.id === stepId);
3314
4156
  if (!found) {
@@ -3389,7 +4231,11 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
3389
4231
  passed: true,
3390
4232
  skipped: true,
3391
4233
  skip_reason: "fixture_unsatisfied",
3392
- skip: { reason: "not_applicable", detail: resolution.detail },
4234
+ // Canonical too, now that `fixture_unsatisfied` is in
4235
+ // `RunnerSkipReason`. Both unsatisfied paths spell it the same way,
4236
+ // which is what the contract asks for and what the previous
4237
+ // `not_applicable` mapping obscured.
4238
+ skip: { reason: "fixture_unsatisfied", detail: resolution.detail },
3393
4239
  duration_ms: 0,
3394
4240
  validations: [],
3395
4241
  context,
@@ -3397,18 +4243,30 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
3397
4243
  contributions: Array.from(options.contributions ?? [])
3398
4244
  };
3399
4245
  }
3400
- for (const key of [
3401
- "capability_id",
3402
- "target_format_id",
3403
- "message",
3404
- "creative_manifest",
3405
- "creative_id",
3406
- "transformer_id",
3407
- "config"
3408
- ]) {
3409
- delete context[`${storyboard.creative_build_probe.context_prefix}_${key}`];
3410
- }
3411
- Object.assign(context, resolution.context);
4246
+ const probeViolation = tryProtectedContextPatch(
4247
+ context,
4248
+ {
4249
+ unset: CREATIVE_PROBE_CONTEXT_SUFFIXES.map((key) => `${storyboard.creative_build_probe.context_prefix}_${key}`),
4250
+ set: resolution.context
4251
+ },
4252
+ { origin: `creative_build_probe '${storyboard.creative_build_probe.context_prefix}'` }
4253
+ );
4254
+ if (probeViolation) {
4255
+ return {
4256
+ storyboard_id: storyboard.id,
4257
+ step_id: found.step.id,
4258
+ phase_id: found.phaseId,
4259
+ title: found.step.title,
4260
+ task: found.step.task,
4261
+ passed: false,
4262
+ duration_ms: 0,
4263
+ validations: [probeViolation],
4264
+ context: withoutProtectedCaptures(context),
4265
+ extraction: { path: "none" },
4266
+ contributions: Array.from(options.contributions ?? []),
4267
+ error: probeViolation.error
4268
+ };
4269
+ }
3412
4270
  options = { ...options, contracts: [.../* @__PURE__ */ new Set([...options.contracts ?? [], "creative_build_probe_runner"])] };
3413
4271
  }
3414
4272
  const injectedReceiver = options._webhookReceiver;
@@ -3448,8 +4306,35 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
3448
4306
  agentProfile: profile,
3449
4307
  agentLibraryVersion: profile?.library_version,
3450
4308
  storyboardRequiresRequestSigner: resolveStoryboardRequires(storyboard, options).includes("request_signer"),
3451
- storyboardRequiresPublisherAuthRunner: storyboard.requires?.includes("trusted_match_publisher_auth_runner") === true
4309
+ storyboardRequiresPublisherAuthRunner: storyboard.requires?.includes("trusted_match_publisher_auth_runner") === true,
4310
+ protectedWriter
3452
4311
  };
4312
+ const prepared = standaloneGate;
4313
+ if (prepared.used && "state" in prepared) {
4314
+ const disposition = prepared.state.disposition;
4315
+ if (disposition.status !== "resolved") {
4316
+ return buildPreparedDispositionStepResult(storyboard, found, context, disposition, contributions);
4317
+ }
4318
+ const hydrationViolation = hydratePreparedCaptures(prepared.state, context, protectedWriter, contextProvenance);
4319
+ if (hydrationViolation) {
4320
+ return {
4321
+ storyboard_id: storyboard.id,
4322
+ step_id: found.step.id,
4323
+ phase_id: found.phaseId,
4324
+ title: found.step.title,
4325
+ task: found.step.task,
4326
+ passed: false,
4327
+ duration_ms: 0,
4328
+ validations: [hydrationViolation],
4329
+ context: withoutProtectedCaptures(context),
4330
+ extraction: { path: "none" },
4331
+ contributions: Array.from(contributions),
4332
+ error: hydrationViolation.error
4333
+ };
4334
+ }
4335
+ runState.fixtureBindings = (0, import_prepared_fixtures.preparedBindingRegistryFor)(prepared.state);
4336
+ runState.preparedFixtures = prepared.state;
4337
+ }
3453
4338
  let prerequisiteFailure;
3454
4339
  if (storyboard.id === "media_buy_seller/acceptance_policy_discovery" && found.step.id === "get_contextual_products" && options.acceptancePolicyDiscovery?.enabled !== false) {
3455
4340
  const capabilityStep = allSteps.find((value) => value.step.id === "get_acceptance_policy_capability");
@@ -3505,7 +4390,11 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
3505
4390
  contributions.add(found.step.contributes_to);
3506
4391
  }
3507
4392
  }
3508
- return { ...result, contributions: Array.from(contributions) };
4393
+ return {
4394
+ ...result,
4395
+ context: withoutProtectedCaptures(result.context ?? {}),
4396
+ contributions: Array.from(contributions)
4397
+ };
3509
4398
  } finally {
3510
4399
  await Promise.all([
3511
4400
  ...!clientResolution.reusedShared ? [(0, import_protocols.closeScopedConnections)(options.protocol)] : [],
@@ -3515,7 +4404,7 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
3515
4404
  }
3516
4405
  function unresolvedStepContextVars(step, request, context, runState) {
3517
4406
  const unavailableInputs = (step.context_inputs ?? []).filter(
3518
- (input) => !(input.key in context) && (runState.capabilityUnavailableContextKeys?.has(input.key) === true || runState.missingPrerequisiteContextKeys?.has(input.key) === true)
4407
+ (input) => !Object.hasOwn(context, input.key) && (runState.capabilityUnavailableContextKeys?.has(input.key) === true || runState.missingPrerequisiteContextKeys?.has(input.key) === true)
3519
4408
  );
3520
4409
  return [
3521
4410
  ...findUnresolvedContextVars(request),
@@ -3808,7 +4697,7 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
3808
4697
  synthesized.push({
3809
4698
  check: "unresolved_substitution",
3810
4699
  passed: false,
3811
- description: v.token === import_creative_assets.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}`,
4700
+ description: v.token === import_creative_assets.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}`,
3812
4701
  json_pointer: null,
3813
4702
  expected: v.token,
3814
4703
  actual: null,
@@ -4327,21 +5216,38 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
4327
5216
  allSteps
4328
5217
  );
4329
5218
  if (responseDerivedSkip && !step.expect_error) {
4330
- for (const key of responseDerivedSkip.contextKeys) {
4331
- runState.responseDerivedNotApplicableContextKeys?.set(key, responseDerivedSkip.detail);
4332
- }
5219
+ const unavailableViolation = tryProtectedContextPatch(
5220
+ context,
5221
+ {
5222
+ markUnavailable: Object.fromEntries(
5223
+ responseDerivedSkip.contextKeys.map((key) => [key, responseDerivedSkip.detail])
5224
+ )
5225
+ },
5226
+ {
5227
+ ...runState.responseDerivedNotApplicableContextKeys && {
5228
+ sinks: { unavailable: runState.responseDerivedNotApplicableContextKeys }
5229
+ },
5230
+ origin: `step '${step.id}' not_applicable_if response gate`
5231
+ }
5232
+ );
4333
5233
  const next2 = getNextStepPreview(step.id, allSteps, context, runState.runnerVars);
4334
5234
  return {
4335
5235
  step_id: step.id,
4336
5236
  phase_id: phaseId,
4337
5237
  title: step.title,
4338
5238
  task: step.task,
4339
- passed: true,
4340
- skipped: true,
4341
- skip_reason: "not_applicable",
4342
- skip: buildSkip("not_applicable", responseDerivedSkip.detail),
5239
+ // A refused metadata write is a failure, not a skip: the gate did not
5240
+ // actually mark anything unavailable, so reporting `not_applicable`
5241
+ // would claim a disposition the runner never reached.
5242
+ passed: unavailableViolation === void 0,
5243
+ ...unavailableViolation === void 0 && {
5244
+ skipped: true,
5245
+ skip_reason: "not_applicable",
5246
+ skip: buildSkip("not_applicable", responseDerivedSkip.detail)
5247
+ },
4343
5248
  duration_ms: stepResult.duration_ms,
4344
- validations: [],
5249
+ validations: unavailableViolation ? [unavailableViolation] : [],
5250
+ ...unavailableViolation && { error: unavailableViolation.error },
4345
5251
  context,
4346
5252
  ...responseDerivedContextResult(runState),
4347
5253
  response: (0, import_redact_secrets.redactSecrets)(taskResult?.data),
@@ -4581,33 +5487,60 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
4581
5487
  }
4582
5488
  const updatedContext = { ...context };
4583
5489
  (0, import_context.forwardAliasCache)(context, updatedContext);
5490
+ (0, import_protected_context.forwardProtectedContext)(context, updatedContext);
5491
+ const protectedSinks = {
5492
+ ...runState.contextProvenance && { provenance: runState.contextProvenance },
5493
+ ...runState.responseDerivedNotApplicableContextKeys && {
5494
+ unavailable: runState.responseDerivedNotApplicableContextKeys
5495
+ }
5496
+ };
5497
+ const protectedViolations = [];
5498
+ const commitContextPatch = (patch, origin) => {
5499
+ try {
5500
+ (0, import_protected_context.applyProtectedContextPatch)(updatedContext, patch, { sinks: protectedSinks, origin });
5501
+ } catch (error) {
5502
+ if (!(error instanceof import_protected_context.ProtectedContextViolation)) throw error;
5503
+ protectedViolations.push({
5504
+ check: "protected_context_violation",
5505
+ passed: false,
5506
+ description: "Reserved fixture-preparation context keys are immutable for the rest of the run",
5507
+ error: error.message,
5508
+ json_pointer: null,
5509
+ expected: "no write, delete or metadata change to the __adcp_fixture_ namespace",
5510
+ actual: error.keys.join(", "),
5511
+ schema_id: null,
5512
+ schema_url: null
5513
+ });
5514
+ }
5515
+ };
4584
5516
  const hasData = taskResult?.data !== void 0 && taskResult?.data !== null;
4585
5517
  if (passed && hasData && taskResult && !(effectiveStep.task === "sync_accounts" && request.dry_run === true)) {
4586
5518
  const extracted = (0, import_context.extractContextWithProvenance)(effectiveStep.task, taskResult.data, step.id);
5519
+ const conventionUnset = [];
4587
5520
  for (const group of extracted.clearGroups ?? []) {
4588
5521
  if (group.when && !group.when.values.includes(updatedContext[group.when.key])) continue;
4589
5522
  for (const key of group.keys) {
4590
5523
  const provenance = runState.contextProvenance?.get(key);
4591
5524
  if (provenance && provenance.source_kind !== "convention") continue;
4592
- delete updatedContext[key];
4593
- runState.responseDerivedNotApplicableContextKeys?.delete(key);
4594
- runState.contextProvenance?.delete(key);
4595
- }
4596
- }
4597
- Object.assign(updatedContext, extracted.values);
4598
- for (const key of Object.keys(extracted.values)) {
4599
- runState.responseDerivedNotApplicableContextKeys?.delete(key);
4600
- }
4601
- if (runState.contextProvenance) {
4602
- for (const [key, entry] of Object.entries(extracted.provenance)) {
4603
- runState.contextProvenance.set(key, entry);
5525
+ conventionUnset.push(key);
4604
5526
  }
4605
5527
  }
5528
+ commitContextPatch(
5529
+ {
5530
+ set: extracted.values,
5531
+ unset: conventionUnset,
5532
+ setProvenance: extracted.provenance,
5533
+ unsetProvenance: conventionUnset,
5534
+ clearUnavailable: [...conventionUnset, ...Object.keys(extracted.values)]
5535
+ },
5536
+ `step '${step.id}' convention extraction for ${effectiveStep.task}`
5537
+ );
4606
5538
  }
4607
5539
  if (step.context_outputs?.length) {
4608
- for (const output of step.context_outputs) {
4609
- if (output.key) runState.responseDerivedNotApplicableContextKeys?.delete(output.key);
4610
- }
5540
+ commitContextPatch(
5541
+ { clearUnavailable: step.context_outputs.map((output) => output.key).filter((key) => typeof key === "string") },
5542
+ `step '${step.id}' context_outputs availability reset`
5543
+ );
4611
5544
  const taskCompletionResolution = await resolveTaskCompletionOutputs(
4612
5545
  taskResult,
4613
5546
  step.context_outputs,
@@ -4624,15 +5557,14 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
4624
5557
  effectiveStep.task,
4625
5558
  updatedContext
4626
5559
  );
4627
- Object.assign(updatedContext, explicit.values);
4628
- for (const key of Object.keys(explicit.values)) {
4629
- runState.responseDerivedNotApplicableContextKeys?.delete(key);
4630
- }
4631
- if (runState.contextProvenance) {
4632
- for (const [key, entry] of Object.entries(explicit.provenance)) {
4633
- runState.contextProvenance.set(key, entry);
4634
- }
4635
- }
5560
+ commitContextPatch(
5561
+ {
5562
+ set: explicit.values,
5563
+ setProvenance: explicit.provenance,
5564
+ clearUnavailable: Object.keys(explicit.values)
5565
+ },
5566
+ `step '${step.id}' context_outputs capture`
5567
+ );
4636
5568
  if (explicit.failures && explicit.failures.length > 0) {
4637
5569
  for (const failure of explicit.failures) {
4638
5570
  const wasTaskCompletion = step.context_outputs.some(
@@ -4669,6 +5601,7 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
4669
5601
  }
4670
5602
  }
4671
5603
  }
5604
+ if (protectedViolations.length > 0) validations.push(...protectedViolations);
4672
5605
  const allValidationsPassedFinal = validations.every((v) => !(0, import_validations.validationFailsStep)(v));
4673
5606
  const stepFailed = !(passed && allValidationsPassedFinal);
4674
5607
  const contextRejectionHints = stepFailed && runState.contextProvenance ? (0, import_rejection_hints.detectContextRejectionHints)(taskResult, request, context, runState.contextProvenance, effectiveStep.task) : [];
@@ -4699,9 +5632,21 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
4699
5632
  // modern `response_record.payload` path is already redacted.
4700
5633
  response: (0, import_redact_secrets.redactSecrets)(taskResult?.data),
4701
5634
  validations,
5635
+ // NOT projected here: the full runner threads this map from step to
5636
+ // step, so stripping the reserved namespace would break the very
5637
+ // substitution the captures exist for. The STANDALONE entry point
5638
+ // projects it on the way out — see `runStoryboardStepBody` — because
5639
+ // there the map is a thread-back channel the next call refuses.
4702
5640
  context: updatedContext,
4703
5641
  ...runState.contextProvenance && runState.contextProvenance.size > 0 && {
4704
- context_provenance: Object.fromEntries(runState.contextProvenance)
5642
+ // Reserved-namespace provenance is withheld from the echo: the
5643
+ // standalone contract threads this map back in on the NEXT call,
5644
+ // where `assertUnreservedCallerMetadata` refuses it. Emitting
5645
+ // something we then reject would break the hosted step loop this
5646
+ // feature exists for.
5647
+ context_provenance: Object.fromEntries(
5648
+ [...runState.contextProvenance].filter(([key]) => !(0, import_protected_context.isProtectedContextKey)(key))
5649
+ )
4705
5650
  },
4706
5651
  ...responseDerivedContextResult(runState),
4707
5652
  error: step.expect_error || schemaRejectionIsAdvisory ? void 0 : truncateError(stepResult.error || taskResult?.error),
@@ -6715,12 +7660,22 @@ function collectUpstreamIdentifierDigests(validations, requestPayload, sampleReq
6715
7660
  function sha256Hex(value) {
6716
7661
  return (0, import_node_crypto.createHash)("sha256").update(value, "utf8").digest("hex");
6717
7662
  }
7663
+ const DOTTED_CONTEXT_TOKEN_RE = /^\$context\.(\w+)((?:\.\w+|\[\d+\])+)$/;
7664
+ function suggestedCapturePath(token) {
7665
+ const match = token.match(DOTTED_CONTEXT_TOKEN_RE);
7666
+ const accessor = match?.[2] ?? "";
7667
+ return accessor.startsWith(".") ? accessor.slice(1) : accessor;
7668
+ }
6718
7669
  function findUnresolvedContextVars(obj) {
6719
7670
  const vars = [];
6720
7671
  const walk = (val) => {
6721
7672
  if (typeof val === "string") {
6722
7673
  const match = val.match(/^\$context\.(\w+)$/);
6723
7674
  if (match?.[1]) vars.push({ key: match[1], token: val });
7675
+ else {
7676
+ const dotted = val.match(DOTTED_CONTEXT_TOKEN_RE);
7677
+ if (dotted?.[1]) vars.push({ key: dotted[1], token: val });
7678
+ }
6724
7679
  } else if (Array.isArray(val)) {
6725
7680
  val.forEach(walk);
6726
7681
  } else if (val !== null && typeof val === "object") {
@@ -6921,6 +7876,7 @@ function getFirstStepPreview(storyboard, context = {}) {
6921
7876
  getFirstStepPreview,
6922
7877
  listStrictOnlyFailures,
6923
7878
  prepareGovernedStoryboardWireArgs,
7879
+ prepareStoryboardFixtures,
6924
7880
  resolveCapabilityPath,
6925
7881
  runStoryboard,
6926
7882
  runStoryboardStep,