chainlesschain 0.162.149 → 0.162.151

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 (285) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -2
  3. package/src/assets/web-panel/assets/{AIOps-BJif14yR.js → AIOps-DuEOzebi.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-CXek6gJY.js → ActionButton-C6GiJa5H.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-BgQhdAJi.js → Analytics-BBIiL_pO.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-DTYl5NtR.js → AppLayout-CuI2gZRy.js} +3 -3
  7. package/src/assets/web-panel/assets/{Audit-BzATQAeR.js → Audit-CbUjfr-K.js} +1 -1
  8. package/src/assets/web-panel/assets/{Backup-BzdPEQhL.js → Backup-CsSW9ljf.js} +1 -1
  9. package/src/assets/web-panel/assets/{BaseInput-BM0KVh8E.js → BaseInput-kM3rLe7F.js} +1 -1
  10. package/src/assets/web-panel/assets/{Chat-D7fHXHkz.js → Chat-kfE51YN3.js} +4 -4
  11. package/src/assets/web-panel/assets/{ChatBubbleRenderer-Wm34RR-b.js → ChatBubbleRenderer-Bg2FTW6A.js} +1 -1
  12. package/src/assets/web-panel/assets/{Checkbox-CAiSQ9vO.js → Checkbox-jYb1uj-M.js} +1 -1
  13. package/src/assets/web-panel/assets/{Codegen-Df40CrEe.js → Codegen-X9XiD5Lj.js} +1 -1
  14. package/src/assets/web-panel/assets/{Col-DIT2fNFU.js → Col-BfbHu1xI.js} +1 -1
  15. package/src/assets/web-panel/assets/{Community-CSTIbW2k.js → Community-c4oJMFS-.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compact-DtqXZZUi.js → Compact-BFmnZpH7.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compliance-CUtzw6o7.js → Compliance-s9shVHBS.js} +1 -1
  18. package/src/assets/web-panel/assets/{Cowork-CezmlnmU.js → Cowork-C5da--TC.js} +2 -2
  19. package/src/assets/web-panel/assets/{Cron-P4BITarg.js → Cron-9kNbyNE4.js} +2 -2
  20. package/src/assets/web-panel/assets/{Crosschain-BRRiYwvq.js → Crosschain-C1k3NJD4.js} +1 -1
  21. package/src/assets/web-panel/assets/{DID-L5ijB9i4.js → DID-DkheEvNK.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dashboard-CFxro4ob.js → Dashboard-C3FuSGmJ.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dropdown-BmAFCQ71.js → Dropdown-CAV6FMFD.js} +1 -1
  24. package/src/assets/web-panel/assets/{EmailListRenderer-DJBCEuon.js → EmailListRenderer-Cc-Ulc4l.js} +1 -1
  25. package/src/assets/web-panel/assets/{FamilyGuardDashboard-z4oLq6eT.js → FamilyGuardDashboard-8CitHEtv.js} +1 -1
  26. package/src/assets/web-panel/assets/{Federation-DsVCpHMF.js → Federation-DFSZ5KDt.js} +1 -1
  27. package/src/assets/web-panel/assets/{FormItemContext-DP3bP5th.js → FormItemContext-CEWCtnG2.js} +1 -1
  28. package/src/assets/web-panel/assets/{GenericCardRenderer-BAXP2Gc6.js → GenericCardRenderer-CwRzNzmX.js} +1 -1
  29. package/src/assets/web-panel/assets/{Git-I3g_bAw9.js → Git-0rgDM7cJ.js} +2 -2
  30. package/src/assets/web-panel/assets/{Governance-jHS5-ioS.js → Governance-CfBKtK9F.js} +1 -1
  31. package/src/assets/web-panel/assets/{Inference-CPL7sfx9.js → Inference-CzcUVvud.js} +1 -1
  32. package/src/assets/web-panel/assets/{KnowledgeGraph-Jf236h4C.js → KnowledgeGraph-Dwc4YL4A.js} +1 -1
  33. package/src/assets/web-panel/assets/{Logs-Cm2tcSKg.js → Logs-DgFPwR2F.js} +2 -2
  34. package/src/assets/web-panel/assets/{Marketplace-CZK82rhi.js → Marketplace-DnpPTbGU.js} +1 -1
  35. package/src/assets/web-panel/assets/{McpTools-CZCeji7a.js → McpTools-DRxxINyW.js} +4 -4
  36. package/src/assets/web-panel/assets/{Memory-3l2KVS9k.js → Memory-OhmW_6Z3.js} +2 -2
  37. package/src/assets/web-panel/assets/{MobileBridge-VYdpoLh6.js → MobileBridge-DkTW-RK5.js} +1 -1
  38. package/src/assets/web-panel/assets/{MobileProjects-CiB9cmm1.js → MobileProjects-DpCGV_lI.js} +1 -1
  39. package/src/assets/web-panel/assets/{Mtc-wCFZbBuL.js → Mtc--vmkL0AS.js} +2 -2
  40. package/src/assets/web-panel/assets/{MtcAudit-1a3THIyG.js → MtcAudit-DGoFWjD4.js} +4 -4
  41. package/src/assets/web-panel/assets/{Multisig-DMzLB2hG.js → Multisig-B4kWgq95.js} +3 -3
  42. package/src/assets/web-panel/assets/{NLProgramming-D9wZbf6l.js → NLProgramming-YQwvommE.js} +1 -1
  43. package/src/assets/web-panel/assets/{Notes-hrFe2ZM-.js → Notes-CgjtUADJ.js} +3 -3
  44. package/src/assets/web-panel/assets/{NotificationSettings-CHGX5Jwm.js → NotificationSettings-6JDrpBG5.js} +1 -1
  45. package/src/assets/web-panel/assets/OrderTableRenderer-PM6IlxjO.js +1 -0
  46. package/src/assets/web-panel/assets/{Organization-By7FkhtY.js → Organization-8McOT_OF.js} +4 -4
  47. package/src/assets/web-panel/assets/{Overflow--khGSzJn.js → Overflow-DAZpSCGc.js} +1 -1
  48. package/src/assets/web-panel/assets/{P2P-CAG9dH35.js → P2P-B7l2ZWLy.js} +2 -2
  49. package/src/assets/web-panel/assets/{PdhVaultBrowser-B3SQZmK9.js → PdhVaultBrowser-W1Ud-NAL.js} +3 -3
  50. package/src/assets/web-panel/assets/{Permissions-D35ec6JA.js → Permissions-Czm_w9u_.js} +4 -4
  51. package/src/assets/web-panel/assets/{PersonalDataHub-CxBF7hW_.js → PersonalDataHub-B4F0_ZVM.js} +2 -2
  52. package/src/assets/web-panel/assets/{Pipeline-s6EtOO1s.js → Pipeline-B37aFiWv.js} +1 -1
  53. package/src/assets/web-panel/assets/{Privacy-BbJYmWmO.js → Privacy-94KBnRrD.js} +1 -1
  54. package/src/assets/web-panel/assets/{ProjectInit-VGQq1jMT.js → ProjectInit-B5h8_dlh.js} +2 -2
  55. package/src/assets/web-panel/assets/{ProjectSettings-B1ew3Teh.js → ProjectSettings-lmeI3Lpm.js} +2 -2
  56. package/src/assets/web-panel/assets/Projects-D5AYOasl.js +1 -0
  57. package/src/assets/web-panel/assets/{Providers-mrO47FIK.js → Providers-B3cNWwJg.js} +1 -1
  58. package/src/assets/web-panel/assets/{QrScannerModal-IJF2mHyw.js → QrScannerModal-CHRvOw1R.js} +1 -1
  59. package/src/assets/web-panel/assets/{QuickAsk-CU0wN_Z0.js → QuickAsk-D1JMgFEd.js} +1 -1
  60. package/src/assets/web-panel/assets/{Recommend-DhN37R6G.js → Recommend-EYWGR79p.js} +1 -1
  61. package/src/assets/web-panel/assets/{RemoteSession-D_xOX_hu.js → RemoteSession-BAMC2pJt.js} +2 -2
  62. package/src/assets/web-panel/assets/{Reputation-D_Ssv7uH.js → Reputation-X2Hk1XG_.js} +1 -1
  63. package/src/assets/web-panel/assets/{Row-Kx5dKxX7.js → Row-Eq98LRtU.js} +1 -1
  64. package/src/assets/web-panel/assets/{RssFeed-BlGwqZkY.js → RssFeed-i76s6z_o.js} +2 -2
  65. package/src/assets/web-panel/assets/{Search-hLFQzxpb.js → Search-D_Es7CAI.js} +1 -1
  66. package/src/assets/web-panel/assets/{Security-DMlvsVt4.js → Security-kayD2e94.js} +3 -3
  67. package/src/assets/web-panel/assets/{Services-Bd0Qsj2m.js → Services-Ctsm6ul8.js} +2 -2
  68. package/src/assets/web-panel/assets/{Skeleton-DFclq9X3.js → Skeleton-CRNYlfdf.js} +1 -1
  69. package/src/assets/web-panel/assets/{Skills-BNtln2qY.js → Skills-DkZYpF4-.js} +1 -1
  70. package/src/assets/web-panel/assets/{Sla-Dch5H19G.js → Sla-DXkoBcOP.js} +1 -1
  71. package/src/assets/web-panel/assets/{SpeechSettings-2UfQcU1g.js → SpeechSettings-DXi_GI1d.js} +1 -1
  72. package/src/assets/web-panel/assets/{SyncSettings-CD9YQr4i.js → SyncSettings-rNKpfomn.js} +2 -2
  73. package/src/assets/web-panel/assets/{Tasks-BNu-7MzI.js → Tasks-XF4uPtG-.js} +1 -1
  74. package/src/assets/web-panel/assets/{Templates-D1n9CaIR.js → Templates-i0Hzfeyu.js} +1 -1
  75. package/src/assets/web-panel/assets/{Tenant-DenL3iBW.js → Tenant-sMpHAi27.js} +1 -1
  76. package/src/assets/web-panel/assets/Terminal-BoZvaYAl.js +3 -0
  77. package/src/assets/web-panel/assets/{TimelineRenderer-D87IfukO.js → TimelineRenderer-yuC6K7Rf.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tokens-BkiZc8E3.js → Tokens-B8O8i0s6.js} +1 -1
  79. package/src/assets/web-panel/assets/{Trigger-BNRSytGN.js → Trigger-Bv6yrlCr.js} +1 -1
  80. package/src/assets/web-panel/assets/{Trust-Bwbd4X2m.js → Trust-C4mW95Ev.js} +1 -1
  81. package/src/assets/web-panel/assets/{UkeySign-B8O2bs0o.js → UkeySign-DV6yKaHn.js} +1 -1
  82. package/src/assets/web-panel/assets/{VideoEditing-C2fL8zmH.js → VideoEditing-UI479-sD.js} +1 -1
  83. package/src/assets/web-panel/assets/{Wallet-BORYDdE1.js → Wallet-M5oV9EVP.js} +4 -4
  84. package/src/assets/web-panel/assets/{WebAuthn-C0V5S2FM.js → WebAuthn-DDyDHPDR.js} +4 -4
  85. package/src/assets/web-panel/assets/{WorkflowEditor-2dR0fcHL.js → WorkflowEditor-RoBv4rqk.js} +1 -1
  86. package/src/assets/web-panel/assets/{chat-BmuAFAn8.js → chat-Cw1mE5LS.js} +1 -1
  87. package/src/assets/web-panel/assets/{colors-CN3smMcK.js → colors-BFP62Gwf.js} +1 -1
  88. package/src/assets/web-panel/assets/{compact-item-DCnxb4kW.js → compact-item-17HL6pyC.js} +1 -1
  89. package/src/assets/web-panel/assets/{createContext-vlR43pHn.js → createContext-D55X4fZX.js} +1 -1
  90. package/src/assets/web-panel/assets/devWarning-CqbHYuRc.js +1 -0
  91. package/src/assets/web-panel/assets/{hasIn-D04tHYFd.js → hasIn-D3E5mNsP.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-DbVsQBOH.js → index--44J50mc.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-TslMTwNy.js → index-ASPYTzMK.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-CTOCnIQ7.js → index-BC6zxOlU.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-ljuBiQW9.js → index-BEa1RW4c.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-jzR7Ujuw.js → index-BQmZg5si.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BudvKQfG.js → index-BY_xZ2jy.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-CP9m9Ggr.js → index-BgN5G5vF.js} +3 -3
  99. package/src/assets/web-panel/assets/{index-Cr-A0FYJ.js → index-BnGpft-Z.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-D5LZVZ0L.js → index-Bq83UBDr.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-ToUh2b1-.js → index-Bzpp2z2S.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-DbADHwhr.js → index-CFYGn88j.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-ClhAHDK3.js → index-CGyTos4p.js} +1 -1
  104. package/src/assets/web-panel/assets/index-CKBI1U5K.js +1 -0
  105. package/src/assets/web-panel/assets/{index-C-6NO5il.js → index-CLEzsZ8A.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-D8vwp4qp.js → index-CR9rNUvi.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-_FIkN3jd.js → index-CW6LLoJO.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-D65_XseV.js → index-C_MfcrLf.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-CnXebZLL.js → index-Cdvk2_2q.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-D_n8_vfI.js → index-CjCwi7we.js} +1 -1
  111. package/src/assets/web-panel/assets/index-CkLp6DTO.js +1 -0
  112. package/src/assets/web-panel/assets/{index-DDwLgQY9.js → index-CvCFKojj.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-430S68MN.js → index-D4LHSde4.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BXRg8GFQ.js → index-DCLu84x-.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DTtRscUa.js → index-DD_qGBrt.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BasvsWDM.js → index-DIRvdwBX.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BeGDEXFs.js → index-DOjA8bvj.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CsAsCPJ6.js → index-Da27u6j0.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-D28DeAAB.js → index-DkBVYlE4.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CngCC8hC.js → index-DtAnPBWP.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-B78xL3s2.js → index-DzG7mkgW.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-DIpY0qM5.js → index-H9_X1Cg_.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-C5Sxb1sE.js → index-ITeEHb8L.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-BTPEZTk1.js → index-KtLJXn4s.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-hpvvtAZ3.js → index-RYZgFIwo.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-HhNjnCfe.js → index-gAVxDiTZ.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DTWg-18U.js → index-paVubXzn.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BqhASEL4.js → index-tUH_6bdJ.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-DYAtmqiv.js → index-tVRPgYGr.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-0DnaZGkC.js → index-uSsiwgZ2.js} +1 -1
  131. package/src/assets/web-panel/assets/{initDefaultProps-C3wUb4je.js → initDefaultProps-Ckp7xVZS.js} +1 -1
  132. package/src/assets/web-panel/assets/{motion-D-jYFUNA.js → motion-DUZgCpn6.js} +1 -1
  133. package/src/assets/web-panel/assets/{move-tqb8nwJ2.js → move-B2rttglH.js} +1 -1
  134. package/src/assets/web-panel/assets/{mtc-parser-DSOjMJMz.js → mtc-parser-BYWcLTuN.js} +1 -1
  135. package/src/assets/web-panel/assets/{omit-CJ7VimIW.js → omit-DhHqKeFx.js} +1 -1
  136. package/src/assets/web-panel/assets/{pickAttrs-qXiG1iT3.js → pickAttrs-ANpJaWMO.js} +1 -1
  137. package/src/assets/web-panel/assets/{placementArrow-CkGBcy1Z.js → placementArrow-CfZWe7CA.js} +1 -1
  138. package/src/assets/web-panel/assets/{responsiveObserve-D3WvTlLO.js → responsiveObserve-D7Xz2y-R.js} +1 -1
  139. package/src/assets/web-panel/assets/{slide-BEE2ftge.js → slide-DekqCBY9.js} +1 -1
  140. package/src/assets/web-panel/assets/{statusUtils-CsfBpxiG.js → statusUtils-BET6XU7h.js} +1 -1
  141. package/src/assets/web-panel/assets/{styleChecker-xPrIiJPI.js → styleChecker-qHmJjV62.js} +1 -1
  142. package/src/assets/web-panel/assets/{useFlexGapSupport-C5Yd-SCR.js → useFlexGapSupport-BoMCTNyu.js} +1 -1
  143. package/src/assets/web-panel/assets/{useFs-D_MDS8HI.js → useFs-TdW-NgFC.js} +1 -1
  144. package/src/assets/web-panel/assets/{usePersonalDataHub-BqJh-usA.js → usePersonalDataHub-Dyox7wCu.js} +1 -1
  145. package/src/assets/web-panel/assets/{vnode-chm_p-gz.js → vnode-XUn5S8CP.js} +1 -1
  146. package/src/assets/web-panel/assets/{zoom-3-YDNz0Z.js → zoom-Ls77i8hn.js} +1 -1
  147. package/src/assets/web-panel/index.html +1 -1
  148. package/src/command-manifest.json +8 -1
  149. package/src/commands/a2a.js +19 -4
  150. package/src/commands/activitypub.js +20 -3
  151. package/src/commands/agent.js +27 -5
  152. package/src/commands/audit.js +40 -24
  153. package/src/commands/changelog.js +148 -0
  154. package/src/commands/codeintel.js +68 -0
  155. package/src/commands/collab.js +15 -2
  156. package/src/commands/compliance.js +5 -0
  157. package/src/commands/config.js +4 -1
  158. package/src/commands/dao.js +86 -14
  159. package/src/commands/dev.js +2 -0
  160. package/src/commands/did.js +7 -1
  161. package/src/commands/dlp.js +23 -1
  162. package/src/commands/economy.js +29 -3
  163. package/src/commands/eval.js +7 -0
  164. package/src/commands/evomap.js +26 -0
  165. package/src/commands/governance.js +17 -3
  166. package/src/commands/hardening.js +18 -0
  167. package/src/commands/incentive.js +5 -0
  168. package/src/commands/init.js +46 -2
  169. package/src/commands/kg.js +4 -0
  170. package/src/commands/loop.js +6 -1
  171. package/src/commands/lowcode.js +15 -2
  172. package/src/commands/marketplace.js +40 -6
  173. package/src/commands/matrix.js +20 -1
  174. package/src/commands/memory.js +4 -1
  175. package/src/commands/mtc.js +7 -1
  176. package/src/commands/nostr.js +31 -4
  177. package/src/commands/note.js +7 -4
  178. package/src/commands/plugin.js +125 -5
  179. package/src/commands/pqc.js +5 -0
  180. package/src/commands/reputation.js +10 -1
  181. package/src/commands/scim.js +6 -0
  182. package/src/commands/session.js +32 -10
  183. package/src/commands/siem.js +11 -0
  184. package/src/commands/sla.js +18 -3
  185. package/src/commands/social.js +38 -5
  186. package/src/commands/sso.js +10 -2
  187. package/src/commands/stress.js +23 -4
  188. package/src/commands/team.js +9 -2
  189. package/src/commands/tech.js +2 -0
  190. package/src/commands/tenant.js +10 -1
  191. package/src/commands/terraform.js +6 -0
  192. package/src/commands/update.js +1 -1
  193. package/src/commands/zkp.js +20 -0
  194. package/src/data/changelog.json +189 -0
  195. package/src/gateways/ws/message-dispatcher.js +5 -2
  196. package/src/gateways/ws/remote-session-protocol.js +110 -42
  197. package/src/harness/jsonl-session-store.js +12 -5
  198. package/src/harness/plugin-manager.js +16 -4
  199. package/src/harness/remote-command-ledger.js +46 -15
  200. package/src/harness/worktree-isolator.js +16 -6
  201. package/src/index.js +2 -0
  202. package/src/lib/__tests__/logger.test.js +5 -2
  203. package/src/lib/a2a-protocol.js +25 -1
  204. package/src/lib/activitypub-bridge.js +61 -0
  205. package/src/lib/agent-core.js +2 -0
  206. package/src/lib/agent-economy.js +262 -29
  207. package/src/lib/agent-network.js +7 -1
  208. package/src/lib/agent-team/team-runner.js +25 -9
  209. package/src/lib/agent-team/team-worktree.js +36 -4
  210. package/src/lib/async-hook-supervisor.cjs +102 -16
  211. package/src/lib/audit-logger.js +7 -6
  212. package/src/lib/autonomous-developer.js +60 -0
  213. package/src/lib/changelog.js +252 -0
  214. package/src/lib/chat-core.js +17 -4
  215. package/src/lib/collaboration-governance.js +56 -0
  216. package/src/lib/community-governance.js +68 -0
  217. package/src/lib/compliance-manager.js +63 -0
  218. package/src/lib/cross-chain.js +5 -0
  219. package/src/lib/dao-governance.js +151 -2
  220. package/src/lib/did-manager.js +23 -10
  221. package/src/lib/dlp-engine.js +70 -0
  222. package/src/lib/eval/runner.js +89 -1
  223. package/src/lib/eval/tasks.js +170 -0
  224. package/src/lib/eval/trend.js +16 -1
  225. package/src/lib/evolution-system.js +92 -0
  226. package/src/lib/evomap-federation.js +55 -0
  227. package/src/lib/evomap-governance.js +94 -0
  228. package/src/lib/fatal-handler.js +17 -1
  229. package/src/lib/hardening-manager.js +73 -0
  230. package/src/lib/hierarchical-memory.js +14 -8
  231. package/src/lib/knowledge-exporter.js +8 -2
  232. package/src/lib/knowledge-graph.js +79 -0
  233. package/src/lib/llm-providers.js +23 -14
  234. package/src/lib/logger.js +4 -1
  235. package/src/lib/lsp/__tests__/benchmark.test.js +88 -0
  236. package/src/lib/lsp/__tests__/lsp-client.test.js +11 -0
  237. package/src/lib/lsp/__tests__/lsp-manager.test.js +69 -0
  238. package/src/lib/lsp/benchmark.js +257 -0
  239. package/src/lib/lsp/lsp-client.js +30 -9
  240. package/src/lib/lsp/lsp-manager.js +54 -2
  241. package/src/lib/lsp/lsp-server-registry.js +15 -4
  242. package/src/lib/matrix-bridge.js +84 -0
  243. package/src/lib/memory-manager.js +5 -3
  244. package/src/lib/nostr-bridge.js +53 -0
  245. package/src/lib/permission-engine.js +22 -4
  246. package/src/lib/plugin-runtime/__tests__/remote-source.test.js +256 -0
  247. package/src/lib/plugin-runtime/hooks.js +20 -4
  248. package/src/lib/plugin-runtime/install.js +9 -1
  249. package/src/lib/plugin-runtime/monitors.js +20 -4
  250. package/src/lib/plugin-runtime/policy.js +9 -1
  251. package/src/lib/plugin-runtime/remote-source.js +240 -0
  252. package/src/lib/plugin-runtime/signature.js +16 -0
  253. package/src/lib/pqc-manager.js +64 -0
  254. package/src/lib/reputation-optimizer.js +101 -0
  255. package/src/lib/sandbox-egress-proxy.js +195 -59
  256. package/src/lib/sandbox-fs-policy.js +112 -0
  257. package/src/lib/sandbox-network-policy.js +44 -5
  258. package/src/lib/sandbox-v2.js +14 -14
  259. package/src/lib/scim-manager.js +36 -0
  260. package/src/lib/session-manager.js +5 -2
  261. package/src/lib/settings-hooks.cjs +1 -0
  262. package/src/lib/siem-exporter.js +45 -0
  263. package/src/lib/skill-loader.js +20 -2
  264. package/src/lib/skill-marketplace.js +83 -0
  265. package/src/lib/sla-manager.js +88 -0
  266. package/src/lib/social-manager.js +74 -0
  267. package/src/lib/stress-tester.js +65 -0
  268. package/src/lib/tech-learning-engine.js +75 -0
  269. package/src/lib/telemetry/span-recorder.js +12 -1
  270. package/src/lib/tenant-saas.js +107 -0
  271. package/src/lib/terraform-manager.js +67 -0
  272. package/src/lib/token-incentive.js +180 -29
  273. package/src/lib/wallet-manager.js +81 -35
  274. package/src/lib/zkp-engine.js +87 -0
  275. package/src/repl/agent-repl.js +17 -2
  276. package/src/runtime/__tests__/auto-pin.test.js +104 -0
  277. package/src/runtime/agent-core.js +241 -68
  278. package/src/runtime/auto-pin.js +117 -0
  279. package/src/runtime/headless-runner.js +35 -0
  280. package/src/assets/web-panel/assets/OrderTableRenderer-CRIFE2Tj.js +0 -1
  281. package/src/assets/web-panel/assets/Projects-CGcNIs_g.js +0 -1
  282. package/src/assets/web-panel/assets/Terminal-ZzU0Io1Y.js +0 -3
  283. package/src/assets/web-panel/assets/devWarning-DxwmsL4j.js +0 -1
  284. package/src/assets/web-panel/assets/index-Cg2ldRfU.js +0 -1
  285. package/src/assets/web-panel/assets/index-Dwzo-dtJ.js +0 -1
