chainlesschain 0.162.147 → 0.162.149

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -2
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-DywJ7xTv.js → AIOps-BJif14yR.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-BTlqcY-q.js → ActionButton-CXek6gJY.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-FdG1Mvwy.js → Analytics-BgQhdAJi.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CVsnbvN1.js → AppLayout-DTYl5NtR.js} +5 -5
  8. package/src/assets/web-panel/assets/{Audit-ITdpJ7vR.js → Audit-BzATQAeR.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-BfXqnXo1.js → Backup-BzdPEQhL.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-C09ip3_E.js → BaseInput-BM0KVh8E.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-qdkhB42l.js → Chat-D7fHXHkz.js} +6 -6
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-D2KfETZD.js → ChatBubbleRenderer-Wm34RR-b.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-j6WZCuff.js → Checkbox-CAiSQ9vO.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-CGZ-K3uK.js → Codegen-Df40CrEe.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-CIlPOAu6.js → Col-DIT2fNFU.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-sY-i-hic.js → Community-CSTIbW2k.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-DcoEfyL8.js → Compact-DtqXZZUi.js} +1 -1
  18. package/src/assets/web-panel/assets/Compliance-CUtzw6o7.js +1 -0
  19. package/src/assets/web-panel/assets/{Cowork-DLY_fkLv.js → Cowork-CezmlnmU.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-D049pZBC.js → Cron-P4BITarg.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DCXdE8M9.js → Crosschain-BRRiYwvq.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-JCvPZtjW.js → DID-L5ijB9i4.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-Cg2xu3iE.js → Dashboard-CFxro4ob.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-DjJLjbLh.js → Dropdown-BmAFCQ71.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-DugNcSLR.js → EmailListRenderer-DJBCEuon.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-J6Nb-E30.js → FamilyGuardDashboard-z4oLq6eT.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-CQ3Ttpbl.js → Federation-DsVCpHMF.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Cz0NKag4.js → FormItemContext-DP3bP5th.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-NYcMP6iz.js → GenericCardRenderer-BAXP2Gc6.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-B4hYN18N.js → Git-I3g_bAw9.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-DOQPCcC0.js → Governance-jHS5-ioS.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-HtLF746W.js → Inference-CPL7sfx9.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-BXrk8rMm.js → KnowledgeGraph-Jf236h4C.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-DS0JnD3-.js → Logs-Cm2tcSKg.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-Cqnjihrz.js → Marketplace-CZK82rhi.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-Cx3Psk-U.js → McpTools-CZCeji7a.js} +5 -5
  37. package/src/assets/web-panel/assets/{Memory-BjyysPtj.js → Memory-3l2KVS9k.js} +2 -2
  38. package/src/assets/web-panel/assets/MobileBridge-D3Arxfeg.css +1 -0
  39. package/src/assets/web-panel/assets/MobileBridge-VYdpoLh6.js +1 -0
  40. package/src/assets/web-panel/assets/{MobileProjects-Xwf-fdOQ.js → MobileProjects-CiB9cmm1.js} +1 -1
  41. package/src/assets/web-panel/assets/{Mtc-Djkql5m5.js → Mtc-wCFZbBuL.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-D2BAZlu0.js → MtcAudit-1a3THIyG.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-ByqP1ZzH.js → Multisig-DMzLB2hG.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-C_kn0nE2.js → NLProgramming-D9wZbf6l.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-7l7eXHPq.js → Notes-hrFe2ZM-.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-BrCDoitw.js → NotificationSettings-CHGX5Jwm.js} +1 -1
  47. package/src/assets/web-panel/assets/OrderTableRenderer-CRIFE2Tj.js +1 -0
  48. package/src/assets/web-panel/assets/{Organization-DdoNAxsM.js → Organization-By7FkhtY.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-Bw7R6dE3.js → Overflow--khGSzJn.js} +1 -1
  50. package/src/assets/web-panel/assets/{OverrideContext-C2r4vyBb.js → OverrideContext-BlgL9440.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-DJNtBugA.js → P2P-CAG9dH35.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-Dwhd5_ue.js → PdhVaultBrowser-B3SQZmK9.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-BYoSfhPR.js → Permissions-D35ec6JA.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-aYCYJbnH.js → PersonalDataHub-CxBF7hW_.js} +2 -2
  55. package/src/assets/web-panel/assets/{Pipeline-DJp6CprF.js → Pipeline-s6EtOO1s.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-Cq9HZweR.js → Privacy-BbJYmWmO.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-CmF0HsSt.js → ProjectInit-VGQq1jMT.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-P8V5Aea2.js → ProjectSettings-B1ew3Teh.js} +2 -2
  59. package/src/assets/web-panel/assets/Projects-CGcNIs_g.js +1 -0
  60. package/src/assets/web-panel/assets/{Providers-D589v0q2.js → Providers-mrO47FIK.js} +1 -1
  61. package/src/assets/web-panel/assets/QrScannerModal-IJF2mHyw.js +1 -0
  62. package/src/assets/web-panel/assets/{MobileBridge-zJpPuf4E.css → QrScannerModal-NwAHNfiJ.css} +1 -1
  63. package/src/assets/web-panel/assets/{QuickAsk-lvRV2osB.js → QuickAsk-CU0wN_Z0.js} +1 -1
  64. package/src/assets/web-panel/assets/{Recommend-sb84MTpY.js → Recommend-DhN37R6G.js} +1 -1
  65. package/src/assets/web-panel/assets/RemoteSession-D_xOX_hu.js +4 -0
  66. package/src/assets/web-panel/assets/{Reputation-d9Y3vy-W.js → Reputation-D_Ssv7uH.js} +1 -1
  67. package/src/assets/web-panel/assets/{Row-DMdhLUTp.js → Row-Kx5dKxX7.js} +1 -1
  68. package/src/assets/web-panel/assets/{RssFeed-OL-SMDkz.js → RssFeed-BlGwqZkY.js} +2 -2
  69. package/src/assets/web-panel/assets/{Search-BjStLKq1.js → Search-hLFQzxpb.js} +1 -1
  70. package/src/assets/web-panel/assets/{Security-MeI411qr.js → Security-DMlvsVt4.js} +2 -2
  71. package/src/assets/web-panel/assets/{Services-UajosuhT.js → Services-Bd0Qsj2m.js} +2 -2
  72. package/src/assets/web-panel/assets/{Skeleton-CGpG-QJ1.js → Skeleton-DFclq9X3.js} +1 -1
  73. package/src/assets/web-panel/assets/{Skills-C6P2RPY-.js → Skills-BNtln2qY.js} +1 -1
  74. package/src/assets/web-panel/assets/{Sla-B_fPprgw.js → Sla-Dch5H19G.js} +1 -1
  75. package/src/assets/web-panel/assets/{SpeechSettings-CqxLjSlQ.js → SpeechSettings-2UfQcU1g.js} +1 -1
  76. package/src/assets/web-panel/assets/{SyncSettings-DixSfFOQ.js → SyncSettings-CD9YQr4i.js} +2 -2
  77. package/src/assets/web-panel/assets/{Tasks-PavzPBxc.js → Tasks-BNu-7MzI.js} +1 -1
  78. package/src/assets/web-panel/assets/{Templates-BYdGRnfX.js → Templates-D1n9CaIR.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tenant-BQLgnarD.js → Tenant-DenL3iBW.js} +1 -1
  80. package/src/assets/web-panel/assets/Terminal-ZzU0Io1Y.js +3 -0
  81. package/src/assets/web-panel/assets/{TimelineRenderer-Bv7uZRM7.js → TimelineRenderer-D87IfukO.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tokens-DxhgszRJ.js → Tokens-BkiZc8E3.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trigger-nQYHayuc.js → Trigger-BNRSytGN.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trust-CvqZDuZ4.js → Trust-Bwbd4X2m.js} +1 -1
  85. package/src/assets/web-panel/assets/{UkeySign-DaR8GV2I.js → UkeySign-B8O2bs0o.js} +1 -1
  86. package/src/assets/web-panel/assets/{VideoEditing-C6Mpsokw.js → VideoEditing-C2fL8zmH.js} +1 -1
  87. package/src/assets/web-panel/assets/{Wallet-B57VRrWc.js → Wallet-BORYDdE1.js} +4 -4
  88. package/src/assets/web-panel/assets/{WebAuthn-BbtEEtpq.js → WebAuthn-C0V5S2FM.js} +4 -4
  89. package/src/assets/web-panel/assets/{WorkflowEditor-C5LwvKmH.js → WorkflowEditor-2dR0fcHL.js} +1 -1
  90. package/src/assets/web-panel/assets/{chat-MzuPR5jh.js → chat-BmuAFAn8.js} +1 -1
  91. package/src/assets/web-panel/assets/{collapseMotion-mxzzcDsg.js → collapseMotion-DVfhbsdP.js} +1 -1
  92. package/src/assets/web-panel/assets/{colors-HyW8mi_y.js → colors-CN3smMcK.js} +1 -1
  93. package/src/assets/web-panel/assets/{compact-item-DvUSzWAp.js → compact-item-DCnxb4kW.js} +1 -1
  94. package/src/assets/web-panel/assets/{createContext-4MppZl7X.js → createContext-vlR43pHn.js} +1 -1
  95. package/src/assets/web-panel/assets/devWarning-DxwmsL4j.js +1 -0
  96. package/src/assets/web-panel/assets/{echarts-jxgIOFUZ.js → echarts-CL0F2SnX.js} +1 -1
  97. package/src/assets/web-panel/assets/{hasIn-D9q3CJ-u.js → hasIn-D04tHYFd.js} +1 -1
  98. package/src/assets/web-panel/assets/{icons-DP3uiYxy.js → icons-BsDmGpcT.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-C9I5MuJ0.js → index-0DnaZGkC.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Caqtu6Et.js → index-430S68MN.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-eOQO6KnV.js → index-B78xL3s2.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Ci6adKh8.js → index-BTPEZTk1.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-CifKUNtV.js → index-BXRg8GFQ.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-D7TDMMfu.js → index-BasvsWDM.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DNYqw0MO.js → index-BeGDEXFs.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-m0b6Fj9q.js → index-BqhASEL4.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BF2JsbiX.js → index-BudvKQfG.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DFqvWeGc.js → index-C-6NO5il.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-SDblbKj6.js → index-C5Sxb1sE.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-UUMCA2Sq.js → index-CP9m9Ggr.js} +4 -4
  111. package/src/assets/web-panel/assets/{index--Bm6OqmU.js → index-CTOCnIQ7.js} +1 -1
  112. package/src/assets/web-panel/assets/index-Cg2ldRfU.js +1 -0
  113. package/src/assets/web-panel/assets/{index-Dv7ffI2g.js → index-ClhAHDK3.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BcunsBIx.js → index-CnXebZLL.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-CvAqCAo9.js → index-CngCC8hC.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DcMkjb92.js → index-Cr-A0FYJ.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BFGfC5RS.js → index-CsAsCPJ6.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-DDZ6fP5Z.js → index-D28DeAAB.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CgIfXOD9.js → index-D5LZVZ0L.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DmZ6-suL.js → index-D65_XseV.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-Cs-RKRUM.js → index-D8vwp4qp.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-B994UQfE.js → index-DDwLgQY9.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-Btz8pU68.js → index-DIpY0qM5.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-B5E4DhVc.js → index-DTWg-18U.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-D2jrnaq3.js → index-DTtRscUa.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BmCo2Xdp.js → index-DYAtmqiv.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-Zvtru2oE.js → index-D_n8_vfI.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CLAZs1Vd.js → index-DbADHwhr.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-CucRo4Vz.js → index-DbVsQBOH.js} +1 -1
  130. package/src/assets/web-panel/assets/index-Dwzo-dtJ.js +1 -0
  131. package/src/assets/web-panel/assets/{index-C0DiL97c.js → index-HhNjnCfe.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-Bl-E4W4q.js → index-ToUh2b1-.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-ryFLxB1p.js → index-TslMTwNy.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-BGQtFso0.js → index-_FIkN3jd.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-zLwYpvX0.js → index-hpvvtAZ3.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-Dvg5xYuP.js → index-jzR7Ujuw.js} +1 -1
  137. package/src/assets/web-panel/assets/{index-xvUVuFnA.js → index-ljuBiQW9.js} +1 -1
  138. package/src/assets/web-panel/assets/{initDefaultProps-CC--PMsC.js → initDefaultProps-C3wUb4je.js} +1 -1
  139. package/src/assets/web-panel/assets/{motion-DdrBjRF1.js → motion-D-jYFUNA.js} +1 -1
  140. package/src/assets/web-panel/assets/{move-D712Gwl2.js → move-tqb8nwJ2.js} +1 -1
  141. package/src/assets/web-panel/assets/{mtc-parser-39y-keKO.js → mtc-parser-DSOjMJMz.js} +1 -1
  142. package/src/assets/web-panel/assets/{omit-6Y51gDB9.js → omit-CJ7VimIW.js} +1 -1
  143. package/src/assets/web-panel/assets/{pickAttrs-Cy6EHbq9.js → pickAttrs-qXiG1iT3.js} +1 -1
  144. package/src/assets/web-panel/assets/{placementArrow-BYxdAm0p.js → placementArrow-CkGBcy1Z.js} +1 -1
  145. package/src/assets/web-panel/assets/{responsiveObserve-C9R6q_cr.js → responsiveObserve-D3WvTlLO.js} +1 -1
  146. package/src/assets/web-panel/assets/{slide-CXUJlilB.js → slide-BEE2ftge.js} +1 -1
  147. package/src/assets/web-panel/assets/{statusUtils-DOtrOeql.js → statusUtils-CsfBpxiG.js} +1 -1
  148. package/src/assets/web-panel/assets/{styleChecker-D5r39o92.js → styleChecker-xPrIiJPI.js} +1 -1
  149. package/src/assets/web-panel/assets/useFlexGapSupport-C5Yd-SCR.js +1 -0
  150. package/src/assets/web-panel/assets/{useFs-BkrT-Zbc.js → useFs-D_MDS8HI.js} +1 -1
  151. package/src/assets/web-panel/assets/{useMergedState-Cl2q1fes.js → useMergedState-Dc9sO2d7.js} +1 -1
  152. package/src/assets/web-panel/assets/{usePersonalDataHub-BwnnnZjU.js → usePersonalDataHub-BqJh-usA.js} +1 -1
  153. package/src/assets/web-panel/assets/{useRefs-DD9q4mOT.js → useRefs-CAeFRzPf.js} +1 -1
  154. package/src/assets/web-panel/assets/{useState-BK4hy8Ma.js → useState-LwExUYDO.js} +1 -1
  155. package/src/assets/web-panel/assets/{vendor-BvqAck49.js → vendor-BMxUs6UX.js} +16 -16
  156. package/src/assets/web-panel/assets/{vnode-SU-N4pum.js → vnode-chm_p-gz.js} +1 -1
  157. package/src/assets/web-panel/assets/{zoom-DHScfpTP.js → zoom-3-YDNz0Z.js} +1 -1
  158. package/src/assets/web-panel/index.html +3 -3
  159. package/src/assets/web-panel/remote-session-sw.js +43 -0
  160. package/src/command-manifest.json +22 -1
  161. package/src/commands/agent.js +44 -5
  162. package/src/commands/codeintel.js +252 -0
  163. package/src/commands/eval.js +248 -0
  164. package/src/commands/plugin.js +354 -0
  165. package/src/commands/serve.js +10 -0
  166. package/src/commands/team.js +456 -0
  167. package/src/gateways/remote-session-relay.js +160 -0
  168. package/src/gateways/ws/message-dispatcher.js +77 -2
  169. package/src/gateways/ws/remote-session-protocol.js +466 -0
  170. package/src/gateways/ws/ws-server.js +299 -0
  171. package/src/harness/golden-transcript.js +65 -0
  172. package/src/harness/mcp-client.js +6 -2
  173. package/src/harness/prompt-compressor.js +27 -1
  174. package/src/harness/remote-command-ledger.js +158 -0
  175. package/src/harness/remote-session-audit-sink.js +132 -0
  176. package/src/harness/remote-session-audit.js +110 -0
  177. package/src/harness/remote-session-crypto.js +217 -0
  178. package/src/harness/remote-session-push-apns.js +246 -0
  179. package/src/harness/remote-session-push-errors.js +15 -0
  180. package/src/harness/remote-session-push-fcm.js +238 -0
  181. package/src/harness/remote-session-push-huawei.js +174 -0
  182. package/src/harness/remote-session-push-oppo.js +171 -0
  183. package/src/harness/remote-session-push-senders.js +42 -0
  184. package/src/harness/remote-session-push-vivo.js +179 -0
  185. package/src/harness/remote-session-push-web.js +299 -0
  186. package/src/harness/remote-session-push-xiaomi.js +99 -0
  187. package/src/harness/remote-session-push.js +104 -0
  188. package/src/harness/remote-session-registry.js +420 -0
  189. package/src/index.js +6 -0
  190. package/src/lib/agent-core.js +2 -0
  191. package/src/lib/agent-sandbox.js +161 -5
  192. package/src/lib/agent-team/task-lease.js +434 -0
  193. package/src/lib/agent-team/team-budget.js +165 -0
  194. package/src/lib/agent-team/team-mailbox.js +106 -0
  195. package/src/lib/agent-team/team-runner.js +266 -0
  196. package/src/lib/agent-team/team-worktree.js +204 -0
  197. package/src/lib/agents.js +18 -0
  198. package/src/lib/async-hook-supervisor.cjs +305 -0
  199. package/src/lib/did-manager.js +6 -4
  200. package/src/lib/eval/runner.js +163 -0
  201. package/src/lib/eval/tasks.js +456 -0
  202. package/src/lib/eval/trend.js +185 -0
  203. package/src/lib/lsp/__tests__/code-intelligence.integration.test.js +129 -0
  204. package/src/lib/lsp/__tests__/code-intelligence.test.js +228 -0
  205. package/src/lib/lsp/__tests__/jsonrpc-stream.test.js +104 -0
  206. package/src/lib/lsp/__tests__/lsp-client.test.js +258 -0
  207. package/src/lib/lsp/__tests__/lsp-manager.test.js +264 -0
  208. package/src/lib/lsp/__tests__/lsp-server-registry.test.js +128 -0
  209. package/src/lib/lsp/code-intelligence.js +356 -0
  210. package/src/lib/lsp/jsonrpc-stream.js +139 -0
  211. package/src/lib/lsp/lsp-client.js +318 -0
  212. package/src/lib/lsp/lsp-manager.js +323 -0
  213. package/src/lib/lsp/lsp-server-registry.js +196 -0
  214. package/src/lib/mcp-oauth.js +23 -6
  215. package/src/lib/plugin-monitor-supervisor.js +238 -0
  216. package/src/lib/plugin-runtime/agents.js +51 -0
  217. package/src/lib/plugin-runtime/bin.js +100 -0
  218. package/src/lib/plugin-runtime/hooks.js +100 -0
  219. package/src/lib/plugin-runtime/install.js +388 -0
  220. package/src/lib/plugin-runtime/lsp.js +75 -0
  221. package/src/lib/plugin-runtime/manifest.js +458 -0
  222. package/src/lib/plugin-runtime/mcp.js +89 -0
  223. package/src/lib/plugin-runtime/monitors.js +100 -0
  224. package/src/lib/plugin-runtime/policy.js +144 -0
  225. package/src/lib/plugin-runtime/reload.js +79 -0
  226. package/src/lib/plugin-runtime/scopes.js +197 -0
  227. package/src/lib/plugin-runtime/settings.js +119 -0
  228. package/src/lib/plugin-runtime/signature.js +107 -0
  229. package/src/lib/plugin-runtime/skills.js +43 -0
  230. package/src/lib/plugin-runtime/trust.js +140 -0
  231. package/src/lib/sandbox-egress-proxy.js +162 -0
  232. package/src/lib/sandbox-network-policy.js +134 -0
  233. package/src/lib/sandbox-v2.js +10 -4
  234. package/src/lib/session-manager.js +7 -3
  235. package/src/lib/settings-hook-events.cjs +78 -6
  236. package/src/lib/settings-hooks.cjs +29 -3
  237. package/src/lib/settings-loader.cjs +35 -1
  238. package/src/lib/skill-loader.js +18 -0
  239. package/src/lib/telemetry/span-recorder.js +237 -0
  240. package/src/repl/agent-repl.js +400 -4
  241. package/src/runtime/agent-core.js +546 -52
  242. package/src/runtime/agent-runtime.js +8 -13
  243. package/src/runtime/coding-agent-contract-shared.cjs +82 -8
  244. package/src/runtime/coding-agent-policy.cjs +32 -7
  245. package/src/runtime/fallback-model.js +134 -7
  246. package/src/runtime/headless-runner.js +353 -108
  247. package/src/runtime/mcp-config.js +46 -0
  248. package/src/runtime/policies/agent-policy.js +2 -0
  249. package/src/assets/web-panel/assets/Compliance-T8EtTecW.js +0 -1
  250. package/src/assets/web-panel/assets/MobileBridge-CYaITZ7w.js +0 -1
  251. package/src/assets/web-panel/assets/OrderTableRenderer-CmtqIdwr.js +0 -1
  252. package/src/assets/web-panel/assets/Projects-DPd5-lNS.js +0 -1
  253. package/src/assets/web-panel/assets/Terminal-BzOHpq-B.js +0 -3
  254. package/src/assets/web-panel/assets/devWarning-6YJJMjSH.js +0 -1
  255. package/src/assets/web-panel/assets/index-D_oeGLr6.js +0 -1
  256. package/src/assets/web-panel/assets/index-Qa57YG19.js +0 -1
  257. package/src/assets/web-panel/assets/useFlexGapSupport-DfAD5U4Z.js +0 -1
