Sandbox: add shared bind-aware fs path resolver

This commit is contained in:
Vignesh Natarajan
2026-02-14 16:53:43 -08:00
parent f18e3fba79
commit eafda6f526
3 changed files with 344 additions and 1 deletions

View File

@@ -0,0 +1,108 @@
import { describe, expect, it } from "vitest";
import type { SandboxContext } from "./types.js";
import {
buildSandboxFsMounts,
parseSandboxBindMount,
resolveSandboxFsPathWithMounts,
} from "./fs-paths.js";
function createSandbox(overrides?: Partial<SandboxContext>): SandboxContext {
return {
enabled: true,
sessionKey: "sandbox:test",
workspaceDir: "/tmp/workspace",
agentWorkspaceDir: "/tmp/workspace",
workspaceAccess: "rw",
containerName: "openclaw-sbx-test",
containerWorkdir: "/workspace",
docker: {
image: "openclaw-sandbox:bookworm-slim",
containerPrefix: "openclaw-sbx-",
network: "none",
user: "1000:1000",
workdir: "/workspace",
readOnlyRoot: false,
tmpfs: [],
capDrop: [],
seccompProfile: "",
apparmorProfile: "",
setupCommand: "",
binds: [],
dns: [],
extraHosts: [],
pidsLimit: 0,
},
tools: { allow: ["*"], deny: [] },
browserAllowHostControl: false,
...overrides,
};
}
describe("parseSandboxBindMount", () => {
it("parses bind mode and writeability", () => {
expect(parseSandboxBindMount("/tmp/a:/workspace-a:ro")).toEqual({
hostRoot: "/tmp/a",
containerRoot: "/workspace-a",
writable: false,
});
expect(parseSandboxBindMount("/tmp/b:/workspace-b:rw")).toEqual({
hostRoot: "/tmp/b",
containerRoot: "/workspace-b",
writable: true,
});
});
});
describe("resolveSandboxFsPathWithMounts", () => {
it("maps mounted container absolute paths to host paths", () => {
const sandbox = createSandbox({
docker: {
...createSandbox().docker,
binds: ["/tmp/workspace-two:/workspace-two:ro"],
},
});
const mounts = buildSandboxFsMounts(sandbox);
const resolved = resolveSandboxFsPathWithMounts({
filePath: "/workspace-two/docs/AGENTS.md",
cwd: sandbox.workspaceDir,
defaultWorkspaceRoot: sandbox.workspaceDir,
defaultContainerRoot: sandbox.containerWorkdir,
mounts,
});
expect(resolved.hostPath).toBe("/tmp/workspace-two/docs/AGENTS.md");
expect(resolved.containerPath).toBe("/workspace-two/docs/AGENTS.md");
expect(resolved.relativePath).toBe("/workspace-two/docs/AGENTS.md");
expect(resolved.writable).toBe(false);
});
it("keeps workspace-relative display paths for default workspace files", () => {
const sandbox = createSandbox();
const mounts = buildSandboxFsMounts(sandbox);
const resolved = resolveSandboxFsPathWithMounts({
filePath: "src/index.ts",
cwd: sandbox.workspaceDir,
defaultWorkspaceRoot: sandbox.workspaceDir,
defaultContainerRoot: sandbox.containerWorkdir,
mounts,
});
expect(resolved.hostPath).toBe("/tmp/workspace/src/index.ts");
expect(resolved.containerPath).toBe("/workspace/src/index.ts");
expect(resolved.relativePath).toBe("src/index.ts");
expect(resolved.writable).toBe(true);
});
it("preserves legacy sandbox-root error for outside paths", () => {
const sandbox = createSandbox();
const mounts = buildSandboxFsMounts(sandbox);
expect(() =>
resolveSandboxFsPathWithMounts({
filePath: "/etc/passwd",
cwd: sandbox.workspaceDir,
defaultWorkspaceRoot: sandbox.workspaceDir,
defaultContainerRoot: sandbox.containerWorkdir,
mounts,
}),
).toThrow(/Path escapes sandbox root/);
});
});