Skip to content

Commit f5454e1

Browse files
sanchitrkclaude
andauthored
feat(plugin): add bounded scanLines to the sandbox filesystem (#55)
* feat(plugin): add bounded scanLines to the sandbox filesystem Required on every backend: a window of a file's lines, bounded by maxBytes, in one pass and without holding more than the window. LineScanner is the streaming implementation the local VFS uses through positional reads; a POSIX sh + awk script with the same rules runs inside remote sandboxes so only the window crosses the network (Scan.viaShell). The scan is bounded by the file size seen when it starts, so a growing file cannot extend it. A shared corpus checks the scanner, the script and an independent reference against each other, every local backend, and live remote sandboxes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(plugin): use private members in LineScanner Match the rest of the codebase, which uses TypeScript `private` rather than `#` fields; nothing outside the class reads them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent bef2d43 commit f5454e1

13 files changed

Lines changed: 1429 additions & 3 deletions

File tree

‎extras/codework-sandbox-vercel/src/index.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ const filesystem = (remote: Remote, execTimeout: number | undefined): SandboxFil
8787
// same shape `fs.stat` rejects with, so `isNotFoundError` recognises it
8888
throw Object.assign(new Error(`ENOENT: no such file or directory, realpath '${path}'`), { code: "ENOENT" });
8989
},
90+
scanLines: SandboxFileSystem.Scan.viaShell(async (argv) => {
91+
const result = await (await command(remote, execTimeout, argv)).wait();
92+
return { exitCode: result.exitCode, stdout: await result.stdout(), stderr: await result.stderr() };
93+
}),
9094
});
9195

9296
const command = (

‎packages/harness/src/sandbox/fs/vfs.ts‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* oxlint-disable effecttsgo/async-function -- VFS operations implement a Promise-based adapter contract. */
1+
/* oxlint-disable effecttsgo/async-function, effecttsgo/new-promise -- VFS operations implement a Promise-based adapter contract. */
22
import type { VirtualFileSystem, VirtualStats } from "@platformatic/vfs";
33
import { Context, Effect, Layer } from "effect";
44
import { Buffer } from "node:buffer";
@@ -23,6 +23,8 @@ const toFileStat = (stats: VirtualStats): SandboxFileSystem.FileStat => ({
2323
// lifts it into the Effect service the harness consumes.
2424
export interface Interface extends SandboxFileSystem.Provider {}
2525

26+
const SCAN_CHUNK = 64 * 1024;
27+
2628
const bytes = (content: string | Uint8Array) =>
2729
typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
2830

@@ -91,6 +93,34 @@ const make = (vfs: VirtualFileSystem): Interface => {
9193
// Every VFS provider resolves symlinks itself (the host one via
9294
// `fs.realpath`) and rejects with ENOENT for a missing path.
9395
realpath: (path) => vfs.promises.realpath(path),
96+
scanLines: async (path, options) => {
97+
const scanner = new SandboxFileSystem.Scan.LineScanner(options);
98+
// Bounded by the size seen now, so a file that keeps growing ends the scan.
99+
const { size } = await vfs.promises.stat(path);
100+
if (size === 0) return scanner.finish();
101+
// Positional reads through a descriptor: the VFS read stream loads the
102+
// whole file first, and the host provider reads only what is asked.
103+
const fd = await new Promise<number>((resolve, reject) =>
104+
vfs.open(path, "r", (error, value) => (error ? reject(error) : resolve(value!))),
105+
);
106+
try {
107+
const buffer = Buffer.alloc(Math.min(SCAN_CHUNK, size));
108+
for (let position = 0; position < size;) {
109+
const length = Math.min(buffer.length, size - position);
110+
const read = await new Promise<number>((resolve, reject) =>
111+
vfs.read(fd, buffer, 0, length, position, (error, bytesRead) =>
112+
error ? reject(error) : resolve(bytesRead),
113+
),
114+
);
115+
if (read === 0) break;
116+
scanner.push(buffer.subarray(0, read));
117+
position += read;
118+
}
119+
} finally {
120+
await new Promise<void>((resolve) => vfs.close(fd, () => resolve()));
121+
}
122+
return scanner.finish();
123+
},
94124
};
95125
};
96126

‎packages/harness/src/sandboxes/daytona/fs.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const make = (provider: Interface, options?: Options): SandboxFileSystem.
3232
mkdir: (path, mkdirOptions) => provider.mkdir(resolve(path), mkdirOptions),
3333
rm: (path, rmOptions) => provider.rm(resolve(path), rmOptions),
3434
realpath: (path) => provider.realpath(resolve(path)),
35+
scanLines: (path, scanOptions) => provider.scanLines(resolve(path), scanOptions),
3536
};
3637
};
3738

