/**
 * ToolRateLimiter -- Per-(agentId, toolName) sliding-window rate gate.
 *
 * Fires during PreToolUse before any tool dispatches. If the limit is
 * breached the gate throws synchronously (fail-closed). Supports per-tool
 * and per-pack override policies so compliance packs can enforce
 * tighter invocation budgets on sensitive tools.
 *
 * P3-T1 coverage gap: Tool-scoped rate limiting per agent.
 *
 * Compliance bindings:
 *   - SOC 2 CC6.1: access control
 *   - HIPAA 164.312(a)(1): access control minimum-necessary
 *   - MRPC 1.6 / legal packs: per-matter tool budget
 *
 * @connexum/ai-governance
 */
// ---------------------------------------------------------------------------
// Error
// ---------------------------------------------------------------------------
export class ToolRateLimitExceededError extends Error {
    agentId;
    toolName;
    retryAfterMs;
    packBinding;
    constructor(opts) {
        super(`Rate limit exceeded: agent '${opts.agentId}' has reached the invocation limit for tool '${opts.toolName}'. ` +
            `Retry after ${opts.retryAfterMs} ms.`);
        this.name = 'ToolRateLimitExceededError';
        this.agentId = opts.agentId;
        this.toolName = opts.toolName;
        this.retryAfterMs = opts.retryAfterMs;
        this.packBinding = opts.packBinding ?? [];
    }
}
// ---------------------------------------------------------------------------
// ToolRateLimiter
// ---------------------------------------------------------------------------
export class ToolRateLimiter {
    config;
    /** Map key: `${agentId}::${toolName}` */
    windows = new Map();
    constructor(config) {
        if (!config.defaultLimit || config.defaultLimit.windowMs <= 0) {
            throw new Error('ToolRateLimiter: defaultLimit.windowMs must be > 0');
        }
        if (config.defaultLimit.maxInvocations <= 0) {
            throw new Error('ToolRateLimiter: defaultLimit.maxInvocations must be > 0');
        }
        this.config = config;
    }
    /**
     * Resolve the rate-limit window for a given tool, considering the active
     * pack binding. Returns the most-specific matching window.
     */
    resolveWindow(toolName, activePackIds = []) {
        // 1. Per-tool override (highest precedence).
        if (this.config.perToolOverrides[toolName]) {
            return this.config.perToolOverrides[toolName];
        }
        // 2. Per-pack override -- use the first matching active pack.
        for (const packId of activePackIds) {
            if (this.config.perPackOverrides[packId]) {
                return this.config.perPackOverrides[packId];
            }
        }
        // 3. Default.
        return this.config.defaultLimit;
    }
    /**
     * PreToolUse hook entry point.
     *
     * Records the invocation timestamp and throws synchronously if the sliding
     * window limit is exceeded.
     *
     * @param agentId     Identity of the calling agent.
     * @param toolName    Name of the tool being invoked.
     * @param activePackIds Active compliance pack IDs for context/override.
     */
    hook(agentId, toolName, activePackIds = []) {
        const limit = this.resolveWindow(toolName, activePackIds);
        const key = `${agentId}::${toolName}`;
        const now = Date.now();
        const cutoff = now - limit.windowMs;
        // Retrieve or create window entry.
        let entry = this.windows.get(key);
        if (!entry) {
            entry = { timestamps: [] };
            this.windows.set(key, entry);
        }
        // Evict timestamps outside the sliding window.
        entry.timestamps = entry.timestamps.filter((t) => t > cutoff);
        if (entry.timestamps.length >= limit.maxInvocations) {
            // Oldest timestamp in the window determines when the slot frees up.
            const oldest = entry.timestamps[0];
            const retryAfterMs = oldest + limit.windowMs - now;
            const auditEvent = {
                type: 'governance:tool-rate-limited',
                agentId,
                toolName,
                windowMs: limit.windowMs,
                maxInvocations: limit.maxInvocations,
                currentCount: entry.timestamps.length,
                retryAfterMs: Math.max(retryAfterMs, 0),
                packBinding: activePackIds,
                timestamp: new Date().toISOString(),
            };
            this.config.onRateLimited?.(auditEvent);
            throw new ToolRateLimitExceededError({
                agentId,
                toolName,
                retryAfterMs: Math.max(retryAfterMs, 0),
                packBinding: activePackIds,
            });
        }
        // Record successful invocation.
        entry.timestamps.push(now);
    }
    /**
     * Return the current invocation count in the sliding window for a key.
     * Useful for monitoring and tests.
     */
    getCount(agentId, toolName, activePackIds = []) {
        const limit = this.resolveWindow(toolName, activePackIds);
        const key = `${agentId}::${toolName}`;
        const now = Date.now();
        const cutoff = now - limit.windowMs;
        const entry = this.windows.get(key);
        if (!entry)
            return 0;
        return entry.timestamps.filter((t) => t > cutoff).length;
    }
    /**
     * Reset counters for a specific agent/tool pair (for tests or operator reset).
     */
    reset(agentId, toolName) {
        this.windows.delete(`${agentId}::${toolName}`);
    }
}
//# sourceMappingURL=tool-rate-limit.js.map