windows-development-sandbox 1.0.5 → 1.1.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 (368) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -6
  3. package/config.example.json +17 -0
  4. package/dist/api.js +2 -1
  5. package/dist/cli/args.d.ts +20 -0
  6. package/dist/cli/args.js +35 -3
  7. package/dist/cli/colors.d.ts +24 -0
  8. package/dist/cli/colors.js +42 -0
  9. package/dist/cli/commands/agent-commands.d.ts +6 -0
  10. package/dist/cli/commands/agent-commands.js +61 -0
  11. package/dist/cli/commands/context.d.ts +2 -0
  12. package/dist/cli/commands/context.js +3 -0
  13. package/dist/cli/commands/data/clean.d.ts +15 -0
  14. package/dist/cli/commands/data/clean.js +101 -0
  15. package/dist/cli/commands/data/collect.d.ts +11 -0
  16. package/dist/cli/commands/data/collect.js +54 -0
  17. package/dist/cli/commands/data/index.d.ts +4 -0
  18. package/dist/cli/commands/data/index.js +45 -0
  19. package/dist/cli/commands/data/remove.d.ts +2 -0
  20. package/dist/cli/commands/data/remove.js +30 -0
  21. package/dist/cli/commands/index.d.ts +5 -3
  22. package/dist/cli/commands/index.js +5 -3
  23. package/dist/cli/commands/launch.d.ts +26 -2
  24. package/dist/cli/commands/launch.js +220 -39
  25. package/dist/cli/commands/maintenance.js +33 -4
  26. package/dist/cli/commands/project.d.ts +29 -0
  27. package/dist/cli/commands/project.js +63 -0
  28. package/dist/cli/commands/projects.d.ts +14 -0
  29. package/dist/cli/commands/projects.js +146 -0
  30. package/dist/cli/commands/repair/checks.d.ts +2 -0
  31. package/dist/cli/commands/{repair.js → repair/checks.js} +19 -181
  32. package/dist/cli/commands/{repair.d.ts → repair/finders.d.ts} +0 -29
  33. package/dist/cli/commands/repair/finders.js +108 -0
  34. package/dist/cli/commands/repair/index.d.ts +12 -0
  35. package/dist/cli/commands/repair/index.js +61 -0
  36. package/dist/cli/commands/repair/types.d.ts +26 -0
  37. package/dist/cli/commands/repair/types.js +3 -0
  38. package/dist/cli/commands/restart.d.ts +6 -0
  39. package/dist/cli/commands/restart.js +85 -0
  40. package/dist/cli/commands/sessions.d.ts +2 -1
  41. package/dist/cli/commands/sessions.js +189 -45
  42. package/dist/cli/commands/setup/checks.d.ts +13 -0
  43. package/dist/cli/commands/setup/checks.js +71 -0
  44. package/dist/cli/commands/{setup.d.ts → setup/facts.d.ts} +1 -15
  45. package/dist/cli/commands/setup/facts.js +60 -0
  46. package/dist/cli/commands/setup/index.d.ts +2 -0
  47. package/dist/cli/commands/setup/index.js +43 -0
  48. package/dist/cli/commands/user-config.d.ts +8 -0
  49. package/dist/cli/commands/user-config.js +41 -0
  50. package/dist/cli/history.d.ts +11 -0
  51. package/dist/cli/history.js +21 -0
  52. package/dist/cli/index.d.ts +7 -1
  53. package/dist/cli/index.js +38 -13
  54. package/dist/cli/manifest.d.ts +8 -0
  55. package/dist/cli/manifest.js +35 -15
  56. package/dist/cli/menu-multi.d.ts +12 -0
  57. package/dist/cli/menu-multi.js +82 -0
  58. package/dist/cli/menu.d.ts +5 -13
  59. package/dist/cli/menu.js +149 -46
  60. package/dist/cli/progress.d.ts +9 -0
  61. package/dist/cli/progress.js +49 -0
  62. package/dist/cli/settings/edit.d.ts +13 -0
  63. package/dist/cli/settings/edit.js +19 -0
  64. package/dist/cli/settings/index.d.ts +8 -0
  65. package/dist/cli/settings/index.js +131 -0
  66. package/dist/cli/settings/prompts.d.ts +7 -0
  67. package/dist/cli/settings/prompts.js +48 -0
  68. package/dist/cli/settings/schema.d.ts +33 -0
  69. package/dist/cli/settings/schema.js +29 -0
  70. package/dist/cli/validate.d.ts +17 -0
  71. package/dist/cli/validate.js +89 -0
  72. package/dist/core/config.js +4 -1
  73. package/dist/core/disk.d.ts +16 -0
  74. package/dist/core/disk.js +55 -0
  75. package/dist/core/local-config.d.ts +8 -0
  76. package/dist/core/local-config.js +18 -5
  77. package/dist/core/project-policy.d.ts +15 -0
  78. package/dist/core/project-policy.js +35 -0
  79. package/dist/core/prompt.d.ts +4 -0
  80. package/dist/core/prompt.js +15 -0
  81. package/dist/core/safe-paths.d.ts +4 -0
  82. package/dist/core/safe-paths.js +37 -0
  83. package/dist/core/schema/common.d.ts +17 -0
  84. package/dist/core/schema/common.js +15 -0
  85. package/dist/core/schema/config.d.ts +594 -0
  86. package/dist/core/schema/config.js +63 -0
  87. package/dist/core/schema/databases.d.ts +33 -0
  88. package/dist/core/schema/databases.js +35 -0
  89. package/dist/core/schema/harness.d.ts +12 -0
  90. package/dist/core/schema/harness.js +17 -0
  91. package/dist/core/schema/json-schema.d.ts +5 -0
  92. package/dist/core/schema/json-schema.js +25 -0
  93. package/dist/core/schema/profile.d.ts +27 -0
  94. package/dist/core/schema/profile.js +29 -0
  95. package/dist/core/schema/projects.d.ts +9 -0
  96. package/dist/core/schema/projects.js +11 -0
  97. package/dist/core/schema/sandbox.d.ts +50 -0
  98. package/dist/core/schema/sandbox.js +36 -0
  99. package/dist/core/schema/toolchains.d.ts +93 -0
  100. package/dist/core/schema/toolchains.js +83 -0
  101. package/dist/core/schema.d.ts +6 -604
  102. package/dist/core/schema.js +8 -264
  103. package/dist/core/types.d.ts +29 -1
  104. package/dist/core/util.d.ts +7 -0
  105. package/dist/core/util.js +12 -3
  106. package/dist/core/workspace.d.ts +0 -5
  107. package/dist/core/workspace.js +12 -46
  108. package/dist/guest/boot-env.d.ts +4 -0
  109. package/dist/guest/boot-env.js +25 -0
  110. package/dist/guest/cmdline.d.ts +12 -0
  111. package/dist/guest/cmdline.js +23 -0
  112. package/dist/guest/daemon/exec.d.ts +5 -0
  113. package/dist/guest/daemon/exec.js +32 -0
  114. package/dist/guest/daemon/project.d.ts +14 -0
  115. package/dist/guest/daemon/project.js +132 -0
  116. package/dist/guest/daemon/session.d.ts +13 -0
  117. package/dist/guest/daemon/session.js +139 -0
  118. package/dist/guest/daemon/shutdown.d.ts +1 -0
  119. package/dist/guest/daemon/shutdown.js +21 -0
  120. package/dist/guest/daemon/state.d.ts +17 -0
  121. package/dist/guest/daemon/state.js +31 -0
  122. package/dist/guest/daemon.js +8 -141
  123. package/dist/guest/db.js +18 -3
  124. package/dist/guest/env.d.ts +2 -0
  125. package/dist/guest/env.js +4 -2
  126. package/dist/guest/guide.d.ts +6 -2
  127. package/dist/guest/guide.js +5 -4
  128. package/dist/guest/helpers.d.ts +3 -0
  129. package/dist/guest/helpers.js +26 -0
  130. package/dist/guest/main.js +17 -7
  131. package/dist/guest/ports.d.ts +6 -0
  132. package/dist/guest/ports.js +22 -0
  133. package/dist/guest/project-context.d.ts +7 -0
  134. package/dist/guest/project-context.js +10 -0
  135. package/dist/guest/project-env.d.ts +20 -0
  136. package/dist/guest/project-env.js +20 -0
  137. package/dist/guest/project-status.d.ts +5 -0
  138. package/dist/guest/project-status.js +17 -0
  139. package/dist/guest/ptyhost.d.ts +0 -18
  140. package/dist/guest/ptyhost.js +5 -63
  141. package/dist/guest/setup/agents.d.ts +5 -2
  142. package/dist/guest/setup/agents.js +22 -8
  143. package/dist/guest/setup/claude.js +11 -3
  144. package/dist/guest/setup/environment.d.ts +13 -3
  145. package/dist/guest/setup/environment.js +14 -10
  146. package/dist/guest/setup/hooks.d.ts +9 -1
  147. package/dist/guest/setup/hooks.js +28 -14
  148. package/dist/guest/toolchains/catalogue.d.ts +4 -0
  149. package/dist/guest/toolchains/catalogue.js +2 -2
  150. package/dist/guest/toolchains/cli.js +25 -17
  151. package/dist/guest/toolchains/common.d.ts +18 -1
  152. package/dist/guest/toolchains/common.js +34 -11
  153. package/dist/guest/toolchains/databases/dynamodb.d.ts +8 -0
  154. package/dist/guest/toolchains/databases/dynamodb.js +52 -0
  155. package/dist/guest/toolchains/{databases.d.ts → databases/index.d.ts} +2 -3
  156. package/dist/guest/toolchains/databases/index.js +62 -0
  157. package/dist/guest/toolchains/databases/mongodb.d.ts +3 -0
  158. package/dist/guest/toolchains/databases/mongodb.js +31 -0
  159. package/dist/guest/toolchains/databases/mysql.d.ts +3 -0
  160. package/dist/guest/toolchains/databases/mysql.js +38 -0
  161. package/dist/guest/toolchains/databases/postgres.d.ts +5 -0
  162. package/dist/guest/toolchains/databases/postgres.js +38 -0
  163. package/dist/guest/toolchains/databases/redis.d.ts +3 -0
  164. package/dist/guest/toolchains/databases/redis.js +16 -0
  165. package/dist/guest/toolchains/databases/server.d.ts +14 -0
  166. package/dist/guest/toolchains/databases/server.js +52 -0
  167. package/dist/guest/toolchains/git.js +25 -16
  168. package/dist/guest/toolchains/go.js +1 -1
  169. package/dist/guest/toolchains/index.d.ts +13 -9
  170. package/dist/guest/toolchains/index.js +40 -82
  171. package/dist/guest/toolchains/java.js +5 -5
  172. package/dist/guest/toolchains/leftovers.d.ts +2 -0
  173. package/dist/guest/toolchains/leftovers.js +26 -0
  174. package/dist/guest/toolchains/managers.js +12 -10
  175. package/dist/guest/toolchains/{binstall.js → native/binstall.js} +6 -9
  176. package/dist/guest/toolchains/native/cpp.d.ts +4 -0
  177. package/dist/guest/toolchains/native/cpp.js +38 -0
  178. package/dist/guest/toolchains/native/dotnet.d.ts +4 -0
  179. package/dist/guest/toolchains/native/dotnet.js +103 -0
  180. package/dist/guest/toolchains/native/mesa.d.ts +7 -0
  181. package/dist/guest/toolchains/native/mesa.js +46 -0
  182. package/dist/guest/toolchains/native/msvc/environment.d.ts +16 -0
  183. package/dist/guest/toolchains/native/msvc/environment.js +57 -0
  184. package/dist/guest/toolchains/native/msvc/index.d.ts +6 -0
  185. package/dist/guest/toolchains/native/msvc/index.js +48 -0
  186. package/dist/guest/toolchains/native/msvc/packages.d.ts +6 -0
  187. package/dist/guest/toolchains/native/msvc/packages.js +156 -0
  188. package/dist/guest/toolchains/native/msvc/tree.d.ts +15 -0
  189. package/dist/guest/toolchains/native/msvc/tree.js +21 -0
  190. package/dist/guest/toolchains/native/rust.d.ts +3 -0
  191. package/dist/guest/toolchains/native/rust.js +45 -0
  192. package/dist/guest/toolchains/node.js +26 -6
  193. package/dist/guest/toolchains/plan.d.ts +22 -0
  194. package/dist/guest/toolchains/plan.js +96 -0
  195. package/dist/guest/toolchains/pwsh.js +1 -1
  196. package/dist/guest/toolchains/python.js +17 -10
  197. package/dist/guest/toolchains/runtimes.d.ts +2 -0
  198. package/dist/guest/toolchains/runtimes.js +6 -8
  199. package/dist/guest/toolchains/scripting/php.d.ts +3 -0
  200. package/dist/guest/toolchains/scripting/php.js +71 -0
  201. package/dist/guest/toolchains/scripting/ruby.d.ts +3 -0
  202. package/dist/guest/toolchains/scripting/ruby.js +47 -0
  203. package/dist/guest/toolchains/scripting/vcruntime.d.ts +7 -0
  204. package/dist/guest/toolchains/scripting/vcruntime.js +45 -0
  205. package/dist/guest/view.js +21 -1
  206. package/dist/host/accounts.js +14 -5
  207. package/dist/host/agent-commands.d.ts +25 -0
  208. package/dist/host/agent-commands.js +145 -0
  209. package/dist/host/attach.d.ts +17 -0
  210. package/dist/host/attach.js +69 -0
  211. package/dist/host/channel.d.ts +23 -22
  212. package/dist/host/channel.js +66 -84
  213. package/dist/host/claude-profile.js +7 -3
  214. package/dist/host/cleanup.js +2 -11
  215. package/dist/host/database-plan.d.ts +12 -0
  216. package/dist/host/database-plan.js +52 -0
  217. package/dist/host/generate.d.ts +4 -12
  218. package/dist/host/generate.js +48 -135
  219. package/dist/host/join.d.ts +28 -0
  220. package/dist/host/join.js +76 -0
  221. package/dist/host/launch/mappings.d.ts +3 -0
  222. package/dist/host/launch/mappings.js +50 -0
  223. package/dist/host/launch/projects.d.ts +15 -0
  224. package/dist/host/launch/projects.js +56 -0
  225. package/dist/host/launch/stage.d.ts +11 -0
  226. package/dist/host/launch/stage.js +66 -0
  227. package/dist/host/launch/wsb.d.ts +10 -0
  228. package/dist/host/launch/wsb.js +34 -0
  229. package/dist/host/pc-scan.d.ts +6 -0
  230. package/dist/host/pc-scan.js +37 -0
  231. package/dist/host/platform.d.ts +2 -0
  232. package/dist/host/platform.js +13 -1
  233. package/dist/host/project-scan.d.ts +1 -17
  234. package/dist/host/project-scan.js +2 -86
  235. package/dist/host/projects.d.ts +100 -0
  236. package/dist/host/projects.js +147 -0
  237. package/dist/host/repo-guard.d.ts +15 -2
  238. package/dist/host/repo-guard.js +103 -12
  239. package/dist/host/terminal.js +3 -0
  240. package/dist/host/test-guard.d.ts +6 -0
  241. package/dist/host/test-guard.js +15 -0
  242. package/dist/host/window.js +7 -3
  243. package/dist/host/wsb.d.ts +37 -0
  244. package/dist/host/wsb.js +101 -0
  245. package/dist/runtime/daemon.js +10997 -2794
  246. package/dist/runtime/db.js +6428 -464
  247. package/dist/runtime/main.js +7359 -1135
  248. package/dist/runtime/view.js +5922 -47
  249. package/dist/selftest/accounts.test.d.ts +2 -0
  250. package/dist/selftest/accounts.test.js +44 -0
  251. package/dist/selftest/agent-commands.test.d.ts +2 -0
  252. package/dist/selftest/agent-commands.test.js +47 -0
  253. package/dist/selftest/channel.test.d.ts +2 -0
  254. package/dist/selftest/channel.test.js +37 -0
  255. package/dist/selftest/cli.test.d.ts +2 -2
  256. package/dist/selftest/cli.test.js +115 -27
  257. package/dist/selftest/cmdline.test.d.ts +2 -0
  258. package/dist/selftest/cmdline.test.js +45 -0
  259. package/dist/selftest/config.test.d.ts +2 -3
  260. package/dist/selftest/config.test.js +36 -72
  261. package/dist/selftest/data.test.d.ts +2 -2
  262. package/dist/selftest/data.test.js +77 -70
  263. package/dist/selftest/detect.test.d.ts +2 -2
  264. package/dist/selftest/detect.test.js +54 -38
  265. package/dist/selftest/fixtures.d.ts +44 -4
  266. package/dist/selftest/fixtures.js +40 -23
  267. package/dist/selftest/generate.test.d.ts +2 -0
  268. package/dist/selftest/generate.test.js +63 -0
  269. package/dist/selftest/git-access.test.d.ts +2 -0
  270. package/dist/selftest/git-access.test.js +26 -0
  271. package/dist/selftest/guest-boot.test.d.ts +2 -0
  272. package/dist/selftest/guest-boot.test.js +76 -0
  273. package/dist/selftest/harness.d.ts +44 -7
  274. package/dist/selftest/harness.js +62 -21
  275. package/dist/selftest/host-sim.test.d.ts +2 -0
  276. package/dist/selftest/host-sim.test.js +93 -0
  277. package/dist/selftest/hostile-input.test.d.ts +2 -0
  278. package/dist/selftest/hostile-input.test.js +54 -0
  279. package/dist/selftest/index.d.ts +8 -1
  280. package/dist/selftest/index.js +134 -35
  281. package/dist/selftest/links.test.d.ts +2 -0
  282. package/dist/selftest/links.test.js +50 -0
  283. package/dist/selftest/live-daemon.test.d.ts +2 -0
  284. package/dist/selftest/live-daemon.test.js +91 -0
  285. package/dist/selftest/live-git.test.d.ts +2 -0
  286. package/dist/selftest/live-git.test.js +60 -0
  287. package/dist/selftest/live-projects.test.d.ts +2 -0
  288. package/dist/selftest/live-projects.test.js +127 -0
  289. package/dist/selftest/live.d.ts +65 -0
  290. package/dist/selftest/live.js +109 -0
  291. package/dist/selftest/local-config.test.d.ts +2 -0
  292. package/dist/selftest/local-config.test.js +107 -0
  293. package/dist/selftest/mirror.test.d.ts +2 -3
  294. package/dist/selftest/mirror.test.js +92 -65
  295. package/dist/selftest/network.test.d.ts +2 -0
  296. package/dist/selftest/network.test.js +28 -0
  297. package/dist/selftest/personalization.test.d.ts +2 -0
  298. package/dist/selftest/personalization.test.js +53 -0
  299. package/dist/selftest/projects.test.d.ts +2 -0
  300. package/dist/selftest/projects.test.js +254 -0
  301. package/dist/selftest/repair.test.d.ts +2 -0
  302. package/dist/selftest/repair.test.js +42 -0
  303. package/dist/selftest/repo-guard.test.d.ts +2 -0
  304. package/dist/selftest/repo-guard.test.js +63 -0
  305. package/dist/selftest/secrets.test.d.ts +2 -0
  306. package/dist/selftest/secrets.test.js +71 -0
  307. package/dist/selftest/setup.test.d.ts +2 -2
  308. package/dist/selftest/setup.test.js +45 -22
  309. package/dist/selftest/suite.d.ts +13 -0
  310. package/dist/selftest/suite.js +1 -0
  311. package/dist/selftest/sync.test.d.ts +2 -3
  312. package/dist/selftest/sync.test.js +90 -78
  313. package/dist/selftest/test-guard.test.d.ts +2 -0
  314. package/dist/selftest/test-guard.test.js +49 -0
  315. package/dist/selftest/toolchains.test.d.ts +2 -0
  316. package/dist/selftest/toolchains.test.js +103 -0
  317. package/dist/selftest/workspace.test.d.ts +2 -0
  318. package/dist/selftest/workspace.test.js +53 -0
  319. package/dist/shared/download.d.ts +3 -1
  320. package/dist/shared/download.js +19 -4
  321. package/dist/shared/paths.d.ts +5 -0
  322. package/dist/shared/paths.js +6 -0
  323. package/dist/shared/projects.d.ts +53 -0
  324. package/dist/shared/projects.js +67 -0
  325. package/dist/shared/rundir.d.ts +10 -1
  326. package/dist/shared/rundir.js +12 -4
  327. package/dist/shared/safe-remove.js +11 -0
  328. package/dist/shared/schema.d.ts +443 -0
  329. package/dist/shared/schema.js +41 -2
  330. package/dist/shared/test-guard.d.ts +4 -0
  331. package/dist/shared/test-guard.js +37 -0
  332. package/dist/sync/claude-mem.js +3 -1
  333. package/dist/sync/history.d.ts +23 -12
  334. package/dist/sync/history.js +52 -20
  335. package/dist/sync/watcher.d.ts +8 -0
  336. package/dist/sync/watcher.js +23 -4
  337. package/package.json +6 -6
  338. package/sandbox/bootstrap.ps1 +258 -258
  339. package/sandbox/cpucap.ps1 +51 -0
  340. package/sandbox/logon.cmd +18 -16
  341. package/sandbox/openssl.cs +42 -0
  342. package/sandbox/ptyhost.cs +15 -4
  343. package/sandbox/trust.cmd +35 -35
  344. package/schemas/config.schema.json +100 -1
  345. package/schemas/sandbox-config.schema.json +17 -1
  346. package/dist/cli/commands/data.d.ts +0 -46
  347. package/dist/cli/commands/data.js +0 -249
  348. package/dist/cli/commands/setup.js +0 -152
  349. package/dist/cli/settings.d.ts +0 -11
  350. package/dist/cli/settings.js +0 -156
  351. package/dist/guest/toolchains/databases.js +0 -226
  352. package/dist/guest/toolchains/msvc.d.ts +0 -10
  353. package/dist/guest/toolchains/msvc.js +0 -216
  354. package/dist/guest/toolchains/native.d.ts +0 -12
  355. package/dist/guest/toolchains/native.js +0 -220
  356. package/dist/guest/toolchains/scripting.d.ts +0 -10
  357. package/dist/guest/toolchains/scripting.js +0 -137
  358. package/dist/selftest/guest.test.d.ts +0 -3
  359. package/dist/selftest/guest.test.js +0 -61
  360. package/dist/selftest/helpers.test.d.ts +0 -2
  361. package/dist/selftest/helpers.test.js +0 -125
  362. package/dist/selftest/host.test.d.ts +0 -2
  363. package/dist/selftest/host.test.js +0 -71
  364. package/dist/selftest/live.test.d.ts +0 -7
  365. package/dist/selftest/live.test.js +0 -127
  366. package/dist/selftest/security.test.d.ts +0 -3
  367. package/dist/selftest/security.test.js +0 -143
  368. /package/dist/guest/toolchains/{binstall.d.ts → native/binstall.d.ts} +0 -0
