@hachej/boring-agent 0.1.23 → 0.1.26
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-HDOSWEXG.js +29 -0
- package/dist/{chunk-6R7O6BHW.js → chunk-UBWQ5LT4.js} +10 -28
- package/dist/eval/index.d.ts +4 -0
- package/dist/eval/index.js +53 -5
- package/dist/front/index.d.ts +49 -4
- package/dist/front/index.js +432 -76
- package/dist/front/styles.css +8 -0
- package/dist/{harness-BCit36Ha.d.ts → harness-CQ0uw6xW.d.ts} +5 -0
- package/dist/server/index.d.ts +29 -6
- package/dist/server/index.js +990 -609
- package/dist/shared/index.d.ts +17 -17
- package/dist/shared/index.js +8 -6
- package/docs/ERROR_CODES.md +8 -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-UBWQ5LT4.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
|
}
|
|
@@ -1229,318 +1565,44 @@ function buildDeploymentSnapshotRecipe(opts = {}) {
|
|
|
1229
1565
|
]
|
|
1230
1566
|
};
|
|
1231
1567
|
}
|
|
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
|
-
}
|
|
1427
|
-
};
|
|
1428
|
-
}
|
|
1429
|
-
function createNodeWorkspace(root) {
|
|
1430
|
-
let cachedWatcher = null;
|
|
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,20 @@ import {
|
|
|
6616
6797
|
createWriteToolDefinition
|
|
6617
6798
|
} from "@mariozechner/pi-coding-agent";
|
|
6618
6799
|
|
|
6800
|
+
// src/server/runtime/mode.ts
|
|
6801
|
+
function getRuntimeBundleStorageRoot(bundle) {
|
|
6802
|
+
const hostRoot = bundle.storageRoot ?? getNodeWorkspaceHostRoot(bundle.workspace);
|
|
6803
|
+
if (hostRoot) return hostRoot;
|
|
6804
|
+
if (bundle.sandbox.provider === "vercel-sandbox") return bundle.workspace.root;
|
|
6805
|
+
throw new Error(
|
|
6806
|
+
`RuntimeBundle.storageRoot is required for host-filesystem tools. Mode adapters must set storageRoot to the host workspace path. Got workspace.root=${bundle.workspace.root}, sandbox.provider=${bundle.sandbox.provider}`
|
|
6807
|
+
);
|
|
6808
|
+
}
|
|
6809
|
+
|
|
6619
6810
|
// src/server/tools/operations/bound.ts
|
|
6620
6811
|
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
|
|
6812
|
+
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";
|
|
6813
|
+
import { dirname as dirname11, isAbsolute as isAbsolute7, join as join12, relative as relative10, resolve as resolve8 } from "path";
|
|
6623
6814
|
function toPosixPath(value) {
|
|
6624
6815
|
return value.split("\\").join("/");
|
|
6625
6816
|
}
|
|
@@ -6675,7 +6866,7 @@ async function walkMatches(root, current, pattern, ignore, limit, out) {
|
|
|
6675
6866
|
const entries = await readdir8(current, { withFileTypes: true });
|
|
6676
6867
|
for (const entry of entries) {
|
|
6677
6868
|
if (out.length >= limit) return;
|
|
6678
|
-
const absolutePath =
|
|
6869
|
+
const absolutePath = resolve8(current, entry.name);
|
|
6679
6870
|
const relativePath = toPosixPath(relative10(root, absolutePath));
|
|
6680
6871
|
if (entry.isDirectory() && shouldSkipDir(relativePath, ignore)) continue;
|
|
6681
6872
|
if (matchesGlob(relativePath, pattern)) {
|
|
@@ -6690,26 +6881,26 @@ async function findNearestExistingAncestor(absPath) {
|
|
|
6690
6881
|
let current = absPath;
|
|
6691
6882
|
for (; ; ) {
|
|
6692
6883
|
try {
|
|
6693
|
-
await
|
|
6884
|
+
await stat10(current);
|
|
6694
6885
|
return current;
|
|
6695
6886
|
} catch {
|
|
6696
|
-
const parent =
|
|
6887
|
+
const parent = dirname11(current);
|
|
6697
6888
|
if (parent === current) return current;
|
|
6698
6889
|
current = parent;
|
|
6699
6890
|
}
|
|
6700
6891
|
}
|
|
6701
6892
|
}
|
|
6702
6893
|
async function assertWithinWorkspace(workspaceRoot, absPath) {
|
|
6703
|
-
const realRoot = await realpath4(
|
|
6894
|
+
const realRoot = await realpath4(resolve8(workspaceRoot));
|
|
6704
6895
|
try {
|
|
6705
6896
|
const s = await lstat4(absPath);
|
|
6706
6897
|
if (s.isSymbolicLink()) {
|
|
6707
6898
|
const target = await readlink(absPath);
|
|
6708
|
-
const resolvedTarget =
|
|
6899
|
+
const resolvedTarget = resolve8(dirname11(absPath), target);
|
|
6709
6900
|
const nearestAncestor = await findNearestExistingAncestor(resolvedTarget);
|
|
6710
6901
|
const realAncestor = await realpath4(nearestAncestor);
|
|
6711
6902
|
const rel2 = relative10(realRoot, realAncestor);
|
|
6712
|
-
if (rel2.startsWith("..") ||
|
|
6903
|
+
if (rel2.startsWith("..") || isAbsolute7(rel2)) {
|
|
6713
6904
|
throw new Error(`path "${absPath}" is outside workspace`);
|
|
6714
6905
|
}
|
|
6715
6906
|
return;
|
|
@@ -6727,10 +6918,10 @@ async function assertWithinWorkspace(workspaceRoot, absPath) {
|
|
|
6727
6918
|
} catch (err) {
|
|
6728
6919
|
const code = err.code;
|
|
6729
6920
|
if (code === "ENOENT") {
|
|
6730
|
-
const nearestAncestor = await findNearestExistingAncestor(
|
|
6921
|
+
const nearestAncestor = await findNearestExistingAncestor(dirname11(absPath));
|
|
6731
6922
|
const realAncestor = await realpath4(nearestAncestor);
|
|
6732
6923
|
const rel2 = relative10(realRoot, realAncestor);
|
|
6733
|
-
if (rel2.startsWith("..") ||
|
|
6924
|
+
if (rel2.startsWith("..") || isAbsolute7(rel2)) {
|
|
6734
6925
|
throw new Error(`path "${absPath}" is outside workspace`);
|
|
6735
6926
|
}
|
|
6736
6927
|
return;
|
|
@@ -6738,51 +6929,77 @@ async function assertWithinWorkspace(workspaceRoot, absPath) {
|
|
|
6738
6929
|
throw err;
|
|
6739
6930
|
}
|
|
6740
6931
|
const rel = relative10(realRoot, realCandidate);
|
|
6741
|
-
if (rel.startsWith("..") ||
|
|
6932
|
+
if (rel.startsWith("..") || isAbsolute7(rel)) {
|
|
6742
6933
|
throw new Error(`path "${absPath}" is outside workspace`);
|
|
6743
6934
|
}
|
|
6744
6935
|
}
|
|
6745
|
-
function boundFs(workspaceRoot) {
|
|
6936
|
+
function boundFs(workspaceRoot, opts = {}) {
|
|
6937
|
+
const runtimeRoot = opts.runtimeRoot ? opts.runtimeRoot.replace(/\/+$/, "") || "/" : void 0;
|
|
6938
|
+
const shouldMapRuntimeRoot = Boolean(runtimeRoot && runtimeRoot !== workspaceRoot);
|
|
6939
|
+
const toStoragePath = (absolutePath) => {
|
|
6940
|
+
if (!shouldMapRuntimeRoot || !runtimeRoot) return absolutePath;
|
|
6941
|
+
const normalized = toPosixPath(absolutePath);
|
|
6942
|
+
if (normalized === runtimeRoot) return workspaceRoot;
|
|
6943
|
+
if (normalized.startsWith(`${runtimeRoot}/`)) {
|
|
6944
|
+
return join12(workspaceRoot, ...normalized.slice(runtimeRoot.length + 1).split("/"));
|
|
6945
|
+
}
|
|
6946
|
+
return absolutePath;
|
|
6947
|
+
};
|
|
6948
|
+
const toRuntimePath2 = (absolutePath) => {
|
|
6949
|
+
if (!shouldMapRuntimeRoot || !runtimeRoot) return absolutePath;
|
|
6950
|
+
const rel = relative10(workspaceRoot, absolutePath);
|
|
6951
|
+
if (rel === "") return runtimeRoot;
|
|
6952
|
+
if (rel.startsWith("..") || isAbsolute7(rel)) return absolutePath;
|
|
6953
|
+
return `${runtimeRoot}/${toPosixPath(rel)}`;
|
|
6954
|
+
};
|
|
6746
6955
|
const read = {
|
|
6747
6956
|
async readFile(absolutePath) {
|
|
6748
|
-
|
|
6749
|
-
|
|
6957
|
+
const storagePath = toStoragePath(absolutePath);
|
|
6958
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
6959
|
+
return await readFile11(storagePath);
|
|
6750
6960
|
},
|
|
6751
6961
|
async access(absolutePath) {
|
|
6752
|
-
|
|
6753
|
-
await
|
|
6962
|
+
const storagePath = toStoragePath(absolutePath);
|
|
6963
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
6964
|
+
await access4(storagePath, constants4.R_OK);
|
|
6754
6965
|
}
|
|
6755
6966
|
};
|
|
6756
6967
|
const write = {
|
|
6757
6968
|
async writeFile(absolutePath, content) {
|
|
6758
|
-
|
|
6759
|
-
await
|
|
6760
|
-
await
|
|
6969
|
+
const storagePath = toStoragePath(absolutePath);
|
|
6970
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
6971
|
+
await mkdir12(dirname11(storagePath), { recursive: true });
|
|
6972
|
+
await writeFile8(storagePath, content);
|
|
6761
6973
|
},
|
|
6762
6974
|
async mkdir(dir) {
|
|
6763
|
-
|
|
6764
|
-
await
|
|
6975
|
+
const storagePath = toStoragePath(dir);
|
|
6976
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
6977
|
+
await mkdir12(storagePath, { recursive: true });
|
|
6765
6978
|
}
|
|
6766
6979
|
};
|
|
6767
6980
|
const edit = {
|
|
6768
6981
|
async readFile(absolutePath) {
|
|
6769
|
-
|
|
6770
|
-
|
|
6982
|
+
const storagePath = toStoragePath(absolutePath);
|
|
6983
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
6984
|
+
return await readFile11(storagePath);
|
|
6771
6985
|
},
|
|
6772
6986
|
async writeFile(absolutePath, content) {
|
|
6773
|
-
|
|
6774
|
-
await
|
|
6987
|
+
const storagePath = toStoragePath(absolutePath);
|
|
6988
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
6989
|
+
await writeFile8(storagePath, content);
|
|
6775
6990
|
},
|
|
6776
6991
|
async access(absolutePath) {
|
|
6777
|
-
|
|
6778
|
-
await
|
|
6992
|
+
const storagePath = toStoragePath(absolutePath);
|
|
6993
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
6994
|
+
await access4(storagePath, constants4.R_OK | constants4.W_OK);
|
|
6779
6995
|
}
|
|
6780
6996
|
};
|
|
6781
6997
|
const find = {
|
|
6782
6998
|
async exists(absolutePath) {
|
|
6783
|
-
|
|
6999
|
+
const storagePath = toStoragePath(absolutePath);
|
|
7000
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
6784
7001
|
try {
|
|
6785
|
-
await
|
|
7002
|
+
await stat10(storagePath);
|
|
6786
7003
|
return true;
|
|
6787
7004
|
} catch (err) {
|
|
6788
7005
|
if (err.code === "ENOENT") return false;
|
|
@@ -6790,54 +7007,61 @@ function boundFs(workspaceRoot) {
|
|
|
6790
7007
|
}
|
|
6791
7008
|
},
|
|
6792
7009
|
async glob(pattern, cwd, options) {
|
|
6793
|
-
|
|
7010
|
+
const storageCwd = toStoragePath(cwd);
|
|
7011
|
+
await assertWithinWorkspace(workspaceRoot, storageCwd);
|
|
6794
7012
|
const matches = [];
|
|
6795
|
-
await walkMatches(
|
|
6796
|
-
return matches;
|
|
7013
|
+
await walkMatches(storageCwd, storageCwd, pattern, options.ignore, options.limit, matches);
|
|
7014
|
+
return matches.map(toRuntimePath2);
|
|
6797
7015
|
}
|
|
6798
7016
|
};
|
|
6799
7017
|
const grep = {
|
|
6800
7018
|
async isDirectory(absolutePath) {
|
|
6801
|
-
|
|
6802
|
-
|
|
7019
|
+
const storagePath = toStoragePath(absolutePath);
|
|
7020
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7021
|
+
return (await stat10(storagePath)).isDirectory();
|
|
6803
7022
|
},
|
|
6804
7023
|
async readFile(absolutePath) {
|
|
6805
|
-
|
|
6806
|
-
|
|
7024
|
+
const storagePath = toStoragePath(absolutePath);
|
|
7025
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7026
|
+
return await readFile11(storagePath, "utf8");
|
|
6807
7027
|
}
|
|
6808
7028
|
};
|
|
6809
7029
|
const ls = {
|
|
6810
7030
|
async exists(absolutePath) {
|
|
7031
|
+
const storagePath = toStoragePath(absolutePath);
|
|
6811
7032
|
try {
|
|
6812
|
-
await assertWithinWorkspace(workspaceRoot,
|
|
6813
|
-
await
|
|
7033
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7034
|
+
await stat10(storagePath);
|
|
6814
7035
|
return true;
|
|
6815
7036
|
} catch {
|
|
6816
7037
|
return false;
|
|
6817
7038
|
}
|
|
6818
7039
|
},
|
|
6819
7040
|
async stat(absolutePath) {
|
|
6820
|
-
|
|
6821
|
-
|
|
7041
|
+
const storagePath = toStoragePath(absolutePath);
|
|
7042
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7043
|
+
return await stat10(storagePath);
|
|
6822
7044
|
},
|
|
6823
7045
|
async readdir(absolutePath) {
|
|
6824
|
-
|
|
6825
|
-
|
|
7046
|
+
const storagePath = toStoragePath(absolutePath);
|
|
7047
|
+
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7048
|
+
return await readdir8(storagePath);
|
|
6826
7049
|
}
|
|
6827
7050
|
};
|
|
6828
7051
|
return { read, write, edit, find, grep, ls };
|
|
6829
7052
|
}
|
|
6830
7053
|
|
|
6831
7054
|
// src/server/tools/operations/vercel.ts
|
|
6832
|
-
import { isAbsolute as
|
|
7055
|
+
import { isAbsolute as isAbsolute8, relative as relative11 } from "path";
|
|
7056
|
+
var VERCEL_SANDBOX_LEGACY_ROOT = "/vercel/sandbox";
|
|
6833
7057
|
function rootAliases(workspace) {
|
|
6834
7058
|
const aliases = [workspace.root];
|
|
6835
|
-
if (workspace.root === VERCEL_SANDBOX_WORKSPACE_ROOT) aliases.push(
|
|
6836
|
-
if (workspace.root ===
|
|
6837
|
-
return aliases;
|
|
7059
|
+
if (workspace.root === VERCEL_SANDBOX_WORKSPACE_ROOT) aliases.push(VERCEL_SANDBOX_LEGACY_ROOT);
|
|
7060
|
+
if (workspace.root === VERCEL_SANDBOX_LEGACY_ROOT) aliases.push(VERCEL_SANDBOX_WORKSPACE_ROOT);
|
|
7061
|
+
return Array.from(new Set(aliases));
|
|
6838
7062
|
}
|
|
6839
7063
|
function isOutsideWorkspaceRel(rel) {
|
|
6840
|
-
return rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") ||
|
|
7064
|
+
return rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isAbsolute8(rel);
|
|
6841
7065
|
}
|
|
6842
7066
|
function toRelPath(workspace, absolutePath) {
|
|
6843
7067
|
for (const root of rootAliases(workspace)) {
|
|
@@ -6854,7 +7078,7 @@ function toRelPath(workspace, absolutePath) {
|
|
|
6854
7078
|
return `.agents/skills/${skillPath}`;
|
|
6855
7079
|
}
|
|
6856
7080
|
throw new Error(
|
|
6857
|
-
`path "${absolutePath}" is outside workspace; use a path relative to the workspace root or under ${
|
|
7081
|
+
`path "${absolutePath}" is outside workspace; use a path relative to the workspace root or under ${workspace.root}`
|
|
6858
7082
|
);
|
|
6859
7083
|
}
|
|
6860
7084
|
function vercelBashOps(sandbox, opts = {}) {
|
|
@@ -6915,13 +7139,23 @@ function vercelEditOps(workspace) {
|
|
|
6915
7139
|
}
|
|
6916
7140
|
};
|
|
6917
7141
|
}
|
|
6918
|
-
function
|
|
6919
|
-
if (value ===
|
|
6920
|
-
if (value.startsWith(`${
|
|
6921
|
-
return `${VERCEL_SANDBOX_REMOTE_ROOT}${value.slice(
|
|
7142
|
+
function toRemotePath(value) {
|
|
7143
|
+
if (value === VERCEL_SANDBOX_LEGACY_ROOT) return VERCEL_SANDBOX_REMOTE_ROOT;
|
|
7144
|
+
if (value.startsWith(`${VERCEL_SANDBOX_LEGACY_ROOT}/`)) {
|
|
7145
|
+
return `${VERCEL_SANDBOX_REMOTE_ROOT}${value.slice(VERCEL_SANDBOX_LEGACY_ROOT.length)}`;
|
|
7146
|
+
}
|
|
7147
|
+
return value;
|
|
7148
|
+
}
|
|
7149
|
+
function toRuntimePath(value) {
|
|
7150
|
+
if (value === VERCEL_SANDBOX_LEGACY_ROOT) return VERCEL_SANDBOX_WORKSPACE_ROOT;
|
|
7151
|
+
if (value.startsWith(`${VERCEL_SANDBOX_LEGACY_ROOT}/`)) {
|
|
7152
|
+
return `${VERCEL_SANDBOX_WORKSPACE_ROOT}${value.slice(VERCEL_SANDBOX_LEGACY_ROOT.length)}`;
|
|
6922
7153
|
}
|
|
6923
7154
|
return value;
|
|
6924
7155
|
}
|
|
7156
|
+
function sanitizeRuntimeText(value) {
|
|
7157
|
+
return value.replaceAll(VERCEL_SANDBOX_LEGACY_ROOT, VERCEL_SANDBOX_WORKSPACE_ROOT);
|
|
7158
|
+
}
|
|
6925
7159
|
function findPredicate(pattern) {
|
|
6926
7160
|
const isPathShaped = pattern.includes("/") || pattern.includes("**");
|
|
6927
7161
|
if (!isPathShaped) return `-name ${shellEscape(pattern)}`;
|
|
@@ -6966,7 +7200,7 @@ function vercelFindOps(sandbox, workspace) {
|
|
|
6966
7200
|
return result.exitCode === 0;
|
|
6967
7201
|
},
|
|
6968
7202
|
async glob(pattern, cwd, options) {
|
|
6969
|
-
const remoteCwd =
|
|
7203
|
+
const remoteCwd = toRemotePath(cwd);
|
|
6970
7204
|
const args = ["fd", "--glob", "--no-require-git", "--max-results", String(options.limit)];
|
|
6971
7205
|
for (const ig of options.ignore) {
|
|
6972
7206
|
args.push("--exclude", ig);
|
|
@@ -6983,11 +7217,11 @@ function vercelFindOps(sandbox, workspace) {
|
|
|
6983
7217
|
});
|
|
6984
7218
|
}
|
|
6985
7219
|
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
6986
|
-
const stderr = Buffer.from(result.stderr).toString("utf-8").trim();
|
|
7220
|
+
const stderr = sanitizeRuntimeText(Buffer.from(result.stderr).toString("utf-8").trim());
|
|
6987
7221
|
throw new Error(`file search failed (exit ${result.exitCode}): ${stderr}`);
|
|
6988
7222
|
}
|
|
6989
7223
|
const stdout = Buffer.from(result.stdout).toString("utf-8");
|
|
6990
|
-
return stdout.split("\n").filter(Boolean);
|
|
7224
|
+
return stdout.split("\n").filter(Boolean).map(toRuntimePath);
|
|
6991
7225
|
}
|
|
6992
7226
|
};
|
|
6993
7227
|
}
|
|
@@ -7025,7 +7259,7 @@ import {
|
|
|
7025
7259
|
truncateHead,
|
|
7026
7260
|
truncateLine
|
|
7027
7261
|
} from "@mariozechner/pi-coding-agent";
|
|
7028
|
-
import { resolve as
|
|
7262
|
+
import { resolve as resolve9, relative as relative12 } from "path";
|
|
7029
7263
|
|
|
7030
7264
|
// src/server/catalog/tools/_shared.ts
|
|
7031
7265
|
function makeError(message) {
|
|
@@ -7177,7 +7411,7 @@ function vercelGrepTool(sandbox, workspaceRoot) {
|
|
|
7177
7411
|
return makeError("pattern is required");
|
|
7178
7412
|
}
|
|
7179
7413
|
if (workspaceRoot && typeof params.path === "string" && params.path.length > 0) {
|
|
7180
|
-
const resolved =
|
|
7414
|
+
const resolved = resolve9(workspaceRoot, params.path);
|
|
7181
7415
|
const rel = relative12(workspaceRoot, resolved);
|
|
7182
7416
|
if (rel.startsWith("..") || rel.startsWith("/")) {
|
|
7183
7417
|
return makeError(`path "${params.path}" is outside workspace`);
|
|
@@ -7212,6 +7446,7 @@ function isTextContent(content) {
|
|
|
7212
7446
|
function adaptPiTool(piTool) {
|
|
7213
7447
|
return {
|
|
7214
7448
|
name: piTool.name,
|
|
7449
|
+
readinessRequirements: ["workspace-fs"],
|
|
7215
7450
|
description: piTool.description,
|
|
7216
7451
|
promptSnippet: piTool.promptSnippet,
|
|
7217
7452
|
parameters: piTool.parameters,
|
|
@@ -7237,17 +7472,18 @@ function adaptPiTool(piTool) {
|
|
|
7237
7472
|
}
|
|
7238
7473
|
function buildFilesystemAgentTools(bundle) {
|
|
7239
7474
|
const cwd = bundle.workspace.root;
|
|
7475
|
+
const storageRoot = getRuntimeBundleStorageRoot(bundle);
|
|
7240
7476
|
if (bundle.sandbox.provider === "vercel-sandbox") {
|
|
7241
7477
|
return [
|
|
7242
7478
|
adaptPiTool(createReadToolDefinition(cwd, { operations: vercelReadOps(bundle.workspace) })),
|
|
7243
7479
|
adaptPiTool(createWriteToolDefinition(cwd, { operations: vercelWriteOps(bundle.workspace) })),
|
|
7244
7480
|
adaptPiTool(createEditToolDefinition(cwd, { operations: vercelEditOps(bundle.workspace) })),
|
|
7245
7481
|
adaptPiTool(createFindToolDefinition(cwd, { operations: vercelFindOps(bundle.sandbox, bundle.workspace) })),
|
|
7246
|
-
vercelGrepTool(bundle.sandbox, cwd),
|
|
7482
|
+
{ ...vercelGrepTool(bundle.sandbox, cwd), readinessRequirements: ["workspace-fs"] },
|
|
7247
7483
|
adaptPiTool(createLsToolDefinition(cwd, { operations: vercelLsOps(bundle.workspace) }))
|
|
7248
7484
|
];
|
|
7249
7485
|
}
|
|
7250
|
-
const ops = boundFs(cwd);
|
|
7486
|
+
const ops = boundFs(storageRoot, { runtimeRoot: cwd });
|
|
7251
7487
|
return [
|
|
7252
7488
|
adaptPiTool(createReadToolDefinition(cwd, { operations: ops.read })),
|
|
7253
7489
|
adaptPiTool(createWriteToolDefinition(cwd, { operations: ops.write })),
|
|
@@ -7303,6 +7539,7 @@ function directSpawnHook(workspaceRoot, runtime) {
|
|
|
7303
7539
|
}
|
|
7304
7540
|
var VERCEL_SAFE_DEFAULT_PATH = "/vercel/runtimes/node24/bin:/vercel/runtimes/node22/bin:/usr/local/bin:/usr/bin:/bin";
|
|
7305
7541
|
function bashOptionsForMode(bundle, runtime) {
|
|
7542
|
+
const storageRoot = getRuntimeBundleStorageRoot(bundle);
|
|
7306
7543
|
switch (bundle.sandbox.provider) {
|
|
7307
7544
|
case "vercel-sandbox":
|
|
7308
7545
|
return {
|
|
@@ -7316,12 +7553,12 @@ function bashOptionsForMode(bundle, runtime) {
|
|
|
7316
7553
|
case "bwrap":
|
|
7317
7554
|
return {
|
|
7318
7555
|
operations: createLocalBashOperations(),
|
|
7319
|
-
spawnHook: bwrapSpawnHook(
|
|
7556
|
+
spawnHook: bwrapSpawnHook(storageRoot, runtime)
|
|
7320
7557
|
};
|
|
7321
7558
|
default:
|
|
7322
7559
|
return {
|
|
7323
7560
|
operations: createLocalBashOperations(),
|
|
7324
|
-
spawnHook: directSpawnHook(
|
|
7561
|
+
spawnHook: directSpawnHook(storageRoot, runtime)
|
|
7325
7562
|
};
|
|
7326
7563
|
}
|
|
7327
7564
|
}
|
|
@@ -7703,7 +7940,8 @@ function treeRoutes(app, opts, done) {
|
|
|
7703
7940
|
return reply.code(statusCode).send({
|
|
7704
7941
|
error: {
|
|
7705
7942
|
code: typeof stableCode === "string" ? stableCode : ERROR_CODE_INTERNAL,
|
|
7706
|
-
message
|
|
7943
|
+
message,
|
|
7944
|
+
details: err?.details
|
|
7707
7945
|
}
|
|
7708
7946
|
});
|
|
7709
7947
|
}
|
|
@@ -8118,6 +8356,17 @@ function chatRoutes(app, opts, done) {
|
|
|
8118
8356
|
});
|
|
8119
8357
|
buf.markComplete(() => buffers.evict(sessionId, turnId));
|
|
8120
8358
|
if (streamStarted) return;
|
|
8359
|
+
const statusCode = err?.statusCode;
|
|
8360
|
+
const stableCode = err?.code;
|
|
8361
|
+
if (typeof statusCode === "number" && statusCode >= 400 && statusCode < 600) {
|
|
8362
|
+
return reply.code(statusCode).send({
|
|
8363
|
+
error: {
|
|
8364
|
+
code: typeof stableCode === "string" ? stableCode : ERROR_CODE_INTERNAL,
|
|
8365
|
+
message: err instanceof Error ? err.message : "chat route failed",
|
|
8366
|
+
details: err?.details
|
|
8367
|
+
}
|
|
8368
|
+
});
|
|
8369
|
+
}
|
|
8121
8370
|
return reply.code(500).send({
|
|
8122
8371
|
error: { code: ERROR_CODE_INTERNAL, message: "internal error" }
|
|
8123
8372
|
});
|
|
@@ -8160,17 +8409,17 @@ function chatRoutes(app, opts, done) {
|
|
|
8160
8409
|
const replayed = buf.replay(cursor);
|
|
8161
8410
|
for (const e of replayed) writer.write(e.chunk);
|
|
8162
8411
|
if (buf.complete) return;
|
|
8163
|
-
await new Promise((
|
|
8412
|
+
await new Promise((resolve10) => {
|
|
8164
8413
|
const unsub = buf.subscribe(
|
|
8165
8414
|
(e) => writer.write(e.chunk),
|
|
8166
8415
|
() => {
|
|
8167
8416
|
unsub();
|
|
8168
|
-
|
|
8417
|
+
resolve10();
|
|
8169
8418
|
}
|
|
8170
8419
|
);
|
|
8171
8420
|
request.raw.on("close", () => {
|
|
8172
8421
|
unsub();
|
|
8173
|
-
|
|
8422
|
+
resolve10();
|
|
8174
8423
|
});
|
|
8175
8424
|
});
|
|
8176
8425
|
}
|
|
@@ -8510,6 +8759,18 @@ function isNotFoundError(err) {
|
|
|
8510
8759
|
return err instanceof SessionNotFoundError || err instanceof Error && /not found/i.test(err.message);
|
|
8511
8760
|
}
|
|
8512
8761
|
function classifySessionError(err, reply) {
|
|
8762
|
+
const statusCode = err?.statusCode;
|
|
8763
|
+
const stableCode = err?.code;
|
|
8764
|
+
if (typeof statusCode === "number" && statusCode >= 400 && statusCode < 600) {
|
|
8765
|
+
const message2 = err instanceof Error ? err.message : "session route failed";
|
|
8766
|
+
return reply.code(statusCode).send({
|
|
8767
|
+
error: {
|
|
8768
|
+
code: typeof stableCode === "string" ? stableCode : ERROR_CODE_INTERNAL,
|
|
8769
|
+
message: message2,
|
|
8770
|
+
details: err?.details
|
|
8771
|
+
}
|
|
8772
|
+
});
|
|
8773
|
+
}
|
|
8513
8774
|
if (isNotFoundError(err)) {
|
|
8514
8775
|
return reply.code(404).send({
|
|
8515
8776
|
error: {
|
|
@@ -8908,8 +9169,9 @@ function catalogRoutes(app, opts, done) {
|
|
|
8908
9169
|
|
|
8909
9170
|
// src/server/http/routes/readyStatus.ts
|
|
8910
9171
|
function readyStatusRoutes(app, opts, done) {
|
|
8911
|
-
const { tracker } = opts;
|
|
8912
9172
|
app.get("/api/v1/ready-status", async (request, reply) => {
|
|
9173
|
+
const tracker = opts.getTracker ? await opts.getTracker(request) : opts.tracker;
|
|
9174
|
+
if (!tracker) throw new Error("ready-status route requires tracker or getTracker");
|
|
8913
9175
|
reply.raw.writeHead(200, {
|
|
8914
9176
|
"Content-Type": "text/event-stream",
|
|
8915
9177
|
"Cache-Control": "no-cache",
|
|
@@ -8930,7 +9192,7 @@ function readyStatusRoutes(app, opts, done) {
|
|
|
8930
9192
|
data: ${JSON.stringify(event)}
|
|
8931
9193
|
|
|
8932
9194
|
`);
|
|
8933
|
-
if (event.state === "ready") closeStream();
|
|
9195
|
+
if (event.state === "ready" || event.state === "degraded") closeStream();
|
|
8934
9196
|
});
|
|
8935
9197
|
request.raw.on("close", closeStream);
|
|
8936
9198
|
reply.hijack();
|
|
@@ -9009,6 +9271,18 @@ function searchRoutes(app, opts, done) {
|
|
|
9009
9271
|
const results = await fileSearch.search(q, limit);
|
|
9010
9272
|
return reply.send({ results });
|
|
9011
9273
|
} catch (err) {
|
|
9274
|
+
const statusCode = err?.statusCode;
|
|
9275
|
+
const stableCode = err?.code;
|
|
9276
|
+
if (typeof statusCode === "number" && statusCode >= 400 && statusCode < 600) {
|
|
9277
|
+
const message = err instanceof Error ? err.message : "search failed";
|
|
9278
|
+
return reply.code(statusCode).send({
|
|
9279
|
+
error: {
|
|
9280
|
+
code: typeof stableCode === "string" ? stableCode : ERROR_CODE_INTERNAL,
|
|
9281
|
+
message,
|
|
9282
|
+
details: err?.details
|
|
9283
|
+
}
|
|
9284
|
+
});
|
|
9285
|
+
}
|
|
9012
9286
|
request.log.error({ err }, "[search] error");
|
|
9013
9287
|
return reply.code(500).send({
|
|
9014
9288
|
error: { code: ERROR_CODE_INTERNAL, message: "search failed" }
|
|
@@ -9227,6 +9501,36 @@ function applyCspHeaders(response, opts = {}) {
|
|
|
9227
9501
|
import { basename as basename3 } from "path";
|
|
9228
9502
|
import { AuthStorage as AuthStorage3, ModelRegistry as ModelRegistry3 } from "@mariozechner/pi-coding-agent";
|
|
9229
9503
|
|
|
9504
|
+
// src/server/catalog/toolReadiness.ts
|
|
9505
|
+
var WORKSPACE_PREPARING_MESSAGE = "Workspace is still preparing. Try again in a moment.";
|
|
9506
|
+
function workspaceNotReadyToolResult(requirement) {
|
|
9507
|
+
return {
|
|
9508
|
+
content: [{ type: "text", text: WORKSPACE_PREPARING_MESSAGE }],
|
|
9509
|
+
isError: true,
|
|
9510
|
+
details: {
|
|
9511
|
+
code: ErrorCode.enum.WORKSPACE_NOT_READY,
|
|
9512
|
+
retryable: true,
|
|
9513
|
+
requirement
|
|
9514
|
+
}
|
|
9515
|
+
};
|
|
9516
|
+
}
|
|
9517
|
+
function withReadinessRequirements(tool, readinessRequirements) {
|
|
9518
|
+
if (tool.readinessRequirements === readinessRequirements) return tool;
|
|
9519
|
+
return { ...tool, readinessRequirements };
|
|
9520
|
+
}
|
|
9521
|
+
function wrapToolForReadiness(tool, checkReadiness) {
|
|
9522
|
+
if (!checkReadiness || !tool.readinessRequirements || tool.readinessRequirements.length === 0) return tool;
|
|
9523
|
+
return {
|
|
9524
|
+
...tool,
|
|
9525
|
+
async execute(params, ctx) {
|
|
9526
|
+
for (const requirement of tool.readinessRequirements ?? []) {
|
|
9527
|
+
if (!checkReadiness(requirement, tool)) return workspaceNotReadyToolResult(requirement);
|
|
9528
|
+
}
|
|
9529
|
+
return await tool.execute(params, ctx);
|
|
9530
|
+
}
|
|
9531
|
+
};
|
|
9532
|
+
}
|
|
9533
|
+
|
|
9230
9534
|
// src/server/catalog/mergeTools.ts
|
|
9231
9535
|
function setLastRegistered(merged, tool) {
|
|
9232
9536
|
merged.delete(tool.name);
|
|
@@ -9252,15 +9556,16 @@ function mergeTools(options) {
|
|
|
9252
9556
|
`[catalog] Tool "${tool.name}" overridden by plugin ${plugin.pluginName}`
|
|
9253
9557
|
);
|
|
9254
9558
|
}
|
|
9255
|
-
|
|
9559
|
+
const pluginTool = tool.readinessRequirements === void 0 ? withReadinessRequirements(tool, ["workspace-fs"]) : tool;
|
|
9560
|
+
setLastRegistered(merged, pluginTool);
|
|
9256
9561
|
}
|
|
9257
9562
|
}
|
|
9258
|
-
return [...merged.values()];
|
|
9563
|
+
return [...merged.values()].map((tool) => wrapToolForReadiness(tool, options.checkReadiness));
|
|
9259
9564
|
}
|
|
9260
9565
|
|
|
9261
9566
|
// src/server/tools/upload/index.ts
|
|
9262
9567
|
import { readFile as readFile12 } from "fs/promises";
|
|
9263
|
-
import { extname as extname4, join as
|
|
9568
|
+
import { extname as extname4, join as join13 } from "path";
|
|
9264
9569
|
var DEFAULT_UPLOAD_DIR = "assets/images";
|
|
9265
9570
|
var MAX_UPLOAD_BYTES2 = 10 * 1024 * 1024;
|
|
9266
9571
|
function contentTypeFromExt(path4) {
|
|
@@ -9300,10 +9605,11 @@ function basenameForUpload2(filename) {
|
|
|
9300
9605
|
}
|
|
9301
9606
|
function buildUploadAgentTools(bundle) {
|
|
9302
9607
|
const { workspace } = bundle;
|
|
9303
|
-
const
|
|
9608
|
+
const storageRoot = getRuntimeBundleStorageRoot(bundle);
|
|
9304
9609
|
return [
|
|
9305
9610
|
{
|
|
9306
9611
|
name: "upload_file",
|
|
9612
|
+
readinessRequirements: ["workspace-fs"],
|
|
9307
9613
|
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
9614
|
parameters: {
|
|
9309
9615
|
type: "object",
|
|
@@ -9328,7 +9634,7 @@ function buildUploadAgentTools(bundle) {
|
|
|
9328
9634
|
const rawDir = typeof input.directory === "string" ? input.directory.trim() : "";
|
|
9329
9635
|
const dir = rawDir ? rawDir.replace(/^\.\/+/, "").replace(/\/+$/, "") : DEFAULT_UPLOAD_DIR;
|
|
9330
9636
|
try {
|
|
9331
|
-
const bytes = await readFile12(
|
|
9637
|
+
const bytes = workspace.readBinaryFile ? Buffer.from(await workspace.readBinaryFile(filePath)) : await readFile12(join13(storageRoot, filePath));
|
|
9332
9638
|
if (bytes.byteLength === 0 || bytes.byteLength > MAX_UPLOAD_BYTES2) {
|
|
9333
9639
|
return {
|
|
9334
9640
|
content: [{ type: "text", text: `file must be between 1 byte and ${MAX_UPLOAD_BYTES2} bytes` }],
|
|
@@ -9396,9 +9702,10 @@ function selectRuntimeModeAdapter(mode, sandboxHandleStore) {
|
|
|
9396
9702
|
function getRequestWorkspaceId(request) {
|
|
9397
9703
|
return request.workspaceContext?.workspaceId ?? DEFAULT_WORKSPACE_ID3;
|
|
9398
9704
|
}
|
|
9399
|
-
function isWorkspaceAgnosticAgentRequest(request) {
|
|
9705
|
+
function isWorkspaceAgnosticAgentRequest(request, options) {
|
|
9400
9706
|
const pathname = request.url.split("?")[0] ?? request.url;
|
|
9401
|
-
|
|
9707
|
+
if (pathname === "/api/v1/ready-status") return !options?.readyStatusWorkspaceScoped;
|
|
9708
|
+
return pathname === "/health" || pathname === "/ready" || pathname === "/api/v1/agent/models";
|
|
9402
9709
|
}
|
|
9403
9710
|
function extractHttpStatus2(error) {
|
|
9404
9711
|
const statusCode = error?.statusCode;
|
|
@@ -9421,6 +9728,32 @@ function isExpiredSandboxRuntimeError(error) {
|
|
|
9421
9728
|
const message = error instanceof Error ? error.message : String(error);
|
|
9422
9729
|
return /status code (404|410) is not ok/i.test(message);
|
|
9423
9730
|
}
|
|
9731
|
+
function createHttpError(code, message, details = {}) {
|
|
9732
|
+
const error = new Error(message);
|
|
9733
|
+
error.code = code;
|
|
9734
|
+
error.statusCode = 503;
|
|
9735
|
+
error.details = details;
|
|
9736
|
+
return error;
|
|
9737
|
+
}
|
|
9738
|
+
function createAgentRuntimeNotReadyError(workspaceId) {
|
|
9739
|
+
return createHttpError(
|
|
9740
|
+
ErrorCode.enum.AGENT_RUNTIME_NOT_READY,
|
|
9741
|
+
"Agent runtime is still preparing. Try again in a moment.",
|
|
9742
|
+
{ workspaceId, retryable: true }
|
|
9743
|
+
);
|
|
9744
|
+
}
|
|
9745
|
+
function createRuntimeProvisioningFailedError(workspaceId, cause) {
|
|
9746
|
+
const causeCode = cause?.code;
|
|
9747
|
+
return createHttpError(
|
|
9748
|
+
ErrorCode.enum.RUNTIME_PROVISIONING_FAILED,
|
|
9749
|
+
"Agent runtime provisioning failed. Reload the workspace and try again.",
|
|
9750
|
+
{
|
|
9751
|
+
workspaceId,
|
|
9752
|
+
retryable: true,
|
|
9753
|
+
...typeof causeCode === "string" ? { causeCode } : {}
|
|
9754
|
+
}
|
|
9755
|
+
);
|
|
9756
|
+
}
|
|
9424
9757
|
var registerAgentRoutes = async (app, opts) => {
|
|
9425
9758
|
const workspaceRoot = opts.workspaceRoot ?? process.cwd();
|
|
9426
9759
|
const sessionId = opts.sessionId ?? DEFAULT_WORKSPACE_ID3;
|
|
@@ -9430,7 +9763,7 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9430
9763
|
app.addHook("onClose", async () => {
|
|
9431
9764
|
await modeAdapter.dispose?.();
|
|
9432
9765
|
});
|
|
9433
|
-
const requestScopedRuntime = typeof opts.getWorkspaceId === "function" || typeof opts.getWorkspaceRoot === "function" || typeof opts.getTemplatePath === "function" || typeof opts.getPi === "function" || typeof opts.getSessionNamespace === "function";
|
|
9766
|
+
const requestScopedRuntime = typeof opts.getWorkspaceId === "function" || typeof opts.getWorkspaceRoot === "function" || typeof opts.getTemplatePath === "function" || typeof opts.getPi === "function" || typeof opts.getSessionNamespace === "function" || typeof opts.getSystemPromptDynamic === "function";
|
|
9434
9767
|
const sessionChangesTracker = new InMemorySessionChangesTracker();
|
|
9435
9768
|
const runtimeBindings = /* @__PURE__ */ new Map();
|
|
9436
9769
|
const MAX_RUNTIME_BINDINGS = 256;
|
|
@@ -9464,7 +9797,25 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9464
9797
|
async function resolveSkillScope(workspaceId, request) {
|
|
9465
9798
|
const root = request && opts.getWorkspaceRoot ? await opts.getWorkspaceRoot(workspaceId, request) : workspaceRoot;
|
|
9466
9799
|
const pi = opts.getPi ? await opts.getPi({ workspaceId, workspaceRoot: root, request }) : opts.pi;
|
|
9467
|
-
|
|
9800
|
+
const hot = pi?.getHotReloadableResources?.();
|
|
9801
|
+
return {
|
|
9802
|
+
root,
|
|
9803
|
+
pi: hot ? {
|
|
9804
|
+
...pi,
|
|
9805
|
+
additionalSkillPaths: [
|
|
9806
|
+
...pi?.additionalSkillPaths ?? [],
|
|
9807
|
+
...hot.additionalSkillPaths ?? []
|
|
9808
|
+
],
|
|
9809
|
+
packages: [
|
|
9810
|
+
...pi?.packages ?? [],
|
|
9811
|
+
...hot.packages ?? []
|
|
9812
|
+
],
|
|
9813
|
+
extensionPaths: [
|
|
9814
|
+
...pi?.extensionPaths ?? [],
|
|
9815
|
+
...hot.extensionPaths ?? []
|
|
9816
|
+
]
|
|
9817
|
+
} : pi
|
|
9818
|
+
};
|
|
9468
9819
|
}
|
|
9469
9820
|
async function runRuntimeProvisioning(workspaceId, scope, request) {
|
|
9470
9821
|
if (opts.provisionWorkspace === false || !opts.provisionRuntime) return void 0;
|
|
@@ -9557,6 +9908,7 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9557
9908
|
cwd: root,
|
|
9558
9909
|
sessionNamespace: scope.sessionNamespace,
|
|
9559
9910
|
systemPromptAppend: opts.systemPromptAppend,
|
|
9911
|
+
systemPromptDynamic: opts.getSystemPromptDynamic ? () => opts.getSystemPromptDynamic?.({ workspaceId, workspaceRoot: root }) : opts.systemPromptDynamic,
|
|
9560
9912
|
telemetry: opts.telemetry
|
|
9561
9913
|
});
|
|
9562
9914
|
const readyTracker = new ReadyStatusTracker({
|
|
@@ -9578,24 +9930,55 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9578
9930
|
readyTracker
|
|
9579
9931
|
};
|
|
9580
9932
|
}
|
|
9581
|
-
|
|
9933
|
+
function createRuntimeBindingEntry(workspaceId, scope, request) {
|
|
9934
|
+
const entry = {
|
|
9935
|
+
state: "pending",
|
|
9936
|
+
promise: Promise.resolve(null)
|
|
9937
|
+
};
|
|
9938
|
+
entry.promise = createRuntimeBinding(workspaceId, scope, request).then(
|
|
9939
|
+
(binding) => {
|
|
9940
|
+
entry.state = "ready";
|
|
9941
|
+
return binding;
|
|
9942
|
+
},
|
|
9943
|
+
(error) => {
|
|
9944
|
+
entry.state = "failed";
|
|
9945
|
+
entry.error = error;
|
|
9946
|
+
throw error;
|
|
9947
|
+
}
|
|
9948
|
+
);
|
|
9949
|
+
entry.promise.catch(() => {
|
|
9950
|
+
});
|
|
9951
|
+
return entry;
|
|
9952
|
+
}
|
|
9953
|
+
async function getOrCreateRuntimeBinding(workspaceId, request, options = {}) {
|
|
9582
9954
|
const scope = await resolveRuntimeScope(workspaceId, request);
|
|
9583
9955
|
const existing = runtimeBindings.get(scope.key);
|
|
9584
9956
|
if (existing) {
|
|
9585
|
-
|
|
9586
|
-
workspaceId
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9957
|
+
if (options.failIfPending && existing.state === "pending") {
|
|
9958
|
+
throw createAgentRuntimeNotReadyError(workspaceId);
|
|
9959
|
+
}
|
|
9960
|
+
if (existing.state === "failed") {
|
|
9961
|
+
if (options.failIfPending) throw createRuntimeProvisioningFailedError(workspaceId, existing.error);
|
|
9962
|
+
runtimeBindings.delete(scope.key);
|
|
9963
|
+
} else {
|
|
9964
|
+
return await ensureRuntimeBindingReady(
|
|
9965
|
+
workspaceId,
|
|
9966
|
+
scope,
|
|
9967
|
+
await existing.promise
|
|
9968
|
+
);
|
|
9969
|
+
}
|
|
9590
9970
|
}
|
|
9591
|
-
const created =
|
|
9971
|
+
const created = createRuntimeBindingEntry(workspaceId, scope, request);
|
|
9592
9972
|
runtimeBindings.set(scope.key, created);
|
|
9593
9973
|
evictRuntimeBindings();
|
|
9974
|
+
if (options.failIfPending) {
|
|
9975
|
+
throw createAgentRuntimeNotReadyError(workspaceId);
|
|
9976
|
+
}
|
|
9594
9977
|
try {
|
|
9595
9978
|
return await ensureRuntimeBindingReady(
|
|
9596
9979
|
workspaceId,
|
|
9597
9980
|
scope,
|
|
9598
|
-
await created
|
|
9981
|
+
await created.promise
|
|
9599
9982
|
);
|
|
9600
9983
|
} catch (error) {
|
|
9601
9984
|
if (runtimeBindings.get(scope.key) === created) runtimeBindings.delete(scope.key);
|
|
@@ -9605,11 +9988,11 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9605
9988
|
async function recreateRuntimeBinding(workspaceId, scope) {
|
|
9606
9989
|
runtimeBindings.delete(scope.key);
|
|
9607
9990
|
evictSandboxHandleCacheForWorkspace(workspaceId);
|
|
9608
|
-
const created =
|
|
9991
|
+
const created = createRuntimeBindingEntry(workspaceId, scope);
|
|
9609
9992
|
runtimeBindings.set(scope.key, created);
|
|
9610
9993
|
evictRuntimeBindings();
|
|
9611
9994
|
try {
|
|
9612
|
-
const binding = await created;
|
|
9995
|
+
const binding = await created.promise;
|
|
9613
9996
|
binding.lastHealthCheckMs = Date.now();
|
|
9614
9997
|
return binding;
|
|
9615
9998
|
} catch (error) {
|
|
@@ -9647,9 +10030,9 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9647
10030
|
}
|
|
9648
10031
|
return promise;
|
|
9649
10032
|
}
|
|
9650
|
-
async function getBindingForRequest(request) {
|
|
10033
|
+
async function getBindingForRequest(request, options = {}) {
|
|
9651
10034
|
if (staticBinding) return staticBinding;
|
|
9652
|
-
return await getOrCreateRuntimeBinding(getRequestWorkspaceId(request), request);
|
|
10035
|
+
return await getOrCreateRuntimeBinding(getRequestWorkspaceId(request), request, options);
|
|
9653
10036
|
}
|
|
9654
10037
|
const agentToolNames = staticBinding ? staticBinding.tools.map((tool) => tool.name) : [
|
|
9655
10038
|
...STANDARD_AGENT_TOOL_NAMES,
|
|
@@ -9671,7 +10054,7 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9671
10054
|
app.addHook("onRequest", async (request, reply) => {
|
|
9672
10055
|
const user = request.user;
|
|
9673
10056
|
let workspaceId = DEFAULT_WORKSPACE_ID3;
|
|
9674
|
-
if (opts.getWorkspaceId && !isWorkspaceAgnosticAgentRequest(request)) {
|
|
10057
|
+
if (opts.getWorkspaceId && !isWorkspaceAgnosticAgentRequest(request, { readyStatusWorkspaceScoped: requestScopedRuntime })) {
|
|
9675
10058
|
try {
|
|
9676
10059
|
workspaceId = (await opts.getWorkspaceId(request)).trim();
|
|
9677
10060
|
} catch (error) {
|
|
@@ -9719,7 +10102,7 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9719
10102
|
});
|
|
9720
10103
|
await app.register(chatRoutes, {
|
|
9721
10104
|
getRuntime: async (request) => {
|
|
9722
|
-
const binding = await getBindingForRequest(request);
|
|
10105
|
+
const binding = await getBindingForRequest(request, { failIfPending: hasRuntimeProvisioningInput });
|
|
9723
10106
|
return {
|
|
9724
10107
|
harness: binding.harness,
|
|
9725
10108
|
workdir: binding.runtimeBundle.workspace.root
|
|
@@ -9800,12 +10183,10 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9800
10183
|
catalogRoutes,
|
|
9801
10184
|
staticBinding ? { tools: staticBinding.tools } : { getTools: async (request) => (await getBindingForRequest(request)).tools }
|
|
9802
10185
|
);
|
|
9803
|
-
await app.register(
|
|
9804
|
-
|
|
9805
|
-
|
|
9806
|
-
|
|
9807
|
-
})
|
|
9808
|
-
});
|
|
10186
|
+
await app.register(
|
|
10187
|
+
readyStatusRoutes,
|
|
10188
|
+
staticBinding ? { tracker: staticBinding.readyTracker } : { getTracker: async (request) => (await getBindingForRequest(request)).readyTracker }
|
|
10189
|
+
);
|
|
9809
10190
|
};
|
|
9810
10191
|
export {
|
|
9811
10192
|
FileHandleStore,
|