openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
484 lines (483 loc) • 20.6 kB
JavaScript
import { c as normalizeOptionalLowercaseString } from "./string-coerce-1VuiLaaR.mjs";
import { r as resolveGlobalMap } from "./global-singleton-0m-JziJW.mjs";
import { t as createContainerEnvFile } from "./container-env-file-BtM4d167.mjs";
import { w as SANDBOX_GITHUB_CONFIG_DIR } from "./constants-B3lt5mRy.mjs";
import { r as resolveSandboxConfigForAgent } from "./config-CqeBZkfZ.mjs";
import { t as SandboxRuntimeRetiredError } from "./provisioning-error-z8JcKnZ7.mjs";
import { t as buildGitHubExecLaunchArgv } from "./github-exec-launch-C9SzlEVq.mjs";
import { c as DOCKER_SANDBOX_ENGINE, d as execContainerRaw, l as PODMAN_SANDBOX_ENGINE, n as bindPodmanSandboxEngine, o as resolvePodmanSandboxRuntimeInfo, s as validateSandboxContainerEngineTarget, u as execContainer } from "./podman-runtime-B7hhCHYt.mjs";
import { f as captureSandboxContainerTermination, h as containerState, l as resolveSandboxContainerOnlyMounts, m as containerHasTerminated, p as removeSandboxContainerRuntime, r as ensureSandboxContainer } from "./docker-DML2JD9P.mjs";
import { f as reserveSandboxRegistryEntry, h as withSandboxRegistryEntryLock, m as updateRegistry, n as assertSandboxRegistryEntryCurrent, r as completeSandboxRegistryReservation } from "./registry-By6hiBqz.mjs";
import { d as resolveRemoteShellRuntimePaths, n as createSshSandboxBackend, r as sshSandboxBackendManager } from "./ssh-backend-Cl4rzfBf.mjs";
import { randomUUID } from "node:crypto";
//#region src/agents/sandbox/process-cleanup.ts
const SANDBOX_PROCESS_REAP_TIMEOUT_MS = 4500;
const SANDBOX_REMOTE_PROCESS_PENDING_EXIT_CODE = 75;
const SANDBOX_EXEC_MARKER = "CODEX_SANDBOX_EXEC_ID";
function prepareSandboxProcessCleanup(backend, env) {
return backend.prepareProcessCleanup?.(env) ?? createSandboxProcessCleanup(backend.runShellCommand.bind(backend), env);
}
/** Only termination retains cleanup custody; interrupts still require current execution authority. */
function createSandboxProcessCleanup(runShellCommand, env, runTermination = runShellCommand) {
const marker = randomUUID();
return {
env: {
...env,
[SANDBOX_EXEC_MARKER]: marker
},
interrupt: async (timeoutMs) => {
const result = await runShellCommand({
script: `${SANDBOX_REMOTE_FIND_OWNED_PIDS}\nowned="$(find_owned_pids "$1")"\n[ -n "$owned" ] || exit ${SANDBOX_REMOTE_PROCESS_PENDING_EXIT_CODE}\nkill -INT $owned 2>/dev/null || true`,
args: [`${SANDBOX_EXEC_MARKER}=${marker}`],
allowFailure: true,
signal: AbortSignal.timeout(timeoutMs)
});
if (result.code === SANDBOX_REMOTE_PROCESS_PENDING_EXIT_CODE) return false;
if (result.code !== 0) throw new Error(`Sandbox process interrupt failed with code ${result.code}`);
return true;
},
terminate: async () => {
const result = await runTermination({
script: SANDBOX_REMOTE_TERMINATE_SCRIPT,
args: [`${SANDBOX_EXEC_MARKER}=${marker}`],
allowFailure: true,
signal: AbortSignal.timeout(SANDBOX_PROCESS_REAP_TIMEOUT_MS)
});
if (result.code !== 0) {
const detail = result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim();
throw new Error(detail || `Sandbox process tree cleanup failed with code ${result.code}; tear down the sandbox environment and inspect surviving processes before retrying.`);
}
}
};
}
const SANDBOX_REMOTE_FIND_OWNED_PIDS = String.raw`
find_owned_pids() {
for env_file in /proc/[0-9]*/environ; do
if [ -r "$env_file" ] && tr '\0' '\n' < "$env_file" 2>/dev/null | grep -Fqx "$1"; then
basename "$(dirname "$env_file")"
fi
done
}
`.trim();
const SANDBOX_REMOTE_TERMINATE_SCRIPT = String.raw`
${SANDBOX_REMOTE_FIND_OWNED_PIDS}
owned="$(find_owned_pids "$1")"
[ -z "$owned" ] || kill -TERM $owned 2>/dev/null || true
sleep 1
owned="$(find_owned_pids "$1")"
[ -z "$owned" ] || kill -KILL $owned 2>/dev/null || true
sleep 1
owned="$(find_owned_pids "$1")"
[ -z "$owned" ] || { echo "Sandbox process IDs survived SIGKILL: $owned" >&2; exit 1; }
`.trim();
//#endregion
//#region src/agents/sandbox/docker-backend.ts
function buildContainerExecArgs(params) {
const args = ["exec", "-i"];
if (params.tty) args.push("-t");
if (params.workdir) args.push("-w", params.workdir);
args.push("--env-file", params.envFile);
if (params.managedGitHubIdentity) args.push("--env", "GH_TOKEN", "--env", "GITHUB_TOKEN");
const pathExport = params.env.PATH ? "export PATH=\"${OPENCLAW_PREPEND_PATH}:$PATH\"; unset OPENCLAW_PREPEND_PATH; " : "";
args.push(params.containerName, "/bin/sh", "-lc", `${pathExport}${params.command}`);
return args;
}
async function createContainerSandboxBackend(engine, params, operatorAuthority, githubIdentity) {
const assertCurrent = () => {
operatorAuthority?.assertCurrent();
params.assertRuntimeCurrent?.();
};
assertCurrent();
if (engine.id === "podman" && params.cfg.browser.enabled) throw new Error("Podman sandboxing does not support browser sandboxes. Install Docker and select the docker backend, or disable sandbox.browser.enabled.");
const podmanTarget = engine.id === "podman" ? (await resolvePodmanSandboxRuntimeInfo()).target : void 0;
const boundEngine = podmanTarget ? bindPodmanSandboxEngine(podmanTarget) : engine;
const { containerName, containerId } = await ensureSandboxContainer({
engine: boundEngine,
...podmanTarget ? { podmanTarget } : {},
scopeKey: params.scopeKey,
workspaceDir: params.workspaceDir,
workspaceSource: params.workspaceSource,
assertCurrent,
operatorAuthority,
agentWorkspaceDir: params.agentWorkspaceDir,
skillsWorkspaceDir: params.skillsWorkspaceDir,
readOnlyResourceMounts: params.readOnlyResourceMounts,
cfg: params.cfg,
...params.requireCurrentConfig !== void 0 ? { requireCurrentConfig: params.requireCurrentConfig } : {}
});
assertCurrent();
const containerOnlyMounts = await resolveSandboxContainerOnlyMounts({
engine: boundEngine,
containerName: containerId,
assertCurrent
});
assertCurrent();
const { createSandboxFsBridge } = await import("./fs-bridge-H6p7b3_9.mjs");
assertCurrent();
const handle = createContainerSandboxBackendHandle({
engine: boundEngine,
containerName,
containerId,
workdir: params.cfg.docker.workdir,
env: params.cfg.docker.env,
image: params.cfg.docker.image,
podmanTarget,
assertCurrent,
githubIdentity
});
handle.createFsBridge = ({ sandbox }) => createSandboxFsBridge({
sandbox: {
...sandbox,
backend: sandbox.backend ?? handle
},
containerOnlyMounts
});
return handle;
}
async function createDockerSandboxBackend(params, operatorAuthority, githubIdentity) {
return await createContainerSandboxBackend(DOCKER_SANDBOX_ENGINE, params, operatorAuthority, githubIdentity);
}
async function createPodmanSandboxBackend(params, operatorAuthority, githubIdentity) {
return await createContainerSandboxBackend(PODMAN_SANDBOX_ENGINE, params, operatorAuthority, githubIdentity);
}
function createContainerSandboxBackendHandle(params) {
const run = (command, assertCurrent) => runContainerSandboxShellCommand({
engine: params.engine,
containerName: params.containerId,
podmanTarget: params.podmanTarget,
...command,
assertCurrent
});
return {
id: params.engine.id,
runtimeId: params.containerName,
runtimeLabel: params.containerName,
workdir: params.workdir,
env: params.env,
configLabel: params.image,
configLabelKind: "Image",
capabilities: {
browser: params.engine.id === "docker",
readOnlyResourceMounts: true
},
async buildExecSpec({ command, workdir, env: requestedEnv, usePty }) {
await validateSandboxContainerEngineTarget(params.engine, params.podmanTarget);
const identity = params.githubIdentity;
const githubProfileDir = identity?.localIdentityEnv.GH_CONFIG_DIR;
const externalCommandShell = githubProfileDir && process.platform === "win32" ? (await import("./shell-utils-DYBuR4WP.mjs")).getShellConfig() : void 0;
params.assertCurrent?.();
const env = identity ? {
...requestedEnv,
...identity.credentialScrubEnv,
...identity.localIdentityEnv,
GH_CONFIG_DIR: SANDBOX_GITHUB_CONFIG_DIR
} : requestedEnv;
const { PATH: requestedPath, ...containerEnv } = env;
if (requestedPath) containerEnv.OPENCLAW_PREPEND_PATH = requestedPath;
const envFile = await createContainerEnvFile(containerEnv);
try {
params.assertCurrent?.();
const argv = [
params.engine.command,
...params.engine.globalArgs ?? [],
...buildContainerExecArgs({
containerName: params.containerId,
command,
workdir: workdir ?? params.workdir,
env,
envFile: envFile.path,
tty: usePty,
managedGitHubIdentity: Boolean(githubProfileDir)
})
];
return {
argv: githubProfileDir ? buildGitHubExecLaunchArgv(argv, githubProfileDir, externalCommandShell ? { externalCommandShell } : void 0) : argv,
env: process.env,
stdinMode: usePty ? "pipe-open" : "pipe-closed",
finalizeToken: envFile.cleanup
};
} catch (error) {
await envFile.cleanup();
throw error;
}
},
async finalizeExec({ token }) {
if (token === void 0) return;
if (typeof token !== "function") throw new Error("Invalid container sandbox execution cleanup token.");
await token();
},
prepareProcessCleanup(env) {
params.assertCurrent?.();
const wasTerminated = captureSandboxContainerTermination(params.engine, params.containerName, params.containerId);
return createSandboxProcessCleanup((command) => run(command, params.assertCurrent), env, async (command) => {
const settled = {
code: 0,
stdout: Buffer.alloc(0),
stderr: Buffer.alloc(0)
};
try {
if (wasTerminated()) return settled;
const result = await run(command);
if (result.code === 0) return result;
if (wasTerminated() || await containerHasTerminated(params.engine, params.containerId, command.signal) || wasTerminated()) return settled;
return result;
} catch (error) {
if (wasTerminated()) return settled;
throw error;
}
});
},
runShellCommand(command) {
return run(command, params.assertCurrent);
}
};
}
async function runContainerSandboxShellCommand(params) {
await validateSandboxContainerEngineTarget(params.engine, params.podmanTarget);
const dockerArgs = [
"exec",
"-i",
params.containerName,
"sh",
"-c",
params.script,
"openclaw-sandbox-fs"
];
if (params.args?.length) dockerArgs.push(...params.args);
params.assertCurrent?.();
return execContainerRaw(params.engine, dockerArgs, {
input: params.stdin,
allowFailure: params.allowFailure,
signal: params.signal
});
}
function createContainerSandboxBackendManager(engine) {
const resolvePodmanTarget = (entry) => {
if (engine.id !== "podman") return;
if (entry.backendTarget) return entry.backendTarget;
throw Object.assign(/* @__PURE__ */ new Error(`Podman sandbox runtime ${entry.containerName} has no recorded engine target. Remove that unshipped runtime manually before managing it.`), { code: "INVALID_CONFIG" });
};
return {
async describeRuntime({ entry, config, agentId }) {
const podmanTarget = resolvePodmanTarget(entry);
await validateSandboxContainerEngineTarget(engine, podmanTarget);
const runtimeEngine = podmanTarget ? bindPodmanSandboxEngine(podmanTarget) : engine;
const state = await containerState(runtimeEngine, entry.containerName);
let actualConfigLabel = entry.image;
let actualImageId;
if (state.exists) try {
const result = await execContainer(runtimeEngine, [
"inspect",
"-f",
runtimeEngine.id === "podman" ? "{{.ImageName}} {{.Image}}" : "{{.Config.Image}}",
entry.containerName
], { allowFailure: true });
if (result.code === 0) {
const inspected = result.stdout.trim();
if (runtimeEngine.id === "podman") {
const [imageName, imageId] = inspected.split(" ", 2);
actualConfigLabel = imageName || actualConfigLabel;
actualImageId = imageId;
} else actualConfigLabel = inspected || actualConfigLabel;
}
} catch {}
const sandboxCfg = resolveSandboxConfigForAgent(config, agentId);
const configuredImage = entry.configLabelKind === "BrowserImage" ? sandboxCfg.browser.image : sandboxCfg.docker.image;
let configLabelMatch = actualConfigLabel === configuredImage;
if (runtimeEngine.id === "podman" && !configLabelMatch && actualImageId) try {
const result = await execContainer(runtimeEngine, [
"image",
"inspect",
"-f",
"{{.Id}}",
configuredImage
], { allowFailure: true });
if (result.code === 0) {
const normalizeImageId = (value) => value.trim().replace(/^sha256:/u, "");
configLabelMatch = normalizeImageId(actualImageId) === normalizeImageId(result.stdout);
}
} catch {}
return {
running: state.running,
actualConfigLabel,
configLabelMatch
};
},
async removeRuntime({ entry }) {
const podmanTarget = resolvePodmanTarget(entry);
await validateSandboxContainerEngineTarget(engine, podmanTarget);
const runtimeEngine = podmanTarget ? bindPodmanSandboxEngine(podmanTarget) : engine;
await removeSandboxContainerRuntime(runtimeEngine, entry.containerName);
}
};
}
const dockerSandboxBackendManager = createContainerSandboxBackendManager(DOCKER_SANDBOX_ENGINE);
const podmanSandboxBackendManager = createContainerSandboxBackendManager(PODMAN_SANDBOX_ENGINE);
//#endregion
//#region src/agents/sandbox/backend.ts
const SANDBOX_BACKEND_FACTORIES_STATE_KEY = Symbol.for("openclaw.sandboxBackendFactories");
function getSandboxBackendFactories() {
return resolveGlobalMap(SANDBOX_BACKEND_FACTORIES_STATE_KEY);
}
function normalizeSandboxBackendId(id) {
const normalized = normalizeOptionalLowercaseString(id);
if (!normalized) throw new Error("Sandbox backend id must not be empty.");
return normalized;
}
/** Register or replace a sandbox backend and return a restore callback. */
function registerSandboxBackend(id, registration) {
const normalizedId = normalizeSandboxBackendId(id);
const resolved = typeof registration === "function" ? { factory: registration } : registration;
const factories = getSandboxBackendFactories();
const generation = {
registration: resolved,
previous: factories.get(normalizedId),
retired: false
};
factories.set(normalizedId, generation);
return () => {
if (generation.retired) return;
generation.retired = true;
if (factories.get(normalizedId) !== generation) return;
let previous = generation.previous;
while (previous?.retired) previous = previous.previous;
if (previous) {
factories.set(normalizedId, previous);
return;
}
factories.delete(normalizedId);
};
}
function getSandboxBackendFactory(id) {
const registration = resolveSandboxBackendRegistration(id);
if (!registration) return null;
if (!registration.reserveRuntimeId) return registration.factory;
const factory = registration.factory;
return async (params) => {
const { runtimeId, assertRuntimeCurrent } = params;
if (!runtimeId || !assertRuntimeCurrent) throw new Error(`Sandbox backend "${id}" requires a registry-reserved runtime and its current owner.`);
assertRuntimeCurrent();
return factory({
...params,
runtimeId,
assertRuntimeCurrent
});
};
}
function getSandboxBackendManager(id) {
return resolveSandboxBackendRegistration(id)?.manager ?? null;
}
/** Include legacy rows in the lifecycle selected by the currently registered backend. */
function usesSandboxRuntimeReservations(id) {
return resolveSandboxBackendRegistration(id)?.reserveRuntimeId !== void 0;
}
function getSandboxBackendWorkdirResolver(id) {
return resolveSandboxBackendRegistration(id)?.resolveWorkdir ?? null;
}
/** Read static backend capabilities without provisioning a sandbox runtime. */
function getSandboxBackendCapabilities(id) {
return resolveSandboxBackendRegistration(id)?.capabilities;
}
function requireSandboxBackendFactory(id) {
const factory = getSandboxBackendFactory(id);
if (factory) return factory;
throw new Error([`Sandbox backend "${id}" is not registered.`, "Load the plugin that provides it, or set agents.defaults.sandbox.backend=docker."].join("\n"));
}
/** Create and publish a backend, reserving provider IDs only for opted-in factories. */
async function createSandboxBackend(params, operatorAuthority, githubIdentity, guard) {
params.assertRuntimeCurrent?.();
const factory = requireSandboxBackendFactory(params.cfg.backend);
if (githubIdentity && factory !== createDockerSandboxBackend && factory !== createPodmanSandboxBackend) throw new Error("Sandbox GitHub identity requires the built-in Docker or Podman backend.");
const reserveRuntimeId = resolveSandboxBackendRegistration(params.cfg.backend)?.reserveRuntimeId;
const toEntry = (backend) => ({
containerName: backend.runtimeId,
backendId: backend.id,
runtimeLabel: backend.runtimeLabel,
sessionKey: params.scopeKey,
createdAtMs: Date.now(),
lastUsedAtMs: Date.now(),
image: backend.configLabel ?? params.cfg.docker.image,
configLabelKind: backend.configLabelKind ?? "Image"
});
if (!reserveRuntimeId) {
const backend = factory === createDockerSandboxBackend ? await createDockerSandboxBackend(params, operatorAuthority, githubIdentity) : factory === createPodmanSandboxBackend ? await createPodmanSandboxBackend(params, operatorAuthority, githubIdentity) : await factory(params);
await updateRegistry(toEntry(backend), {
...guard,
beforeLegacyApply: params.assertRuntimeCurrent
});
return backend;
}
for (let attempt = 0;; attempt++) {
params.assertRuntimeCurrent?.();
const reservation = await reserveSandboxRegistryEntry({
containerName: reserveRuntimeId(params),
backendId: params.cfg.backend,
sessionKey: params.scopeKey,
createdAtMs: Date.now(),
lastUsedAtMs: Date.now(),
image: params.cfg.docker.image,
workspaceDir: params.workspaceDir
}, {
...guard,
beforeLegacyApply: params.assertRuntimeCurrent
});
try {
return await withSandboxRegistryEntryLock(reservation, async () => {
const assertCurrent = () => {
params.assertRuntimeCurrent?.();
assertSandboxRegistryEntryCurrent(reservation);
};
try {
const backend = await factory({
...params,
workspaceDir: reservation.workspaceDir ?? params.workspaceDir,
runtimeId: reservation.containerName,
assertRuntimeCurrent: assertCurrent
});
if (backend.runtimeId !== reservation.containerName || backend.id !== reservation.backendId) throw new Error("Sandbox backend returned a runtime outside its reserved generation.");
await completeSandboxRegistryReservation({
...reservation,
...toEntry(backend),
createdAtMs: reservation.createdAtMs
}, false, {
...guard,
beforeLegacyApply: params.assertRuntimeCurrent
});
return backend;
} catch (error) {
if (error instanceof SandboxRuntimeRetiredError && error.runtimeId === reservation.containerName) await completeSandboxRegistryReservation(reservation, true, {
...guard,
beforeLegacyApply: params.assertRuntimeCurrent
});
throw error;
}
});
} catch (error) {
if (!(error instanceof SandboxRuntimeRetiredError) || error.runtimeId !== reservation.containerName || attempt > 0) throw error;
}
}
}
const builtinSandboxBackends = /* @__PURE__ */ new Map();
builtinSandboxBackends.set("docker", {
factory: createDockerSandboxBackend,
manager: dockerSandboxBackendManager,
resolveWorkdir: ({ cfg }) => cfg.docker.workdir,
capabilities: { readOnlyResourceMounts: true }
});
builtinSandboxBackends.set("podman", {
factory: createPodmanSandboxBackend,
manager: podmanSandboxBackendManager,
resolveWorkdir: ({ cfg }) => cfg.docker.workdir,
capabilities: { readOnlyResourceMounts: true }
});
builtinSandboxBackends.set("ssh", {
factory: createSshSandboxBackend,
manager: sshSandboxBackendManager,
resolveWorkdir: ({ cfg, scopeKey }) => resolveRemoteShellRuntimePaths(cfg.ssh.workspaceRoot, scopeKey).remoteWorkspaceDir
});
function resolveSandboxBackendRegistration(id) {
const normalizedId = normalizeSandboxBackendId(id);
return getSandboxBackendFactories().get(normalizedId)?.registration ?? builtinSandboxBackends.get(normalizedId);
}
//#endregion
export { getSandboxBackendWorkdirResolver as a, usesSandboxRuntimeReservations as c, getSandboxBackendManager as i, dockerSandboxBackendManager as l, getSandboxBackendCapabilities as n, registerSandboxBackend as o, getSandboxBackendFactory as r, requireSandboxBackendFactory as s, createSandboxBackend as t, prepareSandboxProcessCleanup as u };