chainlesschain 0.162.157 → 0.162.159

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 (212) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-CfMP2Oud.js → AIOps-ByIiF5bT.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-Zu0N8BQL.js → ActionButton-NlgSFZq5.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-DZTwj5T_.js → Analytics-D17cOUcb.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CVFJh47v.js → AppLayout-B4mEZcbl.js} +5 -5
  8. package/src/assets/web-panel/assets/Artifacts-CASsmyQw.js +1 -0
  9. package/src/assets/web-panel/assets/Artifacts-CTVt96zW.css +1 -0
  10. package/src/assets/web-panel/assets/{Audit-BMfiLj7T.js → Audit-DYAia2e2.js} +1 -1
  11. package/src/assets/web-panel/assets/{BackgroundAgents-BslPCdGF.js → BackgroundAgents-C1lvYUSu.js} +1 -1
  12. package/src/assets/web-panel/assets/{Backup-CQ_fMC1r.js → Backup-B6agK6bA.js} +1 -1
  13. package/src/assets/web-panel/assets/{BaseInput-CsFsARiC.js → BaseInput-DM43R2vy.js} +1 -1
  14. package/src/assets/web-panel/assets/{Chat-CwfBobco.js → Chat-Dtdp3eXp.js} +6 -6
  15. package/src/assets/web-panel/assets/Chat-KSGPtU0L.css +1 -0
  16. package/src/assets/web-panel/assets/{ChatBubbleRenderer-K6Nf3xeN.js → ChatBubbleRenderer-BL5c_OQ1.js} +1 -1
  17. package/src/assets/web-panel/assets/{Checkbox-DkSyjjPp.js → Checkbox-D2xf7uZt.js} +1 -1
  18. package/src/assets/web-panel/assets/{Codegen-B7Fx3zZ6.js → Codegen-D3j45jJL.js} +1 -1
  19. package/src/assets/web-panel/assets/{Col-Dzfqn8Ep.js → Col-voPJ9fUZ.js} +1 -1
  20. package/src/assets/web-panel/assets/{Community-DhHPdzMM.js → Community-DEsyTESF.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compact-Ct18_8y7.js → Compact-OHV_QXLD.js} +1 -1
  22. package/src/assets/web-panel/assets/{Compliance-CY64IqBi.js → Compliance-eDTJWC-J.js} +1 -1
  23. package/src/assets/web-panel/assets/{Cowork-BYSmWlUl.js → Cowork-BBvy-6d1.js} +2 -2
  24. package/src/assets/web-panel/assets/{Cron-_4xtP3Ug.js → Cron-Dw2eoJxu.js} +2 -2
  25. package/src/assets/web-panel/assets/{Crosschain-B4qY-Ycr.js → Crosschain-DQHms_Bh.js} +1 -1
  26. package/src/assets/web-panel/assets/{DID-CkCzWkx1.js → DID-Cyu7Id1c.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dashboard-C1scT7Ke.js → Dashboard-sIDzkG92.js} +2 -2
  28. package/src/assets/web-panel/assets/{Dropdown-DBnQ2EWd.js → Dropdown-DUW3T2ey.js} +1 -1
  29. package/src/assets/web-panel/assets/{EmailListRenderer-C9JhOTq0.js → EmailListRenderer-CECELXW8.js} +1 -1
  30. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CWXv5RUv.js → FamilyGuardDashboard-BIZR37ov.js} +1 -1
  31. package/src/assets/web-panel/assets/{Federation-fYdcbz9W.js → Federation-BEZIQX84.js} +1 -1
  32. package/src/assets/web-panel/assets/{FormItemContext-CJqOT3Yj.js → FormItemContext-BcdUMznL.js} +1 -1
  33. package/src/assets/web-panel/assets/{GenericCardRenderer-DGCsHc5K.js → GenericCardRenderer-DsN_fr5H.js} +1 -1
  34. package/src/assets/web-panel/assets/{Git-EH2K-H0Q.js → Git-63LhjBx2.js} +2 -2
  35. package/src/assets/web-panel/assets/{Governance-DJn1X2pR.js → Governance-CYtQtIcH.js} +1 -1
  36. package/src/assets/web-panel/assets/{Inference-Dch1E8qd.js → Inference-BwR6UUox.js} +1 -1
  37. package/src/assets/web-panel/assets/{KnowledgeGraph-QIur8j8-.js → KnowledgeGraph-D9lVNTUA.js} +1 -1
  38. package/src/assets/web-panel/assets/{Logs-DUgHldPg.js → Logs-C4q5h7Ut.js} +2 -2
  39. package/src/assets/web-panel/assets/MarkdownRenderer-DbzhFwvz.css +1 -0
  40. package/src/assets/web-panel/assets/MarkdownRenderer-DexLlXfN.js +1 -0
  41. package/src/assets/web-panel/assets/{Marketplace-DafrSfvd.js → Marketplace-Plksliuw.js} +1 -1
  42. package/src/assets/web-panel/assets/{McpTools-D2xRbfJx.js → McpTools-DZUFfeuG.js} +3 -3
  43. package/src/assets/web-panel/assets/{Memory-C6RbVHAo.js → Memory-NoDYt29V.js} +2 -2
  44. package/src/assets/web-panel/assets/{MobileBridge-BPtVBKqn.js → MobileBridge-CEXGTkdh.js} +1 -1
  45. package/src/assets/web-panel/assets/{MobileProjects-Cuw832cr.js → MobileProjects-DnbJkbZo.js} +1 -1
  46. package/src/assets/web-panel/assets/{Mtc-D6O9fWFa.js → Mtc-DTAa0cwy.js} +5 -5
  47. package/src/assets/web-panel/assets/{MtcAudit-CWZcXIH1.js → MtcAudit-CVV7LnNS.js} +4 -4
  48. package/src/assets/web-panel/assets/{Multisig-DhIBxTFd.js → Multisig-BqqGrjt_.js} +3 -3
  49. package/src/assets/web-panel/assets/{NLProgramming-wkBFS33e.js → NLProgramming-C4WP8DRp.js} +1 -1
  50. package/src/assets/web-panel/assets/{Notes-B09FxncA.js → Notes-Zo4DJUVI.js} +3 -3
  51. package/src/assets/web-panel/assets/{NotificationSettings-oQwtH6-9.js → NotificationSettings-DBcjRaz2.js} +1 -1
  52. package/src/assets/web-panel/assets/OrderTableRenderer-BCQel9bD.js +1 -0
  53. package/src/assets/web-panel/assets/{Organization-CsF-NIjz.js → Organization-BT9WPBOX.js} +4 -4
  54. package/src/assets/web-panel/assets/{Overflow-bLxv9Dqi.js → Overflow-D6b4bZ3q.js} +1 -1
  55. package/src/assets/web-panel/assets/{P2P-DVwQtm4q.js → P2P-Cw3OsYx-.js} +2 -2
  56. package/src/assets/web-panel/assets/{PdhVaultBrowser-Bgic3QKm.js → PdhVaultBrowser-C0nyiK2b.js} +4 -4
  57. package/src/assets/web-panel/assets/{Permissions-as80-9ep.js → Permissions-zVBZEB_C.js} +3 -3
  58. package/src/assets/web-panel/assets/{PersonalDataHub-BGmUw36a.js → PersonalDataHub-BOspsFew.js} +2 -2
  59. package/src/assets/web-panel/assets/{Pipeline-BM96zBxG.js → Pipeline-CQWfZ4XH.js} +1 -1
  60. package/src/assets/web-panel/assets/{Privacy-BBOzBSWe.js → Privacy-C2oGJ_WL.js} +1 -1
  61. package/src/assets/web-panel/assets/{ProjectInit-GHX41V28.js → ProjectInit-BZMR4qY1.js} +2 -2
  62. package/src/assets/web-panel/assets/{ProjectSettings-DraqzxKD.js → ProjectSettings-DAujoP63.js} +2 -2
  63. package/src/assets/web-panel/assets/{Projects-BzzdUml4.js → Projects-DvH_P2Uq.js} +1 -1
  64. package/src/assets/web-panel/assets/{Providers-CSeYNAtW.js → Providers-D_RGHM-v.js} +1 -1
  65. package/src/assets/web-panel/assets/{QrScannerModal-DlzBCdXh.js → QrScannerModal-Mf6KfL2J.js} +1 -1
  66. package/src/assets/web-panel/assets/{QuickAsk-p8iB7od_.js → QuickAsk-5D8URoI_.js} +1 -1
  67. package/src/assets/web-panel/assets/{Recommend-DjOVXacd.js → Recommend-Df4kC8Gy.js} +1 -1
  68. package/src/assets/web-panel/assets/RemoteSession-DGi3f3vO.js +4 -0
  69. package/src/assets/web-panel/assets/{Reputation-CjT3M0Di.js → Reputation-jmkGFNWb.js} +1 -1
  70. package/src/assets/web-panel/assets/{Row-tw8m6T0c.js → Row-DP6Pcu3m.js} +1 -1
  71. package/src/assets/web-panel/assets/{RssFeed-B47KBx_f.js → RssFeed-D60hSuPO.js} +3 -3
  72. package/src/assets/web-panel/assets/{Search-D2sx1PBe.js → Search-0mf1BQUK.js} +1 -1
  73. package/src/assets/web-panel/assets/{Security-JmtL8g7D.js → Security-CfZtLFYV.js} +3 -3
  74. package/src/assets/web-panel/assets/{Services-BxrNqjin.js → Services-D9zpZLWz.js} +2 -2
  75. package/src/assets/web-panel/assets/{Skeleton-udk8L18t.js → Skeleton-D1_44eMD.js} +1 -1
  76. package/src/assets/web-panel/assets/{Skills-DooLT9of.js → Skills-cVFa8zkz.js} +1 -1
  77. package/src/assets/web-panel/assets/{Sla-ITVZE1xI.js → Sla-Cinpzc3L.js} +1 -1
  78. package/src/assets/web-panel/assets/{SpeechSettings-DCkfqY1n.js → SpeechSettings-bqdiJE_P.js} +1 -1
  79. package/src/assets/web-panel/assets/{SyncSettings-CkNlgFjM.js → SyncSettings-Dgl_SLgj.js} +2 -2
  80. package/src/assets/web-panel/assets/{Tasks-BMAoyCM_.js → Tasks-DawWAEhy.js} +1 -1
  81. package/src/assets/web-panel/assets/{Templates-BnrFTlrz.js → Templates-Ca0nY651.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tenant-BjNsy3-i.js → Tenant-CgaGEhLm.js} +1 -1
  83. package/src/assets/web-panel/assets/{Terminal-C0gxLL7V.js → Terminal-D-9EnpRx.js} +2 -2
  84. package/src/assets/web-panel/assets/{TimelineRenderer-CUtj_KHA.js → TimelineRenderer-CSOxDHZ6.js} +1 -1
  85. package/src/assets/web-panel/assets/{Tokens-DOg61zlq.js → Tokens-BpwLZpL3.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trigger-D4ldCewj.js → Trigger-DgowwBK5.js} +1 -1
  87. package/src/assets/web-panel/assets/{Trust-3lk_Zd8j.js → Trust-BZutapjE.js} +1 -1
  88. package/src/assets/web-panel/assets/{UkeySign-kWhFR6Li.js → UkeySign-QZPt8_Sp.js} +1 -1
  89. package/src/assets/web-panel/assets/{VideoEditing-D3GOFVWg.js → VideoEditing-DlQx5UDJ.js} +1 -1
  90. package/src/assets/web-panel/assets/{Wallet-DzrZfBuh.js → Wallet-ZfdTgtWA.js} +4 -4
  91. package/src/assets/web-panel/assets/{WebAuthn-CyVJVp_g.js → WebAuthn-CAp0y_Cr.js} +5 -5
  92. package/src/assets/web-panel/assets/{WorkflowEditor-i_b0wi9b.js → WorkflowEditor-BVITZEmr.js} +1 -1
  93. package/src/assets/web-panel/assets/{chat-DwPCK8I7.js → chat-CC2tPaNg.js} +1 -1
  94. package/src/assets/web-panel/assets/{colors-D0vaVqZU.js → colors-31brc9CA.js} +1 -1
  95. package/src/assets/web-panel/assets/{compact-item-Don7nGW2.js → compact-item-DLMkC9C-.js} +1 -1
  96. package/src/assets/web-panel/assets/{createContext-D84R5qy2.js → createContext-BMtveI6F.js} +1 -1
  97. package/src/assets/web-panel/assets/devWarning-CB8pPOtd.js +1 -0
  98. package/src/assets/web-panel/assets/{hasIn-BeUqVVKU.js → hasIn-DbD6WHW2.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-m9bn0F1p.js → index-3oBsXZmF.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CfYRv-zA.js → index-ALvoNTRF.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-4MkfPLGv.js → index-AaIKG60c.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-LMo9bBLJ.js → index-BFRLmCR3.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-wesQQ2cH.js → index-BKAB6-6V.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CzT56x-_.js → index-BOM-VFlv.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-L73NrmlW.js → index-BPpS-lGb.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-WRw1pM5E.js → index-Bbc2JeQG.js} +1 -1
  107. package/src/assets/web-panel/assets/index-BhXiaiev.js +1 -0
  108. package/src/assets/web-panel/assets/{index-Br3SOdER.js → index-BiRQUoyE.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-D8r7WKk-.js → index-BnFLzejT.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-CAdCTTBC.js → index-BoO4WEJB.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BlfTf0ka.js → index-C-cGlCDW.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-B57al0Z5.js → index-C1PfeI88.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-D4b5XqIy.js → index-C21pdBqs.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-CnnxzIx0.js → index-C7LHNw_N.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BNpJf-sf.js → index-CEXa-4zd.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DT40zd7K.js → index-CEwjBA9q.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CL71rIwo.js → index-CFXKbOek.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-BZ1N07Sp.js → index-CJhREOlG.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-C0_sKVi7.js → index-COHHQ9cU.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DBzBLX-K.js → index-CUmLMFsy.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-De85D9Pc.js → index-CWgfipcC.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-x7VYlV41.js → index-C_eUKGYP.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-hjgrIX5I.js → index-CcVszn2b.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DMT8n9vB.js → index-Cod97Urs.js} +3 -3
  125. package/src/assets/web-panel/assets/{index-DFFzwrNs.js → index-Cwbqh0KZ.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-YKLCotoo.js → index-D67kIagl.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-IGGttsCv.js → index-D7cV0tu0.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-DQor_7NF.js → index-DJvxFyIz.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-D84BnuFg.js → index-DZl-UXB6.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-CrnVEEQO.js → index-Dm0byjm1.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BER98o4P.js → index-DndNnYc0.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DA0-EJKx.js → index-WQUO2vvK.js} +1 -1
  133. package/src/assets/web-panel/assets/index-WpaYaE3D.js +1 -0
  134. package/src/assets/web-panel/assets/{index-CExWtm98.js → index-bdaJwTXJ.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-MKZrJrP_.js → index-ep6JchNG.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-7QUwcGTg.js → index-jG6h22Qg.js} +1 -1
  137. package/src/assets/web-panel/assets/{index-BohrJpBf.js → index-ntVQqSML.js} +1 -1
  138. package/src/assets/web-panel/assets/{initDefaultProps-DGeU4vPS.js → initDefaultProps-D5UoBRhN.js} +1 -1
  139. package/src/assets/web-panel/assets/{motion-Dgqj9ab3.js → motion-DltOw0CR.js} +1 -1
  140. package/src/assets/web-panel/assets/{move-BFyEU6Lr.js → move-_oPkk5zj.js} +1 -1
  141. package/src/assets/web-panel/assets/{mtc-parser-CueCXXPC.js → mtc-parser-EES8ROCS.js} +1 -1
  142. package/src/assets/web-panel/assets/{omit-D2pU5OyE.js → omit-B-_39x_M.js} +1 -1
  143. package/src/assets/web-panel/assets/{pickAttrs-CKTf6zD6.js → pickAttrs-l6D0yJHz.js} +1 -1
  144. package/src/assets/web-panel/assets/{placementArrow-DieJaGCN.js → placementArrow-DIeUK31L.js} +1 -1
  145. package/src/assets/web-panel/assets/{responsiveObserve-Bxr-rmU6.js → responsiveObserve-dG4EzUQo.js} +1 -1
  146. package/src/assets/web-panel/assets/{slide-CDEDk3Se.js → slide-DWFzYGdH.js} +1 -1
  147. package/src/assets/web-panel/assets/{statusUtils-icf2eCTx.js → statusUtils-CECdTA5H.js} +1 -1
  148. package/src/assets/web-panel/assets/{styleChecker-qKvgmBhE.js → styleChecker-D6kP-mMK.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFlexGapSupport-1_UhiyJZ.js → useFlexGapSupport-BWfna_rd.js} +1 -1
  150. package/src/assets/web-panel/assets/{useFs-CFGLXDWh.js → useFs-Bb-j6rzo.js} +1 -1
  151. package/src/assets/web-panel/assets/{usePersonalDataHub-UafOXvQ6.js → usePersonalDataHub-D5OIsYM4.js} +1 -1
  152. package/src/assets/web-panel/assets/{vnode-BfPqSx79.js → vnode-DNpwzFTM.js} +1 -1
  153. package/src/assets/web-panel/assets/{zoom-CXsVcRB6.js → zoom-vNLpFQBW.js} +1 -1
  154. package/src/assets/web-panel/index.html +1 -1
  155. package/src/command-manifest.json +17 -3
  156. package/src/commands/agent.js +11 -1
  157. package/src/commands/background-session.js +143 -3
  158. package/src/commands/browse.js +86 -0
  159. package/src/commands/cloud.js +232 -0
  160. package/src/commands/doctor.js +61 -6
  161. package/src/commands/mcp.js +61 -0
  162. package/src/commands/plugin.js +63 -1
  163. package/src/commands/routine.js +367 -0
  164. package/src/commands/session.js +326 -0
  165. package/src/data/changelog.json +12 -2
  166. package/src/harness/jsonl-session-store.js +82 -2
  167. package/src/harness/session-index.js +295 -0
  168. package/src/harness/session-mirror.js +142 -0
  169. package/src/harness/transcript-integrity.js +182 -0
  170. package/src/index.js +8 -0
  171. package/src/lib/background-agent-supervisor.js +126 -2
  172. package/src/lib/channels/channel-manager.js +126 -0
  173. package/src/lib/channels/telegram-channel.js +112 -0
  174. package/src/lib/channels/webhook-channel.js +140 -0
  175. package/src/lib/chrome-connector.js +425 -4
  176. package/src/lib/cloud/bundle.js +110 -0
  177. package/src/lib/cloud/cloud-client.js +152 -0
  178. package/src/lib/computer-use/authorization.js +173 -0
  179. package/src/lib/computer-use/computer-use-server.js +276 -0
  180. package/src/lib/computer-use/control-backend.js +150 -0
  181. package/src/lib/doctor-checkup.js +652 -0
  182. package/src/lib/jsonl-session-store.js +4 -0
  183. package/src/lib/lsp/lsp-server-registry.js +48 -21
  184. package/src/lib/plugin-runtime/governance.js +174 -0
  185. package/src/lib/pr-link-ledger.js +221 -0
  186. package/src/lib/remote-control.js +35 -0
  187. package/src/lib/routine-store.js +352 -0
  188. package/src/lib/session-store-guard.js +40 -0
  189. package/src/lib/session-usage.js +318 -2
  190. package/src/lib/sub-agent-context.js +23 -0
  191. package/src/lib/web-ui-server.js +120 -7
  192. package/src/repl/agent-repl.js +592 -61
  193. package/src/repl/bg-dashboard.js +355 -0
  194. package/src/repl/fast-mode.js +103 -0
  195. package/src/repl/fullscreen-tui.js +197 -0
  196. package/src/repl/remote-approval.js +111 -0
  197. package/src/repl/slash-command-registry.js +11 -3
  198. package/src/repl/voice-dictation.js +132 -0
  199. package/src/runtime/agent-core.js +278 -1
  200. package/src/runtime/coding-agent-contract-shared.cjs +141 -0
  201. package/src/runtime/coding-agent-policy.cjs +33 -0
  202. package/src/runtime/headless-runner.js +71 -7
  203. package/src/runtime/headless-stream.js +14 -8
  204. package/src/runtime/policies/agent-policy.js +6 -0
  205. package/src/assets/web-panel/assets/Artifacts-BbN5V7au.js +0 -1
  206. package/src/assets/web-panel/assets/Artifacts-CRkepl4t.css +0 -1
  207. package/src/assets/web-panel/assets/Chat-DDh58PCf.css +0 -1
  208. package/src/assets/web-panel/assets/OrderTableRenderer-iFKeNf6Y.js +0 -1
  209. package/src/assets/web-panel/assets/RemoteSession-CIGk7gk-.js +0 -4
  210. package/src/assets/web-panel/assets/devWarning-BBwWehQ7.js +0 -1
  211. package/src/assets/web-panel/assets/index-DJ8etkBT.js +0 -1
  212. package/src/assets/web-panel/assets/index-OeaK-zXZ.js +0 -1
