@adcp/sdk 14.0.0-rc.52 → 14.0.0-rc.53

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 (39) hide show
  1. package/dist/lib/registry/types.generated.d.mts +12 -0
  2. package/dist/lib/registry/types.generated.d.ts +12 -0
  3. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  4. package/dist/lib/server/auth-signature.d.mts +5 -2
  5. package/dist/lib/server/auth-signature.d.ts +5 -2
  6. package/dist/lib/signing/content-digest.d.mts +8 -0
  7. package/dist/lib/signing/content-digest.d.ts +8 -0
  8. package/dist/lib/signing/content-digest.js +13 -4
  9. package/dist/lib/signing/content-digest.mjs +11 -4
  10. package/dist/lib/signing/parser.d.mts +1 -1
  11. package/dist/lib/signing/parser.d.ts +1 -1
  12. package/dist/lib/signing/parser.js +3 -2
  13. package/dist/lib/signing/parser.mjs +3 -2
  14. package/dist/lib/signing/verifier.d.mts +6 -2
  15. package/dist/lib/signing/verifier.d.ts +6 -2
  16. package/dist/lib/signing/verifier.js +14 -10
  17. package/dist/lib/signing/verifier.mjs +15 -10
  18. package/dist/lib/testing/compliance/comply.d.mts +107 -1
  19. package/dist/lib/testing/compliance/comply.d.ts +107 -1
  20. package/dist/lib/testing/compliance/comply.js +313 -1
  21. package/dist/lib/testing/compliance/comply.mjs +320 -2
  22. package/dist/lib/testing/compliance/index.d.mts +1 -1
  23. package/dist/lib/testing/compliance/index.d.ts +1 -1
  24. package/dist/lib/testing/index.d.mts +1 -1
  25. package/dist/lib/testing/index.d.ts +1 -1
  26. package/dist/lib/testing/storyboard/runner.js +10 -0
  27. package/dist/lib/testing/storyboard/runner.mjs +8 -0
  28. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  29. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  30. package/dist/lib/version.d.mts +3 -3
  31. package/dist/lib/version.d.ts +3 -3
  32. package/dist/lib/version.js +3 -3
  33. package/dist/lib/version.mjs +3 -3
  34. package/docs/TYPE-SUMMARY.md +1 -1
  35. package/docs/guides/VALIDATE-YOUR-AGENT.md +49 -0
  36. package/docs/llms.txt +1 -1
  37. package/docs/migration-13-to-14.md +14 -7
  38. package/docs/migration-14.x-rc-worksheet.md +4 -4
  39. package/package.json +1 -1
@@ -911,6 +911,314 @@ function buildNotApplicableStoryboardResult(agentUrl, na) {
911
911
  notices: []
912
912
  };
913
913
  }
