mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-22 02:54:31 -07:00
feat(a2a): merge latest main and resolve agent manager conflicts
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user