chainlesschain 0.162.182 → 0.162.184
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-DzZ5_xoj.js → AIOps-DzAl5fGx.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DeLXddIg.js → ActionButton-DAHtMGeC.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-rvDCmet1.js → Analytics-BHQaexv5.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-Yt6QVDnp.js → AppLayout-Phz_pD0n.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-BS112K5L.js → Artifacts-DHt47kCA.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-0wJuQrNu.js → Audit--Y_8FULl.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-B90PMgbn.js → BackgroundAgents-N1zchH7D.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-TkgMglQQ.js → Backup-B_BoyN30.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CQVS9p1l.js → BaseInput-Gl4uQHcT.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-YTjQHoC7.js → Chat-Cg-eIyAZ.js} +5 -5
- package/src/assets/web-panel/assets/ChatBubbleRenderer-BzeUi3JH.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-5bwaOabq.js → Checkbox-D0MGR2wA.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BFmNQ_Xe.js → Codegen-BdObJEFn.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BT_u4Dzp.js → Col--ghxXT3z.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CJP52u5b.js → Community-BUFeXOqz.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BpzhJE9i.js → Compact-DhQO2_Mr.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-Cg7rfslp.js → Compliance-3HoTqKYd.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-JisxL6Yy.js → Cowork-B3zmO4VW.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-Bu7Ug_MC.js → Cron-CJDrZOcc.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CvuqoLdj.js → Crosschain-SMl15obd.js} +1 -1
- package/src/assets/web-panel/assets/{DID-C2u9G8Pu.js → DID-CgTrTWrl.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-BFt5Yv3d.js → Dashboard-dXFKaKol.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DIaT9Wxq.js → Dropdown-qQHEhCvI.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-D_A601Y4.js → EmailListRenderer-C_ej0MWH.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard--IyQ8I8w.js → FamilyGuardDashboard-D6ajoE7I.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BihWHxuY.js → Federation-Cio2y49h.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BB8dOXc_.js → FormItemContext-ClJz1cYp.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DQYhj1HH.js → GenericCardRenderer-Dr_8Maca.js} +1 -1
- package/src/assets/web-panel/assets/{Git-B-2QZxGf.js → Git-CevLwp5u.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CusSb-nG.js → Governance-CW9YyQUY.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BAjmvPPL.js → Inference-CQqeAyER.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DoNYWKYq.js → KnowledgeGraph-cuGVxHrw.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-B8fKWD-n.js → Logs-DIuHri5d.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-BlJm1J7E.js → MarkdownRenderer-R91Qw5JA.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-CyOscbSL.js → Marketplace-CY1VhFGx.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-Ex6Om5da.js → McpTools-BBAbi1vt.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-CDcD8l8r.js → Memory-BWYGHxJA.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-xfBqoFDg.js → MobileBridge-CPIFkW7A.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-BwObMMA2.js → MobileProjects-DQyWxCho.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-CIWd36OV.js → Mtc-Bo3GnQbx.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-CPX0ouuA.js → MtcAudit-C8-c4rSW.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-BDJsIUuW.js → Multisig-ChqMTtoq.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DEkbuD3N.js → NLProgramming-C48TyhvI.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DJpHRhH1.js → Notes-zogUFWxp.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-C1UZ-Qd5.js → NotificationSettings-v-K9XpSB.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-P94JGGNz.js +1 -0
- package/src/assets/web-panel/assets/{Organization-Bbhj0fac.js → Organization-Boi1dQfR.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-1H04x3kx.js → Overflow-DM5YWwgA.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CDpPe3et.js → P2P-C57LKpKK.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-MUX0Bpvw.js → PdhVaultBrowser-BFkPqu6Q.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-A07-0Ci1.js → Permissions-D6fB9YSp.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DhhDWxAZ.js → PersonalDataHub-Bp7EefEo.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-CyfbJfe-.js → Pipeline-BVln3hZf.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CNFps3ED.js → Privacy-Bf8P3nF6.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit--T-u3Dr5.js → ProjectInit-DM6wA2E2.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DJmrK2fC.js → ProjectSettings-CGbc5Iho.js} +2 -2
- package/src/assets/web-panel/assets/Projects-D0p9EOxm.js +1 -0
- package/src/assets/web-panel/assets/{Providers-DLzzbOZd.js → Providers-B2kJcxS6.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CexOomKS.js → QrScannerModal-Cxu7dXYy.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-QklGi19_.js → QuickAsk-DTlrTPms.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Bb9kwSw8.js → Recommend-Cm3X-cCj.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-DZuVNNN5.js → RemoteSession-DNb2kjvt.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-Dr5cLP0N.js → Reputation-CZUFFjc1.js} +1 -1
- package/src/assets/web-panel/assets/{Row-oCQHYv5t.js → Row-DAPgloxA.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-IFDEykIR.js → RssFeed-dsx9S793.js} +2 -2
- package/src/assets/web-panel/assets/{Search-CspVIxDc.js → Search-DnnC8kz-.js} +1 -1
- package/src/assets/web-panel/assets/{Security-Bm66o_DH.js → Security-VuZknRMK.js} +4 -4
- package/src/assets/web-panel/assets/{Services-DvanNqXl.js → Services-B3XNX8Vx.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-CHjEe9_O.js → Skeleton-Dl1nVtCj.js} +1 -1
- package/src/assets/web-panel/assets/Skills-CY09CZ_W.js +1 -0
- package/src/assets/web-panel/assets/{Sla-DxZm1491.js → Sla-HzU56n1z.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DWxEEclC.js → SpeechSettings-CRle7Jl2.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-FIvOFw5p.js → SyncSettings-DWadz7Sl.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-CNVTDGg8.js +1 -0
- package/src/assets/web-panel/assets/{Templates-CaNjzV_t.js → Templates-BBAgvrlM.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DjlP6TmH.js → Tenant-BsbbvGN8.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-uscpZI4q.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-DoZ8y7y8.js → TimelineRenderer-mTyjNE6r.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-390cg2TC.js → Tokens-Bz97oBgs.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-CLrtCj5_.js → Trigger-BTRKC140.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DpESfSPn.js → Trust-B4A_KuyX.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CN6279O0.js → UkeySign-DES9TmLU.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DPtAUlSe.js → VideoEditing-ClXs7Qxp.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-nKOMV_1n.js → Wallet-IzZqFz-7.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-HuXlSOc4.js → WebAuthn-BtOP04g_.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-CUfFjARi.js → WorkflowEditor-_0cqnopY.js} +1 -1
- package/src/assets/web-panel/assets/{chat-6Eq0PcZJ.js → chat-ax7193uU.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BP_Sf6Iy.js → colors-Bu4WGJah.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BufMKIh5.js → compact-item-BX1dlTW0.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BBMXxWSW.js → createContext-CktA_Dj7.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-x877iKKS.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-fUK_tRO2.js → hasIn-D5wd817l.js} +1 -1
- package/src/assets/web-panel/assets/{index-t8N1tSeX.js → index-5PYYkr8S.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLpIs_P_.js → index-A4uBPQCz.js} +1 -1
- package/src/assets/web-panel/assets/{index-DVsqvI4M.js → index-BAtBNfkr.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bkg4ZePE.js → index-BLLsUNDc.js} +1 -1
- package/src/assets/web-panel/assets/{index-cpEli44X.js → index-BQtoRyTO.js} +1 -1
- package/src/assets/web-panel/assets/{index-wKTLbKyc.js → index-BRfXoRuh.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7Etk0Te.js → index-Bad2CYeH.js} +1 -1
- package/src/assets/web-panel/assets/{index-CwSf5pnf.js → index-BcGdL3sG.js} +1 -1
- package/src/assets/web-panel/assets/{index-3FuwBewR.js → index-C3HmzCLt.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHy6yNiF.js → index-C8DkWyNr.js} +1 -1
- package/src/assets/web-panel/assets/{index-CgiECHu2.js → index-C8tLR2TG.js} +1 -1
- package/src/assets/web-panel/assets/{index-C44atv4k.js → index-CP8ELJBc.js} +1 -1
- package/src/assets/web-panel/assets/{index-CW_G-MXw.js → index-CUForzsG.js} +1 -1
- package/src/assets/web-panel/assets/{index-pum7HYBA.js → index-CYRE0NJA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BWKoBU8R.js → index-CeVZaJSC.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4_MuGkd.js → index-CfzxR61U.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dyngbd29.js → index-Ci3y1ZAv.js} +19 -19
- package/src/assets/web-panel/assets/{index-fzaMtSJ8.js → index-CjEJjl5C.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cp8vhyZy.js → index-Cv7bVWJ5.js} +1 -1
- package/src/assets/web-panel/assets/{index-DPdUNk1i.js → index-D-o4Rwjk.js} +1 -1
- package/src/assets/web-panel/assets/index-D7pfPouB.js +1 -0
- package/src/assets/web-panel/assets/{index-CeASi4fr.js → index-D9op8poE.js} +1 -1
- package/src/assets/web-panel/assets/{index-DASluSqY.js → index-DF1TysIF.js} +1 -1
- package/src/assets/web-panel/assets/{index-DocdRcbd.js → index-DYo9S5OT.js} +1 -1
- package/src/assets/web-panel/assets/{index-2A5ph8b2.js → index-D_cycOET.js} +1 -1
- package/src/assets/web-panel/assets/{index-wV6iGdpY.js → index-Db3CpWTr.js} +1 -1
- package/src/assets/web-panel/assets/{index-BMLHv389.js → index-DkD1K-fF.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9WYSn4m.js → index-DmPJ6T6F.js} +1 -1
- package/src/assets/web-panel/assets/{index-uCbEN7cY.js → index-DmVUeWUk.js} +1 -1
- package/src/assets/web-panel/assets/{index-DmkMXwnR.js → index-Dzdr0zDf.js} +1 -1
- package/src/assets/web-panel/assets/{index-CZ6Yt17L.js → index-PBLzwYnu.js} +1 -1
- package/src/assets/web-panel/assets/{index-CE2DdTP2.js → index-bFCaM7Eq.js} +1 -1
- package/src/assets/web-panel/assets/index-e4i-VLBd.js +1 -0
- package/src/assets/web-panel/assets/{index-GwdRUZ-x.js → index-h3KJZr95.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2vS66eI.js → index-iHcPtQ2e.js} +1 -1
- package/src/assets/web-panel/assets/{index-6GmBa4Kg.js → index-jzlzm4VK.js} +1 -1
- package/src/assets/web-panel/assets/{index-D9vU25ij.js → index-o14NNnxw.js} +1 -1
- package/src/assets/web-panel/assets/{index-CGjmILDm.js → index-tYPSfGbd.js} +1 -1
- package/src/assets/web-panel/assets/{index-LvCBfTLo.js → index-ytp2H5Zx.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-V5Fo95FM.js → initDefaultProps-D2Iut83B.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BYWU0hUT.js → motion-BuaJVdK9.js} +1 -1
- package/src/assets/web-panel/assets/{move-Cr1M-J2h.js → move-CaEuM92y.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BysQKi2N.js → mtc-parser-wuKNBwys.js} +1 -1
- package/src/assets/web-panel/assets/{omit-D1zHU8GC.js → omit-7a3eklaq.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CUArxtfi.js → pickAttrs-Dn4z7KvJ.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-ChoYEys0.js → placementArrow-DVn_qT4-.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-BQRs3Mo7.js → responsiveObserve-Dodz1OMQ.js} +1 -1
- package/src/assets/web-panel/assets/{slide-B8zbsCkg.js → slide-CxoGskbp.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-C2U8Gs6h.js → statusUtils-DBh_epQH.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Dd8MgL_V.js → styleChecker-RXQ4gG5P.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-hknqY5NH.js → useFlexGapSupport-1Wxm0HEG.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CLTAmcfw.js → useFs-CI_ySNxJ.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-ByegHbiB.js → usePersonalDataHub-C6973rZP.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DqlAy5DP.js → vnode-CalynwKr.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DiXiztQn.js → zoom-CM1cd-Q0.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +74 -1
- package/src/commands/agent.js +9 -0
- package/src/commands/background-session.js +60 -1
- package/src/commands/batch.js +160 -5
- package/src/commands/plugin.js +318 -43
- package/src/commands/session.js +44 -1
- package/src/commands/team.js +212 -6
- package/src/data/changelog.json +26 -0
- package/src/harness/jsonl-session-store.js +59 -6
- package/src/harness/mcp-client.js +65 -43
- package/src/lib/agent-batch.js +4 -1
- package/src/lib/agent-team/team-runner.js +19 -1
- package/src/lib/agent-team/team-worktree.js +3 -0
- package/src/lib/async-hook-supervisor.cjs +82 -10
- package/src/lib/async-hook-supervisor.js +50 -4
- package/src/lib/background-agent-supervisor.js +49 -0
- package/src/lib/collaboration-run-store.js +362 -0
- package/src/lib/hook-runner.cjs +98 -17
- package/src/lib/hook-runner.js +77 -6
- package/src/lib/hook-shell-command.cjs +148 -0
- package/src/lib/hooks-v2-runtime.js +188 -77
- package/src/lib/jsonl-session-store.js +2 -0
- package/src/lib/lsp/__tests__/lsp-client.test.js +18 -0
- package/src/lib/lsp/lsp-client.js +12 -1
- package/src/lib/plugin-monitor-supervisor.js +53 -16
- package/src/lib/plugin-runtime/bin.js +1 -1
- package/src/lib/plugin-runtime/install.js +465 -54
- package/src/lib/plugin-runtime/mcp.js +21 -2
- package/src/lib/plugin-runtime/monitors.js +19 -3
- package/src/lib/plugin-runtime/sandbox-policy.js +99 -0
- package/src/lib/plugin-runtime/scopes.js +13 -2
- package/src/lib/plugin-usage-attribution.js +44 -0
- package/src/lib/process-execution-broker/index.js +705 -13
- package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +1347 -0
- package/src/lib/process-execution-broker/linux-generic-bwrap.js +1260 -0
- package/src/lib/process-execution-broker/platform-sandbox.js +1831 -157
- package/src/lib/session-usage.js +278 -1
- package/src/lib/stream-retry.js +29 -0
- package/src/repl/agent-repl.js +38 -9
- package/src/runtime/agent-core.js +32 -4
- package/src/runtime/headless-runner.js +46 -1
- package/src/runtime/headless-stream.js +54 -3
- package/src/runtime/mcp-config.js +15 -5
- package/src/assets/web-panel/assets/ChatBubbleRenderer-PR074hUN.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-GlJtqLjT.js +0 -1
- package/src/assets/web-panel/assets/Projects-Qx_MdX9k.js +0 -1
- package/src/assets/web-panel/assets/Skills-Cb8jWjMX.js +0 -1
- package/src/assets/web-panel/assets/Tasks-CyuLnDFR.js +0 -1
- package/src/assets/web-panel/assets/Terminal-DwzGMniO.js +0 -3
- package/src/assets/web-panel/assets/devWarning-DgVKduH2.js +0 -1
- package/src/assets/web-panel/assets/index-DOZTSoS7.js +0 -1
- package/src/assets/web-panel/assets/index-DhpvjpqI.js +0 -1
|
@@ -0,0 +1,1260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linux generic workspace-command sandbox contract and bubblewrap planner.
|
|
3
|
+
*
|
|
4
|
+
* This module is intentionally independent from platform-sandbox.js so the
|
|
5
|
+
* Broker can adopt it without weakening the narrow Plugin-bin contract. A raw
|
|
6
|
+
* manifest policy can request filesystem/network boundaries, but it cannot
|
|
7
|
+
* nominate a writable root: only an exact object issued here is accepted, and
|
|
8
|
+
* every contract is single-use.
|
|
9
|
+
*
|
|
10
|
+
* The planner starts from bubblewrap's empty root. It admits only:
|
|
11
|
+
* - one descriptor-bound writable workspace;
|
|
12
|
+
* - anonymous tmpfs scratch locations;
|
|
13
|
+
* - descriptor-bound, root-owned read-only system runtime directories; and
|
|
14
|
+
* - a small exact /etc file allowlist.
|
|
15
|
+
*
|
|
16
|
+
* It never maps the host root or HOME. Network isolation always combines a new
|
|
17
|
+
* network namespace with the repository's deny-socket seccomp policy.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import crypto from "node:crypto";
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import os from "node:os";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
|
|
25
|
+
export const LINUX_GENERIC_BWRAP_BACKEND = "linux-bwrap-workspace";
|
|
26
|
+
export const LINUX_GENERIC_CONTRACT_KIND = "strict-workspace-command";
|
|
27
|
+
|
|
28
|
+
const SUPPORTED_BOUNDARIES = new Set(["filesystem", "network"]);
|
|
29
|
+
const SYSTEM_RUNTIME_DESTINATIONS = new Set([
|
|
30
|
+
"/usr",
|
|
31
|
+
"/bin",
|
|
32
|
+
"/sbin",
|
|
33
|
+
"/lib",
|
|
34
|
+
"/lib32",
|
|
35
|
+
"/lib64",
|
|
36
|
+
]);
|
|
37
|
+
const SYSTEM_RUNTIME_SYMLINKS = new Map([
|
|
38
|
+
["/bin", new Set(["usr/bin"])],
|
|
39
|
+
["/sbin", new Set(["usr/sbin"])],
|
|
40
|
+
["/lib", new Set(["usr/lib"])],
|
|
41
|
+
["/lib32", new Set(["usr/lib32"])],
|
|
42
|
+
["/lib64", new Set(["usr/lib64"])],
|
|
43
|
+
]);
|
|
44
|
+
const ETC_FILE_ALLOWLIST = new Set([
|
|
45
|
+
"/etc/group",
|
|
46
|
+
"/etc/hosts",
|
|
47
|
+
"/etc/ld.so.cache",
|
|
48
|
+
"/etc/nsswitch.conf",
|
|
49
|
+
"/etc/passwd",
|
|
50
|
+
]);
|
|
51
|
+
const FORBIDDEN_EXACT_WORKSPACE_ROOTS = new Set([
|
|
52
|
+
"/",
|
|
53
|
+
"/home",
|
|
54
|
+
"/home/sandbox",
|
|
55
|
+
"/media",
|
|
56
|
+
"/mnt",
|
|
57
|
+
"/opt",
|
|
58
|
+
"/root",
|
|
59
|
+
"/run",
|
|
60
|
+
"/srv",
|
|
61
|
+
"/tmp",
|
|
62
|
+
"/var",
|
|
63
|
+
"/var/tmp",
|
|
64
|
+
]);
|
|
65
|
+
const FORBIDDEN_WORKSPACE_SUBTREES = [
|
|
66
|
+
"/bin",
|
|
67
|
+
"/boot",
|
|
68
|
+
"/dev",
|
|
69
|
+
"/etc",
|
|
70
|
+
"/lib",
|
|
71
|
+
"/lib32",
|
|
72
|
+
"/lib64",
|
|
73
|
+
"/proc",
|
|
74
|
+
"/sbin",
|
|
75
|
+
"/sys",
|
|
76
|
+
"/usr",
|
|
77
|
+
];
|
|
78
|
+
const ENV_DENYLIST = new Set([
|
|
79
|
+
"BASH_ENV",
|
|
80
|
+
"CDPATH",
|
|
81
|
+
"ENV",
|
|
82
|
+
"GCONV_PATH",
|
|
83
|
+
"GLIBC_TUNABLES",
|
|
84
|
+
"LD_AUDIT",
|
|
85
|
+
"LD_DEBUG",
|
|
86
|
+
"LD_LIBRARY_PATH",
|
|
87
|
+
"LD_PRELOAD",
|
|
88
|
+
"NODE_OPTIONS",
|
|
89
|
+
"NODE_PATH",
|
|
90
|
+
"PERL5OPT",
|
|
91
|
+
"PYTHONHOME",
|
|
92
|
+
"PYTHONPATH",
|
|
93
|
+
"RUBYOPT",
|
|
94
|
+
"SHELLOPTS",
|
|
95
|
+
]);
|
|
96
|
+
const CONTRACT_VERSION = 1;
|
|
97
|
+
const PLAN_VERSION = 1;
|
|
98
|
+
const SANDBOX_HOME = "/home/sandbox";
|
|
99
|
+
const SUPERVISOR_STAGING_PATH = "/run/.chainless-bwrap-supervisor";
|
|
100
|
+
const SCRATCH_MOUNTS = ["/tmp", "/run", "/var/tmp", SANDBOX_HOME];
|
|
101
|
+
|
|
102
|
+
const issuedContracts = new WeakMap();
|
|
103
|
+
const admittedContracts = new WeakMap();
|
|
104
|
+
const plannedPolicies = new WeakMap();
|
|
105
|
+
|
|
106
|
+
function sha256(value) {
|
|
107
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function stableJson(value) {
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
return `[${value.map((entry) => stableJson(entry)).join(",")}]`;
|
|
113
|
+
}
|
|
114
|
+
if (value && typeof value === "object") {
|
|
115
|
+
return `{${Object.keys(value)
|
|
116
|
+
.sort()
|
|
117
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
118
|
+
.join(",")}}`;
|
|
119
|
+
}
|
|
120
|
+
return JSON.stringify(value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function freezeIdentity(identity) {
|
|
124
|
+
return Object.freeze({
|
|
125
|
+
realPath: identity.realPath,
|
|
126
|
+
fileId: Object.freeze({
|
|
127
|
+
dev: String(identity.fileId.dev),
|
|
128
|
+
ino: String(identity.fileId.ino),
|
|
129
|
+
}),
|
|
130
|
+
mode: Number(identity.mode),
|
|
131
|
+
uid: Number(identity.uid),
|
|
132
|
+
gid: Number(identity.gid),
|
|
133
|
+
attestation: identity.attestation || "canonical-directory-file-id",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function sameIdentity(left, right) {
|
|
138
|
+
return (
|
|
139
|
+
left?.realPath === right?.realPath &&
|
|
140
|
+
String(left?.fileId?.dev) === String(right?.fileId?.dev) &&
|
|
141
|
+
String(left?.fileId?.ino) === String(right?.fileId?.ino) &&
|
|
142
|
+
Number(left?.mode) === Number(right?.mode) &&
|
|
143
|
+
Number(left?.uid) === Number(right?.uid) &&
|
|
144
|
+
Number(left?.gid) === Number(right?.gid)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isWithin(root, target) {
|
|
149
|
+
const relative = path.posix.relative(root, target);
|
|
150
|
+
return (
|
|
151
|
+
relative === "" ||
|
|
152
|
+
(relative !== ".." &&
|
|
153
|
+
!relative.startsWith("../") &&
|
|
154
|
+
!path.posix.isAbsolute(relative))
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function assertString(value, label) {
|
|
159
|
+
if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
|
|
160
|
+
throw new TypeError(`${label} must be a non-empty NUL-free string`);
|
|
161
|
+
}
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function normalizeArgs(args) {
|
|
166
|
+
if (!Array.isArray(args)) {
|
|
167
|
+
throw new TypeError("args must be an array");
|
|
168
|
+
}
|
|
169
|
+
return Object.freeze(
|
|
170
|
+
args.map((value, index) => assertString(String(value), `args[${index}]`)),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function normalizeBoundaries(boundaries) {
|
|
175
|
+
if (!Array.isArray(boundaries) || boundaries.length === 0) {
|
|
176
|
+
throw new TypeError("requiredBoundaries must be a non-empty array");
|
|
177
|
+
}
|
|
178
|
+
const normalized = [...new Set(boundaries)].sort();
|
|
179
|
+
if (
|
|
180
|
+
normalized.some(
|
|
181
|
+
(boundary) =>
|
|
182
|
+
typeof boundary !== "string" || !SUPPORTED_BOUNDARIES.has(boundary),
|
|
183
|
+
)
|
|
184
|
+
) {
|
|
185
|
+
throw new TypeError(
|
|
186
|
+
"requiredBoundaries may contain only filesystem and network",
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return Object.freeze(normalized);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizeShell(shell) {
|
|
193
|
+
if (shell === undefined || shell === null || shell === false) return false;
|
|
194
|
+
throw new TypeError("strong Linux workspace sandbox requires shell:false");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeStdio(stdio) {
|
|
198
|
+
let entries;
|
|
199
|
+
if (stdio === undefined || stdio === null || stdio === "pipe") {
|
|
200
|
+
entries = ["pipe", "pipe", "pipe"];
|
|
201
|
+
} else if (stdio === "ignore") {
|
|
202
|
+
entries = ["ignore", "ignore", "ignore"];
|
|
203
|
+
} else if (Array.isArray(stdio)) {
|
|
204
|
+
entries = [...stdio];
|
|
205
|
+
while (entries.length < 3) entries.push(undefined);
|
|
206
|
+
} else {
|
|
207
|
+
throw new TypeError(
|
|
208
|
+
"strong Linux workspace sandbox requires pipe/ignore/array stdio",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let ipcCount = 0;
|
|
213
|
+
const normalized = entries.map((entry, index) => {
|
|
214
|
+
if (entry === undefined || entry === null) {
|
|
215
|
+
return index < 3 ? "pipe" : "ignore";
|
|
216
|
+
}
|
|
217
|
+
if (entry === "pipe" || entry === "ignore") return entry;
|
|
218
|
+
if (entry === "ipc" && index >= 3) {
|
|
219
|
+
ipcCount += 1;
|
|
220
|
+
return entry;
|
|
221
|
+
}
|
|
222
|
+
throw new TypeError(
|
|
223
|
+
`strong Linux workspace sandbox rejects inherited/numeric stdio at fd ${index}`,
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
if (ipcCount > 1) {
|
|
227
|
+
throw new TypeError("strong Linux workspace sandbox allows one IPC fd");
|
|
228
|
+
}
|
|
229
|
+
return Object.freeze(normalized);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function defaultWorkspaceAttestation(workspaceRoot, cwd) {
|
|
233
|
+
const resolve = (value) => path.resolve(value);
|
|
234
|
+
const canonical = (value) => fs.realpathSync.native(resolve(value));
|
|
235
|
+
const requestedRoot = resolve(workspaceRoot);
|
|
236
|
+
const canonicalRoot = canonical(requestedRoot);
|
|
237
|
+
if (requestedRoot !== canonicalRoot) {
|
|
238
|
+
throw new Error("workspace root must be canonical and symlink-free");
|
|
239
|
+
}
|
|
240
|
+
const requestedCwd = resolve(cwd);
|
|
241
|
+
const canonicalCwd = canonical(requestedCwd);
|
|
242
|
+
if (requestedCwd !== canonicalCwd) {
|
|
243
|
+
throw new Error("working directory must be canonical and symlink-free");
|
|
244
|
+
}
|
|
245
|
+
const rootStat = fs.statSync(canonicalRoot);
|
|
246
|
+
const cwdStat = fs.statSync(canonicalCwd);
|
|
247
|
+
if (!rootStat.isDirectory() || !cwdStat.isDirectory()) {
|
|
248
|
+
throw new Error("workspace root and cwd must be directories");
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
workspaceRoot: canonicalRoot.replaceAll("\\", "/"),
|
|
252
|
+
workingDirectory: canonicalCwd.replaceAll("\\", "/"),
|
|
253
|
+
rootIdentity: {
|
|
254
|
+
realPath: canonicalRoot.replaceAll("\\", "/"),
|
|
255
|
+
fileId: { dev: String(rootStat.dev), ino: String(rootStat.ino) },
|
|
256
|
+
mode: rootStat.mode,
|
|
257
|
+
uid: rootStat.uid,
|
|
258
|
+
gid: rootStat.gid,
|
|
259
|
+
},
|
|
260
|
+
cwdIdentity: {
|
|
261
|
+
realPath: canonicalCwd.replaceAll("\\", "/"),
|
|
262
|
+
fileId: { dev: String(cwdStat.dev), ino: String(cwdStat.ino) },
|
|
263
|
+
mode: cwdStat.mode,
|
|
264
|
+
uid: cwdStat.uid,
|
|
265
|
+
gid: cwdStat.gid,
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function validateWorkspaceAttestation(attestation, homedir) {
|
|
271
|
+
const root = assertString(attestation?.workspaceRoot, "workspaceRoot");
|
|
272
|
+
const cwd = assertString(attestation?.workingDirectory, "workingDirectory");
|
|
273
|
+
if (!path.posix.isAbsolute(root) || !path.posix.isAbsolute(cwd)) {
|
|
274
|
+
throw new Error("workspace root and cwd must be absolute POSIX paths");
|
|
275
|
+
}
|
|
276
|
+
const canonicalHome =
|
|
277
|
+
typeof homedir === "string" && path.posix.isAbsolute(homedir)
|
|
278
|
+
? path.posix.normalize(homedir)
|
|
279
|
+
: null;
|
|
280
|
+
if (
|
|
281
|
+
FORBIDDEN_EXACT_WORKSPACE_ROOTS.has(root) ||
|
|
282
|
+
root === canonicalHome ||
|
|
283
|
+
FORBIDDEN_WORKSPACE_SUBTREES.some(
|
|
284
|
+
(blocked) => root === blocked || isWithin(blocked, root),
|
|
285
|
+
)
|
|
286
|
+
) {
|
|
287
|
+
throw new Error(`unsafe writable workspace root: ${root}`);
|
|
288
|
+
}
|
|
289
|
+
if (!isWithin(root, cwd)) {
|
|
290
|
+
throw new Error("working directory escapes the canonical workspace root");
|
|
291
|
+
}
|
|
292
|
+
for (const [label, identity, expectedPath] of [
|
|
293
|
+
["root", attestation.rootIdentity, root],
|
|
294
|
+
["cwd", attestation.cwdIdentity, cwd],
|
|
295
|
+
]) {
|
|
296
|
+
if (
|
|
297
|
+
identity?.realPath !== expectedPath ||
|
|
298
|
+
identity.fileId?.dev === undefined ||
|
|
299
|
+
identity.fileId?.ino === undefined ||
|
|
300
|
+
!Number.isSafeInteger(Number(identity.mode)) ||
|
|
301
|
+
(Number(identity.mode) & 0o170000) !== 0o040000
|
|
302
|
+
) {
|
|
303
|
+
throw new Error(`invalid ${label} directory attestation`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
workspaceRoot: root,
|
|
308
|
+
workingDirectory: cwd,
|
|
309
|
+
rootIdentity: freezeIdentity(attestation.rootIdentity),
|
|
310
|
+
cwdIdentity: freezeIdentity(attestation.cwdIdentity),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Issue a one-launch authority contract. Callers must supply a trusted
|
|
316
|
+
* workspace root; renderer/manifest values must not be forwarded here.
|
|
317
|
+
*/
|
|
318
|
+
export function issueLinuxGenericSandboxExecutionContract(
|
|
319
|
+
spec,
|
|
320
|
+
runtimeOverrides = {},
|
|
321
|
+
) {
|
|
322
|
+
const origin = assertString(spec?.origin, "origin");
|
|
323
|
+
const command = assertString(spec?.command, "command");
|
|
324
|
+
const args = normalizeArgs(spec?.args || []);
|
|
325
|
+
const shell = normalizeShell(spec?.shell);
|
|
326
|
+
const sync = spec?.sync === true;
|
|
327
|
+
const stdio = normalizeStdio(spec?.stdio);
|
|
328
|
+
const requiredBoundaries = normalizeBoundaries(spec?.requiredBoundaries);
|
|
329
|
+
if (
|
|
330
|
+
spec?.detached === true ||
|
|
331
|
+
spec?.uid !== undefined ||
|
|
332
|
+
spec?.gid !== undefined ||
|
|
333
|
+
spec?.argv0 !== undefined ||
|
|
334
|
+
spec?.serialization !== undefined
|
|
335
|
+
) {
|
|
336
|
+
throw new TypeError(
|
|
337
|
+
"strong Linux workspace sandbox rejects detached/identity/argv0/serialization overrides",
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
const attestWorkspace =
|
|
341
|
+
runtimeOverrides.attestWorkspace || defaultWorkspaceAttestation;
|
|
342
|
+
const attestation = validateWorkspaceAttestation(
|
|
343
|
+
attestWorkspace(spec?.workspaceRoot, spec?.cwd || spec?.workspaceRoot),
|
|
344
|
+
(runtimeOverrides.homedir || os.homedir)(),
|
|
345
|
+
);
|
|
346
|
+
const contractBinding = {
|
|
347
|
+
contractVersion: CONTRACT_VERSION,
|
|
348
|
+
kind: LINUX_GENERIC_CONTRACT_KIND,
|
|
349
|
+
workspaceRoot: attestation.workspaceRoot,
|
|
350
|
+
workingDirectory: attestation.workingDirectory,
|
|
351
|
+
rootIdentity: attestation.rootIdentity,
|
|
352
|
+
cwdIdentity: attestation.cwdIdentity,
|
|
353
|
+
origin,
|
|
354
|
+
command,
|
|
355
|
+
args,
|
|
356
|
+
shell,
|
|
357
|
+
sync,
|
|
358
|
+
stdio,
|
|
359
|
+
requiredBoundaries,
|
|
360
|
+
};
|
|
361
|
+
const contractDigest = sha256(stableJson(contractBinding));
|
|
362
|
+
const contract = Object.freeze({
|
|
363
|
+
contractVersion: CONTRACT_VERSION,
|
|
364
|
+
kind: LINUX_GENERIC_CONTRACT_KIND,
|
|
365
|
+
workspaceRoot: attestation.workspaceRoot,
|
|
366
|
+
workingDirectory: attestation.workingDirectory,
|
|
367
|
+
rootIdentity: attestation.rootIdentity,
|
|
368
|
+
cwdIdentity: attestation.cwdIdentity,
|
|
369
|
+
contractDigest,
|
|
370
|
+
});
|
|
371
|
+
issuedContracts.set(
|
|
372
|
+
contract,
|
|
373
|
+
Object.freeze({
|
|
374
|
+
...contractBinding,
|
|
375
|
+
contractDigest,
|
|
376
|
+
}),
|
|
377
|
+
);
|
|
378
|
+
return contract;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function issuedBindingMatches(issued, provenance = {}) {
|
|
382
|
+
if (!issued) return false;
|
|
383
|
+
let args;
|
|
384
|
+
let boundaries;
|
|
385
|
+
let shell;
|
|
386
|
+
let stdio;
|
|
387
|
+
try {
|
|
388
|
+
args = normalizeArgs(provenance.args || []);
|
|
389
|
+
boundaries = normalizeBoundaries(provenance.requiredBoundaries);
|
|
390
|
+
shell = normalizeShell(provenance.shell);
|
|
391
|
+
stdio = normalizeStdio(provenance.stdio);
|
|
392
|
+
} catch {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
return (
|
|
396
|
+
provenance.origin === issued.origin &&
|
|
397
|
+
provenance.command === issued.command &&
|
|
398
|
+
provenance.cwd === issued.workingDirectory &&
|
|
399
|
+
provenance.sync === issued.sync &&
|
|
400
|
+
shell === issued.shell &&
|
|
401
|
+
args.length === issued.args.length &&
|
|
402
|
+
args.every((value, index) => value === issued.args[index]) &&
|
|
403
|
+
stdio.length === issued.stdio.length &&
|
|
404
|
+
stdio.every((value, index) => value === issued.stdio[index]) &&
|
|
405
|
+
issued.requiredBoundaries.every((boundary) =>
|
|
406
|
+
boundaries.includes(boundary),
|
|
407
|
+
) &&
|
|
408
|
+
boundaries.every((boundary) => SUPPORTED_BOUNDARIES.has(boundary))
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function verifyIssuedLinuxGenericSandboxExecutionContract(
|
|
413
|
+
contract,
|
|
414
|
+
provenance = {},
|
|
415
|
+
) {
|
|
416
|
+
return issuedBindingMatches(issuedContracts.get(contract), provenance);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function consumeIssuedLinuxGenericSandboxExecutionContract(
|
|
420
|
+
contract,
|
|
421
|
+
provenance = {},
|
|
422
|
+
) {
|
|
423
|
+
const issued = issuedContracts.get(contract);
|
|
424
|
+
if (!issuedBindingMatches(issued, provenance)) return null;
|
|
425
|
+
issuedContracts.delete(contract);
|
|
426
|
+
return issued;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Broker admission consumes the public one-launch object before permission or
|
|
431
|
+
* credential processing can return early. The platform adapter receives only
|
|
432
|
+
* this second opaque object, which is also single-use.
|
|
433
|
+
*/
|
|
434
|
+
export function admitLinuxGenericSandboxExecutionContract(
|
|
435
|
+
contract,
|
|
436
|
+
provenance = {},
|
|
437
|
+
) {
|
|
438
|
+
const issued = consumeIssuedLinuxGenericSandboxExecutionContract(
|
|
439
|
+
contract,
|
|
440
|
+
provenance,
|
|
441
|
+
);
|
|
442
|
+
if (!issued) return null;
|
|
443
|
+
const admitted = Object.freeze({
|
|
444
|
+
contractVersion: CONTRACT_VERSION,
|
|
445
|
+
kind: LINUX_GENERIC_CONTRACT_KIND,
|
|
446
|
+
contractDigest: issued.contractDigest,
|
|
447
|
+
workspaceRoot: issued.workspaceRoot,
|
|
448
|
+
workingDirectory: issued.workingDirectory,
|
|
449
|
+
});
|
|
450
|
+
admittedContracts.set(admitted, issued);
|
|
451
|
+
return admitted;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function consumePlanningAuthority(contract, provenance) {
|
|
455
|
+
const admitted = admittedContracts.get(contract);
|
|
456
|
+
if (admitted) {
|
|
457
|
+
admittedContracts.delete(contract);
|
|
458
|
+
return issuedBindingMatches(admitted, provenance) ? admitted : null;
|
|
459
|
+
}
|
|
460
|
+
return consumeIssuedLinuxGenericSandboxExecutionContract(
|
|
461
|
+
contract,
|
|
462
|
+
provenance,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function unavailablePlan(reason, requiredBoundaries = [], cleanup) {
|
|
467
|
+
cleanup?.();
|
|
468
|
+
return Object.freeze({
|
|
469
|
+
contractVersion: PLAN_VERSION,
|
|
470
|
+
applied: false,
|
|
471
|
+
platform: "linux",
|
|
472
|
+
profile: "strict",
|
|
473
|
+
command: "",
|
|
474
|
+
args: Object.freeze([]),
|
|
475
|
+
options: Object.freeze({}),
|
|
476
|
+
enforcement: null,
|
|
477
|
+
backend: null,
|
|
478
|
+
candidateBackend: LINUX_GENERIC_BWRAP_BACKEND,
|
|
479
|
+
guarantees: Object.freeze([]),
|
|
480
|
+
requiredBoundaries: Object.freeze([...requiredBoundaries]),
|
|
481
|
+
policyAttested: false,
|
|
482
|
+
policyDigest: null,
|
|
483
|
+
reason,
|
|
484
|
+
postSpawn: Object.freeze({ required: false, mode: "none" }),
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function validateRootOwnedIdentity(identity, { directory }) {
|
|
489
|
+
const expectedType = directory ? 0o040000 : 0o100000;
|
|
490
|
+
return (
|
|
491
|
+
identity &&
|
|
492
|
+
identity.fileId?.dev !== undefined &&
|
|
493
|
+
identity.fileId?.ino !== undefined &&
|
|
494
|
+
Number(identity.uid) === 0 &&
|
|
495
|
+
Number.isSafeInteger(Number(identity.mode)) &&
|
|
496
|
+
(Number(identity.mode) & 0o170000) === expectedType &&
|
|
497
|
+
(Number(identity.mode) & 0o022) === 0
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function expectedTargetInvocation(issued) {
|
|
502
|
+
return {
|
|
503
|
+
requestedCommand: issued.command,
|
|
504
|
+
args: issued.args,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function validateWorkspaceMountTopology(value, issued) {
|
|
509
|
+
if (
|
|
510
|
+
!value ||
|
|
511
|
+
value.version !== 1 ||
|
|
512
|
+
value.source !== "proc-self-mountinfo" ||
|
|
513
|
+
value.workspaceRoot !== issued.workspaceRoot ||
|
|
514
|
+
!/^[a-f0-9]{64}$/.test(value.digest || "") ||
|
|
515
|
+
!Number.isSafeInteger(value.lineageEntryCount) ||
|
|
516
|
+
value.lineageEntryCount < 1 ||
|
|
517
|
+
!Number.isSafeInteger(value.filesystemEntryCount) ||
|
|
518
|
+
value.filesystemEntryCount < 1 ||
|
|
519
|
+
!Number.isSafeInteger(value.aliasCount) ||
|
|
520
|
+
value.aliasCount < 1 ||
|
|
521
|
+
!Number.isSafeInteger(value.forbiddenIdentityCount) ||
|
|
522
|
+
value.forbiddenIdentityCount < 1 ||
|
|
523
|
+
value.strictDescendantMountsAtAttestation !== 0 ||
|
|
524
|
+
value.rootAliasAttested !== true ||
|
|
525
|
+
value.recursiveBind !== true ||
|
|
526
|
+
value.mountTopologyAtomic !== false
|
|
527
|
+
) {
|
|
528
|
+
throw new Error(
|
|
529
|
+
"workspace mount topology is missing or permits host aliases",
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
return Object.freeze({
|
|
533
|
+
version: value.version,
|
|
534
|
+
source: value.source,
|
|
535
|
+
workspaceRoot: value.workspaceRoot,
|
|
536
|
+
digest: value.digest,
|
|
537
|
+
lineageEntryCount: value.lineageEntryCount,
|
|
538
|
+
filesystemEntryCount: value.filesystemEntryCount,
|
|
539
|
+
aliasCount: value.aliasCount,
|
|
540
|
+
forbiddenIdentityCount: value.forbiddenIdentityCount,
|
|
541
|
+
strictDescendantMountsAtAttestation:
|
|
542
|
+
value.strictDescendantMountsAtAttestation,
|
|
543
|
+
rootAliasAttested: value.rootAliasAttested,
|
|
544
|
+
recursiveBind: value.recursiveBind,
|
|
545
|
+
mountTopologyAtomic: value.mountTopologyAtomic,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function validateResourceContract(resources, issued) {
|
|
550
|
+
if (
|
|
551
|
+
!resources ||
|
|
552
|
+
typeof resources !== "object" ||
|
|
553
|
+
resources.attestedContractDigest !== issued.contractDigest ||
|
|
554
|
+
typeof resources.attestContract !== "function" ||
|
|
555
|
+
resources.attestContract(issued) !== true
|
|
556
|
+
) {
|
|
557
|
+
throw new Error("generic workspace resource attestation is missing");
|
|
558
|
+
}
|
|
559
|
+
if (
|
|
560
|
+
!sameIdentity(resources.workspace?.identity, issued.rootIdentity) ||
|
|
561
|
+
!Number.isInteger(resources.workspace?.probeFd) ||
|
|
562
|
+
!Number.isInteger(resources.workspace?.finalFd)
|
|
563
|
+
) {
|
|
564
|
+
throw new Error("workspace descriptor does not match contract authority");
|
|
565
|
+
}
|
|
566
|
+
const workspaceMountTopology = validateWorkspaceMountTopology(
|
|
567
|
+
resources.workspace.mountTopology,
|
|
568
|
+
issued,
|
|
569
|
+
);
|
|
570
|
+
if (
|
|
571
|
+
!validateRootOwnedIdentity(resources.supervisor?.identity, {
|
|
572
|
+
directory: false,
|
|
573
|
+
}) ||
|
|
574
|
+
!/^[a-f0-9]{64}$/.test(resources.supervisor?.sha256 || "") ||
|
|
575
|
+
!Number.isSafeInteger(resources.supervisor?.bytes) ||
|
|
576
|
+
resources.supervisor.bytes <= 0 ||
|
|
577
|
+
!Number.isInteger(resources.supervisor?.probeFd) ||
|
|
578
|
+
!Number.isInteger(resources.supervisor?.finalFd)
|
|
579
|
+
) {
|
|
580
|
+
throw new Error("bubblewrap supervisor is not root-owned and pinned");
|
|
581
|
+
}
|
|
582
|
+
const system = Array.isArray(resources.system) ? resources.system : [];
|
|
583
|
+
if (!system.some((entry) => entry.destination === "/usr")) {
|
|
584
|
+
throw new Error("root-owned /usr runtime mount is required");
|
|
585
|
+
}
|
|
586
|
+
for (const entry of system) {
|
|
587
|
+
if (
|
|
588
|
+
!SYSTEM_RUNTIME_DESTINATIONS.has(entry.destination) ||
|
|
589
|
+
!validateRootOwnedIdentity(entry.identity, { directory: true }) ||
|
|
590
|
+
!Number.isInteger(entry.probeFd) ||
|
|
591
|
+
!Number.isInteger(entry.finalFd)
|
|
592
|
+
) {
|
|
593
|
+
throw new Error("invalid root-owned system runtime descriptor");
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const symlinks = Array.isArray(resources.systemSymlinks)
|
|
597
|
+
? resources.systemSymlinks
|
|
598
|
+
: [];
|
|
599
|
+
for (const entry of symlinks) {
|
|
600
|
+
if (!SYSTEM_RUNTIME_SYMLINKS.get(entry.destination)?.has(entry.target)) {
|
|
601
|
+
throw new Error("invalid system runtime symlink");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
const etc = Array.isArray(resources.etc) ? resources.etc : [];
|
|
605
|
+
for (const entry of etc) {
|
|
606
|
+
if (
|
|
607
|
+
!ETC_FILE_ALLOWLIST.has(entry.destination) ||
|
|
608
|
+
!validateRootOwnedIdentity(entry.identity, { directory: false }) ||
|
|
609
|
+
!Number.isInteger(entry.probeFd) ||
|
|
610
|
+
!Number.isInteger(entry.finalFd)
|
|
611
|
+
) {
|
|
612
|
+
throw new Error("invalid exact /etc runtime file descriptor");
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (
|
|
616
|
+
resources.seccomp?.policy !== "deny-network-creation" ||
|
|
617
|
+
!/^[a-f0-9]{64}$/.test(resources.seccomp?.sha256 || "") ||
|
|
618
|
+
!Number.isInteger(resources.seccomp?.probeFd) ||
|
|
619
|
+
!Number.isInteger(resources.seccomp?.finalFd)
|
|
620
|
+
) {
|
|
621
|
+
throw new Error("network seccomp descriptor is missing or unattested");
|
|
622
|
+
}
|
|
623
|
+
const target = resources.target;
|
|
624
|
+
const expectedInvocation = expectedTargetInvocation(issued);
|
|
625
|
+
if (
|
|
626
|
+
!target ||
|
|
627
|
+
target.attestedContractDigest !== issued.contractDigest ||
|
|
628
|
+
target.requestedCommand !== expectedInvocation.requestedCommand ||
|
|
629
|
+
typeof target.resolvedCommand !== "string" ||
|
|
630
|
+
!path.posix.isAbsolute(target.resolvedCommand) ||
|
|
631
|
+
!Array.isArray(target.args) ||
|
|
632
|
+
target.args.length !== expectedInvocation.args.length ||
|
|
633
|
+
!target.args.every(
|
|
634
|
+
(value, index) =>
|
|
635
|
+
typeof value === "string" && value === expectedInvocation.args[index],
|
|
636
|
+
)
|
|
637
|
+
) {
|
|
638
|
+
throw new Error("resolved target is not bound to the command contract");
|
|
639
|
+
}
|
|
640
|
+
if (target.scope === "workspace") {
|
|
641
|
+
if (!isWithin(issued.workspaceRoot, target.resolvedCommand)) {
|
|
642
|
+
throw new Error("workspace target escapes the authorized root");
|
|
643
|
+
}
|
|
644
|
+
} else if (
|
|
645
|
+
target.scope !== "system" ||
|
|
646
|
+
!validateRootOwnedIdentity(target.identity, { directory: false }) ||
|
|
647
|
+
(Number(target.identity.mode) & 0o111) === 0 ||
|
|
648
|
+
![...SYSTEM_RUNTIME_DESTINATIONS].some((root) =>
|
|
649
|
+
isWithin(root, target.resolvedCommand),
|
|
650
|
+
)
|
|
651
|
+
) {
|
|
652
|
+
throw new Error("target must be workspace-bound or root-owned system code");
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
supervisor: {
|
|
656
|
+
identity: resources.supervisor.identity,
|
|
657
|
+
sha256: resources.supervisor.sha256,
|
|
658
|
+
bytes: resources.supervisor.bytes,
|
|
659
|
+
},
|
|
660
|
+
system,
|
|
661
|
+
symlinks,
|
|
662
|
+
etc,
|
|
663
|
+
seccomp: {
|
|
664
|
+
policy: resources.seccomp.policy,
|
|
665
|
+
sha256: resources.seccomp.sha256,
|
|
666
|
+
},
|
|
667
|
+
workspaceMountTopology,
|
|
668
|
+
target,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function sanitizedEnvironment(environment, stdio) {
|
|
673
|
+
const out = {};
|
|
674
|
+
for (const [key, value] of Object.entries(environment || {})) {
|
|
675
|
+
if (
|
|
676
|
+
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) ||
|
|
677
|
+
ENV_DENYLIST.has(key) ||
|
|
678
|
+
value === undefined ||
|
|
679
|
+
value === null ||
|
|
680
|
+
String(value).includes("\0")
|
|
681
|
+
) {
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
out[key] = String(value);
|
|
685
|
+
}
|
|
686
|
+
out.HOME = SANDBOX_HOME;
|
|
687
|
+
out.TMPDIR = "/tmp";
|
|
688
|
+
out.PATH = "/usr/local/bin:/usr/bin:/bin";
|
|
689
|
+
out.OPENSSL_CONF = "/dev/null";
|
|
690
|
+
out.CHAINLESS_SANDBOXED = "1";
|
|
691
|
+
const ipcIndex = stdio.indexOf("ipc");
|
|
692
|
+
if (ipcIndex >= 3) {
|
|
693
|
+
out.NODE_CHANNEL_FD = String(ipcIndex);
|
|
694
|
+
out.NODE_CHANNEL_SERIALIZATION_MODE = "json";
|
|
695
|
+
}
|
|
696
|
+
return out;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function passthroughSpawnOptions(options) {
|
|
700
|
+
const out = {};
|
|
701
|
+
for (const key of [
|
|
702
|
+
"encoding",
|
|
703
|
+
"input",
|
|
704
|
+
"killSignal",
|
|
705
|
+
"maxBuffer",
|
|
706
|
+
"signal",
|
|
707
|
+
"timeout",
|
|
708
|
+
"windowsHide",
|
|
709
|
+
"windowsVerbatimArguments",
|
|
710
|
+
]) {
|
|
711
|
+
if (options?.[key] !== undefined) out[key] = options[key];
|
|
712
|
+
}
|
|
713
|
+
return out;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function directoryArguments(directoryDestinations, fileDestinations) {
|
|
717
|
+
const directories = new Set([
|
|
718
|
+
"/dev",
|
|
719
|
+
"/etc",
|
|
720
|
+
"/home",
|
|
721
|
+
SANDBOX_HOME,
|
|
722
|
+
"/proc",
|
|
723
|
+
"/run",
|
|
724
|
+
"/tmp",
|
|
725
|
+
"/var",
|
|
726
|
+
"/var/tmp",
|
|
727
|
+
]);
|
|
728
|
+
for (const destination of directoryDestinations) {
|
|
729
|
+
let current = destination;
|
|
730
|
+
while (current !== "/") {
|
|
731
|
+
directories.add(current);
|
|
732
|
+
current = path.posix.dirname(current);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
for (const destination of fileDestinations) {
|
|
736
|
+
let current = path.posix.dirname(destination);
|
|
737
|
+
while (current !== "/") {
|
|
738
|
+
directories.add(current);
|
|
739
|
+
current = path.posix.dirname(current);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return [...directories].sort((left, right) => {
|
|
743
|
+
const depth =
|
|
744
|
+
left.split("/").filter(Boolean).length -
|
|
745
|
+
right.split("/").filter(Boolean).length;
|
|
746
|
+
return depth || left.localeCompare(right);
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function descriptorLayout(stdio, resources, validated, workspaceRoot, phase) {
|
|
751
|
+
const descriptors = [
|
|
752
|
+
{
|
|
753
|
+
kind: "supervisor",
|
|
754
|
+
fd: resources.supervisor[`${phase}Fd`],
|
|
755
|
+
destination: SUPERVISOR_STAGING_PATH,
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
kind: "workspace",
|
|
759
|
+
fd: resources.workspace[`${phase}Fd`],
|
|
760
|
+
destination: workspaceRoot,
|
|
761
|
+
},
|
|
762
|
+
...validated.system.map((entry) => ({
|
|
763
|
+
kind: "system",
|
|
764
|
+
fd: entry[`${phase}Fd`],
|
|
765
|
+
destination: entry.destination,
|
|
766
|
+
})),
|
|
767
|
+
...validated.etc.map((entry) => ({
|
|
768
|
+
kind: "etc",
|
|
769
|
+
fd: entry[`${phase}Fd`],
|
|
770
|
+
destination: entry.destination,
|
|
771
|
+
})),
|
|
772
|
+
{
|
|
773
|
+
kind: "seccomp",
|
|
774
|
+
fd: resources.seccomp[`${phase}Fd`],
|
|
775
|
+
destination: null,
|
|
776
|
+
},
|
|
777
|
+
];
|
|
778
|
+
const seen = new Set();
|
|
779
|
+
for (const descriptor of descriptors) {
|
|
780
|
+
if (!Number.isInteger(descriptor.fd) || seen.has(descriptor.fd)) {
|
|
781
|
+
throw new Error("sandbox descriptor ownership is ambiguous");
|
|
782
|
+
}
|
|
783
|
+
seen.add(descriptor.fd);
|
|
784
|
+
}
|
|
785
|
+
return descriptors.map((descriptor, index) => ({
|
|
786
|
+
...descriptor,
|
|
787
|
+
childFd: stdio.length + index,
|
|
788
|
+
}));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function buildPolicyArgs({ issued, validated, descriptors, environment }) {
|
|
792
|
+
const byKind = (kind) =>
|
|
793
|
+
descriptors.filter((descriptor) => descriptor.kind === kind);
|
|
794
|
+
const supervisor = byKind("supervisor")[0];
|
|
795
|
+
const workspace = byKind("workspace")[0];
|
|
796
|
+
const seccomp = byKind("seccomp")[0];
|
|
797
|
+
const directoryDestinations = [
|
|
798
|
+
issued.workspaceRoot,
|
|
799
|
+
...validated.system.map((entry) => entry.destination),
|
|
800
|
+
];
|
|
801
|
+
const fileDestinations = validated.etc.map((entry) => entry.destination);
|
|
802
|
+
const directories = directoryArguments(
|
|
803
|
+
directoryDestinations,
|
|
804
|
+
fileDestinations,
|
|
805
|
+
);
|
|
806
|
+
const args = [
|
|
807
|
+
"--die-with-parent",
|
|
808
|
+
"--new-session",
|
|
809
|
+
"--unshare-user",
|
|
810
|
+
"--disable-userns",
|
|
811
|
+
"--assert-userns-disabled",
|
|
812
|
+
"--unshare-pid",
|
|
813
|
+
"--unshare-ipc",
|
|
814
|
+
"--unshare-net",
|
|
815
|
+
"--unshare-uts",
|
|
816
|
+
"--unshare-cgroup-try",
|
|
817
|
+
"--cap-drop",
|
|
818
|
+
"ALL",
|
|
819
|
+
"--hostname",
|
|
820
|
+
"chainless-sandbox",
|
|
821
|
+
"--clearenv",
|
|
822
|
+
];
|
|
823
|
+
for (const directory of directories) {
|
|
824
|
+
args.push("--dir", directory);
|
|
825
|
+
}
|
|
826
|
+
args.push(
|
|
827
|
+
"--perms",
|
|
828
|
+
"0000",
|
|
829
|
+
"--file",
|
|
830
|
+
String(supervisor.childFd),
|
|
831
|
+
SUPERVISOR_STAGING_PATH,
|
|
832
|
+
// Consuming the executable descriptor into a mode-0000 staging file keeps
|
|
833
|
+
// it out of the target descriptor table. The following /run tmpfs then
|
|
834
|
+
// deliberately hides that copy; bwrap itself was already execve'd through
|
|
835
|
+
// /proc/self/fd/N by the Broker.
|
|
836
|
+
);
|
|
837
|
+
for (const descriptor of byKind("system")) {
|
|
838
|
+
args.push(
|
|
839
|
+
"--ro-bind-fd",
|
|
840
|
+
String(descriptor.childFd),
|
|
841
|
+
descriptor.destination,
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
// Merged-/usr links must be created while the synthetic root is writable.
|
|
845
|
+
// Root is remounted read-only immediately after all declared system and
|
|
846
|
+
// exact-file mounts are installed.
|
|
847
|
+
for (const link of validated.symlinks) {
|
|
848
|
+
args.push("--symlink", link.target, link.destination);
|
|
849
|
+
}
|
|
850
|
+
for (const descriptor of byKind("etc")) {
|
|
851
|
+
args.push(
|
|
852
|
+
"--ro-bind-fd",
|
|
853
|
+
String(descriptor.childFd),
|
|
854
|
+
descriptor.destination,
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
args.push(
|
|
858
|
+
"--seccomp",
|
|
859
|
+
String(seccomp.childFd),
|
|
860
|
+
"--remount-ro",
|
|
861
|
+
"/",
|
|
862
|
+
"--perms",
|
|
863
|
+
"1777",
|
|
864
|
+
"--size",
|
|
865
|
+
String(64 * 1024 * 1024),
|
|
866
|
+
"--tmpfs",
|
|
867
|
+
"/tmp",
|
|
868
|
+
"--perms",
|
|
869
|
+
"0755",
|
|
870
|
+
"--size",
|
|
871
|
+
String(16 * 1024 * 1024),
|
|
872
|
+
"--tmpfs",
|
|
873
|
+
"/run",
|
|
874
|
+
"--perms",
|
|
875
|
+
"1777",
|
|
876
|
+
"--size",
|
|
877
|
+
String(32 * 1024 * 1024),
|
|
878
|
+
"--tmpfs",
|
|
879
|
+
"/var/tmp",
|
|
880
|
+
"--perms",
|
|
881
|
+
"0700",
|
|
882
|
+
"--size",
|
|
883
|
+
String(16 * 1024 * 1024),
|
|
884
|
+
"--tmpfs",
|
|
885
|
+
SANDBOX_HOME,
|
|
886
|
+
);
|
|
887
|
+
// A project may be a proper subtree of /tmp, /run, /var/tmp, or the
|
|
888
|
+
// anonymous HOME. Those tmpfs mounts replaced their pre-created children, so
|
|
889
|
+
// recreate only the authorized destination chain before binding the pinned
|
|
890
|
+
// workspace descriptor.
|
|
891
|
+
for (const directory of directories) {
|
|
892
|
+
if (
|
|
893
|
+
SCRATCH_MOUNTS.some(
|
|
894
|
+
(scratch) => directory !== scratch && isWithin(scratch, directory),
|
|
895
|
+
)
|
|
896
|
+
) {
|
|
897
|
+
args.push("--dir", directory);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
args.push("--bind-fd", String(workspace.childFd), issued.workspaceRoot);
|
|
901
|
+
args.push("--proc", "/proc", "--dev", "/dev");
|
|
902
|
+
for (const key of Object.keys(environment).sort()) {
|
|
903
|
+
args.push("--setenv", key, environment[key]);
|
|
904
|
+
}
|
|
905
|
+
args.push("--chdir", issued.workingDirectory);
|
|
906
|
+
return args;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function policyBinding({
|
|
910
|
+
issued,
|
|
911
|
+
validated,
|
|
912
|
+
environment,
|
|
913
|
+
requiredBoundaries,
|
|
914
|
+
policyArgs,
|
|
915
|
+
}) {
|
|
916
|
+
return {
|
|
917
|
+
version: PLAN_VERSION,
|
|
918
|
+
backend: LINUX_GENERIC_BWRAP_BACKEND,
|
|
919
|
+
contractDigest: issued.contractDigest,
|
|
920
|
+
supervisor: validated.supervisor,
|
|
921
|
+
workspace: {
|
|
922
|
+
root: issued.workspaceRoot,
|
|
923
|
+
cwd: issued.workingDirectory,
|
|
924
|
+
rootIdentity: issued.rootIdentity,
|
|
925
|
+
cwdIdentity: issued.cwdIdentity,
|
|
926
|
+
access: "read-write",
|
|
927
|
+
descriptorBound: true,
|
|
928
|
+
mountTopology: validated.workspaceMountTopology,
|
|
929
|
+
},
|
|
930
|
+
system: validated.system.map((entry) => ({
|
|
931
|
+
destination: entry.destination,
|
|
932
|
+
identity: entry.identity,
|
|
933
|
+
access: "read-only",
|
|
934
|
+
})),
|
|
935
|
+
systemSymlinks: validated.symlinks,
|
|
936
|
+
etc: validated.etc.map((entry) => ({
|
|
937
|
+
destination: entry.destination,
|
|
938
|
+
identity: entry.identity,
|
|
939
|
+
access: "read-only",
|
|
940
|
+
})),
|
|
941
|
+
network: {
|
|
942
|
+
namespace: "new",
|
|
943
|
+
seccomp: validated.seccomp,
|
|
944
|
+
},
|
|
945
|
+
target: {
|
|
946
|
+
requestedCommand: validated.target.requestedCommand,
|
|
947
|
+
command: validated.target.resolvedCommand,
|
|
948
|
+
args: validated.target.args,
|
|
949
|
+
scope: validated.target.scope,
|
|
950
|
+
...(validated.target.scope === "system"
|
|
951
|
+
? { identity: validated.target.identity }
|
|
952
|
+
: {}),
|
|
953
|
+
},
|
|
954
|
+
requiredBoundaries: [...requiredBoundaries].sort(),
|
|
955
|
+
environmentDigest: sha256(stableJson(environment)),
|
|
956
|
+
policyArgs,
|
|
957
|
+
hostRootMapped: false,
|
|
958
|
+
hostHomeMapped: false,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Build an exact descriptor-backed bubblewrap plan.
|
|
964
|
+
*
|
|
965
|
+
* `resources` is supplied by the platform adapter and must include an
|
|
966
|
+
* `attestContract()` callback plus independent probe/final descriptors. This is
|
|
967
|
+
* the seam used by unit tests; production callers receive resources only from
|
|
968
|
+
* the trusted platform adapter.
|
|
969
|
+
*/
|
|
970
|
+
export function planLinuxGenericBubblewrap(
|
|
971
|
+
{
|
|
972
|
+
contract,
|
|
973
|
+
provenance,
|
|
974
|
+
resources,
|
|
975
|
+
environment = process.env,
|
|
976
|
+
spawnOptions = {},
|
|
977
|
+
probe,
|
|
978
|
+
preflightFailure = null,
|
|
979
|
+
},
|
|
980
|
+
runtimeOverrides = {},
|
|
981
|
+
) {
|
|
982
|
+
const requiredBoundaries = provenance?.requiredBoundaries || [];
|
|
983
|
+
if ((runtimeOverrides.platform || os.platform()) !== "linux") {
|
|
984
|
+
return unavailablePlan("platform_mismatch", requiredBoundaries);
|
|
985
|
+
}
|
|
986
|
+
const issued = consumePlanningAuthority(contract, provenance);
|
|
987
|
+
if (!issued) {
|
|
988
|
+
return unavailablePlan(
|
|
989
|
+
"linux_generic_execution_contract_invalid",
|
|
990
|
+
requiredBoundaries,
|
|
991
|
+
resources?.cleanup,
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
if (typeof preflightFailure === "string" && preflightFailure) {
|
|
995
|
+
return unavailablePlan(
|
|
996
|
+
preflightFailure,
|
|
997
|
+
requiredBoundaries,
|
|
998
|
+
resources?.cleanup,
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const cleanup = () => {
|
|
1003
|
+
try {
|
|
1004
|
+
resources?.closeProbe?.();
|
|
1005
|
+
} catch {
|
|
1006
|
+
// Preserve the original fail-closed result.
|
|
1007
|
+
}
|
|
1008
|
+
try {
|
|
1009
|
+
resources?.cleanup?.();
|
|
1010
|
+
} catch {
|
|
1011
|
+
// Preserve the original fail-closed result.
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
let validated;
|
|
1015
|
+
let finalDescriptors;
|
|
1016
|
+
let probeDescriptors;
|
|
1017
|
+
let policyArgs;
|
|
1018
|
+
let targetEnvironment;
|
|
1019
|
+
try {
|
|
1020
|
+
validated = validateResourceContract(resources, issued);
|
|
1021
|
+
finalDescriptors = descriptorLayout(
|
|
1022
|
+
issued.stdio,
|
|
1023
|
+
resources,
|
|
1024
|
+
validated,
|
|
1025
|
+
issued.workspaceRoot,
|
|
1026
|
+
"final",
|
|
1027
|
+
);
|
|
1028
|
+
probeDescriptors = descriptorLayout(
|
|
1029
|
+
issued.stdio,
|
|
1030
|
+
resources,
|
|
1031
|
+
validated,
|
|
1032
|
+
issued.workspaceRoot,
|
|
1033
|
+
"probe",
|
|
1034
|
+
);
|
|
1035
|
+
targetEnvironment = sanitizedEnvironment(environment, issued.stdio);
|
|
1036
|
+
policyArgs = buildPolicyArgs({
|
|
1037
|
+
issued,
|
|
1038
|
+
validated,
|
|
1039
|
+
descriptors: finalDescriptors,
|
|
1040
|
+
environment: targetEnvironment,
|
|
1041
|
+
});
|
|
1042
|
+
} catch {
|
|
1043
|
+
return unavailablePlan(
|
|
1044
|
+
"linux_generic_resources_unattested",
|
|
1045
|
+
requiredBoundaries,
|
|
1046
|
+
cleanup,
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
const binding = policyBinding({
|
|
1050
|
+
issued,
|
|
1051
|
+
validated,
|
|
1052
|
+
environment: targetEnvironment,
|
|
1053
|
+
requiredBoundaries,
|
|
1054
|
+
policyArgs,
|
|
1055
|
+
});
|
|
1056
|
+
const policyDigest = sha256(stableJson(binding));
|
|
1057
|
+
const probePolicyArgs = buildPolicyArgs({
|
|
1058
|
+
issued,
|
|
1059
|
+
validated,
|
|
1060
|
+
descriptors: probeDescriptors,
|
|
1061
|
+
environment: targetEnvironment,
|
|
1062
|
+
});
|
|
1063
|
+
const probeCommand = `/proc/self/fd/${
|
|
1064
|
+
probeDescriptors.find((entry) => entry.kind === "supervisor").childFd
|
|
1065
|
+
}`;
|
|
1066
|
+
const probeOptions = {
|
|
1067
|
+
cwd: "/",
|
|
1068
|
+
shell: false,
|
|
1069
|
+
encoding: "utf8",
|
|
1070
|
+
stdio: [
|
|
1071
|
+
...issued.stdio.map((_, index) =>
|
|
1072
|
+
index === 1 || index === 2 ? "pipe" : "ignore",
|
|
1073
|
+
),
|
|
1074
|
+
...probeDescriptors.map((descriptor) => descriptor.fd),
|
|
1075
|
+
],
|
|
1076
|
+
env: { PATH: "/usr/bin:/bin", LANG: "C", LC_ALL: "C" },
|
|
1077
|
+
};
|
|
1078
|
+
let probeResult;
|
|
1079
|
+
try {
|
|
1080
|
+
probeResult =
|
|
1081
|
+
typeof probe === "function"
|
|
1082
|
+
? probe({
|
|
1083
|
+
command: probeCommand,
|
|
1084
|
+
args: probePolicyArgs,
|
|
1085
|
+
options: probeOptions,
|
|
1086
|
+
policyDigest,
|
|
1087
|
+
contractDigest: issued.contractDigest,
|
|
1088
|
+
})
|
|
1089
|
+
: null;
|
|
1090
|
+
} catch {
|
|
1091
|
+
probeResult = null;
|
|
1092
|
+
}
|
|
1093
|
+
try {
|
|
1094
|
+
resources.closeProbe?.();
|
|
1095
|
+
} catch {
|
|
1096
|
+
return unavailablePlan(
|
|
1097
|
+
"linux_generic_probe_cleanup_failed",
|
|
1098
|
+
requiredBoundaries,
|
|
1099
|
+
cleanup,
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
if (
|
|
1103
|
+
probeResult?.runnable !== true ||
|
|
1104
|
+
probeResult.policyDigest !== policyDigest ||
|
|
1105
|
+
probeResult.contractDigest !== issued.contractDigest ||
|
|
1106
|
+
probeResult.emptyRoot !== true ||
|
|
1107
|
+
probeResult.undeclaredRootReadOnly !== true ||
|
|
1108
|
+
probeResult.workspaceReadWrite !== true ||
|
|
1109
|
+
probeResult.workspaceMountTopologyAttested !== true ||
|
|
1110
|
+
probeResult.anonymousDevWritable !== true ||
|
|
1111
|
+
probeResult.systemReadOnly !== true ||
|
|
1112
|
+
probeResult.hostHomeHidden !== true ||
|
|
1113
|
+
probeResult.outsideMarkerHidden !== true ||
|
|
1114
|
+
probeResult.networkNamespace !== true ||
|
|
1115
|
+
probeResult.networkNamespaceChanged !== true ||
|
|
1116
|
+
probeResult.socketCreationDenied !== true
|
|
1117
|
+
) {
|
|
1118
|
+
return unavailablePlan(
|
|
1119
|
+
"linux_generic_policy_probe_failed",
|
|
1120
|
+
requiredBoundaries,
|
|
1121
|
+
cleanup,
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
let finalAttested = false;
|
|
1125
|
+
try {
|
|
1126
|
+
finalAttested = resources.attestFinal?.(issued) === true;
|
|
1127
|
+
} catch {
|
|
1128
|
+
finalAttested = false;
|
|
1129
|
+
}
|
|
1130
|
+
if (!finalAttested) {
|
|
1131
|
+
return unavailablePlan(
|
|
1132
|
+
"linux_generic_execution_contract_changed",
|
|
1133
|
+
requiredBoundaries,
|
|
1134
|
+
cleanup,
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const supervisor = finalDescriptors.find(
|
|
1139
|
+
(entry) => entry.kind === "supervisor",
|
|
1140
|
+
);
|
|
1141
|
+
const command = `/proc/self/fd/${supervisor.childFd}`;
|
|
1142
|
+
const args = Object.freeze([
|
|
1143
|
+
...policyArgs,
|
|
1144
|
+
"--",
|
|
1145
|
+
validated.target.resolvedCommand,
|
|
1146
|
+
...validated.target.args,
|
|
1147
|
+
]);
|
|
1148
|
+
const options = Object.freeze({
|
|
1149
|
+
...passthroughSpawnOptions(spawnOptions),
|
|
1150
|
+
cwd: "/",
|
|
1151
|
+
shell: false,
|
|
1152
|
+
detached: false,
|
|
1153
|
+
env: Object.freeze({ PATH: "/usr/bin:/bin", LANG: "C", LC_ALL: "C" }),
|
|
1154
|
+
stdio: Object.freeze([
|
|
1155
|
+
...issued.stdio,
|
|
1156
|
+
...finalDescriptors.map((descriptor) => descriptor.fd),
|
|
1157
|
+
]),
|
|
1158
|
+
});
|
|
1159
|
+
let cleaned = false;
|
|
1160
|
+
const finalCleanup = () => {
|
|
1161
|
+
if (cleaned) return;
|
|
1162
|
+
cleaned = true;
|
|
1163
|
+
resources.cleanup?.();
|
|
1164
|
+
};
|
|
1165
|
+
const plan = Object.freeze({
|
|
1166
|
+
contractVersion: PLAN_VERSION,
|
|
1167
|
+
applied: true,
|
|
1168
|
+
platform: "linux",
|
|
1169
|
+
profile: "strict",
|
|
1170
|
+
command,
|
|
1171
|
+
args,
|
|
1172
|
+
options,
|
|
1173
|
+
enforcement: LINUX_GENERIC_BWRAP_BACKEND,
|
|
1174
|
+
backend: LINUX_GENERIC_BWRAP_BACKEND,
|
|
1175
|
+
candidateBackend: null,
|
|
1176
|
+
guarantees: Object.freeze(["filesystem", "network"]),
|
|
1177
|
+
requiredBoundaries: Object.freeze([...requiredBoundaries]),
|
|
1178
|
+
policyAttested: true,
|
|
1179
|
+
policyDigest,
|
|
1180
|
+
reason: null,
|
|
1181
|
+
runtimeProbe: Object.freeze({
|
|
1182
|
+
kind: "linux-bwrap-generic-workspace-policy-v1",
|
|
1183
|
+
attempted: true,
|
|
1184
|
+
runnable: true,
|
|
1185
|
+
reason: null,
|
|
1186
|
+
probeRuntime: "posix-sh",
|
|
1187
|
+
targetRuntime: "generic-command",
|
|
1188
|
+
contentSnapshot: false,
|
|
1189
|
+
handleAtomic: false,
|
|
1190
|
+
mountTopologyAtomic: false,
|
|
1191
|
+
contractDigest: issued.contractDigest,
|
|
1192
|
+
policyDigest,
|
|
1193
|
+
mountTopologyDigest: validated.workspaceMountTopology.digest,
|
|
1194
|
+
emptyRoot: true,
|
|
1195
|
+
undeclaredRootReadOnly: true,
|
|
1196
|
+
workspaceReadWrite: true,
|
|
1197
|
+
workspaceMountTopologyAttested: true,
|
|
1198
|
+
workspaceRootAliasAttested: true,
|
|
1199
|
+
anonymousDevWritable: true,
|
|
1200
|
+
systemReadOnly: true,
|
|
1201
|
+
hostHomeHidden: true,
|
|
1202
|
+
outsideMarkerHidden: true,
|
|
1203
|
+
networkNamespace: true,
|
|
1204
|
+
networkNamespaceChanged: true,
|
|
1205
|
+
socketCreationDenied: true,
|
|
1206
|
+
descriptorMounts: true,
|
|
1207
|
+
}),
|
|
1208
|
+
filesystemPolicy: Object.freeze({
|
|
1209
|
+
workspaceRoot: issued.workspaceRoot,
|
|
1210
|
+
workingDirectory: issued.workingDirectory,
|
|
1211
|
+
workspaceAccess: "read-write",
|
|
1212
|
+
systemAccess: "read-only",
|
|
1213
|
+
undeclaredRootAccess: "read-only",
|
|
1214
|
+
anonymousWritablePaths: Object.freeze([
|
|
1215
|
+
SANDBOX_HOME,
|
|
1216
|
+
"/dev",
|
|
1217
|
+
"/run",
|
|
1218
|
+
"/tmp",
|
|
1219
|
+
"/var/tmp",
|
|
1220
|
+
]),
|
|
1221
|
+
hostRootMapped: false,
|
|
1222
|
+
hostHomeMapped: false,
|
|
1223
|
+
workspaceDescriptorBound: true,
|
|
1224
|
+
systemDescriptorBound: true,
|
|
1225
|
+
exactEtcFileDescriptors: true,
|
|
1226
|
+
workspaceRecursiveBind: true,
|
|
1227
|
+
workspaceMountTopology:
|
|
1228
|
+
"no-strict-descendants-or-forbidden-root-aliases-at-attestation",
|
|
1229
|
+
mountTopologySource: validated.workspaceMountTopology.source,
|
|
1230
|
+
mountTopologyDigest: validated.workspaceMountTopology.digest,
|
|
1231
|
+
mountTopologyAtomic: false,
|
|
1232
|
+
}),
|
|
1233
|
+
networkPolicy: Object.freeze({
|
|
1234
|
+
namespace: "new",
|
|
1235
|
+
namespaceIdentityChanged: true,
|
|
1236
|
+
seccomp: "deny-network-creation",
|
|
1237
|
+
}),
|
|
1238
|
+
postSpawn: Object.freeze({ required: false, mode: "none" }),
|
|
1239
|
+
cleanup: finalCleanup,
|
|
1240
|
+
});
|
|
1241
|
+
plannedPolicies.set(plan, policyDigest);
|
|
1242
|
+
return plan;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
export function verifyLinuxGenericBubblewrapPlan(plan) {
|
|
1246
|
+
return (
|
|
1247
|
+
plannedPolicies.get(plan) === plan?.policyDigest &&
|
|
1248
|
+
Object.isFrozen(plan) &&
|
|
1249
|
+
Object.isFrozen(plan.args) &&
|
|
1250
|
+
Object.isFrozen(plan.options) &&
|
|
1251
|
+
Object.isFrozen(plan.options.stdio)
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
export function _resetLinuxGenericSandboxContractsForTest() {
|
|
1256
|
+
// WeakMaps cannot be cleared without replacing module-level authority. Tests
|
|
1257
|
+
// use fresh contract objects, so this hook exists only as an explicit marker
|
|
1258
|
+
// that no enumerable/global token registry is involved.
|
|
1259
|
+
return true;
|
|
1260
|
+
}
|