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,6 +2,20 @@ import type { Config, HostProbe, ResolvedRun, RunRequest } from '../core/types.j
2
2
  import { generateSandbox } from '../host/generate.js';
3
3
  import type { Harness } from './harness.js';
4
4
  export declare const HOST_NODE_VERSION = "v22.3.0";
5
+ /** Colour codes are removed from captured output, so tests can match plain text. */
6
+ export declare const stripAnsi: (text: string) => string;
7
+ export interface NodeRun {
8
+ status: number | null;
9
+ out: string;
10
+ timedOut: boolean;
11
+ }
12
+ export interface NodeRunOptions {
13
+ cwd?: string;
14
+ env?: NodeJS.ProcessEnv;
15
+ timeoutMs?: number;
16
+ }
17
+ /** Runs `node <args>` and collects stdout + stderr (colours removed). Never throws; a hung process is killed at the timeout. */
18
+ export declare const runNode: (args: string[], options?: NodeRunOptions) => Promise<NodeRun>;
5
19
  export declare const createFixtures: (t: Harness) => {
6
20
  at: (...parts: string[]) => string;
7
21
  hostNode: string;
@@ -17,6 +31,18 @@ export declare const createFixtures: (t: Harness) => {
17
31
  defaultHarness: string;
18
32
  guestWorkspace: string;
19
33
  readOnly: boolean;
34
+ workspaceFilter: {
35
+ enabled: boolean;
36
+ exclude: string[];
37
+ include: string[];
38
+ };
39
+ audit: {
40
+ enabled: boolean;
41
+ days: number;
42
+ maxMb: number;
43
+ processes: boolean;
44
+ files: boolean;
45
+ };
20
46
  mappings: {
21
47
  host: string;
22
48
  guest: string;
@@ -26,6 +52,9 @@ export declare const createFixtures: (t: Harness) => {
26
52
  sandbox: {
27
53
  memoryMb: number | "auto";
28
54
  maxMemoryMb: number;
55
+ maxMemoryPercent: number;
56
+ cpuLimit: number | "none";
57
+ pageFileMb: number;
29
58
  reserveHostMb: number;
30
59
  vGpu: boolean;
31
60
  networking: boolean;
@@ -35,14 +64,25 @@ export declare const createFixtures: (t: Harness) => {
35
64
  printerRedirection: boolean;
36
65
  protectedClient: boolean;
37
66
  headless: boolean;
67
+ keepAwake: boolean;
38
68
  };
39
69
  storage: {
40
70
  root: string;
41
71
  persistAgentState: boolean;
42
72
  dataName: string;
73
+ markSandboxFiles: boolean;
74
+ warnDataGb: number;
43
75
  };
44
76
  network: {
45
77
  blockPrivateRanges: boolean;
78
+ allowOnly: string[];
79
+ blockHosts: string[];
80
+ forwardPorts: {
81
+ pc: number;
82
+ sandbox: number;
83
+ }[];
84
+ dns: string[];
85
+ proxy: string;
46
86
  };
47
87
  provisioning: {
48
88
  copyFromHost: boolean;
@@ -55,6 +95,12 @@ export declare const createFixtures: (t: Harness) => {
55
95
  terminal: {
56
96
  enabled: boolean;
57
97
  };
98
+ conpty: {
99
+ enabled: boolean;
100
+ };
101
+ };
102
+ downloads: {
103
+ requireChecksum: boolean;
58
104
  };
59
105
  toolchains: {
60
106
  node: {
@@ -145,6 +191,9 @@ export declare const createFixtures: (t: Harness) => {
145
191
  };
146
192
  vscode: {
147
193
  enabled: boolean;
194
+ setup: boolean;
195
+ recommended: boolean;
196
+ extensions: string[];
148
197
  };
149
198
  environment: Record<string, string>;
150
199
  };
@@ -184,6 +233,10 @@ export declare const createFixtures: (t: Harness) => {
184
233
  auto: boolean;
185
234
  autoHide: boolean;
186
235
  };
236
+ agentCommands: {
237
+ enabled: boolean;
238
+ agents: string[] | "auto";
239
+ };
187
240
  secrets: {
188
241
  strip: boolean;
189
242
  allow: string[];
@@ -193,6 +246,19 @@ export declare const createFixtures: (t: Harness) => {
193
246
  personalization: {
194
247
  enabled: boolean;
195
248
  };
249
+ apps: {
250
+ install: string[];
251
+ custom: Record<string, {
252
+ name: string;
253
+ exe: string;
254
+ github?: string | undefined;
255
+ asset?: string | undefined;
256
+ url?: string | undefined;
257
+ sha256?: string | undefined;
258
+ unpack?: "file" | "archive" | "nsis" | "msi" | undefined;
259
+ cli?: boolean | undefined;
260
+ }>;
261
+ };
196
262
  claudeProfile: {
197
263
  enabled: boolean;
198
264
  hostDir: string;
@@ -201,7 +267,9 @@ export declare const createFixtures: (t: Harness) => {
201
267
  copyCredentials: boolean;
202
268
  overwrite: boolean;
203
269
  syncHistory: boolean;
270
+ historyScope: "project" | "all";
204
271
  syncClaudeMem: boolean;
272
+ syncClaudeMemBack: boolean;
205
273
  };
206
274
  harnesses: Record<string, {
207
275
  arguments: string[];
@@ -217,16 +285,33 @@ export declare const createFixtures: (t: Harness) => {
217
285
  hooks: {
218
286
  failFast: boolean;
219
287
  };
288
+ projects: {
289
+ all: boolean;
290
+ exclude: string[];
291
+ roots: string[];
292
+ askToAddRoot: boolean;
293
+ declinedRoots: string[];
294
+ wsbShare: boolean;
295
+ };
220
296
  $schema?: string | undefined;
221
297
  };
222
298
  probe: HostProbe;
223
299
  run: (over?: Partial<RunRequest>, config?: Config) => Promise<ResolvedRun>;
224
300
  generate: (r: ResolvedRun, args?: string[], p?: HostProbe) => ReturnType<typeof generateSandbox>;
301
+ launch: (over?: Partial<RunRequest>, options?: {
302
+ config?: Config;
303
+ args?: string[];
304
+ probe?: HostProbe;
305
+ }) => Promise<ReturnType<typeof generateSandbox>>;
225
306
  writeLocal: (dir: string, content: unknown) => void;
226
- runGuest: (launchPath: string, data: string, workspace: string, extra?: Record<string, string>) => {
227
- status: number | null;
228
- out: string;
307
+ historySides: (name: string, hostWorkspace: string, guestWorkspace: string) => {
308
+ hostClaudeDir: string;
309
+ guestClaudeDir: string;
310
+ hostWorkspace: string;
311
+ guestWorkspace: string;
229
312
  };
230
- readJson: (file: string) => any;
313
+ runGuest: (launchPath: string, data: string, workspace: string, extra?: Record<string, string>) => Promise<NodeRun>;
314
+ runBin: (name: string, args: string[], options?: NodeRunOptions) => Promise<NodeRun>;
315
+ readJson: <T = any>(file: string) => T;
231
316
  };
232
317
  export type Fixtures = ReturnType<typeof createFixtures>;
@@ -1,16 +1,27 @@
1
- // A fake "host PC" inside the temp folder: Node, claude.exe, a ~/.claude profile, workspaces, and helpers to resolve,
2
- // generate and run the guest against it.
3
- import fs from 'node:fs';
1
+ // A fake "host PC" inside the temp folder: Node, claude.exe, a ~/.claude profile, workspaces, and helpers to resolve a
2
+ // run, generate the sandbox for it, run the guest against it and start the CLI. Shared by all suites.
4
3
  import path from 'node:path';
5
- import { spawnSync } from 'node:child_process';
4
+ import { spawn } from 'node:child_process';
6
5
  import { DEFAULTS } from '../core/config.js';
7
6
  import { generateSandbox } from '../host/generate.js';
8
- import { ensureDir } from '../core/util.js';
9
7
  import { resolveRun } from '../core/workspace.js';
10
8
  export const HOST_NODE_VERSION = 'v22.3.0';
9
+ /** Colour codes are removed from captured output, so tests can match plain text. */
10
+ export const stripAnsi = (text) => text.replace(/\x1b\[[0-9;]*m/g, '');
11
+ /** Runs `node <args>` and collects stdout + stderr (colours removed). Never throws; a hung process is killed at the timeout. */
12
+ export const runNode = (args, options = {}) => new Promise((resolve) => {
13
+ const child = spawn(process.execPath, args, { cwd: options.cwd, env: options.env ?? process.env, windowsHide: true });
14
+ let out = '';
15
+ let timedOut = false;
16
+ const timer = setTimeout(() => { timedOut = true; child.kill(); }, options.timeoutMs ?? 120000);
17
+ child.stdout.on('data', (c) => { out += c.toString(); });
18
+ child.stderr.on('data', (c) => { out += c.toString(); });
19
+ child.on('error', (e) => { out += String(e); });
20
+ child.on('close', (status) => { clearTimeout(timer); resolve({ status: timedOut ? null : status, out: stripAnsi(out), timedOut }); });
21
+ });
11
22
  export const createFixtures = (t) => {
12
- const { temp, write } = t;
13
- const at = (...parts) => path.join(temp, ...parts);
23
+ const { at, write } = t;
24
+ // The PC: Node with npm, Claude Code, a ~/.claude profile (with a login that must never be copied) and ~/.claude.json.
14
25
  const hostNode = at('host-node');
15
26
  write(path.join(hostNode, 'node.exe'), 'x');
16
27
  write(path.join(hostNode, 'node_modules', 'npm', 'bin', 'npm-cli.js'), '');
@@ -22,16 +33,18 @@ export const createFixtures = (t) => {
22
33
  write(path.join(hostProfile, 'commands', 'review.md'), 'review cmd');
23
34
  write(path.join(hostProfile, 'plugins', 'installed_plugins.json'), JSON.stringify({ path: hostProfile + '\\plugins\\cache\\claude-mem' }));
24
35
  write(path.join(hostProfile, '.credentials.json'), '{ "token": "secret" }');
25
- const hostGlobalState = write(at('host-claude.json'), JSON.stringify({ hasCompletedOnboarding: true, mcpServers: { github: { command: 'npx' } } }));
36
+ const hostGlobalState = t.writeJson(at('host-claude.json'), { hasCompletedOnboarding: true, mcpServers: { github: { command: 'npx' } } });
37
+ // The install folder, two workspaces (B has a setup script), a folder to share, the storage root, and tools "from this PC".
26
38
  const install = at('install');
27
- ensureDir(path.join(install, 'sub'));
28
- const workA = at('work-a');
39
+ t.dir('install', 'sub');
40
+ const workA = t.dir('work-a');
29
41
  const workB = at('work-b');
30
- const shared = at('shared');
31
- for (const d of [workA, workB, shared, path.join(workB, 'assets'), at('storage')])
32
- ensureDir(d);
42
+ t.dir('work-b', 'assets');
43
+ const shared = t.dir('shared');
44
+ t.dir('storage');
33
45
  write(path.join(workB, 'setup.mjs'), "import fs from 'node:fs'; import path from 'node:path'; fs.writeFileSync(path.join(process.env.AGENT_DATA, 'hook-ran.txt'), process.env.AGENT_WORKSPACE);\n");
34
46
  write(at('host-tools', 'agents', 'claude', 'claude.exe'), 'fake-claude');
47
+ /** The default config, pointed at the fake PC. `copyFromHost = false` makes the sandbox download everything instead. */
35
48
  const makeConfig = (copyFromHost = true) => {
36
49
  const c = structuredClone(DEFAULTS);
37
50
  c.storage.root = at('storage');
@@ -47,17 +60,23 @@ export const createFixtures = (t) => {
47
60
  };
48
61
  const baseConfig = makeConfig();
49
62
  const probe = { nodeDir: hostNode, harnessExe: { dir: hostClaude, file: 'claude.exe' }, gitDir: null, nvmDir: null, hostNodeVersion: HOST_NODE_VERSION };
50
- const run = (over = {}, config = baseConfig) => resolveRun({ packageRoot: install, loaded: { config, configPath: null, base: temp }, cwd: workA, noAgent: false, noLocalConfig: false, trustConfig: false, scripts: [], ...over }, HOST_NODE_VERSION);
51
- const generate = (r, args = [], p = probe) => generateSandbox({ packageRoot: t.packageRoot, run: r, harnessArgs: args, probe: p });
63
+ /** Resolves a run for `workA` (or whatever `over` says). */
64
+ const run = (over = {}, config = baseConfig) => resolveRun({ packageRoot: install, loaded: { config, configPath: null, base: t.temp }, cwd: workA, noAgent: false, noLocalConfig: false, trustConfig: false, scripts: [], ...over }, HOST_NODE_VERSION);
65
+ const generate = (r, args = [], p = probe) =>
66
+ // A fixed "this PC has" list, so "auto" toolchains decide the same on every machine the tests run on.
67
+ generateSandbox({ packageRoot: t.packageRoot, run: r, harnessArgs: args, probe: p, pcLanguages: ['python', 'rust', 'cpp', 'dotnet'] });
68
+ /** Resolves a run and generates its sandbox (.wsb + launch folder + session.json) in one step. */
69
+ const launch = async (over = {}, options = {}) => generate(await run(over, options.config ?? baseConfig), options.args, options.probe);
70
+ /** Writes a project's `.sandbox-config.json` (objects are serialised; strings are written as they are). */
52
71
  const writeLocal = (dir, content) => { write(path.join(dir, '.sandbox-config.json'), typeof content === 'string' ? content : JSON.stringify(content)); };
72
+ /** The four folders history sync works with, for a workspace on the PC and its place in the sandbox. */
73
+ const historySides = (name, hostWorkspace, guestWorkspace) => ({ hostClaudeDir: at(name + '-host'), guestClaudeDir: at(name + '-guest'), hostWorkspace, guestWorkspace });
53
74
  /** Runs the guest's bundled main.js in dry-run mode against fake sandbox folders. */
54
- const runGuest = (launchPath, data, workspace, extra = {}) => {
55
- const r = spawnSync(process.execPath, [path.join(launchPath, 'runtime', 'main.js')], {
56
- encoding: 'utf8',
57
- env: { ...process.env, AGENT_LAUNCH_DIR: launchPath, AGENT_DATA_DIR: data, AGENT_HOST_TOOLS_DIR: at('host-tools'), AGENT_WORKSPACE: workspace, AGENT_LOCAL_DIR: at('local'), AGENT_MANAGED_DIR: at('managed'), AGENT_DRY_RUN: '1', ...extra },
58
- });
59
- return { status: r.status, out: (r.stdout ?? '') + (r.stderr ?? '') };
60
- };
61
- const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
62
- return { at, hostNode, hostClaude, hostProfile, hostGlobalState, install, workA, workB, shared, makeConfig, baseConfig, probe, run, generate, writeLocal, runGuest, readJson };
75
+ const runGuest = async (launchPath, data, workspace, extra = {}) => runNode([path.join(launchPath, 'runtime', 'main.js')], {
76
+ env: { ...process.env, AGENT_LAUNCH_DIR: launchPath, AGENT_DATA_DIR: data, AGENT_HOST_TOOLS_DIR: at('host-tools'), AGENT_WORKSPACE: workspace, AGENT_LOCAL_DIR: at('local'), AGENT_MANAGED_DIR: at('managed'), AGENT_DRY_RUN: '1', ...extra },
77
+ });
78
+ /** Runs a built command (dist/bin/<name>.js) as its own process. */
79
+ const runBin = (name, args, options = {}) => runNode([path.join(t.packageRoot, 'dist', 'bin', name + '.js'), ...args], options);
80
+ const readJson = t.readJson;
81
+ return { at, hostNode, hostClaude, hostProfile, hostGlobalState, install, workA, workB, shared, makeConfig, baseConfig, probe, run, generate, launch, writeLocal, historySides, runGuest, runBin, readJson };
63
82
  };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const generateSuite: Suite;
@@ -0,0 +1,68 @@
1
+ // What the host generates for a sandbox: the .wsb file, the launch folder (runtime, C# sources, boot scripts),
2
+ // session.json, the staged Claude profile, and the values read from this PC (git identity, free RAM).
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { LocalOverrides } from '../core/schema.js';
6
+ import { logonEnv } from '../host/generate.js';
7
+ import { gitIdentity } from '../host/launch/stage.js';
8
+ import { buildWsb, resolveMemoryMb } from '../host/launch/wsb.js';
9
+ export const generateSuite = {
10
+ name: 'generate',
11
+ title: 'generated .wsb, launch folder and session.json',
12
+ run: async (t, f) => {
13
+ t.section('the .wsb file');
14
+ const gen = await f.launch({}, { args: ['--version'], probe: { ...f.probe, gitDir: f.shared, nvmDir: f.workB } });
15
+ const wsb = fs.readFileSync(gen.wsbPath, 'utf8');
16
+ t.includes(wsb, '<MemoryInMB>8192</MemoryInMB>', 'wsb: the configured memory');
17
+ t.includes(wsb, '<SandboxFolder>C:\\Workspace\\work-a</SandboxFolder>', 'wsb: the workspace is mapped to its guest folder');
18
+ t.includes(wsb, 'C:\\HostTools\\git', 'wsb: this PC\'s git is mapped');
19
+ t.includes(wsb, 'C:\\HostTools\\nvm', 'wsb: this PC\'s nvm is mapped');
20
+ t.excludes(wsb, 'Missing', 'wsb: an optional mapping whose folder is missing is skipped');
21
+ t.includes(wsb, 'logon.cmd</Command>', 'wsb: the logon command');
22
+ t.includes(wsb, '<vGPU>Disable</vGPU>', 'wsb: vGPU is off');
23
+ t.includes(buildWsb(f.baseConfig, 4096, [{ host: 'C:\\a&b', guest: 'C:\\X', readOnly: true }]), 'C:\\a&amp;b', 'wsb values are XML-escaped');
24
+ t.section('the launch folder');
25
+ const expectedFiles = ['package.json', 'runtime/main.js', 'runtime/daemon.js', 'runtime/view.js', 'runtime/gitguard.js', 'sandbox/bootstrap.ps1', 'sandbox/logon.cmd', 'sandbox/trust.cmd', 'sandbox/ptyhost.cs', 'sandbox/gitshim.cs', 'sandbox/cpucap.ps1', 'sandbox/cpucap.cs', 'sandbox/pagefile.ps1', 'sandbox/pagefile.cs', 'sandbox/memlog.ps1'];
26
+ const missing = expectedFiles.filter((file) => !fs.existsSync(path.join(gen.launchPath, file)));
27
+ t.equal(missing, [], 'the launch folder has the runtime, the C# sources and the boot scripts');
28
+ t.equal(fs.readdirSync(path.join(gen.launchPath, 'runtime')).length, 6, 'the runtime is 6 self-contained files');
29
+ const logon = fs.readFileSync(path.join(gen.launchPath, 'sandbox', 'logon.cmd'), 'utf8');
30
+ t.check(logon.indexOf('trust.cmd') < logon.indexOf('wt.exe'), 'logon.cmd applies the sandbox trust settings before opening Windows Terminal');
31
+ t.includes(logon, 'tools\\terminal\\wt.exe', 'logon.cmd opens Windows Terminal from the tools folder');
32
+ t.includes(logon, '-NoExit', 'logon.cmd falls back to a PowerShell window');
33
+ t.check(logon.indexOf('env.cmd') > 0 && logon.indexOf('env.cmd') < logon.indexOf('wt.exe'), 'logon.cmd sets AGENT_WORKSPACE (env.cmd) before Windows Terminal starts, so new tabs can open in it');
34
+ t.includes(fs.readFileSync(path.join(gen.launchPath, 'env.cmd'), 'utf8'), 'AGENT_WORKSPACE=', 'env.cmd names the workspace');
35
+ t.equal(logonEnv('C:\\Workspace\\100%\\x'), '@set "AGENT_WORKSPACE=C:\\Workspace\\100%%\\x"\r\n', 'a % in the workspace path is escaped for cmd');
36
+ t.section('session.json');
37
+ const session = f.readJson(gen.sessionPath);
38
+ t.equal({
39
+ launchId: session.launchId === gen.launchId,
40
+ workspace: session.workspace,
41
+ workspaceHost: session.workspaceHost,
42
+ firstArgument: session.harnessArguments[0],
43
+ gitFromHost: Boolean(session.hostTools.git),
44
+ nvmFromHost: Boolean(session.hostTools.nvm),
45
+ nodeSpec: session.versions.node.spec,
46
+ gitAccess: session.provisioning.git.access,
47
+ }, { launchId: true, workspace: 'C:\\Workspace\\work-a', workspaceHost: f.workA, firstArgument: '--version', gitFromHost: true, nvmFromHost: true, nodeSpec: '22.3.0', gitAccess: 'full' }, 'session.json carries the id, paths, agent arguments, host tools, versions and git access');
48
+ t.matches(session.hostTools.nodeVersion ?? '', /^\d+\.\d+\.\d+$/, 'session.json: the host Node version is a plain version');
49
+ t.equal(session.cpuLimitPercent, 80, 'session.json carries the CPU cap for cpucap.ps1 (default 80 %)');
50
+ t.equal(session.pageFileMb, 16384, 'session.json carries the paging file limit for pagefile.ps1 (default 16 GB)');
51
+ t.check(!LocalOverrides.safeParse({ sandbox: { cpuLimit: 'none' } }).success, "a project's .sandbox-config.json cannot lift your CPU cap (sandbox.cpuLimit is yours only)");
52
+ t.section('what is copied from this PC');
53
+ const staged = path.join(gen.launchPath, 'claude-profile');
54
+ const stagedMissing = ['settings.json', 'CLAUDE.md', '.claude.json', 'plugins'].filter((name) => !fs.existsSync(path.join(staged, name)));
55
+ t.equal(stagedMissing, [], 'Claude settings, instructions, global state and plugins are staged');
56
+ t.check(!fs.existsSync(path.join(staged, '.credentials.json')), 'the Claude login is not staged');
57
+ const gitconfig = '[user]\n\tname = Josh\n\temail = "j@x.io"\n[credential]\n\thelper = store\n[url "https://TOKEN@github.com/"]\n\tinsteadOf = https://github.com/\n';
58
+ t.equal(gitIdentity(gitconfig), '[user]\n\tname = Josh\n\temail = j@x.io\n', 'only name/email from ~/.gitconfig reach the sandbox (no credential helpers or token URLs)');
59
+ t.equal(gitIdentity('[core]\n\tname = x\n'), '', 'a name outside [user] is not an identity');
60
+ const auto = { ...f.baseConfig.sandbox, memoryMb: 'auto' };
61
+ const MB = 1048576;
62
+ const pcRam = 64 * 1024 * MB;
63
+ t.equal(resolveMemoryMb(auto, 40000 * MB, pcRam), 31744, "memory 'auto' follows free RAM minus the reserve");
64
+ await t.throws(() => resolveMemoryMb(auto, 9000 * MB, pcRam), 'Not enough free RAM', "memory 'auto' refuses when too little RAM is free");
65
+ t.equal(resolveMemoryMb({ ...auto, maxMemoryPercent: 80 }, 30000 * MB, 32 * 1024 * MB), 21504, "memory 'auto' never takes more than maxMemoryPercent of the PC's RAM");
66
+ t.equal(resolveMemoryMb({ ...auto, maxMemoryPercent: 50 }, 60000 * MB, pcRam), 32768, "memory 'auto' is capped at half the PC's RAM with maxMemoryPercent 50 (free RAM and maxMemoryMb still apply)");
67
+ },
68
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const gitAccessSuite: Suite;
@@ -0,0 +1,26 @@
1
+ // Git access modes ("full", "readonly", "none"): which git commands the guard allows and the matching Claude deny rules.
2
+ import { gitDecision } from '../guest/gitguard.js';
3
+ import { gitDenyRules } from '../guest/gitrules.js';
4
+ export const gitAccessSuite = {
5
+ name: 'git-access',
6
+ title: 'git access modes and Claude deny rules',
7
+ run: (t) => {
8
+ t.section('readonly');
9
+ const allowed = (args, mode = 'readonly') => gitDecision(args, mode).allow;
10
+ const reading = ['status', 'log --oneline', 'diff HEAD~1', 'show HEAD', 'blame a.ts', 'branch', 'branch -a', 'branch --contains HEAD', 'tag', 'tag -l v*', 'remote -v', 'config --get user.name', 'stash list', '-C sub status', '--no-pager log'];
11
+ t.equal(reading.filter((command) => !allowed(command.split(' '))), [], 'readonly: reading commands are allowed');
12
+ const writing = ['commit -m x', 'push', 'add .', 'checkout main', 'switch -c x', 'reset --hard', 'stash', 'stash pop', 'branch new', 'branch -D x', 'tag v1', 'merge x', 'rebase main', 'fetch', 'pull', 'config user.name x', 'remote add o u', '-c core.hooksPath=x status', '--git-dir=x status', 'clean -fd', 'apply p.diff'];
13
+ t.equal(writing.filter((command) => allowed(command.split(' '))), [], 'readonly: anything that writes (or overrides config) is refused');
14
+ t.section('none and full');
15
+ t.check(!allowed(['status'], 'none') && !allowed(['--version'], 'none'), 'none: every git call is refused');
16
+ t.check(allowed(['commit'], 'full'), 'full: everything is allowed');
17
+ t.section('Claude managed deny rules');
18
+ const readonlyRules = gitDenyRules('readonly');
19
+ const noneRules = gitDenyRules('none');
20
+ t.equal(gitDenyRules('full').length, 0, 'full: no deny rules');
21
+ t.includes(readonlyRules.join('\n'), 'Bash(git push:*)', 'readonly: pushing is denied');
22
+ t.check(!readonlyRules.some((r) => r.startsWith('Read(')), 'readonly: reading .git is not denied');
23
+ t.check(noneRules.includes('Bash(git:*)'), 'none: every git command is denied');
24
+ t.check(noneRules.includes('Read(**/.git/**)'), 'none: reading .git is denied too');
25
+ },
26
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const guestBootSuite: Suite;
@@ -0,0 +1,76 @@
1
+ // The guest's boot script (runtime/main.js) run in dry-run mode against fake sandbox folders: what it provisions,
2
+ // what it copies in, and what it refuses to.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { readJsonFile } from '../shared/rundir.js';
6
+ export const guestBootSuite = {
7
+ name: 'guest-boot',
8
+ title: 'guest boot in dry-run mode (provisioning, Claude state, git policy)',
9
+ run: async (t, f) => {
10
+ const gen = await f.launch({}, { args: ['--version'], probe: { ...f.probe, gitDir: f.shared, nvmDir: f.workB } });
11
+ t.section('first boot');
12
+ const data1 = t.at('data1');
13
+ const boot = await f.runGuest(gen.launchPath, data1, f.workA, { AGENT_TEST_EXEMPT: '172.28.0.1' });
14
+ t.check(boot.status === 0, 'the guest boots and exits cleanly', 'exit code ' + boot.status + '\n' + boot.out);
15
+ t.includes(boot.out, 'would run:', 'the guest starts the agent...');
16
+ t.includes(boot.out, '--version', '... with its arguments...');
17
+ t.includes(boot.out, 'cwd=' + f.workA, '... in the workspace');
18
+ for (const step of ['would provision nvm and Node 22.3.0 (from this PC)', 'would provision Python 3', 'would provision MinGW-w64 GCC', 'would provision Rust', '.NET SDK', 'would provision bun', 'would provision uv', 'would provision Git for Windows from this PC']) {
19
+ t.includes(boot.out, step, 'the guest provisions: ' + step);
20
+ }
21
+ t.includes(boot.out, 'AgentSandbox-Block-Private', 'the firewall plan blocks the LAN...');
22
+ t.includes(boot.out, 'remoteport=1-52,54-65535', '... but keeps DNS to the gateway');
23
+ t.equal(readJsonFile(path.join(data1, 'run', 'sandbox.json'))?.phase, 'ready', 'the guest reports "ready" in run/sandbox.json');
24
+ t.section('Claude state copied in');
25
+ const claudeState = path.join(data1, 'state', 'claude');
26
+ t.includes(fs.readFileSync(path.join(claudeState, 'settings.json'), 'utf8'), 'dark', 'the guest applied this PC\'s Claude settings');
27
+ t.check(fs.existsSync(path.join(claudeState, 'commands', 'review.md')), 'custom commands are copied');
28
+ t.check(!fs.existsSync(path.join(claudeState, '.credentials.json')), 'the login is not copied');
29
+ const plugins = fs.readFileSync(path.join(claudeState, 'plugins', 'installed_plugins.json'), 'utf8');
30
+ t.includes(plugins, JSON.stringify(claudeState).slice(1, -1), 'plugin manifests are re-pointed at the sandbox Claude folder');
31
+ const guestState = f.readJson(path.join(claudeState, '.claude.json'));
32
+ t.equal(guestState.mcpServers?.github?.command, 'npx', 'MCP servers from this PC are in the sandbox');
33
+ const manifest = fs.readFileSync(f.at('managed', 'CLAUDE.md'), 'utf8');
34
+ t.includes(manifest, 'DISPOSABLE Windows Sandbox', 'the agent manifest goes to the managed CLAUDE.md...');
35
+ t.includes(manifest, f.workA, '... naming the workspace');
36
+ t.check(manifest.split('\n').length < 16, 'the managed manifest stays tiny', manifest.split('\n').length + ' lines');
37
+ t.equal(fs.readFileSync(path.join(claudeState, 'CLAUDE.md'), 'utf8'), 'my rules', 'the user\'s own CLAUDE.md is untouched');
38
+ t.check(fs.existsSync(path.join(data1, 'SANDBOX.md')), 'SANDBOX.md is written to the data folder');
39
+ t.check(!fs.existsSync(f.at('managed', 'managed-settings.json')), 'git access full: no managed deny rules');
40
+ t.section('second boot');
41
+ // The host changed its MCP servers: the sandbox keeps its own login and .claude.json but gets the new servers.
42
+ fs.writeFileSync(path.join(claudeState, '.claude.json'), JSON.stringify({ ...guestState, oauthAccount: { emailAddress: 'sandbox@x' }, mcpServers: { local: { command: 'x' } } }));
43
+ t.writeJson(path.join(gen.launchPath, 'claude-profile', '.claude.json'), { mcpServers: { github: { command: 'npx2' } }, projects: { [f.workA.replace(/\\/g, '/')]: { mcpServers: { db: { command: 'pg' } } } } });
44
+ await f.runGuest(gen.launchPath, data1, f.workA);
45
+ const merged = f.readJson(path.join(claudeState, '.claude.json'));
46
+ t.equal(merged.oauthAccount.emailAddress, 'sandbox@x', 'MCP re-sync leaves the sandbox login alone');
47
+ t.equal(merged.mcpServers.github.command, 'npx2', 'MCP re-sync updates a server changed on this PC');
48
+ t.equal(merged.mcpServers.local.command, 'x', 'MCP re-sync keeps a server added in the sandbox');
49
+ const projectServers = Object.values(merged.projects);
50
+ t.check(projectServers.some((p) => p.mcpServers?.db), 'MCP re-sync brings in project-scope servers', JSON.stringify(merged.projects));
51
+ t.section('other modes');
52
+ const readonlyConfig = f.makeConfig();
53
+ readonlyConfig.provisioning.git.access = 'readonly';
54
+ const genRo = await f.launch({}, { config: readonlyConfig });
55
+ await f.runGuest(genRo.launchPath, t.at('data-ro'), f.workA, { AGENT_MANAGED_DIR: t.at('managed-ro') });
56
+ const deny = f.readJson(t.at('managed-ro', 'managed-settings.json')).permissions.deny;
57
+ t.check(deny.includes('Bash(git commit:*)') && deny.includes('PowerShell(git push:*)'), 'git readonly: Claude managed settings deny writing git commands', JSON.stringify(deny));
58
+ t.check(deny.includes('Edit(**/.git/**)'), 'git readonly: .git edits are denied');
59
+ t.check(!deny.includes('Bash(git status:*)'), 'git readonly: reading git commands stay allowed');
60
+ f.writeLocal(f.workB, { scripts: ['./setup.mjs'] });
61
+ const genHook = await f.launch({ cwd: f.workB, harness: 'claude', trustConfig: true });
62
+ const hook = await f.runGuest(genHook.launchPath, t.at('data-hook'), f.workB);
63
+ t.check(hook.status === 0, 'a project with "scripts" boots', hook.out);
64
+ t.equal(fs.readFileSync(t.at('data-hook', 'hook-ran.txt'), 'utf8'), f.workB, 'local "scripts" run inside the sandbox with the workspace set');
65
+ const genInstall = await f.launch({}, { config: f.makeConfig(false), probe: { ...f.probe, nodeDir: null, harnessExe: null } });
66
+ const install = await f.runGuest(genInstall.launchPath, t.at('data3'), f.workA, { AGENT_HOST_TOOLS_DIR: t.at('empty') });
67
+ t.check(install.status === 0, 'install mode boots', install.out);
68
+ t.includes(install.out, 'npm-cli.js', 'install mode: npm is used...');
69
+ t.includes(install.out, '@anthropic-ai/claude-code', '... to install the agent');
70
+ t.excludes(fs.readFileSync(genInstall.wsbPath, 'utf8'), 'HostTools', 'install mode: nothing is mapped from this PC');
71
+ const genNoAgent = await f.launch({ noAgent: true });
72
+ const noAgent = await f.runGuest(genNoAgent.launchPath, t.at('data5'), f.workA);
73
+ t.check(noAgent.status === 0, '--no-agent boots', noAgent.out);
74
+ t.includes(noAgent.out, 'no agent requested', '--no-agent provisions without an agent');
75
+ },
76
+ };
@@ -1,14 +1,51 @@
1
+ /** Text a failure message must contain (string) or match (RegExp). */
2
+ export type Expected = string | RegExp;
1
3
  export interface Harness {
2
4
  packageRoot: string;
5
+ /** The folder every test works in; removed at the end. */
3
6
  temp: string;
4
- check: (ok: boolean, name: string) => void;
5
- rejects: (fn: () => unknown, needle: string) => Promise<boolean>;
6
- write: (file: string, content: string) => string;
7
- /** Polls `read` until it returns something truthy (or the timeout passes). */
8
- waitFor: <T>(read: () => T | null | undefined | false | Promise<T | null | undefined | false>, ms?: number) => Promise<T | null>;
9
7
  section: (title: string) => void;
8
+ /** Prints a SKIP line (not a check). */
9
+ skip: (what: string) => void;
10
+ /** The basic assertion. `detail` is printed under a failing check, so say what was found. */
11
+ check: (ok: boolean, name: string, detail?: string) => void;
12
+ /** Deep equality; on failure both values are shown. */
13
+ equal: <T>(actual: T, expected: T, name: string) => void;
14
+ /** `text` contains `needle`; on failure the text is shown. */
15
+ includes: (text: string, needle: string, name: string) => void;
16
+ /** `text` does not contain `needle`. */
17
+ excludes: (text: string, needle: string, name: string) => void;
18
+ /** `text` matches the pattern. */
19
+ matches: (text: string, pattern: RegExp, name: string) => void;
20
+ /** `fn` (sync or async) throws an error whose message (or `fix` hint) contains/matches `expected`. */
21
+ throws: (fn: () => unknown, expected: Expected, name: string) => Promise<void>;
22
+ /** Runs `fn` and returns the message of the error it throws ('' when it does not throw). */
23
+ thrownMessage: (fn: () => unknown) => Promise<string>;
24
+ /** Path below the temp folder (nothing is created). */
25
+ at: (...parts: string[]) => string;
26
+ /** Creates (if needed) and returns a folder below the temp folder. */
27
+ dir: (...parts: string[]) => string;
28
+ /** Writes a text file below anywhere in the temp folder (parents are created) and returns its path. */
29
+ write: (file: string, content: string) => string;
30
+ /** Writes `value` as JSON and returns the path. */
31
+ writeJson: (file: string, value: unknown) => string;
32
+ readJson: <T = any>(file: string) => T;
33
+ /**
34
+ * Sets a file's modified time to `secondsFromNow` (default: 10 s ahead). Sync decides what changed by comparing
35
+ * modified times, so tests make a change clearly newer this way instead of sleeping.
36
+ */
37
+ touch: (file: string, secondsFromNow?: number) => void;
38
+ /**
39
+ * Polls `read` every `everyMs` until it returns something truthy and returns that, or returns null after `ms`.
40
+ * Use this instead of fixed sleeps: it is fast when the machine is idle and patient when it is busy.
41
+ */
42
+ waitFor: <T>(read: () => T | null | undefined | false | Promise<T | null | undefined | false>, ms?: number, everyMs?: number) => Promise<T | null>;
10
43
  }
11
- export declare const createHarness: (packageRoot: string) => Harness & {
44
+ export interface RunningHarness extends Harness {
45
+ /** Number of checks that failed so far. */
12
46
  failures: () => number;
47
+ /** Number of checks that ran so far (passed or failed). */
48
+ checks: () => number;
13
49
  dispose: () => void;
14
- };
50
+ }
51
+ export declare const createHarness: (packageRoot: string) => RunningHarness;
@@ -1,36 +1,77 @@
1
- // Minimal test harness: PASS/FAIL lines, a temp folder that is always removed, small helpers.
1
+ // Minimal test harness: PASS/FAIL lines (a failure shows what was expected and what was found), assertion helpers,
2
+ // and a temp folder that is always removed. Every test writes only below `t.temp`.
2
3
  import fs from 'node:fs';
3
4
  import os from 'node:os';
4
5
  import path from 'node:path';
6
+ import { inspect, isDeepStrictEqual } from 'node:util';
5
7
  import { ensureDir, errMsg, sleep } from '../core/util.js';
8
+ const show = (value) => inspect(value, { depth: 6, breakLength: 110, maxStringLength: 600, maxArrayLength: 40 });
9
+ const indent = (text) => text.split('\n').map((line) => ' ' + line).join('\n');
10
+ const clip = (text, max = 800) => (text.length > max ? text.slice(0, max) + ' ... (' + text.length + ' characters)' : text);
6
11
  export const createHarness = (packageRoot) => {
7
- let failures = 0;
12
+ let failed = 0;
13
+ let total = 0;
8
14
  const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-sandbox-test-'));
9
- return {
10
- packageRoot,
11
- temp,
12
- failures: () => failures,
13
- check: (ok, name) => { console.log((ok ? '\x1b[32mPASS\x1b[0m ' : '\x1b[31mFAIL\x1b[0m ') + name); if (!ok)
14
- failures++; },
15
- rejects: async (fn, needle) => { try {
15
+ const check = (ok, name, detail) => {
16
+ total++;
17
+ console.log((ok ? '\x1b[32mPASS\x1b[0m ' : '\x1b[31mFAIL\x1b[0m ') + name);
18
+ if (ok)
19
+ return;
20
+ failed++;
21
+ if (detail)
22
+ console.log(indent(detail));
23
+ };
24
+ /** Runs `fn` once; the message includes the `fix` hint of a UserError. */
25
+ const attempt = async (fn) => {
26
+ try {
16
27
  await fn();
17
- return false;
28
+ return { threw: false };
18
29
  }
19
30
  catch (e) {
20
- return (errMsg(e) + ' ' + (e.fix ?? '')).includes(needle);
21
- } },
22
- write: (file, content) => { ensureDir(path.dirname(file)); fs.writeFileSync(file, content); return file; },
23
- waitFor: async (read, ms = 30000) => {
31
+ return { threw: true, message: (errMsg(e) + ' ' + (e.fix ?? '')).trim() };
32
+ }
33
+ };
34
+ const thrownMessage = async (fn) => {
35
+ const outcome = await attempt(fn);
36
+ return outcome.threw ? outcome.message : '';
37
+ };
38
+ const write = (file, content) => { ensureDir(path.dirname(file)); fs.writeFileSync(file, content); return file; };
39
+ return {
40
+ packageRoot,
41
+ temp,
42
+ failures: () => failed,
43
+ checks: () => total,
44
+ check,
45
+ equal: (actual, expected, name) => check(isDeepStrictEqual(actual, expected), name, 'expected: ' + show(expected) + '\nactual: ' + show(actual)),
46
+ includes: (text, needle, name) => check(text.includes(needle), name, 'expected to contain: ' + show(needle) + '\nactual: ' + show(clip(text))),
47
+ excludes: (text, needle, name) => check(!text.includes(needle), name, 'expected NOT to contain: ' + show(needle) + '\nactual: ' + show(clip(text))),
48
+ matches: (text, pattern, name) => check(pattern.test(text), name, 'expected to match: ' + String(pattern) + '\nactual: ' + show(clip(text))),
49
+ throws: async (fn, expected, name) => {
50
+ const outcome = await attempt(fn);
51
+ const message = outcome.threw ? outcome.message : '';
52
+ const ok = outcome.threw && (typeof expected === 'string' ? message.includes(expected) : expected.test(message));
53
+ check(ok, name, 'expected an error containing: ' + String(expected) + '\n' + (outcome.threw ? 'actual error: ' + show(clip(message)) : 'but nothing was thrown'));
54
+ },
55
+ thrownMessage,
56
+ section: (title) => console.log('\n\x1b[36m-- ' + title + '\x1b[0m'),
57
+ skip: (what) => console.log('SKIP ' + what),
58
+ at: (...parts) => path.join(temp, ...parts),
59
+ dir: (...parts) => { const p = path.join(temp, ...parts); ensureDir(p); return p; },
60
+ write,
61
+ writeJson: (file, value) => write(file, JSON.stringify(value)),
62
+ readJson: (file) => JSON.parse(fs.readFileSync(file, 'utf8')),
63
+ touch: (file, secondsFromNow = 10) => { const when = new Date(Date.now() + secondsFromNow * 1000); fs.utimesSync(file, when, when); },
64
+ waitFor: async (read, ms = 30000, everyMs = 100) => {
24
65
  const end = Date.now() + ms;
25
- while (Date.now() < end) {
26
- const v = await read();
27
- if (v)
28
- return v;
29
- await sleep(250);
66
+ for (;;) {
67
+ const value = await read();
68
+ if (value)
69
+ return value;
70
+ if (Date.now() >= end)
71
+ return null;
72
+ await sleep(everyMs);
30
73
  }
31
- return null;
32
74
  },
33
- section: (title) => console.log('\n\x1b[36m-- ' + title + '\x1b[0m'),
34
75
  dispose: () => fs.rmSync(temp, { recursive: true, force: true, maxRetries: 5, retryDelay: 300 }),
35
76
  };
36
77
  };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const hostSimSuite: Suite;