chainlesschain 0.162.181 → 0.162.183
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/scripts/build-windows-sandbox-helper.ps1 +163 -0
- package/src/assets/web-panel/assets/{AIOps-cq0dwbnf.js → AIOps-BapS7lz4.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CvcgGl3w.js → ActionButton-CXYdXEUI.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-hM2-jFqg.js → Analytics-CKR5Lexn.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-B9_Gsd_g.js → AppLayout-DATVUOXB.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-B7bu52Og.js → Artifacts-vb_0ZlV1.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DThfDgqB.js → Audit-BI1l9GWt.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DjlFiD4v.js → BackgroundAgents-BFXbaUEf.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CK9xJKnG.js → Backup-DEYBJJ6H.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DEiSU8K8.js → BaseInput-COdzeO0M.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CpT96czj.js → Chat-oSN6sfM5.js} +6 -6
- package/src/assets/web-panel/assets/ChatBubbleRenderer-CI_jT0ru.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-C1GfDnO0.js → Checkbox-BezEa4tm.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-3DAKgiLx.js → Codegen-CNEL0Ife.js} +1 -1
- package/src/assets/web-panel/assets/{Col-74cOwZU1.js → Col-Bf2Y2Bn1.js} +1 -1
- package/src/assets/web-panel/assets/{Community-tn9Kkz9j.js → Community-BfUlu4Hc.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BcoXH7m4.js → Compact-BN4xWP9e.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DIPGUBgi.js → Compliance-Bt6BKmEP.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-doD5_91c.js → Cowork-DKLp8YxO.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-C-IDvubz.js → Cron-o7Hhw6ky.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CdcCoBdj.js → Crosschain-DaNXWJIw.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DdH5aMMA.js → DID-B-Qa2mha.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DHLA4mm0.js → Dashboard-CMX0Qnxw.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-dEeCt7Md.js → Dropdown-BLuLyRu_.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-BMaecFv9.js → EmailListRenderer-CFBAeMij.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DUaUukTN.js → FamilyGuardDashboard-DWzXQ67V.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CMBE0_rp.js → Federation-DB1QSLgr.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-PG-JDbXq.js → FormItemContext-C-OKCToP.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-Bcb0p88m.js → GenericCardRenderer-DMJFtFZf.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Bgv15myB.js → Git-DsGSujOV.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DB8YTArI.js → Governance-B0iS9NEW.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BQMwCWaS.js → Inference-CH004H7c.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-B1_ZziiB.js → KnowledgeGraph-Q9MTQ83c.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DovEEnt_.js → Logs-B_fyjZK9.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-D201OL4T.js → MarkdownRenderer-C0o4FtpD.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-BlCXSpyB.js → Marketplace-BIStF9d1.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CsCR09Wj.js → McpTools-pGfal768.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-CBVRLKno.js → Memory-xbNqpIqB.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-C9OIafgA.js → MobileBridge-B5EQGrIK.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-VTc3gT1y.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-vE-HdvLj.js → Mtc-a_rmfxJH.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-CPLaekCd.js → MtcAudit-BmKuofUr.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-C_UP9C2o.js → Multisig-BQbJ-CAN.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-2nODJcU1.js → NLProgramming-JtPJKX25.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BhE-SJGu.js → Notes-ZFKznTSG.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-D21q7BNU.js → NotificationSettings-B6aHRBBR.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-0b62GYOD.js → OrderTableRenderer-DdcsOqEz.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-CEqmw_sb.js → Organization-BFDTAUgS.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-MW-gMDO_.js → Overflow-0FoTfbWH.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CY0hjI_G.js → P2P-CWd90etw.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BbWh7Man.js → PdhVaultBrowser-DtCGC-W-.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-ChqEA_4Q.js → Permissions-CvQ7nr7T.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-B3YrbYfD.js → PersonalDataHub-rdNPcR-Z.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-B8WBhhlW.js → Pipeline-CmrfiNOs.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-Cmb0PXNx.js → Privacy-D7ep1h_t.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-hF6zW3Nz.js → ProjectInit-C0xKINHS.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BFSVfpW0.js → ProjectSettings-AOmZFp-F.js} +2 -2
- package/src/assets/web-panel/assets/Projects-BWHSkXZP.js +1 -0
- package/src/assets/web-panel/assets/{Providers-B8yux8Dx.js → Providers-Cb7JCTPL.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-N76cQluO.js → QrScannerModal-BDMehcXC.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-ClvZ0KYR.js → QuickAsk-BF6_1nvx.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-B-GfgBpT.js → Recommend-BHfT-A28.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-B6EEf8hh.js +5 -0
- package/src/assets/web-panel/assets/{Reputation-DJIZzQeG.js → Reputation-CsWHLEcj.js} +1 -1
- package/src/assets/web-panel/assets/{Row-COzxu_bb.js → Row-D6srnWhm.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CoYsJQuE.js → RssFeed-Xhr-KyVd.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DC77Mq9S.js → Search-vIbIJvwL.js} +1 -1
- package/src/assets/web-panel/assets/{Security-Loy2EdMO.js → Security-Bzi5Aiko.js} +4 -4
- package/src/assets/web-panel/assets/{Services-GUD07Eu0.js → Services-BHtVX52P.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DWzkptYU.js → Skeleton-C0R8B2jf.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CEDn1r-Z.js → Skills-DtT6rWOX.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-ub_k4pXh.js → Sla-0B37mxVU.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-D7HLX9CA.js → SpeechSettings-0V2jE1Bs.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DbwOvl6m.js → SyncSettings-CYedRzaj.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-D-R1x8vm.js +1 -0
- package/src/assets/web-panel/assets/{Templates-BNVeIhud.js → Templates-BKoHztGT.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-PS9AzhXU.js → Tenant--17nwVNZ.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-Cgb4eLB_.js → Terminal-Dnkos7vW.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CrmXMOkg.js → TimelineRenderer-C5jx4AE3.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DD-VDHw1.js → Tokens-DYJKYa9t.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BsoN_n-n.js → Trigger-DChpWiQ9.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-n8LmN4ZV.js → Trust-DfWG52EA.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-C9_7MImA.js → UkeySign-CMugYcyA.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DyBteNgn.js → VideoEditing-BOSmrHAd.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BS5ixeXI.js → Wallet-D5g3k3Sa.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-spc4UImU.js → WebAuthn-D-7u4Mkc.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-D7iVvaBO.js → WorkflowEditor-CFzZMSUJ.js} +1 -1
- package/src/assets/web-panel/assets/{chat-6CmBkHrL.js → chat-VbR6l6mc.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BGoCLkxI.js → colors-BSZn8EDS.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-fYZBetMO.js → compact-item-y74obMEq.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BeM-1UVj.js → createContext-C326bjex.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-D3Bho_6u.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-CE7ZuEnV.js → hasIn-D_INJQT_.js} +1 -1
- package/src/assets/web-panel/assets/{index-ukg99D1V.js → index-4PIeTM--.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2U-ObMq.js → index-4eyH-eC2.js} +1 -1
- package/src/assets/web-panel/assets/index-5iAFwYx1.js +1 -0
- package/src/assets/web-panel/assets/{index-CRLjEP1H.js → index-9PIVuXqo.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfPL-m46.js → index-B7SrC-On.js} +1 -1
- package/src/assets/web-panel/assets/{index--lap6cf7.js → index-BJi3UqBt.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3CDgvNW.js → index-BY8-nCgs.js} +1 -1
- package/src/assets/web-panel/assets/index-BfnB7LPx.js +1 -0
- package/src/assets/web-panel/assets/{index-De0quFDw.js → index-Bfw2hf2s.js} +1 -1
- package/src/assets/web-panel/assets/{index-BunaQCa0.js → index-BjcWKg1S.js} +1 -1
- package/src/assets/web-panel/assets/{index-BsMfoaeo.js → index-BnSfnSMP.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0v6YKKO.js → index-Bnua8IX3.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4SJVjs4.js → index-C-ZCaUJc.js} +1 -1
- package/src/assets/web-panel/assets/{index-BNZDILcO.js → index-C2NyllnW.js} +1 -1
- package/src/assets/web-panel/assets/{index-YjcumOZ4.js → index-C327pLbw.js} +1 -1
- package/src/assets/web-panel/assets/{index-CRnUE8Cb.js → index-CMHqs3Jb.js} +1 -1
- package/src/assets/web-panel/assets/{index-CIpVrf5P.js → index-CfLJ42kv.js} +1 -1
- package/src/assets/web-panel/assets/{index-BEVm0lCS.js → index-Cr3uuz0C.js} +3 -3
- package/src/assets/web-panel/assets/{index-Dw1PyYPm.js → index-CsovTpx9.js} +1 -1
- package/src/assets/web-panel/assets/{index-lJBjB1QX.js → index-CzE53HiB.js} +1 -1
- package/src/assets/web-panel/assets/{index-CpIGIB85.js → index-DBkzrpiL.js} +1 -1
- package/src/assets/web-panel/assets/{index-DoXk5KJ8.js → index-DNMxIsDg.js} +1 -1
- package/src/assets/web-panel/assets/{index-DsvfrESp.js → index-DUU2606o.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cip8emOQ.js → index-DdyjrCAu.js} +1 -1
- package/src/assets/web-panel/assets/{index-ATcSr_2O.js → index-DkH1JvEt.js} +1 -1
- package/src/assets/web-panel/assets/{index-BV551878.js → index-Dlxh9D8D.js} +1 -1
- package/src/assets/web-panel/assets/{index-BU_2qiDk.js → index-DuIkfnTF.js} +1 -1
- package/src/assets/web-panel/assets/{index-zb7rVOhV.js → index-DwHSZLXg.js} +1 -1
- package/src/assets/web-panel/assets/{index-MtOuq2i6.js → index-E_v1ZCTP.js} +1 -1
- package/src/assets/web-panel/assets/{index-B8BsdNV_.js → index-FQBsuExX.js} +1 -1
- package/src/assets/web-panel/assets/{index-WUrJ6S1j.js → index-GA80ir_u.js} +1 -1
- package/src/assets/web-panel/assets/{index-RMZ8a7Yt.js → index-IZAWt0H8.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1r9tT3J.js → index-Iw0FfqUz.js} +1 -1
- package/src/assets/web-panel/assets/{index-BowH1eLQ.js → index-KF9Zmqln.js} +1 -1
- package/src/assets/web-panel/assets/{index-Puq6Luf1.js → index-abmn_8Yz.js} +1 -1
- package/src/assets/web-panel/assets/{index-QIpnpZgz.js → index-gINyI1rh.js} +1 -1
- package/src/assets/web-panel/assets/{index-C9Y3k0HI.js → index-jYaogFE8.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4wiTEOM.js → index-nSn2-C4N.js} +1 -1
- package/src/assets/web-panel/assets/{index-DT3zPSVX.js → index-qMA4GYdQ.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-CYHMs1ag.js → initDefaultProps-DI0Zaf8-.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DDB2Ffk5.js → motion-BZDKSy3Z.js} +1 -1
- package/src/assets/web-panel/assets/{move-Bg3cD-dX.js → move-DsIW01Vp.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-B5nedjTS.js → mtc-parser-CohkDhsf.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BHyuUILu.js → omit-DpiN5rzg.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-C9YQnEN9.js → pickAttrs-qbuqni3y.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DBUFYUe7.js → placementArrow-mBky6714.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-nBs2Obnk.js → responsiveObserve-Bas3eQ2K.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DkCEaXNU.js → slide-CBx_SKoK.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-aqE1Wi6U.js → statusUtils-BlYguJSV.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-CN5bhfHB.js → styleChecker-BCYQmRGv.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-B_oX_kvJ.js → useFlexGapSupport-Dp5dybLM.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-rCuXHeme.js → useFs-DHAMBgA2.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-DZR9Abyd.js → usePersonalDataHub-W6jsSw76.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-ConCYQlX.js → vnode-BkI6aayc.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CUdDxG5M.js → zoom-C_IMGueq.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +115 -3
- package/src/commands/agent.js +15 -0
- package/src/commands/background-session.js +49 -0
- package/src/commands/plugin.js +235 -23
- package/src/commands/session.js +13 -0
- package/src/data/changelog.json +26 -0
- package/src/gateways/terminal/PtyManager.js +45 -4
- package/src/gateways/ws/ws-server.js +10 -1
- package/src/harness/background-task-manager.js +70 -2
- package/src/harness/jsonl-session-store.js +6 -4
- package/src/harness/worktree-isolator.js +14 -3
- package/src/lib/agent-core.js +1 -0
- package/src/lib/agent-schedule-store.js +45 -22
- package/src/lib/agent-team/team-runner.js +19 -1
- package/src/lib/background-agent-supervisor.js +49 -0
- package/src/lib/hook-runner.js +6 -2
- package/src/lib/packer/pkg-config-generator.js +3 -1
- package/src/lib/plugin-runtime/bin.js +1148 -70
- package/src/lib/plugin-runtime/install.js +226 -7
- package/src/lib/plugin-runtime/manifest.js +68 -12
- package/src/lib/plugin-runtime/scopes.js +33 -12
- package/src/lib/plugin-usage-attribution.js +44 -0
- package/src/lib/process-execution-broker/index.js +626 -48
- package/src/lib/process-execution-broker/platform-sandbox.js +4971 -266
- package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
- package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
- package/src/lib/process-execution-broker/windows-sandbox.cs +3856 -0
- package/src/lib/repl-bang-memorize.js +67 -10
- package/src/lib/session-usage.js +77 -0
- package/src/lib/slash-commands.js +27 -4
- package/src/repl/agent-repl.js +10 -9
- package/src/runtime/agent-core.js +376 -52
- package/src/runtime/agent-runtime.js +13 -1
- package/src/runtime/headless-runner.js +13 -4
- package/src/runtime/headless-stream.js +20 -1
- package/src/runtime/mcp-config.js +15 -5
- package/src/assets/web-panel/assets/ChatBubbleRenderer-BKPPNmZI.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-D_zoJ8Xs.js +0 -1
- package/src/assets/web-panel/assets/Projects-DAWkVFkc.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-mlglCdQi.js +0 -5
- package/src/assets/web-panel/assets/Tasks-CBQzoAu5.js +0 -1
- package/src/assets/web-panel/assets/devWarning-BzPwHnw8.js +0 -1
- package/src/assets/web-panel/assets/index-DacrAdc-.js +0 -1
- package/src/assets/web-panel/assets/index-EHzNJozo.js +0 -1
- package/src/lib/process-execution-broker/windows-sandbox.ps1 +0 -999
|
@@ -1,32 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* session (Phase 3.3n). A plugin ships helper executables under `bin/`; making
|
|
4
|
-
* them resolvable means the agent's `run_shell` (and monitors/hooks, which
|
|
5
|
-
* inherit the process env) can invoke `some-plugin-tool` by name.
|
|
2
|
+
* Trusted plugin `bin` discovery and direct invocation.
|
|
6
3
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* command or get its binary invoked.
|
|
4
|
+
* Plugin binaries used to be exposed by prepending their directories to the
|
|
5
|
+
* process-wide PATH. That made the eventual target impossible to prove at the
|
|
6
|
+
* Broker boundary: a shell could resolve a different file, a compound command
|
|
7
|
+
* could append an unsandboxed process, and every unrelated descendant inherited
|
|
8
|
+
* the plugin PATH. Policy-bearing bins therefore never enter PATH. `run_shell`
|
|
9
|
+
* resolves their declared alias here, converts one command into literal argv,
|
|
10
|
+
* attests the exact on-disk target, and passes the absolute launch target to
|
|
11
|
+
* ProcessExecutionBroker with `shell:false`. Legacy bins without a policy keep
|
|
12
|
+
* their historical PATH compatibility.
|
|
17
13
|
*/
|
|
18
14
|
|
|
19
15
|
import path from "path";
|
|
20
16
|
import fs from "fs";
|
|
17
|
+
import crypto from "node:crypto";
|
|
21
18
|
import { discoverPlugins } from "./scopes.js";
|
|
22
19
|
import { partitionByTrust, warnUntrustedOnce } from "./trust.js";
|
|
23
20
|
import { componentCapabilityDenial } from "./capabilities.js";
|
|
21
|
+
import { isWithin } from "./manifest.js";
|
|
24
22
|
|
|
25
23
|
// One-time stderr notice when a plugin's bin dir is refused at the COMPONENT
|
|
26
24
|
// level because the plugin opted into the capability model but did not declare
|
|
27
25
|
// the `process` capability its executables need. Distinct from the trust gate:
|
|
28
26
|
// these plugins ARE trusted, but their bin component is denied.
|
|
29
27
|
const _capabilityDenied = new Set();
|
|
28
|
+
const _sandboxPolicyPins = new Map();
|
|
29
|
+
const _issuedPluginBinInvocations = new WeakMap();
|
|
30
|
+
const _issuedPluginSandboxContracts = new WeakMap();
|
|
31
|
+
const PLUGIN_BIN_MAX_BYTES = 64 * 1024 * 1024;
|
|
32
|
+
const PLUGIN_NODE_RUNTIME_MAX_BYTES = 256 * 1024 * 1024;
|
|
33
|
+
const ATTESTATION_HASH_CHUNK_BYTES = 1024 * 1024;
|
|
30
34
|
function warnBinCapabilityDeniedOnce(entries) {
|
|
31
35
|
if (!entries || entries.length === 0) return;
|
|
32
36
|
if (_capabilityDenied.has("bin-capability")) return;
|
|
@@ -48,16 +52,33 @@ export function _resetBinWarnings() {
|
|
|
48
52
|
_capabilityDenied.clear();
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
export function _resetPluginBinSandboxPolicyPins() {
|
|
56
|
+
_sandboxPolicyPins.clear();
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
/**
|
|
52
|
-
* Collect trusted, installed plugins'
|
|
60
|
+
* Collect trusted, installed plugins' declared bin commands. Unlike a PATH
|
|
61
|
+
* directory, each record preserves the manifest alias, exact target, sandbox
|
|
62
|
+
* requirement, and immutable plugin-version provenance.
|
|
63
|
+
*
|
|
53
64
|
* @param {object} [opts] { cwd, scopes }
|
|
54
|
-
* @returns {Array<
|
|
65
|
+
* @returns {Array<object>}
|
|
55
66
|
*/
|
|
56
|
-
export function
|
|
67
|
+
export function collectPluginBinCommands(opts = {}) {
|
|
57
68
|
let plugins = [];
|
|
58
69
|
try {
|
|
59
|
-
plugins = discoverPlugins({
|
|
60
|
-
|
|
70
|
+
plugins = discoverPlugins({
|
|
71
|
+
cwd: opts.cwd,
|
|
72
|
+
scopes: opts.scopes,
|
|
73
|
+
strictIo: opts.failClosed === true,
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (opts.failClosed === true) {
|
|
77
|
+
throw pluginBinError(
|
|
78
|
+
"ERR_PLUGIN_BIN_DISCOVERY_FAILED",
|
|
79
|
+
`plugin bin policy discovery failed: ${error.message}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
61
82
|
return [];
|
|
62
83
|
}
|
|
63
84
|
const { trusted, skipped } = partitionByTrust(plugins);
|
|
@@ -73,23 +94,33 @@ export function collectPluginBinDirs(opts = {}) {
|
|
|
73
94
|
if (!p.manifest || p.manifest.ok !== true) continue;
|
|
74
95
|
const bins = p.manifest.components?.bin;
|
|
75
96
|
if (!Array.isArray(bins) || bins.length === 0) continue;
|
|
76
|
-
// Component-level capability gate: a plugin that declared a permissions
|
|
77
|
-
// block but did not declare the `process` capability its executables need
|
|
78
|
-
// gets its bin dir refused here (mirrors the MCP collector's denial).
|
|
79
97
|
const denial = componentCapabilityDenial(p.manifest, ["process"]);
|
|
80
98
|
if (denial) {
|
|
81
99
|
denied.push({ name: p.name, reason: denial.reason });
|
|
82
100
|
continue;
|
|
83
101
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
102
|
+
for (const bin of bins) {
|
|
103
|
+
if (
|
|
104
|
+
!bin ||
|
|
105
|
+
typeof bin.name !== "string" ||
|
|
106
|
+
typeof bin.absPath !== "string"
|
|
107
|
+
) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
88
110
|
out.push({
|
|
89
111
|
plugin: p.name,
|
|
90
112
|
scope: p.scope,
|
|
91
113
|
version: p.version,
|
|
92
|
-
|
|
114
|
+
root: p.root,
|
|
115
|
+
manifestPath: p.manifest.manifestPath,
|
|
116
|
+
name: bin.name,
|
|
117
|
+
path: bin.path,
|
|
118
|
+
absPath: bin.absPath,
|
|
119
|
+
...(bin.sandboxPolicy
|
|
120
|
+
? { sandboxPolicy: bin.sandboxPolicy }
|
|
121
|
+
: p.manifest.sandboxPolicy
|
|
122
|
+
? { sandboxPolicy: p.manifest.sandboxPolicy }
|
|
123
|
+
: {}),
|
|
93
124
|
});
|
|
94
125
|
}
|
|
95
126
|
}
|
|
@@ -98,18 +129,122 @@ export function collectPluginBinDirs(opts = {}) {
|
|
|
98
129
|
}
|
|
99
130
|
|
|
100
131
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
132
|
+
* Return a tighten-only union of every trusted policy-bearing bin visible from
|
|
133
|
+
* this workspace. The union is intentionally broader than alias attribution:
|
|
134
|
+
* shell indirection (`node <script>`, compound commands, PATH assignment,
|
|
135
|
+
* expansion, wrappers) cannot reliably identify the eventual executable from
|
|
136
|
+
* source text, so every agent shell invocation inherits the union.
|
|
137
|
+
*
|
|
138
|
+
* Pinning by workspace prevents a manifest mutation or trust change from
|
|
139
|
+
* weakening an already observed requirement during the process lifetime.
|
|
140
|
+
*/
|
|
141
|
+
export function collectPluginBinSandboxPolicy(opts = {}) {
|
|
142
|
+
const pinKey = path.resolve(opts.cwd || process.cwd());
|
|
143
|
+
const required = new Set(_sandboxPolicyPins.get(pinKey) || []);
|
|
144
|
+
const commands = collectPluginBinCommands({
|
|
145
|
+
...opts,
|
|
146
|
+
failClosed: true,
|
|
147
|
+
});
|
|
148
|
+
for (const command of commands) {
|
|
149
|
+
for (const boundary of command.sandboxPolicy?.requiredBoundaries || []) {
|
|
150
|
+
required.add(boundary);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return pinPluginBinSandboxPolicy(
|
|
154
|
+
{ requiredBoundaries: [...required] },
|
|
155
|
+
{ cwd: pinKey },
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Collect the tighten-only union for a fixed workspace and the requested
|
|
161
|
+
* execution directory, then pin the result to the fixed workspace key.
|
|
162
|
+
* Callers must not let an execution-directory override replace the workspace
|
|
163
|
+
* root: it may only discover additional boundaries.
|
|
164
|
+
*/
|
|
165
|
+
export function collectWorkspacePluginBinSandboxPolicy(opts = {}) {
|
|
166
|
+
const workspaceCwd = path.resolve(
|
|
167
|
+
opts.workspaceCwd || opts.cwd || process.cwd(),
|
|
168
|
+
);
|
|
169
|
+
const executionCwd = path.resolve(opts.executionCwd || workspaceCwd);
|
|
170
|
+
const required = new Set();
|
|
171
|
+
for (const policyCwd of new Set([workspaceCwd, executionCwd])) {
|
|
172
|
+
const observed = collectPluginBinSandboxPolicy({
|
|
173
|
+
cwd: policyCwd,
|
|
174
|
+
...(opts.scopes !== undefined ? { scopes: opts.scopes } : {}),
|
|
175
|
+
});
|
|
176
|
+
for (const boundary of observed?.requiredBoundaries || []) {
|
|
177
|
+
required.add(boundary);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return pinPluginBinSandboxPolicy(
|
|
181
|
+
{ requiredBoundaries: [...required] },
|
|
182
|
+
{ cwd: workspaceCwd },
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function pinPluginBinSandboxPolicy(policy, opts = {}) {
|
|
187
|
+
const pinKey = path.resolve(opts.cwd || process.cwd());
|
|
188
|
+
const required = new Set(_sandboxPolicyPins.get(pinKey) || []);
|
|
189
|
+
for (const boundary of policy?.requiredBoundaries || []) {
|
|
190
|
+
required.add(boundary);
|
|
191
|
+
}
|
|
192
|
+
if (required.size === 0) return null;
|
|
193
|
+
const pinnedPolicy = Object.freeze({
|
|
194
|
+
requiredBoundaries: Object.freeze([...required].sort()),
|
|
195
|
+
});
|
|
196
|
+
_sandboxPolicyPins.set(pinKey, pinnedPolicy.requiredBoundaries);
|
|
197
|
+
return pinnedPolicy;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Compatibility view used by plugin status surfaces. Directories are never
|
|
202
|
+
* filtered here; `applyPluginBinPath` decides which legacy-only directories
|
|
203
|
+
* remain safe to expose.
|
|
204
|
+
*/
|
|
205
|
+
export function collectPluginBinDirs(opts = {}) {
|
|
206
|
+
const out = [];
|
|
207
|
+
const seen = new Set();
|
|
208
|
+
for (const bin of collectPluginBinCommands(opts)) {
|
|
209
|
+
const dir = path.dirname(bin.absPath);
|
|
210
|
+
const key = `${bin.plugin}\0${dir}`;
|
|
211
|
+
if (seen.has(key)) continue;
|
|
212
|
+
seen.add(key);
|
|
213
|
+
out.push({
|
|
214
|
+
plugin: bin.plugin,
|
|
215
|
+
scope: bin.scope,
|
|
216
|
+
version: bin.version,
|
|
217
|
+
dir,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Preserve the legacy PATH contract only for plugins that did not request a
|
|
225
|
+
* sandbox boundary. A directory containing even one policy-bearing bin is
|
|
226
|
+
* excluded as a unit: PATH exposes directories, so adding it for a legacy
|
|
227
|
+
* sibling would also make the strict sibling shell-resolvable.
|
|
104
228
|
*
|
|
105
229
|
* @param {object} [opts] { cwd, scopes, env }
|
|
106
230
|
* @returns {{ added: string[], restore: () => void }}
|
|
107
231
|
*/
|
|
108
232
|
export function applyPluginBinPath(opts = {}) {
|
|
109
233
|
const env = opts.env || process.env;
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
234
|
+
const byDir = new Map();
|
|
235
|
+
for (const bin of collectPluginBinCommands({
|
|
236
|
+
cwd: opts.cwd,
|
|
237
|
+
scopes: opts.scopes,
|
|
238
|
+
})) {
|
|
239
|
+
const dir = path.dirname(bin.absPath);
|
|
240
|
+
const state = byDir.get(dir) || { legacy: false, strict: false };
|
|
241
|
+
if (bin.sandboxPolicy) state.strict = true;
|
|
242
|
+
else state.legacy = true;
|
|
243
|
+
byDir.set(dir, state);
|
|
244
|
+
}
|
|
245
|
+
const dirs = [...byDir]
|
|
246
|
+
.filter(([, state]) => state.legacy && !state.strict)
|
|
247
|
+
.map(([dir]) => dir);
|
|
113
248
|
const prevPath = env.PATH;
|
|
114
249
|
if (dirs.length === 0) {
|
|
115
250
|
return { added: [], restore: () => {} };
|
|
@@ -119,7 +254,7 @@ export function applyPluginBinPath(opts = {}) {
|
|
|
119
254
|
.split(path.delimiter)
|
|
120
255
|
.filter(Boolean),
|
|
121
256
|
);
|
|
122
|
-
const added = dirs.filter((
|
|
257
|
+
const added = dirs.filter((dir) => !existing.has(dir));
|
|
123
258
|
if (added.length === 0) {
|
|
124
259
|
return { added: [], restore: () => {} };
|
|
125
260
|
}
|
|
@@ -130,53 +265,996 @@ export function applyPluginBinPath(opts = {}) {
|
|
|
130
265
|
restore: () => {
|
|
131
266
|
if (restored) return;
|
|
132
267
|
restored = true;
|
|
133
|
-
// Restore only if nothing else re-touched PATH out from under us; either
|
|
134
|
-
// way, put the recorded previous value back (session-scoped semantics).
|
|
135
268
|
env.PATH = prevPath;
|
|
136
269
|
},
|
|
137
270
|
};
|
|
138
271
|
}
|
|
139
272
|
|
|
273
|
+
function pluginBinError(code, message) {
|
|
274
|
+
const error = new Error(message);
|
|
275
|
+
error.code = code;
|
|
276
|
+
error.pluginBinFailClosed = true;
|
|
277
|
+
return error;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function hashOpenFile(fd, bytes) {
|
|
281
|
+
const digest = crypto.createHash("sha256");
|
|
282
|
+
const chunk = Buffer.allocUnsafe(
|
|
283
|
+
Math.max(1, Math.min(ATTESTATION_HASH_CHUNK_BYTES, bytes)),
|
|
284
|
+
);
|
|
285
|
+
let offset = 0;
|
|
286
|
+
while (offset < bytes) {
|
|
287
|
+
const read = fs.readSync(
|
|
288
|
+
fd,
|
|
289
|
+
chunk,
|
|
290
|
+
0,
|
|
291
|
+
Math.min(chunk.length, bytes - offset),
|
|
292
|
+
offset,
|
|
293
|
+
);
|
|
294
|
+
if (read <= 0) {
|
|
295
|
+
throw new Error("file ended before its attested size");
|
|
296
|
+
}
|
|
297
|
+
digest.update(chunk.subarray(0, read));
|
|
298
|
+
offset += read;
|
|
299
|
+
}
|
|
300
|
+
return digest.digest("hex");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function preciseOpenFileIdentity(stat) {
|
|
304
|
+
const timestamp = (nanosecondsKey, millisecondsKey) =>
|
|
305
|
+
stat[nanosecondsKey] !== undefined
|
|
306
|
+
? String(stat[nanosecondsKey])
|
|
307
|
+
: String(Math.trunc(Number(stat[millisecondsKey] || 0) * 1_000_000));
|
|
308
|
+
return Object.freeze({
|
|
309
|
+
dev: String(stat.dev),
|
|
310
|
+
ino: String(stat.ino),
|
|
311
|
+
size: String(stat.size),
|
|
312
|
+
mode: String(stat.mode),
|
|
313
|
+
nlink: String(stat.nlink),
|
|
314
|
+
birthtimeNs: timestamp("birthtimeNs", "birthtimeMs"),
|
|
315
|
+
ctimeNs: timestamp("ctimeNs", "ctimeMs"),
|
|
316
|
+
mtimeNs: timestamp("mtimeNs", "mtimeMs"),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function samePreciseOpenFileIdentity(left, right) {
|
|
321
|
+
return (
|
|
322
|
+
left.dev === right.dev &&
|
|
323
|
+
left.ino === right.ino &&
|
|
324
|
+
left.size === right.size &&
|
|
325
|
+
left.mode === right.mode &&
|
|
326
|
+
left.nlink === right.nlink &&
|
|
327
|
+
left.birthtimeNs === right.birthtimeNs &&
|
|
328
|
+
left.ctimeNs === right.ctimeNs &&
|
|
329
|
+
left.mtimeNs === right.mtimeNs
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function samePreciseFileObject(left, right) {
|
|
334
|
+
return (
|
|
335
|
+
left.dev === right.dev && left.ino === right.ino && left.size === right.size
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function openFileForAttestation(file) {
|
|
340
|
+
return fs.openSync(
|
|
341
|
+
file,
|
|
342
|
+
Number(fs.constants.O_RDONLY) |
|
|
343
|
+
Number(fs.constants.O_NOFOLLOW || 0) |
|
|
344
|
+
Number(fs.constants.O_NONBLOCK || 0),
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function assertRegularNonLinkPath(file, label) {
|
|
349
|
+
const stat = fs.lstatSync(file);
|
|
350
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
351
|
+
throw new Error(`${label} path is not a regular, non-symlink file`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
140
355
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
356
|
+
* Windows/libuv does not guarantee that file IDs reported by stat(path) and
|
|
357
|
+
* fstat(fd) are comparable on every volume. Bind the canonical path to the
|
|
358
|
+
* already-open primary handle through a second handle instead: fstat-to-fstat
|
|
359
|
+
* retains object identity, while the digest also proves byte identity.
|
|
360
|
+
*/
|
|
361
|
+
function attestWindowsPathHandle(
|
|
362
|
+
file,
|
|
363
|
+
expectedIdentity,
|
|
364
|
+
expectedSha256,
|
|
365
|
+
maxBytes,
|
|
366
|
+
{ label, requireSingleLink = false },
|
|
367
|
+
) {
|
|
368
|
+
let pathFd;
|
|
369
|
+
try {
|
|
370
|
+
assertRegularNonLinkPath(file, label);
|
|
371
|
+
pathFd = openFileForAttestation(file);
|
|
372
|
+
const before = fs.fstatSync(pathFd, { bigint: true });
|
|
373
|
+
if (!before.isFile()) {
|
|
374
|
+
throw new Error(`${label} path handle is not a regular file`);
|
|
375
|
+
}
|
|
376
|
+
if (before.size < 0n || before.size > BigInt(maxBytes)) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
`${label} path handle exceeds the attestation size limit`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
const beforeIdentity = preciseOpenFileIdentity(before);
|
|
382
|
+
if (!samePreciseFileObject(expectedIdentity, beforeIdentity)) {
|
|
383
|
+
throw new Error(`${label} path handle identity changed`);
|
|
384
|
+
}
|
|
385
|
+
if (requireSingleLink && before.nlink !== 1n) {
|
|
386
|
+
throw new Error(`${label} path handle must not be hard-linked`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const sha256 = hashOpenFile(pathFd, Number(before.size));
|
|
390
|
+
const after = fs.fstatSync(pathFd, { bigint: true });
|
|
391
|
+
const afterIdentity = preciseOpenFileIdentity(after);
|
|
392
|
+
if (
|
|
393
|
+
!samePreciseOpenFileIdentity(beforeIdentity, afterIdentity) ||
|
|
394
|
+
!samePreciseFileObject(expectedIdentity, afterIdentity)
|
|
395
|
+
) {
|
|
396
|
+
throw new Error(`${label} path handle changed during attestation`);
|
|
397
|
+
}
|
|
398
|
+
if (sha256 !== expectedSha256) {
|
|
399
|
+
throw new Error(`${label} path handle content changed`);
|
|
400
|
+
}
|
|
401
|
+
if (requireSingleLink && after.nlink !== 1n) {
|
|
402
|
+
throw new Error(`${label} path handle must not be hard-linked`);
|
|
403
|
+
}
|
|
404
|
+
assertRegularNonLinkPath(file, label);
|
|
405
|
+
} finally {
|
|
406
|
+
if (pathFd !== undefined) {
|
|
407
|
+
try {
|
|
408
|
+
fs.closeSync(pathFd);
|
|
409
|
+
} catch {
|
|
410
|
+
/* best-effort */
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function sameName(left, right) {
|
|
417
|
+
return process.platform === "win32"
|
|
418
|
+
? String(left).toLowerCase() === String(right).toLowerCase()
|
|
419
|
+
: String(left) === String(right);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function firstCommandToken(command) {
|
|
423
|
+
const value = String(command || "");
|
|
424
|
+
let token = "";
|
|
425
|
+
let quote = null;
|
|
426
|
+
let escaping = false;
|
|
427
|
+
let started = false;
|
|
428
|
+
|
|
429
|
+
for (let i = 0; i < value.length; i++) {
|
|
430
|
+
const ch = value[i];
|
|
431
|
+
if (!started && /\s/.test(ch)) continue;
|
|
432
|
+
if (escaping) {
|
|
433
|
+
token += ch;
|
|
434
|
+
escaping = false;
|
|
435
|
+
started = true;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (quote === '"') {
|
|
439
|
+
if (ch === "\\") {
|
|
440
|
+
const next = value[i + 1];
|
|
441
|
+
if (next === '"' || next === "\\") {
|
|
442
|
+
escaping = true;
|
|
443
|
+
} else {
|
|
444
|
+
token += ch;
|
|
445
|
+
}
|
|
446
|
+
} else if (ch === '"') {
|
|
447
|
+
quote = null;
|
|
448
|
+
} else {
|
|
449
|
+
token += ch;
|
|
450
|
+
}
|
|
451
|
+
started = true;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (quote === "'") {
|
|
455
|
+
if (ch === "'") quote = null;
|
|
456
|
+
else token += ch;
|
|
457
|
+
started = true;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (ch === '"' || ch === "'") {
|
|
461
|
+
quote = ch;
|
|
462
|
+
started = true;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (/\s/.test(ch) || /[|&;<>()`]/.test(ch)) break;
|
|
466
|
+
token += ch;
|
|
467
|
+
started = true;
|
|
468
|
+
}
|
|
469
|
+
return token;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Parse exactly one command into literal argv. Every unquoted shell control
|
|
474
|
+
* operator is rejected. Once parsed, no shell sees these values.
|
|
475
|
+
*/
|
|
476
|
+
export function parsePluginBinCommand(command) {
|
|
477
|
+
if (typeof command !== "string" || command.trim() === "") {
|
|
478
|
+
throw pluginBinError(
|
|
479
|
+
"ERR_PLUGIN_BIN_COMMAND_INVALID",
|
|
480
|
+
"plugin bin command must be a non-empty string",
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
if (command.includes("\0")) {
|
|
484
|
+
throw pluginBinError(
|
|
485
|
+
"ERR_PLUGIN_BIN_COMMAND_INVALID",
|
|
486
|
+
"plugin bin command contains a NUL byte",
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
const argv = [];
|
|
490
|
+
let current = "";
|
|
491
|
+
let quote = null;
|
|
492
|
+
let escaping = false;
|
|
493
|
+
let tokenStarted = false;
|
|
494
|
+
|
|
495
|
+
const flush = () => {
|
|
496
|
+
if (!tokenStarted) return;
|
|
497
|
+
argv.push(current);
|
|
498
|
+
current = "";
|
|
499
|
+
tokenStarted = false;
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
for (let i = 0; i < command.length; i++) {
|
|
503
|
+
const ch = command[i];
|
|
504
|
+
if (escaping) {
|
|
505
|
+
current += ch;
|
|
506
|
+
tokenStarted = true;
|
|
507
|
+
escaping = false;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (quote === '"') {
|
|
511
|
+
if (ch === "\\") {
|
|
512
|
+
const next = command[i + 1];
|
|
513
|
+
if (next === '"' || next === "\\") {
|
|
514
|
+
escaping = true;
|
|
515
|
+
} else {
|
|
516
|
+
current += ch;
|
|
517
|
+
tokenStarted = true;
|
|
518
|
+
}
|
|
519
|
+
} else if (ch === '"') {
|
|
520
|
+
quote = null;
|
|
521
|
+
tokenStarted = true;
|
|
522
|
+
} else {
|
|
523
|
+
current += ch;
|
|
524
|
+
tokenStarted = true;
|
|
525
|
+
}
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (quote === "'") {
|
|
529
|
+
if (ch === "'") {
|
|
530
|
+
quote = null;
|
|
531
|
+
tokenStarted = true;
|
|
532
|
+
} else {
|
|
533
|
+
current += ch;
|
|
534
|
+
tokenStarted = true;
|
|
535
|
+
}
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (ch === '"' || ch === "'") {
|
|
539
|
+
quote = ch;
|
|
540
|
+
tokenStarted = true;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (ch === "\r" || ch === "\n" || /[|&;<>()`]/.test(ch)) {
|
|
544
|
+
throw pluginBinError(
|
|
545
|
+
"ERR_PLUGIN_BIN_COMPOUND_COMMAND",
|
|
546
|
+
"plugin bin commands must be a single direct invocation; shell operators are not allowed",
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
if (ch === "$" && command[i + 1] === "(") {
|
|
550
|
+
throw pluginBinError(
|
|
551
|
+
"ERR_PLUGIN_BIN_COMPOUND_COMMAND",
|
|
552
|
+
"plugin bin commands may not contain command substitution",
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
if (ch === " " || ch === "\t") {
|
|
556
|
+
flush();
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
current += ch;
|
|
560
|
+
tokenStarted = true;
|
|
561
|
+
}
|
|
562
|
+
if (quote || escaping) {
|
|
563
|
+
throw pluginBinError(
|
|
564
|
+
"ERR_PLUGIN_BIN_COMMAND_INVALID",
|
|
565
|
+
"plugin bin command contains an unmatched quote or escape",
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
flush();
|
|
569
|
+
if (argv.length === 0 || argv[0] === "") {
|
|
570
|
+
throw pluginBinError(
|
|
571
|
+
"ERR_PLUGIN_BIN_COMMAND_INVALID",
|
|
572
|
+
"plugin bin command has no executable",
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
return argv;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function fileIdentity(file, root) {
|
|
579
|
+
let fd;
|
|
580
|
+
try {
|
|
581
|
+
const rootReal = fs.realpathSync.native(path.resolve(root));
|
|
582
|
+
const lst = fs.lstatSync(file);
|
|
583
|
+
if (lst.isSymbolicLink() || !lst.isFile()) {
|
|
584
|
+
throw new Error("target must be a regular, non-symlink file");
|
|
585
|
+
}
|
|
586
|
+
const targetReal = fs.realpathSync.native(file);
|
|
587
|
+
if (!isWithin(rootReal, targetReal)) {
|
|
588
|
+
throw new Error("target resolves outside the plugin root");
|
|
589
|
+
}
|
|
590
|
+
fd = openFileForAttestation(targetReal);
|
|
591
|
+
const before = fs.fstatSync(fd, { bigint: true });
|
|
592
|
+
if (!before.isFile())
|
|
593
|
+
throw new Error("opened target is not a regular file");
|
|
594
|
+
if (before.size < 0n || before.size > BigInt(PLUGIN_BIN_MAX_BYTES)) {
|
|
595
|
+
throw new Error("target exceeds the attestation size limit");
|
|
596
|
+
}
|
|
597
|
+
const pathBefore = fs.statSync(targetReal, { bigint: true });
|
|
598
|
+
const beforeIdentity = preciseOpenFileIdentity(before);
|
|
599
|
+
const pathBeforeIdentity = preciseOpenFileIdentity(pathBefore);
|
|
600
|
+
if (
|
|
601
|
+
process.platform !== "win32" &&
|
|
602
|
+
!samePreciseFileObject(beforeIdentity, pathBeforeIdentity)
|
|
603
|
+
) {
|
|
604
|
+
throw new Error("target identity changed during attestation");
|
|
605
|
+
}
|
|
606
|
+
const sha256 = hashOpenFile(fd, Number(before.size));
|
|
607
|
+
const after = fs.fstatSync(fd, { bigint: true });
|
|
608
|
+
const pathAfter = fs.statSync(targetReal, { bigint: true });
|
|
609
|
+
const afterIdentity = preciseOpenFileIdentity(after);
|
|
610
|
+
const pathAfterIdentity = preciseOpenFileIdentity(pathAfter);
|
|
611
|
+
const stable =
|
|
612
|
+
samePreciseOpenFileIdentity(beforeIdentity, afterIdentity) &&
|
|
613
|
+
samePreciseOpenFileIdentity(pathBeforeIdentity, pathAfterIdentity) &&
|
|
614
|
+
(process.platform === "win32" ||
|
|
615
|
+
samePreciseFileObject(afterIdentity, pathAfterIdentity));
|
|
616
|
+
if (!stable) throw new Error("target identity changed during attestation");
|
|
617
|
+
if (process.platform === "win32") {
|
|
618
|
+
attestWindowsPathHandle(
|
|
619
|
+
targetReal,
|
|
620
|
+
afterIdentity,
|
|
621
|
+
sha256,
|
|
622
|
+
PLUGIN_BIN_MAX_BYTES,
|
|
623
|
+
{ label: "target", requireSingleLink: true },
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
if (after.nlink !== 1n) {
|
|
627
|
+
throw new Error("target must not be hard-linked");
|
|
628
|
+
}
|
|
629
|
+
const metadata = fs.fstatSync(fd);
|
|
630
|
+
const finalStat = fs.fstatSync(fd, { bigint: true });
|
|
631
|
+
const finalIdentity = preciseOpenFileIdentity(finalStat);
|
|
632
|
+
const finalPathIdentity = preciseOpenFileIdentity(
|
|
633
|
+
fs.statSync(targetReal, { bigint: true }),
|
|
634
|
+
);
|
|
635
|
+
if (
|
|
636
|
+
!samePreciseOpenFileIdentity(afterIdentity, finalIdentity) ||
|
|
637
|
+
!samePreciseOpenFileIdentity(pathAfterIdentity, finalPathIdentity) ||
|
|
638
|
+
(process.platform !== "win32" &&
|
|
639
|
+
!samePreciseFileObject(finalIdentity, finalPathIdentity))
|
|
640
|
+
) {
|
|
641
|
+
throw new Error("target identity changed during attestation");
|
|
642
|
+
}
|
|
643
|
+
if (process.platform === "win32") {
|
|
644
|
+
attestWindowsPathHandle(
|
|
645
|
+
targetReal,
|
|
646
|
+
finalIdentity,
|
|
647
|
+
sha256,
|
|
648
|
+
PLUGIN_BIN_MAX_BYTES,
|
|
649
|
+
{ label: "target", requireSingleLink: true },
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
if (finalStat.nlink !== 1n) {
|
|
653
|
+
throw new Error("target must not be hard-linked");
|
|
654
|
+
}
|
|
655
|
+
return Object.freeze({
|
|
656
|
+
realPath: targetReal,
|
|
657
|
+
sha256,
|
|
658
|
+
bytes: Number(after.size),
|
|
659
|
+
dev: String(after.dev),
|
|
660
|
+
ino: String(after.ino),
|
|
661
|
+
mtimeMs: metadata.mtimeMs,
|
|
662
|
+
mode: Number(after.mode),
|
|
663
|
+
});
|
|
664
|
+
} catch (err) {
|
|
665
|
+
throw pluginBinError(
|
|
666
|
+
"ERR_PLUGIN_BIN_IDENTITY_UNATTESTED",
|
|
667
|
+
`plugin bin target identity could not be attested: ${err.message}`,
|
|
668
|
+
);
|
|
669
|
+
} finally {
|
|
670
|
+
if (fd !== undefined) {
|
|
671
|
+
try {
|
|
672
|
+
fs.closeSync(fd);
|
|
673
|
+
} catch {
|
|
674
|
+
/* best-effort */
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Attest the exact Node runtime used by a direct policy-bearing plugin bin.
|
|
144
682
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
683
|
+
* This identity is intentionally produced outside the plugin manifest. The
|
|
684
|
+
* manifest may request boundaries, but it cannot nominate a broader runtime
|
|
685
|
+
* or filesystem root for a strong-sandbox execution contract.
|
|
148
686
|
*/
|
|
149
|
-
export function
|
|
687
|
+
export function attestPluginNodeRuntime(command) {
|
|
688
|
+
let fd;
|
|
689
|
+
try {
|
|
690
|
+
if (
|
|
691
|
+
typeof command !== "string" ||
|
|
692
|
+
!path.isAbsolute(command) ||
|
|
693
|
+
command !== process.execPath
|
|
694
|
+
) {
|
|
695
|
+
throw new Error("runtime must be the current absolute process.execPath");
|
|
696
|
+
}
|
|
697
|
+
const requestedPath = path.resolve(command);
|
|
698
|
+
const runtimeReal = fs.realpathSync.native(requestedPath);
|
|
699
|
+
const lst = fs.lstatSync(runtimeReal);
|
|
700
|
+
if (lst.isSymbolicLink() || !lst.isFile()) {
|
|
701
|
+
throw new Error("runtime must resolve to a regular file");
|
|
702
|
+
}
|
|
703
|
+
if (process.platform !== "win32" && (lst.mode & 0o111) === 0) {
|
|
704
|
+
throw new Error("runtime is not executable");
|
|
705
|
+
}
|
|
706
|
+
fd = openFileForAttestation(runtimeReal);
|
|
707
|
+
const before = fs.fstatSync(fd, { bigint: true });
|
|
708
|
+
if (
|
|
709
|
+
!before.isFile() ||
|
|
710
|
+
before.size < 0n ||
|
|
711
|
+
before.size > BigInt(PLUGIN_NODE_RUNTIME_MAX_BYTES)
|
|
712
|
+
) {
|
|
713
|
+
throw new Error("runtime exceeds the attestation size limit");
|
|
714
|
+
}
|
|
715
|
+
const pathBefore = fs.statSync(runtimeReal, { bigint: true });
|
|
716
|
+
const beforeIdentity = preciseOpenFileIdentity(before);
|
|
717
|
+
const pathBeforeIdentity = preciseOpenFileIdentity(pathBefore);
|
|
718
|
+
if (
|
|
719
|
+
process.platform !== "win32" &&
|
|
720
|
+
!samePreciseFileObject(beforeIdentity, pathBeforeIdentity)
|
|
721
|
+
) {
|
|
722
|
+
throw new Error("runtime identity changed during attestation");
|
|
723
|
+
}
|
|
724
|
+
const sha256 = hashOpenFile(fd, Number(before.size));
|
|
725
|
+
const after = fs.fstatSync(fd, { bigint: true });
|
|
726
|
+
const pathAfter = fs.statSync(runtimeReal, { bigint: true });
|
|
727
|
+
const afterIdentity = preciseOpenFileIdentity(after);
|
|
728
|
+
const pathAfterIdentity = preciseOpenFileIdentity(pathAfter);
|
|
729
|
+
const stable =
|
|
730
|
+
samePreciseOpenFileIdentity(beforeIdentity, afterIdentity) &&
|
|
731
|
+
samePreciseOpenFileIdentity(pathBeforeIdentity, pathAfterIdentity) &&
|
|
732
|
+
(process.platform === "win32" ||
|
|
733
|
+
samePreciseFileObject(afterIdentity, pathAfterIdentity));
|
|
734
|
+
if (!stable) throw new Error("runtime identity changed during attestation");
|
|
735
|
+
if (process.platform === "win32") {
|
|
736
|
+
attestWindowsPathHandle(
|
|
737
|
+
runtimeReal,
|
|
738
|
+
afterIdentity,
|
|
739
|
+
sha256,
|
|
740
|
+
PLUGIN_NODE_RUNTIME_MAX_BYTES,
|
|
741
|
+
{ label: "runtime" },
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
const metadata = fs.fstatSync(fd);
|
|
745
|
+
const finalIdentity = preciseOpenFileIdentity(
|
|
746
|
+
fs.fstatSync(fd, { bigint: true }),
|
|
747
|
+
);
|
|
748
|
+
const finalPathIdentity = preciseOpenFileIdentity(
|
|
749
|
+
fs.statSync(runtimeReal, { bigint: true }),
|
|
750
|
+
);
|
|
751
|
+
if (
|
|
752
|
+
!samePreciseOpenFileIdentity(afterIdentity, finalIdentity) ||
|
|
753
|
+
!samePreciseOpenFileIdentity(pathAfterIdentity, finalPathIdentity) ||
|
|
754
|
+
(process.platform !== "win32" &&
|
|
755
|
+
!samePreciseFileObject(finalIdentity, finalPathIdentity))
|
|
756
|
+
) {
|
|
757
|
+
throw new Error("runtime identity changed during attestation");
|
|
758
|
+
}
|
|
759
|
+
if (process.platform === "win32") {
|
|
760
|
+
attestWindowsPathHandle(
|
|
761
|
+
runtimeReal,
|
|
762
|
+
finalIdentity,
|
|
763
|
+
sha256,
|
|
764
|
+
PLUGIN_NODE_RUNTIME_MAX_BYTES,
|
|
765
|
+
{ label: "runtime" },
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
return Object.freeze({
|
|
769
|
+
requestedPath,
|
|
770
|
+
realPath: runtimeReal,
|
|
771
|
+
sha256,
|
|
772
|
+
bytes: Number(after.size),
|
|
773
|
+
dev: String(after.dev),
|
|
774
|
+
ino: String(after.ino),
|
|
775
|
+
mtimeMs: metadata.mtimeMs,
|
|
776
|
+
mode: Number(after.mode),
|
|
777
|
+
});
|
|
778
|
+
} catch (err) {
|
|
779
|
+
throw pluginBinError(
|
|
780
|
+
"ERR_PLUGIN_NODE_RUNTIME_UNATTESTED",
|
|
781
|
+
`plugin Node runtime identity could not be attested: ${err.message}`,
|
|
782
|
+
);
|
|
783
|
+
} finally {
|
|
784
|
+
if (fd !== undefined) {
|
|
785
|
+
try {
|
|
786
|
+
fs.closeSync(fd);
|
|
787
|
+
} catch {
|
|
788
|
+
/* best-effort */
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function attestPluginRootIdentity(pluginRoot) {
|
|
795
|
+
const before = fs.lstatSync(pluginRoot);
|
|
796
|
+
const after = fs.statSync(pluginRoot);
|
|
797
|
+
if (
|
|
798
|
+
before.isSymbolicLink() ||
|
|
799
|
+
!before.isDirectory() ||
|
|
800
|
+
!after.isDirectory() ||
|
|
801
|
+
before.dev !== after.dev ||
|
|
802
|
+
before.ino !== after.ino
|
|
803
|
+
) {
|
|
804
|
+
throw new Error("plugin root identity changed during attestation");
|
|
805
|
+
}
|
|
806
|
+
return Object.freeze({
|
|
807
|
+
realPath: pluginRoot,
|
|
808
|
+
dev: String(after.dev),
|
|
809
|
+
ino: String(after.ino),
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function launchForTarget(identity, userArgs) {
|
|
814
|
+
const ext = path.extname(identity.realPath).toLowerCase();
|
|
815
|
+
if ([".js", ".cjs", ".mjs"].includes(ext)) {
|
|
816
|
+
return {
|
|
817
|
+
command: process.execPath,
|
|
818
|
+
args: [identity.realPath, ...userArgs],
|
|
819
|
+
runtime: "node",
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
if (process.platform === "win32" && [".cmd", ".bat", ".ps1"].includes(ext)) {
|
|
823
|
+
throw pluginBinError(
|
|
824
|
+
"ERR_PLUGIN_BIN_WRAPPER_UNSUPPORTED",
|
|
825
|
+
`plugin bin wrapper ${ext} is not supported by the direct native route; ship a Node script or native executable`,
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
if (process.platform !== "win32" && (identity.mode & 0o111) === 0) {
|
|
829
|
+
throw pluginBinError(
|
|
830
|
+
"ERR_PLUGIN_BIN_NOT_EXECUTABLE",
|
|
831
|
+
"plugin bin target is not executable",
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
command: identity.realPath,
|
|
836
|
+
args: userArgs,
|
|
837
|
+
runtime: "native",
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function matchesEntryToken(token, entry, opts) {
|
|
842
|
+
if (sameName(token, entry.name)) return true;
|
|
843
|
+
if (process.platform === "win32" && !/[\\/]/.test(token)) {
|
|
844
|
+
const executableExtensions = new Set([".exe", ".com", ".cmd", ".bat"]);
|
|
845
|
+
const tokenExt = path.extname(token).toLowerCase();
|
|
846
|
+
const aliasExt = path.extname(entry.name).toLowerCase();
|
|
847
|
+
if (
|
|
848
|
+
executableExtensions.has(tokenExt) &&
|
|
849
|
+
!aliasExt &&
|
|
850
|
+
sameName(token.slice(0, -tokenExt.length), entry.name)
|
|
851
|
+
) {
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
if (
|
|
855
|
+
!tokenExt &&
|
|
856
|
+
executableExtensions.has(aliasExt) &&
|
|
857
|
+
sameName(token, entry.name.slice(0, -aliasExt.length))
|
|
858
|
+
) {
|
|
859
|
+
return true;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (!path.isAbsolute(token) && !/[\\/]/.test(token)) return false;
|
|
863
|
+
try {
|
|
864
|
+
const resolved = path.resolve(
|
|
865
|
+
opts.commandCwd || opts.cwd || process.cwd(),
|
|
866
|
+
token,
|
|
867
|
+
);
|
|
868
|
+
return sameName(resolved, path.resolve(entry.absPath));
|
|
869
|
+
} catch {
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Resolve a trusted declared alias to a direct Broker invocation. Ordinary
|
|
876
|
+
* commands return null. A matching plugin alias that cannot be parsed or
|
|
877
|
+
* attested throws fail-closed instead of falling back to a shell.
|
|
878
|
+
*
|
|
879
|
+
* @returns {object|null}
|
|
880
|
+
*/
|
|
881
|
+
export function resolvePluginBinInvocation(command, opts = {}) {
|
|
150
882
|
if (typeof command !== "string") return null;
|
|
151
|
-
const
|
|
152
|
-
const token = match?.[1] || match?.[2] || match?.[3] || "";
|
|
883
|
+
const token = firstCommandToken(command);
|
|
153
884
|
if (!token) return null;
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
885
|
+
const matches = collectPluginBinCommands(opts).filter((entry) =>
|
|
886
|
+
matchesEntryToken(token, entry, opts),
|
|
887
|
+
);
|
|
888
|
+
if (matches.length === 0) return null;
|
|
889
|
+
const policyRequired = matches.some((entry) => entry.sandboxPolicy);
|
|
890
|
+
if (matches.length > 1) {
|
|
891
|
+
if (!policyRequired) return null;
|
|
892
|
+
throw pluginBinError(
|
|
893
|
+
"ERR_PLUGIN_BIN_COMMAND_AMBIGUOUS",
|
|
894
|
+
`plugin bin alias "${token}" is declared by multiple trusted plugins`,
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
let words;
|
|
898
|
+
try {
|
|
899
|
+
words = parsePluginBinCommand(command);
|
|
900
|
+
} catch (err) {
|
|
901
|
+
if (!policyRequired && err?.pluginBinFailClosed) return null;
|
|
902
|
+
throw err;
|
|
903
|
+
}
|
|
904
|
+
const entry = matches[0];
|
|
905
|
+
if (!matchesEntryToken(words[0], entry, opts)) {
|
|
906
|
+
throw pluginBinError(
|
|
907
|
+
"ERR_PLUGIN_BIN_COMMAND_INVALID",
|
|
908
|
+
"plugin bin command target changed while parsing",
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
let identity;
|
|
912
|
+
let launch;
|
|
913
|
+
try {
|
|
914
|
+
identity = fileIdentity(entry.absPath, entry.root);
|
|
915
|
+
launch = launchForTarget(identity, words.slice(1));
|
|
916
|
+
} catch (err) {
|
|
917
|
+
if (!policyRequired && err?.pluginBinFailClosed) return null;
|
|
918
|
+
throw err;
|
|
919
|
+
}
|
|
920
|
+
const sandboxPolicy = entry.sandboxPolicy
|
|
921
|
+
? Object.freeze({
|
|
922
|
+
requiredBoundaries: Object.freeze([
|
|
923
|
+
...(entry.sandboxPolicy.requiredBoundaries || []),
|
|
924
|
+
]),
|
|
925
|
+
})
|
|
926
|
+
: null;
|
|
927
|
+
const invocation = Object.freeze({
|
|
928
|
+
command: launch.command,
|
|
929
|
+
args: Object.freeze([...launch.args]),
|
|
930
|
+
runtime: launch.runtime,
|
|
931
|
+
shell: false,
|
|
932
|
+
pluginId: entry.plugin,
|
|
933
|
+
pluginVersion: entry.version || null,
|
|
934
|
+
pluginSource: entry.manifestPath || entry.root,
|
|
935
|
+
scope: entry.scope || null,
|
|
936
|
+
binName: entry.name,
|
|
937
|
+
binPath: identity.realPath,
|
|
938
|
+
pluginRoot: entry.root,
|
|
939
|
+
executableIdentity: identity,
|
|
940
|
+
...(sandboxPolicy ? { sandboxPolicy } : {}),
|
|
941
|
+
});
|
|
942
|
+
_issuedPluginBinInvocations.set(
|
|
943
|
+
invocation,
|
|
944
|
+
Object.freeze({
|
|
945
|
+
command,
|
|
946
|
+
cwd: path.resolve(opts.cwd || process.cwd()),
|
|
947
|
+
commandCwd: path.resolve(opts.commandCwd || opts.cwd || process.cwd()),
|
|
948
|
+
scopes: Array.isArray(opts.scopes)
|
|
949
|
+
? Object.freeze([...opts.scopes])
|
|
950
|
+
: undefined,
|
|
951
|
+
}),
|
|
952
|
+
);
|
|
953
|
+
return invocation;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Compare the target with the identity captured during resolution. Call this
|
|
958
|
+
* immediately before the Broker method so a version-dir mutation cannot be
|
|
959
|
+
* silently accepted after policy resolution.
|
|
960
|
+
*/
|
|
961
|
+
export function reattestPluginBinInvocation(invocation) {
|
|
962
|
+
if (!invocation?.binPath || !invocation?.pluginRoot) {
|
|
963
|
+
throw pluginBinError(
|
|
964
|
+
"ERR_PLUGIN_BIN_IDENTITY_UNATTESTED",
|
|
965
|
+
"plugin bin invocation has no attested target",
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
const current = fileIdentity(invocation.binPath, invocation.pluginRoot);
|
|
969
|
+
const expected = invocation.executableIdentity || {};
|
|
970
|
+
for (const field of [
|
|
971
|
+
"realPath",
|
|
972
|
+
"sha256",
|
|
973
|
+
"bytes",
|
|
974
|
+
"dev",
|
|
975
|
+
"ino",
|
|
976
|
+
"mtimeMs",
|
|
977
|
+
]) {
|
|
978
|
+
if (current[field] !== expected[field]) {
|
|
979
|
+
throw pluginBinError(
|
|
980
|
+
"ERR_PLUGIN_BIN_IDENTITY_CHANGED",
|
|
981
|
+
`plugin bin target identity changed before launch (${field})`,
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return current;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Issue the private strong-sandbox contract for one resolver-produced
|
|
990
|
+
* policy-bearing Plugin Node or native invocation. Object identity is the
|
|
991
|
+
* capability: callers cannot mint a usable contract by copying public fields.
|
|
992
|
+
*/
|
|
993
|
+
export function createPluginSandboxExecutionContract(invocation) {
|
|
994
|
+
const issuedResolution =
|
|
995
|
+
invocation && typeof invocation === "object"
|
|
996
|
+
? _issuedPluginBinInvocations.get(invocation)
|
|
997
|
+
: null;
|
|
998
|
+
const runtime = invocation?.runtime;
|
|
999
|
+
const runtimeLabel = runtime === "native" ? "native" : "Node";
|
|
1000
|
+
const errorPrefix =
|
|
1001
|
+
runtime === "native" ? "ERR_PLUGIN_NATIVE" : "ERR_PLUGIN_NODE";
|
|
1002
|
+
if (!invocation || typeof invocation !== "object" || !issuedResolution) {
|
|
1003
|
+
throw pluginBinError(
|
|
1004
|
+
`${errorPrefix}_SANDBOX_CONTRACT_UNTRUSTED`,
|
|
1005
|
+
`plugin ${runtimeLabel} sandbox execution contract requires a resolver-issued invocation`,
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
// A resolver-issued invocation is itself a one-shot capability. Consume it
|
|
1009
|
+
// before any further work so failed or successful issuance cannot be replayed
|
|
1010
|
+
// after trust, capability, or manifest state changes.
|
|
1011
|
+
_issuedPluginBinInvocations.delete(invocation);
|
|
1012
|
+
const directNode =
|
|
1013
|
+
runtime === "node" &&
|
|
1014
|
+
invocation.command === process.execPath &&
|
|
1015
|
+
invocation.args?.[0] === invocation.binPath;
|
|
1016
|
+
const directNative =
|
|
1017
|
+
runtime === "native" && invocation.command === invocation.binPath;
|
|
1018
|
+
if (
|
|
1019
|
+
(!directNode && !directNative) ||
|
|
1020
|
+
invocation.shell !== false ||
|
|
1021
|
+
!invocation.sandboxPolicy?.requiredBoundaries?.length
|
|
1022
|
+
) {
|
|
1023
|
+
throw pluginBinError(
|
|
1024
|
+
`${errorPrefix}_SANDBOX_CONTRACT_UNSUPPORTED`,
|
|
1025
|
+
`plugin ${runtimeLabel} sandbox execution contract requires one direct policy-bearing bin`,
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
try {
|
|
1030
|
+
let currentInvocation;
|
|
1031
|
+
try {
|
|
1032
|
+
currentInvocation = resolvePluginBinInvocation(issuedResolution.command, {
|
|
1033
|
+
cwd: issuedResolution.cwd,
|
|
1034
|
+
commandCwd: issuedResolution.commandCwd,
|
|
1035
|
+
...(issuedResolution.scopes !== undefined
|
|
1036
|
+
? { scopes: issuedResolution.scopes }
|
|
1037
|
+
: {}),
|
|
1038
|
+
failClosed: true,
|
|
1039
|
+
});
|
|
1040
|
+
const scalarFields = [
|
|
1041
|
+
"command",
|
|
1042
|
+
"runtime",
|
|
1043
|
+
"shell",
|
|
1044
|
+
"pluginId",
|
|
1045
|
+
"pluginVersion",
|
|
1046
|
+
"pluginSource",
|
|
1047
|
+
"scope",
|
|
1048
|
+
"binName",
|
|
1049
|
+
"binPath",
|
|
1050
|
+
"pluginRoot",
|
|
1051
|
+
];
|
|
1052
|
+
const identityFields = [
|
|
1053
|
+
"realPath",
|
|
1054
|
+
"sha256",
|
|
1055
|
+
"bytes",
|
|
1056
|
+
"dev",
|
|
1057
|
+
"ino",
|
|
1058
|
+
"mtimeMs",
|
|
1059
|
+
"mode",
|
|
1060
|
+
];
|
|
1061
|
+
const originalBoundaries = [
|
|
1062
|
+
...(invocation.sandboxPolicy?.requiredBoundaries || []),
|
|
1063
|
+
].sort();
|
|
1064
|
+
const currentBoundaries = [
|
|
1065
|
+
...(currentInvocation?.sandboxPolicy?.requiredBoundaries || []),
|
|
1066
|
+
].sort();
|
|
1067
|
+
if (
|
|
1068
|
+
!currentInvocation ||
|
|
1069
|
+
scalarFields.some(
|
|
1070
|
+
(field) => currentInvocation[field] !== invocation[field],
|
|
1071
|
+
) ||
|
|
1072
|
+
currentInvocation.args.length !== invocation.args.length ||
|
|
1073
|
+
currentInvocation.args.some(
|
|
1074
|
+
(value, index) => value !== invocation.args[index],
|
|
1075
|
+
) ||
|
|
1076
|
+
originalBoundaries.length !== currentBoundaries.length ||
|
|
1077
|
+
originalBoundaries.some(
|
|
1078
|
+
(value, index) => value !== currentBoundaries[index],
|
|
1079
|
+
) ||
|
|
1080
|
+
identityFields.some(
|
|
1081
|
+
(field) =>
|
|
1082
|
+
currentInvocation.executableIdentity?.[field] !==
|
|
1083
|
+
invocation.executableIdentity?.[field],
|
|
1084
|
+
)
|
|
1085
|
+
) {
|
|
1086
|
+
throw pluginBinError(
|
|
1087
|
+
`${errorPrefix}_SANDBOX_CONTRACT_STALE`,
|
|
1088
|
+
`plugin ${runtimeLabel} sandbox execution contract provenance changed after resolution`,
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
} finally {
|
|
1092
|
+
if (currentInvocation) {
|
|
1093
|
+
_issuedPluginBinInvocations.delete(currentInvocation);
|
|
171
1094
|
}
|
|
172
|
-
return {
|
|
173
|
-
pluginId: entry.plugin,
|
|
174
|
-
pluginVersion: entry.version || null,
|
|
175
|
-
pluginSource: entry.dir,
|
|
176
|
-
scope: entry.scope || null,
|
|
177
|
-
binPath,
|
|
178
|
-
};
|
|
179
1095
|
}
|
|
1096
|
+
reattestPluginBinInvocation(invocation);
|
|
1097
|
+
const pluginRoot = fs.realpathSync.native(
|
|
1098
|
+
path.resolve(invocation.pluginRoot),
|
|
1099
|
+
);
|
|
1100
|
+
const entryRelative = path.relative(
|
|
1101
|
+
pluginRoot,
|
|
1102
|
+
invocation.executableIdentity.realPath,
|
|
1103
|
+
);
|
|
1104
|
+
if (
|
|
1105
|
+
entryRelative === "" ||
|
|
1106
|
+
entryRelative === ".." ||
|
|
1107
|
+
entryRelative.startsWith(`..${path.sep}`) ||
|
|
1108
|
+
path.isAbsolute(entryRelative)
|
|
1109
|
+
) {
|
|
1110
|
+
throw new Error("plugin bin target is outside its canonical plugin root");
|
|
1111
|
+
}
|
|
1112
|
+
// The trusted Node runtime is both the Node launch target and the
|
|
1113
|
+
// capability-probe runtime for a native launch. Keeping the probe runtime
|
|
1114
|
+
// in the contract prevents a plugin from nominating a host executable.
|
|
1115
|
+
const runtimeIdentity = attestPluginNodeRuntime(process.execPath);
|
|
1116
|
+
const rootIdentity = attestPluginRootIdentity(pluginRoot);
|
|
1117
|
+
const contract = Object.freeze({
|
|
1118
|
+
contractVersion: 1,
|
|
1119
|
+
kind:
|
|
1120
|
+
runtime === "native"
|
|
1121
|
+
? "strict-plugin-native-static-elf-bin"
|
|
1122
|
+
: "strict-plugin-node-bin",
|
|
1123
|
+
pluginRoot,
|
|
1124
|
+
workingDirectory: pluginRoot,
|
|
1125
|
+
runtimePath: runtimeIdentity.realPath,
|
|
1126
|
+
rootIdentity,
|
|
1127
|
+
entryIdentity: invocation.executableIdentity,
|
|
1128
|
+
runtimeIdentity,
|
|
1129
|
+
});
|
|
1130
|
+
_issuedPluginSandboxContracts.set(
|
|
1131
|
+
contract,
|
|
1132
|
+
Object.freeze({
|
|
1133
|
+
origin: "plugin:bin",
|
|
1134
|
+
command:
|
|
1135
|
+
runtime === "native" ? invocation.command : runtimeIdentity.realPath,
|
|
1136
|
+
args: invocation.args,
|
|
1137
|
+
cwd: pluginRoot,
|
|
1138
|
+
pluginId: invocation.pluginId,
|
|
1139
|
+
pluginVersion: invocation.pluginVersion ?? null,
|
|
1140
|
+
pluginSource: invocation.pluginSource,
|
|
1141
|
+
pluginExecutableIdentity: invocation.executableIdentity,
|
|
1142
|
+
requiredBoundaries: invocation.sandboxPolicy.requiredBoundaries,
|
|
1143
|
+
sync: true,
|
|
1144
|
+
}),
|
|
1145
|
+
);
|
|
1146
|
+
return contract;
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
if (error?.pluginBinFailClosed) throw error;
|
|
1149
|
+
throw pluginBinError(
|
|
1150
|
+
`${errorPrefix}_SANDBOX_CONTRACT_UNATTESTED`,
|
|
1151
|
+
`plugin ${runtimeLabel} sandbox execution contract could not be created: ${error.message}`,
|
|
1152
|
+
);
|
|
180
1153
|
}
|
|
181
|
-
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Backward-compatible Node-only issuer retained for existing callers.
|
|
1158
|
+
*/
|
|
1159
|
+
export function createPluginNodeSandboxExecutionContract(invocation) {
|
|
1160
|
+
if (invocation?.runtime !== "node") {
|
|
1161
|
+
throw pluginBinError(
|
|
1162
|
+
"ERR_PLUGIN_NODE_SANDBOX_CONTRACT_UNSUPPORTED",
|
|
1163
|
+
"plugin Node sandbox execution contract requires one direct policy-bearing Node bin",
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
return createPluginSandboxExecutionContract(invocation);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Validate that a contract is the exact object issued above and is still
|
|
1171
|
+
* attached to the launch provenance for which it was issued.
|
|
1172
|
+
*/
|
|
1173
|
+
function issuedPluginSandboxContractMatches(contract, provenance = {}) {
|
|
1174
|
+
if (!contract || typeof contract !== "object") return false;
|
|
1175
|
+
const issued = _issuedPluginSandboxContracts.get(contract);
|
|
1176
|
+
if (!issued) return false;
|
|
1177
|
+
const args = Array.isArray(provenance.args) ? provenance.args : [];
|
|
1178
|
+
const requiredBoundaries = Array.isArray(provenance.requiredBoundaries)
|
|
1179
|
+
? provenance.requiredBoundaries
|
|
1180
|
+
: [];
|
|
1181
|
+
return (
|
|
1182
|
+
provenance.origin === issued.origin &&
|
|
1183
|
+
provenance.command === issued.command &&
|
|
1184
|
+
args.length === issued.args.length &&
|
|
1185
|
+
args.every((value, index) => value === issued.args[index]) &&
|
|
1186
|
+
provenance.cwd === issued.cwd &&
|
|
1187
|
+
provenance.pluginId === issued.pluginId &&
|
|
1188
|
+
(provenance.pluginVersion ?? null) === issued.pluginVersion &&
|
|
1189
|
+
provenance.pluginSource === issued.pluginSource &&
|
|
1190
|
+
provenance.pluginExecutableIdentity === issued.pluginExecutableIdentity &&
|
|
1191
|
+
provenance.sync === issued.sync &&
|
|
1192
|
+
issued.requiredBoundaries.every((boundary) =>
|
|
1193
|
+
requiredBoundaries.includes(boundary),
|
|
1194
|
+
)
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
export function verifyIssuedPluginNodeSandboxExecutionContract(
|
|
1199
|
+
contract,
|
|
1200
|
+
provenance = {},
|
|
1201
|
+
) {
|
|
1202
|
+
return (
|
|
1203
|
+
contract?.kind === "strict-plugin-node-bin" &&
|
|
1204
|
+
issuedPluginSandboxContractMatches(contract, provenance)
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
export function verifyIssuedPluginSandboxExecutionContract(
|
|
1209
|
+
contract,
|
|
1210
|
+
provenance = {},
|
|
1211
|
+
) {
|
|
1212
|
+
return issuedPluginSandboxContractMatches(contract, provenance);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* Atomically consume a one-launch contract. A trust decision captured for one
|
|
1217
|
+
* Broker call cannot be replayed after trust/policy state changes.
|
|
1218
|
+
*/
|
|
1219
|
+
export function consumeIssuedPluginNodeSandboxExecutionContract(
|
|
1220
|
+
contract,
|
|
1221
|
+
provenance = {},
|
|
1222
|
+
) {
|
|
1223
|
+
if (
|
|
1224
|
+
contract?.kind !== "strict-plugin-node-bin" ||
|
|
1225
|
+
!issuedPluginSandboxContractMatches(contract, provenance)
|
|
1226
|
+
) {
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
return _issuedPluginSandboxContracts.delete(contract);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
export function consumeIssuedPluginSandboxExecutionContract(
|
|
1233
|
+
contract,
|
|
1234
|
+
provenance = {},
|
|
1235
|
+
) {
|
|
1236
|
+
if (!issuedPluginSandboxContractMatches(contract, provenance)) {
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1239
|
+
return _issuedPluginSandboxContracts.delete(contract);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* Backward-compatible provenance resolver. It now succeeds only for commands
|
|
1244
|
+
* that qualify for the direct, attested invocation route.
|
|
1245
|
+
*/
|
|
1246
|
+
export function resolvePluginBinCommand(command, opts = {}) {
|
|
1247
|
+
const invocation = resolvePluginBinInvocation(command, opts);
|
|
1248
|
+
if (!invocation) return null;
|
|
1249
|
+
return {
|
|
1250
|
+
pluginId: invocation.pluginId,
|
|
1251
|
+
pluginVersion: invocation.pluginVersion,
|
|
1252
|
+
pluginSource: invocation.pluginSource,
|
|
1253
|
+
scope: invocation.scope,
|
|
1254
|
+
binPath: invocation.binPath,
|
|
1255
|
+
executableIdentity: invocation.executableIdentity,
|
|
1256
|
+
...(invocation.sandboxPolicy
|
|
1257
|
+
? { sandboxPolicy: invocation.sandboxPolicy }
|
|
1258
|
+
: {}),
|
|
1259
|
+
};
|
|
182
1260
|
}
|