windows-development-sandbox 1.10.0 → 1.11.4

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 (383) hide show
  1. package/README.md +64 -17
  2. package/config.example.json +5 -1
  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 +194 -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 +959 -4
  32. package/dist/app/features/defs.js +151 -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 +297 -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 +220 -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 +224 -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 +21 -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 -89
  149. package/dist/cli/commands/web.d.ts +2 -1
  150. package/dist/cli/commands/web.js +14 -78
  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 +2 -1
  200. package/dist/core/schema/config.d.ts +5 -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/types.d.ts +6 -3
  211. package/dist/guest/boot/boot.js +8 -0
  212. package/dist/guest/daemon/daemon.js +1 -1
  213. package/dist/guest/daemon/sessions.d.ts +3 -2
  214. package/dist/guest/daemon/sessions.js +38 -7
  215. package/dist/guest/governor/core.d.ts +44 -0
  216. package/dist/guest/governor/core.js +124 -0
  217. package/dist/guest/governor/decide.d.ts +46 -18
  218. package/dist/guest/governor/decide.js +136 -82
  219. package/dist/guest/governor/governor.d.ts +2 -38
  220. package/dist/guest/governor/governor.js +33 -113
  221. package/dist/guest/governor/index.d.ts +1 -0
  222. package/dist/guest/governor/index.js +1 -0
  223. package/dist/guest/network/firewall.d.ts +2 -0
  224. package/dist/guest/network/firewall.js +3 -3
  225. package/dist/guest/sessions/viewer.js +5 -1
  226. package/dist/guest/setup/github-token.d.ts +7 -0
  227. package/dist/guest/setup/github-token.js +65 -0
  228. package/dist/guest/toolchains/languages/node.js +11 -1
  229. package/dist/guest/toolchains/native/rust-extras.js +2 -2
  230. package/dist/guest/toolchains/tools/cli.d.ts +8 -0
  231. package/dist/guest/toolchains/tools/cli.js +35 -2
  232. package/dist/guest/vpn/openvpn.d.ts +5 -0
  233. package/dist/guest/vpn/openvpn.js +14 -1
  234. package/dist/guest/vpn/vpn.js +55 -16
  235. package/dist/guest/vpn/wireguard.js +2 -0
  236. package/dist/host/channel/attach.d.ts +1 -0
  237. package/dist/host/channel/attach.js +1 -0
  238. package/dist/host/channel/channel.d.ts +1 -1
  239. package/dist/host/channel/channel.js +2 -2
  240. package/dist/host/engines/hcs/check.d.ts +1 -0
  241. package/dist/host/engines/hcs/check.js +1 -1
  242. package/dist/host/engines/hcs/engine.d.ts +1 -1
  243. package/dist/host/engines/hcs/engine.js +6 -4
  244. package/dist/host/engines/hcs/helper.d.ts +11 -0
  245. package/dist/host/engines/hcs/helper.js +31 -2
  246. package/dist/host/engines/hcs/leftovers.d.ts +42 -0
  247. package/dist/host/engines/hcs/leftovers.js +101 -0
  248. package/dist/host/engines/index.d.ts +1 -0
  249. package/dist/host/engines/index.js +1 -0
  250. package/dist/host/github/deliver.d.ts +13 -0
  251. package/dist/host/github/deliver.js +51 -0
  252. package/dist/host/github/index.d.ts +2 -0
  253. package/dist/host/github/index.js +3 -0
  254. package/dist/host/github/token.d.ts +21 -0
  255. package/dist/host/github/token.js +74 -0
  256. package/dist/host/launch/generate.js +11 -5
  257. package/dist/host/launch/index.d.ts +1 -1
  258. package/dist/host/launch/index.js +1 -1
  259. package/dist/host/launch/session-file.js +1 -2
  260. package/dist/host/launch/wsb-file.d.ts +11 -7
  261. package/dist/host/launch/wsb-file.js +30 -12
  262. package/dist/host/life/host-memory.d.ts +18 -0
  263. package/dist/host/life/host-memory.js +66 -0
  264. package/dist/host/life/watchdog.d.ts +9 -0
  265. package/dist/host/life/watchdog.js +17 -1
  266. package/dist/host/sandbox/tasklist.d.ts +6 -0
  267. package/dist/host/sandbox/tasklist.js +37 -0
  268. package/dist/host/secrets/launch-secrets.d.ts +2 -1
  269. package/dist/host/secrets/launch-secrets.js +4 -2
  270. package/dist/host/vpn/status.js +13 -3
  271. package/dist/host/web/backend.d.ts +1 -3
  272. package/dist/host/web/index.d.ts +2 -1
  273. package/dist/host/web/index.js +2 -1
  274. package/dist/host/web/live.d.ts +23 -0
  275. package/dist/host/web/live.js +107 -0
  276. package/dist/host/web/rpc.d.ts +6 -1
  277. package/dist/host/web/rpc.js +23 -31
  278. package/dist/host/web/runner.d.ts +4 -0
  279. package/dist/host/web/runner.js +12 -1
  280. package/dist/host/web/server.d.ts +30 -2
  281. package/dist/host/web/server.js +87 -29
  282. package/dist/host/web/state.d.ts +1 -0
  283. package/dist/host/web/state.js +2 -0
  284. package/dist/runtime/agent.js +30 -6
  285. package/dist/runtime/daemon.js +337 -177
  286. package/dist/runtime/db.js +30 -6
  287. package/dist/runtime/main.js +389 -77
  288. package/dist/runtime/netproxy.js +30 -6
  289. package/dist/runtime/view.js +495 -146
  290. package/dist/selftest/app.test.js +632 -12
  291. package/dist/selftest/channel.test.js +18 -1
  292. package/dist/selftest/cli.test.js +1 -1
  293. package/dist/selftest/config.test.js +1 -1
  294. package/dist/selftest/contained.test.js +1 -1
  295. package/dist/selftest/data.test.js +1 -2
  296. package/dist/selftest/engine.test.js +3 -1
  297. package/dist/selftest/github.test.d.ts +2 -0
  298. package/dist/selftest/github.test.js +243 -0
  299. package/dist/selftest/hcs.test.js +105 -8
  300. package/dist/selftest/health.test.js +71 -0
  301. package/dist/selftest/heavy-load.test.js +132 -72
  302. package/dist/selftest/index.js +4 -0
  303. package/dist/selftest/layout.test.js +8 -0
  304. package/dist/selftest/live-terminal.test.js +74 -0
  305. package/dist/selftest/menu-screens.js +171 -56
  306. package/dist/selftest/menu.test.js +54 -37
  307. package/dist/selftest/migrate.test.js +14 -12
  308. package/dist/selftest/powershell-variables.test.d.ts +2 -0
  309. package/dist/selftest/powershell-variables.test.js +61 -0
  310. package/dist/selftest/support/engine-golden.js +1 -0
  311. package/dist/selftest/support/fixtures.d.ts +4 -0
  312. package/dist/selftest/support/web-preview.d.ts +1 -0
  313. package/dist/selftest/support/web-preview.js +856 -0
  314. package/dist/selftest/update-service.test.js +15 -8
  315. package/dist/selftest/vpn.test.js +86 -2
  316. package/dist/selftest/web.test.js +58 -11
  317. package/dist/shared/channel/run-dir.d.ts +2 -0
  318. package/dist/shared/channel/run-dir.js +3 -0
  319. package/dist/shared/channel/schema.d.ts +7 -0
  320. package/dist/shared/channel/schema.js +9 -1
  321. package/dist/shared/channel/secrets.d.ts +1 -0
  322. package/dist/shared/channel/secrets.js +3 -0
  323. package/dist/shared/github/index.d.ts +1 -0
  324. package/dist/shared/github/index.js +2 -0
  325. package/dist/shared/github/token.d.ts +29 -0
  326. package/dist/shared/github/token.js +89 -0
  327. package/dist/shared/memory/policy.d.ts +33 -12
  328. package/dist/shared/memory/policy.js +50 -19
  329. package/dist/shared/pty/ctrl-c.d.ts +18 -0
  330. package/dist/shared/pty/ctrl-c.js +36 -0
  331. package/dist/shared/pty/index.d.ts +1 -0
  332. package/dist/shared/pty/index.js +1 -0
  333. package/dist/shared/pty/terminal.d.ts +2 -0
  334. package/dist/shared/pty/terminal.js +21 -2
  335. package/dist/shared/vpn/kill-switch.d.ts +13 -2
  336. package/dist/shared/vpn/kill-switch.js +88 -8
  337. package/dist/shared/web/protocol.d.ts +0 -4
  338. package/dist/sync/watcher.d.ts +2 -0
  339. package/dist/sync/watcher.js +8 -3
  340. package/dist/web/index.html +2 -2
  341. package/dist/web/{main-T5DMCHYV.css → main-MU5XWZMS.css} +1 -1
  342. package/dist/web/main-SLQNJRXD.js +219 -0
  343. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  344. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  345. package/native/csharp/HostTools/MemoryStatus.cs +58 -2
  346. package/native/csharp/HostTools/Program.cs +7 -1
  347. package/package.json +10 -3
  348. package/sandbox/csharp/MemGov.cs +240 -38
  349. package/sandbox/csharp/PtyHost.cs +104 -15
  350. package/sandbox/csharp/SandboxTools/CpuCap.cs +50 -1
  351. package/sandbox/csharp/SandboxTools/Program.cs +1 -1
  352. package/sandbox/scripts/repair-wmi-services.ps1 +26 -0
  353. package/schemas/config.schema.json +19 -5
  354. package/schemas/sandbox-config.schema.json +7 -3
  355. package/dist/cli/commands/accounts.d.ts +0 -2
  356. package/dist/cli/commands/accounts.js +0 -96
  357. package/dist/cli/commands/data/clean.d.ts +0 -3
  358. package/dist/cli/commands/data/clean.js +0 -60
  359. package/dist/cli/commands/data/index.d.ts +0 -4
  360. package/dist/cli/commands/data/index.js +0 -62
  361. package/dist/cli/commands/maintenance/clean.d.ts +0 -2
  362. package/dist/cli/commands/maintenance/clean.js +0 -24
  363. package/dist/cli/commands/maintenance/config-share.d.ts +0 -12
  364. package/dist/cli/commands/maintenance/config-share.js +0 -80
  365. package/dist/cli/commands/projects/projects.d.ts +0 -2
  366. package/dist/cli/commands/projects/projects.js +0 -162
  367. package/dist/cli/commands/storage/contained.d.ts +0 -3
  368. package/dist/cli/commands/storage/contained.js +0 -179
  369. package/dist/cli/commands/storage/storage.d.ts +0 -8
  370. package/dist/cli/commands/storage/storage.js +0 -179
  371. package/dist/cli/menu/add-agent.d.ts +0 -47
  372. package/dist/cli/menu/add-agent.js +0 -168
  373. package/dist/cli/menu/apps.d.ts +0 -69
  374. package/dist/cli/menu/apps.js +0 -214
  375. package/dist/cli/menu/data-folder.d.ts +0 -18
  376. package/dist/cli/menu/data-folder.js +0 -99
  377. package/dist/cli/menu/share-settings.d.ts +0 -21
  378. package/dist/cli/menu/share-settings.js +0 -112
  379. package/dist/cli/menu/vpn-login.d.ts +0 -16
  380. package/dist/cli/menu/vpn-login.js +0 -74
  381. package/dist/web/main-NRRS5O6Z.js +0 -157
  382. /package/dist/cli/{commands/data/collect.d.ts → services/data-collect.d.ts} +0 -0
  383. /package/dist/cli/{commands/data/remove.d.ts → services/data-remove.d.ts} +0 -0
