@hachej/boring-agent 0.1.41 → 0.1.43

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.
Files changed (43) hide show
  1. package/README.md +31 -252
  2. package/dist/{DebugDrawer-2PUDZ2RL.js → DebugDrawer-RYEBQ6CO.js} +1 -1
  3. package/dist/{agentPluginEvents-CKrZLW3g.d.ts → agentPluginEvents-DP-vLNCs.d.ts} +36 -3
  4. package/dist/{session-BRovhe0D.d.ts → chatSubmitPayload-gC61O4Ni.d.ts} +20 -1
  5. package/dist/{chunk-IUJ22EFB.js → chunk-BOIV2I3N.js} +2 -10
  6. package/dist/{chunk-VQ7VSSSX.js → chunk-YUDMSYAO.js} +6 -1
  7. package/dist/front/index.d.ts +22 -5
  8. package/dist/front/index.js +155 -103
  9. package/dist/front/styles.css +0 -6
  10. package/dist/piChatEvent-DR9a-FVz.d.ts +306 -0
  11. package/dist/server/index.d.ts +363 -94
  12. package/dist/server/index.js +1611 -414
  13. package/dist/shared/index.d.ts +29 -23
  14. package/dist/shared/index.js +1 -1
  15. package/docs/ACCESSIBILITY.md +11 -13
  16. package/docs/API.md +86 -59
  17. package/docs/CSP.md +10 -9
  18. package/docs/ERROR_CODES.md +5 -1
  19. package/docs/KNOWN_LIMITATIONS.md +3 -3
  20. package/docs/MIGRATION.md +16 -16
  21. package/docs/PERFORMANCE.md +1 -1
  22. package/docs/PLUGINS.md +42 -12
  23. package/docs/README.md +89 -12
  24. package/docs/RISKS-MULTI-TAB.md +1 -1
  25. package/docs/STYLING.md +6 -4
  26. package/docs/UI-SHADCN.md +7 -7
  27. package/docs/runtime-provisioning.md +7 -3
  28. package/docs/runtime.md +68 -13
  29. package/docs/tools.md +37 -14
  30. package/package.json +2 -2
  31. package/dist/piChatEvent-Ck1BAE_m.d.ts +0 -323
  32. /package/docs/plans/{AGENT_EVAL_FRAMEWORK.md → archive/AGENT_EVAL_FRAMEWORK.md} +0 -0
  33. /package/docs/plans/{agent-package-spec.md → archive/agent-package-spec.md} +0 -0
  34. /package/docs/plans/{harness-followup-capabilities.md → archive/harness-followup-capabilities.md} +0 -0
  35. /package/docs/plans/{harness-tool-ui-capabilities.md → archive/harness-tool-ui-capabilities.md} +0 -0
  36. /package/docs/plans/{pi-followup-history-projection.md → archive/pi-followup-history-projection.md} +0 -0
  37. /package/docs/plans/{pi-tools-migration.md → archive/pi-tools-migration.md} +0 -0
  38. /package/docs/plans/{reviews → archive/reviews}/pi-followup-history-codex-review.md +0 -0
  39. /package/docs/plans/{reviews → archive/reviews}/pi-followup-history-gpt-review.md +0 -0
  40. /package/docs/plans/{reviews → archive/reviews}/pi-followup-history-opus-review.md +0 -0
  41. /package/docs/plans/{reviews → archive/reviews}/pi-followup-history-xai-review.md +0 -0
  42. /package/docs/plans/{vercel-base-snapshot-template-plan.md → archive/vercel-base-snapshot-template-plan.md} +0 -0
  43. /package/docs/plans/{vercel-persistent-sandbox-adapter.md → archive/vercel-persistent-sandbox-adapter.md} +0 -0
@@ -13,7 +13,7 @@ import {
13
13
  StopPayloadSchema,
14
14
  extractToolUiMetadata,
15
15
  sanitizeToolUiMetadata2 as sanitizeToolUiMetadata
16
- } from "../chunk-VQ7VSSSX.js";
16
+ } from "../chunk-YUDMSYAO.js";
17
17
 
18
18
  // src/server/sandbox/direct/createDirectSandbox.ts
19
19
  import { spawn } from "child_process";
