From e719ba6d41652d61be7ffb491a0213e9f8999da9 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 20 Sep 2025 11:03:32 -0400 Subject: [PATCH 1/3] Fix A2A agent card validation endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add /.well-known/agent-card.json as first endpoint (A2A protocol standard) - Remove non-standard /card and /.well-known/agent-card endpoints - Add card_endpoint tracking to show which endpoint was successful - Update UI to display agent name from card data and protocol indicator - Simplify to only check A2A standard and root URL fallback ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/adagents-manager.ts | 448 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 448 insertions(+) create mode 100644 src/adagents-manager.ts diff --git a/src/adagents-manager.ts b/src/adagents-manager.ts new file mode 100644 index 000000000..a49458956 --- /dev/null +++ b/src/adagents-manager.ts @@ -0,0 +1,448 @@ +import axios from 'axios'; +import { + AdAgentsJson, + AuthorizedAgent, + AdAgentsValidationResult, + ValidationError, + ValidationWarning, + AgentCardValidationResult +} from './types/adcp'; + +export class AdAgentsManager { + + /** + * Validates a domain's adagents.json file + */ + async validateDomain(domain: string): Promise { + // Normalize domain - remove protocol and trailing slash + const normalizedDomain = domain.replace(/^https?:\/\//, '').replace(/\/$/, ''); + const url = `https://${normalizedDomain}/.well-known/adagents.json`; + + const result: AdAgentsValidationResult = { + valid: false, + errors: [], + warnings: [], + domain: normalizedDomain, + url + }; + + try { + // Fetch the adagents.json file + const response = await axios.get(url, { + timeout: 10000, + headers: { + 'Accept': 'application/json', + 'User-Agent': 'AdCP-Testing-Framework/1.0' + }, + validateStatus: () => true // Don't throw on non-2xx status codes + }); + + result.status_code = response.status; + + // Check HTTP status + if (response.status !== 200) { + result.errors.push({ + field: 'http_status', + message: `HTTP ${response.status}: adagents.json must return 200 status code`, + severity: 'error' + }); + // Don't include raw HTML error pages - they're not useful for validation + return result; + } + + // Only include raw data for successful responses + result.raw_data = response.data; + + // Parse and validate JSON structure + const adagentsData = response.data; + this.validateStructure(adagentsData, result); + this.validateContent(adagentsData, result); + + // If no errors, mark as valid + result.valid = result.errors.length === 0; + + } catch (error) { + if (axios.isAxiosError(error)) { + if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') { + result.errors.push({ + field: 'connection', + message: `Cannot connect to ${normalizedDomain}`, + severity: 'error' + }); + } else if (error.code === 'ECONNABORTED') { + result.errors.push({ + field: 'timeout', + message: 'Request timed out after 10 seconds', + severity: 'error' + }); + } else { + result.errors.push({ + field: 'network', + message: error.message, + severity: 'error' + }); + } + } else { + result.errors.push({ + field: 'unknown', + message: 'Unknown error occurred', + severity: 'error' + }); + } + } + + return result; + } + + /** + * Validates the structure of adagents.json + */ + private validateStructure(data: any, result: AdAgentsValidationResult): void { + if (typeof data !== 'object' || data === null) { + result.errors.push({ + field: 'root', + message: 'adagents.json must be a valid JSON object', + severity: 'error' + }); + return; + } + + // Check required fields + if (!data.authorized_agents) { + result.errors.push({ + field: 'authorized_agents', + message: 'authorized_agents field is required', + severity: 'error' + }); + return; + } + + if (!Array.isArray(data.authorized_agents)) { + result.errors.push({ + field: 'authorized_agents', + message: 'authorized_agents must be an array', + severity: 'error' + }); + return; + } + + // Validate each agent + data.authorized_agents.forEach((agent: any, index: number) => { + this.validateAgent(agent, index, result); + }); + + // Check optional fields + if (data.$schema && typeof data.$schema !== 'string') { + result.errors.push({ + field: '$schema', + message: '$schema must be a string', + severity: 'error' + }); + } + + if (data.last_updated) { + if (typeof data.last_updated !== 'string') { + result.errors.push({ + field: 'last_updated', + message: 'last_updated must be an ISO 8601 timestamp string', + severity: 'error' + }); + } else { + // Validate ISO 8601 format + const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/; + if (!isoRegex.test(data.last_updated)) { + result.warnings.push({ + field: 'last_updated', + message: 'last_updated should be in ISO 8601 format (YYYY-MM-DDTHH:mm:ssZ)', + suggestion: 'Use new Date().toISOString() format' + }); + } + } + } + + // Recommendations + if (!data.$schema) { + result.warnings.push({ + field: '$schema', + message: 'Consider adding $schema field for validation', + suggestion: 'Add "$schema": "https://adcontextprotocol.org/schemas/v1/adagents.json"' + }); + } + + if (!data.last_updated) { + result.warnings.push({ + field: 'last_updated', + message: 'Consider adding last_updated timestamp', + suggestion: 'Add "last_updated": "' + new Date().toISOString() + '"' + }); + } + } + + /** + * Validates an individual agent entry + */ + private validateAgent(agent: any, index: number, result: AdAgentsValidationResult): void { + const prefix = `authorized_agents[${index}]`; + + if (typeof agent !== 'object' || agent === null) { + result.errors.push({ + field: prefix, + message: 'Each agent must be an object', + severity: 'error' + }); + return; + } + + // Required fields + if (!agent.url) { + result.errors.push({ + field: `${prefix}.url`, + message: 'url field is required', + severity: 'error' + }); + } else if (typeof agent.url !== 'string') { + result.errors.push({ + field: `${prefix}.url`, + message: 'url must be a string', + severity: 'error' + }); + } else { + // Validate URL format + try { + new URL(agent.url); + + // Check HTTPS requirement + if (!agent.url.startsWith('https://')) { + result.errors.push({ + field: `${prefix}.url`, + message: 'Agent URL must use HTTPS', + severity: 'error' + }); + } + } catch { + result.errors.push({ + field: `${prefix}.url`, + message: 'url must be a valid URL', + severity: 'error' + }); + } + } + + if (!agent.authorized_for) { + result.errors.push({ + field: `${prefix}.authorized_for`, + message: 'authorized_for field is required', + severity: 'error' + }); + } else if (typeof agent.authorized_for !== 'string') { + result.errors.push({ + field: `${prefix}.authorized_for`, + message: 'authorized_for must be a string', + severity: 'error' + }); + } else { + // Validate length constraints + if (agent.authorized_for.length < 1) { + result.errors.push({ + field: `${prefix}.authorized_for`, + message: 'authorized_for cannot be empty', + severity: 'error' + }); + } else if (agent.authorized_for.length > 500) { + result.errors.push({ + field: `${prefix}.authorized_for`, + message: 'authorized_for must be 500 characters or less', + severity: 'error' + }); + } + } + } + + /** + * Validates business logic and content + */ + private validateContent(data: any, result: AdAgentsValidationResult): void { + if (!data.authorized_agents || !Array.isArray(data.authorized_agents)) { + return; // Structure validation should have caught this + } + + // Check for duplicate agent URLs + const seenUrls = new Set(); + data.authorized_agents.forEach((agent: any, index: number) => { + if (agent.url && typeof agent.url === 'string') { + if (seenUrls.has(agent.url)) { + result.warnings.push({ + field: `authorized_agents[${index}].url`, + message: 'Duplicate agent URL found', + suggestion: 'Remove duplicate entries or consolidate authorization scopes' + }); + } + seenUrls.add(agent.url); + } + }); + + // Check if no agents are defined + if (data.authorized_agents.length === 0) { + result.warnings.push({ + field: 'authorized_agents', + message: 'No authorized agents defined', + suggestion: 'Add at least one authorized agent' + }); + } + } + + /** + * Validates agent cards for all agents in adagents.json + */ + async validateAgentCards(agents: AuthorizedAgent[]): Promise { + const results: AgentCardValidationResult[] = []; + + // Validate each agent in parallel + const validationPromises = agents.map(agent => this.validateSingleAgentCard(agent.url)); + const validationResults = await Promise.allSettled(validationPromises); + + validationResults.forEach((result, index) => { + if (result.status === 'fulfilled') { + results.push(result.value); + } else { + results.push({ + agent_url: agents[index].url, + valid: false, + errors: [`Validation failed: ${result.reason}`] + }); + } + }); + + return results; + } + + /** + * Validates a single agent's card endpoint + */ + private async validateSingleAgentCard(agentUrl: string): Promise { + const result: AgentCardValidationResult = { + agent_url: agentUrl, + valid: false, + errors: [] + }; + + try { + const startTime = Date.now(); + + // Try to fetch agent card (A2A standard and root fallback) + const cardEndpoints = [ + `${agentUrl}/.well-known/agent-card.json`, // A2A protocol standard + agentUrl // Sometimes the main URL returns the card + ]; + + let cardFound = false; + + for (const endpoint of cardEndpoints) { + try { + const response = await axios.get(endpoint, { + timeout: 5000, + headers: { + 'Accept': 'application/json', + 'User-Agent': 'AdCP-Testing-Framework/1.0' + }, + validateStatus: () => true + }); + + result.response_time_ms = Date.now() - startTime; + result.status_code = response.status; + + if (response.status === 200) { + result.card_data = response.data; + result.card_endpoint = endpoint; + cardFound = true; + + // Check content-type header + const contentType = response.headers['content-type'] || ''; + const isJsonContentType = contentType.includes('application/json'); + + // Basic validation of card structure + if (typeof response.data === 'object' && response.data !== null) { + if (!isJsonContentType) { + result.errors.push(`Endpoint returned JSON data but with content-type: ${contentType}. Should be application/json`); + result.valid = false; + } else { + result.valid = true; + } + } else { + if (contentType.includes('text/html')) { + result.errors.push('Agent card endpoint returned HTML instead of JSON. This appears to be a website, not an agent card endpoint.'); + } else { + result.errors.push(`Agent card is not a valid JSON object (content-type: ${contentType})`); + } + } + break; + } + } catch (endpointError) { + // Try next endpoint + continue; + } + } + + if (!cardFound) { + result.errors.push('No agent card found at /.well-known/agent-card.json or root URL'); + } + + } catch (error) { + if (axios.isAxiosError(error)) { + result.errors.push(`Network error: ${error.message}`); + } else { + result.errors.push('Unknown error occurred while validating agent card'); + } + } + + return result; + } + + /** + * Creates a properly formatted adagents.json file + */ + createAdAgentsJson( + agents: AuthorizedAgent[], + includeSchema: boolean = true, + includeTimestamp: boolean = true + ): string { + const adagents: AdAgentsJson = { + authorized_agents: agents + }; + + if (includeSchema) { + adagents.$schema = 'https://adcontextprotocol.org/schemas/v1/adagents.json'; + } + + if (includeTimestamp) { + adagents.last_updated = new Date().toISOString(); + } + + return JSON.stringify(adagents, null, 2); + } + + /** + * Validates a proposed adagents.json structure before creation + */ + validateProposed(agents: AuthorizedAgent[]): AdAgentsValidationResult { + const mockData = { + $schema: 'https://adcontextprotocol.org/schemas/v1/adagents.json', + authorized_agents: agents, + last_updated: new Date().toISOString() + }; + + const result: AdAgentsValidationResult = { + valid: false, + errors: [], + warnings: [], + domain: 'proposed', + url: 'proposed' + }; + + this.validateStructure(mockData, result); + this.validateContent(mockData, result); + + result.valid = result.errors.length === 0; + return result; + } +} \ No newline at end of file From c95ed99bcae43bbced0925221670c9eb547bb573 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 20 Sep 2025 11:03:37 -0400 Subject: [PATCH 2/3] Add card_endpoint field to AgentCardValidationResult MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Track which endpoint successfully returned the agent card - Enables UI to show protocol indicators (A2A vs Root) ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/types/adcp.ts | 69 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/types/adcp.ts b/src/types/adcp.ts index 062c7a331..0b50f61e8 100644 --- a/src/types/adcp.ts +++ b/src/types/adcp.ts @@ -383,4 +383,73 @@ export interface ListCreativesResponse { has_more: boolean; next_cursor?: string; }; +} + +// AdAgents.json Types - Based on AdCP reference specification +export interface AdAgentsJson { + $schema?: string; + authorized_agents: AuthorizedAgent[]; + last_updated?: string; +} + +export interface AuthorizedAgent { + url: string; + authorized_for: string; +} + +// Validation Types +export interface AdAgentsValidationResult { + valid: boolean; + errors: ValidationError[]; + warnings: ValidationWarning[]; + domain: string; + url: string; + status_code?: number; + raw_data?: any; +} + +export interface ValidationError { + field: string; + message: string; + severity: 'error' | 'warning'; +} + +export interface ValidationWarning { + field: string; + message: string; + suggestion?: string; +} + +export interface AgentCardValidationResult { + agent_url: string; + valid: boolean; + status_code?: number; + card_data?: any; + card_endpoint?: string; + errors: string[]; + response_time_ms?: number; +} + +// API Request/Response Types for AdAgents Management +export interface ValidateAdAgentsRequest { + domain: string; +} + +export interface ValidateAdAgentsResponse { + domain: string; + found: boolean; + validation: AdAgentsValidationResult; + agent_cards?: AgentCardValidationResult[]; +} + +export interface CreateAdAgentsRequest { + authorized_agents: AuthorizedAgent[]; + include_schema?: boolean; + include_timestamp?: boolean; +} + +export interface CreateAdAgentsResponse { + success: boolean; + adagents_json: string; + validation: AdAgentsValidationResult; } \ No newline at end of file From 034b4e2def56b1eb6166d6c345bab81a50c0dec5 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 20 Sep 2025 11:12:28 -0400 Subject: [PATCH 3/3] Add comprehensive AdAgents.json management tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add dedicated /adagents.html page with full management interface - Implement domain validation, agent card validation, and JSON generation - Add top navigation between Agent Testing and AdAgents Manager pages - Reposition debug panel to bottom for better UX - Add server endpoints for adagents validation, creation, and agent card validation - Support both create and update workflows with intelligent domain detection - Pre-populate existing agents when updating existing adagents.json files - Enhanced UI shows agent names from cards and protocol indicators ([A2A] vs [Root]) Features: - ๐Ÿ” Domain validation with adagents.json structure checking - ๐ŸŽฏ Agent card validation with A2A protocol support - ๐Ÿ“ Interactive agent list with add/remove functionality - ๐Ÿ’พ JSON generation with schema and timestamp options - ๐Ÿ“‹ Copy to clipboard and download functionality - ๐Ÿ”„ Smart create vs update mode detection - ๐Ÿ“ Personalized deployment instructions per domain ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/public/adagents.html | 1099 ++++++++++++++++++++++++++++++++++++++ src/public/index.html | 120 ++++- src/server.ts | 167 +++++- 3 files changed, 1371 insertions(+), 15 deletions(-) create mode 100644 src/public/adagents.html diff --git a/src/public/adagents.html b/src/public/adagents.html new file mode 100644 index 000000000..3f9ce5698 --- /dev/null +++ b/src/public/adagents.html @@ -0,0 +1,1099 @@ + + + + + + AdAgents.json Manager - AdCP Testing Framework + + + + + + + + + +
+ + + + +
+

๐Ÿ” Domain Validation

+

Check if a domain has a properly formatted adagents.json file and validate agent cards. Enter any domain to validate compliance with the AdCP specification.

+ +
+ + +
+ + +
+ + +
+

๐Ÿ“ Create AdAgents.json

+

Create a properly formatted adagents.json file for your domain. Add authorized sales agents and generate a compliant file ready for deployment.

+ + +
+

Step 1: Enter Your Domain

+

First, enter your domain name to get personalized deployment instructions.

+
+ + +
+
+ + + + + +
+ + +
+

๐Ÿ“š AdCP Specification

+

The adagents.json file must be hosted at /.well-known/adagents.json and follow the AdCP specification:

+ +
+

Required Structure:

+
{
+  "$schema": "https://adcontextprotocol.org/schemas/v1/adagents.json",
+  "authorized_agents": [
+    {
+      "url": "https://agent.example.com",
+      "authorized_for": "Description of authorization scope"
+    }
+  ],
+  "last_updated": "2025-01-10T12:00:00Z"
+}
+ +
+

Key Requirements:

+
    +
  • HTTPS Required: All agent URLs must use HTTPS
  • +
  • Authorization Scope: 1-500 characters describing what each agent is authorized to do
  • +
  • Domain Matching: Agents validate authorization based on publisher domain
  • +
  • Agent Cards: Each agent should provide a valid agent card at their endpoint
  • +
+
+ +
+

+ Note: This tool validates against the AdCP adagents.json specification. + The structure and requirements shown above reflect the current protocol standards. +

+
+
+
+
+ + + + \ No newline at end of file diff --git a/src/public/index.html b/src/public/index.html index f04cbe026..f2e78b594 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -132,7 +132,66 @@ } .main-content.debug-open { - margin-right: 520px; + /* No margin adjustment needed - debug panel is now at bottom */ + } + + /* Top Navigation */ + .top-nav { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1); + position: sticky; + top: 0; + z-index: 1000; + } + + .nav-container { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; + display: flex; + justify-content: space-between; + align-items: center; + height: 60px; + } + + .nav-brand h2 { + color: white; + margin: 0; + font-size: 18px; + font-weight: 600; + } + + .nav-links { + display: flex; + gap: 30px; + } + + .nav-link { + color: rgba(255, 255, 255, 0.8); + text-decoration: none; + padding: 8px 16px; + border-radius: 6px; + transition: all 0.3s ease; + font-weight: 500; + font-size: 14px; + } + + .nav-link:hover { + color: white; + background: rgba(255, 255, 255, 0.1); + transform: translateY(-1px); + } + + .nav-link.active { + color: white; + background: rgba(255, 255, 255, 0.2); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + } + + /* Adjust main content to account for navigation */ + .main-content { + margin-top: 0; } /* Glass-morphic Card Design */ @@ -169,20 +228,21 @@ 0 4px 12px rgba(0, 0, 0, 0.08); } - /* Glassmorphic Debug Panel */ + /* Glassmorphic Debug Panel - Bottom Positioned */ .debug-panel { position: fixed; - right: -500px; - top: 80px; /* Start below header */ - width: 500px; - height: calc(100vh - 80px); /* Subtract header height */ + bottom: -400px; + left: 0; + right: 0; + width: 100%; + height: 400px; background: rgba(26, 26, 26, 0.95); backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); box-shadow: - -8px 0 32px rgba(0, 0, 0, 0.3), - inset 1px 0 1px rgba(255, 255, 255, 0.1); - transition: right var(--timing-normal) var(--easing); + 0 -8px 32px rgba(0, 0, 0, 0.3), + inset 0 1px 1px rgba(255, 255, 255, 0.1); + transition: bottom var(--timing-normal) var(--easing); display: flex; flex-direction: column; z-index: 1000; @@ -190,7 +250,7 @@ } .debug-panel.open { - right: 0; + bottom: 0; } .debug-header { @@ -474,11 +534,17 @@ border-radius: 25px; cursor: pointer; box-shadow: 0 4px 6px rgba(0,0,0,0.1); - z-index: 999; + z-index: 1001; font-weight: bold; display: flex; align-items: center; gap: 8px; + transition: all 0.3s ease; + } + + /* Move toggle button up when debug panel is open */ + .toggle-debug-btn.panel-open { + bottom: 420px; /* 400px panel height + 20px margin */ } .toggle-debug-btn:hover { @@ -1420,12 +1486,16 @@ } .main-content.debug-open { - margin-right: 10px; + /* No margin adjustment needed - debug panel is now at bottom */ } .debug-panel { - width: 100%; - right: -100%; + height: 300px; /* Smaller height on mobile */ + bottom: -300px; + } + + .toggle-debug-btn.panel-open { + bottom: 320px; /* 300px panel height + 20px margin on mobile */ } /* Stack tables vertically on mobile */ @@ -1510,6 +1580,19 @@ + + +
@@ -4229,15 +4312,18 @@

