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.
- package/README.md +1 -1
- package/package.json +3 -2
- package/src/assets/web-panel/assets/{AIOps-BJif14yR.js → AIOps-DuEOzebi.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CXek6gJY.js → ActionButton-C6GiJa5H.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BgQhdAJi.js → Analytics-BBIiL_pO.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DTYl5NtR.js → AppLayout-CuI2gZRy.js} +3 -3
- package/src/assets/web-panel/assets/{Audit-BzATQAeR.js → Audit-CbUjfr-K.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BzdPEQhL.js → Backup-CsSW9ljf.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BM0KVh8E.js → BaseInput-kM3rLe7F.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-D7fHXHkz.js → Chat-kfE51YN3.js} +4 -4
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-Wm34RR-b.js → ChatBubbleRenderer-Bg2FTW6A.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-CAiSQ9vO.js → Checkbox-jYb1uj-M.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-Df40CrEe.js → Codegen-X9XiD5Lj.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DIT2fNFU.js → Col-BfbHu1xI.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CSTIbW2k.js → Community-c4oJMFS-.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DtqXZZUi.js → Compact-BFmnZpH7.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CUtzw6o7.js → Compliance-s9shVHBS.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CezmlnmU.js → Cowork-C5da--TC.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-P4BITarg.js → Cron-9kNbyNE4.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BRRiYwvq.js → Crosschain-C1k3NJD4.js} +1 -1
- package/src/assets/web-panel/assets/{DID-L5ijB9i4.js → DID-DkheEvNK.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CFxro4ob.js → Dashboard-C3FuSGmJ.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BmAFCQ71.js → Dropdown-CAV6FMFD.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DJBCEuon.js → EmailListRenderer-Cc-Ulc4l.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-z4oLq6eT.js → FamilyGuardDashboard-8CitHEtv.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DsVCpHMF.js → Federation-DFSZ5KDt.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DP3bP5th.js → FormItemContext-CEWCtnG2.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BAXP2Gc6.js → GenericCardRenderer-CwRzNzmX.js} +1 -1
- package/src/assets/web-panel/assets/{Git-I3g_bAw9.js → Git-0rgDM7cJ.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-jHS5-ioS.js → Governance-CfBKtK9F.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CPL7sfx9.js → Inference-CzcUVvud.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Jf236h4C.js → KnowledgeGraph-Dwc4YL4A.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-Cm2tcSKg.js → Logs-DgFPwR2F.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-CZK82rhi.js → Marketplace-DnpPTbGU.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CZCeji7a.js → McpTools-DRxxINyW.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-3l2KVS9k.js → Memory-OhmW_6Z3.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-VYdpoLh6.js → MobileBridge-DkTW-RK5.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-CiB9cmm1.js → MobileProjects-DpCGV_lI.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-wCFZbBuL.js → Mtc--vmkL0AS.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-1a3THIyG.js → MtcAudit-DGoFWjD4.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-DMzLB2hG.js → Multisig-B4kWgq95.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-D9wZbf6l.js → NLProgramming-YQwvommE.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-hrFe2ZM-.js → Notes-CgjtUADJ.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-CHGX5Jwm.js → NotificationSettings-6JDrpBG5.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-PM6IlxjO.js +1 -0
- package/src/assets/web-panel/assets/{Organization-By7FkhtY.js → Organization-8McOT_OF.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow--khGSzJn.js → Overflow-DAZpSCGc.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CAG9dH35.js → P2P-B7l2ZWLy.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-B3SQZmK9.js → PdhVaultBrowser-W1Ud-NAL.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-D35ec6JA.js → Permissions-Czm_w9u_.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-CxBF7hW_.js → PersonalDataHub-B4F0_ZVM.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-s6EtOO1s.js → Pipeline-B37aFiWv.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BbJYmWmO.js → Privacy-94KBnRrD.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-VGQq1jMT.js → ProjectInit-B5h8_dlh.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-B1ew3Teh.js → ProjectSettings-lmeI3Lpm.js} +2 -2
- package/src/assets/web-panel/assets/Projects-D5AYOasl.js +1 -0
- package/src/assets/web-panel/assets/{Providers-mrO47FIK.js → Providers-B3cNWwJg.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-IJF2mHyw.js → QrScannerModal-CHRvOw1R.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CU0wN_Z0.js → QuickAsk-D1JMgFEd.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DhN37R6G.js → Recommend-EYWGR79p.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-D_xOX_hu.js → RemoteSession-BAMC2pJt.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-D_Ssv7uH.js → Reputation-X2Hk1XG_.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Kx5dKxX7.js → Row-Eq98LRtU.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BlGwqZkY.js → RssFeed-i76s6z_o.js} +2 -2
- package/src/assets/web-panel/assets/{Search-hLFQzxpb.js → Search-D_Es7CAI.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DMlvsVt4.js → Security-kayD2e94.js} +3 -3
- package/src/assets/web-panel/assets/{Services-Bd0Qsj2m.js → Services-Ctsm6ul8.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DFclq9X3.js → Skeleton-CRNYlfdf.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-BNtln2qY.js → Skills-DkZYpF4-.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-Dch5H19G.js → Sla-DXkoBcOP.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-2UfQcU1g.js → SpeechSettings-DXi_GI1d.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CD9YQr4i.js → SyncSettings-rNKpfomn.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BNu-7MzI.js → Tasks-XF4uPtG-.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-D1n9CaIR.js → Templates-i0Hzfeyu.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DenL3iBW.js → Tenant-sMpHAi27.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-BoZvaYAl.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-D87IfukO.js → TimelineRenderer-yuC6K7Rf.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BkiZc8E3.js → Tokens-B8O8i0s6.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BNRSytGN.js → Trigger-Bv6yrlCr.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-Bwbd4X2m.js → Trust-C4mW95Ev.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-B8O2bs0o.js → UkeySign-DV6yKaHn.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-C2fL8zmH.js → VideoEditing-UI479-sD.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BORYDdE1.js → Wallet-M5oV9EVP.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-C0V5S2FM.js → WebAuthn-DDyDHPDR.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-2dR0fcHL.js → WorkflowEditor-RoBv4rqk.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BmuAFAn8.js → chat-Cw1mE5LS.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CN3smMcK.js → colors-BFP62Gwf.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DCnxb4kW.js → compact-item-17HL6pyC.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-vlR43pHn.js → createContext-D55X4fZX.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CqbHYuRc.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-D04tHYFd.js → hasIn-D3E5mNsP.js} +1 -1
- package/src/assets/web-panel/assets/{index-DbVsQBOH.js → index--44J50mc.js} +1 -1
- package/src/assets/web-panel/assets/{index-TslMTwNy.js → index-ASPYTzMK.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTOCnIQ7.js → index-BC6zxOlU.js} +1 -1
- package/src/assets/web-panel/assets/{index-ljuBiQW9.js → index-BEa1RW4c.js} +1 -1
- package/src/assets/web-panel/assets/{index-jzR7Ujuw.js → index-BQmZg5si.js} +1 -1
- package/src/assets/web-panel/assets/{index-BudvKQfG.js → index-BY_xZ2jy.js} +1 -1
- package/src/assets/web-panel/assets/{index-CP9m9Ggr.js → index-BgN5G5vF.js} +3 -3
- package/src/assets/web-panel/assets/{index-Cr-A0FYJ.js → index-BnGpft-Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-D5LZVZ0L.js → index-Bq83UBDr.js} +1 -1
- package/src/assets/web-panel/assets/{index-ToUh2b1-.js → index-Bzpp2z2S.js} +1 -1
- package/src/assets/web-panel/assets/{index-DbADHwhr.js → index-CFYGn88j.js} +1 -1
- package/src/assets/web-panel/assets/{index-ClhAHDK3.js → index-CGyTos4p.js} +1 -1
- package/src/assets/web-panel/assets/index-CKBI1U5K.js +1 -0
- package/src/assets/web-panel/assets/{index-C-6NO5il.js → index-CLEzsZ8A.js} +1 -1
- package/src/assets/web-panel/assets/{index-D8vwp4qp.js → index-CR9rNUvi.js} +1 -1
- package/src/assets/web-panel/assets/{index-_FIkN3jd.js → index-CW6LLoJO.js} +1 -1
- package/src/assets/web-panel/assets/{index-D65_XseV.js → index-C_MfcrLf.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnXebZLL.js → index-Cdvk2_2q.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_n8_vfI.js → index-CjCwi7we.js} +1 -1
- package/src/assets/web-panel/assets/index-CkLp6DTO.js +1 -0
- package/src/assets/web-panel/assets/{index-DDwLgQY9.js → index-CvCFKojj.js} +1 -1
- package/src/assets/web-panel/assets/{index-430S68MN.js → index-D4LHSde4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BXRg8GFQ.js → index-DCLu84x-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTtRscUa.js → index-DD_qGBrt.js} +1 -1
- package/src/assets/web-panel/assets/{index-BasvsWDM.js → index-DIRvdwBX.js} +1 -1
- package/src/assets/web-panel/assets/{index-BeGDEXFs.js → index-DOjA8bvj.js} +1 -1
- package/src/assets/web-panel/assets/{index-CsAsCPJ6.js → index-Da27u6j0.js} +1 -1
- package/src/assets/web-panel/assets/{index-D28DeAAB.js → index-DkBVYlE4.js} +1 -1
- package/src/assets/web-panel/assets/{index-CngCC8hC.js → index-DtAnPBWP.js} +1 -1
- package/src/assets/web-panel/assets/{index-B78xL3s2.js → index-DzG7mkgW.js} +1 -1
- package/src/assets/web-panel/assets/{index-DIpY0qM5.js → index-H9_X1Cg_.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5Sxb1sE.js → index-ITeEHb8L.js} +1 -1
- package/src/assets/web-panel/assets/{index-BTPEZTk1.js → index-KtLJXn4s.js} +1 -1
- package/src/assets/web-panel/assets/{index-hpvvtAZ3.js → index-RYZgFIwo.js} +1 -1
- package/src/assets/web-panel/assets/{index-HhNjnCfe.js → index-gAVxDiTZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTWg-18U.js → index-paVubXzn.js} +1 -1
- package/src/assets/web-panel/assets/{index-BqhASEL4.js → index-tUH_6bdJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DYAtmqiv.js → index-tVRPgYGr.js} +1 -1
- package/src/assets/web-panel/assets/{index-0DnaZGkC.js → index-uSsiwgZ2.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-C3wUb4je.js → initDefaultProps-Ckp7xVZS.js} +1 -1
- package/src/assets/web-panel/assets/{motion-D-jYFUNA.js → motion-DUZgCpn6.js} +1 -1
- package/src/assets/web-panel/assets/{move-tqb8nwJ2.js → move-B2rttglH.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DSOjMJMz.js → mtc-parser-BYWcLTuN.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CJ7VimIW.js → omit-DhHqKeFx.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-qXiG1iT3.js → pickAttrs-ANpJaWMO.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-CkGBcy1Z.js → placementArrow-CfZWe7CA.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-D3WvTlLO.js → responsiveObserve-D7Xz2y-R.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BEE2ftge.js → slide-DekqCBY9.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CsfBpxiG.js → statusUtils-BET6XU7h.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-xPrIiJPI.js → styleChecker-qHmJjV62.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-C5Yd-SCR.js → useFlexGapSupport-BoMCTNyu.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-D_MDS8HI.js → useFs-TdW-NgFC.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BqJh-usA.js → usePersonalDataHub-Dyox7wCu.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-chm_p-gz.js → vnode-XUn5S8CP.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-3-YDNz0Z.js → zoom-Ls77i8hn.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-manifest.json +8 -1
- package/src/commands/a2a.js +19 -4
- package/src/commands/activitypub.js +20 -3
- package/src/commands/agent.js +27 -5
- package/src/commands/audit.js +40 -24
- package/src/commands/changelog.js +148 -0
- package/src/commands/codeintel.js +68 -0
- package/src/commands/collab.js +15 -2
- package/src/commands/compliance.js +5 -0
- package/src/commands/config.js +4 -1
- package/src/commands/dao.js +86 -14
- package/src/commands/dev.js +2 -0
- package/src/commands/did.js +7 -1
- package/src/commands/dlp.js +23 -1
- package/src/commands/economy.js +29 -3
- package/src/commands/eval.js +7 -0
- package/src/commands/evomap.js +26 -0
- package/src/commands/governance.js +17 -3
- package/src/commands/hardening.js +18 -0
- package/src/commands/incentive.js +5 -0
- package/src/commands/init.js +46 -2
- package/src/commands/kg.js +4 -0
- package/src/commands/loop.js +6 -1
- package/src/commands/lowcode.js +15 -2
- package/src/commands/marketplace.js +40 -6
- package/src/commands/matrix.js +20 -1
- package/src/commands/memory.js +4 -1
- package/src/commands/mtc.js +7 -1
- package/src/commands/nostr.js +31 -4
- package/src/commands/note.js +7 -4
- package/src/commands/plugin.js +125 -5
- package/src/commands/pqc.js +5 -0
- package/src/commands/reputation.js +10 -1
- package/src/commands/scim.js +6 -0
- package/src/commands/session.js +32 -10
- package/src/commands/siem.js +11 -0
- package/src/commands/sla.js +18 -3
- package/src/commands/social.js +38 -5
- package/src/commands/sso.js +10 -2
- package/src/commands/stress.js +23 -4
- package/src/commands/team.js +9 -2
- package/src/commands/tech.js +2 -0
- package/src/commands/tenant.js +10 -1
- package/src/commands/terraform.js +6 -0
- package/src/commands/update.js +1 -1
- package/src/commands/zkp.js +20 -0
- package/src/data/changelog.json +189 -0
- package/src/gateways/ws/message-dispatcher.js +5 -2
- package/src/gateways/ws/remote-session-protocol.js +110 -42
- package/src/harness/jsonl-session-store.js +12 -5
- package/src/harness/plugin-manager.js +16 -4
- package/src/harness/remote-command-ledger.js +46 -15
- package/src/harness/worktree-isolator.js +16 -6
- package/src/index.js +2 -0
- package/src/lib/__tests__/logger.test.js +5 -2
- package/src/lib/a2a-protocol.js +25 -1
- package/src/lib/activitypub-bridge.js +61 -0
- package/src/lib/agent-core.js +2 -0
- package/src/lib/agent-economy.js +262 -29
- package/src/lib/agent-network.js +7 -1
- package/src/lib/agent-team/team-runner.js +25 -9
- package/src/lib/agent-team/team-worktree.js +36 -4
- package/src/lib/async-hook-supervisor.cjs +102 -16
- package/src/lib/audit-logger.js +7 -6
- package/src/lib/autonomous-developer.js +60 -0
- package/src/lib/changelog.js +252 -0
- package/src/lib/chat-core.js +17 -4
- package/src/lib/collaboration-governance.js +56 -0
- package/src/lib/community-governance.js +68 -0
- package/src/lib/compliance-manager.js +63 -0
- package/src/lib/cross-chain.js +5 -0
- package/src/lib/dao-governance.js +151 -2
- package/src/lib/did-manager.js +23 -10
- package/src/lib/dlp-engine.js +70 -0
- package/src/lib/eval/runner.js +89 -1
- package/src/lib/eval/tasks.js +170 -0
- package/src/lib/eval/trend.js +16 -1
- package/src/lib/evolution-system.js +92 -0
- package/src/lib/evomap-federation.js +55 -0
- package/src/lib/evomap-governance.js +94 -0
- package/src/lib/fatal-handler.js +17 -1
- package/src/lib/hardening-manager.js +73 -0
- package/src/lib/hierarchical-memory.js +14 -8
- package/src/lib/knowledge-exporter.js +8 -2
- package/src/lib/knowledge-graph.js +79 -0
- package/src/lib/llm-providers.js +23 -14
- package/src/lib/logger.js +4 -1
- package/src/lib/lsp/__tests__/benchmark.test.js +88 -0
- package/src/lib/lsp/__tests__/lsp-client.test.js +11 -0
- package/src/lib/lsp/__tests__/lsp-manager.test.js +69 -0
- package/src/lib/lsp/benchmark.js +257 -0
- package/src/lib/lsp/lsp-client.js +30 -9
- package/src/lib/lsp/lsp-manager.js +54 -2
- package/src/lib/lsp/lsp-server-registry.js +15 -4
- package/src/lib/matrix-bridge.js +84 -0
- package/src/lib/memory-manager.js +5 -3
- package/src/lib/nostr-bridge.js +53 -0
- package/src/lib/permission-engine.js +22 -4
- package/src/lib/plugin-runtime/__tests__/remote-source.test.js +256 -0
- package/src/lib/plugin-runtime/hooks.js +20 -4
- package/src/lib/plugin-runtime/install.js +9 -1
- package/src/lib/plugin-runtime/monitors.js +20 -4
- package/src/lib/plugin-runtime/policy.js +9 -1
- package/src/lib/plugin-runtime/remote-source.js +240 -0
- package/src/lib/plugin-runtime/signature.js +16 -0
- package/src/lib/pqc-manager.js +64 -0
- package/src/lib/reputation-optimizer.js +101 -0
- package/src/lib/sandbox-egress-proxy.js +195 -59
- package/src/lib/sandbox-fs-policy.js +112 -0
- package/src/lib/sandbox-network-policy.js +44 -5
- package/src/lib/sandbox-v2.js +14 -14
- package/src/lib/scim-manager.js +36 -0
- package/src/lib/session-manager.js +5 -2
- package/src/lib/settings-hooks.cjs +1 -0
- package/src/lib/siem-exporter.js +45 -0
- package/src/lib/skill-loader.js +20 -2
- package/src/lib/skill-marketplace.js +83 -0
- package/src/lib/sla-manager.js +88 -0
- package/src/lib/social-manager.js +74 -0
- package/src/lib/stress-tester.js +65 -0
- package/src/lib/tech-learning-engine.js +75 -0
- package/src/lib/telemetry/span-recorder.js +12 -1
- package/src/lib/tenant-saas.js +107 -0
- package/src/lib/terraform-manager.js +67 -0
- package/src/lib/token-incentive.js +180 -29
- package/src/lib/wallet-manager.js +81 -35
- package/src/lib/zkp-engine.js +87 -0
- package/src/repl/agent-repl.js +17 -2
- package/src/runtime/__tests__/auto-pin.test.js +104 -0
- package/src/runtime/agent-core.js +241 -68
- package/src/runtime/auto-pin.js +117 -0
- package/src/runtime/headless-runner.js +35 -0
- package/src/assets/web-panel/assets/OrderTableRenderer-CRIFE2Tj.js +0 -1
- package/src/assets/web-panel/assets/Projects-CGcNIs_g.js +0 -1
- package/src/assets/web-panel/assets/Terminal-ZzU0Io1Y.js +0 -3
- package/src/assets/web-panel/assets/devWarning-DxwmsL4j.js +0 -1
- package/src/assets/web-panel/assets/index-Cg2ldRfU.js +0 -1
- package/src/assets/web-panel/assets/index-Dwzo-dtJ.js +0 -1
package/src/lib/pqc-manager.js
CHANGED
|
@@ -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
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
128
|
-
/* already closed */
|
|
241
|
+
return;
|
|
129
242
|
}
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
269
|
+
clientSocket.end();
|
|
134
270
|
} catch {
|
|
135
|
-
/* already
|
|
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 (/^
|
|
67
|
-
// IPv4-mapped IPv6
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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) {
|