/**
 * HookProfileSelector -- Per-agent posture dial with audited maintenance mode.
 *
 * Provides three named profiles (minimal / standard / strict) that bundle
 * related enforcement hooks into coherent postures. Operators pick one per
 * agent; the selector resolves which hook names are active for a given agent
 * at any point in time.
 *
 * Maintenance mode allows temporary, time-bounded, audited relaxation of hooks
 * above the safety floor. The minimal-profile hook set is the floor: it can
 * never be disabled via maintenance mode.
 *
 * See docs/runtime/hook-profile-selector.md for the full contract.
 *
 * BUILD_PLAN V3 line 267: "Hook profile selector (minimal / standard / strict)
 * with maintenance mode audit trail".
 *
 * Related PRs:
 *   PR #12 (cycle-token-budget), PR #13 (cycle-wall-clock-timeout),
 *   PR #14 (tool-rate-limit), PR #15 (session-memory),
 *   PR #17 (self-reflection-loop-detector), PR #18 (rag-source-allowlist),
 *   PR #19 (external-cross-check), PR #20 (rag-citation-enforcement).
 *
 * Compliance bindings:
 *   - HIPAA 164.308(a)(5): Security Awareness -- documented posture per agent
 *   - SOC 2 CC6.1: Logical access controls -- profile-bounded enforcement
 *   - EU AI Act Art. 9: Risk management -- tiered posture selection
 *   - ISO 42001 A.6.1.4: AI resource management -- maintenance audit trail
 *   - NIST 800-53 CM-3: Configuration change control -- profile change record
 *
 * @connexum/ai-governance
 */
import * as crypto from 'node:crypto';
// ---------------------------------------------------------------------------
// Hook name constants
// (Each string is the canonical hook identifier used by enforcers.)
// ---------------------------------------------------------------------------
/**
 * MINIMAL profile hook set -- the absolute safety floor.
 *
 * These hooks are ALWAYS active regardless of profile or maintenance mode.
 * Maintenance mode CANNOT disable any hook in this set.
 *
 * - audit-chain:            SHA-256 tamper-evident audit trail (AuditIntegrity)
 * - capability-immutability: CapabilityBundle immutability enforcement
 * - web-permission-tier:    Agent web access level enforcement
 * - circuit-breaker:        CircuitBreaker fail-safe (3 failures = suspend)
 */
export const MINIMAL_HOOKS = new Set([
    'audit-chain',
    'capability-immutability',
    'web-permission-tier',
    'circuit-breaker',
]);
/**
 * STANDARD profile additions on top of MINIMAL.
 *
 * - cycle-wall-clock-timeout: PR #13 -- wall-clock cycle timeout
 * - cycle-token-budget:       PR #12 -- per-cycle token budget gate
 * - session-memory-auto-clear: PR #15 -- session-scoped memory auto-clear
 * - tool-rate-limit:          PR #14 -- per-tool sliding-window rate gate
 *                             (pass-through by default when no rules configured)
 */
export const STANDARD_ADDITIONS = new Set([
    'cycle-wall-clock-timeout',
    'cycle-token-budget',
    'session-memory-auto-clear',
    'tool-rate-limit',
]);
/**
 * STRICT profile additions on top of STANDARD.
 *
 * - rag-source-allowlist:         PR #18 -- retrieval source allowlisting
 * - rag-citation-enforcement:     PR #20 -- citation chain-of-custody
 * - self-reflection-loop-detector: PR #17 -- loop detection for self-reflecting agents
 * - external-cross-check:         PR #19 -- external verification of reflection outcomes
 *                                 (human-only mode by default; operator can flip to peer-agent)
 */
