windows-development-sandbox 1.27.3 → 1.29.0

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 (125) hide show
  1. package/README.md +5 -4
  2. package/dist/app/features/sandbox/sandbox.run.js +1 -1
  3. package/dist/bin/{chunk-FA5VWLP4.js → chunk-O4EL32TM.js} +1 -1
  4. package/dist/bin/{chunk-RU7XRN62.js → chunk-QYRRWGFY.js} +1637 -1354
  5. package/dist/bin/sandbox.js +10772 -3
  6. package/dist/cli/commands/launch/launch.js +1 -1
  7. package/dist/cli/commands/launch/running.js +2 -2
  8. package/dist/cli/index.js +1 -1
  9. package/dist/cli/menu/pages/agents-page.js +1 -1
  10. package/dist/cli/services/sandbox-service.js +1 -1
  11. package/dist/core/schema/sandbox.js +2 -2
  12. package/dist/guest/apps/app-installer.js +1 -1
  13. package/dist/guest/apps/store-check.d.ts +2 -0
  14. package/dist/guest/apps/store-check.js +3 -1
  15. package/dist/guest/boot/guide-lines.d.ts +2 -0
  16. package/dist/guest/boot/guide-lines.js +7 -0
  17. package/dist/guest/boot/guide.js +2 -1
  18. package/dist/guest/daemon/heartbeat.js +1 -0
  19. package/dist/guest/governor/core.d.ts +53 -1
  20. package/dist/guest/governor/core.js +95 -1
  21. package/dist/guest/governor/decide.d.ts +4 -0
  22. package/dist/guest/governor/decide.js +2 -0
  23. package/dist/guest/governor/governor.js +23 -9
  24. package/dist/guest/toolchains/runtimes.js +7 -6
  25. package/dist/host/accounts/deliver.js +16 -2
  26. package/dist/host/apps/store-package.d.ts +2 -0
  27. package/dist/host/apps/store-package.js +16 -15
  28. package/dist/host/channel/attach.js +1 -1
  29. package/dist/host/channel/channel.js +3 -3
  30. package/dist/host/guard/commands.js +1 -0
  31. package/dist/host/health/path-copies.d.ts +1 -1
  32. package/dist/host/health/path-copies.js +2 -2
  33. package/dist/host/install/index.d.ts +1 -1
  34. package/dist/host/install/index.js +1 -1
  35. package/dist/host/install/shims.d.ts +8 -1
  36. package/dist/host/install/shims.js +39 -1
  37. package/dist/host/launch/generate.js +4 -0
  38. package/dist/host/launch/index.d.ts +1 -0
  39. package/dist/host/launch/index.js +1 -0
  40. package/dist/host/launch/pc-logs.d.ts +6 -0
  41. package/dist/host/launch/pc-logs.js +129 -0
  42. package/dist/host/life/crash-report.d.ts +4 -0
  43. package/dist/host/life/crash-report.js +30 -1
  44. package/dist/host/life/last-end.js +1 -0
  45. package/dist/host/sandbox/platform.js +1 -1
  46. package/dist/host/sandbox/window.js +1 -1
  47. package/dist/host/tools/windows-terminal.d.ts +4 -1
  48. package/dist/host/tools/windows-terminal.js +25 -9
  49. package/dist/runtime/agent.js +6 -2
  50. package/dist/runtime/daemon.js +111 -16
  51. package/dist/runtime/db.js +6 -2
  52. package/dist/runtime/main.js +199 -35
  53. package/dist/runtime/netproxy.js +6 -2
  54. package/dist/runtime/tool.js +6 -2
  55. package/dist/runtime/view.js +126 -28
  56. package/dist/selftest/agent-paths.test.js +3 -1
  57. package/dist/selftest/channel.test.js +33 -0
  58. package/dist/selftest/cli.test.js +5 -5
  59. package/dist/selftest/config.test.js +1 -1
  60. package/dist/selftest/credentials.test.js +1 -1
  61. package/dist/selftest/engine.test.js +1 -1
  62. package/dist/selftest/generate.test.js +66 -1
  63. package/dist/selftest/hcs.test.js +1 -1
  64. package/dist/selftest/health.test.js +21 -10
  65. package/dist/selftest/heavy-load.test.js +112 -4
  66. package/dist/selftest/host-sim.test.js +13 -14
  67. package/dist/selftest/install.test.js +6 -2
  68. package/dist/selftest/live-terminal.test.js +103 -0
  69. package/dist/selftest/personalization.test.js +2 -1
  70. package/dist/selftest/store-app.test.js +62 -4
  71. package/dist/selftest/support/engine-golden.js +2 -0
  72. package/dist/selftest/support/web-preview.js +2 -2
  73. package/dist/shared/accounts/kinds.d.ts +5 -0
  74. package/dist/shared/accounts/kinds.js +8 -0
  75. package/dist/shared/apps/index.d.ts +1 -1
  76. package/dist/shared/apps/index.js +1 -1
  77. package/dist/shared/apps/selection.d.ts +1 -0
  78. package/dist/shared/apps/selection.js +4 -3
  79. package/dist/shared/apps/store-package.d.ts +13 -0
  80. package/dist/shared/apps/store-package.js +24 -0
  81. package/dist/shared/channel/schema.d.ts +2 -0
  82. package/dist/shared/channel/schema.js +4 -0
  83. package/dist/shared/memory/policy.d.ts +12 -0
  84. package/dist/shared/memory/policy.js +4 -0
  85. package/dist/shared/pty/clipboard-watch.d.ts +4 -0
  86. package/dist/shared/pty/clipboard-watch.js +40 -0
  87. package/dist/shared/pty/ctrl-c.d.ts +13 -7
  88. package/dist/shared/pty/ctrl-c.js +35 -19
  89. package/dist/shared/pty/index.d.ts +1 -0
  90. package/dist/shared/pty/index.js +1 -0
  91. package/dist/shared/pty/terminal.d.ts +1 -1
  92. package/dist/shared/pty/terminal.js +16 -13
  93. package/dist/shared/web/ctrl-c.d.ts +26 -0
  94. package/dist/shared/web/ctrl-c.js +64 -0
  95. package/dist/shared/web/index.d.ts +1 -0
  96. package/dist/shared/web/index.js +1 -0
  97. package/dist/web/index.html +1 -1
  98. package/dist/web/{main-NKM7ECSB.js → main-7NY4X6YU.js} +67 -67
  99. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  100. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  101. package/native/csharp/HostTools/Window.cs +1 -1
  102. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  103. package/native/scripts/get-store-package.ps1 +24 -13
  104. package/package.json +4 -14
  105. package/sandbox/csharp/ClipboardWatch.cs +59 -0
  106. package/sandbox/csharp/MemGov.cs +752 -7
  107. package/sandbox/scripts/install-store-package.ps1 +11 -4
  108. package/sandbox/scripts/watch-clipboard.ps1 +8 -0
  109. package/schemas/config.schema.json +2 -2
  110. package/schemas/sandbox-config.schema.json +1 -1
  111. package/dist/bin/agent-sandbox.d.ts +0 -2
  112. package/dist/bin/agent-sandbox.js +0 -9
  113. package/dist/bin/chunk-4M5SZZ5P.js +0 -10778
  114. package/dist/bin/claude-host.d.ts +0 -2
  115. package/dist/bin/claude-host.js +0 -11
  116. package/dist/bin/claude-sbx.d.ts +0 -2
  117. package/dist/bin/claude-sbx.js +0 -9
  118. package/dist/bin/codex-sbx.d.ts +0 -2
  119. package/dist/bin/codex-sbx.js +0 -9
  120. package/dist/bin/gemini-sbx.d.ts +0 -2
  121. package/dist/bin/gemini-sbx.js +0 -9
  122. package/dist/bin/sbx-hide.d.ts +0 -2
  123. package/dist/bin/sbx-hide.js +0 -9
  124. package/dist/bin/sbx-show.d.ts +0 -2
  125. package/dist/bin/sbx-show.js +0 -9
