/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import type { InProcessChecker } from '../built-in.js'; import { SafetyCheckDecision, type SafetyCheckInput, type SafetyCheckResult, } from '../protocol.js'; import { logConsecaPolicyGeneration, ConsecaPolicyGenerationEvent, logConsecaVerdict, ConsecaVerdictEvent, } from '../../telemetry/index.js'; import { debugLogger } from '../../utils/debugLogger.js'; import type { Config } from '../../config/config.js'; import { generatePolicy } from './policy-generator.js'; import { enforcePolicy } from './policy-enforcer.js'; import type { SecurityPolicy } from './types.js'; import type { AgentLoopContext } from '../../config/agent-loop-context.js'; export class ConsecaSafetyChecker implements InProcessChecker { private static instance: ConsecaSafetyChecker | undefined; private currentPolicy: SecurityPolicy | null = null; private activeUserPrompt: string | null = null; private context: AgentLoopContext | null = null; /** * Private constructor to enforce singleton pattern. * Use `getInstance()` to access the instance. */ private constructor() {} static getInstance(): ConsecaSafetyChecker { if (!ConsecaSafetyChecker.instance) { ConsecaSafetyChecker.instance = new ConsecaSafetyChecker(); } return ConsecaSafetyChecker.instance; } /** * Resets the singleton instance. Use only in tests. */ static resetInstance(): void { ConsecaSafetyChecker.instance = undefined; } setContext(context: AgentLoopContext): void { this.context = context; } async check(input: SafetyCheckInput): Promise { debugLogger.debug( `[Conseca] check called. History is: ${JSON.stringify(input.context.history)}`, ); if (!this.context) { debugLogger.debug('[Conseca] check failed: Config not initialized'); return { decision: SafetyCheckDecision.ALLOW, reason: 'Config not initialized', }; } if (!this.context.config.enableConseca) { debugLogger.debug('[Conseca] check skipped: Conseca is not enabled.'); return { decision: SafetyCheckDecision.ALLOW, reason: 'Conseca is disabled', }; } const userPrompt = this.extractUserPrompt(input); let trustedContent = ''; const toolRegistry = this.context.toolRegistry; if (toolRegistry) { const tools = toolRegistry.getFunctionDeclarations(); trustedContent = JSON.stringify(tools, null, 2); } if (userPrompt) { await this.getPolicy(userPrompt, trustedContent, this.context.config); } else { debugLogger.debug( `[Conseca] Skipping policy generation because userPrompt is null`, ); } let result: SafetyCheckResult; if (!this.currentPolicy) { result = { decision: SafetyCheckDecision.ALLOW, // Fallback if no policy generated yet reason: 'No security policy generated.', error: 'No security policy generated.', }; } else { result = await enforcePolicy( this.currentPolicy, input.toolCall, this.context.config, ); } logConsecaVerdict( this.context.config, new ConsecaVerdictEvent( userPrompt || '', JSON.stringify(this.currentPolicy || {}), JSON.stringify(input.toolCall), result.decision, result.reason || '', 'error' in result ? result.error : undefined, ), ); return result; } async getPolicy( userPrompt: string, trustedContent: string, config: Config, ): Promise { if (this.activeUserPrompt === userPrompt && this.currentPolicy) { return this.currentPolicy; } const { policy, error } = await generatePolicy( userPrompt, trustedContent, config, ); this.currentPolicy = policy; this.activeUserPrompt = userPrompt; logConsecaPolicyGeneration( config, new ConsecaPolicyGenerationEvent( userPrompt, trustedContent, JSON.stringify(policy), error, ), ); return policy; } private extractUserPrompt(input: SafetyCheckInput): string | null { const prompt = input.context.history?.turns.at(-1)?.user.text; if (prompt) { return prompt; } debugLogger.debug(`[Conseca] extractUserPrompt failed.`); return null; } // Helper methods for testing state getCurrentPolicy(): SecurityPolicy | null { return this.currentPolicy; } getActiveUserPrompt(): string | null { return this.activeUserPrompt; } }