Showing the changes to apps/server/src/workspace/WorkspaceFileSystem.ts (1 of 26 files). Show all
Unified · Split
| @@ -3,10 +3,13 @@ · 2 unmodified lines | ||
| 3 | 3 | * WorkspaceFileSystem - Effect service contract for workspace file mutations. |
| 4 | 4 | * |
| 5 | 5 | * 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. | |
| 7 | 9 | * |
| 8 | 10 | * @module WorkspaceFileSystem |
| 9 | 11 | */ |
| 12 | +import * as NodeFS from "node:fs"; | |
| 10 | 13 | import * as NodeFSP from "node:fs/promises"; |
| 11 | 14 | |
| 12 | 15 | import type { |
| @@ -104,7 +107,10 @@ export type WorkspaceFileSystemError = typeof WorkspaceFileSystemError.Type; · 91 unmodified lines | ||
| 104 | 107 | export class WorkspaceFileSystem extends Context.Service< |
| 105 | 108 | WorkspaceFileSystem, |
| 106 | 109 | { |
| 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 | + */ | |
| 108 | 114 | readonly readFile: ( |
| 109 | 115 | input: ProjectReadFileInput, |
| 110 | 116 | ) => Effect.Effect< |
| @@ -132,9 +138,31 @@ export const make = Effect.gen(function* () { · 21 unmodified lines | ||
| 132 | 138 | const workspacePaths = yield* WorkspacePaths.WorkspacePaths; |
| 133 | 139 | const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; |
| 134 | 140 | |
| 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 | + | |
| 138 | 166 | const target = yield* workspacePaths.resolveRelativePathWithinRoot({ |
| 139 | 167 | workspaceRoot: input.cwd, |
| 140 | 168 | relativePath: input.relativePath, |
| @@ -177,10 +205,24 @@ export const make = Effect.gen(function* () { · 36 unmodified lines | ||
| 177 | 205 | resolvedPath: realTargetPath, |
| 178 | 206 | }); |
| 179 | 207 | } |
| 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; | |
| 180 | 216 | |
| 181 | 217 | return yield* Effect.acquireUseRelease( |
| 182 | 218 | 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 | + ), | |
| 184 | 226 | catch: (cause) => |
| 185 | 227 | new WorkspaceFileSystemOperationError({ |
| 186 | 228 | workspaceRoot: input.cwd, |