/**
 * BlastRadiusEnforcer -- Per-category, per-cycle blast-radius gate.
 *
 * Complements the autonomy spectrum (PR #24) by adding category-aware
 * impact caps that are enforced at tool-call time. While PR #24's Level 3
 * (Bounded) checks a declared aggregate blast radius (systemsTouched,
 * financialImpactCents, usersAffected) via the agent's own declaration,
 * this enforcer independently counts actual actions by category within the
 * current cycle and rejects calls that would exceed the configured cap for
 * that category at the agent's autonomy level.
 *
 * The key distinction:
 *   PR #24 blast-radius check is DECLARATION-BASED (agent states impact).
 *   This enforcer is COUNT-BASED (system counts actual category invocations).
 *
 * Categories tracked (orthogonal counters; exhausting one does not
 * affect any other):
 *   file-mutations   -- writes, appends, deletes, renames of files/dirs
 *   network-calls    -- HTTP/HTTPS requests to external endpoints
 *   database-writes  -- INSERT, UPDATE, DELETE, schema mutations
 *   secret-reads     -- vault reads, env-var secret access, key material reads
 *   tool-invocations -- total tool calls across all categories
 *
 * Autonomy-level caps (default, operator-tunable):
 *   Level 0 (Manual):       0 / 0 / 0 / 0 / 0   -- no autonomous actions at all
 *   Level 1 (Assisted):     0 / 0 / 0 / 0 / 10  -- read-only; 10 total tools
 *   Level 2 (Conditional):  5 / 2 / 2 / 0 / 25  -- pre-approved classes only
 *   Level 3 (Bounded):     20 / 10 / 10 / 2 / 50 -- within blast limits
 *   Level 4 (Autonomous): 100 / 50 / 50 / 10 / 200 -- widest, still bounded
 *
 * Counter scope: per-cycle. resetCycle() must be called at cycle boundary.
 * Counters for a given (orgId, agentId, cycleId) triple are independent.
 * Calling resetCycle() deletes all counters for that triple.
 *
 * Hook position: fires in PreToolUse, after autonomy-spectrum enforcement.
 * When autonomy-spectrum is not active (main-branch builds without PR #24
 * merged), this enforcer uses the autonomy level passed by the caller;
 * callers not tracking autonomy levels may pass level 4 to get the widest
 * caps or configure custom per-agent caps directly.
 *
 * Audit: every rejection writes a BLAST_RADIUS_CATEGORY_EXCEEDED event
 * with the full packBinding[] to the audit sink. pack-binding primacy is
 * enforced at the entrance of every public method (per
 * feedback_pack_binding_primacy.md): empty packBinding throws
 * UnboundDataError before any counter is incremented.
 *
 * Pen-test gates:
 *   PT-BLAST-1: empty packBinding[] throws UnboundDataError before any
 *     counter increments.
 *   PT-BLAST-2: configured cap of 0 for a category at a given level rejects
 *     all actions in that category.
 *   PT-BLAST-3: tampered config (caps set negative or non-integer) is
 *     rejected at construction time.
 *   PT-BLAST-4: rapid burst of allowed actions followed by a single denied
 *     action produces correct audit-event ordering.
 *
 * Compliance bindings:
 *   HIPAA 164.308(a)(3): Workforce authorization and supervision
 *   HIPAA 164.312(a)(1): Access control -- minimum necessary
 *   SOC 2 CC6.1: Logical access restrictions
 *   GDPR Art. 25: Data protection by design (impact minimisation)
 *   EU AI Act Art. 9: Risk management -- bounded autonomous scope
 *   DORA Art. 16: ICT risk management -- automation limits
 *   NIST 800-53 AC-6: Least privilege
 *   ISO 42001 A.6.1.4: AI resource management
 *
 * Relationship to PR #24 (feat/p3-t1-autonomy-spectrum):
 *   - Does NOT import from autonomy-spectrum.ts; no circular dependency.
 *   - Does NOT duplicate AutonomyLevel type -- defines it locally as the
 *     same numeric union 0|1|2|3|4 for standalone use.
 *   - Does NOT replicate BlastRadius / BlastRadiusLimits types from PR #24;
 *     those capture declared aggregate impact; this file captures actual
 *     per-category invocation counts.
 *   - Both enforcers compose in the PreToolUse chain. Neither replaces the
 *     other. The autonomy spectrum runs first (decides WHAT level); this
 *     enforcer runs alongside/after (counts HOW MANY actions per category).
 *
 * @connexum/ai-governance P3-T2
 */
