windows-development-sandbox 1.0.5 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (368) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -6
  3. package/config.example.json +17 -0
  4. package/dist/api.js +2 -1
  5. package/dist/cli/args.d.ts +20 -0
  6. package/dist/cli/args.js +35 -3
  7. package/dist/cli/colors.d.ts +24 -0
  8. package/dist/cli/colors.js +42 -0
  9. package/dist/cli/commands/agent-commands.d.ts +6 -0
  10. package/dist/cli/commands/agent-commands.js +61 -0
  11. package/dist/cli/commands/context.d.ts +2 -0
  12. package/dist/cli/commands/context.js +3 -0
  13. package/dist/cli/commands/data/clean.d.ts +15 -0
  14. package/dist/cli/commands/data/clean.js +101 -0
  15. package/dist/cli/commands/data/collect.d.ts +11 -0
  16. package/dist/cli/commands/data/collect.js +54 -0
  17. package/dist/cli/commands/data/index.d.ts +4 -0
  18. package/dist/cli/commands/data/index.js +45 -0
  19. package/dist/cli/commands/data/remove.d.ts +2 -0
  20. package/dist/cli/commands/data/remove.js +30 -0
  21. package/dist/cli/commands/index.d.ts +5 -3
  22. package/dist/cli/commands/index.js +5 -3
  23. package/dist/cli/commands/launch.d.ts +26 -2
  24. package/dist/cli/commands/launch.js +220 -39
  25. package/dist/cli/commands/maintenance.js +33 -4
  26. package/dist/cli/commands/project.d.ts +29 -0
  27. package/dist/cli/commands/project.js +63 -0
  28. package/dist/cli/commands/projects.d.ts +14 -0
  29. package/dist/cli/commands/projects.js +146 -0
  30. package/dist/cli/commands/repair/checks.d.ts +2 -0
  31. package/dist/cli/commands/{repair.js → repair/checks.js} +19 -181
  32. package/dist/cli/commands/{repair.d.ts → repair/finders.d.ts} +0 -29
  33. package/dist/cli/commands/repair/finders.js +108 -0
  34. package/dist/cli/commands/repair/index.d.ts +12 -0
  35. package/dist/cli/commands/repair/index.js +61 -0
  36. package/dist/cli/commands/repair/types.d.ts +26 -0
  37. package/dist/cli/commands/repair/types.js +3 -0
  38. package/dist/cli/commands/restart.d.ts +6 -0
  39. package/dist/cli/commands/restart.js +85 -0
  40. package/dist/cli/commands/sessions.d.ts +2 -1
  41. package/dist/cli/commands/sessions.js +189 -45
  42. package/dist/cli/commands/setup/checks.d.ts +13 -0
  43. package/dist/cli/commands/setup/checks.js +71 -0
  44. package/dist/cli/commands/{setup.d.ts → setup/facts.d.ts} +1 -15
  45. package/dist/cli/commands/setup/facts.js +60 -0
  46. package/dist/cli/commands/setup/index.d.ts +2 -0
  47. package/dist/cli/commands/setup/index.js +43 -0
  48. package/dist/cli/commands/user-config.d.ts +8 -0
  49. package/dist/cli/commands/user-config.js +41 -0
  50. package/dist/cli/history.d.ts +11 -0
  51. package/dist/cli/history.js +21 -0
  52. package/dist/cli/index.d.ts +7 -1
  53. package/dist/cli/index.js +38 -13
  54. package/dist/cli/manifest.d.ts +8 -0
  55. package/dist/cli/manifest.js +35 -15
  56. package/dist/cli/menu-multi.d.ts +12 -0
  57. package/dist/cli/menu-multi.js +82 -0
  58. package/dist/cli/menu.d.ts +5 -13
  59. package/dist/cli/menu.js +149 -46
  60. package/dist/cli/progress.d.ts +9 -0
  61. package/dist/cli/progress.js +49 -0
  62. package/dist/cli/settings/edit.d.ts +13 -0
  63. package/dist/cli/settings/edit.js +19 -0
  64. package/dist/cli/settings/index.d.ts +8 -0
  65. package/dist/cli/settings/index.js +131 -0
  66. package/dist/cli/settings/prompts.d.ts +7 -0
  67. package/dist/cli/settings/prompts.js +48 -0
  68. package/dist/cli/settings/schema.d.ts +33 -0
  69. package/dist/cli/settings/schema.js +29 -0
  70. package/dist/cli/validate.d.ts +17 -0
  71. package/dist/cli/validate.js +89 -0
  72. package/dist/core/config.js +4 -1
  73. package/dist/core/disk.d.ts +16 -0
  74. package/dist/core/disk.js +55 -0
  75. package/dist/core/local-config.d.ts +8 -0
  76. package/dist/core/local-config.js +18 -5
  77. package/dist/core/project-policy.d.ts +15 -0
  78. package/dist/core/project-policy.js +35 -0
  79. package/dist/core/prompt.d.ts +4 -0
  80. package/dist/core/prompt.js +15 -0
  81. package/dist/core/safe-paths.d.ts +4 -0
  82. package/dist/core/safe-paths.js +37 -0
  83. package/dist/core/schema/common.d.ts +17 -0
  84. package/dist/core/schema/common.js +15 -0
  85. package/dist/core/schema/config.d.ts +594 -0
  86. package/dist/core/schema/config.js +63 -0
  87. package/dist/core/schema/databases.d.ts +33 -0
  88. package/dist/core/schema/databases.js +35 -0
  89. package/dist/core/schema/harness.d.ts +12 -0
  90. package/dist/core/schema/harness.js +17 -0
  91. package/dist/core/schema/json-schema.d.ts +5 -0
  92. package/dist/core/schema/json-schema.js +25 -0
  93. package/dist/core/schema/profile.d.ts +27 -0
  94. package/dist/core/schema/profile.js +29 -0
  95. package/dist/core/schema/projects.d.ts +9 -0
  96. package/dist/core/schema/projects.js +11 -0
  97. package/dist/core/schema/sandbox.d.ts +50 -0
  98. package/dist/core/schema/sandbox.js +36 -0
  99. package/dist/core/schema/toolchains.d.ts +93 -0
  100. package/dist/core/schema/toolchains.js +83 -0
  101. package/dist/core/schema.d.ts +6 -604
  102. package/dist/core/schema.js +8 -264
  103. package/dist/core/types.d.ts +29 -1
  104. package/dist/core/util.d.ts +7 -0
  105. package/dist/core/util.js +12 -3
  106. package/dist/core/workspace.d.ts +0 -5
  107. package/dist/core/workspace.js +12 -46
  108. package/dist/guest/boot-env.d.ts +4 -0
  109. package/dist/guest/boot-env.js +25 -0
  110. package/dist/guest/cmdline.d.ts +12 -0
  111. package/dist/guest/cmdline.js +23 -0
  112. package/dist/guest/daemon/exec.d.ts +5 -0
  113. package/dist/guest/daemon/exec.js +32 -0
  114. package/dist/guest/daemon/project.d.ts +14 -0
  115. package/dist/guest/daemon/project.js +132 -0
  116. package/dist/guest/daemon/session.d.ts +13 -0
  117. package/dist/guest/daemon/session.js +139 -0
  118. package/dist/guest/daemon/shutdown.d.ts +1 -0
  119. package/dist/guest/daemon/shutdown.js +21 -0
  120. package/dist/guest/daemon/state.d.ts +17 -0
  121. package/dist/guest/daemon/state.js +31 -0
  122. package/dist/guest/daemon.js +8 -141
  123. package/dist/guest/db.js +18 -3
  124. package/dist/guest/env.d.ts +2 -0
  125. package/dist/guest/env.js +4 -2
  126. package/dist/guest/guide.d.ts +6 -2
  127. package/dist/guest/guide.js +5 -4
  128. package/dist/guest/helpers.d.ts +3 -0
  129. package/dist/guest/helpers.js +26 -0
  130. package/dist/guest/main.js +17 -7
  131. package/dist/guest/ports.d.ts +6 -0
  132. package/dist/guest/ports.js +22 -0
  133. package/dist/guest/project-context.d.ts +7 -0
  134. package/dist/guest/project-context.js +10 -0
  135. package/dist/guest/project-env.d.ts +20 -0
  136. package/dist/guest/project-env.js +20 -0
  137. package/dist/guest/project-status.d.ts +5 -0
  138. package/dist/guest/project-status.js +17 -0
  139. package/dist/guest/ptyhost.d.ts +0 -18
  140. package/dist/guest/ptyhost.js +5 -63
  141. package/dist/guest/setup/agents.d.ts +5 -2
  142. package/dist/guest/setup/agents.js +22 -8
  143. package/dist/guest/setup/claude.js +11 -3
  144. package/dist/guest/setup/environment.d.ts +13 -3
  145. package/dist/guest/setup/environment.js +14 -10
  146. package/dist/guest/setup/hooks.d.ts +9 -1
  147. package/dist/guest/setup/hooks.js +28 -14
  148. package/dist/guest/toolchains/catalogue.d.ts +4 -0
  149. package/dist/guest/toolchains/catalogue.js +2 -2
  150. package/dist/guest/toolchains/cli.js +25 -17
  151. package/dist/guest/toolchains/common.d.ts +18 -1
  152. package/dist/guest/toolchains/common.js +34 -11
  153. package/dist/guest/toolchains/databases/dynamodb.d.ts +8 -0
  154. package/dist/guest/toolchains/databases/dynamodb.js +52 -0
  155. package/dist/guest/toolchains/{databases.d.ts → databases/index.d.ts} +2 -3
  156. package/dist/guest/toolchains/databases/index.js +62 -0
  157. package/dist/guest/toolchains/databases/mongodb.d.ts +3 -0
  158. package/dist/guest/toolchains/databases/mongodb.js +31 -0
  159. package/dist/guest/toolchains/databases/mysql.d.ts +3 -0
  160. package/dist/guest/toolchains/databases/mysql.js +38 -0
  161. package/dist/guest/toolchains/databases/postgres.d.ts +5 -0
  162. package/dist/guest/toolchains/databases/postgres.js +38 -0
  163. package/dist/guest/toolchains/databases/redis.d.ts +3 -0
  164. package/dist/guest/toolchains/databases/redis.js +16 -0
  165. package/dist/guest/toolchains/databases/server.d.ts +14 -0
  166. package/dist/guest/toolchains/databases/server.js +52 -0
  167. package/dist/guest/toolchains/git.js +25 -16
  168. package/dist/guest/toolchains/go.js +1 -1
  169. package/dist/guest/toolchains/index.d.ts +13 -9
  170. package/dist/guest/toolchains/index.js +40 -82
  171. package/dist/guest/toolchains/java.js +5 -5
  172. package/dist/guest/toolchains/leftovers.d.ts +2 -0
  173. package/dist/guest/toolchains/leftovers.js +26 -0
  174. package/dist/guest/toolchains/managers.js +12 -10
  175. package/dist/guest/toolchains/{binstall.js → native/binstall.js} +6 -9
  176. package/dist/guest/toolchains/native/cpp.d.ts +4 -0
  177. package/dist/guest/toolchains/native/cpp.js +38 -0
  178. package/dist/guest/toolchains/native/dotnet.d.ts +4 -0
  179. package/dist/guest/toolchains/native/dotnet.js +103 -0
  180. package/dist/guest/toolchains/native/mesa.d.ts +7 -0
  181. package/dist/guest/toolchains/native/mesa.js +46 -0
  182. package/dist/guest/toolchains/native/msvc/environment.d.ts +16 -0
  183. package/dist/guest/toolchains/native/msvc/environment.js +57 -0
  184. package/dist/guest/toolchains/native/msvc/index.d.ts +6 -0
  185. package/dist/guest/toolchains/native/msvc/index.js +48 -0
  186. package/dist/guest/toolchains/native/msvc/packages.d.ts +6 -0
  187. package/dist/guest/toolchains/native/msvc/packages.js +156 -0
  188. package/dist/guest/toolchains/native/msvc/tree.d.ts +15 -0
  189. package/dist/guest/toolchains/native/msvc/tree.js +21 -0
  190. package/dist/guest/toolchains/native/rust.d.ts +3 -0
  191. package/dist/guest/toolchains/native/rust.js +45 -0
  192. package/dist/guest/toolchains/node.js +26 -6
  193. package/dist/guest/toolchains/plan.d.ts +22 -0
  194. package/dist/guest/toolchains/plan.js +96 -0
  195. package/dist/guest/toolchains/pwsh.js +1 -1
  196. package/dist/guest/toolchains/python.js +17 -10
  197. package/dist/guest/toolchains/runtimes.d.ts +2 -0
  198. package/dist/guest/toolchains/runtimes.js +6 -8
  199. package/dist/guest/toolchains/scripting/php.d.ts +3 -0
  200. package/dist/guest/toolchains/scripting/php.js +71 -0
  201. package/dist/guest/toolchains/scripting/ruby.d.ts +3 -0
  202. package/dist/guest/toolchains/scripting/ruby.js +47 -0
  203. package/dist/guest/toolchains/scripting/vcruntime.d.ts +7 -0
  204. package/dist/guest/toolchains/scripting/vcruntime.js +45 -0
  205. package/dist/guest/view.js +21 -1
  206. package/dist/host/accounts.js +14 -5
  207. package/dist/host/agent-commands.d.ts +25 -0
  208. package/dist/host/agent-commands.js +145 -0
  209. package/dist/host/attach.d.ts +17 -0
  210. package/dist/host/attach.js +69 -0
  211. package/dist/host/channel.d.ts +23 -22
  212. package/dist/host/channel.js +66 -84
  213. package/dist/host/claude-profile.js +7 -3
  214. package/dist/host/cleanup.js +2 -11
  215. package/dist/host/database-plan.d.ts +12 -0
  216. package/dist/host/database-plan.js +52 -0
  217. package/dist/host/generate.d.ts +4 -12
  218. package/dist/host/generate.js +48 -135
  219. package/dist/host/join.d.ts +28 -0
  220. package/dist/host/join.js +76 -0
  221. package/dist/host/launch/mappings.d.ts +3 -0
  222. package/dist/host/launch/mappings.js +50 -0
  223. package/dist/host/launch/projects.d.ts +15 -0
  224. package/dist/host/launch/projects.js +56 -0
  225. package/dist/host/launch/stage.d.ts +11 -0
  226. package/dist/host/launch/stage.js +66 -0
  227. package/dist/host/launch/wsb.d.ts +10 -0
  228. package/dist/host/launch/wsb.js +34 -0
  229. package/dist/host/pc-scan.d.ts +6 -0
  230. package/dist/host/pc-scan.js +37 -0
  231. package/dist/host/platform.d.ts +2 -0
  232. package/dist/host/platform.js +13 -1
  233. package/dist/host/project-scan.d.ts +1 -17
  234. package/dist/host/project-scan.js +2 -86
  235. package/dist/host/projects.d.ts +100 -0
  236. package/dist/host/projects.js +147 -0
  237. package/dist/host/repo-guard.d.ts +15 -2
  238. package/dist/host/repo-guard.js +103 -12
  239. package/dist/host/terminal.js +3 -0
  240. package/dist/host/test-guard.d.ts +6 -0
  241. package/dist/host/test-guard.js +15 -0
  242. package/dist/host/window.js +7 -3
  243. package/dist/host/wsb.d.ts +37 -0
  244. package/dist/host/wsb.js +101 -0
  245. package/dist/runtime/daemon.js +10997 -2794
  246. package/dist/runtime/db.js +6428 -464
  247. package/dist/runtime/main.js +7359 -1135
  248. package/dist/runtime/view.js +5922 -47
  249. package/dist/selftest/accounts.test.d.ts +2 -0
  250. package/dist/selftest/accounts.test.js +44 -0
  251. package/dist/selftest/agent-commands.test.d.ts +2 -0
  252. package/dist/selftest/agent-commands.test.js +47 -0
  253. package/dist/selftest/channel.test.d.ts +2 -0
  254. package/dist/selftest/channel.test.js +37 -0
  255. package/dist/selftest/cli.test.d.ts +2 -2
  256. package/dist/selftest/cli.test.js +115 -27
  257. package/dist/selftest/cmdline.test.d.ts +2 -0
  258. package/dist/selftest/cmdline.test.js +45 -0
  259. package/dist/selftest/config.test.d.ts +2 -3
  260. package/dist/selftest/config.test.js +36 -72
  261. package/dist/selftest/data.test.d.ts +2 -2
  262. package/dist/selftest/data.test.js +77 -70
  263. package/dist/selftest/detect.test.d.ts +2 -2
  264. package/dist/selftest/detect.test.js +54 -38
  265. package/dist/selftest/fixtures.d.ts +44 -4
  266. package/dist/selftest/fixtures.js +40 -23
  267. package/dist/selftest/generate.test.d.ts +2 -0
  268. package/dist/selftest/generate.test.js +63 -0
  269. package/dist/selftest/git-access.test.d.ts +2 -0
  270. package/dist/selftest/git-access.test.js +26 -0
  271. package/dist/selftest/guest-boot.test.d.ts +2 -0
  272. package/dist/selftest/guest-boot.test.js +76 -0
  273. package/dist/selftest/harness.d.ts +44 -7
  274. package/dist/selftest/harness.js +62 -21
  275. package/dist/selftest/host-sim.test.d.ts +2 -0
  276. package/dist/selftest/host-sim.test.js +93 -0
  277. package/dist/selftest/hostile-input.test.d.ts +2 -0
  278. package/dist/selftest/hostile-input.test.js +54 -0
  279. package/dist/selftest/index.d.ts +8 -1
  280. package/dist/selftest/index.js +134 -35
  281. package/dist/selftest/links.test.d.ts +2 -0
  282. package/dist/selftest/links.test.js +50 -0
  283. package/dist/selftest/live-daemon.test.d.ts +2 -0
  284. package/dist/selftest/live-daemon.test.js +91 -0
  285. package/dist/selftest/live-git.test.d.ts +2 -0
  286. package/dist/selftest/live-git.test.js +60 -0
  287. package/dist/selftest/live-projects.test.d.ts +2 -0
  288. package/dist/selftest/live-projects.test.js +127 -0
  289. package/dist/selftest/live.d.ts +65 -0
  290. package/dist/selftest/live.js +109 -0
  291. package/dist/selftest/local-config.test.d.ts +2 -0
  292. package/dist/selftest/local-config.test.js +107 -0
  293. package/dist/selftest/mirror.test.d.ts +2 -3
  294. package/dist/selftest/mirror.test.js +92 -65
  295. package/dist/selftest/network.test.d.ts +2 -0
  296. package/dist/selftest/network.test.js +28 -0
  297. package/dist/selftest/personalization.test.d.ts +2 -0
  298. package/dist/selftest/personalization.test.js +53 -0
  299. package/dist/selftest/projects.test.d.ts +2 -0
  300. package/dist/selftest/projects.test.js +254 -0
  301. package/dist/selftest/repair.test.d.ts +2 -0
  302. package/dist/selftest/repair.test.js +42 -0
  303. package/dist/selftest/repo-guard.test.d.ts +2 -0
  304. package/dist/selftest/repo-guard.test.js +63 -0
  305. package/dist/selftest/secrets.test.d.ts +2 -0
  306. package/dist/selftest/secrets.test.js +71 -0
  307. package/dist/selftest/setup.test.d.ts +2 -2
  308. package/dist/selftest/setup.test.js +45 -22
  309. package/dist/selftest/suite.d.ts +13 -0
  310. package/dist/selftest/suite.js +1 -0
  311. package/dist/selftest/sync.test.d.ts +2 -3
  312. package/dist/selftest/sync.test.js +90 -78
  313. package/dist/selftest/test-guard.test.d.ts +2 -0
  314. package/dist/selftest/test-guard.test.js +49 -0
  315. package/dist/selftest/toolchains.test.d.ts +2 -0
  316. package/dist/selftest/toolchains.test.js +103 -0
  317. package/dist/selftest/workspace.test.d.ts +2 -0
  318. package/dist/selftest/workspace.test.js +53 -0
  319. package/dist/shared/download.d.ts +3 -1
  320. package/dist/shared/download.js +19 -4
  321. package/dist/shared/paths.d.ts +5 -0
  322. package/dist/shared/paths.js +6 -0
  323. package/dist/shared/projects.d.ts +53 -0
  324. package/dist/shared/projects.js +67 -0
  325. package/dist/shared/rundir.d.ts +10 -1
  326. package/dist/shared/rundir.js +12 -4
  327. package/dist/shared/safe-remove.js +11 -0
  328. package/dist/shared/schema.d.ts +443 -0
  329. package/dist/shared/schema.js +41 -2
  330. package/dist/shared/test-guard.d.ts +4 -0
  331. package/dist/shared/test-guard.js +37 -0
  332. package/dist/sync/claude-mem.js +3 -1
  333. package/dist/sync/history.d.ts +23 -12
  334. package/dist/sync/history.js +52 -20
  335. package/dist/sync/watcher.d.ts +8 -0
  336. package/dist/sync/watcher.js +23 -4
  337. package/package.json +6 -6
  338. package/sandbox/bootstrap.ps1 +258 -258
  339. package/sandbox/cpucap.ps1 +51 -0
  340. package/sandbox/logon.cmd +18 -16
  341. package/sandbox/openssl.cs +42 -0
  342. package/sandbox/ptyhost.cs +15 -4
  343. package/sandbox/trust.cmd +35 -35
  344. package/schemas/config.schema.json +100 -1
  345. package/schemas/sandbox-config.schema.json +17 -1
  346. package/dist/cli/commands/data.d.ts +0 -46
  347. package/dist/cli/commands/data.js +0 -249
  348. package/dist/cli/commands/setup.js +0 -152
  349. package/dist/cli/settings.d.ts +0 -11
  350. package/dist/cli/settings.js +0 -156
  351. package/dist/guest/toolchains/databases.js +0 -226
  352. package/dist/guest/toolchains/msvc.d.ts +0 -10
  353. package/dist/guest/toolchains/msvc.js +0 -216
  354. package/dist/guest/toolchains/native.d.ts +0 -12
  355. package/dist/guest/toolchains/native.js +0 -220
  356. package/dist/guest/toolchains/scripting.d.ts +0 -10
  357. package/dist/guest/toolchains/scripting.js +0 -137
  358. package/dist/selftest/guest.test.d.ts +0 -3
  359. package/dist/selftest/guest.test.js +0 -61
  360. package/dist/selftest/helpers.test.d.ts +0 -2
  361. package/dist/selftest/helpers.test.js +0 -125
  362. package/dist/selftest/host.test.d.ts +0 -2
  363. package/dist/selftest/host.test.js +0 -71
  364. package/dist/selftest/live.test.d.ts +0 -7
  365. package/dist/selftest/live.test.js +0 -127
  366. package/dist/selftest/security.test.d.ts +0 -3
  367. package/dist/selftest/security.test.js +0 -143
  368. /package/dist/guest/toolchains/{binstall.d.ts → native/binstall.d.ts} +0 -0
