diff --git a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts index 0f5670f1d2de0..63cbea5dc7f01 100644 --- a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts @@ -289,6 +289,25 @@ describe("AntigravityAdapterV2 client file system", () => { context("fs/read_text_file"), ); assert.equal(pasted.content, "pasted"); + yield* fileSystem.writeFileString(insidePath, "\uFEFFfirst\né中🙂\nlast"); + const sliced = yield* readTextFile( + { sessionId: "mock-session-1", path: insidePath, line: 2, limit: 1 }, + context("fs/read_text_file"), + ); + assert.equal(sliced.content, "é中🙂"); + yield* fileSystem.writeFileString(insidePath, "inside"); + 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 c6079234d916c..225c2684d02b9 100644 --- a/apps/server/src/provider/acp/AntigravityClientFiles.ts +++ b/apps/server/src/provider/acp/AntigravityClientFiles.ts @@ -78,13 +78,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) {