mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-13 19:10:39 +00:00
fix: enforce sandbox workspace mount mode (#32227) (thanks @guanyu-zhang)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user