@@ -5,7 +5,9 @@ import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { spawnSync } from 'node:child_process';
7
7
  import { parseCli } from '../cli/args.js';
8
- import { describeResult, runUpdate } from '../cli/commands/maintenance/update.js';
8
+ import { describeResult } from '../app/features/updates/update-lines.js';
9
+ import { updateWith } from '../app/features/updates/updates.run.js';
10
+ import { fakeIO } from '../app/kernel/index.js';
9
11
  import { UpdateService, runsAsSystem } from '../cli/services/index.js';
10
12
  import { InstallLayout, InstallRecordFile, UpdateTask, shimText } from '../host/install/index.js';
11
13
  import { nativeScript } from '../host/native.js';
@@ -170,7 +172,7 @@ async function failedInstall(t) {
170
172
  t.check(bad2?.kind === 'failed' &&
171
173
  bad2.autoUpdateOff &&
172
174
  new InstallRecordFile(root2).read()?.autoUpdate === false &&
173
- (await runUpdate(svc2, { check: false, rollback: false, unattended: true }, () => undefined)) === 0 &&
175
+ (await updateWith(svc2, { check: false, rollback: false, unattended: true }, fakeIO().io)).line === '' &&
174
176
  layout2.activeVersion() === '1.0.0', 'a task that cannot be removed does not stop the rollback; an unattended run with updates off does nothing');
175
177
  publish(t, feed2, '1.0.2', 'good');
176
178
  const ok2 = await svc2.run();
@@ -204,11 +206,17 @@ function pure(t) {
204
206
  async function command(t) {
205
207
  t.section('sandbox update: what it prints');
206
208
  const lines = [];
207
- const print = (line) => void lines.push(line);
209
+ // sandbox update's work through the feature: the line it returns is what the command prints.
210
+ const runU = async (svc, input) => {
211
+ const auto = input.auto ? { auto: input.auto } : {};
212
+ const out = await updateWith(svc, { check: input.check ?? false, rollback: false, unattended: false, ...auto }, fakeIO([true]).io);
213
+ lines.push(out.line);
214
+ return out.line;
215
+ };
208
216
  const npm = new UpdateService(null, { version: '1.0.0' });
209
217
  const npmAuto = await t.thrownMessage(() => npm.setAuto(true));
210
218
  t.check(!npm.managed &&
211
- (await runUpdate(npm, { check: false, rollback: false, unattended: false }, print)) === 0 &&
219
+ (await runU(npm, {})).includes('npm install -g windows-development-sandbox') &&
212
220
  lines.join('\n').includes('npm install -g windows-development-sandbox') &&
213
221
  npmAuto.includes('installed with npm'), 'an npm install is not managed, is told the npm command, and cannot turn on automatic updates', npmAuto);
214
222
  const root = t.dir('cmd', 'install');
@@ -221,17 +229,16 @@ async function command(t) {
221
229
  sandboxRunning: () => false,
222
230
  version: '1.0.0',
223
231
  });
224
- await runUpdate(machine, { check: false, rollback: false, unattended: false, auto: 'on' }, print);
232
+ await runU(machine, { auto: 'on' });
225
233
  const afterOn = [...task.calls];
226
- await runUpdate(machine, { check: false, rollback: false, unattended: false, auto: 'off' }, print);
234
+ await runU(machine, { auto: 'off' });
227
235
  t.check(afterOn.join() === 'register' && task.calls.join() === 'register,remove' && !machine.autoUpdateDue(), '--auto on/off creates and removes the scheduled task of an all-users install; it never updates from a CLI start');
228
236
  task.fail = true;
229
237
  const noAdmin = await t.thrownMessage(() => machine.setAuto(true));
230
238
  t.check(noAdmin.includes('needs admin') && new InstallRecordFile(root).read()?.autoUpdate === false, 'a task that cannot be created fails and the setting stays off', noAdmin);
231
239
  publish(t, feed, '1.0.1', 'good');
232
240
  lines.length = 0;
233
- t.check((await runUpdate(machine, { check: true, rollback: false, unattended: false }, print)) === 0 &&
234
- lines.join('\n').includes('Version 1.0.1 is available'), '--check names the newer version and exits 0');
241
+ t.check((await runU(machine, { check: true })).includes('Version 1.0.1 is available'), '--check names the newer version and exits 0');
235
242
  const waiting = { kind: 'skipped', reason: 'sandbox-running', current: '1.0.0' };
236
243
  t.check(describeResult(null, false).code === 0 &&
237
244
  describeResult(waiting, false).code === 1 &&
@@ -2,7 +2,13 @@
2
2
  // the settings rules for projects, and that the login never lands in session.json or the launch folder.
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
+ import { vpnLoginRowOf, vpnPage, VPN_WARNINGS } from '../app/features/vpn/index.js';
6
+ import { fakeIO, pageActions } from '../app/kernel/index.js';
7
+ import { runDefinition } from '../app/registry.js';
8
+ import { pageLines } from '../cli/output/index.js';
9
+ import { VpnService } from '../cli/services/index.js';
5
10
  import { DEFAULTS } from '../core/config/index.js';
11
+ import { insideSandbox } from '../host/sandbox/platform.js';
6
12
  import { applyOverrides, loosenedRequests, NEVER_LOOSER, normalizeOverrides } from '../core/local-config/index.js';
7
13
  import { ConfigSchema, LocalOverridesSchema } from '../core/schema/index.js';
8
14
  import { SecretStore } from '../host/secrets/index.js';
@@ -11,7 +17,7 @@ import { generateSandbox } from '../host/launch/index.js';
11
17
  import { findCountry, nordProfile, prepareVpn, wireguardFileProblem, recommendedServer, VpnLogin, vpnLine, vpnNotes, } from '../host/vpn/index.js';
12
18
  import { RunDir } from '../shared/channel/index.js';
13
19
  import { OPENVPN_MSI, WIREGUARD_MSI } from '../shared/vpn/download.js';
14
- import { cleanServers, killSwitchCommands, removeKillSwitchCommands, tunnelAddressFromLog, tunnelCommands, } from '../shared/vpn/kill-switch.js';
20
+ import { cleanServers, ipv4Except, killSwitchCommands, removeKillSwitchCommands, tunnelAddressFromLog, tunnelCommands, } from '../shared/vpn/kill-switch.js';
15
21
  import { assertSafeOvpn, inspectOvpn, PROGRAM_DIRECTIVES } from '../shared/vpn/ovpn.js';
16
22
  import { assertSafeWireguard, handshakeDone, inspectWireguard, renderWireguard, serviceRunning, WIREGUARD_PROGRAM_KEYS, wireguardEndpoints, wireguardTunnelAddress, } from '../shared/vpn/wireguard.js';
17
23
  const PROFILE = [
@@ -128,6 +134,32 @@ export const vpnSuite = {
128
134
  c.includes('program=C:\\AgentLaunch\\ptyhost.exe')) &&
129
135
  phase1.some((c) => c.includes('name=AgentSandbox-VPN-DHCP')), 'it allows loopback, each VPN server endpoint, the attach channel on the Hyper-V network and DHCP');
130
136
  t.check(!phase1.some((c) => c.includes('localip=') || (c.includes('action=allow') && c.includes('remoteip=any'))), 'nothing else is allowed before the tunnel exists (no blanket rule)');
137
+ const blockAt = (name) => phase1.findIndex((c) => c.includes('name=AgentSandbox-VPN-' + name + ' '));
138
+ const internet = phase1[blockAt('Block-Internet')] ?? '';
139
+ t.check(['Block-Internet', 'Block-IPv6', 'Block-DNS-UDP', 'Block-DNS-TCP'].every((name) => blockAt(name) >= 0 && blockAt(name) < policyAt && phase1[blockAt(name)].includes('action=block')), 'the kill switch is made of block rules, added before anything else (Windows Sandbox allows all outbound itself)', phase1.join('\n'));
140
+ t.check(internet.includes('1.0.0.0-9.255.255.255') &&
141
+ internet.includes('-185.1.2.2,185.1.2.4-185.9.9.8,185.9.9.10-') &&
142
+ !/(^|[=,])10\.\d/.test(internet.split('remoteip=')[1] ?? '') &&
143
+ internet.includes('-172.15.255.255,172.32.0.0-') &&
144
+ internet.includes('-192.167.255.255,192.169.0.0-') &&
145
+ internet.endsWith('-223.255.255.255'), 'the block covers every public address except the VPN servers (private, loopback and multicast are the LAN block)', internet);
146
+ t.check(phase1[blockAt('Block-IPv6')].includes('remoteip=2000::/3') &&
147
+ phase1[blockAt('Block-DNS-UDP')].includes('protocol=udp remoteport=53') &&
148
+ phase1[blockAt('Block-DNS-TCP')].includes('protocol=tcp remoteport=53'), 'global IPv6 and DNS (which would leak the names you visit) are blocked too');
149
+ t.equal([
150
+ ipv4Except([]),
151
+ ipv4Except([[0, 0]]),
152
+ ipv4Except([[4294967295, 4294967295]]),
153
+ ipv4Except([
154
+ [5, 9],
155
+ [7, 12],
156
+ ]),
157
+ ], [
158
+ '0.0.0.0-255.255.255.255',
159
+ '0.0.0.1-255.255.255.255',
160
+ '0.0.0.0-255.255.255.254',
161
+ '0.0.0.0-0.0.0.4,0.0.0.13-255.255.255.255',
162
+ ], 'address lists without some addresses (edges and overlaps)');
131
163
  t.check(killSwitchCommands([], null).every((c) => !c.join(' ').includes('Server-1') || c[2] === 'delete') &&
132
164
  !killSwitchCommands([], null).some((c) => c.join(' ').includes('name=AgentSandbox-VPN-Host') && c[2] === 'add'), 'with no servers and no session host only the block and the loopback rules remain (the failure case: offline)');
133
165
  t.equal(tunnelCommands('10.8.0.6')
@@ -135,9 +167,13 @@ export const vpnSuite = {
135
167
  .filter((c) => c.includes(' add ')), [
136
168
  'advfirewall firewall add rule name=AgentSandbox-VPN-Tunnel dir=out action=allow enable=yes profile=any protocol=any localip=10.8.0.6',
137
169
  ], 'once the tunnel is up, traffic from its own address (the tunnel) is allowed');
170
+ const phase2 = tunnelCommands('10.8.0.6').map((c) => c.join(' '));
171
+ t.check(['Block-Internet', 'Block-DNS-UDP', 'Block-DNS-TCP'].every((name) => phase2.some((c) => c.startsWith('advfirewall firewall set rule name=AgentSandbox-VPN-' + name + ' new ') &&
172
+ c.endsWith('localip=0.0.0.0-10.8.0.5,10.8.0.7-255.255.255.255'))) && !phase2.some((c) => c.includes('delete') && c.includes('Block-')), 'the block rules are narrowed in place to everything but the tunnel (never deleted, so no moment without them)', phase2.join('\n'));
138
173
  await t.throws(() => tunnelCommands('10.8.0.6; calc'), 'Not an IPv4', 'a tunnel address that is not an address makes no rule');
139
174
  t.check(removeKillSwitchCommands().some((c) => c.join(' ').includes('blockinbound,allowoutbound')) &&
140
- removeKillSwitchCommands().some((c) => c.join(' ').includes('name=AgentSandbox-VPN-Tunnel')), 'taking it away restores the default and deletes the rules');
175
+ removeKillSwitchCommands().some((c) => c.join(' ').includes('name=AgentSandbox-VPN-Tunnel')) &&
176
+ removeKillSwitchCommands().some((c) => c.join(' ').includes('name=AgentSandbox-VPN-Block-Internet')), 'taking it away restores the default and deletes the rules');
141
177
  t.equal([
142
178
  tunnelAddressFromLog('... net_addr_v4_add: 10.100.0.2/16 dev Wintun ...'),
143
179
  tunnelAddressFromLog('ifconfig 10.8.0.6 255.255.255.0'),
@@ -288,6 +324,10 @@ export const vpnSuite = {
288
324
  status('failed', { message: 'login refused' }).text.includes('no internet') &&
289
325
  vpnLine({ state: 'failed', message: 'x', killSwitch: false, updatedAt: 'now' }).text.includes('normal connection') &&
290
326
  status('starting').level === 'warn', "up with another address is fine, up with this PC's own address is flagged, a failure says what the kill switch did");
327
+ t.check(status('failed', { killSwitchHeld: false }).text.includes('ONLINE') &&
328
+ !status('failed', { killSwitchHeld: false }).text.includes('no internet') &&
329
+ status('failed', { killSwitchHeld: true }).text.includes('no internet') &&
330
+ status('up', { publicIp: '185.1.2.3', killSwitchHeld: false }).level === 'warn', 'a kill switch that did not hold is never reported as "no internet" (a failure says the sandbox is online)');
291
331
  t.check(!vpnLine({
292
332
  state: 'failed',
293
333
  message: 'bad \u001b]52;c;ZXZpbA==\u0007 text',
@@ -408,5 +448,49 @@ export const vpnSuite = {
408
448
  clearSecrets(wgSettings.storage.root, wgRun.dataPath);
409
449
  t.check(!fs.existsSync(new RunDir(wgRun.dataPath).secretFile(wgLaunch.launchId)) &&
410
450
  vpnNotes(wgSettings.vpn).some((n) => n.includes('WireGuard private key') && n.includes('turn the tunnel off')), 'the launch warns that the agent can read the key and turn the tunnel off');
451
+ t.section('the VPN feature: status page, login and logout');
452
+ const sample = {
453
+ enabled: true,
454
+ provider: 'nordvpn',
455
+ protocol: 'udp',
456
+ country: '',
457
+ killSwitch: true,
458
+ login: 'none',
459
+ loginSavedAt: null,
460
+ running: null,
461
+ };
462
+ const lines = pageLines(vpnPage(sample)).join('\n');
463
+ t.check(lines.includes('Next: sandbox vpn login') &&
464
+ VPN_WARNINGS.every((w) => lines.includes(w)) &&
465
+ !lines.includes('Running now'), 'vpn status page: the facts, the next step on the command line only, the warnings', lines);
466
+ t.equal(pageActions(vpnPage({ ...sample, provider: 'wireguard-file' })).map((a) => a.kind === 'operation' ? a.op : a.kind), ['settings.set'], 'vpn: wireguard needs no login and offers only turning the VPN on or off');
467
+ t.equal(pageActions(vpnPage({ ...sample, login: 'saved', loginSavedAt: '2026-10-14T09:30:00.000Z' })).map((a) => a.kind === 'operation' ? a.op : a.kind), ['settings.set', 'vpn.login', 'vpn.logout'], 'a saved login offers turning the VPN on or off, saving the login again and deleting it');
468
+ t.equal(vpnLoginRowOf(sample).hint, 'not saved', 'the Settings row says not saved when no login is saved');
469
+ const featureStorage = t.dir('vpn-feature');
470
+ const featureCtx = {
471
+ storageRoot: featureStorage,
472
+ dataPath: t.at('vpn-feature', 'data'),
473
+ loaded: { config: { vpn: { ...DEFAULTS.vpn, provider: 'nordvpn' } } },
474
+ };
475
+ const read = (await runDefinition('vpn.status', featureCtx, { tunnel: false }, fakeIO().io));
476
+ t.check(read.login === 'none' && read.running === null, 'vpn status reads the saved login without the tunnel request', JSON.stringify(read));
477
+ const { io: loginIO, seen: loginSeen } = fakeIO();
478
+ await t.throws(() => runDefinition('vpn.login', featureCtx, { password: PASSWORD }, loginIO), 'does not work', 'the login is never accepted as an input');
479
+ t.check(loginSeen.questions.length === 0, 'a login passed on the command line asks nothing');
480
+ // The on-PC guard only refuses inside a sandbox, so it is tested there; elsewhere there is nothing to assert.
481
+ if (insideSandbox()) {
482
+ const { io, seen } = fakeIO();
483
+ await t.throws(() => runDefinition('vpn.login', featureCtx, {}, io), 'inside the sandbox already', 'vpn login refuses inside the sandbox');
484
+ t.check(seen.questions.length === 0, 'and asks no question first');
485
+ }
486
+ const service = new VpnService(featureCtx, SecretStore.memory());
487
+ service.save({ username: USERNAME, password: PASSWORD });
488
+ const onDisk = fs.readFileSync(path.join(featureStorage, 'vpn', 'nordvpn.json'), 'utf8');
489
+ t.check(!onDisk.includes(PASSWORD) && !onDisk.includes(USERNAME), 'the saved file holds no login in clear');
490
+ const report = await service.report({ tunnel: false });
491
+ t.check(report.login === 'saved' &&
492
+ !JSON.stringify(report).includes(PASSWORD) &&
493
+ !JSON.stringify(report).includes(USERNAME), 'a saved login shows as saved, and the status never holds it');
494
+ t.check(service.remove() && service.loginState() === 'none', 'deleting the login leaves none saved');
411
495
  },
412
496
  };
@@ -8,7 +8,7 @@ import WebSocket from 'ws';
8
8
  import { cancelled } from '../app/kernel/index.js';
9
9
  import { webSession } from '../cli/services/index.js';
10
10
  import { fail } from '../core/util/index.js';
11
- import { cookieValue, csrfOk, hostAllowed, originAllowed, sameSecret, StaticFiles, WebAuth, WebServer, WebStore, } from '../host/web/index.js';
11
+ import { cookieValue, csrfOk, hostAllowed, LiveHub, originAllowed, sameSecret, StaticFiles, WebAuth, WebServer, WebStore, } from '../host/web/index.js';
12
12
  import { encodeFrame, FrameReader, FrameType } from '../shared/pty/index.js';
13
13
  import { CSRF_HEADER, CSRF_VALUE, parseClientMessage } from '../shared/web/index.js';
14
14
  const TOKEN = 'a'.repeat(64);
@@ -153,7 +153,7 @@ export const webSuite = {
153
153
  t.check(good.ok, 'the right token logs in');
154
154
  const cookie = good.ok ? good.cookie : '';
155
155
  t.check(auth.signedIn('x=1; sbxweb=' + cookie), 'the cookie signs in');
156
- t.check(!auth.signedIn('sbxweb=' + cookie.replace(/.$/, '0')) && !auth.signedIn(undefined), 'another cookie does not');
156
+ t.check(!auth.signedIn('sbxweb=' + cookie.replace(/.$/, (c) => (c === '0' ? '1' : '0'))) && !auth.signedIn(undefined), 'another cookie does not');
157
157
  clock += 13 * 60 * 60 * 1000;
158
158
  t.check(!auth.signedIn('sbxweb=' + cookie), 'a cookie unused for 12 hours expires');
159
159
  const limited = new WebAuth(TOKEN, () => clock);
@@ -210,12 +210,46 @@ export const webSuite = {
210
210
  t.excludes(shown, ATTACH_TOKEN, 'the attach token is not in what the browser gets');
211
211
  t.excludes(shown, '1234', 'nor the port');
212
212
  t.excludes(shown, '\x1b', 'text from the sandbox is made safe');
213
+ t.section('live queries are shared, slow down when calm, and stop when nobody watches');
214
+ {
215
+ let value = 1;
216
+ let reads = 0;
217
+ const host = {
218
+ kind: () => 'query',
219
+ liveMs: () => 40,
220
+ run: async () => {
221
+ reads++;
222
+ return { value };
223
+ },
224
+ };
225
+ const hub = new LiveHub(host);
226
+ const seenA = [];
227
+ const seenB = [];
228
+ const stopA = hub.watch('q', {}, (e) => seenA.push(e));
229
+ await t.waitFor(() => seenA.length === 1, 2000);
230
+ const readsBeforeB = reads;
231
+ const stopB = hub.watch('q', {}, (e) => seenB.push(e));
232
+ t.check(reads === readsBeforeB && seenB.length === 1 && hub.size === 1, 'a second watcher of the same query gets the last answer at once, without another read');
233
+ const before = reads;
234
+ await new Promise((r) => setTimeout(r, 800));
235
+ const calmReads = reads - before;
236
+ t.check(calmReads >= 3 && calmReads < 20, 'an unchanged answer is read less often than every interval (' + calmReads + ' reads in 20 intervals)');
237
+ t.check(seenA.length === 1, 'an unchanged answer is not sent again');
238
+ value = 2;
239
+ hub.poke();
240
+ await t.waitFor(() => seenA.length === 2 && seenB.length === 2, 2000);
241
+ t.check(true, 'after an operation (poke) every watcher gets the new answer at once');
242
+ stopA();
243
+ stopB();
244
+ const stopped = reads;
245
+ await new Promise((r) => setTimeout(r, 300));
246
+ t.check(hub.size === 0 && reads === stopped, 'with nobody watching, nothing is read');
247
+ }
213
248
  t.section('the server');
214
249
  const fake = new FakePtyHost();
215
250
  await fake.start();
216
251
  const endpoint = { addresses: ['127.0.0.1'], port: fake.port, token: ATTACH_TOKEN };
217
252
  const listed = webSession(session('abc123'), 0);
218
- const created = [];
219
253
  const backend = {
220
254
  info: () => Promise.resolve({
221
255
  sandbox: 'running',
@@ -227,10 +261,6 @@ export const webSuite = {
227
261
  }),
228
262
  sessions: () => Promise.resolve([listed]),
229
263
  endpoint: (id) => (id === 'abc123' ? endpoint : null),
230
- create: (request) => {
231
- created.push(request);
232
- return Promise.resolve(listed);
233
- },
234
264
  };
235
265
  let stopped = false;
236
266
  const rpc = fakeRpc();
@@ -241,6 +271,7 @@ export const webSuite = {
241
271
  staticRoot: root,
242
272
  rpc: rpc.host,
243
273
  onStop: () => (stopped = true),
274
+ heartbeatMs: 200,
244
275
  });
245
276
  await server.start();
246
277
  const { port } = server;
@@ -276,10 +307,7 @@ export const webSuite = {
276
307
  t.check(list.status === 200 && JSON.parse(list.body).length === 1, 'a signed-in browser lists sessions');
277
308
  t.excludes(list.body, ATTACH_TOKEN, 'the list has no attach token');
278
309
  t.equal((await send(port, 'GET', '/api/info', { Cookie })).status, 200, 'and reads the info');
279
- t.equal((await send(port, 'POST', '/api/sessions', { ...json, Cookie }, '{}')).status, 403, 'a new session needs CSRF and Origin');
280
- t.equal((await send(port, 'POST', '/api/sessions', { ...ajax, Cookie }, '{"harness":"../x","project":"p"}')).status, 400, 'odd names are refused');
281
- const made = await send(port, 'POST', '/api/sessions', { ...ajax, Cookie }, '{"harness":"claude","project":"app"}');
282
- t.check(made.status === 201 && created.length === 1, 'a new session is requested');
310
+ t.equal((await send(port, 'POST', '/api/sessions', { ...json, Cookie }, '{}')).status, 404, 'starting an agent is an operation now: there is no POST /api/sessions');
283
311
  t.section('the terminal bridge');
284
312
  const url = 'ws://127.0.0.1:' + port + '/api/sessions/abc123/terminal?cols=100&rows=40';
285
313
  const attempts = [
@@ -380,6 +408,25 @@ export const webSuite = {
380
408
  leaver.socket.close();
381
409
  await t.waitFor(() => leaver.closeCode !== null, 5000);
382
410
  t.check(rpc.ran.length === runs + 1, 'a browser that leaves mid-question answers no (the call ends)');
411
+ t.section('connections never pile up');
412
+ const tabs = [];
413
+ for (let i = 0; i < 9; i++) {
414
+ const tab = new Tab(rpcUrl, { Origin: origin, Cookie });
415
+ tabs.push(tab);
416
+ await t.waitFor(() => tab.opened || tab.failed, 5000);
417
+ }
418
+ await t.waitFor(() => server.openSockets('rpc') <= 8, 5000);
419
+ t.check(tabs.every((tab) => tab.opened) && server.openSockets('rpc') === 8, 'a new page always connects: at the limit the oldest connection makes room', 'open: ' + server.openSockets('rpc'));
420
+ t.check([browser, ...tabs].some((tab) => tab.closeCode === 4000), 'the oldest is closed with "replaced by a newer page"');
421
+ for (const tab of tabs)
422
+ tab.socket.close();
423
+ const silent = new WebSocket(rpcUrl, { headers: { Origin: origin, Cookie }, autoPong: false });
424
+ let silentClosed = false;
425
+ silent.on('close', () => (silentClosed = true));
426
+ silent.on('error', () => undefined);
427
+ await t.waitFor(() => silent.readyState === WebSocket.OPEN, 5000);
428
+ await t.waitFor(() => silentClosed, 3000);
429
+ t.check(silentClosed, 'a page that stops answering pings (gone without closing) is dropped');
383
430
  t.section('stopping');
384
431
  t.equal((await send(port, 'POST', '/api/stop', { ...json, Cookie }, '{}')).status, 403, 'stop needs CSRF and Origin too');
385
432
  t.equal((await send(port, 'POST', '/api/stop', { ...ajax, Cookie }, '{}')).status, 200, 'a signed-in browser can stop it');
@@ -9,6 +9,8 @@ export declare class RunDir {
9
9
  readonly vpnFile: string;
10
10
  /** What the memory governor in the sandbox is doing (pressure.json). */
11
11
  readonly pressureFile: string;
12
+ /** What the sandbox's programs may really use (budget.json): processors, memory, build jobs, link slots. */
13
+ readonly budgetFile: string;
12
14
  readonly requestsDir: string;
13
15
  readonly responsesDir: string;
14
16
  readonly sessionsDir: string;
@@ -36,6 +36,8 @@ export class RunDir {
36
36
  vpnFile;
37
37
  /** What the memory governor in the sandbox is doing (pressure.json). */
38
38
  pressureFile;
39
+ /** What the sandbox's programs may really use (budget.json): processors, memory, build jobs, link slots. */
40
+ budgetFile;
39
41
  requestsDir;
40
42
  responsesDir;
41
43
  sessionsDir;
@@ -49,6 +51,7 @@ export class RunDir {
49
51
  this.sandboxFile = path.join(this.root, 'sandbox.json');
50
52
  this.vpnFile = path.join(this.root, 'vpn.json');
51
53
  this.pressureFile = path.join(this.root, 'pressure.json');
54
+ this.budgetFile = path.join(this.root, 'budget.json');
52
55
  this.requestsDir = path.join(this.root, 'requests');
53
56
  this.responsesDir = path.join(this.root, 'responses');
54
57
  this.sessionsDir = path.join(this.root, 'sessions');
@@ -556,6 +556,10 @@ export declare const GuestRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
556
556
  id: z.ZodString;
557
557
  reason: z.ZodString;
558
558
  seconds: z.ZodNumber;
559
+ level: z.ZodOptional<z.ZodEnum<{
560
+ slow: "slow";
561
+ severe: "severe";
562
+ }>>;
559
563
  createdAt: z.ZodString;
560
564
  }, z.core.$strict>], "kind">;
561
565
  /** C:\AgentData\run\pressure.json: what the memory governor in the sandbox is doing (guest-written, capped). */
@@ -565,6 +569,7 @@ export declare const PressureStatusSchema: z.ZodObject<{
565
569
  throttled: "throttled";
566
570
  suspending: "suspending";
567
571
  }>;
572
+ step: z.ZodOptional<z.ZodNumber>;
568
573
  profile: z.ZodEnum<{
569
574
  normal: "normal";
570
575
  gentle: "gentle";
@@ -577,6 +582,7 @@ export declare const PressureStatusSchema: z.ZodObject<{
577
582
  pid: z.ZodNumber;
578
583
  seconds: z.ZodNumber;
579
584
  }, z.core.$strip>>;
585
+ cpuPercent: z.ZodOptional<z.ZodNumber>;
580
586
  pcLow: z.ZodBoolean;
581
587
  lastAction: z.ZodString;
582
588
  updatedAt: z.ZodString;
@@ -645,6 +651,7 @@ export declare const VpnStatusSchema: z.ZodObject<{
645
651
  }>;
646
652
  message: z.ZodString;
647
653
  killSwitch: z.ZodBoolean;
654
+ killSwitchHeld: z.ZodOptional<z.ZodBoolean>;
648
655
  server: z.ZodOptional<z.ZodString>;
649
656
  publicIp: z.ZodOptional<z.ZodString>;
650
657
  updatedAt: z.ZodString;
@@ -100,20 +100,26 @@ export const GuestRequestSchema = z.discriminatedUnion('kind', [
100
100
  id: IdSchema,
101
101
  reason: Text(300),
102
102
  seconds: z.number().int().min(10).max(300),
103
+ /** "severe": the PC is nearly out of memory; builds go to the governor's last step at once. */
104
+ level: z.enum(['slow', 'severe']).optional(),
103
105
  createdAt: TimeSchema,
104
106
  }),
105
107
  ]);
106
108
  /** C:\AgentData\run\pressure.json: what the memory governor in the sandbox is doing (guest-written, capped). */
107
109
  export const PressureStatusSchema = z.object({
108
110
  level: z.enum(['ok', 'throttled', 'suspending']),
111
+ /** The governor's dial, 0 (nothing wrong) to 4 (one compiler at a time). */
112
+ step: z.number().int().min(0).max(4).optional(),
109
113
  profile: z.enum(['normal', 'gentle']),
110
114
  /** Share of the sandbox's RAM still free and of its commit limit in use (percent). */
111
115
  ramFreePercent: z.number().int().min(0).max(100),
112
116
  commitPercent: z.number().int().min(0).max(100),
113
117
  /** Programs running at lower priority because of it. */
114
118
  lowered: z.number().int().min(0).max(1024),
115
- /** Programs paused right now (name, process id, seconds left). */
119
+ /** Compilers waiting their turn right now (name, process id, seconds until they may go on). */
116
120
  paused: z.array(z.object({ name: Text(64), pid: z.number().int(), seconds: z.number().int().min(0).max(60) })).max(8),
121
+ /** The CPU share the compilers get together (percent; 100 = no cap). */
122
+ cpuPercent: z.number().int().min(0).max(100).optional(),
117
123
  /** The PC itself asked for slowing down (its memory or CPU is low). */
118
124
  pcLow: z.boolean(),
119
125
  lastAction: Text(300),
@@ -163,6 +169,8 @@ export const VpnStatusSchema = z.object({
163
169
  state: z.enum(['off', 'starting', 'up', 'failed']),
164
170
  message: Text(2000),
165
171
  killSwitch: z.boolean(),
172
+ /** A direct connection was tried after the kill switch was applied: false = it still got out. */
173
+ killSwitchHeld: z.boolean().optional(),
166
174
  /** The server's name, e.g. uk2201.nordvpn.com. */
167
175
  server: Text(128).optional(),
168
176
  /** What the sandbox's traffic looks like from outside, once the tunnel is up. */
@@ -9,6 +9,7 @@ export declare const SecretsFileSchema: z.ZodObject<{
9
9
  password: z.ZodString;
10
10
  }, z.core.$strict>>;
11
11
  wireguardConfig: z.ZodOptional<z.ZodString>;
12
+ githubToken: z.ZodOptional<z.ZodString>;
12
13
  }, z.core.$strict>;
13
14
  export type SecretsFile = z.output<typeof SecretsFileSchema>;
14
15
  interface WithHarnesses {
@@ -4,6 +4,7 @@
4
4
  // sandbox may change run\, never this folder. The agent in the sandbox can still read what it was given (its own
5
5
  // environment, its login): that is inherent. Every part is optional.
6
6
  import { z } from 'zod';
7
+ import { GithubTokenSchema } from '../github/index.js';
7
8
  const Environment = z.record(z.string().max(256), z.string().max(65536));
8
9
  export const SecretsFileSchema = z.strictObject({
9
10
  /** The agent the sandbox booted with: its harness environment. */
@@ -25,6 +26,8 @@ export const SecretsFileSchema = z.strictObject({
25
26
  .min(1)
26
27
  .max(32 * 1024)
27
28
  .optional(),
29
+ /** The GitHub token you saved for sandboxes (github.shareToken), only with git access "full". */
30
+ githubToken: GithubTokenSchema.optional(),
28
31
  });
29
32
  /** The environment a harness gets: its own, then the values that travelled in the secrets file. */
30
33
  const withEnvironment = (harness, environment) => (environment ? { ...harness, environment: { ...harness.environment, ...environment } } : harness);
@@ -0,0 +1 @@
1
+ export { GITHUB_TOKEN_KINDS, GithubTokenSchema, ghLoginCommand, ghOutputText, ghSetupGitCommand, githubTokenKind, githubTokenProblem, type GhCommand, type GithubTokenKind, } from './token.js';
@@ -0,0 +1,2 @@
1
+ // A GitHub token for sandboxes: its rules and the gh commands that use it (host and guest).
2
+ export { GITHUB_TOKEN_KINDS, GithubTokenSchema, ghLoginCommand, ghOutputText, ghSetupGitCommand, githubTokenKind, githubTokenProblem, } from './token.js';
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ export declare const GITHUB_TOKEN_KINDS: readonly ["fine-grained", "classic"];
3
+ export type GithubTokenKind = (typeof GITHUB_TOKEN_KINDS)[number];
4
+ /** The token in the secrets file: the same limits as on the PC. */
5
+ export declare const GithubTokenSchema: z.ZodString;
6
+ /** What kind of personal access token this is, from its prefix (null: not one). */
7
+ export declare function githubTokenKind(token: string): GithubTokenKind | null;
8
+ /** Why this text cannot be saved as the sandbox token (fixed texts: never the token), or null when it can. */
9
+ export declare function githubTokenProblem(token: string): {
10
+ message: string;
11
+ fix: string;
12
+ } | null;
13
+ /** One gh run: the program, its arguments, what goes on its stdin, and its environment. */
14
+ export interface GhCommand {
15
+ readonly file: string;
16
+ readonly args: readonly string[];
17
+ readonly input?: string;
18
+ readonly env: Readonly<Record<string, string>>;
19
+ }
20
+ /**
21
+ * `gh auth login` with the token on stdin, never in the arguments (other processes can read those). The login is
22
+ * kept as a file in configDir (--insecure-storage, rather than Credential Manager), which is on the sandbox's own disk
23
+ * and gone with it.
24
+ */
25
+ export declare function ghLoginCommand(ghExe: string, configDir: string, token: string, base: Readonly<Record<string, string | undefined>>): GhCommand;
26
+ /** `gh auth setup-git`: https git to github.com asks gh for the login. */
27
+ export declare function ghSetupGitCommand(ghExe: string, configDir: string, base: Readonly<Record<string, string | undefined>>): GhCommand;
28
+ /** Text from gh, safe to log: the token blanked out should it ever appear, and kept short. */
29
+ export declare function ghOutputText(text: string, token: string): string;
@@ -0,0 +1,89 @@
1
+ // A GitHub token for sandboxes (github.shareToken): the rules for one (both sides check them) and the gh commands the
2
+ // sandbox runs with it. Pure. The token is never part of a message, an argument or a log line: gh reads it on stdin.
3
+ import { z } from 'zod';
4
+ export const GITHUB_TOKEN_KINDS = ['fine-grained', 'classic'];
5
+ const MIN_LENGTH = 20;
6
+ const MAX_LENGTH = 255;
7
+ /** Printable ASCII without spaces: what GitHub's tokens are made of. */
8
+ const PRINTABLE = /^[\x21-\x7e]+$/;
9
+ /** The token in the secrets file: the same limits as on the PC. */
10
+ export const GithubTokenSchema = z.string().min(MIN_LENGTH).max(MAX_LENGTH).regex(PRINTABLE);
11
+ /** Prefixes of tokens that are not personal access tokens (OAuth from `gh auth login`, app and refresh tokens). */
12
+ const OTHER_TOKENS = {
13
+ gho_: 'This is an OAuth token (from gh auth login or gh auth token), which can reach everything your account can.',
14
+ ghu_: 'This is a GitHub App user token, not a personal access token.',
15
+ ghs_: 'This is a GitHub App installation token, not a personal access token.',
16
+ ghr_: 'This is a refresh token, not a personal access token.',
17
+ };
18
+ const CREATE_FINE_GRAINED = 'Create a fine-grained token at https://github.com/settings/personal-access-tokens/new with only the repositories and permissions the agent needs, then run: sandbox github login';
19
+ /** What kind of personal access token this is, from its prefix (null: not one). */
20
+ export function githubTokenKind(token) {
21
+ if (token.startsWith('github_pat_'))
22
+ return 'fine-grained';
23
+ if (token.startsWith('ghp_'))
24
+ return 'classic';
25
+ return null;
26
+ }
27
+ /** Why this text cannot be saved as the sandbox token (fixed texts: never the token), or null when it can. */
28
+ export function githubTokenProblem(token) {
29
+ if (!token)
30
+ return { message: 'No token was typed.', fix: CREATE_FINE_GRAINED };
31
+ if (!PRINTABLE.test(token))
32
+ return {
33
+ message: 'The token may not contain spaces, line breaks or other special characters.',
34
+ fix: 'Copy the token again from GitHub (only the token itself) and run: sandbox github login',
35
+ };
36
+ if (token.length < MIN_LENGTH || token.length > MAX_LENGTH)
37
+ return {
38
+ message: 'That is not the length of a GitHub token.',
39
+ fix: 'Copy the whole token again from GitHub and run: sandbox github login',
40
+ };
41
+ const other = Object.keys(OTHER_TOKENS).find((p) => token.startsWith(p));
42
+ if (other)
43
+ return { message: OTHER_TOKENS[other], fix: CREATE_FINE_GRAINED };
44
+ if (!githubTokenKind(token))
45
+ return {
46
+ message: 'That is not a GitHub personal access token (they start with github_pat_ or ghp_).',
47
+ fix: CREATE_FINE_GRAINED,
48
+ };
49
+ return null;
50
+ }
51
+ /** Variables that would make gh use another token than the one it is given (gh refuses --with-token with them). */
52
+ const TOKEN_VARIABLES = /^(GH_TOKEN|GITHUB_TOKEN|GH_ENTERPRISE_TOKEN|GITHUB_ENTERPRISE_TOKEN)$/i;
53
+ const ghEnv = (base, configDir) => ({
54
+ ...Object.fromEntries(Object.entries(base).filter((e) => e[1] !== undefined && !TOKEN_VARIABLES.test(e[0]))),
55
+ GH_CONFIG_DIR: configDir,
56
+ GH_PROMPT_DISABLED: '1',
57
+ GH_NO_UPDATE_NOTIFIER: '1',
58
+ });
59
+ /**
60
+ * `gh auth login` with the token on stdin, never in the arguments (other processes can read those). The login is
61
+ * kept as a file in configDir (--insecure-storage, rather than Credential Manager), which is on the sandbox's own disk
62
+ * and gone with it.
63
+ */
64
+ export function ghLoginCommand(ghExe, configDir, token, base) {
65
+ return {
66
+ file: ghExe,
67
+ args: [
68
+ 'auth',
69
+ 'login',
70
+ '--hostname',
71
+ 'github.com',
72
+ '--git-protocol',
73
+ 'https',
74
+ '--insecure-storage',
75
+ '--with-token',
76
+ ],
77
+ input: token + '\n',
78
+ env: ghEnv(base, configDir),
79
+ };
80
+ }
81
+ /** `gh auth setup-git`: https git to github.com asks gh for the login. */
82
+ export function ghSetupGitCommand(ghExe, configDir, base) {
83
+ return { file: ghExe, args: ['auth', 'setup-git', '--hostname', 'github.com'], env: ghEnv(base, configDir) };
84
+ }
85
+ /** Text from gh, safe to log: the token blanked out should it ever appear, and kept short. */
86
+ export function ghOutputText(text, token) {
87
+ const clean = token ? text.split(token).join('***') : text;
88
+ return clean.replace(/\s+/g, ' ').trim().slice(0, 300);
89
+ }
@@ -1,16 +1,27 @@
1
1
  export type LoadProfile = 'normal' | 'gentle';
2
- /** The guest governor: when memory inside the sandbox counts as under pressure, and when it has recovered. */
3
- export interface GuestThresholds {
4
- /** Pressure starts when available RAM is below this share of the sandbox's RAM (percent)... */
5
- readonly enterAvailablePercent: number;
6
- /** ...or commit is above this share of its limit. */
7
- readonly enterCommitPercent: number;
8
- /** Pressure ends only when available RAM is above this and commit is below the next, for `calmSamples` in a row. */
9
- readonly exitAvailablePercent: number;
10
- readonly exitCommitPercent: number;
11
- readonly calmSamples: number;
2
+ /**
3
+ * One step of the guest governor's dial. It turns up when available RAM falls below `availablePercent` of the
4
+ * sandbox's RAM, commit rises above `commitPercent` of its limit, or (from step 3) the commit would reach its limit
5
+ * within `secondsToLimit` at the rate it is growing. It turns down one step after `calmSamples` samples in a row that
6
+ * are clear of the step by the margins below.
7
+ */
8
+ export interface DialStep {
9
+ readonly availablePercent: number;
10
+ readonly commitPercent: number;
11
+ readonly secondsToLimit?: number;
12
12
  }
13
- export declare const GUEST_THRESHOLDS: Readonly<Record<LoadProfile, GuestThresholds>>;
13
+ /** Steps 1 to 4 of the dial, per profile (index 0 is step 1). The gentle profile (vGPU on) turns earlier. */
14
+ export declare const DIAL_STEPS: Readonly<Record<LoadProfile, readonly DialStep[]>>;
15
+ /** How clear of a step a sample must be to count as calm: this much more RAM free and this much less commit. */
16
+ export declare const DIAL_EXIT_MARGIN: {
17
+ readonly availablePercent: 5;
18
+ readonly commitPercent: 4;
19
+ };
20
+ export declare const DIAL_CALM_SAMPLES = 3;
21
+ /** The CPU share (percent of the sandbox) the compilers get together at each step; 100 = no cap. */
22
+ export declare const DIAL_CPU_RATE: readonly number[];
23
+ /** How many of the compilers running may go on at each step (the rest wait their turn), as a share of them. */
24
+ export declare function compilersAllowed(step: number, running: number): number;
14
25
  /** The PC watchdog: when this PC itself is low. */
15
26
  export interface HostThresholds {
16
27
  /** Low when available RAM is below max(this, availableShare of total RAM)... */
@@ -32,7 +43,17 @@ export declare const HOST_THRESHOLDS: Readonly<Record<LoadProfile, HostThreshold
32
43
  export declare function gentleCpuLimit(cpuLimit: 'none' | number): number;
33
44
  /** The CPU cap the sandbox runs with (percent; null = none) for a profile. */
34
45
  export declare function effectiveCpuLimit(cpuLimit: 'none' | number, profile: LoadProfile): number | null;
35
- /** Cargo jobs that fit the memory (about 4 GB per rustc) and the CPUs, fewer when several agent sessions build. */
46
+ /**
47
+ * The build budget of the whole sandbox: how many compile jobs fit together (about 3 GB each after 6 GB for Windows,
48
+ * the agents and their tools), never more than the processors the sandbox may use. The governor slows them down when
49
+ * memory runs short anyway; this keeps builds from starting far more than fits.
50
+ */
51
+ export declare function buildJobs(cpus: number, memoryGb: number): number;
52
+ /** Links that may run at once in the whole sandbox ("auto"): one per 3 GB after 6 GB, 2 to 8. */
53
+ export declare function linkSlotsFor(memoryGb: number): number;
54
+ /** The processors the sandbox's programs may use with a CPU cap of `percent` (affinity): at least 2. */
55
+ export declare function usableCpus(cpus: number, percent: number | null): number;
56
+ /** Cargo jobs for one more agent session: what fits the memory and CPUs, shared with the sessions already building. */
36
57
  export declare function cargoJobs(cpus: number, memoryGb: number, sessions: number): number;
37
58
  /** The most heavy compilers (rustc, clippy-driver, link ...) that run at once in the agents' job: only with vGPU on. */
38
59
  export declare function maxCompilers(cpus: number, profile: LoadProfile): number | null;