windows-development-sandbox 1.11.7 → 1.13.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 (246) hide show
  1. package/README.md +13 -6
  2. package/config.example.json +15 -4
  3. package/dist/app/features/access/access.def.d.ts +190 -0
  4. package/dist/app/features/access/access.def.js +273 -0
  5. package/dist/app/features/access/access.run.d.ts +79 -0
  6. package/dist/app/features/access/access.run.js +129 -0
  7. package/dist/app/features/access/index.d.ts +2 -0
  8. package/dist/app/features/access/index.js +3 -0
  9. package/dist/app/features/agents/agent-entry.js +5 -0
  10. package/dist/app/features/agents/agents.def.js +1 -0
  11. package/dist/app/features/apps/apps-rows.d.ts +4 -1
  12. package/dist/app/features/apps/apps-rows.js +7 -2
  13. package/dist/app/features/apps/apps.def.d.ts +19 -0
  14. package/dist/app/features/apps/apps.def.js +50 -12
  15. package/dist/app/features/apps/apps.run.d.ts +2 -0
  16. package/dist/app/features/apps/apps.run.js +1 -1
  17. package/dist/app/features/audit/audit.def.d.ts +350 -0
  18. package/dist/app/features/audit/audit.def.js +457 -0
  19. package/dist/app/features/audit/audit.run.d.ts +110 -0
  20. package/dist/app/features/audit/audit.run.js +133 -0
  21. package/dist/app/features/audit/index.d.ts +2 -0
  22. package/dist/app/features/audit/index.js +3 -0
  23. package/dist/app/features/defs.d.ts +349 -4
  24. package/dist/app/features/defs.js +16 -1
  25. package/dist/app/features/files/files.def.js +1 -0
  26. package/dist/app/features/health/health.def.js +1 -0
  27. package/dist/app/features/sandbox/sandbox.def.js +3 -0
  28. package/dist/app/features/sessions/sessions.def.d.ts +36 -0
  29. package/dist/app/features/sessions/sessions.def.js +77 -9
  30. package/dist/app/features/sessions/sessions.run.d.ts +25 -2
  31. package/dist/app/features/sessions/sessions.run.js +71 -3
  32. package/dist/app/features/settings/settings.def.d.ts +8 -0
  33. package/dist/app/features/settings/settings.def.js +17 -0
  34. package/dist/app/features/settings/settings.run.d.ts +3 -0
  35. package/dist/app/features/settings/settings.run.js +16 -2
  36. package/dist/app/features/web/web.def.d.ts +9 -0
  37. package/dist/app/features/web/web.def.js +26 -1
  38. package/dist/app/features/web/web.run.d.ts +3 -0
  39. package/dist/app/features/web/web.run.js +11 -3
  40. package/dist/app/kernel/access.d.ts +33 -0
  41. package/dist/app/kernel/access.js +44 -0
  42. package/dist/app/kernel/define.d.ts +26 -2
  43. package/dist/app/kernel/index.d.ts +1 -0
  44. package/dist/app/kernel/index.js +1 -0
  45. package/dist/app/kernel/rpc.d.ts +5 -2
  46. package/dist/app/registry.d.ts +4 -1
  47. package/dist/app/registry.js +26 -4
  48. package/dist/cli/args.d.ts +0 -3
  49. package/dist/cli/args.js +3 -4
  50. package/dist/cli/commands/index.d.ts +1 -1
  51. package/dist/cli/commands/index.js +1 -1
  52. package/dist/cli/commands/launch/prepare.js +1 -1
  53. package/dist/cli/commands/maintenance/index.d.ts +0 -1
  54. package/dist/cli/commands/maintenance/index.js +0 -1
  55. package/dist/cli/commands/registry.js +3 -2
  56. package/dist/cli/commands/remote.d.ts +2 -0
  57. package/dist/cli/commands/remote.js +49 -0
  58. package/dist/cli/commands/sessions/reopen.js +3 -0
  59. package/dist/cli/commands/web.js +66 -0
  60. package/dist/cli/manifest.d.ts +63 -45
  61. package/dist/cli/manifest.js +33 -18
  62. package/dist/cli/menu/io-menu.d.ts +5 -3
  63. package/dist/cli/menu/io-menu.js +12 -5
  64. package/dist/cli/menu/pages/apps-page.js +2 -15
  65. package/dist/cli/menu/pages/maintenance-page.d.ts +4 -0
  66. package/dist/cli/menu/pages/maintenance-page.js +51 -8
  67. package/dist/cli/menu/remote-menu.d.ts +19 -0
  68. package/dist/cli/menu/remote-menu.js +108 -0
  69. package/dist/cli/menu/views.js +4 -0
  70. package/dist/cli/operations/handler.js +15 -1
  71. package/dist/cli/services/remote-client.d.ts +16 -0
  72. package/dist/cli/services/remote-client.js +141 -0
  73. package/dist/cli/services/sessions-service.d.ts +5 -0
  74. package/dist/cli/services/sessions-service.js +17 -1
  75. package/dist/core/config/defaults.js +12 -2
  76. package/dist/core/local-config/file.js +16 -2
  77. package/dist/core/local-config/overrides.js +7 -0
  78. package/dist/core/run/versions.js +9 -8
  79. package/dist/core/schema/apps.d.ts +9 -0
  80. package/dist/core/schema/apps.js +9 -0
  81. package/dist/core/schema/config.d.ts +23 -3
  82. package/dist/core/schema/config.js +42 -5
  83. package/dist/core/schema/sandbox.d.ts +1 -0
  84. package/dist/core/schema/sandbox.js +11 -3
  85. package/dist/core/schema/web.d.ts +51 -0
  86. package/dist/core/schema/web.js +46 -1
  87. package/dist/core/util/fs.d.ts +5 -0
  88. package/dist/core/util/fs.js +23 -0
  89. package/dist/core/util/index.d.ts +1 -1
  90. package/dist/core/util/index.js +1 -1
  91. package/dist/guest/apps/app-installer.js +50 -4
  92. package/dist/guest/apps/index.d.ts +1 -1
  93. package/dist/guest/apps/index.js +1 -1
  94. package/dist/guest/apps/sources.js +1 -1
  95. package/dist/guest/audit/parse.d.ts +32 -2
  96. package/dist/guest/audit/parse.js +43 -5
  97. package/dist/guest/audit/process-audit.d.ts +3 -1
  98. package/dist/guest/audit/process-audit.js +9 -6
  99. package/dist/guest/boot/boot.js +12 -3
  100. package/dist/guest/boot/guide.d.ts +1 -1
  101. package/dist/guest/boot/guide.js +27 -3
  102. package/dist/guest/daemon/daemon.js +2 -0
  103. package/dist/guest/daemon/idle-shutdown.js +2 -1
  104. package/dist/guest/daemon/sessions.d.ts +23 -2
  105. package/dist/guest/daemon/sessions.js +151 -20
  106. package/dist/guest/entries/daemon.js +2 -1
  107. package/dist/guest/git/decision.js +7 -0
  108. package/dist/guest/sessions/cmdline.d.ts +14 -0
  109. package/dist/guest/sessions/cmdline.js +10 -0
  110. package/dist/guest/setup/addon-mcp.d.ts +2 -0
  111. package/dist/guest/setup/addon-mcp.js +49 -0
  112. package/dist/guest/setup/agent-addons.d.ts +9 -0
  113. package/dist/guest/setup/agent-addons.js +22 -0
  114. package/dist/guest/setup/headroom.d.ts +7 -0
  115. package/dist/guest/setup/headroom.js +60 -0
  116. package/dist/guest/setup/managed-policy.d.ts +2 -1
  117. package/dist/guest/setup/managed-policy.js +15 -6
  118. package/dist/guest/toolchains/plan.js +8 -3
  119. package/dist/guest/toolchains/setup.js +3 -0
  120. package/dist/host/audit/attribute.d.ts +46 -0
  121. package/dist/host/audit/attribute.js +76 -0
  122. package/dist/host/audit/collect.d.ts +29 -3
  123. package/dist/host/audit/collect.js +256 -61
  124. package/dist/host/audit/git-commits.d.ts +29 -0
  125. package/dist/host/audit/git-commits.js +157 -0
  126. package/dist/host/audit/index.d.ts +5 -0
  127. package/dist/host/audit/index.js +6 -0
  128. package/dist/host/audit/log.d.ts +45 -7
  129. package/dist/host/audit/log.js +148 -45
  130. package/dist/host/audit/redact.d.ts +2 -0
  131. package/dist/host/audit/redact.js +67 -0
  132. package/dist/host/channel/channel.d.ts +5 -1
  133. package/dist/host/channel/channel.js +12 -0
  134. package/dist/host/guard/agent-files.js +7 -1
  135. package/dist/host/guard/commands.js +13 -1
  136. package/dist/host/guard/git-config.js +27 -7
  137. package/dist/host/guard/git-dirs.js +78 -3
  138. package/dist/host/guard/mark-of-the-web.js +3 -1
  139. package/dist/host/guard/nested-repos.js +7 -2
  140. package/dist/host/guard/store.js +1 -0
  141. package/dist/host/guard/types.d.ts +2 -0
  142. package/dist/host/launch/mappings.js +11 -1
  143. package/dist/host/launch/project-scan.js +2 -21
  144. package/dist/host/launch/stage.js +4 -0
  145. package/dist/host/life/session-ledger.js +2 -1
  146. package/dist/host/remote/access-store.d.ts +156 -0
  147. package/dist/host/remote/access-store.js +308 -0
  148. package/dist/host/remote/cert.d.ts +20 -0
  149. package/dist/host/remote/cert.js +103 -0
  150. package/dist/host/remote/door-client.d.ts +13 -0
  151. package/dist/host/remote/door-client.js +54 -0
  152. package/dist/host/remote/firewall.d.ts +44 -0
  153. package/dist/host/remote/firewall.js +118 -0
  154. package/dist/host/remote/gate.d.ts +125 -0
  155. package/dist/host/remote/gate.js +391 -0
  156. package/dist/host/remote/index.d.ts +8 -0
  157. package/dist/host/remote/index.js +10 -0
  158. package/dist/host/remote/lan.d.ts +24 -0
  159. package/dist/host/remote/lan.js +88 -0
  160. package/dist/host/remote/tls-store.d.ts +22 -0
  161. package/dist/host/remote/tls-store.js +63 -0
  162. package/dist/host/remote/windows-helper.d.ts +67 -0
  163. package/dist/host/remote/windows-helper.js +129 -0
  164. package/dist/host/web/bridge.d.ts +5 -1
  165. package/dist/host/web/bridge.js +6 -2
  166. package/dist/host/web/browser.js +1 -1
  167. package/dist/host/web/control.d.ts +4 -1
  168. package/dist/host/web/control.js +28 -4
  169. package/dist/host/web/rpc.d.ts +16 -2
  170. package/dist/host/web/rpc.js +37 -4
  171. package/dist/host/web/runner.d.ts +4 -0
  172. package/dist/host/web/runner.js +12 -0
  173. package/dist/host/web/server.d.ts +18 -2
  174. package/dist/host/web/server.js +97 -24
  175. package/dist/host/web/state.d.ts +5 -0
  176. package/dist/host/web/state.js +8 -0
  177. package/dist/runtime/agent.js +184 -18
  178. package/dist/runtime/daemon.js +772 -338
  179. package/dist/runtime/db.js +184 -18
  180. package/dist/runtime/gitguard.js +6 -0
  181. package/dist/runtime/main.js +966 -553
  182. package/dist/runtime/netproxy.js +184 -18
  183. package/dist/runtime/view.js +186 -18
  184. package/dist/selftest/app.test.js +66 -7
  185. package/dist/selftest/audit.test.js +354 -47
  186. package/dist/selftest/channel.test.js +16 -0
  187. package/dist/selftest/cmdline.test.js +16 -0
  188. package/dist/selftest/git-access.test.js +8 -0
  189. package/dist/selftest/hcs.test.js +35 -0
  190. package/dist/selftest/hostile-input.test.js +12 -1
  191. package/dist/selftest/index.js +2 -0
  192. package/dist/selftest/live-daemon.test.js +25 -0
  193. package/dist/selftest/local-config.test.js +30 -0
  194. package/dist/selftest/menu-screens.js +1 -1
  195. package/dist/selftest/menu.test.js +3 -3
  196. package/dist/selftest/remote.test.d.ts +2 -0
  197. package/dist/selftest/remote.test.js +536 -0
  198. package/dist/selftest/repo-guard.test.js +56 -2
  199. package/dist/selftest/resume.test.js +2 -1
  200. package/dist/selftest/sharing.test.js +9 -2
  201. package/dist/selftest/support/engine-golden.js +9 -0
  202. package/dist/selftest/support/fixtures.d.ts +14 -0
  203. package/dist/selftest/support/web-preview.js +157 -0
  204. package/dist/selftest/toolchains.test.js +147 -2
  205. package/dist/selftest/web.test.js +2 -0
  206. package/dist/shared/apps/agent-addons.d.ts +64 -0
  207. package/dist/shared/apps/agent-addons.js +95 -0
  208. package/dist/shared/apps/catalogue.d.ts +33 -2
  209. package/dist/shared/apps/catalogue.js +102 -11
  210. package/dist/shared/apps/index.d.ts +3 -2
  211. package/dist/shared/apps/index.js +3 -2
  212. package/dist/shared/apps/selection.d.ts +2 -0
  213. package/dist/shared/apps/selection.js +7 -0
  214. package/dist/shared/channel/schema.d.ts +23 -0
  215. package/dist/shared/channel/schema.js +8 -0
  216. package/dist/shared/pty/protocol.d.ts +2 -0
  217. package/dist/shared/pty/protocol.js +2 -0
  218. package/dist/sync/watcher.js +9 -1
  219. package/dist/web/index.html +2 -2
  220. package/dist/web/{main-RQFN4WBY.css → main-UAQPI5XD.css} +1 -1
  221. package/dist/web/main-Z2WYAE2Z.js +221 -0
  222. package/native/csharp/HcsHost/AssemblyInfo.cs +5 -3
  223. package/native/csharp/HcsHost/Hcs.cs +9 -0
  224. package/native/csharp/HcsHost/Program.cs +26 -1
  225. package/native/csharp/HcsHost/Share.cs +173 -0
  226. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  227. package/native/csharp/HostTools/Program.cs +17 -1
  228. package/native/csharp/HostTools/WebHelper.cs +499 -0
  229. package/native/scripts/get-network-profile.ps1 +19 -0
  230. package/native/scripts/set-folder-access.ps1 +11 -0
  231. package/native/scripts/set-web-firewall.ps1 +103 -0
  232. package/package.json +1 -1
  233. package/sandbox/addons/ccusage/package-lock.json +111 -0
  234. package/sandbox/addons/ccusage/package.json +7 -0
  235. package/sandbox/addons/context7/package-lock.json +1313 -0
  236. package/sandbox/addons/context7/package.json +7 -0
  237. package/sandbox/addons/headroom/requirements.in +1 -0
  238. package/sandbox/addons/headroom/requirements.txt +2922 -0
  239. package/sandbox/addons/playwright-mcp/package-lock.json +56 -0
  240. package/sandbox/addons/playwright-mcp/package.json +7 -0
  241. package/sandbox/csharp/PtyHost.cs +20 -0
  242. package/schemas/config.schema.json +78 -4
  243. package/schemas/sandbox-config.schema.json +2 -20
  244. package/dist/cli/commands/maintenance/audit.d.ts +0 -3
  245. package/dist/cli/commands/maintenance/audit.js +0 -58
  246. package/dist/web/main-K2JMCAZI.js +0 -219
@@ -25560,14 +25560,19 @@ var NetworkSchema = external_exports.strictObject({
25560
25560
  });
25561
25561
  var AuditSchema = external_exports.strictObject({
25562
25562
  enabled: external_exports.boolean().describe(
25563
- "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."
25563
+ "Keep an audit log on this PC of what your agents did: programs started (with their command lines, secrets hidden), files changed in the projects, git commits, sessions, and what the repo guard caught, each linked to the agent session that did it where that can be known. The sandbox cannot change it. Read it with sandbox audit or on the Activity page."
25564
+ ),
25565
+ days: external_exports.number().int().min(1).max(365).describe(
25566
+ "Days the audit log is kept (1 to 30; a larger number keeps 30); older days are deleted when a sandbox starts and once a day."
25564
25567
  ),
25565
- days: external_exports.number().int().min(1).max(365).describe("Days the audit log is kept."),
25566
25568
  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."),
25567
25569
  processes: external_exports.boolean().describe(
25568
25570
  "Record every program started in the sandbox with its command line (Windows process auditing; repeated runs are counted, not listed)."
25569
25571
  ),
25570
- files: external_exports.boolean().describe("Record the files changed in the project while the sandbox runs.")
25572
+ files: external_exports.boolean().describe("Record the files changed in the projects while the sandbox runs."),
25573
+ commits: external_exports.boolean().describe(
25574
+ "Record the git commits made in the projects while the sandbox runs (read from each project's .git folder on this PC)."
25575
+ )
25571
25576
  });