‎packages/harness/src/sandboxes/daytona/provider.ts‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,17 @@ export const statsFrom = (info: FileInfo): RemoteFileSystem.FileStat => {
150150

151151
type RemoteFilesystemProvider = Pick<
152152
RemoteFileSystem.Interface,
153-
"readFile" | "readFileBuffer" | "writeFile" | "stat" | "lstat" | "readdir" | "exists" | "mkdir" | "rm" | "realpath"
153+
| "readFile"
154+
| "readFileBuffer"
155+
| "writeFile"
156+
| "stat"
157+
| "lstat"
158+
| "readdir"
159+
| "exists"
160+
| "mkdir"
161+
| "rm"
162+
| "realpath"
163+
| "scanLines"
154164
>;
155165

156166
const providerFrom = (sandbox: RemoteSandbox, options: Options) => {
@@ -215,6 +225,15 @@ const providerFrom = (sandbox: RemoteSandbox, options: Options) => {
215225
// same shape `fs.stat` rejects with, so `isNotFoundError` recognises it
216226
throw Object.assign(new Error(`ENOENT: no such file or directory, realpath '${path}'`), { code: "ENOENT" });
217227
},
228+
scanLines: SandboxFileSystem.Scan.viaShell(async (argv) => {
229+
const result = await sandbox.process.executeCommand(
230+
quoteArgv(argv),
231+
options.cwd,
232+
undefined,
233+
options.execTimeout,
234+
);
235+
return { exitCode: result.exitCode, stdout: result.result ?? "" };
236+
}),
218237
};
219238

220239
return filesystem;

‎packages/harness/src/sandboxes/vercel/fs.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const make = (provider: Interface, options?: Options): SandboxFileSystem.
5151
// option validation happens in `fromProvider`, before any mutation
5252
rm: (path, rmOptions) => provider.rm(resolve(path), rmOptions),
5353
realpath: (path) => provider.realpath(resolve(path)),
54+
scanLines: (path, scanOptions) => provider.scanLines(resolve(path), scanOptions),
5455
};
5556
};
5657

‎packages/harness/src/sandboxes/vercel/provider.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,17 @@ export const statsFrom = (stats: Stats): RemoteFileSystem.FileStat => {
193193

194194
type RemoteFilesystemProvider = Pick<
195195
RemoteFileSystem.Interface,
196-
"readFile" | "readFileBuffer" | "writeFile" | "stat" | "lstat" | "readdir" | "exists" | "mkdir" | "rm" | "realpath"
196+
| "readFile"
197+
| "readFileBuffer"
198+
| "writeFile"
199+
| "stat"
200+
| "lstat"
201+
| "readdir"
202+
| "exists"
203+
| "mkdir"
204+
| "rm"
205+
| "realpath"
206+
| "scanLines"
197207
>;
198208

199209
// The Vercel `fs` surface is `node:fs/promises`-compatible, so the provider
@@ -244,6 +254,10 @@ const providerFrom = (sandbox: RemoteSandbox, options: Options): RemoteFilesyste
244254
// same shape `fs.stat` rejects with, so `isNotFoundError` recognises it
245255
throw Object.assign(new Error(`ENOENT: no such file or directory, realpath '${path}'`), { code: "ENOENT" });
246256
},
257+
scanLines: SandboxFileSystem.Scan.viaShell(async (argv) => {
258+
const result = await (await spawnArgv(sandbox, options, argv)).wait();
259+
return { exitCode: result.exitCode, stdout: await result.stdout(), stderr: await result.stderr() };
260+
}),
247261
};
248262

249263
return filesystem;

0 commit comments

Comments
 (0)