mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 15:24:58 +00:00
refactor: extract shared sandbox and gateway plumbing
This commit is contained in:
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { BROWSER_BRIDGES } from "./browser-bridges.js";
|
||||
import { ensureSandboxBrowser } from "./browser.js";
|
||||
import { resetNoVncObserverTokensForTests } from "./novnc-auth.js";
|
||||
import { collectDockerFlagValues, findDockerArgsCall } from "./test-args.js";
|
||||
import type { SandboxConfig } from "./types.js";
|
||||
|
||||
const dockerMocks = vi.hoisted(() => ({
|
||||
@@ -85,16 +86,6 @@ function buildConfig(enableNoVnc: boolean): SandboxConfig {
|
||||
};
|
||||
}
|
||||
|
||||
function envEntriesFromDockerArgs(args: string[]): string[] {
|
||||
const values: string[] = [];
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
if (args[i] === "-e" && typeof args[i + 1] === "string") {
|
||||
values.push(args[i + 1]);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
describe("ensureSandboxBrowser create args", () => {
|
||||
beforeEach(() => {
|
||||
BROWSER_BRIDGES.clear();
|
||||
@@ -151,13 +142,11 @@ describe("ensureSandboxBrowser create args", () => {
|
||||
cfg: buildConfig(true),
|
||||
});
|
||||
|
||||
const createArgs = dockerMocks.execDocker.mock.calls.find(
|
||||
(call: unknown[]) => Array.isArray(call[0]) && call[0][0] === "create",
|
||||
)?.[0] as string[] | undefined;
|
||||
const createArgs = findDockerArgsCall(dockerMocks.execDocker.mock.calls, "create");
|
||||
|
||||
expect(createArgs).toBeDefined();
|
||||
expect(createArgs).toContain("127.0.0.1::6080");
|
||||
const envEntries = envEntriesFromDockerArgs(createArgs ?? []);
|
||||
const envEntries = collectDockerFlagValues(createArgs ?? [], "-e");
|
||||
expect(envEntries).toContain("OPENCLAW_BROWSER_NO_SANDBOX=1");
|
||||
const passwordEntry = envEntries.find((entry) =>
|
||||
entry.startsWith("OPENCLAW_BROWSER_NOVNC_PASSWORD="),
|
||||
@@ -175,10 +164,8 @@ describe("ensureSandboxBrowser create args", () => {
|
||||
cfg: buildConfig(false),
|
||||
});
|
||||
|
||||
const createArgs = dockerMocks.execDocker.mock.calls.find(
|
||||
(call: unknown[]) => Array.isArray(call[0]) && call[0][0] === "create",
|
||||
)?.[0] as string[] | undefined;
|
||||
const envEntries = envEntriesFromDockerArgs(createArgs ?? []);
|
||||
const createArgs = findDockerArgsCall(dockerMocks.execDocker.mock.calls, "create");
|
||||
const envEntries = collectDockerFlagValues(createArgs ?? [], "-e");
|
||||
expect(envEntries.some((entry) => entry.startsWith("OPENCLAW_BROWSER_NOVNC_PASSWORD="))).toBe(
|
||||
false,
|
||||
);
|
||||
@@ -196,9 +183,7 @@ describe("ensureSandboxBrowser create args", () => {
|
||||
cfg,
|
||||
});
|
||||
|
||||
const createArgs = dockerMocks.execDocker.mock.calls.find(
|
||||
(call: unknown[]) => Array.isArray(call[0]) && call[0][0] === "create",
|
||||
)?.[0] as string[] | undefined;
|
||||
const createArgs = findDockerArgsCall(dockerMocks.execDocker.mock.calls, "create");
|
||||
|
||||
expect(createArgs).toBeDefined();
|
||||
expect(createArgs).toContain("/tmp/workspace:/workspace:ro");
|
||||
@@ -215,9 +200,7 @@ describe("ensureSandboxBrowser create args", () => {
|
||||
cfg,
|
||||
});
|
||||
|
||||
const createArgs = dockerMocks.execDocker.mock.calls.find(
|
||||
(call: unknown[]) => Array.isArray(call[0]) && call[0][0] === "create",
|
||||
)?.[0] as string[] | undefined;
|
||||
const createArgs = findDockerArgsCall(dockerMocks.execDocker.mock.calls, "create");
|
||||
|
||||
expect(createArgs).toBeDefined();
|
||||
expect(createArgs).toContain("/tmp/workspace:/workspace");
|
||||
|
||||
@@ -11,11 +11,7 @@ import { defaultRuntime } from "../../runtime.js";
|
||||
import { BROWSER_BRIDGES } from "./browser-bridges.js";
|
||||
import { computeSandboxBrowserConfigHash } from "./config-hash.js";
|
||||
import { resolveSandboxBrowserDockerCreateConfig } from "./config.js";
|
||||
import {
|
||||
DEFAULT_SANDBOX_BROWSER_IMAGE,
|
||||
SANDBOX_AGENT_WORKSPACE_MOUNT,
|
||||
SANDBOX_BROWSER_SECURITY_HASH_EPOCH,
|
||||
} from "./constants.js";
|
||||
import { DEFAULT_SANDBOX_BROWSER_IMAGE, SANDBOX_BROWSER_SECURITY_HASH_EPOCH } from "./constants.js";
|
||||
import {
|
||||
buildSandboxCreateArgs,
|
||||
dockerContainerState,
|
||||
@@ -37,6 +33,7 @@ import { resolveSandboxAgentId, slugifySessionKey } from "./shared.js";
|
||||
import { isToolAllowed } from "./tool-policy.js";
|
||||
import type { SandboxBrowserContext, SandboxConfig } from "./types.js";
|
||||
import { validateNetworkMode } from "./validate-sandbox-security.js";
|
||||
import { appendWorkspaceMountArgs } from "./workspace-mounts.js";
|
||||
|
||||
const HOT_BROWSER_WINDOW_MS = 5 * 60 * 1000;
|
||||
const CDP_SOURCE_RANGE_ENV_KEY = "OPENCLAW_BROWSER_CDP_SOURCE_RANGE";
|
||||
@@ -237,15 +234,13 @@ export async function ensureSandboxBrowser(params: {
|
||||
includeBinds: false,
|
||||
bindSourceRoots: [params.workspaceDir, params.agentWorkspaceDir],
|
||||
});
|
||||
const mainMountSuffix = params.cfg.workspaceAccess === "rw" ? "" : ":ro";
|
||||
args.push("-v", `${params.workspaceDir}:${params.cfg.docker.workdir}${mainMountSuffix}`);
|
||||
if (params.cfg.workspaceAccess !== "none" && params.workspaceDir !== params.agentWorkspaceDir) {
|
||||
const agentMountSuffix = params.cfg.workspaceAccess === "ro" ? ":ro" : "";
|
||||
args.push(
|
||||
"-v",
|
||||
`${params.agentWorkspaceDir}:${SANDBOX_AGENT_WORKSPACE_MOUNT}${agentMountSuffix}`,
|
||||
);
|
||||
}
|
||||
appendWorkspaceMountArgs({
|
||||
args,
|
||||
workspaceDir: params.workspaceDir,
|
||||
agentWorkspaceDir: params.agentWorkspaceDir,
|
||||
workdir: params.cfg.docker.workdir,
|
||||
workspaceAccess: params.cfg.workspaceAccess,
|
||||
});
|
||||
if (browserDockerCfg.binds?.length) {
|
||||
for (const bind of browserDockerCfg.binds) {
|
||||
args.push("-v", bind);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Readable } from "node:stream";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { computeSandboxConfigHash } from "./config-hash.js";
|
||||
import { ensureSandboxContainer } from "./docker.js";
|
||||
import { collectDockerFlagValues } from "./test-args.js";
|
||||
import type { SandboxConfig } from "./types.js";
|
||||
|
||||
type SpawnCall = {
|
||||
@@ -237,13 +238,7 @@ describe("ensureSandboxContainer config-hash recreation", () => {
|
||||
expect(createCall).toBeDefined();
|
||||
expect(createCall?.args).toContain(`openclaw.configHash=${expectedHash}`);
|
||||
|
||||
const bindArgs: string[] = [];
|
||||
const args = createCall?.args ?? [];
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
if (args[i] === "-v" && typeof args[i + 1] === "string") {
|
||||
bindArgs.push(args[i + 1]);
|
||||
}
|
||||
}
|
||||
const bindArgs = collectDockerFlagValues(createCall?.args ?? [], "-v");
|
||||
const workspaceMountIdx = bindArgs.indexOf("/tmp/workspace:/workspace");
|
||||
const customMountIdx = bindArgs.indexOf("/tmp/workspace-shared/USER.md:/workspace/USER.md:ro");
|
||||
expect(workspaceMountIdx).toBeGreaterThanOrEqual(0);
|
||||
@@ -277,13 +272,7 @@ describe("ensureSandboxContainer config-hash recreation", () => {
|
||||
);
|
||||
expect(createCall).toBeDefined();
|
||||
|
||||
const bindArgs: string[] = [];
|
||||
const args = createCall?.args ?? [];
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
if (args[i] === "-v" && typeof args[i + 1] === "string") {
|
||||
bindArgs.push(args[i + 1]);
|
||||
}
|
||||
}
|
||||
const bindArgs = collectDockerFlagValues(createCall?.args ?? [], "-v");
|
||||
expect(bindArgs).toContain(expectedMainMount);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -164,11 +164,12 @@ export function execDockerRaw(
|
||||
import { formatCliCommand } from "../../cli/command-format.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { computeSandboxConfigHash } from "./config-hash.js";
|
||||
import { DEFAULT_SANDBOX_IMAGE, SANDBOX_AGENT_WORKSPACE_MOUNT } from "./constants.js";
|
||||
import { DEFAULT_SANDBOX_IMAGE } from "./constants.js";
|
||||
import { readRegistry, updateRegistry } from "./registry.js";
|
||||
import { resolveSandboxAgentId, resolveSandboxScopeKey, slugifySessionKey } from "./shared.js";
|
||||
import type { SandboxConfig, SandboxDockerConfig, SandboxWorkspaceAccess } from "./types.js";
|
||||
import { validateSandboxSecurity } from "./validate-sandbox-security.js";
|
||||
import { appendWorkspaceMountArgs } from "./workspace-mounts.js";
|
||||
|
||||
const log = createSubsystemLogger("docker");
|
||||
|
||||
@@ -452,15 +453,13 @@ async function createSandboxContainer(params: {
|
||||
bindSourceRoots: [workspaceDir, params.agentWorkspaceDir],
|
||||
});
|
||||
args.push("--workdir", cfg.workdir);
|
||||
const mainMountSuffix = params.workspaceAccess === "rw" ? "" : ":ro";
|
||||
args.push("-v", `${workspaceDir}:${cfg.workdir}${mainMountSuffix}`);
|
||||
if (params.workspaceAccess !== "none" && workspaceDir !== params.agentWorkspaceDir) {
|
||||
const agentMountSuffix = params.workspaceAccess === "ro" ? ":ro" : "";
|
||||
args.push(
|
||||
"-v",
|
||||
`${params.agentWorkspaceDir}:${SANDBOX_AGENT_WORKSPACE_MOUNT}${agentMountSuffix}`,
|
||||
);
|
||||
}
|
||||
appendWorkspaceMountArgs({
|
||||
args,
|
||||
workspaceDir,
|
||||
agentWorkspaceDir: params.agentWorkspaceDir,
|
||||
workdir: cfg.workdir,
|
||||
workspaceAccess: params.workspaceAccess,
|
||||
});
|
||||
appendCustomBinds(args, cfg);
|
||||
args.push(cfg.image, "sleep", "infinity");
|
||||
|
||||
|
||||
15
src/agents/sandbox/test-args.ts
Normal file
15
src/agents/sandbox/test-args.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export function findDockerArgsCall(calls: unknown[][], command: string): string[] | undefined {
|
||||
return calls.find((call) => Array.isArray(call[0]) && call[0][0] === command)?.[0] as
|
||||
| string[]
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export function collectDockerFlagValues(args: string[], flag: string): string[] {
|
||||
const values: string[] = [];
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
if (args[i] === flag && typeof args[i + 1] === "string") {
|
||||
values.push(args[i + 1]);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
49
src/agents/sandbox/workspace-mounts.test.ts
Normal file
49
src/agents/sandbox/workspace-mounts.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { appendWorkspaceMountArgs } from "./workspace-mounts.js";
|
||||
|
||||
describe("appendWorkspaceMountArgs", () => {
|
||||
it.each([
|
||||
{ access: "rw" as const, expected: "/tmp/workspace:/workspace" },
|
||||
{ access: "ro" as const, expected: "/tmp/workspace:/workspace:ro" },
|
||||
{ access: "none" as const, expected: "/tmp/workspace:/workspace:ro" },
|
||||
])("sets main mount permissions for workspaceAccess=$access", ({ access, expected }) => {
|
||||
const args: string[] = [];
|
||||
appendWorkspaceMountArgs({
|
||||
args,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentWorkspaceDir: "/tmp/agent-workspace",
|
||||
workdir: "/workspace",
|
||||
workspaceAccess: access,
|
||||
});
|
||||
|
||||
expect(args).toContain(expected);
|
||||
});
|
||||
|
||||
it("omits agent workspace mount when workspaceAccess is none", () => {
|
||||
const args: string[] = [];
|
||||
appendWorkspaceMountArgs({
|
||||
args,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentWorkspaceDir: "/tmp/agent-workspace",
|
||||
workdir: "/workspace",
|
||||
workspaceAccess: "none",
|
||||
});
|
||||
|
||||
const mounts = args.filter((arg) => arg.startsWith("/tmp/"));
|
||||
expect(mounts).toEqual(["/tmp/workspace:/workspace:ro"]);
|
||||
});
|
||||
|
||||
it("omits agent workspace mount when paths are identical", () => {
|
||||
const args: string[] = [];
|
||||
appendWorkspaceMountArgs({
|
||||
args,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentWorkspaceDir: "/tmp/workspace",
|
||||
workdir: "/workspace",
|
||||
workspaceAccess: "rw",
|
||||
});
|
||||
|
||||
const mounts = args.filter((arg) => arg.startsWith("/tmp/"));
|
||||
expect(mounts).toEqual(["/tmp/workspace:/workspace"]);
|
||||
});
|
||||
});
|
||||
28
src/agents/sandbox/workspace-mounts.ts
Normal file
28
src/agents/sandbox/workspace-mounts.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { SANDBOX_AGENT_WORKSPACE_MOUNT } from "./constants.js";
|
||||
import type { SandboxWorkspaceAccess } from "./types.js";
|
||||
|
||||
function mainWorkspaceMountSuffix(access: SandboxWorkspaceAccess): "" | ":ro" {
|
||||
return access === "rw" ? "" : ":ro";
|
||||
}
|
||||
|
||||
function agentWorkspaceMountSuffix(access: SandboxWorkspaceAccess): "" | ":ro" {
|
||||
return access === "ro" ? ":ro" : "";
|
||||
}
|
||||
|
||||
export function appendWorkspaceMountArgs(params: {
|
||||
args: string[];
|
||||
workspaceDir: string;
|
||||
agentWorkspaceDir: string;
|
||||
workdir: string;
|
||||
workspaceAccess: SandboxWorkspaceAccess;
|
||||
}) {
|
||||
const { args, workspaceDir, agentWorkspaceDir, workdir, workspaceAccess } = params;
|
||||
|
||||
args.push("-v", `${workspaceDir}:${workdir}${mainWorkspaceMountSuffix(workspaceAccess)}`);
|
||||
if (workspaceAccess !== "none" && workspaceDir !== agentWorkspaceDir) {
|
||||
args.push(
|
||||
"-v",
|
||||
`${agentWorkspaceDir}:${SANDBOX_AGENT_WORKSPACE_MOUNT}${agentWorkspaceMountSuffix(workspaceAccess)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user