@@ -178,6 +178,70 @@ export function ensurePQCTables(db) {
178
178
  `);
179
179
  }
180
180
 
181
+ /**
182
+ * Rehydrate _keys/_migrations from the persisted pqc_keys/pqc_migration_status
183
+ * tables. The `cc` CLI runs every command in a fresh process, so without this
184
+ * the Maps start empty and a key generated / migration run in a prior
185
+ * invocation is invisible (`cc pqc keys` reports "No PQC keys" and `cc pqc
186
+ * migration-status` is empty even when the rows exist; `cc pqc migrate` counts
187
+ * sourceKeys off the empty Map and reports "Migrated 0/0" while real keys sit
188
+ * unmigrated). Call after ensurePQCTables(db). NOTE: this module holds only
189
+ * public-key inventory metadata — there is no private key or sign/verify path
190
+ * here — so the impact is inventory visibility + migration accounting, not
191
+ * crypto verification.
192
+ */
193
+ export function loadFromDb(db) {
194
+ if (!db) return 0;
195
+ _keys.clear();
196
+ _migrations.clear();
197
+
198
+ const keyRows = db.prepare(`SELECT * FROM pqc_keys`).all();
199
+ for (const row of keyRows) {
200
+ let metadata = {};
201
+ try {
202
+ metadata = row.metadata ? JSON.parse(row.metadata) : {};
203
+ } catch {
204
+ metadata = {};
205
+ }
206
+ // Derived fields (family/publicKeyBytes/signatureBytes) come from the
207
+ // algorithm spec, not the DB — reconstruct them exactly as generateKey does.
208
+ const spec = ALGORITHM_SPECS[row.algorithm] || {};
209
+ _keys.set(row.id, {
210
+ id: row.id,
211
+ algorithm: row.algorithm,
212
+ family: spec.family ?? null,
213
+ purpose: row.purpose,
214
+ publicKey: row.public_key,
215
+ keySize: row.key_size ?? spec.keySize ?? 0,
216
+ publicKeyBytes: spec.publicKeyBytes ?? null,
217
+ signatureBytes: spec.signatureBytes ?? null,
218
+ hybridMode: !!row.hybrid_mode,
219
+ classicalAlgorithm: row.classical_algorithm ?? null,
220
+ status: row.status,
221
+ metadata,
222
+ createdAt: row.created_at ?? null,
223
+ });
224
+ }
225
+
226
+ const migRows = db.prepare(`SELECT * FROM pqc_migration_status`).all();
227
+ for (const row of migRows) {
228
+ _migrations.set(row.id, {
229
+ id: row.id,
230
+ planName: row.plan_name,
231
+ sourceAlgorithm: row.source_algorithm,
232
+ targetAlgorithm: row.target_algorithm,
233
+ totalKeys: row.total_keys ?? 0,
234
+ migratedKeys: row.migrated_keys ?? 0,
235
+ status: row.status,
236
+ startedAt: row.started_at ?? null,
237
+ completedAt: row.completed_at ?? null,
238
+ errorMessage: row.error_message ?? null,
239
+ });
240
+ }
241
+
242
+ return _keys.size + _migrations.size;
243
+ }
244
+
181
245
  /* ── Key Management ───────────────────────────────────────── */
182
246
 
183
247
  export function listKeys(filter = {}) {
@@ -509,6 +509,107 @@ export function _resetState() {
509
509
  _maxConcurrentOptimizations = DEFAULT_MAX_CONCURRENT_OPTIMIZATIONS;
510
510
  }
511
511
 
512
+ /**
513
+ * Rehydrate the in-memory Maps from the persisted reputation_* tables. The CLI
514
+ * runs each `cc reputation` subcommand in a fresh node process, so
515
+ * _observations/_runs/_analytics start empty every invocation. Every write
516
+ * dual-writes (Map + INSERT into reputation_observations/
517
+ * reputation_optimization_runs/reputation_analytics) but every read
518
+ * (computeScore/listScores/detectAnomalies/getOptimizationStatus/getAnalytics/
519
+ * listOptimizationRuns/applyOptimizedParams + the V2 drivers) is Map-only and
520
+ * nothing SELECTed the tables back — so observed scores read back as 0, `list`/
521
+ * `runs`/`anomalies` were always empty, and `status`/`complete`/`apply <runId>`
522
+ * threw "Optimization run not found" for a run that genuinely exists on disk.
523
+ * Call after ensureReputationTables(db).
524
+ *
525
+ * Non-persisted fields default safely: run.history=[] (V1), errorMessage=null.
526
+ */
527
+ export function loadFromDb(db) {
528
+ if (!db) return 0;
529
+ ensureReputationTables(db);
530
+ _observations.clear();
531
+ _runs.clear();
532
+ _analytics.clear();
533
+
534
+ for (const r of db
535
+ .prepare(
536
+ `SELECT observation_id, did, score, kind, weight, recorded_at
537
+ FROM reputation_observations ORDER BY recorded_at ASC`,
538
+ )
539
+ .all()) {
540
+ if (!_observations.has(r.did)) _observations.set(r.did, []);
541
+ _observations.get(r.did).push({
542
+ observationId: r.observation_id,
543
+ did: r.did,
544
+ score: Number(r.score),
545
+ kind: r.kind,
546
+ weight: Number(r.weight),
547
+ recordedAt: Number(r.recorded_at),
548
+ });
549
+ }
550
+
551
+ for (const r of db
552
+ .prepare(
553
+ `SELECT run_id, objective, iterations, param_space, best_params, best_score,
554
+ status, created_at, completed_at
555
+ FROM reputation_optimization_runs ORDER BY created_at ASC`,
556
+ )
557
+ .all()) {
558
+ // One corrupt param/params cell must not sink the whole run list.
559
+ let paramSpace = {};
560
+ let bestParams = null;
561
+ try {
562
+ paramSpace = r.param_space ? JSON.parse(r.param_space) : {};
563
+ } catch {
564
+ paramSpace = {};
565
+ }
566
+ try {
567
+ bestParams = r.best_params ? JSON.parse(r.best_params) : null;
568
+ } catch {
569
+ bestParams = null;
570
+ }
571
+ _runs.set(r.run_id, {
572
+ runId: r.run_id,
573
+ objective: r.objective,
574
+ iterations: Number(r.iterations),
575
+ paramSpace,
576
+ bestParams,
577
+ bestScore: r.best_score == null ? null : Number(r.best_score),
578
+ errorMessage: null,
579
+ status: r.status,
580
+ createdAt: Number(r.created_at),
581
+ completedAt: r.completed_at == null ? null : Number(r.completed_at),
582
+ history: [],
583
+ _seq: ++_seq,
584
+ });
585
+ }
586
+
587
+ for (const r of db
588
+ .prepare(
589
+ `SELECT analytics_id, run_id, reputation_distribution, anomalies, recommendations, created_at
590
+ FROM reputation_analytics ORDER BY created_at ASC`,
591
+ )
592
+ .all()) {
593
+ const parse = (v, fallback) => {
594
+ try {
595
+ return v ? JSON.parse(v) : fallback;
596
+ } catch {
597
+ return fallback;
598
+ }
599
+ };
600
+ _analytics.set(r.run_id, {
601
+ analyticsId: r.analytics_id,
602
+ runId: r.run_id,
603
+ reputationDistribution: parse(r.reputation_distribution, {}),
604
+ anomalies: parse(r.anomalies, []),
605
+ recommendations: parse(r.recommendations, []),
606
+ createdAt: Number(r.created_at),
607
+ });
608
+ }
609
+
610
+ return _observations.size + _runs.size + _analytics.size;
611
+ }
612
+
512
613
  /* ═══════════════════════════════════════════════════════════════
513
614
  * V2 (Phase 60) — Frozen enums + async optimization lifecycle +
514
615
  * concurrency limiter + patch-merged setRunStatus + stats-v2.
@@ -17,7 +17,96 @@
17
17
 
18
18
  import http from "node:http";
19
19
  import net from "node:net";
20
- import { evaluateNetworkAccess } from "./sandbox-network-policy.js";
20
+ import dns from "node:dns";
21
+ import {
22
+ evaluateNetworkAccess,
23
+ isPrivateHost,
24
+ } from "./sandbox-network-policy.js";
25
+
26
+ /** Promisified `dns.lookup` returning ALL resolved addresses. */
27
+ function defaultLookup(host) {
28
+ return new Promise((resolve, reject) => {
29
+ dns.lookup(host, { all: true }, (err, addrs) =>
30
+ err ? reject(err) : resolve(addrs),
31
+ );
32
+ });
33
+ }
34
+
35
+ /**
36
+ * DNS-rebinding / SSRF-by-resolution guard. The domain policy is decided by
37
+ * NAME, but a public-looking name can RESOLVE to a private / metadata IP
38
+ * (`rebind.evil.com → 127.0.0.1` / `169.254.169.254`) and the name check would
39
+ * never see it — a classic rebinding bypass (Phase 1 acceptance "DNS 重绑定有
40
+ * 安全测试"). After a wildcard/permissive allow, resolve the host and reject if
41
+ * ANY address is private (unless `allowPrivate`). Fail CLOSED on an unresolvable
42
+ * host. On success, pin the connection to the exact validated address so a
43
+ * second resolution at connect time can't rebind to a different (private) IP.
44
+ * A `specific` allow (exact/subdomain name the user vetted) is exempt — the
45
+ * caller skips this — so intentional internal-name allowlisting still works.
46
+ * @returns {Promise<{ok:boolean, ip?:string, reason?:string}>}
47
+ */
48
+ async function guardResolvedTarget(host, policy, lookup) {
49
+ let addrs;
50
+ try {
51
+ addrs = await lookup(host);
52
+ } catch (e) {
53
+ return {
54
+ ok: false,
55
+ reason: `unresolvable host (fail-closed): ${e.code || e.message}`,
56
+ };
57
+ }
58
+ const list = Array.isArray(addrs) ? addrs : addrs ? [addrs] : [];
59
+ if (list.length === 0) {
60
+ return { ok: false, reason: "host resolved to no addresses (fail-closed)" };
61
+ }
62
+ for (const a of list) {
63
+ const ip = typeof a === "string" ? a : a && a.address;
64
+ if (ip && isPrivateHost(ip) && !policy.allowPrivate) {
65
+ return {
66
+ ok: false,
67
+ reason: `dns-rebinding: ${host} resolves to private ${ip}`,
68
+ };
69
+ }
70
+ }
71
+ const first = list[0];
72
+ return { ok: true, ip: typeof first === "string" ? first : first.address };
73
+ }
74
+
75
+ /**
76
+ * Parse a CONNECT request target ("host:port") into `{ host, port }`. A naive
77
+ * `split(":")` mangles IPv6 literals — `[2001:db8::1]:8443` would yield host
78
+ * `"[2001"` and a NaN port that silently defaults to 443, so even an IP-pinned
79
+ * tunnel connects to the WRONG port (and, when unpinned, the wrong host). Handle
80
+ * the bracketed-IPv6, bare-IPv6, and `name[:port]` forms explicitly. Port
81
+ * defaults to 443 (the CONNECT/HTTPS default).
82
+ */
83
+ export function parseConnectTarget(target) {
84
+ const s = String(target == null ? "" : target).trim();
85
+ const toPort = (v) => {
86
+ const n = parseInt(v, 10);
87
+ return Number.isInteger(n) && n > 0 && n <= 65535 ? n : 443;
88
+ };
89
+ // Bracketed IPv6: "[::1]" or "[::1]:8443"
90
+ if (s.startsWith("[")) {
91
+ const end = s.indexOf("]");
92
+ if (end > 0) {
93
+ const host = s.slice(1, end);
94
+ const rest = s.slice(end + 1); // "" or ":8443"
95
+ return { host, port: rest.startsWith(":") ? toPort(rest.slice(1)) : 443 };
96
+ }
97
+ // malformed bracket — fall through to the generic handling
98
+ }
99
+ // Bare IPv6 literal (2+ colons, no brackets) → no port component to split off.
100
+ if ((s.match(/:/g) || []).length > 1) {
101
+ return { host: s, port: 443 };
102
+ }
103
+ // "name:port" (rsplit on the single colon) or bare "name".
104
+ const idx = s.lastIndexOf(":");
105
+ if (idx > 0) {
106
+ return { host: s.slice(0, idx), port: toPort(s.slice(idx + 1)) };
107
+ }
108
+ return { host: s, port: 443 };
109
+ }
21
110
 
22
111
  /**
23
112
  * Build the proxy env vars a child process needs to route through the proxy.
@@ -64,77 +153,124 @@ export function createEgressProxy(policy = {}, opts = {}) {
64
153
  return verdict;
65
154
  };
66
155
 
67
- const server = http.createServer((req, res) => {
68
- // A forward-proxy request carries the absolute URL in the request line.
69
- const verdict = decide(req.url, "http");
70
- if (!verdict.allowed) {
71
- res.writeHead(403, { "content-type": "text/plain" });
72
- res.end(`egress blocked: ${verdict.host} ${verdict.reason}\n`);
73
- return;
156
+ const lookup = opts.lookup || defaultLookup;
157
+ const rebindGuard = opts.checkDnsRebinding !== false; // on by default
158
+
159
+ // Second gate: after a NAME-based allow, re-check the RESOLVED address for
160
+ // DNS rebinding (unless the allow was `specific` — a name the user vetted —
161
+ // or the guard is disabled). Returns the verdict to ACT on plus `ip`, the
162
+ // exact address to pin the connection to (null → connect by the original
163
+ // name). When the resolved IP is private the name-based allow is overturned:
164
+ // fix the counters and re-notify observers with the block reason.
165
+ const guard = async (verdict, kind) => {
166
+ if (!verdict.allowed || verdict.specific || !rebindGuard) {
167
+ return { verdict, ip: null };
74
168
  }
75
- let u;
169
+ const res = await guardResolvedTarget(verdict.host, policy, lookup);
170
+ if (res.ok) return { verdict, ip: res.ip };
171
+ state.allowed -= 1;
172
+ state.blocked += 1;
173
+ const blocked = { allowed: false, host: verdict.host, reason: res.reason };
174
+ if (typeof opts.onDecision === "function") {
175
+ try {
176
+ opts.onDecision({ kind, target: verdict.host, ...blocked });
177
+ } catch {
178
+ /* observation is best-effort */
179
+ }
180
+ }
181
+ return { verdict: blocked, ip: null };
182
+ };
183
+
184
+ const server = http.createServer(async (req, res) => {
76
185
  try {
77
- u = new URL(req.url);
186
+ // A forward-proxy request carries the absolute URL in the request line.
187
+ const { verdict, ip } = await guard(decide(req.url, "http"), "http");
188
+ if (!verdict.allowed) {
189
+ res.writeHead(403, { "content-type": "text/plain" });
190
+ res.end(`egress blocked: ${verdict.host} — ${verdict.reason}\n`);
191
+ return;
192
+ }
193
+ let u;
194
+ try {
195
+ u = new URL(req.url);
196
+ } catch {
197
+ res.writeHead(400, { "content-type": "text/plain" });
198
+ res.end("bad request target\n");
199
+ return;
200
+ }
201
+ const upstream = http.request(
202
+ {
203
+ hostname: ip || u.hostname, // pin to the validated IP when re-checked
204
+ port: u.port || 80,
205
+ path: `${u.pathname}${u.search}`,
206
+ method: req.method,
207
+ headers: req.headers, // preserves the original Host header
208
+ },
209
+ (upRes) => {
210
+ res.writeHead(upRes.statusCode || 502, upRes.headers);
211
+ upRes.pipe(res);
212
+ },
213
+ );
214
+ upstream.on("error", () => {
215
+ if (!res.headersSent)
216
+ res.writeHead(502, { "content-type": "text/plain" });
217
+ res.end("upstream error\n");
218
+ });
219
+ req.pipe(upstream);
78
220
  } catch {
79
- res.writeHead(400, { "content-type": "text/plain" });
80
- res.end("bad request target\n");
81
- return;
82
- }
83
- const upstream = http.request(
84
- {
85
- hostname: u.hostname,
86
- port: u.port || 80,
87
- path: `${u.pathname}${u.search}`,
88
- method: req.method,
89
- headers: req.headers,
90
- },
91
- (upRes) => {
92
- res.writeHead(upRes.statusCode || 502, upRes.headers);
93
- upRes.pipe(res);
94
- },
95
- );
96
- upstream.on("error", () => {
97
221
  if (!res.headersSent)
98
222
  res.writeHead(502, { "content-type": "text/plain" });
99
- res.end("upstream error\n");
100
- });
101
- req.pipe(upstream);
223
+ res.end("proxy error\n");
224
+ }
102
225
  });
103
226
 
104
227
  // HTTPS (and any TLS) goes through CONNECT — tunnel only allowed hosts.
105
- server.on("connect", (req, clientSocket, head) => {
106
- const verdict = decide(req.url, "connect"); // req.url = "host:port"
107
- if (!verdict.allowed) {
108
- clientSocket.write(
109
- "HTTP/1.1 403 Forbidden\r\n" +
110
- "Content-Type: text/plain\r\n\r\n" +
111
- `egress blocked: ${verdict.host} — ${verdict.reason}\n`,
112
- );
113
- clientSocket.end();
114
- return;
115
- }
116
- const [host, portStr] = req.url.split(":");
117
- const port = parseInt(portStr, 10) || 443;
118
- const upstream = net.connect(port, host, () => {
119
- clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
120
- if (head && head.length) upstream.write(head);
121
- upstream.pipe(clientSocket);
122
- clientSocket.pipe(upstream);
123
- });
124
- upstream.on("error", () => {
125
- try {
228
+ server.on("connect", async (req, clientSocket, head) => {
229
+ try {
230
+ const { verdict, ip } = await guard(
231
+ decide(req.url, "connect"),
232
+ "connect",
233
+ ); // req.url = "host:port"
234
+ if (!verdict.allowed) {
235
+ clientSocket.write(
236
+ "HTTP/1.1 403 Forbidden\r\n" +
237
+ "Content-Type: text/plain\r\n\r\n" +
238
+ `egress blocked: ${verdict.host} — ${verdict.reason}\n`,
239
+ );
126
240
  clientSocket.end();
127
- } catch {
128
- /* already closed */
241
+ return;
129
242
  }
130
- });
131
- clientSocket.on("error", () => {
243
+ // Parse host:port properly (IPv6 literals break a naive split).
244
+ const { host, port } = parseConnectTarget(req.url);
245
+ // Connect to the pinned validated IP so a rebind at connect time can't
246
+ // swap in a private address between our check and the socket.
247
+ const upstream = net.connect(port, ip || host, () => {
248
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
249
+ if (head && head.length) upstream.write(head);
250
+ upstream.pipe(clientSocket);
251
+ clientSocket.pipe(upstream);
252
+ });
253
+ upstream.on("error", () => {
254
+ try {
255
+ clientSocket.end();
256
+ } catch {
257
+ /* already closed */
258
+ }
259
+ });
260
+ clientSocket.on("error", () => {
261
+ try {
262
+ upstream.destroy();
263
+ } catch {
264
+ /* already gone */
265
+ }
266
+ });
267
+ } catch {
132
268
  try {
133
- upstream.destroy();
269
+ clientSocket.end();
134
270
  } catch {
135
- /* already gone */
271
+ /* already closed */
136
272
  }
137
- });
273
+ }
138
274
  });
139
275
 
140
276
  return {
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Sandbox filesystem path policy (Phase 1) — decide whether a Shell subprocess
3
+ * (or the agent's own file tools) may read/write a given path under
4
+ * `sandbox.filesystem.{allowRead,denyRead,allowWrite,denyWrite}` (here the
5
+ * legacy `{read, write, denied}` shape). OS-level ENFORCEMENT is platform
6
+ * specific (bubblewrap `--bind`/`--ro-bind`/`--tmpfs`, seatbelt), but the POLICY
7
+ * DECISION is pure, shared, and must get the tricky cases right — the twin of
8
+ * `sandbox-network-policy.js`. The prior check used raw `filePath.startsWith()`,
9
+ * which let three attacks through:
10
+ *
11
+ * - PATH TRAVERSAL: `/tmp/../etc/passwd` starts with an allowed `/tmp` but
12
+ * resolves OUTSIDE it. We `path.resolve` first so `..` is collapsed.
13
+ * - BOUNDARY CONFUSION: `/tmpfoo` starts with `/tmp` yet is a different tree.
14
+ * We compare with `path.relative` (a real path boundary), not a substring.
15
+ * - SYMLINK ESCAPE: an allowed dir containing a symlink to `/etc` would read
16
+ * `/etc`. We resolve symlinks (of the longest existing ancestor) before the
17
+ * boundary check, and resolve the policy roots the same way so they stay
18
+ * comparable.
19
+ *
20
+ * Default-DENY: a path must match an allow root for the requested mode and must
21
+ * not fall under any deny root. This mirrors the previous behaviour (an empty
22
+ * allow list denied everything) while fixing the bypasses.
23
+ */
24
+
25
+ import fs from "node:fs";
26
+ import path from "node:path";
27
+
28
+ /** Real path of `abs`, resolving symlinks of the LONGEST EXISTING ancestor and
29
+ * re-appending the non-existent tail — so a symlinked existing parent is caught
30
+ * even when the leaf (e.g. a not-yet-created write target) does not exist. */
31
+ function resolveReal(abs, realpath) {
32
+ let cur = abs;
33
+ const tail = [];
34
+ // Bounded by the path depth; each iteration strips one segment.
35
+ for (;;) {
36
+ try {
37
+ const real = realpath(cur);
38
+ return tail.length ? path.join(real, ...tail.reverse()) : real;
39
+ } catch {
40
+ const parent = path.dirname(cur);
41
+ if (parent === cur) return abs; // hit the root, nothing on this path exists
42
+ tail.push(path.basename(cur));
43
+ cur = parent;
44
+ }
45
+ }
46
+ }
47
+
48
+ /** Is `target` the same as, or nested inside, `root`? Uses a real path boundary
49
+ * (via `path.relative`) so `/tmpfoo` is NOT inside `/tmp` and `..` can't sneak
50
+ * out. Cross-platform (handles Windows drive letters + separators). */
51
+ export function isWithinRoot(root, target) {
52
+ if (!root) return false;
53
+ const rel = path.relative(root, target);
54
+ return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
55
+ }
56
+
57
+ function normalizeRoot(root, cwd, realpath) {
58
+ return resolveReal(path.resolve(cwd, String(root)), realpath);
59
+ }
60
+
61
+ /**
62
+ * Evaluate filesystem access for a target path under a policy.
63
+ *
64
+ * @param {string} target a path (relative resolves against `cwd`)
65
+ * @param {object} opts
66
+ * @param {"read"|"write"} [opts.mode="read"]
67
+ * @param {{read?:string[], write?:string[], denied?:string[]}} [opts.policy]
68
+ * @param {string} [opts.cwd] base for relative targets/roots
69
+ * @param {(p:string)=>string} [opts.realpath] injectable for tests
70
+ * @returns {{allowed:boolean, path:string|null, reason:string}}
71
+ */
72
+ export function evaluatePathAccess(target, opts = {}) {
73
+ const {
74
+ mode = "read",
75
+ policy = {},
76
+ cwd = process.cwd(),
77
+ realpath = fs.realpathSync,
78
+ } = opts;
79
+
80
+ if (target == null || String(target).trim() === "") {
81
+ return { allowed: false, path: null, reason: "empty path" };
82
+ }
83
+ if (mode !== "read" && mode !== "write") {
84
+ return { allowed: false, path: null, reason: `unknown mode "${mode}"` };
85
+ }
86
+
87
+ const abs = path.resolve(cwd, String(target)); // collapses `..`
88
+ const real = resolveReal(abs, realpath); // resolves symlink escapes
89
+
90
+ const denied = policy.denied || [];
91
+ for (const d of denied) {
92
+ if (isWithinRoot(normalizeRoot(d, cwd, realpath), real)) {
93
+ return { allowed: false, path: real, reason: `denied by ${d}` };
94
+ }
95
+ }
96
+
97
+ const allowList = (mode === "read" ? policy.read : policy.write) || [];
98
+ for (const a of allowList) {
99
+ if (isWithinRoot(normalizeRoot(a, cwd, realpath), real)) {
100
+ return { allowed: true, path: real, reason: `within ${a}` };
101
+ }
102
+ }
103
+
104
+ // Default-deny: not under any allowed root for this mode.
105
+ return {
106
+ allowed: false,
107
+ path: real,
108
+ reason: allowList.length
109
+ ? `outside all allowed ${mode} roots`
110
+ : `no allowed ${mode} roots`,
111
+ };
112
+ }
@@ -33,6 +33,13 @@ export function extractHost(target) {
33
33
  }
34
34
  // Strip IPv6 brackets ("[::1]" → "::1") — the documented SSRF gotcha.
35
35
  if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
36
+ // Strip the trailing DNS root dot. "localhost." / "example.com." resolve to
37
+ // the exact same host as the dot-less form, but the extra dot defeats BOTH
38
+ // the deny/allow matcher (host !== "example.com") AND the loopback/private
39
+ // guard ("localhost." !== "localhost") — a one-character SSRF / deny-list
40
+ // bypass. (Node's URL already normalizes a trailing dot away for IPv4 literals
41
+ // but NOT for named hosts, so we must do it here.)
42
+ s = s.replace(/\.+$/, "");
36
43
  return s.toLowerCase() || null;
37
44
  }
38
45
 
@@ -53,6 +60,24 @@ function anyMatch(host, patterns) {
53
60
  return (patterns || []).some((p) => matchesDomain(host, p));
54
61
  }
55
62
 
63
+ /**
64
+ * If `h` is an IPv4-mapped IPv6 address, return the embedded IPv4 in dotted
65
+ * form; else null. Handles both the dotted tail ("::ffff:127.0.0.1", how a user
66
+ * might type it) and the compressed-hex tail ("::ffff:7f00:1", how Node's URL
67
+ * parser serializes it). The hex tail is two 16-bit groups = the 32-bit IPv4.
68
+ */
69
+ function mappedIpv4Tail(h) {
70
+ const dotted = h.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
71
+ if (dotted) return dotted[1];
72
+ const hex = h.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
73
+ if (hex) {
74
+ const high = parseInt(hex[1], 16);
75
+ const low = parseInt(hex[2], 16);
76
+ return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
77
+ }
78
+ return null;
79
+ }
80
+
56
81
  /** Private / loopback / link-local / cloud-metadata target? (SSRF guard) */
57
82
  export function isPrivateHost(host) {
58
83
  if (!host) return false;
@@ -63,10 +88,14 @@ export function isPrivateHost(host) {
63
88
  // IPv6 loopback / unique-local / link-local.
64
89
  if (h === "::1" || h === "::") return true;
65
90
  if (/^f[cd][0-9a-f]{2}:/i.test(h)) return true; // fc00::/7 unique-local
66
- if (/^fe80:/i.test(h)) return true; // link-local
67
- // IPv4-mapped IPv6 (::ffff:127.0.0.1) — check the tail.
68
- const mapped = h.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
69
- const ipv4 = mapped ? mapped[1] : h;
91
+ if (/^fe[89ab][0-9a-f]:/i.test(h)) return true; // fe80::/10 link-local (fe80–febf)
92
+ // IPv4-mapped IPv6 — check the embedded IPv4 tail. Node's WHATWG URL parser
93
+ // serializes "[::ffff:127.0.0.1]" as COMPRESSED HEX ("::ffff:7f00:1"), never
94
+ // dotted-decimal, so we must decode BOTH forms or the guard is dead code for
95
+ // any URL/bracketed input (loopback, private ranges, AND 169.254.169.254 all
96
+ // sail through — confirmed SSRF bypass). Bare non-URL hosts may still arrive
97
+ // dotted, so keep that form too.
98
+ const ipv4 = mappedIpv4Tail(h) || h;
70
99
  const m = ipv4.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
71
100
  if (m) {
72
101
  const [a, b] = [Number(m[1]), Number(m[2])];
@@ -107,7 +136,17 @@ export function evaluateNetworkAccess(target, policy = {}) {
107
136
  allowed.filter((p) => String(p).trim() !== "*"),
108
137
  );
109
138
  if (explicitNonStar) {
110
- return { allowed: true, host, reason: "matched allowedDomains" };
139
+ // `specific: true` marks a host the user vetted by exact/subdomain name (not
140
+ // a bare `*`). The egress proxy trusts these even when they resolve to a
141
+ // private IP (intentional internal/dev allowlisting) and so SKIPS the
142
+ // DNS-rebinding re-check for them — whereas `*`/permissive allows still get
143
+ // their resolved IP checked.
144
+ return {
145
+ allowed: true,
146
+ host,
147
+ reason: "matched allowedDomains",
148
+ specific: true,
149
+ };
111
150
  }
112
151
  // 3) Private / loopback / metadata targets are blocked unless allowed above.
113
152
  if (isPrivateHost(host) && !policy.allowPrivate) {