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
@@ -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;
@@ -0,0 +1,93 @@
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
+ //
6
+ // Each command gets its own home and storage folder, so they cannot disturb each other and run several at a time
7
+ // (every one is a fresh Node process, which is what makes this the slowest suite when run one by one).
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ 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'];
11
+ const PARALLEL = 2;
12
+ const sandbox = (label, ...args) => ({ bin: 'agent-sandbox', label, args, config: true });
13
+ const COMMANDS = [
14
+ { bin: 'agent-sandbox', label: '--help', args: ['--help'] },
15
+ sandbox('status', 'status'),
16
+ sandbox('status --json', 'status', '--json'),
17
+ sandbox('sessions', 'sessions'),
18
+ sandbox('doctor', 'doctor'),
19
+ sandbox('account list', 'account', 'list'),
20
+ sandbox('account use nobody', 'account', 'use', 'nobody'),
21
+ { bin: 'agent-sandbox', label: 'config path', args: ['config', 'path'] },
22
+ sandbox('config show', 'config', 'show'),
23
+ { bin: 'agent-sandbox', label: 'config schema', args: ['config', 'schema'] },
24
+ sandbox('clean', 'clean'),
25
+ sandbox('sync', 'sync'),
26
+ sandbox('logs', 'logs'),
27
+ sandbox('attach', 'attach'),
28
+ { bin: 'agent-sandbox', label: 'exec', args: ['exec'], after: ['--', 'echo', 'hi'], config: true },
29
+ sandbox('restart', 'restart'),
30
+ sandbox('stop', 'stop'),
31
+ sandbox('show', 'show'),
32
+ sandbox('hide', 'hide'),
33
+ sandbox('generate', 'generate'),
34
+ sandbox('launch', 'launch'),
35
+ { bin: 'agent-sandbox', label: 'unknown command', args: ['frobnicate'] },
36
+ { bin: 'claude-sbx', label: 'claude-sbx --headless --continue', args: ['--headless', '--continue'], config: true },
37
+ { bin: 'codex-sbx', label: 'codex-sbx', args: [], config: true },
38
+ { bin: 'gemini-sbx', label: 'gemini-sbx', args: [], config: true },
39
+ { bin: 'sbx-show', label: 'sbx-show', args: [], config: true },
40
+ { bin: 'claude-host', label: 'claude-host --version', args: ['--version'] },
41
+ ];
42
+ export const hostSimSuite = {
43
+ name: 'host-sim',
44
+ title: 'every command run as its own process on a simulated host PC',
45
+ timeoutMs: 300000,
46
+ run: async (t, f) => {
47
+ t.section('every command outside a sandbox');
48
+ const baseEnv = Object.fromEntries(Object.entries(process.env).filter(([k]) => !SANDBOX_ONLY_VARS.includes(k)));
49
+ const project = t.dir('host-sim-project');
50
+ const envFor = (home) => ({
51
+ ...baseEnv,
52
+ AGENT_SANDBOX_HOME: home,
53
+ AGENT_LAUNCH_DIR: t.at('no-agent-launch'),
54
+ AGENT_DATA_DIR: t.at('no-agent-data'),
55
+ AGENT_HOST_TOOLS_DIR: t.at('no-host-tools'),
56
+ NO_COLOR: '1',
57
+ // This test may itself run inside a sandbox: the CLI must behave as it would on a PC.
58
+ AGENT_SANDBOX_HOST_ONLY: '1',
59
+ });
60
+ /** A fresh home, storage and config file for one command. */
61
+ const freshPc = (name) => {
62
+ const home = t.dir('host-sim', name, 'home');
63
+ const storage = t.dir('host-sim', name, 'storage');
64
+ const config = t.writeJson(t.at('host-sim', name, 'config.json'), { storage: { root: storage }, personalization: { enabled: false }, provisioning: { terminal: { enabled: false } } });
65
+ return { home, config };
66
+ };
67
+ const broken = [];
68
+ const runOne = async (command, index, pc = freshPc('c' + index)) => {
69
+ const args = command.config ? [...command.args, '--config', pc.config, ...(command.after ?? [])] : command.args;
70
+ const r = await f.runBin(command.bin, args, { cwd: project, env: envFor(pc.home), timeoutMs: 120000 });
71
+ const crashed = r.status === null || r.status > 1 || /Unexpected error|node:internal|^\s+at .+:\d+:\d+\)?$/m.test(r.out);
72
+ const unexplained = r.status === 1 && !/(^|\n)(Error: |error: )/.test(r.out) && command.bin !== 'claude-host';
73
+ if (crashed || unexplained)
74
+ broken.push(command.label + ' (exit ' + r.status + (r.timedOut ? ', timed out' : '') + '): ' + r.out.trim().split('\n').slice(0, 4).join(' | '));
75
+ };
76
+ // `config init` twice must see the same home (the second run finds the files the first one made).
77
+ const initPc = freshPc('init');
78
+ const queue = COMMANDS.map((command, index) => ({ command, index }));
79
+ const workers = Array.from({ length: PARALLEL }, async () => {
80
+ for (let next = queue.shift(); next; next = queue.shift())
81
+ await runOne(next.command, next.index);
82
+ });
83
+ const initRuns = (async () => {
84
+ await runOne({ bin: 'agent-sandbox', label: 'config init', args: ['config', 'init'] }, 1000, initPc);
85
+ await runOne({ bin: 'agent-sandbox', label: 'config init (again)', args: ['config', 'init'] }, 1001, initPc);
86
+ })();
87
+ await Promise.all([...workers, initRuns]);
88
+ t.equal(broken, [], 'all ' + (COMMANDS.length + 2) + ' commands work or fail with a clear message when run outside a sandbox');
89
+ for (const file of ['config.json', 'config.schema.json', 'sandbox-config.schema.json']) {
90
+ t.check(fs.existsSync(path.join(initPc.home, file)), 'on a fresh PC, config init creates ' + file);
91
+ }
92
+ },
93
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const hostileInputSuite: Suite;
@@ -0,0 +1,54 @@
1
+ // Names, paths and text coming from a hostile sandbox or workspace: file names, deletes, printed text, mappings, state
2
+ // files and the launch path.
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { assertCmdSafePath } from '../host/platform.js';
7
+ import { assertSafeMapping } from '../core/safe-paths.js';
8
+ import { safeText } from '../core/util.js';
9
+ import { isSafeFileName } from '../shared/paths.js';
10
+ import { removeRefusal, safeRemove } from '../shared/safe-remove.js';
11
+ import { writeJsonAtomic } from '../shared/rundir.js';
12
+ export const hostileInputSuite = {
13
+ name: 'hostile-input',
14
+ title: 'file names, deletes, printed text, mappings and state files from an untrusted sandbox',
15
+ run: async (t) => {
16
+ t.section('file names');
17
+ const unsafe = ['CON.jsonl', 'nul', 'aux.json', 'COM1.txt', 'a:b', 'x.jsonl.', 'x ', '..', 'a\\b', 'LPT3'];
18
+ t.equal(unsafe.filter(isSafeFileName), [], 'device names, streams and trailing dots are not valid names to copy into this PC');
19
+ const fine = ['C--Workspace-app', 'a.jsonl', '.credentials.json', 'console.jsonl'];
20
+ t.equal(fine.filter((name) => !isSafeFileName(name)), [], 'ordinary names (even ones that merely contain a device name) are valid');
21
+ t.section('deletes');
22
+ const profile = os.homedir();
23
+ const root = t.at('safe-root');
24
+ const targets = {
25
+ 'your profile': profile,
26
+ 'a folder above your profile': path.dirname(profile),
27
+ 'a drive root': 'C:\\',
28
+ Windows: 'C:\\Windows',
29
+ 'an empty path': '',
30
+ 'a relative path': 'relative\\folder',
31
+ 'a path outside the root': t.at('outside-root', 'x'),
32
+ };
33
+ for (const [what, target] of Object.entries(targets))
34
+ t.check(removeRefusal(target, root) !== null, 'a delete is refused for ' + what);
35
+ t.check(removeRefusal('\\\\?\\C:\\Users\\someone\\x', '\\\\?\\C:\\Users\\someone') !== null, 'a delete is refused for \\\\?\\ paths');
36
+ t.check(removeRefusal('C:\\Windows\\Temp\\x', 'C:\\Windows\\Temp') !== null, 'a delete is refused inside Windows');
37
+ t.check(removeRefusal('C:\\Program Files\\x\\y', 'C:\\Program Files\\x') !== null, 'a delete is refused inside Program Files');
38
+ const allowedTarget = path.join(root, 'tools', 'go');
39
+ t.write(path.join(allowedTarget, 'go.exe'), 'x');
40
+ safeRemove(allowedTarget, root);
41
+ t.check(!fs.existsSync(allowedTarget), 'a folder inside its root is deleted');
42
+ t.section('text, mappings, state files, launch path');
43
+ t.equal(safeText('ok \x1b[31mred\x1b[0m \x1b]52;c;ZXZpbA==\x07\x1b]0;title\x07\x1b[2J\x07end'), 'ok \x1b[31mred\x1b[0m end', 'sandbox text printed on this PC keeps colours but loses every other escape sequence');
44
+ await t.throws(() => assertSafeMapping(path.join(t.packageRoot, 'dist'), t.packageRoot, t.at('storage')), 'overlaps', 'extra mappings may not reach the install folder');
45
+ await t.throws(() => assertSafeMapping(t.at('storage', 'data'), t.packageRoot, t.at('storage')), 'overlaps', 'extra mappings may not reach the storage folder');
46
+ const atomicFile = t.at('atomic', 'state.json');
47
+ writeJsonAtomic(atomicFile, { a: 1 });
48
+ writeJsonAtomic(atomicFile, { a: 2 });
49
+ t.equal(t.readJson(atomicFile).a, 2, 'state files are replaced whole...');
50
+ t.equal(fs.readdirSync(path.dirname(atomicFile)).length, 1, '... with no temp files left behind');
51
+ await t.throws(() => assertCmdSafePath('C:\\a\\%TEMP%\\agent.wsb'), 'cannot be started safely', 'a launch path with %VAR% (which cmd.exe would expand) is refused');
52
+ await t.throws(() => assertCmdSafePath('C:\\a"&calc&"\\agent.wsb'), 'cannot be started safely', 'a launch path with quotes and & is refused');
53
+ },
54
+ };
@@ -1 +1,8 @@
1
- export declare const selfTest: (packageRoot: string) => Promise<number>;
1
+ import type { Suite } from './suite.js';
2
+ /** Every suite, in the order they run. The live ones come last: they are the slowest and need Windows. */
3
+ export declare const SUITES: Suite[];
4
+ /** Windows Sandbox (its user is always WDAGUtilityAccount) or a GitHub Actions runner: never someone's PC. */
5
+ export declare const onThrowawayMachine: () => boolean;
6
+ /** Resolves "a,b" / ["a", "b"] to suites; throws with the list of valid names for an unknown one. */
7
+ export declare const pickSuites: (names: string[]) => Suite[];
8
+ export declare const selfTest: (packageRoot: string, suiteNames?: string[]) => Promise<number>;
@@ -1,55 +1,154 @@
1
- // `agent-sandbox self-test`: offline logic and security tests, plus live tests of the session daemon, ptyhost and
2
- // the git guard when running on Windows (they need csc.exe and ConPTY, both part of Windows 10/11).
1
+ // `agent-sandbox self-test [suite...]`: offline logic and security tests, plus live tests of the session daemon, ptyhost
2
+ // and the git guard when running on Windows (they need csc.exe and ConPTY, both part of Windows 10/11).
3
+ // They start real processes (the guest runtime, the daemon, csc, git), so they only run on a throwaway machine.
4
+ //
5
+ // Each area is one suite in its own *.test.ts file; the list below is the order they run in. Give suite names (or a
6
+ // comma-separated list) to run only those: `sandbox self-test config`, `npm test -- live-daemon`.
7
+ import os from 'node:os';
8
+ import path from 'node:path';
3
9
  import { errMsg } from '../core/util.js';
