/**
 * Capability gate (AUTO.3) — POSIX-style declared-capability check.
 * Spec: `docs/tasks/automation.md` AUTO.3.
 *
 * Precedence: builtin-deny → pack-deny → pack-allow → prompt → deny+audit.
 * Engine-vocabulary: gate speaks in `(pack, capability, target)` only.
 *
 * Risk postures (locked at AUTO.3):
 *   - Shell metacharacters (`; && || $() backticks | > >> <(`) blanket-
 *     reject unless the exact raw command is in the allowlist; SEC.4 ships
 *     full `shell-quote` argv parser.
 *   - HTTP allowlist match is on `new URL(target).hostname` (never glob on
 *     raw URL — foot-gun: `api.github.com.attacker.com`).
 *   - Parse failures + prompt-throw + audit-throw all DENY (constraint C10).
 *
 * Audit: every verdict flows through `auditLog`. CLI.5 lands the libsql
 * `audit_log` table; AUTO.3 ships the interface (no-op default).
 */
import { homedir } from 'node:os';
import { resolve } from 'node:path';
import { minimatch } from 'minimatch';
import { BUILTIN_BINARY_DENY, BUILTIN_CHANNEL_DENY, BUILTIN_PATH_DENY, BUILTIN_SHELL_DENY, BUILTIN_SUBAGENT_DENY, SHELL_METACHARACTERS, trustBuiltinDeny, } from './builtin_denylist.js';
function noopAudit() {
    /* default audit sink (named to satisfy eslint no-empty-function) */
}
function safeParseUrl(target) {
    try {
        return new URL(target);
    }
    catch {
        return null;
    }
}
// Hostname allowlist: exact OR `*.<suffix>` subdomain. The foot-gun
// `api.github.com.attacker.com` is rejected (its hostname is .attacker.com).
function matchHostname(hostname, pattern) {
    if (pattern === hostname)
        return true;
    if (pattern.startsWith('*.')) {
        const suffix = pattern.slice(2);
        return hostname.endsWith(`.${suffix}`);
    }
    return false;
}
function expandHome(path, home) {
    if (path === '~')
        return home;
    if (path.startsWith('~/'))
        return resolve(home, path.slice(2));
    return path;
}
// Matchers are pure; `check()` is async only for the prompt callback.
export class CapabilityGate {
    packs;
    prompt;
    auditLogFn;
    trustBuiltinDeny;
    homeDir;
    overrides;
    constructor(opts) {
        this.packs = opts.packs;
        this.prompt = opts.prompt;
        this.auditLogFn = opts.auditLog ?? noopAudit;
        this.trustBuiltinDeny = opts.trustBuiltinDeny ?? trustBuiltinDeny();
        this.homeDir = opts.homeDir ?? homedir();
        this.overrides = opts.overrides ?? [];
    }
    async check(req) {
        const block = this.packs.get(req.pack)?.permissions?.[req.capability];
        if (this.trustBuiltinDeny) {
            const builtinDeny = this.matchBuiltinDeny(req);
            if (builtinDeny) {
                return this.audit({ allowed: false, source: 'denylist', message: builtinDeny }, req);
            }
        }
        // Pack-local deny wins over user overrides (a pack author explicitly
        // forbidding a pattern shouldn't be silently undermined by an old
        // override file). User override wins over pack allowlist gaps.
        if (block) {
            const packDeny = this.matchPackDeny(req, block);
            if (packDeny) {
                return this.audit({ allowed: false, source: 'denylist', message: packDeny }, req);
            }
        }
        const override = this.matchOverride(req);
        if (override) {
            return this.audit({ allowed: true, source: 'user_override', message: override }, req);
        }
        if (!block)
            return this.handleUndeclared(req);
        const declared = this.matchAllowlist(req, block);
        if (declared)
            return this.audit({ allowed: true, source: 'declared', message: declared }, req);
        return this.handleUndeclared(req);
    }
    matchOverride(req) {
        for (const o of this.overrides) {
            if (o.pack !== req.pack)
                continue;
            if (o.capability !== req.capability)
                continue;
            // http_request override: hostname-exact OR `*.<suffix>` (parity with
            // pack allowlist; never raw-URL glob — foot-gun guard).
            if (req.capability === 'http_request') {
                const url = safeParseUrl(req.target);
                if (url === null)
                    continue;
                if (matchHostname(url.hostname, o.target)) {
                    return `user override: ${url.hostname} matches "${o.target}"`;
                }
                continue;
            }
            // file_write override: ~-expand + minimatch with dot:true (parity
            // with declared allowlist + pack-local deny matchPathList).
            if (req.capability === 'file_write') {
                const expanded = resolve(expandHome(req.target, this.homeDir));
                const expandedPattern = resolve(expandHome(o.target, this.homeDir));
                if (minimatch(expanded, expandedPattern, { dot: true })) {
                    return `user override: matches "${o.target}"`;
                }
                continue;
            }
            // shell_exec override: exact OR glob, but the SHELL_METACHARACTERS
            // gate still applies (an override cannot un-block metachars unless
            // it's an exact-match like the declared allowlist semantic).
            if (req.capability === 'shell_exec') {
                if (o.target === req.target)
                    return `user override exact: "${o.target}"`;
                let hasMeta = false;
                for (const meta of SHELL_METACHARACTERS) {
                    if (req.target.includes(meta)) {
                        hasMeta = true;
                        break;
                    }
                }
                if (hasMeta)
                    continue;
                if (minimatch(req.target, o.target))
                    return `user override glob: "${o.target}"`;
                continue;
            }
            // send_message / subprocess_call / subagent_call → minimatch glob
            // on the raw target (parity with `matchGlobList`).
            if (minimatch(req.target, o.target))
                return `user override: matches "${o.target}"`;
        }
        return null;
    }
    matchBuiltinDeny(req) {
        switch (req.capability) {
            case 'shell_exec':
                for (const re of BUILTIN_SHELL_DENY) {
                    if (re.test(req.target))
                        return `built-in shell deny: matches ${re.source}`;
                }
                return null;
            case 'file_write':
                return this.matchPathList(req.target, BUILTIN_PATH_DENY, 'built-in path deny');
            case 'send_message':
                return matchGlobList(req.target, BUILTIN_CHANNEL_DENY, 'built-in channel deny');
            case 'subprocess_call':
                return matchGlobList(req.target, BUILTIN_BINARY_DENY, 'built-in binary deny');
            case 'subagent_call':
                return matchGlobList(req.target, BUILTIN_SUBAGENT_DENY, 'built-in subagent deny');
            case 'http_request':
                // Parse-failure → deny (C10: no silent fail-open).
                if (safeParseUrl(req.target) === null)
                    return `invalid URL "${req.target}"`;
                return null;
            default: {
                const _exhaustive = req.capability;
                return `unknown capability ${String(_exhaustive)}`;
            }
        }
    }
    matchPackDeny(req, block) {
        const denies = block.deny ?? [];
        if (denies.length === 0)
            return null;
        if (req.capability === 'shell_exec') {
            for (const pattern of denies) {
                if (minimatch(req.target, pattern))
                    return `pack-local shell deny: matches "${pattern}"`;
            }
            return null;
        }
        if (req.capability === 'file_write') {
            return this.matchPathList(req.target, denies, 'pack-local path deny');
        }
        return matchGlobList(req.target, denies, 'pack-local deny');
    }
    // Shared file-path matcher: expand `~`, resolve, minimatch both sides.
    matchPathList(target, patterns, label) {
        const expanded = resolve(expandHome(target, this.homeDir));
        for (const pattern of patterns) {
            const expandedPattern = resolve(expandHome(pattern, this.homeDir));
            if (minimatch(expanded, expandedPattern, { dot: true })) {
                return `${label}: matches "${pattern}"`;
            }
        }
        return null;
    }
    matchAllowlist(req, block) {
        if (req.capability === 'shell_exec') {
            const cmds = block.commands ?? [];
            const exact = cmds.find((c) => c === req.target);
            if (exact !== undefined)
                return `shell allowlist exact: "${exact}"`;
            // Metachar gate (replaced by shell-quote argv parser in SEC.4).
            for (const meta of SHELL_METACHARACTERS) {
                if (req.target.includes(meta))
                    return null;
            }
            for (const pattern of cmds) {
                if (minimatch(req.target, pattern))
                    return `shell allowlist glob: "${pattern}"`;
            }
            return null;
        }
        if (req.capability === 'http_request') {
            const perm = block;
            const url = safeParseUrl(req.target);
            if (url === null)
                return null;
            const method = (req.method ?? 'GET').toUpperCase();
            const methods = perm.methods ?? ['GET'];
            if (!methods.includes(method))
                return null;
            for (const pattern of perm.domains ?? []) {
                if (matchHostname(url.hostname, pattern)) {
                    return `http allowlist: ${url.hostname} matches "${pattern}" (${method})`;
                }
            }
            return null;
        }
        if (req.capability === 'file_write') {
            const paths = block.paths ?? [];
            return this.matchPathList(req.target, paths, 'file allowlist');
        }
        if (req.capability === 'send_message') {
            const channels = block.channels ?? [];
            return matchGlobList(req.target, channels, 'channel allowlist');
        }
        if (req.capability === 'subprocess_call') {
            const bins = block.binaries ?? [];
            return matchGlobList(req.target, bins, 'binary allowlist');
        }
        if (req.capability === 'subagent_call') {
            const targets = block.targets ?? [];
            return matchGlobList(req.target, targets, 'subagent allowlist');
        }
        const _exhaustive = req.capability;
        return `unknown capability ${String(_exhaustive)}`;
    }
    // Undeclared: prompt callback OR deny+audit. C10: prompt-throw +
    // audit-throw both DENY (never fail-open).
    async handleUndeclared(req) {
        if (!this.prompt) {
            return this.audit(denied(`undeclared "${req.capability}" for pack "${req.pack}" (non-interactive)`), req);
        }
        try {
            const approved = await this.prompt(req);
            return this.audit(approved
                ? {
                    allowed: true,
                    source: 'user_approved',
                    message: `user approved "${req.capability}" → "${req.target}"`,
                }
                : denied(`user denied "${req.capability}" → "${req.target}"`), req);
        }
        catch (e) {
            return this.audit(denied(`prompt threw — deny (C10): ${String(e)}`), req);
        }
    }
    audit(verdict, req) {
        try {
            this.auditLogFn(verdict, req);
        }
        catch {
            // Audit-sink errors NEVER influence the verdict (constraint C10).
        }
        return verdict;
    }
}
function matchGlobList(target, patterns, label) {
    for (const pattern of patterns) {
        if (minimatch(target, pattern))
            return `${label}: matches "${pattern}"`;
    }
    return null;
}
function denied(message) {
    return { allowed: false, source: 'denied', message };
}
//# sourceMappingURL=capability_gate.js.map