@@ -5743,7 +5743,7 @@ var OutputFilter = class {
5743
5743
  }
5744
5744
  };
5745
5745
 
5746
- // src/shared/pty/console-input.ts
5746
+ // src/shared/pty/clipboard-watch.ts
5747
5747
  import { spawn } from "node:child_process";
5748
5748
 
5749
5749
  // src/shared/system.ts
@@ -5796,7 +5796,36 @@ function scriptArgs(name, args = []) {
5796
5796
  ];
5797
5797
  }
5798
5798
 
5799
+ // src/shared/pty/clipboard-watch.ts
5800
+ var MAX_PENDING = 1024;
5801
+ function watchClipboard(onChange) {
5802
+ if (process.platform !== "win32") return { stop: () => void 0 };
5803
+ let child = null;
5804
+ try {
5805
+ child = spawn(SYSTEM.powershell(), scriptArgs("watch-clipboard.ps1", ["-ProcessId", String(process.pid)]), {
5806
+ stdio: ["ignore", "pipe", "ignore"],
5807
+ windowsHide: true
5808
+ });
5809
+ } catch {
5810
+ return { stop: () => void 0 };
5811
+ }
5812
+ let pending = "";
5813
+ child.stdout?.setEncoding("latin1");
5814
+ child.stdout?.on("data", (text) => {
5815
+ const lines = (pending + text).split("\n");
5816
+ pending = (lines.pop() ?? "").slice(-MAX_PENDING);
5817
+ for (const line of lines) if (line.trim() === "changed") onChange();
5818
+ });
5819
+ child.on("error", () => void 0);
5820
+ return {
5821
+ stop: () => {
5822
+ if (child && child.exitCode === null && child.signalCode === null) child.kill();
5823
+ }
5824
+ };
5825
+ }
5826
+
5799
5827
  // src/shared/pty/console-input.ts
