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,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin trust gating (Phase 3.3f) — decide whether a plugin's CODE-BEARING
|
|
3
|
+
* components (hooks that run shell, LSP servers that spawn binaries) are allowed
|
|
4
|
+
* to execute.
|
|
5
|
+
*
|
|
6
|
+
* Threat model: `cc plugin add owner/repo` and, worse, a cloned repo that ships
|
|
7
|
+
* a project-scope plugin under `.chainlesschain/plugins/`, could run arbitrary
|
|
8
|
+
* commands the moment the agent starts. So:
|
|
9
|
+
*
|
|
10
|
+
* - user / local scope → TRUSTED. The developer installed it on their own
|
|
11
|
+
* machine (same consent model as an npm dependency's lifecycle scripts).
|
|
12
|
+
* - project scope → UNTRUSTED until the user explicitly trusts it, since
|
|
13
|
+
* it can arrive with a git clone. Trust is pinned to the exact version, so a
|
|
14
|
+
* later version bump re-requires consent.
|
|
15
|
+
*
|
|
16
|
+
* Trust is recorded in the USER data dir (never in the repo), keyed by
|
|
17
|
+
* `<scope>:<name>` → { version, trustedAt }.
|
|
18
|
+
*
|
|
19
|
+
* Only code execution is gated here. Declarative components (skills are prompts,
|
|
20
|
+
* mcp/settings are config) load regardless; this is specifically about not
|
|
21
|
+
* running untrusted SHELL/binaries behind the user's back.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fs from "fs";
|
|
25
|
+
import path from "path";
|
|
26
|
+
import { getElectronUserDataDir } from "../paths.js";
|
|
27
|
+
|
|
28
|
+
export const _deps = {
|
|
29
|
+
existsSync: fs.existsSync,
|
|
30
|
+
readFileSync: fs.readFileSync,
|
|
31
|
+
writeFileSync: fs.writeFileSync,
|
|
32
|
+
mkdirSync: fs.mkdirSync,
|
|
33
|
+
now: () => new Date().toISOString(),
|
|
34
|
+
// Path of the trust store; injectable so unit tests never touch the real
|
|
35
|
+
// user-data dir.
|
|
36
|
+
storePath: () => path.join(getElectronUserDataDir(), "plugin-trust.json"),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const AUTO_TRUSTED_SCOPES = new Set(["user", "local"]);
|
|
40
|
+
|
|
41
|
+
export function loadTrustStore() {
|
|
42
|
+
try {
|
|
43
|
+
const p = _deps.storePath();
|
|
44
|
+
if (!_deps.existsSync(p)) return {};
|
|
45
|
+
const data = JSON.parse(_deps.readFileSync(p, "utf8"));
|
|
46
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
47
|
+
} catch {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function saveTrustStore(store) {
|
|
53
|
+
const p = _deps.storePath();
|
|
54
|
+
_deps.mkdirSync(path.dirname(p), { recursive: true });
|
|
55
|
+
_deps.writeFileSync(p, JSON.stringify(store, null, 2), "utf8");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function trustKey(scope, name) {
|
|
59
|
+
return `${scope}:${name}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* May this plugin's code-bearing components run?
|
|
64
|
+
* @param {{ scope, name, version }} plugin
|
|
65
|
+
*/
|
|
66
|
+
export function isPluginTrusted(plugin) {
|
|
67
|
+
if (!plugin || !plugin.name) return false;
|
|
68
|
+
if (AUTO_TRUSTED_SCOPES.has(plugin.scope)) return true;
|
|
69
|
+
const entry = loadTrustStore()[trustKey(plugin.scope, plugin.name)];
|
|
70
|
+
return Boolean(entry && entry.version === plugin.version);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Trust a plugin (records the exact version). */
|
|
74
|
+
export function trustPlugin(name, { scope = "project", version } = {}) {
|
|
75
|
+
if (!version) throw new Error("trustPlugin requires a version");
|
|
76
|
+
const store = loadTrustStore();
|
|
77
|
+
store[trustKey(scope, name)] = { version, trustedAt: _deps.now() };
|
|
78
|
+
saveTrustStore(store);
|
|
79
|
+
return { name, scope, version };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Revoke trust for a plugin at a scope. */
|
|
83
|
+
export function untrustPlugin(name, { scope = "project" } = {}) {
|
|
84
|
+
const store = loadTrustStore();
|
|
85
|
+
const key = trustKey(scope, name);
|
|
86
|
+
const existed = Object.prototype.hasOwnProperty.call(store, key);
|
|
87
|
+
delete store[key];
|
|
88
|
+
saveTrustStore(store);
|
|
89
|
+
return { name, scope, removed: existed };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** All trust entries (for `cc plugin trust --list`). */
|
|
93
|
+
export function listTrust() {
|
|
94
|
+
return Object.entries(loadTrustStore()).map(([key, v]) => {
|
|
95
|
+
const idx = key.indexOf(":");
|
|
96
|
+
return {
|
|
97
|
+
scope: key.slice(0, idx),
|
|
98
|
+
name: key.slice(idx + 1),
|
|
99
|
+
version: v?.version || null,
|
|
100
|
+
trustedAt: v?.trustedAt || null,
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Split discovered plugins into those whose code may run and those gated out.
|
|
107
|
+
* @returns {{ trusted: any[], skipped: any[] }}
|
|
108
|
+
*/
|
|
109
|
+
export function partitionByTrust(plugins) {
|
|
110
|
+
const trusted = [];
|
|
111
|
+
const skipped = [];
|
|
112
|
+
for (const p of plugins || []) {
|
|
113
|
+
(isPluginTrusted(p) ? trusted : skipped).push(p);
|
|
114
|
+
}
|
|
115
|
+
return { trusted, skipped };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// One-time stderr notice so a user isn't mystified when a project plugin's
|
|
119
|
+
// hooks/servers silently don't run. Guarded so it prints at most once per
|
|
120
|
+
// (component-kind) per process.
|
|
121
|
+
const _warned = new Set();
|
|
122
|
+
export function warnUntrustedOnce(names, kind) {
|
|
123
|
+
if (!names || names.length === 0) return;
|
|
124
|
+
if (_warned.has(kind)) return;
|
|
125
|
+
_warned.add(kind);
|
|
126
|
+
const list = [...new Set(names)].join(", ");
|
|
127
|
+
try {
|
|
128
|
+
process.stderr.write(
|
|
129
|
+
`[plugins] skipped ${kind} from untrusted project plugin(s): ${list}\n` +
|
|
130
|
+
` run \`cc plugin trust <name>\` to enable them.\n`,
|
|
131
|
+
);
|
|
132
|
+
} catch {
|
|
133
|
+
/* stderr notice is best-effort */
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Test hook: reset the one-time warning guard. */
|
|
138
|
+
export function _resetTrustWarnings() {
|
|
139
|
+
_warned.clear();
|
|
140
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox egress-filtering proxy (Phase 1 enforcement).
|
|
3
|
+
*
|
|
4
|
+
* The domain policy in `sandbox-network-policy.js` DECIDES whether a host is
|
|
5
|
+
* reachable; this turns that decision into ENFORCEMENT against real
|
|
6
|
+
* subprocesses. A local forward proxy consults `evaluateNetworkAccess` for
|
|
7
|
+
* every request (plain HTTP) and CONNECT tunnel (HTTPS) and refuses the blocked
|
|
8
|
+
* ones. Point a sandboxed process's `HTTP_PROXY`/`HTTPS_PROXY` at it and its
|
|
9
|
+
* `curl`/`npm`/`pip`/`git`/`wget`/`fetch` traffic is filtered.
|
|
10
|
+
*
|
|
11
|
+
* This is a proxy-level control, not a kernel one: a tool that deliberately
|
|
12
|
+
* ignores proxy env can still open a raw socket, so a hard guarantee also needs
|
|
13
|
+
* OS network isolation (Docker `--network`, bwrap netns). But it is fully
|
|
14
|
+
* cross-platform (works on Windows, no root) and closes the common egress path
|
|
15
|
+
* that the domain allow/deny list is actually about.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import http from "node:http";
|
|
19
|
+
import net from "node:net";
|
|
20
|
+
import { evaluateNetworkAccess } from "./sandbox-network-policy.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build the proxy env vars a child process needs to route through the proxy.
|
|
24
|
+
* `NO_PROXY` is intentionally empty so nothing bypasses the filter.
|
|
25
|
+
* @param {number} port
|
|
26
|
+
* @param {string} host address the CHILD uses to reach the proxy (for a Docker
|
|
27
|
+
* container this is `host.docker.internal`, not localhost)
|
|
28
|
+
*/
|
|
29
|
+
export function proxyEnv(port, host = "127.0.0.1") {
|
|
30
|
+
const url = `http://${host}:${port}`;
|
|
31
|
+
return {
|
|
32
|
+
HTTP_PROXY: url,
|
|
33
|
+
HTTPS_PROXY: url,
|
|
34
|
+
ALL_PROXY: url,
|
|
35
|
+
http_proxy: url,
|
|
36
|
+
https_proxy: url,
|
|
37
|
+
all_proxy: url,
|
|
38
|
+
NO_PROXY: "",
|
|
39
|
+
no_proxy: "",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create (but do not yet listen on) an egress-filtering proxy for a policy.
|
|
45
|
+
* @param {object} policy { allowedDomains?, deniedDomains?, allowPrivate? }
|
|
46
|
+
* @param {object} [opts] { onDecision?(info), bindHost? }
|
|
47
|
+
* @returns {{ listen():Promise<{port,env,server}>, close():Promise<void>,
|
|
48
|
+
* server:import('http').Server, blocked:number, allowed:number }}
|
|
49
|
+
*/
|
|
50
|
+
export function createEgressProxy(policy = {}, opts = {}) {
|
|
51
|
+
const bindHost = opts.bindHost || "127.0.0.1";
|
|
52
|
+
const state = { blocked: 0, allowed: 0 };
|
|
53
|
+
const decide = (target, kind) => {
|
|
54
|
+
const verdict = evaluateNetworkAccess(target, policy);
|
|
55
|
+
if (verdict.allowed) state.allowed += 1;
|
|
56
|
+
else state.blocked += 1;
|
|
57
|
+
if (typeof opts.onDecision === "function") {
|
|
58
|
+
try {
|
|
59
|
+
opts.onDecision({ kind, target, ...verdict });
|
|
60
|
+
} catch {
|
|
61
|
+
/* observation is best-effort */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return verdict;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const server = http.createServer((req, res) => {
|
|
68
|
+
// A forward-proxy request carries the absolute URL in the request line.
|
|
69
|
+
const verdict = decide(req.url, "http");
|
|
70
|
+
if (!verdict.allowed) {
|
|
71
|
+
res.writeHead(403, { "content-type": "text/plain" });
|
|
72
|
+
res.end(`egress blocked: ${verdict.host} — ${verdict.reason}\n`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
let u;
|
|
76
|
+
try {
|
|
77
|
+
u = new URL(req.url);
|
|
78
|
+
} 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
|
+
if (!res.headersSent)
|
|
98
|
+
res.writeHead(502, { "content-type": "text/plain" });
|
|
99
|
+
res.end("upstream error\n");
|
|
100
|
+
});
|
|
101
|
+
req.pipe(upstream);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// HTTPS (and any TLS) goes through CONNECT — tunnel only allowed hosts.
|
|
105
|
+
server.on("connect", (req, clientSocket, head) => {
|
|
106
|
+
const verdict = decide(req.url, "connect"); // req.url = "host:port"
|
|
107
|
+
if (!verdict.allowed) {
|
|
108
|
+
clientSocket.write(
|
|
109
|
+
"HTTP/1.1 403 Forbidden\r\n" +
|
|
110
|
+
"Content-Type: text/plain\r\n\r\n" +
|
|
111
|
+
`egress blocked: ${verdict.host} — ${verdict.reason}\n`,
|
|
112
|
+
);
|
|
113
|
+
clientSocket.end();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const [host, portStr] = req.url.split(":");
|
|
117
|
+
const port = parseInt(portStr, 10) || 443;
|
|
118
|
+
const upstream = net.connect(port, host, () => {
|
|
119
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
120
|
+
if (head && head.length) upstream.write(head);
|
|
121
|
+
upstream.pipe(clientSocket);
|
|
122
|
+
clientSocket.pipe(upstream);
|
|
123
|
+
});
|
|
124
|
+
upstream.on("error", () => {
|
|
125
|
+
try {
|
|
126
|
+
clientSocket.end();
|
|
127
|
+
} catch {
|
|
128
|
+
/* already closed */
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
clientSocket.on("error", () => {
|
|
132
|
+
try {
|
|
133
|
+
upstream.destroy();
|
|
134
|
+
} catch {
|
|
135
|
+
/* already gone */
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
server,
|
|
142
|
+
get blocked() {
|
|
143
|
+
return state.blocked;
|
|
144
|
+
},
|
|
145
|
+
get allowed() {
|
|
146
|
+
return state.allowed;
|
|
147
|
+
},
|
|
148
|
+
listen() {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
server.once("error", reject);
|
|
151
|
+
server.listen(0, bindHost, () => {
|
|
152
|
+
server.removeListener("error", reject);
|
|
153
|
+
const port = server.address().port;
|
|
154
|
+
resolve({ port, env: proxyEnv(port), server });
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
close() {
|
|
159
|
+
return new Promise((resolve) => server.close(() => resolve()));
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox network domain policy (Phase 1) — decide whether a Shell subprocess is
|
|
3
|
+
* allowed to reach a given URL/host under `sandbox.network.allowedDomains` /
|
|
4
|
+
* `deniedDomains`. OS-level ENFORCEMENT is platform-specific (a proxy / seatbelt
|
|
5
|
+
* / bwrap egress filter), but the POLICY decision is pure, shared, and must get
|
|
6
|
+
* the tricky cases right — so it lives here, fully unit-tested:
|
|
7
|
+
*
|
|
8
|
+
* - wildcard subdomains (`*.example.com` matches `api.example.com` AND the
|
|
9
|
+
* apex `example.com`), exact hosts, and `*` (all);
|
|
10
|
+
* - deny takes precedence over allow;
|
|
11
|
+
* - default-DENY when an allowlist is set and the host isn't on it
|
|
12
|
+
* (Phase 1 acceptance "未授权域名无法通过 curl/npm/pip 访问");
|
|
13
|
+
* - private / loopback / link-local / metadata targets are blocked unless
|
|
14
|
+
* explicitly allowed — an SSRF / DNS-rebinding guard, and it fixes the
|
|
15
|
+
* `new URL()` IPv6-bracket gotcha where `[::1]` leaks brackets into the
|
|
16
|
+
* hostname and defeats a naive string check.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Extract a normalized lowercase host from a URL or bare host:port. */
|
|
20
|
+
export function extractHost(target) {
|
|
21
|
+
if (target == null) return null;
|
|
22
|
+
let s = String(target).trim();
|
|
23
|
+
if (!s) return null;
|
|
24
|
+
// Try URL parse first (covers scheme://host:port/path, userinfo, etc.).
|
|
25
|
+
try {
|
|
26
|
+
// Add a scheme if it looks scheme-less so URL() can parse host:port.
|
|
27
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(s) ? s : `http://${s}`;
|
|
28
|
+
const u = new URL(withScheme);
|
|
29
|
+
s = u.hostname; // may be "[::1]" for IPv6 — normalize below
|
|
30
|
+
} catch {
|
|
31
|
+
// Not a URL — treat as a bare host, strip any :port.
|
|
32
|
+
s = s.replace(/:\d+$/, "");
|
|
33
|
+
}
|
|
34
|
+
// Strip IPv6 brackets ("[::1]" → "::1") — the documented SSRF gotcha.
|
|
35
|
+
if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
|
|
36
|
+
return s.toLowerCase() || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Does `host` match a domain `pattern` (`*`, `*.example.com`, or exact)? */
|
|
40
|
+
export function matchesDomain(host, pattern) {
|
|
41
|
+
if (!host || !pattern) return false;
|
|
42
|
+
const p = String(pattern).trim().toLowerCase();
|
|
43
|
+
if (p === "*") return true;
|
|
44
|
+
if (p.startsWith("*.")) {
|
|
45
|
+
const base = p.slice(2);
|
|
46
|
+
// `*.example.com` matches subdomains AND the apex `example.com`.
|
|
47
|
+
return host === base || host.endsWith(`.${base}`);
|
|
48
|
+
}
|
|
49
|
+
return host === p;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function anyMatch(host, patterns) {
|
|
53
|
+
return (patterns || []).some((p) => matchesDomain(host, p));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Private / loopback / link-local / cloud-metadata target? (SSRF guard) */
|
|
57
|
+
export function isPrivateHost(host) {
|
|
58
|
+
if (!host) return false;
|
|
59
|
+
const h = host.toLowerCase();
|
|
60
|
+
if (h === "localhost" || h.endsWith(".localhost")) return true;
|
|
61
|
+
// Cloud metadata endpoint.
|
|
62
|
+
if (h === "169.254.169.254" || h === "metadata.google.internal") return true;
|
|
63
|
+
// IPv6 loopback / unique-local / link-local.
|
|
64
|
+
if (h === "::1" || h === "::") return true;
|
|
65
|
+
if (/^f[cd][0-9a-f]{2}:/i.test(h)) return true; // fc00::/7 unique-local
|
|
66
|
+
if (/^fe80:/i.test(h)) return true; // link-local
|
|
67
|
+
// IPv4-mapped IPv6 (::ffff:127.0.0.1) — check the tail.
|
|
68
|
+
const mapped = h.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
|
|
69
|
+
const ipv4 = mapped ? mapped[1] : h;
|
|
70
|
+
const m = ipv4.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
71
|
+
if (m) {
|
|
72
|
+
const [a, b] = [Number(m[1]), Number(m[2])];
|
|
73
|
+
if (a === 127) return true; // 127.0.0.0/8 loopback
|
|
74
|
+
if (a === 10) return true; // 10.0.0.0/8
|
|
75
|
+
if (a === 192 && b === 168) return true; // 192.168.0.0/16
|
|
76
|
+
if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12
|
|
77
|
+
if (a === 169 && b === 254) return true; // 169.254.0.0/16 link-local
|
|
78
|
+
if (a === 0) return true; // 0.0.0.0/8
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Evaluate network access for a target under a policy.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} target URL or host (optionally with :port)
|
|
87
|
+
* @param {object} policy { allowedDomains?:string[], deniedDomains?:string[],
|
|
88
|
+
* allowPrivate?:boolean }
|
|
89
|
+
* @returns {{ allowed:boolean, host:string|null, reason:string }}
|
|
90
|
+
*/
|
|
91
|
+
export function evaluateNetworkAccess(target, policy = {}) {
|
|
92
|
+
const allowed = policy.allowedDomains || [];
|
|
93
|
+
const denied = policy.deniedDomains || [];
|
|
94
|
+
const host = extractHost(target);
|
|
95
|
+
if (!host) {
|
|
96
|
+
return { allowed: false, host: null, reason: "unparseable target" };
|
|
97
|
+
}
|
|
98
|
+
// 1) Deny wins over everything.
|
|
99
|
+
if (anyMatch(host, denied)) {
|
|
100
|
+
return { allowed: false, host, reason: "denied by deniedDomains" };
|
|
101
|
+
}
|
|
102
|
+
// 2) A SPECIFIC (non-`*`) allow-list match overrides the private-host guard,
|
|
103
|
+
// so an intentionally-allowed localhost/dev host still works. A bare `*`
|
|
104
|
+
// does NOT — it means "all public domains", never internal/metadata.
|
|
105
|
+
const explicitNonStar = anyMatch(
|
|
106
|
+
host,
|
|
107
|
+
allowed.filter((p) => String(p).trim() !== "*"),
|
|
108
|
+
);
|
|
109
|
+
if (explicitNonStar) {
|
|
110
|
+
return { allowed: true, host, reason: "matched allowedDomains" };
|
|
111
|
+
}
|
|
112
|
+
// 3) Private / loopback / metadata targets are blocked unless allowed above.
|
|
113
|
+
if (isPrivateHost(host) && !policy.allowPrivate) {
|
|
114
|
+
return {
|
|
115
|
+
allowed: false,
|
|
116
|
+
host,
|
|
117
|
+
reason: "private/loopback target blocked (SSRF guard)",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// 4) A `*` (or any remaining) allow-list match → allow.
|
|
121
|
+
if (anyMatch(host, allowed)) {
|
|
122
|
+
return { allowed: true, host, reason: "matched allowedDomains" };
|
|
123
|
+
}
|
|
124
|
+
// 5) With an allow-list present, anything not on it is denied (default-deny).
|
|
125
|
+
if (allowed.length > 0) {
|
|
126
|
+
return {
|
|
127
|
+
allowed: false,
|
|
128
|
+
host,
|
|
129
|
+
reason: "not in allowedDomains (default-deny)",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// 6) No allow-list → permissive (deny-list + private guard still applied).
|
|
133
|
+
return { allowed: true, host, reason: "no allowlist (permissive)" };
|
|
134
|
+
}
|
package/src/lib/sandbox-v2.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import crypto from "crypto";
|
|
7
7
|
import { safeJsonParse } from "./safe-json.js";
|
|
8
|
+
import { evaluateNetworkAccess } from "./sandbox-network-policy.js";
|
|
8
9
|
import { createRequire } from "module";
|
|
9
10
|
|
|
10
11
|
const _require = createRequire(import.meta.url);
|
|
@@ -158,11 +159,16 @@ function checkFilePermission(permissions, filePath, mode) {
|
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
function checkNetworkPermission(permissions, host) {
|
|
162
|
+
// Delegate to the shared domain policy: wildcard subdomains, deny-precedence,
|
|
163
|
+
// default-deny under an allowlist, URL/host normalization, and the SSRF /
|
|
164
|
+
// loopback / metadata guard (incl. the IPv6-bracket gotcha). The old check was
|
|
165
|
+
// exact-string only and let e.g. a URL or `[::1]` slip past.
|
|
161
166
|
const net = permissions.network || {};
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
167
|
+
return evaluateNetworkAccess(host, {
|
|
168
|
+
allowedDomains: net.allowed || [],
|
|
169
|
+
deniedDomains: net.denied || [],
|
|
170
|
+
allowPrivate: net.allowPrivate === true,
|
|
171
|
+
}).allowed;
|
|
166
172
|
}
|
|
167
173
|
|
|
168
174
|
function checkSystemCallPermission(permissions, call) {
|
|
@@ -19,6 +19,24 @@
|
|
|
19
19
|
const { collectHooks } = require("./settings-hooks.cjs");
|
|
20
20
|
const { runHooks } = require("./hook-runner.cjs");
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Split collected hooks into the blocking (sync) set and the fire-and-forget
|
|
24
|
+
* (`async: true`) set. Only NON-decision events should dispatch async hooks —
|
|
25
|
+
* an async hook can't gate a turn it no longer blocks, so a UserPromptSubmit
|
|
26
|
+
* hook that must be able to `block` should stay sync. The caller decides which
|
|
27
|
+
* events are eligible; this is just the partition.
|
|
28
|
+
* @returns {{ sync: Array, async: Array }}
|
|
29
|
+
*/
|
|
30
|
+
function partitionAsyncHooks(hooks) {
|
|
31
|
+
const sync = [];
|
|
32
|
+
const asyncHooks = [];
|
|
33
|
+
for (const h of hooks || []) {
|
|
34
|
+
if (h && h.async === true) asyncHooks.push(h);
|
|
35
|
+
else sync.push(h);
|
|
36
|
+
}
|
|
37
|
+
return { sync, async: asyncHooks };
|
|
38
|
+
}
|
|
39
|
+
|
|
22
40
|
/** Join the context emitted by the hooks that ran (additionalContext / plain stdout). */
|
|
23
41
|
function aggregateContext(results) {
|
|
24
42
|
const parts = [];
|
|
@@ -36,19 +54,32 @@ function aggregateContext(results) {
|
|
|
36
54
|
/**
|
|
37
55
|
* UserPromptSubmit settings hooks. A `block`/`ask` decision aborts the turn;
|
|
38
56
|
* otherwise any emitted context is returned for the caller to inject.
|
|
57
|
+
*
|
|
58
|
+
* `async: true` hooks are EXCLUDED from this blocking run — they can't gate a
|
|
59
|
+
* turn they no longer block, so a fire-and-forget hook is dispatched separately
|
|
60
|
+
* (see dispatchAsyncHooks). This keeps the sync/decision path unchanged while a
|
|
61
|
+
* long-running check runs alongside the turn.
|
|
39
62
|
* @returns {{ blocked:boolean, reason?:string, hook?:string, additionalContext:string|null }}
|
|
40
63
|
*/
|
|
41
|
-
function runUserPromptSubmitHooks(
|
|
64
|
+
function runUserPromptSubmitHooks(
|
|
65
|
+
settingsHooks,
|
|
66
|
+
{ prompt, cwd, sessionId } = {},
|
|
67
|
+
) {
|
|
42
68
|
if (!settingsHooks) return { blocked: false, additionalContext: null };
|
|
43
69
|
const matched = collectHooks(settingsHooks, "UserPromptSubmit", "");
|
|
44
70
|
if (matched.length === 0) return { blocked: false, additionalContext: null };
|
|
71
|
+
const { sync } = partitionAsyncHooks(matched);
|
|
72
|
+
if (sync.length === 0) return { blocked: false, additionalContext: null };
|
|
45
73
|
const payload = {
|
|
46
74
|
hook_event_name: "UserPromptSubmit",
|
|
47
75
|
prompt: String(prompt || ""),
|
|
48
76
|
cwd,
|
|
49
77
|
session_id: sessionId || null,
|
|
50
78
|
};
|
|
51
|
-
const outcome = runHooks(
|
|
79
|
+
const outcome = runHooks(sync, payload, {
|
|
80
|
+
cwd,
|
|
81
|
+
event: "UserPromptSubmit",
|
|
82
|
+
});
|
|
52
83
|
if (outcome.decision === "block" || outcome.decision === "ask") {
|
|
53
84
|
return {
|
|
54
85
|
blocked: true,
|
|
@@ -57,7 +88,36 @@ function runUserPromptSubmitHooks(settingsHooks, { prompt, cwd, sessionId } = {}
|
|
|
57
88
|
additionalContext: null,
|
|
58
89
|
};
|
|
59
90
|
}
|
|
60
|
-
return {
|
|
91
|
+
return {
|
|
92
|
+
blocked: false,
|
|
93
|
+
additionalContext: aggregateContext(outcome.results),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Dispatch the `async: true` hooks for an event fire-and-forget onto the given
|
|
99
|
+
* supervisor (see async-hook-supervisor). Sync hooks are ignored here — they run
|
|
100
|
+
* on the blocking path. No-op (returns []) without a supervisor or async hooks,
|
|
101
|
+
* so callers can wire it unconditionally.
|
|
102
|
+
*
|
|
103
|
+
* @param {object} settingsHooks merged hooks block
|
|
104
|
+
* @param {string} event UserPromptSubmit / PostToolUse / Stop / …
|
|
105
|
+
* @param {object} payload extra fields merged into the hook stdin JSON
|
|
106
|
+
* @param {object} opts { cwd, matchTarget, supervisor }
|
|
107
|
+
* @returns {Array<{command:string, dispatched:boolean, reason?:string}>}
|
|
108
|
+
*/
|
|
109
|
+
function dispatchAsyncHooks(settingsHooks, event, payload = {}, opts = {}) {
|
|
110
|
+
const supervisor = opts.supervisor;
|
|
111
|
+
if (!settingsHooks || !supervisor) return [];
|
|
112
|
+
const matched = collectHooks(settingsHooks, event, opts.matchTarget || "");
|
|
113
|
+
if (matched.length === 0) return [];
|
|
114
|
+
const { async: asyncHooks } = partitionAsyncHooks(matched);
|
|
115
|
+
if (asyncHooks.length === 0) return [];
|
|
116
|
+
return supervisor.dispatch(
|
|
117
|
+
asyncHooks,
|
|
118
|
+
{ hook_event_name: event, cwd: opts.cwd, ...payload },
|
|
119
|
+
{ cwd: opts.cwd },
|
|
120
|
+
);
|
|
61
121
|
}
|
|
62
122
|
|
|
63
123
|
/**
|
|
@@ -82,13 +142,23 @@ function runSessionStartHooks(settingsHooks, { source, cwd, sessionId } = {}) {
|
|
|
82
142
|
/**
|
|
83
143
|
* Generic observe-only fire (SessionEnd / Stop / PreCompact). Returns the raw
|
|
84
144
|
* runHooks outcome so callers can read a block reason; never gates flow here.
|
|
145
|
+
*
|
|
146
|
+
* `async: true` hooks are EXCLUDED from this synchronous run — they are always
|
|
147
|
+
* fire-and-forget and are dispatched separately (dispatchAsyncHooks), so running
|
|
148
|
+
* them here too would double-execute them.
|
|
85
149
|
*/
|
|
86
|
-
function runObserveHooks(
|
|
150
|
+
function runObserveHooks(
|
|
151
|
+
settingsHooks,
|
|
152
|
+
event,
|
|
153
|
+
payload = {},
|
|
154
|
+
{ cwd, matchTarget } = {},
|
|
155
|
+
) {
|
|
87
156
|
if (!settingsHooks) return { decision: "continue", results: [] };
|
|
88
157
|
const matched = collectHooks(settingsHooks, event, matchTarget || "");
|
|
89
|
-
|
|
158
|
+
const { sync } = partitionAsyncHooks(matched);
|
|
159
|
+
if (sync.length === 0) return { decision: "continue", results: [] };
|
|
90
160
|
return runHooks(
|
|
91
|
-
|
|
161
|
+
sync,
|
|
92
162
|
{ hook_event_name: event, cwd, ...payload },
|
|
93
163
|
{ cwd, event },
|
|
94
164
|
);
|
|
@@ -99,4 +169,6 @@ module.exports = {
|
|
|
99
169
|
runSessionStartHooks,
|
|
100
170
|
runObserveHooks,
|
|
101
171
|
aggregateContext,
|
|
172
|
+
partitionAsyncHooks,
|
|
173
|
+
dispatchAsyncHooks,
|
|
102
174
|
};
|
|
@@ -39,9 +39,13 @@ const HOOK_EVENTS = Object.freeze([
|
|
|
39
39
|
"PostToolUse",
|
|
40
40
|
"UserPromptSubmit",
|
|
41
41
|
"Stop",
|
|
42
|
+
"SubagentStart",
|
|
42
43
|
"SubagentStop",
|
|
43
44
|
"SessionStart",
|
|
45
|
+
"SessionResume",
|
|
46
|
+
"SessionPause",
|
|
44
47
|
"SessionEnd",
|
|
48
|
+
"ConfigChange",
|
|
45
49
|
"PreCompact",
|
|
46
50
|
"Notification",
|
|
47
51
|
]);
|
|
@@ -210,18 +214,40 @@ function umbrellaFor(tool) {
|
|
|
210
214
|
* matcher is tested against BOTH the CC umbrella (`Bash`) and the raw tool
|
|
211
215
|
* name (`run_shell`), so settings written either way fire.
|
|
212
216
|
*
|
|
213
|
-
*
|
|
217
|
+
* `async`/`asyncRewake` are carried through (default false) so the caller can
|
|
218
|
+
* split fire-and-forget hooks off the blocking path (see async-hook-supervisor).
|
|
219
|
+
* `event` is stamped on each hook so a supervisor can key runs by (event+cmd).
|
|
220
|
+
*
|
|
221
|
+
* @returns {Array<{command:string, timeout?:number, event:string,
|
|
222
|
+
* async:boolean, asyncRewake:boolean}>}
|
|
214
223
|
*/
|
|
215
224
|
function collectHooks(hooksBlock, event, toolName) {
|
|
216
225
|
const groups = (hooksBlock && hooksBlock[event]) || [];
|
|
217
226
|
const umbrella = umbrellaFor(toolName);
|
|
218
227
|
const raw = String(toolName || "");
|
|
228
|
+
// Claude-Code MCP parity: a bare server-prefix matcher (`mcp__github`) fires
|
|
229
|
+
// for EVERY tool from that server (`mcp__github__create_issue`, …). Our
|
|
230
|
+
// matcher is anchored (`^…$`), so the server prefix alone would never match a
|
|
231
|
+
// full `mcp__server__tool` name without also testing the prefix explicitly.
|
|
232
|
+
// (Users can still target one tool with the full name, or all of a server's
|
|
233
|
+
// tools with a `mcp__server*` wildcard / `/mcp__server__/` regex.)
|
|
234
|
+
let mcpServer = null;
|
|
235
|
+
if (raw.startsWith("mcp__")) {
|
|
236
|
+
const parts = raw.split("__");
|
|
237
|
+
if (parts.length >= 3) mcpServer = `${parts[0]}__${parts[1]}`;
|
|
238
|
+
}
|
|
219
239
|
const out = [];
|
|
220
240
|
for (const g of groups) {
|
|
221
241
|
const fn = compileMatcher(g.matcher);
|
|
222
|
-
if (fn(umbrella) || (raw && fn(raw))) {
|
|
242
|
+
if (fn(umbrella) || (raw && fn(raw)) || (mcpServer && fn(mcpServer))) {
|
|
223
243
|
for (const h of g.hooks)
|
|
224
|
-
out.push({
|
|
244
|
+
out.push({
|
|
245
|
+
command: h.command,
|
|
246
|
+
timeout: h.timeout,
|
|
247
|
+
event,
|
|
248
|
+
async: h.async === true,
|
|
249
|
+
asyncRewake: h.asyncRewake === true,
|
|
250
|
+
});
|
|
225
251
|
}
|
|
226
252
|
}
|
|
227
253
|
return out;
|