4
- import { configTests } from './config.test.js';
5
- import { cliTests } from './cli.test.js';
6
- import { dataTests } from './data.test.js';
7
- import { detectTests } from './detect.test.js';
8
- import { mirrorTests } from './mirror.test.js';
9
10
  import { createFixtures } from './fixtures.js';
10
- import { guestTests } from './guest.test.js';
11
11
  import { createHarness } from './harness.js';
12
- import { helperTests } from './helpers.test.js';
13
- import { hostTests } from './host.test.js';
14
- import { daemonTests, gitGuardTests, liveTestsAvailable } from './live.test.js';
15
- import { securityTests } from './security.test.js';
16
- import { setupTests } from './setup.test.js';
17
- import { syncTests } from './sync.test.js';
18
- export const selfTest = async (packageRoot) => {
12
+ import { liveTestsAvailable } from './live.js';
13
+ import { accountsSuite } from './accounts.test.js';
14
+ import { agentCommandsSuite } from './agent-commands.test.js';
15
+ import { channelSuite } from './channel.test.js';
16
+ import { cliSuite } from './cli.test.js';
17
+ import { cmdlineSuite } from './cmdline.test.js';
18
+ import { configSuite } from './config.test.js';
19
+ import { dataSuite } from './data.test.js';
20
+ import { detectSuite } from './detect.test.js';
21
+ import { generateSuite } from './generate.test.js';
22
+ import { gitAccessSuite } from './git-access.test.js';
23
+ import { guestBootSuite } from './guest-boot.test.js';
24
+ import { hostileInputSuite } from './hostile-input.test.js';
25
+ import { hostSimSuite } from './host-sim.test.js';
26
+ import { linksSuite } from './links.test.js';
27
+ import { liveDaemonSuite } from './live-daemon.test.js';
28
+ import { liveGitSuite } from './live-git.test.js';
29
+ import { liveProjectsSuite } from './live-projects.test.js';
30
+ import { localConfigSuite } from './local-config.test.js';
31
+ import { mirrorSuite } from './mirror.test.js';
32
+ import { networkSuite } from './network.test.js';
33
+ import { personalizationSuite } from './personalization.test.js';
34
+ import { projectsSuite } from './projects.test.js';
35
+ import { repairSuite } from './repair.test.js';
36
+ import { repoGuardSuite } from './repo-guard.test.js';
37
+ import { secretsSuite } from './secrets.test.js';
38
+ import { setupSuite } from './setup.test.js';
39
+ import { syncSuite } from './sync.test.js';
40
+ import { testGuardSuite } from './test-guard.test.js';
41
+ import { toolchainsSuite } from './toolchains.test.js';
42
+ import { workspaceSuite } from './workspace.test.js';
43
+ /** Every suite, in the order they run. The live ones come last: they are the slowest and need Windows. */
44
+ export const SUITES = [
45
+ // settings and project files
46
+ configSuite, workspaceSuite, localConfigSuite,
47
+ // the generated sandbox and the guest that boots it
48
+ generateSuite, guestBootSuite, mirrorSuite, projectsSuite,
49
+ // syncing with this PC
50
+ syncSuite, accountsSuite,
51
+ // an untrusted sandbox: what the host refuses to do with what it finds
52
+ linksSuite, channelSuite, gitAccessSuite, repoGuardSuite, secretsSuite, hostileInputSuite, testGuardSuite,
53
+ // pure helpers and the command line
54
+ toolchainsSuite, detectSuite, networkSuite, personalizationSuite, cmdlineSuite, setupSuite, dataSuite, repairSuite, cliSuite, agentCommandsSuite,
55
+ // every command as its own process
56
+ hostSimSuite,
57
+ // real processes
58
+ liveDaemonSuite, liveProjectsSuite, liveGitSuite,
59
+ ];
60
+ /** Windows Sandbox (its user is always WDAGUtilityAccount) or a GitHub Actions runner: never someone's PC. */
61
+ export const onThrowawayMachine = () => os.userInfo().username.toLowerCase() === 'wdagutilityaccount' || process.env.GITHUB_ACTIONS === 'true';
62
+ const withDeadline = async (work, ms, what) => {
63
+ let timer;
64
+ const deadline = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(what + ' did not finish within ' + Math.round(ms / 1000) + ' s')), ms); });
65
+ try {
66
+ return await Promise.race([work, deadline]);
67
+ }
68
+ finally {
69
+ clearTimeout(timer);
70
+ }
71
+ };
72
+ const runSuite = async (suite, t, f) => {
73
+ const checksBefore = t.checks();
74
+ const failedBefore = t.failures();
75
+ const started = Date.now();
76
+ let note;
77
+ if (suite.live && !liveTestsAvailable()) {
78
+ t.section(suite.name);
79
+ t.skip(suite.title + ' (needs Windows with the .NET Framework compiler)');
80
+ note = 'skipped';
81
+ }
82
+ else {
83
+ try {
84
+ await withDeadline(Promise.resolve(suite.run(t, f)), suite.timeoutMs ?? 120000, 'suite "' + suite.name + '"');
85
+ }
86
+ catch (e) {
87
+ t.check(false, 'suite "' + suite.name + '" stopped with an unexpected error', errMsg(e) + (e instanceof Error && e.stack ? '\n' + e.stack : ''));
88
+ }
89
+ }
90
+ return { suite, checks: t.checks() - checksBefore, failed: t.failures() - failedBefore, seconds: (Date.now() - started) / 1000, note };
91
+ };
92
+ const printSummary = (results) => {
93
+ const width = Math.max(...results.map((r) => r.suite.name.length));
94
+ console.log('\n\x1b[36m-- summary\x1b[0m');
95
+ for (const r of results) {
96
+ const state = r.note ?? (r.failed ? r.failed + ' FAILED' : 'ok');
97
+ console.log(' ' + r.suite.name.padEnd(width) + ' ' + String(r.checks).padStart(4) + ' checks ' + r.seconds.toFixed(1).padStart(6) + ' s ' + state);
98
+ }
99
+ const total = results.reduce((sum, r) => sum + r.checks, 0);
100
+ console.log(' ' + 'total'.padEnd(width) + ' ' + String(total).padStart(4) + ' checks ' + results.reduce((sum, r) => sum + r.seconds, 0).toFixed(1).padStart(6) + ' s');
101
+ };
102
+ /** Resolves "a,b" / ["a", "b"] to suites; throws with the list of valid names for an unknown one. */
103
+ export const pickSuites = (names) => {
104
+ const wanted = names.flatMap((n) => n.split(',')).map((n) => n.trim()).filter(Boolean);
105
+ if (wanted.length === 0)
106
+ return SUITES;
107
+ const unknown = wanted.filter((name) => !SUITES.some((s) => s.name === name));
108
+ if (unknown.length)
109
+ throw new Error('No test suite called ' + unknown.join(', ') + '. Suites: ' + SUITES.map((s) => s.name).join(', '));
110
+ return SUITES.filter((s) => wanted.includes(s.name));
111
+ };
112
+ export const selfTest = async (packageRoot, suiteNames = []) => {
113
+ if (!onThrowawayMachine()) {
114
+ console.error('The self-test only runs inside a Windows Sandbox, so it can never change this PC.\nRun it there: `sandbox start` in this folder, then `npm test` in the sandbox\'s terminal.');
115
+ return 1;
116
+ }
117
+ let chosen;
118
+ try {
119
+ chosen = pickSuites(suiteNames);
120
+ }
121
+ catch (e) {
122
+ console.error(errMsg(e));
123
+ return 1;
124
+ }
125
+ // Defence in depth (shared/test-guard.ts): with this set, host code refuses outward actions and writes outside temp.
126
+ process.env.AGENT_SANDBOX_SELFTEST = '1';
19
127
  const t = createHarness(packageRoot);
20
128
  const saved = { home: process.env.AGENT_SANDBOX_HOME, windir: process.env.WINDIR };
21
- process.env.AGENT_SANDBOX_HOME = t.temp + '\\user-home';
129
+ process.env.AGENT_SANDBOX_HOME = path.join(t.temp, 'user-home');
130
+ const results = [];
22
131
  try {
23
132
  const f = createFixtures(t);
24
- await configTests(t, f);
25
- const launchPath = await guestTests(t, f);
26
- await syncTests(t, f);
27
- await mirrorTests(t, f);
28
- await securityTests(t, f);
29
- helperTests(t);
30
- detectTests(t);
31
- setupTests(t);
32
- hostTests(t);
33
- dataTests(t);
34
- cliTests(t);
35
- if (liveTestsAvailable()) {
36
- await daemonTests(t, f, launchPath);
37
- await gitGuardTests(t, f, launchPath);
38
- }
39
- else
40
- console.log('SKIP live tests (need Windows with the .NET Framework compiler)');
133
+ for (const suite of chosen)
134
+ results.push(await runSuite(suite, t, f));
41
135
  }
42
136
  catch (e) {
43
- t.check(false, 'unexpected error: ' + errMsg(e) + (e instanceof Error && e.stack ? '\n' + e.stack : ''));
137
+ t.check(false, 'unexpected error: ' + errMsg(e), e instanceof Error ? e.stack : undefined);
44
138
  }
45
139
  finally {
46
140
  if (saved.home === undefined)
47
141
  delete process.env.AGENT_SANDBOX_HOME;
48
142
  else
49
143
  process.env.AGENT_SANDBOX_HOME = saved.home;
50
- process.env.WINDIR = saved.windir;
144
+ if (saved.windir === undefined)
145
+ delete process.env.WINDIR;
146
+ else
147
+ process.env.WINDIR = saved.windir;
51
148
  t.dispose();
52
149
  }
150
+ if (results.length > 1)
151
+ printSummary(results);
53
152
  const failures = t.failures();
54
153
  console.log(failures === 0 ? '\nALL TESTS PASSED' : '\n' + failures + ' TEST(S) FAILED');
55
154
  return failures === 0 ? 0 : 1;
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const linksSuite: Suite;