914
+ async function runComplyStoryboard(agentUrl, storyboard, runOptions, profile, routeStoryboard) {
915
+ if (!routeStoryboard || (0, import_runner.storyboardCapabilityGateUnmet)(storyboard, runOptions) !== null) {
916
+ return (0, import_runner.runStoryboard)(agentUrl, storyboard, runOptions);
917
+ }
918
+ const routing = await routeStoryboard(storyboard, { agent_url: agentUrl, profile });
919
+ if (routing === void 0 || routing === null) return (0, import_runner.runStoryboard)(agentUrl, storyboard, runOptions);
920
+ if (typeof routing !== "object") {
921
+ throw new TypeError(
922
+ `routeStoryboard(${storyboard.id}) must return undefined, { skip } or { agents, default_agent }; got ${typeof routing}.`
923
+ );
924
+ }
925
+ if ("skip" in routing) {
926
+ if ("agents" in routing) {
927
+ throw new TypeError(`routeStoryboard(${storyboard.id}) returned both \`skip\` and \`agents\`; return one.`);
928
+ }
929
+ if (typeof routing.skip !== "string" || routing.skip.trim() === "") {
930
+ throw new TypeError(`routeStoryboard(${storyboard.id}) returned \`skip\` without a non-empty reason string.`);
931
+ }
932
+ return buildRouteSkippedStoryboardResult(agentUrl, storyboard, routing.skip, runOptions);
933
+ }
934
+ const routed = buildRoutedStoryboardRun(agentUrl, storyboard, runOptions, routing);
935
+ if ("refused" in routed) return buildRouteSkippedStoryboardResult(agentUrl, storyboard, routed.refused, runOptions);
936
+ const result = await (0, import_runner.runStoryboard)("", routed.storyboard, routed.options);
937
+ return gradeRoutedResultForAgentUnderTest(result, routed.agentUnderTestIndexes, routed.keyByIndex);
938
+ }
939
+ function sameAgentUrl(a, b) {
940
+ try {
941
+ return new URL(a).href === new URL(b).href;
942
+ } catch {
943
+ return a === b;
944
+ }
945
+ }
946
+ function isCredentialObject(auth) {
947
+ if (!auth || typeof auth !== "object" || Array.isArray(auth)) return false;
948
+ const a = auth;
949
+ const nonEmpty = (v) => typeof v === "string" && v.length > 0;
950
+ const isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
951
+ switch (a.type) {
952
+ case "bearer":
953
+ return nonEmpty(a.token);
954
+ case "basic":
955
+ return nonEmpty(a.username) && typeof a.password === "string";
956
+ case "oauth":
957
+ return isObject(a.tokens);
958
+ case "oauth_client_credentials":
959
+ return isObject(a.credentials);
960
+ default:
961
+ return false;
962
+ }
963
+ }
964
+ function referencesTestKitAuth(value) {
965
+ if (typeof value === "string") return value.includes("$test_kit.auth");
966
+ if (Array.isArray(value)) return value.some(referencesTestKitAuth);
967
+ if (value && typeof value === "object") return Object.values(value).some(referencesTestKitAuth);
968
+ return false;
969
+ }
970
+ function stepAuthUsesTestKit(auth) {
971
+ return !!auth && typeof auth === "object" && !!auth.from_test_kit;
972
+ }
973
+ function testKitCredentialLeak(storyboard, agentUnderTestKeys) {
974
+ if (referencesTestKitAuth(storyboard.context)) {
975
+ return "its context references `$test_kit.auth`, which comply() will not forward to other routed agents.";
976
+ }
977
+ for (const phase of storyboard.phases ?? []) {
978
+ for (const step of phase.steps ?? []) {
979
+ if (step.agent !== void 0 && agentUnderTestKeys.has(step.agent)) continue;
980
+ const { task: _task, ...rest } = step;
981
+ if (stepAuthUsesTestKit(step.auth) || referencesTestKitAuth(rest)) {
982
+ return `step "${step.id}" is not pinned to the agent under test but reads test-kit credentials; comply() will not forward them to other routed agents.`;
983
+ }
984
+ }
985
+ }
986
+ return void 0;
987
+ }
988
+ function storyboardGradingShape(storyboard) {
989
+ return JSON.stringify({
990
+ requires: storyboard.requires,
991
+ requires_capability: storyboard.requires_capability,
992
+ requires_all_capabilities: storyboard.requires_all_capabilities,
993
+ invariants: storyboard.invariants,
994
+ phases: (storyboard.phases ?? []).map((phase) => ({
995
+ id: phase.id,
996
+ optional: phase.optional,
997
+ branch_set: phase.branch_set,
998
+ requires_capability: phase.requires_capability,
999
+ steps: (phase.steps ?? []).map((step) => ({
1000
+ id: step.id,
1001
+ task: step.task,
1002
+ agent: step.agent,
1003
+ validations: step.validations,
1004
+ expect_error: step.expect_error,
1005
+ requires_tool: step.requires_tool
1006
+ }))
1007
+ }))
1008
+ });
1009
+ }
1010
+ function buildRoutedStoryboardRun(agentUrl, selected, runOptions, route) {
1011
+ const where = `routeStoryboard(${selected.id})`;
1012
+ const storyboard = route.storyboard ?? selected;
1013
+ if (!storyboard || typeof storyboard !== "object" || storyboard.id !== selected.id) {
1014
+ throw new TypeError(
1015
+ `${where} returned a replacement storyboard with a different id; it must keep id "${selected.id}" so the result is attributed to the selected storyboard and its bundle.`
1016
+ );
1017
+ }
1018
+ if (storyboard !== selected && storyboardGradingShape(storyboard) !== storyboardGradingShape(selected)) {
1019
+ throw new TypeError(
1020
+ `${where} returned a replacement storyboard that changes what is graded (phases, steps, tasks, agent pins, validations, expectations or gates). A replacement may only change request payloads and context.`
1021
+ );
1022
+ }
1023
+ const agents = route.agents;
1024
+ if (!agents || typeof agents !== "object" || Object.keys(agents).length === 0) {
1025
+ throw new TypeError(`${where} returned a route without a non-empty \`agents\` map.`);
1026
+ }
1027
+ const defaultKey = route.default_agent;
1028
+ if (typeof defaultKey !== "string" || !Object.prototype.hasOwnProperty.call(agents, defaultKey)) {
1029
+ throw new TypeError(`${where} returned a \`default_agent\` that is not a key in \`agents\`.`);
1030
+ }
1031
+ const headers = runOptions.headers;
1032
+ if (headers && Object.keys(headers).length > 0) {
1033
+ throw new Error(
1034
+ `${where}: refusing to route while run-level \`headers\` are set; the runner sends them to every routed agent. Remove the headers or skip this storyboard.`
1035
+ );
1036
+ }
1037
+ const ordered = [defaultKey, ...Object.keys(agents).filter((key) => key !== defaultKey)];
1038
+ const isolated = {};
1039
+ const agentUnderTestIndexes = /* @__PURE__ */ new Set();
1040
+ const agentUnderTestKeys = /* @__PURE__ */ new Set();
1041
+ const keyByIndex = /* @__PURE__ */ new Map();
1042
+ ordered.forEach((key, index) => {
1043
+ const entry = agents[key];
1044
+ if (!entry || typeof entry !== "object" || typeof entry.url !== "string" || entry.url === "") {
1045
+ throw new TypeError(`${where}: agents['${key}'] needs a non-empty \`url\`.`);
1046
+ }
1047
+ const isAgentUnderTest = sameAgentUrl(entry.url, agentUrl);
1048
+ if (key === defaultKey && !isAgentUnderTest) {
1049
+ throw new Error(
1050
+ `${where}: agents['${key}'] (the default_agent) must be the agent under test (${(0, import_oauth_metadata_graph.redactOAuthUrlForOutput)(agentUrl)}); comply() attributes the result to that agent.`
1051
+ );
1052
+ }
1053
+ const hasOwnAuth = Object.prototype.hasOwnProperty.call(entry, "auth");
1054
+ if (hasOwnAuth ? !isCredentialObject(entry.auth) : !isAgentUnderTest) {
1055
+ throw new Error(
1056
+ `${where}: agents['${key}'] (${(0, import_oauth_metadata_graph.redactOAuthUrlForOutput)(entry.url)}) must declare its own \`auth\` credential object ({ type: "bearer" | "basic" | "oauth" | "oauth_client_credentials", ... }). Without it the runner falls back to the credential for the agent under test.`
1057
+ );
1058
+ }
1059
+ const auth = hasOwnAuth ? entry.auth : runOptions.auth;
1060
+ isolated[key] = {
1061
+ url: entry.url,
1062
+ ...auth !== void 0 && { auth },
1063
+ ...entry.transport !== void 0 && { transport: entry.transport }
1064
+ };
1065
+ keyByIndex.set(index + 1, key);
1066
+ if (isAgentUnderTest) {
1067
+ agentUnderTestIndexes.add(index + 1);
1068
+ agentUnderTestKeys.add(key);
1069
+ }
1070
+ });
1071
+ const leak = testKitCredentialLeak(storyboard, agentUnderTestKeys);
1072
+ if (leak) return { refused: `${selected.id} cannot be routed: ${leak}` };
1073
+ const context = runOptions.context !== void 0 || route.context !== void 0 ? { ...runOptions.context, ...route.context } : void 0;
1074
+ const options = {
1075
+ ...runOptions,
1076
+ agents: isolated,
1077
+ default_agent: defaultKey,
1078
+ // A broken peer agent must grade as a coverage gap for the agent under
1079
+ // test, not fail the whole storyboard: resilient discovery turns it into
1080
+ // per-step routing failures attributed to that peer's `agent_index`.
1081
+ discovery_resilient: true,
1082
+ ...context !== void 0 && { context }
1083
+ };
1084
+ delete options.auth;
1085
+ delete options._client;
1086
+ delete options._profile;
1087
+ delete options.profile;
1088
+ delete options._controllerCapabilities;
1089
+ delete options.agentTools;
1090
+ return { storyboard, options, agentUnderTestIndexes, keyByIndex };
1091
+ }
1092
+ function gradeRoutedResultForAgentUnderTest(result, agentUnderTestIndexes, keyByIndex) {
1093
+ const isDiscoveryFailure = result.phases.length === 1 && result.phases[0].phase_id === "discovery_failed" && result.failed_count > 0;
1094
+ const downgraded = /* @__PURE__ */ new Map();
1095
+ const phases = result.phases.map((phase) => {
1096
+ let changed = false;
1097
+ const steps = phase.steps.map((step) => {
1098
+ if (step.passed || step.skipped) return step;
1099
+ const peer = isDiscoveryFailure ? "a routed agent" : step.agent_index !== void 0 && !agentUnderTestIndexes.has(step.agent_index) ? `routed agent "${keyByIndex.get(step.agent_index) ?? step.agent_index}"` : void 0;
1100
+ if (!peer) return step;
1101
+ changed = true;
1102
+ const assertionErrors = (result.assertions ?? []).filter((a) => !a.passed && a.scope !== "storyboard" && a.step_id === step.step_id).map((a) => a.error ?? a.description);
1103
+ const detail = sanitizeAgentText(
1104
+ `Step served by ${peer} failed; comply() grades it as a coverage gap for the agent under test, not a failure. ${step.error ?? "No error detail."}` + (assertionErrors.length > 0 ? ` Assertions: ${assertionErrors.join("; ")}` : ""),
1105
+ 1e3
1106
+ );
1107
+ downgraded.set(step.step_id, detail);
1108
+ const { error: _error, ...rest } = step;
1109
+ return {
1110
+ ...rest,
1111
+ passed: true,
1112
+ skipped: true,
1113
+ skip_reason: "prerequisite_failed",
1114
+ skip: { reason: "prerequisite_failed", detail }
1115
+ };
1116
+ });
1117
+ return changed ? { ...phase, steps, passed: steps.every((step) => step.passed) } : phase;
1118
+ });
1119
+ const autPassed = phases.some(
1120
+ (phase) => phase.steps.some(
1121
+ (step) => step.passed && !step.skipped && step.agent_index !== void 0 && agentUnderTestIndexes.has(step.agent_index)
1122
+ )
1123
+ );
1124
+ if (downgraded.size === 0 && (autPassed || result.passed_count === 0)) return result;
1125
+ const assertions = result.assertions?.filter(
1126
+ (a) => a.passed || a.scope === "storyboard" || a.step_id === void 0 || !downgraded.has(a.step_id)
1127
+ );
1128
+ const graded = {
1129
+ ...result,
1130
+ phases,
1131
+ ...assertions !== void 0 && { assertions },
1132
+ failed_count: result.failed_count - downgraded.size,
1133
+ skipped_count: result.skipped_count + downgraded.size
1134
+ };
1135
+ if (!autPassed && graded.passed_count > 0) {
1136
+ graded.phases = [
1137
+ ...graded.phases,
1138
+ {
1139
+ phase_id: "agent_under_test_coverage",
1140
+ phase_title: "Agent under test coverage",
1141
+ passed: true,
1142
+ duration_ms: 0,
1143
+ steps: [
1144
+ {
1145
+ storyboard_id: result.storyboard_id,
1146
+ step_id: "agent_under_test_coverage",
1147
+ phase_id: "agent_under_test_coverage",
1148
+ title: "No step served by the agent under test passed",
1149
+ task: "",
1150
+ passed: true,
1151
+ skipped: true,
1152
+ skip_reason: "prerequisite_failed",
1153
+ skip: {
1154
+ reason: "prerequisite_failed",
1155
+ detail: "Every passing step in this routed storyboard was served by another agent, so it carries no evidence about the agent under test."
1156
+ },
1157
+ duration_ms: 0,
1158
+ validations: [],
1159
+ context: {},
1160
+ extraction: { path: "none" }
1161
+ }
1162
+ ]
1163
+ }
1164
+ ];
1165
+ graded.skipped_count += 1;
1166
+ }
1167
+ graded.overall_passed = result.overall_passed || graded.failed_count === 0 && !(graded.assertions ?? []).some((a) => !a.passed && a.scope === "storyboard") && (0, import_storyboard_tracks.collectDetachedAssertionFailures)(graded).length === 0;
1168
+ return graded;
1169
+ }
1170
+ function buildRouteSkippedStoryboardResult(agentUrl, storyboard, reason, runOptions) {
1171
+ const detail = sanitizeAgentText(reason, 1e3);
1172
+ const requirement = storyboard.requires?.includes("multi_agent") ? "multi_agent" : void 0;
1173
+ const stepId = requirement ? `requirement_unmet:${requirement}` : "requirement_unmet:route_storyboard";
1174
+ return {
1175
+ storyboard_id: storyboard.id,
1176
+ storyboard_title: storyboard.title,
1177
+ agent_url: (0, import_oauth_metadata_graph.redactOAuthUrlForOutput)(agentUrl),
1178
+ overall_passed: true,
1179
+ phases: [
1180
+ {
1181
+ phase_id: "requirement_unmet",
1182
+ phase_title: requirement ? `Requirement unmet: ${requirement}` : "Requirement unmet: not routable",
1183
+ passed: true,
1184
+ duration_ms: 0,
1185
+ steps: [
1186
+ {
1187
+ storyboard_id: storyboard.id,
1188
+ step_id: stepId,
1189
+ phase_id: "requirement_unmet",
1190
+ title: requirement ? `Storyboard skipped: requires '${requirement}'` : "Storyboard skipped: not routable by this run",
1191
+ task: "",
1192
+ passed: true,
1193
+ skipped: true,
1194
+ skip_reason: "requirement_unmet",
1195
+ skip: { reason: "requirement_unmet", detail, ...requirement && { requirement } },
1196
+ duration_ms: 0,
1197
+ validations: [],
1198
+ context: {},
1199
+ extraction: { path: "none" }
1200
+ }
1201
+ ]
1202
+ }
1203
+ ],
1204
+ context: {},
1205
+ total_duration_ms: 0,
1206
+ passed_count: 0,
1207
+ failed_count: 0,
1208
+ skipped_count: 1,
1209
+ runner_capability_version: import_version.LIBRARY_VERSION,
1210
+ tested_at: (/* @__PURE__ */ new Date()).toISOString(),
1211
+ strict_validation_summary: {
1212
+ observable: false,
1213
+ checked: 0,
1214
+ passed: 0,
1215
+ failed: 0,
1216
+ strict_only_failures: 0,
1217
+ lenient_also_failed: 0
1218
+ },
1219
+ notices: (0, import_runner.collectCapabilityNotices)(storyboard, runOptions._profile)
1220
+ };
1221
+ }
914
1222
  function extractFailures(results, storyboards, agentRef, fixOptions = {}) {
915
1223
  const failures = [];
916
1224
  const sbLookup = /* @__PURE__ */ new Map();
@@ -1010,8 +1318,12 @@ async function complyImpl(agentUrl, options) {
1010
1318
  complianceDir,
1011
1319
  schemaRoot,
1012
1320
  hostedStableLineAlias,
1321
+ routeStoryboard,
1013
1322
  ...testOptions
1014
1323
  } = options;
1324
+ if (routeStoryboard !== void 0 && typeof routeStoryboard !== "function") {
1325
+ throw new TypeError("routeStoryboard must be a function");
1326
+ }
1015
1327
  const resolveOptions = {
1016
1328
  ...version !== void 0 && { version },
1017
1329
  ...complianceDir !== void 0 && { complianceDir },
@@ -1216,7 +1528,7 @@ async function complyImpl(agentUrl, options) {
1216
1528
  stoppedForTimeoutBudget = true;
1217
1529
  break;
1218
1530
  }
1219
- const result = await (0, import_runner.runStoryboard)(agentUrl, sb, runOptions);
1531
+ const result = await runComplyStoryboard(agentUrl, sb, runOptions, profile, routeStoryboard);
1220
1532
  storyboardResults.push(result);
1221
1533
  executedStoryboards.push(sb);
1222
1534
  }
@@ -1,6 +1,12 @@
1
1
  import { createTestClient, discoverAgentProfile, seedTestClientSigningCapability } from "../client.mjs";
2
2
  import { collectDetachedAssertionFailures, mapStoryboardResultsToTrackResult, TRACK_LABELS } from "./storyboard-tracks.mjs";
3
- import { applyAdcpVersionRunOptions, runStoryboard, storyboardCapabilityPredicates } from "../storyboard/runner.mjs";
3
+ import {
4
+ applyAdcpVersionRunOptions,
5
+ collectCapabilityNotices,
6
+ runStoryboard,
7
+ storyboardCapabilityGateUnmet,
8
+ storyboardCapabilityPredicates
9
+ } from "../storyboard/runner.mjs";
4
10
  import { applyNativeA2AComplianceTransportOptions } from "../storyboard/native-a2a-compliance.mjs";
5
11
  import { validateTestKit } from "../storyboard/test-kit.mjs";
6
12
  import { checkAccountDiscoveryGate, isAccountBearingSpecialism } from "./spec-conformance.mjs";
@@ -888,6 +894,314 @@ function buildNotApplicableStoryboardResult(agentUrl, na) {
888
894
  notices: []
889
895
  };
890
896
  }