import * as crypto from 'node:crypto';
export const ALL_BLAST_CATEGORIES = [
    'file-mutations',
    'network-calls',
    'database-writes',
    'secret-reads',
    'tool-invocations',
];
// ---------------------------------------------------------------------------
// Default cap table
// ---------------------------------------------------------------------------
/**
 * Default caps per autonomy level. Designed so each higher level has equal
 * or wider caps in every category. Operators tune these in deployment config.
 */
export const DEFAULT_LEVEL_CAPS = {
    0: {
        'file-mutations': 0,
        'network-calls': 0,
        'database-writes': 0,
        'secret-reads': 0,
        'tool-invocations': 0,
    },
    1: {
        'file-mutations': 0,
        'network-calls': 0,
        'database-writes': 0,
        'secret-reads': 0,
        'tool-invocations': 10,
    },
    2: {
        'file-mutations': 5,
        'network-calls': 2,
        'database-writes': 2,
        'secret-reads': 0,
        'tool-invocations': 25,
    },
    3: {
        'file-mutations': 20,
        'network-calls': 10,
        'database-writes': 10,
        'secret-reads': 2,
        'tool-invocations': 50,
    },
    4: {
        'file-mutations': 100,
        'network-calls': 50,
        'database-writes': 50,
        'secret-reads': 10,
        'tool-invocations': 200,
    },
};
// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------
/**
 * Thrown when an operation is called without a non-empty packBinding[].
 * Pack-binding primacy invariant: every action must declare which compliance
 * packs govern it. Counters are never incremented when this fires.
 */
export class UnboundDataError extends Error {
    constructor(operation) {
        super(`UnboundDataError: packBinding must be a non-empty string array for ` +
            `'${operation}'. Every blast-radius operation must declare which ` +
            `compliance packs govern it (feedback_pack_binding_primacy.md).`);
        this.name = 'UnboundDataError';
    }
}
/**
 * Thrown when the enforcer is constructed with invalid cap values (negative
 * or non-integer). PT-BLAST-3.
 */
export class InvalidBlastCapError extends Error {
    level;
    category;
    value;
    constructor(level, category, value) {
        super(`InvalidBlastCapError: cap for level ${level} category '${category}' ` +
            `must be a non-negative integer, got ${JSON.stringify(value)}.`);
        this.name = 'InvalidBlastCapError';
        this.level = level;
        this.category = category;
        this.value = value;
    }
}
/**
 * Thrown when a tool call would exceed the configured cap for a category
 * at the agent's current autonomy level. The action is rejected; the
 * rejection is written to the audit chain.
 */
