feat(security): Introduce Conseca framework (#13193)

This commit is contained in:
Rishabh Khandelwal
2026-02-23 18:44:28 -08:00
committed by GitHub
parent 05bc0399f3
commit dde844dbe1
30 changed files with 1887 additions and 51 deletions
@@ -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);
}
+6
View File
@@ -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';
+99
View File
@@ -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';