@@ -257,7 +257,7 @@ function createDirectSandbox(opts = {}) {
257
257
  import { access, stat as stat3 } from "fs/promises";
258
258
  import { constants } from "fs";
259
259
  import { spawn as spawn2 } from "child_process";
260
- import { dirname as dirname4, isAbsolute as isAbsolute3, join as join2, posix, relative as relative3, resolve as resolve4, sep as sep2 } from "path";
260
+ import { dirname as dirname4, isAbsolute as isAbsolute3, join as join3, posix, relative as relative3, resolve as resolve4, sep as sep2 } from "path";
261
261
 
262
262
  // src/server/sandbox/bwrap/buildBwrapArgs.ts
263
263
  import { isAbsolute } from "path";
@@ -305,10 +305,13 @@ function validateWorkspaceRoot(workspaceRoot) {
305
305
  }
306
306
  function buildBwrapArgs(workspaceRoot, options) {
307
307
  validateWorkspaceRoot(workspaceRoot);
308
+ const network = options?.network ?? "shared";
308
309
  const args = [
309
310
  "--unshare-all",
310
- "--share-net",
311
+ ...network === "shared" ? ["--share-net"] : [],
311
312
  "--die-with-parent",
313
+ ...options?.newSession === false ? [] : ["--new-session"],
314
+ ...options?.dropAllCapabilities ? ["--cap-drop", "ALL"] : [],
312
315
  "--tmpfs",
313
316
  "/",
314
317
  "--proc",
@@ -345,9 +348,8 @@ function buildBwrapArgs(workspaceRoot, options) {
345
348
  }
346
349
 
347
350
  // src/server/workspace/createNodeWorkspace.ts
348
- import { lstat as lstat2, mkdir, readdir, readFile, rename, rm, stat as stat2, unlink, writeFile } from "fs/promises";
349
- import { dirname as dirname3, relative as relative2, resolve as resolve3, sep } from "path";
350
- import chokidar from "chokidar";
351
+ import { lstat as lstat2, mkdir, readdir as readdir2, readFile, rename, rm, stat as stat2, unlink, writeFile } from "fs/promises";
352
+ import { dirname as dirname3, resolve as resolve3 } from "path";
351
353
 
352
354
  // src/server/workspace/paths.ts
353
355
  import { lstat, realpath, stat } from "fs/promises";
@@ -428,6 +430,11 @@ async function ensureWritableWorkspacePath(workspaceRoot, relPath) {
428
430
  return absPath;
429
431
  }
430
432
 
433
+ // src/server/workspace/nodeWatcher.ts
434
+ import { readdir } from "fs/promises";
435
+ import { join as join2, relative as relative2, sep } from "path";
436
+ import chokidar from "chokidar";
437
+
431
438
  // src/server/workspace/ignore.ts
432
439
  var DEFAULT_IGNORED_DIR_NAMES = [
433
440
  "node_modules",
@@ -446,19 +453,136 @@ function isIgnoredDirName(name) {
446
453
  return IGNORED_SET.has(name) || name.endsWith(".tsbuildinfo");
447
454
  }
448
455
 
449
- // src/server/workspace/createNodeWorkspace.ts
450
- var EPERM_CODE = "EPERM";
456
+ // src/server/logging.ts
457
+ var SENSITIVE_KEYS = new Set([
458
+ "apiKey",
459
+ "api_key",
460
+ "token",
461
+ "secret",
462
+ "password",
463
+ "authorization",
464
+ "cookie",
465
+ "oidcToken",
466
+ "accessToken",
467
+ "refreshToken",
468
+ "ANTHROPIC_API_KEY",
469
+ "OPENAI_API_KEY",
470
+ "VERCEL_OIDC_TOKEN",
471
+ "VERCEL_TEAM_ID"
472
+ ].map((key) => key.toLowerCase()));
473
+ function isSensitiveKey(key) {
474
+ return SENSITIVE_KEYS.has(key.toLowerCase());
475
+ }
476
+ function redactValue(key, value, seen) {
477
+ if (key && isSensitiveKey(key) && value != null) return "***";
478
+ if (value == null || typeof value !== "object") return value;
479
+ if (value instanceof Date) return value;
480
+ if (seen.has(value)) return "[Circular]";
481
+ seen.add(value);
482
+ if (Array.isArray(value)) {
483
+ const out2 = value.map((item) => redactValue(void 0, item, seen));
484
+ seen.delete(value);
485
+ return out2;
486
+ }
487
+ const out = {};
488
+ for (const [childKey, childValue] of Object.entries(value)) {
489
+ out[childKey] = redactValue(childKey, childValue, seen);
490
+ }
491
+ seen.delete(value);
492
+ return out;
493
+ }
494
+ function redact(fields) {
495
+ const out = {};
496
+ const seen = /* @__PURE__ */ new WeakSet();
497
+ for (const [k, v] of Object.entries(fields)) {
498
+ out[k] = redactValue(k, v, seen);
499
+ }
500
+ return out;
501
+ }
502
+ var verbose = typeof process !== "undefined" && process.env?.BORING_AGENT_VERBOSE === "1";
503
+ function createLogger(prefix) {
504
+ function emit(level, msg, fields) {
505
+ const entry = {
506
+ level,
507
+ prefix,
508
+ msg,
509
+ ...fields ? redact(fields) : {},
510
+ t: (/* @__PURE__ */ new Date()).toISOString()
511
+ };
512
+ if (level === "error") {
513
+ console.error(JSON.stringify(entry));
514
+ } else if (level === "warn") {
515
+ console.warn(JSON.stringify(entry));
516
+ } else {
517
+ console.log(JSON.stringify(entry));
518
+ }
519
+ }
520
+ return {
521
+ debug(msg, fields) {
522
+ if (verbose) emit("debug", msg, fields);
523
+ },
524
+ info(msg, fields) {
525
+ emit("info", msg, fields);
526
+ },
527
+ warn(msg, fields) {
528
+ emit("warn", msg, fields);
529
+ },
530
+ error(msg, fields) {
531
+ emit("error", msg, fields);
532
+ }
533
+ };
534
+ }
535
+
536
+ // src/server/workspace/nodeWatcher.ts
537
+ var log = createLogger("workspace-watch");
451
538
  function shouldIgnoreWatchPath(root, path4) {
452
539
  const relPath = relative2(root, path4);
453
540
  const parts = relPath.split(sep);
454
541
  return parts.some((part) => isIgnoredDirName(part));
455
542
  }
543
+ function toPosixRel(root, absPath) {
544
+ return relative2(root, absPath).split(sep).join("/");
545
+ }
546
+ var RENAME_ECHO_IDLE_MS = 1e3;
547
+ var RENAME_ECHO_MAX_MS = 3e4;
548
+ var RENAME_ECHO_FROM_KINDS = /* @__PURE__ */ new Set(["unlink", "unlinkDir"]);
549
+ var RENAME_ECHO_TO_KINDS = /* @__PURE__ */ new Set(["add", "addDir"]);
550
+ var DEFAULT_MAX_WATCHED_ENTRIES = 5e4;
551
+ function maxWatchedEntries() {
552
+ const raw = getEnv("BORING_MAX_WATCHED_ENTRIES");
553
+ if (!raw) return DEFAULT_MAX_WATCHED_ENTRIES;
554
+ const n = Number.parseInt(raw, 10);
555
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_MAX_WATCHED_ENTRIES;
556
+ }
557
+ async function countWatchableEntries(root, cap) {
558
+ let count = 0;
559
+ const stack = [root];
560
+ while (stack.length > 0) {
561
+ const dir = stack.pop();
562
+ let entries;
563
+ try {
564
+ entries = await readdir(dir, { withFileTypes: true });
565
+ } catch {
566
+ continue;
567
+ }
568
+ for (const entry of entries) {
569
+ count += 1;
570
+ if (count > cap) return count;
571
+ if (entry.isDirectory() && !isIgnoredDirName(entry.name)) {
572
+ stack.push(join2(dir, entry.name));
573
+ }
574
+ }
575
+ }
576
+ return count;
577
+ }
456
578
  function createNodeWatcher(root) {
457
579
  const listeners = /* @__PURE__ */ new Set();
580
+ const suppressions = [];
458
581
  let fsw = null;
459
582
  let closed = false;
460
- const ensureFsw = () => {
461
- if (fsw) return fsw;
583
+ let readiness = null;
584
+ const startFsw = () => {
585
+ if (fsw || closed) return;
462
586
  fsw = chokidar.watch(root, {
463
587
  ignored: (path4) => shouldIgnoreWatchPath(root, path4),
464
588
  ignoreInitial: true,
@@ -468,23 +592,43 @@ function createNodeWatcher(root) {
468
592
  // servers. Consumers already reconcile by mtime after invalidation.
469
593
  // No native renames from chokidar — `unlinkDir`/`addDir`/
470
594
  // `unlink`/`add` are the primitives we get. We surface them as
471
- // separate `unlink` + `write`/`mkdir` events; renames are best
472
- // recovered at the consumer level if needed.
595
+ // separate `unlink` + `write`/`mkdir` events. Renames the host
596
+ // performs itself arrive via `emitRename` instead, which also
597
+ // mutes this echo.
473
598
  });
474
- fsw.on("add", (p, s) => emit({ op: "write", path: rel(p), mtimeMs: s?.mtimeMs }));
475
- fsw.on("change", (p, s) => emit({ op: "write", path: rel(p), mtimeMs: s?.mtimeMs }));
476
- fsw.on("addDir", (p) => emit({ op: "mkdir", path: rel(p) }));
477
- fsw.on("unlink", (p) => emit({ op: "unlink", path: rel(p) }));
478
- fsw.on("unlinkDir", (p) => emit({ op: "unlink", path: rel(p) }));
479
- fsw.on("error", () => {
599
+ fsw.on("add", (p, s) => emit("add", { op: "write", path: rel(p), mtimeMs: s?.mtimeMs }));
600
+ fsw.on("change", (p, s) => emit("change", { op: "write", path: rel(p), mtimeMs: s?.mtimeMs }));
601
+ fsw.on("addDir", (p) => emit("addDir", { op: "mkdir", path: rel(p) }));
602
+ fsw.on("unlink", (p) => emit("unlink", { op: "unlink", path: rel(p) }));
603
+ fsw.on("unlinkDir", (p) => emit("unlinkDir", { op: "unlink", path: rel(p) }));
604
+ let loggedError = false;
605
+ fsw.on("error", (err) => {
606
+ if (loggedError) return;
607
+ loggedError = true;
608
+ log.error("file watcher error \u2014 live file events may be incomplete", {
609
+ root,
610
+ error: err instanceof Error ? err.message : String(err)
611
+ });
480
612
  });
481
- return fsw;
482
613
  };
483
- const rel = (abs) => {
484
- const r = relative2(root, abs);
485
- return r.split(sep).join("/");
614
+ const ensureFsw = () => {
615
+ if (readiness) return readiness;
616
+ readiness = (async () => {
617
+ const cap = maxWatchedEntries();
618
+ const count = await countWatchableEntries(root, cap);
619
+ if (closed) return { ok: false, reason: "closed" };
620
+ if (count > cap) {
621
+ const message = `workspace at ${root} has more than ${cap} entries \u2014 file watching disabled. Start boring-ui in a smaller subfolder for live file updates, or raise BORING_MAX_WATCHED_ENTRIES.`;
622
+ log.error(message, { root, cap });
623
+ return { ok: false, reason: "workspace_too_large", message };
624
+ }
625
+ startFsw();
626
+ return { ok: true };
627
+ })();
628
+ return readiness;
486
629
  };
487
- const emit = (event) => {
630
+ const rel = (abs) => toPosixRel(root, abs);
631
+ const fanout = (event) => {
488
632
  if (closed) return;
489
633
  if (event.path === "" || event.path.startsWith("..")) return;
490
634
  for (const l of [...listeners]) {
@@ -494,26 +638,67 @@ function createNodeWatcher(root) {
494
638
  }
495
639
  }
496
640
  };
641
+ const isSuppressedEcho = (kind, path4) => {
642
+ const now = Date.now();
643
+ for (let i = suppressions.length - 1; i >= 0; i--) {
644
+ const s = suppressions[i];
645
+ if (now > s.idleDeadline || now > s.hardDeadline) {
646
+ suppressions.splice(i, 1);
647
+ continue;
648
+ }
649
+ if (s.kinds.has(kind) && (path4 === s.prefix || path4.startsWith(`${s.prefix}/`))) {
650
+ s.idleDeadline = Math.min(now + RENAME_ECHO_IDLE_MS, s.hardDeadline);
651
+ return true;
652
+ }
653
+ }
654
+ return false;
655
+ };
656
+ const emit = (kind, event) => {
657
+ if (isSuppressedEcho(kind, event.path)) return;
658
+ fanout(event);
659
+ };
497
660
  return {
498
661
  subscribe(listener) {
499
662
  if (closed) return () => {
500
663
  };
501
664
  listeners.add(listener);
502
- ensureFsw();
665
+ void ensureFsw();
503
666
  return () => {
504
667
  listeners.delete(listener);
505
668
  };
506
669
  },
670
+ whenReady() {
671
+ return ensureFsw();
672
+ },
673
+ emitRename(fromRel, toRel) {
674
+ if (closed) return;
675
+ if (fsw) {
676
+ const now = Date.now();
677
+ const window = {
678
+ idleDeadline: now + RENAME_ECHO_IDLE_MS,
679
+ hardDeadline: now + RENAME_ECHO_MAX_MS
680
+ };
681
+ suppressions.push(
682
+ { prefix: fromRel, kinds: RENAME_ECHO_FROM_KINDS, ...window },
683
+ { prefix: toRel, kinds: RENAME_ECHO_TO_KINDS, ...window }
684
+ );
685
+ }
686
+ fanout({ op: "rename", path: toRel, oldPath: fromRel });
687
+ },
507
688
  close() {
508
689
  if (closed) return;
509
690
  closed = true;
510
691
  listeners.clear();
692
+ suppressions.length = 0;
511
693
  fsw?.close().catch(() => {
512
694
  });
513
695
  fsw = null;
514
696
  }
515
697
  };
516
698
  }
699
+
700
+ // src/server/workspace/createNodeWorkspace.ts
701
+ var EPERM_CODE = "EPERM";
517
702
  var nodeWorkspaceHostRoots = /* @__PURE__ */ new WeakMap();
518
703
  function getNodeWorkspaceHostRoot(workspace) {
519
704
  return nodeWorkspaceHostRoots.get(workspace);
@@ -594,7 +779,7 @@ function createNodeWorkspace(root, opts = {}) {
594
779
  },
595
780
  async readdir(relPath) {
596
781
  const absPath = await ensureExistingWorkspacePath(root, relPath);
597
- const entries = await readdir(absPath, { withFileTypes: true });
782
+ const entries = await readdir2(absPath, { withFileTypes: true });
598
783
  return entries.map((entry) => ({
599
784
  name: entry.name,
600
785
  kind: entry.isDirectory() ? "dir" : "file"
@@ -632,6 +817,7 @@ function createNodeWorkspace(root, opts = {}) {
632
817
  const fromAbsPath = await ensureExistingWorkspacePath(root, fromRelPath);
633
818
  const toAbsPath = await ensureWritableWorkspacePath(root, toRelPath2);
634
819
  await rename(fromAbsPath, toAbsPath);
820
+ cachedWatcher?.emitRename(toPosixRel(root, fromAbsPath), toPosixRel(root, toAbsPath));
635
821
  }
636
822
  };
637
823
  nodeWorkspaceHostRoots.set(workspace, root);
@@ -757,8 +943,8 @@ async function buildGlobalToolMounts(workspaceRoot) {
757
943
  if (globalRoot === workspaceRoot) return [];
758
944
  const args = [];
759
945
  const mountIfChildLacks = async (runtimeRelPath) => {
760
- const parentRuntimePath = join2(globalRoot, runtimeRelPath);
761
- const childRuntimePath = join2(workspaceRoot, runtimeRelPath);
946
+ const parentRuntimePath = join3(globalRoot, runtimeRelPath);
947
+ const childRuntimePath = join3(workspaceRoot, runtimeRelPath);
762
948
  if (!await dirExists(parentRuntimePath)) return false;
763
949
  if (await pathExists(childRuntimePath)) return false;
764
950
  args.push("--ro-bind", parentRuntimePath, `${SANDBOX_HOME2}/${runtimeRelPath}`);
@@ -770,10 +956,45 @@ async function buildGlobalToolMounts(workspaceRoot) {
770
956
  }
771
957
  return args;
772
958
  }
959
+ function positiveInteger(value, name) {
960
+ if (value === void 0) return void 0;
961
+ if (!Number.isFinite(value) || value <= 0) {
962
+ throw new Error(`${name} must be a positive integer`);
963
+ }
964
+ return Math.floor(value);
965
+ }
966
+ function buildResourceLimitCommands(limits) {
967
+ if (!limits) return [];
968
+ const commands = [];
969
+ const cpuSeconds = positiveInteger(limits.cpuSeconds, "resourceLimits.cpuSeconds");
970
+ const fileSizeBlocks = positiveInteger(limits.fileSizeBlocks, "resourceLimits.fileSizeBlocks");
971
+ const maxProcesses = positiveInteger(limits.maxProcesses, "resourceLimits.maxProcesses");
972
+ const openFiles = positiveInteger(limits.openFiles, "resourceLimits.openFiles");
973
+ const virtualMemoryKb = positiveInteger(limits.virtualMemoryKb, "resourceLimits.virtualMemoryKb");
974
+ if (cpuSeconds !== void 0) commands.push(`ulimit -t ${cpuSeconds}`);
975
+ if (fileSizeBlocks !== void 0) commands.push(`ulimit -f ${fileSizeBlocks}`);
976
+ if (maxProcesses !== void 0) commands.push(`ulimit -u ${maxProcesses}`);
977
+ if (openFiles !== void 0) commands.push(`ulimit -n ${openFiles}`);
978
+ if (virtualMemoryKb !== void 0) commands.push(`ulimit -v ${virtualMemoryKb}`);
979
+ return commands;
980
+ }
981
+ function buildCommandArgs(cmd, limits) {
982
+ const limitCommands = buildResourceLimitCommands(limits);
983
+ if (limitCommands.length === 0) return ["bash", "-c", cmd];
984
+ return [
985
+ "bash",
986
+ "-c",
987
+ `${limitCommands.join("\n")}
988
+ exec bash -c "$1"`,
989
+ "boring-exec",
990
+ cmd
991
+ ];
992
+ }
773
993
  function createBwrapSandbox(opts = {}) {
994
+ const sandboxOptions = opts;
774
995
  let workspace = null;
775
- let hostWorkspaceRoot = opts.hostWorkspaceRoot;
776
- let runtimeContext = opts.runtimeContext ?? { runtimeCwd: SANDBOX_HOME2 };
996
+ let hostWorkspaceRoot = sandboxOptions.hostWorkspaceRoot;
997
+ let runtimeContext = sandboxOptions.runtimeContext ?? { runtimeCwd: SANDBOX_HOME2 };
777
998
  return {
778
999
  id: "bwrap",
779
1000
  placement: "server",
@@ -784,8 +1005,8 @@ function createBwrapSandbox(opts = {}) {
784
1005
  },
785
1006
  async init(ctx) {
786
1007
  workspace = ctx.workspace;
787
- hostWorkspaceRoot = opts.hostWorkspaceRoot ?? getNodeWorkspaceHostRoot(ctx.workspace) ?? ctx.workspace.root;
788
- runtimeContext = opts.runtimeContext ?? { runtimeCwd: SANDBOX_HOME2 };
1008
+ hostWorkspaceRoot = sandboxOptions.hostWorkspaceRoot ?? getNodeWorkspaceHostRoot(ctx.workspace) ?? ctx.workspace.root;
1009
+ runtimeContext = sandboxOptions.runtimeContext ?? { runtimeCwd: SANDBOX_HOME2 };
789
1010
  await assertBwrapAvailable();
790
1011
  },
791
1012
  async exec(cmd, opts2) {
@@ -798,12 +1019,14 @@ function createBwrapSandbox(opts = {}) {
798
1019
  const workspaceRoot = hostWorkspaceRoot ?? workspace.root;
799
1020
  const sandboxCwd = computeSandboxCwd(workspaceRoot, runtimeContext.runtimeCwd, opts2?.cwd);
800
1021
  const postWorkspaceArgs = await buildGlobalToolMounts(workspaceRoot);
801
- const baseArgs = buildBwrapArgs(workspaceRoot, { postWorkspaceArgs });
1022
+ const baseArgs = buildBwrapArgs(workspaceRoot, {
1023
+ postWorkspaceArgs,
1024
+ network: sandboxOptions.network,
1025
+ dropAllCapabilities: sandboxOptions.dropAllCapabilities
1026
+ });
802
1027
  const args = [
803
1028
  ...withSandboxCwd(baseArgs, sandboxCwd),
804
- "bash",
805
- "-c",
806
- cmd
1029
+ ...buildCommandArgs(cmd, sandboxOptions.resourceLimits)
807
1030
  ];
808
1031
  return await new Promise((resolve11, reject) => {
809
1032
  const child = spawn2("bwrap", args, {
@@ -1634,6 +1857,492 @@ async function prepareVercelDeploymentSnapshot(opts) {
1634
1857
  });
1635
1858
  }
1636
1859
 
1860
+ // src/server/runtime/createServerFileSearch.ts
1861
+ var DEFAULT_LIMIT = 500;
1862
+ var MAX_LIMIT = 5e3;
1863
+ var SEARCH_TIMEOUT_MS = 5e3;
1864
+ var SEARCH_MAX_OUTPUT_BYTES = 256e3;
1865
+ function shellQuote2(value) {
1866
+ return `'${value.replaceAll("'", "'\\''")}'`;
1867
+ }
1868
+ function normalizeLimit(limit) {
1869
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
1870
+ return DEFAULT_LIMIT;
1871
+ }
1872
+ const normalized = Math.trunc(limit);
1873
+ if (normalized <= 0) return DEFAULT_LIMIT;
1874
+ return Math.min(normalized, MAX_LIMIT);
1875
+ }
1876
+ function buildFindArgs(glob) {
1877
+ const isPathShaped = glob.includes("/") || glob.includes("**");
1878
+ if (!isPathShaped) {
1879
+ return `-iname ${shellQuote2(glob)}`;
1880
+ }
1881
+ let translated = glob.replaceAll("**", "*");
1882
+ translated = translated.replace(/^\/+/, "");
1883
+ if (!translated.startsWith("*")) {
1884
+ translated = `*${translated}`;
1885
+ }
1886
+ return `-ipath ${shellQuote2(translated)}`;
1887
+ }
1888
+ function createServerFileSearch(workspace, sandbox) {
1889
+ return {
1890
+ async search(glob, limit = DEFAULT_LIMIT) {
1891
+ const safeLimit = normalizeLimit(limit);
1892
+ const command = [
1893
+ "find .",
1894
+ "-maxdepth 10",
1895
+ "\\( -path './.boring-agent' -o -path './.git' -o -path './node_modules' \\) -prune -o",
1896
+ buildFindArgs(glob),
1897
+ "-type f",
1898
+ "-print",
1899
+ `| head -n ${safeLimit}`
1900
+ ].join(" ");
1901
+ const { stdout, exitCode } = await sandbox.exec(command, {
1902
+ cwd: workspace.root,
1903
+ timeoutMs: SEARCH_TIMEOUT_MS,
1904
+ maxOutputBytes: SEARCH_MAX_OUTPUT_BYTES
1905
+ });
1906
+ if (exitCode !== 0) {
1907
+ throw new Error(`file-search failed: exit ${exitCode}`);
1908
+ }
1909
+ const decoded = new TextDecoder().decode(stdout);
1910
+ return decoded.split("\n").map((line) => line.replace(/\r$/, "")).filter((line) => line.length > 0).map((line) => line.replace(/^\.\//, ""));
1911
+ }
1912
+ };
1913
+ }
1914
+
1915
+ // src/server/sandbox/remote-worker/protocol.ts
1916
+ var REMOTE_WORKER_RUNTIME_CWD = "/workspace";
1917
+ var REMOTE_WORKER_PROVIDER = "remote-worker";
1918
+ var WORKER_INTERNAL_TOKEN_HEADER = "x-boring-internal-token";
1919
+ var WORKER_WORKSPACE_ID_HEADER = "x-boring-workspace-id";
1920
+ var WORKER_REQUEST_ID_HEADER = "x-boring-request-id";
1921
+
1922
+ // src/server/sandbox/remote-worker/createRemoteWorkerSandbox.ts
1923
+ function filteredEnv(env) {
1924
+ if (!env) return void 0;
1925
+ return Object.fromEntries(
1926
+ Object.entries(env).filter((entry) => typeof entry[1] === "string")
1927
+ );
1928
+ }
1929
+ function createRemoteWorkerSandbox(client) {
1930
+ return {
1931
+ id: REMOTE_WORKER_PROVIDER,
1932
+ placement: "remote",
1933
+ provider: REMOTE_WORKER_PROVIDER,
1934
+ capabilities: ["exec"],
1935
+ runtimeContext: { runtimeCwd: REMOTE_WORKER_RUNTIME_CWD },
1936
+ async init() {
1937
+ await client.health();
1938
+ },
1939
+ async exec(cmd, opts = {}) {
1940
+ const request = {
1941
+ cmd,
1942
+ cwd: opts.cwd,
1943
+ env: filteredEnv(opts.env),
1944
+ timeoutMs: opts.timeoutMs,
1945
+ maxOutputBytes: opts.maxOutputBytes
1946
+ };
1947
+ const startedAt = Date.now();
1948
+ const heartbeat = opts.onHeartbeat ? setInterval(() => opts.onHeartbeat?.(Date.now() - startedAt), 1e3) : null;
1949
+ try {
1950
+ const result = await client.exec(request, { signal: opts.signal });
1951
+ opts.onStdout?.(result.stdout);
1952
+ opts.onStderr?.(result.stderr);
1953
+ return result;
1954
+ } finally {
1955
+ if (heartbeat) clearInterval(heartbeat);
1956
+ }
1957
+ }
1958
+ };
1959
+ }
1960
+
1961
+ // src/server/sandbox/remote-worker/workerClient.ts
1962
+ import { timingSafeEqual } from "crypto";
1963
+ var DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
1964
+ var DEFAULT_EXEC_TIMEOUT_MS = 3e4;
1965
+ var DEFAULT_EXEC_REQUEST_GRACE_MS = 1e4;
1966
+ var RemoteWorkerClientError = class extends Error {
1967
+ statusCode;
1968
+ code;
1969
+ details;
1970
+ constructor(message, opts) {
1971
+ super(message);
1972
+ this.name = "RemoteWorkerClientError";
1973
+ this.statusCode = opts.statusCode;
1974
+ this.code = opts.code ?? "remote_worker_error";
1975
+ this.details = opts.details;
1976
+ }
1977
+ };
1978
+ function requireNonEmpty(value, label) {
1979
+ const trimmed = value.trim();
1980
+ if (!trimmed) throw new Error(`${label} is required for ${REMOTE_WORKER_PROVIDER} mode`);
1981
+ return trimmed;
1982
+ }
1983
+ function normalizeBaseUrl(value) {
1984
+ return requireNonEmpty(value, "BORING_WORKER_BASE_URL").replace(/\/+$/, "");
1985
+ }
1986
+ function positiveInteger2(value, fallback, label) {
1987
+ if (value === void 0) return fallback;
1988
+ if (!Number.isFinite(value) || value <= 0) throw new Error(`${label} must be a positive integer`);
1989
+ return Math.trunc(value);
1990
+ }
1991
+ function encodeWorkspaceId(workspaceId) {
1992
+ return encodeURIComponent(requireNonEmpty(workspaceId, "workspaceId"));
1993
+ }
1994
+ function bytesToBase64(bytes) {
1995
+ return Buffer.from(bytes).toString("base64");
1996
+ }
1997
+ function base64ToBytes(value) {
1998
+ return new Uint8Array(Buffer.from(value, "base64"));
1999
+ }
2000
+ function makeHeaders(opts) {
2001
+ const headers = new Headers();
2002
+ headers.set(WORKER_INTERNAL_TOKEN_HEADER, requireNonEmpty(opts.token, "BORING_WORKER_INTERNAL_TOKEN"));
2003
+ headers.set(WORKER_WORKSPACE_ID_HEADER, requireNonEmpty(opts.workspaceId, "workspaceId"));
2004
+ if (opts.requestId) headers.set(WORKER_REQUEST_ID_HEADER, opts.requestId);
2005
+ if (opts.contentType) headers.set("content-type", opts.contentType);
2006
+ return headers;
2007
+ }
2008
+ async function parseError(response) {
2009
+ let payload;
2010
+ try {
2011
+ payload = await response.json();
2012
+ } catch {
2013
+ }
2014
+ return new RemoteWorkerClientError(
2015
+ payload?.error?.message ?? `remote worker request failed (${response.status})`,
2016
+ {
2017
+ statusCode: payload?.error?.statusCode ?? response.status,
2018
+ code: payload?.error?.code,
2019
+ details: payload?.error?.details
2020
+ }
2021
+ );
2022
+ }
2023
+ var RemoteWorkerClient = class {
2024
+ baseUrl;
2025
+ token;
2026
+ workspaceId;
2027
+ requestId;
2028
+ fetchImpl;
2029
+ requestTimeoutMs;
2030
+ execTimeoutMs;
2031
+ execRequestGraceMs;
2032
+ constructor(opts) {
2033
+ this.baseUrl = normalizeBaseUrl(opts.baseUrl);
2034
+ this.token = requireNonEmpty(opts.token, "BORING_WORKER_INTERNAL_TOKEN");
2035
+ this.workspaceId = requireNonEmpty(opts.workspaceId, "workspaceId");
2036
+ this.requestId = opts.requestId;
2037
+ this.fetchImpl = opts.fetchImpl ?? fetch;
2038
+ this.requestTimeoutMs = positiveInteger2(opts.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS, "requestTimeoutMs");
2039
+ this.execTimeoutMs = positiveInteger2(opts.execTimeoutMs, DEFAULT_EXEC_TIMEOUT_MS, "execTimeoutMs");
2040
+ this.execRequestGraceMs = positiveInteger2(opts.execRequestGraceMs, DEFAULT_EXEC_REQUEST_GRACE_MS, "execRequestGraceMs");
2041
+ }
2042
+ timeoutError(timeoutMs) {
2043
+ return new RemoteWorkerClientError("remote worker request timed out", {
2044
+ statusCode: 504,
2045
+ code: ErrorCode.enum.REMOTE_WORKER_TIMEOUT,
2046
+ details: { timeoutMs, retryable: true }
2047
+ });
2048
+ }
2049
+ abortedError() {
2050
+ return new RemoteWorkerClientError("remote worker request aborted", {
2051
+ statusCode: 499,
2052
+ code: ErrorCode.enum.ABORTED
2053
+ });
2054
+ }
2055
+ async fetchWithTimeout(url, init, timeoutMs) {
2056
+ const controller = new AbortController();
2057
+ let timedOut = false;
2058
+ const timer = setTimeout(() => {
2059
+ timedOut = true;
2060
+ controller.abort();
2061
+ }, timeoutMs);
2062
+ const upstreamSignal = init.signal;
2063
+ const abortFromUpstream = () => controller.abort();
2064
+ if (upstreamSignal?.aborted) {
2065
+ clearTimeout(timer);
2066
+ throw this.abortedError();
2067
+ }
2068
+ upstreamSignal?.addEventListener("abort", abortFromUpstream, { once: true });
2069
+ try {
2070
+ return await this.fetchImpl(url, { ...init, signal: controller.signal });
2071
+ } catch (error) {
2072
+ if (timedOut) throw this.timeoutError(timeoutMs);
2073
+ if (upstreamSignal?.aborted) throw this.abortedError();
2074
+ throw error;
2075
+ } finally {
2076
+ clearTimeout(timer);
2077
+ upstreamSignal?.removeEventListener("abort", abortFromUpstream);
2078
+ }
2079
+ }
2080
+ async health() {
2081
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/internal/health`, {
2082
+ headers: makeHeaders({ token: this.token, workspaceId: this.workspaceId, requestId: this.requestId })
2083
+ }, this.requestTimeoutMs);
2084
+ if (!response.ok) throw await parseError(response);
2085
+ }
2086
+ async workspace(op) {
2087
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/internal/workspaces/${encodeWorkspaceId(this.workspaceId)}/fs`, {
2088
+ method: "POST",
2089
+ headers: makeHeaders({ token: this.token, workspaceId: this.workspaceId, requestId: this.requestId, contentType: "application/json" }),
2090
+ body: JSON.stringify(op)
2091
+ }, this.requestTimeoutMs);
2092
+ if (!response.ok) throw await parseError(response);
2093
+ return await response.json();
2094
+ }
2095
+ async exec(input, opts = {}) {
2096
+ const timeoutMs = (input.timeoutMs ?? this.execTimeoutMs) + this.execRequestGraceMs;
2097
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/internal/workspaces/${encodeWorkspaceId(this.workspaceId)}/exec`, {
2098
+ method: "POST",
2099
+ headers: makeHeaders({ token: this.token, workspaceId: this.workspaceId, requestId: this.requestId, contentType: "application/json" }),
2100
+ body: JSON.stringify(input),
2101
+ signal: opts.signal
2102
+ }, timeoutMs);
2103
+ if (!response.ok) throw await parseError(response);
2104
+ const body = await response.json();
2105
+ return {
2106
+ stdout: base64ToBytes(body.stdoutBase64),
2107
+ stderr: base64ToBytes(body.stderrBase64),
2108
+ exitCode: body.exitCode,
2109
+ durationMs: body.durationMs,
2110
+ truncated: body.truncated,
2111
+ stdoutEncoding: body.stdoutEncoding,
2112
+ stderrEncoding: body.stderrEncoding
2113
+ };
2114
+ }
2115
+ watch(onEvent, onError) {
2116
+ const controller = new AbortController();
2117
+ void this.consumeEvents(controller.signal, onEvent).catch((error) => {
2118
+ if (!controller.signal.aborted) onError?.(error instanceof Error ? error : new Error(String(error)));
2119
+ });
2120
+ return { close: () => controller.abort() };
2121
+ }
2122
+ async consumeEvents(signal, onEvent) {
2123
+ const response = await this.fetchImpl(`${this.baseUrl}/internal/workspaces/${encodeWorkspaceId(this.workspaceId)}/fs/events`, {
2124
+ headers: makeHeaders({ token: this.token, workspaceId: this.workspaceId, requestId: this.requestId }),
2125
+ signal
2126
+ });
2127
+ if (!response.ok) throw await parseError(response);
2128
+ if (!response.body) throw new RemoteWorkerClientError("remote worker event stream missing body", { statusCode: 502 });
2129
+ const reader = response.body.getReader();
2130
+ const decoder2 = new TextDecoder();
2131
+ let buffer = "";
2132
+ try {
2133
+ for (; ; ) {
2134
+ const { done, value } = await reader.read();
2135
+ if (done) {
2136
+ if (signal.aborted) return;
2137
+ throw new RemoteWorkerClientError("remote worker event stream closed", {
2138
+ statusCode: 502,
2139
+ code: ErrorCode.enum.REMOTE_WORKER_STREAM_CLOSED
2140
+ });
2141
+ }
2142
+ buffer += decoder2.decode(value, { stream: true });
2143
+ let boundary = buffer.indexOf("\n\n");
2144
+ while (boundary >= 0) {
2145
+ const frame = buffer.slice(0, boundary);
2146
+ buffer = buffer.slice(boundary + 2);
2147
+ this.handleSseFrame(frame, onEvent);
2148
+ boundary = buffer.indexOf("\n\n");
2149
+ }
2150
+ }
2151
+ } finally {
2152
+ reader.releaseLock();
2153
+ }
2154
+ }
2155
+ handleSseFrame(frame, onEvent) {
2156
+ let eventName = "message";
2157
+ const dataLines = [];
2158
+ for (const line of frame.split("\n")) {
2159
+ if (line.startsWith(":")) continue;
2160
+ if (line.startsWith("event:")) eventName = line.slice("event:".length).trim();
2161
+ if (line.startsWith("data:")) dataLines.push(line.slice("data:".length).trimStart());
2162
+ }
2163
+ if (eventName !== "change" || dataLines.length === 0) return;
2164
+ const payload = JSON.parse(dataLines.join("\n"));
2165
+ onEvent(payload.event);
2166
+ }
2167
+ };
2168
+ function encodeBytesForWorker(bytes) {
2169
+ return bytesToBase64(bytes);
2170
+ }
2171
+ function decodeBytesFromWorker(value) {
2172
+ return base64ToBytes(value);
2173
+ }
2174
+ function constantTimeTokenEqual(a, b) {
2175
+ if (!a || !b) return false;
2176
+ const aBytes = Buffer.from(a);
2177
+ const bBytes = Buffer.from(b);
2178
+ if (aBytes.length !== bBytes.length) return false;
2179
+ return timingSafeEqual(aBytes, bBytes);
2180
+ }
2181
+
2182
+ // src/server/workspace/createRemoteWorkerWorkspace.ts
2183
+ function expectContent(result) {
2184
+ if ("content" in result && typeof result.content === "string") return result.content;
2185
+ throw new Error("remote worker returned invalid file content response");
2186
+ }
2187
+ function expectData(result) {
2188
+ if ("dataBase64" in result && typeof result.dataBase64 === "string") return decodeBytesFromWorker(result.dataBase64);
2189
+ throw new Error("remote worker returned invalid binary response");
2190
+ }
2191
+ function expectStat(result) {
2192
+ if ("stat" in result && result.stat) return result.stat;
2193
+ throw new Error("remote worker returned invalid stat response");
2194
+ }
2195
+ function expectEntries(result) {
2196
+ if ("entries" in result && Array.isArray(result.entries)) return result.entries;
2197
+ throw new Error("remote worker returned invalid readdir response");
2198
+ }
2199
+ function createRemoteWatcher(client) {
2200
+ const listeners = /* @__PURE__ */ new Map();
2201
+ let stream = null;
2202
+ let reconnectTimer = null;
2203
+ let closed = false;
2204
+ const clearReconnectTimer = () => {
2205
+ if (!reconnectTimer) return;
2206
+ clearTimeout(reconnectTimer);
2207
+ reconnectTimer = null;
2208
+ };
2209
+ const scheduleReconnect = () => {
2210
+ if (closed || listeners.size === 0 || reconnectTimer) return;
2211
+ reconnectTimer = setTimeout(() => {
2212
+ reconnectTimer = null;
2213
+ ensureStream();
2214
+ }, 1e3);
2215
+ };
2216
+ const handleStreamEnd = () => {
2217
+ stream = null;
2218
+ for (const options of [...listeners.values()]) {
2219
+ try {
2220
+ options?.onControlEvent?.({ type: "resync-required", reason: "remote_worker_stream_closed" });
2221
+ } catch {
2222
+ }
2223
+ }
2224
+ scheduleReconnect();
2225
+ };
2226
+ const ensureStream = () => {
2227
+ if (stream || closed || listeners.size === 0) return;
2228
+ clearReconnectTimer();
2229
+ stream = client.watch((event) => {
2230
+ for (const listener of [...listeners.keys()]) {
2231
+ try {
2232
+ listener(event);
2233
+ } catch {
2234
+ }
2235
+ }
2236
+ }, handleStreamEnd);
2237
+ };
2238
+ return {
2239
+ subscribe(listener, options) {
2240
+ if (closed) return () => {
2241
+ };
2242
+ listeners.set(listener, options);
2243
+ ensureStream();
2244
+ return () => {
2245
+ listeners.delete(listener);
2246
+ if (listeners.size === 0) {
2247
+ clearReconnectTimer();
2248
+ stream?.close();
2249
+ stream = null;
2250
+ }
2251
+ };
2252
+ },
2253
+ close() {
2254
+ if (closed) return;
2255
+ closed = true;
2256
+ listeners.clear();
2257
+ clearReconnectTimer();
2258
+ stream?.close();
2259
+ stream = null;
2260
+ }
2261
+ };
2262
+ }
2263
+ function createRemoteWorkerWorkspace(client) {
2264
+ let watcher = null;
2265
+ return {
2266
+ root: REMOTE_WORKER_RUNTIME_CWD,
2267
+ runtimeContext: { runtimeCwd: REMOTE_WORKER_RUNTIME_CWD },
2268
+ fsCapability: "best-effort",
2269
+ watch() {
2270
+ watcher ??= createRemoteWatcher(client);
2271
+ return watcher;
2272
+ },
2273
+ async readFile(path4) {
2274
+ return expectContent(await client.workspace({ op: "readFile", path: path4 }));
2275
+ },
2276
+ async readBinaryFile(path4) {
2277
+ return expectData(await client.workspace({ op: "readBinaryFile", path: path4 }));
2278
+ },
2279
+ async writeFile(path4, data) {
2280
+ await client.workspace({ op: "writeFile", path: path4, data });
2281
+ },
2282
+ async writeBinaryFile(path4, data) {
2283
+ await client.workspace({ op: "writeBinaryFile", path: path4, dataBase64: encodeBytesForWorker(data) });
2284
+ },
2285
+ async readFileWithStat(path4) {
2286
+ const result = await client.workspace({ op: "readFileWithStat", path: path4 });
2287
+ if ("content" in result && "stat" in result) return { content: result.content, stat: result.stat };
2288
+ throw new Error("remote worker returned invalid readFileWithStat response");
2289
+ },
2290
+ async writeFileWithStat(path4, data) {
2291
+ return expectStat(await client.workspace({ op: "writeFileWithStat", path: path4, data }));
2292
+ },
2293
+ async writeBinaryFileWithStat(path4, data) {
2294
+ return expectStat(await client.workspace({ op: "writeBinaryFileWithStat", path: path4, dataBase64: encodeBytesForWorker(data) }));
2295
+ },
2296
+ async unlink(path4) {
2297
+ await client.workspace({ op: "unlink", path: path4 });
2298
+ },
2299
+ async readdir(path4) {
2300
+ return expectEntries(await client.workspace({ op: "readdir", path: path4 }));
2301
+ },
2302
+ async stat(path4) {
2303
+ return expectStat(await client.workspace({ op: "stat", path: path4 }));
2304
+ },
2305
+ async mkdir(path4, opts) {
2306
+ await client.workspace({ op: "mkdir", path: path4, recursive: opts?.recursive });
2307
+ },
2308
+ async rename(from, to) {
2309
+ await client.workspace({ op: "rename", from, to });
2310
+ }
2311
+ };
2312
+ }
2313
+
2314
+ // src/server/runtime/modes/remote-worker.ts
2315
+ function requireOption(value, name) {
2316
+ const trimmed = value?.trim();
2317
+ if (!trimmed) throw new Error(`${name} is required for remote-worker mode`);
2318
+ return trimmed;
2319
+ }
2320
+ function createRemoteWorkerModeAdapter(opts = {}) {
2321
+ return {
2322
+ id: REMOTE_WORKER_PROVIDER,
2323
+ workspaceFsCapability: "best-effort",
2324
+ async create(ctx) {
2325
+ const workspaceId = requireOption(ctx.workspaceId ?? ctx.sessionId, "workspaceId");
2326
+ const client = new RemoteWorkerClient({
2327
+ baseUrl: requireOption(opts.baseUrl ?? getEnv("BORING_WORKER_BASE_URL"), "BORING_WORKER_BASE_URL"),
2328
+ token: requireOption(opts.token ?? getEnv("BORING_WORKER_INTERNAL_TOKEN"), "BORING_WORKER_INTERNAL_TOKEN"),
2329
+ workspaceId,
2330
+ requestId: ctx.requestId,
2331
+ fetchImpl: opts.fetchImpl
2332
+ });
2333
+ const workspace = createRemoteWorkerWorkspace(client);
2334
+ const sandbox = createRemoteWorkerSandbox(client);
2335
+ await sandbox.init?.({ workspace, sessionId: ctx.sessionId });
2336
+ return {
2337
+ runtimeContext: { runtimeCwd: REMOTE_WORKER_RUNTIME_CWD },
2338
+ workspace,
2339
+ sandbox,
2340
+ fileSearch: createServerFileSearch(workspace, sandbox)
2341
+ };
2342
+ }
2343
+ };
2344
+ }
2345
+
1637
2346
  // src/server/http/routes/file.ts
1638
2347
  import { dirname as dirname5, extname as extname2, relative as relative4 } from "path/posix";
1639
2348
 
@@ -1682,102 +2391,22 @@ function createAuthMiddleware(opts = {}) {
1682
2391
  }
1683
2392
  return;
1684
2393
  }
1685
- const header = request.headers.authorization;
1686
- if (typeof header !== "string" || !header.startsWith("Bearer ")) {
1687
- reply.code(401).send(
1688
- errorPayload(
1689
- ERROR_CODE_AUTH_REQUIRED,
1690
- "Missing Bearer token"
1691
- )
1692
- );
1693
- return;
1694
- }
1695
- const candidateToken = header.slice("Bearer ".length);
1696
- if (candidateToken !== authToken) {
1697
- reply.code(403).send(errorPayload(ERROR_CODE_AUTH_INVALID, "Invalid token"));
1698
- return;
1699
- }
1700
- ensureWorkspaceContext(request, workspaceId, true);
1701
- };
1702
- }
1703
-
1704
- // src/server/logging.ts
1705
- var SENSITIVE_KEYS = new Set([
1706
- "apiKey",
1707
- "api_key",
1708
- "token",
1709
- "secret",
1710
- "password",
1711
- "authorization",
1712
- "cookie",
1713
- "oidcToken",
1714
- "accessToken",
1715
- "refreshToken",
1716
- "ANTHROPIC_API_KEY",
1717
- "OPENAI_API_KEY",
1718
- "VERCEL_OIDC_TOKEN",
1719
- "VERCEL_TEAM_ID"
1720
- ].map((key) => key.toLowerCase()));
1721
- function isSensitiveKey(key) {
1722
- return SENSITIVE_KEYS.has(key.toLowerCase());
1723
- }
1724
- function redactValue(key, value, seen) {
1725
- if (key && isSensitiveKey(key) && value != null) return "***";
1726
- if (value == null || typeof value !== "object") return value;
1727
- if (value instanceof Date) return value;
1728
- if (seen.has(value)) return "[Circular]";
1729
- seen.add(value);
1730
- if (Array.isArray(value)) {
1731
- const out2 = value.map((item) => redactValue(void 0, item, seen));
1732
- seen.delete(value);
1733
- return out2;
1734
- }
1735
- const out = {};
1736
- for (const [childKey, childValue] of Object.entries(value)) {
1737
- out[childKey] = redactValue(childKey, childValue, seen);
1738
- }
1739
- seen.delete(value);
1740
- return out;
1741
- }
1742
- function redact(fields) {
1743
- const out = {};
1744
- const seen = /* @__PURE__ */ new WeakSet();
1745
- for (const [k, v] of Object.entries(fields)) {
1746
- out[k] = redactValue(k, v, seen);
1747
- }
1748
- return out;
1749
- }
1750
- var verbose = typeof process !== "undefined" && process.env?.BORING_AGENT_VERBOSE === "1";
1751
- function createLogger(prefix) {
1752
- function emit(level, msg, fields) {
1753
- const entry = {
1754
- level,
1755
- prefix,
1756
- msg,
1757
- ...fields ? redact(fields) : {},
1758
- t: (/* @__PURE__ */ new Date()).toISOString()
1759
- };
1760
- if (level === "error") {
1761
- console.error(JSON.stringify(entry));
1762
- } else if (level === "warn") {
1763
- console.warn(JSON.stringify(entry));
1764
- } else {
1765
- console.log(JSON.stringify(entry));
1766
- }
1767
- }
1768
- return {
1769
- debug(msg, fields) {
1770
- if (verbose) emit("debug", msg, fields);
1771
- },
1772
- info(msg, fields) {
1773
- emit("info", msg, fields);
1774
- },
1775
- warn(msg, fields) {
1776
- emit("warn", msg, fields);
1777
- },
1778
- error(msg, fields) {
1779
- emit("error", msg, fields);
2394
+ const header = request.headers.authorization;
2395
+ if (typeof header !== "string" || !header.startsWith("Bearer ")) {
2396
+ reply.code(401).send(
2397
+ errorPayload(
2398
+ ERROR_CODE_AUTH_REQUIRED,
2399
+ "Missing Bearer token"
2400
+ )
2401
+ );
2402
+ return;
2403
+ }
2404
+ const candidateToken = header.slice("Bearer ".length);
2405
+ if (candidateToken !== authToken) {
2406
+ reply.code(403).send(errorPayload(ERROR_CODE_AUTH_INVALID, "Invalid token"));
2407
+ return;
1780
2408
  }
2409
+ ensureWorkspaceContext(request, workspaceId, true);
1781
2410
  };
1782
2411
  }
1783
2412
 
@@ -1987,7 +2616,7 @@ function buildFileRecordsResult(args) {
1987
2616
  }
1988
2617
 
1989
2618
  // src/server/http/routes/file.ts
1990
- var log = createLogger("boring/workspace-settings");
2619
+ var log2 = createLogger("boring/workspace-settings");
1991
2620
  var BORING_SETTINGS_PATH = ".boring/settings";
1992
2621
  var DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR = "assets/images";
1993
2622
  var MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
@@ -2062,7 +2691,7 @@ function parseWorkspaceSettings(raw) {
2062
2691
  }
2063
2692
  };
2064
2693
  } catch (err) {
2065
- log.warn("failed to parse .boring/settings \u2014 falling back to defaults", {
2694
+ log2.warn("failed to parse .boring/settings \u2014 falling back to defaults", {
2066
2695
  error: err instanceof Error ? err.message : String(err)
2067
2696
  });
2068
2697
  return defaultWorkspaceSettings();
@@ -2410,8 +3039,8 @@ function fileRoutes(app, opts, done) {
2410
3039
  // src/server/workspace/provisionRuntime.ts
2411
3040
  import { createHash as createHash3 } from "crypto";
2412
3041
  import { constants as constants2 } from "fs";
2413
- import { access as access2, chmod as chmod3, cp, mkdir as mkdir4, readdir as readdir2, readFile as readFile4, realpath as realpath2, stat as stat4, writeFile as writeFile4 } from "fs/promises";
2414
- import { dirname as dirname6, isAbsolute as isAbsolute4, join as join3, relative as relative5, resolve as resolve5 } from "path";
3042
+ import { access as access2, chmod as chmod3, cp, mkdir as mkdir4, readdir as readdir3, readFile as readFile4, realpath as realpath2, stat as stat4, writeFile as writeFile4 } from "fs/promises";
3043
+ import { dirname as dirname6, isAbsolute as isAbsolute4, join as join4, relative as relative5, resolve as resolve5 } from "path";
2415
3044
  import { fileURLToPath } from "url";
2416
3045
  import { execFile } from "child_process";
2417
3046
  import { promisify } from "util";
@@ -2447,11 +3076,11 @@ async function commandExists(cmd) {
2447
3076
  async function hashPath(path4, hash) {
2448
3077
  const info = await stat4(path4);
2449
3078
  if (info.isDirectory()) {
2450
- const entries = (await readdir2(path4)).filter((entry) => entry !== "__pycache__" && entry !== "build" && !entry.endsWith(".egg-info")).sort();
3079
+ const entries = (await readdir3(path4)).filter((entry) => entry !== "__pycache__" && entry !== "build" && !entry.endsWith(".egg-info")).sort();
2451
3080
  for (const entry of entries) {
2452
3081
  hash.update(`dir:${entry}
2453
3082
  `);
2454
- await hashPath(join3(path4, entry), hash);
3083
+ await hashPath(join4(path4, entry), hash);
2455
3084
  }
2456
3085
  return;
2457
3086
  }
@@ -2484,16 +3113,16 @@ async function fingerprint(contributions) {
2484
3113
  const packageRoot = toPath(spec.packageRoot);
2485
3114
  hash.update(`node-package:${spec.id}:${spec.packageName}:${packageRoot}
2486
3115
  `);
2487
- const packageJson = join3(packageRoot, "package.json");
2488
- const distDir = join3(packageRoot, "dist");
2489
- const docsDir = join3(packageRoot, "docs");
2490
- const skillsDir = join3(packageRoot, "skills");
2491
- const referencesDir = join3(packageRoot, "references");
2492
- const sourceDocsDir = join3(packageRoot, "src", "server", "docs");
3116
+ const packageJson = join4(packageRoot, "package.json");
3117
+ const distDir = join4(packageRoot, "dist");
3118
+ const docsDir = join4(packageRoot, "docs");
3119
+ const skillsDir = join4(packageRoot, "skills");
3120
+ const referencesDir = join4(packageRoot, "references");
3121
+ const sourceDocsDir = join4(packageRoot, "src", "server", "docs");
2493
3122
  if (await exists(packageJson)) await hashPath(packageJson, hash);
2494
3123
  if (await exists(distDir)) await hashPath(distDir, hash);
2495
- const templatesDir = join3(packageRoot, "templates");
2496
- const publicDir = join3(packageRoot, "public");
3124
+ const templatesDir = join4(packageRoot, "templates");
3125
+ const publicDir = join4(packageRoot, "public");
2497
3126
  if (await exists(templatesDir)) await hashPath(templatesDir, hash);
2498
3127
  if (await exists(publicDir)) await hashPath(publicDir, hash);
2499
3128
  if (await exists(docsDir)) await hashPath(docsDir, hash);
@@ -2544,7 +3173,7 @@ function nodePackageTarget(workspaceRoot, packageName) {
2544
3173
  if (parts.length === 0 || parts.some((part) => part === "." || part === "..")) {
2545
3174
  throw new Error(`Invalid node package name: ${packageName}`);
2546
3175
  }
2547
- return join3(workspaceRoot, "node_modules", ...parts);
3176
+ return join4(workspaceRoot, "node_modules", ...parts);
2548
3177
  }
2549
3178
  async function copyIfExists(source, target) {
2550
3179
  if (!await exists(source)) return false;
@@ -2562,27 +3191,27 @@ async function ensureNodePackages(workspaceRoot, specs) {
2562
3191
  const packageRoot = toPath(spec.packageRoot);
2563
3192
  const target = nodePackageTarget(workspaceRoot, spec.packageName);
2564
3193
  await mkdir4(target, { recursive: true });
2565
- const copiedPackageJson = await copyIfExists(join3(packageRoot, "package.json"), join3(target, "package.json"));
3194
+ const copiedPackageJson = await copyIfExists(join4(packageRoot, "package.json"), join4(target, "package.json"));
2566
3195
  if (!copiedPackageJson) {
2567
3196
  throw new Error(`Node package provisioning source is missing package.json: ${packageRoot}`);
2568
3197
  }
2569
- await copyIfExists(join3(packageRoot, "dist"), join3(target, "dist"));
2570
- await copyIfExists(join3(packageRoot, "templates"), join3(target, "templates"));
2571
- await copyIfExists(join3(packageRoot, "public"), join3(target, "public"));
2572
- if (!await exists(join3(target, "dist", "docs", "plugins.md"))) {
2573
- await copyIfExists(join3(packageRoot, "src", "server", "docs"), join3(target, "dist", "docs"));
3198
+ await copyIfExists(join4(packageRoot, "dist"), join4(target, "dist"));
3199
+ await copyIfExists(join4(packageRoot, "templates"), join4(target, "templates"));
3200
+ await copyIfExists(join4(packageRoot, "public"), join4(target, "public"));
3201
+ if (!await exists(join4(target, "dist", "docs", "plugins.md"))) {
3202
+ await copyIfExists(join4(packageRoot, "src", "server", "docs"), join4(target, "dist", "docs"));
2574
3203
  }
2575
- if (!await copyIfExists(join3(packageRoot, "docs"), join3(target, "docs"))) {
2576
- await copyIfExists(join3(packageRoot, "src", "server", "docs"), join3(target, "docs"));
3204
+ if (!await copyIfExists(join4(packageRoot, "docs"), join4(target, "docs"))) {
3205
+ await copyIfExists(join4(packageRoot, "src", "server", "docs"), join4(target, "docs"));
2577
3206
  }
2578
- await copyIfExists(join3(packageRoot, "skills"), join3(target, "skills"));
2579
- await copyIfExists(join3(packageRoot, "references"), join3(target, "references"));
2580
- await copyIfExists(join3(packageRoot, "src", "globals.css"), join3(target, "src", "globals.css"));
3207
+ await copyIfExists(join4(packageRoot, "skills"), join4(target, "skills"));
3208
+ await copyIfExists(join4(packageRoot, "references"), join4(target, "references"));
3209
+ await copyIfExists(join4(packageRoot, "src", "globals.css"), join4(target, "src", "globals.css"));
2581
3210
  }
2582
3211
  }
2583
3212
  async function ensurePython(workspaceRoot, specs) {
2584
3213
  if (specs.length === 0) return;
2585
- const venvPython = join3(workspaceRoot, ".boring-agent", "venv", "bin", "python");
3214
+ const venvPython = join4(workspaceRoot, ".boring-agent", "venv", "bin", "python");
2586
3215
  const uv = await commandExists("uv");
2587
3216
  if (!await exists(venvPython)) {
2588
3217
  if (uv) await run("uv", ["venv", ".boring-agent/venv"], workspaceRoot);
@@ -2615,17 +3244,17 @@ function assertEnvKey(key) {
2615
3244
  }
2616
3245
  async function isRuntimeMaterialized(workspaceRoot, contributions) {
2617
3246
  const hasPython = contributions.some(({ provisioning }) => (provisioning.python ?? []).length > 0);
2618
- if (hasPython && !await exists(join3(workspaceRoot, ".boring-agent", "venv", "bin", "python"))) return false;
3247
+ if (hasPython && !await exists(join4(workspaceRoot, ".boring-agent", "venv", "bin", "python"))) return false;
2619
3248
  for (const { provisioning } of contributions) {
2620
3249
  for (const spec of provisioning.nodePackages ?? []) {
2621
- if (!await exists(join3(nodePackageTarget(workspaceRoot, spec.packageName), "package.json"))) return false;
3250
+ if (!await exists(join4(nodePackageTarget(workspaceRoot, spec.packageName), "package.json"))) return false;
2622
3251
  }
2623
3252
  }
2624
3253
  return true;
2625
3254
  }
2626
3255
  async function writeShims(workspaceRoot, env) {
2627
- const shimDir = join3(workspaceRoot, ".boring-agent", "bin");
2628
- const venvBin = join3(workspaceRoot, ".boring-agent", "venv", "bin");
3256
+ const shimDir = join4(workspaceRoot, ".boring-agent", "bin");
3257
+ const venvBin = join4(workspaceRoot, ".boring-agent", "venv", "bin");
2629
3258
  await mkdir4(shimDir, { recursive: true });
2630
3259
  const exports = Object.entries(env).map(([key, value]) => {
2631
3260
  assertEnvKey(key);
@@ -2639,21 +3268,21 @@ export BORING_AGENT_WORKSPACE_ROOT="$WORKSPACE_ROOT"
2639
3268
  ${exports}
2640
3269
  VENV_BIN="$WORKSPACE_ROOT/.boring-agent/venv/bin"
2641
3270
  `;
2642
- await writeExecutable(join3(shimDir, "python"), `${base}exec "$VENV_BIN/python" "$@"
3271
+ await writeExecutable(join4(shimDir, "python"), `${base}exec "$VENV_BIN/python" "$@"
2643
3272
  `);
2644
- await writeExecutable(join3(shimDir, "python3"), `${base}exec "$VENV_BIN/python" "$@"
3273
+ await writeExecutable(join4(shimDir, "python3"), `${base}exec "$VENV_BIN/python" "$@"
2645
3274
  `);
2646
- await writeExecutable(join3(shimDir, "pip"), `${base}exec "$VENV_BIN/python" -m pip "$@"
3275
+ await writeExecutable(join4(shimDir, "pip"), `${base}exec "$VENV_BIN/python" -m pip "$@"
2647
3276
  `);
2648
- await writeExecutable(join3(shimDir, "pip3"), `${base}exec "$VENV_BIN/python" -m pip "$@"
3277
+ await writeExecutable(join4(shimDir, "pip3"), `${base}exec "$VENV_BIN/python" -m pip "$@"
2649
3278
  `);
2650
3279
  if (await exists(venvBin)) {
2651
- for (const entry of await readdir2(venvBin)) {
3280
+ for (const entry of await readdir3(venvBin)) {
2652
3281
  if (["python", "python3", "pip", "pip3"].includes(entry)) continue;
2653
- const full = join3(venvBin, entry);
3282
+ const full = join4(venvBin, entry);
2654
3283
  const info = await stat4(full).catch(() => null);
2655
3284
  if (!info?.isFile()) continue;
2656
- await writeExecutable(join3(shimDir, entry), `${base}TARGET="$VENV_BIN"/${bashSingleQuote(entry)}
3285
+ await writeExecutable(join4(shimDir, entry), `${base}TARGET="$VENV_BIN"/${bashSingleQuote(entry)}
2657
3286
  SHEBANG="$(head -n 1 "$TARGET" 2>/dev/null || true)"
2658
3287
  case "$SHEBANG" in
2659
3288
  *python*) exec "$VENV_BIN/python" "$TARGET" "$@" ;;
@@ -2676,8 +3305,8 @@ async function provisionRuntimeWorkspace({
2676
3305
  await mkdir4(workspaceRoot, { recursive: true });
2677
3306
  const env = collectEnv(active);
2678
3307
  const hash = await fingerprint(active);
2679
- const markerPath = join3(workspaceRoot, ".boring-agent", "provisioning.json");
2680
- const binDir = join3(workspaceRoot, ".boring-agent", "bin");
3308
+ const markerPath = join4(workspaceRoot, ".boring-agent", "provisioning.json");
3309
+ const binDir = join4(workspaceRoot, ".boring-agent", "bin");
2681
3310
  if (!force && await exists(markerPath)) {
2682
3311
  try {
2683
3312
  const marker = JSON.parse(await readFile4(markerPath, "utf8"));
@@ -2748,7 +3377,7 @@ function cloneStat(stat11) {
2748
3377
  function cloneEntries(entries) {
2749
3378
  return entries.map((entry) => ({ name: entry.name, kind: entry.kind }));
2750
3379
  }
2751
- function shellQuote2(value) {
3380
+ function shellQuote3(value) {
2752
3381
  return `'${value.replace(/'/g, `'\\''`)}'`;
2753
3382
  }
2754
3383
  async function runJson(sandbox, script) {
@@ -2828,7 +3457,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
2828
3457
  async function assertRealPathWithinSandboxRoot(sandboxPath) {
2829
3458
  const isWithinRoot = await runJson(
2830
3459
  remote,
2831
- `node -e ${shellQuote2(`const fs=require('fs'); const path=require('path'); const root=fs.realpathSync(process.argv[1]); const target=fs.realpathSync(process.argv[2]); const rel=path.relative(root,target); process.stdout.write(JSON.stringify(rel===''||(!rel.startsWith('..')&&!path.isAbsolute(rel))))`)} ${shellQuote2(VERCEL_SANDBOX_REMOTE_ROOT)} ${shellQuote2(sandboxPath)}`
3460
+ `node -e ${shellQuote3(`const fs=require('fs'); const path=require('path'); const root=fs.realpathSync(process.argv[1]); const target=fs.realpathSync(process.argv[2]); const rel=path.relative(root,target); process.stdout.write(JSON.stringify(rel===''||(!rel.startsWith('..')&&!path.isAbsolute(rel))))`)} ${shellQuote3(VERCEL_SANDBOX_REMOTE_ROOT)} ${shellQuote3(sandboxPath)}`
2832
3461
  );
2833
3462
  if (!isWithinRoot) {
2834
3463
  throw Object.assign(new Error("resolved path escapes workspace root"), { code: EPERM_CODE2 });
@@ -2837,7 +3466,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
2837
3466
  async function isSandboxSymlink(sandboxPath) {
2838
3467
  return await runJson(
2839
3468
  remote,
2840
- `node -e ${shellQuote2(`const fs=require('fs'); process.stdout.write(JSON.stringify(fs.lstatSync(process.argv[1]).isSymbolicLink()))`)} ${shellQuote2(sandboxPath)}`
3469
+ `node -e ${shellQuote3(`const fs=require('fs'); process.stdout.write(JSON.stringify(fs.lstatSync(process.argv[1]).isSymbolicLink()))`)} ${shellQuote3(sandboxPath)}`
2841
3470
  );
2842
3471
  }
2843
3472
  async function listDescendantPaths(relPath, sandboxPath) {
@@ -2857,7 +3486,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
2857
3486
  }
2858
3487
  return await runJson(
2859
3488
  remote,
2860
- `node -e ${shellQuote2(`const fs=require('fs'); const path=require('path'); const root=process.argv[1]; const relRoot=process.argv[2]; function walk(abs,rel){ const s=fs.statSync(abs); if(!s.isDirectory()) return []; const out=[]; for (const entry of fs.readdirSync(abs,{withFileTypes:true})) { const childRel=rel==='.'?entry.name:rel+'/'+entry.name; out.push(childRel); if(entry.isDirectory()) out.push(...walk(path.join(abs,entry.name),childRel)); } return out; } process.stdout.write(JSON.stringify(walk(root,relRoot)))`)} ${shellQuote2(sandboxPath)} ${shellQuote2(relPath)}`
3489
+ `node -e ${shellQuote3(`const fs=require('fs'); const path=require('path'); const root=process.argv[1]; const relRoot=process.argv[2]; function walk(abs,rel){ const s=fs.statSync(abs); if(!s.isDirectory()) return []; const out=[]; for (const entry of fs.readdirSync(abs,{withFileTypes:true})) { const childRel=rel==='.'?entry.name:rel+'/'+entry.name; out.push(childRel); if(entry.isDirectory()) out.push(...walk(path.join(abs,entry.name),childRel)); } return out; } process.stdout.write(JSON.stringify(walk(root,relRoot)))`)} ${shellQuote3(sandboxPath)} ${shellQuote3(relPath)}`
2861
3490
  );
2862
3491
  }
2863
3492
  return {
@@ -2950,7 +3579,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
2950
3579
  const version = metadataVersion;
2951
3580
  const result = await runJson(
2952
3581
  remote,
2953
- `node -e ${shellQuote2(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); const content=fs.readFileSync(p,'utf8'); process.stdout.write(JSON.stringify({content,stat:{size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}}))`)} ${shellQuote2(sandboxPath)}`
3582
+ `node -e ${shellQuote3(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); const content=fs.readFileSync(p,'utf8'); process.stdout.write(JSON.stringify({content,stat:{size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}}))`)} ${shellQuote3(sandboxPath)}`
2954
3583
  );
2955
3584
  if (metadataVersion === version) {
2956
3585
  statCache.set(sandboxPath, result.stat);
@@ -2978,7 +3607,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
2978
3607
  };
2979
3608
  })() : await runJson(
2980
3609
  remote,
2981
- `node -e ${shellQuote2(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote2(sandboxPath)}`
3610
+ `node -e ${shellQuote3(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote3(sandboxPath)}`
2982
3611
  );
2983
3612
  statCache.set(sandboxPath, writtenStat2);
2984
3613
  emitChange({ op: "write", path: relPath, mtimeMs: writtenStat2.mtimeMs });
@@ -2987,7 +3616,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
2987
3616
  const encoded = payload.toString("base64");
2988
3617
  const writtenStat = await runJson(
2989
3618
  remote,
2990
- `node -e ${shellQuote2(`const fs=require('fs'); const p=process.argv[1]; const data=Buffer.from(process.argv[2],'base64'); fs.writeFileSync(p,data); const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote2(sandboxPath)} ${shellQuote2(encoded)}`
3619
+ `node -e ${shellQuote3(`const fs=require('fs'); const p=process.argv[1]; const data=Buffer.from(process.argv[2],'base64'); fs.writeFileSync(p,data); const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote3(sandboxPath)} ${shellQuote3(encoded)}`
2991
3620
  );
2992
3621
  invalidateMetadataCache();
2993
3622
  statCache.set(sandboxPath, writtenStat);
@@ -3016,7 +3645,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3016
3645
  };
3017
3646
  })() : await runJson(
3018
3647
  remote,
3019
- `node -e ${shellQuote2(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote2(sandboxPath)}`
3648
+ `node -e ${shellQuote3(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote3(sandboxPath)}`
3020
3649
  );
3021
3650
  statCache.set(sandboxPath, writtenStat2);
3022
3651
  emitChange({ op: "write", path: relPath, mtimeMs: writtenStat2.mtimeMs });
@@ -3025,7 +3654,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3025
3654
  const encoded = payload.toString("base64");
3026
3655
  const writtenStat = await runJson(
3027
3656
  remote,
3028
- `node -e ${shellQuote2(`const fs=require('fs'); const p=process.argv[1]; const data=Buffer.from(process.argv[2],'base64'); fs.writeFileSync(p,data); const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote2(sandboxPath)} ${shellQuote2(encoded)}`
3657
+ `node -e ${shellQuote3(`const fs=require('fs'); const p=process.argv[1]; const data=Buffer.from(process.argv[2],'base64'); fs.writeFileSync(p,data); const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote3(sandboxPath)} ${shellQuote3(encoded)}`
3029
3658
  );
3030
3659
  invalidateMetadataCache();
3031
3660
  statCache.set(sandboxPath, writtenStat);
@@ -3041,7 +3670,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3041
3670
  await assertRealPathWithinSandboxRoot(sandboxPath);
3042
3671
  const descendantPaths = await isSandboxSymlink(sandboxPath) ? [] : await listDescendantPaths(relPath, sandboxPath);
3043
3672
  if (remote.fs?.rm) await remote.fs.rm(sandboxPath, { recursive: true, force: false });
3044
- else await runShell(remote, `rm -r -- ${shellQuote2(sandboxPath)}`);
3673
+ else await runShell(remote, `rm -r -- ${shellQuote3(sandboxPath)}`);
3045
3674
  invalidateMetadataCache();
3046
3675
  workspaceOpts.onMutation?.();
3047
3676
  emitChange({ op: "unlink", path: relPath });
@@ -3059,7 +3688,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3059
3688
  kind: entry.isDirectory() ? "dir" : "file"
3060
3689
  })) : await runJson(
3061
3690
  remote,
3062
- `node -e ${shellQuote2(`const fs=require('fs'); const p=process.argv[1]; const entries=fs.readdirSync(p,{withFileTypes:true}).map((e)=>({name:e.name,kind:e.isDirectory()?'dir':'file'})); process.stdout.write(JSON.stringify(entries))`)} ${shellQuote2(sandboxPath)}`
3691
+ `node -e ${shellQuote3(`const fs=require('fs'); const p=process.argv[1]; const entries=fs.readdirSync(p,{withFileTypes:true}).map((e)=>({name:e.name,kind:e.isDirectory()?'dir':'file'})); process.stdout.write(JSON.stringify(entries))`)} ${shellQuote3(sandboxPath)}`
3063
3692
  );
3064
3693
  if (metadataVersion === version) {
3065
3694
  readdirCache.set(sandboxPath, mappedEntries);
@@ -3082,7 +3711,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3082
3711
  } else {
3083
3712
  mappedStat = await runJson(
3084
3713
  remote,
3085
- `node -e ${shellQuote2(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote2(sandboxPath)}`
3714
+ `node -e ${shellQuote3(`const fs=require('fs'); const p=process.argv[1]; const s=fs.statSync(p); process.stdout.write(JSON.stringify({size:s.size,mtimeMs:s.mtimeMs,kind:s.isDirectory()?'dir':'file'}))`)} ${shellQuote3(sandboxPath)}`
3086
3715
  );
3087
3716
  }
3088
3717
  if (metadataVersion === version) {
@@ -3093,9 +3722,9 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3093
3722
  async mkdir(relPath, opts) {
3094
3723
  const sandboxPath = toSandboxPath(relPath);
3095
3724
  if (remote.fs?.mkdir) await remote.fs.mkdir(sandboxPath, { recursive: opts?.recursive ?? false });
3096
- else if (opts?.recursive) await runShell(remote, `mkdir -p -- ${shellQuote2(sandboxPath)}`);
3725
+ else if (opts?.recursive) await runShell(remote, `mkdir -p -- ${shellQuote3(sandboxPath)}`);
3097
3726
  else if (remote.mkDir) await remote.mkDir(sandboxPath);
3098
- else await runShell(remote, `mkdir -- ${shellQuote2(sandboxPath)}`);
3727
+ else await runShell(remote, `mkdir -- ${shellQuote3(sandboxPath)}`);
3099
3728
  invalidateMetadataCache();
3100
3729
  workspaceOpts.onMutation?.();
3101
3730
  emitChange({ op: "mkdir", path: relPath });
@@ -3104,7 +3733,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3104
3733
  const fromSandboxPath = toSandboxPath(fromRelPath);
3105
3734
  const toSandboxAbsolutePath = toSandboxPath(toRelPath2);
3106
3735
  if (remote.fs?.rename) await remote.fs.rename(fromSandboxPath, toSandboxAbsolutePath);
3107
- else await runShell(remote, `mv -- ${shellQuote2(fromSandboxPath)} ${shellQuote2(toSandboxAbsolutePath)}`);
3736
+ else await runShell(remote, `mv -- ${shellQuote3(fromSandboxPath)} ${shellQuote3(toSandboxAbsolutePath)}`);
3108
3737
  invalidateMetadataCache();
3109
3738
  workspaceOpts.onMutation?.();
3110
3739
  emitChange({ op: "rename", path: toRelPath2, oldPath: fromRelPath });
@@ -3115,7 +3744,7 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
3115
3744
  // src/server/workspace/provisioning/packArtifact.ts
3116
3745
  import { execFile as execFile2 } from "child_process";
3117
3746
  import { mkdir as mkdir5, mkdtemp, rename as rename4 } from "fs/promises";
3118
- import { dirname as dirname7, isAbsolute as isAbsolute5, join as join4 } from "path";
3747
+ import { dirname as dirname7, isAbsolute as isAbsolute5, join as join5 } from "path";
3119
3748
  import { tmpdir } from "os";
3120
3749
  import { fileURLToPath as fileURLToPath2 } from "url";
3121
3750
  import { promisify as promisify2 } from "util";
@@ -3160,7 +3789,7 @@ async function packProvisioningArtifact(request) {
3160
3789
  ], { maxBuffer: 1024 * 1024 * 20 });
3161
3790
  const packedName = stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
3162
3791
  if (!packedName) throw new Error(`pnpm pack produced no artifact for ${sourcePath}`);
3163
- const packedPath = isAbsolute5(packedName) ? packedName : join4(dirname7(request.outputPath), packedName);
3792
+ const packedPath = isAbsolute5(packedName) ? packedName : join5(dirname7(request.outputPath), packedName);
3164
3793
  await rename4(packedPath, request.outputPath);
3165
3794
  return;
3166
3795
  }
@@ -3182,8 +3811,8 @@ async function resolveArtifactInstallSource(args) {
3182
3811
  const name = provisioningArtifactName(kind, id, fingerprint2);
3183
3812
  const workspaceRel = `.boring-agent/tmp/${name}`;
3184
3813
  if (!await args.workspaceFs.exists(workspaceRel)) {
3185
- const artifactDir = await mkdtemp(join4(tmpdir(), "boring-agent-artifact-"));
3186
- const outputPath = join4(artifactDir, name);
3814
+ const artifactDir = await mkdtemp(join5(tmpdir(), "boring-agent-artifact-"));
3815
+ const outputPath = join5(artifactDir, name);
3187
3816
  try {
3188
3817
  await args.prepareArtifact({ kind, id, fingerprint: fingerprint2, source: args.source, outputPath });
3189
3818
  await args.workspaceFs.copyFromHost(outputPath, workspaceRel);
@@ -3290,7 +3919,7 @@ function createPythonRuntimeFingerprint(input) {
3290
3919
  }
3291
3920
 
3292
3921
  // src/server/workspace/provisioning/node.ts
3293
- import { join as join5, relative as relative6, sep as sep3 } from "path";
3922
+ import { join as join6, relative as relative6, sep as sep3 } from "path";
3294
3923
  var NODE_RUNTIME_REL = ".boring-agent/node";
3295
3924
  var NODE_PACKAGE_JSON_REL = `${NODE_RUNTIME_REL}/package.json`;
3296
3925
  var NODE_FINGERPRINT_REL = `${NODE_RUNTIME_REL}/.fingerprint`;
@@ -3328,9 +3957,9 @@ function nodeInstallSource(spec) {
3328
3957
  }
3329
3958
  function expectedNodeOutputs(paths, packages) {
3330
3959
  return [
3331
- join5(paths.node, "package-lock.json"),
3960
+ join6(paths.node, "package-lock.json"),
3332
3961
  ...packages.flatMap(
3333
- (pkg) => (pkg.expectedBins ?? []).map((bin) => join5(paths.nodeBin, bin))
3962
+ (pkg) => (pkg.expectedBins ?? []).map((bin) => join6(paths.nodeBin, bin))
3334
3963
  )
3335
3964
  ];
3336
3965
  }
@@ -3421,7 +4050,7 @@ async function ensureNodeRuntime(options) {
3421
4050
  }
3422
4051
 
3423
4052
  // src/server/workspace/provisioning/python.ts
3424
- import { dirname as dirname9, join as join6, relative as relative7, sep as sep4 } from "path";
4053
+ import { dirname as dirname9, join as join7, relative as relative7, sep as sep4 } from "path";
3425
4054
  import { fileURLToPath as fileURLToPath3 } from "url";
3426
4055
  var VENV_REL = ".boring-agent/venv";
3427
4056
  var VENV_FINGERPRINT_REL = `${VENV_REL}/.fingerprint`;
@@ -3440,7 +4069,7 @@ async function ensureUv(options) {
3440
4069
  const uvVersion = await commandOutput2(options.adapter, explicitUvBin, ["--version"]) || "uv unknown";
3441
4070
  try {
3442
4071
  await options.adapter.exec("mkdir", ["-p", options.runtimeLayout.uvBin], { cwd: options.runtimeLayout.workspaceRoot });
3443
- await options.adapter.exec("ln", ["-sf", explicitUvBin, join6(options.runtimeLayout.uvBin, "uv")], { cwd: options.runtimeLayout.workspaceRoot });
4072
+ await options.adapter.exec("ln", ["-sf", explicitUvBin, join7(options.runtimeLayout.uvBin, "uv")], { cwd: options.runtimeLayout.workspaceRoot });
3444
4073
  } catch {
3445
4074
  }
3446
4075
  return { uvBin: explicitUvBin, uvVersion, installedWorkspaceUv: false };
@@ -3467,7 +4096,7 @@ async function ensureUv(options) {
3467
4096
  await options.adapter.workspaceFs.mkdir(".boring-agent/sdk/uv/bin");
3468
4097
  await options.adapter.workspaceFs.rm(UV_BIN_REL);
3469
4098
  await options.adapter.workspaceFs.copyFromHost(options.uvStandaloneSource, UV_BIN_REL);
3470
- const uvBin = join6(options.runtimeLayout.uvBin, "uv");
4099
+ const uvBin = join7(options.runtimeLayout.uvBin, "uv");
3471
4100
  await options.adapter.exec("chmod", ["+x", uvBin], { cwd: options.runtimeLayout.workspaceRoot });
3472
4101
  return {
3473
4102
  uvBin,
@@ -3500,7 +4129,7 @@ function expectedPythonOutputs(paths, packages) {
3500
4129
  return [
3501
4130
  paths.venvPython,
3502
4131
  ...packages.flatMap(
3503
- (pkg) => (pkg.expectedBins ?? []).map((bin) => join6(paths.venvBin, bin))
4132
+ (pkg) => (pkg.expectedBins ?? []).map((bin) => join7(paths.venvBin, bin))
3504
4133
  )
3505
4134
  ];
3506
4135
  }
@@ -3613,7 +4242,7 @@ async function ensurePythonRuntime(options) {
3613
4242
 
3614
4243
  // src/server/workspace/provisioning/skills.ts
3615
4244
  import { stat as stat5 } from "fs/promises";
3616
- import { join as join7 } from "path";
4245
+ import { join as join8 } from "path";
3617
4246
  import { fileURLToPath as fileURLToPath4 } from "url";
3618
4247
  var GENERATED_SKILLS_REL = ".boring-agent/skills";
3619
4248
  var USER_SKILLS_REL = ".agents/skills";
@@ -3626,7 +4255,7 @@ function assertSafeSegment(kind, value) {
3626
4255
  }
3627
4256
  }
3628
4257
  function getProvisionedSkillPaths(paths) {
3629
- return [paths.skills, join7(paths.workspaceRoot, USER_SKILLS_REL)];
4258
+ return [paths.skills, join8(paths.workspaceRoot, USER_SKILLS_REL)];
3630
4259
  }
3631
4260
  async function mirrorPluginSkills(options) {
3632
4261
  await options.adapter.workspaceFs.rm(GENERATED_SKILLS_REL);
@@ -3659,7 +4288,7 @@ async function mirrorPluginSkills(options) {
3659
4288
  // src/server/workspace/provisioning/workspaceFiles.ts
3660
4289
  import { basename, join as joinPath } from "path";
3661
4290
  import { posix as posix2 } from "path";
3662
- import { readdir as readdir3, stat as stat6 } from "fs/promises";
4291
+ import { readdir as readdir4, stat as stat6 } from "fs/promises";
3663
4292
  import { fileURLToPath as fileURLToPath5, pathToFileURL } from "url";
3664
4293
  function sourceToPath3(source) {
3665
4294
  return source instanceof URL ? fileURLToPath5(source) : source;
@@ -3701,7 +4330,7 @@ async function collectTemplateWorkItems(template) {
3701
4330
  kind: "directory"
3702
4331
  });
3703
4332
  }
3704
- const entries = await readdir3(dir, { withFileTypes: true });
4333
+ const entries = await readdir4(dir, { withFileTypes: true });
3705
4334
  for (const entry of entries) {
3706
4335
  const childPath = joinPath(dir, entry.name);
3707
4336
  const childRel = rel ? `${rel}/${entry.name}` : entry.name;
@@ -3958,61 +4587,6 @@ import { spawnSync } from "child_process";
3958
4587
  // src/server/runtime/modes/direct.ts
3959
4588
  import { mkdir as mkdir8 } from "fs/promises";
3960
4589
 
3961
- // src/server/runtime/createServerFileSearch.ts
3962
- var DEFAULT_LIMIT = 500;
3963
- var MAX_LIMIT = 5e3;
3964
- var SEARCH_TIMEOUT_MS = 5e3;
3965
- var SEARCH_MAX_OUTPUT_BYTES = 256e3;
3966
- function shellQuote3(value) {
3967
- return `'${value.replaceAll("'", "'\\''")}'`;
3968
- }
3969
- function normalizeLimit(limit) {
3970
- if (typeof limit !== "number" || !Number.isFinite(limit)) {
3971
- return DEFAULT_LIMIT;
3972
- }
3973
- const normalized = Math.trunc(limit);
3974
- if (normalized <= 0) return DEFAULT_LIMIT;
3975
- return Math.min(normalized, MAX_LIMIT);
3976
- }
3977
- function buildFindArgs(glob) {
3978
- const isPathShaped = glob.includes("/") || glob.includes("**");
3979
- if (!isPathShaped) {
3980
- return `-iname ${shellQuote3(glob)}`;
3981
- }
3982
- let translated = glob.replaceAll("**", "*");
3983
- translated = translated.replace(/^\/+/, "");
3984
- if (!translated.startsWith("*")) {
3985
- translated = `*${translated}`;
3986
- }
3987
- return `-ipath ${shellQuote3(translated)}`;
3988
- }
3989
- function createServerFileSearch(workspace, sandbox) {
3990
- return {
3991
- async search(glob, limit = DEFAULT_LIMIT) {
3992
- const safeLimit = normalizeLimit(limit);
3993
- const command = [
3994
- "find .",
3995
- "-maxdepth 10",
3996
- "\\( -path './.boring-agent' -o -path './.git' -o -path './node_modules' \\) -prune -o",
3997
- buildFindArgs(glob),
3998
- "-type f",
3999
- "-print",
4000
- `| head -n ${safeLimit}`
4001
- ].join(" ");
4002
- const { stdout, exitCode } = await sandbox.exec(command, {
4003
- cwd: workspace.root,
4004
- timeoutMs: SEARCH_TIMEOUT_MS,
4005
- maxOutputBytes: SEARCH_MAX_OUTPUT_BYTES
4006
- });
4007
- if (exitCode !== 0) {
4008
- throw new Error(`file-search failed: exit ${exitCode}`);
4009
- }
4010
- const decoded = new TextDecoder().decode(stdout);
4011
- return decoded.split("\n").map((line) => line.replace(/\r$/, "")).filter((line) => line.length > 0).map((line) => line.replace(/^\.\//, ""));
4012
- }
4013
- };
4014
- }
4015
-
4016
4590
  // src/server/workspace/provision.ts
4017
4591
  import { cp as cp2 } from "fs/promises";
4018
4592
  async function copyTemplate(templatePath, workspaceRoot) {
@@ -4300,8 +4874,8 @@ var localModeAdapter = {
4300
4874
  };
4301
4875
 
4302
4876
  // src/server/runtime/modes/vercel-sandbox.ts
4303
- import { readFile as readFile8, readdir as readdir5, stat as stat8 } from "fs/promises";
4304
- import { join as join8 } from "path";
4877
+ import { readFile as readFile8, readdir as readdir6, stat as stat8 } from "fs/promises";
4878
+ import { join as join9 } from "path";
4305
4879
  import { fileURLToPath as fileURLToPath7 } from "url";
4306
4880
  import { Sandbox as VercelSandbox } from "@vercel/sandbox";
4307
4881
 
@@ -4447,20 +5021,20 @@ function createVercelSandboxExec(sandbox, execOpts = {}) {
4447
5021
 
4448
5022
  // src/server/sandbox/vercel-sandbox/packageTemplate.ts
4449
5023
  import { createHash as createHash5 } from "crypto";
4450
- import { readFile as readFile7, readdir as readdir4 } from "fs/promises";
5024
+ import { readFile as readFile7, readdir as readdir5 } from "fs/promises";
4451
5025
  import path3 from "path";
4452
5026
  import { Readable } from "stream";
4453
5027
  import { createGzip } from "zlib";
4454
5028
  import { put } from "@vercel/blob";
4455
5029
  var urlCache = /* @__PURE__ */ new Map();
4456
5030
  var LOG_PREFIX = "[template-tarball]";
4457
- function log2(msg, meta = {}) {
5031
+ function log3(msg, meta = {}) {
4458
5032
  const metaStr = Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : "";
4459
5033
  process.stderr.write(`${LOG_PREFIX} ${msg}${metaStr}
4460
5034
  `);
4461
5035
  }
4462
5036
  async function collectFiles(dir, base = "") {
4463
- const entries = await readdir4(dir, { withFileTypes: true });
5037
+ const entries = await readdir5(dir, { withFileTypes: true });
4464
5038
  const files = [];
4465
5039
  for (const entry of entries) {
4466
5040
  const fullPath = path3.join(dir, entry.name);
@@ -4537,11 +5111,11 @@ async function packageTemplate(templatePath, opts = {}) {
4537
5111
  const hash = computeTemplateHash(files);
4538
5112
  const cached = urlCache.get(hash);
4539
5113
  if (cached) {
4540
- log2("cache hit", { hash, fileCount: files.length });
5114
+ log3("cache hit", { hash, fileCount: files.length });
4541
5115
  return { url: cached, hash };
4542
5116
  }
4543
5117
  const tarball = await buildTarGz(files);
4544
- log2("tarball built", {
5118
+ log3("tarball built", {
4545
5119
  hash,
4546
5120
  fileCount: files.length,
4547
5121
  sizeBytes: tarball.length,
@@ -4550,7 +5124,7 @@ async function packageTemplate(templatePath, opts = {}) {
4550
5124
  const upload = opts.uploadFn ?? defaultUpload;
4551
5125
  const url = await upload(hash, tarball);
4552
5126
  urlCache.set(hash, url);
4553
- log2("uploaded", { hash, url, totalMs: Date.now() - startMs });
5127
+ log3("uploaded", { hash, url, totalMs: Date.now() - startMs });
4554
5128
  return { url, hash };
4555
5129
  }
4556
5130
 
@@ -4716,10 +5290,10 @@ async function copyHostPathToVercelWorkspace(options) {
4716
5290
  const sourceStat = await stat8(sourcePath);
4717
5291
  if (sourceStat.isDirectory()) {
4718
5292
  await options.workspace.mkdir(options.targetRel, { recursive: true });
4719
- for (const entry of await readdir5(sourcePath, { withFileTypes: true })) {
5293
+ for (const entry of await readdir6(sourcePath, { withFileTypes: true })) {
4720
5294
  await copyHostPathToVercelWorkspace({
4721
5295
  workspace: options.workspace,
4722
- source: join8(sourcePath, entry.name),
5296
+ source: join9(sourcePath, entry.name),
4723
5297
  targetRel: `${options.targetRel}/${entry.name}`
4724
5298
  });
4725
5299
  }
@@ -5147,7 +5721,7 @@ function getRuntimeBundleStorageRoot(bundle) {
5147
5721
 
5148
5722
  // src/server/harness/pi-coding-agent/createHarness.ts
5149
5723
  import { existsSync, readFileSync as readFileSync2 } from "fs";
5150
- import { extname as extname3, join as join10 } from "path";
5724
+ import { extname as extname3, join as join11 } from "path";
5151
5725
  import {
5152
5726
  createAgentSession,
5153
5727
  SessionManager,
@@ -5427,7 +6001,7 @@ function createPiAgentSessionAdapter(session, options = {}) {
5427
6001
  // src/server/harness/pi-coding-agent/sessions.ts
5428
6002
  import { randomUUID } from "crypto";
5429
6003
  import {
5430
- readdir as readdir6,
6004
+ readdir as readdir7,
5431
6005
  readFile as readFile9,
5432
6006
  stat as fsStat,
5433
6007
  rm as rm4,
@@ -5438,7 +6012,7 @@ import {
5438
6012
  open as open2
5439
6013
  } from "fs/promises";
5440
6014
  import { closeSync, openSync, readFileSync, readSync, readdirSync, writeFileSync as writeFileSync2 } from "fs";
5441
- import { join as join9, basename as basename2, resolve as resolve7 } from "path";
6015
+ import { join as join10, basename as basename2, resolve as resolve7 } from "path";
5442
6016
  import { homedir as homedir3 } from "os";
5443
6017
  import {
5444
6018
  parseSessionEntries,
@@ -5446,7 +6020,7 @@ import {
5446
6020
  } from "@mariozechner/pi-coding-agent";
5447
6021
  function defaultSessionDir(cwd) {
5448
6022
  const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
5449
- return join9(homedir3(), ".pi", "agent", "sessions", safePath);
6023
+ return join10(homedir3(), ".pi", "agent", "sessions", safePath);
5450
6024
  }
5451
6025
  var SAFE_ID = /^[a-zA-Z0-9_-]+$/;
5452
6026
  var SAFE_SESSION_NAMESPACE = /^[a-zA-Z0-9_-]+$/;
@@ -5456,7 +6030,7 @@ function sessionDirForNamespace(namespace) {
5456
6030
  if (!SAFE_SESSION_NAMESPACE.test(safeNamespace)) {
5457
6031
  throw new Error("session namespace must contain only letters, numbers, underscores, and dashes");
5458
6032
  }
5459
- return join9(homedir3(), ".pi", "agent", "sessions", safeNamespace);
6033
+ return join10(homedir3(), ".pi", "agent", "sessions", safeNamespace);
5460
6034
  }
5461
6035
  function normalizeListOptions(options) {
5462
6036
  return {
@@ -5501,9 +6075,9 @@ var PiSessionStore = class {
5501
6075
  }
5502
6076
  }
5503
6077
  async listUncached(_ctx, options) {
5504
- const files = await readdir6(this.sessionDir).catch(() => []);
6078
+ const files = await readdir7(this.sessionDir).catch(() => []);
5505
6079
  const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
5506
- const filepaths = jsonlFiles.map((f) => join9(this.sessionDir, f));
6080
+ const filepaths = jsonlFiles.map((f) => join10(this.sessionDir, f));
5507
6081
  const fileStats = await Promise.all(filepaths.map(async (filepath) => {
5508
6082
  try {
5509
6083
  return { filepath, stat: await fsStat(filepath) };
@@ -5547,7 +6121,7 @@ var PiSessionStore = class {
5547
6121
  };
5548
6122
  lines.push(JSON.stringify(infoEntry));
5549
6123
  }
5550
- const filepath = join9(this.sessionDir, `${id}.jsonl`);
6124
+ const filepath = join10(this.sessionDir, `${id}.jsonl`);
5551
6125
  await writeFile6(filepath, lines.join("\n") + "\n", "utf-8");
5552
6126
  return {
5553
6127
  id,
@@ -5629,7 +6203,7 @@ var PiSessionStore = class {
5629
6203
  loadPiSessionFileSync(sessionId) {
5630
6204
  if (!SAFE_ID.test(sessionId)) return null;
5631
6205
  try {
5632
- const direct = join9(this.sessionDir, `${sessionId}.jsonl`);
6206
+ const direct = join10(this.sessionDir, `${sessionId}.jsonl`);
5633
6207
  let filepath = direct;
5634
6208
  let content;
5635
6209
  try {
@@ -5639,7 +6213,7 @@ var PiSessionStore = class {
5639
6213
  (f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
5640
6214
  );
5641
6215
  if (files.length === 0) return null;
5642
- filepath = join9(this.sessionDir, files[0]);
6216
+ filepath = join10(this.sessionDir, files[0]);
5643
6217
  content = readFileSync(filepath, "utf-8");
5644
6218
  }
5645
6219
  const entries = safeParseEntries(content);
@@ -5695,18 +6269,18 @@ var PiSessionStore = class {
5695
6269
  if (!SAFE_ID.test(sessionId)) {
5696
6270
  throw new Error(`Session not found: ${sessionId}`);
5697
6271
  }
5698
- const direct = join9(this.sessionDir, `${sessionId}.jsonl`);
6272
+ const direct = join10(this.sessionDir, `${sessionId}.jsonl`);
5699
6273
  try {
5700
6274
  await fsStat(direct);
5701
6275
  return direct;
5702
6276
  } catch {
5703
6277
  }
5704
- const files = await readdir6(this.sessionDir).catch(() => []);
6278
+ const files = await readdir7(this.sessionDir).catch(() => []);
5705
6279
  const match = files.find(
5706
6280
  (f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
5707
6281
  );
5708
6282
  if (!match) throw new Error(`Session not found: ${sessionId}`);
5709
- const matchedPath = join9(this.sessionDir, match);
6283
+ const matchedPath = join10(this.sessionDir, match);
5710
6284
  if (!isTimestampNamedPiSessionFile(matchedPath, sessionId)) return matchedPath;
5711
6285
  const existingWrapper = await this.findWrapperReferencingNativeSession(matchedPath);
5712
6286
  if (existingWrapper) {
@@ -5871,7 +6445,7 @@ var PiSessionStore = class {
5871
6445
  try {
5872
6446
  const files = readdirSync(this.sessionDir).filter((file) => file.endsWith(".jsonl"));
5873
6447
  for (const file of files) {
5874
- const filepath = join9(this.sessionDir, file);
6448
+ const filepath = join10(this.sessionDir, file);
5875
6449
  if (resolve7(filepath) === resolvedNativePath) continue;
5876
6450
  try {
5877
6451
  const linkedPiFile = extractPiSessionFilePath(parseJsonlPrefixEntries(readJsonlPrefixSync(filepath)));
@@ -5886,10 +6460,10 @@ var PiSessionStore = class {
5886
6460
  }
5887
6461
  async findWrapperReferencingNativeSession(nativePath) {
5888
6462
  const resolvedNativePath = resolve7(nativePath);
5889
- const files = await readdir6(this.sessionDir).catch(() => []);
6463
+ const files = await readdir7(this.sessionDir).catch(() => []);
5890
6464
  for (const file of files) {
5891
6465
  if (!file.endsWith(".jsonl")) continue;
5892
- const filepath = join9(this.sessionDir, file);
6466
+ const filepath = join10(this.sessionDir, file);
5893
6467
  if (resolve7(filepath) === resolvedNativePath) continue;
5894
6468
  try {
5895
6469
  const linkedPiFile = extractPiSessionFilePath(parseJsonlPrefixEntries(await readJsonlPrefix(filepath)));
@@ -5900,7 +6474,7 @@ var PiSessionStore = class {
5900
6474
  return null;
5901
6475
  }
5902
6476
  ensureWrapperForNativeSessionSync(sessionId, nativePath, entries) {
5903
- const wrapperPath = join9(this.sessionDir, `${sessionId}.jsonl`);
6477
+ const wrapperPath = join10(this.sessionDir, `${sessionId}.jsonl`);
5904
6478
  if (resolve7(wrapperPath) === resolve7(nativePath)) return wrapperPath;
5905
6479
  try {
5906
6480
  readFileSync(wrapperPath, "utf-8");
@@ -5920,7 +6494,7 @@ var PiSessionStore = class {
5920
6494
  return wrapperPath;
5921
6495
  }
5922
6496
  async ensureWrapperForNativeSession(sessionId, nativePath) {
5923
- const wrapperPath = join9(this.sessionDir, `${sessionId}.jsonl`);
6497
+ const wrapperPath = join10(this.sessionDir, `${sessionId}.jsonl`);
5924
6498
  if (resolve7(wrapperPath) === resolve7(nativePath)) return wrapperPath;
5925
6499
  try {
5926
6500
  await fsStat(wrapperPath);
@@ -6087,6 +6661,11 @@ var INFOMANIAK_PROVIDER = "infomaniak";
6087
6661
  var INFOMANIAK_API_BASE = "https://api.infomaniak.com";
6088
6662
  var DEFAULT_CUSTOM_MODEL_MAX_TOKENS = 16384;
6089
6663
  var DEFAULT_CUSTOM_MODEL_CONTEXT_WINDOW = 2e5;
6664
+ var DEFAULT_INFOMANIAK_MODELS = [
6665
+ "moonshotai/Kimi-K2.6",
6666
+ "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8",
6667
+ "Qwen/Qwen3.5-122B-A10B-FP8"
6668
+ ];
6090
6669
  function clean(value) {
6091
6670
  const trimmed = value?.trim();
6092
6671
  return trimmed && trimmed.length > 0 ? trimmed : void 0;
@@ -6110,6 +6689,19 @@ function readModelInput(name) {
6110
6689
  const parsed = raw.split(",").map((part) => part.trim()).filter((part) => part === "text" || part === "image");
6111
6690
  return parsed.length > 0 ? parsed : ["text"];
6112
6691
  }
6692
+ function readMaxTokensField(name, fallback = "max_completion_tokens") {
6693
+ const raw = clean(getEnv(name));
6694
+ return raw === "max_tokens" || raw === "max_completion_tokens" ? raw : fallback;
6695
+ }
6696
+ function buildOpenAICompletionsCompat(envPrefix, defaults = {}) {
6697
+ return {
6698
+ supportsStore: readBoolean(`${envPrefix}_SUPPORTS_STORE`, defaults.supportsStore ?? false),
6699
+ supportsDeveloperRole: readBoolean(`${envPrefix}_SUPPORTS_DEVELOPER_ROLE`, defaults.supportsDeveloperRole ?? true),
6700
+ supportsReasoningEffort: readBoolean(`${envPrefix}_SUPPORTS_REASONING_EFFORT`, defaults.supportsReasoningEffort ?? true),
6701
+ supportsUsageInStreaming: readBoolean(`${envPrefix}_SUPPORTS_USAGE_IN_STREAMING`, defaults.supportsUsageInStreaming ?? true),
6702
+ maxTokensField: readMaxTokensField(`${envPrefix}_MAX_TOKENS_FIELD`, defaults.maxTokensField)
6703
+ };
6704
+ }
6113
6705
  function readApiKeyEnv(candidates) {
6114
6706
  for (const candidate of candidates) {
6115
6707
  const envName = clean(candidate);
@@ -6122,31 +6714,23 @@ function buildOpenAICompatibleProviderConfig(opts) {
6122
6714
  baseUrl: opts.baseUrl,
6123
6715
  apiKey: opts.apiKeyEnv,
6124
6716
  api: "openai-completions",
6125
- models: [
6126
- {
6127
- id: opts.modelId,
6128
- name: opts.modelName ?? opts.modelId,
6129
- api: "openai-completions",
6130
- reasoning: readBoolean(`${opts.envPrefix}_REASONING`, true),
6131
- input: readModelInput(`${opts.envPrefix}_INPUT`),
6132
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
6133
- contextWindow: readPositiveInt(
6134
- `${opts.envPrefix}_CONTEXT_WINDOW`,
6135
- DEFAULT_CUSTOM_MODEL_CONTEXT_WINDOW
6136
- ),
6137
- maxTokens: readPositiveInt(
6138
- `${opts.envPrefix}_MAX_TOKENS`,
6139
- DEFAULT_CUSTOM_MODEL_MAX_TOKENS
6140
- ),
6141
- compat: {
6142
- supportsStore: false,
6143
- supportsDeveloperRole: true,
6144
- supportsReasoningEffort: true,
6145
- supportsUsageInStreaming: true,
6146
- maxTokensField: "max_completion_tokens"
6147
- }
6148
- }
6149
- ]
6717
+ models: opts.models.map((model) => ({
6718
+ id: model.id,
6719
+ name: model.name ?? model.id,
6720
+ api: "openai-completions",
6721
+ reasoning: readBoolean(`${opts.envPrefix}_REASONING`, true),
6722
+ input: readModelInput(`${opts.envPrefix}_INPUT`),
6723
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
6724
+ contextWindow: readPositiveInt(
6725
+ `${opts.envPrefix}_CONTEXT_WINDOW`,
6726
+ DEFAULT_CUSTOM_MODEL_CONTEXT_WINDOW
6727
+ ),
6728
+ maxTokens: readPositiveInt(
6729
+ `${opts.envPrefix}_MAX_TOKENS`,
6730
+ DEFAULT_CUSTOM_MODEL_MAX_TOKENS
6731
+ ),
6732
+ compat: buildOpenAICompletionsCompat(opts.envPrefix, opts.compatDefaults)
6733
+ }))
6150
6734
  };
6151
6735
  }
6152
6736
  function readInfomaniakBaseUrl() {
@@ -6156,25 +6740,46 @@ function readInfomaniakBaseUrl() {
6156
6740
  if (!productId) return void 0;
6157
6741
  return `${INFOMANIAK_API_BASE}/2/ai/${productId}/openai/v1`;
6158
6742
  }
6743
+ function readInfomaniakModelIds() {
6744
+ const configured = clean(getEnv("BORING_AGENT_INFOMANIAK_MODELS"))?.split(",").map((part) => part.trim()).filter(Boolean);
6745
+ const ids = configured?.length ? configured : [...DEFAULT_INFOMANIAK_MODELS];
6746
+ const legacyModel = clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL"));
6747
+ if (legacyModel && !ids.includes(legacyModel)) ids.push(legacyModel);
6748
+ return Array.from(new Set(ids));
6749
+ }
6159
6750
  function readInfomaniakProvider() {
6160
- const modelId = clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL")) ?? clean(getEnv("BORING_AGENT_DEFAULT_MODEL_ID"));
6751
+ const modelIds = readInfomaniakModelIds();
6752
+ const defaultModelId = clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL")) ?? clean(getEnv("BORING_AGENT_DEFAULT_MODEL_ID")) ?? modelIds[0];
6161
6753
  const baseUrl = readInfomaniakBaseUrl();
6162
6754
  const apiKeyEnv = readApiKeyEnv([
6163
6755
  clean(getEnv("BORING_AGENT_INFOMANIAK_API_KEY_ENV")) ?? "",
6164
6756
  "INFOMANIAK_API_TOKEN",
6165
6757
  "BORING_AGENT_INFOMANIAK_API_KEY"
6166
6758
  ]);
6167
- if (!modelId || !baseUrl || !apiKeyEnv) return void 0;
6759
+ if (!defaultModelId || !baseUrl || !apiKeyEnv) return void 0;
6168
6760
  const provider = clean(getEnv("BORING_AGENT_INFOMANIAK_PROVIDER")) ?? INFOMANIAK_PROVIDER;
6761
+ const modelName = clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL_NAME"));
6762
+ const models = modelIds.map((id) => {
6763
+ const model = { id };
6764
+ if (id === defaultModelId && modelName) model.name = modelName;
6765
+ return model;
6766
+ });
6767
+ if (!models.some((model) => model.id === defaultModelId)) {
6768
+ models.push({ id: defaultModelId, name: modelName });
6769
+ }
6169
6770
  return {
6170
6771
  provider,
6171
- model: { provider, id: modelId },
6772
+ models: models.map((model) => ({ provider, id: model.id })),
6773
+ defaultModel: { provider, id: defaultModelId },
6172
6774
  config: buildOpenAICompatibleProviderConfig({
6173
- modelId,
6174
- modelName: clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL_NAME")),
6775
+ models,
6175
6776
  apiKeyEnv,
6176
6777
  baseUrl,
6177
- envPrefix: "BORING_AGENT_INFOMANIAK"
6778
+ envPrefix: "BORING_AGENT_INFOMANIAK",
6779
+ // Infomaniak's OpenAI-compatible chat endpoint rejects OpenAI's newer
6780
+ // `developer` role (surfacing as "400 Unexpected message role"). Hosts
6781
+ // can opt back in with BORING_AGENT_INFOMANIAK_SUPPORTS_DEVELOPER_ROLE=1.
6782
+ compatDefaults: { supportsDeveloperRole: false, supportsReasoningEffort: false }
6178
6783
  })
6179
6784
  };
6180
6785
  }
@@ -6187,12 +6792,15 @@ function readCustomProvider() {
6187
6792
  "BORING_AGENT_CUSTOM_MODEL_API_KEY"
6188
6793
  ]);
6189
6794
  if (!provider || !modelId || !baseUrl || !apiKeyEnv) return void 0;
6795
+ const modelName = clean(getEnv("BORING_AGENT_CUSTOM_MODEL_NAME"));
6796
+ const model = { id: modelId };
6797
+ if (modelName) model.name = modelName;
6190
6798
  return {
6191
6799
  provider,
6192
- model: { provider, id: modelId },
6800
+ models: [{ provider, id: modelId }],
6801
+ defaultModel: { provider, id: modelId },
6193
6802
  config: buildOpenAICompatibleProviderConfig({
6194
- modelId,
6195
- modelName: clean(getEnv("BORING_AGENT_CUSTOM_MODEL_NAME")),
6803
+ models: [model],
6196
6804
  apiKeyEnv,
6197
6805
  baseUrl,
6198
6806
  envPrefix: "BORING_AGENT_CUSTOM_MODEL"
@@ -6206,7 +6814,7 @@ function registerConfiguredModelProviders(registry) {
6206
6814
  if (!provider) continue;
6207
6815
  if (seen.has(provider.provider)) continue;
6208
6816
  registry.registerProvider(provider.provider, provider.config);
6209
- registered.push(provider.model);
6817
+ registered.push(...provider.models);
6210
6818
  seen.add(provider.provider);
6211
6819
  }
6212
6820
  return registered;
@@ -6234,7 +6842,7 @@ function readConfiguredDefaultModel() {
6234
6842
  if (explicitProvider && explicitId) {
6235
6843
  return { provider: explicitProvider, id: explicitId };
6236
6844
  }
6237
- return readPiSettingsDefaultModel() ?? readInfomaniakProvider()?.model ?? readCustomProvider()?.model;
6845
+ return readPiSettingsDefaultModel() ?? readInfomaniakProvider()?.defaultModel ?? readCustomProvider()?.defaultModel;
6238
6846
  }
6239
6847
 
6240
6848
  // src/server/piPackages.ts
@@ -6299,6 +6907,10 @@ var PYTHON_RUNTIME_GUIDELINE = [
6299
6907
  function composeSystemPromptAppend(hostAppend) {
6300
6908
  return [WORKSPACE_PATHS_GUIDELINE, PYTHON_RUNTIME_GUIDELINE, hostAppend?.trim()].filter(Boolean).join("\n\n");
6301
6909
  }
6910
+ function withPiHarnessDefaults(pi) {
6911
+ const { noContextFiles = true, noSkills = true, ...rest } = pi ?? {};
6912
+ return { ...rest, noContextFiles, noSkills };
6913
+ }
6302
6914
  function buildDynamicPromptExtension(source) {
6303
6915
  return (pi) => {
6304
6916
  pi.on("before_agent_start", async (event) => {
@@ -6354,8 +6966,8 @@ function mergeInjectedProjectPackages(settingsJson, piPackages) {
6354
6966
  }
6355
6967
  function createResourceSettingsManager(cwd, agentDir, piPackages) {
6356
6968
  if (piPackages.length === 0) return SettingsManager2.create(cwd, agentDir);
6357
- const globalSettingsPath = join10(agentDir, "settings.json");
6358
- const projectSettingsPath = join10(cwd, ".pi", "settings.json");
6969
+ const globalSettingsPath = join11(agentDir, "settings.json");
6970
+ const projectSettingsPath = join11(cwd, ".pi", "settings.json");
6359
6971
  let globalSettingsOverrideJson;
6360
6972
  let projectSettingsOverrideJson;
6361
6973
  const storage = {
@@ -6385,7 +6997,7 @@ async function applyRequestedSessionOptions(handle, input) {
6385
6997
  handle.piSession.setThinkingLevel(input.thinkingLevel);
6386
6998
  }
6387
6999
  }
6388
- var log3 = createLogger("pi-harness");
7000
+ var log4 = createLogger("pi-harness");
6389
7001
  function deriveSourcePlugin(sourceInfo) {
6390
7002
  if (!sourceInfo) return void 0;
6391
7003
  const path4 = typeof sourceInfo.path === "string" ? sourceInfo.path : "";
@@ -6410,6 +7022,7 @@ function normalizeSlashCommandInfo(command) {
6410
7022
  };
6411
7023
  }
6412
7024
  function createPiCodingAgentHarness(opts) {
7025
+ const pi = withPiHarnessDefaults(opts.pi);
6413
7026
  const sessionStore = new PiSessionStore(opts.runtimeCwd ?? opts.cwd, {
6414
7027
  sessionNamespace: opts.sessionNamespace,
6415
7028
  sessionDir: opts.sessionDir,
@@ -6420,22 +7033,22 @@ function createPiCodingAgentHarness(opts) {
6420
7033
  const effectivePackages = [];
6421
7034
  const effectiveExtensionPaths = [];
6422
7035
  const refreshEffectiveResources = () => {
6423
- const dynamic = opts.pi?.getHotReloadableResources?.() ?? {};
7036
+ const dynamic = pi.getHotReloadableResources?.() ?? {};
6424
7037
  effectiveSkillPaths.splice(
6425
7038
  0,
6426
7039
  effectiveSkillPaths.length,
6427
- ...opts.pi?.additionalSkillPaths ?? [],
7040
+ ...pi.additionalSkillPaths ?? [],
6428
7041
  ...dynamic.additionalSkillPaths ?? []
6429
7042
  );
6430
7043
  effectivePackages.splice(
6431
7044
  0,
6432
7045
  effectivePackages.length,
6433
- ...mergePiPackageSources(opts.pi?.packages ?? [], dynamic.packages ?? [])
7046
+ ...mergePiPackageSources(pi.packages ?? [], dynamic.packages ?? [])
6434
7047
  );
6435
7048
  effectiveExtensionPaths.splice(
6436
7049
  0,
6437
7050
  effectiveExtensionPaths.length,
6438
- ...opts.pi?.extensionPaths ?? [],
7051
+ ...pi.extensionPaths ?? [],
6439
7052
  ...dynamic.extensionPaths ?? []
6440
7053
  );
6441
7054
  };
@@ -6492,7 +7105,7 @@ function createPiCodingAgentHarness(opts) {
6492
7105
  const extensionFactories = [
6493
7106
  toolErrorResultExtension,
6494
7107
  ...dynamicPromptExtension ? [dynamicPromptExtension] : [],
6495
- ...opts.pi?.extensionFactories ?? []
7108
+ ...pi.extensionFactories ?? []
6496
7109
  ];
6497
7110
  const settingsManager = createResourceSettingsManager(
6498
7111
  opts.cwd,
@@ -6506,8 +7119,8 @@ function createPiCodingAgentHarness(opts) {
6506
7119
  appendSystemPromptOverride: (base) => [...base, composedSystemPromptAppend],
6507
7120
  ...effectiveExtensionPaths.length ? { additionalExtensionPaths: effectiveExtensionPaths } : {},
6508
7121
  ...extensionFactories.length ? { extensionFactories } : {},
6509
- ...opts.pi?.noContextFiles ? { noContextFiles: true } : {},
6510
- ...opts.pi?.noSkills ? { noSkills: true } : {},
7122
+ ...pi.noContextFiles ? { noContextFiles: true } : {},
7123
+ ...pi.noSkills ? { noSkills: true } : {},
6511
7124
  ...effectiveSkillPaths.length ? { additionalSkillPaths: effectiveSkillPaths } : {},
6512
7125
  // skillsOverride REPLACES Pi's resolved skill set, which includes
6513
7126
  // skills contributed by host-declared pi packages (e.g.
@@ -6516,7 +7129,7 @@ function createPiCodingAgentHarness(opts) {
6516
7129
  // Passing additionalSkillPaths is not, by itself, a request to throw
6517
7130
  // away package skills — those should keep flowing through Pi's loader
6518
7131
  // and merge with the additional paths.
6519
- ...opts.pi?.noSkills ? {
7132
+ ...pi.noSkills ? {
6520
7133
  skillsOverride: () => loadSkills({
6521
7134
  cwd: opts.cwd,
6522
7135
  agentDir,
@@ -6647,12 +7260,12 @@ ${entry.message}`;
6647
7260
  }
6648
7261
 
6649
7262
  // src/server/harness/pi-coding-agent/pluginLoader.ts
6650
- import { readdir as readdir7, stat as stat9, readFile as readFile10 } from "fs/promises";
6651
- import { join as join11, extname as extname4, resolve as resolve8, sep as sep6, relative as relative9 } from "path";
7263
+ import { readdir as readdir8, stat as stat9, readFile as readFile10 } from "fs/promises";
7264
+ import { join as join12, extname as extname4, resolve as resolve8, sep as sep6, relative as relative9 } from "path";
6652
7265
  import { homedir as homedir4 } from "os";
6653
7266
  import { pathToFileURL as pathToFileURL2 } from "url";
6654
7267
  var VALID_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs"]);
6655
- var GLOBAL_DIR = join11(homedir4(), ".pi", "agent", "extensions");
7268
+ var GLOBAL_DIR = join12(homedir4(), ".pi", "agent", "extensions");
6656
7269
  var LOCAL_DIR = ".pi/extensions";
6657
7270
  var EXTENSIONS_JSON = ".pi/extensions.json";
6658
7271
  async function dirExists2(path4) {
@@ -6673,8 +7286,8 @@ async function fileExists(path4) {
6673
7286
  }
6674
7287
  async function discoverFromDir(dir, source) {
6675
7288
  if (!await dirExists2(dir)) return [];
6676
- const entries = await readdir7(dir);
6677
- return entries.filter((e) => VALID_EXTENSIONS.has(extname4(e))).map((e) => ({ path: join11(dir, e), source }));
7289
+ const entries = await readdir8(dir);
7290
+ return entries.filter((e) => VALID_EXTENSIONS.has(extname4(e))).map((e) => ({ path: join12(dir, e), source }));
6678
7291
  }
6679
7292
  function extractTools(mod) {
6680
7293
  const tools = [];
@@ -6704,17 +7317,17 @@ async function loadModule(filePath, importFn) {
6704
7317
  return extractTools(mod);
6705
7318
  }
6706
7319
  async function discoverNpmPlugins(cwd) {
6707
- const nodeModulesDir = join11(cwd, "node_modules");
7320
+ const nodeModulesDir = join12(cwd, "node_modules");
6708
7321
  if (!await dirExists2(nodeModulesDir)) return [];
6709
7322
  try {
6710
- const entries = await readdir7(nodeModulesDir);
6711
- return entries.filter((e) => e.startsWith("pi-plugin-")).map((e) => join11(nodeModulesDir, e));
7323
+ const entries = await readdir8(nodeModulesDir);
7324
+ return entries.filter((e) => e.startsWith("pi-plugin-")).map((e) => join12(nodeModulesDir, e));
6712
7325
  } catch {
6713
7326
  return [];
6714
7327
  }
6715
7328
  }
6716
7329
  async function loadNpmPlugin(pkgDir, importFn) {
6717
- const pkgJsonPath = join11(pkgDir, "package.json");
7330
+ const pkgJsonPath = join12(pkgDir, "package.json");
6718
7331
  if (!await fileExists(pkgJsonPath)) return [];
6719
7332
  const pkgJson = JSON.parse(await readFile10(pkgJsonPath, "utf-8"));
6720
7333
  const main = pkgJson.main ?? "index.js";
@@ -6730,7 +7343,7 @@ async function loadNpmPlugin(pkgDir, importFn) {
6730
7343
  return loadModule(resolvedMain, importFn);
6731
7344
  }
6732
7345
  async function loadExtensionsJson(cwd) {
6733
- const configPath = join11(cwd, EXTENSIONS_JSON);
7346
+ const configPath = join12(cwd, EXTENSIONS_JSON);
6734
7347
  if (!await fileExists(configPath)) return null;
6735
7348
  try {
6736
7349
  const raw = await readFile10(configPath, "utf-8");
@@ -6748,7 +7361,7 @@ async function loadPlugins(options) {
6748
7361
  const globals = await discoverFromDir(GLOBAL_DIR, "global");
6749
7362
  candidates.push(...globals);
6750
7363
  }
6751
- const localDir = join11(options.cwd, LOCAL_DIR);
7364
+ const localDir = join12(options.cwd, LOCAL_DIR);
6752
7365
  const locals = await discoverFromDir(localDir, "local");
6753
7366
  candidates.push(...locals);
6754
7367
  const npmDirs = await discoverNpmPlugins(options.cwd);
@@ -6758,7 +7371,7 @@ async function loadPlugins(options) {
6758
7371
  const config = await loadExtensionsJson(options.cwd);
6759
7372
  if (config?.npm) {
6760
7373
  for (const pkg of config.npm) {
6761
- const pkgDir = join11(options.cwd, "node_modules", pkg);
7374
+ const pkgDir = join12(options.cwd, "node_modules", pkg);
6762
7375
  if (await dirExists2(pkgDir)) {
6763
7376
  const already = candidates.some((c) => c.path === pkgDir);
6764
7377
  if (!already) {
@@ -6807,8 +7420,8 @@ import {
6807
7420
 
6808
7421
  // src/server/tools/operations/bound.ts
6809
7422
  import { constants as constants4 } from "fs";
6810
- import { access as access4, lstat as lstat4, mkdir as mkdir12, readFile as readFile11, readdir as readdir8, readlink, realpath as realpath4, stat as stat10, writeFile as writeFile7 } from "fs/promises";
6811
- import { dirname as dirname11, isAbsolute as isAbsolute7, join as join12, relative as relative10, resolve as resolve9 } from "path";
7423
+ import { access as access4, lstat as lstat4, mkdir as mkdir12, readFile as readFile11, readdir as readdir9, readlink, realpath as realpath4, stat as stat10, writeFile as writeFile7 } from "fs/promises";
7424
+ import { dirname as dirname11, isAbsolute as isAbsolute7, join as join13, relative as relative10, resolve as resolve9 } from "path";
6812
7425
  function toPosixPath(value) {
6813
7426
  return value.split("\\").join("/");
6814
7427
  }
@@ -6861,7 +7474,7 @@ function shouldSkipDir(relativePath, ignore) {
6861
7474
  }
6862
7475
  async function walkMatches(root, current, pattern, ignore, limit, out) {
6863
7476
  if (out.length >= limit) return;
6864
- const entries = await readdir8(current, { withFileTypes: true });
7477
+ const entries = await readdir9(current, { withFileTypes: true });
6865
7478
  for (const entry of entries) {
6866
7479
  if (out.length >= limit) return;
6867
7480
  const absolutePath = resolve9(current, entry.name);
@@ -6939,7 +7552,7 @@ function boundFs(workspaceRoot, opts = {}) {
6939
7552
  const normalized = toPosixPath(absolutePath);
6940
7553
  if (normalized === runtimeRoot) return workspaceRoot;
6941
7554
  if (normalized.startsWith(`${runtimeRoot}/`)) {
6942
- return join12(workspaceRoot, ...normalized.slice(runtimeRoot.length + 1).split("/"));
7555
+ return join13(workspaceRoot, ...normalized.slice(runtimeRoot.length + 1).split("/"));
6943
7556
  }
6944
7557
  return absolutePath;
6945
7558
  };
@@ -7043,7 +7656,7 @@ function boundFs(workspaceRoot, opts = {}) {
7043
7656
  async readdir(absolutePath) {
7044
7657
  const storagePath = toStoragePath(absolutePath);
7045
7658
  await assertWithinWorkspace(workspaceRoot, storagePath);
7046
- return await readdir8(storagePath);
7659
+ return await readdir9(storagePath);
7047
7660
  }
7048
7661
  };
7049
7662
  return { read, write, edit, find, grep, ls };
@@ -7083,10 +7696,10 @@ function vercelBashOps(sandbox, opts = {}) {
7083
7696
  return {
7084
7697
  exec(command, cwd, { onData, signal, timeout, env }) {
7085
7698
  const effectiveEnv = opts.mergeEnv ? opts.mergeEnv(env) : env;
7086
- const filteredEnv = effectiveEnv ? Object.fromEntries(Object.entries(effectiveEnv).filter((e) => e[1] != null)) : void 0;
7699
+ const filteredEnv2 = effectiveEnv ? Object.fromEntries(Object.entries(effectiveEnv).filter((e) => e[1] != null)) : void 0;
7087
7700
  return sandbox.exec(command, {
7088
7701
  cwd,
7089
- env: filteredEnv,
7702
+ env: filteredEnv2,
7090
7703
  signal,
7091
7704
  timeoutMs: timeout ? timeout * 1e3 : void 0,
7092
7705
  onStdout: (chunk) => onData(Buffer.from(chunk)),
@@ -7470,8 +8083,7 @@ function adaptPiTool(piTool) {
7470
8083
  }
7471
8084
  function buildFilesystemAgentTools(bundle) {
7472
8085
  const cwd = bundle.workspace.root;
7473
- const storageRoot = getRuntimeBundleStorageRoot(bundle);
7474
- if (bundle.sandbox.provider === "vercel-sandbox") {
8086
+ if (bundle.sandbox.provider === "vercel-sandbox" || bundle.sandbox.provider === "remote-worker") {
7475
8087
  return [
7476
8088
  adaptPiTool(createReadToolDefinition(cwd, { operations: vercelReadOps(bundle.workspace) })),
7477
8089
  adaptPiTool(createWriteToolDefinition(cwd, { operations: vercelWriteOps(bundle.workspace) })),
@@ -7481,6 +8093,7 @@ function buildFilesystemAgentTools(bundle) {
7481
8093
  adaptPiTool(createLsToolDefinition(cwd, { operations: vercelLsOps(bundle.workspace) }))
7482
8094
  ];
7483
8095
  }
8096
+ const storageRoot = getRuntimeBundleStorageRoot(bundle);
7484
8097
  const ops = boundFs(storageRoot, { runtimeRoot: cwd });
7485
8098
  return [
7486
8099
  adaptPiTool(createReadToolDefinition(cwd, { operations: ops.read })),
@@ -7616,9 +8229,9 @@ function directSpawnHook(workspaceRoot, runtime) {
7616
8229
  }
7617
8230
  var VERCEL_SAFE_DEFAULT_PATH = "/vercel/runtimes/node24/bin:/vercel/runtimes/node22/bin:/usr/local/bin:/usr/bin:/bin";
7618
8231
  function bashOptionsForMode(bundle, runtime) {
7619
- const storageRoot = getRuntimeBundleStorageRoot(bundle);
7620
8232
  switch (bundle.sandbox.provider) {
7621
8233
  case "vercel-sandbox":
8234
+ case "remote-worker":
7622
8235
  return {
7623
8236
  operations: vercelBashOps(bundle.sandbox, {
7624
8237
  // The pi bash tool's env may include the host process env. Never
@@ -7627,16 +8240,20 @@ function bashOptionsForMode(bundle, runtime) {
7627
8240
  mergeEnv: () => mergeRuntimeEnv(runtime, { PATH: VERCEL_SAFE_DEFAULT_PATH })
7628
8241
  })
7629
8242
  };
7630
- case "bwrap":
8243
+ case "bwrap": {
8244
+ const storageRoot = getRuntimeBundleStorageRoot(bundle);
7631
8245
  return {
7632
8246
  operations: createLocalBashOperations(),
7633
8247
  spawnHook: bwrapSpawnHook(storageRoot, runtime)
7634
8248
  };
7635
- default:
8249
+ }
8250
+ default: {
8251
+ const storageRoot = getRuntimeBundleStorageRoot(bundle);
7636
8252
  return {
7637
8253
  operations: createLocalBashOperations(),
7638
8254
  spawnHook: directSpawnHook(storageRoot, runtime)
7639
8255
  };
8256
+ }
7640
8257
  }
7641
8258
  }
7642
8259
  function isRuntimeReady(readiness) {
@@ -7813,9 +8430,21 @@ var DEFAULT_BUFFER_SIZE = 1e3;
7813
8430
  function createFsEventBroadcaster(watcher, opts = {}) {
7814
8431
  const bufferSize = opts.bufferSize ?? DEFAULT_BUFFER_SIZE;
7815
8432
  const buffer = [];
7816
- const listeners = /* @__PURE__ */ new Set();
8433
+ const listeners = /* @__PURE__ */ new Map();
7817
8434
  let nextSeq = 1;
7818
8435
  let closed = false;
8436
+ let gapAfterSeq = null;
8437
+ const handleControlEvent = (event) => {
8438
+ if (closed || event.type !== "resync-required") return;
8439
+ gapAfterSeq = nextSeq - 1;
8440
+ buffer.length = 0;
8441
+ for (const { onResyncRequired } of [...listeners.values()]) {
8442
+ try {
8443
+ onResyncRequired?.();
8444
+ } catch {
8445
+ }
8446
+ }
8447
+ };
7819
8448
  const watcherUnsub = watcher.subscribe((change) => {
7820
8449
  if (closed) return;
7821
8450
  const env = {
@@ -7826,13 +8455,13 @@ function createFsEventBroadcaster(watcher, opts = {}) {
7826
8455
  };
7827
8456
  buffer.push(env);
7828
8457
  if (buffer.length > bufferSize) buffer.shift();
7829
- for (const l of [...listeners]) {
8458
+ for (const l of [...listeners.keys()]) {
7830
8459
  try {
7831
8460
  l(env);
7832
8461
  } catch {
7833
8462
  }
7834
8463
  }
7835
- });
8464
+ }, { onControlEvent: handleControlEvent });
7836
8465
  return {
7837
8466
  subscribe(listener, sopts) {
7838
8467
  if (closed) {
@@ -7843,13 +8472,13 @@ function createFsEventBroadcaster(watcher, opts = {}) {
7843
8472
  let resyncRequired = false;
7844
8473
  if (typeof sopts?.lastSeenSeq === "number" && sopts.lastSeenSeq > 0) {
7845
8474
  const head = buffer.length > 0 ? buffer[0].seq : nextSeq;
7846
- if (sopts.lastSeenSeq < head - 1) {
8475
+ if (gapAfterSeq != null && sopts.lastSeenSeq <= gapAfterSeq || sopts.lastSeenSeq < head - 1) {
7847
8476
  resyncRequired = true;
7848
8477
  } else {
7849
8478
  replay = buffer.filter((e) => e.seq > sopts.lastSeenSeq);
7850
8479
  }
7851
8480
  }
7852
- listeners.add(listener);
8481
+ listeners.set(listener, { onResyncRequired: sopts?.onResyncRequired });
7853
8482
  return {
7854
8483
  replay,
7855
8484
  resyncRequired,
@@ -7877,11 +8506,20 @@ function fsEventsRoutes(app, opts, done) {
7877
8506
  const ensureBroadcaster = async (request) => {
7878
8507
  const workspace = opts.getWorkspace ? await opts.getWorkspace(request) : opts.workspace;
7879
8508
  if (!workspace) throw new Error("fs event route requires workspace or getWorkspace");
7880
- if (typeof workspace.watch !== "function") return null;
8509
+ if (typeof workspace.watch !== "function") {
8510
+ return { unsupported: { reason: "watch_not_implemented" } };
8511
+ }
7881
8512
  const workspaceId = request.workspaceContext?.workspaceId ?? "default";
7882
8513
  const existing = broadcasters.get(workspaceId);
7883
8514
  if (existing) return { workspaceId, entry: existing };
7884
- const broadcaster = createFsEventBroadcaster(workspace.watch());
8515
+ const watcher = workspace.watch();
8516
+ const readiness = await watcher.whenReady?.() ?? { ok: true };
8517
+ if (!readiness.ok) {
8518
+ return { unsupported: { reason: readiness.reason, ...readiness.message ? { message: readiness.message } : {} } };
8519
+ }
8520
+ const existingAfterWait = broadcasters.get(workspaceId);
8521
+ if (existingAfterWait) return { workspaceId, entry: existingAfterWait };
8522
+ const broadcaster = createFsEventBroadcaster(watcher);
7885
8523
  const entry = { broadcaster, subscribers: 0 };
7886
8524
  broadcasters.set(workspaceId, entry);
7887
8525
  return { workspaceId, entry };
@@ -7901,8 +8539,8 @@ function fsEventsRoutes(app, opts, done) {
7901
8539
  const resolved = await ensureBroadcaster(request);
7902
8540
  reply.hijack();
7903
8541
  setupSse(request, reply.raw);
7904
- if (!resolved) {
7905
- writeSse(reply.raw, "unsupported", { reason: "watch_not_implemented" });
8542
+ if ("unsupported" in resolved) {
8543
+ writeSse(reply.raw, "unsupported", resolved.unsupported);
7906
8544
  reply.raw.end();
7907
8545
  return;
7908
8546
  }
@@ -7913,7 +8551,10 @@ function fsEventsRoutes(app, opts, done) {
7913
8551
  try {
7914
8552
  sub = entry.broadcaster.subscribe(
7915
8553
  (env) => writeChange(reply.raw, env),
7916
- lastSeenSeq != null ? { lastSeenSeq } : void 0
8554
+ {
8555
+ ...lastSeenSeq != null ? { lastSeenSeq } : {},
8556
+ onResyncRequired: () => writeSse(reply.raw, "resync-required", {})
8557
+ }
7917
8558
  );
7918
8559
  } catch (error) {
7919
8560
  releaseBroadcaster(workspaceId, entry);
@@ -7924,10 +8565,6 @@ function fsEventsRoutes(app, opts, done) {
7924
8565
  }
7925
8566
  if (sub.resyncRequired) {
7926
8567
  writeSse(reply.raw, "resync-required", {});
7927
- sub.unsubscribe();
7928
- releaseBroadcaster(workspaceId, entry);
7929
- reply.raw.end();
7930
- return;
7931
8568
  }
7932
8569
  for (const env of sub.replay) writeChange(reply.raw, env);
7933
8570
  const heartbeat = setInterval(() => {
@@ -8128,8 +8765,8 @@ function skillsRoutes(app, opts, done) {
8128
8765
  const workspaceRoot = opts.getWorkspaceRoot ? await opts.getWorkspaceRoot(request) : opts.workspaceRoot;
8129
8766
  const additionalSkillPaths = opts.getAdditionalSkillPaths ? await opts.getAdditionalSkillPaths(request) : opts.additionalSkillPaths;
8130
8767
  const piPackages = opts.getPiPackages ? await opts.getPiPackages(request) : opts.piPackages;
8131
- const noSkills = opts.getNoSkills ? await opts.getNoSkills(request) : opts.noSkills;
8132
- const cacheKey = JSON.stringify([workspaceRoot, additionalSkillPaths ?? [], piPackages ?? [], Boolean(noSkills)]);
8768
+ const noSkills = (opts.getNoSkills ? await opts.getNoSkills(request) : opts.noSkills) ?? withPiHarnessDefaults().noSkills;
8769
+ const cacheKey = JSON.stringify([workspaceRoot, additionalSkillPaths ?? [], piPackages ?? [], noSkills]);
8133
8770
  const now = Date.now();
8134
8771
  for (const [key, entry] of cached) {
8135
8772
  if (entry.expiresAt <= now) cached.delete(key);
@@ -8158,7 +8795,7 @@ function skillsRoutes(app, opts, done) {
8158
8795
  cwd: workspaceRoot,
8159
8796
  agentDir,
8160
8797
  skillPaths: [...packageSkillPaths, ...additionalSkillPaths ?? []],
8161
- includeDefaults: false
8798
+ includeDefaults: !noSkills
8162
8799
  });
8163
8800
  const skills = result.skills.map((s) => ({
8164
8801
  name: s.name,
@@ -8567,6 +9204,7 @@ function statusCodeFromError(err) {
8567
9204
  }
8568
9205
  const parsedCode = ErrorCode.safeParse(err?.code);
8569
9206
  if (parsedCode.success && parsedCode.data === ErrorCode.enum.SESSION_NOT_FOUND) return 404;
9207
+ if (parsedCode.success && parsedCode.data === ErrorCode.enum.PAYMENT_REQUIRED) return 402;
8570
9208
  return 500;
8571
9209
  }
8572
9210
 
@@ -9012,9 +9650,10 @@ function gitRoutes(app, opts, done) {
9012
9650
  if (path4 === null) return;
9013
9651
  const workspaceRoot = await resolveWorkspaceRoot(request);
9014
9652
  if (!workspaceRoot) {
9015
- return reply.code(500).send({
9016
- error: { code: ERROR_CODE_INTERNAL, message: "workspace root unavailable" }
9017
- });
9653
+ return {
9654
+ enabled: false,
9655
+ reason: "Git file URLs are unavailable for this runtime."
9656
+ };
9018
9657
  }
9019
9658
  try {
9020
9659
  return await resolveGitFileUrl(workspaceRoot, path4);
@@ -9406,7 +10045,9 @@ var PiChatEventMapper = class {
9406
10045
  const mapped = [
9407
10046
  ...this.mapAgentEndFinalAssistant(event, turnId),
9408
10047
  ...this.mapAgentEndError(event, turnId, status),
9409
- this.event({ type: "agent-end", turnId, status })
10048
+ // willRetry marks a non-terminal end (auto-retry coming) so once-per-settle
10049
+ // consumers can ignore it; mirrors mapAgentEndError's own willRetry gate.
10050
+ this.event({ type: "agent-end", turnId, status, ...event.willRetry === true ? { willRetry: true } : {} })
9410
10051
  ];
9411
10052
  this.activeAssistantMessageId = void 0;
9412
10053
  this.toolCallMessageIds.clear();
@@ -10105,6 +10746,454 @@ function messageCreatedAtMs(message) {
10105
10746
  return Number.isFinite(timestamp) ? timestamp : void 0;
10106
10747
  }
10107
10748
 
10749
+ // src/server/pi-chat/metering.ts
10750
+ var meteringLogger = createLogger("pi-chat-metering");
10751
+ var defaultMeteringErrorLogger = (message, error) => {
10752
+ meteringLogger.warn(message, { error });
10753
+ };
10754
+ function promptRunId(sessionId, clientNonce) {
10755
+ return `pi-run:${sessionId}:prompt:${clientNonce}`;
10756
+ }
10757
+ function followUpRunId(sessionId, clientNonce, clientSeq) {
10758
+ return `pi-run:${sessionId}:followup:${clientNonce}:${clientSeq}`;
10759
+ }
10760
+ function followUpMatches(run2, selector) {
10761
+ const fu = run2.followUp;
10762
+ if (!fu) return false;
10763
+ if (selector.clientNonce !== void 0) return fu.clientNonce === selector.clientNonce;
10764
+ if (selector.clientSeq !== void 0) return fu.clientSeq === selector.clientSeq;
10765
+ return false;
10766
+ }
10767
+ function takeQueuedFollowUp(state, selector) {
10768
+ const index = state.queued.findIndex((run2) => followUpMatches(run2, selector));
10769
+ if (index < 0) return void 0;
10770
+ return state.queued.splice(index, 1)[0];
10771
+ }
10772
+ function isRecord3(value) {
10773
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10774
+ }
10775
+ function readTokenCount(value) {
10776
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
10777
+ }
10778
+ function normalizeMeteringUsage(value) {
10779
+ if (!isRecord3(value)) return void 0;
10780
+ const cost = isRecord3(value.cost) ? value.cost : {};
10781
+ return {
10782
+ input: readTokenCount(value.input),
10783
+ output: readTokenCount(value.output),
10784
+ cacheRead: readTokenCount(value.cacheRead),
10785
+ cacheWrite: readTokenCount(value.cacheWrite),
10786
+ cost: {
10787
+ input: readTokenCount(cost.input),
10788
+ output: readTokenCount(cost.output),
10789
+ cacheRead: readTokenCount(cost.cacheRead),
10790
+ cacheWrite: readTokenCount(cost.cacheWrite),
10791
+ total: readTokenCount(cost.total)
10792
+ }
10793
+ };
10794
+ }
10795
+ var PiChatMeteringCoordinator = class {
10796
+ sessions = /* @__PURE__ */ new Map();
10797
+ inflightOps = /* @__PURE__ */ new Set();
10798
+ runInstances = 0;
10799
+ sink;
10800
+ logError;
10801
+ constructor(sink, logError) {
10802
+ this.sink = sink;
10803
+ this.logError = logError ?? defaultMeteringErrorLogger;
10804
+ }
10805
+ /** Reserve a prompt run. Throws (fail closed) when the sink rejects. */
10806
+ async reservePrompt(input) {
10807
+ const state = this.sessionState(input.sessionId);
10808
+ const runId = promptRunId(input.sessionId, input.clientNonce);
10809
+ const existing = this.findRun(state, runId);
10810
+ if (existing) {
10811
+ await existing.reservation;
10812
+ return "duplicate";
10813
+ }
10814
+ const run2 = this.createRun(input, "prompt", runId);
10815
+ state.pendingPrompts.push(run2);
10816
+ return this.materializeReservation(state, run2, input);
10817
+ }
10818
+ /** Reserve a follow-up run. Throws (fail closed) when the sink rejects.
10819
+ * Returns 'duplicate' when this selector already has a tracked/consumed run. */
10820
+ async reserveFollowUp(input) {
10821
+ const state = this.sessionState(input.sessionId);
10822
+ const runId = followUpRunId(input.sessionId, input.clientNonce, input.clientSeq);
10823
+ const existing = this.findRun(state, runId);
10824
+ if (existing) {
10825
+ await existing.reservation;
10826
+ return "duplicate";
10827
+ }
10828
+ if (state.consumedFollowUpNonces.has(input.clientNonce)) return "duplicate";
10829
+ const run2 = this.createRun(input, "followup", runId);
10830
+ run2.followUp = { clientNonce: input.clientNonce, clientSeq: input.clientSeq };
10831
+ state.queued.push(run2);
10832
+ return this.materializeReservation(state, run2, input);
10833
+ }
10834
+ /**
10835
+ * Acquire the reservation for a freshly-registered run. The reserve is put on
10836
+ * the run's ops chain so a concurrent release/settle is ordered strictly
10837
+ * after the reservation row exists. Returns 'cancelled' (skip execution) when
10838
+ * a concurrent stop/interrupt/delete terminated the run while the reserve was
10839
+ * in flight; throws (fail closed) when the sink rejects.
10840
+ */
10841
+ async materializeReservation(state, run2, input) {
10842
+ run2.reservation = this.applyReservation(run2, input);
10843
+ const reserveOp = run2.reservation.catch(() => {
10844
+ });
10845
+ run2.ops = reserveOp;
10846
+ this.inflightOps.add(reserveOp);
10847
+ void reserveOp.finally(() => this.inflightOps.delete(reserveOp));
10848
+ try {
10849
+ await run2.reservation;
10850
+ } catch (err) {
10851
+ this.removeReservingRun(state, run2, input.sessionId);
10852
+ throw err;
10853
+ }
10854
+ return run2.terminal ? "cancelled" : "created";
10855
+ }
10856
+ /** True while a non-terminal prompt run exists for this nonce (accept →
10857
+ * agent-end). Used to suppress duplicate-nonce execution for the full run
10858
+ * lifetime, not just until the user message-start is consumed. */
10859
+ hasPromptRun(sessionId, clientNonce) {
10860
+ const state = this.sessions.get(sessionId);
10861
+ if (!state) return false;
10862
+ const run2 = this.findRun(state, promptRunId(sessionId, clientNonce));
10863
+ return run2 !== void 0 && !run2.terminal;
10864
+ }
10865
+ /** True while a non-terminal follow-up run exists for this selector (queued
10866
+ * or consumed-and-active). Used to suppress duplicate follow-up enqueues. */
10867
+ hasFollowUpRun(sessionId, selector) {
10868
+ const state = this.sessions.get(sessionId);
10869
+ if (!state) return false;
10870
+ if (selector.clientNonce !== void 0 && state.consumedFollowUpNonces.has(selector.clientNonce)) return true;
10871
+ if (state.queued.some((run2) => followUpMatches(run2, selector))) return true;
10872
+ const active = state.active;
10873
+ return active !== void 0 && !active.terminal && active.followUp !== void 0 && followUpMatches(active, selector);
10874
+ }
10875
+ /** The accepted prompt failed before/without running (sync throw or run rejection). */
10876
+ failPromptRun(sessionId, clientNonce) {
10877
+ const state = this.sessions.get(sessionId);
10878
+ if (!state) return;
10879
+ const runId = promptRunId(sessionId, clientNonce);
10880
+ const pendingIndex = state.pendingPrompts.findIndex((run2) => run2.scope.runId === runId);
10881
+ if (pendingIndex >= 0) {
10882
+ const [run2] = state.pendingPrompts.splice(pendingIndex, 1);
10883
+ if (run2) this.finishRun(run2, "error");
10884
+ } else if (state.active?.scope.runId === runId) {
10885
+ this.finishRun(state.active, "error");
10886
+ state.active = void 0;
10887
+ }
10888
+ this.pruneSession(sessionId, state);
10889
+ }
10890
+ /** A queued follow-up was rejected by the adapter before being queued. */
10891
+ failFollowUpRun(sessionId, selector) {
10892
+ const state = this.sessions.get(sessionId);
10893
+ if (!state) return;
10894
+ const run2 = takeQueuedFollowUp(state, selector);
10895
+ if (!run2) return;
10896
+ this.release(run2, "run-rejected");
10897
+ this.pruneSession(sessionId, state);
10898
+ }
10899
+ /**
10900
+ * A queued follow-up is being re-posted as a plain prompt (interrupt
10901
+ * fallback for runtimes without continueQueuedFollowUp). No
10902
+ * `followup-consumed` event will arrive, so bind its reservation to the
10903
+ * next agent-start instead.
10904
+ */
10905
+ promoteQueuedToPrompt(sessionId, selector) {
10906
+ const state = this.sessions.get(sessionId);
10907
+ if (!state) return;
10908
+ const run2 = takeQueuedFollowUp(state, selector);
10909
+ if (!run2) return;
10910
+ state.pendingPrompts.push(run2);
10911
+ }
10912
+ /**
10913
+ * A promoted-to-prompt follow-up failed before agent-start (the fallback
10914
+ * repost rejected). Release its reservation instead of stranding it in
10915
+ * pendingPrompts, where a later agent-start would otherwise misattribute
10916
+ * usage to it.
10917
+ */
10918
+ failPromotedFollowUp(sessionId, selector) {
10919
+ const state = this.sessions.get(sessionId);
10920
+ if (!state || selector.clientNonce === void 0 || selector.clientSeq === void 0) return;
10921
+ const runId = followUpRunId(sessionId, selector.clientNonce, selector.clientSeq);
10922
+ const index = state.pendingPrompts.findIndex((run3) => run3.scope.runId === runId);
10923
+ if (index < 0) return;
10924
+ const [run2] = state.pendingPrompts.splice(index, 1);
10925
+ if (run2) this.release(run2, "run-rejected");
10926
+ this.pruneSession(sessionId, state);
10927
+ }
10928
+ /**
10929
+ * Release prompt runs reserved but not yet bound to an agent-start —
10930
+ * e.g. a stop/interrupt landing in the window between acceptance and the
10931
+ * native agent_start. Without this they would sit `active` in the store
10932
+ * until their TTL, holding the user's balance. No charge.
10933
+ */
10934
+ releasePending(sessionId) {
10935
+ const state = this.sessions.get(sessionId);
10936
+ if (!state) return;
10937
+ for (const run2 of state.pendingPrompts) this.release(run2, "cancelled");
10938
+ state.pendingPrompts = [];
10939
+ this.pruneSession(sessionId, state);
10940
+ }
10941
+ /** Queue cleared via selector or entirely; release affected reservations. */
10942
+ releaseQueued(sessionId, selector) {
10943
+ const state = this.sessions.get(sessionId);
10944
+ if (!state) return;
10945
+ if (selector) {
10946
+ const run2 = takeQueuedFollowUp(state, selector);
10947
+ if (run2) this.release(run2, "queue-cleared");
10948
+ } else {
10949
+ for (const run2 of state.queued) this.release(run2, "queue-cleared");
10950
+ state.queued = [];
10951
+ }
10952
+ this.pruneSession(sessionId, state);
10953
+ }
10954
+ /** Session deleted: tear down every non-terminal run without charging. */
10955
+ releaseSession(sessionId) {
10956
+ const state = this.sessions.get(sessionId);
10957
+ if (!state) return;
10958
+ for (const run2 of state.queued) this.release(run2, "cancelled");
10959
+ for (const run2 of state.pendingPrompts) this.release(run2, "cancelled");
10960
+ if (state.active) this.finishRun(state.active, "aborted");
10961
+ this.sessions.delete(sessionId);
10962
+ }
10963
+ /**
10964
+ * Feed one native adapter event and its mapped PiChatEvents through the
10965
+ * correlation state machine. Must be called after the mapped events were
10966
+ * published (the mapper assigns turn ids during mapping).
10967
+ */
10968
+ observe(sessionId, nativeEvent, mappedEvents) {
10969
+ const state = this.sessions.get(sessionId);
10970
+ if (!state) return;
10971
+ for (const event of mappedEvents) {
10972
+ switch (event.type) {
10973
+ case "agent-start": {
10974
+ let next = state.pendingPrompts.shift();
10975
+ while (next && next.terminal) next = state.pendingPrompts.shift();
10976
+ if (next) {
10977
+ if (state.active && !state.active.terminal) this.finishRun(state.active, "ok");
10978
+ state.active = next;
10979
+ }
10980
+ break;
10981
+ }
10982
+ case "message-start": {
10983
+ if (event.role === "user" && event.clientSeq !== void 0) {
10984
+ this.consumeFollowUp(state, event);
10985
+ }
10986
+ break;
10987
+ }
10988
+ case "followup-consumed": {
10989
+ this.consumeFollowUp(state, event);
10990
+ break;
10991
+ }
10992
+ case "auto-retry-start": {
10993
+ if (state.active) {
10994
+ state.active.recordedMessageIds.clear();
10995
+ state.active.lastIdlessUsageKey = void 0;
10996
+ }
10997
+ break;
10998
+ }
10999
+ case "agent-end": {
11000
+ this.harvestAgentEndUsage(state, nativeEvent);
11001
+ if (isRecord3(nativeEvent) && nativeEvent.willRetry === true) break;
11002
+ if (state.active && !state.active.terminal) this.finishRun(state.active, event.status);
11003
+ state.active = void 0;
11004
+ break;
11005
+ }
11006
+ default:
11007
+ break;
11008
+ }
11009
+ }
11010
+ this.observeMessageEndUsage(state, nativeEvent);
11011
+ this.pruneSession(sessionId, state);
11012
+ }
11013
+ /** Promote a queued follow-up to the active run, settling the previous one. */
11014
+ consumeFollowUp(state, selector) {
11015
+ const run2 = takeQueuedFollowUp(state, selector);
11016
+ if (!run2) return;
11017
+ if (run2.followUp?.clientNonce) state.consumedFollowUpNonces.add(run2.followUp.clientNonce);
11018
+ if (state.active && !state.active.terminal) this.finishRun(state.active, "ok");
11019
+ state.active = run2;
11020
+ }
11021
+ /** Test/diagnostic hook: resolves after every queued sink call settles. */
11022
+ async flush() {
11023
+ while (this.inflightOps.size > 0) {
11024
+ await Promise.all([...this.inflightOps]);
11025
+ }
11026
+ }
11027
+ observeMessageEndUsage(state, nativeEvent) {
11028
+ if (!isRecord3(nativeEvent) || nativeEvent.type !== "message_end") return;
11029
+ this.recordAssistantUsage(state, nativeEvent.message);
11030
+ }
11031
+ /**
11032
+ * Some failure/abort paths never emit message_end; the final assistant
11033
+ * message (and its usage) only rides on agent_end's messages array. Runs
11034
+ * with already-recorded usage dedupe via recordedMessageIds.
11035
+ */
11036
+ harvestAgentEndUsage(state, nativeEvent) {
11037
+ if (!isRecord3(nativeEvent) || !Array.isArray(nativeEvent.messages)) return;
11038
+ for (let index = nativeEvent.messages.length - 1; index >= 0; index -= 1) {
11039
+ const message = nativeEvent.messages[index];
11040
+ if (!isRecord3(message) || message.role !== "assistant") continue;
11041
+ this.recordAssistantUsage(state, message, { isAgentEndFinal: true });
11042
+ return;
11043
+ }
11044
+ }
11045
+ recordAssistantUsage(state, message, opts = {}) {
11046
+ if (!isRecord3(message) || message.role !== "assistant") return;
11047
+ const usage = normalizeMeteringUsage(message.usage);
11048
+ if (!usage) return;
11049
+ const run2 = state.active ?? state.pendingPrompts[0];
11050
+ if (!run2 || run2.terminal) {
11051
+ this.logError("assistant usage arrived with no reserved run; usage not metered", { messageRole: "assistant" });
11052
+ return;
11053
+ }
11054
+ const messageId = typeof message.id === "string" && message.id.length > 0 ? message.id : void 0;
11055
+ if (messageId) {
11056
+ if (run2.recordedMessageIds.has(messageId)) return;
11057
+ run2.recordedMessageIds.add(messageId);
11058
+ } else {
11059
+ const stopReason2 = typeof message.stopReason === "string" ? message.stopReason : "";
11060
+ const signature = `sig:${usage.input}:${usage.output}:${usage.cacheRead}:${usage.cacheWrite}:${usage.cost.total}:${stopReason2}`;
11061
+ if (opts.isAgentEndFinal && signature === run2.lastIdlessUsageKey) return;
11062
+ run2.lastIdlessUsageKey = signature;
11063
+ }
11064
+ run2.usageCount += 1;
11065
+ const model = typeof message.model === "string" && message.model.length > 0 ? message.model : void 0;
11066
+ const provider = typeof message.provider === "string" && message.provider.length > 0 ? message.provider : void 0;
11067
+ const stopReason = typeof message.stopReason === "string" ? message.stopReason : void 0;
11068
+ const usageId = messageId ? `pi-usage:${run2.scope.sessionId}:message:${messageId}` : run2.reservationId ? `pi-usage:reservation:${run2.reservationId}:${run2.usageCount}` : `pi-usage:${run2.scope.runId}:${run2.instanceId}:${run2.usageCount}`;
11069
+ this.enqueue(
11070
+ run2,
11071
+ async () => {
11072
+ try {
11073
+ const result = await this.sink.recordUsage({
11074
+ ...run2.scope,
11075
+ reservationId: run2.reservationId,
11076
+ usageId,
11077
+ messageId,
11078
+ model: model || provider ? { provider, id: model } : void 0,
11079
+ usage,
11080
+ stopReason
11081
+ });
11082
+ if (result.billedMicros > 0) run2.billableUsageCount += 1;
11083
+ } catch (error) {
11084
+ const couldBill = usage.input + usage.output + usage.cacheRead + usage.cacheWrite > 0 || usage.cost.total > 0;
11085
+ if (couldBill) run2.usageWriteFailed = true;
11086
+ throw error;
11087
+ }
11088
+ },
11089
+ "recordUsage failed"
11090
+ );
11091
+ }
11092
+ /** Build a run synchronously (no await) so it can be registered before the
11093
+ * reserve sink call, closing the concurrent-duplicate race. */
11094
+ createRun(input, kind, runId) {
11095
+ return {
11096
+ scope: {
11097
+ workspaceId: input.workspaceId,
11098
+ userId: input.userId,
11099
+ sessionId: input.sessionId,
11100
+ runId,
11101
+ source: "pi-chat"
11102
+ },
11103
+ kind,
11104
+ reservationId: void 0,
11105
+ instanceId: this.runInstances += 1,
11106
+ usageCount: 0,
11107
+ billableUsageCount: 0,
11108
+ recordedMessageIds: /* @__PURE__ */ new Set(),
11109
+ lastIdlessUsageKey: void 0,
11110
+ usageWriteFailed: false,
11111
+ reservation: Promise.resolve(),
11112
+ terminal: false,
11113
+ ops: Promise.resolve()
11114
+ };
11115
+ }
11116
+ /** Acquire the host reservation; throws (fail closed) on a rejecting sink. */
11117
+ async applyReservation(run2, input) {
11118
+ const result = await this.sink.reserveRun({
11119
+ ...run2.scope,
11120
+ kind: run2.kind,
11121
+ message: input.message,
11122
+ model: input.model
11123
+ });
11124
+ run2.reservationId = result?.reservationId;
11125
+ }
11126
+ /** Remove a still-reserving run whose reservation failed, from either list. */
11127
+ removeReservingRun(state, run2, sessionId) {
11128
+ const pendingIndex = state.pendingPrompts.indexOf(run2);
11129
+ if (pendingIndex >= 0) state.pendingPrompts.splice(pendingIndex, 1);
11130
+ const queuedIndex = state.queued.indexOf(run2);
11131
+ if (queuedIndex >= 0) state.queued.splice(queuedIndex, 1);
11132
+ this.pruneSession(sessionId, state);
11133
+ }
11134
+ findRun(state, runId) {
11135
+ if (state.active && !state.active.terminal && state.active.scope.runId === runId) return state.active;
11136
+ const pending = state.pendingPrompts.find((run2) => run2.scope.runId === runId);
11137
+ if (pending) return pending;
11138
+ return state.queued.find((run2) => run2.scope.runId === runId);
11139
+ }
11140
+ finishRun(run2, status) {
11141
+ if (run2.terminal) return;
11142
+ run2.terminal = true;
11143
+ this.enqueue(
11144
+ run2,
11145
+ () => {
11146
+ if (run2.usageWriteFailed) {
11147
+ return this.sink.releaseRun({ ...run2.scope, reservationId: run2.reservationId, reason: "usage-write-failed" });
11148
+ }
11149
+ if (run2.billableUsageCount > 0) {
11150
+ return this.sink.settleRun({ ...run2.scope, reservationId: run2.reservationId, status });
11151
+ }
11152
+ const didPaidWork = status === "ok";
11153
+ if (didPaidWork) {
11154
+ return this.sink.releaseRun({ ...run2.scope, reservationId: run2.reservationId, reason: "fallback-hold-charge" });
11155
+ }
11156
+ return this.sink.releaseRun({
11157
+ ...run2.scope,
11158
+ reservationId: run2.reservationId,
11159
+ reason: status === "error" ? "error-before-usage" : "cancelled"
11160
+ });
11161
+ },
11162
+ "finishRun failed"
11163
+ );
11164
+ }
11165
+ release(run2, reason) {
11166
+ if (run2.terminal) return;
11167
+ run2.terminal = true;
11168
+ this.enqueue(
11169
+ run2,
11170
+ () => this.sink.releaseRun({ ...run2.scope, reservationId: run2.reservationId, reason }),
11171
+ "releaseRun failed"
11172
+ );
11173
+ }
11174
+ enqueue(run2, op, failureMessage) {
11175
+ const chained = run2.ops.then(op).catch((error) => {
11176
+ this.logError(`${failureMessage} (run ${run2.scope.runId})`, error);
11177
+ });
11178
+ run2.ops = chained;
11179
+ this.inflightOps.add(chained);
11180
+ void chained.finally(() => this.inflightOps.delete(chained));
11181
+ }
11182
+ sessionState(sessionId) {
11183
+ let state = this.sessions.get(sessionId);
11184
+ if (!state) {
11185
+ state = { pendingPrompts: [], queued: [], consumedFollowUpNonces: /* @__PURE__ */ new Set() };
11186
+ this.sessions.set(sessionId, state);
11187
+ }
11188
+ return state;
11189
+ }
11190
+ pruneSession(sessionId, state) {
11191
+ if (!state.active && state.pendingPrompts.length === 0 && state.queued.length === 0 && state.consumedFollowUpNonces.size === 0) {
11192
+ this.sessions.delete(sessionId);
11193
+ }
11194
+ }
11195
+ };
11196
+
10108
11197
  // src/server/pi-chat/harnessPiChatService.ts
10109
11198
  var HarnessPiChatService = class {
10110
11199
  harness;
@@ -10120,10 +11209,16 @@ var HarnessPiChatService = class {
10120
11209
  activePromptRuns = /* @__PURE__ */ new Map();
10121
11210
  syntheticPromptFailures = /* @__PURE__ */ new Map();
10122
11211
  activeSyntheticPromptErrors = /* @__PURE__ */ new Map();
11212
+ metering;
10123
11213
  constructor(options) {
10124
11214
  this.harness = options.harness;
10125
11215
  this.sessionStore = options.sessionStore;
10126
11216
  this.workdir = options.workdir;
11217
+ this.metering = options.metering ? new PiChatMeteringCoordinator(options.metering, options.meteringLogger) : void 0;
11218
+ }
11219
+ /** Test/diagnostic hook: resolves once queued metering sink calls settle. */
11220
+ async flushMetering() {
11221
+ await this.metering?.flush();
10127
11222
  }
10128
11223
  async listSessions(ctx, options) {
10129
11224
  return this.sessionStore.list(toSessionCtx(ctx), options);
@@ -10132,8 +11227,16 @@ var HarnessPiChatService = class {
10132
11227
  return this.sessionStore.create(toSessionCtx(ctx), init);
10133
11228
  }
10134
11229
  async deleteSession(ctx, sessionId) {
10135
- this.channels.get(sessionId)?.unsubscribe();
11230
+ const channel = this.channels.get(sessionId);
11231
+ if (channel) {
11232
+ const activeRun = this.activePromptRuns.get(sessionId);
11233
+ await channel.adapter.abort();
11234
+ await activeRun?.catch(() => {
11235
+ });
11236
+ }
11237
+ channel?.unsubscribe();
10136
11238
  this.channels.delete(sessionId);
11239
+ this.metering?.releaseSession(sessionId);
10137
11240
  this.messageMetadata.clearSession(sessionId);
10138
11241
  this.syntheticPromptFailures.delete(sessionId);
10139
11242
  this.activeSyntheticPromptErrors.delete(sessionId);
@@ -10183,16 +11286,35 @@ var HarnessPiChatService = class {
10183
11286
  async prompt(ctx, sessionId, payload) {
10184
11287
  const adapter = await this.getAdapter(ctx, sessionId, payload);
10185
11288
  await this.ensureChannel(ctx, sessionId, adapter);
11289
+ const outcome = await this.metering?.reservePrompt({
11290
+ workspaceId: ctx.workspaceId,
11291
+ userId: ctx.authSubject,
11292
+ sessionId,
11293
+ clientNonce: payload.clientNonce,
11294
+ message: payload.message,
11295
+ model: payload.model
11296
+ }) ?? "created";
11297
+ if (outcome === "duplicate") {
11298
+ return {
11299
+ accepted: true,
11300
+ cursor: this.channels.get(sessionId)?.buffer.latestSeq ?? 0,
11301
+ clientNonce: payload.clientNonce,
11302
+ duplicate: true
11303
+ };
11304
+ }
11305
+ if (outcome === "cancelled") throw promptCancelledError();
10186
11306
  this.messageMetadata.recordPrompt(sessionId, payload);
10187
11307
  const channel = this.channels.get(sessionId);
10188
11308
  const receiptCursor = nextPromptReceiptCursor(channel);
10189
11309
  try {
10190
11310
  const run2 = this.trackActiveRun(sessionId, adapter.prompt(toPiPromptInput(payload)));
10191
11311
  run2.catch((error) => {
11312
+ this.metering?.failPromptRun(sessionId, payload.clientNonce);
10192
11313
  if (!this.messageMetadata.hasPrompt(sessionId, { clientNonce: payload.clientNonce, displayText: payload.displayMessage ?? payload.message })) return;
10193
11314
  this.publishPromptRunError(sessionId, channel, payload, error);
10194
11315
  });
10195
11316
  } catch (err) {
11317
+ this.metering?.failPromptRun(sessionId, payload.clientNonce);
10196
11318
  this.messageMetadata.removePrompt(sessionId, { clientNonce: payload.clientNonce });
10197
11319
  throw err;
10198
11320
  }
@@ -10201,6 +11323,25 @@ var HarnessPiChatService = class {
10201
11323
  async followUp(ctx, sessionId, payload) {
10202
11324
  const adapter = await this.getAdapter(ctx, sessionId, payload.message);
10203
11325
  await this.ensureChannel(ctx, sessionId, adapter);
11326
+ const outcome = await this.metering?.reserveFollowUp({
11327
+ workspaceId: ctx.workspaceId,
11328
+ userId: ctx.authSubject,
11329
+ sessionId,
11330
+ clientNonce: payload.clientNonce,
11331
+ clientSeq: payload.clientSeq,
11332
+ message: payload.message
11333
+ }) ?? "created";
11334
+ if (outcome === "duplicate") {
11335
+ return {
11336
+ accepted: true,
11337
+ queued: true,
11338
+ cursor: this.channels.get(sessionId)?.buffer.latestSeq ?? 0,
11339
+ clientNonce: payload.clientNonce,
11340
+ clientSeq: payload.clientSeq,
11341
+ duplicate: true
11342
+ };
11343
+ }
11344
+ if (outcome === "cancelled") throw promptCancelledError();
10204
11345
  this.messageMetadata.recordFollowUp(sessionId, payload);
10205
11346
  try {
10206
11347
  await adapter.followUp(payload.message, {
@@ -10209,6 +11350,7 @@ var HarnessPiChatService = class {
10209
11350
  clientSeq: payload.clientSeq
10210
11351
  });
10211
11352
  } catch (err) {
11353
+ this.metering?.failFollowUpRun(sessionId, payload);
10212
11354
  this.messageMetadata.removeFollowUp(sessionId, payload);
10213
11355
  throw err;
10214
11356
  }
@@ -10220,10 +11362,14 @@ var HarnessPiChatService = class {
10220
11362
  const before = adapter.readSnapshot().followUpMessages.length;
10221
11363
  adapter.clearFollowUp(payload);
10222
11364
  const after = adapter.readSnapshot().followUpMessages.length;
10223
- if (after < before) this.messageMetadata.removeFollowUp(sessionId, payload);
11365
+ if (after < before) {
11366
+ this.messageMetadata.removeFollowUp(sessionId, payload);
11367
+ this.metering?.releaseQueued(sessionId, payload);
11368
+ }
10224
11369
  return { accepted: true, cursor: this.channels.get(sessionId)?.buffer.latestSeq ?? 0, cleared: Math.max(0, before - after) };
10225
11370
  }
10226
11371
  const clearedQueue = this.clearAllFollowUps(adapter, sessionId);
11372
+ this.metering?.releaseQueued(sessionId);
10227
11373
  return { accepted: true, cursor: this.channels.get(sessionId)?.buffer.latestSeq ?? 0, cleared: clearedQueue.length };
10228
11374
  }
10229
11375
  async interrupt(ctx, sessionId, _payload) {
@@ -10236,12 +11382,15 @@ var HarnessPiChatService = class {
10236
11382
  if (wasActive) await adapter.abort();
10237
11383
  await activeRun?.catch(() => {
10238
11384
  });
11385
+ this.metering?.releasePending(sessionId);
10239
11386
  if (nextFollowUp) await this.autoPostInterruptedFollowUp(sessionId, adapter, nextFollowUp);
10240
11387
  return { accepted: true, cursor: this.channels.get(sessionId)?.buffer.latestSeq ?? 0 };
10241
11388
  }
10242
11389
  async stop(ctx, sessionId, _payload) {
10243
11390
  const adapter = await this.getAdapter(ctx, sessionId, "");
10244
11391
  const clearedQueue = this.clearAllFollowUps(adapter, sessionId);
11392
+ this.metering?.releaseQueued(sessionId);
11393
+ this.metering?.releasePending(sessionId);
10245
11394
  await adapter.abort();
10246
11395
  return { accepted: true, stopped: true, cursor: this.channels.get(sessionId)?.buffer.latestSeq ?? 0, clearedQueue: buildPiChatQueuedFollowUps(sessionId, clearedQueue) };
10247
11396
  }
@@ -10263,13 +11412,24 @@ var HarnessPiChatService = class {
10263
11412
  const metadata = this.messageMetadata.findFollowUpForQueueItem(sessionId, followUp);
10264
11413
  this.messageMetadata.recordConsumingFollowUp(sessionId, followUp, metadata?.serverText);
10265
11414
  if (adapter.continueQueuedFollowUp) {
10266
- await this.trackActiveRun(sessionId, adapter.continueQueuedFollowUp());
11415
+ try {
11416
+ await this.trackActiveRun(sessionId, adapter.continueQueuedFollowUp());
11417
+ } catch (err) {
11418
+ this.metering?.failFollowUpRun(sessionId, followUp);
11419
+ throw err;
11420
+ }
10267
11421
  return;
10268
11422
  }
10269
11423
  if (!this.canClearAutoPostedFollowUpForFallback(adapter, followUp)) {
10270
11424
  throw new AutoPostFollowUpError("Cannot auto-post queued follow-up because this runtime cannot safely remove only the consumed queued item.");
10271
11425
  }
10272
- await this.runPrompt(sessionId, adapter, metadata?.serverText ?? followUp.displayText);
11426
+ this.metering?.promoteQueuedToPrompt(sessionId, followUp);
11427
+ try {
11428
+ await this.runPrompt(sessionId, adapter, metadata?.serverText ?? followUp.displayText);
11429
+ } catch (err) {
11430
+ this.metering?.failPromotedFollowUp(sessionId, followUp);
11431
+ throw err;
11432
+ }
10273
11433
  this.clearAutoPostedFollowUpForFallback(sessionId, adapter, followUp);
10274
11434
  }
10275
11435
  async runPrompt(sessionId, adapter, input) {
@@ -10414,10 +11574,14 @@ var HarnessPiChatService = class {
10414
11574
  const channel = { buffer, adapter, unsubscribe: () => {
10415
11575
  }, mapper, messageTurnIds: /* @__PURE__ */ new Map() };
10416
11576
  const unsubscribe = adapter.subscribe((event) => {
10417
- for (const mapped of mapper.map(event)) {
11577
+ const mappedEvents = mapper.map(event);
11578
+ const enrichedEvents = [];
11579
+ for (const mapped of mappedEvents) {
10418
11580
  const enriched = this.messageMetadata.enrichEvent(sessionId, mapped);
11581
+ enrichedEvents.push(enriched);
10419
11582
  this.publishChannelEvent(sessionId, channel, enriched);
10420
11583
  }
11584
+ this.metering?.observe(sessionId, event, enrichedEvents);
10421
11585
  });
10422
11586
  channel.unsubscribe = unsubscribe;
10423
11587
  this.channels.set(sessionId, channel);
@@ -10426,6 +11590,13 @@ var HarnessPiChatService = class {
10426
11590
  };
10427
11591
  var AutoPostFollowUpError = class extends Error {
10428
11592
  };
11593
+ function promptCancelledError() {
11594
+ return Object.assign(new Error("request cancelled before execution"), {
11595
+ statusCode: 409,
11596
+ code: ErrorCode.enum.ABORTED,
11597
+ retryable: true
11598
+ });
11599
+ }
10429
11600
  function nextPromptReceiptCursor(channel) {
10430
11601
  return (channel?.buffer.latestSeq ?? 0) + 1;
10431
11602
  }
@@ -10558,7 +11729,8 @@ async function createAgentApp(opts = {}) {
10558
11729
  templatePath
10559
11730
  });
10560
11731
  const pluginTools = [];
10561
- if (modeAdapter.workspaceFsCapability === "strong") {
11732
+ const externalPluginsEnabled = opts.externalPlugins !== false;
11733
+ if (externalPluginsEnabled && modeAdapter.workspaceFsCapability === "strong") {
10562
11734
  const pluginResult = await loadPlugins({ cwd: workspaceRoot });
10563
11735
  if (pluginResult.errors.length > 0) {
10564
11736
  for (const e of pluginResult.errors) {
@@ -10571,7 +11743,7 @@ async function createAgentApp(opts = {}) {
10571
11743
  }
10572
11744
  const getRuntimeProvisioning = opts.getRuntimeProvisioning ?? (() => opts.runtimeProvisioning);
10573
11745
  const runtimePi = {
10574
- ...opts.pi,
11746
+ ...withPiHarnessDefaults(opts.pi),
10575
11747
  additionalSkillPaths: [
10576
11748
  ...getRuntimeProvisioning()?.skillPaths ?? [],
10577
11749
  ...opts.pi?.additionalSkillPaths ?? []
@@ -10589,21 +11761,17 @@ async function createAgentApp(opts = {}) {
10589
11761
  ...opts.disableDefaultFileTools ? [] : buildFilesystemAgentTools(runtimeBundle),
10590
11762
  ...opts.extraTools ?? [],
10591
11763
  ...pluginTools,
10592
- createPluginDiagnosticsTool({
11764
+ ...externalPluginsEnabled ? [createPluginDiagnosticsTool({
10593
11765
  getLastReloadDiagnostics: () => lastReloadDiagnostics,
10594
11766
  getHarness: () => harnessRef,
10595
11767
  ...opts.getPluginDiagnostics ? {
10596
11768
  getPluginErrors: () => opts.getPluginDiagnostics({ workspaceId: sessionId, workspaceRoot })
10597
11769
  } : {}
10598
- })
11770
+ })] : []
10599
11771
  ];
10600
11772
  const harnessFactory = opts.harnessFactory ?? ((input) => createPiCodingAgentHarness({
10601
11773
  ...input,
10602
- pi: {
10603
- noContextFiles: true,
10604
- noSkills: true,
10605
- ...runtimePi
10606
- }
11774
+ pi: runtimePi
10607
11775
  }));
10608
11776
  const harness = await harnessFactory({
10609
11777
  tools,
@@ -10638,11 +11806,17 @@ async function createAgentApp(opts = {}) {
10638
11806
  await app.register(fsEventsRoutes, { workspace: runtimeBundle.workspace });
10639
11807
  await app.register(treeRoutes, { workspace: runtimeBundle.workspace });
10640
11808
  await app.register(searchRoutes, { fileSearch: runtimeBundle.fileSearch });
10641
- await app.register(gitRoutes, { getWorkspaceRoot: () => getRuntimeBundleStorageRoot(runtimeBundle) });
11809
+ await app.register(gitRoutes, {
11810
+ getWorkspaceRoot: () => {
11811
+ if (runtimeBundle.sandbox.provider === "remote-worker") return void 0;
11812
+ return getRuntimeBundleStorageRoot(runtimeBundle);
11813
+ }
11814
+ });
10642
11815
  const piChatService = new HarnessPiChatService({
10643
11816
  harness,
10644
11817
  sessionStore: harness.sessions,
10645
- workdir: runtimeBundle.workspace.root
11818
+ workdir: runtimeBundle.workspace.root,
11819
+ metering: opts.metering
10646
11820
  });
10647
11821
  await app.register(piChatRoutes, { service: piChatService });
10648
11822
  await app.register(systemPromptRoutes, { harness });
@@ -10739,7 +11913,7 @@ function mergeTools(options) {
10739
11913
 
10740
11914
  // src/server/tools/upload/index.ts
10741
11915
  import { readFile as readFile12 } from "fs/promises";
10742
- import { extname as extname5, join as join13 } from "path";
11916
+ import { extname as extname5, join as join14 } from "path";
10743
11917
  var DEFAULT_UPLOAD_DIR = "assets/images";
10744
11918
  var MAX_UPLOAD_BYTES2 = 10 * 1024 * 1024;
10745
11919
  function contentTypeFromExt(path4) {
@@ -10779,7 +11953,6 @@ function basenameForUpload2(filename) {
10779
11953
  }
10780
11954
  function buildUploadAgentTools(bundle) {
10781
11955
  const { workspace } = bundle;
10782
- const storageRoot = getRuntimeBundleStorageRoot(bundle);
10783
11956
  return [
10784
11957
  {
10785
11958
  name: "upload_file",
@@ -10808,7 +11981,7 @@ function buildUploadAgentTools(bundle) {
10808
11981
  const rawDir = typeof input.directory === "string" ? input.directory.trim() : "";
10809
11982
  const dir = rawDir ? rawDir.replace(/^\.\/+/, "").replace(/\/+$/, "") : DEFAULT_UPLOAD_DIR;
10810
11983
  try {
10811
- const bytes = workspace.readBinaryFile ? Buffer.from(await workspace.readBinaryFile(filePath)) : await readFile12(join13(storageRoot, filePath));
11984
+ const bytes = workspace.readBinaryFile ? Buffer.from(await workspace.readBinaryFile(filePath)) : await readFile12(join14(getRuntimeBundleStorageRoot(bundle), filePath));
10812
11985
  if (bytes.byteLength === 0 || bytes.byteLength > MAX_UPLOAD_BYTES2) {
10813
11986
  return {
10814
11987
  content: [{ type: "text", text: `file must be between 1 byte and ${MAX_UPLOAD_BYTES2} bytes` }],
@@ -10962,6 +12135,7 @@ var registerAgentRoutes = async (app, opts) => {
10962
12135
  });
10963
12136
  const requestScopedRuntime = typeof opts.getWorkspaceId === "function" || typeof opts.getWorkspaceRoot === "function" || typeof opts.getTemplatePath === "function" || typeof opts.getPi === "function" || typeof opts.getSessionNamespace === "function" || typeof opts.getSystemPromptDynamic === "function";
10964
12137
  const sessionChangesTracker = new InMemorySessionChangesTracker();
12138
+ const externalPluginsEnabled = opts.externalPlugins !== false;
10965
12139
  const runtimeBindings = /* @__PURE__ */ new Map();
10966
12140
  const MAX_RUNTIME_BINDINGS = 256;
10967
12141
  function evictRuntimeBindings() {
@@ -10971,10 +12145,13 @@ var registerAgentRoutes = async (app, opts) => {
10971
12145
  runtimeBindings.delete(keys[i]);
10972
12146
  }
10973
12147
  }
12148
+ async function resolveScopePi(workspaceId, root, request) {
12149
+ return withPiHarnessDefaults(opts.getPi ? await opts.getPi({ workspaceId, workspaceRoot: root, request }) : opts.pi);
12150
+ }
10974
12151
  async function resolveRuntimeScope(workspaceId, request) {
10975
12152
  const root = request && opts.getWorkspaceRoot ? await opts.getWorkspaceRoot(workspaceId, request) : workspaceRoot;
10976
12153
  const scopedTemplatePath = opts.getTemplatePath ? await opts.getTemplatePath({ workspaceId, workspaceRoot: root, request }) : templatePath;
10977
- const pi = opts.getPi ? await opts.getPi({ workspaceId, workspaceRoot: root, request }) : opts.pi;
12154
+ const pi = await resolveScopePi(workspaceId, root, request);
10978
12155
  const sessionNamespace = normalizeSessionNamespace(opts.getSessionNamespace ? await opts.getSessionNamespace({ workspaceId, workspaceRoot: root, request }) : opts.sessionNamespace);
10979
12156
  return {
10980
12157
  root,
@@ -10986,29 +12163,29 @@ var registerAgentRoutes = async (app, opts) => {
10986
12163
  workspaceId,
10987
12164
  root,
10988
12165
  scopedTemplatePath ?? null,
10989
- pi ?? null,
12166
+ pi,
10990
12167
  sessionNamespace ?? null
10991
12168
  ])
10992
12169
  };
10993
12170
  }
10994
12171
  async function resolveSkillScope(workspaceId, request) {
10995
12172
  const root = request && opts.getWorkspaceRoot ? await opts.getWorkspaceRoot(workspaceId, request) : workspaceRoot;
10996
- const pi = opts.getPi ? await opts.getPi({ workspaceId, workspaceRoot: root, request }) : opts.pi;
10997
- const hot = pi?.getHotReloadableResources?.();
12173
+ const pi = await resolveScopePi(workspaceId, root, request);
12174
+ const hot = pi.getHotReloadableResources?.();
10998
12175
  return {
10999
12176
  root,
11000
12177
  pi: hot ? {
11001
12178
  ...pi,
11002
12179
  additionalSkillPaths: [
11003
- ...pi?.additionalSkillPaths ?? [],
12180
+ ...pi.additionalSkillPaths ?? [],
11004
12181
  ...hot.additionalSkillPaths ?? []
11005
12182
  ],
11006
12183
  packages: [
11007
- ...pi?.packages ?? [],
12184
+ ...pi.packages ?? [],
11008
12185
  ...hot.packages ?? []
11009
12186
  ],
11010
12187
  extensionPaths: [
11011
- ...pi?.extensionPaths ?? [],
12188
+ ...pi.extensionPaths ?? [],
11012
12189
  ...hot.extensionPaths ?? []
11013
12190
  ]
11014
12191
  } : pi
@@ -11142,17 +12319,17 @@ var registerAgentRoutes = async (app, opts) => {
11142
12319
  }),
11143
12320
  ...buildFilesystemAgentTools(runtimeBundle),
11144
12321
  ...buildUploadAgentTools(runtimeBundle),
11145
- createPluginDiagnosticsTool({
12322
+ ...externalPluginsEnabled ? [createPluginDiagnosticsTool({
11146
12323
  // `binding` is assigned later in this function; read through thunks.
11147
12324
  getLastReloadDiagnostics: () => binding?.lastReloadDiagnostics ?? [],
11148
12325
  getHarness: () => binding?.harness,
11149
12326
  ...opts.getPluginDiagnostics ? {
11150
12327
  getPluginErrors: () => opts.getPluginDiagnostics({ workspaceId, workspaceRoot: root })
11151
12328
  } : {}
11152
- })
12329
+ })] : []
11153
12330
  ];
11154
12331
  const pluginTools = [];
11155
- if (modeAdapter.workspaceFsCapability === "strong") {
12332
+ if (externalPluginsEnabled && modeAdapter.workspaceFsCapability === "strong") {
11156
12333
  const pluginResult = await loadPlugins({ cwd: root });
11157
12334
  if (pluginResult.errors.length > 0) {
11158
12335
  for (const e of pluginResult.errors) {
@@ -11185,14 +12362,13 @@ var registerAgentRoutes = async (app, opts) => {
11185
12362
  const harnessFactory = opts.harnessFactory ?? ((input) => createPiCodingAgentHarness({
11186
12363
  ...input,
11187
12364
  pi: {
11188
- noContextFiles: true,
11189
- noSkills: true,
12365
+ // scope.pi is already defaulted at the resolveScopePi chokepoint.
11190
12366
  ...scope.pi,
11191
12367
  additionalSkillPaths: [
11192
- ...scope.pi?.additionalSkillPaths ?? []
12368
+ ...scope.pi.additionalSkillPaths ?? []
11193
12369
  ],
11194
12370
  getHotReloadableResources: () => {
11195
- const hot = scope.pi?.getHotReloadableResources?.() ?? {};
12371
+ const hot = scope.pi.getHotReloadableResources?.() ?? {};
11196
12372
  return {
11197
12373
  ...hot,
11198
12374
  additionalSkillPaths: [
@@ -11412,7 +12588,11 @@ var registerAgentRoutes = async (app, opts) => {
11412
12588
  getFileSearch: async (request) => (await getBindingForRequest(request)).runtimeBundle.fileSearch
11413
12589
  });
11414
12590
  await app.register(gitRoutes, {
11415
- getWorkspaceRoot: async (request) => getRuntimeBundleStorageRoot((await getBindingForRequest(request)).runtimeBundle)
12591
+ getWorkspaceRoot: async (request) => {
12592
+ const bundle = (await getBindingForRequest(request)).runtimeBundle;
12593
+ if (bundle.sandbox.provider === "remote-worker") return void 0;
12594
+ return getRuntimeBundleStorageRoot(bundle);
12595
+ }
11416
12596
  });
11417
12597
  await app.register(piChatRoutes, {
11418
12598
  getService: async (request) => {
@@ -11420,7 +12600,8 @@ var registerAgentRoutes = async (app, opts) => {
11420
12600
  binding.piChatService ??= new HarnessPiChatService({
11421
12601
  harness: binding.harness,
11422
12602
  sessionStore: binding.harness.sessions,
11423
- workdir: binding.runtimeBundle.workspace.root
12603
+ workdir: binding.runtimeBundle.workspace.root,
12604
+ metering: opts.metering
11424
12605
  });
11425
12606
  return binding.piChatService;
11426
12607
  }
@@ -11436,19 +12617,21 @@ var registerAgentRoutes = async (app, opts) => {
11436
12617
  ...opts.pi?.additionalSkillPaths ?? []
11437
12618
  ],
11438
12619
  piPackages: opts.pi?.packages,
12620
+ // Undefined is fine: skillsRoutes resolves it through the canonical
12621
+ // harness policy (withPiHarnessDefaults), same as the factory above.
11439
12622
  noSkills: opts.pi?.noSkills,
11440
12623
  getWorkspaceRoot: staticBinding ? void 0 : async (request) => (await getSkillsScopeForRequest(request)).root,
11441
12624
  getAdditionalSkillPaths: staticBinding && !hasRuntimeProvisioningInput ? void 0 : async (request) => {
11442
12625
  const scope = await getSkillsScopeForRequest(request);
11443
- if (!hasRuntimeProvisioningInput) return scope.pi?.additionalSkillPaths;
12626
+ if (!hasRuntimeProvisioningInput) return scope.pi.additionalSkillPaths;
11444
12627
  const binding = await getBindingForRequest(request);
11445
12628
  return [
11446
12629
  ...binding.runtimeProvisioning?.skillPaths ?? [],
11447
- ...scope.pi?.additionalSkillPaths ?? []
12630
+ ...scope.pi.additionalSkillPaths ?? []
11448
12631
  ];
11449
12632
  },
11450
- getPiPackages: staticBinding ? void 0 : async (request) => (await getSkillsScopeForRequest(request)).pi?.packages,
11451
- getNoSkills: staticBinding ? void 0 : async (request) => (await getSkillsScopeForRequest(request)).pi?.noSkills
12633
+ getPiPackages: staticBinding ? void 0 : async (request) => (await getSkillsScopeForRequest(request)).pi.packages,
12634
+ getNoSkills: staticBinding ? void 0 : async (request) => (await getSkillsScopeForRequest(request)).pi.noSkills
11452
12635
  });
11453
12636
  await app.register(sessionChangesRoutes, { tracker: sessionChangesTracker });
11454
12637
  app.post("/api/v1/agent/reload", async (request, reply) => {
@@ -11507,10 +12690,17 @@ var registerAgentRoutes = async (app, opts) => {
11507
12690
  export {
11508
12691
  FileHandleStore,
11509
12692
  PI_PACKAGE_RESOURCE_FILTERS,
12693
+ REMOTE_WORKER_PROVIDER,
12694
+ REMOTE_WORKER_RUNTIME_CWD,
12695
+ RemoteWorkerClient,
12696
+ RemoteWorkerClientError,
11510
12697
  UV_SETUP_COMMANDS,
11511
12698
  VERCEL_PROVISIONING_CACHE_ROOT,
11512
12699
  VERCEL_SANDBOX_WORKSPACE_ROOT,
11513
12700
  UV_SETUP_COMMANDS as VERCEL_UV_SETUP_COMMANDS,
12701
+ WORKER_INTERNAL_TOKEN_HEADER,
12702
+ WORKER_REQUEST_ID_HEADER,
12703
+ WORKER_WORKSPACE_ID_HEADER,
11514
12704
  applyCspHeaders,
11515
12705
  autoDetectMode,
11516
12706
  bakeSnapshotIfNeeded,
@@ -11518,21 +12708,28 @@ export {
11518
12708
  buildPackageHash,
11519
12709
  buildSnapshotRecipeHash,
11520
12710
  compactPiPackages,
12711
+ constantTimeTokenEqual,
11521
12712
  createAgentApp,
11522
12713
  createBwrapSandbox,
11523
12714
  createDirectSandbox,
11524
12715
  createLogger,
11525
12716
  createNodeWorkspace,
12717
+ createRemoteWorkerModeAdapter,
12718
+ createRemoteWorkerSandbox,
12719
+ createRemoteWorkerWorkspace,
11526
12720
  createResourceSettingsManager,
11527
12721
  createVercelDeploymentSnapshotProvider,
11528
12722
  createVercelProvisioningAdapter,
11529
12723
  createVercelSandboxWorkspace,
12724
+ decodeBytesFromWorker,
12725
+ encodeBytesForWorker,
11530
12726
  fileRoutes,
11531
12727
  getBoringAgentPathEntries,
11532
12728
  getBoringAgentRuntimeEnv,
11533
12729
  getBoringAgentRuntimePaths,
11534
12730
  hasBwrap,
11535
12731
  mergePiPackageSources,
12732
+ normalizeMeteringUsage,
11536
12733
  piPackageSourceKey,
11537
12734
  prepareDeploymentSnapshot,
11538
12735
  prepareVercelDeploymentSnapshot,