/**
 * GateGuard -- Forces agents to prove file comprehension before editing.
 *
 * Before any file modification, the agent must state:
 *   1. Imports/dependencies used by the code
 *   2. API surface (exports, routes, types)
 *   3. Verbatim instruction authorizing the change
 *
 * This prevents hallucination-based code modifications where an agent
 * "thinks" a function exists without having actually read the file.
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
export class GateGuard {
    config;
    readLog = new Map();
    auditFile;
    constructor(config) {
        this.config = {
            strictMode: true,
            activePackIds: [],
            exemptPatterns: [
                '*.json',
                '*.env*',
                '*.config.*',
                '*.lock',
                '.gitignore',
                'package.json',
                'tsconfig.json',
            ],
            readEditWindowMs: 10 * 60 * 1000,
            ...config,
        };
        if (!fs.existsSync(config.auditDir)) {
            fs.mkdirSync(config.auditDir, { recursive: true });
        }
        this.auditFile = path.join(config.auditDir, `gateguard-${new Date().toISOString().split('T')[0]}.jsonl`);
    }
    /**
     * Record that a file was read by an agent.
     * Call this when a Read tool is used.
     */
    recordRead(filePath, agentName) {
        const resolved = path.resolve(filePath);
        this.readLog.set(resolved, {
            filePath: resolved,
            timestamp: Date.now(),
            agent: agentName,
        });
    }
    /**
     * Verify that an agent can edit a file.
     * Returns allowed=true if the agent has demonstrated comprehension.
     */
    verify(filePath, agentName, verification) {
        const resolved = path.resolve(filePath);
        const filename = path.basename(resolved);
        // Check exemptions
        if (this.isExempt(filename)) {
            return { allowed: true, reason: 'File exempt from GateGuard', missingFacts: [] };
        }
        // Check if file is new (doesn't exist yet)
        if (!fs.existsSync(resolved)) {
            return { allowed: true, reason: 'New file creation', missingFacts: [] };
        }
        // Check if file was read in this session
        const readRecord = this.readLog.get(resolved);
        if (!readRecord) {
            this.logAudit(agentName, resolved, 'BLOCKED', 'File not read in current session');
            return {
                allowed: false,
                reason: `File '${filename}' was not read in this session. Read it first before editing.`,
                missingFacts: ['file_not_read'],
            };
        }
        // Check if read was by the same agent
        if (readRecord.agent !== agentName) {
            this.logAudit(agentName, resolved, 'BLOCKED', `Read by different agent: ${readRecord.agent}`);
            return {
                allowed: false,
                reason: `File was read by '${readRecord.agent}', not by '${agentName}'. Each agent must read files they modify.`,
                missingFacts: ['wrong_agent'],
            };
        }
        // Check read-edit window
        const elapsed = Date.now() - readRecord.timestamp;
        if (elapsed > this.config.readEditWindowMs) {
            this.logAudit(agentName, resolved, 'BLOCKED', `Read too long ago: ${Math.round(elapsed / 1000)}s`);
            return {
                allowed: false,
                reason: `File was read ${Math.round(elapsed / 60000)} minutes ago. Re-read before editing (max ${this.config.readEditWindowMs / 60000} min window).`,
                missingFacts: ['stale_read'],
            };
        }
        // Verify fact categories
        const missingFacts = [];
        let factCount = 0;
        if (verification.imports && verification.imports.length > 0) {
            factCount++;
        }
        else {
            missingFacts.push('imports');
        }
        if (verification.apiSurface && verification.apiSurface.length > 0) {
            factCount++;
        }
        else {
            missingFacts.push('api_surface');
        }
        if (verification.authorization && verification.authorization.trim().length > 0) {
            factCount++;
        }
        else {
            missingFacts.push('authorization');
        }
        const requiredFacts = this.config.strictMode ? 3 : 2;
        // SYSTEMIC-M3: when strictMode is false, every audit event is flagged so
        // auditors can identify non-strict sessions on the chain.
        if (!this.config.strictMode) {
            this.logAudit(agentName, resolved, 'WARN', 'strictMode-disabled: running at 2-of-3 fact requirement');
            // Emit an additional WARN if any EU AI Act pack is active -- EU AI Act
            // Art. 14 requires human oversight; disabling strictMode removes the
            // mandatory authorization field check.
            const euPackIds = ['eu-ai-act', 'eu_ai_act', 'euaiact'];
            const hasEuPack = this.config.activePackIds.some((id) => euPackIds.includes(id.toLowerCase()));
            if (hasEuPack) {
                this.logAudit(agentName, resolved, 'WARN', 'strictMode-disabled with EU AI Act pack active: human oversight requirement (Art. 14) may not be met');
            }
        }
        if (factCount < requiredFacts) {
            this.logAudit(agentName, resolved, 'BLOCKED', `Only ${factCount}/${requiredFacts} facts provided. Missing: ${missingFacts.join(', ')}`);
            return {
                allowed: false,
                reason: `GateGuard requires ${requiredFacts} of 3 fact categories. Got ${factCount}. Missing: ${missingFacts.join(', ')}`,
                missingFacts,
            };
        }
        // Verify that stated imports actually exist in the file
        if (verification.imports && verification.imports.length > 0) {
            const fileContent = fs.readFileSync(resolved, 'utf-8');
            const falseImports = verification.imports.filter((imp) => !fileContent.includes(imp));
            if (falseImports.length > 0 && falseImports.length === verification.imports.length) {
                this.logAudit(agentName, resolved, 'BLOCKED', `All stated imports are false: ${falseImports.join(', ')}`);
                return {
                    allowed: false,
                    reason: `None of the stated imports were found in the file. This suggests the file was not actually read or understood.`,
                    missingFacts: ['false_imports'],
                };
            }
        }
        // All checks passed
        this.logAudit(agentName, resolved, 'ALLOWED', `${factCount} facts verified`);
        return {
            allowed: true,
            reason: 'GateGuard verification passed',
            missingFacts: [],
            verification: {
                filePath: resolved,
                imports: verification.imports || [],
                apiSurface: verification.apiSurface || [],
                authorization: verification.authorization || '',
                readTimestamp: new Date(readRecord.timestamp).toISOString(),
            },
        };
    }
    /**
     * Quick check: was this file read recently by this agent?
     * Lighter weight than full verify() for preliminary checks.
     */
    wasRead(filePath, agentName) {
        const resolved = path.resolve(filePath);
        const record = this.readLog.get(resolved);
        if (!record)
            return false;
        if (record.agent !== agentName)
            return false;
        return (Date.now() - record.timestamp) <= this.config.readEditWindowMs;
    }
    /**
     * Get read log for monitoring/debugging.
     */
    getReadLog() {
        return Array.from(this.readLog.values());
    }
    /**
     * Clear stale read records (older than window).
     */
    clearStaleReads() {
        const now = Date.now();
        let cleared = 0;
        for (const [key, record] of this.readLog) {
            if (now - record.timestamp > this.config.readEditWindowMs * 2) {
                this.readLog.delete(key);
                cleared++;
            }
        }
        return cleared;
    }
    /**
     * Parse a GateGuard block from agent reasoning.
     * Expected format:
     *   <!-- GATEGUARD
     *   imports: fs, path, crypto
     *   api: createCapabilityBundle, parsePermissions
     *   auth: "User requested adding HMAC verification to handoffs"
     *   -->
     */
    static parseGateGuardBlock(text) {
        const match = text.match(/<!--\s*GATEGUARD\s*([\s\S]*?)-->/i);
        if (!match)
            return null;
        const block = match[1];
        const result = {};
        const importsMatch = block.match(/imports?:\s*(.+)/i);
        if (importsMatch) {
            result.imports = importsMatch[1].split(',').map((s) => s.trim()).filter(Boolean);
        }
        const apiMatch = block.match(/api(?:_surface)?:\s*(.+)/i);
        if (apiMatch) {
            result.apiSurface = apiMatch[1].split(',').map((s) => s.trim()).filter(Boolean);
        }
        const authMatch = block.match(/auth(?:orization)?:\s*"?([^"]+)"?/i);
        if (authMatch) {
            result.authorization = authMatch[1].trim();
        }
        return result;
    }
    isExempt(filename) {
        return this.config.exemptPatterns.some((pattern) => {
            // Handle glob patterns with internal wildcards like *.config.* and *.env*
            if (pattern.includes('*')) {
                // Convert glob to regex: * -> .*, escape dots
                const regexStr = '^' + pattern
                    .replace(/\./g, '\\.')
                    .replace(/\*/g, '.*')
                    + '$';
                try {
                    return new RegExp(regexStr).test(filename);
                }
                catch {
                    return false;
                }
            }
            return filename === pattern;
        });
    }
    logAudit(agent, file, result, detail) {
        const entry = {
            timestamp: new Date().toISOString(),
            agent,
            file,
            result,
            detail,
        };
        try {
            fs.appendFileSync(this.auditFile, JSON.stringify(entry) + '\n');
        }
        catch {
            // Fire-and-forget
        }
    }
}
//# sourceMappingURL=gateguard.js.map