fix(sandbox): bound sandbox seed bootstrap file reads (#110017)

* 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 <ju.yaohui@xydigit.com>

* test(agents): cover async bootstrap read retries

* test(agents): align bootstrap read mocks with main

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
juyaohuidt
2026-07-19 05:53:31 +08:00
committed by GitHub
parent 87de81a5fd
commit d71ab44a0c
2 changed files with 55 additions and 2 deletions

View File

@@ -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");
});
});

View File

@@ -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);
}