@@ -33,6 +33,38 @@ import path from "path";
33
33
  import http from "http";
34
34
 
35
35
  export const DEFAULT_CDP_PORT = 9222;
36
+
37
+ /** Coerce + bound-check a CDP port (goes into URLs and Chrome argv). */
38
+ export function normalizeCdpPort(port) {
39
+ const n = Number(port ?? DEFAULT_CDP_PORT);
40
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
41
+ throw new Error(`Invalid CDP port: ${port}`);
42
+ }
43
+ return n;
44
+ }
45
+
46
+ /**
47
+ * A launch URL must be a real web page. Anything else — and especially a
48
+ * value starting with `--`, which Chrome would parse as a SWITCH (e.g.
49
+ * --renderer-cmd-prefix=… is command execution) — is refused. Today `url`
50
+ * only comes from the local `cc browse chrome launch --url` flag, but this
51
+ * keeps the argv safe if it ever becomes agent- or remote-driven.
52
+ */
53
+ export function normalizeLaunchUrl(url) {
54
+ if (url == null || url === "") return null;
55
+ let parsed;
56
+ try {
57
+ parsed = new URL(String(url));
58
+ } catch {
59
+ throw new Error(`Invalid launch URL: ${url}`);
60
+ }
61
+ if (!["http:", "https:", "about:"].includes(parsed.protocol)) {
62
+ throw new Error(
63
+ `Launch URL must be http(s):// or about: (got ${parsed.protocol})`,
64
+ );
65
+ }
66
+ return String(url); // validated — pass through byte-identical
67
+ }
36
68
  export const DEFAULT_DOM_CAP = 150000;
