Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions src/tools/platform/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,27 @@ interface WriteInput {
description?: string;
}

function handleWrite(registryPath: string, filesDir: string, args: WriteInput): object {
function handleWrite(
registryPath: string,
filesDir: string,
args: WriteInput,
maxFileSize: number,
): object {
ensureDir(filesDir);

const id = generateId();
const decoded = Buffer.from(args.base64_data, "base64");

// Defense-in-depth: enforce the configured per-file size limit at the point
// of write. The API body-limit middleware caps HTTP uploads, but
// agent-initiated tool calls reach this function without traversing that
// middleware — so the limit must hold here independently.
if (decoded.length > maxFileSize) {
throw new Error(
`File "${args.filename}" (${decoded.length} bytes) exceeds limit of ${maxFileSize} bytes`,
);
}

const filePath = join(filesDir, `${id}_${args.filename}`);

writeFileSync(filePath, decoded);
Expand Down Expand Up @@ -450,7 +466,10 @@ export function createFilesSource(runtime: Runtime): InlineSource {
handler: async (input: Record<string, unknown>): Promise<ToolResult> => {
try {
const { filesDir, registryPath } = getFilePaths();
return ok(handleWrite(registryPath, filesDir, input as unknown as WriteInput));
const { maxFileSize } = runtime.getFilesConfig();
return ok(
handleWrite(registryPath, filesDir, input as unknown as WriteInput, maxFileSize),
);
} catch (err) {
return fail(err instanceof Error ? err.message : String(err));
}
Expand Down
83 changes: 83 additions & 0 deletions test/unit/tools/platform-files.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { FileConfig } from "../../../src/files/types.ts";
import { DEFAULT_FILE_CONFIG } from "../../../src/files/types.ts";
import type { Runtime } from "../../../src/runtime/runtime.ts";
import { createFilesSource } from "../../../src/tools/platform/files.ts";

function makeStubRuntime(workDir: string, config: Partial<FileConfig> = {}): Runtime {
const merged = { ...DEFAULT_FILE_CONFIG, ...config };
return {
getWorkspaceScopedDir: () => workDir,
getFilesConfig: () => merged,
} as unknown as Runtime;
}

function extractText(result: { content: { type: string; text?: string }[] }): string {
return result.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("");
}

describe("platform files source — write size enforcement", () => {
let workDir: string;

beforeEach(() => {
workDir = mkdtempSync(join(tmpdir(), "nb-files-test-"));
mkdirSync(join(workDir, "files"), { recursive: true });
});

afterEach(() => {
rmSync(workDir, { recursive: true, force: true });
});

test("write succeeds when decoded size is within maxFileSize", async () => {
const runtime = makeStubRuntime(workDir, { maxFileSize: 1024 });
const source = createFilesSource(runtime);

const payload = Buffer.from("hello, world").toString("base64");
const result = await source.execute("write", {
filename: "hello.txt",
base64_data: payload,
mime_type: "text/plain",
});

expect(result.isError).toBe(false);
});

test("write rejects when decoded size exceeds maxFileSize", async () => {
const runtime = makeStubRuntime(workDir, { maxFileSize: 8 });
const source = createFilesSource(runtime);

// 16 bytes decoded — over the 8-byte limit
const payload = Buffer.from("0123456789abcdef").toString("base64");
const result = await source.execute("write", {
filename: "too-big.bin",
base64_data: payload,
mime_type: "application/octet-stream",
});

expect(result.isError).toBe(true);
expect(extractText(result)).toContain("exceeds limit");
expect(extractText(result)).toContain("too-big.bin");
});

test("write uses runtime config override (not just defaults)", async () => {
const runtime = makeStubRuntime(workDir, { maxFileSize: 100 });
const source = createFilesSource(runtime);

// 50-byte decoded — under the 100 limit. Pins the runtime config as the
// source of truth (not a hard-coded default).
const payload = Buffer.from("x".repeat(50)).toString("base64");
const result = await source.execute("write", {
filename: "ok.txt",
base64_data: payload,
mime_type: "text/plain",
});

expect(result.isError).toBe(false);
});
});