windows-development-sandbox 1.1.0 → 1.4.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 (187) hide show
  1. package/README.md +3 -2
  2. package/config.example.json +38 -5
  3. package/dist/cli/args.d.ts +3 -0
  4. package/dist/cli/args.js +3 -1
  5. package/dist/cli/commands/accounts.js +2 -2
  6. package/dist/cli/commands/agent-commands.js +1 -1
  7. package/dist/cli/commands/context.js +2 -1
  8. package/dist/cli/commands/data/clean.js +6 -1
  9. package/dist/cli/commands/data/collect.js +3 -3
  10. package/dist/cli/commands/index.d.ts +1 -1
  11. package/dist/cli/commands/index.js +1 -1
  12. package/dist/cli/commands/launch.js +36 -3
  13. package/dist/cli/commands/maintenance.d.ts +2 -0
  14. package/dist/cli/commands/maintenance.js +35 -2
  15. package/dist/cli/commands/sessions.js +3 -3
  16. package/dist/cli/commands/user-config.js +1 -1
  17. package/dist/cli/index.js +6 -4
  18. package/dist/cli/manifest.js +5 -0
  19. package/dist/cli/menu/apps.d.ts +21 -0
  20. package/dist/cli/menu/apps.js +122 -0
  21. package/dist/cli/menu/command-form.d.ts +28 -0
  22. package/dist/cli/menu/command-form.js +219 -0
  23. package/dist/cli/menu/index.d.ts +26 -0
  24. package/dist/cli/menu/index.js +180 -0
  25. package/dist/cli/menu/settings/categories.d.ts +9 -0
  26. package/dist/cli/menu/settings/categories.js +28 -0
  27. package/dist/cli/menu/settings/config-file.d.ts +40 -0
  28. package/dist/cli/menu/settings/config-file.js +71 -0
  29. package/dist/cli/menu/settings/editors.d.ts +30 -0
  30. package/dist/cli/menu/settings/editors.js +305 -0
  31. package/dist/cli/menu/settings/index.d.ts +32 -0
  32. package/dist/cli/menu/settings/index.js +136 -0
  33. package/dist/cli/menu/settings/schema-node.d.ts +62 -0
  34. package/dist/cli/menu/settings/schema-node.js +104 -0
  35. package/dist/cli/{menu-multi.d.ts → menu/several.d.ts} +10 -2
  36. package/dist/cli/menu/several.js +102 -0
  37. package/dist/cli/tui/app.d.ts +39 -0
  38. package/dist/cli/tui/app.js +52 -0
  39. package/dist/cli/tui/dialogs.d.ts +57 -0
  40. package/dist/cli/tui/dialogs.js +162 -0
  41. package/dist/cli/tui/frame.d.ts +18 -0
  42. package/dist/cli/tui/frame.js +27 -0
  43. package/dist/cli/tui/index.d.ts +7 -0
  44. package/dist/cli/tui/index.js +8 -0
  45. package/dist/cli/tui/list-screen.d.ts +101 -0
  46. package/dist/cli/tui/list-screen.js +299 -0
  47. package/dist/cli/tui/terminal.d.ts +37 -0
  48. package/dist/cli/tui/terminal.js +89 -0
  49. package/dist/cli/tui/text.d.ts +20 -0
  50. package/dist/cli/tui/text.js +105 -0
  51. package/dist/cli/tui/theme.d.ts +45 -0
  52. package/dist/cli/tui/theme.js +30 -0
  53. package/dist/core/config.js +9 -6
  54. package/dist/core/local-config.d.ts +15 -0
  55. package/dist/core/local-config.js +63 -4
  56. package/dist/core/schema/apps.d.ts +40 -0
  57. package/dist/core/schema/apps.js +27 -0
  58. package/dist/core/schema/config.d.ts +66 -1
  59. package/dist/core/schema/config.js +18 -6
  60. package/dist/core/schema/profile.d.ts +5 -0
  61. package/dist/core/schema/profile.js +4 -2
  62. package/dist/core/schema/sandbox.d.ts +28 -0
  63. package/dist/core/schema/sandbox.js +28 -0
  64. package/dist/core/schema/toolchains.d.ts +3 -0
  65. package/dist/core/schema/toolchains.js +6 -1
  66. package/dist/core/types.d.ts +6 -0
  67. package/dist/core/util.d.ts +2 -0
  68. package/dist/core/util.js +2 -0
  69. package/dist/core/workspace.js +22 -5
  70. package/dist/guest/apps/index.d.ts +10 -0
  71. package/dist/guest/apps/index.js +206 -0
  72. package/dist/guest/apps/resolvers.d.ts +8 -0
  73. package/dist/guest/apps/resolvers.js +90 -0
  74. package/dist/guest/audit-parse.d.ts +7 -0
  75. package/dist/guest/audit-parse.js +24 -0
  76. package/dist/guest/audit.d.ts +4 -0
  77. package/dist/guest/audit.js +47 -0
  78. package/dist/guest/daemon/relays.d.ts +1 -0
  79. package/dist/guest/daemon/relays.js +18 -0
  80. package/dist/guest/daemon.js +4 -0
  81. package/dist/guest/env.d.ts +1 -0
  82. package/dist/guest/env.js +3 -1
  83. package/dist/guest/helpers.d.ts +1 -1
  84. package/dist/guest/main.js +15 -2
  85. package/dist/guest/netproxy.d.ts +1 -0
  86. package/dist/guest/netproxy.js +100 -0
  87. package/dist/guest/ptyhost.js +14 -4
  88. package/dist/guest/setup/claude.d.ts +0 -4
  89. package/dist/guest/setup/claude.js +48 -2
  90. package/dist/guest/setup/network.d.ts +9 -0
  91. package/dist/guest/setup/network.js +97 -4
  92. package/dist/guest/setup/user.js +4 -0
  93. package/dist/guest/toolchains/native/mesa.js +13 -3
  94. package/dist/guest/toolchains/plan.d.ts +3 -1
  95. package/dist/guest/toolchains/plan.js +53 -9
  96. package/dist/guest/toolchains/python.js +26 -2
  97. package/dist/guest/toolchains/vscode-profile.d.ts +30 -0
  98. package/dist/guest/toolchains/vscode-profile.js +69 -0
  99. package/dist/guest/toolchains/vscode.d.ts +17 -0
  100. package/dist/guest/toolchains/vscode.js +87 -1
  101. package/dist/host/attach.js +2 -1
  102. package/dist/host/audit/collect.d.ts +34 -0
  103. package/dist/host/audit/collect.js +126 -0
  104. package/dist/host/audit/log.d.ts +46 -0
  105. package/dist/host/audit/log.js +143 -0
  106. package/dist/host/generate.d.ts +6 -0
  107. package/dist/host/generate.js +10 -0
  108. package/dist/host/launch/mappings.js +5 -1
  109. package/dist/host/launch/stage.js +1 -1
  110. package/dist/host/motw.d.ts +17 -0
  111. package/dist/host/motw.js +74 -0
  112. package/dist/host/platform.js +3 -0
  113. package/dist/host/port-forward.d.ts +8 -0
  114. package/dist/host/port-forward.js +37 -0
  115. package/dist/host/repo-guard.d.ts +10 -3
  116. package/dist/host/repo-guard.js +142 -14
  117. package/dist/host/stops.d.ts +67 -0
  118. package/dist/host/stops.js +126 -0
  119. package/dist/host/terminal.d.ts +8 -0
  120. package/dist/host/terminal.js +45 -1
  121. package/dist/host/workspace-view/index.d.ts +20 -0
  122. package/dist/host/workspace-view/index.js +53 -0
  123. package/dist/host/workspace-view/rules.d.ts +26 -0
  124. package/dist/host/workspace-view/rules.js +93 -0
  125. package/dist/host/workspace-view/view.d.ts +50 -0
  126. package/dist/host/workspace-view/view.js +281 -0
  127. package/dist/runtime/daemon.js +1319 -552
  128. package/dist/runtime/db.js +168 -13
  129. package/dist/runtime/main.js +2898 -608
  130. package/dist/runtime/netproxy.js +25936 -0
  131. package/dist/runtime/view.js +272 -23
  132. package/dist/selftest/audit.test.d.ts +2 -0
  133. package/dist/selftest/audit.test.js +57 -0
  134. package/dist/selftest/channel.test.js +7 -0
  135. package/dist/selftest/cli.test.js +47 -17
  136. package/dist/selftest/fixtures.d.ts +45 -0
  137. package/dist/selftest/fixtures.js +3 -1
  138. package/dist/selftest/generate.test.js +7 -2
  139. package/dist/selftest/index.js +5 -2
  140. package/dist/selftest/live-network.test.d.ts +2 -0
  141. package/dist/selftest/live-network.test.js +63 -0
  142. package/dist/selftest/local-config.test.js +16 -3
  143. package/dist/selftest/network.test.js +12 -0
  144. package/dist/selftest/projects.test.js +1 -1
  145. package/dist/selftest/repo-guard.test.js +59 -1
  146. package/dist/selftest/sync.test.js +50 -10
  147. package/dist/selftest/toolchains.test.js +30 -0
  148. package/dist/selftest/workspace-view.test.d.ts +2 -0
  149. package/dist/selftest/workspace-view.test.js +89 -0
  150. package/dist/shared/apps.d.ts +49 -0
  151. package/dist/shared/apps.js +114 -0
  152. package/dist/shared/netpolicy.d.ts +27 -0
  153. package/dist/shared/netpolicy.js +51 -0
  154. package/dist/shared/pty-client.d.ts +9 -4
  155. package/dist/shared/pty-client.js +139 -17
  156. package/dist/shared/rundir.d.ts +2 -0
  157. package/dist/shared/rundir.js +2 -0
  158. package/dist/shared/schema.d.ts +10 -0
  159. package/dist/shared/schema.js +2 -0
  160. package/dist/shared/system.d.ts +1 -0
  161. package/dist/shared/system.js +1 -0
  162. package/dist/sync/claude-mem.js +3 -2
  163. package/dist/sync/history.d.ts +6 -0
  164. package/dist/sync/history.js +26 -11
  165. package/dist/sync/watcher.js +78 -6
  166. package/package.json +2 -1
  167. package/sandbox/cargoshim.cs +56 -0
  168. package/sandbox/cpucap.cs +33 -0
  169. package/sandbox/cpucap.ps1 +4 -35
  170. package/sandbox/logon.cmd +6 -0
  171. package/sandbox/memlog.ps1 +34 -0
  172. package/sandbox/pagefile.cs +31 -0
  173. package/sandbox/pagefile.ps1 +21 -0
  174. package/sandbox/ptyhost.cs +105 -7
  175. package/schemas/config.schema.json +303 -2
  176. package/schemas/sandbox-config.schema.json +126 -12
  177. package/dist/cli/menu-multi.js +0 -82
  178. package/dist/cli/menu.d.ts +0 -14
  179. package/dist/cli/menu.js +0 -290
  180. package/dist/cli/settings/edit.d.ts +0 -13
  181. package/dist/cli/settings/edit.js +0 -19
  182. package/dist/cli/settings/index.d.ts +0 -8
  183. package/dist/cli/settings/index.js +0 -131
  184. package/dist/cli/settings/prompts.d.ts +0 -7
  185. package/dist/cli/settings/prompts.js +0 -48
  186. package/dist/cli/settings/schema.d.ts +0 -33
  187. package/dist/cli/settings/schema.js +0 -29
