openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
336 lines (335 loc) • 13.6 kB
JavaScript
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 };