windows-development-sandbox 1.9.0 → 1.11.3

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 (398) hide show
  1. package/README.md +64 -17
  2. package/config.example.json +7 -2
  3. package/dist/app/features/accounts/accounts.def.d.ts +122 -0
  4. package/dist/app/features/accounts/accounts.def.js +243 -0
  5. package/dist/app/features/accounts/accounts.run.d.ts +51 -0
  6. package/dist/app/features/accounts/accounts.run.js +125 -0
  7. package/dist/app/features/accounts/index.d.ts +2 -0
  8. package/dist/app/features/accounts/index.js +3 -0
  9. package/dist/app/features/agents/agent-entry.d.ts +17 -0
  10. package/dist/app/features/agents/agent-entry.js +50 -0
  11. package/dist/app/features/agents/agents.def.d.ts +65 -0
  12. package/dist/app/features/agents/agents.def.js +142 -0
  13. package/dist/app/features/agents/agents.run.d.ts +31 -0
  14. package/dist/app/features/agents/agents.run.js +189 -0
  15. package/dist/app/features/agents/index.d.ts +3 -0
  16. package/dist/app/features/agents/index.js +4 -0
  17. package/dist/app/features/apps/apps-rows.d.ts +31 -0
  18. package/dist/app/features/apps/apps-rows.js +73 -0
  19. package/dist/app/features/apps/apps.def.d.ts +181 -0
  20. package/dist/app/features/apps/apps.def.js +264 -0
  21. package/dist/app/features/apps/apps.run.d.ts +58 -0
  22. package/dist/app/features/apps/apps.run.js +76 -0
  23. package/dist/app/features/apps/index.d.ts +3 -0
  24. package/dist/app/features/apps/index.js +4 -0
  25. package/dist/app/features/data/data.def.d.ts +154 -0
  26. package/dist/app/features/data/data.def.js +201 -0
  27. package/dist/app/features/data/data.run.d.ts +46 -0
  28. package/dist/app/features/data/data.run.js +141 -0
  29. package/dist/app/features/data/index.d.ts +2 -0
  30. package/dist/app/features/data/index.js +3 -0
  31. package/dist/app/features/defs.d.ts +952 -4
  32. package/dist/app/features/defs.js +147 -10
  33. package/dist/app/features/engine/engine.def.d.ts +228 -0
  34. package/dist/app/features/engine/engine.def.js +194 -0
  35. package/dist/app/features/engine/engine.run.d.ts +72 -0
  36. package/dist/app/features/engine/engine.run.js +77 -0
  37. package/dist/app/features/engine/index.d.ts +2 -0
  38. package/dist/app/features/engine/index.js +4 -0
  39. package/dist/app/features/files/files.def.d.ts +51 -0
  40. package/dist/app/features/files/files.def.js +38 -0
  41. package/dist/app/features/files/files.run.d.ts +20 -0
  42. package/dist/app/features/files/files.run.js +59 -0
  43. package/dist/app/features/files/index.d.ts +2 -0
  44. package/dist/app/features/files/index.js +3 -0
  45. package/dist/app/features/github/github.def.d.ts +80 -0
  46. package/dist/app/features/github/github.def.js +142 -0
  47. package/dist/app/features/github/github.run.d.ts +36 -0
  48. package/dist/app/features/github/github.run.js +59 -0
  49. package/dist/app/features/github/index.d.ts +2 -0
  50. package/dist/app/features/github/index.js +3 -0
  51. package/dist/app/features/health/health.def.d.ts +275 -0
  52. package/dist/app/features/health/health.def.js +281 -0
  53. package/dist/app/features/health/health.run.d.ts +125 -0
  54. package/dist/app/features/health/health.run.js +204 -0
  55. package/dist/app/features/health/index.d.ts +2 -0
  56. package/dist/app/features/health/index.js +3 -0
  57. package/dist/app/features/logs/index.d.ts +2 -0
  58. package/dist/app/features/logs/index.js +3 -0
  59. package/dist/app/features/logs/logs.def.d.ts +78 -0
  60. package/dist/app/features/logs/logs.def.js +115 -0
  61. package/dist/app/features/logs/logs.run.d.ts +20 -0
  62. package/dist/app/features/logs/logs.run.js +42 -0
  63. package/dist/app/features/projects/index.d.ts +2 -0
  64. package/dist/app/features/projects/index.js +3 -0
  65. package/dist/app/features/projects/projects.def.d.ts +101 -0
  66. package/dist/app/features/projects/projects.def.js +208 -0
  67. package/dist/app/features/projects/projects.run.d.ts +56 -0
  68. package/dist/app/features/projects/projects.run.js +155 -0
  69. package/dist/app/features/sandbox/sandbox.def.js +11 -5
  70. package/dist/app/features/sessions/sessions.def.js +3 -0
  71. package/dist/app/features/settings/categories.def.js +3 -2
  72. package/dist/{cli/menu/settings/share.d.ts → app/features/settings/settings-share.d.ts} +1 -1
  73. package/dist/{cli/menu/settings/share.js → app/features/settings/settings-share.js} +3 -2
  74. package/dist/app/features/settings/settings.def.d.ts +22 -5
  75. package/dist/app/features/settings/settings.def.js +81 -6
  76. package/dist/app/features/settings/settings.run.d.ts +17 -0
  77. package/dist/app/features/settings/settings.run.js +89 -4
  78. package/dist/app/features/storage/index.d.ts +2 -0
  79. package/dist/app/features/storage/index.js +3 -0
  80. package/dist/app/features/storage/storage.def.d.ts +133 -0
  81. package/dist/app/features/storage/storage.def.js +225 -0
  82. package/dist/app/features/storage/storage.run.d.ts +62 -0
  83. package/dist/app/features/storage/storage.run.js +370 -0
  84. package/dist/app/features/updates/index.d.ts +3 -0
  85. package/dist/app/features/updates/index.js +4 -0
  86. package/dist/app/features/updates/update-lines.d.ts +13 -0
  87. package/dist/app/features/updates/update-lines.js +61 -0
  88. package/dist/app/features/updates/updates.def.d.ts +89 -0
  89. package/dist/app/features/updates/updates.def.js +225 -0
  90. package/dist/app/features/updates/updates.run.d.ts +48 -0
  91. package/dist/app/features/updates/updates.run.js +154 -0
  92. package/dist/app/features/vpn/index.d.ts +2 -0
  93. package/dist/app/features/vpn/index.js +3 -0
  94. package/dist/app/features/vpn/vpn.def.d.ts +112 -0
  95. package/dist/app/features/vpn/vpn.def.js +174 -0
  96. package/dist/app/features/vpn/vpn.run.d.ts +37 -0
  97. package/dist/app/features/vpn/vpn.run.js +54 -0
  98. package/dist/app/features/web/index.d.ts +2 -0
  99. package/dist/app/features/web/index.js +3 -0
  100. package/dist/app/features/web/web.def.d.ts +33 -0
  101. package/dist/app/features/web/web.def.js +90 -0
  102. package/dist/app/features/web/web.run.d.ts +24 -0
  103. package/dist/app/features/web/web.run.js +93 -0
  104. package/dist/app/kernel/define.d.ts +10 -1
  105. package/dist/app/kernel/io.d.ts +34 -0
  106. package/dist/app/kernel/io.js +9 -0
  107. package/dist/app/kernel/rpc.d.ts +17 -0
  108. package/dist/app/kernel/text.d.ts +2 -0
  109. package/dist/app/kernel/text.js +7 -0
  110. package/dist/app/kernel/view.d.ts +24 -1
  111. package/dist/app/kernel/view.js +1 -1
  112. package/dist/app/registry.js +74 -1
  113. package/dist/cli/args.js +1 -1
  114. package/dist/cli/commands/context.d.ts +7 -0
  115. package/dist/cli/commands/context.js +29 -2
  116. package/dist/cli/commands/engine.js +5 -58
  117. package/dist/cli/commands/github.d.ts +2 -0
  118. package/dist/cli/commands/github.js +11 -0
  119. package/dist/cli/commands/index.d.ts +3 -5
  120. package/dist/cli/commands/index.js +3 -5
  121. package/dist/cli/commands/launch/launch.js +23 -4
  122. package/dist/cli/commands/launch/running.js +4 -1
  123. package/dist/cli/commands/maintenance/config.js +1 -6
  124. package/dist/cli/commands/maintenance/index.d.ts +0 -1
  125. package/dist/cli/commands/maintenance/index.js +1 -2
  126. package/dist/cli/commands/maintenance/logs.js +27 -32
  127. package/dist/cli/commands/maintenance/migrate.d.ts +0 -8
  128. package/dist/cli/commands/maintenance/migrate.js +2 -44
  129. package/dist/cli/commands/maintenance/update.d.ts +0 -19
  130. package/dist/cli/commands/maintenance/update.js +1 -99
  131. package/dist/cli/commands/projects/index.d.ts +0 -1
  132. package/dist/cli/commands/projects/index.js +0 -1
  133. package/dist/cli/commands/registry.d.ts +1 -1
  134. package/dist/cli/commands/registry.js +29 -11
  135. package/dist/cli/commands/repair/checks/data.js +1 -1
  136. package/dist/cli/commands/repair/checks/hcs.js +28 -1
  137. package/dist/cli/commands/repair/checks/sandbox.js +1 -1
  138. package/dist/cli/commands/repair/health-run.d.ts +30 -0
  139. package/dist/cli/commands/repair/health-run.js +32 -0
  140. package/dist/cli/commands/repair/printer.d.ts +2 -2
  141. package/dist/cli/commands/repair/printer.js +2 -11
  142. package/dist/cli/commands/repair/run.d.ts +1 -21
  143. package/dist/cli/commands/repair/run.js +13 -70
  144. package/dist/cli/commands/storage/index.d.ts +0 -1
  145. package/dist/cli/commands/storage/index.js +0 -1
  146. package/dist/cli/commands/storage/offer.js +6 -2
  147. package/dist/cli/commands/vpn.d.ts +1 -3
  148. package/dist/cli/commands/vpn.js +8 -88
  149. package/dist/cli/commands/web.d.ts +7 -1
  150. package/dist/cli/commands/web.js +45 -71
  151. package/dist/cli/index.js +3 -2
  152. package/dist/cli/manifest.d.ts +16 -153
  153. package/dist/cli/manifest.js +20 -50
  154. package/dist/cli/menu/command-form.js +1 -2
  155. package/dist/cli/menu/io-menu.d.ts +2 -2
  156. package/dist/cli/menu/io-menu.js +6 -4
  157. package/dist/cli/menu/menu.js +1 -1
  158. package/dist/cli/menu/pages/accounts-page.d.ts +5 -17
  159. package/dist/cli/menu/pages/accounts-page.js +28 -170
  160. package/dist/cli/menu/pages/agents-page.d.ts +17 -18
  161. package/dist/cli/menu/pages/agents-page.js +67 -124
  162. package/dist/cli/menu/pages/apps-page.d.ts +15 -8
  163. package/dist/cli/menu/pages/apps-page.js +121 -29
  164. package/dist/cli/menu/pages/list-page.js +10 -1
  165. package/dist/cli/menu/pages/maintenance-page.d.ts +51 -52
  166. package/dist/cli/menu/pages/maintenance-page.js +246 -427
  167. package/dist/cli/menu/pages/sandbox-page.d.ts +3 -5
  168. package/dist/cli/menu/pages/sandbox-page.js +34 -112
  169. package/dist/cli/menu/pages/settings-page.d.ts +21 -3
  170. package/dist/cli/menu/pages/settings-page.js +89 -17
  171. package/dist/cli/menu/render-view.js +2 -1
  172. package/dist/cli/menu/settings/index.d.ts +0 -1
  173. package/dist/cli/menu/settings/index.js +3 -3
  174. package/dist/cli/menu/views.js +28 -0
  175. package/dist/cli/operations/handler.js +7 -3
  176. package/dist/cli/operations/input.d.ts +1 -1
  177. package/dist/cli/operations/input.js +29 -5
  178. package/dist/cli/operations/io-cli.js +9 -1
  179. package/dist/cli/operations/spec.js +8 -2
  180. package/dist/cli/output/render-view.js +2 -0
  181. package/dist/cli/{commands/data/collect.js → services/data-collect.js} +2 -2
  182. package/dist/cli/{commands/data/remove.js → services/data-remove.js} +2 -2
  183. package/dist/cli/services/data-service.d.ts +1 -1
  184. package/dist/cli/services/data-service.js +2 -2
  185. package/dist/cli/services/databases-service.d.ts +5 -0
  186. package/dist/cli/services/databases-service.js +15 -0
  187. package/dist/cli/services/health-service.d.ts +8 -4
  188. package/dist/cli/services/health-service.js +10 -8
  189. package/dist/cli/services/index.d.ts +4 -1
  190. package/dist/cli/services/index.js +4 -1
  191. package/dist/cli/services/logs-service.d.ts +7 -0
  192. package/dist/cli/services/logs-service.js +13 -0
  193. package/dist/cli/services/storage-service.d.ts +19 -0
  194. package/dist/cli/services/storage-service.js +71 -0
  195. package/dist/cli/services/vpn-service.d.ts +6 -4
  196. package/dist/cli/services/vpn-service.js +5 -11
  197. package/dist/cli/services/web-service.d.ts +4 -3
  198. package/dist/cli/services/web-service.js +12 -32
  199. package/dist/core/config/defaults.js +3 -2
  200. package/dist/core/schema/config.d.ts +6 -0
  201. package/dist/core/schema/config.js +2 -0
  202. package/dist/core/schema/github.d.ts +4 -0
  203. package/dist/core/schema/github.js +7 -0
  204. package/dist/core/schema/index.d.ts +1 -0
  205. package/dist/core/schema/index.js +1 -0
  206. package/dist/core/schema/profile.d.ts +1 -0
  207. package/dist/core/schema/profile.js +3 -0
  208. package/dist/core/schema/sandbox.js +3 -3
  209. package/dist/core/schema/toolchains.js +2 -2
  210. package/dist/core/schema/vpn.js +1 -1
  211. package/dist/core/schema/web.d.ts +1 -0
  212. package/dist/core/schema/web.js +4 -1
  213. package/dist/core/types.d.ts +6 -3
  214. package/dist/guest/boot/boot.js +11 -1
  215. package/dist/guest/daemon/daemon.js +1 -1
  216. package/dist/guest/daemon/sessions.d.ts +3 -2
  217. package/dist/guest/daemon/sessions.js +38 -7
  218. package/dist/guest/governor/core.d.ts +44 -0
  219. package/dist/guest/governor/core.js +124 -0
  220. package/dist/guest/governor/decide.d.ts +53 -15
  221. package/dist/guest/governor/decide.js +155 -72
  222. package/dist/guest/governor/governor.d.ts +2 -35
  223. package/dist/guest/governor/governor.js +35 -99
  224. package/dist/guest/governor/index.d.ts +1 -0
  225. package/dist/guest/governor/index.js +1 -0
  226. package/dist/guest/network/firewall.d.ts +3 -0
  227. package/dist/guest/network/firewall.js +65 -23
  228. package/dist/guest/runtime/process.js +53 -5
  229. package/dist/guest/sessions/viewer.js +5 -1
  230. package/dist/guest/setup/claude-mem.js +12 -4
  231. package/dist/guest/setup/github-token.d.ts +7 -0
  232. package/dist/guest/setup/github-token.js +65 -0
  233. package/dist/guest/toolchains/languages/node.js +40 -4
  234. package/dist/guest/toolchains/native/rust-extras.js +2 -2
  235. package/dist/guest/toolchains/tools/cli.d.ts +8 -0
  236. package/dist/guest/toolchains/tools/cli.js +16 -2
  237. package/dist/guest/vpn/openvpn.d.ts +5 -0
  238. package/dist/guest/vpn/openvpn.js +14 -1
  239. package/dist/guest/vpn/vpn.js +55 -16
  240. package/dist/guest/vpn/wireguard.js +2 -0
  241. package/dist/host/channel/attach.d.ts +1 -0
  242. package/dist/host/channel/attach.js +1 -0
  243. package/dist/host/channel/channel.d.ts +1 -1
  244. package/dist/host/channel/channel.js +2 -2
  245. package/dist/host/engines/hcs/check.d.ts +1 -0
  246. package/dist/host/engines/hcs/check.js +1 -1
  247. package/dist/host/engines/hcs/engine.d.ts +1 -1
  248. package/dist/host/engines/hcs/engine.js +6 -4
  249. package/dist/host/engines/hcs/helper.d.ts +11 -0
  250. package/dist/host/engines/hcs/helper.js +31 -2
  251. package/dist/host/engines/hcs/leftovers.d.ts +42 -0
  252. package/dist/host/engines/hcs/leftovers.js +101 -0
  253. package/dist/host/engines/index.d.ts +1 -0
  254. package/dist/host/engines/index.js +1 -0
  255. package/dist/host/github/deliver.d.ts +13 -0
  256. package/dist/host/github/deliver.js +51 -0
  257. package/dist/host/github/index.d.ts +2 -0
  258. package/dist/host/github/index.js +3 -0
  259. package/dist/host/github/token.d.ts +21 -0
  260. package/dist/host/github/token.js +74 -0
  261. package/dist/host/launch/generate.js +11 -5
  262. package/dist/host/launch/index.d.ts +1 -1
  263. package/dist/host/launch/index.js +1 -1
  264. package/dist/host/launch/session-file.js +1 -2
  265. package/dist/host/launch/wsb-file.d.ts +11 -7
  266. package/dist/host/launch/wsb-file.js +30 -12
  267. package/dist/host/life/host-memory.d.ts +1 -0
  268. package/dist/host/life/host-memory.js +4 -1
  269. package/dist/host/life/watchdog.d.ts +11 -0
  270. package/dist/host/life/watchdog.js +36 -4
  271. package/dist/host/sandbox/tasklist.d.ts +6 -0
  272. package/dist/host/sandbox/tasklist.js +37 -0
  273. package/dist/host/secrets/launch-secrets.d.ts +2 -1
  274. package/dist/host/secrets/launch-secrets.js +4 -2
  275. package/dist/host/vpn/status.js +13 -3
  276. package/dist/host/web/backend.d.ts +1 -3
  277. package/dist/host/web/index.d.ts +2 -1
  278. package/dist/host/web/index.js +2 -1
  279. package/dist/host/web/live.d.ts +23 -0
  280. package/dist/host/web/live.js +107 -0
  281. package/dist/host/web/rpc.d.ts +6 -1
  282. package/dist/host/web/rpc.js +23 -31
  283. package/dist/host/web/runner.d.ts +4 -0
  284. package/dist/host/web/runner.js +12 -1
  285. package/dist/host/web/server.d.ts +30 -2
  286. package/dist/host/web/server.js +87 -29
  287. package/dist/host/web/state.d.ts +1 -0
  288. package/dist/host/web/state.js +2 -0
  289. package/dist/runtime/agent.js +44 -16
  290. package/dist/runtime/daemon.js +1154 -857
  291. package/dist/runtime/db.js +44 -16
  292. package/dist/runtime/main.js +532 -115
  293. package/dist/runtime/netproxy.js +34 -9
  294. package/dist/runtime/view.js +499 -149
  295. package/dist/selftest/app.test.js +620 -12
  296. package/dist/selftest/channel.test.js +18 -1
  297. package/dist/selftest/cli.test.js +1 -1
  298. package/dist/selftest/config.test.js +1 -1
  299. package/dist/selftest/contained.test.js +1 -1
  300. package/dist/selftest/data.test.js +1 -2
  301. package/dist/selftest/engine.test.js +3 -1
  302. package/dist/selftest/github.test.d.ts +2 -0
  303. package/dist/selftest/github.test.js +243 -0
  304. package/dist/selftest/guest-boot.test.js +47 -1
  305. package/dist/selftest/hcs.test.js +105 -8
  306. package/dist/selftest/health.test.js +71 -0
  307. package/dist/selftest/heavy-load.test.js +152 -66
  308. package/dist/selftest/index.js +4 -0
  309. package/dist/selftest/layout.test.js +8 -0
  310. package/dist/selftest/live-terminal.test.js +74 -0
  311. package/dist/selftest/menu-screens.js +171 -56
  312. package/dist/selftest/menu.test.js +54 -37
  313. package/dist/selftest/migrate.test.js +14 -12
  314. package/dist/selftest/network.test.js +20 -1
  315. package/dist/selftest/powershell-variables.test.d.ts +2 -0
  316. package/dist/selftest/powershell-variables.test.js +61 -0
  317. package/dist/selftest/support/engine-golden.js +1 -0
  318. package/dist/selftest/support/fixtures.d.ts +5 -0
  319. package/dist/selftest/support/web-preview.d.ts +1 -0
  320. package/dist/selftest/support/web-preview.js +856 -0
  321. package/dist/selftest/update-service.test.js +15 -8
  322. package/dist/selftest/vpn.test.js +86 -2
  323. package/dist/selftest/web.test.js +58 -11
  324. package/dist/shared/channel/run-dir.d.ts +2 -0
  325. package/dist/shared/channel/run-dir.js +3 -0
  326. package/dist/shared/channel/schema.d.ts +7 -0
  327. package/dist/shared/channel/schema.js +9 -1
  328. package/dist/shared/channel/secrets.d.ts +1 -0
  329. package/dist/shared/channel/secrets.js +3 -0
  330. package/dist/shared/github/index.d.ts +1 -0
  331. package/dist/shared/github/index.js +2 -0
  332. package/dist/shared/github/token.d.ts +29 -0
  333. package/dist/shared/github/token.js +89 -0
  334. package/dist/shared/io/copy-tree.d.ts +7 -0
  335. package/dist/shared/io/copy-tree.js +45 -0
  336. package/dist/shared/io/index.d.ts +2 -0
  337. package/dist/shared/io/index.js +2 -0
  338. package/dist/shared/io/robocopy-result.d.ts +6 -0
  339. package/dist/shared/io/robocopy-result.js +23 -0
  340. package/dist/shared/memory/policy.d.ts +38 -12
  341. package/dist/shared/memory/policy.js +58 -19
  342. package/dist/shared/network/firewall-rules.d.ts +8 -0
  343. package/dist/shared/network/firewall-rules.js +28 -0
  344. package/dist/shared/pty/ctrl-c.d.ts +18 -0
  345. package/dist/shared/pty/ctrl-c.js +36 -0
  346. package/dist/shared/pty/index.d.ts +1 -0
  347. package/dist/shared/pty/index.js +1 -0
  348. package/dist/shared/pty/terminal.d.ts +2 -0
  349. package/dist/shared/pty/terminal.js +21 -2
  350. package/dist/shared/vpn/kill-switch.d.ts +13 -2
  351. package/dist/shared/vpn/kill-switch.js +88 -8
  352. package/dist/shared/web/protocol.d.ts +0 -4
  353. package/dist/sync/watcher.js +1 -1
  354. package/dist/web/index.html +2 -2
  355. package/dist/web/{main-T5DMCHYV.css → main-6JF3ASB5.css} +1 -1
  356. package/dist/web/main-Z645W45E.js +219 -0
  357. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  358. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  359. package/native/csharp/HostTools/MemoryStatus.cs +128 -107
  360. package/native/scripts/memory-status.ps1 +10 -2
  361. package/package.json +10 -3
  362. package/sandbox/csharp/MemGov.cs +668 -398
  363. package/sandbox/csharp/PtyHost.cs +104 -15
  364. package/sandbox/csharp/SandboxTools/CpuCap.cs +50 -1
  365. package/sandbox/csharp/SandboxTools/Program.cs +1 -1
  366. package/sandbox/scripts/new-shortcuts.ps1 +3 -1
  367. package/sandbox/scripts/repair-wmi-services.ps1 +26 -0
  368. package/schemas/config.schema.json +25 -7
  369. package/schemas/sandbox-config.schema.json +7 -3
  370. package/dist/cli/commands/accounts.d.ts +0 -2
  371. package/dist/cli/commands/accounts.js +0 -96
  372. package/dist/cli/commands/data/clean.d.ts +0 -3
  373. package/dist/cli/commands/data/clean.js +0 -60
  374. package/dist/cli/commands/data/index.d.ts +0 -4
  375. package/dist/cli/commands/data/index.js +0 -62
  376. package/dist/cli/commands/maintenance/clean.d.ts +0 -2
  377. package/dist/cli/commands/maintenance/clean.js +0 -24
  378. package/dist/cli/commands/maintenance/config-share.d.ts +0 -12
  379. package/dist/cli/commands/maintenance/config-share.js +0 -80
  380. package/dist/cli/commands/projects/projects.d.ts +0 -2
  381. package/dist/cli/commands/projects/projects.js +0 -162
  382. package/dist/cli/commands/storage/contained.d.ts +0 -3
  383. package/dist/cli/commands/storage/contained.js +0 -179
  384. package/dist/cli/commands/storage/storage.d.ts +0 -8
  385. package/dist/cli/commands/storage/storage.js +0 -179
  386. package/dist/cli/menu/add-agent.d.ts +0 -47
  387. package/dist/cli/menu/add-agent.js +0 -168
  388. package/dist/cli/menu/apps.d.ts +0 -69
  389. package/dist/cli/menu/apps.js +0 -214
  390. package/dist/cli/menu/data-folder.d.ts +0 -18
  391. package/dist/cli/menu/data-folder.js +0 -99
  392. package/dist/cli/menu/share-settings.d.ts +0 -21
  393. package/dist/cli/menu/share-settings.js +0 -112
  394. package/dist/cli/menu/vpn-login.d.ts +0 -16
  395. package/dist/cli/menu/vpn-login.js +0 -74
  396. package/dist/web/main-NRRS5O6Z.js +0 -157
  397. /package/dist/cli/{commands/data/collect.d.ts → services/data-collect.d.ts} +0 -0
  398. /package/dist/cli/{commands/data/remove.d.ts → services/data-remove.d.ts} +0 -0