5828
+ import { spawn as spawn2 } from "node:child_process";
5800
5829
  function enableVtInput() {
5801
5830
  if (process.platform !== "win32" || !process.stdin.isTTY)
5802
5831
  return { ready: Promise.resolve(false), stop: () => Promise.resolve() };
@@ -5804,7 +5833,7 @@ function enableVtInput() {
5804
5833
  let exited = Promise.resolve();
5805
5834
  const ready = new Promise((resolve) => {
5806
5835
  try {
5807
- child = spawn(SYSTEM.powershell(), scriptArgs("enable-vt-input.ps1", ["-ProcessId", String(process.pid)]), {
5836
+ child = spawn2(SYSTEM.powershell(), scriptArgs("enable-vt-input.ps1", ["-ProcessId", String(process.pid)]), {
5808
5837
  stdio: "ignore",
5809
5838
  windowsHide: true
5810
5839
  });
@@ -5863,30 +5892,94 @@ function unfinishedSequence(data) {
5863
5892
  return data.length;
5864
5893
  }
5865
5894
 
5866
- // src/shared/pty/ctrl-c.ts
5895
+ // src/shared/web/protocol.ts
5896
+ var MAX_CLIENT_MESSAGE_BYTES = 256 * 1024;
5897
+
5898
+ // src/shared/web/ctrl-c.ts
5867
5899
  var CTRL_C_CONFIRM_MS = 1500;
5868
5900
  var CTRL_C_HINT = "Ctrl+C again to send it to the program (Esc stops an agent)";
5869
- var OSC_TITLE = /\x1b\][02];([^\x07\x1b]{0,256})(?:\x07|\x1b\\)/g;
5870
- var CtrlCGuard = class {
5901
+ var CTRL_C_COPY_HINT = "Copied: Ctrl+C after selecting text is not sent to the program (Esc stops an agent)";
5902
+ var CtrlCDecider = class {
5903
+ constructor(confirm = true) {
5904
+ this.confirm = confirm;
5905
+ }
5906
+ confirm;
5871
5907
  armedAt = Number.NEGATIVE_INFINITY;
5872
- lastTitle = "";
5873
- /** What to send for one read from the keyboard: a lone Ctrl+C only when another came just before. */
5874
- push(chunk, nowMs) {
5875
- if (chunk.length !== 1 || chunk[0] !== 3) return { send: chunk, held: false };
5908
+ selected = false;
5909
+ /** Text was selected in the program: Ctrl+C is "copy" from now on. */
5910
+ selection() {
5911
+ this.selected = true;
5912
+ this.armedAt = Number.NEGATIVE_INFINITY;
5913
+ }
5914
+ /** Any other key or a click: Ctrl+C is an interrupt again. */
5915
+ otherInput() {
5916
+ this.selected = false;
5917
+ }
5918
+ /** What to do with a lone Ctrl+C pressed at `nowMs`. */
5919
+ ctrlC(nowMs) {
5920
+ if (this.selected) return "copy";
5921
+ if (!this.confirm) return "send";
5876
5922
  if (nowMs - this.armedAt <= CTRL_C_CONFIRM_MS) {
5877
5923
  this.armedAt = Number.NEGATIVE_INFINITY;
5878
- return { send: chunk, held: false };
5924
+ return "send";
5879
5925
  }
5880
5926
  this.armedAt = nowMs;
5881
- return { send: Buffer.alloc(0), held: true };
5927
+ return "hold";
5928
+ }
5929
+ };
5930
+ var MOUSE_REPORT = /\x1b\[<(\d{1,4});\d{1,5};\d{1,5}([Mm])|\x1b\[M([\s\S])[\s\S]{2}/g;
5931
+ function classifyInput(text) {
5932
+ let drag = false;
5933
+ let click = false;
5934
+ for (const m of text.matchAll(MOUSE_REPORT)) {
5935
+ const sgr = m[1] !== void 0;
5936
+ const b = sgr ? Number(m[1]) : (m[3] ?? " ").charCodeAt(0) - 32;
5937
+ if (b & 64) continue;
5938
+ if (b & 32) {
5939
+ if ((b & 3) !== 3) drag = true;
5940
+ } else if (sgr ? m[2] === "M" : (b & 3) !== 3) click = true;
5941
+ }
5942
+ return { mouse: drag ? "drag" : click ? "click" : null, keys: text.replace(MOUSE_REPORT, "").length > 0 };
5943
+ }
5944
+ function hasClipboardWrite(text) {
5945
+ return /\x1b\]0{0,2}52;/.test(text) || /\u009d0{0,2}52;/.test(text);
5946
+ }
5947
+
5948
+ // src/shared/pty/ctrl-c.ts
5949
+ var OSC_TITLE = /\x1b\][02];([^\x07\x1b]{0,256})(?:\x07|\x1b\\)/g;
5950
+ var CtrlCGuard = class {
5951
+ decider;
5952
+ lastTitle = "";
5953
+ constructor(options = {}) {
5954
+ this.decider = new CtrlCDecider(options.confirm !== false);
5955
+ }
5956
+ /** What to send for one read from the keyboard: a lone Ctrl+C only when it is not held back or taken as "copy". */
5957
+ push(chunk, nowMs) {
5958
+ if (chunk.length === 1 && chunk[0] === 3) {
5959
+ const action = this.decider.ctrlC(nowMs);
5960
+ if (action === "send") return { send: chunk, held: false, copied: false };
5961
+ return { send: Buffer.alloc(0), held: action === "hold", copied: action === "copy" };
5962
+ }
5963
+ const c = classifyInput(chunk.toString("latin1"));
5964
+ if (c.keys || c.mouse === "click") this.decider.otherInput();
5965
+ else if (c.mouse === "drag") this.decider.selection();
5966
+ return { send: chunk, held: false, copied: false };
5967
+ }
5968
+ /** The clipboard changed since the last key: you just copied. */
5969
+ clipboardChanged() {
5970
+ this.decider.selection();
5971
+ }
5972
+ /** The program wrote to the clipboard: that is a selection too. */
5973
+ watchClipboard(raw) {
5974
+ if (hasClipboardWrite(raw)) this.decider.selection();
5882
5975
  }
5883
5976
  /** Remembers the title the program last set (from its output), to put it back after the hint. */
5884
5977
  watchOutput(text) {
5885
5978
  for (const m of text.matchAll(OSC_TITLE)) this.lastTitle = m[1];
5886
5979
  }
5887
5980
  /** The escape sequence that shows the hint, and the one that puts the program's title back. */
5888
- hintTitle() {
5889
- return "\x1B]0;" + CTRL_C_HINT + "\x07";
5981
+ hintTitle(kind = "hold") {
5982
+ return "\x1B]0;" + (kind === "copy" ? CTRL_C_COPY_HINT : CTRL_C_HINT) + "\x07";
5890
5983
  }
5891
5984
  restoreTitle() {
5892
5985
  return "\x1B]0;" + this.lastTitle.replace(/[\x00-\x1f\x7f]/g, "") + "\x07";
@@ -6090,6 +6183,7 @@ function runTerminalClient(socket, token, options = {}) {
6090
6183
  clearTimeout(joinTimer);
6091
6184
  clearTimeout(hintTimer);
6092
6185
  clearTimeout(vtTimer);
6186
+ clipboard.stop();
6093
6187
  socket.destroy();
6094
6188
  const tracked = mouse.tracking();
6095
6189
  const reset = "\x1B[0m\x1B[?25h" + MOUSE_OFF + (mouse.has(2004) ? "\x1B[?2004l" : "") + "\r\n";
@@ -6107,23 +6201,22 @@ function runTerminalClient(socket, token, options = {}) {
6107
6201
  if (data.length && !done) socket.write(encodeFrame(FrameType.Input, data));
6108
6202
  };
6109
6203
  const sendInput = (data) => write(keys.push(passthrough ? mouse.filter(data) : data));
6110
- const guard = options.confirmCtrlC ? new CtrlCGuard() : null;
6204
+ const guard = new CtrlCGuard({ confirm: options.confirmCtrlC !== false });
6111
6205
  let hintTimer;
6206
+ const clipboard = watchClipboard(() => guard.clipboardChanged());
6112
6207
  const onInput = (chunk) => {
6113
6208
  if (options.detachKey !== void 0 && chunk.includes(options.detachKey)) {
6114
6209
  finish({ reason: "detach" });
6115
6210
  return;
6116
6211
  }
6117
- if (guard) {
6118
- const decided = guard.push(chunk, Date.now());
6119
- clearTimeout(hintTimer);
6120
- if (decided.held) {
6121
- stdout.write(guard.hintTitle());
6122
- hintTimer = setTimeout(() => stdout.write(guard.restoreTitle()), CTRL_C_CONFIRM_MS);
6123
- return;
6124
- }
6125
- if (decided.send.length === 1 && decided.send[0] === 3) stdout.write(guard.restoreTitle());
6212
+ const decided = guard.push(chunk, Date.now());
6213
+ clearTimeout(hintTimer);
6214
+ if (decided.held || decided.copied) {
6215
+ stdout.write(guard.hintTitle(decided.copied ? "copy" : "hold"));
6216
+ hintTimer = setTimeout(() => stdout.write(guard.restoreTitle()), CTRL_C_CONFIRM_MS);
6217
+ return;
6126
6218
  }
6219
+ if (decided.send.length === 1 && decided.send[0] === 3) stdout.write(guard.restoreTitle());
6127
6220
  clearTimeout(joinTimer);
6128
6221
  sendInput(joiner.push(chunk));
6129
6222
  if (joiner.holding()) joinTimer = setTimeout(() => sendInput(joiner.flush()), JOIN_MS);
@@ -6156,9 +6249,10 @@ function runTerminalClient(socket, token, options = {}) {
6156
6249
  }
6157
6250
  for (const frame of frames) {
6158
6251
  if (frame.type === FrameType.Output) {
6252
+ guard.watchClipboard(frame.payload.toString("latin1"));
6159
6253
  const out = filter.push(frame.payload);
6160
6254
  mouse.watch(out);
6161
- guard?.watchOutput(out.toString("latin1"));
6255
+ guard.watchOutput(out.toString("latin1"));
6162
6256
  stdout.write(out);
6163
6257
  } else if (frame.type === FrameType.Console) passthrough = frame.payload.toString("latin1") === "conpty.dll";
6164
6258
  else if (frame.type === FrameType.Restarting) {
@@ -26106,7 +26200,7 @@ var SandboxSchema = external_exports.strictObject({
26106
26200
  'The last resort that keeps the sandbox up, in MB below its commit limit (RAM plus paging file): builds are slowed down long before (sandbox.pressureResponse); only if everything the agents start together would take the sandbox to the very edge of its memory, where Windows inside it would start failing, is a build refused memory instead of Windows. "auto" = the larger of 1536 MB and 3 % of the commit limit; "none" = no backstop. Yours only: a project file cannot change it.'
26107
26201
  ),
26108
26202
  pressureResponse: external_exports.boolean().describe(
26109
- "When the sandbox (or this PC) runs low on memory, slow builds down in steps, like a PC that gets slower instead of failing: build programs get lower CPU and memory priority, then less CPU, then compilers take turns (the waiting ones briefly suspended and trimmed to the paging file). Nothing is stopped or killed, and the agents themselves are never touched. Yours only: a project file cannot change it."
26203
+ "When the sandbox (or this PC) runs low on memory, slow builds down in steps, like a PC that gets slower instead of failing: build programs get lower CPU and memory priority, then less CPU, then compilers take turns (the waiting ones briefly suspended and trimmed to the paging file). Nothing is stopped or killed, and the agents themselves are never touched. A memory gate also holds new compilers and linkers for a moment when the memory of the build programs grows faster than 4 GB in 3 seconds (3 GB with vGPU), and lets them go one at a time. Yours only: a project file cannot change it."
26110
26204
  ),
26111
26205
  vGpu: external_exports.boolean().describe(
26112
26206
  "Virtual GPU. Off by default: Microsoft notes it widens the sandbox attack surface. The sandbox then uses this PC's graphics card, and with some graphics drivers heavy use can freeze the whole PC: turn it on only when a program in the sandbox needs the GPU (the mesa toolchain gives software OpenGL without it). Browsers in the sandbox never use it (they draw in software)."
@@ -26120,7 +26214,7 @@ var SandboxSchema = external_exports.strictObject({
26120
26214
  printerRedirection: external_exports.boolean().describe("Printers from this PC."),
26121
26215
  protectedClient: external_exports.boolean().describe("Run the sandbox window in Protected Client mode (recommended)."),
26122
26216
  headless: external_exports.boolean().describe(
26123
- "Hide the sandbox window (sbx-show brings it back). A headless sandbox shuts down when its last session ends."
26217
+ 'Hide the sandbox window ("Show the window" brings it back). A headless sandbox shuts down when its last session ends.'
26124
26218
  ),
26125
26219
  keepAwake: external_exports.boolean().describe(
26126
26220
  "Keep this PC from sleeping while a sandbox runs, so agents left working overnight keep going (the screen may still turn off; closing a laptop lid still sleeps it)."
@@ -28304,6 +28398,8 @@ var PressureStatusSchema = external_exports.object({
28304
28398
  lowered: external_exports.number().int().min(0).max(1024),
28305
28399
  /** Compilers waiting their turn right now (name, process id, seconds until they may go on). */
28306
28400
  paused: external_exports.array(external_exports.object({ name: Text(64), pid: external_exports.number().int(), seconds: external_exports.number().int().min(0).max(60) })).max(8),
28401
+ /** Build programs the memory gate holds right now (let go one at a time). */
28402
+ held: external_exports.number().int().min(0).max(1024).optional(),
28307
28403
  /** The CPU share the compilers get together (percent; 100 = no cap). */
28308
28404
  cpuPercent: external_exports.number().int().min(0).max(100).optional(),
28309
28405
  /** The PC itself asked for slowing down (its memory or CPU is low). */
@@ -28324,6 +28420,8 @@ var HeartbeatSchema = external_exports.object({
28324
28420
  topProcesses: external_exports.array(external_exports.object({ name: Text(64), privateMb: external_exports.number().int().min(0).max(1e7) })).max(5),
28325
28421
  /** The disk queue this PC last reported while asking the sandbox to slow down. */
28326
28422
  diskQueueReported: external_exports.number().min(0).max(1e5).optional(),
28423
+ /** Build programs the memory gate holds right now (let go one at a time). */
28424
+ held: external_exports.number().int().min(0).max(1024).optional(),
28327
28425
  /** "ending": the sandbox is shutting down in order (idle shutdown, sandbox stop, Windows shutting down). */
28328
28426
  phase: external_exports.enum(["ready", "working", "ending"])
28329
28427
  });
@@ -28652,7 +28750,7 @@ import crypto from "node:crypto";
28652
28750
 
28653
28751
  // src/shared/download/spawn.ts
28654
28752
  import os3 from "node:os";
28655
- import { spawn as spawn2 } from "node:child_process";
28753
+ import { spawn as spawn3 } from "node:child_process";
28656
28754
  function lowerPriority(pid) {
28657
28755
  if (pid === void 0) return;
28658
28756
  try {
@@ -28663,7 +28761,7 @@ function lowerPriority(pid) {
28663
28761
  function spawnAsync(file2, args, options = {}) {
28664
28762
  const { lowPriority, ...spawnOptions } = options;
28665
28763
  return new Promise((resolve, reject) => {
28666
- const child = spawn2(file2, args, { stdio: "ignore", windowsHide: true, ...spawnOptions });
28764
+ const child = spawn3(file2, args, { stdio: "ignore", windowsHide: true, ...spawnOptions });
28667
28765
  if (lowPriority) lowerPriority(child.pid);
28668
28766
  child.once("error", reject);
28669
28767
  child.once("close", (code) => resolve(code ?? -1));
@@ -7,7 +7,7 @@ import { isWin } from '../core/util/index.js';
7
7
  import { SYSTEM } from '../shared/system.js';
8
8
  import { agentCommand, agentShims, bootResetPaths, isCopyableDatabaseFile, isDatabaseFile, localStateEntries, shimText, } from '../guest/agents/agent-files.js';
9
9
  import { appendMissing, expandVars, installedToolPaths, isGitUsrBin, isMsvcToolsDir, isPinnedNodeDir, msvcBeforeGitUsr, parseRegPath, pathKey, planPath, rebuildPath, sessionPath, splitPath, } from '../guest/setup/path-plan.js';
10
- import { withReloadEnvironment } from '../host/tools/index.js';
10
+ import { withCopyOnSelect, withReloadEnvironment } from '../host/tools/index.js';
11
11
  import { HarnessSchema } from '../core/schema/index.js';
12
12
  export const agentPathsSuite = {
13
13
  name: 'agent-paths',
@@ -136,6 +136,8 @@ export const agentPathsSuite = {
136
136
  t.section('Windows Terminal reloads the environment');
137
137
  t.equal(withReloadEnvironment({ compatibility: { reloadEnvironmentVariables: true } }), null, 'already set: unchanged');
138
138
  t.equal(withReloadEnvironment({ theme: 'dark', compatibility: { other: 1 } })?.compatibility, { other: 1, reloadEnvironmentVariables: true }, 'set without touching the rest of compatibility');
139
+ t.equal(withCopyOnSelect({ theme: 'dark' })?.copyOnSelect, true, 'copy on select is added when the key is missing');
140
+ t.equal(withCopyOnSelect({ copyOnSelect: false }), null, 'a copy-on-select value the user set stays');
139
141
  if (!isWin)
140
142
  return;
141
143
  t.section('shell start-up (pathsync.exe and the start-up files)');
@@ -4,6 +4,7 @@ import { safeId } from '../shared/channel/index.js';
4
4
  import { GuestRequestSchema, SandboxStatusSchema, SessionStatusSchema } from '../shared/channel/index.js';
5
5
  import { bootLine, firstStartNotice, pendingDownloads, readyIn, stillWorking } from '../cli/commands/launch/index.js';
6
6
  import { CTRL_C_CONFIRM_MS, CTRL_C_HINT, CtrlCGuard, encodeFrame, FrameReader, FrameType, fixWindowsKeys, helloFrame, InputJoiner, KeyGate, MAX_FRAME_BYTES, MOUSE_OFF, MouseModes, OutputFilter, } from '../shared/pty/index.js';
7
+ import { classifyInput } from '../shared/web/index.js';
7
8
  import { bootSilent, FIRST_STATUS_MS } from '../host/channel/index.js';
8
9
  export const channelSuite = {
9
10
  name: 'channel',
@@ -251,6 +252,38 @@ export const channelSuite = {
251
252
  !g.push(Buffer.from('x'), 6000).held, 'a lone Ctrl+C is held back, a second one soon after is sent, a late second one is held again, other input passes');
252
253
  g.watchOutput('\x1b]0;claude: working\x07more \x1b]2;\u0001evil\x07');
253
254
  t.check(g.hintTitle().includes(CTRL_C_HINT) && g.restoreTitle() === '\x1b]0;evil\x07', "the hint shows in the title, and the program's last title comes back without control characters");
255
+ const drag = Buffer.from('\x1b[<32;10;5M');
256
+ const release = Buffer.from('\x1b[<0;12;5m');
257
+ const copyG = new CtrlCGuard();
258
+ copyG.push(drag, 0);
259
+ copyG.push(release, 10);
260
+ const copies = [100, 200, 300].map((at) => copyG.push(ctrlC, at));
261
+ t.check(copies.every((c) => c.send.length === 0 && c.copied && !c.held), 'after a mouse drag in the program every Ctrl+C is copied and never sent, however often it is pressed');
262
+ copyG.push(Buffer.from('x'), 400);
263
+ t.check(copyG.push(ctrlC, 500).held, 'after another key a lone Ctrl+C is held back again');
264
+ const clickG = new CtrlCGuard();
265
+ clickG.push(drag, 0);
266
+ clickG.push(Buffer.from('\x1b[<0;3;3M'), 10);
267
+ t.check(clickG.push(ctrlC, 20).held, 'a mouse click after a drag ends copy mode');
268
+ const quietG = new CtrlCGuard();
269
+ quietG.push(Buffer.from('\x1b[<64;3;3M'), 0);
270
+ quietG.push(Buffer.from('\x1b[<35;3;3M'), 1);
271
+ t.check(quietG.push(ctrlC, 2).held, 'mouse wheel and hover reports do not start copy mode');
272
+ const clipG = new CtrlCGuard();
273
+ clipG.watchClipboard('\x1b]52;c;aGk=');
274
+ t.check(clipG.push(ctrlC, 0).copied, 'the program writing the clipboard (OSC 52) starts copy mode');
275
+ const copiedG = new CtrlCGuard();
276
+ copiedG.clipboardChanged();
277
+ const afterCopy = copiedG.push(ctrlC, 0);
278
+ copiedG.push(Buffer.from('x'), 1);
279
+ t.check(afterCopy.copied && !afterCopy.held && afterCopy.send.length === 0 && copiedG.push(ctrlC, 2).held, 'after the clipboard changed a lone Ctrl+C is copied (not sent, not held) until another key is pressed');
280
+ const noConfirm = new CtrlCGuard({ confirm: false });
281
+ const sentAtOnce = noConfirm.push(ctrlC, 0);
282
+ noConfirm.push(drag, 1);
283
+ t.check(sentAtOnce.send.equals(ctrlC) && noConfirm.push(ctrlC, 2).copied, 'without confirmation a lone Ctrl+C is sent at once, but after a drag it is still copied');
284
+ const x10 = classifyInput('\x1b[M' + String.fromCharCode(32 + 32) + '!!');
285
+ const arrow = classifyInput('\x1b[A');
286
+ t.check(x10.mouse === 'drag' && !x10.keys && arrow.mouse === null && arrow.keys, 'an X10 drag is a drag without keys, and an arrow key is a key without a mouse report');
254
287
  t.section('typed input keeps escape sequences whole');
255
288
  const joined = (...chunks) => {
256
289
  const j = new InputJoiner();
@@ -47,17 +47,17 @@ export const cliSuite = {
47
47
  ['postgres', 'redis', 'mongodb'],
48
48
  ['--continue'],
49
49
  ], '`start` is launch without an agent; positionals, repeated lists and arguments after -- parse');
50
- t.section('parser: agent wrappers (claude-sbx ...) and agent-sandbox');
50
+ t.section('parser: agent mode (sandbox claude ...) and plain commands');
51
51
  const wrapped = parseCli(['--terminal', '--continue', '--headless', '--node', '20', '--', '--terminal'], true);
52
- t.equal([wrapped.command, wrapped.terminal, wrapped.headless, wrapped.node, wrapped.passthrough], ['launch', true, true, '20', ['--continue', '--terminal']], 'claude-sbx: sandbox flags are consumed, everything else goes to the agent');
53
- await t.throws(() => parseCli(['status', '--bogus']), 'unknown option', 'agent-sandbox: unknown options are errors');
52
+ t.equal([wrapped.command, wrapped.terminal, wrapped.headless, wrapped.node, wrapped.passthrough], ['launch', true, true, '20', ['--continue', '--terminal']], 'agent mode: sandbox flags are consumed, everything else goes to the agent');
53
+ await t.throws(() => parseCli(['status', '--bogus']), 'unknown option', 'plain commands: unknown options are errors');
54
54
  const agentFlags = parseCli(['--model', 'opus', '-p', 'fix it', '--json', '-f', 'x', '--timeout', '5', '--resume', 'abc'], true);
55
55
  t.check(agentFlags.passthrough.join('|') === '--model|opus|-p|fix it|--json|-f|x|--timeout|5|--resume|abc' &&
56
56
  !agentFlags.json &&
57
57
  !agentFlags.follow &&
58
- agentFlags.timeout === 600, 'claude-sbx: any flag that is not a launch option goes to the agent in order and sets no agent-sandbox option');
58
+ agentFlags.timeout === 600, 'agent mode: any flag that is not a launch option goes to the agent in order and sets no sandbox option');
59
59
  const reserved = parseCli(['--sbx-terminal', '--terminal', '--sbx-node=18'], true);
60
- t.equal([reserved.terminal, reserved.node, reserved.passthrough], [true, '18', []], '--sbx-<option> always means agent-sandbox');
60
+ t.equal([reserved.terminal, reserved.node, reserved.passthrough], [true, '18', []], '--sbx-<option> always means the sandbox option');
61
61
  await t.throws(() => parseCli(['--sbx-nope'], true), 'Unknown option: --sbx-nope', 'an unknown --sbx-<option> is an error');
62
62
  t.section('menu helpers');
63
63
  t.check(splitArgs('--model "big one" -c \'x y\'').join('|') === '--model|big one|-c|x y' &&
@@ -75,7 +75,7 @@ export const configSuite = {
75
75
  });
76
76
  const sharedFile = t.at('share', 'theirs.json');
77
77
  fs.writeFileSync(sharedFile, shared);
78
- const bin = (args) => f.runBin('agent-sandbox', [...args, '--config', yours], { cwd: f.workA, env });
78
+ const bin = (args) => f.runBin('sandbox', [...args, '--config', yours], { cwd: f.workA, env });
79
79
  const outFile = t.at('share', 'out.json');
80
80
  const exportRun = await bin(['config', 'export', outFile]);
81
81
  const written = readJsonc(outFile);
@@ -191,7 +191,7 @@ export const credentialsSuite = {
191
191
  ['vpn', 'status', '--json'],
192
192
  ['status', '--json'],
193
193
  ]) {
194
- const out = await f.runBin('agent-sandbox', [...args, '--config', cliConfig], { cwd: f.workA, env });
194
+ const out = await f.runBin('sandbox', [...args, '--config', cliConfig], { cwd: f.workA, env });
195
195
  t.check(!out.out.includes(API_KEY) && (args[0] !== 'config' || out.out.includes('***')), 'sandbox ' + args.join(' ') + ' never prints the planted key' + (args[0] === 'config' ? ' (it shows ***)' : ''), out.out.slice(0, 400));
196
196
  }
197
197
  },
@@ -117,7 +117,7 @@ export const engineSuite = {
117
117
  !LocalOverridesSchema.safeParse({ storage: { containedMaxGb: 8 } }).success, "a project's .sandbox-config.json cannot set sandbox.engine, storage.contained or storage.containedMaxGb");
118
118
  t.check(LocalOverridesSchema.safeParse({ sandbox: { vGpu: false } }).success, 'other sandbox settings are still allowed in a project file');
119
119
  t.section('sandbox engine');
120
- const run = await f.runBin('agent-sandbox', ['engine', '--json', '--config', t.writeJson(t.at('engine-config.json'), {})], {
120
+ const run = await f.runBin('sandbox', ['engine', '--json', '--config', t.writeJson(t.at('engine-config.json'), {})], {
121
121
  cwd: f.workA,
122
122
  env: { ...process.env, NO_COLOR: '1', AGENT_SANDBOX_HOME: t.dir('engine-home'), AGENT_SANDBOX_HOST_ONLY: '1' },
123
123
  });
@@ -3,7 +3,8 @@
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { LocalOverridesSchema } from '../core/schema/index.js';
6
- import { buildWsb, gitIdentity, logonEnv, resolveMemoryMb } from '../host/launch/index.js';
6
+ import { buildWsb, gitIdentity, isSandboxSourceRepo, launchHasSandboxSource, logonEnv, resolveMemoryMb, stagePcLogs, } from '../host/launch/index.js';
7
+ import { pcLogsGuideLine } from '../guest/boot/guide-lines.js';
7
8
  export const generateSuite = {
8
9
  name: 'generate',
9
10
  title: 'generated .wsb, launch folder and session.json',
@@ -90,5 +91,69 @@ export const generateSuite = {
90
91
  resolveMemoryMb({ ...auto, maxMemoryPercent: 50 }, 60000 * MB, pcRam),
91
92
  ], [31744, 21504, 32768], "memory 'auto' follows free RAM minus the reserve and never takes more than maxMemoryPercent of the PC's RAM");
92
93
  await t.throws(() => resolveMemoryMb(auto, 9000 * MB, pcRam), 'Not enough free RAM', "memory 'auto' refuses when too little RAM is free");
94
+ t.section('PC logs for developing the sandbox');
95
+ const root = path.join(t.temp, 'pc-logs');
96
+ const URL = 'git+https://github.com/JoshDiDuca/windows-development-sandbox.git';
97
+ const project = (name, pkg) => {
98
+ t.write(path.join(root, name, 'package.json'), pkg);
99
+ return path.join(root, name);
100
+ };
101
+ const own = project('own', JSON.stringify({ name: 'windows-development-sandbox', repository: { type: 'git', url: URL } }));
102
+ const pkgOf = (url, name = 'windows-development-sandbox') => JSON.stringify({ name, repository: { url } });
103
+ const variants = [
104
+ project('v-https', pkgOf('https://github.com/joshdiduca/windows-development-sandbox')),
105
+ project('v-dotgit', pkgOf('https://github.com/JoshDiDuca/windows-development-sandbox.git')),
106
+ project('v-string', JSON.stringify({ name: 'windows-development-sandbox', repository: URL })),
107
+ ];
108
+ t.equal([isSandboxSourceRepo(own, own), ...variants.map((v) => isSandboxSourceRepo(v, own))], [true, true, true, true], 'a folder with the sandbox name and repository url is its source (git+, .git and case variants too)');
109
+ const big = project('big', pkgOf(URL) + ' '.repeat(300 * 1024));
110
+ const link = path.join(root, 'linked');
111
+ let linked = false;
112
+ try {
113
+ fs.symlinkSync(path.join(root, 'own'), link, 'junction');
114
+ linked = true;
115
+ }
116
+ catch {
117
+ // No permission for links here: that case is skipped.
118
+ }
119
+ t.equal([
120
+ isSandboxSourceRepo(project('wrong-url', pkgOf('https://github.com/someone/windows-development-sandbox')), own),
121
+ isSandboxSourceRepo(project('wrong-name', pkgOf(URL, 'other')), own),
122
+ isSandboxSourceRepo(project('invalid', '{ nope'), own),
123
+ isSandboxSourceRepo(path.join(root, 'missing'), own),
124
+ isSandboxSourceRepo(big, own),
125
+ linked ? isSandboxSourceRepo(link, own) : false,
126
+ ], [false, false, false, false, false, false], 'a wrong url or name, a missing, oversized or invalid package.json and a link are not the sandbox source');
127
+ t.check(launchHasSandboxSource([f.workA, own], own) && !launchHasSandboxSource([f.workA, f.workB], own), 'any project of the launch can be the source; a launch without it has none');
128
+ t.check(!fs.existsSync(path.join(gen.launchPath, 'pc-logs')), 'a launch of another project gets no pc-logs');
129
+ const storage = path.join(root, 'storage');
130
+ const dataPath = path.join(root, 'data-x');
131
+ for (let i = 0; i < 12; i++)
132
+ t.write(path.join(storage, 'logs', 'crashes', 'crash-2026-01-' + String(10 + i) + '.txt'), 'report ' + i);
133
+ t.write(path.join(storage, 'logs', 'host-memory-data-x.log'), 'start\n' + 'm'.repeat(600 * 1024) + 'END');
134
+ t.write(path.join(storage, 'logs', 'host-sync-data-x.log'), 'small');
135
+ t.write(path.join(storage, 'state', 'live-folders.log'), 'live');
136
+ t.write(path.join(storage, 'state', 'logins.json'), 'SECRET');
137
+ t.write(path.join(storage, 'secrets', 'x.json'), 'SECRET');
138
+ const launch = path.join(root, 'launch');
139
+ const out = stagePcLogs(storage, dataPath, launch);
140
+ const listing = (dir) => fs
141
+ .readdirSync(dir, { recursive: true, withFileTypes: true })
142
+ .flatMap((e) => e.isFile() ? [path.relative(out, path.join(e.parentPath, e.name)).replaceAll('\\', '/')] : []);
143
+ const copied = listing(out).sort();
144
+ const reports = copied.filter((n) => n.startsWith('crashes/'));
145
+ const memory = fs.readFileSync(path.join(out, 'host-memory.log'), 'utf8');
146
+ t.check(reports.length === 10 &&
147
+ reports.includes('crashes/crash-2026-01-21.txt') &&
148
+ !reports.includes('crashes/crash-2026-01-11.txt') &&
149
+ fs.statSync(path.join(out, 'host-memory.log')).size <= 256 * 1024 &&
150
+ memory.endsWith('END') &&
151
+ fs.readFileSync(path.join(out, 'README.txt'), 'utf8').includes('never commit'), 'the newest 10 crash reports, the last 256 KB of the memory log and a README are copied', copied.join(', '));
152
+ t.equal(copied.filter((n) => !n.startsWith('crashes/')), ['README.txt', 'host-memory.log', 'host-sync.log', 'live-folders.log'], 'nothing else from the storage folder is copied (no state, logins or secrets)');
153
+ t.section('the guide line for the PC logs');
154
+ t.equal([pcLogsGuideLine('C:\\AgentLaunch', () => false), pcLogsGuideLine('C:\\AgentLaunch', () => true)], [
155
+ [],
156
+ ['- PC crash reports and logs (this project only, read-only, never commit them): `C:\\AgentLaunch\\pc-logs`'],
157
+ ], 'the guide mentions the PC logs only when the folder exists');
93
158
  },
94
159
  };
@@ -514,7 +514,7 @@ export const hcsSuite = {
514
514
  LEFTOVER_DEPS.current = null;
515
515
  }
516
516
  t.section('sandbox engine check hcs');
517
- const out = await f.runBin('agent-sandbox', ['engine', 'check', 'hcs', '--config', t.writeJson(t.at('hcs-config.json'), {})], {
517
+ const out = await f.runBin('sandbox', ['engine', 'check', 'hcs', '--config', t.writeJson(t.at('hcs-config.json'), {})], {
518
518
  cwd: f.workA,
519
519
  env: { ...process.env, NO_COLOR: '1', AGENT_SANDBOX_HOME: t.dir('hcs-home'), AGENT_SANDBOX_HOST_ONLY: '1' },
520
520
  });
@@ -14,7 +14,7 @@ import { logText, writeRepairLog } from '../cli/commands/repair/log.js';
14
14
  import { HealthPrinter, summaryText } from '../cli/commands/repair/printer.js';
15
15
  import { runHealth } from '../cli/commands/repair/run.js';
16
16
  import { configBackups, findCommandCopies, fileOnDisk, groupEntries, inspectPointers, lineDiff, orphanWatchers, parseGroupMembers, parseIcacls, parseRegPath, pathHas, privacyProblems, repairPointers, runStateProblems, shimProblems, staleActiveRecords, staleLaunchFolders, watcherRecords, } from '../host/health/index.js';
17
- import { InstallLayout, writeShims } from '../host/install/index.js';
17
+ import { InstallLayout, REMOVED_COMMANDS, shimText, writeShims } from '../host/install/index.js';
18
18
  import { ActiveRecords } from '../host/sharing/index.js';
19
19
  const SID = 'S-1-5-21-1-2-3-1001';
20
20
  function fakeSystem(over = {}) {
@@ -225,7 +225,7 @@ function installChecks(t, mode) {
225
225
  t.writeJson(path.join(layout.packageDir(v), 'package.json'), { name: 'x', version: v, bin });
226
226
  };
227
227
  pkg('1.4.0', { sandbox: 'dist/bin/sandbox.js' });
228
- pkg('1.5.0', { sandbox: 'dist/bin/sandbox.js', 'agent-sandbox': 'dist/bin/agent-sandbox.js' });
228
+ pkg('1.5.0', { sandbox: 'dist/bin/sandbox.js', 'other-tool': 'dist/bin/other-tool.js' });
229
229
  t.dir('health', 'install-' + mode, 'WindowsDevelopmentSandbox', 'versions', '1.6.0', 'staging');
230
230
  t.write(layout.nodeExe('v22.1.0'), 'x');
231
231
  t.write(layout.nodeExe('v22.11.0'), 'x');
@@ -257,13 +257,13 @@ async function pointers(t, layout, env) {
257
257
  async function shims(t, layout, env) {
258
258
  t.section('install: shims');
259
259
  layout.setActive('1.5.0', 'v22.11.0');
260
- const bins = { sandbox: 'dist/bin/sandbox.js', 'agent-sandbox': 'dist/bin/agent-sandbox.js' };
260
+ const bins = { sandbox: 'dist/bin/sandbox.js', 'other-tool': 'dist/bin/other-tool.js' };
261
261
  writeShims(layout, bins);
262
262
  t.equal(shimProblems(layout, bins), { missing: [], different: [] }, 'freshly written shims are fine');
263
263
  fs.rmSync(path.join(layout.binDir, 'sandbox.cmd'));
264
- fs.writeFileSync(path.join(layout.binDir, 'agent-sandbox'), '#!/bin/sh\necho old\n');
264
+ fs.writeFileSync(path.join(layout.binDir, 'other-tool'), '#!/bin/sh\necho old\n');
265
265
  const problems = shimProblems(layout, bins);
266
- t.equal([problems.missing, problems.different], [['sandbox.cmd'], ['agent-sandbox']], 'a missing and an out-of-date shim are found');
266
+ t.equal([problems.missing, problems.different], [['sandbox.cmd'], ['other-tool']], 'a missing and an out-of-date shim are found');
267
267
  const { run, result } = await lookAt(env, 'install-shims');
268
268
  t.check(result.status === 'needs-you' && result.fixable, 'the shims check offers to rewrite them');
269
269
  const fixed = await run.fix([result]);
@@ -271,6 +271,19 @@ async function shims(t, layout, env) {
271
271
  t.equal(shimProblems(layout, bins), { missing: [], different: [] }, 'the shims read the active version again');
272
272
  fs.writeFileSync(path.join(layout.binDir, 'extra.cmd'), 'echo mine');
273
273
  t.check(fs.existsSync(path.join(layout.binDir, 'extra.cmd')), 'a file that is not one of ours is left alone');
274
+ t.section('install: shims of removed commands');
275
+ for (const [name, target] of Object.entries(REMOVED_COMMANDS)) {
276
+ fs.writeFileSync(path.join(layout.binDir, name + '.cmd'), shimText(name, 'cmd', target));
277
+ fs.writeFileSync(path.join(layout.binDir, name), shimText(name, 'sh', target));
278
+ }
279
+ fs.writeFileSync(path.join(layout.binDir, 'claude-sbx.cmd'), '@echo off rem mine');
280
+ writeShims(layout, bins);
281
+ const left = Object.keys(REMOVED_COMMANDS).flatMap((name) => [name + '.cmd', name].filter((file) => fs.existsSync(path.join(layout.binDir, file))));
282
+ t.equal(left, ['claude-sbx.cmd'], 'writing the shims removes the shims of old commands, but not a file that is not ours');
283
+ const kept = { ...bins, 'claude-sbx': 'dist/bin/claude-sbx.js' };
284
+ writeShims(layout, kept);
285
+ t.check(fs.readFileSync(path.join(layout.binDir, 'claude-sbx'), 'utf8') ===
286
+ shimText('claude-sbx', 'sh', kept['claude-sbx']), 'a package that still declares an old command keeps its shim (a rollback)');
274
287
  }
275
288
  async function copiesOnPath(t) {
276
289
  t.section('install: copies of the commands on the PATH');
@@ -282,9 +295,7 @@ async function copiesOnPath(t) {
282
295
  t.write(path.join(npmBin, 'sandbox'), '#!/bin/sh');
283
296
  t.write(path.join(npmBin, 'node_modules', 'windows-development-sandbox', 'package.json'), '{}');
284
297
  t.write(path.join(managedBin, 'sandbox.cmd'), '@echo off');
285
- t.write(path.join(managedBin, 'claude-sbx.cmd'), '@echo off');
286
- t.write(path.join(managedBin, 'claude-sbx.exe'), '');
287
- t.write(path.join(oldBin, 'claude-sbx.cmd'), '@echo off');
298
+ t.write(path.join(oldBin, 'sandbox.cmd'), '@echo off');
288
299
  const machinePath = [npmBin, managedBin].join(';');
289
300
  const userPath = [oldBin, npmBin, t.dir('copies', 'empty')].join(';');
290
301
  const pathext = '.COM;.EXE;.BAT;.CMD';
@@ -298,8 +309,8 @@ async function copiesOnPath(t) {
298
309
  });
299
310
  t.equal(found.map((c) => [c.kind, c.files.join(',')]), [
300
311
  ['npm', 'sandbox.cmd'],
301
- ['managed-machine', 'sandbox.cmd,claude-sbx.exe'],
302
- ['old-scripts', 'claude-sbx.cmd'],
312
+ ['managed-machine', 'sandbox.cmd'],
313
+ ['old-scripts', 'sandbox.cmd'],
303
314
  ], 'copies come in PATH order (machine first, a folder once), each command as its PATHEXT match, extensionless files ignored');
304
315
  const asked = [];
305
316
  const saved = process.env.ProgramFiles;