"use strict";
/**
 * ExternalCrossCheckEnforcer -- Opt-in external verification gate for self-reflection outcomes.
 *
 * A self-reflecting agent (Tier 1, Agent Type Coverage Strategy) produces a reflection
 * outcome (self-check verdict). This primitive bounds the trust placed in that single
 * self-reflection by requiring an external cross-check before the cycle proceeds.
 *
 * ORTHOGONAL TO the SelfReflectionLoopDetector (anomaly-self-reflection.ts):
 *   - LoopDetector: catches pathological loops (N consecutive regenerate triplets).
 *   - ExternalCrossCheckEnforcer: limits trust in any single self-reflection outcome,
 *     regardless of whether a loop exists.
 *
 * Two operating modes (operator-configured per agent):
 *   'peer-agent'  -- Reflection outcome is forwarded to a designated peer agent
 *                    (different model / different prompt). Peer returns a verdict.
 *                    Accept -> cycle continues. Reject or inconclusive -> human queue.
 *   'human-only'  -- Every reflection outcome creates a human approval queue entry.
 *                    No peer agent is involved.
 *
 * Pen-test coverage:
 *   PT-CROSS-CHECK-1: Peer-collusion detection via agreement-rate tracking.
 *   PT-CROSS-CHECK-2: Tier-spoofing prevention via queue auth check.
 *   PT-CROSS-CHECK-3: Cycle-replay prevention via terminated-cycle guard.
 *
 * Compliance bindings:
 *   - EU AI Act Art. 14: Human oversight of high-risk AI outputs.
 *   - HIPAA 164.308(a)(3): Workforce controls for access to PHI processing agents.
 *   - SOC 2 CC1.4: Control environment -- independent verification of agent output.
 *   - ISO 27001 A.6.1.1: Information security roles and responsibilities.
 *   - NIST 800-53 AU-12: Audit record generation -- cross-check outcome logged.
 *
 * BUILD_PLAN V3 line 259: External cross-check option (second agent or human verifies
 * reflection outcome via approval queue).
 *
 * @connexum/ai-governance
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExternalCrossCheckEnforcer = exports.CycleAlreadyTerminatedError = exports.CrossCheckConfigError = exports.SELF_REFLECTION_CROSS_CHECK_TIER = void 0;
const crypto = __importStar(require("node:crypto"));
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/** Approval-queue tier string for all entries created by this enforcer. */
exports.SELF_REFLECTION_CROSS_CHECK_TIER = 'SELF_REFLECTION_CROSS_CHECK';
/** Default peer-agent verdict timeout in milliseconds. */
const DEFAULT_PEER_TIMEOUT_MS = 10_000;
/**
 * Default peer-agreement-rate window for PT-CROSS-CHECK-1 collusion detection.
 * Tracked as a rolling window of the last N verdicts per peerAgentId.
 */
const DEFAULT_COLLUSION_WINDOW = 20;
/**
 * If a peer's accept rate over the collusion window meets or exceeds this
 * fraction, emit PEER_COLLUSION_SUSPECTED.
 */
const DEFAULT_COLLUSION_ACCEPT_THRESHOLD = 1.0; // 100% accepts over the window
// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------
class CrossCheckConfigError extends Error {
    constructor(message) {
        super(message);
        this.name = 'CrossCheckConfigError';
    }
}
exports.CrossCheckConfigError = CrossCheckConfigError;
class CycleAlreadyTerminatedError extends Error {
    cycleId;
    constructor(cycleId, reason) {
        super(`Cycle '${cycleId}' has already been terminated` +
            (reason ? `: ${reason}` : '.'));
        this.name = 'CycleAlreadyTerminatedError';
        this.cycleId = cycleId;
    }
}
exports.CycleAlreadyTerminatedError = CycleAlreadyTerminatedError;
// ---------------------------------------------------------------------------
// Peer agreement-rate tracker (PT-CROSS-CHECK-1)
// ---------------------------------------------------------------------------
/**
 * Tracks the rolling accept rate for each peerAgentId.
 * Emits PEER_COLLUSION_SUSPECTED when the rate exceeds the threshold.
 */
