UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

1,459 lines (1,451 loc) 48.8 kB
import { d as resolveIsNixMode, g as resolveStateDir } from "./paths-BDd7_JUB.js"; import { n as resolveAgentConfig } from "./agent-scope-CrgUOY3f.js"; import { I as colorize, L as isRich, R as theme, c as defaultRuntime } from "./subsystem-46MXi6Ip.js"; import "./utils-Dg0Xbl6w.js"; import "./exec-CTo4hK94.js"; import "./model-selection-Cp1maz7B.js"; import "./github-copilot-token-VZsS4013.js"; import { t as formatCliCommand } from "./command-format-BQK1OIvH.js"; import "./boolean-CE7i9tBR.js"; import "./env-DOcCob95.js"; import { M as VERSION, r as loadConfig } from "./config-qgIz1lbh.js"; import "./manifest-registry-BFpLJJDB.js"; import "./chrome-D2LUApAY.js"; import { l as detectMime } from "./routes-Ds-tIZFJ.js"; import { i as resolveBrowserConfig } from "./server-context-D2cv-pIA.js"; import { m as GATEWAY_CLIENT_NAMES, p as GATEWAY_CLIENT_MODES } from "./message-channel-CQGWXVL4.js"; import "./logging-BdnOSVPD.js"; import { i as getMachineDisplayName, n as isWSL, t as createBrowserRouteDispatcher } from "./dispatcher-BNB5aCZ6.js"; import { At as loadOrCreateDeviceIdentity, t as GatewayClient } from "./client-zqMhLTAX.js"; import { t as formatDocsLink } from "./links-C9fyAH-V.js"; import { a as evaluateShellAllowlist, c as normalizeExecApprovals, d as requiresExecApproval, f as resolveExecApprovals, g as saveExecApprovals, h as resolveSafeBins, i as evaluateExecAllowlist, l as readExecApprovalsSnapshot, m as resolveExecApprovalsSocketPath, n as analyzeArgvCommand, r as ensureExecApprovals, t as addAllowlistEntry, u as recordAllowlistUse } from "./exec-approvals-CK-Umdr3.js"; import { r as startBrowserControlServiceFromConfig, t as createBrowserControlContext } from "./control-service-BW5sW2U1.js"; import { c as formatNodeServiceDescription, g as resolveNodeWindowsTaskName, h as resolveNodeSystemdServiceName, m as resolveNodeLaunchAgentLabel } from "./constants-3er_81qc.js"; import { t as ensureOpenClawCliOnPath } from "./path-env-CuGC6r1F.js"; import { o as resolveGatewayLogPaths } from "./service-CAxAjHNB.js"; import { r as isSystemdUserServiceAvailable } from "./systemd-CNYEkRek.js"; import { t as resolveNodeService } from "./node-service-CjtBRyjp.js"; import { d as renderSystemNodeWarning, f as resolvePreferredNodePath, h as resolveNodeProgramArguments, o as resolveGatewayDevMode, p as resolveSystemNodeInfo, r as isGatewayDaemonRuntime, s as buildNodeServiceEnvironment, t as DEFAULT_GATEWAY_DAEMON_RUNTIME } from "./daemon-runtime-vNkYv9tq.js"; import { n as renderSystemdUnavailableHints } from "./systemd-hints-CcgK8AJE.js"; import { d as createNullWriter, f as emitDaemonActionJson, i as parsePort, n as formatRuntimeStatus, u as buildDaemonServiceSnapshot } from "./shared-ChNOqAzp.js"; import path from "node:path"; import fs from "node:fs"; import fs$1 from "node:fs/promises"; import { spawn } from "node:child_process"; import crypto from "node:crypto"; import net from "node:net"; //#region src/node-host/config.ts const NODE_HOST_FILE = "node.json"; function resolveNodeHostConfigPath() { return path.join(resolveStateDir(), NODE_HOST_FILE); } function normalizeConfig(config) { const base = { version: 1, nodeId: "", token: config?.token, displayName: config?.displayName, gateway: config?.gateway }; if (config?.version === 1 && typeof config.nodeId === "string") base.nodeId = config.nodeId.trim(); if (!base.nodeId) base.nodeId = crypto.randomUUID(); return base; } async function loadNodeHostConfig() { const filePath = resolveNodeHostConfigPath(); try { const raw = await fs$1.readFile(filePath, "utf8"); return normalizeConfig(JSON.parse(raw)); } catch { return null; } } async function saveNodeHostConfig(config) { const filePath = resolveNodeHostConfigPath(); await fs$1.mkdir(path.dirname(filePath), { recursive: true }); const payload = JSON.stringify(config, null, 2); await fs$1.writeFile(filePath, `${payload}\n`, { mode: 384 }); try { await fs$1.chmod(filePath, 384); } catch {} } async function ensureNodeHostConfig() { const normalized = normalizeConfig(await loadNodeHostConfig()); await saveNodeHostConfig(normalized); return normalized; } //#endregion //#region src/infra/exec-host.ts async function requestExecHostViaSocket(params) { const { socketPath, token, request } = params; if (!socketPath || !token) return null; const timeoutMs = params.timeoutMs ?? 2e4; return await new Promise((resolve) => { const client = new net.Socket(); let settled = false; let buffer = ""; const finish = (value) => { if (settled) return; settled = true; try { client.destroy(); } catch {} resolve(value); }; const requestJson = JSON.stringify(request); const nonce = crypto.randomBytes(16).toString("hex"); const ts = Date.now(); const hmac = crypto.createHmac("sha256", token).update(`${nonce}:${ts}:${requestJson}`).digest("hex"); const payload = JSON.stringify({ type: "exec", id: crypto.randomUUID(), nonce, ts, hmac, requestJson }); const timer = setTimeout(() => finish(null), timeoutMs); client.on("error", () => finish(null)); client.connect(socketPath, () => { client.write(`${payload}\n`); }); client.on("data", (data) => { buffer += data.toString("utf8"); let idx = buffer.indexOf("\n"); while (idx !== -1) { const line = buffer.slice(0, idx).trim(); buffer = buffer.slice(idx + 1); idx = buffer.indexOf("\n"); if (!line) continue; try { const msg = JSON.parse(line); if (msg?.type === "exec-res") { clearTimeout(timer); if (msg.ok === true && msg.payload) { finish({ ok: true, payload: msg.payload }); return; } if (msg.ok === false && msg.error) { finish({ ok: false, error: msg.error }); return; } finish(null); return; } } catch {} } }); }); } //#endregion //#region src/node-host/runner.ts 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 = 2e5; const OUTPUT_EVENT_TAIL = 2e4; 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", "PYTHONHOME", "PYTHONPATH", "PERL5LIB", "PERL5OPT", "RUBYOPT" ]); const blockedEnvPrefixes = ["DYLD_", "LD_"]; var SkillBinsCache = class { constructor(fetch) { this.bins = /* @__PURE__ */ new Set(); this.lastRefresh = 0; this.ttlMs = 9e4; 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 = /* @__PURE__ */ new Set(); } } }; function sanitizeEnv(overrides) { if (!overrides) return; const merged = { ...process.env }; 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 proxy = loadConfig().nodeHost?.browserProxy; const allowProfiles = normalizeProfileAllowlist(proxy?.allowProfiles); return { enabled: proxy?.enabled !== false, allowProfiles }; } let browserControlReady = null; async function ensureBrowserControlService() { if (browserControlReady) return browserControlReady; browserControlReady = (async () => { const cfg = loadConfig(); if (!resolveBrowserConfig(cfg.browser, cfg).enabled) throw new Error("browser control disabled"); if (!await startBrowserControlServiceFromConfig()) throw new Error("browser control disabled"); })(); return browserControlReady; } async function withTimeout(promise, timeoutMs, label) { const resolved = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) ? Math.max(1, Math.floor(timeoutMs)) : void 0; if (!resolved) return await promise; let timer; const timeoutPromise = new Promise((_, reject) => { timer = setTimeout(() => { reject(/* @__PURE__ */ new Error(`${label ?? "request"} timed out`)); }, resolved); }); try { return await Promise.race([promise, timeoutPromise]); } finally { if (timer) clearTimeout(timer); } } 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 = /* @__PURE__ */ 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 fs$1.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 fs$1.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 "\"\""; if (!/\s|"/.test(trimmed)) 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 } : void 0 }; } 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 {} }, 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(void 0, err.message); }); child.on("exit", (code) => { finalize(code === null ? void 0 : code, null); }); }); } function resolveEnvPath(env) { return (env?.PATH ?? env?.Path ?? process.env.PATH ?? process.env.Path ?? DEFAULT_NODE_PATH).split(path.delimiter).filter(Boolean); } function ensureNodePathEnv() { ensureOpenClawCliOnPath({ 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 }); } 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, cfg); const browserProxyEnabled = browserProxy.enabled && resolvedBrowser.enabled; 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 url = `${gateway.tls ? "wss" : "ws"}://${host}:${port}`; const pathEnv = ensureNodePathEnv(); console.log(`node host PATH: ${pathEnv}`); const client = new GatewayClient({ url, token: token?.trim() || void 0, password: password?.trim() || void 0, 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: void 0, deviceIdentity: loadOrCreateDeviceIdentity(), tlsFingerprint: gateway.tlsFingerprint, onEvent: (evt) => { if (evt.event !== "node.invoke.request") return; const payload = coerceNodeInvokePayload(evt.payload); if (!payload) return; handleInvoke(payload, client, skillBins); }, onConnectError: (err) => { console.error(`node host gateway connect failed: ${err.message}`); }, onClose: (code, reason) => { console.error(`node host gateway closed (${code}): ${reason}`); } }); const skillBins = new SkillBinsCache(async () => { const res = await client.request("skills.bins", {}); return Array.isArray(res?.bins) ? res.bins.map((bin) => String(bin)) : []; }); 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) { const message = String(err); await sendInvokeResult(client, frame, { ok: false, error: { code: message.toLowerCase().includes("timed out") ? "TIMEOUT" : "INVALID_REQUEST", message } }); } 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 ?? ""; saveExecApprovals({ ...normalized, socket: { path: socketPath, token } }); 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 payload = await handleSystemWhich(params, sanitizeEnv(void 0)); 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 ensureBrowserControlService(); const cfg = loadConfig(); const resolved = resolveBrowserConfig(cfg.browser, cfg); const requestedProfile = typeof params.profile === "string" ? params.profile.trim() : ""; const allowedProfiles = proxyConfig.allowProfiles; if (allowedProfiles.length > 0) { if (pathValue !== "/profiles") { if (!isProfileAllowed({ allowProfiles: allowedProfiles, profile: requestedProfile || resolved.defaultProfile })) 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 method = typeof params.method === "string" ? params.method.toUpperCase() : "GET"; const path = pathValue.startsWith("/") ? pathValue : `/${pathValue}`; const body = params.body; const query = {}; if (requestedProfile) query.profile = requestedProfile; const rawQuery = params.query ?? {}; for (const [key, value] of Object.entries(rawQuery)) { if (value === void 0 || value === null) continue; query[key] = typeof value === "string" ? value : String(value); } const response = await withTimeout(createBrowserRouteDispatcher(createBrowserControlContext()).dispatch({ method: method === "DELETE" ? "DELETE" : method === "POST" ? "POST" : "GET", path, query, body }), params.timeoutMs, "browser proxy request"); if (response.status >= 400) { const message = response.body && typeof response.body === "object" && "error" in response.body ? String(response.body.error) : `HTTP ${response.status}`; throw new Error(message); } const result = response.body; if (allowedProfiles.length > 0 && path === "/profiles") { const obj = typeof result === "object" && result !== null ? result : {}; obj.profiles = (Array.isArray(obj.profiles) ? obj.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)}`, { cause: 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() || void 0; const cfg = loadConfig(); const agentExec = agentId ? resolveAgentConfig(cfg, agentId)?.tools?.exec : void 0; const approvals = resolveExecApprovals(agentId, { security: resolveExecSecurity(agentExec?.security ?? cfg.tools?.exec?.security), ask: resolveExecAsk(agentExec?.ask ?? cfg.tools?.exec?.ask) }); 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 ?? void 0); const safeBins = resolveSafeBins(agentExec?.safeBins ?? cfg.tools?.exec?.safeBins); const bins = autoAllowSkills ? await skillBins.current() : /* @__PURE__ */ 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 ?? void 0, 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 ?? void 0, env }); const allowlistEval = evaluateExecAllowlist({ analysis, allowlist: approvals.allowlist, safeBins, cwd: params.cwd ?? void 0, 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; } if (process.platform === "darwin") { const approvalDecision = params.approvalDecision === "allow-once" || params.approvalDecision === "allow-always" ? params.approvalDecision : null; const response = await runViaMacAppExecHost({ approvals, request: { 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 } }); 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) { await sendNodeEvent(client, "exec.denied", buildExecEventPayload({ sessionKey, runId, host: "node", command: cmdText, reason: response.error.reason ?? "approval-required" })); 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) for (const segment of segments) { const pattern = segment.resolution?.resolvedPath ?? ""; 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 = /* @__PURE__ */ 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) execArgv = segments[0].argv; const result = await runCommand(execArgv, params.cwd?.trim() || void 0, env, params.timeoutMs ?? void 0); 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; return { id, nodeId, command, paramsJSON: typeof obj.paramsJSON === "string" ? obj.paramsJSON : obj.params !== void 0 ? JSON.stringify(obj.params) : null, timeoutMs: typeof obj.timeoutMs === "number" ? obj.timeoutMs : null, idempotencyKey: typeof obj.idempotencyKey === "string" ? obj.idempotencyKey : null }; } async function sendInvokeResult(client, frame, result) { try { await client.request("node.invoke.result", buildNodeInvokeResultParams(frame, result)); } catch {} } function buildNodeInvokeResultParams(frame, result) { const params = { id: frame.id, nodeId: frame.nodeId, ok: result.ok }; if (result.payload !== void 0) 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 {} } //#endregion //#region src/commands/node-daemon-install-helpers.ts async function buildNodeInstallPlan(params) { const devMode = params.devMode ?? resolveGatewayDevMode(); const nodePath = params.nodePath ?? await resolvePreferredNodePath({ env: params.env, runtime: params.runtime }); const { programArguments, workingDirectory } = await resolveNodeProgramArguments({ host: params.host, port: params.port, tls: params.tls, tlsFingerprint: params.tlsFingerprint, nodeId: params.nodeId, displayName: params.displayName, dev: devMode, runtime: params.runtime, nodePath }); if (params.runtime === "node") { const warning = renderSystemNodeWarning(await resolveSystemNodeInfo({ env: params.env }), programArguments[0]); if (warning) params.warn?.(warning, "Node daemon runtime"); } const environment = buildNodeServiceEnvironment({ env: params.env }); return { programArguments, workingDirectory, environment, description: formatNodeServiceDescription({ version: environment.OPENCLAW_SERVICE_VERSION }) }; } //#endregion //#region src/commands/node-daemon-runtime.ts const DEFAULT_NODE_DAEMON_RUNTIME = DEFAULT_GATEWAY_DAEMON_RUNTIME; function isNodeDaemonRuntime(value) { return isGatewayDaemonRuntime(value); } //#endregion //#region src/cli/node-cli/daemon.ts function renderNodeServiceStartHints() { const base = [formatCliCommand("openclaw node install"), formatCliCommand("openclaw node start")]; switch (process.platform) { case "darwin": return [...base, `launchctl bootstrap gui/$UID ~/Library/LaunchAgents/${resolveNodeLaunchAgentLabel()}.plist`]; case "linux": return [...base, `systemctl --user start ${resolveNodeSystemdServiceName()}.service`]; case "win32": return [...base, `schtasks /Run /TN "${resolveNodeWindowsTaskName()}"`]; default: return base; } } function buildNodeRuntimeHints(env = process.env) { if (process.platform === "darwin") { const logs = resolveGatewayLogPaths(env); return [`Launchd stdout (if installed): ${logs.stdoutPath}`, `Launchd stderr (if installed): ${logs.stderrPath}`]; } if (process.platform === "linux") return [`Logs: journalctl --user -u ${resolveNodeSystemdServiceName()}.service -n 200 --no-pager`]; if (process.platform === "win32") return [`Logs: schtasks /Query /TN "${resolveNodeWindowsTaskName()}" /V /FO LIST`]; return []; } function resolveNodeDefaults(opts, config) { const host = opts.host?.trim() || config?.gateway?.host || "127.0.0.1"; const portOverride = parsePort(opts.port); if (opts.port !== void 0 && portOverride === null) return { host, port: null }; return { host, port: portOverride ?? config?.gateway?.port ?? 18789 }; } async function runNodeDaemonInstall(opts) { const json = Boolean(opts.json); const warnings = []; const stdout = json ? createNullWriter() : process.stdout; const emit = (payload) => { if (!json) return; emitDaemonActionJson({ action: "install", ...payload }); }; const fail = (message, hints) => { if (json) emit({ ok: false, error: message, hints, warnings: warnings.length ? warnings : void 0 }); else { defaultRuntime.error(message); if (hints?.length) for (const hint of hints) defaultRuntime.log(`Tip: ${hint}`); } defaultRuntime.exit(1); }; if (resolveIsNixMode(process.env)) { fail("Nix mode detected; service install is disabled."); return; } const config = await loadNodeHostConfig(); const { host, port } = resolveNodeDefaults(opts, config); if (!Number.isFinite(port ?? NaN) || (port ?? 0) <= 0) { fail("Invalid port"); return; } const runtimeRaw = opts.runtime ? String(opts.runtime) : DEFAULT_NODE_DAEMON_RUNTIME; if (!isNodeDaemonRuntime(runtimeRaw)) { fail("Invalid --runtime (use \"node\" or \"bun\")"); return; } const service = resolveNodeService(); let loaded = false; try { loaded = await service.isLoaded({ env: process.env }); } catch (err) { fail(`Node service check failed: ${String(err)}`); return; } if (loaded && !opts.force) { emit({ ok: true, result: "already-installed", message: `Node service already ${service.loadedText}.`, service: buildDaemonServiceSnapshot(service, loaded), warnings: warnings.length ? warnings : void 0 }); if (!json) { defaultRuntime.log(`Node service already ${service.loadedText}.`); defaultRuntime.log(`Reinstall with: ${formatCliCommand("openclaw node install --force")}`); } return; } const tlsFingerprint = opts.tlsFingerprint?.trim() || config?.gateway?.tlsFingerprint; const tls = Boolean(opts.tls) || Boolean(tlsFingerprint) || Boolean(config?.gateway?.tls); const { programArguments, workingDirectory, environment, description } = await buildNodeInstallPlan({ env: process.env, host, port: port ?? 18789, tls, tlsFingerprint: tlsFingerprint || void 0, nodeId: opts.nodeId, displayName: opts.displayName, runtime: runtimeRaw, warn: (message) => { if (json) warnings.push(message); else defaultRuntime.log(message); } }); try { await service.install({ env: process.env, stdout, programArguments, workingDirectory, environment, description }); } catch (err) { fail(`Node install failed: ${String(err)}`); return; } let installed = true; try { installed = await service.isLoaded({ env: process.env }); } catch { installed = true; } emit({ ok: true, result: "installed", service: buildDaemonServiceSnapshot(service, installed), warnings: warnings.length ? warnings : void 0 }); } async function runNodeDaemonUninstall(opts = {}) { const json = Boolean(opts.json); const stdout = json ? createNullWriter() : process.stdout; const emit = (payload) => { if (!json) return; emitDaemonActionJson({ action: "uninstall", ...payload }); }; const fail = (message) => { if (json) emit({ ok: false, error: message }); else defaultRuntime.error(message); defaultRuntime.exit(1); }; if (resolveIsNixMode(process.env)) { fail("Nix mode detected; service uninstall is disabled."); return; } const service = resolveNodeService(); try { await service.uninstall({ env: process.env, stdout }); } catch (err) { fail(`Node uninstall failed: ${String(err)}`); return; } let loaded = false; try { loaded = await service.isLoaded({ env: process.env }); } catch { loaded = false; } emit({ ok: true, result: "uninstalled", service: buildDaemonServiceSnapshot(service, loaded) }); } async function runNodeDaemonRestart(opts = {}) { const json = Boolean(opts.json); const stdout = json ? createNullWriter() : process.stdout; const emit = (payload) => { if (!json) return; emitDaemonActionJson({ action: "restart", ...payload }); }; const fail = (message, hints) => { if (json) emit({ ok: false, error: message, hints }); else defaultRuntime.error(message); defaultRuntime.exit(1); }; const service = resolveNodeService(); let loaded = false; try { loaded = await service.isLoaded({ env: process.env }); } catch (err) { fail(`Node service check failed: ${String(err)}`); return; } if (!loaded) { let hints = renderNodeServiceStartHints(); if (process.platform === "linux") { if (!await isSystemdUserServiceAvailable().catch(() => false)) hints = [...hints, ...renderSystemdUnavailableHints({ wsl: await isWSL() })]; } emit({ ok: true, result: "not-loaded", message: `Node service ${service.notLoadedText}.`, hints, service: buildDaemonServiceSnapshot(service, loaded) }); if (!json) { defaultRuntime.log(`Node service ${service.notLoadedText}.`); for (const hint of hints) defaultRuntime.log(`Start with: ${hint}`); } return; } try { await service.restart({ env: process.env, stdout }); } catch (err) { const hints = renderNodeServiceStartHints(); fail(`Node restart failed: ${String(err)}`, hints); return; } let restarted = true; try { restarted = await service.isLoaded({ env: process.env }); } catch { restarted = true; } emit({ ok: true, result: "restarted", service: buildDaemonServiceSnapshot(service, restarted) }); } async function runNodeDaemonStop(opts = {}) { const json = Boolean(opts.json); const stdout = json ? createNullWriter() : process.stdout; const emit = (payload) => { if (!json) return; emitDaemonActionJson({ action: "stop", ...payload }); }; const fail = (message) => { if (json) emit({ ok: false, error: message }); else defaultRuntime.error(message); defaultRuntime.exit(1); }; const service = resolveNodeService(); let loaded = false; try { loaded = await service.isLoaded({ env: process.env }); } catch (err) { fail(`Node service check failed: ${String(err)}`); return; } if (!loaded) { emit({ ok: true, result: "not-loaded", message: `Node service ${service.notLoadedText}.`, service: buildDaemonServiceSnapshot(service, loaded) }); if (!json) defaultRuntime.log(`Node service ${service.notLoadedText}.`); return; } try { await service.stop({ env: process.env, stdout }); } catch (err) { fail(`Node stop failed: ${String(err)}`); return; } let stopped = false; try { stopped = await service.isLoaded({ env: process.env }); } catch { stopped = false; } emit({ ok: true, result: "stopped", service: buildDaemonServiceSnapshot(service, stopped) }); } async function runNodeDaemonStatus(opts = {}) { const json = Boolean(opts.json); const service = resolveNodeService(); const [loaded, command, runtime] = await Promise.all([ service.isLoaded({ env: process.env }).catch(() => false), service.readCommand(process.env).catch(() => null), service.readRuntime(process.env).catch((err) => ({ status: "unknown", detail: String(err) })) ]); const payload = { service: { ...buildDaemonServiceSnapshot(service, loaded), command, runtime } }; if (json) { defaultRuntime.log(JSON.stringify(payload, null, 2)); return; } const rich = isRich(); const label = (value) => colorize(rich, theme.muted, value); const accent = (value) => colorize(rich, theme.accent, value); const infoText = (value) => colorize(rich, theme.info, value); const okText = (value) => colorize(rich, theme.success, value); const warnText = (value) => colorize(rich, theme.warn, value); const errorText = (value) => colorize(rich, theme.error, value); const serviceStatus = loaded ? okText(service.loadedText) : warnText(service.notLoadedText); defaultRuntime.log(`${label("Service:")} ${accent(service.label)} (${serviceStatus})`); if (command?.programArguments?.length) defaultRuntime.log(`${label("Command:")} ${infoText(command.programArguments.join(" "))}`); if (command?.sourcePath) defaultRuntime.log(`${label("Service file:")} ${infoText(command.sourcePath)}`); if (command?.workingDirectory) defaultRuntime.log(`${label("Working dir:")} ${infoText(command.workingDirectory)}`); const runtimeLine = formatRuntimeStatus(runtime); if (runtimeLine) { const runtimeStatus = runtime?.status ?? "unknown"; const runtimeColor = runtimeStatus === "running" ? theme.success : runtimeStatus === "stopped" ? theme.error : runtimeStatus === "unknown" ? theme.muted : theme.warn; defaultRuntime.log(`${label("Runtime:")} ${colorize(rich, runtimeColor, runtimeLine)}`); } if (!loaded) { defaultRuntime.log(""); for (const hint of renderNodeServiceStartHints()) defaultRuntime.log(`${warnText("Start with:")} ${infoText(hint)}`); return; } const baseEnv = { ...process.env, ...command?.environment ?? void 0 }; const hintEnv = { ...baseEnv, OPENCLAW_LOG_PREFIX: baseEnv.OPENCLAW_LOG_PREFIX ?? "node" }; if (runtime?.missingUnit) { defaultRuntime.error(errorText("Service unit not found.")); for (const hint of buildNodeRuntimeHints(hintEnv)) defaultRuntime.error(errorText(hint)); return; } if (runtime?.status === "stopped") { defaultRuntime.error(errorText("Service is loaded but not running.")); for (const hint of buildNodeRuntimeHints(hintEnv)) defaultRuntime.error(errorText(hint)); } } //#endregion //#region src/cli/node-cli/register.ts function parsePortWithFallback(value, fallback) { return parsePort(value) ?? fallback; } function registerNodeCli(program) { const node = program.command("node").description("Run a headless node host (system.run/system.which)").addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/node", "docs.openclaw.ai/cli/node")}\n`); node.command("run").description("Run the headless node host (foreground)").option("--host <host>", "Gateway host").option("--port <port>", "Gateway port").option("--tls", "Use TLS for the gateway connection", false).option("--tls-fingerprint <sha256>", "Expected TLS certificate fingerprint (sha256)").option("--node-id <id>", "Override node id (clears pairing token)").option("--display-name <name>", "Override node display name").action(async (opts) => { const existing = await loadNodeHostConfig(); await runNodeHost({ gatewayHost: opts.host?.trim() || existing?.gateway?.host || "127.0.0.1", gatewayPort: parsePortWithFallback(opts.port, existing?.gateway?.port ?? 18789), gatewayTls: Boolean(opts.tls) || Boolean(opts.tlsFingerprint), gatewayTlsFingerprint: opts.tlsFingerprint, nodeId: opts.nodeId, displayName: opts.displayName }); }); node.command("status").description("Show node host status").option("--json", "Output JSON", false).action(async (opts) => { await runNodeDaemonStatus(opts); }); node.command("install").description("Install the node host service (launchd/systemd/schtasks)").option("--host <host>", "Gateway host").option("--port <port>", "Gateway port").option("--tls", "Use TLS for the gateway connection", false).option("--tls-fingerprint <sha256>", "Expected TLS certificate fingerprint (sha256)").option("--node-id <id>", "Override node id (clears pairing token)").option("--display-name <name>", "Override node display name").option("--runtime <runtime>", "Service runtime (node|bun). Default: node").option("--force", "Reinstall/overwrite if already installed", false).option("--json", "Output JSON", false).action(async (opts) => { await runNodeDaemonInstall(opts); }); node.command("uninstall").description("Uninstall the node host service (launchd/systemd/schtasks)").option("--json", "Output JSON", false).action(async (opts) => { await runNodeDaemonUninstall(opts); }); node.command("stop").description("Stop the node host service (launchd/systemd/schtasks)").option("--json", "Output JSON", false).action(async (opts) => { await runNodeDaemonStop(opts); }); node.command("restart").description("Restart the node host service (launchd/systemd/schtasks)").option("--json", "Output JSON", false).action(async (opts) => { await runNodeDaemonRestart(opts); }); } //#endregion export { registerNodeCli };