diff --git a/src/agents/sandbox-paths.test.ts b/src/agents/sandbox-paths.test.ts index 328366860..0969c8550 100644 --- a/src/agents/sandbox-paths.test.ts +++ b/src/agents/sandbox-paths.test.ts @@ -1,10 +1,54 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { pathToFileURL } from "node:url"; import { describe, expect, it } from "vitest"; import { resolveSandboxedMediaSource } from "./sandbox-paths.js"; describe("resolveSandboxedMediaSource", () => { + // Group 1: /tmp paths (the bug fix) + it("allows absolute paths under os.tmpdir()", async () => { + const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); + try { + const result = await resolveSandboxedMediaSource({ + media: path.join(os.tmpdir(), "image.png"), + sandboxRoot: sandboxDir, + }); + expect(result).toBe(path.join(os.tmpdir(), "image.png")); + } finally { + await fs.rm(sandboxDir, { recursive: true, force: true }); + } + }); + + it("allows file:// URLs pointing to os.tmpdir()", async () => { + const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); + try { + const tmpFile = path.join(os.tmpdir(), "photo.png"); + const fileUrl = pathToFileURL(tmpFile).href; + const result = await resolveSandboxedMediaSource({ + media: fileUrl, + sandboxRoot: sandboxDir, + }); + expect(result).toBe(tmpFile); + } finally { + await fs.rm(sandboxDir, { recursive: true, force: true }); + } + }); + + it("allows nested paths under os.tmpdir()", async () => { + const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); + try { + const result = await resolveSandboxedMediaSource({ + media: path.join(os.tmpdir(), "subdir", "deep", "file.png"), + sandboxRoot: sandboxDir, + }); + expect(result).toBe(path.join(os.tmpdir(), "subdir", "deep", "file.png")); + } finally { + await fs.rm(sandboxDir, { recursive: true, force: true }); + } + }); + + // Group 2: Sandbox-relative paths (existing behavior) it("resolves sandbox-relative paths", async () => { const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); try { @@ -18,7 +62,8 @@ describe("resolveSandboxedMediaSource", () => { } }); - it("rejects paths outside sandbox root", async () => { + // Group 3: Rejections (security) + it("rejects paths outside sandbox root and tmpdir", async () => { const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); try { await expect( @@ -71,6 +116,7 @@ describe("resolveSandboxedMediaSource", () => { } }); + // Group 4: Passthrough it("passes HTTP URLs through unchanged", async () => { const result = await resolveSandboxedMediaSource({ media: "https://example.com/image.png", diff --git a/src/agents/sandbox-paths.ts b/src/agents/sandbox-paths.ts index c5547291c..8dbe822d3 100644 --- a/src/agents/sandbox-paths.ts +++ b/src/agents/sandbox-paths.ts @@ -90,12 +90,18 @@ export async function resolveSandboxedMediaSource(params: { throw new Error(`Invalid file:// URL for sandboxed media: ${raw}`); } } - const resolved = await assertSandboxPath({ + // Allow files under os.tmpdir() — consistent with buildMediaLocalRoots() defaults. + const resolved = path.resolve(params.sandboxRoot, candidate); + const tmpDir = os.tmpdir(); + if (resolved === tmpDir || resolved.startsWith(tmpDir + path.sep)) { + return resolved; + } + const sandboxResult = await assertSandboxPath({ filePath: candidate, cwd: params.sandboxRoot, root: params.sandboxRoot, }); - return resolved.resolved; + return sandboxResult.resolved; } async function assertNoSymlinkEscape(