windows-development-sandbox 1.18.4 → 1.23.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 (438) hide show
  1. package/README.md +1 -1
  2. package/config.example.json +17 -1
  3. package/dist/app/features/access/access.def.js +5 -2
  4. package/dist/app/features/access/access.run.js +7 -3
  5. package/dist/app/features/accounts/accounts.def.d.ts +33 -1
  6. package/dist/app/features/accounts/accounts.def.js +10 -4
  7. package/dist/app/features/accounts/accounts.run.d.ts +2 -0
  8. package/dist/app/features/accounts/accounts.run.js +7 -2
  9. package/dist/app/features/accounts/services.def.d.ts +7 -1
  10. package/dist/app/features/accounts/services.def.js +19 -16
  11. package/dist/app/features/accounts/services.run.d.ts +2 -0
  12. package/dist/app/features/accounts/services.run.js +44 -16
  13. package/dist/app/features/agent-commands/agent-commands.def.d.ts +37 -0
  14. package/dist/app/features/agent-commands/agent-commands.def.js +91 -0
  15. package/dist/app/features/agent-commands/agent-commands.run.d.ts +28 -0
  16. package/dist/app/features/agent-commands/agent-commands.run.js +35 -0
  17. package/dist/app/features/agent-commands/index.d.ts +2 -0
  18. package/dist/app/features/agent-commands/index.js +3 -0
  19. package/dist/app/features/agents/agents.def.d.ts +14 -1
  20. package/dist/app/features/agents/agents.def.js +61 -6
  21. package/dist/app/features/agents/agents.run.d.ts +1 -0
  22. package/dist/app/features/agents/agents.run.js +8 -1
  23. package/dist/app/features/agents/claude-env.def.d.ts +36 -0
  24. package/dist/app/features/agents/claude-env.def.js +305 -0
  25. package/dist/app/features/agents/env.def.d.ts +163 -0
  26. package/dist/app/features/agents/env.def.js +184 -0
  27. package/dist/app/features/agents/env.run.d.ts +43 -0
  28. package/dist/app/features/agents/env.run.js +86 -0
  29. package/dist/app/features/agents/index.d.ts +2 -0
  30. package/dist/app/features/agents/index.js +2 -0
  31. package/dist/app/features/defs.d.ts +171 -3
  32. package/dist/app/features/defs.js +9 -0
  33. package/dist/app/features/engine/engine.run.js +1 -1
  34. package/dist/app/features/github/github.def.d.ts +44 -3
  35. package/dist/app/features/github/github.def.js +145 -63
  36. package/dist/app/features/github/github.run.d.ts +9 -2
  37. package/dist/app/features/github/github.run.js +43 -5
  38. package/dist/app/features/health/health.def.js +8 -2
  39. package/dist/app/features/logs/logs.def.js +0 -8
  40. package/dist/app/features/projects/projects.def.js +2 -2
  41. package/dist/app/features/projects/projects.run.js +5 -5
  42. package/dist/app/features/sandbox/sandbox.def.js +7 -3
  43. package/dist/app/features/sessions/sessions.def.d.ts +4 -4
  44. package/dist/app/features/sessions/sessions.def.js +0 -5
  45. package/dist/app/features/sessions/sessions.run.d.ts +1 -1
  46. package/dist/app/features/sessions/sessions.run.js +2 -2
  47. package/dist/app/features/settings/categories.def.js +7 -2
  48. package/dist/app/features/settings/settings-rows.js +6 -2
  49. package/dist/app/features/settings/settings.def.js +1 -1
  50. package/dist/app/features/settings/settings.run.d.ts +8 -0
  51. package/dist/app/features/settings/settings.run.js +12 -3
  52. package/dist/app/features/storage/storage.run.js +18 -9
  53. package/dist/app/features/updates/update-lines.js +2 -2
  54. package/dist/app/features/updates/updates.def.js +1 -1
  55. package/dist/app/features/updates/updates.run.js +13 -3
  56. package/dist/app/features/vpn/vpn.def.d.ts +33 -0
  57. package/dist/app/features/vpn/vpn.def.js +30 -6
  58. package/dist/app/features/vpn/vpn.run.d.ts +11 -0
  59. package/dist/app/features/vpn/vpn.run.js +4 -1
  60. package/dist/app/features/web/web.def.js +2 -2
  61. package/dist/app/features/web/web.run.js +12 -4
  62. package/dist/app/kernel/errors.d.ts +7 -0
  63. package/dist/app/kernel/errors.js +18 -3
  64. package/dist/app/kernel/rpc.d.ts +6 -1
  65. package/dist/app/kernel/view.d.ts +11 -1
  66. package/dist/app/registry.js +7 -1
  67. package/dist/bin/agent-sandbox.js +2 -2
  68. package/dist/bin/{chunk-N4GXYTLV.js → chunk-JETGP2C3.js} +239 -72
  69. package/dist/bin/{chunk-MY7NLQQY.js → chunk-XZKOPGNH.js} +1 -1
  70. package/dist/bin/{chunk-FRPLQLSY.js → chunk-ZTTOF5V3.js} +6566 -4964
  71. package/dist/bin/claude-host.js +2 -2
  72. package/dist/bin/claude-sbx.js +2 -2
  73. package/dist/bin/codex-sbx.js +2 -2
  74. package/dist/bin/gemini-sbx.js +2 -2
  75. package/dist/bin/sandbox.js +2 -2
  76. package/dist/bin/sbx-hide.js +2 -2
  77. package/dist/bin/sbx-show.js +2 -2
  78. package/dist/cli/commands/agent-commands.d.ts +0 -1
  79. package/dist/cli/commands/agent-commands.js +2 -32
  80. package/dist/cli/commands/index.d.ts +1 -1
  81. package/dist/cli/commands/index.js +1 -1
  82. package/dist/cli/commands/launch/launch.js +2 -2
  83. package/dist/cli/commands/registry.js +3 -2
  84. package/dist/cli/manifest.d.ts +3 -3
  85. package/dist/cli/manifest.js +3 -2
  86. package/dist/cli/menu/coverage-exempt.d.ts +17 -0
  87. package/dist/cli/menu/coverage-exempt.js +61 -0
  88. package/dist/cli/menu/io-menu.d.ts +2 -0
  89. package/dist/cli/menu/io-menu.js +50 -5
  90. package/dist/cli/menu/menu.d.ts +1 -1
  91. package/dist/cli/menu/menu.js +3 -3
  92. package/dist/cli/menu/pages/accounts-page.js +1 -1
  93. package/dist/cli/menu/pages/agents-page.d.ts +7 -2
  94. package/dist/cli/menu/pages/agents-page.js +75 -13
  95. package/dist/cli/menu/pages/maintenance-page.d.ts +6 -0
  96. package/dist/cli/menu/pages/maintenance-page.js +10 -4
  97. package/dist/cli/menu/pages/sessions-page.d.ts +2 -0
  98. package/dist/cli/menu/pages/sessions-page.js +12 -4
  99. package/dist/cli/menu/pages/settings-page.js +13 -1
  100. package/dist/cli/menu/remote-menu.js +1 -1
  101. package/dist/cli/menu/shell.d.ts +5 -0
  102. package/dist/cli/menu/shell.js +13 -0
  103. package/dist/cli/menu/views.js +2 -1
  104. package/dist/cli/report.js +2 -0
  105. package/dist/cli/services/agent-commands-service.d.ts +2 -0
  106. package/dist/cli/services/agent-commands-service.js +13 -7
  107. package/dist/cli/services/vpn-service.d.ts +5 -1
  108. package/dist/cli/services/vpn-service.js +6 -1
  109. package/dist/cli/tui/app.d.ts +5 -0
  110. package/dist/cli/tui/app.js +2 -0
  111. package/dist/cli/tui/dialogs/text-screen.js +2 -1
  112. package/dist/cli/tui/terminal.js +3 -1
  113. package/dist/core/config/defaults.js +12 -1
  114. package/dist/core/detect/toolchains.js +9 -0
  115. package/dist/core/disk.d.ts +1 -5
  116. package/dist/core/disk.js +1 -16
  117. package/dist/core/schema/accounts.d.ts +6 -0
  118. package/dist/core/schema/accounts.js +13 -11
  119. package/dist/core/schema/codex.d.ts +29 -0
  120. package/dist/core/schema/codex.js +24 -0
  121. package/dist/core/schema/config.d.ts +58 -14
  122. package/dist/core/schema/config.js +6 -4
  123. package/dist/core/schema/github.js +1 -1
  124. package/dist/core/schema/index.d.ts +1 -0
  125. package/dist/core/schema/index.js +1 -0
  126. package/dist/core/schema/profile.js +8 -4
  127. package/dist/core/schema/resume.js +1 -1
  128. package/dist/core/schema/sandbox.js +3 -3
  129. package/dist/core/schema/toolchains.d.ts +4 -0
  130. package/dist/core/schema/toolchains.js +3 -0
  131. package/dist/core/schema/vpn.js +1 -1
  132. package/dist/core/schema/web.js +3 -3
  133. package/dist/core/types.d.ts +8 -0
  134. package/dist/core/util/errors.d.ts +11 -1
  135. package/dist/core/util/errors.js +7 -1
  136. package/dist/core/util/index.d.ts +1 -1
  137. package/dist/core/util/index.js +1 -1
  138. package/dist/guest/accounts/adapter.d.ts +2 -0
  139. package/dist/guest/accounts/adapters.js +4 -0
  140. package/dist/guest/accounts/apply.d.ts +10 -6
  141. package/dist/guest/accounts/kinds/cfx.d.ts +7 -0
  142. package/dist/guest/accounts/kinds/cfx.js +33 -3
  143. package/dist/guest/accounts/kinds/comet.d.ts +7 -0
  144. package/dist/guest/accounts/kinds/comet.js +50 -0
  145. package/dist/guest/accounts/kinds/perplexity.d.ts +2 -0
  146. package/dist/guest/accounts/kinds/perplexity.js +12 -0
  147. package/dist/guest/accounts/watch.d.ts +2 -0
  148. package/dist/guest/accounts/watch.js +12 -2
  149. package/dist/guest/agents/agent-files.d.ts +26 -6
  150. package/dist/guest/agents/agent-files.js +84 -14
  151. package/dist/guest/agents/agent-installer.js +1 -1
  152. package/dist/guest/agents/local-state.d.ts +5 -2
  153. package/dist/guest/agents/local-state.js +24 -10
  154. package/dist/guest/agents/profile-links.d.ts +2 -0
  155. package/dist/guest/agents/profile-links.js +18 -4
  156. package/dist/guest/apps/after-install.js +19 -0
  157. package/dist/guest/apps/app-installer.d.ts +11 -0
  158. package/dist/guest/apps/app-installer.js +69 -6
  159. package/dist/guest/apps/comet-unpack.d.ts +20 -0
  160. package/dist/guest/apps/comet-unpack.js +127 -0
  161. package/dist/guest/apps/index.d.ts +1 -1
  162. package/dist/guest/apps/index.js +1 -1
  163. package/dist/guest/apps/resolvers-agents-web.d.ts +2 -0
  164. package/dist/guest/apps/resolvers-agents-web.js +7 -0
  165. package/dist/guest/apps/resolvers.js +2 -0
  166. package/dist/guest/apps/shortcuts.d.ts +2 -0
  167. package/dist/guest/apps/shortcuts.js +1 -0
  168. package/dist/guest/apps/unpack.js +14 -4
  169. package/dist/guest/boot/boot.d.ts +45 -16
  170. package/dist/guest/boot/boot.js +325 -149
  171. package/dist/guest/boot/graph.d.ts +252 -0
  172. package/dist/guest/boot/graph.js +257 -0
  173. package/dist/guest/boot/guide-lines.d.ts +25 -0
  174. package/dist/guest/boot/guide-lines.js +41 -0
  175. package/dist/guest/boot/guide.d.ts +1 -4
  176. package/dist/guest/boot/guide.js +6 -8
  177. package/dist/guest/boot/runner.d.ts +77 -0
  178. package/dist/guest/boot/runner.js +215 -0
  179. package/dist/guest/browser/cdp.d.ts +71 -0
  180. package/dist/guest/browser/cdp.js +306 -0
  181. package/dist/guest/browser/comet.d.ts +28 -0
  182. package/dist/guest/browser/comet.js +155 -0
  183. package/dist/guest/daemon/project-provisioner.js +6 -1
  184. package/dist/guest/daemon/sessions.js +2 -1
  185. package/dist/guest/devtools/fivem.d.ts +1 -1
  186. package/dist/guest/devtools/fivem.js +243 -3
  187. package/dist/guest/entries/daemon.js +2 -0
  188. package/dist/guest/runtime/log.d.ts +12 -0
  189. package/dist/guest/runtime/log.js +29 -5
  190. package/dist/guest/runtime/pools.d.ts +24 -0
  191. package/dist/guest/runtime/pools.js +60 -0
  192. package/dist/guest/runtime/process.d.ts +4 -0
  193. package/dist/guest/runtime/process.js +38 -1
  194. package/dist/guest/runtime/status.d.ts +5 -1
  195. package/dist/guest/runtime/status.js +19 -0
  196. package/dist/guest/setup/addon-mcp.d.ts +1 -1
  197. package/dist/guest/setup/addon-mcp.js +52 -12
  198. package/dist/guest/setup/agent-addons.d.ts +3 -2
  199. package/dist/guest/setup/agent-addons.js +12 -3
  200. package/dist/guest/setup/environment.d.ts +1 -1
  201. package/dist/guest/setup/environment.js +25 -12
  202. package/dist/guest/setup/headroom.d.ts +8 -3
  203. package/dist/guest/setup/headroom.js +44 -15
  204. package/dist/guest/setup/path-plan.d.ts +28 -0
  205. package/dist/guest/setup/path-plan.js +69 -2
  206. package/dist/guest/setup/shell-path.d.ts +1 -1
  207. package/dist/guest/setup/shell-path.js +3 -3
  208. package/dist/guest/toolchains/fivem/artifacts.d.ts +48 -1
  209. package/dist/guest/toolchains/fivem/artifacts.js +97 -4
  210. package/dist/guest/toolchains/fivem/fivem.d.ts +32 -1
  211. package/dist/guest/toolchains/fivem/fivem.js +168 -3
  212. package/dist/guest/toolchains/fivem/scaffold.d.ts +35 -0
  213. package/dist/guest/toolchains/fivem/scaffold.js +105 -0
  214. package/dist/guest/toolchains/fivem/serve-plan.d.ts +34 -0
  215. package/dist/guest/toolchains/fivem/serve-plan.js +61 -0
  216. package/dist/guest/toolchains/installers.d.ts +6 -10
  217. package/dist/guest/toolchains/installers.js +4 -12
  218. package/dist/guest/toolchains/languages/go.js +1 -1
  219. package/dist/guest/toolchains/languages/node.js +2 -1
  220. package/dist/guest/toolchains/languages/php.d.ts +2 -0
  221. package/dist/guest/toolchains/languages/php.js +13 -1
  222. package/dist/guest/toolchains/languages/python-tools.js +1 -1
  223. package/dist/guest/toolchains/languages/ruby.js +1 -1
  224. package/dist/guest/toolchains/native/autosdk.d.ts +3 -0
  225. package/dist/guest/toolchains/native/autosdk.js +50 -0
  226. package/dist/guest/toolchains/native/binstall.js +1 -1
  227. package/dist/guest/toolchains/native/msvc/packages.js +10 -4
  228. package/dist/guest/toolchains/native/rust.d.ts +2 -0
  229. package/dist/guest/toolchains/native/rust.js +11 -4
  230. package/dist/guest/toolchains/native/unreal.d.ts +2 -0
  231. package/dist/guest/toolchains/native/unreal.js +19 -6
  232. package/dist/guest/toolchains/native/vcruntime.d.ts +2 -3
  233. package/dist/guest/toolchains/native/vcruntime.js +12 -7
  234. package/dist/guest/toolchains/native/visual-studio.d.ts +5 -2
  235. package/dist/guest/toolchains/native/visual-studio.js +127 -25
  236. package/dist/guest/toolchains/native/vs-request.d.ts +23 -0
  237. package/dist/guest/toolchains/native/vs-request.js +62 -1
  238. package/dist/guest/toolchains/not-yet.d.ts +1 -1
  239. package/dist/guest/toolchains/not-yet.js +3 -3
  240. package/dist/guest/toolchains/persisted.js +2 -7
  241. package/dist/guest/toolchains/plan.js +27 -11
  242. package/dist/guest/toolchains/scheduler.d.ts +2 -21
  243. package/dist/guest/toolchains/scheduler.js +7 -121
  244. package/dist/guest/toolchains/setup.d.ts +15 -2
  245. package/dist/guest/toolchains/setup.js +39 -20
  246. package/dist/guest/toolchains/signature.d.ts +5 -0
  247. package/dist/guest/toolchains/signature.js +22 -0
  248. package/dist/guest/toolchains/split.d.ts +28 -0
  249. package/dist/guest/toolchains/split.js +60 -0
  250. package/dist/guest/toolchains/steam/client.js +2 -1
  251. package/dist/guest/toolchains/step-scheduler.d.ts +51 -0
  252. package/dist/guest/toolchains/step-scheduler.js +147 -0
  253. package/dist/guest/toolchains/step.d.ts +13 -0
  254. package/dist/guest/toolchains/tools/git.js +18 -0
  255. package/dist/guest/vpn/openvpn.js +3 -4
  256. package/dist/guest/vpn/tunnel.d.ts +3 -0
  257. package/dist/guest/vpn/tunnel.js +10 -12
  258. package/dist/guest/vpn/vpn.js +6 -1
  259. package/dist/guest/vpn/wireguard.js +11 -4
  260. package/dist/host/accounts/deliver.js +7 -0
  261. package/dist/host/accounts/handoff.js +2 -6
  262. package/dist/host/accounts/index.d.ts +1 -1
  263. package/dist/host/accounts/index.js +1 -1
  264. package/dist/host/accounts/kinds/cfx.d.ts +1 -1
  265. package/dist/host/accounts/kinds/cfx.js +47 -4
  266. package/dist/host/accounts/kinds/comet.d.ts +2 -0
  267. package/dist/host/accounts/kinds/comet.js +6 -0
  268. package/dist/host/accounts/kinds/perplexity.d.ts +2 -0
  269. package/dist/host/accounts/kinds/perplexity.js +30 -0
  270. package/dist/host/accounts/leases.d.ts +9 -3
  271. package/dist/host/accounts/leases.js +13 -2
  272. package/dist/host/accounts/source.d.ts +7 -1
  273. package/dist/host/accounts/source.js +28 -1
  274. package/dist/host/accounts/sources.js +4 -0
  275. package/dist/host/agent-commands/commands.js +1 -1
  276. package/dist/host/agent-commands/forwarders.d.ts +14 -2
  277. package/dist/host/agent-commands/forwarders.js +31 -13
  278. package/dist/host/agent-commands/reconcile.d.ts +2 -1
  279. package/dist/host/agent-commands/reconcile.js +7 -8
  280. package/dist/host/agent-commands/user-path.d.ts +5 -0
  281. package/dist/host/agent-commands/user-path.js +13 -0
  282. package/dist/host/github/deliver.d.ts +1 -1
  283. package/dist/host/github/deliver.js +20 -1
  284. package/dist/host/github/index.d.ts +2 -1
  285. package/dist/host/github/index.js +1 -0
  286. package/dist/host/github/token.d.ts +13 -1
  287. package/dist/host/github/token.js +19 -2
  288. package/dist/host/github/verify.d.ts +27 -0
  289. package/dist/host/github/verify.js +61 -0
  290. package/dist/host/launch/hints/fivem.d.ts +3 -1
  291. package/dist/host/launch/hints/fivem.js +68 -2
  292. package/dist/host/launch/session-file.js +5 -0
  293. package/dist/host/launch/toolchains.d.ts +2 -1
  294. package/dist/host/launch/toolchains.js +5 -3
  295. package/dist/host/profile/accounts.js +2 -4
  296. package/dist/host/profile/claude-login.d.ts +3 -2
  297. package/dist/host/profile/claude-login.js +3 -13
  298. package/dist/host/vpn/index.d.ts +2 -1
  299. package/dist/host/vpn/index.js +2 -1
  300. package/dist/host/vpn/last.d.ts +28 -0
  301. package/dist/host/vpn/last.js +43 -0
  302. package/dist/host/vpn/login.d.ts +3 -1
  303. package/dist/host/vpn/login.js +17 -3
  304. package/dist/host/vpn/nordvpn.d.ts +2 -4
  305. package/dist/host/vpn/nordvpn.js +2 -24
  306. package/dist/runtime/agent.js +433 -90
  307. package/dist/runtime/daemon.js +3378 -1546
  308. package/dist/runtime/db.js +363 -90
  309. package/dist/runtime/main.js +5134 -2096
  310. package/dist/runtime/netproxy.js +308 -69
  311. package/dist/runtime/tool.js +2867 -1765
  312. package/dist/runtime/view.js +308 -69
  313. package/dist/selftest/agent-commands.test.js +45 -7
  314. package/dist/selftest/agent-paths.test.js +40 -13
  315. package/dist/selftest/agent-settings.test.d.ts +2 -0
  316. package/dist/selftest/agent-settings.test.js +94 -0
  317. package/dist/selftest/agents-web.test.d.ts +2 -0
  318. package/dist/selftest/agents-web.test.js +647 -0
  319. package/dist/selftest/app.test.js +7 -3
  320. package/dist/selftest/boot-runner.test.d.ts +2 -0
  321. package/dist/selftest/boot-runner.test.js +456 -0
  322. package/dist/selftest/channel.test.js +15 -0
  323. package/dist/selftest/cli.test.js +4 -0
  324. package/dist/selftest/fivem.test.js +249 -3
  325. package/dist/selftest/github.test.js +94 -9
  326. package/dist/selftest/guest-boot.test.js +3 -0
  327. package/dist/selftest/health.test.js +1 -1
  328. package/dist/selftest/index.js +10 -0
  329. package/dist/selftest/layout.test.js +5 -0
  330. package/dist/selftest/menu-coverage.test.d.ts +2 -0
  331. package/dist/selftest/menu-coverage.test.js +46 -0
  332. package/dist/selftest/menu-screens.js +37 -1
  333. package/dist/selftest/menu.test.js +5 -3
  334. package/dist/selftest/pc-installs.test.js +5 -0
  335. package/dist/selftest/persisted-installs.test.js +72 -0
  336. package/dist/selftest/service-accounts.test.js +3 -2
  337. package/dist/selftest/support/engine-golden.js +28 -1
  338. package/dist/selftest/support/fixtures.d.ts +15 -0
  339. package/dist/selftest/support/web-preview.d.ts +34 -1
  340. package/dist/selftest/support/web-preview.js +21 -18
  341. package/dist/selftest/toolchains.test.js +23 -1
  342. package/dist/selftest/ui-text.test.d.ts +7 -0
  343. package/dist/selftest/ui-text.test.js +300 -0
  344. package/dist/selftest/vpn.test.js +51 -3
  345. package/dist/shared/accounts/anthropic.d.ts +5 -0
  346. package/dist/shared/accounts/anthropic.js +15 -0
  347. package/dist/shared/accounts/index.d.ts +3 -0
  348. package/dist/shared/accounts/index.js +3 -0
  349. package/dist/shared/accounts/kinds/comet.d.ts +12 -0
  350. package/dist/shared/accounts/kinds/comet.js +64 -0
  351. package/dist/shared/accounts/kinds/perplexity.d.ts +8 -0
  352. package/dist/shared/accounts/kinds/perplexity.js +37 -0
  353. package/dist/shared/accounts/kinds.d.ts +24 -5
  354. package/dist/shared/accounts/kinds.js +86 -4
  355. package/dist/shared/accounts/payloads.d.ts +81 -0
  356. package/dist/shared/accounts/payloads.js +39 -1
  357. package/dist/shared/apps/agent-addons.d.ts +33 -5
  358. package/dist/shared/apps/agent-addons.js +95 -3
  359. package/dist/shared/apps/catalogue-agents-web.d.ts +14 -0
  360. package/dist/shared/apps/catalogue-agents-web.js +112 -0
  361. package/dist/shared/apps/catalogue-modding.js +32 -1
  362. package/dist/shared/apps/catalogue.d.ts +27 -3
  363. package/dist/shared/apps/catalogue.js +2 -1
  364. package/dist/shared/apps/index.d.ts +3 -1
  365. package/dist/shared/apps/index.js +3 -1
  366. package/dist/shared/channel/schema.d.ts +40 -3
  367. package/dist/shared/channel/schema.js +9 -0
  368. package/dist/shared/channel/secrets.d.ts +19 -0
  369. package/dist/shared/concurrency/index.d.ts +3 -0
  370. package/dist/shared/concurrency/index.js +3 -0
  371. package/dist/shared/concurrency/installer-retry.d.ts +6 -0
  372. package/dist/shared/concurrency/installer-retry.js +10 -0
  373. package/dist/shared/concurrency/limiter.d.ts +19 -0
  374. package/dist/shared/concurrency/limiter.js +67 -0
  375. package/dist/shared/concurrency/pools.d.ts +7 -0
  376. package/dist/shared/concurrency/pools.js +9 -0
  377. package/dist/shared/download/archive.d.ts +2 -0
  378. package/dist/shared/download/archive.js +11 -1
  379. package/dist/shared/download/bounded-fetch.d.ts +13 -0
  380. package/dist/shared/download/bounded-fetch.js +40 -0
  381. package/dist/shared/download/downloader.d.ts +5 -0
  382. package/dist/shared/download/downloader.js +50 -38
  383. package/dist/shared/download/index.d.ts +2 -1
  384. package/dist/shared/download/index.js +2 -1
  385. package/dist/shared/io/disk.d.ts +9 -0
  386. package/dist/shared/io/disk.js +19 -0
  387. package/dist/shared/io/index.d.ts +1 -0
  388. package/dist/shared/io/index.js +1 -0
  389. package/dist/shared/readiness.d.ts +9 -0
  390. package/dist/shared/readiness.js +11 -0
  391. package/dist/shared/signer.d.ts +24 -0
  392. package/dist/shared/signer.js +15 -0
  393. package/dist/shared/vpn/failure.d.ts +10 -0
  394. package/dist/shared/vpn/failure.js +28 -0
  395. package/dist/sync/watcher.js +2 -0
  396. package/dist/web/index.html +1 -1
  397. package/dist/web/{main-GVZWWP4I.js → main-CCILLX2M.js} +75 -74
  398. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  399. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  400. package/native/csharp/HostTools/Program.cs +14 -1
  401. package/native/csharp/HostTools/UserPath.cs +17 -0
  402. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  403. package/native/scripts/edit-user-path.ps1 +6 -2
  404. package/package.json +1 -1
  405. package/sandbox/addons/browser-use-mcp/requirements.in +1 -0
  406. package/sandbox/addons/browser-use-mcp/requirements.txt +2350 -0
  407. package/sandbox/addons/chrome-devtools-mcp/package-lock.json +38 -0
  408. package/sandbox/addons/chrome-devtools-mcp/package.json +7 -0
  409. package/sandbox/addons/comet-mcp/package-lock.json +1277 -0
  410. package/sandbox/addons/comet-mcp/package.json +7 -0
  411. package/sandbox/addons/perplexity-mcp/package-lock.json +1655 -0
  412. package/sandbox/addons/perplexity-mcp/package.json +7 -0
  413. package/sandbox/scripts/add-firewall-port.ps1 +18 -4
  414. package/sandbox/scripts/get-signature.ps1 +13 -0
  415. package/sandbox/scripts/set-machine-environment.ps1 +30 -15
  416. package/sandbox/templates/fivem/README.txt +6 -2
  417. package/sandbox/templates/fivem/cs/Client/Client.csproj +15 -0
  418. package/sandbox/templates/fivem/cs/Client/ClientMain.cs +20 -0
  419. package/sandbox/templates/fivem/cs/Server/Server.csproj +14 -0
  420. package/sandbox/templates/fivem/cs/Server/ServerMain.cs +20 -0
  421. package/sandbox/templates/fivem/cs/fxmanifest.lua +11 -0
  422. package/sandbox/templates/fivem/js/client.js +12 -0
  423. package/sandbox/templates/fivem/js/fxmanifest.lua +9 -0
  424. package/sandbox/templates/fivem/js/server.js +6 -0
  425. package/sandbox/templates/fivem/lua/client.lua +8 -0
  426. package/sandbox/templates/fivem/lua/fxmanifest.lua +10 -0
  427. package/sandbox/templates/fivem/lua/server.lua +6 -0
  428. package/sandbox/templates/fivem/server.cfg +37 -0
  429. package/sandbox/templates/fivem/ts/build.js +17 -0
  430. package/sandbox/templates/fivem/ts/fxmanifest.lua +9 -0
  431. package/sandbox/templates/fivem/ts/package.json +16 -0
  432. package/sandbox/templates/fivem/ts/src/client/client.ts +12 -0
  433. package/sandbox/templates/fivem/ts/src/client/tsconfig.json +13 -0
  434. package/sandbox/templates/fivem/ts/src/server/server.ts +6 -0
  435. package/sandbox/templates/fivem/ts/src/server/tsconfig.json +13 -0
  436. package/schemas/config.schema.json +120 -27
  437. package/schemas/sandbox-config.schema.json +45 -14
  438. package/sandbox/scripts/get-signer.ps1 +0 -8
