From d71ab44a0c2a63bfb52dd490609ff21452407668 Mon Sep 17 00:00:00 2001 From: juyaohuidt Date: Sun, 19 Jul 2026 05:53:31 +0800 Subject: [PATCH] fix(sandbox): bound sandbox seed bootstrap file reads (#110017) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(sandbox): bound sandbox seed bootstrap file reads ensureSandboxWorkspace copies each seed bootstrap file (AGENTS.md / SOUL.md / TOOLS.md / ...) into a fresh sandbox workspace with an unbounded readFileSync after the root-boundary open. The seed path comes from user config (seedFrom), so a misconfigured or oversized seed file forced an unbounded allocation at sandbox-creation time. Use readFileDescriptorBoundedSync with a 2 MiB limit (matching the workspace bootstrap file limit) and skip the oversized seed file with a warning instead of reading it all. * fix(sandbox): bound workspace seed reads Co-authored-by: 琚耀辉0668001366 * test(agents): cover async bootstrap read retries * test(agents): align bootstrap read mocks with main --------- Co-authored-by: Peter Steinberger --- src/agents/sandbox/workspace.test.ts | 41 +++++++++++++++++++++++++++- src/agents/sandbox/workspace.ts | 16 ++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) 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); }