From 9f5c35df3701946ac0a8a40d300cd37ff84a6a35 Mon Sep 17 00:00:00 2001 From: Alisa Novikova <62909685+alisa-alisa@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:34:06 -0700 Subject: [PATCH] feat(a2a): merge latest main and resolve agent manager conflicts --- packages/a2a-server/src/config/config.ts | 1 + packages/a2a-server/src/config/settings.ts | 3 ++ .../src/agents/a2a-client-manager.test.ts | 18 ++++++++++ .../core/src/agents/a2a-client-manager.ts | 5 +-- .../src/agents/acknowledgedAgents.test.ts | 11 +++++++ .../core/src/agents/acknowledgedAgents.ts | 12 +++++++ packages/core/src/agents/registry.test.ts | 33 ++++++++++++++++++- packages/core/src/agents/registry.ts | 26 ++++++++++++--- .../agents/registry_acknowledgement.test.ts | 22 +++++++++++-- 9 files changed, 121 insertions(+), 10 deletions(-) diff --git a/packages/a2a-server/src/config/config.ts b/packages/a2a-server/src/config/config.ts index 607695f173..4844bc3677 100644 --- a/packages/a2a-server/src/config/config.ts +++ b/packages/a2a-server/src/config/config.ts @@ -110,6 +110,7 @@ export async function loadConfig( interactive: !isHeadlessMode(), enableInteractiveShell: !isHeadlessMode(), ptyInfo: 'auto', + enableAgents: settings.experimental?.enableAgents ?? false, }; const fileService = new FileDiscoveryService(workspaceDir, { diff --git a/packages/a2a-server/src/config/settings.ts b/packages/a2a-server/src/config/settings.ts index da9db4e069..ced11a4daa 100644 --- a/packages/a2a-server/src/config/settings.ts +++ b/packages/a2a-server/src/config/settings.ts @@ -48,6 +48,9 @@ export interface Settings { enableRecursiveFileSearch?: boolean; customIgnoreFilePaths?: string[]; }; + experimental?: { + enableAgents?: boolean; + }; } export interface SettingsError { diff --git a/packages/core/src/agents/a2a-client-manager.test.ts b/packages/core/src/agents/a2a-client-manager.test.ts index 0a0aa4d956..abf164548a 100644 --- a/packages/core/src/agents/a2a-client-manager.test.ts +++ b/packages/core/src/agents/a2a-client-manager.test.ts @@ -186,6 +186,7 @@ describe('A2AClientManager', () => { }); it('should configure ClientFactory with REST, JSON-RPC, and gRPC transports', async () => { +<<<<<<< HEAD await manager.loadAgent('TestAgent', 'http://test.agent/card'); expect(ClientFactoryOptions.createFrom).toHaveBeenCalled(); }); @@ -195,6 +196,23 @@ describe('A2AClientManager', () => { await expect( manager.loadAgent('TestAgent', 'http://test.agent/card'), ).rejects.toThrow("Agent with name 'TestAgent' is already loaded."); +======= + await manager.loadAgent('TestAgent', 'http://test.agent/card'); + expect(ClientFactoryOptions.createFrom).toHaveBeenCalled(); + }); + + it('should return the cached card if an agent with the same name is already loaded (idempotent)', async () => { + const card1 = await manager.loadAgent( + 'TestAgent', + 'http://test.agent/card', + ); + const card2 = await manager.loadAgent( + 'TestAgent', + 'http://test.agent/card', + ); + expect(card1).toBe(card2); + expect(vi.mocked(DefaultAgentCardResolver)).toHaveBeenCalledTimes(1); +>>>>>>> 7af0b4745 }); it('should use native fetch by default', async () => { diff --git a/packages/core/src/agents/a2a-client-manager.ts b/packages/core/src/agents/a2a-client-manager.ts index 3a03c033d8..3480949527 100644 --- a/packages/core/src/agents/a2a-client-manager.ts +++ b/packages/core/src/agents/a2a-client-manager.ts @@ -109,8 +109,9 @@ export class A2AClientManager { agentCardUrl: string, authHandler?: AuthenticationHandler, ): Promise { - if (this.clients.has(name) && this.agentCards.has(name)) { - throw new Error(`Agent with name '${name}' is already loaded.`); + const existingCard = this.agentCards.get(name); + if (existingCard) { + return existingCard; } // Authenticated fetch for API calls (transports). diff --git a/packages/core/src/agents/acknowledgedAgents.test.ts b/packages/core/src/agents/acknowledgedAgents.test.ts index f6e45360db..3f01925ca1 100644 --- a/packages/core/src/agents/acknowledgedAgents.test.ts +++ b/packages/core/src/agents/acknowledgedAgents.test.ts @@ -63,6 +63,17 @@ describe('AcknowledgedAgentsService', () => { ); }); + it('should return true for acknowledged agent via isAcknowledgedSync', async () => { + const service = new AcknowledgedAgentsService(); + + await service.acknowledge('/project', 'AgentA', 'hash1'); + + expect(service.isAcknowledgedSync('/project', 'AgentA', 'hash1')).toBe(true); + expect(service.isAcknowledgedSync('/project', 'AgentA', 'hash2')).toBe( + false, + ); + }); + it('should load acknowledged agents from disk', async () => { const ackPath = Storage.getAcknowledgedAgentsPath(); const data = { diff --git a/packages/core/src/agents/acknowledgedAgents.ts b/packages/core/src/agents/acknowledgedAgents.ts index 98c90afb96..4d90b68022 100644 --- a/packages/core/src/agents/acknowledgedAgents.ts +++ b/packages/core/src/agents/acknowledgedAgents.ts @@ -66,6 +66,18 @@ export class AcknowledgedAgentsService { hash: string, ): Promise { await this.load(); + return this.isAcknowledgedSync(projectPath, agentName, hash); + } + + /** + * Synchronous check for acknowledgment. + * Note: Assumes load() has already been called and awaited (e.g. during registry init). + */ + isAcknowledgedSync( + projectPath: string, + agentName: string, + hash: string, + ): boolean { const projectAgents = this.acknowledgedAgents[projectPath]; if (!projectAgents) return false; return projectAgents[agentName] === hash; diff --git a/packages/core/src/agents/registry.test.ts b/packages/core/src/agents/registry.test.ts index 49786de4b0..9f260a92d8 100644 --- a/packages/core/src/agents/registry.test.ts +++ b/packages/core/src/agents/registry.test.ts @@ -29,7 +29,7 @@ import { SimpleExtensionLoader } from '../utils/extensionLoader.js'; import type { ToolRegistry } from '../tools/tool-registry.js'; import { ThinkingLevel } from '@google/genai'; import type { AcknowledgedAgentsService } from './acknowledgedAgents.js'; -import { PolicyDecision } from '../policy/types.js'; +import { PolicyDecision, ApprovalMode } from '../policy/types.js'; import { A2AAuthProviderFactory } from './auth-provider/factory.js'; import type { A2AAuthProvider } from './auth-provider/types.js'; @@ -1171,6 +1171,37 @@ describe('AgentRegistry', () => { }), ); }); + + it('should register remote agents with ALLOW decision in YOLO mode', async () => { + const remoteAgent: AgentDefinition = { + kind: 'remote', + name: 'YoloAgent', + description: 'A remote agent in YOLO mode', + agentCardUrl: 'https://example.com/card', + inputConfig: { inputSchema: { type: 'object' } }, + }; + + vi.mocked(A2AClientManager.getInstance).mockReturnValue({ + loadAgent: vi.fn().mockResolvedValue({ name: 'YoloAgent' }), + } as unknown as A2AClientManager); + + const policyEngine = mockConfig.getPolicyEngine(); + vi.spyOn(mockConfig, 'getApprovalMode').mockReturnValue( + ApprovalMode.YOLO, + ); + const addRuleSpy = vi.spyOn(policyEngine, 'addRule'); + + await registry.testRegisterAgent(remoteAgent); + + // In YOLO mode, even remote agents should be registered with ALLOW. + expect(addRuleSpy).toHaveBeenLastCalledWith( + expect.objectContaining({ + toolName: 'YoloAgent', + decision: PolicyDecision.ALLOW, + source: 'AgentRegistry (Dynamic)', + }), + ); + }); }); describe('reload', () => { diff --git a/packages/core/src/agents/registry.ts b/packages/core/src/agents/registry.ts index 6eb642da72..ee8133fe03 100644 --- a/packages/core/src/agents/registry.ts +++ b/packages/core/src/agents/registry.ts @@ -23,7 +23,11 @@ import { type ModelConfig, ModelConfigService, } from '../services/modelConfigService.js'; -import { PolicyDecision, PRIORITY_SUBAGENT_TOOL } from '../policy/types.js'; +import { + PolicyDecision, + PRIORITY_SUBAGENT_TOOL, + ApprovalMode, +} from '../policy/types.js'; import { A2AAgentError, AgentAuthConfigMissingError } from './a2a-errors.js'; /** @@ -176,9 +180,8 @@ export class AgentRegistry { agent.metadata.hash, ); - if (isAcknowledged) { - agentsToRegister.push(agent); - } else { + agentsToRegister.push(agent); + if (!isAcknowledged) { unacknowledgedAgents.push(agent); } } @@ -340,10 +343,23 @@ export class AgentRegistry { policyEngine.removeRulesForTool(definition.name, 'AgentRegistry (Dynamic)'); // Add the new dynamic policy + const isYolo = this.config.getApprovalMode() === ApprovalMode.YOLO; + const isAcknowledged = + definition.kind === 'local' && + (!definition.metadata?.hash || + (this.config.getProjectRoot() && + this.config + .getAcknowledgedAgentsService() + ?.isAcknowledgedSync?.( + this.config.getProjectRoot(), + definition.name, + definition.metadata.hash, + ))); + policyEngine.addRule({ toolName: definition.name, decision: - definition.kind === 'local' + isAcknowledged || isYolo ? PolicyDecision.ALLOW : PolicyDecision.ASK_USER, priority: PRIORITY_SUBAGENT_TOOL, diff --git a/packages/core/src/agents/registry_acknowledgement.test.ts b/packages/core/src/agents/registry_acknowledgement.test.ts index 5ac563091d..283767ae78 100644 --- a/packages/core/src/agents/registry_acknowledgement.test.ts +++ b/packages/core/src/agents/registry_acknowledgement.test.ts @@ -12,6 +12,7 @@ import { coreEvents } from '../utils/events.js'; import * as tomlLoader from './agentLoader.js'; import { type Config } from '../config/config.js'; import { AcknowledgedAgentsService } from './acknowledgedAgents.js'; +import { PolicyDecision } from '../policy/types.js'; import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import * as os from 'node:os'; @@ -103,13 +104,22 @@ describe('AgentRegistry Acknowledgement', () => { await fs.rm(tempDir, { recursive: true, force: true }); }); - it('should not register unacknowledged project agents and emit event', async () => { + it('should register unacknowledged project agents and emit event', async () => { const emitSpy = vi.spyOn(coreEvents, 'emitAgentsDiscovered'); await registry.initialize(); - expect(registry.getDefinition('ProjectAgent')).toBeUndefined(); + // Now unacknowledged agents ARE registered (but with ASK_USER policy) + expect(registry.getDefinition('ProjectAgent')).toBeDefined(); expect(emitSpy).toHaveBeenCalledWith([MOCK_AGENT_WITH_HASH]); + + // Verify policy + const policyEngine = config.getPolicyEngine(); + expect( + await policyEngine?.check({ name: 'ProjectAgent', args: {} }, undefined), + ).toMatchObject({ + decision: PolicyDecision.ASK_USER, + }); }); it('should register acknowledged project agents', async () => { @@ -134,6 +144,14 @@ describe('AgentRegistry Acknowledgement', () => { expect(registry.getDefinition('ProjectAgent')).toBeDefined(); expect(emitSpy).not.toHaveBeenCalled(); + + // Verify policy is ALLOW for acknowledged agent + const policyEngine = config.getPolicyEngine(); + expect( + await policyEngine?.check({ name: 'ProjectAgent', args: {} }, undefined), + ).toMatchObject({ + decision: PolicyDecision.ALLOW, + }); }); it('should register agents without hash (legacy/safe?)', async () => {