"use strict";
/**
 * GovernanceRuntime -- Core enforcement layer for AI agent governance.
 *
 * This module provides SHA-256 tamper detection on governance files,
 * deny-by-default permission filtering, and activity logging.
 *
 * Portable: works in any Claude Code project. No external dependencies.
 *
 * @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.GovernanceRuntime = exports.GovernanceTamperError = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const install_context_js_1 = require("./install-context.js");
class GovernanceTamperError extends Error {
    expectedHash;
    actualHash;
    file;
    constructor(expectedHash, actualHash, file) {
        super(`Governance file tampered: ${file}. ` +
            `Expected ${expectedHash.slice(0, 16)}..., got ${actualHash.slice(0, 16)}...`);
        this.expectedHash = expectedHash;
        this.actualHash = actualHash;
        this.file = file;
        this.name = 'GovernanceTamperError';
    }
}
exports.GovernanceTamperError = GovernanceTamperError;
class GovernanceRuntime {
    config;
    governanceHash = null;
    hashManifest = new Map();
    activityLog = [];
    /**
     * AIAG-024: Cache parsed permissions keyed by (agentName, file-mtime-ms).
     * Prevents a disk read on every preAction/filterTools/checkToolAccess call
     * in hot paths. Cache auto-invalidates when the permissions file is edited.
     */
    permissionsCache = new Map();
    /**
     * Wired install context (LicensePackLoader + MonthlyVerifyScheduler).
     * Undefined when no installJwt was provided or when JWT parsing fails.
     * Per Invariant 2: absence never blocks runtime operation.
     */
    installContext;
    constructor(config) {
        this.config = {
            tamperDetection: true,
            cycleTimeoutMs: 600000, // 10 min (DEV MODE -- production: 300000 / 5min)
            adminToken: '',
            installJwt: '',
            agentJwt: '',
            licenseServerBaseUrl: 'https://license.my-cc.io',
            ...config,
        };
        // Allow admin token from environment variable
        if (!this.config.adminToken && process.env.GOVERNANCE_ADMIN_TOKEN) {
            this.config.adminToken = process.env.GOVERNANCE_ADMIN_TOKEN;
        }
        // Ensure directories exist
        this.ensureDir(this.config.logDir);
        this.ensureDir(this.config.permissionsDir);
        // Compute initial governance hash and persist/verify against stored hash
        if (this.config.tamperDetection && fs.existsSync(this.config.governanceFile)) {
            const currentHash = this.computeHash(this.config.governanceFile);
            const hashFilePath = this.getHashFilePath();
            if (fs.existsSync(hashFilePath)) {
                // Compare against persisted hash from previous run
                const persistedHash = fs.readFileSync(hashFilePath, 'utf-8').trim();
                if (persistedHash && persistedHash !== currentHash) {
                    // Log the tamper detection event before throwing
                    this.logActivity({
                        timestamp: new Date().toISOString(),
                        agent: 'system',
                        action: 'governance_tamper_detected_on_startup',
                        target: this.config.governanceFile,
                        result: 'error',
                        detail: `Persisted hash ${persistedHash.slice(0, 16)}... does not match current ${currentHash.slice(0, 16)}...`,
                    });
                    throw new GovernanceTamperError(persistedHash, currentHash, this.config.governanceFile);
                }
            }
            this.governanceHash = currentHash;
            this.persistHash(currentHash);
        }
        // -----------------------------------------------------------------------
        // Wire install context (LicensePackLoader + MonthlyVerifyScheduler).
        // Per Invariant 2: never throw here — boot continues regardless.
        // -----------------------------------------------------------------------
        if (this.config.installJwt) {
            try {
                this.installContext = (0, install_context_js_1.initInstallContext)({
                    installJwt: this.config.installJwt,
                    agentJwt: this.config.agentJwt || this.config.installJwt,
                    licenseServerBaseUrl: this.config.licenseServerBaseUrl,
                });
                // Fire-and-forget pack loading at boot (Invariant 2: never block).
                if (this.installContext) {
                    const { loader, packCodes } = this.installContext;
                    void loader.loadPacksFromJwt(packCodes).catch(() => {
                        // Swallow — Invariant 2: failure must never surface to caller.
                    });
                }
            }
            catch {
                // Defensive: initInstallContext should never throw (it is fail-soft),
                // but guard here so the constructor is unconditionally safe.
                process.stderr.write('[GovernanceRuntime] Unexpected error wiring install context — ' +
                    'runtime continues without license pack loading (Invariant 2).\n');
                this.installContext = undefined;
            }
        }
    }
    /* -------------------------------------------------------------------------
     * Install context public API
     * Per Invariant 2: every method here is fail-soft and never throws.
     * ---------------------------------------------------------------------- */
    /**
     * Return the wired install context (LicensePackLoader + MonthlyVerifyScheduler),
     * or undefined when no installJwt was supplied or when JWT parsing failed.
     *
     * Callers can use the loader to reload packs on demand and the scheduler to
     * inspect the current license status via scheduler.licenseStatus().
     */
    getInstallContext() {
        return this.installContext;
    }
    /**
     * Notify the install context that a tool was observed for a given org/agent.
     *
     * Wire this into your PreToolUse hook:
     * ```ts
     * hooks.preToolUse = async (event) => {
     *   void runtime.onToolObserved(event.orgId, event.agentId);
     * };
     * ```
     *
     * On the first call of each calendar month per agent, this triggers
     * MonthlyVerifyScheduler.onToolObserved() → POST /api/v1/license/verify-monthly.
     * All subsequent calls in the same month are no-ops.
     *
     * Per Invariant 2: never throws. If no install context is wired, returns
     * immediately without side effects.
     *
     * @param orgId  - Org identifier (used for logging; scheduler uses agentJwt for auth).
     * @param agentId - Agent identifier passed to the scheduler.
     */
    onToolObserved(orgId, agentId) {
        if (!this.installContext)
            return;
        // Fire-and-forget — must never block the hook chain (Invariant 2).
        void this.installContext.scheduler.onToolObserved(agentId).catch(() => {
            // Swallow — schedule failures are handled inside the scheduler itself.
        });
        // Suppress unused-variable lint warning; orgId is reserved for future
        // per-org audit logging when the gov-server endpoint supports it.
        void orgId;
    }
    /**
     * Verify governance file integrity.
     * Throws GovernanceTamperError if tampered.
     */
    verifyGovernance() {
        const timestamp = new Date().toISOString();
        if (!fs.existsSync(this.config.governanceFile)) {
            return {
                valid: false,
                hash: '',
                expectedHash: this.governanceHash,
                timestamp,
            };
        }
        const currentHash = this.computeHash(this.config.governanceFile);
        if (this.governanceHash && currentHash !== this.governanceHash) {
            throw new GovernanceTamperError(this.governanceHash, currentHash, this.config.governanceFile);
        }
        // First run -- set baseline
        if (!this.governanceHash) {
            this.governanceHash = currentHash;
        }
        return {
            valid: true,
            hash: currentHash,
            expectedHash: this.governanceHash,
            timestamp,
        };
    }
    /**
     * Build and verify a hash manifest for multiple governance files.
     */
    buildHashManifest(files) {
        for (const file of files) {
            if (fs.existsSync(file)) {
                const hash = this.computeHash(file);
                this.hashManifest.set(file, hash);
            }
        }
        return new Map(this.hashManifest);
    }
    /**
     * Verify all files in the hash manifest.
     * Returns list of tampered/missing files.
     */
    verifyManifest() {
        const result = { tampered: [], missing: [], valid: [] };
        for (const [file, expectedHash] of this.hashManifest) {
            if (!fs.existsSync(file)) {
                result.missing.push(file);
                continue;
            }
            const currentHash = this.computeHash(file);
            if (currentHash !== expectedHash) {
                result.tampered.push(file);
            }
            else {
                result.valid.push(file);
            }
        }
        return result;
    }
    /**
     * Load and parse agent permissions from PERMISSIONS.md format.
     * Returns empty map if file missing (deny-by-default).
     */
    loadPermissions(agentName) {
        const permFile = path.join(this.config.permissionsDir, `${agentName}.md`);
        if (!fs.existsSync(permFile)) {
            // Deny-by-default: no permissions file = zero tools
            return new Map();
        }
        // AIAG-024: return cached parse if file hasn't changed since last load.
        let mtimeMs = 0;
        try {
            mtimeMs = fs.statSync(permFile).mtimeMs;
        }
        catch {
            mtimeMs = 0;
        }
        const cached = this.permissionsCache.get(agentName);
        if (cached && cached.mtimeMs === mtimeMs) {
            return cached.map;
        }
        const permissions = new Map();
        const content = fs.readFileSync(permFile, 'utf-8');
        const regex = /^-\s+(\w+)\s*:\s*(ALLOWED|BLOCKED|APPROVAL_REQUIRED)/gm;
        let match;
        while ((match = regex.exec(content)) !== null) {
            permissions.set(match[1], {
                tool: match[1],
                level: match[2],
            });
        }
        this.permissionsCache.set(agentName, { mtimeMs, map: permissions });
        return permissions;
    }
    /**
     * Filter tools based on agent permissions. Deny-by-default.
     * Tools not in permissions = BLOCKED.
     */
    filterTools(tools, agentName) {
        const permissions = this.loadPermissions(agentName);
        // No permissions file = zero tools (deny-by-default)
        if (permissions.size === 0) {
            return [];
        }
        return tools.filter((tool) => {
            const perm = permissions.get(tool);
            // Unlisted tool = BLOCKED (deny-by-default)
            if (!perm)
                return false;
            return perm.level === 'ALLOWED';
        });
    }
    /**
     * Check if a specific tool call is allowed for an agent.
     */
    checkToolAccess(toolName, agentName) {
        const permissions = this.loadPermissions(agentName);
        if (permissions.size === 0) {
            return {
                allowed: false,
                level: 'BLOCKED',
                reason: `No permissions file for agent '${agentName}'. Deny-by-default.`,
            };
        }
        const perm = permissions.get(toolName);
        if (!perm) {
            return {
                allowed: false,
                level: 'BLOCKED',
                reason: `Tool '${toolName}' not listed in permissions for '${agentName}'. Deny-by-default.`,
            };
        }
        return {
            allowed: perm.level === 'ALLOWED',
            level: perm.level,
            reason: perm.level === 'ALLOWED'
                ? 'Permitted'
                : perm.level === 'APPROVAL_REQUIRED'
                    ? 'Requires human approval before execution'
                    : 'Explicitly blocked',
        };
    }
    /**
     * Log an activity entry.
     */
    logActivity(entry) {
        this.activityLog.push(entry);
        // Write to disk
        const logFile = path.join(this.config.logDir, `activity-${new Date().toISOString().split('T')[0]}.jsonl`);
        try {
            fs.appendFileSync(logFile, JSON.stringify(entry) + '\n');
        }
        catch (err) {
            // Compliance logging failure is a critical event -- report to stderr
            process.stderr.write(`[GOVERNANCE CRITICAL] Failed to write audit log: ${err.message}\n`);
        }
    }
    /**
     * Get activity log entries (in-memory).
     */
    getActivityLog() {
        return [...this.activityLog];
    }
    /**
     * Update governance hash after an authorized change.
     * Only call this when a human operator intentionally modifies governance.
     * Requires an authorization token to prevent unauthorized hash resets.
     */
    updateGovernanceHash(authorizationToken) {
        if (!authorizationToken) {
            throw new Error('Authorization required: updateGovernanceHash() must be called with a valid authorization token. ' +
                'This prevents agents from silently resetting governance file integrity checks.');
        }
        // Validate the token against the configured admin token using timing-safe comparison
        const expectedToken = this.config.adminToken;
        if (!expectedToken) {
            throw new Error('No admin token configured. Set adminToken in GovernanceConfig or GOVERNANCE_ADMIN_TOKEN environment variable.');
        }
        const tokenBuffer = Buffer.from(authorizationToken);
        const expectedBuffer = Buffer.from(expectedToken);
        if (tokenBuffer.length !== expectedBuffer.length ||
            !crypto.timingSafeEqual(tokenBuffer, expectedBuffer)) {
            // Log the failed attempt
            this.logActivity({
                timestamp: new Date().toISOString(),
                agent: 'system',
                action: 'governance_hash_update_unauthorized',
                target: this.config.governanceFile,
                result: 'blocked',
                detail: 'Invalid authorization token provided for governance hash update',
            });
            throw new Error('Invalid authorization token. Governance hash update denied.');
        }
        if (!fs.existsSync(this.config.governanceFile)) {
            throw new Error('Governance file not found');
        }
        // Log the hash update for audit trail
        this.logActivity({
            timestamp: new Date().toISOString(),
            agent: 'system',
            action: 'governance_hash_update',
            target: this.config.governanceFile,
            result: 'allowed',
            detail: `Hash updated with authorization token ${authorizationToken.slice(0, 8)}...`,
        });
        this.governanceHash = this.computeHash(this.config.governanceFile);
        this.persistHash(this.governanceHash);
        return this.governanceHash;
    }
    /**
     * Export hash manifest to JSON file for persistence across sessions.
     */
    exportManifest(outputPath) {
        const manifest = {};
        for (const [file, hash] of this.hashManifest) {
            manifest[file] = hash;
        }
        fs.writeFileSync(outputPath, JSON.stringify(manifest, null, 2));
    }
    /**
     * Import hash manifest from JSON file.
     */
    importManifest(inputPath) {
        if (!fs.existsSync(inputPath))
            return;
        const manifest = JSON.parse(fs.readFileSync(inputPath, 'utf-8'));
        for (const [file, hash] of Object.entries(manifest)) {
            this.hashManifest.set(file, hash);
        }
    }
    /**
     * Get the path where the governance hash is persisted.
     */
    getHashFilePath() {
        return path.join(this.config.logDir, '.governance-hash');
    }
    /**
     * Persist governance hash to disk for cross-restart tamper detection.
     */
    persistHash(hash) {
        try {
            const hashFilePath = this.getHashFilePath();
            this.ensureDir(path.dirname(hashFilePath));
            fs.writeFileSync(hashFilePath, hash + '\n');
        }
        catch (err) {
            process.stderr.write(`[GOVERNANCE CRITICAL] Failed to persist governance hash: ${err.message}\n`);
        }
    }
    // ---------------------------------------------------------------------------
    // W2: Per-project pack selection
    // ---------------------------------------------------------------------------
    /**
     * Set the active compliance pack selection for a project via the governance server.
     *
     * Calls POST /api/v1/projects/:projectId/packs and emits a
     * project.packs.changed canonical audit event server-side.
     *
     * @param projectId - The project to configure
     * @param packIds - Pack IDs to activate for this project
     * @param versions - Optional semver pins per packId
     * @param opts - Connection options (apiUrl, licenseKey). Defaults to env vars.
     */
    async setProjectPacks(projectId, packIds, versions, opts) {
        const apiUrl = opts?.apiUrl ?? process.env['GOVERNANCE_API_URL'] ?? 'http://localhost:3200';
        const licenseKey = opts?.licenseKey ?? process.env['CONNEXUM_LICENSE'] ?? '';
        const payload = { packIds };
        if (versions)
            payload['versions'] = versions;
        const response = await fetch(`${apiUrl.replace(/\/$/, '')}/api/v1/projects/${encodeURIComponent(projectId)}/packs`, {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${licenseKey}`,
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(payload),
        });
        if (!response.ok) {
            const err = await response.json().catch(() => ({ error: `HTTP ${response.status}` }));
            throw new Error(`setProjectPacks failed (HTTP ${response.status}): ${err.error ?? 'unknown error'}`);
        }
        const data = await response.json();
        return {
            selectedPackIds: data.selectedPackIds,
            selectedPackVersions: data.selectedPackVersions,
        };
    }
    computeHash(filePath) {
        const content = fs.readFileSync(filePath);
        return crypto.createHash('sha256').update(content).digest('hex');
    }
    ensureDir(dir) {
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir, { recursive: true });
        }
    }
}
exports.GovernanceRuntime = GovernanceRuntime;
//# sourceMappingURL=governance-runtime.js.map