@@ -0,0 +1,179 @@
1
+ // Remote Session vendor push — vivo (vpush) sender.
2
+ //
3
+ // A concrete implementation of the RemoteSessionPushDispatcher `sender` contract
4
+ // for vivo/iQOO devices via the vivo Push server API. Auth is a two-step
5
+ // handshake: sign = MD5(appId + appKey + timestamp + appSecret) is exchanged for
6
+ // a short-lived `authToken` (valid ~24h), then a JSON POST to the single-send
7
+ // endpoint carries that token in the `authToken` header. Plain HTTPS with an
8
+ // injectable `fetch` (default global), fully unit-testable with a fake
9
+ // transport. The payload carries only routing ids (no session content).
10
+
11
+ import { createHash, randomUUID } from "node:crypto";
12
+ import { PushTokenUnregisteredError } from "./remote-session-push-errors.js";
13
+
14
+ const DEFAULT_HOST = "https://api-push.vivo.com.cn";
15
+ const AUTH_PATH = "/message/auth";
16
+ const SEND_PATH = "/message/send";
17
+ // vivo authToken is valid for ~24h; refresh a few minutes early.
18
+ const AUTH_TOKEN_TTL_MS = 24 * 60 * 60 * 1000;
19
+ const AUTH_TOKEN_SKEW_MS = 5 * 60_000;
20
+ const RESULT_SUCCESS = 0;
21
+ // vivo regId-invalid result codes (single-send). 10302/10303 cover an illegal
22
+ // regId or one that does not belong to this app.
23
+ const INVALID_TOKEN_CODES = new Set([10302, 10303]);
24
+ // vivo has no single canonical string either; also match the desc text.
25
+ const INVALID_TOKEN_RE =
26
+ /invalid.*reg|reg.*invalid|illegal|unregist|not ?exist|无效|不存在|非法/i;
27
+
28
+ async function safeText(res) {
29
+ try {
30
+ return await res.text();
31
+ } catch {
32
+ return "";
33
+ }
34
+ }
35
+
36
+ function md5Hex(input) {
37
+ return createHash("md5").update(input, "utf8").digest("hex");
38
+ }
39
+
40
+ /** Mints + caches a vivo authToken from app id + key + secret. */
41
+ export class VivoAuthProvider {
42
+ constructor(options = {}) {
43
+ if (!options.appId) throw new Error("VivoAuthProvider requires an appId");
44
+ if (!options.appKey) throw new Error("VivoAuthProvider requires an appKey");
45
+ if (!options.appSecret) {
46
+ throw new Error("VivoAuthProvider requires an appSecret");
47
+ }
48
+ this.appId = options.appId;
49
+ this.appKey = options.appKey;
50
+ this.appSecret = options.appSecret;
51
+ this.host = options.host || DEFAULT_HOST;
52
+ this.now = options.now || Date.now;
53
+ this.fetch = options.fetch || globalThis.fetch;
54
+ this._cached = null; // { token, expiresAt }
55
+ }
56
+
57
+ async getAuthToken() {
58
+ const now = this.now();
59
+ if (this._cached && this._cached.expiresAt - AUTH_TOKEN_SKEW_MS > now) {
60
+ return this._cached.token;
61
+ }
62
+ const timestamp = now;
63
+ const sign = md5Hex(
64
+ `${this.appId}${this.appKey}${timestamp}${this.appSecret}`,
65
+ );
66
+ const res = await this.fetch(`${this.host}${AUTH_PATH}`, {
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify({
70
+ appId: this.appId,
71
+ appKey: this.appKey,
72
+ timestamp,
73
+ sign,
74
+ }),
75
+ });
76
+ if (!res.ok) {
77
+ throw new Error(
78
+ `vivo auth failed (${res.status}): ${await safeText(res)}`,
79
+ );
80
+ }
81
+ const json = await res.json();
82
+ if (Number(json.result) !== RESULT_SUCCESS || !json.authToken) {
83
+ throw new Error(
84
+ `vivo auth response missing authToken: ${json.desc || json.result}`,
85
+ );
86
+ }
87
+ this._cached = {
88
+ token: json.authToken,
89
+ expiresAt: now + AUTH_TOKEN_TTL_MS,
90
+ };
91
+ return this._cached.token;
92
+ }
93
+ }
94
+
95
+ /** Sends one notification to a vivo regId. */
96
+ export class VivoPushSender {
97
+ constructor(options = {}) {
98
+ if (!options.authProvider) {
99
+ throw new Error("VivoPushSender requires an authProvider");
100
+ }
101
+ this.authProvider = options.authProvider;
102
+ this.host = options.host || DEFAULT_HOST;
103
+ this.fetch = options.fetch || globalThis.fetch;
104
+ // vivo requires a unique requestId per message; injectable for tests.
105
+ this.requestId =
106
+ options.requestId || (() => randomUUID().replace(/-/g, ""));
107
+ }
108
+
109
+ /** Bound function matching the dispatcher's `sender` contract. */
110
+ get handler() {
111
+ return (payload) => this.send(payload);
112
+ }
113
+
114
+ async send({ token, notification, sessionId, clientId } = {}) {
115
+ if (!token) throw new Error("vivo send requires a regId");
116
+ const authToken = await this.authProvider.getAuthToken();
117
+ // Routing ids only — no session content.
118
+ const custom = { type: "remote-session.approval-request" };
119
+ if (sessionId != null) custom.sessionId = String(sessionId);
120
+ if (clientId != null) custom.clientId = String(clientId);
121
+ const message = {
122
+ regId: token,
123
+ notifyType: 1, // 1 = notification bar
124
+ title: notification?.title || "Approval requested",
125
+ content: notification?.body || "A coding session needs your approval",
126
+ skipType: 1, // 1 = open the app
127
+ requestId: this.requestId(),
128
+ clientCustomMap: custom,
129
+ };
130
+ const res = await this.fetch(`${this.host}${SEND_PATH}`, {
131
+ method: "POST",
132
+ headers: {
133
+ "Content-Type": "application/json",
134
+ authToken,
135
+ },
136
+ body: JSON.stringify(message),
137
+ });
138
+ const json = await res.json().catch(() => ({}));
139
+ if (res.ok && Number(json.result) === RESULT_SUCCESS) {
140
+ return { id: json.taskId || null };
141
+ }
142
+ const code = Number(json.result);
143
+ const desc = json.desc || `HTTP ${res.status}`;
144
+ if (INVALID_TOKEN_CODES.has(code) || INVALID_TOKEN_RE.test(String(desc))) {
145
+ throw new PushTokenUnregisteredError(`vivo regId invalid: ${desc}`);
146
+ }
147
+ throw new Error(
148
+ `vivo push failed (${Number.isNaN(code) ? res.status : code}): ${desc}`,
149
+ );
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Build a bound vivo `sender` from env, or null when unconfigured.
155
+ *
156
+ * vivo env:
157
+ * CHAINLESSCHAIN_REMOTE_SESSION_VIVO_APP_ID
158
+ * CHAINLESSCHAIN_REMOTE_SESSION_VIVO_APP_KEY
159
+ * CHAINLESSCHAIN_REMOTE_SESSION_VIVO_APP_SECRET
160
+ * CHAINLESSCHAIN_REMOTE_SESSION_VIVO_HOST (optional; override the API host)
161
+ */
162
+ export function createVivoPushSender(env = {}, options = {}) {
163
+ const appId = options.appId || env.CHAINLESSCHAIN_REMOTE_SESSION_VIVO_APP_ID;
164
+ const appKey =
165
+ options.appKey || env.CHAINLESSCHAIN_REMOTE_SESSION_VIVO_APP_KEY;
166
+ const appSecret =
167
+ options.appSecret || env.CHAINLESSCHAIN_REMOTE_SESSION_VIVO_APP_SECRET;
168
+ if (!appId || !appKey || !appSecret) return null;
169
+ const host = options.host || env.CHAINLESSCHAIN_REMOTE_SESSION_VIVO_HOST;
170
+ let authProvider;
171
+ try {
172
+ authProvider =
173
+ options.authProvider ||
174
+ new VivoAuthProvider({ appId, appKey, appSecret, host, ...options });
175
+ } catch {
176
+ return null; // Malformed credentials — stay disabled rather than crash.
177
+ }
178
+ return new VivoPushSender({ authProvider, host, ...options }).handler;
179
+ }
@@ -0,0 +1,299 @@
1
+ // Remote Session vendor push — Web Push (browser) sender.
2
+ //
3
+ // A concrete implementation of the RemoteSessionPushDispatcher `sender` contract
4
+ // for browser clients, using the Web Push protocol: VAPID (RFC 8292) auth via an
5
+ // ES256 JWT signed with the server VAPID key, and RFC 8291 (aes128gcm) payload
6
+ // encryption bound to the subscription's ECDH public key + auth secret. The
7
+ // "token" is the browser PushSubscription (endpoint + keys) as JSON.
8
+ //
9
+ // Web Push endpoints accept plain HTTPS, so the network transport is an
10
+ // injectable `fetch` (default global). Encryption/signing use node:crypto and
11
+ // are exercised by a real encrypt→decrypt round-trip in the tests — no live
12
+ // push service or credentials needed. The payload carries only routing ids.
13
+
14
+ import crypto from "node:crypto";
15
+ import { PushTokenUnregisteredError } from "./remote-session-push-errors.js";
16
+
17
+ const VAPID_TTL_MS = 12 * 60 * 60 * 1000;
18
+ const VAPID_SKEW_MS = 60 * 1000;
19
+ const RECORD_SIZE = 4096;
20
+ const DEFAULT_TTL_SECONDS = 2419200; // 28 days, capped by the push service
21
+
22
+ function base64url(buf) {
23
+ return Buffer.from(buf)
24
+ .toString("base64")
25
+ .replace(/\+/g, "-")
26
+ .replace(/\//g, "_")
27
+ .replace(/=+$/, "");
28
+ }
29
+
30
+ function fromBase64url(value) {
31
+ return Buffer.from(String(value), "base64url");
32
+ }
33
+
34
+ function hmac(key, data) {
35
+ return crypto.createHmac("sha256", key).update(data).digest();
36
+ }
37
+
38
+ // HKDF-Expand for a single output block (len <= 32).
39
+ function hkdfExpand(prk, info, len) {
40
+ return hmac(prk, Buffer.concat([info, Buffer.from([0x01])])).subarray(0, len);
41
+ }
42
+
43
+ // HKDF(salt, ikm, info, len) = Expand(Extract(salt, ikm), info, len).
44
+ function hkdf(salt, ikm, info, len) {
45
+ return hkdfExpand(hmac(salt, ikm), info, len);
46
+ }
47
+
48
+ function uint32be(value) {
49
+ const buf = Buffer.alloc(4);
50
+ buf.writeUInt32BE(value);
51
+ return buf;
52
+ }
53
+
54
+ // RFC 8291 §3.4 + RFC 8188 §2.1 content-encryption key + nonce.
55
+ function deriveContentKeys({
56
+ ecdhSecret,
57
+ authSecret,
58
+ salt,
59
+ uaPublic,
60
+ asPublic,
61
+ }) {
62
+ const keyInfo = Buffer.concat([
63
+ Buffer.from("WebPush: info\0", "utf8"),
64
+ uaPublic,
65
+ asPublic,
66
+ ]);
67
+ const ikm = hkdf(authSecret, ecdhSecret, keyInfo, 32);
68
+ const prk = hmac(salt, ikm); // HKDF-Extract(salt, IKM)
69
+ const cek = hkdfExpand(
70
+ prk,
71
+ Buffer.from("Content-Encoding: aes128gcm\0", "utf8"),
72
+ 16,
73
+ );
74
+ const nonce = hkdfExpand(
75
+ prk,
76
+ Buffer.from("Content-Encoding: nonce\0", "utf8"),
77
+ 12,
78
+ );
79
+ return { cek, nonce };
80
+ }
81
+
82
+ /**
83
+ * Encrypt a payload for a Web Push subscription (RFC 8291, aes128gcm). Returns
84
+ * the full content-encoded body (header + ciphertext). Generates a fresh
85
+ * ephemeral key each call unless one is injected (tests).
86
+ */
87
+ export function encryptWebPushPayload({
88
+ payload,
89
+ uaPublicKey,
90
+ authSecret,
91
+ ecdh,
92
+ salt,
93
+ }) {
94
+ const server = ecdh || crypto.createECDH("prime256v1");
95
+ if (!ecdh) server.generateKeys();
96
+ const asPublic = server.getPublicKey();
97
+ const ecdhSecret = server.computeSecret(uaPublicKey);
98
+ const useSalt = salt || crypto.randomBytes(16);
99
+ const { cek, nonce } = deriveContentKeys({
100
+ ecdhSecret,
101
+ authSecret,
102
+ salt: useSalt,
103
+ uaPublic: uaPublicKey,
104
+ asPublic,
105
+ });
106
+ const data = Buffer.isBuffer(payload)
107
+ ? payload
108
+ : Buffer.from(payload, "utf8");
109
+ const record = Buffer.concat([data, Buffer.from([0x02])]); // last-record delimiter
110
+ const cipher = crypto.createCipheriv("aes-128-gcm", cek, nonce);
111
+ const ciphertext = Buffer.concat([
112
+ cipher.update(record),
113
+ cipher.final(),
114
+ cipher.getAuthTag(),
115
+ ]);
116
+ const header = Buffer.concat([
117
+ useSalt,
118
+ uint32be(RECORD_SIZE),
119
+ Buffer.from([asPublic.length]),
120
+ asPublic,
121
+ ]);
122
+ return Buffer.concat([header, ciphertext]);
123
+ }
124
+
125
+ /** Inverse of {@link encryptWebPushPayload}; used for interop tests. */
126
+ export function decryptWebPushPayload({ body, uaEcdh, authSecret }) {
127
+ const salt = body.subarray(0, 16);
128
+ const idlen = body.readUInt8(20);
129
+ const asPublic = body.subarray(21, 21 + idlen);
130
+ const ciphertext = body.subarray(21 + idlen);
131
+ const ecdhSecret = uaEcdh.computeSecret(asPublic);
132
+ const { cek, nonce } = deriveContentKeys({
133
+ ecdhSecret,
134
+ authSecret,
135
+ salt,
136
+ uaPublic: uaEcdh.getPublicKey(),
137
+ asPublic,
138
+ });
139
+ const tag = ciphertext.subarray(ciphertext.length - 16);
140
+ const body2 = ciphertext.subarray(0, ciphertext.length - 16);
141
+ const decipher = crypto.createDecipheriv("aes-128-gcm", cek, nonce);
142
+ decipher.setAuthTag(tag);
143
+ const record = Buffer.concat([decipher.update(body2), decipher.final()]);
144
+ let end = record.length - 1;
145
+ while (end >= 0 && record[end] === 0x00) end -= 1; // strip padding
146
+ return record.subarray(0, end); // drop the 0x02 delimiter
147
+ }
148
+
149
+ /** Builds an EC private key object from raw base64url VAPID keys. */
150
+ function importVapidKey(publicKeyB64url, privateKeyB64url) {
151
+ const pub = fromBase64url(publicKeyB64url);
152
+ if (pub.length !== 65 || pub[0] !== 0x04) {
153
+ throw new Error(
154
+ "VAPID public key must be a 65-byte uncompressed P-256 key",
155
+ );
156
+ }
157
+ return crypto.createPrivateKey({
158
+ key: {
159
+ kty: "EC",
160
+ crv: "P-256",
161
+ d: privateKeyB64url,
162
+ x: base64url(pub.subarray(1, 33)),
163
+ y: base64url(pub.subarray(33, 65)),
164
+ },
165
+ format: "jwk",
166
+ });
167
+ }
168
+
169
+ /** Mints + caches a VAPID (RFC 8292) ES256 JWT per push-endpoint origin. */
170
+ export class VapidTokenProvider {
171
+ constructor(options = {}) {
172
+ if (!options.publicKey)
173
+ throw new Error("VapidTokenProvider requires publicKey");
174
+ if (!options.privateKey)
175
+ throw new Error("VapidTokenProvider requires privateKey");
176
+ if (!options.subject) {
177
+ throw new Error("VapidTokenProvider requires a subject (mailto: or URL)");
178
+ }
179
+ this.publicKey = options.publicKey;
180
+ this.subject = options.subject;
181
+ this.now = options.now || Date.now;
182
+ this._key =
183
+ options.key || importVapidKey(options.publicKey, options.privateKey);
184
+ this.sign =
185
+ options.sign ||
186
+ ((input, key) =>
187
+ crypto.sign("sha256", Buffer.from(input), {
188
+ key,
189
+ dsaEncoding: "ieee-p1363",
190
+ }));
191
+ this._cache = new Map(); // origin → { token, expiresAt }
192
+ }
193
+
194
+ getToken(aud) {
195
+ const now = this.now();
196
+ const cached = this._cache.get(aud);
197
+ if (cached && cached.expiresAt - VAPID_SKEW_MS > now) return cached.token;
198
+ const header = base64url(JSON.stringify({ typ: "JWT", alg: "ES256" }));
199
+ const payload = base64url(
200
+ JSON.stringify({
201
+ aud,
202
+ exp: Math.floor((now + VAPID_TTL_MS) / 1000),
203
+ sub: this.subject,
204
+ }),
205
+ );
206
+ const signingInput = `${header}.${payload}`;
207
+ const token = `${signingInput}.${base64url(this.sign(signingInput, this._key))}`;
208
+ this._cache.set(aud, { token, expiresAt: now + VAPID_TTL_MS });
209
+ return token;
210
+ }
211
+ }
212
+
213
+ /** Sends one encrypted Web Push message to a browser PushSubscription. */
214
+ export class WebPushSender {
215
+ constructor(options = {}) {
216
+ if (!options.vapid)
217
+ throw new Error("WebPushSender requires a vapid provider");
218
+ this.vapid = options.vapid;
219
+ this.fetch = options.fetch || globalThis.fetch;
220
+ this.ttl = options.ttl || DEFAULT_TTL_SECONDS;
221
+ }
222
+
223
+ get handler() {
224
+ return (payload) => this.send(payload);
225
+ }
226
+
227
+ async send({ token, notification, sessionId, clientId } = {}) {
228
+ if (!token) throw new Error("Web Push send requires a subscription");
229
+ const subscription = typeof token === "string" ? JSON.parse(token) : token;
230
+ const endpoint = subscription?.endpoint;
231
+ const p256dh = subscription?.keys?.p256dh;
232
+ const auth = subscription?.keys?.auth;
233
+ if (!endpoint || !p256dh || !auth) {
234
+ throw new Error("Web Push subscription is missing endpoint/keys");
235
+ }
236
+ const message = { type: "remote-session.approval-request" };
237
+ if (notification?.title) message.title = notification.title;
238
+ if (notification?.body) message.body = notification.body;
239
+ if (sessionId != null) message.sessionId = String(sessionId);
240
+ if (clientId != null) message.clientId = String(clientId);
241
+
242
+ const body = encryptWebPushPayload({
243
+ payload: JSON.stringify(message),
244
+ uaPublicKey: fromBase64url(p256dh),
245
+ authSecret: fromBase64url(auth),
246
+ });
247
+ const origin = new URL(endpoint).origin;
248
+ const res = await this.fetch(endpoint, {
249
+ method: "POST",
250
+ headers: {
251
+ Authorization: `vapid t=${this.vapid.getToken(origin)}, k=${this.vapid.publicKey}`,
252
+ "Content-Encoding": "aes128gcm",
253
+ "Content-Type": "application/octet-stream",
254
+ TTL: String(this.ttl),
255
+ Urgency: "high",
256
+ },
257
+ body,
258
+ });
259
+ if (res.status === 201 || res.ok) {
260
+ return { id: res.headers?.get?.("location") || null };
261
+ }
262
+ // 404/410 mean the subscription is gone — prune it rather than retry.
263
+ if (res.status === 404 || res.status === 410) {
264
+ throw new PushTokenUnregisteredError(
265
+ `Web Push subscription expired (${res.status})`,
266
+ );
267
+ }
268
+ throw new Error(`Web Push failed (${res.status})`);
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Build a bound Web Push `sender` from env, or null when VAPID is unconfigured.
274
+ *
275
+ * Web env:
276
+ * CHAINLESSCHAIN_REMOTE_SESSION_VAPID_PUBLIC_KEY (base64url, 65-byte P-256)
277
+ * CHAINLESSCHAIN_REMOTE_SESSION_VAPID_PRIVATE_KEY (base64url, 32-byte scalar)
278
+ * CHAINLESSCHAIN_REMOTE_SESSION_VAPID_SUBJECT (mailto: or https URL)
279
+ */
280
+ export function createWebPushSender(env = {}, options = {}) {
281
+ const publicKey =
282
+ options.vapidPublicKey ||
283
+ env.CHAINLESSCHAIN_REMOTE_SESSION_VAPID_PUBLIC_KEY;
284
+ const privateKey =
285
+ options.vapidPrivateKey ||
286
+ env.CHAINLESSCHAIN_REMOTE_SESSION_VAPID_PRIVATE_KEY;
287
+ const subject =
288
+ options.vapidSubject || env.CHAINLESSCHAIN_REMOTE_SESSION_VAPID_SUBJECT;
289
+ if (!publicKey || !privateKey || !subject) return null;
290
+ let vapid;
291
+ try {
292
+ vapid =
293
+ options.vapid ||
294
+ new VapidTokenProvider({ publicKey, privateKey, subject, ...options });
295
+ } catch {
296
+ return null; // Malformed VAPID keys — stay disabled rather than crash.
297
+ }
298
+ return new WebPushSender({ vapid, ...options }).handler;
299
+ }
@@ -0,0 +1,99 @@
1
+ // Remote Session vendor push — Xiaomi (小米) push sender.
2
+ //
3
+ // A concrete implementation of the RemoteSessionPushDispatcher `sender` contract
4
+ // for MIUI devices, using the Xiaomi Push server API. Unlike FCM/APNs there is
5
+ // no JWT — auth is a static app AppSecret in the `Authorization: key=` header —
6
+ // so this sender is a straight form-encoded POST over plain HTTPS with an
7
+ // injectable `fetch` (default global), fully unit-testable with a fake
8
+ // transport. The payload carries only routing ids (no session content).
9
+
10
+ import { PushTokenUnregisteredError } from "./remote-session-push-errors.js";
11
+
12
+ const DEFAULT_HOST = "https://api.xmpush.xiaomi.com";
13
+ const SEND_PATH = "/v3/message/regid";
14
+
15
+ // Xiaomi does not use a single canonical "unregistered" code; match the reason
16
+ // text it returns for a stale/invalid registration id.
17
+ const INVALID_TOKEN_RE = /invalid|not valid|unregist|not ?exist|无效|不存在/i;
18
+
19
+ /** Sends one notification to a Xiaomi registration id. */
20
+ export class XiaomiPushSender {
21
+ constructor(options = {}) {
22
+ if (!options.appSecret) {
23
+ throw new Error("XiaomiPushSender requires an appSecret");
24
+ }
25
+ if (!options.packageName) {
26
+ throw new Error("XiaomiPushSender requires a packageName");
27
+ }
28
+ this.appSecret = options.appSecret;
29
+ this.packageName = options.packageName;
30
+ this.host = options.host || DEFAULT_HOST;
31
+ this.fetch = options.fetch || globalThis.fetch;
32
+ }
33
+
34
+ /** Bound function matching the dispatcher's `sender` contract. */
35
+ get handler() {
36
+ return (payload) => this.send(payload);
37
+ }
38
+
39
+ async send({ token, notification, sessionId, clientId } = {}) {
40
+ if (!token) throw new Error("Xiaomi send requires a registration id");
41
+ const params = new URLSearchParams({
42
+ registration_id: token,
43
+ restricted_package_name: this.packageName,
44
+ pass_through: "0", // 0 = show a notification, 1 = data-only
45
+ notify_type: "-1", // default sound/vibrate
46
+ title: notification?.title || "Approval requested",
47
+ description: notification?.body || "A coding session needs your approval",
48
+ });
49
+ // Routing ids only — no session content.
50
+ const payload = { type: "remote-session.approval-request" };
51
+ if (sessionId != null) payload.sessionId = String(sessionId);
52
+ if (clientId != null) payload.clientId = String(clientId);
53
+ params.set("payload", JSON.stringify(payload));
54
+
55
+ const res = await this.fetch(`${this.host}${SEND_PATH}`, {
56
+ method: "POST",
57
+ headers: {
58
+ Authorization: `key=${this.appSecret}`,
59
+ "Content-Type": "application/x-www-form-urlencoded",
60
+ },
61
+ body: params.toString(),
62
+ });
63
+ const json = await res.json().catch(() => ({}));
64
+ if (res.ok && json.result === "ok" && Number(json.code) === 0) {
65
+ return { id: json.data?.id || null };
66
+ }
67
+ const reason = json.reason || json.description || `HTTP ${res.status}`;
68
+ if (INVALID_TOKEN_RE.test(String(reason))) {
69
+ throw new PushTokenUnregisteredError(
70
+ `Xiaomi registration id invalid: ${reason}`,
71
+ );
72
+ }
73
+ throw new Error(`Xiaomi push failed: ${reason}`);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Build a bound Xiaomi `sender` from env, or null when unconfigured.
79
+ *
80
+ * Xiaomi env:
81
+ * CHAINLESSCHAIN_REMOTE_SESSION_XIAOMI_APP_SECRET
82
+ * CHAINLESSCHAIN_REMOTE_SESSION_XIAOMI_PACKAGE_NAME
83
+ * CHAINLESSCHAIN_REMOTE_SESSION_XIAOMI_HOST (optional; e.g. the global host)
84
+ */
85
+ export function createXiaomiPushSender(env = {}, options = {}) {
86
+ const appSecret =
87
+ options.appSecret || env.CHAINLESSCHAIN_REMOTE_SESSION_XIAOMI_APP_SECRET;
88
+ const packageName =
89
+ options.packageName ||
90
+ env.CHAINLESSCHAIN_REMOTE_SESSION_XIAOMI_PACKAGE_NAME;
91
+ if (!appSecret || !packageName) return null;
92
+ const host = options.host || env.CHAINLESSCHAIN_REMOTE_SESSION_XIAOMI_HOST;
93
+ return new XiaomiPushSender({
94
+ appSecret,
95
+ packageName,
96
+ host,
97
+ ...options,
98
+ }).handler;
99
+ }
@@ -0,0 +1,104 @@
1
+ // Remote Session push notifications (vendor push).
2
+ //
3
+ // Wakes a paired device for time-sensitive events (an approval request) even
4
+ // when its relay WebSocket is suspended — the case a foreground-only local
5
+ // notification can't cover. The actual delivery (FCM / APNs / Xiaomi / Huawei /
6
+ // OPPO) is a pluggable `sender` injected by the host, so this layer stays
7
+ // credential-free and fully unit-testable; with no sender it degrades to a
8
+ // recorded no-op. Payloads carry NO session content, only the shape needed to
9
+ // route the wake-up (aligns with the audit log's privacy-first stance).
10
+
11
+ const DEFAULT_DEDUPE_WINDOW_MS = 30_000;
12
+
13
+ /**
14
+ * True for host events that should wake a backgrounded device. Approval /
15
+ * permission requests qualify; their `resolved` counterparts do not.
16
+ */
17
+ export function isApprovalRequestEvent(type) {
18
+ const value = String(type || "");
19
+ return /approval|permission/i.test(value) && !/resolv/i.test(value);
20
+ }
21
+
22
+ export class RemoteSessionPushDispatcher {
23
+ constructor(options = {}) {
24
+ this.now = options.now || Date.now;
25
+ this.sender = typeof options.sender === "function" ? options.sender : null;
26
+ this.provider = options.provider || null;
27
+ this.dedupeWindowMs = options.dedupeWindowMs || DEFAULT_DEDUPE_WINDOW_MS;
28
+ this.recent = new Map();
29
+ this.stats = { sent: 0, skipped: 0, failed: 0 };
30
+ }
31
+
32
+ get enabled() {
33
+ return Boolean(this.sender);
34
+ }
35
+
36
+ /**
37
+ * Deliver one wake-up. Never throws — a push is best-effort; the relay event
38
+ * and in-app notification remain the primary channels. Returns a structured
39
+ * outcome the caller can audit.
40
+ */
41
+ async dispatch({
42
+ token,
43
+ provider,
44
+ sessionId,
45
+ clientId,
46
+ notification,
47
+ dedupeKey,
48
+ } = {}) {
49
+ if (!token) return this._skip("no-token");
50
+ if (!this.sender) return this._skip("no-sender");
51
+ if (dedupeKey && this._isDuplicate(clientId, dedupeKey)) {
52
+ return this._skip("deduplicated");
53
+ }
54
+ try {
55
+ const result = await this.sender({
56
+ token,
57
+ provider: provider || this.provider,
58
+ sessionId,
59
+ clientId,
60
+ notification,
61
+ });
62
+ this.stats.sent += 1;
63
+ return { status: "sent", provider: provider || this.provider, result };
64
+ } catch (error) {
65
+ this.stats.failed += 1;
66
+ // Propagate a typed code (e.g. PUSH_TOKEN_UNREGISTERED) so the caller can
67
+ // prune a dead device token instead of retrying it forever.
68
+ return {
69
+ status: "failed",
70
+ error: error.message,
71
+ code: error.code || null,
72
+ };
73
+ }
74
+ }
75
+
76
+ _skip(reason) {
77
+ this.stats.skipped += 1;
78
+ return { status: "skipped", reason };
79
+ }
80
+
81
+ _isDuplicate(clientId, dedupeKey) {
82
+ const key = `${clientId || ""}:${dedupeKey}`;
83
+ const now = this.now();
84
+ const last = this.recent.get(key);
85
+ if (last != null && now - last < this.dedupeWindowMs) return true;
86
+ this.recent.set(key, now);
87
+ this._gc(now);
88
+ return false;
89
+ }
90
+
91
+ _gc(now) {
92
+ if (this.recent.size < 500) return;
93
+ for (const [key, ts] of this.recent) {
94
+ if (now - ts >= this.dedupeWindowMs) this.recent.delete(key);
95
+ }
96
+ }
97
+
98
+ static fromEnv(env = {}, options = {}) {
99
+ return new RemoteSessionPushDispatcher({
100
+ provider: env.CHAINLESSCHAIN_REMOTE_SESSION_PUSH_PROVIDER || null,
101
+ ...options,
102
+ });
103
+ }
104
+ }