From 05760160f4e38adbe8fc3b1f5ac0643d0d0b0d1f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 20 Jun 2026 16:05:55 +0000 Subject: [PATCH 1/3] Use Effect FileSystem for workspace reads Co-authored-by: Julius Marminge --- .../src/workspace/WorkspaceFileSystem.test.ts | 95 +++++++++++++++---- .../src/workspace/WorkspaceFileSystem.ts | 79 ++++++++------- 2 files changed, 115 insertions(+), 59 deletions(-) diff --git a/apps/server/src/workspace/WorkspaceFileSystem.test.ts b/apps/server/src/workspace/WorkspaceFileSystem.test.ts index aa2dabb3337..c58dd103115 100644 --- a/apps/server/src/workspace/WorkspaceFileSystem.test.ts +++ b/apps/server/src/workspace/WorkspaceFileSystem.test.ts @@ -1,5 +1,5 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; -import { it, describe, expect } from "@effect/vitest"; +import { assert, it, describe } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; @@ -12,6 +12,8 @@ import * as WorkspaceEntries from "./WorkspaceEntries.ts"; import * as WorkspaceFileSystem from "./WorkspaceFileSystem.ts"; import * as WorkspacePaths from "./WorkspacePaths.ts"; +const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024; + const ProjectLayer = WorkspaceFileSystem.layer.pipe( Layer.provide(WorkspacePaths.layer), Layer.provide(WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer))), @@ -51,6 +53,20 @@ const writeTextFile = Effect.fn("writeTextFile")(function* ( yield* fileSystem.writeFileString(absolutePath, contents).pipe(Effect.orDie); }); +const writeBinaryFile = Effect.fn("writeBinaryFile")(function* ( + cwd: string, + relativePath: string, + contents: Uint8Array, +) { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const absolutePath = path.join(cwd, relativePath); + yield* fileSystem + .makeDirectory(path.dirname(absolutePath), { recursive: true }) + .pipe(Effect.orDie); + yield* fileSystem.writeFile(absolutePath, contents).pipe(Effect.orDie); +}); + it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (it) => { describe("readFile", () => { it.effect("reads UTF-8 files relative to the workspace root", () => @@ -64,7 +80,7 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i relativePath: "src/index.ts", }); - expect(result).toEqual({ + assert.deepStrictEqual(result, { relativePath: "src/index.ts", contents: "export const answer = 42;\n", byteLength: 26, @@ -82,9 +98,47 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i .readFile({ cwd, relativePath: "../escape.md" }) .pipe(Effect.flip); - expect(error.message).toContain( - "Workspace file path must be relative to the project root: ../escape.md", + assert.match( + error.message, + /Workspace file path must be relative to the project root: \.\.\/escape\.md/, + ); + }), + ); + + it.effect("truncates text files larger than the preview byte limit", () => + Effect.gen(function* () { + const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; + const cwd = yield* makeTempDir; + yield* writeTextFile(cwd, "large.txt", `${"a".repeat(PROJECT_READ_FILE_MAX_BYTES)}tail`); + + const result = yield* workspaceFileSystem.readFile({ + cwd, + relativePath: "large.txt", + }); + + assert.strictEqual(result.byteLength, PROJECT_READ_FILE_MAX_BYTES + 4); + assert.strictEqual(result.contents.length, PROJECT_READ_FILE_MAX_BYTES); + assert.strictEqual(result.contents, "a".repeat(PROJECT_READ_FILE_MAX_BYTES)); + assert.strictEqual(result.truncated, true); + }), + ); + + it.effect("rejects binary files before decoding them as text", () => + Effect.gen(function* () { + const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; + const cwd = yield* makeTempDir; + yield* writeBinaryFile(cwd, "image.bin", new Uint8Array([0x66, 0x6f, 0x00, 0x6f])); + + const error = yield* workspaceFileSystem + .readFile({ cwd, relativePath: "image.bin" }) + .pipe(Effect.flip); + + assert.strictEqual( + error.message, + `Workspace file operation 'workspaceFileSystem.readFile' failed for 'image.bin' in '${cwd}'.`, ); + assert.ok(error.cause instanceof Error); + assert.strictEqual((error.cause as Error).message, "Binary files cannot be previewed as text."); }), ); @@ -105,11 +159,13 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i .readFile({ cwd, relativePath: "linked-secret.txt" }) .pipe(Effect.flip); - expect(error.message).toBe( + assert.strictEqual( + error.message, `Workspace file operation 'workspaceFileSystem.readFile' failed for 'linked-secret.txt' in '${cwd}'.`, ); - expect(error.cause).toBeInstanceOf(Error); - expect((error.cause as Error).message).toBe( + assert.ok(error.cause instanceof Error); + assert.strictEqual( + (error.cause as Error).message, "Workspace file path resolves outside the project root.", ); }), @@ -132,8 +188,8 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i .readFileString(path.join(cwd, "plans/effect-rpc.md")) .pipe(Effect.orDie); - expect(result).toEqual({ relativePath: "plans/effect-rpc.md" }); - expect(saved).toBe("# Plan\n"); + assert.deepStrictEqual(result, { relativePath: "plans/effect-rpc.md" }); + assert.strictEqual(saved, "# Plan\n"); }), ); @@ -145,7 +201,8 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i yield* writeTextFile(cwd, "src/existing.ts", "export {};\n"); const beforeWrite = yield* workspaceEntries.list({ cwd }); - expect(beforeWrite.entries.some((entry) => entry.path === "plans/effect-rpc.md")).toBe( + assert.strictEqual( + beforeWrite.entries.some((entry) => entry.path === "plans/effect-rpc.md"), false, ); @@ -156,10 +213,11 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i }); const afterWrite = yield* workspaceEntries.list({ cwd }); - expect(afterWrite.entries).toEqual( - expect.arrayContaining([expect.objectContaining({ path: "plans/effect-rpc.md" })]), + assert.strictEqual( + afterWrite.entries.some((entry) => entry.path === "plans/effect-rpc.md"), + true, ); - expect(afterWrite.truncated).toBe(false); + assert.strictEqual(afterWrite.truncated, false); }), ); @@ -178,15 +236,14 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i }) .pipe(Effect.flip); - expect(error.message).toContain( - "Workspace file path must be relative to the project root: ../escape.md", + assert.match( + error.message, + /Workspace file path must be relative to the project root: \.\.\/escape\.md/, ); const escapedPath = path.resolve(cwd, "..", "escape.md"); - const escapedStat = yield* fileSystem - .stat(escapedPath) - .pipe(Effect.orElseSucceed(() => null)); - expect(escapedStat).toBeNull(); + const escapedExists = yield* fileSystem.exists(escapedPath); + assert.strictEqual(escapedExists, false); }), ); }); diff --git a/apps/server/src/workspace/WorkspaceFileSystem.ts b/apps/server/src/workspace/WorkspaceFileSystem.ts index 8cd176db3dd..6f30d9f282d 100644 --- a/apps/server/src/workspace/WorkspaceFileSystem.ts +++ b/apps/server/src/workspace/WorkspaceFileSystem.ts @@ -1,4 +1,3 @@ -// @effect-diagnostics nodeBuiltinImport:off /** * WorkspaceFileSystem - Effect service contract for workspace file mutations. * @@ -7,8 +6,6 @@ * * @module WorkspaceFileSystem */ -import * as NodeFSP from "node:fs/promises"; - import type { ProjectReadFileInput, ProjectReadFileResult, @@ -86,11 +83,11 @@ export const make = Effect.gen(function* () { relativePath: input.relativePath, }); - return yield* Effect.tryPromise({ - try: async () => { - const [realWorkspaceRoot, realTargetPath] = await Promise.all([ - NodeFSP.realpath(input.cwd), - NodeFSP.realpath(target.absolutePath), + return yield* Effect.scoped( + Effect.gen(function* () { + const [realWorkspaceRoot, realTargetPath] = yield* Effect.all([ + fileSystem.realPath(input.cwd), + fileSystem.realPath(target.absolutePath), ]); const relativeRealPath = path.relative(realWorkspaceRoot, realTargetPath); if ( @@ -98,41 +95,43 @@ export const make = Effect.gen(function* () { relativeRealPath === ".." || path.isAbsolute(relativeRealPath) ) { - throw new Error("Workspace file path resolves outside the project root."); + return yield* Effect.fail( + new Error("Workspace file path resolves outside the project root."), + ); } - const handle = await NodeFSP.open(realTargetPath, "r"); - try { - const stat = await handle.stat(); - if (!stat.isFile()) { - throw new Error("Workspace path is not a file."); - } - const bytesToRead = Math.min(stat.size, PROJECT_READ_FILE_MAX_BYTES); - const buffer = Buffer.alloc(bytesToRead); - const { bytesRead } = await handle.read(buffer, 0, bytesToRead, 0); - const fileBytes = buffer.subarray(0, bytesRead); - if (fileBytes.includes(0)) { - throw new Error("Binary files cannot be previewed as text."); - } - const contents = new TextDecoder("utf-8").decode(fileBytes); - return { - relativePath: target.relativePath, - contents, - byteLength: stat.size, - truncated: stat.size > PROJECT_READ_FILE_MAX_BYTES, - }; - } finally { - await handle.close(); + const handle = yield* fileSystem.open(realTargetPath, { flag: "r" }); + const stat = yield* handle.stat; + if (stat.type !== "File") { + return yield* Effect.fail(new Error("Workspace path is not a file.")); } - }, - catch: (cause) => - new WorkspaceFileSystemError({ - cwd: input.cwd, - relativePath: input.relativePath, - operation: "workspaceFileSystem.readFile", - cause, - }), - }); + const byteLength = Number(stat.size); + const bytesToRead = Math.min(byteLength, PROJECT_READ_FILE_MAX_BYTES); + const buffer = new Uint8Array(bytesToRead); + const bytesRead = yield* handle.read(buffer); + const fileBytes = buffer.subarray(0, Number(bytesRead)); + if (fileBytes.includes(0)) { + return yield* Effect.fail(new Error("Binary files cannot be previewed as text.")); + } + const contents = new TextDecoder("utf-8").decode(fileBytes); + return { + relativePath: target.relativePath, + contents, + byteLength, + truncated: stat.size > BigInt(PROJECT_READ_FILE_MAX_BYTES), + }; + }), + ).pipe( + Effect.mapError( + (cause) => + new WorkspaceFileSystemError({ + cwd: input.cwd, + relativePath: input.relativePath, + operation: "workspaceFileSystem.readFile", + cause, + }), + ), + ); }); const writeFile: WorkspaceFileSystem["Service"]["writeFile"] = Effect.fn( From 7f8914863cba39a7e22f4c080d3e843885dea788 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 20 Jun 2026 16:08:45 +0000 Subject: [PATCH 2/3] Use tagged workspace read errors Co-authored-by: Julius Marminge --- .../src/workspace/WorkspaceFileSystem.test.ts | 17 +++-- .../src/workspace/WorkspaceFileSystem.ts | 66 +++++++++++++++++-- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/apps/server/src/workspace/WorkspaceFileSystem.test.ts b/apps/server/src/workspace/WorkspaceFileSystem.test.ts index c58dd103115..5c1b694ea0c 100644 --- a/apps/server/src/workspace/WorkspaceFileSystem.test.ts +++ b/apps/server/src/workspace/WorkspaceFileSystem.test.ts @@ -137,8 +137,14 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i error.message, `Workspace file operation 'workspaceFileSystem.readFile' failed for 'image.bin' in '${cwd}'.`, ); - assert.ok(error.cause instanceof Error); - assert.strictEqual((error.cause as Error).message, "Binary files cannot be previewed as text."); + assert.strictEqual( + (error.cause as { readonly _tag?: string })._tag, + "WorkspaceReadFileBinaryFileError", + ); + assert.strictEqual( + (error.cause as { readonly message?: string }).message, + "Binary files cannot be previewed as text.", + ); }), ); @@ -163,9 +169,12 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i error.message, `Workspace file operation 'workspaceFileSystem.readFile' failed for 'linked-secret.txt' in '${cwd}'.`, ); - assert.ok(error.cause instanceof Error); assert.strictEqual( - (error.cause as Error).message, + (error.cause as { readonly _tag?: string })._tag, + "WorkspaceReadFileResolvedOutsideRootError", + ); + assert.strictEqual( + (error.cause as { readonly message?: string }).message, "Workspace file path resolves outside the project root.", ); }), diff --git a/apps/server/src/workspace/WorkspaceFileSystem.ts b/apps/server/src/workspace/WorkspaceFileSystem.ts index 6f30d9f282d..5af54b2a9df 100644 --- a/apps/server/src/workspace/WorkspaceFileSystem.ts +++ b/apps/server/src/workspace/WorkspaceFileSystem.ts @@ -24,6 +24,46 @@ import * as WorkspacePaths from "./WorkspacePaths.ts"; const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024; +class WorkspaceReadFileResolvedOutsideRootError extends Schema.TaggedErrorClass()( + "WorkspaceReadFileResolvedOutsideRootError", + { + cwd: Schema.String, + relativePath: Schema.String, + realWorkspaceRoot: Schema.String, + realTargetPath: Schema.String, + }, +) { + override get message(): string { + return "Workspace file path resolves outside the project root."; + } +} + +class WorkspaceReadFileNotFileError extends Schema.TaggedErrorClass()( + "WorkspaceReadFileNotFileError", + { + cwd: Schema.String, + relativePath: Schema.String, + fileType: Schema.String, + }, +) { + override get message(): string { + return "Workspace path is not a file."; + } +} + +class WorkspaceReadFileBinaryFileError extends Schema.TaggedErrorClass()( + "WorkspaceReadFileBinaryFileError", + { + cwd: Schema.String, + relativePath: Schema.String, + nulByteOffset: Schema.Number, + }, +) { + override get message(): string { + return "Binary files cannot be previewed as text."; + } +} + export class WorkspaceFileSystemError extends Schema.TaggedErrorClass()( "WorkspaceFileSystemError", { @@ -96,22 +136,40 @@ export const make = Effect.gen(function* () { path.isAbsolute(relativeRealPath) ) { return yield* Effect.fail( - new Error("Workspace file path resolves outside the project root."), + new WorkspaceReadFileResolvedOutsideRootError({ + cwd: input.cwd, + relativePath: target.relativePath, + realWorkspaceRoot, + realTargetPath, + }), ); } const handle = yield* fileSystem.open(realTargetPath, { flag: "r" }); const stat = yield* handle.stat; if (stat.type !== "File") { - return yield* Effect.fail(new Error("Workspace path is not a file.")); + return yield* Effect.fail( + new WorkspaceReadFileNotFileError({ + cwd: input.cwd, + relativePath: target.relativePath, + fileType: stat.type, + }), + ); } const byteLength = Number(stat.size); const bytesToRead = Math.min(byteLength, PROJECT_READ_FILE_MAX_BYTES); const buffer = new Uint8Array(bytesToRead); const bytesRead = yield* handle.read(buffer); const fileBytes = buffer.subarray(0, Number(bytesRead)); - if (fileBytes.includes(0)) { - return yield* Effect.fail(new Error("Binary files cannot be previewed as text.")); + const nulByteOffset = fileBytes.indexOf(0); + if (nulByteOffset !== -1) { + return yield* Effect.fail( + new WorkspaceReadFileBinaryFileError({ + cwd: input.cwd, + relativePath: target.relativePath, + nulByteOffset, + }), + ); } const contents = new TextDecoder("utf-8").decode(fileBytes); return { From 201f6547be4e80c846a468330dd4a5091a70505e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 20 Jun 2026 16:11:45 +0000 Subject: [PATCH 3/3] Yield tagged workspace read errors directly Co-authored-by: Julius Marminge --- .../src/workspace/WorkspaceFileSystem.ts | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/apps/server/src/workspace/WorkspaceFileSystem.ts b/apps/server/src/workspace/WorkspaceFileSystem.ts index 5af54b2a9df..8f52c1985e5 100644 --- a/apps/server/src/workspace/WorkspaceFileSystem.ts +++ b/apps/server/src/workspace/WorkspaceFileSystem.ts @@ -135,26 +135,22 @@ export const make = Effect.gen(function* () { relativeRealPath === ".." || path.isAbsolute(relativeRealPath) ) { - return yield* Effect.fail( - new WorkspaceReadFileResolvedOutsideRootError({ - cwd: input.cwd, - relativePath: target.relativePath, - realWorkspaceRoot, - realTargetPath, - }), - ); + return yield* new WorkspaceReadFileResolvedOutsideRootError({ + cwd: input.cwd, + relativePath: target.relativePath, + realWorkspaceRoot, + realTargetPath, + }); } const handle = yield* fileSystem.open(realTargetPath, { flag: "r" }); const stat = yield* handle.stat; if (stat.type !== "File") { - return yield* Effect.fail( - new WorkspaceReadFileNotFileError({ - cwd: input.cwd, - relativePath: target.relativePath, - fileType: stat.type, - }), - ); + return yield* new WorkspaceReadFileNotFileError({ + cwd: input.cwd, + relativePath: target.relativePath, + fileType: stat.type, + }); } const byteLength = Number(stat.size); const bytesToRead = Math.min(byteLength, PROJECT_READ_FILE_MAX_BYTES); @@ -163,13 +159,11 @@ export const make = Effect.gen(function* () { const fileBytes = buffer.subarray(0, Number(bytesRead)); const nulByteOffset = fileBytes.indexOf(0); if (nulByteOffset !== -1) { - return yield* Effect.fail( - new WorkspaceReadFileBinaryFileError({ - cwd: input.cwd, - relativePath: target.relativePath, - nulByteOffset, - }), - ); + return yield* new WorkspaceReadFileBinaryFileError({ + cwd: input.cwd, + relativePath: target.relativePath, + nulByteOffset, + }); } const contents = new TextDecoder("utf-8").decode(fileBytes); return {