History

feat(files): open markdown, HTML, and PDF files outside the workspace (#9140)

Julius Marminge committed 2 months ago

Commit f46a7091 parent f2a914b26 files · +394 −89Browse files

Showing the changes to apps/server/src/workspace/WorkspaceFileSystem.ts (1 of 26 files). Show all

Unified · Split

apps/server/src/workspace/WorkspaceFileSystem.ts+48−6
@@ -3,10 +3,13 @@ · 2 unmodified lines
33 * WorkspaceFileSystem - Effect service contract for workspace file mutations.
44 *
55 * Owns workspace-root-relative file read/write operations and their associated
6− * safety checks and cache invalidation hooks.
6+ * safety checks and cache invalidation hooks. Reads also accept absolute host
7+ * paths so clients can show files an agent left outside the workspace; writes
8+ * never leave the root.
79 *
810 * @module WorkspaceFileSystem
911 */
12+import * as NodeFS from "node:fs";
1013 import * as NodeFSP from "node:fs/promises";
1114
1215 import type {
@@ -104,7 +107,10 @@ export type WorkspaceFileSystemError = typeof WorkspaceFileSystemError.Type; · 91 unmodified lines
104107 export class WorkspaceFileSystem extends Context.Service<
105108 WorkspaceFileSystem,
106109 {
107− /** Read a UTF-8 text file relative to the workspace root. */
110+ /**
111+ * Read a UTF-8 text file relative to the workspace root, or any host file by
112+ * absolute path.
113+ */
108114 readonly readFile: (
109115 input: ProjectReadFileInput,
110116 ) => Effect.Effect<
@@ -132,9 +138,31 @@ export const make = Effect.gen(function* () { · 21 unmodified lines
132138 const workspacePaths = yield* WorkspacePaths.WorkspacePaths;
133139 const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
134140
135− const readFile: WorkspaceFileSystem["Service"]["readFile"] = Effect.fn(
136− "WorkspaceFileSystem.readFile",
137− )(function* (input) {
141+ /**
142+ * Resolves the file a read targets. Workspace-relative paths must stay inside the
143+ * root, symlinks included. An absolute path reads a host file in place, such as a
144+ * report an agent wrote to a temp directory; it gets no root check.
145+ */
146+ const resolveReadTarget = Effect.fn("WorkspaceFileSystem.resolveReadTarget")(function* (
147+ input: ProjectReadFileInput,
148+ ) {
149+ const requestedPath = input.relativePath.trim();
150+ if (path.isAbsolute(requestedPath)) {
151+ const realTargetPath = yield* Effect.tryPromise({
152+ try: () => NodeFSP.realpath(requestedPath),
153+ catch: (cause) =>
154+ new WorkspaceFileSystemOperationError({
155+ workspaceRoot: input.cwd,
156+ relativePath: input.relativePath,
157+ resolvedPath: requestedPath,
158+ operationPath: requestedPath,
159+ operation: "realpath-target",
160+ cause,
161+ }),
162+ });
163+ return { relativePath: requestedPath, realTargetPath };
164+ }
165+
138166 const target = yield* workspacePaths.resolveRelativePathWithinRoot({
139167 workspaceRoot: input.cwd,
140168 relativePath: input.relativePath,
@@ -177,10 +205,24 @@ export const make = Effect.gen(function* () { · 36 unmodified lines
177205 resolvedPath: realTargetPath,
178206 });
179207 }
208+ return { relativePath: target.relativePath, realTargetPath };
209+ });
210+
211+ const readFile: WorkspaceFileSystem["Service"]["readFile"] = Effect.fn(
212+ "WorkspaceFileSystem.readFile",
213+ )(function* (input) {
214+ const target = yield* resolveReadTarget(input);
215+ const realTargetPath = target.realTargetPath;
180216
181217 return yield* Effect.acquireUseRelease(
182218 Effect.tryPromise({
183− try: () => NodeFSP.open(realTargetPath, "r"),
219+ // Non-blocking so a FIFO cannot hang the open; the stat below rejects
220+ // it. Regular files ignore the flag. Windows lacks it.
221+ try: () =>
222+ NodeFSP.open(
223+ realTargetPath,
224+ NodeFS.constants.O_RDONLY | (NodeFS.constants.O_NONBLOCK ?? 0),
225+ ),
184226 catch: (cause) =>
185227 new WorkspaceFileSystemOperationError({
186228 workspaceRoot: input.cwd,