/** * Lightweight AgentBase for reference implementation. * * When instantiated by the platform Host, deps (AgentDeps from platform_impl) * is passed as the third constructor argument. AgentBase wires callTool to go * through the platform's MCPlet router with pool-access, rate-limit, and * Passkey enforcement — without importing platform types directly. * * In standalone/test mode, call bindCallTool() to inject a mock implementation. */ import { randomUUID } from 'node:crypto'; import type { A2ATaskRequest, A2ATaskResponse } from './platform-types.js'; // Structural shape of the platform deps we need (duck typing, no import required) interface PlatformDeps { mcpRouter?: { callTool?: ( toolName: string, args: Record, ) => Promise<{ content: Array<{ type: string; text?: string }> }>; }; poolRegistry?: { getAllTools?: () => Array<{ name: string; meta: { pool?: string; mcpletType: string; auth?: { required?: string; enforcement?: string; promptMessage?: string }; }; }>; canAgentAccess?: (agentId: string, pool: string | undefined, pools: string[]) => boolean; checkRateLimit?: (pool: string) => boolean; }; auditLog?: { record?: (entry: Record) => void; }; passkeyServer?: { startCeremony?: (message: string) => Promise; }; } export abstract class AgentBase { abstract readonly agentId: string; abstract readonly accessiblePools: string[]; abstract handle(task: A2ATaskRequest): Promise; protected callTool: ( toolName: string, args: Record, ) => Promise; /** Set by subclasses (or AgentBase.handle wrapper) before calling tools in a task. */ protected _currentContextId: string | undefined = undefined; /** Per-contextId assertion cache for 'workflow' enforcement tools. */ protected readonly _assertionCache = new Map(); // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(_agentId?: string, _accessiblePools?: string[], deps?: any) { if (deps) { this.callTool = buildPlatformCallTool(this as unknown as AgentBase & { agentId: string; accessiblePools: string[] }, deps as PlatformDeps); } else { this.callTool = async (toolName) => { throw new Error( `[agent-base] callTool("${toolName}") called without a bound implementation. ` + 'Inject via bindCallTool() or use the platform Host.', ); }; } } /** Bind a tool caller (used in standalone/test mode). */ bindCallTool(fn: (toolName: string, args: Record) => Promise): void { this.callTool = fn; } protected success(task: A2ATaskRequest, result: unknown): A2ATaskResponse { return { messageId: randomUUID(), contextId: task.contextId, senderId: this.agentId, recipientId: task.senderId, timestamp: new Date().toISOString(), type: 'task_response', replyToMessageId: task.messageId, status: 'success', payload: { result }, }; } protected error(task: A2ATaskRequest, message: string, code?: string): A2ATaskResponse { return { messageId: randomUUID(), contextId: task.contextId, senderId: this.agentId, recipientId: task.senderId, timestamp: new Date().toISOString(), type: 'task_response', replyToMessageId: task.messageId, status: 'error', payload: { error: { message, code } }, }; } protected cancelled(task: A2ATaskRequest, reason: string): A2ATaskResponse { return { messageId: randomUUID(), contextId: task.contextId, senderId: this.agentId, recipientId: task.senderId, timestamp: new Date().toISOString(), type: 'task_response', replyToMessageId: task.messageId, status: 'cancelled', payload: { error: { message: reason } }, }; } } /** * Build a callTool function that goes through the platform's MCPlet router * with pool-access, rate-limit, and Passkey enforcement. */ function buildPlatformCallTool( agent: AgentBase & { agentId: string; accessiblePools: string[] }, deps: PlatformDeps, ): (toolName: string, args: Record) => Promise { // Access protected cache fields via a typed cast (same class file, safe). const agentCache = agent as unknown as { _currentContextId: string | undefined; _assertionCache: Map; }; return async (toolName: string, args: Record) => { const tools = deps.poolRegistry?.getAllTools?.() ?? []; const tool = tools.find((t) => t.name === toolName); // Pool access check if (tool?.meta.pool) { const canAccess = deps.poolRegistry?.canAgentAccess?.(agent.agentId, tool.meta.pool, agent.accessiblePools) ?? true; if (!canAccess) { throw new Error(`[${agent.agentId}] Pool access denied for tool "${toolName}" (pool: ${tool.meta.pool})`); } } // Rate limit check if (tool?.meta.pool) { const allowed = deps.poolRegistry?.checkRateLimit?.(tool.meta.pool) ?? true; if (!allowed) { throw new Error(`[${agent.agentId}] Rate limit exceeded for pool "${tool.meta.pool}"`); } } // Passkey interception for action tools let callArgs = args; if (tool?.meta.mcpletType === 'action' && tool.meta.auth?.required === 'passkey') { const enforcement = tool.meta.auth.enforcement ?? 'strict'; let assertion: unknown; if (enforcement === 'workflow') { // Cache key: contextId (scopes assertion to one Director cycle) const contextId = agentCache._currentContextId ?? 'default'; if (agentCache._assertionCache.has(contextId)) { assertion = agentCache._assertionCache.get(contextId); console.log(`[${agent.agentId}] Reusing cached Passkey assertion for contextId="${contextId}" (tool: ${toolName})`); } else { assertion = await deps.passkeyServer?.startCeremony?.( tool.meta.auth.promptMessage ?? `Authorize workflow actions for this task`, ); if (!assertion) { throw new Error(`[${agent.agentId}] Passkey authentication cancelled for "${toolName}"`); } agentCache._assertionCache.set(contextId, assertion); console.log(`[${agent.agentId}] Passkey assertion cached for contextId="${contextId}"`); } } else { // 'strict': new ceremony per invocation assertion = await deps.passkeyServer?.startCeremony?.( tool.meta.auth.promptMessage ?? `Authorize action: ${toolName}`, ); if (!assertion) { throw new Error(`[${agent.agentId}] Passkey authentication cancelled for "${toolName}"`); } } callArgs = { ...args, _mcplet_auth: assertion }; } // Audit log for action tools if (tool?.meta.mcpletType === 'action') { deps.auditLog?.record?.({ type: 'action_invocation', agentId: agent.agentId, toolName, timestamp: new Date().toISOString(), }); } if (!deps.mcpRouter?.callTool) { throw new Error(`[${agent.agentId}] mcpRouter not available for tool "${toolName}"`); } const result = await deps.mcpRouter.callTool(toolName, callArgs); const text = result.content.find((c) => c.type === 'text')?.text ?? '{}'; let parsed: unknown; try { parsed = JSON.parse(text); } catch { return text; } // MCPlet server wraps results in { result, _meta } envelope — unwrap it. const envelope = parsed as { result?: unknown; error?: { message: string; code: string } }; if (envelope && typeof envelope === 'object' && 'error' in envelope && envelope.error) { throw new Error(`Tool "${toolName}" returned error: ${envelope.error.message} (${envelope.error.code})`); } if (envelope && typeof envelope === 'object' && 'result' in envelope) { return envelope.result; } return parsed; }; }