@adcp/sdk 7.0.0 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/bin/adcp-config.js +10 -1
  2. package/bin/adcp.js +424 -22
  3. package/dist/lib/auth/oauth/authorization-required.d.ts +17 -0
  4. package/dist/lib/auth/oauth/authorization-required.d.ts.map +1 -1
  5. package/dist/lib/auth/oauth/authorization-required.js +20 -0
  6. package/dist/lib/auth/oauth/authorization-required.js.map +1 -1
  7. package/dist/lib/auth/oauth/index.d.ts +1 -1
  8. package/dist/lib/auth/oauth/index.d.ts.map +1 -1
  9. package/dist/lib/auth/oauth/index.js +2 -1
  10. package/dist/lib/auth/oauth/index.js.map +1 -1
  11. package/dist/lib/core/AgentClient.d.ts.map +1 -1
  12. package/dist/lib/core/SingleAgentClient.d.ts.map +1 -1
  13. package/dist/lib/core/SingleAgentClient.js +13 -2
  14. package/dist/lib/core/SingleAgentClient.js.map +1 -1
  15. package/dist/lib/discovery/property-crawler.d.ts +13 -1
  16. package/dist/lib/discovery/property-crawler.d.ts.map +1 -1
  17. package/dist/lib/discovery/property-crawler.js +40 -14
  18. package/dist/lib/discovery/property-crawler.js.map +1 -1
  19. package/dist/lib/discovery/resolve-agent-properties.d.ts +103 -0
  20. package/dist/lib/discovery/resolve-agent-properties.d.ts.map +1 -0
  21. package/dist/lib/discovery/resolve-agent-properties.js +182 -0
  22. package/dist/lib/discovery/resolve-agent-properties.js.map +1 -0
  23. package/dist/lib/discovery/types.d.ts +41 -2
  24. package/dist/lib/discovery/types.d.ts.map +1 -1
  25. package/dist/lib/discovery/types.js +2 -1
  26. package/dist/lib/discovery/types.js.map +1 -1
  27. package/dist/lib/discovery/validate-adagents.d.ts +114 -0
  28. package/dist/lib/discovery/validate-adagents.d.ts.map +1 -0
  29. package/dist/lib/discovery/validate-adagents.js +417 -0
  30. package/dist/lib/discovery/validate-adagents.js.map +1 -0
  31. package/dist/lib/errors/index.d.ts +42 -5
  32. package/dist/lib/errors/index.d.ts.map +1 -1
  33. package/dist/lib/errors/index.js +64 -9
  34. package/dist/lib/errors/index.js.map +1 -1
  35. package/dist/lib/index.d.ts +3 -1
  36. package/dist/lib/index.d.ts.map +1 -1
  37. package/dist/lib/index.js +17 -10
  38. package/dist/lib/index.js.map +1 -1
  39. package/dist/lib/protocols/a2a.d.ts.map +1 -1
  40. package/dist/lib/protocols/a2a.js +70 -11
  41. package/dist/lib/protocols/a2a.js.map +1 -1
  42. package/dist/lib/protocols/mcp.d.ts.map +1 -1
  43. package/dist/lib/protocols/mcp.js +61 -5
  44. package/dist/lib/protocols/mcp.js.map +1 -1
  45. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  46. package/dist/lib/signing/fetch-async.d.ts.map +1 -1
  47. package/dist/lib/signing/fetch-async.js +5 -0
  48. package/dist/lib/signing/fetch-async.js.map +1 -1
  49. package/dist/lib/signing/fetch.d.ts.map +1 -1
  50. package/dist/lib/signing/fetch.js +11 -0
  51. package/dist/lib/signing/fetch.js.map +1 -1
  52. package/dist/lib/testing/client.d.ts +16 -2
  53. package/dist/lib/testing/client.d.ts.map +1 -1
  54. package/dist/lib/testing/client.js +1 -0
  55. package/dist/lib/testing/client.js.map +1 -1
  56. package/dist/lib/testing/compliance/comply.d.ts +17 -2
  57. package/dist/lib/testing/compliance/comply.d.ts.map +1 -1
  58. package/dist/lib/testing/compliance/comply.js +38 -0
  59. package/dist/lib/testing/compliance/comply.js.map +1 -1
  60. package/dist/lib/testing/compliance/spec-conformance.d.ts.map +1 -1
  61. package/dist/lib/testing/compliance/spec-conformance.js +1 -0
  62. package/dist/lib/testing/compliance/spec-conformance.js.map +1 -1
  63. package/dist/lib/testing/compliance/types.d.ts +11 -0
  64. package/dist/lib/testing/compliance/types.d.ts.map +1 -1
  65. package/dist/lib/testing/index.d.ts +1 -1
  66. package/dist/lib/testing/index.d.ts.map +1 -1
  67. package/dist/lib/testing/index.js.map +1 -1
  68. package/dist/lib/testing/storyboard/default-invariants.js +21 -2
  69. package/dist/lib/testing/storyboard/default-invariants.js.map +1 -1
  70. package/dist/lib/testing/storyboard/index.d.ts +1 -1
  71. package/dist/lib/testing/storyboard/index.d.ts.map +1 -1
  72. package/dist/lib/testing/storyboard/index.js.map +1 -1
  73. package/dist/lib/testing/storyboard/runner.d.ts.map +1 -1
  74. package/dist/lib/testing/storyboard/runner.js +284 -16
  75. package/dist/lib/testing/storyboard/runner.js.map +1 -1
  76. package/dist/lib/testing/storyboard/types.d.ts +112 -1
  77. package/dist/lib/testing/storyboard/types.d.ts.map +1 -1
  78. package/dist/lib/testing/storyboard/types.js +1 -0
  79. package/dist/lib/testing/storyboard/types.js.map +1 -1
  80. package/dist/lib/utils/response-unwrapper.d.ts +33 -0
  81. package/dist/lib/utils/response-unwrapper.d.ts.map +1 -1
  82. package/dist/lib/utils/response-unwrapper.js +41 -3
  83. package/dist/lib/utils/response-unwrapper.js.map +1 -1
  84. package/dist/lib/version.d.ts +10 -6
  85. package/dist/lib/version.d.ts.map +1 -1
  86. package/dist/lib/version.js +11 -4
  87. package/dist/lib/version.js.map +1 -1
  88. package/docs/llms.txt +10 -2
  89. package/package.json +1 -1
  90. package/skills/call-adcp-agent/SKILL.md +1 -1
