windows-development-sandbox 1.0.5 → 1.4.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 (453) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -8
  3. package/config.example.json +55 -5
  4. package/dist/api.js +2 -1
  5. package/dist/cli/args.d.ts +23 -0
  6. package/dist/cli/args.js +38 -4
  7. package/dist/cli/colors.d.ts +24 -0
  8. package/dist/cli/colors.js +42 -0
  9. package/dist/cli/commands/accounts.js +2 -2
  10. package/dist/cli/commands/agent-commands.d.ts +6 -0
  11. package/dist/cli/commands/agent-commands.js +61 -0
  12. package/dist/cli/commands/context.d.ts +2 -0
  13. package/dist/cli/commands/context.js +5 -1
  14. package/dist/cli/commands/data/clean.d.ts +15 -0
  15. package/dist/cli/commands/data/clean.js +106 -0
  16. package/dist/cli/commands/data/collect.d.ts +11 -0
  17. package/dist/cli/commands/data/collect.js +54 -0
  18. package/dist/cli/commands/data/index.d.ts +4 -0
  19. package/dist/cli/commands/data/index.js +45 -0
  20. package/dist/cli/commands/data/remove.d.ts +2 -0
  21. package/dist/cli/commands/data/remove.js +30 -0
  22. package/dist/cli/commands/index.d.ts +6 -4
  23. package/dist/cli/commands/index.js +6 -4
  24. package/dist/cli/commands/launch.d.ts +26 -2
  25. package/dist/cli/commands/launch.js +256 -42
  26. package/dist/cli/commands/maintenance.d.ts +2 -0
  27. package/dist/cli/commands/maintenance.js +67 -5
  28. package/dist/cli/commands/project.d.ts +29 -0
  29. package/dist/cli/commands/project.js +63 -0
  30. package/dist/cli/commands/projects.d.ts +14 -0
  31. package/dist/cli/commands/projects.js +146 -0
  32. package/dist/cli/commands/repair/checks.d.ts +2 -0
  33. package/dist/cli/commands/{repair.js → repair/checks.js} +19 -181
  34. package/dist/cli/commands/{repair.d.ts → repair/finders.d.ts} +0 -29
  35. package/dist/cli/commands/repair/finders.js +108 -0
  36. package/dist/cli/commands/repair/index.d.ts +12 -0
  37. package/dist/cli/commands/repair/index.js +61 -0
  38. package/dist/cli/commands/repair/types.d.ts +26 -0
  39. package/dist/cli/commands/repair/types.js +3 -0
  40. package/dist/cli/commands/restart.d.ts +6 -0
  41. package/dist/cli/commands/restart.js +85 -0
  42. package/dist/cli/commands/sessions.d.ts +2 -1
  43. package/dist/cli/commands/sessions.js +189 -45
  44. package/dist/cli/commands/setup/checks.d.ts +13 -0
  45. package/dist/cli/commands/setup/checks.js +71 -0
  46. package/dist/cli/commands/{setup.d.ts → setup/facts.d.ts} +1 -15
  47. package/dist/cli/commands/setup/facts.js +60 -0
  48. package/dist/cli/commands/setup/index.d.ts +2 -0
  49. package/dist/cli/commands/setup/index.js +43 -0
  50. package/dist/cli/commands/user-config.d.ts +8 -0
  51. package/dist/cli/commands/user-config.js +41 -0
  52. package/dist/cli/history.d.ts +11 -0
  53. package/dist/cli/history.js +21 -0
  54. package/dist/cli/index.d.ts +7 -1
  55. package/dist/cli/index.js +42 -15
  56. package/dist/cli/manifest.d.ts +8 -0
  57. package/dist/cli/manifest.js +40 -15
  58. package/dist/cli/menu/apps.d.ts +21 -0
  59. package/dist/cli/menu/apps.js +122 -0
  60. package/dist/cli/menu/command-form.d.ts +28 -0
  61. package/dist/cli/menu/command-form.js +219 -0
  62. package/dist/cli/menu/index.d.ts +26 -0
  63. package/dist/cli/menu/index.js +180 -0
  64. package/dist/cli/menu/settings/categories.d.ts +9 -0
  65. package/dist/cli/menu/settings/categories.js +28 -0
  66. package/dist/cli/menu/settings/config-file.d.ts +40 -0
  67. package/dist/cli/menu/settings/config-file.js +71 -0
  68. package/dist/cli/menu/settings/editors.d.ts +30 -0
  69. package/dist/cli/menu/settings/editors.js +305 -0
  70. package/dist/cli/menu/settings/index.d.ts +32 -0
  71. package/dist/cli/menu/settings/index.js +136 -0
  72. package/dist/cli/menu/settings/schema-node.d.ts +62 -0
  73. package/dist/cli/menu/settings/schema-node.js +104 -0
  74. package/dist/cli/menu/several.d.ts +20 -0
  75. package/dist/cli/menu/several.js +102 -0
  76. package/dist/cli/progress.d.ts +9 -0
  77. package/dist/cli/progress.js +49 -0
  78. package/dist/cli/tui/app.d.ts +39 -0
  79. package/dist/cli/tui/app.js +52 -0
  80. package/dist/cli/tui/dialogs.d.ts +57 -0
  81. package/dist/cli/tui/dialogs.js +162 -0
  82. package/dist/cli/tui/frame.d.ts +18 -0
  83. package/dist/cli/tui/frame.js +27 -0
  84. package/dist/cli/tui/index.d.ts +7 -0
  85. package/dist/cli/tui/index.js +8 -0
  86. package/dist/cli/tui/list-screen.d.ts +101 -0
  87. package/dist/cli/tui/list-screen.js +299 -0
  88. package/dist/cli/tui/terminal.d.ts +37 -0
  89. package/dist/cli/tui/terminal.js +89 -0
  90. package/dist/cli/tui/text.d.ts +20 -0
  91. package/dist/cli/tui/text.js +105 -0
  92. package/dist/cli/tui/theme.d.ts +45 -0
  93. package/dist/cli/tui/theme.js +30 -0
  94. package/dist/cli/validate.d.ts +17 -0
  95. package/dist/cli/validate.js +89 -0
  96. package/dist/core/config.js +12 -6
  97. package/dist/core/disk.d.ts +16 -0
  98. package/dist/core/disk.js +55 -0
  99. package/dist/core/local-config.d.ts +23 -0
  100. package/dist/core/local-config.js +79 -7
  101. package/dist/core/project-policy.d.ts +15 -0
  102. package/dist/core/project-policy.js +35 -0
  103. package/dist/core/prompt.d.ts +4 -0
  104. package/dist/core/prompt.js +15 -0
  105. package/dist/core/safe-paths.d.ts +4 -0
  106. package/dist/core/safe-paths.js +37 -0
  107. package/dist/core/schema/apps.d.ts +40 -0
  108. package/dist/core/schema/apps.js +27 -0
  109. package/dist/core/schema/common.d.ts +17 -0
  110. package/dist/core/schema/common.js +15 -0
  111. package/dist/core/schema/config.d.ts +659 -0
  112. package/dist/core/schema/config.js +75 -0
  113. package/dist/core/schema/databases.d.ts +33 -0
  114. package/dist/core/schema/databases.js +35 -0
  115. package/dist/core/schema/harness.d.ts +12 -0
  116. package/dist/core/schema/harness.js +17 -0
  117. package/dist/core/schema/json-schema.d.ts +5 -0
  118. package/dist/core/schema/json-schema.js +25 -0
  119. package/dist/core/schema/profile.d.ts +32 -0
  120. package/dist/core/schema/profile.js +31 -0
  121. package/dist/core/schema/projects.d.ts +9 -0
  122. package/dist/core/schema/projects.js +11 -0
  123. package/dist/core/schema/sandbox.d.ts +78 -0
  124. package/dist/core/schema/sandbox.js +64 -0
  125. package/dist/core/schema/toolchains.d.ts +96 -0
  126. package/dist/core/schema/toolchains.js +88 -0
  127. package/dist/core/schema.d.ts +6 -604
  128. package/dist/core/schema.js +8 -264
  129. package/dist/core/types.d.ts +35 -1
  130. package/dist/core/util.d.ts +9 -0
  131. package/dist/core/util.js +14 -3
  132. package/dist/core/workspace.d.ts +0 -5
  133. package/dist/core/workspace.js +33 -50
  134. package/dist/guest/apps/index.d.ts +10 -0
  135. package/dist/guest/apps/index.js +206 -0
  136. package/dist/guest/apps/resolvers.d.ts +8 -0
  137. package/dist/guest/apps/resolvers.js +90 -0
  138. package/dist/guest/audit-parse.d.ts +7 -0
  139. package/dist/guest/audit-parse.js +24 -0
  140. package/dist/guest/audit.d.ts +4 -0
  141. package/dist/guest/audit.js +47 -0
  142. package/dist/guest/boot-env.d.ts +4 -0
  143. package/dist/guest/boot-env.js +25 -0
  144. package/dist/guest/cmdline.d.ts +12 -0
  145. package/dist/guest/cmdline.js +23 -0
  146. package/dist/guest/daemon/exec.d.ts +5 -0
  147. package/dist/guest/daemon/exec.js +32 -0
  148. package/dist/guest/daemon/project.d.ts +14 -0
  149. package/dist/guest/daemon/project.js +132 -0
  150. package/dist/guest/daemon/relays.d.ts +1 -0
  151. package/dist/guest/daemon/relays.js +18 -0
  152. package/dist/guest/daemon/session.d.ts +13 -0
  153. package/dist/guest/daemon/session.js +139 -0
  154. package/dist/guest/daemon/shutdown.d.ts +1 -0
  155. package/dist/guest/daemon/shutdown.js +21 -0
  156. package/dist/guest/daemon/state.d.ts +17 -0
  157. package/dist/guest/daemon/state.js +31 -0
  158. package/dist/guest/daemon.js +12 -141
  159. package/dist/guest/db.js +18 -3
  160. package/dist/guest/env.d.ts +3 -0
  161. package/dist/guest/env.js +7 -3
  162. package/dist/guest/guide.d.ts +6 -2
  163. package/dist/guest/guide.js +5 -4
  164. package/dist/guest/helpers.d.ts +3 -0
  165. package/dist/guest/helpers.js +26 -0
  166. package/dist/guest/main.js +31 -8
  167. package/dist/guest/netproxy.d.ts +1 -0
  168. package/dist/guest/netproxy.js +100 -0
  169. package/dist/guest/ports.d.ts +6 -0
  170. package/dist/guest/ports.js +22 -0
  171. package/dist/guest/project-context.d.ts +7 -0
  172. package/dist/guest/project-context.js +10 -0
  173. package/dist/guest/project-env.d.ts +20 -0
  174. package/dist/guest/project-env.js +20 -0
  175. package/dist/guest/project-status.d.ts +5 -0
  176. package/dist/guest/project-status.js +17 -0
  177. package/dist/guest/ptyhost.d.ts +0 -18
  178. package/dist/guest/ptyhost.js +17 -65
  179. package/dist/guest/setup/agents.d.ts +5 -2
  180. package/dist/guest/setup/agents.js +22 -8
  181. package/dist/guest/setup/claude.d.ts +0 -4
  182. package/dist/guest/setup/claude.js +59 -5
  183. package/dist/guest/setup/environment.d.ts +13 -3
  184. package/dist/guest/setup/environment.js +14 -10
  185. package/dist/guest/setup/hooks.d.ts +9 -1
  186. package/dist/guest/setup/hooks.js +28 -14
  187. package/dist/guest/setup/network.d.ts +9 -0
  188. package/dist/guest/setup/network.js +97 -4
  189. package/dist/guest/setup/user.js +4 -0
  190. package/dist/guest/toolchains/catalogue.d.ts +4 -0
  191. package/dist/guest/toolchains/catalogue.js +2 -2
  192. package/dist/guest/toolchains/cli.js +25 -17
  193. package/dist/guest/toolchains/common.d.ts +18 -1
  194. package/dist/guest/toolchains/common.js +34 -11
  195. package/dist/guest/toolchains/databases/dynamodb.d.ts +8 -0
  196. package/dist/guest/toolchains/databases/dynamodb.js +52 -0
  197. package/dist/guest/toolchains/{databases.d.ts → databases/index.d.ts} +2 -3
  198. package/dist/guest/toolchains/databases/index.js +62 -0
  199. package/dist/guest/toolchains/databases/mongodb.d.ts +3 -0
  200. package/dist/guest/toolchains/databases/mongodb.js +31 -0
  201. package/dist/guest/toolchains/databases/mysql.d.ts +3 -0
  202. package/dist/guest/toolchains/databases/mysql.js +38 -0
  203. package/dist/guest/toolchains/databases/postgres.d.ts +5 -0
  204. package/dist/guest/toolchains/databases/postgres.js +38 -0
  205. package/dist/guest/toolchains/databases/redis.d.ts +3 -0
  206. package/dist/guest/toolchains/databases/redis.js +16 -0
  207. package/dist/guest/toolchains/databases/server.d.ts +14 -0
  208. package/dist/guest/toolchains/databases/server.js +52 -0
  209. package/dist/guest/toolchains/git.js +25 -16
  210. package/dist/guest/toolchains/go.js +1 -1
  211. package/dist/guest/toolchains/index.d.ts +13 -9
  212. package/dist/guest/toolchains/index.js +40 -82
  213. package/dist/guest/toolchains/java.js +5 -5
  214. package/dist/guest/toolchains/leftovers.d.ts +2 -0
  215. package/dist/guest/toolchains/leftovers.js +26 -0
  216. package/dist/guest/toolchains/managers.js +12 -10
  217. package/dist/guest/toolchains/{binstall.js → native/binstall.js} +6 -9
  218. package/dist/guest/toolchains/native/cpp.d.ts +4 -0
  219. package/dist/guest/toolchains/native/cpp.js +38 -0
  220. package/dist/guest/toolchains/native/dotnet.d.ts +4 -0
  221. package/dist/guest/toolchains/native/dotnet.js +103 -0
  222. package/dist/guest/toolchains/native/mesa.d.ts +7 -0
  223. package/dist/guest/toolchains/native/mesa.js +56 -0
  224. package/dist/guest/toolchains/native/msvc/environment.d.ts +16 -0
  225. package/dist/guest/toolchains/native/msvc/environment.js +57 -0
  226. package/dist/guest/toolchains/native/msvc/index.d.ts +6 -0
  227. package/dist/guest/toolchains/native/msvc/index.js +48 -0
  228. package/dist/guest/toolchains/native/msvc/packages.d.ts +6 -0
  229. package/dist/guest/toolchains/native/msvc/packages.js +156 -0
  230. package/dist/guest/toolchains/native/msvc/tree.d.ts +15 -0
  231. package/dist/guest/toolchains/native/msvc/tree.js +21 -0
  232. package/dist/guest/toolchains/native/rust.d.ts +3 -0
  233. package/dist/guest/toolchains/native/rust.js +45 -0
  234. package/dist/guest/toolchains/node.js +26 -6
  235. package/dist/guest/toolchains/plan.d.ts +24 -0
  236. package/dist/guest/toolchains/plan.js +140 -0
  237. package/dist/guest/toolchains/pwsh.js +1 -1
  238. package/dist/guest/toolchains/python.js +43 -12
  239. package/dist/guest/toolchains/runtimes.d.ts +2 -0
  240. package/dist/guest/toolchains/runtimes.js +6 -8
  241. package/dist/guest/toolchains/scripting/php.d.ts +3 -0
  242. package/dist/guest/toolchains/scripting/php.js +71 -0
  243. package/dist/guest/toolchains/scripting/ruby.d.ts +3 -0
  244. package/dist/guest/toolchains/scripting/ruby.js +47 -0
  245. package/dist/guest/toolchains/scripting/vcruntime.d.ts +7 -0
  246. package/dist/guest/toolchains/scripting/vcruntime.js +45 -0
  247. package/dist/guest/toolchains/vscode-profile.d.ts +30 -0
  248. package/dist/guest/toolchains/vscode-profile.js +69 -0
  249. package/dist/guest/toolchains/vscode.d.ts +17 -0
  250. package/dist/guest/toolchains/vscode.js +87 -1
  251. package/dist/guest/view.js +21 -1
  252. package/dist/host/accounts.js +14 -5
  253. package/dist/host/agent-commands.d.ts +25 -0
  254. package/dist/host/agent-commands.js +145 -0
  255. package/dist/host/attach.d.ts +17 -0
  256. package/dist/host/attach.js +70 -0
  257. package/dist/host/audit/collect.d.ts +34 -0
  258. package/dist/host/audit/collect.js +126 -0
  259. package/dist/host/audit/log.d.ts +46 -0
  260. package/dist/host/audit/log.js +143 -0
  261. package/dist/host/channel.d.ts +23 -22
  262. package/dist/host/channel.js +66 -84
  263. package/dist/host/claude-profile.js +7 -3
  264. package/dist/host/cleanup.js +2 -11
  265. package/dist/host/database-plan.d.ts +12 -0
  266. package/dist/host/database-plan.js +52 -0
  267. package/dist/host/generate.d.ts +10 -12
  268. package/dist/host/generate.js +58 -135
  269. package/dist/host/join.d.ts +28 -0
  270. package/dist/host/join.js +76 -0
  271. package/dist/host/launch/mappings.d.ts +3 -0
  272. package/dist/host/launch/mappings.js +54 -0
  273. package/dist/host/launch/projects.d.ts +15 -0
  274. package/dist/host/launch/projects.js +56 -0
  275. package/dist/host/launch/stage.d.ts +11 -0
  276. package/dist/host/launch/stage.js +66 -0
  277. package/dist/host/launch/wsb.d.ts +10 -0
  278. package/dist/host/launch/wsb.js +34 -0
  279. package/dist/host/motw.d.ts +17 -0
  280. package/dist/host/motw.js +74 -0
  281. package/dist/host/pc-scan.d.ts +6 -0
  282. package/dist/host/pc-scan.js +37 -0
  283. package/dist/host/platform.d.ts +2 -0
  284. package/dist/host/platform.js +16 -1
  285. package/dist/host/port-forward.d.ts +8 -0
  286. package/dist/host/port-forward.js +37 -0
  287. package/dist/host/project-scan.d.ts +1 -17
  288. package/dist/host/project-scan.js +2 -86
  289. package/dist/host/projects.d.ts +100 -0
  290. package/dist/host/projects.js +147 -0
  291. package/dist/host/repo-guard.d.ts +25 -5
  292. package/dist/host/repo-guard.js +242 -23
  293. package/dist/host/stops.d.ts +67 -0
  294. package/dist/host/stops.js +126 -0
  295. package/dist/host/terminal.d.ts +8 -0
  296. package/dist/host/terminal.js +48 -1
  297. package/dist/host/test-guard.d.ts +6 -0
  298. package/dist/host/test-guard.js +15 -0
  299. package/dist/host/window.js +7 -3
  300. package/dist/host/workspace-view/index.d.ts +20 -0
  301. package/dist/host/workspace-view/index.js +53 -0
  302. package/dist/host/workspace-view/rules.d.ts +26 -0
  303. package/dist/host/workspace-view/rules.js +93 -0
  304. package/dist/host/workspace-view/view.d.ts +50 -0
  305. package/dist/host/workspace-view/view.js +281 -0
  306. package/dist/host/wsb.d.ts +37 -0
  307. package/dist/host/wsb.js +101 -0
  308. package/dist/runtime/daemon.js +11791 -2821
  309. package/dist/runtime/db.js +6585 -466
  310. package/dist/runtime/main.js +10238 -1724
  311. package/dist/runtime/netproxy.js +25936 -0
  312. package/dist/runtime/view.js +6183 -59
  313. package/dist/selftest/accounts.test.d.ts +2 -0
  314. package/dist/selftest/accounts.test.js +44 -0
  315. package/dist/selftest/agent-commands.test.d.ts +2 -0
  316. package/dist/selftest/agent-commands.test.js +47 -0
  317. package/dist/selftest/audit.test.d.ts +2 -0
  318. package/dist/selftest/audit.test.js +57 -0
  319. package/dist/selftest/channel.test.d.ts +2 -0
  320. package/dist/selftest/channel.test.js +44 -0
  321. package/dist/selftest/cli.test.d.ts +2 -2
  322. package/dist/selftest/cli.test.js +145 -27
  323. package/dist/selftest/cmdline.test.d.ts +2 -0
  324. package/dist/selftest/cmdline.test.js +45 -0
  325. package/dist/selftest/config.test.d.ts +2 -3
  326. package/dist/selftest/config.test.js +36 -72
  327. package/dist/selftest/data.test.d.ts +2 -2
  328. package/dist/selftest/data.test.js +77 -70
  329. package/dist/selftest/detect.test.d.ts +2 -2
  330. package/dist/selftest/detect.test.js +54 -38
  331. package/dist/selftest/fixtures.d.ts +89 -4
  332. package/dist/selftest/fixtures.js +43 -24
  333. package/dist/selftest/generate.test.d.ts +2 -0
  334. package/dist/selftest/generate.test.js +68 -0
  335. package/dist/selftest/git-access.test.d.ts +2 -0
  336. package/dist/selftest/git-access.test.js +26 -0
  337. package/dist/selftest/guest-boot.test.d.ts +2 -0
  338. package/dist/selftest/guest-boot.test.js +76 -0
  339. package/dist/selftest/harness.d.ts +44 -7
  340. package/dist/selftest/harness.js +62 -21
  341. package/dist/selftest/host-sim.test.d.ts +2 -0
  342. package/dist/selftest/host-sim.test.js +93 -0
  343. package/dist/selftest/hostile-input.test.d.ts +2 -0
  344. package/dist/selftest/hostile-input.test.js +54 -0
  345. package/dist/selftest/index.d.ts +8 -1
  346. package/dist/selftest/index.js +137 -35
  347. package/dist/selftest/links.test.d.ts +2 -0
  348. package/dist/selftest/links.test.js +50 -0
  349. package/dist/selftest/live-daemon.test.d.ts +2 -0
  350. package/dist/selftest/live-daemon.test.js +91 -0
  351. package/dist/selftest/live-git.test.d.ts +2 -0
  352. package/dist/selftest/live-git.test.js +60 -0
  353. package/dist/selftest/live-network.test.d.ts +2 -0
  354. package/dist/selftest/live-network.test.js +63 -0
  355. package/dist/selftest/live-projects.test.d.ts +2 -0
  356. package/dist/selftest/live-projects.test.js +127 -0
  357. package/dist/selftest/live.d.ts +65 -0
  358. package/dist/selftest/live.js +109 -0
  359. package/dist/selftest/local-config.test.d.ts +2 -0
  360. package/dist/selftest/local-config.test.js +120 -0
  361. package/dist/selftest/mirror.test.d.ts +2 -3
  362. package/dist/selftest/mirror.test.js +92 -65
  363. package/dist/selftest/network.test.d.ts +2 -0
  364. package/dist/selftest/network.test.js +40 -0
  365. package/dist/selftest/personalization.test.d.ts +2 -0
  366. package/dist/selftest/personalization.test.js +53 -0
  367. package/dist/selftest/projects.test.d.ts +2 -0
  368. package/dist/selftest/projects.test.js +254 -0
  369. package/dist/selftest/repair.test.d.ts +2 -0
  370. package/dist/selftest/repair.test.js +42 -0
  371. package/dist/selftest/repo-guard.test.d.ts +2 -0
  372. package/dist/selftest/repo-guard.test.js +121 -0
  373. package/dist/selftest/secrets.test.d.ts +2 -0
  374. package/dist/selftest/secrets.test.js +71 -0
  375. package/dist/selftest/setup.test.d.ts +2 -2
  376. package/dist/selftest/setup.test.js +45 -22
  377. package/dist/selftest/suite.d.ts +13 -0
  378. package/dist/selftest/suite.js +1 -0
  379. package/dist/selftest/sync.test.d.ts +2 -3
  380. package/dist/selftest/sync.test.js +130 -78
  381. package/dist/selftest/test-guard.test.d.ts +2 -0
  382. package/dist/selftest/test-guard.test.js +49 -0
  383. package/dist/selftest/toolchains.test.d.ts +2 -0
  384. package/dist/selftest/toolchains.test.js +133 -0
  385. package/dist/selftest/workspace-view.test.d.ts +2 -0
  386. package/dist/selftest/workspace-view.test.js +89 -0
  387. package/dist/selftest/workspace.test.d.ts +2 -0
  388. package/dist/selftest/workspace.test.js +53 -0
  389. package/dist/shared/apps.d.ts +49 -0
  390. package/dist/shared/apps.js +114 -0
  391. package/dist/shared/download.d.ts +3 -1
  392. package/dist/shared/download.js +19 -4
  393. package/dist/shared/netpolicy.d.ts +27 -0
  394. package/dist/shared/netpolicy.js +51 -0
  395. package/dist/shared/paths.d.ts +5 -0
  396. package/dist/shared/paths.js +6 -0
  397. package/dist/shared/projects.d.ts +53 -0
  398. package/dist/shared/projects.js +67 -0
  399. package/dist/shared/pty-client.d.ts +9 -4
  400. package/dist/shared/pty-client.js +139 -17
  401. package/dist/shared/rundir.d.ts +12 -1
  402. package/dist/shared/rundir.js +14 -4
  403. package/dist/shared/safe-remove.js +11 -0
  404. package/dist/shared/schema.d.ts +453 -0
  405. package/dist/shared/schema.js +43 -2
  406. package/dist/shared/system.d.ts +1 -0
  407. package/dist/shared/system.js +1 -0
  408. package/dist/shared/test-guard.d.ts +4 -0
  409. package/dist/shared/test-guard.js +37 -0
  410. package/dist/sync/claude-mem.js +6 -3
  411. package/dist/sync/history.d.ts +29 -12
  412. package/dist/sync/history.js +72 -25
  413. package/dist/sync/watcher.d.ts +8 -0
  414. package/dist/sync/watcher.js +99 -8
  415. package/package.json +7 -6
  416. package/sandbox/bootstrap.ps1 +258 -258
  417. package/sandbox/cargoshim.cs +56 -0
  418. package/sandbox/cpucap.cs +33 -0
  419. package/sandbox/cpucap.ps1 +20 -0
  420. package/sandbox/logon.cmd +24 -16
  421. package/sandbox/memlog.ps1 +34 -0
  422. package/sandbox/openssl.cs +42 -0
  423. package/sandbox/pagefile.cs +31 -0
  424. package/sandbox/pagefile.ps1 +21 -0
  425. package/sandbox/ptyhost.cs +120 -11
  426. package/sandbox/trust.cmd +35 -35
  427. package/schemas/config.schema.json +403 -3
  428. package/schemas/sandbox-config.schema.json +143 -13
  429. package/dist/cli/commands/data.d.ts +0 -46
  430. package/dist/cli/commands/data.js +0 -249
  431. package/dist/cli/commands/setup.js +0 -152
  432. package/dist/cli/menu.d.ts +0 -22
  433. package/dist/cli/menu.js +0 -187
  434. package/dist/cli/settings.d.ts +0 -11
  435. package/dist/cli/settings.js +0 -156
  436. package/dist/guest/toolchains/databases.js +0 -226
  437. package/dist/guest/toolchains/msvc.d.ts +0 -10
  438. package/dist/guest/toolchains/msvc.js +0 -216
  439. package/dist/guest/toolchains/native.d.ts +0 -12
  440. package/dist/guest/toolchains/native.js +0 -220
  441. package/dist/guest/toolchains/scripting.d.ts +0 -10
  442. package/dist/guest/toolchains/scripting.js +0 -137
  443. package/dist/selftest/guest.test.d.ts +0 -3
  444. package/dist/selftest/guest.test.js +0 -61
  445. package/dist/selftest/helpers.test.d.ts +0 -2
  446. package/dist/selftest/helpers.test.js +0 -125
  447. package/dist/selftest/host.test.d.ts +0 -2
  448. package/dist/selftest/host.test.js +0 -71
  449. package/dist/selftest/live.test.d.ts +0 -7
  450. package/dist/selftest/live.test.js +0 -127
  451. package/dist/selftest/security.test.d.ts +0 -3
  452. package/dist/selftest/security.test.js +0 -143
  453. /package/dist/guest/toolchains/{binstall.d.ts → native/binstall.d.ts} +0 -0
