@adcp/client 4.22.1 → 4.23.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 (65) hide show
  1. package/dist/lib/index.d.ts +1 -1
  2. package/dist/lib/index.d.ts.map +1 -1
  3. package/dist/lib/index.js.map +1 -1
  4. package/dist/lib/testing/compliance/comply.d.ts +22 -5
  5. package/dist/lib/testing/compliance/comply.d.ts.map +1 -1
  6. package/dist/lib/testing/compliance/comply.js +242 -276
  7. package/dist/lib/testing/compliance/comply.js.map +1 -1
  8. package/dist/lib/testing/compliance/index.d.ts +1 -0
  9. package/dist/lib/testing/compliance/index.d.ts.map +1 -1
  10. package/dist/lib/testing/compliance/index.js +6 -1
  11. package/dist/lib/testing/compliance/index.js.map +1 -1
  12. package/dist/lib/testing/compliance/platform-storyboards.d.ts +44 -0
  13. package/dist/lib/testing/compliance/platform-storyboards.d.ts.map +1 -0
  14. package/dist/lib/testing/compliance/platform-storyboards.js +230 -0
  15. package/dist/lib/testing/compliance/platform-storyboards.js.map +1 -0
  16. package/dist/lib/testing/compliance/storyboard-tracks.d.ts +2 -9
  17. package/dist/lib/testing/compliance/storyboard-tracks.d.ts.map +1 -1
  18. package/dist/lib/testing/compliance/storyboard-tracks.js +4 -44
  19. package/dist/lib/testing/compliance/storyboard-tracks.js.map +1 -1
  20. package/dist/lib/testing/index.d.ts +1 -1
  21. package/dist/lib/testing/index.d.ts.map +1 -1
  22. package/dist/lib/testing/index.js +6 -1
  23. package/dist/lib/testing/index.js.map +1 -1
  24. package/dist/lib/types/core.generated.d.ts +241 -2
  25. package/dist/lib/types/core.generated.d.ts.map +1 -1
  26. package/dist/lib/types/core.generated.js +1 -1
  27. package/dist/lib/types/schemas.generated.d.ts +3380 -3154
  28. package/dist/lib/types/schemas.generated.d.ts.map +1 -1
  29. package/dist/lib/types/schemas.generated.js +220 -115
  30. package/dist/lib/types/schemas.generated.js.map +1 -1
  31. package/dist/lib/types/tools.generated.d.ts +267 -74
  32. package/dist/lib/types/tools.generated.d.ts.map +1 -1
  33. package/dist/lib/version.d.ts +3 -3
  34. package/dist/lib/version.js +3 -3
  35. package/docs/llms.txt +46 -30
  36. package/package.json +1 -1
  37. package/storyboards/audience_sync.yaml +18 -29
  38. package/storyboards/behavioral_analysis.yaml +40 -72
  39. package/storyboards/brand_rights.yaml +172 -75
  40. package/storyboards/campaign_governance_conditions.yaml +187 -0
  41. package/storyboards/campaign_governance_delivery.yaml +231 -0
  42. package/storyboards/campaign_governance_denied.yaml +135 -0
  43. package/storyboards/capability_discovery.yaml +106 -0
  44. package/storyboards/content_standards.yaml +251 -0
  45. package/storyboards/creative_ad_server.yaml +108 -16
  46. package/storyboards/creative_lifecycle.yaml +284 -0
  47. package/storyboards/creative_sales_agent.yaml +2 -6
  48. package/storyboards/creative_template.yaml +1 -5
  49. package/storyboards/error_compliance.yaml +105 -108
  50. package/storyboards/media_buy_catalog_creative.yaml +6 -4
  51. package/storyboards/media_buy_governance_escalation.yaml +9 -5
  52. package/storyboards/media_buy_guaranteed_approval.yaml +9 -7
  53. package/storyboards/media_buy_non_guaranteed.yaml +7 -6
  54. package/storyboards/media_buy_proposal_mode.yaml +9 -8
  55. package/storyboards/media_buy_seller.yaml +153 -165
  56. package/storyboards/media_buy_state_machine.yaml +100 -99
  57. package/storyboards/property_governance.yaml +239 -0
  58. package/storyboards/schema.yaml +2 -2
  59. package/storyboards/schema_validation.yaml +58 -51
  60. package/storyboards/si_session.yaml +99 -317
  61. package/storyboards/signal_marketplace.yaml +6 -5
  62. package/storyboards/signal_owned.yaml +5 -5
  63. package/storyboards/social_platform.yaml +274 -0
  64. package/storyboards/governance_content_standards.yaml +0 -213
  65. package/storyboards/governance_property_lists.yaml +0 -372
