chainlesschain 0.166.76 → 0.166.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -8
- package/completions/_cc +1 -1
- package/completions/cc.bash +1 -1
- package/completions/cc.fish +1 -1
- package/completions/cc.ps1 +1 -1
- package/package.json +1 -1
- package/src/assets/linux-subreaper/linux-arm64/manifest.json +10 -0
- package/src/assets/linux-subreaper/linux-arm64/supervisor +0 -0
- package/src/assets/linux-subreaper/linux-x64/manifest.json +10 -0
- package/src/assets/linux-subreaper/linux-x64/supervisor +0 -0
- package/src/assets/web-panel/assets/{AIOps-CNaejClV.js → AIOps-DSyZEY2G.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CF_CVKT_.js → ActionButton-DhNyZ577.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-a3Z6J7gv.js → Analytics-HMMpxmyi.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BFtWEnct.js → AppLayout-CwKMM5Ie.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-BQ8oeMvX.js → Artifacts-CNzDmph7.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-NOo9bmgk.js → Audit-DxWpUW3V.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-QRc7F3Tp.js → BackgroundAgents-C1R2ST-P.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-UlVgLn2P.js → Backup-B6biTaEV.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BER9LJAr.js → BaseInput-CVnkD2Z5.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-Z8z2Fdfz.js → Chat-Dbewl4Je.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-DdmuBrdQ.js → ChatBubbleRenderer-DVZ6SwC5.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-CXPgyB8L.js → Checkbox-BqqiF1JE.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-D1vOsLJ7.js → Codegen-BVMkGumc.js} +1 -1
- package/src/assets/web-panel/assets/{Col-ucLnUJC7.js → Col-DmiRHy96.js} +1 -1
- package/src/assets/web-panel/assets/{Community-COsV9n9t.js → Community-BDlpygqA.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-D0EkBEYh.js → Compact-B2NME_m-.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-C8w9SUjt.js → Compliance-BlzDLE-R.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CmVaKEJS.js → Cowork-C8raPybB.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-B_FiMMV2.js → Cron-DA8Py0zP.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-ByO3YL8N.js → Crosschain-DU2XyHqB.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DwpsLM9r.js → DID-Df098fT_.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-B_8IUvM_.js → Dashboard-D1rT702x.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-Bo6WQkuX.js → Dropdown-BweXGo1O.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-C9LiQesx.js → EmailListRenderer-B9ShbgJH.js} +1 -1
- package/src/assets/web-panel/assets/EvolutionSettings-D75_iBoc.js +1 -0
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-Dst6VUnO.js → FamilyGuardDashboard-DrznZFVs.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-Ci7CoLkJ.js → Federation-DQZ2adIA.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-bdDyUhUM.js → FormItemContext-DN30Dowh.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CrRwOUVi.js → GenericCardRenderer-DRdt0j1e.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Dis8h-Z-.js → Git-wwRgStzk.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DX7OH5w2.js → Governance-CI4Uw0Nh.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Cavv_Q_Z.js → Inference-5TuE2AJI.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Cck8tnZn.js → KnowledgeGraph-oXyvmFSR.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BQt3aGMu.js → Logs-ewudSWuv.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-Cz5nyufm.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-CdmAKNSN.js → Marketplace-DcZeWFrs.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BUlJdyPY.js → McpTools-BcV9p1RS.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-DQRTQDr-.js → Memory-CMhYvugt.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-C0dnxgsl.js → MobileBridge-5RP2uwvT.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-DQ5Gy7fa.js → MobileProjects-BIuM9Ze4.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-Xl7u2isS.js → Mtc-DmBszTDe.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-C3KX0574.js → MtcAudit-Czdp5ZGt.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-CHEhfkn8.js → Multisig-DRWUuNr-.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-Ce38mH-8.js → NLProgramming-IRQQN0-c.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-C7nuTd7c.js → Notes-CGhD1lW9.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-BcoEcyq8.js → NotificationSettings-BtyPgQAh.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-D-t5hKkx.js → OrderTableRenderer-yUaqwCty.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-C_BGi4xi.js → Organization-C3kTVn5i.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BodhraDq.js → Overflow-C2-0iZhq.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BTa3sAIy.js → P2P-CISoEUxA.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BjZslP9T.js → PdhVaultBrowser-NfTVtvKQ.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-u3UDzGef.js → Permissions-CfK0jJhm.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-Cb7ybv65.js → PersonalDataHub-OsVxxh_j.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-CS24k-P7.js → Pipeline-DEmK_Ch2.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-Ddj6Kqs7.js → Privacy-D61s73b0.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Bg5uQIgg.js → ProjectInit-DkLF5gHb.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-AmPR8T12.js → ProjectSettings-BA7odTrI.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-DGSbAyTK.js → Projects-CdI1GcAG.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-Dc1C3lbB.js → Providers-D9BYyP1r.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-Cv3HXpce.js → QrScannerModal-CXMItUDD.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-3dpbXARb.js → QuickAsk-8-tF9ffZ.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-5aGRVnJK.js → Recommend-C2QGWOSi.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-CTS2jx0p.js → RemoteSession-B1M_v1lx.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-BxaKI0hY.js → Reputation-D0WZHgv1.js} +1 -1
- package/src/assets/web-panel/assets/{Row-C23uQdwd.js → Row-D6nf9-c-.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-ssX9YOhm.js → RssFeed-ClZ6M-mN.js} +3 -3
- package/src/assets/web-panel/assets/{Search-C24GogII.js → Search-ZWkt8vuO.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DdAcGKkk.js → Security-BUlQut2m.js} +3 -3
- package/src/assets/web-panel/assets/{Services-D0dCiEuq.js → Services-Cd_3xJOc.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DEY44o0v.js → Skeleton-C8e2-cHW.js} +1 -1
- package/src/assets/web-panel/assets/Skills-DucVRJp1.js +1 -0
- package/src/assets/web-panel/assets/{Sla-B_bbo2vB.js → Sla-Csr5pUwI.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-CXvyaRAT.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-CpIXql0Z.js → SyncSettings-KdsIcV5h.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-B-eHmJm9.js +1 -0
- package/src/assets/web-panel/assets/{Templates-C_44nVPq.js → Templates-BglVeaLK.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CyZ2ey2P.js → Tenant-BBbQ8qan.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DwGNHWnp.js → Terminal-Ca_pCVBi.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BTAHNkqU.js → TimelineRenderer-YD6XG_vZ.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DCJWkUwc.js → Tokens-2lRoeVli.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BCjDO9-E.js → Trigger-C-3XNAU9.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-_YS8WbRb.js → Trust-k-1PxdLG.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-m6R8xGq7.js → UkeySign-FG-qW646.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-xOfk29c4.js → VideoEditing-BNOe1cmd.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-B1Gw0awc.js → Wallet-D5C3i2Kw.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-CHK5ft18.js → WebAuthn-BOpztqd3.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-DEp1ukQQ.js → WorkflowEditor-BpVrpUEv.js} +1 -1
- package/src/assets/web-panel/assets/{chat-B2sFizun.js → chat-DIuMHb38.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DJ17YV1E.js → colors-CTFJPyzQ.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CBhJJgEC.js → compact-item-CONfmJcW.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BZkpvNJy.js → createContext-RkW5CRrz.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DaEBln_q.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-C0ZKo6Ei.js → hasIn-CW4XOjDC.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bc3vVG2X.js → index-7wMSBCU7.js} +1 -1
- package/src/assets/web-panel/assets/{index-vFMwiAN4.js → index-Akmg1WdH.js} +1 -1
- package/src/assets/web-panel/assets/{index-DOBXNMdh.js → index-B54qoVff.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLo8Ls9u.js → index-B8r2t0Ch.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7gv9dvb.js → index-B9XoBoty.js} +1 -1
- package/src/assets/web-panel/assets/{index-DHtpMr1G.js → index-B9aS2VLu.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dxe0YlZe.js → index-BFs1Tznc.js} +1 -1
- package/src/assets/web-panel/assets/{index-DumLgj1t.js → index-BKgaRT0b.js} +1 -1
- package/src/assets/web-panel/assets/{index-BNp6HKFh.js → index-BM7bOUvu.js} +1 -1
- package/src/assets/web-panel/assets/{index-DM0rWF6z.js → index-BMg2JFmW.js} +1 -1
- package/src/assets/web-panel/assets/{index-4oULtRqE.js → index-BRZsPxNn.js} +1 -1
- package/src/assets/web-panel/assets/{index-LPPe0val.js → index-Bf19fKef.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ctguam0K.js → index-BkAbfBF-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DrU5xVpF.js → index-CBjb2A5R.js} +1 -1
- package/src/assets/web-panel/assets/{index-CCOr6Q27.js → index-CEZWQthY.js} +1 -1
- package/src/assets/web-panel/assets/{index-C9gYiACH.js → index-CFGLf88d.js} +1 -1
- package/src/assets/web-panel/assets/{index-CZW84cEf.js → index-CLUa68JH.js} +3 -3
- package/src/assets/web-panel/assets/{index-BoDUYyuo.js → index-CM2X25Er.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyiCbHeL.js → index-CX4T2uKN.js} +1 -1
- package/src/assets/web-panel/assets/{index-CW5o1Gar.js → index-CixqfdRk.js} +1 -1
- package/src/assets/web-panel/assets/{index-5K2z_tgO.js → index-CkRojEip.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOfPp5OV.js → index-Cvmc3f25.js} +1 -1
- package/src/assets/web-panel/assets/{index-CdxJm-HB.js → index-D-lpTPS8.js} +1 -1
- package/src/assets/web-panel/assets/{index-8ueG48gl.js → index-D59GbWCN.js} +1 -1
- package/src/assets/web-panel/assets/{index-BMxOoITz.js → index-DAESljiJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLMr5KMd.js → index-DDHUx4Yz.js} +1 -1
- package/src/assets/web-panel/assets/index-DG1mcYTa.js +1 -0
- package/src/assets/web-panel/assets/{index-BGIvhCPl.js → index-DP5pPedy.js} +1 -1
- package/src/assets/web-panel/assets/index-DRs5Pe8u.js +1 -0
- package/src/assets/web-panel/assets/{index-DjdSFO0d.js → index-DSlXL6Ju.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cn5UzvgR.js → index-DWCJnn2O.js} +1 -1
- package/src/assets/web-panel/assets/{index-Chct9tL8.js → index-DWY7YwDu.js} +1 -1
- package/src/assets/web-panel/assets/{index-BEnd5fiz.js → index-DZTwP364.js} +1 -1
- package/src/assets/web-panel/assets/{index-KzNhWykP.js → index-DidrMvOx.js} +1 -1
- package/src/assets/web-panel/assets/{index-BpyrhOeh.js → index-Dj3K5kHs.js} +1 -1
- package/src/assets/web-panel/assets/{index-CZlkPrxb.js → index-YaeGscSw.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dfgxe-fw.js → index-YlMnFNOi.js} +1 -1
- package/src/assets/web-panel/assets/{index-IHgzfc1u.js → index-asWsB34i.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJkoJzTU.js → index-gkjiFjQd.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-Cqfoq0TJ.js → initDefaultProps-BOr4igh3.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CIiVJHNu.js → motion-CK469n2j.js} +1 -1
- package/src/assets/web-panel/assets/{move-CFl1o_A8.js → move-DzjudlYQ.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DYoCxcZ1.js → mtc-parser-BQaKUxC6.js} +1 -1
- package/src/assets/web-panel/assets/{omit-C1P2q5TY.js → omit-MciovCFD.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BtTRpJtC.js → pickAttrs-DDPJ4zAz.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-Cst2PcNM.js → placementArrow-Cznyb-jI.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-CF5kalV2.js → responsiveObserve-AJK3SLzJ.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CtHGqpXN.js → slide-CPQ28wu7.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-N__biHXf.js → statusUtils-CFjzvqf_.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DR8V_H-i.js → styleChecker-BPyFfTuR.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-D274dqun.js → useFlexGapSupport-Dq7N6R05.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BwHLYU5b.js → useFs-C3GS0uTc.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BiA-hVqt.js → usePersonalDataHub-BhgIVzP_.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-Bugl94dy.js → vnode-4ZD9-MDr.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DEFl8GCu.js → zoom-Bx4MRVNW.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +1 -1
- package/src/command-manifest.json +1 -1
- package/src/commands/checkpoint.js +42 -7
- package/src/commands/orchestrate.js +85 -25
- package/src/commands/session-location.js +5 -0
- package/src/commands/session-show.js +109 -1
- package/src/commands/session.js +26 -2
- package/src/commands/tokens.js +9 -4
- package/src/data/changelog.json +26 -0
- package/src/generated/cc-agent-protocol.schema.json +71 -0
- package/src/harness/jsonl-session-store.js +357 -114
- package/src/harness/mcp-client.js +44 -1
- package/src/lib/agent-router.js +35 -7
- package/src/lib/app-server/server.js +14 -0
- package/src/lib/background-agent-supervisor.js +14 -4
- package/src/lib/causal-observability.js +64 -1
- package/src/lib/checkpoint-summary-projection.js +98 -0
- package/src/lib/checkpoint-timeline-authority.js +15 -51
- package/src/lib/claude-code-bridge.js +302 -60
- package/src/lib/codex-app-server-adapter.js +161 -35
- package/src/lib/cost-budget.js +4 -0
- package/src/lib/decision-layer/providers.js +84 -9
- package/src/lib/decision-layer/runtime.js +86 -49
- package/src/lib/direct-model-usage.js +14 -3
- package/src/lib/doctor-checkup.js +31 -2
- package/src/lib/durable-security-store.js +6 -1
- package/src/lib/evolution/evolution-artifact-identity.js +21 -0
- package/src/lib/evolution/evolution-artifact-ports.js +5 -11
- package/src/lib/evolution/evolution-eval-cohort-receipt-reconciliation.js +389 -0
- package/src/lib/execution-location-failure-site.js +63 -0
- package/src/lib/execution-location-target.js +102 -8
- package/src/lib/external-agent-adapters.js +2 -0
- package/src/lib/image-file-boundary.js +154 -0
- package/src/lib/image-input.js +16 -3
- package/src/lib/llm-pricing.js +114 -8
- package/src/lib/mcp-call-ledger-store.js +7 -2
- package/src/lib/model-capabilities.js +21 -10
- package/src/lib/model-context-catalog.js +60 -1
- package/src/lib/openai-responses.js +7 -1
- package/src/lib/orchestrator.js +8 -2
- package/src/lib/process-execution-broker/index.js +93 -1
- package/src/lib/process-execution-broker/linux-subreaper-artifact.js +120 -0
- package/src/lib/process-execution-broker/linux-subreaper-helper.js +344 -0
- package/src/lib/process-execution-broker/linux-subreaper-process.js +326 -0
- package/src/lib/process-execution-broker/linux-subreaper-supervisor.c +360 -0
- package/src/lib/process-execution-broker/owned-posix-process-group-worker.mjs +131 -0
- package/src/lib/process-execution-broker/owned-posix-process-group.js +267 -0
- package/src/lib/process-execution-broker/process-ownership-journal.js +297 -0
- package/src/lib/process-execution-broker/process-ownership-quarantine.js +91 -0
- package/src/lib/runtime-usage-ledger.js +3 -0
- package/src/lib/secure-fs.js +158 -42
- package/src/lib/session-branch-history.js +240 -0
- package/src/lib/session-history-origins.js +285 -0
- package/src/lib/session-input-receipt.js +110 -0
- package/src/lib/session-resource-budget.js +8 -8
- package/src/lib/session-transcript-history.js +456 -0
- package/src/lib/session-transcript-page.js +164 -0
- package/src/lib/session-transcript-reference.js +38 -0
- package/src/lib/session-usage.js +8 -0
- package/src/lib/token-tracker.js +32 -54
- package/src/lib/usage-pricing-context.js +93 -0
- package/src/runtime/headless-runner.js +8 -0
- package/src/runtime/headless-stream.js +202 -4
- package/src/assets/web-panel/assets/EvolutionSettings-Pc1IpRfk.js +0 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-BIUeeiVA.js +0 -1
- package/src/assets/web-panel/assets/Skills-CYt0Mu2W.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-a2BBjtTL.js +0 -1
- package/src/assets/web-panel/assets/Tasks-BYQKU-GL.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DZhu13vj.js +0 -1
- package/src/assets/web-panel/assets/index-C6iFyQYz.js +0 -1
- package/src/assets/web-panel/assets/index-CztThH9f.js +0 -1
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import {
|
|
7
|
+
LINUX_SUBREAPER_SOURCE_DIGEST,
|
|
8
|
+
MAX_SUBREAPER_IMAGE_BYTES,
|
|
9
|
+
MAX_SUBREAPER_MANIFEST_BYTES,
|
|
10
|
+
validateLinuxSubreaperArtifact,
|
|
11
|
+
validateLinuxSubreaperElf,
|
|
12
|
+
} from "./linux-subreaper-artifact.js";
|
|
13
|
+
export { LINUX_SUBREAPER_SOURCE_DIGEST } from "./linux-subreaper-artifact.js";
|
|
14
|
+
|
|
15
|
+
const MAX_SOURCE_BYTES = 128 * 1024;
|
|
16
|
+
const MAX_IMAGE_BYTES = MAX_SUBREAPER_IMAGE_BYTES;
|
|
17
|
+
const images = new Map();
|
|
18
|
+
const leases = new WeakMap();
|
|
19
|
+
const sha = (bytes) =>
|
|
20
|
+
"sha256:" + crypto.createHash("sha256").update(bytes).digest("hex");
|
|
21
|
+
|
|
22
|
+
function failure(reason) {
|
|
23
|
+
const error = new Error(
|
|
24
|
+
`Linux process supervision helper unavailable: ${reason}`,
|
|
25
|
+
);
|
|
26
|
+
error.code = "EXTERNAL_AGENT_HELPER_UNAVAILABLE";
|
|
27
|
+
return error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readDescriptor(fd, maximum) {
|
|
31
|
+
const before = fs.fstatSync(fd, { bigint: true });
|
|
32
|
+
if (!before.isFile() || before.size < 1n || before.size > BigInt(maximum))
|
|
33
|
+
throw failure("invalid-file");
|
|
34
|
+
const bytes = Buffer.alloc(Number(before.size));
|
|
35
|
+
let offset = 0;
|
|
36
|
+
while (offset < bytes.length) {
|
|
37
|
+
const count = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
38
|
+
if (count <= 0) throw failure("short-file-read");
|
|
39
|
+
offset += count;
|
|
40
|
+
}
|
|
41
|
+
const after = fs.fstatSync(fd, { bigint: true });
|
|
42
|
+
if (
|
|
43
|
+
before.dev !== after.dev ||
|
|
44
|
+
before.ino !== after.ino ||
|
|
45
|
+
before.size !== after.size ||
|
|
46
|
+
before.mtimeNs !== after.mtimeNs ||
|
|
47
|
+
before.ctimeNs !== after.ctimeNs
|
|
48
|
+
)
|
|
49
|
+
throw failure("file-changed");
|
|
50
|
+
return { bytes, stat: after };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function compilerPath() {
|
|
54
|
+
// Only the system toolchain is eligible. Do not consult PATH, CC, loader,
|
|
55
|
+
// include-path, or plugin-provided compiler settings.
|
|
56
|
+
const compiler = fs.realpathSync("/usr/bin/cc");
|
|
57
|
+
let current = compiler;
|
|
58
|
+
for (;;) {
|
|
59
|
+
const info = fs.lstatSync(current);
|
|
60
|
+
if (info.isSymbolicLink() || info.uid !== 0 || (info.mode & 0o022) !== 0)
|
|
61
|
+
throw failure("untrusted-system-compiler");
|
|
62
|
+
if (current === compiler && (!info.isFile() || (info.mode & 0o111) === 0))
|
|
63
|
+
throw failure("invalid-system-compiler");
|
|
64
|
+
if (current === "/") break;
|
|
65
|
+
current = path.dirname(current);
|
|
66
|
+
}
|
|
67
|
+
return compiler;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function packagedImage() {
|
|
71
|
+
const root = fileURLToPath(
|
|
72
|
+
new URL("../../assets/linux-subreaper/", import.meta.url),
|
|
73
|
+
);
|
|
74
|
+
// Only source checkouts with no packaged directory may use the development
|
|
75
|
+
// compiler path. A partial/corrupt installed payload must never fall back.
|
|
76
|
+
try {
|
|
77
|
+
if (!fs.lstatSync(root).isDirectory())
|
|
78
|
+
throw failure("invalid-packaged-directory");
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error.code === "ENOENT") return null;
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
let rootFd = null;
|
|
84
|
+
let dirFd = null;
|
|
85
|
+
const read = (name, maximum) => {
|
|
86
|
+
const fd = fs.openSync(
|
|
87
|
+
`/proc/self/fd/${dirFd}/${name}`,
|
|
88
|
+
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW,
|
|
89
|
+
);
|
|
90
|
+
try {
|
|
91
|
+
const snapshot = readDescriptor(fd, maximum);
|
|
92
|
+
if (snapshot.stat.nlink !== 1n) throw failure("invalid-packaged-file");
|
|
93
|
+
return snapshot.bytes;
|
|
94
|
+
} finally {
|
|
95
|
+
fs.closeSync(fd);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
try {
|
|
99
|
+
rootFd = fs.openSync(
|
|
100
|
+
root,
|
|
101
|
+
fs.constants.O_RDONLY |
|
|
102
|
+
fs.constants.O_DIRECTORY |
|
|
103
|
+
fs.constants.O_NOFOLLOW,
|
|
104
|
+
);
|
|
105
|
+
dirFd = fs.openSync(
|
|
106
|
+
`/proc/self/fd/${rootFd}/linux-${process.arch}`,
|
|
107
|
+
fs.constants.O_RDONLY |
|
|
108
|
+
fs.constants.O_DIRECTORY |
|
|
109
|
+
fs.constants.O_NOFOLLOW,
|
|
110
|
+
);
|
|
111
|
+
const manifest = read("manifest.json", MAX_SUBREAPER_MANIFEST_BYTES);
|
|
112
|
+
const bytes = read("supervisor", MAX_IMAGE_BYTES);
|
|
113
|
+
const identity = validateLinuxSubreaperArtifact(manifest, bytes, {
|
|
114
|
+
arch: process.arch,
|
|
115
|
+
});
|
|
116
|
+
return { bytes, identity };
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error.code === "EXTERNAL_AGENT_HELPER_UNAVAILABLE") throw error;
|
|
119
|
+
throw failure("packaged-helper-unavailable");
|
|
120
|
+
} finally {
|
|
121
|
+
if (dirFd !== null) fs.closeSync(dirFd);
|
|
122
|
+
if (rootFd !== null) fs.closeSync(rootFd);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function compileImage(spawnSync) {
|
|
127
|
+
let sourceFd = null;
|
|
128
|
+
let directoryFd = null;
|
|
129
|
+
let imageFd = null;
|
|
130
|
+
let temporaryRoot = null;
|
|
131
|
+
let imagePath = null;
|
|
132
|
+
try {
|
|
133
|
+
// Vite may import the Broker through an http: module URL on non-Linux
|
|
134
|
+
// hosts. Resolve this Linux-only asset only when native supervision runs.
|
|
135
|
+
const source = fileURLToPath(
|
|
136
|
+
new URL("./linux-subreaper-supervisor.c", import.meta.url),
|
|
137
|
+
);
|
|
138
|
+
sourceFd = fs.openSync(
|
|
139
|
+
source,
|
|
140
|
+
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW,
|
|
141
|
+
);
|
|
142
|
+
const snapshot = readDescriptor(sourceFd, MAX_SOURCE_BYTES);
|
|
143
|
+
// npm/git may transport text with CRLF. Compile the exact canonical bytes
|
|
144
|
+
// that the source digest commits to, passed on stdin rather than reopened.
|
|
145
|
+
const sourceBytes = Buffer.from(
|
|
146
|
+
snapshot.bytes.toString("utf8").replace(/\r\n/g, "\n"),
|
|
147
|
+
);
|
|
148
|
+
if (
|
|
149
|
+
snapshot.stat.nlink !== 1n ||
|
|
150
|
+
sha(sourceBytes) !== LINUX_SUBREAPER_SOURCE_DIGEST
|
|
151
|
+
)
|
|
152
|
+
throw failure("source-digest-mismatch");
|
|
153
|
+
fs.closeSync(sourceFd);
|
|
154
|
+
sourceFd = null;
|
|
155
|
+
const packaged = packagedImage();
|
|
156
|
+
const compiler = packaged ? null : compilerPath();
|
|
157
|
+
temporaryRoot = fs.mkdtempSync(
|
|
158
|
+
path.join(os.tmpdir(), "cc-linux-subreaper-build-"),
|
|
159
|
+
);
|
|
160
|
+
fs.chmodSync(temporaryRoot, 0o700);
|
|
161
|
+
directoryFd = fs.openSync(
|
|
162
|
+
temporaryRoot,
|
|
163
|
+
fs.constants.O_RDONLY |
|
|
164
|
+
fs.constants.O_DIRECTORY |
|
|
165
|
+
fs.constants.O_NOFOLLOW,
|
|
166
|
+
);
|
|
167
|
+
const directory = fs.fstatSync(directoryFd);
|
|
168
|
+
if (
|
|
169
|
+
!directory.isDirectory() ||
|
|
170
|
+
directory.uid !== process.getuid() ||
|
|
171
|
+
(directory.mode & 0o777) !== 0o700
|
|
172
|
+
)
|
|
173
|
+
throw failure("untrusted-build-directory");
|
|
174
|
+
imagePath = path.join(temporaryRoot, "supervisor");
|
|
175
|
+
// The compiler writes through its inherited directory handle. An ancestor
|
|
176
|
+
// rename cannot redirect the build to a caller-supplied executable path.
|
|
177
|
+
if (packaged) {
|
|
178
|
+
fs.writeFileSync(
|
|
179
|
+
`/proc/self/fd/${directoryFd}/supervisor`,
|
|
180
|
+
packaged.bytes,
|
|
181
|
+
{ flag: "wx", mode: 0o500 },
|
|
182
|
+
);
|
|
183
|
+
} else {
|
|
184
|
+
const built = spawnSync(
|
|
185
|
+
compiler,
|
|
186
|
+
[
|
|
187
|
+
"-std=c11",
|
|
188
|
+
"-Wall",
|
|
189
|
+
"-Wextra",
|
|
190
|
+
"-Werror",
|
|
191
|
+
"-O2",
|
|
192
|
+
"-fstack-protector-strong",
|
|
193
|
+
"-D_FORTIFY_SOURCE=2",
|
|
194
|
+
"-x",
|
|
195
|
+
"c",
|
|
196
|
+
"-",
|
|
197
|
+
"-o",
|
|
198
|
+
"/proc/self/fd/3/supervisor",
|
|
199
|
+
],
|
|
200
|
+
{
|
|
201
|
+
cwd: temporaryRoot,
|
|
202
|
+
env: { PATH: "/usr/bin:/bin", LC_ALL: "C" },
|
|
203
|
+
shell: false,
|
|
204
|
+
input: sourceBytes,
|
|
205
|
+
timeout: 30000,
|
|
206
|
+
maxBuffer: 1024 * 1024,
|
|
207
|
+
stdio: ["pipe", "pipe", "pipe", directoryFd],
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
if (built.error || built.status !== 0 || built.signal)
|
|
211
|
+
throw failure("native-build-failed");
|
|
212
|
+
}
|
|
213
|
+
imageFd = fs.openSync(
|
|
214
|
+
`/proc/self/fd/${directoryFd}/supervisor`,
|
|
215
|
+
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW,
|
|
216
|
+
);
|
|
217
|
+
fs.fchmodSync(imageFd, 0o500);
|
|
218
|
+
const image = readDescriptor(imageFd, MAX_IMAGE_BYTES);
|
|
219
|
+
if (
|
|
220
|
+
image.stat.uid !== BigInt(process.getuid()) ||
|
|
221
|
+
image.stat.nlink !== 1n ||
|
|
222
|
+
(image.stat.mode & 0o777n) !== 0o500n
|
|
223
|
+
)
|
|
224
|
+
throw failure("untrusted-helper-image");
|
|
225
|
+
validateLinuxSubreaperElf(image.bytes, {
|
|
226
|
+
arch: process.arch,
|
|
227
|
+
staticOnly: Boolean(packaged),
|
|
228
|
+
});
|
|
229
|
+
if (packaged && sha(image.bytes) !== packaged.identity.imageDigest)
|
|
230
|
+
throw failure("packaged-copy-changed");
|
|
231
|
+
// Remove the name before granting a launch lease. The executable can only
|
|
232
|
+
// be reached through the held descriptor; a later pathname swap is inert.
|
|
233
|
+
fs.unlinkSync(`/proc/self/fd/${directoryFd}/supervisor`);
|
|
234
|
+
imagePath = null;
|
|
235
|
+
const held = fs.fstatSync(imageFd, { bigint: true });
|
|
236
|
+
if (
|
|
237
|
+
held.nlink !== 0n ||
|
|
238
|
+
held.dev !== image.stat.dev ||
|
|
239
|
+
held.ino !== image.stat.ino
|
|
240
|
+
)
|
|
241
|
+
throw failure("helper-unlink-unconfirmed");
|
|
242
|
+
const entry = {
|
|
243
|
+
fd: imageFd,
|
|
244
|
+
dev: held.dev,
|
|
245
|
+
ino: held.ino,
|
|
246
|
+
digest: sha(image.bytes),
|
|
247
|
+
distribution: packaged ? "packaged-static" : "local-build",
|
|
248
|
+
};
|
|
249
|
+
imageFd = null;
|
|
250
|
+
return entry;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (error.code === "EXTERNAL_AGENT_HELPER_UNAVAILABLE") throw error;
|
|
253
|
+
// Do not expose compiler stdout/stderr, paths, environment, or native argv.
|
|
254
|
+
throw failure(
|
|
255
|
+
error.code === "ENOENT"
|
|
256
|
+
? "system-compiler-or-source-missing"
|
|
257
|
+
: "native-build-setup-failed",
|
|
258
|
+
);
|
|
259
|
+
} finally {
|
|
260
|
+
if (sourceFd !== null) fs.closeSync(sourceFd);
|
|
261
|
+
if (imageFd !== null) fs.closeSync(imageFd);
|
|
262
|
+
if (imagePath && directoryFd !== null) {
|
|
263
|
+
try {
|
|
264
|
+
fs.unlinkSync(`/proc/self/fd/${directoryFd}/supervisor`);
|
|
265
|
+
} catch {
|
|
266
|
+
/* Preserve the admission failure. */
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (directoryFd !== null) fs.closeSync(directoryFd);
|
|
270
|
+
if (temporaryRoot) {
|
|
271
|
+
try {
|
|
272
|
+
fs.rmdirSync(temporaryRoot);
|
|
273
|
+
} catch {
|
|
274
|
+
/* Never recursively remove an unexpected build entry. */
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Acquire a one-use descriptor lease from trusted packaged source. */
|
|
281
|
+
export function acquireLinuxSubreaperHelper({ spawnSync }) {
|
|
282
|
+
if (process.platform !== "linux" || typeof spawnSync !== "function")
|
|
283
|
+
throw failure("unsupported-host");
|
|
284
|
+
let entry = images.get(spawnSync);
|
|
285
|
+
if (!entry) {
|
|
286
|
+
entry = compileImage(spawnSync);
|
|
287
|
+
images.set(spawnSync, entry);
|
|
288
|
+
// Outstanding leases hold their own duplicated descriptor. Bound cached
|
|
289
|
+
// native-seam variants without invalidating already-started children.
|
|
290
|
+
if (images.size > 4) {
|
|
291
|
+
const oldest = images.keys().next().value;
|
|
292
|
+
fs.closeSync(images.get(oldest).fd);
|
|
293
|
+
images.delete(oldest);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const held = readDescriptor(entry.fd, MAX_IMAGE_BYTES);
|
|
297
|
+
if (
|
|
298
|
+
held.stat.dev !== entry.dev ||
|
|
299
|
+
held.stat.ino !== entry.ino ||
|
|
300
|
+
held.stat.nlink !== 0n ||
|
|
301
|
+
sha(held.bytes) !== entry.digest
|
|
302
|
+
)
|
|
303
|
+
throw failure("cached-helper-changed");
|
|
304
|
+
const descriptor = fs.openSync(
|
|
305
|
+
`/proc/self/fd/${entry.fd}`,
|
|
306
|
+
fs.constants.O_RDONLY,
|
|
307
|
+
);
|
|
308
|
+
try {
|
|
309
|
+
const borrowed = fs.fstatSync(descriptor, { bigint: true });
|
|
310
|
+
if (
|
|
311
|
+
borrowed.dev !== entry.dev ||
|
|
312
|
+
borrowed.ino !== entry.ino ||
|
|
313
|
+
borrowed.nlink !== 0n
|
|
314
|
+
)
|
|
315
|
+
throw failure("helper-lease-changed");
|
|
316
|
+
} catch (error) {
|
|
317
|
+
fs.closeSync(descriptor);
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
const lease = Object.freeze({ kind: "linux-subreaper-helper-lease/v1" });
|
|
321
|
+
leases.set(lease, {
|
|
322
|
+
descriptor,
|
|
323
|
+
sourceDigest: LINUX_SUBREAPER_SOURCE_DIGEST,
|
|
324
|
+
imageDigest: entry.digest,
|
|
325
|
+
distribution: entry.distribution,
|
|
326
|
+
});
|
|
327
|
+
return lease;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function consumeLinuxSubreaperHelper(lease) {
|
|
331
|
+
const entry = leases.get(lease);
|
|
332
|
+
if (!entry) throw failure("invalid-or-consumed-helper-lease");
|
|
333
|
+
leases.delete(lease);
|
|
334
|
+
let released = false;
|
|
335
|
+
return Object.freeze({
|
|
336
|
+
...entry,
|
|
337
|
+
release() {
|
|
338
|
+
if (!released) {
|
|
339
|
+
released = true;
|
|
340
|
+
fs.closeSync(entry.descriptor);
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { constants } from "node:os";
|
|
4
|
+
import { consumeLinuxSubreaperHelper } from "./linux-subreaper-helper.js";
|
|
5
|
+
|
|
6
|
+
const MAGIC = Buffer.from("CCSUBR01");
|
|
7
|
+
const MAX_FRAME = 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
export function encodeLinuxSubreaperLaunch(command, args, options) {
|
|
10
|
+
if (
|
|
11
|
+
typeof command !== "string" ||
|
|
12
|
+
!command ||
|
|
13
|
+
command.includes("\0") ||
|
|
14
|
+
!Array.isArray(args) ||
|
|
15
|
+
args.length >= 4096 ||
|
|
16
|
+
args.some((arg) => typeof arg !== "string" || arg.includes("\0")) ||
|
|
17
|
+
typeof options.cwd !== "string" ||
|
|
18
|
+
!isAbsolute(options.cwd) ||
|
|
19
|
+
options.cwd.includes("\0") ||
|
|
20
|
+
!options.env ||
|
|
21
|
+
typeof options.env !== "object" ||
|
|
22
|
+
Array.isArray(options.env)
|
|
23
|
+
)
|
|
24
|
+
throw new TypeError("Invalid Linux subreaper launch");
|
|
25
|
+
const entries = Object.entries(options.env);
|
|
26
|
+
if (
|
|
27
|
+
entries.length > 8192 ||
|
|
28
|
+
entries.some(
|
|
29
|
+
([key, value]) =>
|
|
30
|
+
!key ||
|
|
31
|
+
key.includes("=") ||
|
|
32
|
+
key.includes("\0") ||
|
|
33
|
+
typeof value !== "string" ||
|
|
34
|
+
value.includes("\0"),
|
|
35
|
+
)
|
|
36
|
+
) {
|
|
37
|
+
throw new TypeError("Invalid Linux subreaper environment");
|
|
38
|
+
}
|
|
39
|
+
const graceMs = options.graceMs ?? 250;
|
|
40
|
+
if (!Number.isSafeInteger(graceMs) || graceMs < 0 || graceMs > 2147483647)
|
|
41
|
+
throw new TypeError("Invalid Linux subreaper grace period");
|
|
42
|
+
const strings = [
|
|
43
|
+
options.cwd,
|
|
44
|
+
command,
|
|
45
|
+
...args,
|
|
46
|
+
...entries.map(([key, value]) => `${key}=${value}`),
|
|
47
|
+
].map((value) => Buffer.from(value, "utf8"));
|
|
48
|
+
const length = 24 + strings.reduce((sum, value) => sum + 4 + value.length, 0);
|
|
49
|
+
if (length > MAX_FRAME)
|
|
50
|
+
throw new TypeError("Linux subreaper launch exceeds its byte limit");
|
|
51
|
+
const frame = Buffer.alloc(length);
|
|
52
|
+
MAGIC.copy(frame);
|
|
53
|
+
frame.writeUInt32BE(length, 8);
|
|
54
|
+
frame.writeUInt32BE(args.length + 1, 12);
|
|
55
|
+
frame.writeUInt32BE(entries.length, 16);
|
|
56
|
+
frame.writeUInt32BE(graceMs, 20);
|
|
57
|
+
let offset = 24;
|
|
58
|
+
for (const value of strings) {
|
|
59
|
+
frame.writeUInt32BE(value.length, offset);
|
|
60
|
+
offset += 4;
|
|
61
|
+
value.copy(frame, offset);
|
|
62
|
+
offset += value.length;
|
|
63
|
+
}
|
|
64
|
+
return frame;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Native lifecycle primitive for an admitted Broker launch plan. Production
|
|
69
|
+
* uses a one-use, privately branded helper descriptor. The explicit path seam
|
|
70
|
+
* remains for native build/verification tests, never Broker request options.
|
|
71
|
+
*/
|
|
72
|
+
export function spawnLinuxSubreaper(command, args, options, native) {
|
|
73
|
+
// Claim the lease before validating the launch so every rejection closes its
|
|
74
|
+
// descriptor. A bad plan must not strand an otherwise valid installation pin.
|
|
75
|
+
const helper = options.helper
|
|
76
|
+
? consumeLinuxSubreaperHelper(options.helper)
|
|
77
|
+
: null;
|
|
78
|
+
let launch;
|
|
79
|
+
let child;
|
|
80
|
+
try {
|
|
81
|
+
if (
|
|
82
|
+
(native.platform ?? process.platform) !== "linux" ||
|
|
83
|
+
typeof native.spawn !== "function"
|
|
84
|
+
)
|
|
85
|
+
throw new TypeError("Linux Broker native spawn is required");
|
|
86
|
+
if (
|
|
87
|
+
!options.helper &&
|
|
88
|
+
(typeof options.helperPath !== "string" ||
|
|
89
|
+
!isAbsolute(options.helperPath))
|
|
90
|
+
)
|
|
91
|
+
throw new TypeError(
|
|
92
|
+
"An absolute Linux subreaper helper path is required",
|
|
93
|
+
);
|
|
94
|
+
if (options.helper && options.helperPath)
|
|
95
|
+
throw new TypeError("Ambiguous Linux subreaper helper identity");
|
|
96
|
+
if (
|
|
97
|
+
Object.keys(options).some(
|
|
98
|
+
(key) =>
|
|
99
|
+
!["cwd", "env", "graceMs", "helperPath", "helper"].includes(key),
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
throw new TypeError("Unsupported Linux subreaper option");
|
|
103
|
+
launch = encodeLinuxSubreaperLaunch(command, args, options);
|
|
104
|
+
child = native.spawn(helper ? "/proc/self/fd/4" : options.helperPath, [], {
|
|
105
|
+
cwd: options.cwd,
|
|
106
|
+
env: { PATH: "/usr/bin:/bin", LC_ALL: "C" },
|
|
107
|
+
shell: false,
|
|
108
|
+
detached: false,
|
|
109
|
+
stdio: [
|
|
110
|
+
"pipe",
|
|
111
|
+
"pipe",
|
|
112
|
+
"pipe",
|
|
113
|
+
"pipe",
|
|
114
|
+
...(helper ? [helper.descriptor] : []),
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
} finally {
|
|
118
|
+
// Native spawn duplicates stdio synchronously. Never reopen the verified
|
|
119
|
+
// executable path; the child's descriptor now owns that exact image.
|
|
120
|
+
helper?.release();
|
|
121
|
+
}
|
|
122
|
+
const control = child.stdio[3];
|
|
123
|
+
let closed = false;
|
|
124
|
+
let requestedSignal = null;
|
|
125
|
+
let buffer = "";
|
|
126
|
+
let bytes = 0;
|
|
127
|
+
let targetPid = null;
|
|
128
|
+
let target = null;
|
|
129
|
+
let cleanup = null;
|
|
130
|
+
let protocolError = null;
|
|
131
|
+
const terminate = (signal = "SIGTERM") => {
|
|
132
|
+
if (!["SIGTERM", "SIGKILL"].includes(signal))
|
|
133
|
+
throw new TypeError("Invalid Linux subreaper stop signal");
|
|
134
|
+
if (closed || control.destroyed || !control.writable) return false;
|
|
135
|
+
if (requestedSignal === "SIGKILL" || requestedSignal === signal)
|
|
136
|
+
return true;
|
|
137
|
+
requestedSignal = signal;
|
|
138
|
+
control.write(signal === "SIGKILL" ? "K" : "T");
|
|
139
|
+
return true;
|
|
140
|
+
};
|
|
141
|
+
control.on("error", () => {
|
|
142
|
+
protocolError ??= "control-channel-failed";
|
|
143
|
+
control.destroy();
|
|
144
|
+
});
|
|
145
|
+
control.on("data", (chunk) => {
|
|
146
|
+
bytes += chunk.length;
|
|
147
|
+
if (bytes > 16 * 1024) {
|
|
148
|
+
protocolError = "control-reply-limit";
|
|
149
|
+
control.destroy();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
buffer += chunk.toString("ascii");
|
|
153
|
+
let newline;
|
|
154
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
155
|
+
const line = buffer.slice(0, newline);
|
|
156
|
+
buffer = buffer.slice(newline + 1);
|
|
157
|
+
try {
|
|
158
|
+
const message = JSON.parse(line);
|
|
159
|
+
if (
|
|
160
|
+
message.type === "started" &&
|
|
161
|
+
targetPid === null &&
|
|
162
|
+
Number.isSafeInteger(message.pid) &&
|
|
163
|
+
message.pid > 0
|
|
164
|
+
) {
|
|
165
|
+
targetPid = message.pid;
|
|
166
|
+
} else if (
|
|
167
|
+
message.type === "target-exit" &&
|
|
168
|
+
target === null &&
|
|
169
|
+
targetPid !== null &&
|
|
170
|
+
Number.isInteger(message.code) &&
|
|
171
|
+
message.code >= -1 &&
|
|
172
|
+
message.code <= 255 &&
|
|
173
|
+
Number.isInteger(message.signal) &&
|
|
174
|
+
message.signal >= 0 &&
|
|
175
|
+
message.signal <= 64 &&
|
|
176
|
+
Number.isInteger(message.spawnErrno) &&
|
|
177
|
+
message.spawnErrno >= -1 &&
|
|
178
|
+
message.spawnErrno <= 4095
|
|
179
|
+
) {
|
|
180
|
+
target = {
|
|
181
|
+
code: message.code,
|
|
182
|
+
signal: message.signal,
|
|
183
|
+
spawnErrno: message.spawnErrno,
|
|
184
|
+
};
|
|
185
|
+
} else if (
|
|
186
|
+
message.type === "cleanup" &&
|
|
187
|
+
cleanup === null &&
|
|
188
|
+
target !== null &&
|
|
189
|
+
typeof message.confirmed === "boolean" &&
|
|
190
|
+
message.rootReaped === true &&
|
|
191
|
+
Number.isSafeInteger(message.reaped) &&
|
|
192
|
+
message.reaped >= 1
|
|
193
|
+
) {
|
|
194
|
+
cleanup = message;
|
|
195
|
+
} else if (
|
|
196
|
+
message.type === "error" &&
|
|
197
|
+
["subreaper-unavailable", "cleanup-unconfirmed"].includes(
|
|
198
|
+
message.code,
|
|
199
|
+
)
|
|
200
|
+
) {
|
|
201
|
+
protocolError ??= message.code;
|
|
202
|
+
} else throw new Error("invalid reply");
|
|
203
|
+
} catch {
|
|
204
|
+
protocolError = "invalid-control-reply";
|
|
205
|
+
control.destroy();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
const completion = new Promise((resolve) => {
|
|
211
|
+
child.on("error", () => {
|
|
212
|
+
protocolError ??= "supervisor-spawn-failed";
|
|
213
|
+
});
|
|
214
|
+
child.once("close", (code, signal) => {
|
|
215
|
+
closed = true;
|
|
216
|
+
control.destroy();
|
|
217
|
+
if (buffer) protocolError ??= "truncated-control-reply";
|
|
218
|
+
if (!cleanup) protocolError ??= "missing-cleanup-receipt";
|
|
219
|
+
else if (!cleanup.confirmed)
|
|
220
|
+
protocolError ??= "unconfirmed-cleanup-receipt";
|
|
221
|
+
const confirmed =
|
|
222
|
+
cleanup?.confirmed === true &&
|
|
223
|
+
code === 0 &&
|
|
224
|
+
signal === null &&
|
|
225
|
+
!protocolError;
|
|
226
|
+
resolve({
|
|
227
|
+
targetPid,
|
|
228
|
+
target,
|
|
229
|
+
supervisor: { code, signal, closed: true },
|
|
230
|
+
cleanup: {
|
|
231
|
+
mode: "linux-subreaper",
|
|
232
|
+
confirmed,
|
|
233
|
+
descendantsReaped: confirmed,
|
|
234
|
+
reaped: cleanup?.reaped ?? null,
|
|
235
|
+
protocolError,
|
|
236
|
+
// The zero-child fence is a lifecycle fact, not hostile-code isolation.
|
|
237
|
+
processTreeContained: false,
|
|
238
|
+
},
|
|
239
|
+
...(helper
|
|
240
|
+
? {
|
|
241
|
+
helper: {
|
|
242
|
+
sourceDigest: helper.sourceDigest,
|
|
243
|
+
imageDigest: helper.imageDigest,
|
|
244
|
+
binding: "unlinked-inherited-fd",
|
|
245
|
+
distribution: helper.distribution,
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
: {}),
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
control.write(launch);
|
|
253
|
+
return Object.freeze({ child, terminate, completion });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Broker-facing process handle: close is fenced by the kernel tree receipt. */
|
|
257
|
+
export function spawnLinuxSubreaperChild(command, args, options, native) {
|
|
258
|
+
const owner = spawnLinuxSubreaper(command, args, options, native);
|
|
259
|
+
const raw = owner.child;
|
|
260
|
+
const child = new EventEmitter();
|
|
261
|
+
Object.assign(child, {
|
|
262
|
+
pid: raw.pid,
|
|
263
|
+
sandboxWrapperPid: raw.pid,
|
|
264
|
+
stdin: raw.stdin,
|
|
265
|
+
stdout: raw.stdout,
|
|
266
|
+
stderr: raw.stderr,
|
|
267
|
+
stdio: raw.stdio,
|
|
268
|
+
exitCode: null,
|
|
269
|
+
signalCode: null,
|
|
270
|
+
killed: false,
|
|
271
|
+
ownedProcessTreeClosed: owner.completion,
|
|
272
|
+
kill(signal = "SIGTERM") {
|
|
273
|
+
const requested = owner.terminate(signal);
|
|
274
|
+
if (requested) child.killed = true;
|
|
275
|
+
return requested;
|
|
276
|
+
},
|
|
277
|
+
ref() {
|
|
278
|
+
raw.ref?.();
|
|
279
|
+
return child;
|
|
280
|
+
},
|
|
281
|
+
unref() {
|
|
282
|
+
raw.unref?.();
|
|
283
|
+
return child;
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
raw.once("spawn", () => child.emit("spawn"));
|
|
287
|
+
owner.completion.then((receipt) => {
|
|
288
|
+
child.ownedProcessTreeEvidence = receipt;
|
|
289
|
+
child.sandboxTargetPid = receipt.targetPid;
|
|
290
|
+
if (!receipt.cleanup.confirmed) {
|
|
291
|
+
const error = new Error(
|
|
292
|
+
"Linux external-agent process-tree cleanup is unconfirmed",
|
|
293
|
+
);
|
|
294
|
+
error.code = raw.pid
|
|
295
|
+
? "EXTERNAL_AGENT_CLEANUP_UNCONFIRMED"
|
|
296
|
+
: "EXTERNAL_AGENT_SPAWN_FAILED";
|
|
297
|
+
// A failed native spawn never created an owner or target. All other
|
|
298
|
+
// losses retain the close fence: helper exit alone cannot release an
|
|
299
|
+
// agent while an unobserved adopted descendant may still be executing.
|
|
300
|
+
child.emit("error", error);
|
|
301
|
+
if (!raw.pid) {
|
|
302
|
+
child.exitCode = -1;
|
|
303
|
+
child.emit("exit", -1, null);
|
|
304
|
+
child.emit("close", -1, null);
|
|
305
|
+
} else {
|
|
306
|
+
child.emit("cleanup:unconfirmed", receipt);
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (receipt.target.spawnErrno) {
|
|
311
|
+
const error = new Error("External agent executable could not be started");
|
|
312
|
+
error.code = "EXTERNAL_AGENT_SPAWN_FAILED";
|
|
313
|
+
child.emit("error", error);
|
|
314
|
+
}
|
|
315
|
+
child.exitCode = receipt.target.code < 0 ? null : receipt.target.code;
|
|
316
|
+
child.signalCode =
|
|
317
|
+
receipt.target.signal === 0
|
|
318
|
+
? null
|
|
319
|
+
: (Object.keys(constants.signals).find(
|
|
320
|
+
(signal) => constants.signals[signal] === receipt.target.signal,
|
|
321
|
+
) ?? "SIGUNKNOWN");
|
|
322
|
+
child.emit("exit", child.exitCode, child.signalCode);
|
|
323
|
+
child.emit("close", child.exitCode, child.signalCode);
|
|
324
|
+
});
|
|
325
|
+
return child;
|
|
326
|
+
}
|