@@ -0,0 +1,63 @@
1
+ // Live: the sandbox's allowOnly proxy (the real bundled runtime\netproxy.js) and the PC-side port forwarder, on
2
+ // loopback only; and that the cargo shim compiles with the compiler that ships with Windows.
3
+ import net from 'node:net';
4
+ import path from 'node:path';
5
+ import { spawn, spawnSync } from 'node:child_process';
6
+ import { startPortForwards } from '../host/port-forward.js';
7
+ import { PROXY_PORT, relayPort } from '../shared/netpolicy.js';
8
+ import { CSC } from './live.js';
9
+ /** A TCP server that answers every line with "echo:<line>". */
10
+ const echoServer = (port = 0) => new Promise((resolve) => {
11
+ const server = net.createServer((s) => s.on('data', (d) => s.write('echo:' + d.toString())));
12
+ server.listen(port, '127.0.0.1', () => resolve(server));
13
+ });
14
+ const portOf = (server) => server.address().port;
15
+ /** Sends `text` after `preamble` (a CONNECT) and resolves with everything received within a short while. */
16
+ const talk = (port, preamble, text) => new Promise((resolve) => {
17
+ const socket = net.connect(port, '127.0.0.1');
18
+ let got = '';
19
+ socket.on('data', (d) => { got += d.toString(); if (preamble && got.includes('\r\n\r\n') && got.startsWith('HTTP/1.1 200') && !got.includes('echo:'))
20
+ socket.write(text); });
21
+ socket.on('error', () => resolve(got));
22
+ socket.on('connect', () => socket.write(preamble || text));
23
+ setTimeout(() => { socket.destroy(); resolve(got); }, 1500);
24
+ });
25
+ export const liveNetworkSuite = {
26
+ name: 'live-network',
27
+ title: 'live: allowOnly proxy, port forwarding, cargo shim',
28
+ live: true,
29
+ run: async (t, f) => {
30
+ t.section('allowOnly proxy');
31
+ const gen = await f.launch({}, { config: { ...f.baseConfig, network: { ...f.baseConfig.network, allowOnly: ['localhost'] } } });
32
+ const echo = await echoServer();
33
+ const proxy = spawn(process.execPath, [path.join(gen.launchPath, 'runtime', 'netproxy.js')], { env: { ...process.env, AGENT_LAUNCH_DIR: gen.launchPath, AGENT_DATA_DIR: t.dir('netproxy-data') }, stdio: 'ignore' });
34
+ try {
35
+ const up = await t.waitFor(() => new Promise((resolve) => { const s = net.connect(PROXY_PORT, '127.0.0.1', () => { s.destroy(); resolve(true); }); s.on('error', () => resolve(false)); }), 10000, 200);
36
+ t.check(Boolean(up), 'the proxy listens on localhost');
37
+ const allowed = await talk(PROXY_PORT, 'CONNECT localhost:' + portOf(echo) + ' HTTP/1.1\r\nHost: localhost\r\n\r\n', 'hi');
38
+ t.check(allowed.startsWith('HTTP/1.1 200') && allowed.includes('echo:hi'), 'an allowed host is tunnelled through', allowed);
39
+ const blocked = await talk(PROXY_PORT, 'CONNECT example.com:443 HTTP/1.1\r\nHost: example.com\r\n\r\n', '');
40
+ t.check(blocked.startsWith('HTTP/1.1 403'), 'any other host is refused (403)', blocked);
41
+ }
42
+ finally {
43
+ proxy.kill();
44
+ echo.close();
45
+ }
46
+ t.section('port forwarding');
47
+ const relay = await echoServer(relayPort(0));
48
+ const free = await echoServer();
49
+ const pcPort = portOf(free);
50
+ free.close();
51
+ await new Promise((r) => setTimeout(r, 200));
52
+ const stop = startPortForwards([{ pc: pcPort, sandbox: 3000 }], () => '127.0.0.1');
53
+ await new Promise((r) => setTimeout(r, 300));
54
+ const forwarded = await talk(pcPort, '', 'ping');
55
+ stop();
56
+ relay.close();
57
+ t.equal(forwarded, 'echo:ping', 'localhost:<pc> on this PC reaches the sandbox\'s relay');
58
+ t.section('cargo shim');
59
+ const out = path.join(t.dir('cargoshim'), 'cargo.exe');
60
+ const compiled = spawnSync(CSC, ['/nologo', '/optimize', '/out:' + out, path.join(gen.launchPath, 'sandbox', 'cargoshim.cs')], { encoding: 'utf8' });
61
+ t.check(compiled.status === 0, 'cargoshim.cs compiles with the .NET Framework compiler (C# 5)', compiled.stdout + compiled.stderr);
62
+ },
63
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const liveProjectsSuite: Suite;
@@ -0,0 +1,127 @@
1
+ // Live: the real session daemon + ptyhost with two projects at once. The launch folder (A) and a project in a root
2
+ // (B) each get two sessions requested at the same moment; each runs in its own folder with its own environment; B is
3
+ // set up once for both; viewers attach, detach and reattach, two at a time; exec and restart follow the project.
4
+ // Toolchain installers run as a dry run (no downloads); everything else is real. cmd.exe stands in for the agent.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { projectSpec } from '../host/join.js';
8
+ import { resizeFrame } from '../shared/pty-client.js';
9
+ import { readJsonFile } from '../shared/rundir.js';
10
+ import { copyCmdAsFakeAgent, openViewer, sees, startDaemon, tail, viewersWhen } from './live.js';
11
+ export const liveProjectsSuite = {
12
+ name: 'live-projects',
13
+ title: 'live: two projects, several sessions, several viewers',
14
+ live: true,
15
+ timeoutMs: 480000,
16
+ run: async (t, f) => {
17
+ const gen = await f.launch();
18
+ const data = t.at('live-multi-data');
19
+ const root = t.dir('live-root');
20
+ const projB = t.dir('live-root', 'projB');
21
+ copyCmdAsFakeAgent(data);
22
+ const session = f.readJson(gen.sessionPath);
23
+ // B's root appears at its own path here (like guestWorkspace "mirror"), so host and guest paths are the same.
24
+ Object.assign(session, { workspace: f.workA, attach: { enabled: true }, projectsDir: t.at('live-projects'), roots: [root], projectId: 'work-a' });
25
+ Object.assign(session.harness, { arguments: ['/d', '/k', 'echo AGENT-READY'], resumeArguments: ['/d', '/k', 'echo AGENT-RESUMED'] });
26
+ t.writeJson(gen.sessionPath, session);
27
+ const local = t.at('live-multi-local');
28
+ const daemon = startDaemon(t, f, { launchPath: gen.launchPath, data, workspace: f.workA, env: { AGENT_LOCAL_DIR: local, AGENT_DRY_RUN: '1', AGENT_MANAGED_DIR: t.at('live-managed') } });
29
+ const viewers = [];
30
+ const view = async (attach, cols = 100) => {
31
+ const v = await openViewer(attach, { cols });
32
+ viewers.push(v);
33
+ return v;
34
+ };
35
+ try {
36
+ t.section('live: four sessions at once');
37
+ const cfg = structuredClone(f.baseConfig);
38
+ cfg.toolchains.environment = { LIVE_B: 'b-env' };
39
+ const spec = projectSpec(await f.run({ cwd: projB, trustConfig: true }, cfg), { name: 'projB', host: projB, guest: projB, primary: false, mechanism: 'root', registeredAt: '' }, false);
40
+ // All four at the same moment: two in the launch folder, two in B (whose setup they share).
41
+ daemon.request({ kind: 'session', id: 'a1', harness: 'claude', args: [], cwd: f.workA });
42
+ daemon.request({ kind: 'session', id: 'b1', harness: 'claude', args: [], cwd: projB, project: spec });
43
+ daemon.request({ kind: 'session', id: 'b2', harness: 'claude', args: [], cwd: projB, project: spec });
44
+ daemon.request({ kind: 'session', id: 'a2', harness: 'claude', args: [], cwd: f.workA });
45
+ const [a1, a2, b1, b2] = await Promise.all(['a1', 'a2', 'b1', 'b2'].map((id) => daemon.started(id)));
46
+ const all = [a1, a2, b1, b2];
47
+ const notRunning = all.filter((s) => s?.phase !== 'running' || !s.attach);
48
+ t.check(notRunning.length === 0, 'four sessions requested at once all start (two per project)', notRunning.map((s) => s?.id + ': ' + s?.phase + ' ' + s?.message).join('\n') + '\n' + daemon.diagnostics());
49
+ if (notRunning.length > 0)
50
+ return;
51
+ t.equal([a1.project, b1.project, b2.project], ['work-a', 'projB', 'projB'], 'each session knows its project');
52
+ const log = fs.readdirSync(path.join(data, 'logs')).filter((n) => n.startsWith('daemon')).map((n) => fs.readFileSync(path.join(data, 'logs', n), 'utf8')).join('\n');
53
+ t.equal((log.match(/Project projB: setting up/g) ?? []).length, 1, 'two sessions for the same new project at once share one setup (no second provisioning run)');
54
+ t.equal(readJsonFile(path.join(daemon.run, 'projects', 'projB.json'))?.phase, 'ready', 'the project\'s setup status is written for the host (sandbox status)');
55
+ t.section('live: each session has its own folder and environment');
56
+ const views = await Promise.all(all.map((s) => view(s.attach)));
57
+ for (const v of views)
58
+ await sees(t, v, 'AGENT-READY');
59
+ for (const v of views)
60
+ v.type('echo P=%AGENT_PROJECT% W=%AGENT_WORKSPACE% B=[%LIVE_B%]');
61
+ const bSees = await sees(t, views[2], 'P=projB W=' + projB + ' B=[b-env]');
62
+ t.check(Boolean(bSees), 'B\'s first session runs with B\'s workspace, project and variables', tail(views[2].output()));
63
+ t.check(Boolean(await sees(t, views[3], 'B=[b-env]')), 'B\'s second session runs with B\'s variables', tail(views[3].output()));
64
+ // (The daemon here runs without the boot's environment, so A's own AGENT_* variables are not set at all.)
65
+ t.check(Boolean(await sees(t, views[0], 'B=[%LIVE_B%]')), 'A\'s first session is untouched by B (B\'s variables never reach it)', tail(views[0].output()));
66
+ t.check(Boolean(await sees(t, views[1], 'B=[%LIVE_B%]')), 'A\'s second session is untouched by B', tail(views[1].output()));
67
+ views[2].type('echo CWD=%CD%.');
68
+ t.check(Boolean(await sees(t, views[2], 'CWD=' + projB + '.')), 'B\'s session starts in B\'s folder', tail(views[2].output()));
69
+ t.section('live: several viewers on one session');
70
+ const second = await view(b1.attach, 60);
71
+ const two = await viewersWhen(t, b1.attach, (v) => v.viewers === 2);
72
+ t.check(two !== null, 'a second viewer attaches to the same session');
73
+ second.socket.write(resizeFrame(90, 25));
74
+ views[2].type('echo TWO-VIEWERS-OK');
75
+ t.check(Boolean(await sees(t, views[2], 'TWO-VIEWERS-OK')), 'the first viewer sees what it typed (after the second one resized)', tail(views[2].output()));
76
+ t.check(Boolean(await sees(t, second, 'TWO-VIEWERS-OK')), 'the second viewer sees it too', tail(second.output()));
77
+ second.type('echo FROM-SECOND');
78
+ t.check(Boolean(await sees(t, views[2], 'FROM-SECOND')), 'input from the second viewer reaches the agent and the first viewer', tail(views[2].output()));
79
+ views[2].close();
80
+ second.close();
81
+ const none = await viewersWhen(t, b1.attach, (v) => v.viewers === 0);
82
+ t.check(none !== null, 'closing every viewer is noticed');
83
+ t.equal(daemon.status('b1')?.phase, 'running', 'closing every viewer never ends the session');
84
+ for (let round = 1; round <= 3; round++) {
85
+ const again = await view(b1.attach);
86
+ again.type('echo REATTACHED-' + round);
87
+ t.check(Boolean(await sees(t, again, 'REATTACHED-' + round)), 'reattach ' + round + ': a new viewer works on the same session', tail(again.output()));
88
+ again.close();
89
+ }
90
+ t.section('live: exec, restart and refusals follow the projects');
91
+ daemon.request({ kind: 'exec', id: 'xb', command: ['echo', '%LIVE_B%'], cwd: projB, timeoutSeconds: 30 });
92
+ daemon.request({ kind: 'exec', id: 'xa', command: ['echo', '[%LIVE_B%]'], cwd: f.workA, timeoutSeconds: 30 });
93
+ const [xb, xa] = await Promise.all([daemon.response('xb'), daemon.response('xa')]);
94
+ t.check(Boolean(xb?.stdout?.includes('b-env')), 'exec in B\'s folder gets B\'s environment', JSON.stringify(xb));
95
+ t.check(Boolean(xa?.stdout?.includes('[%LIVE_B%]')), 'exec in A\'s folder gets A\'s', JSON.stringify(xa));
96
+ daemon.request({ kind: 'session', id: 'bad1', harness: 'claude', args: [], cwd: f.workB, project: { ...spec, name: 'outside', workspace: f.workB } });
97
+ const bad = await daemon.started('bad1');
98
+ t.check(bad?.phase === 'failed' && bad.message.includes('not visible inside the sandbox'), 'a project outside the launch folder, the roots and C:\\Projects is refused', JSON.stringify(bad));
99
+ daemon.request({ kind: 'session', id: 'bad2', harness: 'claude', args: [], cwd: f.workA, project: spec });
100
+ const claimed = await daemon.started('bad2');
101
+ t.includes(claimed?.message ?? '', 'not inside project projB', 'a session cannot claim a project its folder is not in');
102
+ daemon.request({ kind: 'restart', id: 'rb', harness: null, cwd: projB });
103
+ t.equal((await daemon.response('rb'))?.message, 'restarted 2 session(s)', 'restart scoped to a project restarts only its sessions');
104
+ t.check(fs.existsSync(path.join(local, 'projects', 'projB', 'shell.cmd')), 'sbx-shell projB has a shell script');
105
+ t.includes(fs.readFileSync(path.join(local, 'projects', 'projB', 'env.cmd'), 'utf8'), 'LIVE_B=b-env', 'sbx-shell projB opens with B\'s environment');
106
+ t.check(fs.existsSync(path.join(local, 'bin', 'sbx-shell.cmd')), 'sbx-shell is on the sandbox\'s PATH');
107
+ }
108
+ finally {
109
+ for (const v of viewers)
110
+ v.close();
111
+ // End every agent: a fresh viewer types exit into each still-running session.
112
+ for (const id of ['a1', 'a2', 'b1', 'b2']) {
113
+ const s = daemon.status(id);
114
+ if (s?.phase !== 'running' || !s.attach)
115
+ continue;
116
+ try {
117
+ const v = await openViewer(s.attach);
118
+ v.type('exit');
119
+ await t.waitFor(() => v.exitCode() !== null, 10000);
120
+ v.close();
121
+ }
122
+ catch { /* already gone */ }
123
+ }
124
+ await daemon.stop();
125
+ }
126
+ },
127
+ };
@@ -0,0 +1,65 @@
1
+ import { type ChildProcess } from 'node:child_process';
2
+ import type net from 'node:net';
3
+ import type { GuestResponse, SessionStatus } from '../core/types.js';
4
+ import { type Endpoint, type Viewers } from '../shared/pty-client.js';
5
+ import type { Fixtures } from './fixtures.js';
6
+ import type { Harness } from './harness.js';
7
+ export declare const CSC: string;
8
+ /** Live suites need Windows (ConPTY) and the .NET Framework compiler that ships with it. */
9
+ export declare const liveTestsAvailable: () => boolean;
10
+ /** Deadlines, in ms. Generous on purpose: they only matter when something is really wrong. */
11
+ export declare const WAIT: {
12
+ readonly start: 120000;
13
+ readonly screen: 45000;
14
+ readonly response: 60000;
15
+ readonly viewers: 45000;
16
+ readonly exit: 45000;
17
+ };
18
+ export declare const tail: (text: string, chars?: number) => string;
19
+ /** Connects, retrying until `ms`: a connection attempt can time out while the machine is busy. */
20
+ export declare const connectPatiently: (endpoint: Endpoint, ms?: number) => Promise<net.Socket>;
21
+ /** One connection to a session's ptyhost, collecting what the server sends. */
22
+ export interface Viewer {
23
+ socket: net.Socket;
24
+ /** Everything the agent has printed since this viewer connected. */
25
+ output: () => string;
26
+ /** The exit code frame, once the agent ended. */
27
+ exitCode: () => number | null;
28
+ /** The error frame ("access denied"), if the server sent one. */
29
+ error: () => string;
30
+ /** True once the server closed the connection. */
31
+ closed: () => boolean;
32
+ type: (text: string) => void;
33
+ close: () => void;
34
+ }
35
+ /** Connects like a terminal tab would (`token` can be wrong on purpose) and says hello. */
36
+ export declare const openViewer: (attach: Endpoint, options?: {
37
+ token?: string;
38
+ cols?: number;
39
+ rows?: number;
40
+ }) => Promise<Viewer>;
41
+ /** Polls until the viewer's screen shows `needle`. */
42
+ export declare const sees: (t: Harness, viewer: Viewer, needle: string, ms?: number) => Promise<true | null>;
43
+ /** Polls the session's viewer count until `accept` is satisfied; null when it never was. A query that gets no answer is retried. */
44
+ export declare const viewersWhen: (t: Harness, attach: Endpoint, accept: (v: Viewers) => boolean, ms?: number) => Promise<Viewers | null>;
45
+ export declare const copyCmdAsFakeAgent: (dataPath: string) => void;
46
+ /** A real session daemon (guest runtime) working on a generated launch folder, driven through the run folder like the host does. */
47
+ export interface LiveDaemon {
48
+ run: string;
49
+ child: ChildProcess;
50
+ request: (body: Record<string, unknown>) => void;
51
+ response: (id: string, ms?: number) => Promise<GuestResponse | null>;
52
+ status: (id: string) => SessionStatus | null;
53
+ /** Waits until a session has left "starting". */
54
+ started: (id: string, ms?: number) => Promise<SessionStatus | null>;
55
+ /** The daemon's stdout/stderr and its log files, for failure messages. */
56
+ diagnostics: () => string;
57
+ /** Ends the daemon. */
58
+ stop: () => Promise<void>;
59
+ }
60
+ export declare const startDaemon: (t: Harness, f: Fixtures, options: {
61
+ launchPath: string;
62
+ data: string;
63
+ workspace: string;
64
+ env?: Record<string, string>;
65
+ }) => LiveDaemon;
@@ -0,0 +1,109 @@
1
+ // Helpers for the live suites: they run the real session daemon, ptyhost (ConPTY) and the compiled git shim on Windows.
2
+ //
3
+ // Timing rule: a live test never sleeps for a fixed time and never gives up early. It polls until the condition holds,
4
+ // with a generous deadline (a busy machine, a cold csc.exe or an antivirus scan can make any step 10x slower), and a
5
+ // failure prints what was seen (the viewer's screen, the daemon's log) so the cause is visible.
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { spawn } from 'node:child_process';
9
+ import { ensureDir, sleep } from '../core/util.js';
10
+ import { connect, encodeFrame, FrameReader, FrameType, helloFrame, queryViewers } from '../shared/pty-client.js';
11
+ import { readJsonFile, writeJsonAtomic } from '../shared/rundir.js';
12
+ const WINDIR = process.env.WINDIR ?? 'C:\\Windows';
13
+ export const CSC = path.join(WINDIR, 'Microsoft.NET', 'Framework64', 'v4.0.30319', 'csc.exe');
14
+ /** Live suites need Windows (ConPTY) and the .NET Framework compiler that ships with it. */
15
+ export const liveTestsAvailable = () => process.platform === 'win32' && fs.existsSync(CSC);
16
+ /** Deadlines, in ms. Generous on purpose: they only matter when something is really wrong. */
17
+ export const WAIT = { start: 120000, screen: 45000, response: 60000, viewers: 45000, exit: 45000 };
18
+ export const tail = (text, chars = 500) => (text.length > chars ? '...' + text.slice(-chars) : text).replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
19
+ /** Connects, retrying until `ms`: a connection attempt can time out while the machine is busy. */
20
+ export const connectPatiently = async (endpoint, ms = 60000) => {
21
+ const end = Date.now() + ms;
22
+ for (;;) {
23
+ try {
24
+ return await connect(endpoint, 10000);
25
+ }
26
+ catch (e) {
27
+ if (Date.now() >= end)
28
+ throw e;
29
+ await sleep(250);
30
+ }
31
+ }
32
+ };
33
+ /** Connects like a terminal tab would (`token` can be wrong on purpose) and says hello. */
34
+ export const openViewer = async (attach, options = {}) => {
35
+ const socket = await connectPatiently({ ...attach, addresses: ['127.0.0.1'] });
36
+ const reader = new FrameReader();
37
+ let output = '';
38
+ let exitCode = null;
39
+ let error = '';
40
+ let closed = false;
41
+ socket.on('data', (chunk) => {
42
+ for (const frame of reader.push(chunk)) {
43
+ if (frame.type === FrameType.Output)
44
+ output += frame.payload.toString();
45
+ else if (frame.type === FrameType.Exit)
46
+ exitCode = frame.payload.readInt32LE(0);
47
+ else if (frame.type === FrameType.Error)
48
+ error = frame.payload.toString();
49
+ }
50
+ });
51
+ socket.on('error', () => undefined);
52
+ socket.on('close', () => { closed = true; });
53
+ socket.write(helloFrame(options.token ?? attach.token, options.cols ?? 100, options.rows ?? 30));
54
+ return {
55
+ socket,
56
+ output: () => output,
57
+ exitCode: () => exitCode,
58
+ error: () => error,
59
+ closed: () => closed,
60
+ type: (text) => { socket.write(encodeFrame(FrameType.Input, Buffer.from(text + '\r'))); },
61
+ close: () => socket.destroy(),
62
+ };
63
+ };
64
+ /** Polls until the viewer's screen shows `needle`. */
65
+ export const sees = (t, viewer, needle, ms = WAIT.screen) => t.waitFor(() => viewer.output().includes(needle), ms);
66
+ /** Polls the session's viewer count until `accept` is satisfied; null when it never was. A query that gets no answer is retried. */
67
+ export const viewersWhen = (t, attach, accept, ms = WAIT.viewers) => t.waitFor(async () => {
68
+ const v = await queryViewers({ ...attach, addresses: ['127.0.0.1'] }, 5000);
69
+ return v && accept(v) ? v : null;
70
+ }, ms);
71
+ export const copyCmdAsFakeAgent = (dataPath) => {
72
+ const agentDir = path.join(dataPath, 'tools', 'agents', 'claude');
73
+ ensureDir(agentDir);
74
+ fs.copyFileSync(path.join(WINDIR, 'System32', 'cmd.exe'), path.join(agentDir, 'claude.exe'));
75
+ };
76
+ export const startDaemon = (t, f, options) => {
77
+ const { launchPath, data } = options;
78
+ const output = t.at(path.basename(data) + '-daemon.out');
79
+ const fd = fs.openSync(output, 'w');
80
+ const env = { ...process.env, AGENT_LAUNCH_DIR: launchPath, AGENT_DATA_DIR: data, AGENT_WORKSPACE: options.workspace, AGENT_LOCAL_DIR: f.at(path.basename(data) + '-local'), AGENT_LOG_PREFIX: 'daemon', AGENT_NO_TABS: '1', ...options.env };
81
+ const child = spawn(process.execPath, [path.join(launchPath, 'runtime', 'daemon.js')], { env, stdio: ['ignore', fd, fd], windowsHide: true });
82
+ let exited = false;
83
+ child.on('exit', () => { exited = true; });
84
+ const run = path.join(data, 'run');
85
+ const status = (id) => readJsonFile(path.join(run, 'sessions', id + '.json'));
86
+ return {
87
+ run,
88
+ child,
89
+ request: (body) => writeJsonAtomic(path.join(run, 'requests', body.id + '.json'), { createdAt: new Date().toISOString(), ...body }),
90
+ response: (id, ms = WAIT.response) => t.waitFor(() => readJsonFile(path.join(run, 'responses', id + '.json')), ms),
91
+ status,
92
+ started: (id, ms = WAIT.start) => t.waitFor(() => { const s = status(id); return s && s.phase !== 'starting' ? s : null; }, ms),
93
+ diagnostics: () => {
94
+ const logs = path.join(data, 'logs');
95
+ const files = fs.existsSync(logs) ? fs.readdirSync(logs).filter((n) => n.startsWith('daemon')) : [];
96
+ const logText = files.map((n) => fs.readFileSync(path.join(logs, n), 'utf8')).join('\n');
97
+ return 'daemon output: ' + tail(fs.readFileSync(output, 'utf8'), 600) + '\ndaemon log: ' + tail(logText, 1200);
98
+ },
99
+ stop: async () => {
100
+ if (!exited)
101
+ child.kill();
102
+ await t.waitFor(() => exited, 10000);
103
+ try {
104
+ fs.closeSync(fd);
105
+ }
106
+ catch { /* already closed */ }
107
+ },
108
+ };
109
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './suite.js';
2
+ export declare const localConfigSuite: Suite;
@@ -0,0 +1,120 @@
1
+ // A project's .sandbox-config.json: it must be approved before it applies, it can never loosen protections, and it is
2
+ // validated. The project file is untrusted input (it arrives with a cloned repository).
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { download, setChecksumRequired } from '../shared/download.js';
7
+ import { allowedAnswer, allowStorePath, saveAllowed } from '../core/local-config.js';
8
+ export const localConfigSuite = {
9
+ name: 'local-config',
10
+ title: '.sandbox-config.json: approval, tighten-only rules, validation',
11
+ run: async (t, f) => {
12
+ t.section('approval');
13
+ const localB = {
14
+ harness: 'codex',
15
+ sandbox: { memoryMb: 4096, networking: false, headless: true },
16
+ toolchains: { node: { version: '18' }, rust: { enabled: false } },
17
+ mappings: [{ host: './assets', guest: 'C:\\Assets', readOnly: true }, { host: './data', guest: 'C:\\Data', readOnly: false }],
18
+ scripts: ['./setup.mjs'],
19
+ };
20
+ f.writeLocal(f.workB, localB);
21
+ const untrusted = await t.thrownMessage(() => f.run({ cwd: f.workB }));
22
+ t.includes(untrusted, '--trust-config', 'an untrusted .sandbox-config.json is refused, with the way to approve it');
23
+ t.includes(untrusted, 'READ/WRITE', 'the refusal lists what the file would change (read/write folders)');
24
+ await t.throws(() => f.run({ cwd: f.workB, ask: async () => false }), 'has not been approved', 'answering "no" refuses it');
25
+ const approved = await f.run({ cwd: f.workB, ask: async () => true });
26
+ t.equal({
27
+ harness: approved.harnessName,
28
+ memoryMb: approved.config.sandbox.memoryMb,
29
+ headless: approved.config.sandbox.headless,
30
+ rustEnabled: approved.config.toolchains.rust.enabled,
31
+ pythonStillOn: Boolean(approved.config.toolchains.python.enabled),
32
+ nodeSource: approved.versions.node.source,
33
+ }, { harness: 'codex', memoryMb: 4096, headless: true, rustEnabled: false, pythonStillOn: true, nodeSource: '.sandbox-config.json' }, 'approved local settings apply over the defaults (including toolchains and headless)');
34
+ t.check(approved.config.mappings.some((m) => m.host === path.join(f.workB, 'data') && !m.readOnly), 'relative mapping paths resolve against the folder of .sandbox-config.json', 'mappings: ' + JSON.stringify(approved.config.mappings));
35
+ t.check(approved.scripts.includes(path.join(f.workB, 'setup.mjs')), 'relative script paths resolve against the folder of .sandbox-config.json', 'scripts: ' + JSON.stringify(approved.scripts));
36
+ t.equal((await f.run({ cwd: f.workB })).harnessName, 'codex', 'an approval is remembered');
37
+ t.equal((await f.run({ cwd: f.workB, noLocalConfig: true })).harnessName, 'claude', '--no-local-config ignores the file');
38
+ t.equal((await f.run({ cwd: f.workB, harness: 'gemini' })).harnessName, 'gemini', '--harness beats the file');
39
+ t.equal((await f.run({ cwd: f.workB, headless: false })).config.sandbox.headless, false, '--window beats a local headless setting');
40
+ f.writeLocal(f.workB, { ...localB, sandbox: { memoryMb: 6144 } });
41
+ await t.throws(() => f.run({ cwd: f.workB }), 'has not been approved', 'changing the file requires approval again');
42
+ t.section('validation');
43
+ const dirC = t.dir('work-c');
44
+ const rejectedBecause = (content, needle, name) => {
45
+ f.writeLocal(dirC, content);
46
+ return t.throws(() => f.run({ cwd: dirC, trustConfig: true }), needle, name);
47
+ };
48
+ await rejectedBecause({ storage: { root: 'C:\\x' } }, 'storage is not allowed', 'a project file cannot change the storage folder');
49
+ await rejectedBecause({ harnesses: {} }, 'harnesses is not allowed', 'a project file cannot change agent executables');
50
+ await rejectedBecause({ claudeProfile: { hostDir: 'C:\\x' } }, 'claudeProfile.hostDir is not allowed', 'a project file cannot point at the Claude profile folder');
51
+ await rejectedBecause({ toolchains: { node: { hostDir: 'x' } } }, 'toolchains.node.hostDir is not allowed', 'a project file cannot point at host tool folders');
52
+ await rejectedBecause({ sandbox: { memoryMb: 12 } }, 'sandbox.memoryMb:', 'values are type-checked (memory too small)');
53
+ await rejectedBecause({ mappings: [{ host: 'x', guest: 'C:\\Windows\\x', readOnly: true }] }, 'reserved or invalid guest path', 'reserved guest paths are rejected');
54
+ await rejectedBecause({ mappings: [{ host: '../shared', guest: 'C:\\Shared', readOnly: true }] }, 'only map folders inside the project', 'a project file cannot map folders outside the project');
55
+ await rejectedBecause({ mappings: [{ host: '%APPDATA%', guest: 'C:\\Startup', readOnly: false }] }, 'only map folders inside the project', '... not even through environment variables');
56
+ await rejectedBecause({ provisioning: { git: { access: 'admin' } } }, 'provisioning.git.access:', 'unknown git access modes are rejected');
57
+ await rejectedBecause('{\n // a comment\n "readOnly": tru\n}', 'line 3', 'broken JSON is reported with its line');
58
+ await rejectedBecause({ _comment: 'old style' }, '_comment is not allowed', 'unknown keys are rejected');
59
+ f.writeLocal(dirC, '// comments and trailing commas are fine\n{\n "readOnly": true, /* inline */\n}\n');
60
+ t.equal((await f.run({ cwd: dirC, trustConfig: true })).config.readOnly, true, '.sandbox-config.json may contain comments and trailing commas');
61
+ t.section('a project can only tighten protections');
62
+ // Settings that would loosen a protection are listed before approval, then ignored.
63
+ f.writeLocal(dirC, {
64
+ claudeProfile: { copyCredentials: true },
65
+ network: { blockPrivateRanges: false },
66
+ provisioning: { git: { access: 'full' } },
67
+ sandbox: { vGpu: true, clipboard: true, networking: true, protectedClient: false },
68
+ readOnly: false,
69
+ });
70
+ await t.throws(() => f.run({ cwd: dirC }), 'only used if it is stricter', 'settings a project may only tighten are called out before approval');
71
+ const strictUser = {
72
+ ...f.baseConfig,
73
+ readOnly: true,
74
+ network: { ...f.baseConfig.network, blockPrivateRanges: true },
75
+ provisioning: { ...f.baseConfig.provisioning, git: { ...f.baseConfig.provisioning.git, access: 'readonly' } },
76
+ sandbox: { ...f.baseConfig.sandbox, vGpu: false, clipboard: false, networking: false, protectedClient: true },
77
+ };
78
+ const keptRun = await f.run({ cwd: dirC, trustConfig: true }, strictUser);
79
+ const kept = keptRun.config;
80
+ t.check(keptRun.notes.some((n) => n.includes('sandbox.vGpu = true') && n.includes('your own settings')), 'what a project asked for and did not get is said at launch, with how to allow it', keptRun.notes.join('\n'));
81
+ t.equal({
82
+ readOnly: kept.readOnly,
83
+ blockPrivateRanges: kept.network.blockPrivateRanges,
84
+ gitAccess: kept.provisioning.git.access,
85
+ vGpu: kept.sandbox.vGpu,
86
+ clipboard: kept.sandbox.clipboard,
87
+ networking: kept.sandbox.networking,
88
+ protectedClient: kept.sandbox.protectedClient,
89
+ copyCredentials: kept.claudeProfile.copyCredentials,
90
+ }, { readOnly: true, blockPrivateRanges: true, gitAccess: 'readonly', vGpu: false, clipboard: false, networking: false, protectedClient: true, copyCredentials: false }, 'a project file can never loosen network, git, read-only, GPU, clipboard or login settings');
91
+ // You allowed one request for this project (asked once at an interactive launch): only that one is let through.
92
+ const store = allowStorePath(strictUser.storage.root);
93
+ saveAllowed(store, path.join(dirC, '.sandbox-config.json'), 'sandbox.vGpu', true, true);
94
+ saveAllowed(store, path.join(dirC, '.sandbox-config.json'), 'sandbox.clipboard', true, false);
95
+ const allowedRun = await f.run({ cwd: dirC, trustConfig: true }, strictUser);
96
+ t.equal({ vGpu: allowedRun.config.sandbox.vGpu, clipboard: allowedRun.config.sandbox.clipboard, networking: allowedRun.config.sandbox.networking }, { vGpu: true, clipboard: false, networking: false }, 'a request you allowed for this project is used; one you declined and the others are not');
97
+ t.check(allowedRun.notes.some((n) => n.includes('Allowed for this project: sandbox.vGpu')) && allowedRun.notes.some((n) => n.includes('sandbox.clipboard') && n.includes('You said no')), 'launch says what was allowed and what you declined', allowedRun.notes.join('\n'));
98
+ t.equal(allowedAnswer(store, path.join(f.workB, '.sandbox-config.json'), 'sandbox.vGpu', true), null, 'an answer is kept per project file');
99
+ f.writeLocal(dirC, { readOnly: true, network: { blockPrivateRanges: true }, provisioning: { git: { access: 'none' } }, sandbox: { networking: false, clipboard: false } });
100
+ const tightened = (await f.run({ cwd: dirC, trustConfig: true })).config;
101
+ t.equal({ readOnly: tightened.readOnly, gitAccess: tightened.provisioning.git.access, networking: tightened.sandbox.networking, clipboard: tightened.sandbox.clipboard }, { readOnly: true, gitAccess: 'none', networking: false, clipboard: false }, 'a project file can still make things stricter');
102
+ t.section('downloads.requireChecksum');
103
+ f.writeLocal(dirC, { downloads: { requireChecksum: false } });
104
+ const keptOn = (await f.run({ cwd: dirC, trustConfig: true }, { ...f.baseConfig, downloads: { requireChecksum: true } })).config;
105
+ f.writeLocal(dirC, { downloads: { requireChecksum: true } });
106
+ const turnedOn = (await f.run({ cwd: dirC, trustConfig: true })).config;
107
+ t.equal(keptOn.downloads.requireChecksum, true, 'a project file cannot switch the checksum requirement off');
108
+ t.equal(turnedOn.downloads.requireChecksum, true, 'a project file can switch the checksum requirement on');
109
+ t.equal(f.baseConfig.downloads.requireChecksum, false, '(the requirement is off in the defaults)');
110
+ // With the requirement on, a download without a hash is refused before anything is fetched.
111
+ setChecksumRequired(true);
112
+ const target = path.join(os.tmpdir(), 'agent-sandbox-no-such-download.bin');
113
+ const refused = await download('http://127.0.0.1:9/tool.zip', target).then(() => '', (e) => e.message);
114
+ setChecksumRequired(false);
115
+ t.includes(refused, 'requireChecksum', 'a download with no hash is refused when checksums are required');
116
+ t.includes(refused, 'tool.zip', 'the refusal names the download');
117
+ t.check(!fs.existsSync(target) && !fs.existsSync(target + '.part'), 'a refused download leaves nothing behind');
118
+ fs.rmSync(path.join(dirC, '.sandbox-config.json'));
119
+ },
120
+ };
@@ -1,3 +1,2 @@
1
- import type { Fixtures } from './fixtures.js';
2
- import type { Harness } from './harness.js';
3
- export declare const mirrorTests: (t: Harness, f: Fixtures) => Promise<void>;
1
+ import type { Suite } from './suite.js';
2
+ export declare const mirrorSuite: Suite;