openmausbot 0.1.55

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 (499) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +14 -0
  3. package/cli.js +2 -0
  4. package/dist/app-icon.svg +29 -0
  5. package/dist/assets/abap-BdImnpbu.js +1 -0
  6. package/dist/assets/actionscript-3-B3316cI-.js +1 -0
  7. package/dist/assets/ada-bCR0ucgS.js +1 -0
  8. package/dist/assets/ahk-CsyLZFj1.js +1 -0
  9. package/dist/assets/ahk2-8Zs4aa1G.js +1 -0
  10. package/dist/assets/andromeeda-C4gqWexZ.js +1 -0
  11. package/dist/assets/angular-html-DA-rfuFy.js +1 -0
  12. package/dist/assets/angular-ts-BrjP3tb8.js +1 -0
  13. package/dist/assets/apache-Pmp26Uib.js +1 -0
  14. package/dist/assets/apex-DhZFqWV2.js +1 -0
  15. package/dist/assets/apl-CORt7UWP.js +1 -0
  16. package/dist/assets/applescript-Co6uUVPk.js +1 -0
  17. package/dist/assets/ara-BRHolxvo.js +1 -0
  18. package/dist/assets/asciidoc-CSVQ5wI8.js +1 -0
  19. package/dist/assets/asm-D_Q5rh1f.js +1 -0
  20. package/dist/assets/astro-HNnZUWAn.js +1 -0
  21. package/dist/assets/aurora-x-D-2ljcwZ.js +1 -0
  22. package/dist/assets/awk-DMzUqQB5.js +1 -0
  23. package/dist/assets/ayu-dark-DYE7WIF3.js +1 -0
  24. package/dist/assets/ayu-light-BA47KaF1.js +1 -0
  25. package/dist/assets/ayu-mirage-32ctXXKs.js +1 -0
  26. package/dist/assets/ballerina-BFfxhgS-.js +1 -0
  27. package/dist/assets/bat-CickPsom.js +1 -0
  28. package/dist/assets/beancount-k_qm7-4y.js +1 -0
  29. package/dist/assets/berry-uYugtg8r.js +1 -0
  30. package/dist/assets/bibtex-CHM0blh-.js +1 -0
  31. package/dist/assets/bicep-Bmn6On1c.js +1 -0
  32. package/dist/assets/bird2-Bx8U0n9b.js +1 -0
  33. package/dist/assets/blade-2xfisSek.js +1 -0
  34. package/dist/assets/bsl-DlhNcFeZ.js +1 -0
  35. package/dist/assets/c-BIGW1oBm.js +1 -0
  36. package/dist/assets/c3-Dp5svz6Z.js +1 -0
  37. package/dist/assets/cadence-Bv_4Rxtq.js +1 -0
  38. package/dist/assets/cairo-KRGpt6FW.js +1 -0
  39. package/dist/assets/catppuccin-frappe-CZL1YF0i.js +1 -0
  40. package/dist/assets/catppuccin-latte-DH-8KZSZ.js +1 -0
  41. package/dist/assets/catppuccin-macchiato-B7yYVSCf.js +1 -0
  42. package/dist/assets/catppuccin-mocha-Ct7hS0mc.js +1 -0
  43. package/dist/assets/chapel-DTp_pixX.js +1 -0
  44. package/dist/assets/clarity-Dn5IMItf.js +1 -0
  45. package/dist/assets/clojure-P80f7IUj.js +1 -0
  46. package/dist/assets/cmake-D1j8_8rp.js +1 -0
  47. package/dist/assets/cobol-nBiQ_Alo.js +1 -0
  48. package/dist/assets/codeowners-Bp6g37R7.js +1 -0
  49. package/dist/assets/codeql-DsOJ9woJ.js +1 -0
  50. package/dist/assets/coffee-Ch7k5sss.js +1 -0
  51. package/dist/assets/common-lisp-Cg-RD9OK.js +1 -0
  52. package/dist/assets/coq-C7JzOVbR.js +1 -0
  53. package/dist/assets/cpp-BMRokrvK.js +1 -0
  54. package/dist/assets/crystal-DGywbUpC.js +1 -0
  55. package/dist/assets/csharp-DSvCPggb.js +1 -0
  56. package/dist/assets/css-CLj8gQPS.js +1 -0
  57. package/dist/assets/csv-fuZLfV_i.js +1 -0
  58. package/dist/assets/cue-D82EKSYY.js +1 -0
  59. package/dist/assets/cypher-COkxafJQ.js +1 -0
  60. package/dist/assets/d-85-TOEBH.js +1 -0
  61. package/dist/assets/dark-plus-C3mMm8J8.js +1 -0
  62. package/dist/assets/dart-bE4Kk8sk.js +1 -0
  63. package/dist/assets/dax-CEL-wOlO.js +1 -0
  64. package/dist/assets/desktop-BmXAJ9_W.js +1 -0
  65. package/dist/assets/diff-D97Zzqfu.js +1 -0
  66. package/dist/assets/docker-BcOcwvcX.js +1 -0
  67. package/dist/assets/dotenv-Da5cRb03.js +1 -0
  68. package/dist/assets/dracula-BzJJZx-M.js +1 -0
  69. package/dist/assets/dracula-soft-BXkSAIEj.js +1 -0
  70. package/dist/assets/dream-maker-BtqSS_iP.js +1 -0
  71. package/dist/assets/edge-FbVlp4U3.js +1 -0
  72. package/dist/assets/elixir-CkH2-t6x.js +1 -0
  73. package/dist/assets/elm-DbKCFpqz.js +1 -0
  74. package/dist/assets/emacs-lisp-C_m_b--Z.js +1 -0
  75. package/dist/assets/erb-DXfck5VN.js +1 -0
  76. package/dist/assets/erlang-DsQrWhSR.js +1 -0
  77. package/dist/assets/everforest-dark-BgDCqdQA.js +1 -0
  78. package/dist/assets/everforest-light-C8M2exoo.js +1 -0
  79. package/dist/assets/fennel-BYunw83y.js +1 -0
  80. package/dist/assets/fish-BvzEVeQv.js +1 -0
  81. package/dist/assets/fluent-C4IJs8-o.js +1 -0
  82. package/dist/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  83. package/dist/assets/fortran-free-form-BxgE0vQu.js +1 -0
  84. package/dist/assets/fsharp-CXgrBDvD.js +1 -0
  85. package/dist/assets/gdresource-TyuKm33G.js +1 -0
  86. package/dist/assets/gdscript-DqcFQ5yU.js +1 -0
  87. package/dist/assets/gdshader-DkwncUOv.js +1 -0
  88. package/dist/assets/genie-D0YGMca9.js +1 -0
  89. package/dist/assets/gherkin-DyxjwDmM.js +1 -0
  90. package/dist/assets/git-commit-F4YmCXRG.js +1 -0
  91. package/dist/assets/git-rebase-r7XF79zn.js +1 -0
  92. package/dist/assets/github-dark-DHJKELXO.js +1 -0
  93. package/dist/assets/github-dark-default-Cuk6v7N8.js +1 -0
  94. package/dist/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  95. package/dist/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  96. package/dist/assets/github-light-DAi9KRSo.js +1 -0
  97. package/dist/assets/github-light-default-D7oLnXFd.js +1 -0
  98. package/dist/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  99. package/dist/assets/gleam-BspZqrRM.js +1 -0
  100. package/dist/assets/glimmer-js-ByusRIyA.js +1 -0
  101. package/dist/assets/glimmer-ts-BfAWNZQY.js +1 -0
  102. package/dist/assets/glsl-DplSGwfg.js +1 -0
  103. package/dist/assets/gn-n2N0HUVH.js +1 -0
  104. package/dist/assets/gnuplot-DdkO51Og.js +1 -0
  105. package/dist/assets/go-C27-OAKa.js +1 -0
  106. package/dist/assets/graphql-ChdNCCLP.js +1 -0
  107. package/dist/assets/groovy-gcz8RCvz.js +1 -0
  108. package/dist/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  109. package/dist/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  110. package/dist/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  111. package/dist/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  112. package/dist/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  113. package/dist/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  114. package/dist/assets/hack-BWmVpMyf.js +1 -0
  115. package/dist/assets/haml-D5jkg6IW.js +1 -0
  116. package/dist/assets/handlebars-BpdQsYii.js +1 -0
  117. package/dist/assets/haskell-Df6bDoY_.js +1 -0
  118. package/dist/assets/haxe-CfZj7gIn.js +1 -0
  119. package/dist/assets/hcl-BWvSN4gD.js +1 -0
  120. package/dist/assets/hjson-D5-asLiD.js +1 -0
  121. package/dist/assets/hlsl-D3lLCCz7.js +1 -0
  122. package/dist/assets/horizon-BUw7H-hv.js +1 -0
  123. package/dist/assets/horizon-bright-CUuTKBJd.js +1 -0
  124. package/dist/assets/houston-DnULxvSX.js +1 -0
  125. package/dist/assets/html-derivative-DlHx6ybY.js +1 -0
  126. package/dist/assets/html-pp8916En.js +1 -0
  127. package/dist/assets/http-jrhK8wxY.js +1 -0
  128. package/dist/assets/hurl-irOxFIW8.js +1 -0
  129. package/dist/assets/hxml-2-FPmUDs.js +1 -0
  130. package/dist/assets/hy-DFXneXwc.js +1 -0
  131. package/dist/assets/imba-DGztddWO.js +1 -0
  132. package/dist/assets/index-B9rLIY81.js +263 -0
  133. package/dist/assets/index-CJBrtDbC.css +1 -0
  134. package/dist/assets/index-D-u0LYsV.js +153 -0
  135. package/dist/assets/ini-BEwlwnbL.js +1 -0
  136. package/dist/assets/java-CylS5w8V.js +1 -0
  137. package/dist/assets/javascript-wDzz0qaB.js +1 -0
  138. package/dist/assets/jinja-f2NsQr07.js +1 -0
  139. package/dist/assets/jison-wvAkD_A8.js +1 -0
  140. package/dist/assets/json-Cp-IABpG.js +1 -0
  141. package/dist/assets/json5-C9tS-k6U.js +1 -0
  142. package/dist/assets/jsonc-Des-eS-w.js +1 -0
  143. package/dist/assets/jsonl-DcaNXYhu.js +1 -0
  144. package/dist/assets/jsonnet-DFQXde-d.js +1 -0
  145. package/dist/assets/jssm-C2t-YnRu.js +1 -0
  146. package/dist/assets/jsx-g9-lgVsj.js +1 -0
  147. package/dist/assets/julia-5Bft2YPA.js +1 -0
  148. package/dist/assets/just-Cwhn7H3k.js +1 -0
  149. package/dist/assets/kanagawa-dragon-BuwD2xS4.js +1 -0
  150. package/dist/assets/kanagawa-lotus-C3DzagqV.js +1 -0
  151. package/dist/assets/kanagawa-wave-DFGoQZhC.js +1 -0
  152. package/dist/assets/kdl-DV7GczEv.js +1 -0
  153. package/dist/assets/kotlin-BdnUsdx6.js +1 -0
  154. package/dist/assets/kusto-wEQ09or8.js +1 -0
  155. package/dist/assets/laserwave-DUszq2jm.js +1 -0
  156. package/dist/assets/latex-D5pSuvFb.js +1 -0
  157. package/dist/assets/lean-BZvkOJ9d.js +1 -0
  158. package/dist/assets/less-B1dDrJ26.js +1 -0
  159. package/dist/assets/light-plus-B7mTdjB0.js +1 -0
  160. package/dist/assets/liquid-C0sCDyMI.js +1 -0
  161. package/dist/assets/llvm-BZoOZj88.js +1 -0
  162. package/dist/assets/log-2UxHyX5q.js +1 -0
  163. package/dist/assets/logo-BtOb2qkB.js +1 -0
  164. package/dist/assets/lua-BaeVxFsk.js +1 -0
  165. package/dist/assets/luau-BnpPk5vE.js +1 -0
  166. package/dist/assets/make-CHLpvVh8.js +1 -0
  167. package/dist/assets/markdown-Cvjx9yec.js +1 -0
  168. package/dist/assets/marko-DjSrsDqO.js +1 -0
  169. package/dist/assets/material-theme-D5KoaKCx.js +1 -0
  170. package/dist/assets/material-theme-darker-BfHTSMKl.js +1 -0
  171. package/dist/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  172. package/dist/assets/material-theme-ocean-CyktbL80.js +1 -0
  173. package/dist/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  174. package/dist/assets/matlab-D7o27uSR.js +1 -0
  175. package/dist/assets/mdc-D1_yUvq7.js +1 -0
  176. package/dist/assets/mdx-Cmh6b_Ma.js +1 -0
  177. package/dist/assets/mermaid-CQcHuHx7.js +1 -0
  178. package/dist/assets/min-dark-CafNBF8u.js +1 -0
  179. package/dist/assets/min-light-CTRr51gU.js +1 -0
  180. package/dist/assets/mipsasm-CKIfxQSi.js +1 -0
  181. package/dist/assets/mojo-DJz3ZmWd.js +1 -0
  182. package/dist/assets/monokai-BRVnQi9A.js +1 -0
  183. package/dist/assets/moonbit-CHtswR0a.js +1 -0
  184. package/dist/assets/move-el3G9tDJ.js +1 -0
  185. package/dist/assets/narrat-DRg8JJMk.js +1 -0
  186. package/dist/assets/nextflow-C-mBbutL.js +1 -0
  187. package/dist/assets/nextflow-groovy-vE_lwT2v.js +1 -0
  188. package/dist/assets/nginx-BpAMiNFr.js +1 -0
  189. package/dist/assets/night-owl-C39BiMTA.js +1 -0
  190. package/dist/assets/night-owl-light-CMTm3GFP.js +1 -0
  191. package/dist/assets/nim-BIad80T-.js +1 -0
  192. package/dist/assets/nix-CwoSXNpI.js +1 -0
  193. package/dist/assets/nord-Ddv68eIx.js +1 -0
  194. package/dist/assets/nsis-BlV79W_Q.js +1 -0
  195. package/dist/assets/nushell-D3jzshHO.js +1 -0
  196. package/dist/assets/objective-c-DXmwc3jG.js +1 -0
  197. package/dist/assets/objective-cpp-CLxacb5B.js +1 -0
  198. package/dist/assets/ocaml-C0hk2d4L.js +1 -0
  199. package/dist/assets/odin-BBf5iR-q.js +1 -0
  200. package/dist/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  201. package/dist/assets/one-light-C3Wv6jpd.js +1 -0
  202. package/dist/assets/openscad-C4EeE6gA.js +1 -0
  203. package/dist/assets/org-DM6o9KBp.js +1 -0
  204. package/dist/assets/pascal-D93ZcfNL.js +1 -0
  205. package/dist/assets/perl-B9cMNwum.js +1 -0
  206. package/dist/assets/php-Csjmro_R.js +1 -0
  207. package/dist/assets/pkl-u5AG7uiY.js +1 -0
  208. package/dist/assets/plastic-3e1v2bzS.js +1 -0
  209. package/dist/assets/plsql-ChMvpjG-.js +1 -0
  210. package/dist/assets/po-BTJTHyun.js +1 -0
  211. package/dist/assets/poimandres-CS3Unz2-.js +1 -0
  212. package/dist/assets/polar-C0HS_06l.js +1 -0
  213. package/dist/assets/postcss-CXtECtnM.js +1 -0
  214. package/dist/assets/powerquery-CEu0bR-o.js +1 -0
  215. package/dist/assets/powershell-BmBUJMz7.js +1 -0
  216. package/dist/assets/prisma-Vru482bI.js +1 -0
  217. package/dist/assets/prolog-CbFg5uaA.js +1 -0
  218. package/dist/assets/proto-C7zT0LnQ.js +1 -0
  219. package/dist/assets/pug-DKIMFp6K.js +1 -0
  220. package/dist/assets/puppet-BMWR74SV.js +1 -0
  221. package/dist/assets/purescript-CklMAg4u.js +1 -0
  222. package/dist/assets/python-B6aJPvgy.js +1 -0
  223. package/dist/assets/qml-3beO22l8.js +1 -0
  224. package/dist/assets/qmldir-C8lEn-DE.js +1 -0
  225. package/dist/assets/qss-IeuSbFQv.js +1 -0
  226. package/dist/assets/r-Cf5RLm7j.js +1 -0
  227. package/dist/assets/racket-BqYA7rlc.js +1 -0
  228. package/dist/assets/raku-DXvB9xmW.js +1 -0
  229. package/dist/assets/razor-BjBPvh-w.js +1 -0
  230. package/dist/assets/rbs-CpoqiR4B.js +1 -0
  231. package/dist/assets/red-hvxz__6c.js +1 -0
  232. package/dist/assets/reg-C-SQnVFl.js +1 -0
  233. package/dist/assets/regexp-CDVJQ6XC.js +1 -0
  234. package/dist/assets/rel-C3B-1QV4.js +1 -0
  235. package/dist/assets/riscv-BM1_JUlF.js +1 -0
  236. package/dist/assets/ron-D8l8udqQ.js +1 -0
  237. package/dist/assets/rose-pine-5qJOZa0Y.js +1 -0
  238. package/dist/assets/rose-pine-dawn-zx0QlTCp.js +1 -0
  239. package/dist/assets/rose-pine-moon-C9pEdX9L.js +1 -0
  240. package/dist/assets/rosmsg-BJDFO7_C.js +1 -0
  241. package/dist/assets/rst-bs7f0vWN.js +1 -0
  242. package/dist/assets/ruby-C0TQ7zu5.js +1 -0
  243. package/dist/assets/rust-B1yitclQ.js +1 -0
  244. package/dist/assets/sas-DEy46yEz.js +1 -0
  245. package/dist/assets/sass-Cj5Yp3dK.js +1 -0
  246. package/dist/assets/scala-CqE71os6.js +1 -0
  247. package/dist/assets/scheme-C98Dy4si.js +1 -0
  248. package/dist/assets/scss-D5BDwBP9.js +1 -0
  249. package/dist/assets/sdbl-DVxCFoDh.js +1 -0
  250. package/dist/assets/shaderlab-Dg9Lc6iA.js +1 -0
  251. package/dist/assets/shellscript-Yzrsuije.js +1 -0
  252. package/dist/assets/shellsession-BADoaaVG.js +1 -0
  253. package/dist/assets/slack-dark-BthQWCQV.js +1 -0
  254. package/dist/assets/slack-ochin-DqwNpetd.js +1 -0
  255. package/dist/assets/smalltalk-BOQMe2GC.js +1 -0
  256. package/dist/assets/smithy-cds9vsN8.js +1 -0
  257. package/dist/assets/snazzy-light-Bw305WKR.js +1 -0
  258. package/dist/assets/solarized-dark-CpvCGNkr.js +1 -0
  259. package/dist/assets/solarized-light-Dlz6yCKv.js +1 -0
  260. package/dist/assets/solidity-DijEV5ha.js +1 -0
  261. package/dist/assets/soy-8wufbnw4.js +1 -0
  262. package/dist/assets/sparql-rVzFXLq3.js +1 -0
  263. package/dist/assets/splunk-BtCnVYZw.js +1 -0
  264. package/dist/assets/sql-CRqJ_cUM.js +1 -0
  265. package/dist/assets/ssh-config-_ykCGR6B.js +1 -0
  266. package/dist/assets/stata-DI20mbqo.js +1 -0
  267. package/dist/assets/stylus-BEDo0Tqx.js +1 -0
  268. package/dist/assets/surrealql-Cjom0U5J.js +1 -0
  269. package/dist/assets/svelte-Cy7k_4gC.js +1 -0
  270. package/dist/assets/swift-C2oV4EkX.js +1 -0
  271. package/dist/assets/synthwave-84-CbfX1IO0.js +1 -0
  272. package/dist/assets/system-verilog-0hqHdDBg.js +1 -0
  273. package/dist/assets/systemd-4A_iFExJ.js +1 -0
  274. package/dist/assets/talonscript-CkByrt1z.js +1 -0
  275. package/dist/assets/tasl-QIJgUcNo.js +1 -0
  276. package/dist/assets/tcl-dwOrl1Do.js +1 -0
  277. package/dist/assets/templ-DhtptRzy.js +1 -0
  278. package/dist/assets/terraform-BETggiCN.js +1 -0
  279. package/dist/assets/tex-D96PA37w.js +1 -0
  280. package/dist/assets/tokyo-night-hegEt444.js +1 -0
  281. package/dist/assets/toml-vGWfd6FD.js +1 -0
  282. package/dist/assets/ts-tags-D351s5mN.js +1 -0
  283. package/dist/assets/tsv-B_m7g4N7.js +1 -0
  284. package/dist/assets/tsx-COt5Ahok.js +1 -0
  285. package/dist/assets/turtle-BsS91CYL.js +1 -0
  286. package/dist/assets/twig-27uCiNez.js +1 -0
  287. package/dist/assets/typescript-BPQ3VLAy.js +1 -0
  288. package/dist/assets/typespec-CAFt9gP4.js +1 -0
  289. package/dist/assets/typst-BUadGCkm.js +1 -0
  290. package/dist/assets/v-BGw2Nkan.js +1 -0
  291. package/dist/assets/vala-CsfeWuGM.js +1 -0
  292. package/dist/assets/vb-Cu-pLBUe.js +1 -0
  293. package/dist/assets/verilog-nZwndyjY.js +1 -0
  294. package/dist/assets/vesper-DRje8inN.js +1 -0
  295. package/dist/assets/vhdl-CeAyd5Ju.js +1 -0
  296. package/dist/assets/viml-CJc9bBzg.js +1 -0
  297. package/dist/assets/vitesse-black-Bkuqu6BP.js +1 -0
  298. package/dist/assets/vitesse-dark-D0r3Knsf.js +1 -0
  299. package/dist/assets/vitesse-light-CVO1_9PV.js +1 -0
  300. package/dist/assets/vue-BqiEGhQt.js +1 -0
  301. package/dist/assets/vue-html-AaS7Mt5G.js +1 -0
  302. package/dist/assets/vue-vine-BoDAl6tE.js +1 -0
  303. package/dist/assets/vyper-CDx5xZoG.js +1 -0
  304. package/dist/assets/wasm-CG6Dc4jp.js +1 -0
  305. package/dist/assets/wasm-MzD3tlZU.js +1 -0
  306. package/dist/assets/wenyan-BV7otONQ.js +1 -0
  307. package/dist/assets/wgsl-Dx-B1_4e.js +1 -0
  308. package/dist/assets/wikitext-BhOHFoWU.js +1 -0
  309. package/dist/assets/wit-5i3qLPDT.js +1 -0
  310. package/dist/assets/wolfram-lXgVvXCa.js +1 -0
  311. package/dist/assets/xml-sdJ4AIDG.js +1 -0
  312. package/dist/assets/xsl-CtQFsRM5.js +1 -0
  313. package/dist/assets/yaml-Buea-lGh.js +1 -0
  314. package/dist/assets/zenscript-DVFEvuxE.js +1 -0
  315. package/dist/assets/zig-VOosw3JB.js +1 -0
  316. package/dist/index.html +14 -0
  317. package/dist-server/companion/src/listener.js +187 -0
  318. package/dist-server/computer-proxy.js +1185 -0
  319. package/dist-server/connector-proxy.js +186 -0
  320. package/dist-server/container-mcp.js +15186 -0
  321. package/dist-server/drivers/agents-proxy.js +887 -0
  322. package/dist-server/drivers/browser-proxy.js +15037 -0
  323. package/dist-server/drivers/dweb-proxy.js +175 -0
  324. package/dist-server/drivers/phone-proxy.js +339 -0
  325. package/dist-server/drivers/pi-mcp-extension.ts +638 -0
  326. package/dist-server/index.js +64869 -0
  327. package/dist-server/local-computer.js +198 -0
  328. package/dist-server/mcp-server.js +1233 -0
  329. package/dist-server/openmausbot.js +3671 -0
  330. package/dist-server/pair-cli.js +3671 -0
  331. package/dist-server/permission-proxy.js +126 -0
  332. package/dist-server/prepare-cloudflared.js +253 -0
  333. package/dist-server/proxy-paths.js +29 -0
  334. package/dist-server/server/atomic.js +40 -0
  335. package/dist-server/server/attachments.js +471 -0
  336. package/dist-server/server/auto-approve.js +162 -0
  337. package/dist-server/server/auto-review.js +79 -0
  338. package/dist-server/server/avatar-image.js +144 -0
  339. package/dist-server/server/bot-cwd.js +29 -0
  340. package/dist-server/server/bot-directory.js +127 -0
  341. package/dist-server/server/bot-package.js +268 -0
  342. package/dist-server/server/bot-profile.js +76 -0
  343. package/dist-server/server/box-create-idempotency.js +463 -0
  344. package/dist-server/server/box.js +944 -0
  345. package/dist-server/server/brand.js +65 -0
  346. package/dist-server/server/browser-connection.js +187 -0
  347. package/dist-server/server/browser-lifecycle-cleanup.js +359 -0
  348. package/dist-server/server/calendar-calls.js +314 -0
  349. package/dist-server/server/channel-queue.js +70 -0
  350. package/dist-server/server/checkpoints.js +405 -0
  351. package/dist-server/server/chief-of-staff.js +49 -0
  352. package/dist-server/server/claude-update.js +70 -0
  353. package/dist-server/server/cli.js +524 -0
  354. package/dist-server/server/cloud-backend.js +30 -0
  355. package/dist-server/server/comms-visibility.js +109 -0
  356. package/dist-server/server/composio.js +945 -0
  357. package/dist-server/server/computer-control.js +149 -0
  358. package/dist-server/server/computer-observation.js +133 -0
  359. package/dist-server/server/computer-proxy.js +1061 -0
  360. package/dist-server/server/config.js +760 -0
  361. package/dist-server/server/connector-proxy.js +228 -0
  362. package/dist-server/server/container-computer.js +840 -0
  363. package/dist-server/server/container-mcp.js +27 -0
  364. package/dist-server/server/contracts.js +24 -0
  365. package/dist-server/server/control-client.js +100 -0
  366. package/dist-server/server/data-dir-lease.js +3 -0
  367. package/dist-server/server/decision-log.js +106 -0
  368. package/dist-server/server/delegations.js +628 -0
  369. package/dist-server/server/drivers/acp/core.js +904 -0
  370. package/dist-server/server/drivers/acp/cursor.js +365 -0
  371. package/dist-server/server/drivers/acp/custom.js +51 -0
  372. package/dist-server/server/drivers/acp/droid.js +225 -0
  373. package/dist-server/server/drivers/acp/gemini.js +95 -0
  374. package/dist-server/server/drivers/acp/grok.js +249 -0
  375. package/dist-server/server/drivers/acp/hermes.js +429 -0
  376. package/dist-server/server/drivers/acp/kimi.js +559 -0
  377. package/dist-server/server/drivers/acp/opencode-go.js +370 -0
  378. package/dist-server/server/drivers/acp/qwen.js +98 -0
  379. package/dist-server/server/drivers/agents-proxy.js +941 -0
  380. package/dist-server/server/drivers/antigravity-acp.js +507 -0
  381. package/dist-server/server/drivers/antigravity-release.js +65 -0
  382. package/dist-server/server/drivers/antigravity-runtime.js +395 -0
  383. package/dist-server/server/drivers/antigravity.js +144 -0
  384. package/dist-server/server/drivers/boxagent.js +245 -0
  385. package/dist-server/server/drivers/browser-proxy.js +520 -0
  386. package/dist-server/server/drivers/builtIn.js +36 -0
  387. package/dist-server/server/drivers/claude.js +1337 -0
  388. package/dist-server/server/drivers/codex-catalog.js +409 -0
  389. package/dist-server/server/drivers/codex.js +1116 -0
  390. package/dist-server/server/drivers/dweb-proxy.js +193 -0
  391. package/dist-server/server/drivers/grok.js +48 -0
  392. package/dist-server/server/drivers/grokagent.js +4 -0
  393. package/dist-server/server/drivers/local-inject.js +388 -0
  394. package/dist-server/server/drivers/minimax.js +108 -0
  395. package/dist-server/server/drivers/native.js +21 -0
  396. package/dist-server/server/drivers/openai-chat.js +240 -0
  397. package/dist-server/server/drivers/openai-compat.js +143 -0
  398. package/dist-server/server/drivers/phone-proxy.js +372 -0
  399. package/dist-server/server/drivers/pi.js +855 -0
  400. package/dist-server/server/drivers/retry.js +96 -0
  401. package/dist-server/server/enterprise.js +93 -0
  402. package/dist-server/server/env-path.js +329 -0
  403. package/dist-server/server/environment.js +105 -0
  404. package/dist-server/server/generated-image.js +39 -0
  405. package/dist-server/server/graceful-shutdown.js +24 -0
  406. package/dist-server/server/group-goal-run.js +124 -0
  407. package/dist-server/server/harness/bus.js +86 -0
  408. package/dist-server/server/harness/registry.js +217 -0
  409. package/dist-server/server/index.js +11634 -0
  410. package/dist-server/server/installed-playbooks.js +34 -0
  411. package/dist-server/server/local-computer.js +278 -0
  412. package/dist-server/server/local-routing.js +9 -0
  413. package/dist-server/server/local-vm-idle.js +44 -0
  414. package/dist-server/server/local-vm-inventory.js +34 -0
  415. package/dist-server/server/local-vm-lease.js +58 -0
  416. package/dist-server/server/mcp-bridge.js +251 -0
  417. package/dist-server/server/mcp-probe.js +153 -0
  418. package/dist-server/server/mcp-registry.js +130 -0
  419. package/dist-server/server/member-turn.js +6 -0
  420. package/dist-server/server/message-db.js +358 -0
  421. package/dist-server/server/message-file.js +439 -0
  422. package/dist-server/server/names.js +26 -0
  423. package/dist-server/server/notify.js +69 -0
  424. package/dist-server/server/openmaus-status-capsule.js +329 -0
  425. package/dist-server/server/openmausbot.js +6 -0
  426. package/dist-server/server/package-export.js +157 -0
  427. package/dist-server/server/pair-cli.js +7 -0
  428. package/dist-server/server/peer-approval-key.js +5 -0
  429. package/dist-server/server/peer-approval.js +182 -0
  430. package/dist-server/server/peer-provenance.js +44 -0
  431. package/dist-server/server/peer-roster.js +139 -0
  432. package/dist-server/server/permission-proxy.js +157 -0
  433. package/dist-server/server/phone-secret.js +358 -0
  434. package/dist-server/server/private-screen-capture.js +17 -0
  435. package/dist-server/server/procs.js +123 -0
  436. package/dist-server/server/project-scout.js +304 -0
  437. package/dist-server/server/proxy-paths.js +47 -0
  438. package/dist-server/server/redact.js +102 -0
  439. package/dist-server/server/remote-computer.js +170 -0
  440. package/dist-server/server/repeat-detector.js +48 -0
  441. package/dist-server/server/replies.js +38 -0
  442. package/dist-server/server/request-auth.js +327 -0
  443. package/dist-server/server/room-cwd.js +15 -0
  444. package/dist-server/server/room-post-budget.js +112 -0
  445. package/dist-server/server/room-turn-timeout.js +115 -0
  446. package/dist-server/server/routine-requests.js +871 -0
  447. package/dist-server/server/routines.js +1111 -0
  448. package/dist-server/server/schema.js +11 -0
  449. package/dist-server/server/screen-frame-gate.js +80 -0
  450. package/dist-server/server/section-context.js +85 -0
  451. package/dist-server/server/send-idempotency.js +53 -0
  452. package/dist-server/server/sessions.js +319 -0
  453. package/dist-server/server/skill-fetch.js +148 -0
  454. package/dist-server/server/skill-learn.js +61 -0
  455. package/dist-server/server/skill-library.js +116 -0
  456. package/dist-server/server/skills.js +1223 -0
  457. package/dist-server/server/steer-queue.js +114 -0
  458. package/dist-server/server/store.js +1217 -0
  459. package/dist-server/server/surface.js +107 -0
  460. package/dist-server/server/tailscale.js +103 -0
  461. package/dist-server/server/team-backup.js +183 -0
  462. package/dist-server/server/team-library.js +205 -0
  463. package/dist-server/server/team-manifest.js +224 -0
  464. package/dist-server/server/thread-events.js +226 -0
  465. package/dist-server/server/tts/elevenlabs.js +92 -0
  466. package/dist-server/server/tts/index.js +79 -0
  467. package/dist-server/server/tts/speech-text.js +195 -0
  468. package/dist-server/server/tts/system-voices.js +74 -0
  469. package/dist-server/server/tunnel.js +202 -0
  470. package/dist-server/server/turn-context.js +40 -0
  471. package/dist-server/server/turn-dispatch-guard.js +152 -0
  472. package/dist-server/server/turn-images.js +138 -0
  473. package/dist-server/server/turn-watchdog.js +65 -0
  474. package/dist-server/server/vps-computer.js +1099 -0
  475. package/dist-server/server/vps-container-mcp.js +31 -0
  476. package/dist-server/server/webhook-ingress.js +173 -0
  477. package/dist-server/server/webhooks.js +491 -0
  478. package/dist-server/server/workspace.js +178 -0
  479. package/dist-server/shared/approval-mode.js +39 -0
  480. package/dist-server/shared/bot-avatar.js +33 -0
  481. package/dist-server/shared/bot-profile.js +7 -0
  482. package/dist-server/shared/connector-availability.js +12 -0
  483. package/dist-server/shared/credential-request.js +82 -0
  484. package/dist-server/shared/group-goal-run.js +1 -0
  485. package/dist-server/shared/import-name.js +11 -0
  486. package/dist-server/shared/mascot-bodies.js +107 -0
  487. package/dist-server/shared/routine-request.js +10 -0
  488. package/dist-server/shared/routine-run.js +1 -0
  489. package/dist-server/shared/skill-request.js +16 -0
  490. package/dist-server/shared/team-backup.js +148 -0
  491. package/dist-server/src/lib/composer-attachments.js +676 -0
  492. package/dist-server/tunnel-guardian.js +390 -0
  493. package/dist-server/vps-container-mcp.js +15209 -0
  494. package/package.json +32 -0
  495. package/skills/create-verification-skill/SKILL.md +65 -0
  496. package/skills/create-verification-skill/manifest.json +13 -0
  497. package/skills/phone-harness/SKILL.md +26 -0
  498. package/skills/phone-harness/agents/openai.yaml +13 -0
  499. package/skills/phone-harness/manifest.json +17 -0
