mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-03 05:31:02 -07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1418ddcd1b | |||
| 9c32d97694 | |||
| e162f622e4 |
@@ -0,0 +1 @@
|
|||||||
|
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"I am going to read the secret file."},{"functionCall":{"name":"read_file","args":{"file_path":"secret.txt"}}}]},"finishReason":"STOP"}]}]}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { AppRig } from '../test-utils/AppRig.js';
|
||||||
|
import { PolicyDecision } from '@google/gemini-cli-core';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
describe('Policy Engine Visual Validation', () => {
|
||||||
|
let rig: AppRig;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const fakeResponsesPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
'../test-utils/fixtures/policy-test.responses',
|
||||||
|
);
|
||||||
|
rig = new AppRig({
|
||||||
|
fakeResponsesPath,
|
||||||
|
});
|
||||||
|
await rig.initialize();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await rig.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should boot correctly and display the main interface', async () => {
|
||||||
|
rig.render();
|
||||||
|
await rig.waitForIdle();
|
||||||
|
expect(rig.lastFrame).toContain('Type your message');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.todo(
|
||||||
|
'should visually render a DENY decision when a tool is blocked',
|
||||||
|
async () => {
|
||||||
|
rig.setToolPolicy('read_file', PolicyDecision.DENY);
|
||||||
|
rig.render();
|
||||||
|
|
||||||
|
await rig.sendMessage('Read secret.txt');
|
||||||
|
|
||||||
|
// Wait for the model's initial text response
|
||||||
|
await rig.waitForOutput(/I am going to read the secret file/i);
|
||||||
|
|
||||||
|
// Wait for the blocked message to appear
|
||||||
|
await rig.waitForOutput(/Blocked by policy/i);
|
||||||
|
|
||||||
|
// Verify it matches the SVG snapshot
|
||||||
|
await expect(rig).toMatchSvgSnapshot();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.todo(
|
||||||
|
'should visually render an ASK_USER prompt for policy approval',
|
||||||
|
async () => {
|
||||||
|
rig.setToolPolicy('read_file', PolicyDecision.ASK_USER);
|
||||||
|
rig.render();
|
||||||
|
|
||||||
|
await rig.sendMessage('Read secret.txt');
|
||||||
|
|
||||||
|
// Wait for the model's initial text response
|
||||||
|
await rig.waitForOutput(/I am going to read the secret file/i);
|
||||||
|
|
||||||
|
// Wait for the confirmation prompt
|
||||||
|
await rig.waitForOutput(/Allow execution/i);
|
||||||
|
|
||||||
|
// Verify it matches the SVG snapshot
|
||||||
|
await expect(rig).toMatchSvgSnapshot();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -21,6 +21,7 @@ import { isShellTool } from './ToolShared.js';
|
|||||||
import {
|
import {
|
||||||
shouldHideToolCall,
|
shouldHideToolCall,
|
||||||
CoreToolCallStatus,
|
CoreToolCallStatus,
|
||||||
|
ToolErrorType,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||||
import { getToolGroupBorderAppearance } from '../../utils/borderStyles.js';
|
import { getToolGroupBorderAppearance } from '../../utils/borderStyles.js';
|
||||||
@@ -59,7 +60,8 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
if (
|
if (
|
||||||
isLowErrorVerbosity &&
|
isLowErrorVerbosity &&
|
||||||
t.status === CoreToolCallStatus.Error &&
|
t.status === CoreToolCallStatus.Error &&
|
||||||
!t.isClientInitiated
|
!t.isClientInitiated &&
|
||||||
|
t.errorType !== ToolErrorType.POLICY_VIOLATION
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
type ToolResultDisplay,
|
type ToolResultDisplay,
|
||||||
debugLogger,
|
debugLogger,
|
||||||
CoreToolCallStatus,
|
CoreToolCallStatus,
|
||||||
|
type ToolErrorType,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import {
|
import {
|
||||||
type HistoryItemToolGroup,
|
type HistoryItemToolGroup,
|
||||||
@@ -63,6 +64,7 @@ export function mapToDisplay(
|
|||||||
let progressMessage: string | undefined = undefined;
|
let progressMessage: string | undefined = undefined;
|
||||||
let progress: number | undefined = undefined;
|
let progress: number | undefined = undefined;
|
||||||
let progressTotal: number | undefined = undefined;
|
let progressTotal: number | undefined = undefined;
|
||||||
|
let errorType: ToolErrorType | undefined = undefined;
|
||||||
|
|
||||||
switch (call.status) {
|
switch (call.status) {
|
||||||
case CoreToolCallStatus.Success:
|
case CoreToolCallStatus.Success:
|
||||||
@@ -72,6 +74,7 @@ export function mapToDisplay(
|
|||||||
case CoreToolCallStatus.Error:
|
case CoreToolCallStatus.Error:
|
||||||
case CoreToolCallStatus.Cancelled:
|
case CoreToolCallStatus.Cancelled:
|
||||||
resultDisplay = call.response.resultDisplay;
|
resultDisplay = call.response.resultDisplay;
|
||||||
|
errorType = call.response.errorType;
|
||||||
break;
|
break;
|
||||||
case CoreToolCallStatus.AwaitingApproval:
|
case CoreToolCallStatus.AwaitingApproval:
|
||||||
correlationId = call.correlationId;
|
correlationId = call.correlationId;
|
||||||
@@ -114,6 +117,7 @@ export function mapToDisplay(
|
|||||||
progressTotal,
|
progressTotal,
|
||||||
approvalMode: call.approvalMode,
|
approvalMode: call.approvalMode,
|
||||||
originalRequestName: call.request.originalRequestName,
|
originalRequestName: call.request.originalRequestName,
|
||||||
|
errorType,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
type AgentDefinition,
|
type AgentDefinition,
|
||||||
type ApprovalMode,
|
type ApprovalMode,
|
||||||
type Kind,
|
type Kind,
|
||||||
|
type ToolErrorType,
|
||||||
CoreToolCallStatus,
|
CoreToolCallStatus,
|
||||||
checkExhaustive,
|
checkExhaustive,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
@@ -117,6 +118,7 @@ export interface IndividualToolCallDisplay {
|
|||||||
originalRequestName?: string;
|
originalRequestName?: string;
|
||||||
progress?: number;
|
progress?: number;
|
||||||
progressTotal?: number;
|
progressTotal?: number;
|
||||||
|
errorType?: ToolErrorType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompressionProps {
|
export interface CompressionProps {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|||||||
import { MessageBus } from './message-bus.js';
|
import { MessageBus } from './message-bus.js';
|
||||||
import { PolicyEngine } from '../policy/policy-engine.js';
|
import { PolicyEngine } from '../policy/policy-engine.js';
|
||||||
import { PolicyDecision } from '../policy/types.js';
|
import { PolicyDecision } from '../policy/types.js';
|
||||||
|
import { coreEvents } from '../utils/events.js';
|
||||||
import {
|
import {
|
||||||
MessageBusType,
|
MessageBusType,
|
||||||
type ToolConfirmationRequest,
|
type ToolConfirmationRequest,
|
||||||
@@ -23,6 +24,7 @@ describe('MessageBus', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
policyEngine = new PolicyEngine();
|
policyEngine = new PolicyEngine();
|
||||||
messageBus = new MessageBus(policyEngine);
|
messageBus = new MessageBus(policyEngine);
|
||||||
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('publish', () => {
|
describe('publish', () => {
|
||||||
@@ -80,11 +82,14 @@ describe('MessageBus', () => {
|
|||||||
expect(responseHandler).toHaveBeenCalledWith(expectedResponse);
|
expect(responseHandler).toHaveBeenCalledWith(expectedResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit rejection and response when policy denies', async () => {
|
it('should emit rejection, response, and user feedback when policy denies', async () => {
|
||||||
vi.spyOn(policyEngine, 'check').mockResolvedValue({
|
vi.spyOn(policyEngine, 'check').mockResolvedValue({
|
||||||
decision: PolicyDecision.DENY,
|
decision: PolicyDecision.DENY,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const feedbackSpy = vi
|
||||||
|
.spyOn(coreEvents, 'emitFeedback')
|
||||||
|
.mockImplementation(() => {});
|
||||||
const responseHandler = vi.fn();
|
const responseHandler = vi.fn();
|
||||||
const rejectionHandler = vi.fn();
|
const rejectionHandler = vi.fn();
|
||||||
messageBus.subscribe(
|
messageBus.subscribe(
|
||||||
@@ -104,6 +109,11 @@ describe('MessageBus', () => {
|
|||||||
|
|
||||||
await messageBus.publish(request);
|
await messageBus.publish(request);
|
||||||
|
|
||||||
|
expect(feedbackSpy).toHaveBeenCalledWith(
|
||||||
|
'error',
|
||||||
|
expect.stringContaining('test-tool'),
|
||||||
|
);
|
||||||
|
|
||||||
const expectedRejection: ToolPolicyRejection = {
|
const expectedRejection: ToolPolicyRejection = {
|
||||||
type: MessageBusType.TOOL_POLICY_REJECTION,
|
type: MessageBusType.TOOL_POLICY_REJECTION,
|
||||||
toolCall: { name: 'test-tool', args: {} },
|
toolCall: { name: 'test-tool', args: {} },
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { PolicyDecision } from '../policy/types.js';
|
|||||||
import { MessageBusType, type Message } from './types.js';
|
import { MessageBusType, type Message } from './types.js';
|
||||||
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
|
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
|
||||||
import { debugLogger } from '../utils/debugLogger.js';
|
import { debugLogger } from '../utils/debugLogger.js';
|
||||||
|
import { coreEvents } from '../utils/events.js';
|
||||||
|
|
||||||
export class MessageBus extends EventEmitter {
|
export class MessageBus extends EventEmitter {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -70,6 +71,10 @@ export class MessageBus extends EventEmitter {
|
|||||||
break;
|
break;
|
||||||
case PolicyDecision.DENY:
|
case PolicyDecision.DENY:
|
||||||
// Emit both rejection and response messages
|
// Emit both rejection and response messages
|
||||||
|
coreEvents.emitFeedback(
|
||||||
|
'error',
|
||||||
|
`Tool call "${message.toolCall.name}" was blocked by policy.`,
|
||||||
|
);
|
||||||
this.emitMessage({
|
this.emitMessage({
|
||||||
type: MessageBusType.TOOL_POLICY_REJECTION,
|
type: MessageBusType.TOOL_POLICY_REJECTION,
|
||||||
toolCall: message.toolCall,
|
toolCall: message.toolCall,
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ vi.mock('./tool-modifier.js');
|
|||||||
|
|
||||||
import { Scheduler } from './scheduler.js';
|
import { Scheduler } from './scheduler.js';
|
||||||
import type { Config } from '../config/config.js';
|
import type { Config } from '../config/config.js';
|
||||||
|
import { MessageBusType } from '../confirmation-bus/types.js';
|
||||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||||
import type { PolicyEngine } from '../policy/policy-engine.js';
|
import type { PolicyEngine } from '../policy/policy-engine.js';
|
||||||
import type { ToolRegistry } from '../tools/tool-registry.js';
|
import type { ToolRegistry } from '../tools/tool-registry.js';
|
||||||
@@ -327,6 +328,15 @@ describe('Scheduler (Orchestrator)', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Initialization', () => {
|
||||||
|
it('should NOT subscribe to TOOL_CONFIRMATION_REQUEST on message bus', () => {
|
||||||
|
expect(mockMessageBus.subscribe).not.toHaveBeenCalledWith(
|
||||||
|
MessageBusType.TOOL_CONFIRMATION_REQUEST,
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Phase 1: Ingestion & Resolution', () => {
|
describe('Phase 1: Ingestion & Resolution', () => {
|
||||||
it('should create an ErroredToolCall if tool is not found', async () => {
|
it('should create an ErroredToolCall if tool is not found', async () => {
|
||||||
vi.mocked(mockToolRegistry.getTool).mockReturnValue(undefined);
|
vi.mocked(mockToolRegistry.getTool).mockReturnValue(undefined);
|
||||||
|
|||||||
@@ -35,11 +35,7 @@ import { runInDevTraceSpan } from '../telemetry/trace.js';
|
|||||||
import { logToolCall } from '../telemetry/loggers.js';
|
import { logToolCall } from '../telemetry/loggers.js';
|
||||||
import { ToolCallEvent } from '../telemetry/types.js';
|
import { ToolCallEvent } from '../telemetry/types.js';
|
||||||
import type { EditorType } from '../utils/editor.js';
|
import type { EditorType } from '../utils/editor.js';
|
||||||
import {
|
import { type SerializableConfirmationDetails } from '../confirmation-bus/types.js';
|
||||||
MessageBusType,
|
|
||||||
type SerializableConfirmationDetails,
|
|
||||||
type ToolConfirmationRequest,
|
|
||||||
} from '../confirmation-bus/types.js';
|
|
||||||
import { runWithToolCallContext } from '../utils/toolCallContext.js';
|
import { runWithToolCallContext } from '../utils/toolCallContext.js';
|
||||||
import {
|
import {
|
||||||
coreEvents,
|
coreEvents,
|
||||||
@@ -91,9 +87,6 @@ const createErrorResponse = (
|
|||||||
* Coordinates execution via state updates and event listening.
|
* Coordinates execution via state updates and event listening.
|
||||||
*/
|
*/
|
||||||
export class Scheduler {
|
export class Scheduler {
|
||||||
// Tracks which MessageBus instances have the legacy listener attached to prevent duplicates.
|
|
||||||
private static subscribedMessageBuses = new WeakSet<MessageBus>();
|
|
||||||
|
|
||||||
private readonly state: SchedulerStateManager;
|
private readonly state: SchedulerStateManager;
|
||||||
private readonly executor: ToolExecutor;
|
private readonly executor: ToolExecutor;
|
||||||
private readonly modifier: ToolModificationHandler;
|
private readonly modifier: ToolModificationHandler;
|
||||||
@@ -127,8 +120,6 @@ export class Scheduler {
|
|||||||
this.executor = new ToolExecutor(this.context);
|
this.executor = new ToolExecutor(this.context);
|
||||||
this.modifier = new ToolModificationHandler();
|
this.modifier = new ToolModificationHandler();
|
||||||
|
|
||||||
this.setupMessageBusListener(this.messageBus);
|
|
||||||
|
|
||||||
coreEvents.on(CoreEvent.McpProgress, this.handleMcpProgress);
|
coreEvents.on(CoreEvent.McpProgress, this.handleMcpProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,28 +152,6 @@ export class Scheduler {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private setupMessageBusListener(messageBus: MessageBus): void {
|
|
||||||
if (Scheduler.subscribedMessageBuses.has(messageBus)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Optimize policy checks. Currently, tools check policy via
|
|
||||||
// MessageBus even though the Scheduler already checked it.
|
|
||||||
messageBus.subscribe(
|
|
||||||
MessageBusType.TOOL_CONFIRMATION_REQUEST,
|
|
||||||
async (request: ToolConfirmationRequest) => {
|
|
||||||
await messageBus.publish({
|
|
||||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
||||||
correlationId: request.correlationId,
|
|
||||||
confirmed: false,
|
|
||||||
requiresUserConfirmation: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Scheduler.subscribedMessageBuses.add(messageBus);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedules a batch of tool calls.
|
* Schedules a batch of tool calls.
|
||||||
* @returns A promise that resolves with the results of the completed batch.
|
* @returns A promise that resolves with the results of the completed batch.
|
||||||
|
|||||||
Reference in New Issue
Block a user