/**
 * CapabilityBundle -- Frozen, sandboxed capability set for AI agents.
 *
 * Creates an Object.freeze'd bundle of allowed operations per agent.
 * Agents cannot monkey-patch or extend their own permissions at runtime.
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import { execFile } from 'child_process';
const DEFAULT_IMMUTABLE = ['SOUL.md', 'governance.md'];
const DEFAULT_APPEND_ONLY = ['handoff.md', 'learnings.md'];
const DEFAULT_BASH_ALLOWLIST = [
    'ls', 'cat', 'head', 'tail', 'wc', 'grep', 'find',
    'date', 'uptime', 'df', 'du', 'free', 'ps',
    'node',
    // npm, npx, git REMOVED from defaults (security risk):
    // npm installs packages (supply chain attack vector)
    // npx executes arbitrary packages
    // git push can exfiltrate data
    // Clients/packs can add these explicitly via bashAllowlist config
];
const DEFAULT_COMMAND_TIMEOUT = 30000;
const MEMORY_HASH_PATTERN = /<!-- HASH:sha256:([a-f0-9]{64}) -->/;
/**
 * Creates a frozen capability bundle for an agent.
 * The returned object cannot be modified at runtime.
 */
export function createCapabilityBundle(config) {
    const { agentName, agentDir, sharedDir, immutableFiles = DEFAULT_IMMUTABLE, appendOnlyFiles = DEFAULT_APPEND_ONLY, bashAllowlist = DEFAULT_BASH_ALLOWLIST, commandTimeoutMs = DEFAULT_COMMAND_TIMEOUT, } = config;
    function resolveSafe(base, relative) {
        const resolved = path.resolve(base, relative);
        const resolvedBase = path.resolve(base);
        if (!resolved.startsWith(resolvedBase)) {
            return null; // Path traversal attempt
        }
        // Resolve symlinks to prevent escaping sandbox via symlink
        try {
            if (fs.existsSync(resolved)) {
                const realPath = fs.realpathSync(resolved);
                if (!realPath.startsWith(resolvedBase)) {
                    return null; // Symlink escapes sandbox
                }
                return realPath;
            }
        }
        catch {
            // File doesn't exist yet (new file creation) -- allow based on path check
        }
        return resolved;
    }
    function isImmutable(filePath) {
        const basename = path.basename(filePath);
        return immutableFiles.some((f) => basename.toLowerCase() === f.toLowerCase());
    }
    function isAppendOnly(filePath) {
        const basename = path.basename(filePath);
        return appendOnlyFiles.some((f) => basename.toLowerCase() === f.toLowerCase());
    }
    function computeContentHash(content) {
        return crypto.createHash('sha256').update(content).digest('hex');
    }
    const bundle = {
        agentName,
        agentDir,
        sharedDir,
        readFile(relativePath) {
            // Can read from agentDir or sharedDir
            let resolved = resolveSafe(agentDir, relativePath);
            if (!resolved || !fs.existsSync(resolved)) {
                resolved = resolveSafe(sharedDir, relativePath);
            }
            if (!resolved || !fs.existsSync(resolved)) {
                return { error: `File not found: ${relativePath}` };
            }
            try {
                const content = fs.readFileSync(resolved, 'utf-8');
                return { content };
            }
            catch (err) {
                return { error: `Read failed: ${err.message}` };
            }
        },
        writeFile(relativePath, content) {
            const resolved = resolveSafe(agentDir, relativePath);
            if (!resolved) {
                return { error: `Path escapes agent directory: ${relativePath}` };
            }
            if (isImmutable(resolved)) {
                return { error: `Cannot modify immutable file: ${path.basename(resolved)}` };
            }
            if (isAppendOnly(resolved)) {
                return { error: `File is append-only: ${path.basename(resolved)}. Use appendFile().` };
            }
            try {
                const dir = path.dirname(resolved);
                if (!fs.existsSync(dir)) {
                    fs.mkdirSync(dir, { recursive: true });
                }
                // Add integrity hash for memory files
                let finalContent = content;
                if (path.basename(resolved) === 'memory.md') {
                    const hash = computeContentHash(content);
                    finalContent = content + `\n<!-- HASH:sha256:${hash} -->`;
                    // Create backup
                    if (fs.existsSync(resolved)) {
                        fs.copyFileSync(resolved, resolved + '.bak');
                    }
                }
                fs.writeFileSync(resolved, finalContent);
                return { ok: true, path: resolved };
            }
            catch (err) {
                return { error: `Write failed: ${err.message}` };
            }
        },
        appendFile(relativePath, content) {
            // Can append to agentDir or specific shared files
            let resolved = resolveSafe(agentDir, relativePath);
            if (!resolved) {
                resolved = resolveSafe(sharedDir, relativePath);
            }
            if (!resolved) {
                return { error: `Invalid path: ${relativePath}` };
            }
            if (isImmutable(resolved)) {
                return { error: `Cannot modify immutable file: ${path.basename(resolved)}` };
            }
            try {
                fs.appendFileSync(resolved, content);
                return { ok: true, path: resolved };
            }
            catch (err) {
                return { error: `Append failed: ${err.message}` };
            }
        },
        listFiles(relativePath) {
            const base = relativePath
                ? resolveSafe(agentDir, relativePath)
                : agentDir;
            if (!base || !fs.existsSync(base)) {
                return { error: `Directory not found: ${relativePath || agentDir}` };
            }
            try {
                const files = fs.readdirSync(base);
                return { files };
            }
            catch (err) {
                return { error: `List failed: ${err.message}` };
            }
        },
        verifyMemory() {
            const memoryFile = path.join(agentDir, 'memory.md');
            const backupFile = memoryFile + '.bak';
            if (!fs.existsSync(memoryFile)) {
                return { tampered: false, restored: false, detail: 'No memory file' };
            }
            const content = fs.readFileSync(memoryFile, 'utf-8');
            const match = content.match(MEMORY_HASH_PATTERN);
            if (!match) {
                return {
                    tampered: false,
                    restored: false,
                    detail: 'No integrity hash found (legacy file)',
                };
            }
            const storedHash = match[1];
            const contentWithoutHash = content.replace(/\n<!-- HASH:sha256:[a-f0-9]{64} -->$/, '');
            const actualHash = computeContentHash(contentWithoutHash);
            if (storedHash === actualHash) {
                return { tampered: false, restored: false, detail: 'Integrity verified' };
            }
            // Tampered -- try to restore from backup
            if (fs.existsSync(backupFile)) {
                fs.copyFileSync(backupFile, memoryFile);
                return {
                    tampered: true,
                    restored: true,
                    detail: `Memory tampered (expected ${storedHash.slice(0, 16)}..., got ${actualHash.slice(0, 16)}...). Restored from backup.`,
                };
            }
            return {
                tampered: true,
                restored: false,
                detail: `Memory tampered (expected ${storedHash.slice(0, 16)}..., got ${actualHash.slice(0, 16)}...). No backup available.`,
            };
        },
        runCommand(command, args = [], opts = {}) {
            if (!bashAllowlist.includes(command)) {
                return Promise.resolve({
                    error: `Command '${command}' not in allowlist: [${bashAllowlist.join(', ')}]`,
                });
            }
            const timeout = opts.timeout || commandTimeoutMs;
            const cwd = opts.cwd || agentDir;
            return new Promise((resolve) => {
                execFile(command, args, { timeout, cwd, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
                    if (err) {
                        resolve({ error: err.message, stderr });
                    }
                    else {
                        resolve({ stdout, stderr });
                    }
                });
            });
        },
        handoff(toAgent, task, context = '', priority = 'normal') {
            const handoffFile = path.join(sharedDir, 'handoff.md');
            const timestamp = new Date().toISOString();
            // Create HMAC signature
            const soulFile = path.join(agentDir, 'SOUL.md');
            let signature = 'unsigned';
            if (fs.existsSync(soulFile)) {
                const soulHash = computeContentHash(fs.readFileSync(soulFile, 'utf-8'));
                const payload = `${agentName}|${toAgent}|${task}|${timestamp}`;
                signature = crypto
                    .createHmac('sha256', soulHash)
                    .update(payload)
                    .digest('hex')
                    .slice(0, 16);
            }
            const entry = `\n## Handoff: ${agentName} -> ${toAgent}\n` +
                `- **Task**: ${task}\n` +
                `- **Priority**: ${priority}\n` +
                `- **Context**: ${context}\n` +
                `- **Time**: ${timestamp}\n` +
                `- **Signature**: ${signature}\n`;
            try {
                fs.appendFileSync(handoffFile, entry);
                return { ok: true };
            }
            catch (err) {
                return { error: `Handoff failed: ${err.message}` };
            }
        },
    };
    return Object.freeze(bundle);
}
//# sourceMappingURL=capability-bundle.js.map