UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

322 lines (321 loc) • 15.9 kB
import { u as toErrorObject } from "./error-coercion-CvJK4lmH.mjs"; import { r as isPathInside } from "./path-guards-CH7noY0Z.mjs"; import { i as createAbortError } from "./src-D1BHGNTj.mjs"; import "./abort-signal-QqAmLWTl.mjs"; import "./errors-O33D01XW.mjs"; import { i as isPlainCommandExitFailure } from "./exec-result-s7WOth0N.mjs"; import "./exec-CMAKGVCq.mjs"; import { o as spawnCommand } from "./exec-spawn-IxJJNQFo.mjs"; import { a as splitSandboxBindSpec, i as resolveSandboxHostPathViaExistingAncestor } from "./host-paths-C5NtLpi_.mjs"; import { x as SANDBOX_COMMAND_MAX_BUFFER_BYTES } from "./constants-sQ6GWokQ.mjs"; import { t as hashTextSha256 } from "./hash-DV5OH4uO.mjs"; import path from "node:path"; import os from "node:os"; import { isIP } from "node:net"; //#region src/agents/sandbox/container-engine.ts /** * Shared local container-engine process execution and backend selection. */ const DOCKER_SANDBOX_ENGINE = { id: "docker", command: "docker", displayName: "Docker" }; const PODMAN_SANDBOX_ENGINE = { id: "podman", command: "podman", displayName: "Podman" }; function missingContainerEngineMessage(engine) { if (engine.id === "docker") return "Sandbox mode requires Docker, but the \"docker\" command was not found in PATH. Install Docker (and ensure \"docker\" is available), or set `agents.defaults.sandbox.mode=off` to disable sandboxing."; return "Sandbox mode requires Podman, but the \"podman\" command was not found in PATH. Install Podman (and ensure \"podman\" is available), choose another sandbox backend, or set `agents.defaults.sandbox.mode=off` to disable sandboxing."; } async function execContainerRaw(engine, args, opts) { let result; try { result = await spawnCommand([ engine.command, ...engine.globalArgs ?? [], ...args ], { cancelSignal: opts?.signal, encoding: "buffer", input: opts?.input ?? Buffer.alloc(0), maxBuffer: SANDBOX_COMMAND_MAX_BUFFER_BYTES, reject: false, stripFinalNewline: false }); } catch (error) { if (opts?.signal?.aborted) throw createAbortError("Aborted"); if (error.code === "ENOENT") throw Object.assign(new Error(missingContainerEngineMessage(engine)), { code: "INVALID_CONFIG", cause: error }); throw error; } if (opts?.signal?.aborted || result.isCanceled) throw createAbortError("Aborted"); if (result.failed && !isPlainCommandExitFailure(result)) { if (result.code === "ENOENT") throw Object.assign(new Error(missingContainerEngineMessage(engine)), { code: "INVALID_CONFIG", cause: result }); throw toErrorObject(result, `${engine.displayName} command execution failed`); } const stdout = Buffer.from(result.stdout); const stderr = Buffer.from(result.stderr); const exitCode = result.exitCode ?? (result.failed ? 1 : 0); if (exitCode !== 0 && !opts?.allowFailure) { let message = stderr.length > 0 ? stderr.toString("utf8").trim() : ""; if (engine.id === "podman" && args[0] === "create" && /^(?:Error: )?(?:lookup init binary|container-init binary not found on the host):/mu.test(message)) message += "\nInstall catatonit on the Podman engine host, or repair its configured init_path/helper_binaries_dir in containers.conf, then retry. The init executable must be available to the engine, not only inside the sandbox image. Keep --init and sandboxing enabled so orphaned processes are reaped."; throw Object.assign(new Error(message || `${engine.displayName} command failed (exit ${exitCode})`), { code: exitCode, stdout, stderr }); } return { stdout, stderr, code: exitCode }; } async function execContainer(engine, args, opts) { const result = await execContainerRaw(engine, args, opts); return { stdout: result.stdout.toString("utf8"), stderr: result.stderr.toString("utf8"), code: result.code }; } //#endregion //#region src/agents/sandbox/podman-runtime.ts const SANDBOX_ENGINE_PROBE_TIMEOUT_MS = 5e3; const PODMAN_INIT_PATH = "/run/podman-init"; const PODMAN_KEEP_ID_MAPPING_MIN_VERSION = [4, 3]; const PODMAN_GPUS_MIN_VERSION = [5, 0]; function hashPodmanTarget(kind, ...parts) { return `${kind}:${hashTextSha256(parts.join("\0")).slice(0, 32)}`; } function invalidPodmanConfig(message) { return Object.assign(new Error(message), { code: "INVALID_CONFIG" }); } function resolvePodmanKeepIdMode(user) { const normalized = user?.trim(); if (!normalized) return "keep-id"; const match = /^(\d+)(?::(\d+))?$/u.exec(normalized); if (!match) throw invalidPodmanConfig(`Rootless Podman sandbox user "${normalized}" must be a numeric UID or UID:GID so keep-id can preserve bind-mount ownership.`); const uid = match[1] ?? ""; const gid = match[2]; const normalizedUid = BigInt(uid).toString(); const normalizedGid = gid === void 0 ? void 0 : BigInt(gid).toString(); if (normalizedUid === "0" || normalizedGid === "0") throw invalidPodmanConfig(`Rootless Podman sandbox user "${normalized}" cannot use UID or GID 0 while preserving workspace bind ownership. Bake root-required setup into the image or use rootful Podman.`); return normalizedGid ? `keep-id:uid=${normalizedUid},gid=${normalizedGid}` : `keep-id:uid=${normalizedUid}`; } function assertPodmanVersionAtLeast(version, minimum, feature) { const match = /^(\d+)\.(\d+)/u.exec(version.trim()); const actualMajor = match ? Number(match[1]) : NaN; const actualMinor = match ? Number(match[2]) : NaN; if (actualMajor > minimum[0] || actualMajor === minimum[0] && actualMinor >= minimum[1]) return; throw invalidPodmanConfig(`${feature} requires Podman ${minimum.join(".")} or newer, but the active engine reports "${version || "unknown"}". Upgrade Podman or choose another sandbox backend.`); } async function isPodmanMachineConnection(params) { let uri; try { uri = new URL(params.uri); } catch { return false; } const hostname = uri.hostname.replace(/^\[|\]$/gu, ""); const loopback = isIP(hostname) === 4 && hostname.startsWith("127.") || isIP(hostname) === 6 && hostname === "::1"; if (uri.protocol !== "ssh:" || !loopback || !uri.port || !uri.username) return false; const result = await execContainer(PODMAN_SANDBOX_ENGINE, [ "machine", "list", "--format", "json" ], { allowFailure: true, signal: AbortSignal.timeout(SANDBOX_ENGINE_PROBE_TIMEOUT_MS) }); if (result.code !== 0) return false; let parsed; try { parsed = JSON.parse(result.stdout); } catch { return false; } if (!Array.isArray(parsed)) return false; const selectedIdentity = params.identity ? path.resolve(params.identity) : ""; return parsed.some((entry) => { if (typeof entry !== "object" || entry === null) return false; const machine = entry; const machineName = typeof machine.Name === "string" ? machine.Name : ""; const nameMatches = !params.selectedName || params.selectedName === machineName || params.selectedName === `${machineName}-root`; const portMatches = (typeof machine.Port === "string" || typeof machine.Port === "number" ? String(machine.Port) : "") === uri.port; const connectionUser = decodeURIComponent(uri.username); const rootConnection = params.selectedName === `${machineName}-root`; const userMatches = typeof machine.RemoteUsername === "string" && (connectionUser === machine.RemoteUsername || rootConnection && connectionUser === "root"); const machineIdentity = typeof machine.IdentityPath === "string" && machine.IdentityPath ? path.resolve(machine.IdentityPath) : ""; const identityMatches = !selectedIdentity || !machineIdentity || selectedIdentity === machineIdentity; return machine.Running === true && nameMatches && portMatches && userMatches && identityMatches; }); } async function assertSupportedPodmanConnection(remoteSocketPath) { const result = await execContainer(PODMAN_SANDBOX_ENGINE, [ "system", "connection", "list", "--format", "json" ], { allowFailure: true, signal: AbortSignal.timeout(SANDBOX_ENGINE_PROBE_TIMEOUT_MS) }); if (result.code !== 0) { const detail = result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`; throw new Error(`Failed to inspect the active Podman connection: ${detail}`); } let parsed; try { parsed = JSON.parse(result.stdout); } catch (error) { throw new Error("Podman returned invalid connection metadata", { cause: error }); } const connections = Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "object" && entry !== null) : []; const configuredUri = process.env.CONTAINER_HOST; const configuredName = process.env.CONTAINER_CONNECTION; let useConfiguredHost = Boolean(configuredUri); if (configuredUri !== void 0 && configuredName) { const client = await execContainer(PODMAN_SANDBOX_ENGINE, ["--version"], { allowFailure: true, signal: AbortSignal.timeout(SANDBOX_ENGINE_PROBE_TIMEOUT_MS) }); const version = /^podman(?:-remote)?(?:\.exe)? version (\d+)\.(\d+)\.\d+(?:[-+]\S+)?$/u.exec(client.stdout.trim()); if (client.code !== 0 || !version) throw invalidPodmanConfig("Cannot determine Podman client connection precedence. Unset either CONTAINER_HOST or CONTAINER_CONNECTION, or repair `podman --version`, before using Podman sandboxing."); const major = Number(version[1]); useConfiguredHost = major < 4 || major === 4 && Number(version[2]) < 8; } let selected; if (useConfiguredHost) selected = connections.find((entry) => entry.URI === configuredUri); else if (configuredName) selected = connections.find((entry) => entry.Name === configuredName); else selected = connections.find((entry) => entry.Default === true); const selectedUri = (useConfiguredHost ? configuredUri : typeof selected?.URI === "string" ? selected.URI : "") || (remoteSocketPath ? `unix://${remoteSocketPath}` : ""); const unsupportedRemoteError = () => invalidPodmanConfig("Podman sandboxing supports a local Podman engine or Podman Machine, but the active Podman connection is remote or could not be identified. Use the SSH sandbox backend for a remote host."); if (!useConfiguredHost && configuredName && !selected) throw unsupportedRemoteError(); if (!selectedUri) throw unsupportedRemoteError(); if (selectedUri && !selectedUri.startsWith("unix://")) { const identity = useConfiguredHost ? process.env.CONTAINER_SSHKEY || "" : typeof selected?.Identity === "string" ? selected.Identity : ""; if (await isPodmanMachineConnection({ selectedName: typeof selected?.Name === "string" ? selected.Name : "", uri: selectedUri, identity })) return { machine: true, target: { key: hashPodmanTarget("machine", selectedUri, identity), globalArgs: [ "--url", selectedUri, ...identity ? ["--identity", identity] : [] ] } }; throw unsupportedRemoteError(); } return { machine: false, target: { key: hashPodmanTarget("socket", selectedUri), globalArgs: ["--url", selectedUri] } }; } async function resolvePodmanSandboxRuntimeInfo() { const result = await execContainer(PODMAN_SANDBOX_ENGINE, [ "info", "--format", "{{.Host.Security.Rootless}} {{.Host.ServiceIsRemote}} {{.Host.RemoteSocket.Path}} {{.Version.Version}}" ], { allowFailure: true, signal: AbortSignal.timeout(SANDBOX_ENGINE_PROBE_TIMEOUT_MS) }); if (result.code !== 0) { const detail = result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`; throw new Error(`Failed to inspect Podman user namespace mode: ${detail}`); } const [rootless = "", serviceIsRemote = "", remoteSocketPath = "", version = ""] = result.stdout.trim().split(" ", 4); let machine = false; let target = { key: "local", globalArgs: [] }; if (serviceIsRemote === "true") ({machine, target} = await assertSupportedPodmanConnection(remoteSocketPath)); return { machine, rootless: rootless === "true", target, version }; } async function validateSandboxContainerEngineTarget(engine, expectedTarget) { if (engine.id === "podman") assertPodmanSandboxTarget(expectedTarget, (await resolvePodmanSandboxRuntimeInfo()).target); } function assertPodmanSandboxTarget(expectedTarget, actualTarget) { if (expectedTarget && (actualTarget.key !== expectedTarget.key || actualTarget.globalArgs.length !== expectedTarget.globalArgs.length || actualTarget.globalArgs.some((arg, index) => arg !== expectedTarget.globalArgs[index]))) throw invalidPodmanConfig("The active Podman connection changed after this sandbox runtime was created. Restore the original Podman target before inspecting, executing, or removing the runtime."); } function bindPodmanSandboxEngine(target) { return { ...PODMAN_SANDBOX_ENGINE, globalArgs: target.globalArgs }; } function mountTargetCoversPodmanInit(target) { const normalizedTarget = path.posix.normalize(target); return normalizedTarget === "/" || normalizedTarget === PODMAN_INIT_PATH || PODMAN_INIT_PATH.startsWith(`${normalizedTarget}/`) || normalizedTarget.startsWith(`${PODMAN_INIT_PATH}/`); } function assertPodmanMachineBindSourcesSupported(params) { const hostHome = resolveSandboxHostPathViaExistingAncestor(path.resolve(os.homedir())); const sources = /* @__PURE__ */ new Set([params.workspaceDir]); if (params.workspaceAccess !== "none" && params.workspaceDir !== params.agentWorkspaceDir) sources.add(params.agentWorkspaceDir); for (const mount of params.readOnlyWorkspaceSkillMounts) sources.add(mount.hostPath); for (const bind of params.cfg.binds ?? []) { const source = splitSandboxBindSpec(bind)?.host; if (source) sources.add(source); } for (const source of sources) { const canonicalSource = resolveSandboxHostPathViaExistingAncestor(path.resolve(source)); if (isPathInside(hostHome, canonicalSource)) continue; throw invalidPodmanConfig(`Podman Machine sandbox bind source "${source}" is outside the default host home share "${os.homedir()}". Move the workspace or bind under the host home directory, or use Docker or the SSH sandbox backend.`); } } function resolvePodmanSandboxCreatePolicy(params) { const cfg = params.dockerTmpfsSource === "default" ? { ...params.cfg, tmpfs: params.cfg.tmpfs.filter((entry) => entry !== "/run") } : params.cfg; if (mountTargetCoversPodmanInit(params.cfg.workdir) || cfg.tmpfs.some((entry) => mountTargetCoversPodmanInit(entry.split(":", 1)[0] || "")) || params.cfg.binds?.some((bind) => { const target = splitSandboxBindSpec(bind)?.container; return target ? mountTargetCoversPodmanInit(target) : false; }) === true) throw invalidPodmanConfig("Podman sandbox configuration would cover Podman's init path at /run/podman-init. Remove the conflicting tmpfs or bind mount so orphaned sandbox processes can be reaped."); if (params.runtimeInfo.machine) assertPodmanMachineBindSourcesSupported(params); if (params.cfg.gpus?.trim()) assertPodmanVersionAtLeast(params.runtimeInfo.version, PODMAN_GPUS_MIN_VERSION, "Podman sandbox GPU passthrough"); const extraCreateArgs = ["--http-proxy=false"]; if (params.cfg.readOnlyRoot) extraCreateArgs.push("--read-only-tmpfs=true"); if (params.runtimeInfo.rootless) { if (params.cfg.user?.trim()) assertPodmanVersionAtLeast(params.runtimeInfo.version, PODMAN_KEEP_ID_MAPPING_MIN_VERSION, "Rootless Podman sandbox user mapping"); extraCreateArgs.push("--userns", resolvePodmanKeepIdMode(params.cfg.user)); } return { cfg, extraCreateArgs }; } function resolvePodmanSandboxConfigHash(params) { const userMode = params.configuredUser ? "configured-user" : "keep-id"; return `${params.genericConfigHash}:podman-runtime-v9:${userMode}:${params.dockerTmpfsSource}`; } function resolvePodmanSandboxContainerPrefix(containerPrefix) { return `${containerPrefix}podman-`; } //#endregion export { resolvePodmanSandboxCreatePolicy as a, DOCKER_SANDBOX_ENGINE as c, execContainerRaw as d, resolvePodmanSandboxContainerPrefix as i, PODMAN_SANDBOX_ENGINE as l, bindPodmanSandboxEngine as n, resolvePodmanSandboxRuntimeInfo as o, resolvePodmanSandboxConfigHash as r, validateSandboxContainerEngineTarget as s, assertPodmanSandboxTarget as t, execContainer as u };