chainlesschain 0.162.148 → 0.162.149
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-DwpCCp64.js → AIOps-BJif14yR.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-C9pBYocA.js → ActionButton-CXek6gJY.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BWAkeXxK.js → Analytics-BgQhdAJi.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-CivFGH6B.js → AppLayout-DTYl5NtR.js} +3 -3
- package/src/assets/web-panel/assets/{Audit-Cm8hRpZm.js → Audit-BzATQAeR.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DXdFMsE8.js → Backup-BzdPEQhL.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-uAwSTeql.js → BaseInput-BM0KVh8E.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-KU8eeJJE.js → Chat-D7fHXHkz.js} +4 -4
- package/src/assets/web-panel/assets/ChatBubbleRenderer-Wm34RR-b.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-C6AMDkjd.js → Checkbox-CAiSQ9vO.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-t2moDxcO.js → Codegen-Df40CrEe.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DCcsRRhN.js → Col-DIT2fNFU.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DtB-8SAC.js → Community-CSTIbW2k.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CZm8THYA.js → Compact-DtqXZZUi.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-LiQgC7g1.js → Compliance-CUtzw6o7.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CA8SAxht.js → Cowork-CezmlnmU.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-CtRaF1wD.js → Cron-P4BITarg.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-6-br6Hub.js → Crosschain-BRRiYwvq.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Do2EccrL.js → DID-L5ijB9i4.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-wrXcbzGe.js → Dashboard-CFxro4ob.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-p3FsT245.js → Dropdown-BmAFCQ71.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-D95A2L8q.js → EmailListRenderer-DJBCEuon.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-cLuJW2zo.js → FamilyGuardDashboard-z4oLq6eT.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-Brgq_cbN.js → Federation-DsVCpHMF.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DWLCkNgh.js → FormItemContext-DP3bP5th.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BBqUfQd6.js → GenericCardRenderer-BAXP2Gc6.js} +1 -1
- package/src/assets/web-panel/assets/{Git-8F090w4I.js → Git-I3g_bAw9.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CCogEbxb.js → Governance-jHS5-ioS.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CfLD7v7C.js → Inference-CPL7sfx9.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DDuHJewd.js → KnowledgeGraph-Jf236h4C.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CE8KSEVC.js → Logs-Cm2tcSKg.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-DmwpZmhT.js → Marketplace-CZK82rhi.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-Bf7AazU8.js → McpTools-CZCeji7a.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-D0QU_00S.js → Memory-3l2KVS9k.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-yXS9xdhx.js → MobileBridge-VYdpoLh6.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-BuDUoGUP.js → MobileProjects-CiB9cmm1.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BYyHy28p.js → Mtc-wCFZbBuL.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-CK0aqpiZ.js → MtcAudit-1a3THIyG.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-mNimKYeb.js → Multisig-DMzLB2hG.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-AioAJ0YA.js → NLProgramming-D9wZbf6l.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CVVNCLHE.js → Notes-hrFe2ZM-.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-C4ABj-TK.js → NotificationSettings-CHGX5Jwm.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-CRIFE2Tj.js +1 -0
- package/src/assets/web-panel/assets/{Organization-uozl_gWK.js → Organization-By7FkhtY.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BCZOM2hK.js → Overflow--khGSzJn.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CArcaiaj.js → P2P-CAG9dH35.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Sgq2Srr8.js → PdhVaultBrowser-B3SQZmK9.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-BYrQrXnH.js → Permissions-D35ec6JA.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BzHStAbz.js → PersonalDataHub-CxBF7hW_.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-k7KqxX1M.js → Pipeline-s6EtOO1s.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-IumTUWqx.js → Privacy-BbJYmWmO.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BRyImYTf.js → ProjectInit-VGQq1jMT.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BFIqTBFk.js → ProjectSettings-B1ew3Teh.js} +2 -2
- package/src/assets/web-panel/assets/Projects-CGcNIs_g.js +1 -0
- package/src/assets/web-panel/assets/{Providers-CyyFnUPs.js → Providers-mrO47FIK.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-C82cRx-X.js → QrScannerModal-IJF2mHyw.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DGxDF521.js → QuickAsk-CU0wN_Z0.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Cns-Am1y.js → Recommend-DhN37R6G.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-R7OqAIlg.js → RemoteSession-D_xOX_hu.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-D7mgPIYV.js → Reputation-D_Ssv7uH.js} +1 -1
- package/src/assets/web-panel/assets/{Row-B3lEURyd.js → Row-Kx5dKxX7.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-9_UVrpBt.js → RssFeed-BlGwqZkY.js} +2 -2
- package/src/assets/web-panel/assets/{Search-ClZeO80q.js → Search-hLFQzxpb.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BNNJczEp.js → Security-DMlvsVt4.js} +3 -3
- package/src/assets/web-panel/assets/{Services-C5SjrWUj.js → Services-Bd0Qsj2m.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-Ci_obQ-g.js → Skeleton-DFclq9X3.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DdebN15P.js → Skills-BNtln2qY.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-OinZP2vi.js → Sla-Dch5H19G.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-CxzbNF51.js → SpeechSettings-2UfQcU1g.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-D06N_66b.js → SyncSettings-CD9YQr4i.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BdEdW0AZ.js → Tasks-BNu-7MzI.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-F1ctKmPa.js → Templates-D1n9CaIR.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CVz1Urot.js → Tenant-DenL3iBW.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-ZzU0Io1Y.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-Doitzjmf.js → TimelineRenderer-D87IfukO.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BpyVRpVA.js → Tokens-BkiZc8E3.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-nWhWYTbU.js → Trigger-BNRSytGN.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DK_G-wLx.js → Trust-Bwbd4X2m.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-B5yBUojO.js → UkeySign-B8O2bs0o.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-C5D51qAe.js → VideoEditing-C2fL8zmH.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CLbL9K7v.js → Wallet-BORYDdE1.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DZUelI3V.js → WebAuthn-C0V5S2FM.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-8RPxt4KW.js → WorkflowEditor-2dR0fcHL.js} +1 -1
- package/src/assets/web-panel/assets/{chat-4N4tOA3d.js → chat-BmuAFAn8.js} +1 -1
- package/src/assets/web-panel/assets/{colors-zbbGVrua.js → colors-CN3smMcK.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-D7iMkbnE.js → compact-item-DCnxb4kW.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CBzPJv-w.js → createContext-vlR43pHn.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DxwmsL4j.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-bHdrQSqZ.js → hasIn-D04tHYFd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DbE5D0WL.js → index-0DnaZGkC.js} +1 -1
- package/src/assets/web-panel/assets/{index-308gCGh7.js → index-430S68MN.js} +1 -1
- package/src/assets/web-panel/assets/{index-kmh1TxRa.js → index-B78xL3s2.js} +1 -1
- package/src/assets/web-panel/assets/{index-QFObYeo1.js → index-BTPEZTk1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZb8F8YG.js → index-BXRg8GFQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DE9j5YgT.js → index-BasvsWDM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiG1IZao.js → index-BeGDEXFs.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bqmx24kh.js → index-BqhASEL4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCr0geV9.js → index-BudvKQfG.js} +1 -1
- package/src/assets/web-panel/assets/{index-S74FpAIn.js → index-C-6NO5il.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyoNgbXl.js → index-C5Sxb1sE.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtbbdLnf.js → index-CP9m9Ggr.js} +3 -3
- package/src/assets/web-panel/assets/{index-CkxK86vf.js → index-CTOCnIQ7.js} +1 -1
- package/src/assets/web-panel/assets/index-Cg2ldRfU.js +1 -0
- package/src/assets/web-panel/assets/{index-BTIjjXry.js → index-ClhAHDK3.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7b1hGry.js → index-CnXebZLL.js} +1 -1
- package/src/assets/web-panel/assets/{index-CPxkoKgA.js → index-CngCC8hC.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ci009ijp.js → index-Cr-A0FYJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CdmMoYN1.js → index-CsAsCPJ6.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBNr77E1.js → index-D28DeAAB.js} +1 -1
- package/src/assets/web-panel/assets/{index-B1s0Bz9O.js → index-D5LZVZ0L.js} +1 -1
- package/src/assets/web-panel/assets/{index-oDKNgCsP.js → index-D65_XseV.js} +1 -1
- package/src/assets/web-panel/assets/{index-QNAG4JtR.js → index-D8vwp4qp.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQfu1U78.js → index-DDwLgQY9.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnBOpqv3.js → index-DIpY0qM5.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQ6NcfWz.js → index-DTWg-18U.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4XKpj6c.js → index-DTtRscUa.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1YDh7Wr.js → index-DYAtmqiv.js} +1 -1
- package/src/assets/web-panel/assets/{index-CvRMA9uT.js → index-D_n8_vfI.js} +1 -1
- package/src/assets/web-panel/assets/{index-CahryTX0.js → index-DbADHwhr.js} +1 -1
- package/src/assets/web-panel/assets/{index-BJ4ZGyW0.js → index-DbVsQBOH.js} +1 -1
- package/src/assets/web-panel/assets/index-Dwzo-dtJ.js +1 -0
- package/src/assets/web-panel/assets/{index-xYCm6W2h.js → index-HhNjnCfe.js} +1 -1
- package/src/assets/web-panel/assets/{index-BS4_Mwk6.js → index-ToUh2b1-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DISWAYce.js → index-TslMTwNy.js} +1 -1
- package/src/assets/web-panel/assets/{index-DmqlJed-.js → index-_FIkN3jd.js} +1 -1
- package/src/assets/web-panel/assets/{index-CqdPyWm5.js → index-hpvvtAZ3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BsKehmmS.js → index-jzR7Ujuw.js} +1 -1
- package/src/assets/web-panel/assets/{index-DUyjUkY7.js → index-ljuBiQW9.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BD55yNBt.js → initDefaultProps-C3wUb4je.js} +1 -1
- package/src/assets/web-panel/assets/{motion-B5Bbe77U.js → motion-D-jYFUNA.js} +1 -1
- package/src/assets/web-panel/assets/{move-CTvIqHEH.js → move-tqb8nwJ2.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BkNyPQKB.js → mtc-parser-DSOjMJMz.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BaXJXgHA.js → omit-CJ7VimIW.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-Bw6-YTxH.js → pickAttrs-qXiG1iT3.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BLdbkLqJ.js → placementArrow-CkGBcy1Z.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-IVYkzntU.js → responsiveObserve-D3WvTlLO.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DFMFwwtY.js → slide-BEE2ftge.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CURYRtZ1.js → statusUtils-CsfBpxiG.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Cfz63s1r.js → styleChecker-xPrIiJPI.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-COJL0KE3.js → useFlexGapSupport-C5Yd-SCR.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-YLLojQQf.js → useFs-D_MDS8HI.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BF_21qUC.js → usePersonalDataHub-BqJh-usA.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-9ZpkA7bb.js → vnode-chm_p-gz.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DhtAfhl8.js → zoom-3-YDNz0Z.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/assets/web-panel/remote-session-sw.js +43 -0
- package/src/command-manifest.json +22 -1
- package/src/commands/agent.js +44 -5
- package/src/commands/codeintel.js +252 -0
- package/src/commands/eval.js +248 -0
- package/src/commands/plugin.js +354 -0
- package/src/commands/team.js +456 -0
- package/src/gateways/ws/message-dispatcher.js +45 -2
- package/src/gateways/ws/remote-session-protocol.js +37 -0
- package/src/gateways/ws/ws-server.js +1 -1
- package/src/harness/prompt-compressor.js +27 -1
- package/src/harness/remote-command-ledger.js +158 -0
- package/src/harness/remote-session-push-apns.js +246 -0
- package/src/harness/remote-session-push-errors.js +15 -0
- package/src/harness/remote-session-push-fcm.js +9 -25
- package/src/harness/remote-session-push-huawei.js +174 -0
- package/src/harness/remote-session-push-oppo.js +171 -0
- package/src/harness/remote-session-push-senders.js +42 -0
- package/src/harness/remote-session-push-vivo.js +179 -0
- package/src/harness/remote-session-push-web.js +299 -0
- package/src/harness/remote-session-push-xiaomi.js +99 -0
- package/src/index.js +6 -0
- package/src/lib/agent-core.js +2 -0
- package/src/lib/agent-sandbox.js +161 -5
- package/src/lib/agent-team/task-lease.js +434 -0
- package/src/lib/agent-team/team-budget.js +165 -0
- package/src/lib/agent-team/team-mailbox.js +106 -0
- package/src/lib/agent-team/team-runner.js +266 -0
- package/src/lib/agent-team/team-worktree.js +204 -0
- package/src/lib/agents.js +18 -0
- package/src/lib/async-hook-supervisor.cjs +305 -0
- package/src/lib/eval/runner.js +163 -0
- package/src/lib/eval/tasks.js +456 -0
- package/src/lib/eval/trend.js +185 -0
- package/src/lib/lsp/__tests__/code-intelligence.integration.test.js +129 -0
- package/src/lib/lsp/__tests__/code-intelligence.test.js +228 -0
- package/src/lib/lsp/__tests__/jsonrpc-stream.test.js +104 -0
- package/src/lib/lsp/__tests__/lsp-client.test.js +258 -0
- package/src/lib/lsp/__tests__/lsp-manager.test.js +264 -0
- package/src/lib/lsp/__tests__/lsp-server-registry.test.js +128 -0
- package/src/lib/lsp/code-intelligence.js +356 -0
- package/src/lib/lsp/jsonrpc-stream.js +139 -0
- package/src/lib/lsp/lsp-client.js +318 -0
- package/src/lib/lsp/lsp-manager.js +323 -0
- package/src/lib/lsp/lsp-server-registry.js +196 -0
- package/src/lib/plugin-monitor-supervisor.js +238 -0
- package/src/lib/plugin-runtime/agents.js +51 -0
- package/src/lib/plugin-runtime/bin.js +100 -0
- package/src/lib/plugin-runtime/hooks.js +100 -0
- package/src/lib/plugin-runtime/install.js +388 -0
- package/src/lib/plugin-runtime/lsp.js +75 -0
- package/src/lib/plugin-runtime/manifest.js +458 -0
- package/src/lib/plugin-runtime/mcp.js +89 -0
- package/src/lib/plugin-runtime/monitors.js +100 -0
- package/src/lib/plugin-runtime/policy.js +144 -0
- package/src/lib/plugin-runtime/reload.js +79 -0
- package/src/lib/plugin-runtime/scopes.js +197 -0
- package/src/lib/plugin-runtime/settings.js +119 -0
- package/src/lib/plugin-runtime/signature.js +107 -0
- package/src/lib/plugin-runtime/skills.js +43 -0
- package/src/lib/plugin-runtime/trust.js +140 -0
- package/src/lib/sandbox-egress-proxy.js +162 -0
- package/src/lib/sandbox-network-policy.js +134 -0
- package/src/lib/sandbox-v2.js +10 -4
- package/src/lib/settings-hook-events.cjs +78 -6
- package/src/lib/settings-hooks.cjs +29 -3
- package/src/lib/settings-loader.cjs +35 -1
- package/src/lib/skill-loader.js +18 -0
- package/src/lib/telemetry/span-recorder.js +237 -0
- package/src/repl/agent-repl.js +400 -4
- package/src/runtime/agent-core.js +546 -52
- package/src/runtime/coding-agent-contract-shared.cjs +82 -8
- package/src/runtime/coding-agent-policy.cjs +32 -7
- package/src/runtime/fallback-model.js +134 -7
- package/src/runtime/headless-runner.js +353 -108
- package/src/runtime/mcp-config.js +46 -0
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DCNfbdlx.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-DIp8Xcbd.js +0 -1
- package/src/assets/web-panel/assets/Projects-Dpid9CDg.js +0 -1
- package/src/assets/web-panel/assets/Terminal-BvAK_Zel.js +0 -3
- package/src/assets/web-panel/assets/devWarning-C2Z5LRgq.js +0 -1
- package/src/assets/web-panel/assets/index-BGp6Yr-Y.js +0 -1
- package/src/assets/web-panel/assets/index-BXiw9dQf.js +0 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed org policy for plugin LOADING (Phase 3.3j — fail-closed allow/deny at
|
|
3
|
+
* discovery time, not just at `cc plugin install`).
|
|
4
|
+
*
|
|
5
|
+
* The security primitives already exist in ../plugin-security.js
|
|
6
|
+
* (`enforcePluginPolicy` for name/source allow-deny, `verifyPluginManifest` for
|
|
7
|
+
* SHA-256 + Ed25519 signature). But before this, they were consulted only when a
|
|
8
|
+
* user ran `cc plugin validate`/`install` — a plugin that an org later added to
|
|
9
|
+
* `deniedPlugins`, or one dropped into a scope dir out-of-band, still loaded its
|
|
10
|
+
* skills/agents/hooks/MCP/LSP/monitors at runtime.
|
|
11
|
+
*
|
|
12
|
+
* This wires the SAME `enforcePluginPolicy` into `discoverPlugins`, the single
|
|
13
|
+
* chokepoint every component collector funnels through, so an org's
|
|
14
|
+
* allowedPlugins / deniedPlugins is enforced fail-closed across ALL six
|
|
15
|
+
* component types in one place. When there is no managed settings file the
|
|
16
|
+
* policy is null and nothing is filtered (zero impact on the common case).
|
|
17
|
+
*
|
|
18
|
+
* NOTE: source allow/deny (blockedPluginSources / allowedPluginSources) stays an
|
|
19
|
+
* INSTALL-time gate — an installed plugin doesn't carry its origin on disk, so
|
|
20
|
+
* only the name allow/deny applies at load. Signature enforcement
|
|
21
|
+
* (requireSignedPlugins) is a separate follow-up: it needs the installer to
|
|
22
|
+
* persist a verified-signature marker, else every plugin would fail closed.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { enforcePluginPolicy } from "../plugin-security.js";
|
|
26
|
+
import { loadManagedSettings } from "../settings-loader.cjs";
|
|
27
|
+
import { verifyInstalledSignature } from "./signature.js";
|
|
28
|
+
|
|
29
|
+
// Managed settings are org-admin controlled and effectively static for a
|
|
30
|
+
// session, and discoverPlugins is called by every collector — memoize the load
|
|
31
|
+
// (keyed by resolved file path) so we don't re-read the file on every call.
|
|
32
|
+
let _cache = new Map();
|
|
33
|
+
|
|
34
|
+
export const _deps = { loadManagedSettings };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Load the managed plugin policy (memoized). Returns the managed settings object
|
|
38
|
+
* or null when there is no managed settings file.
|
|
39
|
+
* @param {object} [opts] { env, managedSettingsFile }
|
|
40
|
+
*/
|
|
41
|
+
export function loadManagedPluginPolicy(opts = {}) {
|
|
42
|
+
const env = opts.env || process.env;
|
|
43
|
+
const key = String(
|
|
44
|
+
opts.managedSettingsFile || env.CC_MANAGED_SETTINGS || "<default>",
|
|
45
|
+
);
|
|
46
|
+
if (_cache.has(key)) return _cache.get(key);
|
|
47
|
+
let settings = null;
|
|
48
|
+
try {
|
|
49
|
+
const loaded = _deps.loadManagedSettings({
|
|
50
|
+
env: opts.env,
|
|
51
|
+
managedSettingsFile: opts.managedSettingsFile,
|
|
52
|
+
});
|
|
53
|
+
settings = loaded.settings;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
// Malformed managed settings → fail closed by propagating: a caller that
|
|
56
|
+
// wants org enforcement must not silently proceed with no policy.
|
|
57
|
+
_cache.set(key, { __invalid: err });
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
_cache.set(key, settings);
|
|
61
|
+
return settings;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Clear the memoized policy (tests / after a managed-settings change). */
|
|
65
|
+
export function _resetPolicyCache() {
|
|
66
|
+
_cache = new Map();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Filter a discovered-plugin list by managed name allow/deny. Non-throwing:
|
|
71
|
+
* a plugin the org denies (or omits from a required allowlist) is DROPPED and
|
|
72
|
+
* collected in `dropped`, rather than throwing — so one denied plugin never
|
|
73
|
+
* breaks loading of the rest.
|
|
74
|
+
*
|
|
75
|
+
* @param {Array<{name}>} plugins
|
|
76
|
+
* @param {object|null} managed managed settings (null = no policy → keep all)
|
|
77
|
+
* @returns {{ kept: Array, dropped: Array<{name, reason}> }}
|
|
78
|
+
*/
|
|
79
|
+
export function filterByManagedPolicy(plugins, managed) {
|
|
80
|
+
if (!managed) return { kept: plugins, dropped: [] };
|
|
81
|
+
// requireSignedPlugins (fail-closed): every plugin must carry a valid recorded
|
|
82
|
+
// signature whose locked manifest hash still matches on disk. Optionally the
|
|
83
|
+
// signing key must be among managed.trustedPluginKeySha256.
|
|
84
|
+
const requireSigned =
|
|
85
|
+
managed.requireSignedPlugins === true ||
|
|
86
|
+
managed.requireSignedPlugins === "require";
|
|
87
|
+
const trustedKeys = requireSigned
|
|
88
|
+
? new Set(
|
|
89
|
+
(Array.isArray(managed.trustedPluginKeySha256)
|
|
90
|
+
? managed.trustedPluginKeySha256
|
|
91
|
+
: []
|
|
92
|
+
)
|
|
93
|
+
.map((k) => String(k || "").trim())
|
|
94
|
+
.filter(Boolean),
|
|
95
|
+
)
|
|
96
|
+
: null;
|
|
97
|
+
const kept = [];
|
|
98
|
+
const dropped = [];
|
|
99
|
+
for (const p of plugins) {
|
|
100
|
+
try {
|
|
101
|
+
enforcePluginPolicy(
|
|
102
|
+
{ name: p.name, source: null, action: "load" },
|
|
103
|
+
managed,
|
|
104
|
+
);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
dropped.push({ name: p.name, reason: err.message });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (requireSigned) {
|
|
110
|
+
const v = verifyInstalledSignature(p, { trustedKeys });
|
|
111
|
+
if (!v.signed) {
|
|
112
|
+
dropped.push({
|
|
113
|
+
name: p.name,
|
|
114
|
+
reason: `requireSignedPlugins: ${v.reason}`,
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
kept.push(p);
|
|
120
|
+
}
|
|
121
|
+
return { kept, dropped };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let _warnedDropped = new Set();
|
|
125
|
+
|
|
126
|
+
/** One-time stderr warning naming plugins an org policy blocked from loading. */
|
|
127
|
+
export function warnDroppedOnce(dropped) {
|
|
128
|
+
const fresh = dropped.filter((d) => !_warnedDropped.has(d.name));
|
|
129
|
+
if (fresh.length === 0) return;
|
|
130
|
+
for (const d of fresh) _warnedDropped.add(d.name);
|
|
131
|
+
try {
|
|
132
|
+
process.stderr.write(
|
|
133
|
+
`[plugins] blocked by managed policy: ${fresh
|
|
134
|
+
.map((d) => `${d.name} (${d.reason})`)
|
|
135
|
+
.join(", ")}\n`,
|
|
136
|
+
);
|
|
137
|
+
} catch {
|
|
138
|
+
/* best-effort */
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function _resetPolicyWarnings() {
|
|
143
|
+
_warnedDropped = new Set();
|
|
144
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hot-reload the plugin runtime (Phase 3.3m — `/reload-plugins`).
|
|
3
|
+
*
|
|
4
|
+
* Installing / trusting / upgrading a plugin mid-session normally needs a
|
|
5
|
+
* restart because several collectors memoize their work (LSP registration is
|
|
6
|
+
* registered once per root; the managed-policy load + trust/untrusted warnings
|
|
7
|
+
* are cached). This resets that memoized state and re-runs the declarative
|
|
8
|
+
* collectors so a freshly-installed/trusted plugin's components appear without
|
|
9
|
+
* restarting.
|
|
10
|
+
*
|
|
11
|
+
* Returns a summary of what is now discovered (per-component counts) so the REPL
|
|
12
|
+
* can report it. The caller (agent-repl) additionally re-merges hooks and
|
|
13
|
+
* restarts the monitor supervisor with the fresh set — those hold live session
|
|
14
|
+
* state (the effective hook map, running child processes) the pure collectors
|
|
15
|
+
* can't touch. Already-connected MCP servers stay connected for this session
|
|
16
|
+
* (reconnecting mid-turn is out of scope); newly-added MCP servers are picked up
|
|
17
|
+
* on the next session.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { discoverPlugins } from "./scopes.js";
|
|
21
|
+
import { ensurePluginLspServers, _resetPluginLspLoadState } from "./lsp.js";
|
|
22
|
+
import { _resetPluginServers } from "../lsp/lsp-server-registry.js";
|
|
23
|
+
import { _resetPolicyCache, _resetPolicyWarnings } from "./policy.js";
|
|
24
|
+
import { _resetTrustWarnings } from "./trust.js";
|
|
25
|
+
import { discoverPluginSkillLayers } from "./skills.js";
|
|
26
|
+
import { discoverPluginAgentLayers } from "./agents.js";
|
|
27
|
+
import { collectPluginHooks } from "./hooks.js";
|
|
28
|
+
import { collectPluginMcpServers } from "./mcp.js";
|
|
29
|
+
import { collectPluginMonitors } from "./monitors.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reset memoized plugin-runtime state and re-scan. Pure w.r.t. session state —
|
|
33
|
+
* the REPL layer applies the hooks/monitors side of the reload.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} [opts] { cwd, scopes }
|
|
36
|
+
* @returns {{ plugins, lspRegistered, skills, agents, hooks, mcp, monitors }}
|
|
37
|
+
*/
|
|
38
|
+
export function reloadPluginRuntime(opts = {}) {
|
|
39
|
+
const cwd = opts.cwd || process.cwd();
|
|
40
|
+
const scopes = opts.scopes;
|
|
41
|
+
|
|
42
|
+
// 1) Clear caches so the next discovery re-reads managed policy + re-evaluates
|
|
43
|
+
// trust, and LSP servers can be cleanly re-registered.
|
|
44
|
+
_resetPolicyCache();
|
|
45
|
+
_resetPolicyWarnings();
|
|
46
|
+
_resetTrustWarnings();
|
|
47
|
+
_resetPluginServers();
|
|
48
|
+
_resetPluginLspLoadState();
|
|
49
|
+
|
|
50
|
+
// 2) Re-register plugin LSP servers from scratch (force past the per-root memo).
|
|
51
|
+
let lspRegistered = 0;
|
|
52
|
+
try {
|
|
53
|
+
const res = ensurePluginLspServers({ cwd, scopes, force: true });
|
|
54
|
+
lspRegistered = res?.registered?.length || 0;
|
|
55
|
+
} catch {
|
|
56
|
+
lspRegistered = 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 3) Re-scan the declarative collectors for a fresh summary.
|
|
60
|
+
const count = (fn, pick) => {
|
|
61
|
+
try {
|
|
62
|
+
const r = fn({ cwd, scopes });
|
|
63
|
+
return pick(r);
|
|
64
|
+
} catch {
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const plugins = count(discoverPlugins, (r) => r.length);
|
|
69
|
+
const skills = count(discoverPluginSkillLayers, (r) => r.length);
|
|
70
|
+
const agents = count(discoverPluginAgentLayers, (r) => r.length);
|
|
71
|
+
const hooks = count(collectPluginHooks, (r) => Object.keys(r).length);
|
|
72
|
+
const mcp = count(
|
|
73
|
+
collectPluginMcpServers,
|
|
74
|
+
(r) => Object.keys(r.servers).length,
|
|
75
|
+
);
|
|
76
|
+
const monitors = count(collectPluginMonitors, (r) => r.length);
|
|
77
|
+
|
|
78
|
+
return { plugins, lspRegistered, skills, agents, hooks, mcp, monitors };
|
|
79
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin install scopes + on-disk discovery (Phase 3).
|
|
3
|
+
*
|
|
4
|
+
* A plugin can be installed at three scopes, mirroring the skill-loader layering:
|
|
5
|
+
*
|
|
6
|
+
* user — <userData>/plugins/ global to the machine/user
|
|
7
|
+
* project — <root>/.chainlesschain/plugins/ committed, shared via the repo
|
|
8
|
+
* local — <root>/.chainlesschain/plugins.local/ gitignored, per-developer
|
|
9
|
+
*
|
|
10
|
+
* Precedence on a name collision: local > project > user (the most specific
|
|
11
|
+
* wins, same spirit as the skill layers).
|
|
12
|
+
*
|
|
13
|
+
* Each plugin lives in an IMMUTABLE version directory:
|
|
14
|
+
*
|
|
15
|
+
* <scopeRoot>/<encodedName>/<version>/ (+ the plugin body)
|
|
16
|
+
*
|
|
17
|
+
* so an in-flight session keeps running its version even while another is
|
|
18
|
+
* installed. The active version per (scope,name) is the one named by a
|
|
19
|
+
* `.active` file, or the highest semver present if none is set.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fs from "fs";
|
|
23
|
+
import path from "path";
|
|
24
|
+
import semver from "semver";
|
|
25
|
+
import { getElectronUserDataDir } from "../paths.js";
|
|
26
|
+
import { parsePluginManifest } from "./manifest.js";
|
|
27
|
+
import {
|
|
28
|
+
loadManagedPluginPolicy,
|
|
29
|
+
filterByManagedPolicy,
|
|
30
|
+
warnDroppedOnce,
|
|
31
|
+
} from "./policy.js";
|
|
32
|
+
|
|
33
|
+
export const _deps = {
|
|
34
|
+
existsSync: fs.existsSync,
|
|
35
|
+
readdirSync: fs.readdirSync,
|
|
36
|
+
readFileSync: fs.readFileSync,
|
|
37
|
+
statSync: fs.statSync,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Lowest → highest precedence.
|
|
41
|
+
export const SCOPES = ["user", "project", "local"];
|
|
42
|
+
|
|
43
|
+
/** Filesystem-safe encoding of a (possibly scoped, e.g. @org/name) plugin name. */
|
|
44
|
+
export function encodeName(name) {
|
|
45
|
+
return String(name || "").replace(/[^a-zA-Z0-9._-]/g, "__");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the root directory for a scope.
|
|
50
|
+
* @param {"user"|"project"|"local"} scope
|
|
51
|
+
* @param {object} [opts] { cwd }
|
|
52
|
+
*/
|
|
53
|
+
export function scopeRoot(scope, opts = {}) {
|
|
54
|
+
const cwd = opts.cwd || process.cwd();
|
|
55
|
+
switch (scope) {
|
|
56
|
+
case "user":
|
|
57
|
+
return path.join(getElectronUserDataDir(), "plugins");
|
|
58
|
+
case "project":
|
|
59
|
+
return path.join(cwd, ".chainlesschain", "plugins");
|
|
60
|
+
case "local":
|
|
61
|
+
return path.join(cwd, ".chainlesschain", "plugins.local");
|
|
62
|
+
default:
|
|
63
|
+
throw new Error(`unknown plugin scope: ${scope}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Directory that holds all versions of one plugin at a scope. */
|
|
68
|
+
export function pluginNameDir(scope, name, opts = {}) {
|
|
69
|
+
return path.join(scopeRoot(scope, opts), encodeName(name));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Immutable install dir for a specific plugin version. */
|
|
73
|
+
export function pluginVersionDir(scope, name, version, opts = {}) {
|
|
74
|
+
return path.join(pluginNameDir(scope, name, opts), String(version));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Versions present on disk for a plugin, newest semver first. */
|
|
78
|
+
export function listInstalledVersions(scope, name, opts = {}) {
|
|
79
|
+
const dir = pluginNameDir(scope, name, opts);
|
|
80
|
+
if (!dirExists(dir)) return [];
|
|
81
|
+
return _deps
|
|
82
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
83
|
+
.filter((e) => e.isDirectory())
|
|
84
|
+
.map((e) => e.name)
|
|
85
|
+
.filter((v) => semver.valid(v))
|
|
86
|
+
.sort(semver.rcompare);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The active version for a plugin: `.active` file if valid, else highest semver. */
|
|
90
|
+
export function activeVersion(scope, name, opts = {}) {
|
|
91
|
+
const versions = listInstalledVersions(scope, name, opts);
|
|
92
|
+
if (versions.length === 0) return null;
|
|
93
|
+
const activeFile = path.join(pluginNameDir(scope, name, opts), ".active");
|
|
94
|
+
if (_deps.existsSync(activeFile)) {
|
|
95
|
+
try {
|
|
96
|
+
const pinned = _deps.readFileSync(activeFile, "utf8").trim();
|
|
97
|
+
if (versions.includes(pinned)) return pinned;
|
|
98
|
+
} catch {
|
|
99
|
+
/* fall through to highest */
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return versions[0];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Discover every installed plugin's ACTIVE version across all scopes, applying
|
|
107
|
+
* scope precedence (local > project > user) so a name installed at multiple
|
|
108
|
+
* scopes resolves to one parsed manifest.
|
|
109
|
+
*
|
|
110
|
+
* Managed org policy (allowedPlugins / deniedPlugins) is enforced fail-closed
|
|
111
|
+
* here — the single chokepoint every component collector funnels through — so a
|
|
112
|
+
* denied plugin loads NONE of its six component types. Pass `skipPolicy:true`
|
|
113
|
+
* for tooling that must see even blocked plugins (e.g. `cc plugin installed`
|
|
114
|
+
* showing why something is blocked). No managed settings file → no filtering.
|
|
115
|
+
*
|
|
116
|
+
* @param {object} [opts] { cwd, scopes?, skipPolicy?, env?, managedSettingsFile? }
|
|
117
|
+
* @returns {Array<{scope, name, version, root, manifest}>}
|
|
118
|
+
*/
|
|
119
|
+
export function discoverPlugins(opts = {}) {
|
|
120
|
+
const scopes = opts.scopes || SCOPES;
|
|
121
|
+
const byName = new Map(); // name → record (later scope overrides earlier)
|
|
122
|
+
for (const scope of scopes) {
|
|
123
|
+
const root = scopeRoot(scope, opts);
|
|
124
|
+
if (!dirExists(root)) continue;
|
|
125
|
+
for (const encoded of listDirs(root)) {
|
|
126
|
+
const nameDir = path.join(root, encoded);
|
|
127
|
+
const version = activeVersionForDir(nameDir);
|
|
128
|
+
if (!version) continue;
|
|
129
|
+
const versionDir = path.join(nameDir, version);
|
|
130
|
+
const manifest = parsePluginManifest(versionDir);
|
|
131
|
+
manifest.scope = scope;
|
|
132
|
+
const name = manifest.metadata?.name || encoded;
|
|
133
|
+
byName.set(name, {
|
|
134
|
+
scope,
|
|
135
|
+
name,
|
|
136
|
+
version: manifest.metadata?.version || version,
|
|
137
|
+
root: versionDir,
|
|
138
|
+
manifest,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const all = [...byName.values()];
|
|
143
|
+
if (opts.skipPolicy) return all;
|
|
144
|
+
// Enforce managed org allow/deny at load time (fail-closed). A malformed
|
|
145
|
+
// managed settings file throws out of loadManagedPluginPolicy — that
|
|
146
|
+
// propagates so a broken org policy never silently degrades to "no policy".
|
|
147
|
+
const managed = loadManagedPluginPolicy({
|
|
148
|
+
env: opts.env,
|
|
149
|
+
managedSettingsFile: opts.managedSettingsFile,
|
|
150
|
+
});
|
|
151
|
+
if (!managed) return all;
|
|
152
|
+
const { kept, dropped } = filterByManagedPolicy(all, managed);
|
|
153
|
+
if (dropped.length > 0) warnDroppedOnce(dropped);
|
|
154
|
+
return kept;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── internals ────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
function activeVersionForDir(nameDir) {
|
|
160
|
+
if (!dirExists(nameDir)) return null;
|
|
161
|
+
const versions = _deps
|
|
162
|
+
.readdirSync(nameDir, { withFileTypes: true })
|
|
163
|
+
.filter((e) => e.isDirectory())
|
|
164
|
+
.map((e) => e.name)
|
|
165
|
+
.filter((v) => semver.valid(v))
|
|
166
|
+
.sort(semver.rcompare);
|
|
167
|
+
if (versions.length === 0) return null;
|
|
168
|
+
const activeFile = path.join(nameDir, ".active");
|
|
169
|
+
if (_deps.existsSync(activeFile)) {
|
|
170
|
+
try {
|
|
171
|
+
const pinned = _deps.readFileSync(activeFile, "utf8").trim();
|
|
172
|
+
if (versions.includes(pinned)) return pinned;
|
|
173
|
+
} catch {
|
|
174
|
+
/* fall through */
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return versions[0];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function dirExists(dir) {
|
|
181
|
+
try {
|
|
182
|
+
return _deps.existsSync(dir) && _deps.statSync(dir).isDirectory();
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function listDirs(dir) {
|
|
189
|
+
try {
|
|
190
|
+
return _deps
|
|
191
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
192
|
+
.filter((e) => e.isDirectory())
|
|
193
|
+
.map((e) => e.name);
|
|
194
|
+
} catch {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin `settings` component — a plugin's default configuration (Phase 3.3o).
|
|
3
|
+
*
|
|
4
|
+
* A plugin ships a `settings.json` with defaults. We contribute ONLY the safe,
|
|
5
|
+
* non-security config-override keys — `env` (default environment variables) and
|
|
6
|
+
* `model` (a default model) — as the LOWEST-priority layer: the user's own
|
|
7
|
+
* settings, the system environment, and managed settings ALL override them.
|
|
8
|
+
*
|
|
9
|
+
* SECURITY: a plugin's settings deliberately CANNOT contribute permission rules,
|
|
10
|
+
* hooks, MCP servers, or any security-relevant key — those either have their own
|
|
11
|
+
* dedicated (trust-gated) wiring or must come from the user/managed settings, so
|
|
12
|
+
* a plugin can never widen the permission envelope through `settings.json`. Even
|
|
13
|
+
* so, because `env` can influence tool behavior, collection is trust-gated:
|
|
14
|
+
* only user/local-scope and explicitly-trusted project plugins contribute.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import { discoverPlugins } from "./scopes.js";
|
|
19
|
+
import { partitionByTrust, warnUntrustedOnce } from "./trust.js";
|
|
20
|
+
|
|
21
|
+
export const _deps = { readFileSync: fs.readFileSync };
|
|
22
|
+
|
|
23
|
+
/** Keys a plugin's settings.json is allowed to contribute (safe subset). */
|
|
24
|
+
const SAFE_KEYS = new Set(["env", "model"]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Merge trusted plugins' settings.json into a safe defaults object. A later
|
|
28
|
+
* plugin (in discovery precedence) wins on an env-key / model clash.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} [opts] { cwd, scopes }
|
|
31
|
+
* @returns {{ env: Record<string,string>, model: string|null, sources: string[] }}
|
|
32
|
+
*/
|
|
33
|
+
export function collectPluginSettings(opts = {}) {
|
|
34
|
+
let plugins = [];
|
|
35
|
+
try {
|
|
36
|
+
plugins = discoverPlugins({ cwd: opts.cwd, scopes: opts.scopes });
|
|
37
|
+
} catch {
|
|
38
|
+
return { env: {}, model: null, sources: [] };
|
|
39
|
+
}
|
|
40
|
+
const { trusted, skipped } = partitionByTrust(plugins);
|
|
41
|
+
warnUntrustedOnce(
|
|
42
|
+
skipped.filter((p) => p.manifest?.components?.settings).map((p) => p.name),
|
|
43
|
+
"settings",
|
|
44
|
+
);
|
|
45
|
+
const env = {};
|
|
46
|
+
let model = null;
|
|
47
|
+
const sources = [];
|
|
48
|
+
for (const p of trusted) {
|
|
49
|
+
if (!p.manifest || p.manifest.ok !== true) continue;
|
|
50
|
+
const s = p.manifest.components?.settings;
|
|
51
|
+
if (!s || !s.absPath) continue;
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(_deps.readFileSync(s.absPath, "utf8"));
|
|
55
|
+
} catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
59
|
+
continue;
|
|
60
|
+
let contributed = false;
|
|
61
|
+
// ONLY the safe subset — everything else (permissions/hooks/mcp/…) ignored.
|
|
62
|
+
if (
|
|
63
|
+
SAFE_KEYS.has("env") &&
|
|
64
|
+
parsed.env &&
|
|
65
|
+
typeof parsed.env === "object" &&
|
|
66
|
+
!Array.isArray(parsed.env)
|
|
67
|
+
) {
|
|
68
|
+
for (const [k, v] of Object.entries(parsed.env)) {
|
|
69
|
+
if (typeof v === "string") {
|
|
70
|
+
env[k] = v;
|
|
71
|
+
contributed = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (
|
|
76
|
+
SAFE_KEYS.has("model") &&
|
|
77
|
+
typeof parsed.model === "string" &&
|
|
78
|
+
parsed.model.trim()
|
|
79
|
+
) {
|
|
80
|
+
model = parsed.model.trim();
|
|
81
|
+
contributed = true;
|
|
82
|
+
}
|
|
83
|
+
if (contributed) sources.push(s.absPath);
|
|
84
|
+
}
|
|
85
|
+
return { env, model, sources };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Apply trusted plugins' default env vars to `env` (defaults to process.env) —
|
|
90
|
+
* but ONLY for keys not already set, so the user's / system's environment always
|
|
91
|
+
* wins. Session-scoped: returns `restore()` + the keys added.
|
|
92
|
+
*
|
|
93
|
+
* @param {object} [opts] { cwd, scopes, env }
|
|
94
|
+
* @returns {{ added: string[], model: string|null, restore: () => void }}
|
|
95
|
+
*/
|
|
96
|
+
export function applyPluginSettingsEnv(opts = {}) {
|
|
97
|
+
const env = opts.env || process.env;
|
|
98
|
+
const { env: pluginEnv, model } = collectPluginSettings({
|
|
99
|
+
cwd: opts.cwd,
|
|
100
|
+
scopes: opts.scopes,
|
|
101
|
+
});
|
|
102
|
+
const added = [];
|
|
103
|
+
for (const [k, v] of Object.entries(pluginEnv)) {
|
|
104
|
+
if (env[k] === undefined) {
|
|
105
|
+
env[k] = v;
|
|
106
|
+
added.push(k);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
let restored = false;
|
|
110
|
+
return {
|
|
111
|
+
added,
|
|
112
|
+
model,
|
|
113
|
+
restore: () => {
|
|
114
|
+
if (restored) return;
|
|
115
|
+
restored = true;
|
|
116
|
+
for (const k of added) delete env[k];
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin signature lock (Phase 3.3l — `requireSignedPlugins` load enforcement).
|
|
3
|
+
*
|
|
4
|
+
* `verifyPluginManifest` (../plugin-security.js) verifies a plugin manifest's
|
|
5
|
+
* SHA-256 + detached Ed25519 signature, but only at the moment of `cc plugin
|
|
6
|
+
* add/validate`. To enforce signing at LOAD time (an org's requireSignedPlugins),
|
|
7
|
+
* install records the verification result in a `.plugin-lock.json` inside the
|
|
8
|
+
* immutable version dir; discovery then re-checks it fail-closed.
|
|
9
|
+
*
|
|
10
|
+
* The lock records the manifest's sha256 + the signing key's fingerprint. At
|
|
11
|
+
* load we RE-HASH the on-disk manifest and compare — so a manifest tampered with
|
|
12
|
+
* after install (even inside the version dir) fails the check. The signature
|
|
13
|
+
* itself covers only the manifest file, not every component file — that is a
|
|
14
|
+
* known limitation (documented), an integrity anchor rather than a full
|
|
15
|
+
* supply-chain seal; the immutable version dir + this re-hash catch manifest
|
|
16
|
+
* tampering, which is where the component wiring is declared.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
import fs from "fs";
|
|
21
|
+
import path from "path";
|
|
22
|
+
|
|
23
|
+
const LOCK = ".plugin-lock.json";
|
|
24
|
+
|
|
25
|
+
export const _deps = {
|
|
26
|
+
readFileSync: fs.readFileSync,
|
|
27
|
+
writeFileSync: fs.writeFileSync,
|
|
28
|
+
existsSync: fs.existsSync,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Record a verified signature into the version dir. */
|
|
32
|
+
export function writePluginLock(
|
|
33
|
+
versionDir,
|
|
34
|
+
{ manifestFile, sha256, publicKeySha256, signatureVerified },
|
|
35
|
+
) {
|
|
36
|
+
const rel = path.relative(versionDir, manifestFile).replace(/\\/g, "/");
|
|
37
|
+
const lock = {
|
|
38
|
+
lockVersion: 1,
|
|
39
|
+
manifest: rel || "plugin.json",
|
|
40
|
+
sha256,
|
|
41
|
+
publicKeySha256: publicKeySha256 || null,
|
|
42
|
+
signatureVerified: signatureVerified === true,
|
|
43
|
+
};
|
|
44
|
+
_deps.writeFileSync(
|
|
45
|
+
path.join(versionDir, LOCK),
|
|
46
|
+
JSON.stringify(lock, null, 2),
|
|
47
|
+
"utf8",
|
|
48
|
+
);
|
|
49
|
+
return lock;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Read a version dir's signature lock, or null when absent/unreadable. */
|
|
53
|
+
export function readPluginLock(versionDir) {
|
|
54
|
+
const p = path.join(versionDir, LOCK);
|
|
55
|
+
if (!_deps.existsSync(p)) return null;
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(_deps.readFileSync(p, "utf8"));
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Verify an installed plugin's recorded signature against its on-disk manifest.
|
|
65
|
+
* `signed:true` ONLY when a lock exists, was signature-verified, the manifest
|
|
66
|
+
* still hashes to the locked sha256 (tamper detection), and — when trustedKeys
|
|
67
|
+
* is a non-empty Set — the signing key fingerprint is among them.
|
|
68
|
+
*
|
|
69
|
+
* @param {{root:string}} plugin
|
|
70
|
+
* @param {object} [opts] { trustedKeys?: Set<string> }
|
|
71
|
+
* @returns {{ signed:boolean, reason?:string, publicKeySha256?:string }}
|
|
72
|
+
*/
|
|
73
|
+
export function verifyInstalledSignature(plugin, opts = {}) {
|
|
74
|
+
const lock = readPluginLock(plugin.root);
|
|
75
|
+
if (!lock) return { signed: false, reason: "no signature lock" };
|
|
76
|
+
if (lock.signatureVerified !== true) {
|
|
77
|
+
return { signed: false, reason: "lock is not signature-verified" };
|
|
78
|
+
}
|
|
79
|
+
const manifestFile = path.join(plugin.root, lock.manifest || "plugin.json");
|
|
80
|
+
if (!_deps.existsSync(manifestFile)) {
|
|
81
|
+
return { signed: false, reason: "locked manifest file is missing" };
|
|
82
|
+
}
|
|
83
|
+
let sha;
|
|
84
|
+
try {
|
|
85
|
+
sha = createHash("sha256")
|
|
86
|
+
.update(_deps.readFileSync(manifestFile))
|
|
87
|
+
.digest("hex");
|
|
88
|
+
} catch {
|
|
89
|
+
return { signed: false, reason: "manifest unreadable" };
|
|
90
|
+
}
|
|
91
|
+
if (sha.toLowerCase() !== String(lock.sha256 || "").toLowerCase()) {
|
|
92
|
+
return {
|
|
93
|
+
signed: false,
|
|
94
|
+
reason: "manifest tampered since install (sha256 mismatch)",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const trustedKeys = opts.trustedKeys;
|
|
98
|
+
if (trustedKeys && trustedKeys.size > 0) {
|
|
99
|
+
if (!lock.publicKeySha256 || !trustedKeys.has(lock.publicKeySha256)) {
|
|
100
|
+
return {
|
|
101
|
+
signed: false,
|
|
102
|
+
reason: `signing key not trusted (${lock.publicKeySha256 || "unsigned"})`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { signed: true, publicKeySha256: lock.publicKeySha256 };
|
|
107
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover installed plugins' skill directories so the skill-loader can expose
|
|
3
|
+
* them to the agent (Phase 3.3 — plugin Skill component reaches the agent).
|
|
4
|
+
*
|
|
5
|
+
* A plugin ships skills under `<pluginRoot>/skills/<name>/SKILL.md` (the same
|
|
6
|
+
* `<group>/<skill>/SKILL.md` layout every skill layer uses), so each installed
|
|
7
|
+
* plugin's `skills/` directory becomes an extra skill layer. Only plugins whose
|
|
8
|
+
* manifest fully validated (`manifest.ok`) contribute — a plugin with a
|
|
9
|
+
* traversal/schema error in ANY component loads nothing.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { discoverPlugins } from "./scopes.js";
|
|
15
|
+
|
|
16
|
+
export const _deps = { existsSync: fs.existsSync };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} [opts] { cwd, scopes }
|
|
20
|
+
* @returns {Array<{ name, scope, version, path }>} one entry per plugin that has
|
|
21
|
+
* a `skills/` directory; `path` is that directory (a scan root for the loader).
|
|
22
|
+
*/
|
|
23
|
+
export function discoverPluginSkillLayers(opts = {}) {
|
|
24
|
+
let plugins = [];
|
|
25
|
+
try {
|
|
26
|
+
plugins = discoverPlugins({ cwd: opts.cwd, scopes: opts.scopes });
|
|
27
|
+
} catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const p of plugins) {
|
|
32
|
+
if (!p.manifest || p.manifest.ok !== true) continue;
|
|
33
|
+
const skillsDir = path.join(p.root, "skills");
|
|
34
|
+
if (!_deps.existsSync(skillsDir)) continue;
|
|
35
|
+
out.push({
|
|
36
|
+
name: p.name,
|
|
37
|
+
scope: p.scope,
|
|
38
|
+
version: p.version,
|
|
39
|
+
path: skillsDir,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|