@adcp/sdk 6.19.1 → 7.1.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/bin/adcp.js +48 -0
- package/dist/lib/core/AgentClient.d.ts.map +1 -1
- package/dist/lib/core/ConversationTypes.d.ts +33 -2
- package/dist/lib/core/ConversationTypes.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
- package/dist/lib/core/SingleAgentClient.js +21 -8
- package/dist/lib/core/SingleAgentClient.js.map +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/create-adcp-server.d.ts.map +1 -1
- package/dist/lib/server/create-adcp-server.js +8 -7
- package/dist/lib/server/create-adcp-server.js.map +1 -1
- package/dist/lib/server/decisioning/runtime/from-platform.js +54 -52
- package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
- package/dist/lib/server/envelope-allowlist.d.ts.map +1 -1
- package/dist/lib/server/envelope-allowlist.js +17 -0
- package/dist/lib/server/envelope-allowlist.js.map +1 -1
- package/dist/lib/server/idempotency/store.d.ts +8 -0
- package/dist/lib/server/idempotency/store.d.ts.map +1 -1
- package/dist/lib/server/idempotency/store.js +28 -4
- package/dist/lib/server/idempotency/store.js.map +1 -1
- package/dist/lib/server/wire-spec-fields.generated.js +1 -1
- package/dist/lib/testing/client.d.ts +16 -2
- package/dist/lib/testing/client.d.ts.map +1 -1
- package/dist/lib/testing/client.js +1 -0
- package/dist/lib/testing/client.js.map +1 -1
- package/dist/lib/testing/compliance/comply.d.ts.map +1 -1
- package/dist/lib/testing/compliance/comply.js +79 -15
- package/dist/lib/testing/compliance/comply.js.map +1 -1
- package/dist/lib/testing/compliance/spec-conformance.d.ts.map +1 -1
- package/dist/lib/testing/compliance/spec-conformance.js +1 -0
- package/dist/lib/testing/compliance/spec-conformance.js.map +1 -1
- package/dist/lib/testing/compliance/types.d.ts +35 -0
- package/dist/lib/testing/compliance/types.d.ts.map +1 -1
- package/dist/lib/testing/index.d.ts +1 -1
- package/dist/lib/testing/index.d.ts.map +1 -1
- package/dist/lib/testing/index.js.map +1 -1
- package/dist/lib/testing/storyboard/default-invariants.js +21 -2
- package/dist/lib/testing/storyboard/default-invariants.js.map +1 -1
- package/dist/lib/testing/storyboard/index.d.ts +1 -1
- package/dist/lib/testing/storyboard/index.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/index.js.map +1 -1
- package/dist/lib/testing/storyboard/loader.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/loader.js +17 -0
- package/dist/lib/testing/storyboard/loader.js.map +1 -1
- package/dist/lib/testing/storyboard/parallel-dispatch.d.ts +110 -0
- package/dist/lib/testing/storyboard/parallel-dispatch.d.ts.map +1 -0
- package/dist/lib/testing/storyboard/parallel-dispatch.js +221 -0
- package/dist/lib/testing/storyboard/parallel-dispatch.js.map +1 -0
- package/dist/lib/testing/storyboard/request-builder.js +1 -1
- package/dist/lib/testing/storyboard/request-builder.js.map +1 -1
- package/dist/lib/testing/storyboard/runner.d.ts +3 -1
- package/dist/lib/testing/storyboard/runner.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/runner.js +591 -49
- package/dist/lib/testing/storyboard/runner.js.map +1 -1
- package/dist/lib/testing/storyboard/task-map.d.ts +1 -0
- package/dist/lib/testing/storyboard/task-map.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/task-map.js +7 -1
- package/dist/lib/testing/storyboard/task-map.js.map +1 -1
- package/dist/lib/testing/storyboard/types.d.ts +209 -2
- package/dist/lib/testing/storyboard/types.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/types.js +2 -0
- package/dist/lib/testing/storyboard/types.js.map +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +38 -0
- package/dist/lib/testing/storyboard/validations.d.ts.map +1 -1
- package/dist/lib/testing/storyboard/validations.js +143 -0
- package/dist/lib/testing/storyboard/validations.js.map +1 -1
- package/dist/lib/testing/types.d.ts +2 -0
- package/dist/lib/testing/types.d.ts.map +1 -1
- package/dist/lib/utils/error-extraction.d.ts +2 -1
- package/dist/lib/utils/error-extraction.d.ts.map +1 -1
- package/dist/lib/utils/error-extraction.js +28 -0
- package/dist/lib/utils/error-extraction.js.map +1 -1
- package/dist/lib/utils/request-normalizer.d.ts +1 -0
- package/dist/lib/utils/request-normalizer.d.ts.map +1 -1
- package/dist/lib/utils/request-normalizer.js +34 -11
- package/dist/lib/utils/request-normalizer.js.map +1 -1
- package/dist/lib/utils/response-unwrapper.d.ts +33 -0
- package/dist/lib/utils/response-unwrapper.d.ts.map +1 -1
- package/dist/lib/utils/response-unwrapper.js +41 -3
- package/dist/lib/utils/response-unwrapper.js.map +1 -1
- package/dist/lib/version.d.ts +10 -6
- package/dist/lib/version.d.ts.map +1 -1
- package/dist/lib/version.js +11 -4
- package/dist/lib/version.js.map +1 -1
- package/package.json +1 -1
- package/skills/build-decisioning-platform/advanced/REFERENCE.md +2 -0
|
@@ -29,8 +29,10 @@ const rejection_hints_1 = require("./rejection-hints");
|
|
|
29
29
|
const shape_drift_hints_1 = require("./shape-drift-hints");
|
|
30
30
|
const strict_validation_hints_1 = require("./strict-validation-hints");
|
|
31
31
|
const validations_1 = require("./validations");
|
|
32
|
+
const parallel_dispatch_1 = require("./parallel-dispatch");
|
|
32
33
|
const path_1 = require("./path");
|
|
33
34
|
const redact_secrets_1 = require("../../utils/redact-secrets");
|
|
35
|
+
const response_unwrapper_1 = require("../../utils/response-unwrapper");
|
|
34
36
|
const test_controller_1 = require("../test-controller");
|
|
35
37
|
const request_builder_1 = require("./request-builder");
|
|
36
38
|
const client_2 = require("../client");
|
|
@@ -242,6 +244,7 @@ function applyBranchSetGrading(phases, phaseResults, branchSetByPhaseId, contrib
|
|
|
242
244
|
step.skip_reason = 'peer_branch_taken';
|
|
243
245
|
step.skip = { reason: 'peer_branch_taken', detail };
|
|
244
246
|
delete step.error;
|
|
247
|
+
delete step.adcp_error;
|
|
245
248
|
skippedDelta++;
|
|
246
249
|
regraded = true;
|
|
247
250
|
}
|
|
@@ -679,6 +682,7 @@ function buildCapabilityUnsupportedResult(agentUrls, storyboard, detail) {
|
|
|
679
682
|
strict_only_failures: 0,
|
|
680
683
|
lenient_also_failed: 0,
|
|
681
684
|
},
|
|
685
|
+
notices: [],
|
|
682
686
|
};
|
|
683
687
|
}
|
|
684
688
|
/**
|
|
@@ -698,6 +702,8 @@ const REQUIREMENT_TO_SKIP_REASON = {
|
|
|
698
702
|
controller: 'missing_test_controller',
|
|
699
703
|
seeded_state: 'requirement_unmet',
|
|
700
704
|
real_wire: 'requirement_unmet',
|
|
705
|
+
webhook_receiver: 'requirement_unmet',
|
|
706
|
+
request_signer: 'not_applicable',
|
|
701
707
|
};
|
|
702
708
|
/**
|
|
703
709
|
* Build a minimal StoryboardResult for a storyboard skipped because a
|
|
@@ -753,6 +759,7 @@ function buildRequirementUnmetResult(agentUrls, storyboard, requirement, detail)
|
|
|
753
759
|
strict_only_failures: 0,
|
|
754
760
|
lenient_also_failed: 0,
|
|
755
761
|
},
|
|
762
|
+
notices: [],
|
|
756
763
|
};
|
|
757
764
|
}
|
|
758
765
|
/**
|
|
@@ -770,8 +777,25 @@ function buildRequirementUnmetResult(agentUrls, storyboard, requirement, detail)
|
|
|
770
777
|
* - `real_wire` — always available; the runner is hitting a real wire
|
|
771
778
|
* by definition. The tag is a no-op gate today, reserved for a
|
|
772
779
|
* future `--mock-only` mode.
|
|
780
|
+
* - `webhook_receiver` — operator supplied `options.webhook_receiver`.
|
|
781
|
+
* Autodetected from step `sample_request` token presence (see
|
|
782
|
+
* `detectImplicitRequires`); authors don't write this tag manually.
|
|
783
|
+
* Spec: adcp-client#1678.
|
|
784
|
+
* - `request_signer` — agent advertises `request_signing.supported: true`
|
|
785
|
+
* in `get_adcp_capabilities`. Autodetected for any storyboard whose
|
|
786
|
+
* id is `'signed_requests'` or that contains a `request_signing_probe`
|
|
787
|
+
* step (see `detectImplicitRequires`); authors don't write this tag
|
|
788
|
+
* manually. Absence-skip semantics are INVERTED from the general
|
|
789
|
+
* `requires_capability` rule: the signed-requests universal
|
|
790
|
+
* storyboard's own gating spec (signed-requests.yaml prerequisites)
|
|
791
|
+
* declares "agents that do not advertise support are not tested
|
|
792
|
+
* against this storyboard — absence of advertisement is not a
|
|
793
|
+
* failure". When the runner can't read capabilities (no profile
|
|
794
|
+
* threaded through), the gate is a no-op — the caller accepted
|
|
795
|
+
* responsibility for capability compatibility by reusing an external
|
|
796
|
+
* client. Spec: adcp-client#1702.
|
|
773
797
|
*/
|
|
774
|
-
function checkRequires(requires, options) {
|
|
798
|
+
function checkRequires(requires, options, profile) {
|
|
775
799
|
for (const requirement of requires) {
|
|
776
800
|
switch (requirement) {
|
|
777
801
|
case 'controller': {
|
|
@@ -799,10 +823,119 @@ function checkRequires(requires, options) {
|
|
|
799
823
|
case 'real_wire':
|
|
800
824
|
// Always available — reserved for a future --mock-only mode.
|
|
801
825
|
break;
|
|
826
|
+
case 'webhook_receiver': {
|
|
827
|
+
if (options.webhook_receiver === undefined) {
|
|
828
|
+
return {
|
|
829
|
+
requirement,
|
|
830
|
+
detail: 'Storyboard references `{{runner.webhook_url:…}}` or `{{runner.webhook_base}}` but no ' +
|
|
831
|
+
'webhook receiver is configured. Pass `webhook_receiver` in StoryboardRunOptions ' +
|
|
832
|
+
'(or `--webhook-receiver` on the CLI) to host a receiver, or omit this storyboard. ' +
|
|
833
|
+
'Required by the webhook-emission universal: ' +
|
|
834
|
+
'compliance/{version}/universal/webhook-emission.yaml grades not_applicable when ' +
|
|
835
|
+
'no receiver is configured (prerequisites section).',
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
case 'request_signer': {
|
|
841
|
+
// Gate is a no-op when no profile is threaded through (external
|
|
842
|
+
// `_client` mode). The caller has accepted responsibility for
|
|
843
|
+
// capability compatibility; failing the gate here would surprise
|
|
844
|
+
// them with a skip they can't explain from CLI options alone.
|
|
845
|
+
if (!profile?.raw_capabilities)
|
|
846
|
+
break;
|
|
847
|
+
const supported = resolveCapabilityPath(profile.raw_capabilities, 'request_signing.supported');
|
|
848
|
+
if (supported === true)
|
|
849
|
+
break;
|
|
850
|
+
return {
|
|
851
|
+
requirement,
|
|
852
|
+
detail: 'Storyboard requires `request_signing.supported: true` in `get_adcp_capabilities`; ' +
|
|
853
|
+
`agent declared ${supported === undefined ? 'no request_signing block' : JSON.stringify(supported)}. ` +
|
|
854
|
+
'Per compliance/{version}/universal/signed-requests.yaml: "Agents that do not advertise ' +
|
|
855
|
+
'support are not tested against this storyboard — absence of advertisement is not a ' +
|
|
856
|
+
'failure, it is a declaration that the agent does not offer verified signed requests." ' +
|
|
857
|
+
'To opt in, advertise `request_signing.supported: true` and pre-register the runner ' +
|
|
858
|
+
'compliance test keypair per test-kits/signed-requests-runner.yaml. ' +
|
|
859
|
+
'Forward-readiness: optional in AdCP 3.0; the schema (request_signing block description) ' +
|
|
860
|
+
'declares request signing required for spend-committing operations in AdCP 4.0. Sellers ' +
|
|
861
|
+
'that intend to support spend-committing tools SHOULD advertise the capability and ' +
|
|
862
|
+
'register the test keypair before the 4.0 cut to avoid a hard compliance failure then.',
|
|
863
|
+
};
|
|
864
|
+
}
|
|
802
865
|
}
|
|
803
866
|
}
|
|
804
867
|
return null;
|
|
805
868
|
}
|
|
869
|
+
/**
|
|
870
|
+
* Webhook-token regex used to autodetect the implicit `webhook_receiver`
|
|
871
|
+
* requirement. Matches `{{runner.webhook_url:<step_id>}}` and the bare
|
|
872
|
+
* `{{runner.webhook_base}}`. Same shape as the `MUSTACHE_TOKEN_RE` in
|
|
873
|
+
* context.ts but scoped to the two webhook-bearing tokens — we only care
|
|
874
|
+
* whether the storyboard needs a receiver, not what substitution it
|
|
875
|
+
* would produce. Single `[^{}]+` body avoids the polynomial-redos
|
|
876
|
+
* backtrack pattern flagged by CodeQL alert #49.
|
|
877
|
+
*/
|
|
878
|
+
const WEBHOOK_TOKEN_RE = /\{\{runner\.(?:webhook_url:[A-Za-z0-9_]+|webhook_base)\}\}/;
|
|
879
|
+
/**
|
|
880
|
+
* Recursively scan a JSON-like value for any webhook receiver token. The
|
|
881
|
+
* scan only touches strings; objects and arrays are walked structurally
|
|
882
|
+
* so deeply-nested `push_notification_config.url` entries (or any other
|
|
883
|
+
* authoring pattern) are discovered without a hard-coded field list.
|
|
884
|
+
*/
|
|
885
|
+
function valueContainsWebhookToken(value) {
|
|
886
|
+
if (typeof value === 'string') {
|
|
887
|
+
return WEBHOOK_TOKEN_RE.test(value);
|
|
888
|
+
}
|
|
889
|
+
if (Array.isArray(value)) {
|
|
890
|
+
return value.some(valueContainsWebhookToken);
|
|
891
|
+
}
|
|
892
|
+
if (value !== null && typeof value === 'object') {
|
|
893
|
+
return Object.values(value).some(valueContainsWebhookToken);
|
|
894
|
+
}
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Return the implicit requirements a storyboard needs based on its
|
|
899
|
+
* structure (not its declared `requires:` list). Today:
|
|
900
|
+
* - `'webhook_receiver'`, autodetected from `{{runner.webhook_url:…}}`
|
|
901
|
+
* / `{{runner.webhook_base}}` token presence inside any step's
|
|
902
|
+
* `sample_request`. Token presence is the authoring contract — a
|
|
903
|
+
* storyboard that names the runner's webhook receiver cannot run
|
|
904
|
+
* without one — so authors don't need to remember to add
|
|
905
|
+
* `requires: [webhook_receiver]` separately. Spec: adcp-client#1678.
|
|
906
|
+
* - `'request_signer'`, autodetected from `storyboard.id ===
|
|
907
|
+
* 'signed_requests'` or any step using the synthesized
|
|
908
|
+
* `request_signing_probe` task. The signed-requests universal
|
|
909
|
+
* storyboard's own prerequisites section declares the
|
|
910
|
+
* `request_signing.supported: true` capability gate; the runner
|
|
911
|
+
* enforces it here so adopters don't see false-negative vector
|
|
912
|
+
* failures on bearer-only agents that never claimed signing.
|
|
913
|
+
* Spec: adcp-client#1702.
|
|
914
|
+
*/
|
|
915
|
+
function detectImplicitRequires(storyboard) {
|
|
916
|
+
const requires = [];
|
|
917
|
+
let needsWebhook = false;
|
|
918
|
+
let needsSigner = storyboard.id === 'signed_requests';
|
|
919
|
+
for (const phase of storyboard.phases) {
|
|
920
|
+
for (const step of phase.steps) {
|
|
921
|
+
if (!needsWebhook && step.sample_request && valueContainsWebhookToken(step.sample_request)) {
|
|
922
|
+
needsWebhook = true;
|
|
923
|
+
}
|
|
924
|
+
if (!needsSigner && step.task === 'request_signing_probe') {
|
|
925
|
+
needsSigner = true;
|
|
926
|
+
}
|
|
927
|
+
if (needsWebhook && needsSigner)
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
if (needsWebhook && needsSigner)
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
if (needsWebhook)
|
|
934
|
+
requires.push('webhook_receiver');
|
|
935
|
+
if (needsSigner)
|
|
936
|
+
requires.push('request_signer');
|
|
937
|
+
return requires;
|
|
938
|
+
}
|
|
806
939
|
/**
|
|
807
940
|
* Build a hard-failure StoryboardResult for when agent capability
|
|
808
941
|
* discovery (`get_agent_info` / MCP `tools/list`) failed. Surfacing
|
|
@@ -858,6 +991,7 @@ function buildDiscoveryFailedResult(agentUrls, storyboard, discoveryStep) {
|
|
|
858
991
|
strict_only_failures: 0,
|
|
859
992
|
lenient_also_failed: 0,
|
|
860
993
|
},
|
|
994
|
+
notices: [],
|
|
861
995
|
};
|
|
862
996
|
}
|
|
863
997
|
/**
|
|
@@ -915,8 +1049,85 @@ function buildRequiredToolsMissingResult(agentUrls, storyboard, detail) {
|
|
|
915
1049
|
strict_only_failures: 0,
|
|
916
1050
|
lenient_also_failed: 0,
|
|
917
1051
|
},
|
|
1052
|
+
notices: [],
|
|
918
1053
|
};
|
|
919
1054
|
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Collect protocol-compliance notices for a storyboard run based on the
|
|
1057
|
+
* agent's declared capabilities. Returns an empty array when rawCaps is
|
|
1058
|
+
* absent (standalone runner without pre-fetched profile).
|
|
1059
|
+
*
|
|
1060
|
+
* Currently emits two spec-grounded notices (adcp-client#1704):
|
|
1061
|
+
*
|
|
1062
|
+
* - `request_signing.required`: `request_signing.supported` is absent or
|
|
1063
|
+
* false on the signed_requests storyboard. Signing becomes required for
|
|
1064
|
+
* spend-committing operations in `effective_version: '4.0'`.
|
|
1065
|
+
*
|
|
1066
|
+
* - `webhook_signing.legacy_hmac_fallback.removed`: agent claims
|
|
1067
|
+
* `webhook_signing.legacy_hmac_fallback: true`, which is removed in
|
|
1068
|
+
* `effective_version: '4.0'`.
|
|
1069
|
+
*
|
|
1070
|
+
* Note: a third notice (`signed_requests_specialism_deprecated`) is deferred
|
|
1071
|
+
* pending upstream deprecation of the `'signed-requests'` specialism value in
|
|
1072
|
+
* adcontextprotocol/adcp#4418 — the value is still active in the spec.
|
|
1073
|
+
*/
|
|
1074
|
+
function collectCapabilityNotices(storyboard, rawCaps) {
|
|
1075
|
+
const notices = [];
|
|
1076
|
+
if (!rawCaps || typeof rawCaps !== 'object')
|
|
1077
|
+
return notices;
|
|
1078
|
+
const caps = rawCaps;
|
|
1079
|
+
// Notice: request_signing required in AdCP 4.0.
|
|
1080
|
+
// Fired when this is the signed_requests storyboard and the agent hasn't
|
|
1081
|
+
// declared request_signing support. The storyboard is identified by id OR
|
|
1082
|
+
// by the presence of a request_signing_probe step (mirrors the implicit-
|
|
1083
|
+
// require detection in detectImplicitRequires for PR #1703's gate).
|
|
1084
|
+
const isSignedRequestsStoryboard = storyboard.id === 'signed_requests' ||
|
|
1085
|
+
storyboard.phases.some(p => p.steps.some(s => s.task === 'request_signing_probe'));
|
|
1086
|
+
if (isSignedRequestsStoryboard) {
|
|
1087
|
+
const requestSigning = caps['request_signing'];
|
|
1088
|
+
if (requestSigning?.['supported'] !== true) {
|
|
1089
|
+
notices.push({
|
|
1090
|
+
severity: 'future_required',
|
|
1091
|
+
code: 'request_signing.required',
|
|
1092
|
+
message: 'RFC 9421 request signing (`request_signing.supported: true`) is not advertised. ' +
|
|
1093
|
+
'Required for spend-committing operations in AdCP 4.0 — declare the capability and ' +
|
|
1094
|
+
'pre-register the runner compliance test keypair before the 4.0 cut.',
|
|
1095
|
+
effective_version: '4.0',
|
|
1096
|
+
capability_path: 'request_signing.supported',
|
|
1097
|
+
docs_url: 'https://adcontextprotocol.org/docs/building/implementation/security#signed-requests-transport-layer',
|
|
1098
|
+
storyboard_ids: [storyboard.id],
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
// Notice: legacy_hmac_fallback removed in AdCP 4.0.
|
|
1103
|
+
// Scoped via the WEBHOOK_STEP_TASKS step-task set rather than an id-regex.
|
|
1104
|
+
// Step-task presence is the authoring contract — a storyboard that asserts
|
|
1105
|
+
// webhook delivery uses one of these tasks. A storyboard id alone (e.g. a
|
|
1106
|
+
// hypothetical `webhook_authoring_guide`) shouldn't trigger the notice
|
|
1107
|
+
// unless it actually exercises the delivery path.
|
|
1108
|
+
const WEBHOOK_STEP_TASKS = new Set([
|
|
1109
|
+
'expect_webhook',
|
|
1110
|
+
'expect_webhook_retry_keys_stable',
|
|
1111
|
+
'expect_webhook_signature_valid',
|
|
1112
|
+
]);
|
|
1113
|
+
const isWebhookRelatedStoryboard = storyboard.phases.some(p => p.steps.some(s => WEBHOOK_STEP_TASKS.has(s.task)));
|
|
1114
|
+
if (isWebhookRelatedStoryboard) {
|
|
1115
|
+
const webhookSigning = caps['webhook_signing'];
|
|
1116
|
+
if (webhookSigning?.['legacy_hmac_fallback'] === true) {
|
|
1117
|
+
notices.push({
|
|
1118
|
+
severity: 'deprecation',
|
|
1119
|
+
code: 'webhook_signing.legacy_hmac_fallback.removed',
|
|
1120
|
+
message: '`webhook_signing.legacy_hmac_fallback: true` is deprecated and removed in AdCP 4.0. ' +
|
|
1121
|
+
'Migrate webhook signature verification to RFC 9421 before the 4.0 cut.',
|
|
1122
|
+
effective_version: '4.0',
|
|
1123
|
+
capability_path: 'webhook_signing.legacy_hmac_fallback',
|
|
1124
|
+
docs_url: 'https://adcontextprotocol.org/docs/building/implementation/webhooks',
|
|
1125
|
+
storyboard_ids: [storyboard.id],
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return notices;
|
|
1130
|
+
}
|
|
920
1131
|
/**
|
|
921
1132
|
* Execute a single pass of the storyboard against the supplied replica URLs
|
|
922
1133
|
* using round-robin dispatch starting at `dispatchOffset`. Called directly
|
|
@@ -1030,12 +1241,33 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
|
|
|
1030
1241
|
// Spec: adcp-client#1626. The default (no `requires` field) is
|
|
1031
1242
|
// `[real_wire]`, which is always available — untagged storyboards
|
|
1032
1243
|
// run unchanged.
|
|
1033
|
-
|
|
1034
|
-
|
|
1244
|
+
//
|
|
1245
|
+
// Implicit requirements (adcp-client#1678) are unioned with the
|
|
1246
|
+
// declared list: today this means `'webhook_receiver'` is added when
|
|
1247
|
+
// any step's `sample_request` references `{{runner.webhook_url:…}}`
|
|
1248
|
+
// or `{{runner.webhook_base}}`. Authors do not need to retag those
|
|
1249
|
+
// storyboards — the token presence is the declaration. Without the
|
|
1250
|
+
// implicit gate, storyboards that name the receiver but run without
|
|
1251
|
+
// one would silently ship literal mustache tokens on the wire and
|
|
1252
|
+
// get rejected by 3.0-strict sellers as `INVALID_REQUEST: relative URL`.
|
|
1253
|
+
// Pre-flight notice collection. Uses options._profile (set by the comply()
|
|
1254
|
+
// pipeline before calling runStoryboard) so the notices are available on
|
|
1255
|
+
// early-return results (requirement-unmet, capability-unsupported). In the
|
|
1256
|
+
// standalone runner path options._profile may be undefined; notices will be
|
|
1257
|
+
// collected again from the fully-fetched profile at result-build time.
|
|
1258
|
+
const preflightNotices = collectCapabilityNotices(storyboard, options._profile?.raw_capabilities);
|
|
1259
|
+
const declared = storyboard.requires ?? [];
|
|
1260
|
+
const implicit = detectImplicitRequires(storyboard);
|
|
1261
|
+
const allRequires = [...declared, ...implicit.filter(r => !declared.includes(r))];
|
|
1262
|
+
if (allRequires.length) {
|
|
1263
|
+
const unmet = checkRequires(allRequires, options, profile);
|
|
1035
1264
|
if (unmet) {
|
|
1036
1265
|
if (!options._client)
|
|
1037
1266
|
await (0, protocols_1.closeConnections)(options.protocol);
|
|
1038
|
-
return
|
|
1267
|
+
return {
|
|
1268
|
+
...buildRequirementUnmetResult(agentUrls, storyboard, unmet.requirement, unmet.detail),
|
|
1269
|
+
notices: preflightNotices,
|
|
1270
|
+
};
|
|
1039
1271
|
}
|
|
1040
1272
|
}
|
|
1041
1273
|
// Evaluate requires_capability predicate before any phase setup.
|
|
@@ -1061,7 +1293,7 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
|
|
|
1061
1293
|
`agent declared ${JSON.stringify(actual)}.`;
|
|
1062
1294
|
if (!options._client)
|
|
1063
1295
|
await (0, protocols_1.closeConnections)(options.protocol);
|
|
1064
|
-
return buildCapabilityUnsupportedResult(agentUrls, storyboard, detail);
|
|
1296
|
+
return { ...buildCapabilityUnsupportedResult(agentUrls, storyboard, detail), notices: preflightNotices };
|
|
1065
1297
|
}
|
|
1066
1298
|
}
|
|
1067
1299
|
}
|
|
@@ -1081,7 +1313,10 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
|
|
|
1081
1313
|
if (!hasAnyRequired) {
|
|
1082
1314
|
if (!options._client)
|
|
1083
1315
|
await (0, protocols_1.closeConnections)(options.protocol);
|
|
1084
|
-
return
|
|
1316
|
+
return {
|
|
1317
|
+
...buildRequiredToolsMissingResult(agentUrls, storyboard, `agent does not advertise any of [${storyboard.required_tools.join(', ')}]`),
|
|
1318
|
+
notices: preflightNotices,
|
|
1319
|
+
};
|
|
1085
1320
|
}
|
|
1086
1321
|
}
|
|
1087
1322
|
let context = { ...options.context };
|
|
@@ -1366,6 +1601,18 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
|
|
|
1366
1601
|
});
|
|
1367
1602
|
continue;
|
|
1368
1603
|
}
|
|
1604
|
+
// Pre-empt OAuth-metadata probes when the agent's capabilities never
|
|
1605
|
+
// advertised OAuth at all (adcp-client#1702). Without this, an
|
|
1606
|
+
// API-key-only agent (e.g. Wonderstruck) has its PRM endpoint
|
|
1607
|
+
// probed; if the well-known path returns anything other than 404
|
|
1608
|
+
// (the existing reactive cascade trigger in `executeProbeStep`),
|
|
1609
|
+
// validations fail and `oauth_discovery` produces 5 false-negative
|
|
1610
|
+
// step failures even though the phase is `optional: true`. The skip
|
|
1611
|
+
// is phase-level — not storyboard-level — so the universal
|
|
1612
|
+
// `unauth_rejection` and `mechanism_required` phases still run.
|
|
1613
|
+
if (!phaseAbsent && phaseContainsOauthMetadataProbe(phase) && !agentAdvertisesOauth(profile)) {
|
|
1614
|
+
phaseAbsent = true;
|
|
1615
|
+
}
|
|
1369
1616
|
// Reset alias cache at this phase boundary (#1657). $generate:uuid_v4#alias
|
|
1370
1617
|
// is designed to be stable within a scenario — the initial call and its
|
|
1371
1618
|
// idempotency replay share the same UUID — but aliases must NOT bleed across
|
|
@@ -1569,6 +1816,20 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
|
|
|
1569
1816
|
}
|
|
1570
1817
|
stepResults.push(result);
|
|
1571
1818
|
priorStepResults.set(step.id, result);
|
|
1819
|
+
// Schema-validation short-circuit (adcp-client#1709). When the
|
|
1820
|
+
// response unwrapper rejected the agent's response against the SDK's
|
|
1821
|
+
// Zod schema, the step-execution path synthesized a failing
|
|
1822
|
+
// `response_schema` ValidationResult on `result.validations`.
|
|
1823
|
+
// Running step-scope invariants against a response the SDK has
|
|
1824
|
+
// already declared malformed produces noise — the invariants
|
|
1825
|
+
// can't meaningfully grade a payload that didn't parse. Worse,
|
|
1826
|
+
// before #1709 the invariants' failure entries crowded out the
|
|
1827
|
+
// schema-validation entry in `extractFailures`, masking the root
|
|
1828
|
+
// cause across BidMachine's 10+ deploys (see adcp#4419). Skip the
|
|
1829
|
+
// invariant pass entirely on this step and emit a single skipped
|
|
1830
|
+
// `assertion` entry per invariant so consumers can still see WHICH
|
|
1831
|
+
// invariants were skipped and why.
|
|
1832
|
+
const schemaInvalidResponse = result.validations.some(v => v.check === 'response_schema' && !v.passed);
|
|
1572
1833
|
// Fire per-step assertions. Each result is appended to the step's
|
|
1573
1834
|
// `validations[]` under `check: "assertion"` so existing UI renders
|
|
1574
1835
|
// them alongside inline checks, and mirrored into `assertionResults`
|
|
@@ -1583,6 +1844,18 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
|
|
|
1583
1844
|
// behavior the invariant would flag (validated at runner start).
|
|
1584
1845
|
if ((0, assertions_1.stepDisablesAssertion)(step.invariants, spec.id))
|
|
1585
1846
|
continue;
|
|
1847
|
+
if (schemaInvalidResponse) {
|
|
1848
|
+
// Emit a skipped marker so the report shows the invariant was
|
|
1849
|
+
// intentionally bypassed (not silently dropped). `passed: true`
|
|
1850
|
+
// keeps it from flipping `result.passed` — the schema-validation
|
|
1851
|
+
// failure already did that.
|
|
1852
|
+
result.validations.push({
|
|
1853
|
+
check: 'assertion',
|
|
1854
|
+
passed: true,
|
|
1855
|
+
description: `${spec.id}: skipped — response failed schema validation (adcp-client#1709)`,
|
|
1856
|
+
});
|
|
1857
|
+
continue;
|
|
1858
|
+
}
|
|
1586
1859
|
const raw = await spec.onStep(assertionContexts.get(spec.id), result);
|
|
1587
1860
|
for (const r of raw) {
|
|
1588
1861
|
const full = { ...r, assertion_id: spec.id, scope: 'step', step_id: step.id };
|
|
@@ -1944,6 +2217,10 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
|
|
|
1944
2217
|
const schemasUsed = collectSchemasUsed(phaseResults);
|
|
1945
2218
|
const strictSummary = summarizeStrictValidation(phaseResults);
|
|
1946
2219
|
const validationsNotApplicable = countValidationsNotApplicable(phaseResults);
|
|
2220
|
+
// Use the fully-fetched profile for notice detection; fall back to pre-flight
|
|
2221
|
+
// notices (which used options._profile) when profile was not re-fetched in
|
|
2222
|
+
// this pass (standalone runner with options._profile pre-set skips the fetch).
|
|
2223
|
+
const notices = collectCapabilityNotices(storyboard, profile?.raw_capabilities ?? options._profile?.raw_capabilities);
|
|
1947
2224
|
const result = {
|
|
1948
2225
|
storyboard_id: storyboard.id,
|
|
1949
2226
|
storyboard_title: storyboard.title,
|
|
@@ -1966,6 +2243,7 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
|
|
|
1966
2243
|
...(schemasUsed.length > 0 ? { schemas_used: schemasUsed } : {}),
|
|
1967
2244
|
...(assertionResults.length > 0 ? { assertions: assertionResults } : {}),
|
|
1968
2245
|
strict_validation_summary: strictSummary,
|
|
2246
|
+
notices,
|
|
1969
2247
|
};
|
|
1970
2248
|
// Close protocol connections when the runner created its own client. The
|
|
1971
2249
|
// connection pool is keyed by URL+auth, so a single closeConnections() call
|
|
@@ -2042,6 +2320,8 @@ async function runMultiPass(agentUrls, storyboard, options) {
|
|
|
2042
2320
|
// readers see a per-pass timeline; de-duplicating would hide a real
|
|
2043
2321
|
// "passed on pass 1, failed on pass 2" divergence.
|
|
2044
2322
|
const assertionsAgg = passResults.flatMap(r => r.assertions ?? []);
|
|
2323
|
+
// Notices are identical across passes (same agent, same capabilities); deduplicate by code.
|
|
2324
|
+
const noticesDedup = [...new Map(passResults.flatMap(r => r.notices).map(n => [n.code, n])).values()];
|
|
2045
2325
|
return {
|
|
2046
2326
|
storyboard_id: storyboard.id,
|
|
2047
2327
|
storyboard_title: storyboard.title,
|
|
@@ -2060,6 +2340,7 @@ async function runMultiPass(agentUrls, storyboard, options) {
|
|
|
2060
2340
|
tested_at: new Date().toISOString(),
|
|
2061
2341
|
...(schemasDedup.length > 0 ? { schemas_used: schemasDedup } : {}),
|
|
2062
2342
|
...(assertionsAgg.length > 0 ? { assertions: assertionsAgg } : {}),
|
|
2343
|
+
notices: noticesDedup,
|
|
2063
2344
|
};
|
|
2064
2345
|
}
|
|
2065
2346
|
/**
|
|
@@ -2426,7 +2707,9 @@ client, step, phaseId, context, allSteps, options, state) {
|
|
|
2426
2707
|
// match the options. Enforcing this here (after builder + sample_request)
|
|
2427
2708
|
// prevents session-key divergence across create/get/update/delete steps
|
|
2428
2709
|
// when individual builders or sample_request YAML omit brand.
|
|
2429
|
-
|
|
2710
|
+
// `step.omit_account` suppresses account synthesis for schema_validation
|
|
2711
|
+
// steps that deliberately test the seller's missing-account rejection path.
|
|
2712
|
+
request = applyBrandInvariant(request, options, effectiveStep.task, { omit_account: step.omit_account });
|
|
2430
2713
|
// Per-run sandbox-bypass hint (#841). When the operator passes
|
|
2431
2714
|
// `--no-sandbox` (or sets `disable_sandbox: true` programmatically), the
|
|
2432
2715
|
// runner stamps `ext.adcp.disable_sandbox: true` on every outgoing
|
|
@@ -2507,24 +2790,115 @@ client, step, phaseId, context, allSteps, options, state) {
|
|
|
2507
2790
|
// and the server never sees a missing-key request. Paired flags so the two
|
|
2508
2791
|
// layers agree; see `applyIdempotencyInvariant` for the runner-level skip.
|
|
2509
2792
|
const testsMissingIdempotencyKey = step.omit_idempotency_key === true && (0, idempotency_1.isMutatingTask)(effectiveStep.task);
|
|
2510
|
-
//
|
|
2511
|
-
// `
|
|
2512
|
-
// `
|
|
2513
|
-
//
|
|
2514
|
-
//
|
|
2515
|
-
//
|
|
2516
|
-
//
|
|
2793
|
+
// Analogous to `testsMissingIdempotencyKey`: when a step sets
|
|
2794
|
+
// `omit_account: true` the runner has already suppressed account synthesis
|
|
2795
|
+
// in `applyBrandInvariant` (above — ordering is load-bearing: this must
|
|
2796
|
+
// come after `applyBrandInvariant` so the comment "above" stays accurate
|
|
2797
|
+
// if either block is reordered). Track the flag here so the raw-probe
|
|
2798
|
+
// defense-in-depth path is also triggered and no SDK-layer normalization
|
|
2799
|
+
// can silently re-inject an account before the wire call.
|
|
2800
|
+
const testsMissingAccount = step.omit_account === true && effectiveStep.task === 'create_media_buy';
|
|
2801
|
+
// Defense-in-depth for the missing-field vectors: when a step sets
|
|
2802
|
+
// `omit_idempotency_key: true` or `omit_account: true` and `step.auth` is
|
|
2803
|
+
// unset, route via `rawMcpProbe` anyway so no SDK-layer normalization can
|
|
2804
|
+
// slip the missing field onto the wire. The SDK's `skipIdempotencyAutoInject`
|
|
2805
|
+
// / `skipAccountValidation` plumbing already honors these flags, but the
|
|
2806
|
+
// raw-HTTP path removes the escape hatch entirely. A2A and oauth stay on
|
|
2807
|
+
// the SDK path — their dispatch can't be replicated here (A2A uses a
|
|
2808
|
+
// different envelope; oauth needs refresh semantics).
|
|
2517
2809
|
const rawProbeHeaders = step.auth !== undefined
|
|
2518
2810
|
? authHeadersForStep(step.auth, options)
|
|
2519
|
-
: testsMissingIdempotencyKey && options.protocol !== 'a2a'
|
|
2811
|
+
: (testsMissingIdempotencyKey || testsMissingAccount) && options.protocol !== 'a2a'
|
|
2520
2812
|
? defaultAuthHeadersForRawProbe(options)
|
|
2521
2813
|
: undefined;
|
|
2522
2814
|
const useRawProbe = rawProbeHeaders !== undefined;
|
|
2523
2815
|
let taskResult;
|
|
2524
2816
|
let stepResult;
|
|
2817
|
+
// Raw caught error from the dispatch fn — preserved as `unknown` so the
|
|
2818
|
+
// schema-validation attribution path below can `instanceof` it against
|
|
2819
|
+
// `ResponseSchemaValidationError`. Undefined when the step succeeded or
|
|
2820
|
+
// failed for a non-typed reason. Spec: adcp-client#1709.
|
|
2821
|
+
let caughtError;
|
|
2525
2822
|
let httpResult;
|
|
2526
2823
|
let responseRecord;
|
|
2527
2824
|
let a2aEnvelope;
|
|
2825
|
+
let crossResponses;
|
|
2826
|
+
// Parallel-dispatch fan-out: when the storyboard step declares
|
|
2827
|
+
// `parallel_dispatch`, the runner fires N concurrent dispatches against
|
|
2828
|
+
// the same agent with the same idempotency_key (default) and grades the
|
|
2829
|
+
// cross-response set instead of a single response. Gated on the
|
|
2830
|
+
// `parallel_dispatch_runner` test-kit contract — runners (or runs) without
|
|
2831
|
+
// it grade the step `not_applicable` so older runners don't fail on
|
|
2832
|
+
// newer storyboard contracts.
|
|
2833
|
+
if (step.parallel_dispatch && !useRawProbe) {
|
|
2834
|
+
const contractsInScope = new Set(options.contracts ?? []);
|
|
2835
|
+
if (!contractsInScope.has(parallel_dispatch_1.PARALLEL_DISPATCH_CONTRACT)) {
|
|
2836
|
+
const next = getNextStepPreview(step.id, allSteps, context, runState.runnerVars);
|
|
2837
|
+
const detail = `Test-kit contract "${parallel_dispatch_1.PARALLEL_DISPATCH_CONTRACT}" is not configured on this runner; concurrent-retry grading requires it.`;
|
|
2838
|
+
return {
|
|
2839
|
+
step_id: step.id,
|
|
2840
|
+
phase_id: phaseId,
|
|
2841
|
+
title: step.title,
|
|
2842
|
+
task: step.task,
|
|
2843
|
+
passed: true,
|
|
2844
|
+
skipped: true,
|
|
2845
|
+
skip_reason: 'not_applicable',
|
|
2846
|
+
skip: buildSkip('not_applicable', detail),
|
|
2847
|
+
duration_ms: 0,
|
|
2848
|
+
validations: [],
|
|
2849
|
+
context,
|
|
2850
|
+
next,
|
|
2851
|
+
extraction: { path: 'none' },
|
|
2852
|
+
};
|
|
2853
|
+
}
|
|
2854
|
+
const specError = (0, parallel_dispatch_1.validateParallelDispatchSpec)(step.parallel_dispatch);
|
|
2855
|
+
if (specError) {
|
|
2856
|
+
const next = getNextStepPreview(step.id, allSteps, context, runState.runnerVars);
|
|
2857
|
+
return {
|
|
2858
|
+
step_id: step.id,
|
|
2859
|
+
phase_id: phaseId,
|
|
2860
|
+
title: step.title,
|
|
2861
|
+
task: step.task,
|
|
2862
|
+
passed: false,
|
|
2863
|
+
duration_ms: 0,
|
|
2864
|
+
validations: [
|
|
2865
|
+
{
|
|
2866
|
+
check: 'parallel_dispatch_misconfigured',
|
|
2867
|
+
passed: false,
|
|
2868
|
+
description: specError,
|
|
2869
|
+
json_pointer: null,
|
|
2870
|
+
expected: 'a well-formed parallel_dispatch spec',
|
|
2871
|
+
actual: step.parallel_dispatch,
|
|
2872
|
+
schema_id: null,
|
|
2873
|
+
schema_url: null,
|
|
2874
|
+
},
|
|
2875
|
+
],
|
|
2876
|
+
context,
|
|
2877
|
+
error: specError,
|
|
2878
|
+
next,
|
|
2879
|
+
extraction: { path: 'none' },
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
if (step.parallel_dispatch.mode === 'distributed') {
|
|
2883
|
+
const next = getNextStepPreview(step.id, allSteps, context, runState.runnerVars);
|
|
2884
|
+
const detail = 'parallel_dispatch.mode: distributed is not implemented in @adcp/sdk; use process_local for in-process grading.';
|
|
2885
|
+
return {
|
|
2886
|
+
step_id: step.id,
|
|
2887
|
+
phase_id: phaseId,
|
|
2888
|
+
title: step.title,
|
|
2889
|
+
task: step.task,
|
|
2890
|
+
passed: true,
|
|
2891
|
+
skipped: true,
|
|
2892
|
+
skip_reason: 'not_applicable',
|
|
2893
|
+
skip: buildSkip('not_applicable', detail),
|
|
2894
|
+
duration_ms: 0,
|
|
2895
|
+
validations: [],
|
|
2896
|
+
context,
|
|
2897
|
+
next,
|
|
2898
|
+
extraction: { path: 'none' },
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2528
2902
|
// Capture the ISO timestamp immediately before the step's AdCP request
|
|
2529
2903
|
// dispatch. `upstream_traffic` validations use this as the default
|
|
2530
2904
|
// `since_timestamp` window bound when querying the controller. Recorded
|
|
@@ -2583,40 +2957,92 @@ client, step, phaseId, context, allSteps, options, state) {
|
|
|
2583
2957
|
// lands, key the capture off the negotiated transport instead.
|
|
2584
2958
|
const captureA2a = options.protocol === 'a2a';
|
|
2585
2959
|
let a2aCaptures;
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2960
|
+
if (step.parallel_dispatch) {
|
|
2961
|
+
// Fan out N concurrent dispatches via the SDK client. All dispatches
|
|
2962
|
+
// share the runner-minted idempotency_key (default) so the seller
|
|
2963
|
+
// sees one logical request and resolves the race deterministically.
|
|
2964
|
+
//
|
|
2965
|
+
// Known limitation: `dispatchWithBarrier` uses `Promise.race` against a
|
|
2966
|
+
// timer; when the barrier wins, the underlying SDK request is NOT
|
|
2967
|
+
// aborted — it continues to completion against the seller. The runner
|
|
2968
|
+
// reports the dispatch as `timed_out`, but a late-arriving success
|
|
2969
|
+
// can still land on the seller's idempotency cache. Storyboard
|
|
2970
|
+
// authors writing follow-up steps that observe seller state after a
|
|
2971
|
+
// barrier timeout should account for this race.
|
|
2972
|
+
const started = Date.now();
|
|
2973
|
+
crossResponses = await (0, parallel_dispatch_1.runParallelDispatches)(client, effectiveStep.task, request, {
|
|
2974
|
+
spec: step.parallel_dispatch,
|
|
2975
|
+
keyMinter: idempotency_1.generateIdempotencyKey,
|
|
2976
|
+
correlationPrefix: step.id,
|
|
2977
|
+
});
|
|
2978
|
+
const durationMs = Date.now() - started;
|
|
2979
|
+
// Representative TaskResult is ALWAYS `dispatches[0]` — pinning the
|
|
2980
|
+
// representative to a fixed index keeps the aggregation loop's `i=1`
|
|
2981
|
+
// start aligned (every dispatch graded exactly once). Picking the
|
|
2982
|
+
// "best" resolved arm would double-count whichever dispatch won and
|
|
2983
|
+
// skip dispatches[0] from per-response grading entirely.
|
|
2984
|
+
const firstDispatch = crossResponses.dispatches[0];
|
|
2985
|
+
const allTimedOut = crossResponses.dispatches.every(d => d.timed_out);
|
|
2986
|
+
const barrierTimeoutError = 'parallel_dispatch_barrier_timeout: no dispatch resolved within barrier_timeout_ms';
|
|
2987
|
+
taskResult = firstDispatch?.taskResult ?? {
|
|
2988
|
+
success: false,
|
|
2989
|
+
...(allTimedOut && { error: barrierTimeoutError }),
|
|
2990
|
+
};
|
|
2991
|
+
// Pass/fail is derived from the cross-response set rather than any
|
|
2992
|
+
// single arm: the step passes when at least one dispatch resolved
|
|
2993
|
+
// (cross-response validations then grade the race outcome). All-
|
|
2994
|
+
// timed-out is a hard failure regardless of validations.
|
|
2995
|
+
stepResult = {
|
|
2996
|
+
duration_ms: durationMs,
|
|
2997
|
+
passed: !allTimedOut && crossResponses.resolved.length > 0,
|
|
2998
|
+
...(allTimedOut && { error: barrierTimeoutError }),
|
|
2999
|
+
};
|
|
3000
|
+
if (taskResult) {
|
|
3001
|
+
responseRecord = {
|
|
3002
|
+
transport: options.protocol === 'a2a' ? 'a2a' : 'mcp',
|
|
3003
|
+
payload: (0, redact_secrets_1.redactSecrets)(taskResult.data ?? taskResult.error ?? null),
|
|
3004
|
+
duration_ms: durationMs,
|
|
3005
|
+
};
|
|
2607
3006
|
}
|
|
2608
|
-
});
|
|
2609
|
-
taskResult = run.result;
|
|
2610
|
-
stepResult = run.step;
|
|
2611
|
-
if (captureA2a && a2aCaptures) {
|
|
2612
|
-
a2aEnvelope = parseLastA2aMessageSendCapture(a2aCaptures);
|
|
2613
3007
|
}
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
3008
|
+
else {
|
|
3009
|
+
const dispatch = () => (0, task_map_1.executeStoryboardTask)(client, effectiveStep.task, request, {
|
|
3010
|
+
skipIdempotencyAutoInject: testsMissingIdempotencyKey,
|
|
3011
|
+
skipAccountValidation: testsMissingAccount,
|
|
3012
|
+
});
|
|
3013
|
+
const run = await (0, client_1.runStep)(step.title, effectiveStep.task, async () => {
|
|
3014
|
+
if (!captureA2a)
|
|
3015
|
+
return dispatch();
|
|
3016
|
+
try {
|
|
3017
|
+
const { result: dispatchResult, captures } = await (0, rawResponseCapture_1.withRawResponseCapture)(dispatch);
|
|
3018
|
+
a2aCaptures = captures;
|
|
3019
|
+
return dispatchResult;
|
|
3020
|
+
}
|
|
3021
|
+
catch (err) {
|
|
3022
|
+
// `withRawResponseCapture` attaches partial captures to the
|
|
3023
|
+
// thrown error so we still get the wire-shape envelope when
|
|
3024
|
+
// the SDK threw mid-parse (e.g. agent emitted malformed JSON).
|
|
3025
|
+
// Bare-throw cases (network errors, no captures attached)
|
|
3026
|
+
// leave `a2aCaptures` undefined and the validator self-skips.
|
|
3027
|
+
const partial = (0, rawResponseCapture_1.getCapturesFromError)(err);
|
|
3028
|
+
if (partial)
|
|
3029
|
+
a2aCaptures = partial;
|
|
3030
|
+
throw err;
|
|
3031
|
+
}
|
|
3032
|
+
});
|
|
3033
|
+
taskResult = run.result;
|
|
3034
|
+
stepResult = run.step;
|
|
3035
|
+
caughtError = run.caughtError;
|
|
3036
|
+
if (captureA2a && a2aCaptures) {
|
|
3037
|
+
a2aEnvelope = parseLastA2aMessageSendCapture(a2aCaptures);
|
|
3038
|
+
}
|
|
3039
|
+
if (taskResult) {
|
|
3040
|
+
responseRecord = {
|
|
3041
|
+
transport: options.protocol === 'a2a' ? 'a2a' : 'mcp',
|
|
3042
|
+
payload: (0, redact_secrets_1.redactSecrets)(taskResult.data ?? taskResult.error ?? null),
|
|
3043
|
+
duration_ms: stepResult.duration_ms,
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
2620
3046
|
}
|
|
2621
3047
|
}
|
|
2622
3048
|
const requestRecord = {
|
|
@@ -2662,13 +3088,21 @@ client, step, phaseId, context, allSteps, options, state) {
|
|
|
2662
3088
|
// Step passes when the task fails (returns an error)
|
|
2663
3089
|
passed = !taskResult?.success || !!stepResult.error;
|
|
2664
3090
|
}
|
|
3091
|
+
else if (crossResponses) {
|
|
3092
|
+
// Parallel-dispatch step: pass/fail is driven by the cross-response
|
|
3093
|
+
// set, not the representative arm. The representative is pinned to
|
|
3094
|
+
// `dispatches[0]` (which may itself have failed under the race) so
|
|
3095
|
+
// gating on its `.success` would false-fail steps where later arms
|
|
3096
|
+
// resolved correctly. Validations grade the actual race outcome.
|
|
3097
|
+
passed = stepResult.passed;
|
|
3098
|
+
}
|
|
2665
3099
|
else {
|
|
2666
3100
|
passed = stepResult.passed && (taskResult?.success ?? false);
|
|
2667
3101
|
}
|
|
3102
|
+
let validations = [];
|
|
2668
3103
|
// Run validations. Resolve `$context.<key>` placeholders in `value` and
|
|
2669
3104
|
// `allowed_values` fields so expected values can reference prior steps
|
|
2670
3105
|
// (e.g., replay tests assert `media_buy_id === $context.initial_media_buy_id`).
|
|
2671
|
-
let validations = [];
|
|
2672
3106
|
if (step.validations?.length && (taskResult || httpResult)) {
|
|
2673
3107
|
const resolvedValidations = step.validations.map(v => {
|
|
2674
3108
|
const resolved = { ...v };
|
|
@@ -2701,6 +3135,7 @@ client, step, phaseId, context, allSteps, options, state) {
|
|
|
2701
3135
|
...(a2aEnvelope && { a2aEnvelope }),
|
|
2702
3136
|
...(upstreamTraffic && { upstreamTraffic }),
|
|
2703
3137
|
...(step.sample_request && { storyboardStep: { sample_request: step.sample_request } }),
|
|
3138
|
+
...(crossResponses && { crossResponses }),
|
|
2704
3139
|
...(() => {
|
|
2705
3140
|
// Walk back through the run's captured A2A envelopes and use
|
|
2706
3141
|
// the most recent prior step's envelope as the comparison
|
|
@@ -2722,6 +3157,78 @@ client, step, phaseId, context, allSteps, options, state) {
|
|
|
2722
3157
|
})(),
|
|
2723
3158
|
};
|
|
2724
3159
|
validations = (0, validations_1.runValidations)(resolvedValidations, vctx);
|
|
3160
|
+
// Parallel-dispatch aggregation: per-response checks (`response_schema`,
|
|
3161
|
+
// `field_present`, `error_code`, etc.) declared by the storyboard MUST
|
|
3162
|
+
// grade against every dispatch's resolved response, not just the
|
|
3163
|
+
// representative one. Re-run each non-cross-response validation against
|
|
3164
|
+
// each dispatch's TaskResult and append the additional results so the
|
|
3165
|
+
// step's overall pass/fail reflects the full fan-out. The first run
|
|
3166
|
+
// (above) already covers dispatch[0]; this loop covers dispatches[1..N].
|
|
3167
|
+
if (crossResponses && crossResponses.dispatches.length > 1) {
|
|
3168
|
+
const perResponseValidations = resolvedValidations.filter(v => v.check !== 'cross_response_field_equal' && v.check !== 'cross_response_count_distinct');
|
|
3169
|
+
for (let i = 1; i < crossResponses.dispatches.length; i++) {
|
|
3170
|
+
const d = crossResponses.dispatches[i];
|
|
3171
|
+
if (!d || !d.taskResult)
|
|
3172
|
+
continue;
|
|
3173
|
+
const dispatchCtx = {
|
|
3174
|
+
...vctx,
|
|
3175
|
+
taskResult: d.taskResult,
|
|
3176
|
+
// Per-dispatch responseRecord stays minimal — the redacted payload
|
|
3177
|
+
// captures enough for failure attribution without bloating success.
|
|
3178
|
+
...(responseRecord && {
|
|
3179
|
+
response: {
|
|
3180
|
+
...responseRecord,
|
|
3181
|
+
payload: (0, redact_secrets_1.redactSecrets)(d.taskResult.data ?? d.taskResult.error ?? null),
|
|
3182
|
+
duration_ms: d.duration_ms,
|
|
3183
|
+
},
|
|
3184
|
+
}),
|
|
3185
|
+
};
|
|
3186
|
+
// Drop crossResponses on the per-dispatch context so cross-response
|
|
3187
|
+
// checks don't re-fire here (they already ran once above on the
|
|
3188
|
+
// representative dispatch and produced their single result).
|
|
3189
|
+
delete dispatchCtx.crossResponses;
|
|
3190
|
+
const dispatchResults = (0, validations_1.runValidations)(perResponseValidations, dispatchCtx).map(r => ({
|
|
3191
|
+
...r,
|
|
3192
|
+
description: `[dispatch ${d.correlation_id}] ${r.description}`,
|
|
3193
|
+
}));
|
|
3194
|
+
validations.push(...dispatchResults);
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
// Schema-validation attribution (adcp-client#1709). When the response
|
|
3199
|
+
// unwrapper rejected the agent's response against the SDK's Zod schema
|
|
3200
|
+
// for the tool, it threw a typed `ResponseSchemaValidationError` that
|
|
3201
|
+
// surfaced on `caughtError`. Without this attribution, the rejection
|
|
3202
|
+
// would silently propagate into whichever step-scope invariant (e.g.
|
|
3203
|
+
// `context.no_secret_echo`) happened to fire next — the BidMachine
|
|
3204
|
+
// misdiagnosis trace from adcp-client#1709 / adcp#4419 ate 10+ deploys
|
|
3205
|
+
// chasing the wrong cause.
|
|
3206
|
+
//
|
|
3207
|
+
// Synthesize a canonical `response_schema` ValidationResult and prepend
|
|
3208
|
+
// it so `extractFailures.find(v => !v.passed)` resolves it before any
|
|
3209
|
+
// invariant entry. Step-scope invariants downstream of this point will
|
|
3210
|
+
// short-circuit on the schema-invalid response (see the invariant
|
|
3211
|
+
// dispatch loop in `executeStoryboardPass`).
|
|
3212
|
+
if (caughtError instanceof response_unwrapper_1.ResponseSchemaValidationError) {
|
|
3213
|
+
const issues = caughtError.issues
|
|
3214
|
+
.slice(0, 5)
|
|
3215
|
+
.map(i => `${i.path.join('.') || '(root)'}: ${i.message}`)
|
|
3216
|
+
.join('; ');
|
|
3217
|
+
const firstIssue = caughtError.issues[0];
|
|
3218
|
+
const jsonPointer = firstIssue ? '/' + firstIssue.path.map(s => String(s)).join('/') : null;
|
|
3219
|
+
const synthSchemaResult = {
|
|
3220
|
+
check: 'response_schema',
|
|
3221
|
+
passed: false,
|
|
3222
|
+
description: `Response schema validation for ${caughtError.toolName}`,
|
|
3223
|
+
error: issues,
|
|
3224
|
+
json_pointer: jsonPointer,
|
|
3225
|
+
expected: `response schema for ${caughtError.toolName}`,
|
|
3226
|
+
actual: caughtError.issues,
|
|
3227
|
+
};
|
|
3228
|
+
// Prepend so extractFailures picks it up before any inline validation
|
|
3229
|
+
// entry that may also be failing (e.g. `field_present` checks that
|
|
3230
|
+
// legitimately can't observe their target against an unparsed payload).
|
|
3231
|
+
validations = [synthSchemaResult, ...validations];
|
|
2725
3232
|
}
|
|
2726
3233
|
// Persist the captured A2A envelope keyed by step id so cross-step
|
|
2727
3234
|
// validators (`a2a_context_continuity`) on subsequent steps can
|
|
@@ -2916,6 +3423,7 @@ client, step, phaseId, context, allSteps, options, state) {
|
|
|
2916
3423
|
context_provenance: Object.fromEntries(runState.contextProvenance),
|
|
2917
3424
|
}),
|
|
2918
3425
|
error: step.expect_error ? undefined : truncateError(stepResult.error || taskResult?.error),
|
|
3426
|
+
...(!step.expect_error && taskResult?.adcp_error && { adcp_error: taskResult.adcp_error }),
|
|
2919
3427
|
next,
|
|
2920
3428
|
request: requestRecord,
|
|
2921
3429
|
...(responseRecord && { response_record: responseRecord }),
|
|
@@ -3151,6 +3659,33 @@ function isTaskShape(result) {
|
|
|
3151
3659
|
// ────────────────────────────────────────────────────────────
|
|
3152
3660
|
// Phase / step skip predicates
|
|
3153
3661
|
// ────────────────────────────────────────────────────────────
|
|
3662
|
+
/**
|
|
3663
|
+
* True when the phase contains an OAuth-metadata probe step
|
|
3664
|
+
* (`protected_resource_metadata` or `oauth_auth_server_metadata`). Used
|
|
3665
|
+
* to pre-emptively trigger the existing `phaseAbsent` cascade when the
|
|
3666
|
+
* agent's capabilities never advertised OAuth in the first place, so we
|
|
3667
|
+
* don't burn step failures probing a well-known path the agent doesn't
|
|
3668
|
+
* claim. Spec: adcp-client#1702.
|
|
3669
|
+
*/
|
|
3670
|
+
function phaseContainsOauthMetadataProbe(phase) {
|
|
3671
|
+
return phase.steps.some(s => s.task === 'protected_resource_metadata' || s.task === 'oauth_auth_server_metadata');
|
|
3672
|
+
}
|
|
3673
|
+
/**
|
|
3674
|
+
* True when the agent's `get_adcp_capabilities` response declares an
|
|
3675
|
+
* OAuth issuer (today: `account.authorization_endpoint`). When the
|
|
3676
|
+
* profile or its capabilities aren't available — e.g. external
|
|
3677
|
+
* `_client` mode — we conservatively return `true` so the runner falls
|
|
3678
|
+
* through to the existing reactive cascade (`phaseAbsent` flips on a
|
|
3679
|
+
* 404 from the PRM probe). That keeps backward compatibility for
|
|
3680
|
+
* callers that haven't threaded a profile through. Spec: adcp-client#1702.
|
|
3681
|
+
*/
|
|
3682
|
+
function agentAdvertisesOauth(profile) {
|
|
3683
|
+
const rawCaps = profile?.raw_capabilities;
|
|
3684
|
+
if (rawCaps === undefined)
|
|
3685
|
+
return true;
|
|
3686
|
+
const endpoint = resolveCapabilityPath(rawCaps, 'account.authorization_endpoint');
|
|
3687
|
+
return typeof endpoint === 'string' && endpoint.length > 0;
|
|
3688
|
+
}
|
|
3154
3689
|
/**
|
|
3155
3690
|
* Evaluate a phase's `skip_if` expression against the runtime options. Only
|
|
3156
3691
|
* a tiny grammar is supported today; unknown expressions fail closed (phase runs).
|
|
@@ -3333,7 +3868,7 @@ function evalContributesIf(expr, priorStepResults) {
|
|
|
3333
3868
|
* the function fails open and injects as before. Schema checks use raw JSON
|
|
3334
3869
|
* reads, not AJV internals.
|
|
3335
3870
|
*/
|
|
3336
|
-
function applyBrandInvariant(request, options, taskName) {
|
|
3871
|
+
function applyBrandInvariant(request, options, taskName, stepFlags) {
|
|
3337
3872
|
// Only force the invariant when the caller has actually supplied a brand.
|
|
3338
3873
|
// Storyboards that don't exercise brand-scoped tools (e.g. security
|
|
3339
3874
|
// probes) legitimately run without one and should pass through unchanged.
|
|
@@ -3349,6 +3884,13 @@ function applyBrandInvariant(request, options, taskName) {
|
|
|
3349
3884
|
const result = { ...request };
|
|
3350
3885
|
if (topBrandOk)
|
|
3351
3886
|
result.brand = brand;
|
|
3887
|
+
// When a storyboard step sets `omit_account: true` it is deliberately
|
|
3888
|
+
// testing the seller's missing-account rejection path. Skip all account
|
|
3889
|
+
// synthesis — both the natural-key-merge branch (existing account on the
|
|
3890
|
+
// request) and the synthetic-construction branch (no account on the
|
|
3891
|
+
// request) — so the request reaches the wire exactly as authored.
|
|
3892
|
+
if (stepFlags?.omit_account)
|
|
3893
|
+
return result;
|
|
3352
3894
|
if ('account' in request) {
|
|
3353
3895
|
// Caller sent an account — merge brand in only when it's a plain object
|
|
3354
3896
|
// using AccountReference's natural-key variant (`{brand, operator, sandbox?}`).
|