export const STRICT_ADDITIONS = new Set([
    'rag-source-allowlist',
    'rag-citation-enforcement',
    'self-reflection-loop-detector',
    'external-cross-check',
]);
/** Full hook set per profile (pre-computed for fast lookup). */
export const PROFILE_HOOKS = {
    minimal: MINIMAL_HOOKS,
    standard: new Set([...MINIMAL_HOOKS, ...STANDARD_ADDITIONS]),
    strict: new Set([...MINIMAL_HOOKS, ...STANDARD_ADDITIONS, ...STRICT_ADDITIONS]),
};
// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------
export class ProfileChangeReasonRequiredError extends Error {
    constructor() {
        super('HookProfileSelector: a non-empty reason string is required when setting a profile. ' +
            'Empty or whitespace-only reasons are rejected to prevent silent posture changes.');
        this.name = 'ProfileChangeReasonRequiredError';
    }
}
export class MaintenanceDurationExceededError extends Error {
    constructor(requestedMs, maxMs) {
        super(`HookProfileSelector: maintenance mode durationMs ${requestedMs} ms exceeds the ` +
            `maximum allowed duration of ${maxMs} ms (${maxMs / 3_600_000} hours).`);
        this.name = 'MaintenanceDurationExceededError';
    }
}
export class MaintenanceCannotDisableFloorHookError extends Error {
    hookName;
    constructor(hookName) {
        super(`HookProfileSelector: cannot disable '${hookName}' via maintenance mode -- ` +
            `it is part of the minimal-profile safety floor and must remain active at all times.`);
        this.name = 'MaintenanceCannotDisableFloorHookError';
        this.hookName = hookName;
    }
}
export class PackBindingRequiredError extends Error {
    constructor(operation) {
        super(`HookProfileSelector: packBinding is required for '${operation}'. ` +
            'Every profile change or maintenance-mode operation must declare the governing compliance packs.');
        this.name = 'PackBindingRequiredError';
    }
}
// ---------------------------------------------------------------------------
// Maximum maintenance mode duration
// ---------------------------------------------------------------------------
/** 4 hours in milliseconds -- operators who need longer must explicitly renew. */
export const MAX_MAINTENANCE_DURATION_MS = 4 * 60 * 60 * 1_000;
// ---------------------------------------------------------------------------
// HookProfileSelector
// ---------------------------------------------------------------------------
export class HookProfileSelector {
    /** key: `${orgId}::${agentId}` */
    profiles = new Map();
    /** key: `${orgId}::${agentId}` */
    maintenance = new Map();
    /** key: `${orgId}::${agentId}` -- last audit hash per agent */
    lastHashes = new Map();
    auditSink;
    constructor(auditSink) {
        this.auditSink = auditSink ?? (() => undefined);
    }
    // -------------------------------------------------------------------------
    // Profile management
    // -------------------------------------------------------------------------
    /**
     * Set the active hook profile for an agent.
     *
     * @param orgId       Tenant / organisation identifier.
     * @param agentId     Agent identifier within the org.
     * @param profile     One of 'minimal' | 'standard' | 'strict'.
     * @param packBinding Compliance pack IDs that govern this agent (mandatory, non-empty).
     * @param reason      Human-readable explanation -- mandatory, must be non-empty.
     */
    setProfile(orgId, agentId, profile, packBinding, reason) {
        if (!reason || reason.trim() === '') {
            throw new ProfileChangeReasonRequiredError();
        }
        if (!packBinding || packBinding.length === 0) {
            throw new PackBindingRequiredError('setProfile');
        }
        const key = this.key(orgId, agentId);
        const previous = this.profiles.get(key);
        this.profiles.set(key, {
            profile,
            packBinding,
            setAt: new Date().toISOString(),
            reason,
            operatorOverrides: previous?.operatorOverrides ?? new Map(),
        });
        this.emitAudit({
            type: 'PROFILE_CHANGED',
            orgId,
            agentId,
            severity: 'INFO',
            data: {
                previousProfile: previous?.profile ?? null,
                newProfile: profile,
                reason,
                packBinding,
            },
        });
    }
    /**
     * Get the active profile for an agent. Returns 'standard' if not explicitly set.
     */
    getProfile(orgId, agentId) {
        return this.profiles.get(this.key(orgId, agentId))?.profile ?? 'standard';
    }
    // -------------------------------------------------------------------------
    // Hook resolution
    // -------------------------------------------------------------------------
    /**
     * Returns the set of currently enabled hook names for an agent.
     *
     * Resolution order:
     *   1. Start with the profile's base hook set.
     *   2. Subtract hooks currently disabled by maintenance mode.
     *   3. Apply operator explicit overrides (hook explicitly enabled wins even
     *      if maintenance mode disabled it; a hook explicitly disabled overrides
     *      the profile default).
     *
     * NOTE: Minimal-floor hooks are never returned as disabled -- maintenance mode
     * cannot subtract them and operator overrides cannot disable them.
     */
    getEnabledHooks(orgId, agentId) {
        const profile = this.getProfile(orgId, agentId);
        const base = PROFILE_HOOKS[profile];
        const key = this.key(orgId, agentId);
        const maint = this.maintenance.get(key);
        const state = this.profiles.get(key);
        const enabled = new Set(base);
        // Subtract maintenance-disabled hooks (floor hooks are immune)
        if (maint) {
            for (const h of maint.disabledHooks) {
                if (!MINIMAL_HOOKS.has(h)) {
                    enabled.delete(h);
                }
            }
        }
        // Apply operator overrides
        if (state) {
            for (const [h, on] of state.operatorOverrides) {
                if (on) {
                    // Explicitly enabled -- add it even if not in profile
                    enabled.add(h);
                }
                else {
                    // Explicitly disabled -- but never allow floor hooks to be removed
                    if (!MINIMAL_HOOKS.has(h)) {
                        enabled.delete(h);
                    }
                }
            }
        }
        return enabled;
    }
    /**
     * Returns whether a specific hook is currently enabled for an agent.
     *
     * This is the primary integration surface for enforcers. An enforcer calls
     * this at the top of its guard methods and becomes a pass-through when it
     * returns false.
     *
     * Usage pattern for new enforcers:
     *
     *   guardPreTool(orgId: string, agentId: string, ...): void {
     *     if (!selector.isHookEnabled(orgId, agentId, 'cycle-token-budget')) return;
     *     // ... existing guard logic ...
     *   }
     *
     * NOTE: Enforcers integrated via this API should emit a single
     * "hook skipped due to profile" audit event per cycle (not per call)
     * to avoid flooding the audit chain.
     */
    isHookEnabled(orgId, agentId, hookName) {
        return this.getEnabledHooks(orgId, agentId).has(hookName);
    }
    // -------------------------------------------------------------------------
    // Operator explicit overrides
    // -------------------------------------------------------------------------
    /**
     * Explicitly enable or disable a hook for an agent, overriding the profile
     * default. Floor hooks cannot be disabled.
     *
     * This is the operator-level escape hatch for individual hook customisation
     * beyond what the three profiles provide. Use sparingly.
     */
    setHookOverride(orgId, agentId, hookName, enabled) {
        if (!enabled && MINIMAL_HOOKS.has(hookName)) {
            throw new MaintenanceCannotDisableFloorHookError(hookName);
        }
        const key = this.key(orgId, agentId);
        const state = this.profiles.get(key);
        if (state) {
            state.operatorOverrides.set(hookName, enabled);
        }
        else {
            // Agent has no explicit profile yet -- create a stub with standard defaults
            this.profiles.set(key, {
                profile: 'standard',
                packBinding: [],
                setAt: new Date().toISOString(),
                reason: 'operator override applied before explicit profile set',
                operatorOverrides: new Map([[hookName, enabled]]),
            });
        }
    }
    // -------------------------------------------------------------------------
    // Maintenance mode
    // -------------------------------------------------------------------------
    /**
     * Enter maintenance mode for an agent: temporarily disable specific hooks
     * above the safety floor.
     *
     * - Maximum duration: 4 hours (MAX_MAINTENANCE_DURATION_MS).
     * - Cannot disable floor hooks (minimal profile set).
     * - Reason and approvedBy are mandatory.
     * - Emits MAINTENANCE_MODE_ENTERED audit event at severity ERROR.
     * - Auto-exit fires MAINTENANCE_MODE_AUTO_EXITED at severity WARNING.
     *
     * PERSISTENCE CAVEAT (v0 limitation):
     *   The auto-exit timer is in-memory only. If the process restarts while
     *   maintenance mode is active, the timer will not fire and hooks will
     *   remain in whichever state they were last saved. On startup, callers
     *   should call checkMaintenanceExpiry() to expire any overdue entries.
     *   Persistent storage of maintenance-mode state (deadline checkpointing)
     *   is deferred to v1 as DEFERRED-HOOK-PROFILE-PERSIST-MAINTENANCE.
     */
    enterMaintenanceMode(orgId, agentId, opts, packBinding) {
        if (!opts.reason || opts.reason.trim() === '') {
            throw new ProfileChangeReasonRequiredError();
        }
        if (!packBinding || packBinding.length === 0) {
            throw new PackBindingRequiredError('enterMaintenanceMode');
        }
        if (opts.durationMs > MAX_MAINTENANCE_DURATION_MS) {
            throw new MaintenanceDurationExceededError(opts.durationMs, MAX_MAINTENANCE_DURATION_MS);
        }
        // Validate: no floor hooks in the disabled set
        for (const hookName of opts.disabledHooks) {
            if (MINIMAL_HOOKS.has(hookName)) {
                throw new MaintenanceCannotDisableFloorHookError(hookName);
            }
        }
        const key = this.key(orgId, agentId);
        // Cancel existing timer if re-entering
        const existing = this.maintenance.get(key);
        if (existing?.timerHandle) {
            clearTimeout(existing.timerHandle);
        }
        const now = Date.now();
        const expiresAt = now + opts.durationMs;
        const timerHandle = setTimeout(() => {
            this.autoExitMaintenanceMode(orgId, agentId);
        }, opts.durationMs);
        // Allow the timer to be garbage-collected if the process is shutting down
        if (timerHandle.unref) {
            timerHandle.unref();
        }
        this.maintenance.set(key, {
            disabledHooks: new Set(opts.disabledHooks),
            reason: opts.reason,
            approvedBy: opts.approvedBy,
            enteredAt: new Date(now).toISOString(),
            expiresAt,
            packBinding,
            timerHandle,
        });
        this.emitAudit({
            type: 'MAINTENANCE_MODE_ENTERED',
            orgId,
            agentId,
            severity: 'ERROR', // Posture relaxation is unusual -- deserves a red entry
            data: {
                disabledHooks: opts.disabledHooks,
                reason: opts.reason,
                approvedBy: opts.approvedBy,
                durationMs: opts.durationMs,
                expiresAt: new Date(expiresAt).toISOString(),
                packBinding,
            },
        });
    }
    /**
     * Exit maintenance mode immediately and re-enable all temporarily-disabled hooks.
     *
     * Emits MAINTENANCE_MODE_EXITED at severity NOTICE.
     */
    exitMaintenanceMode(orgId, agentId) {
        const key = this.key(orgId, agentId);
        const maint = this.maintenance.get(key);
        if (!maint)
            return; // Idempotent -- already not in maintenance mode
        if (maint.timerHandle) {
            clearTimeout(maint.timerHandle);
        }
        const disabledHooks = [...maint.disabledHooks];
        this.maintenance.delete(key);
        this.emitAudit({
            type: 'MAINTENANCE_MODE_EXITED',
            orgId,
            agentId,
            severity: 'NOTICE',
            data: {
                disabledHooks,
                reason: maint.reason,
                approvedBy: maint.approvedBy,
                enteredAt: maint.enteredAt,
                exitedAt: new Date().toISOString(),
                packBinding: maint.packBinding,
            },
        });
    }
    /**
     * Check whether a maintenance-mode entry has expired (for post-restart recovery).
     *
     * If the deadline has passed, fires auto-exit logic including the WARNING
     * audit event. Call this from application startup to handle the case where
     * a process restarted while maintenance mode was active.
     *
     * This is the v0 mitigation for the in-memory-timer limitation documented
     * in enterMaintenanceMode(). See DEFERRED-HOOK-PROFILE-PERSIST-MAINTENANCE.
     */
    checkMaintenanceExpiry(orgId, agentId) {
        const key = this.key(orgId, agentId);
        const maint = this.maintenance.get(key);
        if (!maint)
            return;
        if (Date.now() >= maint.expiresAt) {
            this.autoExitMaintenanceMode(orgId, agentId);
        }
    }
    /**
     * Returns true when the agent is currently in maintenance mode.
     */
    isInMaintenanceMode(orgId, agentId) {
        return this.maintenance.has(this.key(orgId, agentId));
    }
    // -------------------------------------------------------------------------
    // Private helpers
    // -------------------------------------------------------------------------
    autoExitMaintenanceMode(orgId, agentId) {
        const key = this.key(orgId, agentId);
        const maint = this.maintenance.get(key);
        if (!maint)
            return;
        const disabledHooks = [...maint.disabledHooks];
        this.maintenance.delete(key);
        // WARNING -- auto-exit should not be the norm; operators should renew explicitly
        this.emitAudit({
            type: 'MAINTENANCE_MODE_AUTO_EXITED',
            orgId,
            agentId,
            severity: 'WARNING',
            data: {
                disabledHooks,
                reason: maint.reason,
                approvedBy: maint.approvedBy,
                enteredAt: maint.enteredAt,
                autoExitedAt: new Date().toISOString(),
                packBinding: maint.packBinding,
            },
        });
    }
    key(orgId, agentId) {
        return `${orgId}::${agentId}`;
    }
    emitAudit(opts) {
        const key = this.key(opts.orgId, opts.agentId);
        const prevHash = this.lastHashes.get(key) ?? '';
        const timestamp = new Date().toISOString();
        const payload = {
            type: opts.type,
            orgId: opts.orgId,
            agentId: opts.agentId,
            severity: opts.severity,
            timestamp,
            data: opts.data,
        };
        const hash = crypto
            .createHash('sha256')
            .update(JSON.stringify(payload) + prevHash)
            .digest('hex');
        const event = {
            ...payload,
            prevHash,
            hash,
        };
        this.lastHashes.set(key, hash);
        this.auditSink(event);
    }
}
//# sourceMappingURL=hook-profile.js.map