mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-22 11:04:42 -07:00
feat(security): Introduce Conseca framework (#13193)
This commit is contained in:
committed by
GitHub
parent
05bc0399f3
commit
dde844dbe1
@@ -115,6 +115,8 @@ export enum EventNames {
|
||||
TOOL_OUTPUT_MASKING = 'tool_output_masking',
|
||||
KEYCHAIN_AVAILABILITY = 'keychain_availability',
|
||||
TOKEN_STORAGE_INITIALIZATION = 'token_storage_initialization',
|
||||
CONSECA_POLICY_GENERATION = 'conseca_policy_generation',
|
||||
CONSECA_VERDICT = 'conseca_verdict',
|
||||
}
|
||||
|
||||
export interface LogResponse {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// Defines valid event metadata keys for Clearcut logging.
|
||||
export enum EventMetadataKey {
|
||||
// Deleted enums: 24
|
||||
// Next ID: 159
|
||||
// Next ID: 167
|
||||
|
||||
GEMINI_CLI_KEY_UNKNOWN = 0,
|
||||
|
||||
@@ -605,4 +605,30 @@ export enum EventMetadataKey {
|
||||
|
||||
// Logs whether the token storage type was forced by an environment variable.
|
||||
GEMINI_CLI_TOKEN_STORAGE_FORCED = 158,
|
||||
// Conseca Event Keys
|
||||
// ==========================================================================
|
||||
|
||||
// Logs the policy generation event.
|
||||
CONSECA_POLICY_GENERATION = 159,
|
||||
|
||||
// Logs the verdict event.
|
||||
CONSECA_VERDICT = 160,
|
||||
|
||||
// Logs the generated policy content.
|
||||
CONSECA_GENERATED_POLICY = 161,
|
||||
|
||||
// Logs the verdict result (e.g. ALLOW/BLOCK).
|
||||
CONSECA_VERDICT_RESULT = 162,
|
||||
|
||||
// Logs the verdict rationale.
|
||||
CONSECA_VERDICT_RATIONALE = 163,
|
||||
|
||||
// Logs the trusted content used.
|
||||
CONSECA_TRUSTED_CONTENT = 164,
|
||||
|
||||
// Logs the user prompt for Conseca events.
|
||||
CONSECA_USER_PROMPT = 165,
|
||||
|
||||
// Logs the error message for Conseca events.
|
||||
CONSECA_ERROR = 166,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { logs, type Logger } from '@opentelemetry/api-logs';
|
||||
import {
|
||||
logConsecaPolicyGeneration,
|
||||
logConsecaVerdict,
|
||||
} from './conseca-logger.js';
|
||||
import {
|
||||
ConsecaPolicyGenerationEvent,
|
||||
ConsecaVerdictEvent,
|
||||
EVENT_CONSECA_POLICY_GENERATION,
|
||||
EVENT_CONSECA_VERDICT,
|
||||
} from './types.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
import * as sdk from './sdk.js';
|
||||
import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js';
|
||||
|
||||
vi.mock('@opentelemetry/api-logs');
|
||||
vi.mock('./sdk.js');
|
||||
vi.mock('./clearcut-logger/clearcut-logger.js');
|
||||
|
||||
describe('conseca-logger', () => {
|
||||
let mockConfig: Config;
|
||||
let mockLogger: { emit: ReturnType<typeof vi.fn> };
|
||||
let mockClearcutLogger: {
|
||||
enqueueLogEvent: ReturnType<typeof vi.fn>;
|
||||
createLogEvent: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfig = {
|
||||
getTelemetryEnabled: vi.fn().mockReturnValue(true),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(true),
|
||||
isInteractive: vi.fn().mockReturnValue(true),
|
||||
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
|
||||
} as unknown as Config;
|
||||
|
||||
mockLogger = {
|
||||
emit: vi.fn(),
|
||||
};
|
||||
vi.mocked(logs.getLogger).mockReturnValue(mockLogger as unknown as Logger);
|
||||
vi.mocked(sdk.isTelemetrySdkInitialized).mockReturnValue(true);
|
||||
|
||||
mockClearcutLogger = {
|
||||
enqueueLogEvent: vi.fn(),
|
||||
createLogEvent: vi.fn().mockReturnValue({ event_name: 'test' }),
|
||||
};
|
||||
vi.mocked(ClearcutLogger.getInstance).mockReturnValue(
|
||||
mockClearcutLogger as unknown as ClearcutLogger,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should log policy generation event to OTEL and Clearcut', () => {
|
||||
const event = new ConsecaPolicyGenerationEvent(
|
||||
'user prompt',
|
||||
'trusted content',
|
||||
'generated policy',
|
||||
);
|
||||
|
||||
logConsecaPolicyGeneration(mockConfig, event);
|
||||
|
||||
// Verify OTEL
|
||||
expect(logs.getLogger).toHaveBeenCalled();
|
||||
expect(mockLogger.emit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: 'Conseca Policy Generation.',
|
||||
attributes: expect.objectContaining({
|
||||
'event.name': EVENT_CONSECA_POLICY_GENERATION,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
// Verify Clearcut
|
||||
expect(ClearcutLogger.getInstance).toHaveBeenCalledWith(mockConfig);
|
||||
expect(mockClearcutLogger.createLogEvent).toHaveBeenCalled();
|
||||
expect(mockClearcutLogger.enqueueLogEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log policy generation error to Clearcut', () => {
|
||||
const event = new ConsecaPolicyGenerationEvent(
|
||||
'user prompt',
|
||||
'trusted content',
|
||||
'{}',
|
||||
'some error',
|
||||
);
|
||||
|
||||
logConsecaPolicyGeneration(mockConfig, event);
|
||||
|
||||
expect(mockClearcutLogger.createLogEvent).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
value: 'some error',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should log verdict event to OTEL and Clearcut', () => {
|
||||
const event = new ConsecaVerdictEvent(
|
||||
'user prompt',
|
||||
'policy',
|
||||
'tool call',
|
||||
'ALLOW',
|
||||
'rationale',
|
||||
);
|
||||
|
||||
logConsecaVerdict(mockConfig, event);
|
||||
|
||||
// Verify OTEL
|
||||
expect(logs.getLogger).toHaveBeenCalled();
|
||||
expect(mockLogger.emit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: 'Conseca Verdict: ALLOW.',
|
||||
attributes: expect.objectContaining({
|
||||
'event.name': EVENT_CONSECA_VERDICT,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
// Verify Clearcut
|
||||
expect(ClearcutLogger.getInstance).toHaveBeenCalledWith(mockConfig);
|
||||
expect(mockClearcutLogger.createLogEvent).toHaveBeenCalled();
|
||||
expect(mockClearcutLogger.enqueueLogEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not log if SDK is not initialized', () => {
|
||||
vi.mocked(sdk.isTelemetrySdkInitialized).mockReturnValue(false);
|
||||
const event = new ConsecaPolicyGenerationEvent('a', 'b', 'c');
|
||||
|
||||
logConsecaPolicyGeneration(mockConfig, event);
|
||||
|
||||
expect(mockLogger.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { LogRecord } from '@opentelemetry/api-logs';
|
||||
import { logs } from '@opentelemetry/api-logs';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { SERVICE_NAME } from './constants.js';
|
||||
import { isTelemetrySdkInitialized } from './sdk.js';
|
||||
import {
|
||||
ClearcutLogger,
|
||||
EventNames,
|
||||
} from './clearcut-logger/clearcut-logger.js';
|
||||
import { EventMetadataKey } from './clearcut-logger/event-metadata-key.js';
|
||||
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
|
||||
import type {
|
||||
ConsecaPolicyGenerationEvent,
|
||||
ConsecaVerdictEvent,
|
||||
} from './types.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
|
||||
export function logConsecaPolicyGeneration(
|
||||
config: Config,
|
||||
event: ConsecaPolicyGenerationEvent,
|
||||
): void {
|
||||
debugLogger.debug('Conseca Policy Generation Event:', event);
|
||||
const clearcutLogger = ClearcutLogger.getInstance(config);
|
||||
if (clearcutLogger) {
|
||||
const data = [
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_USER_PROMPT,
|
||||
value: safeJsonStringify(event.user_prompt),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_TRUSTED_CONTENT,
|
||||
value: safeJsonStringify(event.trusted_content),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_GENERATED_POLICY,
|
||||
value: safeJsonStringify(event.policy),
|
||||
},
|
||||
];
|
||||
|
||||
if (event.error) {
|
||||
data.push({
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_ERROR,
|
||||
value: event.error,
|
||||
});
|
||||
}
|
||||
|
||||
clearcutLogger.enqueueLogEvent(
|
||||
clearcutLogger.createLogEvent(EventNames.CONSECA_POLICY_GENERATION, data),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
}
|
||||
|
||||
export function logConsecaVerdict(
|
||||
config: Config,
|
||||
event: ConsecaVerdictEvent,
|
||||
): void {
|
||||
debugLogger.debug('Conseca Verdict Event:', event);
|
||||
const clearcutLogger = ClearcutLogger.getInstance(config);
|
||||
if (clearcutLogger) {
|
||||
const data = [
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_USER_PROMPT,
|
||||
value: safeJsonStringify(event.user_prompt),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_GENERATED_POLICY,
|
||||
value: safeJsonStringify(event.policy),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_NAME,
|
||||
value: safeJsonStringify(event.tool_call),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_VERDICT_RESULT,
|
||||
value: safeJsonStringify(event.verdict),
|
||||
},
|
||||
{
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_VERDICT_RATIONALE,
|
||||
value: event.verdict_rationale,
|
||||
},
|
||||
];
|
||||
|
||||
if (event.error) {
|
||||
data.push({
|
||||
gemini_cli_key: EventMetadataKey.CONSECA_ERROR,
|
||||
value: event.error,
|
||||
});
|
||||
}
|
||||
|
||||
clearcutLogger.enqueueLogEvent(
|
||||
clearcutLogger.createLogEvent(EventNames.CONSECA_VERDICT, data),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
}
|
||||
@@ -48,6 +48,10 @@ export {
|
||||
logWebFetchFallbackAttempt,
|
||||
logRewind,
|
||||
} from './loggers.js';
|
||||
export {
|
||||
logConsecaPolicyGeneration,
|
||||
logConsecaVerdict,
|
||||
} from './conseca-logger.js';
|
||||
export type { SlashCommandEvent, ChatCompressionEvent } from './types.js';
|
||||
export {
|
||||
SlashCommandStatus,
|
||||
@@ -64,6 +68,8 @@ export {
|
||||
WebFetchFallbackAttemptEvent,
|
||||
ToolCallDecision,
|
||||
RewindEvent,
|
||||
ConsecaPolicyGenerationEvent,
|
||||
ConsecaVerdictEvent,
|
||||
} from './types.js';
|
||||
export { LlmRole } from './llmRole.js';
|
||||
export { makeSlashCommandEvent, makeChatCompressionEvent } from './types.js';
|
||||
|
||||
@@ -879,6 +879,105 @@ export class NextSpeakerCheckEvent implements BaseTelemetryEvent {
|
||||
}
|
||||
}
|
||||
|
||||
export const EVENT_CONSECA_POLICY_GENERATION =
|
||||
'gemini_cli.conseca.policy_generation';
|
||||
export class ConsecaPolicyGenerationEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'conseca_policy_generation';
|
||||
'event.timestamp': string;
|
||||
user_prompt: string;
|
||||
trusted_content: string;
|
||||
policy: string;
|
||||
error?: string;
|
||||
|
||||
constructor(
|
||||
user_prompt: string,
|
||||
trusted_content: string,
|
||||
policy: string,
|
||||
error?: string,
|
||||
) {
|
||||
this['event.name'] = 'conseca_policy_generation';
|
||||
this['event.timestamp'] = new Date().toISOString();
|
||||
this.user_prompt = user_prompt;
|
||||
this.trusted_content = trusted_content;
|
||||
this.policy = policy;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
toOpenTelemetryAttributes(config: Config): LogAttributes {
|
||||
const attributes: LogAttributes = {
|
||||
...getCommonAttributes(config),
|
||||
'event.name': EVENT_CONSECA_POLICY_GENERATION,
|
||||
'event.timestamp': this['event.timestamp'],
|
||||
user_prompt: this.user_prompt,
|
||||
trusted_content: this.trusted_content,
|
||||
policy: this.policy,
|
||||
};
|
||||
|
||||
if (this.error) {
|
||||
attributes['error'] = this.error;
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
toLogBody(): string {
|
||||
return `Conseca Policy Generation.`;
|
||||
}
|
||||
}
|
||||
|
||||
export const EVENT_CONSECA_VERDICT = 'gemini_cli.conseca.verdict';
|
||||
export class ConsecaVerdictEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'conseca_verdict';
|
||||
'event.timestamp': string;
|
||||
user_prompt: string;
|
||||
policy: string;
|
||||
tool_call: string;
|
||||
verdict: string;
|
||||
verdict_rationale: string;
|
||||
error?: string;
|
||||
|
||||
constructor(
|
||||
user_prompt: string,
|
||||
policy: string,
|
||||
tool_call: string,
|
||||
verdict: string,
|
||||
verdict_rationale: string,
|
||||
error?: string,
|
||||
) {
|
||||
this['event.name'] = 'conseca_verdict';
|
||||
this['event.timestamp'] = new Date().toISOString();
|
||||
this.user_prompt = user_prompt;
|
||||
this.policy = policy;
|
||||
this.tool_call = tool_call;
|
||||
this.verdict = verdict;
|
||||
this.verdict_rationale = verdict_rationale;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
toOpenTelemetryAttributes(config: Config): LogAttributes {
|
||||
const attributes: LogAttributes = {
|
||||
...getCommonAttributes(config),
|
||||
'event.name': EVENT_CONSECA_VERDICT,
|
||||
'event.timestamp': this['event.timestamp'],
|
||||
user_prompt: this.user_prompt,
|
||||
policy: this.policy,
|
||||
tool_call: this.tool_call,
|
||||
verdict: this.verdict,
|
||||
verdict_rationale: this.verdict_rationale,
|
||||
};
|
||||
|
||||
if (this.error) {
|
||||
attributes['error'] = this.error;
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
toLogBody(): string {
|
||||
return `Conseca Verdict: ${this.verdict}.`;
|
||||
}
|
||||
}
|
||||
|
||||
export const EVENT_SLASH_COMMAND = 'gemini_cli.slash_command';
|
||||
export interface SlashCommandEvent extends BaseTelemetryEvent {
|
||||
'event.name': 'slash_command';
|
||||
|
||||
Reference in New Issue
Block a user