From ce2c85cdef97facbf6c2bcc04a94cfe2b1e6c2c0 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Sun, 25 Oct 2026 22:30:03 -0500 Subject: [PATCH] fix(antigravity): reject binary client file reads instead of returning corrupted text fs/read_text_file decoded any file with a non-fatal UTF-8 decoder, so an Antigravity agent reading an image, PDF or audio file through client_view_file got replacement-character text it could not act on. Decode strictly and reject NUL bytes, the same check text attachments already use, so the agent gets a clear invalid-params error instead. Refs #24969 Co-Authored-By: Claude Opus 5.5 --- .../Adapters/AntigravityAdapterV2.test.ts | 14 +++++++++++++- .../src/provider/acp/AntigravityClientFiles.ts | 16 ++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts index c991487b8d0c2..847ec7670d6ad 100644 --- a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts @@ -127,7 +127,7 @@ const layerSession = Layer.mergeAll( ); describe("AntigravityAdapterV2 client file system", () => { - it.effect("confines agent file requests to the workspace under full access", () => + it.effect("confines agent file requests to workspace text files under full access", () => Effect.gen(function* () { const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const fileSystem = yield* FileSystem.FileSystem; @@ -214,6 +214,18 @@ describe("AntigravityAdapterV2 client file system", () => { context("fs/read_text_file"), ); assert.equal(pasted.content, "pasted"); + for (const [name, bytes] of [ + ["image.png", new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])], + ["data.bin", new Uint8Array([0x61, 0, 0x62])], + ] as const) { + const binaryPath = path.join(workspace, name); + yield* fileSystem.writeFile(binaryPath, bytes); + const binaryRead = yield* readTextFile( + { sessionId: "mock-session-1", path: binaryPath }, + context("fs/read_text_file"), + ).pipe(Effect.flip); + assert.propertyVal(binaryRead, "code", -32602, `${name} is refused as binary`); + } const outsideRead = yield* readTextFile( { sessionId: "mock-session-1", path: outsideFile }, diff --git a/apps/server/src/provider/acp/AntigravityClientFiles.ts b/apps/server/src/provider/acp/AntigravityClientFiles.ts index ebbc234e149b9..af0b8933aee25 100644 --- a/apps/server/src/provider/acp/AntigravityClientFiles.ts +++ b/apps/server/src/provider/acp/AntigravityClientFiles.ts @@ -74,13 +74,25 @@ export const readAntigravityClientTextFile = Effect.fn("AntigravityClientFiles.r `File '${input.request.path}' is not a readable text file under ${CLIENT_FILE_MAX_BYTES} bytes.`, ); } - const text = yield* input.fileSystem - .readFileString(filePath) + const bytes = yield* input.fileSystem + .readFile(filePath) .pipe( Effect.mapError(() => EffectAcpErrors.AcpRequestError.internalError(`Could not read '${input.request.path}'.`), ), ); + const text = yield* Effect.try({ + try: () => new TextDecoder("utf-8", { fatal: true }).decode(bytes), + catch: () => + EffectAcpErrors.AcpRequestError.invalidParams( + `File '${input.request.path}' is not a UTF-8 text file.`, + ), + }); + if (text.includes("\0")) { + return yield* EffectAcpErrors.AcpRequestError.invalidParams( + `File '${input.request.path}' contains binary data.`, + ); + } const line = input.request.line ?? undefined; const limit = input.request.limit ?? undefined; if (line === undefined && limit === undefined) {