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,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
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { getMachineSecurityAnchorDir } from "../paths.js";
|
|
3
|
+
import { ProcessOwnershipJournal } from "./process-ownership-journal.js";
|
|
4
|
+
|
|
5
|
+
// Shared by every Broker in this JS runtime. A new Broker, different cwd or
|
|
6
|
+
// cleared audit history cannot turn a lost process owner into cleanup evidence.
|
|
7
|
+
// Linux additionally records ownership before the native launch. Retaining
|
|
8
|
+
// that record across restart is a fence, not proof of descendant cleanup.
|
|
9
|
+
const unresolvedOwners = new Map();
|
|
10
|
+
let journal = null;
|
|
11
|
+
|
|
12
|
+
function durableJournal() {
|
|
13
|
+
if (process.platform !== "linux") return null;
|
|
14
|
+
journal ??= new ProcessOwnershipJournal(
|
|
15
|
+
path.join(getMachineSecurityAnchorDir(), "process-ownership-v1"),
|
|
16
|
+
);
|
|
17
|
+
return journal;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function prepareProcessOwnership(executionId) {
|
|
21
|
+
return durableJournal()?.prepare(executionId) ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const PROCESS_OWNERSHIP_UNCONFIRMED =
|
|
25
|
+
"BROKER_PROCESS_OWNERSHIP_UNCONFIRMED";
|
|
26
|
+
|
|
27
|
+
export function observeProcessOwnership(proc, executionId, lease = null) {
|
|
28
|
+
// The facade emits close only after actual tree cleanup, or after a native
|
|
29
|
+
// spawn that created no supervisor at all. Persist that fact before Bridge
|
|
30
|
+
// callers receive close. An error/exit/PID probe cannot settle this record.
|
|
31
|
+
if (lease)
|
|
32
|
+
proc.prependOnceListener("close", () => {
|
|
33
|
+
if (
|
|
34
|
+
proc.pid &&
|
|
35
|
+
proc.ownedProcessTreeEvidence?.cleanup?.confirmed !== true
|
|
36
|
+
)
|
|
37
|
+
return;
|
|
38
|
+
try {
|
|
39
|
+
lease.settle();
|
|
40
|
+
} catch (error) {
|
|
41
|
+
proc.emit("error", error);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
if (!proc?.ownedProcessTreeClosed || !proc.pid) return;
|
|
45
|
+
const retain = (receipt) => {
|
|
46
|
+
if (receipt?.cleanup?.confirmed !== false) return;
|
|
47
|
+
lease?.retain();
|
|
48
|
+
if (!unresolvedOwners.has(executionId)) {
|
|
49
|
+
unresolvedOwners.set(executionId, { proc, durable: lease !== null });
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
// Install before post-spawn bookkeeping and caller listeners. The error
|
|
53
|
+
// precedes cleanup:unconfirmed, so reentrant callers must already be fenced.
|
|
54
|
+
proc.on("error", (error) => {
|
|
55
|
+
if (error.code === "EXTERNAL_AGENT_CLEANUP_UNCONFIRMED")
|
|
56
|
+
retain(proc.ownedProcessTreeEvidence);
|
|
57
|
+
});
|
|
58
|
+
proc.once("cleanup:unconfirmed", retain);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getProcessOwnershipStatus() {
|
|
62
|
+
const durable = durableJournal()?.inspect();
|
|
63
|
+
return {
|
|
64
|
+
blocked: unresolvedOwners.size > 0 || durable?.blocked === true,
|
|
65
|
+
unresolvedExecutionIds: [
|
|
66
|
+
...new Set([
|
|
67
|
+
...unresolvedOwners.keys(),
|
|
68
|
+
...(durable?.blocked ? durable.pendingExecutionIds : []),
|
|
69
|
+
]),
|
|
70
|
+
],
|
|
71
|
+
recoveryRequired: unresolvedOwners.size > 0 || durable?.blocked === true,
|
|
72
|
+
durable:
|
|
73
|
+
!!durable &&
|
|
74
|
+
[...unresolvedOwners.values()].every((owner) => owner.durable),
|
|
75
|
+
restartSafe: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function assertProcessOwnershipAvailable() {
|
|
80
|
+
if (unresolvedOwners.size === 0) {
|
|
81
|
+
durableJournal()?.assertAvailable();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const error = new Error(
|
|
85
|
+
"Managed process execution is blocked because an earlier process tree has unconfirmed cleanup; restarting does not prove cleanup",
|
|
86
|
+
);
|
|
87
|
+
error.code = PROCESS_OWNERSHIP_UNCONFIRMED;
|
|
88
|
+
error.recoveryRequired = true;
|
|
89
|
+
error.executionStarted = false;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { projectUsagePricingContext } from "./usage-pricing-context.js";
|
|
2
|
+
|
|
1
3
|
const MAX_LABEL_LENGTH = 160;
|
|
2
4
|
const MAX_CALL_ID_LENGTH = 128;
|
|
3
5
|
const MAX_OPERATION_ID_LENGTH = 160;
|
|
@@ -115,6 +117,7 @@ export function projectRuntimeTokenUsage(event = {}) {
|
|
|
115
117
|
throw new TypeError("runtime token usage requires an object");
|
|
116
118
|
}
|
|
117
119
|
const usage = {};
|
|
120
|
+
Object.assign(usage, projectUsagePricingContext(event.usage));
|
|
118
121
|
for (const [index, [canonical, alias]] of USAGE_FIELDS.entries()) {
|
|
119
122
|
usage[canonical] = tokenCount(event.usage, canonical, alias, {
|
|
120
123
|
// A known settlement must prove both billable input and output counts.
|