import crypto from "node:crypto";
import { spawn } from "node:child_process";
import fs from "node:fs";
import fsPromises from "node:fs/promises";
import path from "node:path";
import { addAllowlistEntry, analyzeArgvCommand, evaluateExecAllowlist, evaluateShellAllowlist, requiresExecApproval, normalizeExecApprovals, recordAllowlistUse, resolveAllowAlwaysPatterns, resolveExecApprovals, resolveSafeBins, ensureExecApprovals, readExecApprovalsSnapshot, resolveExecApprovalsSocketPath, saveExecApprovals, } from "../infra/exec-approvals.js";
import { requestExecHostViaSocket, } from "../infra/exec-host.js";
import { getMachineDisplayName } from "../infra/machine-name.js";
import { loadOrCreateDeviceIdentity } from "../infra/device-identity.js";
import { loadConfig } from "../config/config.js";
import { resolveBrowserConfig, shouldStartLocalBrowserServer } from "../browser/config.js";
import { detectMime } from "../media/mime.js";
import { resolveAgentConfig } from "../agents/agent-scope.js";
import { ensureClawdbotCliOnPath } from "../infra/path-env.js";
import { VERSION } from "../version.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
import { ensureNodeHostConfig, saveNodeHostConfig } from "./config.js";
import { GatewayClient } from "../gateway/client.js";
function resolveExecSecurity(value) {
    return value === "deny" || value === "allowlist" || value === "full" ? value : "allowlist";
}
function isCmdExeInvocation(argv) {
    const token = argv[0]?.trim();
    if (!token) {
        return false;
    }
    const base = path.win32.basename(token).toLowerCase();
    return base === "cmd.exe" || base === "cmd";
}
function resolveExecAsk(value) {
    return value === "off" || value === "on-miss" || value === "always" ? value : "on-miss";
}
const OUTPUT_CAP = 200_000;
const OUTPUT_EVENT_TAIL = 20_000;
const DEFAULT_NODE_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
const BROWSER_PROXY_MAX_FILE_BYTES = 10 * 1024 * 1024;
const execHostEnforced = process.env.OPENCLAW_NODE_EXEC_HOST?.trim().toLowerCase() === "app";
const execHostFallbackAllowed = process.env.OPENCLAW_NODE_EXEC_FALLBACK?.trim().toLowerCase() !== "0";
const blockedEnvKeys = new Set([
    "NODE_OPTIONS",
    "NODE_PATH",
    "PYTHONHOME",
    "PYTHONPATH",
    "PERL5LIB",
    "PERL5OPT",
    "RUBYLIB",
    "RUBYOPT",
    "BASH_ENV",
    "ENV",
    "GCONV_PATH",
    "IFS",
    "SSLKEYLOGFILE",
]);
const blockedEnvPrefixes = ["DYLD_", "LD_", "BASH_FUNC_"];
class SkillBinsCache {
    bins = new Set();
    lastRefresh = 0;
    ttlMs = 90_000;
    fetch;
    constructor(fetch) {
        this.fetch = fetch;
    }
    async current(force = false) {
        if (force || Date.now() - this.lastRefresh > this.ttlMs) {
            await this.refresh();
        }
        return this.bins;
    }
    async refresh() {
        try {
            const bins = await this.fetch();
            this.bins = new Set(bins);
            this.lastRefresh = Date.now();
        }
        catch {
            if (!this.lastRefresh) {
                this.bins = new Set();
            }
        }
    }
}
export function sanitizeEnv(overrides) {
    // Sanitize the base environment to strip dangerous vars that could be inherited.
    const merged = {};
    for (const [rawKey, value] of Object.entries(process.env)) {
        if (value === undefined)
            continue;
        const key = rawKey.trim();
        if (!key)
            continue;
        const upper = key.toUpperCase();
        if (blockedEnvKeys.has(upper))
            continue;
        if (blockedEnvPrefixes.some((prefix) => upper.startsWith(prefix)))
            continue;
        merged[key] = value;
    }
    if (!overrides)
        return merged;
    const basePath = process.env.PATH ?? DEFAULT_NODE_PATH;
    for (const [rawKey, value] of Object.entries(overrides)) {
        const key = rawKey.trim();
        if (!key)
            continue;
        const upper = key.toUpperCase();
        if (upper === "PATH") {
            const trimmed = value.trim();
            if (!trimmed)
                continue;
            if (!basePath || trimmed === basePath) {
                merged[key] = trimmed;
                continue;
            }
            const suffix = `${path.delimiter}${basePath}`;
            if (trimmed.endsWith(suffix)) {
                merged[key] = trimmed;
            }
            continue;
        }
        if (blockedEnvKeys.has(upper))
            continue;
        if (blockedEnvPrefixes.some((prefix) => upper.startsWith(prefix)))
            continue;
        merged[key] = value;
    }
    return merged;
}
function normalizeProfileAllowlist(raw) {
    return Array.isArray(raw) ? raw.map((entry) => entry.trim()).filter(Boolean) : [];
}
function resolveBrowserProxyConfig() {
    const cfg = loadConfig();
    const proxy = cfg.nodeHost?.browserProxy;
    const allowProfiles = normalizeProfileAllowlist(proxy?.allowProfiles);
    const enabled = proxy?.enabled !== false;
    return { enabled, allowProfiles };
}
let browserControlReady = null;
async function ensureBrowserControlServer() {
    if (browserControlReady)
        return browserControlReady;
    browserControlReady = (async () => {
        const cfg = loadConfig();
        const resolved = resolveBrowserConfig(cfg.browser);
        if (!resolved.enabled) {
            throw new Error("browser control disabled");
        }
        if (!shouldStartLocalBrowserServer(resolved)) {
            throw new Error("browser control URL is non-loopback");
        }
        const mod = await import("../browser/server.js");
        await mod.startBrowserControlServerFromConfig();
    })();
    return browserControlReady;
}
function isProfileAllowed(params) {
    const { allowProfiles, profile } = params;
    if (!allowProfiles.length)
        return true;
    if (!profile)
        return false;
    return allowProfiles.includes(profile.trim());
}
function collectBrowserProxyPaths(payload) {
    const paths = new Set();
    const obj = typeof payload === "object" && payload !== null ? payload : null;
    if (!obj)
        return [];
    if (typeof obj.path === "string" && obj.path.trim())
        paths.add(obj.path.trim());
    if (typeof obj.imagePath === "string" && obj.imagePath.trim())
        paths.add(obj.imagePath.trim());
    const download = obj.download;
    if (download && typeof download === "object") {
        const dlPath = download.path;
        if (typeof dlPath === "string" && dlPath.trim())
            paths.add(dlPath.trim());
    }
    return [...paths];
}
async function readBrowserProxyFile(filePath) {
    const stat = await fsPromises.stat(filePath).catch(() => null);
    if (!stat || !stat.isFile())
        return null;
    if (stat.size > BROWSER_PROXY_MAX_FILE_BYTES) {
        throw new Error(`browser proxy file exceeds ${Math.round(BROWSER_PROXY_MAX_FILE_BYTES / (1024 * 1024))}MB`);
    }
    const buffer = await fsPromises.readFile(filePath);
    const mimeType = await detectMime({ buffer, filePath });
    return { path: filePath, base64: buffer.toString("base64"), mimeType };
}
function formatCommand(argv) {
    return argv
        .map((arg) => {
        const trimmed = arg.trim();
        if (!trimmed)
            return '""';
        const needsQuotes = /\s|"/.test(trimmed);
        if (!needsQuotes)
            return trimmed;
        return `"${trimmed.replace(/"/g, '\\"')}"`;
    })
        .join(" ");
}
function truncateOutput(raw, maxChars) {
    if (raw.length <= maxChars)
        return { text: raw, truncated: false };
    return { text: `... (truncated) ${raw.slice(raw.length - maxChars)}`, truncated: true };
}
function redactExecApprovals(file) {
    const socketPath = file.socket?.path?.trim();
    return {
        ...file,
        socket: socketPath ? { path: socketPath } : undefined,
    };
}
function requireExecApprovalsBaseHash(params, snapshot) {
    if (!snapshot.exists)
        return;
    if (!snapshot.hash) {
        throw new Error("INVALID_REQUEST: exec approvals base hash unavailable; reload and retry");
    }
    const baseHash = typeof params.baseHash === "string" ? params.baseHash.trim() : "";
    if (!baseHash) {
        throw new Error("INVALID_REQUEST: exec approvals base hash required; reload and retry");
    }
    if (baseHash !== snapshot.hash) {
        throw new Error("INVALID_REQUEST: exec approvals changed; reload and retry");
    }
}
async function runCommand(argv, cwd, env, timeoutMs) {
    return await new Promise((resolve) => {
        let stdout = "";
        let stderr = "";
        let outputLen = 0;
        let truncated = false;
        let timedOut = false;
        let settled = false;
        const child = spawn(argv[0], argv.slice(1), {
            cwd,
            env,
            stdio: ["ignore", "pipe", "pipe"],
            windowsHide: true,
        });
        const onChunk = (chunk, target) => {
            if (outputLen >= OUTPUT_CAP) {
                truncated = true;
                return;
            }
            const remaining = OUTPUT_CAP - outputLen;
            const slice = chunk.length > remaining ? chunk.subarray(0, remaining) : chunk;
            const str = slice.toString("utf8");
            outputLen += slice.length;
            if (target === "stdout")
                stdout += str;
            else
                stderr += str;
            if (chunk.length > remaining)
                truncated = true;
        };
        child.stdout?.on("data", (chunk) => onChunk(chunk, "stdout"));
        child.stderr?.on("data", (chunk) => onChunk(chunk, "stderr"));
        let timer;
        if (timeoutMs && timeoutMs > 0) {
            timer = setTimeout(() => {
                timedOut = true;
                try {
                    child.kill("SIGKILL");
                }
                catch {
                    // ignore
                }
            }, timeoutMs);
        }
        const finalize = (exitCode, error) => {
            if (settled)
                return;
            settled = true;
            if (timer)
                clearTimeout(timer);
            resolve({
                exitCode,
                timedOut,
                success: exitCode === 0 && !timedOut && !error,
                stdout,
                stderr,
                error: error ?? null,
                truncated,
            });
        };
        child.on("error", (err) => {
            finalize(undefined, err.message);
        });
        child.on("exit", (code) => {
            finalize(code === null ? undefined : code, null);
        });
    });
}
function resolveEnvPath(env) {
    const raw = env?.PATH ??
        env?.Path ??
        process.env.PATH ??
        process.env.Path ??
        DEFAULT_NODE_PATH;
    return raw.split(path.delimiter).filter(Boolean);
}
function ensureNodePathEnv() {
    ensureClawdbotCliOnPath({ pathEnv: process.env.PATH ?? "" });
    const current = process.env.PATH ?? "";
    if (current.trim())
        return current;
    process.env.PATH = DEFAULT_NODE_PATH;
    return DEFAULT_NODE_PATH;
}
function resolveExecutable(bin, env) {
    if (bin.includes("/") || bin.includes("\\"))
        return null;
    const extensions = process.platform === "win32"
        ? (process.env.PATHEXT ?? process.env.PathExt ?? ".EXE;.CMD;.BAT;.COM")
            .split(";")
            .map((ext) => ext.toLowerCase())
        : [""];
    for (const dir of resolveEnvPath(env)) {
        for (const ext of extensions) {
            const candidate = path.join(dir, bin + ext);
            if (fs.existsSync(candidate))
                return candidate;
        }
    }
    return null;
}
async function handleSystemWhich(params, env) {
    const bins = params.bins.map((bin) => bin.trim()).filter(Boolean);
    const found = {};
    for (const bin of bins) {
        const path = resolveExecutable(bin, env);
        if (path)
            found[bin] = path;
    }
    return { bins: found };
}
function buildExecEventPayload(payload) {
    if (!payload.output)
        return payload;
    const trimmed = payload.output.trim();
    if (!trimmed)
        return payload;
    const { text } = truncateOutput(trimmed, OUTPUT_EVENT_TAIL);
    return { ...payload, output: text };
}
async function runViaMacAppExecHost(params) {
    const { approvals, request } = params;
    return await requestExecHostViaSocket({
        socketPath: approvals.socketPath,
        token: approvals.token,
        request,
    });
}
export async function runNodeHost(opts) {
    const config = await ensureNodeHostConfig();
    const nodeId = opts.nodeId?.trim() || config.nodeId;
    if (nodeId !== config.nodeId) {
        config.nodeId = nodeId;
    }
    const displayName = opts.displayName?.trim() || config.displayName || (await getMachineDisplayName());
    config.displayName = displayName;
    const gateway = {
        host: opts.gatewayHost,
        port: opts.gatewayPort,
        tls: opts.gatewayTls ?? loadConfig().gateway?.tls?.enabled ?? false,
        tlsFingerprint: opts.gatewayTlsFingerprint,
    };
    config.gateway = gateway;
    await saveNodeHostConfig(config);
    const cfg = loadConfig();
    const browserProxy = resolveBrowserProxyConfig();
    const resolvedBrowser = resolveBrowserConfig(cfg.browser);
    const browserProxyEnabled = browserProxy.enabled &&
        resolvedBrowser.enabled &&
        shouldStartLocalBrowserServer(resolvedBrowser);
    const isRemoteMode = cfg.gateway?.mode === "remote";
    const token = process.env.OPENCLAW_GATEWAY_TOKEN?.trim() ||
        (isRemoteMode ? cfg.gateway?.remote?.token : cfg.gateway?.auth?.token);
    const password = process.env.OPENCLAW_GATEWAY_PASSWORD?.trim() ||
        (isRemoteMode ? cfg.gateway?.remote?.password : cfg.gateway?.auth?.password);
    const host = gateway.host ?? "127.0.0.1";
    const port = gateway.port ?? 18789;
    const scheme = gateway.tls ? "wss" : "ws";
    const url = `${scheme}://${host}:${port}`;
    const pathEnv = ensureNodePathEnv();
    // eslint-disable-next-line no-console
    console.log(`node host PATH: ${pathEnv}`);
    const client = new GatewayClient({
        url,
        token: token?.trim() || undefined,
        password: password?.trim() || undefined,
        instanceId: nodeId,
        clientName: GATEWAY_CLIENT_NAMES.NODE_HOST,
        clientDisplayName: displayName,
        clientVersion: VERSION,
        platform: process.platform,
        mode: GATEWAY_CLIENT_MODES.NODE,
        role: "node",
        scopes: [],
        caps: ["system", ...(browserProxyEnabled ? ["browser"] : [])],
        commands: [
            "system.run",
            "system.which",
            "system.execApprovals.get",
            "system.execApprovals.set",
            ...(browserProxyEnabled ? ["browser.proxy"] : []),
        ],
        pathEnv,
        permissions: undefined,
        deviceIdentity: loadOrCreateDeviceIdentity(),
        tlsFingerprint: gateway.tlsFingerprint,
        onEvent: (evt) => {
            if (evt.event !== "node.invoke.request")
                return;
            const payload = coerceNodeInvokePayload(evt.payload);
            if (!payload)
                return;
            void handleInvoke(payload, client, skillBins);
        },
        onConnectError: (err) => {
            // keep retrying (handled by GatewayClient)
            // eslint-disable-next-line no-console
            console.error(`node host gateway connect failed: ${err.message}`);
        },
        onClose: (code, reason) => {
            // eslint-disable-next-line no-console
            console.error(`node host gateway closed (${code}): ${reason}`);
        },
    });
    const skillBins = new SkillBinsCache(async () => {
        const res = (await client.request("skills.bins", {}));
        const bins = Array.isArray(res?.bins) ? res.bins.map((bin) => String(bin)) : [];
        return bins;
    });
    client.start();
    await new Promise(() => { });
}
async function handleInvoke(frame, client, skillBins) {
    const command = String(frame.command ?? "");
    if (command === "system.execApprovals.get") {
        try {
            ensureExecApprovals();
            const snapshot = readExecApprovalsSnapshot();
            const payload = {
                path: snapshot.path,
                exists: snapshot.exists,
                hash: snapshot.hash,
                file: redactExecApprovals(snapshot.file),
            };
            await sendInvokeResult(client, frame, {
                ok: true,
                payloadJSON: JSON.stringify(payload),
            });
        }
        catch (err) {
            await sendInvokeResult(client, frame, {
                ok: false,
                error: { code: "INVALID_REQUEST", message: String(err) },
            });
        }
        return;
    }
    if (command === "system.execApprovals.set") {
        try {
            const params = decodeParams(frame.paramsJSON);
            if (!params.file || typeof params.file !== "object") {
                throw new Error("INVALID_REQUEST: exec approvals file required");
            }
            ensureExecApprovals();
            const snapshot = readExecApprovalsSnapshot();
            requireExecApprovalsBaseHash(params, snapshot);
            const normalized = normalizeExecApprovals(params.file);
            const currentSocketPath = snapshot.file.socket?.path?.trim();
            const currentToken = snapshot.file.socket?.token?.trim();
            const socketPath = normalized.socket?.path?.trim() ?? currentSocketPath ?? resolveExecApprovalsSocketPath();
            const token = normalized.socket?.token?.trim() ?? currentToken ?? "";
            const next = {
                ...normalized,
                socket: {
                    path: socketPath,
                    token,
                },
            };
            saveExecApprovals(next);
            const nextSnapshot = readExecApprovalsSnapshot();
            const payload = {
                path: nextSnapshot.path,
                exists: nextSnapshot.exists,
                hash: nextSnapshot.hash,
                file: redactExecApprovals(nextSnapshot.file),
            };
            await sendInvokeResult(client, frame, {
                ok: true,
                payloadJSON: JSON.stringify(payload),
            });
        }
        catch (err) {
            await sendInvokeResult(client, frame, {
                ok: false,
                error: { code: "INVALID_REQUEST", message: String(err) },
            });
        }
        return;
    }
    if (command === "system.which") {
        try {
            const params = decodeParams(frame.paramsJSON);
            if (!Array.isArray(params.bins)) {
                throw new Error("INVALID_REQUEST: bins required");
            }
            const env = sanitizeEnv(undefined);
            const payload = await handleSystemWhich(params, env);
            await sendInvokeResult(client, frame, {
                ok: true,
                payloadJSON: JSON.stringify(payload),
            });
        }
        catch (err) {
            await sendInvokeResult(client, frame, {
                ok: false,
                error: { code: "INVALID_REQUEST", message: String(err) },
            });
        }
        return;
    }
    if (command === "browser.proxy") {
        try {
            const params = decodeParams(frame.paramsJSON);
            const pathValue = typeof params.path === "string" ? params.path.trim() : "";
            if (!pathValue) {
                throw new Error("INVALID_REQUEST: path required");
            }
            const proxyConfig = resolveBrowserProxyConfig();
            if (!proxyConfig.enabled) {
                throw new Error("UNAVAILABLE: node browser proxy disabled");
            }
            await ensureBrowserControlServer();
            const resolved = resolveBrowserConfig(loadConfig().browser);
            const requestedProfile = typeof params.profile === "string" ? params.profile.trim() : "";
            const allowedProfiles = proxyConfig.allowProfiles;
            if (allowedProfiles.length > 0) {
                if (pathValue !== "/profiles") {
                    const profileToCheck = requestedProfile || resolved.defaultProfile;
                    if (!isProfileAllowed({ allowProfiles: allowedProfiles, profile: profileToCheck })) {
                        throw new Error("INVALID_REQUEST: browser profile not allowed");
                    }
                }
                else if (requestedProfile) {
                    if (!isProfileAllowed({ allowProfiles: allowedProfiles, profile: requestedProfile })) {
                        throw new Error("INVALID_REQUEST: browser profile not allowed");
                    }
                }
            }
            const url = new URL(pathValue.startsWith("/") ? pathValue : `/${pathValue}`, resolved.controlUrl);
            if (requestedProfile) {
                url.searchParams.set("profile", requestedProfile);
            }
            const query = params.query ?? {};
            for (const [key, value] of Object.entries(query)) {
                if (value === undefined || value === null)
                    continue;
                url.searchParams.set(key, String(value));
            }
            const method = typeof params.method === "string" ? params.method.toUpperCase() : "GET";
            const body = params.body;
            const ctrl = new AbortController();
            const timeoutMs = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
                ? Math.max(1, Math.floor(params.timeoutMs))
                : 20_000;
            const timer = setTimeout(() => ctrl.abort(), timeoutMs);
            const headers = new Headers();
            let bodyJson;
            if (body !== undefined) {
                headers.set("Content-Type", "application/json");
                bodyJson = JSON.stringify(body);
            }
            const token = process.env.OPENCLAW_BROWSER_CONTROL_TOKEN?.trim() || resolved.controlToken?.trim();
            if (token) {
                headers.set("Authorization", `Bearer ${token}`);
            }
            let res;
            try {
                res = await fetch(url.toString(), {
                    method,
                    headers,
                    body: bodyJson,
                    signal: ctrl.signal,
                });
            }
            finally {
                clearTimeout(timer);
            }
            if (!res.ok) {
                const text = await res.text().catch(() => "");
                throw new Error(text ? `${res.status}: ${text}` : `HTTP ${res.status}`);
            }
            const result = (await res.json());
            if (allowedProfiles.length > 0 && url.pathname === "/profiles") {
                const obj = typeof result === "object" && result !== null ? result : {};
                const profiles = Array.isArray(obj.profiles) ? obj.profiles : [];
                obj.profiles = profiles.filter((entry) => {
                    if (!entry || typeof entry !== "object")
                        return false;
                    const name = entry.name;
                    return typeof name === "string" && allowedProfiles.includes(name);
                });
            }
            let files;
            const paths = collectBrowserProxyPaths(result);
            if (paths.length > 0) {
                const loaded = await Promise.all(paths.map(async (p) => {
                    try {
                        const file = await readBrowserProxyFile(p);
                        if (!file) {
                            throw new Error("file not found");
                        }
                        return file;
                    }
                    catch (err) {
                        throw new Error(`browser proxy file read failed for ${p}: ${String(err)}`);
                    }
                }));
                if (loaded.length > 0)
                    files = loaded;
            }
            const payload = files ? { result, files } : { result };
            await sendInvokeResult(client, frame, {
                ok: true,
                payloadJSON: JSON.stringify(payload),
            });
        }
        catch (err) {
            await sendInvokeResult(client, frame, {
                ok: false,
                error: { code: "INVALID_REQUEST", message: String(err) },
            });
        }
        return;
    }
    if (command !== "system.run") {
        await sendInvokeResult(client, frame, {
            ok: false,
            error: { code: "UNAVAILABLE", message: "command not supported" },
        });
        return;
    }
    let params;
    try {
        params = decodeParams(frame.paramsJSON);
    }
    catch (err) {
        await sendInvokeResult(client, frame, {
            ok: false,
            error: { code: "INVALID_REQUEST", message: String(err) },
        });
        return;
    }
    if (!Array.isArray(params.command) || params.command.length === 0) {
        await sendInvokeResult(client, frame, {
            ok: false,
            error: { code: "INVALID_REQUEST", message: "command required" },
        });
        return;
    }
    const argv = params.command.map((item) => String(item));
    const rawCommand = typeof params.rawCommand === "string" ? params.rawCommand.trim() : "";
    const cmdText = rawCommand || formatCommand(argv);
    const agentId = params.agentId?.trim() || undefined;
    const cfg = loadConfig();
    const agentExec = agentId ? resolveAgentConfig(cfg, agentId)?.tools?.exec : undefined;
    const configuredSecurity = resolveExecSecurity(agentExec?.security ?? cfg.tools?.exec?.security);
    const configuredAsk = resolveExecAsk(agentExec?.ask ?? cfg.tools?.exec?.ask);
    const approvals = resolveExecApprovals(agentId, {
        security: configuredSecurity,
        ask: configuredAsk,
    });
    const security = approvals.agent.security;
    const ask = approvals.agent.ask;
    const autoAllowSkills = approvals.agent.autoAllowSkills;
    const sessionKey = params.sessionKey?.trim() || "node";
    const runId = params.runId?.trim() || crypto.randomUUID();
    const env = sanitizeEnv(params.env ?? undefined);
    const safeBins = resolveSafeBins(agentExec?.safeBins ?? cfg.tools?.exec?.safeBins);
    const bins = autoAllowSkills ? await skillBins.current() : new Set();
    let analysisOk = false;
    let allowlistMatches = [];
    let allowlistSatisfied = false;
    let segments = [];
    if (rawCommand) {
        const allowlistEval = evaluateShellAllowlist({
            command: rawCommand,
            allowlist: approvals.allowlist,
            safeBins,
            cwd: params.cwd ?? undefined,
            env,
            skillBins: bins,
            autoAllowSkills,
            platform: process.platform,
        });
        analysisOk = allowlistEval.analysisOk;
        allowlistMatches = allowlistEval.allowlistMatches;
        allowlistSatisfied =
            security === "allowlist" && analysisOk ? allowlistEval.allowlistSatisfied : false;
        segments = allowlistEval.segments;
    }
    else {
        const analysis = analyzeArgvCommand({ argv, cwd: params.cwd ?? undefined, env });
        const allowlistEval = evaluateExecAllowlist({
            analysis,
            allowlist: approvals.allowlist,
            safeBins,
            cwd: params.cwd ?? undefined,
            skillBins: bins,
            autoAllowSkills,
        });
        analysisOk = analysis.ok;
        allowlistMatches = allowlistEval.allowlistMatches;
        allowlistSatisfied =
            security === "allowlist" && analysisOk ? allowlistEval.allowlistSatisfied : false;
        segments = analysis.segments;
    }
    const isWindows = process.platform === "win32";
    const cmdInvocation = rawCommand
        ? isCmdExeInvocation(segments[0]?.argv ?? [])
        : isCmdExeInvocation(argv);
    if (security === "allowlist" && isWindows && cmdInvocation) {
        analysisOk = false;
        allowlistSatisfied = false;
    }
    const useMacAppExec = process.platform === "darwin";
    if (useMacAppExec) {
        const approvalDecision = params.approvalDecision === "allow-once" || params.approvalDecision === "allow-always"
            ? params.approvalDecision
            : null;
        const execRequest = {
            command: argv,
            rawCommand: rawCommand || null,
            cwd: params.cwd ?? null,
            env: params.env ?? null,
            timeoutMs: params.timeoutMs ?? null,
            needsScreenRecording: params.needsScreenRecording ?? null,
            agentId: agentId ?? null,
            sessionKey: sessionKey ?? null,
            approvalDecision,
        };
        const response = await runViaMacAppExecHost({ approvals, request: execRequest });
        if (!response) {
            if (execHostEnforced || !execHostFallbackAllowed) {
                await sendNodeEvent(client, "exec.denied", buildExecEventPayload({
                    sessionKey,
                    runId,
                    host: "node",
                    command: cmdText,
                    reason: "companion-unavailable",
                }));
                await sendInvokeResult(client, frame, {
                    ok: false,
                    error: {
                        code: "UNAVAILABLE",
                        message: "COMPANION_APP_UNAVAILABLE: macOS app exec host unreachable",
                    },
                });
                return;
            }
        }
        else if (!response.ok) {
            const reason = response.error.reason ?? "approval-required";
            await sendNodeEvent(client, "exec.denied", buildExecEventPayload({
                sessionKey,
                runId,
                host: "node",
                command: cmdText,
                reason,
            }));
            await sendInvokeResult(client, frame, {
                ok: false,
                error: { code: "UNAVAILABLE", message: response.error.message },
            });
            return;
        }
        else {
            const result = response.payload;
            const combined = [result.stdout, result.stderr, result.error].filter(Boolean).join("\n");
            await sendNodeEvent(client, "exec.finished", buildExecEventPayload({
                sessionKey,
                runId,
                host: "node",
                command: cmdText,
                exitCode: result.exitCode,
                timedOut: result.timedOut,
                success: result.success,
                output: combined,
            }));
            await sendInvokeResult(client, frame, {
                ok: true,
                payloadJSON: JSON.stringify(result),
            });
            return;
        }
    }
    if (security === "deny") {
        await sendNodeEvent(client, "exec.denied", buildExecEventPayload({
            sessionKey,
            runId,
            host: "node",
            command: cmdText,
            reason: "security=deny",
        }));
        await sendInvokeResult(client, frame, {
            ok: false,
            error: { code: "UNAVAILABLE", message: "SYSTEM_RUN_DISABLED: security=deny" },
        });
        return;
    }
    const requiresAsk = requiresExecApproval({
        ask,
        security,
        analysisOk,
        allowlistSatisfied,
    });
    const approvalDecision = params.approvalDecision === "allow-once" || params.approvalDecision === "allow-always"
        ? params.approvalDecision
        : null;
    const approvedByAsk = approvalDecision !== null || params.approved === true;
    if (requiresAsk && !approvedByAsk) {
        await sendNodeEvent(client, "exec.denied", buildExecEventPayload({
            sessionKey,
            runId,
            host: "node",
            command: cmdText,
            reason: "approval-required",
        }));
        await sendInvokeResult(client, frame, {
            ok: false,
            error: { code: "UNAVAILABLE", message: "SYSTEM_RUN_DENIED: approval required" },
        });
        return;
    }
    if (approvalDecision === "allow-always" && security === "allowlist") {
        if (analysisOk) {
            const patterns = resolveAllowAlwaysPatterns({
                segments,
                cwd: params.cwd ?? undefined,
                env,
                platform: process.platform,
            });
            for (const pattern of patterns) {
                if (pattern)
                    addAllowlistEntry(approvals.file, agentId, pattern);
            }
        }
    }
    if (security === "allowlist" && (!analysisOk || !allowlistSatisfied) && !approvedByAsk) {
        await sendNodeEvent(client, "exec.denied", buildExecEventPayload({
            sessionKey,
            runId,
            host: "node",
            command: cmdText,
            reason: "allowlist-miss",
        }));
        await sendInvokeResult(client, frame, {
            ok: false,
            error: { code: "UNAVAILABLE", message: "SYSTEM_RUN_DENIED: allowlist miss" },
        });
        return;
    }
    if (allowlistMatches.length > 0) {
        const seen = new Set();
        for (const match of allowlistMatches) {
            if (!match?.pattern || seen.has(match.pattern))
                continue;
            seen.add(match.pattern);
            recordAllowlistUse(approvals.file, agentId, match, cmdText, segments[0]?.resolution?.resolvedPath);
        }
    }
    if (params.needsScreenRecording === true) {
        await sendNodeEvent(client, "exec.denied", buildExecEventPayload({
            sessionKey,
            runId,
            host: "node",
            command: cmdText,
            reason: "permission:screenRecording",
        }));
        await sendInvokeResult(client, frame, {
            ok: false,
            error: { code: "UNAVAILABLE", message: "PERMISSION_MISSING: screenRecording" },
        });
        return;
    }
    let execArgv = argv;
    if (security === "allowlist" &&
        isWindows &&
        !approvedByAsk &&
        rawCommand &&
        analysisOk &&
        allowlistSatisfied &&
        segments.length === 1 &&
        segments[0]?.argv.length > 0) {
        // Avoid cmd.exe in allowlist mode on Windows; run the parsed argv directly.
        execArgv = segments[0].argv;
    }
    const result = await runCommand(execArgv, params.cwd?.trim() || undefined, env, params.timeoutMs ?? undefined);
    if (result.truncated) {
        const suffix = "... (truncated)";
        if (result.stderr.trim().length > 0) {
            result.stderr = `${result.stderr}\n${suffix}`;
        }
        else {
            result.stdout = `${result.stdout}\n${suffix}`;
        }
    }
    const combined = [result.stdout, result.stderr, result.error].filter(Boolean).join("\n");
    await sendNodeEvent(client, "exec.finished", buildExecEventPayload({
        sessionKey,
        runId,
        host: "node",
        command: cmdText,
        exitCode: result.exitCode,
        timedOut: result.timedOut,
        success: result.success,
        output: combined,
    }));
    await sendInvokeResult(client, frame, {
        ok: true,
        payloadJSON: JSON.stringify({
            exitCode: result.exitCode,
            timedOut: result.timedOut,
            success: result.success,
            stdout: result.stdout,
            stderr: result.stderr,
            error: result.error ?? null,
        }),
    });
}
function decodeParams(raw) {
    if (!raw) {
        throw new Error("INVALID_REQUEST: paramsJSON required");
    }
    return JSON.parse(raw);
}
function coerceNodeInvokePayload(payload) {
    if (!payload || typeof payload !== "object")
        return null;
    const obj = payload;
    const id = typeof obj.id === "string" ? obj.id.trim() : "";
    const nodeId = typeof obj.nodeId === "string" ? obj.nodeId.trim() : "";
    const command = typeof obj.command === "string" ? obj.command.trim() : "";
    if (!id || !nodeId || !command)
        return null;
    const paramsJSON = typeof obj.paramsJSON === "string"
        ? obj.paramsJSON
        : obj.params !== undefined
            ? JSON.stringify(obj.params)
            : null;
    const timeoutMs = typeof obj.timeoutMs === "number" ? obj.timeoutMs : null;
    const idempotencyKey = typeof obj.idempotencyKey === "string" ? obj.idempotencyKey : null;
    return {
        id,
        nodeId,
        command,
        paramsJSON,
        timeoutMs,
        idempotencyKey,
    };
}
async function sendInvokeResult(client, frame, result) {
    try {
        await client.request("node.invoke.result", buildNodeInvokeResultParams(frame, result));
    }
    catch {
        // ignore: node invoke responses are best-effort
    }
}
export function buildNodeInvokeResultParams(frame, result) {
    const params = {
        id: frame.id,
        nodeId: frame.nodeId,
        ok: result.ok,
    };
    if (result.payload !== undefined) {
        params.payload = result.payload;
    }
    if (typeof result.payloadJSON === "string") {
        params.payloadJSON = result.payloadJSON;
    }
    if (result.error) {
        params.error = result.error;
    }
    return params;
}
async function sendNodeEvent(client, event, payload) {
    try {
        await client.request("node.event", {
            event,
            payloadJSON: payload ? JSON.stringify(payload) : null,
        });
    }
    catch {
        // ignore: node events are best-effort
    }
}
