UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

336 lines (335 loc) • 13.6 kB
import { i as resolveOpenClawPackageRootsSync } from "./openclaw-root-BGdnWXIW.mjs"; import { t as hasErrnoCode } from "./errno-FR4CRyya.mjs"; import { p as shortenHomePath } from "./utils-CitNzLyF.mjs"; import { t as formatCliCommand } from "./command-format-DwEdrDKL.mjs"; import { r as listAgentEntriesWithSource } from "./agent-roster-DH6r_8f4.mjs"; import "./agent-scope-config-BEk6-ThS.mjs"; import { i as runCommandWithTimeout, n as runExec } from "./exec-DdJWnONN.mjs"; import { n as resolveSandboxScope } from "./config-contract-CgxcWtCc.mjs"; import { t as note } from "./note-Bi0VsI6R.mjs"; import { S as SANDBOX_CONTAINERS_DIR, T as SANDBOX_REGISTRY_PATH, _ as SANDBOX_BROWSERS_DIR, y as SANDBOX_BROWSER_REGISTRY_PATH } from "./constants-B3lt5mRy.mjs"; import { c as DOCKER_SANDBOX_ENGINE, l as PODMAN_SANDBOX_ENGINE, s as validateSandboxContainerEngineTarget } from "./podman-runtime-B7hhCHYt.mjs"; import { s as isDockerDaemonUnavailable } from "./docker-DML2JD9P.mjs"; import "./sandbox-GCAKkjF5.mjs"; import { n as assertNoRetiredStateFiles, r as createRetiredStateInspectionError } from "./state-migrations.retired-files-Dlsbs5FH.mjs"; import fs from "node:fs"; import path from "node:path"; //#region src/commands/doctor-sandbox.ts const SANDBOX_REGISTRY_FILES_CHECK_ID = "core/doctor/sandbox/registry-files"; function resolveSandboxScript(scriptRel) { const roots = resolveOpenClawPackageRootsSync({ cwd: process.cwd(), argv1: process.argv[1] }); for (const root of roots) { const scriptPath = path.join(root, scriptRel); if (fs.existsSync(scriptPath)) return { scriptPath, cwd: root }; } return null; } async function runSandboxScript(scriptRel, runtime) { const script = resolveSandboxScript(scriptRel); if (!script) { note(`Unable to locate ${scriptRel}. Run it from the repo root.`, "Sandbox"); return; } runtime.log(`Running ${scriptRel}...`); const result = await runCommandWithTimeout(["bash", script.scriptPath], { timeoutMs: 12e5, cwd: script.cwd }); if (result.code !== 0) { runtime.error(`Failed running ${scriptRel}: ${result.stderr.trim() || result.stdout.trim() || "unknown error"}`); return; } runtime.log(`Completed ${scriptRel}.`); } async function isContainerEngineAvailable(command) { try { await runExec(command, command === "docker" ? [ "version", "--format", "{{.Server.Version}}" ] : ["info"], { timeoutMs: 5e3 }); return true; } catch { return false; } } function formatNamespaceProbeCommand(args) { return ["unshare", ...args].join(" "); } async function runCodexBwrapNamespaceProbe(kind, args) { try { await runExec("unshare", args, { timeoutMs: 5e3 }); return { ok: true }; } catch (error) { const reason = error?.stderr?.trim() || error?.stdout?.trim() || (error instanceof Error ? error.message : String(error)); return { ok: false, kind, command: formatNamespaceProbeCommand(args), reason }; } } function codexBwrapNeedsNetworkNamespaceProbe(cfg) { const network = cfg.agents?.defaults?.sandbox?.docker?.network?.trim().toLowerCase(); return network === void 0 || network === "" || network === "none"; } async function probeCodexBwrapNamespaces(cfg, options) { const userProbe = await runCodexBwrapNamespaceProbe("user", [ "--user", "--map-root-user", "true" ]); if (!userProbe.ok || !codexBwrapNeedsNetworkNamespaceProbe(cfg)) return userProbe; try { const { resolveCodexHealthApi } = await import("./bundled-health-checks-BgVK3QQM.mjs"); const selected = resolveCodexHealthApi({ cfg, ...options }); if (selected.status === "not-configured") return { ok: true }; if (selected.status === "unavailable") return { ok: false, kind: "unverified", reason: selected.reason }; if (typeof selected.api.probeCodexWorkspaceWriteSandbox !== "function") return { ok: false, kind: "unverified", reason: "The selected Codex plugin does not provide a workspace-write sandbox check." }; const probe = await selected.api.probeCodexWorkspaceWriteSandbox({ cfg, env: options.env }); if (probe.status === "ok" || probe.status === "skipped") return { ok: true }; if (probe.status === "inconclusive") return { ok: false, kind: "unverified", ...probe.command ? { command: probe.command } : {}, reason: probe.reason }; return { ok: false, kind: probe.denial.startsWith("bwrap: loopback:") ? "network" : "user", command: probe.command, reason: probe.denial }; } catch (error) { return { ok: false, kind: "unverified", reason: error instanceof Error ? error.message : String(error) }; } } /** Resolves the enabled sandbox backend and its local container engine, when it has one. */ function resolveEnabledSandboxBackend(cfg) { const sandbox = cfg.agents?.defaults?.sandbox; const mode = sandbox?.mode ?? "off"; if (!sandbox || mode === "off") return; const backend = (sandbox.backend?.trim() || "docker").toLowerCase(); return { sandbox, mode, backend, containerEngine: backend === "podman" ? PODMAN_SANDBOX_ENGINE : backend === "docker" ? DOCKER_SANDBOX_ENGINE : void 0 }; } async function noteCodexBwrapNamespaceWarnings(cfg, options = {}) { const containerEngine = resolveEnabledSandboxBackend(cfg)?.containerEngine; if (process.platform !== "linux" || !containerEngine) return; if (!await isContainerEngineAvailable(containerEngine.command)) return; const engineName = containerEngine.displayName; const probe = await probeCodexBwrapNamespaces(cfg, options); if (probe.ok) return; if (probe.kind === "unverified") { note([ "Doctor could not verify the Codex bwrap network sandbox.", ...probe.command ? [`Check command: ${probe.command}`] : [], `Check result: ${probe.reason}`, `Resolve the check result above, then rerun ${formatCliCommand("openclaw doctor")} to check Codex's host namespace policy.` ].join("\n"), "Sandbox"); return; } const symptom = probe.kind === "user" ? " bwrap: setting up uid map: Permission denied" : " bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted"; const networkSentence = codexBwrapNeedsNetworkNamespaceProbe(cfg) ? `With ${engineName} sandbox network egress disabled, it also needs an unprivileged network namespace.` : `${engineName} sandbox network egress is enabled, so doctor only checked the user namespace.`; const lines = [ `Codex bwrap ${probe.kind} namespace check failed while ${engineName} sandbox mode is enabled.`, `Codex app-server \`workspace-write\` shell execution needs unprivileged user namespaces. ${networkSentence}`, "On Ubuntu/AppArmor hosts this usually appears as:", symptom, `Check command: ${probe.command}`, `Check result: ${probe.reason}`, "", "Fix the host namespace policy for the OpenClaw service user, then restart the gateway.", "Prefer an AppArmor profile that grants the required namespaces to the OpenClaw service process.", "`kernel.apparmor_restrict_unprivileged_userns=0` is a host-wide fallback with security tradeoffs; use it only when that host posture is acceptable.", "Do not add broad Docker container privileges just to satisfy nested bwrap; that weakens the outer sandbox." ]; note(lines.join("\n"), "Sandbox"); } async function containerImageExists(command, image) { try { await runExec(command, [ "image", "inspect", image ], { timeoutMs: 5e3 }); return true; } catch (error) { const stderr = error?.stderr || error?.message || ""; if ((command === "docker" ? stderr.includes("No such image") : /No such image|image not known|image .* not found/iu.test(stderr)) || command === "docker" && isDockerDaemonUnavailable(stderr)) return false; throw error; } } async function handleMissingSandboxImage(params, runtime, prompter) { if (await containerImageExists(params.engineCommand, params.image)) return; const buildHint = params.buildScript ? `Build it with ${params.buildScript}.` : "Build or pull it first."; note(`Sandbox ${params.kind} image missing: ${params.image}. ${buildHint}`, "Sandbox"); if (params.buildScript) { if (await prompter.confirmRuntimeRepair({ message: `Build ${params.kind} sandbox image now?`, initialValue: true })) await runSandboxScript(params.buildScript, runtime); } } /** * Checks configured sandbox images and optionally runs repo build scripts for missing defaults. * * Non-container backends skip image checks. */ async function maybeRepairSandboxImages(cfg, runtime, prompter) { const enabled = resolveEnabledSandboxBackend(cfg); if (!enabled) return; const { sandbox, mode, backend, containerEngine } = enabled; if (!containerEngine) { if (sandbox.browser?.enabled) note(`Sandbox backend "${backend}" selected. Docker browser health checks are skipped; browser sandbox currently requires the docker backend.`, "Sandbox"); return; } if (!await isContainerEngineAvailable(containerEngine.command)) { const name = containerEngine.displayName; const lines = [ `Sandbox mode is enabled (mode: "${mode}") but ${name} is not available.`, containerEngine.id === "docker" ? "Docker is required for sandbox mode to function." : "Podman is required by the selected sandbox backend.", `Isolated sessions (automations, sub-agents) will fail without ${name}.`, "", "Options:", `- Install ${name} and restart the gateway`, "- Disable sandbox mode: openclaw config set agents.defaults.sandbox.mode off" ]; note(lines.join("\n"), "Sandbox"); return; } await validateSandboxContainerEngineTarget(containerEngine); const dockerImage = sandbox.docker?.image?.trim() || "openclaw-sandbox:bookworm-slim"; await handleMissingSandboxImage({ engineCommand: containerEngine.command, kind: "base", image: dockerImage, buildScript: containerEngine.id !== "docker" ? void 0 : dockerImage === "openclaw-sandbox-common:bookworm-slim" ? "scripts/sandbox-common-setup.sh" : dockerImage === "openclaw-sandbox:bookworm-slim" ? "scripts/sandbox-setup.sh" : void 0 }, runtime, prompter); if (sandbox.browser?.enabled && containerEngine.id === "docker") await handleMissingSandboxImage({ engineCommand: containerEngine.command, kind: "browser", image: sandbox.browser.image?.trim() || "openclaw-sandbox-browser:bookworm-slim", buildScript: "scripts/sandbox-browser-setup.sh" }, runtime, prompter); else if (sandbox.browser?.enabled) note("Podman sandbox selected. Browser sandbox health checks are skipped because browser sandboxing requires the Docker engine.", "Sandbox"); } function formatLegacyRegistryInspectionLine(file) { return `- ${file.kind} ${file.source}: ${shortenHomePath(file.path)} (retired)`; } function legacySandboxRegistryUpgradeHint() { return `Upgrade through OpenClaw 2026.9.7 and run ${formatCliCommand("openclaw doctor --fix")} on the original host before retrying. The retired files are left unchanged.`; } async function detectLegacySandboxRegistryFileIssues() { return [ { kind: "containers", path: SANDBOX_REGISTRY_PATH, source: "monolithic" }, { kind: "containers", path: SANDBOX_CONTAINERS_DIR, source: "sharded" }, { kind: "browsers", path: SANDBOX_BROWSER_REGISTRY_PATH, source: "monolithic" }, { kind: "browsers", path: SANDBOX_BROWSERS_DIR, source: "sharded" } ].filter((target) => { try { fs.lstatSync(target.path); return true; } catch (error) { if (hasErrnoCode(error, "ENOENT")) return false; throw createRetiredStateInspectionError(target.path, error); } }); } function legacySandboxRegistryInspectionToHealthFinding(file) { return { checkId: SANDBOX_REGISTRY_FILES_CHECK_ID, severity: "warning", message: `Retired sandbox registry file detected. ${formatLegacyRegistryInspectionLine(file)}`, path: file.path, fixHint: legacySandboxRegistryUpgradeHint() }; } function legacySandboxRegistryInspectionToRepairEffect(file) { return { kind: "state", action: "requires-intermediate-sandbox-registry-upgrade", target: file.path, dryRunSafe: false }; } async function maybeRepairSandboxRegistryFiles(prompter) { const legacyFiles = await detectLegacySandboxRegistryFileIssues(); if (legacyFiles.length === 0) return; if (prompter.shouldRepair) { assertNoRetiredStateFiles("Sandbox registries", legacyFiles.map((file) => file.path)); return; } note([ "Retired sandbox registry files detected.", ...legacyFiles.map(formatLegacyRegistryInspectionLine), legacySandboxRegistryUpgradeHint() ].join("\n"), "Sandbox"); } function noteSandboxScopeWarnings(cfg) { const globalSandbox = cfg.agents?.defaults?.sandbox; const warnings = []; for (const { entry: agent, source } of listAgentEntriesWithSource(cfg)) { const agentId = agent.id; const agentSandbox = agent.sandbox; if (!agentSandbox) continue; if (resolveSandboxScope({ scope: agentSandbox.scope ?? globalSandbox?.scope }) !== "shared") continue; const overrides = [ "docker", "browser", "prune" ].filter((key) => agentSandbox[key] && Object.keys(agentSandbox[key]).length > 0); if (overrides.length === 0) continue; const agentPath = source.kind === "entries" ? `agents.entries.${source.key}` : `agents.list (id "${agentId}")`; warnings.push(`- ${agentPath} sandbox ${overrides.join("/")} overrides ignored.\n scope resolves to "shared".`); } if (warnings.length > 0) note(warnings.join("\n"), "Sandbox"); } //#endregion export { detectLegacySandboxRegistryFileIssues, legacySandboxRegistryInspectionToHealthFinding, legacySandboxRegistryInspectionToRepairEffect, maybeRepairSandboxImages, maybeRepairSandboxRegistryFiles, noteCodexBwrapNamespaceWarnings, noteSandboxScopeWarnings };