import { realpathSync } from "node:fs";
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
import { renderShellArgvForHistory } from "./capability-history.js";
import { classifySensitivePath } from "./sensitive-resource-policy.js";
export const DEFAULT_ALLOWED_TOOLS = [
    "echo",
    "ask-user",
    "delegate_tasks",
    "workspace-search",
    "workspace-files",
    "content-search",
    "read-file",
    "inspect-env",
    "read-scratch",
    "shell",
    "fetch-url",
    "web-search",
    "write-scratch",
    "append-scratch",
    "write-workspace",
    "edit",
    "apply_patch",
    "verify-swift-package",
    "attach-text",
    "write-artifact",
];
export class RuntimePolicy {
    #allowedShellPrefixes;
    #allowedNetworkHosts;
    #allowedTools;
    #allowAllShell;
    #comparisonReadableRoots;
    #comparisonWritableRoots;
    #readableRoots;
    #writableRoots;
    constructor(options) {
        this.#allowedShellPrefixes = options.allowedShellPrefixes ?? [];
        this.#allowedNetworkHosts = new Set(options.allowedNetworkHosts ?? []);
        this.#allowedTools = new Set(options.allowedTools ?? []);
        this.#allowAllShell = options.allowAllShell ?? false;
        this.#comparisonReadableRoots = normalizeComparisonRoots(options.readableRoots ?? []);
        this.#comparisonWritableRoots = normalizeComparisonRoots(options.writableRoots ?? []);
        this.#readableRoots = normalizeRoots(options.readableRoots ?? []);
        this.#writableRoots = normalizeRoots(options.writableRoots ?? []);
    }
    evaluate(request) {
        switch (request.group) {
            case "tool":
                return this.#evaluateTool(request.toolName);
            case "filesystem":
                return this.#evaluateFilesystem(request);
            case "shell":
                return this.#evaluateShell(request.argv);
            case "network":
                return this.#evaluateNetwork(request.host);
        }
    }
    allowTools(toolNames) {
        for (const toolName of toolNames) {
            this.#allowedTools.add(toolName);
        }
    }
    snapshot() {
        return {
            allowedShellPrefixes: this.#allowedShellPrefixes.map((prefix) => [...prefix]),
            allowedNetworkHosts: Array.from(this.#allowedNetworkHosts),
            allowedTools: Array.from(this.#allowedTools),
            allowAllShell: this.#allowAllShell,
            readableRoots: [...this.#readableRoots],
            writableRoots: [...this.#writableRoots],
        };
    }
    overlay(options = {}) {
        const snapshot = this.snapshot();
        const deniedTools = new Set(options.deniedTools ?? []);
        return new RuntimePolicy({
            allowedShellPrefixes: [
                ...snapshot.allowedShellPrefixes,
                ...(options.allowedShellPrefixes ?? []),
            ],
            allowedNetworkHosts: [
                ...snapshot.allowedNetworkHosts,
                ...(options.allowedNetworkHosts ?? []),
            ],
            allowedTools: [
                ...snapshot.allowedTools.filter((toolName) => !deniedTools.has(toolName)),
                ...(options.allowedTools ?? []).filter((toolName) => !deniedTools.has(toolName)),
            ],
            allowAllShell: options.allowAllShell ?? snapshot.allowAllShell,
            readableRoots: [...snapshot.readableRoots, ...(options.readableRoots ?? [])],
            writableRoots: [...snapshot.writableRoots, ...(options.writableRoots ?? [])],
        });
    }
    #evaluateTool(toolName) {
        if (this.#allowedTools.has(toolName)) {
            return {
                verdict: "allow",
                group: "tool",
                riskClass: "low",
                reviewMode: "direct",
                sandboxProfile: "none",
            };
        }
        return {
            verdict: "deny",
            group: "tool",
            riskClass: "medium",
            reviewMode: "direct",
            sandboxProfile: "none",
            reason: `Tool ${toolName} is not allowed by the active runtime policy.`,
        };
    }
    #evaluateFilesystem(request) {
        const requestedPath = normalizeComparisonPath(resolve(request.path));
        const path = canonicalizePath(request.path);
        if (request.action === "read") {
            const sensitiveKind = classifySensitivePath(path);
            if (sensitiveKind) {
                return sensitiveReadDecision("filesystem", `Reading ${basename(path)} may expose ${sensitiveKind === "env" ? "dotenv secrets" : "credentials"}.`);
            }
        }
        const configuredRoots = request.action === "read"
            ? [...this.#readableRoots, ...(request.roots ?? [])]
            : [...this.#writableRoots, ...(request.roots ?? [])];
        const roots = normalizeRoots(configuredRoots);
        const comparisonRoots = request.action === "read"
            ? [...this.#comparisonReadableRoots, ...normalizeComparisonRoots(request.roots ?? [])]
            : [...this.#comparisonWritableRoots, ...normalizeComparisonRoots(request.roots ?? [])];
        if (roots.some((root) => isWithinRoot(path, root))) {
            return {
                verdict: "allow",
                group: "filesystem",
                riskClass: "low",
                reviewMode: "direct",
                sandboxProfile: "no-network-no-write",
            };
        }
        const escapesAllowedRoot = requestedPath !== path && comparisonRoots.some((root) => isWithinRoot(requestedPath, root));
        if (escapesAllowedRoot) {
            return {
                verdict: "deny",
                group: "filesystem",
                riskClass: request.action === "read" ? "medium" : "high",
                reviewMode: request.action === "read" ? "direct" : "guardian",
                sandboxProfile: request.action === "read" ? "no-network-no-write" : "no-network",
                reason: request.action === "read"
                    ? `Read access to ${path} is not allowed by the active runtime policy.`
                    : `Write access to ${path} is not allowed by the active runtime policy.`,
            };
        }
        return {
            verdict: "ask",
            group: "filesystem",
            riskClass: request.action === "read" ? "medium" : "high",
            reviewMode: request.action === "read" ? "direct" : "guardian",
            sandboxProfile: request.action === "read" ? "no-network-no-write" : "no-network",
            reason: request.action === "read"
                ? `Read access to ${path} requires approval under the active runtime policy.`
                : `Write access to ${path} requires approval under the active runtime policy.`,
        };
    }
    #evaluateShell(argv) {
        if (argv.length === 0) {
            return {
                verdict: "deny",
                group: "shell",
                riskClass: "medium",
                reviewMode: "direct",
                sandboxProfile: "no-network-no-write",
                reason: "Command execution requires at least one argv entry.",
            };
        }
        if (readsSensitiveShellArgv(argv)) {
            return sensitiveReadDecision("shell", "Command may expose secrets from environment or credential files.");
        }
        const highRiskShellReason = classifyHighRiskShellArgv(argv);
        if (highRiskShellReason) {
            return {
                verdict: "ask",
                group: "shell",
                riskClass: "high",
                reviewMode: "guardian",
                sandboxProfile: "no-network",
                reason: highRiskShellReason,
            };
        }
        if (this.#allowAllShell) {
            return {
                verdict: "allow",
                group: "shell",
                riskClass: "low",
                reviewMode: "direct",
                sandboxProfile: "none",
            };
        }
        if (this.#allowedShellPrefixes.some((prefix) => matchesPrefix(argv, prefix))) {
            return {
                verdict: "allow",
                group: "shell",
                riskClass: "low",
                reviewMode: "direct",
                sandboxProfile: "no-network-no-write",
            };
        }
        if (isReadOnlyShellEval(argv)) {
            return {
                verdict: "allow",
                group: "shell",
                riskClass: "low",
                reviewMode: "direct",
                sandboxProfile: "no-network-no-write",
            };
        }
        return {
            verdict: "ask",
            group: "shell",
            riskClass: "high",
            reviewMode: "guardian",
            sandboxProfile: "none",
            reason: `Command ${renderShellArgvForHistory(argv)} requires approval under the active runtime policy.`,
        };
    }
    #evaluateNetwork(host) {
        if (this.#allowedNetworkHosts.has(host)) {
            return {
                verdict: "allow",
                group: "network",
                riskClass: "low",
                reviewMode: "direct",
                sandboxProfile: "none",
            };
        }
        return {
            verdict: "ask",
            group: "network",
            riskClass: "high",
            reviewMode: "guardian",
            sandboxProfile: "none",
            reason: `Network access to ${host} requires approval under the active runtime policy.`,
        };
    }
}
export function createDefaultRuntimePolicy(options = {}) {
    const cwd = resolve(options.cwd ?? process.cwd());
    const readableRoots = options.readableRoots ?? [cwd];
    const writableRoots = options.writableRoots ?? [cwd];
    return new RuntimePolicy({
        ...options,
        allowedShellPrefixes: options.allowedShellPrefixes ?? [
            ["pwd"],
            ["date"],
            ["ls"],
            ["cat"],
            ["echo"],
            ["uname"],
            ["head"],
            ["tail"],
            ["wc"],
            ["find"],
            ["file"],
            ["which"],
            ["command", "-v"],
            ["type"],
            ["git", "status"],
            ["git", "log"],
            ["git", "diff"],
            ["git", "show"],
            ["git", "branch"],
            ["swift", "build"],
            ["xcrun", "swift", "build"],
        ],
        allowedNetworkHosts: options.allowedNetworkHosts ?? ["127.0.0.1", "localhost"],
        allowedTools: options.allowedTools ?? [...DEFAULT_ALLOWED_TOOLS],
        readableRoots,
        writableRoots,
    });
}
function normalizeRoots(roots) {
    return Array.from(new Set(roots.map((root) => canonicalizePath(root))));
}
function normalizeComparisonRoots(roots) {
    return Array.from(new Set(roots.map((root) => normalizeComparisonPath(resolve(root)))));
}
function isWithinRoot(path, root) {
    const candidate = normalizeComparisonPath(path);
    const normalizedRoot = normalizeComparisonPath(root);
    const relativePath = relative(normalizedRoot, candidate);
    return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
}
function canonicalizePath(path) {
    const resolvedPath = normalizeComparisonPath(resolve(path));
    const existingAncestor = findExistingAncestor(resolvedPath);
    if (!existingAncestor) {
        return resolvedPath;
    }
    const canonicalAncestor = normalizeComparisonPath(realpathSync.native(existingAncestor));
    const suffix = relative(existingAncestor, resolvedPath);
    return suffix ? normalizeComparisonPath(resolve(canonicalAncestor, suffix)) : canonicalAncestor;
}
function findExistingAncestor(path) {
    let current = path;
    for (;;) {
        try {
            realpathSync.native(current);
            return current;
        }
        catch {
            const parent = dirname(current);
            if (parent === current) {
                return null;
            }
            current = parent;
        }
    }
}
function normalizeComparisonPath(path) {
    const normalized = resolve(path);
    return process.platform === "win32" ? normalized.toLowerCase() : normalized;
}
function matchesPrefix(argv, prefix) {
    if (argv.length < prefix.length) {
        return false;
    }
    return prefix.every((value, index) => argv[index] === value);
}
const READ_ONLY_SHELL_PREFIXES = [
    "pwd",
    "date",
    "ls",
    "ls ",
    "cat ",
    "head ",
    "tail ",
    "wc ",
    "find ",
    "file ",
    "which ",
    "command -v ",
    "type ",
    "rg ",
    "grep ",
    "sed -n ",
    "awk ",
    "sort",
    "sort ",
    "git status",
    "git log",
    "git log ",
    "git diff",
    "git diff ",
    "git show",
    "git show ",
    "git branch",
    "git branch ",
    "git rev-parse",
    "git rev-parse ",
    "git ls-files",
    "git ls-files ",
    "git diff-tree ",
    "git config --get",
    "git config --get ",
    "git config --list",
];
const SHELL_WRITE_INDICATORS = [
    />\s*[^|&]/,
    />>/,
    /\btee\b/,
    /\brm\b/,
    /\bmkdir\b/,
    /\btouch\b/,
    /\bmv\b/,
    /\bcp\b/,
    /\bsudo\b/,
    /\bchmod\b/,
    /\bchown\b/,
    /`[^`]*`/,
    /\$\(/,
];
function isReadOnlyShellEval(argv) {
    const command = extractShellEvalCommand(argv);
    return command ? isReadOnlyShellCommand(command) : false;
}
function extractShellEvalCommand(argv) {
    const executable = basename(argv[0] ?? "");
    if (executable !== "sh" && executable !== "bash" && executable !== "zsh")
        return null;
    const evalArgIndex = argv.findIndex((arg) => arg === "-c" || arg === "-lc");
    const command = evalArgIndex >= 0 ? argv[evalArgIndex + 1] : undefined;
    return typeof command === "string" && command.trim() ? command : null;
}
function isReadOnlyShellCommand(command) {
    const trimmed = stripLeadingPipefail(command.trim());
    if (!trimmed || trimmed.includes("||") || trimmed.includes(";"))
        return false;
    if (SHELL_WRITE_INDICATORS.some((pattern) => pattern.test(stripSafeShellRedirections(trimmed))))
        return false;
    const cdAndCommand = trimmed.match(/^cd\s+("[^"]+"|'[^']+'|[^;&|`$]+)\s+&&\s+(.+)$/);
    if (cdAndCommand)
        return isReadOnlyShellCommand(cdAndCommand[2] ?? "");
    if (trimmed.includes("&&")) {
        const segments = trimmed.split(/\s*&&\s*/);
        return segments.length > 1 && segments.every((segment) => isReadOnlyShellCommand(segment));
    }
    if (trimmed.includes("|")) {
        const segments = trimmed.split(/\s*\|\s*/);
        return segments.length > 1 && segments.every((segment) => isReadOnlyShellCommand(segment));
    }
    const normalized = trimmed.toLowerCase();
    return READ_ONLY_SHELL_PREFIXES.some((prefix) => normalized === prefix.trimEnd() || normalized.startsWith(prefix));
}
function stripSafeShellRedirections(command) {
    return command.replace(/(^|\s)(?:[012])?>\s*\/dev\/null(?=\s|$)/g, "$1");
}
function stripLeadingPipefail(command) {
    return command.replace(/^set\s+-o\s+pipefail\s*;\s*/u, "");
}
function sensitiveReadDecision(group, reason) {
    return {
        verdict: "deny",
        group,
        riskClass: "high",
        reviewMode: "direct",
        sandboxProfile: "no-network-no-write",
        reason,
    };
}
function readsSensitiveShellArgv(argv) {
    const shellCommand = extractShellEvalCommand(argv);
    if (shellCommand) {
        return readsSensitiveFileViaShell(shellCommand);
    }
    const command = basename(argv[0] ?? "");
    if (command === "env" || command === "printenv" || command === "set") {
        return true;
    }
    if (command === "find") {
        return findNamesSensitivePath(argv.slice(1));
    }
    if (INLINE_SCRIPT_COMMANDS.has(command)) {
        return argv
            .slice(1)
            .some((arg) => tokenMentionsSensitivePath(arg) || tokenMentionsSensitiveEnvAccess(arg));
    }
    if (!SENSITIVE_READ_COMMANDS.has(command)) {
        return false;
    }
    return argv.slice(1).some((arg) => tokenMentionsSensitivePath(arg));
}
function classifyHighRiskShellArgv(argv) {
    const shellCommand = extractShellEvalCommand(argv);
    const commandText = shellCommand ?? argv.join(" ");
    if (/\bkillall\s+Docker\b|\bosascript\b.*tell application ['"]Docker['"].*quit/iu.test(commandText)) {
        return "This command restarts Docker Desktop and may interrupt running containers.";
    }
    return undefined;
}
const INLINE_SCRIPT_COMMANDS = new Set(["node", "perl", "python", "python3", "ruby"]);
const SENSITIVE_READ_COMMANDS = new Set([
    "awk",
    "cat",
    "file",
    "grep",
    "head",
    "jq",
    "rg",
    "sed",
    "tail",
    "wc",
]);
export function readsSensitiveShellCommand(command) {
    return readsSensitiveFileViaShell(command);
}
function readsSensitiveFileViaShell(command) {
    const segments = stripLeadingPipefail(command.trim())
        .split(/\s*(?:&&|\|\||\||;)\s*/u)
        .map((segment) => segment.trim())
        .filter(Boolean);
    return segments.some((segment) => readsSensitiveFileViaShellSegment(segment));
}
function readsSensitiveFileViaShellSegment(segment) {
    const withoutCd = segment.replace(/^cd\s+(?:"[^"]+"|'[^']+'|[^;&|`$]+)\s+&&\s+/u, "");
    const tokens = splitShellTokens(withoutCd);
    if (tokens.length === 0) {
        return false;
    }
    let index = 0;
    if (tokens[index] === "sudo") {
        index += 1;
    }
    while (tokens[index] && /^[A-Za-z_][A-Za-z0-9_]*=/u.test(tokens[index] ?? "")) {
        index += 1;
    }
    const command = basename(tokens[index] ?? "");
    if (command === "env" || command === "printenv" || command === "set") {
        return true;
    }
    if (command === "find") {
        return findNamesSensitivePath(tokens.slice(index + 1));
    }
    if (INLINE_SCRIPT_COMMANDS.has(command)) {
        return tokens
            .slice(index + 1)
            .some((token) => tokenMentionsSensitivePath(token) || tokenMentionsSensitiveEnvAccess(token));
    }
    if (!SENSITIVE_READ_COMMANDS.has(command)) {
        return false;
    }
    return tokens.slice(index + 1).some((token) => tokenMentionsSensitivePath(token));
}
function findNamesSensitivePath(args) {
    return args.some((arg, index) => {
        const token = stripShellToken(arg);
        if (tokenMentionsSensitivePath(token)) {
            return true;
        }
        if ((token === "-name" || token === "-iname" || token === "-path" || token === "-ipath") &&
            args[index + 1]) {
            return tokenMentionsSensitivePath(args[index + 1] ?? "");
        }
        return false;
    });
}
function tokenMentionsSensitivePath(token) {
    const candidates = stripShellToken(token)
        .split(/[^A-Za-z0-9_.@%+=:/\\-]+/u)
        .filter(Boolean);
    return candidates.some((candidate) => classifySensitivePath(candidate));
}
const SENSITIVE_ENV_NAME_PATTERN = /\b[A-Za-z_][A-Za-z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|AUTH|CREDENTIAL|PRIVATE[_-]?KEY|PASSWORD|PASS)[A-Za-z0-9_]*\b/iu;
function tokenMentionsSensitiveEnvAccess(token) {
    return (/\b(?:process\.env|os\.environ|ENV)\b/u.test(token) && SENSITIVE_ENV_NAME_PATTERN.test(token));
}
export function isSensitiveCapabilityRequest(request) {
    if (request.group === "filesystem") {
        return (request.action === "read" &&
            classifySensitivePath(canonicalizePath(request.path)) !== undefined);
    }
    return request.group === "shell" && readsSensitiveShellArgv(request.argv);
}
function splitShellTokens(command) {
    const tokens = [];
    let current = "";
    let quote;
    for (let index = 0; index < command.length; index += 1) {
        const char = command[index] ?? "";
        if (quote) {
            if (char === quote) {
                quote = undefined;
            }
            else {
                current += char;
            }
            continue;
        }
        if (char === "'" || char === '"') {
            quote = char;
            continue;
        }
        if (/\s/u.test(char)) {
            if (current) {
                tokens.push(current);
                current = "";
            }
            continue;
        }
        current += char;
    }
    if (current) {
        tokens.push(current);
    }
    return tokens;
}
function stripShellToken(token) {
    return token.replace(/^['"]|['"]$/gu, "");
}
//# sourceMappingURL=runtime-policy.js.map