37
69
 
38
70
  const _deps = {
@@ -41,6 +73,7 @@ const _deps = {
41
73
  homedir: () => os.homedir(),
42
74
  platform: () => process.platform,
43
75
  env: () => process.env,
76
+ tmpdir: () => os.tmpdir(),
44
77
  httpGet,
45
78
  importPlaywright: () => import("playwright"),
46
79
  };
@@ -65,7 +98,9 @@ export async function discoverCdp({
65
98
  port = DEFAULT_CDP_PORT,
66
99
  deps = _deps,
67
100
  } = {}) {
68
- const res = await deps.httpGet(`http://127.0.0.1:${port}/json/version`);
101
+ const res = await deps.httpGet(
102
+ `http://127.0.0.1:${normalizeCdpPort(port)}/json/version`,
103
+ );
69
104
  if (res.status !== 200) return { ok: false, port };
70
105
  try {
71
106
  const info = JSON.parse(res.body);
@@ -150,7 +185,7 @@ export function buildChromeLaunchArgs({
150
185
  deps = _deps,
151
186
  } = {}) {
152
187
  const args = [
153
- `--remote-debugging-port=${port}`,
188
+ `--remote-debugging-port=${normalizeCdpPort(port)}`,
154
189
  // No first-run wizards in the dedicated profile.
155
190
  "--no-first-run",
156
191
  "--no-default-browser-check",
@@ -158,7 +193,8 @@ export function buildChromeLaunchArgs({
158
193
  if (!defaultProfile) {
159
194
  args.push(`--user-data-dir=${profileDir || connectorProfileDir({ deps })}`);
160
195
  }
161
- if (url) args.push(String(url));
196
+ const launchUrl = normalizeLaunchUrl(url);
197
+ if (launchUrl) args.push(launchUrl);
162
198
  return args;
163
199
  }
164
200
 
@@ -216,7 +252,7 @@ export async function captureState({
216
252
  let browser;
217
253
  try {
218
254
  browser = await playwright.chromium.connectOverCDP(
219
- `http://127.0.0.1:${port}`,
255
+ `http://127.0.0.1:${normalizeCdpPort(port)}`,
220
256
  );
221
257
  } catch (err) {
222
258
  return {
@@ -311,4 +347,389 @@ export async function captureState({
311
347
  }
312
348
  }
313
349
 
350
+ // ─── Browser Action mode (gap-analysis #6) ────────────────────────────────
351
+ //
352
+ // captureState/browser_state stay the read-only DEFAULT. performActions is
353
+ // the explicit, approval-gated capability that DRIVES the connected browser:
354
+ // click / type / press / navigate / waitForSelector / screenshot /
355
+ // assertText. Every executed step is appended to an audit JSONL under
356
+ // ~/.chainlesschain/browser-actions/<date>.jsonl (CC_BROWSER_ACTIONS_DIR
357
+ // overrides the dir for tests). Screenshot paths are ALWAYS generated
358
+ // internally — a caller-supplied path is refused (same invariant as
359
+ // browser_state: an action tool must not double as an arbitrary-file writer).
360
+
361
+ export const SUPPORTED_BROWSER_ACTIONS = Object.freeze([
362
+ "click",
363
+ "type",
364
+ "press",
365
+ "navigate",
366
+ "waitForSelector",
367
+ "screenshot",
368
+ "assertText",
369
+ ]);
370
+ export const MAX_BROWSER_ACTIONS = 30;
371
+ export const DEFAULT_ACTION_TIMEOUT_MS = 10000;
372
+ export const MAX_ACTION_TIMEOUT_MS = 30000;
373
+
374
+ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
375
+
376
+ /** Audit log directory (CC_BROWSER_ACTIONS_DIR override for tests). */
377
+ export function browserActionsDir({ deps = _deps } = {}) {
378
+ const env = deps.env();
379
+ if (env.CC_BROWSER_ACTIONS_DIR) return env.CC_BROWSER_ACTIONS_DIR;
380
+ return path.join(deps.homedir(), ".chainlesschain", "browser-actions");
381
+ }
382
+
383
+ /**
384
+ * Resolve an optional caller CDP endpoint to a PORT, enforcing the connector's
385
+ * loopback-only contract: only http:// against 127.0.0.1 / localhost / [::1]
386
+ * is accepted. performActions then connects to `http://127.0.0.1:<port>` —
387
+ * the caller string is never used as the connection target verbatim, so a
388
+ * crafted endpoint cannot widen the trust boundary the prior security fix
389
+ * established.
390
+ */
391
+ export function resolveLoopbackCdpPort(
392
+ cdpUrl,
393
+ fallbackPort = DEFAULT_CDP_PORT,
394
+ ) {
395
+ if (cdpUrl == null || cdpUrl === "") return normalizeCdpPort(fallbackPort);
396
+ let parsed;
397
+ try {
398
+ parsed = new URL(String(cdpUrl));
399
+ } catch {
400
+ throw new Error(`Invalid CDP endpoint: ${cdpUrl}`);
401
+ }
402
+ if (parsed.protocol !== "http:") {
403
+ throw new Error(
404
+ `CDP endpoint must be http:// on loopback (got ${parsed.protocol})`,
405
+ );
406
+ }
407
+ if (!LOOPBACK_HOSTS.has(parsed.hostname.toLowerCase())) {
408
+ throw new Error(
409
+ `CDP endpoint must be loopback-only (127.0.0.1 / localhost) — refusing host "${parsed.hostname}"`,
410
+ );
411
+ }
412
+ return normalizeCdpPort(parsed.port || DEFAULT_CDP_PORT);
413
+ }
414
+
415
+ function truncateForAudit(value, cap = 200) {
416
+ const s = String(value);
417
+ return s.length > cap ? `${s.slice(0, cap)}…` : s;
418
+ }
419
+
420
+ /**
421
+ * Validate + normalize the caller's action list BEFORE anything connects.
422
+ * Throws on the first invalid action — performActions turns that into
423
+ * {ok:false, error} without touching the browser. Screenshot paths are
424
+ * generated here (deps.tmpdir), and any caller-supplied path key is refused.
425
+ */
426
+ export function normalizeBrowserActions(actions, { deps = _deps } = {}) {
427
+ if (!Array.isArray(actions) || actions.length === 0) {
428
+ throw new Error("actions must be a non-empty array of action objects");
429
+ }
430
+ if (actions.length > MAX_BROWSER_ACTIONS) {
431
+ throw new Error(
432
+ `too many actions (${actions.length}) — max ${MAX_BROWSER_ACTIONS} per call`,
433
+ );
434
+ }
435
+ const requireSelector = (raw, type) => {
436
+ const sel = raw.selector;
437
+ if (typeof sel !== "string" || sel.trim() === "" || sel.length > 1000) {
438
+ throw new Error(`${type} requires a non-empty "selector" string`);
439
+ }
440
+ return sel;
441
+ };
442
+ return actions.map((raw, i) => {
443
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) {
444
+ throw new Error(`action[${i}] must be an object with a "type"`);
445
+ }
446
+ const type = String(raw.type || "");
447
+ if (!SUPPORTED_BROWSER_ACTIONS.includes(type)) {
448
+ throw new Error(
449
+ `action[${i}] has unsupported type "${type}". Supported: ${SUPPORTED_BROWSER_ACTIONS.join(", ")}`,
450
+ );
451
+ }
452
+ switch (type) {
453
+ case "click":
454
+ return { type, selector: requireSelector(raw, "click") };
455
+ case "type": {
456
+ if (typeof raw.text !== "string") {
457
+ throw new Error(`type requires a "text" string`);
458
+ }
459
+ return { type, selector: requireSelector(raw, "type"), text: raw.text };
460
+ }
461
+ case "press": {
462
+ if (
463
+ typeof raw.key !== "string" ||
464
+ raw.key.trim() === "" ||
465
+ raw.key.length > 50
466
+ ) {
467
+ throw new Error(`press requires a non-empty "key" string`);
468
+ }
469
+ return { type, key: raw.key };
470
+ }
471
+ case "navigate": {
472
+ let parsed;
473
+ try {
474
+ parsed = new URL(String(raw.url || ""));
475
+ } catch {
476
+ throw new Error(`navigate has an invalid "url": ${raw.url}`);
477
+ }
478
+ if (!["http:", "https:"].includes(parsed.protocol)) {
479
+ throw new Error(
480
+ `navigate url must be http(s):// (got ${parsed.protocol})`,
481
+ );
482
+ }
483
+ return { type, url: String(raw.url) };
484
+ }
485
+ case "waitForSelector": {
486
+ const t = Number(raw.timeoutMs ?? raw.timeout_ms ?? 5000);
487
+ const timeoutMs = Math.min(
488
+ Math.max(Number.isFinite(t) ? t : 5000, 1),
489
+ MAX_ACTION_TIMEOUT_MS,
490
+ );
491
+ return {
492
+ type,
493
+ selector: requireSelector(raw, "waitForSelector"),
494
+ timeoutMs,
495
+ };
496
+ }
497
+ case "screenshot": {
498
+ // The path is GENERATED — never caller-supplied. Refuse loudly so a
499
+ // model (or user) learns the invariant instead of being silently
500
+ // second-guessed.
501
+ for (const k of ["path", "file", "output", "screenshotPath"]) {
502
+ if (raw[k] != null) {
503
+ throw new Error(
504
+ `screenshot path is generated internally — remove "${k}"`,
505
+ );
506
+ }
507
+ }
508
+ return {
509
+ type,
510
+ screenshotPath: path.join(
511
+ deps.tmpdir(),
512
+ `cc-browser-act-${Date.now()}-${i}.png`,
513
+ ),
514
+ };
515
+ }
516
+ case "assertText": {
517
+ if (typeof raw.expected !== "string" || raw.expected === "") {
518
+ throw new Error(`assertText requires a non-empty "expected" string`);
519
+ }
520
+ return {
521
+ type,
522
+ selector: requireSelector(raw, "assertText"),
523
+ expected: raw.expected,
524
+ };
525
+ }
526
+ /* c8 ignore next 2 -- unreachable: type already validated above */
527
+ default:
528
+ throw new Error(`unsupported action type "${type}"`);
529
+ }
530
+ });
531
+ }
532
+
533
+ /** One JSONL row per EXECUTED step (explicit utf-8, best-effort). */
534
+ function appendActionAudit(entry, deps) {
535
+ const dir = browserActionsDir({ deps });
536
+ deps.fs.mkdirSync(dir, { recursive: true });
537
+ const date = entry.ts.slice(0, 10);
538
+ deps.fs.appendFileSync(
539
+ path.join(dir, `${date}.jsonl`),
540
+ JSON.stringify(entry) + "\n",
541
+ "utf-8",
542
+ );
543
+ }
544
+
545
+ /**
546
+ * Does the dedicated connector profile own the CDP port we are attaching to?
547
+ * Chrome writes DevToolsActivePort (first line = port) into the user-data-dir
548
+ * it was launched with; if the file under ~/.chainlesschain/chrome-profile is
549
+ * missing or names a different port, we are driving some OTHER profile —
550
+ * possibly the user's real logged-in browser — and the result carries a
551
+ * profileWarning.
552
+ */
553
+ function connectorProfileOwnsPort(port, deps) {
554
+ try {
555
+ const marker = path.join(
556
+ connectorProfileDir({ deps }),
557
+ "DevToolsActivePort",
558
+ );
559
+ if (!deps.fs.existsSync(marker)) return false;
560
+ const firstLine = String(deps.fs.readFileSync(marker, "utf-8"))
561
+ .split(/\r?\n/)[0]
562
+ .trim();
563
+ return Number(firstLine) === Number(port);
564
+ } catch {
565
+ return false;
566
+ }
567
+ }
568
+
569
+ async function runOneAction(page, act, timeoutMs) {
570
+ switch (act.type) {
571
+ case "click":
572
+ await page.click(act.selector, { timeout: timeoutMs });
573
+ return `clicked ${act.selector}`;
574
+ case "type":
575
+ await page.fill(act.selector, act.text, { timeout: timeoutMs });
576
+ return `typed ${act.text.length} chars into ${act.selector}`;
577
+ case "press":
578
+ await page.keyboard.press(act.key);
579
+ return `pressed ${act.key}`;
580
+ case "navigate":
581
+ await page.goto(act.url, {
582
+ waitUntil: "domcontentloaded",
583
+ timeout: timeoutMs,
584
+ });
585
+ return `navigated to ${act.url}`;
586
+ case "waitForSelector":
587
+ await page.waitForSelector(act.selector, { timeout: act.timeoutMs });
588
+ return `selector appeared: ${act.selector}`;
589
+ case "screenshot":
590
+ await page.screenshot({ path: act.screenshotPath });
591
+ return `screenshot saved: ${act.screenshotPath}`;
592
+ case "assertText": {
593
+ const text = await page.textContent(act.selector, {
594
+ timeout: timeoutMs,
595
+ });
596
+ if (text == null) {
597
+ throw new Error(`assertText: no element matches ${act.selector}`);
598
+ }
599
+ if (!text.includes(act.expected)) {
600
+ throw new Error(
601
+ `assertText FAILED: "${truncateForAudit(act.expected, 80)}" not found in ${act.selector} (got: ${truncateForAudit(text.trim(), 120)})`,
602
+ );
603
+ }
604
+ return `assertText passed: ${act.selector} contains "${truncateForAudit(act.expected, 80)}"`;
605
+ }
606
+ /* c8 ignore next 2 -- unreachable: normalizeBrowserActions validated type */
607
+ default:
608
+ throw new Error(`unsupported action type "${act.type}"`);
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Execute an ordered list of explicit browser actions against the connected
614
+ * Chrome. Fail-fast on the first failed step unless `continueOnError`. Each
615
+ * step result is {ok, action, detail, durationMs}; the overall result adds
616
+ * final page url/title, `executed` count, optional `profileWarning` (attached
617
+ * browser is NOT the dedicated connector profile) and `auditError` (audit
618
+ * write failed — actions themselves are never aborted for audit IO).
619
+ */
620
+ export async function performActions(
621
+ actions,
622
+ {
623
+ port = DEFAULT_CDP_PORT,
624
+ cdpUrl = null,
625
+ tab = 0,
626
+ continueOnError = false,
627
+ sessionId = null,
628
+ actionTimeoutMs = DEFAULT_ACTION_TIMEOUT_MS,
629
+ deps = _deps,
630
+ } = {},
631
+ ) {
632
+ let normalized;
633
+ let resolvedPort;
634
+ try {
635
+ resolvedPort = resolveLoopbackCdpPort(cdpUrl, port);
636
+ normalized = normalizeBrowserActions(actions, { deps });
637
+ } catch (err) {
638
+ return { ok: false, error: err.message };
639
+ }
640
+ const stepTimeout = Math.min(
641
+ Math.max(Number(actionTimeoutMs) || DEFAULT_ACTION_TIMEOUT_MS, 1),
642
+ MAX_ACTION_TIMEOUT_MS,
643
+ );
644
+
645
+ let playwright;
646
+ try {
647
+ playwright = await deps.importPlaywright();
648
+ } catch {
649
+ return {
650
+ ok: false,
651
+ error: "playwright is not installed — npm install playwright",
652
+ };
653
+ }
654
+ let browser;
655
+ try {
656
+ browser = await playwright.chromium.connectOverCDP(
657
+ `http://127.0.0.1:${resolvedPort}`,
658
+ );
659
+ } catch (err) {
660
+ return {
661
+ ok: false,
662
+ error:
663
+ `cannot attach to CDP on port ${resolvedPort} (${err.message.split("\n")[0]}) — ` +
664
+ "launch a debuggable Chrome first (cc browse chrome launch)",
665
+ };
666
+ }
667
+ try {
668
+ // Same post-connect target-attach race as captureState: poll briefly.
669
+ let pages = [];
670
+ for (let i = 0; i < 20; i++) {
671
+ pages = browser.contexts().flatMap((c) => c.pages());
672
+ if (pages.length > 0) break;
673
+ await new Promise((r) => setTimeout(r, 150));
674
+ }
675
+ if (pages.length === 0) {
676
+ return { ok: false, error: "the connected Chrome has no open tabs" };
677
+ }
678
+ const index = Math.min(Math.max(0, Number(tab) || 0), pages.length - 1);
679
+ const page = pages[index];
680
+
681
+ const result = { ok: false, port: resolvedPort, tab: index, steps: [] };
682
+ if (!connectorProfileOwnsPort(resolvedPort, deps)) {
683
+ result.profileWarning =
684
+ "the attached Chrome does not appear to be running the dedicated connector profile " +
685
+ "(~/.chainlesschain/chrome-profile) — actions run in that browser's REAL session";
686
+ }
687
+
688
+ let auditError = null;
689
+ for (const act of normalized) {
690
+ const started = Date.now();
691
+ const step = { ok: false, action: act.type, detail: "", durationMs: 0 };
692
+ try {
693
+ step.detail = await runOneAction(page, act, stepTimeout);
694
+ step.ok = true;
695
+ if (act.screenshotPath) step.screenshotPath = act.screenshotPath;
696
+ } catch (err) {
697
+ step.detail = String(err && err.message ? err.message : err).split(
698
+ "\n",
699
+ )[0];
700
+ }
701
+ step.durationMs = Date.now() - started;
702
+ result.steps.push(step);
703
+ try {
704
+ const entry = {
705
+ ts: new Date(started).toISOString(),
706
+ action: act.type,
707
+ ok: step.ok,
708
+ durationMs: step.durationMs,
709
+ };
710
+ if (sessionId) entry.sessionId = String(sessionId);
711
+ if (act.selector) entry.selector = truncateForAudit(act.selector);
712
+ if (act.url) entry.url = truncateForAudit(act.url);
713
+ if (act.key) entry.key = truncateForAudit(act.key, 50);
714
+ appendActionAudit(entry, deps);
715
+ } catch (err) {
716
+ auditError = err.message;
717
+ }
718
+ if (!step.ok && !continueOnError) break;
719
+ }
720
+
721
+ result.executed = result.steps.length;
722
+ result.ok =
723
+ result.steps.length === normalized.length &&
724
+ result.steps.every((s) => s.ok);
725
+ if (auditError) result.auditError = auditError;
726
+ result.url = page.url();
727
+ result.title = await page.title().catch(() => "");
728
+ return result;
729
+ } finally {
730
+ // connectOverCDP close() disconnects the client; the browser lives on.
731
+ await browser.close().catch(() => {});
732
+ }
733
+ }
734
+
314
735
  export { _deps };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Git branch bundling + result reflow for cloud handoff. A `git bundle` of the
3
+ * current branch (base64) is the portable snapshot the private runner clones;
4
+ * the runner returns a patch/plan/pr/artifacts which we apply/persist locally.
5
+ *
6
+ * All git calls go through the injectable `run` so the flow is testable without
7
+ * a repo.
8
+ */
9
+
10
+ import { execFileSync } from "node:child_process";
11
+ import { writeFileSync, mkdtempSync } from "node:fs";
12
+ import { readFileSync, rmSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+
16
+ export const _deps = { execFileSync };
17
+
18
+ function git(args, cwd, deps = _deps) {
19
+ return deps
20
+ .execFileSync("git", args, {
21
+ cwd,
22
+ encoding: "utf-8",
23
+ maxBuffer: 64 * 1024 * 1024,
24
+ stdio: ["ignore", "pipe", "pipe"],
25
+ })
26
+ .toString();
27
+ }
28
+
29
+ /**
30
+ * Bundle the current branch into a base64 git bundle.
31
+ * @returns {{ bundle:string, branch:string, baseSha:string, bytes:number }}
32
+ */
33
+ export function bundleBranch(cwd = process.cwd(), deps = _deps) {
34
+ const branch = git(["rev-parse", "--abbrev-ref", "HEAD"], cwd, deps).trim();
35
+ const baseSha = git(["rev-parse", "HEAD"], cwd, deps).trim();
36
+ const tmp = mkdtempSync(join(tmpdir(), "cc-cloud-bundle-"));
37
+ const bundleFile = join(tmp, "branch.bundle");
38
+ try {
39
+ // Bundle the full branch history so the runner can clone a working repo.
40
+ git(["bundle", "create", bundleFile, "HEAD", branch], cwd, deps);
41
+ const buf = readFileSync(bundleFile);
42
+ return {
43
+ bundle: buf.toString("base64"),
44
+ branch,
45
+ baseSha,
46
+ bytes: buf.length,
47
+ };
48
+ } finally {
49
+ try {
50
+ rmSync(tmp, { recursive: true, force: true });
51
+ } catch {
52
+ /* best-effort */
53
+ }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Apply a unified-diff patch the runner produced onto the local worktree.
59
+ * Uses `git apply --3way` so it merges cleanly on top of local edits when
60
+ * possible; returns { applied, reason }.
61
+ */
62
+ export function applyResultPatch(patch, cwd = process.cwd(), deps = _deps) {
63
+ if (!patch || !String(patch).trim()) {
64
+ return { applied: false, reason: "no patch in result" };
65
+ }
66
+ const tmp = mkdtempSync(join(tmpdir(), "cc-cloud-patch-"));
67
+ const patchFile = join(tmp, "result.patch");
68
+ try {
69
+ writeFileSync(patchFile, patch, "utf-8");
70
+ git(["apply", "--3way", "--whitespace=nowarn", patchFile], cwd, deps);
71
+ return { applied: true, reason: "patch applied (3way)" };
72
+ } catch (err) {
73
+ return {
74
+ applied: false,
75
+ reason: `patch did not apply cleanly: ${err.message.split("\n")[0]}`,
76
+ };
77
+ } finally {
78
+ try {
79
+ rmSync(tmp, { recursive: true, force: true });
80
+ } catch {
81
+ /* best-effort */
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Persist non-patch result artifacts (plan text, artifact blobs) under the
88
+ * job's local dir and return where they landed. `writeFile` injected.
89
+ */
90
+ export function persistResultArtifacts(result, destDir, deps = {}) {
91
+ const writeFile = deps.writeFileSync || writeFileSync;
92
+ const written = [];
93
+ if (result?.plan) {
94
+ const p = join(destDir, "plan.md");
95
+ writeFile(p, String(result.plan), "utf-8");
96
+ written.push(p);
97
+ }
98
+ for (const art of Array.isArray(result?.artifacts) ? result.artifacts : []) {
99
+ if (!art?.name || art.content == null) continue;
100
+ const safe = String(art.name).replace(/[^\w.-]/g, "_");
101
+ const p = join(destDir, safe);
102
+ const content =
103
+ art.encoding === "base64"
104
+ ? Buffer.from(String(art.content), "base64")
105
+ : String(art.content);
106
+ writeFile(p, content);
107
+ written.push(p);
108
+ }
109
+ return written;
110
+ }