From 41ade360455d198202b5904e867f333a5efebd7a Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 1 Aug 2026 11:41:36 +0800 Subject: [PATCH] fix(sandbox): normalize protected mount destinations --- src/agents/sandbox/docker.config-hash-recreate.test.ts | 3 ++- src/agents/sandbox/fs-paths.test.ts | 2 ++ src/agents/sandbox/workspace-mounts.test.ts | 2 +- src/agents/sandbox/workspace-mounts.ts | 9 ++++++--- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/agents/sandbox/docker.config-hash-recreate.test.ts b/src/agents/sandbox/docker.config-hash-recreate.test.ts index c7b931edc85e..08950241f329 100644 --- a/src/agents/sandbox/docker.config-hash-recreate.test.ts +++ b/src/agents/sandbox/docker.config-hash-recreate.test.ts @@ -599,6 +599,7 @@ describe("ensureSandboxContainer config-hash recreation", () => { const customMount = `${customRoot}:/workspace/skills:rw`; const cfg = createSandboxConfig([], [customMount]); cfg.backend = backend; + cfg.docker.workdir = "/workspace/."; cfg.docker.dangerouslyAllowExternalBindSources = true; spawnState.inspectRunning = false; registryMocks.readRegistryEntry.mockResolvedValue(null); @@ -609,7 +610,7 @@ describe("ensureSandboxContainer config-hash recreation", () => { expect(createCall.command).toBe(backend); expect(bindArgs).not.toContain(customMount); - expect(bindArgs).toContain(`${path.join(workspaceDir, "skills")}:/workspace/skills:ro,z`); + expect(bindArgs).toContain(`${path.join(workspaceDir, "skills")}:/workspace/./skills:ro,z`); }, ); diff --git a/src/agents/sandbox/fs-paths.test.ts b/src/agents/sandbox/fs-paths.test.ts index ff00d6697801..bcbc05030ea8 100644 --- a/src/agents/sandbox/fs-paths.test.ts +++ b/src/agents/sandbox/fs-paths.test.ts @@ -214,8 +214,10 @@ describe("resolveSandboxFsPathWithMounts", () => { const sandbox = createSandbox({ workspaceDir, agentWorkspaceDir: workspaceDir, + containerWorkdir: "/workspace/.", docker: { ...createSandbox().docker, + workdir: "/workspace/.", binds: [`${customRoot}:/workspace/skills:rw`], }, }); diff --git a/src/agents/sandbox/workspace-mounts.test.ts b/src/agents/sandbox/workspace-mounts.test.ts index 15fd2db6bbcc..6b0d0bf76a88 100644 --- a/src/agents/sandbox/workspace-mounts.test.ts +++ b/src/agents/sandbox/workspace-mounts.test.ts @@ -265,7 +265,7 @@ describe("resolveProtectedSkillMountContainerPaths", () => { it("returns container paths from skill mounts", () => { const mounts: ReadOnlyWorkspaceSkillMount[] = [ { hostPath: "/host/skills", containerPath: "/workspace/skills" }, - { hostPath: "/host/.agents/skills", containerPath: "/workspace/.agents/skills" }, + { hostPath: "/host/.agents/skills", containerPath: "/workspace/./.agents/skills/" }, ]; const paths = resolveProtectedSkillMountContainerPaths(mounts); expect(paths).toEqual(new Set(["/workspace/skills", "/workspace/.agents/skills"])); diff --git a/src/agents/sandbox/workspace-mounts.ts b/src/agents/sandbox/workspace-mounts.ts index 4f0db23d047a..23cea49dd664 100644 --- a/src/agents/sandbox/workspace-mounts.ts +++ b/src/agents/sandbox/workspace-mounts.ts @@ -38,6 +38,10 @@ function containerJoin(root: string, ...parts: string[]): string { return suffix ? `${normalizedRoot}/${suffix}` : normalizedRoot; } +function normalizeMountContainerPath(containerPath: string): string { + return normalizeContainerPath(containerPath).replace(/\/+$/, "") || "/"; +} + /** Hidden workspace used to materialize non-workspace skills for rw sandboxes. */ export function resolveMaterializedSandboxSkillsWorkspaceDir(rootDir: string): string { return path.join(rootDir, ...MATERIALIZED_SANDBOX_SKILLS_WORKSPACE_PARTS); @@ -126,7 +130,7 @@ export function formatReadOnlyWorkspaceSkillMountHashState( export function resolveProtectedSkillMountContainerPaths( mounts: readonly ReadOnlyWorkspaceSkillMount[], ): Set { - return new Set(mounts.map((m) => m.containerPath)); + return new Set(mounts.map((mount) => normalizeMountContainerPath(mount.containerPath))); } /** @@ -151,8 +155,7 @@ export function filterBindsConflictingWithProtectedMounts( filtered.push(bind); continue; } - // Strip trailing slashes so /workspace/skills/ matches /workspace/skills from containerJoin. - const containerPath = normalizeContainerPath(spec.container).replace(/\/+$/, "") || "/"; + const containerPath = normalizeMountContainerPath(spec.container); if (!protectedContainerPaths.has(containerPath)) { filtered.push(bind); }