class PeerAgreementTracker {
    /** Map: peerAgentId -> circular buffer of 'accept' booleans. */
    windows = new Map();
    windowSize;
    threshold;
    constructor(windowSize, threshold) {
        this.windowSize = windowSize;
        this.threshold = threshold;
    }
    /**
     * Record a verdict. Returns true if collusion is suspected after this verdict.
     * windowSize and threshold override the instance defaults when provided.
     */
    record(peerAgentId, verdict, windowSize, threshold) {
        const win = windowSize ?? this.windowSize;
        const thr = threshold ?? this.threshold;
        if (!this.windows.has(peerAgentId)) {
            this.windows.set(peerAgentId, []);
        }
        const buf = this.windows.get(peerAgentId);
        buf.push(verdict === 'accept');
        if (buf.length > win) {
            buf.shift();
        }
        // Only evaluate once the window is full.
        if (buf.length < win)
            return false;
        const acceptCount = buf.filter(Boolean).length;
        return acceptCount / buf.length >= thr;
    }
    /** For testing: retrieve current window for a peer. */
    getWindow(peerAgentId) {
        return [...(this.windows.get(peerAgentId) ?? [])];
    }
}
// ---------------------------------------------------------------------------
// ExternalCrossCheckEnforcer
// ---------------------------------------------------------------------------
class ExternalCrossCheckEnforcer {
    peerCallback;
    auditIntegrity;
    approvalQueue;
    /** Registry: `${orgId}::${agentId}` -> config */
    configs = new Map();
    /**
     * Registry: approvalId -> { orgId, agentId, cycleId, packBinding }
     * Used by resumeFromApproval to retrieve the cycle context.
     */
    pendingResumes = new Map();
    /**
     * Terminated cycles: `${orgId}::${agentId}::${cycleId}`
     * Populated when resumeFromApproval returns 'terminated'.
     * PT-CROSS-CHECK-3: guards against cycle replay under the same cycleId.
     */
    terminatedCycles = new Set();
    /** Stale queue entries that were auto-resolved due to cycle replay. */
    staleResolvedEntries = new Map(); // approvalId -> reason
    collusionTracker;
    constructor(opts) {
        this.approvalQueue = opts.approvalQueue;
        this.peerCallback = opts.peerCallback;
        this.auditIntegrity = opts.auditIntegrity;
        // Collusion tracker uses conservative defaults; overridden per-agent on configure().
        this.collusionTracker = new PeerAgreementTracker(DEFAULT_COLLUSION_WINDOW, DEFAULT_COLLUSION_ACCEPT_THRESHOLD);
    }
    // -------------------------------------------------------------------------
    // configure
    // -------------------------------------------------------------------------
    /**
     * Register a cross-check rule for an agent.
     *
     * @throws {CrossCheckConfigError} if packBinding is empty.
     * @throws {CrossCheckConfigError} if mode is 'peer-agent' and peerAgentId equals agentId.
     * @throws {CrossCheckConfigError} if mode is 'peer-agent' and peerAgentId is not supplied.
     */
    configure(orgId, agentId, mode, peerAgentId, packBinding = [], opts = {}) {
        if (!packBinding || packBinding.length === 0) {
            throw new CrossCheckConfigError(`[ExternalCrossCheckEnforcer] configure() requires non-empty packBinding. ` +
                `orgId=${orgId} agentId=${agentId}`);
        }
        if (mode === 'peer-agent') {
            if (!peerAgentId) {
                throw new CrossCheckConfigError(`[ExternalCrossCheckEnforcer] mode 'peer-agent' requires peerAgentId. ` +
                    `orgId=${orgId} agentId=${agentId}`);
            }
            if (peerAgentId === agentId) {
                throw new CrossCheckConfigError(`[ExternalCrossCheckEnforcer] An agent cannot cross-check itself. ` +
                    `orgId=${orgId} agentId=${agentId} peerAgentId=${peerAgentId}`);
            }
        }
        const key = this.configKey(orgId, agentId);
        this.configs.set(key, {
            orgId,
            agentId,
            mode,
            peerAgentId: peerAgentId ?? '',
            packBinding,
            peerTimeoutMs: opts.peerTimeoutMs ?? DEFAULT_PEER_TIMEOUT_MS,
            collusionWindow: opts.collusionWindow ?? DEFAULT_COLLUSION_WINDOW,
            collusionAcceptThreshold: opts.collusionAcceptThreshold ?? DEFAULT_COLLUSION_ACCEPT_THRESHOLD,
        });
    }
    // -------------------------------------------------------------------------
    // evaluateReflection
    // -------------------------------------------------------------------------
    /**
     * Evaluate a self-reflection outcome.
     *
     * In 'peer-agent' mode:
     *   - Invokes the PeerAgentVerdictCallback with a timeout.
     *   - 'accept' -> emit SELF_REFLECTION_CROSS_CHECK_PASSED (NOTICE); returns { status: 'continue' }.
     *   - 'reject' | 'inconclusive' -> emit SELF_REFLECTION_CROSS_CHECK_DISAGREEMENT (WARNING);
     *     create SELF_REFLECTION_CROSS_CHECK queue entry; returns { status: 'queued', approvalId }.
     *   - Peer timeout -> treated as 'inconclusive'.
     *
     * In 'human-only' mode:
     *   - Always creates a queue entry; returns { status: 'queued', approvalId }.
     *
     * Reflection text is NEVER stored. Only its SHA-256 hash enters the audit chain.
     *
     * @throws {Error} if no config is registered for this agent.
     * @throws {CycleAlreadyTerminatedError} if the cycle was already terminated.
     */
    async evaluateReflection(orgId, agentId, cycleId, reflectionOutcome) {
        const cycleKey = this.cycleKey(orgId, agentId, cycleId);
        if (this.terminatedCycles.has(cycleKey)) {
            throw new CycleAlreadyTerminatedError(cycleId, 'cycle replay prevented');
        }
        const config = this.getConfig(orgId, agentId);
        // Hash the reflection text -- raw text must never reach the audit chain.
        const textHash = reflectionOutcome.text
            ? crypto.createHash('sha256').update(reflectionOutcome.text).digest('hex')
            : '';
        if (config.mode === 'human-only') {
            const approvalId = this.createQueueEntry(orgId, agentId, cycleId, textHash, config.packBinding, 'human-only');
            this.emitAuditEvent({
                type: 'SELF_REFLECTION_CROSS_CHECK_QUEUED',
                severity: 'NOTICE',
                orgId,
                agentId,
                cycleId,
                approvalId,
                mode: 'human-only',
                textHash,
                packBinding: config.packBinding,
            });
            return { status: 'queued', approvalId };
        }
        // peer-agent mode
        const peerVerdict = await this.invokePeer(config, agentId, reflectionOutcome);
        // PT-CROSS-CHECK-1: track peer agreement rate using per-agent configured window/threshold
        const collusionSuspected = this.collusionTracker.record(config.peerAgentId, peerVerdict.verdict, config.collusionWindow, config.collusionAcceptThreshold);
        if (collusionSuspected) {
            this.emitAuditEvent({
                type: 'PEER_COLLUSION_SUSPECTED',
                severity: 'WARNING',
                orgId,
                agentId,
                cycleId,
                peerAgentId: config.peerAgentId,
                packBinding: config.packBinding,
            });
        }
        if (peerVerdict.verdict === 'accept') {
            this.emitAuditEvent({
                type: 'SELF_REFLECTION_CROSS_CHECK_PASSED',
                severity: 'NOTICE',
                orgId,
                agentId,
                cycleId,
                peerAgentId: config.peerAgentId,
                peerVerdict: 'accept',
                confidence: peerVerdict.confidence,
                textHash,
                packBinding: config.packBinding,
            });
            return { status: 'continue', peerVerdict: 'accept', collusionSuspected };
        }
        // reject or inconclusive -> fall through to human queue
        const approvalId = this.createQueueEntry(orgId, agentId, cycleId, textHash, config.packBinding, 'peer-disagreement', peerVerdict.verdict);
        this.emitAuditEvent({
            type: 'SELF_REFLECTION_CROSS_CHECK_DISAGREEMENT',
            severity: 'WARNING',
            orgId,
            agentId,
            cycleId,
            peerAgentId: config.peerAgentId,
            peerVerdict: peerVerdict.verdict,
            confidence: peerVerdict.confidence,
            approvalId,
            textHash,
            packBinding: config.packBinding,
        });
        return {
            status: 'queued',
            approvalId,
            peerVerdict: peerVerdict.verdict,
            collusionSuspected,
        };
    }
    // -------------------------------------------------------------------------
    // resumeFromApproval
    // -------------------------------------------------------------------------
    /**
     * Called by the approval-queue resolver when a human resolves a
     * SELF_REFLECTION_CROSS_CHECK queue entry.
     *
     * approved: true  -> { status: 'continue' }
     * approved: false -> marks cycle as terminated, { status: 'terminated' }
     *
     * PT-CROSS-CHECK-3: once a cycle is terminated, any subsequent evaluateReflection
     * call for the same cycleId throws CycleAlreadyTerminatedError.
     *
     * PT-CROSS-CHECK-2: this method does NOT bypass the approval-queue's auth check.
     * The queue's resolve() method validates the resolvedBy identity and HMAC token
     * before this is called. Self-approval is blocked at the queue layer.
     *
     * @throws {Error} if approvalId is not tracked by this enforcer.
     */
    resumeFromApproval(approvalId, approved, resolvedBy) {
        const ctx = this.pendingResumes.get(approvalId);
        if (!ctx) {
            // Check if it was auto-resolved due to cycle replay.
            if (this.staleResolvedEntries.has(approvalId)) {
                return { status: 'terminated' };
            }
            throw new Error(`[ExternalCrossCheckEnforcer] Unknown approvalId '${approvalId}'. ` +
                `Either already resolved or not issued by this enforcer.`);
        }
        this.pendingResumes.delete(approvalId);
        if (!approved) {
            const cycleKey = this.cycleKey(ctx.orgId, ctx.agentId, ctx.cycleId);
            this.terminatedCycles.add(cycleKey);
            this.emitAuditEvent({
                type: 'SELF_REFLECTION_CROSS_CHECK_TERMINATED',
                severity: 'WARNING',
                orgId: ctx.orgId,
                agentId: ctx.agentId,
                cycleId: ctx.cycleId,
                approvalId,
                resolvedBy: resolvedBy ?? 'unknown',
                packBinding: ctx.packBinding,
            });
            return { status: 'terminated' };
        }
        this.emitAuditEvent({
            type: 'SELF_REFLECTION_CROSS_CHECK_RESUMED',
            severity: 'NOTICE',
            orgId: ctx.orgId,
            agentId: ctx.agentId,
            cycleId: ctx.cycleId,
            approvalId,
            resolvedBy: resolvedBy ?? 'unknown',
            packBinding: ctx.packBinding,
        });
        return { status: 'continue' };
    }
    // -------------------------------------------------------------------------
    // PT-CROSS-CHECK-3: Cycle replay detection
    // -------------------------------------------------------------------------
    /**
     * Auto-resolve any pending queue entries for a terminated cycleId as
     * 'terminated_by_cycle_replay'. Called when a new cycle attempts to use
     * a cycleId that was already terminated.
     */
    resolveStaleEntriesForCycle(orgId, agentId, cycleId) {
        const cycleKey = this.cycleKey(orgId, agentId, cycleId);
        const resolvedIds = [];
        for (const [id, ctx] of this.pendingResumes) {
            if (ctx.orgId === orgId &&
                ctx.agentId === agentId &&
                ctx.cycleId === cycleId) {
                this.staleResolvedEntries.set(id, 'terminated_by_cycle_replay');
                this.pendingResumes.delete(id);
                resolvedIds.push(id);
                this.emitAuditEvent({
                    type: 'STALE_QUEUE_ENTRY_AUTO_RESOLVED',
                    severity: 'WARNING',
                    orgId,
                    agentId,
                    cycleId,
                    approvalId: id,
                    reason: 'terminated_by_cycle_replay',
                });
            }
        }
        // Ensure the cycle is marked terminated.
        this.terminatedCycles.add(cycleKey);
        return resolvedIds;
    }
    // -------------------------------------------------------------------------
    // Inspection helpers (for tests)
    // -------------------------------------------------------------------------
    /** Whether a cross-check config is registered for this agent. */
    isConfigured(orgId, agentId) {
        return this.configs.has(this.configKey(orgId, agentId));
    }
    /** Whether a cycle is marked as terminated. */
    isCycleTerminated(orgId, agentId, cycleId) {
        return this.terminatedCycles.has(this.cycleKey(orgId, agentId, cycleId));
    }
    /** Expose the collusion tracker for testing PT-CROSS-CHECK-1. */
    getPeerAgreementWindow(peerAgentId) {
        return this.collusionTracker.getWindow(peerAgentId);
    }
    // -------------------------------------------------------------------------
    // Private helpers
    // -------------------------------------------------------------------------
    configKey(orgId, agentId) {
        return `${orgId}::${agentId}`;
    }
    cycleKey(orgId, agentId, cycleId) {
        return `${orgId}::${agentId}::${cycleId}`;
    }
    getConfig(orgId, agentId) {
        const cfg = this.configs.get(this.configKey(orgId, agentId));
        if (!cfg) {
            throw new Error(`[ExternalCrossCheckEnforcer] No cross-check config for orgId=${orgId} agentId=${agentId}. ` +
                `Call configure() first.`);
        }
        return cfg;
    }
    async invokePeer(config, agentId, reflectionOutcome) {
        if (!this.peerCallback) {
            // No callback supplied; treat as inconclusive (safe default).
            process.stderr.write(`[ExternalCrossCheckEnforcer] No PeerAgentVerdictCallback configured. ` +
                `Treating as inconclusive.\n`);
            return { verdict: 'inconclusive', confidence: 0, reasoning: 'no-callback' };
        }
        const timeoutMs = config.peerTimeoutMs;
        const peerPromise = this.peerCallback({
            agentId,
            peerAgentId: config.peerAgentId,
            reflectionOutcome,
        });
        const timeoutPromise = new Promise((resolve) => {
            setTimeout(() => resolve({ verdict: 'inconclusive', confidence: 0, reasoning: 'timeout' }), timeoutMs);
        });
        return Promise.race([peerPromise, timeoutPromise]);
    }
    createQueueEntry(orgId, agentId, cycleId, textHash, packBinding, reason, peerVerdict) {
        const inputSummary = JSON.stringify({
            orgId,
            agentId,
            cycleId,
            textHash, // raw text NEVER included -- SHA-256 hash only
            peerVerdict: peerVerdict ?? null,
            packBinding,
        });
        const entry = this.approvalQueue.submit(agentId, exports.SELF_REFLECTION_CROSS_CHECK_TIER, inputSummary, `Self-reflection cross-check required. Reason: ${reason}. Cycle: ${cycleId}.`);
        this.pendingResumes.set(entry.id, { orgId, agentId, cycleId, packBinding });
        return entry.id;
    }
    emitAuditEvent(data) {
        const payload = {
            ...data,
            timestamp: new Date().toISOString(),
        };
        if (this.auditIntegrity) {
            try {
                this.auditIntegrity.append(payload);
            }
            catch (err) {
                process.stderr.write(`[ExternalCrossCheckEnforcer] Audit chain write failed: ${err.message}\n`);
            }
        }
        else {
            process.stderr.write(`[AUDIT] ${JSON.stringify(payload)}\n`);
        }
    }
}
exports.ExternalCrossCheckEnforcer = ExternalCrossCheckEnforcer;
//# sourceMappingURL=external-cross-check.js.map