windows-development-sandbox 1.0.5 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (453) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -8
  3. package/config.example.json +55 -5
  4. package/dist/api.js +2 -1
  5. package/dist/cli/args.d.ts +23 -0
  6. package/dist/cli/args.js +38 -4
  7. package/dist/cli/colors.d.ts +24 -0
  8. package/dist/cli/colors.js +42 -0
  9. package/dist/cli/commands/accounts.js +2 -2
  10. package/dist/cli/commands/agent-commands.d.ts +6 -0
  11. package/dist/cli/commands/agent-commands.js +61 -0
  12. package/dist/cli/commands/context.d.ts +2 -0
  13. package/dist/cli/commands/context.js +5 -1
  14. package/dist/cli/commands/data/clean.d.ts +15 -0
  15. package/dist/cli/commands/data/clean.js +106 -0
  16. package/dist/cli/commands/data/collect.d.ts +11 -0
  17. package/dist/cli/commands/data/collect.js +54 -0
  18. package/dist/cli/commands/data/index.d.ts +4 -0
  19. package/dist/cli/commands/data/index.js +45 -0
  20. package/dist/cli/commands/data/remove.d.ts +2 -0
  21. package/dist/cli/commands/data/remove.js +30 -0
  22. package/dist/cli/commands/index.d.ts +6 -4
  23. package/dist/cli/commands/index.js +6 -4
  24. package/dist/cli/commands/launch.d.ts +26 -2
  25. package/dist/cli/commands/launch.js +256 -42
  26. package/dist/cli/commands/maintenance.d.ts +2 -0
  27. package/dist/cli/commands/maintenance.js +67 -5
  28. package/dist/cli/commands/project.d.ts +29 -0
  29. package/dist/cli/commands/project.js +63 -0
  30. package/dist/cli/commands/projects.d.ts +14 -0
  31. package/dist/cli/commands/projects.js +146 -0
  32. package/dist/cli/commands/repair/checks.d.ts +2 -0
  33. package/dist/cli/commands/{repair.js → repair/checks.js} +19 -181
  34. package/dist/cli/commands/{repair.d.ts → repair/finders.d.ts} +0 -29
  35. package/dist/cli/commands/repair/finders.js +108 -0
  36. package/dist/cli/commands/repair/index.d.ts +12 -0
  37. package/dist/cli/commands/repair/index.js +61 -0
  38. package/dist/cli/commands/repair/types.d.ts +26 -0
  39. package/dist/cli/commands/repair/types.js +3 -0
  40. package/dist/cli/commands/restart.d.ts +6 -0
  41. package/dist/cli/commands/restart.js +85 -0
  42. package/dist/cli/commands/sessions.d.ts +2 -1
  43. package/dist/cli/commands/sessions.js +189 -45
  44. package/dist/cli/commands/setup/checks.d.ts +13 -0
  45. package/dist/cli/commands/setup/checks.js +71 -0
  46. package/dist/cli/commands/{setup.d.ts → setup/facts.d.ts} +1 -15
  47. package/dist/cli/commands/setup/facts.js +60 -0
  48. package/dist/cli/commands/setup/index.d.ts +2 -0
  49. package/dist/cli/commands/setup/index.js +43 -0
  50. package/dist/cli/commands/user-config.d.ts +8 -0
  51. package/dist/cli/commands/user-config.js +41 -0
  52. package/dist/cli/history.d.ts +11 -0
  53. package/dist/cli/history.js +21 -0
  54. package/dist/cli/index.d.ts +7 -1
  55. package/dist/cli/index.js +42 -15
  56. package/dist/cli/manifest.d.ts +8 -0
  57. package/dist/cli/manifest.js +40 -15
  58. package/dist/cli/menu/apps.d.ts +21 -0
  59. package/dist/cli/menu/apps.js +122 -0
  60. package/dist/cli/menu/command-form.d.ts +28 -0
  61. package/dist/cli/menu/command-form.js +219 -0
  62. package/dist/cli/menu/index.d.ts +26 -0
  63. package/dist/cli/menu/index.js +180 -0
  64. package/dist/cli/menu/settings/categories.d.ts +9 -0
  65. package/dist/cli/menu/settings/categories.js +28 -0
  66. package/dist/cli/menu/settings/config-file.d.ts +40 -0
  67. package/dist/cli/menu/settings/config-file.js +71 -0
  68. package/dist/cli/menu/settings/editors.d.ts +30 -0
  69. package/dist/cli/menu/settings/editors.js +305 -0
  70. package/dist/cli/menu/settings/index.d.ts +32 -0
  71. package/dist/cli/menu/settings/index.js +136 -0
  72. package/dist/cli/menu/settings/schema-node.d.ts +62 -0
  73. package/dist/cli/menu/settings/schema-node.js +104 -0
  74. package/dist/cli/menu/several.d.ts +20 -0
  75. package/dist/cli/menu/several.js +102 -0
  76. package/dist/cli/progress.d.ts +9 -0
  77. package/dist/cli/progress.js +49 -0
  78. package/dist/cli/tui/app.d.ts +39 -0
  79. package/dist/cli/tui/app.js +52 -0
  80. package/dist/cli/tui/dialogs.d.ts +57 -0
  81. package/dist/cli/tui/dialogs.js +162 -0
  82. package/dist/cli/tui/frame.d.ts +18 -0
  83. package/dist/cli/tui/frame.js +27 -0
  84. package/dist/cli/tui/index.d.ts +7 -0
  85. package/dist/cli/tui/index.js +8 -0
  86. package/dist/cli/tui/list-screen.d.ts +101 -0
  87. package/dist/cli/tui/list-screen.js +299 -0
  88. package/dist/cli/tui/terminal.d.ts +37 -0
  89. package/dist/cli/tui/terminal.js +89 -0
  90. package/dist/cli/tui/text.d.ts +20 -0
  91. package/dist/cli/tui/text.js +105 -0
  92. package/dist/cli/tui/theme.d.ts +45 -0
  93. package/dist/cli/tui/theme.js +30 -0
  94. package/dist/cli/validate.d.ts +17 -0
  95. package/dist/cli/validate.js +89 -0
  96. package/dist/core/config.js +12 -6
  97. package/dist/core/disk.d.ts +16 -0
  98. package/dist/core/disk.js +55 -0
  99. package/dist/core/local-config.d.ts +23 -0
  100. package/dist/core/local-config.js +79 -7
  101. package/dist/core/project-policy.d.ts +15 -0
  102. package/dist/core/project-policy.js +35 -0
  103. package/dist/core/prompt.d.ts +4 -0
  104. package/dist/core/prompt.js +15 -0
  105. package/dist/core/safe-paths.d.ts +4 -0
  106. package/dist/core/safe-paths.js +37 -0
  107. package/dist/core/schema/apps.d.ts +40 -0
  108. package/dist/core/schema/apps.js +27 -0
  109. package/dist/core/schema/common.d.ts +17 -0
  110. package/dist/core/schema/common.js +15 -0
  111. package/dist/core/schema/config.d.ts +659 -0
  112. package/dist/core/schema/config.js +75 -0
  113. package/dist/core/schema/databases.d.ts +33 -0
  114. package/dist/core/schema/databases.js +35 -0
  115. package/dist/core/schema/harness.d.ts +12 -0
  116. package/dist/core/schema/harness.js +17 -0
  117. package/dist/core/schema/json-schema.d.ts +5 -0
  118. package/dist/core/schema/json-schema.js +25 -0
  119. package/dist/core/schema/profile.d.ts +32 -0
  120. package/dist/core/schema/profile.js +31 -0
  121. package/dist/core/schema/projects.d.ts +9 -0
  122. package/dist/core/schema/projects.js +11 -0
  123. package/dist/core/schema/sandbox.d.ts +78 -0
  124. package/dist/core/schema/sandbox.js +64 -0
  125. package/dist/core/schema/toolchains.d.ts +96 -0
  126. package/dist/core/schema/toolchains.js +88 -0
  127. package/dist/core/schema.d.ts +6 -604
  128. package/dist/core/schema.js +8 -264
  129. package/dist/core/types.d.ts +35 -1
  130. package/dist/core/util.d.ts +9 -0
  131. package/dist/core/util.js +14 -3
  132. package/dist/core/workspace.d.ts +0 -5
  133. package/dist/core/workspace.js +33 -50
  134. package/dist/guest/apps/index.d.ts +10 -0
  135. package/dist/guest/apps/index.js +206 -0
  136. package/dist/guest/apps/resolvers.d.ts +8 -0
  137. package/dist/guest/apps/resolvers.js +90 -0
  138. package/dist/guest/audit-parse.d.ts +7 -0
  139. package/dist/guest/audit-parse.js +24 -0
  140. package/dist/guest/audit.d.ts +4 -0
  141. package/dist/guest/audit.js +47 -0
  142. package/dist/guest/boot-env.d.ts +4 -0
  143. package/dist/guest/boot-env.js +25 -0
  144. package/dist/guest/cmdline.d.ts +12 -0
  145. package/dist/guest/cmdline.js +23 -0
  146. package/dist/guest/daemon/exec.d.ts +5 -0
  147. package/dist/guest/daemon/exec.js +32 -0
  148. package/dist/guest/daemon/project.d.ts +14 -0
  149. package/dist/guest/daemon/project.js +132 -0
  150. package/dist/guest/daemon/relays.d.ts +1 -0
  151. package/dist/guest/daemon/relays.js +18 -0
  152. package/dist/guest/daemon/session.d.ts +13 -0
  153. package/dist/guest/daemon/session.js +139 -0
  154. package/dist/guest/daemon/shutdown.d.ts +1 -0
  155. package/dist/guest/daemon/shutdown.js +21 -0
  156. package/dist/guest/daemon/state.d.ts +17 -0
  157. package/dist/guest/daemon/state.js +31 -0
  158. package/dist/guest/daemon.js +12 -141
  159. package/dist/guest/db.js +18 -3
  160. package/dist/guest/env.d.ts +3 -0
  161. package/dist/guest/env.js +7 -3
  162. package/dist/guest/guide.d.ts +6 -2
  163. package/dist/guest/guide.js +5 -4
  164. package/dist/guest/helpers.d.ts +3 -0
  165. package/dist/guest/helpers.js +26 -0
  166. package/dist/guest/main.js +31 -8
  167. package/dist/guest/netproxy.d.ts +1 -0
  168. package/dist/guest/netproxy.js +100 -0
  169. package/dist/guest/ports.d.ts +6 -0
  170. package/dist/guest/ports.js +22 -0
  171. package/dist/guest/project-context.d.ts +7 -0
  172. package/dist/guest/project-context.js +10 -0
  173. package/dist/guest/project-env.d.ts +20 -0
  174. package/dist/guest/project-env.js +20 -0
  175. package/dist/guest/project-status.d.ts +5 -0
  176. package/dist/guest/project-status.js +17 -0
  177. package/dist/guest/ptyhost.d.ts +0 -18
  178. package/dist/guest/ptyhost.js +17 -65
  179. package/dist/guest/setup/agents.d.ts +5 -2
  180. package/dist/guest/setup/agents.js +22 -8
  181. package/dist/guest/setup/claude.d.ts +0 -4
  182. package/dist/guest/setup/claude.js +59 -5
  183. package/dist/guest/setup/environment.d.ts +13 -3
  184. package/dist/guest/setup/environment.js +14 -10
  185. package/dist/guest/setup/hooks.d.ts +9 -1
  186. package/dist/guest/setup/hooks.js +28 -14
  187. package/dist/guest/setup/network.d.ts +9 -0
  188. package/dist/guest/setup/network.js +97 -4
  189. package/dist/guest/setup/user.js +4 -0
  190. package/dist/guest/toolchains/catalogue.d.ts +4 -0
  191. package/dist/guest/toolchains/catalogue.js +2 -2
  192. package/dist/guest/toolchains/cli.js +25 -17
  193. package/dist/guest/toolchains/common.d.ts +18 -1
  194. package/dist/guest/toolchains/common.js +34 -11
  195. package/dist/guest/toolchains/databases/dynamodb.d.ts +8 -0
  196. package/dist/guest/toolchains/databases/dynamodb.js +52 -0
  197. package/dist/guest/toolchains/{databases.d.ts → databases/index.d.ts} +2 -3
  198. package/dist/guest/toolchains/databases/index.js +62 -0
  199. package/dist/guest/toolchains/databases/mongodb.d.ts +3 -0
  200. package/dist/guest/toolchains/databases/mongodb.js +31 -0
  201. package/dist/guest/toolchains/databases/mysql.d.ts +3 -0
  202. package/dist/guest/toolchains/databases/mysql.js +38 -0
  203. package/dist/guest/toolchains/databases/postgres.d.ts +5 -0
  204. package/dist/guest/toolchains/databases/postgres.js +38 -0
  205. package/dist/guest/toolchains/databases/redis.d.ts +3 -0
  206. package/dist/guest/toolchains/databases/redis.js +16 -0
  207. package/dist/guest/toolchains/databases/server.d.ts +14 -0
  208. package/dist/guest/toolchains/databases/server.js +52 -0
  209. package/dist/guest/toolchains/git.js +25 -16
  210. package/dist/guest/toolchains/go.js +1 -1
  211. package/dist/guest/toolchains/index.d.ts +13 -9
  212. package/dist/guest/toolchains/index.js +40 -82
  213. package/dist/guest/toolchains/java.js +5 -5
  214. package/dist/guest/toolchains/leftovers.d.ts +2 -0
  215. package/dist/guest/toolchains/leftovers.js +26 -0
  216. package/dist/guest/toolchains/managers.js +12 -10
  217. package/dist/guest/toolchains/{binstall.js → native/binstall.js} +6 -9
  218. package/dist/guest/toolchains/native/cpp.d.ts +4 -0
  219. package/dist/guest/toolchains/native/cpp.js +38 -0
  220. package/dist/guest/toolchains/native/dotnet.d.ts +4 -0
  221. package/dist/guest/toolchains/native/dotnet.js +103 -0
  222. package/dist/guest/toolchains/native/mesa.d.ts +7 -0
  223. package/dist/guest/toolchains/native/mesa.js +56 -0
  224. package/dist/guest/toolchains/native/msvc/environment.d.ts +16 -0
  225. package/dist/guest/toolchains/native/msvc/environment.js +57 -0
  226. package/dist/guest/toolchains/native/msvc/index.d.ts +6 -0
  227. package/dist/guest/toolchains/native/msvc/index.js +48 -0
  228. package/dist/guest/toolchains/native/msvc/packages.d.ts +6 -0
  229. package/dist/guest/toolchains/native/msvc/packages.js +156 -0
  230. package/dist/guest/toolchains/native/msvc/tree.d.ts +15 -0
  231. package/dist/guest/toolchains/native/msvc/tree.js +21 -0
  232. package/dist/guest/toolchains/native/rust.d.ts +3 -0
  233. package/dist/guest/toolchains/native/rust.js +45 -0
  234. package/dist/guest/toolchains/node.js +26 -6
  235. package/dist/guest/toolchains/plan.d.ts +24 -0
  236. package/dist/guest/toolchains/plan.js +140 -0
  237. package/dist/guest/toolchains/pwsh.js +1 -1
  238. package/dist/guest/toolchains/python.js +43 -12
  239. package/dist/guest/toolchains/runtimes.d.ts +2 -0
  240. package/dist/guest/toolchains/runtimes.js +6 -8
  241. package/dist/guest/toolchains/scripting/php.d.ts +3 -0
  242. package/dist/guest/toolchains/scripting/php.js +71 -0
  243. package/dist/guest/toolchains/scripting/ruby.d.ts +3 -0
  244. package/dist/guest/toolchains/scripting/ruby.js +47 -0
  245. package/dist/guest/toolchains/scripting/vcruntime.d.ts +7 -0
  246. package/dist/guest/toolchains/scripting/vcruntime.js +45 -0
  247. package/dist/guest/toolchains/vscode-profile.d.ts +30 -0
  248. package/dist/guest/toolchains/vscode-profile.js +69 -0
  249. package/dist/guest/toolchains/vscode.d.ts +17 -0
  250. package/dist/guest/toolchains/vscode.js +87 -1
  251. package/dist/guest/view.js +21 -1
  252. package/dist/host/accounts.js +14 -5
  253. package/dist/host/agent-commands.d.ts +25 -0
  254. package/dist/host/agent-commands.js +145 -0
  255. package/dist/host/attach.d.ts +17 -0
  256. package/dist/host/attach.js +70 -0
  257. package/dist/host/audit/collect.d.ts +34 -0
  258. package/dist/host/audit/collect.js +126 -0
  259. package/dist/host/audit/log.d.ts +46 -0
  260. package/dist/host/audit/log.js +143 -0
  261. package/dist/host/channel.d.ts +23 -22
  262. package/dist/host/channel.js +66 -84
  263. package/dist/host/claude-profile.js +7 -3
  264. package/dist/host/cleanup.js +2 -11
  265. package/dist/host/database-plan.d.ts +12 -0
  266. package/dist/host/database-plan.js +52 -0
  267. package/dist/host/generate.d.ts +10 -12
  268. package/dist/host/generate.js +58 -135
  269. package/dist/host/join.d.ts +28 -0
  270. package/dist/host/join.js +76 -0
  271. package/dist/host/launch/mappings.d.ts +3 -0
  272. package/dist/host/launch/mappings.js +54 -0
  273. package/dist/host/launch/projects.d.ts +15 -0
  274. package/dist/host/launch/projects.js +56 -0
  275. package/dist/host/launch/stage.d.ts +11 -0
  276. package/dist/host/launch/stage.js +66 -0
  277. package/dist/host/launch/wsb.d.ts +10 -0
  278. package/dist/host/launch/wsb.js +34 -0
  279. package/dist/host/motw.d.ts +17 -0
  280. package/dist/host/motw.js +74 -0
  281. package/dist/host/pc-scan.d.ts +6 -0
  282. package/dist/host/pc-scan.js +37 -0
  283. package/dist/host/platform.d.ts +2 -0
  284. package/dist/host/platform.js +16 -1
  285. package/dist/host/port-forward.d.ts +8 -0
  286. package/dist/host/port-forward.js +37 -0
  287. package/dist/host/project-scan.d.ts +1 -17
  288. package/dist/host/project-scan.js +2 -86
  289. package/dist/host/projects.d.ts +100 -0
  290. package/dist/host/projects.js +147 -0
  291. package/dist/host/repo-guard.d.ts +25 -5
  292. package/dist/host/repo-guard.js +242 -23
  293. package/dist/host/stops.d.ts +67 -0
  294. package/dist/host/stops.js +126 -0
  295. package/dist/host/terminal.d.ts +8 -0
  296. package/dist/host/terminal.js +48 -1
  297. package/dist/host/test-guard.d.ts +6 -0
  298. package/dist/host/test-guard.js +15 -0
  299. package/dist/host/window.js +7 -3
  300. package/dist/host/workspace-view/index.d.ts +20 -0
  301. package/dist/host/workspace-view/index.js +53 -0
  302. package/dist/host/workspace-view/rules.d.ts +26 -0
  303. package/dist/host/workspace-view/rules.js +93 -0
  304. package/dist/host/workspace-view/view.d.ts +50 -0
  305. package/dist/host/workspace-view/view.js +281 -0
  306. package/dist/host/wsb.d.ts +37 -0
  307. package/dist/host/wsb.js +101 -0
  308. package/dist/runtime/daemon.js +11791 -2821
  309. package/dist/runtime/db.js +6585 -466
  310. package/dist/runtime/main.js +10238 -1724
  311. package/dist/runtime/netproxy.js +25936 -0
  312. package/dist/runtime/view.js +6183 -59
  313. package/dist/selftest/accounts.test.d.ts +2 -0
  314. package/dist/selftest/accounts.test.js +44 -0
  315. package/dist/selftest/agent-commands.test.d.ts +2 -0
  316. package/dist/selftest/agent-commands.test.js +47 -0
  317. package/dist/selftest/audit.test.d.ts +2 -0
  318. package/dist/selftest/audit.test.js +57 -0
  319. package/dist/selftest/channel.test.d.ts +2 -0
  320. package/dist/selftest/channel.test.js +44 -0
  321. package/dist/selftest/cli.test.d.ts +2 -2
  322. package/dist/selftest/cli.test.js +145 -27
  323. package/dist/selftest/cmdline.test.d.ts +2 -0
  324. package/dist/selftest/cmdline.test.js +45 -0
  325. package/dist/selftest/config.test.d.ts +2 -3
  326. package/dist/selftest/config.test.js +36 -72
  327. package/dist/selftest/data.test.d.ts +2 -2
  328. package/dist/selftest/data.test.js +77 -70
  329. package/dist/selftest/detect.test.d.ts +2 -2
  330. package/dist/selftest/detect.test.js +54 -38
  331. package/dist/selftest/fixtures.d.ts +89 -4
  332. package/dist/selftest/fixtures.js +43 -24
  333. package/dist/selftest/generate.test.d.ts +2 -0
  334. package/dist/selftest/generate.test.js +68 -0
  335. package/dist/selftest/git-access.test.d.ts +2 -0
  336. package/dist/selftest/git-access.test.js +26 -0
  337. package/dist/selftest/guest-boot.test.d.ts +2 -0
  338. package/dist/selftest/guest-boot.test.js +76 -0
  339. package/dist/selftest/harness.d.ts +44 -7
  340. package/dist/selftest/harness.js +62 -21
  341. package/dist/selftest/host-sim.test.d.ts +2 -0
  342. package/dist/selftest/host-sim.test.js +93 -0
  343. package/dist/selftest/hostile-input.test.d.ts +2 -0
  344. package/dist/selftest/hostile-input.test.js +54 -0
  345. package/dist/selftest/index.d.ts +8 -1
  346. package/dist/selftest/index.js +137 -35
  347. package/dist/selftest/links.test.d.ts +2 -0
  348. package/dist/selftest/links.test.js +50 -0
  349. package/dist/selftest/live-daemon.test.d.ts +2 -0
  350. package/dist/selftest/live-daemon.test.js +91 -0
  351. package/dist/selftest/live-git.test.d.ts +2 -0
  352. package/dist/selftest/live-git.test.js +60 -0
  353. package/dist/selftest/live-network.test.d.ts +2 -0
  354. package/dist/selftest/live-network.test.js +63 -0
  355. package/dist/selftest/live-projects.test.d.ts +2 -0
  356. package/dist/selftest/live-projects.test.js +127 -0
  357. package/dist/selftest/live.d.ts +65 -0
  358. package/dist/selftest/live.js +109 -0
  359. package/dist/selftest/local-config.test.d.ts +2 -0
  360. package/dist/selftest/local-config.test.js +120 -0
  361. package/dist/selftest/mirror.test.d.ts +2 -3
  362. package/dist/selftest/mirror.test.js +92 -65
  363. package/dist/selftest/network.test.d.ts +2 -0
  364. package/dist/selftest/network.test.js +40 -0
  365. package/dist/selftest/personalization.test.d.ts +2 -0
  366. package/dist/selftest/personalization.test.js +53 -0
  367. package/dist/selftest/projects.test.d.ts +2 -0
  368. package/dist/selftest/projects.test.js +254 -0
  369. package/dist/selftest/repair.test.d.ts +2 -0
  370. package/dist/selftest/repair.test.js +42 -0
  371. package/dist/selftest/repo-guard.test.d.ts +2 -0
  372. package/dist/selftest/repo-guard.test.js +121 -0
  373. package/dist/selftest/secrets.test.d.ts +2 -0
  374. package/dist/selftest/secrets.test.js +71 -0
  375. package/dist/selftest/setup.test.d.ts +2 -2
  376. package/dist/selftest/setup.test.js +45 -22
  377. package/dist/selftest/suite.d.ts +13 -0
  378. package/dist/selftest/suite.js +1 -0
  379. package/dist/selftest/sync.test.d.ts +2 -3
  380. package/dist/selftest/sync.test.js +130 -78
  381. package/dist/selftest/test-guard.test.d.ts +2 -0
  382. package/dist/selftest/test-guard.test.js +49 -0
  383. package/dist/selftest/toolchains.test.d.ts +2 -0
  384. package/dist/selftest/toolchains.test.js +133 -0
  385. package/dist/selftest/workspace-view.test.d.ts +2 -0
  386. package/dist/selftest/workspace-view.test.js +89 -0
  387. package/dist/selftest/workspace.test.d.ts +2 -0
  388. package/dist/selftest/workspace.test.js +53 -0
  389. package/dist/shared/apps.d.ts +49 -0
  390. package/dist/shared/apps.js +114 -0
  391. package/dist/shared/download.d.ts +3 -1
  392. package/dist/shared/download.js +19 -4
  393. package/dist/shared/netpolicy.d.ts +27 -0
  394. package/dist/shared/netpolicy.js +51 -0
  395. package/dist/shared/paths.d.ts +5 -0
  396. package/dist/shared/paths.js +6 -0
  397. package/dist/shared/projects.d.ts +53 -0
  398. package/dist/shared/projects.js +67 -0
  399. package/dist/shared/pty-client.d.ts +9 -4
  400. package/dist/shared/pty-client.js +139 -17
  401. package/dist/shared/rundir.d.ts +12 -1
  402. package/dist/shared/rundir.js +14 -4
  403. package/dist/shared/safe-remove.js +11 -0
  404. package/dist/shared/schema.d.ts +453 -0
  405. package/dist/shared/schema.js +43 -2
  406. package/dist/shared/system.d.ts +1 -0
  407. package/dist/shared/system.js +1 -0
  408. package/dist/shared/test-guard.d.ts +4 -0
  409. package/dist/shared/test-guard.js +37 -0
  410. package/dist/sync/claude-mem.js +6 -3
  411. package/dist/sync/history.d.ts +29 -12
  412. package/dist/sync/history.js +72 -25
  413. package/dist/sync/watcher.d.ts +8 -0
  414. package/dist/sync/watcher.js +99 -8
  415. package/package.json +7 -6
  416. package/sandbox/bootstrap.ps1 +258 -258
  417. package/sandbox/cargoshim.cs +56 -0
  418. package/sandbox/cpucap.cs +33 -0
  419. package/sandbox/cpucap.ps1 +20 -0
  420. package/sandbox/logon.cmd +24 -16
  421. package/sandbox/memlog.ps1 +34 -0
  422. package/sandbox/openssl.cs +42 -0
  423. package/sandbox/pagefile.cs +31 -0
  424. package/sandbox/pagefile.ps1 +21 -0
  425. package/sandbox/ptyhost.cs +120 -11
  426. package/sandbox/trust.cmd +35 -35
  427. package/schemas/config.schema.json +403 -3
  428. package/schemas/sandbox-config.schema.json +143 -13
  429. package/dist/cli/commands/data.d.ts +0 -46
  430. package/dist/cli/commands/data.js +0 -249
  431. package/dist/cli/commands/setup.js +0 -152
  432. package/dist/cli/menu.d.ts +0 -22
  433. package/dist/cli/menu.js +0 -187
  434. package/dist/cli/settings.d.ts +0 -11
  435. package/dist/cli/settings.js +0 -156
  436. package/dist/guest/toolchains/databases.js +0 -226
  437. package/dist/guest/toolchains/msvc.d.ts +0 -10
  438. package/dist/guest/toolchains/msvc.js +0 -216
  439. package/dist/guest/toolchains/native.d.ts +0 -12
  440. package/dist/guest/toolchains/native.js +0 -220
  441. package/dist/guest/toolchains/scripting.d.ts +0 -10
  442. package/dist/guest/toolchains/scripting.js +0 -137
  443. package/dist/selftest/guest.test.d.ts +0 -3
  444. package/dist/selftest/guest.test.js +0 -61
  445. package/dist/selftest/helpers.test.d.ts +0 -2
  446. package/dist/selftest/helpers.test.js +0 -125
  447. package/dist/selftest/host.test.d.ts +0 -2
  448. package/dist/selftest/host.test.js +0 -71
  449. package/dist/selftest/live.test.d.ts +0 -7
  450. package/dist/selftest/live.test.js +0 -127
  451. package/dist/selftest/security.test.d.ts +0 -3
  452. package/dist/selftest/security.test.js +0 -143
  453. /package/dist/guest/toolchains/{binstall.d.ts → native/binstall.d.ts} +0 -0