package/dist/guest/env.js CHANGED
@@ -2,8 +2,9 @@
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { spawnSync } from 'node:child_process';
5
- import { moveIntoPlace, spawnAsync, stagingFolder } from '../shared/download.js';
5
+ import { moveIntoPlace, setChecksumRequired, spawnAsync, stagingFolder } from '../shared/download.js';
6
6
  import { now, runPaths, writeJsonAtomic } from '../shared/rundir.js';
7
+ import { safeRemove } from '../shared/safe-remove.js';
7
8
  import { SYSTEM } from '../shared/system.js';
8
9
  export const isWin = process.platform === 'win32';
9
10
  export const dryRun = process.env.AGENT_DRY_RUN === '1';
@@ -18,6 +19,7 @@ export const toolsDir = path.join(dataDir, 'tools');
18
19
  export const logsDir = path.join(dataDir, 'logs');
19
20
  export const run = runPaths(dataDir);
20
21
  export const session = JSON.parse(fs.readFileSync(path.join(launchDir, 'session.json'), 'utf8').replace(/^\uFEFF/, ''));
22
+ setChecksumRequired(session.downloads?.requireChecksum === true);
21
23
  export const workspace = process.env.AGENT_WORKSPACE ?? session.workspace;
22
24
  export const stateDir = path.join(dataDir, session.persistAgentState ? 'state' : 'state-session');
