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,1347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production resource acquisition for the generic Linux bubblewrap backend.
|
|
3
|
+
*
|
|
4
|
+
* Caller-supplied callbacks never cross this boundary. Every mount, the
|
|
5
|
+
* bubblewrap executable, and the seccomp program is opened by this module and
|
|
6
|
+
* independently re-attested before the final spawn plan is returned.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import crypto from "node:crypto";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { spawnSync as nativeSpawnSync } from "node:child_process";
|
|
14
|
+
import {
|
|
15
|
+
LINUX_GENERIC_BWRAP_BACKEND,
|
|
16
|
+
LINUX_GENERIC_CONTRACT_KIND,
|
|
17
|
+
planLinuxGenericBubblewrap,
|
|
18
|
+
} from "./linux-generic-bwrap.js";
|
|
19
|
+
|
|
20
|
+
const BWRAP_PATH = "/usr/bin/bwrap";
|
|
21
|
+
const MAX_ATTESTED_FILE_BYTES = 256 * 1024 * 1024;
|
|
22
|
+
const MAX_MOUNTINFO_BYTES = 4 * 1024 * 1024;
|
|
23
|
+
const MOUNTINFO_PATH = "/proc/self/mountinfo";
|
|
24
|
+
const MOUNT_TOPOLOGY_VERSION = 1;
|
|
25
|
+
const O_TMPFILE = 0x410000;
|
|
26
|
+
const SYSTEM_DESTINATIONS = [
|
|
27
|
+
"/usr",
|
|
28
|
+
"/bin",
|
|
29
|
+
"/sbin",
|
|
30
|
+
"/lib",
|
|
31
|
+
"/lib32",
|
|
32
|
+
"/lib64",
|
|
33
|
+
];
|
|
34
|
+
const SYSTEM_ROOTS = new Set(SYSTEM_DESTINATIONS);
|
|
35
|
+
const SYSTEM_SYMLINKS = new Map([
|
|
36
|
+
["/bin", "usr/bin"],
|
|
37
|
+
["/sbin", "usr/sbin"],
|
|
38
|
+
["/lib", "usr/lib"],
|
|
39
|
+
["/lib32", "usr/lib32"],
|
|
40
|
+
["/lib64", "usr/lib64"],
|
|
41
|
+
]);
|
|
42
|
+
const ETC_ALLOWLIST = [
|
|
43
|
+
"/etc/group",
|
|
44
|
+
"/etc/hosts",
|
|
45
|
+
"/etc/ld.so.cache",
|
|
46
|
+
"/etc/nsswitch.conf",
|
|
47
|
+
"/etc/passwd",
|
|
48
|
+
];
|
|
49
|
+
const FORBIDDEN_EXACT_WORKSPACE_ALIASES = [
|
|
50
|
+
"/",
|
|
51
|
+
"/home",
|
|
52
|
+
"/home/sandbox",
|
|
53
|
+
"/media",
|
|
54
|
+
"/mnt",
|
|
55
|
+
"/opt",
|
|
56
|
+
"/root",
|
|
57
|
+
"/run",
|
|
58
|
+
"/srv",
|
|
59
|
+
"/tmp",
|
|
60
|
+
"/var",
|
|
61
|
+
"/var/tmp",
|
|
62
|
+
];
|
|
63
|
+
const FORBIDDEN_WORKSPACE_ALIAS_SUBTREES = [
|
|
64
|
+
"/bin",
|
|
65
|
+
"/boot",
|
|
66
|
+
"/dev",
|
|
67
|
+
"/etc",
|
|
68
|
+
"/lib",
|
|
69
|
+
"/lib32",
|
|
70
|
+
"/lib64",
|
|
71
|
+
"/proc",
|
|
72
|
+
"/sbin",
|
|
73
|
+
"/sys",
|
|
74
|
+
"/usr",
|
|
75
|
+
];
|
|
76
|
+
const OPAQUE_NON_ROOT_FILESYSTEMS = new Set([
|
|
77
|
+
"9p",
|
|
78
|
+
"aufs",
|
|
79
|
+
"ceph",
|
|
80
|
+
"cifs",
|
|
81
|
+
"ecryptfs",
|
|
82
|
+
"glusterfs",
|
|
83
|
+
"nfs",
|
|
84
|
+
"nfs4",
|
|
85
|
+
"overlay",
|
|
86
|
+
"smb3",
|
|
87
|
+
"virtiofs",
|
|
88
|
+
]);
|
|
89
|
+
const REQUIRED_BWRAP_OPTIONS = [
|
|
90
|
+
"--assert-userns-disabled",
|
|
91
|
+
"--bind-fd",
|
|
92
|
+
"--disable-userns",
|
|
93
|
+
"--file",
|
|
94
|
+
"--perms",
|
|
95
|
+
"--remount-ro",
|
|
96
|
+
"--ro-bind-fd",
|
|
97
|
+
"--seccomp",
|
|
98
|
+
];
|
|
99
|
+
const SECCOMP_ARCHITECTURES = Object.freeze({
|
|
100
|
+
x64: Object.freeze({
|
|
101
|
+
auditArch: 0xc000003e,
|
|
102
|
+
socketSyscall: 41,
|
|
103
|
+
socketpairSyscall: 53,
|
|
104
|
+
ioUringSetupSyscall: 425,
|
|
105
|
+
x32SyscallBit: 0x40000000,
|
|
106
|
+
}),
|
|
107
|
+
arm64: Object.freeze({
|
|
108
|
+
auditArch: 0xc00000b7,
|
|
109
|
+
socketSyscall: 198,
|
|
110
|
+
socketpairSyscall: 199,
|
|
111
|
+
ioUringSetupSyscall: 425,
|
|
112
|
+
}),
|
|
113
|
+
riscv64: Object.freeze({
|
|
114
|
+
auditArch: 0xc00000f3,
|
|
115
|
+
socketSyscall: 198,
|
|
116
|
+
socketpairSyscall: 199,
|
|
117
|
+
ioUringSetupSyscall: 425,
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const DEFAULT_RUNTIME = Object.freeze({
|
|
122
|
+
platform: os.platform(),
|
|
123
|
+
arch: process.arch,
|
|
124
|
+
execPath: process.execPath,
|
|
125
|
+
fs,
|
|
126
|
+
homedir: () => os.homedir(),
|
|
127
|
+
tmpdir: () => os.tmpdir(),
|
|
128
|
+
randomBytes: (size) => crypto.randomBytes(size),
|
|
129
|
+
spawnSync: nativeSpawnSync,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
function runtimeFor(overrides = {}) {
|
|
133
|
+
return { ...DEFAULT_RUNTIME, ...overrides };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function realpath(runtime, value) {
|
|
137
|
+
const implementation =
|
|
138
|
+
runtime.fs.realpathSync?.native || runtime.fs.realpathSync;
|
|
139
|
+
if (typeof implementation !== "function") {
|
|
140
|
+
throw new Error("realpath_unavailable");
|
|
141
|
+
}
|
|
142
|
+
return implementation.call(runtime.fs.realpathSync, value);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function fileId(stat) {
|
|
146
|
+
return { dev: String(stat.dev), ino: String(stat.ino) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function identity(realPath, stat) {
|
|
150
|
+
return Object.freeze({
|
|
151
|
+
realPath,
|
|
152
|
+
fileId: Object.freeze(fileId(stat)),
|
|
153
|
+
mode: Number(stat.mode),
|
|
154
|
+
uid: Number(stat.uid),
|
|
155
|
+
gid: Number(stat.gid),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function identityMatches(left, right) {
|
|
160
|
+
return (
|
|
161
|
+
left?.realPath === right?.realPath &&
|
|
162
|
+
String(left?.fileId?.dev) === String(right?.fileId?.dev) &&
|
|
163
|
+
String(left?.fileId?.ino) === String(right?.fileId?.ino) &&
|
|
164
|
+
Number(left?.mode) === Number(right?.mode) &&
|
|
165
|
+
Number(left?.uid) === Number(right?.uid) &&
|
|
166
|
+
Number(left?.gid) === Number(right?.gid)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function statMatchesIdentity(stat, expected, { directory }) {
|
|
171
|
+
const expectedType = directory ? 0o040000 : 0o100000;
|
|
172
|
+
return (
|
|
173
|
+
String(stat?.dev) === String(expected?.fileId?.dev) &&
|
|
174
|
+
String(stat?.ino) === String(expected?.fileId?.ino) &&
|
|
175
|
+
Number(stat?.mode) === Number(expected?.mode) &&
|
|
176
|
+
Number(stat?.uid) === Number(expected?.uid) &&
|
|
177
|
+
Number(stat?.gid) === Number(expected?.gid) &&
|
|
178
|
+
(Number(stat?.mode) & 0o170000) === expectedType
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isWithin(root, target) {
|
|
183
|
+
const relative = path.posix.relative(root, target);
|
|
184
|
+
return (
|
|
185
|
+
relative === "" ||
|
|
186
|
+
(relative !== ".." &&
|
|
187
|
+
!relative.startsWith("../") &&
|
|
188
|
+
!path.posix.isAbsolute(relative))
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function rootOwned(stat, { directory, executable = false }) {
|
|
193
|
+
const expectedType = directory ? 0o040000 : 0o100000;
|
|
194
|
+
return (
|
|
195
|
+
(Number(stat?.mode) & 0o170000) === expectedType &&
|
|
196
|
+
Number(stat?.uid) === 0 &&
|
|
197
|
+
(Number(stat?.mode) & 0o022) === 0 &&
|
|
198
|
+
(!executable || (Number(stat?.mode) & 0o111) !== 0)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function hashOpenFile(runtime, fd, bytes) {
|
|
203
|
+
if (
|
|
204
|
+
!Number.isSafeInteger(bytes) ||
|
|
205
|
+
bytes < 1 ||
|
|
206
|
+
bytes > MAX_ATTESTED_FILE_BYTES
|
|
207
|
+
) {
|
|
208
|
+
throw new Error("attested_file_size_invalid");
|
|
209
|
+
}
|
|
210
|
+
const digest = crypto.createHash("sha256");
|
|
211
|
+
const buffer = Buffer.allocUnsafe(Math.min(1024 * 1024, bytes));
|
|
212
|
+
let offset = 0;
|
|
213
|
+
while (offset < bytes) {
|
|
214
|
+
const requested = Math.min(buffer.length, bytes - offset);
|
|
215
|
+
const read = runtime.fs.readSync(fd, buffer, 0, requested, offset);
|
|
216
|
+
if (read <= 0) throw new Error("attested_file_read_failed");
|
|
217
|
+
digest.update(buffer.subarray(0, read));
|
|
218
|
+
offset += read;
|
|
219
|
+
}
|
|
220
|
+
return digest.digest("hex");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function buildNetworkSeccompFilter(arch) {
|
|
224
|
+
const selected = SECCOMP_ARCHITECTURES[arch];
|
|
225
|
+
if (!selected) {
|
|
226
|
+
throw new Error(`unsupported_seccomp_architecture:${String(arch)}`);
|
|
227
|
+
}
|
|
228
|
+
const instructions = [
|
|
229
|
+
[0x20, 0, 0, 4],
|
|
230
|
+
[0x15, 1, 0, selected.auditArch],
|
|
231
|
+
[0x06, 0, 0, 0x80000000],
|
|
232
|
+
[0x20, 0, 0, 0],
|
|
233
|
+
];
|
|
234
|
+
if (selected.x32SyscallBit) {
|
|
235
|
+
instructions.push([0x54, 0, 0, ~selected.x32SyscallBit]);
|
|
236
|
+
}
|
|
237
|
+
instructions.push(
|
|
238
|
+
[0x15, 0, 1, selected.socketSyscall],
|
|
239
|
+
[0x06, 0, 0, 0x00050001],
|
|
240
|
+
[0x15, 0, 1, selected.socketpairSyscall],
|
|
241
|
+
[0x06, 0, 0, 0x00050001],
|
|
242
|
+
[0x15, 0, 1, selected.ioUringSetupSyscall],
|
|
243
|
+
[0x06, 0, 0, 0x00050001],
|
|
244
|
+
[0x06, 0, 0, 0x7fff0000],
|
|
245
|
+
);
|
|
246
|
+
const buffer = Buffer.alloc(instructions.length * 8);
|
|
247
|
+
instructions.forEach(([code, jumpTrue, jumpFalse, value], index) => {
|
|
248
|
+
const offset = index * 8;
|
|
249
|
+
buffer.writeUInt16LE(code, offset);
|
|
250
|
+
buffer.writeUInt8(jumpTrue, offset + 2);
|
|
251
|
+
buffer.writeUInt8(jumpFalse, offset + 3);
|
|
252
|
+
buffer.writeUInt32LE(value >>> 0, offset + 4);
|
|
253
|
+
});
|
|
254
|
+
return Object.freeze({
|
|
255
|
+
arch,
|
|
256
|
+
buffer,
|
|
257
|
+
sha256: crypto.createHash("sha256").update(buffer).digest("hex"),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function shellQuote(value) {
|
|
262
|
+
return `'${String(value).replaceAll("'", `'\\''`)}'`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function stableJson(value) {
|
|
266
|
+
if (Array.isArray(value)) {
|
|
267
|
+
return `[${value.map((entry) => stableJson(entry)).join(",")}]`;
|
|
268
|
+
}
|
|
269
|
+
if (value && typeof value === "object") {
|
|
270
|
+
return `{${Object.keys(value)
|
|
271
|
+
.sort()
|
|
272
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
273
|
+
.join(",")}}`;
|
|
274
|
+
}
|
|
275
|
+
return JSON.stringify(value);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function sha256(value) {
|
|
279
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function decodeMountInfoPath(value) {
|
|
283
|
+
if (typeof value !== "string" || !value) {
|
|
284
|
+
throw new Error("mountinfo_path_invalid");
|
|
285
|
+
}
|
|
286
|
+
const decoded = [];
|
|
287
|
+
const escapes = new Map([
|
|
288
|
+
["040", " "],
|
|
289
|
+
["011", "\t"],
|
|
290
|
+
["012", "\n"],
|
|
291
|
+
["134", "\\"],
|
|
292
|
+
]);
|
|
293
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
294
|
+
if (value[index] !== "\\") {
|
|
295
|
+
decoded.push(value[index]);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const octal = value.slice(index + 1, index + 4);
|
|
299
|
+
if (!escapes.has(octal)) {
|
|
300
|
+
throw new Error("mountinfo_path_escape_invalid");
|
|
301
|
+
}
|
|
302
|
+
decoded.push(escapes.get(octal));
|
|
303
|
+
index += 3;
|
|
304
|
+
}
|
|
305
|
+
const result = decoded.join("");
|
|
306
|
+
if (
|
|
307
|
+
result.includes("\0") ||
|
|
308
|
+
!path.posix.isAbsolute(result) ||
|
|
309
|
+
path.posix.normalize(result) !== result
|
|
310
|
+
) {
|
|
311
|
+
throw new Error("mountinfo_path_invalid");
|
|
312
|
+
}
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function encodeMountInfoPath(value) {
|
|
317
|
+
return String(value)
|
|
318
|
+
.replaceAll("\\", "\\134")
|
|
319
|
+
.replaceAll(" ", "\\040")
|
|
320
|
+
.replaceAll("\t", "\\011")
|
|
321
|
+
.replaceAll("\n", "\\012");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function parseMountInfo(runtime) {
|
|
325
|
+
const raw = String(runtime.fs.readFileSync(MOUNTINFO_PATH, "utf8"));
|
|
326
|
+
if (
|
|
327
|
+
Buffer.byteLength(raw, "utf8") < 1 ||
|
|
328
|
+
Buffer.byteLength(raw, "utf8") > MAX_MOUNTINFO_BYTES
|
|
329
|
+
) {
|
|
330
|
+
throw new Error("mountinfo_size_invalid");
|
|
331
|
+
}
|
|
332
|
+
const entries = [];
|
|
333
|
+
for (const line of raw.split("\n")) {
|
|
334
|
+
if (!line) continue;
|
|
335
|
+
const fields = line.split(" ");
|
|
336
|
+
const separator = fields.indexOf("-");
|
|
337
|
+
if (
|
|
338
|
+
separator < 6 ||
|
|
339
|
+
fields.length < separator + 4 ||
|
|
340
|
+
!/^\d+$/.test(fields[0] || "") ||
|
|
341
|
+
!/^\d+$/.test(fields[1] || "") ||
|
|
342
|
+
!/^\d+:\d+$/.test(fields[2] || "")
|
|
343
|
+
) {
|
|
344
|
+
throw new Error("mountinfo_entry_invalid");
|
|
345
|
+
}
|
|
346
|
+
const root = decodeMountInfoPath(fields[3]);
|
|
347
|
+
const mountPoint = decodeMountInfoPath(fields[4]);
|
|
348
|
+
const fsType = fields[separator + 1];
|
|
349
|
+
if (
|
|
350
|
+
!fsType ||
|
|
351
|
+
fsType.includes("\0") ||
|
|
352
|
+
fields.slice(separator + 1).some((field) => !field)
|
|
353
|
+
) {
|
|
354
|
+
throw new Error("mountinfo_entry_invalid");
|
|
355
|
+
}
|
|
356
|
+
entries.push(
|
|
357
|
+
Object.freeze({
|
|
358
|
+
mountId: fields[0],
|
|
359
|
+
parentId: fields[1],
|
|
360
|
+
majorMinor: fields[2],
|
|
361
|
+
root,
|
|
362
|
+
mountPoint,
|
|
363
|
+
fsType,
|
|
364
|
+
lineDigest: sha256(line),
|
|
365
|
+
}),
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
if (entries.length === 0) throw new Error("mountinfo_empty");
|
|
369
|
+
return entries;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function mountDepth(value) {
|
|
373
|
+
return value === "/" ? 0 : value.split("/").filter(Boolean).length;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function containingMount(entries, target) {
|
|
377
|
+
const candidates = entries.filter((entry) =>
|
|
378
|
+
isWithin(entry.mountPoint, target),
|
|
379
|
+
);
|
|
380
|
+
if (candidates.length === 0) throw new Error("mountinfo_root_unavailable");
|
|
381
|
+
const deepest = Math.max(
|
|
382
|
+
...candidates.map((entry) => mountDepth(entry.mountPoint)),
|
|
383
|
+
);
|
|
384
|
+
const matches = candidates.filter(
|
|
385
|
+
(entry) => mountDepth(entry.mountPoint) === deepest,
|
|
386
|
+
);
|
|
387
|
+
if (matches.length !== 1) {
|
|
388
|
+
throw new Error("mountinfo_workspace_mount_ambiguous");
|
|
389
|
+
}
|
|
390
|
+
return matches[0];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function sameDirectoryIdentity(left, right) {
|
|
394
|
+
return (
|
|
395
|
+
String(left?.dev ?? left?.fileId?.dev) ===
|
|
396
|
+
String(right?.dev ?? right?.fileId?.dev) &&
|
|
397
|
+
String(left?.ino ?? left?.fileId?.ino) ===
|
|
398
|
+
String(right?.ino ?? right?.fileId?.ino)
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function workspaceMountTopology(runtime, workspaceRoot, workspaceIdentity) {
|
|
403
|
+
const entries = parseMountInfo(runtime);
|
|
404
|
+
const descendants = entries.filter(
|
|
405
|
+
(entry) =>
|
|
406
|
+
entry.mountPoint !== workspaceRoot &&
|
|
407
|
+
isWithin(workspaceRoot, entry.mountPoint),
|
|
408
|
+
);
|
|
409
|
+
if (descendants.length > 0) {
|
|
410
|
+
throw new Error("workspace_descendant_mount_detected");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const mountedBy = containingMount(entries, workspaceRoot);
|
|
414
|
+
const rootMount = containingMount(entries, "/");
|
|
415
|
+
const workspaceRelative = path.posix.relative(
|
|
416
|
+
mountedBy.mountPoint,
|
|
417
|
+
workspaceRoot,
|
|
418
|
+
);
|
|
419
|
+
const filesystemPath = path.posix.join(mountedBy.root, workspaceRelative);
|
|
420
|
+
const sameFilesystem = entries.filter(
|
|
421
|
+
(entry) => entry.majorMinor === mountedBy.majorMinor,
|
|
422
|
+
);
|
|
423
|
+
const aliases = new Set();
|
|
424
|
+
for (const entry of sameFilesystem) {
|
|
425
|
+
if (!isWithin(entry.root, filesystemPath)) continue;
|
|
426
|
+
aliases.add(
|
|
427
|
+
path.posix.join(
|
|
428
|
+
entry.mountPoint,
|
|
429
|
+
path.posix.relative(entry.root, filesystemPath),
|
|
430
|
+
),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
if (aliases.size === 0) {
|
|
434
|
+
throw new Error("workspace_mount_provenance_unavailable");
|
|
435
|
+
}
|
|
436
|
+
if (
|
|
437
|
+
mountedBy.mountPoint !== "/" &&
|
|
438
|
+
mountedBy.root !== "/" &&
|
|
439
|
+
mountedBy.mountPoint === workspaceRoot
|
|
440
|
+
) {
|
|
441
|
+
throw new Error("workspace_root_bind_alias_forbidden");
|
|
442
|
+
}
|
|
443
|
+
if ([...aliases].some((alias) => alias !== workspaceRoot)) {
|
|
444
|
+
throw new Error("workspace_root_alias_forbidden");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let home;
|
|
448
|
+
try {
|
|
449
|
+
home = realpath(runtime, runtime.homedir());
|
|
450
|
+
} catch {
|
|
451
|
+
throw new Error("workspace_home_identity_unavailable");
|
|
452
|
+
}
|
|
453
|
+
const forbiddenExact = new Set([
|
|
454
|
+
...FORBIDDEN_EXACT_WORKSPACE_ALIASES,
|
|
455
|
+
...FORBIDDEN_WORKSPACE_ALIAS_SUBTREES,
|
|
456
|
+
home,
|
|
457
|
+
]);
|
|
458
|
+
for (const alias of aliases) {
|
|
459
|
+
if (
|
|
460
|
+
forbiddenExact.has(alias) ||
|
|
461
|
+
FORBIDDEN_WORKSPACE_ALIAS_SUBTREES.some((root) => isWithin(root, alias))
|
|
462
|
+
) {
|
|
463
|
+
throw new Error("workspace_root_alias_forbidden");
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const forbiddenIdentities = [];
|
|
468
|
+
for (const candidate of forbiddenExact) {
|
|
469
|
+
try {
|
|
470
|
+
const canonical = realpath(runtime, candidate);
|
|
471
|
+
const stat = runtime.fs.statSync(canonical);
|
|
472
|
+
if (!stat.isDirectory?.()) continue;
|
|
473
|
+
const observed = {
|
|
474
|
+
path: candidate,
|
|
475
|
+
canonical,
|
|
476
|
+
dev: String(stat.dev),
|
|
477
|
+
ino: String(stat.ino),
|
|
478
|
+
};
|
|
479
|
+
forbiddenIdentities.push(observed);
|
|
480
|
+
if (sameDirectoryIdentity(stat, workspaceIdentity)) {
|
|
481
|
+
throw new Error("workspace_root_identity_alias_forbidden");
|
|
482
|
+
}
|
|
483
|
+
} catch (error) {
|
|
484
|
+
if (error?.message === "workspace_root_identity_alias_forbidden") {
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
487
|
+
// Missing optional system roots do not weaken the paths that exist.
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const opaqueFilesystem =
|
|
492
|
+
mountedBy.fsType.startsWith("fuse") ||
|
|
493
|
+
OPAQUE_NON_ROOT_FILESYSTEMS.has(mountedBy.fsType);
|
|
494
|
+
if (mountedBy.mountPoint !== "/" && opaqueFilesystem) {
|
|
495
|
+
throw new Error("workspace_mount_provenance_opaque");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const lineage = entries.filter((entry) =>
|
|
499
|
+
isWithin(entry.mountPoint, workspaceRoot),
|
|
500
|
+
);
|
|
501
|
+
const binding = {
|
|
502
|
+
version: MOUNT_TOPOLOGY_VERSION,
|
|
503
|
+
source: "proc-self-mountinfo",
|
|
504
|
+
workspaceRoot,
|
|
505
|
+
containingMount: mountedBy.lineDigest,
|
|
506
|
+
rootMount: rootMount.lineDigest,
|
|
507
|
+
lineageEntryDigests: lineage.map((entry) => entry.lineDigest).sort(),
|
|
508
|
+
filesystemEntryDigests: sameFilesystem
|
|
509
|
+
.map((entry) => entry.lineDigest)
|
|
510
|
+
.sort(),
|
|
511
|
+
aliasDigests: [...aliases].map(sha256).sort(),
|
|
512
|
+
forbiddenIdentityDigests: forbiddenIdentities
|
|
513
|
+
.map((entry) => sha256(stableJson(entry)))
|
|
514
|
+
.sort(),
|
|
515
|
+
strictDescendantMountsAtAttestation: 0,
|
|
516
|
+
recursiveBind: true,
|
|
517
|
+
mountTopologyAtomic: false,
|
|
518
|
+
};
|
|
519
|
+
return Object.freeze({
|
|
520
|
+
version: binding.version,
|
|
521
|
+
source: binding.source,
|
|
522
|
+
workspaceRoot: binding.workspaceRoot,
|
|
523
|
+
digest: sha256(stableJson(binding)),
|
|
524
|
+
lineageEntryCount: binding.lineageEntryDigests.length,
|
|
525
|
+
filesystemEntryCount: binding.filesystemEntryDigests.length,
|
|
526
|
+
aliasCount: binding.aliasDigests.length,
|
|
527
|
+
forbiddenIdentityCount: binding.forbiddenIdentityDigests.length,
|
|
528
|
+
strictDescendantMountsAtAttestation:
|
|
529
|
+
binding.strictDescendantMountsAtAttestation,
|
|
530
|
+
rootAliasAttested: true,
|
|
531
|
+
recursiveBind: binding.recursiveBind,
|
|
532
|
+
mountTopologyAtomic: binding.mountTopologyAtomic,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function normalizedStdio(stdio) {
|
|
537
|
+
if (stdio === undefined || stdio === null || stdio === "pipe") {
|
|
538
|
+
return ["pipe", "pipe", "pipe"];
|
|
539
|
+
}
|
|
540
|
+
if (stdio === "ignore") return ["ignore", "ignore", "ignore"];
|
|
541
|
+
if (!Array.isArray(stdio)) return null;
|
|
542
|
+
const result = [...stdio];
|
|
543
|
+
while (result.length < 3) result.push(undefined);
|
|
544
|
+
return result.map((entry, index) => {
|
|
545
|
+
if (entry === undefined || entry === null) {
|
|
546
|
+
return index < 3 ? "pipe" : "ignore";
|
|
547
|
+
}
|
|
548
|
+
return entry;
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function resolveExecutable(runtime, requested, cwd, environment) {
|
|
553
|
+
if (typeof requested !== "string" || !requested || requested.includes("\0")) {
|
|
554
|
+
throw new Error("generic_target_invalid");
|
|
555
|
+
}
|
|
556
|
+
const candidates = [];
|
|
557
|
+
if (requested.includes("/")) {
|
|
558
|
+
candidates.push(
|
|
559
|
+
path.posix.isAbsolute(requested)
|
|
560
|
+
? requested
|
|
561
|
+
: path.posix.resolve(cwd, requested),
|
|
562
|
+
);
|
|
563
|
+
} else {
|
|
564
|
+
for (const entry of String(
|
|
565
|
+
environment?.PATH || "/usr/local/bin:/usr/bin:/bin",
|
|
566
|
+
).split(":")) {
|
|
567
|
+
if (!entry) continue;
|
|
568
|
+
const directory = path.posix.isAbsolute(entry)
|
|
569
|
+
? entry
|
|
570
|
+
: path.posix.resolve(cwd, entry);
|
|
571
|
+
candidates.push(path.posix.join(directory, requested));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
for (const candidate of candidates) {
|
|
575
|
+
try {
|
|
576
|
+
runtime.fs.accessSync(
|
|
577
|
+
candidate,
|
|
578
|
+
runtime.fs.constants?.X_OK ?? fs.constants.X_OK,
|
|
579
|
+
);
|
|
580
|
+
const canonical = realpath(runtime, candidate);
|
|
581
|
+
const stat = runtime.fs.statSync(canonical);
|
|
582
|
+
if (!stat.isFile() || (Number(stat.mode) & 0o111) === 0) continue;
|
|
583
|
+
return { canonical, stat };
|
|
584
|
+
} catch {
|
|
585
|
+
// Continue PATH resolution exactly as child_process would.
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
throw new Error("generic_target_unavailable");
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function systemTarget(canonical) {
|
|
592
|
+
return [...SYSTEM_ROOTS].some((root) => isWithin(root, canonical));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function createTrustedResources(
|
|
596
|
+
contract,
|
|
597
|
+
command,
|
|
598
|
+
args,
|
|
599
|
+
spawnOpts,
|
|
600
|
+
sandboxOpts,
|
|
601
|
+
runtime,
|
|
602
|
+
) {
|
|
603
|
+
const ownedFds = new Set();
|
|
604
|
+
const probeFds = new Set();
|
|
605
|
+
const finalFds = new Set();
|
|
606
|
+
const closeFd = (fd) => {
|
|
607
|
+
if (!Number.isInteger(fd) || !ownedFds.delete(fd)) return;
|
|
608
|
+
probeFds.delete(fd);
|
|
609
|
+
finalFds.delete(fd);
|
|
610
|
+
try {
|
|
611
|
+
runtime.fs.closeSync(fd);
|
|
612
|
+
} catch {
|
|
613
|
+
// Cleanup must preserve the fail-closed boundary result.
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
const closeSet = (set) => {
|
|
617
|
+
for (const fd of [...set]) closeFd(fd);
|
|
618
|
+
};
|
|
619
|
+
const cleanup = () => closeSet(ownedFds);
|
|
620
|
+
const track = (fd, phase) => {
|
|
621
|
+
ownedFds.add(fd);
|
|
622
|
+
(phase === "probe" ? probeFds : finalFds).add(fd);
|
|
623
|
+
return fd;
|
|
624
|
+
};
|
|
625
|
+
const openFlags = ({ directory, readWrite = false }) => {
|
|
626
|
+
const constants = runtime.fs.constants || fs.constants;
|
|
627
|
+
return (
|
|
628
|
+
Number(readWrite ? constants.O_RDWR : constants.O_RDONLY) |
|
|
629
|
+
Number(directory ? constants.O_DIRECTORY || 0 : 0) |
|
|
630
|
+
Number(constants.O_NOFOLLOW || 0) |
|
|
631
|
+
Number(constants.O_NONBLOCK || 0)
|
|
632
|
+
);
|
|
633
|
+
};
|
|
634
|
+
const openPair = (
|
|
635
|
+
source,
|
|
636
|
+
{ directory, rootRequired = false, executable = false },
|
|
637
|
+
) => {
|
|
638
|
+
const canonical = realpath(runtime, source);
|
|
639
|
+
if (canonical !== source) throw new Error("resource_path_not_canonical");
|
|
640
|
+
const pathStat = runtime.fs.lstatSync(source);
|
|
641
|
+
if (
|
|
642
|
+
pathStat.isSymbolicLink?.() ||
|
|
643
|
+
(directory ? !pathStat.isDirectory() : !pathStat.isFile()) ||
|
|
644
|
+
(rootRequired && !rootOwned(pathStat, { directory, executable }))
|
|
645
|
+
) {
|
|
646
|
+
throw new Error("resource_path_unattested");
|
|
647
|
+
}
|
|
648
|
+
const probeFd = track(
|
|
649
|
+
runtime.fs.openSync(source, openFlags({ directory })),
|
|
650
|
+
"probe",
|
|
651
|
+
);
|
|
652
|
+
const finalFd = track(
|
|
653
|
+
runtime.fs.openSync(source, openFlags({ directory })),
|
|
654
|
+
"final",
|
|
655
|
+
);
|
|
656
|
+
const probeStat = runtime.fs.fstatSync(probeFd);
|
|
657
|
+
const finalStat = runtime.fs.fstatSync(finalFd);
|
|
658
|
+
const observedIdentity = identity(source, pathStat);
|
|
659
|
+
if (
|
|
660
|
+
!statMatchesIdentity(probeStat, observedIdentity, { directory }) ||
|
|
661
|
+
!statMatchesIdentity(finalStat, observedIdentity, { directory })
|
|
662
|
+
) {
|
|
663
|
+
throw new Error("resource_descriptor_identity_changed");
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
source,
|
|
667
|
+
probeFd,
|
|
668
|
+
finalFd,
|
|
669
|
+
identity: observedIdentity,
|
|
670
|
+
};
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
try {
|
|
674
|
+
if (
|
|
675
|
+
runtime.platform !== "linux" ||
|
|
676
|
+
contract?.kind !== LINUX_GENERIC_CONTRACT_KIND ||
|
|
677
|
+
spawnOpts?.shell === true ||
|
|
678
|
+
typeof spawnOpts?.shell === "string" ||
|
|
679
|
+
spawnOpts?.detached === true ||
|
|
680
|
+
spawnOpts?.uid !== undefined ||
|
|
681
|
+
spawnOpts?.gid !== undefined ||
|
|
682
|
+
spawnOpts?.argv0 !== undefined ||
|
|
683
|
+
spawnOpts?.serialization !== undefined
|
|
684
|
+
) {
|
|
685
|
+
throw new Error("generic_launch_options_unsupported");
|
|
686
|
+
}
|
|
687
|
+
const hostNetworkNamespace = String(
|
|
688
|
+
runtime.fs.readlinkSync("/proc/self/ns/net"),
|
|
689
|
+
);
|
|
690
|
+
if (!/^net:\[\d+\]$/.test(hostNetworkNamespace)) {
|
|
691
|
+
throw new Error("host_network_namespace_identity_unavailable");
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const workspace = openPair(contract.workspaceRoot, {
|
|
695
|
+
directory: true,
|
|
696
|
+
});
|
|
697
|
+
const cwdPins = openPair(contract.workingDirectory, {
|
|
698
|
+
directory: true,
|
|
699
|
+
});
|
|
700
|
+
const initialWorkspaceMountTopology = workspaceMountTopology(
|
|
701
|
+
runtime,
|
|
702
|
+
contract.workspaceRoot,
|
|
703
|
+
workspace.identity,
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
const supervisor = openPair(BWRAP_PATH, {
|
|
707
|
+
directory: false,
|
|
708
|
+
rootRequired: true,
|
|
709
|
+
executable: true,
|
|
710
|
+
});
|
|
711
|
+
const supervisorBytes = Number(
|
|
712
|
+
runtime.fs.fstatSync(supervisor.probeFd).size,
|
|
713
|
+
);
|
|
714
|
+
const supervisorProbeDigest = hashOpenFile(
|
|
715
|
+
runtime,
|
|
716
|
+
supervisor.probeFd,
|
|
717
|
+
supervisorBytes,
|
|
718
|
+
);
|
|
719
|
+
const supervisorFinalDigest = hashOpenFile(
|
|
720
|
+
runtime,
|
|
721
|
+
supervisor.finalFd,
|
|
722
|
+
supervisorBytes,
|
|
723
|
+
);
|
|
724
|
+
if (supervisorProbeDigest !== supervisorFinalDigest) {
|
|
725
|
+
throw new Error("bubblewrap_supervisor_digest_mismatch");
|
|
726
|
+
}
|
|
727
|
+
supervisor.identity = Object.freeze({
|
|
728
|
+
...supervisor.identity,
|
|
729
|
+
sha256: supervisorFinalDigest,
|
|
730
|
+
bytes: supervisorBytes,
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
const capabilityFd = runtime.fs.openSync(
|
|
734
|
+
BWRAP_PATH,
|
|
735
|
+
openFlags({ directory: false }),
|
|
736
|
+
);
|
|
737
|
+
ownedFds.add(capabilityFd);
|
|
738
|
+
const capabilityStat = runtime.fs.fstatSync(capabilityFd);
|
|
739
|
+
if (
|
|
740
|
+
!statMatchesIdentity(capabilityStat, supervisor.identity, {
|
|
741
|
+
directory: false,
|
|
742
|
+
}) ||
|
|
743
|
+
hashOpenFile(runtime, capabilityFd, supervisorBytes) !==
|
|
744
|
+
supervisorFinalDigest
|
|
745
|
+
) {
|
|
746
|
+
throw new Error("bubblewrap_capability_reader_unattested");
|
|
747
|
+
}
|
|
748
|
+
const capabilityResult = runtime.spawnSync("/proc/self/fd/3", ["--help"], {
|
|
749
|
+
shell: false,
|
|
750
|
+
encoding: "utf8",
|
|
751
|
+
stdio: ["ignore", "pipe", "pipe", capabilityFd],
|
|
752
|
+
timeout: 10_000,
|
|
753
|
+
env: { PATH: "/usr/bin:/bin", LANG: "C", LC_ALL: "C" },
|
|
754
|
+
});
|
|
755
|
+
closeFd(capabilityFd);
|
|
756
|
+
if (capabilityResult?.error || capabilityResult?.status !== 0) {
|
|
757
|
+
const error = new Error("bubblewrap_capability_probe_failed");
|
|
758
|
+
error.code = "LINUX_GENERIC_BWRAP_UNAVAILABLE";
|
|
759
|
+
throw error;
|
|
760
|
+
}
|
|
761
|
+
const supported = new Set(
|
|
762
|
+
`${String(capabilityResult.stdout || "")}\n${String(
|
|
763
|
+
capabilityResult.stderr || "",
|
|
764
|
+
)}`
|
|
765
|
+
.split(/\s+/)
|
|
766
|
+
.filter(Boolean),
|
|
767
|
+
);
|
|
768
|
+
if (REQUIRED_BWRAP_OPTIONS.some((option) => !supported.has(option))) {
|
|
769
|
+
const error = new Error("bubblewrap_required_option_missing");
|
|
770
|
+
error.code = "LINUX_GENERIC_BWRAP_UNAVAILABLE";
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const system = [];
|
|
775
|
+
const systemSymlinks = [];
|
|
776
|
+
for (const destination of SYSTEM_DESTINATIONS) {
|
|
777
|
+
if (!runtime.fs.existsSync(destination)) continue;
|
|
778
|
+
const entryStat = runtime.fs.lstatSync(destination);
|
|
779
|
+
if (entryStat.isSymbolicLink?.()) {
|
|
780
|
+
const expectedTarget = SYSTEM_SYMLINKS.get(destination);
|
|
781
|
+
const observedTarget = runtime.fs.readlinkSync(destination);
|
|
782
|
+
const normalizedTarget = observedTarget.startsWith("/")
|
|
783
|
+
? observedTarget.slice(1)
|
|
784
|
+
: observedTarget;
|
|
785
|
+
if (
|
|
786
|
+
!expectedTarget ||
|
|
787
|
+
normalizedTarget !== expectedTarget ||
|
|
788
|
+
!runtime.fs.existsSync(`/${expectedTarget}`)
|
|
789
|
+
) {
|
|
790
|
+
throw new Error("system_runtime_symlink_unattested");
|
|
791
|
+
}
|
|
792
|
+
systemSymlinks.push({
|
|
793
|
+
destination,
|
|
794
|
+
target: expectedTarget,
|
|
795
|
+
});
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
const pair = openPair(destination, {
|
|
799
|
+
directory: true,
|
|
800
|
+
rootRequired: true,
|
|
801
|
+
});
|
|
802
|
+
system.push({
|
|
803
|
+
destination,
|
|
804
|
+
probeFd: pair.probeFd,
|
|
805
|
+
finalFd: pair.finalFd,
|
|
806
|
+
identity: pair.identity,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
if (!system.some((entry) => entry.destination === "/usr")) {
|
|
810
|
+
throw new Error("system_usr_runtime_unavailable");
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const etc = [];
|
|
814
|
+
for (const destination of ETC_ALLOWLIST) {
|
|
815
|
+
if (!runtime.fs.existsSync(destination)) continue;
|
|
816
|
+
const pair = openPair(destination, {
|
|
817
|
+
directory: false,
|
|
818
|
+
rootRequired: true,
|
|
819
|
+
});
|
|
820
|
+
etc.push({
|
|
821
|
+
destination,
|
|
822
|
+
probeFd: pair.probeFd,
|
|
823
|
+
finalFd: pair.finalFd,
|
|
824
|
+
identity: pair.identity,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const filter = buildNetworkSeccompFilter(runtime.arch);
|
|
829
|
+
const openSeccomp = (phase) => {
|
|
830
|
+
const constants = runtime.fs.constants || fs.constants;
|
|
831
|
+
const flags =
|
|
832
|
+
Number(constants.O_RDWR) |
|
|
833
|
+
Number(constants.O_EXCL) |
|
|
834
|
+
Number(constants.O_TMPFILE ?? O_TMPFILE) |
|
|
835
|
+
Number(constants.O_NOFOLLOW || 0) |
|
|
836
|
+
Number(constants.O_NONBLOCK || 0);
|
|
837
|
+
const fd = track(runtime.fs.openSync("/tmp", flags, 0o400), phase);
|
|
838
|
+
runtime.fs.fchmodSync(fd, 0o400);
|
|
839
|
+
let offset = 0;
|
|
840
|
+
while (offset < filter.buffer.length) {
|
|
841
|
+
const written = runtime.fs.writeSync(
|
|
842
|
+
fd,
|
|
843
|
+
filter.buffer,
|
|
844
|
+
offset,
|
|
845
|
+
filter.buffer.length - offset,
|
|
846
|
+
offset,
|
|
847
|
+
);
|
|
848
|
+
if (written <= 0) throw new Error("seccomp_filter_write_failed");
|
|
849
|
+
offset += written;
|
|
850
|
+
}
|
|
851
|
+
runtime.fs.fsyncSync(fd);
|
|
852
|
+
const stat = runtime.fs.fstatSync(fd);
|
|
853
|
+
if (
|
|
854
|
+
!stat.isFile() ||
|
|
855
|
+
Number(stat.size) !== filter.buffer.length ||
|
|
856
|
+
hashOpenFile(runtime, fd, filter.buffer.length) !== filter.sha256
|
|
857
|
+
) {
|
|
858
|
+
throw new Error("seccomp_filter_unattested");
|
|
859
|
+
}
|
|
860
|
+
return fd;
|
|
861
|
+
};
|
|
862
|
+
const seccomp = {
|
|
863
|
+
probeFd: openSeccomp("probe"),
|
|
864
|
+
finalFd: openSeccomp("final"),
|
|
865
|
+
sha256: filter.sha256,
|
|
866
|
+
policy: "deny-network-creation",
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
const environment = spawnOpts?.env || process.env;
|
|
870
|
+
const resolvedTarget = resolveExecutable(
|
|
871
|
+
runtime,
|
|
872
|
+
command,
|
|
873
|
+
contract.workingDirectory,
|
|
874
|
+
environment,
|
|
875
|
+
);
|
|
876
|
+
let targetScope;
|
|
877
|
+
if (isWithin(contract.workspaceRoot, resolvedTarget.canonical)) {
|
|
878
|
+
targetScope = "workspace";
|
|
879
|
+
} else if (
|
|
880
|
+
systemTarget(resolvedTarget.canonical) &&
|
|
881
|
+
rootOwned(resolvedTarget.stat, {
|
|
882
|
+
directory: false,
|
|
883
|
+
executable: true,
|
|
884
|
+
})
|
|
885
|
+
) {
|
|
886
|
+
targetScope = "system";
|
|
887
|
+
} else {
|
|
888
|
+
throw new Error("generic_target_outside_trusted_runtime");
|
|
889
|
+
}
|
|
890
|
+
const target = {
|
|
891
|
+
attestedContractDigest: contract.contractDigest,
|
|
892
|
+
requestedCommand: command,
|
|
893
|
+
resolvedCommand: resolvedTarget.canonical,
|
|
894
|
+
args: [...(args || [])],
|
|
895
|
+
scope: targetScope,
|
|
896
|
+
...(targetScope === "system"
|
|
897
|
+
? {
|
|
898
|
+
identity: identity(resolvedTarget.canonical, resolvedTarget.stat),
|
|
899
|
+
}
|
|
900
|
+
: {}),
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
const socketProbeCandidates = [
|
|
904
|
+
{ command: runtime.execPath, kind: "node" },
|
|
905
|
+
{ command: "/usr/local/bin/node", kind: "node" },
|
|
906
|
+
{ command: "/usr/bin/node", kind: "node" },
|
|
907
|
+
{ command: "/usr/bin/python3", kind: "python" },
|
|
908
|
+
{ command: "/usr/bin/python", kind: "python" },
|
|
909
|
+
];
|
|
910
|
+
let socketProbeRuntime = null;
|
|
911
|
+
for (const candidate of socketProbeCandidates) {
|
|
912
|
+
if (typeof candidate.command !== "string" || !candidate.command) continue;
|
|
913
|
+
try {
|
|
914
|
+
const resolved = resolveExecutable(
|
|
915
|
+
runtime,
|
|
916
|
+
candidate.command,
|
|
917
|
+
contract.workingDirectory,
|
|
918
|
+
environment,
|
|
919
|
+
);
|
|
920
|
+
if (
|
|
921
|
+
systemTarget(resolved.canonical) &&
|
|
922
|
+
rootOwned(resolved.stat, {
|
|
923
|
+
directory: false,
|
|
924
|
+
executable: true,
|
|
925
|
+
})
|
|
926
|
+
) {
|
|
927
|
+
socketProbeRuntime = Object.freeze({
|
|
928
|
+
command: resolved.canonical,
|
|
929
|
+
kind: candidate.kind,
|
|
930
|
+
});
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
} catch {
|
|
934
|
+
// Try the next root-owned Node runtime.
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (!socketProbeRuntime) {
|
|
938
|
+
throw new Error("socket_probe_runtime_unavailable");
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const descriptorAttested = (fd, expected, directory) => {
|
|
942
|
+
if (!ownedFds.has(fd)) return false;
|
|
943
|
+
try {
|
|
944
|
+
return statMatchesIdentity(runtime.fs.fstatSync(fd), expected, {
|
|
945
|
+
directory,
|
|
946
|
+
});
|
|
947
|
+
} catch {
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
const pathAttested = (source, expected, directory) => {
|
|
952
|
+
try {
|
|
953
|
+
return (
|
|
954
|
+
realpath(runtime, source) === source &&
|
|
955
|
+
statMatchesIdentity(runtime.fs.lstatSync(source), expected, {
|
|
956
|
+
directory,
|
|
957
|
+
})
|
|
958
|
+
);
|
|
959
|
+
} catch {
|
|
960
|
+
return false;
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
const launchStaysBound = (issued) => {
|
|
964
|
+
const stdio = normalizedStdio(spawnOpts?.stdio);
|
|
965
|
+
return (
|
|
966
|
+
issued?.contractDigest === contract.contractDigest &&
|
|
967
|
+
issued.workspaceRoot === contract.workspaceRoot &&
|
|
968
|
+
issued.workingDirectory === contract.workingDirectory &&
|
|
969
|
+
issued.origin === (spawnOpts?.origin || "unknown") &&
|
|
970
|
+
issued.command === command &&
|
|
971
|
+
issued.sync === (sandboxOpts?.sync === true) &&
|
|
972
|
+
issued.shell === false &&
|
|
973
|
+
Array.isArray(stdio) &&
|
|
974
|
+
issued.stdio.length === stdio.length &&
|
|
975
|
+
issued.stdio.every((value, index) => value === stdio[index]) &&
|
|
976
|
+
issued.args.length === (args || []).length &&
|
|
977
|
+
issued.args.every((value, index) => value === args[index]) &&
|
|
978
|
+
identityMatches(issued.rootIdentity, workspace.identity) &&
|
|
979
|
+
identityMatches(issued.cwdIdentity, cwdPins.identity)
|
|
980
|
+
);
|
|
981
|
+
};
|
|
982
|
+
const attestFinal = (issued) => {
|
|
983
|
+
let finalWorkspaceMountTopology;
|
|
984
|
+
try {
|
|
985
|
+
finalWorkspaceMountTopology = workspaceMountTopology(
|
|
986
|
+
runtime,
|
|
987
|
+
contract.workspaceRoot,
|
|
988
|
+
workspace.identity,
|
|
989
|
+
);
|
|
990
|
+
} catch {
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
if (
|
|
994
|
+
!launchStaysBound(issued) ||
|
|
995
|
+
finalWorkspaceMountTopology.digest !==
|
|
996
|
+
initialWorkspaceMountTopology.digest ||
|
|
997
|
+
!pathAttested(contract.workspaceRoot, workspace.identity, true) ||
|
|
998
|
+
!pathAttested(contract.workingDirectory, cwdPins.identity, true) ||
|
|
999
|
+
!descriptorAttested(workspace.finalFd, workspace.identity, true) ||
|
|
1000
|
+
!descriptorAttested(cwdPins.finalFd, cwdPins.identity, true) ||
|
|
1001
|
+
!descriptorAttested(supervisor.finalFd, supervisor.identity, false) ||
|
|
1002
|
+
hashOpenFile(runtime, supervisor.finalFd, supervisorBytes) !==
|
|
1003
|
+
supervisorFinalDigest ||
|
|
1004
|
+
hashOpenFile(runtime, seccomp.finalFd, filter.buffer.length) !==
|
|
1005
|
+
filter.sha256
|
|
1006
|
+
) {
|
|
1007
|
+
return false;
|
|
1008
|
+
}
|
|
1009
|
+
for (const entry of [...system, ...etc]) {
|
|
1010
|
+
if (
|
|
1011
|
+
!descriptorAttested(
|
|
1012
|
+
entry.finalFd,
|
|
1013
|
+
entry.identity,
|
|
1014
|
+
system.includes(entry),
|
|
1015
|
+
)
|
|
1016
|
+
) {
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
try {
|
|
1021
|
+
const currentTarget = resolveExecutable(
|
|
1022
|
+
runtime,
|
|
1023
|
+
command,
|
|
1024
|
+
contract.workingDirectory,
|
|
1025
|
+
environment,
|
|
1026
|
+
);
|
|
1027
|
+
return (
|
|
1028
|
+
currentTarget.canonical === target.resolvedCommand &&
|
|
1029
|
+
(target.scope === "workspace"
|
|
1030
|
+
? isWithin(contract.workspaceRoot, currentTarget.canonical)
|
|
1031
|
+
: rootOwned(currentTarget.stat, {
|
|
1032
|
+
directory: false,
|
|
1033
|
+
executable: true,
|
|
1034
|
+
}) &&
|
|
1035
|
+
statMatchesIdentity(currentTarget.stat, target.identity, {
|
|
1036
|
+
directory: false,
|
|
1037
|
+
}))
|
|
1038
|
+
);
|
|
1039
|
+
} catch {
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
const probe = (call) => {
|
|
1045
|
+
const nonce = runtime.randomBytes(12).toString("hex");
|
|
1046
|
+
const workspaceMarker = path.posix.join(
|
|
1047
|
+
contract.workspaceRoot,
|
|
1048
|
+
`.chainless-bwrap-probe-${nonce}`,
|
|
1049
|
+
);
|
|
1050
|
+
const outsideMarker = path.posix.join(
|
|
1051
|
+
runtime.tmpdir(),
|
|
1052
|
+
`.chainless-bwrap-outside-${nonce}`,
|
|
1053
|
+
);
|
|
1054
|
+
const homeMarker = path.posix.join(
|
|
1055
|
+
runtime.homedir(),
|
|
1056
|
+
`.chainless-bwrap-home-${nonce}`,
|
|
1057
|
+
);
|
|
1058
|
+
const workspaceHomeAliasMarker = path.posix.join(
|
|
1059
|
+
contract.workspaceRoot,
|
|
1060
|
+
path.posix.basename(homeMarker),
|
|
1061
|
+
);
|
|
1062
|
+
const devShmMarker = path.posix.join(
|
|
1063
|
+
"/dev/shm",
|
|
1064
|
+
`.chainless-bwrap-dev-shm-${nonce}`,
|
|
1065
|
+
);
|
|
1066
|
+
const outsideContents = `host-only-${nonce}`;
|
|
1067
|
+
const homeContents = `home-only-${nonce}`;
|
|
1068
|
+
let outsideCreated = false;
|
|
1069
|
+
let homeCreated = false;
|
|
1070
|
+
let devShmCreated = false;
|
|
1071
|
+
try {
|
|
1072
|
+
runtime.fs.writeFileSync(outsideMarker, outsideContents, {
|
|
1073
|
+
encoding: "utf8",
|
|
1074
|
+
flag: "wx",
|
|
1075
|
+
mode: 0o600,
|
|
1076
|
+
});
|
|
1077
|
+
outsideCreated = true;
|
|
1078
|
+
runtime.fs.writeFileSync(homeMarker, homeContents, {
|
|
1079
|
+
encoding: "utf8",
|
|
1080
|
+
flag: "wx",
|
|
1081
|
+
mode: 0o600,
|
|
1082
|
+
});
|
|
1083
|
+
homeCreated = true;
|
|
1084
|
+
runtime.fs.writeFileSync(devShmMarker, `host-dev-shm-${nonce}`, {
|
|
1085
|
+
encoding: "utf8",
|
|
1086
|
+
flag: "wx",
|
|
1087
|
+
mode: 0o600,
|
|
1088
|
+
});
|
|
1089
|
+
devShmCreated = true;
|
|
1090
|
+
const socketProbe =
|
|
1091
|
+
socketProbeRuntime.kind === "node"
|
|
1092
|
+
? {
|
|
1093
|
+
args: [
|
|
1094
|
+
"-e",
|
|
1095
|
+
[
|
|
1096
|
+
'const net = require("node:net");',
|
|
1097
|
+
'const socket = net.connect({host:"127.0.0.1",port:9});',
|
|
1098
|
+
"const timer = setTimeout(() => process.exit(4), 1000);",
|
|
1099
|
+
'socket.once("connect", () => { clearTimeout(timer); process.exit(5); });',
|
|
1100
|
+
'socket.once("error", (error) => {',
|
|
1101
|
+
" clearTimeout(timer);",
|
|
1102
|
+
' process.exit(error?.code === "EPERM" ? 0 : 6);',
|
|
1103
|
+
"});",
|
|
1104
|
+
].join(""),
|
|
1105
|
+
],
|
|
1106
|
+
}
|
|
1107
|
+
: {
|
|
1108
|
+
args: [
|
|
1109
|
+
"-I",
|
|
1110
|
+
"-S",
|
|
1111
|
+
"-c",
|
|
1112
|
+
[
|
|
1113
|
+
"import errno,socket,sys",
|
|
1114
|
+
"try:",
|
|
1115
|
+
" s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)",
|
|
1116
|
+
"except OSError as e:",
|
|
1117
|
+
" sys.exit(0 if e.errno==errno.EPERM else 6)",
|
|
1118
|
+
"else:",
|
|
1119
|
+
" s.close()",
|
|
1120
|
+
" sys.exit(5)",
|
|
1121
|
+
].join("\n"),
|
|
1122
|
+
],
|
|
1123
|
+
};
|
|
1124
|
+
const socketProbeCommand = [
|
|
1125
|
+
shellQuote(socketProbeRuntime.command),
|
|
1126
|
+
...socketProbe.args.map(shellQuote),
|
|
1127
|
+
].join(" ");
|
|
1128
|
+
const script = [
|
|
1129
|
+
"set -eu",
|
|
1130
|
+
"mount_is_read_only() {",
|
|
1131
|
+
' want="$1"',
|
|
1132
|
+
" seen=0",
|
|
1133
|
+
" while IFS=' ' read -r _ _ _ _ mountpoint mountopts _; do",
|
|
1134
|
+
' [ "$mountpoint" = "$want" ] || continue',
|
|
1135
|
+
" seen=1",
|
|
1136
|
+
' case ",$mountopts," in',
|
|
1137
|
+
" *,ro,*) ;;",
|
|
1138
|
+
" *) return 1 ;;",
|
|
1139
|
+
" esac",
|
|
1140
|
+
" done < /proc/self/mountinfo",
|
|
1141
|
+
' [ "$seen" = 1 ]',
|
|
1142
|
+
"}",
|
|
1143
|
+
"workspace_mount_topology_is_attested() {",
|
|
1144
|
+
` want=${shellQuote(encodeMountInfoPath(contract.workspaceRoot))}`,
|
|
1145
|
+
' prefix="${want}/"',
|
|
1146
|
+
" seen=0",
|
|
1147
|
+
" while IFS=' ' read -r _ _ _ _ mountpoint _; do",
|
|
1148
|
+
' [ "$mountpoint" = "$want" ] && seen=1',
|
|
1149
|
+
' case "$mountpoint" in',
|
|
1150
|
+
' "$prefix"*) return 1 ;;',
|
|
1151
|
+
" esac",
|
|
1152
|
+
` done < ${MOUNTINFO_PATH}`,
|
|
1153
|
+
' [ "$seen" = 1 ]',
|
|
1154
|
+
"}",
|
|
1155
|
+
"mount_is_read_only /",
|
|
1156
|
+
"mount_is_read_only /usr",
|
|
1157
|
+
"workspace_mount_topology_is_attested",
|
|
1158
|
+
`test ! -e ${shellQuote("/etc/shadow")}`,
|
|
1159
|
+
`test ! -e ${shellQuote(homeMarker)}`,
|
|
1160
|
+
`test ! -e ${shellQuote(workspaceHomeAliasMarker)}`,
|
|
1161
|
+
`test ! -e ${shellQuote(outsideMarker)}`,
|
|
1162
|
+
`test ! -e ${shellQuote(devShmMarker)}`,
|
|
1163
|
+
`printf %s ${shellQuote(nonce)} > ${shellQuote(devShmMarker)}`,
|
|
1164
|
+
`test "$(cat ${shellQuote(devShmMarker)})" = ${shellQuote(nonce)}`,
|
|
1165
|
+
`rm -f ${shellQuote(devShmMarker)}`,
|
|
1166
|
+
"if printf x > /chainless-undeclared-root 2>/dev/null; then exit 12; fi",
|
|
1167
|
+
"test ! -e /chainless-undeclared-root",
|
|
1168
|
+
"if printf x > /usr/.chainless-system-write 2>/dev/null; then exit 13; fi",
|
|
1169
|
+
"test ! -e /usr/.chainless-system-write",
|
|
1170
|
+
`test "$(readlink /proc/self/ns/net)" != ${shellQuote(
|
|
1171
|
+
hostNetworkNamespace,
|
|
1172
|
+
)}`,
|
|
1173
|
+
`printf %s ${shellQuote(nonce)} > ${shellQuote(workspaceMarker)}`,
|
|
1174
|
+
`test "$(cat ${shellQuote(workspaceMarker)})" = ${shellQuote(nonce)}`,
|
|
1175
|
+
`rm -f ${shellQuote(workspaceMarker)}`,
|
|
1176
|
+
socketProbeCommand,
|
|
1177
|
+
`printf '%s\\n%s\\n' ${shellQuote(call.policyDigest)} ${shellQuote(
|
|
1178
|
+
call.contractDigest,
|
|
1179
|
+
)}`,
|
|
1180
|
+
].join("\n");
|
|
1181
|
+
const result = runtime.spawnSync(
|
|
1182
|
+
call.command,
|
|
1183
|
+
[...call.args, "--", "/bin/sh", "-c", script],
|
|
1184
|
+
call.options,
|
|
1185
|
+
);
|
|
1186
|
+
const outsideUnchanged =
|
|
1187
|
+
outsideCreated &&
|
|
1188
|
+
String(runtime.fs.readFileSync(outsideMarker, "utf8")) ===
|
|
1189
|
+
outsideContents;
|
|
1190
|
+
const homeUnchanged =
|
|
1191
|
+
homeCreated &&
|
|
1192
|
+
String(runtime.fs.readFileSync(homeMarker, "utf8")) === homeContents;
|
|
1193
|
+
const devShmUnchanged =
|
|
1194
|
+
devShmCreated &&
|
|
1195
|
+
String(runtime.fs.readFileSync(devShmMarker, "utf8")) ===
|
|
1196
|
+
`host-dev-shm-${nonce}`;
|
|
1197
|
+
const expectedOutput = `${call.policyDigest}\n${call.contractDigest}\n`;
|
|
1198
|
+
const runnable =
|
|
1199
|
+
!result?.error &&
|
|
1200
|
+
result?.status === 0 &&
|
|
1201
|
+
String(result.stdout) === expectedOutput &&
|
|
1202
|
+
outsideUnchanged &&
|
|
1203
|
+
homeUnchanged &&
|
|
1204
|
+
devShmUnchanged;
|
|
1205
|
+
return {
|
|
1206
|
+
runnable,
|
|
1207
|
+
policyDigest: runnable ? call.policyDigest : null,
|
|
1208
|
+
contractDigest: runnable ? call.contractDigest : null,
|
|
1209
|
+
emptyRoot: runnable,
|
|
1210
|
+
undeclaredRootReadOnly: runnable,
|
|
1211
|
+
workspaceReadWrite: runnable,
|
|
1212
|
+
workspaceMountTopologyAttested: runnable,
|
|
1213
|
+
anonymousDevWritable: runnable,
|
|
1214
|
+
systemReadOnly: runnable,
|
|
1215
|
+
hostHomeHidden: runnable,
|
|
1216
|
+
outsideMarkerHidden: runnable,
|
|
1217
|
+
networkNamespace: runnable,
|
|
1218
|
+
networkNamespaceChanged: runnable,
|
|
1219
|
+
socketCreationDenied: runnable,
|
|
1220
|
+
};
|
|
1221
|
+
} catch {
|
|
1222
|
+
return { runnable: false };
|
|
1223
|
+
} finally {
|
|
1224
|
+
for (const marker of [
|
|
1225
|
+
workspaceMarker,
|
|
1226
|
+
outsideMarker,
|
|
1227
|
+
homeMarker,
|
|
1228
|
+
devShmMarker,
|
|
1229
|
+
]) {
|
|
1230
|
+
try {
|
|
1231
|
+
runtime.fs.unlinkSync(marker);
|
|
1232
|
+
} catch {
|
|
1233
|
+
// Probe cleanup is best effort; a failed probe stays fail-closed.
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
return {
|
|
1240
|
+
attestedContractDigest: contract.contractDigest,
|
|
1241
|
+
attestContract: launchStaysBound,
|
|
1242
|
+
attestFinal,
|
|
1243
|
+
closeProbe: () => closeSet(probeFds),
|
|
1244
|
+
cleanup,
|
|
1245
|
+
supervisor: {
|
|
1246
|
+
probeFd: supervisor.probeFd,
|
|
1247
|
+
finalFd: supervisor.finalFd,
|
|
1248
|
+
identity: supervisor.identity,
|
|
1249
|
+
sha256: supervisorFinalDigest,
|
|
1250
|
+
bytes: supervisorBytes,
|
|
1251
|
+
},
|
|
1252
|
+
workspace: {
|
|
1253
|
+
probeFd: workspace.probeFd,
|
|
1254
|
+
finalFd: workspace.finalFd,
|
|
1255
|
+
identity: workspace.identity,
|
|
1256
|
+
mountTopology: initialWorkspaceMountTopology,
|
|
1257
|
+
},
|
|
1258
|
+
system,
|
|
1259
|
+
systemSymlinks,
|
|
1260
|
+
etc,
|
|
1261
|
+
seccomp,
|
|
1262
|
+
target,
|
|
1263
|
+
probe,
|
|
1264
|
+
};
|
|
1265
|
+
} catch (error) {
|
|
1266
|
+
cleanup();
|
|
1267
|
+
throw error;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* Apply the generic strong backend. This function is called only by the Linux
|
|
1273
|
+
* branch in platform-sandbox; all runtime overrides are internal test seams.
|
|
1274
|
+
*/
|
|
1275
|
+
export function applyLinuxGenericWorkspaceSandbox(
|
|
1276
|
+
command,
|
|
1277
|
+
args,
|
|
1278
|
+
spawnOpts,
|
|
1279
|
+
sandboxOpts,
|
|
1280
|
+
runtimeOverrides = {},
|
|
1281
|
+
) {
|
|
1282
|
+
const runtime = runtimeFor(runtimeOverrides);
|
|
1283
|
+
const contract = sandboxOpts?.executionContract;
|
|
1284
|
+
const requiredBoundaries = Array.isArray(sandboxOpts?.requiredBoundaries)
|
|
1285
|
+
? sandboxOpts.requiredBoundaries
|
|
1286
|
+
: [];
|
|
1287
|
+
const provenance = {
|
|
1288
|
+
origin: spawnOpts?.origin || "unknown",
|
|
1289
|
+
command,
|
|
1290
|
+
args: [...(args || [])],
|
|
1291
|
+
cwd: spawnOpts?.cwd || process.cwd(),
|
|
1292
|
+
shell: spawnOpts?.shell,
|
|
1293
|
+
sync: sandboxOpts?.sync === true,
|
|
1294
|
+
stdio: spawnOpts?.stdio,
|
|
1295
|
+
requiredBoundaries,
|
|
1296
|
+
};
|
|
1297
|
+
let resources;
|
|
1298
|
+
try {
|
|
1299
|
+
resources = createTrustedResources(
|
|
1300
|
+
contract,
|
|
1301
|
+
command,
|
|
1302
|
+
args || [],
|
|
1303
|
+
spawnOpts || {},
|
|
1304
|
+
sandboxOpts || {},
|
|
1305
|
+
runtime,
|
|
1306
|
+
);
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
const preflightFailure =
|
|
1309
|
+
error?.code === "LINUX_GENERIC_BWRAP_UNAVAILABLE"
|
|
1310
|
+
? "linux_generic_bwrap_unavailable"
|
|
1311
|
+
: "linux_generic_resources_unattested";
|
|
1312
|
+
return planLinuxGenericBubblewrap(
|
|
1313
|
+
{
|
|
1314
|
+
contract,
|
|
1315
|
+
provenance,
|
|
1316
|
+
resources: null,
|
|
1317
|
+
environment: spawnOpts?.env || process.env,
|
|
1318
|
+
spawnOptions: spawnOpts,
|
|
1319
|
+
preflightFailure,
|
|
1320
|
+
},
|
|
1321
|
+
{ platform: runtime.platform },
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
return planLinuxGenericBubblewrap(
|
|
1325
|
+
{
|
|
1326
|
+
contract,
|
|
1327
|
+
provenance,
|
|
1328
|
+
resources,
|
|
1329
|
+
environment: spawnOpts?.env || process.env,
|
|
1330
|
+
spawnOptions: spawnOpts,
|
|
1331
|
+
probe: resources.probe,
|
|
1332
|
+
},
|
|
1333
|
+
{ platform: runtime.platform },
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
export function isLinuxGenericWorkspaceContract(contract) {
|
|
1338
|
+
return (
|
|
1339
|
+
contract?.contractVersion === 1 &&
|
|
1340
|
+
contract?.kind === LINUX_GENERIC_CONTRACT_KIND &&
|
|
1341
|
+
typeof contract?.contractDigest === "string" &&
|
|
1342
|
+
contract?.workspaceRoot?.startsWith("/") &&
|
|
1343
|
+
contract?.workingDirectory?.startsWith("/")
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
export { LINUX_GENERIC_BWRAP_BACKEND };
|