@@ -32,6 +32,7 @@ const validations_1 = require("./validations");
32
32
  const parallel_dispatch_1 = require("./parallel-dispatch");
33
33
  const path_1 = require("./path");
34
34
  const redact_secrets_1 = require("../../utils/redact-secrets");
35
+ const response_unwrapper_1 = require("../../utils/response-unwrapper");
35
36
  const test_controller_1 = require("../test-controller");
36
37
  const request_builder_1 = require("./request-builder");
37
38
  const client_2 = require("../client");
@@ -681,6 +682,7 @@ function buildCapabilityUnsupportedResult(agentUrls, storyboard, detail) {
681
682
  strict_only_failures: 0,
682
683
  lenient_also_failed: 0,
683
684
  },
685
+ notices: [],
684
686
  };
685
687
  }
686
688
  /**
@@ -701,6 +703,7 @@ const REQUIREMENT_TO_SKIP_REASON = {
701
703
  seeded_state: 'requirement_unmet',
702
704
  real_wire: 'requirement_unmet',
703
705
  webhook_receiver: 'requirement_unmet',
706
+ request_signer: 'not_applicable',
704
707
  };
705
708
  /**
706
709
  * Build a minimal StoryboardResult for a storyboard skipped because a
@@ -756,6 +759,7 @@ function buildRequirementUnmetResult(agentUrls, storyboard, requirement, detail)
756
759
  strict_only_failures: 0,
757
760
  lenient_also_failed: 0,
758
761
  },
762
+ notices: [],
759
763
  };
760
764
  }
761
765
  /**
@@ -777,8 +781,21 @@ function buildRequirementUnmetResult(agentUrls, storyboard, requirement, detail)
777
781
  * Autodetected from step `sample_request` token presence (see
778
782
  * `detectImplicitRequires`); authors don't write this tag manually.
779
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.
780
797
  */
