openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
468 lines (467 loc) • 18.5 kB
JavaScript
import { C as parseStrictNonNegativeInteger, o as asDateTimestampMs } from "./number-coercion-CuVhdCBs.mjs";
import { r as asNullableRecord } from "./record-coerce-TIsquoVR.mjs";
import { t as createContainerEnvFile } from "./container-env-file-BATVpQPA.mjs";
import { w as SANDBOX_GITHUB_CONFIG_DIR } from "./constants-sQ6GWokQ.mjs";
import { i as resolveSandboxConfigForAgent } from "./config-DGIfx80_.mjs";
import { t as buildGitHubExecLaunchArgv } from "./github-exec-launch-A2x7DRMi.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-bNvz9prY.mjs";
import { f as captureSandboxContainerTermination, h as containerState, l as resolveSandboxContainerOnlyMounts, m as containerHasTerminated, p as removeSandboxContainerRuntime, r as ensureSandboxContainer } from "./docker-DeBQ6N6n.mjs";
import { randomUUID } from "node:crypto";
import { PATH_ALIAS_POLICIES } from "@openclaw/fs-safe/advanced";
import { GUEST_FILESYSTEM_PYTHON } from "@openclaw/fs-safe/guest";
//#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 resolveContainerExecEnv(env) {
const { PATH: requestedPath, ...containerEnv } = env;
if (requestedPath) containerEnv.OPENCLAW_PREPEND_PATH = requestedPath;
return containerEnv;
}
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;
}
function resolveConfiguredDockerRuntimeImage(params) {
const sandboxCfg = resolveSandboxConfigForAgent(params.config, params.agentId);
switch (params.configLabelKind) {
case "BrowserImage": return sandboxCfg.browser.image;
default: return sandboxCfg.docker.image;
}
}
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-CULDLEjL.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,
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-Cug0C2Du.mjs")).getShellConfig() : void 0;
params.assertCurrent?.();
const env = identity ? {
...requestedEnv,
...identity.credentialScrubEnv,
...identity.localIdentityEnv,
GH_CONFIG_DIR: SANDBOX_GITHUB_CONFIG_DIR
} : requestedEnv;
const envFile = await createContainerEnvFile(resolveContainerExecEnv(env));
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 runDockerSandboxShellCommand(params) {
return runContainerSandboxShellCommand({
engine: DOCKER_SANDBOX_ENGINE,
...params
});
}
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 configuredImage = resolveConfiguredDockerRuntimeImage({
config,
agentId,
configLabelKind: entry.configLabelKind
});
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/infra/directory-entries.ts
/** Decode the sandbox directory command's metadata, never file contents. */
function parseDirectoryEntries(text) {
const entries = JSON.parse(text);
if (!Array.isArray(entries)) throw new Error("Invalid sandbox directory listing.");
return entries.map((entry) => {
const record = asNullableRecord(entry);
if (!record || typeof record.name !== "string" || typeof record.isDirectory !== "boolean") throw new Error("Invalid sandbox directory entry.");
return {
name: record.name,
isDirectory: record.isDirectory
};
});
}
//#endregion
//#region src/agents/sandbox/fs-bridge-mutation-helper.ts
/**
* Shell plans for pinned sandbox filesystem operations.
*
* Selects the local interpreter and supplies quoted Python source to local and remote transports.
*/
const SANDBOX_PINNED_MUTATION_PYTHON_CANDIDATES = [
"/usr/bin/python3",
"/usr/local/bin/python3",
"/opt/homebrew/bin/python3",
"/bin/python3"
];
const PINNED_MUTATION_ACTION_LABELS = {
write: "write files",
create: "create files",
mkdir: "create directories",
remove: "remove files",
"copy-destination": "copy files"
};
const SANDBOX_PINNED_MUTATION_PYTHON_SHELL_LITERAL = `'${GUEST_FILESYSTEM_PYTHON.replaceAll("'", `'\\''`)}'`;
function pinnedEntryArgs(pinned) {
return [
pinned.mountRootPath,
pinned.relativeParentPath,
pinned.basename
];
}
/** Encode only already-admitted pinned facts; transport and path checks stay with each bridge. */
function buildPinnedMutationArgs(operation) {
switch (operation.kind) {
case "read": return [
operation.kind,
...pinnedEntryArgs(operation.pinned),
...operation.maxBytes === void 0 ? [] : [String(operation.maxBytes)]
];
case "write":
case "create": return [
operation.kind,
...pinnedEntryArgs(operation.pinned),
operation.mkdir ? "1" : "0"
];
case "mkdirp":
case "readdir": return [
operation.kind,
operation.pinned.mountRootPath,
operation.pinned.relativePath
];
case "remove": return [
operation.kind,
...pinnedEntryArgs(operation.pinned),
operation.recursive ? "1" : "0",
operation.force === false ? "0" : "1"
];
}
return [
operation.kind,
...pinnedEntryArgs(operation.source),
...pinnedEntryArgs(operation.destination),
operation.kind === "rename" || operation.mkdir ? "1" : "0"
];
}
function buildPinnedMutationPlan(operation) {
const checks = operation.kind === "copy" || operation.kind === "rename" ? [operation.sourceCheck, operation.destinationCheck] : [operation.check];
if (operation.kind === "remove" || operation.kind === "rename") {
const check = operation.kind === "remove" ? operation.check : operation.sourceCheck;
checks[0] = {
target: check.target,
options: {
...check.options,
aliasPolicy: PATH_ALIAS_POLICIES.unlinkTarget
}
};
}
const args = buildPinnedMutationArgs(operation);
return {
checks,
recheckBeforeCommand: true,
script: [
"set -eu",
"python_cmd=''",
...SANDBOX_PINNED_MUTATION_PYTHON_CANDIDATES.map((candidate) => `if [ -z "$python_cmd" ] && [ -x '${candidate}' ]; then python_cmd='${candidate}'; fi`),
"if [ -z \"$python_cmd\" ]; then python_cmd=$(command -v python3 2>/dev/null || command -v python 2>/dev/null || true); fi",
"if [ -z \"$python_cmd\" ]; then",
" echo >&2 'sandbox pinned mutation helper requires python3 or python'",
" exit 127",
"fi",
`python_script=${SANDBOX_PINNED_MUTATION_PYTHON_SHELL_LITERAL}`,
"exec \"$python_cmd\" -c \"$python_script\" \"$@\""
].join("\n"),
args
};
}
//#endregion
//#region src/agents/sandbox/fs-bridge-stat-parse.ts
/**
* Stat output parsers for sandbox filesystem bridges.
*
* Handles GNU/BSD size and mtime formats returned through backend shell commands.
*/
function hasMultipleHardlinks(raw) {
const linkCount = parseStrictNonNegativeInteger(raw);
return linkCount === void 0 ? /^\d+$/.test(raw) : linkCount > 1;
}
/** Parses file sizes, capping huge integer strings at the largest safe JS integer. */
function parseSandboxStatSize(value) {
const raw = value ?? "0";
const parsed = parseStrictNonNegativeInteger(raw);
if (parsed !== void 0) return parsed;
return /^\d+$/.test(raw) ? Number.MAX_SAFE_INTEGER : 0;
}
/** Parses stat mtimes from epoch seconds or date strings into millisecond timestamps. */
function parseSandboxStatMtimeMs(value) {
const raw = value ?? "0";
if (/^\d+(?:\.\d+)?$/.test(raw)) {
const mtimeMs = Number(raw) * 1e3;
return asDateTimestampMs(mtimeMs) ?? 0;
}
return asDateTimestampMs(Date.parse(raw)) ?? 0;
}
//#endregion
export { SANDBOX_PINNED_MUTATION_PYTHON_SHELL_LITERAL as a, parseDirectoryEntries as c, dockerSandboxBackendManager as d, podmanSandboxBackendManager as f, PINNED_MUTATION_ACTION_LABELS as i, createDockerSandboxBackend as l, prepareSandboxProcessCleanup as m, parseSandboxStatMtimeMs as n, buildPinnedMutationArgs as o, runDockerSandboxShellCommand as p, parseSandboxStatSize as r, buildPinnedMutationPlan as s, hasMultipleHardlinks as t, createPodmanSandboxBackend as u };