@@ -0,0 +1,1099 @@
1
+ // BYO Linux VPS computer. The agent process stays local; Docker's own SSH
2
+ // transport reaches the user's daemon and the official Cua MCP server stays
3
+ // inside one managed container per bot.
4
+ import { createHash, randomBytes } from "node:crypto";
5
+ import { spawn } from "node:child_process";
6
+ import { createConnection, createServer } from "node:net";
7
+ import { BASE_IMAGE, CUA_DRIVER_VERSION, CUA_SOCKET, IMAGE as CUA_IMAGE, cuaExecArgs, dockerSecurityIsHardened, imageLabelsMatch, managedImageDockerfile, wholeScreenshot, BASE_IMAGE_DIGEST, BASE_IMAGE_LABEL, DRIVER_LABEL, IMAGE_LAYER_LABEL, IMAGE_LAYER_VERSION, MANAGED_LABEL, } from "./container-computer.js";
8
+ import { DATA_DIR, isValidSshAlias, vpsSshAlias } from "./config.js";
9
+ import { augmentedPath, resolveCliSpawn } from "./env-path.js";
10
+ import { loadEnvironmentId } from "./environment.js";
11
+ import { SPAWNED_PROXIES } from "./proxy-paths.js";
12
+ export const VPS_IMAGE = CUA_IMAGE;
13
+ export const VPS_MANAGED_LABEL = "com.openmausbot.vps";
14
+ export const VPS_CONTAINER_LABEL = "com.openmausbot.container";
15
+ export const VPS_ENVIRONMENT_LABEL = "com.openmausbot.environment";
16
+ export const VPS_VIEWER_LABEL = "com.openmausbot.vps-viewer";
17
+ export const VPS_CONTAINER_PREFIX = "openmausbot-vps";
18
+ // The same durable id is also served by the environment discovery endpoint.
19
+ // Resolve it lazily: index must finish legacy data migration and acquire the
20
+ // writer lease before either provider may create the new data directory.
21
+ let vpsEnvironmentIdCache = null;
22
+ function vpsEnvironmentId() {
23
+ if (!vpsEnvironmentIdCache)
24
+ vpsEnvironmentIdCache = loadEnvironmentId(DATA_DIR);
25
+ return vpsEnvironmentIdCache;
26
+ }
27
+ // SIGTERM must give ssh + docker time to tear down the remote exec before the
28
+ // SIGKILL escalation; 1s was routinely too short over a WAN round-trip, and an
29
+ // orphaned remote exec keeps the driver socket busy for the next command.
30
+ const COMMAND_TIMEOUT_KILL_GRACE_MS = 5_000;
31
+ const CONTAINER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/;
32
+ const CONTAINER_ID = /^[a-f0-9]{12,64}$/i;
33
+ const FULL_CONTAINER_ID = /^[a-f0-9]{64}$/i;
34
+ const MANAGED_VPS_CONTAINER_NAME = /^openmausbot-vps-[a-z0-9]{1,12}-[a-f0-9]{12}$/;
35
+ const IMAGE_ID = /^sha256:[a-f0-9]{64}$/i;
36
+ const PIDS_LIMIT = 512;
37
+ const SCREENSHOT_PATH = "/tmp/openmausbot-vps-preview.png";
38
+ const INTERNAL_VIEWER_PORT = 6901;
39
+ const VIEWER_VERSION = "1";
40
+ const lifecycleLocks = new Map();
41
+ /** Pin one SSH destination for a complete provider operation. The shared app
42
+ * config is reloaded in place, so retaining it across awaits could otherwise
43
+ * inspect one host and start/remove a container on another. */
44
+ function snapshotVpsConfig(cfg) {
45
+ const sshAlias = vpsSshAlias(cfg);
46
+ return sshAlias ? { vps: { sshAlias } } : {};
47
+ }
48
+ /** Settings uses this as the reverse side of its config-transition lock: an
49
+ * alias cannot move while a lifecycle action that started first still owns a
50
+ * container lock, including ownerless inventory removals. */
51
+ export function vpsLifecycleBusy() {
52
+ return lifecycleLocks.size > 0;
53
+ }
54
+ // A held lock means a lifecycle mutation (worst case: a 10-minute image
55
+ // build) is running. Waiting it out would wedge Sleep and the screenshot
56
+ // poll behind it, so acquisition fails fast instead.
57
+ const LOCK_ACQUIRE_TIMEOUT_MS = 5_000;
58
+ // The panel polls status every 4-6s and the screen poller re-checks it before
59
+ // every frame; each full status is several docker-over-SSH processes. Same
60
+ // pattern as container-computer's screenshotStatusCache, and the same TTL.
61
+ const STATUS_CACHE_TTL_MS = 10_000;
62
+ const statusCache = new Map();
63
+ const viewerConnections = new Map();
64
+ const desktopTunnels = new Map();
65
+ function containerNamePart(botId) {
66
+ return botId.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 12) || "bot";
67
+ }
68
+ /** Stable across restarts and independent of the bot's editable display name. */
69
+ export function vpsContainerName(botId) {
70
+ const hash = createHash("sha256").update(botId).digest("hex").slice(0, 12);
71
+ return `${VPS_CONTAINER_PREFIX}-${containerNamePart(botId)}-${hash}`;
72
+ }
73
+ export function vpsDockerArgs(alias, args) {
74
+ if (!isValidSshAlias(alias)) {
75
+ throw new Error("invalid VPS SSH config alias");
76
+ }
77
+ return ["-H", `ssh://${alias}`, ...args];
78
+ }
79
+ /** A live VPS desktop is never published by Docker. SSH binds one temporary
80
+ * loopback port on this computer and forwards it to noVNC on the container's
81
+ * private bridge address. Every caller-controlled component is validated
82
+ * before it becomes an argv value. */
83
+ export function vpsSshTunnelArgs(alias, localPort, privateIp) {
84
+ if (!isValidSshAlias(alias))
85
+ throw new Error("invalid VPS SSH config alias");
86
+ if (!Number.isInteger(localPort) || localPort < 1024 || localPort > 65535) {
87
+ throw new Error("invalid VPS viewer port");
88
+ }
89
+ if (!privateDockerIpv4(privateIp))
90
+ throw new Error("invalid VPS private container address");
91
+ return [
92
+ "-N",
93
+ "-o",
94
+ "BatchMode=yes",
95
+ "-o",
96
+ "ExitOnForwardFailure=yes",
97
+ "-o",
98
+ "PermitLocalCommand=no",
99
+ "-o",
100
+ "ServerAliveInterval=30",
101
+ "-o",
102
+ "ServerAliveCountMax=3",
103
+ "-L",
104
+ `127.0.0.1:${localPort}:${privateIp}:${INTERNAL_VIEWER_PORT}`,
105
+ alias,
106
+ ];
107
+ }
108
+ function unusedLoopbackPort() {
109
+ return new Promise((resolve, reject) => {
110
+ const probe = createServer();
111
+ probe.once("error", reject);
112
+ probe.listen(0, "127.0.0.1", () => {
113
+ // SAFETY: this server was bound to an IPv4 TCP address above, so Node
114
+ // returns AddressInfo here rather than a Unix-socket string.
115
+ const port = probe.address().port;
116
+ probe.close((error) => {
117
+ if (error)
118
+ reject(error);
119
+ else if (port >= 1024)
120
+ resolve(port);
121
+ else
122
+ reject(new Error("could not reserve a VPS viewer port"));
123
+ });
124
+ });
125
+ });
126
+ }
127
+ function loopbackAnswers(port) {
128
+ return new Promise((resolve) => {
129
+ const socket = createConnection({ host: "127.0.0.1", port });
130
+ let settled = false;
131
+ const finish = (answer) => {
132
+ if (settled)
133
+ return;
134
+ settled = true;
135
+ socket.destroy();
136
+ resolve(answer);
137
+ };
138
+ socket.once("connect", () => finish(true));
139
+ socket.once("error", () => finish(false));
140
+ socket.setTimeout(250, () => finish(false));
141
+ });
142
+ }
143
+ function stopDesktopTunnel(botId) {
144
+ const tunnel = desktopTunnels.get(botId);
145
+ if (!tunnel)
146
+ return false;
147
+ desktopTunnels.delete(botId);
148
+ clearTimeout(tunnel.expiry);
149
+ if (tunnel.child.exitCode === null && !tunnel.child.killed)
150
+ tunnel.child.kill("SIGTERM");
151
+ return true;
152
+ }
153
+ export function closeVpsDesktopTunnel(botId) {
154
+ return { closed: stopDesktopTunnel(botId) };
155
+ }
156
+ export function closeAllVpsDesktopTunnels() {
157
+ for (const botId of desktopTunnels.keys())
158
+ stopDesktopTunnel(botId);
159
+ }
160
+ const STREAM_CAP_CHARS = 16 * 1024 * 1024;
161
+ /** Keeps the LAST 16MB of a stream without rebuilding one giant string per
162
+ * chunk (a 10-minute `docker build` stream made that rebuild quadratic).
163
+ * Chunks fall off the front as soon as the tail alone covers the cap; the
164
+ * final slice preserves the exact cap semantics of the old accumulator. */
165
+ function tailCollector() {
166
+ const chunks = [];
167
+ let total = 0;
168
+ return {
169
+ push(chunk) {
170
+ chunks.push(chunk);
171
+ total += chunk.length;
172
+ for (;;) {
173
+ const first = chunks[0];
174
+ if (chunks.length < 2 || first === undefined || total - first.length < STREAM_CAP_CHARS)
175
+ break;
176
+ chunks.shift();
177
+ total -= first.length;
178
+ }
179
+ },
180
+ text() {
181
+ return chunks.join("").slice(-STREAM_CAP_CHARS);
182
+ },
183
+ };
184
+ }
185
+ export function defaultRunner(args, options = {}) {
186
+ return new Promise((resolve, reject) => {
187
+ const command = resolveCliSpawn("docker", args);
188
+ const child = spawn(command.command, command.args, {
189
+ shell: false,
190
+ env: { ...process.env, PATH: augmentedPath() },
191
+ stdio: ["pipe", "pipe", "pipe"],
192
+ });
193
+ const stdout = tailCollector();
194
+ const stderr = tailCollector();
195
+ let settled = false;
196
+ let timedOut = false;
197
+ let killTimer;
198
+ let timeout;
199
+ const settle = (finish) => {
200
+ if (settled)
201
+ return;
202
+ settled = true;
203
+ clearTimeout(timeout);
204
+ if (killTimer)
205
+ clearTimeout(killTimer);
206
+ finish();
207
+ };
208
+ timeout = setTimeout(() => {
209
+ if (settled)
210
+ return;
211
+ timedOut = true;
212
+ killTimer = setTimeout(() => {
213
+ if (settled)
214
+ return;
215
+ child.kill("SIGKILL");
216
+ settle(() => reject(new Error("Docker-over-SSH command timed out")));
217
+ }, COMMAND_TIMEOUT_KILL_GRACE_MS);
218
+ killTimer.unref?.();
219
+ child.kill("SIGTERM");
220
+ }, options.timeoutMs ?? 120_000);
221
+ timeout.unref?.();
222
+ child.stdout.setEncoding("utf8");
223
+ child.stderr.setEncoding("utf8");
224
+ child.stdout.on("data", (chunk) => {
225
+ stdout.push(chunk);
226
+ });
227
+ child.stderr.on("data", (chunk) => {
228
+ stderr.push(chunk);
229
+ });
230
+ child.stdin.on("error", (error) => {
231
+ if (timedOut)
232
+ return;
233
+ settle(() => reject(new Error(`Docker-over-SSH stdin failed: ${error.message}`)));
234
+ });
235
+ child.on("error", (error) => {
236
+ settle(() => reject(new Error(`Docker-over-SSH could not start: ${error.message}`)));
237
+ });
238
+ child.on("close", (code, signal) => {
239
+ if (timedOut) {
240
+ settle(() => reject(new Error("Docker-over-SSH command timed out")));
241
+ return;
242
+ }
243
+ settle(() => {
244
+ if (code === 0)
245
+ return resolve({ stdout: stdout.text(), stderr: stderr.text() });
246
+ const detail = stderr.text().trim().slice(-1000);
247
+ reject(new Error(detail || `Docker-over-SSH exited ${code ?? signal ?? "without a status"}`));
248
+ });
249
+ });
250
+ try {
251
+ child.stdin.end(options.input);
252
+ }
253
+ catch (error) {
254
+ settle(() => reject(new Error(`Docker-over-SSH stdin failed: ${error instanceof Error ? error.message : String(error)}`)));
255
+ }
256
+ });
257
+ }
258
+ function emptyStatus(botId, alias) {
259
+ return {
260
+ configured: Boolean(alias),
261
+ sshAlias: alias,
262
+ daemonUp: false,
263
+ image: false,
264
+ imageMatches: false,
265
+ managed: false,
266
+ container: "missing",
267
+ network: "unknown",
268
+ mounts: "unknown",
269
+ security: "unknown",
270
+ desktopReady: false,
271
+ desktop_error: null,
272
+ ready: false,
273
+ problem: alias ? "Docker over SSH is not reachable" : "Configure a VPS SSH alias in App Settings → Connections",
274
+ image_ref: VPS_IMAGE,
275
+ base_image_ref: BASE_IMAGE,
276
+ driver_version: CUA_DRIVER_VERSION,
277
+ container_name: vpsContainerName(botId),
278
+ container_id: null,
279
+ image_id: null,
280
+ };
281
+ }
282
+ /** Docker and Podman both phrase a clean not-found this way ("No such
283
+ * object" / "No such image" / "no such container"); anything else out of an
284
+ * inspect is a transport or daemon failure and must NOT be read as absence —
285
+ * a flaky WAN link that looked like "missing" used to send provision into
286
+ * `docker run --name <existing>` and a baffling name-in-use error. */
287
+ function isMissingObjectMessage(message) {
288
+ return /no such (object|image|container)/i.test(message);
289
+ }
290
+ function transportFailure(message) {
291
+ return `Docker over SSH failed while checking the VPS: ${message.trim().slice(0, 200) || "unknown transport error"}`;
292
+ }
293
+ function privateDockerIpv4(value) {
294
+ if (!value)
295
+ return false;
296
+ const parts = value.split(".").map(Number);
297
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255))
298
+ return false;
299
+ return (parts[0] === 10 ||
300
+ (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
301
+ (parts[0] === 192 && parts[1] === 168));
302
+ }
303
+ function viewerPassword(env) {
304
+ const value = env?.find((entry) => entry.startsWith("VNC_PW="))?.slice("VNC_PW=".length);
305
+ return value && /^[A-Za-z0-9_-]{8,128}$/.test(value) ? value : null;
306
+ }
307
+ function hasNoHostMounts(detail) {
308
+ return (Array.isArray(detail.Mounts) &&
309
+ detail.Mounts.length === 0 &&
310
+ (!detail.HostConfig?.Binds || detail.HostConfig.Binds.length === 0) &&
311
+ (!detail.HostConfig?.VolumesFrom || detail.HostConfig.VolumesFrom.length === 0));
312
+ }
313
+ function hasNoPublishedPorts(config, networks) {
314
+ if (!config)
315
+ return false;
316
+ const networkMode = (config.NetworkMode ?? "").toLowerCase();
317
+ if (!["default", "bridge"].includes(networkMode))
318
+ return false;
319
+ const attached = Object.keys(networks ?? {}).map((name) => name.toLowerCase());
320
+ if (attached.length !== 1 || !["default", "bridge"].includes(attached[0]))
321
+ return false;
322
+ return (config.PublishAllPorts !== true &&
323
+ !Object.values(config.PortBindings ?? {}).some((value) => Array.isArray(value) && value.length > 0));
324
+ }
325
+ function statusProblem(status) {
326
+ if (!status.configured)
327
+ return "Configure a VPS SSH alias in App Settings → Connections";
328
+ if (!status.daemonUp)
329
+ return "Docker over SSH could not reach the VPS; check the SSH alias and Docker on the VPS";
330
+ if (!status.image)
331
+ return `Prepare the pinned OpenMausBot Cua image on the VPS (Driver ${CUA_DRIVER_VERSION})`;
332
+ if (status.container === "missing")
333
+ return "No OpenMausBot container exists for this bot on the VPS";
334
+ if (!status.imageMatches)
335
+ return "The VPS container uses an incompatible or untrusted OpenMausBot image";
336
+ if (!status.managed)
337
+ return "The VPS container name is occupied by a container OpenMausBot did not create";
338
+ if (status.network === "unsafe")
339
+ return "The VPS container uses an unapproved network or publishes ports; refusing to use it";
340
+ if (status.mounts === "unsafe")
341
+ return "The VPS container has host mounts; refusing to use it";
342
+ if (status.security === "unsafe")
343
+ return "The VPS container is missing OpenMausBot safety limits";
344
+ if (status.container === "stopped")
345
+ return "The OpenMausBot VPS container is stopped";
346
+ if (status.desktop_error)
347
+ return `The VPS Cua desktop failed to start: ${status.desktop_error}`;
348
+ if (!status.desktopReady)
349
+ return "The VPS container started, but Cua Driver is not ready yet";
350
+ return null;
351
+ }
352
+ /** The uncached inspection. Lifecycle mutations and their readiness waits
353
+ * call this directly — they must see and publish the truth, never a poll's
354
+ * snapshot; the exported vpsComputerStatus wraps it with the poll cache.
355
+ *
356
+ * `docker info` is deliberately NOT probed as its own round-trip: every
357
+ * docker-over-SSH invocation is a full process + SSH connection, and the
358
+ * image inspect right below already proves the daemon answers — even its
359
+ * "No such image" failure is a daemon reply. Anything that is neither JSON
360
+ * nor a "no such object" reply is attributed to the transport instead. */
361
+ async function computeVpsComputerStatus(cfg, botId, runner) {
362
+ const alias = vpsSshAlias(cfg);
363
+ const status = emptyStatus(botId, alias);
364
+ if (!alias)
365
+ return status;
366
+ viewerConnections.delete(`${alias}:${status.container_name}`);
367
+ const run = (args, timeoutMs = 10_000, input) => runner(vpsDockerArgs(alias, args), { timeoutMs, input });
368
+ let inspectedImageId = null;
369
+ try {
370
+ const inspected = JSON.parse((await run(["image", "inspect", VPS_IMAGE])).stdout);
371
+ status.daemonUp = true;
372
+ const image = inspected[0];
373
+ const labels = image?.Config?.Labels ?? image?.config?.Labels ?? image?.config?.labels;
374
+ const imageId = image?.Id ?? image?.id;
375
+ inspectedImageId = imageId && IMAGE_ID.test(imageId) ? imageId : null;
376
+ status.image_id = inspectedImageId;
377
+ status.image = Boolean(inspectedImageId) && imageLabelsMatch(labels);
378
+ }
379
+ catch (error) {
380
+ const message = error instanceof Error ? error.message : String(error);
381
+ if (!isMissingObjectMessage(message)) {
382
+ status.problem = transportFailure(message);
383
+ return status;
384
+ }
385
+ // A clean "no such image" is still a daemon answer.
386
+ status.daemonUp = true;
387
+ status.image = false;
388
+ }
389
+ try {
390
+ const inspected = JSON.parse((await run(["inspect", status.container_name])).stdout);
391
+ const detail = inspected[0];
392
+ const labels = detail?.Config?.Labels;
393
+ const containerId = detail?.Id ?? detail?.id;
394
+ status.container_id = containerId && CONTAINER_ID.test(containerId) ? containerId : null;
395
+ status.container = detail?.State?.Running ? "running" : "stopped";
396
+ status.imageMatches =
397
+ status.image &&
398
+ Boolean(status.container_id) &&
399
+ (detail?.Config?.Image === VPS_IMAGE || detail?.Config?.Image === inspectedImageId) &&
400
+ Boolean(inspectedImageId) &&
401
+ detail?.Image === inspectedImageId &&
402
+ imageLabelsMatch(labels) &&
403
+ labels?.[VPS_VIEWER_LABEL] === VIEWER_VERSION;
404
+ const environmentLabel = labels?.[VPS_ENVIRONMENT_LABEL];
405
+ status.managed =
406
+ labels?.[VPS_MANAGED_LABEL] === "1" &&
407
+ labels?.[VPS_CONTAINER_LABEL] === status.container_name &&
408
+ // A bot-scoped status is proof that the deterministic legacy container
409
+ // still maps to a bot present in this installation. New containers must
410
+ // carry this installation's durable environment label.
411
+ (environmentLabel === undefined || environmentLabel === vpsEnvironmentId());
412
+ status.network = hasNoPublishedPorts(detail?.HostConfig, detail?.NetworkSettings?.Networks) ? "private" : "unsafe";
413
+ status.mounts = hasNoHostMounts(detail ?? {}) ? "none" : "unsafe";
414
+ status.security = dockerSecurityIsHardened(detail?.HostConfig, { restartPolicy: "unless-stopped" })
415
+ ? "hardened"
416
+ : "unsafe";
417
+ const connectionKey = `${alias}:${status.container_name}`;
418
+ const privateIp = Object.values(detail?.NetworkSettings?.Networks ?? {})[0]?.IPAddress;
419
+ const password = viewerPassword(detail?.Config?.Env);
420
+ if (status.managed && privateIp && privateDockerIpv4(privateIp) && password) {
421
+ viewerConnections.set(connectionKey, { privateIp, password });
422
+ }
423
+ else {
424
+ viewerConnections.delete(connectionKey);
425
+ }
426
+ const containerRef = status.container_id;
427
+ const canProbe = status.container === "running" &&
428
+ status.image &&
429
+ status.imageMatches &&
430
+ status.managed &&
431
+ status.network === "private" &&
432
+ status.mounts === "none" &&
433
+ status.security === "hardened";
434
+ if (canProbe && containerRef) {
435
+ try {
436
+ const version = await run(cuaExecArgs(["--version"], { container: containerRef }));
437
+ if (version.stdout.trim() !== `cua-driver ${CUA_DRIVER_VERSION}`)
438
+ throw new Error("unexpected Cua Driver version");
439
+ await run(cuaExecArgs(["status", "--socket", CUA_SOCKET], { container: containerRef }));
440
+ const health = await run(cuaExecArgs(["call", "health_report", "{}", "--socket", CUA_SOCKET], { container: containerRef }), 15_000);
441
+ const report = JSON.parse(health.stdout);
442
+ if (report.schema_version !== "1" ||
443
+ !Array.isArray(report.checks) ||
444
+ (report.overall !== "ok" && report.overall !== "degraded")) {
445
+ throw new Error(`Cua health report is ${report.overall ?? "invalid"}`);
446
+ }
447
+ // The desktop must ANSWER, not render: get_desktop_state succeeding
448
+ // is the readiness proof. The Local VM also pulls a pixel-validated
449
+ // readiness screenshot because a local exec is free; over SSH that is
450
+ // a full-frame base64 transfer on every status poll, so pixel
451
+ // validation lives solely in vpsComputerScreenshot().
452
+ await run(cuaExecArgs(["call", "get_desktop_state", "{}", "--socket", CUA_SOCKET], { container: containerRef }), 20_000);
453
+ status.desktopReady = true;
454
+ }
455
+ catch (error) {
456
+ status.desktopReady = false;
457
+ status.desktop_error = error instanceof Error ? error.message.slice(0, 320) : null;
458
+ // Mirror the Local VM's probe: when the desktop fails, the
459
+ // supervisor's error log says WHY — a bounded tail turns an endless
460
+ // "not ready yet" into something the user can act on.
461
+ try {
462
+ const errorLog = await run(["exec", containerRef, "tail", "-n", "4", "/var/log/supervisor/cua-driver.error.log"], 10_000);
463
+ status.desktop_error =
464
+ errorLog.stdout.replace(/\s+/g, " ").trim().slice(0, 320) || status.desktop_error;
465
+ }
466
+ catch {
467
+ // The log may not exist during the first seconds of container boot.
468
+ }
469
+ }
470
+ }
471
+ }
472
+ catch (error) {
473
+ const message = error instanceof Error ? error.message : String(error);
474
+ if (isMissingObjectMessage(message)) {
475
+ status.container = "missing";
476
+ }
477
+ else {
478
+ status.daemonUp = false;
479
+ status.problem = transportFailure(message);
480
+ return status;
481
+ }
482
+ }
483
+ status.problem = statusProblem(status);
484
+ status.ready = status.problem === null;
485
+ return status;
486
+ }
487
+ /** Poll-facing status. Real SSH invocations (the default runner) are served
488
+ * from a short TTL cache — the panel and the screen poller each re-check
489
+ * every few seconds, and without the cache one healthy poll cycle cost a
490
+ * dozen SSH connections. Injected runners (tests, lifecycle internals)
491
+ * always recompute. */
492
+ export async function vpsComputerStatus(cfg, botId, runner = defaultRunner) {
493
+ cfg = snapshotVpsConfig(cfg);
494
+ const key = vpsLockKey(cfg, botId);
495
+ const cacheable = runner === defaultRunner && key !== null;
496
+ if (cacheable) {
497
+ const cached = statusCache.get(key);
498
+ if (cached && cached.expiresAt > Date.now())
499
+ return cached.status;
500
+ }
501
+ const status = await computeVpsComputerStatus(cfg, botId, runner);
502
+ if (cacheable)
503
+ statusCache.set(key, { status, expiresAt: Date.now() + STATUS_CACHE_TTL_MS });
504
+ return status;
505
+ }
506
+ const VPS_INVENTORY_LIMIT = 256;
507
+ const VPS_INVENTORY_STATES = new Set([
508
+ "created",
509
+ "restarting",
510
+ "running",
511
+ "removing",
512
+ "paused",
513
+ "exited",
514
+ "dead",
515
+ ]);
516
+ function inventoryFailure(alias, error) {
517
+ const detail = (error instanceof Error ? error.message : String(error))
518
+ .replace(/[\r\n\t]+/g, " ")
519
+ .trim()
520
+ .slice(0, 200);
521
+ return {
522
+ configured: true,
523
+ available: false,
524
+ sshAlias: alias,
525
+ problem: `Docker over SSH could not list managed computers${detail ? `: ${detail}` : ""}`,
526
+ instances: [],
527
+ };
528
+ }
529
+ function managedVpsState(value, running) {
530
+ const state = typeof value === "string" ? value.toLowerCase() : "";
531
+ if (VPS_INVENTORY_STATES.has(state)) {
532
+ return state;
533
+ }
534
+ if (running === true)
535
+ return "running";
536
+ if (running === false)
537
+ return "exited";
538
+ return "unknown";
539
+ }
540
+ /** Read-only account inventory for Settings. This intentionally uses only
541
+ * `container ls` and `container inspect`: opening Settings must never create,
542
+ * start, stop, or probe a desktop. The second managed label and deterministic
543
+ * name are both revalidated before a container is shown as removable. */
544
+ async function scanManagedVpsComputers(cfg, owners, runner = defaultRunner) {
545
+ const alias = vpsSshAlias(cfg);
546
+ if (!alias) {
547
+ return {
548
+ inventory: { configured: false, available: false, sshAlias: null, problem: null, instances: [] },
549
+ containerIds: new Map(),
550
+ };
551
+ }
552
+ try {
553
+ const listed = await runner(vpsDockerArgs(alias, [
554
+ "container",
555
+ "ls",
556
+ "--all",
557
+ "--filter",
558
+ `label=${VPS_MANAGED_LABEL}=1`,
559
+ "--format",
560
+ "{{.ID}}",
561
+ ]), { timeoutMs: 20_000 });
562
+ const ids = [...new Set(listed.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean))];
563
+ if (ids.length > VPS_INVENTORY_LIMIT || ids.some((id) => !CONTAINER_ID.test(id))) {
564
+ throw new Error("the VPS returned an invalid or unexpectedly large managed-container list");
565
+ }
566
+ if (ids.length === 0) {
567
+ return {
568
+ inventory: { configured: true, available: true, sshAlias: alias, problem: null, instances: [] },
569
+ containerIds: new Map(),
570
+ };
571
+ }
572
+ const inspected = await runner(vpsDockerArgs(alias, ["container", "inspect", ...ids]), { timeoutMs: 20_000 });
573
+ const details = JSON.parse(inspected.stdout);
574
+ if (!Array.isArray(details) || details.length !== ids.length) {
575
+ throw new Error("the VPS returned an incomplete managed-container inventory");
576
+ }
577
+ const ownerByName = new Map(owners.map((owner) => [vpsContainerName(owner.botId), owner]));
578
+ const seenIds = new Set();
579
+ const seenNames = new Set();
580
+ const containerIds = new Map();
581
+ const instances = [];
582
+ for (const raw of details) {
583
+ if (!raw || typeof raw !== "object")
584
+ throw new Error("the VPS returned a malformed managed container");
585
+ const detail = raw;
586
+ const id = typeof detail.Id === "string" ? detail.Id.toLowerCase() : "";
587
+ const name = typeof detail.Name === "string" ? detail.Name.replace(/^\//, "") : "";
588
+ const labels = detail.Config?.Labels;
589
+ const listedId = ids.find((candidate) => id.startsWith(candidate.toLowerCase()));
590
+ if (!FULL_CONTAINER_ID.test(id) ||
591
+ !listedId ||
592
+ seenIds.has(listedId) ||
593
+ !MANAGED_VPS_CONTAINER_NAME.test(name) ||
594
+ seenNames.has(name) ||
595
+ !labels ||
596
+ typeof labels !== "object" ||
597
+ labels[VPS_MANAGED_LABEL] !== "1" ||
598
+ labels[VPS_CONTAINER_LABEL] !== name) {
599
+ throw new Error("the VPS returned a managed container whose identity could not be verified");
600
+ }
601
+ const owner = ownerByName.get(name);
602
+ const environmentLabel = labels[VPS_ENVIRONMENT_LABEL];
603
+ seenIds.add(listedId);
604
+ seenNames.add(name);
605
+ // A foreign installation can legitimately share this Docker daemon and
606
+ // therefore appears in the label-filtered provider response. Count the
607
+ // row as inspected, but never return an identifier that could make it
608
+ // removable. Unlabelled pre-environment containers remain manageable
609
+ // only while their deterministic name still maps to a local bot.
610
+ if (environmentLabel !== vpsEnvironmentId() && !(environmentLabel === undefined && owner)) {
611
+ continue;
612
+ }
613
+ containerIds.set(name, id);
614
+ instances.push({
615
+ name,
616
+ state: managedVpsState(detail.State?.Status, detail.State?.Running),
617
+ ownerBotId: owner?.botId ?? null,
618
+ ownerName: owner?.name ?? null,
619
+ orphaned: !owner,
620
+ inUse: owner?.inUse === true,
621
+ });
622
+ }
623
+ if (seenIds.size !== ids.length) {
624
+ throw new Error("the VPS returned an incomplete managed-container inventory");
625
+ }
626
+ instances.sort((left, right) => Number(left.orphaned) - Number(right.orphaned) ||
627
+ (left.ownerName ?? left.name).localeCompare(right.ownerName ?? right.name));
628
+ return {
629
+ inventory: { configured: true, available: true, sshAlias: alias, problem: null, instances },
630
+ containerIds,
631
+ };
632
+ }
633
+ catch (error) {
634
+ return { inventory: inventoryFailure(alias, error), containerIds: new Map() };
635
+ }
636
+ }
637
+ export async function listManagedVpsComputers(cfg, owners, runner = defaultRunner) {
638
+ cfg = snapshotVpsConfig(cfg);
639
+ return (await scanManagedVpsComputers(cfg, owners, runner)).inventory;
640
+ }
641
+ export function vpsContainerRunArgs(containerName, imageRef = VPS_IMAGE, viewerSecret = randomBytes(18).toString("base64url")) {
642
+ if (!CONTAINER_NAME.test(containerName) || (imageRef !== VPS_IMAGE && !IMAGE_ID.test(imageRef))) {
643
+ throw new Error("invalid managed VPS container or image reference");
644
+ }
645
+ if (!/^[A-Za-z0-9_-]{8,128}$/.test(viewerSecret))
646
+ throw new Error("invalid managed VPS viewer secret");
647
+ return [
648
+ "run",
649
+ "-d",
650
+ "--name",
651
+ containerName,
652
+ "--label",
653
+ `${VPS_MANAGED_LABEL}=1`,
654
+ "--label",
655
+ `${VPS_CONTAINER_LABEL}=${containerName}`,
656
+ "--label",
657
+ `${VPS_ENVIRONMENT_LABEL}=${vpsEnvironmentId()}`,
658
+ "--label",
659
+ `${VPS_VIEWER_LABEL}=${VIEWER_VERSION}`,
660
+ "--label",
661
+ `${MANAGED_LABEL}=1`,
662
+ "--label",
663
+ `${DRIVER_LABEL}=${CUA_DRIVER_VERSION}`,
664
+ "--label",
665
+ `${BASE_IMAGE_LABEL}=${BASE_IMAGE_DIGEST}`,
666
+ "--label",
667
+ `${IMAGE_LAYER_LABEL}=${IMAGE_LAYER_VERSION}`,
668
+ "--memory",
669
+ "4g",
670
+ "--memory-swap",
671
+ "4g",
672
+ "--cpus",
673
+ "2",
674
+ "--pids-limit",
675
+ String(PIDS_LIMIT),
676
+ "--network",
677
+ "bridge",
678
+ "--ipc",
679
+ "private",
680
+ "--cgroupns",
681
+ "private",
682
+ "--cap-drop",
683
+ "ALL",
684
+ "--cap-add",
685
+ "SETUID",
686
+ "--cap-add",
687
+ "SETGID",
688
+ "--shm-size",
689
+ "512m",
690
+ // A VPS reboots with nobody watching; without a restart policy the
691
+ // container stays down afterwards and every turn silently degrades until
692
+ // someone opens the panel. unless-stopped survives reboots while still
693
+ // honoring an explicit Stop. The shared hardening check accepts exactly
694
+ // this policy for the VPS caller (and only "no"/unset for the Local VM,
695
+ // whose desktop cannot safely resume).
696
+ "--restart",
697
+ "unless-stopped",
698
+ "-e",
699
+ `VNC_PW=${viewerSecret}`,
700
+ imageRef,
701
+ ];
702
+ }
703
+ function assertUsableContainer(status) {
704
+ if (!status.image ||
705
+ !status.imageMatches ||
706
+ !status.managed ||
707
+ status.network !== "private" ||
708
+ status.mounts !== "none" ||
709
+ status.security !== "hardened") {
710
+ throw Object.assign(new Error(status.problem ?? "The existing VPS container is unsafe or incompatible"), {
711
+ status: 409,
712
+ });
713
+ }
714
+ }
715
+ async function prepareVpsImage(alias, runner) {
716
+ await runner(vpsDockerArgs(alias, ["pull", BASE_IMAGE]), { timeoutMs: 10 * 60_000 });
717
+ await runner(vpsDockerArgs(alias, ["build", "-t", VPS_IMAGE, "-"]), {
718
+ input: managedImageDockerfile(),
719
+ timeoutMs: 10 * 60_000,
720
+ });
721
+ }
722
+ /** Waits for the driver inside a verified, running container to come up.
723
+ * Backoff doubles 0.5s→4s because each poll is a real SSH connection, and
724
+ * between polls only ONE cheap exec (`cua-driver status`) asks whether the
725
+ * driver answers yet — the full multi-invocation status runs again only when
726
+ * that predicate flips, and once more at the deadline, so the returned state
727
+ * is always a complete inspection. */
728
+ async function waitForVpsReady(cfg, botId, runner, budgetMs = 60_000) {
729
+ const alias = vpsSshAlias(cfg);
730
+ const deadline = Date.now() + budgetMs;
731
+ let status = await computeVpsComputerStatus(cfg, botId, runner);
732
+ let delayMs = 500;
733
+ while (!status.ready && alias && Date.now() < deadline) {
734
+ if (!status.daemonUp ||
735
+ !status.image ||
736
+ !status.imageMatches ||
737
+ !status.managed ||
738
+ status.container !== "running" ||
739
+ status.network !== "private" ||
740
+ status.mounts !== "none" ||
741
+ status.security !== "hardened") {
742
+ return status;
743
+ }
744
+ await new Promise((resolve) => {
745
+ const sleep = setTimeout(resolve, Math.min(delayMs, Math.max(0, deadline - Date.now())));
746
+ sleep.unref?.();
747
+ });
748
+ delayMs = Math.min(delayMs * 2, 4_000);
749
+ const container = status.container_id ?? status.container_name;
750
+ const driverAnswers = await runner(vpsDockerArgs(alias, cuaExecArgs(["status", "--socket", CUA_SOCKET], { container })), { timeoutMs: 10_000 }).then(() => true, () => false);
751
+ if (!driverAnswers && Date.now() < deadline)
752
+ continue;
753
+ status = await computeVpsComputerStatus(cfg, botId, runner);
754
+ }
755
+ return status;
756
+ }
757
+ async function withVpsLifecycleLock(key, operation) {
758
+ const previous = lifecycleLocks.get(key);
759
+ let release;
760
+ const current = new Promise((resolve) => {
761
+ release = resolve;
762
+ });
763
+ lifecycleLocks.set(key, current);
764
+ if (previous) {
765
+ let acquireTimer;
766
+ const acquired = await Promise.race([
767
+ previous.then(() => true),
768
+ new Promise((resolve) => {
769
+ acquireTimer = setTimeout(() => resolve(false), LOCK_ACQUIRE_TIMEOUT_MS);
770
+ acquireTimer.unref?.();
771
+ }),
772
+ ]);
773
+ if (acquireTimer)
774
+ clearTimeout(acquireTimer);
775
+ if (!acquired) {
776
+ // Keep the queue serialized: this slot opens only when the holder's
777
+ // does, so a later caller can never run beside the long operation the
778
+ // timed-out one refused to wait for.
779
+ void previous.then(() => {
780
+ release();
781
+ if (lifecycleLocks.get(key) === current)
782
+ lifecycleLocks.delete(key);
783
+ });
784
+ throw Object.assign(new Error("the VPS is being prepared — try again shortly"), { status: 409 });
785
+ }
786
+ }
787
+ try {
788
+ return await operation();
789
+ }
790
+ finally {
791
+ release();
792
+ if (lifecycleLocks.get(key) === current)
793
+ lifecycleLocks.delete(key);
794
+ }
795
+ }
796
+ function vpsLockKey(cfg, botId) {
797
+ const alias = vpsSshAlias(cfg);
798
+ return alias ? `${alias}:${vpsContainerName(botId)}` : null;
799
+ }
800
+ /** Permanently remove one inventory row. The fresh inventory read happens
801
+ * while holding the same per-container lock as provision/start/stop, so a
802
+ * stale Settings tab can never delete a replacement container. */
803
+ export async function removeManagedVpsComputer(cfg, owners, containerName, confirmName, runner = defaultRunner) {
804
+ cfg = snapshotVpsConfig(cfg);
805
+ const alias = vpsSshAlias(cfg);
806
+ if (!alias) {
807
+ throw Object.assign(new Error("VPS is not configured — add an SSH config alias in Connections"), { status: 409 });
808
+ }
809
+ if (!MANAGED_VPS_CONTAINER_NAME.test(containerName)) {
810
+ throw Object.assign(new Error("invalid managed VPS computer name"), { status: 400 });
811
+ }
812
+ const key = `${alias}:${containerName}`;
813
+ return withVpsLifecycleLock(key, async () => {
814
+ const scan = await scanManagedVpsComputers(cfg, owners, runner);
815
+ const inventory = scan.inventory;
816
+ if (!inventory.available) {
817
+ throw Object.assign(new Error(inventory.problem ?? "VPS computer inventory is unavailable"), { status: 503 });
818
+ }
819
+ const instance = inventory.instances.find((candidate) => candidate.name === containerName);
820
+ if (!instance)
821
+ throw Object.assign(new Error("managed VPS computer not found"), { status: 404 });
822
+ if (instance.inUse) {
823
+ throw Object.assign(new Error("this VPS computer is in use — stop its bot's work first"), { status: 409 });
824
+ }
825
+ if (confirmName !== instance.name) {
826
+ throw Object.assign(new Error("confirmation no longer matches this VPS computer — refresh and try again"), { status: 400 });
827
+ }
828
+ const containerId = scan.containerIds.get(instance.name);
829
+ if (!containerId || !FULL_CONTAINER_ID.test(containerId)) {
830
+ throw Object.assign(new Error("the VPS computer identity could not be revalidated"), { status: 409 });
831
+ }
832
+ try {
833
+ // Use the immutable ID from the same inspect, not the mutable name. A
834
+ // VPS administrator replacing a same-name container between inspect
835
+ // and rm must not redirect this explicit removal to the replacement.
836
+ await runner(vpsDockerArgs(alias, ["rm", "-f", containerId]), { timeoutMs: 2 * 60_000 });
837
+ }
838
+ catch (error) {
839
+ const detail = (error instanceof Error ? error.message : String(error))
840
+ .replace(/[\r\n\t]+/g, " ")
841
+ .trim()
842
+ .slice(0, 200);
843
+ throw Object.assign(new Error(`The VPS refused to remove this computer${detail ? `: ${detail}` : ""}`), { status: 502 });
844
+ }
845
+ statusCache.delete(key);
846
+ viewerConnections.delete(key);
847
+ if (instance.ownerBotId)
848
+ stopDesktopTunnel(instance.ownerBotId);
849
+ return { removed: true, name: instance.name };
850
+ });
851
+ }
852
+ export async function vpsComputerAction(action, cfg, botId, runner = defaultRunner) {
853
+ cfg = snapshotVpsConfig(cfg);
854
+ const alias = vpsSshAlias(cfg);
855
+ if (!alias)
856
+ throw Object.assign(new Error("VPS is not configured — add an SSH config alias in App Settings → Connections"), { status: 409 });
857
+ const key = `${alias}:${vpsContainerName(botId)}`;
858
+ const operation = async () => {
859
+ // A mutation invalidates every cached poll answer, before and after: the
860
+ // panel must never keep showing the pre-action world for a TTL.
861
+ statusCache.delete(key);
862
+ try {
863
+ const before = await computeVpsComputerStatus(cfg, botId, runner);
864
+ if (!before.daemonUp)
865
+ throw Object.assign(new Error(before.problem ?? "Docker over SSH is not reachable"), { status: 409 });
866
+ // A real lifecycle change invalidates the remote endpoint. Provision
867
+ // is also the turn-start idempotency path, so leave an already-running
868
+ // viewer alone when no start/replacement will occur.
869
+ if (action !== "provision" || before.container !== "running")
870
+ stopDesktopTunnel(botId);
871
+ const run = (args, timeoutMs = 2 * 60_000) => runner(vpsDockerArgs(alias, args), { timeoutMs });
872
+ const containerRef = before.container_id ?? before.container_name;
873
+ if (action === "provision") {
874
+ if (before.container === "missing") {
875
+ let imageRef = before.image ? before.image_id : null;
876
+ if (!before.image) {
877
+ await prepareVpsImage(alias, runner);
878
+ imageRef = (await computeVpsComputerStatus(cfg, botId, runner)).image_id;
879
+ }
880
+ if (!imageRef)
881
+ throw Object.assign(new Error("The prepared VPS image could not be identified"), { status: 409 });
882
+ await run(vpsContainerRunArgs(before.container_name, imageRef));
883
+ }
884
+ else {
885
+ assertUsableContainer(before);
886
+ if (before.container === "stopped")
887
+ await run(["start", containerRef]);
888
+ }
889
+ }
890
+ else if (action === "start") {
891
+ if (before.container === "missing")
892
+ throw Object.assign(new Error("No VPS container exists for this bot"), { status: 409 });
893
+ if (before.container === "running")
894
+ throw Object.assign(new Error("The VPS container is already running"), { status: 409 });
895
+ assertUsableContainer(before);
896
+ await run(["start", containerRef]);
897
+ }
898
+ else if (action === "remove") {
899
+ // remove exists to escape an incompatible or unsafe container (an
900
+ // IMAGE_LAYER_VERSION bump otherwise bricks the bot: provision 409s
901
+ // on assertUsableContainer forever), so it deliberately skips that
902
+ // check. The ownership labels from the inspect are the only gate:
903
+ // never docker-rm a container OpenMausBot did not create, even one
904
+ // squatting on our name.
905
+ if (before.container === "missing")
906
+ return before;
907
+ if (!before.managed) {
908
+ throw Object.assign(new Error("The VPS container name is occupied by a container OpenMausBot did not create — remove it on the VPS yourself"), { status: 409 });
909
+ }
910
+ await run(["rm", "-f", containerRef]);
911
+ return computeVpsComputerStatus(cfg, botId, runner);
912
+ }
913
+ else {
914
+ if (before.container !== "running")
915
+ throw Object.assign(new Error("The VPS container is not running"), { status: 409 });
916
+ assertUsableContainer(before);
917
+ await run(["stop", containerRef]);
918
+ }
919
+ return action === "stop" ? computeVpsComputerStatus(cfg, botId, runner) : waitForVpsReady(cfg, botId, runner);
920
+ }
921
+ finally {
922
+ statusCache.delete(key);
923
+ }
924
+ };
925
+ return withVpsLifecycleLock(key, operation);
926
+ }
927
+ /** Auto is intentionally read-only: it can attach only to an existing ready
928
+ * container. It recomputes rather than reading the poll cache — routing a
929
+ * turn onto a container that stopped seconds ago is worse than one extra
930
+ * inspection at turn start. */
931
+ export async function reuseVps(cfg, botId, runner = defaultRunner) {
932
+ const status = await inspectVpsForAuto(cfg, botId, runner);
933
+ return status.ready ? status : null;
934
+ }
935
+ /** Auto needs the complete fresh status to explain a failed attach, while
936
+ * retaining reuseVps's no-cache/no-mutation routing guarantee. */
937
+ export async function inspectVpsForAuto(cfg, botId, runner = defaultRunner) {
938
+ cfg = snapshotVpsConfig(cfg);
939
+ const key = vpsLockKey(cfg, botId);
940
+ return key
941
+ ? withVpsLifecycleLock(key, () => computeVpsComputerStatus(cfg, botId, runner))
942
+ : computeVpsComputerStatus(cfg, botId, runner);
943
+ }
944
+ /** Open a temporary noVNC connection through the configured SSH alias. The
945
+ * returned URL is loopback-only and contains the per-container password in
946
+ * its fragment (never sent in HTTP). Closing the app viewer calls the paired
947
+ * close endpoint, and process shutdown closes every remaining child. */
948
+ export async function vpsComputerJoin(cfg, botId, runner = defaultRunner) {
949
+ cfg = snapshotVpsConfig(cfg);
950
+ const alias = vpsSshAlias(cfg);
951
+ if (!alias)
952
+ throw Object.assign(new Error("VPS is not configured"), { status: 409 });
953
+ const existing = desktopTunnels.get(botId);
954
+ if (existing && existing.child.exitCode === null && !existing.child.killed) {
955
+ return { joinUrl: existing.joinUrl, state: "running" };
956
+ }
957
+ stopDesktopTunnel(botId);
958
+ // Always re-inspect here. A cached IP or password from before a container
959
+ // replacement is exactly the sort of secret-bearing stale state a viewer
960
+ // endpoint must not reuse.
961
+ const status = await computeVpsComputerStatus(cfg, botId, runner);
962
+ if (!status.ready) {
963
+ throw Object.assign(new Error(status.problem ?? "The VPS computer is not ready"), { status: 409 });
964
+ }
965
+ const connection = viewerConnections.get(`${alias}:${status.container_name}`);
966
+ if (!connection) {
967
+ throw Object.assign(new Error("This VPS computer predates secure live desktop access — replace its managed container once"), { status: 409 });
968
+ }
969
+ const localPort = await unusedLoopbackPort();
970
+ const child = spawn("ssh", vpsSshTunnelArgs(alias, localPort, connection.privateIp), {
971
+ shell: false,
972
+ env: { ...process.env, PATH: augmentedPath() },
973
+ stdio: ["ignore", "ignore", "pipe"],
974
+ windowsHide: true,
975
+ });
976
+ let failure = null;
977
+ let stderr = "";
978
+ child.stderr?.setEncoding("utf8");
979
+ child.stderr?.on("data", (chunk) => {
980
+ stderr = `${stderr}${chunk}`.slice(-1000);
981
+ });
982
+ child.once("error", (error) => {
983
+ failure = error.message;
984
+ });
985
+ child.once("close", (code) => {
986
+ const active = desktopTunnels.get(botId);
987
+ if (active?.child === child) {
988
+ clearTimeout(active.expiry);
989
+ desktopTunnels.delete(botId);
990
+ }
991
+ if (!failure)
992
+ failure = stderr.trim() || `SSH viewer tunnel exited ${code ?? "without a status"}`;
993
+ });
994
+ const deadline = Date.now() + 10_000;
995
+ while (Date.now() < deadline && !failure) {
996
+ if (await loopbackAnswers(localPort))
997
+ break;
998
+ await new Promise((resolve) => setTimeout(resolve, 100));
999
+ }
1000
+ if (failure || !(await loopbackAnswers(localPort))) {
1001
+ if (child.exitCode === null && !child.killed)
1002
+ child.kill("SIGTERM");
1003
+ const detail = failure || stderr.trim() || "the SSH port forward did not become ready";
1004
+ throw Object.assign(new Error(`Could not open the VPS live desktop: ${detail}`), { status: 502 });
1005
+ }
1006
+ const joinUrl = `http://127.0.0.1:${localPort}/vnc.html#autoconnect=true&resize=scale&password=${encodeURIComponent(connection.password)}`;
1007
+ // Viewer-close is the normal cleanup. This unref'd ceiling is a backstop
1008
+ // for a renderer crash or an old browser client that cannot signal close.
1009
+ const expiry = setTimeout(() => stopDesktopTunnel(botId), 8 * 60 * 60_000);
1010
+ expiry.unref?.();
1011
+ desktopTunnels.set(botId, { child, joinUrl, expiry });
1012
+ return { joinUrl, state: "running" };
1013
+ }
1014
+ export function vpsContainerMcpArgs(alias, containerName) {
1015
+ if (!isValidSshAlias(alias) || (!CONTAINER_NAME.test(containerName) && !CONTAINER_ID.test(containerName))) {
1016
+ throw new Error("invalid VPS MCP connection");
1017
+ }
1018
+ return vpsDockerArgs(alias, cuaExecArgs(["mcp", "--socket", CUA_SOCKET], { container: containerName, interactive: true }));
1019
+ }
1020
+ export function vpsComputerMcp(cfg, botId, containerRef) {
1021
+ const alias = vpsSshAlias(cfg);
1022
+ if (!alias)
1023
+ throw new Error("VPS is not configured — add an SSH config alias first");
1024
+ return {
1025
+ command: process.execPath,
1026
+ args: [SPAWNED_PROXIES.vpsContainerMcp, alias, containerRef ?? vpsContainerName(botId)],
1027
+ env: { ELECTRON_RUN_AS_NODE: "1" },
1028
+ };
1029
+ }
1030
+ export function vpsDriverError(driverKind, computerMcp) {
1031
+ if (driverKind === "boxAgent") {
1032
+ return "The Computer engine runs its agent on Box and cannot use a self-hosted VPS — choose Claude or an ACP engine";
1033
+ }
1034
+ if (!computerMcp) {
1035
+ return "This model engine cannot mount a self-hosted VPS computer — choose Claude or an ACP engine";
1036
+ }
1037
+ return null;
1038
+ }
1039
+ export async function vpsComputerScreenshot(cfg, botId, runner = defaultRunner) {
1040
+ cfg = snapshotVpsConfig(cfg);
1041
+ const alias = vpsSshAlias(cfg);
1042
+ if (!alias)
1043
+ throw Object.assign(new Error("VPS is not configured"), { status: 409 });
1044
+ const key = `${alias}:${vpsContainerName(botId)}`;
1045
+ const cacheable = runner === defaultRunner;
1046
+ return withVpsLifecycleLock(key, async () => {
1047
+ // Same shape as containerComputerScreenshot's screenshotStatusCache: the
1048
+ // poller runs every few seconds, and re-verifying the whole container
1049
+ // between frames multiplied every frame's SSH cost.
1050
+ const cached = cacheable ? statusCache.get(key) : undefined;
1051
+ const status = cached && cached.expiresAt > Date.now()
1052
+ ? cached.status
1053
+ : await computeVpsComputerStatus(cfg, botId, runner);
1054
+ if (!status.ready) {
1055
+ if (cacheable)
1056
+ statusCache.delete(key);
1057
+ throw Object.assign(new Error(status.problem ?? "The VPS computer is not ready"), { status: 409 });
1058
+ }
1059
+ if (cacheable)
1060
+ statusCache.set(key, { status, expiresAt: Date.now() + STATUS_CACHE_TTL_MS });
1061
+ const containerRef = status.container_id ?? status.container_name;
1062
+ // The ref goes straight into docker argv, and a cached status is one
1063
+ // more step removed from the inspect that produced it — revalidate the
1064
+ // exact shapes before spending an exec on it.
1065
+ if (!CONTAINER_ID.test(containerRef) && !CONTAINER_NAME.test(containerRef)) {
1066
+ throw Object.assign(new Error("the VPS container reference is malformed"), { status: 409 });
1067
+ }
1068
+ try {
1069
+ await runner(vpsDockerArgs(alias, cuaExecArgs(["call", "get_desktop_state", "{}", "--socket", CUA_SOCKET, "--screenshot-out-file", SCREENSHOT_PATH], { container: containerRef })), { timeoutMs: 30_000 });
1070
+ const encoded = (await runner(vpsDockerArgs(alias, [
1071
+ "exec",
1072
+ "-u",
1073
+ "cua",
1074
+ "-e",
1075
+ "HOME=/home/cua",
1076
+ containerRef,
1077
+ "base64",
1078
+ "-w0",
1079
+ SCREENSHOT_PATH,
1080
+ ]), { timeoutMs: 30_000 })).stdout.trim();
1081
+ const checked = wholeScreenshot(Buffer.from(encoded, "base64"));
1082
+ if (!checked.ok)
1083
+ throw Object.assign(new Error("Cua Driver returned an incomplete VPS screenshot"), { status: 502 });
1084
+ return { png: encoded, format: checked.mime === "image/jpeg" ? "jpeg" : "png" };
1085
+ }
1086
+ catch (error) {
1087
+ // The failure may mean the world changed (container stopped, link
1088
+ // dropped); a cached "ready" would keep the poller failing for a TTL.
1089
+ if (cacheable)
1090
+ statusCache.delete(key);
1091
+ throw error;
1092
+ }
1093
+ finally {
1094
+ await runner(vpsDockerArgs(alias, ["exec", "-u", "cua", containerRef, "rm", "-f", SCREENSHOT_PATH]), {
1095
+ timeoutMs: 10_000,
1096
+ }).catch(() => { });
1097
+ }
1098
+ });
1099
+ }