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,1337 @@
|
|
|
1
|
+
// Claude driver — upstream ClaudeDriver skeleton over agentcal's
|
|
2
|
+
// drivers/claude.js runtime (stream-json both directions, prompt over
|
|
3
|
+
// stdin, completion from a real `result` event — verified against
|
|
4
|
+
// claude 2.1.211 by agentcal). Per-turn CLI process; the conversation
|
|
5
|
+
// continues across turns via --resume <sessionId> (the resumeCursor).
|
|
6
|
+
//
|
|
7
|
+
// Integrations become MCP servers on the CLI:
|
|
8
|
+
// - Composio Sessions (connected apps → tools) over streamable HTTP
|
|
9
|
+
// - the bot's cloud computer (box.ascii.dev) via server/computer-proxy.ts
|
|
10
|
+
// — screenshot/exec/open_url, the CUA-on-the-box bridge
|
|
11
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
12
|
+
import { chmodSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { createServer as createNetServer } from "node:net";
|
|
14
|
+
import { homedir, tmpdir } from "node:os";
|
|
15
|
+
import { join, dirname } from "node:path";
|
|
16
|
+
import { DATA_DIR, stripWorkspaceCredentialEnv } from "../config.js";
|
|
17
|
+
import { augmentedPath } from "../env-path.js";
|
|
18
|
+
import { brokerSocketPath, describeSpawnFailure, execCli, killCliTree, spawnCli } from "../procs.js";
|
|
19
|
+
import { computerProxyEnv } from "../container-computer.js";
|
|
20
|
+
import { newEventId, newId } from "../contracts.js";
|
|
21
|
+
import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
|
|
22
|
+
import { applyClaudeInject, decodeInjectId, mergeLocalInject, probeLocalInjects, resolveInjectId, } from "./local-inject.js";
|
|
23
|
+
import { appendNative } from "./native.js";
|
|
24
|
+
import { SPAWNED_PROXIES } from "../proxy-paths.js";
|
|
25
|
+
/** Whether `claude` has been signed in.
|
|
26
|
+
*
|
|
27
|
+
* Credential storage is deliberately not inspected here. Claude Code uses the
|
|
28
|
+
* macOS Keychain for OAuth, a JSON file on some platforms, and may gain other
|
|
29
|
+
* backends over time. Presence checks also accept stale credentials. The CLI's
|
|
30
|
+
* own machine-readable auth command is the source of truth for every backend.
|
|
31
|
+
*/
|
|
32
|
+
export function claudeSignedIn(cli, env, run = execCli) {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
run(cli, ["auth", "status", "--json"], { timeout: 8000, env }, (_error, stdout) => {
|
|
35
|
+
try {
|
|
36
|
+
const status = JSON.parse(stdout);
|
|
37
|
+
resolve(typeof status === "object" && status !== null && "loggedIn" in status && status.loggedIn === true);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
resolve(false);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** The CLI environment shared by auth probes and real turns.
|
|
46
|
+
*
|
|
47
|
+
* Subscription users can be billed pay-as-you-go if an inherited API key
|
|
48
|
+
* leaks through, and a nested CLI must not inherit this session's identity.
|
|
49
|
+
* Keeping the probe and turn environments identical prevents setup from
|
|
50
|
+
* claiming an API-key login that the turn itself would deliberately remove.
|
|
51
|
+
*/
|
|
52
|
+
function claudeEnvironment(model, source = process.env) {
|
|
53
|
+
const env = { ...source, PATH: augmentedPath(), NPM_CONFIG_LOGLEVEL: "error" };
|
|
54
|
+
delete env.CLAUDECODE;
|
|
55
|
+
delete env.CLAUDE_CODE_ENTRYPOINT;
|
|
56
|
+
// The harness process may hold workspace credentials (xai/box/voice keys,
|
|
57
|
+
// env-injected at boot); none of them are this CLI's to see.
|
|
58
|
+
stripWorkspaceCredentialEnv(env);
|
|
59
|
+
const applied = applyClaudeInject(env, model);
|
|
60
|
+
if (!applied.injected)
|
|
61
|
+
delete env.ANTHROPIC_API_KEY;
|
|
62
|
+
return env;
|
|
63
|
+
}
|
|
64
|
+
const DRIVER_KIND = "claudeAgent";
|
|
65
|
+
// model catalog ported from upstream packages/contracts/src/model.ts
|
|
66
|
+
export const STATIC_CLAUDE_MODELS = {
|
|
67
|
+
default: "claude-sonnet-5",
|
|
68
|
+
options: [
|
|
69
|
+
{ id: "claude-fable-5-1", label: "Claude Fable 5.1" },
|
|
70
|
+
{ id: "claude-fable-5", label: "Claude Fable 5" },
|
|
71
|
+
{ id: "claude-opus-5", label: "Claude Opus 5" },
|
|
72
|
+
{ id: "claude-sonnet-5", label: "Claude Sonnet 5" },
|
|
73
|
+
{ id: "claude-haiku-4-5", label: "Claude Haiku 4.5" },
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
const CLAUDE_MODEL_ID = /^[a-z0-9][a-z0-9._:/-]*$/i;
|
|
77
|
+
/** Rewrite a leftover API slug (`orcarouter/Qwen…`) to `host::model` when a
|
|
78
|
+
* local host is serving it, so the turn injects instead of asking for /login.
|
|
79
|
+
* Official cloud ids and already-encoded inject ids skip the probe. */
|
|
80
|
+
async function resolveClaudeTurnModel(model, env) {
|
|
81
|
+
if (!model || decodeInjectId(model) || STATIC_CLAUDE_MODELS.options.some((option) => option.id === model)) {
|
|
82
|
+
return model;
|
|
83
|
+
}
|
|
84
|
+
return resolveInjectId(model, await probeLocalInjects(env)) ?? model;
|
|
85
|
+
}
|
|
86
|
+
function claudeConfigDir(env) {
|
|
87
|
+
if (env.CLAUDE_CONFIG_DIR)
|
|
88
|
+
return env.CLAUDE_CONFIG_DIR;
|
|
89
|
+
return join(env.HOME || env.USERPROFILE || homedir(), ".claude");
|
|
90
|
+
}
|
|
91
|
+
function extrasFromUnknown(value) {
|
|
92
|
+
if (!Array.isArray(value))
|
|
93
|
+
return [];
|
|
94
|
+
return value.flatMap((item) => {
|
|
95
|
+
if (typeof item === "string") {
|
|
96
|
+
return CLAUDE_MODEL_ID.test(item) ? [{ id: item, label: item }] : [];
|
|
97
|
+
}
|
|
98
|
+
if (!item || typeof item !== "object")
|
|
99
|
+
return [];
|
|
100
|
+
const row = item;
|
|
101
|
+
const id = [row.id, row.model, row.slug].find((candidate) => typeof candidate === "string");
|
|
102
|
+
if (!id || !CLAUDE_MODEL_ID.test(id))
|
|
103
|
+
return [];
|
|
104
|
+
const label = [row.name, row.displayName, row.label].find((candidate) => typeof candidate === "string");
|
|
105
|
+
return [{ id, label: label || id }];
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/** Extra ids from ~/.claude/settings.json. Official cloud rows stay untagged.
|
|
109
|
+
* `model` is Claude Code's last-used slug, not a catalog — listing it as
|
|
110
|
+
* Custom put a non-inject id in the picker and the turn then had no
|
|
111
|
+
* ANTHROPIC_API_KEY ("Not logged in · Please run /login"). Live injects
|
|
112
|
+
* come from mergeLocalInject. */
|
|
113
|
+
export function readClaudeModelCatalog(env = process.env) {
|
|
114
|
+
let settings = {};
|
|
115
|
+
try {
|
|
116
|
+
settings = JSON.parse(readFileSync(join(claudeConfigDir(env), "settings.json"), "utf8"));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return STATIC_CLAUDE_MODELS;
|
|
120
|
+
}
|
|
121
|
+
const extras = [
|
|
122
|
+
...extrasFromUnknown(settings.availableModels),
|
|
123
|
+
...extrasFromUnknown(settings.customModels),
|
|
124
|
+
...extrasFromUnknown(settings.extraModels),
|
|
125
|
+
];
|
|
126
|
+
const nestedEnv = settings.env && typeof settings.env === "object" ? settings.env : {};
|
|
127
|
+
const envModel = nestedEnv.ANTHROPIC_MODEL ?? env.ANTHROPIC_MODEL;
|
|
128
|
+
if (typeof envModel === "string")
|
|
129
|
+
extras.push(...extrasFromUnknown([envModel]));
|
|
130
|
+
const options = STATIC_CLAUDE_MODELS.options.map((option) => ({ ...option }));
|
|
131
|
+
const seen = new Set(options.map((option) => option.id));
|
|
132
|
+
for (const extra of extras) {
|
|
133
|
+
if (seen.has(extra.id))
|
|
134
|
+
continue;
|
|
135
|
+
seen.add(extra.id);
|
|
136
|
+
options.push({ id: extra.id, label: extra.label, custom: true });
|
|
137
|
+
}
|
|
138
|
+
return { default: STATIC_CLAUDE_MODELS.default, options };
|
|
139
|
+
}
|
|
140
|
+
// Resolved from the server root, never relative to this file: bundling inlines
|
|
141
|
+
// this module into an entry one directory up, so a `".."` here would climb too
|
|
142
|
+
// far. See server/proxy-paths.ts.
|
|
143
|
+
const PROXY_PATH = SPAWNED_PROXIES.computer;
|
|
144
|
+
const PERM_PROXY_PATH = SPAWNED_PROXIES.permission;
|
|
145
|
+
const DWEB_PROXY_PATH = SPAWNED_PROXIES.dweb;
|
|
146
|
+
// in the packaged app process.execPath is the Electron binary — this env
|
|
147
|
+
// makes it behave as plain node for the spawned MCP proxies (harmless in dev)
|
|
148
|
+
const NODE_ENV_FLAG = { ELECTRON_RUN_AS_NODE: "1" };
|
|
149
|
+
function removePrivateTempDir(filePath) {
|
|
150
|
+
if (!filePath)
|
|
151
|
+
return true;
|
|
152
|
+
try {
|
|
153
|
+
rmSync(dirname(filePath), { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const DENY_TIMEOUT_NOTE = "OpenMausBot: nobody answered this permission request in time. Skip this action and finish what you can without it.";
|
|
161
|
+
const QUESTION_TIMEOUT_NOTE = "OpenMausBot: nobody answered in time. Use your best judgment and continue.";
|
|
162
|
+
const DUPLICATE_ASK_ID_NOTE = "OpenMausBot: duplicate ask id — skipping this request.";
|
|
163
|
+
/** The system-source reply for an ask that outlives the turn — used both to
|
|
164
|
+
* drain in-flight `pending` asks on close() and to answer one that arrives
|
|
165
|
+
* on an already-closed broker (see the `closed` branch below). */
|
|
166
|
+
function systemEndedReply(kind) {
|
|
167
|
+
return kind === "question"
|
|
168
|
+
? { behavior: "answer", message: "OpenMausBot: the turn is ending — wrap up." }
|
|
169
|
+
: { behavior: "deny", message: "OpenMausBot: the turn ended" };
|
|
170
|
+
}
|
|
171
|
+
/** One human-readable line for an ask — what the card subtitle shows. */
|
|
172
|
+
function askSummary(ask) {
|
|
173
|
+
const input = ask.input ?? {};
|
|
174
|
+
if (typeof input.question === "string")
|
|
175
|
+
return input.question.slice(0, 300);
|
|
176
|
+
if (typeof input.command === "string")
|
|
177
|
+
return input.command.slice(0, 200);
|
|
178
|
+
if (typeof input.url === "string")
|
|
179
|
+
return input.url.slice(0, 200);
|
|
180
|
+
const text = JSON.stringify(input);
|
|
181
|
+
return text === "{}" ? (ask.tool ?? "tool") : text.slice(0, 200);
|
|
182
|
+
}
|
|
183
|
+
export function permissionSocketPath(threadId) {
|
|
184
|
+
// A readable prefix alone is not unique: ids that agree on their first
|
|
185
|
+
// characters ("t-perm-dup-1", "t-perm-dup-2") would share a socket. POSIX
|
|
186
|
+
// hides that — a new broker's listen replaces the socket FILE, so the name
|
|
187
|
+
// always points at the fresh server — but Windows named pipes live in a
|
|
188
|
+
// global namespace that is never unlinked, and a reused name races the
|
|
189
|
+
// previous broker's async teardown. Half the tag is a digest of the FULL
|
|
190
|
+
// id so distinct threads get distinct sockets; the tag stays at 8 chars
|
|
191
|
+
// total because the POSIX path already brushes the 104-byte sun_path
|
|
192
|
+
// limit under deep tmp home dirs.
|
|
193
|
+
const prefix = threadId.replace(/[^\w-]/g, "").slice(0, 4);
|
|
194
|
+
const digest = createHash("sha256").update(threadId).digest("hex").slice(0, 4);
|
|
195
|
+
return brokerSocketPath(DATA_DIR, `${prefix}${digest}`);
|
|
196
|
+
}
|
|
197
|
+
/** Paths the broker may bind, tried in order. Windows named pipes are never
|
|
198
|
+
* unlinkable, and a hung CLI child from an earlier server process can hold a
|
|
199
|
+
* name for minutes, so fresh suffixes let the new broker bind immediately.
|
|
200
|
+
* POSIX gets a short temp fallback because macOS rejects Unix socket paths
|
|
201
|
+
* longer than its small `sun_path` limit; a deep test HOME or long username
|
|
202
|
+
* can otherwise make every approval silently unavailable. The proxy learns
|
|
203
|
+
* the actual bound path from its argv, so either fallback is transparent. */
|
|
204
|
+
export function brokerSocketCandidates(threadId) {
|
|
205
|
+
const base = permissionSocketPath(threadId);
|
|
206
|
+
if (process.platform !== "win32") {
|
|
207
|
+
const scope = createHash("sha256")
|
|
208
|
+
.update(`${DATA_DIR}\0${process.pid}\0${threadId}`)
|
|
209
|
+
.digest("hex")
|
|
210
|
+
.slice(0, 16);
|
|
211
|
+
return [base, join(tmpdir(), `omb-perm-${scope}.sock`)];
|
|
212
|
+
}
|
|
213
|
+
return [
|
|
214
|
+
base,
|
|
215
|
+
`${base}-${randomBytes(3).toString("hex")}`,
|
|
216
|
+
`${base}-${randomBytes(3).toString("hex")}`,
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
export async function createPermissionBroker(opts) {
|
|
220
|
+
const timeoutMs = opts.timeoutMs ?? 15 * 60_000;
|
|
221
|
+
const pending = new Map();
|
|
222
|
+
// server.close() only stops accepting NEW connections — it does not touch
|
|
223
|
+
// a connection that's already open. A still-alive child's MCP proxy can
|
|
224
|
+
// keep sending asks on such a connection after the turn has ended, and
|
|
225
|
+
// this handler stays fully wired to it. Without this flag those asks would
|
|
226
|
+
// become new `pending` entries and `request.opened` cards for a turn the
|
|
227
|
+
// driver already forgot (`active.delete(threadId)` already ran), which can
|
|
228
|
+
// never be answered — the "zombie card" in issue #211.
|
|
229
|
+
let closed = false;
|
|
230
|
+
let boundPath = opts.socketPaths[0] ?? "";
|
|
231
|
+
const connectionHandler = (conn) => {
|
|
232
|
+
conn.on("error", () => { });
|
|
233
|
+
let buf = "";
|
|
234
|
+
conn.on("data", (chunk) => {
|
|
235
|
+
buf += chunk;
|
|
236
|
+
let nl;
|
|
237
|
+
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
238
|
+
const line = buf.slice(0, nl);
|
|
239
|
+
buf = buf.slice(nl + 1);
|
|
240
|
+
let msg;
|
|
241
|
+
try {
|
|
242
|
+
msg = JSON.parse(line);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (msg.t !== "ask")
|
|
248
|
+
continue;
|
|
249
|
+
const askId = String(msg.id ?? newId());
|
|
250
|
+
const kind = msg.kind === "question" ? "question" : "permission";
|
|
251
|
+
if (closed) {
|
|
252
|
+
// Closure is terminal and takes precedence over every active-turn
|
|
253
|
+
// rule, including duplicate-id rejection. Never register a pending
|
|
254
|
+
// entry or notify onAsk, but always answer an existing connection:
|
|
255
|
+
// permission-proxy.ts only resolves on an explicit answer (or a
|
|
256
|
+
// connection error/close), so a silent drop would hang the tool.
|
|
257
|
+
try {
|
|
258
|
+
conn.write(JSON.stringify({ t: "answer", id: askId, ...systemEndedReply(kind) }) + "\n");
|
|
259
|
+
}
|
|
260
|
+
catch { }
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
// A retained Claude process keeps its proxy connection between
|
|
264
|
+
// turns. Late/background asks must still fail closed without opening
|
|
265
|
+
// a card for a turn that has already settled.
|
|
266
|
+
if (opts.isActive && !opts.isActive()) {
|
|
267
|
+
try {
|
|
268
|
+
conn.write(JSON.stringify({ t: "answer", id: askId, ...systemEndedReply(kind) }) + "\n");
|
|
269
|
+
}
|
|
270
|
+
catch { }
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
// `pending` is server-scoped, not per-connection: two asks with the
|
|
274
|
+
// same id — a buggy/adversarial client, never a legitimate retry
|
|
275
|
+
// (permission-proxy mints a fresh randomUUID per ask) — would
|
|
276
|
+
// otherwise let the second `pending.set` silently overwrite the
|
|
277
|
+
// first, orphaning it as an unanswerable card once the first
|
|
278
|
+
// resolves and deletes the shared key. Reject before either ask
|
|
279
|
+
// becomes visible to onAsk.
|
|
280
|
+
if (pending.has(askId)) {
|
|
281
|
+
// askId is client-controlled; JSON.stringify escapes newlines and
|
|
282
|
+
// control characters so it can't corrupt the log line or terminal.
|
|
283
|
+
console.error(`permission broker on ${boundPath}: duplicate ask id ${JSON.stringify(askId)} — denying`);
|
|
284
|
+
try {
|
|
285
|
+
conn.write(JSON.stringify({ t: "answer", id: askId, behavior: "deny", message: DUPLICATE_ASK_ID_NOTE }) + "\n");
|
|
286
|
+
}
|
|
287
|
+
catch { }
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const ask = { id: askId, kind, tool: msg.tool ?? "tool", input: msg.input ?? {}, at: Date.now() };
|
|
291
|
+
const finish = (behavior, message, source) => {
|
|
292
|
+
if (!pending.delete(askId))
|
|
293
|
+
return;
|
|
294
|
+
clearTimeout(timer);
|
|
295
|
+
try {
|
|
296
|
+
conn.write(JSON.stringify({ t: "answer", id: askId, behavior, message }) + "\n");
|
|
297
|
+
}
|
|
298
|
+
catch { }
|
|
299
|
+
opts.onResolve({ ...ask, behavior, source });
|
|
300
|
+
};
|
|
301
|
+
const timer = setTimeout(() => kind === "question"
|
|
302
|
+
? finish("answer", QUESTION_TIMEOUT_NOTE, "timeout")
|
|
303
|
+
: finish("deny", DENY_TIMEOUT_NOTE, "timeout"), timeoutMs);
|
|
304
|
+
timer.unref?.();
|
|
305
|
+
pending.set(askId, { ask, finish });
|
|
306
|
+
opts.onAsk(ask);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
// Bind the first candidate that will take a listener. A broker that
|
|
311
|
+
// never came up used to be silent — every approval then timed out into a
|
|
312
|
+
// deny nobody could explain. Keep the turn fail-closed on total failure,
|
|
313
|
+
// but leave an actionable diagnostic either way.
|
|
314
|
+
let server = null;
|
|
315
|
+
for (const [index, candidate] of opts.socketPaths.entries()) {
|
|
316
|
+
const attempt = createNetServer(connectionHandler);
|
|
317
|
+
try {
|
|
318
|
+
unlinkSync(candidate);
|
|
319
|
+
}
|
|
320
|
+
catch { }
|
|
321
|
+
let outcome = await new Promise((resolve) => {
|
|
322
|
+
attempt.once("listening", () => resolve("listening"));
|
|
323
|
+
// SAFETY: net 'error' events carry syscall errors; the optional
|
|
324
|
+
// `code` is only read defensively below.
|
|
325
|
+
attempt.once("error", (error) => resolve(error));
|
|
326
|
+
attempt.listen(candidate);
|
|
327
|
+
});
|
|
328
|
+
// A fallback under the shared OS temp root must not be connectable by
|
|
329
|
+
// another local account. DATA_DIR is private already, but applying the
|
|
330
|
+
// same mode to every POSIX socket keeps the rule simple and fail-closed.
|
|
331
|
+
if (outcome === "listening" && process.platform !== "win32") {
|
|
332
|
+
try {
|
|
333
|
+
chmodSync(candidate, 0o600);
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
try {
|
|
337
|
+
attempt.close();
|
|
338
|
+
}
|
|
339
|
+
catch { }
|
|
340
|
+
try {
|
|
341
|
+
unlinkSync(candidate);
|
|
342
|
+
}
|
|
343
|
+
catch { }
|
|
344
|
+
outcome = error;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (outcome === "listening") {
|
|
348
|
+
if (index > 0) {
|
|
349
|
+
console.error(`permission broker: ${opts.socketPaths[0]} is still held — bound fallback ${candidate}`);
|
|
350
|
+
}
|
|
351
|
+
boundPath = candidate;
|
|
352
|
+
server = attempt;
|
|
353
|
+
attempt.on("error", (error) => {
|
|
354
|
+
console.error(`permission broker error on ${candidate}: ${error.message}`);
|
|
355
|
+
});
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
attempt.close();
|
|
360
|
+
}
|
|
361
|
+
catch { }
|
|
362
|
+
if (index === opts.socketPaths.length - 1) {
|
|
363
|
+
console.error(`permission broker unavailable on ${candidate}: ${outcome.message}`);
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// Never hand the proxy an occupied candidate when every bind failed. That
|
|
368
|
+
// could connect it to a stale (or unrelated) listener instead of this
|
|
369
|
+
// broker, defeating the fail-closed boundary.
|
|
370
|
+
if (!server)
|
|
371
|
+
throw new Error("claude: permission broker could not bind a local socket");
|
|
372
|
+
const drain = () => {
|
|
373
|
+
for (const p of [...pending.values()]) {
|
|
374
|
+
const { behavior, message } = systemEndedReply(p.ask.kind);
|
|
375
|
+
p.finish(behavior, message, "system");
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
return {
|
|
379
|
+
answer(askId, behavior, message) {
|
|
380
|
+
const p = pending.get(askId);
|
|
381
|
+
if (!p)
|
|
382
|
+
return false;
|
|
383
|
+
if (p.ask.kind === "question" ? behavior !== "answer" : behavior === "answer")
|
|
384
|
+
return false;
|
|
385
|
+
p.finish(behavior, message, "user");
|
|
386
|
+
return true;
|
|
387
|
+
},
|
|
388
|
+
pause() {
|
|
389
|
+
drain();
|
|
390
|
+
},
|
|
391
|
+
close() {
|
|
392
|
+
closed = true;
|
|
393
|
+
drain();
|
|
394
|
+
try {
|
|
395
|
+
server?.close();
|
|
396
|
+
}
|
|
397
|
+
catch { }
|
|
398
|
+
try {
|
|
399
|
+
unlinkSync(boundPath);
|
|
400
|
+
}
|
|
401
|
+
catch { }
|
|
402
|
+
},
|
|
403
|
+
/** Where the broker actually listens — argv for the proxy child must
|
|
404
|
+
* use this, not the deterministic base, when a fallback was bound. */
|
|
405
|
+
socketPath: boundPath,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function decodeToolList(value, field) {
|
|
409
|
+
if (value === undefined)
|
|
410
|
+
return undefined;
|
|
411
|
+
if (!Array.isArray(value))
|
|
412
|
+
throw new Error(`claude: ${field} must be an array of non-empty strings`);
|
|
413
|
+
const decoded = [];
|
|
414
|
+
const seen = new Set();
|
|
415
|
+
for (const entry of value) {
|
|
416
|
+
if (typeof entry !== "string" || !entry.trim()) {
|
|
417
|
+
throw new Error(`claude: ${field} must be an array of non-empty strings`);
|
|
418
|
+
}
|
|
419
|
+
const normalized = entry.trim();
|
|
420
|
+
if (seen.has(normalized))
|
|
421
|
+
continue;
|
|
422
|
+
seen.add(normalized);
|
|
423
|
+
decoded.push(normalized);
|
|
424
|
+
}
|
|
425
|
+
return decoded;
|
|
426
|
+
}
|
|
427
|
+
function decodeConfig(raw) {
|
|
428
|
+
const o = (raw ?? {});
|
|
429
|
+
const mode = o.permissionMode;
|
|
430
|
+
if (mode !== undefined && mode !== "acceptEdits" && mode !== "auto" && mode !== "bypassPermissions") {
|
|
431
|
+
throw new Error(`claude: invalid permissionMode ${JSON.stringify(mode)}`);
|
|
432
|
+
}
|
|
433
|
+
const tools = decodeToolList(o.tools, "tools");
|
|
434
|
+
const disallowedTools = decodeToolList(o.disallowedTools, "disallowedTools");
|
|
435
|
+
return {
|
|
436
|
+
cli: typeof o.cli === "string" ? o.cli : "claude",
|
|
437
|
+
permissionMode: mode ?? "acceptEdits",
|
|
438
|
+
...(tools !== undefined ? { tools } : {}),
|
|
439
|
+
...(disallowedTools !== undefined ? { disallowedTools } : {}),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function firstText(content) {
|
|
443
|
+
if (typeof content === "string")
|
|
444
|
+
return content;
|
|
445
|
+
if (Array.isArray(content)) {
|
|
446
|
+
return content
|
|
447
|
+
.filter((b) => b?.type === "text" && b.text)
|
|
448
|
+
.map((b) => b.text)
|
|
449
|
+
.join("");
|
|
450
|
+
}
|
|
451
|
+
return "";
|
|
452
|
+
}
|
|
453
|
+
/** Claude's stream-json input accepts the same image source blocks as the
|
|
454
|
+
* Anthropic Messages API. Keep the old string form for text-only turns so a
|
|
455
|
+
* CLI update cannot disturb the overwhelmingly common path. */
|
|
456
|
+
function claudeUserMessage(text, images) {
|
|
457
|
+
if (!images?.length)
|
|
458
|
+
return { type: "user", message: { role: "user", content: text } };
|
|
459
|
+
const content = images.map((image) => ({
|
|
460
|
+
type: "image",
|
|
461
|
+
source: {
|
|
462
|
+
type: "base64",
|
|
463
|
+
media_type: image.mime,
|
|
464
|
+
data: readFileSync(image.path).toString("base64"),
|
|
465
|
+
},
|
|
466
|
+
}));
|
|
467
|
+
if (text)
|
|
468
|
+
content.push({ type: "text", text });
|
|
469
|
+
return { type: "user", message: { role: "user", content } };
|
|
470
|
+
}
|
|
471
|
+
/** Native traces are routinely attached to bug reports. Preserve the image
|
|
472
|
+
* block's shape and size for debugging, but never persist its base64 bytes. */
|
|
473
|
+
function diagnosticClaudeUserMessage(message) {
|
|
474
|
+
if (!Array.isArray(message.message.content))
|
|
475
|
+
return message;
|
|
476
|
+
return {
|
|
477
|
+
...message,
|
|
478
|
+
message: {
|
|
479
|
+
...message.message,
|
|
480
|
+
content: message.message.content.map((block) => block.type === "image"
|
|
481
|
+
? {
|
|
482
|
+
...block,
|
|
483
|
+
source: {
|
|
484
|
+
...block.source,
|
|
485
|
+
data: `[image data: ${block.source.data.length} base64 chars]`,
|
|
486
|
+
},
|
|
487
|
+
}
|
|
488
|
+
: block),
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
export const ClaudeDriver = {
|
|
493
|
+
driverKind: DRIVER_KIND,
|
|
494
|
+
metadata: { displayName: "Claude", supportsMultipleInstances: true },
|
|
495
|
+
// npm on all three: the one recipe that is genuinely cross-platform. The
|
|
496
|
+
// native installers differ per OS and would need verifying separately.
|
|
497
|
+
install: {
|
|
498
|
+
command: {
|
|
499
|
+
darwin: "npm install -g @anthropic-ai/claude-code",
|
|
500
|
+
linux: "npm install -g @anthropic-ai/claude-code",
|
|
501
|
+
win32: "npm install -g @anthropic-ai/claude-code",
|
|
502
|
+
},
|
|
503
|
+
needsNode: true,
|
|
504
|
+
docsUrl: "https://claude.com/claude-code",
|
|
505
|
+
signInCommand: "claude",
|
|
506
|
+
},
|
|
507
|
+
models: STATIC_CLAUDE_MODELS,
|
|
508
|
+
decodeConfig,
|
|
509
|
+
defaultConfig: () => decodeConfig({}),
|
|
510
|
+
async create(input) {
|
|
511
|
+
const { instanceId, config } = input;
|
|
512
|
+
const catalogEnv = { ...process.env, ...input.environment };
|
|
513
|
+
let models = STATIC_CLAUDE_MODELS;
|
|
514
|
+
const refreshModels = async () => {
|
|
515
|
+
try {
|
|
516
|
+
const resolved = await mergeLocalInject(readClaudeModelCatalog(catalogEnv), catalogEnv);
|
|
517
|
+
if (resolved.options.length)
|
|
518
|
+
models = resolved;
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
// Keep the last usable catalog when settings.json is unreadable.
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
await refreshModels();
|
|
525
|
+
const listeners = new Set();
|
|
526
|
+
// one active turn per thread; a second send while busy is a caller bug
|
|
527
|
+
const active = new Map();
|
|
528
|
+
const sessions = new Map();
|
|
529
|
+
const configuredIdleMinimum = Number(process.env.OMB_CLAUDE_SESSION_IDLE_MIN_MS);
|
|
530
|
+
const sessionIdleMinimum = Number.isFinite(configuredIdleMinimum) && configuredIdleMinimum > 0
|
|
531
|
+
? configuredIdleMinimum
|
|
532
|
+
: 10_000;
|
|
533
|
+
const SESSION_IDLE_MS = Math.max(sessionIdleMinimum, Number(process.env.OMB_CLAUDE_SESSION_IDLE_MS) || 10 * 60_000);
|
|
534
|
+
const closeSession = (threadId, why) => {
|
|
535
|
+
const s = sessions.get(threadId);
|
|
536
|
+
if (!s || s.closing)
|
|
537
|
+
return;
|
|
538
|
+
s.closing = true;
|
|
539
|
+
if (s.idleTimer)
|
|
540
|
+
clearTimeout(s.idleTimer);
|
|
541
|
+
// Broker ownership belongs to this session. Detach and close it now,
|
|
542
|
+
// before a replacement can bind the same per-thread socket; the old
|
|
543
|
+
// child's later close event must never unlink a new broker.
|
|
544
|
+
const broker = s.broker;
|
|
545
|
+
s.broker = undefined;
|
|
546
|
+
broker?.close();
|
|
547
|
+
appendNative(threadId, { dir: "out", source: "claude.session", msg: { close: why } });
|
|
548
|
+
// stdin EOF is the CLI's exit signal; give it a moment, then insist
|
|
549
|
+
try {
|
|
550
|
+
s.child.stdin.end();
|
|
551
|
+
}
|
|
552
|
+
catch { }
|
|
553
|
+
const kill = setTimeout(() => {
|
|
554
|
+
if (s.child.exitCode === null)
|
|
555
|
+
killCliTree(s.child);
|
|
556
|
+
}, 5_000);
|
|
557
|
+
kill.unref?.();
|
|
558
|
+
};
|
|
559
|
+
const armIdle = (threadId) => {
|
|
560
|
+
const s = sessions.get(threadId);
|
|
561
|
+
if (!s)
|
|
562
|
+
return;
|
|
563
|
+
if (s.idleTimer)
|
|
564
|
+
clearTimeout(s.idleTimer);
|
|
565
|
+
s.idleTimer = setTimeout(() => closeSession(threadId, "idle"), SESSION_IDLE_MS);
|
|
566
|
+
s.idleTimer.unref?.();
|
|
567
|
+
};
|
|
568
|
+
const writeUser = (s, threadId, promptMsg) => {
|
|
569
|
+
if (!s.child.stdin.writable || s.child.stdin.destroyed)
|
|
570
|
+
return Promise.resolve(false);
|
|
571
|
+
return new Promise((resolve) => {
|
|
572
|
+
try {
|
|
573
|
+
s.child.stdin.write(JSON.stringify(promptMsg) + "\n", (error) => {
|
|
574
|
+
if (error)
|
|
575
|
+
return resolve(false);
|
|
576
|
+
appendNative(threadId, {
|
|
577
|
+
dir: "out",
|
|
578
|
+
source: "claude.sdk.message",
|
|
579
|
+
msg: diagnosticClaudeUserMessage(promptMsg),
|
|
580
|
+
});
|
|
581
|
+
resolve(true);
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
resolve(false);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
};
|
|
589
|
+
const emit = (event) => {
|
|
590
|
+
for (const l of [...listeners])
|
|
591
|
+
l(event);
|
|
592
|
+
};
|
|
593
|
+
const base = (threadId, turnId) => ({
|
|
594
|
+
eventId: newEventId(),
|
|
595
|
+
provider: DRIVER_KIND,
|
|
596
|
+
threadId,
|
|
597
|
+
turnId,
|
|
598
|
+
createdAt: new Date().toISOString(),
|
|
599
|
+
});
|
|
600
|
+
// retry bookkeeping lives PER THREAD, not per sendTurn call: a relaunch
|
|
601
|
+
// is a fresh sendTurn, and the attempt cap must survive across launches
|
|
602
|
+
const retryState = new Map();
|
|
603
|
+
const sendTurn = async (turn) => {
|
|
604
|
+
const { threadId } = turn;
|
|
605
|
+
if (active.has(threadId))
|
|
606
|
+
throw new Error("a turn is already running on this thread");
|
|
607
|
+
// A bot-level mode is authoritative for this turn. In particular, an
|
|
608
|
+
// old provider instance may still be configured with
|
|
609
|
+
// `bypassPermissions`; Ask/Auto must restore Claude's interactive
|
|
610
|
+
// broker instead of inheriting that silent bypass. Calls without a
|
|
611
|
+
// per-turn mode keep the legacy adapter behavior.
|
|
612
|
+
const permissionMode = turn.approvalMode === undefined
|
|
613
|
+
? config.permissionMode
|
|
614
|
+
: turn.approvalMode === "full" ? "bypassPermissions"
|
|
615
|
+
: turn.approvalMode === "auto" ? "auto" : "default";
|
|
616
|
+
const controlsHost = turn.integrations?.localComputer?.scope === "local-computer";
|
|
617
|
+
if (controlsHost && permissionMode === "bypassPermissions" && turn.approvalMode !== "full") {
|
|
618
|
+
throw new Error("local computer control requires the interactive approval broker");
|
|
619
|
+
}
|
|
620
|
+
// Materialize before creating a broker or process. A missing/corrupt
|
|
621
|
+
// attachment must fail this call without leaving a live session behind.
|
|
622
|
+
const promptMsg = claudeUserMessage(turn.text, turn.images);
|
|
623
|
+
const turnId = newId();
|
|
624
|
+
const retryAbort = new AbortController();
|
|
625
|
+
const retry = retryState.get(threadId) ?? { attempt: 0, cancelled: false };
|
|
626
|
+
retry.cancelled = false;
|
|
627
|
+
retryState.set(threadId, retry);
|
|
628
|
+
// a retry relaunches the whole CLI; the backoff is scaled down in tests
|
|
629
|
+
// so a fake's transient failures don't stall real seconds
|
|
630
|
+
const retryScale = Number(process.env.FAKE_CLAUDE_RETRY_SCALE ?? "1");
|
|
631
|
+
const sessionId = typeof turn.resumeCursor === "string" ? turn.resumeCursor : null;
|
|
632
|
+
const newSessionId = sessionId ? null : newId();
|
|
633
|
+
const args = [
|
|
634
|
+
"-p",
|
|
635
|
+
"--output-format", "stream-json",
|
|
636
|
+
"--input-format", "stream-json",
|
|
637
|
+
"--verbose", // required by stream-json output
|
|
638
|
+
// token-level streaming: content_block_delta events between the
|
|
639
|
+
// whole-message frames, so the bubble grows as the model writes
|
|
640
|
+
"--include-partial-messages",
|
|
641
|
+
"--permission-mode", permissionMode,
|
|
642
|
+
];
|
|
643
|
+
if (config.tools !== undefined)
|
|
644
|
+
args.push("--tools", config.tools.join(","));
|
|
645
|
+
if (config.disallowedTools?.length) {
|
|
646
|
+
args.push("--disallowedTools", config.disallowedTools.join(","));
|
|
647
|
+
}
|
|
648
|
+
const turnEnvironment = { ...process.env, ...input.environment };
|
|
649
|
+
const turnModel = await resolveClaudeTurnModel(turn.model, turnEnvironment);
|
|
650
|
+
const injected = applyClaudeInject({ ...turnEnvironment }, turnModel);
|
|
651
|
+
if (injected.model)
|
|
652
|
+
args.push("--model", injected.model);
|
|
653
|
+
if (turn.effort)
|
|
654
|
+
args.push("--effort", turn.effort);
|
|
655
|
+
// A room prompt can contain section context, skills, memory, playbooks,
|
|
656
|
+
// and browser/agent instructions. Passing that text directly on argv
|
|
657
|
+
// exceeds Windows' CreateProcess command-line limit and surfaces as
|
|
658
|
+
// `spawn ENAMETOOLONG`. Claude accepts the same prompt from a file, so
|
|
659
|
+
// keep both the text and its potentially sensitive contents off argv.
|
|
660
|
+
let systemPromptPath = null;
|
|
661
|
+
// integrations → MCP servers; pre-allow their tools (a headless
|
|
662
|
+
// acceptEdits run silently denies anything unlisted)
|
|
663
|
+
const mcpServers = {};
|
|
664
|
+
const allowed = [];
|
|
665
|
+
if (turn.integrations?.composio) {
|
|
666
|
+
mcpServers.composio = { ...turn.integrations.composio };
|
|
667
|
+
allowed.push("mcp__composio");
|
|
668
|
+
}
|
|
669
|
+
if (turn.integrations?.computer) {
|
|
670
|
+
mcpServers.computer = {
|
|
671
|
+
command: process.execPath,
|
|
672
|
+
args: [PROXY_PATH],
|
|
673
|
+
env: { ...NODE_ENV_FLAG, ...computerProxyEnv(turn.integrations.computer) },
|
|
674
|
+
};
|
|
675
|
+
allowed.push("mcp__computer");
|
|
676
|
+
}
|
|
677
|
+
else if (turn.integrations?.localComputer) {
|
|
678
|
+
const local = turn.integrations.localComputer;
|
|
679
|
+
mcpServers.computer = {
|
|
680
|
+
command: local.command,
|
|
681
|
+
args: local.args,
|
|
682
|
+
env: local.env,
|
|
683
|
+
};
|
|
684
|
+
// The isolated Local VM preserves the established pre-allow behavior.
|
|
685
|
+
// Host tools always route through OpenMausBot's permission broker.
|
|
686
|
+
if (!controlsHost)
|
|
687
|
+
allowed.push("mcp__computer");
|
|
688
|
+
}
|
|
689
|
+
// peer-agent comms (list_bots/ask_bot) — the harness builds the whole
|
|
690
|
+
// spawn contract (command/args/env incl. the boot token) in
|
|
691
|
+
// agentsIntegration(); pre-allowing matters doubly here, or the CLI's
|
|
692
|
+
// own ListAgents look-alike shadows it and "@Bot" asks go nowhere
|
|
693
|
+
if (turn.integrations?.agents) {
|
|
694
|
+
// Coordination is foundational, not an optional deferred lookup.
|
|
695
|
+
// Claude waits for always-loaded tools before building the prompt.
|
|
696
|
+
mcpServers.agents = { ...turn.integrations.agents, alwaysLoad: true };
|
|
697
|
+
allowed.push("mcp__agents");
|
|
698
|
+
}
|
|
699
|
+
if (turn.integrations?.phone) {
|
|
700
|
+
mcpServers.phone = { ...turn.integrations.phone };
|
|
701
|
+
allowed.push("mcp__phone");
|
|
702
|
+
}
|
|
703
|
+
if (turn.integrations?.browser) {
|
|
704
|
+
mcpServers.browser = { ...turn.integrations.browser };
|
|
705
|
+
allowed.push("mcp__browser");
|
|
706
|
+
}
|
|
707
|
+
// dweb network daemon (status / repo / opencode model access) via
|
|
708
|
+
// server/drivers/dweb-proxy.ts — points at the configured dweb instance
|
|
709
|
+
if (turn.integrations?.dweb) {
|
|
710
|
+
mcpServers.dweb = {
|
|
711
|
+
command: process.execPath,
|
|
712
|
+
args: [DWEB_PROXY_PATH],
|
|
713
|
+
env: {
|
|
714
|
+
...NODE_ENV_FLAG,
|
|
715
|
+
DWEB_URL: turn.integrations.dweb.url,
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
allowed.push("mcp__dweb");
|
|
719
|
+
}
|
|
720
|
+
// user-configured servers mount like any integration but are NOT
|
|
721
|
+
// pre-allowed: acceptEdits silently denies unlisted tools, which
|
|
722
|
+
// routes every custom tool call through the ogb permission broker
|
|
723
|
+
// into an Allow/Deny card. Reserved names were filtered upstream;
|
|
724
|
+
// skip any residual collision instead of clobbering a built-in.
|
|
725
|
+
for (const [name, server] of Object.entries(turn.integrations?.custom ?? {})) {
|
|
726
|
+
if (name in mcpServers)
|
|
727
|
+
continue;
|
|
728
|
+
mcpServers[name] = { ...server };
|
|
729
|
+
}
|
|
730
|
+
// Keep ask_user available even in Full access. Native bypass skips
|
|
731
|
+
// permission prompts, not questions requiring a person's answer.
|
|
732
|
+
let broker;
|
|
733
|
+
const socketPath = permissionSocketPath(threadId);
|
|
734
|
+
if (permissionMode !== "bypassPermissions") {
|
|
735
|
+
args.push("--permission-prompt-tool", "mcp__ogb__approve");
|
|
736
|
+
}
|
|
737
|
+
mcpServers.ogb = { command: process.execPath, args: [PERM_PROXY_PATH, socketPath], env: { ...NODE_ENV_FLAG }, alwaysLoad: true };
|
|
738
|
+
allowed.push("mcp__ogb");
|
|
739
|
+
// The MCP config carries credentials — a Composio consumer key in a
|
|
740
|
+
// header, the box token in the computer proxy's env, the comms token in
|
|
741
|
+
// the agents proxy's env. On argv every one of those is world-readable
|
|
742
|
+
// through `ps` for the life of the turn, to any local process. The CLI
|
|
743
|
+
// accepts a FILE for this flag, so the secrets go in a 0600 file that
|
|
744
|
+
// is removed when the turn settles.
|
|
745
|
+
let mcpConfigPath = null;
|
|
746
|
+
if (Object.keys(mcpServers).length) {
|
|
747
|
+
mcpConfigPath = join(mkdtempSync(join(tmpdir(), "omb-mcp-")), "mcp.json");
|
|
748
|
+
args.push("--mcp-config", mcpConfigPath);
|
|
749
|
+
args.push("--allowedTools", allowed.join(","));
|
|
750
|
+
}
|
|
751
|
+
const env = claudeEnvironment(turnModel, turnEnvironment);
|
|
752
|
+
// Our approvals and browser credentials expire at the user-turn
|
|
753
|
+
// boundary. Native background workers cannot outlive that boundary;
|
|
754
|
+
// parallel bot work must use the harness's durable delegate_bot path.
|
|
755
|
+
env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS = "1";
|
|
756
|
+
const cwd = turn.cwd ?? homedir();
|
|
757
|
+
// Everything that shapes the process, minus session/turn-specific temp
|
|
758
|
+
// paths. Their contents are represented directly in the key instead.
|
|
759
|
+
const privateFileFlags = new Set(["--mcp-config"]);
|
|
760
|
+
const keyArgs = args.filter((a, i) => !privateFileFlags.has(a) && !privateFileFlags.has(args[i - 1] ?? ""));
|
|
761
|
+
const argsKey = JSON.stringify({
|
|
762
|
+
args: keyArgs,
|
|
763
|
+
system: turn.system ?? null,
|
|
764
|
+
mcpServers,
|
|
765
|
+
cwd,
|
|
766
|
+
model: injected.model ?? null,
|
|
767
|
+
base: env.ANTHROPIC_BASE_URL ?? null,
|
|
768
|
+
});
|
|
769
|
+
// Reuse the live process when it is idle, unchanged, and is the session
|
|
770
|
+
// the harness wants resumed. Anything else: close it and spawn fresh
|
|
771
|
+
// (with --resume, so the conversation continues in the new process).
|
|
772
|
+
const live = sessions.get(threadId);
|
|
773
|
+
if (live && !live.turn && !live.closing && live.child.exitCode === null && live.argsKey === argsKey && (!sessionId || sessionId === live.sessionId)) {
|
|
774
|
+
if (live.idleTimer)
|
|
775
|
+
clearTimeout(live.idleTimer);
|
|
776
|
+
live.turn = { turnId, settled: false, sawStreamDelta: false };
|
|
777
|
+
active.set(threadId, { stop: () => killCliTree(live.child), turnId, broker: live.broker });
|
|
778
|
+
emit({ ...base(threadId, turnId), type: "turn.started" });
|
|
779
|
+
const written = await writeUser(live, threadId, promptMsg);
|
|
780
|
+
if (!written) {
|
|
781
|
+
active.delete(threadId);
|
|
782
|
+
live.turn = null;
|
|
783
|
+
closeSession(threadId, "stdin write failed");
|
|
784
|
+
retryState.delete(threadId);
|
|
785
|
+
if (mcpConfigPath) {
|
|
786
|
+
try {
|
|
787
|
+
rmSync(dirname(mcpConfigPath), { recursive: true, force: true });
|
|
788
|
+
}
|
|
789
|
+
catch { }
|
|
790
|
+
}
|
|
791
|
+
throw new Error("claude session stdin is not writable");
|
|
792
|
+
}
|
|
793
|
+
// the MCP config was for the first spawn; nothing to clean here
|
|
794
|
+
if (mcpConfigPath) {
|
|
795
|
+
try {
|
|
796
|
+
rmSync(dirname(mcpConfigPath), { recursive: true, force: true });
|
|
797
|
+
}
|
|
798
|
+
catch { }
|
|
799
|
+
}
|
|
800
|
+
return { turnId };
|
|
801
|
+
}
|
|
802
|
+
if (live)
|
|
803
|
+
closeSession(threadId, "spawn contract changed");
|
|
804
|
+
// Until sessions.set() below, this turn owns every launch resource.
|
|
805
|
+
// Any bind, private-config or synchronous spawn failure must release
|
|
806
|
+
// them here rather than leave a live listener or credential temp file.
|
|
807
|
+
const cleanupUnownedLaunch = () => {
|
|
808
|
+
broker?.close();
|
|
809
|
+
broker = undefined;
|
|
810
|
+
if (mcpConfigPath) {
|
|
811
|
+
try {
|
|
812
|
+
rmSync(dirname(mcpConfigPath), { recursive: true, force: true });
|
|
813
|
+
}
|
|
814
|
+
catch { }
|
|
815
|
+
mcpConfigPath = null;
|
|
816
|
+
}
|
|
817
|
+
if (systemPromptPath) {
|
|
818
|
+
removePrivateTempDir(systemPromptPath);
|
|
819
|
+
systemPromptPath = null;
|
|
820
|
+
}
|
|
821
|
+
retryState.delete(threadId);
|
|
822
|
+
};
|
|
823
|
+
try {
|
|
824
|
+
// Create the prompt file only for a new process. A compatible live
|
|
825
|
+
// session has already consumed the same system prompt at launch.
|
|
826
|
+
if (turn.system) {
|
|
827
|
+
systemPromptPath = join(mkdtempSync(join(tmpdir(), "omb-system-")), "prompt.txt");
|
|
828
|
+
writeFileSync(systemPromptPath, turn.system, { mode: 0o600 });
|
|
829
|
+
args.push("--append-system-prompt-file", systemPromptPath);
|
|
830
|
+
}
|
|
831
|
+
// Only create a broker for a new process. A compatible retained
|
|
832
|
+
// process keeps its existing proxy connection and broker across turns.
|
|
833
|
+
if (socketPath) {
|
|
834
|
+
// remembers which tool each pending ask came from, so the resolved
|
|
835
|
+
// event can scope approvals to real desktop-control tools only
|
|
836
|
+
const askTools = new Map();
|
|
837
|
+
broker = await createPermissionBroker({
|
|
838
|
+
socketPaths: brokerSocketCandidates(threadId),
|
|
839
|
+
isActive: () => Boolean(sessions.get(threadId)?.turn),
|
|
840
|
+
onAsk: (ask) => {
|
|
841
|
+
const eventTurnId = sessions.get(threadId)?.turn?.turnId ?? turnId;
|
|
842
|
+
askTools.set(ask.id, typeof ask.tool === "string" ? ask.tool : undefined);
|
|
843
|
+
emit({
|
|
844
|
+
...base(threadId, eventTurnId),
|
|
845
|
+
type: "request.opened",
|
|
846
|
+
requestId: ask.id,
|
|
847
|
+
requestType: ask.kind,
|
|
848
|
+
tool: ask.tool,
|
|
849
|
+
summary: askSummary(ask),
|
|
850
|
+
approvalScope: typeof ask.tool === "string" && controlsHost && ask.tool.startsWith("mcp__computer")
|
|
851
|
+
? "local-computer"
|
|
852
|
+
: undefined,
|
|
853
|
+
choices: Array.isArray(ask.input?.choices) ? ask.input.choices.slice(0, 5) : undefined,
|
|
854
|
+
});
|
|
855
|
+
},
|
|
856
|
+
onResolve: (resolved) => {
|
|
857
|
+
const eventTurnId = sessions.get(threadId)?.turn?.turnId ?? turnId;
|
|
858
|
+
emit({
|
|
859
|
+
...base(threadId, eventTurnId),
|
|
860
|
+
type: "request.resolved",
|
|
861
|
+
requestId: resolved.id,
|
|
862
|
+
behavior: resolved.behavior,
|
|
863
|
+
source: resolved.source,
|
|
864
|
+
approvalScope: controlsHost && typeof askTools.get(resolved.id) === "string" && askTools.get(resolved.id).startsWith("mcp__computer") ? "local-computer" : undefined,
|
|
865
|
+
});
|
|
866
|
+
askTools.delete(resolved.id);
|
|
867
|
+
},
|
|
868
|
+
});
|
|
869
|
+
// A fallback bind means the deterministic pipe is still held by an
|
|
870
|
+
// earlier process's child. The proxy learns its path from argv, so
|
|
871
|
+
// point it at the pipe we actually bound. argsKey deliberately keeps
|
|
872
|
+
// the base path: the nonce is not part of the spawn contract, and a
|
|
873
|
+
// retained session keeps its own broker object anyway.
|
|
874
|
+
if (broker.socketPath !== socketPath && mcpConfigPath) {
|
|
875
|
+
mcpServers.ogb = { command: process.execPath, args: [PERM_PROXY_PATH, broker.socketPath], env: { ...NODE_ENV_FLAG }, alwaysLoad: true };
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
// Write once, only after the broker has selected its real endpoint.
|
|
879
|
+
if (mcpConfigPath) {
|
|
880
|
+
writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 0o600 });
|
|
881
|
+
}
|
|
882
|
+
if (sessionId)
|
|
883
|
+
args.push("--resume", sessionId);
|
|
884
|
+
else
|
|
885
|
+
args.push("--session-id", newSessionId);
|
|
886
|
+
}
|
|
887
|
+
catch (error) {
|
|
888
|
+
cleanupUnownedLaunch();
|
|
889
|
+
throw error;
|
|
890
|
+
}
|
|
891
|
+
let child;
|
|
892
|
+
try {
|
|
893
|
+
child = spawnCli(config.cli, args, {
|
|
894
|
+
cwd,
|
|
895
|
+
env,
|
|
896
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
cleanupUnownedLaunch();
|
|
901
|
+
throw error;
|
|
902
|
+
}
|
|
903
|
+
const session = {
|
|
904
|
+
child,
|
|
905
|
+
broker,
|
|
906
|
+
mcpConfigPath,
|
|
907
|
+
systemPromptPath,
|
|
908
|
+
argsKey,
|
|
909
|
+
sessionId: sessionId ?? newSessionId,
|
|
910
|
+
turn: { turnId, settled: false, sawStreamDelta: false },
|
|
911
|
+
idleTimer: null,
|
|
912
|
+
closing: false,
|
|
913
|
+
stderr: "",
|
|
914
|
+
};
|
|
915
|
+
sessions.set(threadId, session);
|
|
916
|
+
// settles the TURN, not the process: the CLI stays for the next
|
|
917
|
+
// message until it has been quiet for SESSION_IDLE_MS
|
|
918
|
+
const settle = (ok, stopReason, cost = null, usage) => {
|
|
919
|
+
const t = session.turn;
|
|
920
|
+
if (!t || t.settled)
|
|
921
|
+
return;
|
|
922
|
+
t.settled = true;
|
|
923
|
+
// Resolve any ask still open for this turn, but keep the broker
|
|
924
|
+
// listening for the next turn on the retained process. Between turns
|
|
925
|
+
// isActive() rejects late background asks without creating cards.
|
|
926
|
+
session.broker?.pause();
|
|
927
|
+
// the config file holds live credentials — the CLI read it at start;
|
|
928
|
+
// it must not sit on disk for the life of the session
|
|
929
|
+
if (session.mcpConfigPath) {
|
|
930
|
+
try {
|
|
931
|
+
rmSync(dirname(session.mcpConfigPath), { recursive: true, force: true });
|
|
932
|
+
}
|
|
933
|
+
catch { }
|
|
934
|
+
session.mcpConfigPath = null;
|
|
935
|
+
}
|
|
936
|
+
if (session.systemPromptPath) {
|
|
937
|
+
if (removePrivateTempDir(session.systemPromptPath))
|
|
938
|
+
session.systemPromptPath = null;
|
|
939
|
+
}
|
|
940
|
+
active.delete(threadId);
|
|
941
|
+
session.turn = null;
|
|
942
|
+
// A settled turn owns no retry budget. Retained CLI sessions may run
|
|
943
|
+
// many later turns on this thread, and each must start fresh.
|
|
944
|
+
retryState.delete(threadId);
|
|
945
|
+
emit({ ...base(threadId, t.turnId), type: "turn.completed", ok, stopReason, cost, ...(usage ? { usage } : {}) });
|
|
946
|
+
if (session.child.exitCode === null && !session.closing)
|
|
947
|
+
armIdle(threadId);
|
|
948
|
+
};
|
|
949
|
+
const currentTurnId = () => session.turn?.turnId ?? turnId;
|
|
950
|
+
const handleLine = (line) => {
|
|
951
|
+
let o;
|
|
952
|
+
try {
|
|
953
|
+
o = JSON.parse(line);
|
|
954
|
+
}
|
|
955
|
+
catch {
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
appendNative(threadId, { dir: "in", source: "claude.sdk.message", msg: o });
|
|
959
|
+
switch (o.type) {
|
|
960
|
+
case "system":
|
|
961
|
+
if (o.subtype === "init") {
|
|
962
|
+
if (typeof o.session_id === "string")
|
|
963
|
+
session.sessionId = o.session_id;
|
|
964
|
+
emit({ ...base(threadId, currentTurnId()), type: "session.started", sessionId: o.session_id, model: o.model });
|
|
965
|
+
}
|
|
966
|
+
else if (o.subtype === "thinking_tokens") {
|
|
967
|
+
emit({ ...base(threadId, currentTurnId()), type: "item.updated", itemType: "reasoning", tokens: o.estimated_tokens });
|
|
968
|
+
}
|
|
969
|
+
break;
|
|
970
|
+
case "stream_event": {
|
|
971
|
+
// subagent narration is dropped — N parallel Tasks would
|
|
972
|
+
// interleave their prose into one bubble (upstream-verified bug)
|
|
973
|
+
if (o.parent_tool_use_id)
|
|
974
|
+
break;
|
|
975
|
+
const ev = o.event ?? {};
|
|
976
|
+
if (ev.type !== "content_block_delta")
|
|
977
|
+
break;
|
|
978
|
+
const d = ev.delta ?? {};
|
|
979
|
+
if (d.type === "text_delta" && typeof d.text === "string" && d.text) {
|
|
980
|
+
if (session.turn)
|
|
981
|
+
session.turn.sawStreamDelta = true;
|
|
982
|
+
emit({ ...base(threadId, currentTurnId()), type: "content.delta", streamKind: "assistant_text", delta: d.text });
|
|
983
|
+
}
|
|
984
|
+
else if (d.type === "thinking_delta" && typeof d.thinking === "string" && d.thinking) {
|
|
985
|
+
emit({ ...base(threadId, currentTurnId()), type: "content.delta", streamKind: "reasoning_text", delta: d.thinking });
|
|
986
|
+
}
|
|
987
|
+
break;
|
|
988
|
+
}
|
|
989
|
+
case "assistant": {
|
|
990
|
+
const msg = o.message ?? {};
|
|
991
|
+
const text = firstText(msg.content);
|
|
992
|
+
if (text.trim()) {
|
|
993
|
+
// fallback delta for CLIs/paths that never streamed the block
|
|
994
|
+
if (!session.turn?.sawStreamDelta) {
|
|
995
|
+
emit({ ...base(threadId, currentTurnId()), type: "content.delta", streamKind: "assistant_text", delta: text });
|
|
996
|
+
}
|
|
997
|
+
if (session.turn)
|
|
998
|
+
session.turn.sawStreamDelta = false;
|
|
999
|
+
emit({ ...base(threadId, currentTurnId()), type: "item.completed", itemType: "assistant_text", text });
|
|
1000
|
+
}
|
|
1001
|
+
for (const b of Array.isArray(msg.content) ? msg.content : []) {
|
|
1002
|
+
if (b.type === "tool_use") {
|
|
1003
|
+
emit({ ...base(threadId, currentTurnId()), type: "item.started", itemType: "tool", itemId: b.id, title: b.name });
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
if (msg.usage) {
|
|
1007
|
+
emit({
|
|
1008
|
+
...base(threadId, currentTurnId()),
|
|
1009
|
+
type: "thread.token-usage.updated",
|
|
1010
|
+
input: (msg.usage.input_tokens || 0) + (msg.usage.cache_read_input_tokens || 0),
|
|
1011
|
+
output: msg.usage.output_tokens || 0,
|
|
1012
|
+
...(typeof msg.usage.cache_read_input_tokens === "number"
|
|
1013
|
+
? { cachedInput: msg.usage.cache_read_input_tokens }
|
|
1014
|
+
: {}),
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
break;
|
|
1018
|
+
}
|
|
1019
|
+
case "user":
|
|
1020
|
+
for (const b of Array.isArray(o.message?.content) ? o.message.content : []) {
|
|
1021
|
+
if (b.type === "tool_result") {
|
|
1022
|
+
emit({ ...base(threadId, currentTurnId()), type: "item.completed", itemType: "tool", itemId: b.tool_use_id, ok: !b.is_error });
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
break;
|
|
1026
|
+
case "result":
|
|
1027
|
+
// A synthetic background completion is not the result of the
|
|
1028
|
+
// submitted user turn. Settling it would revoke browser access
|
|
1029
|
+
// and deny approvals while that user turn is still running.
|
|
1030
|
+
if (o.origin?.kind === "task-notification")
|
|
1031
|
+
break;
|
|
1032
|
+
// result.usage is this invocation's total — one process per turn,
|
|
1033
|
+
// so it is the turn's figure. cache reads count as input: they
|
|
1034
|
+
// are billed (at the cache rate) and they fill the window — but
|
|
1035
|
+
// they are reported separately too, so the UI can show how much
|
|
1036
|
+
// of the figure was context re-read rather than new text.
|
|
1037
|
+
settle(o.is_error !== true, o.stop_reason ?? o.terminal_reason ?? null, o.total_cost_usd ?? null, o.usage
|
|
1038
|
+
? {
|
|
1039
|
+
input: (o.usage.input_tokens || 0) + (o.usage.cache_read_input_tokens || 0) + (o.usage.cache_creation_input_tokens || 0),
|
|
1040
|
+
output: o.usage.output_tokens || 0,
|
|
1041
|
+
...(typeof o.usage.cache_read_input_tokens === "number"
|
|
1042
|
+
? { cachedInput: o.usage.cache_read_input_tokens }
|
|
1043
|
+
: {}),
|
|
1044
|
+
}
|
|
1045
|
+
: undefined);
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
let buf = "";
|
|
1050
|
+
// decode as UTF-8 across chunk boundaries — a raw `buf += chunk` splits
|
|
1051
|
+
// multibyte characters that straddle two reads and corrupts the text
|
|
1052
|
+
child.stdout.setEncoding("utf8");
|
|
1053
|
+
child.stdout.on("data", (chunk) => {
|
|
1054
|
+
buf += chunk;
|
|
1055
|
+
let nl;
|
|
1056
|
+
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
1057
|
+
const line = buf.slice(0, nl);
|
|
1058
|
+
buf = buf.slice(nl + 1);
|
|
1059
|
+
if (line.trim())
|
|
1060
|
+
handleLine(line);
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
child.stderr.on("data", (c) => {
|
|
1064
|
+
session.stderr += c;
|
|
1065
|
+
if (session.stderr.length > 8192)
|
|
1066
|
+
session.stderr = session.stderr.slice(-8192);
|
|
1067
|
+
});
|
|
1068
|
+
child.on("error", (e) => {
|
|
1069
|
+
emit({ ...base(threadId, currentTurnId()), type: "runtime.error", ...describeSpawnFailure(e, config.cli) });
|
|
1070
|
+
settle(false, "spawn_error");
|
|
1071
|
+
});
|
|
1072
|
+
child.on("close", (code) => {
|
|
1073
|
+
// a turn still running when the process died is a failed turn; a
|
|
1074
|
+
// process that exited between turns (idle close, contract change)
|
|
1075
|
+
// is just a session ending
|
|
1076
|
+
if (session.turn && !session.turn.settled) {
|
|
1077
|
+
const message = `claude exited ${code} before result${session.stderr ? `: ${session.stderr.trim().slice(-300)}` : ""}`;
|
|
1078
|
+
const verdict = classifyError({ exitCode: code, stderr: message });
|
|
1079
|
+
if (!retry.cancelled &&
|
|
1080
|
+
code !== 0 &&
|
|
1081
|
+
verdict.transient &&
|
|
1082
|
+
!session.turn.sawStreamDelta &&
|
|
1083
|
+
retry.attempt < RETRY_MAX_ATTEMPTS - 1) {
|
|
1084
|
+
// the CLI is gone but the TURN continues: keep the thread busy,
|
|
1085
|
+
// emit no terminal event, and relaunch after the backoff. The
|
|
1086
|
+
// `active` entry STAYS — it is what makes an interrupt during
|
|
1087
|
+
// the backoff reach this turn's stop() and cancel the retry.
|
|
1088
|
+
const failedBroker = session.broker;
|
|
1089
|
+
session.broker = undefined;
|
|
1090
|
+
failedBroker?.pause();
|
|
1091
|
+
failedBroker?.close();
|
|
1092
|
+
if (session.mcpConfigPath) {
|
|
1093
|
+
try {
|
|
1094
|
+
rmSync(dirname(session.mcpConfigPath), { recursive: true, force: true });
|
|
1095
|
+
}
|
|
1096
|
+
catch { }
|
|
1097
|
+
session.mcpConfigPath = null;
|
|
1098
|
+
}
|
|
1099
|
+
if (session.systemPromptPath) {
|
|
1100
|
+
removePrivateTempDir(session.systemPromptPath);
|
|
1101
|
+
session.systemPromptPath = null;
|
|
1102
|
+
}
|
|
1103
|
+
sessions.delete(threadId);
|
|
1104
|
+
session.turn = null;
|
|
1105
|
+
retry.attempt++;
|
|
1106
|
+
const delayMs = computeBackoff(retry.attempt - 1);
|
|
1107
|
+
emit({
|
|
1108
|
+
...base(threadId, turnId),
|
|
1109
|
+
type: "turn.retrying",
|
|
1110
|
+
attempt: retry.attempt,
|
|
1111
|
+
delayMs,
|
|
1112
|
+
reason: verdict.reason,
|
|
1113
|
+
});
|
|
1114
|
+
void (async () => {
|
|
1115
|
+
const wait = interruptibleDelay(delayMs * retryScale, retryAbort.signal);
|
|
1116
|
+
await wait.promise;
|
|
1117
|
+
// an interrupt during the backoff landed here via stop(); the
|
|
1118
|
+
// turn settles as interrupted and no zombie relaunch happens
|
|
1119
|
+
if (retry.cancelled) {
|
|
1120
|
+
active.delete(threadId);
|
|
1121
|
+
retryState.delete(threadId);
|
|
1122
|
+
emit({
|
|
1123
|
+
...base(threadId, turnId),
|
|
1124
|
+
type: "turn.completed",
|
|
1125
|
+
ok: false,
|
|
1126
|
+
stopReason: "interrupted",
|
|
1127
|
+
cost: null,
|
|
1128
|
+
});
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
// hand the thread back before recursing — the relaunch's own
|
|
1132
|
+
// guard would otherwise reject it as "already running"
|
|
1133
|
+
active.delete(threadId);
|
|
1134
|
+
try {
|
|
1135
|
+
const cursor = session.sessionId ?? sessionId ?? undefined;
|
|
1136
|
+
await sendTurn({ ...turn, resumeCursor: cursor });
|
|
1137
|
+
}
|
|
1138
|
+
catch (e) {
|
|
1139
|
+
retryState.delete(threadId);
|
|
1140
|
+
emit({
|
|
1141
|
+
...base(threadId, turnId),
|
|
1142
|
+
type: "runtime.error",
|
|
1143
|
+
message: e instanceof Error ? e.message : String(e),
|
|
1144
|
+
});
|
|
1145
|
+
emit({
|
|
1146
|
+
...base(threadId, turnId),
|
|
1147
|
+
type: "turn.completed",
|
|
1148
|
+
ok: false,
|
|
1149
|
+
stopReason: "exit_before_result",
|
|
1150
|
+
cost: null,
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
})();
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
retryState.delete(threadId);
|
|
1157
|
+
emit({
|
|
1158
|
+
...base(threadId, currentTurnId()),
|
|
1159
|
+
type: "runtime.error",
|
|
1160
|
+
message,
|
|
1161
|
+
});
|
|
1162
|
+
settle(false, "exit_before_result");
|
|
1163
|
+
}
|
|
1164
|
+
if (session.idleTimer)
|
|
1165
|
+
clearTimeout(session.idleTimer);
|
|
1166
|
+
session.broker?.close();
|
|
1167
|
+
if (session.mcpConfigPath) {
|
|
1168
|
+
try {
|
|
1169
|
+
rmSync(dirname(session.mcpConfigPath), { recursive: true, force: true });
|
|
1170
|
+
}
|
|
1171
|
+
catch { }
|
|
1172
|
+
}
|
|
1173
|
+
removePrivateTempDir(session.systemPromptPath);
|
|
1174
|
+
if (sessions.get(threadId) === session)
|
|
1175
|
+
sessions.delete(threadId);
|
|
1176
|
+
});
|
|
1177
|
+
const stop = () => {
|
|
1178
|
+
retry.cancelled = true;
|
|
1179
|
+
retryAbort.abort();
|
|
1180
|
+
killCliTree(child);
|
|
1181
|
+
};
|
|
1182
|
+
active.set(threadId, { stop, turnId, broker });
|
|
1183
|
+
emit({ ...base(threadId, turnId), type: "turn.started" });
|
|
1184
|
+
// prompt over stdin as a stream-json message — never argv (ARG_MAX).
|
|
1185
|
+
// stdin stays OPEN: that is what keeps the session alive for a
|
|
1186
|
+
// mid-turn steer or the next turn; closeSession() ends it.
|
|
1187
|
+
if (!(await writeUser(session, threadId, promptMsg))) {
|
|
1188
|
+
settle(false, "stdin_write_failed");
|
|
1189
|
+
closeSession(threadId, "stdin write failed");
|
|
1190
|
+
}
|
|
1191
|
+
return { turnId };
|
|
1192
|
+
};
|
|
1193
|
+
/** A user message into the running turn: the CLI delivers it before its
|
|
1194
|
+
* next model call. False when nothing is running here to steer. */
|
|
1195
|
+
const steer = async (threadId, text) => {
|
|
1196
|
+
const s = sessions.get(threadId);
|
|
1197
|
+
if (!s || !s.turn || s.turn.settled || s.closing || s.child.exitCode !== null)
|
|
1198
|
+
return false;
|
|
1199
|
+
return writeUser(s, threadId, claudeUserMessage(text, undefined));
|
|
1200
|
+
};
|
|
1201
|
+
const snapshot = async () => {
|
|
1202
|
+
const env = claudeEnvironment(undefined, { ...process.env, ...input.environment });
|
|
1203
|
+
const version = await new Promise((resolve) => {
|
|
1204
|
+
execCli(config.cli, ["--version"], { timeout: 8000, env }, (err, stdout) => resolve(err ? null : stdout.trim()));
|
|
1205
|
+
});
|
|
1206
|
+
if (!version)
|
|
1207
|
+
return { state: "unavailable", reason: `\`${config.cli}\` CLI not found` };
|
|
1208
|
+
const authenticated = await claudeSignedIn(config.cli, env);
|
|
1209
|
+
// claudeEnvironment strips ANTHROPIC_API_KEY, so turns run on the
|
|
1210
|
+
// CLI's own login (Pro/Max): the cost it reports is what the call
|
|
1211
|
+
// WOULD bill, not a charge
|
|
1212
|
+
return { state: "available", version, authenticated, billing: "subscription" };
|
|
1213
|
+
};
|
|
1214
|
+
/** One-shot Claude call with the prompt on stdin, never argv. Approval
|
|
1215
|
+
* summaries can contain paths, commands, or secrets, so the generic
|
|
1216
|
+
* `claude -p "prompt"` shape is not safe for review. No tools or MCP
|
|
1217
|
+
* servers are mounted in this isolated process. */
|
|
1218
|
+
const generateReview = (prompt, signal) => new Promise((resolve, reject) => {
|
|
1219
|
+
const child = spawnCli(config.cli, ["-p", "--model", "claude-haiku-4-5", "--output-format", "text"], {
|
|
1220
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1221
|
+
env: claudeEnvironment("claude-haiku-4-5", { ...process.env, ...input.environment }),
|
|
1222
|
+
});
|
|
1223
|
+
let stdout = "";
|
|
1224
|
+
let stderr = "";
|
|
1225
|
+
let settled = false;
|
|
1226
|
+
const finish = (error) => {
|
|
1227
|
+
if (settled)
|
|
1228
|
+
return;
|
|
1229
|
+
settled = true;
|
|
1230
|
+
clearTimeout(timer);
|
|
1231
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1232
|
+
if (error)
|
|
1233
|
+
reject(error);
|
|
1234
|
+
else
|
|
1235
|
+
resolve(stdout.trim());
|
|
1236
|
+
};
|
|
1237
|
+
const onAbort = () => {
|
|
1238
|
+
killCliTree(child);
|
|
1239
|
+
finish(new Error("Claude review aborted"));
|
|
1240
|
+
};
|
|
1241
|
+
const timer = setTimeout(() => {
|
|
1242
|
+
killCliTree(child);
|
|
1243
|
+
finish(new Error("Claude review timed out"));
|
|
1244
|
+
}, 60_000);
|
|
1245
|
+
timer.unref?.();
|
|
1246
|
+
child.stdout.setEncoding("utf8");
|
|
1247
|
+
child.stderr.setEncoding("utf8");
|
|
1248
|
+
child.stdout.on("data", (chunk) => {
|
|
1249
|
+
stdout += chunk;
|
|
1250
|
+
if (stdout.length > 1_000_000) {
|
|
1251
|
+
killCliTree(child);
|
|
1252
|
+
finish(new Error("Claude review output exceeded 1 MB"));
|
|
1253
|
+
}
|
|
1254
|
+
});
|
|
1255
|
+
child.stderr.on("data", (chunk) => {
|
|
1256
|
+
stderr = (stderr + chunk).slice(-8_192);
|
|
1257
|
+
});
|
|
1258
|
+
child.on("error", (error) => finish(error));
|
|
1259
|
+
child.on("close", (code) => {
|
|
1260
|
+
if (code === 0)
|
|
1261
|
+
finish();
|
|
1262
|
+
else
|
|
1263
|
+
finish(new Error(stderr.trim() || `Claude review exited ${code}`));
|
|
1264
|
+
});
|
|
1265
|
+
if (signal?.aborted)
|
|
1266
|
+
onAbort();
|
|
1267
|
+
else {
|
|
1268
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1269
|
+
child.stdin.end(prompt);
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
return {
|
|
1273
|
+
instanceId,
|
|
1274
|
+
driverKind: DRIVER_KIND,
|
|
1275
|
+
displayName: input.displayName,
|
|
1276
|
+
enabled: input.enabled,
|
|
1277
|
+
get models() {
|
|
1278
|
+
return models;
|
|
1279
|
+
},
|
|
1280
|
+
refreshModels,
|
|
1281
|
+
snapshot,
|
|
1282
|
+
adapter: {
|
|
1283
|
+
provider: DRIVER_KIND,
|
|
1284
|
+
capabilities: {
|
|
1285
|
+
sessionModelSwitch: "in-session",
|
|
1286
|
+
agentsMcp: true,
|
|
1287
|
+
customMcp: true,
|
|
1288
|
+
computerMcp: true,
|
|
1289
|
+
composioMcp: true,
|
|
1290
|
+
phoneMcp: true,
|
|
1291
|
+
browserMcp: true,
|
|
1292
|
+
images: true,
|
|
1293
|
+
nativeImageInput: true,
|
|
1294
|
+
effortLevels: ["low", "medium", "high", "xhigh", "max"],
|
|
1295
|
+
queueing: true,
|
|
1296
|
+
// Harness turns reassert a per-bot mode and restore the broker even
|
|
1297
|
+
// when an old instance was configured with bypassPermissions.
|
|
1298
|
+
localComputerMcp: true,
|
|
1299
|
+
},
|
|
1300
|
+
sendTurn,
|
|
1301
|
+
steer,
|
|
1302
|
+
interruptTurn: async (threadId) => active.get(threadId)?.stop(),
|
|
1303
|
+
respondToRequest: async (threadId, requestId, decision) => {
|
|
1304
|
+
// fail-closed by construction: no broker, or an ask that already
|
|
1305
|
+
// timed out / settled, is `unavailable` — the caller denies
|
|
1306
|
+
const broker = sessions.get(threadId)?.broker ?? active.get(threadId)?.broker;
|
|
1307
|
+
if (!broker)
|
|
1308
|
+
return "unavailable";
|
|
1309
|
+
const behavior = decision.behavior === "answer" ? "answer" : decision.behavior;
|
|
1310
|
+
if (!broker.answer(requestId, behavior, decision.message))
|
|
1311
|
+
return "unavailable";
|
|
1312
|
+
return behavior === "allow" ? "allowed-once" : behavior === "answer" ? "answered" : "rejected";
|
|
1313
|
+
},
|
|
1314
|
+
hasSession: (threadId) => active.has(threadId),
|
|
1315
|
+
stopAll: async () => {
|
|
1316
|
+
for (const { stop } of active.values())
|
|
1317
|
+
stop();
|
|
1318
|
+
for (const threadId of [...sessions.keys()])
|
|
1319
|
+
closeSession(threadId, "stopAll");
|
|
1320
|
+
},
|
|
1321
|
+
onEvent: (listener) => {
|
|
1322
|
+
listeners.add(listener);
|
|
1323
|
+
return () => listeners.delete(listener);
|
|
1324
|
+
},
|
|
1325
|
+
},
|
|
1326
|
+
generateText: (prompt) => generateReview(prompt),
|
|
1327
|
+
reviewPermission: generateReview,
|
|
1328
|
+
dispose: async () => {
|
|
1329
|
+
for (const { stop } of active.values())
|
|
1330
|
+
stop();
|
|
1331
|
+
for (const threadId of [...sessions.keys()])
|
|
1332
|
+
closeSession(threadId, "dispose");
|
|
1333
|
+
listeners.clear();
|
|
1334
|
+
},
|
|
1335
|
+
};
|
|
1336
|
+
},
|
|
1337
|
+
};
|