@@ -5533,7 +5533,7 @@ var require_lodash = __commonJS({
5533
5533
 
5534
5534
  // src/shared/pty-client.ts
5535
5535
  import net from "node:net";
5536
- var FrameType = { Hello: 1, Input: 2, Resize: 3, Restart: 4, Ping: 5, Output: 16, Exit: 17, Error: 18, Restarting: 19, Status: 20 };
5536
+ var FrameType = { Hello: 1, Input: 2, Resize: 3, Restart: 4, Ping: 5, Output: 16, Exit: 17, Error: 18, Restarting: 19, Status: 20, Console: 21 };
5537
5537
  var PING_INTERVAL_MS = 5e3;
5538
5538
  var encodeFrame = (type, payload = Buffer.alloc(0)) => {
5539
5539
  const header = Buffer.alloc(5);
@@ -5570,18 +5570,111 @@ var FrameReader = class {
5570
5570
  return frames;
5571
5571
  }
5572
5572
  };
5573
+ var KEPT_OSC = (body) => {
5574
+ const number4 = /^(\d{1,4})(;|$)/.exec(body);
5575
+ if (!number4) return false;
5576
+ const n = Number(number4[1]);
5577
+ return n <= 2 || n === 8 || n === 133 || n === 9 && body.startsWith(number4[1] + ";4;");
5578
+ };
5579
+ var MAX_KEPT_STRING = 4096;
5573
5580
  var OutputFilter = class {
5574
- carry = "";
5581
+ /** text: normal output; esc: after ESC; c2: after a 0xC2 lead byte; string: inside an OSC/DCS/...; stringEsc / stringC2: a possible terminator inside one. */
5582
+ state = "text";
5583
+ /** The string being read: an OSC worth keeping is collected until its terminator; null = being dropped. */
5584
+ body = null;
5575
5585
  push(chunk) {
5576
- let text = (this.carry + chunk.toString("latin1")).replace(/\x1b\]52;[^\x07\x1b]*(\x07|\x1b\\)/g, "");
5577
- this.carry = "";
5578
- const open2 = text.lastIndexOf("\x1B");
5579
- const tail = open2 === -1 ? "" : text.slice(open2);
5580
- if (tail.length < 1 << 16 && /^\x1b(\](5(2(;[^\x07\x1b]*)?)?)?)?$/.test(tail)) {
5581
- this.carry = tail;
5582
- text = text.slice(0, open2);
5586
+ const out = Buffer.allocUnsafe(chunk.length + MAX_KEPT_STRING + 8);
5587
+ let n = 0;
5588
+ const put = (...bytes) => {
5589
+ for (const b of bytes) out[n++] = b;
5590
+ };
5591
+ const opens = (b, c1) => {
5592
+ if (b === (c1 ? 157 : 93)) {
5593
+ this.body = c1 ? null : [];
5594
+ this.state = "string";
5595
+ return true;
5596
+ }
5597
+ if ((c1 ? [144, 152, 158, 159] : [80, 88, 94, 95]).includes(b)) {
5598
+ this.body = null;
5599
+ this.state = "string";
5600
+ return true;
5601
+ }
5602
+ return false;
5603
+ };
5604
+ const endString = (terminator) => {
5605
+ const body = this.body && Buffer.from(this.body).toString("latin1");
5606
+ if (body !== null && KEPT_OSC(body)) {
5607
+ put(27, 93);
5608
+ out.write(body, n, "latin1");
5609
+ n += body.length;
5610
+ put(...terminator);
5611
+ }
5612
+ this.body = null;
5613
+ this.state = "text";
5614
+ };
5615
+ const collect = (b) => {
5616
+ if (this.body && this.body.push(b) > MAX_KEPT_STRING) this.body = null;
5617
+ };
5618
+ for (const b of chunk) {
5619
+ switch (this.state) {
5620
+ case "text":
5621
+ if (b === 27) this.state = "esc";
5622
+ else if (b === 194) this.state = "c2";
5623
+ else put(b);
5624
+ break;
5625
+ case "esc":
5626
+ if (opens(b, false)) break;
5627
+ if (b !== 27) {
5628
+ put(27, b);
5629
+ this.state = "text";
5630
+ } else put(27);
5631
+ break;
5632
+ case "c2":
5633
+ if (opens(b, true)) break;
5634
+ if (b === 27) {
5635
+ put(194);
5636
+ this.state = "esc";
5637
+ } else if (b === 194) put(194);
5638
+ else {
5639
+ put(194, b);
5640
+ this.state = "text";
5641
+ }
5642
+ break;
5643
+ case "string":
5644
+ if (b === 7) endString([7]);
5645
+ else if (b === 27) this.state = "stringEsc";
5646
+ else if (b === 194) this.state = "stringC2";
5647
+ else if (b === 24 || b === 26) {
5648
+ this.body = null;
5649
+ this.state = "text";
5650
+ } else collect(b);
5651
+ break;
5652
+ case "stringC2":
5653
+ if (b === 156) endString([27, 92]);
5654
+ else {
5655
+ collect(194);
5656
+ this.state = "string";
5657
+ if (b === 7) endString([7]);
5658
+ else if (b === 27) this.state = "stringEsc";
5659
+ else if (b !== 194) collect(b);
5660
+ else this.state = "stringC2";
5661
+ }
5662
+ break;
5663
+ case "stringEsc":
5664
+ if (b === 92) {
5665
+ endString([27, 92]);
5666
+ break;
5667
+ }
5668
+ this.body = null;
5669
+ this.state = "esc";
5670
+ if (!opens(b, false) && b !== 27) {
5671
+ put(27, b);
5672
+ this.state = "text";
5673
+ }
5674
+ break;
5675
+ }
5583
5676
  }
5584
- return Buffer.from(text, "latin1");
5677
+ return out.subarray(0, n);
5585
5678
  }
5586
5679
  };
5587
5680
  var connectOnce = (host, port, timeoutMs) => {
@@ -5652,11 +5745,12 @@ var runTerminalClient = (socket, token, options = {}) => {
5652
5745
  }
5653
5746
  socket.write(encodeFrame(FrameType.Input, swapKeys ? fixWindowsKeys(chunk) : chunk));
5654
5747
  };
5748
+ let passthrough = false;
5655
5749
  const sendSize = () => {
5656
5750
  const [c2, r2] = size();
5657
5751
  if (c2 === sent[0] && r2 === sent[1]) return;
5658
5752
  sent = [c2, r2];
5659
- stdout.write("\x1B[H\x1B[J");
5753
+ if (!passthrough) stdout.write("\x1B[H\x1B[J");
5660
5754
  socket.write(resizeFrame(c2, r2));
5661
5755
  };
5662
5756
  const onResize = () => {
@@ -5680,6 +5774,7 @@ var runTerminalClient = (socket, token, options = {}) => {
5680
5774
  }
5681
5775
  for (const frame of frames) {
5682
5776
  if (frame.type === FrameType.Output) stdout.write(filter.push(frame.payload));
5777
+ else if (frame.type === FrameType.Console) passthrough = frame.payload.toString("latin1") === "conpty.dll";
5683
5778
  else if (frame.type === FrameType.Restarting) stdout.write("\x1B[2J\x1B[3J\x1B[H");
5684
5779
  else if (frame.type === FrameType.Exit) finish({ reason: "exit", exitCode: frame.payload.length >= 4 ? frame.payload.readInt32LE(0) : 1 });
5685
5780
  else if (frame.type === FrameType.Error) finish({ reason: "error", message: frame.payload.toString("utf8").slice(0, 500).replace(/[\x00-\x1f\x7f]/g, " ") });
@@ -25473,8 +25568,10 @@ var ClaudeProfile = external_exports.strictObject({
25473
25568
  globalStateFile: external_exports.string().describe('Your .claude.json, or "auto". Empty string = do not copy.'),
25474
25569
  copyCredentials: external_exports.boolean().describe("Copy your Claude LOGIN into the sandbox. Prefer sandbox account instead."),
25475
25570
  overwrite: external_exports.boolean().describe("Refresh these settings from this PC on every launch."),
25476
- syncHistory: external_exports.boolean().describe("Bring all conversation history in, and sync new history back (path-translated)."),
25477
- syncClaudeMem: external_exports.boolean().describe("Bring claude-mem data in, and its database back when the sandbox closes.")
25571
+ syncHistory: external_exports.boolean().describe("Bring conversation history in, and sync new history back (path-translated)."),
25572
+ historyScope: external_exports.enum(["project", "all"]).describe(`Which history comes in: "project" = only the projects in this sandbox (other projects' conversations, file backups and prompts stay on this PC), "all" = everything. Only this sandbox's projects ever sync back.`),
25573
+ syncClaudeMem: external_exports.boolean().describe("Bring claude-mem data into the sandbox (its settings and backups stay on this PC)."),
25574
+ syncClaudeMemBack: external_exports.boolean().describe("Also copy claude-mem's database back to this PC when the sandbox closes. Off by default: what is in it is loaded into every Claude session on this PC, so a compromised agent could plant instructions there.")
25478
25575
  });
25479
25576
 
25480
25577
  // src/core/schema/sandbox.ts
@@ -25490,6 +25587,7 @@ var Sandbox = external_exports.strictObject({
25490
25587
  maxMemoryMb: external_exports.number().int().positive().describe('Upper limit for memoryMb "auto".'),
25491
25588
  maxMemoryPercent: external_exports.number().int().min(10).max(100).describe(`memoryMb "auto" never takes more than this share of this PC's total RAM (percent).`),
25492
25589
  cpuLimit: external_exports.union([external_exports.literal("none"), external_exports.number().int().min(5).max(100)]).describe(`Most of this PC's CPU the sandbox may use, in percent (5-100), e.g. 50 keeps half free for you; "none" = no cap (Windows Sandbox would take every processor). Covers everything this tool starts in the sandbox (agents, installs, builds, its terminal).`),
25590
+ pageFileMb: external_exports.number().int().min(0).max(262144).describe(`Size of the sandbox's paging file in MB, created at full size when it starts so it never grows while builds run (8 GB of its disk always stays free). Windows Sandbox starts with about 1 GB: parallel builds then fail with "The paging file is too small", and Windows grows the file under load. 0 = leave Windows' own.`),
25493
25591
  reserveHostMb: external_exports.number().int().nonnegative().describe('RAM always left for this PC when memoryMb is "auto".'),
25494
25592
  vGpu: external_exports.boolean().describe("Virtual GPU. Off by default: Microsoft notes it widens the sandbox attack surface."),
25495
25593
  networking: external_exports.boolean().describe("Internet access. false is the only hard network isolation Windows Sandbox offers."),
@@ -25498,7 +25596,33 @@ var Sandbox = external_exports.strictObject({
25498
25596
  videoInput: external_exports.boolean().describe("Camera access."),
25499
25597
  printerRedirection: external_exports.boolean().describe("Printers from this PC."),
25500
25598
  protectedClient: external_exports.boolean().describe("Run the sandbox window in Protected Client mode (recommended)."),
25501
- headless: external_exports.boolean().describe("Hide the sandbox window (sbx-show brings it back). A headless sandbox shuts down when its last session ends.")
25599
+ headless: external_exports.boolean().describe("Hide the sandbox window (sbx-show brings it back). A headless sandbox shuts down when its last session ends."),
25600
+ keepAwake: external_exports.boolean().describe("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).")
25601
+ });
25602
+ var Port2 = external_exports.number().int().min(1).max(65535);
25603
+ var HostEntry = external_exports.string().max(253).regex(/^(@developer|(\*\.)?([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+)$/, "a host name like example.com, *.example.com, or @developer");
25604
+ var Network = external_exports.strictObject({
25605
+ blockPrivateRanges: external_exports.boolean().describe("Block your LAN (10/8, 172.16/12, 192.168/16, SMB) from inside the sandbox."),
25606
+ allowOnly: external_exports.array(HostEntry).describe("Only these hosts can be reached once the sandbox is set up (tools still download first). *.example.com covers its subdomains; @developer adds package registries, GitHub, toolchain downloads and the AI providers. Programs go through a proxy in the sandbox (HTTP_PROXY); anything else is blocked by its firewall. Empty = no limit."),
25607
+ blockHosts: external_exports.array(external_exports.string().max(253).regex(/^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+$/, "a host name like telemetry.example.com")).describe("Never reachable from the sandbox, e.g. production servers or telemetry (each exact name; the sandbox's hosts file sends them nowhere)."),
25608
+ forwardPorts: external_exports.array(external_exports.strictObject({
25609
+ pc: Port2.describe("The port on this PC (localhost only)."),
25610
+ sandbox: Port2.describe("The port inside the sandbox (a server there may listen on localhost only).")
25611
+ })).describe("Reach servers in the sandbox from this PC: http://localhost:<pc> here goes to <sandbox> inside, while it runs. Nothing else on your network can use them."),
25612
+ dns: external_exports.array(external_exports.string().regex(/^\d{1,3}(\.\d{1,3}){3}$/, "an IPv4 address like 1.1.1.1")).describe("DNS servers for the sandbox instead of your network's, e.g. 1.1.1.1 and 9.9.9.9."),
25613
+ proxy: external_exports.string().regex(/^(https?:\/\/[^\s]+)?$/, "http://host:port").describe("An HTTP proxy the sandbox uses to reach the internet (HTTP_PROXY / HTTPS_PROXY for everything in it), e.g. http://proxy.corp:8080. Empty = none.")
25614
+ });
25615
+ var Audit = external_exports.strictObject({
25616
+ enabled: external_exports.boolean().describe("Keep an audit log on this PC of what happened in your sandboxes: programs started (with their command lines), files changed in the project, sessions, and what the repo guard caught. The sandbox cannot change it. Read it with sandbox audit."),
25617
+ days: external_exports.number().int().min(1).max(365).describe("Days the audit log is kept."),
25618
+ maxMb: external_exports.number().int().min(5).max(5e3).describe("Most space the audit log may take, in MB (older days go first). Days before today are compressed."),
25619
+ processes: external_exports.boolean().describe("Record every program started in the sandbox with its command line (Windows process auditing; repeated runs are counted, not listed)."),
25620
+ files: external_exports.boolean().describe("Record the files changed in the project while the sandbox runs.")
25621
+ });
25622
+ var WorkspaceFilter = external_exports.strictObject({
25623
+ enabled: external_exports.boolean().describe("Keep what your .gitignore lists (.env files, node_modules, build output ...) out of the sandbox. The sandbox works on a copy of the project without those files, kept in step with it both ways within about a second; what it creates that is ignored (its own node_modules, builds) stays in the sandbox. Git works as usual. Off = the project folder itself is mapped."),
25624
+ exclude: external_exports.array(external_exports.string().max(500)).describe("Also keep these out, as .gitignore patterns, e.g. secrets/, *.pem, data/private/."),
25625
+ include: external_exports.array(external_exports.string().max(500)).describe("Let these in although .gitignore lists them, e.g. .env.example, data/fixtures/.")
25502
25626
  });
25503
25627
  var Git = external_exports.strictObject({ enabled: external_exports.boolean().describe("Provide Git for Windows (and Git Bash, which Claude Code needs)."), access: GitAccess });
25504
25628
  var Provisioning = external_exports.strictObject({
@@ -25506,7 +25630,8 @@ var Provisioning = external_exports.strictObject({
25506
25630
  fallbackToInstall: external_exports.boolean().describe("Download anything that cannot be copied from this PC."),
25507
25631
  refresh: external_exports.boolean().describe("Re-copy / re-download everything on the next launch."),
25508
25632
  git: Git,
25509
- terminal: toggle("Windows Terminal as the sandbox window (downloaded once).")
25633
+ terminal: toggle("Windows Terminal as the sandbox window (downloaded once)."),
25634
+ conpty: toggle("Agent sessions use Microsoft's newer pseudo console (conpty.dll, downloaded once) instead of the one built into Windows: no garbled screen after resizing, real scrollback for the mouse wheel. Off = the one built into Windows.")
25510
25635
  });
25511
25636
  var Downloads = external_exports.strictObject({
25512
25637
  requireChecksum: external_exports.boolean().describe("Refuse any toolchain, database or tool download that has no published checksum (SHA-256/SHA-512) to verify it against. false = allow those, as before. Downloads that do have a published checksum are always verified. A project's .sandbox-config.json can only turn this on.")
@@ -25590,10 +25715,119 @@ var Toolchains = external_exports.strictObject({
25590
25715
  enabled: external_exports.boolean().describe("Mesa 3D (llvmpipe): OpenGL 4.5 in software for games and apps when there is no GPU (sandbox.vGpu off). Copy opengl32.dll and libgallium_wgl.dll from %MESA_DIR% next to the program's .exe."),
25591
25716
  vulkan: external_exports.boolean().describe("Also software Vulkan 1.4 (Mesa lavapipe) with the Khronos loader, registered for every program; for wgpu set WGPU_BACKEND=vulkan.")
25592
25717
  }),
25593
- vscode: toggle("Portable VS Code inside the sandbox (code on PATH, desktop shortcut); its settings and extensions persist."),
25718
+ vscode: external_exports.strictObject({
25719
+ enabled: external_exports.boolean().describe("Portable VS Code inside the sandbox (code on PATH, desktop shortcut); its settings and extensions persist."),
25720
+ setup: external_exports.boolean().describe("Point VS Code at the sandbox's own tools (JDK, Python, Go, compilers, .NET, Git, PowerShell 7 and Git Bash terminals) and install the language extensions for the toolchains in use."),
25721
+ recommended: external_exports.boolean().describe("Also install the extensions a project recommends in its .vscode/extensions.json."),
25722
+ extensions: external_exports.array(external_exports.string().regex(/^[\w-]+\.[\w-]+$/, "publisher.name, e.g. eamodio.gitlens")).describe("More extensions to install in every sandbox, e.g. eamodio.gitlens, ms-azuretools.vscode-docker.")
25723
+ }),
25594
25724
  environment: external_exports.record(external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "letters, digits and _ only"), external_exports.string()).describe('Extra environment variables for the agent and every terminal, e.g. registry mirrors: {"NPM_CONFIG_REGISTRY": "https://...", "PIP_INDEX_URL": "https://..."}.')
25595
25725
  });
25596
25726
 
25727
+ // src/shared/apps.ts
25728
+ var gh = (github, asset) => ({ github, asset });
25729
+ var APPS = [
25730
+ // ---- editors
25731
+ { id: "vscode", name: "Visual Studio Code", category: "editors", about: "Code editor with debugging, set up for the sandbox's languages.", size: "150 MB", source: { resolve: "setting" }, exe: "Code.exe", setting: "toolchains.vscode.enabled" },
25732
+ { id: "windows-terminal", name: "Windows Terminal", category: "utilities", about: "The sandbox window: tabs, panes, PowerShell 7 profile.", size: "40 MB", source: { resolve: "setting" }, exe: "WindowsTerminal.exe", setting: "provisioning.terminal.enabled" },
25733
+ { id: "notepadpp", name: "Notepad++", category: "editors", about: "Fast text and code editor.", size: "8 MB", source: gh("notepad-plus-plus/notepad-plus-plus", "^npp\\.[\\d.]+\\.portable\\.x64\\.zip$"), exe: "notepad++.exe" },
25734
+ { id: "sublime", name: "Sublime Text", category: "editors", about: "Fast code editor (paid licence, free to evaluate).", size: "28 MB", source: { resolve: "sublime" }, exe: "sublime_text.exe" },
25735
+ { id: "neovim", name: "Neovim", category: "editors", about: "Terminal text editor (nvim).", size: "12 MB", source: gh("neovim/neovim", "^nvim-win64\\.zip$"), exe: "nvim.exe", cli: true },
25736
+ // ---- database clients
25737
+ { id: "dbeaver", name: "DBeaver Community", category: "databases", about: "Universal database client: PostgreSQL, MySQL, SQLite, SQL Server and more.", size: "120 MB", source: gh("dbeaver/dbeaver", "^dbeaver-ce-[\\d.]+-windows-x86_64\\.zip$"), exe: "dbeaver.exe" },
25738
+ { id: "pgadmin", name: "pgAdmin 4", category: "databases", about: "The PostgreSQL admin GUI (from the PostgreSQL binaries zip).", size: "330 MB", source: { resolve: "pgadmin" }, exe: "pgAdmin4.exe" },
25739
+ { id: "heidisql", name: "HeidiSQL", category: "databases", about: "Light client for MySQL, MariaDB, PostgreSQL and SQL Server.", size: "28 MB", source: gh("HeidiSQL/HeidiSQL", "^HeidiSQL_[\\d.]+_64_Portable\\.zip$"), exe: "heidisql.exe" },
25740
+ { id: "sqlitebrowser", name: "DB Browser for SQLite", category: "databases", about: "Open, browse and edit SQLite files.", size: "21 MB", source: gh("sqlitebrowser/sqlitebrowser", "^DB\\.Browser\\.for\\.SQLite-v?[\\d.]+-win64\\.zip$"), exe: "DB Browser for SQLite.exe" },
25741
+ { id: "compass", name: "MongoDB Compass", category: "databases", about: "The MongoDB GUI.", size: "220 MB", source: gh("mongodb-js/compass", "^mongodb-compass-[\\d.]+-win32-x64\\.zip$"), exe: "MongoDBCompass.exe" },
25742
+ { id: "redisinsight", name: "Redis Insight", category: "databases", about: "The Redis GUI.", size: "110 MB", source: gh("redis/RedisInsight", "^Redis-Insight-win-installer\\.exe$"), unpack: "nsis", exe: "Redis Insight.exe", needs: ["7zip"] },
25743
+ { id: "dbgate", name: "DbGate", category: "databases", about: "Database manager for SQL and NoSQL databases.", size: "180 MB", source: gh("dbgate/dbgate", "^dbgate-[\\d.]+-win_x64\\.zip$"), exe: "DbGate.exe" },
25744
+ { id: "beekeeper", name: "Beekeeper Studio", category: "databases", about: "SQL editor and database manager.", size: "245 MB", source: gh("beekeeper-studio/beekeeper-studio", "^Beekeeper-Studio-[\\d.]+-portable\\.exe$"), unpack: "nsis", exe: "Beekeeper Studio.exe", needs: ["7zip"] },
25745
+ { id: "dynamodb-admin", name: "dynamodb-admin", category: "databases", about: "Web GUI for DynamoDB Local: run dynamodb-admin, then open http://localhost:8001.", size: "5 MB", source: { npm: "dynamodb-admin" }, exe: "dynamodb-admin.cmd", cli: true },
25746
+ // ---- API clients
25747
+ { id: "bruno", name: "Bruno", category: "api", about: "Offline, git-friendly API client (collections are files in your repo).", size: "175 MB", source: gh("usebruno/bruno", "^bruno_[\\d.]+_x64_win\\.zip$"), exe: "Bruno.exe" },
25748
+ { id: "insomnia", name: "Insomnia", category: "api", about: "REST, GraphQL and gRPC client.", size: "590 MB", source: gh("Kong/insomnia", "^insomnia-[\\d.]+-full\\.nupkg$"), exe: "Insomnia.exe" },
25749
+ // ---- git
25750
+ { id: "sourcegit", name: "SourceGit", category: "git", about: "Fast open-source Git GUI.", size: "26 MB", source: gh("sourcegit-scm/sourcegit", "^sourcegit_[\\d.]+\\.win-x64\\.zip$"), exe: "SourceGit.exe" },
25751
+ { id: "githubdesktop", name: "GitHub Desktop", category: "git", about: 'Simple Git GUI from GitHub. It has its own git, so it needs provisioning.git.access "full".', size: "300 MB", source: gh("desktop/desktop", "^GitHubDesktop-[\\d.]+-x64-full\\.nupkg$"), exe: "GitHubDesktop.exe", fullGitOnly: true },
25752
+ { id: "lazygit", name: "lazygit", category: "git", about: "Git in the terminal.", size: "7 MB", source: gh("jesseduffield/lazygit", "^lazygit_[\\d.]+_windows_x86_64\\.zip$"), exe: "lazygit.exe", cli: true },
25753
+ // ---- utilities
25754
+ { id: "7zip", name: "7-Zip", category: "utilities", about: "Archive manager (7z, zip, rar, tar ...). Also unpacks installers for other apps.", size: "2 MB", source: { resolve: "7zip" }, exe: "7zFM.exe" },
25755
+ { id: "winmerge", name: "WinMerge", category: "utilities", about: "Compare and merge files and folders.", size: "23 MB", source: gh("WinMerge/winmerge", "^winmerge-[\\d.]+-x64-exe\\.zip$"), exe: "WinMergeU.exe" },
25756
+ { id: "everything", name: "Everything", category: "utilities", about: "Finds files by name instantly.", size: "2 MB", source: { resolve: "everything" }, exe: "Everything.exe" },
25757
+ { id: "sysinternals", name: "Sysinternals Suite", category: "utilities", about: "Process Explorer, Process Monitor, Autoruns, TCPView and more (on PATH).", size: "200 MB", source: { url: "https://download.sysinternals.com/files/SysinternalsSuite.zip" }, exe: "procexp64.exe" },
25758
+ { id: "imhex", name: "ImHex", category: "utilities", about: "Hex editor for reverse engineering and binary formats.", size: "80 MB", source: gh("WerWolv/ImHex", "^imhex-[\\d.]+-Windows-Portable-NoGPU-x86_64\\.zip$"), exe: "imhex-gui.exe" },
25759
+ // ---- browsers
25760
+ { id: "chrome", name: "Chrome for Testing", category: "browsers", about: "Chrome build made for automated tests (with chromedriver on PATH).", size: "170 MB", source: { resolve: "chrome" }, exe: "chrome.exe" },
25761
+ { id: "firefox", name: "Firefox", category: "browsers", about: "Firefox for testing.", size: "75 MB", source: { resolve: "firefox" }, unpack: "nsis", exe: "firefox.exe", needs: ["7zip"] },
25762
+ // ---- game development
25763
+ { id: "godot", name: "Godot", category: "gamedev", about: "Open-source 2D and 3D game engine.", size: "80 MB", source: gh("godotengine/godot", "^Godot_v[\\d.]+-stable_win64\\.exe\\.zip$"), exe: "/^Godot_v[\\d.]+-stable_win64\\.exe$/", needs: ["mesa"] },
25764
+ { id: "blender", name: "Blender", category: "gamedev", about: "3D modelling, animation and rendering.", size: "400 MB", source: { resolve: "blender" }, exe: "blender.exe", needs: ["mesa"] },
25765
+ { id: "love", name: "L\xD6VE", category: "gamedev", about: "Lua framework for 2D games.", size: "4 MB", source: gh("love2d/love", "^love-[\\d.]+-win64\\.zip$"), exe: "love.exe", needs: ["mesa"] },
25766
+ { id: "defold", name: "Defold", category: "gamedev", about: "Free 2D/3D game engine and editor.", size: "330 MB", source: gh("defold/defold", "^Defold-x86_64-win32\\.zip$"), exe: "Defold.exe", needs: ["mesa"] },
25767
+ { id: "pixelorama", name: "Pixelorama", category: "gamedev", about: "Pixel-art and sprite editor.", size: "40 MB", source: gh("Orama-Interactive/Pixelorama", "^Pixelorama-Windows-64bit\\.zip$"), exe: "Pixelorama.exe", needs: ["mesa"] },
25768
+ { id: "tiled", name: "Tiled", category: "gamedev", about: "Tile map editor.", size: "22 MB", source: gh("mapeditor/tiled", "^Tiled-[\\d.]+_Windows-10\\+_x86_64\\.msi$"), unpack: "msi", exe: "tiled.exe" },
25769
+ // ---- cloud
25770
+ { id: "kubectl", name: "kubectl", category: "cloud", about: "Kubernetes command line.", size: "60 MB", source: { resolve: "kubectl" }, unpack: "file", exe: "kubectl.exe", cli: true },
25771
+ { id: "helm", name: "Helm", category: "cloud", about: "Kubernetes package manager.", size: "20 MB", source: { resolve: "helm" }, exe: "helm.exe", cli: true },
25772
+ { id: "k9s", name: "k9s", category: "cloud", about: "Kubernetes cluster UI in the terminal.", size: "40 MB", source: gh("derailed/k9s", "^k9s_Windows_amd64\\.zip$"), exe: "k9s.exe", cli: true },
25773
+ { id: "terraform", name: "Terraform", category: "cloud", about: "Infrastructure as code.", size: "30 MB", source: { resolve: "terraform" }, exe: "terraform.exe", cli: true },
25774
+ { id: "azure-cli", name: "Azure CLI", category: "cloud", about: "Manage Azure (az).", size: "90 MB", source: { url: "https://aka.ms/installazurecliwindowszipx64" }, exe: "az.cmd", cli: true },
25775
+ { id: "gcloud", name: "Google Cloud CLI", category: "cloud", about: "Manage Google Cloud (gcloud, gsutil, bq).", size: "120 MB", source: { url: "https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-windows-x86_64-bundled-python.zip" }, exe: "gcloud.cmd", cli: true },
25776
+ { id: "aws-cli", name: "AWS CLI", category: "cloud", about: "Manage AWS (aws).", size: "50 MB", source: { url: "https://awscli.amazonaws.com/AWSCLIV2.msi" }, unpack: "msi", exe: "aws.exe", cli: true },
25777
+ // ---- diagrams and docs
25778
+ { id: "drawio", name: "draw.io", category: "diagrams", about: "Diagram editor (flowcharts, architecture, UML).", size: "170 MB", source: gh("jgraph/drawio-desktop", "^draw\\.io-[\\d.]+-windows\\.zip$"), exe: "draw.io.exe" },
25779
+ { id: "pandoc", name: "Pandoc", category: "diagrams", about: "Convert documents between Markdown, HTML, Word, PDF ...", size: "35 MB", source: gh("jgm/pandoc", "^pandoc-[\\d.]+-windows-x86_64\\.zip$"), exe: "pandoc.exe", cli: true },
25780
+ // ---- network
25781
+ { id: "mitmproxy", name: "mitmproxy", category: "network", about: "Intercepting HTTPS proxy (mitmweb opens a web UI).", size: "90 MB", source: { resolve: "mitmproxy" }, exe: "mitmweb.exe", cli: true },
25782
+ { id: "xh", name: "xh", category: "network", about: "Friendly HTTP requests from the terminal (like HTTPie).", size: "5 MB", source: gh("ducaale/xh", "^xh-v[\\d.]+-x86_64-pc-windows-msvc\\.zip$"), exe: "xh.exe", cli: true },
25783
+ { id: "grpcurl", name: "grpcurl", category: "network", about: "curl for gRPC servers.", size: "10 MB", source: gh("fullstorydev/grpcurl", "^grpcurl_[\\d.]+_windows_x86_64\\.zip$"), exe: "grpcurl.exe", cli: true },
25784
+ { id: "k6", name: "k6", category: "network", about: "Load testing with JavaScript scripts.", size: "30 MB", source: gh("grafana/k6", "^k6-v[\\d.]+-windows-amd64\\.zip$"), exe: "k6.exe", cli: true },
25785
+ // ---- command-line tools
25786
+ { id: "jq", name: "jq", category: "cli", about: "Query and reshape JSON.", size: "1 MB", source: gh("jqlang/jq", "^jq-windows-amd64\\.exe$"), unpack: "file", exe: "jq.exe", cli: true },
25787
+ { id: "yq", name: "yq", category: "cli", about: "Query and edit YAML, JSON, XML and TOML.", size: "10 MB", source: gh("mikefarah/yq", "^yq_windows_amd64\\.exe$"), unpack: "file", exe: "yq.exe", cli: true },
25788
+ { id: "ripgrep", name: "ripgrep", category: "cli", about: "Very fast recursive search (rg).", size: "2 MB", source: gh("BurntSushi/ripgrep", "^ripgrep-[\\d.]+-x86_64-pc-windows-msvc\\.zip$"), exe: "rg.exe", cli: true },
25789
+ { id: "fd", name: "fd", category: "cli", about: "Fast, friendly alternative to find.", size: "2 MB", source: gh("sharkdp/fd", "^fd-v[\\d.]+-x86_64-pc-windows-msvc\\.zip$"), exe: "fd.exe", cli: true },
25790
+ { id: "bat", name: "bat", category: "cli", about: "cat with syntax highlighting and git changes.", size: "3 MB", source: gh("sharkdp/bat", "^bat-v[\\d.]+-x86_64-pc-windows-msvc\\.zip$"), exe: "bat.exe", cli: true },
25791
+ { id: "fzf", name: "fzf", category: "cli", about: "Fuzzy finder for the terminal.", size: "2 MB", source: gh("junegunn/fzf", "^fzf-[\\d.]+-windows_amd64\\.zip$"), exe: "fzf.exe", cli: true },
25792
+ { id: "delta", name: "delta", category: "cli", about: "Readable git diffs with syntax highlighting.", size: "3 MB", source: gh("dandavison/delta", "^delta-[\\d.]+-x86_64-pc-windows-msvc\\.zip$"), exe: "delta.exe", cli: true },
25793
+ { id: "just", name: "just", category: "cli", about: "Command runner (a simpler make for project tasks).", size: "2 MB", source: gh("casey/just", "^just-[\\d.]+-x86_64-pc-windows-msvc\\.zip$"), exe: "just.exe", cli: true },
25794
+ { id: "hyperfine", name: "hyperfine", category: "cli", about: "Benchmark commands.", size: "1 MB", source: gh("sharkdp/hyperfine", "^hyperfine-v[\\d.]+-x86_64-pc-windows-msvc\\.zip$"), exe: "hyperfine.exe", cli: true },
25795
+ { id: "watchexec", name: "watchexec", category: "cli", about: "Run a command when files change.", size: "5 MB", source: gh("watchexec/watchexec", "^watchexec-[\\d.]+-x86_64-pc-windows-msvc\\.zip$"), exe: "watchexec.exe", cli: true },
25796
+ { id: "protoc", name: "protoc", category: "cli", about: "Protocol Buffers compiler.", size: "3 MB", source: gh("protocolbuffers/protobuf", "^protoc-[\\d.]+-win64\\.zip$"), exe: "protoc.exe", cli: true },
25797
+ { id: "ninja", name: "Ninja", category: "cli", about: "Small, fast build system (CMake's favourite generator).", size: "1 MB", source: gh("ninja-build/ninja", "^ninja-win\\.zip$"), exe: "ninja.exe", cli: true },
25798
+ { id: "ffmpeg", name: "FFmpeg", category: "cli", about: "Convert and process audio and video.", size: "200 MB", source: gh("BtbN/FFmpeg-Builds", "^ffmpeg-master-latest-win64-gpl\\.zip$"), exe: "ffmpeg.exe", cli: true },
25799
+ { id: "sqlite", name: "SQLite tools", category: "cli", about: "sqlite3 command-line shell.", size: "5 MB", source: { resolve: "sqlite" }, exe: "sqlite3.exe", cli: true },
25800
+ { id: "shellcheck", name: "ShellCheck", category: "cli", about: "Finds bugs in shell scripts.", size: "10 MB", source: gh("koalaman/shellcheck", "^shellcheck-v[\\d.]+\\.zip$"), exe: "shellcheck.exe", cli: true },
25801
+ { id: "gitleaks", name: "Gitleaks", category: "cli", about: "Finds secrets committed to git.", size: "8 MB", source: gh("gitleaks/gitleaks", "^gitleaks_[\\d.]+_windows_x64\\.zip$"), exe: "gitleaks.exe", cli: true }
25802
+ ];
25803
+
25804
+ // src/core/schema/apps.ts
25805
+ var CATALOGUE_IDS = APPS.filter((a) => !a.setting).map((a) => a.id);
25806
+ var AppId = external_exports.enum(CATALOGUE_IDS);
25807
+ var CustomAppId = external_exports.string().regex(/^[a-z0-9][a-z0-9-]{0,40}$/, "lower-case letters, digits and - only");
25808
+ var compiles = (pattern) => {
25809
+ try {
25810
+ new RegExp(pattern);
25811
+ return true;
25812
+ } catch {
25813
+ return false;
25814
+ }
25815
+ };
25816
+ var CustomApp = external_exports.strictObject({
25817
+ name: external_exports.string().min(1).max(80).describe('The name on the desktop shortcut, e.g. "My Tool".'),
25818
+ github: external_exports.string().regex(/^[\w.-]+\/[\w.-]+$/, "owner/repo, e.g. sharkdp/bat").optional().describe("GitHub repository (owner/repo): the file comes from its latest release."),
25819
+ asset: external_exports.string().max(200).refine(compiles, "not a valid pattern").optional().describe("With github: a pattern (regular expression) matching the release file, e.g. windows.*x64\\.zip$"),
25820
+ url: external_exports.string().url().startsWith("https://", "https:// only").optional().describe("Or a direct https:// download link (zip, 7z, exe or msi)."),
25821
+ sha256: external_exports.string().regex(/^[0-9a-fA-F]{64}$/, "64 hex characters").optional().describe("SHA-256 of the download (GitHub releases usually publish one; then this is not needed)."),
25822
+ unpack: external_exports.enum(["archive", "file", "nsis", "msi"]).optional().describe("archive (zip, 7z, tar: the default), file (a single .exe), nsis (an installer 7-Zip can open) or msi."),
25823
+ exe: external_exports.string().min(1).max(200).describe("The program to start (found anywhere in the download), e.g. mytool.exe."),
25824
+ cli: external_exports.boolean().optional().describe("A command-line tool: its folder goes on PATH instead of getting a desktop shortcut.")
25825
+ }).refine((a) => a.github && a.asset ? !a.url : Boolean(a.url) && !a.github && !a.asset, { message: "give github + asset, or url (not both)" });
25826
+ var Apps = external_exports.strictObject({
25827
+ install: external_exports.array(AppId).describe("Apps from the catalogue to install in every sandbox (the Apps screen in the menu lists them by category). Each is downloaded once and kept."),
25828
+ custom: external_exports.record(CustomAppId, CustomApp).describe("Programs of your own: a GitHub release or a download link, and the program to start. Installed like the catalogue apps.")
25829
+ });
25830
+
25597
25831
  // src/core/schema/projects.ts
25598
25832
  var Projects = external_exports.strictObject({
25599
25833
  all: external_exports.boolean().describe("Map every project you have started before (and every folder in your roots) at every start, except those in exclude, so all of them open in the running sandbox without a restart. Every agent in the sandbox can then see all of them. Each one is still only set up when you first use it."),
@@ -25610,14 +25844,18 @@ var Config = external_exports.strictObject({
25610
25844
  defaultHarness: Name.describe("Agent started by `sandbox launch` (sandbox claude always runs claude)."),
25611
25845
  guestWorkspace: GuestWorkspace,
25612
25846
  readOnly: external_exports.boolean().describe("Map the workspace read-only."),
25847
+ workspaceFilter: WorkspaceFilter,
25848
+ audit: Audit,
25613
25849
  mappings: external_exports.array(Mapping).describe("Extra folders from this PC."),
25614
25850
  sandbox: Sandbox,
25615
25851
  storage: external_exports.strictObject({
25616
25852
  root: external_exports.string().min(1).describe("Where everything is kept on this PC (tools, databases, logins, launches)."),
25617
25853
  persistAgentState: external_exports.boolean().describe("Keep agent logins and history between sandboxes."),
25618
- dataName: Name.describe("Name of the persistent data set (C:\\AgentData) shared by all workspaces.")
25854
+ dataName: Name.describe("Name of the persistent data set (C:\\AgentData) shared by all workspaces."),
25855
+ markSandboxFiles: external_exports.boolean().describe(`Programs and scripts (.exe, .ps1, .bat, .cmd, .msi, .lnk ...) that appear or change in your project while a sandbox runs get Windows' "downloaded from the internet" mark, so SmartScreen and PowerShell ask before they run on this PC.`),
25856
+ warnDataGb: external_exports.number().min(0).describe("Say at launch when the persisted data (tools, databases, logins) is bigger than this many GB; 0 = never. sandbox data shows what takes the space.")
25619
25857
  }),
25620
- network: external_exports.strictObject({ blockPrivateRanges: external_exports.boolean().describe("Block your LAN (10/8, 172.16/12, 192.168/16, SMB) from inside the sandbox.") }),
25858
+ network: Network,
25621
25859
  provisioning: Provisioning,
25622
25860
  downloads: Downloads,
25623
25861
  toolchains: Toolchains,
@@ -25626,6 +25864,7 @@ var Config = external_exports.strictObject({
25626
25864
  agentCommands: AgentCommands,
25627
25865
  secrets: Secrets,
25628
25866
  personalization: toggle("Copy your desktop look (theme, accent, taskbar, wallpaper, ...) into the sandbox."),
25867
+ apps: Apps,
25629
25868
  claudeProfile: ClaudeProfile,
25630
25869
  harnesses: external_exports.record(Name, Harness).describe("Agents that can run in the sandbox."),
25631
25870
  hooks: external_exports.strictObject({ failFast: external_exports.boolean().describe("Stop when a hook script fails.") }),
@@ -25636,10 +25875,14 @@ var LocalOverrides = external_exports.strictObject({
25636
25875
  harness: Name.describe("Agent for this project."),
25637
25876
  guestWorkspace: GuestWorkspace,
25638
25877
  readOnly: external_exports.boolean().describe("Map this project read-only."),
25639
- mappings: external_exports.array(Mapping).describe("Extra folders (relative to this file)."),
25878
+ // A project may turn the filter on and keep more out, never let ignored files in.
25879
+ workspaceFilter: WorkspaceFilter.pick({ enabled: true, exclude: true }).partial(),
25880
+ mappings: external_exports.array(Mapping).describe("Extra folders inside this project (relative to this file), e.g. a read-only data folder at another place in the sandbox."),
25640
25881
  scripts: Strings.describe("Scripts (.ps1 / .mjs / .cmd, relative to this file) run inside the sandbox before the agent."),
25641
- sandbox: Sandbox.omit({ cpuLimit: true }).partial(),
25642
- network: external_exports.strictObject({ blockPrivateRanges: external_exports.boolean() }).partial(),
25882
+ // This PC's resources are yours to share: CPU and paging file (this PC's disk) are not a project's to set.
25883
+ sandbox: Sandbox.omit({ cpuLimit: true, pageFileMb: true }).partial(),
25884
+ // A project may block more (the LAN, hosts); it never opens hosts, ports, DNS or a proxy.
25885
+ network: Network.pick({ blockPrivateRanges: true, blockHosts: true }).partial(),
25643
25886
  provisioning: Provisioning.pick({ copyFromHost: true, fallbackToInstall: true, refresh: true }).extend({ git: Git.partial(), terminal: toggle("Windows Terminal as the sandbox window.").partial() }).partial(),
25644
25887
  downloads: Downloads.partial(),
25645
25888
  // Every toolchain setting, each one optional (a project usually only switches languages on or pins a version).
@@ -25650,7 +25893,9 @@ var LocalOverrides = external_exports.strictObject({
25650
25893
  databases: external_exports.strictObject(import_lodash.default.mapValues(Databases.shape, (shape) => shape.partial())).partial(),
25651
25894
  attach: Attach.partial(),
25652
25895
  personalization: toggle("Copy your desktop look into the sandbox.").partial(),
25653
- claudeProfile: ClaudeProfile.pick({ enabled: true, include: true, overwrite: true, copyCredentials: true, syncHistory: true, syncClaudeMem: true }).partial(),
25896
+ // Catalogue apps only (a vetted list): your own programs (apps.custom) download anything, so a repo cannot add them.
25897
+ apps: Apps.pick({ install: true }).partial(),
25898
+ claudeProfile: ClaudeProfile.pick({ enabled: true, overwrite: true, copyCredentials: true, syncHistory: true, syncClaudeMem: true }).partial(),
25654
25899
  hooks: external_exports.strictObject({ failFast: external_exports.boolean() }).partial()
25655
25900
  }).partial();
25656
25901
 
@@ -25670,7 +25915,9 @@ var SandboxStatus = external_exports.object({
25670
25915
  updatedAt: Time,
25671
25916
  workspaceHost: Text(),
25672
25917
  guestWorkspace: Text(),
25673
- headless: external_exports.boolean()
25918
+ headless: external_exports.boolean(),
25919
+ /** The sandbox's IPv4 addresses (for network.forwardPorts; the host only uses ones on its Hyper-V network). */
25920
+ addresses: external_exports.array(external_exports.string().max(45)).max(8).optional()
25674
25921
  });
25675
25922
  var Version = external_exports.strictObject({ spec: Text(128), source: Text(256) });
25676
25923
  var DatabasePlan = external_exports.strictObject({
@@ -25748,6 +25995,8 @@ var runPaths = (dataDir2) => {
25748
25995
  responses: path.join(run2, "responses"),
25749
25996
  sessions: path.join(run2, "sessions"),
25750
25997
  projects: path.join(run2, "projects"),
25998
+ /** audit.processes: programs the sandbox started, in small .jsonl files the host takes in and deletes. */
25999
+ audit: path.join(run2, "audit"),
25751
26000
  request: file2("requests"),
25752
26001
  response: file2("responses"),
25753
26002
  session: file2("sessions"),
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const auditSuite: Suite;
@@ -0,0 +1,57 @@
1
+ // The audit log: kept small (repeats folded, a cap per minute, old days compressed, limits on days and size), read back
2
+ // filtered, and what the sandbox reports is checked like everything else that comes from it.
3
+ import crypto from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import zlib from 'node:zlib';
7
+ import { AuditCollector } from '../host/audit/collect.js';
8
+ import { AuditLog, fold } from '../host/audit/log.js';
9
+ import { parseProcessEvents } from '../guest/audit-parse.js';
10
+ import { runPaths } from '../shared/rundir.js';
11
+ const ev = (t, extra = {}) => ({ t, k: 'proc', p: 'C:\\x\\cargo.exe', c: 'cargo build', ...extra });
12
+ export const auditSuite = {
13
+ name: 'audit',
14
+ title: 'audit log (folding, compression, limits, untrusted reports)',
15
+ run: (t) => {
16
+ t.section('small');
17
+ const folded = fold([ev('2026-01-01T10:00:01Z'), ev('2026-01-01T10:00:02Z'), ev('2026-01-01T10:00:03Z', { c: 'cargo test' }), ev('2026-01-01T10:01:00Z')]);
18
+ t.equal(folded.map((e) => [e.c, e.n ?? 1]), [['cargo build', 2], ['cargo test', 1], ['cargo build', 1]], 'repeats within a minute become one line with a count');
19
+ const flood = fold(Array.from({ length: 50 }, (_, i) => ev('2026-01-01T10:00:00Z', { c: 'step ' + i })), 10);
20
+ t.check(flood.length === 11 && flood[10].k === 'note' && flood[10].p.startsWith('40 more'), 'past the per-minute cap the rest is one note');
21
+ const dir = t.at('audit-log');
22
+ const log = new AuditLog(dir, { days: 3, maxMb: 5 });
23
+ log.append([ev('2026-01-01T10:00:00Z'), ev('2026-01-02T10:00:00Z'), ev('2026-01-04T10:00:00Z', { k: 'file', p: 'src/a.ts', a: 'change', w: 'C:\\app' })]);
24
+ log.maintain('2026-01-04');
25
+ t.equal(fs.readdirSync(dir).sort(), ['2026-01-02.jsonl.gz', '2026-01-04.jsonl'], 'finished days are compressed and days past audit.days are deleted');
26
+ t.includes(zlib.gunzipSync(fs.readFileSync(path.join(dir, '2026-01-02.jsonl.gz'))).toString(), 'cargo build', 'a compressed day still holds its events');
27
+ const big = new AuditLog(t.at('audit-big'), { days: 30, maxMb: 5 });
28
+ // Random text: compression must not make the test data fit after all.
29
+ const line = (day) => ev(day + 'T00:00:00Z', { c: crypto.randomBytes(900).toString('hex') });
30
+ for (const day of ['2026-02-01', '2026-02-02', '2026-02-03'])
31
+ big.append(Array.from({ length: 1400 }, () => line(day)));
32
+ big.maintain('2026-02-03');
33
+ t.check(big.bytes() <= 5 * 1024 * 1024 && fs.readdirSync(big.dir).length < 3, 'past audit.maxMb the oldest days go', String(big.bytes()));
34
+ t.section('reading');
35
+ t.equal(log.read({ days: 10000, kinds: ['file'] }).map((e) => e.p), ['src/a.ts'], 'events can be filtered by kind');
36
+ t.equal(log.read({ days: 10000, grep: 'SRC/A.TS' }).length, 1, '... and by text (any case)');
37
+ t.section('what the sandbox reports');
38
+ const xml = "<Event><System><TimeCreated SystemTime='2026-01-05T09:00:00.000Z'/><EventRecordID>41</EventRecordID></System><EventData><Data Name='NewProcessName'>C:\\AgentData\\tools\\git\\cmd\\git.exe</Data><Data Name='CommandLine'>git push --force &quot;origin&quot;</Data><Data Name='ParentProcessName'>C:\\nvm\\nodejs\\node.exe</Data></EventData></Event>"
39
+ + "<Event><System><EventRecordID>42</EventRecordID></System><EventData><Data Name='NewProcessName'>C:\\Windows\\System32\\conhost.exe</Data></EventData></Event>";
40
+ const parsed = parseProcessEvents(xml);
41
+ t.equal({ last: parsed.last, lines: parsed.lines.map((l) => JSON.parse(l)).map((e) => [e.p, e.c, e.pp]) }, { last: 42, lines: [['C:\\AgentData\\tools\\git\\cmd\\git.exe', 'git push --force "origin"', 'C:\\nvm\\nodejs\\node.exe']] }, 'Windows process events become audit lines; Windows\' own background programs are left out');
42
+ const data = t.at('audit-data', 'data', 'd');
43
+ const storage = t.at('audit-data');
44
+ const reports = runPaths(data).audit;
45
+ t.write(path.join(reports, '1-1.jsonl'), [
46
+ JSON.stringify({ t: '2026-01-05T09:00:00Z', k: 'guard', p: 'fake: all clear' }),
47
+ JSON.stringify({ t: '2026-01-05T09:00:01Z', k: 'proc', p: 'C:\\x.exe', c: 'x --y', w: 'C:\\Users\\me' }),
48
+ 'not json', JSON.stringify({ t: '2026-01-05T09:00:02Z', k: 'proc', p: 'x'.repeat(5000) }),
49
+ ].join('\n'));
50
+ t.write(path.join(reports, 'not-a-report.txt'), 'ignored');
51
+ const collector = new AuditCollector(data, storage, { enabled: true, days: 14, maxMb: 50, processes: true, files: false, projects: [] });
52
+ collector.flush();
53
+ const got = new AuditLog(AuditLog.dirFor(storage, data), { days: 14, maxMb: 50 }).read({ days: 100000 });
54
+ t.equal(got.map((e) => [e.k, e.p, e.w]), [['proc', 'fake: all clear', undefined], ['proc', 'C:\\x.exe', undefined]], 'sandbox reports can only be programs: other kinds are recorded as such, fields it may not set are dropped, oversized lines are refused');
55
+ t.check(!fs.existsSync(path.join(reports, '1-1.jsonl')), 'reports are deleted once taken in');
56
+ },
57
+ };
@@ -27,6 +27,13 @@ export const channelSuite = {
27
27
  filter.push(Buffer.from('2;c;cGF5\x1b\\d')),
28
28
  ]).toString('latin1');
29
29
  t.equal(filtered, 'a' + 'b\x1b]0;title\x07' + 'c' + 'd', 'clipboard writes (OSC 52) from the sandbox are stripped, even split across chunks; titles pass');
30
+ const through = (...chunks) => { const f = new OutputFilter(); return Buffer.concat(chunks.map((s) => f.push(Buffer.from(s, 'latin1')))).toString('latin1'); };
31
+ t.equal(through('x\x1b]52;c;QUFB\x1b', '\\y'), 'xy', 'an OSC 52 whose terminator is split between chunks is still stripped');
32
+ t.equal(through('x\x1b]052;c;QUFB\x07y'), 'xy', 'OSC 052 (leading zero) is stripped too');
33
+ t.equal(through('x\x1b]52;c;' + 'A'.repeat(70000), 'B'.repeat(100), '\x07y'), 'xy', 'a huge OSC 52 stays stripped across chunks');
34
+ t.equal(through('x\xc2\x9d52;c;QUFB\xc2\x9cy\x1bP1$qm\x1b\\z'), 'xyz', 'C1 (UTF-8) OSC and DCS strings are stripped');
35
+ t.equal(through('\x1b]8;;https://a.b\x1b\\link\x1b]8;;\x1b\\ \x1b[31mred\x1b[0m é\xc2\xa0'), '\x1b]8;;https://a.b\x1b\\link\x1b]8;;\x1b\\ \x1b[31mred\x1b[0m é\xc2\xa0', 'links, colours and ordinary UTF-8 (incl. U+00A0) pass unchanged');
36
+ t.equal(through('\x1b]9;9;macro\x07\x1b]9;4;1;50\x07'), '\x1b]9;4;1;50\x07', 'only the progress form of OSC 9 passes (ConEmu macros do not)');
30
37
  const oversizedHeader = Buffer.concat([Buffer.from([16]), Buffer.from([0, 0, 0, 0x7f])]);
31
38
  await t.throws(() => new FrameReader().push(oversizedHeader), 'frame too large', 'a frame announcing too much data is rejected instead of buffered');
32
39
  t.equal(new FrameReader().push(encodeFrame(16, Buffer.alloc(MAX_FRAME_BYTES))).length, 1, 'a frame of exactly the maximum size is accepted');
@@ -5,13 +5,14 @@ import path from 'node:path';
5
5
  import { buildProgram, parseCli } from '../cli/args.js';
6
6
  import { COMMANDS } from '../cli/manifest.js';
7
7
  import { commandLine, readHistory, recordHistory } from '../cli/history.js';
8
- import { optionProblem, splitArgs } from '../cli/menu.js';
8
+ import { splitArgs } from '../cli/args.js';
9
+ import { optionProblem } from '../cli/menu/index.js';
9
10
  import { cleanTargetOf } from '../cli/commands/data/collect.js';
10
11
  import { ago, colorEnabled, shorten, shortenMiddle } from '../cli/colors.js';
11
12
  import { hoistConfig, withoutConfig } from '../cli/index.js';
12
13
  import { closest, validNodeSpec, validPythonSpec } from '../cli/validate.js';
13
- import { editConfigText, SECTION_TITLES } from '../cli/settings/index.js';
14
- import { childrenOf, isSection, jsonExample, leavesOf, promptKind } from '../cli/settings/schema.js';
14
+ import { CATEGORIES, editConfigText, nameOf, SchemaNode } from '../cli/menu/settings/index.js';
15
+ import { Frame, ListScreen, Text, TextScreen } from '../cli/tui/index.js';
15
16
  import { Config, LocalOverrides } from '../core/schema.js';
16
17
  export const cliSuite = {
17
18
  name: 'cli',
@@ -99,20 +100,49 @@ export const cliSuite = {
99
100
  const projectBad = editConfigText('{}', ['storage', 'root'], 'C:\\x', 'project');
100
101
  t.check('text' in projectEdit && projectEdit.text.includes('requireChecksum'), 'settings screen: a project file accepts project settings');
101
102
  t.check('problems' in projectBad, 'settings screen: a project file is validated against the project schema (storage cannot be set there)');
102
- // Every setting must be reachable in the settings screen: each leaf has a prompt, each top-level section a title.
103
- const userLeaves = leavesOf(Config);
104
- const projectLeaves = leavesOf(LocalOverrides);
105
- const sectionNames = (schema) => childrenOf(schema).filter(([, s]) => isSection(s)).map(([name]) => name);
106
- const kinds = new Set(userLeaves.map((l) => promptKind(l.schema)));
103
+ // Every setting must be reachable in the settings screen, in a category, with an editor that fits it.
104
+ const userSchema = new SchemaNode(Config);
105
+ const projectSchema = new SchemaNode(LocalOverrides);
106
+ const userLeaves = userSchema.leaves();
107
+ const projectLeaves = projectSchema.leaves();
107
108
  t.check(userLeaves.length > 100 && projectLeaves.length > 50, 'settings screen: every setting is a leaf (' + userLeaves.length + ' settings, ' + projectLeaves.length + ' in a project file)');
108
- const missingKinds = ['boolean', 'enum', 'union', 'number', 'string', 'string-list', 'json'].filter((k) => !kinds.has(k));
109
- t.equal(missingKinds, [], 'settings screen: every kind of setting has a prompt');
110
- t.check(userLeaves.some((l) => l.key.join('.') === 'downloads.requireChecksum'), 'settings screen: downloads.requireChecksum is reachable in your settings');
111
- t.check(projectLeaves.some((l) => l.key.join('.') === 'downloads.requireChecksum'), 'settings screen: downloads.requireChecksum is reachable in a project file');
112
- const untitled = [...sectionNames(Config), ...sectionNames(LocalOverrides)].filter((name) => !SECTION_TITLES[name]);
113
- t.equal(untitled, [], 'settings screen: every top-level section has a title');
114
- const jsonLeaves = userLeaves.filter((l) => promptKind(l.schema) === 'json');
115
- t.check(jsonLeaves.length > 0 && jsonLeaves.every((l) => jsonExample(l.key, l.schema).length > 5), 'settings screen: JSON settings show an example of the expected shape');
116
- t.includes(jsonExample(['harnesses'], jsonLeaves[0].schema), 'npmPackage', 'settings screen: the agent definition example shows its fields');
109
+ const uncategorised = [...userSchema.children(), ...projectSchema.children()].map(([name]) => name).filter((name) => !CATEGORIES.some((c) => c.keys.includes(name)));
110
+ t.equal(uncategorised, [], 'settings screen: every top-level setting belongs to a category');
111
+ const kinds = new Set(userLeaves.map((l) => l.node.editor));
112
+ t.equal(['boolean', 'choice', 'union', 'number', 'string', 'string-list', 'choice-list', 'object-list', 'string-map', 'object-map'].filter((k) => !kinds.has(k)), [], 'settings screen: every kind of editor is used');
113
+ t.equal(userLeaves.filter((l) => l.node.editor === 'json').map((l) => l.key.join('.')), [], 'settings screen: no setting falls back to typing JSON');
114
+ t.check(userLeaves.some((l) => l.key.join('.') === 'downloads.requireChecksum') && projectLeaves.some((l) => l.key.join('.') === 'downloads.requireChecksum'), 'settings screen: downloads.requireChecksum is reachable in both files');
115
+ const harness = userSchema.child('harnesses').element;
116
+ t.includes(String(harness.check(harness.initial())), 'npmPackage', 'settings screen: a new agent says what it still needs (a package to install)');
117
+ t.equal(harness.check({ ...harness.initial(), npmPackage: 'my-cli', npmBin: 'my-cli' }), true, '... and is valid once it has one');
118
+ const mapping = userSchema.child('mappings').element;
119
+ t.check(mapping.check(mapping.initial()) !== true, 'settings screen: a new mapped folder needs its folders filled in before it is valid');
120
+ t.equal([Text.humanize('memoryMb'), Text.humanize('vGpu'), nameOf('node')], ['Memory MB', 'V GPU', 'Node.js'], 'settings screen: readable names');
121
+ t.section('screens');
122
+ const fit = Text.fit('\x1b[31mred text\x1b[0m', 5);
123
+ t.equal([Text.width(fit), Text.strip(fit)], [5, 'red …'], 'a coloured label is cut to the width without breaking its colour');
124
+ t.equal(Text.width('日本'), 4, 'wide characters take two columns');
125
+ const screen = new ListScreen({ path: ['T'], items: [{ value: 'a', label: 'Alpha', category: 'x' }, { value: 'b', label: 'Beta', category: 'y', description: 'second' }], categories: [{ id: 'x', label: 'X' }, { id: 'y', label: 'Y' }] });
126
+ const key = (name, text = '') => ({ name, text, ctrl: false, meta: false, shift: false });
127
+ screen.handle(key('left'));
128
+ screen.handle(key('down'));
129
+ screen.handle(key('right'));
130
+ t.equal(screen.handle(key('return')), { done: { kind: 'pick', value: 'b' } }, 'list screen: arrows switch category, Enter picks');
131
+ const search = new ListScreen({ path: ['T'], items: [{ value: 'a', label: 'Alpha' }, { value: 'b', label: 'Beta' }] });
132
+ for (const k of [key('', '/'), key('b', 'b'), key('e', 'e')])
133
+ search.handle(k);
134
+ t.equal(search.handle(key('return')), { done: { kind: 'pick', value: 'b' } }, 'list screen: / searches');
135
+ const lines = Frame.render(screen.render({ width: 60, height: Frame.bodyHeight(20) }), 60, 20);
136
+ t.equal(lines.length, 20, 'a frame fills the terminal height exactly');
137
+ const multi = new ListScreen({ path: ['T'], multi: true, items: [{ value: 'a', label: 'A' }, { value: 'b', label: 'B', checked: true }] });
138
+ multi.handle(key('space'));
139
+ t.equal(multi.handle(key('return')), { done: { kind: 'multi', values: ['a', 'b'] } }, 'list screen: space ticks several');
140
+ const typed = new TextScreen({ path: ['T'], label: 'Port', validate: (v) => (Number(v) > 0 ? true : 'a number') });
141
+ for (const ch of 'x')
142
+ typed.handle(key(ch, ch));
143
+ t.equal(typed.handle(key('return')), undefined, 'text field: an invalid value is not accepted');
144
+ typed.handle(key('backspace'));
145
+ typed.handle(key('8', '8'));
146
+ t.equal(typed.handle(key('return')), { done: '8' }, 'text field: a valid value is');
117
147
  },
118
148
  };