23
25
  export const claudeDir = path.join(stateDir, 'claude');
@@ -94,6 +96,6 @@ export const copyNode = (from, to) => {
94
96
  moveIntoPlace(work, to);
95
97
  }
96
98
  finally {
97
- fs.rmSync(work, { recursive: true, force: true });
99
+ safeRemove(work, path.dirname(work));
98
100
  }
99
101
  };
@@ -1,5 +1,9 @@
1
1
  /** Deliberately tiny: it is loaded into every conversation. */
2
- export declare const sandboxGuide: (toolSummary: string[], lan?: "blocked" | "open" | "offline") => string;
2
+ export interface GuideProject {
3
+ guest: string;
4
+ host: string;
5
+ }
6
+ export declare const sandboxGuide: (toolSummary: string[], lan?: "blocked" | "open" | "offline", projects?: GuideProject[]) => string;
3
7
  /** Inserts or replaces the generated section, keeping whatever else the file contains (the host's CLAUDE.md). */
4
8
  export declare const upsertGuide: (file: string, guide: string) => void;
5
9
  /**
@@ -9,4 +13,4 @@ export declare const upsertGuide: (file: string, guide: string) => void;
9
13
  * Codex/Gemini have no machine-level file, so the block goes into their sandbox-only global instruction files.
10
14
  */
