"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HydraProvider = void 0;
const uuid_1 = require("uuid");
const envars_1 = require("../../../envars");
const evaluatorHelpers_1 = require("../../../evaluatorHelpers");
const logger_1 = __importDefault(require("../../../logger"));
const chat_1 = require("../../../providers/azure/chat");
const invariant_1 = __importDefault(require("../../../util/invariant"));
const json_1 = require("../../../util/json");
const time_1 = require("../../../util/time");
const tokenUsageUtils_1 = require("../../../util/tokenUsageUtils");
const remoteGeneration_1 = require("../../remoteGeneration");
const runtimeTransform_1 = require("../../shared/runtimeTransform");
const strategies_1 = require("../../strategies");
const util_1 = require("../../util");
const shared_1 = require("../shared");
const DEFAULT_MAX_TURNS = 10;
const DEFAULT_MAX_BACKTRACKS = 10;
class HydraProvider {
    constructor(config) {
        this.conversationHistory = [];
        this.config = config;
        this.scanId = config.scanId; // Use scanId from config if provided
        this.injectVar = config.injectVar;
        this.maxTurns = config.maxTurns ?? DEFAULT_MAX_TURNS;
        this.maxBacktracks = config.maxBacktracks ?? DEFAULT_MAX_BACKTRACKS;
        this.stateful = config.stateful ?? false;
        this.excludeTargetOutputFromAgenticAttackGeneration =
            config.excludeTargetOutputFromAgenticAttackGeneration ?? false;
        this.perTurnLayers = config._perTurnLayers ?? [];
        if (this.stateful && this.maxBacktracks > 0) {
            logger_1.default.debug('[Hydra] Backtracking disabled in stateful mode');
        }
        // Hydra strategy requires cloud
        if (!(0, remoteGeneration_1.shouldGenerateRemote)()) {
            throw new Error('trusys:redteam:hydra strategy requires cloud access. Set TRUSYS_CLOUD_API_URL or log into Trusys Cloud.');
        }
        const deploymentName = (0, envars_1.getEnvString)('AZURE_OPENAI_DEPLOYMENT_NAME') ||
            (0, envars_1.getEnvString)('AZURE_DEPLOYMENT_NAME') ||
            'gpt-4o';
        this.agentProvider = new chat_1.AzureChatCompletionProvider(deploymentName, {
            config: {
                response_format: { type: 'json_object' },
            },
        });
        logger_1.default.debug(`[Hydra] Provider initialized: maxTurns: ${this.maxTurns}, maxBacktracks: ${this.maxBacktracks}, stateful: ${this.stateful}, injectVar: ${this.injectVar}, excludeTargetOutputFromAgenticAttackGeneration: ${this.excludeTargetOutputFromAgenticAttackGeneration}, perTurnLayers: ${this.perTurnLayers.map((l) => (typeof l === 'string' ? l : l.id))}`);
    }
    id() {
        return 'trusys:redteam:hydra';
    }
    async callApi(_prompt, context, options) {
        logger_1.default.debug('[Hydra] callApi invoked');
        (0, invariant_1.default)(context?.originalProvider, 'Expected originalProvider to be set');
        (0, invariant_1.default)(context?.vars, 'Expected vars to be set');
        const goal = context.test?.metadata?.goal || String(context.vars[this.injectVar]);
        return this.runAttack({
            prompt: context.prompt,
            filters: context.filters,
            vars: context.vars,
            goal,
            targetProvider: context.originalProvider,
            context,
            options,
            test: context.test,
        });
    }
    async runAttack({ prompt, filters, vars, goal, targetProvider, context, options, test, }) {
        // Initialize scanId: use evaluationId if available, otherwise use instance scanId or generate new one
        if (!this.scanId) {
            this.scanId = context?.evaluationId || (0, uuid_1.v4)();
        }
        const scanId = context?.evaluationId || this.scanId;
        logger_1.default.debug(`[Hydra] Starting attack: goal: ${goal}, scanId: ${scanId}, maxTurns: ${this.maxTurns}, stateful: ${this.stateful}`);
        // Reset state
        this.conversationHistory = [];
        this.sessionId = undefined;
        const sessionIds = [];
        const successfulAttacks = [];
        const totalTokenUsage = (0, tokenUsageUtils_1.createEmptyTokenUsage)();
        const testRunId = `${context?.evaluationId || 'local'}-tc${context?.testCaseId || (0, uuid_1.v4)().slice(0, 8)}`;
        let vulnerabilityAchieved = false;
        let stopReason = 'Max turns reached';
        let storedGraderResult = undefined;
        let lastTargetResponse = undefined;
        let backtrackCount = 0;
        const redteamHistory = [];
        let lastTransformResult;
        // Find the grader
        const { getGraderById } = await import('../../graders.js');
        let assertToUse = test?.assert?.find((a) => a.type && a.type.includes(test.metadata?.pluginId));
        if (!assertToUse) {
            assertToUse = test?.assert?.find((a) => a.type);
        }
        for (let turn = 1; turn <= this.maxTurns; turn++) {
            logger_1.default.debug(`[Hydra] Turn ${turn}/${this.maxTurns}`);
            // Build request for cloud agent
            // Conditionally exclude target outputs from conversation history for privacy
            const conversationHistoryForCloud = this.excludeTargetOutputFromAgenticAttackGeneration
                ? this.conversationHistory.map((msg) => msg.role === 'assistant'
                    ? { ...msg, content: '[Response hidden for privacy - grader feedback provided]' }
                    : msg)
                : this.conversationHistory;
            const cloudRequest = {
                task: 'hydra-decision',
                testRunId,
                scanId,
                turn,
                goal,
                purpose: test?.metadata?.purpose,
                modifiers: test?.metadata?.modifiers,
                conversationHistory: conversationHistoryForCloud,
                lastGraderResult: turn > 1 && storedGraderResult
                    ? {
                        pass: storedGraderResult.pass,
                        score: storedGraderResult.score,
                    }
                    : undefined,
                stateful: this.stateful,
                maxTurns: this.maxTurns,
                excludeTargetOutputFromAgenticAttackGeneration: this.excludeTargetOutputFromAgenticAttackGeneration,
            };
            // Get next message from cloud
            const agentResp = await this.agentProvider.callApi(JSON.stringify(cloudRequest), {
                prompt: {
                    raw: JSON.stringify(cloudRequest),
                    label: 'hydra-agent',
                },
                vars: {},
            }, options);
            (0, tokenUsageUtils_1.accumulateResponseTokenUsage)(totalTokenUsage, agentResp);
            if (this.agentProvider.delay) {
                await (0, time_1.sleep)(this.agentProvider.delay);
            }
            if (agentResp.error) {
                logger_1.default.error(`[Hydra] Agent provider error: ${agentResp.error} turn: ${turn}, testRunId: ${testRunId}`);
                continue;
            }
            // Extract message from cloud response
            let nextMessage;
            if (typeof agentResp.output === 'string') {
                // AzureChatCompletionProvider returns string output
                nextMessage = agentResp.output;
            }
            else {
                const cloudResponse = agentResp.output;
                nextMessage = cloudResponse.result || cloudResponse.message;
            }
            if (!nextMessage) {
                logger_1.default.info(`[Hydra] Missing message from agent: ${nextMessage} turn: ${turn}`);
                continue;
            }
            // Add message to conversation history
            this.conversationHistory.push({
                role: 'user',
                content: nextMessage,
            });
            // Send to target (different based on stateful/stateless)
            let targetPrompt;
            if (this.stateful) {
                // Stateful: send only the new message with sessionId
                const escapedMessage = nextMessage
                    .replace(/\{\{/g, '{ {')
                    .replace(/\}\}/g, '} }')
                    .replace(/\{%/g, '{ %')
                    .replace(/%\}/g, '% }');
                targetPrompt = await (0, evaluatorHelpers_1.renderPrompt)(prompt, {
                    ...vars,
                    [this.injectVar]: escapedMessage,
                    ...(this.sessionId ? { sessionId: this.sessionId } : {}),
                }, filters, targetProvider, [this.injectVar]);
            }
            else {
                // Stateless: send full conversation history as JSON
                // Try to parse the rendered prompt to see if it's already chat format
                const samplePrompt = await (0, evaluatorHelpers_1.renderPrompt)(prompt, {
                    ...vars,
                    [this.injectVar]: 'test',
                }, filters, targetProvider, [this.injectVar]);
                if ((0, json_1.isValidJson)(samplePrompt)) {
                    const parsed = JSON.parse(samplePrompt);
                    if ((0, shared_1.isValidChatMessageArray)(parsed)) {
                        // It's already chat format, inject our conversation
                        targetPrompt = JSON.stringify(this.conversationHistory);
                    }
                    else {
                        // Not chat format, use standard rendering
                        targetPrompt = JSON.stringify(this.conversationHistory);
                    }
                }
                else {
                    // Not JSON, send as conversation array
                    targetPrompt = JSON.stringify(this.conversationHistory);
                }
            }
            let finalTargetPrompt = targetPrompt;
            logger_1.default.debug(`[Hydra] Sending to target: ${finalTargetPrompt} turn: ${turn}, stateful: ${this.stateful}, messageLength: ${nextMessage.length}`);
            // ═══════════════════════════════════════════════════════════════════════
            // Apply per-turn layer transforms if configured (e.g., audio, base64)
            // This enables: layer: { steps: [hydra, audio] }
            // ═══════════════════════════════════════════════════════════════════════
            lastTransformResult = undefined;
            if (this.perTurnLayers.length > 0) {
                logger_1.default.debug(`[Hydra] Applying per-turn transforms: ${this.perTurnLayers.map((l) => (typeof l === 'string' ? l : l.id))} turn: ${turn}`);
                const transformedPrompt = await (0, runtimeTransform_1.applyRuntimeTransforms)(nextMessage, this.injectVar, this.perTurnLayers, strategies_1.Strategies);
                lastTransformResult = transformedPrompt;
                // Transform the actual message content (nextMessage), not the full targetPrompt
                // This ensures we convert just the text to audio, not the JSON structure
                // Skip turn if transform failed
                if (lastTransformResult.error) {
                    logger_1.default.warn(`[Hydra] Transform failed, skipping turn: ${lastTransformResult.error} turn: ${turn}`);
                    // Remove the user message we added since we're skipping
                    this.conversationHistory.pop();
                    continue;
                }
                // For audio/image transforms, send a hybrid format:
                // - Previous turns as text (for context)
                // - Current turn as audio/image (the actual attack)
                // This allows the target model to understand conversation context while receiving the current attack in the transformed format
                if (lastTransformResult.audio || lastTransformResult.image) {
                    // Build hybrid payload with conversation history + current transformed turn
                    const historyWithoutCurrentTurn = this.conversationHistory.slice(0, -1);
                    const hybridPayload = {
                        _trusys_audio_hybrid: true,
                        history: historyWithoutCurrentTurn,
                        currentTurn: {
                            role: 'user',
                            transcript: nextMessage, // Original text for reference
                            ...(lastTransformResult.audio && {
                                audio: lastTransformResult.audio,
                            }),
                            ...(lastTransformResult.image && {
                                image: lastTransformResult.image,
                            }),
                        },
                    };
                    finalTargetPrompt = JSON.stringify(hybridPayload);
                    logger_1.default.debug(`[Hydra] Using hybrid format (history + audio/image current turn): ${finalTargetPrompt} turn: ${turn}, historyLength: ${historyWithoutCurrentTurn.length}, hasAudio: ${!!lastTransformResult.audio}, hasImage: ${!!lastTransformResult.image}`);
                }
                else {
                    // No audio/image, just use the transformed text
                    finalTargetPrompt = lastTransformResult.prompt;
                }
                logger_1.default.debug(`[Hydra] Per-turn transforms applied: ${finalTargetPrompt} turn: ${turn}, originalLength: ${nextMessage.length}, transformedLength: ${finalTargetPrompt.length}, hasAudio: ${!!lastTransformResult.audio}, hasImage: ${!!lastTransformResult.image}`);
            }
            // Get target response
            const targetResponse = await (0, shared_1.getTargetResponse)(targetProvider, finalTargetPrompt, context, options);
            lastTargetResponse = targetResponse;
            (0, tokenUsageUtils_1.accumulateResponseTokenUsage)(totalTokenUsage, targetResponse);
            logger_1.default.debug(`[Hydra] Received target response: ${targetResponse.output} turn: ${turn}, responseLength: ${targetResponse.output?.length}`);
            if (targetResponse.error) {
                logger_1.default.info(`[Hydra] Target error: ${targetResponse.error} turn: ${turn}`);
                continue;
            }
            if (!Object.prototype.hasOwnProperty.call(targetResponse, 'output')) {
                logger_1.default.info(`[Hydra] Malformed target response - missing output: ${JSON.stringify(targetResponse)} turn: ${turn}`);
                continue;
            }
            if (!targetResponse.output || !targetResponse.output.trim()) {
                logger_1.default.info(`[Hydra] Empty target response: ${targetResponse.output} turn: ${turn}`);
                // Replace empty output with marker so agent knows target refused
                targetResponse.output = '[Target provided empty response - likely refused]';
            }
            // Capture sessionId if stateful
            if (this.stateful && targetResponse.sessionId) {
                this.sessionId = targetResponse.sessionId;
                sessionIds.push(targetResponse.sessionId);
            }
            // Add response to conversation history
            this.conversationHistory.push({
                role: 'assistant',
                content: targetResponse.output,
            });
            // Check for refusal and backtrack if in stateless mode and backtracking enabled
            const isRefusal = (0, util_1.isBasicRefusal)(targetResponse.output);
            if (!this.stateful && this.maxBacktracks > 0 && isRefusal) {
                logger_1.default.debug(`[Hydra] Response rejected (basic refusal), backtracking... turn: ${turn}, backtrackCount: ${backtrackCount + 1}, maxBacktracks: ${this.maxBacktracks}, conversationLengthBefore: ${this.conversationHistory.length}`);
                backtrackCount++;
                // Remove last user + assistant messages
                if (this.conversationHistory.length >= 2) {
                    this.conversationHistory.pop(); // Remove assistant
                    this.conversationHistory.pop(); // Remove user
                }
                logger_1.default.debug(`[Hydra] After backtracking state: turn: ${turn}, backtrackCount: ${backtrackCount}, conversationLength: ${this.conversationHistory.length}, willDecrementTurn: ${turn > 1}`);
                if (backtrackCount >= this.maxBacktracks) {
                    logger_1.default.debug(`[Hydra] Max backtracks (${this.maxBacktracks}) reached. Exiting loop. backtrackCount: ${backtrackCount}, maxBacktracks: ${this.maxBacktracks}`);
                    stopReason = 'Max backtracks reached';
                    break;
                }
                if (turn > 1) {
                    turn--; // Decrement turn to retry
                }
                continue;
            }
            // Grade the response
            let graderResult = undefined;
            if (test && assertToUse) {
                const grader = getGraderById(assertToUse.type);
                if (grader) {
                    const { grade } = await grader.getResult(nextMessage, targetResponse.output, test, targetProvider, assertToUse && 'value' in assertToUse ? assertToUse.value : undefined);
                    graderResult = grade;
                    storedGraderResult = grade;
                    logger_1.default.debug(`[Hydra] Grader result: pass: ${grade.pass} turn: ${turn}`);
                }
            }
            // Store in redteam history (with audio/image if transforms were applied)
            redteamHistory.push({
                prompt: nextMessage,
                promptAudio: lastTransformResult?.audio,
                promptImage: lastTransformResult?.image,
                output: targetResponse.output,
                outputAudio: targetResponse.audio
                    ? { data: targetResponse.audio.data || '', format: targetResponse.audio.format || 'wav' }
                    : undefined,
                // Note: outputImage would come from provider if model responds with image
                graderPassed: graderResult?.pass,
            });
            // Check if vulnerability was achieved
            if (graderResult?.pass === false) {
                vulnerabilityAchieved = true;
                successfulAttacks.push({
                    turn,
                    message: nextMessage,
                    response: targetResponse.output,
                });
                stopReason = 'Grader failed';
                logger_1.default.debug(`[Hydra] Vulnerability achieved! turn: ${turn}`);
                break;
            }
        }
        // Update scan learnings
        if (scanId) {
            try {
                const turnsCompleted = this.conversationHistory.filter((m) => m.role === 'user').length;
                const learningRequest = {
                    task: 'hydra-decision',
                    testRunId,
                    scanId,
                    testComplete: true,
                    finalResult: {
                        success: vulnerabilityAchieved,
                        totalTurns: turnsCompleted,
                    },
                };
                await this.agentProvider.callApi(JSON.stringify(learningRequest), {
                    prompt: {
                        raw: JSON.stringify(learningRequest),
                        label: 'hydra-learning-update',
                    },
                    vars: {},
                }, options);
                logger_1.default.debug(`[Hydra] Scan learnings updated: scanId: ${scanId}, testRunId: ${testRunId}`);
            }
            catch (error) {
                logger_1.default.warn(`[Hydra] Failed to update scan learnings: ${error}`);
                // Don't fail test if learning update fails
            }
        }
        const messages = this.conversationHistory.map((msg) => ({
            role: msg.role,
            content: msg.content,
        }));
        return {
            output: lastTargetResponse?.output || '',
            ...(lastTargetResponse?.error ? { error: lastTargetResponse.error } : {}),
            metadata: {
                sessionId: this.sessionId || (0, util_1.getSessionId)(lastTargetResponse, context),
                messages,
                hydraRoundsCompleted: this.conversationHistory.filter((m) => m.role === 'user').length,
                hydraBacktrackCount: backtrackCount,
                hydraResult: vulnerabilityAchieved,
                stopReason,
                successfulAttacks,
                totalSuccessfulAttacks: successfulAttacks.length,
                storedGraderResult,
                redteamHistory,
                sessionIds,
            },
            tokenUsage: totalTokenUsage,
            guardrails: lastTargetResponse?.guardrails,
        };
    }
}
exports.HydraProvider = HydraProvider;
exports.default = HydraProvider;
//# sourceMappingURL=index.js.map