openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
292 lines (291 loc) • 11.7 kB
JavaScript
import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js";
import { g as shortenHomePath } from "./utils-CCC-BEJH.js";
import { i as runExec, r as runCommandWithTimeout } from "./exec-DubsSJS2.js";
import { d as DEFAULT_SANDBOX_IMAGE, r as DEFAULT_SANDBOX_BROWSER_IMAGE } from "./constants-CsuK1VR-.js";
import { s as resolveSandboxScope } from "./config-zzXWNRgc.js";
import { D as migrateLegacySandboxRegistryFiles, E as inspectLegacySandboxRegistryFiles, c as isDockerDaemonUnavailable } from "./docker-Z5I59fn0.js";
import "./sandbox-BQ4p-0vf.js";
import { t as note } from "./note-BRSJp0UF.js";
import fs from "node:fs";
import path from "node:path";
//#region src/commands/doctor-sandbox.ts
/** Doctor checks and repairs for Docker sandbox images, namespaces, and registry state. */
function resolveSandboxScript(scriptRel) {
const candidates = /* @__PURE__ */ new Set();
candidates.add(process.cwd());
const argv1 = process.argv[1];
if (argv1) {
const normalized = path.resolve(argv1);
candidates.add(path.resolve(path.dirname(normalized), ".."));
candidates.add(path.resolve(path.dirname(normalized)));
}
for (const root of candidates) {
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 false;
}
runtime.log(`Running ${scriptRel}...`);
const result = await runCommandWithTimeout(["bash", script.scriptPath], {
timeoutMs: 1200 * 1e3,
cwd: script.cwd
});
if (result.code !== 0) {
runtime.error(`Failed running ${scriptRel}: ${result.stderr.trim() || result.stdout.trim() || "unknown error"}`);
return false;
}
runtime.log(`Completed ${scriptRel}.`);
return true;
}
async function isDockerAvailable() {
try {
await runExec("docker", [
"version",
"--format",
"{{.Server.Version}}"
], { 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) {
if (process.platform !== "linux") return { ok: true };
const userProbe = await runCodexBwrapNamespaceProbe("user", [
"--user",
"--map-root-user",
"true"
]);
if (!userProbe.ok || !codexBwrapNeedsNetworkNamespaceProbe(cfg)) return userProbe;
return await runCodexBwrapNamespaceProbe("network", [
"--user",
"--map-root-user",
"--net",
"true"
]);
}
async function noteCodexBwrapNamespaceWarning(cfg) {
const probe = await probeCodexBwrapNamespaces(cfg);
if (probe.ok) 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 Docker sandbox network egress disabled, it also needs an unprivileged network namespace." : "Docker sandbox network egress is enabled, so doctor only checked the user namespace.";
note([
`Codex bwrap ${probe.kind} namespace probe failed while Docker 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,
`Probe command: ${probe.command}`,
`Probe 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."
].join("\n"), "Sandbox");
}
async function dockerImageExists(image) {
try {
await runExec("docker", [
"image",
"inspect",
image
], { timeoutMs: 5e3 });
return true;
} catch (error) {
const stderr = error?.stderr || error?.message || "";
if (stderr.includes("No such image")) return false;
if (isDockerDaemonUnavailable(stderr)) return false;
throw error;
}
}
function resolveSandboxDockerImage(cfg) {
const image = cfg.agents?.defaults?.sandbox?.docker?.image?.trim();
return image ? image : DEFAULT_SANDBOX_IMAGE;
}
function resolveSandboxBackend(cfg) {
return cfg.agents?.defaults?.sandbox?.backend?.trim() || "docker";
}
function resolveSandboxBrowserImage(cfg) {
const image = cfg.agents?.defaults?.sandbox?.browser?.image?.trim();
return image ? image : DEFAULT_SANDBOX_BROWSER_IMAGE;
}
function updateSandboxDockerImage(cfg, image) {
return {
...cfg,
agents: {
...cfg.agents,
defaults: {
...cfg.agents?.defaults,
sandbox: {
...cfg.agents?.defaults?.sandbox,
docker: {
...cfg.agents?.defaults?.sandbox?.docker,
image
}
}
}
}
};
}
function updateSandboxBrowserImage(cfg, image) {
return {
...cfg,
agents: {
...cfg.agents,
defaults: {
...cfg.agents?.defaults,
sandbox: {
...cfg.agents?.defaults?.sandbox,
browser: {
...cfg.agents?.defaults?.sandbox?.browser,
image
}
}
}
}
};
}
async function handleMissingSandboxImage(params, runtime, prompter) {
if (await dockerImageExists(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-Docker backends skip Docker image checks; Docker mode also probes Codex bwrap namespace
* support because nested app-server shells rely on host user/network namespace policy.
*/
async function maybeRepairSandboxImages(cfg, runtime, prompter) {
const sandbox = cfg.agents?.defaults?.sandbox;
const mode = sandbox?.mode ?? "off";
if (!sandbox || mode === "off") return cfg;
const backend = resolveSandboxBackend(cfg);
if (backend !== "docker") {
if (sandbox.browser?.enabled) note(`Sandbox backend "${backend}" selected. Docker browser health checks are skipped; browser sandbox currently requires the docker backend.`, "Sandbox");
return cfg;
}
if (!await isDockerAvailable()) {
note([
`Sandbox mode is enabled (mode: "${mode}") but Docker is not available.`,
"Docker is required for sandbox mode to function.",
"Isolated sessions (cron jobs, sub-agents) will fail without Docker.",
"",
"Options:",
"- Install Docker and restart the gateway",
"- Disable sandbox mode: openclaw config set agents.defaults.sandbox.mode off"
].join("\n"), "Sandbox");
return cfg;
}
await noteCodexBwrapNamespaceWarning(cfg);
let next = cfg;
const changes = [];
const dockerImage = resolveSandboxDockerImage(cfg);
await handleMissingSandboxImage({
kind: "base",
image: dockerImage,
buildScript: dockerImage === "openclaw-sandbox-common:bookworm-slim" ? "scripts/sandbox-common-setup.sh" : dockerImage === "openclaw-sandbox:bookworm-slim" ? "scripts/sandbox-setup.sh" : void 0,
updateConfig: (image) => {
next = updateSandboxDockerImage(next, image);
changes.push(`Updated agents.defaults.sandbox.docker.image → ${image}`);
}
}, runtime, prompter);
if (sandbox.browser?.enabled) await handleMissingSandboxImage({
kind: "browser",
image: resolveSandboxBrowserImage(cfg),
buildScript: "scripts/sandbox-browser-setup.sh",
updateConfig: (image) => {
next = updateSandboxBrowserImage(next, image);
changes.push(`Updated agents.defaults.sandbox.browser.image → ${image}`);
}
}, runtime, prompter);
if (changes.length > 0) note(changes.join("\n"), "Doctor changes");
return next;
}
function formatLegacyRegistryInspectionLine(file) {
const status = file.valid ? `${file.entries} entr${file.entries === 1 ? "y" : "ies"}` : "invalid";
const sourcePath = file.source === "sharded" ? file.shardedDir : file.registryPath;
return `- ${file.kind} ${file.source}: ${shortenHomePath(sourcePath)} (${status})`;
}
function formatLegacyRegistryMigrationLine(result) {
if (result.status === "migrated") return `- Migrated ${result.kind} registry into ${result.entries} SQLite row${result.entries === 1 ? "" : "s"}.`;
if (result.status === "removed-empty") return `- Removed empty legacy ${result.kind} registry files.`;
if (result.status === "quarantined-invalid") {
const file = shortenHomePath(result.source === "sharded" ? result.shardedDir : result.registryPath);
const quarantine = result.quarantinePath ? ` to ${shortenHomePath(result.quarantinePath)}` : "";
return `- Quarantined invalid legacy ${result.kind} registry ${file}${quarantine}.`;
}
return "";
}
/** Migrates legacy sandbox registry files and directories into SQLite. */
async function maybeRepairSandboxRegistryFiles(prompter) {
const legacyFiles = (await inspectLegacySandboxRegistryFiles()).filter((file) => file.exists);
if (legacyFiles.length === 0) return;
if (!prompter.shouldRepair) {
note([
"Legacy sandbox registry files detected.",
...legacyFiles.map(formatLegacyRegistryInspectionLine),
`Run ${formatCliCommand("openclaw doctor --fix")} to migrate them to SQLite.`
].join("\n"), "Sandbox");
return;
}
const results = (await migrateLegacySandboxRegistryFiles()).filter((result) => result.status !== "missing").map(formatLegacyRegistryMigrationLine).filter((line) => line.length > 0);
if (results.length > 0) note(results.join("\n"), "Doctor changes");
}
/** Warns when agent sandbox overrides are ignored because sandbox scope resolves to shared. */
function noteSandboxScopeWarnings(cfg) {
const globalSandbox = cfg.agents?.defaults?.sandbox;
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
const warnings = [];
for (const agent of agents) {
const agentId = agent.id;
const agentSandbox = agent.sandbox;
if (!agentSandbox) continue;
if (resolveSandboxScope({ scope: agentSandbox.scope ?? globalSandbox?.scope }) !== "shared") continue;
const overrides = [];
if (agentSandbox.docker && Object.keys(agentSandbox.docker).length > 0) overrides.push("docker");
if (agentSandbox.browser && Object.keys(agentSandbox.browser).length > 0) overrides.push("browser");
if (agentSandbox.prune && Object.keys(agentSandbox.prune).length > 0) overrides.push("prune");
if (overrides.length === 0) continue;
warnings.push([`- agents.list (id "${agentId}") sandbox ${overrides.join("/")} overrides ignored.`, ` scope resolves to "shared".`].join("\n"));
}
if (warnings.length > 0) note(warnings.join("\n"), "Sandbox");
}
//#endregion
export { maybeRepairSandboxImages, maybeRepairSandboxRegistryFiles, noteSandboxScopeWarnings };