@hachej/boring-agent 0.1.18 → 0.1.22

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,14 +1,13 @@
1
1
  import {
2
2
  ErrorCode,
3
+ noopTelemetry,
4
+ safeCapture,
3
5
  validateTool
4
- } from "../chunk-W73RUHY3.js";
6
+ } from "../chunk-6R7O6BHW.js";
5
7
 
6
8
  // src/server/sandbox/direct/createDirectSandbox.ts
7
9
  import { spawn } from "child_process";
8
10
 
9
- // src/server/sandbox/workspacePythonEnv.ts
10
- import { join } from "path";
11
-
12
11
  // src/server/config/env.ts
13
12
  function getEnv(name) {
14
13
  return process.env[name];
@@ -17,21 +16,72 @@ function getEnvSnapshot() {
17
16
  return { ...process.env };
18
17
  }
19
18
 
19
+ // src/server/workspace/runtimeLayout.ts
20
+ import { join, resolve } from "path";
21
+ var BORING_AGENT_DIR = ".boring-agent";
22
+ var BORING_AGENT_GITIGNORE_CONTENT = "*\n";
23
+ var BORING_AGENT_RUNTIME_DIR_NAMES = [
24
+ "node",
25
+ "venv",
26
+ "sdk",
27
+ "skills",
28
+ "cache",
29
+ "tmp"
30
+ ];
31
+ function getBoringAgentRuntimePaths(runtimeWorkspaceRoot) {
32
+ const workspaceRoot = resolve(runtimeWorkspaceRoot);
33
+ const agentDir = join(workspaceRoot, BORING_AGENT_DIR);
34
+ const node = join(agentDir, "node");
35
+ const venv = join(agentDir, "venv");
36
+ const sdk = join(agentDir, "sdk");
37
+ const uvHome = join(sdk, "uv");
38
+ const cache = join(agentDir, "cache");
39
+ return {
40
+ workspaceRoot,
41
+ agentDir,
42
+ node,
43
+ nodeModules: join(node, "node_modules"),
44
+ nodeBin: join(node, "node_modules", ".bin"),
45
+ venv,
46
+ venvBin: join(venv, "bin"),
47
+ venvPython: join(venv, "bin", "python"),
48
+ sdk,
49
+ uvHome,
50
+ uvBin: join(uvHome, "bin"),
51
+ skills: join(agentDir, "skills"),
52
+ cache,
53
+ nodeCache: join(cache, "npm"),
54
+ uvCache: join(cache, "uv"),
55
+ pipCache: join(cache, "pip"),
56
+ tmp: join(agentDir, "tmp")
57
+ };
58
+ }
59
+ function getBoringAgentPathEntries(paths) {
60
+ return [paths.nodeBin, paths.venvBin, paths.uvBin];
61
+ }
62
+ function getBoringAgentRuntimeEnv(paths, adapterCacheRoot = paths.cache) {
63
+ return {
64
+ BORING_AGENT_WORKSPACE_ROOT: paths.workspaceRoot,
65
+ VIRTUAL_ENV: paths.venv,
66
+ UV_CACHE_DIR: join(adapterCacheRoot, "uv"),
67
+ PIP_CACHE_DIR: join(adapterCacheRoot, "pip"),
68
+ npm_config_cache: join(adapterCacheRoot, "npm")
69
+ };
70
+ }
71
+
20
72
  // src/server/sandbox/workspacePythonEnv.ts
21
73
  function withWorkspacePythonEnv(opts) {
22
74
  const { workspaceRoot, env, sandboxRoot } = opts;
23
75
  const runtimeRoot = sandboxRoot ?? workspaceRoot;
24
- const venvRoot = join(runtimeRoot, ".venv");
25
- const venvBin = join(venvRoot, "bin");
26
- const shimBin = join(runtimeRoot, ".boring-agent", "bin");
76
+ const paths = getBoringAgentRuntimePaths(runtimeRoot);
27
77
  const baseEnv = env ?? getEnvSnapshot();
28
- const pathParts = [shimBin, venvBin];
78
+ const pathParts = getBoringAgentPathEntries(paths);
29
79
  const existingPath = baseEnv.PATH;
30
80
  if (existingPath) pathParts.push(existingPath);
31
81
  return {
32
82
  ...baseEnv,
33
83
  PATH: pathParts.join(":"),
34
- VIRTUAL_ENV: baseEnv.VIRTUAL_ENV ?? venvRoot,
84
+ VIRTUAL_ENV: baseEnv.VIRTUAL_ENV ?? paths.venv,
35
85
  BORING_AGENT_WORKSPACE_ROOT: baseEnv.BORING_AGENT_WORKSPACE_ROOT ?? runtimeRoot
36
86
  };
37
87
  }
@@ -92,7 +142,7 @@ function createDirectSandbox() {
92
142
  const maxOutputBytes = opts?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
93
143
  const workspaceRoot = workspace.root;
94
144
  const cwd = opts?.cwd ?? workspaceRoot;
95
- return await new Promise((resolve7, reject) => {
145
+ return await new Promise((resolve9, reject) => {
96
146
  const child = spawn(cmd, {
97
147
  cwd,
98
148
  env: withWorkspacePythonEnv({ workspaceRoot, env: opts?.env }),
@@ -121,7 +171,7 @@ function createDirectSandbox() {
121
171
  if (settled) return;
122
172
  settled = true;
123
173
  cleanup();
124
- resolve7({
174
+ resolve9({
125
175
  stdout: new Uint8Array(Buffer.concat(stdoutChunks)),
126
176
  stderr: new Uint8Array(Buffer.concat(stderrChunks)),
127
177
  exitCode: typeof exitCode === "number" ? exitCode : timedOut ? 124 : 1,
@@ -183,7 +233,7 @@ function createDirectSandbox() {
183
233
  import { access } from "fs/promises";
184
234
  import { constants } from "fs";
185
235
  import { spawn as spawn2 } from "child_process";
186
- import { dirname, isAbsolute as isAbsolute2, join as join2, relative, resolve, sep } from "path";
236
+ import { dirname, isAbsolute as isAbsolute2, join as join2, relative, resolve as resolve2, sep } from "path";
187
237
 
188
238
  // src/server/sandbox/bwrap/buildBwrapArgs.ts
189
239
  import { isAbsolute } from "path";
@@ -300,7 +350,7 @@ function terminateProcess2(child, signal) {
300
350
  }
301
351
  function computeSandboxCwd(workspaceRoot, cwd) {
302
352
  if (!cwd) return SANDBOX_HOME2;
303
- const absoluteCwd = isAbsolute2(cwd) ? cwd : resolve(workspaceRoot, cwd);
353
+ const absoluteCwd = isAbsolute2(cwd) ? cwd : resolve2(workspaceRoot, cwd);
304
354
  const relPath = relative(workspaceRoot, absoluteCwd);
305
355
  if (relPath === "") return SANDBOX_HOME2;
306
356
  if (relPath === ".." || relPath.startsWith(`..${sep}`)) {
@@ -400,7 +450,7 @@ function createBwrapSandbox() {
400
450
  "-c",
401
451
  cmd
402
452
  ];
403
- return await new Promise((resolve7, reject) => {
453
+ return await new Promise((resolve9, reject) => {
404
454
  const child = spawn2("bwrap", args, {
405
455
  env: withWorkspacePythonEnv({ workspaceRoot, env: opts?.env, sandboxRoot: SANDBOX_HOME2 }),
406
456
  stdio: ["ignore", "pipe", "pipe"],
@@ -427,7 +477,7 @@ function createBwrapSandbox() {
427
477
  if (settled) return;
428
478
  settled = true;
429
479
  cleanup();
430
- resolve7({
480
+ resolve9({
431
481
  stdout: new Uint8Array(Buffer.concat(stdoutChunks)),
432
482
  stderr: new Uint8Array(Buffer.concat(stderrChunks)),
433
483
  exitCode: typeof exitCode === "number" ? exitCode : timedOut ? 124 : 1,
@@ -1222,7 +1272,7 @@ import chokidar from "chokidar";
1222
1272
 
1223
1273
  // src/server/workspace/paths.ts
1224
1274
  import { lstat, realpath, stat } from "fs/promises";
1225
- import { dirname as dirname2, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve2 } from "path";
1275
+ import { dirname as dirname2, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve3 } from "path";
1226
1276
  function createPathValidationError(reason, requestedPath, message) {
1227
1277
  return Object.assign(new Error(message), {
1228
1278
  statusCode: 400,
@@ -1255,15 +1305,15 @@ function validatePath(workspaceRoot, relPath) {
1255
1305
  if (hasTraversalSegment(normalized) || normalized.startsWith("~") || normalized.startsWith("$") || /[\r\n]/.test(normalized)) {
1256
1306
  throw createPathValidationError("path-escape", relPath, "Path escapes workspace root");
1257
1307
  }
1258
- const resolvedRoot = resolve2(workspaceRoot);
1259
- const resolvedPath = resolve2(workspaceRoot, relPath);
1308
+ const resolvedRoot = resolve3(workspaceRoot);
1309
+ const resolvedPath = resolve3(workspaceRoot, relPath);
1260
1310
  if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}/`)) {
1261
1311
  throw createPathValidationError("path-escape", relPath, "Path escapes workspace root");
1262
1312
  }
1263
1313
  return resolvedPath;
1264
1314
  }
1265
1315
  async function assertRealPathWithinWorkspace(workspaceRoot, absPath) {
1266
- const realRoot = await realpath(resolve2(workspaceRoot));
1316
+ const realRoot = await realpath(resolve3(workspaceRoot));
1267
1317
  const realCandidate = await realpath(absPath);
1268
1318
  const rel = relative2(realRoot, realCandidate);
1269
1319
  if (rel.startsWith("..") || isAbsolute3(rel)) {
@@ -1810,8 +1860,8 @@ function fileRoutes(app, opts, done) {
1810
1860
  error: { code: ERROR_CODE_INTERNAL, message: "workspace does not support binary reads" }
1811
1861
  });
1812
1862
  }
1813
- const stat7 = await workspace.stat(path4);
1814
- if (stat7.kind !== "file") {
1863
+ const stat10 = await workspace.stat(path4);
1864
+ if (stat10.kind !== "file") {
1815
1865
  return reply.code(400).send({
1816
1866
  error: { code: ERROR_CODE_VALIDATION_ERROR, message: "path is not a file", field: "path" }
1817
1867
  });
@@ -1829,12 +1879,12 @@ function fileRoutes(app, opts, done) {
1829
1879
  try {
1830
1880
  const workspace = await resolveWorkspace(request);
1831
1881
  if (workspace.readFileWithStat) {
1832
- const { content: content2, stat: stat8 } = await workspace.readFileWithStat(path4);
1833
- return { content: content2, mtimeMs: stat8.kind === "file" ? stat8.mtimeMs : void 0 };
1882
+ const { content: content2, stat: stat11 } = await workspace.readFileWithStat(path4);
1883
+ return { content: content2, mtimeMs: stat11.kind === "file" ? stat11.mtimeMs : void 0 };
1834
1884
  }
1835
1885
  const content = await workspace.readFile(path4);
1836
- const stat7 = await workspace.stat(path4);
1837
- return { content, mtimeMs: stat7.kind === "file" ? stat7.mtimeMs : void 0 };
1886
+ const stat10 = await workspace.stat(path4);
1887
+ return { content, mtimeMs: stat10.kind === "file" ? stat10.mtimeMs : void 0 };
1838
1888
  } catch (err) {
1839
1889
  return classifyError(err, reply, "file");
1840
1890
  }
@@ -1883,11 +1933,11 @@ function fileRoutes(app, opts, done) {
1883
1933
  if (dir) await workspace.mkdir(dir, { recursive: true });
1884
1934
  }
1885
1935
  const content = body.content;
1886
- const stat7 = workspace.writeFileWithStat ? await workspace.writeFileWithStat(path4, content) : await (async () => {
1936
+ const stat10 = workspace.writeFileWithStat ? await workspace.writeFileWithStat(path4, content) : await (async () => {
1887
1937
  await workspace.writeFile(path4, content);
1888
1938
  return await workspace.stat(path4);
1889
1939
  })();
1890
- return { ok: true, mtimeMs: stat7.kind === "file" ? stat7.mtimeMs : void 0 };
1940
+ return { ok: true, mtimeMs: stat10.kind === "file" ? stat10.mtimeMs : void 0 };
1891
1941
  } catch (err) {
1892
1942
  return classifyError(err, reply, "file");
1893
1943
  }
@@ -1920,7 +1970,7 @@ function fileRoutes(app, opts, done) {
1920
1970
  }
1921
1971
  const bytes = Buffer.from(contentBase64, "base64");
1922
1972
  await workspace.mkdir(dir, { recursive: true });
1923
- const stat7 = workspace.writeBinaryFileWithStat ? await workspace.writeBinaryFileWithStat(path4, bytes) : await (async () => {
1973
+ const stat10 = workspace.writeBinaryFileWithStat ? await workspace.writeBinaryFileWithStat(path4, bytes) : await (async () => {
1924
1974
  if (!workspace.writeBinaryFile) {
1925
1975
  throw new Error("workspace does not support binary uploads");
1926
1976
  }
@@ -1934,7 +1984,7 @@ function fileRoutes(app, opts, done) {
1934
1984
  ok: true,
1935
1985
  path: path4,
1936
1986
  markdownUrl: markdownUrlFor(sourcePath, path4),
1937
- mtimeMs: stat7.kind === "file" ? stat7.mtimeMs : void 0
1987
+ mtimeMs: stat10.kind === "file" ? stat10.mtimeMs : void 0
1938
1988
  };
1939
1989
  } catch (err) {
1940
1990
  return classifyError(err, reply, "upload");
@@ -2021,8 +2071,8 @@ function fileRoutes(app, opts, done) {
2021
2071
  if (path4 === null) return;
2022
2072
  try {
2023
2073
  const workspace = await resolveWorkspace(request);
2024
- const stat7 = await workspace.stat(path4);
2025
- return stat7;
2074
+ const stat10 = await workspace.stat(path4);
2075
+ return stat10;
2026
2076
  } catch (err) {
2027
2077
  return classifyError(err, reply, "path");
2028
2078
  }
@@ -2033,8 +2083,8 @@ function fileRoutes(app, opts, done) {
2033
2083
  // src/server/workspace/provisionRuntime.ts
2034
2084
  import { createHash as createHash3 } from "crypto";
2035
2085
  import { constants as constants2 } from "fs";
2036
- import { access as access2, chmod as chmod3, cp, mkdir as mkdir4, readdir as readdir2, readFile as readFile4, stat as stat3, writeFile as writeFile4 } from "fs/promises";
2037
- import { dirname as dirname5, isAbsolute as isAbsolute4, join as join3, relative as relative5, resolve as resolve3 } from "path";
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";
2038
2088
  import { fileURLToPath } from "url";
2039
2089
  import { execFile } from "child_process";
2040
2090
  import { promisify } from "util";
@@ -2143,8 +2193,8 @@ function resolveTemplateTarget(workspaceRoot, target) {
2143
2193
  if (rawTarget.length === 0 || rawTarget.includes("\0") || rawTarget.includes("\\") || rawTarget.startsWith("/") || rawTarget.startsWith("//") || /^[A-Za-z]:[\\/]/.test(rawTarget) || rawTarget.split("/").includes("..")) {
2144
2194
  throw new Error(`Unsafe runtime template target: ${JSON.stringify(rawTarget)}. Template targets must be relative paths inside the workspace.`);
2145
2195
  }
2146
- const root = resolve3(workspaceRoot);
2147
- const resolved = resolve3(root, rawTarget);
2196
+ const root = resolve4(workspaceRoot);
2197
+ const resolved = resolve4(root, rawTarget);
2148
2198
  const rel = relative5(root, resolved);
2149
2199
  if (rel.startsWith("..") || isAbsolute4(rel)) {
2150
2200
  throw new Error(`Unsafe runtime template target: ${JSON.stringify(rawTarget)} resolves outside the workspace.`);
@@ -2171,6 +2221,8 @@ function nodePackageTarget(workspaceRoot, packageName) {
2171
2221
  }
2172
2222
  async function copyIfExists(source, target) {
2173
2223
  if (!await exists(source)) return false;
2224
+ if (resolve4(source) === resolve4(target)) return true;
2225
+ if (await exists(target) && await realpath2(source) === await realpath2(target)) return true;
2174
2226
  await cp(source, target, {
2175
2227
  recursive: true,
2176
2228
  force: true,
@@ -2203,11 +2255,11 @@ async function ensureNodePackages(workspaceRoot, specs) {
2203
2255
  }
2204
2256
  async function ensurePython(workspaceRoot, specs) {
2205
2257
  if (specs.length === 0) return;
2206
- const venvPython = join3(workspaceRoot, ".venv", "bin", "python");
2258
+ const venvPython = join3(workspaceRoot, ".boring-agent", "venv", "bin", "python");
2207
2259
  const uv = await commandExists("uv");
2208
2260
  if (!await exists(venvPython)) {
2209
- if (uv) await run("uv", ["venv", ".venv"], workspaceRoot);
2210
- else await run("/usr/bin/python3", ["-m", "venv", ".venv"], workspaceRoot);
2261
+ if (uv) await run("uv", ["venv", ".boring-agent/venv"], workspaceRoot);
2262
+ else await run("/usr/bin/python3", ["-m", "venv", ".boring-agent/venv"], workspaceRoot);
2211
2263
  }
2212
2264
  for (const spec of specs) {
2213
2265
  const projectDir = dirname5(toPath(spec.projectFile));
@@ -2236,7 +2288,7 @@ function assertEnvKey(key) {
2236
2288
  }
2237
2289
  async function isRuntimeMaterialized(workspaceRoot, contributions) {
2238
2290
  const hasPython = contributions.some(({ provisioning }) => (provisioning.python ?? []).length > 0);
2239
- if (hasPython && !await exists(join3(workspaceRoot, ".venv", "bin", "python"))) return false;
2291
+ if (hasPython && !await exists(join3(workspaceRoot, ".boring-agent", "venv", "bin", "python"))) return false;
2240
2292
  for (const { provisioning } of contributions) {
2241
2293
  for (const spec of provisioning.nodePackages ?? []) {
2242
2294
  if (!await exists(join3(nodePackageTarget(workspaceRoot, spec.packageName), "package.json"))) return false;
@@ -2246,7 +2298,7 @@ async function isRuntimeMaterialized(workspaceRoot, contributions) {
2246
2298
  }
2247
2299
  async function writeShims(workspaceRoot, env) {
2248
2300
  const shimDir = join3(workspaceRoot, ".boring-agent", "bin");
2249
- const venvBin = join3(workspaceRoot, ".venv", "bin");
2301
+ const venvBin = join3(workspaceRoot, ".boring-agent", "venv", "bin");
2250
2302
  await mkdir4(shimDir, { recursive: true });
2251
2303
  const exports = Object.entries(env).map(([key, value]) => {
2252
2304
  assertEnvKey(key);
@@ -2258,7 +2310,7 @@ SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
2258
2310
  WORKSPACE_ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)"
2259
2311
  export BORING_AGENT_WORKSPACE_ROOT="$WORKSPACE_ROOT"
2260
2312
  ${exports}
2261
- VENV_BIN="$WORKSPACE_ROOT/.venv/bin"
2313
+ VENV_BIN="$WORKSPACE_ROOT/.boring-agent/venv/bin"
2262
2314
  `;
2263
2315
  await writeExecutable(join3(shimDir, "python"), `${base}exec "$VENV_BIN/python" "$@"
2264
2316
  `);
@@ -2357,11 +2409,11 @@ function createTimedLruCache(ttlMs, maxEntries) {
2357
2409
  }
2358
2410
  };
2359
2411
  }
2360
- function cloneStat(stat7) {
2412
+ function cloneStat(stat10) {
2361
2413
  return {
2362
- size: stat7.size,
2363
- mtimeMs: stat7.mtimeMs,
2364
- kind: stat7.kind
2414
+ size: stat10.size,
2415
+ mtimeMs: stat10.mtimeMs,
2416
+ kind: stat10.kind
2365
2417
  };
2366
2418
  }
2367
2419
  function cloneEntries(entries) {
@@ -2519,15 +2571,15 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
2519
2571
  remote.fs.readFile(sandboxPath, "utf8"),
2520
2572
  remote.fs.stat(sandboxPath)
2521
2573
  ]);
2522
- const stat7 = {
2574
+ const stat10 = {
2523
2575
  size: fileStat.size,
2524
2576
  mtimeMs: fileStat.mtimeMs,
2525
2577
  kind: fileStat.isDirectory() ? "dir" : "file"
2526
2578
  };
2527
- statCache.set(sandboxPath, stat7);
2579
+ statCache.set(sandboxPath, stat10);
2528
2580
  return {
2529
2581
  content: typeof content === "string" ? content : Buffer.from(content).toString("utf-8"),
2530
- stat: cloneStat(stat7)
2582
+ stat: cloneStat(stat10)
2531
2583
  };
2532
2584
  }
2533
2585
  const version = metadataVersion;
@@ -2687,11 +2739,801 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
2687
2739
  };
2688
2740
  }
2689
2741
 
2742
+ // src/server/sandbox/vercel-sandbox/provisioningAdapter.ts
2743
+ import { mkdtemp } from "fs/promises";
2744
+ import { join as join4 } from "path";
2745
+ import { tmpdir } from "os";
2746
+
2747
+ // src/server/workspace/provisioning/errors.ts
2748
+ var ProvisioningError = class extends Error {
2749
+ code;
2750
+ details;
2751
+ constructor(code, message, details = {}, cause) {
2752
+ super(message, { cause });
2753
+ this.name = "ProvisioningError";
2754
+ this.code = code;
2755
+ this.details = details;
2756
+ }
2757
+ };
2758
+ function toProvisioningError(code, phase, error, details = {}) {
2759
+ if (error instanceof ProvisioningError) return error;
2760
+ const message = error instanceof Error ? error.message : String(error);
2761
+ return new ProvisioningError(
2762
+ code,
2763
+ `Workspace provisioning failed during ${phase}: ${message}`,
2764
+ { phase, ...details },
2765
+ error
2766
+ );
2767
+ }
2768
+ function logProvisioning(logger, level, message, fields = {}) {
2769
+ logger?.[level]?.(message, fields);
2770
+ }
2771
+
2772
+ // src/server/sandbox/vercel-sandbox/provisioningAdapter.ts
2773
+ var VERCEL_PROVISIONING_CACHE_ROOT = "/tmp/boring-agent-cache";
2774
+ function artifactExtension(kind) {
2775
+ return kind === "node" ? ".tgz" : ".tar.gz";
2776
+ }
2777
+ function artifactName(kind, id, fingerprint2) {
2778
+ const safeId = id.replace(/[^A-Za-z0-9._-]/g, "-");
2779
+ const safeFingerprint = fingerprint2.replace(/^sha256:/, "");
2780
+ return `${safeId}-${safeFingerprint}${artifactExtension(kind)}`;
2781
+ }
2782
+ function createVercelProvisioningAdapter(options) {
2783
+ return {
2784
+ mode: "vercel-sandbox",
2785
+ async exec(command, args, opts) {
2786
+ return await options.exec(command, args, {
2787
+ cwd: opts?.cwd ?? options.runtimeLayout.workspaceRoot,
2788
+ env: opts?.env,
2789
+ timeoutMs: opts?.timeoutMs
2790
+ });
2791
+ },
2792
+ async resolveInstallSource(source, opts) {
2793
+ const name = artifactName(opts.kind, opts.id, opts.fingerprint);
2794
+ const workspaceRel = `.boring-agent/tmp/${name}`;
2795
+ const runtimePath = `${options.runtimeLayout.tmp}/${name}`;
2796
+ if (!await options.workspaceFs.exists(workspaceRel)) {
2797
+ const artifactDir = await mkdtemp(join4(tmpdir(), "boring-agent-vercel-artifact-"));
2798
+ const outputPath = join4(artifactDir, name);
2799
+ try {
2800
+ await options.prepareArtifact({
2801
+ kind: opts.kind,
2802
+ id: opts.id,
2803
+ fingerprint: opts.fingerprint,
2804
+ source,
2805
+ outputPath
2806
+ });
2807
+ await options.workspaceFs.copyFromHost(outputPath, workspaceRel);
2808
+ } catch (error) {
2809
+ throw toProvisioningError(
2810
+ ErrorCode.enum.PROVISIONING_ARTIFACT_FAILED,
2811
+ "adapter-artifact",
2812
+ error,
2813
+ { runtime: opts.kind, id: opts.id, artifact: workspaceRel }
2814
+ );
2815
+ }
2816
+ }
2817
+ return runtimePath;
2818
+ },
2819
+ workspaceFs: options.workspaceFs,
2820
+ getRuntimeCacheRoot() {
2821
+ return options.cacheRoot ?? VERCEL_PROVISIONING_CACHE_ROOT;
2822
+ }
2823
+ };
2824
+ }
2825
+
2826
+ // src/server/workspace/provisioning/fingerprint.ts
2827
+ import { createHash as createHash4 } from "crypto";
2828
+ 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";
2831
+ var FINGERPRINT_RE = /^sha256:[a-f0-9]{64}$/;
2832
+ function isValidFingerprint(value) {
2833
+ return FINGERPRINT_RE.test(value.trim());
2834
+ }
2835
+ function normalizeSource(value) {
2836
+ if (value === void 0) return void 0;
2837
+ return String(value);
2838
+ }
2839
+ function stableStringify(value) {
2840
+ if (value === null || typeof value !== "object") {
2841
+ return JSON.stringify(value);
2842
+ }
2843
+ if (Array.isArray(value)) {
2844
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
2845
+ }
2846
+ const record = value;
2847
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
2848
+ }
2849
+ function createRuntimeFingerprint(input) {
2850
+ const hash = createHash4("sha256");
2851
+ hash.update(stableStringify(input));
2852
+ return `sha256:${hash.digest("hex")}`;
2853
+ }
2854
+ function createNodeRuntimeFingerprint(input) {
2855
+ return createRuntimeFingerprint({
2856
+ kind: "node",
2857
+ nodeVersion: input.nodeVersion,
2858
+ npmVersion: input.npmVersion,
2859
+ packages: input.packages.map((pkg) => ({
2860
+ id: pkg.id,
2861
+ packageName: pkg.packageName,
2862
+ packageRoot: normalizeSource(pkg.packageRoot),
2863
+ version: pkg.version,
2864
+ expectedBins: pkg.expectedBins ?? []
2865
+ }))
2866
+ });
2867
+ }
2868
+ function createPythonRuntimeFingerprint(input) {
2869
+ return createRuntimeFingerprint({
2870
+ kind: "python",
2871
+ pythonVersion: input.pythonVersion,
2872
+ uvVersion: input.uvVersion,
2873
+ packages: input.packages.map((pkg) => ({
2874
+ id: pkg.id,
2875
+ projectFile: normalizeSource(pkg.projectFile),
2876
+ packageName: pkg.packageName,
2877
+ packageRoot: normalizeSource(pkg.packageRoot),
2878
+ version: pkg.version,
2879
+ extraLibs: pkg.extraLibs ?? [],
2880
+ env: Object.fromEntries(
2881
+ Object.entries(pkg.env ?? {}).map(([key, value]) => [key, normalizeSource(value)])
2882
+ ),
2883
+ expectedBins: pkg.expectedBins ?? []
2884
+ }))
2885
+ });
2886
+ }
2887
+
2888
+ // src/server/workspace/provisioning/node.ts
2889
+ import { join as join5, relative as relative6, sep as sep3 } from "path";
2890
+ var NODE_RUNTIME_REL = ".boring-agent/node";
2891
+ var NODE_PACKAGE_JSON_REL = `${NODE_RUNTIME_REL}/package.json`;
2892
+ var NODE_FINGERPRINT_REL = `${NODE_RUNTIME_REL}/.fingerprint`;
2893
+ function parseNodeMajor(version) {
2894
+ const match = version.trim().match(/^v?(\d+)\./);
2895
+ return match ? Number(match[1]) : null;
2896
+ }
2897
+ async function commandOutput(adapter, command, args) {
2898
+ const result = await adapter.exec(command, args);
2899
+ return result?.stdout?.trim() ?? "";
2900
+ }
2901
+ async function ensureNodeEnv(adapter) {
2902
+ try {
2903
+ const nodeVersion = await commandOutput(adapter, "node", ["--version"]);
2904
+ const npmVersion = await commandOutput(adapter, "npm", ["--version"]);
2905
+ const major = parseNodeMajor(nodeVersion || process.version);
2906
+ if (major === null || major < 18) {
2907
+ throw new Error(`Unsupported Node.js version for runtime provisioning: ${nodeVersion || process.version}`);
2908
+ }
2909
+ return {
2910
+ nodeVersion: nodeVersion || process.version,
2911
+ npmVersion: npmVersion || "unknown"
2912
+ };
2913
+ } catch (error) {
2914
+ throw toProvisioningError(
2915
+ ErrorCode.enum.PROVISIONING_NODE_PREFLIGHT_FAILED,
2916
+ "node-preflight",
2917
+ error,
2918
+ { runtime: "node" }
2919
+ );
2920
+ }
2921
+ }
2922
+ function nodeInstallSource(spec) {
2923
+ return spec.version ? `${spec.packageName}@${spec.version}` : spec.packageName;
2924
+ }
2925
+ function expectedNodeOutputs(paths, packages) {
2926
+ return [
2927
+ join5(paths.node, "package-lock.json"),
2928
+ ...packages.flatMap(
2929
+ (pkg) => (pkg.expectedBins ?? []).map((bin) => join5(paths.nodeBin, bin))
2930
+ )
2931
+ ];
2932
+ }
2933
+ function toWorkspaceRel(paths, absolutePath) {
2934
+ return relative6(paths.workspaceRoot, absolutePath).split(sep3).join("/");
2935
+ }
2936
+ async function shouldInstallNodeRuntime(options) {
2937
+ const currentFingerprint = (await options.adapter.workspaceFs.readText(NODE_FINGERPRINT_REL))?.trim();
2938
+ if (!currentFingerprint || !isValidFingerprint(currentFingerprint) || currentFingerprint !== options.desiredFingerprint) {
2939
+ return true;
2940
+ }
2941
+ for (const output of options.expectedOutputs) {
2942
+ if (!await options.adapter.workspaceFs.exists(toWorkspaceRel(options.runtimeLayout, output))) return true;
2943
+ }
2944
+ return false;
2945
+ }
2946
+ async function ensureNodeRuntime(options) {
2947
+ if (options.packages.length === 0) {
2948
+ return { changed: false, pathEntries: [], fingerprint: null };
2949
+ }
2950
+ const versions = await ensureNodeEnv(options.adapter);
2951
+ const fingerprint2 = createNodeRuntimeFingerprint({
2952
+ ...versions,
2953
+ packages: options.packages
2954
+ });
2955
+ const expectedOutputs = expectedNodeOutputs(options.runtimeLayout, options.packages);
2956
+ if (!await shouldInstallNodeRuntime({
2957
+ adapter: options.adapter,
2958
+ runtimeLayout: options.runtimeLayout,
2959
+ desiredFingerprint: fingerprint2,
2960
+ expectedOutputs
2961
+ })) {
2962
+ return {
2963
+ changed: false,
2964
+ pathEntries: [options.runtimeLayout.nodeBin],
2965
+ fingerprint: fingerprint2
2966
+ };
2967
+ }
2968
+ const installSources = [];
2969
+ for (const pkg of options.packages) {
2970
+ if (pkg.packageRoot) {
2971
+ const packageFingerprint = createRuntimeFingerprint({ kind: "node-source", package: pkg });
2972
+ installSources.push(await options.adapter.resolveInstallSource(pkg.packageRoot, {
2973
+ kind: "node",
2974
+ id: pkg.id,
2975
+ fingerprint: packageFingerprint
2976
+ }));
2977
+ continue;
2978
+ }
2979
+ installSources.push(nodeInstallSource(pkg));
2980
+ }
2981
+ await options.adapter.workspaceFs.rm(NODE_RUNTIME_REL);
2982
+ await options.adapter.workspaceFs.mkdir(NODE_RUNTIME_REL);
2983
+ await options.adapter.workspaceFs.writeText(
2984
+ NODE_PACKAGE_JSON_REL,
2985
+ `${JSON.stringify({ name: "boring-agent-runtime", private: true }, null, 2)}
2986
+ `
2987
+ );
2988
+ try {
2989
+ await options.adapter.exec("npm", [
2990
+ "install",
2991
+ "--prefix",
2992
+ options.runtimeLayout.node,
2993
+ ...installSources
2994
+ ], {
2995
+ cwd: options.runtimeLayout.workspaceRoot,
2996
+ env: getBoringAgentRuntimeEnv(
2997
+ options.runtimeLayout,
2998
+ options.adapter.getRuntimeCacheRoot()
2999
+ )
3000
+ });
3001
+ } catch (error) {
3002
+ throw toProvisioningError(
3003
+ ErrorCode.enum.PROVISIONING_NPM_INSTALL_FAILED,
3004
+ "node-packages",
3005
+ error,
3006
+ { runtime: "node", packageIds: options.packages.map((pkg) => pkg.id) }
3007
+ );
3008
+ }
3009
+ await options.adapter.workspaceFs.writeText(NODE_FINGERPRINT_REL, `${fingerprint2}
3010
+ `);
3011
+ return {
3012
+ changed: true,
3013
+ pathEntries: [options.runtimeLayout.nodeBin],
3014
+ fingerprint: fingerprint2
3015
+ };
3016
+ }
3017
+
3018
+ // src/server/workspace/provisioning/python.ts
3019
+ import { dirname as dirname7, join as join6, relative as relative7, sep as sep4 } from "path";
3020
+ import { fileURLToPath as fileURLToPath2 } from "url";
3021
+ var VENV_REL = ".boring-agent/venv";
3022
+ var VENV_FINGERPRINT_REL = `${VENV_REL}/.fingerprint`;
3023
+ var UV_BIN_REL = ".boring-agent/sdk/uv/bin/uv";
3024
+ function sourceToPath(source) {
3025
+ return source instanceof URL ? fileURLToPath2(source) : source;
3026
+ }
3027
+ async function commandOutput2(adapter, command, args) {
3028
+ const result = await adapter.exec(command, args);
3029
+ return result?.stdout?.trim() ?? "";
3030
+ }
3031
+ async function ensureUv(options) {
3032
+ try {
3033
+ return {
3034
+ uvBin: "uv",
3035
+ uvVersion: await commandOutput2(options.adapter, "uv", ["--version"]) || "uv unknown",
3036
+ installedWorkspaceUv: false
3037
+ };
3038
+ } catch {
3039
+ if (!options.uvStandaloneSource) {
3040
+ throw toProvisioningError(
3041
+ ErrorCode.enum.PROVISIONING_UV_BOOTSTRAP_FAILED,
3042
+ "uv-bootstrap",
3043
+ new Error("uv is required for Python runtime provisioning; install uv or provide a standalone uv binary"),
3044
+ { runtime: "python" }
3045
+ );
3046
+ }
3047
+ }
3048
+ try {
3049
+ await options.adapter.workspaceFs.mkdir(".boring-agent/sdk/uv/bin");
3050
+ await options.adapter.workspaceFs.rm(UV_BIN_REL);
3051
+ await options.adapter.workspaceFs.copyFromHost(options.uvStandaloneSource, UV_BIN_REL);
3052
+ const uvBin = join6(options.runtimeLayout.uvBin, "uv");
3053
+ await options.adapter.exec("chmod", ["+x", uvBin], { cwd: options.runtimeLayout.workspaceRoot });
3054
+ return {
3055
+ uvBin,
3056
+ uvVersion: await commandOutput2(options.adapter, uvBin, ["--version"]) || "uv unknown",
3057
+ installedWorkspaceUv: true
3058
+ };
3059
+ } catch (error) {
3060
+ throw toProvisioningError(
3061
+ ErrorCode.enum.PROVISIONING_UV_BOOTSTRAP_FAILED,
3062
+ "uv-bootstrap",
3063
+ error,
3064
+ { runtime: "python" }
3065
+ );
3066
+ }
3067
+ }
3068
+ async function ensurePythonEnv(adapter) {
3069
+ return await commandOutput2(adapter, "python3", ["--version"]) || "Python unknown";
3070
+ }
3071
+ function pythonInstallSource(spec) {
3072
+ if (spec.version && spec.packageName) return `${spec.packageName}==${spec.version}`;
3073
+ if (spec.packageName) return spec.packageName;
3074
+ throw new Error(`Python runtime package ${spec.id} must declare packageName when no projectFile/packageRoot is provided`);
3075
+ }
3076
+ function sourceRootForPythonSpec(spec) {
3077
+ if (spec.packageRoot) return spec.packageRoot;
3078
+ if (spec.projectFile) return dirname7(sourceToPath(spec.projectFile));
3079
+ return null;
3080
+ }
3081
+ function expectedPythonOutputs(paths, packages) {
3082
+ return [
3083
+ paths.venvPython,
3084
+ ...packages.flatMap(
3085
+ (pkg) => (pkg.expectedBins ?? []).map((bin) => join6(paths.venvBin, bin))
3086
+ )
3087
+ ];
3088
+ }
3089
+ function collectPythonEnv(packages) {
3090
+ const env = {};
3091
+ for (const pkg of packages) {
3092
+ for (const [key, value] of Object.entries(pkg.env ?? {})) {
3093
+ env[key] = String(value);
3094
+ }
3095
+ }
3096
+ return env;
3097
+ }
3098
+ function toWorkspaceRel2(paths, absolutePath) {
3099
+ return relative7(paths.workspaceRoot, absolutePath).split(sep4).join("/");
3100
+ }
3101
+ async function shouldInstallPythonRuntime(options) {
3102
+ const currentFingerprint = (await options.adapter.workspaceFs.readText(VENV_FINGERPRINT_REL))?.trim();
3103
+ if (!currentFingerprint || !isValidFingerprint(currentFingerprint) || currentFingerprint !== options.desiredFingerprint) {
3104
+ return true;
3105
+ }
3106
+ for (const output of options.expectedOutputs) {
3107
+ if (!await options.adapter.workspaceFs.exists(toWorkspaceRel2(options.runtimeLayout, output))) return true;
3108
+ }
3109
+ return false;
3110
+ }
3111
+ async function ensurePythonRuntime(options) {
3112
+ const env = collectPythonEnv(options.packages);
3113
+ if (options.packages.length === 0) {
3114
+ return { changed: false, env, pathEntries: [], fingerprint: null };
3115
+ }
3116
+ const pythonVersion = await ensurePythonEnv(options.adapter);
3117
+ const uv = await ensureUv(options);
3118
+ const fingerprint2 = createPythonRuntimeFingerprint({
3119
+ packages: options.packages,
3120
+ pythonVersion,
3121
+ uvVersion: uv.uvVersion
3122
+ });
3123
+ const expectedOutputs = expectedPythonOutputs(options.runtimeLayout, options.packages);
3124
+ if (!await shouldInstallPythonRuntime({
3125
+ adapter: options.adapter,
3126
+ runtimeLayout: options.runtimeLayout,
3127
+ desiredFingerprint: fingerprint2,
3128
+ expectedOutputs
3129
+ })) {
3130
+ return {
3131
+ changed: false,
3132
+ env,
3133
+ pathEntries: [options.runtimeLayout.venvBin, options.runtimeLayout.uvBin],
3134
+ fingerprint: fingerprint2
3135
+ };
3136
+ }
3137
+ const installSources = [];
3138
+ for (const pkg of options.packages) {
3139
+ const sourceRoot = sourceRootForPythonSpec(pkg);
3140
+ if (sourceRoot) {
3141
+ const packageFingerprint = createRuntimeFingerprint({ kind: "python-source", package: pkg });
3142
+ installSources.push(await options.adapter.resolveInstallSource(sourceRoot, {
3143
+ kind: "python",
3144
+ id: pkg.id,
3145
+ fingerprint: packageFingerprint
3146
+ }));
3147
+ } else {
3148
+ installSources.push(pythonInstallSource(pkg));
3149
+ }
3150
+ installSources.push(...pkg.extraLibs ?? []);
3151
+ }
3152
+ await options.adapter.workspaceFs.rm(VENV_REL);
3153
+ try {
3154
+ await options.adapter.exec(uv.uvBin, ["venv", options.runtimeLayout.venv], {
3155
+ cwd: options.runtimeLayout.workspaceRoot,
3156
+ env: getBoringAgentRuntimeEnv(
3157
+ options.runtimeLayout,
3158
+ options.adapter.getRuntimeCacheRoot()
3159
+ )
3160
+ });
3161
+ await options.adapter.exec(uv.uvBin, [
3162
+ "pip",
3163
+ "install",
3164
+ "--python",
3165
+ options.runtimeLayout.venvPython,
3166
+ ...installSources
3167
+ ], {
3168
+ cwd: options.runtimeLayout.workspaceRoot,
3169
+ env: {
3170
+ ...getBoringAgentRuntimeEnv(
3171
+ options.runtimeLayout,
3172
+ options.adapter.getRuntimeCacheRoot()
3173
+ ),
3174
+ ...env
3175
+ }
3176
+ });
3177
+ } catch (error) {
3178
+ throw toProvisioningError(
3179
+ ErrorCode.enum.PROVISIONING_UV_INSTALL_FAILED,
3180
+ "python-packages",
3181
+ error,
3182
+ { runtime: "python", packageIds: options.packages.map((pkg) => pkg.id) }
3183
+ );
3184
+ }
3185
+ await options.adapter.workspaceFs.writeText(VENV_FINGERPRINT_REL, `${fingerprint2}
3186
+ `);
3187
+ return {
3188
+ changed: true,
3189
+ env,
3190
+ pathEntries: [options.runtimeLayout.venvBin, options.runtimeLayout.uvBin],
3191
+ fingerprint: fingerprint2
3192
+ };
3193
+ }
3194
+
3195
+ // src/server/workspace/provisioning/skills.ts
3196
+ import { stat as stat4 } from "fs/promises";
3197
+ import { join as join7 } from "path";
3198
+ import { fileURLToPath as fileURLToPath3 } from "url";
3199
+ var GENERATED_SKILLS_REL = ".boring-agent/skills";
3200
+ var USER_SKILLS_REL = ".agents/skills";
3201
+ function sourceToPath2(source) {
3202
+ return source instanceof URL ? fileURLToPath3(source) : source;
3203
+ }
3204
+ function assertSafeSegment(kind, value) {
3205
+ if (value.length === 0 || value.includes("\0") || value.includes("/") || value.includes("\\") || value === "." || value === "..") {
3206
+ throw new Error(`Invalid ${kind} for plugin skill mirror: ${value}`);
3207
+ }
3208
+ }
3209
+ function getProvisionedSkillPaths(paths) {
3210
+ return [paths.skills, join7(paths.workspaceRoot, USER_SKILLS_REL)];
3211
+ }
3212
+ async function mirrorPluginSkills(options) {
3213
+ await options.adapter.workspaceFs.rm(GENERATED_SKILLS_REL);
3214
+ await options.adapter.workspaceFs.mkdir(GENERATED_SKILLS_REL);
3215
+ const seen = /* @__PURE__ */ new Set();
3216
+ let copiedSkillCount = 0;
3217
+ for (const plugin of options.plugins) {
3218
+ assertSafeSegment("plugin id", plugin.id);
3219
+ for (const skill of plugin.skills ?? []) {
3220
+ assertSafeSegment("skill name", skill.name);
3221
+ const key = `${plugin.id}/${skill.name}`;
3222
+ if (seen.has(key)) {
3223
+ throw new Error(`Duplicate plugin skill mirror target: ${key}`);
3224
+ }
3225
+ seen.add(key);
3226
+ const sourcePath = sourceToPath2(skill.source);
3227
+ const sourceStat = await stat4(sourcePath);
3228
+ const skillTarget = `${GENERATED_SKILLS_REL}/${plugin.id}/${skill.name}`;
3229
+ const target = sourceStat.isDirectory() ? skillTarget : `${skillTarget}/SKILL.md`;
3230
+ await options.adapter.workspaceFs.copyFromHost(skill.source, target);
3231
+ copiedSkillCount += 1;
3232
+ }
3233
+ }
3234
+ return {
3235
+ changed: copiedSkillCount > 0,
3236
+ skillPaths: getProvisionedSkillPaths(options.runtimeLayout)
3237
+ };
3238
+ }
3239
+
3240
+ // src/server/workspace/provisioning/workspaceFiles.ts
3241
+ import { basename, join as joinPath } from "path";
3242
+ import { posix } from "path";
3243
+ import { readdir as readdir3, stat as stat5 } from "fs/promises";
3244
+ import { fileURLToPath as fileURLToPath4, pathToFileURL } from "url";
3245
+ function sourceToPath3(source) {
3246
+ return source instanceof URL ? fileURLToPath4(source) : source;
3247
+ }
3248
+ function toWorkspaceRel3(...parts) {
3249
+ return posix.normalize(parts.filter(Boolean).join("/"));
3250
+ }
3251
+ function sourceForChild(rootSource, childPath) {
3252
+ return rootSource instanceof URL ? pathToFileURL(childPath) : childPath;
3253
+ }
3254
+ async function collectTemplateWorkItems(template) {
3255
+ const sourcePath = sourceToPath3(template.path);
3256
+ const sourceStat = await stat5(sourcePath);
3257
+ const targetPrefix = template.target ?? "";
3258
+ if (!sourceStat.isDirectory()) {
3259
+ const targetRel = toWorkspaceRel3(targetPrefix || basename(sourcePath));
3260
+ return [{
3261
+ hostSourcePath: sourcePath,
3262
+ source: template.path,
3263
+ targetRel,
3264
+ kind: "file"
3265
+ }];
3266
+ }
3267
+ const items = [];
3268
+ async function walk(dir, rel) {
3269
+ const targetRel = toWorkspaceRel3(targetPrefix, rel);
3270
+ if (rel !== "") {
3271
+ items.push({
3272
+ hostSourcePath: dir,
3273
+ source: sourceForChild(template.path, dir),
3274
+ targetRel,
3275
+ kind: "directory"
3276
+ });
3277
+ } else if (targetPrefix) {
3278
+ items.push({
3279
+ hostSourcePath: dir,
3280
+ source: sourceForChild(template.path, dir),
3281
+ targetRel: toWorkspaceRel3(targetPrefix),
3282
+ kind: "directory"
3283
+ });
3284
+ }
3285
+ const entries = await readdir3(dir, { withFileTypes: true });
3286
+ for (const entry of entries) {
3287
+ const childPath = joinPath(dir, entry.name);
3288
+ const childRel = rel ? `${rel}/${entry.name}` : entry.name;
3289
+ if (entry.isDirectory()) {
3290
+ await walk(childPath, childRel);
3291
+ } else if (entry.isFile()) {
3292
+ items.push({
3293
+ hostSourcePath: childPath,
3294
+ source: sourceForChild(template.path, childPath),
3295
+ targetRel: toWorkspaceRel3(targetPrefix, childRel),
3296
+ kind: "file"
3297
+ });
3298
+ }
3299
+ }
3300
+ }
3301
+ await walk(sourcePath, "");
3302
+ return items;
3303
+ }
3304
+ async function seedTemplate(options) {
3305
+ const items = await collectTemplateWorkItems(options.template);
3306
+ let changed = false;
3307
+ for (const item of items) {
3308
+ let exists2;
3309
+ try {
3310
+ exists2 = await options.adapter.workspaceFs.exists(item.targetRel);
3311
+ if (exists2) continue;
3312
+ if (item.kind === "directory") {
3313
+ await options.adapter.workspaceFs.mkdir(item.targetRel);
3314
+ } else {
3315
+ await options.adapter.workspaceFs.copyFromHost(item.source, item.targetRel);
3316
+ }
3317
+ changed = true;
3318
+ } catch (error) {
3319
+ throw new Error(
3320
+ `Failed to seed workspace template file (plugin=${options.pluginId}, template=${options.template.id}, source=${item.hostSourcePath}, targetRel=${item.targetRel}): ${error.message}`,
3321
+ { cause: error }
3322
+ );
3323
+ }
3324
+ }
3325
+ return changed;
3326
+ }
3327
+ async function seedWorkspaceFiles(options) {
3328
+ let changed = false;
3329
+ for (const plugin of options.plugins) {
3330
+ for (const template of plugin.provisioning?.templateDirs ?? []) {
3331
+ const templateChanged = await seedTemplate({
3332
+ adapter: options.adapter,
3333
+ pluginId: plugin.id,
3334
+ template
3335
+ });
3336
+ changed = changed || templateChanged;
3337
+ }
3338
+ }
3339
+ return { changed };
3340
+ }
3341
+
3342
+ // src/server/workspace/provisioning/provisionWorkspaceRuntime.ts
3343
+ async function ensureRuntimeLayout(opts) {
3344
+ let changed = false;
3345
+ const dirs = [
3346
+ BORING_AGENT_DIR,
3347
+ ...BORING_AGENT_RUNTIME_DIR_NAMES.map((dir) => `${BORING_AGENT_DIR}/${dir}`)
3348
+ ];
3349
+ for (const dir of dirs) {
3350
+ if (!await opts.adapter.workspaceFs.exists(dir)) changed = true;
3351
+ await opts.adapter.workspaceFs.mkdir(dir);
3352
+ }
3353
+ const gitignorePath = `${BORING_AGENT_DIR}/.gitignore`;
3354
+ const currentGitignore = await opts.adapter.workspaceFs.readText(gitignorePath);
3355
+ if (currentGitignore !== BORING_AGENT_GITIGNORE_CONTENT) {
3356
+ await opts.adapter.workspaceFs.writeText(gitignorePath, BORING_AGENT_GITIGNORE_CONTENT);
3357
+ changed = true;
3358
+ }
3359
+ return changed;
3360
+ }
3361
+ function collectNodePackages(plugins) {
3362
+ return plugins.flatMap((plugin) => plugin.provisioning?.nodePackages ?? []);
3363
+ }
3364
+ function collectPythonPackages(plugins) {
3365
+ return plugins.flatMap((plugin) => plugin.provisioning?.python ?? []);
3366
+ }
3367
+ function countSkills(plugins) {
3368
+ return plugins.reduce((count, plugin) => count + (plugin.skills?.length ?? 0), 0);
3369
+ }
3370
+ function countTemplateDirs(plugins) {
3371
+ return plugins.reduce((count, plugin) => count + (plugin.provisioning?.templateDirs?.length ?? 0), 0);
3372
+ }
3373
+ function changedString(result) {
3374
+ const changed = result?.changed;
3375
+ return typeof changed === "boolean" ? changed ? "true" : "false" : void 0;
3376
+ }
3377
+ function telemetryProperties(opts, extra = {}) {
3378
+ return {
3379
+ runtimeMode: opts.telemetryContext?.runtimeMode ?? opts.adapter.mode,
3380
+ workspaceId: opts.telemetryContext?.workspaceId,
3381
+ sessionId: opts.telemetryContext?.sessionId,
3382
+ requestId: opts.telemetryContext?.requestId,
3383
+ ...extra
3384
+ };
3385
+ }
3386
+ function captureProvisioningEvent(opts, name, properties) {
3387
+ if (!opts.telemetry) return;
3388
+ safeCapture(opts.telemetry, {
3389
+ name,
3390
+ properties: telemetryProperties(opts, properties)
3391
+ });
3392
+ }
3393
+ async function runPhase(options) {
3394
+ const startedAt = Date.now();
3395
+ logProvisioning(options.logger, "info", `workspace provisioning ${options.phase} started`, options.details);
3396
+ try {
3397
+ const result = await options.run();
3398
+ const changed = changedString(result);
3399
+ captureProvisioningEvent(options.opts, "agent.runtime.provisioning.step", {
3400
+ phase: options.telemetryPhase,
3401
+ status: "ok",
3402
+ durationMs: Date.now() - startedAt,
3403
+ ...changed ? { changed } : {}
3404
+ });
3405
+ logProvisioning(options.logger, "info", `workspace provisioning ${options.phase} completed`, options.details);
3406
+ return result;
3407
+ } catch (error) {
3408
+ const provisioningError = toProvisioningError(
3409
+ options.code,
3410
+ options.phase,
3411
+ error,
3412
+ options.details
3413
+ );
3414
+ captureProvisioningEvent(options.opts, "agent.runtime.provisioning.step", {
3415
+ phase: options.telemetryPhase,
3416
+ status: "error",
3417
+ durationMs: Date.now() - startedAt,
3418
+ errorCode: provisioningError.code
3419
+ });
3420
+ logProvisioning(options.logger, "error", `workspace provisioning ${options.phase} failed`, {
3421
+ code: provisioningError.code,
3422
+ ...provisioningError.details
3423
+ });
3424
+ throw provisioningError;
3425
+ }
3426
+ }
3427
+ async function provisionWorkspaceRuntime(opts) {
3428
+ const logger = opts.logger;
3429
+ const startedAt = Date.now();
3430
+ const pluginIds = opts.plugins.map((plugin) => plugin.id);
3431
+ const nodePackages = collectNodePackages(opts.plugins);
3432
+ const pythonPackages = collectPythonPackages(opts.plugins);
3433
+ const summaryCounts = {
3434
+ nodePackageCount: nodePackages.length,
3435
+ pythonPackageCount: pythonPackages.length,
3436
+ skillCount: countSkills(opts.plugins),
3437
+ templateDirCount: countTemplateDirs(opts.plugins)
3438
+ };
3439
+ captureProvisioningEvent(opts, "agent.runtime.provisioning.started", {
3440
+ status: "started",
3441
+ ...summaryCounts
3442
+ });
3443
+ try {
3444
+ const layoutChanged = await runPhase({
3445
+ opts,
3446
+ logger,
3447
+ phase: "layout",
3448
+ telemetryPhase: "layout",
3449
+ code: ErrorCode.enum.PROVISIONING_LAYOUT_FAILED,
3450
+ details: { workspaceRoot: opts.runtimeLayout.workspaceRoot },
3451
+ run: () => ensureRuntimeLayout(opts)
3452
+ });
3453
+ const skills = await runPhase({
3454
+ opts,
3455
+ logger,
3456
+ phase: "skill mirror",
3457
+ telemetryPhase: "skills-mirror",
3458
+ code: ErrorCode.enum.PROVISIONING_SKILLS_FAILED,
3459
+ details: { workspaceRoot: opts.runtimeLayout.workspaceRoot, pluginIds },
3460
+ run: () => mirrorPluginSkills({
3461
+ plugins: opts.plugins,
3462
+ adapter: opts.adapter,
3463
+ runtimeLayout: opts.runtimeLayout
3464
+ })
3465
+ });
3466
+ const workspaceFiles = await runPhase({
3467
+ opts,
3468
+ logger,
3469
+ phase: "workspace files",
3470
+ telemetryPhase: "workspace-files",
3471
+ code: ErrorCode.enum.PROVISIONING_TEMPLATES_FAILED,
3472
+ details: { workspaceRoot: opts.runtimeLayout.workspaceRoot, pluginIds },
3473
+ run: () => seedWorkspaceFiles({
3474
+ plugins: opts.plugins,
3475
+ adapter: opts.adapter
3476
+ })
3477
+ });
3478
+ const node = await runPhase({
3479
+ opts,
3480
+ logger,
3481
+ phase: "node packages",
3482
+ telemetryPhase: "node-packages",
3483
+ code: ErrorCode.enum.PROVISIONING_NPM_INSTALL_FAILED,
3484
+ details: { workspaceRoot: opts.runtimeLayout.workspaceRoot, packageIds: nodePackages.map((pkg) => pkg.id) },
3485
+ run: () => ensureNodeRuntime({
3486
+ adapter: opts.adapter,
3487
+ runtimeLayout: opts.runtimeLayout,
3488
+ packages: nodePackages
3489
+ })
3490
+ });
3491
+ const python = await runPhase({
3492
+ opts,
3493
+ logger,
3494
+ phase: "python packages",
3495
+ telemetryPhase: "python-packages",
3496
+ code: ErrorCode.enum.PROVISIONING_UV_INSTALL_FAILED,
3497
+ details: { workspaceRoot: opts.runtimeLayout.workspaceRoot, packageIds: pythonPackages.map((pkg) => pkg.id) },
3498
+ run: () => ensurePythonRuntime({
3499
+ adapter: opts.adapter,
3500
+ runtimeLayout: opts.runtimeLayout,
3501
+ packages: pythonPackages
3502
+ })
3503
+ });
3504
+ const result = {
3505
+ changed: layoutChanged || skills.changed || workspaceFiles.changed || node.changed || python.changed,
3506
+ env: {
3507
+ ...getBoringAgentRuntimeEnv(opts.runtimeLayout, opts.adapter.getRuntimeCacheRoot()),
3508
+ ...python.env
3509
+ },
3510
+ pathEntries: getBoringAgentPathEntries(opts.runtimeLayout),
3511
+ skillPaths: skills.skillPaths
3512
+ };
3513
+ captureProvisioningEvent(opts, "agent.runtime.provisioning.completed", {
3514
+ status: "ok",
3515
+ durationMs: Date.now() - startedAt,
3516
+ changed: result.changed ? "true" : "false",
3517
+ ...summaryCounts
3518
+ });
3519
+ return result;
3520
+ } catch (error) {
3521
+ const code = error?.code;
3522
+ captureProvisioningEvent(opts, "agent.runtime.provisioning.failed", {
3523
+ status: "error",
3524
+ durationMs: Date.now() - startedAt,
3525
+ ...typeof code === "string" ? { errorCode: code } : {},
3526
+ ...summaryCounts
3527
+ });
3528
+ throw error;
3529
+ }
3530
+ }
3531
+
2690
3532
  // src/server/runtime/resolveMode.ts
2691
3533
  import { spawnSync } from "child_process";
2692
3534
 
2693
3535
  // src/server/runtime/modes/direct.ts
2694
- import { mkdir as mkdir6 } from "fs/promises";
3536
+ import { mkdir as mkdir7 } from "fs/promises";
2695
3537
 
2696
3538
  // src/server/runtime/createServerFileSearch.ts
2697
3539
  var DEFAULT_LIMIT = 500;
@@ -2749,28 +3591,14 @@ function createServerFileSearch(workspace, sandbox) {
2749
3591
  }
2750
3592
 
2751
3593
  // src/server/workspace/provision.ts
2752
- import { cp as cp2, mkdir as mkdir5, stat as stat4, writeFile as writeFile5 } from "fs/promises";
2753
- import { dirname as dirname6, join as join4 } from "path";
2754
- var PROVISION_MARKER_REL_PATH = ".boring-agent/provisioned";
2755
- async function fileExists(targetPath) {
2756
- try {
2757
- await stat4(targetPath);
2758
- return true;
2759
- } catch (error) {
2760
- if (error.code === "ENOENT") {
2761
- return false;
2762
- }
2763
- throw error;
2764
- }
2765
- }
3594
+ import { cp as cp2 } from "fs/promises";
2766
3595
  async function copyTemplate(templatePath, workspaceRoot) {
2767
3596
  if (!templatePath) return;
2768
- const markerPath = join4(workspaceRoot, PROVISION_MARKER_REL_PATH);
2769
- if (await fileExists(markerPath)) return;
2770
3597
  try {
2771
3598
  await cp2(templatePath, workspaceRoot, {
2772
3599
  recursive: true,
2773
- errorOnExist: false
3600
+ errorOnExist: false,
3601
+ force: false
2774
3602
  });
2775
3603
  } catch (error) {
2776
3604
  const message = error instanceof Error ? error.message : String(error);
@@ -2779,16 +3607,230 @@ async function copyTemplate(templatePath, workspaceRoot) {
2779
3607
  { cause: error }
2780
3608
  );
2781
3609
  }
2782
- await mkdir5(dirname6(markerPath), { recursive: true });
2783
- await writeFile5(markerPath, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
3610
+ }
3611
+
3612
+ // src/server/runtime/modes/provisioningAdapter.ts
3613
+ 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";
3616
+ import { fileURLToPath as fileURLToPath5 } from "url";
3617
+ var LOCAL_SANDBOX_WORKSPACE_ROOT = "/workspace";
3618
+ function sourceToPath4(source) {
3619
+ return source instanceof URL ? fileURLToPath5(source) : source;
3620
+ }
3621
+ async function assertExistingInsideWorkspace(root, relPath) {
3622
+ const absPath = validatePath(root, relPath);
3623
+ try {
3624
+ await assertRealPathWithinWorkspace(root, absPath);
3625
+ return absPath;
3626
+ } catch (error) {
3627
+ if (error.code === "ENOENT") return null;
3628
+ throw error;
3629
+ }
3630
+ }
3631
+ async function prepareWritablePath(root, relPath) {
3632
+ const absPath = validatePath(root, relPath);
3633
+ await mkdir6(dirname8(absPath), { recursive: true });
3634
+ await assertRealPathWithinWorkspace(root, dirname8(absPath));
3635
+ try {
3636
+ const targetStat = await lstat3(absPath);
3637
+ if (targetStat.isSymbolicLink()) {
3638
+ throw Object.assign(new Error("Target path is a symlink"), {
3639
+ statusCode: 400,
3640
+ reason: "symlink-escape",
3641
+ requestedPath: relPath
3642
+ });
3643
+ }
3644
+ } catch (error) {
3645
+ if (error.code !== "ENOENT") throw error;
3646
+ }
3647
+ return absPath;
3648
+ }
3649
+ async function spawnCommand(command, args, opts) {
3650
+ return await new Promise((resolvePromise, rejectPromise) => {
3651
+ const child = spawn3(command, args, {
3652
+ cwd: opts.cwd,
3653
+ env: { ...process.env, ...opts.env },
3654
+ stdio: ["ignore", "pipe", "pipe"],
3655
+ shell: false,
3656
+ windowsHide: true
3657
+ });
3658
+ const stdout = [];
3659
+ const stderr = [];
3660
+ let timeout = null;
3661
+ let settled = false;
3662
+ const settle = (error) => {
3663
+ if (settled) return;
3664
+ settled = true;
3665
+ if (timeout) clearTimeout(timeout);
3666
+ if (error) rejectPromise(error);
3667
+ else resolvePromise({
3668
+ stdout: Buffer.concat(stdout).toString("utf8"),
3669
+ stderr: Buffer.concat(stderr).toString("utf8")
3670
+ });
3671
+ };
3672
+ child.stdout?.on("data", (chunk2) => stdout.push(chunk2));
3673
+ child.stderr?.on("data", (chunk2) => stderr.push(chunk2));
3674
+ child.on("error", settle);
3675
+ child.on("close", (code) => {
3676
+ if (code === 0) {
3677
+ settle();
3678
+ return;
3679
+ }
3680
+ const message = Buffer.concat(stderr).toString("utf8").trim();
3681
+ settle(new Error(`Command failed (${command}) with exit code ${code ?? "unknown"}${message ? `: ${message}` : ""}`));
3682
+ });
3683
+ timeout = setTimeout(() => {
3684
+ child.kill("SIGTERM");
3685
+ settle(new Error(`Command timed out after ${opts.timeoutMs}ms: ${command}`));
3686
+ }, opts.timeoutMs);
3687
+ });
3688
+ }
3689
+ function defaultExecOptions(paths, opts) {
3690
+ return {
3691
+ cwd: opts?.cwd ?? paths.workspaceRoot,
3692
+ env: opts?.env ?? {},
3693
+ timeoutMs: opts?.timeoutMs ?? 12e4
3694
+ };
3695
+ }
3696
+ function mapWorkspacePathToLocalSandbox(paths, value) {
3697
+ const absolute = isAbsolute5(value) ? value : resolve5(paths.workspaceRoot, value);
3698
+ const relPath = relative8(paths.workspaceRoot, absolute);
3699
+ if (relPath === "") return LOCAL_SANDBOX_WORKSPACE_ROOT;
3700
+ if (relPath === ".." || relPath.startsWith(`..${sep5}`)) return value;
3701
+ return `${LOCAL_SANDBOX_WORKSPACE_ROOT}/${relPath.split(sep5).join("/")}`;
3702
+ }
3703
+ function mapValueToLocalSandbox(paths, value) {
3704
+ return value.startsWith(paths.workspaceRoot) ? mapWorkspacePathToLocalSandbox(paths, value) : value;
3705
+ }
3706
+ function mapEnvToLocalSandbox(paths, env) {
3707
+ return Object.fromEntries(
3708
+ Object.entries(env).map(([key, value]) => [key, mapValueToLocalSandbox(paths, value)])
3709
+ );
3710
+ }
3711
+ function sanitizeInstallSourcePart(value) {
3712
+ const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
3713
+ return sanitized.length > 0 ? sanitized : "source";
3714
+ }
3715
+ async function copyExternalSourceIntoWorkspace(paths, sourcePath, opts) {
3716
+ const fingerprint2 = opts.fingerprint.replace(/^sha256:/, "");
3717
+ const relTarget = `.boring-agent/tmp/${sanitizeInstallSourcePart(opts.kind)}-${sanitizeInstallSourcePart(opts.id)}-${sanitizeInstallSourcePart(fingerprint2)}-source`;
3718
+ 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);
3723
+ await cp3(sourcePath, absTarget, {
3724
+ recursive: sourceStat.isDirectory(),
3725
+ force: false,
3726
+ errorOnExist: true
3727
+ });
3728
+ return `${LOCAL_SANDBOX_WORKSPACE_ROOT}/${relTarget}`;
3729
+ }
3730
+ function createWorkspaceFs(workspaceRoot) {
3731
+ return {
3732
+ async exists(workspaceRelativePath) {
3733
+ const absPath = await assertExistingInsideWorkspace(workspaceRoot, workspaceRelativePath);
3734
+ if (!absPath) return false;
3735
+ await lstat3(absPath);
3736
+ return true;
3737
+ },
3738
+ async rm(workspaceRelativePath) {
3739
+ const absPath = await assertExistingInsideWorkspace(workspaceRoot, workspaceRelativePath);
3740
+ if (!absPath) return;
3741
+ await rm2(absPath, { recursive: true, force: true });
3742
+ },
3743
+ async mkdir(workspaceRelativePath) {
3744
+ const absPath = validatePath(workspaceRoot, workspaceRelativePath);
3745
+ await mkdir6(absPath, { recursive: true });
3746
+ await assertRealPathWithinWorkspace(workspaceRoot, absPath);
3747
+ },
3748
+ async writeText(workspaceRelativePath, content) {
3749
+ const absPath = await prepareWritablePath(workspaceRoot, workspaceRelativePath);
3750
+ await writeFile5(absPath, content, "utf8");
3751
+ },
3752
+ async readText(workspaceRelativePath) {
3753
+ const absPath = await assertExistingInsideWorkspace(workspaceRoot, workspaceRelativePath);
3754
+ if (!absPath) return null;
3755
+ return await readFile6(absPath, "utf8");
3756
+ },
3757
+ async copyFromHost(hostSourcePath, workspaceRelativeTarget) {
3758
+ const sourcePath = sourceToPath4(hostSourcePath);
3759
+ const absTarget = await prepareWritablePath(workspaceRoot, workspaceRelativeTarget);
3760
+ const sourceStat = await stat6(sourcePath);
3761
+ await cp3(sourcePath, absTarget, {
3762
+ recursive: sourceStat.isDirectory(),
3763
+ force: false,
3764
+ errorOnExist: true
3765
+ });
3766
+ }
3767
+ };
3768
+ }
3769
+ function createDirectProvisioningAdapter(paths, runner = spawnCommand) {
3770
+ return {
3771
+ mode: "direct",
3772
+ async exec(command, args, opts) {
3773
+ return await runner(command, args, defaultExecOptions(paths, opts));
3774
+ },
3775
+ async resolveInstallSource(source) {
3776
+ return sourceToPath4(source);
3777
+ },
3778
+ workspaceFs: createWorkspaceFs(paths.workspaceRoot),
3779
+ getRuntimeCacheRoot() {
3780
+ return paths.cache;
3781
+ }
3782
+ };
3783
+ }
3784
+ function createLocalProvisioningAdapter(paths, runner = spawnCommand) {
3785
+ const sourceMounts = /* @__PURE__ */ new Map();
3786
+ return {
3787
+ mode: "local",
3788
+ async exec(command, args, opts) {
3789
+ const execOpts = defaultExecOptions(paths, opts);
3790
+ const bwrapArgs = buildBwrapArgs(paths.workspaceRoot, {
3791
+ extraArgs: [
3792
+ "--dir",
3793
+ "/mnt",
3794
+ "--dir",
3795
+ "/mnt/boring-agent-sources",
3796
+ ...[...sourceMounts.entries()].flatMap(([host, sandbox]) => ["--ro-bind", host, sandbox])
3797
+ ]
3798
+ });
3799
+ return await runner("bwrap", [
3800
+ ...bwrapArgs,
3801
+ mapValueToLocalSandbox(paths, command),
3802
+ ...args.map((arg) => mapValueToLocalSandbox(paths, arg))
3803
+ ], {
3804
+ ...execOpts,
3805
+ cwd: paths.workspaceRoot,
3806
+ env: mapEnvToLocalSandbox(paths, execOpts.env)
3807
+ });
3808
+ },
3809
+ async resolveInstallSource(source, opts) {
3810
+ const hostPath = sourceToPath4(source);
3811
+ const realWorkspaceRoot = await realpath3(paths.workspaceRoot);
3812
+ const realSource = await realpath3(hostPath);
3813
+ const relPath = relative8(realWorkspaceRoot, realSource);
3814
+ if (relPath === "") return LOCAL_SANDBOX_WORKSPACE_ROOT;
3815
+ if (!relPath.startsWith("..") && !isAbsolute5(relPath)) {
3816
+ return `${LOCAL_SANDBOX_WORKSPACE_ROOT}/${relPath.split(sep5).join("/")}`;
3817
+ }
3818
+ return await copyExternalSourceIntoWorkspace(paths, realSource, opts);
3819
+ },
3820
+ workspaceFs: createWorkspaceFs(paths.workspaceRoot),
3821
+ getRuntimeCacheRoot() {
3822
+ return paths.cache;
3823
+ }
3824
+ };
2784
3825
  }
2785
3826
 
2786
3827
  // src/server/runtime/modes/direct.ts
2787
3828
  var directModeAdapter = {
2788
3829
  id: "direct",
2789
3830
  workspaceFsCapability: "strong",
3831
+ createProvisioningAdapter: (runtimeLayout) => createDirectProvisioningAdapter(runtimeLayout),
2790
3832
  async create(ctx) {
2791
- await mkdir6(ctx.workspaceRoot, { recursive: true });
3833
+ await mkdir7(ctx.workspaceRoot, { recursive: true });
2792
3834
  await copyTemplate(ctx.templatePath, ctx.workspaceRoot);
2793
3835
  const workspace = createNodeWorkspace(ctx.workspaceRoot);
2794
3836
  const sandbox = createDirectSandbox();
@@ -2802,15 +3844,16 @@ var directModeAdapter = {
2802
3844
  };
2803
3845
 
2804
3846
  // src/server/runtime/modes/local.ts
2805
- import { mkdir as mkdir7 } from "fs/promises";
3847
+ import { mkdir as mkdir8 } from "fs/promises";
2806
3848
  var localModeAdapter = {
2807
3849
  id: "local",
2808
3850
  workspaceFsCapability: "strong",
3851
+ createProvisioningAdapter: (runtimeLayout) => createLocalProvisioningAdapter(runtimeLayout),
2809
3852
  async create(ctx) {
2810
3853
  if (process.platform !== "linux") {
2811
3854
  throw new Error("local mode requires Linux with bubblewrap");
2812
3855
  }
2813
- await mkdir7(ctx.workspaceRoot, { recursive: true });
3856
+ await mkdir8(ctx.workspaceRoot, { recursive: true });
2814
3857
  await copyTemplate(ctx.templatePath, ctx.workspaceRoot);
2815
3858
  const workspace = createNodeWorkspace(ctx.workspaceRoot);
2816
3859
  const sandbox = createBwrapSandbox();
@@ -2824,6 +3867,11 @@ var localModeAdapter = {
2824
3867
  };
2825
3868
 
2826
3869
  // src/server/runtime/modes/vercel-sandbox.ts
3870
+ 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";
3873
+ import { fileURLToPath as fileURLToPath6 } from "url";
3874
+ import { promisify as promisify2 } from "util";
2827
3875
  import { Sandbox as VercelSandbox } from "@vercel/sandbox";
2828
3876
 
2829
3877
  // src/server/sandbox/vercel-sandbox/createVercelSandboxExec.ts
@@ -2978,8 +4026,8 @@ function createVercelSandboxExec(sandbox, execOpts = {}) {
2978
4026
  }
2979
4027
 
2980
4028
  // src/server/sandbox/vercel-sandbox/packageTemplate.ts
2981
- import { createHash as createHash4 } from "crypto";
2982
- import { readFile as readFile5, readdir as readdir3 } from "fs/promises";
4029
+ import { createHash as createHash5 } from "crypto";
4030
+ import { readFile as readFile7, readdir as readdir4 } from "fs/promises";
2983
4031
  import path3 from "path";
2984
4032
  import { Readable } from "stream";
2985
4033
  import { createGzip } from "zlib";
@@ -2992,7 +4040,7 @@ function log2(msg, meta = {}) {
2992
4040
  `);
2993
4041
  }
2994
4042
  async function collectFiles(dir, base = "") {
2995
- const entries = await readdir3(dir, { withFileTypes: true });
4043
+ const entries = await readdir4(dir, { withFileTypes: true });
2996
4044
  const files = [];
2997
4045
  for (const entry of entries) {
2998
4046
  const fullPath = path3.join(dir, entry.name);
@@ -3000,13 +4048,13 @@ async function collectFiles(dir, base = "") {
3000
4048
  if (entry.isDirectory()) {
3001
4049
  files.push(...await collectFiles(fullPath, relPath));
3002
4050
  } else if (entry.isFile()) {
3003
- files.push({ rel: relPath, content: await readFile5(fullPath) });
4051
+ files.push({ rel: relPath, content: await readFile7(fullPath) });
3004
4052
  }
3005
4053
  }
3006
4054
  return files.sort((a, b) => a.rel.localeCompare(b.rel));
3007
4055
  }
3008
4056
  function computeTemplateHash(files) {
3009
- const hash = createHash4("sha256");
4057
+ const hash = createHash5("sha256");
3010
4058
  for (const file of files) {
3011
4059
  hash.update(file.rel);
3012
4060
  hash.update("\0");
@@ -3047,11 +4095,11 @@ async function buildTarGz(files) {
3047
4095
  }
3048
4096
  chunks.push(Buffer.alloc(1024));
3049
4097
  const tarBuffer = Buffer.concat(chunks);
3050
- return new Promise((resolve7, reject) => {
4098
+ return new Promise((resolve9, reject) => {
3051
4099
  const gzip = createGzip({ level: 6 });
3052
4100
  const gzChunks = [];
3053
4101
  gzip.on("data", (chunk2) => gzChunks.push(chunk2));
3054
- gzip.on("end", () => resolve7(Buffer.concat(gzChunks)));
4102
+ gzip.on("end", () => resolve9(Buffer.concat(gzChunks)));
3055
4103
  gzip.on("error", reject);
3056
4104
  Readable.from(tarBuffer).pipe(gzip);
3057
4105
  });
@@ -3089,6 +4137,46 @@ async function packageTemplate(templatePath, opts = {}) {
3089
4137
  // src/server/runtime/modes/vercel-sandbox.ts
3090
4138
  var ORPHAN_GUARD_MAX_IDLE_MS = 24 * 60 * 60 * 1e3;
3091
4139
  var VERCEL_SANDBOX_TIMEOUT_MS_ENV = "BORING_AGENT_VERCEL_SANDBOX_TIMEOUT_MS";
4140
+ var VERCEL_SANDBOX_RUNTIME_ENV = "BORING_AGENT_VERCEL_SANDBOX_RUNTIME";
4141
+ var DEFAULT_VERCEL_SANDBOX_RUNTIME = "node24";
4142
+ var execFileAsync2 = promisify2(execFile2);
4143
+ function sandboxTelemetryProperties(ctx, extra = {}) {
4144
+ return {
4145
+ runtimeMode: "vercel-sandbox",
4146
+ workspaceId: ctx?.workspaceId,
4147
+ sessionId: ctx?.sessionId,
4148
+ requestId: ctx?.requestId,
4149
+ ...extra
4150
+ };
4151
+ }
4152
+ function captureSandboxSetupEvent(telemetry, ctx, name, properties) {
4153
+ if (!telemetry) return;
4154
+ safeCapture(telemetry, {
4155
+ name,
4156
+ properties: sandboxTelemetryProperties(ctx, properties)
4157
+ });
4158
+ }
4159
+ async function runSandboxSetupStep(options) {
4160
+ const startedAt = Date.now();
4161
+ try {
4162
+ const result = await options.run();
4163
+ captureSandboxSetupEvent(options.telemetry, options.ctx, "agent.runtime.sandbox.setup.step", {
4164
+ phase: options.phase,
4165
+ status: "ok",
4166
+ durationMs: Date.now() - startedAt
4167
+ });
4168
+ return result;
4169
+ } catch (error) {
4170
+ const code = error?.code;
4171
+ captureSandboxSetupEvent(options.telemetry, options.ctx, "agent.runtime.sandbox.setup.step", {
4172
+ phase: options.phase,
4173
+ status: "error",
4174
+ durationMs: Date.now() - startedAt,
4175
+ ...typeof code === "string" ? { errorCode: code } : {}
4176
+ });
4177
+ throw error;
4178
+ }
4179
+ }
3092
4180
  function createDefaultVercelClient(auth, opts = {}) {
3093
4181
  const credentials = {
3094
4182
  token: auth.token,
@@ -3101,12 +4189,14 @@ function createDefaultVercelClient(auth, opts = {}) {
3101
4189
  const base = {
3102
4190
  ...createOptions,
3103
4191
  ...params?.name ? { name: params.name } : {},
4192
+ ...opts.runtime ? { runtime: opts.runtime } : {},
3104
4193
  persistent: params?.persistent ?? true,
3105
4194
  snapshotExpiration: params?.snapshotExpiration ?? 0
3106
4195
  };
3107
4196
  if (params?.source?.type === "snapshot") {
4197
+ const { runtime: _runtime, ...snapshotBase } = base;
3108
4198
  return await VercelSandbox.create({
3109
- ...base,
4199
+ ...snapshotBase,
3110
4200
  source: { type: "snapshot", snapshotId: params.source.snapshotId }
3111
4201
  });
3112
4202
  }
@@ -3178,6 +4268,161 @@ async function seedTemplateIntoVercelWorkspace(workspace, templatePath, logger)
3178
4268
  fileCount: files.length
3179
4269
  });
3180
4270
  }
4271
+ function provisioningSourceToPath(source) {
4272
+ return source instanceof URL ? fileURLToPath6(source) : source;
4273
+ }
4274
+ async function prepareVercelProvisioningArtifact(request) {
4275
+ const sourcePath = provisioningSourceToPath(request.source);
4276
+ await mkdir9(dirname9(request.outputPath), { recursive: true });
4277
+ if (request.kind === "node") {
4278
+ const { stdout } = await execFileAsync2("npm", [
4279
+ "pack",
4280
+ sourcePath,
4281
+ "--pack-destination",
4282
+ dirname9(request.outputPath)
4283
+ ], { maxBuffer: 1024 * 1024 * 20 });
4284
+ 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);
4287
+ return;
4288
+ }
4289
+ await execFileAsync2("tar", ["-czf", request.outputPath, "-C", sourcePath, "."], {
4290
+ maxBuffer: 1024 * 1024 * 20
4291
+ });
4292
+ }
4293
+ function shellSingleQuote(value) {
4294
+ return `'${value.replace(/'/g, `'\\''`)}'`;
4295
+ }
4296
+ async function copyHostPathToVercelWorkspace(options) {
4297
+ const sourcePath = provisioningSourceToPath(options.source);
4298
+ const sourceStat = await stat7(sourcePath);
4299
+ if (sourceStat.isDirectory()) {
4300
+ await options.workspace.mkdir(options.targetRel, { recursive: true });
4301
+ for (const entry of await readdir5(sourcePath, { withFileTypes: true })) {
4302
+ await copyHostPathToVercelWorkspace({
4303
+ workspace: options.workspace,
4304
+ source: join8(sourcePath, entry.name),
4305
+ targetRel: `${options.targetRel}/${entry.name}`
4306
+ });
4307
+ }
4308
+ return;
4309
+ }
4310
+ if (!sourceStat.isFile()) return;
4311
+ const bytes = new Uint8Array(await readFile8(sourcePath));
4312
+ if (options.workspace.writeBinaryFile) await options.workspace.writeBinaryFile(options.targetRel, bytes);
4313
+ else await options.workspace.writeFile(options.targetRel, Buffer.from(bytes).toString("utf8"));
4314
+ }
4315
+ function isMissingWorkspacePathError(error) {
4316
+ if (error.code === "ENOENT") return true;
4317
+ const message = error instanceof Error ? error.message : String(error);
4318
+ return /\bENOENT\b|no such file|file not found/i.test(message);
4319
+ }
4320
+ function createVercelWorkspaceFs(options) {
4321
+ const exists2 = async (rel) => {
4322
+ try {
4323
+ await options.workspace.stat(rel);
4324
+ return true;
4325
+ } catch (error) {
4326
+ if (isMissingWorkspacePathError(error)) return false;
4327
+ throw error;
4328
+ }
4329
+ };
4330
+ return {
4331
+ exists: exists2,
4332
+ async rm(rel) {
4333
+ const targetExists = await exists2(rel);
4334
+ if (!targetExists) return;
4335
+ const target = `${VERCEL_SANDBOX_REMOTE_ROOT}/${rel.replace(/^\/+/, "")}`;
4336
+ const result = await options.sandbox.runCommand({
4337
+ cmd: "sh",
4338
+ args: ["-c", `rm -rf -- ${shellSingleQuote(target)}`]
4339
+ });
4340
+ if ((result.exitCode ?? 1) !== 0) {
4341
+ const err = await (result.stderr?.() ?? Promise.resolve(""));
4342
+ throw new Error(err || `failed to remove ${rel}`);
4343
+ }
4344
+ },
4345
+ async mkdir(rel) {
4346
+ await options.workspace.mkdir(rel, { recursive: true });
4347
+ },
4348
+ async writeText(rel, content) {
4349
+ await options.workspace.writeFile(rel, content);
4350
+ },
4351
+ async readText(rel) {
4352
+ try {
4353
+ return await options.workspace.readFile(rel);
4354
+ } catch (error) {
4355
+ if (isMissingWorkspacePathError(error)) return null;
4356
+ throw error;
4357
+ }
4358
+ },
4359
+ async copyFromHost(source, targetRel) {
4360
+ await copyHostPathToVercelWorkspace({ workspace: options.workspace, source, targetRel });
4361
+ }
4362
+ };
4363
+ }
4364
+ async function ensureVercelProvisioningParts(options) {
4365
+ const auth = resolveVercelAuth(options.getEnvVar);
4366
+ const teamId = requireEnvVar("VERCEL_TEAM_ID", options.getEnvVar);
4367
+ const projectId = options.getEnvVar("VERCEL_PROJECT_ID")?.trim();
4368
+ const timeoutMs = readOptionalPositiveIntegerEnv(VERCEL_SANDBOX_TIMEOUT_MS_ENV, options.getEnvVar);
4369
+ const runtime = options.getEnvVar(VERCEL_SANDBOX_RUNTIME_ENV)?.trim() || DEFAULT_VERCEL_SANDBOX_RUNTIME;
4370
+ const vercelClient = options.vercelClient ?? createDefaultVercelClient({
4371
+ token: auth.token,
4372
+ teamId,
4373
+ projectId
4374
+ }, { timeoutMs, runtime });
4375
+ const workspaceId = options.ctx?.workspaceId ?? options.ctx?.workspaceRoot ?? options.runtimeLayout.workspaceRoot;
4376
+ const telemetry = options.ctx?.telemetry;
4377
+ const totalStartedAt = Date.now();
4378
+ captureSandboxSetupEvent(telemetry, options.ctx, "agent.runtime.sandbox.setup.started", {
4379
+ status: "started"
4380
+ });
4381
+ try {
4382
+ const sandboxHandle = await runSandboxSetupStep({
4383
+ telemetry,
4384
+ ctx: options.ctx,
4385
+ phase: "resolve-handle",
4386
+ run: async () => await resolveSandboxHandle(workspaceId, options.store, vercelClient, {
4387
+ logger: options.logger,
4388
+ maxIdleMs: options.orphanGuardMaxIdleMs === null ? void 0 : options.orphanGuardMaxIdleMs ?? ORPHAN_GUARD_MAX_IDLE_MS,
4389
+ expiredSandboxPolicy: options.expiredSandboxPolicy
4390
+ })
4391
+ });
4392
+ await runSandboxSetupStep({
4393
+ telemetry,
4394
+ ctx: options.ctx,
4395
+ phase: "ensure-workspace-root",
4396
+ run: async () => {
4397
+ await ensureVercelWorkspaceRoot(sandboxHandle);
4398
+ }
4399
+ });
4400
+ const workspace = createVercelSandboxWorkspace(sandboxHandle);
4401
+ const workspaceFs = createVercelWorkspaceFs({ workspace, sandbox: sandboxHandle });
4402
+ const sandbox = createVercelSandboxExec(sandboxHandle);
4403
+ await runSandboxSetupStep({
4404
+ telemetry,
4405
+ ctx: options.ctx,
4406
+ phase: "sandbox-init",
4407
+ run: async () => {
4408
+ await sandbox.init?.({ workspace, sessionId: options.ctx?.sessionId ?? "default" });
4409
+ }
4410
+ });
4411
+ captureSandboxSetupEvent(telemetry, options.ctx, "agent.runtime.sandbox.setup.completed", {
4412
+ status: "ok",
4413
+ durationMs: Date.now() - totalStartedAt
4414
+ });
4415
+ return { workspace, workspaceFs, sandbox, sandboxHandle };
4416
+ } catch (error) {
4417
+ const code = error?.code;
4418
+ captureSandboxSetupEvent(telemetry, options.ctx, "agent.runtime.sandbox.setup.failed", {
4419
+ status: "error",
4420
+ durationMs: Date.now() - totalStartedAt,
4421
+ ...typeof code === "string" ? { errorCode: code } : {}
4422
+ });
4423
+ throw error;
4424
+ }
4425
+ }
3181
4426
  async function ensureTemplateExecutables(sandbox) {
3182
4427
  if (!sandbox.runCommand) return;
3183
4428
  await sandbox.runCommand({
@@ -3233,7 +4478,67 @@ function createVercelSandboxModeAdapter(opts = {}) {
3233
4478
  async dispose() {
3234
4479
  await snapshotScheduler?.shutdown();
3235
4480
  },
4481
+ createProvisioningAdapter(runtimeLayout, ctx) {
4482
+ let partsPromise = null;
4483
+ const getParts = () => {
4484
+ partsPromise ??= ensureVercelProvisioningParts({
4485
+ store,
4486
+ getEnvVar,
4487
+ logger,
4488
+ vercelClient: opts.vercelClient,
4489
+ runtimeLayout,
4490
+ ctx,
4491
+ expiredSandboxPolicy: opts.expiredSandboxPolicy,
4492
+ orphanGuardMaxIdleMs: opts.orphanGuardMaxIdleMs
4493
+ });
4494
+ return partsPromise;
4495
+ };
4496
+ return createVercelProvisioningAdapter({
4497
+ runtimeLayout,
4498
+ workspaceFs: {
4499
+ async exists(rel) {
4500
+ return await (await getParts()).workspaceFs.exists(rel);
4501
+ },
4502
+ async rm(rel) {
4503
+ return await (await getParts()).workspaceFs.rm(rel);
4504
+ },
4505
+ async mkdir(rel) {
4506
+ return await (await getParts()).workspaceFs.mkdir(rel);
4507
+ },
4508
+ async writeText(rel, content) {
4509
+ return await (await getParts()).workspaceFs.writeText(rel, content);
4510
+ },
4511
+ async readText(rel) {
4512
+ return await (await getParts()).workspaceFs.readText(rel);
4513
+ },
4514
+ async copyFromHost(source, target) {
4515
+ return await (await getParts()).workspaceFs.copyFromHost(source, target);
4516
+ }
4517
+ },
4518
+ async exec(command, args, execOpts) {
4519
+ const { sandbox } = await getParts();
4520
+ const commandLine = [command, ...args].map(shellSingleQuote).join(" ");
4521
+ const result = await sandbox.exec(commandLine, {
4522
+ cwd: execOpts?.cwd,
4523
+ env: execOpts?.env,
4524
+ timeoutMs: execOpts?.timeoutMs
4525
+ });
4526
+ const stdout = Buffer.from(result.stdout).toString("utf8");
4527
+ const stderr = Buffer.from(result.stderr).toString("utf8");
4528
+ if (result.exitCode !== 0) {
4529
+ throw new Error(`Command failed (${command}) with exit code ${result.exitCode}${stderr.trim() ? `: ${stderr.trim()}` : ""}`);
4530
+ }
4531
+ return { stdout, stderr };
4532
+ },
4533
+ prepareArtifact: prepareVercelProvisioningArtifact
4534
+ });
4535
+ },
3236
4536
  async create(ctx) {
4537
+ const telemetry = ctx.telemetry;
4538
+ const totalStartedAt = Date.now();
4539
+ captureSandboxSetupEvent(telemetry, ctx, "agent.runtime.sandbox.setup.started", {
4540
+ status: "started"
4541
+ });
3237
4542
  const auth = resolveVercelAuth(getEnvVar);
3238
4543
  const teamId = requireEnvVar("VERCEL_TEAM_ID", getEnvVar);
3239
4544
  const projectId = getEnvVar("VERCEL_PROJECT_ID")?.trim();
@@ -3241,80 +4546,127 @@ function createVercelSandboxModeAdapter(opts = {}) {
3241
4546
  VERCEL_SANDBOX_TIMEOUT_MS_ENV,
3242
4547
  getEnvVar
3243
4548
  );
4549
+ const runtime = getEnvVar(VERCEL_SANDBOX_RUNTIME_ENV)?.trim() || DEFAULT_VERCEL_SANDBOX_RUNTIME;
3244
4550
  const vercelClient = opts.vercelClient ?? createDefaultVercelClient({
3245
4551
  token: auth.token,
3246
4552
  teamId,
3247
4553
  projectId
3248
- }, { timeoutMs });
4554
+ }, { timeoutMs, runtime });
3249
4555
  logger.info("[vercel-sandbox:mode] auth resolved", {
3250
4556
  source: auth.source,
3251
4557
  hasProjectId: Boolean(projectId),
3252
- timeoutMs: timeoutMs ?? null
4558
+ timeoutMs: timeoutMs ?? null,
4559
+ runtime
3253
4560
  });
3254
4561
  const workspaceId = ctx.workspaceId ?? ctx.workspaceRoot;
3255
4562
  let tarballUrl;
3256
- if (ctx.templatePath) {
3257
- try {
3258
- const result = await packageTemplate(ctx.templatePath, opts.packageTemplateOpts);
3259
- tarballUrl = result.url;
3260
- logger.info("[vercel-sandbox:mode] template packaged", {
3261
- hash: result.hash,
3262
- url: result.url
3263
- });
3264
- } catch (error) {
3265
- logger.info("[vercel-sandbox:mode] template packaging failed, will use writeFiles fallback", {
3266
- error: error instanceof Error ? error.message : String(error)
3267
- });
3268
- }
3269
- }
3270
- const sandboxHandle = await resolveSandboxHandle(
3271
- workspaceId,
3272
- store,
3273
- vercelClient,
3274
- {
3275
- tarballUrl,
3276
- logger,
3277
- maxIdleMs: opts.orphanGuardMaxIdleMs === null ? void 0 : opts.orphanGuardMaxIdleMs ?? ORPHAN_GUARD_MAX_IDLE_MS,
3278
- expiredSandboxPolicy: opts.expiredSandboxPolicy
4563
+ try {
4564
+ if (ctx.templatePath) {
4565
+ try {
4566
+ const result = await runSandboxSetupStep({
4567
+ telemetry,
4568
+ ctx,
4569
+ phase: "template-package",
4570
+ run: async () => await packageTemplate(ctx.templatePath, opts.packageTemplateOpts)
4571
+ });
4572
+ tarballUrl = result.url;
4573
+ logger.info("[vercel-sandbox:mode] template packaged", {
4574
+ hash: result.hash,
4575
+ url: result.url
4576
+ });
4577
+ } catch (error) {
4578
+ logger.info("[vercel-sandbox:mode] template packaging failed, will use writeFiles fallback", {
4579
+ error: error instanceof Error ? error.message : String(error)
4580
+ });
4581
+ }
3279
4582
  }
3280
- );
3281
- const sandboxId = sandboxHandle.name ?? sandboxHandle.sandboxId ?? "unknown-sandbox";
3282
- logger.info("[vercel-sandbox:mode] resolved sandbox handle", {
3283
- workspaceId,
3284
- sandboxId,
3285
- snapshotId: sandboxHandle.currentSnapshotId ?? sandboxHandle.sourceSnapshotId ?? null,
3286
- tarballUrl: tarballUrl ?? null
3287
- });
3288
- if (snapshotScheduler && sandboxHandle.sandboxId) {
3289
- snapshotScheduler.trackWorkspace({
4583
+ const sandboxHandle = await runSandboxSetupStep({
4584
+ telemetry,
4585
+ ctx,
4586
+ phase: "resolve-handle",
4587
+ run: async () => await resolveSandboxHandle(
4588
+ workspaceId,
4589
+ store,
4590
+ vercelClient,
4591
+ {
4592
+ tarballUrl,
4593
+ logger,
4594
+ maxIdleMs: opts.orphanGuardMaxIdleMs === null ? void 0 : opts.orphanGuardMaxIdleMs ?? ORPHAN_GUARD_MAX_IDLE_MS,
4595
+ expiredSandboxPolicy: opts.expiredSandboxPolicy
4596
+ }
4597
+ )
4598
+ });
4599
+ const sandboxId = sandboxHandle.name ?? sandboxHandle.sandboxId ?? "unknown-sandbox";
4600
+ logger.info("[vercel-sandbox:mode] resolved sandbox handle", {
3290
4601
  workspaceId,
3291
- sandbox: sandboxHandle,
3292
- store
4602
+ sandboxId,
4603
+ snapshotId: sandboxHandle.currentSnapshotId ?? sandboxHandle.sourceSnapshotId ?? null,
4604
+ tarballUrl: tarballUrl ?? null
3293
4605
  });
3294
- }
3295
- const markDirty = () => snapshotScheduler?.markDirty(workspaceId);
3296
- const workspace = createVercelSandboxWorkspace(sandboxHandle, {
3297
- onMutation: markDirty
3298
- });
3299
- await ensureVercelWorkspaceRoot(sandboxHandle);
3300
- if (ctx.templatePath) {
3301
- if (!tarballUrl) {
3302
- logger.info("[vercel-sandbox:mode] falling back to writeFiles for template", {
3303
- templatePath: ctx.templatePath
4606
+ if (snapshotScheduler && sandboxHandle.sandboxId) {
4607
+ snapshotScheduler.trackWorkspace({
4608
+ workspaceId,
4609
+ sandbox: sandboxHandle,
4610
+ store
4611
+ });
4612
+ }
4613
+ const markDirty = () => snapshotScheduler?.markDirty(workspaceId);
4614
+ const workspace = createVercelSandboxWorkspace(sandboxHandle, {
4615
+ onMutation: markDirty
4616
+ });
4617
+ await runSandboxSetupStep({
4618
+ telemetry,
4619
+ ctx,
4620
+ phase: "ensure-workspace-root",
4621
+ run: async () => {
4622
+ await ensureVercelWorkspaceRoot(sandboxHandle);
4623
+ }
4624
+ });
4625
+ if (ctx.templatePath) {
4626
+ if (!tarballUrl) {
4627
+ logger.info("[vercel-sandbox:mode] falling back to writeFiles for template", {
4628
+ templatePath: ctx.templatePath
4629
+ });
4630
+ }
4631
+ await runSandboxSetupStep({
4632
+ telemetry,
4633
+ ctx,
4634
+ phase: "template-seed",
4635
+ run: async () => {
4636
+ await seedTemplateIntoVercelWorkspace(workspace, ctx.templatePath, logger);
4637
+ await ensureTemplateExecutables(sandboxHandle);
4638
+ }
3304
4639
  });
3305
4640
  }
3306
- await seedTemplateIntoVercelWorkspace(workspace, ctx.templatePath, logger);
3307
- await ensureTemplateExecutables(sandboxHandle);
4641
+ const sandbox = createVercelSandboxExec(sandboxHandle, {
4642
+ onMutation: markDirty
4643
+ });
4644
+ await runSandboxSetupStep({
4645
+ telemetry,
4646
+ ctx,
4647
+ phase: "sandbox-init",
4648
+ run: async () => {
4649
+ await sandbox.init?.({ workspace, sessionId: ctx.sessionId });
4650
+ }
4651
+ });
4652
+ captureSandboxSetupEvent(telemetry, ctx, "agent.runtime.sandbox.setup.completed", {
4653
+ status: "ok",
4654
+ durationMs: Date.now() - totalStartedAt
4655
+ });
4656
+ return {
4657
+ workspace,
4658
+ sandbox,
4659
+ fileSearch: createServerFileSearch(workspace, sandbox)
4660
+ };
4661
+ } catch (error) {
4662
+ const code = error?.code;
4663
+ captureSandboxSetupEvent(telemetry, ctx, "agent.runtime.sandbox.setup.failed", {
4664
+ status: "error",
4665
+ durationMs: Date.now() - totalStartedAt,
4666
+ ...typeof code === "string" ? { errorCode: code } : {}
4667
+ });
4668
+ throw error;
3308
4669
  }
3309
- const sandbox = createVercelSandboxExec(sandboxHandle, {
3310
- onMutation: markDirty
3311
- });
3312
- await sandbox.init?.({ workspace, sessionId: ctx.sessionId });
3313
- return {
3314
- workspace,
3315
- sandbox,
3316
- fileSearch: createServerFileSearch(workspace, sandbox)
3317
- };
3318
4670
  }
3319
4671
  };
3320
4672
  }
@@ -3358,8 +4710,8 @@ import Fastify from "fastify";
3358
4710
 
3359
4711
  // src/server/harness/pi-coding-agent/createHarness.ts
3360
4712
  import { existsSync, readFileSync as readFileSync2 } from "fs";
3361
- import { mkdir as mkdir9, writeFile as writeFile7 } from "fs/promises";
3362
- import { extname as extname2, join as join6 } from "path";
4713
+ import { mkdir as mkdir11, writeFile as writeFile7 } from "fs/promises";
4714
+ import { extname as extname2, join as join10 } from "path";
3363
4715
  import {
3364
4716
  createAgentSession,
3365
4717
  SessionManager,
@@ -3372,7 +4724,20 @@ import {
3372
4724
  } from "@mariozechner/pi-coding-agent";
3373
4725
 
3374
4726
  // src/server/harness/pi-coding-agent/tool-adapter.ts
3375
- function adaptToolForPi(tool, sessionId) {
4727
+ function toolTelemetryProperties(toolName2, sessionId, status, startedAt, result) {
4728
+ const properties = {
4729
+ toolName: toolName2,
4730
+ status,
4731
+ durationMs: Date.now() - startedAt
4732
+ };
4733
+ if (sessionId) properties.sessionId = sessionId;
4734
+ const errorCode = result?.details?.code;
4735
+ if (status === "error") {
4736
+ properties.errorCode = ErrorCode.safeParse(errorCode).success ? errorCode : ErrorCode.enum.TOOL_EXECUTION_ERROR;
4737
+ }
4738
+ return properties;
4739
+ }
4740
+ function adaptToolForPi(tool, sessionId, telemetry = noopTelemetry) {
3376
4741
  return {
3377
4742
  name: tool.name,
3378
4743
  label: tool.name,
@@ -3380,24 +4745,47 @@ function adaptToolForPi(tool, sessionId) {
3380
4745
  parameters: tool.parameters,
3381
4746
  promptSnippet: tool.promptSnippet ?? tool.description,
3382
4747
  async execute(toolCallId, params, signal, onUpdate, _ctx) {
3383
- const result = await tool.execute(params, {
3384
- toolCallId,
3385
- abortSignal: signal ?? new AbortController().signal,
3386
- onUpdate: onUpdate ? (partial) => onUpdate({ content: [{ type: "text", text: partial }], details: void 0 }) : void 0,
3387
- sessionId
3388
- });
3389
- if (result.isError) {
3390
- throw new Error(result.content.map((c) => c.text).join("\n"));
4748
+ const startedAt = Date.now();
4749
+ let emittedFailure = false;
4750
+ try {
4751
+ const result = await tool.execute(params, {
4752
+ toolCallId,
4753
+ abortSignal: signal ?? new AbortController().signal,
4754
+ onUpdate: onUpdate ? (partial) => onUpdate({ content: [{ type: "text", text: partial }], details: void 0 }) : void 0,
4755
+ sessionId
4756
+ });
4757
+ safeCapture(telemetry, {
4758
+ name: result.isError ? "agent.tool.failed" : "agent.tool.completed",
4759
+ properties: toolTelemetryProperties(
4760
+ tool.name,
4761
+ sessionId,
4762
+ result.isError ? "error" : "ok",
4763
+ startedAt,
4764
+ result
4765
+ )
4766
+ });
4767
+ if (result.isError) {
4768
+ emittedFailure = true;
4769
+ throw new Error(result.content.map((c) => c.text).join("\n"));
4770
+ }
4771
+ return {
4772
+ content: result.content,
4773
+ details: result.details
4774
+ };
4775
+ } catch (error) {
4776
+ if (!emittedFailure) {
4777
+ safeCapture(telemetry, {
4778
+ name: "agent.tool.failed",
4779
+ properties: toolTelemetryProperties(tool.name, sessionId, "error", startedAt)
4780
+ });
4781
+ }
4782
+ throw error;
3391
4783
  }
3392
- return {
3393
- content: result.content,
3394
- details: result.details
3395
- };
3396
4784
  }
3397
4785
  };
3398
4786
  }
3399
- function adaptToolsForPi(tools, sessionId) {
3400
- return tools.map((tool) => adaptToolForPi(tool, sessionId));
4787
+ function adaptToolsForPi(tools, sessionId, telemetry) {
4788
+ return tools.map((tool) => adaptToolForPi(tool, sessionId, telemetry));
3401
4789
  }
3402
4790
 
3403
4791
  // src/server/harness/pi-coding-agent/stream-adapter.ts
@@ -3585,17 +4973,17 @@ function piEventToChunks(event) {
3585
4973
  // src/server/harness/pi-coding-agent/sessions.ts
3586
4974
  import { randomUUID } from "crypto";
3587
4975
  import {
3588
- readdir as readdir4,
3589
- readFile as readFile6,
4976
+ readdir as readdir6,
4977
+ readFile as readFile9,
3590
4978
  stat as fsStat,
3591
- rm,
3592
- mkdir as mkdir8,
4979
+ rm as rm3,
4980
+ mkdir as mkdir10,
3593
4981
  writeFile as writeFile6,
3594
4982
  appendFile,
3595
4983
  utimes
3596
4984
  } from "fs/promises";
3597
4985
  import { readFileSync, readdirSync } from "fs";
3598
- import { join as join5 } from "path";
4986
+ import { join as join9 } from "path";
3599
4987
  import { homedir as homedir3 } from "os";
3600
4988
  import {
3601
4989
  parseSessionEntries,
@@ -3604,7 +4992,7 @@ import {
3604
4992
  } from "@mariozechner/pi-coding-agent";
3605
4993
  function defaultSessionDir(cwd) {
3606
4994
  const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
3607
- return join5(homedir3(), ".pi", "agent", "sessions", safePath);
4995
+ return join9(homedir3(), ".pi", "agent", "sessions", safePath);
3608
4996
  }
3609
4997
  var SAFE_ID = /^[a-zA-Z0-9_-]+$/;
3610
4998
  var SAFE_SESSION_NAMESPACE = /^[a-zA-Z0-9_-]+$/;
@@ -3613,7 +5001,7 @@ function sessionDirForNamespace(namespace) {
3613
5001
  if (!SAFE_SESSION_NAMESPACE.test(safeNamespace)) {
3614
5002
  throw new Error("session namespace must contain only letters, numbers, underscores, and dashes");
3615
5003
  }
3616
- return join5(homedir3(), ".pi", "agent", "sessions", safeNamespace);
5004
+ return join9(homedir3(), ".pi", "agent", "sessions", safeNamespace);
3617
5005
  }
3618
5006
  var PiSessionStore = class {
3619
5007
  cwd;
@@ -3627,15 +5015,15 @@ var PiSessionStore = class {
3627
5015
  this.sessionDir = options?.sessionDir ?? (options?.sessionNamespace ? sessionDirForNamespace(options.sessionNamespace) : defaultSessionDir(cwd));
3628
5016
  }
3629
5017
  async list(ctx) {
3630
- const files = await readdir4(this.sessionDir).catch(() => []);
5018
+ const files = await readdir6(this.sessionDir).catch(() => []);
3631
5019
  const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
3632
5020
  const summaries = await Promise.all(
3633
- jsonlFiles.map((f) => this.summarizeFile(join5(this.sessionDir, f)))
5021
+ jsonlFiles.map((f) => this.summarizeFile(join9(this.sessionDir, f)))
3634
5022
  );
3635
5023
  return summaries.filter((s) => s !== null).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
3636
5024
  }
3637
5025
  async create(ctx, init) {
3638
- await mkdir8(this.sessionDir, { recursive: true });
5026
+ await mkdir10(this.sessionDir, { recursive: true });
3639
5027
  const id = randomUUID();
3640
5028
  const now = (/* @__PURE__ */ new Date()).toISOString();
3641
5029
  const header = {
@@ -3656,7 +5044,7 @@ var PiSessionStore = class {
3656
5044
  };
3657
5045
  lines.push(JSON.stringify(infoEntry));
3658
5046
  }
3659
- const filepath = join5(this.sessionDir, `${id}.jsonl`);
5047
+ const filepath = join9(this.sessionDir, `${id}.jsonl`);
3660
5048
  await writeFile6(filepath, lines.join("\n") + "\n", "utf-8");
3661
5049
  return {
3662
5050
  id,
@@ -3670,7 +5058,7 @@ var PiSessionStore = class {
3670
5058
  const filepath = await this.resolveSessionFile(sessionId);
3671
5059
  let content;
3672
5060
  try {
3673
- content = await readFile6(filepath, "utf-8");
5061
+ content = await readFile9(filepath, "utf-8");
3674
5062
  } catch {
3675
5063
  throw new Error(`Session not found: ${sessionId}`);
3676
5064
  }
@@ -3713,7 +5101,7 @@ var PiSessionStore = class {
3713
5101
  loadPiSessionFileSync(sessionId) {
3714
5102
  if (!SAFE_ID.test(sessionId)) return null;
3715
5103
  try {
3716
- const direct = join5(this.sessionDir, `${sessionId}.jsonl`);
5104
+ const direct = join9(this.sessionDir, `${sessionId}.jsonl`);
3717
5105
  let content;
3718
5106
  try {
3719
5107
  content = readFileSync(direct, "utf-8");
@@ -3722,7 +5110,7 @@ var PiSessionStore = class {
3722
5110
  (f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
3723
5111
  );
3724
5112
  if (files.length === 0) return null;
3725
- content = readFileSync(join5(this.sessionDir, files[0]), "utf-8");
5113
+ content = readFileSync(join9(this.sessionDir, files[0]), "utf-8");
3726
5114
  }
3727
5115
  const entries = safeParseEntries(content);
3728
5116
  let piFilePath = null;
@@ -3740,7 +5128,7 @@ var PiSessionStore = class {
3740
5128
  async loadPiSessionFile(_ctx, sessionId) {
3741
5129
  try {
3742
5130
  const filepath = await this.resolveSessionFile(sessionId);
3743
- const content = await readFile6(filepath, "utf-8");
5131
+ const content = await readFile9(filepath, "utf-8");
3744
5132
  const entries = safeParseEntries(content);
3745
5133
  let piFilePath = null;
3746
5134
  for (const e of entries) {
@@ -3767,7 +5155,7 @@ var PiSessionStore = class {
3767
5155
  const filepath = await this.resolveSessionFile(sessionId).catch(
3768
5156
  () => null
3769
5157
  );
3770
- if (filepath) await rm(filepath, { force: true });
5158
+ if (filepath) await rm3(filepath, { force: true });
3771
5159
  }
3772
5160
  touchSession(sessionId, title) {
3773
5161
  this.resolveSessionFile(sessionId).then((filepath) => {
@@ -3790,24 +5178,24 @@ var PiSessionStore = class {
3790
5178
  if (!SAFE_ID.test(sessionId)) {
3791
5179
  throw new Error(`Session not found: ${sessionId}`);
3792
5180
  }
3793
- const direct = join5(this.sessionDir, `${sessionId}.jsonl`);
5181
+ const direct = join9(this.sessionDir, `${sessionId}.jsonl`);
3794
5182
  try {
3795
5183
  await fsStat(direct);
3796
5184
  return direct;
3797
5185
  } catch {
3798
5186
  }
3799
- const files = await readdir4(this.sessionDir).catch(() => []);
5187
+ const files = await readdir6(this.sessionDir).catch(() => []);
3800
5188
  const match = files.find(
3801
5189
  (f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
3802
5190
  );
3803
5191
  if (!match) throw new Error(`Session not found: ${sessionId}`);
3804
- return join5(this.sessionDir, match);
5192
+ return join9(this.sessionDir, match);
3805
5193
  }
3806
5194
  async summarizeFile(filepath) {
3807
5195
  try {
3808
5196
  const [fileStat, content] = await Promise.all([
3809
5197
  fsStat(filepath),
3810
- readFile6(filepath, "utf-8")
5198
+ readFile9(filepath, "utf-8")
3811
5199
  ]);
3812
5200
  const firstNewline = content.indexOf("\n");
3813
5201
  if (firstNewline === -1) return null;
@@ -3957,7 +5345,7 @@ var DEFAULT_POLL_MS = 250;
3957
5345
  var MAX_PROMPT_CHARS = 1200;
3958
5346
  var MAX_TITLE_CHARS = 80;
3959
5347
  function sleep(ms) {
3960
- return new Promise((resolve7) => setTimeout(resolve7, ms));
5348
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
3961
5349
  }
3962
5350
  function truncateForPrompt(text) {
3963
5351
  const trimmed = text.trim();
@@ -4378,8 +5766,8 @@ function mergeInjectedProjectPackages(settingsJson, piPackages) {
4378
5766
  }
4379
5767
  function createResourceSettingsManager(cwd, agentDir, piPackages) {
4380
5768
  if (piPackages.length === 0) return SettingsManager2.create(cwd, agentDir);
4381
- const globalSettingsPath = join6(agentDir, "settings.json");
4382
- const projectSettingsPath = join6(cwd, ".pi", "settings.json");
5769
+ const globalSettingsPath = join10(agentDir, "settings.json");
5770
+ const projectSettingsPath = join10(cwd, ".pi", "settings.json");
4383
5771
  let globalSettingsOverrideJson;
4384
5772
  let projectSettingsOverrideJson;
4385
5773
  const storage = {
@@ -4532,7 +5920,7 @@ function createPiCodingAgentHarness(opts) {
4532
5920
  const { session: piSession } = await createAgentSession({
4533
5921
  cwd: ctx.workdir,
4534
5922
  tools: [],
4535
- customTools: adaptToolsForPi(opts.tools, input.sessionId),
5923
+ customTools: adaptToolsForPi(opts.tools, input.sessionId, opts.telemetry),
4536
5924
  model,
4537
5925
  thinkingLevel: input.thinkingLevel ?? "off",
4538
5926
  sessionManager,
@@ -4980,8 +6368,8 @@ function createPiCodingAgentHarness(opts) {
4980
6368
  const base = basenameForAttachment(a.filename ?? "image");
4981
6369
  const unique = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
4982
6370
  const relPath = `${DEFAULT_ATTACHMENT_DIR}/${base}-${unique}.${ext}`;
4983
- await mkdir9(join6(ctx.workdir, DEFAULT_ATTACHMENT_DIR), { recursive: true });
4984
- await writeFile7(join6(ctx.workdir, relPath), bytes);
6371
+ await mkdir11(join10(ctx.workdir, DEFAULT_ATTACHMENT_DIR), { recursive: true });
6372
+ await writeFile7(join10(ctx.workdir, relPath), bytes);
4985
6373
  savedPaths.push(relPath);
4986
6374
  } catch (err) {
4987
6375
  const msg = err instanceof Error ? err.message : String(err);
@@ -5070,25 +6458,25 @@ ${attachmentNotes.join("\n")}` : message,
5070
6458
  }
5071
6459
 
5072
6460
  // src/server/harness/pi-coding-agent/pluginLoader.ts
5073
- import { readdir as readdir5, stat as stat5, readFile as readFile7 } from "fs/promises";
5074
- import { join as join7, extname as extname3, resolve as resolve4, sep as sep3, relative as relative6 } from "path";
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";
5075
6463
  import { homedir as homedir4 } from "os";
5076
- import { pathToFileURL } from "url";
6464
+ import { pathToFileURL as pathToFileURL2 } from "url";
5077
6465
  var VALID_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs"]);
5078
- var GLOBAL_DIR = join7(homedir4(), ".pi", "agent", "extensions");
6466
+ var GLOBAL_DIR = join11(homedir4(), ".pi", "agent", "extensions");
5079
6467
  var LOCAL_DIR = ".pi/extensions";
5080
6468
  var EXTENSIONS_JSON = ".pi/extensions.json";
5081
6469
  async function dirExists(path4) {
5082
6470
  try {
5083
- const s = await stat5(path4);
6471
+ const s = await stat8(path4);
5084
6472
  return s.isDirectory();
5085
6473
  } catch {
5086
6474
  return false;
5087
6475
  }
5088
6476
  }
5089
- async function fileExists2(path4) {
6477
+ async function fileExists(path4) {
5090
6478
  try {
5091
- const s = await stat5(path4);
6479
+ const s = await stat8(path4);
5092
6480
  return s.isFile();
5093
6481
  } catch {
5094
6482
  return false;
@@ -5096,8 +6484,8 @@ async function fileExists2(path4) {
5096
6484
  }
5097
6485
  async function discoverFromDir(dir, source) {
5098
6486
  if (!await dirExists(dir)) return [];
5099
- const entries = await readdir5(dir);
5100
- return entries.filter((e) => VALID_EXTENSIONS.has(extname3(e))).map((e) => ({ path: join7(dir, e), source }));
6487
+ const entries = await readdir7(dir);
6488
+ return entries.filter((e) => VALID_EXTENSIONS.has(extname3(e))).map((e) => ({ path: join11(dir, e), source }));
5101
6489
  }
5102
6490
  function extractTools(mod) {
5103
6491
  const tools = [];
@@ -5122,41 +6510,41 @@ function extractTools(mod) {
5122
6510
  return tools;
5123
6511
  }
5124
6512
  async function loadModule(filePath, importFn) {
5125
- const url = pathToFileURL(filePath).href;
6513
+ const url = pathToFileURL2(filePath).href;
5126
6514
  const mod = await importFn(url);
5127
6515
  return extractTools(mod);
5128
6516
  }
5129
6517
  async function discoverNpmPlugins(cwd) {
5130
- const nodeModulesDir = join7(cwd, "node_modules");
6518
+ const nodeModulesDir = join11(cwd, "node_modules");
5131
6519
  if (!await dirExists(nodeModulesDir)) return [];
5132
6520
  try {
5133
- const entries = await readdir5(nodeModulesDir);
5134
- return entries.filter((e) => e.startsWith("pi-plugin-")).map((e) => join7(nodeModulesDir, e));
6521
+ const entries = await readdir7(nodeModulesDir);
6522
+ return entries.filter((e) => e.startsWith("pi-plugin-")).map((e) => join11(nodeModulesDir, e));
5135
6523
  } catch {
5136
6524
  return [];
5137
6525
  }
5138
6526
  }
5139
6527
  async function loadNpmPlugin(pkgDir, importFn) {
5140
- const pkgJsonPath = join7(pkgDir, "package.json");
5141
- if (!await fileExists2(pkgJsonPath)) return [];
5142
- const pkgJson = JSON.parse(await readFile7(pkgJsonPath, "utf-8"));
6528
+ const pkgJsonPath = join11(pkgDir, "package.json");
6529
+ if (!await fileExists(pkgJsonPath)) return [];
6530
+ const pkgJson = JSON.parse(await readFile10(pkgJsonPath, "utf-8"));
5143
6531
  const main = pkgJson.main ?? "index.js";
5144
- const resolvedMain = resolve4(pkgDir, main);
5145
- const resolvedPkgDir = resolve4(pkgDir);
5146
- if (resolvedMain !== resolvedPkgDir && !resolvedMain.startsWith(resolvedPkgDir + sep3)) {
6532
+ const resolvedMain = resolve6(pkgDir, main);
6533
+ const resolvedPkgDir = resolve6(pkgDir);
6534
+ if (resolvedMain !== resolvedPkgDir && !resolvedMain.startsWith(resolvedPkgDir + sep6)) {
5147
6535
  return [];
5148
6536
  }
5149
- const relPath = relative6(resolvedPkgDir, resolvedMain);
6537
+ const relPath = relative9(resolvedPkgDir, resolvedMain);
5150
6538
  if (relPath.startsWith("..")) {
5151
6539
  return [];
5152
6540
  }
5153
6541
  return loadModule(resolvedMain, importFn);
5154
6542
  }
5155
6543
  async function loadExtensionsJson(cwd) {
5156
- const configPath = join7(cwd, EXTENSIONS_JSON);
5157
- if (!await fileExists2(configPath)) return null;
6544
+ const configPath = join11(cwd, EXTENSIONS_JSON);
6545
+ if (!await fileExists(configPath)) return null;
5158
6546
  try {
5159
- const raw = await readFile7(configPath, "utf-8");
6547
+ const raw = await readFile10(configPath, "utf-8");
5160
6548
  return JSON.parse(raw);
5161
6549
  } catch {
5162
6550
  return null;
@@ -5171,7 +6559,7 @@ async function loadPlugins(options) {
5171
6559
  const globals = await discoverFromDir(GLOBAL_DIR, "global");
5172
6560
  candidates.push(...globals);
5173
6561
  }
5174
- const localDir = join7(options.cwd, LOCAL_DIR);
6562
+ const localDir = join11(options.cwd, LOCAL_DIR);
5175
6563
  const locals = await discoverFromDir(localDir, "local");
5176
6564
  candidates.push(...locals);
5177
6565
  const npmDirs = await discoverNpmPlugins(options.cwd);
@@ -5181,7 +6569,7 @@ async function loadPlugins(options) {
5181
6569
  const config = await loadExtensionsJson(options.cwd);
5182
6570
  if (config?.npm) {
5183
6571
  for (const pkg of config.npm) {
5184
- const pkgDir = join7(options.cwd, "node_modules", pkg);
6572
+ const pkgDir = join11(options.cwd, "node_modules", pkg);
5185
6573
  if (await dirExists(pkgDir)) {
5186
6574
  const already = candidates.some((c) => c.path === pkgDir);
5187
6575
  if (!already) {
@@ -5229,9 +6617,9 @@ import {
5229
6617
  } from "@mariozechner/pi-coding-agent";
5230
6618
 
5231
6619
  // src/server/tools/operations/bound.ts
5232
- import { constants as constants3 } from "fs";
5233
- import { access as access3, lstat as lstat3, mkdir as mkdir10, readFile as readFile8, readdir as readdir6, readlink, realpath as realpath2, stat as stat6, writeFile as writeFile8 } from "fs/promises";
5234
- import { dirname as dirname7, isAbsolute as isAbsolute5, relative as relative7, resolve as resolve5 } from "path";
6620
+ 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";
5235
6623
  function toPosixPath(value) {
5236
6624
  return value.split("\\").join("/");
5237
6625
  }
@@ -5276,19 +6664,19 @@ function matchesGlob(relativePath, pattern) {
5276
6664
  }
5277
6665
  function shouldSkipDir(relativePath, ignore) {
5278
6666
  const normalizedRel = toPosixPath(relativePath);
5279
- const basename3 = normalizedRel.split("/").at(-1) ?? normalizedRel;
5280
- if (basename3 === ".git" || basename3 === "node_modules") return true;
6667
+ const basename4 = normalizedRel.split("/").at(-1) ?? normalizedRel;
6668
+ if (basename4 === ".git" || basename4 === "node_modules") return true;
5281
6669
  return ignore.some((pattern) => {
5282
6670
  return matchesGlob(normalizedRel, pattern) || matchesGlob(`${normalizedRel}/`, pattern);
5283
6671
  });
5284
6672
  }
5285
6673
  async function walkMatches(root, current, pattern, ignore, limit, out) {
5286
6674
  if (out.length >= limit) return;
5287
- const entries = await readdir6(current, { withFileTypes: true });
6675
+ const entries = await readdir8(current, { withFileTypes: true });
5288
6676
  for (const entry of entries) {
5289
6677
  if (out.length >= limit) return;
5290
- const absolutePath = resolve5(current, entry.name);
5291
- const relativePath = toPosixPath(relative7(root, absolutePath));
6678
+ const absolutePath = resolve7(current, entry.name);
6679
+ const relativePath = toPosixPath(relative10(root, absolutePath));
5292
6680
  if (entry.isDirectory() && shouldSkipDir(relativePath, ignore)) continue;
5293
6681
  if (matchesGlob(relativePath, pattern)) {
5294
6682
  out.push(absolutePath);
@@ -5302,26 +6690,26 @@ async function findNearestExistingAncestor(absPath) {
5302
6690
  let current = absPath;
5303
6691
  for (; ; ) {
5304
6692
  try {
5305
- await stat6(current);
6693
+ await stat9(current);
5306
6694
  return current;
5307
6695
  } catch {
5308
- const parent = dirname7(current);
6696
+ const parent = dirname10(current);
5309
6697
  if (parent === current) return current;
5310
6698
  current = parent;
5311
6699
  }
5312
6700
  }
5313
6701
  }
5314
6702
  async function assertWithinWorkspace(workspaceRoot, absPath) {
5315
- const realRoot = await realpath2(resolve5(workspaceRoot));
6703
+ const realRoot = await realpath4(resolve7(workspaceRoot));
5316
6704
  try {
5317
- const s = await lstat3(absPath);
6705
+ const s = await lstat4(absPath);
5318
6706
  if (s.isSymbolicLink()) {
5319
6707
  const target = await readlink(absPath);
5320
- const resolvedTarget = resolve5(dirname7(absPath), target);
6708
+ const resolvedTarget = resolve7(dirname10(absPath), target);
5321
6709
  const nearestAncestor = await findNearestExistingAncestor(resolvedTarget);
5322
- const realAncestor = await realpath2(nearestAncestor);
5323
- const rel2 = relative7(realRoot, realAncestor);
5324
- if (rel2.startsWith("..") || isAbsolute5(rel2)) {
6710
+ const realAncestor = await realpath4(nearestAncestor);
6711
+ const rel2 = relative10(realRoot, realAncestor);
6712
+ if (rel2.startsWith("..") || isAbsolute6(rel2)) {
5325
6713
  throw new Error(`path "${absPath}" is outside workspace`);
5326
6714
  }
5327
6715
  return;
@@ -5335,22 +6723,22 @@ async function assertWithinWorkspace(workspaceRoot, absPath) {
5335
6723
  }
5336
6724
  let realCandidate;
5337
6725
  try {
5338
- realCandidate = await realpath2(absPath);
6726
+ realCandidate = await realpath4(absPath);
5339
6727
  } catch (err) {
5340
6728
  const code = err.code;
5341
6729
  if (code === "ENOENT") {
5342
- const nearestAncestor = await findNearestExistingAncestor(dirname7(absPath));
5343
- const realAncestor = await realpath2(nearestAncestor);
5344
- const rel2 = relative7(realRoot, realAncestor);
5345
- if (rel2.startsWith("..") || isAbsolute5(rel2)) {
6730
+ const nearestAncestor = await findNearestExistingAncestor(dirname10(absPath));
6731
+ const realAncestor = await realpath4(nearestAncestor);
6732
+ const rel2 = relative10(realRoot, realAncestor);
6733
+ if (rel2.startsWith("..") || isAbsolute6(rel2)) {
5346
6734
  throw new Error(`path "${absPath}" is outside workspace`);
5347
6735
  }
5348
6736
  return;
5349
6737
  }
5350
6738
  throw err;
5351
6739
  }
5352
- const rel = relative7(realRoot, realCandidate);
5353
- if (rel.startsWith("..") || isAbsolute5(rel)) {
6740
+ const rel = relative10(realRoot, realCandidate);
6741
+ if (rel.startsWith("..") || isAbsolute6(rel)) {
5354
6742
  throw new Error(`path "${absPath}" is outside workspace`);
5355
6743
  }
5356
6744
  }
@@ -5358,28 +6746,28 @@ function boundFs(workspaceRoot) {
5358
6746
  const read = {
5359
6747
  async readFile(absolutePath) {
5360
6748
  await assertWithinWorkspace(workspaceRoot, absolutePath);
5361
- return await readFile8(absolutePath);
6749
+ return await readFile11(absolutePath);
5362
6750
  },
5363
6751
  async access(absolutePath) {
5364
6752
  await assertWithinWorkspace(workspaceRoot, absolutePath);
5365
- await access3(absolutePath, constants3.R_OK);
6753
+ await access4(absolutePath, constants4.R_OK);
5366
6754
  }
5367
6755
  };
5368
6756
  const write = {
5369
6757
  async writeFile(absolutePath, content) {
5370
6758
  await assertWithinWorkspace(workspaceRoot, absolutePath);
5371
- await mkdir10(dirname7(absolutePath), { recursive: true });
6759
+ await mkdir12(dirname10(absolutePath), { recursive: true });
5372
6760
  await writeFile8(absolutePath, content);
5373
6761
  },
5374
6762
  async mkdir(dir) {
5375
6763
  await assertWithinWorkspace(workspaceRoot, dir);
5376
- await mkdir10(dir, { recursive: true });
6764
+ await mkdir12(dir, { recursive: true });
5377
6765
  }
5378
6766
  };
5379
6767
  const edit = {
5380
6768
  async readFile(absolutePath) {
5381
6769
  await assertWithinWorkspace(workspaceRoot, absolutePath);
5382
- return await readFile8(absolutePath);
6770
+ return await readFile11(absolutePath);
5383
6771
  },
5384
6772
  async writeFile(absolutePath, content) {
5385
6773
  await assertWithinWorkspace(workspaceRoot, absolutePath);
@@ -5387,14 +6775,14 @@ function boundFs(workspaceRoot) {
5387
6775
  },
5388
6776
  async access(absolutePath) {
5389
6777
  await assertWithinWorkspace(workspaceRoot, absolutePath);
5390
- await access3(absolutePath, constants3.R_OK | constants3.W_OK);
6778
+ await access4(absolutePath, constants4.R_OK | constants4.W_OK);
5391
6779
  }
5392
6780
  };
5393
6781
  const find = {
5394
6782
  async exists(absolutePath) {
5395
6783
  await assertWithinWorkspace(workspaceRoot, absolutePath);
5396
6784
  try {
5397
- await stat6(absolutePath);
6785
+ await stat9(absolutePath);
5398
6786
  return true;
5399
6787
  } catch (err) {
5400
6788
  if (err.code === "ENOENT") return false;
@@ -5411,18 +6799,18 @@ function boundFs(workspaceRoot) {
5411
6799
  const grep = {
5412
6800
  async isDirectory(absolutePath) {
5413
6801
  await assertWithinWorkspace(workspaceRoot, absolutePath);
5414
- return (await stat6(absolutePath)).isDirectory();
6802
+ return (await stat9(absolutePath)).isDirectory();
5415
6803
  },
5416
6804
  async readFile(absolutePath) {
5417
6805
  await assertWithinWorkspace(workspaceRoot, absolutePath);
5418
- return await readFile8(absolutePath, "utf8");
6806
+ return await readFile11(absolutePath, "utf8");
5419
6807
  }
5420
6808
  };
5421
6809
  const ls = {
5422
6810
  async exists(absolutePath) {
5423
6811
  try {
5424
6812
  await assertWithinWorkspace(workspaceRoot, absolutePath);
5425
- await stat6(absolutePath);
6813
+ await stat9(absolutePath);
5426
6814
  return true;
5427
6815
  } catch {
5428
6816
  return false;
@@ -5430,18 +6818,18 @@ function boundFs(workspaceRoot) {
5430
6818
  },
5431
6819
  async stat(absolutePath) {
5432
6820
  await assertWithinWorkspace(workspaceRoot, absolutePath);
5433
- return await stat6(absolutePath);
6821
+ return await stat9(absolutePath);
5434
6822
  },
5435
6823
  async readdir(absolutePath) {
5436
6824
  await assertWithinWorkspace(workspaceRoot, absolutePath);
5437
- return await readdir6(absolutePath);
6825
+ return await readdir8(absolutePath);
5438
6826
  }
5439
6827
  };
5440
6828
  return { read, write, edit, find, grep, ls };
5441
6829
  }
5442
6830
 
5443
6831
  // src/server/tools/operations/vercel.ts
5444
- import { isAbsolute as isAbsolute6, relative as relative8 } from "path";
6832
+ import { isAbsolute as isAbsolute7, relative as relative11 } from "path";
5445
6833
  function rootAliases(workspace) {
5446
6834
  const aliases = [workspace.root];
5447
6835
  if (workspace.root === VERCEL_SANDBOX_WORKSPACE_ROOT) aliases.push(VERCEL_SANDBOX_REMOTE_ROOT);
@@ -5449,11 +6837,11 @@ function rootAliases(workspace) {
5449
6837
  return aliases;
5450
6838
  }
5451
6839
  function isOutsideWorkspaceRel(rel) {
5452
- return rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isAbsolute6(rel);
6840
+ return rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isAbsolute7(rel);
5453
6841
  }
5454
6842
  function toRelPath(workspace, absolutePath) {
5455
6843
  for (const root of rootAliases(workspace)) {
5456
- const rel = relative8(root, absolutePath);
6844
+ const rel = relative11(root, absolutePath);
5457
6845
  if (!isOutsideWorkspaceRel(rel)) return rel;
5458
6846
  }
5459
6847
  const skillMarker = "/.agents/skills/";
@@ -5469,10 +6857,11 @@ function toRelPath(workspace, absolutePath) {
5469
6857
  `path "${absolutePath}" is outside workspace; use a path relative to the workspace root or under ${rootAliases(workspace).join(" / ")}`
5470
6858
  );
5471
6859
  }
5472
- function vercelBashOps(sandbox) {
6860
+ function vercelBashOps(sandbox, opts = {}) {
5473
6861
  return {
5474
6862
  exec(command, cwd, { onData, signal, timeout, env }) {
5475
- const filteredEnv = env ? Object.fromEntries(Object.entries(env).filter((e) => e[1] != null)) : void 0;
6863
+ const effectiveEnv = opts.mergeEnv ? opts.mergeEnv(env) : env;
6864
+ const filteredEnv = effectiveEnv ? Object.fromEntries(Object.entries(effectiveEnv).filter((e) => e[1] != null)) : void 0;
5476
6865
  return sandbox.exec(command, {
5477
6866
  cwd,
5478
6867
  env: filteredEnv,
@@ -5636,7 +7025,7 @@ import {
5636
7025
  truncateHead,
5637
7026
  truncateLine
5638
7027
  } from "@mariozechner/pi-coding-agent";
5639
- import { resolve as resolve6, relative as relative9 } from "path";
7028
+ import { resolve as resolve8, relative as relative12 } from "path";
5640
7029
 
5641
7030
  // src/server/catalog/tools/_shared.ts
5642
7031
  function makeError(message) {
@@ -5788,8 +7177,8 @@ function vercelGrepTool(sandbox, workspaceRoot) {
5788
7177
  return makeError("pattern is required");
5789
7178
  }
5790
7179
  if (workspaceRoot && typeof params.path === "string" && params.path.length > 0) {
5791
- const resolved = resolve6(workspaceRoot, params.path);
5792
- const rel = relative9(workspaceRoot, resolved);
7180
+ const resolved = resolve8(workspaceRoot, params.path);
7181
+ const rel = relative12(workspaceRoot, resolved);
5793
7182
  if (rel.startsWith("..") || rel.startsWith("/")) {
5794
7183
  return makeError(`path "${params.path}" is outside workspace`);
5795
7184
  }
@@ -5877,7 +7266,20 @@ import {
5877
7266
  function shellEscape2(s) {
5878
7267
  return `'${s.replace(/'/g, "'\\''")}'`;
5879
7268
  }
5880
- function bwrapSpawnHook(workspaceRoot) {
7269
+ function mergeRuntimeEnv(runtime, commandEnv) {
7270
+ const current = runtime?.getCurrent?.() ?? runtime;
7271
+ if (!current?.env && !current?.pathEntries?.length) return commandEnv;
7272
+ const merged = {
7273
+ ...current.env ?? {},
7274
+ ...commandEnv ?? {}
7275
+ };
7276
+ const pathParts = [...current.pathEntries ?? []];
7277
+ if (current.env?.PATH) pathParts.push(current.env.PATH);
7278
+ if (commandEnv?.PATH) pathParts.push(commandEnv.PATH);
7279
+ if (pathParts.length > 0) merged.PATH = pathParts.join(":");
7280
+ return merged;
7281
+ }
7282
+ function bwrapSpawnHook(workspaceRoot, runtime) {
5881
7283
  const args = buildBwrapArgs(workspaceRoot);
5882
7284
  const bwrapPrefix = ["bwrap", ...args].map(shellEscape2).join(" ");
5883
7285
  return (context) => ({
@@ -5885,30 +7287,41 @@ function bwrapSpawnHook(workspaceRoot) {
5885
7287
  command: `${bwrapPrefix} bash -lc ${shellEscape2(context.command)}`,
5886
7288
  env: withWorkspacePythonEnv({
5887
7289
  workspaceRoot,
5888
- env: context.env,
7290
+ env: mergeRuntimeEnv(runtime, context.env),
5889
7291
  sandboxRoot: "/workspace"
5890
7292
  })
5891
7293
  });
5892
7294
  }
5893
- function directSpawnHook(workspaceRoot) {
7295
+ function directSpawnHook(workspaceRoot, runtime) {
5894
7296
  return (context) => ({
5895
7297
  ...context,
5896
- env: withWorkspacePythonEnv({ workspaceRoot, env: context.env })
7298
+ env: withWorkspacePythonEnv({
7299
+ workspaceRoot,
7300
+ env: mergeRuntimeEnv(runtime, context.env)
7301
+ })
5897
7302
  });
5898
7303
  }
5899
- function bashOptionsForMode(bundle) {
7304
+ var VERCEL_SAFE_DEFAULT_PATH = "/vercel/runtimes/node24/bin:/vercel/runtimes/node22/bin:/usr/local/bin:/usr/bin:/bin";
7305
+ function bashOptionsForMode(bundle, runtime) {
5900
7306
  switch (bundle.sandbox.provider) {
5901
7307
  case "vercel-sandbox":
5902
- return { operations: vercelBashOps(bundle.sandbox) };
7308
+ return {
7309
+ operations: vercelBashOps(bundle.sandbox, {
7310
+ // The pi bash tool's env may include the host process env. Never
7311
+ // forward host secrets into a remote sandbox; provide only the
7312
+ // provisioned runtime env plus a conservative remote PATH tail.
7313
+ mergeEnv: () => mergeRuntimeEnv(runtime, { PATH: VERCEL_SAFE_DEFAULT_PATH })
7314
+ })
7315
+ };
5903
7316
  case "bwrap":
5904
7317
  return {
5905
7318
  operations: createLocalBashOperations(),
5906
- spawnHook: bwrapSpawnHook(bundle.workspace.root)
7319
+ spawnHook: bwrapSpawnHook(bundle.workspace.root, runtime)
5907
7320
  };
5908
7321
  default:
5909
7322
  return {
5910
7323
  operations: createLocalBashOperations(),
5911
- spawnHook: directSpawnHook(bundle.workspace.root)
7324
+ spawnHook: directSpawnHook(bundle.workspace.root, runtime)
5912
7325
  };
5913
7326
  }
5914
7327
  }
@@ -5991,9 +7404,9 @@ function createExecuteIsolatedCodeTool(sandbox) {
5991
7404
  }
5992
7405
  };
5993
7406
  }
5994
- function buildHarnessAgentTools(bundle) {
7407
+ function buildHarnessAgentTools(bundle, runtime) {
5995
7408
  const tools = [
5996
- adaptPiTool2(createBashToolDefinition(bundle.workspace.root, bashOptionsForMode(bundle)))
7409
+ adaptPiTool2(createBashToolDefinition(bundle.workspace.root, bashOptionsForMode(bundle, runtime)))
5997
7410
  ];
5998
7411
  if (bundle.sandbox.capabilities.includes("isolated-code")) {
5999
7412
  tools.push(createExecuteIsolatedCodeTool(bundle.sandbox));
@@ -6218,7 +7631,7 @@ var PathError = class extends Error {
6218
7631
  this.name = "PathError";
6219
7632
  }
6220
7633
  };
6221
- function joinPath(base, name) {
7634
+ function joinPath2(base, name) {
6222
7635
  if (base === ".") return name;
6223
7636
  return `${base}/${name}`;
6224
7637
  }
@@ -6233,7 +7646,7 @@ async function listTree(workspace, dir, recursive) {
6233
7646
  const batch = queue.shift();
6234
7647
  for (const e of batch.items) {
6235
7648
  if (entries.length >= MAX_ENTRIES) break;
6236
- const entryPath = joinPath(batch.parentDir, e.name);
7649
+ const entryPath = joinPath2(batch.parentDir, e.name);
6237
7650
  entries.push({ name: e.name, kind: e.kind, path: entryPath });
6238
7651
  if (recursive && e.kind === "dir" && batch.depth < MAX_DEPTH) {
6239
7652
  try {
@@ -6542,8 +7955,17 @@ var chatBodySchema = z.object({
6542
7955
  })
6543
7956
  ).max(20).optional()
6544
7957
  });
7958
+ function addTelemetryProperty(properties, key, value) {
7959
+ if (typeof value === "string" && value) properties[key] = value;
7960
+ if (typeof value === "number" && Number.isFinite(value)) properties[key] = value;
7961
+ }
7962
+ function safeTelemetryErrorCode(value) {
7963
+ const parsed = ErrorCode.safeParse(value);
7964
+ return parsed.success ? parsed.data : ErrorCode.enum.INTERNAL_ERROR;
7965
+ }
6545
7966
  function chatRoutes(app, opts, done) {
6546
7967
  const { sessionChangesTracker } = opts;
7968
+ const telemetry = opts.telemetry ?? noopTelemetry;
6547
7969
  const validateBody = createBodyValidator(chatBodySchema);
6548
7970
  const buffers = new StreamBufferStore();
6549
7971
  const lastFollowUpBySession = /* @__PURE__ */ new Map();
@@ -6588,8 +8010,18 @@ function chatRoutes(app, opts, done) {
6588
8010
  async (request, reply) => {
6589
8011
  const { sessionId, message, model, thinkingLevel, attachments } = request.body;
6590
8012
  const turnId = randomUUID3();
8013
+ const startedAt = Date.now();
8014
+ const telemetryProperties2 = {
8015
+ sessionId,
8016
+ requestId: request.id
8017
+ };
8018
+ addTelemetryProperty(telemetryProperties2, "workspaceId", request.workspaceContext?.workspaceId);
8019
+ addTelemetryProperty(telemetryProperties2, "modelProvider", model?.provider);
6591
8020
  request.log.info({ sessionId, turnId, model, thinkingLevel }, "[chat] start");
6592
- const runtime = await resolveRuntime(request);
8021
+ safeCapture(telemetry, {
8022
+ name: "agent.chat.started",
8023
+ properties: telemetryProperties2
8024
+ });
6593
8025
  const abortController = new AbortController();
6594
8026
  let streamStarted = false;
6595
8027
  let streamCompleted = false;
@@ -6598,18 +8030,24 @@ function chatRoutes(app, opts, done) {
6598
8030
  abortController.abort();
6599
8031
  }
6600
8032
  });
6601
- const ctx = {
6602
- abortSignal: abortController.signal,
6603
- workdir: runtime.workdir
6604
- };
6605
8033
  const buf = buffers.create(sessionId, turnId);
6606
8034
  try {
8035
+ const runtime = await resolveRuntime(request);
8036
+ const ctx = {
8037
+ abortSignal: abortController.signal,
8038
+ workdir: runtime.workdir
8039
+ };
8040
+ safeCapture(telemetry, {
8041
+ name: "agent.chat.message.submitted",
8042
+ properties: telemetryProperties2
8043
+ });
6607
8044
  const chunks = runtime.harness.sendMessage(
6608
8045
  { sessionId, message, model, thinkingLevel, attachments },
6609
8046
  ctx
6610
8047
  );
6611
8048
  const stream = createUIMessageStream({
6612
8049
  async execute({ writer }) {
8050
+ let streamFailed = false;
6613
8051
  try {
6614
8052
  for await (const chunk2 of chunks) {
6615
8053
  const c = chunk2;
@@ -6621,7 +8059,17 @@ function chatRoutes(app, opts, done) {
6621
8059
  writer.write(c);
6622
8060
  }
6623
8061
  } catch (err) {
8062
+ streamFailed = true;
6624
8063
  request.log.error({ err, sessionId }, "[chat] stream error");
8064
+ safeCapture(telemetry, {
8065
+ name: "agent.chat.failed",
8066
+ properties: {
8067
+ ...telemetryProperties2,
8068
+ status: "error",
8069
+ durationMs: Date.now() - startedAt,
8070
+ errorCode: ErrorCode.enum.INTERNAL_ERROR
8071
+ }
8072
+ });
6625
8073
  const errChunk = {
6626
8074
  type: "error",
6627
8075
  errorText: "internal error"
@@ -6630,6 +8078,16 @@ function chatRoutes(app, opts, done) {
6630
8078
  writer.write(errChunk);
6631
8079
  } finally {
6632
8080
  streamCompleted = true;
8081
+ if (!streamFailed) {
8082
+ safeCapture(telemetry, {
8083
+ name: "agent.chat.completed",
8084
+ properties: {
8085
+ ...telemetryProperties2,
8086
+ status: "ok",
8087
+ durationMs: Date.now() - startedAt
8088
+ }
8089
+ });
8090
+ }
6633
8091
  buf.markComplete(() => buffers.evict(sessionId, turnId));
6634
8092
  }
6635
8093
  }
@@ -6649,6 +8107,15 @@ function chatRoutes(app, opts, done) {
6649
8107
  return;
6650
8108
  } catch (err) {
6651
8109
  request.log.error({ err, sessionId }, "[chat] error");
8110
+ safeCapture(telemetry, {
8111
+ name: "agent.chat.failed",
8112
+ properties: {
8113
+ ...telemetryProperties2,
8114
+ status: "error",
8115
+ durationMs: Date.now() - startedAt,
8116
+ errorCode: safeTelemetryErrorCode(err?.code)
8117
+ }
8118
+ });
6652
8119
  buf.markComplete(() => buffers.evict(sessionId, turnId));
6653
8120
  if (streamStarted) return;
6654
8121
  return reply.code(500).send({
@@ -6693,17 +8160,17 @@ function chatRoutes(app, opts, done) {
6693
8160
  const replayed = buf.replay(cursor);
6694
8161
  for (const e of replayed) writer.write(e.chunk);
6695
8162
  if (buf.complete) return;
6696
- await new Promise((resolve7) => {
8163
+ await new Promise((resolve9) => {
6697
8164
  const unsub = buf.subscribe(
6698
8165
  (e) => writer.write(e.chunk),
6699
8166
  () => {
6700
8167
  unsub();
6701
- resolve7();
8168
+ resolve9();
6702
8169
  }
6703
8170
  );
6704
8171
  request.raw.on("close", () => {
6705
8172
  unsub();
6706
- resolve7();
8173
+ resolve9();
6707
8174
  });
6708
8175
  });
6709
8176
  }
@@ -7059,7 +8526,7 @@ function classifySessionError(err, reply) {
7059
8526
  }
7060
8527
  });
7061
8528
  }
7062
- function stableStringify(value) {
8529
+ function stableStringify2(value) {
7063
8530
  if (typeof value === "string") return value;
7064
8531
  try {
7065
8532
  return JSON.stringify(value, null, 2);
@@ -7085,10 +8552,10 @@ function formatToolPart(part) {
7085
8552
  const name = toolName(part);
7086
8553
  const lines = [`[tool:${name}]`];
7087
8554
  if ("input" in part) {
7088
- lines.push("input:", stableStringify(part.input));
8555
+ lines.push("input:", stableStringify2(part.input));
7089
8556
  }
7090
8557
  if ("output" in part) {
7091
- lines.push("output:", stableStringify(part.output));
8558
+ lines.push("output:", stableStringify2(part.output));
7092
8559
  }
7093
8560
  if (typeof part.errorText === "string" && part.errorText.length > 0) {
7094
8561
  lines.push("error:", part.errorText);
@@ -7638,8 +9105,21 @@ async function createAgentApp(opts = {}) {
7638
9105
  pluginTools.push(...plugin.tools);
7639
9106
  }
7640
9107
  }
9108
+ const getRuntimeProvisioning = opts.getRuntimeProvisioning ?? (() => opts.runtimeProvisioning);
9109
+ const runtimePi = {
9110
+ ...opts.pi,
9111
+ additionalSkillPaths: [
9112
+ ...getRuntimeProvisioning()?.skillPaths ?? [],
9113
+ ...opts.pi?.additionalSkillPaths ?? []
9114
+ ]
9115
+ };
7641
9116
  const tools = [
7642
- ...buildHarnessAgentTools(runtimeBundle),
9117
+ ...buildHarnessAgentTools(runtimeBundle, {
9118
+ getCurrent: () => {
9119
+ const current = getRuntimeProvisioning();
9120
+ return current ? { env: current.env, pathEntries: current.pathEntries } : void 0;
9121
+ }
9122
+ }),
7643
9123
  ...opts.disableDefaultFileTools ? [] : buildFilesystemAgentTools(runtimeBundle),
7644
9124
  ...opts.extraTools ?? [],
7645
9125
  ...pluginTools
@@ -7649,7 +9129,7 @@ async function createAgentApp(opts = {}) {
7649
9129
  pi: {
7650
9130
  noContextFiles: true,
7651
9131
  noSkills: true,
7652
- ...opts.pi
9132
+ ...runtimePi
7653
9133
  }
7654
9134
  }));
7655
9135
  const harness = await harnessFactory({
@@ -7658,7 +9138,8 @@ async function createAgentApp(opts = {}) {
7658
9138
  sessionNamespace: opts.sessionNamespace,
7659
9139
  sessionDir: opts.sessionDir,
7660
9140
  systemPromptAppend: opts.systemPromptAppend,
7661
- systemPromptDynamic: opts.systemPromptDynamic
9141
+ systemPromptDynamic: opts.systemPromptDynamic,
9142
+ telemetry: opts.telemetry
7662
9143
  });
7663
9144
  const sessionChangesTracker = new InMemorySessionChangesTracker();
7664
9145
  const readyTracker = new ReadyStatusTracker({
@@ -7686,7 +9167,8 @@ async function createAgentApp(opts = {}) {
7686
9167
  await app.register(chatRoutes, {
7687
9168
  harness,
7688
9169
  workdir: runtimeBundle.workspace.root,
7689
- sessionChangesTracker
9170
+ sessionChangesTracker,
9171
+ telemetry: opts.telemetry
7690
9172
  });
7691
9173
  await app.register(sessionRoutes, {
7692
9174
  sessionStore: harness.sessions,
@@ -7697,9 +9179,18 @@ async function createAgentApp(opts = {}) {
7697
9179
  await app.register(modelsRoutes);
7698
9180
  await app.register(skillsRoutes, {
7699
9181
  workspaceRoot,
7700
- additionalSkillPaths: opts.pi?.additionalSkillPaths,
7701
- piPackages: opts.pi?.packages,
7702
- noSkills: opts.pi?.noSkills
9182
+ additionalSkillPaths: runtimePi.additionalSkillPaths,
9183
+ piPackages: runtimePi.packages,
9184
+ noSkills: runtimePi.noSkills,
9185
+ getAdditionalSkillPaths: () => [
9186
+ ...getRuntimeProvisioning()?.skillPaths ?? [],
9187
+ ...opts.pi?.additionalSkillPaths ?? [],
9188
+ ...opts.pi?.getHotReloadableResources?.().additionalSkillPaths ?? []
9189
+ ],
9190
+ getPiPackages: () => [
9191
+ ...opts.pi?.packages ?? [],
9192
+ ...opts.pi?.getHotReloadableResources?.().packages ?? []
9193
+ ]
7703
9194
  });
7704
9195
  await app.register(sessionChangesRoutes, { tracker: sessionChangesTracker });
7705
9196
  await app.register(catalogRoutes, { tools });
@@ -7733,7 +9224,7 @@ function applyCspHeaders(response, opts = {}) {
7733
9224
  }
7734
9225
 
7735
9226
  // src/server/registerAgentRoutes.ts
7736
- import { basename as basename2 } from "path";
9227
+ import { basename as basename3 } from "path";
7737
9228
  import { AuthStorage as AuthStorage3, ModelRegistry as ModelRegistry3 } from "@mariozechner/pi-coding-agent";
7738
9229
 
7739
9230
  // src/server/catalog/mergeTools.ts
@@ -7768,8 +9259,8 @@ function mergeTools(options) {
7768
9259
  }
7769
9260
 
7770
9261
  // src/server/tools/upload/index.ts
7771
- import { readFile as readFile9 } from "fs/promises";
7772
- import { extname as extname4, join as join8 } from "path";
9262
+ import { readFile as readFile12 } from "fs/promises";
9263
+ import { extname as extname4, join as join12 } from "path";
7773
9264
  var DEFAULT_UPLOAD_DIR = "assets/images";
7774
9265
  var MAX_UPLOAD_BYTES2 = 10 * 1024 * 1024;
7775
9266
  function contentTypeFromExt(path4) {
@@ -7837,7 +9328,7 @@ function buildUploadAgentTools(bundle) {
7837
9328
  const rawDir = typeof input.directory === "string" ? input.directory.trim() : "";
7838
9329
  const dir = rawDir ? rawDir.replace(/^\.\/+/, "").replace(/\/+$/, "") : DEFAULT_UPLOAD_DIR;
7839
9330
  try {
7840
- const bytes = await readFile9(join8(cwd, filePath));
9331
+ const bytes = await readFile12(join12(cwd, filePath));
7841
9332
  if (bytes.byteLength === 0 || bytes.byteLength > MAX_UPLOAD_BYTES2) {
7842
9333
  return {
7843
9334
  content: [{ type: "text", text: `file must be between 1 byte and ${MAX_UPLOAD_BYTES2} bytes` }],
@@ -7880,7 +9371,7 @@ var DEFAULT_WORKSPACE_ID3 = "default";
7880
9371
  var STANDARD_AGENT_TOOL_NAMES = ["bash", "read", "write", "edit", "find", "grep", "ls"];
7881
9372
  var VERCEL_BINDING_HEALTHCHECK_INTERVAL_MS = 15e3;
7882
9373
  function pluginNameFromPath(path4) {
7883
- const fileName = basename2(path4);
9374
+ const fileName = basename3(path4);
7884
9375
  if (fileName.endsWith(".mjs")) return fileName.slice(0, -4);
7885
9376
  if (fileName.endsWith(".js")) return fileName.slice(0, -3);
7886
9377
  return fileName;
@@ -7975,16 +9466,47 @@ var registerAgentRoutes = async (app, opts) => {
7975
9466
  const pi = opts.getPi ? await opts.getPi({ workspaceId, workspaceRoot: root, request }) : opts.pi;
7976
9467
  return { root, pi };
7977
9468
  }
9469
+ async function runRuntimeProvisioning(workspaceId, scope, request) {
9470
+ if (opts.provisionWorkspace === false || !opts.provisionRuntime) return void 0;
9471
+ const modeCtx = {
9472
+ workspaceRoot: scope.root,
9473
+ sessionId: workspaceId,
9474
+ workspaceId,
9475
+ templatePath: scope.templatePath,
9476
+ requestId: request?.id,
9477
+ telemetry: opts.telemetry
9478
+ };
9479
+ const runtimeLayout = getBoringAgentRuntimePaths(
9480
+ resolvedMode === "vercel-sandbox" ? VERCEL_SANDBOX_WORKSPACE_ROOT : scope.root
9481
+ );
9482
+ return await opts.provisionRuntime({
9483
+ workspaceId,
9484
+ workspaceRoot: scope.root,
9485
+ runtimeMode: resolvedMode,
9486
+ runtimeLayout,
9487
+ provisioningAdapter: modeAdapter.createProvisioningAdapter?.(runtimeLayout, modeCtx),
9488
+ request
9489
+ });
9490
+ }
7978
9491
  async function createRuntimeBinding(workspaceId, scope, request) {
7979
9492
  const root = scope.root;
7980
- const runtimeBundle = await modeAdapter.create({
9493
+ const modeCtx = {
7981
9494
  workspaceRoot: root,
7982
9495
  sessionId: workspaceId,
7983
9496
  workspaceId,
7984
- templatePath: scope.templatePath
7985
- });
9497
+ templatePath: scope.templatePath,
9498
+ requestId: request?.id,
9499
+ telemetry: opts.telemetry
9500
+ };
9501
+ let runtimeProvisioning = await runRuntimeProvisioning(workspaceId, scope, request);
9502
+ const runtimeBundle = await modeAdapter.create(modeCtx);
7986
9503
  const standardTools = [
7987
- ...buildHarnessAgentTools(runtimeBundle),
9504
+ ...buildHarnessAgentTools(runtimeBundle, {
9505
+ getCurrent: () => runtimeProvisioning ? {
9506
+ env: runtimeProvisioning.env,
9507
+ pathEntries: runtimeProvisioning.pathEntries
9508
+ } : void 0
9509
+ }),
7988
9510
  ...buildFilesystemAgentTools(runtimeBundle),
7989
9511
  ...buildUploadAgentTools(runtimeBundle)
7990
9512
  ];
@@ -8023,14 +9545,19 @@ var registerAgentRoutes = async (app, opts) => {
8023
9545
  pi: {
8024
9546
  noContextFiles: true,
8025
9547
  noSkills: true,
8026
- ...scope.pi
9548
+ ...scope.pi,
9549
+ additionalSkillPaths: [
9550
+ ...runtimeProvisioning?.skillPaths ?? [],
9551
+ ...scope.pi?.additionalSkillPaths ?? []
9552
+ ]
8027
9553
  }
8028
9554
  }));
8029
9555
  const harness = await harnessFactory({
8030
9556
  tools,
8031
9557
  cwd: root,
8032
9558
  sessionNamespace: scope.sessionNamespace,
8033
- systemPromptAppend: opts.systemPromptAppend
9559
+ systemPromptAppend: opts.systemPromptAppend,
9560
+ telemetry: opts.telemetry
8034
9561
  });
8035
9562
  const readyTracker = new ReadyStatusTracker({
8036
9563
  sandboxReady: resolvedMode !== "vercel-sandbox",
@@ -8041,6 +9568,11 @@ var registerAgentRoutes = async (app, opts) => {
8041
9568
  }
8042
9569
  return {
8043
9570
  runtimeBundle,
9571
+ runtimeProvisioning,
9572
+ reprovision: async (reloadRequest) => {
9573
+ runtimeProvisioning = await runRuntimeProvisioning(workspaceId, scope, reloadRequest);
9574
+ return runtimeProvisioning;
9575
+ },
8044
9576
  harness,
8045
9577
  tools,
8046
9578
  readyTracker
@@ -8104,6 +9636,7 @@ var registerAgentRoutes = async (app, opts) => {
8104
9636
  return await recreateRuntimeBinding(workspaceId, scope);
8105
9637
  }
8106
9638
  }
9639
+ const hasRuntimeProvisioningInput = opts.provisionWorkspace !== false && Boolean(opts.provisionRuntime);
8107
9640
  const staticBinding = requestScopedRuntime ? null : await getOrCreateRuntimeBinding(sessionId);
8108
9641
  const skillsScopeByRequest = /* @__PURE__ */ new WeakMap();
8109
9642
  function getSkillsScopeForRequest(request) {
@@ -8192,7 +9725,8 @@ var registerAgentRoutes = async (app, opts) => {
8192
9725
  workdir: binding.runtimeBundle.workspace.root
8193
9726
  };
8194
9727
  },
8195
- sessionChangesTracker
9728
+ sessionChangesTracker,
9729
+ telemetry: opts.telemetry
8196
9730
  });
8197
9731
  await app.register(sessionRoutes, {
8198
9732
  getSessionStore: async (request) => {
@@ -8213,15 +9747,55 @@ var registerAgentRoutes = async (app, opts) => {
8213
9747
  await app.register(modelsRoutes);
8214
9748
  await app.register(skillsRoutes, {
8215
9749
  workspaceRoot,
8216
- additionalSkillPaths: opts.pi?.additionalSkillPaths,
9750
+ additionalSkillPaths: [
9751
+ ...staticBinding?.runtimeProvisioning?.skillPaths ?? [],
9752
+ ...opts.pi?.additionalSkillPaths ?? []
9753
+ ],
8217
9754
  piPackages: opts.pi?.packages,
8218
9755
  noSkills: opts.pi?.noSkills,
8219
9756
  getWorkspaceRoot: staticBinding ? void 0 : async (request) => (await getSkillsScopeForRequest(request)).root,
8220
- getAdditionalSkillPaths: staticBinding ? void 0 : async (request) => (await getSkillsScopeForRequest(request)).pi?.additionalSkillPaths,
9757
+ getAdditionalSkillPaths: staticBinding && !hasRuntimeProvisioningInput ? void 0 : async (request) => {
9758
+ const scope = await getSkillsScopeForRequest(request);
9759
+ if (!hasRuntimeProvisioningInput) return scope.pi?.additionalSkillPaths;
9760
+ const binding = await getBindingForRequest(request);
9761
+ return [
9762
+ ...binding.runtimeProvisioning?.skillPaths ?? [],
9763
+ ...scope.pi?.additionalSkillPaths ?? []
9764
+ ];
9765
+ },
8221
9766
  getPiPackages: staticBinding ? void 0 : async (request) => (await getSkillsScopeForRequest(request)).pi?.packages,
8222
9767
  getNoSkills: staticBinding ? void 0 : async (request) => (await getSkillsScopeForRequest(request)).pi?.noSkills
8223
9768
  });
8224
9769
  await app.register(sessionChangesRoutes, { tracker: sessionChangesTracker });
9770
+ app.post("/api/v1/agent/reload", async (request, reply) => {
9771
+ const workspaceId = getRequestWorkspaceId(request);
9772
+ const binding = await getBindingForRequest(request);
9773
+ if (!binding.harness.reloadSession) {
9774
+ return reply.status(501).send({ ok: false, error: "Agent harness does not support reload" });
9775
+ }
9776
+ try {
9777
+ binding.runtimeProvisioning = await binding.reprovision(request);
9778
+ const hookResult = await opts.beforeReload?.({
9779
+ workspaceId,
9780
+ workspaceRoot: binding.runtimeBundle.workspace.root,
9781
+ request
9782
+ });
9783
+ const reloadSessionId = request.body?.sessionId || sessionId;
9784
+ const reloaded = await binding.harness.reloadSession(reloadSessionId);
9785
+ const restart_warnings = hookResult?.restart_warnings;
9786
+ const diagnostics = hookResult?.diagnostics;
9787
+ return {
9788
+ ok: true,
9789
+ sessionId: reloadSessionId,
9790
+ reloaded,
9791
+ ...restart_warnings && restart_warnings.length > 0 ? { restart_warnings } : {},
9792
+ ...diagnostics && diagnostics.length > 0 ? { diagnostics } : {}
9793
+ };
9794
+ } catch (error) {
9795
+ const message = error instanceof Error ? error.message : String(error);
9796
+ return reply.status(422).send({ ok: false, error: message });
9797
+ }
9798
+ });
8225
9799
  await app.register(
8226
9800
  catalogRoutes,
8227
9801
  staticBinding ? { tools: staticBinding.tools } : { getTools: async (request) => (await getBindingForRequest(request)).tools }
@@ -8237,6 +9811,8 @@ export {
8237
9811
  FileHandleStore,
8238
9812
  PI_PACKAGE_RESOURCE_FILTERS,
8239
9813
  UV_SETUP_COMMANDS,
9814
+ VERCEL_PROVISIONING_CACHE_ROOT,
9815
+ VERCEL_SANDBOX_WORKSPACE_ROOT,
8240
9816
  UV_SETUP_COMMANDS as VERCEL_UV_SETUP_COMMANDS,
8241
9817
  applyCspHeaders,
8242
9818
  autoDetectMode,
@@ -8252,14 +9828,19 @@ export {
8252
9828
  createNodeWorkspace,
8253
9829
  createResourceSettingsManager,
8254
9830
  createVercelDeploymentSnapshotProvider,
9831
+ createVercelProvisioningAdapter,
8255
9832
  createVercelSandboxWorkspace,
8256
9833
  fileRoutes,
9834
+ getBoringAgentPathEntries,
9835
+ getBoringAgentRuntimeEnv,
9836
+ getBoringAgentRuntimePaths,
8257
9837
  hasBwrap,
8258
9838
  mergePiPackageSources,
8259
9839
  piPackageSourceKey,
8260
9840
  prepareDeploymentSnapshot,
8261
9841
  prepareVercelDeploymentSnapshot,
8262
9842
  provisionRuntimeWorkspace,
9843
+ provisionWorkspaceRuntime,
8263
9844
  registerAgentRoutes,
8264
9845
  resolveMode,
8265
9846
  resolveSandboxHandle