mirror of https://github.com/openclaw/openclaw.git
fix(sandbox): relabel managed workspace mounts for SELinux (#58025)
This commit is contained in:
parent
e89bd883d8
commit
7516b423eb
|
|
@ -1,5 +1,6 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { collectDockerFlagValues, findDockerArgsCall } from "./test-args.js";
|
||||
import { SANDBOX_MOUNT_FORMAT_VERSION } from "./workspace-mounts.js";
|
||||
import type { SandboxConfig } from "./types.js";
|
||||
|
||||
let BROWSER_BRIDGES: Map<string, unknown>;
|
||||
|
|
@ -168,7 +169,7 @@ describe("ensureSandboxBrowser create args", () => {
|
|||
entry.startsWith("OPENCLAW_BROWSER_NOVNC_PASSWORD="),
|
||||
);
|
||||
expect(passwordEntry).toMatch(/^OPENCLAW_BROWSER_NOVNC_PASSWORD=[A-Za-z0-9]{8}$/);
|
||||
expect(result?.noVncUrl).toMatch(/^http:\/\/127\.0\.0\.1:19000\/sandbox\/novnc\?token=/);
|
||||
expect(result?.noVncUrl).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/sandbox\/novnc\?token=/);
|
||||
expect(result?.noVncUrl).not.toContain("password=");
|
||||
});
|
||||
|
||||
|
|
@ -202,7 +203,7 @@ describe("ensureSandboxBrowser create args", () => {
|
|||
const createArgs = findDockerArgsCall(dockerMocks.execDocker.mock.calls, "create");
|
||||
|
||||
expect(createArgs).toBeDefined();
|
||||
expect(createArgs).toContain("/tmp/workspace:/workspace:ro");
|
||||
expect(createArgs).toContain("/tmp/workspace:/workspace:ro,z");
|
||||
});
|
||||
|
||||
it("keeps the main workspace writable when workspaceAccess is rw", async () => {
|
||||
|
|
@ -219,7 +220,20 @@ describe("ensureSandboxBrowser create args", () => {
|
|||
const createArgs = findDockerArgsCall(dockerMocks.execDocker.mock.calls, "create");
|
||||
|
||||
expect(createArgs).toBeDefined();
|
||||
expect(createArgs).toContain("/tmp/workspace:/workspace");
|
||||
expect(createArgs).not.toContain("/tmp/workspace:/workspace:ro");
|
||||
expect(createArgs).toContain("/tmp/workspace:/workspace:z");
|
||||
expect(createArgs).not.toContain("/tmp/workspace:/workspace:ro,z");
|
||||
});
|
||||
|
||||
it("stamps the mount format version label on browser containers", async () => {
|
||||
await ensureSandboxBrowser({
|
||||
scopeKey: "session:test",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentWorkspaceDir: "/tmp/workspace",
|
||||
cfg: buildConfig(false),
|
||||
});
|
||||
|
||||
const createArgs = findDockerArgsCall(dockerMocks.execDocker.mock.calls, "create");
|
||||
const labels = collectDockerFlagValues(createArgs ?? [], "--label");
|
||||
expect(labels).toContain(`openclaw.mountFormatVersion=${SANDBOX_MOUNT_FORMAT_VERSION}`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,7 +35,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";
|
||||
import { appendWorkspaceMountArgs, SANDBOX_MOUNT_FORMAT_VERSION } from "./workspace-mounts.js";
|
||||
|
||||
const HOT_BROWSER_WINDOW_MS = 5 * 60 * 1000;
|
||||
const CDP_SOURCE_RANGE_ENV_KEY = "OPENCLAW_BROWSER_CDP_SOURCE_RANGE";
|
||||
|
|
@ -167,6 +167,7 @@ export async function ensureSandboxBrowser(params: {
|
|||
workspaceAccess: params.cfg.workspaceAccess,
|
||||
workspaceDir: params.workspaceDir,
|
||||
agentWorkspaceDir: params.agentWorkspaceDir,
|
||||
mountFormatVersion: SANDBOX_MOUNT_FORMAT_VERSION,
|
||||
});
|
||||
|
||||
const now = Date.now();
|
||||
|
|
@ -270,6 +271,7 @@ export async function ensureSandboxBrowser(params: {
|
|||
args.push(browserImage);
|
||||
await execDocker(args);
|
||||
await execDocker(["start", containerName]);
|
||||
} else if (!running) {
|
||||
} else if (!running) {
|
||||
await execDocker(["start", containerName]);
|
||||
}
|
||||
|
|
@ -333,8 +335,8 @@ export async function ensureSandboxBrowser(params: {
|
|||
|
||||
const onEnsureAttachTarget = params.cfg.browser.autoStart
|
||||
? async () => {
|
||||
const state = await dockerContainerState(containerName);
|
||||
if (state.exists && !state.running) {
|
||||
const currentState = await dockerContainerState(containerName);
|
||||
if (currentState.exists && !currentState.running) {
|
||||
await execDocker(["start", containerName]);
|
||||
}
|
||||
const ok = await waitForSandboxCdp({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { computeSandboxBrowserConfigHash, computeSandboxConfigHash } from "./config-hash.js";
|
||||
import { SANDBOX_MOUNT_FORMAT_VERSION } from "./workspace-mounts.js";
|
||||
import type { SandboxDockerConfig } from "./types.js";
|
||||
|
||||
function createDockerConfig(overrides?: Partial<SandboxDockerConfig>): SandboxDockerConfig {
|
||||
|
|
@ -59,6 +60,7 @@ describe("computeSandboxConfigHash", () => {
|
|||
workspaceAccess: "rw" as const,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentWorkspaceDir: "/tmp/workspace",
|
||||
mountFormatVersion: SANDBOX_MOUNT_FORMAT_VERSION,
|
||||
};
|
||||
const left = computeSandboxConfigHash({
|
||||
...shared,
|
||||
|
|
@ -88,6 +90,7 @@ describe("computeSandboxConfigHash", () => {
|
|||
workspaceAccess: "rw" as const,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentWorkspaceDir: "/tmp/workspace",
|
||||
mountFormatVersion: SANDBOX_MOUNT_FORMAT_VERSION,
|
||||
};
|
||||
const left = computeSandboxConfigHash({
|
||||
...shared,
|
||||
|
|
@ -120,6 +123,7 @@ describe("computeSandboxBrowserConfigHash", () => {
|
|||
workspaceAccess: "rw" as const,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentWorkspaceDir: "/tmp/workspace",
|
||||
mountFormatVersion: SANDBOX_MOUNT_FORMAT_VERSION,
|
||||
};
|
||||
const left = computeSandboxBrowserConfigHash({
|
||||
...shared,
|
||||
|
|
@ -150,6 +154,7 @@ describe("computeSandboxBrowserConfigHash", () => {
|
|||
workspaceAccess: "rw" as const,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentWorkspaceDir: "/tmp/workspace",
|
||||
mountFormatVersion: SANDBOX_MOUNT_FORMAT_VERSION,
|
||||
};
|
||||
const left = computeSandboxBrowserConfigHash({
|
||||
...shared,
|
||||
|
|
@ -176,6 +181,7 @@ describe("computeSandboxBrowserConfigHash", () => {
|
|||
workspaceAccess: "rw" as const,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentWorkspaceDir: "/tmp/workspace",
|
||||
mountFormatVersion: SANDBOX_MOUNT_FORMAT_VERSION,
|
||||
};
|
||||
const left = computeSandboxBrowserConfigHash({
|
||||
...shared,
|
||||
|
|
@ -187,4 +193,31 @@ describe("computeSandboxBrowserConfigHash", () => {
|
|||
});
|
||||
expect(left).not.toBe(right);
|
||||
});
|
||||
|
||||
it("changes when mount format version changes", () => {
|
||||
const shared = {
|
||||
docker: createDockerConfig(),
|
||||
browser: {
|
||||
cdpPort: 9222,
|
||||
cdpSourceRange: undefined,
|
||||
vncPort: 5900,
|
||||
noVncPort: 6080,
|
||||
headless: false,
|
||||
enableNoVnc: true,
|
||||
},
|
||||
securityEpoch: "epoch-v1",
|
||||
workspaceAccess: "rw" as const,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentWorkspaceDir: "/tmp/workspace",
|
||||
};
|
||||
const left = computeSandboxBrowserConfigHash({
|
||||
...shared,
|
||||
mountFormatVersion: SANDBOX_MOUNT_FORMAT_VERSION,
|
||||
});
|
||||
const right = computeSandboxBrowserConfigHash({
|
||||
...shared,
|
||||
mountFormatVersion: SANDBOX_MOUNT_FORMAT_VERSION - 1,
|
||||
});
|
||||
expect(left).not.toBe(right);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ type SandboxHashInput = {
|
|||
workspaceAccess: SandboxWorkspaceAccess;
|
||||
workspaceDir: string;
|
||||
agentWorkspaceDir: string;
|
||||
mountFormatVersion: number;
|
||||
};
|
||||
|
||||
type SandboxBrowserHashInput = {
|
||||
|
|
@ -18,6 +19,7 @@ type SandboxBrowserHashInput = {
|
|||
workspaceAccess: SandboxWorkspaceAccess;
|
||||
workspaceDir: string;
|
||||
agentWorkspaceDir: string;
|
||||
mountFormatVersion: number;
|
||||
};
|
||||
|
||||
function normalizeForHash(value: unknown): unknown {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Readable } from "node:stream";
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { computeSandboxConfigHash } from "./config-hash.js";
|
||||
import { collectDockerFlagValues } from "./test-args.js";
|
||||
import { SANDBOX_MOUNT_FORMAT_VERSION } from "./workspace-mounts.js";
|
||||
import type { SandboxConfig } from "./types.js";
|
||||
|
||||
type SpawnCall = {
|
||||
|
|
@ -183,12 +184,14 @@ describe("ensureSandboxContainer config-hash recreation", () => {
|
|||
workspaceAccess: oldCfg.workspaceAccess,
|
||||
workspaceDir,
|
||||
agentWorkspaceDir: workspaceDir,
|
||||
mountFormatVersion: SANDBOX_MOUNT_FORMAT_VERSION,
|
||||
});
|
||||
const newHash = computeSandboxConfigHash({
|
||||
docker: newCfg.docker,
|
||||
workspaceAccess: newCfg.workspaceAccess,
|
||||
workspaceDir,
|
||||
agentWorkspaceDir: workspaceDir,
|
||||
mountFormatVersion: SANDBOX_MOUNT_FORMAT_VERSION,
|
||||
});
|
||||
expect(newHash).not.toBe(oldHash);
|
||||
|
||||
|
|
@ -244,6 +247,7 @@ describe("ensureSandboxContainer config-hash recreation", () => {
|
|||
workspaceAccess: cfg.workspaceAccess,
|
||||
workspaceDir,
|
||||
agentWorkspaceDir: workspaceDir,
|
||||
mountFormatVersion: SANDBOX_MOUNT_FORMAT_VERSION,
|
||||
});
|
||||
|
||||
spawnState.inspectRunning = false;
|
||||
|
|
@ -275,16 +279,16 @@ describe("ensureSandboxContainer config-hash recreation", () => {
|
|||
expect(createCall?.args).toContain(`openclaw.configHash=${expectedHash}`);
|
||||
|
||||
const bindArgs = collectDockerFlagValues(createCall?.args ?? [], "-v");
|
||||
const workspaceMountIdx = bindArgs.indexOf("/tmp/workspace:/workspace");
|
||||
const workspaceMountIdx = bindArgs.indexOf("/tmp/workspace:/workspace:z");
|
||||
const customMountIdx = bindArgs.indexOf("/tmp/workspace-shared/USER.md:/workspace/USER.md:ro");
|
||||
expect(workspaceMountIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(customMountIdx).toBeGreaterThan(workspaceMountIdx);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ workspaceAccess: "rw" as const, expectedMainMount: "/tmp/workspace:/workspace" },
|
||||
{ workspaceAccess: "ro" as const, expectedMainMount: "/tmp/workspace:/workspace:ro" },
|
||||
{ workspaceAccess: "none" as const, expectedMainMount: "/tmp/workspace:/workspace:ro" },
|
||||
{ workspaceAccess: "rw" as const, expectedMainMount: "/tmp/workspace:/workspace:z" },
|
||||
{ workspaceAccess: "ro" as const, expectedMainMount: "/tmp/workspace:/workspace:ro,z" },
|
||||
{ workspaceAccess: "none" as const, expectedMainMount: "/tmp/workspace:/workspace:ro,z" },
|
||||
])(
|
||||
"uses expected main mount permissions when workspaceAccess=$workspaceAccess",
|
||||
async ({ workspaceAccess, expectedMainMount }) => {
|
||||
|
|
@ -312,4 +316,26 @@ describe("ensureSandboxContainer config-hash recreation", () => {
|
|||
expect(bindArgs).toContain(expectedMainMount);
|
||||
},
|
||||
);
|
||||
|
||||
it("stamps the mount format version label on created containers", async () => {
|
||||
const workspaceDir = "/tmp/workspace";
|
||||
const cfg = createSandboxConfig([]);
|
||||
|
||||
spawnState.inspectRunning = false;
|
||||
spawnState.labelHash = "";
|
||||
registryMocks.readRegistry.mockResolvedValue({ entries: [] });
|
||||
|
||||
await ensureSandboxContainer({
|
||||
sessionKey: "agent:main:session-1",
|
||||
workspaceDir,
|
||||
agentWorkspaceDir: workspaceDir,
|
||||
cfg,
|
||||
});
|
||||
|
||||
const createCall = spawnState.calls.find(
|
||||
(call) => call.command === "docker" && call.args[0] === "create",
|
||||
);
|
||||
expect(createCall).toBeDefined();
|
||||
expect(createCall?.args).toContain(`openclaw.mountFormatVersion=${SANDBOX_MOUNT_FORMAT_VERSION}`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ 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";
|
||||
import { appendWorkspaceMountArgs, SANDBOX_MOUNT_FORMAT_VERSION } from "./workspace-mounts.js";
|
||||
|
||||
const log = createSubsystemLogger("docker");
|
||||
|
||||
|
|
@ -348,6 +348,7 @@ export function buildSandboxCreateArgs(params: {
|
|||
args.push("--label", "openclaw.sandbox=1");
|
||||
args.push("--label", `openclaw.sessionKey=${params.scopeKey}`);
|
||||
args.push("--label", `openclaw.createdAtMs=${createdAtMs}`);
|
||||
args.push("--label", `openclaw.mountFormatVersion=${SANDBOX_MOUNT_FORMAT_VERSION}`);
|
||||
if (params.configHash) {
|
||||
args.push("--label", `openclaw.configHash=${params.configHash}`);
|
||||
}
|
||||
|
|
@ -504,6 +505,7 @@ export async function ensureSandboxContainer(params: {
|
|||
workspaceAccess: params.cfg.workspaceAccess,
|
||||
workspaceDir: params.workspaceDir,
|
||||
agentWorkspaceDir: params.agentWorkspaceDir,
|
||||
mountFormatVersion: SANDBOX_MOUNT_FORMAT_VERSION,
|
||||
});
|
||||
const now = Date.now();
|
||||
const state = await dockerContainerState(containerName);
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ 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" },
|
||||
{ access: "rw" as const, expected: "/tmp/workspace:/workspace:z" },
|
||||
{ access: "ro" as const, expected: "/tmp/workspace:/workspace:ro,z" },
|
||||
{ access: "none" as const, expected: "/tmp/workspace:/workspace:ro,z" },
|
||||
])("sets main mount permissions for workspaceAccess=$access", ({ access, expected }) => {
|
||||
const args: string[] = [];
|
||||
appendWorkspaceMountArgs({
|
||||
|
|
@ -30,7 +30,7 @@ describe("appendWorkspaceMountArgs", () => {
|
|||
});
|
||||
|
||||
const mounts = args.filter((arg) => arg.startsWith("/tmp/"));
|
||||
expect(mounts).toEqual(["/tmp/workspace:/workspace:ro"]);
|
||||
expect(mounts).toEqual(["/tmp/workspace:/workspace:ro,z"]);
|
||||
});
|
||||
|
||||
it("omits agent workspace mount when paths are identical", () => {
|
||||
|
|
@ -44,6 +44,23 @@ describe("appendWorkspaceMountArgs", () => {
|
|||
});
|
||||
|
||||
const mounts = args.filter((arg) => arg.startsWith("/tmp/"));
|
||||
expect(mounts).toEqual(["/tmp/workspace:/workspace"]);
|
||||
expect(mounts).toEqual(["/tmp/workspace:/workspace:z"]);
|
||||
});
|
||||
|
||||
it("marks split agent workspace mounts shared for SELinux", () => {
|
||||
const args: string[] = [];
|
||||
appendWorkspaceMountArgs({
|
||||
args,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentWorkspaceDir: "/tmp/agent-workspace",
|
||||
workdir: "/workspace",
|
||||
workspaceAccess: "ro",
|
||||
});
|
||||
|
||||
const mounts = args.filter((arg) => arg.startsWith("/tmp/"));
|
||||
expect(mounts).toEqual([
|
||||
"/tmp/workspace:/workspace:ro,z",
|
||||
"/tmp/agent-workspace:/agent:ro,z",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { SANDBOX_AGENT_WORKSPACE_MOUNT } from "./constants.js";
|
||||
import type { SandboxWorkspaceAccess } from "./types.js";
|
||||
|
||||
function mainWorkspaceMountSuffix(access: SandboxWorkspaceAccess): "" | ":ro" {
|
||||
return access === "rw" ? "" : ":ro";
|
||||
}
|
||||
export const SANDBOX_MOUNT_FORMAT_VERSION = 2;
|
||||
|
||||
function agentWorkspaceMountSuffix(access: SandboxWorkspaceAccess): "" | ":ro" {
|
||||
return access === "ro" ? ":ro" : "";
|
||||
function formatManagedWorkspaceBind(params: {
|
||||
hostPath: string;
|
||||
containerPath: string;
|
||||
readOnly: boolean;
|
||||
}): string {
|
||||
return `${params.hostPath}:${params.containerPath}:${params.readOnly ? "ro,z" : "z"}`;
|
||||
}
|
||||
|
||||
export function appendWorkspaceMountArgs(params: {
|
||||
|
|
@ -18,11 +20,22 @@ export function appendWorkspaceMountArgs(params: {
|
|||
}) {
|
||||
const { args, workspaceDir, agentWorkspaceDir, workdir, workspaceAccess } = params;
|
||||
|
||||
args.push("-v", `${workspaceDir}:${workdir}${mainWorkspaceMountSuffix(workspaceAccess)}`);
|
||||
args.push(
|
||||
"-v",
|
||||
formatManagedWorkspaceBind({
|
||||
hostPath: workspaceDir,
|
||||
containerPath: workdir,
|
||||
readOnly: workspaceAccess !== "rw",
|
||||
}),
|
||||
);
|
||||
if (workspaceAccess !== "none" && workspaceDir !== agentWorkspaceDir) {
|
||||
args.push(
|
||||
"-v",
|
||||
`${agentWorkspaceDir}:${SANDBOX_AGENT_WORKSPACE_MOUNT}${agentWorkspaceMountSuffix(workspaceAccess)}`,
|
||||
formatManagedWorkspaceBind({
|
||||
hostPath: agentWorkspaceDir,
|
||||
containerPath: SANDBOX_AGENT_WORKSPACE_MOUNT,
|
||||
readOnly: workspaceAccess === "ro",
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue