@adcp/sdk 7.1.0 → 7.3.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 (85) hide show
  1. package/bin/adcp-config.js +10 -1
  2. package/bin/adcp.js +376 -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/server/idempotency/store.d.ts +6 -5
  47. package/dist/lib/server/idempotency/store.d.ts.map +1 -1
  48. package/dist/lib/server/idempotency/store.js +11 -14
  49. package/dist/lib/server/idempotency/store.js.map +1 -1
  50. package/dist/lib/signing/fetch-async.d.ts.map +1 -1
  51. package/dist/lib/signing/fetch-async.js +5 -0
  52. package/dist/lib/signing/fetch-async.js.map +1 -1
  53. package/dist/lib/signing/fetch.d.ts.map +1 -1
  54. package/dist/lib/signing/fetch.js +11 -0
  55. package/dist/lib/signing/fetch.js.map +1 -1
  56. package/dist/lib/testing/client.d.ts +8 -0
  57. package/dist/lib/testing/client.d.ts.map +1 -1
  58. package/dist/lib/testing/client.js +73 -2
  59. package/dist/lib/testing/client.js.map +1 -1
  60. package/dist/lib/testing/compliance/comply.d.ts +27 -3
  61. package/dist/lib/testing/compliance/comply.d.ts.map +1 -1
  62. package/dist/lib/testing/compliance/comply.js +21 -31
  63. package/dist/lib/testing/compliance/comply.js.map +1 -1
  64. package/dist/lib/testing/compliance/index.d.ts +1 -1
  65. package/dist/lib/testing/compliance/index.d.ts.map +1 -1
  66. package/dist/lib/testing/compliance/index.js +2 -1
  67. package/dist/lib/testing/compliance/index.js.map +1 -1
  68. package/dist/lib/testing/scenarios/error-compliance.d.ts.map +1 -1
  69. package/dist/lib/testing/scenarios/error-compliance.js +11 -1
  70. package/dist/lib/testing/scenarios/error-compliance.js.map +1 -1
  71. package/dist/lib/testing/storyboard/parallel-dispatch.d.ts.map +1 -1
  72. package/dist/lib/testing/storyboard/parallel-dispatch.js +6 -4
  73. package/dist/lib/testing/storyboard/parallel-dispatch.js.map +1 -1
  74. package/dist/lib/testing/storyboard/validations.d.ts.map +1 -1
  75. package/dist/lib/testing/storyboard/validations.js +18 -7
  76. package/dist/lib/testing/storyboard/validations.js.map +1 -1
  77. package/dist/lib/types/index.d.ts +2 -0
  78. package/dist/lib/types/index.d.ts.map +1 -1
  79. package/dist/lib/types/index.js +4 -0
  80. package/dist/lib/types/index.js.map +1 -1
  81. package/dist/lib/version.d.ts +3 -3
  82. package/dist/lib/version.js +3 -3
  83. package/docs/llms.txt +10 -2
  84. package/package.json +2 -1
  85. package/skills/call-adcp-agent/SKILL.md +1 -1
package/bin/adcp.js CHANGED
@@ -266,6 +266,163 @@ function mergeHeaders(savedHeaders, cliHeaders) {
266
266
  return Object.keys(filtered).length > 0 ? filtered : undefined;
267
267
  }
268
268
 
