UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

208 lines (207 loc) • 6.23 kB
import { h as resolveTimerTimeoutMs, l as parseStrictNonNegativeInteger } from "./number-coercion-PJEgwXYS.mjs"; import { i as sanitizeHostExecEnv, o as isTruthyEnvValue } from "./host-env-security-B3DKP9_D.mjs"; import { t as formatErrorMessage } from "./errors-Qhjprk1B.mjs"; import { t as LruCache } from "./lru-cache-C3Xgvc4Q.mjs"; import "./executable-path-DP5t-qND.mjs"; import { execFileSync } from "node:child_process"; import fsSync from "node:fs"; import path from "node:path"; import os from "node:os"; //#region src/infra/shell-env.ts const DEFAULT_TIMEOUT_MS = 15e3; const DEFAULT_MAX_BUFFER_BYTES = 2097152; const DEFAULT_SHELL = "/bin/sh"; const LOGIN_SHELL_ENV_COMMAND = "printf '\\0'; env -0"; let cachedEtcShells; let nextExecCacheId = 1; const loginShellEnvProbeCache = new LruCache(64); const execCacheIds = /* @__PURE__ */ new WeakMap(); function resolveShellExecEnv(env) { const execEnv = sanitizeHostExecEnv({ baseEnv: env }); const home = os.homedir().trim(); if (home) execEnv.HOME = home; else delete execEnv.HOME; delete execEnv.ZDOTDIR; return execEnv; } function resolveTimeoutMs(timeoutMs) { return resolveTimerTimeoutMs(timeoutMs, DEFAULT_TIMEOUT_MS, 0); } function readEtcShells() { if (cachedEtcShells !== void 0) return cachedEtcShells; try { const entries = fsSync.readFileSync("/etc/shells", "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && path.isAbsolute(line)); cachedEtcShells = new Set(entries); } catch { cachedEtcShells = null; } return cachedEtcShells; } function isTrustedShellPath(shell) { if (!path.isAbsolute(shell)) return false; if (path.normalize(shell) !== shell) return false; return readEtcShells()?.has(shell) === true; } function resolveShell(env) { const shell = env.SHELL?.trim(); if (shell && isTrustedShellPath(shell)) return shell; return DEFAULT_SHELL; } function createLoginShellExecSpec(params) { const args = params.purpose === "environment-import" && path.basename(params.shell) === "bash" ? ["-lic", LOGIN_SHELL_ENV_COMMAND] : [ "-l", "-c", LOGIN_SHELL_ENV_COMMAND ]; const options = { encoding: "buffer", timeout: params.timeoutMs, maxBuffer: DEFAULT_MAX_BUFFER_BYTES, env: params.env, windowsHide: true, stdio: [ "ignore", "pipe", "pipe" ], detached: true }; return { shell: params.shell, args, options }; } function parseShellEnv(stdout) { const shellEnv = /* @__PURE__ */ new Map(); const frameEnd = stdout.indexOf(0); if (frameEnd < 0) return shellEnv; const parts = stdout.subarray(frameEnd + 1).toString("utf8").split("\0"); for (const part of parts) { const eq = part.indexOf("="); if (eq <= 0) continue; shellEnv.set(part.slice(0, eq), part.slice(eq + 1)); } return shellEnv; } function resolveExecCacheId(exec) { if (!exec) return "default"; let id = execCacheIds.get(exec); if (!id) { id = nextExecCacheId; nextExecCacheId += 1; execCacheIds.set(exec, id); } return `exec:${id}`; } function createLoginShellEnvCacheKey(params) { const startupEnvEntries = Object.entries(params.execEnv).filter(([key]) => { if (key === "HOME" || key === "PATH" || key === "TERM" || key === "LANG" || key === "LC_ALL" || key === "LC_CTYPE" || key === "USER" || key === "LOGNAME" || key === "TMPDIR") return true; return key.startsWith("XDG_") || key.startsWith("OPENCLAW_"); }).toSorted(([left], [right]) => left.localeCompare(right)); return JSON.stringify([ params.shell, params.timeoutMs, params.purpose, resolveExecCacheId(params.exec), startupEnvEntries ]); } function prepareLoginShellEnvProbe(params, exec) { const request = { timeoutMs: resolveTimeoutMs(params.timeoutMs), shell: resolveShell(params.env), env: resolveShellExecEnv(params.env), purpose: params.purpose }; return { request, cacheKey: createLoginShellEnvCacheKey({ ...request, exec, execEnv: request.env }) }; } function probeLoginShellEnv(params) { if ((params.platform ?? process.platform) === "win32") return { ok: true, shellEnv: /* @__PURE__ */ new Map() }; const exec = params.exec ?? execFileSync; const { request, cacheKey } = prepareLoginShellEnvProbe(params, params.exec); const cached = loginShellEnvProbeCache.get(cacheKey); if (cached) return { ok: true, shellEnv: new Map(cached) }; try { const spec = createLoginShellExecSpec(request); const shellEnv = parseShellEnv(exec(spec.shell, spec.args, spec.options)); loginShellEnvProbeCache.set(cacheKey, [...shellEnv.entries()]); return { ok: true, shellEnv }; } catch (err) { return { ok: false, error: formatErrorMessage(err) }; } } function loadShellEnvFallback(opts) { const logger = opts.logger ?? console; if (!opts.enabled) return { ok: true, applied: [], skippedReason: "disabled" }; const missingExpectedKeys = opts.expectedKeys.filter((key) => !Object.hasOwn(opts.env, key)); if (missingExpectedKeys.length === 0) return { ok: true, applied: [], skippedReason: "already-has-keys" }; const probe = probeLoginShellEnv({ env: opts.env, timeoutMs: opts.timeoutMs, exec: opts.exec, platform: opts.platform, purpose: "environment-import" }); if (!probe.ok) { logger.warn(`[openclaw] shell env fallback failed: ${probe.error}`); return { ok: false, error: probe.error, applied: [] }; } const applied = []; for (const key of missingExpectedKeys) { const value = probe.shellEnv.get(key); if (!value?.trim()) continue; opts.env[key] = value; applied.push(key); } return { ok: true, applied }; } function shouldEnableShellEnvFallback(env) { return isTruthyEnvValue(env.OPENCLAW_LOAD_SHELL_ENV); } function shouldDeferShellEnvFallback(env) { return isTruthyEnvValue(env.OPENCLAW_DEFER_SHELL_ENV_FALLBACK); } function resolveShellEnvFallbackTimeoutMs(env) { const raw = env.OPENCLAW_SHELL_ENV_TIMEOUT_MS?.trim(); if (!raw) return DEFAULT_TIMEOUT_MS; const parsed = parseStrictNonNegativeInteger(raw); if (parsed === void 0) return DEFAULT_TIMEOUT_MS; return resolveTimeoutMs(parsed); } //#endregion export { loadShellEnvFallback, resolveShellEnvFallbackTimeoutMs, shouldDeferShellEnvFallback, shouldEnableShellEnvFallback };