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.
- package/LICENSE +202 -0
- package/README.md +14 -0
- package/cli.js +2 -0
- package/dist/app-icon.svg +29 -0
- package/dist/assets/abap-BdImnpbu.js +1 -0
- package/dist/assets/actionscript-3-B3316cI-.js +1 -0
- package/dist/assets/ada-bCR0ucgS.js +1 -0
- package/dist/assets/ahk-CsyLZFj1.js +1 -0
- package/dist/assets/ahk2-8Zs4aa1G.js +1 -0
- package/dist/assets/andromeeda-C4gqWexZ.js +1 -0
- package/dist/assets/angular-html-DA-rfuFy.js +1 -0
- package/dist/assets/angular-ts-BrjP3tb8.js +1 -0
- package/dist/assets/apache-Pmp26Uib.js +1 -0
- package/dist/assets/apex-DhZFqWV2.js +1 -0
- package/dist/assets/apl-CORt7UWP.js +1 -0
- package/dist/assets/applescript-Co6uUVPk.js +1 -0
- package/dist/assets/ara-BRHolxvo.js +1 -0
- package/dist/assets/asciidoc-CSVQ5wI8.js +1 -0
- package/dist/assets/asm-D_Q5rh1f.js +1 -0
- package/dist/assets/astro-HNnZUWAn.js +1 -0
- package/dist/assets/aurora-x-D-2ljcwZ.js +1 -0
- package/dist/assets/awk-DMzUqQB5.js +1 -0
- package/dist/assets/ayu-dark-DYE7WIF3.js +1 -0
- package/dist/assets/ayu-light-BA47KaF1.js +1 -0
- package/dist/assets/ayu-mirage-32ctXXKs.js +1 -0
- package/dist/assets/ballerina-BFfxhgS-.js +1 -0
- package/dist/assets/bat-CickPsom.js +1 -0
- package/dist/assets/beancount-k_qm7-4y.js +1 -0
- package/dist/assets/berry-uYugtg8r.js +1 -0
- package/dist/assets/bibtex-CHM0blh-.js +1 -0
- package/dist/assets/bicep-Bmn6On1c.js +1 -0
- package/dist/assets/bird2-Bx8U0n9b.js +1 -0
- package/dist/assets/blade-2xfisSek.js +1 -0
- package/dist/assets/bsl-DlhNcFeZ.js +1 -0
- package/dist/assets/c-BIGW1oBm.js +1 -0
- package/dist/assets/c3-Dp5svz6Z.js +1 -0
- package/dist/assets/cadence-Bv_4Rxtq.js +1 -0
- package/dist/assets/cairo-KRGpt6FW.js +1 -0
- package/dist/assets/catppuccin-frappe-CZL1YF0i.js +1 -0
- package/dist/assets/catppuccin-latte-DH-8KZSZ.js +1 -0
- package/dist/assets/catppuccin-macchiato-B7yYVSCf.js +1 -0
- package/dist/assets/catppuccin-mocha-Ct7hS0mc.js +1 -0
- package/dist/assets/chapel-DTp_pixX.js +1 -0
- package/dist/assets/clarity-Dn5IMItf.js +1 -0
- package/dist/assets/clojure-P80f7IUj.js +1 -0
- package/dist/assets/cmake-D1j8_8rp.js +1 -0
- package/dist/assets/cobol-nBiQ_Alo.js +1 -0
- package/dist/assets/codeowners-Bp6g37R7.js +1 -0
- package/dist/assets/codeql-DsOJ9woJ.js +1 -0
- package/dist/assets/coffee-Ch7k5sss.js +1 -0
- package/dist/assets/common-lisp-Cg-RD9OK.js +1 -0
- package/dist/assets/coq-C7JzOVbR.js +1 -0
- package/dist/assets/cpp-BMRokrvK.js +1 -0
- package/dist/assets/crystal-DGywbUpC.js +1 -0
- package/dist/assets/csharp-DSvCPggb.js +1 -0
- package/dist/assets/css-CLj8gQPS.js +1 -0
- package/dist/assets/csv-fuZLfV_i.js +1 -0
- package/dist/assets/cue-D82EKSYY.js +1 -0
- package/dist/assets/cypher-COkxafJQ.js +1 -0
- package/dist/assets/d-85-TOEBH.js +1 -0
- package/dist/assets/dark-plus-C3mMm8J8.js +1 -0
- package/dist/assets/dart-bE4Kk8sk.js +1 -0
- package/dist/assets/dax-CEL-wOlO.js +1 -0
- package/dist/assets/desktop-BmXAJ9_W.js +1 -0
- package/dist/assets/diff-D97Zzqfu.js +1 -0
- package/dist/assets/docker-BcOcwvcX.js +1 -0
- package/dist/assets/dotenv-Da5cRb03.js +1 -0
- package/dist/assets/dracula-BzJJZx-M.js +1 -0
- package/dist/assets/dracula-soft-BXkSAIEj.js +1 -0
- package/dist/assets/dream-maker-BtqSS_iP.js +1 -0
- package/dist/assets/edge-FbVlp4U3.js +1 -0
- package/dist/assets/elixir-CkH2-t6x.js +1 -0
- package/dist/assets/elm-DbKCFpqz.js +1 -0
- package/dist/assets/emacs-lisp-C_m_b--Z.js +1 -0
- package/dist/assets/erb-DXfck5VN.js +1 -0
- package/dist/assets/erlang-DsQrWhSR.js +1 -0
- package/dist/assets/everforest-dark-BgDCqdQA.js +1 -0
- package/dist/assets/everforest-light-C8M2exoo.js +1 -0
- package/dist/assets/fennel-BYunw83y.js +1 -0
- package/dist/assets/fish-BvzEVeQv.js +1 -0
- package/dist/assets/fluent-C4IJs8-o.js +1 -0
- package/dist/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
- package/dist/assets/fortran-free-form-BxgE0vQu.js +1 -0
- package/dist/assets/fsharp-CXgrBDvD.js +1 -0
- package/dist/assets/gdresource-TyuKm33G.js +1 -0
- package/dist/assets/gdscript-DqcFQ5yU.js +1 -0
- package/dist/assets/gdshader-DkwncUOv.js +1 -0
- package/dist/assets/genie-D0YGMca9.js +1 -0
- package/dist/assets/gherkin-DyxjwDmM.js +1 -0
- package/dist/assets/git-commit-F4YmCXRG.js +1 -0
- package/dist/assets/git-rebase-r7XF79zn.js +1 -0
- package/dist/assets/github-dark-DHJKELXO.js +1 -0
- package/dist/assets/github-dark-default-Cuk6v7N8.js +1 -0
- package/dist/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- package/dist/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- package/dist/assets/github-light-DAi9KRSo.js +1 -0
- package/dist/assets/github-light-default-D7oLnXFd.js +1 -0
- package/dist/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- package/dist/assets/gleam-BspZqrRM.js +1 -0
- package/dist/assets/glimmer-js-ByusRIyA.js +1 -0
- package/dist/assets/glimmer-ts-BfAWNZQY.js +1 -0
- package/dist/assets/glsl-DplSGwfg.js +1 -0
- package/dist/assets/gn-n2N0HUVH.js +1 -0
- package/dist/assets/gnuplot-DdkO51Og.js +1 -0
- package/dist/assets/go-C27-OAKa.js +1 -0
- package/dist/assets/graphql-ChdNCCLP.js +1 -0
- package/dist/assets/groovy-gcz8RCvz.js +1 -0
- package/dist/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
- package/dist/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
- package/dist/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
- package/dist/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
- package/dist/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
- package/dist/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
- package/dist/assets/hack-BWmVpMyf.js +1 -0
- package/dist/assets/haml-D5jkg6IW.js +1 -0
- package/dist/assets/handlebars-BpdQsYii.js +1 -0
- package/dist/assets/haskell-Df6bDoY_.js +1 -0
- package/dist/assets/haxe-CfZj7gIn.js +1 -0
- package/dist/assets/hcl-BWvSN4gD.js +1 -0
- package/dist/assets/hjson-D5-asLiD.js +1 -0
- package/dist/assets/hlsl-D3lLCCz7.js +1 -0
- package/dist/assets/horizon-BUw7H-hv.js +1 -0
- package/dist/assets/horizon-bright-CUuTKBJd.js +1 -0
- package/dist/assets/houston-DnULxvSX.js +1 -0
- package/dist/assets/html-derivative-DlHx6ybY.js +1 -0
- package/dist/assets/html-pp8916En.js +1 -0
- package/dist/assets/http-jrhK8wxY.js +1 -0
- package/dist/assets/hurl-irOxFIW8.js +1 -0
- package/dist/assets/hxml-2-FPmUDs.js +1 -0
- package/dist/assets/hy-DFXneXwc.js +1 -0
- package/dist/assets/imba-DGztddWO.js +1 -0
- package/dist/assets/index-B9rLIY81.js +263 -0
- package/dist/assets/index-CJBrtDbC.css +1 -0
- package/dist/assets/index-D-u0LYsV.js +153 -0
- package/dist/assets/ini-BEwlwnbL.js +1 -0
- package/dist/assets/java-CylS5w8V.js +1 -0
- package/dist/assets/javascript-wDzz0qaB.js +1 -0
- package/dist/assets/jinja-f2NsQr07.js +1 -0
- package/dist/assets/jison-wvAkD_A8.js +1 -0
- package/dist/assets/json-Cp-IABpG.js +1 -0
- package/dist/assets/json5-C9tS-k6U.js +1 -0
- package/dist/assets/jsonc-Des-eS-w.js +1 -0
- package/dist/assets/jsonl-DcaNXYhu.js +1 -0
- package/dist/assets/jsonnet-DFQXde-d.js +1 -0
- package/dist/assets/jssm-C2t-YnRu.js +1 -0
- package/dist/assets/jsx-g9-lgVsj.js +1 -0
- package/dist/assets/julia-5Bft2YPA.js +1 -0
- package/dist/assets/just-Cwhn7H3k.js +1 -0
- package/dist/assets/kanagawa-dragon-BuwD2xS4.js +1 -0
- package/dist/assets/kanagawa-lotus-C3DzagqV.js +1 -0
- package/dist/assets/kanagawa-wave-DFGoQZhC.js +1 -0
- package/dist/assets/kdl-DV7GczEv.js +1 -0
- package/dist/assets/kotlin-BdnUsdx6.js +1 -0
- package/dist/assets/kusto-wEQ09or8.js +1 -0
- package/dist/assets/laserwave-DUszq2jm.js +1 -0
- package/dist/assets/latex-D5pSuvFb.js +1 -0
- package/dist/assets/lean-BZvkOJ9d.js +1 -0
- package/dist/assets/less-B1dDrJ26.js +1 -0
- package/dist/assets/light-plus-B7mTdjB0.js +1 -0
- package/dist/assets/liquid-C0sCDyMI.js +1 -0
- package/dist/assets/llvm-BZoOZj88.js +1 -0
- package/dist/assets/log-2UxHyX5q.js +1 -0
- package/dist/assets/logo-BtOb2qkB.js +1 -0
- package/dist/assets/lua-BaeVxFsk.js +1 -0
- package/dist/assets/luau-BnpPk5vE.js +1 -0
- package/dist/assets/make-CHLpvVh8.js +1 -0
- package/dist/assets/markdown-Cvjx9yec.js +1 -0
- package/dist/assets/marko-DjSrsDqO.js +1 -0
- package/dist/assets/material-theme-D5KoaKCx.js +1 -0
- package/dist/assets/material-theme-darker-BfHTSMKl.js +1 -0
- package/dist/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- package/dist/assets/material-theme-ocean-CyktbL80.js +1 -0
- package/dist/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- package/dist/assets/matlab-D7o27uSR.js +1 -0
- package/dist/assets/mdc-D1_yUvq7.js +1 -0
- package/dist/assets/mdx-Cmh6b_Ma.js +1 -0
- package/dist/assets/mermaid-CQcHuHx7.js +1 -0
- package/dist/assets/min-dark-CafNBF8u.js +1 -0
- package/dist/assets/min-light-CTRr51gU.js +1 -0
- package/dist/assets/mipsasm-CKIfxQSi.js +1 -0
- package/dist/assets/mojo-DJz3ZmWd.js +1 -0
- package/dist/assets/monokai-BRVnQi9A.js +1 -0
- package/dist/assets/moonbit-CHtswR0a.js +1 -0
- package/dist/assets/move-el3G9tDJ.js +1 -0
- package/dist/assets/narrat-DRg8JJMk.js +1 -0
- package/dist/assets/nextflow-C-mBbutL.js +1 -0
- package/dist/assets/nextflow-groovy-vE_lwT2v.js +1 -0
- package/dist/assets/nginx-BpAMiNFr.js +1 -0
- package/dist/assets/night-owl-C39BiMTA.js +1 -0
- package/dist/assets/night-owl-light-CMTm3GFP.js +1 -0
- package/dist/assets/nim-BIad80T-.js +1 -0
- package/dist/assets/nix-CwoSXNpI.js +1 -0
- package/dist/assets/nord-Ddv68eIx.js +1 -0
- package/dist/assets/nsis-BlV79W_Q.js +1 -0
- package/dist/assets/nushell-D3jzshHO.js +1 -0
- package/dist/assets/objective-c-DXmwc3jG.js +1 -0
- package/dist/assets/objective-cpp-CLxacb5B.js +1 -0
- package/dist/assets/ocaml-C0hk2d4L.js +1 -0
- package/dist/assets/odin-BBf5iR-q.js +1 -0
- package/dist/assets/one-dark-pro-DVMEJ2y_.js +1 -0
- package/dist/assets/one-light-C3Wv6jpd.js +1 -0
- package/dist/assets/openscad-C4EeE6gA.js +1 -0
- package/dist/assets/org-DM6o9KBp.js +1 -0
- package/dist/assets/pascal-D93ZcfNL.js +1 -0
- package/dist/assets/perl-B9cMNwum.js +1 -0
- package/dist/assets/php-Csjmro_R.js +1 -0
- package/dist/assets/pkl-u5AG7uiY.js +1 -0
- package/dist/assets/plastic-3e1v2bzS.js +1 -0
- package/dist/assets/plsql-ChMvpjG-.js +1 -0
- package/dist/assets/po-BTJTHyun.js +1 -0
- package/dist/assets/poimandres-CS3Unz2-.js +1 -0
- package/dist/assets/polar-C0HS_06l.js +1 -0
- package/dist/assets/postcss-CXtECtnM.js +1 -0
- package/dist/assets/powerquery-CEu0bR-o.js +1 -0
- package/dist/assets/powershell-BmBUJMz7.js +1 -0
- package/dist/assets/prisma-Vru482bI.js +1 -0
- package/dist/assets/prolog-CbFg5uaA.js +1 -0
- package/dist/assets/proto-C7zT0LnQ.js +1 -0
- package/dist/assets/pug-DKIMFp6K.js +1 -0
- package/dist/assets/puppet-BMWR74SV.js +1 -0
- package/dist/assets/purescript-CklMAg4u.js +1 -0
- package/dist/assets/python-B6aJPvgy.js +1 -0
- package/dist/assets/qml-3beO22l8.js +1 -0
- package/dist/assets/qmldir-C8lEn-DE.js +1 -0
- package/dist/assets/qss-IeuSbFQv.js +1 -0
- package/dist/assets/r-Cf5RLm7j.js +1 -0
- package/dist/assets/racket-BqYA7rlc.js +1 -0
- package/dist/assets/raku-DXvB9xmW.js +1 -0
- package/dist/assets/razor-BjBPvh-w.js +1 -0
- package/dist/assets/rbs-CpoqiR4B.js +1 -0
- package/dist/assets/red-hvxz__6c.js +1 -0
- package/dist/assets/reg-C-SQnVFl.js +1 -0
- package/dist/assets/regexp-CDVJQ6XC.js +1 -0
- package/dist/assets/rel-C3B-1QV4.js +1 -0
- package/dist/assets/riscv-BM1_JUlF.js +1 -0
- package/dist/assets/ron-D8l8udqQ.js +1 -0
- package/dist/assets/rose-pine-5qJOZa0Y.js +1 -0
- package/dist/assets/rose-pine-dawn-zx0QlTCp.js +1 -0
- package/dist/assets/rose-pine-moon-C9pEdX9L.js +1 -0
- package/dist/assets/rosmsg-BJDFO7_C.js +1 -0
- package/dist/assets/rst-bs7f0vWN.js +1 -0
- package/dist/assets/ruby-C0TQ7zu5.js +1 -0
- package/dist/assets/rust-B1yitclQ.js +1 -0
- package/dist/assets/sas-DEy46yEz.js +1 -0
- package/dist/assets/sass-Cj5Yp3dK.js +1 -0
- package/dist/assets/scala-CqE71os6.js +1 -0
- package/dist/assets/scheme-C98Dy4si.js +1 -0
- package/dist/assets/scss-D5BDwBP9.js +1 -0
- package/dist/assets/sdbl-DVxCFoDh.js +1 -0
- package/dist/assets/shaderlab-Dg9Lc6iA.js +1 -0
- package/dist/assets/shellscript-Yzrsuije.js +1 -0
- package/dist/assets/shellsession-BADoaaVG.js +1 -0
- package/dist/assets/slack-dark-BthQWCQV.js +1 -0
- package/dist/assets/slack-ochin-DqwNpetd.js +1 -0
- package/dist/assets/smalltalk-BOQMe2GC.js +1 -0
- package/dist/assets/smithy-cds9vsN8.js +1 -0
- package/dist/assets/snazzy-light-Bw305WKR.js +1 -0
- package/dist/assets/solarized-dark-CpvCGNkr.js +1 -0
- package/dist/assets/solarized-light-Dlz6yCKv.js +1 -0
- package/dist/assets/solidity-DijEV5ha.js +1 -0
- package/dist/assets/soy-8wufbnw4.js +1 -0
- package/dist/assets/sparql-rVzFXLq3.js +1 -0
- package/dist/assets/splunk-BtCnVYZw.js +1 -0
- package/dist/assets/sql-CRqJ_cUM.js +1 -0
- package/dist/assets/ssh-config-_ykCGR6B.js +1 -0
- package/dist/assets/stata-DI20mbqo.js +1 -0
- package/dist/assets/stylus-BEDo0Tqx.js +1 -0
- package/dist/assets/surrealql-Cjom0U5J.js +1 -0
- package/dist/assets/svelte-Cy7k_4gC.js +1 -0
- package/dist/assets/swift-C2oV4EkX.js +1 -0
- package/dist/assets/synthwave-84-CbfX1IO0.js +1 -0
- package/dist/assets/system-verilog-0hqHdDBg.js +1 -0
- package/dist/assets/systemd-4A_iFExJ.js +1 -0
- package/dist/assets/talonscript-CkByrt1z.js +1 -0
- package/dist/assets/tasl-QIJgUcNo.js +1 -0
- package/dist/assets/tcl-dwOrl1Do.js +1 -0
- package/dist/assets/templ-DhtptRzy.js +1 -0
- package/dist/assets/terraform-BETggiCN.js +1 -0
- package/dist/assets/tex-D96PA37w.js +1 -0
- package/dist/assets/tokyo-night-hegEt444.js +1 -0
- package/dist/assets/toml-vGWfd6FD.js +1 -0
- package/dist/assets/ts-tags-D351s5mN.js +1 -0
- package/dist/assets/tsv-B_m7g4N7.js +1 -0
- package/dist/assets/tsx-COt5Ahok.js +1 -0
- package/dist/assets/turtle-BsS91CYL.js +1 -0
- package/dist/assets/twig-27uCiNez.js +1 -0
- package/dist/assets/typescript-BPQ3VLAy.js +1 -0
- package/dist/assets/typespec-CAFt9gP4.js +1 -0
- package/dist/assets/typst-BUadGCkm.js +1 -0
- package/dist/assets/v-BGw2Nkan.js +1 -0
- package/dist/assets/vala-CsfeWuGM.js +1 -0
- package/dist/assets/vb-Cu-pLBUe.js +1 -0
- package/dist/assets/verilog-nZwndyjY.js +1 -0
- package/dist/assets/vesper-DRje8inN.js +1 -0
- package/dist/assets/vhdl-CeAyd5Ju.js +1 -0
- package/dist/assets/viml-CJc9bBzg.js +1 -0
- package/dist/assets/vitesse-black-Bkuqu6BP.js +1 -0
- package/dist/assets/vitesse-dark-D0r3Knsf.js +1 -0
- package/dist/assets/vitesse-light-CVO1_9PV.js +1 -0
- package/dist/assets/vue-BqiEGhQt.js +1 -0
- package/dist/assets/vue-html-AaS7Mt5G.js +1 -0
- package/dist/assets/vue-vine-BoDAl6tE.js +1 -0
- package/dist/assets/vyper-CDx5xZoG.js +1 -0
- package/dist/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/assets/wasm-MzD3tlZU.js +1 -0
- package/dist/assets/wenyan-BV7otONQ.js +1 -0
- package/dist/assets/wgsl-Dx-B1_4e.js +1 -0
- package/dist/assets/wikitext-BhOHFoWU.js +1 -0
- package/dist/assets/wit-5i3qLPDT.js +1 -0
- package/dist/assets/wolfram-lXgVvXCa.js +1 -0
- package/dist/assets/xml-sdJ4AIDG.js +1 -0
- package/dist/assets/xsl-CtQFsRM5.js +1 -0
- package/dist/assets/yaml-Buea-lGh.js +1 -0
- package/dist/assets/zenscript-DVFEvuxE.js +1 -0
- package/dist/assets/zig-VOosw3JB.js +1 -0
- package/dist/index.html +14 -0
- package/dist-server/companion/src/listener.js +187 -0
- package/dist-server/computer-proxy.js +1185 -0
- package/dist-server/connector-proxy.js +186 -0
- package/dist-server/container-mcp.js +15186 -0
- package/dist-server/drivers/agents-proxy.js +887 -0
- package/dist-server/drivers/browser-proxy.js +15037 -0
- package/dist-server/drivers/dweb-proxy.js +175 -0
- package/dist-server/drivers/phone-proxy.js +339 -0
- package/dist-server/drivers/pi-mcp-extension.ts +638 -0
- package/dist-server/index.js +64869 -0
- package/dist-server/local-computer.js +198 -0
- package/dist-server/mcp-server.js +1233 -0
- package/dist-server/openmausbot.js +3671 -0
- package/dist-server/pair-cli.js +3671 -0
- package/dist-server/permission-proxy.js +126 -0
- package/dist-server/prepare-cloudflared.js +253 -0
- package/dist-server/proxy-paths.js +29 -0
- package/dist-server/server/atomic.js +40 -0
- package/dist-server/server/attachments.js +471 -0
- package/dist-server/server/auto-approve.js +162 -0
- package/dist-server/server/auto-review.js +79 -0
- package/dist-server/server/avatar-image.js +144 -0
- package/dist-server/server/bot-cwd.js +29 -0
- package/dist-server/server/bot-directory.js +127 -0
- package/dist-server/server/bot-package.js +268 -0
- package/dist-server/server/bot-profile.js +76 -0
- package/dist-server/server/box-create-idempotency.js +463 -0
- package/dist-server/server/box.js +944 -0
- package/dist-server/server/brand.js +65 -0
- package/dist-server/server/browser-connection.js +187 -0
- package/dist-server/server/browser-lifecycle-cleanup.js +359 -0
- package/dist-server/server/calendar-calls.js +314 -0
- package/dist-server/server/channel-queue.js +70 -0
- package/dist-server/server/checkpoints.js +405 -0
- package/dist-server/server/chief-of-staff.js +49 -0
- package/dist-server/server/claude-update.js +70 -0
- package/dist-server/server/cli.js +524 -0
- package/dist-server/server/cloud-backend.js +30 -0
- package/dist-server/server/comms-visibility.js +109 -0
- package/dist-server/server/composio.js +945 -0
- package/dist-server/server/computer-control.js +149 -0
- package/dist-server/server/computer-observation.js +133 -0
- package/dist-server/server/computer-proxy.js +1061 -0
- package/dist-server/server/config.js +760 -0
- package/dist-server/server/connector-proxy.js +228 -0
- package/dist-server/server/container-computer.js +840 -0
- package/dist-server/server/container-mcp.js +27 -0
- package/dist-server/server/contracts.js +24 -0
- package/dist-server/server/control-client.js +100 -0
- package/dist-server/server/data-dir-lease.js +3 -0
- package/dist-server/server/decision-log.js +106 -0
- package/dist-server/server/delegations.js +628 -0
- package/dist-server/server/drivers/acp/core.js +904 -0
- package/dist-server/server/drivers/acp/cursor.js +365 -0
- package/dist-server/server/drivers/acp/custom.js +51 -0
- package/dist-server/server/drivers/acp/droid.js +225 -0
- package/dist-server/server/drivers/acp/gemini.js +95 -0
- package/dist-server/server/drivers/acp/grok.js +249 -0
- package/dist-server/server/drivers/acp/hermes.js +429 -0
- package/dist-server/server/drivers/acp/kimi.js +559 -0
- package/dist-server/server/drivers/acp/opencode-go.js +370 -0
- package/dist-server/server/drivers/acp/qwen.js +98 -0
- package/dist-server/server/drivers/agents-proxy.js +941 -0
- package/dist-server/server/drivers/antigravity-acp.js +507 -0
- package/dist-server/server/drivers/antigravity-release.js +65 -0
- package/dist-server/server/drivers/antigravity-runtime.js +395 -0
- package/dist-server/server/drivers/antigravity.js +144 -0
- package/dist-server/server/drivers/boxagent.js +245 -0
- package/dist-server/server/drivers/browser-proxy.js +520 -0
- package/dist-server/server/drivers/builtIn.js +36 -0
- package/dist-server/server/drivers/claude.js +1337 -0
- package/dist-server/server/drivers/codex-catalog.js +409 -0
- package/dist-server/server/drivers/codex.js +1116 -0
- package/dist-server/server/drivers/dweb-proxy.js +193 -0
- package/dist-server/server/drivers/grok.js +48 -0
- package/dist-server/server/drivers/grokagent.js +4 -0
- package/dist-server/server/drivers/local-inject.js +388 -0
- package/dist-server/server/drivers/minimax.js +108 -0
- package/dist-server/server/drivers/native.js +21 -0
- package/dist-server/server/drivers/openai-chat.js +240 -0
- package/dist-server/server/drivers/openai-compat.js +143 -0
- package/dist-server/server/drivers/phone-proxy.js +372 -0
- package/dist-server/server/drivers/pi.js +855 -0
- package/dist-server/server/drivers/retry.js +96 -0
- package/dist-server/server/enterprise.js +93 -0
- package/dist-server/server/env-path.js +329 -0
- package/dist-server/server/environment.js +105 -0
- package/dist-server/server/generated-image.js +39 -0
- package/dist-server/server/graceful-shutdown.js +24 -0
- package/dist-server/server/group-goal-run.js +124 -0
- package/dist-server/server/harness/bus.js +86 -0
- package/dist-server/server/harness/registry.js +217 -0
- package/dist-server/server/index.js +11634 -0
- package/dist-server/server/installed-playbooks.js +34 -0
- package/dist-server/server/local-computer.js +278 -0
- package/dist-server/server/local-routing.js +9 -0
- package/dist-server/server/local-vm-idle.js +44 -0
- package/dist-server/server/local-vm-inventory.js +34 -0
- package/dist-server/server/local-vm-lease.js +58 -0
- package/dist-server/server/mcp-bridge.js +251 -0
- package/dist-server/server/mcp-probe.js +153 -0
- package/dist-server/server/mcp-registry.js +130 -0
- package/dist-server/server/member-turn.js +6 -0
- package/dist-server/server/message-db.js +358 -0
- package/dist-server/server/message-file.js +439 -0
- package/dist-server/server/names.js +26 -0
- package/dist-server/server/notify.js +69 -0
- package/dist-server/server/openmaus-status-capsule.js +329 -0
- package/dist-server/server/openmausbot.js +6 -0
- package/dist-server/server/package-export.js +157 -0
- package/dist-server/server/pair-cli.js +7 -0
- package/dist-server/server/peer-approval-key.js +5 -0
- package/dist-server/server/peer-approval.js +182 -0
- package/dist-server/server/peer-provenance.js +44 -0
- package/dist-server/server/peer-roster.js +139 -0
- package/dist-server/server/permission-proxy.js +157 -0
- package/dist-server/server/phone-secret.js +358 -0
- package/dist-server/server/private-screen-capture.js +17 -0
- package/dist-server/server/procs.js +123 -0
- package/dist-server/server/project-scout.js +304 -0
- package/dist-server/server/proxy-paths.js +47 -0
- package/dist-server/server/redact.js +102 -0
- package/dist-server/server/remote-computer.js +170 -0
- package/dist-server/server/repeat-detector.js +48 -0
- package/dist-server/server/replies.js +38 -0
- package/dist-server/server/request-auth.js +327 -0
- package/dist-server/server/room-cwd.js +15 -0
- package/dist-server/server/room-post-budget.js +112 -0
- package/dist-server/server/room-turn-timeout.js +115 -0
- package/dist-server/server/routine-requests.js +871 -0
- package/dist-server/server/routines.js +1111 -0
- package/dist-server/server/schema.js +11 -0
- package/dist-server/server/screen-frame-gate.js +80 -0
- package/dist-server/server/section-context.js +85 -0
- package/dist-server/server/send-idempotency.js +53 -0
- package/dist-server/server/sessions.js +319 -0
- package/dist-server/server/skill-fetch.js +148 -0
- package/dist-server/server/skill-learn.js +61 -0
- package/dist-server/server/skill-library.js +116 -0
- package/dist-server/server/skills.js +1223 -0
- package/dist-server/server/steer-queue.js +114 -0
- package/dist-server/server/store.js +1217 -0
- package/dist-server/server/surface.js +107 -0
- package/dist-server/server/tailscale.js +103 -0
- package/dist-server/server/team-backup.js +183 -0
- package/dist-server/server/team-library.js +205 -0
- package/dist-server/server/team-manifest.js +224 -0
- package/dist-server/server/thread-events.js +226 -0
- package/dist-server/server/tts/elevenlabs.js +92 -0
- package/dist-server/server/tts/index.js +79 -0
- package/dist-server/server/tts/speech-text.js +195 -0
- package/dist-server/server/tts/system-voices.js +74 -0
- package/dist-server/server/tunnel.js +202 -0
- package/dist-server/server/turn-context.js +40 -0
- package/dist-server/server/turn-dispatch-guard.js +152 -0
- package/dist-server/server/turn-images.js +138 -0
- package/dist-server/server/turn-watchdog.js +65 -0
- package/dist-server/server/vps-computer.js +1099 -0
- package/dist-server/server/vps-container-mcp.js +31 -0
- package/dist-server/server/webhook-ingress.js +173 -0
- package/dist-server/server/webhooks.js +491 -0
- package/dist-server/server/workspace.js +178 -0
- package/dist-server/shared/approval-mode.js +39 -0
- package/dist-server/shared/bot-avatar.js +33 -0
- package/dist-server/shared/bot-profile.js +7 -0
- package/dist-server/shared/connector-availability.js +12 -0
- package/dist-server/shared/credential-request.js +82 -0
- package/dist-server/shared/group-goal-run.js +1 -0
- package/dist-server/shared/import-name.js +11 -0
- package/dist-server/shared/mascot-bodies.js +107 -0
- package/dist-server/shared/routine-request.js +10 -0
- package/dist-server/shared/routine-run.js +1 -0
- package/dist-server/shared/skill-request.js +16 -0
- package/dist-server/shared/team-backup.js +148 -0
- package/dist-server/src/lib/composer-attachments.js +676 -0
- package/dist-server/tunnel-guardian.js +390 -0
- package/dist-server/vps-container-mcp.js +15209 -0
- package/package.json +32 -0
- package/skills/create-verification-skill/SKILL.md +65 -0
- package/skills/create-verification-skill/manifest.json +13 -0
- package/skills/phone-harness/SKILL.md +26 -0
- package/skills/phone-harness/agents/openai.yaml +13 -0
- package/skills/phone-harness/manifest.json +17 -0
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
// Cua-backed Local VM lifecycle and health checks.
|
|
2
|
+
//
|
|
3
|
+
// OpenMausBot owns only the sandbox boundary: image preparation, container
|
|
4
|
+
// lifecycle, resource limits, loopback viewer, and target-scoped lease in the
|
|
5
|
+
// harness. Desktop automation itself is Cua Driver. Agents connect directly to
|
|
6
|
+
// `cua-driver mcp` inside the container; this module never reimplements clicks,
|
|
7
|
+
// typing, screenshots, accessibility, or window discovery.
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
10
|
+
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join, resolve } from "node:path";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
|
+
import { augmentedPath, resolveCliSpawn } from "./env-path.js";
|
|
15
|
+
import { DATA_DIR } from "./config.js";
|
|
16
|
+
import { SPAWNED_PROXIES } from "./proxy-paths.js";
|
|
17
|
+
const run = promisify(execFile);
|
|
18
|
+
const SCREENSHOT_STATUS_TTL_MS = 10_000;
|
|
19
|
+
export const CUA_DRIVER_VERSION = "0.20.0";
|
|
20
|
+
export const BASE_IMAGE_REPOSITORY = "docker.io/trycua/xfce-cua";
|
|
21
|
+
// Official multi-architecture Cua XFCE 0.1.0 manifest (amd64 + arm64).
|
|
22
|
+
export const BASE_IMAGE_DIGEST = "sha256:274eb636f5cf3fc58f705916ee72b7a701270b3877369d08533a385c5325be9b";
|
|
23
|
+
export const BASE_IMAGE = `${BASE_IMAGE_REPOSITORY}@${BASE_IMAGE_DIGEST}`;
|
|
24
|
+
// This tag is built locally from the pinned Cua base. The explicit localhost
|
|
25
|
+
// registry is required by Podman: it prepends localhost to unqualified build
|
|
26
|
+
// tags, then may otherwise resolve the same name to Docker Hub when running it.
|
|
27
|
+
// Image and container labels below remain the authoritative compatibility
|
|
28
|
+
// check, not the mutable tag.
|
|
29
|
+
export const IMAGE_REPOSITORY = "localhost/openmausbot/cua-local-vm";
|
|
30
|
+
export const IMAGE_LAYER_VERSION = "4";
|
|
31
|
+
export const IMAGE_LAYER_LABEL = "com.openmausbot.image-layer";
|
|
32
|
+
export const IMAGE = `${IMAGE_REPOSITORY}:driver-${CUA_DRIVER_VERSION}-v${IMAGE_LAYER_VERSION}`;
|
|
33
|
+
export const CONTAINER = "openmausbot-computer";
|
|
34
|
+
export const MANAGED_LABEL = "com.openmausbot.local-vm";
|
|
35
|
+
export const DRIVER_LABEL = "com.openmausbot.cua-driver";
|
|
36
|
+
export const BASE_IMAGE_LABEL = "com.openmausbot.cua-base";
|
|
37
|
+
export const WORKSPACE_LABEL = "com.openmausbot.workspace";
|
|
38
|
+
export const TARGET_LABEL = "com.openmausbot.local-vm-target";
|
|
39
|
+
export const VM_WORKSPACE_DIR = join(DATA_DIR, "vm-home");
|
|
40
|
+
export const VM_WORKSPACE_GUEST = "/home/cua/workspace";
|
|
41
|
+
export const DISPLAY = ":1";
|
|
42
|
+
export const CUA_SOCKET = "/run/user/1000/openmausbot-cua.sock";
|
|
43
|
+
export const CUA_EXECUTABLE = "/usr/local/libexec/openmausbot/cua-driver";
|
|
44
|
+
const RUNTIMES = ["docker", "podman", "container"];
|
|
45
|
+
const INTERNAL_VIEWER_PORT = 6901;
|
|
46
|
+
const HOST_VIEWER_PORT = 6080;
|
|
47
|
+
const MEMORY_BYTES = 4 * 1024 * 1024 * 1024;
|
|
48
|
+
const NANO_CPUS = 2_000_000_000;
|
|
49
|
+
const PIDS_LIMIT = 512;
|
|
50
|
+
const SHM_BYTES = 512 * 1024 * 1024;
|
|
51
|
+
export const SHARED_LOCAL_VM_TARGET = {
|
|
52
|
+
key: "shared",
|
|
53
|
+
containerName: CONTAINER,
|
|
54
|
+
workspaceDir: VM_WORKSPACE_DIR,
|
|
55
|
+
viewerPort: HOST_VIEWER_PORT,
|
|
56
|
+
label: "shared",
|
|
57
|
+
};
|
|
58
|
+
/** Derive filesystem/container identities from a digest, never from a bot's
|
|
59
|
+
* display name or caller-controlled path fragment. */
|
|
60
|
+
export function perBotLocalVmTarget(botId) {
|
|
61
|
+
const digest = createHash("sha256").update(botId).digest("hex");
|
|
62
|
+
const short = digest.slice(0, 16);
|
|
63
|
+
return {
|
|
64
|
+
key: `bot:${digest}`,
|
|
65
|
+
containerName: `${CONTAINER}-${short}`,
|
|
66
|
+
workspaceDir: join(DATA_DIR, "vm-homes", short),
|
|
67
|
+
viewerPort: null,
|
|
68
|
+
label: digest,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const LINUX_WHEELS = {
|
|
72
|
+
x86_64: {
|
|
73
|
+
url: "https://files.pythonhosted.org/packages/fa/d7/a43008a328a40c85e7bc706fc20235b9abedc75e28b413817655153157ff/cua_driver-0.20.0-py3-none-manylinux_2_31_x86_64.whl",
|
|
74
|
+
sha256: "f60c35696a37f37ac954935e478ae4754f220856d022036625c9400d72185961",
|
|
75
|
+
},
|
|
76
|
+
aarch64: {
|
|
77
|
+
url: "https://files.pythonhosted.org/packages/94/9d/1c1838b69067e83266c3d2aae02d74eef353a43dc8644884ccf03fe7f933/cua_driver-0.20.0-py3-none-manylinux_2_31_aarch64.whl",
|
|
78
|
+
sha256: "48833bc5e4c60e701fc9eefb57dbac36ec77ef3990f816fbbe85b4e954af2c77",
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
/** Reproducible, multi-architecture derivative of Cua's sandbox desktop.
|
|
82
|
+
* Both Linux wheels are exact-version and SHA-256 verified. Supervisor owns
|
|
83
|
+
* the daemon so it starts, restarts, and stops with the desktop container.
|
|
84
|
+
*
|
|
85
|
+
* The first RUN also rejects a defective base image before anything uses it:
|
|
86
|
+
* some published ARM64 layers of upstream bases have shipped zero-byte
|
|
87
|
+
* OpenSSL libraries, which surfaces later as a baffling "curl: error while
|
|
88
|
+
* loading shared libraries … file too short" that reads as a network fault.
|
|
89
|
+
* The gate names the actual problem at the step that can act on it. */
|
|
90
|
+
export function managedImageDockerfile() {
|
|
91
|
+
return `FROM ${BASE_IMAGE}
|
|
92
|
+
USER root
|
|
93
|
+
RUN set -eux; \\
|
|
94
|
+
arch="$(uname -m)"; \\
|
|
95
|
+
case "$arch" in \\
|
|
96
|
+
x86_64) wheel_url='${LINUX_WHEELS.x86_64.url}'; wheel_sha='${LINUX_WHEELS.x86_64.sha256}'; wheel_path='/tmp/cua_driver-${CUA_DRIVER_VERSION}-py3-none-manylinux_2_31_x86_64.whl'; lib_triplet='x86_64-linux-gnu' ;; \\
|
|
97
|
+
aarch64|arm64) wheel_url='${LINUX_WHEELS.aarch64.url}'; wheel_sha='${LINUX_WHEELS.aarch64.sha256}'; wheel_path='/tmp/cua_driver-${CUA_DRIVER_VERSION}-py3-none-manylinux_2_31_aarch64.whl'; lib_triplet='aarch64-linux-gnu' ;; \\
|
|
98
|
+
*) echo "unsupported architecture: $arch" >&2; exit 1 ;; \\
|
|
99
|
+
esac; \\
|
|
100
|
+
for ssl_lib in "/lib/$lib_triplet/libssl.so.3" "/lib/$lib_triplet/libcrypto.so.3"; do \\
|
|
101
|
+
if [ -e "$ssl_lib" ] && [ ! -s "$ssl_lib" ]; then \\
|
|
102
|
+
echo "pinned base image is defective on $arch: $ssl_lib is zero bytes, so curl cannot start — re-pull or replace the base image instead of debugging the wheel download" >&2; \\
|
|
103
|
+
exit 1; \\
|
|
104
|
+
fi; \\
|
|
105
|
+
done; \\
|
|
106
|
+
curl -fsSL "$wheel_url" -o "$wheel_path"; \\
|
|
107
|
+
echo "$wheel_sha $wheel_path" | sha256sum -c -; \\
|
|
108
|
+
/opt/venv/bin/python -m pip install --no-cache-dir --force-reinstall --no-deps "$wheel_path"; \\
|
|
109
|
+
rm -f "$wheel_path"; \\
|
|
110
|
+
driver_bin="$(find /opt/venv/lib -path '*/cua_driver/bin/cua-driver' -type f -print -quit)"; \\
|
|
111
|
+
test -n "$driver_bin"; \\
|
|
112
|
+
install -D -m 0755 "$driver_bin" ${CUA_EXECUTABLE}; \\
|
|
113
|
+
install -d -o cua -g cua -m 0700 ${VM_WORKSPACE_GUEST}; \\
|
|
114
|
+
test "$(${CUA_EXECUTABLE} --version)" = "cua-driver ${CUA_DRIVER_VERSION}"
|
|
115
|
+
RUN printf '%s\\n' \\
|
|
116
|
+
'#!/bin/sh' \\
|
|
117
|
+
'set -eu' \\
|
|
118
|
+
'workspace=${VM_WORKSPACE_GUEST}' \\
|
|
119
|
+
'profiles="$workspace/.browser-profiles"' \\
|
|
120
|
+
'mkdir -p "$profiles/google-chrome" "$profiles/chromium" "$HOME/.config"' \\
|
|
121
|
+
'if ! chmod 0700 "$workspace" "$profiles" "$profiles/google-chrome" "$profiles/chromium" 2>/dev/null; then' \\
|
|
122
|
+
' for directory in "$workspace" "$profiles" "$profiles/google-chrome" "$profiles/chromium"; do' \\
|
|
123
|
+
' test -r "$directory" && test -w "$directory" && test -x "$directory"' \\
|
|
124
|
+
' done' \\
|
|
125
|
+
'fi' \\
|
|
126
|
+
'migrate_profile() {' \\
|
|
127
|
+
' name="$1"' \\
|
|
128
|
+
' source="$HOME/.config/$name"' \\
|
|
129
|
+
' target="$profiles/$name"' \\
|
|
130
|
+
' if [ -d "$source" ] && [ ! -L "$source" ] && [ -z "$(find "$target" -mindepth 1 -print -quit)" ]; then' \\
|
|
131
|
+
' cp -a "$source"/. "$target"/' \\
|
|
132
|
+
' fi' \\
|
|
133
|
+
' rm -rf "$source"' \\
|
|
134
|
+
' ln -s "$target" "$source"' \\
|
|
135
|
+
'}' \\
|
|
136
|
+
'migrate_profile google-chrome' \\
|
|
137
|
+
'migrate_profile chromium' \\
|
|
138
|
+
'find "$profiles" \\( -name SingletonLock -o -name SingletonSocket -o -name SingletonCookie -o -name .parentlock \\) -delete' \\
|
|
139
|
+
> /usr/local/bin/prepare-openmausbot-workspace.sh \\
|
|
140
|
+
&& chmod 0755 /usr/local/bin/prepare-openmausbot-workspace.sh
|
|
141
|
+
RUN printf '%s\\n' \\
|
|
142
|
+
'#!/bin/sh' \\
|
|
143
|
+
'/usr/local/bin/prepare-openmausbot-workspace.sh' \\
|
|
144
|
+
'attempt=0' \\
|
|
145
|
+
'until DISPLAY=:1 xset q >/dev/null 2>&1; do' \\
|
|
146
|
+
' attempt=$((attempt + 1))' \\
|
|
147
|
+
' if [ "$attempt" -ge 45 ]; then echo "X display :1 did not become ready within 45 seconds" >&2; exit 1; fi' \\
|
|
148
|
+
' sleep 1' \\
|
|
149
|
+
'done' \\
|
|
150
|
+
'exec env CUA_DRIVER_INSTALL_CHANNEL=python_package CUA_DRIVER_RS_TELEMETRY_ENABLED=0 ${CUA_EXECUTABLE} serve --socket ${CUA_SOCKET} --permission-mode standard' \\
|
|
151
|
+
> /usr/local/bin/start-openmausbot-cua-driver.sh \\
|
|
152
|
+
&& chmod 0755 /usr/local/bin/start-openmausbot-cua-driver.sh
|
|
153
|
+
RUN printf '%s\\n' \\
|
|
154
|
+
'' \\
|
|
155
|
+
'[program:openmausbot-cua-driver]' \\
|
|
156
|
+
'command=/usr/local/bin/start-openmausbot-cua-driver.sh' \\
|
|
157
|
+
'user=cua' \\
|
|
158
|
+
'environment=HOME="/home/cua",USER="cua",DISPLAY=":1"' \\
|
|
159
|
+
'autorestart=true' \\
|
|
160
|
+
'startsecs=2' \\
|
|
161
|
+
'stdout_logfile=/var/log/supervisor/cua-driver.log' \\
|
|
162
|
+
'stderr_logfile=/var/log/supervisor/cua-driver.error.log' \\
|
|
163
|
+
'priority=30' \\
|
|
164
|
+
>> /etc/supervisor/supervisord.conf
|
|
165
|
+
LABEL ${MANAGED_LABEL}="1" \\
|
|
166
|
+
${DRIVER_LABEL}="${CUA_DRIVER_VERSION}" \\
|
|
167
|
+
${BASE_IMAGE_LABEL}="${BASE_IMAGE_DIGEST}" \\
|
|
168
|
+
${IMAGE_LAYER_LABEL}="${IMAGE_LAYER_VERSION}"
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
async function sh(cmd, args, timeout = 8000) {
|
|
172
|
+
const resolved = resolveCliSpawn(cmd, args);
|
|
173
|
+
const { stdout } = await run(resolved.command, resolved.args, {
|
|
174
|
+
timeout,
|
|
175
|
+
encoding: "utf8",
|
|
176
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
177
|
+
env: { ...process.env, PATH: augmentedPath() },
|
|
178
|
+
});
|
|
179
|
+
return { stdout };
|
|
180
|
+
}
|
|
181
|
+
async function installed(cmd, runner, platform) {
|
|
182
|
+
try {
|
|
183
|
+
await runner(platform === "win32" ? "where.exe" : "/usr/bin/which", [cmd], 4000);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** Inspect only the host runtime. Unlike a full Local VM status check, this
|
|
191
|
+
* never opens a container, calls Cua, or reads a desktop screenshot. */
|
|
192
|
+
export async function containerRuntimeStatus(runner = sh, platform = process.platform) {
|
|
193
|
+
// Podman is the supported Windows VM lane and owns the pinned managed image.
|
|
194
|
+
// Docker may also be installed and healthy on the same host, so the generic
|
|
195
|
+
// Docker-first order would silently select an empty, unrelated image store.
|
|
196
|
+
const candidates = platform === "win32"
|
|
197
|
+
? ["podman", "docker"]
|
|
198
|
+
: RUNTIMES.filter((runtime) => runtime !== "container" || platform === "darwin");
|
|
199
|
+
const present = await Promise.all(candidates.map((runtime) => installed(runtime, runner, platform)));
|
|
200
|
+
const available = candidates.filter((_, index) => present[index]);
|
|
201
|
+
const healthy = await Promise.all(available.map(async (candidate) => {
|
|
202
|
+
try {
|
|
203
|
+
const infoArgs = candidate === "container"
|
|
204
|
+
? ["system", "status"]
|
|
205
|
+
: candidate === "podman"
|
|
206
|
+
? ["info", "--format", "json"]
|
|
207
|
+
: ["info", "--format", "{{.ServerVersion}}"];
|
|
208
|
+
await runner(candidate, infoArgs, 10_000);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}));
|
|
215
|
+
const healthyIndex = healthy.indexOf(true);
|
|
216
|
+
return {
|
|
217
|
+
runtime: healthyIndex >= 0 ? available[healthyIndex] : (available[0] ?? null),
|
|
218
|
+
available,
|
|
219
|
+
daemonUp: healthyIndex >= 0,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function emptyStatus(platform, target) {
|
|
223
|
+
return {
|
|
224
|
+
platform,
|
|
225
|
+
runtime: null,
|
|
226
|
+
available: [],
|
|
227
|
+
daemonUp: false,
|
|
228
|
+
image: false,
|
|
229
|
+
imageMatches: false,
|
|
230
|
+
managed: false,
|
|
231
|
+
container: "missing",
|
|
232
|
+
network: "unknown",
|
|
233
|
+
security: "unknown",
|
|
234
|
+
persistence: "unknown",
|
|
235
|
+
desktopReady: false,
|
|
236
|
+
desktop_error: null,
|
|
237
|
+
create_supported: true,
|
|
238
|
+
ready: false,
|
|
239
|
+
problem: "Install a supported container runtime first",
|
|
240
|
+
image_ref: IMAGE,
|
|
241
|
+
image_id: null,
|
|
242
|
+
base_image_ref: BASE_IMAGE,
|
|
243
|
+
driver_version: CUA_DRIVER_VERSION,
|
|
244
|
+
container_name: target.containerName,
|
|
245
|
+
target_key: target.key,
|
|
246
|
+
workspace_path: target.workspaceDir,
|
|
247
|
+
workspace_guest_path: VM_WORKSPACE_GUEST,
|
|
248
|
+
viewer_port: target.viewerPort,
|
|
249
|
+
viewer_url: target.viewerPort ? `http://127.0.0.1:${target.viewerPort}/vnc.html` : "",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/** Whether a turn may recreate this Local VM itself instead of failing.
|
|
253
|
+
*
|
|
254
|
+
* True for exactly one state: the container is gone, and a plain `run` is all
|
|
255
|
+
* that is needed to bring it back. That is what `LocalVmIdleTimer` leaves
|
|
256
|
+
* behind — it removes an unused Local VM rather than pausing it — so a turn
|
|
257
|
+
* arriving after an idle period should not have to send the person to App
|
|
258
|
+
* Settings for a container the app itself deleted.
|
|
259
|
+
*
|
|
260
|
+
* Every other problem in `statusProblem` stays the person's call and returns
|
|
261
|
+
* false here: no runtime, daemon down, image never prepared, `create_supported`
|
|
262
|
+
* false, and any existing container — stale image, unmanaged, unsafe network,
|
|
263
|
+
* security or persistence. A stopped container is excluded deliberately, since
|
|
264
|
+
* `statusProblem` says this desktop image cannot safely resume and asks for a
|
|
265
|
+
* recreate rather than a start.
|
|
266
|
+
*/
|
|
267
|
+
export function localVmRecreatableOnDemand(status) {
|
|
268
|
+
return Boolean(status.runtime)
|
|
269
|
+
&& status.daemonUp
|
|
270
|
+
&& status.image
|
|
271
|
+
&& status.container === "missing"
|
|
272
|
+
&& status.create_supported;
|
|
273
|
+
}
|
|
274
|
+
function statusProblem(status) {
|
|
275
|
+
if (!status.runtime)
|
|
276
|
+
return "Install a supported container runtime first";
|
|
277
|
+
if (!status.daemonUp)
|
|
278
|
+
return `Start ${status.runtime} first`;
|
|
279
|
+
if (!status.image)
|
|
280
|
+
return `Prepare the Cua desktop image with Driver ${CUA_DRIVER_VERSION}`;
|
|
281
|
+
if (status.container === "missing" && !status.create_supported) {
|
|
282
|
+
return "Per-bot Local VMs require Docker or Podman because Apple container requires a fixed host port";
|
|
283
|
+
}
|
|
284
|
+
if (status.container === "missing")
|
|
285
|
+
return "Create the Local VM";
|
|
286
|
+
if (!status.imageMatches)
|
|
287
|
+
return "The existing Local VM uses an older desktop or Cua Driver; recreate it";
|
|
288
|
+
if (!status.managed)
|
|
289
|
+
return "The existing container was not created by OpenMausBot; recreate it";
|
|
290
|
+
if (status.network === "unsafe")
|
|
291
|
+
return "The existing Local VM exposes its viewer publicly; recreate it";
|
|
292
|
+
if (status.security === "unsafe")
|
|
293
|
+
return "The existing Local VM is missing safety limits; recreate it";
|
|
294
|
+
if (status.persistence === "unsafe")
|
|
295
|
+
return "The existing Local VM is missing its durable workspace; recreate it";
|
|
296
|
+
if (status.container === "stopped")
|
|
297
|
+
return "This desktop image cannot safely resume; recreate the Local VM";
|
|
298
|
+
if (status.desktop_error)
|
|
299
|
+
return `The Local VM desktop failed to start: ${status.desktop_error}`;
|
|
300
|
+
if (!status.desktopReady)
|
|
301
|
+
return "The Local VM started, but Cua Driver is not ready yet";
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
/** Shared with the BYO-VPS backend (vps-computer.ts): both containers are
|
|
305
|
+
* built from the same pinned derivative, so image compatibility is one rule. */
|
|
306
|
+
export function imageLabelsMatch(labels) {
|
|
307
|
+
return (labels?.[MANAGED_LABEL] === "1" &&
|
|
308
|
+
labels?.[DRIVER_LABEL] === CUA_DRIVER_VERSION &&
|
|
309
|
+
labels?.[BASE_IMAGE_LABEL] === BASE_IMAGE_DIGEST &&
|
|
310
|
+
labels?.[IMAGE_LAYER_LABEL] === IMAGE_LAYER_VERSION);
|
|
311
|
+
}
|
|
312
|
+
/** Ownership is intentionally independent of the current image/driver
|
|
313
|
+
* versions. An older OpenMausBot container must stay removable (and eligible
|
|
314
|
+
* for idle cleanup), while imageMatches keeps readiness version-strict. */
|
|
315
|
+
function containerOwnershipLabelsMatch(labels, target) {
|
|
316
|
+
return (labels?.[MANAGED_LABEL] === "1" &&
|
|
317
|
+
labels?.[WORKSPACE_LABEL] === "1" &&
|
|
318
|
+
(target.key === SHARED_LOCAL_VM_TARGET.key
|
|
319
|
+
? labels?.[TARGET_LABEL] === undefined || labels?.[TARGET_LABEL] === target.label
|
|
320
|
+
: labels?.[TARGET_LABEL] === target.label));
|
|
321
|
+
}
|
|
322
|
+
function normalizeImageId(id) {
|
|
323
|
+
return id?.trim().replace(/^sha256:/, "") || null;
|
|
324
|
+
}
|
|
325
|
+
function inspectedImage(stdout) {
|
|
326
|
+
const parsed = JSON.parse(stdout);
|
|
327
|
+
const image = parsed[0];
|
|
328
|
+
return {
|
|
329
|
+
labels: image?.Config?.Labels ?? image?.config?.Labels ?? image?.config?.labels ?? image?.configuration?.labels,
|
|
330
|
+
id: normalizeImageId(image?.Id ?? image?.id ?? image?.configuration?.descriptor?.digest),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function viewerPassword(env) {
|
|
334
|
+
if (Array.isArray(env)) {
|
|
335
|
+
return env.find((entry) => entry.startsWith("VNC_PW="))?.slice("VNC_PW=".length) || null;
|
|
336
|
+
}
|
|
337
|
+
return env?.VNC_PW || null;
|
|
338
|
+
}
|
|
339
|
+
function viewerUrl(password, port) {
|
|
340
|
+
if (!port)
|
|
341
|
+
return "";
|
|
342
|
+
const base = `http://127.0.0.1:${port}/vnc.html`;
|
|
343
|
+
if (!password)
|
|
344
|
+
return base;
|
|
345
|
+
const fragment = new URLSearchParams({ autoconnect: "true", resize: "scale", password });
|
|
346
|
+
return `${base}#${fragment.toString()}`;
|
|
347
|
+
}
|
|
348
|
+
/** The one authoritative `exec … cua-driver` argv. Shared with the BYO-VPS
|
|
349
|
+
* backend and both MCP bridge entry points so the identity, env, and
|
|
350
|
+
* telemetry knobs can never drift between the Local VM and a VPS container. */
|
|
351
|
+
export function cuaExecArgs(args, options = {}) {
|
|
352
|
+
return [
|
|
353
|
+
"exec",
|
|
354
|
+
...(options.interactive ? ["-i"] : []),
|
|
355
|
+
"-u",
|
|
356
|
+
"cua",
|
|
357
|
+
"-e",
|
|
358
|
+
"HOME=/home/cua",
|
|
359
|
+
"-e",
|
|
360
|
+
`DISPLAY=${DISPLAY}`,
|
|
361
|
+
"-e",
|
|
362
|
+
"CUA_DRIVER_INSTALL_CHANNEL=python_package",
|
|
363
|
+
"-e",
|
|
364
|
+
"CUA_DRIVER_RS_TELEMETRY_ENABLED=0",
|
|
365
|
+
options.container ?? CONTAINER,
|
|
366
|
+
CUA_EXECUTABLE,
|
|
367
|
+
...args,
|
|
368
|
+
];
|
|
369
|
+
}
|
|
370
|
+
export async function containerComputerStatus(runner = sh, platform = process.platform, target = SHARED_LOCAL_VM_TARGET) {
|
|
371
|
+
const status = emptyStatus(platform, target);
|
|
372
|
+
const runtimeStatus = await containerRuntimeStatus(runner, platform);
|
|
373
|
+
status.available = runtimeStatus.available;
|
|
374
|
+
status.runtime = runtimeStatus.runtime;
|
|
375
|
+
status.daemonUp = runtimeStatus.daemonUp;
|
|
376
|
+
status.create_supported = target.key === SHARED_LOCAL_VM_TARGET.key || status.runtime !== "container";
|
|
377
|
+
if (!status.runtime || !status.daemonUp) {
|
|
378
|
+
status.problem = statusProblem(status);
|
|
379
|
+
return status;
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const { stdout } = await runner(status.runtime, ["image", "inspect", IMAGE]);
|
|
383
|
+
const image = inspectedImage(stdout);
|
|
384
|
+
status.image = imageLabelsMatch(image.labels);
|
|
385
|
+
status.image_id = image.id;
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
// The prepared OpenMausBot derivative has not been built yet.
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
const { stdout } = await runner(status.runtime, ["inspect", target.containerName]);
|
|
392
|
+
if (status.runtime === "container") {
|
|
393
|
+
const inspected = JSON.parse(stdout);
|
|
394
|
+
const detail = inspected[0];
|
|
395
|
+
status.container = detail?.status?.state === "running" ? "running" : "stopped";
|
|
396
|
+
status.network = applePortsAreLocal(detail?.configuration?.publishedPorts) ? "loopback" : "unsafe";
|
|
397
|
+
status.viewer_port = appleViewerPort(detail?.configuration?.publishedPorts, target.viewerPort);
|
|
398
|
+
const appleImage = typeof detail?.configuration?.image === "string"
|
|
399
|
+
? detail.configuration.image
|
|
400
|
+
: detail?.configuration?.image?.reference ?? detail?.configuration?.imageReference;
|
|
401
|
+
const appleImageId = typeof detail?.configuration?.image === "object"
|
|
402
|
+
? normalizeImageId(detail.configuration.image.descriptor?.digest)
|
|
403
|
+
: null;
|
|
404
|
+
status.imageMatches =
|
|
405
|
+
appleImage === IMAGE && status.image_id !== null && appleImageId === status.image_id;
|
|
406
|
+
status.managed = containerOwnershipLabelsMatch(detail?.configuration?.labels, target);
|
|
407
|
+
status.persistence = appleWorkspaceMountIsSafe(detail?.configuration?.mounts, platform, target.workspaceDir)
|
|
408
|
+
? "durable"
|
|
409
|
+
: "unsafe";
|
|
410
|
+
const resources = detail?.configuration?.resources;
|
|
411
|
+
status.security =
|
|
412
|
+
(resources?.memoryInBytes ?? 0) >= MEMORY_BYTES && resources?.cpus === 2 ? "hardened" : "unsafe";
|
|
413
|
+
status.viewer_url = viewerUrl(viewerPassword(detail?.configuration?.environment), status.viewer_port);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
const inspected = JSON.parse(stdout);
|
|
417
|
+
const detail = inspected[0];
|
|
418
|
+
status.container = detail?.State?.Running ? "running" : "stopped";
|
|
419
|
+
status.network = dockerPortsAreLocal(detail?.HostConfig?.PortBindings) ? "loopback" : "unsafe";
|
|
420
|
+
status.viewer_port = dockerViewerPort(detail?.NetworkSettings?.Ports, target.viewerPort);
|
|
421
|
+
status.imageMatches =
|
|
422
|
+
detail?.Config?.Image === IMAGE &&
|
|
423
|
+
imageLabelsMatch(detail?.Config?.Labels) &&
|
|
424
|
+
status.image_id !== null &&
|
|
425
|
+
normalizeImageId(detail?.Image) === status.image_id;
|
|
426
|
+
status.managed = containerOwnershipLabelsMatch(detail?.Config?.Labels, target);
|
|
427
|
+
status.persistence = dockerWorkspaceMountIsSafe(detail?.Mounts, platform, target.workspaceDir, status.runtime) ? "durable" : "unsafe";
|
|
428
|
+
status.security = (status.runtime === "podman"
|
|
429
|
+
? podmanSecurityIsHardened(detail?.HostConfig, detail?.EffectiveCaps, detail?.BoundingCaps)
|
|
430
|
+
: dockerSecurityIsHardened(detail?.HostConfig)) ? "hardened" : "unsafe";
|
|
431
|
+
status.viewer_url = viewerUrl(viewerPassword(detail?.Config?.Env), status.viewer_port);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
// No container with this name.
|
|
436
|
+
}
|
|
437
|
+
const canProbe = status.container === "running" &&
|
|
438
|
+
status.imageMatches &&
|
|
439
|
+
status.managed &&
|
|
440
|
+
status.network === "loopback" &&
|
|
441
|
+
status.security === "hardened" &&
|
|
442
|
+
status.persistence === "durable";
|
|
443
|
+
if (canProbe) {
|
|
444
|
+
try {
|
|
445
|
+
const expected = `cua-driver ${CUA_DRIVER_VERSION}`;
|
|
446
|
+
const version = await runner(status.runtime, cuaExecArgs(["--version"], { container: target.containerName }), 8000);
|
|
447
|
+
if (version.stdout.trim() !== expected)
|
|
448
|
+
throw new Error(`expected ${expected}`);
|
|
449
|
+
await runner(status.runtime, cuaExecArgs(["status", "--socket", CUA_SOCKET], { container: target.containerName }), 8000);
|
|
450
|
+
const health = await runner(status.runtime, cuaExecArgs(["call", "health_report", "{}", "--socket", CUA_SOCKET], { container: target.containerName }), 15_000);
|
|
451
|
+
const report = JSON.parse(health.stdout);
|
|
452
|
+
if (report.schema_version !== "1" ||
|
|
453
|
+
!Array.isArray(report.checks) ||
|
|
454
|
+
(report.overall !== "ok" && report.overall !== "degraded")) {
|
|
455
|
+
throw new Error(`Cua health report is ${report.overall ?? "invalid"}`);
|
|
456
|
+
}
|
|
457
|
+
const readinessShot = "/tmp/openmausbot-readiness.png";
|
|
458
|
+
await runner(status.runtime, cuaExecArgs([
|
|
459
|
+
"call",
|
|
460
|
+
"get_desktop_state",
|
|
461
|
+
"{}",
|
|
462
|
+
"--socket",
|
|
463
|
+
CUA_SOCKET,
|
|
464
|
+
"--screenshot-out-file",
|
|
465
|
+
readinessShot,
|
|
466
|
+
], { container: target.containerName }), 20_000);
|
|
467
|
+
const captured = await runner(status.runtime, ["exec", target.containerName, "base64", "-w0", readinessShot], 20_000);
|
|
468
|
+
if (!wholeScreenshot(Buffer.from(captured.stdout.trim(), "base64")).ok) {
|
|
469
|
+
throw new Error("Cua Driver returned an incomplete readiness screenshot");
|
|
470
|
+
}
|
|
471
|
+
status.desktopReady = true;
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
// An empty log means XFCE and the supervisor-owned Cua daemon are
|
|
475
|
+
// probably still starting. A real startup failure should be actionable
|
|
476
|
+
// in the panel instead of looking like an endless readiness wait.
|
|
477
|
+
status.desktop_error = error instanceof Error ? error.message.slice(0, 320) : null;
|
|
478
|
+
try {
|
|
479
|
+
const errorLog = await runner(status.runtime, ["exec", target.containerName, "tail", "-n", "4", "/var/log/supervisor/cua-driver.error.log"], 4000);
|
|
480
|
+
status.desktop_error =
|
|
481
|
+
errorLog.stdout.replace(/\s+/g, " ").trim().slice(0, 320) ||
|
|
482
|
+
status.desktop_error;
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
// The log may not exist during the first seconds of container boot.
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
status.problem = statusProblem(status);
|
|
490
|
+
status.ready = status.problem === null;
|
|
491
|
+
return status;
|
|
492
|
+
}
|
|
493
|
+
function loopback(address) {
|
|
494
|
+
return address === "127.0.0.1" || address === "::1" || address === "[::1]";
|
|
495
|
+
}
|
|
496
|
+
function dockerPortsAreLocal(bindings) {
|
|
497
|
+
const viewer = bindings?.[`${INTERNAL_VIEWER_PORT}/tcp`] ?? [];
|
|
498
|
+
const published = Object.values(bindings ?? {}).flatMap((entries) => entries ?? []);
|
|
499
|
+
return viewer.length > 0 && published.length === viewer.length && published.every((entry) => loopback(entry.HostIp));
|
|
500
|
+
}
|
|
501
|
+
function dockerViewerPort(bindings, fallback) {
|
|
502
|
+
const raw = bindings?.[`${INTERNAL_VIEWER_PORT}/tcp`]?.find((entry) => loopback(entry.HostIp))?.HostPort;
|
|
503
|
+
const parsed = raw ? Number(raw) : NaN;
|
|
504
|
+
return Number.isInteger(parsed) && parsed > 0 && parsed <= 65_535 ? parsed : fallback;
|
|
505
|
+
}
|
|
506
|
+
function applePortsAreLocal(bindings) {
|
|
507
|
+
return Boolean(bindings?.length === 1 &&
|
|
508
|
+
bindings[0]?.containerPort === INTERNAL_VIEWER_PORT &&
|
|
509
|
+
loopback(bindings[0]?.hostAddress));
|
|
510
|
+
}
|
|
511
|
+
function appleViewerPort(bindings, fallback) {
|
|
512
|
+
const raw = bindings?.find((binding) => binding.containerPort === INTERNAL_VIEWER_PORT && loopback(binding.hostAddress))?.hostPort;
|
|
513
|
+
return Number.isInteger(raw) && Number(raw) > 0 && Number(raw) <= 65_535 ? Number(raw) : fallback;
|
|
514
|
+
}
|
|
515
|
+
function sameWorkspaceSource(source, platform, expectedWorkspace) {
|
|
516
|
+
if (!source)
|
|
517
|
+
return false;
|
|
518
|
+
const actual = resolve(source);
|
|
519
|
+
const expected = resolve(expectedWorkspace);
|
|
520
|
+
return platform === "win32" ? actual.toLowerCase() === expected.toLowerCase() : actual === expected;
|
|
521
|
+
}
|
|
522
|
+
/** Podman Machine exposes a Windows bind source through its WSL mount path.
|
|
523
|
+
* Accept only the exact drive/path translation; no parent or prefix match. */
|
|
524
|
+
function samePodmanWindowsWorkspaceSource(source, expectedWorkspace) {
|
|
525
|
+
if (!source)
|
|
526
|
+
return false;
|
|
527
|
+
const match = expectedWorkspace.match(/^([A-Za-z]):[\\/](.+)$/);
|
|
528
|
+
if (!match)
|
|
529
|
+
return false;
|
|
530
|
+
const expected = `/mnt/${match[1].toLowerCase()}/${match[2].replaceAll("\\", "/")}`;
|
|
531
|
+
const actual = source.replaceAll("\\", "/");
|
|
532
|
+
return actual.toLowerCase() === expected.toLowerCase();
|
|
533
|
+
}
|
|
534
|
+
function dockerWorkspaceMountIsSafe(mounts, platform, expectedWorkspace, runtime = "docker") {
|
|
535
|
+
const sourceMatches = sameWorkspaceSource(mounts?.[0]?.Source, platform, expectedWorkspace) ||
|
|
536
|
+
(runtime === "podman" &&
|
|
537
|
+
platform === "win32" &&
|
|
538
|
+
samePodmanWindowsWorkspaceSource(mounts?.[0]?.Source, expectedWorkspace));
|
|
539
|
+
return Boolean(mounts?.length === 1 &&
|
|
540
|
+
mounts[0]?.Type === "bind" &&
|
|
541
|
+
sourceMatches &&
|
|
542
|
+
mounts[0]?.Destination === VM_WORKSPACE_GUEST &&
|
|
543
|
+
mounts[0]?.RW !== false);
|
|
544
|
+
}
|
|
545
|
+
function appleWorkspaceMountIsSafe(mounts, platform, expectedWorkspace) {
|
|
546
|
+
const options = mounts?.[0]?.options ?? [];
|
|
547
|
+
return Boolean(mounts?.length === 1 &&
|
|
548
|
+
sameWorkspaceSource(mounts[0]?.source, platform, expectedWorkspace) &&
|
|
549
|
+
mounts[0]?.destination === VM_WORKSPACE_GUEST &&
|
|
550
|
+
!options.some((option) => option === "ro" || option === "readonly"));
|
|
551
|
+
}
|
|
552
|
+
/** One hardening contract for both managed containers (Local VM here, the
|
|
553
|
+
* BYO-VPS backend in vps-computer.ts): exact resource limits, no privilege,
|
|
554
|
+
* no host namespaces or devices, no disabled security profiles. The only
|
|
555
|
+
* knob the callers legitimately disagree on is the restart policy — the VPS
|
|
556
|
+
* container must survive a reboot nobody is watching ("unless-stopped"),
|
|
557
|
+
* while the Local VM must NOT auto-resume: its desktop leaves a stale X lock
|
|
558
|
+
* on stop, so a restarted container is a broken one. */
|
|
559
|
+
export function dockerSecurityIsHardened(config, options = {}) {
|
|
560
|
+
if (!config)
|
|
561
|
+
return false;
|
|
562
|
+
const capDrop = (config.CapDrop ?? []).map((cap) => cap.toLowerCase());
|
|
563
|
+
const capAdd = (config.CapAdd ?? [])
|
|
564
|
+
.map((cap) => cap.toLowerCase().replace(/^cap_/, ""))
|
|
565
|
+
.sort();
|
|
566
|
+
const unsafeSecurityOption = (config.SecurityOpt ?? []).some((option) => /(?:^|=)(?:unconfined|disable)$/i.test(option));
|
|
567
|
+
const restartPolicy = config.RestartPolicy?.Name;
|
|
568
|
+
const restartPolicyOk = options.restartPolicy === "unless-stopped"
|
|
569
|
+
? restartPolicy === "unless-stopped"
|
|
570
|
+
: restartPolicy === undefined || restartPolicy === "" || restartPolicy === "no";
|
|
571
|
+
return (config.Memory === MEMORY_BYTES &&
|
|
572
|
+
(config.MemorySwap ?? 0) === MEMORY_BYTES &&
|
|
573
|
+
(config.NanoCpus ?? 0) === NANO_CPUS &&
|
|
574
|
+
config.PidsLimit === PIDS_LIMIT &&
|
|
575
|
+
capDrop.includes("all") &&
|
|
576
|
+
capAdd.join(",") === "setgid,setuid" &&
|
|
577
|
+
config.Privileged === false &&
|
|
578
|
+
!config.PidMode &&
|
|
579
|
+
config.IpcMode === "private" &&
|
|
580
|
+
!config.UTSMode &&
|
|
581
|
+
config.ShmSize === SHM_BYTES &&
|
|
582
|
+
(!config.Devices || config.Devices.length === 0) &&
|
|
583
|
+
(!config.DeviceRequests || config.DeviceRequests.length === 0) &&
|
|
584
|
+
!unsafeSecurityOption &&
|
|
585
|
+
!config.UsernsMode &&
|
|
586
|
+
config.CgroupnsMode === "private" &&
|
|
587
|
+
config.OomKillDisable !== true &&
|
|
588
|
+
config.AutoRemove !== true &&
|
|
589
|
+
restartPolicyOk);
|
|
590
|
+
}
|
|
591
|
+
/** Podman normalizes HostConfig capability and namespace fields when it
|
|
592
|
+
* serializes inspect output. Validate its authoritative effective/bounding
|
|
593
|
+
* sets, then normalize only those known representation differences through
|
|
594
|
+
* the unchanged Docker hardening contract. */
|
|
595
|
+
export function podmanSecurityIsHardened(config, effectiveCaps, boundingCaps) {
|
|
596
|
+
if (!config)
|
|
597
|
+
return false;
|
|
598
|
+
const normalizeCaps = (caps) => (caps ?? [])
|
|
599
|
+
.map((cap) => cap.toLowerCase().replace(/^cap_/, ""))
|
|
600
|
+
.sort();
|
|
601
|
+
const exactCaps = "setgid,setuid";
|
|
602
|
+
if (normalizeCaps(effectiveCaps).join(",") !== exactCaps)
|
|
603
|
+
return false;
|
|
604
|
+
if (normalizeCaps(boundingCaps).join(",") !== exactCaps)
|
|
605
|
+
return false;
|
|
606
|
+
return dockerSecurityIsHardened({
|
|
607
|
+
...config,
|
|
608
|
+
CapDrop: ["all"],
|
|
609
|
+
CapAdd: effectiveCaps,
|
|
610
|
+
PidMode: config.PidMode === "private" ? "" : config.PidMode,
|
|
611
|
+
UTSMode: config.UTSMode === "private" ? "" : config.UTSMode,
|
|
612
|
+
CgroupnsMode: config.CgroupnsMode || "private",
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
export function containerRunArgs(runtime, password = "CHANGE_ME", target = SHARED_LOCAL_VM_TARGET) {
|
|
616
|
+
if (runtime === "container" && target.key !== SHARED_LOCAL_VM_TARGET.key) {
|
|
617
|
+
throw new Error("Per-bot Local VMs require Docker or Podman because Apple container requires a fixed host port");
|
|
618
|
+
}
|
|
619
|
+
const common = ["run", "-d", "--name", target.containerName];
|
|
620
|
+
common.push("--label", `${MANAGED_LABEL}=1`, "--label", `${DRIVER_LABEL}=${CUA_DRIVER_VERSION}`, "--label", `${BASE_IMAGE_LABEL}=${BASE_IMAGE_DIGEST}`, "--label", `${IMAGE_LAYER_LABEL}=${IMAGE_LAYER_VERSION}`, "--label", `${WORKSPACE_LABEL}=1`, "--label", `${TARGET_LABEL}=${target.label}`);
|
|
621
|
+
if (runtime === "container") {
|
|
622
|
+
// Apple container already places each Linux container in a lightweight VM.
|
|
623
|
+
common.push("--memory", "4g", "--cpus", "2", "--cap-drop", "ALL", "--cap-add", "SETUID", "--cap-add", "SETGID", "--shm-size", "512m");
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
common.push("--hostname", target.containerName, "--memory", "4g", "--memory-swap", "4g", "--cpus", "2", "--pids-limit", String(PIDS_LIMIT),
|
|
627
|
+
// Pinned explicitly rather than trusting daemon defaults: the shared
|
|
628
|
+
// hardening check requires private IPC and cgroup namespaces, and a
|
|
629
|
+
// daemon configured with host-mode defaults would otherwise create a
|
|
630
|
+
// container its own acceptance check then rejects.
|
|
631
|
+
"--ipc", "private", "--cgroupns", "private", "--cap-drop", "ALL", "--cap-add", "SETUID", "--cap-add", "SETGID", "--shm-size", "512m");
|
|
632
|
+
}
|
|
633
|
+
common.push("--mount", runtime === "podman"
|
|
634
|
+
? `type=bind,source=${target.workspaceDir},target=${VM_WORKSPACE_GUEST},relabel=private,U=true`
|
|
635
|
+
: `type=bind,source=${target.workspaceDir},target=${VM_WORKSPACE_GUEST}`, "-e", `VNC_PW=${password}`, "-p", target.viewerPort
|
|
636
|
+
? `127.0.0.1:${target.viewerPort}:${INTERNAL_VIEWER_PORT}`
|
|
637
|
+
: `127.0.0.1::${INTERNAL_VIEWER_PORT}`, IMAGE);
|
|
638
|
+
return common;
|
|
639
|
+
}
|
|
640
|
+
async function ensureVmWorkspace(platform, target) {
|
|
641
|
+
await mkdir(target.workspaceDir, { recursive: true, mode: 0o700 });
|
|
642
|
+
if (platform !== "win32")
|
|
643
|
+
await chmod(target.workspaceDir, 0o700);
|
|
644
|
+
}
|
|
645
|
+
async function prepareManagedImage(runtime, runner) {
|
|
646
|
+
await runner(runtime, ["pull", BASE_IMAGE], 10 * 60_000);
|
|
647
|
+
const context = await mkdtemp(join(tmpdir(), "openmausbot-cua-image-"));
|
|
648
|
+
try {
|
|
649
|
+
await writeFile(join(context, "Dockerfile"), managedImageDockerfile(), { mode: 0o600 });
|
|
650
|
+
await runner(runtime, ["build", "-t", IMAGE, context], 10 * 60_000);
|
|
651
|
+
}
|
|
652
|
+
finally {
|
|
653
|
+
await rm(context, { recursive: true, force: true });
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
export async function containerComputerAction(action, runner = sh, platform = process.platform, target = SHARED_LOCAL_VM_TARGET) {
|
|
657
|
+
if (runner === sh && platform === process.platform)
|
|
658
|
+
screenshotStatusCache.delete(target.key);
|
|
659
|
+
const before = await containerComputerStatus(runner, platform, target);
|
|
660
|
+
const runtime = before.runtime;
|
|
661
|
+
if (!runtime)
|
|
662
|
+
throw Object.assign(new Error(before.problem ?? "No container runtime is installed"), { status: 409 });
|
|
663
|
+
if (!before.daemonUp)
|
|
664
|
+
throw Object.assign(new Error(before.problem ?? `${runtime} is not running`), { status: 409 });
|
|
665
|
+
if (action === "run" && before.container !== "missing") {
|
|
666
|
+
throw Object.assign(new Error("A Local VM already exists; remove it before creating a replacement"), { status: 409 });
|
|
667
|
+
}
|
|
668
|
+
if (action === "run" && !before.image) {
|
|
669
|
+
throw Object.assign(new Error("Prepare the Cua desktop image before creating the Local VM"), { status: 409 });
|
|
670
|
+
}
|
|
671
|
+
if (action === "run" && !before.create_supported) {
|
|
672
|
+
throw Object.assign(new Error(before.problem ?? "This runtime cannot create a per-bot Local VM"), { status: 409 });
|
|
673
|
+
}
|
|
674
|
+
if (action === "start") {
|
|
675
|
+
throw Object.assign(new Error("This desktop image cannot safely resume; remove and recreate the Local VM"), {
|
|
676
|
+
status: 409,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
if (action === "stop" && before.container !== "running") {
|
|
680
|
+
throw Object.assign(new Error("The Local VM is not running"), { status: 409 });
|
|
681
|
+
}
|
|
682
|
+
if (action === "remove" && before.container === "missing")
|
|
683
|
+
return before;
|
|
684
|
+
if (action === "remove" && !before.managed) {
|
|
685
|
+
throw Object.assign(new Error(`The existing container named ${target.containerName} was not created by OpenMausBot; remove it manually in ${runtime}`), { status: 409 });
|
|
686
|
+
}
|
|
687
|
+
if (action === "pull") {
|
|
688
|
+
await prepareManagedImage(runtime, runner);
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
if (action === "run")
|
|
692
|
+
await ensureVmWorkspace(platform, target);
|
|
693
|
+
const args = action === "run"
|
|
694
|
+
? containerRunArgs(runtime, randomBytes(6).toString("base64url"), target)
|
|
695
|
+
: action === "remove"
|
|
696
|
+
? ["rm", runtime === "container" ? "--force" : "-f", target.containerName]
|
|
697
|
+
: [action, target.containerName];
|
|
698
|
+
await runner(runtime, args, 2 * 60_000);
|
|
699
|
+
}
|
|
700
|
+
return containerComputerStatus(runner, platform, target);
|
|
701
|
+
}
|
|
702
|
+
/** Cheap capacity probe used by the per-bot pool. It deliberately checks an
|
|
703
|
+
* exact derived container name rather than parsing a broad daemon listing. */
|
|
704
|
+
export async function containerComputerExists(runtime, target, runner = sh) {
|
|
705
|
+
try {
|
|
706
|
+
await runner(runtime, ["inspect", target.containerName], 8_000);
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/** Shared with the BYO-VPS backend: a truncated base64 transfer must never
|
|
714
|
+
* become a "successful" preview frame on either transport. */
|
|
715
|
+
export function wholeScreenshot(bytes) {
|
|
716
|
+
if (bytes.length < 512)
|
|
717
|
+
return { ok: false, mime: "image/png" };
|
|
718
|
+
const png = bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47;
|
|
719
|
+
if (png) {
|
|
720
|
+
return {
|
|
721
|
+
ok: bytes.subarray(Math.max(0, bytes.length - 12)).includes(Buffer.from("IEND", "ascii")),
|
|
722
|
+
mime: "image/png",
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
const jpeg = bytes[0] === 0xff && bytes[1] === 0xd8;
|
|
726
|
+
return {
|
|
727
|
+
ok: jpeg && bytes.subarray(Math.max(0, bytes.length - 32)).includes(Buffer.from([0xff, 0xd9])),
|
|
728
|
+
mime: "image/jpeg",
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
export async function containerComputerScreenshot(runner = sh, platform = process.platform, target = SHARED_LOCAL_VM_TARGET) {
|
|
732
|
+
const cacheable = runner === sh && platform === process.platform;
|
|
733
|
+
const now = Date.now();
|
|
734
|
+
const cached = screenshotStatusCache.get(target.key);
|
|
735
|
+
const status = cacheable && cached && cached.expiresAt > now
|
|
736
|
+
? cached.status
|
|
737
|
+
: await containerComputerStatus(runner, platform, target);
|
|
738
|
+
if (!status.ready || !status.runtime) {
|
|
739
|
+
if (cacheable)
|
|
740
|
+
screenshotStatusCache.delete(target.key);
|
|
741
|
+
throw Object.assign(new Error(status.problem ?? "The Local VM is not ready"), { status: 409 });
|
|
742
|
+
}
|
|
743
|
+
if (cacheable)
|
|
744
|
+
screenshotStatusCache.set(target.key, { status, expiresAt: now + SCREENSHOT_STATUS_TTL_MS });
|
|
745
|
+
try {
|
|
746
|
+
const screenshot = "/tmp/openmausbot-preview.png";
|
|
747
|
+
await runner(status.runtime, cuaExecArgs([
|
|
748
|
+
"call",
|
|
749
|
+
"get_desktop_state",
|
|
750
|
+
"{}",
|
|
751
|
+
"--socket",
|
|
752
|
+
CUA_SOCKET,
|
|
753
|
+
"--screenshot-out-file",
|
|
754
|
+
screenshot,
|
|
755
|
+
], { container: target.containerName }), 30_000);
|
|
756
|
+
const { stdout } = await runner(status.runtime, ["exec", target.containerName, "base64", "-w0", screenshot], 30_000);
|
|
757
|
+
const data = stdout.trim();
|
|
758
|
+
const checked = wholeScreenshot(Buffer.from(data, "base64"));
|
|
759
|
+
if (!checked.ok) {
|
|
760
|
+
throw Object.assign(new Error("Cua Driver returned an incomplete screenshot"), { status: 502 });
|
|
761
|
+
}
|
|
762
|
+
return `data:${checked.mime};base64,${data}`;
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
if (cacheable)
|
|
766
|
+
screenshotStatusCache.delete(target.key);
|
|
767
|
+
throw error;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
const screenshotStatusCache = new Map();
|
|
771
|
+
const containerMcpPath = SPAWNED_PROXIES.containerMcp;
|
|
772
|
+
export function containerComputerMcp(runtime, control, target = SHARED_LOCAL_VM_TARGET) {
|
|
773
|
+
return {
|
|
774
|
+
command: process.execPath,
|
|
775
|
+
args: [containerMcpPath, runtime, target.containerName, CUA_SOCKET],
|
|
776
|
+
// The control pair rides in env, not argv — argv is world-readable
|
|
777
|
+
// through `ps` for the life of the bridge.
|
|
778
|
+
env: {
|
|
779
|
+
ELECTRON_RUN_AS_NODE: "1",
|
|
780
|
+
...(control ? { OMB_CONTROL_URL: control.url, OMB_CONTROL_TOKEN: control.token } : {}),
|
|
781
|
+
},
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
/** Commands shown as a transparent fallback. Normal setup builds the pinned
|
|
785
|
+
* derivative through the API, so users do not need to author a Dockerfile. */
|
|
786
|
+
export function setupCommands(runtime, platform = process.platform, target = SHARED_LOCAL_VM_TARGET) {
|
|
787
|
+
const install = platform === "darwin"
|
|
788
|
+
? "brew install podman; podman machine init; podman machine start"
|
|
789
|
+
: platform === "win32"
|
|
790
|
+
? "winget install -e --id RedHat.Podman-Desktop"
|
|
791
|
+
: null;
|
|
792
|
+
const runtimeStart = runtime === "container"
|
|
793
|
+
? "container system start"
|
|
794
|
+
: runtime === "podman" && platform !== "linux"
|
|
795
|
+
? "podman machine init; podman machine start"
|
|
796
|
+
: runtime === "docker" && platform === "darwin"
|
|
797
|
+
? "colima start || open -a Docker"
|
|
798
|
+
: runtime === "docker" && platform === "linux"
|
|
799
|
+
? "sudo systemctl start docker"
|
|
800
|
+
: null;
|
|
801
|
+
if (!runtime) {
|
|
802
|
+
return {
|
|
803
|
+
install,
|
|
804
|
+
runtimeStart: null,
|
|
805
|
+
pull: null,
|
|
806
|
+
run: null,
|
|
807
|
+
start: null,
|
|
808
|
+
stop: null,
|
|
809
|
+
remove: null,
|
|
810
|
+
view: target.viewerPort ? `http://127.0.0.1:${target.viewerPort}/vnc.html` : "",
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
const command = (args) => [runtime, ...args].join(" ");
|
|
814
|
+
return {
|
|
815
|
+
install,
|
|
816
|
+
runtimeStart,
|
|
817
|
+
// This is the inspectable base download. The normal Prepare button also
|
|
818
|
+
// builds the checksum-pinned 0.20.0 derivative automatically.
|
|
819
|
+
pull: command(["pull", BASE_IMAGE]),
|
|
820
|
+
run: runtime === "container" && target.key !== SHARED_LOCAL_VM_TARGET.key
|
|
821
|
+
? null
|
|
822
|
+
: command(containerRunArgs(runtime, "CHANGE_ME", target)),
|
|
823
|
+
start: null,
|
|
824
|
+
stop: command(["stop", target.containerName]),
|
|
825
|
+
remove: command(["rm", runtime === "container" ? "--force" : "-f", target.containerName]),
|
|
826
|
+
view: target.viewerPort ? `http://127.0.0.1:${target.viewerPort}/vnc.html` : "",
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
/** Cloud boxes still use OpenMausBot's high-latency REST adapter. Local VMs
|
|
830
|
+
* bypass it and mount Cua Driver's official MCP server through
|
|
831
|
+
* containerComputerMcp(). */
|
|
832
|
+
export function computerProxyEnv(computer) {
|
|
833
|
+
return {
|
|
834
|
+
OGB_BOX_ID: computer.boxId ?? "",
|
|
835
|
+
OGB_BOX_TOKEN: computer.token ?? "",
|
|
836
|
+
...(computer.control
|
|
837
|
+
? { OMB_CONTROL_URL: computer.control.url, OMB_CONTROL_TOKEN: computer.control.token }
|
|
838
|
+
: {}),
|
|
839
|
+
};
|
|
840
|
+
}
|