269
+ /**
270
+ * Parse `--auth-scheme bearer|basic`. Defaults to `bearer` (preserving the
271
+ * pre-existing CLI contract where `--auth TOKEN` is always a bearer). When
272
+ * the flag is absent, falls back to the `ADCP_AUTH_SCHEME` env var so CI
273
+ * jobs can flip the scheme without rewriting their command line. Returns
274
+ * `null` when neither the flag nor the env var supplied a value — callers
275
+ * use that to defer to a saved alias's `auth_scheme`.
276
+ */
277
+ function parseAuthSchemeFlag(args) {
278
+ let value = null;
279
+ let source = null;
280
+ // Long-form: --auth-scheme VALUE
281
+ const longIdx = args.indexOf('--auth-scheme');
282
+ if (longIdx !== -1) {
283
+ if (longIdx + 1 >= args.length || args[longIdx + 1].startsWith('--')) {
284
+ console.error('ERROR: --auth-scheme requires a value (bearer|basic)\n');
285
+ process.exit(2);
286
+ }
287
+ value = args[longIdx + 1];
288
+ source = 'flag';
289
+ }
290
+ // Single-token form: --auth-scheme=VALUE. Security-reviewer L3 from PR #1719
291
+ // flagged that the long-form path treats `--auth-scheme=basic` (no space) as
292
+ // an unknown arg, which silently falls through to env-var lookup. Match the
293
+ // other flags' equals-form by checking for the literal prefix explicitly.
294
+ if (value === null) {
295
+ const eqArg = args.find(a => a.startsWith('--auth-scheme='));
296
+ if (eqArg) {
297
+ value = eqArg.slice('--auth-scheme='.length);
298
+ source = 'flag';
299
+ }
300
+ }
301
+ if (value === null && process.env.ADCP_AUTH_SCHEME) {
302
+ value = process.env.ADCP_AUTH_SCHEME;
303
+ source = 'env';
304
+ }
305
+ if (value === null) return null;
306
+ if (value !== 'bearer' && value !== 'basic') {
307
+ // Name the source in the error so the operator knows whether to fix the
308
+ // flag invocation or the environment variable.
309
+ const sourceLabel = source === 'env' ? 'ADCP_AUTH_SCHEME env var' : '--auth-scheme';
310
+ console.error(`ERROR: ${sourceLabel} must be 'bearer' or 'basic', got: ${value}\n`);
311
+ process.exit(2);
312
+ }
313
+ return value;
314
+ }
315
+
316
+ /**
317
+ * Warn when `ADCP_AUTH_SCHEME` was set in the environment but the resolved
318
+ * scheme didn't end up applied (because no token / no credential resolved to
319
+ * the request). The inverse case (token without scheme → silent bearer) is
320
+ * the safe direction and gets no warning.
321
+ *
322
+ * Called once per top-level invocation, after auth resolution completes.
323
+ * The check is purely advisory; it doesn't change behavior, just surfaces a
324
+ * likely misconfiguration before the user wonders why their Basic gateway
325
+ * keeps 401ing.
326
+ */
327
+ function maybeWarnAuthSchemeIneffective(resolvedAuthToken, resolvedAuthScheme) {
328
+ const envScheme = process.env.ADCP_AUTH_SCHEME;
329
+ if (!envScheme) return;
330
+ if (envScheme !== 'bearer' && envScheme !== 'basic') return; // parse error already exited
331
+ // Effective: a token resolved AND the resolved scheme matches what the env
332
+ // asked for. If we have no token, basic is meaningless; if we have a token
333
+ // but the scheme resolved differently (e.g. saved alias overrode), the env
334
+ // is no-op-shadowed and worth flagging.
335
+ const effective = !!resolvedAuthToken && resolvedAuthScheme === envScheme;
336
+ if (!effective) {
337
+ console.error(
338
+ `Warning: ADCP_AUTH_SCHEME=${envScheme} is set but did not apply ` +
339
+ `(${!resolvedAuthToken ? 'no --auth / saved auth_token resolved' : `resolved scheme is '${resolvedAuthScheme}'`}). ` +
340
+ `Pass --auth (or an alias with credentials) to use the env-supplied scheme.`
341
+ );
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Split a `user:pass` credential string and validate per RFC 7617:
347
+ * - userid must not contain `:` (a colon would decode ambiguously on the
348
+ * server, and undici/curl would mis-parse the cached header).
349
+ * - CR, LF, and non-printable ASCII are rejected — these are the same
350
+ * header-smuggling shapes parseHeaderFlags refuses on `-H` input.
351
+ *
352
+ * Returns `{ username, password }`. Exits 2 with a stderr message on any
353
+ * validation failure so the failure mode matches the rest of the CLI's
354
+ * argument validation (no half-encoded header reaches the wire).
355
+ */
356
+ function decodeBasicCredentials(userpass, source = '--auth') {
357
+ if (typeof userpass !== 'string' || userpass.length === 0) {
358
+ console.error(`ERROR: ${source} value for basic auth must be 'user:pass' (got empty value)\n`);
359
+ process.exit(2);
360
+ }
361
+ const colon = userpass.indexOf(':');
362
+ if (colon === -1) {
363
+ // Don't echo the credential itself — even a length leak is enough to help
364
+ // someone reasoning about the value off-screen. The fix is the same
365
+ // regardless of what they passed in.
366
+ console.error(`ERROR: ${source} basic-auth credential must be in 'user:pass' form (no ':' found)\n`);
367
+ process.exit(2);
368
+ }
369
+ const username = userpass.slice(0, colon);
370
+ const password = userpass.slice(colon + 1);
371
+ if (username.length === 0) {
372
+ console.error(`ERROR: ${source} basic auth username must not be empty\n`);
373
+ process.exit(2);
374
+ }
375
+ if (username.includes(':')) {
376
+ // Impossible from a single-colon split, but kept as a defensive guard
377
+ // in case a future refactor changes the split logic.
378
+ console.error(`ERROR: ${source} basic auth username must not contain ':' (RFC 7617)\n`);
379
+ process.exit(2);
380
+ }
381
+ const badChar = /[\r\n\0]|[^\x20-\x7E]/;
382
+ if (badChar.test(username)) {
383
+ console.error(`ERROR: ${source} basic auth username contains CR, LF, NUL, or non-printable ASCII\n`);
384
+ process.exit(2);
385
+ }
386
+ if (badChar.test(password)) {
387
+ console.error(`ERROR: ${source} basic auth password contains CR, LF, NUL, or non-printable ASCII\n`);
388
+ process.exit(2);
389
+ }
390
+ return { username, password };
391
+ }
392
+
393
+ /**
394
+ * Encode RFC 7617 `Authorization: Basic <base64(user:pass)>` from a validated
395
+ * `{username, password}` pair. Caller MUST have run `decodeBasicCredentials`
396
+ * first — this helper does not re-validate.
397
+ */
398
+ function encodeBasicAuthHeader({ username, password }) {
399
+ return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
400
+ }
401
+
402
+ /**
403
+ * Inject `Authorization: Basic <base64(user:pass)>` into the header map the
404
+ * CLI hands to the SDK. Called AFTER `mergeHeaders()` runs so the
405
+ * reserved-key filter doesn't strip the injected Authorization. The protocol
406
+ * layer (`src/lib/protocols/mcp.ts` and `protocols/a2a.ts`) spreads
407
+ * `customHeaders` BEFORE SDK-supplied Authorization, so this injected value
408
+ * reaches the wire intact only when the caller has suppressed `auth_token`
409
+ * (the bearer-path overrides Authorization in that case).
410
+ *
411
+ * **Invariant for future contributors**: this MUST be called after
412
+ * `mergeHeaders()` and the bearer-path `auth_token` must be omitted when
413
+ * `useBasicAuth` is true. Moving the injection inside `mergeHeaders()` would
414
+ * silently drop the header (reserved-key filter is case-insensitive on
415
+ * `authorization`). The test at
416
+ * `test/lib/cli-auth-scheme.test.js:'wire test'` is the regression guard —
417
+ * if a refactor moves the injection wrong, that test stops sending Basic.
418
+ *
419
+ * Returns the merged header bag (always defined, even if empty in).
420
+ */
421
+ function injectBasicAuthHeader(mergedHeaders, userpass, source = '--auth') {
422
+ const { username, password } = decodeBasicCredentials(userpass, source);
423
+ return { ...(mergedHeaders || {}), Authorization: encodeBasicAuthHeader({ username, password }) };
424
+ }
425
+
269
426
  /**
270
427
  * Extract human-readable protocol message from conversation
271
428
  */
@@ -439,6 +596,10 @@ async function handleTestCommand(args) {
439
596
  }
440
597
  authToken = args[authIndex + 1];
441
598
  }
599
+ // `--auth-scheme bearer|basic` selects how `--auth` (or the saved alias's
600
+ // auth_token) is sent. Default null → defer to saved alias's `auth_scheme`,
601
+ // then fall back to `bearer`.
602
+ const authScheme = parseAuthSchemeFlag(args);
442
603
 
443
604
  // adcp-client#1612: accept `--transport` as an alias for `--protocol`.
444
605
  // The original symptom report used `--transport a2a` (per A2A SDK
@@ -476,7 +637,7 @@ async function handleTestCommand(args) {
476
637
 
477
638
  // Filter out flag arguments to find positional arguments
478
639
  const positionalArgs = args.filter(
479
- arg => !arg.startsWith('--') && arg !== authToken && arg !== protocolFlag && arg !== brief
640
+ arg => !arg.startsWith('--') && arg !== authToken && arg !== authScheme && arg !== protocolFlag && arg !== brief
480
641
  );
481
642
 
482
643
  if (positionalArgs.length === 0) {
@@ -507,6 +668,7 @@ async function handleTestCommand(args) {
507
668
  let agentUrl;
508
669
  let protocol = protocolFlag;
509
670
  let finalAuthToken = authToken;
671
+ let finalAuthScheme = authScheme; // null = defer to alias's saved scheme
510
672
  let oauthTokens = null;
511
673
 
512
674
  // Resolve agent
@@ -520,6 +682,9 @@ async function handleTestCommand(args) {
520
682
  agentUrl = savedAgent.url;
521
683
  protocol = protocol || savedAgent.protocol;
522
684
  finalAuthToken = finalAuthToken || getEffectiveAuthToken(savedAgent);
685
+ if (finalAuthScheme === null && savedAgent.auth_scheme) {
686
+ finalAuthScheme = savedAgent.auth_scheme;
687
+ }
523
688
  if (savedAgent.oauth_client_credentials) {
524
689
  // Client credentials: tokens are refreshed inside `ProtocolClient.callTool`
525
690
  // via `ensureClientCredentialsTokens`. Surface cached tokens so the call
@@ -583,11 +748,12 @@ async function handleTestCommand(args) {
583
748
  }
584
749
  }
585
750
 
586
- // Build test options
751
+ // Build test options. `buildResolvedAuthOption` handles the bearer/basic
752
+ // split based on the resolved scheme (default: bearer).
587
753
  const testOptions = {
588
754
  protocol,
589
755
  brief,
590
- ...(finalAuthToken && { auth: { type: 'bearer', token: finalAuthToken } }),
756
+ ...buildResolvedAuthOption({ resolvedAuth: finalAuthToken, resolvedAuthScheme: finalAuthScheme || 'bearer' }),
591
757
  };
592
758
 
593
759
  if (!jsonOutput) {
@@ -713,6 +879,9 @@ function parseAgentOptions(args) {
713
879
  if (authIndex !== -1 && authIndex + 1 < args.length && !args[authIndex + 1].startsWith('--')) {
714
880
  authToken = args[authIndex + 1];
715
881
  }
882
+ // `--auth-scheme bearer|basic` selects the scheme used to send `--auth`.
883
+ // Default (null) defers to the saved alias's `auth_scheme`, then to `bearer`.
884
+ const authScheme = parseAuthSchemeFlag(args);
716
885
 
717
886
  // adcp-client#1612: accept `--transport` as an alias for `--protocol`.
718
887
  // See parseStorboardArgs for full rationale.
@@ -894,6 +1063,7 @@ function parseAgentOptions(args) {
894
1063
  // so falsy-but-valid values (e.g. port "0") aren't dropped from the filter.
895
1064
  const flagValues = [
896
1065
  authToken,
1066
+ authScheme,
897
1067
  protocolFlag,
898
1068
  brief,
899
1069
  contextValue,
@@ -921,6 +1091,7 @@ function parseAgentOptions(args) {
921
1091
 
922
1092
  return {
923
1093
  authToken,
1094
+ authScheme,
924
1095
  protocolFlag,
925
1096
  brief,
926
1097
  file,
@@ -1105,6 +1276,7 @@ async function ensureOAuthTokensForAlias(alias, url, { quiet = false, allowHttp
1105
1276
  */
1106
1277
  function buildResolvedAuthOption({
1107
1278
  resolvedAuth,
1279
+ resolvedAuthScheme,
1108
1280
  resolvedOauthTokens,
1109
1281
  resolvedOauthClient,
1110
1282
  resolvedOauthClientCredentials,
@@ -1128,6 +1300,21 @@ function buildResolvedAuthOption({
1128
1300
  };
1129
1301
  }
1130
1302
  if (resolvedAuth) {
1303
+ if (resolvedAuthScheme === 'basic') {
1304
+ // `resolvedAuth` is the raw `user:pass` string (from `--auth` or the
1305
+ // saved alias's `auth_token`). Decode it here so both the storyboard
1306
+ // runner and `createTestClient` receive the `type: 'basic'` shape they
1307
+ // already know how to send (`src/lib/testing/storyboard/runner.ts:4176`
1308
+ // and `src/lib/testing/client.ts:155`). Most paths validated at
1309
+ // register/parse time already, so this is a second-line defense; the
1310
+ // source string names the alias-resolved origin so a malformed
1311
+ // hand-edited config surfaces with the right hint instead of "--auth".
1312
+ const { username, password } = decodeBasicCredentials(
1313
+ resolvedAuth,
1314
+ 'resolved basic credential (saved alias or --auth)'
1315
+ );
1316
+ return { auth: { type: 'basic', username, password } };
1317
+ }
1131
1318
  return { auth: { type: 'bearer', token: resolvedAuth } };
1132
1319
  }
1133
1320
  return {};
@@ -1149,10 +1336,11 @@ function buildResolvedAuthOption({
1149
1336
  * function deliberately does NOT exchange — it just surfaces the saved
1150
1337
  * credentials so the caller can hand them to the testing/protocol layer.
1151
1338
  */
1152
- async function resolveAgent(agentArg, authToken, protocolFlag, jsonOutput) {
1339
+ async function resolveAgent(agentArg, authToken, protocolFlag, jsonOutput, authScheme = null) {
1153
1340
  let agentUrl;
1154
1341
  let protocol = protocolFlag;
1155
1342
  let finalAuthToken = authToken;
1343
+ let finalAuthScheme = authScheme; // Explicit CLI flag wins; null = defer.
1156
1344
  let oauthTokens;
1157
1345
  let oauthClient;
1158
1346
  let oauthClientCredentials;
@@ -1164,6 +1352,8 @@ async function resolveAgent(agentArg, authToken, protocolFlag, jsonOutput) {
1164
1352
  agentUrl = builtIn.url;
1165
1353
  protocol = protocol || builtIn.protocol;
1166
1354
  finalAuthToken = finalAuthToken || builtIn.auth_token;
1355
+ // Built-ins are bearer-only — no basic-auth gateway in front of them.
1356
+ if (finalAuthScheme === null) finalAuthScheme = 'bearer';
1167
1357
  } else if (isAlias(agentArg)) {
1168
1358
  const savedAgent = getAgent(agentArg);
1169
1359
  agentUrl = savedAgent.url;
@@ -1177,6 +1367,9 @@ async function resolveAgent(agentArg, authToken, protocolFlag, jsonOutput) {
1177
1367
  oauthClient = savedAgent.oauth_client;
1178
1368
  }
1179
1369
  finalAuthToken = finalAuthToken || getEffectiveAuthToken(savedAgent);
1370
+ if (finalAuthScheme === null && savedAgent.auth_scheme) {
1371
+ finalAuthScheme = savedAgent.auth_scheme;
1372
+ }
1180
1373
  if (savedAgent.headers && Object.keys(savedAgent.headers).length > 0) {
1181
1374
  savedHeaders = { ...savedAgent.headers };
1182
1375
  }
@@ -1205,6 +1398,11 @@ async function resolveAgent(agentArg, authToken, protocolFlag, jsonOutput) {
1205
1398
  agentUrl,
1206
1399
  protocol,
1207
1400
  authToken: finalAuthToken,
1401
+ // Default to 'bearer' so callers can compare without null-checking. The
1402
+ // distinction between an explicit `bearer` and an absent flag is only
1403
+ // meaningful during alias resolution above — once we've returned, the
1404
+ // scheme has been resolved.
1405
+ authScheme: finalAuthScheme || 'bearer',
1208
1406
  oauthTokens,
1209
1407
  oauthClient,
1210
1408
  oauthClientCredentials,
@@ -1387,6 +1585,7 @@ QUICK START:
1387
1585
  AGENT MANAGEMENT:
1388
1586
  --save-auth <alias> [url] Save agent with alias
1389
1587
  Static bearer: --auth <token> | --no-auth
1588
+ HTTP Basic: --auth <user:pass> --auth-scheme basic
1390
1589
  OAuth (browser): --oauth
1391
1590
  OAuth (M2M): --client-id <id> | --client-id-env <VAR>
1392
1591
  --client-secret <s> | --client-secret-env <VAR>
@@ -1399,7 +1598,11 @@ AGENT MANAGEMENT:
1399
1598
  OPTIONS:
1400
1599
  --protocol PROTO Force protocol: mcp or a2a (default: auto-detect)
1401
1600
  --transport PROTO Alias for --protocol (A2A SDK convention)
1402
- --auth TOKEN Authentication token
1601
+ --auth TOKEN Authentication token (or 'user:pass' with --auth-scheme basic)
1602
+ --auth-scheme bearer|basic
1603
+ Send --auth as Bearer (default) or RFC 7617 Basic for
1604
+ gateway-fronted agents. Env: ADCP_AUTH_SCHEME. Details:
1605
+ "adcp --save-auth --help".
1403
1606
  -H, --header K=V Extra HTTP header on every request (repeatable). Auth wins on conflict.
1404
1607
  Common use: -H x-adcp-tenant=<id> for tenant routing behind a reverse proxy.
1405
1608
  --oauth OAuth authentication (MCP only, opens browser)
@@ -1519,7 +1722,11 @@ OPTIONS:
1519
1722
  --context JSON Pass context from previous step (step only)
1520
1723
  --request JSON Override sample_request for the step (step only)
1521
1724
  --json JSON output (recommended for LLM consumption)
1522
- --auth TOKEN Authentication token
1725
+ --auth TOKEN Authentication token (or 'user:pass' with --auth-scheme basic)
1726
+ --auth-scheme bearer|basic
1727
+ How --auth is sent (default: bearer). Use 'basic' for
1728
+ gateways that require RFC 7617 Authorization: Basic
1729
+ instead of Authorization: Bearer.
1523
1730
  --oauth Run the browser OAuth flow inline if the saved alias
1524
1731
  has no valid tokens yet. Requires a saved alias
1525
1732
  (use --save-auth first for raw URLs). MCP only.
@@ -2067,7 +2274,17 @@ function enforceStrictFlags(args, removedFound) {
2067
2274
 
2068
2275
  async function handleStoryboardRun(args) {
2069
2276
  const opts = parseAgentOptions(args);
2070
- const { authToken, protocolFlag, jsonOutput, dryRun, positionalArgs, file: filePath, localAgent, format } = opts;
2277
+ const {
2278
+ authToken,
2279
+ authScheme,
2280
+ protocolFlag,
2281
+ jsonOutput,
2282
+ dryRun,
2283
+ positionalArgs,
2284
+ file: filePath,
2285
+ localAgent,
2286
+ format,
2287
+ } = opts;
2071
2288
 
2072
2289
  enforceStrictFlags(args, warnRemovedFlags(args));
2073
2290
 
@@ -2152,11 +2369,12 @@ async function handleStoryboardRun(args) {
2152
2369
  agentUrl,
2153
2370
  protocol,
2154
2371
  authToken: resolvedAuth,
2372
+ authScheme: resolvedAuthScheme,
2155
2373
  oauthTokens: resolvedOauthTokens,
2156
2374
  oauthClient: resolvedOauthClient,
2157
2375
  oauthClientCredentials: resolvedOauthClientCredentials,
2158
2376
  savedHeaders,
2159
- } = await resolveAgent(agentArg, authToken, protocolFlag, jsonOutput);
2377
+ } = await resolveAgent(agentArg, authToken, protocolFlag, jsonOutput, authScheme);
2160
2378
 
2161
2379
  const mergedRunHeaders = mergeHeaders(savedHeaders, opts.customHeaders);
2162
2380
 
@@ -2222,6 +2440,7 @@ async function handleStoryboardRun(args) {
2222
2440
  protocol,
2223
2441
  ...buildResolvedAuthOption({
2224
2442
  resolvedAuth,
2443
+ resolvedAuthScheme,
2225
2444
  resolvedOauthTokens,
2226
2445
  resolvedOauthClient,
2227
2446
  resolvedOauthClientCredentials,
@@ -3236,7 +3455,7 @@ function aggregateStrictSummaries(summaries) {
3236
3455
  }
3237
3456
 
3238
3457
  async function handleMultiInstanceStoryboardRun(args, opts, urls) {
3239
- const { authToken, protocolFlag, jsonOutput, dryRun, positionalArgs, file: filePath } = opts;
3458
+ const { authToken, authScheme, protocolFlag, jsonOutput, dryRun, positionalArgs, file: filePath } = opts;
3240
3459
 
3241
3460
  if (urls.length < 2) {
3242
3461
  console.error('ERROR: Multi-instance mode requires 2+ --url flags. Drop --url for single-instance.');
@@ -3445,7 +3664,7 @@ async function handleMultiInstanceStoryboardRun(args, opts, urls) {
3445
3664
 
3446
3665
  const runOptions = {
3447
3666
  protocol,
3448
- ...(authToken ? { auth: { type: 'bearer', token: authToken } } : {}),
3667
+ ...buildResolvedAuthOption({ resolvedAuth: authToken, resolvedAuthScheme: authScheme || 'bearer' }),
3449
3668
  ...(opts.allowHttp && { allow_http: true }),
3450
3669
  multi_instance_strategy: strategy,
3451
3670
  ...(webhookReceiverOpts ?? {}),
@@ -3556,7 +3775,7 @@ async function handleMultiInstanceStoryboardRun(args, opts, urls) {
3556
3775
  * Storyboard ID, bundle ID, or `--file` is required.
3557
3776
  */
3558
3777
  async function handleAgentsRoutedStoryboardRun(args, opts, routing) {
3559
- const { authToken, protocolFlag, jsonOutput, dryRun, positionalArgs, file: filePath, format } = opts;
3778
+ const { authToken, authScheme, protocolFlag, jsonOutput, dryRun, positionalArgs, file: filePath, format } = opts;
3560
3779
 
3561
3780
  const webhookAutoTunnel = args.includes('--webhook-receiver-auto-tunnel');
3562
3781
  const webhookReceiverBase = extractWebhookReceiverOptions(args);
@@ -3703,7 +3922,7 @@ async function handleAgentsRoutedStoryboardRun(args, opts, routing) {
3703
3922
 
3704
3923
  const runOptions = {
3705
3924
  protocol,
3706
- ...(authToken ? { auth: { type: 'bearer', token: authToken } } : {}),
3925
+ ...buildResolvedAuthOption({ resolvedAuth: authToken, resolvedAuthScheme: authScheme || 'bearer' }),
3707
3926
  ...(opts.allowHttp && { allow_http: true }),
3708
3927
  agents: routing.agents,
3709
3928
  ...(routing.default_agent ? { default_agent: routing.default_agent } : {}),
@@ -3831,11 +4050,12 @@ async function runFullAssessment(agentArg, rawArgs, parsedOpts) {
3831
4050
  agentUrl,
3832
4051
  protocol,
3833
4052
  authToken: finalAuthToken,
4053
+ authScheme: finalAuthScheme,
3834
4054
  oauthTokens,
3835
4055
  oauthClient,
3836
4056
  oauthClientCredentials,
3837
4057
  savedHeaders,
3838
- } = await resolveAgent(agentArg, opts.authToken, opts.protocolFlag, opts.jsonOutput);
4058
+ } = await resolveAgent(agentArg, opts.authToken, opts.protocolFlag, opts.jsonOutput, opts.authScheme);
3839
4059
 
3840
4060
  const mergedAssessmentHeaders = mergeHeaders(savedHeaders, opts.customHeaders);
3841
4061
 
@@ -3888,6 +4108,7 @@ async function runFullAssessment(agentArg, rawArgs, parsedOpts) {
3888
4108
 
3889
4109
  const { auth: authOption } = buildResolvedAuthOption({
3890
4110
  resolvedAuth: finalAuthToken,
4111
+ resolvedAuthScheme: finalAuthScheme,
3891
4112
  resolvedOauthTokens: oauthTokens,
3892
4113
  resolvedOauthClient: oauthClient,
3893
4114
  resolvedOauthClientCredentials: oauthClientCredentials,
@@ -4047,7 +4268,7 @@ async function runFullAssessment(agentArg, rawArgs, parsedOpts) {
4047
4268
 
4048
4269
  async function handleStoryboardStepCmd(args) {
4049
4270
  const { getComplianceStoryboardById, runStoryboardStep } = await import('../dist/lib/testing/storyboard/index.js');
4050
- const { authToken, protocolFlag, jsonOutput, debug, positionalArgs } = parseAgentOptions(args);
4271
+ const { authToken, authScheme, protocolFlag, jsonOutput, debug, positionalArgs } = parseAgentOptions(args);
4051
4272
 
4052
4273
  enforceStrictFlags(args, warnRemovedFlags(args));
4053
4274
 
@@ -4072,10 +4293,11 @@ async function handleStoryboardStepCmd(args) {
4072
4293
  agentUrl,
4073
4294
  protocol,
4074
4295
  authToken: resolvedAuth,
4296
+ authScheme: resolvedAuthScheme,
4075
4297
  oauthTokens: resolvedOauthTokens,
4076
4298
  oauthClient: resolvedOauthClient,
4077
4299
  oauthClientCredentials: resolvedOauthClientCredentials,
4078
- } = await resolveAgent(agentArg, authToken, protocolFlag, jsonOutput);
4300
+ } = await resolveAgent(agentArg, authToken, protocolFlag, jsonOutput, authScheme);
4079
4301
 
4080
4302
  // Parse --context and --request flags (supports inline JSON or @file.json)
4081
4303
  let context = {};
@@ -4095,6 +4317,7 @@ async function handleStoryboardStepCmd(args) {
4095
4317
  request,
4096
4318
  ...buildResolvedAuthOption({
4097
4319
  resolvedAuth,
4320
+ resolvedAuthScheme,
4098
4321
  resolvedOauthTokens,
4099
4322
  resolvedOauthClient,
4100
4323
  resolvedOauthClientCredentials,
@@ -4632,10 +4855,17 @@ Save an agent URL under an alias in ~/.adcp/config.json so future commands
4632
4855
  can use the alias in place of the URL.
4633
4856
 
4634
4857
  AUTH METHODS (pick one):
4635
- Static bearer token:
4858
+ Static bearer token (default):
4636
4859
  --auth <token> Pre-issued bearer token
4637
4860
  --no-auth Agent requires no auth
4638
4861
 
4862
+ HTTP Basic auth (RFC 7617, for gateways like Apigee/Kong/AWS API GW that
4863
+ front the agent with a BasicAuthentication policy):
4864
+ --auth <user:pass> Credentials in 'user:pass' form
4865
+ --auth-scheme basic Required to switch from bearer to Basic. The
4866
+ username is checked for the RFC 7617 ban on
4867
+ ':', and both halves are checked for CR/LF/NUL.
4868
+
4639
4869
  HEADERS (compose with any auth method):
4640
4870
  -H, --header K=V Extra HTTP header on every request to this agent.
4641
4871
  Repeatable. Persists in ~/.adcp/config.json.
@@ -4666,6 +4896,9 @@ EXAMPLES:
4666
4896
  # Static bearer (local dev)
4667
4897
  adcp --save-auth mine https://agent.example.com/mcp --auth AB12
4668
4898
 
4899
+ # HTTP Basic (gateway-fronted agent — Apigee, Kong, AWS API GW)
4900
+ adcp --save-auth mine https://agent.example.com/mcp --auth user:pass --auth-scheme basic
4901
+
4669
4902
  # Browser OAuth
4670
4903
  adcp --save-auth mine https://agent.example.com/mcp --oauth
4671
4904
 
@@ -4704,6 +4937,7 @@ credential material — never sync or commit.
4704
4937
  // consume the next token; boolean flags consume only themselves.
4705
4938
  const valueFlags = new Set([
4706
4939
  '--auth',
4940
+ '--auth-scheme',
4707
4941
  '--oauth-token-url',
4708
4942
  '--client-id',
4709
4943
  '--client-id-env',
@@ -4739,13 +4973,40 @@ credential material — never sync or commit.
4739
4973
  } else if (booleanFlags.has(tok)) {
4740
4974
  parsedFlags[tok] = true;
4741
4975
  } else {
4742
- positional.push(tok);
4976
+ // Equals-form: `--auth-scheme=basic` (no space). The long-form
4977
+ // valueFlags check above only matches the bare flag name; the
4978
+ // equals form lands here. Mirror the parsing pattern used at the
4979
+ // top-level parseAuthSchemeFlag so the two surfaces stay aligned.
4980
+ const eqMatch = /^(--[a-z-]+)=(.+)$/i.exec(tok);
4981
+ if (eqMatch && valueFlags.has(eqMatch[1])) {
4982
+ parsedFlags[eqMatch[1]] = eqMatch[2];
4983
+ } else {
4984
+ positional.push(tok);
4985
+ }
4743
4986
  }
4744
4987
  }
4745
4988
  }
4746
4989
  const savedHeaders = savedHeadersFromFlags.customHeaders;
4747
4990
 
4748
4991
  const providedAuthToken = parsedFlags['--auth'] ?? null;
4992
+ // Honor `ADCP_AUTH_SCHEME` on the save path too — CI scripts that set it
4993
+ // globally shouldn't have to re-pass the flag on every `adcp --save-auth`.
4994
+ // The CLI flag wins on conflict (consistent with the runtime path).
4995
+ const providedAuthScheme = parsedFlags['--auth-scheme'] ?? process.env.ADCP_AUTH_SCHEME ?? null;
4996
+ if (providedAuthScheme !== null && providedAuthScheme !== 'bearer' && providedAuthScheme !== 'basic') {
4997
+ const sourceLabel = parsedFlags['--auth-scheme'] !== undefined ? '--auth-scheme' : 'ADCP_AUTH_SCHEME env var';
4998
+ console.error(`ERROR: ${sourceLabel} must be 'bearer' or 'basic', got: ${providedAuthScheme}\n`);
4999
+ process.exit(2);
5000
+ }
5001
+ if (providedAuthScheme === 'basic' && providedAuthToken !== null) {
5002
+ // Validate at register time so a typo (missing `:`) doesn't get persisted
5003
+ // to disk and then surface as a confusing decode error on every later call.
5004
+ decodeBasicCredentials(providedAuthToken, '--auth');
5005
+ }
5006
+ if (providedAuthScheme !== null && providedAuthToken === null) {
5007
+ console.error('ERROR: --auth-scheme requires --auth (no token to interpret)\n');
5008
+ process.exit(2);
5009
+ }
4749
5010
  const noAuthFlag = parsedFlags['--no-auth'] === true;
4750
5011
  const oauthFlag = parsedFlags['--oauth'] === true;
4751
5012
  const dryRunFlag = parsedFlags['--dry-run'] === true;
@@ -5169,7 +5430,16 @@ credential material — never sync or commit.
5169
5430
  const hasAuthDecision = providedAuthToken !== null || noAuthFlag;
5170
5431
  const nonInteractive = url && hasAuthDecision;
5171
5432
 
5172
- await interactiveSetup(alias, url, protocol, providedAuthToken, nonInteractive, noAuthFlag, savedHeaders);
5433
+ await interactiveSetup(
5434
+ alias,
5435
+ url,
5436
+ protocol,
5437
+ providedAuthToken,
5438
+ nonInteractive,
5439
+ noAuthFlag,
5440
+ savedHeaders,
5441
+ providedAuthScheme
5442
+ );
5173
5443
  process.exit(0);
5174
5444
  }
5175
5445
 
@@ -5192,7 +5462,17 @@ credential material — never sync or commit.
5192
5462
  console.log(` Protocol: ${agent.protocol}`);
5193
5463
  }
5194
5464
  if (agent.auth_token) {
5195
- console.log(` Auth: token configured`);
5465
+ if (agent.auth_scheme === 'basic') {
5466
+ // For basic, show the username — it's already on disk in cleartext
5467
+ // and seeing it makes the alias immediately recognizable (e.g. tells
5468
+ // you which gateway tenant this alias talks to). Password is the
5469
+ // sensitive half and stays hidden.
5470
+ const colonIdx = agent.auth_token.indexOf(':');
5471
+ const username = colonIdx > 0 ? agent.auth_token.slice(0, colonIdx) : '(malformed)';
5472
+ console.log(` Auth: HTTP Basic (user=${username})`);
5473
+ } else {
5474
+ console.log(` Auth: bearer token configured`);
5475
+ }
5196
5476
  }
5197
5477
  if (agent.oauth_client_credentials) {
5198
5478
  // Intentionally minimal: show only that CC is configured and the
@@ -5283,6 +5563,10 @@ credential material — never sync or commit.
5283
5563
  // Parse options first
5284
5564
  const authIndex = args.indexOf('--auth');
5285
5565
  let authToken = authIndex !== -1 ? args[authIndex + 1] : process.env.ADCP_AUTH_TOKEN;
5566
+ // `--auth-scheme bearer|basic`. Default null → defer to saved alias's
5567
+ // `auth_scheme`, then fall back to `bearer`. `--auth user:pass --auth-scheme
5568
+ // basic` is the gateway-Basic shape (e.g. an Apigee BasicAuthentication policy).
5569
+ let authScheme = parseAuthSchemeFlag(args);
5286
5570
  // adcp-client#1612: accept `--transport` as an alias for `--protocol`.
5287
5571
  // Hard-fail when the flag is present without a value, matching sites 1
5288
5572
  // and 2 — security review of #1619 flagged the silent-fallthrough as a
@@ -5323,6 +5607,7 @@ credential material — never sync or commit.
5323
5607
  arg =>
5324
5608
  !arg.startsWith('--') &&
5325
5609
  arg !== authToken && // Don't include the auth token value
5610
+ arg !== authScheme && // Don't include the auth-scheme value
5326
5611
  arg !== protocolFlag && // Don't include the protocol value
5327
5612
  arg !== (timeoutIndex !== -1 ? args[timeoutIndex + 1] : null) && // Don't include timeout value
5328
5613
  !headerTokens.has(arg) // Drop -H and its KEY=VALUE payload
@@ -5379,6 +5664,12 @@ credential material — never sync or commit.
5379
5664
  if (!authToken) {
5380
5665
  authToken = getEffectiveAuthToken(savedAgent);
5381
5666
  }
5667
+ // Honor the alias's saved scheme only when the caller didn't override
5668
+ // it explicitly. Lets a basic-auth alias work without `--auth-scheme basic`
5669
+ // on every invocation.
5670
+ if (authScheme === null && savedAgent.auth_scheme) {
5671
+ authScheme = savedAgent.auth_scheme;
5672
+ }
5382
5673
 
5383
5674
  if (debug) {
5384
5675
  console.error(`DEBUG: Using saved agent '${firstArg}'`);
@@ -5445,7 +5736,14 @@ credential material — never sync or commit.
5445
5736
  console.error(` Protocol: ${protocol}`);
5446
5737
  console.error(` Agent URL: ${agentUrl}`);
5447
5738
  console.error(` Tool: ${toolName || '(list tools)'}`);
5448
- console.error(` Auth: ${authToken ? 'provided' : useOAuth ? 'oauth' : 'none'}`);
5739
+ const authLabel = authToken
5740
+ ? authScheme === 'basic'
5741
+ ? 'basic (user:pass)'
5742
+ : 'bearer'
5743
+ : useOAuth
5744
+ ? 'oauth'
5745
+ : 'none';
5746
+ console.error(` Auth: ${authLabel}`);
5449
5747
  console.error(` Payload: ${JSON.stringify(payload, null, 2)}`);
5450
5748
  console.error('');
5451
5749
  }
@@ -5480,9 +5778,47 @@ credential material — never sync or commit.
5480
5778
  }
5481
5779
  }
5482
5780
 
5781
+ // Runtime mutex: `--auth-scheme basic` is incompatible with any OAuth shape
5782
+ // (browser flow, refresh-token alias, or client credentials). The
5783
+ // `--save-auth` flow catches this at register time, but a user invoking
5784
+ // `adcp <oauth-alias> <tool> --auth user:pass --auth-scheme basic` would
5785
+ // previously have the basic credential silently dropped because the gating
5786
+ // logic below excludes basic when any OAuth material is present. Fail
5787
+ // closed instead — better a clear error than a credential silently ignored.
5788
+ if (authScheme === 'basic' && (useOAuth || agentOAuthClientCredentials || agentOAuthTokens)) {
5789
+ console.error(
5790
+ '\n❌ ERROR: --auth-scheme basic cannot be combined with --oauth or with an alias that has OAuth tokens / client credentials.\n' +
5791
+ ' The agent is already configured for OAuth — pick one auth method, not both.\n'
5792
+ );
5793
+ process.exit(2);
5794
+ }
5795
+
5483
5796
  // Merge saved-alias headers (per-agent routing context, e.g. x-adcp-tenant)
5484
5797
  // with `-H KEY=VALUE` flags from the current invocation. CLI wins on conflict.
5485
- const mergedHeaders = mergeHeaders(savedAgent && savedAgent.headers, cliHeaders);
5798
+ let mergedHeaders = mergeHeaders(savedAgent && savedAgent.headers, cliHeaders);
5799
+
5800
+ // Basic auth path: gateways like Apigee/Kong/AWS API GW with a
5801
+ // BasicAuthentication policy speak RFC 7617 (`Authorization: Basic
5802
+ // <base64(user:pass)>`), not OAuth bearer. We inject the encoded header
5803
+ // AFTER `mergeHeaders` runs so the reserved-key filter doesn't strip it.
5804
+ // The protocol layer (`src/lib/protocols/mcp.ts:475-477`,
5805
+ // `src/lib/protocols/a2a.ts:283-292`) spreads `customHeaders` before the
5806
+ // SDK-supplied Authorization, so suppressing `auth_token` here lets our
5807
+ // injected Basic header reach the wire intact.
5808
+ const useBasicAuth = authScheme === 'basic' && authToken;
5809
+ if (useBasicAuth) {
5810
+ // Helper extraction protects the invariant: this MUST run after
5811
+ // mergeHeaders() so the reserved-key filter doesn't strip the injection.
5812
+ // See `injectBasicAuthHeader`'s docstring for the full rationale.
5813
+ mergedHeaders = injectBasicAuthHeader(mergedHeaders, authToken);
5814
+ }
5815
+
5816
+ // Advisory warning: surface ADCP_AUTH_SCHEME=basic when no token resolved
5817
+ // (otherwise the env var is silently no-op and adopters wonder why their
5818
+ // Basic gateway keeps 401ing). Only fires in the env-set-but-not-applied
5819
+ // case — the inverse (token-without-scheme → silent bearer) is the safe
5820
+ // direction.
5821
+ maybeWarnAuthSchemeIneffective(authToken, authScheme || 'bearer');
5486
5822
 
5487
5823
  // Create agent config
5488
5824
  const agentConfig = {
@@ -5490,7 +5826,11 @@ credential material — never sync or commit.
5490
5826
  name: 'CLI Agent',
5491
5827
  agent_uri: agentUrl,
5492
5828
  protocol: protocol,
5493
- ...(authToken && !useOAuth && !agentOAuthClientCredentials && { auth_token: authToken, requiresAuth: true }),
5829
+ ...(authToken &&
5830
+ !useOAuth &&
5831
+ !agentOAuthClientCredentials &&
5832
+ !useBasicAuth && { auth_token: authToken, requiresAuth: true }),
5833
+ ...(useBasicAuth && { requiresAuth: true }),
5494
5834
  ...(agentOAuthTokens && { oauth_tokens: agentOAuthTokens }),
5495
5835
  ...(agentOAuthClient && { oauth_client: agentOAuthClient }),
5496
5836
  ...(agentOAuthClientCredentials && { oauth_client_credentials: agentOAuthClientCredentials }),
@@ -6011,14 +6351,28 @@ credential material — never sync or commit.
6011
6351
  // `NeedsAuthorizationError` is the richer form (already extended from
6012
6352
  // AuthenticationRequiredError); the string-match branches cover older
6013
6353
  // error shapes from the MCP SDK and other 401 paths.
6354
+ // `Authentication required` matches the plain `AuthenticationRequiredError`
6355
+ // (thrown when the SDK got 401 but couldn't walk OAuth metadata) — that's
6356
+ // the exact shape a Basic-fronted gateway produces, so it's critical the
6357
+ // Basic-hint path catches it.
6014
6358
  const isUnauthorized =
6015
6359
  error instanceof NeedsAuthorizationError ||
6016
6360
  error.name === 'UnauthorizedError' ||
6361
+ error.name === 'AuthenticationRequiredError' ||
6017
6362
  error.message?.toLowerCase().includes('unauthorized') ||
6363
+ error.message?.toLowerCase().includes('authentication required') ||
6018
6364
  error.message?.includes('401');
6019
6365
 
6020
6366
  if (isUnauthorized && protocol === 'mcp' && !useOAuth && !authToken) {
6021
6367
  console.log('\n🔐 Server requires authentication.');
6368
+ // Some agents sit behind an API gateway with a BasicAuthentication policy
6369
+ // (Apigee, Kong, AWS API GW, nginx auth_basic). OAuth won't succeed
6370
+ // against those gateways no matter how the browser flow goes — surface
6371
+ // the alternative before opening the browser so a Basic-fronted adopter
6372
+ // doesn't bounce through a doomed OAuth handshake first.
6373
+ console.log(
6374
+ 'If your agent is fronted by an HTTP-Basic gateway, retry with: --auth <user:pass> --auth-scheme basic'
6375
+ );
6022
6376
  console.log('Starting OAuth authentication...\n');
6023
6377
 
6024
6378
  // Run OAuth flow automatically