781
- function checkRequires(requires, options) {
798
+ function checkRequires(requires, options, profile) {
782
799
  for (const requirement of requires) {
783
800
  switch (requirement) {
784
801
  case 'controller': {
@@ -820,6 +837,31 @@ function checkRequires(requires, options) {
820
837
  }
821
838
  break;
822
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
+ }
823
865
  }
824
866
  }
825
867
  return null;
@@ -854,23 +896,45 @@ function valueContainsWebhookToken(value) {
854
896
  }
855
897
  /**
856
898
  * Return the implicit requirements a storyboard needs based on its
857
- * structure (not its declared `requires:` list). Today: only
858
- * `'webhook_receiver'`, autodetected from `{{runner.webhook_url:…}}` /
859
- * `{{runner.webhook_base}}` token presence inside any step's
860
- * `sample_request`. Token presence is the authoring contract — a
861
- * storyboard that names the runner's webhook receiver cannot run
862
- * without one — so authors don't need to remember to add
863
- * `requires: [webhook_receiver]` separately. Spec: adcp-client#1678.
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.
864
914
  */
865
915
  function detectImplicitRequires(storyboard) {
916
+ const requires = [];
917
+ let needsWebhook = false;
918
+ let needsSigner = storyboard.id === 'signed_requests';
866
919
  for (const phase of storyboard.phases) {
867
920
  for (const step of phase.steps) {
868
- if (step.sample_request && valueContainsWebhookToken(step.sample_request)) {
869
- return ['webhook_receiver'];
921
+ if (!needsWebhook && step.sample_request && valueContainsWebhookToken(step.sample_request)) {
922
+ needsWebhook = true;
870
923
  }
924
+ if (!needsSigner && step.task === 'request_signing_probe') {
925
+ needsSigner = true;
926
+ }
927
+ if (needsWebhook && needsSigner)
928
+ break;
871
929
  }
930
+ if (needsWebhook && needsSigner)
931
+ break;
872
932
  }
873
- return [];
933
+ if (needsWebhook)
934
+ requires.push('webhook_receiver');
935
+ if (needsSigner)
936
+ requires.push('request_signer');
937
+ return requires;
874
938
  }
875
939
  /**
876
940
  * Build a hard-failure StoryboardResult for when agent capability
@@ -927,6 +991,7 @@ function buildDiscoveryFailedResult(agentUrls, storyboard, discoveryStep) {
927
991
  strict_only_failures: 0,
928
992
  lenient_also_failed: 0,
929
993
  },
994
+ notices: [],
930
995
  };
931
996
  }
932
997
  /**
@@ -984,8 +1049,85 @@ function buildRequiredToolsMissingResult(agentUrls, storyboard, detail) {
984
1049
  strict_only_failures: 0,
985
1050
  lenient_also_failed: 0,
986
1051
  },
1052
+ notices: [],
987
1053
  };
988
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
+ }
989
1131
  /**
990
1132
  * Execute a single pass of the storyboard against the supplied replica URLs
991
1133
  * using round-robin dispatch starting at `dispatchOffset`. Called directly
@@ -1108,15 +1250,24 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
1108
1250
  // implicit gate, storyboards that name the receiver but run without
1109
1251
  // one would silently ship literal mustache tokens on the wire and
1110
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);
1111
1259
  const declared = storyboard.requires ?? [];
1112
1260
  const implicit = detectImplicitRequires(storyboard);
1113
1261
  const allRequires = [...declared, ...implicit.filter(r => !declared.includes(r))];
1114
1262
  if (allRequires.length) {
1115
- const unmet = checkRequires(allRequires, options);
1263
+ const unmet = checkRequires(allRequires, options, profile);
1116
1264
  if (unmet) {
1117
1265
  if (!options._client)
1118
1266
  await (0, protocols_1.closeConnections)(options.protocol);
1119
- return buildRequirementUnmetResult(agentUrls, storyboard, unmet.requirement, unmet.detail);
1267
+ return {
1268
+ ...buildRequirementUnmetResult(agentUrls, storyboard, unmet.requirement, unmet.detail),
1269
+ notices: preflightNotices,
1270
+ };
1120
1271
  }
1121
1272
  }
1122
1273
  // Evaluate requires_capability predicate before any phase setup.
@@ -1142,7 +1293,7 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
1142
1293
  `agent declared ${JSON.stringify(actual)}.`;
1143
1294
  if (!options._client)
1144
1295
  await (0, protocols_1.closeConnections)(options.protocol);
1145
- return buildCapabilityUnsupportedResult(agentUrls, storyboard, detail);
1296
+ return { ...buildCapabilityUnsupportedResult(agentUrls, storyboard, detail), notices: preflightNotices };
1146
1297
  }
1147
1298
  }
1148
1299
  }
@@ -1162,7 +1313,10 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
1162
1313
  if (!hasAnyRequired) {
1163
1314
  if (!options._client)
1164
1315
  await (0, protocols_1.closeConnections)(options.protocol);
1165
- return buildRequiredToolsMissingResult(agentUrls, storyboard, `agent does not advertise any of [${storyboard.required_tools.join(', ')}]`);
1316
+ return {
1317
+ ...buildRequiredToolsMissingResult(agentUrls, storyboard, `agent does not advertise any of [${storyboard.required_tools.join(', ')}]`),
1318
+ notices: preflightNotices,
1319
+ };
1166
1320
  }
1167
1321
  }
1168
1322
  let context = { ...options.context };
@@ -1447,6 +1601,18 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
1447
1601
  });
1448
1602
  continue;
1449
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
+ }
1450
1616
  // Reset alias cache at this phase boundary (#1657). $generate:uuid_v4#alias
1451
1617
  // is designed to be stable within a scenario — the initial call and its
1452
1618
  // idempotency replay share the same UUID — but aliases must NOT bleed across
@@ -1650,6 +1816,20 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
1650
1816
  }
1651
1817
  stepResults.push(result);
1652
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);
1653
1833
  // Fire per-step assertions. Each result is appended to the step's
1654
1834
  // `validations[]` under `check: "assertion"` so existing UI renders
1655
1835
  // them alongside inline checks, and mirrored into `assertionResults`
@@ -1664,6 +1844,18 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
1664
1844
  // behavior the invariant would flag (validated at runner start).
1665
1845
  if ((0, assertions_1.stepDisablesAssertion)(step.invariants, spec.id))
1666
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
+ }
1667
1859
  const raw = await spec.onStep(assertionContexts.get(spec.id), result);
1668
1860
  for (const r of raw) {
1669
1861
  const full = { ...r, assertion_id: spec.id, scope: 'step', step_id: step.id };
@@ -2025,6 +2217,10 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2025
2217
  const schemasUsed = collectSchemasUsed(phaseResults);
2026
2218
  const strictSummary = summarizeStrictValidation(phaseResults);
2027
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);
2028
2224
  const result = {
2029
2225
  storyboard_id: storyboard.id,
2030
2226
  storyboard_title: storyboard.title,
@@ -2047,6 +2243,7 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
2047
2243
  ...(schemasUsed.length > 0 ? { schemas_used: schemasUsed } : {}),
2048
2244
  ...(assertionResults.length > 0 ? { assertions: assertionResults } : {}),
2049
2245
  strict_validation_summary: strictSummary,
2246
+ notices,
2050
2247
  };
2051
2248
  // Close protocol connections when the runner created its own client. The
2052
2249
  // connection pool is keyed by URL+auth, so a single closeConnections() call
@@ -2123,6 +2320,8 @@ async function runMultiPass(agentUrls, storyboard, options) {
2123
2320
  // readers see a per-pass timeline; de-duplicating would hide a real
2124
2321
  // "passed on pass 1, failed on pass 2" divergence.
2125
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()];
2126
2325
  return {
2127
2326
  storyboard_id: storyboard.id,
2128
2327
  storyboard_title: storyboard.title,
@@ -2141,6 +2340,7 @@ async function runMultiPass(agentUrls, storyboard, options) {
2141
2340
  tested_at: new Date().toISOString(),
2142
2341
  ...(schemasDedup.length > 0 ? { schemas_used: schemasDedup } : {}),
2143
2342
  ...(assertionsAgg.length > 0 ? { assertions: assertionsAgg } : {}),
2343
+ notices: noticesDedup,
2144
2344
  };
2145
2345
  }
2146
2346
  /**
@@ -2614,6 +2814,11 @@ client, step, phaseId, context, allSteps, options, state) {
2614
2814
  const useRawProbe = rawProbeHeaders !== undefined;
2615
2815
  let taskResult;
2616
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;
2617
2822
  let httpResult;
2618
2823
  let responseRecord;
2619
2824
  let a2aEnvelope;
@@ -2827,6 +3032,7 @@ client, step, phaseId, context, allSteps, options, state) {
2827
3032
  });
2828
3033
  taskResult = run.result;
2829
3034
  stepResult = run.step;
3035
+ caughtError = run.caughtError;
2830
3036
  if (captureA2a && a2aCaptures) {
2831
3037
  a2aEnvelope = parseLastA2aMessageSendCapture(a2aCaptures);
2832
3038
  }
@@ -2893,10 +3099,10 @@ client, step, phaseId, context, allSteps, options, state) {
2893
3099
  else {
2894
3100
  passed = stepResult.passed && (taskResult?.success ?? false);
2895
3101
  }
3102
+ let validations = [];
2896
3103
  // Run validations. Resolve `$context.<key>` placeholders in `value` and
2897
3104
  // `allowed_values` fields so expected values can reference prior steps
2898
3105
  // (e.g., replay tests assert `media_buy_id === $context.initial_media_buy_id`).
2899
- let validations = [];
2900
3106
  if (step.validations?.length && (taskResult || httpResult)) {
2901
3107
  const resolvedValidations = step.validations.map(v => {
2902
3108
  const resolved = { ...v };
@@ -2989,6 +3195,41 @@ client, step, phaseId, context, allSteps, options, state) {
2989
3195
  }
2990
3196
  }
2991
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];
3232
+ }
2992
3233
  // Persist the captured A2A envelope keyed by step id so cross-step
2993
3234
  // validators (`a2a_context_continuity`) on subsequent steps can
2994
3235
  // compare against it. Only fires when this step actually captured
@@ -3418,6 +3659,33 @@ function isTaskShape(result) {
3418
3659
  // ────────────────────────────────────────────────────────────
3419
3660
  // Phase / step skip predicates
3420
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
+ }
3421
3689
  /**
3422
3690
  * Evaluate a phase's `skip_if` expression against the runtime options. Only
3423
3691
  * a tiny grammar is supported today; unknown expressions fail closed (phase runs).