@@ -0,0 +1,647 @@
1
+ // Perplexity and the computer-use tools (P8): the perplexity and comet sign-ins (shapes, limits, where the key may
2
+ // and may not appear, the cookie allow-list, several sandboxes handing Comet back), the DevTools client over a fake
3
+ // socket, Codex's config.toml (MCP sign-ins in a file, the add-ons' servers), the catalogue entries, the signer
4
+ // check and the Chromium unpacking helpers. Temp folders and SecretStore.memory() only; no download, nothing run.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { Duplex } from 'node:stream';
8
+ import { importServiceFlow, loginServiceFlow, serviceRow } from '../app/features/accounts/index.js';
9
+ import { fakeIO } from '../app/kernel/index.js';
10
+ import { DEFAULTS } from '../core/config/index.js';
11
+ import { applyOverrides, normalizeOverrides, OFF_IS_SAFER } from '../core/local-config/index.js';
12
+ import { ConfigSchema, LocalOverridesSchema } from '../core/schema/index.js';
13
+ import { AccountLeases, HOST_SOURCES, prepareServiceAccounts, parseRegValue, ServiceAccountStore, takeHandoffs, } from '../host/accounts/index.js';
14
+ import { generateSandbox, toolchainsOnForBoot } from '../host/launch/index.js';
15
+ import { handoffDir } from '../host/accounts/index.js';
16
+ import { clearSecrets, SecretStore } from '../host/secrets/index.js';
17
+ import { applyServiceAccounts } from '../guest/accounts/apply.js';
18
+ import { AccountWatch } from '../guest/accounts/watch.js';
19
+ import { cometAdapter, cometPendingFile, takePendingComet } from '../guest/accounts/kinds/comet.js';
20
+ import { perplexityAdapter } from '../guest/accounts/kinds/perplexity.js';
21
+ import { chromiumBin, keptInstaller, nextInner } from '../guest/apps/comet-unpack.js';
22
+ import { acceptFor, CdpConnection, encodeFrame, FrameReader, MAX_MESSAGE_BYTES, OPCODE, parseDevToolsActivePort, readHandshake, } from '../guest/browser/cdp.js';
23
+ import { cometLaunchArgs, cometShortcutArgs } from '../guest/browser/comet.js';
24
+ import { appUsersOn, cometPayloadFromCookies, cookiesToRestore, fingerprint, MAX_COMET_COOKIES, PAYLOADS, perplexityKeyFromText, SERVICE_KINDS, SERVICE_SPECS, serviceWanted, usedByText, } from '../shared/accounts/index.js';
25
+ import { addonMcpServer, AGENTS_WEB_APPS, APPS, appsNeedUv, appsToInstall, codexWithAddonMcpServers, COMET_SIGNER, PERPLEXITY_COMPUTER_MCP_URL, signerMatches, } from '../shared/apps/index.js';
26
+ import { RunDir } from '../shared/channel/index.js';
27
+ const KEY = 'pplx-PlantedPerplexityKey0123456789abcdWXYZ';
28
+ const OTHER_SECRET = 'sk-PlantedOtherSecretInTheSameEnvFile';
29
+ const NOW = 1_800_000_000;
30
+ const DAY = 86400;
31
+ /** Every file below `dir` (text). */
32
+ function allText(dir) {
33
+ if (!fs.existsSync(dir))
34
+ return [];
35
+ return fs
36
+ .readdirSync(dir, { recursive: true, withFileTypes: true })
37
+ .filter((e) => e.isFile())
38
+ .map((e) => fs.readFileSync(path.join(e.parentPath, e.name), 'utf8'));
39
+ }
40
+ /** A cookie as CDP reports it, signed in by default. */
41
+ const cdpCookie = (over = {}) => ({
42
+ name: '__Secure-next-auth.session-token',
43
+ value: 'PlantedSessionToken.abc',
44
+ domain: '.perplexity.ai',
45
+ path: '/',
46
+ expires: NOW + 30 * DAY + 1234.5,
47
+ size: 40,
48
+ httpOnly: true,
49
+ secure: true,
50
+ session: false,
51
+ sameSite: 'Lax',
52
+ priority: 'Medium',
53
+ ...over,
54
+ });
55
+ /** A server's frame that is not the last of its message. */
56
+ const unfinished = (opcode, payload) => {
57
+ const frame = encodeFrame(opcode, payload, null);
58
+ frame[0] &= 0x7f;
59
+ return frame;
60
+ };
61
+ /** A fake browser behind a fake socket: answers the handshake, then each call with `answer`. */
62
+ function fakeBrowser(answer, o = {}) {
63
+ const seen = [];
64
+ const pongs = [];
65
+ const reader = new FrameReader(MAX_MESSAGE_BYTES, true);
66
+ let shook = false;
67
+ const socket = new Duplex({
68
+ read() {
69
+ // The test pushes the browser's bytes.
70
+ },
71
+ write(chunk, _encoding, done) {
72
+ if (!shook) {
73
+ shook = true;
74
+ const key = /Sec-WebSocket-Key: (\S+)/.exec(chunk.toString('latin1'))?.[1] ?? '';
75
+ const accept = o.badAccept ? acceptFor('wrong') : acceptFor(key);
76
+ const head = 'HTTP/1.1 101 WebSocket Protocol Handshake\r\nUpgrade: WebSocket\r\nConnection: Upgrade\r\n';
77
+ socket.push(Buffer.from(head + 'Sec-WebSocket-Accept: ' + accept + '\r\n\r\n'));
78
+ // A ping right behind the handshake, in the same chunk as nothing else: the client must answer it.
79
+ socket.push(encodeFrame(OPCODE.ping, Buffer.from('hi'), null));
80
+ }
81
+ else {
82
+ for (const frame of reader.push(chunk)) {
83
+ if (frame.opcode === OPCODE.pong)
84
+ pongs.push(frame.payload);
85
+ if (frame.opcode !== OPCODE.text)
86
+ continue;
87
+ const call = JSON.parse(frame.payload.toString('utf8'));
88
+ seen.push({ method: call.method, ...(call.sessionId ? { sessionId: call.sessionId } : {}) });
89
+ // Byte by byte: frames split anywhere must still be read.
90
+ for (const part of answer(call))
91
+ for (const byte of part)
92
+ socket.push(Buffer.from([byte]));
93
+ }
94
+ }
95
+ done();
96
+ },
97
+ });
98
+ return { socket, seen, pongs };
99
+ }
100
+ const ENDPOINT = { port: 9222, path: '/devtools/browser/0b1c2d3e-aaaa-bbbb-cccc-0123456789ab' };
101
+ export const agentsWebSuite = {
102
+ name: 'agents-web',
103
+ title: 'Perplexity and computer-use tools: the two sign-ins, DevTools client, Codex config, catalogue, signer',
104
+ run: async (t, f) => {
105
+ t.section('the perplexity and comet kinds');
106
+ const perplexity = SERVICE_SPECS.perplexity;
107
+ const comet = SERVICE_SPECS.comet;
108
+ t.check(SERVICE_KINDS.includes('perplexity') &&
109
+ SERVICE_KINDS.includes('comet') &&
110
+ DEFAULTS.accounts.perplexity.share === false &&
111
+ DEFAULTS.accounts.comet.share === false, 'both are kinds, and neither is shared by default');
112
+ t.check(perplexity.maxBytes === 1024 &&
113
+ !perplexity.returns &&
114
+ !perplexity.singleHolder &&
115
+ perplexity.sources.join() === 'pc,file,typed' &&
116
+ comet.maxBytes === 256 * 1024 &&
117
+ comet.returns &&
118
+ !comet.singleHolder &&
119
+ comet.sources.join() === 'sandbox', 'perplexity: from this PC, a file or typed, never handed back; comet: only from a sandbox, handed back, many at once');
120
+ const words = [...perplexity.warnings, ...comet.warnings].join(' ');
121
+ t.check(words.includes('Perplexity Pro does not include API use; the API key is billed separately.') &&
122
+ words.includes("Your Comet sign-in can't be copied from this PC: Windows locks it to your PC account. Sign in once inside a sandbox and it is kept for the next ones.") &&
123
+ words.includes('spending limit') &&
124
+ words.includes('Computer credits') &&
125
+ words.includes('prompt injection') &&
126
+ words.includes('never saved passwords') &&
127
+ words.includes('Never import your main browser profile') &&
128
+ perplexity.warnings[0].startsWith('The agent in the sandbox can use') &&
129
+ comet.warnings[0].startsWith('The agent in the sandbox can use'), 'the wording and the security notes are in the kinds (every surface shows the warnings)');
130
+ const store = new ServiceAccountStore(t.dir('web-rows'), SecretStore.memory());
131
+ const leases0 = new AccountLeases(t.dir('web-rows'));
132
+ const rows = ['perplexity', 'comet'].map((k) => serviceRow(k, DEFAULTS, store, leases0));
133
+ t.check(rows[0].canLogin &&
134
+ rows[0].canImport &&
135
+ !rows[0].needsFile &&
136
+ !rows[1].canLogin &&
137
+ !rows[1].canImport &&
138
+ rows[1].usedFor === 'the Comet app' &&
139
+ rows[0].warnings.length === perplexity.warnings.length &&
140
+ rows[1].warnings.length === comet.warnings.length, 'their Accounts rows: perplexity can be imported (this PC or a file) or typed; comet only signs in inside');
141
+ t.check(OFF_IS_SAFER.includes('accounts.perplexity.share') &&
142
+ OFF_IS_SAFER.includes('accounts.comet.share') &&
143
+ applyOverrides(structuredClone(DEFAULTS), normalizeOverrides({ accounts: { perplexity: { share: true }, comet: { share: true } } }, f.workA, 'x')).accounts.perplexity.share === false, 'a project file can only turn them off');
144
+ t.check(appUsersOn(['perplexity-mcp']).join() === 'perplexity' &&
145
+ appUsersOn(['comet', 'rtk']).join() === 'comet' &&
146
+ appUsersOn(['perplexity-computer-mcp', 'comet-mcp']).length === 0 &&
147
+ serviceWanted('perplexity', new Set(['perplexity'])) &&
148
+ !serviceWanted('perplexity', new Set(['comet', 'unreal'])) &&
149
+ serviceWanted('comet', new Set(['comet'])) &&
150
+ usedByText(['reactNative', 'flutter']) === 'reactNative or flutter', 'the key goes only where the Perplexity MCP app is on, the cookies only where Comet is');
151
+ const appsConfig = f.makeConfig();
152
+ appsConfig.apps.install = ['perplexity-mcp', 'comet'];
153
+ const onForBoot = toolchainsOnForBoot({ run: await f.run({}, appsConfig), pcLanguages: [] });
154
+ t.check(onForBoot.has('perplexity') && onForBoot.has('comet'), 'a launch with those apps turns both on');
155
+ t.section('payload shapes and limits');
156
+ const keyOk = (apiKey) => PAYLOADS.perplexity.safeParse({ apiKey }).success;
157
+ t.check(keyOk(KEY) &&
158
+ !keyOk('pplx-short') &&
159
+ !keyOk('sk-' + 'a'.repeat(40)) &&
160
+ !keyOk(KEY + '\n') &&
161
+ !keyOk(KEY + ' extra') &&
162
+ !keyOk('pplx-' + 'a'.repeat(201)) &&
163
+ !PAYLOADS.perplexity.safeParse({ apiKey: KEY, more: 1 }).success, 'a Perplexity key is pplx- and 8 to 200 key characters, nothing else');
164
+ const cookieOk = (over) => PAYLOADS.comet.safeParse({
165
+ cookies: [
166
+ { name: 'a', value: 'b', domain: '.perplexity.ai', path: '/', secure: true, httpOnly: true, ...over },
167
+ ],
168
+ }).success;
169
+ t.check(cookieOk({}) &&
170
+ cookieOk({ domain: 'www.perplexity.ai', expires: NOW, sameSite: 'Strict' }) &&
171
+ !cookieOk({ domain: 'example.com' }) &&
172
+ !cookieOk({ domain: 'perplexity.ai.evil.com' }) &&
173
+ !cookieOk({ domain: 'evilperplexity.ai' }) &&
174
+ !cookieOk({ value: 'x'.repeat(8193) }) &&
175
+ !cookieOk({ name: 'x'.repeat(257) }) &&
176
+ !cookieOk({ value: 'line\nbreak' }) &&
177
+ !cookieOk({ name: 'a;b' }) &&
178
+ !cookieOk({ path: 'relative' }) &&
179
+ !cookieOk({ sameSite: 'Sometimes' }) &&
180
+ !cookieOk({ password: 'x' }), 'a kept cookie is a perplexity.ai cookie of bounded, printable text, and nothing more');
181
+ const many = Array.from({ length: MAX_COMET_COOKIES + 1 }, (_, i) => ({
182
+ name: 'c' + i,
183
+ value: 'v',
184
+ domain: '.perplexity.ai',
185
+ path: '/',
186
+ secure: true,
187
+ httpOnly: true,
188
+ }));
189
+ t.check(!PAYLOADS.comet.safeParse({ cookies: many }).success &&
190
+ PAYLOADS.comet.safeParse({ cookies: many.slice(1) }).success &&
191
+ !PAYLOADS.comet.safeParse({ cookies: [] }).success, 'at most ' + MAX_COMET_COOKIES + ' cookies, and at least one');
192
+ t.section('a key from a .env file');
193
+ const env = [
194
+ '# keys',
195
+ 'OPENAI_API_KEY=' + OTHER_SECRET,
196
+ 'export PERPLEXITY_API_KEY="pplx-old0123456789"',
197
+ 'PERPLEXITY_API_KEY=' + KEY + ' # the new one',
198
+ 'OTHER=1',
199
+ ].join('\r\n');
200
+ t.equal(perplexityKeyFromText(env), { apiKey: KEY }, 'its PERPLEXITY_API_KEY line is read (the last one wins)');
201
+ t.equal(perplexityKeyFromText('\uFEFF' + KEY + '\n'), { apiKey: KEY }, 'a file holding only the key is read');
202
+ t.equal(perplexityKeyFromText("PERPLEXITY_API_KEY='" + KEY + "'"), { apiKey: KEY }, 'quotes are taken off');
203
+ t.check(perplexityKeyFromText('OPENAI_API_KEY=' + OTHER_SECRET) === null &&
204
+ perplexityKeyFromText('PERPLEXITY_API_KEY=not-a-key') === null &&
205
+ perplexityKeyFromText('') === null, 'a file without a Perplexity key gives none (another key is never taken instead)');
206
+ const flowStore = new ServiceAccountStore(t.dir('web-flow'), SecretStore.memory());
207
+ const envFile = t.write(t.at('web-env', '.env'), env);
208
+ const imported = await importServiceFlow(fakeIO([true]).io, 'perplexity', envFile, {
209
+ store: flowStore,
210
+ pc: { localAppData: '', appData: '', programData: '' },
211
+ share: false,
212
+ });
213
+ const sealedFiles = allText(path.join(t.at('web-flow'), 'accounts')).join('\n');
214
+ t.check(flowStore.read('perplexity')?.apiKey === KEY &&
215
+ flowStore.info('perplexity')?.source === 'file' &&
216
+ flowStore.info('perplexity')?.summary === 'pplx-...WXYZ' &&
217
+ !sealedFiles.includes(KEY) &&
218
+ !sealedFiles.includes(OTHER_SECRET) &&
219
+ imported.message.includes('Share with sandboxes'), 'import --from .env saves only the key, sealed; the status shows pplx-... and its last 4 characters');
220
+ await t.throws(() => importServiceFlow(fakeIO([true]).io, 'perplexity', t.write(t.at('web-env', 'none.env'), 'A=1'), {
221
+ store: flowStore,
222
+ pc: { localAppData: '', appData: '', programData: '' },
223
+ share: false,
224
+ }), 'holds no Perplexity API key', 'a file without the key is refused');
225
+ const big = t.write(t.at('web-env', 'big.env'), '# pad\n'.repeat(4000) + 'PERPLEXITY_API_KEY=' + KEY);
226
+ await importServiceFlow(fakeIO([true]).io, 'perplexity', big, {
227
+ store: flowStore,
228
+ pc: { localAppData: '', appData: '', programData: '' },
229
+ share: false,
230
+ });
231
+ t.check(flowStore.read('perplexity')?.apiKey === KEY, 'a .env file bigger than the key itself is read (bounded)');
232
+ const noPc = { localAppData: '', appData: '', programData: '' };
233
+ let hkcuReads = 0;
234
+ const fromTerminal = await importServiceFlow(fakeIO([true]).io, 'perplexity', undefined, {
235
+ store: flowStore,
236
+ pc: {
237
+ ...noPc,
238
+ env: { PERPLEXITY_API_KEY: ' "' + KEY + '" ' },
239
+ userVariable: () => (hkcuReads++, 'pplx-unused0123456789'),
240
+ },
241
+ share: false,
242
+ });
243
+ t.check(flowStore.read('perplexity')?.apiKey === KEY &&
244
+ flowStore.info('perplexity')?.source === 'pc' &&
245
+ hkcuReads === 0 &&
246
+ !fromTerminal.message.includes(KEY), "import without --from takes this PC's PERPLEXITY_API_KEY (this terminal's first, unquoted; HKCU not read), never printing it");
247
+ const asked = [];
248
+ await importServiceFlow(fakeIO([true]).io, 'perplexity', undefined, {
249
+ store: flowStore,
250
+ pc: { ...noPc, env: {}, userVariable: (name) => (asked.push(name), '"' + KEY.replace('WXYZ', 'USER') + '"') },
251
+ share: false,
252
+ });
253
+ t.check(flowStore.read('perplexity')?.apiKey === KEY.replace('WXYZ', 'USER') && asked.join() === 'PERPLEXITY_API_KEY', "else your saved Windows user environment's (HKCU\\Environment)");
254
+ const none = await importServiceFlow(fakeIO([true]).io, 'perplexity', undefined, {
255
+ store: flowStore,
256
+ pc: { ...noPc, env: { PERPLEXITY_API_KEY: 'not-a-key' }, userVariable: () => null },
257
+ share: false,
258
+ });
259
+ t.check(none.message.includes('PERPLEXITY_API_KEY is not set'), 'no key on this PC: it says how else to save one');
260
+ t.equal([
261
+ parseRegValue('\r\nHKEY_CURRENT_USER\\Environment\r\n PERPLEXITY_API_KEY REG_SZ ' + KEY + '\r\n', 'PERPLEXITY_API_KEY'),
262
+ parseRegValue(' OTHER REG_SZ x', 'PERPLEXITY_API_KEY'),
263
+ ], [KEY, null], "reg.exe's answer is read for that one value only");
264
+ await t.throws(() => importServiceFlow(fakeIO([true]).io, 'comet', undefined, { store: flowStore, pc: noPc, share: false }), "can't be copied from this PC", "Comet import says why it can't be copied from this PC");
265
+ await t.throws(() => loginServiceFlow(fakeIO([]).io, 'comet', { store: flowStore, share: false }), 'Windows locks it to your PC account', 'and so does Comet login');
266
+ const typedIO = fakeIO([KEY]);
267
+ await loginServiceFlow(typedIO.io, 'perplexity', { store: flowStore, share: true });
268
+ const typedQuestion = JSON.stringify(typedIO.seen.questions);
269
+ t.check(flowStore.info('perplexity')?.source === 'typed' &&
270
+ typedIO.seen.questions.length === 1 &&
271
+ typedIO.seen.questions[0].kind === 'secret' &&
272
+ !typedQuestion.includes(KEY), 'login asks for the key once, hidden');
273
+ await t.throws(() => loginServiceFlow(fakeIO(['sk-nope']).io, 'perplexity', { store: flowStore, share: true }), '', 'a typed value that is not a Perplexity key is refused');
274
+ t.check(!HOST_SOURCES.comet.fromPc && !HOST_SOURCES.comet.fromFile && !HOST_SOURCES.comet.fromAnswers, "Comet's sign-in is never taken from this PC, a file or typing");
275
+ t.section('the key never sits in a file the sandbox keeps');
276
+ const config = f.makeConfig();
277
+ config.accounts.perplexity.share = true;
278
+ config.apps.install = ['perplexity-mcp'];
279
+ const run = await f.run({}, config);
280
+ const launched = generateSandbox({
281
+ packageRoot: t.packageRoot,
282
+ run,
283
+ harnessArgs: [],
284
+ probe: f.probe,
285
+ pcLanguages: [],
286
+ serviceAccounts: { perplexity: { apiKey: KEY } },
287
+ });
288
+ const runDir = new RunDir(run.dataPath);
289
+ const secretsFile = runDir.secretFile(launched.launchId);
290
+ t.check(!allText(launched.launchPath).some((text) => text.includes(KEY)) &&
291
+ !allText(runDir.root).some((text) => text.includes(KEY)), 'not in session.json, the launch folder or <data>\\run');
292
+ t.equal(f.readJson(secretsFile).serviceAccounts?.perplexity?.apiKey, KEY, 'only in the one-shot secrets file');
293
+ clearSecrets(config.storage.root, run.dataPath);
294
+ const local = t.dir('web-guest-local');
295
+ const oldLocal = process.env.LOCALAPPDATA;
296
+ process.env.LOCALAPPDATA = local;
297
+ try {
298
+ const guestRoot = t.dir('web-guest-accounts');
299
+ const given = await applyServiceAccounts({ perplexity: { apiKey: KEY } }, { root: guestRoot });
300
+ const applied = await perplexityAdapter.apply({ apiKey: KEY });
301
+ t.check(given.PERPLEXITY_API_KEY === KEY &&
302
+ Object.keys(applied.env ?? {}).join() === 'PERPLEXITY_API_KEY' &&
303
+ !applied.summary.includes(KEY) &&
304
+ !allText(guestRoot).some((text) => text.includes(KEY)) &&
305
+ allText(local).length === 0, 'in the sandbox it is only PERPLEXITY_API_KEY in memory: delivered.json and the profile never hold it');
306
+ t.section("Comet's sign-in in the sandbox");
307
+ const payload = {
308
+ cookies: [
309
+ { name: 'a', value: 'PlantedCookie', domain: '.perplexity.ai', path: '/', secure: true, httpOnly: true },
310
+ ],
311
+ };
312
+ const summary = (await cometAdapter.apply(payload)).summary;
313
+ const pending = cometPendingFile();
314
+ t.check(pending.startsWith(path.join(local, 'AgentSandbox', 'accounts', 'comet')) &&
315
+ fs.existsSync(pending) &&
316
+ !summary.includes('PlantedCookie'), "it waits for Comet in the sandbox's own accounts folder (wiped with the sandbox)");
317
+ t.equal(takePendingComet(), payload, 'the apps step takes it once Comet is there');
318
+ t.check(!fs.existsSync(pending) && takePendingComet() === null, 'and the file is gone');
319
+ fs.mkdirSync(path.dirname(pending), { recursive: true });
320
+ fs.writeFileSync(pending, JSON.stringify({ cookies: [{ ...payload.cookies[0], domain: 'bank.example' }] }));
321
+ t.check(takePendingComet() === null && !fs.existsSync(pending), 'one of the wrong shape is dropped');
322
+ t.check(cometAdapter.current?.() !== undefined, 'the watch can look at what Comet holds');
323
+ const args = cometLaunchArgs(0, 'C:\\P');
324
+ t.check(args.includes('--user-data-dir=C:\\P') &&
325
+ args.includes('--remote-debugging-port=0') &&
326
+ args.includes('--remote-debugging-address=127.0.0.1') &&
327
+ cometShortcutArgs(9222, 'C:\\With Space').startsWith('"--user-data-dir=C:\\With Space" --remote-debugging-port=9222'), "Comet starts on the sandbox's profile, DevTools on 127.0.0.1 only (any free port, 9222 for comet-mcp)");
328
+ }
329
+ finally {
330
+ if (oldLocal === undefined)
331
+ delete process.env.LOCALAPPDATA;
332
+ else
333
+ process.env.LOCALAPPDATA = oldLocal;
334
+ }
335
+ t.section('the cookie allow-list');
336
+ const jar = [
337
+ cdpCookie(),
338
+ cdpCookie({ name: 'pplx.visitor', domain: 'www.perplexity.ai', expires: NOW + DAY }),
339
+ cdpCookie({ name: 'bank', domain: '.bank.example' }),
340
+ cdpCookie({ name: 'look-alike', domain: 'perplexity.ai.evil.com' }),
341
+ cdpCookie({ name: 'script', httpOnly: false }),
342
+ cdpCookie({ name: 'tab', session: true, expires: -1 }),
343
+ cdpCookie({ name: 'old', expires: NOW - 1 }),
344
+ cdpCookie({ name: '__cf_bm' }),
345
+ cdpCookie({ name: 'third', partitionKey: { topLevelSite: 'https://x.example' } }),
346
+ 'not a cookie',
347
+ null,
348
+ ];
349
+ const kept = cometPayloadFromCookies(jar, NOW);
350
+ t.equal(kept?.cookies.map((c) => c.name), ['__Secure-next-auth.session-token', 'pplx.visitor'], "only perplexity.ai's own persistent HttpOnly cookies are kept (no other site, script, session, partitioned or Cloudflare cookie)");
351
+ t.check(kept?.cookies[0].expires === Math.floor((NOW + 30 * DAY + 1234.5) / 3600) * 3600 &&
352
+ kept.cookies[0].sameSite === 'Lax' &&
353
+ !('size' in kept.cookies[0]) &&
354
+ !('priority' in kept.cookies[0]), 'kept to the hour, only the fields setCookies needs');
355
+ t.equal(fingerprint(cometPayloadFromCookies([...jar].reverse(), NOW + 60)), fingerprint(kept), 'the same sign-in looks the same in any order and a minute later (no handoff for nothing)');
356
+ t.check(cometPayloadFromCookies([cdpCookie({ name: 'script', httpOnly: false })], NOW) === null &&
357
+ cometPayloadFromCookies(Array.from({ length: MAX_COMET_COOKIES + 1 }, (_, i) => cdpCookie({ name: 'n' + i })), NOW) === null, 'no sign-in cookie, or more than a sign-in could need: none');
358
+ t.equal(kept ? cookiesToRestore(kept, NOW + 2 * DAY).map((c) => c.name) : [], ['__Secure-next-auth.session-token'], 'only cookies still valid are written back');
359
+ t.section('several sandboxes hand Comet back');
360
+ const shared = f.makeConfig();
361
+ shared.accounts.comet.share = true;
362
+ const root = shared.storage.root;
363
+ const signIns = new ServiceAccountStore(root, SecretStore.memory());
364
+ const leases = new AccountLeases(root);
365
+ const runA = await f.run({}, shared);
366
+ const dataB = path.join(root, 'data', 'second');
367
+ const deliver = (dataPath, launchId) => prepareServiceAccounts({
368
+ config: shared,
369
+ others: [],
370
+ on: new Set(['comet']),
371
+ storageRoot: root,
372
+ dataPath,
373
+ launchId,
374
+ isRunning: () => true,
375
+ store: signIns,
376
+ leases,
377
+ });
378
+ const first = deliver(runA.dataPath, 'L-a');
379
+ const second = deliver(dataB, 'L-b');
380
+ t.check(first.leased.includes('comet') &&
381
+ second.leased.includes('comet') &&
382
+ !first.payloads.comet &&
383
+ first.notes.some((n) => n.includes('sign in to Comet once in this sandbox')) &&
384
+ !first.notes.some((n) => n.includes('in use by sandbox')), 'nothing saved yet: both sandboxes may sign in and hand it back, neither waits for the other');
385
+ const hand = (dataPath, launchId, value) => {
386
+ t.write(path.join(handoffDir(dataPath), 'comet.json'), JSON.stringify({
387
+ kind: 'comet',
388
+ launchId,
389
+ payload: { cookies: [{ ...payload0(), value }] },
390
+ }));
391
+ };
392
+ const payload0 = () => ({
393
+ name: 's',
394
+ domain: '.perplexity.ai',
395
+ path: '/',
396
+ secure: true,
397
+ httpOnly: true,
398
+ expires: NOW + DAY,
399
+ });
400
+ hand(runA.dataPath, 'L-a', 'from-a');
401
+ hand(dataB, 'L-b', 'from-b');
402
+ const tookA = takeHandoffs(runA.dataPath, false, signIns, leases);
403
+ const tookB = takeHandoffs(dataB, false, signIns, leases);
404
+ t.check(tookA.taken.includes('comet') &&
405
+ tookB.taken.includes('comet') &&
406
+ signIns.read('comet')?.cookies[0].value === 'from-b' &&
407
+ signIns.info('comet')?.summary === '1 perplexity.ai sign-in cookie', 'each sandbox hands its sign-in back; the last one is kept');
408
+ hand(runA.dataPath, 'L-old', 'stale');
409
+ t.check(takeHandoffs(runA.dataPath, true, signIns, leases).dropped.includes('comet') &&
410
+ signIns.read('comet')?.cookies[0].value === 'from-b' &&
411
+ leases.leaseOf('comet', path.basename(runA.dataPath)) === null &&
412
+ leases.leaseOf('comet', 'second') !== null, "one from an older launch is dropped; a sandbox's last sync lets go of its own lease only");
413
+ t.check(deliver(runA.dataPath, 'L-c').payloads.comet?.cookies[0].value === 'from-b', 'the next sandbox gets the newest sign-in');
414
+ t.section('the watch asks Comet first');
415
+ const watchRoot = t.dir('web-watch');
416
+ const fake = { held: null, asked: 0 };
417
+ const fakeComet = {
418
+ kind: 'comet',
419
+ apply: () => ({ summary: 'fake' }),
420
+ refresh: async () => {
421
+ fake.asked++;
422
+ fake.held = { cookies: [{ ...payload0(), value: 'renewed' }] };
423
+ await Promise.resolve();
424
+ },
425
+ current: () => fake.held,
426
+ };
427
+ await applyServiceAccounts({ comet: { cookies: [{ ...payload0(), value: 'given' }] } }, { leased: ['comet'], root: watchRoot, adapters: { comet: fakeComet } });
428
+ const guestData = t.dir('web-watch-data');
429
+ const watch = new AccountWatch(['comet'], {
430
+ dataDir: guestData,
431
+ launchId: 'L-w',
432
+ root: watchRoot,
433
+ adapters: { comet: fakeComet },
434
+ });
435
+ await watch.flush(true);
436
+ const back = path.join(guestData, 'state', 'accounts', 'comet.json');
437
+ t.check(fake.asked === 1 &&
438
+ fs.existsSync(back) &&
439
+ f.readJson(back).payload.cookies[0].value === 'renewed', 'a flush waits for the asynchronous read, then hands back what changed');
440
+ t.section('the DevTools client');
441
+ t.equal(parseDevToolsActivePort('56789\n/devtools/browser/0b1c2d3e-aaaa-bbbb-cccc-0123456789ab\n'), { port: 56789, path: '/devtools/browser/0b1c2d3e-aaaa-bbbb-cccc-0123456789ab' }, 'DevToolsActivePort: the port and the browser target');
442
+ t.check(parseDevToolsActivePort('') === null &&
443
+ parseDevToolsActivePort('70000\n/devtools/browser/x') === null &&
444
+ parseDevToolsActivePort('9222\n/devtools/page/x') === null &&
445
+ parseDevToolsActivePort('9222\n/devtools/browser/../../etc') === null &&
446
+ parseDevToolsActivePort('9222 \n/devtools/browser/a b') === null, 'anything else in that file is refused');
447
+ const roundTrip = (n) => {
448
+ const payload = Buffer.alloc(n, 7);
449
+ const frames = new FrameReader(MAX_MESSAGE_BYTES, true).push(encodeFrame(OPCODE.text, payload));
450
+ return frames.length === 1 && frames[0].fin && frames[0].payload.equals(payload);
451
+ };
452
+ t.check(roundTrip(0) && roundTrip(125) && roundTrip(126) && roundTrip(65535) && roundTrip(70000), 'frames of every length size round-trip, masked');
453
+ const refuses = (bytes, max = MAX_MESSAGE_BYTES) => {
454
+ try {
455
+ new FrameReader(max).push(bytes);
456
+ return false;
457
+ }
458
+ catch {
459
+ return true;
460
+ }
461
+ };
462
+ t.check(refuses(encodeFrame(OPCODE.text, Buffer.from('x'))) &&
463
+ refuses(Buffer.from([0xc1, 0x00])) &&
464
+ refuses(Buffer.from([0x83, 0x00])) &&
465
+ refuses(unfinished(OPCODE.ping, Buffer.alloc(1))) &&
466
+ refuses(Buffer.from([0x81, 127, 0, 0, 0, 1, 0, 0, 0, 0])) &&
467
+ refuses(encodeFrame(OPCODE.text, Buffer.alloc(2000), null), 1000), 'a masked or reserved-bit frame, an unknown type, a split control frame and an oversized frame are refused');
468
+ t.check(new FrameReader().push(encodeFrame(OPCODE.text, Buffer.from('hello'), null).subarray(0, 4)).length === 0, 'a frame cut short waits for the rest');
469
+ await t.throws(() => Promise.resolve(readHandshake(Buffer.from('HTTP/1.1 403 Forbidden\r\n\r\n'), 'k')), 'refused', 'a handshake that is not a switch to WebSocket is refused');
470
+ const browser = fakeBrowser((call) => {
471
+ if (call.method === 'Target.getTargets') {
472
+ const body = Buffer.from(JSON.stringify({ id: call.id, result: { targetInfos: [{ targetId: 'T1', type: 'page' }] } }));
473
+ // One message in two frames: a text frame that is not the last, then its continuation.
474
+ return [
475
+ unfinished(OPCODE.text, body.subarray(0, 10)),
476
+ encodeFrame(OPCODE.continuation, body.subarray(10), null),
477
+ ];
478
+ }
479
+ if (call.method === 'Fail.me')
480
+ return [
481
+ encodeFrame(OPCODE.text, Buffer.from(JSON.stringify({ id: call.id, error: { message: 'nope' } })), null),
482
+ ];
483
+ const event = Buffer.from(JSON.stringify({ method: 'Target.targetCreated', params: {} }));
484
+ const result = Buffer.from(JSON.stringify({ id: call.id, result: { echoed: call.sessionId ?? null } }));
485
+ return [encodeFrame(OPCODE.text, event, null), encodeFrame(OPCODE.text, result, null)];
486
+ });
487
+ const cdp = await CdpConnection.over(browser.socket, ENDPOINT, 3000);
488
+ const targets = await cdp.call('Target.getTargets');
489
+ const echoed = await cdp.call('Network.getCookies', { urls: [] }, { sessionId: 'S1' });
490
+ t.check(targets.targetInfos[0].targetId === 'T1' &&
491
+ echoed.echoed === 'S1' &&
492
+ browser.seen.map((s) => s.method).join() === 'Target.getTargets,Network.getCookies' &&
493
+ browser.seen[1].sessionId === 'S1', 'calls get their answers (split frames, byte by byte, events in between, a page session)');
494
+ t.check(browser.pongs.length === 1 && browser.pongs[0].toString() === 'hi', 'a ping is answered with its pong');
495
+ await t.throws(() => cdp.call('Fail.me'), 'nope', "the browser's error is the call's error");
496
+ cdp.close();
497
+ await t.throws(() => cdp.call('Target.getTargets'), 'closed', 'a closed connection takes no more calls');
498
+ const impostor = fakeBrowser(() => [], { badAccept: true });
499
+ await t.throws(() => CdpConnection.over(impostor.socket, ENDPOINT, 3000), 'does not match', 'a server that does not answer the WebSocket key is refused');
500
+ t.section("Codex's config.toml");
501
+ t.check(DEFAULTS.harnesses.codex.home?.loginFiles.includes('.credentials.json') === true &&
502
+ DEFAULTS.harnesses.codex.home?.loginFiles.includes('auth.json') === true, "Codex's saved logins carry its MCP sign-ins (.credentials.json) beside auth.json");
503
+ const knownCodex = APPS.flatMap((a) => (a.mcp?.codex ? [a.mcp.name] : []));
504
+ const perplexityEntry = APPS.find((a) => a.id === 'perplexity-mcp');
505
+ const computerEntry = APPS.find((a) => a.id === 'perplexity-computer-mcp');
506
+ const pplxServer = addonMcpServer('C:\\AgentData\\tools\\apps\\perplexity-mcp', perplexityEntry.mcp);
507
+ const computerServer = addonMcpServer('C:\\AgentData\\tools\\apps\\perplexity-computer-mcp', computerEntry.mcp);
508
+ const mine = '[mcp_servers.mine]\ncommand = "mine.exe"\n\n[mcp_servers.sbx-perplexity]\ncommand = "old"\n[mcp_servers."sbx-perplexity".env]\nX = "1"\n\n[profiles.p]\nmodel = "o3"\n';
509
+ const added = codexWithAddonMcpServers(mine, {
510
+ 'sbx-perplexity': { ...pplxServer, envVars: ['PERPLEXITY_API_KEY'] },
511
+ 'sbx-perplexity-computer': computerServer,
512
+ }, knownCodex);
513
+ t.check(added.changed &&
514
+ added.toml.includes('[mcp_servers.mine]\ncommand = "mine.exe"') &&
515
+ added.toml.includes('[profiles.p]\nmodel = "o3"') &&
516
+ !added.toml.includes('command = "old"') &&
517
+ !added.toml.includes('X = "1"') &&
518
+ added.toml.includes('env_vars = ["PERPLEXITY_API_KEY"]') &&
519
+ added.toml.includes('args = ["C:\\\\AgentData\\\\tools\\\\apps\\\\perplexity-mcp\\\\node_modules\\\\@perplexity-ai\\\\mcp-server\\\\dist\\\\index.js"]') &&
520
+ added.toml.includes('[mcp_servers.sbx-perplexity-computer]\n# Set by the sandbox') &&
521
+ added.toml.includes('url = "' + PERPLEXITY_COMPUTER_MCP_URL + '"') &&
522
+ !added.toml.includes(KEY), 'the add-ons go in as [mcp_servers.sbx-...] (an old copy replaced, your servers and profiles kept, no key)');
523
+ t.check(!codexWithAddonMcpServers(added.toml, {
524
+ 'sbx-perplexity': { ...pplxServer, envVars: ['PERPLEXITY_API_KEY'] },
525
+ 'sbx-perplexity-computer': computerServer,
526
+ }, knownCodex).changed, 'writing the same again changes nothing');
527
+ const removed = codexWithAddonMcpServers(added.toml, {}, knownCodex);
528
+ t.check(removed.changed &&
529
+ !removed.toml.includes('sbx-') &&
530
+ removed.toml.includes('[mcp_servers.mine]') &&
531
+ removed.toml.includes('[profiles.p]'), 'turned off: their tables are removed, nothing else');
532
+ const inline = '[mcp_servers]\nsbx-perplexity = { command = "mine" }\n';
533
+ const clash = codexWithAddonMcpServers(inline, { 'sbx-perplexity': pplxServer }, knownCodex);
534
+ t.check(clash.skipped.join() === 'sbx-perplexity' && !clash.changed, 'a name used some other way in the file is left out (never the same key twice)');
535
+ t.check(!codexWithAddonMcpServers('', {}, knownCodex).changed &&
536
+ !codexWithAddonMcpServers('model = "o3"\r\n', {}, knownCodex).changed, 'nothing on and nothing there: the file is not touched');
537
+ t.section('the catalogue entries');
538
+ const ids = [
539
+ 'perplexity-mcp',
540
+ 'perplexity-computer-mcp',
541
+ 'comet',
542
+ 'comet-mcp',
543
+ 'chrome-devtools-mcp',
544
+ 'browser-use-mcp',
545
+ ];
546
+ t.equal(AGENTS_WEB_APPS.map((a) => a.id), ids, 'the six entries');
547
+ for (const entry of AGENTS_WEB_APPS) {
548
+ t.check(APPS.filter((a) => a.id === entry.id).length === 1 &&
549
+ entry.userOnly === true &&
550
+ ['agents', 'browsers'].includes(entry.category) &&
551
+ !LocalOverridesSchema.safeParse({ apps: { install: [entry.id] } }).success &&
552
+ ConfigSchema.shape.apps.shape.install.safeParse([entry.id]).success &&
553
+ (!entry.mcp || (entry.mcp.name.startsWith('sbx-') && entry.service === true)), entry.id + ': listed once, yours only (a project cannot turn it on); an MCP server is sbx-... and a service');
554
+ const source = entry.source;
555
+ if ('locked' in source) {
556
+ const lockDir = path.join(t.packageRoot, 'sandbox', 'addons', entry.id);
557
+ if (source.locked === 'npm') {
558
+ const manifest = JSON.parse(fs.readFileSync(path.join(lockDir, 'package.json'), 'utf8'));
559
+ const lock = JSON.parse(fs.readFileSync(path.join(lockDir, 'package-lock.json'), 'utf8'));
560
+ const deps = Object.entries(lock.packages).filter(([k]) => k !== '');
561
+ t.check(manifest.dependencies[source.package] === source.version &&
562
+ Object.keys(manifest.dependencies).length === 1 &&
563
+ lock.lockfileVersion === 3 &&
564
+ lock.packages['node_modules/' + source.package]?.version === source.version &&
565
+ deps.every(([, p]) => !p.link &&
566
+ (p.resolved ?? '').startsWith('https://registry.npmjs.org/') &&
567
+ /^sha512-[A-Za-z0-9+/]+=*$/.test(p.integrity ?? '')) &&
568
+ (!entry.mcp?.script || lock.packages['node_modules/' + source.package] !== undefined), entry.id +
569
+ ': its lock file pins ' +
570
+ source.package +
571
+ ' ' +
572
+ source.version +
573
+ ', every package from npm with a sha512');
574
+ }
575
+ else {
576
+ const lines = fs.readFileSync(path.join(lockDir, 'requirements.txt'), 'utf8').split(/\r?\n/);
577
+ const pins = lines.filter((l) => /^[A-Za-z0-9]/.test(l));
578
+ t.check(pins.includes(source.package + '==' + source.version + ' \\') &&
579
+ pins.every((l) => /^[A-Za-z0-9._-]+(\[[^\]]+\])?==[^\s]+ \\$/.test(l)) &&
580
+ lines
581
+ .join('\n')
582
+ .split(/\n(?=[A-Za-z0-9])/)
583
+ .slice(1)
584
+ .every((b) => /--hash=sha256:[0-9a-f]{64}/.test(b)) &&
585
+ !lines.some((l) => /^\s*(-e|--index-url|--extra-index-url|-f|--find-links|git\+|https?:)/.test(l.trim())), entry.id + ': its requirements.txt pins every package with SHA-256 hashes, from PyPI only');
586
+ }
587
+ }
588
+ }
589
+ const cometEntry = APPS.find((a) => a.id === 'comet');
590
+ t.check(cometEntry.unpack === 'chromium' &&
591
+ cometEntry.needs?.includes('7zip') === true &&
592
+ cometEntry.signer === COMET_SIGNER &&
593
+ 'resolve' in cometEntry.source &&
594
+ appsToInstall({ install: ['comet'], custom: {} }, 'full').entries[0].id === '7zip', 'comet: unpacked with 7-Zip (brought first), its signer checked');
595
+ t.check('remote' in computerEntry.source &&
596
+ computerServer.type === 'http' &&
597
+ computerServer.type === 'http' &&
598
+ PERPLEXITY_COMPUTER_MCP_URL.startsWith('https://www.perplexity.ai/') &&
599
+ pplxServer.type === 'stdio' &&
600
+ Object.keys(pplxServer.env).length === 0 &&
601
+ perplexityEntry.mcp?.envVars?.join() === 'PERPLEXITY_API_KEY', 'Perplexity Computer is a remote HTTP server (nothing installed); the API server reads its key from the environment only');
602
+ const browserUse = APPS.find((a) => a.id === 'browser-use-mcp');
603
+ const buServer = addonMcpServer('C:\\x', browserUse.mcp, { exe: 'C:\\x\\venv\\Scripts\\browser-use.exe' });
604
+ t.check(addonMcpServer('C:\\x', browserUse.mcp) === null &&
605
+ buServer?.type === 'stdio' &&
606
+ buServer.command === 'C:\\x\\venv\\Scripts\\browser-use.exe' &&
607
+ buServer.args.join() === '--mcp' &&
608
+ buServer.env.ANONYMIZED_TELEMETRY === 'false' &&
609
+ appsNeedUv({ install: ['browser-use-mcp'], custom: {} }), 'browser-use: its own program runs the server, telemetry off; it brings the uv step');
610
+ const devtools = addonMcpServer('C:\\y', APPS.find((a) => a.id === 'chrome-devtools-mcp').mcp);
611
+ t.check(devtools?.type === 'stdio' &&
612
+ devtools.args.includes('--isolated') &&
613
+ devtools.args.includes('--no-usage-statistics') &&
614
+ devtools.args.some((a) => a.endsWith('msedge.exe')), 'chrome-devtools-mcp: Edge with a throwaway profile, no usage statistics');
615
+ const withPath = addonMcpServer('C:\\z', APPS.find((a) => a.id === 'comet-mcp').mcp, {
616
+ env: { COMET_PATH: 'C:\\comet\\comet.exe' },
617
+ });
618
+ t.check(withPath?.type === 'stdio' && withPath.env.COMET_PATH === 'C:\\comet\\comet.exe', 'comet-mcp is told where Comet is');
619
+ t.section("Comet's signer");
620
+ const signed = {
621
+ subject: 'CN="PERPLEXITY AI, INC.", O="PERPLEXITY AI, INC.", L=San Francisco, S=California, C=US, SERIALNUMBER=1234, OID.2.5.4.15=Private Organization',
622
+ issuer: 'CN=GlobalSign GCC R45 EV CodeSigning CA 2020, O=GlobalSign nv-sa, C=BE',
623
+ };
624
+ t.check(signerMatches(signed, COMET_SIGNER), 'Perplexity AI, Inc. with its GlobalSign EV certificate passes');
625
+ t.check(!signerMatches(null, COMET_SIGNER) &&
626
+ !signerMatches({ ...signed, subject: signed.subject.replace('O="PERPLEXITY AI, INC."', 'O=Someone Else') }, COMET_SIGNER) &&
627
+ !signerMatches({ ...signed, subject: 'CN="PERPLEXITY AI, INC. EVIL", O="PERPLEXITY AI, INC. EVIL"' }, COMET_SIGNER) &&
628
+ !signerMatches({ ...signed, issuer: 'CN=Some Other CA, O=X' }, COMET_SIGNER) &&
629
+ !signerMatches({ subject: '', issuer: signed.issuer }, COMET_SIGNER), 'unsigned, another organisation, a look-alike name or another CA is refused');
630
+ t.section("unpacking Comet's installer");
631
+ const stage = t.dir('web-unpack');
632
+ t.write(path.join(stage, 'a', 'UPDATER.PACKED.7Z'), 'x');
633
+ t.write(path.join(stage, 'b', 'mini_installer.exe'), 'x');
634
+ t.check(path.basename(nextInner(stage) ?? '') === 'mini_installer.exe', 'the latest stage found is opened next');
635
+ t.write(path.join(stage, 'c', 'chrome.7z'), 'x');
636
+ t.check(path.basename(nextInner(stage) ?? '') === 'chrome.7z', 'down to chrome.7z');
637
+ t.check(chromiumBin(stage, 'comet.exe') === null, 'no Chrome-bin yet');
638
+ fs.mkdirSync(path.join(stage, 'd', 'Chrome-bin'), { recursive: true });
639
+ t.check(chromiumBin(stage, 'comet.exe') === null, 'a Chrome-bin without the browser does not count');
640
+ t.write(path.join(stage, 'd', 'Chrome-bin', 'comet.exe'), 'x');
641
+ t.equal(chromiumBin(stage, 'comet.exe'), path.join(stage, 'd', 'Chrome-bin'), 'the browser folder is found');
642
+ const appDir = t.dir('web-kept');
643
+ t.check(keptInstaller(appDir) === null, 'no installer kept when it was unpacked');
644
+ t.write(path.join(appDir, 'installer', 'comet_installer.exe'), 'x');
645
+ t.equal(keptInstaller(appDir), path.join(appDir, 'installer', 'comet_installer.exe'), 'a kept installer is found');
646
+ },
647
+ };