@@ -3,7 +3,7 @@
3
3
  import { safeId } from '../shared/channel/index.js';
4
4
  import { GuestRequestSchema, SandboxStatusSchema, SessionStatusSchema } from '../shared/channel/index.js';
5
5
  import { bootLine, firstStartNotice, pendingDownloads, readyIn, stillWorking } from '../cli/commands/launch/index.js';
6
- import { encodeFrame, FrameReader, FrameType, fixWindowsKeys, helloFrame, InputJoiner, MAX_FRAME_BYTES, MOUSE_OFF, OutputFilter, } from '../shared/pty/index.js';
6
+ import { CTRL_C_CONFIRM_MS, CTRL_C_HINT, CtrlCGuard, encodeFrame, FrameReader, FrameType, fixWindowsKeys, helloFrame, InputJoiner, MAX_FRAME_BYTES, MOUSE_OFF, OutputFilter, } from '../shared/pty/index.js';
7
7
  import { bootSilent, FIRST_STATUS_MS } from '../host/channel/index.js';
8
8
  export const channelSuite = {
9
9
  name: 'channel',
@@ -132,6 +132,23 @@ export const channelSuite = {
132
132
  hello.payload[4] === 2 &&
133
133
  hello.payload.subarray(5).toString() === 'tok', 'a frame of exactly the maximum size is accepted; a hello frame carries size, flags and token');
134
134
  t.equal(fixWindowsKeys(Buffer.from([0x61, 0x08, 0x7f, 0x1b])), Buffer.from([0x61, 0x7f, 0x08, 0x1b]), 'Windows console Backspace (^H) reaches the agent as DEL, so it deletes one character, not a word');
135
+ t.section('Ctrl+C meant as "copy" does not reach the agent');
136
+ const ctrlC = Buffer.from([3]);
137
+ const g = new CtrlCGuard();
138
+ const first = g.push(ctrlC, 1000);
139
+ const second = g.push(ctrlC, 1000 + CTRL_C_CONFIRM_MS - 1);
140
+ const third = g.push(ctrlC, 5000);
141
+ const late = g.push(ctrlC, 5000 + CTRL_C_CONFIRM_MS + 1);
142
+ t.check(first.held &&
143
+ first.send.length === 0 &&
144
+ !second.held &&
145
+ second.send.equals(ctrlC) &&
146
+ third.held &&
147
+ late.held &&
148
+ !g.push(Buffer.from('a\x03'), 6000).held &&
149
+ !g.push(Buffer.from('x'), 6000).held, 'a lone Ctrl+C is held back, a second one soon after is sent, a late second one is held again, other input passes');
150
+ g.watchOutput('\x1b]0;claude: working\x07more \x1b]2;\u0001evil\x07');
151
+ t.check(g.hintTitle().includes(CTRL_C_HINT) && g.restoreTitle() === '\x1b]0;evil\x07', "the hint shows in the title, and the program's last title comes back without control characters");
135
152
  t.section('typed input keeps escape sequences whole');
136
153
  const joined = (...chunks) => {
137
154
  const j = new InputJoiner();
@@ -8,7 +8,7 @@ import { COMMANDS } from '../cli/manifest.js';
8
8
  import { commandLine, readHistory, recordHistory } from '../cli/history.js';
9
9
  import { splitArgs } from '../cli/args.js';
10
10
  import { optionProblem } from '../cli/menu/index.js';
11
- import { cleanTargetOf } from '../cli/commands/data/collect.js';
11
+ import { cleanTargetOf } from '../cli/services/index.js';
12
12
  import { ago, colorEnabled, shorten } from '../cli/output/index.js';
13
13
  import { versionText } from '../cli/version.js';
14
14
  import { hoistConfig, withoutConfig } from '../cli/argv.js';
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import _ from 'lodash';
4
4
  import fs from 'node:fs';
5
5
  import { DEFAULTS, loadConfig } from '../core/config/index.js';
6
- import { diffText, exportSettings, planImport } from '../cli/menu/settings/index.js';
6
+ import { diffText, exportSettings, planImport } from '../app/features/settings/settings-share.js';
7
7
  import { readJsonc } from '../core/util/index.js';
8
8
  export const configSuite = {
9
9
  name: 'config',
@@ -9,7 +9,7 @@ import { spawnSync } from 'node:child_process';
9
9
  import { assertNoGuestOverlap } from '../core/config/index.js';
10
10
  import { isWin } from '../core/util/index.js';
11
11
  import { CONTAINED_CHECKS } from '../cli/commands/repair/checks/contained.js';
12
- import { storageBusy } from '../cli/commands/storage/storage.js';
12
+ import { storageBusy } from '../cli/services/index.js';
13
13
  import { generateSandbox, GUEST } from '../host/launch/index.js';
14
14
  import { currentUserSid } from '../host/sharing/index.js';
15
15
  import { addHolder, CONTAINED_FOLDERS, containedAvailability, ContainedStorage, containedMappings, containedPaths, diagnose, hostDataFolder, markMounted, mountedSets, planTransfer, providerFor, pruneHolders, removeHolder, runTransfer, ScriptDiskPort, setNameOf, setStateOf, staleMountNote, StateFile, } from '../host/storage/index.js';
@@ -2,9 +2,8 @@
2
2
  // launch folders, logs and downloads are pruned.
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { resolveCleanTargets } from '../cli/commands/data/clean.js';
6
- import { removeTree } from '../cli/commands/data/remove.js';
7
5
  import { findLinks } from '../cli/commands/repair/finders.js';
6
+ import { removeTree, resolveCleanTargets } from '../cli/services/index.js';
8
7
  import { humanBytes, sizeOf } from '../core/disk.js';
9
8
  import { pruneStorage } from '../host/storage/index.js';
10
9
  export const dataSuite = {
@@ -66,7 +66,9 @@ export const engineSuite = {
66
66
  t.equal(fs.readFileSync(gen.wsbPath, 'utf8'), legacyWsb(f.baseConfig, gen.memoryMb, gen.mappings), 'the launch writes the .wsb the old writer would have (same settings, memory and mappings)');
67
67
  t.equal(path.basename(gen.wsbPath), 'agent.wsb', 'the engine file is still agent.wsb in the launch folder');
68
68
  t.equal(gen.enginePlan.entry, gen.wsbPath, 'the engine plan starts from that file');
69
- t.equal(filesUnder(gen.launchPath), [...LAUNCH_FILES_GOLDEN], 'the launch folder has exactly the files it had before');
69
+ // home/.gitconfig is written only when this machine's ~/.gitconfig has a name or email (a GitHub runner has none).
70
+ const sameOnEveryMachine = (f) => f !== 'home/.gitconfig';
71
+ t.equal(filesUnder(gen.launchPath).filter(sameOnEveryMachine), LAUNCH_FILES_GOLDEN.filter(sameOnEveryMachine), 'the launch folder has exactly the files it had before');
70
72
  t.equal(gen.mappings.find((m) => m.guest === 'C:\\AgentData')?.host, f.baseConfig.storage.root + '\\data\\testdata', 'the data folder is still mapped at C:\\AgentData (through the folder provider)');
71
73
  t.section('capabilities');
72
74
  const caps = (over, cli) => new WindowsSandboxEngine({ ...DEFAULTS.sandbox, ...over }, () => cli).capabilities();
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './support/suite.js';
2
+ export declare const githubSuite: Suite;
@@ -0,0 +1,243 @@
1
+ // The GitHub token for sandboxes: which tokens are accepted, the sealed store, when a launch hands it over (only in the
2
+ // one-shot secrets file), the shared-core operations, the project-file rules, and the gh commands the sandbox runs.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { deleteGithubTokenFlow, githubPage, githubTokenRowOf, saveGithubTokenFlow, } from '../app/features/github/index.js';
6
+ import { fakeIO } from '../app/kernel/index.js';
7
+ import { runDefinition } from '../app/registry.js';
8
+ import { pageLines } from '../cli/output/index.js';
9
+ import { DEFAULTS } from '../core/config/index.js';
10
+ import { applyOverrides, normalizeOverrides } from '../core/local-config/index.js';
11
+ import { ConfigSchema, LocalOverridesSchema } from '../core/schema/index.js';
12
+ import { checkGithubToken, GithubToken, githubTokenAllowed, prepareGithubToken } from '../host/github/index.js';
13
+ import { generateSandbox } from '../host/launch/index.js';
14
+ import { insideSandbox } from '../host/sandbox/platform.js';
15
+ import { clearSecrets, SecretStore } from '../host/secrets/index.js';
16
+ import { RunDir } from '../shared/channel/index.js';
17
+ import { SecretsFileSchema } from '../shared/channel/secrets.js';
18
+ import { ghLoginCommand, ghOutputText, ghSetupGitCommand, githubTokenProblem } from '../shared/github/index.js';
19
+ const FINE = 'github_pat_' + 'PlantedFine0123456789'.repeat(3);
20
+ const CLASSIC = 'ghp_' + 'PlantedClassic0123456789ab';
21
+ const OAUTH = 'gho_' + 'PlantedOauth0123456789abcd';
22
+ /** Every file under a folder, as text. */
23
+ function allText(dir) {
24
+ const out = [];
25
+ const walk = (d) => {
26
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
27
+ const p = path.join(d, e.name);
28
+ if (e.isDirectory())
29
+ walk(p);
30
+ else
31
+ out.push(fs.readFileSync(p, 'latin1'));
32
+ }
33
+ };
34
+ walk(dir);
35
+ return out;
36
+ }
37
+ const message = async (work) => {
38
+ try {
39
+ await work();
40
+ return '';
41
+ }
42
+ catch (e) {
43
+ const err = e;
44
+ return err.message + ' ' + (err.fix ?? '');
45
+ }
46
+ };
47
+ export const githubSuite = {
48
+ name: 'github',
49
+ title: 'GitHub token: accepted tokens, sealed store, delivery only in the one-shot file, operations, gh on stdin',
50
+ run: async (t, f) => {
51
+ t.section('which tokens are accepted');
52
+ t.check(checkGithubToken(FINE) === 'fine-grained', 'a fine-grained token (github_pat_) is accepted');
53
+ t.check(checkGithubToken(CLASSIC) === 'classic', 'a classic token (ghp_) is accepted');
54
+ for (const [bad, why, expect] of [
55
+ [OAUTH, 'an OAuth token from gh auth token', 'OAuth'],
56
+ ['not-a-token-' + 'x'.repeat(20), 'text that is not a token', 'github_pat_'],
57
+ ['github_pat_ ' + 'x'.repeat(30), 'a token with a space', 'spaces'],
58
+ ['github_pat_\n' + 'x'.repeat(30), 'a token with a line break', 'line breaks'],
59
+ ['ghp_short', 'a token too short', 'length'],
60
+ ['github_pat_' + 'x'.repeat(300), 'a token too long', 'length'],
61
+ ]) {
62
+ const text = await message(() => checkGithubToken(bad));
63
+ t.check(text.includes(expect) && !text.includes(bad) && !text.includes(bad.slice(4, 24)), why + ' is refused, and the refusal does not repeat it', text);
64
+ }
65
+ t.check((githubTokenProblem(OAUTH)?.fix ?? '').includes('personal-access-tokens/new'), 'a refused OAuth token says to create a fine-grained token instead');
66
+ t.check(SecretsFileSchema.safeParse({ githubToken: FINE }).success &&
67
+ !SecretsFileSchema.safeParse({ githubToken: 'a b' }).success &&
68
+ !SecretsFileSchema.safeParse({ githubToken: 'x'.repeat(256) }).success, 'the secrets file holds the token with the same limits');
69
+ t.section('the saved token');
70
+ const storage = t.dir('github-storage');
71
+ const store = new GithubToken(storage, SecretStore.memory());
72
+ t.check(store.state() === 'none' && store.kind() === null, 'nothing is saved at first');
73
+ await t.throws(() => store.save(OAUTH), 'OAuth', 'an OAuth token is not saved');
74
+ t.check(store.state() === 'none', 'and nothing was written');
75
+ t.check(store.save(FINE) === 'fine-grained', 'a fine-grained token is saved');
76
+ const file = path.join(storage, 'github', 'token.json');
77
+ t.check(store.state() === 'saved' && store.kind() === 'fine-grained' && !fs.readFileSync(file, 'utf8').includes(FINE), 'it is saved sealed: the token is not in the file');
78
+ t.check(store.read() === FINE, 'and it opens for the launch');
79
+ fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace(/"sealed": "[^"]*"/, '"sealed": "dpapi1:AAAA"'));
80
+ t.check(store.state() === 'unreadable', 'a token this Windows user cannot open is reported as unreadable');
81
+ t.check(store.remove() && store.state() === 'none' && !store.remove(), 'logout deletes it (once)');
82
+ t.section('when a launch hands it over');
83
+ const config = f.makeConfig();
84
+ const tokens = new GithubToken(config.storage.root, SecretStore.memory());
85
+ t.check(DEFAULTS.github.shareToken === false, 'sharing is off by default');
86
+ t.equal(prepareGithubToken(config, config.storage.root, tokens), { notes: [] }, 'off: nothing, and nothing said');
87
+ config.github.shareToken = true;
88
+ const none = prepareGithubToken(config, config.storage.root, tokens);
89
+ t.check(none.token === undefined && none.notes.some((n) => n.includes('sandbox github login')), 'on without a saved token: not sent, and it says how to save one');
90
+ tokens.save(FINE);
91
+ for (const access of ['readonly', 'none']) {
92
+ const strict = structuredClone(config);
93
+ strict.provisioning.git.access = access;
94
+ const d = prepareGithubToken(strict, config.storage.root, tokens);
95
+ t.check(d.token === undefined && !githubTokenAllowed(strict) && d.notes.some((n) => n.includes('"' + access + '"')), 'git access ' + access + ': not sent, with a note why');
96
+ }
97
+ const unreadable = new GithubToken(t.dir('github-unreadable'), SecretStore.memory());
98
+ unreadable.save(FINE);
99
+ const uFile = path.join(t.at('github-unreadable'), 'github', 'token.json');
100
+ fs.writeFileSync(uFile, fs.readFileSync(uFile, 'utf8').replace(/"sealed": "[^"]*"/, '"sealed": "dpapi1:AAAA"'));
101
+ const u = prepareGithubToken(config, config.storage.root, unreadable);
102
+ t.check(u.token === undefined && u.notes.some((n) => n.includes('could not be read')), 'a token that cannot be decrypted is a warning, not a crash');
103
+ const all = prepareGithubToken(config, config.storage.root, tokens);
104
+ t.check(all.token === FINE && all.notes.every((n) => !n.includes(FINE)) && all.notes.some((n) => n.includes('can read')), 'all on: the token is handed over, the notes never hold it and say the agent can read it');
105
+ const run = await f.run({}, config);
106
+ clearSecrets(config.storage.root, run.dataPath);
107
+ const launched = generateSandbox({
108
+ packageRoot: t.packageRoot,
109
+ run,
110
+ harnessArgs: [],
111
+ probe: f.probe,
112
+ pcLanguages: ['python', 'rust', 'cpp', 'dotnet'],
113
+ githubToken: FINE,
114
+ });
115
+ const session = fs.readFileSync(launched.sessionPath, 'utf8');
116
+ t.check(!session.includes(FINE) && !allText(launched.launchPath).some((text) => text.includes(FINE)), 'the token is in neither session.json nor any file of the launch folder');
117
+ const runDir = new RunDir(run.dataPath);
118
+ const handed = f.readJson(runDir.secretFile(launched.launchId));
119
+ t.check(handed.githubToken === FINE, 'it is only in the one-shot file for the sandbox');
120
+ const runFolder = path.join(run.dataPath, 'run');
121
+ t.check(!fs.existsSync(runFolder) || !allText(runFolder).some((text) => text.includes(FINE)), 'and not in run\\');
122
+ clearSecrets(config.storage.root, run.dataPath);
123
+ const readonlyConfig = structuredClone(config);
124
+ readonlyConfig.provisioning.git.access = 'readonly';
125
+ const readonlyRun = await f.run({}, readonlyConfig);
126
+ const strictLaunch = generateSandbox({
127
+ packageRoot: t.packageRoot,
128
+ run: readonlyRun,
129
+ harnessArgs: [],
130
+ probe: f.probe,
131
+ pcLanguages: [],
132
+ githubToken: FINE,
133
+ });
134
+ const handedTo = (id) => fs.existsSync(runDir.secretFile(id)) ? f.readJson(runDir.secretFile(id)) : {};
135
+ t.check(handedTo(strictLaunch.launchId).githubToken === undefined, 'generateSandbox checks again: a readonly sandbox never gets it, whatever it was given');
136
+ clearSecrets(config.storage.root, run.dataPath);
137
+ const plain = generateSandbox({
138
+ packageRoot: t.packageRoot,
139
+ run: await f.run({ noAgent: true }, config),
140
+ harnessArgs: [],
141
+ probe: f.probe,
142
+ pcLanguages: [],
143
+ githubToken: FINE,
144
+ });
145
+ t.check(handedTo(plain.launchId).githubToken === FINE, 'a sandbox without an agent (no other secret to hand over) still gets the one-shot file with the token');
146
+ clearSecrets(config.storage.root, run.dataPath);
147
+ const strictOther = structuredClone(config);
148
+ strictOther.provisioning.git.access = 'readonly';
149
+ const others = [{ name: 'other-project', config: strictOther }];
150
+ const held = prepareGithubToken(config, config.storage.root, tokens, others);
151
+ t.check(held.token === undefined &&
152
+ !githubTokenAllowed(config, others) &&
153
+ held.notes.some((n) => n.includes('other-project')), 'another project mapped at boot with readonly git access holds the token back, and the note names it');
154
+ const withOther = generateSandbox({
155
+ packageRoot: t.packageRoot,
156
+ run: await f.run({ noAgent: true }, config),
157
+ harnessArgs: [],
158
+ probe: f.probe,
159
+ pcLanguages: [],
160
+ githubToken: FINE,
161
+ bootWants: { projects: others, keepOff: [] },
162
+ });
163
+ t.check(handedTo(withOther.launchId).githubToken === undefined, 'and generateSandbox checks the projects at boot again');
164
+ clearSecrets(config.storage.root, run.dataPath);
165
+ t.section("settings: a project's file cannot turn it on");
166
+ t.check(ConfigSchema.safeParse(DEFAULTS).success, 'the defaults are valid');
167
+ t.check(!ConfigSchema.safeParse({ ...DEFAULTS, github: { shareToken: true, token: FINE } }).success, 'there is no setting that holds a token');
168
+ t.check(!LocalOverridesSchema.safeParse({ github: { shareToken: true } }).success &&
169
+ !LocalOverridesSchema.safeParse({ github: { shareToken: false } }).success, "a project's .sandbox-config.json cannot set github at all");
170
+ await t.throws(() => normalizeOverrides({ github: { shareToken: true } }, f.workA, '.sandbox-config.json'), 'not valid', 'such a file is refused as invalid');
171
+ const mine = structuredClone(DEFAULTS);
172
+ t.check(applyOverrides(mine, normalizeOverrides({ readOnly: true }, f.workA, 'x')).github.shareToken === false, 'and other project settings leave it as you set it');
173
+ t.section('the shared-core operations');
174
+ const featureStore = new GithubToken(t.dir('github-feature'), SecretStore.memory());
175
+ const { io: loginIO, seen: loginSeen } = fakeIO([FINE]);
176
+ const saved = await saveGithubTokenFlow(loginIO, featureStore);
177
+ t.check(loginSeen.questions.length === 1 &&
178
+ loginSeen.questions[0].kind === 'secret' &&
179
+ featureStore.state() === 'saved' &&
180
+ loginSeen.logs.some((l) => l.text.includes('personal-access-tokens/new')) &&
181
+ loginSeen.logs.some((l) => l.text.includes('agent in the sandbox can read')), 'login shows the steps, asks exactly one hidden question, saves, and warns the agent can read it', JSON.stringify(loginSeen.questions));
182
+ t.check(!JSON.stringify(saved).includes(FINE) && !JSON.stringify(loginSeen).includes(FINE.slice(12)), 'nothing it returns or logs holds the token');
183
+ const { io: retryIO, seen: retrySeen } = fakeIO([OAUTH, CLASSIC]);
184
+ await saveGithubTokenFlow(retryIO, featureStore);
185
+ t.check(retrySeen.questions.length === 2 &&
186
+ featureStore.kind() === 'classic' &&
187
+ !JSON.stringify(retrySeen).includes(OAUTH.slice(4)) &&
188
+ retrySeen.logs.some((l) => l.text.includes('classic')), 'a refused token is asked again (the refusal never repeats it), and a classic one is saved with a warning');
189
+ const status = (await runDefinition('github.status', {
190
+ storageRoot: t.dir('github-status'),
191
+ dataPath: t.at('github-status', 'data'),
192
+ loaded: { config: { ...DEFAULTS, github: { shareToken: true } } },
193
+ }, {}, fakeIO().io));
194
+ t.check(status.shareToken && status.token === 'none' && status.kind === null, 'status reads the setting');
195
+ const sample = {
196
+ shareToken: true,
197
+ gitAccess: 'readonly',
198
+ token: 'saved',
199
+ kind: 'classic',
200
+ savedAt: '2026-10-18T10:00:00.000Z',
201
+ };
202
+ const lines = pageLines(githubPage(sample)).join('\n');
203
+ t.check(lines.includes('saved (encrypted)') &&
204
+ lines.includes('only handed over with "full"') &&
205
+ lines.includes('classic'), 'the status page says saved, the git access it needs, and the kind', lines);
206
+ t.equal(githubTokenRowOf({ ...sample, token: 'none' }).hint, 'not saved', 'the Settings row says not saved');
207
+ const { io: logoutIO, seen: logoutSeen } = fakeIO([true]);
208
+ const out = await deleteGithubTokenFlow(logoutIO, featureStore);
209
+ t.check(out.deleted &&
210
+ logoutSeen.questions.length === 1 &&
211
+ logoutSeen.questions[0].kind === 'confirm' &&
212
+ featureStore.state() === 'none', 'logout asks to confirm, then deletes');
213
+ const { io: noneIO, seen: noneSeen } = fakeIO();
214
+ t.check(!(await deleteGithubTokenFlow(noneIO, featureStore)).deleted && noneSeen.questions.length === 0, 'logout with nothing saved asks nothing');
215
+ const { io: inputIO, seen: inputSeen } = fakeIO();
216
+ await t.throws(() => runDefinition('github.login', {}, { token: FINE }, inputIO), 'does not work', 'the token is never accepted as an input');
217
+ t.check(inputSeen.questions.length === 0, 'a token passed on the command line asks nothing');
218
+ if (insideSandbox()) {
219
+ const { io, seen } = fakeIO();
220
+ await t.throws(() => runDefinition('github.login', {}, {}, io), 'inside the sandbox already', 'github login refuses inside the sandbox');
221
+ t.check(seen.questions.length === 0, 'and asks no question first');
222
+ }
223
+ t.section('gh in the sandbox: the token on stdin only');
224
+ const base = {
225
+ PATH: 'C:\\x',
226
+ GH_TOKEN: 'other',
227
+ github_token: 'other2',
228
+ GH_CONFIG_DIR: 'C:\\AgentData\\state\\gh',
229
+ };
230
+ const login = ghLoginCommand('C:\\tools\\gh.exe', 'C:\\nvm\\gh-token', FINE, base);
231
+ t.check(login.input === FINE + '\n' &&
232
+ !login.args.some((a) => a.includes(FINE)) &&
233
+ login.args.includes('--with-token') &&
234
+ !Object.values(login.env).some((v) => v.includes(FINE)), 'gh auth login gets the token on stdin, never in its arguments or environment', JSON.stringify(login.args));
235
+ t.check(login.env.GH_CONFIG_DIR === 'C:\\nvm\\gh-token' &&
236
+ !('GH_TOKEN' in login.env) &&
237
+ !('github_token' in login.env) &&
238
+ login.env.PATH === 'C:\\x', 'it uses the sandbox-only folder, and other tokens in the environment are dropped (gh would prefer them)');
239
+ const setup = ghSetupGitCommand('C:\\tools\\gh.exe', 'C:\\nvm\\gh-token', base);
240
+ t.check(setup.input === undefined && setup.args.join(' ') === 'auth setup-git --hostname github.com', 'setup-git gets nothing on stdin');
241
+ t.check(!ghOutputText('error: bad credentials ' + FINE, FINE).includes(FINE), 'what gh prints is logged with the token blanked out');
242
+ },
243
+ };
@@ -2,7 +2,10 @@
2
2
  // what it copies in, and what it refuses to.
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { readJsonFile } from '../shared/io/index.js';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { copyTreeTolerant, readJsonFile, robocopyErrors, robocopyFailed, robocopyMeaning } from '../shared/io/index.js';
7
+ import { scriptArgs } from '../shared/scripts.js';
8
+ import { SYSTEM } from '../shared/system.js';
6
9
  export const guestBootSuite = {
7
10
  name: 'guest-boot',
8
11
  title: 'guest boot in dry-run mode (provisioning, Claude state, git policy)',
@@ -92,5 +95,48 @@ export const guestBootSuite = {
92
95
  const genNoAgent = await f.launch({ noAgent: true });
93
96
  const noAgent = await f.runGuest(genNoAgent.launchPath, t.at('data5'), f.workA);
94
97
  t.check(noAgent.status === 0 && noAgent.out.includes('no agent requested'), '--no-agent provisions without an agent', noAgent.out);
98
+ t.section('robocopy results, vanished files, shortcuts');
99
+ t.check([0, 1, 2, 3, 7].every((c) => !robocopyFailed(c)) &&
100
+ [8, 9, 16].every((c) => robocopyFailed(c)) &&
101
+ robocopyFailed(null), 'robocopy: exit codes below 8 are success, 8 and up (9 = some copied, some failed) are failures');
102
+ t.includes(robocopyMeaning(9), 'some files were copied and some could not be', 'robocopy: exit 9 is explained');
103
+ t.equal(robocopyErrors('x\r\n2026/10/11 ERROR 32 (0x00000020) Copying File C:\\a\\b.dll\r\nThe process cannot access the file because it is being used by another process.\r\n', 5), [
104
+ '2026/10/11 ERROR 32 (0x00000020) Copying File C:\\a\\b.dll',
105
+ 'The process cannot access the file because it is being used by another process.',
106
+ ], 'robocopy: the failing file and the reason are picked out of its output');
107
+ const spool = t.dir('copy-tree', 'live');
108
+ t.write(path.join(spool, 'a.json'), '1');
109
+ t.write(path.join(spool, 'b.json'), '2');
110
+ t.write(path.join(spool, 'db', 'chroma.sqlite3'), 'db');
111
+ t.write(path.join(spool, 'db', 'chroma.sqlite3-journal'), 'j');
112
+ t.write(path.join(spool, 'x.lock'), 'l');
113
+ const copied = copyTreeTolerant(spool, t.at('copy-tree', 'out'), (name) => /\.lock$|-journal$/.test(name));
114
+ t.check(copied.copied === 3 && !fs.existsSync(t.at('copy-tree', 'out', 'db', 'chroma.sqlite3-journal')), 'copy tree: files are copied, a -journal and a lock file are left out');
115
+ // The first file of the listing deletes the second before it is reached: the second has vanished, not failed.
116
+ const racing = t.dir('copy-tree', 'racing');
117
+ t.write(path.join(racing, 'a.json'), '1');
118
+ t.write(path.join(racing, 'b.json'), '2');
119
+ const result = copyTreeTolerant(racing, t.at('copy-tree', 'racing-out'), (name) => {
120
+ if (name === 'a.json')
121
+ fs.rmSync(path.join(racing, 'b.json'), { force: true });
122
+ return false;
123
+ });
124
+ t.check(result.copied === 1 && result.vanished === 1, 'copy tree: a file that vanishes during the copy is skipped, not an error');
125
+ t.equal(copyTreeTolerant(t.at('copy-tree', 'nothing'), t.at('copy-tree', 'nothing-out')).vanished, 1, 'copy tree: a missing folder is tolerated');
126
+ if (process.platform === 'win32') {
127
+ // Windows PowerShell 5.1 hands a JSON array over as one object: two or more shortcuts used to fail.
128
+ const out = t.dir('shortcuts');
129
+ const list = t.writeJson(path.join(out, 'in.json'), [
130
+ { link: path.join(out, 'one', 'A.lnk'), target: path.join(process.env.WINDIR ?? 'C:\\Windows', 'notepad.exe') },
131
+ { link: path.join(out, 'two', 'B.lnk'), target: path.join(process.env.WINDIR ?? 'C:\\Windows', 'notepad.exe') },
132
+ ]);
133
+ const made = spawnSync(SYSTEM.powershell(), scriptArgs('new-shortcuts.ps1', ['-ListFile', list]), {
134
+ encoding: 'utf8',
135
+ windowsHide: true,
136
+ });
137
+ t.check(made.status === 0 &&
138
+ fs.existsSync(path.join(out, 'one', 'A.lnk')) &&
139
+ fs.existsSync(path.join(out, 'two', 'B.lnk')), 'new-shortcuts.ps1 makes every shortcut of a list of two', String(made.stderr));
140
+ }
95
141
  },
96
142
  };
@@ -4,13 +4,15 @@
4
4
  import crypto from 'node:crypto';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
+ import { cleanEngineRun, engineLeftoversRun } from '../app/features/engine/index.js';
8
+ import { fakeIO } from '../app/kernel/index.js';
7
9
  import { DEFAULTS } from '../core/config/index.js';
8
10
  import { isWin } from '../core/util/index.js';
9
11
  import { HCS_CHECKS, HCS_PROBES } from '../cli/commands/repair/checks/hcs.js';
10
- import { checkHcs, engineFor, HCS_CAPABILITIES, HcsEngine, openEngine, } from '../host/engines/index.js';
12
+ import { checkHcs, engineFor, HCS_CAPABILITIES, HcsEngine, LEFTOVER_DEPS, lookForLeftovers, openEngine, stopLeftovers, } from '../host/engines/index.js';
11
13
  import { parseWhoamiGroups } from '../host/engines/hcs/check.js';
12
14
  import { buildDocuments, cpuCount, layerGuid } from '../host/engines/hcs/documents.js';
13
- import { HcsHelper, HcsReplySchema, buildHcsHostTo } from '../host/engines/hcs/helper.js';
15
+ import { HcsHelper, HcsReplySchema, buildHcsHostTo, userSystemPrefix, } from '../host/engines/hcs/helper.js';
14
16
  import { chainIds, DockerImageStore, FolderImageStore } from '../host/engines/hcs/images.js';
15
17
  import { SYSTEM } from '../shared/system.js';
16
18
  import { fakeProbes, FAKE_IMAGE } from './support/hcs-probes.js';
@@ -378,16 +380,18 @@ export const hcsSuite = {
378
380
  });
379
381
  for (const file of plan.files)
380
382
  t.write(file.path, file.text);
383
+ const mineId = userSystemPrefix() + '1';
381
384
  const running = await live.start(plan);
382
385
  t.equal(log, [
383
- 'grant-vm-access agent-sandbox-1 C:\\S\\images\\set.vhdx',
384
- 'grant-vm-access agent-sandbox-1 ' + path.join(launch.launchPath, 'hcs', 'scratch.vhdx'),
386
+ 'grant-vm-access ' + mineId + ' C:\\S\\images\\set.vhdx',
387
+ 'grant-vm-access ' + mineId + ' ' + path.join(launch.launchPath, 'hcs', 'scratch.vhdx'),
385
388
  'create <request>',
386
- 'start agent-sandbox-1',
389
+ 'start ' + mineId,
387
390
  'create <request>',
388
- 'start agent-sandbox-1-c',
391
+ 'start ' + mineId + '-c',
389
392
  ], 'disks get access, the utility VM is created and started, then the container');
390
- t.check(running.id === 'agent-sandbox-1' && running.engine === 'hcs', 'start() reports the sandbox');
393
+ t.check(running.id === mineId && running.engine === 'hcs', 'start() reports the sandbox');
394
+ t.check(mineId !== userSystemPrefix('someone-else', 'PC') + '1', "the id carries this Windows user's tag (another user's sandbox of the same name gets another id)");
391
395
  t.section('the helper: private temp folder, checked answers');
392
396
  const tempRoot = t.dir('hcs-helper-checks');
393
397
  const answers = [];
@@ -416,6 +420,99 @@ export const hcsSuite = {
416
420
  HCS_PROBES.current = null;
417
421
  t.check(finding.status === 'needs-you' && /docker pull/.test(finding.fix ?? ''), 'the row says what is missing and the command, and repair has no automatic fix for it');
418
422
  t.check(row.fix === undefined, 'repair never fixes HCS prerequisites (they change Windows)');
423
+ t.section('HCS systems left running: only ours are listed and stopped (fake helper)');
424
+ const mine = userSystemPrefix() + '1';
425
+ const theirs = userSystemPrefix('someone-else', 'PC') + '1';
426
+ const systems = [
427
+ {
428
+ Id: mine,
429
+ SystemType: 'VirtualMachine',
430
+ Owner: 'agent-sandbox',
431
+ State: 'Running',
432
+ CreateTimestamp: '2026-01-01T10:00:00Z',
433
+ },
434
+ { Id: mine + '-c', SystemType: 'Container', Owner: 'agent-sandbox', State: 'Running' },
435
+ { Id: theirs, SystemType: 'VirtualMachine', Owner: 'agent-sandbox', State: 'Running' },
436
+ { Id: 'WindowsSandbox-9', SystemType: 'VirtualMachine', Owner: 'agent-sandbox', State: 'Running' },
437
+ { Id: 'agent-sandbox-evil', SystemType: 'VirtualMachine', Owner: 'docker', State: 'Running' },
438
+ { Id: 'docker-1', SystemType: 'Container', Owner: 'docker', State: 'Running' },
439
+ ];
440
+ const calls = [];
441
+ let present = [...systems];
442
+ const leftoverRun = (_exe, args) => {
443
+ const verb = args[0];
444
+ calls.push(args.slice(0, args.indexOf('--out')).join(' '));
445
+ if (verb === 'terminate')
446
+ present = present.filter((s) => s.Id !== args[1]);
447
+ const json = verb === 'enumerate' ? JSON.stringify(present) : '';
448
+ fs.writeFileSync(args[args.indexOf('--out') + 1], JSON.stringify({ ok: true, hresult: '0x0', message: 'ok', json, values: {} }), 'utf8');
449
+ return { status: 0 };
450
+ };
451
+ const leftoverTemp = t.dir('hcs-leftovers-temp');
452
+ const helperCalls = [];
453
+ const useLeftovers = (probes = fakeProbes()) => {
454
+ LEFTOVER_DEPS.current = {
455
+ probes,
456
+ helper: () => {
457
+ helperCalls.push(true);
458
+ return new HcsHelper(() => 'C:\\fake\\HcsHost.exe', leftoverRun, leftoverTemp);
459
+ },
460
+ };
461
+ };
462
+ const ctx = { loaded: { config: hcsConfig } };
463
+ try {
464
+ useLeftovers();
465
+ const look = lookForLeftovers('C:\\S');
466
+ t.check(look.available &&
467
+ look.systems.map((s) => s.id).join() === mine + ',' + mine + '-c' &&
468
+ look.systems[0].kind === 'VirtualMachine' &&
469
+ look.systems[0].since === '2026-01-01T10:00:00Z' &&
470
+ look.systems[1].since === null, "only systems with our Owner and this user's id prefix are listed (not another user's, Windows Sandbox, Docker, or a foreign owner)", JSON.stringify(look));
471
+ const query = await engineLeftoversRun.run(ctx, {}, fakeIO().io);
472
+ t.check(query.available && query.systems.length === 2 && /2 HCS systems/.test(query.message), 'engine.leftovers returns them with a plain message');
473
+ calls.length = 0;
474
+ const outcome = stopLeftovers('C:\\S', [mine, mine + '-c', theirs, 'WindowsSandbox-9', 'docker-1', '--evil']);
475
+ t.equal(calls.filter((c) => c.startsWith('terminate')), ['terminate ' + mine + '-c', 'terminate ' + mine], 'only ours reach terminate, the container before its utility VM');
476
+ t.check(outcome.filter((o) => o.stopped).length === 2 && outcome.filter((o) => !o.stopped).length === 4, 'ids that are not ours (or not running) are reported, never terminated');
477
+ present = [...systems];
478
+ calls.length = 0;
479
+ const declined = fakeIO([false]);
480
+ await t.throws(() => cleanEngineRun.run(ctx, {}, declined.io), 'Nothing changed', 'declining the question stops cleanly');
481
+ t.check(!calls.some((c) => c.startsWith('terminate')), 'declining terminates nothing');
482
+ calls.length = 0;
483
+ const yes = fakeIO([true]);
484
+ const cleaned = await cleanEngineRun.run(ctx, {}, yes.io);
485
+ t.check(cleaned.stopped === 2 &&
486
+ cleaned.failed === 0 &&
487
+ yes.seen.questions.length === 1 &&
488
+ yes.seen.questions[0].kind === 'confirm' &&
489
+ calls.filter((c) => c.startsWith('terminate')).length === 2, 'engine.clean asks once, then stops what is ours');
490
+ const again = await cleanEngineRun.run(ctx, {}, fakeIO().io);
491
+ t.check(again.stopped === 0 && /Nothing to clean/.test(again.message), 'with none left it asks nothing and says so');
492
+ calls.length = 0;
493
+ helperCalls.length = 0;
494
+ useLeftovers(fakeProbes({ computecore: false }));
495
+ const missing = await cleanEngineRun.run(ctx, {}, fakeIO().io);
496
+ t.check(!missing.available && /not on this PC/.test(missing.message) && calls.length === 0 && helperCalls.length === 0, 'HCS missing: a plain message, the helper is never started, nothing to clean');
497
+ t.section('doctor row: HCS systems left running');
498
+ present = [...systems];
499
+ useLeftovers();
500
+ const leftoverRow = HCS_CHECKS[1];
501
+ t.check(leftoverRow.name === 'HCS systems left running' &&
502
+ leftoverRow.applies?.(rowEnv(f.baseConfig)) === true &&
503
+ helperCalls.every(Boolean), 'shown on any engine when one of ours is found, and the doctor never builds HcsHost.exe to look');
504
+ const warn = await leftoverRow.run(rowEnv(f.baseConfig));
505
+ t.check(warn.status === 'warn' && warn.fix === 'sandbox engine clean' && warn.detail.includes(mine), 'a warning with the fix "sandbox engine clean"');
506
+ present = [];
507
+ t.check(leftoverRow.applies?.(rowEnv(f.baseConfig)) === false &&
508
+ leftoverRow.applies?.(rowEnv(hcsConfig)) === true &&
509
+ (await leftoverRow.run(rowEnv(hcsConfig))).status === 'ok', 'hidden when none is left (unless the engine is hcs, where it says none is running)');
510
+ useLeftovers(fakeProbes({ computecore: false }));
511
+ t.check(leftoverRow.applies?.(rowEnv(f.baseConfig)) === false, 'hidden on a PC without HCS');
512
+ }
513
+ finally {
514
+ LEFTOVER_DEPS.current = null;
515
+ }
419
516
  t.section('sandbox engine check hcs');
420
517
  const out = await f.runBin('agent-sandbox', ['engine', 'check', 'hcs', '--config', t.writeJson(t.at('hcs-config.json'), {})], {
421
518
  cwd: f.workA,
@@ -425,7 +522,7 @@ export const hcsSuite = {
425
522
  (out.status === 0 || out.out.includes('To fix:')), 'the command prints the check in plain words with the fixes', out.out);
426
523
  t.check(DEFAULTS.sandbox.engine === 'windows-sandbox', 'the default engine is unchanged');
427
524
  t.section("documents against Microsoft's published HCS schema (Windows 11 21H2 and Windows 10 1809)");
428
- const schemaDir = path.join(t.packageRoot, 'test-fixtures', 'hcs-schema');
525
+ const schemaDir = path.join(t.packageRoot, 'src', 'selftest', 'fixtures', 'hcs-schema');
429
526
  if (!fs.existsSync(schemaDir)) {
430
527
  t.check(true, 'skipped: the schema fixtures are only in the repository');
431
528
  }