@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.
- package/AGENTS.md +2 -0
- package/dist/lib/adapters/version/3.1/assignments.d.mts +12 -0
- package/dist/lib/adapters/version/3.1/assignments.d.ts +12 -0
- package/dist/lib/adapters/version/3.1/assignments.js +67 -0
- package/dist/lib/adapters/version/3.1/assignments.mjs +43 -0
- package/dist/lib/adapters/version/3.1/bidding.d.mts +3 -0
- package/dist/lib/adapters/version/3.1/bidding.d.ts +3 -0
- package/dist/lib/adapters/version/3.1/bidding.js +115 -0
- package/dist/lib/adapters/version/3.1/bidding.mjs +90 -0
- package/dist/lib/adapters/version/3.1/delivery.d.mts +7 -0
- package/dist/lib/adapters/version/3.1/delivery.d.ts +7 -0
- package/dist/lib/adapters/version/3.1/delivery.js +78 -0
- package/dist/lib/adapters/version/3.1/delivery.mjs +54 -0
- package/dist/lib/adapters/version/3.1/index.d.mts +3 -0
- package/dist/lib/adapters/version/3.1/index.d.ts +3 -0
- package/dist/lib/adapters/version/3.1/index.js +36 -0
- package/dist/lib/adapters/version/3.1/index.mjs +12 -0
- package/dist/lib/adapters/version/index.d.mts +59 -7
- package/dist/lib/adapters/version/index.d.ts +59 -7
- package/dist/lib/adapters/version/index.js +51 -2
- package/dist/lib/adapters/version/index.mjs +49 -1
- package/dist/lib/adapters/version/negotiate.d.mts +71 -0
- package/dist/lib/adapters/version/negotiate.d.ts +71 -0
- package/dist/lib/adapters/version/negotiate.js +101 -0
- package/dist/lib/adapters/version/negotiate.mjs +73 -0
- package/dist/lib/adapters/version/schema-projection.d.mts +33 -0
- package/dist/lib/adapters/version/schema-projection.d.ts +33 -0
- package/dist/lib/adapters/version/schema-projection.js +294 -0
- package/dist/lib/adapters/version/schema-projection.mjs +269 -0
- package/dist/lib/adapters/version/translate.d.mts +122 -0
- package/dist/lib/adapters/version/translate.d.ts +122 -0
- package/dist/lib/adapters/version/translate.js +169 -0
- package/dist/lib/adapters/version/translate.mjs +141 -0
- package/dist/lib/adapters/version/types.d.mts +51 -10
- package/dist/lib/adapters/version/types.d.ts +51 -10
- package/dist/lib/core/AgentClient.d.mts +1 -1
- package/dist/lib/core/AgentClient.d.ts +1 -1
- package/dist/lib/core/AgentClient.js +17 -1
- package/dist/lib/core/AgentClient.mjs +17 -1
- package/dist/lib/core/ConversationTypes.d.mts +7 -0
- package/dist/lib/core/ConversationTypes.d.ts +7 -0
- package/dist/lib/core/SingleAgentClient.d.mts +82 -0
- package/dist/lib/core/SingleAgentClient.d.ts +82 -0
- package/dist/lib/core/SingleAgentClient.js +336 -15
- package/dist/lib/core/SingleAgentClient.mjs +350 -17
- package/dist/lib/core/TaskExecutor.d.mts +1 -1
- package/dist/lib/core/TaskExecutor.d.ts +1 -1
- package/dist/lib/core/TaskExecutor.js +182 -31
- package/dist/lib/core/TaskExecutor.mjs +183 -32
- package/dist/lib/core/account-reduction.d.mts +36 -0
- package/dist/lib/core/account-reduction.d.ts +36 -0
- package/dist/lib/core/account-reduction.js +127 -0
- package/dist/lib/core/account-reduction.mjs +98 -0
- package/dist/lib/core/buyer-account-registry.d.mts +52 -0
- package/dist/lib/core/buyer-account-registry.d.ts +52 -0
- package/dist/lib/core/buyer-account-registry.js +142 -6
- package/dist/lib/core/buyer-account-registry.mjs +142 -6
- package/dist/lib/errors/index.d.mts +34 -0
- package/dist/lib/errors/index.d.ts +34 -0
- package/dist/lib/errors/index.js +11 -0
- package/dist/lib/errors/index.mjs +10 -0
- package/dist/lib/index.d.mts +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.js +2 -0
- package/dist/lib/index.mjs +2 -0
- package/dist/lib/media-buy/action-contracts.js +4 -4
- package/dist/lib/media-buy/action-contracts.mjs +4 -4
- package/dist/lib/media-buy/action-types.d.mts +3 -2
- package/dist/lib/media-buy/action-types.d.ts +3 -2
- package/dist/lib/media-buy/compatibility.d.mts +1 -0
- package/dist/lib/media-buy/compatibility.d.ts +1 -0
- package/dist/lib/media-buy/compatibility.js +33 -10
- package/dist/lib/media-buy/compatibility.mjs +32 -14
- package/dist/lib/media-buy/update-fields.generated.d.mts +3 -2
- package/dist/lib/media-buy/update-fields.generated.d.ts +3 -2
- package/dist/lib/negotiation/buyer.js +11 -5
- package/dist/lib/negotiation/buyer.mjs +11 -5
- package/dist/lib/negotiation/types.d.mts +4 -2
- package/dist/lib/negotiation/types.d.ts +4 -2
- package/dist/lib/negotiation/verification.d.mts +15 -6
- package/dist/lib/negotiation/verification.d.ts +15 -6
- package/dist/lib/negotiation/verification.js +146 -27
- package/dist/lib/negotiation/verification.mjs +146 -28
- package/dist/lib/negotiation/version.d.mts +6 -0
- package/dist/lib/negotiation/version.d.ts +6 -0
- package/dist/lib/negotiation/version.js +60 -0
- package/dist/lib/negotiation/version.mjs +33 -0
- package/dist/lib/registry/types.generated.d.mts +1 -1
- package/dist/lib/registry/types.generated.d.ts +1 -1
- package/dist/lib/server/create-adcp-server.d.mts +21 -0
- package/dist/lib/server/create-adcp-server.d.ts +21 -0
- package/dist/lib/server/create-adcp-server.js +98 -16
- package/dist/lib/server/create-adcp-server.mjs +84 -2
- package/dist/lib/server/error-arm-tools.d.mts +10 -10
- package/dist/lib/server/error-arm-tools.d.ts +10 -10
- package/dist/lib/server/error-arm-tools.js +22 -58
- package/dist/lib/server/error-arm-tools.mjs +30 -65
- package/dist/lib/server/index.d.mts +1 -1
- package/dist/lib/server/index.d.ts +1 -1
- package/dist/lib/storage/interfaces.d.mts +2 -0
- package/dist/lib/storage/interfaces.d.ts +2 -0
- package/dist/lib/testing/compliance/comply.js +17 -0
- package/dist/lib/testing/compliance/comply.mjs +17 -0
- package/dist/lib/testing/index.d.mts +3 -0
- package/dist/lib/testing/index.d.ts +3 -0
- package/dist/lib/testing/index.js +44 -2
- package/dist/lib/testing/index.mjs +42 -0
- package/dist/lib/testing/storyboard/collection-selection.d.mts +259 -0
- package/dist/lib/testing/storyboard/collection-selection.d.ts +259 -0
- package/dist/lib/testing/storyboard/collection-selection.js +384 -0
- package/dist/lib/testing/storyboard/collection-selection.mjs +348 -0
- package/dist/lib/testing/storyboard/context.js +12 -6
- package/dist/lib/testing/storyboard/context.mjs +12 -6
- package/dist/lib/testing/storyboard/fixture-resolution.d.mts +39 -1
- package/dist/lib/testing/storyboard/fixture-resolution.d.ts +39 -1
- package/dist/lib/testing/storyboard/fixture-resolution.js +216 -12
- package/dist/lib/testing/storyboard/fixture-resolution.mjs +218 -12
- package/dist/lib/testing/storyboard/index.d.mts +9 -2
- package/dist/lib/testing/storyboard/index.d.ts +9 -2
- package/dist/lib/testing/storyboard/index.js +51 -2
- package/dist/lib/testing/storyboard/index.mjs +49 -1
- package/dist/lib/testing/storyboard/prepared-fixtures.d.mts +471 -0
- package/dist/lib/testing/storyboard/prepared-fixtures.d.ts +471 -0
- package/dist/lib/testing/storyboard/prepared-fixtures.js +434 -0
- package/dist/lib/testing/storyboard/prepared-fixtures.mjs +389 -0
- package/dist/lib/testing/storyboard/protected-context.d.mts +127 -0
- package/dist/lib/testing/storyboard/protected-context.d.ts +127 -0
- package/dist/lib/testing/storyboard/protected-context.js +301 -0
- package/dist/lib/testing/storyboard/protected-context.mjs +267 -0
- package/dist/lib/testing/storyboard/rejection-hints.js +13 -4
- package/dist/lib/testing/storyboard/rejection-hints.mjs +13 -4
- package/dist/lib/testing/storyboard/runner.d.mts +24 -0
- package/dist/lib/testing/storyboard/runner.d.ts +24 -0
- package/dist/lib/testing/storyboard/runner.js +1061 -105
- package/dist/lib/testing/storyboard/runner.mjs +1075 -106
- package/dist/lib/testing/storyboard/seeding.d.mts +15 -1
- package/dist/lib/testing/storyboard/seeding.d.ts +15 -1
- package/dist/lib/testing/storyboard/seeding.js +272 -12
- package/dist/lib/testing/storyboard/seeding.mjs +274 -13
- package/dist/lib/testing/storyboard/types.d.mts +207 -10
- package/dist/lib/testing/storyboard/types.d.ts +207 -10
- package/dist/lib/testing/storyboard/types.js +1 -1
- package/dist/lib/testing/storyboard/types.mjs +1 -1
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/testing/storyboard/validations.js +95 -0
- package/dist/lib/testing/storyboard/validations.mjs +95 -0
- package/dist/lib/validation/bundled-schema-store.js +5 -0
- package/dist/lib/validation/bundled-schema-store.mjs +4 -0
- package/dist/lib/validation/schema-loader.js +19 -2
- package/dist/lib/validation/schema-loader.mjs +22 -3
- package/dist/lib/version.d.mts +3 -3
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.mjs +3 -3
- package/docs/TYPE-SUMMARY.md +2 -2
- package/docs/guides/COLLECTION-FIXTURE-FOUNDATION.md +850 -0
- package/docs/guides/PROPOSAL-TERMS-VERIFICATION.md +66 -0
- package/docs/llms.txt +4 -3
- package/docs/migration-14.x-rc-worksheet.md +4 -4
- 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 {
|
|
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
|
-
|
|
813
|
-
|
|
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
|
|
826
|
-
options
|
|
827
|
-
(
|
|
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,
|
|
907
|
+
return await withExternalSchemaRoot(schemaRoot.adcpVersion, schemaRoot.schemaRoot, gatedBody);
|
|
831
908
|
}
|
|
832
|
-
return await
|
|
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
|
-
|
|
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
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
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
|
-
|
|
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
|
|
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),
|
|
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 =
|
|
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
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
3338
|
-
|
|
3339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
4359
|
-
|
|
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
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4637
|
-
|
|
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
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
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
|
-
|
|
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,
|