@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.
- package/dist/lib/registry/types.generated.d.mts +12 -0
- package/dist/lib/registry/types.generated.d.ts +12 -0
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/auth-signature.d.mts +5 -2
- package/dist/lib/server/auth-signature.d.ts +5 -2
- package/dist/lib/signing/content-digest.d.mts +8 -0
- package/dist/lib/signing/content-digest.d.ts +8 -0
- package/dist/lib/signing/content-digest.js +13 -4
- package/dist/lib/signing/content-digest.mjs +11 -4
- package/dist/lib/signing/parser.d.mts +1 -1
- package/dist/lib/signing/parser.d.ts +1 -1
- package/dist/lib/signing/parser.js +3 -2
- package/dist/lib/signing/parser.mjs +3 -2
- package/dist/lib/signing/verifier.d.mts +6 -2
- package/dist/lib/signing/verifier.d.ts +6 -2
- package/dist/lib/signing/verifier.js +14 -10
- package/dist/lib/signing/verifier.mjs +15 -10
- package/dist/lib/testing/compliance/comply.d.mts +107 -1
- package/dist/lib/testing/compliance/comply.d.ts +107 -1
- package/dist/lib/testing/compliance/comply.js +313 -1
- package/dist/lib/testing/compliance/comply.mjs +320 -2
- package/dist/lib/testing/compliance/index.d.mts +1 -1
- package/dist/lib/testing/compliance/index.d.ts +1 -1
- package/dist/lib/testing/index.d.mts +1 -1
- package/dist/lib/testing/index.d.ts +1 -1
- package/dist/lib/testing/storyboard/runner.js +10 -0
- package/dist/lib/testing/storyboard/runner.mjs +8 -0
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- 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 +1 -1
- package/docs/guides/VALIDATE-YOUR-AGENT.md +49 -0
- package/docs/llms.txt +1 -1
- package/docs/migration-13-to-14.md +14 -7
- package/docs/migration-14.x-rc-worksheet.md +4 -4
- 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 (
|
|
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 {
|
|
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
|
|
1514
|
+
const result = await runComplyStoryboard(agentUrl, sb, runOptions, profile, routeStoryboard);
|
|
1197
1515
|
storyboardResults.push(result);
|
|
1198
1516
|
executedStoryboards.push(sb);
|
|
1199
1517
|
}
|