"use strict";
/**
 * CircuitBreaker -- Prevents cascading agent failures.
 * OWASP ASI08 remediation.
 *
 * ASI-10 (SDK kill-switch): adds per-agent forced OPEN state via
 * forceOpen(agentId, at?) / resetForced(agentId) mirroring the proxy
 * circuit-breaker (packages/governance-proxy/src/security/circuit-breaker.ts).
 *
 * The forced set is an in-memory Map keyed by agentId so isolation between
 * agents is guaranteed — a forced OPEN on agent-A does NOT affect agent-B.
 * allowExecutionForAgent(agentId) is the per-agent check used by the
 * SDK PreToolUse path: it returns false immediately when the agentId is in
 * the forced-OPEN set (Locked Invariant 3 — no I/O on the enforcement path).
 *
 * States:
 *   CLOSED  - Normal operation. Failures tracked but below threshold.
 *   OPEN    - Agent suspended. All operations blocked.
 *   HALF    - Probationary. One test cycle allowed.
 *
 * @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.CircuitBreaker = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
class CircuitBreaker {
    stateFile;
    failureThreshold;
    halfOpenAfterMs;
    agentName;
    /**
     * ASI-10: per-agent forced-OPEN set (SDK kill-switch).
     *
     * Key = agentId. Value = ISO timestamp when the force was applied.
     * Isolated per-agent — forcing one agent never changes another's entry.
     * Plain in-memory Map: no I/O, no throws (Locked Invariant 3).
     *
     * Source of truth for SDK-embedded deployments: the SuspendStateCache
     * (src/security/suspend-state-cache.ts) polls GET /api/v1/agents/:id
     * and calls forceOpen(agentId) / resetForced(agentId) on state changes.
     * The forced set itself is always checked synchronously on the hot path.
     */
    forcedOpenAgents = new Map();
    constructor(config) {
        this.agentName = config.agentName;
        this.failureThreshold = config.failureThreshold ?? 3;
        this.halfOpenAfterMs = config.halfOpenAfterMs ?? 10 * 60 * 1000;
        if (!fs.existsSync(config.stateDir)) {
            fs.mkdirSync(config.stateDir, { recursive: true });
        }
        this.stateFile = path.join(config.stateDir, `circuit-${config.agentName}.json`);
    }
    /**
     * Check if the agent is allowed to execute.
     */
    allowExecution() {
        const state = this.loadState();
        switch (state.state) {
            case 'CLOSED':
                return true;
            case 'OPEN': {
                // Check if enough time has passed for half-open
                if (state.openedAt) {
                    const elapsed = Date.now() - new Date(state.openedAt).getTime();
                    if (elapsed >= this.halfOpenAfterMs) {
                        state.state = 'HALF_OPEN';
                        this.saveState(state);
                        return true;
                    }
                }
                return false;
            }
            case 'HALF_OPEN':
                return true;
            default:
                return false;
        }
    }
    /**
     * Record a successful operation.
     */
    recordSuccess() {
        const state = this.loadState();
        state.consecutiveFailures = 0;
        state.lastSuccess = new Date().toISOString();
        state.totalSuccesses++;
        if (state.state === 'HALF_OPEN') {
            state.state = 'CLOSED';
            state.openedAt = null;
        }
        this.saveState(state);
    }
    /**
     * Record a failed operation.
     */
    recordFailure(reason) {
        const state = this.loadState();
        state.consecutiveFailures++;
        state.lastFailure = new Date().toISOString();
        state.totalFailures++;
        if (state.state === 'HALF_OPEN') {
            // Failed during probation -- back to OPEN
            state.state = 'OPEN';
            state.openedAt = new Date().toISOString();
        }
        else if (state.consecutiveFailures >= this.failureThreshold) {
            state.state = 'OPEN';
            state.openedAt = new Date().toISOString();
        }
        this.saveState(state);
    }
    /**
     * Get current state (read-only).
     */
    getState() {
        return this.loadState();
    }
    /**
     * Manually reset circuit breaker to CLOSED.
     * Only for human operator use.
     */
    reset() {
        const state = this.loadState();
        state.state = 'CLOSED';
        state.consecutiveFailures = 0;
        state.openedAt = null;
        this.saveState(state);
    }
    // ---------------------------------------------------------------------------
    // ASI-10: per-agent forced OPEN (SDK kill-switch)
    // ---------------------------------------------------------------------------
    /**
     * ASI-10: per-agent execution check.
     *
     * Returns false if:
     *   (a) the specific agentId is in the forced-OPEN set (operator kill-switch), OR
     *   (b) the shared breaker is OPEN from accumulated failure count.
     *
     * Returns true otherwise (CLOSED or HALF_OPEN with no forced override).
     *
     * Never throws — forced set is a plain Map; no I/O on this path.
     * This is the method called by the SDK PreToolUse path.
     */
    allowExecutionForAgent(agentId) {
        if (this.forcedOpenAgents.has(agentId))
            return false;
        return this.allowExecution();
    }
    /**
     * ASI-10: operator-initiated hard kill-switch.
     *
     * Forces the breaker OPEN for a specific agentId regardless of the shared
     * failure count. A different agent calling allowExecutionForAgent() with a
     * different agentId is completely unaffected (Locked Invariant 3).
     *
     * Called by SuspendStateCache when it detects operatorSuspended=true.
     *
     * @param agentId  Stable agent record id (e.g. "agent-abc12345").
     * @param at       ISO timestamp of when the suspension was initiated
     *                 (for audit logging). Defaults to now.
     */
    forceOpen(agentId, at) {
        this.forcedOpenAgents.set(agentId, at ?? new Date().toISOString());
    }
    /**
     * ASI-10: reverse the per-agent forced OPEN (resume after suspension).
     *
     * Removes agentId from the forced set. If the shared breaker happens to be
     * in OPEN state from accumulated failures, those still apply — the resume
     * only clears the OPERATOR-FORCED block, not the auto-tripped breaker.
     *
     * Called by SuspendStateCache when it detects operatorSuspended=false.
     */
    resetForced(agentId) {
        this.forcedOpenAgents.delete(agentId);
    }
    /** ASI-10: check whether a specific agentId is currently force-suspended. */
    isForcedOpen(agentId) {
        return this.forcedOpenAgents.has(agentId);
    }
    /** ASI-10: snapshot of all currently force-suspended agent ids. */
    forcedOpenSnapshot() {
        return this.forcedOpenAgents;
    }
    loadState() {
        if (!fs.existsSync(this.stateFile)) {
            return {
                state: 'CLOSED',
                consecutiveFailures: 0,
                lastFailure: null,
                lastSuccess: null,
                openedAt: null,
                totalFailures: 0,
                totalSuccesses: 0,
            };
        }
        try {
            return JSON.parse(fs.readFileSync(this.stateFile, 'utf-8'));
        }
        catch {
            return {
                state: 'CLOSED',
                consecutiveFailures: 0,
                lastFailure: null,
                lastSuccess: null,
                openedAt: null,
                totalFailures: 0,
                totalSuccesses: 0,
            };
        }
    }
    saveState(state) {
        try {
            fs.writeFileSync(this.stateFile, JSON.stringify(state, null, 2));
        }
        catch (err) {
            // State save failure is critical -- a suspended agent could resume on restart
            process.stderr.write(`[CIRCUIT BREAKER CRITICAL] Failed to save state for ${this.agentName}: ${err.message}\n`);
            // Fail-closed: if we can't persist the OPEN state, throw so caller knows
            if (state.state === 'OPEN') {
                throw new Error(`Failed to persist OPEN circuit state for ${this.agentName}. ` +
                    `Agent may resume unsafely on restart.`);
            }
        }
    }
}
exports.CircuitBreaker = CircuitBreaker;
//# sourceMappingURL=circuit-breaker.js.map