@@ -1,61 +0,0 @@
1
- // Generated sandbox (.wsb + launch folder) and the guest boot in dry-run mode.
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { buildWsb, gitIdentity, resolveMemoryMb } from '../host/generate.js';
5
- import { readJsonFile } from '../shared/rundir.js';
6
- export const guestTests = async (t, f) => {
7
- const { check, rejects, at } = { ...t, at: f.at };
8
- t.section('generated sandbox');
9
- const rA = await f.run();
10
- const gen = f.generate(rA, ['--version'], { ...f.probe, gitDir: f.shared, nvmDir: f.workB });
11
- const wsb = fs.readFileSync(gen.wsbPath, 'utf8');
12
- check(wsb.includes('<MemoryInMB>8192</MemoryInMB>') && wsb.includes('<SandboxFolder>C:\\Workspace\\work-a</SandboxFolder>') && wsb.includes('C:\\HostTools\\git') && wsb.includes('C:\\HostTools\\nvm') && !wsb.includes('Missing') && wsb.includes('logon.cmd</Command>') && wsb.includes('<vGPU>Disable</vGPU>'), 'wsb: memory, workspace, host git/nvm, optional mapping skipped, logon command, vGPU off');
13
- const files = ['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'];
14
- const missing = files.filter((file) => !fs.existsSync(path.join(gen.launchPath, file)));
15
- check(missing.length === 0 && fs.readdirSync(path.join(gen.launchPath, 'runtime')).length === 5, 'launch folder has the bundled sandbox runtime (5 self-contained files), the C# sources and the boot scripts' + (missing.length ? ' (missing: ' + missing.join(', ') + ')' : ''));
16
- const logon = fs.readFileSync(path.join(gen.launchPath, 'sandbox', 'logon.cmd'), 'utf8');
17
- check(logon.indexOf('trust.cmd') < logon.indexOf('wt.exe') && logon.includes('tools\\terminal\\wt.exe') && logon.includes('-NoExit'), 'logon.cmd applies sandbox trust settings first, then opens Windows Terminal (PowerShell fallback)');
18
- const session = f.readJson(gen.sessionPath);
19
- check(session.launchId === gen.launchId && session.workspace === 'C:\\Workspace\\work-a' && session.workspaceHost === f.workA && session.harnessArguments[0] === '--version' && session.hostTools.git && session.hostTools.nvm && /^\d+\.\d+\.\d+$/.test(session.hostTools.nodeVersion ?? '') && session.versions.node.spec === '22.3.0' && session.provisioning.git.access === 'full', 'session.json carries ids, paths, versions, host tools and git access');
20
- const staged = path.join(gen.launchPath, 'claude-profile');
21
- check(['settings.json', 'CLAUDE.md', '.claude.json', 'plugins'].every((file) => fs.existsSync(path.join(staged, file))) && !fs.existsSync(path.join(staged, '.credentials.json')), 'Claude settings and plugins staged; the login is not');
22
- check(gitIdentity('[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') === '[user]\n\tname = Josh\n\temail = j@x.io\n' && gitIdentity('[core]\n\tname = x\n') === '', 'only name/email from ~/.gitconfig reach the sandbox (no credential helpers or token URLs)');
23
- check(buildWsb(f.baseConfig, 4096, [{ host: 'C:\\a&b', guest: 'C:\\X', readOnly: true }]).includes('C:\\a&amp;b'), 'wsb values are XML-escaped');
24
- check(resolveMemoryMb({ ...f.baseConfig.sandbox, memoryMb: 'auto' }, 40000 * 1048576) === 31744 && await rejects(() => resolveMemoryMb({ ...f.baseConfig.sandbox, memoryMb: 'auto' }, 9000 * 1048576), 'Not enough free RAM'), "memory 'auto' follows free RAM minus reserve");
25
- t.section('guest boot (dry run)');
26
- const data1 = at('data1');
27
- const r1 = f.runGuest(gen.launchPath, data1, f.workA, { AGENT_TEST_EXEMPT: '172.28.0.1' });
28
- check(r1.status === 0 && r1.out.includes('would run:') && r1.out.includes('--version') && r1.out.includes('cwd=' + f.workA), 'guest starts the agent with its arguments in the workspace' + (r1.status === 0 ? '' : '\n' + r1.out));
29
- check(['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'].every((s) => r1.out.includes(s)), 'guest provisions nvm/Node, Python, MinGW, Rust, .NET, Bun, uv, Git');
30
- const claudeState = path.join(data1, 'state', 'claude');
31
- check(fs.readFileSync(path.join(claudeState, 'settings.json'), 'utf8').includes('dark') && fs.existsSync(path.join(claudeState, 'commands', 'review.md')) && !fs.existsSync(path.join(claudeState, '.credentials.json')), 'guest applied host Claude settings (no login)');
32
- check(fs.readFileSync(path.join(claudeState, 'plugins', 'installed_plugins.json'), 'utf8').includes(JSON.stringify(claudeState).slice(1, -1)), 'plugin manifests are re-pointed at the sandbox Claude folder');
33
- const guestState = f.readJson(path.join(claudeState, '.claude.json'));
34
- check(guestState.mcpServers?.github?.command === 'npx', 'MCP servers from this PC are in the sandbox');
35
- const manifest = fs.readFileSync(at('managed', 'CLAUDE.md'), 'utf8');
36
- check(manifest.includes('DISPOSABLE Windows Sandbox') && manifest.includes(f.workA) && manifest.split('\n').length < 16 && fs.readFileSync(path.join(claudeState, 'CLAUDE.md'), 'utf8') === 'my rules' && fs.existsSync(path.join(data1, 'SANDBOX.md')), 'agent manifest goes to the managed CLAUDE.md (tiny); the user CLAUDE.md is untouched');
37
- check(!fs.existsSync(at('managed', 'managed-settings.json')), 'git access full: no managed deny rules');
38
- check(r1.out.includes('AgentSandbox-Block-Private') && r1.out.includes('remoteport=1-52,54-65535'), 'guest firewall plan blocks the LAN but keeps DNS to the gateway');
39
- check(readJsonFile(path.join(data1, 'run', 'sandbox.json'))?.phase === 'ready', 'guest reports "ready" in run/sandbox.json');
40
- // Second boot: host changed its MCP servers; the sandbox keeps its own .claude.json but gets the new servers.
41
- fs.writeFileSync(path.join(claudeState, '.claude.json'), JSON.stringify({ ...guestState, oauthAccount: { emailAddress: 'sandbox@x' }, mcpServers: { local: { command: 'x' } } }));
42
- fs.writeFileSync(path.join(gen.launchPath, 'claude-profile', '.claude.json'), JSON.stringify({ mcpServers: { github: { command: 'npx2' } }, projects: { [f.workA.replace(/\\/g, '/')]: { mcpServers: { db: { command: 'pg' } } } } }));
43
- f.runGuest(gen.launchPath, data1, f.workA);
44
- const merged = f.readJson(path.join(claudeState, '.claude.json'));
45
- check(merged.oauthAccount.emailAddress === 'sandbox@x' && merged.mcpServers.github.command === 'npx2' && merged.mcpServers.local.command === 'x' && Object.values(merged.projects).some((p) => p.mcpServers?.db), 'MCP servers re-sync every boot (user + project scope) without touching the sandbox login');
46
- const readonlyConfig = f.makeConfig();
47
- readonlyConfig.provisioning.git.access = 'readonly';
48
- const genRo = f.generate(await f.run({}, readonlyConfig));
49
- f.runGuest(genRo.launchPath, at('data-ro'), f.workA, { AGENT_MANAGED_DIR: at('managed-ro') });
50
- const policy = f.readJson(at('managed-ro', 'managed-settings.json'));
51
- check(policy.permissions.deny.includes('Bash(git commit:*)') && policy.permissions.deny.includes('PowerShell(git push:*)') && policy.permissions.deny.includes('Edit(**/.git/**)') && !policy.permissions.deny.includes('Bash(git status:*)'), 'git readonly: Claude managed settings deny writing git commands and .git edits');
52
- const genHook = f.generate(await f.run({ cwd: f.workB, harness: 'claude', trustConfig: true }));
53
- const rHook = f.runGuest(genHook.launchPath, at('data-hook'), f.workB);
54
- check(rHook.status === 0 && fs.readFileSync(at('data-hook', 'hook-ran.txt'), 'utf8') === f.workB, 'local "scripts" run inside the sandbox with the workspace set');
55
- const genInstall = f.generate(await f.run({}, f.makeConfig(false)), [], { ...f.probe, nodeDir: null, harnessExe: null });
56
- const r3 = f.runGuest(genInstall.launchPath, at('data3'), f.workA, { AGENT_HOST_TOOLS_DIR: at('empty') });
57
- check(r3.status === 0 && r3.out.includes('npm-cli.js') && r3.out.includes('@anthropic-ai/claude-code') && !fs.readFileSync(genInstall.wsbPath, 'utf8').includes('HostTools'), 'install mode: nothing mapped from this PC, agent installed with npm');
58
- const r5 = f.runGuest(f.generate(await f.run({ noAgent: true })).launchPath, at('data5'), f.workA);
59
- check(r5.status === 0 && r5.out.includes('no agent requested'), '--no-agent provisions without an agent');
60
- return gen.launchPath;
61
- };
@@ -1,2 +0,0 @@
1
- import type { Harness } from './harness.js';
2
- export declare const helperTests: (t: Harness) => void;
@@ -1,125 +0,0 @@
1
- // Small pure helpers: version picking, .reg generation, firewall maths, storage cleanup.
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { spawnSync } from 'node:child_process';
5
- import { pathToFileURL } from 'node:url';
6
- import { pruneStorage } from '../host/cleanup.js';
7
- import { toRegFile } from '../host/personalize.js';
8
- import { javaMajorFrom, pickGoRelease, pickNodeVersion, pickPackageVersion, pickSpreadVersions, projectUses, specFromVersionFile } from '../guest/toolchains/versions.js';
9
- import { parseReleaseTags } from '../shared/download.js';
10
- import { buildFirewallCommands, cidrToRange, ipv4DnsServers, parseDefaultGateways, parseDnsServers, subtractIps } from '../shared/netrules.js';
11
- import { ensureDir, isWin } from '../core/util.js';
12
- import { decidePwsh, pcHasPwsh, PWSH_MAX_BYTES, readPwshScripts, usesPwsh } from '../core/detect-pwsh.js';
13
- import { binstallAsset, CLI_TOOLS, decideCli, pwshAsset } from '../guest/toolchains/catalogue.js';
14
- import { PWSH_PROFILE_GUID, syncPwshProfile, withPwshProfile } from '../host/terminal.js';
15
- export const helperTests = (t) => {
16
- const { check, write } = t;
17
- t.section('versions, registry, firewall');
18
- const releases = [{ version: 'v23.1.0', lts: false }, { version: 'v22.11.0', lts: 'Jod' }, { version: 'v20.18.0', lts: 'Iron' }, { version: 'v20.9.0', lts: 'Iron' }];
19
- check(pickNodeVersion(releases, 'lts/*') === '22.11.0' && pickNodeVersion(releases, 'lts/iron') === '20.18.0' && pickNodeVersion(releases, '20') === '20.18.0' && pickNodeVersion(releases, 'v20.9.0') === '20.9.0' && pickNodeVersion(releases, 'node') === '23.1.0' && pickNodeVersion(releases, '99') === null, '.nvmrc specs resolve (lts/*, lts/<name>, major, exact, node)');
20
- const spread = pickSpreadVersions(['16.20.2', '18.19.0', '18.20.4', '20.18.0', '22.3.0', '22.11.0', '23.1.0', 'notes'], '22.11.0', 3);
21
- check(JSON.stringify(spread) === '["22.11.0","16.20.2","23.1.0"]' && pickSpreadVersions(['20.18.0'], '22.3.0', 3).join() === '22.3.0,20.18.0' && pickSpreadVersions(['18.20.4', '20.18.0'], '22.3.0', 1).join() === '22.3.0', 'host nvm versions: capped, current first, rest spread across other majors');
22
- const go = [{ version: 'go1.24rc1', stable: false, files: [] }, { version: 'go1.23.2', stable: true, files: [] }, { version: 'go1.23.1', stable: true, files: [] }, { version: 'go1.22.8', stable: true, files: [] }];
23
- check(pickGoRelease(go, 'stable')?.version === 'go1.23.2' && pickGoRelease(go, '1.22')?.version === 'go1.22.8' && pickGoRelease(go, 'go1.23.1')?.version === 'go1.23.1' && pickGoRelease(go, '1.30') === null, 'Go versions resolve (stable, minor prefix, exact; release candidates skipped)');
24
- check(javaMajorFrom('21\n') === '21' && javaMajorFrom('17.0.9') === '17' && javaMajorFrom('1.8') === '8' && javaMajorFrom('temurin-21.0.2') === '21' && javaMajorFrom('# sdkman\njava=21.0.2-tem\n', true) === '21' && specFromVersionFile('ruby-3.3.1\n') === '3.3.1', 'version files: .java-version, .sdkmanrc, .ruby-version');
25
- check(projectUses(['README.md', 'server/go.mod'], ['go.mod']) && projectUses(['App.csproj'], ['*.csproj']) && !projectUses(['notes.txt', 'docs/readme.md'], ['go.mod', '*.csproj']), 'enabled "auto": a language is used when its marker file is in the workspace or one folder down');
26
- check(pickPackageVersion(['3.9.13', '3.12.7', '3.12.10', '3.13.0-rc1', '3.13.1'], '3') === '3.13.1' && pickPackageVersion(['3.12.7', '3.12.10'], '3.12') === '3.12.10' && pickPackageVersion(['2.7.18'], '2.7') === '2.7.18', 'Python versions resolve to the newest stable match');
27
- const reg = toRegFile([{ key: 'Control Panel\\Desktop', name: 'WallpaperStyle', kind: 'String', value: '10' }, { key: 'X', name: 'D', kind: 'DWord', value: 4294967295 }, { key: 'X', name: 'B', kind: 'Binary', value: Buffer.from([1, 255]).toString('base64') }]);
28
- check(reg.includes('[HKEY_CURRENT_USER\\Control Panel\\Desktop]') && reg.includes('"WallpaperStyle"="10"') && reg.includes('"D"=dword:ffffffff') && reg.includes('"B"=hex:01,ff'), 'personalisation: registry values become a valid .reg file (HKCU only)');
29
- const [s172, e172] = cidrToRange('172.16.0.0/12');
30
- const gw = 172 * 16777216 + 28 * 65536 + 1;
31
- check(subtractIps([[s172, e172]], ['172.28.0.1']).every(([s, e]) => gw < s || gw > e) && !buildFirewallCommands(['8.8.8.8']).join().includes('8.8.8.8'), 'firewall maths: private block minus the gateway; public addresses ignored');
32
- check(parseDefaultGateways(' 0.0.0.0 0.0.0.0 172.28.80.1 172.28.85.200 5\n').join() === '172.28.80.1' && parseDnsServers(' DNS Servers . . . : 172.28.80.1\n 8.8.8.8\n NetBIOS : x\n').join() === '172.28.80.1,8.8.8.8', 'parses gateway and DNS servers');
33
- check(ipv4DnsServers(['172.28.80.1', '8.8.8.8:1053', '[::1]:53', 'fe80::1']).join() === '172.28.80.1,8.8.8.8', 'DNS servers from the resolver (any Windows language): IPv4 only, ports dropped');
34
- check(parseReleaseTags('<a href="/coreybutler/nvm-windows/releases/tag/1.2.2">x</a><a href="/coreybutler/nvm-windows/releases/tag/1.2.2">y</a><a href="/other/repo/releases/tag/9">z</a><a href="/coreybutler/nvm-windows/releases/tag/1.1.12">w</a>', 'coreybutler/nvm-windows').join() === '1.2.2,1.1.12', 'GitHub release list page: this repo\'s tags in order, once each');
35
- t.section('cmd quoting (npm agents)');
36
- // ptyhost.ts loads env.ts, which reads session.json: import it in a child with a throwaway launch/data folder.
37
- const quoteDir = path.join(t.temp, 'quote');
38
- write(path.join(quoteDir, 'launch', 'session.json'), '{}');
39
- const agentCmd = write(path.join(quoteDir, 'agent.cmd'), '@"' + process.execPath + '" -e "console.log(JSON.stringify(process.argv.slice(1)))" %*\r\n');
40
- // Odd quote counts flip cmd's quote state for every later argument: those come after them on purpose.
41
- const tricky = ['https://x?a=1&b=2', '50% off', 'a|b<c>d', 'x^y', 'say "hi" & go', 'plain', '%PATH%', 'a"b&c', 'q"x|y^z"w', 'e"5%', 'k&v', '100%'];
42
- const probe = spawnSync(process.execPath, ['--input-type=module', '-e', [
43
- 'const m = await import(process.argv[1]); const [cmd, tricky] = JSON.parse(process.argv[2]);',
44
- 'console.log(JSON.stringify({ normal: m.commandLine("C:\\\\W\\\\cmd.exe", ["/d", "/c", "C:\\\\a\\\\agent.cmd", "--model", "big one", "plain", ""]), direct: m.commandLine("C:\\\\a\\\\agent.exe", ["a&b", "50%"]),',
45
- ' url: m.cmdArgs(["https://x?a=1&b=2"]), pct: m.cmdArgs(["50%"]), live: m.commandLine("cmd.exe", ["/d", "/c", cmd, ...tricky]).slice(8) }));',
46
- ].join('\n'), pathToFileURL(path.join(t.packageRoot, 'dist', 'guest', 'ptyhost.js')).href, JSON.stringify([agentCmd, tricky])], {
47
- encoding: 'utf8', env: { ...process.env, AGENT_LAUNCH_DIR: path.join(quoteDir, 'launch'), AGENT_DATA_DIR: path.join(quoteDir, 'data') },
48
- });
49
- const quoting = (() => { try {
50
- return JSON.parse(probe.stdout);
51
- }
52
- catch {
53
- return {};
54
- } })();
55
- check(quoting.normal === 'C:\\W\\cmd.exe /d /c C:\\a\\agent.cmd --model "big one" plain ""' && quoting.direct === 'C:\\a\\agent.exe a&b 50%', 'command lines: ordinary arguments quoted as before; a program started directly gets & and % unchanged' + (quoting.normal ? '' : ' (' + (probe.stderr || probe.stdout).trim().slice(0, 300) + ')'));
56
- check(quoting.url === '"https://x?a=1&b=2"' && quoting.pct === '"50"%""', 'cmd /c arguments: & | < > ^ quoted, % split out so no %name% expands');
57
- // (Agents live under C:\AgentData\tools; a temp folder with spaces would also need cmd's quote rules, so skip there.)
58
- if (isWin && quoting.live && !/\s/.test(agentCmd)) {
59
- const echoed = spawnSync(process.env.ComSpec ?? 'cmd.exe', [quoting.live], { encoding: 'utf8', windowsVerbatimArguments: true, windowsHide: true });
60
- check(echoed.stdout.trim() === JSON.stringify(tricky), 'cmd /d /c <agent>.cmd passes &, |, <, >, ^, % and quotes through to the agent unchanged (got ' + echoed.stdout.trim() + ')');
61
- }
62
- t.section('cleanup');
63
- const store = path.join(t.temp, 'prune');
64
- const aged = (p, days) => { const d = new Date(Date.now() - days * 86400000); fs.utimesSync(p, d, d); };
65
- for (const [name, days] of [['old', 9], ['kept', 30], ['new', 1]]) {
66
- aged(write(path.join(store, 'launches', name, 'agent.wsb'), 'x'), days);
67
- aged(path.join(store, 'launches', name), days);
68
- }
69
- aged(write(path.join(store, 'data', 'd', 'logs', 'guest-old.log'), 'x'), 20);
70
- write(path.join(store, 'data', 'd', 'logs', 'guest-new.log'), 'x');
71
- ensureDir(path.join(store, 'data', 'd', 'tools', 'git.staging-ab12'));
72
- aged(path.join(store, 'data', 'd', 'tools', 'git.staging-ab12'), 2);
73
- ensureDir(path.join(store, 'data', 'd', 'tools', 'node'));
74
- const pruned = pruneStorage(store, { keep: ['kept'] });
75
- const left = (p) => fs.existsSync(path.join(store, p));
76
- check(pruned.launches === 1 && !left('launches/old') && left('launches/kept') && left('launches/new') && !left('data/d/logs/guest-old.log') && left('data/d/logs/guest-new.log') && !left('data/d/tools/git.staging-ab12') && left('data/d/tools/node'), 'cleanup: launches > 7 days, logs > 14 days and interrupted downloads go; the running launch and real tools stay');
77
- t.section('PowerShell 7, CLI catalogue, cargo-binstall');
78
- const pwshNames = ['PowerShell-7.6.6-win-x64.zip', 'PowerShell-7.6.6-win-arm64.zip', 'PowerShell-7.6.6-win-x64.msi', 'PowerShell-7.6.6-win-fxdependent.zip', 'PowerShell-7.6.6-win-x86.zip', 'PowerShell-7.6.6-win-x64.zip.sha256'];
79
- check(pwshNames.filter((n) => pwshAsset('x64').test(n)).join() === 'PowerShell-7.6.6-win-x64.zip' && pwshNames.filter((n) => pwshAsset('arm64').test(n)).join() === 'PowerShell-7.6.6-win-arm64.zip', 'PowerShell 7: the portable zip for the architecture (no MSI, fxdependent or x86 build)');
80
- const binstallNames = ['cargo-binstall-x86_64-pc-windows-msvc.zip', 'cargo-binstall-x86_64-pc-windows-msvc.full.zip', 'cargo-binstall-x86_64-pc-windows-msvc.zip.sig', 'cargo-binstall-aarch64-pc-windows-msvc.zip', 'cargo-binstall-x86_64-unknown-linux-musl.tgz'];
81
- check(binstallNames.filter((n) => binstallAsset('x64').test(n)).join() === 'cargo-binstall-x86_64-pc-windows-msvc.zip' && binstallNames.filter((n) => binstallAsset('arm64').test(n)).join() === 'cargo-binstall-aarch64-pc-windows-msvc.zip', 'cargo-binstall: the plain Windows zip for the architecture (not .full, not the signature)');
82
- const cliAsset = (name, arch, names) => names.filter((n) => CLI_TOOLS.find((c) => c.name === name).asset(arch).test(n)).join();
83
- const ghNames = ['gh_2.102.0_windows_amd64.zip', 'gh_2.102.0_windows_amd64.msi', 'gh_2.102.0_windows_386.zip', 'gh_2.102.0_windows_arm64.zip', 'gh_2.102.0_linux_amd64.tar.gz'];
84
- const zstdNames = ['zstd-v1.5.7-win64.zip', 'zstd-v1.5.7-win32.zip', 'zstd-v1.5.7.tar.gz'];
85
- check(cliAsset('gh', 'x64', ghNames) === 'gh_2.102.0_windows_amd64.zip' && cliAsset('gh', 'arm64', ghNames) === 'gh_2.102.0_windows_arm64.zip' && cliAsset('zstd', 'x64', zstdNames) === 'zstd-v1.5.7-win64.zip' && cliAsset('zstd', 'arm64', zstdNames) === 'zstd-v1.5.7-win64.zip' && CLI_TOOLS.every((c) => c.exe.endsWith('.exe')), 'CLI catalogue: gh and zstd release zips per architecture (zstd: win64 also on Arm)');
86
- const cli = (settings, gitAccess, hasGithubDir = false) => Object.entries(decideCli(settings, { gitAccess, hasGithubDir })).filter(([, d]) => d.on).map(([n]) => n).join();
87
- check(cli({ gh: true, zstd: true, openssl: true }, 'full') === 'gh,zstd,openssl' && cli({ gh: true, zstd: true }, 'readonly') === 'zstd' && cli({ gh: 'auto' }, 'none', true) === '' && decideCli({ gh: true }, { gitAccess: 'readonly', hasGithubDir: false }).gh.reason.includes('"full"'), 'CLI catalogue: gh only with git access "full" (it pushes through the API), whatever the config says');
88
- check(cli({ gh: 'auto', zstd: 'auto', openssl: 'auto' }, 'full', true) === 'gh,openssl' && cli({ gh: 'auto', zstd: 'auto' }, 'full', false) === '' && cli({}, 'full', true) === '', 'CLI catalogue: "auto" = gh with a .github folder, openssl always (a shim), zstd only when listed; unlisted tools stay off');
89
- const ps = (text, file = 'moh.ps1') => usesPwsh([{ path: file, text }]);
90
- check(ps('#Requires -Version 7.2\nparam()') === 'moh.ps1: #Requires -Version 7' && ps(' #requires -PSEdition Core') !== null && ps('#Requires -Modules Pester -Version 7') !== null && ps('<#\n.SYNOPSIS\n Usage: pwsh scripts/moh/build-openmohaa.ps1 -Release\n#>') !== null && ps('& pwsh.exe -NoProfile -File x.ps1') !== null && ps('C:\\Program Files\\PowerShell\\7\\pwsh.exe -c 1') !== null, 'pwsh markers: #Requires -Version 7+ / -PSEdition Core, and pwsh usage lines');
91
- check(ps('#Requires -Version 5.1') === null && ps('#Requires -PSEdition Desktop') === null && ps('# Works in Windows PowerShell and pwsh too.') === null && ps('$pwsh -x; Write-Host mypwsh -y') === null && ps('#Requires -Version 7', 'notes.txt') === null && ps('Write-Host hi') === null, 'pwsh markers: Windows PowerShell scripts, prose, variables and non-scripts do not count');
92
- const evilStart = Date.now();
93
- const evilText = (' #requires' + ' -version'.repeat(5000) + '\n' + 'xpwsh pwsh\t\t'.repeat(20000) + '\n' + ' '.repeat(100000) + 'pwsh\n' + 'pwsh\n'.repeat(50000)).repeat(3);
94
- const evil = usesPwsh(Array.from({ length: 100 }, (_, i) => ({ path: 'a' + i + '.ps1', text: evilText })));
95
- check(evil === null && Date.now() - evilStart < 2000, 'pwsh markers: hostile scripts (huge lines, endless repeats) are capped and scanned in linear time (' + (Date.now() - evilStart) + ' ms)');
96
- check(decidePwsh('auto', [], true, true).on && !decidePwsh('auto', [], false, true).on && !decidePwsh('auto', [], true, false).on && decidePwsh('auto', [{ path: 'x/run.ps1', text: '#Requires -PSEdition Core' }], false, false).reason === 'x/run.ps1: #Requires -PSEdition Core' && !decidePwsh(false, [{ path: 'a.ps1', text: '#Requires -Version 7' }], true, true).on && decidePwsh(true, [], false, false).on, 'pwsh "auto": a script that needs it, else this PC has it (new folder only); true/false win');
97
- const pf = 'C:\\PF';
98
- check(pcHasPwsh({ ProgramFiles: pf }, (p) => p === 'C:\\PF\\PowerShell\\7\\pwsh.exe') && pcHasPwsh({ PATH: 'C:\\x;C:\\tools\\pwsh' }, (p) => p === 'C:\\tools\\pwsh\\pwsh.exe') && !pcHasPwsh({ ProgramFiles: pf, PATH: 'C:\\Users\\u\\AppData\\Local\\Microsoft\\WindowsApps' }, (p) => /WindowsApps/.test(p)), 'pwsh on this PC: Program Files\\PowerShell\\7 or PATH, not a Store alias');
99
- const psRoot = path.join(t.temp, 'pwsh-scan');
100
- write(path.join(psRoot, 'moh.ps1'), '#Requires -Version 7\n');
101
- write(path.join(psRoot, 'scripts', 'build.ps1'), 'x'.repeat(200000) + '\n#Requires -Version 7');
102
- write(path.join(psRoot, 'node_modules', 'pkg.ps1'), '#Requires -Version 7');
103
- write(path.join(psRoot, 'scripts', 'deep', 'far.ps1'), '#Requires -Version 7');
104
- write(path.join(psRoot, 'readme.md'), 'pwsh -File x.ps1');
105
- const scanned = readPwshScripts(psRoot);
106
- check(scanned.map((f) => f.path).join() === 'moh.ps1,scripts/build.ps1' && scanned[1].text.length === PWSH_MAX_BYTES, 'pwsh scan: *.ps1 at the root and one folder down only (node_modules skipped), each read up to its cap');
107
- const shown = withPwshProfile({ theme: 'dark', profiles: { defaults: { font: {} } } }, true);
108
- const shownList = shown?.profiles;
109
- const tweaked = { profiles: { list: [{ guid: 'other' }, { guid: PWSH_PROFILE_GUID.toUpperCase(), name: 'My pwsh', fontSize: 14 }] } };
110
- const hidden = withPwshProfile(tweaked, false)?.profiles;
111
- check(shownList?.list.length === 1 && shownList.list[0].guid === PWSH_PROFILE_GUID && String(shownList.list[0].commandline).includes('pwsh.exe') && Boolean(shownList.defaults) && shown?.theme === 'dark' && withPwshProfile(shown, true) === null && withPwshProfile({ profiles: { defaults: {} } }, false) === null, 'Windows Terminal: a PowerShell 7 profile is added once pwsh is installed, nothing rewritten when already there or never wanted');
112
- check(hidden?.list[1].hidden === true && hidden.list[1].name === 'My pwsh' && hidden.list[1].fontSize === 14 && hidden.list.length === 2 && (withPwshProfile({ profiles: [{ guid: 'a' }] }, true)?.profiles).list.length === 2, 'Windows Terminal: turning pwsh off hides the profile, keeping the user\'s edits; the old array form of "profiles" works');
113
- const wtData = path.join(t.temp, 'wt-data');
114
- const wtSettings = write(path.join(wtData, 'tools', 'terminal', 'settings', 'settings.json'), '// edited in the sandbox\n{ "theme": "light", "profiles": { "list": [{ "guid": "{x}", }] } }');
115
- syncPwshProfile(wtData, true);
116
- const before = fs.readFileSync(wtSettings, 'utf8');
117
- write(path.join(wtData, 'tools', 'pwsh', 'pwsh.exe'), 'MZ');
118
- syncPwshProfile(wtData, true);
119
- const after = JSON.parse(fs.readFileSync(wtSettings, 'utf8'));
120
- syncPwshProfile(wtData, false);
121
- const off = JSON.parse(fs.readFileSync(wtSettings, 'utf8'));
122
- write(path.join(wtData, 'tools', 'terminal', 'settings', 'settings.json'), '{ not json');
123
- syncPwshProfile(wtData, true);
124
- check(before.startsWith('// edited') && after.theme === 'light' && after.profiles.list.map((p) => p.guid).join() === '{x},' + PWSH_PROFILE_GUID && off.profiles.list[1].hidden === true && fs.readFileSync(wtSettings, 'utf8') === '{ not json', 'Windows Terminal settings.json (JSONC): untouched until pwsh is installed, then the profile is added, hidden when off; an unreadable file is left alone');
125
- };
@@ -1,2 +0,0 @@
1
- import type { Harness } from './harness.js';
2
- export declare const hostTests: (t: Harness) => void;
@@ -1,71 +0,0 @@
1
- // Runs every command and shortcut binary as a separate process in a simulated HOST: none of the sandbox-only folders
2
- // or variables exist (C:\AgentLaunch, C:\AgentData, CLAUDE_CONFIG_DIR, ...), with a fresh home and storage. Tests
3
- // usually run inside a sandbox, where those exist, so this is what catches code that only works there. Every command
4
- // must either succeed or fail with a friendly "Error:" message; a crash or stack trace fails.
5
- import fs from 'node:fs';
6
- import path from 'node:path';
7
- import { spawnSync } from 'node:child_process';
8
- import { ensureDir } from '../core/util.js';
9
- const SANDBOX_ONLY_VARS = ['CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'GEMINI_CLI_HOME', 'AGENT_WORKSPACE', 'AGENT_DATA', 'AGENT_TOOLS', 'AGENT_PROJECT', 'NVM_HOME', 'NVM_SYMLINK', 'BASH_ENV', 'CLAUDE_CODE_GIT_BASH_PATH', 'AGENT_GIT_ACCESS', 'AGENT_REAL_GIT', 'AGENT_SANDBOX_CONFIG'];
10
- export const hostTests = (t) => {
11
- t.section('simulated host: every command, outside a sandbox');
12
- const home = path.join(t.temp, 'host-sim-home');
13
- const project = path.join(t.temp, 'host-sim-project');
14
- const storage = path.join(t.temp, 'host-sim-storage');
15
- for (const dir of [home, project, storage])
16
- ensureDir(dir);
17
- const config = t.write(path.join(t.temp, 'host-sim-config.json'), JSON.stringify({ storage: { root: storage }, personalization: { enabled: false }, provisioning: { terminal: { enabled: false } } }));
18
- const env = {
19
- ...Object.fromEntries(Object.entries(process.env).filter(([k]) => !SANDBOX_ONLY_VARS.includes(k))),
20
- AGENT_SANDBOX_HOME: home,
21
- AGENT_LAUNCH_DIR: path.join(t.temp, 'no-agent-launch'),
22
- AGENT_DATA_DIR: path.join(t.temp, 'no-agent-data'),
23
- AGENT_HOST_TOOLS_DIR: path.join(t.temp, 'no-host-tools'),
24
- NO_COLOR: '1',
25
- // This test may itself run inside a sandbox: the CLI must behave as it would on a PC.
26
- AGENT_SANDBOX_HOST_ONLY: '1',
27
- };
28
- const bin = (name) => path.join(t.packageRoot, 'dist', 'bin', name + '.js');
29
- const runs = [
30
- ['agent-sandbox', '--help', ['--help']],
31
- ['agent-sandbox', 'status', ['status', '--config', config]],
32
- ['agent-sandbox', 'status --json', ['status', '--json', '--config', config]],
33
- ['agent-sandbox', 'sessions', ['sessions', '--config', config]],
34
- ['agent-sandbox', 'doctor', ['doctor', '--config', config]],
35
- ['agent-sandbox', 'account list', ['account', 'list', '--config', config]],
36
- ['agent-sandbox', 'account use nobody', ['account', 'use', 'nobody', '--config', config]],
37
- ['agent-sandbox', 'config path', ['config', 'path']],
38
- ['agent-sandbox', 'config show', ['config', 'show', '--config', config]],
39
- ['agent-sandbox', 'config schema', ['config', 'schema']],
40
- ['agent-sandbox', 'config init', ['config', 'init']],
41
- ['agent-sandbox', 'config init (again)', ['config', 'init']],
42
- ['agent-sandbox', 'clean', ['clean', '--config', config]],
43
- ['agent-sandbox', 'sync', ['sync', '--config', config]],
44
- ['agent-sandbox', 'logs', ['logs', '--config', config]],
45
- ['agent-sandbox', 'attach', ['attach', '--config', config]],
46
- ['agent-sandbox', 'exec', ['exec', '--config', config, '--', 'echo', 'hi']],
47
- ['agent-sandbox', 'restart', ['restart', '--config', config]],
48
- ['agent-sandbox', 'stop', ['stop', '--config', config]],
49
- ['agent-sandbox', 'show', ['show', '--config', config]],
50
- ['agent-sandbox', 'hide', ['hide', '--config', config]],
51
- ['agent-sandbox', 'generate', ['generate', '--config', config]],
52
- ['agent-sandbox', 'launch', ['launch', '--config', config]],
53
- ['agent-sandbox', 'unknown command', ['frobnicate']],
54
- ['claude-sbx', 'claude-sbx --headless --continue', ['--headless', '--continue', '--config', config]],
55
- ['codex-sbx', 'codex-sbx', ['--config', config]],
56
- ['gemini-sbx', 'gemini-sbx', ['--config', config]],
57
- ['sbx-show', 'sbx-show', ['--config', config]],
58
- ['claude-host', 'claude-host --version', ['--version']],
59
- ];
60
- const broken = [];
61
- for (const [name, label, args] of runs) {
62
- const r = spawnSync(process.execPath, [bin(name), ...args], { cwd: project, env, encoding: 'utf8', timeout: 120000 });
63
- const output = ((r.stdout ?? '') + (r.stderr ?? '')).replace(/\x1b\[[0-9;]*m/g, '');
64
- const crashed = r.status === null || r.status > 1 || /Unexpected error|node:internal|^\s+at .+:\d+:\d+\)?$/m.test(output);
65
- const unexplained = r.status === 1 && !/(^|\n)(Error: |error: )/.test(output) && name !== 'claude-host';
66
- if (crashed || unexplained)
67
- broken.push(label + ' (exit ' + r.status + '): ' + output.trim().split('\n').slice(0, 4).join(' | '));
68
- }
69
- t.check(broken.length === 0, 'all ' + runs.length + ' commands work or fail with a clear message when run outside a sandbox' + (broken.length ? ':\n ' + broken.join('\n ') : ''));
70
- t.check(fs.existsSync(path.join(home, 'config.json')) && fs.existsSync(path.join(home, 'config.schema.json')) && fs.existsSync(path.join(home, 'sandbox-config.schema.json')), 'on a fresh PC, config init and the local schema files are created');
71
- };
@@ -1,7 +0,0 @@
1
- import type { Fixtures } from './fixtures.js';
2
- import type { Harness } from './harness.js';
3
- export declare const liveTestsAvailable: () => boolean;
4
- /** Drives the real daemon through the run folder exactly like the host does, with cmd.exe standing in for claude. */
5
- export declare const daemonTests: (t: Harness, f: Fixtures, launchPath: string) => Promise<void>;
6
- /** Compiles gitshim.cs and runs real git through it in a scratch repository, as git.exe and through Git Bash. */
7
- export declare const gitGuardTests: (t: Harness, f: Fixtures, launchPath: string) => Promise<void>;
@@ -1,127 +0,0 @@
1
- // Live tests on Windows: the real guest session daemon + ptyhost (ConPTY), and the compiled git guard on a real repo.
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { spawn, spawnSync } from 'node:child_process';
5
- import { ensureDir, findOnPath, sleep } from '../core/util.js';
6
- import { connect, encodeFrame, fixWindowsKeys, FrameReader, FrameType, helloFrame, queryViewers } from '../shared/pty-client.js';
7
- import { readJsonFile, writeJsonAtomic } from '../shared/rundir.js';
8
- const CSC = path.join(process.env.WINDIR ?? 'C:\\Windows', 'Microsoft.NET', 'Framework64', 'v4.0.30319', 'csc.exe');
9
- export const liveTestsAvailable = () => process.platform === 'win32' && fs.existsSync(CSC);
10
- /** Drives the real daemon through the run folder exactly like the host does, with cmd.exe standing in for claude. */
11
- export const daemonTests = async (t, f, launchPath) => {
12
- const { check, waitFor } = t;
13
- t.section('live: session daemon + ptyhost');
14
- const data = f.at('live-data');
15
- const workspace = f.workA;
16
- ensureDir(path.join(data, 'tools', 'agents', 'claude'));
17
- fs.copyFileSync(path.join(process.env.WINDIR ?? 'C:\\Windows', 'System32', 'cmd.exe'), path.join(data, 'tools', 'agents', 'claude', 'claude.exe'));
18
- const sessionFile = path.join(launchPath, 'session.json');
19
- const session = f.readJson(sessionFile);
20
- Object.assign(session, { workspace, attach: { enabled: true } });
21
- Object.assign(session.harness, { arguments: ['/d', '/k', 'echo LIVE-AGENT-READY'], resumeArguments: ['/d', '/k', 'echo LIVE-AGENT-RESUMED'] });
22
- fs.writeFileSync(sessionFile, JSON.stringify(session));
23
- const env = { ...process.env, AGENT_LAUNCH_DIR: launchPath, AGENT_DATA_DIR: data, AGENT_WORKSPACE: workspace, AGENT_LOCAL_DIR: f.at('local'), AGENT_LOG_PREFIX: 'daemon', AGENT_NO_TABS: '1' };
24
- const daemon = spawn(process.execPath, [path.join(launchPath, 'runtime', 'daemon.js')], { env, stdio: 'ignore', windowsHide: true });
25
- const run = path.join(data, 'run');
26
- const request = (body) => writeJsonAtomic(path.join(run, 'requests', body.id + '.json'), { createdAt: new Date().toISOString(), ...body });
27
- const response = (id) => waitFor(() => readJsonFile(path.join(run, 'responses', id + '.json')));
28
- try {
29
- request({ kind: 'session', id: 'live1', harness: 'claude', args: [], cwd: workspace });
30
- const s = await waitFor(() => { const v = readJsonFile(path.join(run, 'sessions', 'live1.json')); return v && v.phase !== 'starting' ? v : null; }, 60000);
31
- check(s?.phase === 'running' && Boolean(s.attach?.port), 'daemon compiles ptyhost and starts a session on request' + (s?.phase === 'failed' ? ' (' + s.message + ')' : ''));
32
- if (!s?.attach)
33
- return;
34
- const socket = await connect({ ...s.attach, addresses: ['127.0.0.1'] });
35
- const reader = new FrameReader();
36
- let out = '';
37
- let exitCode = null;
38
- socket.on('data', (c) => { for (const fr of reader.push(c)) {
39
- if (fr.type === FrameType.Output)
40
- out += fr.payload.toString();
41
- if (fr.type === FrameType.Exit)
42
- exitCode = fr.payload.readInt32LE(0);
43
- } });
44
- socket.write(helloFrame(s.attach.token, 100, 30));
45
- check(Boolean(await waitFor(() => out.includes('LIVE-AGENT-READY'), 10000)), 'an attached client sees the agent\'s screen');
46
- socket.write(encodeFrame(FrameType.Input, Buffer.from('echo TYPED-%USERNAME:~0,0%OK\r')));
47
- check(Boolean(await waitFor(() => out.includes('TYPED-OK'), 10000)), 'keystrokes reach the agent');
48
- const badClient = await connect({ ...s.attach, addresses: ['127.0.0.1'] });
49
- const badReader = new FrameReader();
50
- let denied = '';
51
- badClient.on('data', (c) => { for (const fr of badReader.push(c))
52
- if (fr.type === FrameType.Error)
53
- denied = fr.payload.toString(); });
54
- badClient.write(helloFrame('wrong-token', 80, 24));
55
- check(Boolean(await waitFor(() => denied, 20000)) && denied.includes('denied'), 'a client with the wrong token is refused');
56
- const local = { ...s.attach, addresses: ['127.0.0.1'] };
57
- const first = await queryViewers(local);
58
- const second = await connect(local);
59
- second.on('data', () => undefined);
60
- second.write(helloFrame(s.attach.token, 60, 20));
61
- const both = await waitFor(async () => ((await queryViewers(local))?.viewers === 2 ? true : null), 5000);
62
- second.destroy();
63
- const after = await waitFor(async () => { const v = await queryViewers(local); return v?.viewers === 1 ? v : null; }, 5000);
64
- const still = readJsonFile(path.join(run, 'sessions', 'live1.json'));
65
- check(first?.viewers === 1 && first.remote === 0 && Boolean(both) && after?.remote === 0 && still?.phase === 'running', 'viewers are counted live: a closed terminal stops counting at once and the session keeps running');
66
- check(await queryViewers({ ...local, token: 'wrong-token' }) === null, 'the viewer query needs the session token too');
67
- check(fixWindowsKeys(Buffer.from([0x61, 0x08, 0x7f, 0x1b])).equals(Buffer.from([0x61, 0x7f, 0x08, 0x1b])), 'Windows console Backspace (^H) reaches the agent as DEL, so it deletes one character, not a word');
68
- request({ kind: 'exec', id: 'exec1', command: ['echo', 'hello from exec'], cwd: workspace, timeoutSeconds: 30 });
69
- const ex = await response('exec1');
70
- check(Boolean(ex?.ok && ex.stdout?.includes('hello from exec')), 'exec runs a command in the sandbox and returns its output');
71
- request({ kind: 'exec', id: 'exec2', command: ['dir'], cwd: 'C:\\Windows', timeoutSeconds: 30 });
72
- const ex2 = await response('exec2');
73
- check(ex2?.ok === false && ex2.message.includes('not inside the sandbox workspace'), 'exec refuses folders outside the workspace');
74
- request({ kind: 'shell', id: 'evil', command: 'calc' });
75
- request({ kind: 'ping', id: 'ping1' });
76
- const ping = await response('ping1');
77
- check(Boolean(ping?.ok) && !fs.existsSync(path.join(run, 'responses', 'evil.json')) && !fs.existsSync(path.join(run, 'requests', 'evil.json')), 'ping answers; unknown request kinds are deleted unanswered');
78
- request({ kind: 'restart', id: 'restart1', harness: 'claude' });
79
- check(Boolean(await waitFor(() => out.includes('LIVE-AGENT-RESUMED'), 15000)), 'restart relaunches the agent with its resume arguments while attached');
80
- socket.write(encodeFrame(FrameType.Input, Buffer.from('exit 3\r')));
81
- await waitFor(() => exitCode !== null, 10000);
82
- const ended = await waitFor(() => { const v = readJsonFile(path.join(run, 'sessions', 'live1.json')); return v?.phase === 'exited' ? v : null; });
83
- check(exitCode === 3 && ended?.exitCode === 3, 'the agent\'s exit code reaches the client and the session status');
84
- socket.destroy();
85
- badClient.destroy();
86
- }
87
- finally {
88
- daemon.kill();
89
- await sleep(500);
90
- }
91
- };
92
- /** Compiles gitshim.cs and runs real git through it in a scratch repository, as git.exe and through Git Bash. */
93
- export const gitGuardTests = async (t, f, launchPath) => {
94
- const realGit = findOnPath(['git.exe']);
95
- t.section('live: git access guard');
96
- if (!realGit) {
97
- console.log('SKIP git guard (no git.exe on PATH)');
98
- return;
99
- }
100
- const shimDir = f.at('gitshim');
101
- ensureDir(shimDir);
102
- const compiled = spawnSync(CSC, ['/nologo', '/optimize', '/out:' + path.join(shimDir, 'git.exe'), path.join(launchPath, 'sandbox', 'gitshim.cs')], { encoding: 'utf8' });
103
- t.check(compiled.status === 0, 'gitshim.cs compiles with the .NET Framework compiler' + (compiled.status === 0 ? '' : ': ' + compiled.stdout));
104
- if (compiled.status !== 0)
105
- return;
106
- const repo = f.at('guard-repo');
107
- ensureDir(repo);
108
- spawnSync(realGit, ['init', '-q'], { cwd: repo });
109
- fs.writeFileSync(path.join(repo, 'a.txt'), 'hello');
110
- const guard = path.join(launchPath, 'runtime', 'gitguard.js');
111
- const env = (access) => ({ ...process.env, PATH: shimDir + path.delimiter + process.env.PATH, AGENT_GIT_ACCESS: access, AGENT_REAL_GIT: access === 'readonly' ? realGit : '', AGENT_GUARD_NODE: process.execPath, AGENT_GUARD_SCRIPT: guard, GIT_CONFIG_NOSYSTEM: '1' });
112
- const git = (access, args) => spawnSync('git', args, { cwd: repo, env: env(access), encoding: 'utf8' });
113
- const status = git('readonly', ['status', '--porcelain']);
114
- t.check(status.status === 0 && status.stdout.includes('a.txt'), 'readonly: `git status` works through the git.exe stand-in (exact arguments preserved)');
115
- const add = git('readonly', ['add', 'a.txt']);
116
- t.check(add.status === 128 && add.stderr.includes('read-only') && git('readonly', ['status', '--porcelain']).stdout.includes('??'), 'readonly: `git add` is refused and nothing changes');
117
- const none = git('none', ['status']);
118
- t.check(none.status === 128 && none.stderr.includes('disabled'), 'none: every git call is refused');
119
- const bash = [path.join(path.dirname(path.dirname(realGit)), 'bin', 'bash.exe'), path.join(path.dirname(path.dirname(realGit)), 'usr', 'bin', 'bash.exe')].find((b) => fs.existsSync(b));
120
- if (bash) {
121
- const bashEnv = t.write(f.at('gitshim', 'bashenv.sh'), 'git() { "' + process.execPath.replace(/\\/g, '/') + '" "' + guard.replace(/\\/g, '/') + '" "$@"; }\nexport -f git\n');
122
- const viaBash = spawnSync(bash, ['-c', 'git commit -m x'], { cwd: repo, env: { ...env('readonly'), BASH_ENV: bashEnv.replace(/\\/g, '/') }, encoding: 'utf8' });
123
- t.check(viaBash.status === 128 && viaBash.stderr.includes('read-only'), 'readonly: git inside Git Bash goes through the guard too (BASH_ENV)');
124
- }
125
- else
126
- console.log('SKIP Git Bash check (no bash.exe next to git)');
127
- };
@@ -1,3 +0,0 @@
1
- import type { Fixtures } from './fixtures.js';
2
- import type { Harness } from './harness.js';
3
- export declare const securityTests: (t: Harness, f: Fixtures) => Promise<void>;