export class BlastRadiusCategoryExceededError extends Error {
    orgId;
    agentId;
    cycleId;
    category;
    level;
    count;
    cap;
    packBinding;
    constructor(opts) {
        super(`BlastRadiusCategoryExceededError: agent '${opts.agentId}' (org ` +
            `'${opts.orgId}') in cycle '${opts.cycleId}' has reached the blast-radius ` +
            `cap for category '${opts.category}' at autonomy level ${opts.level}. ` +
            `Count: ${opts.count}, Cap: ${opts.cap}. Action rejected.`);
        this.name = 'BlastRadiusCategoryExceededError';
        this.orgId = opts.orgId;
        this.agentId = opts.agentId;
        this.cycleId = opts.cycleId;
        this.category = opts.category;
        this.level = opts.level;
        this.count = opts.count;
        this.cap = opts.cap;
        this.packBinding = opts.packBinding;
    }
}
// ---------------------------------------------------------------------------
// BlastRadiusEnforcer
// ---------------------------------------------------------------------------
/**
 * Blast-radius enforcement gate with per-category, per-cycle counters and
 * per-autonomy-level caps. Designed to be instantiated once per
 * governance-server instance and shared across agents/cycles.
 *
 * Usage in PreToolUse hook:
 *
 *   const enforcer = new BlastRadiusEnforcer(config);
 *   // At cycle start, no reset needed -- new cycleId is clean.
 *
 *   // On each tool call:
 *   enforcer.check({
 *     orgId, agentId, cycleId,
 *     category: 'file-mutations',
 *     packBinding: ['hipaa'],
 *   });
 *   // Throws BlastRadiusCategoryExceededError if cap exceeded.
 *
 *   // At cycle end (or on cycle reset):
 *   enforcer.resetCycle(orgId, agentId, cycleId, packBinding);
 */
