@hachej/boring-agent 0.1.22 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agentPluginEvents-zyIvVjsA.d.ts → agentPluginEvents-CF0BGY4D.d.ts} +11 -1
- package/dist/{chunk-6R7O6BHW.js → chunk-FILARC5X.js} +6 -28
- package/dist/chunk-HDOSWEXG.js +29 -0
- package/dist/eval/index.js +33 -1
- package/dist/front/index.d.ts +49 -4
- package/dist/front/index.js +495 -97
- package/dist/front/styles.css +69 -21
- package/dist/{harness-BCit36Ha.d.ts → harness-CQ0uw6xW.d.ts} +5 -0
- package/dist/server/index.d.ts +23 -6
- package/dist/server/index.js +971 -615
- package/dist/shared/index.d.ts +17 -17
- package/dist/shared/index.js +8 -6
- package/docs/ERROR_CODES.md +4 -0
- package/docs/README.md +1 -0
- package/docs/runtime.md +133 -2
- package/package.json +9 -8
package/dist/server/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
ErrorCode,
|
|
3
2
|
noopTelemetry,
|
|
4
3
|
safeCapture,
|
|
5
4
|
validateTool
|
|
6
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-HDOSWEXG.js";
|
|
6
|
+
import {
|
|
7
|
+
ErrorCode
|
|
8
|
+
} from "../chunk-FILARC5X.js";
|
|
7
9
|
|
|
8
10
|
// src/server/sandbox/direct/createDirectSandbox.ts
|
|
9
11
|
import { spawn } from "child_process";
|
|
@@ -17,7 +19,8 @@ function getEnvSnapshot() {
|
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
// src/server/workspace/runtimeLayout.ts
|
|
20
|
-
import {
|
|
22
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
23
|
+
import { dirname, join, resolve } from "path";
|
|
21
24
|
var BORING_AGENT_DIR = ".boring-agent";
|
|
22
25
|
var BORING_AGENT_GITIGNORE_CONTENT = "*\n";
|
|
23
26
|
var BORING_AGENT_RUNTIME_DIR_NAMES = [
|
|
@@ -81,8 +84,10 @@ function withWorkspacePythonEnv(opts) {
|
|
|
81
84
|
return {
|
|
82
85
|
...baseEnv,
|
|
83
86
|
PATH: pathParts.join(":"),
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
HOME: runtimeRoot,
|
|
88
|
+
VIRTUAL_ENV: paths.venv,
|
|
89
|
+
PYTHONHOME: void 0,
|
|
90
|
+
BORING_AGENT_WORKSPACE_ROOT: runtimeRoot
|
|
86
91
|
};
|
|
87
92
|
}
|
|
88
93
|
|
|
@@ -123,29 +128,34 @@ function terminateProcess(child, signal) {
|
|
|
123
128
|
} catch {
|
|
124
129
|
}
|
|
125
130
|
}
|
|
126
|
-
function createDirectSandbox() {
|
|
131
|
+
function createDirectSandbox(opts = {}) {
|
|
127
132
|
let workspace = null;
|
|
133
|
+
let runtimeContext = opts.runtimeContext ?? { runtimeCwd: process.cwd() };
|
|
128
134
|
return {
|
|
129
135
|
id: "direct",
|
|
130
136
|
placement: "server",
|
|
131
137
|
provider: "direct",
|
|
132
138
|
capabilities: ["exec"],
|
|
139
|
+
get runtimeContext() {
|
|
140
|
+
return runtimeContext;
|
|
141
|
+
},
|
|
133
142
|
async init(ctx) {
|
|
134
143
|
workspace = ctx.workspace;
|
|
144
|
+
runtimeContext = opts.runtimeContext ?? ctx.workspace.runtimeContext;
|
|
135
145
|
},
|
|
136
|
-
async exec(cmd,
|
|
146
|
+
async exec(cmd, opts2) {
|
|
137
147
|
if (!workspace) {
|
|
138
148
|
throw new Error("DirectSandbox not initialized");
|
|
139
149
|
}
|
|
140
150
|
const start = Date.now();
|
|
141
|
-
const timeoutMs =
|
|
142
|
-
const maxOutputBytes =
|
|
143
|
-
const workspaceRoot =
|
|
144
|
-
const cwd =
|
|
145
|
-
return await new Promise((
|
|
151
|
+
const timeoutMs = opts2?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
152
|
+
const maxOutputBytes = opts2?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
153
|
+
const workspaceRoot = runtimeContext.runtimeCwd;
|
|
154
|
+
const cwd = opts2?.cwd ?? workspaceRoot;
|
|
155
|
+
return await new Promise((resolve10, reject) => {
|
|
146
156
|
const child = spawn(cmd, {
|
|
147
157
|
cwd,
|
|
148
|
-
env: withWorkspacePythonEnv({ workspaceRoot, env:
|
|
158
|
+
env: withWorkspacePythonEnv({ workspaceRoot, env: opts2?.env }),
|
|
149
159
|
shell: true,
|
|
150
160
|
windowsHide: true,
|
|
151
161
|
detached: process.platform !== "win32"
|
|
@@ -171,7 +181,7 @@ function createDirectSandbox() {
|
|
|
171
181
|
if (settled) return;
|
|
172
182
|
settled = true;
|
|
173
183
|
cleanup();
|
|
174
|
-
|
|
184
|
+
resolve10({
|
|
175
185
|
stdout: new Uint8Array(Buffer.concat(stdoutChunks)),
|
|
176
186
|
stderr: new Uint8Array(Buffer.concat(stderrChunks)),
|
|
177
187
|
exitCode: typeof exitCode === "number" ? exitCode : timedOut ? 124 : 1,
|
|
@@ -182,10 +192,10 @@ function createDirectSandbox() {
|
|
|
182
192
|
});
|
|
183
193
|
};
|
|
184
194
|
child.stdout?.on("data", (chunk2) => {
|
|
185
|
-
appendOutput(stdoutChunks, chunk2, captureState,
|
|
195
|
+
appendOutput(stdoutChunks, chunk2, captureState, opts2?.onStdout);
|
|
186
196
|
});
|
|
187
197
|
child.stderr?.on("data", (chunk2) => {
|
|
188
|
-
appendOutput(stderrChunks, chunk2, captureState,
|
|
198
|
+
appendOutput(stderrChunks, chunk2, captureState, opts2?.onStderr);
|
|
189
199
|
});
|
|
190
200
|
child.on("error", (error) => {
|
|
191
201
|
if (settled) return;
|
|
@@ -203,24 +213,24 @@ function createDirectSandbox() {
|
|
|
203
213
|
if (!settled) terminateProcess(child, "SIGKILL");
|
|
204
214
|
}, TERMINATION_GRACE_MS);
|
|
205
215
|
}, timeoutMs);
|
|
206
|
-
if (
|
|
216
|
+
if (opts2?.onHeartbeat) {
|
|
207
217
|
heartbeatHandle = setInterval(() => {
|
|
208
|
-
|
|
218
|
+
opts2.onHeartbeat?.(Date.now() - start);
|
|
209
219
|
}, 1e3);
|
|
210
220
|
}
|
|
211
|
-
if (
|
|
221
|
+
if (opts2?.signal) {
|
|
212
222
|
const abort = () => {
|
|
213
223
|
terminateProcess(child, "SIGTERM");
|
|
214
224
|
killHandle = setTimeout(() => {
|
|
215
225
|
if (!settled) terminateProcess(child, "SIGKILL");
|
|
216
226
|
}, TERMINATION_GRACE_MS);
|
|
217
227
|
};
|
|
218
|
-
if (
|
|
228
|
+
if (opts2.signal.aborted) {
|
|
219
229
|
abort();
|
|
220
230
|
} else {
|
|
221
|
-
|
|
231
|
+
opts2.signal.addEventListener("abort", abort, { once: true });
|
|
222
232
|
child.on("close", () => {
|
|
223
|
-
|
|
233
|
+
opts2.signal?.removeEventListener("abort", abort);
|
|
224
234
|
});
|
|
225
235
|
}
|
|
226
236
|
}
|
|
@@ -230,10 +240,10 @@ function createDirectSandbox() {
|
|
|
230
240
|
}
|
|
231
241
|
|
|
232
242
|
// src/server/sandbox/bwrap/createBwrapSandbox.ts
|
|
233
|
-
import { access } from "fs/promises";
|
|
243
|
+
import { access, stat as stat3 } from "fs/promises";
|
|
234
244
|
import { constants } from "fs";
|
|
235
245
|
import { spawn as spawn2 } from "child_process";
|
|
236
|
-
import { dirname, isAbsolute as
|
|
246
|
+
import { dirname as dirname4, isAbsolute as isAbsolute3, join as join2, posix, relative as relative3, resolve as resolve4, sep as sep2 } from "path";
|
|
237
247
|
|
|
238
248
|
// src/server/sandbox/bwrap/buildBwrapArgs.ts
|
|
239
249
|
import { isAbsolute } from "path";
|
|
@@ -251,6 +261,12 @@ var RO_BIND_DIRS = [
|
|
|
251
261
|
"/etc/ssl",
|
|
252
262
|
"/etc/ca-certificates"
|
|
253
263
|
];
|
|
264
|
+
var RO_BIND_TRY_DIRS = [
|
|
265
|
+
// Ubuntu/systemd commonly makes /etc/resolv.conf a symlink to this
|
|
266
|
+
// directory. The sandbox already shares the network namespace, but DNS
|
|
267
|
+
// still fails if the symlink target is hidden by the tmpfs root.
|
|
268
|
+
"/run/systemd/resolve"
|
|
269
|
+
];
|
|
254
270
|
function validateWorkspaceRoot(workspaceRoot) {
|
|
255
271
|
if (workspaceRoot.length === 0) {
|
|
256
272
|
throw new Error("workspaceRoot must not be empty");
|
|
@@ -291,6 +307,9 @@ function buildBwrapArgs(workspaceRoot, options) {
|
|
|
291
307
|
for (const dir of RO_BIND_DIRS) {
|
|
292
308
|
args.push("--ro-bind", dir, dir);
|
|
293
309
|
}
|
|
310
|
+
for (const dir of RO_BIND_TRY_DIRS) {
|
|
311
|
+
args.push("--ro-bind-try", dir, dir);
|
|
312
|
+
}
|
|
294
313
|
if (options?.extraArgs) {
|
|
295
314
|
args.push(...options.extraArgs);
|
|
296
315
|
}
|
|
@@ -311,6 +330,292 @@ function buildBwrapArgs(workspaceRoot, options) {
|
|
|
311
330
|
return args;
|
|
312
331
|
}
|
|
313
332
|
|
|
333
|
+
// src/server/workspace/createNodeWorkspace.ts
|
|
334
|
+
import { lstat as lstat2, mkdir, readdir, readFile, rename, rm, stat as stat2, unlink, writeFile } from "fs/promises";
|
|
335
|
+
import { dirname as dirname3, relative as relative2, resolve as resolve3, sep } from "path";
|
|
336
|
+
import chokidar from "chokidar";
|
|
337
|
+
|
|
338
|
+
// src/server/workspace/paths.ts
|
|
339
|
+
import { lstat, realpath, stat } from "fs/promises";
|
|
340
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, relative, resolve as resolve2 } from "path";
|
|
341
|
+
function createPathValidationError(reason, requestedPath, message) {
|
|
342
|
+
return Object.assign(new Error(message), {
|
|
343
|
+
statusCode: 400,
|
|
344
|
+
reason,
|
|
345
|
+
requestedPath
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
function normalizeForTraversalChecks(requestedPath) {
|
|
349
|
+
let decoded = requestedPath;
|
|
350
|
+
try {
|
|
351
|
+
decoded = decodeURIComponent(requestedPath);
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
return decoded.replace(/\\/g, "/");
|
|
355
|
+
}
|
|
356
|
+
function hasTraversalSegment(requestedPath) {
|
|
357
|
+
return requestedPath.split("/").some((segment) => segment === ".." || segment.startsWith(".."));
|
|
358
|
+
}
|
|
359
|
+
function isWindowsAbsolutePath(requestedPath) {
|
|
360
|
+
return /^[A-Za-z]:[\\/]/.test(requestedPath) || requestedPath.startsWith("\\\\");
|
|
361
|
+
}
|
|
362
|
+
function validatePath(workspaceRoot, relPath) {
|
|
363
|
+
if (relPath.includes("\0")) {
|
|
364
|
+
throw createPathValidationError("null-byte", relPath, "Null byte in path");
|
|
365
|
+
}
|
|
366
|
+
const normalized = normalizeForTraversalChecks(relPath);
|
|
367
|
+
if (isAbsolute2(relPath) || normalized.startsWith("/") || isWindowsAbsolutePath(relPath) || isWindowsAbsolutePath(normalized)) {
|
|
368
|
+
throw createPathValidationError("absolute-path", relPath, "Absolute paths are not allowed");
|
|
369
|
+
}
|
|
370
|
+
if (hasTraversalSegment(normalized) || normalized.startsWith("~") || normalized.startsWith("$") || /[\r\n]/.test(normalized)) {
|
|
371
|
+
throw createPathValidationError("path-escape", relPath, "Path escapes workspace root");
|
|
372
|
+
}
|
|
373
|
+
const resolvedRoot = resolve2(workspaceRoot);
|
|
374
|
+
const resolvedPath = resolve2(workspaceRoot, relPath);
|
|
375
|
+
if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}/`)) {
|
|
376
|
+
throw createPathValidationError("path-escape", relPath, "Path escapes workspace root");
|
|
377
|
+
}
|
|
378
|
+
return resolvedPath;
|
|
379
|
+
}
|
|
380
|
+
async function assertRealPathWithinWorkspace(workspaceRoot, absPath) {
|
|
381
|
+
const realRoot = await realpath(resolve2(workspaceRoot));
|
|
382
|
+
const realCandidate = await realpath(absPath);
|
|
383
|
+
const rel = relative(realRoot, realCandidate);
|
|
384
|
+
if (rel.startsWith("..") || isAbsolute2(rel)) {
|
|
385
|
+
throw createPathValidationError(
|
|
386
|
+
"symlink-escape",
|
|
387
|
+
absPath,
|
|
388
|
+
"Resolved path escapes workspace root"
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async function ensureExistingWorkspacePath(workspaceRoot, relPath) {
|
|
393
|
+
const absPath = validatePath(workspaceRoot, relPath);
|
|
394
|
+
await assertRealPathWithinWorkspace(workspaceRoot, absPath);
|
|
395
|
+
await stat(absPath);
|
|
396
|
+
return absPath;
|
|
397
|
+
}
|
|
398
|
+
async function ensureWritableWorkspacePath(workspaceRoot, relPath) {
|
|
399
|
+
const absPath = validatePath(workspaceRoot, relPath);
|
|
400
|
+
const parentPath = dirname2(absPath);
|
|
401
|
+
await stat(parentPath);
|
|
402
|
+
await assertRealPathWithinWorkspace(workspaceRoot, parentPath);
|
|
403
|
+
try {
|
|
404
|
+
const pathStat = await lstat(absPath);
|
|
405
|
+
if (pathStat.isSymbolicLink()) {
|
|
406
|
+
throw createPathValidationError("symlink-escape", relPath, "Target path is a symlink");
|
|
407
|
+
}
|
|
408
|
+
} catch (error) {
|
|
409
|
+
const code = error.code;
|
|
410
|
+
if (code !== "ENOENT") {
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return absPath;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/server/workspace/createNodeWorkspace.ts
|
|
418
|
+
var EPERM_CODE = "EPERM";
|
|
419
|
+
var DEFAULT_WATCH_IGNORES = [
|
|
420
|
+
"node_modules",
|
|
421
|
+
".git",
|
|
422
|
+
".DS_Store",
|
|
423
|
+
"dist",
|
|
424
|
+
".next",
|
|
425
|
+
".turbo",
|
|
426
|
+
"test-results"
|
|
427
|
+
];
|
|
428
|
+
function shouldIgnoreWatchPath(path4) {
|
|
429
|
+
const parts = path4.split(sep);
|
|
430
|
+
return parts.some(
|
|
431
|
+
(part) => DEFAULT_WATCH_IGNORES.includes(part) || part.endsWith(".tsbuildinfo")
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
function createNodeWatcher(root) {
|
|
435
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
436
|
+
let fsw = null;
|
|
437
|
+
let closed = false;
|
|
438
|
+
const ensureFsw = () => {
|
|
439
|
+
if (fsw) return fsw;
|
|
440
|
+
fsw = chokidar.watch(root, {
|
|
441
|
+
ignored: shouldIgnoreWatchPath,
|
|
442
|
+
ignoreInitial: true,
|
|
443
|
+
persistent: true,
|
|
444
|
+
followSymlinks: false
|
|
445
|
+
// Avoid chokidar's awaitWriteFinish polling loop in long-lived app
|
|
446
|
+
// servers. Consumers already reconcile by mtime after invalidation.
|
|
447
|
+
// No native renames from chokidar — `unlinkDir`/`addDir`/
|
|
448
|
+
// `unlink`/`add` are the primitives we get. We surface them as
|
|
449
|
+
// separate `unlink` + `write`/`mkdir` events; renames are best
|
|
450
|
+
// recovered at the consumer level if needed.
|
|
451
|
+
});
|
|
452
|
+
fsw.on("add", (p, s) => emit({ op: "write", path: rel(p), mtimeMs: s?.mtimeMs }));
|
|
453
|
+
fsw.on("change", (p, s) => emit({ op: "write", path: rel(p), mtimeMs: s?.mtimeMs }));
|
|
454
|
+
fsw.on("addDir", (p) => emit({ op: "mkdir", path: rel(p) }));
|
|
455
|
+
fsw.on("unlink", (p) => emit({ op: "unlink", path: rel(p) }));
|
|
456
|
+
fsw.on("unlinkDir", (p) => emit({ op: "unlink", path: rel(p) }));
|
|
457
|
+
fsw.on("error", () => {
|
|
458
|
+
});
|
|
459
|
+
return fsw;
|
|
460
|
+
};
|
|
461
|
+
const rel = (abs) => {
|
|
462
|
+
const r = relative2(root, abs);
|
|
463
|
+
return r.split(sep).join("/");
|
|
464
|
+
};
|
|
465
|
+
const emit = (event) => {
|
|
466
|
+
if (closed) return;
|
|
467
|
+
if (event.path === "" || event.path.startsWith("..")) return;
|
|
468
|
+
for (const l of [...listeners]) {
|
|
469
|
+
try {
|
|
470
|
+
l(event);
|
|
471
|
+
} catch {
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
return {
|
|
476
|
+
subscribe(listener) {
|
|
477
|
+
if (closed) return () => {
|
|
478
|
+
};
|
|
479
|
+
listeners.add(listener);
|
|
480
|
+
ensureFsw();
|
|
481
|
+
return () => {
|
|
482
|
+
listeners.delete(listener);
|
|
483
|
+
};
|
|
484
|
+
},
|
|
485
|
+
close() {
|
|
486
|
+
if (closed) return;
|
|
487
|
+
closed = true;
|
|
488
|
+
listeners.clear();
|
|
489
|
+
fsw?.close().catch(() => {
|
|
490
|
+
});
|
|
491
|
+
fsw = null;
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
var nodeWorkspaceHostRoots = /* @__PURE__ */ new WeakMap();
|
|
496
|
+
function getNodeWorkspaceHostRoot(workspace) {
|
|
497
|
+
return nodeWorkspaceHostRoots.get(workspace);
|
|
498
|
+
}
|
|
499
|
+
function createNodeWorkspace(root, opts = {}) {
|
|
500
|
+
const runtimeContext = opts.runtimeContext ?? { runtimeCwd: root };
|
|
501
|
+
let cachedWatcher = null;
|
|
502
|
+
const workspace = {
|
|
503
|
+
root: runtimeContext.runtimeCwd,
|
|
504
|
+
runtimeContext,
|
|
505
|
+
fsCapability: "strong",
|
|
506
|
+
watch() {
|
|
507
|
+
if (!cachedWatcher) cachedWatcher = createNodeWatcher(root);
|
|
508
|
+
return cachedWatcher;
|
|
509
|
+
},
|
|
510
|
+
async readFile(relPath) {
|
|
511
|
+
const absPath = await ensureExistingWorkspacePath(root, relPath);
|
|
512
|
+
return await readFile(absPath, "utf-8");
|
|
513
|
+
},
|
|
514
|
+
async readBinaryFile(relPath) {
|
|
515
|
+
const absPath = await ensureExistingWorkspacePath(root, relPath);
|
|
516
|
+
return new Uint8Array(await readFile(absPath));
|
|
517
|
+
},
|
|
518
|
+
async writeFile(relPath, data) {
|
|
519
|
+
const absPath = await ensureWritableWorkspacePath(root, relPath);
|
|
520
|
+
await writeFile(absPath, data, "utf-8");
|
|
521
|
+
},
|
|
522
|
+
async writeBinaryFile(relPath, data) {
|
|
523
|
+
const absPath = await ensureWritableWorkspacePath(root, relPath);
|
|
524
|
+
await writeFile(absPath, data);
|
|
525
|
+
},
|
|
526
|
+
async readFileWithStat(relPath) {
|
|
527
|
+
const absPath = await ensureExistingWorkspacePath(root, relPath);
|
|
528
|
+
const [content, fileStat] = await Promise.all([
|
|
529
|
+
readFile(absPath, "utf-8"),
|
|
530
|
+
stat2(absPath)
|
|
531
|
+
]);
|
|
532
|
+
return {
|
|
533
|
+
content,
|
|
534
|
+
stat: {
|
|
535
|
+
size: fileStat.size,
|
|
536
|
+
mtimeMs: fileStat.mtimeMs,
|
|
537
|
+
kind: fileStat.isDirectory() ? "dir" : "file"
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
},
|
|
541
|
+
async writeFileWithStat(relPath, data) {
|
|
542
|
+
const absPath = await ensureWritableWorkspacePath(root, relPath);
|
|
543
|
+
await writeFile(absPath, data, "utf-8");
|
|
544
|
+
const fileStat = await stat2(absPath);
|
|
545
|
+
return {
|
|
546
|
+
size: fileStat.size,
|
|
547
|
+
mtimeMs: fileStat.mtimeMs,
|
|
548
|
+
kind: fileStat.isDirectory() ? "dir" : "file"
|
|
549
|
+
};
|
|
550
|
+
},
|
|
551
|
+
async writeBinaryFileWithStat(relPath, data) {
|
|
552
|
+
const absPath = await ensureWritableWorkspacePath(root, relPath);
|
|
553
|
+
await writeFile(absPath, data);
|
|
554
|
+
const fileStat = await stat2(absPath);
|
|
555
|
+
return {
|
|
556
|
+
size: fileStat.size,
|
|
557
|
+
mtimeMs: fileStat.mtimeMs,
|
|
558
|
+
kind: fileStat.isDirectory() ? "dir" : "file"
|
|
559
|
+
};
|
|
560
|
+
},
|
|
561
|
+
async unlink(relPath) {
|
|
562
|
+
const absPath = await ensureExistingWorkspacePath(root, relPath);
|
|
563
|
+
if (absPath === resolve3(root)) {
|
|
564
|
+
throw Object.assign(new Error("cannot remove workspace root"), { code: EPERM_CODE });
|
|
565
|
+
}
|
|
566
|
+
const pathStat = await lstat2(absPath);
|
|
567
|
+
if (pathStat.isDirectory()) {
|
|
568
|
+
await rm(absPath, { recursive: true, force: false });
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
await unlink(absPath);
|
|
572
|
+
},
|
|
573
|
+
async readdir(relPath) {
|
|
574
|
+
const absPath = await ensureExistingWorkspacePath(root, relPath);
|
|
575
|
+
const entries = await readdir(absPath, { withFileTypes: true });
|
|
576
|
+
return entries.map((entry) => ({
|
|
577
|
+
name: entry.name,
|
|
578
|
+
kind: entry.isDirectory() ? "dir" : "file"
|
|
579
|
+
}));
|
|
580
|
+
},
|
|
581
|
+
async stat(relPath) {
|
|
582
|
+
const absPath = await ensureExistingWorkspacePath(root, relPath);
|
|
583
|
+
const fileStat = await stat2(absPath);
|
|
584
|
+
return {
|
|
585
|
+
size: fileStat.size,
|
|
586
|
+
mtimeMs: fileStat.mtimeMs,
|
|
587
|
+
kind: fileStat.isDirectory() ? "dir" : "file"
|
|
588
|
+
};
|
|
589
|
+
},
|
|
590
|
+
async mkdir(relPath, opts2) {
|
|
591
|
+
const absPath = validatePath(root, relPath);
|
|
592
|
+
let existingAncestor = absPath;
|
|
593
|
+
while (true) {
|
|
594
|
+
try {
|
|
595
|
+
await stat2(existingAncestor);
|
|
596
|
+
break;
|
|
597
|
+
} catch (error) {
|
|
598
|
+
const code = error.code;
|
|
599
|
+
if (code !== "ENOENT") throw error;
|
|
600
|
+
const parent = dirname3(existingAncestor);
|
|
601
|
+
if (parent === existingAncestor) throw error;
|
|
602
|
+
existingAncestor = parent;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
await assertRealPathWithinWorkspace(root, existingAncestor);
|
|
606
|
+
await mkdir(absPath, { recursive: opts2?.recursive ?? false });
|
|
607
|
+
},
|
|
608
|
+
async rename(fromRelPath, toRelPath2) {
|
|
609
|
+
validatePath(root, toRelPath2);
|
|
610
|
+
const fromAbsPath = await ensureExistingWorkspacePath(root, fromRelPath);
|
|
611
|
+
const toAbsPath = await ensureWritableWorkspacePath(root, toRelPath2);
|
|
612
|
+
await rename(fromAbsPath, toAbsPath);
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
nodeWorkspaceHostRoots.set(workspace, root);
|
|
616
|
+
return workspace;
|
|
617
|
+
}
|
|
618
|
+
|
|
314
619
|
// src/server/sandbox/bwrap/createBwrapSandbox.ts
|
|
315
620
|
var DEFAULT_TIMEOUT_MS2 = BWRAP_TIMEOUT_SECONDS * 1e3;
|
|
316
621
|
var DEFAULT_MAX_OUTPUT_BYTES2 = 1048576;
|
|
@@ -348,16 +653,24 @@ function terminateProcess2(child, signal) {
|
|
|
348
653
|
} catch {
|
|
349
654
|
}
|
|
350
655
|
}
|
|
351
|
-
function computeSandboxCwd(workspaceRoot, cwd) {
|
|
352
|
-
if (!cwd) return
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
if (
|
|
356
|
-
|
|
656
|
+
function computeSandboxCwd(workspaceRoot, runtimeCwd, cwd) {
|
|
657
|
+
if (!cwd) return runtimeCwd;
|
|
658
|
+
const normalizedRuntimeCwd = posix.normalize(runtimeCwd).replace(/\/+$/, "") || "/";
|
|
659
|
+
if (cwd === normalizedRuntimeCwd) return normalizedRuntimeCwd;
|
|
660
|
+
if (cwd.startsWith(`${normalizedRuntimeCwd}/`)) {
|
|
661
|
+
const normalizedCwd = posix.normalize(cwd);
|
|
662
|
+
if (normalizedCwd === normalizedRuntimeCwd) return normalizedRuntimeCwd;
|
|
663
|
+
if (normalizedCwd.startsWith(`${normalizedRuntimeCwd}/`)) return normalizedCwd;
|
|
664
|
+
throw new Error("cwd must stay within workspace root");
|
|
665
|
+
}
|
|
666
|
+
const absoluteCwd = isAbsolute3(cwd) ? cwd : resolve4(workspaceRoot, cwd);
|
|
667
|
+
const relPath = relative3(workspaceRoot, absoluteCwd);
|
|
668
|
+
if (relPath === "") return normalizedRuntimeCwd;
|
|
669
|
+
if (relPath === ".." || relPath.startsWith(`..${sep2}`)) {
|
|
357
670
|
throw new Error("cwd must stay within workspace root");
|
|
358
671
|
}
|
|
359
|
-
const posixRelPath = relPath.split(
|
|
360
|
-
return `${
|
|
672
|
+
const posixRelPath = relPath.split(sep2).join("/");
|
|
673
|
+
return `${normalizedRuntimeCwd}/${posixRelPath}`;
|
|
361
674
|
}
|
|
362
675
|
function withSandboxCwd(baseArgs, sandboxCwd) {
|
|
363
676
|
const args = [...baseArgs];
|
|
@@ -402,7 +715,7 @@ async function assertBwrapAvailable() {
|
|
|
402
715
|
});
|
|
403
716
|
});
|
|
404
717
|
}
|
|
405
|
-
async function
|
|
718
|
+
async function pathExists(path4) {
|
|
406
719
|
try {
|
|
407
720
|
await access(path4, constants.F_OK);
|
|
408
721
|
return true;
|
|
@@ -410,38 +723,58 @@ async function dirAccessible(path4) {
|
|
|
410
723
|
return false;
|
|
411
724
|
}
|
|
412
725
|
}
|
|
726
|
+
async function dirExists(path4) {
|
|
727
|
+
try {
|
|
728
|
+
return (await stat3(path4)).isDirectory();
|
|
729
|
+
} catch {
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
413
733
|
async function buildGlobalToolMounts(workspaceRoot) {
|
|
414
|
-
const globalRoot =
|
|
734
|
+
const globalRoot = dirname4(workspaceRoot);
|
|
415
735
|
if (globalRoot === workspaceRoot) return [];
|
|
416
736
|
const args = [];
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
737
|
+
const mountIfChildLacks = async (runtimeRelPath) => {
|
|
738
|
+
const parentRuntimePath = join2(globalRoot, runtimeRelPath);
|
|
739
|
+
const childRuntimePath = join2(workspaceRoot, runtimeRelPath);
|
|
740
|
+
if (!await dirExists(parentRuntimePath)) return false;
|
|
741
|
+
if (await pathExists(childRuntimePath)) return false;
|
|
742
|
+
args.push("--ro-bind", parentRuntimePath, `${SANDBOX_HOME2}/${runtimeRelPath}`);
|
|
743
|
+
return true;
|
|
744
|
+
};
|
|
745
|
+
const mountedParentAgentDir = await mountIfChildLacks(".boring-agent");
|
|
746
|
+
if (!mountedParentAgentDir) {
|
|
747
|
+
await mountIfChildLacks(".boring-agent/venv");
|
|
422
748
|
}
|
|
423
749
|
return args;
|
|
424
750
|
}
|
|
425
|
-
function createBwrapSandbox() {
|
|
751
|
+
function createBwrapSandbox(opts = {}) {
|
|
426
752
|
let workspace = null;
|
|
753
|
+
let hostWorkspaceRoot = opts.hostWorkspaceRoot;
|
|
754
|
+
let runtimeContext = opts.runtimeContext ?? { runtimeCwd: SANDBOX_HOME2 };
|
|
427
755
|
return {
|
|
428
756
|
id: "bwrap",
|
|
429
757
|
placement: "server",
|
|
430
758
|
provider: "bwrap",
|
|
431
759
|
capabilities: ["exec"],
|
|
760
|
+
get runtimeContext() {
|
|
761
|
+
return runtimeContext;
|
|
762
|
+
},
|
|
432
763
|
async init(ctx) {
|
|
433
764
|
workspace = ctx.workspace;
|
|
765
|
+
hostWorkspaceRoot = opts.hostWorkspaceRoot ?? getNodeWorkspaceHostRoot(ctx.workspace) ?? ctx.workspace.root;
|
|
766
|
+
runtimeContext = opts.runtimeContext ?? { runtimeCwd: SANDBOX_HOME2 };
|
|
434
767
|
await assertBwrapAvailable();
|
|
435
768
|
},
|
|
436
|
-
async exec(cmd,
|
|
769
|
+
async exec(cmd, opts2) {
|
|
437
770
|
if (!workspace) {
|
|
438
771
|
throw new Error("BwrapSandbox not initialized");
|
|
439
772
|
}
|
|
440
773
|
const start = Date.now();
|
|
441
|
-
const timeoutMs =
|
|
442
|
-
const maxOutputBytes =
|
|
443
|
-
const workspaceRoot = workspace.root;
|
|
444
|
-
const sandboxCwd = computeSandboxCwd(workspaceRoot,
|
|
774
|
+
const timeoutMs = opts2?.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
775
|
+
const maxOutputBytes = opts2?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES2;
|
|
776
|
+
const workspaceRoot = hostWorkspaceRoot ?? workspace.root;
|
|
777
|
+
const sandboxCwd = computeSandboxCwd(workspaceRoot, runtimeContext.runtimeCwd, opts2?.cwd);
|
|
445
778
|
const postWorkspaceArgs = await buildGlobalToolMounts(workspaceRoot);
|
|
446
779
|
const baseArgs = buildBwrapArgs(workspaceRoot, { postWorkspaceArgs });
|
|
447
780
|
const args = [
|
|
@@ -450,9 +783,12 @@ function createBwrapSandbox() {
|
|
|
450
783
|
"-c",
|
|
451
784
|
cmd
|
|
452
785
|
];
|
|
453
|
-
return await new Promise((
|
|
786
|
+
return await new Promise((resolve10, reject) => {
|
|
454
787
|
const child = spawn2("bwrap", args, {
|
|
455
|
-
env:
|
|
788
|
+
env: {
|
|
789
|
+
...withWorkspacePythonEnv({ workspaceRoot, env: opts2?.env, sandboxRoot: SANDBOX_HOME2 }),
|
|
790
|
+
PWD: sandboxCwd
|
|
791
|
+
},
|
|
456
792
|
stdio: ["ignore", "pipe", "pipe"],
|
|
457
793
|
detached: process.platform !== "win32"
|
|
458
794
|
});
|
|
@@ -477,7 +813,7 @@ function createBwrapSandbox() {
|
|
|
477
813
|
if (settled) return;
|
|
478
814
|
settled = true;
|
|
479
815
|
cleanup();
|
|
480
|
-
|
|
816
|
+
resolve10({
|
|
481
817
|
stdout: new Uint8Array(Buffer.concat(stdoutChunks)),
|
|
482
818
|
stderr: new Uint8Array(Buffer.concat(stderrChunks)),
|
|
483
819
|
exitCode: typeof exitCode === "number" ? exitCode : timedOut ? 124 : 1,
|
|
@@ -488,10 +824,10 @@ function createBwrapSandbox() {
|
|
|
488
824
|
});
|
|
489
825
|
};
|
|
490
826
|
child.stdout?.on("data", (chunk2) => {
|
|
491
|
-
appendOutput2(stdoutChunks, chunk2, captureState,
|
|
827
|
+
appendOutput2(stdoutChunks, chunk2, captureState, opts2?.onStdout);
|
|
492
828
|
});
|
|
493
829
|
child.stderr?.on("data", (chunk2) => {
|
|
494
|
-
appendOutput2(stderrChunks, chunk2, captureState,
|
|
830
|
+
appendOutput2(stderrChunks, chunk2, captureState, opts2?.onStderr);
|
|
495
831
|
});
|
|
496
832
|
child.on("error", (error) => {
|
|
497
833
|
if (settled) return;
|
|
@@ -509,24 +845,24 @@ function createBwrapSandbox() {
|
|
|
509
845
|
if (!settled) terminateProcess2(child, "SIGKILL");
|
|
510
846
|
}, KILL_GRACE_SECONDS * 1e3);
|
|
511
847
|
}, timeoutMs);
|
|
512
|
-
if (
|
|
848
|
+
if (opts2?.onHeartbeat) {
|
|
513
849
|
heartbeatHandle = setInterval(() => {
|
|
514
|
-
|
|
850
|
+
opts2.onHeartbeat?.(Date.now() - start);
|
|
515
851
|
}, 1e3);
|
|
516
852
|
}
|
|
517
|
-
if (
|
|
853
|
+
if (opts2?.signal) {
|
|
518
854
|
const abort = () => {
|
|
519
855
|
terminateProcess2(child, "SIGTERM");
|
|
520
856
|
killHandle = setTimeout(() => {
|
|
521
857
|
if (!settled) terminateProcess2(child, "SIGKILL");
|
|
522
858
|
}, KILL_GRACE_SECONDS * 1e3);
|
|
523
859
|
};
|
|
524
|
-
if (
|
|
860
|
+
if (opts2.signal.aborted) {
|
|
525
861
|
abort();
|
|
526
862
|
} else {
|
|
527
|
-
|
|
863
|
+
opts2.signal.addEventListener("abort", abort, { once: true });
|
|
528
864
|
child.on("close", () => {
|
|
529
|
-
|
|
865
|
+
opts2.signal?.removeEventListener("abort", abort);
|
|
530
866
|
});
|
|
531
867
|
}
|
|
532
868
|
}
|
|
@@ -536,7 +872,7 @@ function createBwrapSandbox() {
|
|
|
536
872
|
}
|
|
537
873
|
|
|
538
874
|
// src/server/sandbox/vercel-sandbox/FileHandleStore.ts
|
|
539
|
-
import { chmod, mkdir, readFile, rename, unlink, writeFile } from "fs/promises";
|
|
875
|
+
import { chmod, mkdir as mkdir2, readFile as readFile2, rename as rename2, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
|
|
540
876
|
import { homedir } from "os";
|
|
541
877
|
import path from "path";
|
|
542
878
|
var DEFAULT_STORE_PATH = path.join(
|
|
@@ -573,7 +909,7 @@ var FileHandleStore = class {
|
|
|
573
909
|
}
|
|
574
910
|
async readStore() {
|
|
575
911
|
try {
|
|
576
|
-
const raw = await
|
|
912
|
+
const raw = await readFile2(this.storePath, "utf8");
|
|
577
913
|
if (!raw.trim()) {
|
|
578
914
|
return {};
|
|
579
915
|
}
|
|
@@ -587,23 +923,23 @@ var FileHandleStore = class {
|
|
|
587
923
|
}
|
|
588
924
|
}
|
|
589
925
|
async writeStore(store) {
|
|
590
|
-
await
|
|
926
|
+
await mkdir2(path.dirname(this.storePath), { recursive: true, mode: 448 });
|
|
591
927
|
const tmpPath = `${this.storePath}.tmp-${process.pid}-${Date.now()}`;
|
|
592
928
|
const content = `${JSON.stringify(store, null, 2)}
|
|
593
929
|
`;
|
|
594
930
|
let tmpWritten = false;
|
|
595
931
|
let renamed = false;
|
|
596
932
|
try {
|
|
597
|
-
await
|
|
933
|
+
await writeFile2(tmpPath, content, { encoding: "utf8", mode: 384 });
|
|
598
934
|
tmpWritten = true;
|
|
599
935
|
await chmod(tmpPath, 384);
|
|
600
|
-
await
|
|
936
|
+
await rename2(tmpPath, this.storePath);
|
|
601
937
|
renamed = true;
|
|
602
938
|
await chmod(this.storePath, 384);
|
|
603
939
|
} finally {
|
|
604
940
|
if (tmpWritten && !renamed) {
|
|
605
941
|
try {
|
|
606
|
-
await
|
|
942
|
+
await unlink2(tmpPath);
|
|
607
943
|
} catch {
|
|
608
944
|
}
|
|
609
945
|
}
|
|
@@ -997,7 +1333,7 @@ function evictSandboxHandleCacheForWorkspace(workspaceId) {
|
|
|
997
1333
|
|
|
998
1334
|
// src/server/sandbox/vercel-sandbox/bake.ts
|
|
999
1335
|
import { createHash as createHash2 } from "crypto";
|
|
1000
|
-
import { chmod as chmod2, mkdir as
|
|
1336
|
+
import { chmod as chmod2, mkdir as mkdir3, readFile as readFile3, rename as rename3, unlink as unlink3, writeFile as writeFile3 } from "fs/promises";
|
|
1001
1337
|
import { homedir as homedir2 } from "os";
|
|
1002
1338
|
import path2 from "path";
|
|
1003
1339
|
var DEFAULT_RUNTIME = "python3.13";
|
|
@@ -1033,7 +1369,7 @@ function defaultCacheStore() {
|
|
|
1033
1369
|
}
|
|
1034
1370
|
async function readCache(cachePath) {
|
|
1035
1371
|
try {
|
|
1036
|
-
const raw = await
|
|
1372
|
+
const raw = await readFile3(cachePath, "utf8");
|
|
1037
1373
|
if (!raw.trim()) {
|
|
1038
1374
|
return defaultCacheStore();
|
|
1039
1375
|
}
|
|
@@ -1054,23 +1390,23 @@ async function readCache(cachePath) {
|
|
|
1054
1390
|
}
|
|
1055
1391
|
}
|
|
1056
1392
|
async function writeCache(cachePath, store) {
|
|
1057
|
-
await
|
|
1393
|
+
await mkdir3(path2.dirname(cachePath), { recursive: true, mode: 448 });
|
|
1058
1394
|
const tmpPath = `${cachePath}.tmp-${process.pid}-${Date.now()}`;
|
|
1059
1395
|
const content = `${JSON.stringify(store, null, 2)}
|
|
1060
1396
|
`;
|
|
1061
1397
|
let tmpWritten = false;
|
|
1062
1398
|
let renamed = false;
|
|
1063
1399
|
try {
|
|
1064
|
-
await
|
|
1400
|
+
await writeFile3(tmpPath, content, { encoding: "utf8", mode: 384 });
|
|
1065
1401
|
tmpWritten = true;
|
|
1066
1402
|
await chmod2(tmpPath, 384);
|
|
1067
|
-
await
|
|
1403
|
+
await rename3(tmpPath, cachePath);
|
|
1068
1404
|
renamed = true;
|
|
1069
1405
|
await chmod2(cachePath, 384);
|
|
1070
1406
|
} finally {
|
|
1071
1407
|
if (tmpWritten && !renamed) {
|
|
1072
1408
|
try {
|
|
1073
|
-
await
|
|
1409
|
+
await unlink3(tmpPath);
|
|
1074
1410
|
} catch {
|
|
1075
1411
|
}
|
|
1076
1412
|
}
|
|
@@ -1213,334 +1549,60 @@ async function bakeSnapshotIfNeeded(opts) {
|
|
|
1213
1549
|
}
|
|
1214
1550
|
}
|
|
1215
1551
|
|
|
1216
|
-
// src/server/sandbox/snapshots/deploymentSnapshot.ts
|
|
1217
|
-
var UV_SETUP_COMMANDS = [
|
|
1218
|
-
"command -v uv >/dev/null 2>&1 || python3 -m pip install --upgrade uv",
|
|
1219
|
-
"uv --version"
|
|
1220
|
-
];
|
|
1221
|
-
function buildDeploymentSnapshotRecipe(opts = {}) {
|
|
1222
|
-
return {
|
|
1223
|
-
runtime: opts.runtime,
|
|
1224
|
-
systemPackages: opts.systemPackages,
|
|
1225
|
-
pythonPackages: opts.pythonPackages,
|
|
1226
|
-
setupCommands: [
|
|
1227
|
-
...opts.includeUv === false ? [] : UV_SETUP_COMMANDS,
|
|
1228
|
-
...opts.setupCommands ?? []
|
|
1229
|
-
]
|
|
1230
|
-
};
|
|
1231
|
-
}
|
|
1232
|
-
async function prepareDeploymentSnapshot(provider, recipe) {
|
|
1233
|
-
return await provider.prepareDeploymentSnapshot(recipe);
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// src/server/sandbox/vercel-sandbox/deploymentSnapshot.ts
|
|
1237
|
-
function createVercelDeploymentSnapshotProvider(opts) {
|
|
1238
|
-
return {
|
|
1239
|
-
async prepareDeploymentSnapshot(recipe) {
|
|
1240
|
-
return await bakeSnapshotIfNeeded({
|
|
1241
|
-
client: opts.client,
|
|
1242
|
-
snapshotId: opts.snapshotId,
|
|
1243
|
-
runtime: recipe.runtime,
|
|
1244
|
-
cachePath: opts.cachePath,
|
|
1245
|
-
logger: opts.logger,
|
|
1246
|
-
now: opts.now,
|
|
1247
|
-
setupCommands: recipe.setupCommands,
|
|
1248
|
-
pythonPackages: recipe.pythonPackages,
|
|
1249
|
-
systemPackages: recipe.systemPackages
|
|
1250
|
-
});
|
|
1251
|
-
}
|
|
1252
|
-
};
|
|
1253
|
-
}
|
|
1254
|
-
async function prepareVercelDeploymentSnapshot(opts) {
|
|
1255
|
-
return await bakeSnapshotIfNeeded({
|
|
1256
|
-
client: opts.client,
|
|
1257
|
-
snapshotId: opts.snapshotId,
|
|
1258
|
-
runtime: opts.runtime,
|
|
1259
|
-
cachePath: opts.cachePath,
|
|
1260
|
-
logger: opts.logger,
|
|
1261
|
-
now: opts.now,
|
|
1262
|
-
setupCommands: opts.setupCommands,
|
|
1263
|
-
pythonPackages: opts.pythonPackages,
|
|
1264
|
-
systemPackages: opts.systemPackages
|
|
1265
|
-
});
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
// src/server/workspace/createNodeWorkspace.ts
|
|
1269
|
-
import { lstat as lstat2, mkdir as mkdir3, readdir, readFile as readFile3, rename as rename3, rmdir, stat as stat2, unlink as unlink3, writeFile as writeFile3 } from "fs/promises";
|
|
1270
|
-
import { dirname as dirname3, relative as relative3, sep as sep2 } from "path";
|
|
1271
|
-
import chokidar from "chokidar";
|
|
1272
|
-
|
|
1273
|
-
// src/server/workspace/paths.ts
|
|
1274
|
-
import { lstat, realpath, stat } from "fs/promises";
|
|
1275
|
-
import { dirname as dirname2, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve3 } from "path";
|
|
1276
|
-
function createPathValidationError(reason, requestedPath, message) {
|
|
1277
|
-
return Object.assign(new Error(message), {
|
|
1278
|
-
statusCode: 400,
|
|
1279
|
-
reason,
|
|
1280
|
-
requestedPath
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
function normalizeForTraversalChecks(requestedPath) {
|
|
1284
|
-
let decoded = requestedPath;
|
|
1285
|
-
try {
|
|
1286
|
-
decoded = decodeURIComponent(requestedPath);
|
|
1287
|
-
} catch {
|
|
1288
|
-
}
|
|
1289
|
-
return decoded.replace(/\\/g, "/");
|
|
1290
|
-
}
|
|
1291
|
-
function hasTraversalSegment(requestedPath) {
|
|
1292
|
-
return requestedPath.split("/").some((segment) => segment === ".." || segment.startsWith(".."));
|
|
1293
|
-
}
|
|
1294
|
-
function isWindowsAbsolutePath(requestedPath) {
|
|
1295
|
-
return /^[A-Za-z]:[\\/]/.test(requestedPath) || requestedPath.startsWith("\\\\");
|
|
1296
|
-
}
|
|
1297
|
-
function validatePath(workspaceRoot, relPath) {
|
|
1298
|
-
if (relPath.includes("\0")) {
|
|
1299
|
-
throw createPathValidationError("null-byte", relPath, "Null byte in path");
|
|
1300
|
-
}
|
|
1301
|
-
const normalized = normalizeForTraversalChecks(relPath);
|
|
1302
|
-
if (isAbsolute3(relPath) || normalized.startsWith("/") || isWindowsAbsolutePath(relPath) || isWindowsAbsolutePath(normalized)) {
|
|
1303
|
-
throw createPathValidationError("absolute-path", relPath, "Absolute paths are not allowed");
|
|
1304
|
-
}
|
|
1305
|
-
if (hasTraversalSegment(normalized) || normalized.startsWith("~") || normalized.startsWith("$") || /[\r\n]/.test(normalized)) {
|
|
1306
|
-
throw createPathValidationError("path-escape", relPath, "Path escapes workspace root");
|
|
1307
|
-
}
|
|
1308
|
-
const resolvedRoot = resolve3(workspaceRoot);
|
|
1309
|
-
const resolvedPath = resolve3(workspaceRoot, relPath);
|
|
1310
|
-
if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}/`)) {
|
|
1311
|
-
throw createPathValidationError("path-escape", relPath, "Path escapes workspace root");
|
|
1312
|
-
}
|
|
1313
|
-
return resolvedPath;
|
|
1314
|
-
}
|
|
1315
|
-
async function assertRealPathWithinWorkspace(workspaceRoot, absPath) {
|
|
1316
|
-
const realRoot = await realpath(resolve3(workspaceRoot));
|
|
1317
|
-
const realCandidate = await realpath(absPath);
|
|
1318
|
-
const rel = relative2(realRoot, realCandidate);
|
|
1319
|
-
if (rel.startsWith("..") || isAbsolute3(rel)) {
|
|
1320
|
-
throw createPathValidationError(
|
|
1321
|
-
"symlink-escape",
|
|
1322
|
-
absPath,
|
|
1323
|
-
"Resolved path escapes workspace root"
|
|
1324
|
-
);
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
async function ensureExistingWorkspacePath(workspaceRoot, relPath) {
|
|
1328
|
-
const absPath = validatePath(workspaceRoot, relPath);
|
|
1329
|
-
await assertRealPathWithinWorkspace(workspaceRoot, absPath);
|
|
1330
|
-
await stat(absPath);
|
|
1331
|
-
return absPath;
|
|
1332
|
-
}
|
|
1333
|
-
async function ensureWritableWorkspacePath(workspaceRoot, relPath) {
|
|
1334
|
-
const absPath = validatePath(workspaceRoot, relPath);
|
|
1335
|
-
const parentPath = dirname2(absPath);
|
|
1336
|
-
await stat(parentPath);
|
|
1337
|
-
await assertRealPathWithinWorkspace(workspaceRoot, parentPath);
|
|
1338
|
-
try {
|
|
1339
|
-
const pathStat = await lstat(absPath);
|
|
1340
|
-
if (pathStat.isSymbolicLink()) {
|
|
1341
|
-
throw createPathValidationError("symlink-escape", relPath, "Target path is a symlink");
|
|
1342
|
-
}
|
|
1343
|
-
} catch (error) {
|
|
1344
|
-
const code = error.code;
|
|
1345
|
-
if (code !== "ENOENT") {
|
|
1346
|
-
throw error;
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
return absPath;
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
// src/server/workspace/createNodeWorkspace.ts
|
|
1353
|
-
var DEFAULT_WATCH_IGNORES = [
|
|
1354
|
-
"node_modules",
|
|
1355
|
-
".git",
|
|
1356
|
-
".DS_Store",
|
|
1357
|
-
"dist",
|
|
1358
|
-
".next",
|
|
1359
|
-
".turbo",
|
|
1360
|
-
"test-results"
|
|
1361
|
-
];
|
|
1362
|
-
function shouldIgnoreWatchPath(path4) {
|
|
1363
|
-
const parts = path4.split(sep2);
|
|
1364
|
-
return parts.some(
|
|
1365
|
-
(part) => DEFAULT_WATCH_IGNORES.includes(part) || part.endsWith(".tsbuildinfo")
|
|
1366
|
-
);
|
|
1367
|
-
}
|
|
1368
|
-
function createNodeWatcher(root) {
|
|
1369
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
1370
|
-
let fsw = null;
|
|
1371
|
-
let closed = false;
|
|
1372
|
-
const ensureFsw = () => {
|
|
1373
|
-
if (fsw) return fsw;
|
|
1374
|
-
fsw = chokidar.watch(root, {
|
|
1375
|
-
ignored: shouldIgnoreWatchPath,
|
|
1376
|
-
ignoreInitial: true,
|
|
1377
|
-
persistent: true,
|
|
1378
|
-
followSymlinks: false
|
|
1379
|
-
// Avoid chokidar's awaitWriteFinish polling loop in long-lived app
|
|
1380
|
-
// servers. Consumers already reconcile by mtime after invalidation.
|
|
1381
|
-
// No native renames from chokidar — `unlinkDir`/`addDir`/
|
|
1382
|
-
// `unlink`/`add` are the primitives we get. We surface them as
|
|
1383
|
-
// separate `unlink` + `write`/`mkdir` events; renames are best
|
|
1384
|
-
// recovered at the consumer level if needed.
|
|
1385
|
-
});
|
|
1386
|
-
fsw.on("add", (p, s) => emit({ op: "write", path: rel(p), mtimeMs: s?.mtimeMs }));
|
|
1387
|
-
fsw.on("change", (p, s) => emit({ op: "write", path: rel(p), mtimeMs: s?.mtimeMs }));
|
|
1388
|
-
fsw.on("addDir", (p) => emit({ op: "mkdir", path: rel(p) }));
|
|
1389
|
-
fsw.on("unlink", (p) => emit({ op: "unlink", path: rel(p) }));
|
|
1390
|
-
fsw.on("unlinkDir", (p) => emit({ op: "unlink", path: rel(p) }));
|
|
1391
|
-
fsw.on("error", () => {
|
|
1392
|
-
});
|
|
1393
|
-
return fsw;
|
|
1394
|
-
};
|
|
1395
|
-
const rel = (abs) => {
|
|
1396
|
-
const r = relative3(root, abs);
|
|
1397
|
-
return r.split(sep2).join("/");
|
|
1398
|
-
};
|
|
1399
|
-
const emit = (event) => {
|
|
1400
|
-
if (closed) return;
|
|
1401
|
-
if (event.path === "" || event.path.startsWith("..")) return;
|
|
1402
|
-
for (const l of [...listeners]) {
|
|
1403
|
-
try {
|
|
1404
|
-
l(event);
|
|
1405
|
-
} catch {
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
};
|
|
1409
|
-
return {
|
|
1410
|
-
subscribe(listener) {
|
|
1411
|
-
if (closed) return () => {
|
|
1412
|
-
};
|
|
1413
|
-
listeners.add(listener);
|
|
1414
|
-
ensureFsw();
|
|
1415
|
-
return () => {
|
|
1416
|
-
listeners.delete(listener);
|
|
1417
|
-
};
|
|
1418
|
-
},
|
|
1419
|
-
close() {
|
|
1420
|
-
if (closed) return;
|
|
1421
|
-
closed = true;
|
|
1422
|
-
listeners.clear();
|
|
1423
|
-
fsw?.close().catch(() => {
|
|
1424
|
-
});
|
|
1425
|
-
fsw = null;
|
|
1426
|
-
}
|
|
1552
|
+
// src/server/sandbox/snapshots/deploymentSnapshot.ts
|
|
1553
|
+
var UV_SETUP_COMMANDS = [
|
|
1554
|
+
"command -v uv >/dev/null 2>&1 || python3 -m pip install --upgrade uv",
|
|
1555
|
+
"uv --version"
|
|
1556
|
+
];
|
|
1557
|
+
function buildDeploymentSnapshotRecipe(opts = {}) {
|
|
1558
|
+
return {
|
|
1559
|
+
runtime: opts.runtime,
|
|
1560
|
+
systemPackages: opts.systemPackages,
|
|
1561
|
+
pythonPackages: opts.pythonPackages,
|
|
1562
|
+
setupCommands: [
|
|
1563
|
+
...opts.includeUv === false ? [] : UV_SETUP_COMMANDS,
|
|
1564
|
+
...opts.setupCommands ?? []
|
|
1565
|
+
]
|
|
1427
1566
|
};
|
|
1428
1567
|
}
|
|
1429
|
-
function
|
|
1430
|
-
|
|
1568
|
+
async function prepareDeploymentSnapshot(provider, recipe) {
|
|
1569
|
+
return await provider.prepareDeploymentSnapshot(recipe);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// src/server/sandbox/vercel-sandbox/deploymentSnapshot.ts
|
|
1573
|
+
function createVercelDeploymentSnapshotProvider(opts) {
|
|
1431
1574
|
return {
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
return new Uint8Array(await readFile3(absPath));
|
|
1445
|
-
},
|
|
1446
|
-
async writeFile(relPath, data) {
|
|
1447
|
-
const absPath = await ensureWritableWorkspacePath(root, relPath);
|
|
1448
|
-
await writeFile3(absPath, data, "utf-8");
|
|
1449
|
-
},
|
|
1450
|
-
async writeBinaryFile(relPath, data) {
|
|
1451
|
-
const absPath = await ensureWritableWorkspacePath(root, relPath);
|
|
1452
|
-
await writeFile3(absPath, data);
|
|
1453
|
-
},
|
|
1454
|
-
async readFileWithStat(relPath) {
|
|
1455
|
-
const absPath = await ensureExistingWorkspacePath(root, relPath);
|
|
1456
|
-
const [content, fileStat] = await Promise.all([
|
|
1457
|
-
readFile3(absPath, "utf-8"),
|
|
1458
|
-
stat2(absPath)
|
|
1459
|
-
]);
|
|
1460
|
-
return {
|
|
1461
|
-
content,
|
|
1462
|
-
stat: {
|
|
1463
|
-
size: fileStat.size,
|
|
1464
|
-
mtimeMs: fileStat.mtimeMs,
|
|
1465
|
-
kind: fileStat.isDirectory() ? "dir" : "file"
|
|
1466
|
-
}
|
|
1467
|
-
};
|
|
1468
|
-
},
|
|
1469
|
-
async writeFileWithStat(relPath, data) {
|
|
1470
|
-
const absPath = await ensureWritableWorkspacePath(root, relPath);
|
|
1471
|
-
await writeFile3(absPath, data, "utf-8");
|
|
1472
|
-
const fileStat = await stat2(absPath);
|
|
1473
|
-
return {
|
|
1474
|
-
size: fileStat.size,
|
|
1475
|
-
mtimeMs: fileStat.mtimeMs,
|
|
1476
|
-
kind: fileStat.isDirectory() ? "dir" : "file"
|
|
1477
|
-
};
|
|
1478
|
-
},
|
|
1479
|
-
async writeBinaryFileWithStat(relPath, data) {
|
|
1480
|
-
const absPath = await ensureWritableWorkspacePath(root, relPath);
|
|
1481
|
-
await writeFile3(absPath, data);
|
|
1482
|
-
const fileStat = await stat2(absPath);
|
|
1483
|
-
return {
|
|
1484
|
-
size: fileStat.size,
|
|
1485
|
-
mtimeMs: fileStat.mtimeMs,
|
|
1486
|
-
kind: fileStat.isDirectory() ? "dir" : "file"
|
|
1487
|
-
};
|
|
1488
|
-
},
|
|
1489
|
-
async unlink(relPath) {
|
|
1490
|
-
const absPath = await ensureExistingWorkspacePath(root, relPath);
|
|
1491
|
-
const pathStat = await lstat2(absPath);
|
|
1492
|
-
if (pathStat.isDirectory()) {
|
|
1493
|
-
await rmdir(absPath);
|
|
1494
|
-
return;
|
|
1495
|
-
}
|
|
1496
|
-
await unlink3(absPath);
|
|
1497
|
-
},
|
|
1498
|
-
async readdir(relPath) {
|
|
1499
|
-
const absPath = await ensureExistingWorkspacePath(root, relPath);
|
|
1500
|
-
const entries = await readdir(absPath, { withFileTypes: true });
|
|
1501
|
-
return entries.map((entry) => ({
|
|
1502
|
-
name: entry.name,
|
|
1503
|
-
kind: entry.isDirectory() ? "dir" : "file"
|
|
1504
|
-
}));
|
|
1505
|
-
},
|
|
1506
|
-
async stat(relPath) {
|
|
1507
|
-
const absPath = await ensureExistingWorkspacePath(root, relPath);
|
|
1508
|
-
const fileStat = await stat2(absPath);
|
|
1509
|
-
return {
|
|
1510
|
-
size: fileStat.size,
|
|
1511
|
-
mtimeMs: fileStat.mtimeMs,
|
|
1512
|
-
kind: fileStat.isDirectory() ? "dir" : "file"
|
|
1513
|
-
};
|
|
1514
|
-
},
|
|
1515
|
-
async mkdir(relPath, opts) {
|
|
1516
|
-
const absPath = validatePath(root, relPath);
|
|
1517
|
-
let existingAncestor = absPath;
|
|
1518
|
-
while (true) {
|
|
1519
|
-
try {
|
|
1520
|
-
await stat2(existingAncestor);
|
|
1521
|
-
break;
|
|
1522
|
-
} catch (error) {
|
|
1523
|
-
const code = error.code;
|
|
1524
|
-
if (code !== "ENOENT") throw error;
|
|
1525
|
-
const parent = dirname3(existingAncestor);
|
|
1526
|
-
if (parent === existingAncestor) throw error;
|
|
1527
|
-
existingAncestor = parent;
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
await assertRealPathWithinWorkspace(root, existingAncestor);
|
|
1531
|
-
await mkdir3(absPath, { recursive: opts?.recursive ?? false });
|
|
1532
|
-
},
|
|
1533
|
-
async rename(fromRelPath, toRelPath2) {
|
|
1534
|
-
validatePath(root, toRelPath2);
|
|
1535
|
-
const fromAbsPath = await ensureExistingWorkspacePath(root, fromRelPath);
|
|
1536
|
-
const toAbsPath = await ensureWritableWorkspacePath(root, toRelPath2);
|
|
1537
|
-
await rename3(fromAbsPath, toAbsPath);
|
|
1575
|
+
async prepareDeploymentSnapshot(recipe) {
|
|
1576
|
+
return await bakeSnapshotIfNeeded({
|
|
1577
|
+
client: opts.client,
|
|
1578
|
+
snapshotId: opts.snapshotId,
|
|
1579
|
+
runtime: recipe.runtime,
|
|
1580
|
+
cachePath: opts.cachePath,
|
|
1581
|
+
logger: opts.logger,
|
|
1582
|
+
now: opts.now,
|
|
1583
|
+
setupCommands: recipe.setupCommands,
|
|
1584
|
+
pythonPackages: recipe.pythonPackages,
|
|
1585
|
+
systemPackages: recipe.systemPackages
|
|
1586
|
+
});
|
|
1538
1587
|
}
|
|
1539
1588
|
};
|
|
1540
1589
|
}
|
|
1590
|
+
async function prepareVercelDeploymentSnapshot(opts) {
|
|
1591
|
+
return await bakeSnapshotIfNeeded({
|
|
1592
|
+
client: opts.client,
|
|
1593
|
+
snapshotId: opts.snapshotId,
|
|
1594
|
+
runtime: opts.runtime,
|
|
1595
|
+
cachePath: opts.cachePath,
|
|
1596
|
+
logger: opts.logger,
|
|
1597
|
+
now: opts.now,
|
|
1598
|
+
setupCommands: opts.setupCommands,
|
|
1599
|
+
pythonPackages: opts.pythonPackages,
|
|
1600
|
+
systemPackages: opts.systemPackages
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1541
1603
|
|
|
1542
1604
|
// src/server/http/routes/file.ts
|
|
1543
|
-
import { dirname as
|
|
1605
|
+
import { dirname as dirname5, extname, relative as relative4 } from "path/posix";
|
|
1544
1606
|
|
|
1545
1607
|
// src/server/http/middleware.ts
|
|
1546
1608
|
var ERROR_CODE_AUTH_REQUIRED = "auth_required";
|
|
@@ -1739,6 +1801,17 @@ function classifyError(err, reply, subject) {
|
|
|
1739
1801
|
error: { code: ERROR_CODE_ALREADY_EXISTS, message: `${subject} already exists` }
|
|
1740
1802
|
});
|
|
1741
1803
|
}
|
|
1804
|
+
const statusCode = err?.statusCode;
|
|
1805
|
+
const stableCode = err?.code;
|
|
1806
|
+
if (typeof statusCode === "number" && statusCode >= 400 && statusCode < 600) {
|
|
1807
|
+
return reply.code(statusCode).send({
|
|
1808
|
+
error: {
|
|
1809
|
+
code: typeof stableCode === "string" ? stableCode : ERROR_CODE_INTERNAL,
|
|
1810
|
+
message,
|
|
1811
|
+
details: err?.details
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1742
1815
|
return reply.code(500).send({
|
|
1743
1816
|
error: { code: ERROR_CODE_INTERNAL, message }
|
|
1744
1817
|
});
|
|
@@ -1809,7 +1882,7 @@ function basenameForUpload(filename) {
|
|
|
1809
1882
|
}
|
|
1810
1883
|
function markdownUrlFor(sourcePath, assetPath) {
|
|
1811
1884
|
if (!sourcePath) return assetPath;
|
|
1812
|
-
const fromDir =
|
|
1885
|
+
const fromDir = dirname5(sourcePath.replace(/\\/g, "/"));
|
|
1813
1886
|
const rel = relative4(fromDir === "." ? "" : fromDir, assetPath);
|
|
1814
1887
|
return rel && !rel.startsWith(".") ? rel : rel || assetPath;
|
|
1815
1888
|
}
|
|
@@ -1860,8 +1933,8 @@ function fileRoutes(app, opts, done) {
|
|
|
1860
1933
|
error: { code: ERROR_CODE_INTERNAL, message: "workspace does not support binary reads" }
|
|
1861
1934
|
});
|
|
1862
1935
|
}
|
|
1863
|
-
const
|
|
1864
|
-
if (
|
|
1936
|
+
const stat11 = await workspace.stat(path4);
|
|
1937
|
+
if (stat11.kind !== "file") {
|
|
1865
1938
|
return reply.code(400).send({
|
|
1866
1939
|
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "path is not a file", field: "path" }
|
|
1867
1940
|
});
|
|
@@ -1879,12 +1952,12 @@ function fileRoutes(app, opts, done) {
|
|
|
1879
1952
|
try {
|
|
1880
1953
|
const workspace = await resolveWorkspace(request);
|
|
1881
1954
|
if (workspace.readFileWithStat) {
|
|
1882
|
-
const { content: content2, stat:
|
|
1883
|
-
return { content: content2, mtimeMs:
|
|
1955
|
+
const { content: content2, stat: stat12 } = await workspace.readFileWithStat(path4);
|
|
1956
|
+
return { content: content2, mtimeMs: stat12.kind === "file" ? stat12.mtimeMs : void 0 };
|
|
1884
1957
|
}
|
|
1885
1958
|
const content = await workspace.readFile(path4);
|
|
1886
|
-
const
|
|
1887
|
-
return { content, mtimeMs:
|
|
1959
|
+
const stat11 = await workspace.stat(path4);
|
|
1960
|
+
return { content, mtimeMs: stat11.kind === "file" ? stat11.mtimeMs : void 0 };
|
|
1888
1961
|
} catch (err) {
|
|
1889
1962
|
return classifyError(err, reply, "file");
|
|
1890
1963
|
}
|
|
@@ -1933,11 +2006,11 @@ function fileRoutes(app, opts, done) {
|
|
|
1933
2006
|
if (dir) await workspace.mkdir(dir, { recursive: true });
|
|
1934
2007
|
}
|
|
1935
2008
|
const content = body.content;
|
|
1936
|
-
const
|
|
2009
|
+
const stat11 = workspace.writeFileWithStat ? await workspace.writeFileWithStat(path4, content) : await (async () => {
|
|
1937
2010
|
await workspace.writeFile(path4, content);
|
|
1938
2011
|
return await workspace.stat(path4);
|
|
1939
2012
|
})();
|
|
1940
|
-
return { ok: true, mtimeMs:
|
|
2013
|
+
return { ok: true, mtimeMs: stat11.kind === "file" ? stat11.mtimeMs : void 0 };
|
|
1941
2014
|
} catch (err) {
|
|
1942
2015
|
return classifyError(err, reply, "file");
|
|
1943
2016
|
}
|
|
@@ -1970,7 +2043,7 @@ function fileRoutes(app, opts, done) {
|
|
|
1970
2043
|
}
|
|
1971
2044
|
const bytes = Buffer.from(contentBase64, "base64");
|
|
1972
2045
|
await workspace.mkdir(dir, { recursive: true });
|
|
1973
|
-
const
|
|
2046
|
+
const stat11 = workspace.writeBinaryFileWithStat ? await workspace.writeBinaryFileWithStat(path4, bytes) : await (async () => {
|
|
1974
2047
|
if (!workspace.writeBinaryFile) {
|
|
1975
2048
|
throw new Error("workspace does not support binary uploads");
|
|
1976
2049
|
}
|
|
@@ -1984,7 +2057,7 @@ function fileRoutes(app, opts, done) {
|
|
|
1984
2057
|
ok: true,
|
|
1985
2058
|
path: path4,
|
|
1986
2059
|
markdownUrl: markdownUrlFor(sourcePath, path4),
|
|
1987
|
-
mtimeMs:
|
|
2060
|
+
mtimeMs: stat11.kind === "file" ? stat11.mtimeMs : void 0
|
|
1988
2061
|
};
|
|
1989
2062
|
} catch (err) {
|
|
1990
2063
|
return classifyError(err, reply, "upload");
|
|
@@ -2071,8 +2144,8 @@ function fileRoutes(app, opts, done) {
|
|
|
2071
2144
|
if (path4 === null) return;
|
|
2072
2145
|
try {
|
|
2073
2146
|
const workspace = await resolveWorkspace(request);
|
|
2074
|
-
const
|
|
2075
|
-
return
|
|
2147
|
+
const stat11 = await workspace.stat(path4);
|
|
2148
|
+
return stat11;
|
|
2076
2149
|
} catch (err) {
|
|
2077
2150
|
return classifyError(err, reply, "path");
|
|
2078
2151
|
}
|
|
@@ -2083,8 +2156,8 @@ function fileRoutes(app, opts, done) {
|
|
|
2083
2156
|
// src/server/workspace/provisionRuntime.ts
|
|
2084
2157
|
import { createHash as createHash3 } from "crypto";
|
|
2085
2158
|
import { constants as constants2 } from "fs";
|
|
2086
|
-
import { access as access2, chmod as chmod3, cp, mkdir as mkdir4, readdir as readdir2, readFile as readFile4, realpath as realpath2, stat as
|
|
2087
|
-
import { dirname as
|
|
2159
|
+
import { access as access2, chmod as chmod3, cp, mkdir as mkdir4, readdir as readdir2, readFile as readFile4, realpath as realpath2, stat as stat4, writeFile as writeFile4 } from "fs/promises";
|
|
2160
|
+
import { dirname as dirname6, isAbsolute as isAbsolute4, join as join3, relative as relative5, resolve as resolve5 } from "path";
|
|
2088
2161
|
import { fileURLToPath } from "url";
|
|
2089
2162
|
import { execFile } from "child_process";
|
|
2090
2163
|
import { promisify } from "util";
|
|
@@ -2118,7 +2191,7 @@ async function commandExists(cmd) {
|
|
|
2118
2191
|
}
|
|
2119
2192
|
}
|
|
2120
2193
|
async function hashPath(path4, hash) {
|
|
2121
|
-
const info = await
|
|
2194
|
+
const info = await stat4(path4);
|
|
2122
2195
|
if (info.isDirectory()) {
|
|
2123
2196
|
const entries = (await readdir2(path4)).filter((entry) => entry !== "__pycache__" && entry !== "build" && !entry.endsWith(".egg-info")).sort();
|
|
2124
2197
|
for (const entry of entries) {
|
|
@@ -2151,7 +2224,7 @@ async function fingerprint(contributions) {
|
|
|
2151
2224
|
`);
|
|
2152
2225
|
hash.update(JSON.stringify(spec.extraLibs ?? []));
|
|
2153
2226
|
hash.update(JSON.stringify(Object.fromEntries(Object.entries(spec.env ?? {}).map(([k, v]) => [k, String(v)]))));
|
|
2154
|
-
if (await exists(projectFile)) await hashPath(
|
|
2227
|
+
if (await exists(projectFile)) await hashPath(dirname6(projectFile), hash);
|
|
2155
2228
|
}
|
|
2156
2229
|
for (const spec of provisioning.nodePackages ?? []) {
|
|
2157
2230
|
const packageRoot = toPath(spec.packageRoot);
|
|
@@ -2193,8 +2266,8 @@ function resolveTemplateTarget(workspaceRoot, target) {
|
|
|
2193
2266
|
if (rawTarget.length === 0 || rawTarget.includes("\0") || rawTarget.includes("\\") || rawTarget.startsWith("/") || rawTarget.startsWith("//") || /^[A-Za-z]:[\\/]/.test(rawTarget) || rawTarget.split("/").includes("..")) {
|
|
2194
2267
|
throw new Error(`Unsafe runtime template target: ${JSON.stringify(rawTarget)}. Template targets must be relative paths inside the workspace.`);
|
|
2195
2268
|
}
|
|
2196
|
-
const root =
|
|
2197
|
-
const resolved =
|
|
2269
|
+
const root = resolve5(workspaceRoot);
|
|
2270
|
+
const resolved = resolve5(root, rawTarget);
|
|
2198
2271
|
const rel = relative5(root, resolved);
|
|
2199
2272
|
if (rel.startsWith("..") || isAbsolute4(rel)) {
|
|
2200
2273
|
throw new Error(`Unsafe runtime template target: ${JSON.stringify(rawTarget)} resolves outside the workspace.`);
|
|
@@ -2221,7 +2294,7 @@ function nodePackageTarget(workspaceRoot, packageName) {
|
|
|
2221
2294
|
}
|
|
2222
2295
|
async function copyIfExists(source, target) {
|
|
2223
2296
|
if (!await exists(source)) return false;
|
|
2224
|
-
if (
|
|
2297
|
+
if (resolve5(source) === resolve5(target)) return true;
|
|
2225
2298
|
if (await exists(target) && await realpath2(source) === await realpath2(target)) return true;
|
|
2226
2299
|
await cp(source, target, {
|
|
2227
2300
|
recursive: true,
|
|
@@ -2262,7 +2335,7 @@ async function ensurePython(workspaceRoot, specs) {
|
|
|
2262
2335
|
else await run("/usr/bin/python3", ["-m", "venv", ".boring-agent/venv"], workspaceRoot);
|
|
2263
2336
|
}
|
|
2264
2337
|
for (const spec of specs) {
|
|
2265
|
-
const projectDir =
|
|
2338
|
+
const projectDir = dirname6(toPath(spec.projectFile));
|
|
2266
2339
|
if (uv) {
|
|
2267
2340
|
await run("uv", ["pip", "install", "--python", venvPython, projectDir], workspaceRoot);
|
|
2268
2341
|
if (spec.extraLibs?.length) {
|
|
@@ -2324,7 +2397,7 @@ VENV_BIN="$WORKSPACE_ROOT/.boring-agent/venv/bin"
|
|
|
2324
2397
|
for (const entry of await readdir2(venvBin)) {
|
|
2325
2398
|
if (["python", "python3", "pip", "pip3"].includes(entry)) continue;
|
|
2326
2399
|
const full = join3(venvBin, entry);
|
|
2327
|
-
const info = await
|
|
2400
|
+
const info = await stat4(full).catch(() => null);
|
|
2328
2401
|
if (!info?.isFile()) continue;
|
|
2329
2402
|
await writeExecutable(join3(shimDir, entry), `${base}TARGET="$VENV_BIN"/${bashSingleQuote(entry)}
|
|
2330
2403
|
SHEBANG="$(head -n 1 "$TARGET" 2>/dev/null || true)"
|
|
@@ -2365,14 +2438,16 @@ async function provisionRuntimeWorkspace({
|
|
|
2365
2438
|
await ensureNodePackages(workspaceRoot, active.flatMap(({ provisioning }) => provisioning.nodePackages ?? []));
|
|
2366
2439
|
await ensurePython(workspaceRoot, active.flatMap(({ provisioning }) => provisioning.python ?? []));
|
|
2367
2440
|
const actualBinDir = await writeShims(workspaceRoot, env);
|
|
2368
|
-
await mkdir4(
|
|
2441
|
+
await mkdir4(dirname6(markerPath), { recursive: true });
|
|
2369
2442
|
await writeFile4(markerPath, JSON.stringify({ v: PROVISIONING_VERSION, fingerprint: hash }, null, 2), "utf8");
|
|
2370
2443
|
return { fingerprint: hash, changed: true, env, binDir: actualBinDir };
|
|
2371
2444
|
}
|
|
2372
2445
|
|
|
2373
2446
|
// src/server/workspace/createVercelSandboxWorkspace.ts
|
|
2374
|
-
var VERCEL_SANDBOX_REMOTE_ROOT = "/vercel/sandbox";
|
|
2375
2447
|
var VERCEL_SANDBOX_WORKSPACE_ROOT = "/workspace";
|
|
2448
|
+
var VERCEL_SANDBOX_REMOTE_ROOT = VERCEL_SANDBOX_WORKSPACE_ROOT;
|
|
2449
|
+
var VERCEL_SANDBOX_RUNTIME_CONTEXT = { runtimeCwd: VERCEL_SANDBOX_WORKSPACE_ROOT };
|
|
2450
|
+
var EPERM_CODE2 = "EPERM";
|
|
2376
2451
|
var CACHE_TTL_MS = 15e3;
|
|
2377
2452
|
var CACHE_MAX_ENTRIES = 512;
|
|
2378
2453
|
var MAX_INLINE_WRITE_BYTES = 128 * 1024;
|
|
@@ -2409,11 +2484,11 @@ function createTimedLruCache(ttlMs, maxEntries) {
|
|
|
2409
2484
|
}
|
|
2410
2485
|
};
|
|
2411
2486
|
}
|
|
2412
|
-
function cloneStat(
|
|
2487
|
+
function cloneStat(stat11) {
|
|
2413
2488
|
return {
|
|
2414
|
-
size:
|
|
2415
|
-
mtimeMs:
|
|
2416
|
-
kind:
|
|
2489
|
+
size: stat11.size,
|
|
2490
|
+
mtimeMs: stat11.mtimeMs,
|
|
2491
|
+
kind: stat11.kind
|
|
2417
2492
|
};
|
|
2418
2493
|
}
|
|
2419
2494
|
function cloneEntries(entries) {
|
|
@@ -2496,8 +2571,44 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
|
|
|
2496
2571
|
registerMetadataInvalidator(sandbox, invalidateMetadataCache);
|
|
2497
2572
|
const { emit: emitChange, watcher } = createSandboxBroadcaster();
|
|
2498
2573
|
const remote = sandbox;
|
|
2574
|
+
async function assertRealPathWithinSandboxRoot(sandboxPath) {
|
|
2575
|
+
const isWithinRoot = await runJson(
|
|
2576
|
+
remote,
|
|
2577
|
+
`node -e ${shellQuote2(`const fs=require('fs'); const path=require('path'); const root=fs.realpathSync(process.argv[1]); const target=fs.realpathSync(process.argv[2]); const rel=path.relative(root,target); process.stdout.write(JSON.stringify(rel===''||(!rel.startsWith('..')&&!path.isAbsolute(rel))))`)} ${shellQuote2(VERCEL_SANDBOX_REMOTE_ROOT)} ${shellQuote2(sandboxPath)}`
|
|
2578
|
+
);
|
|
2579
|
+
if (!isWithinRoot) {
|
|
2580
|
+
throw Object.assign(new Error("resolved path escapes workspace root"), { code: EPERM_CODE2 });
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
async function isSandboxSymlink(sandboxPath) {
|
|
2584
|
+
return await runJson(
|
|
2585
|
+
remote,
|
|
2586
|
+
`node -e ${shellQuote2(`const fs=require('fs'); process.stdout.write(JSON.stringify(fs.lstatSync(process.argv[1]).isSymbolicLink()))`)} ${shellQuote2(sandboxPath)}`
|
|
2587
|
+
);
|
|
2588
|
+
}
|
|
2589
|
+
async function listDescendantPaths(relPath, sandboxPath) {
|
|
2590
|
+
if (remote.fs?.stat && remote.fs.readdir) {
|
|
2591
|
+
const fileStat = await remote.fs.stat(sandboxPath);
|
|
2592
|
+
if (!fileStat.isDirectory()) return [];
|
|
2593
|
+
const entries = await remote.fs.readdir(sandboxPath, { withFileTypes: true });
|
|
2594
|
+
const descendants = [];
|
|
2595
|
+
for (const entry of entries) {
|
|
2596
|
+
const childRelPath = relPath === "." ? entry.name : `${relPath}/${entry.name}`;
|
|
2597
|
+
descendants.push(childRelPath);
|
|
2598
|
+
if (entry.isDirectory()) {
|
|
2599
|
+
descendants.push(...await listDescendantPaths(childRelPath, `${sandboxPath}/${entry.name}`));
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
return descendants;
|
|
2603
|
+
}
|
|
2604
|
+
return await runJson(
|
|
2605
|
+
remote,
|
|
2606
|
+
`node -e ${shellQuote2(`const fs=require('fs'); const path=require('path'); const root=process.argv[1]; const relRoot=process.argv[2]; function walk(abs,rel){ const s=fs.statSync(abs); if(!s.isDirectory()) return []; const out=[]; for (const entry of fs.readdirSync(abs,{withFileTypes:true})) { const childRel=rel==='.'?entry.name:rel+'/'+entry.name; out.push(childRel); if(entry.isDirectory()) out.push(...walk(path.join(abs,entry.name),childRel)); } return out; } process.stdout.write(JSON.stringify(walk(root,relRoot)))`)} ${shellQuote2(sandboxPath)} ${shellQuote2(relPath)}`
|
|
2607
|
+
);
|
|
2608
|
+
}
|
|
2499
2609
|
return {
|
|
2500
|
-
root:
|
|
2610
|
+
root: VERCEL_SANDBOX_RUNTIME_CONTEXT.runtimeCwd,
|
|
2611
|
+
runtimeContext: VERCEL_SANDBOX_RUNTIME_CONTEXT,
|
|
2501
2612
|
fsCapability: "best-effort",
|
|
2502
2613
|
watch() {
|
|
2503
2614
|
return watcher;
|
|
@@ -2512,7 +2623,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
|
|
|
2512
2623
|
}
|
|
2513
2624
|
const content = await remote.readFileToBuffer?.({ path: sandboxPath });
|
|
2514
2625
|
if (!content) {
|
|
2515
|
-
const err = new Error(`ENOENT: file not found, open '${
|
|
2626
|
+
const err = new Error(`ENOENT: file not found, open '${validatePath(VERCEL_SANDBOX_WORKSPACE_ROOT, relPath)}'`);
|
|
2516
2627
|
err.code = "ENOENT";
|
|
2517
2628
|
throw err;
|
|
2518
2629
|
}
|
|
@@ -2526,7 +2637,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
|
|
|
2526
2637
|
}
|
|
2527
2638
|
const content = await remote.readFileToBuffer?.({ path: sandboxPath });
|
|
2528
2639
|
if (!content) {
|
|
2529
|
-
const err = new Error(`ENOENT: file not found, open '${
|
|
2640
|
+
const err = new Error(`ENOENT: file not found, open '${validatePath(VERCEL_SANDBOX_WORKSPACE_ROOT, relPath)}'`);
|
|
2530
2641
|
err.code = "ENOENT";
|
|
2531
2642
|
throw err;
|
|
2532
2643
|
}
|
|
@@ -2571,15 +2682,15 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
|
|
|
2571
2682
|
remote.fs.readFile(sandboxPath, "utf8"),
|
|
2572
2683
|
remote.fs.stat(sandboxPath)
|
|
2573
2684
|
]);
|
|
2574
|
-
const
|
|
2685
|
+
const stat11 = {
|
|
2575
2686
|
size: fileStat.size,
|
|
2576
2687
|
mtimeMs: fileStat.mtimeMs,
|
|
2577
2688
|
kind: fileStat.isDirectory() ? "dir" : "file"
|
|
2578
2689
|
};
|
|
2579
|
-
statCache.set(sandboxPath,
|
|
2690
|
+
statCache.set(sandboxPath, stat11);
|
|
2580
2691
|
return {
|
|
2581
2692
|
content: typeof content === "string" ? content : Buffer.from(content).toString("utf-8"),
|
|
2582
|
-
stat: cloneStat(
|
|
2693
|
+
stat: cloneStat(stat11)
|
|
2583
2694
|
};
|
|
2584
2695
|
}
|
|
2585
2696
|
const version = metadataVersion;
|
|
@@ -2670,11 +2781,19 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
|
|
|
2670
2781
|
},
|
|
2671
2782
|
async unlink(relPath) {
|
|
2672
2783
|
const sandboxPath = toSandboxPath(relPath);
|
|
2673
|
-
if (
|
|
2674
|
-
|
|
2784
|
+
if (sandboxPath === VERCEL_SANDBOX_REMOTE_ROOT) {
|
|
2785
|
+
throw Object.assign(new Error("cannot remove workspace root"), { code: EPERM_CODE2 });
|
|
2786
|
+
}
|
|
2787
|
+
await assertRealPathWithinSandboxRoot(sandboxPath);
|
|
2788
|
+
const descendantPaths = await isSandboxSymlink(sandboxPath) ? [] : await listDescendantPaths(relPath, sandboxPath);
|
|
2789
|
+
if (remote.fs?.rm) await remote.fs.rm(sandboxPath, { recursive: true, force: false });
|
|
2790
|
+
else await runShell(remote, `rm -r -- ${shellQuote2(sandboxPath)}`);
|
|
2675
2791
|
invalidateMetadataCache();
|
|
2676
2792
|
workspaceOpts.onMutation?.();
|
|
2677
2793
|
emitChange({ op: "unlink", path: relPath });
|
|
2794
|
+
for (const path4 of descendantPaths) {
|
|
2795
|
+
emitChange({ op: "unlink", path: path4 });
|
|
2796
|
+
}
|
|
2678
2797
|
},
|
|
2679
2798
|
async readdir(relPath) {
|
|
2680
2799
|
const sandboxPath = toSandboxPath(relPath);
|
|
@@ -2777,7 +2896,8 @@ function artifactExtension(kind) {
|
|
|
2777
2896
|
function artifactName(kind, id, fingerprint2) {
|
|
2778
2897
|
const safeId = id.replace(/[^A-Za-z0-9._-]/g, "-");
|
|
2779
2898
|
const safeFingerprint = fingerprint2.replace(/^sha256:/, "");
|
|
2780
|
-
|
|
2899
|
+
const formatVersion = kind === "node" ? "pnpm-pack-v2" : "v1";
|
|
2900
|
+
return `${safeId}-${formatVersion}-${safeFingerprint}${artifactExtension(kind)}`;
|
|
2781
2901
|
}
|
|
2782
2902
|
function createVercelProvisioningAdapter(options) {
|
|
2783
2903
|
return {
|
|
@@ -2826,8 +2946,8 @@ function createVercelProvisioningAdapter(options) {
|
|
|
2826
2946
|
// src/server/workspace/provisioning/fingerprint.ts
|
|
2827
2947
|
import { createHash as createHash4 } from "crypto";
|
|
2828
2948
|
import { constants as constants3 } from "fs";
|
|
2829
|
-
import { access as access3, mkdir as mkdir5, open, readFile as readFile5, rename as rename4, rm } from "fs/promises";
|
|
2830
|
-
import { dirname as
|
|
2949
|
+
import { access as access3, mkdir as mkdir5, open, readFile as readFile5, rename as rename4, rm as rm2 } from "fs/promises";
|
|
2950
|
+
import { dirname as dirname7 } from "path";
|
|
2831
2951
|
var FINGERPRINT_RE = /^sha256:[a-f0-9]{64}$/;
|
|
2832
2952
|
function isValidFingerprint(value) {
|
|
2833
2953
|
return FINGERPRINT_RE.test(value.trim());
|
|
@@ -2996,7 +3116,8 @@ async function ensureNodeRuntime(options) {
|
|
|
2996
3116
|
env: getBoringAgentRuntimeEnv(
|
|
2997
3117
|
options.runtimeLayout,
|
|
2998
3118
|
options.adapter.getRuntimeCacheRoot()
|
|
2999
|
-
)
|
|
3119
|
+
),
|
|
3120
|
+
timeoutMs: 3e5
|
|
3000
3121
|
});
|
|
3001
3122
|
} catch (error) {
|
|
3002
3123
|
throw toProvisioningError(
|
|
@@ -3016,7 +3137,7 @@ async function ensureNodeRuntime(options) {
|
|
|
3016
3137
|
}
|
|
3017
3138
|
|
|
3018
3139
|
// src/server/workspace/provisioning/python.ts
|
|
3019
|
-
import { dirname as
|
|
3140
|
+
import { dirname as dirname8, join as join6, relative as relative7, sep as sep4 } from "path";
|
|
3020
3141
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3021
3142
|
var VENV_REL = ".boring-agent/venv";
|
|
3022
3143
|
var VENV_FINGERPRINT_REL = `${VENV_REL}/.fingerprint`;
|
|
@@ -3075,7 +3196,7 @@ function pythonInstallSource(spec) {
|
|
|
3075
3196
|
}
|
|
3076
3197
|
function sourceRootForPythonSpec(spec) {
|
|
3077
3198
|
if (spec.packageRoot) return spec.packageRoot;
|
|
3078
|
-
if (spec.projectFile) return
|
|
3199
|
+
if (spec.projectFile) return dirname8(sourceToPath(spec.projectFile));
|
|
3079
3200
|
return null;
|
|
3080
3201
|
}
|
|
3081
3202
|
function expectedPythonOutputs(paths, packages) {
|
|
@@ -3193,7 +3314,7 @@ async function ensurePythonRuntime(options) {
|
|
|
3193
3314
|
}
|
|
3194
3315
|
|
|
3195
3316
|
// src/server/workspace/provisioning/skills.ts
|
|
3196
|
-
import { stat as
|
|
3317
|
+
import { stat as stat5 } from "fs/promises";
|
|
3197
3318
|
import { join as join7 } from "path";
|
|
3198
3319
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3199
3320
|
var GENERATED_SKILLS_REL = ".boring-agent/skills";
|
|
@@ -3224,7 +3345,7 @@ async function mirrorPluginSkills(options) {
|
|
|
3224
3345
|
}
|
|
3225
3346
|
seen.add(key);
|
|
3226
3347
|
const sourcePath = sourceToPath2(skill.source);
|
|
3227
|
-
const sourceStat = await
|
|
3348
|
+
const sourceStat = await stat5(sourcePath);
|
|
3228
3349
|
const skillTarget = `${GENERATED_SKILLS_REL}/${plugin.id}/${skill.name}`;
|
|
3229
3350
|
const target = sourceStat.isDirectory() ? skillTarget : `${skillTarget}/SKILL.md`;
|
|
3230
3351
|
await options.adapter.workspaceFs.copyFromHost(skill.source, target);
|
|
@@ -3239,21 +3360,21 @@ async function mirrorPluginSkills(options) {
|
|
|
3239
3360
|
|
|
3240
3361
|
// src/server/workspace/provisioning/workspaceFiles.ts
|
|
3241
3362
|
import { basename, join as joinPath } from "path";
|
|
3242
|
-
import { posix } from "path";
|
|
3243
|
-
import { readdir as readdir3, stat as
|
|
3363
|
+
import { posix as posix2 } from "path";
|
|
3364
|
+
import { readdir as readdir3, stat as stat6 } from "fs/promises";
|
|
3244
3365
|
import { fileURLToPath as fileURLToPath4, pathToFileURL } from "url";
|
|
3245
3366
|
function sourceToPath3(source) {
|
|
3246
3367
|
return source instanceof URL ? fileURLToPath4(source) : source;
|
|
3247
3368
|
}
|
|
3248
3369
|
function toWorkspaceRel3(...parts) {
|
|
3249
|
-
return
|
|
3370
|
+
return posix2.normalize(parts.filter(Boolean).join("/"));
|
|
3250
3371
|
}
|
|
3251
3372
|
function sourceForChild(rootSource, childPath) {
|
|
3252
3373
|
return rootSource instanceof URL ? pathToFileURL(childPath) : childPath;
|
|
3253
3374
|
}
|
|
3254
3375
|
async function collectTemplateWorkItems(template) {
|
|
3255
3376
|
const sourcePath = sourceToPath3(template.path);
|
|
3256
|
-
const sourceStat = await
|
|
3377
|
+
const sourceStat = await stat6(sourcePath);
|
|
3257
3378
|
const targetPrefix = template.target ?? "";
|
|
3258
3379
|
if (!sourceStat.isDirectory()) {
|
|
3259
3380
|
const targetRel = toWorkspaceRel3(targetPrefix || basename(sourcePath));
|
|
@@ -3570,7 +3691,7 @@ function createServerFileSearch(workspace, sandbox) {
|
|
|
3570
3691
|
const command = [
|
|
3571
3692
|
"find .",
|
|
3572
3693
|
"-maxdepth 10",
|
|
3573
|
-
"-path './.boring-agent' -prune -o",
|
|
3694
|
+
"\\( -path './.boring-agent' -o -path './.git' -o -path './node_modules' \\) -prune -o",
|
|
3574
3695
|
buildFindArgs(glob),
|
|
3575
3696
|
"-type f",
|
|
3576
3697
|
"-print",
|
|
@@ -3611,8 +3732,8 @@ async function copyTemplate(templatePath, workspaceRoot) {
|
|
|
3611
3732
|
|
|
3612
3733
|
// src/server/runtime/modes/provisioningAdapter.ts
|
|
3613
3734
|
import { spawn as spawn3 } from "child_process";
|
|
3614
|
-
import { cp as cp3, lstat as lstat3, mkdir as mkdir6, readFile as readFile6, realpath as realpath3, rm as
|
|
3615
|
-
import { dirname as
|
|
3735
|
+
import { cp as cp3, lstat as lstat3, mkdir as mkdir6, readFile as readFile6, realpath as realpath3, rm as rm3, stat as stat7, writeFile as writeFile5 } from "fs/promises";
|
|
3736
|
+
import { dirname as dirname9, isAbsolute as isAbsolute5, relative as relative8, resolve as resolve6, sep as sep5 } from "path";
|
|
3616
3737
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
3617
3738
|
var LOCAL_SANDBOX_WORKSPACE_ROOT = "/workspace";
|
|
3618
3739
|
function sourceToPath4(source) {
|
|
@@ -3630,8 +3751,8 @@ async function assertExistingInsideWorkspace(root, relPath) {
|
|
|
3630
3751
|
}
|
|
3631
3752
|
async function prepareWritablePath(root, relPath) {
|
|
3632
3753
|
const absPath = validatePath(root, relPath);
|
|
3633
|
-
await mkdir6(
|
|
3634
|
-
await assertRealPathWithinWorkspace(root,
|
|
3754
|
+
await mkdir6(dirname9(absPath), { recursive: true });
|
|
3755
|
+
await assertRealPathWithinWorkspace(root, dirname9(absPath));
|
|
3635
3756
|
try {
|
|
3636
3757
|
const targetStat = await lstat3(absPath);
|
|
3637
3758
|
if (targetStat.isSymbolicLink()) {
|
|
@@ -3694,7 +3815,7 @@ function defaultExecOptions(paths, opts) {
|
|
|
3694
3815
|
};
|
|
3695
3816
|
}
|
|
3696
3817
|
function mapWorkspacePathToLocalSandbox(paths, value) {
|
|
3697
|
-
const absolute = isAbsolute5(value) ? value :
|
|
3818
|
+
const absolute = isAbsolute5(value) ? value : resolve6(paths.workspaceRoot, value);
|
|
3698
3819
|
const relPath = relative8(paths.workspaceRoot, absolute);
|
|
3699
3820
|
if (relPath === "") return LOCAL_SANDBOX_WORKSPACE_ROOT;
|
|
3700
3821
|
if (relPath === ".." || relPath.startsWith(`..${sep5}`)) return value;
|
|
@@ -3716,10 +3837,10 @@ async function copyExternalSourceIntoWorkspace(paths, sourcePath, opts) {
|
|
|
3716
3837
|
const fingerprint2 = opts.fingerprint.replace(/^sha256:/, "");
|
|
3717
3838
|
const relTarget = `.boring-agent/tmp/${sanitizeInstallSourcePart(opts.kind)}-${sanitizeInstallSourcePart(opts.id)}-${sanitizeInstallSourcePart(fingerprint2)}-source`;
|
|
3718
3839
|
const absTarget = validatePath(paths.workspaceRoot, relTarget);
|
|
3719
|
-
await
|
|
3720
|
-
await mkdir6(
|
|
3721
|
-
await assertRealPathWithinWorkspace(paths.workspaceRoot,
|
|
3722
|
-
const sourceStat = await
|
|
3840
|
+
await rm3(absTarget, { recursive: true, force: true });
|
|
3841
|
+
await mkdir6(dirname9(absTarget), { recursive: true });
|
|
3842
|
+
await assertRealPathWithinWorkspace(paths.workspaceRoot, dirname9(absTarget));
|
|
3843
|
+
const sourceStat = await stat7(sourcePath);
|
|
3723
3844
|
await cp3(sourcePath, absTarget, {
|
|
3724
3845
|
recursive: sourceStat.isDirectory(),
|
|
3725
3846
|
force: false,
|
|
@@ -3738,7 +3859,7 @@ function createWorkspaceFs(workspaceRoot) {
|
|
|
3738
3859
|
async rm(workspaceRelativePath) {
|
|
3739
3860
|
const absPath = await assertExistingInsideWorkspace(workspaceRoot, workspaceRelativePath);
|
|
3740
3861
|
if (!absPath) return;
|
|
3741
|
-
await
|
|
3862
|
+
await rm3(absPath, { recursive: true, force: true });
|
|
3742
3863
|
},
|
|
3743
3864
|
async mkdir(workspaceRelativePath) {
|
|
3744
3865
|
const absPath = validatePath(workspaceRoot, workspaceRelativePath);
|
|
@@ -3757,7 +3878,7 @@ function createWorkspaceFs(workspaceRoot) {
|
|
|
3757
3878
|
async copyFromHost(hostSourcePath, workspaceRelativeTarget) {
|
|
3758
3879
|
const sourcePath = sourceToPath4(hostSourcePath);
|
|
3759
3880
|
const absTarget = await prepareWritablePath(workspaceRoot, workspaceRelativeTarget);
|
|
3760
|
-
const sourceStat = await
|
|
3881
|
+
const sourceStat = await stat7(sourcePath);
|
|
3761
3882
|
await cp3(sourcePath, absTarget, {
|
|
3762
3883
|
recursive: sourceStat.isDirectory(),
|
|
3763
3884
|
force: false,
|
|
@@ -3832,10 +3953,13 @@ var directModeAdapter = {
|
|
|
3832
3953
|
async create(ctx) {
|
|
3833
3954
|
await mkdir7(ctx.workspaceRoot, { recursive: true });
|
|
3834
3955
|
await copyTemplate(ctx.templatePath, ctx.workspaceRoot);
|
|
3835
|
-
const
|
|
3836
|
-
const
|
|
3956
|
+
const runtimeContext = { runtimeCwd: ctx.workspaceRoot };
|
|
3957
|
+
const workspace = createNodeWorkspace(ctx.workspaceRoot, { runtimeContext });
|
|
3958
|
+
const sandbox = createDirectSandbox({ runtimeContext });
|
|
3837
3959
|
await sandbox.init?.({ workspace, sessionId: ctx.sessionId });
|
|
3838
3960
|
return {
|
|
3961
|
+
runtimeContext,
|
|
3962
|
+
storageRoot: ctx.workspaceRoot,
|
|
3839
3963
|
workspace,
|
|
3840
3964
|
sandbox,
|
|
3841
3965
|
fileSearch: createServerFileSearch(workspace, sandbox)
|
|
@@ -3855,10 +3979,16 @@ var localModeAdapter = {
|
|
|
3855
3979
|
}
|
|
3856
3980
|
await mkdir8(ctx.workspaceRoot, { recursive: true });
|
|
3857
3981
|
await copyTemplate(ctx.templatePath, ctx.workspaceRoot);
|
|
3858
|
-
const
|
|
3859
|
-
const
|
|
3982
|
+
const runtimeContext = { runtimeCwd: "/workspace" };
|
|
3983
|
+
const workspace = createNodeWorkspace(ctx.workspaceRoot, { runtimeContext });
|
|
3984
|
+
const sandbox = createBwrapSandbox({
|
|
3985
|
+
hostWorkspaceRoot: ctx.workspaceRoot,
|
|
3986
|
+
runtimeContext
|
|
3987
|
+
});
|
|
3860
3988
|
await sandbox.init?.({ workspace, sessionId: ctx.sessionId });
|
|
3861
3989
|
return {
|
|
3990
|
+
runtimeContext,
|
|
3991
|
+
storageRoot: ctx.workspaceRoot,
|
|
3862
3992
|
workspace,
|
|
3863
3993
|
sandbox,
|
|
3864
3994
|
fileSearch: createServerFileSearch(workspace, sandbox)
|
|
@@ -3868,16 +3998,18 @@ var localModeAdapter = {
|
|
|
3868
3998
|
|
|
3869
3999
|
// src/server/runtime/modes/vercel-sandbox.ts
|
|
3870
4000
|
import { execFile as execFile2 } from "child_process";
|
|
3871
|
-
import { mkdir as mkdir9, readFile as readFile8, readdir as readdir5, rename as rename5, stat as
|
|
3872
|
-
import { dirname as
|
|
4001
|
+
import { mkdir as mkdir9, readFile as readFile8, readdir as readdir5, rename as rename5, stat as stat8 } from "fs/promises";
|
|
4002
|
+
import { dirname as dirname10, isAbsolute as isAbsolute6, join as join8 } from "path";
|
|
3873
4003
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
3874
4004
|
import { promisify as promisify2 } from "util";
|
|
3875
4005
|
import { Sandbox as VercelSandbox } from "@vercel/sandbox";
|
|
3876
4006
|
|
|
3877
4007
|
// src/server/sandbox/vercel-sandbox/createVercelSandboxExec.ts
|
|
4008
|
+
import { posix as posix3 } from "path";
|
|
3878
4009
|
import { Writable } from "stream";
|
|
3879
4010
|
var DEFAULT_TIMEOUT_MS3 = 3e4;
|
|
3880
4011
|
var DEFAULT_MAX_OUTPUT_BYTES3 = 1048576;
|
|
4012
|
+
var VERCEL_SANDBOX_DEFAULT_PATH = "/vercel/runtimes/node24/bin:/vercel/runtimes/node22/bin:/vercel/runtimes/python/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
3881
4013
|
function createStreamWritable(collector, shared, onChunk) {
|
|
3882
4014
|
return new Writable({
|
|
3883
4015
|
write(chunk2, _enc, cb) {
|
|
@@ -3914,39 +4046,25 @@ function timeoutResult(durationMs) {
|
|
|
3914
4046
|
stderrEncoding: "utf-8"
|
|
3915
4047
|
};
|
|
3916
4048
|
}
|
|
3917
|
-
function
|
|
3918
|
-
|
|
3919
|
-
if (
|
|
3920
|
-
|
|
4049
|
+
function normalizeVercelCwd(cwd) {
|
|
4050
|
+
const requested = cwd ?? VERCEL_SANDBOX_WORKSPACE_ROOT;
|
|
4051
|
+
if (!posix3.isAbsolute(requested)) throw new Error(`Vercel sandbox cwd must be absolute: ${requested}`);
|
|
4052
|
+
const normalized = posix3.normalize(requested);
|
|
4053
|
+
if (normalized !== VERCEL_SANDBOX_WORKSPACE_ROOT && !normalized.startsWith(`${VERCEL_SANDBOX_WORKSPACE_ROOT}/`)) {
|
|
4054
|
+
throw new Error(`Vercel sandbox cwd must stay under ${VERCEL_SANDBOX_WORKSPACE_ROOT}: ${requested}`);
|
|
3921
4055
|
}
|
|
3922
|
-
return
|
|
3923
|
-
}
|
|
3924
|
-
function escapeRegExp(value) {
|
|
3925
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3926
|
-
}
|
|
3927
|
-
var WORKSPACE_ALIAS_PREFIX_BOUNDARY = String.raw`(^|[\s"'=:;,\[({<>&|])`;
|
|
3928
|
-
var WORKSPACE_ALIAS_SUFFIX_BOUNDARY = String.raw`(?=/|$|[\s"'=:;,)\]}> &|])`;
|
|
3929
|
-
var WORKSPACE_ALIAS_PATTERN = new RegExp(
|
|
3930
|
-
`${WORKSPACE_ALIAS_PREFIX_BOUNDARY}${escapeRegExp(VERCEL_SANDBOX_WORKSPACE_ROOT)}${WORKSPACE_ALIAS_SUFFIX_BOUNDARY}`,
|
|
3931
|
-
"g"
|
|
3932
|
-
);
|
|
3933
|
-
function replaceWorkspaceAliases(value) {
|
|
3934
|
-
return value.replace(
|
|
3935
|
-
WORKSPACE_ALIAS_PATTERN,
|
|
3936
|
-
(_match, prefix) => `${prefix}${VERCEL_SANDBOX_REMOTE_ROOT}`
|
|
3937
|
-
);
|
|
3938
|
-
}
|
|
3939
|
-
function toRemoteCwd(cwd) {
|
|
3940
|
-
if (!cwd) return cwd;
|
|
3941
|
-
return toRemotePath(cwd);
|
|
3942
|
-
}
|
|
3943
|
-
function toRemoteCommand(command) {
|
|
3944
|
-
return replaceWorkspaceAliases(command);
|
|
4056
|
+
return normalized;
|
|
3945
4057
|
}
|
|
3946
4058
|
function toRemoteEnv(env) {
|
|
3947
|
-
|
|
4059
|
+
const baseEnv = {
|
|
4060
|
+
...env ?? {},
|
|
4061
|
+
PATH: env?.PATH ? `${env.PATH}:${VERCEL_SANDBOX_DEFAULT_PATH}` : VERCEL_SANDBOX_DEFAULT_PATH
|
|
4062
|
+
};
|
|
3948
4063
|
return Object.fromEntries(
|
|
3949
|
-
Object.entries(
|
|
4064
|
+
Object.entries(withWorkspacePythonEnv({
|
|
4065
|
+
workspaceRoot: VERCEL_SANDBOX_WORKSPACE_ROOT,
|
|
4066
|
+
env: baseEnv
|
|
4067
|
+
})).filter((entry) => entry[1] != null)
|
|
3950
4068
|
);
|
|
3951
4069
|
}
|
|
3952
4070
|
function createVercelSandboxExec(sandbox, execOpts = {}) {
|
|
@@ -3955,6 +4073,7 @@ function createVercelSandboxExec(sandbox, execOpts = {}) {
|
|
|
3955
4073
|
placement: "remote",
|
|
3956
4074
|
provider: "vercel-sandbox",
|
|
3957
4075
|
capabilities: ["exec"],
|
|
4076
|
+
runtimeContext: VERCEL_SANDBOX_RUNTIME_CONTEXT,
|
|
3958
4077
|
async init() {
|
|
3959
4078
|
},
|
|
3960
4079
|
async exec(cmd, opts) {
|
|
@@ -3993,8 +4112,8 @@ function createVercelSandboxExec(sandbox, execOpts = {}) {
|
|
|
3993
4112
|
}
|
|
3994
4113
|
const result = await sandbox.runCommand({
|
|
3995
4114
|
cmd: "sh",
|
|
3996
|
-
args: ["-c",
|
|
3997
|
-
cwd:
|
|
4115
|
+
args: ["-c", cmd],
|
|
4116
|
+
cwd: normalizeVercelCwd(opts?.cwd),
|
|
3998
4117
|
env: toRemoteEnv(opts?.env),
|
|
3999
4118
|
signal: controller.signal,
|
|
4000
4119
|
stdout: createStreamWritable(stdoutCollector, captureState, opts?.onStdout),
|
|
@@ -4095,11 +4214,11 @@ async function buildTarGz(files) {
|
|
|
4095
4214
|
}
|
|
4096
4215
|
chunks.push(Buffer.alloc(1024));
|
|
4097
4216
|
const tarBuffer = Buffer.concat(chunks);
|
|
4098
|
-
return new Promise((
|
|
4217
|
+
return new Promise((resolve10, reject) => {
|
|
4099
4218
|
const gzip = createGzip({ level: 6 });
|
|
4100
4219
|
const gzChunks = [];
|
|
4101
4220
|
gzip.on("data", (chunk2) => gzChunks.push(chunk2));
|
|
4102
|
-
gzip.on("end", () =>
|
|
4221
|
+
gzip.on("end", () => resolve10(Buffer.concat(gzChunks)));
|
|
4103
4222
|
gzip.on("error", reject);
|
|
4104
4223
|
Readable.from(tarBuffer).pipe(gzip);
|
|
4105
4224
|
});
|
|
@@ -4273,17 +4392,19 @@ function provisioningSourceToPath(source) {
|
|
|
4273
4392
|
}
|
|
4274
4393
|
async function prepareVercelProvisioningArtifact(request) {
|
|
4275
4394
|
const sourcePath = provisioningSourceToPath(request.source);
|
|
4276
|
-
await mkdir9(
|
|
4395
|
+
await mkdir9(dirname10(request.outputPath), { recursive: true });
|
|
4277
4396
|
if (request.kind === "node") {
|
|
4278
|
-
const { stdout } = await execFileAsync2("
|
|
4279
|
-
"
|
|
4397
|
+
const { stdout } = await execFileAsync2("pnpm", [
|
|
4398
|
+
"--dir",
|
|
4280
4399
|
sourcePath,
|
|
4400
|
+
"pack",
|
|
4281
4401
|
"--pack-destination",
|
|
4282
|
-
|
|
4402
|
+
dirname10(request.outputPath)
|
|
4283
4403
|
], { maxBuffer: 1024 * 1024 * 20 });
|
|
4284
4404
|
const packedName = stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
|
|
4285
|
-
if (!packedName) throw new Error(`
|
|
4286
|
-
|
|
4405
|
+
if (!packedName) throw new Error(`pnpm pack produced no artifact for ${sourcePath}`);
|
|
4406
|
+
const packedPath = isAbsolute6(packedName) ? packedName : join8(dirname10(request.outputPath), packedName);
|
|
4407
|
+
await rename5(packedPath, request.outputPath);
|
|
4287
4408
|
return;
|
|
4288
4409
|
}
|
|
4289
4410
|
await execFileAsync2("tar", ["-czf", request.outputPath, "-C", sourcePath, "."], {
|
|
@@ -4295,7 +4416,7 @@ function shellSingleQuote(value) {
|
|
|
4295
4416
|
}
|
|
4296
4417
|
async function copyHostPathToVercelWorkspace(options) {
|
|
4297
4418
|
const sourcePath = provisioningSourceToPath(options.source);
|
|
4298
|
-
const sourceStat = await
|
|
4419
|
+
const sourceStat = await stat8(sourcePath);
|
|
4299
4420
|
if (sourceStat.isDirectory()) {
|
|
4300
4421
|
await options.workspace.mkdir(options.targetRel, { recursive: true });
|
|
4301
4422
|
for (const entry of await readdir5(sourcePath, { withFileTypes: true })) {
|
|
@@ -4656,7 +4777,8 @@ function createVercelSandboxModeAdapter(opts = {}) {
|
|
|
4656
4777
|
return {
|
|
4657
4778
|
workspace,
|
|
4658
4779
|
sandbox,
|
|
4659
|
-
fileSearch: createServerFileSearch(workspace, sandbox)
|
|
4780
|
+
fileSearch: createServerFileSearch(workspace, sandbox),
|
|
4781
|
+
runtimeContext: workspace.runtimeContext
|
|
4660
4782
|
};
|
|
4661
4783
|
} catch (error) {
|
|
4662
4784
|
const code = error?.code;
|
|
@@ -4724,6 +4846,18 @@ import {
|
|
|
4724
4846
|
} from "@mariozechner/pi-coding-agent";
|
|
4725
4847
|
|
|
4726
4848
|
// src/server/harness/pi-coding-agent/tool-adapter.ts
|
|
4849
|
+
var BORING_TOOL_ERROR_MARKER = "__boringToolError";
|
|
4850
|
+
function markToolResultErrorDetails(details) {
|
|
4851
|
+
return details && typeof details === "object" && !Array.isArray(details) ? { ...details, [BORING_TOOL_ERROR_MARKER]: true } : { [BORING_TOOL_ERROR_MARKER]: true, details };
|
|
4852
|
+
}
|
|
4853
|
+
function unmarkToolResultErrorDetails(details) {
|
|
4854
|
+
if (!details || typeof details !== "object" || Array.isArray(details)) return { isMarked: false, details };
|
|
4855
|
+
const record = { ...details };
|
|
4856
|
+
if (record[BORING_TOOL_ERROR_MARKER] !== true) return { isMarked: false, details };
|
|
4857
|
+
delete record[BORING_TOOL_ERROR_MARKER];
|
|
4858
|
+
if (Object.keys(record).length === 1 && "details" in record) return { isMarked: true, details: record.details };
|
|
4859
|
+
return { isMarked: true, details: record };
|
|
4860
|
+
}
|
|
4727
4861
|
function toolTelemetryProperties(toolName2, sessionId, status, startedAt, result) {
|
|
4728
4862
|
const properties = {
|
|
4729
4863
|
toolName: toolName2,
|
|
@@ -4766,7 +4900,10 @@ function adaptToolForPi(tool, sessionId, telemetry = noopTelemetry) {
|
|
|
4766
4900
|
});
|
|
4767
4901
|
if (result.isError) {
|
|
4768
4902
|
emittedFailure = true;
|
|
4769
|
-
|
|
4903
|
+
return {
|
|
4904
|
+
content: result.content,
|
|
4905
|
+
details: markToolResultErrorDetails(result.details)
|
|
4906
|
+
};
|
|
4770
4907
|
}
|
|
4771
4908
|
return {
|
|
4772
4909
|
content: result.content,
|
|
@@ -4976,7 +5113,7 @@ import {
|
|
|
4976
5113
|
readdir as readdir6,
|
|
4977
5114
|
readFile as readFile9,
|
|
4978
5115
|
stat as fsStat,
|
|
4979
|
-
rm as
|
|
5116
|
+
rm as rm4,
|
|
4980
5117
|
mkdir as mkdir10,
|
|
4981
5118
|
writeFile as writeFile6,
|
|
4982
5119
|
appendFile,
|
|
@@ -5012,7 +5149,10 @@ var PiSessionStore = class {
|
|
|
5012
5149
|
this.sessionDir = options;
|
|
5013
5150
|
return;
|
|
5014
5151
|
}
|
|
5015
|
-
this.sessionDir = options?.sessionDir ?? (options?.sessionNamespace ? sessionDirForNamespace(options.sessionNamespace) : defaultSessionDir(cwd));
|
|
5152
|
+
this.sessionDir = options?.sessionDir ?? (options?.sessionNamespace ? sessionDirForNamespace(options.sessionNamespace) : defaultSessionDir(options?.storageCwd ?? cwd));
|
|
5153
|
+
}
|
|
5154
|
+
getSessionDir() {
|
|
5155
|
+
return this.sessionDir;
|
|
5016
5156
|
}
|
|
5017
5157
|
async list(ctx) {
|
|
5018
5158
|
const files = await readdir6(this.sessionDir).catch(() => []);
|
|
@@ -5155,7 +5295,7 @@ var PiSessionStore = class {
|
|
|
5155
5295
|
const filepath = await this.resolveSessionFile(sessionId).catch(
|
|
5156
5296
|
() => null
|
|
5157
5297
|
);
|
|
5158
|
-
if (filepath) await
|
|
5298
|
+
if (filepath) await rm4(filepath, { force: true });
|
|
5159
5299
|
}
|
|
5160
5300
|
touchSession(sessionId, title) {
|
|
5161
5301
|
this.resolveSessionFile(sessionId).then((filepath) => {
|
|
@@ -5345,7 +5485,7 @@ var DEFAULT_POLL_MS = 250;
|
|
|
5345
5485
|
var MAX_PROMPT_CHARS = 1200;
|
|
5346
5486
|
var MAX_TITLE_CHARS = 80;
|
|
5347
5487
|
function sleep(ms) {
|
|
5348
|
-
return new Promise((
|
|
5488
|
+
return new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
5349
5489
|
}
|
|
5350
5490
|
function truncateForPrompt(text) {
|
|
5351
5491
|
const trimmed = text.trim();
|
|
@@ -5677,7 +5817,7 @@ function mergePiPackageSources(base = [], additional = []) {
|
|
|
5677
5817
|
var WORKSPACE_PATHS_GUIDELINE = [
|
|
5678
5818
|
"## Workspace paths",
|
|
5679
5819
|
"",
|
|
5680
|
-
'- The "Current working directory"
|
|
5820
|
+
'- The "Current working directory" line in this prompt is the workspace root. Tool path arguments must be relative to it (e.g. `README.md`, `src/foo.ts`).',
|
|
5681
5821
|
"- Never pass an absolute path or a path that walks outside the workspace (no leading `/`, no `..` that escapes the root). The sandbox will reject it and the call is wasted.",
|
|
5682
5822
|
"- For `find`/`grep`/`ls`: omit the `path` argument to search from the workspace root. Pass `path` only when you need to restrict to a subdirectory, and only as a workspace-relative path.",
|
|
5683
5823
|
"- For `read`/`edit`/`write`: pass workspace-relative paths only."
|
|
@@ -5696,6 +5836,18 @@ ${extra}` };
|
|
|
5696
5836
|
});
|
|
5697
5837
|
};
|
|
5698
5838
|
}
|
|
5839
|
+
function buildToolErrorResultExtension() {
|
|
5840
|
+
return (pi) => {
|
|
5841
|
+
pi.on("tool_result", async (event) => {
|
|
5842
|
+
const marked = unmarkToolResultErrorDetails(event.details);
|
|
5843
|
+
if (!marked.isMarked) return;
|
|
5844
|
+
return {
|
|
5845
|
+
details: marked.details,
|
|
5846
|
+
isError: true
|
|
5847
|
+
};
|
|
5848
|
+
});
|
|
5849
|
+
};
|
|
5850
|
+
}
|
|
5699
5851
|
function extractUserMessageText(message) {
|
|
5700
5852
|
const record = message;
|
|
5701
5853
|
if (record?.role !== "user") return "";
|
|
@@ -5816,9 +5968,10 @@ function basenameForAttachment(filename) {
|
|
|
5816
5968
|
return safe || "image";
|
|
5817
5969
|
}
|
|
5818
5970
|
function createPiCodingAgentHarness(opts) {
|
|
5819
|
-
const sessionStore = new PiSessionStore(opts.cwd, {
|
|
5971
|
+
const sessionStore = new PiSessionStore(opts.runtimeCwd ?? opts.cwd, {
|
|
5820
5972
|
sessionNamespace: opts.sessionNamespace,
|
|
5821
|
-
sessionDir: opts.sessionDir
|
|
5973
|
+
sessionDir: opts.sessionDir,
|
|
5974
|
+
storageCwd: opts.cwd
|
|
5822
5975
|
});
|
|
5823
5976
|
const piSessions = /* @__PURE__ */ new Map();
|
|
5824
5977
|
const effectiveSkillPaths = [];
|
|
@@ -5864,15 +6017,17 @@ function createPiCodingAgentHarness(opts) {
|
|
|
5864
6017
|
const savedPiFile = sessionStore.loadPiSessionFileSync(sessionId);
|
|
5865
6018
|
let sessionManager;
|
|
5866
6019
|
let isNewPiSession = false;
|
|
6020
|
+
const runtimeCwd = opts.runtimeCwd ?? ctx.workdir;
|
|
6021
|
+
const nativeSessionDir = sessionStore.getSessionDir();
|
|
5867
6022
|
if (savedPiFile) {
|
|
5868
6023
|
try {
|
|
5869
|
-
sessionManager = SessionManager.open(savedPiFile, void 0,
|
|
6024
|
+
sessionManager = SessionManager.open(savedPiFile, void 0, runtimeCwd);
|
|
5870
6025
|
} catch {
|
|
5871
|
-
sessionManager = SessionManager.create(
|
|
6026
|
+
sessionManager = SessionManager.create(runtimeCwd, nativeSessionDir);
|
|
5872
6027
|
isNewPiSession = true;
|
|
5873
6028
|
}
|
|
5874
6029
|
} else {
|
|
5875
|
-
sessionManager = SessionManager.create(
|
|
6030
|
+
sessionManager = SessionManager.create(runtimeCwd, nativeSessionDir);
|
|
5876
6031
|
isNewPiSession = true;
|
|
5877
6032
|
}
|
|
5878
6033
|
const resolvedModel = resolveRequestedModel(modelRegistry, input);
|
|
@@ -5881,17 +6036,19 @@ function createPiCodingAgentHarness(opts) {
|
|
|
5881
6036
|
const composedSystemPromptAppend = composeSystemPromptAppend(opts.systemPromptAppend);
|
|
5882
6037
|
const dynamicPromptExtension = opts.systemPromptDynamic ? buildDynamicPromptExtension(opts.systemPromptDynamic) : void 0;
|
|
5883
6038
|
const agentDir = getAgentDir2();
|
|
6039
|
+
const toolErrorResultExtension = buildToolErrorResultExtension();
|
|
5884
6040
|
const extensionFactories = [
|
|
6041
|
+
toolErrorResultExtension,
|
|
5885
6042
|
...dynamicPromptExtension ? [dynamicPromptExtension] : [],
|
|
5886
6043
|
...opts.pi?.extensionFactories ?? []
|
|
5887
6044
|
];
|
|
5888
6045
|
const settingsManager = createResourceSettingsManager(
|
|
5889
|
-
|
|
6046
|
+
opts.cwd,
|
|
5890
6047
|
agentDir,
|
|
5891
6048
|
effectivePackages
|
|
5892
6049
|
);
|
|
5893
6050
|
const resourceLoader = new DefaultResourceLoader({
|
|
5894
|
-
cwd:
|
|
6051
|
+
cwd: opts.cwd,
|
|
5895
6052
|
agentDir,
|
|
5896
6053
|
settingsManager,
|
|
5897
6054
|
appendSystemPromptOverride: (base) => [...base, composedSystemPromptAppend],
|
|
@@ -5909,7 +6066,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
5909
6066
|
// and merge with the additional paths.
|
|
5910
6067
|
...opts.pi?.noSkills ? {
|
|
5911
6068
|
skillsOverride: () => loadSkills({
|
|
5912
|
-
cwd:
|
|
6069
|
+
cwd: opts.cwd,
|
|
5913
6070
|
agentDir,
|
|
5914
6071
|
skillPaths: effectiveSkillPaths,
|
|
5915
6072
|
includeDefaults: false
|
|
@@ -5918,8 +6075,11 @@ function createPiCodingAgentHarness(opts) {
|
|
|
5918
6075
|
});
|
|
5919
6076
|
await resourceLoader?.reload();
|
|
5920
6077
|
const { session: piSession } = await createAgentSession({
|
|
5921
|
-
cwd:
|
|
5922
|
-
tools
|
|
6078
|
+
cwd: runtimeCwd,
|
|
6079
|
+
// Suppress Pi's built-in filesystem/shell tools while keeping Boring's
|
|
6080
|
+
// adapted tool catalog active. Passing `tools: []` is an allowlist of
|
|
6081
|
+
// zero tools in Pi v0.75+, which disables customTools too.
|
|
6082
|
+
noTools: "builtin",
|
|
5923
6083
|
customTools: adaptToolsForPi(opts.tools, input.sessionId, opts.telemetry),
|
|
5924
6084
|
model,
|
|
5925
6085
|
thinkingLevel: input.thinkingLevel ?? "off",
|
|
@@ -6115,6 +6275,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6115
6275
|
let sawTextChunk = false;
|
|
6116
6276
|
let inlineTurnIndex = 0;
|
|
6117
6277
|
let currentPiAssistantMessageId = null;
|
|
6278
|
+
let pendingTerminalErrorChunks = [];
|
|
6118
6279
|
const messageIdsWithStreamedReasoning = /* @__PURE__ */ new Set();
|
|
6119
6280
|
let piSeq = 0;
|
|
6120
6281
|
const nextPiSeq = () => ++piSeq;
|
|
@@ -6302,7 +6463,16 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6302
6463
|
converted = piHistoryChunks;
|
|
6303
6464
|
} else {
|
|
6304
6465
|
const sdkChunks = namespaceInlinePartIds(dedupStartChunks(piEventToChunks(event)));
|
|
6305
|
-
const
|
|
6466
|
+
const shouldBufferTerminalError = event.type === "message_update" && event.assistantMessageEvent?.type === "error";
|
|
6467
|
+
const visibleSdkChunks = shouldBufferTerminalError ? sdkChunks.filter((chunk2) => {
|
|
6468
|
+
const type = chunk2.type;
|
|
6469
|
+
if (type === "error" || type === "finish") {
|
|
6470
|
+
pendingTerminalErrorChunks.push(chunk2);
|
|
6471
|
+
return false;
|
|
6472
|
+
}
|
|
6473
|
+
return true;
|
|
6474
|
+
}) : sdkChunks;
|
|
6475
|
+
const sdkChunksForTurn = filterSdkChunksForCurrentSegment(visibleSdkChunks);
|
|
6306
6476
|
converted = [...piHistoryChunks, ...sdkChunksForTurn];
|
|
6307
6477
|
}
|
|
6308
6478
|
for (const chunk2 of converted) {
|
|
@@ -6317,7 +6487,17 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6317
6487
|
}
|
|
6318
6488
|
chunks.push(...converted);
|
|
6319
6489
|
if (event.type === "agent_end") {
|
|
6320
|
-
|
|
6490
|
+
const willRetry = Boolean(event.willRetry);
|
|
6491
|
+
if (willRetry) {
|
|
6492
|
+
pendingTerminalErrorChunks = [];
|
|
6493
|
+
sawTextChunk = false;
|
|
6494
|
+
currentPiAssistantMessageId = null;
|
|
6495
|
+
messageIdsWithStreamedReasoning.clear();
|
|
6496
|
+
} else if (pendingTerminalErrorChunks.length > 0) {
|
|
6497
|
+
chunks.push(...pendingTerminalErrorChunks);
|
|
6498
|
+
pendingTerminalErrorChunks = [];
|
|
6499
|
+
sawTextChunk = true;
|
|
6500
|
+
} else if (!sawTextChunk) {
|
|
6321
6501
|
const { role, text, errorText } = extractAssistantMessageText(
|
|
6322
6502
|
findLastAssistantMessage(
|
|
6323
6503
|
event.messages
|
|
@@ -6336,7 +6516,8 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6336
6516
|
assistantText += text;
|
|
6337
6517
|
}
|
|
6338
6518
|
}
|
|
6339
|
-
if (
|
|
6519
|
+
if (willRetry) {
|
|
6520
|
+
} else if (nativeFollowUpPending.has(input.sessionId) && !ctx.abortSignal.aborted) {
|
|
6340
6521
|
} else {
|
|
6341
6522
|
done = true;
|
|
6342
6523
|
stopHeartbeat();
|
|
@@ -6368,12 +6549,12 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6368
6549
|
const base = basenameForAttachment(a.filename ?? "image");
|
|
6369
6550
|
const unique = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
6370
6551
|
const relPath = `${DEFAULT_ATTACHMENT_DIR}/${base}-${unique}.${ext}`;
|
|
6371
|
-
await mkdir11(join10(
|
|
6372
|
-
await writeFile7(join10(
|
|
6552
|
+
await mkdir11(join10(opts.cwd, DEFAULT_ATTACHMENT_DIR), { recursive: true });
|
|
6553
|
+
await writeFile7(join10(opts.cwd, relPath), bytes);
|
|
6373
6554
|
savedPaths.push(relPath);
|
|
6374
6555
|
} catch (err) {
|
|
6375
6556
|
const msg = err instanceof Error ? err.message : String(err);
|
|
6376
|
-
log3.error("attachment write failed", { workdir:
|
|
6557
|
+
log3.error("attachment write failed", { workdir: opts.cwd, error: msg });
|
|
6377
6558
|
writeErrors.push(`${a.filename ?? "image"}: ${msg}`);
|
|
6378
6559
|
}
|
|
6379
6560
|
}
|
|
@@ -6458,17 +6639,17 @@ ${attachmentNotes.join("\n")}` : message,
|
|
|
6458
6639
|
}
|
|
6459
6640
|
|
|
6460
6641
|
// src/server/harness/pi-coding-agent/pluginLoader.ts
|
|
6461
|
-
import { readdir as readdir7, stat as
|
|
6462
|
-
import { join as join11, extname as extname3, resolve as
|
|
6642
|
+
import { readdir as readdir7, stat as stat9, readFile as readFile10 } from "fs/promises";
|
|
6643
|
+
import { join as join11, extname as extname3, resolve as resolve7, sep as sep6, relative as relative9 } from "path";
|
|
6463
6644
|
import { homedir as homedir4 } from "os";
|
|
6464
6645
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
6465
6646
|
var VALID_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs"]);
|
|
6466
6647
|
var GLOBAL_DIR = join11(homedir4(), ".pi", "agent", "extensions");
|
|
6467
6648
|
var LOCAL_DIR = ".pi/extensions";
|
|
6468
6649
|
var EXTENSIONS_JSON = ".pi/extensions.json";
|
|
6469
|
-
async function
|
|
6650
|
+
async function dirExists2(path4) {
|
|
6470
6651
|
try {
|
|
6471
|
-
const s = await
|
|
6652
|
+
const s = await stat9(path4);
|
|
6472
6653
|
return s.isDirectory();
|
|
6473
6654
|
} catch {
|
|
6474
6655
|
return false;
|
|
@@ -6476,14 +6657,14 @@ async function dirExists(path4) {
|
|
|
6476
6657
|
}
|
|
6477
6658
|
async function fileExists(path4) {
|
|
6478
6659
|
try {
|
|
6479
|
-
const s = await
|
|
6660
|
+
const s = await stat9(path4);
|
|
6480
6661
|
return s.isFile();
|
|
6481
6662
|
} catch {
|
|
6482
6663
|
return false;
|
|
6483
6664
|
}
|
|
6484
6665
|
}
|
|
6485
6666
|
async function discoverFromDir(dir, source) {
|
|
6486
|
-
if (!await
|
|
6667
|
+
if (!await dirExists2(dir)) return [];
|
|
6487
6668
|
const entries = await readdir7(dir);
|
|
6488
6669
|
return entries.filter((e) => VALID_EXTENSIONS.has(extname3(e))).map((e) => ({ path: join11(dir, e), source }));
|
|
6489
6670
|
}
|
|
@@ -6516,7 +6697,7 @@ async function loadModule(filePath, importFn) {
|
|
|
6516
6697
|
}
|
|
6517
6698
|
async function discoverNpmPlugins(cwd) {
|
|
6518
6699
|
const nodeModulesDir = join11(cwd, "node_modules");
|
|
6519
|
-
if (!await
|
|
6700
|
+
if (!await dirExists2(nodeModulesDir)) return [];
|
|
6520
6701
|
try {
|
|
6521
6702
|
const entries = await readdir7(nodeModulesDir);
|
|
6522
6703
|
return entries.filter((e) => e.startsWith("pi-plugin-")).map((e) => join11(nodeModulesDir, e));
|
|
@@ -6529,8 +6710,8 @@ async function loadNpmPlugin(pkgDir, importFn) {
|
|
|
6529
6710
|
if (!await fileExists(pkgJsonPath)) return [];
|
|
6530
6711
|
const pkgJson = JSON.parse(await readFile10(pkgJsonPath, "utf-8"));
|
|
6531
6712
|
const main = pkgJson.main ?? "index.js";
|
|
6532
|
-
const resolvedMain =
|
|
6533
|
-
const resolvedPkgDir =
|
|
6713
|
+
const resolvedMain = resolve7(pkgDir, main);
|
|
6714
|
+
const resolvedPkgDir = resolve7(pkgDir);
|
|
6534
6715
|
if (resolvedMain !== resolvedPkgDir && !resolvedMain.startsWith(resolvedPkgDir + sep6)) {
|
|
6535
6716
|
return [];
|
|
6536
6717
|
}
|
|
@@ -6570,7 +6751,7 @@ async function loadPlugins(options) {
|
|
|
6570
6751
|
if (config?.npm) {
|
|
6571
6752
|
for (const pkg of config.npm) {
|
|
6572
6753
|
const pkgDir = join11(options.cwd, "node_modules", pkg);
|
|
6573
|
-
if (await
|
|
6754
|
+
if (await dirExists2(pkgDir)) {
|
|
6574
6755
|
const already = candidates.some((c) => c.path === pkgDir);
|
|
6575
6756
|
if (!already) {
|
|
6576
6757
|
candidates.push({ path: pkgDir, source: "npm" });
|
|
@@ -6616,10 +6797,15 @@ import {
|
|
|
6616
6797
|
createWriteToolDefinition
|
|
6617
6798
|
} from "@mariozechner/pi-coding-agent";
|
|
6618
6799
|
|
|
6800
|
+
// src/server/runtime/mode.ts
|
|
6801
|
+
function getRuntimeBundleStorageRoot(bundle) {
|
|
6802
|
+
return bundle.storageRoot ?? getNodeWorkspaceHostRoot(bundle.workspace) ?? bundle.workspace.root;
|
|
6803
|
+
}
|
|
6804
|
+
|
|
6619
6805
|
// src/server/tools/operations/bound.ts
|
|
6620
6806
|
import { constants as constants4 } from "fs";
|
|
6621
|
-
import { access as access4, lstat as lstat4, mkdir as mkdir12, readFile as readFile11, readdir as readdir8, readlink, realpath as realpath4, stat as
|
|
6622
|
-
import { dirname as
|
|
6807
|
+
import { access as access4, lstat as lstat4, mkdir as mkdir12, readFile as readFile11, readdir as readdir8, readlink, realpath as realpath4, stat as stat10, writeFile as writeFile8 } from "fs/promises";
|
|
6808
|
+
import { dirname as dirname11, isAbsolute as isAbsolute7, join as join12, relative as relative10, resolve as resolve8 } from "path";
|
|
6623
6809
|
function toPosixPath(value) {
|
|
6624
6810
|
return value.split("\\").join("/");
|
|
6625
6811
|
}
|
|
@@ -6675,7 +6861,7 @@ async function walkMatches(root, current, pattern, ignore, limit, out) {
|
|
|
6675
6861
|
const entries = await readdir8(current, { withFileTypes: true });
|
|
6676
6862
|
for (const entry of entries) {
|
|
6677
6863
|
if (out.length >= limit) return;
|
|
6678
|
-
const absolutePath =
|
|
6864
|
+
const absolutePath = resolve8(current, entry.name);
|
|
6679
6865
|
const relativePath = toPosixPath(relative10(root, absolutePath));
|
|
6680
6866
|
if (entry.isDirectory() && shouldSkipDir(relativePath, ignore)) continue;
|
|
6681
6867
|
if (matchesGlob(relativePath, pattern)) {
|
|
@@ -6690,26 +6876,26 @@ async function findNearestExistingAncestor(absPath) {
|
|
|
6690
6876
|
let current = absPath;
|
|
6691
6877
|
for (; ; ) {
|
|
6692
6878
|
try {
|
|
6693
|
-
await
|
|
6879
|
+
await stat10(current);
|
|
6694
6880
|
return current;
|
|
6695
6881
|
} catch {
|
|
6696
|
-
const parent =
|
|
6882
|
+
const parent = dirname11(current);
|
|
6697
6883
|
if (parent === current) return current;
|
|
6698
6884
|
current = parent;
|
|
6699
6885
|
}
|
|
6700
6886
|
}
|
|
6701
6887
|
}
|
|
6702
6888
|
async function assertWithinWorkspace(workspaceRoot, absPath) {
|
|
6703
|
-
const realRoot = await realpath4(
|
|
6889
|
+
const realRoot = await realpath4(resolve8(workspaceRoot));
|
|
6704
6890
|
try {
|
|
6705
6891
|
const s = await lstat4(absPath);
|
|
6706
6892
|
if (s.isSymbolicLink()) {
|
|
6707
6893
|
const target = await readlink(absPath);
|
|
6708
|
-
const resolvedTarget =
|
|
6894
|
+
const resolvedTarget = resolve8(dirname11(absPath), target);
|
|
6709
6895
|
const nearestAncestor = await findNearestExistingAncestor(resolvedTarget);
|
|
6710
6896
|
const realAncestor = await realpath4(nearestAncestor);
|
|
6711
6897
|
const rel2 = relative10(realRoot, realAncestor);
|
|
6712
|
-
if (rel2.startsWith("..") ||
|
|
6898
|
+
if (rel2.startsWith("..") || isAbsolute7(rel2)) {
|
|
6713
6899
|
throw new Error(`path "${absPath}" is outside workspace`);
|
|
6714
6900
|
}
|
|
6715
6901
|
return;
|
|
@@ -6727,10 +6913,10 @@ async function assertWithinWorkspace(workspaceRoot, absPath) {
|
|
|
6727
6913
|
} catch (err) {
|
|
6728
6914
|
const code = err.code;
|
|
6729
6915
|
if (code === "ENOENT") {
|
|
6730
|
-
const nearestAncestor = await findNearestExistingAncestor(
|
|
6916
|
+
const nearestAncestor = await findNearestExistingAncestor(dirname11(absPath));
|
|
6731
6917
|
const realAncestor = await realpath4(nearestAncestor);
|
|
6732
6918
|
const rel2 = relative10(realRoot, realAncestor);
|
|
6733
|
-
if (rel2.startsWith("..") ||
|
|
6919
|
+
if (rel2.startsWith("..") || isAbsolute7(rel2)) {
|
|
6734
6920
|
throw new Error(`path "${absPath}" is outside workspace`);
|
|
6735
6921
|
}
|
|
6736
6922
|
return;
|
|
@@ -6738,51 +6924,77 @@ async function assertWithinWorkspace(workspaceRoot, absPath) {
|
|
|
6738
6924
|
throw err;
|
|
6739
6925
|
}
|
|
6740
6926
|
const rel = relative10(realRoot, realCandidate);
|
|
6741
|
-
if (rel.startsWith("..") ||
|
|
6927
|
+
if (rel.startsWith("..") || isAbsolute7(rel)) {
|
|
6742
6928
|
throw new Error(`path "${absPath}" is outside workspace`);
|
|
6743
6929
|
}
|
|
6744
6930
|
}
|
|
6745
|
-
function boundFs(workspaceRoot) {
|
|
6931
|
+
function boundFs(workspaceRoot, opts = {}) {
|
|
6932
|
+
const runtimeRoot = opts.runtimeRoot ? opts.runtimeRoot.replace(/\/+$/, "") || "/" : void 0;
|
|
6933
|
+
const shouldMapRuntimeRoot = Boolean(runtimeRoot && runtimeRoot !== workspaceRoot);
|
|
6934
|
+
const toStoragePath = (absolutePath) => {
|
|
6935
|
+
if (!shouldMapRuntimeRoot || !runtimeRoot) return absolutePath;
|
|
6936
|
+
const normalized = toPosixPath(absolutePath);
|
|
6937
|
+
if (normalized === runtimeRoot) return workspaceRoot;
|
|
6938
|
+
if (normalized.startsWith(`${runtimeRoot}/`)) {
|
|
6939
|
+
return join12(workspaceRoot, ...normalized.slice(runtimeRoot.length + 1).split("/"));
|
|
6940
|
+
}
|
|
6941
|
+
return absolutePath;
|
|
6942
|
+
};
|
|
6943
|
+
const toRuntimePath2 = (absolutePath) => {
|
|
6944
|
+
if (!shouldMapRuntimeRoot || !runtimeRoot) return absolutePath;
|
|
6945
|
+
const rel = relative10(workspaceRoot, absolutePath);
|
|
6946
|
+
if (rel === "") return runtimeRoot;
|
|
6947
|
+
if (rel.startsWith("..") || isAbsolute7(rel)) return absolutePath;
|
|
6948
|
+
return `${runtimeRoot}/${toPosixPath(rel)}`;
|
|
6949
|
+
};
|
|
6746
6950
|
const read = {
|
|
6747
6951
|
async readFile(absolutePath) {
|
|
6748
|
-
|
|
6749
|
-
|
|
6952
|
+
const storagePath = toStoragePath(absolutePath);
|
|
6953
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
6954
|
+
return await readFile11(storagePath);
|
|
6750
6955
|
},
|
|
6751
6956
|
async access(absolutePath) {
|
|
6752
|
-
|
|
6753
|
-
await
|
|
6957
|
+
const storagePath = toStoragePath(absolutePath);
|
|
6958
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
6959
|
+
await access4(storagePath, constants4.R_OK);
|
|
6754
6960
|
}
|
|
6755
6961
|
};
|
|
6756
6962
|
const write = {
|
|
6757
6963
|
async writeFile(absolutePath, content) {
|
|
6758
|
-
|
|
6759
|
-
await
|
|
6760
|
-
await
|
|
6964
|
+
const storagePath = toStoragePath(absolutePath);
|
|
6965
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
6966
|
+
await mkdir12(dirname11(storagePath), { recursive: true });
|
|
6967
|
+
await writeFile8(storagePath, content);
|
|
6761
6968
|
},
|
|
6762
6969
|
async mkdir(dir) {
|
|
6763
|
-
|
|
6764
|
-
await
|
|
6970
|
+
const storagePath = toStoragePath(dir);
|
|
6971
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
6972
|
+
await mkdir12(storagePath, { recursive: true });
|
|
6765
6973
|
}
|
|
6766
6974
|
};
|
|
6767
6975
|
const edit = {
|
|
6768
6976
|
async readFile(absolutePath) {
|
|
6769
|
-
|
|
6770
|
-
|
|
6977
|
+
const storagePath = toStoragePath(absolutePath);
|
|
6978
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
6979
|
+
return await readFile11(storagePath);
|
|
6771
6980
|
},
|
|
6772
6981
|
async writeFile(absolutePath, content) {
|
|
6773
|
-
|
|
6774
|
-
await
|
|
6982
|
+
const storagePath = toStoragePath(absolutePath);
|
|
6983
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
6984
|
+
await writeFile8(storagePath, content);
|
|
6775
6985
|
},
|
|
6776
6986
|
async access(absolutePath) {
|
|
6777
|
-
|
|
6778
|
-
await
|
|
6987
|
+
const storagePath = toStoragePath(absolutePath);
|
|
6988
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
6989
|
+
await access4(storagePath, constants4.R_OK | constants4.W_OK);
|
|
6779
6990
|
}
|
|
6780
6991
|
};
|
|
6781
6992
|
const find = {
|
|
6782
6993
|
async exists(absolutePath) {
|
|
6783
|
-
|
|
6994
|
+
const storagePath = toStoragePath(absolutePath);
|
|
6995
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
6784
6996
|
try {
|
|
6785
|
-
await
|
|
6997
|
+
await stat10(storagePath);
|
|
6786
6998
|
return true;
|
|
6787
6999
|
} catch (err) {
|
|
6788
7000
|
if (err.code === "ENOENT") return false;
|
|
@@ -6790,54 +7002,61 @@ function boundFs(workspaceRoot) {
|
|
|
6790
7002
|
}
|
|
6791
7003
|
},
|
|
6792
7004
|
async glob(pattern, cwd, options) {
|
|
6793
|
-
|
|
7005
|
+
const storageCwd = toStoragePath(cwd);
|
|
7006
|
+
await assertWithinWorkspace(workspaceRoot, storageCwd);
|
|
6794
7007
|
const matches = [];
|
|
6795
|
-
await walkMatches(
|
|
6796
|
-
return matches;
|
|
7008
|
+
await walkMatches(storageCwd, storageCwd, pattern, options.ignore, options.limit, matches);
|
|
7009
|
+
return matches.map(toRuntimePath2);
|
|
6797
7010
|
}
|
|
6798
7011
|
};
|
|
6799
7012
|
const grep = {
|
|
6800
7013
|
async isDirectory(absolutePath) {
|
|
6801
|
-
|
|
6802
|
-
|
|
7014
|
+
const storagePath = toStoragePath(absolutePath);
|
|
7015
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7016
|
+
return (await stat10(storagePath)).isDirectory();
|
|
6803
7017
|
},
|
|
6804
7018
|
async readFile(absolutePath) {
|
|
6805
|
-
|
|
6806
|
-
|
|
7019
|
+
const storagePath = toStoragePath(absolutePath);
|
|
7020
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7021
|
+
return await readFile11(storagePath, "utf8");
|
|
6807
7022
|
}
|
|
6808
7023
|
};
|
|
6809
7024
|
const ls = {
|
|
6810
7025
|
async exists(absolutePath) {
|
|
7026
|
+
const storagePath = toStoragePath(absolutePath);
|
|
6811
7027
|
try {
|
|
6812
|
-
await assertWithinWorkspace(workspaceRoot,
|
|
6813
|
-
await
|
|
7028
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7029
|
+
await stat10(storagePath);
|
|
6814
7030
|
return true;
|
|
6815
7031
|
} catch {
|
|
6816
7032
|
return false;
|
|
6817
7033
|
}
|
|
6818
7034
|
},
|
|
6819
7035
|
async stat(absolutePath) {
|
|
6820
|
-
|
|
6821
|
-
|
|
7036
|
+
const storagePath = toStoragePath(absolutePath);
|
|
7037
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7038
|
+
return await stat10(storagePath);
|
|
6822
7039
|
},
|
|
6823
7040
|
async readdir(absolutePath) {
|
|
6824
|
-
|
|
6825
|
-
|
|
7041
|
+
const storagePath = toStoragePath(absolutePath);
|
|
7042
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7043
|
+
return await readdir8(storagePath);
|
|
6826
7044
|
}
|
|
6827
7045
|
};
|
|
6828
7046
|
return { read, write, edit, find, grep, ls };
|
|
6829
7047
|
}
|
|
6830
7048
|
|
|
6831
7049
|
// src/server/tools/operations/vercel.ts
|
|
6832
|
-
import { isAbsolute as
|
|
7050
|
+
import { isAbsolute as isAbsolute8, relative as relative11 } from "path";
|
|
7051
|
+
var VERCEL_SANDBOX_LEGACY_ROOT = "/vercel/sandbox";
|
|
6833
7052
|
function rootAliases(workspace) {
|
|
6834
7053
|
const aliases = [workspace.root];
|
|
6835
|
-
if (workspace.root === VERCEL_SANDBOX_WORKSPACE_ROOT) aliases.push(
|
|
6836
|
-
if (workspace.root ===
|
|
6837
|
-
return aliases;
|
|
7054
|
+
if (workspace.root === VERCEL_SANDBOX_WORKSPACE_ROOT) aliases.push(VERCEL_SANDBOX_LEGACY_ROOT);
|
|
7055
|
+
if (workspace.root === VERCEL_SANDBOX_LEGACY_ROOT) aliases.push(VERCEL_SANDBOX_WORKSPACE_ROOT);
|
|
7056
|
+
return Array.from(new Set(aliases));
|
|
6838
7057
|
}
|
|
6839
7058
|
function isOutsideWorkspaceRel(rel) {
|
|
6840
|
-
return rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") ||
|
|
7059
|
+
return rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isAbsolute8(rel);
|
|
6841
7060
|
}
|
|
6842
7061
|
function toRelPath(workspace, absolutePath) {
|
|
6843
7062
|
for (const root of rootAliases(workspace)) {
|
|
@@ -6854,7 +7073,7 @@ function toRelPath(workspace, absolutePath) {
|
|
|
6854
7073
|
return `.agents/skills/${skillPath}`;
|
|
6855
7074
|
}
|
|
6856
7075
|
throw new Error(
|
|
6857
|
-
`path "${absolutePath}" is outside workspace; use a path relative to the workspace root or under ${
|
|
7076
|
+
`path "${absolutePath}" is outside workspace; use a path relative to the workspace root or under ${workspace.root}`
|
|
6858
7077
|
);
|
|
6859
7078
|
}
|
|
6860
7079
|
function vercelBashOps(sandbox, opts = {}) {
|
|
@@ -6915,13 +7134,23 @@ function vercelEditOps(workspace) {
|
|
|
6915
7134
|
}
|
|
6916
7135
|
};
|
|
6917
7136
|
}
|
|
6918
|
-
function
|
|
6919
|
-
if (value ===
|
|
6920
|
-
if (value.startsWith(`${
|
|
6921
|
-
return `${VERCEL_SANDBOX_REMOTE_ROOT}${value.slice(
|
|
7137
|
+
function toRemotePath(value) {
|
|
7138
|
+
if (value === VERCEL_SANDBOX_LEGACY_ROOT) return VERCEL_SANDBOX_REMOTE_ROOT;
|
|
7139
|
+
if (value.startsWith(`${VERCEL_SANDBOX_LEGACY_ROOT}/`)) {
|
|
7140
|
+
return `${VERCEL_SANDBOX_REMOTE_ROOT}${value.slice(VERCEL_SANDBOX_LEGACY_ROOT.length)}`;
|
|
7141
|
+
}
|
|
7142
|
+
return value;
|
|
7143
|
+
}
|
|
7144
|
+
function toRuntimePath(value) {
|
|
7145
|
+
if (value === VERCEL_SANDBOX_LEGACY_ROOT) return VERCEL_SANDBOX_WORKSPACE_ROOT;
|
|
7146
|
+
if (value.startsWith(`${VERCEL_SANDBOX_LEGACY_ROOT}/`)) {
|
|
7147
|
+
return `${VERCEL_SANDBOX_WORKSPACE_ROOT}${value.slice(VERCEL_SANDBOX_LEGACY_ROOT.length)}`;
|
|
6922
7148
|
}
|
|
6923
7149
|
return value;
|
|
6924
7150
|
}
|
|
7151
|
+
function sanitizeRuntimeText(value) {
|
|
7152
|
+
return value.replaceAll(VERCEL_SANDBOX_LEGACY_ROOT, VERCEL_SANDBOX_WORKSPACE_ROOT);
|
|
7153
|
+
}
|
|
6925
7154
|
function findPredicate(pattern) {
|
|
6926
7155
|
const isPathShaped = pattern.includes("/") || pattern.includes("**");
|
|
6927
7156
|
if (!isPathShaped) return `-name ${shellEscape(pattern)}`;
|
|
@@ -6966,7 +7195,7 @@ function vercelFindOps(sandbox, workspace) {
|
|
|
6966
7195
|
return result.exitCode === 0;
|
|
6967
7196
|
},
|
|
6968
7197
|
async glob(pattern, cwd, options) {
|
|
6969
|
-
const remoteCwd =
|
|
7198
|
+
const remoteCwd = toRemotePath(cwd);
|
|
6970
7199
|
const args = ["fd", "--glob", "--no-require-git", "--max-results", String(options.limit)];
|
|
6971
7200
|
for (const ig of options.ignore) {
|
|
6972
7201
|
args.push("--exclude", ig);
|
|
@@ -6983,11 +7212,11 @@ function vercelFindOps(sandbox, workspace) {
|
|
|
6983
7212
|
});
|
|
6984
7213
|
}
|
|
6985
7214
|
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
6986
|
-
const stderr = Buffer.from(result.stderr).toString("utf-8").trim();
|
|
7215
|
+
const stderr = sanitizeRuntimeText(Buffer.from(result.stderr).toString("utf-8").trim());
|
|
6987
7216
|
throw new Error(`file search failed (exit ${result.exitCode}): ${stderr}`);
|
|
6988
7217
|
}
|
|
6989
7218
|
const stdout = Buffer.from(result.stdout).toString("utf-8");
|
|
6990
|
-
return stdout.split("\n").filter(Boolean);
|
|
7219
|
+
return stdout.split("\n").filter(Boolean).map(toRuntimePath);
|
|
6991
7220
|
}
|
|
6992
7221
|
};
|
|
6993
7222
|
}
|
|
@@ -7025,7 +7254,7 @@ import {
|
|
|
7025
7254
|
truncateHead,
|
|
7026
7255
|
truncateLine
|
|
7027
7256
|
} from "@mariozechner/pi-coding-agent";
|
|
7028
|
-
import { resolve as
|
|
7257
|
+
import { resolve as resolve9, relative as relative12 } from "path";
|
|
7029
7258
|
|
|
7030
7259
|
// src/server/catalog/tools/_shared.ts
|
|
7031
7260
|
function makeError(message) {
|
|
@@ -7177,7 +7406,7 @@ function vercelGrepTool(sandbox, workspaceRoot) {
|
|
|
7177
7406
|
return makeError("pattern is required");
|
|
7178
7407
|
}
|
|
7179
7408
|
if (workspaceRoot && typeof params.path === "string" && params.path.length > 0) {
|
|
7180
|
-
const resolved =
|
|
7409
|
+
const resolved = resolve9(workspaceRoot, params.path);
|
|
7181
7410
|
const rel = relative12(workspaceRoot, resolved);
|
|
7182
7411
|
if (rel.startsWith("..") || rel.startsWith("/")) {
|
|
7183
7412
|
return makeError(`path "${params.path}" is outside workspace`);
|
|
@@ -7212,6 +7441,7 @@ function isTextContent(content) {
|
|
|
7212
7441
|
function adaptPiTool(piTool) {
|
|
7213
7442
|
return {
|
|
7214
7443
|
name: piTool.name,
|
|
7444
|
+
readinessRequirements: ["workspace-fs"],
|
|
7215
7445
|
description: piTool.description,
|
|
7216
7446
|
promptSnippet: piTool.promptSnippet,
|
|
7217
7447
|
parameters: piTool.parameters,
|
|
@@ -7237,17 +7467,18 @@ function adaptPiTool(piTool) {
|
|
|
7237
7467
|
}
|
|
7238
7468
|
function buildFilesystemAgentTools(bundle) {
|
|
7239
7469
|
const cwd = bundle.workspace.root;
|
|
7470
|
+
const storageRoot = getRuntimeBundleStorageRoot(bundle);
|
|
7240
7471
|
if (bundle.sandbox.provider === "vercel-sandbox") {
|
|
7241
7472
|
return [
|
|
7242
7473
|
adaptPiTool(createReadToolDefinition(cwd, { operations: vercelReadOps(bundle.workspace) })),
|
|
7243
7474
|
adaptPiTool(createWriteToolDefinition(cwd, { operations: vercelWriteOps(bundle.workspace) })),
|
|
7244
7475
|
adaptPiTool(createEditToolDefinition(cwd, { operations: vercelEditOps(bundle.workspace) })),
|
|
7245
7476
|
adaptPiTool(createFindToolDefinition(cwd, { operations: vercelFindOps(bundle.sandbox, bundle.workspace) })),
|
|
7246
|
-
vercelGrepTool(bundle.sandbox, cwd),
|
|
7477
|
+
{ ...vercelGrepTool(bundle.sandbox, cwd), readinessRequirements: ["workspace-fs"] },
|
|
7247
7478
|
adaptPiTool(createLsToolDefinition(cwd, { operations: vercelLsOps(bundle.workspace) }))
|
|
7248
7479
|
];
|
|
7249
7480
|
}
|
|
7250
|
-
const ops = boundFs(cwd);
|
|
7481
|
+
const ops = boundFs(storageRoot, { runtimeRoot: cwd });
|
|
7251
7482
|
return [
|
|
7252
7483
|
adaptPiTool(createReadToolDefinition(cwd, { operations: ops.read })),
|
|
7253
7484
|
adaptPiTool(createWriteToolDefinition(cwd, { operations: ops.write })),
|
|
@@ -7703,7 +7934,8 @@ function treeRoutes(app, opts, done) {
|
|
|
7703
7934
|
return reply.code(statusCode).send({
|
|
7704
7935
|
error: {
|
|
7705
7936
|
code: typeof stableCode === "string" ? stableCode : ERROR_CODE_INTERNAL,
|
|
7706
|
-
message
|
|
7937
|
+
message,
|
|
7938
|
+
details: err?.details
|
|
7707
7939
|
}
|
|
7708
7940
|
});
|
|
7709
7941
|
}
|
|
@@ -8118,6 +8350,17 @@ function chatRoutes(app, opts, done) {
|
|
|
8118
8350
|
});
|
|
8119
8351
|
buf.markComplete(() => buffers.evict(sessionId, turnId));
|
|
8120
8352
|
if (streamStarted) return;
|
|
8353
|
+
const statusCode = err?.statusCode;
|
|
8354
|
+
const stableCode = err?.code;
|
|
8355
|
+
if (typeof statusCode === "number" && statusCode >= 400 && statusCode < 600) {
|
|
8356
|
+
return reply.code(statusCode).send({
|
|
8357
|
+
error: {
|
|
8358
|
+
code: typeof stableCode === "string" ? stableCode : ERROR_CODE_INTERNAL,
|
|
8359
|
+
message: err instanceof Error ? err.message : "chat route failed",
|
|
8360
|
+
details: err?.details
|
|
8361
|
+
}
|
|
8362
|
+
});
|
|
8363
|
+
}
|
|
8121
8364
|
return reply.code(500).send({
|
|
8122
8365
|
error: { code: ERROR_CODE_INTERNAL, message: "internal error" }
|
|
8123
8366
|
});
|
|
@@ -8160,17 +8403,17 @@ function chatRoutes(app, opts, done) {
|
|
|
8160
8403
|
const replayed = buf.replay(cursor);
|
|
8161
8404
|
for (const e of replayed) writer.write(e.chunk);
|
|
8162
8405
|
if (buf.complete) return;
|
|
8163
|
-
await new Promise((
|
|
8406
|
+
await new Promise((resolve10) => {
|
|
8164
8407
|
const unsub = buf.subscribe(
|
|
8165
8408
|
(e) => writer.write(e.chunk),
|
|
8166
8409
|
() => {
|
|
8167
8410
|
unsub();
|
|
8168
|
-
|
|
8411
|
+
resolve10();
|
|
8169
8412
|
}
|
|
8170
8413
|
);
|
|
8171
8414
|
request.raw.on("close", () => {
|
|
8172
8415
|
unsub();
|
|
8173
|
-
|
|
8416
|
+
resolve10();
|
|
8174
8417
|
});
|
|
8175
8418
|
});
|
|
8176
8419
|
}
|
|
@@ -8510,6 +8753,18 @@ function isNotFoundError(err) {
|
|
|
8510
8753
|
return err instanceof SessionNotFoundError || err instanceof Error && /not found/i.test(err.message);
|
|
8511
8754
|
}
|
|
8512
8755
|
function classifySessionError(err, reply) {
|
|
8756
|
+
const statusCode = err?.statusCode;
|
|
8757
|
+
const stableCode = err?.code;
|
|
8758
|
+
if (typeof statusCode === "number" && statusCode >= 400 && statusCode < 600) {
|
|
8759
|
+
const message2 = err instanceof Error ? err.message : "session route failed";
|
|
8760
|
+
return reply.code(statusCode).send({
|
|
8761
|
+
error: {
|
|
8762
|
+
code: typeof stableCode === "string" ? stableCode : ERROR_CODE_INTERNAL,
|
|
8763
|
+
message: message2,
|
|
8764
|
+
details: err?.details
|
|
8765
|
+
}
|
|
8766
|
+
});
|
|
8767
|
+
}
|
|
8513
8768
|
if (isNotFoundError(err)) {
|
|
8514
8769
|
return reply.code(404).send({
|
|
8515
8770
|
error: {
|
|
@@ -8908,8 +9163,9 @@ function catalogRoutes(app, opts, done) {
|
|
|
8908
9163
|
|
|
8909
9164
|
// src/server/http/routes/readyStatus.ts
|
|
8910
9165
|
function readyStatusRoutes(app, opts, done) {
|
|
8911
|
-
const { tracker } = opts;
|
|
8912
9166
|
app.get("/api/v1/ready-status", async (request, reply) => {
|
|
9167
|
+
const tracker = opts.getTracker ? await opts.getTracker(request) : opts.tracker;
|
|
9168
|
+
if (!tracker) throw new Error("ready-status route requires tracker or getTracker");
|
|
8913
9169
|
reply.raw.writeHead(200, {
|
|
8914
9170
|
"Content-Type": "text/event-stream",
|
|
8915
9171
|
"Cache-Control": "no-cache",
|
|
@@ -8930,7 +9186,7 @@ function readyStatusRoutes(app, opts, done) {
|
|
|
8930
9186
|
data: ${JSON.stringify(event)}
|
|
8931
9187
|
|
|
8932
9188
|
`);
|
|
8933
|
-
if (event.state === "ready") closeStream();
|
|
9189
|
+
if (event.state === "ready" || event.state === "degraded") closeStream();
|
|
8934
9190
|
});
|
|
8935
9191
|
request.raw.on("close", closeStream);
|
|
8936
9192
|
reply.hijack();
|
|
@@ -9009,6 +9265,18 @@ function searchRoutes(app, opts, done) {
|
|
|
9009
9265
|
const results = await fileSearch.search(q, limit);
|
|
9010
9266
|
return reply.send({ results });
|
|
9011
9267
|
} catch (err) {
|
|
9268
|
+
const statusCode = err?.statusCode;
|
|
9269
|
+
const stableCode = err?.code;
|
|
9270
|
+
if (typeof statusCode === "number" && statusCode >= 400 && statusCode < 600) {
|
|
9271
|
+
const message = err instanceof Error ? err.message : "search failed";
|
|
9272
|
+
return reply.code(statusCode).send({
|
|
9273
|
+
error: {
|
|
9274
|
+
code: typeof stableCode === "string" ? stableCode : ERROR_CODE_INTERNAL,
|
|
9275
|
+
message,
|
|
9276
|
+
details: err?.details
|
|
9277
|
+
}
|
|
9278
|
+
});
|
|
9279
|
+
}
|
|
9012
9280
|
request.log.error({ err }, "[search] error");
|
|
9013
9281
|
return reply.code(500).send({
|
|
9014
9282
|
error: { code: ERROR_CODE_INTERNAL, message: "search failed" }
|
|
@@ -9227,6 +9495,36 @@ function applyCspHeaders(response, opts = {}) {
|
|
|
9227
9495
|
import { basename as basename3 } from "path";
|
|
9228
9496
|
import { AuthStorage as AuthStorage3, ModelRegistry as ModelRegistry3 } from "@mariozechner/pi-coding-agent";
|
|
9229
9497
|
|
|
9498
|
+
// src/server/catalog/toolReadiness.ts
|
|
9499
|
+
var WORKSPACE_PREPARING_MESSAGE = "Workspace is still preparing. Try again in a moment.";
|
|
9500
|
+
function workspaceNotReadyToolResult(requirement) {
|
|
9501
|
+
return {
|
|
9502
|
+
content: [{ type: "text", text: WORKSPACE_PREPARING_MESSAGE }],
|
|
9503
|
+
isError: true,
|
|
9504
|
+
details: {
|
|
9505
|
+
code: ErrorCode.enum.WORKSPACE_NOT_READY,
|
|
9506
|
+
retryable: true,
|
|
9507
|
+
requirement
|
|
9508
|
+
}
|
|
9509
|
+
};
|
|
9510
|
+
}
|
|
9511
|
+
function withReadinessRequirements(tool, readinessRequirements) {
|
|
9512
|
+
if (tool.readinessRequirements === readinessRequirements) return tool;
|
|
9513
|
+
return { ...tool, readinessRequirements };
|
|
9514
|
+
}
|
|
9515
|
+
function wrapToolForReadiness(tool, checkReadiness) {
|
|
9516
|
+
if (!checkReadiness || !tool.readinessRequirements || tool.readinessRequirements.length === 0) return tool;
|
|
9517
|
+
return {
|
|
9518
|
+
...tool,
|
|
9519
|
+
async execute(params, ctx) {
|
|
9520
|
+
for (const requirement of tool.readinessRequirements ?? []) {
|
|
9521
|
+
if (!checkReadiness(requirement, tool)) return workspaceNotReadyToolResult(requirement);
|
|
9522
|
+
}
|
|
9523
|
+
return await tool.execute(params, ctx);
|
|
9524
|
+
}
|
|
9525
|
+
};
|
|
9526
|
+
}
|
|
9527
|
+
|
|
9230
9528
|
// src/server/catalog/mergeTools.ts
|
|
9231
9529
|
function setLastRegistered(merged, tool) {
|
|
9232
9530
|
merged.delete(tool.name);
|
|
@@ -9252,15 +9550,16 @@ function mergeTools(options) {
|
|
|
9252
9550
|
`[catalog] Tool "${tool.name}" overridden by plugin ${plugin.pluginName}`
|
|
9253
9551
|
);
|
|
9254
9552
|
}
|
|
9255
|
-
|
|
9553
|
+
const pluginTool = tool.readinessRequirements === void 0 ? withReadinessRequirements(tool, ["workspace-fs"]) : tool;
|
|
9554
|
+
setLastRegistered(merged, pluginTool);
|
|
9256
9555
|
}
|
|
9257
9556
|
}
|
|
9258
|
-
return [...merged.values()];
|
|
9557
|
+
return [...merged.values()].map((tool) => wrapToolForReadiness(tool, options.checkReadiness));
|
|
9259
9558
|
}
|
|
9260
9559
|
|
|
9261
9560
|
// src/server/tools/upload/index.ts
|
|
9262
9561
|
import { readFile as readFile12 } from "fs/promises";
|
|
9263
|
-
import { extname as extname4, join as
|
|
9562
|
+
import { extname as extname4, join as join13 } from "path";
|
|
9264
9563
|
var DEFAULT_UPLOAD_DIR = "assets/images";
|
|
9265
9564
|
var MAX_UPLOAD_BYTES2 = 10 * 1024 * 1024;
|
|
9266
9565
|
function contentTypeFromExt(path4) {
|
|
@@ -9300,10 +9599,11 @@ function basenameForUpload2(filename) {
|
|
|
9300
9599
|
}
|
|
9301
9600
|
function buildUploadAgentTools(bundle) {
|
|
9302
9601
|
const { workspace } = bundle;
|
|
9303
|
-
const
|
|
9602
|
+
const storageRoot = getRuntimeBundleStorageRoot(bundle);
|
|
9304
9603
|
return [
|
|
9305
9604
|
{
|
|
9306
9605
|
name: "upload_file",
|
|
9606
|
+
readinessRequirements: ["workspace-fs"],
|
|
9307
9607
|
description: "Copy a workspace file into artifact storage (assets/images by default) and return its workspace-relative path. Use this when you want to embed an image in markdown or make a generated file accessible as a stable artifact. The returned path can be used directly in markdown image syntax: ",
|
|
9308
9608
|
parameters: {
|
|
9309
9609
|
type: "object",
|
|
@@ -9328,7 +9628,7 @@ function buildUploadAgentTools(bundle) {
|
|
|
9328
9628
|
const rawDir = typeof input.directory === "string" ? input.directory.trim() : "";
|
|
9329
9629
|
const dir = rawDir ? rawDir.replace(/^\.\/+/, "").replace(/\/+$/, "") : DEFAULT_UPLOAD_DIR;
|
|
9330
9630
|
try {
|
|
9331
|
-
const bytes = await readFile12(
|
|
9631
|
+
const bytes = workspace.readBinaryFile ? Buffer.from(await workspace.readBinaryFile(filePath)) : await readFile12(join13(storageRoot, filePath));
|
|
9332
9632
|
if (bytes.byteLength === 0 || bytes.byteLength > MAX_UPLOAD_BYTES2) {
|
|
9333
9633
|
return {
|
|
9334
9634
|
content: [{ type: "text", text: `file must be between 1 byte and ${MAX_UPLOAD_BYTES2} bytes` }],
|
|
@@ -9396,9 +9696,10 @@ function selectRuntimeModeAdapter(mode, sandboxHandleStore) {
|
|
|
9396
9696
|
function getRequestWorkspaceId(request) {
|
|
9397
9697
|
return request.workspaceContext?.workspaceId ?? DEFAULT_WORKSPACE_ID3;
|
|
9398
9698
|
}
|
|
9399
|
-
function isWorkspaceAgnosticAgentRequest(request) {
|
|
9699
|
+
function isWorkspaceAgnosticAgentRequest(request, options) {
|
|
9400
9700
|
const pathname = request.url.split("?")[0] ?? request.url;
|
|
9401
|
-
|
|
9701
|
+
if (pathname === "/api/v1/ready-status") return !options?.readyStatusWorkspaceScoped;
|
|
9702
|
+
return pathname === "/health" || pathname === "/ready" || pathname === "/api/v1/agent/models";
|
|
9402
9703
|
}
|
|
9403
9704
|
function extractHttpStatus2(error) {
|
|
9404
9705
|
const statusCode = error?.statusCode;
|
|
@@ -9421,6 +9722,34 @@ function isExpiredSandboxRuntimeError(error) {
|
|
|
9421
9722
|
const message = error instanceof Error ? error.message : String(error);
|
|
9422
9723
|
return /status code (404|410) is not ok/i.test(message);
|
|
9423
9724
|
}
|
|
9725
|
+
function createHttpError(code, message, details = {}) {
|
|
9726
|
+
const error = new Error(message);
|
|
9727
|
+
error.code = code;
|
|
9728
|
+
error.statusCode = 503;
|
|
9729
|
+
error.details = details;
|
|
9730
|
+
return error;
|
|
9731
|
+
}
|
|
9732
|
+
function createAgentRuntimeNotReadyError(workspaceId) {
|
|
9733
|
+
return createHttpError(
|
|
9734
|
+
ErrorCode.enum.AGENT_RUNTIME_NOT_READY,
|
|
9735
|
+
"Agent runtime is still preparing. Try again in a moment.",
|
|
9736
|
+
{ workspaceId, retryable: true }
|
|
9737
|
+
);
|
|
9738
|
+
}
|
|
9739
|
+
function createRuntimeProvisioningFailedError(workspaceId, cause) {
|
|
9740
|
+
const causeCode = cause?.code;
|
|
9741
|
+
const details = cause?.details;
|
|
9742
|
+
return createHttpError(
|
|
9743
|
+
ErrorCode.enum.RUNTIME_PROVISIONING_FAILED,
|
|
9744
|
+
"Agent runtime provisioning failed. Reload the workspace and try again.",
|
|
9745
|
+
{
|
|
9746
|
+
workspaceId,
|
|
9747
|
+
retryable: true,
|
|
9748
|
+
...typeof causeCode === "string" ? { causeCode } : {},
|
|
9749
|
+
...details && typeof details === "object" ? { causeDetails: details } : {}
|
|
9750
|
+
}
|
|
9751
|
+
);
|
|
9752
|
+
}
|
|
9424
9753
|
var registerAgentRoutes = async (app, opts) => {
|
|
9425
9754
|
const workspaceRoot = opts.workspaceRoot ?? process.cwd();
|
|
9426
9755
|
const sessionId = opts.sessionId ?? DEFAULT_WORKSPACE_ID3;
|
|
@@ -9578,24 +9907,53 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9578
9907
|
readyTracker
|
|
9579
9908
|
};
|
|
9580
9909
|
}
|
|
9581
|
-
|
|
9910
|
+
function createRuntimeBindingEntry(workspaceId, scope, request) {
|
|
9911
|
+
const entry = {
|
|
9912
|
+
state: "pending",
|
|
9913
|
+
promise: Promise.resolve(null)
|
|
9914
|
+
};
|
|
9915
|
+
entry.promise = createRuntimeBinding(workspaceId, scope, request).then(
|
|
9916
|
+
(binding) => {
|
|
9917
|
+
entry.state = "ready";
|
|
9918
|
+
return binding;
|
|
9919
|
+
},
|
|
9920
|
+
(error) => {
|
|
9921
|
+
entry.state = "failed";
|
|
9922
|
+
entry.error = error;
|
|
9923
|
+
throw error;
|
|
9924
|
+
}
|
|
9925
|
+
);
|
|
9926
|
+
entry.promise.catch(() => {
|
|
9927
|
+
});
|
|
9928
|
+
return entry;
|
|
9929
|
+
}
|
|
9930
|
+
async function getOrCreateRuntimeBinding(workspaceId, request, options = {}) {
|
|
9582
9931
|
const scope = await resolveRuntimeScope(workspaceId, request);
|
|
9583
9932
|
const existing = runtimeBindings.get(scope.key);
|
|
9584
9933
|
if (existing) {
|
|
9934
|
+
if (options.failIfPending && existing.state === "pending") {
|
|
9935
|
+
throw createAgentRuntimeNotReadyError(workspaceId);
|
|
9936
|
+
}
|
|
9937
|
+
if (options.failIfPending && existing.state === "failed") {
|
|
9938
|
+
throw createRuntimeProvisioningFailedError(workspaceId, existing.error);
|
|
9939
|
+
}
|
|
9585
9940
|
return await ensureRuntimeBindingReady(
|
|
9586
9941
|
workspaceId,
|
|
9587
9942
|
scope,
|
|
9588
|
-
await existing
|
|
9943
|
+
await existing.promise
|
|
9589
9944
|
);
|
|
9590
9945
|
}
|
|
9591
|
-
const created =
|
|
9946
|
+
const created = createRuntimeBindingEntry(workspaceId, scope, request);
|
|
9592
9947
|
runtimeBindings.set(scope.key, created);
|
|
9593
9948
|
evictRuntimeBindings();
|
|
9949
|
+
if (options.failIfPending) {
|
|
9950
|
+
throw createAgentRuntimeNotReadyError(workspaceId);
|
|
9951
|
+
}
|
|
9594
9952
|
try {
|
|
9595
9953
|
return await ensureRuntimeBindingReady(
|
|
9596
9954
|
workspaceId,
|
|
9597
9955
|
scope,
|
|
9598
|
-
await created
|
|
9956
|
+
await created.promise
|
|
9599
9957
|
);
|
|
9600
9958
|
} catch (error) {
|
|
9601
9959
|
if (runtimeBindings.get(scope.key) === created) runtimeBindings.delete(scope.key);
|
|
@@ -9605,11 +9963,11 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9605
9963
|
async function recreateRuntimeBinding(workspaceId, scope) {
|
|
9606
9964
|
runtimeBindings.delete(scope.key);
|
|
9607
9965
|
evictSandboxHandleCacheForWorkspace(workspaceId);
|
|
9608
|
-
const created =
|
|
9966
|
+
const created = createRuntimeBindingEntry(workspaceId, scope);
|
|
9609
9967
|
runtimeBindings.set(scope.key, created);
|
|
9610
9968
|
evictRuntimeBindings();
|
|
9611
9969
|
try {
|
|
9612
|
-
const binding = await created;
|
|
9970
|
+
const binding = await created.promise;
|
|
9613
9971
|
binding.lastHealthCheckMs = Date.now();
|
|
9614
9972
|
return binding;
|
|
9615
9973
|
} catch (error) {
|
|
@@ -9647,9 +10005,9 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9647
10005
|
}
|
|
9648
10006
|
return promise;
|
|
9649
10007
|
}
|
|
9650
|
-
async function getBindingForRequest(request) {
|
|
10008
|
+
async function getBindingForRequest(request, options = {}) {
|
|
9651
10009
|
if (staticBinding) return staticBinding;
|
|
9652
|
-
return await getOrCreateRuntimeBinding(getRequestWorkspaceId(request), request);
|
|
10010
|
+
return await getOrCreateRuntimeBinding(getRequestWorkspaceId(request), request, options);
|
|
9653
10011
|
}
|
|
9654
10012
|
const agentToolNames = staticBinding ? staticBinding.tools.map((tool) => tool.name) : [
|
|
9655
10013
|
...STANDARD_AGENT_TOOL_NAMES,
|
|
@@ -9671,7 +10029,7 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9671
10029
|
app.addHook("onRequest", async (request, reply) => {
|
|
9672
10030
|
const user = request.user;
|
|
9673
10031
|
let workspaceId = DEFAULT_WORKSPACE_ID3;
|
|
9674
|
-
if (opts.getWorkspaceId && !isWorkspaceAgnosticAgentRequest(request)) {
|
|
10032
|
+
if (opts.getWorkspaceId && !isWorkspaceAgnosticAgentRequest(request, { readyStatusWorkspaceScoped: requestScopedRuntime })) {
|
|
9675
10033
|
try {
|
|
9676
10034
|
workspaceId = (await opts.getWorkspaceId(request)).trim();
|
|
9677
10035
|
} catch (error) {
|
|
@@ -9719,7 +10077,7 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9719
10077
|
});
|
|
9720
10078
|
await app.register(chatRoutes, {
|
|
9721
10079
|
getRuntime: async (request) => {
|
|
9722
|
-
const binding = await getBindingForRequest(request);
|
|
10080
|
+
const binding = await getBindingForRequest(request, { failIfPending: hasRuntimeProvisioningInput });
|
|
9723
10081
|
return {
|
|
9724
10082
|
harness: binding.harness,
|
|
9725
10083
|
workdir: binding.runtimeBundle.workspace.root
|
|
@@ -9800,12 +10158,10 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9800
10158
|
catalogRoutes,
|
|
9801
10159
|
staticBinding ? { tools: staticBinding.tools } : { getTools: async (request) => (await getBindingForRequest(request)).tools }
|
|
9802
10160
|
);
|
|
9803
|
-
await app.register(
|
|
9804
|
-
|
|
9805
|
-
|
|
9806
|
-
|
|
9807
|
-
})
|
|
9808
|
-
});
|
|
10161
|
+
await app.register(
|
|
10162
|
+
readyStatusRoutes,
|
|
10163
|
+
staticBinding ? { tracker: staticBinding.readyTracker } : { getTracker: async (request) => (await getBindingForRequest(request)).readyTracker }
|
|
10164
|
+
);
|
|
9809
10165
|
};
|
|
9810
10166
|
export {
|
|
9811
10167
|
FileHandleStore,
|