11
15
  export declare const managedClaudeMd: () => string;
12
- export declare const writeGuides: (toolSummary: string[], lan: "blocked" | "open" | "offline") => string;
16
+ export declare const writeGuides: (toolSummary: string[], lan: "blocked" | "open" | "offline", projects?: GuideProject[]) => string;
@@ -13,12 +13,13 @@ const mirrorLine = () => {
13
13
  return [];
14
14
  return ['- ' + 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).'];
15
15
  };
16
- /** Deliberately tiny: it is loaded into every conversation. */
17
- export const sandboxGuide = (toolSummary, lan = 'blocked') => [
16
+ export const sandboxGuide = (toolSummary, lan = 'blocked', projects = []) => [
18
17
  BEGIN,
19
18
  '## Sandbox manifest — you are in a DISPOSABLE Windows Sandbox',
20
19
  'Wiped on close: everything except the paths below (installers, winget/choco, C:\\Users, Program Files, registry).',
21
20
  '- RW, persists: `' + workspace + '` = host folder ' + session.workspaceHost,
21
+ // Projects started from other folders while this sandbox runs: every agent here can see them.
22
+ ...projects.map((p) => '- RW, persists: `' + p.guest + '` = host folder ' + p.host + ' (another project in this sandbox; your own is `%AGENT_WORKSPACE%`)'),
22
23
  '- RW, persists: `' + toolsDir + '` (toolchains) — put portable exes in `' + path.join(toolsDir, 'bin') + '` (on PATH)',
23
24
  '- RW, persists: `' + stateDir + '` (agent config/history; do not edit logins) · logs: `' + path.join(dataDir, 'logs') + '`',
24
25
  '- RO: `' + launchDir + '` (this boot\'s scripts) · lost on close: everything else, incl. `C:\\nvm` (Node symlink)',
@@ -45,8 +46,8 @@ export const upsertGuide = (file, guide) => {
45
46
  * Codex/Gemini have no machine-level file, so the block goes into their sandbox-only global instruction files.
46
47
  */
47
48
  export const managedClaudeMd = () => path.join(process.env.AGENT_MANAGED_DIR ?? path.join(process.env.ProgramFiles ?? 'C:\\Program Files', 'ClaudeCode'), 'CLAUDE.md');
48
- export const writeGuides = (toolSummary, lan) => {
49
- const guide = sandboxGuide(toolSummary, lan);
49
+ export const writeGuides = (toolSummary, lan, projects = []) => {
50
+ const guide = sandboxGuide(toolSummary, lan, projects);
50
51
  const managed = managedClaudeMd();
51
52
  fs.mkdirSync(path.dirname(managed), { recursive: true });
52
53
  fs.writeFileSync(managed, guide + '\n', 'utf8');
@@ -0,0 +1,3 @@
1
+ export type Helper = 'ptyhost' | 'gitshim' | 'vswhere' | 'openssl';
2
+ /** %TEMP%\agent-sandbox\<name>-<hash>.exe, compiled from this launch's sandbox\<name>.cs the first time it is needed. */
3
+ export declare const ensureCompiled: (name: Helper) => string;
@@ -0,0 +1,26 @@
1
+ // Builds the sandbox's small C# helpers (ptyhost.cs, gitshim.cs, vswhere.cs, openssl.cs) with the .NET Framework
2
+ // compiler that ships with Windows.
3
+ //
4
+ // The helpers are compiled onto the sandbox's own disk (wiped on close) on every boot, never cached in persistent
5
+ // storage: a tampered ptyhost.exe there would survive the wipe and see every keystroke typed from this PC.
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import crypto from 'node:crypto';
10
+ import { spawnSync } from 'node:child_process';
11
+ import { exists, launchDir, mkdirp } from './env.js';
12
+ const CSC = path.join(process.env.WINDIR ?? 'C:\\Windows', 'Microsoft.NET', 'Framework64', 'v4.0.30319', 'csc.exe');
13
+ const sourceHash = (name) => crypto.createHash('sha256').update(fs.readFileSync(path.join(launchDir, 'sandbox', name + '.cs'))).digest('hex').slice(0, 12);
14
+ /** %TEMP%\agent-sandbox\<name>-<hash>.exe, compiled from this launch's sandbox\<name>.cs the first time it is needed. */
15
+ export const ensureCompiled = (name) => {
16
+ const exe = path.join(os.tmpdir(), 'agent-sandbox', name + '-' + sourceHash(name) + '.exe');
17
+ if (exists(exe))
18
+ return exe;
19
+ if (!exists(CSC))
20
+ throw new Error('The .NET Framework C# compiler was not found at ' + CSC + ' (it ships with Windows 10/11).');
21
+ mkdirp(path.dirname(exe));
22
+ const result = spawnSync(CSC, ['/nologo', '/optimize', '/target:exe', '/out:' + exe, path.join(launchDir, 'sandbox', name + '.cs')], { encoding: 'utf8', windowsHide: true });
23
+ if (result.status !== 0)
24
+ throw new Error('Compiling ' + name + ' failed: ' + ((result.stdout ?? '') + (result.stderr ?? '')).trim().slice(0, 1500));
25
+ return exe;
26
+ };
@@ -12,9 +12,12 @@ import { runHooks } from './setup/hooks.js';
12
12
  import { applyNetworkPolicy } from './setup/network.js';
13
13
  import { writeManagedSettings } from './setup/policy.js';
14
14
  import { applyPersonalization, setupGitConfig } from './setup/user.js';
15
- import { ensurePtyHost, pruneToolLeftovers } from './ptyhost.js';
15
+ import { ensurePtyHost } from './ptyhost.js';
16
+ import { pruneToolLeftovers } from './toolchains/leftovers.js';
16
17
  import { now, readSession, writeJsonAtomic } from '../shared/rundir.js';
17
18
  import { viewSession } from './view.js';
19
+ import { saveBootEnv } from './boot-env.js';
20
+ import { primaryProject, saveProjectStatus } from './project-status.js';
18
21
  import { dataDir, dryRun, errMsg, isWin, localDir, log, logPath, logsDir, mkdirp, run, session, setSandboxStatus, toolsDir, warn, workspace } from './env.js';
19
22
  const STEPS = 8;
20
23
  const step = (n, title) => { log('\n\x1b[36m[' + n + '/' + STEPS + '] ' + title + '\x1b[0m'); setSandboxStatus(n === 1 ? 'booting' : 'provisioning', '[' + n + '/' + STEPS + '] ' + title); };
@@ -63,12 +66,17 @@ const main = async () => {
63
66
  runHooks('pre-provision', process.env);
64
67
  await soft('Personalisation', applyPersonalization);
65
68
  const gitAccess = session.provisioning.git.access ?? 'full';
66
- step(3, 'Toolchains (Git, nvm/Node, Python, Rust, .NET, Bun, uv)');
67
- const tools = await setupToolchains(session.toolchains, session.versions, { gitEnabled: session.provisioning.git.enabled, gitAccess, hostGit: session.hostTools.git, refresh: session.provisioning.refresh, databases: session.databases ?? [] });
69
+ step(3, 'Developer tools (the first start downloads them once; later starts are quick)');
70
+ const basePath = process.env.PATH ?? '';
71
+ const tools = await setupToolchains(session.toolchains, session.versions, {
72
+ gitEnabled: session.provisioning.git.enabled, gitAccess, hostGit: session.hostTools.git, refresh: session.provisioning.refresh, databases: session.databases ?? [],
73
+ });
68
74
  if (!dryRun)
69
75
  tools.prepend.push(writeDbCommand());
70
76
  const env = agentEnv(tools);
71
77
  persistEnvironment(env, Object.keys(tools.vars));
78
+ saveBootEnv({ basePath, toolVars: Object.keys(tools.vars), steps: tools.steps, summary: tools.summary, lan });
79
+ saveProjectStatus(primaryProject(), workspace, 'ready', 'Ready', session.databases ?? [], tools.summary);
72
80
  for (const line of tools.summary)
73
81
  log(' ' + line);
74
82
  await soft('Cleaning old tool builds', () => pruneToolLeftovers());
@@ -82,15 +90,16 @@ const main = async () => {
82
90
  await soft('Claude managed settings', () => writeManagedSettings(gitAccess));
83
91
  step(6, 'Agent: ' + session.harnessName);
84
92
  if (session.harness)
85
- provisionHarness(session.harnessName, session.harness, env);
93
+ await provisionHarness(session.harnessName, session.harness, env);
86
94
  step(7, 'Pre-agent hooks');
87
95
  runHooks('pre-agent', env);
88
96
  step(8, 'Starting ' + session.harnessName);
89
97
  setSandboxStatus('ready', 'Ready');
90
- if (!session.harness) {
98
+ // No agent, or a restart that adds a project (the host opens its sessions itself once the sandbox is ready).
99
+ if (!session.harness || session.autoSession === false) {
91
100
  if (!dryRun)
92
101
  startDaemon(env);
93
- log('Ready (no agent requested). Workspace: ' + workspace);
102
+ log('Ready' + (session.harness ? '' : ' (no agent requested)') + '. Workspace: ' + workspace);
94
103
  return;
95
104
  }
96
105
  if (dryRun) {
@@ -115,6 +124,7 @@ main().catch((e) => {
115
124
  fs.writeFileSync(path.join(logsDir, 'guest-error.txt'), message);
116
125
  }
117
126
  catch { /* ignore */ }
118
- console.error('\n\x1b[31mFATAL: ' + message + '\x1b[0m\nLogs: ' + logsDir);
127
+ // The window gets the plain message; the stack trace is in guest-error.txt.
128
+ console.error('\n\x1b[31mThe sandbox could not start: ' + errMsg(e) + '\x1b[0m\nDetails: ' + path.join(logsDir, 'guest-error.txt'));
119
129
  process.exitCode = 1;
120
130
  });
@@ -0,0 +1,6 @@
1
+ /** A free port on loopback: finding one must not count as "node listens on the network" (that raises firewall prompts). */
2
+ export declare const freePort: () => Promise<number>;
3
+ /** Whether something accepts connections on 127.0.0.1:port. */
4
+ export declare const portOpen: (port: number) => Promise<boolean>;
5
+ /** Polls until the port accepts connections; false when `seconds` pass first. */
6
+ export declare const waitForPort: (port: number, seconds: number) => Promise<boolean>;
@@ -0,0 +1,22 @@
1
+ // Local TCP port helpers shared by the session host (ptyhost) and the databases.
2
+ import net from 'node:net';
3
+ /** A free port on loopback: finding one must not count as "node listens on the network" (that raises firewall prompts). */
4
+ export const freePort = () => new Promise((resolve, reject) => {
5
+ const server = net.createServer();
6
+ server.once('error', reject);
7
+ server.listen(0, '127.0.0.1', () => { const { port } = server.address(); server.close(() => resolve(port)); });
8
+ });
9
+ /** Whether something accepts connections on 127.0.0.1:port. */
10
+ export const portOpen = (port) => new Promise((resolve) => {
11
+ const socket = net.connect({ host: '127.0.0.1', port }, () => { socket.destroy(); resolve(true); });
12
+ socket.once('error', () => resolve(false));
13
+ });
14
+ /** Polls until the port accepts connections; false when `seconds` pass first. */
15
+ export const waitForPort = async (port, seconds) => {
16
+ for (const end = Date.now() + seconds * 1000; Date.now() < end;) {
17
+ if (await portOpen(port))
18
+ return true;
19
+ await new Promise((r) => setTimeout(r, 300));
20
+ }
21
+ return false;
22
+ };
@@ -0,0 +1,7 @@
1
+ export interface ProjectContext {
2
+ workspace: string;
3
+ duringSession: boolean;
4
+ }
5
+ export declare const withProject: <T>(context: ProjectContext, work: () => Promise<T>) => Promise<T>;
6
+ export declare const projectWorkspace: () => string;
7
+ export declare const duringSession: () => boolean;
@@ -0,0 +1,10 @@
1
+ // Which project a toolchain step is provisioning. At boot that is the launch folder; the daemon provisions projects
2
+ // registered later (another folder on the PC) while other sessions keep running, several at once. Steps read version
3
+ // files and markers from projectWorkspace(), and duringSession() tells them to leave machine-wide state alone (the
4
+ // nvm symlink, shims, compiled helpers in use) because sessions of other projects depend on it.
5
+ import { AsyncLocalStorage } from 'node:async_hooks';
6
+ import { workspace } from './env.js';
7
+ const store = new AsyncLocalStorage();
8
+ export const withProject = (context, work) => store.run(context, work);
9
+ export const projectWorkspace = () => store.getStore()?.workspace ?? workspace;
10
+ export const duringSession = () => store.getStore()?.duringSession ?? false;
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ export declare const BootEnv: z.ZodObject<{
3
+ basePath: z.ZodString;
4
+ toolVars: z.ZodArray<z.ZodString>;
5
+ steps: z.ZodRecord<z.ZodString, z.ZodObject<{
6
+ vars: z.ZodRecord<z.ZodString, z.ZodString>;
7
+ prepend: z.ZodArray<z.ZodString>;
8
+ append: z.ZodArray<z.ZodString>;
9
+ summary: z.ZodArray<z.ZodString>;
10
+ }, z.core.$strip>>;
11
+ summary: z.ZodArray<z.ZodString>;
12
+ lan: z.ZodEnum<{
13
+ open: "open";
14
+ blocked: "blocked";
15
+ offline: "offline";
16
+ }>;
17
+ }, z.core.$strip>;
18
+ export type BootEnvT = z.output<typeof BootEnv>;
19
+ /** The environment a later project starts from: the daemon's own, without the boot project's toolchains. */
20
+ export declare const baseEnvFor: (boot: BootEnvT | null, current?: NodeJS.ProcessEnv) => NodeJS.ProcessEnv;
@@ -0,0 +1,20 @@
1
+ // Pure (no guest state): the environment a project that joined after boot starts from. Kept apart from boot-env.ts,
2
+ // which reads the guest's session, so the host-side tests can import it.
3
+ import { z } from 'zod';
4
+ const Env = z.object({ vars: z.record(z.string(), z.string()), prepend: z.array(z.string()), append: z.array(z.string()), summary: z.array(z.string()) });
5
+ export const BootEnv = z.object({
6
+ basePath: z.string(), toolVars: z.array(z.string()), steps: z.record(z.string(), Env),
7
+ /** The boot's tool summary and LAN state, for the agent guide that lists every project. */
8
+ summary: z.array(z.string()), lan: z.enum(['blocked', 'open', 'offline']),
9
+ });
10
+ /** Variables the boot set for its own project (its workspace, its toolchains): never inherited by another project. */
11
+ const PROJECT_VARS = /^(AGENT_WORKSPACE|AGENT_PROJECT|AGENT_SANDBOX_WORKSPACE|AGENT_PROJECT_FILE|CARGO_TARGET_DIR|VIRTUAL_ENV)$/i;
12
+ /** The environment a later project starts from: the daemon's own, without the boot project's toolchains. */
13
+ export const baseEnvFor = (boot, current = process.env) => {
14
+ if (!boot)
15
+ return { ...current };
16
+ const drop = new Set(boot.toolVars.map((v) => v.toLowerCase()));
17
+ // Windows spells it Path; a plain object would keep both spellings.
18
+ const base = Object.fromEntries(Object.entries(current).filter(([k]) => !drop.has(k.toLowerCase()) && !PROJECT_VARS.test(k) && k.toLowerCase() !== 'path'));
19
+ return { ...base, PATH: boot.basePath };
20
+ };
@@ -0,0 +1,5 @@
1
+ import type { DatabasePlan } from '../core/types.js';
2
+ import type { ProjectStatusT } from '../shared/schema.js';
3
+ /** The launch folder's project id (older launch folders: derived from its name). */
4
+ export declare const primaryProject: () => string;
5
+ export declare const saveProjectStatus: (name: string, workspace: string, phase: ProjectStatusT["phase"], message: string, databases: DatabasePlan[], summary: string[]) => void;
@@ -0,0 +1,17 @@
1
+ import { now, writeJsonAtomic } from '../shared/rundir.js';
2
+ import { hubName } from '../shared/projects.js';
3
+ import { errMsg, run, session, warn } from './env.js';
4
+ /** The launch folder's project id (older launch folders: derived from its name). */
5
+ export const primaryProject = () => session.projectId ?? hubName(session.workspaceHost || session.project, []);
6
+ export const saveProjectStatus = (name, workspace, phase, message, databases, summary) => {
7
+ try {
8
+ writeJsonAtomic(run.project(name), {
9
+ name, workspace, phase, message: message.slice(0, 3500), updatedAt: now(),
10
+ databases: databases.filter((d) => d.database !== 'sqlserver').slice(0, 16).map((d) => ({ database: d.database, port: d.port })),
11
+ summary: summary.slice(0, 64).map((s) => s.slice(0, 500)),
12
+ });
13
+ }
14
+ catch (e) {
15
+ warn('Could not write the status of project ' + name + ': ' + errMsg(e));
16
+ }
17
+ };
@@ -1,21 +1,5 @@
1
1
  import { type ChildProcess } from 'node:child_process';
2
- export declare const quoteArg: (arg: string) => string;
3
- /**
4
- * Arguments on a `cmd /d /c <agent>.cmd ...` line (npm agents). cmd parses them twice: the /c line, then the .cmd's %*.
5
- * & | < > ^ are literal inside quotes, so arguments with them are quoted (https://x?a=1&b=2 survives). % expands even
6
- * inside quotes, so each one sits between quoted pieces ("50"%" off"): every %name% cmd could see then starts with a
7
- * quote, and no variable is called that. cmd's quote state runs across the whole line (an embedded \" toggles it too),
8
- * so a metacharacter it would see outside quotes gets ^^^ (one caret left after each parse). Arguments without any
9
- * of these are quoted as usual.
10
- */
11
- export declare const cmdArgs: (args: string[]) => string;
12
- /** The command line ptyhost starts; when the program is cmd.exe its arguments are quoted for cmd (cmdArgs). */
13
- export declare const commandLine: (file: string, args: string[]) => string;
14
- export type Helper = 'ptyhost' | 'gitshim' | 'vswhere';
15
- /** %TEMP%\agent-sandbox\<name>-<hash>.exe, compiled from this launch's sandbox\<name>.cs the first time it is needed. */
16
- export declare const ensureCompiled: (name: Helper) => string;
17
2
  export declare const ensurePtyHost: () => string;
18
- export declare const freePort: () => Promise<number>;
19
3
  /** Resolves when ptyhost listens; rejects early if it exits first (e.g. the agent could not be started). */
20
4
  export declare const waitForPtyHost: (child: ChildProcess, port: number, timeoutMs: number) => Promise<void>;
21
5
  export declare const ptyHostLog: (id: string) => string;
@@ -36,5 +20,3 @@ export interface PtyHostOptions {
36
20
  }
37
21
  /** Starts ptyhost detached, so closing a terminal tab never kills the agent; only the agent exiting ends it. */
38
22
  export declare const startPtyHost: (o: PtyHostOptions) => ChildProcess;
39
- /** Removes helper builds older versions left in persistent storage, and interrupted downloads in the tools folder. */
40
- export declare const pruneToolLeftovers: () => void;
@@ -1,62 +1,11 @@
1
- // Builds the sandbox's small C# helpers (ptyhost.cs, gitshim.cs) with the .NET Framework compiler that ships with
2
- // Windows, and starts one ptyhost per agent session.
3
- //
4
- // The helpers are compiled onto the sandbox's own disk (wiped on close) on every boot, never cached in persistent
5
- // storage: a tampered ptyhost.exe there would survive the wipe and see every keystroke typed from this PC.
1
+ // Starts one ptyhost (the ConPTY bridge, sandbox/ptyhost.cs) per agent session and waits for it to listen.
6
2
  import fs from 'node:fs';
7
- import net from 'node:net';
8
- import os from 'node:os';
9
3
  import path from 'node:path';
10
- import crypto from 'node:crypto';
11
- import { spawn, spawnSync } from 'node:child_process';
12
- import { exists, launchDir, logsDir, mkdirp, toolsDir } from './env.js';
13
- const CSC = path.join(process.env.WINDIR ?? 'C:\\Windows', 'Microsoft.NET', 'Framework64', 'v4.0.30319', 'csc.exe');
14
- const quoted = (arg) => '"' + arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1') + '"';
15
- export const quoteArg = (arg) => (arg !== '' && !/[\s"]/.test(arg) ? arg : quoted(arg));
16
- /**
17
- * Arguments on a `cmd /d /c <agent>.cmd ...` line (npm agents). cmd parses them twice: the /c line, then the .cmd's %*.
18
- * & | < > ^ are literal inside quotes, so arguments with them are quoted (https://x?a=1&b=2 survives). % expands even
19
- * inside quotes, so each one sits between quoted pieces ("50"%" off"): every %name% cmd could see then starts with a
20
- * quote, and no variable is called that. cmd's quote state runs across the whole line (an embedded \" toggles it too),
21
- * so a metacharacter it would see outside quotes gets ^^^ (one caret left after each parse). Arguments without any
22
- * of these are quoted as usual.
23
- */
24
- export const cmdArgs = (args) => {
25
- const pieces = args.map((arg) => (/[&|<>^%]/.test(arg) ? arg.split('%').map(quoted).join('%') : quoteArg(arg))).join(' ');
26
- let inQuotes = false;
27
- return [...pieces].map((c) => {
28
- if (c === '"')
29
- inQuotes = !inQuotes;
30
- return !inQuotes && /[&|<>^]/.test(c) ? '^^^' + c : c;
31
- }).join('');
32
- };
33
- /** The command line ptyhost starts; when the program is cmd.exe its arguments are quoted for cmd (cmdArgs). */
34
- export const commandLine = (file, args) => path.win32.basename(file).toLowerCase() === 'cmd.exe' ? [quoteArg(file), cmdArgs(args)].filter(Boolean).join(' ') : [file, ...args].map(quoteArg).join(' ');
35
- const sourceHash = (name) => crypto.createHash('sha256').update(fs.readFileSync(path.join(launchDir, 'sandbox', name + '.cs'))).digest('hex').slice(0, 12);
36
- /** %TEMP%\agent-sandbox\<name>-<hash>.exe, compiled from this launch's sandbox\<name>.cs the first time it is needed. */
37
- export const ensureCompiled = (name) => {
38
- const exe = path.join(os.tmpdir(), 'agent-sandbox', name + '-' + sourceHash(name) + '.exe');
39
- if (exists(exe))
40
- return exe;
41
- if (!exists(CSC))
42
- throw new Error('The .NET Framework C# compiler was not found at ' + CSC + ' (it ships with Windows 10/11).');
43
- mkdirp(path.dirname(exe));
44
- const result = spawnSync(CSC, ['/nologo', '/optimize', '/target:exe', '/out:' + exe, path.join(launchDir, 'sandbox', name + '.cs')], { encoding: 'utf8', windowsHide: true });
45
- if (result.status !== 0)
46
- throw new Error('Compiling ' + name + ' failed: ' + ((result.stdout ?? '') + (result.stderr ?? '')).trim().slice(0, 1500));
47
- return exe;
48
- };
4
+ import { spawn } from 'node:child_process';
5
+ import { ensureCompiled } from './helpers.js';
6
+ import { portOpen } from './ports.js';
7
+ import { logsDir } from './env.js';
49
8
  export const ensurePtyHost = () => ensureCompiled('ptyhost');
50
- export const freePort = () => new Promise((resolve, reject) => {
51
- const server = net.createServer();
52
- server.once('error', reject);
53
- // Loopback: finding a port must not count as "node listens on the network" (that is what raises firewall prompts).
54
- server.listen(0, '127.0.0.1', () => { const { port } = server.address(); server.close(() => resolve(port)); });
55
- });
56
- const portOpen = (port) => new Promise((resolve) => {
57
- const socket = net.connect({ host: '127.0.0.1', port }, () => { socket.destroy(); resolve(true); });
58
- socket.once('error', () => resolve(false));
59
- });
60
9
  /** Resolves when ptyhost listens; rejects early if it exits first (e.g. the agent could not be started). */
61
10
  export const waitForPtyHost = async (child, port, timeoutMs) => {
62
11
  let exitCode;
@@ -85,10 +34,3 @@ export const ptyHostLogTail = (id, lines = 12) => {
85
34
  export const startPtyHost = (o) => spawn(o.exe, [String(o.port), o.token, String(o.cols), String(o.rows), ptyHostLog(o.id), o.command, o.restartCommand], {
86
35
  cwd: o.cwd, env: { ...o.env, PTYHOST_BIND: o.remote ? 'any' : 'loopback' }, detached: true, stdio: 'ignore', windowsHide: true,
87
36
  });
88
- /** Removes helper builds older versions left in persistent storage, and interrupted downloads in the tools folder. */
89
- export const pruneToolLeftovers = () => {
90
- for (const legacy of ['ptyhost', 'gitshim'])
91
- fs.rmSync(path.join(toolsDir, legacy), { recursive: true, force: true });
92
- for (const name of fs.readdirSync(toolsDir).filter((n) => /\.staging-[0-9a-f]+$/i.test(n)))
93
- fs.rmSync(path.join(toolsDir, name), { recursive: true, force: true });
94
- };
@@ -1,6 +1,9 @@
1
1
  import type { Harness } from '../../core/types.js';
2
- /** Copies the agent from this PC when it was mapped in (primary agent only), otherwise installs it with npm or uv. */
3
- export declare const provisionHarness: (name: string, harness: Harness, env: NodeJS.ProcessEnv, refresh?: boolean) => void;
2
+ /**
3
+ * Copies the agent from this PC when it was mapped in (primary agent only), otherwise installs it with npm or uv.
4
+ * Asynchronous, so the session daemon keeps serving other requests while an agent installs.
5
+ */
6
+ export declare const provisionHarness: (name: string, harness: Harness, env: NodeJS.ProcessEnv, refresh?: boolean) => Promise<void>;
4
7
  /** Program + arguments that start the agent (an npm .cmd shim has to go through cmd.exe). */
5
8
  export declare const resolveLaunch: (name: string, harness: Harness) => {
6
9
  file: string;
@@ -2,14 +2,15 @@
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { SYSTEM } from '../../shared/system.js';
5
- import { dryRun, exec, exists, hostToolsDir, isWin, log, mkdirp, session, toolsDir, warn } from '../env.js';
5
+ import { uvVars } from '../toolchains/runtimes.js';
6
+ import { dryRun, execAsync, exists, hostToolsDir, isWin, log, mkdirp, session, toolsDir, warn } from '../env.js';
6
7
  const harnessPaths = (name, harness) => {
7
8
  const dir = path.join(toolsDir, 'agents', name);
8
9
  const exe = harness.hostExecutable ?? (harness.uvBin ? harness.uvBin + '.exe' : null);
9
10
  return { dir, exe: exe ? path.join(dir, exe) : null, cmd: harness.npmBin ? path.join(dir, harness.npmBin + '.cmd') : null };
10
11
  };
11
12
  /** npm install with the runner's own npm into tools\agents\<name> (no global state outside persistent storage). */
12
- const installViaNpm = (name, harness, dir, env) => {
13
+ const installViaNpm = async (name, harness, dir, env) => {
13
14
  if (!harness.npmPackage)
14
15
  throw new Error('No npmPackage configured for ' + name + ' and no host executable was available.');
15
16
  const npmCli = path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
@@ -21,10 +22,10 @@ const installViaNpm = (name, harness, dir, env) => {
21
22
  }
22
23
  if (!exists(npmCli))
23
24
  throw new Error('npm not found at ' + npmCli);
24
- exec(process.execPath, args, { env });
25
+ await execAsync(process.execPath, args, { env });
25
26
  };
26
27
  /** `uv tool install` with its command put straight into tools\agents\<name> (Python agents such as aider). */
27
- const installViaUv = (name, harness, dir, env) => {
28
+ const installViaUv = async (name, harness, dir, env) => {
28
29
  const uv = path.join(toolsDir, 'uv', 'uv.exe');
29
30
  log('Installing ' + harness.uvPackage + ' with uv (this can take a minute)...');
30
31
  if (dryRun) {
@@ -33,10 +34,23 @@ const installViaUv = (name, harness, dir, env) => {
33
34
  }
34
35
  if (!exists(uv))
35
36
  throw new Error(name + ' is installed with uv; enable toolchains.uv');
36
- exec(uv, ['tool', 'install', '--force', '--python', '3.12', harness.uvPackage], { env: { ...env, UV_TOOL_BIN_DIR: dir, UV_TOOL_DIR: path.join(toolsDir, 'uv-tools'), UV_PYTHON_INSTALL_DIR: path.join(toolsDir, 'uv-python'), UV_CACHE_DIR: path.join(toolsDir, 'uv-cache') } });
37
+ await execAsync(uv, ['tool', 'install', '--force', '--python', '3.12', harness.uvPackage], { env: { ...env, ...uvVars(), UV_TOOL_BIN_DIR: dir } });
37
38
  };
38
- /** Copies the agent from this PC when it was mapped in (primary agent only), otherwise installs it with npm or uv. */
39
+ /** One install per agent at a time: two sessions asking for a new agent at once share the first one's install. */
40
+ const installing = new Map();
41
+ /**
42
+ * Copies the agent from this PC when it was mapped in (primary agent only), otherwise installs it with npm or uv.
43
+ * Asynchronous, so the session daemon keeps serving other requests while an agent installs.
44
+ */
39
45
  export const provisionHarness = (name, harness, env, refresh = session.provisioning.refresh) => {
46
+ const current = installing.get(name);
47
+ if (current)
48
+ return current;
49
+ const work = installHarness(name, harness, env, refresh).finally(() => installing.delete(name));
50
+ installing.set(name, work);
51
+ return work;
52
+ };
53
+ const installHarness = async (name, harness, env, refresh) => {
40
54
  const p = harnessPaths(name, harness);
41
55
  const cached = (p.exe !== null && exists(p.exe)) || (p.cmd !== null && exists(p.cmd));
42
56
  if (cached && !refresh) {
@@ -56,9 +70,9 @@ export const provisionHarness = (name, harness, env, refresh = session.provision
56
70
  warn('Host copy of ' + name + ' not available; installing instead.');
57
71
  }
58
72
  if (harness.uvPackage && !harness.npmPackage)
59
- installViaUv(name, harness, p.dir, env);
73
+ await installViaUv(name, harness, p.dir, env);
60
74
  else
61
- installViaNpm(name, harness, p.dir, env);
75
+ await installViaNpm(name, harness, p.dir, env);
62
76
  };
63
77
  /** Program + arguments that start the agent (an npm .cmd shim has to go through cmd.exe). */
64
78
  export const resolveLaunch = (name, harness) => {
@@ -3,6 +3,8 @@ import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { translatePaths } from '../../shared/paths.js';
6
+ import { writeJsonAtomic } from '../../shared/rundir.js';
7
+ import { safeRemove } from '../../shared/safe-remove.js';
6
8
  import { claudeDir, dryRun, exists, launchDir, log, mkdirp, robocopy, session, stateDir, warn, workspace } from '../env.js';
7
9
  /** The sandbox keeps its own login and global state; everything else is refreshed from this PC each boot. */
8
10
  const KEEP_SANDBOX_COPY = ['.claude.json', '.credentials.json'];
@@ -41,7 +43,7 @@ export const mergeMcpServers = (hostFile, guestFile, hostWorkspace, guestWorkspa
41
43
  const guestKey = Object.keys(projects).find((key) => sameFolder(key, guestWorkspace)) ?? guestWorkspace.replace(/\\/g, '/');
42
44
  projects[guestKey] = { ...(projects[guestKey] ?? {}), mcpServers: { ...(projects[guestKey]?.mcpServers ?? {}), ...hostProject } };
43
45
  const merged = { ...guest, mcpServers: { ...(guest.mcpServers ?? {}), ...(host.mcpServers ?? {}) }, projects };
44
- fs.writeFileSync(guestFile, JSON.stringify(merged, null, 2) + '\n', 'utf8');
46
+ writeJsonAtomic(guestFile, merged);
45
47
  return Object.keys(host.mcpServers ?? {}).length + Object.keys(hostProject).length;
46
48
  };
47
49
  export const syncClaudeProfile = () => {
@@ -63,7 +65,7 @@ export const syncClaudeProfile = () => {
63
65
  }
64
66
  // Folders (skills, commands, plugins) are mirrored: what was removed on this PC goes here too.
65
67
  if (fs.statSync(from).isDirectory())
66
- fs.rmSync(to, { recursive: true, force: true });
68
+ safeRemove(to, claudeDir);
67
69
  fs.cpSync(from, to, { recursive: true, force: true });
68
70
  log('Claude settings: copied ' + name + ' from this PC');
69
71
  }
@@ -81,7 +83,13 @@ export const syncClaudeProfile = () => {
81
83
  export const claudeMemPaths = { store: path.join(stateDir, 'claude-mem'), local: path.join(os.homedir(), '.claude-mem') };
82
84
  const claudeMemEnabled = () => session.claudeProfile.syncClaudeMem && !dryRun;
83
85
  export const restoreClaudeMem = () => {
84
- if (!claudeMemEnabled() || !exists(claudeMemPaths.store))
86
+ if (!claudeMemEnabled())
87
+ return;
88
+ // A save cut off between its two renames (see saveClaudeMem) leaves only the aside copy: it is the newest data.
89
+ const previous = claudeMemPaths.store + '.previous';
90
+ if (!exists(claudeMemPaths.store) && exists(previous))
91
+ fs.renameSync(previous, claudeMemPaths.store);
92
+ if (!exists(claudeMemPaths.store))
85
93
  return;
86
94
  robocopy(claudeMemPaths.store, claudeMemPaths.local);
87
95
  log('claude-mem data restored from persistent storage.');
@@ -1,6 +1,16 @@
1
1
  import type { ToolEnv } from '../toolchains/index.js';
2
- export declare const agentEnv: (tools: Pick<ToolEnv, "vars" | "prepend" | "append">) => NodeJS.ProcessEnv;
2
+ /** Whose environment: the launch folder's at boot, or a project registered later (with the boot's base environment). */
3
+ export interface EnvTarget {
4
+ workspace: string;
5
+ project: string;
6
+ base: NodeJS.ProcessEnv;
7
+ }
8
+ export declare const agentEnv: (tools: Pick<ToolEnv, "vars" | "prepend" | "append">, target?: EnvTarget) => NodeJS.ProcessEnv;
3
9
  /** Makes the agent environment machine-wide, so new terminal tabs and anything the agent starts see the same tools. */
4
10
  export declare const persistEnvironment: (env: NodeJS.ProcessEnv, toolVars: string[]) => void;
5
- /** Adds variables and PATH folders machine-wide after boot (`sbx-db start`): new terminal tabs see them. */
6
- export declare const addMachineEnvironment: (vars: Record<string, string>, dirs: string[]) => void;
11
+ /**
12
+ * Adds variables and PATH folders machine-wide after boot (`sbx-db start`): new terminal tabs see them.
13
+ * additive: for a project registered later. Only variables not set yet, and folders go to the END of PATH, so what
14
+ * the boot project's terminals see never changes; the project's own sessions get its full environment anyway.
15
+ */
16
+ export declare const addMachineEnvironment: (vars: Record<string, string>, dirs: string[], additive?: boolean) => void;
@@ -3,28 +3,28 @@ import path from 'node:path';
3
3
  import { claudeDir, dataDir, dryRun, errMsg, isWin, testMode, mkdirp, powershell, session, stateDir, toolsDir, warn, workspace } from '../env.js';
4
4
  /** Variables agent-sandbox itself sets (besides the toolchains' own). */
5
5
  const AGENT_VARS = ['CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'GEMINI_CLI_HOME', 'npm_config_cache', 'npm_config_update_notifier', 'AGENT_WORKSPACE', 'AGENT_DATA', 'AGENT_TOOLS', 'AGENT_PROJECT', 'AGENT_SANDBOX', 'AGENT_SANDBOX_WORKSPACE'];
6
- export const agentEnv = (tools) => {
6
+ export const agentEnv = (tools, target = { workspace, project: session.project, base: process.env }) => {
7
7
  const env = {
8
- ...process.env,
8
+ ...target.base,
9
9
  ...tools.vars,
10
10
  CLAUDE_CONFIG_DIR: claudeDir,
11
11
  CODEX_HOME: path.join(stateDir, 'codex'),
12
12
  GEMINI_CLI_HOME: path.join(stateDir, 'gemini'),
13
13
  npm_config_cache: path.join(toolsDir, 'npm-cache'),
14
14
  npm_config_update_notifier: 'false',
15
- AGENT_WORKSPACE: workspace,
15
+ AGENT_WORKSPACE: target.workspace,
16
16
  AGENT_DATA: dataDir,
17
17
  AGENT_TOOLS: toolsDir,
18
- AGENT_PROJECT: session.project,
18
+ AGENT_PROJECT: target.project,
19
19
  // For project scripts: "am I in the sandbox, and where is the project?" (stable names; documented).
20
20
  AGENT_SANDBOX: '1',
21
- AGENT_SANDBOX_WORKSPACE: workspace,
21
+ AGENT_SANDBOX_WORKSPACE: target.workspace,
22
22
  };
23
23
  for (const dir of [env.CLAUDE_CONFIG_DIR, env.CODEX_HOME, env.GEMINI_CLI_HOME, env.npm_config_cache])
24
24
  mkdirp(dir);
25
25
  // The runner's own Node (bootstrap.ps1 puts it first on PATH) is not exposed: `node` is whatever nvm selected.
26
26
  const runners = [path.join(toolsDir, 'node'), path.dirname(process.execPath)].map((d) => d.toLowerCase());
27
- const base = (process.env.PATH ?? '').split(path.delimiter).filter((d) => d && !runners.includes(d.toLowerCase()));
27
+ const base = (target.base.PATH ?? '').split(path.delimiter).filter((d) => d && !runners.includes(d.toLowerCase()));
28
28
  env.PATH = [...new Set([...tools.prepend, ...base, ...tools.append])].join(path.delimiter);
29
29
  return env;
30
30
  };
@@ -45,16 +45,20 @@ export const persistEnvironment = (env, toolVars) => {
45
45
  warn('Could not persist the environment machine-wide: ' + errMsg(e));
46
46
  }
47
47
  };
48
- /** Adds variables and PATH folders machine-wide after boot (`sbx-db start`): new terminal tabs see them. */
49
- export const addMachineEnvironment = (vars, dirs) => {
48
+ /**
49
+ * Adds variables and PATH folders machine-wide after boot (`sbx-db start`): new terminal tabs see them.
50
+ * additive: for a project registered later. Only variables not set yet, and folders go to the END of PATH, so what
51
+ * the boot project's terminals see never changes; the project's own sessions get its full environment anyway.
52
+ */
53
+ export const addMachineEnvironment = (vars, dirs, additive = false) => {
50
54
  if (!isWin || dryRun || testMode)
51
55
  return;
52
56
  const script = [
53
57
  "$vars = '" + JSON.stringify(vars).replace(/'/g, "''") + "' | ConvertFrom-Json",
54
- "foreach ($p in $vars.PSObject.Properties) { [Environment]::SetEnvironmentVariable($p.Name, [string]$p.Value, 'Machine') }",
58
+ "foreach ($p in $vars.PSObject.Properties) { " + (additive ? "if ([Environment]::GetEnvironmentVariable($p.Name, 'Machine')) { continue }; " : '') + "[Environment]::SetEnvironmentVariable($p.Name, [string]$p.Value, 'Machine') }",
55
59
  "$path = [Environment]::GetEnvironmentVariable('Path', 'Machine') -split ';'",
56
60
  "$add = '" + JSON.stringify(dirs).replace(/'/g, "''") + "' | ConvertFrom-Json | Where-Object { $path -notcontains $_ }",
57
- "if ($add) { [Environment]::SetEnvironmentVariable('Path', ((@($add) + $path) -join ';'), 'Machine') }",
61
+ "if ($add) { [Environment]::SetEnvironmentVariable('Path', (" + (additive ? '($path + @($add))' : '(@($add) + $path)') + " -join ';'), 'Machine') }",
58
62
  ].join('\n');
59
63
  try {
60
64
  powershell(script);
@@ -1 +1,9 @@
1
- export declare const runHooks: (stage: string, env: NodeJS.ProcessEnv) => void;
1
+ /** Hooks of the launch folder (C:\AgentLaunch\hooks), or of a project registered later (...\projects\<name>\hooks). */
2
+ export interface HookTarget {
3
+ root: string;
4
+ cwd: string;
5
+ }
6
+ export declare const projectHooks: (name: string, cwd: string) => HookTarget;
7
+ export declare const runHooks: (stage: string, env: NodeJS.ProcessEnv, target?: HookTarget) => void;
8
+ /** The same without blocking: the session daemon keeps answering other requests while a project's hooks run. */
9
+ export declare const runHooksAsync: (stage: string, env: NodeJS.ProcessEnv, target: HookTarget) => Promise<void>;