25572
25577
  var WorkspaceFilterSchema = external_exports.strictObject({
25573
25578
  enabled: external_exports.boolean().describe(
@@ -25770,7 +25775,100 @@ var ToolchainsSchema = external_exports.strictObject({
25770
25775
 
25771
25776
  // src/shared/apps/catalogue.ts
25772
25777
  var gh = (github, asset) => ({ github, asset });
25778
+ var RTK_VERSION = "1.0.1";
25779
+ var RTK_URL = "https://github.com/rtk-ai/rtk/releases/download/v" + RTK_VERSION + "/rtk-oss-x86_64-pc-windows-msvc.zip";
25780
+ var RTK_SHA256 = "0e53e2eca471e15fbd3636e64e6bc0b182cbc278ed0339c80450a2dd03f4cd6b";
25781
+ var HEADROOM_VERSION = "0.41.0";
25782
+ var CCUSAGE_VERSION = "20.0.36";
25783
+ var PLAYWRIGHT_MCP_VERSION = "0.0.83";
25784
+ var CONTEXT7_MCP_VERSION = "4.4.2";
25773
25785
  var APPS = [
25786
+ // ---- agent add-ons
25787
+ {
25788
+ id: "rtk",
25789
+ name: "rtk (Rust Token Killer)",
25790
+ category: "agents",
25791
+ about: "Compacts the output of shell commands (git, tests, builds, ls ...) before the agent reads it, so it uses fewer tokens. Claude Code's Bash commands go through it on their own.",
25792
+ size: "6 MB",
25793
+ source: { url: RTK_URL, sha256: RTK_SHA256 },
25794
+ exe: "rtk.exe",
25795
+ cli: true,
25796
+ plugsInto: "Claude Code through a hook in the sandbox's managed settings; other agents are told to use it",
25797
+ userOnly: true
25798
+ },
25799
+ {
25800
+ id: "headroom",
25801
+ name: "Headroom",
25802
+ category: "agents",
25803
+ about: "Compresses what Claude Code sends to the model (tool output, logs, files) so it uses fewer tokens: a local proxy on 127.0.0.1:8787 that sits in Claude Code's API path, offline apart from forwarding to api.anthropic.com.",
25804
+ size: "150 MB",
25805
+ source: { locked: "uv", package: "headroom-ai", version: HEADROOM_VERSION },
25806
+ exe: "headroom.exe",
25807
+ service: true,
25808
+ plugsInto: "Claude Code: the sandbox starts the proxy and points Claude Code at it (managed settings)",
25809
+ userOnly: true
25810
+ },
25811
+ {
25812
+ id: "ccusage",
25813
+ name: "ccusage",
25814
+ category: "agents",
25815
+ about: "Reports the agents' token use and cost by day, month or session from their logs in the sandbox (`ccusage daily`; add --offline to skip fetching prices).",
25816
+ size: "5 MB",
25817
+ source: { locked: "npm", package: "ccusage", version: CCUSAGE_VERSION },
25818
+ exe: "ccusage.cmd",
25819
+ cli: true,
25820
+ plugsInto: "Reads Claude Code's session logs in the sandbox; changes nothing",
25821
+ userOnly: true
25822
+ },
25823
+ {
25824
+ id: "playwright-mcp",
25825
+ name: "Playwright MCP",
25826
+ category: "agents",
25827
+ about: "Lets Claude Code drive a browser (Microsoft Edge, already in the sandbox) to open pages, click, fill forms and take screenshots: the MCP server of microsoft/playwright-mcp.",
25828
+ size: "20 MB",
25829
+ source: { locked: "npm", package: "@playwright/mcp", version: PLAYWRIGHT_MCP_VERSION },
25830
+ exe: "playwright-mcp.cmd",
25831
+ service: true,
25832
+ mcp: { name: "sbx-playwright", script: "node_modules/@playwright/mcp/cli.js", args: ["--browser", "msedge"] },
25833
+ plugsInto: "Claude Code as the MCP server sbx-playwright (the sandbox's Claude settings)",
25834
+ userOnly: true
25835
+ },
25836
+ {
25837
+ id: "context7",
25838
+ name: "Context7 MCP",
25839
+ category: "agents",
25840
+ about: "Up-to-date documentation and code examples for libraries, looked up by Claude Code while it works (upstash/context7). The library names and questions go to Upstash's context7.com.",
25841
+ size: "55 MB",
25842
+ source: { locked: "npm", package: "@upstash/context7-mcp", version: CONTEXT7_MCP_VERSION },
25843
+ exe: "context7-mcp.cmd",
25844
+ service: true,
25845
+ mcp: { name: "sbx-context7", script: "node_modules/@upstash/context7-mcp/dist/index.js" },
25846
+ plugsInto: "Claude Code as the MCP server sbx-context7 (the sandbox's Claude settings)",
25847
+ userOnly: true
25848
+ },
25849
+ {
25850
+ id: "claude-mem",
25851
+ name: "claude-mem",
25852
+ category: "agents",
25853
+ about: "Persistent memory for Claude Code (a plugin you install on this PC): its database is brought into the sandbox.",
25854
+ size: "",
25855
+ source: { resolve: "setting" },
25856
+ exe: "",
25857
+ setting: "claudeProfile.syncClaudeMem",
25858
+ plugsInto: "Claude Code, with your Claude settings copied from this PC",
25859
+ userOnly: true
25860
+ },
25861
+ {
25862
+ id: "rea",
25863
+ name: "REA (Reverse Engineer Anything)",
25864
+ category: "agents",
25865
+ about: "Agent-driven reverse engineering (MCP server and CLI, by morluto). Run `rea setup` once to add it to Claude Code and Codex. Native binaries need Ghidra; needs Node 22.19+.",
25866
+ size: "8 MB",
25867
+ source: { npm: "rea-agents" },
25868
+ exe: "rea.cmd",
25869
+ cli: true,
25870
+ plugsInto: "Claude Code and Codex as an MCP server, after `rea setup`"
25871
+ },
25774
25872
  // ---- editors
25775
25873
  {
25776
25874
  id: "vscode",
@@ -26232,16 +26330,6 @@ var APPS = [
26232
26330
  cli: true
26233
26331
  },
26234
26332
  // ---- reverse engineering
26235
- {
26236
- id: "rea",
26237
- name: "REA (Reverse Engineer Anything)",
26238
- category: "reversing",
26239
- about: "Agent-driven reverse engineering (MCP server and CLI, by morluto). Run `rea setup` once to add it to Claude Code and Codex. Native binaries need Ghidra; needs Node 22.19+.",
26240
- size: "8 MB",
26241
- source: { npm: "rea-agents" },
26242
- exe: "rea.cmd",
26243
- cli: true
26244
- },
26245
26333
  {
26246
26334
  id: "ghidra",
26247
26335
  name: "Ghidra",
@@ -26554,13 +26642,39 @@ function appsToInstall(apps, gitAccess) {
26554
26642
  const needs7zip = allowed.some((e) => e.needs?.includes("7zip") || e.unpack === "nsis") && !allowed.some((e) => e.id === "7zip");
26555
26643
  return { entries: [...needs7zip ? [appById("7zip")] : [], ...allowed], skipped };
26556
26644
  }
26645
+ function appsNeedUv(apps) {
26646
+ return Boolean(
26647
+ apps?.install.some((id) => {
26648
+ const source = appById(id)?.source;
26649
+ return source !== void 0 && "locked" in source && source.locked === "uv";
26650
+ })
26651
+ );
26652
+ }
26557
26653
  function appsNeedMesa(apps) {
26558
26654
  return Boolean(apps?.install.some((id) => appById(id)?.needs?.includes("mesa")));
26559
26655
  }
26560
26656
 
26657
+ // src/shared/apps/agent-addons.ts
26658
+ var RTK_GUIDE_LINE = "- rtk is on PATH and compacts command output: Claude Code's Bash commands go through it on their own; in other agents prefix shell commands with `rtk` (`rtk git status`, `rtk cargo test`, `rtk ls`).";
26659
+ var HEADROOM_PORT = 8787;
26660
+ function headroomUrl(port = HEADROOM_PORT) {
26661
+ return "http://127.0.0.1:" + port;
26662
+ }
26663
+ function headroomGuideLine(logFile2) {
26664
+ return "- Headroom compresses Claude Code's requests: Claude Code reaches the API through its proxy on " + headroomUrl() + " (log: `" + logFile2 + "`). If Claude Code cannot reach the API, the proxy stopped: it starts again with the next sandbox, or turn Headroom off in Apps.";
26665
+ }
26666
+ function addonGuideLine(mcpNames, ccusage) {
26667
+ return [
26668
+ ...mcpNames.length ? ["- MCP servers in Claude Code from Apps: " + mcpNames.join(", ") + "."] : [],
26669
+ ...ccusage ? ["- ccusage is on PATH: `ccusage daily` (or `monthly`, `session`) reports token use and cost."] : []
26670
+ ];
26671
+ }
26672
+
26561
26673
  // src/core/schema/apps.ts
26562
26674
  var CATALOGUE_IDS = APPS.filter((a) => !a.setting).map((a) => a.id);
26563
26675
  var AppIdSchema = external_exports.enum(CATALOGUE_IDS);
26676
+ var PROJECT_IDS = APPS.filter((a) => !a.setting && !a.userOnly).map((a) => a.id);
26677
+ var ProjectAppIdSchema = external_exports.enum(PROJECT_IDS);
26564
26678
  var CustomAppIdSchema = external_exports.string().regex(/^[a-z0-9][a-z0-9-]{0,40}$/, "lower-case letters, digits and - only");
26565
26679
  var compiles = (pattern) => {
26566
26680
  try {
@@ -26584,6 +26698,11 @@ var CustomAppSchema = external_exports.strictObject({
26584
26698
  }).refine((a) => a.github && a.asset ? !a.url : Boolean(a.url) && !a.github && !a.asset, {
26585
26699
  message: "give github + asset, or url (not both)"
26586
26700
  });
26701
+ var ProjectAppsSchema = external_exports.strictObject({
26702
+ install: external_exports.array(ProjectAppIdSchema).describe(
26703
+ "Apps from the catalogue this project adds to yours (agent add-ons such as rtk, Headroom and the MCP servers are set in your own settings only)."
26704
+ )
26705
+ });
26587
26706
  var AppsSchema = external_exports.strictObject({
26588
26707
  install: external_exports.array(AppIdSchema).describe(
26589
26708
  "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."
@@ -26624,12 +26743,39 @@ var ResumeSchema = external_exports.strictObject({
26624
26743
  });
26625
26744
 
26626
26745
  // src/core/schema/web.ts
26746
+ var REMOTE_ROLES = ["view", "control"];
26747
+ var WindowsUserNameSchema = external_exports.string().trim().min(1).max(120).regex(
26748
+ /^(?:[A-Za-z0-9][A-Za-z0-9._ -]{0,62}\\)?[^\\/"[\]:;|=,+*?<>@]{1,64}$/,
26749
+ "a Windows user name (name or DOMAIN\\name)"
26750
+ );
26751
+ var RemoteUserSchema = external_exports.strictObject({
26752
+ name: WindowsUserNameSchema.describe("The Windows user (name, or DOMAIN\\name)"),
26753
+ role: external_exports.enum(REMOTE_ROLES).describe("view: sees everything, terminals read-only. control: everything but remote access, logins and tokens.")
26754
+ });
26755
+ var WebLanSchema = external_exports.strictObject({
26756
+ enabled: external_exports.boolean().describe(
26757
+ "(Beta) Devices on your network (a phone): an HTTPS address on this PC for devices you pair with a one-time code (sandbox web pair). Needs a firewall rule (sandbox web lan on asks Windows once). Off: every paired device is signed out."
26758
+ ),
26759
+ port: external_exports.number().int().min(1024).max(65535).describe("HTTPS port for paired devices on your network."),
26760
+ address: external_exports.string().trim().max(15).regex(/^$|^\d{1,3}(\.\d{1,3}){3}$/, "an IPv4 address or empty").describe("This PC's private address to listen on. Empty: the one of its network adapter (never a public one).")
26761
+ });
26762
+ var WebRemoteSchema = external_exports.strictObject({
26763
+ windowsSignIn: external_exports.boolean().describe(
26764
+ "(Beta) Other Windows users of this PC named in web.remote.users sign in to your web UI (and sandbox remote) with their own Windows account: no password, no link."
26765
+ ),
26766
+ users: external_exports.array(RemoteUserSchema).max(32).describe("Who else may sign in with Windows, and their role (view or control)."),
26767
+ rememberDays: external_exports.number().int().min(1).max(90).describe(
26768
+ "How many days a paired device or a Windows sign-in stays signed in (from when it signed in, not renewed)."
26769
+ ),
26770
+ lan: WebLanSchema
26771
+ });
26627
26772
  var WebSchema = external_exports.strictObject({
26628
26773
  enabled: external_exports.boolean().describe(
26629
26774
  "(Beta) The web UI: your sandbox sessions in a browser on this PC (http://127.0.0.1:<port>, this PC only). On: every sandbox start also starts it in the background (it keeps running until sandbox web stop or a restart of the PC). Off: only sandbox web start runs it. Open it signed in with sandbox web open."
26630
26775
  ),
26631
26776
  port: external_exports.number().int().min(1024).max(65535).describe("Port of the web UI on 127.0.0.1 (this PC only). Pick another when it is taken."),
26632
- openOnLaunch: external_exports.boolean().describe("(Beta) With the web UI on: open it in your browser, already signed in, every time a sandbox starts.")
26777
+ openOnLaunch: external_exports.boolean().describe("(Beta) With the web UI on: open it in your browser, already signed in, every time a sandbox starts."),
26778
+ remote: WebRemoteSchema.describe("(Beta) Access for other Windows users of this PC and for paired devices.")
26633
26779
  });
26634
26780
 
26635
26781
  // src/core/schema/vpn.ts
@@ -26659,6 +26805,36 @@ var GithubSchema = external_exports.strictObject({
26659
26805
  });
26660
26806
 
26661
26807
  // src/core/schema/config.ts
26808
+ var RESERVED_ENV_PREFIX = /^(AGENT_|GIT_|GH_|GITHUB_|NODE_|NPM_CONFIG_(USERCONFIG|GLOBALCONFIG|PREFIX|SCRIPT_SHELL)|SSL_|CLAUDE|ANTHROPIC_|OPENAI_|CODEX_|GEMINI_|GOOGLE_|COPILOT_|AWS_|AZURE_)/i;
26809
+ var RESERVED_ENV_SUFFIX = /(_PROXY|_BASE_URL|_ENDPOINT|_CA_BUNDLE|_CA_CERTS)$/i;
26810
+ var RESERVED_ENV_NAMES = new Set(
26811
+ [
26812
+ "PATH",
26813
+ "PATHEXT",
26814
+ "BASH_ENV",
26815
+ "ENV",
26816
+ "COMSPEC",
26817
+ "PSMODULEPATH",
26818
+ "PYTHONPATH",
26819
+ "PYTHONSTARTUP",
26820
+ "PERL5OPT",
26821
+ "RUBYOPT",
26822
+ "JAVA_TOOL_OPTIONS",
26823
+ "LD_PRELOAD",
26824
+ "SYSTEMROOT",
26825
+ "WINDIR",
26826
+ "HOME",
26827
+ "USERPROFILE",
26828
+ "APPDATA",
26829
+ "LOCALAPPDATA",
26830
+ "PROGRAMDATA",
26831
+ "TEMP",
26832
+ "TMP"
26833
+ ].map((n) => n.toUpperCase())
26834
+ );
26835
+ function reservedEnvName(name) {
26836
+ return RESERVED_ENV_PREFIX.test(name) || RESERVED_ENV_SUFFIX.test(name) || RESERVED_ENV_NAMES.has(name.toUpperCase());
26837
+ }
26662
26838
  var ConfigSchema = external_exports.strictObject({
26663
26839
  $schema: SchemaRefSchema.optional(),
26664
26840
  defaultHarness: NameSchema.describe("Agent started by `sandbox launch` (sandbox claude always runs claude)."),
@@ -26725,10 +26901,14 @@ var LocalOverridesSchema = external_exports.strictObject({
26725
26901
  scripts: StringsSchema.describe(
26726
26902
  "Scripts (.ps1 / .mjs / .cmd, relative to this file) run inside the sandbox before the agent."
26727
26903
  ),
26728
- // This PC's resources are yours to share: CPU and paging file (this PC's disk) are not a project's to set.
26904
+ // This PC's resources are yours to share: CPU, paging file (this PC's disk) and the RAM kept for this PC are not a
26905
+ // project's to set.
26729
26906
  sandbox: SandboxSchema.omit({
26730
26907
  cpuLimit: true,
26731
26908
  pageFileMb: true,
26909
+ reserveHostMb: true,
26910
+ maxMemoryPercent: true,
26911
+ maxMemoryMb: true,
26732
26912
  engine: true,
26733
26913
  agentMemoryHeadroomMb: true,
26734
26914
  pressureResponse: true
@@ -26742,14 +26922,17 @@ var LocalOverridesSchema = external_exports.strictObject({
26742
26922
  // Every toolchain setting, each one optional (a project usually only switches languages on or pins a version).
26743
26923
  toolchains: external_exports.strictObject({
26744
26924
  ...import_lodash.default.mapValues(import_lodash.default.omit(ToolchainsSchema.shape, "environment"), (shape) => shape.partial()),
26745
- environment: ToolchainsSchema.shape.environment
26925
+ environment: ToolchainsSchema.shape.environment.refine(
26926
+ (env) => !Object.keys(env).some(reservedEnvName),
26927
+ "names the sandbox sets or that carry logins (AGENT_*, GIT_*, PATH, BASH_ENV, NODE_*, proxies, ANTHROPIC_*, CLAUDE_* ...) cannot be set by a project"
26928
+ )
26746
26929
  }).partial(),
26747
26930
  databases: external_exports.strictObject(import_lodash.default.mapValues(DatabasesSchema.shape, (shape) => shape.partial())).partial(),
26748
26931
  attach: AttachSchema.partial(),
26749
26932
  personalization: toggle("Copy your desktop look into the sandbox.").partial(),
26750
26933
  // Catalogue apps only (a vetted list): your own programs (apps.custom) download anything, so a repo cannot add
26751
- // them.
26752
- apps: AppsSchema.pick({ install: true }).partial(),
26934
+ // them, nor agent add-ons that are yours only (rtk hooks every agent command, Headroom sits in its API path).
26935
+ apps: ProjectAppsSchema.partial(),
26753
26936
  claudeProfile: ClaudeProfileSchema.pick({
26754
26937
  enabled: true,
26755
26938
  overwrite: true,
@@ -26812,6 +26995,7 @@ var ProjectSpecSchema = external_exports.strictObject({
26812
26995
  /** Its hooks were staged in C:\AgentLaunch\projects\<name>\hooks. */
26813
26996
  hooks: external_exports.boolean()
26814
26997
  });
26998
+ var ShellChoiceSchema = external_exports.enum(["auto", "pwsh", "powershell", "cmd"]);
26815
26999
  var GuestRequestSchema = external_exports.discriminatedUnion("kind", [
26816
27000
  external_exports.strictObject({
26817
27001
  kind: external_exports.literal("session"),
@@ -26821,8 +27005,12 @@ var GuestRequestSchema = external_exports.discriminatedUnion("kind", [
26821
27005
  cwd: Text(),
26822
27006
  project: ProjectSpecSchema.optional(),
26823
27007
  harnessSpec: HarnessSchema.optional(),
27008
+ /** A plain shell instead of an agent (harness is then "shell" and args are ignored). */
27009
+ shell: ShellChoiceSchema.optional(),
26824
27010
  createdAt: TimeSchema
26825
27011
  }),
27012
+ /** End a running session (an agent or a shell) and everything it started. */
27013
+ external_exports.strictObject({ kind: external_exports.literal("close"), id: IdSchema, session: IdSchema, createdAt: TimeSchema }),
26826
27014
  /** Provision a registered project without starting an agent (sandbox start / exec / db from its folder). */
26827
27015
  external_exports.strictObject({ kind: external_exports.literal("project"), id: IdSchema, project: ProjectSpecSchema, createdAt: TimeSchema }),
26828
27016
  external_exports.strictObject({
@@ -26888,6 +27076,8 @@ var SessionStatusSchema = external_exports.object({
26888
27076
  id: IdSchema,
26889
27077
  launchId: Text(64),
26890
27078
  harness: Text(64),
27079
+ /** A plain shell (harness is then the shell: pwsh, powershell or cmd); absent: an agent. */
27080
+ kind: external_exports.enum(["agent", "shell"]).optional(),
26891
27081
  cwd: Text(),
26892
27082
  /** The project it runs in: its hub name, or the launch folder's for the first project. */
26893
27083
  project: Text(64).optional(),
@@ -27599,9 +27789,29 @@ var NOISE = /* @__PURE__ */ new Set([
27599
27789
  "usoclient.exe",
27600
27790
  "musnotification.exe"
27601
27791
  ]);
27792
+ var ROOT_SLACK_MS = 5e3;
27793
+ var MAX_KNOWN = 5e4;
27794
+ var Attributor = class {
27795
+ owner = /* @__PURE__ */ new Map();
27796
+ /** The session of a new process (pid, started by ppid at time t), or undefined. */
27797
+ attribute(e, roots) {
27798
+ const root = roots.find(
27799
+ (r) => r.pid === e.ppid && e.t >= r.from - ROOT_SLACK_MS && (r.to === void 0 || e.t <= r.to + ROOT_SLACK_MS)
27800
+ );
27801
+ const session2 = root?.session ?? this.owner.get(e.ppid) ?? void 0;
27802
+ this.owner.delete(e.pid);
27803
+ this.owner.set(e.pid, session2 ?? null);
27804
+ if (this.owner.size > MAX_KNOWN) this.owner.delete(this.owner.keys().next().value);
27805
+ return session2;
27806
+ }
27807
+ };
27602
27808
  var decode3 = (s) => s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
27603
27809
  var field = (event, name) => decode3(new RegExp("<Data Name='" + name + "'>([^<]*)</Data>").exec(event)?.[1] ?? "");
27604
- function parseProcessEvents(xml) {
27810
+ var pidOf = (text) => {
27811
+ const n = /^0x[0-9a-f]{1,8}$/i.test(text) ? parseInt(text, 16) : /^\d{1,10}$/.test(text) ? Number(text) : 0;
27812
+ return Number.isSafeInteger(n) ? n : 0;
27813
+ };
27814
+ function parseProcessEvents(xml, attribution) {
27605
27815
  const lines = [];
27606
27816
  let last = 0;
27607
27817
  for (const event of xml.split("</Event>")) {
@@ -27609,15 +27819,19 @@ function parseProcessEvents(xml) {
27609
27819
  if (!id) continue;
27610
27820
  last = Math.max(last, id);
27611
27821
  const program = field(event, "NewProcessName");
27612
- if (!program || NOISE.has(path15.win32.basename(program).toLowerCase())) continue;
27613
27822
  const t = /SystemTime='([^']+)'/.exec(event)?.[1] ?? (/* @__PURE__ */ new Date()).toISOString();
27823
+ const pid = pidOf(field(event, "NewProcessId"));
27824
+ const ppid = pidOf(field(event, "ProcessId"));
27825
+ const session2 = attribution && pid && ppid ? attribution.attributor.attribute({ pid, ppid, t: Date.parse(t) || Date.now() }, attribution.roots) : void 0;
27826
+ if (!program || NOISE.has(path15.win32.basename(program).toLowerCase())) continue;
27614
27827
  lines.push(
27615
27828
  JSON.stringify({
27616
27829
  t,
27617
27830
  k: "proc",
27618
27831
  p: program.slice(0, 1024),
27619
27832
  c: field(event, "CommandLine").slice(0, 2e3),
27620
- pp: field(event, "ParentProcessName").slice(0, 1024)
27833
+ pp: field(event, "ParentProcessName").slice(0, 1024),
27834
+ ...session2 ? { s: session2 } : {}
27621
27835
  })
27622
27836
  );
27623
27837
  }
@@ -27645,9 +27859,10 @@ var newestRecord = () => {
27645
27859
  });
27646
27860
  return parseProcessEvents(r.stdout ?? "").last;
27647
27861
  };
27648
- function startProcessCollector() {
27862
+ function startProcessCollector(roots = () => []) {
27649
27863
  if (!auditOn()) return;
27650
27864
  let last = newestRecord();
27865
+ const attributor = new Attributor();
27651
27866
  const pass = () => {
27652
27867
  try {
27653
27868
  const r = spawnSync(
@@ -27661,7 +27876,7 @@ function startProcessCollector() {
27661
27876
  ],
27662
27877
  { encoding: "utf8", windowsHide: true, maxBuffer: 64 * 1024 * 1024 }
27663
27878
  );
27664
- const { lines, last: newest } = parseProcessEvents(r.stdout ?? "");
27879
+ const { lines, last: newest } = parseProcessEvents(r.stdout ?? "", { attributor, roots: roots() });
27665
27880
  if (newest) last = newest;
27666
27881
  if (lines.length)
27667
27882
  fs12.writeFileSync(
@@ -27795,7 +28010,7 @@ function withRequestSecrets(req) {
27795
28010
  }
27796
28011
 
27797
28012
  // src/guest/daemon/daemon.ts
27798
- import fs54 from "node:fs";
28013
+ import fs55 from "node:fs";
27799
28014
 
27800
28015
  // src/guest/setup/claude-mem.ts
27801
28016
  import fs15 from "node:fs";
@@ -28281,18 +28496,76 @@ function readBootEnv() {
28281
28496
  }
28282
28497
 
28283
28498
  // src/guest/boot/guide.ts
28499
+ import fs19 from "node:fs";
28500
+ import path27 from "node:path";
28501
+
28502
+ // src/guest/apps/find-exe.ts
28284
28503
  import fs18 from "node:fs";
28285
28504
  import path25 from "node:path";
28505
+ var MAX_DEPTH = 7;
28506
+ var MAX_ENTRIES = 5e4;
28507
+ var X64_FOLDER = /(^|[\\/])(amd64|x64|x86_64|win64)([\\/]|$)/i;
28508
+ function findExe(root, exe) {
28509
+ const match = exeMatcher(exe);
28510
+ const best = (hits) => hits.find((h) => X64_FOLDER.test(path25.relative(root, h))) ?? hits[0] ?? null;
28511
+ let queue = [root];
28512
+ let seen = 0;
28513
+ for (let depth = 0; depth < MAX_DEPTH && queue.length; depth++) {
28514
+ const next = [];
28515
+ const hits = [];
28516
+ for (const dir of queue) {
28517
+ let entries2;
28518
+ try {
28519
+ entries2 = fs18.readdirSync(dir, { withFileTypes: true });
28520
+ } catch {
28521
+ continue;
28522
+ }
28523
+ const hit = entries2.find((e) => e.isFile() && match(e.name));
28524
+ if (hit) hits.push(path25.join(dir, hit.name));
28525
+ else
28526
+ next.push(...entries2.filter((e) => e.isDirectory() && !e.isSymbolicLink()).map((e) => path25.join(dir, e.name)));
28527
+ if ((seen += entries2.length) > MAX_ENTRIES) return best(hits);
28528
+ }
28529
+ if (hits.length) return best(hits);
28530
+ queue = next;
28531
+ }
28532
+ return null;
28533
+ }
28534
+
28535
+ // src/guest/toolchains/tool-env.ts
28536
+ import path26 from "node:path";
28537
+ function emptyEnv() {
28538
+ return { vars: {}, prepend: [], append: [], summary: [] };
28539
+ }
28540
+ var arch = process.arch === "arm64" ? "arm64" : "x64";
28541
+ function tool(...parts2) {
28542
+ return path26.join(toolsDir, ...parts2);
28543
+ }
28544
+ function versionOf(exe, args = ["--version"], options = {}) {
28545
+ return /\d+\.\d+\.\d+\S*/.exec(execOutput(exe, args, options))?.[0] ?? "installed";
28546
+ }
28547
+
28548
+ // src/guest/setup/agent-addons.ts
28549
+ function installedAddon(id) {
28550
+ const entry = appById(id);
28551
+ if (dryRun || !entry || !(session.apps?.install ?? []).includes(id)) return null;
28552
+ return findExe(tool("apps", id), entry.exe);
28553
+ }
28554
+ function installedRtk() {
28555
+ return installedAddon("rtk");
28556
+ }
28557
+
28558
+ // src/guest/boot/guide.ts
28286
28559
  var BEGIN = "<!-- agent-sandbox:begin (generated every boot, do not edit) -->";
28287
28560
  var END = "<!-- agent-sandbox:end -->";
28288
28561
  var mirrorLine = () => {
28289
- const drives = (session.substDrives ?? []).filter((d) => fs18.existsSync(d.letter + ":\\"));
28562
+ const drives = (session.substDrives ?? []).filter((d) => fs19.existsSync(d.letter + ":\\"));
28290
28563
  if (!drives.length) return [];
28291
28564
  return [
28292
28565
  "- " + drives.map((d) => d.letter + ":").join(", ") + " = mapped folders at their PC paths (subst onto `" + drives.map((d) => d.target).join("`, `") + "`; real-path tools may print that form)."
28293
28566
  ];
28294
28567
  };
28295
- function sandboxGuide(toolSummary, lan = "blocked", projects = []) {
28568
+ function sandboxGuide(toolSummary, lan = "blocked", projects = [], addons = []) {
28296
28569
  return [
28297
28570
  BEGIN,
28298
28571
  "## Sandbox manifest \u2014 you are in a DISPOSABLE Windows Sandbox",
@@ -28302,11 +28575,13 @@ function sandboxGuide(toolSummary, lan = "blocked", projects = []) {
28302
28575
  ...projects.map(
28303
28576
  (p) => "- RW, persists: `" + p.guest + "` = host folder " + p.host + " (another project in this sandbox; your own is `%AGENT_WORKSPACE%`)"
28304
28577
  ),
28305
- "- RW, persists: `" + toolsDir + "` (toolchains) \u2014 put portable exes in `" + path25.join(toolsDir, "bin") + "` (on PATH)",
28306
- "- RW, persists: `" + stateDir + "` (agent config/history; do not edit logins) \xB7 logs: `" + path25.join(dataDir, "logs") + "`",
28578
+ "- RW, persists: `" + toolsDir + "` (toolchains) \u2014 put portable exes in `" + path27.join(toolsDir, "bin") + "` (on PATH)",
28579
+ "- RW, persists: `" + stateDir + "` (agent config/history; do not edit logins) \xB7 logs: `" + path27.join(dataDir, "logs") + "`",
28307
28580
  "- RO: `" + launchDir + "` (this boot's scripts) \xB7 lost on close: everything else, incl. `C:\\nvm` (Node symlink)",
28308
28581
  ...mirrorLine(),
28309
28582
  "- Tools: " + toolSummary.join(" \xB7 "),
28583
+ // Agent add-ons that change how the agent works (rtk, Headroom, MCP servers, ccusage).
28584
+ ...addons,
28310
28585
  "- Persistent installs: `npm i -g` (per nvm version), `pip`, `cargo install`, `dotnet tool -g`, `uv tool`, `bun add -g`. Real installers must re-run each boot via a hook (ask the user).",
28311
28586
  "- Mapped folders (workspace, C:\\AgentData) allow NO symlinks or junctions: pnpm runs hoisted (set), npm workspaces / `file:` deps and deno node_modules dirs fail there (use a folder on C:\\ outside them), bun cannot save its lockfile there (`bun install --no-save`).",
28312
28587
  "- Network: " + (lan === "offline" ? "OFF" : lan === "blocked" ? "internet yes, host LAN blocked" : "internet yes, host LAN NOT blocked this time") + ". Other sessions may share this workspace; the user may be typing from the host.",
@@ -28314,37 +28589,54 @@ function sandboxGuide(toolSummary, lan = "blocked", projects = []) {
28314
28589
  ].join("\n");
28315
28590
  }
28316
28591
  function upsertGuide(file3, guide) {
28317
- fs18.mkdirSync(path25.dirname(file3), { recursive: true });
28318
- const current = fs18.existsSync(file3) ? fs18.readFileSync(file3, "utf8") : "";
28592
+ fs19.mkdirSync(path27.dirname(file3), { recursive: true });
28593
+ const current = fs19.existsSync(file3) ? fs19.readFileSync(file3, "utf8") : "";
28319
28594
  const start = current.indexOf(BEGIN);
28320
28595
  const end = current.indexOf(END);
28321
28596
  const without = start >= 0 && end > start ? current.slice(0, start) + current.slice(end + END.length) : current;
28322
- fs18.writeFileSync(file3, without.trimEnd() + (without.trim() ? "\n\n" : "") + guide + "\n", "utf8");
28597
+ fs19.writeFileSync(file3, without.trimEnd() + (without.trim() ? "\n\n" : "") + guide + "\n", "utf8");
28323
28598
  }
28324
28599
  function managedClaudeMd() {
28325
- return path25.join(
28326
- process.env.AGENT_MANAGED_DIR ?? path25.join(process.env.ProgramFiles ?? "C:\\Program Files", "ClaudeCode"),
28600
+ return path27.join(
28601
+ process.env.AGENT_MANAGED_DIR ?? path27.join(process.env.ProgramFiles ?? "C:\\Program Files", "ClaudeCode"),
28327
28602
  "CLAUDE.md"
28328
28603
  );
28329
28604
  }
28605
+ var headroomRuns = () => {
28606
+ try {
28607
+ const file3 = path27.join(path27.dirname(managedClaudeMd()), "managed-settings.json");
28608
+ const settings = JSON.parse(fs19.readFileSync(file3, "utf8"));
28609
+ return typeof settings.env?.ANTHROPIC_BASE_URL === "string";
28610
+ } catch {
28611
+ return false;
28612
+ }
28613
+ };
28614
+ function addonLines() {
28615
+ const mcp = APPS.flatMap((a) => a.mcp && installedAddon(a.id) ? [a.mcp.name] : []);
28616
+ return [
28617
+ ...installedRtk() ? [RTK_GUIDE_LINE] : [],
28618
+ ...installedAddon("headroom") && headroomRuns() ? [headroomGuideLine(path27.join(logsDir, "headroom.log"))] : [],
28619
+ ...addonGuideLine(mcp, Boolean(installedAddon("ccusage")))
28620
+ ];
28621
+ }
28330
28622
  function writeGuides(toolSummary, lan, projects = []) {
28331
- const guide = sandboxGuide(toolSummary, lan, projects);
28623
+ const guide = sandboxGuide(toolSummary, lan, projects, addonLines());
28332
28624
  const managed = managedClaudeMd();
28333
- fs18.mkdirSync(path25.dirname(managed), { recursive: true });
28334
- fs18.writeFileSync(managed, guide + "\n", "utf8");
28335
- fs18.writeFileSync(path25.join(dataDir, "SANDBOX.md"), guide + "\n", "utf8");
28336
- upsertGuide(path25.join(stateDir, "codex", "AGENTS.md"), guide);
28337
- upsertGuide(path25.join(stateDir, "gemini", ".gemini", "GEMINI.md"), guide);
28338
- removeGuide(path25.join(claudeDir, "CLAUDE.md"));
28625
+ fs19.mkdirSync(path27.dirname(managed), { recursive: true });
28626
+ fs19.writeFileSync(managed, guide + "\n", "utf8");
28627
+ fs19.writeFileSync(path27.join(dataDir, "SANDBOX.md"), guide + "\n", "utf8");
28628
+ upsertGuide(path27.join(stateDir, "codex", "AGENTS.md"), guide);
28629
+ upsertGuide(path27.join(stateDir, "gemini", ".gemini", "GEMINI.md"), guide);
28630
+ removeGuide(path27.join(claudeDir, "CLAUDE.md"));
28339
28631
  return managed;
28340
28632
  }
28341
28633
  var removeGuide = (file3) => {
28342
- if (!fs18.existsSync(file3)) return;
28343
- const current = fs18.readFileSync(file3, "utf8");
28634
+ if (!fs19.existsSync(file3)) return;
28635
+ const current = fs19.readFileSync(file3, "utf8");
28344
28636
  const start = current.indexOf(BEGIN);
28345
28637
  const end = current.indexOf(END);
28346
28638
  if (start >= 0 && end > start)
28347
- fs18.writeFileSync(file3, (current.slice(0, start) + current.slice(end + END.length)).trim() + "\n", "utf8");
28639
+ fs19.writeFileSync(file3, (current.slice(0, start) + current.slice(end + END.length)).trim() + "\n", "utf8");
28348
28640
  };
28349
28641
 
28350
28642
  // src/guest/runtime/project-context.ts
@@ -28381,11 +28673,11 @@ function saveProjectStatus(name, workspace2, phase, message, databases, summary)
28381
28673
  }
28382
28674
 
28383
28675
  // src/guest/setup/hooks.ts
28384
- import fs19 from "node:fs";
28385
- import path26 from "node:path";
28676
+ import fs20 from "node:fs";
28677
+ import path28 from "node:path";
28386
28678
  import { spawnSync as spawnSync3 } from "node:child_process";
28387
28679
  var commandFor = (file3) => {
28388
- const ext = path26.extname(file3).toLowerCase();
28680
+ const ext = path28.extname(file3).toLowerCase();
28389
28681
  if (ext === ".mjs" || ext === ".js") return [process.execPath, [file3]];
28390
28682
  if (!isWin) return null;
28391
28683
  if (ext === ".ps1")
@@ -28393,18 +28685,18 @@ var commandFor = (file3) => {
28393
28685
  if (ext === ".cmd" || ext === ".bat") return [SYSTEM.cmd(), ["/d", "/c", file3]];
28394
28686
  return null;
28395
28687
  };
28396
- var BOOT = { root: path26.join(launchDir, "hooks"), cwd: workspace };
28688
+ var BOOT = { root: path28.join(launchDir, "hooks"), cwd: workspace };
28397
28689
  function projectHooks(name, cwd) {
28398
28690
  return {
28399
- root: path26.join(launchDir, "projects", name, "hooks"),
28691
+ root: path28.join(launchDir, "projects", name, "hooks"),
28400
28692
  cwd
28401
28693
  };
28402
28694
  }
28403
28695
  var hooksOf = (stage, target) => {
28404
- const dir = path26.join(target.root, stage);
28696
+ const dir = path28.join(target.root, stage);
28405
28697
  if (!exists(dir)) return [];
28406
- return fs19.readdirSync(dir).sort().flatMap((name) => {
28407
- const command = commandFor(path26.join(dir, name));
28698
+ return fs20.readdirSync(dir).sort().flatMap((name) => {
28699
+ const command = commandFor(path28.join(dir, name));
28408
28700
  return command ? [{ name, command }] : [];
28409
28701
  });
28410
28702
  };
@@ -28433,26 +28725,109 @@ async function runHooksAsync(stage, env, target) {
28433
28725
  }
28434
28726
  }
28435
28727
 
28436
- // src/guest/toolchains/tool-env.ts
28437
- import path27 from "node:path";
28438
- function emptyEnv() {
28439
- return { vars: {}, prepend: [], append: [], summary: [] };
28440
- }
28441
- var arch = process.arch === "arm64" ? "arm64" : "x64";
28442
- function tool(...parts2) {
28443
- return path27.join(toolsDir, ...parts2);
28728
+ // src/guest/apps/app-installer.ts
28729
+ import fs24 from "node:fs";
28730
+ import path33 from "node:path";
28731
+
28732
+ // src/guest/toolchains/runtimes.ts
28733
+ import path29 from "node:path";
28734
+
28735
+ // src/guest/toolchains/install.ts
28736
+ import fs21 from "node:fs";
28737
+ async function downloadFile(url2, file3, sha256 = null) {
28738
+ const part = file3 + ".part";
28739
+ try {
28740
+ await downloader.file(url2, part, sha256);
28741
+ fs21.rmSync(file3, { force: true });
28742
+ fs21.renameSync(part, file3);
28743
+ } finally {
28744
+ fs21.rmSync(part, { force: true });
28745
+ }
28444
28746
  }
28445
- function versionOf(exe, args = ["--version"], options = {}) {
28446
- return /\d+\.\d+\.\d+\S*/.exec(execOutput(exe, args, options))?.[0] ?? "installed";
28747
+ async function installFromGithub(repo, pattern, o) {
28748
+ const asset = await githubAsset(repo, pattern, o.searchOlder);
28749
+ log("Downloading " + (typeof o.what === "string" ? o.what : o.what(asset)) + " ...");
28750
+ await downloader.archive(asset.url, o.dest, {
28751
+ sha256: asset.sha256,
28752
+ fileName: asset.name,
28753
+ ...o.locate ? { locate: o.locate } : {},
28754
+ ...o.up ? { up: o.up } : {}
28755
+ });
28447
28756
  }
28448
28757
 
28449
- // src/guest/apps/app-installer.ts
28450
- import fs23 from "node:fs";
28451
- import path32 from "node:path";
28758
+ // src/guest/toolchains/runtimes.ts
28759
+ async function setupSingleExe(env, name, repo, pattern, exeName, dir, options = {}) {
28760
+ if (dryRun) {
28761
+ log("(dry run) would provision " + name);
28762
+ return;
28763
+ }
28764
+ const bin = options.subdir ? path29.join(dir, options.subdir) : dir;
28765
+ const exe = path29.join(bin, exeName);
28766
+ if (!exists(exe) || options.refresh) {
28767
+ const size = options.size ? ", ~" + options.size : "";
28768
+ await installFromGithub(repo, pattern, {
28769
+ dest: dir,
28770
+ what: (a) => name + " (" + a.name + size + " once)",
28771
+ ...options.subdir ? {} : { locate: exeName }
28772
+ });
28773
+ }
28774
+ if (!exists(exe)) throw new Error(exeName + " missing after installing " + name);
28775
+ env.prepend.push(bin);
28776
+ env.summary.push(name + ": " + versionOf(exe, options.versionArgs) + (options.note ?? ""));
28777
+ }
28778
+ async function setupBun(env) {
28779
+ await setupSingleExe(
28780
+ env,
28781
+ "bun",
28782
+ "oven-sh/bun",
28783
+ arch === "arm64" ? /^bun-windows-aarch64\.zip$/ : /^bun-windows-x64\.zip$/,
28784
+ "bun.exe",
28785
+ tool("bun", "bin"),
28786
+ { size: "35 MB" }
28787
+ );
28788
+ Object.assign(env.vars, {
28789
+ BUN_INSTALL: tool("bun"),
28790
+ BUN_INSTALL_CACHE_DIR: path29.join(localDir, "bun-cache"),
28791
+ BUN_TMPDIR: path29.join(localDir, "bun-tmp")
28792
+ });
28793
+ }
28794
+ async function setupDeno(env) {
28795
+ await setupSingleExe(
28796
+ env,
28797
+ "deno",
28798
+ "denoland/deno",
28799
+ arch === "arm64" ? /^deno-aarch64-pc-windows-msvc[.]zip$/ : /^deno-x86_64-pc-windows-msvc[.]zip$/,
28800
+ "deno.exe",
28801
+ tool("deno", "bin"),
28802
+ { size: "40 MB" }
28803
+ );
28804
+ Object.assign(env.vars, { DENO_DIR: tool("deno-cache"), DENO_INSTALL_ROOT: tool("deno"), DENO_NO_UPDATE_CHECK: "1" });
28805
+ }
28806
+ function uvVars() {
28807
+ return {
28808
+ UV_CACHE_DIR: tool("uv-cache"),
28809
+ UV_PYTHON_INSTALL_DIR: tool("uv-python"),
28810
+ UV_TOOL_DIR: tool("uv-tools"),
28811
+ UV_TOOL_BIN_DIR: tool("uv-bin")
28812
+ };
28813
+ }
28814
+ async function setupUv(env) {
28815
+ await setupSingleExe(
28816
+ env,
28817
+ "uv",
28818
+ "astral-sh/uv",
28819
+ arch === "arm64" ? /^uv-aarch64-pc-windows-msvc\.zip$/ : /^uv-x86_64-pc-windows-msvc\.zip$/,
28820
+ "uv.exe",
28821
+ tool("uv"),
28822
+ { size: "20 MB" }
28823
+ );
28824
+ Object.assign(env.vars, uvVars());
28825
+ env.prepend.push(mkdirp(tool("uv-bin")));
28826
+ }
28452
28827
 
28453
28828
  // src/guest/apps/after-install.ts
28454
- import fs20 from "node:fs";
28455
- import path28 from "node:path";
28829
+ import fs22 from "node:fs";
28830
+ import path30 from "node:path";
28456
28831
  var SYSINTERNALS_TOOLS = [
28457
28832
  "Process Explorer",
28458
28833
  "Process Monitor",
@@ -28473,16 +28848,16 @@ var SYSINTERNALS_TOOLS = [
28473
28848
  "ZoomIt"
28474
28849
  ];
28475
28850
  async function afterInstall(entry, exe) {
28476
- const dir = path28.dirname(exe);
28851
+ const dir = path30.dirname(exe);
28477
28852
  if (entry.needs?.includes("mesa")) {
28478
28853
  const mesa = tool("mesa", arch === "arm64" ? "arm64" : "x64");
28479
28854
  for (const dll of ["opengl32.dll", "libgallium_wgl.dll", "dxil.dll"]) {
28480
- if (exists(path28.join(mesa, dll)) && !exists(path28.join(dir, dll)))
28481
- fs20.copyFileSync(path28.join(mesa, dll), path28.join(dir, dll));
28855
+ if (exists(path30.join(mesa, dll)) && !exists(path30.join(dir, dll)))
28856
+ fs22.copyFileSync(path30.join(mesa, dll), path30.join(dir, dll));
28482
28857
  }
28483
28858
  }
28484
- if (entry.id === "godot" && !exists(path28.join(dir, "._sc_"))) fs20.writeFileSync(path28.join(dir, "._sc_"), "");
28485
- if (entry.id === "sublime") mkdirp(path28.join(dir, "Data"));
28859
+ if (entry.id === "godot" && !exists(path30.join(dir, "._sc_"))) fs22.writeFileSync(path30.join(dir, "._sc_"), "");
28860
+ if (entry.id === "sublime") mkdirp(path30.join(dir, "Data"));
28486
28861
  if (entry.id === "sysinternals") {
28487
28862
  for (const name of SYSINTERNALS_TOOLS) {
28488
28863
  await execAsync(
@@ -28494,47 +28869,14 @@ async function afterInstall(entry, exe) {
28494
28869
  }
28495
28870
  }
28496
28871
 
28497
- // src/guest/apps/find-exe.ts
28498
- import fs21 from "node:fs";
28499
- import path29 from "node:path";
28500
- var MAX_DEPTH = 7;
28501
- var MAX_ENTRIES = 5e4;
28502
- var X64_FOLDER = /(^|[\\/])(amd64|x64|x86_64|win64)([\\/]|$)/i;
28503
- function findExe(root, exe) {
28504
- const match = exeMatcher(exe);
28505
- const best = (hits) => hits.find((h) => X64_FOLDER.test(path29.relative(root, h))) ?? hits[0] ?? null;
28506
- let queue = [root];
28507
- let seen = 0;
28508
- for (let depth = 0; depth < MAX_DEPTH && queue.length; depth++) {
28509
- const next = [];
28510
- const hits = [];
28511
- for (const dir of queue) {
28512
- let entries2;
28513
- try {
28514
- entries2 = fs21.readdirSync(dir, { withFileTypes: true });
28515
- } catch {
28516
- continue;
28517
- }
28518
- const hit = entries2.find((e) => e.isFile() && match(e.name));
28519
- if (hit) hits.push(path29.join(dir, hit.name));
28520
- else
28521
- next.push(...entries2.filter((e) => e.isDirectory() && !e.isSymbolicLink()).map((e) => path29.join(dir, e.name)));
28522
- if ((seen += entries2.length) > MAX_ENTRIES) return best(hits);
28523
- }
28524
- if (hits.length) return best(hits);
28525
- queue = next;
28526
- }
28527
- return null;
28528
- }
28529
-
28530
28872
  // src/guest/apps/shortcuts.ts
28531
28873
  import os5 from "node:os";
28532
- import path30 from "node:path";
28874
+ import path31 from "node:path";
28533
28875
  function shortcuts(apps) {
28534
28876
  if (!apps.length) return;
28535
- const desktop = path30.join(os5.homedir(), "Desktop");
28536
- const start = path30.join(
28537
- process.env.APPDATA ?? path30.join(os5.homedir(), "AppData", "Roaming"),
28877
+ const desktop = path31.join(os5.homedir(), "Desktop");
28878
+ const start = path31.join(
28879
+ process.env.APPDATA ?? path31.join(os5.homedir(), "AppData", "Roaming"),
28538
28880
  "Microsoft",
28539
28881
  "Windows",
28540
28882
  "Start Menu",
@@ -28544,9 +28886,9 @@ function shortcuts(apps) {
28544
28886
  const links = apps.flatMap((app) => {
28545
28887
  const name = app.name.replace(/[\\/:*?"<>|]/g, "");
28546
28888
  return [desktop, start].map((folder) => ({
28547
- link: path30.join(folder, name + ".lnk"),
28889
+ link: path31.join(folder, name + ".lnk"),
28548
28890
  target: app.exe,
28549
- workingDirectory: path30.dirname(app.exe)
28891
+ workingDirectory: path31.dirname(app.exe)
28550
28892
  }));
28551
28893
  });
28552
28894
  runScriptWithJson("new-shortcuts.ps1", "ListFile", links);
@@ -28671,19 +29013,19 @@ async function resolve(entry) {
28671
29013
  }
28672
29014
  if ("url" in s) {
28673
29015
  const name = decodeURIComponent(new URL(s.url).pathname.split("/").pop() || entry.id);
28674
- return { url: s.url, fileName: /\.\w{2,5}$/.test(name) ? name : name + ".zip" };
29016
+ return { url: s.url, fileName: /\.\w{2,5}$/.test(name) ? name : name + ".zip", sha256: s.sha256 };
28675
29017
  }
28676
29018
  if ("resolve" in s && RESOLVERS[s.resolve]) return RESOLVERS[s.resolve]();
28677
29019
  throw new Error("no download source for " + entry.name);
28678
29020
  }
28679
29021
 
28680
29022
  // src/guest/apps/unpack.ts
28681
- import fs22 from "node:fs";
28682
- import path31 from "node:path";
29023
+ import fs23 from "node:fs";
29024
+ import path32 from "node:path";
28683
29025
  var sevenZip = () => tool("apps", "7zip", "7z.exe");
28684
29026
  async function unpack(entry, file3, out, d) {
28685
29027
  if (entry.id === "7zip") {
28686
- const sevenZr = path31.join(path31.dirname(file3), "7zr.exe");
29028
+ const sevenZr = path32.join(path32.dirname(file3), "7zr.exe");
28687
29029
  const zr = await githubAsset("ip7z/7zip", /^7zr\.exe$/);
28688
29030
  await downloader.file(zr.url, sevenZr, zr.sha256);
28689
29031
  await execAsync(sevenZr, ["x", "-y", "-o" + out, file3], { stdio: "ignore" });
@@ -28692,17 +29034,17 @@ async function unpack(entry, file3, out, d) {
28692
29034
  switch (entry.unpack ?? "archive") {
28693
29035
  case "file": {
28694
29036
  mkdirp(out);
28695
- fs22.renameSync(file3, path31.join(out, /^\//.test(entry.exe) ? path31.basename(file3) : entry.exe));
29037
+ fs23.renameSync(file3, path32.join(out, /^\//.test(entry.exe) ? path32.basename(file3) : entry.exe));
28696
29038
  return;
28697
29039
  }
28698
29040
  case "nsis": {
28699
29041
  if (!exists(sevenZip()))
28700
29042
  throw new Error("7-Zip is needed to unpack " + entry.name + " and could not be installed");
28701
- const raw = path31.join(path31.dirname(out), "raw");
29043
+ const raw = path32.join(path32.dirname(out), "raw");
28702
29044
  await execAsync(sevenZip(), ["x", "-y", "-o" + raw, file3], { stdio: "ignore" });
28703
29045
  const inner = findExe(raw, "/^app-64\\.7z$/");
28704
29046
  if (inner) await execAsync(sevenZip(), ["x", "-y", "-o" + out, inner], { stdio: "ignore" });
28705
- else fs22.renameSync(raw, out);
29047
+ else fs23.renameSync(raw, out);
28706
29048
  return;
28707
29049
  }
28708
29050
  case "msi": {
@@ -28752,12 +29094,12 @@ var AppInstaller = class {
28752
29094
  async installOne(entry) {
28753
29095
  try {
28754
29096
  const dest = tool("apps", ...entry.id.split("/"));
28755
- if (!exists(path32.join(dest, MARKER)) || this.options.refresh) await this.download(entry, dest);
29097
+ if (!exists(path33.join(dest, MARKER)) || this.options.refresh) await this.download(entry, dest);
28756
29098
  const exe = findExe(dest, entry.exe);
28757
29099
  if (!exe) throw new Error(entry.exe + " not found in the download");
28758
29100
  await afterInstall(entry, exe);
28759
- if (entry.cli || entry.id === "sysinternals") this.env.prepend.push(path32.dirname(exe));
28760
- if (!entry.cli) this.gui.push({ name: entry.name, exe });
29101
+ if (!entry.service && (entry.cli || entry.id === "sysinternals")) this.env.prepend.push(path33.dirname(exe));
29102
+ if (!entry.cli && !entry.service) this.gui.push({ name: entry.name, exe });
28761
29103
  this.installed.push(entry.name);
28762
29104
  } catch (e) {
28763
29105
  warn(
@@ -28769,10 +29111,13 @@ var AppInstaller = class {
28769
29111
  /** Downloads and unpacks one entry into its folder (replaced in one step: never half there). */
28770
29112
  async download(entry, dest) {
28771
29113
  const work = stagingFolder(dest);
28772
- const out = path32.join(work, "out");
29114
+ const out = path33.join(work, "out");
28773
29115
  try {
28774
- if ("npm" in entry.source) {
28775
- const npmCli = path32.join(path32.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
29116
+ if ("locked" in entry.source) {
29117
+ log("Installing " + entry.name + " " + entry.source.version + " (pinned, every file checked) ...");
29118
+ await installLocked(entry.id, entry.source.locked, mkdirp(out));
29119
+ } else if ("npm" in entry.source) {
29120
+ const npmCli = path33.join(path33.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
28776
29121
  log("Installing " + entry.name + " (npm) ...");
28777
29122
  await execAsync(process.execPath, [
28778
29123
  npmCli,
@@ -28787,17 +29132,60 @@ var AppInstaller = class {
28787
29132
  } else {
28788
29133
  const d = await resolve(entry);
28789
29134
  log("Downloading " + entry.name + " (" + d.fileName + ", ~" + entry.size + " once) ...");
28790
- const file3 = path32.join(mkdirp(work), d.fileName.replace(/[\\/:*?"<>|]/g, "_"));
29135
+ const file3 = path33.join(mkdirp(work), d.fileName.replace(/[\\/:*?"<>|]/g, "_"));
28791
29136
  await downloader.file(d.url, file3, { sha256: d.sha256 ?? null });
28792
29137
  await unpack(entry, file3, out, d);
28793
29138
  }
28794
- fs23.writeFileSync(path32.join(out, MARKER), JSON.stringify({ id: entry.id, at: (/* @__PURE__ */ new Date()).toISOString() }));
29139
+ fs24.writeFileSync(path33.join(out, MARKER), JSON.stringify({ id: entry.id, at: (/* @__PURE__ */ new Date()).toISOString() }));
28795
29140
  moveIntoPlace(out, dest);
28796
29141
  } finally {
28797
- safeRemove(work, path32.dirname(work));
29142
+ safeRemove(work, path33.dirname(work));
28798
29143
  }
28799
29144
  }
28800
29145
  };
29146
+ var LOCKED_PYTHON = "3.13";
29147
+ async function installLocked(id, kind, out) {
29148
+ const lock = path33.join(launchDir, "sandbox", "addons", id);
29149
+ if (kind === "npm") {
29150
+ for (const f of ["package.json", "package-lock.json"]) fs24.copyFileSync(path33.join(lock, f), path33.join(out, f));
29151
+ const npmCli = path33.join(path33.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
29152
+ await execAsync(
29153
+ process.execPath,
29154
+ [npmCli, "ci", "--ignore-scripts", "--no-audit", "--no-fund", "--loglevel=error"],
29155
+ { cwd: out }
29156
+ );
29157
+ return;
29158
+ }
29159
+ const uv = findExe(tool("uv"), "uv.exe");
29160
+ if (!uv) throw new Error("uv is not installed (the uv step failed)");
29161
+ const env = { ...process.env, ...uvVars() };
29162
+ const venv = path33.join(out, "venv");
29163
+ await execAsync(
29164
+ uv,
29165
+ ["venv", "--relocatable", "--python", LOCKED_PYTHON, "--python-preference", "only-managed", venv],
29166
+ {
29167
+ env
29168
+ }
29169
+ );
29170
+ await execAsync(
29171
+ uv,
29172
+ [
29173
+ "pip",
29174
+ "install",
29175
+ "--python",
29176
+ path33.join(venv, "Scripts", "python.exe"),
29177
+ "--require-hashes",
29178
+ "--no-deps",
29179
+ "--only-binary",
29180
+ ":all:",
29181
+ "--link-mode",
29182
+ "copy",
29183
+ "-r",
29184
+ path33.join(lock, "requirements.txt")
29185
+ ],
29186
+ { env }
29187
+ );
29188
+ }
28801
29189
  async function setupApps(env, apps, o) {
28802
29190
  const { entries: entries2, skipped } = appsToInstall(apps, o.gitAccess);
28803
29191
  for (const s of skipped) env.summary.push("app skipped: " + s);
@@ -28809,8 +29197,8 @@ async function setupApps(env, apps, o) {
28809
29197
  }
28810
29198
 
28811
29199
  // src/guest/toolchains/detect.ts
28812
- import fs24 from "node:fs";
28813
- import path33 from "node:path";
29200
+ import fs25 from "node:fs";
29201
+ import path34 from "node:path";
28814
29202
 
28815
29203
  // src/core/detect/versions.ts
28816
29204
  function pickNodeVersion(releases, spec) {
@@ -28942,14 +29330,14 @@ function workspaceEntries() {
28942
29330
  if (known) return known;
28943
29331
  const list = (dir) => {
28944
29332
  try {
28945
- return fs24.readdirSync(dir, { withFileTypes: true });
29333
+ return fs25.readdirSync(dir, { withFileTypes: true });
28946
29334
  } catch {
28947
29335
  return [];
28948
29336
  }
28949
29337
  };
28950
29338
  const top = list(workspace2);
28951
29339
  const below = top.filter((d) => d.isDirectory() && !d.name.startsWith(".") && !SKIP_DIRS.has(d.name.toLowerCase())).flatMap(
28952
- (d) => list(path33.join(workspace2, d.name)).filter((f) => f.isFile()).map((f) => d.name + "/" + f.name)
29340
+ (d) => list(path34.join(workspace2, d.name)).filter((f) => f.isFile()).map((f) => d.name + "/" + f.name)
28953
29341
  );
28954
29342
  const found = [...top.filter((d) => d.isFile()).map((d) => d.name), ...below];
28955
29343
  entries.set(workspace2.toLowerCase(), found);
@@ -28960,9 +29348,9 @@ function wanted(setting, toolchain) {
28960
29348
  }
28961
29349
  function versionFromFiles(files, parse5 = (t) => specFromVersionFile(t)) {
28962
29350
  for (const name of files) {
28963
- const file3 = path33.join(projectWorkspace(), name);
29351
+ const file3 = path34.join(projectWorkspace(), name);
28964
29352
  if (!exists(file3)) continue;
28965
- const spec = parse5(fs24.readFileSync(file3, "utf8"), name);
29353
+ const spec = parse5(fs25.readFileSync(file3, "utf8"), name);
28966
29354
  if (spec) return { spec, source: name };
28967
29355
  }
28968
29356
  return null;
@@ -29035,22 +29423,22 @@ var DatabaseServer = class {
29035
29423
  };
29036
29424
 
29037
29425
  // src/guest/databases/dynamodb.ts
29038
- import fs29 from "node:fs";
29039
- import path36 from "node:path";
29426
+ import fs30 from "node:fs";
29427
+ import path37 from "node:path";
29040
29428
  import { spawn as spawn3 } from "node:child_process";
29041
29429
 
29042
29430
  // src/guest/toolchains/languages/java.ts
29043
- import fs27 from "node:fs";
29044
- import path34 from "node:path";
29431
+ import fs28 from "node:fs";
29432
+ import path35 from "node:path";
29045
29433
 
29046
29434
  // src/guest/toolchains/version-cache.ts
29047
- import fs26 from "node:fs";
29435
+ import fs27 from "node:fs";
29048
29436
 
29049
29437
  // src/guest/toolchains/state-file.ts
29050
- import fs25 from "node:fs";
29438
+ import fs26 from "node:fs";
29051
29439
  function readState(file3, schema, empty) {
29052
29440
  try {
29053
- return schema.safeParse(JSON.parse(fs25.readFileSync(file3, "utf8").replace(/^\uFEFF/, ""))).data ?? empty;
29441
+ return schema.safeParse(JSON.parse(fs26.readFileSync(file3, "utf8").replace(/^\uFEFF/, ""))).data ?? empty;
29054
29442
  } catch {
29055
29443
  return empty;
29056
29444
  }
@@ -29085,7 +29473,7 @@ var VersionCache = class {
29085
29473
  return cached2.value;
29086
29474
  try {
29087
29475
  const value = await resolve2();
29088
- fs26.mkdirSync(toolsDir, { recursive: true });
29476
+ fs27.mkdirSync(toolsDir, { recursive: true });
29089
29477
  writeJsonAtomic(this.file(), { ...this.remembered(), [key2]: { value, at: Date.now() } });
29090
29478
  return value;
29091
29479
  } catch (e) {
@@ -29138,7 +29526,7 @@ var setupJdk = async (env, config2) => {
29138
29526
  const { major, source } = await javaMajor(config2);
29139
29527
  const release = await resolveCached("java:" + major, async () => (await temurin(major)).release_name);
29140
29528
  const home = tool("java", release.replace(/[^A-Za-z0-9.-]/g, "_"));
29141
- if (!exists(path34.join(home, "bin", "java.exe"))) {
29529
+ if (!exists(path35.join(home, "bin", "java.exe"))) {
29142
29530
  const asset = await temurin(major);
29143
29531
  log("Downloading Java " + release + " (JDK, ~200 MB once) ...");
29144
29532
  await downloader.archive(asset.binary.package.link, home, {
@@ -29148,13 +29536,13 @@ var setupJdk = async (env, config2) => {
29148
29536
  });
29149
29537
  }
29150
29538
  env.vars.JAVA_HOME = home;
29151
- env.prepend.push(path34.join(home, "bin"));
29539
+ env.prepend.push(path35.join(home, "bin"));
29152
29540
  env.summary.push("java: " + release + " (Temurin, from " + source + ")");
29153
29541
  };
29154
29542
  async function ensureJdk() {
29155
29543
  const root = tool("java");
29156
- const installed = exists(root) ? fs27.readdirSync(root).filter((d) => exists(path34.join(root, d, "bin", "java.exe"))).sort((a, b) => b.localeCompare(a, void 0, { numeric: true })) : [];
29157
- if (installed.length) return path34.join(root, installed[0]);
29544
+ const installed = exists(root) ? fs28.readdirSync(root).filter((d) => exists(path35.join(root, d, "bin", "java.exe"))).sort((a, b) => b.localeCompare(a, void 0, { numeric: true })) : [];
29545
+ if (installed.length) return path35.join(root, installed[0]);
29158
29546
  const scratch = emptyEnv();
29159
29547
  await setupJdk(scratch, { enabled: true, version: "lts", maven: false, gradle: false });
29160
29548
  return scratch.vars.JAVA_HOME;
@@ -29167,7 +29555,7 @@ var setupMaven = async (env) => {
29167
29555
  return pickPackageVersion(versions, "3") ?? Promise.reject(new Error("No Maven 3 release found"));
29168
29556
  });
29169
29557
  const home = tool("maven", version2);
29170
- if (!exists(path34.join(home, "bin", "mvn.cmd"))) {
29558
+ if (!exists(path35.join(home, "bin", "mvn.cmd"))) {
29171
29559
  const url2 = MAVEN + version2 + "/apache-maven-" + version2 + "-bin.zip";
29172
29560
  log("Downloading Maven " + version2 + " (~10 MB once) ...");
29173
29561
  await downloader.archive(url2, home, {
@@ -29177,7 +29565,7 @@ var setupMaven = async (env) => {
29177
29565
  }
29178
29566
  const repo = tool("maven-repository");
29179
29567
  Object.assign(env.vars, { MAVEN_HOME: home, M2_HOME: home, MAVEN_OPTS: "-Dmaven.repo.local=" + repo });
29180
- env.prepend.push(path34.join(home, "bin"));
29568
+ env.prepend.push(path35.join(home, "bin"));
29181
29569
  env.summary.push("maven: " + version2);
29182
29570
  };
29183
29571
  var setupGradle = async (env) => {
@@ -29189,7 +29577,7 @@ var setupGradle = async (env) => {
29189
29577
  async () => current?.version ?? Promise.reject(new Error("Gradle version lookup failed"))
29190
29578
  );
29191
29579
  const home = tool("gradle", version2);
29192
- if (!exists(path34.join(home, "bin", "gradle.bat"))) {
29580
+ if (!exists(path35.join(home, "bin", "gradle.bat"))) {
29193
29581
  if (!current || current.version !== version2)
29194
29582
  throw new Error("Gradle " + version2 + " is not installed and cannot be downloaded right now");
29195
29583
  log("Downloading Gradle " + version2 + " (~130 MB once) ...");
@@ -29200,13 +29588,13 @@ var setupGradle = async (env) => {
29200
29588
  });
29201
29589
  }
29202
29590
  const userHome = tool("gradle-home");
29203
- fs27.mkdirSync(userHome, { recursive: true });
29591
+ fs28.mkdirSync(userHome, { recursive: true });
29204
29592
  Object.assign(env.vars, {
29205
29593
  GRADLE_HOME: home,
29206
29594
  GRADLE_USER_HOME: userHome,
29207
29595
  GRADLE_OPTS: "-Dorg.gradle.vfs.watch=false"
29208
29596
  });
29209
- env.prepend.push(path34.join(home, "bin"));
29597
+ env.prepend.push(path35.join(home, "bin"));
29210
29598
  env.summary.push("gradle: " + version2);
29211
29599
  };
29212
29600
  async function setupJava(env, config2) {
@@ -29230,29 +29618,29 @@ async function setupJava(env, config2) {
29230
29618
  }
29231
29619
 
29232
29620
  // src/guest/databases/server.ts
29233
- import fs28 from "node:fs";
29234
- import path35 from "node:path";
29621
+ import fs29 from "node:fs";
29622
+ import path36 from "node:path";
29235
29623
  import { spawn as spawn2 } from "node:child_process";
29236
29624
  function dataFor(name) {
29237
- return path35.join(dataDir, "databases", name);
29625
+ return path36.join(dataDir, "databases", name);
29238
29626
  }
29239
29627
  async function startServer(name, exe, args, port) {
29240
29628
  if (await portOpen(port)) throw new Error("port " + port + " is already in use (change databases." + name + ".port)");
29241
- const logFile2 = path35.join(logsDir, name + ".log");
29242
- const out = fs28.openSync(logFile2, "a");
29629
+ const logFile2 = path36.join(logsDir, name + ".log");
29630
+ const out = fs29.openSync(logFile2, "a");
29243
29631
  spawn2(exe, args, { detached: true, stdio: ["ignore", out, out], windowsHide: true }).unref();
29244
- fs28.closeSync(out);
29632
+ fs29.closeSync(out);
29245
29633
  if (!await waitForPort(port, 60))
29246
29634
  throw new Error(name + " did not start listening on port " + port + "; see " + logFile2);
29247
29635
  }
29248
29636
  async function clearStaleLocks(dir, files, port) {
29249
29637
  if (await portOpen(port)) return;
29250
- for (const f of files) fs28.rmSync(path35.join(dir, f), { force: true });
29638
+ for (const f of files) fs29.rmSync(path36.join(dir, f), { force: true });
29251
29639
  }
29252
29640
  async function createOnce(data, marker, create) {
29253
- if (exists(path35.join(data, marker))) return;
29641
+ if (exists(path36.join(data, marker))) return;
29254
29642
  const existed = exists(data);
29255
- mkdirp(path35.dirname(data));
29643
+ mkdirp(path36.dirname(data));
29256
29644
  try {
29257
29645
  await create();
29258
29646
  } catch (e) {
@@ -29276,9 +29664,9 @@ var DynamoServer = class extends DatabaseServer {
29276
29664
  * local endpoint up from AWS_ENDPOINT_URL_DYNAMODB, with dummy credentials (real ones never reach the sandbox).
29277
29665
  */
29278
29666
  async start(env, plan2) {
29279
- const java = path36.join(await ensureJdk(), "bin", "java.exe");
29667
+ const java = path37.join(await ensureJdk(), "bin", "java.exe");
29280
29668
  const home = tool("dynamodb-local");
29281
- if (!exists(path36.join(home, "DynamoDBLocal.jar"))) {
29669
+ if (!exists(path37.join(home, "DynamoDBLocal.jar"))) {
29282
29670
  log("Downloading DynamoDB Local (~60 MB once) ...");
29283
29671
  const sha256 = (await fetchText(DYNAMO_ZIP + ".sha256")).trim().split(/\s+/)[0];
29284
29672
  await downloader.archive(DYNAMO_ZIP, home, {
@@ -29292,9 +29680,9 @@ var DynamoServer = class extends DatabaseServer {
29292
29680
  "dynamodb",
29293
29681
  java,
29294
29682
  [
29295
- "-Djava.library.path=" + path36.join(home, "DynamoDBLocal_lib"),
29683
+ "-Djava.library.path=" + path37.join(home, "DynamoDBLocal_lib"),
29296
29684
  "-jar",
29297
- path36.join(home, "DynamoDBLocal.jar"),
29685
+ path37.join(home, "DynamoDBLocal.jar"),
29298
29686
  "-sharedDb",
29299
29687
  "-dbPath",
29300
29688
  data,
@@ -29318,10 +29706,10 @@ var DynamoServer = class extends DatabaseServer {
29318
29706
  );
29319
29707
  if (!plan2.adminPort) return;
29320
29708
  const prefix = tool("dynamodb-admin");
29321
- const script = path36.join(prefix, "node_modules", "dynamodb-admin", "dist", "dynamodb-admin.js");
29709
+ const script = path37.join(prefix, "node_modules", "dynamodb-admin", "dist", "dynamodb-admin.js");
29322
29710
  if (!exists(script)) {
29323
29711
  log("Installing dynamodb-admin, the DynamoDB web UI (~20 MB once) ...");
29324
- const npmCli = path36.join(path36.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
29712
+ const npmCli = path37.join(path37.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
29325
29713
  await execAsync(
29326
29714
  process.execPath,
29327
29715
  [npmCli, "install", "--global", "--prefix", prefix, "dynamodb-admin", "--no-fund", "--no-audit"],
@@ -29334,15 +29722,15 @@ var DynamoServer = class extends DatabaseServer {
29334
29722
  );
29335
29723
  return;
29336
29724
  }
29337
- const out = fs29.openSync(path36.join(logsDir, "dynamodb-admin.log"), "a");
29725
+ const out = fs30.openSync(path37.join(logsDir, "dynamodb-admin.log"), "a");
29338
29726
  spawn3(
29339
29727
  process.execPath,
29340
29728
  [script, "--port", String(plan2.adminPort), "--dynamo-endpoint", endpoint, "--skip-default-credentials"],
29341
29729
  { detached: true, stdio: ["ignore", out, out], windowsHide: true, env: { ...process.env, ...aws } }
29342
29730
  ).unref();
29343
- fs29.closeSync(out);
29731
+ fs30.closeSync(out);
29344
29732
  if (!await waitForPort(plan2.adminPort, 30))
29345
- throw new Error("dynamodb-admin did not start; see " + path36.join(logsDir, "dynamodb-admin.log"));
29733
+ throw new Error("dynamodb-admin did not start; see " + path37.join(logsDir, "dynamodb-admin.log"));
29346
29734
  env.summary.push(
29347
29735
  "dynamodb-admin: http://localhost:" + plan2.adminPort + " (browse and edit tables in the sandbox's browser)"
29348
29736
  );
@@ -29350,32 +29738,7 @@ var DynamoServer = class extends DatabaseServer {
29350
29738
  };
29351
29739
 
29352
29740
  // src/guest/databases/mongodb.ts
29353
- import path37 from "node:path";
29354
-
29355
- // src/guest/toolchains/install.ts
29356
- import fs30 from "node:fs";
29357
- async function downloadFile(url2, file3, sha256 = null) {
29358
- const part = file3 + ".part";
29359
- try {
29360
- await downloader.file(url2, part, sha256);
29361
- fs30.rmSync(file3, { force: true });
29362
- fs30.renameSync(part, file3);
29363
- } finally {
29364
- fs30.rmSync(part, { force: true });
29365
- }
29366
- }
29367
- async function installFromGithub(repo, pattern, o) {
29368
- const asset = await githubAsset(repo, pattern, o.searchOlder);
29369
- log("Downloading " + (typeof o.what === "string" ? o.what : o.what(asset)) + " ...");
29370
- await downloader.archive(asset.url, o.dest, {
29371
- sha256: asset.sha256,
29372
- fileName: asset.name,
29373
- ...o.locate ? { locate: o.locate } : {},
29374
- ...o.up ? { up: o.up } : {}
29375
- });
29376
- }
29377
-
29378
- // src/guest/databases/mongodb.ts
29741
+ import path38 from "node:path";
29379
29742
  var MongoServer = class extends DatabaseServer {
29380
29743
  id = "mongodb";
29381
29744
  async start(env, plan2) {
@@ -29390,13 +29753,13 @@ var MongoServer = class extends DatabaseServer {
29390
29753
  };
29391
29754
  const version2 = await resolveCached("mongodb:" + plan2.version, async () => (await find()).version);
29392
29755
  const home = tool("mongodb", version2);
29393
- if (!exists(path37.join(home, "bin", "mongod.exe"))) {
29756
+ if (!exists(path38.join(home, "bin", "mongod.exe"))) {
29394
29757
  const { archive } = await find();
29395
29758
  log("Downloading MongoDB " + version2 + " (~400 MB once) ...");
29396
29759
  await downloader.archive(archive.url, home, { sha256: archive.sha256, locate: "mongod.exe", up: 1 });
29397
29760
  }
29398
29761
  const shell = tool("mongosh");
29399
- if (!exists(path37.join(shell, "mongosh.exe"))) {
29762
+ if (!exists(path38.join(shell, "mongosh.exe"))) {
29400
29763
  await installFromGithub("mongodb-js/mongosh", /^mongosh-[\d.]+-win32-x64\.zip$/, {
29401
29764
  dest: shell,
29402
29765
  what: "mongosh, the MongoDB shell (~50 MB once)",
@@ -29407,17 +29770,17 @@ var MongoServer = class extends DatabaseServer {
29407
29770
  await clearStaleLocks(data, ["mongod.lock"], plan2.port);
29408
29771
  await startServer(
29409
29772
  "mongodb",
29410
- path37.join(home, "bin", "mongod.exe"),
29773
+ path38.join(home, "bin", "mongod.exe"),
29411
29774
  ["--dbpath", data, "--bind_ip", "127.0.0.1", "--port", String(plan2.port)],
29412
29775
  plan2.port
29413
29776
  );
29414
- env.prepend.push(path37.join(home, "bin"), shell);
29777
+ env.prepend.push(path38.join(home, "bin"), shell);
29415
29778
  env.summary.push("mongodb " + version2 + ": mongodb://127.0.0.1:" + plan2.port + " (mongosh on PATH)");
29416
29779
  }
29417
29780
  };
29418
29781
 
29419
29782
  // src/guest/databases/mysql.ts
29420
- import path38 from "node:path";
29783
+ import path39 from "node:path";
29421
29784
  var MARIA = "https://downloads.mariadb.org/rest-api/mariadb/";
29422
29785
  var MysqlServer = class extends DatabaseServer {
29423
29786
  id = "mysql";
@@ -29435,8 +29798,8 @@ var MysqlServer = class extends DatabaseServer {
29435
29798
  };
29436
29799
  const version2 = await resolveCached("mariadb-version:" + series, async () => (await latest()).version);
29437
29800
  const home = tool("mariadb", version2);
29438
- const bin = path38.join(home, "bin");
29439
- if (!exists(path38.join(bin, "mariadbd.exe"))) {
29801
+ const bin = path39.join(home, "bin");
29802
+ if (!exists(path39.join(bin, "mariadbd.exe"))) {
29440
29803
  const { zip } = await latest();
29441
29804
  log("Downloading MariaDB " + version2 + " (~90 MB once) ...");
29442
29805
  await downloader.archive(zip.file_download_url.replace(/^http:/, "https:"), home, {
@@ -29449,13 +29812,13 @@ var MysqlServer = class extends DatabaseServer {
29449
29812
  const data = dataFor("mariadb");
29450
29813
  await createOnce(data, "mysql", async () => {
29451
29814
  log("Creating the MariaDB database (first start) ...");
29452
- await execAsync(path38.join(bin, "mariadb-install-db.exe"), ["--datadir=" + data, "--port=" + plan2.port], {
29815
+ await execAsync(path39.join(bin, "mariadb-install-db.exe"), ["--datadir=" + data, "--port=" + plan2.port], {
29453
29816
  stdio: "ignore"
29454
29817
  });
29455
29818
  });
29456
29819
  await startServer(
29457
29820
  "mysql",
29458
- path38.join(bin, "mariadbd.exe"),
29821
+ path39.join(bin, "mariadbd.exe"),
29459
29822
  ["--datadir=" + data, "--port=" + plan2.port, "--bind-address=127.0.0.1", "--console"],
29460
29823
  plan2.port
29461
29824
  );
@@ -29469,7 +29832,7 @@ var MysqlServer = class extends DatabaseServer {
29469
29832
 
29470
29833
  // src/guest/databases/postgres.ts
29471
29834
  import fs31 from "node:fs";
29472
- import path39 from "node:path";
29835
+ import path40 from "node:path";
29473
29836
  function postgresData(version2) {
29474
29837
  return dataFor("postgres-" + version2.split(".")[0]);
29475
29838
  }
@@ -29484,8 +29847,8 @@ var PostgresServer = class extends DatabaseServer {
29484
29847
  return hit ? hit.major + "." + hit.latestMinor : Promise.reject(new Error("No supported PostgreSQL " + plan2.version));
29485
29848
  });
29486
29849
  const home = tool("postgres", exact);
29487
- const bin = path39.join(home, "bin");
29488
- if (!exists(path39.join(bin, "pg_ctl.exe"))) {
29850
+ const bin = path40.join(home, "bin");
29851
+ if (!exists(path40.join(bin, "pg_ctl.exe"))) {
29489
29852
  log("Downloading PostgreSQL " + exact + " (~330 MB once) ...");
29490
29853
  await downloader.archive(
29491
29854
  "https://get.enterprisedb.com/postgresql/postgresql-" + exact + "-1-windows-x64-binaries.zip",
@@ -29502,7 +29865,7 @@ var PostgresServer = class extends DatabaseServer {
29502
29865
  await createOnce(data, "PG_VERSION", async () => {
29503
29866
  log("Creating the PostgreSQL database (first start) ...");
29504
29867
  await execAsync(
29505
- path39.join(bin, "initdb.exe"),
29868
+ path40.join(bin, "initdb.exe"),
29506
29869
  ["-D", data, "-U", "postgres", "-A", "trust", "-E", "UTF8", "--locale=C"],
29507
29870
  { stdio: "ignore" }
29508
29871
  );
@@ -29511,7 +29874,7 @@ var PostgresServer = class extends DatabaseServer {
29511
29874
  throw new Error("port " + plan2.port + " is already in use (change databases.postgres.port)");
29512
29875
  await clearStaleLocks(data, ["postmaster.pid"], plan2.port);
29513
29876
  await execAsync(
29514
- path39.join(bin, "pg_ctl.exe"),
29877
+ path40.join(bin, "pg_ctl.exe"),
29515
29878
  [
29516
29879
  "start",
29517
29880
  "-D",
@@ -29520,7 +29883,7 @@ var PostgresServer = class extends DatabaseServer {
29520
29883
  "-t",
29521
29884
  "60",
29522
29885
  "-l",
29523
- path39.join(logsDir, "postgres.log"),
29886
+ path40.join(logsDir, "postgres.log"),
29524
29887
  "-o",
29525
29888
  "-p " + plan2.port + " -c listen_addresses=localhost"
29526
29889
  ],
@@ -29537,8 +29900,8 @@ var PostgresServer = class extends DatabaseServer {
29537
29900
  if (exists(tool("postgres"))) {
29538
29901
  for (const version2 of fs31.readdirSync(tool("postgres"))) {
29539
29902
  const data = postgresData(version2);
29540
- const pgCtl = path39.join(tool("postgres", version2), "bin", "pg_ctl.exe");
29541
- if (exists(path39.join(data, "postmaster.pid")) && exists(pgCtl))
29903
+ const pgCtl = path40.join(tool("postgres", version2), "bin", "pg_ctl.exe");
29904
+ if (exists(path40.join(data, "postmaster.pid")) && exists(pgCtl))
29542
29905
  await execAsync(pgCtl, ["stop", "-D", data, "-m", "fast", "-w"], { stdio: "ignore" });
29543
29906
  }
29544
29907
  }
@@ -29547,12 +29910,12 @@ var PostgresServer = class extends DatabaseServer {
29547
29910
  };
29548
29911
 
29549
29912
  // src/guest/databases/redis.ts
29550
- import path40 from "node:path";
29913
+ import path41 from "node:path";
29551
29914
  var RedisServer = class extends DatabaseServer {
29552
29915
  id = "redis";
29553
29916
  async start(env, plan2) {
29554
29917
  const home = tool("redis");
29555
- if (!exists(path40.join(home, "redis-server.exe"))) {
29918
+ if (!exists(path41.join(home, "redis-server.exe"))) {
29556
29919
  await installFromGithub("redis-windows/redis-windows", /^Redis-[\d.]+-Windows-x64-msys2\.zip$/, {
29557
29920
  dest: home,
29558
29921
  what: (a) => "Redis (" + a.name + ", ~10 MB once)",
@@ -29562,7 +29925,7 @@ var RedisServer = class extends DatabaseServer {
29562
29925
  const data = mkdirp(dataFor("redis"));
29563
29926
  await startServer(
29564
29927
  "redis",
29565
- path40.join(home, "redis-server.exe"),
29928
+ path41.join(home, "redis-server.exe"),
29566
29929
  ["--port", String(plan2.port), "--bind", "127.0.0.1", "--dir", data.replace(/\\/g, "/"), "--appendonly", "yes"],
29567
29930
  plan2.port
29568
29931
  );
@@ -29601,15 +29964,15 @@ async function setupDatabase(env, plan2) {
29601
29964
 
29602
29965
  // src/guest/toolchains/tools/git.ts
29603
29966
  import fs33 from "node:fs";
29604
- import path42 from "node:path";
29967
+ import path43 from "node:path";
29605
29968
 
29606
29969
  // src/guest/runtime/csc.ts
29607
29970
  import fs32 from "node:fs";
29608
29971
  import os6 from "node:os";
29609
- import path41 from "node:path";
29972
+ import path42 from "node:path";
29610
29973
  import crypto3 from "node:crypto";
29611
29974
  import { spawnSync as spawnSync5 } from "node:child_process";
29612
- var CSC = path41.join(process.env.WINDIR ?? "C:\\Windows", "Microsoft.NET", "Framework64", "v4.0.30319", "csc.exe");
29975
+ var CSC = path42.join(process.env.WINDIR ?? "C:\\Windows", "Microsoft.NET", "Framework64", "v4.0.30319", "csc.exe");
29613
29976
  var SOURCES = {
29614
29977
  ptyhost: "PtyHost.cs",
29615
29978
  gitshim: "GitShim.cs",
@@ -29621,17 +29984,17 @@ var SOURCES = {
29621
29984
  memgov: "MemGov.cs"
29622
29985
  };
29623
29986
  function sourceOf(name) {
29624
- return path41.join(launchDir, "sandbox", "csharp", SOURCES[name]);
29987
+ return path42.join(launchDir, "sandbox", "csharp", SOURCES[name]);
29625
29988
  }
29626
29989
  function sourceHash(name) {
29627
29990
  return crypto3.createHash("sha256").update(fs32.readFileSync(sourceOf(name))).digest("hex").slice(0, 12);
29628
29991
  }
29629
29992
  function ensureCompiled(name) {
29630
- const exe = path41.join(os6.tmpdir(), "agent-sandbox", name + "-" + sourceHash(name) + ".exe");
29993
+ const exe = path42.join(os6.tmpdir(), "agent-sandbox", name + "-" + sourceHash(name) + ".exe");
29631
29994
  if (exists(exe)) return exe;
29632
29995
  if (!exists(CSC))
29633
29996
  throw new Error("The .NET Framework C# compiler was not found at " + CSC + " (it ships with Windows 10/11).");
29634
- mkdirp(path41.dirname(exe));
29997
+ mkdirp(path42.dirname(exe));
29635
29998
  const result = spawnSync5(CSC, ["/nologo", "/optimize", "/target:exe", "/out:" + exe, sourceOf(name)], {
29636
29999
  encoding: "utf8",
29637
30000
  windowsHide: true
@@ -29670,18 +30033,18 @@ var installPortableGit = async (root) => {
29670
30033
  };
29671
30034
  var installGuard = (env, access, realGit) => {
29672
30035
  const dir = mkdirp(tool("gitguard"));
29673
- const guard = path42.join(launchDir, "runtime", "gitguard.js");
29674
- if (duringSession() && (exists(path42.join(dir, "git.exe")) || exists(path42.join(dir, "git.cmd")))) {
30036
+ const guard = path43.join(launchDir, "runtime", "gitguard.js");
30037
+ if (duringSession() && (exists(path43.join(dir, "git.exe")) || exists(path43.join(dir, "git.cmd")))) {
29675
30038
  } else
29676
30039
  try {
29677
- fs33.copyFileSync(ensureCompiled("gitshim"), path42.join(dir, "git.exe"));
29678
- fs33.rmSync(path42.join(dir, "git.cmd"), { force: true });
30040
+ fs33.copyFileSync(ensureCompiled("gitshim"), path43.join(dir, "git.exe"));
30041
+ fs33.rmSync(path43.join(dir, "git.cmd"), { force: true });
29679
30042
  } catch (e) {
29680
30043
  warn("git guard: falling back to git.cmd (" + errMsg(e) + ")");
29681
- fs33.writeFileSync(path42.join(dir, "git.cmd"), '@"' + process.execPath + '" "' + guard + '" %*\r\n');
30044
+ fs33.writeFileSync(path43.join(dir, "git.cmd"), '@"' + process.execPath + '" "' + guard + '" %*\r\n');
29682
30045
  }
29683
30046
  fs33.writeFileSync(
29684
- path42.join(dir, "bashenv.sh"),
30047
+ path43.join(dir, "bashenv.sh"),
29685
30048
  'git() { "' + posix(process.execPath) + '" "' + posix(guard) + '" "$@"; }\nexport -f git\n'
29686
30049
  );
29687
30050
  env.prepend.unshift(dir);
@@ -29690,13 +30053,13 @@ var installGuard = (env, access, realGit) => {
29690
30053
  AGENT_REAL_GIT: access === "readonly" ? realGit : "",
29691
30054
  AGENT_GUARD_NODE: process.execPath,
29692
30055
  AGENT_GUARD_SCRIPT: guard,
29693
- BASH_ENV: posix(path42.join(dir, "bashenv.sh"))
30056
+ BASH_ENV: posix(path43.join(dir, "bashenv.sh"))
29694
30057
  });
29695
30058
  if (access === "readonly") {
29696
- const hooks = mkdirp(path42.join(dir, "hooks"));
30059
+ const hooks = mkdirp(path43.join(dir, "hooks"));
29697
30060
  for (const hook of BLOCKED_HOOKS)
29698
30061
  fs33.writeFileSync(
29699
- path42.join(hooks, hook),
30062
+ path43.join(hooks, hook),
29700
30063
  '#!/bin/sh\necho "git is read-only in this sandbox: ' + hook + ' refused." >&2\nexit 1\n'
29701
30064
  );
29702
30065
  Object.assign(env.vars, {
@@ -29705,12 +30068,12 @@ var installGuard = (env, access, realGit) => {
29705
30068
  GIT_CONFIG_VALUE_0: posix(hooks)
29706
30069
  });
29707
30070
  } else {
29708
- env.vars.GIT_DIR = path42.join(dataDir, "git-disabled");
30071
+ env.vars.GIT_DIR = path43.join(dataDir, "git-disabled");
29709
30072
  }
29710
30073
  };
29711
30074
  var pointAtOwnCertificates = (root) => {
29712
- const config2 = path42.join(root, "etc", "gitconfig");
29713
- const bundle = path42.join(root, "mingw64", "etc", "ssl", "certs", "ca-bundle.crt");
30075
+ const config2 = path43.join(root, "etc", "gitconfig");
30076
+ const bundle = path43.join(root, "mingw64", "etc", "ssl", "certs", "ca-bundle.crt");
29714
30077
  if (!exists(config2) || !exists(bundle)) return;
29715
30078
  const text = fs33.readFileSync(config2, "utf8");
29716
30079
  const fixed = text.replace(/^(\s*sslCAInfo\s*=\s*).*$/gim, (_m, key2) => key2 + posix(bundle));
@@ -29718,9 +30081,9 @@ var pointAtOwnCertificates = (root) => {
29718
30081
  };
29719
30082
  async function setupGit(env, hostCopy, refresh, access) {
29720
30083
  const root = tool("git");
29721
- const bash = path42.join(root, "bin", "bash.exe");
29722
- const gitExe = path42.join(root, "cmd", "git.exe");
29723
- const hostGit = path42.join(hostToolsDir, "git");
30084
+ const bash = path43.join(root, "bin", "bash.exe");
30085
+ const gitExe = path43.join(root, "cmd", "git.exe");
30086
+ const hostGit = path43.join(hostToolsDir, "git");
29724
30087
  if (exists(root) && !exists(bash)) {
29725
30088
  log("Replacing the old MinGit (no bash) with full Git for Windows.");
29726
30089
  safeRemove(root, toolsDir);
@@ -29734,11 +30097,11 @@ async function setupGit(env, hostCopy, refresh, access) {
29734
30097
  }
29735
30098
  const work = stagingFolder(root);
29736
30099
  try {
29737
- if (hostCopy && exists(path42.join(hostGit, "bin", "bash.exe"))) {
30100
+ if (hostCopy && exists(path43.join(hostGit, "bin", "bash.exe"))) {
29738
30101
  log("Copying Git for Windows from this PC ...");
29739
30102
  robocopy(hostGit, work);
29740
30103
  } else await installPortableGit(work);
29741
- if (!exists(path42.join(work, "bin", "bash.exe"))) throw new Error("bash.exe missing after installing Git");
30104
+ if (!exists(path43.join(work, "bin", "bash.exe"))) throw new Error("bash.exe missing after installing Git");
29742
30105
  moveIntoPlace(work, root);
29743
30106
  } finally {
29744
30107
  safeRemove(work, toolsDir);
@@ -29747,10 +30110,10 @@ async function setupGit(env, hostCopy, refresh, access) {
29747
30110
  if (!exists(bash)) throw new Error("bash.exe missing after installing Git");
29748
30111
  pointAtOwnCertificates(root);
29749
30112
  env.vars.CLAUDE_CODE_GIT_BASH_PATH = bash;
29750
- env.append.push(path42.join(root, "usr", "bin"));
30113
+ env.append.push(path43.join(root, "usr", "bin"));
29751
30114
  const version2 = execOutput(gitExe, ["--version"]).replace(/^git version /, "");
29752
30115
  if (access === "full") {
29753
- env.prepend.push(path42.join(root, "cmd"));
30116
+ env.prepend.push(path43.join(root, "cmd"));
29754
30117
  env.summary.push("git: " + version2);
29755
30118
  return;
29756
30119
  }
@@ -29762,7 +30125,7 @@ async function setupGit(env, hostCopy, refresh, access) {
29762
30125
 
29763
30126
  // src/guest/toolchains/languages/go.ts
29764
30127
  import fs34 from "node:fs";
29765
- import path43 from "node:path";
30128
+ import path44 from "node:path";
29766
30129
 
29767
30130
  // src/guest/toolchains/package-ledger.ts
29768
30131
  var LedgerSchema = external_exports.record(external_exports.string(), external_exports.string());
@@ -29816,7 +30179,7 @@ async function setupGo(env, config2) {
29816
30179
  }
29817
30180
  const version2 = await resolveCached("go:" + config2.version, async () => (await findRelease(config2.version)).version);
29818
30181
  const root = tool("go", version2);
29819
- if (!exists(path43.join(root, "bin", "go.exe"))) {
30182
+ if (!exists(path44.join(root, "bin", "go.exe"))) {
29820
30183
  const goArch = arch === "arm64" ? "arm64" : "amd64";
29821
30184
  const file3 = (await findRelease(version2)).files.find(
29822
30185
  (f) => f.os === "windows" && f.arch === goArch && f.kind === "archive"
@@ -29833,16 +30196,16 @@ async function setupGo(env, config2) {
29833
30196
  const vars = {
29834
30197
  GOROOT: root,
29835
30198
  GOPATH,
29836
- GOBIN: path43.join(GOPATH, "bin"),
29837
- GOMODCACHE: path43.join(GOPATH, "pkg", "mod"),
30199
+ GOBIN: path44.join(GOPATH, "bin"),
30200
+ GOMODCACHE: path44.join(GOPATH, "pkg", "mod"),
29838
30201
  GOCACHE: tool("go-cache"),
29839
30202
  GOTOOLCHAIN: "auto",
29840
30203
  GOTELEMETRY: "off"
29841
30204
  };
29842
30205
  fs34.mkdirSync(vars.GOBIN, { recursive: true });
29843
30206
  Object.assign(env.vars, vars);
29844
- env.prepend.push(path43.join(root, "bin"), vars.GOBIN);
29845
- const go = path43.join(root, "bin", "go.exe");
30207
+ env.prepend.push(path44.join(root, "bin"), vars.GOBIN);
30208
+ const go = path44.join(root, "bin", "go.exe");
29846
30209
  env.summary.push("go: " + versionOf(go, ["version"]));
29847
30210
  await installOnce(
29848
30211
  env,
@@ -29900,77 +30263,6 @@ function decideCli(settings, ctx) {
29900
30263
  return Object.fromEntries(CLI_NAMES.map((name) => [name, decide2(name)]));
29901
30264
  }
29902
30265
 
29903
- // src/guest/toolchains/runtimes.ts
29904
- import path44 from "node:path";
29905
- async function setupSingleExe(env, name, repo, pattern, exeName, dir, options = {}) {
29906
- if (dryRun) {
29907
- log("(dry run) would provision " + name);
29908
- return;
29909
- }
29910
- const bin = options.subdir ? path44.join(dir, options.subdir) : dir;
29911
- const exe = path44.join(bin, exeName);
29912
- if (!exists(exe) || options.refresh) {
29913
- const size = options.size ? ", ~" + options.size : "";
29914
- await installFromGithub(repo, pattern, {
29915
- dest: dir,
29916
- what: (a) => name + " (" + a.name + size + " once)",
29917
- ...options.subdir ? {} : { locate: exeName }
29918
- });
29919
- }
29920
- if (!exists(exe)) throw new Error(exeName + " missing after installing " + name);
29921
- env.prepend.push(bin);
29922
- env.summary.push(name + ": " + versionOf(exe, options.versionArgs) + (options.note ?? ""));
29923
- }
29924
- async function setupBun(env) {
29925
- await setupSingleExe(
29926
- env,
29927
- "bun",
29928
- "oven-sh/bun",
29929
- arch === "arm64" ? /^bun-windows-aarch64\.zip$/ : /^bun-windows-x64\.zip$/,
29930
- "bun.exe",
29931
- tool("bun", "bin"),
29932
- { size: "35 MB" }
29933
- );
29934
- Object.assign(env.vars, {
29935
- BUN_INSTALL: tool("bun"),
29936
- BUN_INSTALL_CACHE_DIR: path44.join(localDir, "bun-cache"),
29937
- BUN_TMPDIR: path44.join(localDir, "bun-tmp")
29938
- });
29939
- }
29940
- async function setupDeno(env) {
29941
- await setupSingleExe(
29942
- env,
29943
- "deno",
29944
- "denoland/deno",
29945
- arch === "arm64" ? /^deno-aarch64-pc-windows-msvc[.]zip$/ : /^deno-x86_64-pc-windows-msvc[.]zip$/,
29946
- "deno.exe",
29947
- tool("deno", "bin"),
29948
- { size: "40 MB" }
29949
- );
29950
- Object.assign(env.vars, { DENO_DIR: tool("deno-cache"), DENO_INSTALL_ROOT: tool("deno"), DENO_NO_UPDATE_CHECK: "1" });
29951
- }
29952
- function uvVars() {
29953
- return {
29954
- UV_CACHE_DIR: tool("uv-cache"),
29955
- UV_PYTHON_INSTALL_DIR: tool("uv-python"),
29956
- UV_TOOL_DIR: tool("uv-tools"),
29957
- UV_TOOL_BIN_DIR: tool("uv-bin")
29958
- };
29959
- }
29960
- async function setupUv(env) {
29961
- await setupSingleExe(
29962
- env,
29963
- "uv",
29964
- "astral-sh/uv",
29965
- arch === "arm64" ? /^uv-aarch64-pc-windows-msvc\.zip$/ : /^uv-x86_64-pc-windows-msvc\.zip$/,
29966
- "uv.exe",
29967
- tool("uv"),
29968
- { size: "20 MB" }
29969
- );
29970
- Object.assign(env.vars, uvVars());
29971
- env.prepend.push(mkdirp(tool("uv-bin")));
29972
- }
29973
-
29974
30266
  // src/guest/toolchains/tools/cli.ts
29975
30267
  function ghConfigDir() {
29976
30268
  return path45.join(stateDir, "gh");
@@ -31610,7 +31902,12 @@ function plan(config2, versions, o) {
31610
31902
  },
31611
31903
  { name: "Deno", enabled: wanted(config2.deno.enabled, "deno"), install: (env) => setupDeno(env) },
31612
31904
  { name: "Bun", enabled: wanted(config2.bun.enabled, "bun"), install: (env) => setupBun(env) },
31613
- { name: "uv", enabled: wanted(config2.uv.enabled, "uv"), install: (env) => setupUv(env) },
31905
+ // Headroom (an agent add-on) installs with uv: the uv step runs for it too, before the apps.
31906
+ {
31907
+ name: "uv",
31908
+ enabled: wanted(config2.uv.enabled, "uv") || appsNeedUv(o.apps),
31909
+ install: (env) => setupUv(env)
31910
+ },
31614
31911
  { name: "PowerShell 7", enabled: wanted(config2.pwsh.enabled, "pwsh"), install: (env) => setupPwsh(env, o.refresh) },
31615
31912
  // The openssl tool comes from Git's install: after Git.
31616
31913
  {
@@ -31636,7 +31933,7 @@ function plan(config2, versions, o) {
31636
31933
  name: "Apps",
31637
31934
  enabled: apps,
31638
31935
  shared: true,
31639
- after: ["Mesa 3D", "Visual C++ runtime"],
31936
+ after: ["Mesa 3D", "Visual C++ runtime", "uv"],
31640
31937
  install: (env) => setupApps(env, o.apps, { gitAccess: o.gitAccess, refresh: o.refresh })
31641
31938
  },
31642
31939
  {
@@ -31820,6 +32117,7 @@ async function setupToolchains(config2, versions, options, run2 = {}) {
31820
32117
  env.summary.push(...part.summary);
31821
32118
  }
31822
32119
  Object.assign(env.vars, config2.environment);
32120
+ Object.assign(env.vars, envs.get("Git")?.vars);
31823
32121
  log("Set up " + steps.length + " toolchains and databases.");
31824
32122
  return env;
31825
32123
  }
@@ -32016,6 +32314,7 @@ function execCommand(req) {
32016
32314
  }
32017
32315
 
32018
32316
  // src/guest/daemon/sessions.ts
32317
+ import fs53 from "node:fs";
32019
32318
  import path68 from "node:path";
32020
32319
  import crypto4 from "node:crypto";
32021
32320
  import os11 from "node:os";
@@ -32028,6 +32327,8 @@ var FrameType = {
32028
32327
  Resize: 3,
32029
32328
  Restart: 4,
32030
32329
  Ping: 5,
32330
+ /** Ends the program and everything it started, and with it the session. */
32331
+ Close: 6,
32031
32332
  Output: 16,
32032
32333
  Exit: 17,
32033
32334
  Error: 18,
@@ -32111,6 +32412,16 @@ function cmdArgs(args) {
32111
32412
  function commandLine(file3, args) {
32112
32413
  return path64.win32.basename(file3).toLowerCase() === "cmd.exe" ? [quoteArg(file3), cmdArgs(args)].filter(Boolean).join(" ") : [file3, ...args].map(quoteArg).join(" ");
32113
32414
  }
32415
+ function shellLaunch(asked, paths) {
32416
+ if (asked === "cmd") return { name: "cmd", file: paths.cmd, args: [] };
32417
+ if (asked === "powershell" || asked === "auto" && !paths.pwsh)
32418
+ return { name: "powershell", file: paths.powershell, args: ["-NoLogo"] };
32419
+ if (!paths.pwsh)
32420
+ throw new Error(
32421
+ "PowerShell 7 is not installed in this sandbox (toolchains.pwsh). Open powershell or cmd instead, or turn pwsh on."
32422
+ );
32423
+ return { name: "pwsh", file: paths.pwsh, args: ["-NoLogo"] };
32424
+ }
32114
32425
 
32115
32426
  // src/guest/sessions/pty-host.ts
32116
32427
  import fs50 from "node:fs";
@@ -32330,6 +32641,8 @@ var IdleShutdown = class {
32330
32641
  };
32331
32642
 
32332
32643
  // src/guest/daemon/sessions.ts
32644
+ var CLOSE_WAIT_MS = 8e3;
32645
+ var ROOTS_KEPT_MS = 10 * 60 * 1e3;
32333
32646
  function terminalTab(id, title, cwd) {
32334
32647
  const view = [process.execPath, path68.join(path68.dirname(process.argv[1]), "view.js"), id];
32335
32648
  const wt = path68.join(toolsDir, "terminal", "wt.exe");
@@ -32374,14 +32687,38 @@ function writeBudget(cpus, memoryGb, jobs, env) {
32374
32687
  warn("budget.json could not be written: " + errMsg(e));
32375
32688
  }
32376
32689
  }
32377
- var launchPtyHost = async (req, remote, env, say, sessions2) => {
32690
+ var agentProgram = async (req, env, say) => {
32378
32691
  const harness = harnessFor(req);
32379
32692
  say("Preparing " + req.harness);
32380
32693
  await agents.provision(req.harness, harness, env, false);
32381
32694
  const launch = agents.resolveLaunch(req.harness, harness);
32382
32695
  const base = [...launch.args, ...harness.arguments];
32696
+ return {
32697
+ command: commandLine(launch.file, [...base, ...req.args]),
32698
+ restartCommand: commandLine(launch.file, [...base, ...harness.resumeArguments ?? []]),
32699
+ environment: harness.environment
32700
+ };
32701
+ };
32702
+ function shellPaths() {
32703
+ const candidates = [
32704
+ path68.join(pwshDir(), "pwsh.exe"),
32705
+ path68.join(process.env.ProgramFiles ?? "C:\\Program Files", "PowerShell", "7", "pwsh.exe")
32706
+ ];
32707
+ return { pwsh: candidates.find((f) => fs53.existsSync(f)) ?? null, powershell: SYSTEM.powershell(), cmd: SYSTEM.cmd() };
32708
+ }
32709
+ var shellProgram = (shell) => {
32710
+ const line = commandLine(shell.file, shell.args);
32711
+ return { command: line, restartCommand: line, environment: {} };
32712
+ };
32713
+ var launchPtyHost = async (req, program, remote, env, sessions2, onSpawn) => {
32383
32714
  const port = await freePort();
32384
32715
  const token = crypto4.randomBytes(24).toString("base64url");
32716
+ const sessionEnv = {
32717
+ ...withRegistryPath(env),
32718
+ ...program.environment,
32719
+ ...memoryEnv(env, sessions2)
32720
+ };
32721
+ if (req.shell) delete sessionEnv.PSModulePath;
32385
32722
  const child = startPtyHost({
32386
32723
  exe: ensurePtyHost(),
32387
32724
  port,
@@ -32390,11 +32727,12 @@ var launchPtyHost = async (req, remote, env, say, sessions2) => {
32390
32727
  rows: 32,
32391
32728
  id: req.id,
32392
32729
  cwd: req.cwd,
32393
- env: { ...withRegistryPath(env), ...harness.environment, ...memoryEnv(env, sessions2) },
32730
+ env: sessionEnv,
32394
32731
  remote,
32395
- command: commandLine(launch.file, [...base, ...req.args]),
32396
- restartCommand: commandLine(launch.file, [...base, ...harness.resumeArguments ?? []])
32732
+ command: program.command,
32733
+ restartCommand: program.restartCommand
32397
32734
  });
32735
+ onSpawn(child);
32398
32736
  try {
32399
32737
  await waitForPtyHost(child, port, 15e3);
32400
32738
  } catch (e) {
@@ -32407,6 +32745,17 @@ var launchPtyHost = async (req, remote, env, say, sessions2) => {
32407
32745
  };
32408
32746
  var placeFor = async (req, say) => {
32409
32747
  if (!req.project) {
32748
+ const known = insidePrimary(req.cwd) ? null : provisioner.provisionedFor(req.cwd);
32749
+ if (known && exists(req.cwd))
32750
+ return {
32751
+ env: known.env,
32752
+ hooks: known.spec.hooks ? projectHooks(known.spec.name, known.spec.workspace) : null,
32753
+ project: known.spec.name
32754
+ };
32755
+ if (!insidePrimary(req.cwd) && insideWorkspace(req.cwd))
32756
+ throw new Error(
32757
+ "The project in " + req.cwd + " is not set up in the sandbox yet: run sandbox start in its folder first."
32758
+ );
32410
32759
  if (!insidePrimary(req.cwd) || !exists(req.cwd))
32411
32760
  throw new Error("Folder is not inside the sandbox workspace " + workspace + ": " + req.cwd);
32412
32761
  return { env: process.env, hooks: null, project: primaryProject() };
@@ -32434,20 +32783,49 @@ var SessionManager = class {
32434
32783
  /** Sessions and projects being set up right now (a headless sandbox must not shut down under them). */
32435
32784
  starting = 0;
32436
32785
  idle = new IdleShutdown(() => this.live.size > 0 || this.starting > 0);
32786
+ /** Each session's ptyhost (process id and when it ran), for linking the programs it starts to it (audit). */
32787
+ ptyHosts = /* @__PURE__ */ new Map();
32437
32788
  /** How many sessions are running. */
32438
32789
  get count() {
32439
32790
  return this.live.size;
32440
32791
  }
32441
- /** `session` requests: an agent in its own ptyhost, in the launch folder's project or a registered one. */
32792
+ /** The sessions' ptyhosts, running or ended within ROOTS_KEPT_MS (audit events arrive after they happen). */
32793
+ roots() {
32794
+ const cutoff = Date.now() - ROOTS_KEPT_MS;
32795
+ for (const [id, r] of this.ptyHosts) if (r.to !== void 0 && r.to < cutoff) this.ptyHosts.delete(id);
32796
+ return [...this.ptyHosts.values()];
32797
+ }
32798
+ rootStarted(id, child, from) {
32799
+ if (child.pid) this.ptyHosts.set(id, { session: id, pid: child.pid, from });
32800
+ child.once("exit", () => this.rootEnded(id));
32801
+ }
32802
+ rootEnded(id) {
32803
+ const r = this.ptyHosts.get(id);
32804
+ if (r && r.to === void 0) this.ptyHosts.set(id, { ...r, to: Date.now() });
32805
+ }
32806
+ /**
32807
+ * `session` requests: an agent (or a plain shell) in its own ptyhost, in the launch folder's project or a registered
32808
+ * one.
32809
+ */
32442
32810
  async start(req) {
32811
+ let shell = null;
32812
+ let problem = null;
32813
+ if (req.shell)
32814
+ try {
32815
+ shell = shellLaunch(req.shell, shellPaths());
32816
+ } catch (e) {
32817
+ problem = errMsg(e);
32818
+ }
32819
+ const name = shell?.name ?? (req.shell ? "shell" : req.harness);
32443
32820
  let status = {
32444
32821
  id: req.id,
32445
32822
  launchId: session.launchId,
32446
- harness: req.harness,
32823
+ harness: name,
32824
+ ...req.shell ? { kind: "shell" } : {},
32447
32825
  cwd: req.cwd,
32448
32826
  ...req.project ? { project: req.project.name } : {},
32449
32827
  phase: "starting",
32450
- message: "Starting " + req.harness,
32828
+ message: "Starting " + name,
32451
32829
  updatedAt: now(),
32452
32830
  startedAt: now()
32453
32831
  };
@@ -32458,28 +32836,40 @@ var SessionManager = class {
32458
32836
  };
32459
32837
  this.starting++;
32460
32838
  if (req.id !== session.launchId && process.env.AGENT_NO_TABS !== "1")
32461
- safely("terminal tab", () => terminalTab(req.id, req.harness + " " + path68.basename(req.cwd), req.cwd));
32839
+ safely("terminal tab", () => terminalTab(req.id, name + " " + path68.basename(req.cwd), req.cwd));
32462
32840
  try {
32841
+ if (problem) throw new Error(problem);
32463
32842
  const place = await placeFor(req, say);
32843
+ const program = shell ? shellProgram(shell) : await agentProgram(req, place.env, say);
32464
32844
  const remote = session.networking && session.attach.enabled;
32465
- const { child, port, token } = await launchPtyHost(req, remote, place.env, say, this.live.size + 1);
32466
- const resumed = req.harness === "claude" ? resumedConversation(req.args) : void 0;
32845
+ const from = Date.now();
32846
+ const { child, port, token } = await launchPtyHost(
32847
+ req,
32848
+ program,
32849
+ remote,
32850
+ place.env,
32851
+ this.live.size + 1,
32852
+ (c) => this.rootStarted(req.id, c, from)
32853
+ );
32854
+ const agent = shell === null;
32855
+ const resumed = agent && req.harness === "claude" ? resumedConversation(req.args) : void 0;
32467
32856
  const running = {
32468
32857
  ...status,
32469
32858
  ...resumed ? { conversation: resumed } : {},
32470
32859
  project: place.project,
32471
32860
  phase: "running",
32472
- message: req.harness + " is running",
32861
+ message: name + " is running",
32473
32862
  attach: { addresses: remote ? guestAddresses() : [], port, token }
32474
32863
  };
32475
32864
  this.live.set(req.id, { status: running, child });
32476
32865
  child.once("exit", (code) => this.onExit(running, place, code));
32477
32866
  saveStatus(running);
32478
- if (req.harness === "claude") this.trackConversation(running.id, resumed);
32867
+ if (agent && req.harness === "claude") this.trackConversation(running.id, resumed);
32479
32868
  log(
32480
- "Session " + req.id + " started (" + req.harness + " in " + req.cwd + ", project " + place.project + ", port " + port + ")"
32869
+ "Session " + req.id + " started (" + name + " in " + req.cwd + ", project " + place.project + ", port " + port + ")"
32481
32870
  );
32482
32871
  } catch (e) {
32872
+ this.rootEnded(req.id);
32483
32873
  saveStatus({ ...status, phase: "failed", message: errMsg(e) });
32484
32874
  warn("Session " + req.id + " failed: " + errMsg(e));
32485
32875
  this.idle.check();
@@ -32505,10 +32895,13 @@ var SessionManager = class {
32505
32895
  this.idle.check();
32506
32896
  }
32507
32897
  }
32508
- /** `restart` requests: the matching sessions restart their agent (continuing the conversation where it can). */
32898
+ /**
32899
+ * `restart` requests: the matching sessions restart their agent (continuing the conversation where it can). Shells
32900
+ * are left alone: restarting one would only throw away what it was doing.
32901
+ */
32509
32902
  async restart(req) {
32510
32903
  const targets = [...this.live.values()].filter(
32511
- (s) => s.status.attach && (req.harness === null || s.status.harness === req.harness) && (!req.cwd || isWithin(req.cwd, s.status.cwd))
32904
+ (s) => s.status.attach && s.status.kind !== "shell" && (req.harness === null || s.status.harness === req.harness) && (!req.cwd || isWithin(req.cwd, s.status.cwd))
32512
32905
  );
32513
32906
  for (const s of targets)
32514
32907
  await sendControl(
@@ -32517,6 +32910,41 @@ var SessionManager = class {
32517
32910
  );
32518
32911
  respond(req.id, { ok: true, message: "restarted " + targets.length + " session(s)" });
32519
32912
  }
32913
+ /**
32914
+ * `close` requests: the session's ptyhost ends its program and everything it started (ProcessTree), then exits as
32915
+ * if the program had. One that does not end in time has its ptyhost ended instead (closing the pseudo console ends
32916
+ * what is attached to it).
32917
+ */
32918
+ async close(req) {
32919
+ const entry = this.live.get(req.session);
32920
+ if (!entry?.status.attach) {
32921
+ respond(req.id, { ok: false, message: "Session " + req.session + " is not running." });
32922
+ return;
32923
+ }
32924
+ const { child, status } = entry;
32925
+ const ended = new Promise((resolve2) => {
32926
+ if (child.exitCode !== null || child.signalCode !== null) return resolve2(true);
32927
+ const timer = setTimeout(() => resolve2(false), CLOSE_WAIT_MS);
32928
+ child.once("exit", () => {
32929
+ clearTimeout(timer);
32930
+ resolve2(true);
32931
+ });
32932
+ });
32933
+ try {
32934
+ await sendControl(
32935
+ { addresses: ["127.0.0.1"], port: status.attach.port, token: status.attach.token },
32936
+ FrameType.Close
32937
+ );
32938
+ } catch (e) {
32939
+ warn("Session " + req.session + " did not take the close request: " + errMsg(e));
32940
+ }
32941
+ if (!await ended) {
32942
+ warn("Session " + req.session + " did not end in time; ending its session host.");
32943
+ safely("ending a session host", () => child.kill());
32944
+ }
32945
+ log("Session " + req.session + " closed on request");
32946
+ respond(req.id, { ok: true, message: "Closed " + status.harness + " session " + req.session + "." });
32947
+ }
32520
32948
  /**
32521
32949
  * Finds out which of Claude's conversation files this session writes (it appears with the first message) and puts
32522
32950
  * it in the status, so the host can reopen this very conversation. A reopened session starts with the one it was
@@ -32559,6 +32987,10 @@ var SessionManager = class {
32559
32987
  attach: void 0
32560
32988
  });
32561
32989
  log("Session " + running.id + " ended (exit " + code + ")");
32990
+ if (running.kind === "shell") {
32991
+ this.idle.check();
32992
+ return;
32993
+ }
32562
32994
  if (place.hooks)
32563
32995
  void runHooksAsync("post-agent", place.env, place.hooks).catch(
32564
32996
  (e) => warn("post-agent hooks failed: " + errMsg(e))
@@ -32898,7 +33330,7 @@ var GovernorCore = class {
32898
33330
  };
32899
33331
 
32900
33332
  // src/guest/governor/governor.ts
32901
- import fs53 from "node:fs";
33333
+ import fs54 from "node:fs";
32902
33334
  import os12 from "node:os";
32903
33335
  import path69 from "node:path";
32904
33336
  import { spawn as spawn8 } from "node:child_process";
@@ -32971,11 +33403,11 @@ var MemGovProcess = class {
32971
33403
  function appendToMemoryLog(line) {
32972
33404
  try {
32973
33405
  const logs = path69.join(dataDir, "logs");
32974
- const newest = fs53.readdirSync(logs).filter((f) => /^memory-\d{8}-\d{6}\.log$/.test(f)).sort().pop();
33406
+ const newest = fs54.readdirSync(logs).filter((f) => /^memory-\d{8}-\d{6}\.log$/.test(f)).sort().pop();
32975
33407
  if (!newest) return;
32976
33408
  const t = /* @__PURE__ */ new Date();
32977
33409
  const stamp = [t.getHours(), t.getMinutes(), t.getSeconds()].map((n) => String(n).padStart(2, "0")).join(":");
32978
- fs53.appendFileSync(path69.join(logs, newest), stamp + " | " + line + "\r\n");
33410
+ fs54.appendFileSync(path69.join(logs, newest), stamp + " | " + line + "\r\n");
32979
33411
  } catch {
32980
33412
  }
32981
33413
  }
@@ -33032,9 +33464,9 @@ var Daemon = class {
33032
33464
  tick() {
33033
33465
  const { valid, invalid } = run.scanRequests();
33034
33466
  for (const file3 of invalid)
33035
- safely("removing an invalid request", () => fs54.rmSync(file3, { recursive: true, force: true }));
33467
+ safely("removing an invalid request", () => fs55.rmSync(file3, { recursive: true, force: true }));
33036
33468
  for (const req of valid) {
33037
- safely("claiming a request", () => fs54.rmSync(run.requestFile(req.id), { force: true }));
33469
+ safely("claiming a request", () => fs55.rmSync(run.requestFile(req.id), { force: true }));
33038
33470
  if (run.readSession(req.id)) continue;
33039
33471
  void Promise.resolve(this.handle(withRequestSecrets(req))).catch(
33040
33472
  (e) => respond(req.id, { ok: false, message: errMsg(e) })
@@ -33056,6 +33488,8 @@ var Daemon = class {
33056
33488
  return execCommand(req);
33057
33489
  case "restart":
33058
33490
  return sessions.restart(req);
33491
+ case "close":
33492
+ return sessions.close(req);
33059
33493
  case "pressure":
33060
33494
  return governor.hostPressure(req.seconds, req.reason, req.level ?? "slow");
33061
33495
  case "ping":
@@ -33070,7 +33504,7 @@ process.on("unhandledRejection", (e) => warn("daemon: unexpected error (continui
33070
33504
  adoptBootSecrets();
33071
33505
  log("Session daemon started (pid " + process.pid + ").");
33072
33506
  safely("port forwards", startRelays);
33073
- safely("audit", startProcessCollector);
33507
+ safely("audit", () => startProcessCollector(() => sessions.roots()));
33074
33508
  safely("memory governor", () => governor.start());
33075
33509
  log("Memory governor: " + governor.describe());
33076
33510
  new Daemon().start();