chainlesschain 0.166.77 → 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 +16 -7
- 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-CpiCBhRh.js → AIOps-DSyZEY2G.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CnU-Xvx-.js → ActionButton-DhNyZ577.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BJW-CNtf.js → Analytics-HMMpxmyi.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-UFQJL3G9.js → AppLayout-CwKMM5Ie.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-Dr_IUDoY.js → Artifacts-CNzDmph7.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-aglWo6rr.js → Audit-DxWpUW3V.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-Dm_TCiXC.js → BackgroundAgents-C1R2ST-P.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DUdX4-gA.js → Backup-B6biTaEV.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BTcLjy1o.js → BaseInput-CVnkD2Z5.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-Bhy9qZrl.js → Chat-Dbewl4Je.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-D0WrJQaQ.js → ChatBubbleRenderer-DVZ6SwC5.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-CblrPa21.js → Checkbox-BqqiF1JE.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-SEAHzOlr.js → Codegen-BVMkGumc.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DJz8iFOR.js → Col-DmiRHy96.js} +1 -1
- package/src/assets/web-panel/assets/{Community-RufKhcQt.js → Community-BDlpygqA.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Dlifx08U.js → Compact-B2NME_m-.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DdhQWcHh.js → Compliance-BlzDLE-R.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BLNn6oOm.js → Cowork-C8raPybB.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-Dv-JWY2d.js → Cron-DA8Py0zP.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-k7Pz7O-b.js → Crosschain-DU2XyHqB.js} +1 -1
- package/src/assets/web-panel/assets/{DID-C_10syUW.js → DID-Df098fT_.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CMyH6Szx.js → Dashboard-D1rT702x.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DjmD2Saa.js → Dropdown-BweXGo1O.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-36_IRUSo.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-DEgf9kyP.js → FamilyGuardDashboard-DrznZFVs.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DGO1wQls.js → Federation-DQZ2adIA.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Doj1FmKW.js → FormItemContext-DN30Dowh.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-GzOU27iT.js → GenericCardRenderer-DRdt0j1e.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Dd7D1EDK.js → Git-wwRgStzk.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-6IY8LcjI.js → Governance-CI4Uw0Nh.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BB9gl8q1.js → Inference-5TuE2AJI.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-CeCed5wd.js → KnowledgeGraph-oXyvmFSR.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CYOeHuaI.js → Logs-ewudSWuv.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-Bk8Zqc2R.js → MarkdownRenderer-Cz5nyufm.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-D-_xgHgM.js → Marketplace-DcZeWFrs.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-NnaUTF5t.js → McpTools-BcV9p1RS.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CndN9pMt.js → Memory-CMhYvugt.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BXblFmhY.js → MobileBridge-5RP2uwvT.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-CC26Dm2k.js → MobileProjects-BIuM9Ze4.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BhaqWCiW.js → Mtc-DmBszTDe.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-D6fktM2w.js → MtcAudit-Czdp5ZGt.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-CFvahz7s.js → Multisig-DRWUuNr-.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-Co73Ivya.js → NLProgramming-IRQQN0-c.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-B4g33L1o.js → Notes-CGhD1lW9.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-lXd17EbN.js → NotificationSettings-BtyPgQAh.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-BiOB0GIi.js → OrderTableRenderer-yUaqwCty.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-JQ7_wQIA.js → Organization-C3kTVn5i.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-gwgJgozQ.js → Overflow-C2-0iZhq.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-D9L_0gie.js → P2P-CISoEUxA.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-CbOPsFE9.js → PdhVaultBrowser-NfTVtvKQ.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-Cz42HNQf.js → Permissions-CfK0jJhm.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DjorYtrF.js → PersonalDataHub-OsVxxh_j.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-DyWPhmm5.js → Pipeline-DEmK_Ch2.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DyfLMl2j.js → Privacy-D61s73b0.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CyqDx1OE.js → ProjectInit-DkLF5gHb.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DsBw0Csy.js → ProjectSettings-BA7odTrI.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-CRaSI25Z.js → Projects-CdI1GcAG.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-CCI_klN-.js → Providers-D9BYyP1r.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-DNsnlDdS.js → QrScannerModal-CXMItUDD.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-B9r2iaee.js → QuickAsk-8-tF9ffZ.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BJDe03GR.js → Recommend-C2QGWOSi.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-J7JR3rRo.js → RemoteSession-B1M_v1lx.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-DR39AJeV.js → Reputation-D0WZHgv1.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Dh77m3NZ.js → Row-D6nf9-c-.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BUdg28nJ.js → RssFeed-ClZ6M-mN.js} +3 -3
- package/src/assets/web-panel/assets/{Search-BNC7sGhj.js → Search-ZWkt8vuO.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DISENGI7.js → Security-BUlQut2m.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BBBGpJ1-.js → Services-Cd_3xJOc.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DMcIoIv2.js → Skeleton-C8e2-cHW.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-QAyFbI3r.js → Skills-DucVRJp1.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CSg5wLH7.js → Sla-Csr5pUwI.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DMPxTc5x.js → SpeechSettings-CXvyaRAT.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CPG7l30y.js → SyncSettings-KdsIcV5h.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-Y1kOVvVL.js → Tasks-B-eHmJm9.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-jt9NagL0.js → Templates-BglVeaLK.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DauuJICx.js → Tenant-BBbQ8qan.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-E9rnKeGq.js → Terminal-Ca_pCVBi.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-Cc7z2APG.js → TimelineRenderer-YD6XG_vZ.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CV0UthRA.js → Tokens-2lRoeVli.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BFcJJGfu.js → Trigger-C-3XNAU9.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BAydbNJA.js → Trust-k-1PxdLG.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DcOS6sqL.js → UkeySign-FG-qW646.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BLHJZvif.js → VideoEditing-BNOe1cmd.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-4QFLKHwW.js → Wallet-D5C3i2Kw.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-BocEOOSj.js → WebAuthn-BOpztqd3.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-DCfF7D0B.js → WorkflowEditor-BpVrpUEv.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CxvfsYGr.js → chat-DIuMHb38.js} +1 -1
- package/src/assets/web-panel/assets/{colors-KtQB9DmQ.js → colors-CTFJPyzQ.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-Dx4J6arN.js → compact-item-CONfmJcW.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-Cq0olgzo.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-Bf9-h3D-.js → hasIn-CW4XOjDC.js} +1 -1
- package/src/assets/web-panel/assets/{index-CK2U_AHi.js → index-7wMSBCU7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMR-keCL.js → index-Akmg1WdH.js} +1 -1
- package/src/assets/web-panel/assets/{index-6dDDSCHD.js → index-B54qoVff.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmQuG0fr.js → index-B8r2t0Ch.js} +1 -1
- package/src/assets/web-panel/assets/{index-ClUfSChI.js → index-B9XoBoty.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3xfiItB.js → index-B9aS2VLu.js} +1 -1
- package/src/assets/web-panel/assets/{index-DQDR-_ai.js → index-BFs1Tznc.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZWjYfWt.js → index-BKgaRT0b.js} +1 -1
- package/src/assets/web-panel/assets/{index-nmyDOh_b.js → index-BM7bOUvu.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_8UcDan.js → index-BMg2JFmW.js} +1 -1
- package/src/assets/web-panel/assets/{index-C9yaUHm9.js → index-BRZsPxNn.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cz7f4Jaq.js → index-Bf19fKef.js} +1 -1
- package/src/assets/web-panel/assets/{index-CpO6DKuu.js → index-BkAbfBF-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DFdEDvFn.js → index-CBjb2A5R.js} +1 -1
- package/src/assets/web-panel/assets/{index-gt5N6VM_.js → index-CEZWQthY.js} +1 -1
- package/src/assets/web-panel/assets/{index-0fi8DIVQ.js → index-CFGLf88d.js} +1 -1
- package/src/assets/web-panel/assets/{index-hSzyoXri.js → index-CLUa68JH.js} +3 -3
- package/src/assets/web-panel/assets/{index-Cy865Hhq.js → index-CM2X25Er.js} +1 -1
- package/src/assets/web-panel/assets/{index-DDAe1UmZ.js → index-CX4T2uKN.js} +1 -1
- package/src/assets/web-panel/assets/{index-BMcC4cbj.js → index-CixqfdRk.js} +1 -1
- package/src/assets/web-panel/assets/{index-DsCdl2Od.js → index-CkRojEip.js} +1 -1
- package/src/assets/web-panel/assets/{index-DnU-dUWd.js → index-Cvmc3f25.js} +1 -1
- package/src/assets/web-panel/assets/{index-Br1mDMfO.js → index-D-lpTPS8.js} +1 -1
- package/src/assets/web-panel/assets/{index-CaIclw-E.js → index-D59GbWCN.js} +1 -1
- package/src/assets/web-panel/assets/{index-DBRlZdZj.js → index-DAESljiJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-B8WuPI-h.js → index-DDHUx4Yz.js} +1 -1
- package/src/assets/web-panel/assets/index-DG1mcYTa.js +1 -0
- package/src/assets/web-panel/assets/{index-D2BfSIrn.js → index-DP5pPedy.js} +1 -1
- package/src/assets/web-panel/assets/index-DRs5Pe8u.js +1 -0
- package/src/assets/web-panel/assets/{index-CqnggDos.js → index-DSlXL6Ju.js} +1 -1
- package/src/assets/web-panel/assets/{index-_FIz-0tp.js → index-DWCJnn2O.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKtQyb2o.js → index-DWY7YwDu.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfrFNMvz.js → index-DZTwP364.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHmdMKYl.js → index-DidrMvOx.js} +1 -1
- package/src/assets/web-panel/assets/{index-BO6XvO-C.js → index-Dj3K5kHs.js} +1 -1
- package/src/assets/web-panel/assets/{index-iE9tqEN5.js → index-YaeGscSw.js} +1 -1
- package/src/assets/web-panel/assets/{index-DI5bRqNG.js → index-YlMnFNOi.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cfsn7N18.js → index-asWsB34i.js} +1 -1
- package/src/assets/web-panel/assets/{index-CeYP_kT9.js → index-gkjiFjQd.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DvzKJ6o5.js → initDefaultProps-BOr4igh3.js} +1 -1
- package/src/assets/web-panel/assets/{motion-D1yTvmxW.js → motion-CK469n2j.js} +1 -1
- package/src/assets/web-panel/assets/{move-DxtSIF8K.js → move-DzjudlYQ.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BDsQgIHY.js → mtc-parser-BQaKUxC6.js} +1 -1
- package/src/assets/web-panel/assets/{omit-UVBJW8kh.js → omit-MciovCFD.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-D4QqtTyX.js → pickAttrs-DDPJ4zAz.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-QXmu6vBf.js → placementArrow-Cznyb-jI.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-D-PmXO9M.js → responsiveObserve-AJK3SLzJ.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CKbGxEZJ.js → slide-CPQ28wu7.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BbgXPkXm.js → statusUtils-CFjzvqf_.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DOr9rk68.js → styleChecker-BPyFfTuR.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-B2wO2r36.js → useFlexGapSupport-Dq7N6R05.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-SeVGBK34.js → useFs-C3GS0uTc.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-DUJBCxjD.js → usePersonalDataHub-BhgIVzP_.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CF4EYsig.js → vnode-4ZD9-MDr.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-pRUs5pqI.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 +13 -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/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-stream.js +202 -4
- package/src/assets/web-panel/assets/EvolutionSettings-CmtU0XmM.js +0 -1
- package/src/assets/web-panel/assets/devWarning-CmgDNkoO.js +0 -1
- package/src/assets/web-panel/assets/index-BpIZ2S01.js +0 -1
- package/src/assets/web-panel/assets/index-C-VmKuyv.js +0 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Broker implementation detail. fd 3 is a private caller lifeline, never
|
|
2
|
+
// inherited by the target. stdout/stderr remain the target's raw byte streams.
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { Socket } from "node:net";
|
|
5
|
+
import { StringDecoder } from "node:string_decoder";
|
|
6
|
+
|
|
7
|
+
const control = new Socket({ fd: 3, readable: true, writable: true });
|
|
8
|
+
const MAX_REQUEST_BYTES = 1024 * 1024;
|
|
9
|
+
let buffer = "";
|
|
10
|
+
const decoder = new StringDecoder("utf8");
|
|
11
|
+
let bytes = 0;
|
|
12
|
+
let target = null;
|
|
13
|
+
let launched = false;
|
|
14
|
+
let stopping = false;
|
|
15
|
+
let killing = false;
|
|
16
|
+
let graceMs = 250;
|
|
17
|
+
let killTimer;
|
|
18
|
+
|
|
19
|
+
function hardStop() {
|
|
20
|
+
if (killing) return;
|
|
21
|
+
killing = true;
|
|
22
|
+
clearTimeout(killTimer);
|
|
23
|
+
// We are still the live leader, so -process.pid cannot name a reused group.
|
|
24
|
+
const killGroup = () => {
|
|
25
|
+
try {
|
|
26
|
+
process.kill(-process.pid, "SIGKILL");
|
|
27
|
+
} catch {
|
|
28
|
+
process.exit(70);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
if (!control.destroyed && control.writable) {
|
|
32
|
+
control.write('{"type":"terminating"}\n', killGroup);
|
|
33
|
+
setTimeout(killGroup, 100);
|
|
34
|
+
} else {
|
|
35
|
+
killGroup();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function stop(signal = "SIGTERM") {
|
|
40
|
+
if (signal === "SIGKILL") {
|
|
41
|
+
hardStop();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (stopping || killing) return;
|
|
45
|
+
stopping = true;
|
|
46
|
+
killTimer = setTimeout(hardStop, graceMs);
|
|
47
|
+
try {
|
|
48
|
+
process.kill(-process.pid, "SIGTERM");
|
|
49
|
+
} catch {
|
|
50
|
+
hardStop();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
process.on("SIGTERM", () => stop());
|
|
55
|
+
process.on("SIGINT", () => stop());
|
|
56
|
+
control.on("end", () => stop());
|
|
57
|
+
control.on("error", () => stop());
|
|
58
|
+
control.on("close", () => stop());
|
|
59
|
+
// A caller that disappears or never sends a launch cannot leave a keeper.
|
|
60
|
+
const startupTimer = setTimeout(() => stop(), 10_000);
|
|
61
|
+
|
|
62
|
+
function sendTargetExit(code, signal, spawnError = null) {
|
|
63
|
+
if (!control.destroyed && control.writable) {
|
|
64
|
+
control.write(
|
|
65
|
+
JSON.stringify({ type: "target-exit", code, signal, spawnError }) + "\n",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
stop();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function receive(message) {
|
|
72
|
+
if (
|
|
73
|
+
message.type === "stop" &&
|
|
74
|
+
["SIGTERM", "SIGKILL"].includes(message.signal)
|
|
75
|
+
) {
|
|
76
|
+
stop(message.signal);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (message.type !== "launch" || launched || stopping || killing)
|
|
80
|
+
throw new Error("invalid launch");
|
|
81
|
+
launched = true;
|
|
82
|
+
clearTimeout(startupTimer);
|
|
83
|
+
if (
|
|
84
|
+
!Number.isSafeInteger(message.graceMs) ||
|
|
85
|
+
message.graceMs < 1 ||
|
|
86
|
+
message.graceMs > 5000
|
|
87
|
+
)
|
|
88
|
+
throw new Error("invalid grace");
|
|
89
|
+
graceMs = message.graceMs;
|
|
90
|
+
// This is the exact, pre-admitted plan supplied by the Broker-side caller.
|
|
91
|
+
// No shell, inherited control fd, detached target, or env expansion.
|
|
92
|
+
target = spawn(message.command, message.args, {
|
|
93
|
+
cwd: message.cwd,
|
|
94
|
+
env: message.env,
|
|
95
|
+
shell: false,
|
|
96
|
+
detached: false,
|
|
97
|
+
stdio: [0, 1, 2],
|
|
98
|
+
});
|
|
99
|
+
target.once("spawn", () => {
|
|
100
|
+
control.write(JSON.stringify({ type: "started", pid: target.pid }) + "\n");
|
|
101
|
+
});
|
|
102
|
+
target.once("error", (error) =>
|
|
103
|
+
sendTargetExit(
|
|
104
|
+
null,
|
|
105
|
+
null,
|
|
106
|
+
/^[A-Z0-9_]{1,40}$/.test(error.code ?? "") ? error.code : "SPAWN_FAILED",
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
// exit, not close: a descendant can retain inherited stdout after root exit.
|
|
110
|
+
target.once("exit", (code, signal) => sendTargetExit(code, signal));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
control.on("data", (chunk) => {
|
|
114
|
+
bytes += chunk.length;
|
|
115
|
+
if (bytes > MAX_REQUEST_BYTES + 1024) {
|
|
116
|
+
stop();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
buffer += decoder.write(chunk);
|
|
120
|
+
let newline;
|
|
121
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
122
|
+
const line = buffer.slice(0, newline);
|
|
123
|
+
buffer = buffer.slice(newline + 1);
|
|
124
|
+
try {
|
|
125
|
+
receive(JSON.parse(line));
|
|
126
|
+
} catch {
|
|
127
|
+
stop();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const WORKER = fileURLToPath(
|
|
6
|
+
new URL("./owned-posix-process-group-worker.mjs", import.meta.url),
|
|
7
|
+
);
|
|
8
|
+
const MAX_REQUEST_BYTES = 1024 * 1024;
|
|
9
|
+
const MAX_REPLY_BYTES = 16 * 1024;
|
|
10
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
+
|
|
12
|
+
function milliseconds(value, fallback, maximum) {
|
|
13
|
+
if (value === undefined) return fallback;
|
|
14
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
|
|
15
|
+
throw new TypeError("Invalid owned process group deadline");
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Read-only observation. Never use this numeric identity to send a signal. */
|
|
21
|
+
export function inspectPosixProcessGroup(pgid, { platform, spawnSync }) {
|
|
22
|
+
if (!Number.isSafeInteger(pgid) || pgid < 1) return null;
|
|
23
|
+
try {
|
|
24
|
+
if (platform === "linux") {
|
|
25
|
+
const states = [];
|
|
26
|
+
for (const entry of readdirSync("/proc")) {
|
|
27
|
+
if (!/^\d+$/.test(entry)) continue;
|
|
28
|
+
let raw;
|
|
29
|
+
try {
|
|
30
|
+
raw = readFileSync(`/proc/${entry}/stat`, "utf8");
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.code === "ENOENT" || error.code === "ESRCH") continue;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
// comm may itself contain spaces or parentheses.
|
|
36
|
+
const fields = raw
|
|
37
|
+
.slice(raw.lastIndexOf(")") + 2)
|
|
38
|
+
.trim()
|
|
39
|
+
.split(/\s+/);
|
|
40
|
+
if (fields.length < 4 || !/^[A-Zt]$/.test(fields[0])) return null;
|
|
41
|
+
if (Number(fields[2]) === pgid) states.push(fields[0]);
|
|
42
|
+
}
|
|
43
|
+
return states;
|
|
44
|
+
}
|
|
45
|
+
if (platform === "darwin") {
|
|
46
|
+
const result = spawnSync("/bin/ps", ["-axo", "pgid=,stat="], {
|
|
47
|
+
encoding: "utf8",
|
|
48
|
+
shell: false,
|
|
49
|
+
timeout: 2000,
|
|
50
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
51
|
+
env: { PATH: "/usr/bin:/bin", LC_ALL: "C" },
|
|
52
|
+
});
|
|
53
|
+
if (result.error || result.status !== 0 || !result.stdout?.trim()) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const states = [];
|
|
57
|
+
for (const line of result.stdout.trim().split("\n")) {
|
|
58
|
+
const match = /^\s*(\d+)\s+([A-Z?][A-Za-z+<>sNXEVL-]*)\s*$/.exec(line);
|
|
59
|
+
if (!match) return null;
|
|
60
|
+
if (Number(match[1]) === pgid) states.push(match[2][0]);
|
|
61
|
+
}
|
|
62
|
+
return states;
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Missing visibility is not proof of group exit.
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Low-level lifecycle primitive for an already admitted Broker launch plan.
|
|
72
|
+
* Callers supply the Broker's native seams; this module grants no execution
|
|
73
|
+
* permission and does not add a sandbox guarantee. Production wiring is kept
|
|
74
|
+
* separate so policy/credential/descriptor plans cannot silently be bypassed.
|
|
75
|
+
*
|
|
76
|
+
* The detached worker remains the group leader after the target exits. Only
|
|
77
|
+
* that living worker sends TERM/KILL to its own group. The caller sends control
|
|
78
|
+
* messages over a private inherited socket, never a delayed numeric signal.
|
|
79
|
+
* A POSIX group is escapable with setsid; confirmation covers group members,
|
|
80
|
+
* not arbitrary descendants or containment against a hostile executable.
|
|
81
|
+
*/
|
|
82
|
+
export function spawnOwnedPosixProcessGroup(command, args, options, native) {
|
|
83
|
+
const platform = native.platform ?? process.platform;
|
|
84
|
+
if (!["linux", "darwin"].includes(platform)) {
|
|
85
|
+
throw new TypeError("Owned POSIX process groups require Linux or macOS");
|
|
86
|
+
}
|
|
87
|
+
if (
|
|
88
|
+
typeof native.spawn !== "function" ||
|
|
89
|
+
typeof native.spawnSync !== "function"
|
|
90
|
+
) {
|
|
91
|
+
throw new TypeError("Broker native process seams are required");
|
|
92
|
+
}
|
|
93
|
+
const allowed = new Set(["cwd", "env", "graceMs", "confirmMs"]);
|
|
94
|
+
if (Object.keys(options).some((key) => !allowed.has(key))) {
|
|
95
|
+
throw new TypeError("Unsupported owned process group option");
|
|
96
|
+
}
|
|
97
|
+
if (
|
|
98
|
+
typeof command !== "string" ||
|
|
99
|
+
!command ||
|
|
100
|
+
command.includes("\0") ||
|
|
101
|
+
!Array.isArray(args) ||
|
|
102
|
+
args.some((arg) => typeof arg !== "string" || arg.includes("\0")) ||
|
|
103
|
+
typeof options.cwd !== "string" ||
|
|
104
|
+
!isAbsolute(options.cwd) ||
|
|
105
|
+
options.cwd.includes("\0") ||
|
|
106
|
+
!options.env ||
|
|
107
|
+
typeof options.env !== "object" ||
|
|
108
|
+
Array.isArray(options.env) ||
|
|
109
|
+
Object.entries(options.env).some(
|
|
110
|
+
([key, value]) =>
|
|
111
|
+
!key ||
|
|
112
|
+
key.includes("=") ||
|
|
113
|
+
key.includes("\0") ||
|
|
114
|
+
typeof value !== "string" ||
|
|
115
|
+
value.includes("\0"),
|
|
116
|
+
)
|
|
117
|
+
) {
|
|
118
|
+
throw new TypeError("Invalid owned process group invocation");
|
|
119
|
+
}
|
|
120
|
+
const graceMs = milliseconds(options.graceMs, 250, 5000);
|
|
121
|
+
const confirmMs = milliseconds(options.confirmMs, 2000, 10000);
|
|
122
|
+
const request =
|
|
123
|
+
JSON.stringify({
|
|
124
|
+
type: "launch",
|
|
125
|
+
command,
|
|
126
|
+
args,
|
|
127
|
+
cwd: options.cwd,
|
|
128
|
+
env: options.env,
|
|
129
|
+
graceMs,
|
|
130
|
+
}) + "\n";
|
|
131
|
+
if (Buffer.byteLength(request) > MAX_REQUEST_BYTES) {
|
|
132
|
+
throw new TypeError("Owned process group launch exceeds its byte limit");
|
|
133
|
+
}
|
|
134
|
+
const child = native.spawn(process.execPath, [WORKER], {
|
|
135
|
+
cwd: options.cwd,
|
|
136
|
+
// The target receives the admitted environment over the control socket.
|
|
137
|
+
// NODE_OPTIONS/loader injection must not run in the unsandboxed supervisor.
|
|
138
|
+
env: { PATH: "/usr/bin:/bin", LC_ALL: "C" },
|
|
139
|
+
shell: false,
|
|
140
|
+
detached: true,
|
|
141
|
+
stdio: ["pipe", "pipe", "pipe", "pipe"],
|
|
142
|
+
});
|
|
143
|
+
const control = child.stdio[3];
|
|
144
|
+
let supervisorClosed = false;
|
|
145
|
+
let target = null;
|
|
146
|
+
let targetPid = null;
|
|
147
|
+
let terminating = false;
|
|
148
|
+
let protocolError = null;
|
|
149
|
+
let reply = "";
|
|
150
|
+
let replyBytes = 0;
|
|
151
|
+
let requestedSignal = null;
|
|
152
|
+
const terminate = (signal = "SIGTERM") => {
|
|
153
|
+
if (!["SIGTERM", "SIGKILL"].includes(signal)) {
|
|
154
|
+
throw new TypeError("Unsupported owned process group signal");
|
|
155
|
+
}
|
|
156
|
+
if (supervisorClosed || control.destroyed || !control.writable)
|
|
157
|
+
return false;
|
|
158
|
+
if (requestedSignal === "SIGKILL" || requestedSignal === signal)
|
|
159
|
+
return true;
|
|
160
|
+
requestedSignal = signal;
|
|
161
|
+
control.write(JSON.stringify({ type: "stop", signal }) + "\n");
|
|
162
|
+
return true;
|
|
163
|
+
};
|
|
164
|
+
control.on("error", () => {
|
|
165
|
+
protocolError ??= "control-channel-failed";
|
|
166
|
+
// Closing the lifeline asks the living worker to clean its own group.
|
|
167
|
+
control.destroy();
|
|
168
|
+
});
|
|
169
|
+
control.on("data", (chunk) => {
|
|
170
|
+
replyBytes += chunk.length;
|
|
171
|
+
if (replyBytes > MAX_REPLY_BYTES) {
|
|
172
|
+
protocolError = "control-reply-limit";
|
|
173
|
+
control.destroy();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
reply += chunk.toString("utf8");
|
|
177
|
+
let newline;
|
|
178
|
+
while ((newline = reply.indexOf("\n")) >= 0) {
|
|
179
|
+
const line = reply.slice(0, newline);
|
|
180
|
+
reply = reply.slice(newline + 1);
|
|
181
|
+
try {
|
|
182
|
+
const message = JSON.parse(line);
|
|
183
|
+
if (
|
|
184
|
+
message.type === "started" &&
|
|
185
|
+
targetPid === null &&
|
|
186
|
+
Number.isSafeInteger(message.pid) &&
|
|
187
|
+
message.pid > 0
|
|
188
|
+
) {
|
|
189
|
+
targetPid = message.pid;
|
|
190
|
+
} else if (
|
|
191
|
+
message.type === "target-exit" &&
|
|
192
|
+
target === null &&
|
|
193
|
+
(message.code === null || Number.isInteger(message.code)) &&
|
|
194
|
+
(message.signal === null || typeof message.signal === "string")
|
|
195
|
+
) {
|
|
196
|
+
target = {
|
|
197
|
+
code: message.code,
|
|
198
|
+
signal: message.signal,
|
|
199
|
+
spawnError: message.spawnError ?? null,
|
|
200
|
+
};
|
|
201
|
+
} else if (message.type === "terminating" && !terminating) {
|
|
202
|
+
terminating = true;
|
|
203
|
+
} else {
|
|
204
|
+
throw new Error("invalid reply");
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
protocolError = "invalid-control-reply";
|
|
208
|
+
control.destroy();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
const completion = new Promise((resolveCompletion) => {
|
|
214
|
+
child.on("error", () => {
|
|
215
|
+
protocolError ??= "supervisor-spawn-failed";
|
|
216
|
+
});
|
|
217
|
+
child.once("close", async (code, signal) => {
|
|
218
|
+
supervisorClosed = true;
|
|
219
|
+
if (reply.length > 0) protocolError ??= "truncated-control-reply";
|
|
220
|
+
control.destroy();
|
|
221
|
+
const inspect =
|
|
222
|
+
native.inspectGroup ??
|
|
223
|
+
((pid) =>
|
|
224
|
+
inspectPosixProcessGroup(pid, {
|
|
225
|
+
platform,
|
|
226
|
+
spawnSync: native.spawnSync,
|
|
227
|
+
}));
|
|
228
|
+
const deadline = performance.now() + confirmMs;
|
|
229
|
+
let states = null;
|
|
230
|
+
do {
|
|
231
|
+
try {
|
|
232
|
+
states = inspect(child.pid);
|
|
233
|
+
} catch {
|
|
234
|
+
states = null;
|
|
235
|
+
}
|
|
236
|
+
if (
|
|
237
|
+
Array.isArray(states) &&
|
|
238
|
+
states.every((state) => state === "Z" || state === "X")
|
|
239
|
+
)
|
|
240
|
+
break;
|
|
241
|
+
await delay(25);
|
|
242
|
+
} while (performance.now() < deadline);
|
|
243
|
+
const groupStopped =
|
|
244
|
+
Array.isArray(states) &&
|
|
245
|
+
states.every((state) => state === "Z" || state === "X");
|
|
246
|
+
resolveCompletion({
|
|
247
|
+
targetPid,
|
|
248
|
+
target,
|
|
249
|
+
supervisor: { code, signal, closed: true },
|
|
250
|
+
cleanup: {
|
|
251
|
+
mode: "posix-process-group",
|
|
252
|
+
confirmed:
|
|
253
|
+
groupStopped &&
|
|
254
|
+
terminating &&
|
|
255
|
+
!protocolError &&
|
|
256
|
+
signal === "SIGKILL",
|
|
257
|
+
groupStopped,
|
|
258
|
+
states,
|
|
259
|
+
protocolError,
|
|
260
|
+
processTreeContained: false,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
control.write(request);
|
|
266
|
+
return Object.freeze({ child, terminate, completion });
|
|
267
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { withFileLock } from "../with-file-lock.js";
|
|
5
|
+
import { writeSecurityStore } from "../durable-security-store.js";
|
|
6
|
+
import { ensurePrivateDirectory } from "../secure-fs.js";
|
|
7
|
+
|
|
8
|
+
export const PROCESS_OWNERSHIP_PENDING = "BROKER_PROCESS_OWNERSHIP_PENDING";
|
|
9
|
+
export const PROCESS_OWNERSHIP_JOURNAL_UNAVAILABLE =
|
|
10
|
+
"BROKER_PROCESS_OWNERSHIP_JOURNAL_UNAVAILABLE";
|
|
11
|
+
const SCHEMA = "chainlesschain.process-ownership-journal/v1";
|
|
12
|
+
const MAX_BYTES = 1024 * 1024;
|
|
13
|
+
const MAX_PENDING = 1024;
|
|
14
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
15
|
+
|
|
16
|
+
function unavailable(cause) {
|
|
17
|
+
const error = new Error(
|
|
18
|
+
"Process ownership journal is unavailable; execution is denied",
|
|
19
|
+
{ cause },
|
|
20
|
+
);
|
|
21
|
+
error.code = PROCESS_OWNERSHIP_JOURNAL_UNAVAILABLE;
|
|
22
|
+
error.executionStarted = false;
|
|
23
|
+
error.recoveryRequired = true;
|
|
24
|
+
return error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validate(state) {
|
|
28
|
+
if (
|
|
29
|
+
!state ||
|
|
30
|
+
Object.keys(state).sort().join() !== "pending,schema" ||
|
|
31
|
+
state.schema !== SCHEMA ||
|
|
32
|
+
!Array.isArray(state.pending) ||
|
|
33
|
+
state.pending.length > MAX_PENDING
|
|
34
|
+
)
|
|
35
|
+
throw new Error("Invalid ownership journal schema");
|
|
36
|
+
const ids = new Set();
|
|
37
|
+
for (const entry of state.pending) {
|
|
38
|
+
if (
|
|
39
|
+
!entry ||
|
|
40
|
+
Object.keys(entry).sort().join() !== "executionId,ownerPid,token" ||
|
|
41
|
+
!UUID.test(entry.executionId) ||
|
|
42
|
+
!UUID.test(entry.token) ||
|
|
43
|
+
!Number.isSafeInteger(entry.ownerPid) ||
|
|
44
|
+
entry.ownerPid < 1 ||
|
|
45
|
+
ids.has(entry.executionId)
|
|
46
|
+
)
|
|
47
|
+
throw new Error("Invalid ownership journal entry");
|
|
48
|
+
ids.add(entry.executionId);
|
|
49
|
+
}
|
|
50
|
+
return state;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function privateEntry(entry, directory) {
|
|
54
|
+
return (
|
|
55
|
+
(directory ? entry.isDirectory() : entry.isFile() && entry.nlink === 1) &&
|
|
56
|
+
entry.uid === process.getuid() &&
|
|
57
|
+
(entry.mode & 0o077) === 0
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Linux cooperative restart fence. Uses the existing strict file lock and
|
|
63
|
+
* atomic security-store writer; this is not a workspace lock or a sandbox.
|
|
64
|
+
* Peers must wait for outstanding supervised launches to close. PID liveness,
|
|
65
|
+
* age, owner death and reboot are never authorities to erase a pending launch.
|
|
66
|
+
*/
|
|
67
|
+
export class ProcessOwnershipJournal {
|
|
68
|
+
#directory;
|
|
69
|
+
#local = new Map();
|
|
70
|
+
#fault = null;
|
|
71
|
+
#initialized = false;
|
|
72
|
+
|
|
73
|
+
constructor(directory) {
|
|
74
|
+
if (process.platform !== "linux" || !path.isAbsolute(directory))
|
|
75
|
+
throw new TypeError(
|
|
76
|
+
"Linux ownership journal requires an absolute directory",
|
|
77
|
+
);
|
|
78
|
+
this.#directory = path.resolve(directory);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#withDirectory(create, operation) {
|
|
82
|
+
let descriptor;
|
|
83
|
+
try {
|
|
84
|
+
let before;
|
|
85
|
+
try {
|
|
86
|
+
before = fs.lstatSync(this.#directory);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error.code !== "ENOENT" || this.#initialized) throw error;
|
|
89
|
+
if (!create) return operation(null);
|
|
90
|
+
ensurePrivateDirectory(this.#directory, { failIfUnavailable: true });
|
|
91
|
+
// Publish the new authority directory before admitting its first launch.
|
|
92
|
+
const parent = fs.openSync(
|
|
93
|
+
path.dirname(this.#directory),
|
|
94
|
+
fs.constants.O_RDONLY | fs.constants.O_DIRECTORY,
|
|
95
|
+
);
|
|
96
|
+
try {
|
|
97
|
+
fs.fsyncSync(parent);
|
|
98
|
+
} finally {
|
|
99
|
+
fs.closeSync(parent);
|
|
100
|
+
}
|
|
101
|
+
before = fs.lstatSync(this.#directory);
|
|
102
|
+
}
|
|
103
|
+
if (!privateEntry(before, true))
|
|
104
|
+
throw new Error("Unsafe ownership directory");
|
|
105
|
+
if (fs.realpathSync(this.#directory) !== this.#directory)
|
|
106
|
+
throw new Error("Ownership directory contains a link");
|
|
107
|
+
descriptor = fs.openSync(
|
|
108
|
+
this.#directory,
|
|
109
|
+
fs.constants.O_RDONLY |
|
|
110
|
+
fs.constants.O_DIRECTORY |
|
|
111
|
+
fs.constants.O_NOFOLLOW,
|
|
112
|
+
);
|
|
113
|
+
const opened = fs.fstatSync(descriptor);
|
|
114
|
+
if (
|
|
115
|
+
before.dev !== opened.dev ||
|
|
116
|
+
before.ino !== opened.ino ||
|
|
117
|
+
!privateEntry(opened, true)
|
|
118
|
+
)
|
|
119
|
+
throw new Error("Ownership directory changed");
|
|
120
|
+
const result = operation(`/proc/self/fd/${descriptor}/journal.json`);
|
|
121
|
+
const after = fs.lstatSync(this.#directory);
|
|
122
|
+
if (
|
|
123
|
+
after.dev !== opened.dev ||
|
|
124
|
+
after.ino !== opened.ino ||
|
|
125
|
+
!privateEntry(after, true)
|
|
126
|
+
)
|
|
127
|
+
throw new Error("Ownership directory replaced");
|
|
128
|
+
return result;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error.code === PROCESS_OWNERSHIP_PENDING) throw error;
|
|
131
|
+
this.#fault = unavailable(error);
|
|
132
|
+
throw this.#fault;
|
|
133
|
+
} finally {
|
|
134
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#read(file) {
|
|
139
|
+
if (file === null) return { schema: SCHEMA, pending: [] };
|
|
140
|
+
let descriptor;
|
|
141
|
+
try {
|
|
142
|
+
try {
|
|
143
|
+
descriptor = fs.openSync(
|
|
144
|
+
file,
|
|
145
|
+
fs.constants.O_RDONLY |
|
|
146
|
+
fs.constants.O_NOFOLLOW |
|
|
147
|
+
fs.constants.O_NONBLOCK,
|
|
148
|
+
);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error.code === "ENOENT" && !this.#initialized)
|
|
151
|
+
return { schema: SCHEMA, pending: [] };
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
const before = fs.fstatSync(descriptor);
|
|
155
|
+
if (!privateEntry(before, false) || before.size > MAX_BYTES)
|
|
156
|
+
throw new Error("Unsafe ownership journal");
|
|
157
|
+
const bytes = Buffer.alloc(before.size + 1);
|
|
158
|
+
let length = 0;
|
|
159
|
+
while (length < bytes.length) {
|
|
160
|
+
const count = fs.readSync(
|
|
161
|
+
descriptor,
|
|
162
|
+
bytes,
|
|
163
|
+
length,
|
|
164
|
+
bytes.length - length,
|
|
165
|
+
null,
|
|
166
|
+
);
|
|
167
|
+
if (count === 0) break;
|
|
168
|
+
length += count;
|
|
169
|
+
}
|
|
170
|
+
const after = fs.fstatSync(descriptor);
|
|
171
|
+
if (
|
|
172
|
+
length !== before.size ||
|
|
173
|
+
before.size !== after.size ||
|
|
174
|
+
before.mtimeMs !== after.mtimeMs ||
|
|
175
|
+
before.ctimeMs !== after.ctimeMs
|
|
176
|
+
)
|
|
177
|
+
throw new Error("Ownership journal changed during read");
|
|
178
|
+
const state = validate(
|
|
179
|
+
JSON.parse(bytes.subarray(0, length).toString("utf8")),
|
|
180
|
+
);
|
|
181
|
+
this.#initialized = true;
|
|
182
|
+
return state;
|
|
183
|
+
} finally {
|
|
184
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#assertState(state) {
|
|
189
|
+
const blocked = state.pending.filter(
|
|
190
|
+
(entry) => this.#local.get(entry.executionId)?.token !== entry.token,
|
|
191
|
+
);
|
|
192
|
+
if (blocked.length) {
|
|
193
|
+
const error = new Error(
|
|
194
|
+
"Process ownership is pending in another runtime; confirmed cleanup is required before execution",
|
|
195
|
+
);
|
|
196
|
+
error.code = PROCESS_OWNERSHIP_PENDING;
|
|
197
|
+
error.executionStarted = false;
|
|
198
|
+
error.recoveryRequired = true;
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
// Removal/replacement of our admitted record must not make this process
|
|
202
|
+
// forget a live launch. Cross-restart deletion of the whole anchor is
|
|
203
|
+
// outside this cooperative same-user persistence boundary.
|
|
204
|
+
for (const [id, local] of this.#local) {
|
|
205
|
+
if (
|
|
206
|
+
!state.pending.some(
|
|
207
|
+
(entry) => entry.executionId === id && entry.token === local.token,
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
throw new Error("Admitted ownership record disappeared");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
assertAvailable() {
|
|
215
|
+
if (this.#fault) throw this.#fault;
|
|
216
|
+
this.#withDirectory(false, (file) => this.#assertState(this.#read(file)));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
inspect() {
|
|
220
|
+
if (this.#fault) throw this.#fault;
|
|
221
|
+
return this.#withDirectory(false, (file) => {
|
|
222
|
+
const state = this.#read(file);
|
|
223
|
+
return {
|
|
224
|
+
pendingExecutionIds: state.pending.map((entry) => entry.executionId),
|
|
225
|
+
blocked: state.pending.some(
|
|
226
|
+
(entry) => this.#local.get(entry.executionId)?.token !== entry.token,
|
|
227
|
+
),
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
prepare(executionId) {
|
|
233
|
+
if (!UUID.test(executionId))
|
|
234
|
+
throw new TypeError("Invalid ownership execution id");
|
|
235
|
+
if (this.#fault) throw this.#fault;
|
|
236
|
+
const entry = { executionId, ownerPid: process.pid, token: randomUUID() };
|
|
237
|
+
this.#withDirectory(true, (file) =>
|
|
238
|
+
withFileLock(
|
|
239
|
+
file,
|
|
240
|
+
() => {
|
|
241
|
+
const state = this.#read(file);
|
|
242
|
+
this.#assertState(state);
|
|
243
|
+
if (
|
|
244
|
+
state.pending.length >= MAX_PENDING ||
|
|
245
|
+
state.pending.some((item) => item.executionId === executionId)
|
|
246
|
+
)
|
|
247
|
+
throw new Error("Ownership journal admission limit");
|
|
248
|
+
state.pending.push(entry);
|
|
249
|
+
writeSecurityStore(file, "Process ownership", state);
|
|
250
|
+
this.#initialized = true;
|
|
251
|
+
this.#local.set(executionId, entry);
|
|
252
|
+
},
|
|
253
|
+
{ failIfUnavailable: true, timeoutMs: 2000 },
|
|
254
|
+
),
|
|
255
|
+
);
|
|
256
|
+
let settled = false;
|
|
257
|
+
let retained = false;
|
|
258
|
+
return Object.freeze({
|
|
259
|
+
retain: () => {
|
|
260
|
+
retained = true;
|
|
261
|
+
this.#local.delete(executionId);
|
|
262
|
+
},
|
|
263
|
+
// Broker calls this only before entering native spawn or after its
|
|
264
|
+
// authentic child close fence. No public reset/reclaim-by-PID endpoint.
|
|
265
|
+
settle: () => {
|
|
266
|
+
if (settled) return;
|
|
267
|
+
if (retained)
|
|
268
|
+
throw unavailable(
|
|
269
|
+
new Error("Retained ownership cannot be settled without recovery"),
|
|
270
|
+
);
|
|
271
|
+
this.#withDirectory(false, (file) =>
|
|
272
|
+
withFileLock(
|
|
273
|
+
file,
|
|
274
|
+
() => {
|
|
275
|
+
const state = this.#read(file);
|
|
276
|
+
if (
|
|
277
|
+
!state.pending.some(
|
|
278
|
+
(item) =>
|
|
279
|
+
item.executionId === executionId &&
|
|
280
|
+
item.token === entry.token,
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
throw new Error("Ownership settlement lost its record");
|
|
284
|
+
state.pending = state.pending.filter(
|
|
285
|
+
(item) => item.executionId !== executionId,
|
|
286
|
+
);
|
|
287
|
+
writeSecurityStore(file, "Process ownership", state);
|
|
288
|
+
this.#local.delete(executionId);
|
|
289
|
+
settled = true;
|
|
290
|
+
},
|
|
291
|
+
{ failIfUnavailable: true, timeoutMs: 2000 },
|
|
292
|
+
),
|
|
293
|
+
);
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|