@hachej/boring-agent 0.1.23 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,11 @@
1
1
  import {
2
- ErrorCode,
3
2
  noopTelemetry,
4
3
  safeCapture,
5
4
  validateTool
6
- } from "../chunk-6R7O6BHW.js";
5
+ } from "../chunk-HDOSWEXG.js";
6
+ import {
7
+ ErrorCode
8
+ } from "../chunk-FILARC5X.js";
7
9
 
8
10
  // src/server/sandbox/direct/createDirectSandbox.ts
9
11
  import { spawn } from "child_process";
@@ -17,7 +19,8 @@ function getEnvSnapshot() {
17
19
  }
18
20
 
19
21
  // src/server/workspace/runtimeLayout.ts
20
- import { join, resolve } from "path";
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
- VIRTUAL_ENV: baseEnv.VIRTUAL_ENV ?? paths.venv,
85
- BORING_AGENT_WORKSPACE_ROOT: baseEnv.BORING_AGENT_WORKSPACE_ROOT ?? runtimeRoot
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, opts) {
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 = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
142
- const maxOutputBytes = opts?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
143
- const workspaceRoot = workspace.root;
144
- const cwd = opts?.cwd ?? workspaceRoot;
145
- return await new Promise((resolve9, reject) => {
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: opts?.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
- resolve9({
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, opts?.onStdout);
195
+ appendOutput(stdoutChunks, chunk2, captureState, opts2?.onStdout);
186
196
  });
187
197
  child.stderr?.on("data", (chunk2) => {
188
- appendOutput(stderrChunks, chunk2, captureState, opts?.onStderr);
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 (opts?.onHeartbeat) {
216
+ if (opts2?.onHeartbeat) {
207
217
  heartbeatHandle = setInterval(() => {
208
- opts.onHeartbeat?.(Date.now() - start);
218
+ opts2.onHeartbeat?.(Date.now() - start);
209
219
  }, 1e3);
210
220
  }
211
- if (opts?.signal) {
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 (opts.signal.aborted) {
228
+ if (opts2.signal.aborted) {
219
229
  abort();
220
230
  } else {
221
- opts.signal.addEventListener("abort", abort, { once: true });
231
+ opts2.signal.addEventListener("abort", abort, { once: true });
222
232
  child.on("close", () => {
223
- opts.signal?.removeEventListener("abort", abort);
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 isAbsolute2, join as join2, relative, resolve as resolve2, sep } from "path";
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 SANDBOX_HOME2;
353
- const absoluteCwd = isAbsolute2(cwd) ? cwd : resolve2(workspaceRoot, cwd);
354
- const relPath = relative(workspaceRoot, absoluteCwd);
355
- if (relPath === "") return SANDBOX_HOME2;
356
- if (relPath === ".." || relPath.startsWith(`..${sep}`)) {
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(sep).join("/");
360
- return `${SANDBOX_HOME2}/${posixRelPath}`;
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 dirAccessible(path4) {
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 = dirname(workspaceRoot);
734
+ const globalRoot = dirname4(workspaceRoot);
415
735
  if (globalRoot === workspaceRoot) return [];
416
736
  const args = [];
417
- if (await dirAccessible(join2(globalRoot, ".boring-agent"))) {
418
- args.push("--ro-bind", join2(globalRoot, ".boring-agent"), `${SANDBOX_HOME2}/.boring-agent`);
419
- }
420
- if (await dirAccessible(join2(globalRoot, ".venv"))) {
421
- args.push("--ro-bind", join2(globalRoot, ".venv"), `${SANDBOX_HOME2}/.venv`);
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, opts) {
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 = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
442
- const maxOutputBytes = opts?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES2;
443
- const workspaceRoot = workspace.root;
444
- const sandboxCwd = computeSandboxCwd(workspaceRoot, opts?.cwd);
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((resolve9, reject) => {
786
+ return await new Promise((resolve10, reject) => {
454
787
  const child = spawn2("bwrap", args, {
455
- env: withWorkspacePythonEnv({ workspaceRoot, env: opts?.env, sandboxRoot: SANDBOX_HOME2 }),
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
- resolve9({
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, opts?.onStdout);
827
+ appendOutput2(stdoutChunks, chunk2, captureState, opts2?.onStdout);
492
828
  });
493
829
  child.stderr?.on("data", (chunk2) => {
494
- appendOutput2(stderrChunks, chunk2, captureState, opts?.onStderr);
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 (opts?.onHeartbeat) {
848
+ if (opts2?.onHeartbeat) {
513
849
  heartbeatHandle = setInterval(() => {
514
- opts.onHeartbeat?.(Date.now() - start);
850
+ opts2.onHeartbeat?.(Date.now() - start);
515
851
  }, 1e3);
516
852
  }
517
- if (opts?.signal) {
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 (opts.signal.aborted) {
860
+ if (opts2.signal.aborted) {
525
861
  abort();
526
862
  } else {
527
- opts.signal.addEventListener("abort", abort, { once: true });
863
+ opts2.signal.addEventListener("abort", abort, { once: true });
528
864
  child.on("close", () => {
529
- opts.signal?.removeEventListener("abort", abort);
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 readFile(this.storePath, "utf8");
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 mkdir(path.dirname(this.storePath), { recursive: true, mode: 448 });
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 writeFile(tmpPath, content, { encoding: "utf8", mode: 384 });
933
+ await writeFile2(tmpPath, content, { encoding: "utf8", mode: 384 });
598
934
  tmpWritten = true;
599
935
  await chmod(tmpPath, 384);
600
- await rename(tmpPath, this.storePath);
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 unlink(tmpPath);
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 mkdir2, readFile as readFile2, rename as rename2, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
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 readFile2(cachePath, "utf8");
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 mkdir2(path2.dirname(cachePath), { recursive: true, mode: 448 });
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 writeFile2(tmpPath, content, { encoding: "utf8", mode: 384 });
1400
+ await writeFile3(tmpPath, content, { encoding: "utf8", mode: 384 });
1065
1401
  tmpWritten = true;
1066
1402
  await chmod2(tmpPath, 384);
1067
- await rename2(tmpPath, cachePath);
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 unlink2(tmpPath);
1409
+ await unlink3(tmpPath);
1074
1410
  } catch {
1075
1411
  }
1076
1412
  }
@@ -1213,334 +1549,60 @@ async function bakeSnapshotIfNeeded(opts) {
1213
1549
  }
1214
1550
  }
1215
1551
 
1216
- // src/server/sandbox/snapshots/deploymentSnapshot.ts
1217
- var UV_SETUP_COMMANDS = [
1218
- "command -v uv >/dev/null 2>&1 || python3 -m pip install --upgrade uv",
1219
- "uv --version"
1220
- ];
1221
- function buildDeploymentSnapshotRecipe(opts = {}) {
1222
- return {
1223
- runtime: opts.runtime,
1224
- systemPackages: opts.systemPackages,
1225
- pythonPackages: opts.pythonPackages,
1226
- setupCommands: [
1227
- ...opts.includeUv === false ? [] : UV_SETUP_COMMANDS,
1228
- ...opts.setupCommands ?? []
1229
- ]
1230
- };
1231
- }
1232
- async function prepareDeploymentSnapshot(provider, recipe) {
1233
- return await provider.prepareDeploymentSnapshot(recipe);
1234
- }
1235
-
1236
- // src/server/sandbox/vercel-sandbox/deploymentSnapshot.ts
1237
- function createVercelDeploymentSnapshotProvider(opts) {
1238
- return {
1239
- async prepareDeploymentSnapshot(recipe) {
1240
- return await bakeSnapshotIfNeeded({
1241
- client: opts.client,
1242
- snapshotId: opts.snapshotId,
1243
- runtime: recipe.runtime,
1244
- cachePath: opts.cachePath,
1245
- logger: opts.logger,
1246
- now: opts.now,
1247
- setupCommands: recipe.setupCommands,
1248
- pythonPackages: recipe.pythonPackages,
1249
- systemPackages: recipe.systemPackages
1250
- });
1251
- }
1252
- };
1253
- }
1254
- async function prepareVercelDeploymentSnapshot(opts) {
1255
- return await bakeSnapshotIfNeeded({
1256
- client: opts.client,
1257
- snapshotId: opts.snapshotId,
1258
- runtime: opts.runtime,
1259
- cachePath: opts.cachePath,
1260
- logger: opts.logger,
1261
- now: opts.now,
1262
- setupCommands: opts.setupCommands,
1263
- pythonPackages: opts.pythonPackages,
1264
- systemPackages: opts.systemPackages
1265
- });
1266
- }
1267
-
1268
- // src/server/workspace/createNodeWorkspace.ts
1269
- import { lstat as lstat2, mkdir as mkdir3, readdir, readFile as readFile3, rename as rename3, rmdir, stat as stat2, unlink as unlink3, writeFile as writeFile3 } from "fs/promises";
1270
- import { dirname as dirname3, relative as relative3, sep as sep2 } from "path";
1271
- import chokidar from "chokidar";
1272
-
1273
- // src/server/workspace/paths.ts
1274
- import { lstat, realpath, stat } from "fs/promises";
1275
- import { dirname as dirname2, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve3 } from "path";
1276
- function createPathValidationError(reason, requestedPath, message) {
1277
- return Object.assign(new Error(message), {
1278
- statusCode: 400,
1279
- reason,
1280
- requestedPath
1281
- });
1282
- }
1283
- function normalizeForTraversalChecks(requestedPath) {
1284
- let decoded = requestedPath;
1285
- try {
1286
- decoded = decodeURIComponent(requestedPath);
1287
- } catch {
1288
- }
1289
- return decoded.replace(/\\/g, "/");
1290
- }
1291
- function hasTraversalSegment(requestedPath) {
1292
- return requestedPath.split("/").some((segment) => segment === ".." || segment.startsWith(".."));
1293
- }
1294
- function isWindowsAbsolutePath(requestedPath) {
1295
- return /^[A-Za-z]:[\\/]/.test(requestedPath) || requestedPath.startsWith("\\\\");
1296
- }
1297
- function validatePath(workspaceRoot, relPath) {
1298
- if (relPath.includes("\0")) {
1299
- throw createPathValidationError("null-byte", relPath, "Null byte in path");
1300
- }
1301
- const normalized = normalizeForTraversalChecks(relPath);
1302
- if (isAbsolute3(relPath) || normalized.startsWith("/") || isWindowsAbsolutePath(relPath) || isWindowsAbsolutePath(normalized)) {
1303
- throw createPathValidationError("absolute-path", relPath, "Absolute paths are not allowed");
1304
- }
1305
- if (hasTraversalSegment(normalized) || normalized.startsWith("~") || normalized.startsWith("$") || /[\r\n]/.test(normalized)) {
1306
- throw createPathValidationError("path-escape", relPath, "Path escapes workspace root");
1307
- }
1308
- const resolvedRoot = resolve3(workspaceRoot);
1309
- const resolvedPath = resolve3(workspaceRoot, relPath);
1310
- if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}/`)) {
1311
- throw createPathValidationError("path-escape", relPath, "Path escapes workspace root");
1312
- }
1313
- return resolvedPath;
1314
- }
1315
- async function assertRealPathWithinWorkspace(workspaceRoot, absPath) {
1316
- const realRoot = await realpath(resolve3(workspaceRoot));
1317
- const realCandidate = await realpath(absPath);
1318
- const rel = relative2(realRoot, realCandidate);
1319
- if (rel.startsWith("..") || isAbsolute3(rel)) {
1320
- throw createPathValidationError(
1321
- "symlink-escape",
1322
- absPath,
1323
- "Resolved path escapes workspace root"
1324
- );
1325
- }
1326
- }
1327
- async function ensureExistingWorkspacePath(workspaceRoot, relPath) {
1328
- const absPath = validatePath(workspaceRoot, relPath);
1329
- await assertRealPathWithinWorkspace(workspaceRoot, absPath);
1330
- await stat(absPath);
1331
- return absPath;
1332
- }
1333
- async function ensureWritableWorkspacePath(workspaceRoot, relPath) {
1334
- const absPath = validatePath(workspaceRoot, relPath);
1335
- const parentPath = dirname2(absPath);
1336
- await stat(parentPath);
1337
- await assertRealPathWithinWorkspace(workspaceRoot, parentPath);
1338
- try {
1339
- const pathStat = await lstat(absPath);
1340
- if (pathStat.isSymbolicLink()) {
1341
- throw createPathValidationError("symlink-escape", relPath, "Target path is a symlink");
1342
- }
1343
- } catch (error) {
1344
- const code = error.code;
1345
- if (code !== "ENOENT") {
1346
- throw error;
1347
- }
1348
- }
1349
- return absPath;
1350
- }
1351
-
1352
- // src/server/workspace/createNodeWorkspace.ts
1353
- var DEFAULT_WATCH_IGNORES = [
1354
- "node_modules",
1355
- ".git",
1356
- ".DS_Store",
1357
- "dist",
1358
- ".next",
1359
- ".turbo",
1360
- "test-results"
1361
- ];
1362
- function shouldIgnoreWatchPath(path4) {
1363
- const parts = path4.split(sep2);
1364
- return parts.some(
1365
- (part) => DEFAULT_WATCH_IGNORES.includes(part) || part.endsWith(".tsbuildinfo")
1366
- );
1367
- }
1368
- function createNodeWatcher(root) {
1369
- const listeners = /* @__PURE__ */ new Set();
1370
- let fsw = null;
1371
- let closed = false;
1372
- const ensureFsw = () => {
1373
- if (fsw) return fsw;
1374
- fsw = chokidar.watch(root, {
1375
- ignored: shouldIgnoreWatchPath,
1376
- ignoreInitial: true,
1377
- persistent: true,
1378
- followSymlinks: false
1379
- // Avoid chokidar's awaitWriteFinish polling loop in long-lived app
1380
- // servers. Consumers already reconcile by mtime after invalidation.
1381
- // No native renames from chokidar — `unlinkDir`/`addDir`/
1382
- // `unlink`/`add` are the primitives we get. We surface them as
1383
- // separate `unlink` + `write`/`mkdir` events; renames are best
1384
- // recovered at the consumer level if needed.
1385
- });
1386
- fsw.on("add", (p, s) => emit({ op: "write", path: rel(p), mtimeMs: s?.mtimeMs }));
1387
- fsw.on("change", (p, s) => emit({ op: "write", path: rel(p), mtimeMs: s?.mtimeMs }));
1388
- fsw.on("addDir", (p) => emit({ op: "mkdir", path: rel(p) }));
1389
- fsw.on("unlink", (p) => emit({ op: "unlink", path: rel(p) }));
1390
- fsw.on("unlinkDir", (p) => emit({ op: "unlink", path: rel(p) }));
1391
- fsw.on("error", () => {
1392
- });
1393
- return fsw;
1394
- };
1395
- const rel = (abs) => {
1396
- const r = relative3(root, abs);
1397
- return r.split(sep2).join("/");
1398
- };
1399
- const emit = (event) => {
1400
- if (closed) return;
1401
- if (event.path === "" || event.path.startsWith("..")) return;
1402
- for (const l of [...listeners]) {
1403
- try {
1404
- l(event);
1405
- } catch {
1406
- }
1407
- }
1408
- };
1409
- return {
1410
- subscribe(listener) {
1411
- if (closed) return () => {
1412
- };
1413
- listeners.add(listener);
1414
- ensureFsw();
1415
- return () => {
1416
- listeners.delete(listener);
1417
- };
1418
- },
1419
- close() {
1420
- if (closed) return;
1421
- closed = true;
1422
- listeners.clear();
1423
- fsw?.close().catch(() => {
1424
- });
1425
- fsw = null;
1426
- }
1552
+ // src/server/sandbox/snapshots/deploymentSnapshot.ts
1553
+ var UV_SETUP_COMMANDS = [
1554
+ "command -v uv >/dev/null 2>&1 || python3 -m pip install --upgrade uv",
1555
+ "uv --version"
1556
+ ];
1557
+ function buildDeploymentSnapshotRecipe(opts = {}) {
1558
+ return {
1559
+ runtime: opts.runtime,
1560
+ systemPackages: opts.systemPackages,
1561
+ pythonPackages: opts.pythonPackages,
1562
+ setupCommands: [
1563
+ ...opts.includeUv === false ? [] : UV_SETUP_COMMANDS,
1564
+ ...opts.setupCommands ?? []
1565
+ ]
1427
1566
  };
1428
1567
  }
1429
- function 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
- root,
1433
- fsCapability: "strong",
1434
- watch() {
1435
- if (!cachedWatcher) cachedWatcher = createNodeWatcher(root);
1436
- return cachedWatcher;
1437
- },
1438
- async readFile(relPath) {
1439
- const absPath = await ensureExistingWorkspacePath(root, relPath);
1440
- return await readFile3(absPath, "utf-8");
1441
- },
1442
- async readBinaryFile(relPath) {
1443
- const absPath = await ensureExistingWorkspacePath(root, relPath);
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 dirname4, extname, relative as relative4 } from "path/posix";
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 = dirname4(sourcePath.replace(/\\/g, "/"));
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 stat10 = await workspace.stat(path4);
1864
- if (stat10.kind !== "file") {
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: stat11 } = await workspace.readFileWithStat(path4);
1883
- return { content: content2, mtimeMs: stat11.kind === "file" ? stat11.mtimeMs : void 0 };
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 stat10 = await workspace.stat(path4);
1887
- return { content, mtimeMs: stat10.kind === "file" ? stat10.mtimeMs : void 0 };
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 stat10 = workspace.writeFileWithStat ? await workspace.writeFileWithStat(path4, content) : await (async () => {
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: stat10.kind === "file" ? stat10.mtimeMs : void 0 };
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 stat10 = workspace.writeBinaryFileWithStat ? await workspace.writeBinaryFileWithStat(path4, bytes) : await (async () => {
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: stat10.kind === "file" ? stat10.mtimeMs : void 0
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 stat10 = await workspace.stat(path4);
2075
- return stat10;
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 stat3, writeFile as writeFile4 } from "fs/promises";
2087
- import { dirname as dirname5, isAbsolute as isAbsolute4, join as join3, relative as relative5, resolve as resolve4 } from "path";
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 stat3(path4);
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(dirname5(projectFile), hash);
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 = resolve4(workspaceRoot);
2197
- const resolved = resolve4(root, rawTarget);
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 (resolve4(source) === resolve4(target)) return true;
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 = dirname5(toPath(spec.projectFile));
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 stat3(full).catch(() => null);
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(dirname5(markerPath), { recursive: true });
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(stat10) {
2487
+ function cloneStat(stat11) {
2413
2488
  return {
2414
- size: stat10.size,
2415
- mtimeMs: stat10.mtimeMs,
2416
- kind: stat10.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: VERCEL_SANDBOX_WORKSPACE_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 '${sandboxPath}'`);
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 '${sandboxPath}'`);
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 stat10 = {
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, stat10);
2690
+ statCache.set(sandboxPath, stat11);
2580
2691
  return {
2581
2692
  content: typeof content === "string" ? content : Buffer.from(content).toString("utf-8"),
2582
- stat: cloneStat(stat10)
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 (remote.fs?.rm) await remote.fs.rm(sandboxPath, { recursive: false, force: false });
2674
- else await runShell(remote, `rm -- ${shellQuote2(sandboxPath)}`);
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
- return `${safeId}-${safeFingerprint}${artifactExtension(kind)}`;
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 dirname6 } from "path";
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 dirname7, join as join6, relative as relative7, sep as sep4 } from "path";
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 dirname7(sourceToPath(spec.projectFile));
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 stat4 } from "fs/promises";
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 stat4(sourcePath);
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 stat5 } from "fs/promises";
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 posix.normalize(parts.filter(Boolean).join("/"));
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 stat5(sourcePath);
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 rm2, stat as stat6, writeFile as writeFile5 } from "fs/promises";
3615
- import { dirname as dirname8, isAbsolute as isAbsolute5, relative as relative8, resolve as resolve5, sep as sep5 } from "path";
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(dirname8(absPath), { recursive: true });
3634
- await assertRealPathWithinWorkspace(root, dirname8(absPath));
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 : resolve5(paths.workspaceRoot, 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 rm2(absTarget, { recursive: true, force: true });
3720
- await mkdir6(dirname8(absTarget), { recursive: true });
3721
- await assertRealPathWithinWorkspace(paths.workspaceRoot, dirname8(absTarget));
3722
- const sourceStat = await stat6(sourcePath);
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 rm2(absPath, { recursive: true, force: true });
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 stat6(sourcePath);
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 workspace = createNodeWorkspace(ctx.workspaceRoot);
3836
- const sandbox = createDirectSandbox();
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 workspace = createNodeWorkspace(ctx.workspaceRoot);
3859
- const sandbox = createBwrapSandbox();
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 stat7 } from "fs/promises";
3872
- import { dirname as dirname9, join as join8 } from "path";
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 toRemotePath(value) {
3918
- if (value === VERCEL_SANDBOX_WORKSPACE_ROOT) return VERCEL_SANDBOX_REMOTE_ROOT;
3919
- if (value.startsWith(`${VERCEL_SANDBOX_WORKSPACE_ROOT}/`)) {
3920
- return `${VERCEL_SANDBOX_REMOTE_ROOT}${value.slice(VERCEL_SANDBOX_WORKSPACE_ROOT.length)}`;
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 value;
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
- if (!env) return void 0;
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(env).map(([key, value]) => [key, replaceWorkspaceAliases(toRemotePath(value))])
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", toRemoteCommand(cmd)],
3997
- cwd: toRemoteCwd(opts?.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((resolve9, reject) => {
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", () => resolve9(Buffer.concat(gzChunks)));
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(dirname9(request.outputPath), { recursive: true });
4395
+ await mkdir9(dirname10(request.outputPath), { recursive: true });
4277
4396
  if (request.kind === "node") {
4278
- const { stdout } = await execFileAsync2("npm", [
4279
- "pack",
4397
+ const { stdout } = await execFileAsync2("pnpm", [
4398
+ "--dir",
4280
4399
  sourcePath,
4400
+ "pack",
4281
4401
  "--pack-destination",
4282
- dirname9(request.outputPath)
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(`npm pack produced no artifact for ${sourcePath}`);
4286
- await rename5(join8(dirname9(request.outputPath), packedName), request.outputPath);
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 stat7(sourcePath);
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
- throw new Error(result.content.map((c) => c.text).join("\n"));
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 rm3,
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 rm3(filepath, { force: true });
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((resolve9) => setTimeout(resolve9, ms));
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" above is the workspace root. Tool path arguments must be relative to it (e.g. `README.md`, `src/foo.ts`).',
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, ctx.workdir);
6024
+ sessionManager = SessionManager.open(savedPiFile, void 0, runtimeCwd);
5870
6025
  } catch {
5871
- sessionManager = SessionManager.create(ctx.workdir);
6026
+ sessionManager = SessionManager.create(runtimeCwd, nativeSessionDir);
5872
6027
  isNewPiSession = true;
5873
6028
  }
5874
6029
  } else {
5875
- sessionManager = SessionManager.create(ctx.workdir);
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
- ctx.workdir,
6046
+ opts.cwd,
5890
6047
  agentDir,
5891
6048
  effectivePackages
5892
6049
  );
5893
6050
  const resourceLoader = new DefaultResourceLoader({
5894
- cwd: ctx.workdir,
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: ctx.workdir,
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: ctx.workdir,
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 sdkChunksForTurn = filterSdkChunksForCurrentSegment(sdkChunks);
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
- if (!sawTextChunk) {
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 (nativeFollowUpPending.has(input.sessionId) && !ctx.abortSignal.aborted) {
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(ctx.workdir, DEFAULT_ATTACHMENT_DIR), { recursive: true });
6372
- await writeFile7(join10(ctx.workdir, relPath), bytes);
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: ctx.workdir, error: msg });
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 stat8, readFile as readFile10 } from "fs/promises";
6462
- import { join as join11, extname as extname3, resolve as resolve6, sep as sep6, relative as relative9 } from "path";
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 dirExists(path4) {
6650
+ async function dirExists2(path4) {
6470
6651
  try {
6471
- const s = await stat8(path4);
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 stat8(path4);
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 dirExists(dir)) return [];
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 dirExists(nodeModulesDir)) return [];
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 = resolve6(pkgDir, main);
6533
- const resolvedPkgDir = resolve6(pkgDir);
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 dirExists(pkgDir)) {
6754
+ if (await dirExists2(pkgDir)) {
6574
6755
  const already = candidates.some((c) => c.path === pkgDir);
6575
6756
  if (!already) {
6576
6757
  candidates.push({ path: pkgDir, source: "npm" });
@@ -6616,10 +6797,15 @@ import {
6616
6797
  createWriteToolDefinition
6617
6798
  } from "@mariozechner/pi-coding-agent";
6618
6799
 
6800
+ // src/server/runtime/mode.ts
6801
+ function getRuntimeBundleStorageRoot(bundle) {
6802
+ return bundle.storageRoot ?? getNodeWorkspaceHostRoot(bundle.workspace) ?? bundle.workspace.root;
6803
+ }
6804
+
6619
6805
  // src/server/tools/operations/bound.ts
6620
6806
  import { constants as constants4 } from "fs";
6621
- import { access as access4, lstat as lstat4, mkdir as mkdir12, readFile as readFile11, readdir as readdir8, readlink, realpath as realpath4, stat as stat9, writeFile as writeFile8 } from "fs/promises";
6622
- import { dirname as dirname10, isAbsolute as isAbsolute6, relative as relative10, resolve as resolve7 } from "path";
6807
+ import { access as access4, lstat as lstat4, mkdir as mkdir12, readFile as readFile11, readdir as readdir8, readlink, realpath as realpath4, stat as stat10, writeFile as writeFile8 } from "fs/promises";
6808
+ import { dirname as dirname11, isAbsolute as isAbsolute7, join as join12, relative as relative10, resolve as resolve8 } from "path";
6623
6809
  function toPosixPath(value) {
6624
6810
  return value.split("\\").join("/");
6625
6811
  }
@@ -6675,7 +6861,7 @@ async function walkMatches(root, current, pattern, ignore, limit, out) {
6675
6861
  const entries = await readdir8(current, { withFileTypes: true });
6676
6862
  for (const entry of entries) {
6677
6863
  if (out.length >= limit) return;
6678
- const absolutePath = resolve7(current, entry.name);
6864
+ const absolutePath = resolve8(current, entry.name);
6679
6865
  const relativePath = toPosixPath(relative10(root, absolutePath));
6680
6866
  if (entry.isDirectory() && shouldSkipDir(relativePath, ignore)) continue;
6681
6867
  if (matchesGlob(relativePath, pattern)) {
@@ -6690,26 +6876,26 @@ async function findNearestExistingAncestor(absPath) {
6690
6876
  let current = absPath;
6691
6877
  for (; ; ) {
6692
6878
  try {
6693
- await stat9(current);
6879
+ await stat10(current);
6694
6880
  return current;
6695
6881
  } catch {
6696
- const parent = dirname10(current);
6882
+ const parent = dirname11(current);
6697
6883
  if (parent === current) return current;
6698
6884
  current = parent;
6699
6885
  }
6700
6886
  }
6701
6887
  }
6702
6888
  async function assertWithinWorkspace(workspaceRoot, absPath) {
6703
- const realRoot = await realpath4(resolve7(workspaceRoot));
6889
+ const realRoot = await realpath4(resolve8(workspaceRoot));
6704
6890
  try {
6705
6891
  const s = await lstat4(absPath);
6706
6892
  if (s.isSymbolicLink()) {
6707
6893
  const target = await readlink(absPath);
6708
- const resolvedTarget = resolve7(dirname10(absPath), target);
6894
+ const resolvedTarget = resolve8(dirname11(absPath), target);
6709
6895
  const nearestAncestor = await findNearestExistingAncestor(resolvedTarget);
6710
6896
  const realAncestor = await realpath4(nearestAncestor);
6711
6897
  const rel2 = relative10(realRoot, realAncestor);
6712
- if (rel2.startsWith("..") || isAbsolute6(rel2)) {
6898
+ if (rel2.startsWith("..") || isAbsolute7(rel2)) {
6713
6899
  throw new Error(`path "${absPath}" is outside workspace`);
6714
6900
  }
6715
6901
  return;
@@ -6727,10 +6913,10 @@ async function assertWithinWorkspace(workspaceRoot, absPath) {
6727
6913
  } catch (err) {
6728
6914
  const code = err.code;
6729
6915
  if (code === "ENOENT") {
6730
- const nearestAncestor = await findNearestExistingAncestor(dirname10(absPath));
6916
+ const nearestAncestor = await findNearestExistingAncestor(dirname11(absPath));
6731
6917
  const realAncestor = await realpath4(nearestAncestor);
6732
6918
  const rel2 = relative10(realRoot, realAncestor);
6733
- if (rel2.startsWith("..") || isAbsolute6(rel2)) {
6919
+ if (rel2.startsWith("..") || isAbsolute7(rel2)) {
6734
6920
  throw new Error(`path "${absPath}" is outside workspace`);
6735
6921
  }
6736
6922
  return;
@@ -6738,51 +6924,77 @@ async function assertWithinWorkspace(workspaceRoot, absPath) {
6738
6924
  throw err;
6739
6925
  }
6740
6926
  const rel = relative10(realRoot, realCandidate);
6741
- if (rel.startsWith("..") || isAbsolute6(rel)) {
6927
+ if (rel.startsWith("..") || isAbsolute7(rel)) {
6742
6928
  throw new Error(`path "${absPath}" is outside workspace`);
6743
6929
  }
6744
6930
  }
6745
- function boundFs(workspaceRoot) {
6931
+ function boundFs(workspaceRoot, opts = {}) {
6932
+ const runtimeRoot = opts.runtimeRoot ? opts.runtimeRoot.replace(/\/+$/, "") || "/" : void 0;
6933
+ const shouldMapRuntimeRoot = Boolean(runtimeRoot && runtimeRoot !== workspaceRoot);
6934
+ const toStoragePath = (absolutePath) => {
6935
+ if (!shouldMapRuntimeRoot || !runtimeRoot) return absolutePath;
6936
+ const normalized = toPosixPath(absolutePath);
6937
+ if (normalized === runtimeRoot) return workspaceRoot;
6938
+ if (normalized.startsWith(`${runtimeRoot}/`)) {
6939
+ return join12(workspaceRoot, ...normalized.slice(runtimeRoot.length + 1).split("/"));
6940
+ }
6941
+ return absolutePath;
6942
+ };
6943
+ const toRuntimePath2 = (absolutePath) => {
6944
+ if (!shouldMapRuntimeRoot || !runtimeRoot) return absolutePath;
6945
+ const rel = relative10(workspaceRoot, absolutePath);
6946
+ if (rel === "") return runtimeRoot;
6947
+ if (rel.startsWith("..") || isAbsolute7(rel)) return absolutePath;
6948
+ return `${runtimeRoot}/${toPosixPath(rel)}`;
6949
+ };
6746
6950
  const read = {
6747
6951
  async readFile(absolutePath) {
6748
- await assertWithinWorkspace(workspaceRoot, absolutePath);
6749
- return await readFile11(absolutePath);
6952
+ const storagePath = toStoragePath(absolutePath);
6953
+ await assertWithinWorkspace(workspaceRoot, storagePath);
6954
+ return await readFile11(storagePath);
6750
6955
  },
6751
6956
  async access(absolutePath) {
6752
- await assertWithinWorkspace(workspaceRoot, absolutePath);
6753
- await access4(absolutePath, constants4.R_OK);
6957
+ const storagePath = toStoragePath(absolutePath);
6958
+ await assertWithinWorkspace(workspaceRoot, storagePath);
6959
+ await access4(storagePath, constants4.R_OK);
6754
6960
  }
6755
6961
  };
6756
6962
  const write = {
6757
6963
  async writeFile(absolutePath, content) {
6758
- await assertWithinWorkspace(workspaceRoot, absolutePath);
6759
- await mkdir12(dirname10(absolutePath), { recursive: true });
6760
- await writeFile8(absolutePath, content);
6964
+ const storagePath = toStoragePath(absolutePath);
6965
+ await assertWithinWorkspace(workspaceRoot, storagePath);
6966
+ await mkdir12(dirname11(storagePath), { recursive: true });
6967
+ await writeFile8(storagePath, content);
6761
6968
  },
6762
6969
  async mkdir(dir) {
6763
- await assertWithinWorkspace(workspaceRoot, dir);
6764
- await mkdir12(dir, { recursive: true });
6970
+ const storagePath = toStoragePath(dir);
6971
+ await assertWithinWorkspace(workspaceRoot, storagePath);
6972
+ await mkdir12(storagePath, { recursive: true });
6765
6973
  }
6766
6974
  };
6767
6975
  const edit = {
6768
6976
  async readFile(absolutePath) {
6769
- await assertWithinWorkspace(workspaceRoot, absolutePath);
6770
- return await readFile11(absolutePath);
6977
+ const storagePath = toStoragePath(absolutePath);
6978
+ await assertWithinWorkspace(workspaceRoot, storagePath);
6979
+ return await readFile11(storagePath);
6771
6980
  },
6772
6981
  async writeFile(absolutePath, content) {
6773
- await assertWithinWorkspace(workspaceRoot, absolutePath);
6774
- await writeFile8(absolutePath, content);
6982
+ const storagePath = toStoragePath(absolutePath);
6983
+ await assertWithinWorkspace(workspaceRoot, storagePath);
6984
+ await writeFile8(storagePath, content);
6775
6985
  },
6776
6986
  async access(absolutePath) {
6777
- await assertWithinWorkspace(workspaceRoot, absolutePath);
6778
- await access4(absolutePath, constants4.R_OK | constants4.W_OK);
6987
+ const storagePath = toStoragePath(absolutePath);
6988
+ await assertWithinWorkspace(workspaceRoot, storagePath);
6989
+ await access4(storagePath, constants4.R_OK | constants4.W_OK);
6779
6990
  }
6780
6991
  };
6781
6992
  const find = {
6782
6993
  async exists(absolutePath) {
6783
- await assertWithinWorkspace(workspaceRoot, absolutePath);
6994
+ const storagePath = toStoragePath(absolutePath);
6995
+ await assertWithinWorkspace(workspaceRoot, storagePath);
6784
6996
  try {
6785
- await stat9(absolutePath);
6997
+ await stat10(storagePath);
6786
6998
  return true;
6787
6999
  } catch (err) {
6788
7000
  if (err.code === "ENOENT") return false;
@@ -6790,54 +7002,61 @@ function boundFs(workspaceRoot) {
6790
7002
  }
6791
7003
  },
6792
7004
  async glob(pattern, cwd, options) {
6793
- await assertWithinWorkspace(workspaceRoot, cwd);
7005
+ const storageCwd = toStoragePath(cwd);
7006
+ await assertWithinWorkspace(workspaceRoot, storageCwd);
6794
7007
  const matches = [];
6795
- await walkMatches(cwd, cwd, pattern, options.ignore, options.limit, matches);
6796
- return matches;
7008
+ await walkMatches(storageCwd, storageCwd, pattern, options.ignore, options.limit, matches);
7009
+ return matches.map(toRuntimePath2);
6797
7010
  }
6798
7011
  };
6799
7012
  const grep = {
6800
7013
  async isDirectory(absolutePath) {
6801
- await assertWithinWorkspace(workspaceRoot, absolutePath);
6802
- return (await stat9(absolutePath)).isDirectory();
7014
+ const storagePath = toStoragePath(absolutePath);
7015
+ await assertWithinWorkspace(workspaceRoot, storagePath);
7016
+ return (await stat10(storagePath)).isDirectory();
6803
7017
  },
6804
7018
  async readFile(absolutePath) {
6805
- await assertWithinWorkspace(workspaceRoot, absolutePath);
6806
- return await readFile11(absolutePath, "utf8");
7019
+ const storagePath = toStoragePath(absolutePath);
7020
+ await assertWithinWorkspace(workspaceRoot, storagePath);
7021
+ return await readFile11(storagePath, "utf8");
6807
7022
  }
6808
7023
  };
6809
7024
  const ls = {
6810
7025
  async exists(absolutePath) {
7026
+ const storagePath = toStoragePath(absolutePath);
6811
7027
  try {
6812
- await assertWithinWorkspace(workspaceRoot, absolutePath);
6813
- await stat9(absolutePath);
7028
+ await assertWithinWorkspace(workspaceRoot, storagePath);
7029
+ await stat10(storagePath);
6814
7030
  return true;
6815
7031
  } catch {
6816
7032
  return false;
6817
7033
  }
6818
7034
  },
6819
7035
  async stat(absolutePath) {
6820
- await assertWithinWorkspace(workspaceRoot, absolutePath);
6821
- return await stat9(absolutePath);
7036
+ const storagePath = toStoragePath(absolutePath);
7037
+ await assertWithinWorkspace(workspaceRoot, storagePath);
7038
+ return await stat10(storagePath);
6822
7039
  },
6823
7040
  async readdir(absolutePath) {
6824
- await assertWithinWorkspace(workspaceRoot, absolutePath);
6825
- return await readdir8(absolutePath);
7041
+ const storagePath = toStoragePath(absolutePath);
7042
+ await assertWithinWorkspace(workspaceRoot, storagePath);
7043
+ return await readdir8(storagePath);
6826
7044
  }
6827
7045
  };
6828
7046
  return { read, write, edit, find, grep, ls };
6829
7047
  }
6830
7048
 
6831
7049
  // src/server/tools/operations/vercel.ts
6832
- import { isAbsolute as isAbsolute7, relative as relative11 } from "path";
7050
+ import { isAbsolute as isAbsolute8, relative as relative11 } from "path";
7051
+ var VERCEL_SANDBOX_LEGACY_ROOT = "/vercel/sandbox";
6833
7052
  function rootAliases(workspace) {
6834
7053
  const aliases = [workspace.root];
6835
- if (workspace.root === VERCEL_SANDBOX_WORKSPACE_ROOT) aliases.push(VERCEL_SANDBOX_REMOTE_ROOT);
6836
- if (workspace.root === VERCEL_SANDBOX_REMOTE_ROOT) aliases.push(VERCEL_SANDBOX_WORKSPACE_ROOT);
6837
- return aliases;
7054
+ if (workspace.root === VERCEL_SANDBOX_WORKSPACE_ROOT) aliases.push(VERCEL_SANDBOX_LEGACY_ROOT);
7055
+ if (workspace.root === VERCEL_SANDBOX_LEGACY_ROOT) aliases.push(VERCEL_SANDBOX_WORKSPACE_ROOT);
7056
+ return Array.from(new Set(aliases));
6838
7057
  }
6839
7058
  function isOutsideWorkspaceRel(rel) {
6840
- return rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isAbsolute7(rel);
7059
+ return rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isAbsolute8(rel);
6841
7060
  }
6842
7061
  function toRelPath(workspace, absolutePath) {
6843
7062
  for (const root of rootAliases(workspace)) {
@@ -6854,7 +7073,7 @@ function toRelPath(workspace, absolutePath) {
6854
7073
  return `.agents/skills/${skillPath}`;
6855
7074
  }
6856
7075
  throw new Error(
6857
- `path "${absolutePath}" is outside workspace; use a path relative to the workspace root or under ${rootAliases(workspace).join(" / ")}`
7076
+ `path "${absolutePath}" is outside workspace; use a path relative to the workspace root or under ${workspace.root}`
6858
7077
  );
6859
7078
  }
6860
7079
  function vercelBashOps(sandbox, opts = {}) {
@@ -6915,13 +7134,23 @@ function vercelEditOps(workspace) {
6915
7134
  }
6916
7135
  };
6917
7136
  }
6918
- function toRemotePath2(value) {
6919
- if (value === VERCEL_SANDBOX_WORKSPACE_ROOT) return VERCEL_SANDBOX_REMOTE_ROOT;
6920
- if (value.startsWith(`${VERCEL_SANDBOX_WORKSPACE_ROOT}/`)) {
6921
- return `${VERCEL_SANDBOX_REMOTE_ROOT}${value.slice(VERCEL_SANDBOX_WORKSPACE_ROOT.length)}`;
7137
+ function toRemotePath(value) {
7138
+ if (value === VERCEL_SANDBOX_LEGACY_ROOT) return VERCEL_SANDBOX_REMOTE_ROOT;
7139
+ if (value.startsWith(`${VERCEL_SANDBOX_LEGACY_ROOT}/`)) {
7140
+ return `${VERCEL_SANDBOX_REMOTE_ROOT}${value.slice(VERCEL_SANDBOX_LEGACY_ROOT.length)}`;
7141
+ }
7142
+ return value;
7143
+ }
7144
+ function toRuntimePath(value) {
7145
+ if (value === VERCEL_SANDBOX_LEGACY_ROOT) return VERCEL_SANDBOX_WORKSPACE_ROOT;
7146
+ if (value.startsWith(`${VERCEL_SANDBOX_LEGACY_ROOT}/`)) {
7147
+ return `${VERCEL_SANDBOX_WORKSPACE_ROOT}${value.slice(VERCEL_SANDBOX_LEGACY_ROOT.length)}`;
6922
7148
  }
6923
7149
  return value;
6924
7150
  }
7151
+ function sanitizeRuntimeText(value) {
7152
+ return value.replaceAll(VERCEL_SANDBOX_LEGACY_ROOT, VERCEL_SANDBOX_WORKSPACE_ROOT);
7153
+ }
6925
7154
  function findPredicate(pattern) {
6926
7155
  const isPathShaped = pattern.includes("/") || pattern.includes("**");
6927
7156
  if (!isPathShaped) return `-name ${shellEscape(pattern)}`;
@@ -6966,7 +7195,7 @@ function vercelFindOps(sandbox, workspace) {
6966
7195
  return result.exitCode === 0;
6967
7196
  },
6968
7197
  async glob(pattern, cwd, options) {
6969
- const remoteCwd = toRemotePath2(cwd);
7198
+ const remoteCwd = toRemotePath(cwd);
6970
7199
  const args = ["fd", "--glob", "--no-require-git", "--max-results", String(options.limit)];
6971
7200
  for (const ig of options.ignore) {
6972
7201
  args.push("--exclude", ig);
@@ -6983,11 +7212,11 @@ function vercelFindOps(sandbox, workspace) {
6983
7212
  });
6984
7213
  }
6985
7214
  if (result.exitCode !== 0 && result.exitCode !== 1) {
6986
- const stderr = Buffer.from(result.stderr).toString("utf-8").trim();
7215
+ const stderr = sanitizeRuntimeText(Buffer.from(result.stderr).toString("utf-8").trim());
6987
7216
  throw new Error(`file search failed (exit ${result.exitCode}): ${stderr}`);
6988
7217
  }
6989
7218
  const stdout = Buffer.from(result.stdout).toString("utf-8");
6990
- return stdout.split("\n").filter(Boolean);
7219
+ return stdout.split("\n").filter(Boolean).map(toRuntimePath);
6991
7220
  }
6992
7221
  };
6993
7222
  }
@@ -7025,7 +7254,7 @@ import {
7025
7254
  truncateHead,
7026
7255
  truncateLine
7027
7256
  } from "@mariozechner/pi-coding-agent";
7028
- import { resolve as resolve8, relative as relative12 } from "path";
7257
+ import { resolve as resolve9, relative as relative12 } from "path";
7029
7258
 
7030
7259
  // src/server/catalog/tools/_shared.ts
7031
7260
  function makeError(message) {
@@ -7177,7 +7406,7 @@ function vercelGrepTool(sandbox, workspaceRoot) {
7177
7406
  return makeError("pattern is required");
7178
7407
  }
7179
7408
  if (workspaceRoot && typeof params.path === "string" && params.path.length > 0) {
7180
- const resolved = resolve8(workspaceRoot, params.path);
7409
+ const resolved = resolve9(workspaceRoot, params.path);
7181
7410
  const rel = relative12(workspaceRoot, resolved);
7182
7411
  if (rel.startsWith("..") || rel.startsWith("/")) {
7183
7412
  return makeError(`path "${params.path}" is outside workspace`);
@@ -7212,6 +7441,7 @@ function isTextContent(content) {
7212
7441
  function adaptPiTool(piTool) {
7213
7442
  return {
7214
7443
  name: piTool.name,
7444
+ readinessRequirements: ["workspace-fs"],
7215
7445
  description: piTool.description,
7216
7446
  promptSnippet: piTool.promptSnippet,
7217
7447
  parameters: piTool.parameters,
@@ -7237,17 +7467,18 @@ function adaptPiTool(piTool) {
7237
7467
  }
7238
7468
  function buildFilesystemAgentTools(bundle) {
7239
7469
  const cwd = bundle.workspace.root;
7470
+ const storageRoot = getRuntimeBundleStorageRoot(bundle);
7240
7471
  if (bundle.sandbox.provider === "vercel-sandbox") {
7241
7472
  return [
7242
7473
  adaptPiTool(createReadToolDefinition(cwd, { operations: vercelReadOps(bundle.workspace) })),
7243
7474
  adaptPiTool(createWriteToolDefinition(cwd, { operations: vercelWriteOps(bundle.workspace) })),
7244
7475
  adaptPiTool(createEditToolDefinition(cwd, { operations: vercelEditOps(bundle.workspace) })),
7245
7476
  adaptPiTool(createFindToolDefinition(cwd, { operations: vercelFindOps(bundle.sandbox, bundle.workspace) })),
7246
- vercelGrepTool(bundle.sandbox, cwd),
7477
+ { ...vercelGrepTool(bundle.sandbox, cwd), readinessRequirements: ["workspace-fs"] },
7247
7478
  adaptPiTool(createLsToolDefinition(cwd, { operations: vercelLsOps(bundle.workspace) }))
7248
7479
  ];
7249
7480
  }
7250
- const ops = boundFs(cwd);
7481
+ const ops = boundFs(storageRoot, { runtimeRoot: cwd });
7251
7482
  return [
7252
7483
  adaptPiTool(createReadToolDefinition(cwd, { operations: ops.read })),
7253
7484
  adaptPiTool(createWriteToolDefinition(cwd, { operations: ops.write })),
@@ -7703,7 +7934,8 @@ function treeRoutes(app, opts, done) {
7703
7934
  return reply.code(statusCode).send({
7704
7935
  error: {
7705
7936
  code: typeof stableCode === "string" ? stableCode : ERROR_CODE_INTERNAL,
7706
- message
7937
+ message,
7938
+ details: err?.details
7707
7939
  }
7708
7940
  });
7709
7941
  }
@@ -8118,6 +8350,17 @@ function chatRoutes(app, opts, done) {
8118
8350
  });
8119
8351
  buf.markComplete(() => buffers.evict(sessionId, turnId));
8120
8352
  if (streamStarted) return;
8353
+ const statusCode = err?.statusCode;
8354
+ const stableCode = err?.code;
8355
+ if (typeof statusCode === "number" && statusCode >= 400 && statusCode < 600) {
8356
+ return reply.code(statusCode).send({
8357
+ error: {
8358
+ code: typeof stableCode === "string" ? stableCode : ERROR_CODE_INTERNAL,
8359
+ message: err instanceof Error ? err.message : "chat route failed",
8360
+ details: err?.details
8361
+ }
8362
+ });
8363
+ }
8121
8364
  return reply.code(500).send({
8122
8365
  error: { code: ERROR_CODE_INTERNAL, message: "internal error" }
8123
8366
  });
@@ -8160,17 +8403,17 @@ function chatRoutes(app, opts, done) {
8160
8403
  const replayed = buf.replay(cursor);
8161
8404
  for (const e of replayed) writer.write(e.chunk);
8162
8405
  if (buf.complete) return;
8163
- await new Promise((resolve9) => {
8406
+ await new Promise((resolve10) => {
8164
8407
  const unsub = buf.subscribe(
8165
8408
  (e) => writer.write(e.chunk),
8166
8409
  () => {
8167
8410
  unsub();
8168
- resolve9();
8411
+ resolve10();
8169
8412
  }
8170
8413
  );
8171
8414
  request.raw.on("close", () => {
8172
8415
  unsub();
8173
- resolve9();
8416
+ resolve10();
8174
8417
  });
8175
8418
  });
8176
8419
  }
@@ -8510,6 +8753,18 @@ function isNotFoundError(err) {
8510
8753
  return err instanceof SessionNotFoundError || err instanceof Error && /not found/i.test(err.message);
8511
8754
  }
8512
8755
  function classifySessionError(err, reply) {
8756
+ const statusCode = err?.statusCode;
8757
+ const stableCode = err?.code;
8758
+ if (typeof statusCode === "number" && statusCode >= 400 && statusCode < 600) {
8759
+ const message2 = err instanceof Error ? err.message : "session route failed";
8760
+ return reply.code(statusCode).send({
8761
+ error: {
8762
+ code: typeof stableCode === "string" ? stableCode : ERROR_CODE_INTERNAL,
8763
+ message: message2,
8764
+ details: err?.details
8765
+ }
8766
+ });
8767
+ }
8513
8768
  if (isNotFoundError(err)) {
8514
8769
  return reply.code(404).send({
8515
8770
  error: {
@@ -8908,8 +9163,9 @@ function catalogRoutes(app, opts, done) {
8908
9163
 
8909
9164
  // src/server/http/routes/readyStatus.ts
8910
9165
  function readyStatusRoutes(app, opts, done) {
8911
- const { tracker } = opts;
8912
9166
  app.get("/api/v1/ready-status", async (request, reply) => {
9167
+ const tracker = opts.getTracker ? await opts.getTracker(request) : opts.tracker;
9168
+ if (!tracker) throw new Error("ready-status route requires tracker or getTracker");
8913
9169
  reply.raw.writeHead(200, {
8914
9170
  "Content-Type": "text/event-stream",
8915
9171
  "Cache-Control": "no-cache",
@@ -8930,7 +9186,7 @@ function readyStatusRoutes(app, opts, done) {
8930
9186
  data: ${JSON.stringify(event)}
8931
9187
 
8932
9188
  `);
8933
- if (event.state === "ready") closeStream();
9189
+ if (event.state === "ready" || event.state === "degraded") closeStream();
8934
9190
  });
8935
9191
  request.raw.on("close", closeStream);
8936
9192
  reply.hijack();
@@ -9009,6 +9265,18 @@ function searchRoutes(app, opts, done) {
9009
9265
  const results = await fileSearch.search(q, limit);
9010
9266
  return reply.send({ results });
9011
9267
  } catch (err) {
9268
+ const statusCode = err?.statusCode;
9269
+ const stableCode = err?.code;
9270
+ if (typeof statusCode === "number" && statusCode >= 400 && statusCode < 600) {
9271
+ const message = err instanceof Error ? err.message : "search failed";
9272
+ return reply.code(statusCode).send({
9273
+ error: {
9274
+ code: typeof stableCode === "string" ? stableCode : ERROR_CODE_INTERNAL,
9275
+ message,
9276
+ details: err?.details
9277
+ }
9278
+ });
9279
+ }
9012
9280
  request.log.error({ err }, "[search] error");
9013
9281
  return reply.code(500).send({
9014
9282
  error: { code: ERROR_CODE_INTERNAL, message: "search failed" }
@@ -9227,6 +9495,36 @@ function applyCspHeaders(response, opts = {}) {
9227
9495
  import { basename as basename3 } from "path";
9228
9496
  import { AuthStorage as AuthStorage3, ModelRegistry as ModelRegistry3 } from "@mariozechner/pi-coding-agent";
9229
9497
 
9498
+ // src/server/catalog/toolReadiness.ts
9499
+ var WORKSPACE_PREPARING_MESSAGE = "Workspace is still preparing. Try again in a moment.";
9500
+ function workspaceNotReadyToolResult(requirement) {
9501
+ return {
9502
+ content: [{ type: "text", text: WORKSPACE_PREPARING_MESSAGE }],
9503
+ isError: true,
9504
+ details: {
9505
+ code: ErrorCode.enum.WORKSPACE_NOT_READY,
9506
+ retryable: true,
9507
+ requirement
9508
+ }
9509
+ };
9510
+ }
9511
+ function withReadinessRequirements(tool, readinessRequirements) {
9512
+ if (tool.readinessRequirements === readinessRequirements) return tool;
9513
+ return { ...tool, readinessRequirements };
9514
+ }
9515
+ function wrapToolForReadiness(tool, checkReadiness) {
9516
+ if (!checkReadiness || !tool.readinessRequirements || tool.readinessRequirements.length === 0) return tool;
9517
+ return {
9518
+ ...tool,
9519
+ async execute(params, ctx) {
9520
+ for (const requirement of tool.readinessRequirements ?? []) {
9521
+ if (!checkReadiness(requirement, tool)) return workspaceNotReadyToolResult(requirement);
9522
+ }
9523
+ return await tool.execute(params, ctx);
9524
+ }
9525
+ };
9526
+ }
9527
+
9230
9528
  // src/server/catalog/mergeTools.ts
9231
9529
  function setLastRegistered(merged, tool) {
9232
9530
  merged.delete(tool.name);
@@ -9252,15 +9550,16 @@ function mergeTools(options) {
9252
9550
  `[catalog] Tool "${tool.name}" overridden by plugin ${plugin.pluginName}`
9253
9551
  );
9254
9552
  }
9255
- setLastRegistered(merged, tool);
9553
+ const pluginTool = tool.readinessRequirements === void 0 ? withReadinessRequirements(tool, ["workspace-fs"]) : tool;
9554
+ setLastRegistered(merged, pluginTool);
9256
9555
  }
9257
9556
  }
9258
- return [...merged.values()];
9557
+ return [...merged.values()].map((tool) => wrapToolForReadiness(tool, options.checkReadiness));
9259
9558
  }
9260
9559
 
9261
9560
  // src/server/tools/upload/index.ts
9262
9561
  import { readFile as readFile12 } from "fs/promises";
9263
- import { extname as extname4, join as join12 } from "path";
9562
+ import { extname as extname4, join as join13 } from "path";
9264
9563
  var DEFAULT_UPLOAD_DIR = "assets/images";
9265
9564
  var MAX_UPLOAD_BYTES2 = 10 * 1024 * 1024;
9266
9565
  function contentTypeFromExt(path4) {
@@ -9300,10 +9599,11 @@ function basenameForUpload2(filename) {
9300
9599
  }
9301
9600
  function buildUploadAgentTools(bundle) {
9302
9601
  const { workspace } = bundle;
9303
- const cwd = workspace.root;
9602
+ const storageRoot = getRuntimeBundleStorageRoot(bundle);
9304
9603
  return [
9305
9604
  {
9306
9605
  name: "upload_file",
9606
+ readinessRequirements: ["workspace-fs"],
9307
9607
  description: "Copy a workspace file into artifact storage (assets/images by default) and return its workspace-relative path. Use this when you want to embed an image in markdown or make a generated file accessible as a stable artifact. The returned path can be used directly in markdown image syntax: ![](returned-path)",
9308
9608
  parameters: {
9309
9609
  type: "object",
@@ -9328,7 +9628,7 @@ function buildUploadAgentTools(bundle) {
9328
9628
  const rawDir = typeof input.directory === "string" ? input.directory.trim() : "";
9329
9629
  const dir = rawDir ? rawDir.replace(/^\.\/+/, "").replace(/\/+$/, "") : DEFAULT_UPLOAD_DIR;
9330
9630
  try {
9331
- const bytes = await readFile12(join12(cwd, filePath));
9631
+ const bytes = workspace.readBinaryFile ? Buffer.from(await workspace.readBinaryFile(filePath)) : await readFile12(join13(storageRoot, filePath));
9332
9632
  if (bytes.byteLength === 0 || bytes.byteLength > MAX_UPLOAD_BYTES2) {
9333
9633
  return {
9334
9634
  content: [{ type: "text", text: `file must be between 1 byte and ${MAX_UPLOAD_BYTES2} bytes` }],
@@ -9396,9 +9696,10 @@ function selectRuntimeModeAdapter(mode, sandboxHandleStore) {
9396
9696
  function getRequestWorkspaceId(request) {
9397
9697
  return request.workspaceContext?.workspaceId ?? DEFAULT_WORKSPACE_ID3;
9398
9698
  }
9399
- function isWorkspaceAgnosticAgentRequest(request) {
9699
+ function isWorkspaceAgnosticAgentRequest(request, options) {
9400
9700
  const pathname = request.url.split("?")[0] ?? request.url;
9401
- return pathname === "/health" || pathname === "/ready" || pathname === "/api/v1/agent/models" || pathname === "/api/v1/ready-status";
9701
+ if (pathname === "/api/v1/ready-status") return !options?.readyStatusWorkspaceScoped;
9702
+ return pathname === "/health" || pathname === "/ready" || pathname === "/api/v1/agent/models";
9402
9703
  }
9403
9704
  function extractHttpStatus2(error) {
9404
9705
  const statusCode = error?.statusCode;
@@ -9421,6 +9722,34 @@ function isExpiredSandboxRuntimeError(error) {
9421
9722
  const message = error instanceof Error ? error.message : String(error);
9422
9723
  return /status code (404|410) is not ok/i.test(message);
9423
9724
  }
9725
+ function createHttpError(code, message, details = {}) {
9726
+ const error = new Error(message);
9727
+ error.code = code;
9728
+ error.statusCode = 503;
9729
+ error.details = details;
9730
+ return error;
9731
+ }
9732
+ function createAgentRuntimeNotReadyError(workspaceId) {
9733
+ return createHttpError(
9734
+ ErrorCode.enum.AGENT_RUNTIME_NOT_READY,
9735
+ "Agent runtime is still preparing. Try again in a moment.",
9736
+ { workspaceId, retryable: true }
9737
+ );
9738
+ }
9739
+ function createRuntimeProvisioningFailedError(workspaceId, cause) {
9740
+ const causeCode = cause?.code;
9741
+ const details = cause?.details;
9742
+ return createHttpError(
9743
+ ErrorCode.enum.RUNTIME_PROVISIONING_FAILED,
9744
+ "Agent runtime provisioning failed. Reload the workspace and try again.",
9745
+ {
9746
+ workspaceId,
9747
+ retryable: true,
9748
+ ...typeof causeCode === "string" ? { causeCode } : {},
9749
+ ...details && typeof details === "object" ? { causeDetails: details } : {}
9750
+ }
9751
+ );
9752
+ }
9424
9753
  var registerAgentRoutes = async (app, opts) => {
9425
9754
  const workspaceRoot = opts.workspaceRoot ?? process.cwd();
9426
9755
  const sessionId = opts.sessionId ?? DEFAULT_WORKSPACE_ID3;
@@ -9578,24 +9907,53 @@ var registerAgentRoutes = async (app, opts) => {
9578
9907
  readyTracker
9579
9908
  };
9580
9909
  }
9581
- async function getOrCreateRuntimeBinding(workspaceId, request) {
9910
+ function createRuntimeBindingEntry(workspaceId, scope, request) {
9911
+ const entry = {
9912
+ state: "pending",
9913
+ promise: Promise.resolve(null)
9914
+ };
9915
+ entry.promise = createRuntimeBinding(workspaceId, scope, request).then(
9916
+ (binding) => {
9917
+ entry.state = "ready";
9918
+ return binding;
9919
+ },
9920
+ (error) => {
9921
+ entry.state = "failed";
9922
+ entry.error = error;
9923
+ throw error;
9924
+ }
9925
+ );
9926
+ entry.promise.catch(() => {
9927
+ });
9928
+ return entry;
9929
+ }
9930
+ async function getOrCreateRuntimeBinding(workspaceId, request, options = {}) {
9582
9931
  const scope = await resolveRuntimeScope(workspaceId, request);
9583
9932
  const existing = runtimeBindings.get(scope.key);
9584
9933
  if (existing) {
9934
+ if (options.failIfPending && existing.state === "pending") {
9935
+ throw createAgentRuntimeNotReadyError(workspaceId);
9936
+ }
9937
+ if (options.failIfPending && existing.state === "failed") {
9938
+ throw createRuntimeProvisioningFailedError(workspaceId, existing.error);
9939
+ }
9585
9940
  return await ensureRuntimeBindingReady(
9586
9941
  workspaceId,
9587
9942
  scope,
9588
- await existing
9943
+ await existing.promise
9589
9944
  );
9590
9945
  }
9591
- const created = createRuntimeBinding(workspaceId, scope, request);
9946
+ const created = createRuntimeBindingEntry(workspaceId, scope, request);
9592
9947
  runtimeBindings.set(scope.key, created);
9593
9948
  evictRuntimeBindings();
9949
+ if (options.failIfPending) {
9950
+ throw createAgentRuntimeNotReadyError(workspaceId);
9951
+ }
9594
9952
  try {
9595
9953
  return await ensureRuntimeBindingReady(
9596
9954
  workspaceId,
9597
9955
  scope,
9598
- await created
9956
+ await created.promise
9599
9957
  );
9600
9958
  } catch (error) {
9601
9959
  if (runtimeBindings.get(scope.key) === created) runtimeBindings.delete(scope.key);
@@ -9605,11 +9963,11 @@ var registerAgentRoutes = async (app, opts) => {
9605
9963
  async function recreateRuntimeBinding(workspaceId, scope) {
9606
9964
  runtimeBindings.delete(scope.key);
9607
9965
  evictSandboxHandleCacheForWorkspace(workspaceId);
9608
- const created = createRuntimeBinding(workspaceId, scope);
9966
+ const created = createRuntimeBindingEntry(workspaceId, scope);
9609
9967
  runtimeBindings.set(scope.key, created);
9610
9968
  evictRuntimeBindings();
9611
9969
  try {
9612
- const binding = await created;
9970
+ const binding = await created.promise;
9613
9971
  binding.lastHealthCheckMs = Date.now();
9614
9972
  return binding;
9615
9973
  } catch (error) {
@@ -9647,9 +10005,9 @@ var registerAgentRoutes = async (app, opts) => {
9647
10005
  }
9648
10006
  return promise;
9649
10007
  }
9650
- async function getBindingForRequest(request) {
10008
+ async function getBindingForRequest(request, options = {}) {
9651
10009
  if (staticBinding) return staticBinding;
9652
- return await getOrCreateRuntimeBinding(getRequestWorkspaceId(request), request);
10010
+ return await getOrCreateRuntimeBinding(getRequestWorkspaceId(request), request, options);
9653
10011
  }
9654
10012
  const agentToolNames = staticBinding ? staticBinding.tools.map((tool) => tool.name) : [
9655
10013
  ...STANDARD_AGENT_TOOL_NAMES,
@@ -9671,7 +10029,7 @@ var registerAgentRoutes = async (app, opts) => {
9671
10029
  app.addHook("onRequest", async (request, reply) => {
9672
10030
  const user = request.user;
9673
10031
  let workspaceId = DEFAULT_WORKSPACE_ID3;
9674
- if (opts.getWorkspaceId && !isWorkspaceAgnosticAgentRequest(request)) {
10032
+ if (opts.getWorkspaceId && !isWorkspaceAgnosticAgentRequest(request, { readyStatusWorkspaceScoped: requestScopedRuntime })) {
9675
10033
  try {
9676
10034
  workspaceId = (await opts.getWorkspaceId(request)).trim();
9677
10035
  } catch (error) {
@@ -9719,7 +10077,7 @@ var registerAgentRoutes = async (app, opts) => {
9719
10077
  });
9720
10078
  await app.register(chatRoutes, {
9721
10079
  getRuntime: async (request) => {
9722
- const binding = await getBindingForRequest(request);
10080
+ const binding = await getBindingForRequest(request, { failIfPending: hasRuntimeProvisioningInput });
9723
10081
  return {
9724
10082
  harness: binding.harness,
9725
10083
  workdir: binding.runtimeBundle.workspace.root
@@ -9800,12 +10158,10 @@ var registerAgentRoutes = async (app, opts) => {
9800
10158
  catalogRoutes,
9801
10159
  staticBinding ? { tools: staticBinding.tools } : { getTools: async (request) => (await getBindingForRequest(request)).tools }
9802
10160
  );
9803
- await app.register(readyStatusRoutes, {
9804
- tracker: staticBinding?.readyTracker ?? new ReadyStatusTracker({
9805
- sandboxReady: true,
9806
- harnessReady: true
9807
- })
9808
- });
10161
+ await app.register(
10162
+ readyStatusRoutes,
10163
+ staticBinding ? { tracker: staticBinding.readyTracker } : { getTracker: async (request) => (await getBindingForRequest(request)).readyTracker }
10164
+ );
9809
10165
  };
9810
10166
  export {
9811
10167
  FileHandleStore,