897
+ async function runComplyStoryboard(agentUrl, storyboard, runOptions, profile, routeStoryboard) {
898
+ if (!routeStoryboard || storyboardCapabilityGateUnmet(storyboard, runOptions) !== null) {
899
+ return runStoryboard(agentUrl, storyboard, runOptions);
900
+ }
901
+ const routing = await routeStoryboard(storyboard, { agent_url: agentUrl, profile });
902
+ if (routing === void 0 || routing === null) return runStoryboard(agentUrl, storyboard, runOptions);
903
+ if (typeof routing !== "object") {
904
+ throw new TypeError(
905
+ `routeStoryboard(${storyboard.id}) must return undefined, { skip } or { agents, default_agent }; got ${typeof routing}.`
906
+ );
907
+ }
908
+ if ("skip" in routing) {
909
+ if ("agents" in routing) {
910
+ throw new TypeError(`routeStoryboard(${storyboard.id}) returned both \`skip\` and \`agents\`; return one.`);
911
+ }
912
+ if (typeof routing.skip !== "string" || routing.skip.trim() === "") {
913
+ throw new TypeError(`routeStoryboard(${storyboard.id}) returned \`skip\` without a non-empty reason string.`);
914
+ }
915
+ return buildRouteSkippedStoryboardResult(agentUrl, storyboard, routing.skip, runOptions);
916
+ }
917
+ const routed = buildRoutedStoryboardRun(agentUrl, storyboard, runOptions, routing);
918
+ if ("refused" in routed) return buildRouteSkippedStoryboardResult(agentUrl, storyboard, routed.refused, runOptions);
919
+ const result = await runStoryboard("", routed.storyboard, routed.options);
920
+ return gradeRoutedResultForAgentUnderTest(result, routed.agentUnderTestIndexes, routed.keyByIndex);
921
+ }
922
+ function sameAgentUrl(a, b) {
923
+ try {
924
+ return new URL(a).href === new URL(b).href;
925
+ } catch {
926
+ return a === b;
927
+ }
928
+ }
929
+ function isCredentialObject(auth) {
930
+ if (!auth || typeof auth !== "object" || Array.isArray(auth)) return false;
931
+ const a = auth;
932
+ const nonEmpty = (v) => typeof v === "string" && v.length > 0;
933
+ const isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
934
+ switch (a.type) {
935
+ case "bearer":
936
+ return nonEmpty(a.token);
937
+ case "basic":
938
+ return nonEmpty(a.username) && typeof a.password === "string";
939
+ case "oauth":
940
+ return isObject(a.tokens);
941
+ case "oauth_client_credentials":
942
+ return isObject(a.credentials);
943
+ default:
944
+ return false;
945
+ }
946
+ }
947
+ function referencesTestKitAuth(value) {
948
+ if (typeof value === "string") return value.includes("$test_kit.auth");
949
+ if (Array.isArray(value)) return value.some(referencesTestKitAuth);
950
+ if (value && typeof value === "object") return Object.values(value).some(referencesTestKitAuth);
951
+ return false;
952
+ }
953
+ function stepAuthUsesTestKit(auth) {
954
+ return !!auth && typeof auth === "object" && !!auth.from_test_kit;
955
+ }
956
+ function testKitCredentialLeak(storyboard, agentUnderTestKeys) {
957
+ if (referencesTestKitAuth(storyboard.context)) {
958
+ return "its context references `$test_kit.auth`, which comply() will not forward to other routed agents.";
959
+ }
960
+ for (const phase of storyboard.phases ?? []) {
961
+ for (const step of phase.steps ?? []) {
962
+ if (step.agent !== void 0 && agentUnderTestKeys.has(step.agent)) continue;
963
+ const { task: _task, ...rest } = step;
964
+ if (stepAuthUsesTestKit(step.auth) || referencesTestKitAuth(rest)) {
965
+ return `step "${step.id}" is not pinned to the agent under test but reads test-kit credentials; comply() will not forward them to other routed agents.`;
966
+ }
967
+ }
968
+ }
969
+ return void 0;
970
+ }
971
+ function storyboardGradingShape(storyboard) {
972
+ return JSON.stringify({
973
+ requires: storyboard.requires,
974
+ requires_capability: storyboard.requires_capability,
975
+ requires_all_capabilities: storyboard.requires_all_capabilities,
976
+ invariants: storyboard.invariants,
977
+ phases: (storyboard.phases ?? []).map((phase) => ({
978
+ id: phase.id,
979
+ optional: phase.optional,
980
+ branch_set: phase.branch_set,
981
+ requires_capability: phase.requires_capability,
982
+ steps: (phase.steps ?? []).map((step) => ({
983
+ id: step.id,
984
+ task: step.task,
985
+ agent: step.agent,
986
+ validations: step.validations,
987
+ expect_error: step.expect_error,
988
+ requires_tool: step.requires_tool
989
+ }))
990
+ }))
991
+ });
992
+ }
993
+ function buildRoutedStoryboardRun(agentUrl, selected, runOptions, route) {
994
+ const where = `routeStoryboard(${selected.id})`;
995
+ const storyboard = route.storyboard ?? selected;
996
+ if (!storyboard || typeof storyboard !== "object" || storyboard.id !== selected.id) {
997
+ throw new TypeError(
998
+ `${where} returned a replacement storyboard with a different id; it must keep id "${selected.id}" so the result is attributed to the selected storyboard and its bundle.`
999
+ );
1000
+ }
1001
+ if (storyboard !== selected && storyboardGradingShape(storyboard) !== storyboardGradingShape(selected)) {
1002
+ throw new TypeError(
1003
+ `${where} returned a replacement storyboard that changes what is graded (phases, steps, tasks, agent pins, validations, expectations or gates). A replacement may only change request payloads and context.`
1004
+ );
1005
+ }
1006
+ const agents = route.agents;
1007
+ if (!agents || typeof agents !== "object" || Object.keys(agents).length === 0) {
1008
+ throw new TypeError(`${where} returned a route without a non-empty \`agents\` map.`);
1009
+ }
1010
+ const defaultKey = route.default_agent;
1011
+ if (typeof defaultKey !== "string" || !Object.prototype.hasOwnProperty.call(agents, defaultKey)) {
1012
+ throw new TypeError(`${where} returned a \`default_agent\` that is not a key in \`agents\`.`);
1013
+ }
1014
+ const headers = runOptions.headers;
1015
+ if (headers && Object.keys(headers).length > 0) {
1016
+ throw new Error(
1017
+ `${where}: refusing to route while run-level \`headers\` are set; the runner sends them to every routed agent. Remove the headers or skip this storyboard.`
1018
+ );
1019
+ }
1020
+ const ordered = [defaultKey, ...Object.keys(agents).filter((key) => key !== defaultKey)];
1021
+ const isolated = {};
1022
+ const agentUnderTestIndexes = /* @__PURE__ */ new Set();
1023
+ const agentUnderTestKeys = /* @__PURE__ */ new Set();
1024
+ const keyByIndex = /* @__PURE__ */ new Map();
1025
+ ordered.forEach((key, index) => {
1026
+ const entry = agents[key];
1027
+ if (!entry || typeof entry !== "object" || typeof entry.url !== "string" || entry.url === "") {
1028
+ throw new TypeError(`${where}: agents['${key}'] needs a non-empty \`url\`.`);
1029
+ }
1030
+ const isAgentUnderTest = sameAgentUrl(entry.url, agentUrl);
1031
+ if (key === defaultKey && !isAgentUnderTest) {
1032
+ throw new Error(
1033
+ `${where}: agents['${key}'] (the default_agent) must be the agent under test (${redactOAuthUrlForOutput(agentUrl)}); comply() attributes the result to that agent.`
1034
+ );
1035
+ }
1036
+ const hasOwnAuth = Object.prototype.hasOwnProperty.call(entry, "auth");
1037
+ if (hasOwnAuth ? !isCredentialObject(entry.auth) : !isAgentUnderTest) {
1038
+ throw new Error(
1039
+ `${where}: agents['${key}'] (${redactOAuthUrlForOutput(entry.url)}) must declare its own \`auth\` credential object ({ type: "bearer" | "basic" | "oauth" | "oauth_client_credentials", ... }). Without it the runner falls back to the credential for the agent under test.`
1040
+ );
1041
+ }
1042
+ const auth = hasOwnAuth ? entry.auth : runOptions.auth;
1043
+ isolated[key] = {
1044
+ url: entry.url,
1045
+ ...auth !== void 0 && { auth },
1046
+ ...entry.transport !== void 0 && { transport: entry.transport }
1047
+ };
1048
+ keyByIndex.set(index + 1, key);
1049
+ if (isAgentUnderTest) {
1050
+ agentUnderTestIndexes.add(index + 1);
1051
+ agentUnderTestKeys.add(key);
1052
+ }
1053
+ });
1054
+ const leak = testKitCredentialLeak(storyboard, agentUnderTestKeys);
1055
+ if (leak) return { refused: `${selected.id} cannot be routed: ${leak}` };
1056
+ const context = runOptions.context !== void 0 || route.context !== void 0 ? { ...runOptions.context, ...route.context } : void 0;
1057
+ const options = {
1058
+ ...runOptions,
1059
+ agents: isolated,
1060
+ default_agent: defaultKey,
1061
+ // A broken peer agent must grade as a coverage gap for the agent under
1062
+ // test, not fail the whole storyboard: resilient discovery turns it into
1063
+ // per-step routing failures attributed to that peer's `agent_index`.
1064
+ discovery_resilient: true,
1065
+ ...context !== void 0 && { context }
1066
+ };
1067
+ delete options.auth;
1068
+ delete options._client;
1069
+ delete options._profile;
1070
+ delete options.profile;
1071
+ delete options._controllerCapabilities;
1072
+ delete options.agentTools;
1073
+ return { storyboard, options, agentUnderTestIndexes, keyByIndex };
1074
+ }
1075
+ function gradeRoutedResultForAgentUnderTest(result, agentUnderTestIndexes, keyByIndex) {
1076
+ const isDiscoveryFailure = result.phases.length === 1 && result.phases[0].phase_id === "discovery_failed" && result.failed_count > 0;
1077
+ const downgraded = /* @__PURE__ */ new Map();
1078
+ const phases = result.phases.map((phase) => {
1079
+ let changed = false;
1080
+ const steps = phase.steps.map((step) => {
1081
+ if (step.passed || step.skipped) return step;
1082
+ const peer = isDiscoveryFailure ? "a routed agent" : step.agent_index !== void 0 && !agentUnderTestIndexes.has(step.agent_index) ? `routed agent "${keyByIndex.get(step.agent_index) ?? step.agent_index}"` : void 0;
1083
+ if (!peer) return step;
1084
+ changed = true;
1085
+ const assertionErrors = (result.assertions ?? []).filter((a) => !a.passed && a.scope !== "storyboard" && a.step_id === step.step_id).map((a) => a.error ?? a.description);
1086
+ const detail = sanitizeAgentText(
1087
+ `Step served by ${peer} failed; comply() grades it as a coverage gap for the agent under test, not a failure. ${step.error ?? "No error detail."}` + (assertionErrors.length > 0 ? ` Assertions: ${assertionErrors.join("; ")}` : ""),
1088
+ 1e3
1089
+ );
1090
+ downgraded.set(step.step_id, detail);
1091
+ const { error: _error, ...rest } = step;
1092
+ return {
1093
+ ...rest,
1094
+ passed: true,
1095
+ skipped: true,
1096
+ skip_reason: "prerequisite_failed",
1097
+ skip: { reason: "prerequisite_failed", detail }
1098
+ };
1099
+ });
1100
+ return changed ? { ...phase, steps, passed: steps.every((step) => step.passed) } : phase;
1101
+ });
1102
+ const autPassed = phases.some(
1103
+ (phase) => phase.steps.some(
1104
+ (step) => step.passed && !step.skipped && step.agent_index !== void 0 && agentUnderTestIndexes.has(step.agent_index)
1105
+ )
1106
+ );
1107
+ if (downgraded.size === 0 && (autPassed || result.passed_count === 0)) return result;
1108
+ const assertions = result.assertions?.filter(
1109
+ (a) => a.passed || a.scope === "storyboard" || a.step_id === void 0 || !downgraded.has(a.step_id)
1110
+ );
1111
+ const graded = {
1112
+ ...result,
1113
+ phases,
1114
+ ...assertions !== void 0 && { assertions },
1115
+ failed_count: result.failed_count - downgraded.size,
1116
+ skipped_count: result.skipped_count + downgraded.size
1117
+ };
1118
+ if (!autPassed && graded.passed_count > 0) {
1119
+ graded.phases = [
1120
+ ...graded.phases,
1121
+ {
1122
+ phase_id: "agent_under_test_coverage",
1123
+ phase_title: "Agent under test coverage",
1124
+ passed: true,
1125
+ duration_ms: 0,
1126
+ steps: [
1127
+ {
1128
+ storyboard_id: result.storyboard_id,
1129
+ step_id: "agent_under_test_coverage",
1130
+ phase_id: "agent_under_test_coverage",
1131
+ title: "No step served by the agent under test passed",
1132
+ task: "",
1133
+ passed: true,
1134
+ skipped: true,
1135
+ skip_reason: "prerequisite_failed",
1136
+ skip: {
1137
+ reason: "prerequisite_failed",
1138
+ detail: "Every passing step in this routed storyboard was served by another agent, so it carries no evidence about the agent under test."
1139
+ },
1140
+ duration_ms: 0,
1141
+ validations: [],
1142
+ context: {},
1143
+ extraction: { path: "none" }
1144
+ }
1145
+ ]
1146
+ }
1147
+ ];
1148
+ graded.skipped_count += 1;
1149
+ }
1150
+ graded.overall_passed = result.overall_passed || graded.failed_count === 0 && !(graded.assertions ?? []).some((a) => !a.passed && a.scope === "storyboard") && collectDetachedAssertionFailures(graded).length === 0;
1151
+ return graded;
1152
+ }
1153
+ function buildRouteSkippedStoryboardResult(agentUrl, storyboard, reason, runOptions) {
1154
+ const detail = sanitizeAgentText(reason, 1e3);
1155
+ const requirement = storyboard.requires?.includes("multi_agent") ? "multi_agent" : void 0;
1156
+ const stepId = requirement ? `requirement_unmet:${requirement}` : "requirement_unmet:route_storyboard";
1157
+ return {
1158
+ storyboard_id: storyboard.id,
1159
+ storyboard_title: storyboard.title,
1160
+ agent_url: redactOAuthUrlForOutput(agentUrl),
1161
+ overall_passed: true,
1162
+ phases: [
1163
+ {
1164
+ phase_id: "requirement_unmet",
1165
+ phase_title: requirement ? `Requirement unmet: ${requirement}` : "Requirement unmet: not routable",
1166
+ passed: true,
1167
+ duration_ms: 0,
1168
+ steps: [
1169
+ {
1170
+ storyboard_id: storyboard.id,
1171
+ step_id: stepId,
1172
+ phase_id: "requirement_unmet",
1173
+ title: requirement ? `Storyboard skipped: requires '${requirement}'` : "Storyboard skipped: not routable by this run",
1174
+ task: "",
1175
+ passed: true,
1176
+ skipped: true,
1177
+ skip_reason: "requirement_unmet",
1178
+ skip: { reason: "requirement_unmet", detail, ...requirement && { requirement } },
1179
+ duration_ms: 0,
1180
+ validations: [],
1181
+ context: {},
1182
+ extraction: { path: "none" }
1183
+ }
1184
+ ]
1185
+ }
1186
+ ],
1187
+ context: {},
1188
+ total_duration_ms: 0,
1189
+ passed_count: 0,
1190
+ failed_count: 0,
1191
+ skipped_count: 1,
1192
+ runner_capability_version: LIBRARY_VERSION,
1193
+ tested_at: (/* @__PURE__ */ new Date()).toISOString(),
1194
+ strict_validation_summary: {
1195
+ observable: false,
1196
+ checked: 0,
1197
+ passed: 0,
1198
+ failed: 0,
1199
+ strict_only_failures: 0,
1200
+ lenient_also_failed: 0
1201
+ },
1202
+ notices: collectCapabilityNotices(storyboard, runOptions._profile)
1203
+ };
1204
+ }
891
1205
  function extractFailures(results, storyboards, agentRef, fixOptions = {}) {
892
1206
  const failures = [];
893
1207
  const sbLookup = /* @__PURE__ */ new Map();
@@ -987,8 +1301,12 @@ async function complyImpl(agentUrl, options) {
987
1301
  complianceDir,
988
1302
  schemaRoot,
989
1303
  hostedStableLineAlias,
1304
+ routeStoryboard,
990
1305
  ...testOptions
991
1306
  } = options;
1307
+ if (routeStoryboard !== void 0 && typeof routeStoryboard !== "function") {
1308
+ throw new TypeError("routeStoryboard must be a function");
1309
+ }
992
1310
  const resolveOptions = {
993
1311
  ...version !== void 0 && { version },
994
1312
  ...complianceDir !== void 0 && { complianceDir },
@@ -1193,7 +1511,7 @@ async function complyImpl(agentUrl, options) {
1193
1511
  stoppedForTimeoutBudget = true;
1194
1512
  break;
1195
1513
  }
1196
- const result = await runStoryboard(agentUrl, sb, runOptions);
1514
+ const result = await runComplyStoryboard(agentUrl, sb, runOptions, profile, routeStoryboard);
1197
1515
  storyboardResults.push(result);
1198
1516
  executedStoryboards.push(sb);
1199
1517
  }