windows-development-sandbox 1.7.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (432) hide show
  1. package/config.example.json +12 -1
  2. package/dist/app/context.d.ts +8 -0
  3. package/dist/app/context.js +12 -0
  4. package/dist/app/features/defs.d.ts +322 -0
  5. package/dist/app/features/defs.js +28 -0
  6. package/dist/app/features/sandbox/index.d.ts +2 -0
  7. package/dist/app/features/sandbox/index.js +3 -0
  8. package/dist/app/features/sandbox/sandbox.def.d.ts +193 -0
  9. package/dist/app/features/sandbox/sandbox.def.js +245 -0
  10. package/dist/app/features/sandbox/sandbox.run.d.ts +72 -0
  11. package/dist/app/features/sandbox/sandbox.run.js +98 -0
  12. package/dist/app/features/sessions/index.d.ts +2 -0
  13. package/dist/app/features/sessions/index.js +3 -0
  14. package/dist/app/features/sessions/sessions.def.d.ts +141 -0
  15. package/dist/app/features/sessions/sessions.def.js +176 -0
  16. package/dist/app/features/sessions/sessions.run.d.ts +39 -0
  17. package/dist/app/features/sessions/sessions.run.js +58 -0
  18. package/dist/{cli/menu/settings/categories.d.ts → app/features/settings/categories.def.d.ts} +2 -0
  19. package/dist/{cli/menu/settings/categories.js → app/features/settings/categories.def.js} +24 -5
  20. package/dist/app/features/settings/index.d.ts +5 -0
  21. package/dist/app/features/settings/index.js +6 -0
  22. package/dist/{cli/menu/settings/config-file.d.ts → app/features/settings/settings-file.d.ts} +1 -1
  23. package/dist/{cli/menu/settings/config-file.js → app/features/settings/settings-file.js} +1 -1
  24. package/dist/app/features/settings/settings-rows.d.ts +35 -0
  25. package/dist/app/features/settings/settings-rows.js +94 -0
  26. package/dist/app/features/settings/settings.def.d.ts +157 -0
  27. package/dist/app/features/settings/settings.def.js +179 -0
  28. package/dist/app/features/settings/settings.run.d.ts +67 -0
  29. package/dist/app/features/settings/settings.run.js +84 -0
  30. package/dist/app/kernel/define.d.ts +143 -0
  31. package/dist/app/kernel/define.js +42 -0
  32. package/dist/app/kernel/errors.d.ts +34 -0
  33. package/dist/app/kernel/errors.js +45 -0
  34. package/dist/app/kernel/fields.d.ts +70 -0
  35. package/dist/app/kernel/fields.js +243 -0
  36. package/dist/app/kernel/index.d.ts +8 -0
  37. package/dist/app/kernel/index.js +10 -0
  38. package/dist/app/kernel/io.d.ts +193 -0
  39. package/dist/app/kernel/io.js +126 -0
  40. package/dist/app/kernel/rpc.d.ts +156 -0
  41. package/dist/app/kernel/rpc.js +47 -0
  42. package/dist/app/kernel/text.d.ts +6 -0
  43. package/dist/app/kernel/text.js +57 -0
  44. package/dist/app/kernel/view.d.ts +146 -0
  45. package/dist/app/kernel/view.js +28 -0
  46. package/dist/app/registry.d.ts +10 -0
  47. package/dist/app/registry.js +50 -0
  48. package/dist/cli/args.d.ts +5 -0
  49. package/dist/cli/args.js +6 -0
  50. package/dist/cli/commands/engine.d.ts +2 -0
  51. package/dist/cli/commands/engine.js +60 -0
  52. package/dist/cli/commands/index.d.ts +1 -1
  53. package/dist/cli/commands/index.js +1 -1
  54. package/dist/cli/commands/launch/launch.js +49 -7
  55. package/dist/cli/commands/launch/prepare.js +3 -3
  56. package/dist/cli/commands/launch/running.js +2 -1
  57. package/dist/cli/commands/launch/summary.d.ts +2 -0
  58. package/dist/cli/commands/maintenance/config-share.d.ts +12 -0
  59. package/dist/cli/commands/maintenance/config-share.js +80 -0
  60. package/dist/cli/commands/maintenance/config.js +8 -82
  61. package/dist/cli/commands/maintenance/logs.js +14 -0
  62. package/dist/cli/commands/registry.js +3 -7
  63. package/dist/cli/commands/repair/checks/contained.d.ts +2 -0
  64. package/dist/cli/commands/repair/checks/contained.js +139 -0
  65. package/dist/cli/commands/repair/checks/hcs.d.ts +7 -0
  66. package/dist/cli/commands/repair/checks/hcs.js +28 -0
  67. package/dist/cli/commands/repair/checks/index.js +4 -0
  68. package/dist/cli/commands/repair/checks/pc.js +32 -1
  69. package/dist/cli/commands/repair/checks/sandbox.js +16 -1
  70. package/dist/cli/commands/repair/checks/vpn.js +34 -9
  71. package/dist/cli/commands/repair/env.js +2 -1
  72. package/dist/cli/commands/sessions/index.d.ts +0 -4
  73. package/dist/cli/commands/sessions/index.js +3 -7
  74. package/dist/cli/commands/shared-use.d.ts +5 -2
  75. package/dist/cli/commands/shared-use.js +11 -8
  76. package/dist/cli/commands/storage/contained.d.ts +3 -0
  77. package/dist/cli/commands/storage/contained.js +179 -0
  78. package/dist/cli/commands/storage/offer.js +7 -1
  79. package/dist/cli/commands/storage/storage.js +10 -2
  80. package/dist/cli/commands/user-config.js +1 -1
  81. package/dist/cli/commands/vpn.js +20 -3
  82. package/dist/cli/commands/web.d.ts +5 -0
  83. package/dist/cli/commands/web.js +40 -0
  84. package/dist/cli/index.js +2 -1
  85. package/dist/cli/manifest.d.ts +72 -104
  86. package/dist/cli/manifest.js +64 -39
  87. package/dist/cli/menu/add-agent.d.ts +1 -1
  88. package/dist/cli/menu/add-agent.js +1 -1
  89. package/dist/cli/menu/apps.d.ts +7 -1
  90. package/dist/cli/menu/apps.js +36 -0
  91. package/dist/cli/menu/command-fields.d.ts +1 -1
  92. package/dist/cli/menu/command-form.js +2 -2
  93. package/dist/cli/menu/io-menu.d.ts +29 -0
  94. package/dist/cli/menu/io-menu.js +126 -0
  95. package/dist/cli/menu/live.d.ts +27 -0
  96. package/dist/cli/menu/live.js +69 -0
  97. package/dist/cli/menu/memory.d.ts +19 -0
  98. package/dist/cli/menu/memory.js +53 -0
  99. package/dist/cli/menu/menu.d.ts +10 -1
  100. package/dist/cli/menu/menu.js +27 -9
  101. package/dist/cli/menu/pages/agents-page.d.ts +3 -1
  102. package/dist/cli/menu/pages/agents-page.js +12 -5
  103. package/dist/cli/menu/pages/apps-page.d.ts +3 -1
  104. package/dist/cli/menu/pages/apps-page.js +19 -2
  105. package/dist/cli/menu/pages/list-page.d.ts +27 -0
  106. package/dist/cli/menu/pages/list-page.js +109 -7
  107. package/dist/cli/menu/pages/maintenance-page.d.ts +0 -5
  108. package/dist/cli/menu/pages/maintenance-page.js +17 -19
  109. package/dist/cli/menu/pages/sandbox-page.d.ts +4 -6
  110. package/dist/cli/menu/pages/sandbox-page.js +26 -118
  111. package/dist/cli/menu/pages/scopes.d.ts +1 -1
  112. package/dist/cli/menu/pages/sessions-page.d.ts +5 -11
  113. package/dist/cli/menu/pages/sessions-page.js +19 -88
  114. package/dist/cli/menu/pages/settings-page.d.ts +2 -10
  115. package/dist/cli/menu/pages/settings-page.js +64 -94
  116. package/dist/cli/menu/render-view.d.ts +28 -0
  117. package/dist/cli/menu/render-view.js +112 -0
  118. package/dist/cli/menu/settings/{path-kind.d.ts → ask-path.d.ts} +1 -7
  119. package/dist/cli/menu/settings/ask-path.js +14 -0
  120. package/dist/cli/menu/settings/collections/collection-editor.d.ts +1 -1
  121. package/dist/cli/menu/settings/collections/string-list-editor.d.ts +1 -2
  122. package/dist/cli/menu/settings/collections/string-list-editor.js +1 -1
  123. package/dist/cli/menu/settings/draft.d.ts +1 -1
  124. package/dist/cli/menu/settings/format.d.ts +1 -3
  125. package/dist/cli/menu/settings/format.js +6 -36
  126. package/dist/cli/menu/settings/index.d.ts +1 -3
  127. package/dist/cli/menu/settings/index.js +3 -3
  128. package/dist/cli/menu/settings/object-editor.d.ts +1 -1
  129. package/dist/cli/menu/settings/share.d.ts +36 -0
  130. package/dist/cli/menu/settings/share.js +131 -0
  131. package/dist/cli/menu/settings/value-editor.d.ts +5 -1
  132. package/dist/cli/menu/settings/value-editor.js +57 -54
  133. package/dist/cli/menu/several.js +1 -1
  134. package/dist/cli/menu/share-settings.d.ts +21 -0
  135. package/dist/cli/menu/share-settings.js +112 -0
  136. package/dist/cli/menu/shell.d.ts +21 -5
  137. package/dist/cli/menu/shell.js +31 -6
  138. package/dist/cli/menu/views.d.ts +7 -0
  139. package/dist/cli/menu/views.js +11 -0
  140. package/dist/cli/menu/vpn-login.js +8 -0
  141. package/dist/cli/operations/handler.d.ts +6 -0
  142. package/dist/cli/operations/handler.js +61 -0
  143. package/dist/cli/operations/index.d.ts +4 -0
  144. package/dist/cli/operations/index.js +5 -0
  145. package/dist/cli/operations/input.d.ts +6 -0
  146. package/dist/cli/operations/input.js +37 -0
  147. package/dist/cli/operations/io-cli.d.ts +7 -0
  148. package/dist/cli/operations/io-cli.js +62 -0
  149. package/dist/cli/operations/spec.d.ts +18 -0
  150. package/dist/cli/operations/spec.js +108 -0
  151. package/dist/cli/output/colors.d.ts +2 -2
  152. package/dist/cli/output/colors.js +2 -11
  153. package/dist/cli/output/index.d.ts +1 -0
  154. package/dist/cli/output/index.js +1 -0
  155. package/dist/cli/output/print.d.ts +3 -1
  156. package/dist/cli/output/print.js +7 -3
  157. package/dist/cli/output/progress.d.ts +15 -0
  158. package/dist/cli/output/progress.js +44 -0
  159. package/dist/cli/output/render-view.d.ts +4 -0
  160. package/dist/cli/output/render-view.js +63 -0
  161. package/dist/cli/services/agent-commands-service.d.ts +2 -0
  162. package/dist/cli/services/agent-commands-service.js +11 -2
  163. package/dist/cli/services/logs-service.js +4 -3
  164. package/dist/cli/tui/app.d.ts +6 -1
  165. package/dist/cli/tui/app.js +26 -4
  166. package/dist/cli/tui/list/list-screen.d.ts +14 -0
  167. package/dist/cli/tui/list/list-screen.js +64 -36
  168. package/dist/cli/tui/list/list-types.d.ts +2 -0
  169. package/dist/cli/tui/terminal.d.ts +7 -0
  170. package/dist/cli/tui/terminal.js +30 -10
  171. package/dist/core/config/defaults.js +9 -1
  172. package/dist/core/schema/config.d.ts +14 -0
  173. package/dist/core/schema/config.js +16 -1
  174. package/dist/core/schema/harness.d.ts +1 -0
  175. package/dist/core/schema/harness.js +5 -0
  176. package/dist/core/schema/sandbox.d.ts +6 -0
  177. package/dist/core/schema/sandbox.js +20 -3
  178. package/dist/core/schema/vpn.d.ts +2 -1
  179. package/dist/core/schema/vpn.js +4 -4
  180. package/dist/core/schema/web.d.ts +1 -0
  181. package/dist/core/schema/web.js +4 -1
  182. package/dist/core/types.d.ts +23 -5
  183. package/dist/core/util/fs.d.ts +2 -0
  184. package/dist/core/util/fs.js +9 -0
  185. package/dist/core/util/index.d.ts +1 -1
  186. package/dist/core/util/index.js +1 -1
  187. package/dist/guest/agents/agent-files.d.ts +5 -0
  188. package/dist/guest/agents/agent-files.js +4 -0
  189. package/dist/guest/agents/local-state.d.ts +2 -0
  190. package/dist/guest/agents/local-state.js +21 -1
  191. package/dist/guest/boot/boot.js +5 -2
  192. package/dist/guest/daemon/daemon.js +4 -0
  193. package/dist/guest/daemon/sessions.d.ts +5 -0
  194. package/dist/guest/daemon/sessions.js +17 -3
  195. package/dist/guest/entries/daemon.js +3 -0
  196. package/dist/guest/governor/decide.d.ts +71 -0
  197. package/dist/guest/governor/decide.js +211 -0
  198. package/dist/guest/governor/governor.d.ts +45 -0
  199. package/dist/guest/governor/governor.js +230 -0
  200. package/dist/guest/governor/index.d.ts +2 -0
  201. package/dist/guest/governor/index.js +2 -0
  202. package/dist/guest/network/firewall.d.ts +1 -0
  203. package/dist/guest/network/firewall.js +65 -23
  204. package/dist/guest/runtime/csc.d.ts +1 -0
  205. package/dist/guest/runtime/csc.js +1 -0
  206. package/dist/guest/runtime/process.js +53 -5
  207. package/dist/guest/sessions/viewer.js +3 -1
  208. package/dist/guest/setup/claude-mem.js +12 -4
  209. package/dist/guest/toolchains/languages/node.js +29 -3
  210. package/dist/guest/toolchains/native/rust-extras.js +4 -2
  211. package/dist/guest/vpn/vpn.js +69 -21
  212. package/dist/guest/vpn/wireguard.d.ts +32 -0
  213. package/dist/guest/vpn/wireguard.js +132 -0
  214. package/dist/host/agent-commands/commands.d.ts +4 -0
  215. package/dist/host/agent-commands/commands.js +31 -7
  216. package/dist/host/audit/log.d.ts +1 -1
  217. package/dist/host/channel/attach.d.ts +3 -1
  218. package/dist/host/channel/attach.js +5 -2
  219. package/dist/host/channel/channel.d.ts +2 -0
  220. package/dist/host/channel/channel.js +4 -0
  221. package/dist/host/engines/engine.d.ts +12 -0
  222. package/dist/host/engines/engine.js +1 -0
  223. package/dist/host/engines/hcs/check.d.ts +25 -0
  224. package/dist/host/engines/hcs/check.js +95 -0
  225. package/dist/host/engines/hcs/documents.d.ts +44 -0
  226. package/dist/host/engines/hcs/documents.js +155 -0
  227. package/dist/host/engines/hcs/engine.d.ts +39 -0
  228. package/dist/host/engines/hcs/engine.js +171 -0
  229. package/dist/host/engines/hcs/helper.d.ts +43 -0
  230. package/dist/host/engines/hcs/helper.js +173 -0
  231. package/dist/host/engines/hcs/images.d.ts +37 -0
  232. package/dist/host/engines/hcs/images.js +152 -0
  233. package/dist/host/engines/index.d.ts +7 -0
  234. package/dist/host/engines/index.js +5 -0
  235. package/dist/host/engines/registry.d.ts +16 -0
  236. package/dist/host/engines/registry.js +30 -0
  237. package/dist/host/engines/types.d.ts +58 -0
  238. package/dist/host/engines/types.js +1 -0
  239. package/dist/host/engines/windows-sandbox/engine.d.ts +16 -0
  240. package/dist/host/engines/windows-sandbox/engine.js +63 -0
  241. package/dist/host/engines/windows-sandbox/wsb-text.d.ts +2 -0
  242. package/dist/host/engines/windows-sandbox/wsb-text.js +29 -0
  243. package/dist/host/launch/generate.d.ts +5 -1
  244. package/dist/host/launch/generate.js +50 -8
  245. package/dist/host/launch/index.d.ts +3 -1
  246. package/dist/host/launch/index.js +3 -1
  247. package/dist/host/launch/launch-plan.d.ts +13 -0
  248. package/dist/host/launch/launch-plan.js +26 -0
  249. package/dist/host/launch/mappings.js +2 -1
  250. package/dist/host/launch/risk-notes.d.ts +10 -0
  251. package/dist/host/launch/risk-notes.js +18 -0
  252. package/dist/host/launch/session-file.d.ts +2 -0
  253. package/dist/host/launch/session-file.js +7 -1
  254. package/dist/host/launch/wsb-file.d.ts +28 -4
  255. package/dist/host/launch/wsb-file.js +61 -42
  256. package/dist/host/life/crash-report.d.ts +27 -1
  257. package/dist/host/life/crash-report.js +173 -7
  258. package/dist/host/life/host-memory.d.ts +41 -0
  259. package/dist/host/life/host-memory.js +140 -0
  260. package/dist/host/life/index.d.ts +2 -0
  261. package/dist/host/life/index.js +2 -0
  262. package/dist/host/life/stop-events.d.ts +2 -0
  263. package/dist/host/life/stop-events.js +20 -3
  264. package/dist/host/life/stop-warning.js +1 -1
  265. package/dist/host/life/watchdog.d.ts +56 -0
  266. package/dist/host/life/watchdog.js +124 -0
  267. package/dist/host/native.d.ts +3 -2
  268. package/dist/host/native.js +22 -3
  269. package/dist/host/profile/claude-login.js +3 -2
  270. package/dist/host/sandbox/contained-deps.d.ts +2 -0
  271. package/dist/host/sandbox/contained-deps.js +17 -0
  272. package/dist/host/sandbox/index.d.ts +2 -0
  273. package/dist/host/sandbox/index.js +2 -0
  274. package/dist/host/sandbox/platform.d.ts +2 -0
  275. package/dist/host/sandbox/platform.js +7 -7
  276. package/dist/host/sandbox/tasklist.d.ts +13 -0
  277. package/dist/host/sandbox/tasklist.js +47 -0
  278. package/dist/host/secrets/launch-secrets.js +2 -1
  279. package/dist/host/sharing/current.js +2 -6
  280. package/dist/host/storage/contained/check.d.ts +9 -0
  281. package/dist/host/storage/contained/check.js +37 -0
  282. package/dist/host/storage/contained/contained-storage.d.ts +66 -0
  283. package/dist/host/storage/contained/contained-storage.js +168 -0
  284. package/dist/host/storage/contained/disk-port.d.ts +43 -0
  285. package/dist/host/storage/contained/disk-port.js +122 -0
  286. package/dist/host/storage/contained/index.d.ts +9 -0
  287. package/dist/host/storage/contained/index.js +8 -0
  288. package/dist/host/storage/contained/layout.d.ts +28 -0
  289. package/dist/host/storage/contained/layout.js +45 -0
  290. package/dist/host/storage/contained/resolve.d.ts +4 -0
  291. package/dist/host/storage/contained/resolve.js +30 -0
  292. package/dist/host/storage/contained/setting.d.ts +1 -0
  293. package/dist/host/storage/contained/setting.js +5 -0
  294. package/dist/host/storage/contained/state.d.ts +89 -0
  295. package/dist/host/storage/contained/state.js +166 -0
  296. package/dist/host/storage/contained/transfer.d.ts +24 -0
  297. package/dist/host/storage/contained/transfer.js +121 -0
  298. package/dist/host/storage/index.d.ts +2 -0
  299. package/dist/host/storage/index.js +2 -0
  300. package/dist/host/storage/providers/contained.d.ts +15 -0
  301. package/dist/host/storage/providers/contained.js +39 -0
  302. package/dist/host/storage/providers/folder.d.ts +12 -0
  303. package/dist/host/storage/providers/folder.js +20 -0
  304. package/dist/host/storage/providers/index.d.ts +4 -0
  305. package/dist/host/storage/providers/index.js +3 -0
  306. package/dist/host/storage/providers/registry.d.ts +9 -0
  307. package/dist/host/storage/providers/registry.js +14 -0
  308. package/dist/host/storage/providers/types.d.ts +33 -0
  309. package/dist/host/storage/providers/types.js +1 -0
  310. package/dist/host/tools/conpty.js +3 -2
  311. package/dist/host/tools/windows-terminal.js +8 -7
  312. package/dist/host/vpn/index.d.ts +1 -1
  313. package/dist/host/vpn/index.js +1 -1
  314. package/dist/host/vpn/prepare.d.ts +2 -0
  315. package/dist/host/vpn/prepare.js +63 -2
  316. package/dist/host/web/browser.d.ts +2 -0
  317. package/dist/host/web/browser.js +14 -1
  318. package/dist/host/web/index.d.ts +3 -2
  319. package/dist/host/web/index.js +3 -2
  320. package/dist/host/web/rpc.d.ts +24 -0
  321. package/dist/host/web/rpc.js +149 -0
  322. package/dist/host/web/runner.d.ts +2 -0
  323. package/dist/host/web/runner.js +1 -2
  324. package/dist/host/web/server.d.ts +7 -0
  325. package/dist/host/web/server.js +23 -3
  326. package/dist/runtime/agent.js +83 -18
  327. package/dist/runtime/daemon.js +1805 -1221
  328. package/dist/runtime/db.js +83 -18
  329. package/dist/runtime/main.js +779 -219
  330. package/dist/runtime/netproxy.js +73 -11
  331. package/dist/runtime/view.js +91 -16
  332. package/dist/selftest/agent-paths.test.js +11 -1
  333. package/dist/selftest/app.test.d.ts +2 -0
  334. package/dist/selftest/app.test.js +324 -0
  335. package/dist/selftest/channel.test.js +13 -0
  336. package/dist/selftest/cli.test.js +2 -1
  337. package/dist/selftest/config.test.js +59 -0
  338. package/dist/selftest/contained.test.d.ts +2 -0
  339. package/dist/selftest/contained.test.js +544 -0
  340. package/dist/selftest/credentials.test.js +1 -1
  341. package/dist/selftest/engine.test.d.ts +2 -0
  342. package/dist/selftest/engine.test.js +127 -0
  343. package/dist/selftest/guest-boot.test.js +47 -1
  344. package/dist/selftest/hcs.test.d.ts +2 -0
  345. package/dist/selftest/hcs.test.js +482 -0
  346. package/dist/selftest/heavy-load.test.d.ts +2 -0
  347. package/dist/selftest/heavy-load.test.js +355 -0
  348. package/dist/selftest/index.js +12 -0
  349. package/dist/selftest/layout.test.js +12 -0
  350. package/dist/selftest/menu-perf.test.d.ts +2 -0
  351. package/dist/selftest/menu-perf.test.js +248 -0
  352. package/dist/selftest/menu-screens.js +4 -2
  353. package/dist/selftest/menu.test.js +36 -8
  354. package/dist/selftest/network.test.js +20 -1
  355. package/dist/selftest/support/engine-cases.d.ts +8 -0
  356. package/dist/selftest/support/engine-cases.js +65 -0
  357. package/dist/selftest/support/engine-golden.d.ts +3 -0
  358. package/dist/selftest/support/engine-golden.js +66 -0
  359. package/dist/selftest/support/fixtures.d.ts +8 -1
  360. package/dist/selftest/support/hcs-probes.d.ts +17 -0
  361. package/dist/selftest/support/hcs-probes.js +41 -0
  362. package/dist/selftest/vpn.test.js +136 -3
  363. package/dist/selftest/web.test.js +101 -3
  364. package/dist/shared/channel/run-dir.d.ts +4 -1
  365. package/dist/shared/channel/run-dir.js +7 -1
  366. package/dist/shared/channel/schema.d.ts +35 -1
  367. package/dist/shared/channel/schema.js +27 -0
  368. package/dist/shared/channel/secrets.d.ts +1 -0
  369. package/dist/shared/channel/secrets.js +6 -0
  370. package/dist/shared/io/copy-tree.d.ts +7 -0
  371. package/dist/shared/io/copy-tree.js +45 -0
  372. package/dist/shared/io/index.d.ts +2 -0
  373. package/dist/shared/io/index.js +2 -0
  374. package/dist/shared/io/robocopy-result.d.ts +6 -0
  375. package/dist/shared/io/robocopy-result.js +23 -0
  376. package/dist/shared/memory/index.d.ts +1 -0
  377. package/dist/shared/memory/index.js +1 -0
  378. package/dist/shared/memory/policy.d.ts +38 -0
  379. package/dist/shared/memory/policy.js +63 -0
  380. package/dist/shared/network/firewall-rules.d.ts +8 -0
  381. package/dist/shared/network/firewall-rules.js +28 -0
  382. package/dist/shared/pty/output-filter.d.ts +14 -1
  383. package/dist/shared/pty/output-filter.js +22 -4
  384. package/dist/shared/pty/terminal.d.ts +4 -1
  385. package/dist/shared/pty/terminal.js +4 -2
  386. package/dist/shared/system.d.ts +1 -0
  387. package/dist/shared/system.js +1 -0
  388. package/dist/shared/vpn/download.d.ts +16 -0
  389. package/dist/shared/vpn/download.js +16 -0
  390. package/dist/shared/vpn/wireguard.d.ts +51 -0
  391. package/dist/shared/vpn/wireguard.js +227 -0
  392. package/dist/sync/sync-cycle.js +5 -5
  393. package/dist/sync/watcher.d.ts +13 -0
  394. package/dist/sync/watcher.js +92 -3
  395. package/dist/web/index.html +4 -3
  396. package/dist/web/main-NRRS5O6Z.js +157 -0
  397. package/dist/web/main-T5DMCHYV.css +1 -0
  398. package/native/csharp/HcsHost/AssemblyInfo.cs +14 -0
  399. package/native/csharp/HcsHost/Guard.cs +100 -0
  400. package/native/csharp/HcsHost/Hcs.cs +256 -0
  401. package/native/csharp/HcsHost/Json.cs +78 -0
  402. package/native/csharp/HcsHost/Program.cs +185 -0
  403. package/native/csharp/HcsHost/VirtualDisk.cs +95 -0
  404. package/native/csharp/HostTools/AssemblyInfo.cs +14 -0
  405. package/native/csharp/HostTools/MemoryStatus.cs +128 -0
  406. package/native/csharp/HostTools/Program.cs +58 -17
  407. package/native/scripts/check-contained.ps1 +28 -0
  408. package/native/scripts/contained-common.ps1 +241 -0
  409. package/native/scripts/dismount-contained.ps1 +66 -0
  410. package/native/scripts/memory-status.ps1 +42 -0
  411. package/native/scripts/mount-contained.ps1 +139 -0
  412. package/native/scripts/run-elevated.ps1 +17 -4
  413. package/native/scripts/stop-events.ps1 +23 -8
  414. package/package.json +9 -2
  415. package/sandbox/csharp/MemGov.cs +466 -0
  416. package/sandbox/csharp/PtyHost.cs +111 -5
  417. package/sandbox/scripts/new-shortcuts.ps1 +3 -1
  418. package/schemas/config.schema.json +66 -8
  419. package/schemas/sandbox-config.schema.json +3 -3
  420. package/dist/cli/commands/sessions/control.d.ts +0 -4
  421. package/dist/cli/commands/sessions/control.js +0 -66
  422. package/dist/cli/commands/sessions/list.d.ts +0 -2
  423. package/dist/cli/commands/sessions/list.js +0 -48
  424. package/dist/cli/commands/sessions/status.d.ts +0 -4
  425. package/dist/cli/commands/sessions/status.js +0 -114
  426. package/dist/cli/commands/sessions/window.d.ts +0 -3
  427. package/dist/cli/commands/sessions/window.js +0 -18
  428. package/dist/cli/menu/settings/path-kind.js +0 -44
  429. package/dist/web/main-CXESL347.js +0 -45
  430. package/dist/web/main-VLR7DVQ4.css +0 -1
  431. /package/dist/{cli/menu/settings → app/kernel}/schema-node.d.ts +0 -0
  432. /package/dist/{cli/menu/settings → app/kernel}/schema-node.js +0 -0
