diff --git a/CHANGELOG.md b/CHANGELOG.md index c3ddad56e80..cf84c589d51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/agents/sandbox/browser.create.test.ts b/src/agents/sandbox/browser.create.test.ts index 2e83737ae57..7962b0909ce 100644 --- a/src/agents/sandbox/browser.create.test.ts +++ b/src/agents/sandbox/browser.create.test.ts @@ -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"); + }); }); diff --git a/src/agents/sandbox/docker.config-hash-recreate.test.ts b/src/agents/sandbox/docker.config-hash-recreate.test.ts index 1664cb16a03..289bbef49e1 100644 --- a/src/agents/sandbox/docker.config-hash-recreate.test.ts +++ b/src/agents/sandbox/docker.config-hash-recreate.test.ts @@ -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); + }, + ); });