@@ -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) => {
@@ -14,8 +14,4 @@ export declare const claudeMemPaths: {
14
14
  local: string;
15
15
  };
16
16
  export declare const restoreClaudeMem: () => void;
17
- /**
18
- * Copies the live claude-mem folder back to persistent storage (staging folder + swap, never a half copy). Called from
19
- * the daemon tick: when no database changed since the last save it does nothing (no copy of the whole folder).
20
- */
21
17
  export declare const saveClaudeMem: () => void;
@@ -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,24 @@ 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
+ // A store that is gone or damaged: the newest backup that is a real database.
93
+ const backups = claudeMemPaths.store + '.backups';
94
+ if (!looksLikeSqlite(path.join(claudeMemPaths.store, 'claude-mem.db')) || !exists(claudeMemPaths.store)) {
95
+ const good = (exists(backups) ? fs.readdirSync(backups).sort().reverse() : []).find((b) => exists(path.join(backups, b, 'claude-mem.db')) && looksLikeSqlite(path.join(backups, b, 'claude-mem.db')));
96
+ if (good) {
97
+ if (exists(claudeMemPaths.store))
98
+ fs.renameSync(claudeMemPaths.store, claudeMemPaths.store + '.damaged-' + Date.now());
99
+ fs.cpSync(path.join(backups, good), claudeMemPaths.store, { recursive: true });
100
+ warn('claude-mem: the stored database was missing or damaged; restored the backup from ' + new Date(Number(good)).toISOString() + '.');
101
+ }
102
+ }
103
+ if (!exists(claudeMemPaths.store))
85
104
  return;
86
105
  robocopy(claudeMemPaths.store, claudeMemPaths.local);
87
106
  log('claude-mem data restored from persistent storage.');
@@ -109,6 +128,36 @@ let lastSaved = null;
109
128
  * Copies the live claude-mem folder back to persistent storage (staging folder + swap, never a half copy). Called from
110
129
  * the daemon tick: when no database changed since the last save it does nothing (no copy of the whole folder).
111
130
  */
131
+ const SQLITE_HEADER = 'SQLite format 3\0';
132
+ /** A database file that starts like SQLite and is not empty (a missing one counts as fine: nothing to protect yet). */
133
+ const looksLikeSqlite = (file) => {
134
+ if (!exists(file))
135
+ return true;
136
+ const fd = fs.openSync(file, 'r');
137
+ try {
138
+ const head = Buffer.alloc(16);
139
+ return fs.readSync(fd, head, 0, 16, 0) === 16 && head.toString('latin1') === SQLITE_HEADER;
140
+ }
141
+ finally {
142
+ fs.closeSync(fd);
143
+ }
144
+ };
145
+ const MAX_BACKUPS = 3;
146
+ const BACKUP_EVERY_MS = 6 * 3600 * 1000;
147
+ /** Moves the replaced copy into `dir` as a dated backup (at most one per 6 hours, the newest 3 kept); else deletes it. */
148
+ const keepBackup = (previous, dir) => {
149
+ if (!exists(previous))
150
+ return;
151
+ const backups = exists(dir) ? fs.readdirSync(dir).filter((n) => /^\d{13}$/.test(n)).sort() : [];
152
+ const newest = Number(backups[backups.length - 1] ?? 0);
153
+ if (Date.now() - newest < BACKUP_EVERY_MS) {
154
+ fs.rmSync(previous, { recursive: true, force: true });
155
+ return;
156
+ }
157
+ fs.renameSync(previous, path.join(mkdirp(dir), String(Date.now())));
158
+ for (const old of [...backups, String(Date.now())].slice(0, -MAX_BACKUPS))
159
+ fs.rmSync(path.join(dir, old), { recursive: true, force: true });
160
+ };
112
161
  export const saveClaudeMem = () => {
113
162
  if (!claudeMemEnabled() || !exists(claudeMemPaths.local))
114
163
  return;
@@ -116,12 +165,17 @@ export const saveClaudeMem = () => {
116
165
  const fingerprint = databaseFingerprint(local);
117
166
  if (fingerprint === lastSaved && exists(store))
118
167
  return;
168
+ // A damaged copy (zero-filled after a crash, cut short) must never replace the store: that once destroyed the only copy.
169
+ if (!looksLikeSqlite(path.join(local, 'claude-mem.db'))) {
170
+ warn('claude-mem: the local database does not look valid, so it was not saved over the stored copy (' + local + ').');
171
+ return;
172
+ }
119
173
  const incoming = store + '.incoming';
120
174
  const previous = store + '.previous';
121
175
  fs.rmSync(incoming, { recursive: true, force: true });
122
176
  // The database, its -wal and -shm files are copied together, so the copy is consistent with itself.
123
177
  fs.cpSync(local, incoming, { recursive: true, filter: (src) => !/\.(pid|lock)$/i.test(src) });
124
- // Swap without a window where no copy exists: old aside, new in, then drop the old one.
178
+ // Swap without a window where no copy exists: old aside, new in, then keep the old one as a dated backup.
125
179
  fs.rmSync(previous, { recursive: true, force: true });
126
180
  if (exists(store))
127
181
  fs.renameSync(store, previous);
@@ -133,6 +187,6 @@ export const saveClaudeMem = () => {
133
187
  fs.renameSync(previous, store);
134
188
  throw e;
135
189
  }
136
- fs.rmSync(previous, { recursive: true, force: true });
190
+ keepBackup(previous, store + '.backups');
137
191
  lastSaved = fingerprint;
138
192
  };
@@ -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>;
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { SYSTEM } from '../../shared/system.js';
6
+ import { spawnAsync } from '../../shared/download.js';
6
7
  import { exists, isWin, launchDir, log, session, warn, workspace } from '../env.js';
7
8
  const commandFor = (file) => {
8
9
  const ext = path.extname(file).toLowerCase();
@@ -16,21 +17,34 @@ const commandFor = (file) => {
16
17
  return [SYSTEM.cmd(), ['/d', '/c', file]];
17
18
  return null;
18
19
  };
19
- export const runHooks = (stage, env) => {
20
- const dir = path.join(launchDir, 'hooks', stage);
20
+ const BOOT = { root: path.join(launchDir, 'hooks'), cwd: workspace };
21
+ export const projectHooks = (name, cwd) => ({ root: path.join(launchDir, 'projects', name, 'hooks'), cwd });
22
+ const hooksOf = (stage, target) => {
23
+ const dir = path.join(target.root, stage);
21
24
  if (!exists(dir))
22
- return;
23
- for (const name of fs.readdirSync(dir).sort()) {
24
- const command = commandFor(path.join(dir, name));
25
- if (!command)
26
- continue;
25
+ return [];
26
+ return fs.readdirSync(dir).sort().flatMap((name) => { const command = commandFor(path.join(dir, name)); return command ? [{ name, command }] : []; });
27
+ };
28
+ const failed = (stage, name, status) => {
29
+ const message = 'Hook failed (' + stage + '/' + name + ', exit ' + status + ')';
30
+ if (session.hooks.failFast)
31
+ throw new Error(message);
32
+ warn(message);
33
+ };
34
+ export const runHooks = (stage, env, target = BOOT) => {
35
+ for (const { name, command } of hooksOf(stage, target)) {
36
+ log('Hook [' + stage + ']: ' + name);
37
+ const result = spawnSync(command[0], command[1], { stdio: 'inherit', cwd: target.cwd, env });
38
+ if (result.status !== 0)
39
+ failed(stage, name, result.status);
40
+ }
41
+ };
42
+ /** The same without blocking: the session daemon keeps answering other requests while a project's hooks run. */
43
+ export const runHooksAsync = async (stage, env, target) => {
44
+ for (const { name, command } of hooksOf(stage, target)) {
27
45
  log('Hook [' + stage + ']: ' + name);
28
- const result = spawnSync(command[0], command[1], { stdio: 'inherit', cwd: workspace, env });
29
- if (result.status === 0)
30
- continue;
31
- const message = 'Hook failed (' + stage + '/' + name + ', exit ' + result.status + ')';
32
- if (session.hooks.failFast)
33
- throw new Error(message);
34
- warn(message);
46
+ const status = await spawnAsync(command[0], command[1], { stdio: 'ignore', cwd: target.cwd, env, windowsHide: true });
47
+ if (status !== 0)
48
+ failed(stage, name, status);
35
49
  }
36
50
  };
@@ -6,5 +6,14 @@ export type LanState = 'blocked' | 'open' | 'offline';
6
6
  * all, the block stays in place: being offline is never a reason to open the LAN.
7
7
  */
8
8
  export declare const applyNetworkPolicy: (ptyHostExe: string | null) => Promise<LanState>;
9
+ /** HTTP_PROXY & co. for everything in the sandbox: its own proxy (allowOnly) or yours (network.proxy). */
10
+ export declare const proxyEnv: () => Record<string, string>;
11
+ /**
12
+ * network.allowOnly, after the tools are installed: the sandbox's proxy (a copy of Node named sbx-netproxy.exe, so the
13
+ * firewall can tell it apart) is the only program allowed out, besides DNS and DHCP; Windows and browsers use it as
14
+ * the system proxy, everything else through HTTP_PROXY. A guardrail for agents that behave: an admin in the sandbox
15
+ * could undo it, which is why sandbox.networking false is the only hard isolation.
16
+ */
17
+ export declare const applyAllowOnly: () => Promise<boolean>;
9
18
  /** Guest IPv4 addresses the host can use to reach session ports. */
10
19
  export declare const guestAddresses: () => string[];
@@ -1,10 +1,13 @@
1
1
  // Guest firewall: block the host's LAN (keeping DNS/DHCP to the gateway) and let the host reach session ports.
2
2
  import dns from 'node:dns';
3
+ import fs from 'node:fs';
3
4
  import os from 'node:os';
4
- import { spawnSync } from 'node:child_process';
5
- import { buildFirewallCommands, ipv4DnsServers, parseDefaultGateways, parseDnsServers } from '../../shared/netrules.js';
5
+ import path from 'node:path';
6
+ import { spawn, spawnSync } from 'node:child_process';
7
+ import { expandAllowList, PROXY_PORT, relayPort, withBlockedHosts } from '../../shared/netpolicy.js';
8
+ import { buildFirewallCommands, ipv4DnsServers, isIPv4, parseDefaultGateways, parseDnsServers } from '../../shared/netrules.js';
6
9
  import { SYSTEM } from '../../shared/system.js';
7
- import { dryRun, isWin, log, session, warn } from '../env.js';
10
+ import { dryRun, errMsg, isWin, launchDir, log, session, toolsDir, warn } from '../env.js';
8
11
  const ATTACH_RULE = 'AgentSandbox-Attach';
9
12
  const netsh = (args) => {
10
13
  const r = spawnSync(SYSTEM.netsh(), args, { encoding: 'utf8', windowsHide: true });
@@ -68,7 +71,21 @@ export const applyNetworkPolicy = async (ptyHostExe) => {
68
71
  const block = session.network.blockPrivateRanges;
69
72
  const exempt = block ? discoverExemptIps() : [];
70
73
  const lanRules = block ? buildFirewallCommands(exempt) : [];
71
- const commands = [...lanRules, ...(ptyHostExe && session.attach.enabled ? attachRule(ptyHostExe) : [])];
74
+ const commands = [...lanRules, ...(ptyHostExe && session.attach.enabled ? attachRule(ptyHostExe) : []), ...forwardRule()];
75
+ if (isWin && !dryRun) {
76
+ try {
77
+ applyBlockedHosts();
78
+ }
79
+ catch (e) {
80
+ warn('network.blockHosts could not be applied: ' + errMsg(e));
81
+ }
82
+ try {
83
+ applyDns();
84
+ }
85
+ catch (e) {
86
+ warn('network.dns could not be applied: ' + errMsg(e));
87
+ }
88
+ }
72
89
  if (!block)
73
90
  log('Private-range blocking disabled by config.');
74
91
  if (block)
@@ -95,5 +112,81 @@ export const applyNetworkPolicy = async (ptyHostExe) => {
95
112
  log('LAN blocked in the guest firewall (10/8, 172.16/12, 192.168/16, 100.64/10, link-local, multicast, IPv6 local, SMB); DNS/DHCP to the gateway allowed.');
96
113
  return 'blocked';
97
114
  };
115
+ // ---- the rest of the network settings -----------------------------------------------------------------------------
116
+ const FORWARD_RULE = 'AgentSandbox-Forward';
117
+ const ALLOW_RULES = ['AgentSandbox-Proxy-Out', 'AgentSandbox-DNS-Out-UDP', 'AgentSandbox-DNS-Out-TCP', 'AgentSandbox-DHCP-Out'];
118
+ const HOSTS_FILE = path.join(process.env.WINDIR ?? 'C:\\Windows', 'System32', 'drivers', 'etc', 'hosts');
119
+ /** network.blockHosts: those names go nowhere (the sandbox's hosts file; the registry and files are new every boot). */
120
+ const applyBlockedHosts = () => {
121
+ const hosts = session.network.blockHosts ?? [];
122
+ if (!hosts.length)
123
+ return;
124
+ const text = fs.existsSync(HOSTS_FILE) ? fs.readFileSync(HOSTS_FILE, 'utf8') : '';
125
+ fs.writeFileSync(HOSTS_FILE, withBlockedHosts(text, hosts));
126
+ log('Blocked hosts (network.blockHosts): ' + hosts.join(', '));
127
+ };
128
+ /** network.dns: these servers on every connected adapter. */
129
+ const applyDns = () => {
130
+ const servers = (session.network.dns ?? []).filter(isIPv4);
131
+ if (!servers.length)
132
+ return;
133
+ const r = spawnSync(SYSTEM.powershell(), ['-NoLogo', '-NoProfile', '-Command', "Get-NetAdapter | Where-Object Status -eq 'Up' | Set-DnsClientServerAddress -ServerAddresses " + servers.map((s) => "'" + s + "'").join(',')], { encoding: 'utf8', windowsHide: true });
134
+ if (r.status !== 0)
135
+ throw new Error('could not set the DNS servers: ' + (r.stderr ?? '').trim().slice(0, 300));
136
+ log('DNS servers (network.dns): ' + servers.join(', '));
137
+ };
138
+ /** network.forwardPorts: the relays (in the daemon) take connections from the PC only, i.e. the sandbox's own subnet. */
139
+ const forwardRule = () => {
140
+ const forwards = session.network.forwardPorts ?? [];
141
+ return forwards.length ? [
142
+ ['advfirewall', 'firewall', 'delete', 'rule', 'name=' + FORWARD_RULE],
143
+ ['advfirewall', 'firewall', 'add', 'rule', 'name=' + FORWARD_RULE, 'dir=in', 'action=allow', 'enable=yes', 'profile=any', 'protocol=tcp', 'remoteip=LocalSubnet', 'localport=' + forwards.map((_f, i) => relayPort(i)).join(',')],
144
+ ] : [];
145
+ };
146
+ /** HTTP_PROXY & co. for everything in the sandbox: its own proxy (allowOnly) or yours (network.proxy). */
147
+ export const proxyEnv = () => {
148
+ const own = (session.network.allowOnly ?? []).length > 0;
149
+ const url = own ? 'http://127.0.0.1:' + PROXY_PORT : session.network.proxy ?? '';
150
+ if (!url)
151
+ return {};
152
+ const noProxy = 'localhost,127.0.0.1,::1';
153
+ return { HTTP_PROXY: url, HTTPS_PROXY: url, http_proxy: url, https_proxy: url, NO_PROXY: noProxy, no_proxy: noProxy };
154
+ };
155
+ /**
156
+ * network.allowOnly, after the tools are installed: the sandbox's proxy (a copy of Node named sbx-netproxy.exe, so the
157
+ * firewall can tell it apart) is the only program allowed out, besides DNS and DHCP; Windows and browsers use it as
158
+ * the system proxy, everything else through HTTP_PROXY. A guardrail for agents that behave: an admin in the sandbox
159
+ * could undo it, which is why sandbox.networking false is the only hard isolation.
160
+ */
161
+ export const applyAllowOnly = async () => {
162
+ const hosts = session.network.allowOnly ?? [];
163
+ if (!session.networking || !hosts.length)
164
+ return false;
165
+ if (!isWin || dryRun) {
166
+ log('(dry run) would allow only: ' + hosts.join(', '));
167
+ return false;
168
+ }
169
+ const exe = path.join(toolsDir, 'netproxy', 'sbx-netproxy.exe');
170
+ fs.mkdirSync(path.dirname(exe), { recursive: true });
171
+ if (!fs.existsSync(exe) || fs.statSync(exe).size !== fs.statSync(process.execPath).size)
172
+ fs.copyFileSync(process.execPath, exe);
173
+ spawn(exe, [path.join(launchDir, 'runtime', 'netproxy.js')], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
174
+ const add = (name, extra) => ['advfirewall', 'firewall', 'add', 'rule', 'name=' + name, 'dir=out', 'action=allow', 'enable=yes', 'profile=any', ...extra];
175
+ run([
176
+ ...ALLOW_RULES.map((n) => ['advfirewall', 'firewall', 'delete', 'rule', 'name=' + n]),
177
+ add('AgentSandbox-Proxy-Out', ['program=' + exe]),
178
+ add('AgentSandbox-DNS-Out-UDP', ['protocol=udp', 'remoteport=53']),
179
+ add('AgentSandbox-DNS-Out-TCP', ['protocol=tcp', 'remoteport=53']),
180
+ add('AgentSandbox-DHCP-Out', ['protocol=udp', 'remoteport=67', 'localport=68']),
181
+ ['advfirewall', 'set', 'allprofiles', 'firewallpolicy', 'blockinbound,blockoutbound'],
182
+ ['winhttp', 'set', 'proxy', '127.0.0.1:' + PROXY_PORT, '<local>'],
183
+ ]);
184
+ const internet = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings';
185
+ for (const [name, type, value] of [['ProxyEnable', 'REG_DWORD', '1'], ['ProxyServer', 'REG_SZ', '127.0.0.1:' + PROXY_PORT], ['ProxyOverride', 'REG_SZ', '<local>']]) {
186
+ spawnSync(SYSTEM.reg(), ['add', internet, '/v', name, '/t', type, '/d', value, '/f'], { windowsHide: true, stdio: 'ignore' });
187
+ }
188
+ log('Only these hosts can be reached now (network.allowOnly): ' + expandAllowList(hosts).join(', ') + '. Refused ones are listed in logs\\netproxy.log.');
189
+ return true;
190
+ };
98
191
  /** Guest IPv4 addresses the host can use to reach session ports. */
99
192
  export const guestAddresses = () => Object.values(os.networkInterfaces()).flat().filter((i) => Boolean(i) && i.family === 'IPv4' && !i.internal).map((i) => i.address);
@@ -44,4 +44,8 @@ export const setupGitConfig = (env) => {
44
44
  spawnSync(git, ['config', '--global', '--add', 'safe.directory', '*'], { env: rest, windowsHide: true, stdio: 'ignore' });
45
45
  // Windows long paths are on in the sandbox, but git skips files over 260 characters unless told.
46
46
  spawnSync(git, ['config', '--global', 'core.longpaths', 'true'], { env: rest, windowsHide: true, stdio: 'ignore' });
47
+ // Git for Windows' system config says autocrlf=true: checkouts made in the sandbox (worktrees, clones) got CRLF and
48
+ // tools that want LF (rustfmt, shellcheck, prettier) failed on every file. "input" keeps the line endings the repo
49
+ // has, and a checkout from your PC (CRLF in the folder) still shows as unchanged.
50
+ spawnSync(git, ['config', '--global', 'core.autocrlf', 'input'], { env: rest, windowsHide: true, stdio: 'ignore' });
47
51
  };
@@ -17,6 +17,8 @@ export interface CliTool {
17
17
  subdir?: string;
18
18
  /** What the tool is for (summary and docs). */
19
19
  about: string;
20
+ /** Rough download size for the progress line. */
21
+ size: string;
20
22
  }
21
23
  export declare const CLI_TOOLS: readonly [{
22
24
  readonly name: "gh";
@@ -24,12 +26,14 @@ export declare const CLI_TOOLS: readonly [{
24
26
  readonly asset: (arch: Arch) => RegExp;
25
27
  readonly exe: "gh.exe";
26
28
  readonly about: "GitHub CLI";
29
+ readonly size: "15 MB";
27
30
  }, {
28
31
  readonly name: "zstd";
29
32
  readonly repo: "facebook/zstd";
30
33
  readonly asset: () => RegExp;
31
34
  readonly exe: "zstd.exe";
32
35
  readonly about: "Zstandard compression";
36
+ readonly size: "2 MB";
33
37
  }];
34
38
  export type CliName = (typeof CLI_TOOLS)[number]['name'] | 'openssl';
35
39
  export declare const CLI_NAMES: readonly CliName[];
@@ -4,9 +4,9 @@ export const pwshAsset = (arch) => (arch === 'arm64' ? /^PowerShell-\d+\.\d+\.\d
4
4
  export const binstallAsset = (arch) => (arch === 'arm64' ? /^cargo-binstall-aarch64-pc-windows-msvc\.zip$/ : /^cargo-binstall-x86_64-pc-windows-msvc\.zip$/);
5
5
  export const CLI_TOOLS = [
6
6
  // gh_2.102.0_windows_amd64.zip holds bin\gh.exe.
7
- { name: 'gh', repo: 'cli/cli', asset: (arch) => (arch === 'arm64' ? /^gh_\d+\.\d+\.\d+_windows_arm64\.zip$/ : /^gh_\d+\.\d+\.\d+_windows_amd64\.zip$/), exe: 'gh.exe', about: 'GitHub CLI' },
7
+ { name: 'gh', repo: 'cli/cli', asset: (arch) => (arch === 'arm64' ? /^gh_\d+\.\d+\.\d+_windows_arm64\.zip$/ : /^gh_\d+\.\d+\.\d+_windows_amd64\.zip$/), exe: 'gh.exe', about: 'GitHub CLI', size: '15 MB' },
8
8
  // zstd-v1.5.7-win64.zip; there is no arm64 build, and Windows on Arm runs the x64 one.
9
- { name: 'zstd', repo: 'facebook/zstd', asset: () => /^zstd-v\d+\.\d+\.\d+-win64\.zip$/, exe: 'zstd.exe', about: 'Zstandard compression' },
9
+ { name: 'zstd', repo: 'facebook/zstd', asset: () => /^zstd-v\d+\.\d+\.\d+-win64\.zip$/, exe: 'zstd.exe', about: 'Zstandard compression', size: '2 MB' },
10
10
  ];
11
11
  export const CLI_NAMES = [...CLI_TOOLS.map((t) => t.name), 'openssl'];
12
12
  /**
@@ -2,39 +2,40 @@
2
2
  // `openssl` shim onto the copy Git for Windows already ships.
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { dryRun, errMsg, exists, log, mkdirp, stateDir, warn, workspace } from '../env.js';
5
+ import { dryRun, errMsg, exists, log, mkdirp, stateDir, warn } from '../env.js';
6
+ import { projectWorkspace } from '../project-context.js';
6
7
  import { CLI_TOOLS, decideCli } from './catalogue.js';
7
8
  import { arch, tool } from './common.js';
8
9
  import { setupSingleExe } from './runtimes.js';
10
+ import { ensureCompiled } from '../helpers.js';
9
11
  /** gh keeps `gh auth login` here, under the agent state (persists like Claude's own login; never copied from the PC). */
10
12
  export const ghConfigDir = () => path.join(stateDir, 'gh');
11
13
  const toolVars = {
12
14
  gh: () => ({ GH_CONFIG_DIR: mkdirp(ghConfigDir()), GH_NO_UPDATE_NOTIFIER: '1', GH_NO_EXTENSION_UPDATE_NOTIFIER: '1' }),
13
15
  };
14
16
  /**
15
- * openssl.cmd in tools\shims, running Git's mingw64\bin\openssl.exe. Only the shim: putting mingw64\bin itself on PATH
16
- * would bring Git's own gcc-runtime DLLs, curl, and friends along and could shadow MinGW's gcc from the C/C++ step.
17
- * setlocal keeps OPENSSL_CONF from leaking into a cmd prompt that runs `openssl`. (A .cmd is found by shells and
18
- * by programs that use cmd /c; Rust's Command::new("openssl") looks only for openssl.exe.)
17
+ * tools\shims\openssl.exe (sandbox/openssl.cs), a launcher for Git's mingw64\bin\openssl.exe. A real .exe so programs
18
+ * that call CreateProcess directly (Rust's Command::new("openssl")) find it, and only the shim is on PATH: mingw64\bin
19
+ * itself would bring Git's runtime DLLs along and could shadow MinGW's gcc from the C/C++ step.
19
20
  */
20
21
  const shimOpenssl = (env) => {
21
- const exe = tool('git', 'mingw64', 'bin', 'openssl.exe');
22
- if (!exists(exe)) {
22
+ if (!exists(tool('git', 'mingw64', 'bin', 'openssl.exe'))) {
23
23
  env.summary.push('openssl: not in this Git for Windows (skipped)');
24
24
  return;
25
25
  }
26
- const conf = tool('git', 'mingw64', 'etc', 'ssl', 'openssl.cnf');
27
- const lines = ['@echo off', 'setlocal', ...(exists(conf) ? ['if not defined OPENSSL_CONF set "OPENSSL_CONF=' + conf + '"'] : []), '"' + exe + '" %*'];
28
- fs.writeFileSync(path.join(mkdirp(tool('shims')), 'openssl.cmd'), lines.join('\r\n') + '\r\n');
29
- env.prepend.push(tool('shims'));
30
- env.summary.push('openssl: Git for Windows\' copy (shim)');
26
+ const shims = mkdirp(tool('shims'));
27
+ fs.copyFileSync(ensureCompiled('openssl'), path.join(shims, 'openssl.exe'));
28
+ // An earlier version wrote a .cmd; it would still win for shells that prefer it.
29
+ fs.rmSync(path.join(shims, 'openssl.cmd'), { force: true });
30
+ env.prepend.push(shims);
31
+ env.summary.push('openssl: Git for Windows\' copy');
31
32
  };
32
33
  /**
33
34
  * Installs the tools toolchains.cli asks for. Settings still "auto" here (normally the host decides) use the same
34
35
  * rules on the workspace as the host would. The gh rule is enforced here again, whatever the host sent.
35
36
  */
36
37
  export const setupCli = async (env, settings, gitAccess, refresh = false) => {
37
- const decisions = decideCli(settings, { gitAccess, hasGithubDir: exists(path.join(workspace, '.github')) });
38
+ const decisions = decideCli(settings, { gitAccess, hasGithubDir: exists(path.join(projectWorkspace(), '.github')) });
38
39
  if (settings.gh && !decisions.gh.on) {
39
40
  env.summary.push('gh: off (' + decisions.gh.reason + ')');
40
41
  // A login from an earlier full-access sandbox is still on disk; the agent could read the token from there.
@@ -50,15 +51,22 @@ export const setupCli = async (env, settings, gitAccess, refresh = false) => {
50
51
  if (!decisions[name].on)
51
52
  continue;
52
53
  try {
53
- await setupSingleExe(env, name, entry.repo, entry.asset(arch), entry.exe, tool('cli', name), { refresh, subdir: entry.subdir });
54
+ await setupSingleExe(env, name, entry.repo, entry.asset(arch), entry.exe, tool('cli', name), { refresh, subdir: entry.subdir, size: entry.size });
54
55
  Object.assign(env.vars, toolVars[name]?.() ?? {});
55
56
  }
56
57
  catch (e) {
57
58
  // One tool failing never takes the others down.
58
- warn(name + ' setup failed (continuing without it): ' + errMsg(e));
59
+ warn(name + ' could not be set up and is skipped; the other tools are not affected. Reason: ' + errMsg(e));
59
60
  env.summary.push(entry.name + ': FAILED');
60
61
  }
61
62
  }
62
- if (decisions.openssl.on)
63
- shimOpenssl(env);
63
+ if (decisions.openssl.on) {
64
+ try {
65
+ shimOpenssl(env);
66
+ }
67
+ catch (e) {
68
+ warn('openssl could not be set up and is skipped (Git Bash still has its own). Reason: ' + errMsg(e));
69
+ env.summary.push('openssl: FAILED');
70
+ }
71
+ }
64
72
  };