@@ -2,8 +2,10 @@
2
2
  /**
3
3
  * Compliance Engine
4
4
  *
5
- * Runs all applicable capability tracks against an agent
6
- * and reports results for every track never stops at the first failure.
5
+ * Storyboard-driven compliance assessment. Storyboards are the routing
6
+ * mechanism; tracks are a reporting layer derived from storyboard results.
7
+ *
8
+ * Resolution priority: storyboards > platform_type > all applicable.
7
9
  */
8
10
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
11
  if (k2 === undefined) k2 = k;
@@ -45,98 +47,15 @@ exports.formatComplianceResults = formatComplianceResults;
45
47
  exports.formatComplianceResultsJSON = formatComplianceResultsJSON;
46
48
  const client_1 = require("../client");
47
49
  const storyboard_tracks_1 = require("./storyboard-tracks");
50
+ const runner_1 = require("../storyboard/runner");
51
+ const loader_1 = require("../storyboard/loader");
52
+ const platform_storyboards_1 = require("./platform-storyboards");
48
53
  const profiles_1 = require("./profiles");
49
54
  const mcp_1 = require("../../protocols/mcp");
50
55
  const test_controller_1 = require("../test-controller");
51
56
  /**
52
- * Maps each track to its constituent scenarios and a human-readable label.
53
- */
54
- const TRACK_DEFINITIONS = {
55
- core: {
56
- label: 'Core Protocol',
57
- scenarios: [
58
- 'health_check',
59
- 'discovery',
60
- 'capability_discovery',
61
- 'schema_compliance',
62
- 'controller_validation',
63
- 'deterministic_account',
64
- ],
65
- },
66
- products: {
67
- label: 'Product Discovery',
68
- scenarios: ['pricing_edge_cases', 'behavior_analysis', 'response_consistency'],
69
- },
70
- media_buy: {
71
- label: 'Media Buy Lifecycle',
72
- scenarios: [
73
- 'create_media_buy',
74
- 'full_sales_flow',
75
- 'creative_inline',
76
- 'temporal_validation',
77
- 'media_buy_lifecycle',
78
- 'terminal_state_enforcement',
79
- 'package_lifecycle',
80
- 'seller_governance_context',
81
- 'deterministic_media_buy',
82
- 'deterministic_budget',
83
- ],
84
- },
85
- creative: {
86
- label: 'Creative Management',
87
- scenarios: ['creative_sync', 'creative_flow', 'deterministic_creative'],
88
- },
89
- reporting: {
90
- label: 'Reporting',
91
- scenarios: ['reporting_flow', 'deterministic_delivery'],
92
- },
93
- governance: {
94
- label: 'Governance',
95
- scenarios: ['governance_property_lists', 'governance_content_standards', 'property_list_filters'],
96
- },
97
- campaign_governance: {
98
- label: 'Campaign Governance',
99
- scenarios: [
100
- 'campaign_governance',
101
- 'campaign_governance_denied',
102
- 'campaign_governance_conditions',
103
- 'campaign_governance_delivery',
104
- ],
105
- },
106
- signals: {
107
- label: 'Signals',
108
- scenarios: ['signals_flow'],
109
- },
110
- si: {
111
- label: 'Sponsored Intelligence',
112
- scenarios: ['si_session_lifecycle', 'si_availability', 'si_handoff', 'deterministic_session'],
113
- },
114
- audiences: {
115
- label: 'Audience Management',
116
- scenarios: ['sync_audiences'],
117
- },
118
- error_handling: {
119
- label: 'Error Compliance',
120
- scenarios: ['error_codes', 'error_structure', 'error_transport'],
121
- },
122
- };
123
- /**
124
- * Which tools make a track "applicable" — if the agent has at least one
125
- * of these tools, the track should be attempted.
57
+ * All compliance tracks in display order.
126
58
  */
