feat(a2a): merge latest main and resolve agent manager conflicts

This commit is contained in:
Alisa Novikova
2026-03-12 15:34:06 -07:00
parent d7d53981f3
commit 9f5c35df37
9 changed files with 121 additions and 10 deletions
+1
View File
@@ -110,6 +110,7 @@ export async function loadConfig(
interactive: !isHeadlessMode(),
enableInteractiveShell: !isHeadlessMode(),
ptyInfo: 'auto',
enableAgents: settings.experimental?.enableAgents ?? false,
};
const fileService = new FileDiscoveryService(workspaceDir, {
@@ -48,6 +48,9 @@ export interface Settings {
enableRecursiveFileSearch?: boolean;
customIgnoreFilePaths?: string[];
};
experimental?: {
enableAgents?: boolean;
};
}
export interface SettingsError {
@@ -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 () => {
@@ -109,8 +109,9 @@ export class A2AClientManager {
agentCardUrl: string,
authHandler?: AuthenticationHandler,
): Promise<AgentCard> {
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).
@@ -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 = {
@@ -66,6 +66,18 @@ export class AcknowledgedAgentsService {
hash: string,
): Promise<boolean> {
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;
+32 -1
View File
@@ -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', () => {
+21 -5
View File
@@ -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,
@@ -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 () => {