๐Ÿ“Š Preview

const mainContent = document.getElementById('main-content'); const btnText = document.getElementById('debug-btn-text'); const btnIcon = document.getElementById('debug-btn-icon'); + const toggleBtn = document.querySelector('.toggle-debug-btn'); if (debugPanelOpen) { panel.classList.add('open'); mainContent.classList.add('debug-open'); + toggleBtn.classList.add('panel-open'); btnText.textContent = 'Hide Debug'; btnIcon.textContent = 'โœ–๏ธ'; } else { panel.classList.remove('open'); mainContent.classList.remove('debug-open'); + toggleBtn.classList.remove('panel-open'); btnText.textContent = 'Show Debug'; btnIcon.textContent = '๐Ÿ”'; } @@ -7052,6 +7138,12 @@

๐Ÿ“ฅ Response

// Agents are loaded dynamically from API - no hardcoded defaults // This prevents issues with mismatched agent IDs + // Initialize debug panel button position since it starts open + const toggleBtn = document.querySelector('.toggle-debug-btn'); + if (debugPanelOpen && toggleBtn) { + toggleBtn.classList.add('panel-open'); + } + // Load custom agents from localStorage loadCustomAgentsFromStorage(); diff --git a/src/server.ts b/src/server.ts index 24a5a86b3..8f6e29fb6 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,7 +4,8 @@ import fastifyStatic from '@fastify/static'; import fastifyCors from '@fastify/cors'; import path from 'path'; import { testAgents, getAgentList, testSingleAgent, getStandardFormats } from './protocols'; -import { TestRequest, ApiResponse, TestResponse, AgentListResponse } from './types/adcp'; +import { TestRequest, ApiResponse, TestResponse, AgentListResponse, ValidateAdAgentsRequest, ValidateAdAgentsResponse, CreateAdAgentsRequest, CreateAdAgentsResponse } from './types/adcp'; +import { AdAgentsManager } from './adagents-manager'; // __dirname is available in CommonJS mode @@ -488,6 +489,170 @@ app.setErrorHandler((error, request, reply) => { }); }); +// AdAgents.json Management Endpoints +const adagentsManager = new AdAgentsManager(); + +// Validate domain's adagents.json +app.post<{ + Body: ValidateAdAgentsRequest; + Reply: ApiResponse; +}>('/api/adagents/validate', async (request, reply) => { + try { + const { domain } = request.body; + + if (!domain || domain.trim().length === 0) { + reply.code(400); + return { + success: false, + error: 'Domain is required', + timestamp: new Date().toISOString() + }; + } + + app.log.info(`Validating adagents.json for domain: ${domain}`); + + // Validate the domain's adagents.json + const validation = await adagentsManager.validateDomain(domain); + + let agentCards = undefined; + + // If adagents.json is found and has agents, validate their cards + if (validation.valid && validation.raw_data?.authorized_agents?.length > 0) { + app.log.info(`Validating ${validation.raw_data.authorized_agents.length} agent cards`); + agentCards = await adagentsManager.validateAgentCards(validation.raw_data.authorized_agents); + } + + return { + success: true, + data: { + domain: validation.domain, + found: validation.status_code === 200, + validation, + agent_cards: agentCards + }, + timestamp: new Date().toISOString() + }; + + } catch (error) { + app.log.error('Failed to validate domain: ' + (error instanceof Error ? error.message : String(error))); + reply.code(500); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + timestamp: new Date().toISOString() + }; + } +}); + +// Create adagents.json file +app.post<{ + Body: CreateAdAgentsRequest; + Reply: ApiResponse; +}>('/api/adagents/create', async (request, reply) => { + try { + const { authorized_agents, include_schema = true, include_timestamp = true } = request.body; + + if (!authorized_agents || !Array.isArray(authorized_agents)) { + reply.code(400); + return { + success: false, + error: 'authorized_agents array is required', + timestamp: new Date().toISOString() + }; + } + + if (authorized_agents.length === 0) { + reply.code(400); + return { + success: false, + error: 'At least one authorized agent is required', + timestamp: new Date().toISOString() + }; + } + + app.log.info(`Creating adagents.json with ${authorized_agents.length} agents`); + + // Validate the proposed structure + const validation = adagentsManager.validateProposed(authorized_agents); + + if (!validation.valid) { + reply.code(400); + return { + success: false, + error: `Validation failed: ${validation.errors.map(e => e.message).join(', ')}`, + timestamp: new Date().toISOString() + }; + } + + // Create the adagents.json content + const adagentsJson = adagentsManager.createAdAgentsJson( + authorized_agents, + include_schema, + include_timestamp + ); + + return { + success: true, + data: { + success: true, + adagents_json: adagentsJson, + validation + }, + timestamp: new Date().toISOString() + }; + + } catch (error) { + app.log.error('Failed to create adagents.json: ' + (error instanceof Error ? error.message : String(error))); + reply.code(500); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + timestamp: new Date().toISOString() + }; + } +}); + +// Validate agent cards only (utility endpoint) +app.post<{ + Body: { agent_urls: string[] }; + Reply: ApiResponse<{ agent_cards: any[] }>; +}>('/api/adagents/validate-cards', async (request, reply) => { + try { + const { agent_urls } = request.body; + + if (!agent_urls || !Array.isArray(agent_urls) || agent_urls.length === 0) { + reply.code(400); + return { + success: false, + error: 'agent_urls array with at least one URL is required', + timestamp: new Date().toISOString() + }; + } + + app.log.info(`Validating ${agent_urls.length} agent cards`); + + const agents = agent_urls.map(url => ({ url, authorized_for: 'validation' })); + const agentCards = await adagentsManager.validateAgentCards(agents); + + return { + success: true, + data: { + agent_cards: agentCards + }, + timestamp: new Date().toISOString() + }; + + } catch (error) { + app.log.error('Failed to validate agent cards: ' + (error instanceof Error ? error.message : String(error))); + reply.code(500); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + timestamp: new Date().toISOString() + }; + } +}); + // Start server const start = async () => { try {