@@ -0,0 +1,544 @@
1
+ // Contained storage (beta, phase 2 of docs/design/engines.md): the data set split into mappings, the mount record and
2
+ // its refcount, stale mounts, the provider, the move into and out of an image, and the PowerShell scripts (parsed,
3
+ // CRLF, run in their dry-run mode against temp folders: they print the diskpart lines they would run and refuse what
4
+ // is not safe). No test mounts a disk: the real disk port is only started on a PC, everything else uses a fake.
5
+ import crypto from 'node:crypto';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { spawnSync } from 'node:child_process';
9
+ import { assertNoGuestOverlap } from '../core/config/index.js';
10
+ import { isWin } from '../core/util/index.js';
11
+ import { CONTAINED_CHECKS } from '../cli/commands/repair/checks/contained.js';
12
+ import { storageBusy } from '../cli/commands/storage/storage.js';
13
+ import { generateSandbox, GUEST } from '../host/launch/index.js';
14
+ import { currentUserSid } from '../host/sharing/index.js';
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';
16
+ import { EMPTY_STATE } from '../host/storage/contained/state.js';
17
+ import { SYSTEM } from '../shared/system.js';
18
+ const SCRIPTS = ['contained-common', 'mount-contained', 'dismount-contained', 'check-contained', 'run-elevated'];
19
+ /** Mixed-endian GUID bytes, as VHDX (and .NET's Guid) store them. */
20
+ function guidBytes(text) {
21
+ const hex = text.replace(/-/g, '');
22
+ const raw = Buffer.from(hex, 'hex');
23
+ return Buffer.concat([
24
+ Buffer.from(raw.subarray(0, 4)).reverse(),
25
+ Buffer.from(raw.subarray(4, 6)).reverse(),
26
+ Buffer.from(raw.subarray(6, 8)).reverse(),
27
+ raw.subarray(8, 16),
28
+ ]);
29
+ }
30
+ /** A file with the parts of a VHDX header the scripts read (signature, region table, metadata table). */
31
+ function fakeVhdx(file, o) {
32
+ const buf = Buffer.alloc(2 * 1024 * 1024);
33
+ buf.write('vhdxfile', 0, 'ascii');
34
+ const table = 196608;
35
+ buf.write('regi', table, 'ascii');
36
+ buf.writeUInt32LE(1, table + 8);
37
+ guidBytes('8b7ca206-4790-4b9a-b8fe-575f050f886e').copy(buf, table + 16);
38
+ buf.writeBigUInt64LE(BigInt(1048576), table + 32);
39
+ buf.writeUInt32LE(65536, table + 40);
40
+ const md = 1048576;
41
+ buf.write('metadata', md, 'ascii');
42
+ const items = [
43
+ { guid: '2fa54224-cd1b-4876-b211-5dbed83bf4b8', offset: 65536 },
44
+ ...(o.parent ? [{ guid: 'a8d35f2d-b30b-454d-abf7-d3d84834ab0c', offset: 65600 }] : []),
45
+ ];
46
+ buf.writeUInt16LE(items.length, md + 10);
47
+ items.forEach((item, i) => {
48
+ const at = md + 32 + 32 * i;
49
+ guidBytes(item.guid).copy(buf, at);
50
+ buf.writeUInt32LE(item.offset, at + 16);
51
+ buf.writeUInt32LE(8, at + 20);
52
+ });
53
+ buf.writeBigUInt64LE(BigInt(o.gb) * 1024n ** 3n, md + 65536);
54
+ fs.writeFileSync(file, buf);
55
+ }
56
+ class FakePort {
57
+ paths;
58
+ calls = [];
59
+ mounted = false;
60
+ imageExists = false;
61
+ failMount = null;
62
+ constructor(paths) {
63
+ this.paths = paths;
64
+ }
65
+ mount(request) {
66
+ this.calls.push('mount' + (request.create ? ':create' : ''));
67
+ if (this.failMount)
68
+ return Promise.resolve(this.outcome(false, this.failMount));
69
+ this.mounted = true;
70
+ this.imageExists = true;
71
+ return Promise.resolve(this.outcome(true, 'mounted'));
72
+ }
73
+ dismount() {
74
+ this.calls.push('dismount');
75
+ this.mounted = false;
76
+ return Promise.resolve(this.outcome(true, 'detached'));
77
+ }
78
+ reality() {
79
+ return Promise.resolve({
80
+ imageExists: this.imageExists,
81
+ imageBytes: 1,
82
+ mountExists: fs.existsSync(this.paths.mount),
83
+ mountIsMountPoint: this.mounted,
84
+ attached: this.mounted,
85
+ });
86
+ }
87
+ outcome(ok, message) {
88
+ return { ok, message, created: false, alreadyMounted: false, notes: [] };
89
+ }
90
+ }
91
+ export const containedSuite = {
92
+ name: 'contained',
93
+ title: 'contained storage: mapping split, mount record and refcount, stale mounts, scripts (beta)',
94
+ timeoutMs: 300000,
95
+ run: async (t, f) => {
96
+ const config = f.baseConfig;
97
+ const GUEST_DATA = 'C:\\AgentData';
98
+ t.section('the data set splits into mappings with the same guest paths');
99
+ const storageRoot = t.dir('contained', 'storage');
100
+ const dataPath = t.dir('contained', 'storage', 'data', 'main');
101
+ const paths = containedPaths(storageRoot, 'main');
102
+ const mappings = containedMappings(dataPath, paths.mount, GUEST_DATA);
103
+ t.equal(mappings.map((m) => m.guest), ['run', ...CONTAINED_FOLDERS].map((n) => GUEST_DATA + '\\' + n), 'run and every contained folder is mapped at its place under C:\\AgentData');
104
+ t.check(['run', 'tools', 'state', 'logs'].every((n) => mappings.some((m) => m.guest === GUEST_DATA + '\\' + n)), 'the guest still has C:\\AgentData\\run, tools, state and logs (what one mapping of the data folder gave)');
105
+ t.equal(mappings[0].host, path.join(dataPath, 'run'), 'run stays the plain host folder (the host reads it)');
106
+ t.check(mappings.slice(1).every((m) => m.host.startsWith(paths.mount + path.sep) && !m.readOnly), 'the others come from inside the mount folder, read/write');
107
+ assertNoGuestOverlap(mappings);
108
+ t.check(true, 'no mapping lies inside another');
109
+ t.section('a launch with contained storage maps the split, not the whole folder');
110
+ const vhdxConfig = { ...structuredClone(config), storage: { ...config.storage, contained: 'vhdx' } };
111
+ const run = await f.run({}, vhdxConfig);
112
+ const launchMappings = containedMappings(run.dataPath, containedPaths(run.storageRoot, setNameOf(run.dataPath)).mount, GUEST.data);
113
+ const generated = generateSandbox({
114
+ packageRoot: t.packageRoot,
115
+ run,
116
+ harnessArgs: ['--version'],
117
+ probe: f.probe,
118
+ pcLanguages: ['python'],
119
+ dataMappings: launchMappings,
120
+ });
121
+ const guests = generated.mappings.map((m) => m.guest);
122
+ t.check(!guests.includes(GUEST.data), 'C:\\AgentData itself is no longer one mapping');
123
+ t.check(launchMappings.every((m) => guests.includes(m.guest) && generated.mappings.some((g) => g.host === m.host)), 'every split mapping is in the launch');
124
+ const wsb = fs.readFileSync(generated.wsbPath, 'utf8');
125
+ t.check(CONTAINED_FOLDERS.every((n) => wsb.includes('<SandboxFolder>C:\\AgentData\\' + n + '</SandboxFolder>')) &&
126
+ wsb.includes('<SandboxFolder>C:\\AgentData\\run</SandboxFolder>'), 'the .wsb has a folder for each of them');
127
+ await t.throws(() => generateSandbox({ packageRoot: t.packageRoot, run, harnessArgs: [], probe: f.probe }), 'Contained storage was not prepared for this launch', 'contained storage without a mounted image stops before anything is written');
128
+ t.section('the mount record: refcount and transitions');
129
+ let state = EMPTY_STATE;
130
+ const holder = (id, pid) => ({ id, pid, since: 'now' });
131
+ state = addHolder(addHolder(state, 'main', holder('launch', 10)), 'main', holder('watcher', 11));
132
+ t.equal(setStateOf(state, 'main').holders.length, 2, 'two holders');
133
+ state = addHolder(state, 'main', holder('watcher', 12));
134
+ t.equal(setStateOf(state, 'main').holders.map((h) => h.pid), [10, 12], 'the same holder id is refreshed, not added twice');
135
+ t.equal(setStateOf(pruneHolders(state, 'main', (pid) => pid === 12), 'main').holders.map((h) => h.id), ['watcher'], 'a holder whose process is gone is dropped');
136
+ t.equal(setStateOf(removeHolder(state, 'main', 'launch'), 'main').holders.map((h) => h.id), ['watcher'], 'a holder lets go by id');
137
+ t.check(setStateOf(markMounted(state, 'main', true), 'main').mounted, 'mounted is recorded');
138
+ t.equal(state.sets.other, undefined, 'another set is untouched');
139
+ const base = t.dir('contained', 'life');
140
+ const lifePaths = containedPaths(base, 'main');
141
+ const port = new FakePort(lifePaths);
142
+ let alive = new Set([100, 101]);
143
+ let running = false;
144
+ const deps = {
145
+ storageRoot: base,
146
+ maxGb: 64,
147
+ protect: () => undefined,
148
+ userSid: () => 'S-1-5-21-1-2-3-1001',
149
+ port,
150
+ sandboxRunning: () => running,
151
+ alive: (pid) => alive.has(pid),
152
+ deviceOf: (dir) => (dir === lifePaths.mount && port.mounted ? 2 : 1),
153
+ };
154
+ const storage = new ContainedStorage(deps, 'main');
155
+ await t.throws(() => storage.acquire({ id: 'launch', pid: 100 }), 'image is missing', 'an image that is not there is not made by a launch');
156
+ t.equal(port.calls, [], 'nothing was mounted for that');
157
+ t.equal(storage.liveHolders(), 0, 'and the failed holder was removed again');
158
+ const mount = await storage.acquire({ id: 'launch', pid: 100 }, { create: true });
159
+ t.equal([mount, port.calls], [lifePaths.mount, ['mount:create']], 'the first launch creates and mounts the image once');
160
+ t.check(CONTAINED_FOLDERS.every((n) => fs.existsSync(path.join(mount, n))), 'the data set folders exist inside the mount folder');
161
+ await storage.acquire({ id: 'watcher', pid: 101 });
162
+ t.equal(port.calls, ['mount:create'], 'a second holder that finds it mounted does not mount again');
163
+ t.equal(storage.liveHolders(), 2, 'both hold it');
164
+ t.equal(await storage.release('launch'), { dismounted: false, note: 'another command still uses it' }, 'the first to let go leaves it mounted');
165
+ running = true;
166
+ alive = new Set([]);
167
+ t.equal((await storage.release('watcher')).dismounted, false, 'never dismounted while a sandbox runs');
168
+ running = false;
169
+ t.equal(storage.recordedMounted(), true, 'it stays recorded as mounted');
170
+ const done = await storage.release('watcher');
171
+ t.equal([done.dismounted, port.calls], [true, ['mount:create', 'dismount']], 'the last to let go dismounts, once');
172
+ t.equal(storage.recordedMounted(), false, 'and the record says so');
173
+ t.equal((await storage.release('watcher')).dismounted, false, 'letting go of nothing mounted asks for no permission');
174
+ t.equal(port.calls.length, 2, 'no extra dismount call');
175
+ port.calls.length = 0;
176
+ alive = new Set([100]);
177
+ await storage.acquire({ id: 'launch', pid: 100 });
178
+ t.equal(port.calls, ['mount'], 'with the image there it is only attached, never created again');
179
+ t.section('failures leave nothing behind');
180
+ port.failMount = 'The image is bigger than the limit of 64 GB.';
181
+ port.mounted = false;
182
+ port.calls.length = 0;
183
+ const other = new ContainedStorage({ ...deps, storageRoot: t.dir('contained', 'fail') }, 'main');
184
+ port.paths = other.paths;
185
+ port.imageExists = true;
186
+ await t.throws(() => other.acquire({ id: 'launch', pid: 100 }), 'bigger than the limit', 'a refusal of the script stops the launch with its reason');
187
+ t.equal([other.liveHolders(), other.recordedMounted()], [0, false], 'no holder and no mounted record are left');
188
+ port.failMount = null;
189
+ const wrongDevice = new ContainedStorage({ ...deps, storageRoot: t.dir('contained', 'device'), deviceOf: () => 1 }, 'main');
190
+ port.paths = wrongDevice.paths;
191
+ await t.throws(() => wrongDevice.acquire({ id: 'launch', pid: 100 }), 'not mounted at', "a mount folder on this PC's own disk is never written to");
192
+ t.equal(fs.readdirSync(wrongDevice.paths.mount), [], 'nothing was written into it');
193
+ t.section('stale mounts');
194
+ const reality = (over) => ({
195
+ imageExists: true,
196
+ imageBytes: 1,
197
+ mountExists: true,
198
+ mountIsMountPoint: false,
199
+ attached: false,
200
+ ...over,
201
+ });
202
+ const record = (mounted) => ({ mounted, holders: [] });
203
+ t.equal(diagnose({ state: record(false), reality: reality({}), live: 0, sandboxRunning: false }).kind, 'ok', 'nothing mounted, nothing recorded: fine');
204
+ t.equal(diagnose({ state: record(true), reality: reality({}), live: 0, sandboxRunning: false }).kind, 'stale-state', 'recorded as mounted but not: the record is stale');
205
+ t.equal(diagnose({
206
+ state: record(true),
207
+ reality: reality({ mountIsMountPoint: true, attached: true }),
208
+ live: 0,
209
+ sandboxRunning: false,
210
+ }).kind, 'stale-mount', 'mounted with no holder and no sandbox: a stale mount (dismount it)');
211
+ t.equal(diagnose({ state: record(false), reality: reality({ mountIsMountPoint: true }), live: 0, sandboxRunning: false })
212
+ .kind, 'stale-mount', 'mounted but not even recorded: also stale');
213
+ t.equal(diagnose({ state: record(true), reality: reality({ mountIsMountPoint: true }), live: 1, sandboxRunning: false })
214
+ .kind, 'in-use', 'a live holder: in use');
215
+ t.equal(diagnose({ state: record(true), reality: reality({ mountIsMountPoint: true }), live: 0, sandboxRunning: true })
216
+ .kind, 'in-use', 'a running sandbox: in use');
217
+ t.equal(diagnose({ state: record(false), reality: reality({ imageExists: false }), live: 0, sandboxRunning: false }).kind, 'image-missing', 'no image file');
218
+ const staleBase = t.dir('contained', 'stale');
219
+ const staleFile = new StateFile(containedPaths(staleBase, 'main'), (pid) => pid === 5);
220
+ staleFile.update((s) => addHolder(markMounted(s, 'main', true), 'main', holder('watcher', 5)));
221
+ t.equal(staleMountNote(staleBase, 'main', false, (pid) => pid === 5), null, 'a running holder: no note');
222
+ t.includes(staleMountNote(staleBase, 'main', false, () => false) ?? '', 'sandbox repair', 'a mount nobody holds: the note points at repair');
223
+ t.equal(staleMountNote(staleBase, 'main', true, () => false), null, 'no note while a sandbox runs');
224
+ t.equal(mountedSets(staleBase), ['main'], 'a storage move sees the mounted set');
225
+ t.includes(storageBusy(staleBase) ?? '', 'is mounted', 'and waits for it');
226
+ fs.writeFileSync(containedPaths(staleBase, 'main').stateFile, '{ not json');
227
+ t.equal([mountedSets(staleBase), staleMountNote(staleBase, 'main', false)], [[], null], 'a broken record counts as empty');
228
+ const staleLock = containedPaths(staleBase, 'main').lockFile;
229
+ fs.writeFileSync(staleLock, '99999999');
230
+ new StateFile(containedPaths(staleBase, 'main'), () => false).update((s) => markMounted(s, 'main', false));
231
+ t.check(!fs.existsSync(staleLock), 'a lock left by a dead process is taken over and removed');
232
+ t.section('where the host finds the folders');
233
+ t.equal(hostDataFolder(dataPath, 'tools'), path.join(dataPath, 'tools'), 'without contained storage: the plain data folder');
234
+ const fakeStale = containedPaths(storageRoot, 'main');
235
+ new StateFile(fakeStale).update((s) => markMounted(s, 'main', true));
236
+ fs.mkdirSync(fakeStale.mount, { recursive: true });
237
+ t.equal(hostDataFolder(dataPath, 'tools'), path.join(dataPath, 'tools'), 'a record that says mounted, on the same volume: still the plain folder (never into the empty mount folder)');
238
+ t.section('availability and the provider');
239
+ const facts = { windows: true, insideSandbox: false, diskpartPresent: true, powershellPresent: true };
240
+ t.equal(containedAvailability(facts).available, true, 'a normal Windows PC can use it');
241
+ t.includes(containedAvailability({ ...facts, insideSandbox: true }).reasons.join(' '), 'inside a Windows Sandbox', 'not inside a sandbox');
242
+ t.includes(containedAvailability({ ...facts, diskpartPresent: false }).reasons.join(' '), 'diskpart.exe', 'not without diskpart');
243
+ t.includes(containedAvailability({ ...facts, windows: false }).reasons.join(' '), 'needs Windows', 'not off Windows');
244
+ const unavailable = () => ({ available: false, reasons: ['No.'], fixes: ['Fix it.'] });
245
+ await t.throws(() => providerFor(vhdxConfig, { availability: unavailable }), 'Contained storage cannot be used: No.', 'the provider is refused with the reasons');
246
+ await t.throws(() => providerFor(vhdxConfig, { availability: () => containedAvailability(facts) }), 'was not set up', 'and needs its deps');
247
+ t.equal(providerFor(config).id, 'folder', 'off: the folder provider');
248
+ const providerBase = t.dir('contained', 'provider');
249
+ const providerPaths = containedPaths(providerBase, 'main');
250
+ const providerPort = new FakePort(providerPaths);
251
+ providerPort.imageExists = true;
252
+ const provider = providerFor(vhdxConfig, {
253
+ availability: () => containedAvailability(facts),
254
+ contained: {
255
+ ...deps,
256
+ storageRoot: providerBase,
257
+ port: providerPort,
258
+ alive: () => true,
259
+ deviceOf: (dir) => (dir === providerPaths.mount && providerPort.mounted ? 2 : 1),
260
+ },
261
+ });
262
+ t.equal(provider.id, 'vhdx-mounted', 'on: the vhdx-mounted provider');
263
+ const providerData = t.dir('contained', 'provider', 'data', 'main');
264
+ const prepared = await provider.prepare({ dataPath: providerData, guest: GUEST_DATA });
265
+ t.equal(prepared.mappings, containedMappings(providerData, providerPaths.mount, GUEST_DATA), 'prepare mounts and returns the split');
266
+ t.equal(prepared.attachments.length, prepared.mappings.length, 'one attachment for each mapping');
267
+ t.check(fs.existsSync(path.join(providerData, 'run')), 'the run folder exists on this PC');
268
+ await provider.release();
269
+ t.equal(providerPort.calls, ['mount', 'dismount'], 'release lets go and, with nobody else holding it, dismounts');
270
+ t.section('a repair check only shows up when contained storage is in use');
271
+ const env = { ctx: { loaded: { config } }, storageRoot: t.dir('contained', 'doctor') };
272
+ t.equal(CONTAINED_CHECKS.map((c) => c.applies?.(env) ?? true), [false, false], 'not shown while it is off and no image exists');
273
+ t.section('moving the folders into the image and back');
274
+ const home = t.dir('contained', 'move', 'plain');
275
+ const image = t.dir('contained', 'move', 'image');
276
+ t.write(path.join(home, 'tools', 'node', 'node.exe'), 'node');
277
+ t.write(path.join(home, 'tools', 'bin', 'a.txt'), 'a');
278
+ t.write(path.join(home, 'logs', 'daemon.log'), 'log');
279
+ t.dir('contained', 'move', 'plain', 'state');
280
+ t.write(path.join(home, 'run', 'sandbox.json'), '{}');
281
+ const plan = planTransfer(home, image, home);
282
+ t.equal(plan.folders.map((p) => p.name), ['tools', 'logs'], 'only folders that hold something, never run');
283
+ t.equal([plan.total.files, plan.total.bytes], [3, 8], 'size and file count are measured first');
284
+ t.check(plan.estimate.seconds >= 0 && plan.estimate.fits, 'with a time and room check');
285
+ const moved = await runTransfer(plan, home);
286
+ t.equal([moved.files, moved.notes], [3, []], 'everything is copied');
287
+ t.equal(fs.readFileSync(path.join(image, 'tools', 'node', 'node.exe'), 'utf8'), 'node', 'the content arrived');
288
+ t.check(!fs.existsSync(path.join(home, 'tools')) && !fs.existsSync(path.join(home, 'logs')), 'the originals are removed afterwards');
289
+ t.check(fs.existsSync(path.join(home, 'run', 'sandbox.json')), 'the run folder was not touched');
290
+ const back = planTransfer(image, home, home);
291
+ t.equal(back.folders.map((p) => p.name), ['tools', 'logs'], 'moving back out lists the same folders');
292
+ t.write(path.join(home, 'logs', 'blocker.log'), 'x');
293
+ await t.throws(() => runTransfer(back, image), 'already holds files', 'a target that holds files is never merged or overwritten');
294
+ t.check(fs.existsSync(path.join(image, 'tools', 'node', 'node.exe')), 'and nothing was removed from the image');
295
+ fs.rmSync(path.join(home, 'logs'), { recursive: true });
296
+ const returned = await runTransfer(back, image);
297
+ t.equal(returned.files, 3, 'with the target free, the files go back');
298
+ t.check(fs.existsSync(path.join(home, 'tools', 'node', 'node.exe')) && !fs.existsSync(path.join(image, 'tools')), 'and leave the image');
299
+ t.section('the disk port asks the scripts, through files');
300
+ const seen = [];
301
+ const runner = (_file, args) => {
302
+ const json = args[args.indexOf('-Json') + 1];
303
+ const sha = args[args.indexOf('-Sha256') + 1];
304
+ seen.push({
305
+ args: [...args],
306
+ request: JSON.parse(fs.readFileSync(json, 'utf8')),
307
+ sha,
308
+ fileSha: crypto.createHash('sha256').update(fs.readFileSync(json)).digest('hex'),
309
+ });
310
+ fs.writeFileSync(path.join(path.dirname(json), 'result.json'), JSON.stringify({ ok: true, message: 'ok', created: true, notes: 'one' }));
311
+ return Promise.resolve({ status: 0, stdout: '', stderr: '' });
312
+ };
313
+ const fakeSid = 'S-1-5-21-1-2-3-1001';
314
+ const outcome = await new ScriptDiskPort(runner).mount({
315
+ paths: lifePaths,
316
+ maxGb: 64,
317
+ create: true,
318
+ userSid: fakeSid,
319
+ });
320
+ t.equal([outcome.ok, outcome.created, outcome.notes], [true, true, ['one']], 'the answer is read from result.json');
321
+ t.equal(seen[0].request, {
322
+ set: 'main',
323
+ storageRoot: lifePaths.storageRoot,
324
+ image: lifePaths.image,
325
+ mount: lifePaths.mount,
326
+ userSid: fakeSid,
327
+ maxGb: 64,
328
+ create: true,
329
+ }, 'the request names the image, the mount folder and your SID');
330
+ t.equal(seen[0].sha, seen[0].fileSha, 'with the checksum of the request file, which the script checks');
331
+ t.check(seen[0].args.includes('run-elevated.ps1') || seen[0].args.some((a) => a.endsWith('run-elevated.ps1')), 'it goes through the one elevation helper');
332
+ t.check(seen[0].args.some((a) => a.endsWith('mount-contained.ps1')), 'which runs the fixed mount script');
333
+ t.check(!fs.existsSync(path.dirname(String(seen[0].args[seen[0].args.indexOf('-Json') + 1]))), 'the private temp folder is removed afterwards');
334
+ const silent = () => Promise.resolve({ status: 1, stdout: '', stderr: '' });
335
+ await t.throws(() => new ScriptDiskPort(silent).dismount({ paths: lifePaths, userSid: fakeSid }), 'did not give permission', 'a declined prompt is said plainly');
336
+ if (!isWin) {
337
+ t.skip('PowerShell scripts (needs Windows PowerShell 5.1)');
338
+ return;
339
+ }
340
+ t.section('the scripts: encoding, syntax');
341
+ const scriptDir = path.join(t.packageRoot, 'native', 'scripts');
342
+ for (const name of SCRIPTS) {
343
+ const file = path.join(scriptDir, name + '.ps1');
344
+ const text = fs.readFileSync(file, 'utf8');
345
+ t.check(!/(?<!\r)\n/.test(text) && text.includes('\r\n'), name + '.ps1 is CRLF all the way');
346
+ t.check(text.startsWith('#Requires -Version 5.1'), name + '.ps1 starts with #Requires -Version 5.1');
347
+ const parse = spawnSync(SYSTEM.powershell(), [
348
+ '-NoProfile',
349
+ '-NonInteractive',
350
+ '-ExecutionPolicy',
351
+ 'Bypass',
352
+ '-File',
353
+ path.join(scriptDir, 'check-syntax.ps1'),
354
+ '-Path',
355
+ file,
356
+ ], { encoding: 'utf8' });
357
+ t.check(parse.status === 0, name + '.ps1 parses in PowerShell 5.1', parse.stderr + parse.stdout);
358
+ }
359
+ for (const name of ['mount-contained', 'dismount-contained']) {
360
+ const text = fs.readFileSync(path.join(scriptDir, name + '.ps1'), 'utf8');
361
+ t.check(/^param\(/m.test(text), name + '.ps1 has a param() block');
362
+ }
363
+ const mountText = fs.readFileSync(path.join(scriptDir, 'mount-contained.ps1'), 'utf8');
364
+ const dismountText = fs.readFileSync(path.join(scriptDir, 'dismount-contained.ps1'), 'utf8');
365
+ t.equal((mountText.match(/format fs=ntfs/g) ?? []).length, 1, 'the mount script has exactly one format line');
366
+ const formatAt = mountText.indexOf('format fs=ntfs');
367
+ const elseAt = mountText.lastIndexOf('} else {', formatAt);
368
+ t.check(elseAt > mountText.indexOf('# Attach and mount the first partition') &&
369
+ mountText.slice(elseAt, formatAt).includes('create vdisk'), 'and it sits in the branch for an image that did not exist');
370
+ const code = (text) => text
371
+ .split('\r\n')
372
+ .filter((l) => !l.trim().startsWith('#'))
373
+ .join('\n');
374
+ t.check(!/format|create vdisk|delete|Remove-Item|clean all/i.test(code(dismountText)), 'the dismount script never formats, creates or deletes');
375
+ t.check(!/Remove-Item|del |Delete\(/i.test(code(mountText)), 'the mount script never deletes anything');
376
+ t.section('the scripts in dry-run: what they print and what they refuse');
377
+ const sid = currentUserSid();
378
+ const dryRoot = t.dir('contained', 'dry', 'storage');
379
+ const dry = containedPaths(dryRoot, 'main');
380
+ for (const dir of [dry.imagesDir, dry.mountsDir, dry.mount])
381
+ fs.mkdirSync(dir, { recursive: true });
382
+ let n = 0;
383
+ const dryRun = (script, request, extra = []) => {
384
+ const dir = t.dir('contained', 'dry', 'req' + n++);
385
+ const file = path.join(dir, 'request.json');
386
+ fs.writeFileSync(file, JSON.stringify(request));
387
+ const r = spawnSync(SYSTEM.powershell(), [
388
+ '-NoProfile',
389
+ '-NonInteractive',
390
+ '-ExecutionPolicy',
391
+ 'Bypass',
392
+ '-File',
393
+ path.join(scriptDir, script + '.ps1'),
394
+ '-Json',
395
+ file,
396
+ '-DryRun',
397
+ ...extra,
398
+ ], { encoding: 'utf8' });
399
+ return { status: r.status, out: (r.stdout ?? '') + (r.stderr ?? '') };
400
+ };
401
+ const request = (over = {}) => ({
402
+ set: 'main',
403
+ storageRoot: dry.storageRoot,
404
+ image: dry.image,
405
+ mount: dry.mount,
406
+ userSid: sid,
407
+ maxGb: 64,
408
+ create: false,
409
+ ...over,
410
+ });
411
+ const diskpartLines = (out) => out
412
+ .split(/\r?\n/)
413
+ .filter((l) => l.startsWith('DISKPART: '))
414
+ .map((l) => l.slice(10));
415
+ const created = dryRun('mount-contained', request({ create: true }));
416
+ t.check(created.status === 0 && created.out.includes('RESULT: ok'), 'a first mount (no image yet) is accepted', created.out);
417
+ t.equal(diskpartLines(created.out), [
418
+ `create vdisk file="${dry.image}" maximum=65536 type=expandable`,
419
+ `select vdisk file="${dry.image}"`,
420
+ 'attach vdisk',
421
+ 'create partition primary',
422
+ 'format fs=ntfs quick label=AgentData',
423
+ `assign mount="${dry.mount}"`,
424
+ ], 'it would create a dynamic image of the size limit, partition, format and mount it');
425
+ fakeVhdx(dry.image, { gb: 64 });
426
+ const existing = dryRun('mount-contained', request());
427
+ t.check(existing.status === 0, 'an existing image is accepted', existing.out);
428
+ t.equal(diskpartLines(existing.out), [`select vdisk file="${dry.image}"`, 'attach vdisk', 'select partition 1', `assign mount="${dry.mount}"`], 'an existing image is only attached and mounted');
429
+ t.check(!/format|create /i.test(existing.out.replace('RESULT: ok', '')), 'never formatted, never created over');
430
+ const named = diskpartLines(existing.out)
431
+ .concat(diskpartLines(created.out))
432
+ .filter((l) => /file=|mount=/.test(l));
433
+ t.check(named.every((l) => [...l.matchAll(/(?:file|mount)="([^"]*)"/g)].every((m) => m[1] === dry.image || m[1] === dry.mount)), 'every diskpart line names only the given image file and mount folder');
434
+ const detach = dryRun('dismount-contained', request());
435
+ t.check(detach.status === 0, 'a dismount is accepted', detach.out);
436
+ t.equal(diskpartLines(detach.out), [`select vdisk file="${dry.image}"`, 'detach vdisk'], 'it would only detach this image');
437
+ const refused = (what, run, text) => t.check(run.status === 1 &&
438
+ run.out.includes('RESULT: refused') &&
439
+ run.out.toLowerCase().includes(text.toLowerCase()) &&
440
+ diskpartLines(run.out).length === 0, 'refused: ' + what, run.out);
441
+ const outside = t.dir('contained', 'dry', 'outside');
442
+ refused('an image outside the storage folder', dryRun('mount-contained', request({ image: path.join(outside, 'main.vhdx') })), 'must be');
443
+ refused('a mount folder outside the storage folder', dryRun('mount-contained', request({ mount: outside })), 'must be');
444
+ refused('a storage folder that is not the one the paths are in', dryRun('mount-contained', request({ storageRoot: outside })), 'must be');
445
+ refused('a path with ..', dryRun('mount-contained', request({ image: dry.imagesDir + '\\..\\images\\main.vhdx' })), 'plain full path');
446
+ refused('a relative path', dryRun('mount-contained', request({ mount: 'mounts\\main' })), 'plain full path');
447
+ refused('a data set name that is not allowed', dryRun('mount-contained', request({ set: 'a b' })), 'not allowed');
448
+ refused('another user (the files are not theirs)', dryRun('mount-contained', request({ userSid: 'S-1-5-21-1-2-3-1001' })), 'not yours');
449
+ refused('a size limit out of range', dryRun('mount-contained', request({ maxGb: 100000 })), 'between 8 and 4096');
450
+ refused('a wrong checksum', dryRun('mount-contained', request(), ['-Sha256', '00']), 'changed after it was written');
451
+ refused('creating over an image that exists', dryRun('mount-contained', request({ create: true })), 'refusing to create over');
452
+ const gone = path.join(dry.imagesDir, 'main.vhdx.away');
453
+ fs.renameSync(dry.image, gone);
454
+ refused('an image that is missing when it should exist', dryRun('mount-contained', request()), 'missing');
455
+ refused('a dismount of an image that is missing', dryRun('dismount-contained', request()), 'missing');
456
+ fakeVhdx(dry.image, { gb: 64, parent: true });
457
+ refused('a differencing image (it has a parent)', dryRun('mount-contained', request()), 'differencing');
458
+ fakeVhdx(dry.image, { gb: 128 });
459
+ refused('an image bigger than the limit', dryRun('mount-contained', request()), 'bigger than the limit');
460
+ fs.writeFileSync(dry.image, 'not a vhdx');
461
+ refused('a file that is not a VHDX', dryRun('mount-contained', request()), 'not a VHDX');
462
+ fakeVhdx(dry.image, { gb: 64 });
463
+ fs.writeFileSync(path.join(dry.mount, 'stray.txt'), 'x');
464
+ refused('a mount folder that is not empty', dryRun('mount-contained', request()), 'not empty');
465
+ fs.rmSync(path.join(dry.mount, 'stray.txt'));
466
+ const linkTarget = t.dir('contained', 'dry', 'linktarget');
467
+ const linkedRoot = t.dir('contained', 'dry', 'linked');
468
+ const linked = containedPaths(linkedRoot, 'main');
469
+ fs.mkdirSync(linked.mountsDir, { recursive: true });
470
+ fs.mkdirSync(linked.mount, { recursive: true });
471
+ const junction = spawnSync(SYSTEM.cmd(), ['/c', 'mklink', '/J', linked.imagesDir, linkTarget], {
472
+ encoding: 'utf8',
473
+ });
474
+ if (junction.status === 0) {
475
+ fakeVhdx(path.join(linkTarget, 'main.vhdx'), { gb: 8 });
476
+ refused('an images folder that is a link', dryRun('mount-contained', {
477
+ ...request(),
478
+ storageRoot: linked.storageRoot,
479
+ image: linked.image,
480
+ mount: linked.mount,
481
+ }), 'link');
482
+ fs.rmdirSync(linked.imagesDir);
483
+ }
484
+ else
485
+ t.skip('images folder as a junction (could not make one here)');
486
+ const linkedRoot2 = t.dir('contained', 'dry', 'linked2');
487
+ const linked2 = containedPaths(linkedRoot2, 'main');
488
+ fs.mkdirSync(linked2.imagesDir, { recursive: true });
489
+ fs.mkdirSync(linked2.mountsDir, { recursive: true });
490
+ fs.mkdirSync(linked2.mount, { recursive: true });
491
+ fakeVhdx(path.join(linkTarget, 'real.vhdx'), { gb: 8 });
492
+ try {
493
+ fs.symlinkSync(path.join(linkTarget, 'real.vhdx'), linked2.image, 'file');
494
+ refused('an image file that is a link', dryRun('mount-contained', {
495
+ ...request(),
496
+ storageRoot: linked2.storageRoot,
497
+ image: linked2.image,
498
+ mount: linked2.mount,
499
+ }), 'link');
500
+ }
501
+ catch {
502
+ t.skip('image as a symbolic link (not allowed here)');
503
+ }
504
+ const junctionMount = t.dir('contained', 'dry', 'linked3');
505
+ const linked3 = containedPaths(junctionMount, 'main');
506
+ fs.mkdirSync(linked3.imagesDir, { recursive: true });
507
+ fs.mkdirSync(linked3.mountsDir, { recursive: true });
508
+ fakeVhdx(linked3.image, { gb: 8 });
509
+ const mountJunction = spawnSync(SYSTEM.cmd(), ['/c', 'mklink', '/J', linked3.mount, linkTarget], {
510
+ encoding: 'utf8',
511
+ });
512
+ if (mountJunction.status === 0) {
513
+ refused('a mount folder that is already a link or mount point', dryRun('mount-contained', {
514
+ ...request(),
515
+ storageRoot: linked3.storageRoot,
516
+ image: linked3.image,
517
+ mount: linked3.mount,
518
+ }), 'mount point');
519
+ fs.rmdirSync(linked3.mount);
520
+ }
521
+ else
522
+ t.skip('mount folder as a junction (could not make one here)');
523
+ t.section('the status script only looks');
524
+ const look = spawnSync(SYSTEM.powershell(), [
525
+ '-NoProfile',
526
+ '-NonInteractive',
527
+ '-ExecutionPolicy',
528
+ 'Bypass',
529
+ '-File',
530
+ path.join(scriptDir, 'check-contained.ps1'),
531
+ '-Image',
532
+ dry.image,
533
+ '-Mount',
534
+ dry.mount,
535
+ ], { encoding: 'utf8' });
536
+ const answer = JSON.parse(look.stdout);
537
+ t.check(look.status === 0 &&
538
+ answer.imageExists === true &&
539
+ answer.mountExists === true &&
540
+ answer.mountIsMountPoint === false, 'it reports the image and a plain mount folder', look.stdout + look.stderr);
541
+ const real = await new ScriptDiskPort().reality(dry);
542
+ t.check(real.imageExists && real.mountExists && !real.mountIsMountPoint, 'and the disk port reads that answer');
543
+ },
544
+ };
@@ -4,7 +4,7 @@ import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { DEFAULTS } from '../core/config/index.js';
6
6
  import { ConfigSchema, HarnessSchema, redactSecrets, scrubSecrets, secretPaths } from '../core/schema/index.js';
7
- import { SchemaNode } from '../cli/menu/settings/index.js';
7
+ import { SchemaNode } from '../app/kernel/index.js';
8
8
  import { SandboxChannel } from '../host/channel/index.js';
9
9
  import { AccountStore, AgentAccountStore } from '../host/profile/index.js';
10
10
  import { clearSecrets, SecretStore, withoutSecrets } from '../host/secrets/index.js';
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './support/suite.js';
2
+ export declare const engineSuite: Suite;