mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 14:54:31 +00:00
fix: execute sandboxed file ops inside containers (#4026)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 795ec6aa2f
Co-authored-by: davidbors-snyk <240482518+davidbors-snyk@users.noreply.github.com>
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
Reviewed-by: @steipete
This commit is contained in:
@@ -1,5 +1,109 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import type { SandboxConfig, SandboxDockerConfig, SandboxWorkspaceAccess } from "./types.js";
|
||||
|
||||
type ExecDockerRawOptions = {
|
||||
allowFailure?: boolean;
|
||||
input?: Buffer | string;
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
export type ExecDockerRawResult = {
|
||||
stdout: Buffer;
|
||||
stderr: Buffer;
|
||||
code: number;
|
||||
};
|
||||
|
||||
type ExecDockerRawError = Error & {
|
||||
code: number;
|
||||
stdout: Buffer;
|
||||
stderr: Buffer;
|
||||
};
|
||||
|
||||
function createAbortError(): Error {
|
||||
const err = new Error("Aborted");
|
||||
err.name = "AbortError";
|
||||
return err;
|
||||
}
|
||||
|
||||
export function execDockerRaw(
|
||||
args: string[],
|
||||
opts?: ExecDockerRawOptions,
|
||||
): Promise<ExecDockerRawResult> {
|
||||
return new Promise<ExecDockerRawResult>((resolve, reject) => {
|
||||
const child = spawn("docker", args, {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
const stdoutChunks: Buffer[] = [];
|
||||
const stderrChunks: Buffer[] = [];
|
||||
let aborted = false;
|
||||
|
||||
const signal = opts?.signal;
|
||||
const handleAbort = () => {
|
||||
if (aborted) {
|
||||
return;
|
||||
}
|
||||
aborted = true;
|
||||
child.kill("SIGTERM");
|
||||
};
|
||||
if (signal) {
|
||||
if (signal.aborted) {
|
||||
handleAbort();
|
||||
} else {
|
||||
signal.addEventListener("abort", handleAbort);
|
||||
}
|
||||
}
|
||||
|
||||
child.stdout?.on("data", (chunk) => {
|
||||
stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
});
|
||||
child.stderr?.on("data", (chunk) => {
|
||||
stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
if (signal) {
|
||||
signal.removeEventListener("abort", handleAbort);
|
||||
}
|
||||
reject(error);
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (signal) {
|
||||
signal.removeEventListener("abort", handleAbort);
|
||||
}
|
||||
const stdout = Buffer.concat(stdoutChunks);
|
||||
const stderr = Buffer.concat(stderrChunks);
|
||||
if (aborted || signal?.aborted) {
|
||||
reject(createAbortError());
|
||||
return;
|
||||
}
|
||||
const exitCode = code ?? 0;
|
||||
if (exitCode !== 0 && !opts?.allowFailure) {
|
||||
const message = stderr.length > 0 ? stderr.toString("utf8").trim() : "";
|
||||
const error: ExecDockerRawError = Object.assign(
|
||||
new Error(message || `docker ${args.join(" ")} failed`),
|
||||
{
|
||||
code: exitCode,
|
||||
stdout,
|
||||
stderr,
|
||||
},
|
||||
);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve({ stdout, stderr, code: exitCode });
|
||||
});
|
||||
|
||||
const stdin = child.stdin;
|
||||
if (stdin) {
|
||||
if (opts?.input !== undefined) {
|
||||
stdin.end(opts.input);
|
||||
} else {
|
||||
stdin.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
import { formatCliCommand } from "../../cli/command-format.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { computeSandboxConfigHash } from "./config-hash.js";
|
||||
@@ -9,28 +113,15 @@ import { resolveSandboxAgentId, resolveSandboxScopeKey, slugifySessionKey } from
|
||||
|
||||
const HOT_CONTAINER_WINDOW_MS = 5 * 60 * 1000;
|
||||
|
||||
export function execDocker(args: string[], opts?: { allowFailure?: boolean }) {
|
||||
return new Promise<{ stdout: string; stderr: string; code: number }>((resolve, reject) => {
|
||||
const child = spawn("docker", args, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout?.on("data", (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr?.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
child.on("close", (code) => {
|
||||
const exitCode = code ?? 0;
|
||||
if (exitCode !== 0 && !opts?.allowFailure) {
|
||||
reject(new Error(stderr.trim() || `docker ${args.join(" ")} failed`));
|
||||
return;
|
||||
}
|
||||
resolve({ stdout, stderr, code: exitCode });
|
||||
});
|
||||
});
|
||||
export type ExecDockerOptions = ExecDockerRawOptions;
|
||||
|
||||
export async function execDocker(args: string[], opts?: ExecDockerOptions) {
|
||||
const result = await execDockerRaw(args, opts);
|
||||
return {
|
||||
stdout: result.stdout.toString("utf8"),
|
||||
stderr: result.stderr.toString("utf8"),
|
||||
code: result.code,
|
||||
};
|
||||
}
|
||||
|
||||
export async function readDockerPort(containerName: string, port: number) {
|
||||
@@ -195,9 +286,7 @@ export function buildSandboxCreateArgs(params: {
|
||||
if (typeof params.cfg.cpus === "number" && params.cfg.cpus > 0) {
|
||||
args.push("--cpus", String(params.cfg.cpus));
|
||||
}
|
||||
for (const [name, value] of Object.entries(params.cfg.ulimits ?? {}) as Array<
|
||||
[string, string | number | { soft?: number; hard?: number }]
|
||||
>) {
|
||||
for (const [name, value] of Object.entries(params.cfg.ulimits ?? {})) {
|
||||
const formatted = formatUlimitValue(name, value);
|
||||
if (formatted) {
|
||||
args.push("--ulimit", formatted);
|
||||
|
||||
Reference in New Issue
Block a user