diff --git a/src/agents/sandbox/workspace.test.ts b/src/agents/sandbox/workspace.test.ts index 0ca9d174682b..1f94da9918a5 100644 --- a/src/agents/sandbox/workspace.test.ts +++ b/src/agents/sandbox/workspace.test.ts @@ -4,7 +4,8 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { DEFAULT_AGENTS_FILENAME } from "../workspace.js"; +import { MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES } from "../workspace-bootstrap-read.js"; +import { DEFAULT_AGENTS_FILENAME, DEFAULT_TOOLS_FILENAME } from "../workspace.js"; import { ensureSandboxWorkspace } from "./workspace.js"; const tempRoots: string[] = []; @@ -77,4 +78,42 @@ describe("ensureSandboxWorkspace", () => { "no such file", ); }); + + it("skips an oversized seed file but still seeds the others", async () => { + // An unbounded read would copy the oversized file through; the bound skips it. + const root = await makeTempRoot(); + const seed = path.join(root, "seed"); + const sandbox = path.join(root, "sandbox"); + await fs.mkdir(seed, { recursive: true }); + await fs.writeFile( + path.join(seed, DEFAULT_AGENTS_FILENAME), + `## Startup\n\n` + "x".repeat(MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES), + "utf-8", + ); + await fs.writeFile(path.join(seed, DEFAULT_TOOLS_FILENAME), "seeded-tools", "utf-8"); + + await ensureSandboxWorkspace(sandbox, seed, true); + + await expect(fs.readFile(path.join(sandbox, DEFAULT_AGENTS_FILENAME), "utf-8")).rejects.toThrow( + "no such file", + ); + await expect(fs.readFile(path.join(sandbox, DEFAULT_TOOLS_FILENAME), "utf-8")).resolves.toBe( + "seeded-tools", + ); + }); + + it("seeds a bootstrap file at the byte read limit", async () => { + const root = await makeTempRoot(); + const seed = path.join(root, "seed"); + const sandbox = path.join(root, "sandbox"); + await fs.mkdir(seed, { recursive: true }); + const content = "## Startup\n\nDo startup things.\n"; + const padding = "x".repeat(MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES - content.length); + await fs.writeFile(path.join(seed, DEFAULT_AGENTS_FILENAME), content + padding, "utf-8"); + + await ensureSandboxWorkspace(sandbox, seed, true); + + const seeded = await fs.readFile(path.join(sandbox, DEFAULT_AGENTS_FILENAME), "utf-8"); + expect(seeded).toContain("Do startup things"); + }); }); diff --git a/src/agents/sandbox/workspace.ts b/src/agents/sandbox/workspace.ts index 11d975de18e1..b81f9f354591 100644 --- a/src/agents/sandbox/workspace.ts +++ b/src/agents/sandbox/workspace.ts @@ -8,7 +8,12 @@ import fs from "node:fs/promises"; import path from "node:path"; import type { OptionalBootstrapFileName } from "../../config/types.agent-defaults.js"; import { openRootFile } from "../../infra/boundary-file-read.js"; +import { createSubsystemLogger } from "../../logging/subsystem.js"; import { resolveUserPath } from "../../utils.js"; +import { + MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES, + readWorkspaceBootstrapFile, +} from "../workspace-bootstrap-read.js"; import { DEFAULT_AGENTS_FILENAME, DEFAULT_BOOTSTRAP_FILENAME, @@ -20,6 +25,8 @@ import { ensureAgentWorkspace, } from "../workspace.js"; +const log = createSubsystemLogger("sandbox-workspace"); + export async function ensureSandboxWorkspace( workspaceDir: string, seedFrom?: string, @@ -54,8 +61,15 @@ export async function ensureSandboxWorkspace( continue; } try { - const content = syncFs.readFileSync(opened.fd, "utf-8"); + const content = await readWorkspaceBootstrapFile(opened.fd); await fs.writeFile(dest, content, { encoding: "utf-8", flag: "wx" }); + } catch (err) { + if (err instanceof RangeError) { + log.warn( + `Ignoring oversized sandbox seed file ${src}: file exceeds the ${MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES}-byte limit`, + ); + } + // ignore missing or oversized seed file } finally { syncFs.closeSync(opened.fd); }