chainlesschain 0.162.149 → 0.162.150
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/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-BJif14yR.js → AIOps-CBLvrjeG.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CXek6gJY.js → ActionButton-DYv2GtE0.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BgQhdAJi.js → Analytics-CL6PZ-Ww.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DTYl5NtR.js → AppLayout-xuyHt4uA.js} +3 -3
- package/src/assets/web-panel/assets/{Audit-BzATQAeR.js → Audit-DOvSVycq.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BzdPEQhL.js → Backup-DMhfTswr.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BM0KVh8E.js → BaseInput-CQxDdm63.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-D7fHXHkz.js → Chat-BEeNhK2a.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-Wm34RR-b.js → ChatBubbleRenderer-8nul8eTq.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-CAiSQ9vO.js → Checkbox-CL04O-ER.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-Df40CrEe.js → Codegen-C50o-n7x.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DIT2fNFU.js → Col-CH-bDkzs.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CSTIbW2k.js → Community-DqAIMvSe.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DtqXZZUi.js → Compact-C43fo_my.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CUtzw6o7.js → Compliance-BERnMrdP.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CezmlnmU.js → Cowork-Baw3w1gp.js} +4 -4
- package/src/assets/web-panel/assets/{Cron-P4BITarg.js → Cron-BEqAHF1-.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BRRiYwvq.js → Crosschain-CvnmKwK4.js} +1 -1
- package/src/assets/web-panel/assets/{DID-L5ijB9i4.js → DID-iMPxgVdt.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CFxro4ob.js → Dashboard--4SJX3YR.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BmAFCQ71.js → Dropdown-BgLV6xgz.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DJBCEuon.js → EmailListRenderer-Bvb8oUD8.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-z4oLq6eT.js → FamilyGuardDashboard-DpXPjJB5.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DsVCpHMF.js → Federation-BbFfJ1Xi.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DP3bP5th.js → FormItemContext-DeGq1B-q.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BAXP2Gc6.js → GenericCardRenderer-CNW7s9zM.js} +1 -1
- package/src/assets/web-panel/assets/{Git-I3g_bAw9.js → Git-L1PjW-lT.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-jHS5-ioS.js → Governance-PK-X58to.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CPL7sfx9.js → Inference-BePWEH-d.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Jf236h4C.js → KnowledgeGraph-LF_sBvdL.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-Cm2tcSKg.js → Logs-BKkfk9hy.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-CZK82rhi.js → Marketplace-BNMqUDla.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CZCeji7a.js → McpTools-w4WPiyz2.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-3l2KVS9k.js → Memory-DUKMVGNi.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-VYdpoLh6.js → MobileBridge-BWhiEyTJ.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-CiB9cmm1.js → MobileProjects-Ocf5JEkX.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-wCFZbBuL.js → Mtc-D2B6MMhU.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-1a3THIyG.js → MtcAudit-DdYjCq1O.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-DMzLB2hG.js → Multisig-Cb1hfuUZ.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-D9wZbf6l.js → NLProgramming-CTeImGp7.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-hrFe2ZM-.js → Notes-BjYNc7eQ.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-CHGX5Jwm.js → NotificationSettings-Dn_NtRXQ.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-DVYYIizh.js +1 -0
- package/src/assets/web-panel/assets/{Organization-By7FkhtY.js → Organization-Be2Kmr5j.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow--khGSzJn.js → Overflow-Cu_-qRlR.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CAG9dH35.js → P2P-DD2HoGzE.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-B3SQZmK9.js → PdhVaultBrowser-BRI7DVth.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-D35ec6JA.js → Permissions-C9Srcs2M.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-CxBF7hW_.js → PersonalDataHub-CUqcMgo1.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-s6EtOO1s.js → Pipeline-HBrQGLXt.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BbJYmWmO.js → Privacy-D-B0vjwh.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-VGQq1jMT.js → ProjectInit-eTeLIu9e.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-B1ew3Teh.js → ProjectSettings-DrRqCsC6.js} +2 -2
- package/src/assets/web-panel/assets/Projects-1G9DNOhI.js +1 -0
- package/src/assets/web-panel/assets/{Providers-mrO47FIK.js → Providers-RkTZzpxP.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-IJF2mHyw.js → QrScannerModal-BSCUgsgC.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CU0wN_Z0.js → QuickAsk-CHgyM3Bz.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DhN37R6G.js → Recommend-0lry4OZq.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-D_xOX_hu.js → RemoteSession-Cn45UroZ.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-D_Ssv7uH.js → Reputation-Q-Zjb707.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Kx5dKxX7.js → Row-BdM70oda.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BlGwqZkY.js → RssFeed-DmOAKKap.js} +2 -2
- package/src/assets/web-panel/assets/{Search-hLFQzxpb.js → Search-Dwavbm07.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DMlvsVt4.js → Security-DukXFCnk.js} +3 -3
- package/src/assets/web-panel/assets/{Services-Bd0Qsj2m.js → Services-BIlRnITu.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DFclq9X3.js → Skeleton-YYVQdhhQ.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-BNtln2qY.js → Skills-B8_iYHz2.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-Dch5H19G.js → Sla-BdVrhT31.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-2UfQcU1g.js → SpeechSettings-CD3ggRx7.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CD9YQr4i.js → SyncSettings-Bp02VZFH.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BNu-7MzI.js → Tasks-DEVmCEsr.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-D1n9CaIR.js → Templates-B6czWc4N.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DenL3iBW.js → Tenant-BCJLv17r.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-ZzU0Io1Y.js → Terminal-CD132d2Y.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-D87IfukO.js → TimelineRenderer-ZB-CvkZl.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BkiZc8E3.js → Tokens-BnSY0S-s.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BNRSytGN.js → Trigger-VZw9Oy2w.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-Bwbd4X2m.js → Trust-pZq7Hjd2.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-B8O2bs0o.js → UkeySign-DnQaqk4q.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-C2fL8zmH.js → VideoEditing-BEUfmvJV.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BORYDdE1.js → Wallet-CoZKMXJ2.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-C0V5S2FM.js → WebAuthn-CBOGVx1I.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-2dR0fcHL.js → WorkflowEditor-huSKn7TT.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BmuAFAn8.js → chat-A_3w6FBO.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CN3smMcK.js → colors-D1Bem9Oy.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DCnxb4kW.js → compact-item-CJYJj0oO.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-vlR43pHn.js → createContext-BkbFiKT4.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-WDr3_M_N.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-D04tHYFd.js → hasIn-CKvUafVY.js} +1 -1
- package/src/assets/web-panel/assets/{index-0DnaZGkC.js → index-AjrSPnvv.js} +1 -1
- package/src/assets/web-panel/assets/{index-BudvKQfG.js → index-B8i0lViZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BeGDEXFs.js → index-B9Svn0zc.js} +1 -1
- package/src/assets/web-panel/assets/{index-D5LZVZ0L.js → index-BHdHSX06.js} +1 -1
- package/src/assets/web-panel/assets/{index-BTPEZTk1.js → index-BHdailyo.js} +1 -1
- package/src/assets/web-panel/assets/{index-DIpY0qM5.js → index-BJwlEnYo.js} +1 -1
- package/src/assets/web-panel/assets/{index-CsAsCPJ6.js → index-BOoAQjJz.js} +1 -1
- package/src/assets/web-panel/assets/{index-ljuBiQW9.js → index-BSgWxAOG.js} +1 -1
- package/src/assets/web-panel/assets/{index-hpvvtAZ3.js → index-BdutMsne.js} +1 -1
- package/src/assets/web-panel/assets/{index-_FIkN3jd.js → index-BiDWxzbn.js} +1 -1
- package/src/assets/web-panel/assets/{index-C-6NO5il.js → index-By5a0T_W.js} +1 -1
- package/src/assets/web-panel/assets/{index-B78xL3s2.js → index-C-WIY-FQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-ClhAHDK3.js → index-C37f6iey.js} +1 -1
- package/src/assets/web-panel/assets/{index-ToUh2b1-.js → index-C9bWmTcO.js} +1 -1
- package/src/assets/web-panel/assets/{index-D28DeAAB.js → index-CBaosWRO.js} +1 -1
- package/src/assets/web-panel/assets/{index-D8vwp4qp.js → index-CBlBVJ_m.js} +1 -1
- package/src/assets/web-panel/assets/{index-BqhASEL4.js → index-CD62EEGo.js} +1 -1
- package/src/assets/web-panel/assets/{index-DDwLgQY9.js → index-CUj-l7GM.js} +1 -1
- package/src/assets/web-panel/assets/{index-DbVsQBOH.js → index-CVS8Ymuq.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cr-A0FYJ.js → index-ChkXUj3f.js} +1 -1
- package/src/assets/web-panel/assets/{index-D65_XseV.js → index-Cj0OZ-37.js} +1 -1
- package/src/assets/web-panel/assets/{index-BXRg8GFQ.js → index-Co3OFL0t.js} +1 -1
- package/src/assets/web-panel/assets/{index-430S68MN.js → index-Crp8CbRg.js} +1 -1
- package/src/assets/web-panel/assets/{index-CngCC8hC.js → index-D22zNXiS.js} +1 -1
- package/src/assets/web-panel/assets/{index-HhNjnCfe.js → index-DTs_D1OL.js} +1 -1
- package/src/assets/web-panel/assets/{index-CP9m9Ggr.js → index-DfgCdFrN.js} +3 -3
- package/src/assets/web-panel/assets/{index-jzR7Ujuw.js → index-DjYrecNb.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTOCnIQ7.js → index-DpW9cf3e.js} +1 -1
- package/src/assets/web-panel/assets/index-Dwix3p4g.js +1 -0
- package/src/assets/web-panel/assets/{index-D_n8_vfI.js → index-F--HuPG1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BasvsWDM.js → index-Fuk6t_3K.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnXebZLL.js → index-JEAGMii9.js} +1 -1
- package/src/assets/web-panel/assets/{index-DYAtmqiv.js → index-PF-mpv8K.js} +1 -1
- package/src/assets/web-panel/assets/index-SHaoZ0CA.js +1 -0
- package/src/assets/web-panel/assets/{index-DTWg-18U.js → index-b-sbVdjQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5Sxb1sE.js → index-pyvMVX8r.js} +1 -1
- package/src/assets/web-panel/assets/{index-TslMTwNy.js → index-qNTtOVi_.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTtRscUa.js → index-tgO8iza6.js} +1 -1
- package/src/assets/web-panel/assets/{index-DbADHwhr.js → index-ttDQVhZy.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-C3wUb4je.js → initDefaultProps-DtrnfsZH.js} +1 -1
- package/src/assets/web-panel/assets/{motion-D-jYFUNA.js → motion-D4zqSaX3.js} +1 -1
- package/src/assets/web-panel/assets/{move-tqb8nwJ2.js → move-CGFGOUQj.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DSOjMJMz.js → mtc-parser-DH2HvkJl.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CJ7VimIW.js → omit-jJ2at5HX.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-qXiG1iT3.js → pickAttrs-CRyMT1Fv.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-CkGBcy1Z.js → placementArrow-DZ_CX0Pt.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-D3WvTlLO.js → responsiveObserve-wruHk-h5.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BEE2ftge.js → slide-BGgx8q67.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CsfBpxiG.js → statusUtils-Do5M7gKm.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-xPrIiJPI.js → styleChecker-CRa-GZpX.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-C5Yd-SCR.js → useFlexGapSupport-BxPcq6pj.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-D_MDS8HI.js → useFs-v5O2ZcJm.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BqJh-usA.js → usePersonalDataHub--z2FU0Px.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-chm_p-gz.js → vnode-1emidVKd.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-3-YDNz0Z.js → zoom-CcJaRyHs.js} +1 -1
- package/src/assets/web-panel/index.html +1 -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/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/gateways/ws/message-dispatcher.js +5 -2
- package/src/gateways/ws/remote-session-protocol.js +103 -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/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/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/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/install.js +9 -1
- package/src/lib/plugin-runtime/policy.js +9 -1
- package/src/lib/plugin-runtime/remote-source.js +240 -0
- package/src/lib/pqc-manager.js +64 -0
- package/src/lib/reputation-optimizer.js +101 -0
- package/src/lib/sandbox-egress-proxy.js +159 -59
- package/src/lib/sandbox-fs-policy.js +112 -0
- package/src/lib/sandbox-network-policy.js +18 -1
- 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/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/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,60 @@
|
|
|
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
|
+
}
|
|
21
74
|
|
|
22
75
|
/**
|
|
23
76
|
* Build the proxy env vars a child process needs to route through the proxy.
|
|
@@ -64,77 +117,124 @@ export function createEgressProxy(policy = {}, opts = {}) {
|
|
|
64
117
|
return verdict;
|
|
65
118
|
};
|
|
66
119
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
120
|
+
const lookup = opts.lookup || defaultLookup;
|
|
121
|
+
const rebindGuard = opts.checkDnsRebinding !== false; // on by default
|
|
122
|
+
|
|
123
|
+
// Second gate: after a NAME-based allow, re-check the RESOLVED address for
|
|
124
|
+
// DNS rebinding (unless the allow was `specific` — a name the user vetted —
|
|
125
|
+
// or the guard is disabled). Returns the verdict to ACT on plus `ip`, the
|
|
126
|
+
// exact address to pin the connection to (null → connect by the original
|
|
127
|
+
// name). When the resolved IP is private the name-based allow is overturned:
|
|
128
|
+
// fix the counters and re-notify observers with the block reason.
|
|
129
|
+
const guard = async (verdict, kind) => {
|
|
130
|
+
if (!verdict.allowed || verdict.specific || !rebindGuard) {
|
|
131
|
+
return { verdict, ip: null };
|
|
74
132
|
}
|
|
75
|
-
|
|
133
|
+
const res = await guardResolvedTarget(verdict.host, policy, lookup);
|
|
134
|
+
if (res.ok) return { verdict, ip: res.ip };
|
|
135
|
+
state.allowed -= 1;
|
|
136
|
+
state.blocked += 1;
|
|
137
|
+
const blocked = { allowed: false, host: verdict.host, reason: res.reason };
|
|
138
|
+
if (typeof opts.onDecision === "function") {
|
|
139
|
+
try {
|
|
140
|
+
opts.onDecision({ kind, target: verdict.host, ...blocked });
|
|
141
|
+
} catch {
|
|
142
|
+
/* observation is best-effort */
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { verdict: blocked, ip: null };
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const server = http.createServer(async (req, res) => {
|
|
76
149
|
try {
|
|
77
|
-
|
|
150
|
+
// A forward-proxy request carries the absolute URL in the request line.
|
|
151
|
+
const { verdict, ip } = await guard(decide(req.url, "http"), "http");
|
|
152
|
+
if (!verdict.allowed) {
|
|
153
|
+
res.writeHead(403, { "content-type": "text/plain" });
|
|
154
|
+
res.end(`egress blocked: ${verdict.host} — ${verdict.reason}\n`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
let u;
|
|
158
|
+
try {
|
|
159
|
+
u = new URL(req.url);
|
|
160
|
+
} catch {
|
|
161
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
162
|
+
res.end("bad request target\n");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const upstream = http.request(
|
|
166
|
+
{
|
|
167
|
+
hostname: ip || u.hostname, // pin to the validated IP when re-checked
|
|
168
|
+
port: u.port || 80,
|
|
169
|
+
path: `${u.pathname}${u.search}`,
|
|
170
|
+
method: req.method,
|
|
171
|
+
headers: req.headers, // preserves the original Host header
|
|
172
|
+
},
|
|
173
|
+
(upRes) => {
|
|
174
|
+
res.writeHead(upRes.statusCode || 502, upRes.headers);
|
|
175
|
+
upRes.pipe(res);
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
upstream.on("error", () => {
|
|
179
|
+
if (!res.headersSent)
|
|
180
|
+
res.writeHead(502, { "content-type": "text/plain" });
|
|
181
|
+
res.end("upstream error\n");
|
|
182
|
+
});
|
|
183
|
+
req.pipe(upstream);
|
|
78
184
|
} 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
185
|
if (!res.headersSent)
|
|
98
186
|
res.writeHead(502, { "content-type": "text/plain" });
|
|
99
|
-
res.end("
|
|
100
|
-
}
|
|
101
|
-
req.pipe(upstream);
|
|
187
|
+
res.end("proxy error\n");
|
|
188
|
+
}
|
|
102
189
|
});
|
|
103
190
|
|
|
104
191
|
// 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 {
|
|
192
|
+
server.on("connect", async (req, clientSocket, head) => {
|
|
193
|
+
try {
|
|
194
|
+
const { verdict, ip } = await guard(
|
|
195
|
+
decide(req.url, "connect"),
|
|
196
|
+
"connect",
|
|
197
|
+
); // req.url = "host:port"
|
|
198
|
+
if (!verdict.allowed) {
|
|
199
|
+
clientSocket.write(
|
|
200
|
+
"HTTP/1.1 403 Forbidden\r\n" +
|
|
201
|
+
"Content-Type: text/plain\r\n\r\n" +
|
|
202
|
+
`egress blocked: ${verdict.host} — ${verdict.reason}\n`,
|
|
203
|
+
);
|
|
126
204
|
clientSocket.end();
|
|
127
|
-
|
|
128
|
-
/* already closed */
|
|
205
|
+
return;
|
|
129
206
|
}
|
|
130
|
-
|
|
131
|
-
|
|
207
|
+
const [host, portStr] = req.url.split(":");
|
|
208
|
+
const port = parseInt(portStr, 10) || 443;
|
|
209
|
+
// Connect to the pinned validated IP so a rebind at connect time can't
|
|
210
|
+
// swap in a private address between our check and the socket.
|
|
211
|
+
const upstream = net.connect(port, ip || host, () => {
|
|
212
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
213
|
+
if (head && head.length) upstream.write(head);
|
|
214
|
+
upstream.pipe(clientSocket);
|
|
215
|
+
clientSocket.pipe(upstream);
|
|
216
|
+
});
|
|
217
|
+
upstream.on("error", () => {
|
|
218
|
+
try {
|
|
219
|
+
clientSocket.end();
|
|
220
|
+
} catch {
|
|
221
|
+
/* already closed */
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
clientSocket.on("error", () => {
|
|
225
|
+
try {
|
|
226
|
+
upstream.destroy();
|
|
227
|
+
} catch {
|
|
228
|
+
/* already gone */
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
} catch {
|
|
132
232
|
try {
|
|
133
|
-
|
|
233
|
+
clientSocket.end();
|
|
134
234
|
} catch {
|
|
135
|
-
/* already
|
|
235
|
+
/* already closed */
|
|
136
236
|
}
|
|
137
|
-
}
|
|
237
|
+
}
|
|
138
238
|
});
|
|
139
239
|
|
|
140
240
|
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
|
|
|
@@ -107,7 +114,17 @@ export function evaluateNetworkAccess(target, policy = {}) {
|
|
|
107
114
|
allowed.filter((p) => String(p).trim() !== "*"),
|
|
108
115
|
);
|
|
109
116
|
if (explicitNonStar) {
|
|
110
|
-
|
|
117
|
+
// `specific: true` marks a host the user vetted by exact/subdomain name (not
|
|
118
|
+
// a bare `*`). The egress proxy trusts these even when they resolve to a
|
|
119
|
+
// private IP (intentional internal/dev allowlisting) and so SKIPS the
|
|
120
|
+
// DNS-rebinding re-check for them — whereas `*`/permissive allows still get
|
|
121
|
+
// their resolved IP checked.
|
|
122
|
+
return {
|
|
123
|
+
allowed: true,
|
|
124
|
+
host,
|
|
125
|
+
reason: "matched allowedDomains",
|
|
126
|
+
specific: true,
|
|
127
|
+
};
|
|
111
128
|
}
|
|
112
129
|
// 3) Private / loopback / metadata targets are blocked unless allowed above.
|
|
113
130
|
if (isPrivateHost(host) && !policy.allowPrivate) {
|
package/src/lib/sandbox-v2.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import crypto from "crypto";
|
|
7
7
|
import { safeJsonParse } from "./safe-json.js";
|
|
8
8
|
import { evaluateNetworkAccess } from "./sandbox-network-policy.js";
|
|
9
|
+
import { evaluatePathAccess } from "./sandbox-fs-policy.js";
|
|
9
10
|
import { createRequire } from "module";
|
|
10
11
|
|
|
11
12
|
const _require = createRequire(import.meta.url);
|
|
@@ -142,20 +143,19 @@ export function ensureSandboxTables(db) {
|
|
|
142
143
|
// ─── Permission checking ──────────────────────────────────────
|
|
143
144
|
|
|
144
145
|
function checkFilePermission(permissions, filePath, mode) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
return false;
|
|
146
|
+
// Delegate to the shared path policy: `path.resolve` collapses `..` (traversal),
|
|
147
|
+
// a `path.relative` boundary rejects `/tmpfoo` matching `/tmp`, and symlinks are
|
|
148
|
+
// resolved so a link inside an allowed dir can't escape. The old check was
|
|
149
|
+
// `filePath.startsWith(root)` and let all three through.
|
|
150
|
+
const fsPol = permissions.fileSystem || {};
|
|
151
|
+
return evaluatePathAccess(filePath, {
|
|
152
|
+
mode,
|
|
153
|
+
policy: {
|
|
154
|
+
read: fsPol.read || [],
|
|
155
|
+
write: fsPol.write || [],
|
|
156
|
+
denied: fsPol.denied || [],
|
|
157
|
+
},
|
|
158
|
+
}).allowed;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
function checkNetworkPermission(permissions, host) {
|
package/src/lib/scim-manager.js
CHANGED
|
@@ -211,6 +211,42 @@ export function _resetState() {
|
|
|
211
211
|
_syncLog.length = 0;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Rehydrate the in-memory _users Map from the persisted scim_resources table.
|
|
216
|
+
* The CLI runs each `cc scim` subcommand in a fresh node process, so _users
|
|
217
|
+
* starts empty every invocation. createUser dual-writes (Map + INSERT INTO
|
|
218
|
+
* scim_resources) but listUsers/getUser/deleteUser/getStatus are Map-only and
|
|
219
|
+
* nothing SELECTed the table back — so a provisioned user was invisible next
|
|
220
|
+
* invocation (`No SCIM users.` / `User not found`), and deleteUser threw before
|
|
221
|
+
* its DELETE ran, orphaning rows with no CLI path to remove them. The duplicate
|
|
222
|
+
* check in createUser was also blind, allowing duplicate userNames on disk.
|
|
223
|
+
* Call after ensureSCIMTables(db).
|
|
224
|
+
*
|
|
225
|
+
* Mirrors the write-side shape exactly (active INTEGER→boolean).
|
|
226
|
+
*/
|
|
227
|
+
export function loadFromDb(db) {
|
|
228
|
+
if (!db) return 0;
|
|
229
|
+
ensureSCIMTables(db);
|
|
230
|
+
_users.clear();
|
|
231
|
+
for (const r of db
|
|
232
|
+
.prepare(
|
|
233
|
+
`SELECT id, display_name, user_name, email, active, created_at, updated_at
|
|
234
|
+
FROM scim_resources WHERE resource_type = 'User' ORDER BY created_at ASC`,
|
|
235
|
+
)
|
|
236
|
+
.all()) {
|
|
237
|
+
_users.set(r.id, {
|
|
238
|
+
id: r.id,
|
|
239
|
+
userName: r.user_name,
|
|
240
|
+
displayName: r.display_name || r.user_name,
|
|
241
|
+
email: r.email || null,
|
|
242
|
+
active: !!r.active,
|
|
243
|
+
createdAt: r.created_at,
|
|
244
|
+
updatedAt: r.updated_at,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return _users.size;
|
|
248
|
+
}
|
|
249
|
+
|
|
214
250
|
/* ═══════════════════════════════════════════════════════════════
|
|
215
251
|
* V2 Surface — SCIM provisioning lifecycle layer.
|
|
216
252
|
* Tracks identities and sync-job lifecycle independent of legacy
|
|
@@ -129,10 +129,13 @@ export function getSession(db, sessionId) {
|
|
|
129
129
|
|
|
130
130
|
return {
|
|
131
131
|
...session,
|
|
132
|
-
|
|
132
|
+
// A single corrupt (non-JSON, non-empty) column must not make the whole
|
|
133
|
+
// session unloadable — that would block resume of the user's own data.
|
|
134
|
+
// Matches the safeJsonParse fallback listSessions already uses.
|
|
135
|
+
messages: safeJsonParse(session.messages, []),
|
|
133
136
|
metadata:
|
|
134
137
|
typeof session.metadata === "string"
|
|
135
|
-
?
|
|
138
|
+
? safeJsonParse(session.metadata, {})
|
|
136
139
|
: session.metadata || {},
|
|
137
140
|
};
|
|
138
141
|
}
|