export class BlastRadiusEnforcer {
    resolvedCaps;
    agentLevels;
    defaultLevel;
    auditSink;
    emitAllowed;
    /**
     * Counters keyed by `${orgId}:${agentId}:${cycleId}`.
     * Isolated per triple; resetCycle() deletes the entry.
     */
    counters = new Map();
    /**
     * SHA-256 audit chain state keyed by `${orgId}:${agentId}`.
     * Persists across cycles so the chain is continuous per agent.
     */
    chainState = new Map();
    constructor(config = {}) {
        this.resolvedCaps = this.buildResolvedCaps(config.levelCaps ?? {});
        this.agentLevels = new Map(Object.entries(config.agentLevels ?? {}));
        this.defaultLevel = config.defaultAutonomyLevel ?? 1;
        this.auditSink = config.auditSink ?? (() => undefined);
        this.emitAllowed = config.emitAllowedEvents ?? false;
    }
    // -------------------------------------------------------------------------
    // Public API
    // -------------------------------------------------------------------------
    /**
     * Pre-tool-call gate. Call this before dispatching any tool.
     *
     * Increments the category counter for the cycle and rejects the call if
     * the autonomy-level cap for that category is exceeded.
     *
     * Also always increments tool-invocations regardless of the primary
     * category supplied, so that the total-invocations cap is always enforced.
     *
     * @param opts.orgId      Organisation identifier.
     * @param opts.agentId    Agent identifier.
     * @param opts.cycleId    Cycle identifier (resets counters when new).
     * @param opts.category   Blast-radius category of the action being taken.
     * @param opts.packBinding Non-empty compliance pack IDs governing this action.
     * @param opts.level      Optional override for the agent's autonomy level.
     *                        When omitted, the level from agentLevels config or
     *                        defaultAutonomyLevel is used.
     *
     * @throws UnboundDataError             packBinding is empty.
     * @throws BlastRadiusCategoryExceededError  cap exceeded for the category or
     *                                           for tool-invocations.
     */
    check(opts) {
        const { orgId, agentId, cycleId, category, packBinding } = opts;
        // Pack-binding primacy -- PT-BLAST-1.
        if (!packBinding || packBinding.length === 0) {
            throw new UnboundDataError('check');
        }
        const level = opts.level ?? this.getLevel(agentId);
        const caps = this.resolvedCaps[level];
        const counters = this.getOrCreateCounters(orgId, agentId, cycleId);
        // Always increment tool-invocations first (before category-specific check)
        // so that even a rejected action is counted against the invocation budget.
        // We check tool-invocations cap first, then the specific category.
        const newToolCount = counters['tool-invocations'] + 1;
        const toolCap = caps['tool-invocations'];
        if (newToolCount > toolCap) {
            // Reject -- tool-invocations cap exceeded.
            this.emitRejection({
                orgId,
                agentId,
                cycleId,
                category: 'tool-invocations',
                level,
                count: newToolCount,
                cap: toolCap,
                packBinding,
                triggeredByCategory: category,
            });
            throw new BlastRadiusCategoryExceededError({
                orgId,
                agentId,
                cycleId,
                category: 'tool-invocations',
                level,
                count: newToolCount,
                cap: toolCap,
                packBinding,
            });
        }
        // Check the specific category cap if it is different from tool-invocations.
        if (category !== 'tool-invocations') {
            const newCatCount = counters[category] + 1;
            const catCap = caps[category];
            if (newCatCount > catCap) {
                // Reject -- category cap exceeded. Do NOT increment tool-invocations.
                this.emitRejection({
                    orgId,
                    agentId,
                    cycleId,
                    category,
                    level,
                    count: newCatCount,
                    cap: catCap,
                    packBinding,
                    triggeredByCategory: category,
                });
                throw new BlastRadiusCategoryExceededError({
                    orgId,
                    agentId,
                    cycleId,
                    category,
                    level,
                    count: newCatCount,
                    cap: catCap,
                    packBinding,
                });
            }
            // Both checks passed -- commit the category increment.
            counters[category] = newCatCount;
        }
        // Commit the tool-invocations increment.
        counters['tool-invocations'] = newToolCount;
        if (this.emitAllowed) {
            this.emitAllowed_event({ orgId, agentId, cycleId, category, level, packBinding, counters });
        }
    }
    /**
     * Reset all counters for the given cycle. Call at cycle end.
     * The audit chain state is preserved so the chain remains continuous
     * across cycle boundaries.
     *
     * @throws UnboundDataError if packBinding is empty.
     */
    resetCycle(orgId, agentId, cycleId, packBinding) {
        if (!packBinding || packBinding.length === 0) {
            throw new UnboundDataError('resetCycle');
        }
        const key = this.counterKey(orgId, agentId, cycleId);
        const had = this.counters.has(key);
        this.counters.delete(key);
        const prev = this.getChainState(orgId, agentId);
        const payload = {
            type: 'BLAST_RADIUS_CYCLE_RESET',
            orgId,
            agentId,
            cycleId,
            hadCounters: had,
            packBinding,
        };
        const currentHash = sha256(JSON.stringify(payload));
        const event = {
            type: 'BLAST_RADIUS_CYCLE_RESET',
            severity: 'INFO',
            orgId,
            agentId,
            cycleId,
            timestamp: new Date().toISOString(),
            prevHash: prev.prevHash,
            currentHash,
            data: { hadCounters: had, packBinding },
        };
        this.advanceChain(orgId, agentId, currentHash);
        this.auditSink(event);
    }
    /**
     * Return the current counters for an (orgId, agentId, cycleId) triple.
     * Returns all-zero counters if the cycle has not started yet or was reset.
     * Useful for monitoring dashboards.
     */
    getCounters(orgId, agentId, cycleId) {
        const key = this.counterKey(orgId, agentId, cycleId);
        return this.counters.get(key) ?? this.zeroCounters();
    }
    /**
     * Return the resolved caps for a given autonomy level.
     * Useful for surfacing configured limits in dashboards or audit docs.
     */
    getCaps(level) {
        return this.resolvedCaps[level];
    }
    /**
     * Return the effective autonomy level for an agent (from agentLevels config
     * or defaultAutonomyLevel).
     */
    getLevel(agentId) {
        return this.agentLevels.get(agentId) ?? this.defaultLevel;
    }
    // -------------------------------------------------------------------------
    // Private helpers
    // -------------------------------------------------------------------------
    buildResolvedCaps(overrides) {
        const levels = [0, 1, 2, 3, 4];
        const result = {};
        for (const level of levels) {
            const base = DEFAULT_LEVEL_CAPS[level];
            const override = overrides[level] ?? {};
            const merged = {
                'file-mutations': base['file-mutations'],
                'network-calls': base['network-calls'],
                'database-writes': base['database-writes'],
                'secret-reads': base['secret-reads'],
                'tool-invocations': base['tool-invocations'],
            };
            for (const cat of ALL_BLAST_CATEGORIES) {
                if (Object.prototype.hasOwnProperty.call(override, cat)) {
                    const v = override[cat];
                    if (typeof v !== 'number' || !Number.isInteger(v) || v < 0) {
                        throw new InvalidBlastCapError(level, cat, v);
                    }
                    merged[cat] = v;
                }
            }
            result[level] = merged;
        }
        return result;
    }
    counterKey(orgId, agentId, cycleId) {
        return `${orgId}:${agentId}:${cycleId}`;
    }
    chainKey(orgId, agentId) {
        return `${orgId}:${agentId}`;
    }
    getOrCreateCounters(orgId, agentId, cycleId) {
        const key = this.counterKey(orgId, agentId, cycleId);
        let c = this.counters.get(key);
        if (!c) {
            c = this.zeroCounters();
            this.counters.set(key, c);
        }
        return c;
    }
    zeroCounters() {
        return {
            'file-mutations': 0,
            'network-calls': 0,
            'database-writes': 0,
            'secret-reads': 0,
            'tool-invocations': 0,
        };
    }
    getChainState(orgId, agentId) {
        const key = this.chainKey(orgId, agentId);
        let s = this.chainState.get(key);
        if (!s) {
            s = { prevHash: '0'.repeat(64), eventCount: 0 };
            this.chainState.set(key, s);
        }
        return s;
    }
    advanceChain(orgId, agentId, currentHash) {
        const key = this.chainKey(orgId, agentId);
        const s = this.getChainState(orgId, agentId);
        s.prevHash = currentHash;
        s.eventCount += 1;
        this.chainState.set(key, s);
    }
    emitRejection(opts) {
        const prev = this.getChainState(opts.orgId, opts.agentId);
        const data = {
            category: opts.category,
            triggeredByCategory: opts.triggeredByCategory,
            level: opts.level,
            count: opts.count,
            cap: opts.cap,
            packBinding: opts.packBinding,
        };
        const currentHash = sha256(JSON.stringify({
            type: 'BLAST_RADIUS_CATEGORY_EXCEEDED',
            orgId: opts.orgId,
            agentId: opts.agentId,
            cycleId: opts.cycleId,
            ...data,
        }));
        const event = {
            type: 'BLAST_RADIUS_CATEGORY_EXCEEDED',
            severity: 'ERROR',
            orgId: opts.orgId,
            agentId: opts.agentId,
            cycleId: opts.cycleId,
            timestamp: new Date().toISOString(),
            prevHash: prev.prevHash,
            currentHash,
            data,
        };
        this.advanceChain(opts.orgId, opts.agentId, currentHash);
        this.auditSink(event);
    }
    emitAllowed_event(opts) {
        const prev = this.getChainState(opts.orgId, opts.agentId);
        const data = {
            category: opts.category,
            level: opts.level,
            packBinding: opts.packBinding,
            countersAfter: { ...opts.counters },
        };
        const currentHash = sha256(JSON.stringify({
            type: 'BLAST_RADIUS_ACTION_ALLOWED',
            orgId: opts.orgId,
            agentId: opts.agentId,
            cycleId: opts.cycleId,
            ...data,
        }));
        const event = {
            type: 'BLAST_RADIUS_ACTION_ALLOWED',
            severity: 'INFO',
            orgId: opts.orgId,
            agentId: opts.agentId,
            cycleId: opts.cycleId,
            timestamp: new Date().toISOString(),
            prevHash: prev.prevHash,
            currentHash,
            data,
        };
        this.advanceChain(opts.orgId, opts.agentId, currentHash);
        this.auditSink(event);
    }
}
// ---------------------------------------------------------------------------
// Utility
// ---------------------------------------------------------------------------
function sha256(input) {
    return crypto.createHash('sha256').update(input, 'utf8').digest('hex');
}
//# sourceMappingURL=blast-radius-enforcer.js.map