127
- const TRACK_RELEVANCE = {
128
- core: [], // always applicable
129
- products: ['get_products'],
130
- media_buy: ['create_media_buy', 'update_media_buy', 'get_media_buys'],
131
- creative: ['sync_creatives', 'build_creative', 'list_creative_formats'],
132
- reporting: ['get_media_buy_delivery'],
133
- governance: ['create_property_list', 'list_content_standards'],
134
- campaign_governance: ['sync_plans', 'check_governance'],
135
- signals: ['get_signals'],
136
- si: ['si_initiate_session'],
137
- audiences: ['sync_audiences'],
138
- error_handling: ['create_media_buy'],
139
- };
140
59
  const TRACK_ORDER = [
141
60
  'core',
142
61
  'products',
@@ -150,51 +69,6 @@ const TRACK_ORDER = [
150
69
  'audiences',
151
70
  'error_handling',
152
71
  ];
153
- function isTrackApplicable(track, tools) {
154
- const relevantTools = TRACK_RELEVANCE[track];
155
- if (relevantTools.length === 0)
156
- return true;
157
- return relevantTools.some(t => tools.includes(t));
158
- }
159
- function isAuthError(step) {
160
- if (!step.error || step.passed)
161
- return false;
162
- const e = step.error.toLowerCase();
163
- return (e.includes('authentication') ||
164
- e.includes('x-adcp-auth') ||
165
- e.includes('unauthorized') ||
166
- e.includes('missing auth') ||
167
- e.includes('401'));
168
- }
169
- /**
170
- * Check if a scenario failed entirely due to auth errors.
171
- * Returns true if every failed step is an auth error.
172
- */
173
- function isAuthOnlyFailure(result) {
174
- if (result.overall_passed)
175
- return false;
176
- const failedSteps = (result.steps ?? []).filter(s => !s.passed);
177
- return failedSteps.length > 0 && failedSteps.every(isAuthError);
178
- }
179
- function computeTrackStatus(results, skippedCount, hasAuth) {
180
- if (results.length === 0)
181
- return 'skip';
182
- // When running without auth, scenarios that failed only due to auth
183
- // don't count as failures
184
- const effectiveResults = results.map(r => {
185
- if (!hasAuth && isAuthOnlyFailure(r)) {
186
- return { ...r, _authSkipped: true, overall_passed: true };
187
- }
188
- return r;
189
- });
190
- const passed = effectiveResults.filter(r => r.overall_passed).length;
191
- const total = effectiveResults.length;
192
- if (passed === total)
193
- return 'pass';
194
- if (passed === 0)
195
- return 'fail';
196
- return 'partial';
197
- }
198
72
  /**
199
73
  * Collect advisory observations from test results.
200
74
  * Analyzes the actual data for quality signals that aren't pass/fail.
@@ -264,7 +138,6 @@ function collectObservations(track, results, profile) {
264
138
  // Media buy track observations
265
139
  if (track === 'media_buy') {
266
140
  // Check for valid_actions support (first match only)
267
- // Only steps with observation_data are considered — snapshot-only steps don't set it.
268
141
  let checkedValidActions = false;
269
142
  for (const result of results) {
270
143
  if (checkedValidActions)
@@ -281,7 +154,6 @@ function collectObservations(track, results, profile) {
281
154
  'Without valid_actions, buyer agents must hardcode the state machine to know what operations are permitted.',
282
155
  });
283
156
  }
284
- // Check creative_deadline support
285
157
  if (obs.has_creative_deadline === false) {
286
158
  observations.push({
287
159
  category: 'best_practice',
@@ -291,7 +163,6 @@ function collectObservations(track, results, profile) {
291
163
  'Buyers need to know when creative uploads must be finalized to avoid rejected submissions.',
292
164
  });
293
165
  }
294
- // Check history entry shape when present
295
166
  if (obs.history_entries && obs.history_entries > 0 && obs.history_valid === false) {
296
167
  observations.push({
297
168
  category: 'best_practice',
@@ -301,7 +172,6 @@ function collectObservations(track, results, profile) {
301
172
  'History entries must include at least timestamp and action to be useful for audit.',
302
173
  });
303
174
  }
304
- // Check dry_run/sandbox confirmation
305
175
  if (obs.sandbox === undefined || obs.sandbox === null) {
306
176
  observations.push({
307
177
  category: 'best_practice',
@@ -387,6 +257,15 @@ function collectObservations(track, results, profile) {
387
257
  'Buyers need to distinguish buyer-initiated from seller-initiated cancellations.',
388
258
  });
389
259
  }
260
+ if (!obs.canceled_at) {
261
+ observations.push({
262
+ category: 'completeness',
263
+ severity: 'warning',
264
+ track,
265
+ message: 'Agent transitions to canceled status but does not include canceled_at timestamp. ' +
266
+ 'A cancellation timestamp is required for audit and reconciliation.',
267
+ });
268
+ }
390
269
  checkedCancellation = true;
391
270
  }
392
271
  }
@@ -484,7 +363,16 @@ function collectObservations(track, results, profile) {
484
363
  }
485
364
  /**
486
365
  * Run compliance assessment against an agent.
487
- * Assesses all applicable tracks independently never stops at first failure.
366
+ * Assesses all applicable storyboards and reports results grouped by track.
367
+ *
368
+ * Resolution priority:
369
+ * 1. options.storyboards — run exactly these storyboard IDs
370
+ * 2. options.platform_type (when tracks is not set) — resolve via PLATFORM_STORYBOARDS
371
+ * 3. options.tracks — run all storyboards for these tracks
372
+ * 4. Default — run all applicable storyboards
373
+ *
374
+ * When platform_type is set, it always drives coherence checking regardless
375
+ * of how the storyboard pool was resolved.
488
376
  */
489
377
  async function comply(agentUrl, options = {}) {
490
378
  try {
@@ -494,10 +382,106 @@ async function comply(agentUrl, options = {}) {
494
382
  await (0, mcp_1.closeMCPConnections)();
495
383
  }
496
384
  }
385
+ // ────────────────────────────────────────────────────────────
386
+ // Storyboard resolution
387
+ // ────────────────────────────────────────────────────────────
388
+ /**
389
+ * Resolve the storyboard pool based on options.
390
+ * Priority: storyboards > platform_type (when tracks is not set) > tracks > all bundled.
391
+ */
392
+ function resolveStoryboards(options) {
393
+ // Explicit storyboard IDs — highest priority
394
+ if (options.storyboards?.length) {
395
+ const resolved = [];
396
+ for (const id of options.storyboards) {
397
+ const sb = (0, loader_1.getStoryboardById)(id);
398
+ if (!sb) {
399
+ throw new Error(`Unknown storyboard ID: "${id}". Use listStoryboards() to see available IDs.`);
400
+ }
401
+ resolved.push(sb);
402
+ }
403
+ return resolved;
404
+ }
405
+ // Platform type — resolve via PLATFORM_STORYBOARDS
406
+ if (options.platform_type && !options.tracks) {
407
+ const pt = options.platform_type;
408
+ const ids = platform_storyboards_1.PLATFORM_STORYBOARDS[pt];
409
+ if (ids) {
410
+ const resolved = [];
411
+ for (const id of ids) {
412
+ const sb = (0, loader_1.getStoryboardById)(id);
413
+ if (sb) {
414
+ resolved.push(sb);
415
+ }
416
+ else {
417
+ // Data integrity issue — storyboard declared in PLATFORM_STORYBOARDS
418
+ // but not found in bundled set. This is a packaging bug.
419
+ console.warn(`PLATFORM_STORYBOARDS[${pt}] references unknown storyboard "${id}"`);
420
+ }
421
+ }
422
+ // Also include universal storyboards (no platform_types) not already in the set
423
+ const resolvedIds = new Set(resolved.map(s => s.id));
424
+ for (const sb of (0, loader_1.loadBundledStoryboards)()) {
425
+ if (!sb.track)
426
+ continue;
427
+ if (resolvedIds.has(sb.id))
428
+ continue;
429
+ if (!sb.platform_types?.length) {
430
+ resolved.push(sb);
431
+ }
432
+ }
433
+ return resolved;
434
+ }
435
+ }
436
+ // Track filter — run storyboards whose track field matches
437
+ if (options.tracks?.length) {
438
+ const trackSet = new Set(options.tracks);
439
+ return (0, loader_1.loadBundledStoryboards)().filter(sb => sb.track && trackSet.has(sb.track));
440
+ }
441
+ // Default — all compliance storyboards (those with a track field)
442
+ return (0, loader_1.loadBundledStoryboards)().filter(sb => sb.track);
443
+ }
444
+ /**
445
+ * Filter storyboards to those applicable for the agent's tools.
446
+ * A storyboard is applicable if the agent has at least one of its required_tools,
447
+ * or if it has no required_tools at all.
448
+ */
449
+ function filterApplicable(storyboards, agentTools) {
450
+ return storyboards.filter(sb => {
451
+ if (!sb.required_tools?.length)
452
+ return true;
453
+ return sb.required_tools.some(tool => agentTools.includes(tool));
454
+ });
455
+ }
456
+ /**
457
+ * Group storyboard results by track.
458
+ */
459
+ function groupByTrack(results, storyboards) {
460
+ // Build a storyboard ID → track lookup
461
+ const trackLookup = new Map();
462
+ for (const sb of storyboards) {
463
+ if (sb.track) {
464
+ trackLookup.set(sb.id, sb.track);
465
+ }
466
+ }
467
+ const grouped = new Map();
468
+ for (const result of results) {
469
+ const track = trackLookup.get(result.storyboard_id);
470
+ if (!track)
471
+ continue;
472
+ if (!grouped.has(track))
473
+ grouped.set(track, []);
474
+ grouped.get(track).push(result);
475
+ }
476
+ return grouped;
477
+ }
478
+ // ────────────────────────────────────────────────────────────
479
+ // Core implementation
480
+ // ────────────────────────────────────────────────────────────
497
481
  async function complyImpl(agentUrl, options) {
498
482
  const start = Date.now();
499
- const { tracks: trackFilter, platform_type, timeout_ms, signal: externalSignal, ...testOptions } = options;
500
- // Validate platform_type if provided (issue #402: accept string, validate internally)
483
+ const { storyboards: _storyboardIds, tracks: _trackFilter, platform_type, timeout_ms, signal: externalSignal, ...testOptions } = options;
484
+ // Validate platform_type if provided
501
485
  let platformProfile;
502
486
  if (platform_type) {
503
487
  const validTypes = (0, profiles_1.getAllPlatformTypes)();
@@ -537,14 +521,14 @@ async function complyImpl(agentUrl, options) {
537
521
  };
538
522
  // Check for abort before starting
539
523
  signal?.throwIfAborted();
540
- // Collect observations across all tracks (declared early for tool discovery diagnostics)
524
+ // Collect observations across all tracks
541
525
  const allObservations = [];
542
- // Discover agent capabilities once and share across all scenarios
526
+ // Discover agent capabilities once and share across all storyboards
543
527
  const client = (0, client_1.createTestClient)(agentUrl, effectiveOptions.protocol ?? 'mcp', effectiveOptions);
544
528
  const { profile, step: profileStep } = await (0, client_1.discoverAgentProfile)(client);
545
529
  effectiveOptions._client = client;
546
530
  effectiveOptions._profile = profile;
547
- // Log discovered tools for diagnostic purposes
531
+ // Log discovered tools
548
532
  if (profileStep.passed) {
549
533
  allObservations.push({
550
534
  category: 'tool_discovery',
@@ -562,145 +546,62 @@ async function complyImpl(agentUrl, options) {
562
546
  }
563
547
  }
564
548
  if (!profileStep.passed) {
565
- const errorMsg = profileStep.error || 'Unknown error';
566
- const observations = [];
567
- // Check for auth errors — either explicit 401/Unauthorized or MCP SDK's generic
568
- // "Failed to discover" which often wraps a 401
569
- const isExplicitAuthError = errorMsg.includes('401') ||
570
- errorMsg.includes('Unauthorized') ||
571
- errorMsg.includes('unauthorized') ||
572
- errorMsg.includes('authentication') ||
573
- errorMsg.includes('JWS') ||
574
- errorMsg.includes('JWT') ||
575
- errorMsg.includes('signature verification');
576
- // When MCP SDK wraps the error, probe the endpoint directly
577
- let isAuthError = isExplicitAuthError;
578
- if (!isAuthError && errorMsg.includes('Failed to discover')) {
579
- try {
580
- const probe = await fetch(agentUrl, {
581
- method: 'POST',
582
- headers: { 'Content-Type': 'application/json' },
583
- signal,
584
- });
585
- if (probe.status === 401 || probe.status === 403) {
586
- isAuthError = true;
587
- }
588
- }
589
- catch {
590
- // Network error — not an auth issue
591
- }
592
- }
593
- const headline = isAuthError ? `Authentication required` : `Agent unreachable — ${errorMsg}`;
594
- if (isAuthError) {
595
- // Check if agent supports OAuth
596
- const { discoverOAuthMetadata } = await Promise.resolve().then(() => __importStar(require('../../auth/oauth/discovery')));
597
- const oauthMeta = await discoverOAuthMetadata(agentUrl);
598
- if (oauthMeta) {
599
- observations.push({
600
- category: 'auth',
601
- severity: 'error',
602
- message: `Agent requires OAuth (issuer: ${oauthMeta.issuer || 'unknown'}). Save credentials: adcp --save-auth <alias> ${agentUrl} --oauth`,
603
- });
604
- }
605
- else {
606
- observations.push({
607
- category: 'auth',
608
- severity: 'error',
609
- message: 'Agent returned 401. Check your --auth token.',
610
- });
611
- }
612
- }
613
- return {
614
- agent_url: agentUrl,
615
- agent_profile: profile,
616
- overall_status: (isAuthError ? 'auth_required' : 'unreachable'),
617
- tracks: [],
618
- tested_tracks: [],
619
- skipped_tracks: [],
620
- expected_tracks: [],
621
- summary: {
622
- tracks_passed: 0,
623
- tracks_failed: 0,
624
- tracks_skipped: 0,
625
- tracks_partial: 0,
626
- tracks_expected: 0,
627
- headline,
628
- },
629
- observations,
630
- tested_at: new Date().toISOString(),
631
- total_duration_ms: Date.now() - start,
632
- dry_run: effectiveOptions.dry_run !== false,
633
- };
549
+ return buildUnreachableResult(agentUrl, profile, profileStep.error, start, effectiveOptions, signal);
634
550
  }
635
- const tracksToRun = trackFilter ?? TRACK_ORDER;
636
- const trackResults = [];
637
- for (const track of tracksToRun) {
638
- // Check for abort between tracks
551
+ // Resolve and filter storyboard pool
552
+ const allStoryboards = resolveStoryboards(options);
553
+ const applicableStoryboards = filterApplicable(allStoryboards, profile.tools);
554
+ // Run storyboards
555
+ const storyboardResults = [];
556
+ const runOptions = {
557
+ ...effectiveOptions,
558
+ agentTools: profile.tools,
559
+ };
560
+ for (const sb of applicableStoryboards) {
639
561
  signal?.throwIfAborted();
640
- const def = TRACK_DEFINITIONS[track];
641
- if (!def)
562
+ const result = await (0, runner_1.runStoryboard)(agentUrl, sb, runOptions);
563
+ storyboardResults.push(result);
564
+ }
565
+ // Group results by track and build TrackResults
566
+ const grouped = groupByTrack(storyboardResults, applicableStoryboards);
567
+ const trackResults = [];
568
+ // Determine which tracks had storyboards in the pool (even if filtered out by tools)
569
+ const poolTrackSet = new Set();
570
+ for (const sb of allStoryboards) {
571
+ if (sb.track)
572
+ poolTrackSet.add(sb.track);
573
+ }
574
+ for (const track of TRACK_ORDER) {
575
+ if (!poolTrackSet.has(track))
642
576
  continue;
643
- if (!isTrackApplicable(track, profile.tools)) {
577
+ const results = grouped.get(track) ?? [];
578
+ if (results.length > 0) {
579
+ const trackResult = (0, storyboard_tracks_1.mapStoryboardResultsToTrackResult)(track, results, profile);
580
+ const observations = collectObservations(track, trackResult.scenarios, profile);
581
+ trackResult.observations = observations;
582
+ allObservations.push(...observations);
583
+ trackResults.push(trackResult);
584
+ }
585
+ else {
586
+ // Track was in the pool but no storyboards ran (agent lacks tools)
644
587
  const isExpected = track !== 'core' && (platformProfile?.expected_tracks.includes(track) ?? false);
645
- const requiredTools = TRACK_RELEVANCE[track];
646
- const trackObservations = [];
647
- if (requiredTools.length > 0) {
648
- trackObservations.push({
649
- category: 'tool_discovery',
650
- severity: isExpected ? 'warning' : 'info',
651
- message: `Track "${track}" skipped: agent does not advertise any of [${requiredTools.join(', ')}]. ` +
652
- `Agent tools: [${profile.tools.join(', ')}]`,
653
- evidence: { expected_tools: requiredTools, agent_tool_count: profile.tools.length },
654
- });
655
- }
656
- allObservations.push(...trackObservations);
657
588
  trackResults.push({
658
589
  track,
659
590
  status: isExpected ? 'expected' : 'skip',
660
- label: def.label,
661
- scenarios: [],
662
- skipped_scenarios: def.scenarios,
663
- observations: trackObservations,
664
- duration_ms: 0,
665
- });
666
- continue;
667
- }
668
- const trackStart = Date.now();
669
- // Run compliance storyboards for this track
670
- const storyboardResults = await (0, storyboard_tracks_1.runTrackStoryboards)(agentUrl, track, profile.tools, {
671
- ...effectiveOptions,
672
- agentTools: profile.tools,
673
- });
674
- let trackResult;
675
- if (storyboardResults.length > 0) {
676
- // Map storyboard results to TrackResult for backwards compat
677
- trackResult = (0, storyboard_tracks_1.mapStoryboardResultsToTrackResult)(track, storyboardResults, profile);
678
- }
679
- else {
680
- // No storyboards for this track — skip
681
- trackResult = {
682
- track,
683
- status: 'skip',
684
- label: def.label,
591
+ label: storyboard_tracks_1.TRACK_LABELS[track] || track,
685
592
  scenarios: [],
686
593
  skipped_scenarios: [],
687
594
  observations: [],
688
595
  duration_ms: 0,
689
- };
596
+ });
690
597
  }
691
- // Collect observations from track results and agent profile
692
- const observations = collectObservations(track, trackResult.scenarios, profile);
693
- trackResult.observations = observations;
694
- trackResult.duration_ms = Date.now() - trackStart;
695
- allObservations.push(...observations);
696
- trackResults.push(trackResult);
697
598
  }
698
599
  // Build platform coherence result if platform type was declared
699
600
  let platformCoherence;
700
601
  if (platformProfile) {
701
602
  const findings = platformProfile.checkCoherence(profile);
702
- const missingTracks = platformProfile.expected_tracks.filter(t => !isTrackApplicable(t, profile.tools) && t !== 'core');
703
- // Add coherence findings as observations
603
+ const applicableTrackSet = new Set(trackResults.filter(t => t.status !== 'skip' && t.status !== 'expected').map(t => t.track));
604
+ const missingTracks = platformProfile.expected_tracks.filter(t => !applicableTrackSet.has(t) && t !== 'core');
704
605
  for (const finding of findings) {
705
606
  allObservations.push({
706
607
  category: 'coherence',
@@ -720,18 +621,14 @@ async function complyImpl(agentUrl, options) {
720
621
  };
721
622
  }
722
623
  const summary = buildSummary(trackResults);
723
- // Partition tracks by disposition (issue #403)
724
624
  const testedTracks = trackResults.filter(t => t.status === 'pass' || t.status === 'fail' || t.status === 'partial');
725
625
  const skippedTracks = trackResults
726
626
  .filter(t => t.status === 'skip')
727
- .map(t => {
728
- const required = TRACK_RELEVANCE[t.track];
729
- return {
730
- track: t.track,
731
- label: t.label,
732
- reason: required.length > 0 ? `Agent lacks required tools: ${required.join(', ')}` : 'Agent lacks required tools',
733
- };
734
- });
627
+ .map(t => ({
628
+ track: t.track,
629
+ label: t.label,
630
+ reason: 'Agent lacks required tools for applicable storyboards',
631
+ }));
735
632
  const expectedTracks = trackResults
736
633
  .filter(t => t.status === 'expected')
737
634
  .map(t => ({
@@ -739,7 +636,6 @@ async function complyImpl(agentUrl, options) {
739
636
  label: t.label,
740
637
  reason: `Expected for ${platformCoherence?.label ?? 'declared platform type'}`,
741
638
  }));
742
- // Compute overall status (issue #401)
743
639
  const overallStatus = computeOverallStatus(summary);
744
640
  return {
745
641
  agent_url: agentUrl,
@@ -767,6 +663,76 @@ async function complyImpl(agentUrl, options) {
767
663
  }
768
664
  }
769
665
  }
666
+ /**
667
+ * Build result for an unreachable or auth-required agent.
668
+ */
669
+ async function buildUnreachableResult(agentUrl, profile, errorMsg, start, effectiveOptions, signal) {
670
+ const err = errorMsg || 'Unknown error';
671
+ const observations = [];
672
+ const isExplicitAuthError = err.includes('401') ||
673
+ err.includes('Unauthorized') ||
674
+ err.includes('unauthorized') ||
675
+ err.includes('authentication') ||
676
+ err.includes('JWS') ||
677
+ err.includes('JWT') ||
678
+ err.includes('signature verification');
679
+ let isAuthError = isExplicitAuthError;
680
+ if (!isAuthError && err.includes('Failed to discover')) {
681
+ try {
682
+ const probe = await fetch(agentUrl, {
683
+ method: 'POST',
684
+ headers: { 'Content-Type': 'application/json' },
685
+ signal,
686
+ });
687
+ if (probe.status === 401 || probe.status === 403) {
688
+ isAuthError = true;
689
+ }
690
+ }
691
+ catch {
692
+ // Network error — not an auth issue
693
+ }
694
+ }
695
+ const headline = isAuthError ? `Authentication required` : `Agent unreachable — ${err}`;
696
+ if (isAuthError) {
697
+ const { discoverOAuthMetadata } = await Promise.resolve().then(() => __importStar(require('../../auth/oauth/discovery')));
698
+ const oauthMeta = await discoverOAuthMetadata(agentUrl);
699
+ if (oauthMeta) {
700
+ observations.push({
701
+ category: 'auth',
702
+ severity: 'error',
703
+ message: `Agent requires OAuth (issuer: ${oauthMeta.issuer || 'unknown'}). Save credentials: adcp --save-auth <alias> ${agentUrl} --oauth`,
704
+ });
705
+ }
706
+ else {
707
+ observations.push({
708
+ category: 'auth',
709
+ severity: 'error',
710
+ message: 'Agent returned 401. Check your --auth token.',
711
+ });
712
+ }
713
+ }
714
+ return {
715
+ agent_url: agentUrl,
716
+ agent_profile: profile,
717
+ overall_status: (isAuthError ? 'auth_required' : 'unreachable'),
718
+ tracks: [],
719
+ tested_tracks: [],
720
+ skipped_tracks: [],
721
+ expected_tracks: [],
722
+ summary: {
723
+ tracks_passed: 0,
724
+ tracks_failed: 0,
725
+ tracks_skipped: 0,
726
+ tracks_partial: 0,
727
+ tracks_expected: 0,
728
+ headline,
729
+ },
730
+ observations,
731
+ tested_at: new Date().toISOString(),
732
+ total_duration_ms: Date.now() - start,
733
+ dry_run: effectiveOptions.dry_run !== false,
734
+ };
735
+ }
770
736
  /**
771
737
  * Compute overall status for a reachable agent.
772
738
  * auth_required and unreachable are set directly in the early-exit path.