chainlesschain 0.166.85 → 0.166.87
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 +8 -0
- package/package.json +3 -2
- package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
- package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-Cgo7OFpi.js → AIOps-DdQjhXg9.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-lUMnE2Ln.js → ActionButton-CuwA3hPA.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CsvKzTot.js → Analytics-DTVXJSAC.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-C2z1h1Pv.js → AppLayout-j06JBfEQ.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-C5Q0ZhNb.js → Artifacts-BFAK8LEW.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DXnfPKsI.js → Audit-BiGxRpOf.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DjbGcLm3.js → BackgroundAgents-B6uHaPkZ.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-9bVrKvXE.js → Backup-BNAesB8q.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-bx5ri7BD.js → BaseInput-BOuetfCd.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CCFmeUMY.js → Chat-cVzo3m_D.js} +4 -4
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DZ2Ys-CI.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-ei7MQXe9.js → Checkbox-BaCTS-H8.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-CXgBJ9iu.js → Codegen-CBE8NiYh.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DMkMsizS.js → Col-C9wLiUTN.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CvpB_UFR.js → Community-jdClBSIp.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-gay5hbFX.js → Compact-BIjauIlM.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-dG8JQyRL.js → Compliance-CZ-AoaNj.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BA3xfaTW.js → Cowork-BbTKtDS4.js} +4 -4
- package/src/assets/web-panel/assets/{Cron-D1_HsxyY.js → Cron-ImKdtwNi.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DZqoqoy5.js → Crosschain-B54GSHf5.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Cwnr8hCO.js → DID-BQt5WUzs.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CxevypiI.js → Dashboard--gNOffif.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-Bs7-yWgr.js → Dropdown-342A5YJw.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CcEs2Puo.js → EmailListRenderer-CtIZ3ZHd.js} +1 -1
- package/src/assets/web-panel/assets/{EvolutionSettings-C9ipBzAQ.js → EvolutionSettings-CyZPoTio.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-Dt6--mGn.js → FamilyGuardDashboard-B1MCkFEu.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CD0JN5FF.js → Federation-DZWoERUy.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-POypz3Xv.js → FormItemContext-Onmb0mp3.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-Dv9f-0F5.js → GenericCardRenderer-u1qhNNc7.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BbTh5Atv.js → Git-BEowR1C5.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-C71dgoir.js → Governance-BiYd3AAg.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-DwSHxbJB.js → Inference-CsC4Vjh5.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Cx1oQYQZ.js → KnowledgeGraph-BXA7ikPM.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BAJWe00o.js → Logs-C9WoplQm.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-CD8a7lHT.js → MarkdownRenderer-DecEsXsy.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-fwuY7P7Y.js → Marketplace-CCoJBfRL.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-Di4693uc.js → McpTools-CzaOWBoA.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-Cz3L-GUB.js → Memory-BPwh7u1-.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BuXKIkSb.js → MobileBridge-Bk47v6z2.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-D-CY--Sb.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-LnACMFd0.js → Mtc-ExVyAGlW.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-BZy7yE0t.js → MtcAudit-Rrw10csE.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-BFG5iVrE.js → Multisig-8TxxwxZZ.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DgXi04vJ.js → NLProgramming-CNckjtnY.js} +1 -1
- package/src/assets/web-panel/assets/Notes-C4j6cZTu.js +7 -0
- package/src/assets/web-panel/assets/{NotificationSettings-AixrsHw5.js → NotificationSettings-CchU8rWd.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-ZJBoyJ16.js +1 -0
- package/src/assets/web-panel/assets/{Organization-UNCBC3ux.js → Organization-F0Je7EdZ.js} +2 -2
- package/src/assets/web-panel/assets/{Overflow-Drh8oAFT.js → Overflow-B2k-JA3u.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-bc2q0xEB.js → P2P-yui9Xxe6.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BGwKxcDM.js → PdhVaultBrowser-CegM7hT6.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DOnUxUzu.js → Permissions-CSFL0uvh.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-B3qPZkrr.js → PersonalDataHub-BrMcd-vr.js} +6 -6
- package/src/assets/web-panel/assets/{Pipeline-B6hfa4-i.js → Pipeline-fzrliaVP.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-Bun3c35F.js → Privacy-BrkUjJ_g.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CCObdzk9.js → ProjectInit-CF_QCsW9.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-C6lKrSXY.js → ProjectSettings-Duwrd8aC.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-DYcta7MZ.js → Projects-B0dLVXzU.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-BtykxKCe.js → Providers-Db8iS1xQ.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-D9CDyroY.js → QrScannerModal-CHbNzEQs.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CJauCxXx.js → QuickAsk-CIESMfeR.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BbiYZofR.js → Recommend-BTMbCMtR.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-BymCrgFW.js → RemoteSession-DavyK6J-.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-BWrGpMpF.js → Reputation-BFKtMelA.js} +1 -1
- package/src/assets/web-panel/assets/{Row-9pOYIhiW.js → Row-BI9AZUvw.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-D3bQJ4qB.js → RssFeed-1nwXt8NY.js} +2 -2
- package/src/assets/web-panel/assets/{Search-BpKOOFE0.js → Search-Ck8PDYbk.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DC6C0NSz.js → Security-BgDqpCbq.js} +4 -4
- package/src/assets/web-panel/assets/{Services-Lkf1046J.js → Services-DF8anXm4.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-eCQ_SsDb.js → Skeleton-CoHyY4ZX.js} +1 -1
- package/src/assets/web-panel/assets/Skills-dOaD4Ztl.js +1 -0
- package/src/assets/web-panel/assets/{Sla-BQbKy8XJ.js → Sla-XuOawvQY.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DHsRx-XA.js → SpeechSettings-DFJ7nAiu.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CC281X_J.js → SyncSettings-B2xYj-Pi.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-D_SzNTLi.js +1 -0
- package/src/assets/web-panel/assets/{Templates-Cr2vC0tg.js → Templates-BXFmKh7W.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DeYO5pIG.js → Tenant-CRg2AXbd.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DBNgbfPr.js → Terminal-DQgxnuep.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-MoxgaF3x.js → TimelineRenderer-BrPQhQVc.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CT2QfAN7.js → Tokens-RByy4USy.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DPUnomxs.js → Trigger-pE3y8u73.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DnufuaTx.js → Trust-DhID9C33.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CJ6iOdPs.js → UkeySign-CPxEPNBH.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-xBWkH8bL.js → VideoEditing-ZD8DUxeQ.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-WYV-4hMR.js → Wallet-Br9JREEd.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DJ5Mhd9L.js → WebAuthn-Cn-YacJg.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-B__UdOjN.js → WorkflowEditor-BTu0oqMp.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CLlmBVhp.js → chat-CEjws_QF.js} +1 -1
- package/src/assets/web-panel/assets/{colors-B8zoPQYm.js → colors-Bv2RNebW.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CvJ-dUsH.js → compact-item-gSc3EofP.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-LAtMm8SC.js → createContext-kyFDaqWC.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Dcc8pILJ.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-CLYvg42H.js → hasIn-BuMWcjhx.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cv7vwX3z.js → index--nrQmk8X.js} +1 -1
- package/src/assets/web-panel/assets/index-05NB95_V.js +1 -0
- package/src/assets/web-panel/assets/{index-BUVrYDNV.js → index-42PTBO7a.js} +1 -1
- package/src/assets/web-panel/assets/{index-B6zlMzZz.js → index-BCS5j4SQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-oX216HRH.js → index-BSSIJ-rv.js} +1 -1
- package/src/assets/web-panel/assets/{index-B02qgCao.js → index-BUOCcXou.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_RPWP_5.js → index-Bg5jis0q.js} +1 -1
- package/src/assets/web-panel/assets/{index-DX99e8Xy.js → index-C-LPpwe9.js} +1 -1
- package/src/assets/web-panel/assets/{index-DDm9m4vC.js → index-C56Drzol.js} +1 -1
- package/src/assets/web-panel/assets/{index-DHq2Of6z.js → index-C7Jo20JB.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_dq-6_3.js → index-C9U9SjcT.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0Z13xcZ.js → index-CF3TjDNz.js} +1 -1
- package/src/assets/web-panel/assets/{index-B1hHp9_M.js → index-CMS3Mk93.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfOOACfd.js → index-CNt2X-aK.js} +1 -1
- package/src/assets/web-panel/assets/index-Cb6q8wlI.js +1 -0
- package/src/assets/web-panel/assets/{index-DYW4Z3y-.js → index-Cj4RA7oA.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2KFnUsv.js → index-CkfvP2dN.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkcQJvsz.js → index-Cm8K8Evn.js} +1 -1
- package/src/assets/web-panel/assets/{index-CVDcRQuX.js → index-CvSuM_CB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCzyJ1xS.js → index-Czo_hw97.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cpda0SAz.js → index-D3m3EXbL.js} +1 -1
- package/src/assets/web-panel/assets/{index-iJ6s24bA.js → index-DR561Sqc.js} +1 -1
- package/src/assets/web-panel/assets/{index-Di9vKira.js → index-DRa4R7vv.js} +1 -1
- package/src/assets/web-panel/assets/{index-09YzRb6Z.js → index-DYKSuD02.js} +3 -3
- package/src/assets/web-panel/assets/{index-C5v7cEtm.js → index-DYcPWVN9.js} +1 -1
- package/src/assets/web-panel/assets/{index-BGrQ2uEl.js → index-DbVUA5l3.js} +1 -1
- package/src/assets/web-panel/assets/{index-4rr5jFOL.js → index-DbkyTMj-.js} +1 -1
- package/src/assets/web-panel/assets/{index-GR3omApz.js → index-Dej4AGOz.js} +1 -1
- package/src/assets/web-panel/assets/{index-f1ptYBMr.js → index-EpxWlmip.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3uD5H7u.js → index-GpvWtvw3.js} +1 -1
- package/src/assets/web-panel/assets/{index-C6PETyel.js → index-NQ0c4hJ1.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnLXLaEF.js → index-OjZUHz5z.js} +1 -1
- package/src/assets/web-panel/assets/{index-DpeV-6y4.js → index-U-ytt84A.js} +1 -1
- package/src/assets/web-panel/assets/{index-DNQUdPam.js → index-_0bfDTDu.js} +1 -1
- package/src/assets/web-panel/assets/{index-BM2HqHdT.js → index-jmMAqBAm.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bu7-I2L4.js → index-nLemnBZ2.js} +1 -1
- package/src/assets/web-panel/assets/{index-CUcE6rIa.js → index-q65c1kp8.js} +1 -1
- package/src/assets/web-panel/assets/{index-DX0aHEHL.js → index-sJ1F5Txa.js} +1 -1
- package/src/assets/web-panel/assets/{index-CxJ4NPVq.js → index-xfS_WO9b.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-QD-koeaw.js → initDefaultProps-B-wqiSu7.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CTItLW4-.js → motion-BK4_B4MD.js} +1 -1
- package/src/assets/web-panel/assets/{move-CGsdTOdN.js → move-BU2TgDDO.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-C1kW_CNU.js → mtc-parser-DA3E4_EX.js} +1 -1
- package/src/assets/web-panel/assets/{omit-B4PilaYy.js → omit-CaHz1e_q.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DBGUE12e.js → pickAttrs-NXZ19amf.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DZxCLjuo.js → placementArrow-BCXI9AHO.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-CZjQiocN.js → responsiveObserve-DdlZ9tLk.js} +1 -1
- package/src/assets/web-panel/assets/{slide-Btz5hh2_.js → slide-DCZm_Vc4.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CfuxuLIt.js → statusUtils-CuEULKQr.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DdO0Obo_.js → styleChecker-Bo_UXG8x.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-CxftJ-Zs.js → useFlexGapSupport-B9PaTcFH.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-pVJChh6K.js → useFs-DwSTGdVR.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-wv7Q0uNE.js → usePersonalDataHub-FswSxnDC.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DVOVfAqC.js → vnode-EUztvYTj.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-B5tebZuT.js → zoom-D41MAgV-.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +1 -1
- package/src/commands/eval.js +118 -18
- package/src/commands/memory.js +34 -15
- package/src/commands/sandbox.js +9 -0
- package/src/data/changelog.json +26 -0
- package/src/lib/causal-observability.js +6 -45
- package/src/lib/context-memory-kernel/durable-memory-port.js +54 -12
- package/src/lib/context-memory-kernel/index.js +1 -0
- package/src/lib/context-memory-kernel/memory-query-index.js +278 -0
- package/src/lib/context-memory-kernel/memory-service.js +60 -18
- package/src/lib/context-memory-kernel/runtime.js +5 -3
- package/src/lib/context-memory-kernel/segmented-memory-port.js +955 -0
- package/src/lib/eval/outcomes.js +20 -10
- package/src/lib/eval/runner.js +49 -5
- package/src/lib/eval/tasks.js +8 -3
- package/src/lib/eval/verify01-contracts.js +349 -0
- package/src/lib/eval/verify01-execution.js +605 -0
- package/src/lib/execution-support.js +58 -0
- package/src/lib/llm-pricing.js +98 -11
- package/src/lib/model-catalog-review.js +116 -0
- package/src/lib/model-context-catalog.js +23 -1
- package/src/lib/openai-responses.js +39 -0
- package/src/lib/packer/pack-update-applier.js +13 -8
- package/src/lib/permission-authority.js +146 -10
- package/src/lib/process-execution-broker/index.js +8 -0
- package/src/lib/process-execution-broker/linux-subreaper-process.js +24 -2
- package/src/lib/process-execution-broker/process-ownership-journal.js +168 -7
- package/src/lib/process-execution-broker/process-ownership-quarantine.js +56 -1
- package/src/lib/process-execution-broker/process-recovery-cgroup.js +272 -0
- package/src/lib/sandbox-egress-authority-monitor.js +62 -5
- package/src/lib/scoped-permission-store.js +104 -1
- package/src/lib/settings-authority-domain.cjs +37 -2
- package/src/lib/settings-loader.cjs +67 -34
- package/src/lib/settings-permission-authority.cjs +373 -0
- package/src/runtime/agent-core.js +88 -407
- package/src/runtime/headless-runner.js +8 -3
- package/src/runtime/headless-stream.js +10 -3
- package/src/runtime/permission-authority-host.js +151 -0
- package/src/runtime/provider-stream-state.js +379 -0
- package/src/assets/web-panel/assets/ChatBubbleRenderer-ClqsIfTT.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-Bk-lK4Um.js +0 -1
- package/src/assets/web-panel/assets/Notes-BAj0Kgg4.js +0 -7
- package/src/assets/web-panel/assets/OrderTableRenderer-Dwqk3FlG.js +0 -1
- package/src/assets/web-panel/assets/Skills-BvDSQ3-d.js +0 -1
- package/src/assets/web-panel/assets/Tasks-CHHSWHom.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DgZ4fdGI.js +0 -1
- package/src/assets/web-panel/assets/index-jLEdASOV.js +0 -1
- package/src/assets/web-panel/assets/index-jesGo4rl.js +0 -1
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
getProcessOwnershipStatus,
|
|
54
54
|
observeProcessOwnership,
|
|
55
55
|
prepareProcessOwnership,
|
|
56
|
+
recoverProcessOwnership,
|
|
56
57
|
} from "./process-ownership-quarantine.js";
|
|
57
58
|
import {
|
|
58
59
|
MACOS_MCP_LAUNCHER_INPUTS,
|
|
@@ -4525,6 +4526,10 @@ class ProcessExecutionBroker extends EventEmitter {
|
|
|
4525
4526
|
return getProcessOwnershipStatus();
|
|
4526
4527
|
}
|
|
4527
4528
|
|
|
4529
|
+
recoverProcessOwnership(executionId, options) {
|
|
4530
|
+
return recoverProcessOwnership(executionId, options);
|
|
4531
|
+
}
|
|
4532
|
+
|
|
4528
4533
|
listWorkspaceTransactions(options = {}) {
|
|
4529
4534
|
return this._workspaceTransactionManager(
|
|
4530
4535
|
options.stateDir,
|
|
@@ -4873,6 +4878,9 @@ class ProcessExecutionBroker extends EventEmitter {
|
|
|
4873
4878
|
cwd: path.resolve(optsForSpawn.cwd || cwd),
|
|
4874
4879
|
env: optsForSpawn.env || process.env,
|
|
4875
4880
|
graceMs: request.graceMs,
|
|
4881
|
+
...(ownershipLease?.beforeLaunch
|
|
4882
|
+
? { beforeLaunch: ownershipLease.beforeLaunch }
|
|
4883
|
+
: {}),
|
|
4876
4884
|
helper,
|
|
4877
4885
|
},
|
|
4878
4886
|
{
|
|
@@ -96,10 +96,22 @@ export function spawnLinuxSubreaper(command, args, options, native) {
|
|
|
96
96
|
if (
|
|
97
97
|
Object.keys(options).some(
|
|
98
98
|
(key) =>
|
|
99
|
-
![
|
|
99
|
+
![
|
|
100
|
+
"cwd",
|
|
101
|
+
"env",
|
|
102
|
+
"graceMs",
|
|
103
|
+
"helperPath",
|
|
104
|
+
"helper",
|
|
105
|
+
"beforeLaunch",
|
|
106
|
+
].includes(key),
|
|
100
107
|
)
|
|
101
108
|
)
|
|
102
109
|
throw new TypeError("Unsupported Linux subreaper option");
|
|
110
|
+
if (
|
|
111
|
+
options.beforeLaunch !== undefined &&
|
|
112
|
+
typeof options.beforeLaunch !== "function"
|
|
113
|
+
)
|
|
114
|
+
throw new TypeError("Invalid trusted before-launch hook");
|
|
103
115
|
launch = encodeLinuxSubreaperLaunch(command, args, options);
|
|
104
116
|
child = native.spawn(helper ? "/proc/self/fd/4" : options.helperPath, [], {
|
|
105
117
|
cwd: options.cwd,
|
|
@@ -249,7 +261,17 @@ export function spawnLinuxSubreaper(command, args, options, native) {
|
|
|
249
261
|
});
|
|
250
262
|
});
|
|
251
263
|
});
|
|
252
|
-
|
|
264
|
+
try {
|
|
265
|
+
const result = options.beforeLaunch?.(child);
|
|
266
|
+
if (result && typeof result.then === "function")
|
|
267
|
+
throw new TypeError("Before-launch hook must be synchronous");
|
|
268
|
+
control.write(launch);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
// No launch frame was sent. Closing the private control channel releases
|
|
271
|
+
// the blocked supervisor without executing the target.
|
|
272
|
+
control.destroy();
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
253
275
|
return Object.freeze({ child, terminate, completion });
|
|
254
276
|
}
|
|
255
277
|
|
|
@@ -4,11 +4,17 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { withFileLock } from "../with-file-lock.js";
|
|
5
5
|
import { writeSecurityStore } from "../durable-security-store.js";
|
|
6
6
|
import { ensurePrivateDirectory } from "../secure-fs.js";
|
|
7
|
+
import { performance } from "node:perf_hooks";
|
|
8
|
+
import {
|
|
9
|
+
openRecoveryCgroup,
|
|
10
|
+
validateRecoveryCgroup,
|
|
11
|
+
} from "./process-recovery-cgroup.js";
|
|
7
12
|
|
|
8
13
|
export const PROCESS_OWNERSHIP_PENDING = "BROKER_PROCESS_OWNERSHIP_PENDING";
|
|
9
14
|
export const PROCESS_OWNERSHIP_JOURNAL_UNAVAILABLE =
|
|
10
15
|
"BROKER_PROCESS_OWNERSHIP_JOURNAL_UNAVAILABLE";
|
|
11
16
|
const SCHEMA = "chainlesschain.process-ownership-journal/v1";
|
|
17
|
+
const RECOVERY_SCHEMA = "chainlesschain.process-ownership-journal/v2";
|
|
12
18
|
const MAX_BYTES = 1024 * 1024;
|
|
13
19
|
const MAX_PENDING = 1024;
|
|
14
20
|
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
@@ -27,8 +33,12 @@ function unavailable(cause) {
|
|
|
27
33
|
function validate(state) {
|
|
28
34
|
if (
|
|
29
35
|
!state ||
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
!(
|
|
37
|
+
(state.schema === SCHEMA &&
|
|
38
|
+
Object.keys(state).sort().join() === "pending,schema") ||
|
|
39
|
+
(state.schema === RECOVERY_SCHEMA &&
|
|
40
|
+
Object.keys(state).sort().join() === "pending,recoveries,schema")
|
|
41
|
+
) ||
|
|
32
42
|
!Array.isArray(state.pending) ||
|
|
33
43
|
state.pending.length > MAX_PENDING
|
|
34
44
|
)
|
|
@@ -37,7 +47,12 @@ function validate(state) {
|
|
|
37
47
|
for (const entry of state.pending) {
|
|
38
48
|
if (
|
|
39
49
|
!entry ||
|
|
40
|
-
|
|
50
|
+
![
|
|
51
|
+
"executionId,ownerPid,token",
|
|
52
|
+
...(state.schema === RECOVERY_SCHEMA
|
|
53
|
+
? ["executionId,ownerPid,recovery,token"]
|
|
54
|
+
: []),
|
|
55
|
+
].includes(Object.keys(entry).sort().join()) ||
|
|
41
56
|
!UUID.test(entry.executionId) ||
|
|
42
57
|
!UUID.test(entry.token) ||
|
|
43
58
|
!Number.isSafeInteger(entry.ownerPid) ||
|
|
@@ -45,8 +60,31 @@ function validate(state) {
|
|
|
45
60
|
ids.has(entry.executionId)
|
|
46
61
|
)
|
|
47
62
|
throw new Error("Invalid ownership journal entry");
|
|
63
|
+
if (Object.hasOwn(entry, "recovery"))
|
|
64
|
+
validateRecoveryCgroup(entry.recovery, entry.executionId);
|
|
48
65
|
ids.add(entry.executionId);
|
|
49
66
|
}
|
|
67
|
+
if (state.schema === RECOVERY_SCHEMA) {
|
|
68
|
+
if (
|
|
69
|
+
!Array.isArray(state.recoveries) ||
|
|
70
|
+
state.recoveries.length > MAX_PENDING
|
|
71
|
+
)
|
|
72
|
+
throw new Error("Invalid recovery audit inventory");
|
|
73
|
+
for (const receipt of state.recoveries) {
|
|
74
|
+
if (
|
|
75
|
+
!receipt ||
|
|
76
|
+
Object.keys(receipt).sort().join() !==
|
|
77
|
+
"completedAt,executionId,group,killIssued,populated,token" ||
|
|
78
|
+
!UUID.test(receipt.executionId) ||
|
|
79
|
+
!UUID.test(receipt.token) ||
|
|
80
|
+
receipt.killIssued !== true ||
|
|
81
|
+
receipt.populated !== false ||
|
|
82
|
+
!Number.isFinite(Date.parse(receipt.completedAt))
|
|
83
|
+
)
|
|
84
|
+
throw new Error("Invalid recovery audit receipt");
|
|
85
|
+
validateRecoveryCgroup(receipt.group, receipt.executionId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
50
88
|
return state;
|
|
51
89
|
}
|
|
52
90
|
|
|
@@ -211,6 +249,13 @@ export class ProcessOwnershipJournal {
|
|
|
211
249
|
}
|
|
212
250
|
}
|
|
213
251
|
|
|
252
|
+
#write(file, state, label = "Process ownership") {
|
|
253
|
+
validate(state);
|
|
254
|
+
if (Buffer.byteLength(`${JSON.stringify(state, null, 2)}\n`) > MAX_BYTES)
|
|
255
|
+
throw new Error("Ownership journal byte capacity is exhausted");
|
|
256
|
+
writeSecurityStore(file, label, state);
|
|
257
|
+
}
|
|
258
|
+
|
|
214
259
|
assertAvailable() {
|
|
215
260
|
if (this.#fault) throw this.#fault;
|
|
216
261
|
this.#withDirectory(false, (file) => this.#assertState(this.#read(file)));
|
|
@@ -222,6 +267,9 @@ export class ProcessOwnershipJournal {
|
|
|
222
267
|
const state = this.#read(file);
|
|
223
268
|
return {
|
|
224
269
|
pendingExecutionIds: state.pending.map((entry) => entry.executionId),
|
|
270
|
+
recoverableExecutionIds: state.pending
|
|
271
|
+
.filter((entry) => entry.recovery)
|
|
272
|
+
.map((entry) => entry.executionId),
|
|
225
273
|
blocked: state.pending.some(
|
|
226
274
|
(entry) => this.#local.get(entry.executionId)?.token !== entry.token,
|
|
227
275
|
),
|
|
@@ -229,24 +277,34 @@ export class ProcessOwnershipJournal {
|
|
|
229
277
|
});
|
|
230
278
|
}
|
|
231
279
|
|
|
232
|
-
prepare(executionId) {
|
|
280
|
+
prepare(executionId, recovery = null) {
|
|
233
281
|
if (!UUID.test(executionId))
|
|
234
282
|
throw new TypeError("Invalid ownership execution id");
|
|
235
283
|
if (this.#fault) throw this.#fault;
|
|
236
|
-
|
|
284
|
+
if (recovery) validateRecoveryCgroup(recovery, executionId);
|
|
285
|
+
const entry = {
|
|
286
|
+
executionId,
|
|
287
|
+
ownerPid: process.pid,
|
|
288
|
+
token: randomUUID(),
|
|
289
|
+
...(recovery ? { recovery: { ...recovery } } : {}),
|
|
290
|
+
};
|
|
237
291
|
this.#withDirectory(true, (file) =>
|
|
238
292
|
withFileLock(
|
|
239
293
|
file,
|
|
240
294
|
() => {
|
|
241
295
|
const state = this.#read(file);
|
|
242
296
|
this.#assertState(state);
|
|
297
|
+
if (recovery && state.schema === SCHEMA) {
|
|
298
|
+
state.schema = RECOVERY_SCHEMA;
|
|
299
|
+
state.recoveries = [];
|
|
300
|
+
}
|
|
243
301
|
if (
|
|
244
302
|
state.pending.length >= MAX_PENDING ||
|
|
245
303
|
state.pending.some((item) => item.executionId === executionId)
|
|
246
304
|
)
|
|
247
305
|
throw new Error("Ownership journal admission limit");
|
|
248
306
|
state.pending.push(entry);
|
|
249
|
-
|
|
307
|
+
this.#write(file, state);
|
|
250
308
|
this.#initialized = true;
|
|
251
309
|
this.#local.set(executionId, entry);
|
|
252
310
|
},
|
|
@@ -284,7 +342,7 @@ export class ProcessOwnershipJournal {
|
|
|
284
342
|
state.pending = state.pending.filter(
|
|
285
343
|
(item) => item.executionId !== executionId,
|
|
286
344
|
);
|
|
287
|
-
|
|
345
|
+
this.#write(file, state);
|
|
288
346
|
this.#local.delete(executionId);
|
|
289
347
|
settled = true;
|
|
290
348
|
},
|
|
@@ -294,4 +352,107 @@ export class ProcessOwnershipJournal {
|
|
|
294
352
|
},
|
|
295
353
|
});
|
|
296
354
|
}
|
|
355
|
+
|
|
356
|
+
/** Explicitly cancel an orphaned, identity-bound cgroup. Old PID-only records
|
|
357
|
+
* remain quarantined. Neither process death nor age is a cleanup authority.
|
|
358
|
+
*/
|
|
359
|
+
async recover(executionId, { timeoutMs = 5000, pollMs = 10 } = {}) {
|
|
360
|
+
if (
|
|
361
|
+
!UUID.test(executionId) ||
|
|
362
|
+
!Number.isSafeInteger(timeoutMs) ||
|
|
363
|
+
timeoutMs < 1 ||
|
|
364
|
+
timeoutMs > 30000 ||
|
|
365
|
+
!Number.isSafeInteger(pollMs) ||
|
|
366
|
+
pollMs < 1 ||
|
|
367
|
+
pollMs > 1000
|
|
368
|
+
)
|
|
369
|
+
throw new TypeError("Invalid process recovery request");
|
|
370
|
+
if (this.#fault) throw this.#fault;
|
|
371
|
+
if (this.#local.has(executionId))
|
|
372
|
+
throw unavailable(
|
|
373
|
+
new Error(
|
|
374
|
+
"A locally owned live launch must use its normal cancellation fence",
|
|
375
|
+
),
|
|
376
|
+
);
|
|
377
|
+
const entry = this.#withDirectory(false, (file) => {
|
|
378
|
+
const state = this.#read(file);
|
|
379
|
+
const found = state.pending.find(
|
|
380
|
+
(item) => item.executionId === executionId,
|
|
381
|
+
);
|
|
382
|
+
if (!found?.recovery) {
|
|
383
|
+
const error = new Error(
|
|
384
|
+
"This pending execution has no recoverable kernel group identity",
|
|
385
|
+
);
|
|
386
|
+
error.code = PROCESS_OWNERSHIP_PENDING;
|
|
387
|
+
error.recoveryRequired = true;
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
if (state.recoveries.length >= MAX_PENDING)
|
|
391
|
+
throw new Error("Recovery audit capacity is exhausted");
|
|
392
|
+
return { ...found, recovery: { ...found.recovery } };
|
|
393
|
+
});
|
|
394
|
+
const group = openRecoveryCgroup(entry.recovery, executionId);
|
|
395
|
+
let settled = false;
|
|
396
|
+
try {
|
|
397
|
+
group.kill();
|
|
398
|
+
const deadline = performance.now() + timeoutMs;
|
|
399
|
+
while (group.populated()) {
|
|
400
|
+
if (performance.now() >= deadline) {
|
|
401
|
+
const error = new Error(
|
|
402
|
+
"Recovered process group has not reached its empty kernel fence",
|
|
403
|
+
);
|
|
404
|
+
error.code = PROCESS_OWNERSHIP_PENDING;
|
|
405
|
+
error.recoveryRequired = true;
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
409
|
+
}
|
|
410
|
+
const receipt = {
|
|
411
|
+
executionId,
|
|
412
|
+
token: entry.token,
|
|
413
|
+
group: entry.recovery,
|
|
414
|
+
killIssued: true,
|
|
415
|
+
populated: false,
|
|
416
|
+
completedAt: new Date().toISOString(),
|
|
417
|
+
};
|
|
418
|
+
this.#withDirectory(false, (file) =>
|
|
419
|
+
withFileLock(
|
|
420
|
+
file,
|
|
421
|
+
() => {
|
|
422
|
+
const state = this.#read(file);
|
|
423
|
+
const current = state.pending.find(
|
|
424
|
+
(item) => item.executionId === executionId,
|
|
425
|
+
);
|
|
426
|
+
if (
|
|
427
|
+
!current ||
|
|
428
|
+
current.token !== entry.token ||
|
|
429
|
+
JSON.stringify(current.recovery) !==
|
|
430
|
+
JSON.stringify(entry.recovery)
|
|
431
|
+
)
|
|
432
|
+
throw new Error("Ownership recovery lost its original record");
|
|
433
|
+
if (state.recoveries.length >= MAX_PENDING)
|
|
434
|
+
throw new Error("Recovery audit capacity is exhausted");
|
|
435
|
+
// Check the kernel fence again while committing the durable receipt.
|
|
436
|
+
group.confirmEmpty();
|
|
437
|
+
state.pending = state.pending.filter(
|
|
438
|
+
(item) => item.executionId !== executionId,
|
|
439
|
+
);
|
|
440
|
+
state.recoveries.push(receipt);
|
|
441
|
+
this.#write(file, state, "Process ownership recovery");
|
|
442
|
+
settled = true;
|
|
443
|
+
},
|
|
444
|
+
{ failIfUnavailable: true, timeoutMs: 2000 },
|
|
445
|
+
),
|
|
446
|
+
);
|
|
447
|
+
return { ...receipt, cleanupConfirmed: true, executionResumed: false };
|
|
448
|
+
} finally {
|
|
449
|
+
// Publish settlement before removing the empty group. If journal IO
|
|
450
|
+
// fails, its kernel identity remains available for a safe retry.
|
|
451
|
+
try {
|
|
452
|
+
group.close({ remove: settled });
|
|
453
|
+
} catch {
|
|
454
|
+
group.close(); /* Empty directory leftovers never certify a run. */
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
297
458
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { getMachineSecurityAnchorDir } from "../paths.js";
|
|
3
3
|
import { ProcessOwnershipJournal } from "./process-ownership-journal.js";
|
|
4
|
+
import { prepareRecoveryCgroup } from "./process-recovery-cgroup.js";
|
|
4
5
|
|
|
5
6
|
// Shared by every Broker in this JS runtime. A new Broker, different cwd or
|
|
6
7
|
// cleared audit history cannot turn a lost process owner into cleanup evidence.
|
|
@@ -18,7 +19,60 @@ function durableJournal() {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export function prepareProcessOwnership(executionId) {
|
|
21
|
-
|
|
22
|
+
const authority = durableJournal();
|
|
23
|
+
if (!authority) return null;
|
|
24
|
+
const group = prepareRecoveryCgroup(
|
|
25
|
+
executionId,
|
|
26
|
+
process.env.CHAINLESSCHAIN_PROCESS_RECOVERY_CGROUP_ROOT,
|
|
27
|
+
);
|
|
28
|
+
if (!group) return authority.prepare(executionId);
|
|
29
|
+
let lease;
|
|
30
|
+
try {
|
|
31
|
+
lease = authority.prepare(executionId, group.identity);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
group.close({ remove: true });
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
let settled = false,
|
|
37
|
+
retained = false;
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
beforeLaunch: (proc) => group.attachBeforeLaunch(proc),
|
|
40
|
+
settle() {
|
|
41
|
+
if (settled) return;
|
|
42
|
+
if (retained)
|
|
43
|
+
throw new Error("Retained recovery group requires explicit recovery");
|
|
44
|
+
try {
|
|
45
|
+
group.confirmEmpty();
|
|
46
|
+
lease.settle();
|
|
47
|
+
} catch (error) {
|
|
48
|
+
retained = true;
|
|
49
|
+
lease.retain();
|
|
50
|
+
group.close();
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
settled = true;
|
|
54
|
+
try {
|
|
55
|
+
group.close({ remove: true });
|
|
56
|
+
} catch {
|
|
57
|
+
group.close();
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
retain() {
|
|
61
|
+
if (settled || retained) return;
|
|
62
|
+
retained = true;
|
|
63
|
+
lease.retain();
|
|
64
|
+
group.close();
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function recoverProcessOwnership(executionId, options) {
|
|
70
|
+
const authority = durableJournal();
|
|
71
|
+
if (!authority)
|
|
72
|
+
throw new Error("Kernel process ownership recovery requires Linux");
|
|
73
|
+
const receipt = await authority.recover(executionId, options);
|
|
74
|
+
unresolvedOwners.delete(executionId);
|
|
75
|
+
return receipt;
|
|
22
76
|
}
|
|
23
77
|
|
|
24
78
|
export const PROCESS_OWNERSHIP_UNCONFIRMED =
|
|
@@ -73,6 +127,7 @@ export function getProcessOwnershipStatus() {
|
|
|
73
127
|
!!durable &&
|
|
74
128
|
[...unresolvedOwners.values()].every((owner) => owner.durable),
|
|
75
129
|
restartSafe: false,
|
|
130
|
+
recoverableExecutionIds: durable?.recoverableExecutionIds || [],
|
|
76
131
|
};
|
|
77
132
|
}
|
|
78
133
|
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const PROCESS_RECOVERY_CGROUP_SCHEMA =
|
|
5
|
+
"chainlesschain.process-recovery-cgroup/v1";
|
|
6
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/u;
|
|
7
|
+
const CGROUP2_MAGIC = 0x63677270n;
|
|
8
|
+
const BOOT_ID = "/proc/sys/kernel/random/boot_id";
|
|
9
|
+
|
|
10
|
+
function denied(message, cause) {
|
|
11
|
+
return Object.assign(
|
|
12
|
+
new Error(
|
|
13
|
+
`Process recovery cgroup is unavailable: ${message}`,
|
|
14
|
+
cause ? { cause } : undefined,
|
|
15
|
+
),
|
|
16
|
+
{
|
|
17
|
+
code: "BROKER_PROCESS_RECOVERY_UNAVAILABLE",
|
|
18
|
+
recoveryRequired: true,
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
function requireValue(value, reason) {
|
|
23
|
+
if (!value) throw denied(reason);
|
|
24
|
+
}
|
|
25
|
+
function identity(stat) {
|
|
26
|
+
return { dev: String(stat.dev), ino: String(stat.ino) };
|
|
27
|
+
}
|
|
28
|
+
function same(stat, expected, prefix = "") {
|
|
29
|
+
return (
|
|
30
|
+
String(stat.dev) === expected[`${prefix}dev`] &&
|
|
31
|
+
String(stat.ino) === expected[`${prefix}ino`]
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
function rootPath(root) {
|
|
35
|
+
requireValue(
|
|
36
|
+
typeof root === "string" &&
|
|
37
|
+
path.posix.isAbsolute(root) &&
|
|
38
|
+
root !== "/" &&
|
|
39
|
+
!root.includes("\0") &&
|
|
40
|
+
!root.split("/").includes(".."),
|
|
41
|
+
"invalid delegated root",
|
|
42
|
+
);
|
|
43
|
+
return path.posix.normalize(root).replace(/\/+$/u, "");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function validateRecoveryCgroup(value, executionId) {
|
|
47
|
+
requireValue(
|
|
48
|
+
value &&
|
|
49
|
+
Object.keys(value).sort().join() ===
|
|
50
|
+
"bootId,dev,ino,name,root,rootdev,rootino,schema" &&
|
|
51
|
+
value.schema === PROCESS_RECOVERY_CGROUP_SCHEMA &&
|
|
52
|
+
UUID.test(value.bootId) &&
|
|
53
|
+
UUID.test(executionId) &&
|
|
54
|
+
value.name === `cc-owner-${executionId}` &&
|
|
55
|
+
rootPath(value.root) === value.root &&
|
|
56
|
+
[value.dev, value.ino, value.rootdev, value.rootino].every(
|
|
57
|
+
(part) => typeof part === "string" && /^\d{1,30}$/u.test(part),
|
|
58
|
+
),
|
|
59
|
+
"invalid persisted group identity",
|
|
60
|
+
);
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function openDirectory(io, directory) {
|
|
65
|
+
const before = io.lstatSync(directory, { bigint: true });
|
|
66
|
+
requireValue(
|
|
67
|
+
before.isDirectory() && !before.isSymbolicLink(),
|
|
68
|
+
"cgroup path is not a directory",
|
|
69
|
+
);
|
|
70
|
+
requireValue(
|
|
71
|
+
io.realpathSync(directory) === directory,
|
|
72
|
+
"cgroup path traverses a link",
|
|
73
|
+
);
|
|
74
|
+
const fd = io.openSync(
|
|
75
|
+
directory,
|
|
76
|
+
io.constants.O_RDONLY | io.constants.O_DIRECTORY | io.constants.O_NOFOLLOW,
|
|
77
|
+
);
|
|
78
|
+
try {
|
|
79
|
+
const opened = io.fstatSync(fd, { bigint: true });
|
|
80
|
+
requireValue(
|
|
81
|
+
same(opened, identity(before)),
|
|
82
|
+
"cgroup directory changed while opening",
|
|
83
|
+
);
|
|
84
|
+
requireValue(
|
|
85
|
+
BigInt(io.statfsSync(`/proc/self/fd/${fd}`, { bigint: true }).type) ===
|
|
86
|
+
CGROUP2_MAGIC,
|
|
87
|
+
"not a kernel cgroup v2 filesystem",
|
|
88
|
+
);
|
|
89
|
+
return { fd, stat: opened, pinned: `/proc/self/fd/${fd}` };
|
|
90
|
+
} catch (error) {
|
|
91
|
+
io.closeSync(fd);
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Kernel object identities, not PIDs, establish the recovery target. The root
|
|
97
|
+
* must be explicitly delegated by the host. This is cooperative lifecycle
|
|
98
|
+
* recovery, not confinement against same-UID code that can move cgroup members.
|
|
99
|
+
*/
|
|
100
|
+
export function openRecoveryCgroup(value, executionId, runtime = {}) {
|
|
101
|
+
const io = runtime.fs || fs;
|
|
102
|
+
requireValue(
|
|
103
|
+
(runtime.platform || process.platform) === "linux",
|
|
104
|
+
"Linux cgroup v2 is required",
|
|
105
|
+
);
|
|
106
|
+
validateRecoveryCgroup(value, executionId);
|
|
107
|
+
requireValue(
|
|
108
|
+
String(io.readFileSync(BOOT_ID, "utf8")).trim() === value.bootId,
|
|
109
|
+
"boot identity changed",
|
|
110
|
+
);
|
|
111
|
+
const root = openDirectory(io, value.root);
|
|
112
|
+
let group;
|
|
113
|
+
try {
|
|
114
|
+
requireValue(
|
|
115
|
+
same(root.stat, value, "root"),
|
|
116
|
+
"delegated root identity changed",
|
|
117
|
+
);
|
|
118
|
+
group = openDirectory(io, path.posix.join(value.root, value.name));
|
|
119
|
+
requireValue(same(group.stat, value), "recovery group identity changed");
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (group) io.closeSync(group.fd);
|
|
122
|
+
io.closeSync(root.fd);
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
let closed = false;
|
|
126
|
+
const assertIdentity = () => {
|
|
127
|
+
requireValue(!closed, "group lease was closed");
|
|
128
|
+
requireValue(
|
|
129
|
+
String(io.readFileSync(BOOT_ID, "utf8")).trim() === value.bootId,
|
|
130
|
+
"boot identity changed",
|
|
131
|
+
);
|
|
132
|
+
const currentRoot = io.lstatSync(value.root, { bigint: true });
|
|
133
|
+
const currentGroup = io.lstatSync(path.posix.join(value.root, value.name), {
|
|
134
|
+
bigint: true,
|
|
135
|
+
});
|
|
136
|
+
requireValue(
|
|
137
|
+
currentRoot.isDirectory() &&
|
|
138
|
+
!currentRoot.isSymbolicLink() &&
|
|
139
|
+
same(currentRoot, value, "root") &&
|
|
140
|
+
currentGroup.isDirectory() &&
|
|
141
|
+
!currentGroup.isSymbolicLink() &&
|
|
142
|
+
same(currentGroup, value),
|
|
143
|
+
"cgroup authority was replaced",
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
const populated = () => {
|
|
147
|
+
assertIdentity();
|
|
148
|
+
const bytes = io.readFileSync(`${group.pinned}/cgroup.events`, "utf8");
|
|
149
|
+
requireValue(
|
|
150
|
+
typeof bytes === "string" && bytes.length <= 4096,
|
|
151
|
+
"invalid cgroup event payload",
|
|
152
|
+
);
|
|
153
|
+
const lines = bytes
|
|
154
|
+
.trim()
|
|
155
|
+
.split(/\n/u)
|
|
156
|
+
.map((line) => line.trim().split(/\s+/u));
|
|
157
|
+
const entries = lines.filter(([key]) => key === "populated");
|
|
158
|
+
requireValue(
|
|
159
|
+
entries.length === 1 &&
|
|
160
|
+
entries[0].length === 2 &&
|
|
161
|
+
["0", "1"].includes(entries[0][1]),
|
|
162
|
+
"missing or invalid populated fence",
|
|
163
|
+
);
|
|
164
|
+
return entries[0][1] === "1";
|
|
165
|
+
};
|
|
166
|
+
return Object.freeze({
|
|
167
|
+
identity: Object.freeze({ ...value }),
|
|
168
|
+
attachBeforeLaunch(proc) {
|
|
169
|
+
assertIdentity();
|
|
170
|
+
requireValue(
|
|
171
|
+
Number.isSafeInteger(proc?.pid) && proc.pid > 0,
|
|
172
|
+
"supervisor PID is unavailable",
|
|
173
|
+
);
|
|
174
|
+
requireValue(!populated(), "new recovery group was already populated");
|
|
175
|
+
// The native supervisor is still blocked on its private launch socket.
|
|
176
|
+
// No target can fork before this write and membership check complete.
|
|
177
|
+
io.writeFileSync(`${group.pinned}/cgroup.procs`, `${proc.pid}\n`, "utf8");
|
|
178
|
+
const members = String(
|
|
179
|
+
io.readFileSync(`${group.pinned}/cgroup.procs`, "utf8"),
|
|
180
|
+
)
|
|
181
|
+
.trim()
|
|
182
|
+
.split(/\s+/u);
|
|
183
|
+
requireValue(
|
|
184
|
+
members.includes(String(proc.pid)),
|
|
185
|
+
"supervisor membership was not observed",
|
|
186
|
+
);
|
|
187
|
+
},
|
|
188
|
+
kill() {
|
|
189
|
+
assertIdentity();
|
|
190
|
+
io.writeFileSync(`${group.pinned}/cgroup.kill`, "1\n", "utf8");
|
|
191
|
+
},
|
|
192
|
+
populated,
|
|
193
|
+
confirmEmpty() {
|
|
194
|
+
requireValue(!populated(), "recovery group remains populated");
|
|
195
|
+
return true;
|
|
196
|
+
},
|
|
197
|
+
close({ remove = false } = {}) {
|
|
198
|
+
if (closed) return;
|
|
199
|
+
try {
|
|
200
|
+
if (remove) {
|
|
201
|
+
requireValue(
|
|
202
|
+
!populated(),
|
|
203
|
+
"cannot remove a populated recovery group",
|
|
204
|
+
);
|
|
205
|
+
io.rmdirSync(`${root.pinned}/${value.name}`);
|
|
206
|
+
}
|
|
207
|
+
} finally {
|
|
208
|
+
closed = true;
|
|
209
|
+
io.closeSync(group.fd);
|
|
210
|
+
io.closeSync(root.fd);
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function prepareRecoveryCgroup(executionId, rootInput, runtime = {}) {
|
|
217
|
+
if (!rootInput) return null;
|
|
218
|
+
const io = runtime.fs || fs;
|
|
219
|
+
requireValue(
|
|
220
|
+
(runtime.platform || process.platform) === "linux",
|
|
221
|
+
"Linux cgroup v2 is required",
|
|
222
|
+
);
|
|
223
|
+
requireValue(UUID.test(executionId), "invalid execution identity");
|
|
224
|
+
const root = rootPath(rootInput);
|
|
225
|
+
const opened = openDirectory(io, root);
|
|
226
|
+
const name = `cc-owner-${executionId}`;
|
|
227
|
+
let created = false;
|
|
228
|
+
try {
|
|
229
|
+
const bootId = String(io.readFileSync(BOOT_ID, "utf8")).trim();
|
|
230
|
+
requireValue(UUID.test(bootId), "boot identity is unavailable");
|
|
231
|
+
// The unique group is created through the pinned delegated root.
|
|
232
|
+
io.mkdirSync(`${opened.pinned}/${name}`, { mode: 0o700 });
|
|
233
|
+
created = true;
|
|
234
|
+
const directory = path.posix.join(root, name);
|
|
235
|
+
const stat = io.lstatSync(directory, { bigint: true });
|
|
236
|
+
const value = {
|
|
237
|
+
schema: PROCESS_RECOVERY_CGROUP_SCHEMA,
|
|
238
|
+
root,
|
|
239
|
+
name,
|
|
240
|
+
bootId,
|
|
241
|
+
rootdev: String(opened.stat.dev),
|
|
242
|
+
rootino: String(opened.stat.ino),
|
|
243
|
+
...identity(stat),
|
|
244
|
+
};
|
|
245
|
+
const group = openRecoveryCgroup(value, executionId, runtime);
|
|
246
|
+
try {
|
|
247
|
+
group.confirmEmpty();
|
|
248
|
+
// Require the real kernel primitive before a recoverable launch is armed.
|
|
249
|
+
const killFd = io.openSync(
|
|
250
|
+
`${directory}/cgroup.kill`,
|
|
251
|
+
io.constants.O_WRONLY | io.constants.O_NOFOLLOW,
|
|
252
|
+
);
|
|
253
|
+
io.closeSync(killFd);
|
|
254
|
+
return group;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
group.close({ remove: true });
|
|
257
|
+
created = false;
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (created) {
|
|
262
|
+
try {
|
|
263
|
+
io.rmdirSync(`${opened.pinned}/${name}`);
|
|
264
|
+
} catch {
|
|
265
|
+
/* No process was launched into this group. */
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
throw denied("preparation failed", error);
|
|
269
|
+
} finally {
|
|
270
|
+
io.closeSync(opened.fd);
|
|
271
|
+
}
|
|
272
|
+
}
|