fix: enforce sandbox workspace mount mode (#32227) (thanks @guanyu-zhang)

This commit is contained in:
Peter Steinberger
2026-03-02 22:58:02 +00:00
parent 7cbcbbc642
commit 02eeb08e04
3 changed files with 85 additions and 2 deletions

View File

@@ -27,6 +27,8 @@ Docs: https://docs.openclaw.ai
### Fixes
- Sandbox/workspace mount permissions: make primary `/workspace` bind mounts read-only whenever `workspaceAccess` is not `rw` (including `none`) across both core sandbox container and sandbox browser create flows. (#32227) Thanks @guanyu-zhang.
- Signal/message actions: allow `react` to fall back to `toolContext.currentMessageId` when `messageId` is omitted, matching Telegram behavior and unblocking agent-initiated reactions on inbound turns. (#32217) Thanks @dunamismax.
- Gateway/OpenAI chat completions: honor `x-openclaw-message-channel` when building `agentCommand` input for `/v1/chat/completions`, preserving caller channel identity instead of forcing `webchat`. (#30462) Thanks @bmendonca3.
- Secrets/exec resolver timeout defaults: use provider `timeoutMs` as the default inactivity (`noOutputTimeoutMs`) watchdog for exec secret providers, preventing premature no-output kills for resolvers that start producing output after 2s. (#32235) Thanks @bmendonca3.
- Feishu/File upload filenames: percent-encode non-ASCII/special-character `file_name` values in Feishu multipart uploads so Chinese/symbol-heavy filenames are sent as proper attachments instead of plain text links. (#31179) Thanks @Kay-051.

View File

@@ -184,4 +184,43 @@ describe("ensureSandboxBrowser create args", () => {
);
expect(result?.noVncUrl).toBeUndefined();
});
it("mounts the main workspace read-only when workspaceAccess is none", async () => {
const cfg = buildConfig(false);
cfg.workspaceAccess = "none";
await ensureSandboxBrowser({
scopeKey: "session:test",
workspaceDir: "/tmp/workspace",
agentWorkspaceDir: "/tmp/workspace",
cfg,
});
const createArgs = dockerMocks.execDocker.mock.calls.find(
(call: unknown[]) => Array.isArray(call[0]) && call[0][0] === "create",
)?.[0] as string[] | undefined;
expect(createArgs).toBeDefined();
expect(createArgs).toContain("/tmp/workspace:/workspace:ro");
});
it("keeps the main workspace writable when workspaceAccess is rw", async () => {
const cfg = buildConfig(false);
cfg.workspaceAccess = "rw";
await ensureSandboxBrowser({
scopeKey: "session:test",
workspaceDir: "/tmp/workspace",
agentWorkspaceDir: "/tmp/workspace",
cfg,
});
const createArgs = dockerMocks.execDocker.mock.calls.find(
(call: unknown[]) => Array.isArray(call[0]) && call[0][0] === "create",
)?.[0] as string[] | undefined;
expect(createArgs).toBeDefined();
expect(createArgs).toContain("/tmp/workspace:/workspace");
expect(createArgs).not.toContain("/tmp/workspace:/workspace:ro");
});
});

View File

@@ -83,11 +83,15 @@ vi.mock("node:child_process", async (importOriginal) => {
};
});
function createSandboxConfig(dns: string[], binds?: string[]): SandboxConfig {
function createSandboxConfig(
dns: string[],
binds?: string[],
workspaceAccess: "rw" | "ro" | "none" = "rw",
): SandboxConfig {
return {
mode: "all",
scope: "shared",
workspaceAccess: "rw",
workspaceAccess,
workspaceRoot: "~/.openclaw/sandboxes",
docker: {
image: "openclaw-sandbox:test",
@@ -245,4 +249,42 @@ describe("ensureSandboxContainer config-hash recreation", () => {
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" },
])(
"uses expected main mount permissions when workspaceAccess=$workspaceAccess",
async ({ workspaceAccess, expectedMainMount }) => {
const workspaceDir = "/tmp/workspace";
const cfg = createSandboxConfig([], undefined, workspaceAccess);
spawnState.inspectRunning = false;
spawnState.labelHash = "";
registryMocks.readRegistry.mockResolvedValue({ entries: [] });
registryMocks.updateRegistry.mockResolvedValue(undefined);
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();
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]);
}
}
expect(bindArgs).toContain(expectedMainMount);
},
);
});