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
@@ -1,12 +1,19 @@
1
- export interface RepoSnapshot {
2
- gitDir: boolean;
1
+ /** A git folder's own state: config lines, hooks, redirect files. */
2
+ export interface GitState {
3
3
  config: string[];
4
4
  hooks: Record<string, string>;
5
5
  redirects: string[];
6
+ }
7
+ export interface RepoSnapshot extends GitState {
8
+ gitDir: boolean;
6
9
  commands: Record<string, string>;
10
+ /** Fingerprints of AGENT_FILES (missing in snapshots of older versions: then they are not checked). */
11
+ agent?: Record<string, string>;
12
+ /** Other git folders: submodules (.git/modules/...) and repositories nested in the project, by relative path. */
13
+ repos?: Record<string, GitState>;
7
14
  }
8
15
  export interface RepoFinding {
9
- kind: 'hook' | 'config' | 'git' | 'command';
16
+ kind: 'hook' | 'config' | 'git' | 'command' | 'agent';
10
17
  detail: string;
11
18
  }
12
19
  export declare const snapshotRepo: (workspace: string) => RepoSnapshot;
@@ -15,8 +22,21 @@ export declare const snapshotRepo: (workspace: string) => RepoSnapshot;
15
22
  * Returns what was found (and fixed). Never touches what was already there before the sandbox ran.
16
23
  */
17
24
  export declare const guardRepo: (workspace: string, before: RepoSnapshot) => RepoFinding[];
18
- export declare const saveRepoSnapshot: (dataPath: string, workspace: string) => void;
19
- /** Re-checks the workspace of the current/last launch; findings are kept in a host-only file for `status`. */
25
+ /**
26
+ * At launch: every folder the sandbox will be able to write. The launch folder, projects mapped with it and every
27
+ * project already inside a project root are snapshotted as they are now, before the sandbox can touch them.
28
+ */
29
+ export declare const saveRepoSnapshot: (dataPath: string, workspace: string, more?: string[], roots?: string[]) => void;
30
+ /**
31
+ * A project that joins the running sandbox: snapshotted before its first session, then guarded like the launch
32
+ * folder. A project already guarded keeps its first snapshot (what was there before the sandbox could touch it).
33
+ */
34
+ export declare const addRepoSnapshot: (dataPath: string, workspace: string, o?: {
35
+ empty?: boolean;
36
+ }) => void;
37
+ /** The folders being guarded (for status). */
38
+ export declare const guardedProjects: (dataPath: string) => string[];
39
+ /** Re-checks every project of the current/last launch; findings are kept in a host-only file for `status`. Returns the new ones. */
20
40
  export declare const checkRepo: (dataPath: string) => RepoFinding[];
21
41
  export declare const repoFindings: (dataPath: string) => (RepoFinding & {
22
42
  at?: string;
@@ -6,14 +6,22 @@
6
6
  // - git: new or changed hooks, and config that makes git run programs (core.fsmonitor, core.sshCommand, filters,
7
7
  // include.path, ...), plus files that redirect where git reads config and hooks (commondir, config.worktree);
8
8
  // - commands: executables dropped in the project root under the name of something you type there (node.exe,
9
- // git.cmd, npm.cmd, ...), because Windows runs a program from the current folder before searching PATH.
9
+ // git.cmd, npm.cmd, ...), because Windows runs a program from the current folder before searching PATH;
10
+ // - agent and editor files: hooks, helper commands and MCP servers in .claude/ and .mcp.json, VS Code folder-open
11
+ // tasks and program paths, .envrc. Submodules and nested repositories get the git checks too.
10
12
  import crypto from 'node:crypto';
11
13
  import fs from 'node:fs';
12
14
  import path from 'node:path';
13
15
  import _ from 'lodash';
14
- import { dirExists, fileExists, isLink, tryReadJson, writeJson } from '../core/util.js';
16
+ import { z } from 'zod';
17
+ import { parse as parseJsonc } from 'jsonc-parser';
18
+ import { readValid, writeFileAtomic, writeJsonAtomic } from '../shared/rundir.js';
19
+ import { assertTestWritable } from './test-guard.js';
20
+ import { dirExists, fileExists, isLink } from '../core/util.js';
15
21
  /** Config keys that make git run a program, or load more config. */
16
- const DANGEROUS_KEY = /^(fsmonitor|hookspath|sshcommand|pager|editor|askpass|program|gpgprogram|textconv|command|cmd|clean|smudge|process|driver|helper|uploadpack|receivepack|path|attributesfile|excludesfile|worktree|worktreeconfig|gitproxy|proxycommand|sendemailvalidate|tool)\s*=/i;
22
+ const DANGEROUS_KEY = /^(fsmonitor|hookspath|sshcommand|pager|editor|askpass|program|gpgprogram|textconv|command|cmd|clean|smudge|process|driver|helper|uploadpack|receivepack|path|attributesfile|excludesfile|worktree|worktreeconfig|gitproxy|proxycommand|sendemailvalidate|tool|external|difffilter|defaultkeycommand|alternaterefscommand)\s*=/i;
23
+ /** Sections where any key runs a program ([pager] log = x), or where a "!" value does ([alias] st = !x). */
24
+ const dangerousInSection = (section, keyPart) => (section === 'pager' && keyPart.includes('=')) || (section === 'alias' && /^[^=]+=\s*"?\s*!/.test(keyPart));
17
25
  const SECTION = /^\s*\[([^\]]*)\]\s*/;
18
26
  const MARK = '# [agent-sandbox] disabled, added inside the sandbox: ';
19
27
  /** Files in .git that make git read config/hooks from somewhere else. */
@@ -30,7 +38,65 @@ const COMMANDS = new RegExp('^(' + [
30
38
  'make', 'cmake', 'ninja', 'gcc', 'g\\+\\+', 'clang', 'psql', 'mysql', 'mariadb', 'mongosh', 'redis-cli', 'docker', 'kubectl', 'terraform', 'az', 'aws',
31
39
  'powershell', 'pwsh', 'cmd', 'where', 'tasklist', 'tar', 'curl', 'ssh', 'scp',
32
40
  ].join('|') + ')\\.(exe|com|bat|cmd|vbs|vbe|js|jse|wsf|wsh|msc|ps1)$', 'i');
33
- const hash = (file) => (isLink(file) ? 'link' : crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'));
41
+ /**
42
+ * Project files that tools on this PC act on by themselves: Claude Code runs a project's hooks, helpers and MCP servers,
43
+ * VS Code runs folder-open tasks and the programs workspace settings point at, direnv runs .envrc. For each, only the
44
+ * part that can run something is compared (`risky`), so the agent may still edit the rest (permissions, formatting).
45
+ */
46
+ const AGENT_FILES = {
47
+ '.claude/settings.json': (d) => claudeRisk(d),
48
+ '.claude/settings.local.json': (d) => claudeRisk(d),
49
+ '.mcp.json': (_d, text) => text,
50
+ '.vscode/tasks.json': (d) => JSON.stringify(d).match(/"runOn"\s*:\s*"folderOpen"/) ? d : null,
51
+ '.vscode/settings.json': (d) => _.pickBy(_.isPlainObject(d) ? d : {}, (_v, k) => /path|executable|command|interpreter|shell|profile|args|env|exec|program|server|launch|runtime|binary/i.test(k)),
52
+ '.envrc': (_d, text) => text,
53
+ };
54
+ const claudeRisk = (d) => {
55
+ const s = (_.isPlainObject(d) ? d : {});
56
+ const permissions = (_.isPlainObject(s.permissions) ? s.permissions : {});
57
+ return { ..._.pick(s, ['hooks', 'apiKeyHelper', 'awsAuthRefresh', 'awsCredentialExport', 'otelHeadersHelper', 'statusLine', 'env', 'enableAllProjectMcpServers', 'enabledMcpjsonServers', 'disableAllHooks']), permissions: _.pick(permissions, ['defaultMode', 'additionalDirectories']) };
58
+ };
59
+ const MAX_AGENT_FILE_BYTES = 1024 * 1024;
60
+ /** What can run in one agent file, as a short fingerprint (null = the file is not there). */
61
+ const agentRisk = (workspace, rel) => {
62
+ const file = path.join(workspace, ...rel.split('/'));
63
+ if (!fs.existsSync(file))
64
+ return null;
65
+ if (isLink(file) || isLink(path.dirname(file)) || !fileExists(file))
66
+ return 'link-or-not-a-file';
67
+ if (fs.statSync(file).size > MAX_AGENT_FILE_BYTES)
68
+ return 'too-large-' + hash(file);
69
+ const text = fs.readFileSync(file, 'utf8');
70
+ const errors = [];
71
+ const data = parseJsonc(text, errors, { allowTrailingComma: true });
72
+ const risk = AGENT_FILES[rel](rel === '.envrc' ? null : data, text);
73
+ return crypto.createHash('sha256').update(JSON.stringify(risk ?? null) + (errors.length && rel !== '.envrc' && rel !== '.mcp.json' ? hash(file) : '')).digest('hex');
74
+ };
75
+ const agentSnapshot = (workspace) => Object.fromEntries(Object.keys(AGENT_FILES).flatMap((rel) => { const r = agentRisk(workspace, rel); return r ? [[rel, r]] : []; }));
76
+ /** SHA-256 in small chunks: a sandbox can plant a multi-gigabyte "node.exe" to exhaust this PC's memory. */
77
+ const hash = (file) => {
78
+ if (isLink(file))
79
+ return 'link';
80
+ const sum = crypto.createHash('sha256');
81
+ const buffer = Buffer.alloc(1 << 20);
82
+ let fd;
83
+ // A file that cannot be read (locked by a program in the sandbox) never matches the snapshot, so it is dealt with.
84
+ try {
85
+ fd = fs.openSync(file, 'r');
86
+ }
87
+ catch {
88
+ return 'unreadable-' + crypto.randomBytes(4).toString('hex');
89
+ }
90
+ try {
91
+ for (let n = fs.readSync(fd, buffer, 0, buffer.length, null); n > 0; n = fs.readSync(fd, buffer, 0, buffer.length, null))
92
+ sum.update(buffer.subarray(0, n));
93
+ }
94
+ finally {
95
+ fs.closeSync(fd);
96
+ }
97
+ return sum.digest('hex');
98
+ };
99
+ const MAX_CONFIG_BYTES = 4 * 1024 * 1024;
34
100
  const plainGitDir = (workspace) => {
35
101
  const dir = path.join(workspace, '.git');
36
102
  return dirExists(dir) && !isLink(dir) ? dir : null;
@@ -40,16 +106,68 @@ const filesIn = (dir, accept) => dirExists(dir) && !isLink(dir)
40
106
  : {};
41
107
  const configLines = (dir) => {
42
108
  const file = path.join(dir, 'config');
43
- return fileExists(file) && !isLink(file) ? fs.readFileSync(file, 'utf8').split(/\r?\n/) : [];
109
+ return fileExists(file) && !isLink(file) && fs.statSync(file).size <= MAX_CONFIG_BYTES ? fs.readFileSync(file, 'utf8').split(/\r?\n/) : [];
44
110
  };
111
+ const gitState = (dir) => ({
112
+ config: dir ? configLines(dir) : [],
113
+ hooks: dir ? filesIn(path.join(dir, 'hooks'), (n) => !n.endsWith('.sample')) : {},
114
+ redirects: dir ? REDIRECTS.filter((n) => fs.existsSync(path.join(dir, n))) : [],
115
+ });
116
+ const EMPTY_GIT = { config: [], hooks: {}, redirects: [] };
117
+ const MAX_OTHER_REPOS = 64;
118
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'agent-sandbox-quarantine', 'target', 'dist', 'build', 'bin', 'obj', '.venv', 'venv']);
119
+ const plainDirs = (dir, limit) => {
120
+ try {
121
+ return fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.isSymbolicLink()).slice(0, limit).map((e) => path.join(dir, e.name));
122
+ }
123
+ catch {
124
+ return [];
125
+ }
126
+ };
127
+ /**
128
+ * Git folders other than the project's own: submodules (.git/modules/**) and repositories nested up to two levels deep.
129
+ * git runs their hooks and config too (git status in a subfolder, VS Code's repository scan). Bounded: links are never
130
+ * followed and at most MAX_OTHER_REPOS are looked at.
131
+ */
132
+ const otherGitDirs = (workspace) => {
133
+ const found = [];
134
+ const main = plainGitDir(workspace);
135
+ // Submodules: a folder with a config and a HEAD is a git folder, and may have modules of its own.
136
+ const walk = (dir, depth) => {
137
+ for (const d of plainDirs(dir, 200)) {
138
+ if (found.length >= MAX_OTHER_REPOS)
139
+ return;
140
+ if (fileExists(path.join(d, 'config')) && fileExists(path.join(d, 'HEAD'))) {
141
+ found.push(d);
142
+ if (depth < 3)
143
+ walk(path.join(d, 'modules'), depth + 1);
144
+ }
145
+ else if (depth < 3)
146
+ walk(d, depth + 1);
147
+ }
148
+ };
149
+ if (main)
150
+ walk(path.join(main, 'modules'), 0);
151
+ for (const sub of plainDirs(workspace, 500).filter((d) => !SKIP_DIRS.has(path.basename(d).toLowerCase()))) {
152
+ for (const candidate of [sub, ...plainDirs(sub, 200).filter((d) => !SKIP_DIRS.has(path.basename(d).toLowerCase()))]) {
153
+ if (found.length >= MAX_OTHER_REPOS)
154
+ return found;
155
+ const git = plainGitDir(candidate);
156
+ if (git)
157
+ found.push(git);
158
+ }
159
+ }
160
+ return found;
161
+ };
162
+ const relative = (workspace, dir) => path.relative(workspace, dir).split(path.sep).join('/');
45
163
  export const snapshotRepo = (workspace) => {
46
164
  const dir = plainGitDir(workspace);
47
165
  return {
48
166
  gitDir: dir !== null,
49
- config: dir ? configLines(dir) : [],
50
- hooks: dir ? filesIn(path.join(dir, 'hooks'), (n) => !n.endsWith('.sample')) : {},
51
- redirects: dir ? REDIRECTS.filter((n) => fs.existsSync(path.join(dir, n))) : [],
167
+ ...gitState(dir),
52
168
  commands: filesIn(workspace, (n) => COMMANDS.test(n)),
169
+ agent: agentSnapshot(workspace),
170
+ repos: Object.fromEntries(otherGitDirs(workspace).map((d) => [relative(workspace, d), gitState(d)])),
53
171
  };
54
172
  };
55
173
  /** Moves files into <dir>\agent-sandbox-quarantine\<time>\ and reports them. */
@@ -59,7 +177,12 @@ const quarantine = (from, names, into, kind) => {
59
177
  const target = path.join(into, 'agent-sandbox-quarantine', String(Date.now()));
60
178
  fs.mkdirSync(target, { recursive: true });
61
179
  return names.map((name) => {
62
- fs.renameSync(path.join(from, name), path.join(target, name));
180
+ try {
181
+ fs.renameSync(path.join(from, name), path.join(target, name));
182
+ }
183
+ catch (e) {
184
+ return { kind, detail: name + ' (could not be moved: ' + e.code + '; delete it yourself before running anything here)' };
185
+ }
63
186
  return { kind, detail: name + ' (moved to ' + target + ')' };
64
187
  });
65
188
  };
@@ -70,22 +193,30 @@ const disableConfig = (dir, before) => {
70
193
  const seen = {};
71
194
  const findings = [];
72
195
  let inInclude = false;
196
+ let section = '';
73
197
  const fixed = lines.map((line) => {
74
198
  const header = SECTION.exec(line);
75
- if (header)
199
+ if (header) {
200
+ section = header[1].trim().toLowerCase().split(/\s/)[0];
76
201
  inInclude = /^include(if\s+"[^"]*")?$/i.test(header[1].trim());
202
+ }
77
203
  seen[line] = (seen[line] ?? 0) + 1;
78
204
  if (line.startsWith(MARK) || seen[line] <= (known[line] ?? 0))
79
205
  return line;
80
206
  // git also accepts "[core] fsmonitor = x" on one line, so look past a leading section header.
81
207
  const keyPart = line.replace(SECTION, '').trim();
82
- if (!(DANGEROUS_KEY.test(keyPart) || (inInclude && keyPart.includes('='))))
208
+ if (!(DANGEROUS_KEY.test(keyPart) || (inInclude && keyPart.includes('=')) || dangerousInSection(section, keyPart)))
83
209
  return line;
84
210
  findings.push({ kind: 'config', detail: line.trim() });
85
211
  return MARK + line.trim();
86
212
  });
87
213
  if (findings.length)
88
- fs.writeFileSync(path.join(dir, 'config'), fixed.join('\n'), 'utf8');
214
+ writeFileAtomic(path.join(dir, 'config'), fixed.join('\n'));
215
+ const file = path.join(dir, 'config');
216
+ // A link or a huge file is not read at all, so say so instead of letting it pass as "nothing found".
217
+ const unchecked = isLink(file) ? '.git/config is a link' : lines.length === 0 && fileExists(file) && fs.statSync(file).size > MAX_CONFIG_BYTES ? '.git/config is over 4 MB' : null;
218
+ if (unchecked)
219
+ findings.push({ kind: 'git', detail: unchecked + ', so it cannot be checked. Do not run git in this folder until you have looked at it.' });
89
220
  return findings;
90
221
  };
91
222
  /**
@@ -93,9 +224,19 @@ const disableConfig = (dir, before) => {
93
224
  * Returns what was found (and fixed). Never touches what was already there before the sandbox ran.
94
225
  */
95
226
  export const guardRepo = (workspace, before) => {
227
+ assertTestWritable(workspace);
96
228
  const findings = [];
97
229
  const commandsNow = filesIn(workspace, (n) => COMMANDS.test(n));
98
230
  findings.push(...quarantine(workspace, Object.keys(commandsNow).filter((n) => before.commands[n] !== commandsNow[n]), workspace, 'command'));
231
+ // Snapshots from before these checks existed have no baseline: checking against nothing would move your own files.
232
+ if (before.agent)
233
+ findings.push(...guardAgentFiles(workspace, before.agent));
234
+ if (before.repos) {
235
+ for (const other of otherGitDirs(workspace)) {
236
+ const rel = relative(workspace, other);
237
+ findings.push(...guardGitDir(other, before.repos[rel] ?? EMPTY_GIT).map((f) => ({ ...f, detail: rel + ': ' + f.detail })));
238
+ }
239
+ }
99
240
  const dir = plainGitDir(workspace);
100
241
  if (!dir) {
101
242
  const replaced = before.gitDir && fs.existsSync(path.join(workspace, '.git'));
@@ -103,26 +244,104 @@ export const guardRepo = (workspace, before) => {
103
244
  findings.push({ kind: 'git', detail: '.git was replaced by a link or a "gitdir:" file inside the sandbox. Do not run git in ' + workspace + ' until you have checked it.' });
104
245
  return findings;
105
246
  }
247
+ findings.push(...guardGitDir(dir, before));
248
+ return findings;
249
+ };
250
+ const guardGitDir = (dir, before) => {
106
251
  const hooksDir = path.join(dir, 'hooks');
107
252
  const hooksNow = filesIn(hooksDir, (n) => !n.endsWith('.sample'));
108
- findings.push(...quarantine(hooksDir, Object.keys(hooksNow).filter((n) => before.hooks[n] !== hooksNow[n]), dir, 'hook'));
109
- findings.push(...quarantine(dir, REDIRECTS.filter((n) => fs.existsSync(path.join(dir, n)) && !before.redirects.includes(n)), dir, 'git'));
110
- findings.push(...disableConfig(dir, before.config));
111
- return findings;
253
+ return [
254
+ ...quarantine(hooksDir, Object.keys(hooksNow).filter((n) => before.hooks[n] !== hooksNow[n]), dir, 'hook'),
255
+ ...quarantine(dir, REDIRECTS.filter((n) => fs.existsSync(path.join(dir, n)) && !before.redirects.includes(n)), dir, 'git'),
256
+ ...disableConfig(dir, before.config),
257
+ ];
258
+ };
259
+ /** Agent files whose runnable part changed are moved to the quarantine folder (the rest of your edits are in there too). */
260
+ const guardAgentFiles = (workspace, before) => {
261
+ const changed = Object.keys(AGENT_FILES).filter((rel) => { const now = agentRisk(workspace, rel); return now !== null && now !== before[rel]; });
262
+ if (!changed.length)
263
+ return [];
264
+ const target = path.join(workspace, 'agent-sandbox-quarantine', String(Date.now()));
265
+ fs.mkdirSync(target, { recursive: true });
266
+ return changed.map((rel) => {
267
+ const what = rel + ' (the sandbox added something it would run on this PC: hooks, MCP servers, helper commands or tasks';
268
+ try {
269
+ fs.renameSync(path.join(workspace, ...rel.split('/')), path.join(target, rel.replace(/\//g, '_')));
270
+ }
271
+ catch (e) {
272
+ return { kind: 'agent', detail: what + '; could not be moved: ' + e.code + '. Check it before you open this folder in Claude Code or VS Code)' };
273
+ }
274
+ return { kind: 'agent', detail: what + '; moved to ' + target + ')' };
275
+ });
112
276
  };
113
277
  // ---- snapshot storage (host-only, next to the sync state) ---------------------------------------------------------
114
278
  const syncDir = (dataPath) => path.join(path.dirname(path.dirname(dataPath)), 'sync');
115
279
  const snapshotFile = (dataPath) => path.join(syncDir(dataPath), path.basename(dataPath) + '-repo.json');
116
280
  const findingsFile = (dataPath) => path.join(syncDir(dataPath), path.basename(dataPath) + '-repo-findings.json');
117
- export const saveRepoSnapshot = (dataPath, workspace) => writeJson(snapshotFile(dataPath), { workspace, snapshot: snapshotRepo(workspace) });
118
- /** Re-checks the workspace of the current/last launch; findings are kept in a host-only file for `status`. */
281
+ const Finding = z.object({ kind: z.enum(['hook', 'config', 'git', 'command', 'agent']), detail: z.string().max(4096), at: z.string().max(64).optional() });
282
+ const GitStateSchema = z.object({ config: z.array(z.string()), hooks: z.record(z.string(), z.string()), redirects: z.array(z.string()) });
283
+ const Saved = z.object({
284
+ workspace: z.string().max(1024).refine((p) => path.isAbsolute(p)),
285
+ snapshot: GitStateSchema.extend({ gitDir: z.boolean(), commands: z.record(z.string(), z.string()), agent: z.record(z.string(), z.string()).optional(), repos: z.record(z.string(), GitStateSchema).optional() }),
286
+ });
287
+ const Store = z.strictObject({ projects: z.array(Saved).max(4096), roots: z.array(z.string().max(1024).refine((p) => path.isAbsolute(p))).max(64) });
288
+ const StoreAny = z.union([Store, Saved.transform((s) => ({ projects: [s], roots: [] }))]);
289
+ const readStore = (dataPath) => readValid(StoreAny, snapshotFile(dataPath)) ?? { projects: [], roots: [] };
290
+ const EMPTY = { gitDir: false, config: [], hooks: {}, redirects: [], commands: {}, agent: {}, repos: {} };
291
+ const MAX_ROOT_PROJECTS = 2000;
292
+ /** The folders directly inside a project root (plain folders only; links are never followed). */
293
+ const rootProjects = (root) => {
294
+ if (!dirExists(root) || isLink(root))
295
+ return [];
296
+ return fs.readdirSync(root, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.isSymbolicLink()).slice(0, MAX_ROOT_PROJECTS).map((e) => path.join(root, e.name));
297
+ };
298
+ const has = (projects, workspace) => projects.some((s) => s.workspace.toLowerCase() === workspace.toLowerCase());
299
+ /**
300
+ * At launch: every folder the sandbox will be able to write. The launch folder, projects mapped with it and every
301
+ * project already inside a project root are snapshotted as they are now, before the sandbox can touch them.
302
+ */
303
+ export const saveRepoSnapshot = (dataPath, workspace, more = [], roots = []) => {
304
+ const folders = _.uniqBy([workspace, ...more, ...roots.flatMap((r) => [r, ...rootProjects(r)])], (w) => w.toLowerCase());
305
+ writeJsonAtomic(snapshotFile(dataPath), { projects: folders.map((w) => ({ workspace: w, snapshot: snapshotRepo(w) })), roots });
306
+ };
307
+ /**
308
+ * A project that joins the running sandbox: snapshotted before its first session, then guarded like the launch
309
+ * folder. A project already guarded keeps its first snapshot (what was there before the sandbox could touch it).
310
+ */
311
+ export const addRepoSnapshot = (dataPath, workspace, o = {}) => {
312
+ const store = readStore(dataPath);
313
+ // A folder inside one that is guarded already is covered by it only for its root files; it gets its own entry.
314
+ if (has(store.projects, workspace))
315
+ return;
316
+ writeJsonAtomic(snapshotFile(dataPath), { ...store, projects: [...store.projects, { workspace, snapshot: o.empty ? EMPTY : snapshotRepo(workspace) }] });
317
+ };
318
+ /**
319
+ * Folders that appeared inside a project root after the launch: the sandbox could have written them from the start,
320
+ * so they get an EMPTY baseline: any git hook, git config that runs programs (core.hooksPath, core.fsmonitor, ...)
321
+ * or planted command in them counts as added inside the sandbox and is neutralised.
322
+ */
323
+ const discoverRootProjects = (dataPath) => {
324
+ const store = readStore(dataPath);
325
+ const found = store.roots.flatMap(rootProjects).filter((w) => !has(store.projects, w)).map((workspace) => ({ workspace, snapshot: EMPTY }));
326
+ if (!found.length)
327
+ return store.projects;
328
+ const projects = [...store.projects, ...found].slice(0, 4096);
329
+ writeJsonAtomic(snapshotFile(dataPath), { ...store, projects });
330
+ return projects;
331
+ };
332
+ /** The folders being guarded (for status). */
333
+ export const guardedProjects = (dataPath) => readStore(dataPath).projects.map((s) => s.workspace);
334
+ /** Re-checks every project of the current/last launch; findings are kept in a host-only file for `status`. Returns the new ones. */
119
335
  export const checkRepo = (dataPath) => {
120
- const saved = tryReadJson(snapshotFile(dataPath));
121
- if (!saved?.snapshot?.commands)
336
+ const saved = discoverRootProjects(dataPath);
337
+ if (!saved.length)
122
338
  return [];
123
- const findings = guardRepo(saved.workspace, saved.snapshot);
339
+ const old = repoFindings(dataPath);
340
+ // A finding that cannot be fixed (a file locked in the sandbox) is reported once, not every minute.
341
+ const findings = saved.flatMap((s) => guardRepo(s.workspace, s.snapshot).map((f) => (saved.length > 1 ? { ...f, detail: s.workspace + ': ' + f.detail } : f)))
342
+ .filter((f) => !old.some((o) => o.kind === f.kind && o.detail === f.detail));
124
343
  if (findings.length)
125
- writeJson(findingsFile(dataPath), [...repoFindings(dataPath), ...findings.map((f) => ({ ...f, at: new Date().toISOString() }))]);
344
+ writeJsonAtomic(findingsFile(dataPath), [...old, ...findings.map((f) => ({ ...f, at: new Date().toISOString() }))]);
126
345
  return findings;
127
346
  };
128
- export const repoFindings = (dataPath) => tryReadJson(findingsFile(dataPath)) ?? [];
347
+ export const repoFindings = (dataPath) => readValid(z.array(Finding).max(10000), findingsFile(dataPath)) ?? [];
@@ -0,0 +1,67 @@
1
+ import { type ChildProcess } from 'node:child_process';
2
+ import { z } from 'zod';
3
+ declare const Life: z.ZodObject<{
4
+ started: z.ZodString;
5
+ lastSeen: z.ZodString;
6
+ keepAwake: z.ZodBoolean;
7
+ ended: z.ZodOptional<z.ZodObject<{
8
+ at: z.ZodString;
9
+ cause: z.ZodNullable<z.ZodObject<{
10
+ kind: z.ZodEnum<{
11
+ restart: "restart";
12
+ updates: "updates";
13
+ crash: "crash";
14
+ sleep: "sleep";
15
+ memory: "memory";
16
+ sandboxCrash: "sandboxCrash";
17
+ }>;
18
+ at: z.ZodString;
19
+ text: z.ZodString;
20
+ }, z.core.$strip>>;
21
+ }, z.core.$strip>>;
22
+ reported: z.ZodOptional<z.ZodBoolean>;
23
+ }, z.core.$strip>;
24
+ type LifeT = z.infer<typeof Life>;
25
+ export declare const readLife: (storageRoot: string, dataPath: string) => LifeT | null;
26
+ /** At launch: a fresh record (the previous one has been reported by then). */
27
+ export declare const startLife: (storageRoot: string, dataPath: string, keepAwake: boolean, now?: Date) => void;
28
+ /** Every minute while the sandbox runs. */
29
+ export declare const beatLife: (storageRoot: string, dataPath: string, now?: Date) => void;
30
+ /** The watcher saw the sandbox close while this PC kept running: closed by you (no cause), or by sleep / low memory. */
31
+ export declare const endLife: (storageRoot: string, dataPath: string, cause: StopCause | null, now?: Date) => void;
32
+ export interface StopEvent {
33
+ at: string;
34
+ id: number;
35
+ provider: string;
36
+ process: string;
37
+ }
38
+ export type StopKind = 'updates' | 'restart' | 'crash' | 'sleep' | 'memory' | 'sandboxCrash';
39
+ export interface StopCause {
40
+ kind: StopKind;
41
+ at: string;
42
+ text: string;
43
+ }
44
+ /**
45
+ * The first event that ends a running sandbox (pure): Windows inside the sandbox crashing (Hyper-V 18590; `process`
46
+ * holds its message with the stop code), a restart or shutdown, a crash or power loss, sleep, low memory. Switching
47
+ * user does not end it (seen on 13 Oct 2026: the sandbox ran on after a switch).
48
+ */
49
+ export declare const explainStop: (events: StopEvent[]) => StopCause | null;
50
+ /** System log and Hyper-V events since `since` that can end a sandbox (Windows only; empty when the logs cannot be read). */
51
+ export declare const readStopEvents: (since: Date) => StopEvent[];
52
+ /** Looking back from the last time the sandbox was seen running (a minute between checks, plus margin). */
53
+ export declare const causeSince: (lastSeen: string) => StopCause | null;
54
+ /**
55
+ * At launch: one warning when the last sandbox ended without anyone closing it (the PC restarted, crashed, slept or ran
56
+ * out of memory), then never again for that run. `watcherAlive` = its watcher is still running (then it did not end).
57
+ */
58
+ export declare const lastStopWarning: (storageRoot: string, dataPath: string, watcherAlive: boolean, find?: (lastSeen: string) => StopCause | null) => string | null;
59
+ /** Windows has installed updates and waits to restart: it may do so on its own, outside your active hours. */
60
+ export declare const updateRestartPending: () => boolean;
61
+ export declare const UPDATE_RESTART_WARNING = "Windows is waiting to restart for updates and may do so on its own (often overnight), which closes the sandbox. Restart first, or pause updates: Settings > Windows Update > Pause updates.";
62
+ /**
63
+ * sandbox.keepAwake: asks Windows not to sleep while the sandbox runs (as a video player does; the screen may still turn
64
+ * off). A small PowerShell holds the request until `ownerPid` exits, so it never outlives the watcher.
65
+ */
66
+ export declare const keepAwake: (ownerPid: number) => ChildProcess | null;
67
+ export {};
@@ -0,0 +1,126 @@
1
+ // Why the last sandbox stopped, when nobody stopped it. The background watcher writes a host-only "life" record (when
2
+ // the sandbox started, when it was last seen running, how it ended) and keeps this PC awake while it runs
3
+ // (sandbox.keepAwake). When the PC restarts, sleeps or crashes, the watcher stops too and the record has no end: the
4
+ // next launch reads Windows' System log from that moment on and says what happened, and what keeps it from happening.
5
+ import { spawn, spawnSync } from 'node:child_process';
6
+ import path from 'node:path';
7
+ import { z } from 'zod';
8
+ import { SYSTEM } from '../shared/system.js';
9
+ import { readValid, writeJsonAtomic } from '../shared/rundir.js';
10
+ import { isWin } from '../core/util.js';
11
+ import { refuseInSelfTest } from './test-guard.js';
12
+ const Iso = z.string().max(40);
13
+ const Life = z.object({
14
+ started: Iso, lastSeen: Iso, keepAwake: z.boolean(),
15
+ ended: z.object({ at: Iso, cause: z.object({ kind: z.enum(['updates', 'restart', 'crash', 'sleep', 'memory', 'sandboxCrash']), at: Iso, text: z.string().max(400) }).nullable() }).optional(),
16
+ reported: z.boolean().optional(),
17
+ });
18
+ const lifeFile = (storageRoot, dataPath) => path.join(storageRoot, 'sync', path.basename(dataPath) + '-life.json');
19
+ export const readLife = (storageRoot, dataPath) => readValid(Life, lifeFile(storageRoot, dataPath));
20
+ const writeLife = (storageRoot, dataPath, life) => writeJsonAtomic(lifeFile(storageRoot, dataPath), life);
21
+ /** At launch: a fresh record (the previous one has been reported by then). */
22
+ export const startLife = (storageRoot, dataPath, keepAwake, now = new Date()) => writeLife(storageRoot, dataPath, { started: now.toISOString(), lastSeen: now.toISOString(), keepAwake });
23
+ /** Every minute while the sandbox runs. */
24
+ export const beatLife = (storageRoot, dataPath, now = new Date()) => {
25
+ const life = readLife(storageRoot, dataPath);
26
+ if (life && !life.ended)
27
+ writeLife(storageRoot, dataPath, { ...life, lastSeen: now.toISOString() });
28
+ };
29
+ /** The watcher saw the sandbox close while this PC kept running: closed by you (no cause), or by sleep / low memory. */
30
+ export const endLife = (storageRoot, dataPath, cause, now = new Date()) => {
31
+ const life = readLife(storageRoot, dataPath);
32
+ if (life && !life.ended)
33
+ writeLife(storageRoot, dataPath, { ...life, ended: { at: now.toISOString(), cause } });
34
+ };
35
+ const StopEvents = z.array(z.object({ at: Iso, id: z.number().int(), provider: z.string().max(200), process: z.string().max(400).nullable().transform((p) => p ?? '') })).max(100);
36
+ const UPDATERS = /^(trustedinstaller|mousocoreworker|musnotification(ux)?|usoclient|wuauclt|tiworker|updateorchestrator)\.exe$/i;
37
+ /**
38
+ * The first event that ends a running sandbox (pure): Windows inside the sandbox crashing (Hyper-V 18590; `process`
39
+ * holds its message with the stop code), a restart or shutdown, a crash or power loss, sleep, low memory. Switching
40
+ * user does not end it (seen on 13 Oct 2026: the sandbox ran on after a switch).
41
+ */
42
+ export const explainStop = (events) => {
43
+ const causes = events.flatMap((e) => {
44
+ const p = e.provider.toLowerCase();
45
+ if (e.id === 1074 && p.includes('user32')) {
46
+ const exe = e.process.split(/[\\/]/).pop()?.replace(/\s*\(.*$/, '') ?? '';
47
+ return UPDATERS.test(exe)
48
+ ? [{ kind: 'updates', at: e.at, text: 'Windows restarted this PC to install updates' }]
49
+ : [{ kind: 'restart', at: e.at, text: 'this PC was restarted or shut down' + (exe ? ' (by ' + exe + ')' : '') }];
50
+ }
51
+ if ((e.id === 41 && p.includes('kernel-power')) || (e.id === 6008 && p.includes('eventlog')))
52
+ return [{ kind: 'crash', at: e.at, text: 'Windows stopped unexpectedly (a crash, a power cut, or the power button held down)' }];
53
+ if (e.id === 42 && p.includes('kernel-power'))
54
+ return [{ kind: 'sleep', at: e.at, text: 'this PC went to sleep or hibernated' }];
55
+ if (e.id === 2004 && p.includes('resource-exhaustion'))
56
+ return [{ kind: 'memory', at: e.at, text: 'this PC ran out of memory' }];
57
+ if (e.id === 18590 && p.includes('hyper-v-worker')) {
58
+ const code = /ErrorCode0:\s*(0x[0-9a-f]{1,8})/i.exec(e.process)?.[1];
59
+ const sub = /ErrorCode1:\s*(0x[0-9a-f]{1,16})/i.exec(e.process)?.[1];
60
+ return [{ kind: 'sandboxCrash', at: e.at, text: 'Windows inside the sandbox crashed' + (code ? ' (stop code ' + code + (sub ? ', ' + sub : '') + ')' : '') }];
61
+ }
62
+ return [];
63
+ });
64
+ return causes.sort((a, b) => Date.parse(a.at) - Date.parse(b.at))[0] ?? null;
65
+ };
66
+ const ADVICE = {
67
+ updates: 'Before leaving agents overnight, restart for pending updates first or pause them (Settings > Windows Update > Pause updates).',
68
+ restart: 'If that was not you, check Settings > Windows Update and Event Viewer > Windows Logs > System.',
69
+ crash: 'Event Viewer > Windows Logs > System has the details; a laptop also needs to stay plugged in.',
70
+ sleep: 'sandbox.keepAwake keeps this PC awake while a sandbox runs (closing a laptop lid still sleeps it). Check it is on, and that sleep is not forced by a policy.',
71
+ sandboxCrash: 'C:\\AgentData\\logs\\memory-*.log shows what used memory just before. Fewer builds at once inside the sandbox (e.g. cargo build.jobs) makes it less likely.',
72
+ memory: 'Give the sandbox less RAM (sandbox.maxMemoryMb, sandbox.reserveHostMb) or run fewer builds at once inside it.',
73
+ };
74
+ /** System log and Hyper-V events since `since` that can end a sandbox (Windows only; empty when the logs cannot be read). */
75
+ export const readStopEvents = (since) => {
76
+ if (!isWin)
77
+ return [];
78
+ refuseInSelfTest('read the Windows event log');
79
+ const script = "$ErrorActionPreference='SilentlyContinue'; $t=[datetime]::Parse('" + since.toISOString() + "').ToLocalTime(); $e = Get-WinEvent -FilterHashtable @(@{LogName='System'; Id=1074,6008,41,42,2004; StartTime=$t}, @{LogName='Microsoft-Windows-Hyper-V-Worker-Admin'; Id=18590; StartTime=$t}) -MaxEvents 100 | ForEach-Object { [pscustomobject]@{ at=$_.TimeCreated.ToUniversalTime().ToString('o'); id=$_.Id; provider=$_.ProviderName; process=$(if ($_.Id -eq 1074) { [string]$_.Properties[0].Value } elseif ($_.Id -eq 18590) { ([string]$_.Message).Substring(0, [math]::Min(390, ([string]$_.Message).Length)) } else { '' }) } }; ConvertTo-Json -Compress -InputObject @($e)";
80
+ const result = spawnSync(SYSTEM.powershell(), ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64')], { encoding: 'utf8', windowsHide: true, timeout: 60000 });
81
+ try {
82
+ return StopEvents.parse(JSON.parse(result.stdout || '[]'));
83
+ }
84
+ catch {
85
+ return [];
86
+ }
87
+ };
88
+ /** Looking back from the last time the sandbox was seen running (a minute between checks, plus margin). */
89
+ export const causeSince = (lastSeen) => explainStop(readStopEvents(new Date(Date.parse(lastSeen) - 2 * 60 * 1000)));
90
+ const when = (iso) => new Date(iso).toLocaleString(undefined, { weekday: 'short', hour: '2-digit', minute: '2-digit' });
91
+ /**
92
+ * At launch: one warning when the last sandbox ended without anyone closing it (the PC restarted, crashed, slept or ran
93
+ * out of memory), then never again for that run. `watcherAlive` = its watcher is still running (then it did not end).
94
+ */
95
+ export const lastStopWarning = (storageRoot, dataPath, watcherAlive, find = causeSince) => {
96
+ const life = readLife(storageRoot, dataPath);
97
+ if (!life || life.reported || watcherAlive)
98
+ return null;
99
+ writeLife(storageRoot, dataPath, { ...life, reported: true });
100
+ if (life.ended && !life.ended.cause)
101
+ return null;
102
+ const cause = life.ended ? life.ended.cause : find(life.lastSeen);
103
+ const what = cause ? cause.text + ' at ' + when(cause.at) + '. ' + ADVICE[cause.kind] : 'this PC stopped (Windows logged no reason). ' + ADVICE.crash;
104
+ return 'The last sandbox did not close normally: it was last seen running at ' + when(life.lastSeen) + ', and ' + what + ' Agent conversations are kept: /resume in Claude picks one up.';
105
+ };
106
+ // ---- Windows Update and keeping awake ----------------------------------------------------------------------------
107
+ /** Windows has installed updates and waits to restart: it may do so on its own, outside your active hours. */
108
+ export const updateRestartPending = () => isWin && [
109
+ 'HKLM\\SOFTWARE\\Microsoft\\WindowsUpdate\\Auto Update\\RebootRequired',
110
+ 'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootPending',
111
+ ].some((key) => spawnSync(SYSTEM.reg(), ['query', key], { windowsHide: true, stdio: 'ignore', timeout: 15000 }).status === 0);
112
+ export const UPDATE_RESTART_WARNING = 'Windows is waiting to restart for updates and may do so on its own (often overnight), which closes the sandbox. Restart first, or pause updates: Settings > Windows Update > Pause updates.';
113
+ /**
114
+ * sandbox.keepAwake: asks Windows not to sleep while the sandbox runs (as a video player does; the screen may still turn
115
+ * off). A small PowerShell holds the request until `ownerPid` exits, so it never outlives the watcher.
116
+ */
117
+ export const keepAwake = (ownerPid) => {
118
+ if (!isWin)
119
+ return null;
120
+ refuseInSelfTest('keep this PC awake');
121
+ const script = "Add-Type -Name Power -Namespace AgentSandbox -MemberDefinition '[DllImport(\"kernel32.dll\")] public static extern uint SetThreadExecutionState(uint f);'; "
122
+ + '[void][AgentSandbox.Power]::SetThreadExecutionState([uint32]2147483649); Wait-Process -Id ' + Math.trunc(ownerPid) + ' -ErrorAction SilentlyContinue';
123
+ const child = spawn(SYSTEM.powershell(), ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64')], { stdio: 'ignore', windowsHide: true });
124
+ child.on('error', () => undefined);
125
+ return child;
126
+ };
@@ -29,4 +29,12 @@ export declare const ensureTerminal: (dataPath: string, refresh: boolean, option
29
29
  exe: string | null;
30
30
  message: string;
31
31
  }>;
32
+ /** Where the sandbox finds conpty.dll + OpenConsole.exe (guest/ptyhost.ts points ptyhost at it). */
33
+ export declare const conptyDir: (dataPath: string) => string;
34
+ /**
35
+ * Microsoft's newer pseudo console (the Microsoft.Windows.Console.ConPTY package: conpty.dll and OpenConsole.exe, the
36
+ * same one Windows Terminal and VS Code use), downloaded once and verified against the SHA-512 NuGet publishes. Agent
37
+ * sessions use it instead of the one built into Windows, which re-renders the screen and garbles it after resizes.
38
+ */
39
+ export declare const ensureConpty: (dataPath: string, refresh: boolean) => Promise<string>;
32
40
  export {};