chainlesschain 0.162.143 → 0.162.148
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 +1 -1
- package/package.json +3 -3
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-Bq1t-B4V.js → AIOps-DwpCCp64.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BHb3KMc3.js → ActionButton-C9pBYocA.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DtJrE99O.js → Analytics-BWAkeXxK.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-HXt309zh.js → AppLayout-CivFGH6B.js} +3 -3
- package/src/assets/web-panel/assets/{Audit-B-0384hX.js → Audit-Cm8hRpZm.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BXNwUJ7y.js → Backup-DXdFMsE8.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CYdSBe24.js → BaseInput-uAwSTeql.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-C5hSt-bf.js → Chat-KU8eeJJE.js} +5 -5
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DCNfbdlx.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-DZSpGJs8.js → Checkbox-C6AMDkjd.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BKfMmpq3.js → Codegen-t2moDxcO.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DgsxR9Gb.js → Col-DCcsRRhN.js} +1 -1
- package/src/assets/web-panel/assets/{Community-B3GhKOp_.js → Community-DtB-8SAC.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Dtk1xHSY.js → Compact-CZm8THYA.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DmkkUDze.js → Compliance-LiQgC7g1.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CB_Tby_i.js → Cowork-CA8SAxht.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-BLQFn3UU.js → Cron-CtRaF1wD.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-LCVSqtiL.js → Crosschain-6-br6Hub.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Cv3xor-m.js → DID-Do2EccrL.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DHAEYeUk.js → Dashboard-wrXcbzGe.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-FAp99VHO.js → Dropdown-p3FsT245.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Cl4enKIT.js → EmailListRenderer-D95A2L8q.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CgbuK_Rv.js → FamilyGuardDashboard-cLuJW2zo.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-C2I5-34o.js → Federation-Brgq_cbN.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-4RNlhf3D.js → FormItemContext-DWLCkNgh.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DmaKYzyo.js → GenericCardRenderer-BBqUfQd6.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BMrCOEGV.js → Git-8F090w4I.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BLwFR0Vd.js → Governance-CCogEbxb.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Cdlx3Y4t.js → Inference-CfLD7v7C.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-CiwGb2h3.js → KnowledgeGraph-DDuHJewd.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-C6A4r33C.js → Logs-CE8KSEVC.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-DdybH2YZ.js → Marketplace-DmwpZmhT.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-pgwwEzwg.js → McpTools-Bf7AazU8.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-BQ31MLPN.js → Memory-D0QU_00S.js} +2 -2
- package/src/assets/web-panel/assets/MobileBridge-D3Arxfeg.css +1 -0
- package/src/assets/web-panel/assets/MobileBridge-yXS9xdhx.js +1 -0
- package/src/assets/web-panel/assets/{MobileProjects-CmnCTggB.js → MobileProjects-BuDUoGUP.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-3K6RfaUB.js → Mtc-BYyHy28p.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-BN9hXvXq.js → MtcAudit-CK0aqpiZ.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-CRxIT2RV.js → Multisig-mNimKYeb.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CdPvh_dA.js → NLProgramming-AioAJ0YA.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DzQSZ0ps.js → Notes-CVVNCLHE.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-DkqRVvfb.js → NotificationSettings-C4ABj-TK.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-ClTA64ji.js → OrderTableRenderer-DIp8Xcbd.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-B-Pndhhn.js → Organization-uozl_gWK.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Dt56UM9b.js → Overflow-BCZOM2hK.js} +1 -1
- package/src/assets/web-panel/assets/{OverrideContext-C2r4vyBb.js → OverrideContext-BlgL9440.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-JDGQmED0.js → P2P-CArcaiaj.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-Sgq2Srr8.js +7 -0
- package/src/assets/web-panel/assets/{Permissions-Dnqe1sny.js → Permissions-BYrQrXnH.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-mTkYbIR-.js → PersonalDataHub-BzHStAbz.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-BG6J9UZO.js → Pipeline-k7KqxX1M.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BdEydH6w.js → Privacy-IumTUWqx.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-QPxCoUzs.js → ProjectInit-BRyImYTf.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-hU_phubM.js → ProjectSettings-BFIqTBFk.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BBzAEGk4.js → Projects-Dpid9CDg.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-DJ_bIDRG.js → Providers-CyyFnUPs.js} +1 -1
- package/src/assets/web-panel/assets/QrScannerModal-C82cRx-X.js +1 -0
- package/src/assets/web-panel/assets/{MobileBridge-zJpPuf4E.css → QrScannerModal-NwAHNfiJ.css} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BNmX7Jgh.js → QuickAsk-DGxDF521.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Bo1I5nAG.js → Recommend-Cns-Am1y.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-R7OqAIlg.js +4 -0
- package/src/assets/web-panel/assets/{Reputation-Bc9mfsh7.js → Reputation-D7mgPIYV.js} +1 -1
- package/src/assets/web-panel/assets/{Row-D12ZZx5m.js → Row-B3lEURyd.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CXSlmgHl.js → RssFeed-9_UVrpBt.js} +2 -2
- package/src/assets/web-panel/assets/{Search-bNXObahl.js → Search-ClZeO80q.js} +1 -1
- package/src/assets/web-panel/assets/{Security-Bt7yBkJc.js → Security-BNNJczEp.js} +3 -3
- package/src/assets/web-panel/assets/{Services-ECQDydWI.js → Services-C5SjrWUj.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BcgzLbTM.js → Skeleton-Ci_obQ-g.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-py_xMG8b.js → Skills-DdebN15P.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CzNvjwaW.js → Sla-OinZP2vi.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-S3zMMqbB.js → SpeechSettings-CxzbNF51.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BpDfhFv6.js → SyncSettings-D06N_66b.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-CUn36tpM.js → Tasks-BdEdW0AZ.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-yzCxc3lI.js → Templates-F1ctKmPa.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BTMrYsHv.js → Tenant-CVz1Urot.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-BvAK_Zel.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-BwJ4nL9S.js → TimelineRenderer-Doitzjmf.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-B1OwTaT_.js → Tokens-BpyVRpVA.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BDxi9fib.js → Trigger-nWhWYTbU.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-B_5hXqzI.js → Trust-DK_G-wLx.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BpIMO611.js → UkeySign-B5yBUojO.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-Ds-9lxOW.js → VideoEditing-C5D51qAe.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CNfFp8if.js → Wallet-CLbL9K7v.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DbCj1-qn.js → WebAuthn-DZUelI3V.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-DjEeb957.js → WorkflowEditor-8RPxt4KW.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BX-27gI4.js → chat-4N4tOA3d.js} +1 -1
- package/src/assets/web-panel/assets/{collapseMotion-mxzzcDsg.js → collapseMotion-DVfhbsdP.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CfM5FmdA.js → colors-zbbGVrua.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-Cb0Fm64g.js → compact-item-D7iMkbnE.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-D99MZ4gz.js → createContext-CBzPJv-w.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-C2Z5LRgq.js +1 -0
- package/src/assets/web-panel/assets/{echarts-jxgIOFUZ.js → echarts-CL0F2SnX.js} +1 -1
- package/src/assets/web-panel/assets/{hasIn-CW7wPkfb.js → hasIn-bHdrQSqZ.js} +1 -1
- package/src/assets/web-panel/assets/{icons-DP3uiYxy.js → icons-BsDmGpcT.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcCSEGzu.js → index-308gCGh7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CXrNkD5j.js → index-B1s0Bz9O.js} +1 -1
- package/src/assets/web-panel/assets/{index-CxG5S21O.js → index-B7b1hGry.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoCp5SY7.js → index-BBNr77E1.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dt-h8idq.js → index-BCr0geV9.js} +1 -1
- package/src/assets/web-panel/assets/index-BGp6Yr-Y.js +1 -0
- package/src/assets/web-panel/assets/{index-Bf7G5mYl.js → index-BJ4ZGyW0.js} +1 -1
- package/src/assets/web-panel/assets/{index-rIKv98lY.js → index-BS4_Mwk6.js} +1 -1
- package/src/assets/web-panel/assets/{index-ClSExghA.js → index-BTIjjXry.js} +1 -1
- package/src/assets/web-panel/assets/index-BXiw9dQf.js +1 -0
- package/src/assets/web-panel/assets/{index-CzwoBX8m.js → index-BZb8F8YG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CUENEaPU.js → index-BnBOpqv3.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZjN-U7A.js → index-Bqmx24kh.js} +1 -1
- package/src/assets/web-panel/assets/{index-ab4T5cPr.js → index-BsKehmmS.js} +1 -1
- package/src/assets/web-panel/assets/{index-DDGBasj6.js → index-BtbbdLnf.js} +4 -4
- package/src/assets/web-panel/assets/{index-CvFAgMxx.js → index-CPxkoKgA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BqOy4TS5.js → index-CQ6NcfWz.js} +1 -1
- package/src/assets/web-panel/assets/{index--Y8IzVi2.js → index-CQfu1U78.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcRbrdqR.js → index-CahryTX0.js} +1 -1
- package/src/assets/web-panel/assets/{index-ChGWrUQv.js → index-CdmMoYN1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BTlOIaQj.js → index-Ci009ijp.js} +1 -1
- package/src/assets/web-panel/assets/{index-CNhZanpM.js → index-CiG1IZao.js} +1 -1
- package/src/assets/web-panel/assets/{index-kRlIAKNw.js → index-CkxK86vf.js} +1 -1
- package/src/assets/web-panel/assets/{index-DAd31-6W.js → index-CqdPyWm5.js} +1 -1
- package/src/assets/web-panel/assets/{index-kt0S8nXp.js → index-CvRMA9uT.js} +1 -1
- package/src/assets/web-panel/assets/{index-CxFhtfoo.js → index-D1YDh7Wr.js} +1 -1
- package/src/assets/web-panel/assets/{index-7cahuPKG.js → index-D4XKpj6c.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cavk4P2D.js → index-DE9j5YgT.js} +1 -1
- package/src/assets/web-panel/assets/{index-DUxnWbi6.js → index-DISWAYce.js} +1 -1
- package/src/assets/web-panel/assets/{index-CPmtR1Wr.js → index-DUyjUkY7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DFRrP3LL.js → index-DbE5D0WL.js} +1 -1
- package/src/assets/web-panel/assets/{index-DAIk483F.js → index-DmqlJed-.js} +1 -1
- package/src/assets/web-panel/assets/{index-yBiqcb9Z.js → index-DyoNgbXl.js} +1 -1
- package/src/assets/web-panel/assets/{index-DwhDhfZ8.js → index-QFObYeo1.js} +1 -1
- package/src/assets/web-panel/assets/{index-B8tI4Hg_.js → index-QNAG4JtR.js} +1 -1
- package/src/assets/web-panel/assets/{index-BPXhpO02.js → index-S74FpAIn.js} +1 -1
- package/src/assets/web-panel/assets/{index-CaPluhcX.js → index-kmh1TxRa.js} +1 -1
- package/src/assets/web-panel/assets/{index-3TDpYBKK.js → index-oDKNgCsP.js} +1 -1
- package/src/assets/web-panel/assets/{index-D8U1HFcm.js → index-xYCm6W2h.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-mzQV01we.js → initDefaultProps-BD55yNBt.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DgWAtKps.js → motion-B5Bbe77U.js} +1 -1
- package/src/assets/web-panel/assets/{move-Dm9NMjIw.js → move-CTvIqHEH.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BpuQIoKH.js → mtc-parser-BkNyPQKB.js} +1 -1
- package/src/assets/web-panel/assets/{omit-C507FdFH.js → omit-BaXJXgHA.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BL5eykDR.js → pickAttrs-Bw6-YTxH.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-uSE1U6yg.js → placementArrow-BLdbkLqJ.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-BVhSz_U1.js → responsiveObserve-IVYkzntU.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CWCp5XZc.js → slide-DFMFwwtY.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-B_rS9tlX.js → statusUtils-CURYRtZ1.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DRo4TNq4.js → styleChecker-Cfz63s1r.js} +1 -1
- package/src/assets/web-panel/assets/useFlexGapSupport-COJL0KE3.js +1 -0
- package/src/assets/web-panel/assets/{useFs-DhsLsCr_.js → useFs-YLLojQQf.js} +1 -1
- package/src/assets/web-panel/assets/{useMergedState-Cl2q1fes.js → useMergedState-Dc9sO2d7.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BXI-FLwS.js → usePersonalDataHub-BF_21qUC.js} +1 -1
- package/src/assets/web-panel/assets/{useRefs-DD9q4mOT.js → useRefs-CAeFRzPf.js} +1 -1
- package/src/assets/web-panel/assets/{useState-BK4hy8Ma.js → useState-LwExUYDO.js} +1 -1
- package/src/assets/web-panel/assets/{vendor-BvqAck49.js → vendor-BMxUs6UX.js} +16 -16
- package/src/assets/web-panel/assets/{vnode-CYE0CVkI.js → vnode-9ZpkA7bb.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-B2-MtXVF.js → zoom-DhtAfhl8.js} +1 -1
- package/src/assets/web-panel/index.html +3 -3
- package/src/auth/npm-auth.js +65 -0
- package/src/commands/agent.js +92 -0
- package/src/commands/agents.js +68 -0
- package/src/commands/permissions.js +66 -10
- package/src/commands/plugin.js +50 -3
- package/src/commands/serve.js +10 -0
- package/src/gateways/remote-session-relay.js +160 -0
- package/src/gateways/ws/message-dispatcher.js +32 -0
- package/src/gateways/ws/remote-session-protocol.js +429 -0
- package/src/gateways/ws/ws-server.js +299 -0
- package/src/harness/golden-transcript.js +65 -0
- package/src/harness/mcp-client.js +6 -2
- package/src/harness/remote-session-audit-sink.js +132 -0
- package/src/harness/remote-session-audit.js +110 -0
- package/src/harness/remote-session-crypto.js +217 -0
- package/src/harness/remote-session-push-fcm.js +254 -0
- package/src/harness/remote-session-push.js +104 -0
- package/src/harness/remote-session-registry.js +420 -0
- package/src/lib/agent-sandbox.js +72 -0
- package/src/lib/background-agent-supervisor.js +197 -0
- package/src/lib/code-review.js +72 -0
- package/src/lib/did-manager.js +6 -4
- package/src/lib/mcp-oauth.js +23 -6
- package/src/lib/plugin-security.js +128 -0
- package/src/lib/pr-create.js +108 -0
- package/src/lib/publish-workspace.js +107 -0
- package/src/lib/resume-session.js +111 -0
- package/src/lib/session-manager.js +7 -3
- package/src/lib/settings-hooks.cjs +68 -25
- package/src/lib/settings-loader.cjs +115 -2
- package/src/lib/task-list.js +53 -0
- package/src/repl/agent-repl.js +83 -2
- package/src/repl/agents-status.js +8 -2
- package/src/repl/btw-command.js +56 -0
- package/src/repl/chat-repl.js +52 -1
- package/src/repl/hooks-status.js +1 -2
- package/src/repl/slash-command-registry.js +197 -0
- package/src/runtime/agent-core.js +137 -1
- package/src/runtime/agent-runtime.js +8 -13
- package/src/runtime/coding-agent-contract-shared.cjs +37 -0
- package/src/runtime/coding-agent-policy.cjs +14 -0
- package/src/runtime/headless-runner.js +26 -6
- package/src/runtime/headless-stream.js +25 -6
- package/src/runtime/mcp-config.js +46 -2
- package/src/runtime/policies/agent-policy.js +3 -0
- package/src/workers/background-agent-worker.js +60 -0
- package/src/assets/web-panel/assets/ChatBubbleRenderer-BJQ6SG49.js +0 -1
- package/src/assets/web-panel/assets/MobileBridge-CnRXuSP5.js +0 -1
- package/src/assets/web-panel/assets/PdhVaultBrowser-C56Oi811.js +0 -7
- package/src/assets/web-panel/assets/Terminal-BjZry1Jj.js +0 -3
- package/src/assets/web-panel/assets/devWarning-CDh1y3xt.js +0 -1
- package/src/assets/web-panel/assets/index-ACeVMo7x.js +0 -1
- package/src/assets/web-panel/assets/index-gYKDzWA9.js +0 -1
- package/src/assets/web-panel/assets/useFlexGapSupport-QsPzwpvi.js +0 -1
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "crypto";
|
|
2
|
+
|
|
3
|
+
export const REMOTE_SESSION_PROTOCOL_VERSION = "1.0";
|
|
4
|
+
export const REMOTE_SESSION_SCOPES = Object.freeze([
|
|
5
|
+
"observe",
|
|
6
|
+
"prompt",
|
|
7
|
+
"approve",
|
|
8
|
+
"interrupt",
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1000;
|
|
12
|
+
const DEFAULT_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
function hashToken(token) {
|
|
15
|
+
return createHash("sha256").update(String(token), "utf8").digest();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function tokensMatch(token, digest) {
|
|
19
|
+
const candidate = hashToken(token);
|
|
20
|
+
return (
|
|
21
|
+
candidate.length === digest.length && timingSafeEqual(candidate, digest)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeScopes(scopes) {
|
|
26
|
+
const requested = scopes || REMOTE_SESSION_SCOPES;
|
|
27
|
+
if (!Array.isArray(requested) || requested.length === 0) {
|
|
28
|
+
throw new TypeError("Remote session scopes must be a non-empty array");
|
|
29
|
+
}
|
|
30
|
+
const unique = [...new Set(requested)];
|
|
31
|
+
for (const scope of unique) {
|
|
32
|
+
if (!REMOTE_SESSION_SCOPES.includes(scope)) {
|
|
33
|
+
throw new Error(`Unsupported remote session scope: ${scope}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return unique;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Org-level constraints an administrator can impose on Remote Sessions. Lives
|
|
41
|
+
* next to the registry (its single enforcement point) so both the direct WS
|
|
42
|
+
* join and the relay pairing path go through the same checks. Permissive by
|
|
43
|
+
* default — an unconfigured policy is a no-op.
|
|
44
|
+
*/
|
|
45
|
+
export class RemoteSessionPolicy {
|
|
46
|
+
constructor({
|
|
47
|
+
allowedScopes = null,
|
|
48
|
+
maxDevices = null,
|
|
49
|
+
maxSessionTtlMs = null,
|
|
50
|
+
maxTokenTtlMs = null,
|
|
51
|
+
allowRelayPairing = true,
|
|
52
|
+
} = {}) {
|
|
53
|
+
this.allowedScopes = allowedScopes ? [...new Set(allowedScopes)] : null;
|
|
54
|
+
if (this.allowedScopes) {
|
|
55
|
+
if (this.allowedScopes.length === 0) {
|
|
56
|
+
throw new Error("allowedScopes cannot be empty");
|
|
57
|
+
}
|
|
58
|
+
for (const scope of this.allowedScopes) {
|
|
59
|
+
if (!REMOTE_SESSION_SCOPES.includes(scope)) {
|
|
60
|
+
throw new Error(`Unknown scope in org policy: ${scope}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
this.maxDevices =
|
|
65
|
+
maxDevices == null ? null : Math.max(0, Math.floor(maxDevices));
|
|
66
|
+
this.maxSessionTtlMs =
|
|
67
|
+
maxSessionTtlMs == null ? null : Math.max(1, Math.floor(maxSessionTtlMs));
|
|
68
|
+
this.maxTokenTtlMs =
|
|
69
|
+
maxTokenTtlMs == null ? null : Math.max(1, Math.floor(maxTokenTtlMs));
|
|
70
|
+
this.allowRelayPairing = allowRelayPairing !== false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Narrow requested scopes to the org-allowed set. Throws only when the request
|
|
75
|
+
* and the allow-list are wholly disjoint (i.e. nothing could be granted).
|
|
76
|
+
*/
|
|
77
|
+
applyScopes(requestedScopes) {
|
|
78
|
+
if (!this.allowedScopes) {
|
|
79
|
+
return { scopes: requestedScopes || null, narrowed: false };
|
|
80
|
+
}
|
|
81
|
+
const requested =
|
|
82
|
+
requestedScopes && requestedScopes.length
|
|
83
|
+
? [...new Set(requestedScopes)]
|
|
84
|
+
: [...REMOTE_SESSION_SCOPES];
|
|
85
|
+
const granted = requested.filter((scope) =>
|
|
86
|
+
this.allowedScopes.includes(scope),
|
|
87
|
+
);
|
|
88
|
+
if (granted.length === 0) {
|
|
89
|
+
throw new Error("Remote session scopes are not permitted by org policy");
|
|
90
|
+
}
|
|
91
|
+
return { scopes: granted, narrowed: granted.length !== requested.length };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
capSessionTtl(ttlMs) {
|
|
95
|
+
return this.maxSessionTtlMs == null
|
|
96
|
+
? ttlMs
|
|
97
|
+
: Math.min(ttlMs, this.maxSessionTtlMs);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
capTokenTtl(ttlMs) {
|
|
101
|
+
return this.maxTokenTtlMs == null
|
|
102
|
+
? ttlMs
|
|
103
|
+
: Math.min(ttlMs, this.maxTokenTtlMs);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
enforceJoin({ deviceCount, via } = {}) {
|
|
107
|
+
if (via === "relay" && !this.allowRelayPairing) {
|
|
108
|
+
throw new Error("Relay pairing is disabled by org policy");
|
|
109
|
+
}
|
|
110
|
+
if (this.maxDevices != null && deviceCount >= this.maxDevices) {
|
|
111
|
+
throw new Error("Org policy device limit reached");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
describe() {
|
|
116
|
+
return {
|
|
117
|
+
allowedScopes: this.allowedScopes,
|
|
118
|
+
maxDevices: this.maxDevices,
|
|
119
|
+
maxSessionTtlMs: this.maxSessionTtlMs,
|
|
120
|
+
maxTokenTtlMs: this.maxTokenTtlMs,
|
|
121
|
+
allowRelayPairing: this.allowRelayPairing,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
static fromEnv(env = {}) {
|
|
126
|
+
const options = {};
|
|
127
|
+
const scopes = env.CHAINLESSCHAIN_REMOTE_SESSION_ALLOWED_SCOPES;
|
|
128
|
+
if (scopes) {
|
|
129
|
+
options.allowedScopes = scopes
|
|
130
|
+
.split(",")
|
|
131
|
+
.map((scope) => scope.trim())
|
|
132
|
+
.filter(Boolean);
|
|
133
|
+
}
|
|
134
|
+
const num = (value) => {
|
|
135
|
+
const parsed = Number(value);
|
|
136
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
137
|
+
};
|
|
138
|
+
if (env.CHAINLESSCHAIN_REMOTE_SESSION_MAX_DEVICES != null) {
|
|
139
|
+
options.maxDevices = num(env.CHAINLESSCHAIN_REMOTE_SESSION_MAX_DEVICES);
|
|
140
|
+
}
|
|
141
|
+
if (env.CHAINLESSCHAIN_REMOTE_SESSION_MAX_SESSION_TTL_MS != null) {
|
|
142
|
+
options.maxSessionTtlMs = num(
|
|
143
|
+
env.CHAINLESSCHAIN_REMOTE_SESSION_MAX_SESSION_TTL_MS,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (env.CHAINLESSCHAIN_REMOTE_SESSION_MAX_TOKEN_TTL_MS != null) {
|
|
147
|
+
options.maxTokenTtlMs = num(
|
|
148
|
+
env.CHAINLESSCHAIN_REMOTE_SESSION_MAX_TOKEN_TTL_MS,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (env.CHAINLESSCHAIN_REMOTE_SESSION_ALLOW_RELAY != null) {
|
|
152
|
+
options.allowRelayPairing = !/^(0|false|no|off)$/i.test(
|
|
153
|
+
String(env.CHAINLESSCHAIN_REMOTE_SESSION_ALLOW_RELAY).trim(),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return new RemoteSessionPolicy(options);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function publicSession(session) {
|
|
161
|
+
return {
|
|
162
|
+
protocolVersion: REMOTE_SESSION_PROTOCOL_VERSION,
|
|
163
|
+
sessionId: session.sessionId,
|
|
164
|
+
agentSessionId: session.agentSessionId,
|
|
165
|
+
name: session.name,
|
|
166
|
+
hostClientId: session.hostClientId,
|
|
167
|
+
createdAt: session.createdAt,
|
|
168
|
+
expiresAt: session.expiresAt,
|
|
169
|
+
memberCount: session.members.size,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** In-memory authorization state for one local CLI process. */
|
|
174
|
+
export class RemoteSessionRegistry {
|
|
175
|
+
constructor(options = {}) {
|
|
176
|
+
this.now = options.now || Date.now;
|
|
177
|
+
this.tokenTtlMs = options.tokenTtlMs || DEFAULT_TOKEN_TTL_MS;
|
|
178
|
+
this.sessionTtlMs = options.sessionTtlMs || DEFAULT_SESSION_TTL_MS;
|
|
179
|
+
this.policy = options.policy || new RemoteSessionPolicy();
|
|
180
|
+
this.sessions = new Map();
|
|
181
|
+
this.tokens = new Map();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
create({ hostClientId, agentSessionId, name, scopes } = {}) {
|
|
185
|
+
if (!hostClientId) throw new Error("hostClientId is required");
|
|
186
|
+
if (!agentSessionId) throw new Error("agentSessionId is required");
|
|
187
|
+
const now = this.now();
|
|
188
|
+
const session = {
|
|
189
|
+
sessionId: randomUUID(),
|
|
190
|
+
agentSessionId,
|
|
191
|
+
name: name || agentSessionId,
|
|
192
|
+
hostClientId,
|
|
193
|
+
createdAt: now,
|
|
194
|
+
expiresAt: now + this.policy.capSessionTtl(this.sessionTtlMs),
|
|
195
|
+
members: new Map([
|
|
196
|
+
[
|
|
197
|
+
hostClientId,
|
|
198
|
+
{
|
|
199
|
+
clientId: hostClientId,
|
|
200
|
+
scopes: REMOTE_SESSION_SCOPES,
|
|
201
|
+
joinedAt: now,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
]),
|
|
205
|
+
};
|
|
206
|
+
this.sessions.set(session.sessionId, session);
|
|
207
|
+
return {
|
|
208
|
+
session: publicSession(session),
|
|
209
|
+
pairing: this.issuePairingToken(session.sessionId, { scopes }),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
issuePairingToken(sessionId, { scopes } = {}) {
|
|
214
|
+
const session = this.requireSession(sessionId);
|
|
215
|
+
const token = randomBytes(32).toString("base64url");
|
|
216
|
+
const now = this.now();
|
|
217
|
+
const { scopes: applied, narrowed } = this.policy.applyScopes(scopes);
|
|
218
|
+
const grantedScopes = normalizeScopes(applied);
|
|
219
|
+
this.tokens.set(sessionId, {
|
|
220
|
+
digest: hashToken(token),
|
|
221
|
+
scopes: grantedScopes,
|
|
222
|
+
createdAt: now,
|
|
223
|
+
expiresAt: Math.min(
|
|
224
|
+
now + this.policy.capTokenTtl(this.tokenTtlMs),
|
|
225
|
+
session.expiresAt,
|
|
226
|
+
),
|
|
227
|
+
});
|
|
228
|
+
return {
|
|
229
|
+
token,
|
|
230
|
+
expiresAt: this.tokens.get(sessionId).expiresAt,
|
|
231
|
+
scopes: grantedScopes,
|
|
232
|
+
policyNarrowed: narrowed,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
join({
|
|
237
|
+
sessionId,
|
|
238
|
+
clientId,
|
|
239
|
+
token,
|
|
240
|
+
via = "direct",
|
|
241
|
+
pushToken = null,
|
|
242
|
+
pushProvider = null,
|
|
243
|
+
} = {}) {
|
|
244
|
+
if (!clientId) throw new Error("clientId is required");
|
|
245
|
+
const session = this.requireSession(sessionId);
|
|
246
|
+
const pairing = this.tokens.get(sessionId);
|
|
247
|
+
if (!pairing || pairing.expiresAt <= this.now()) {
|
|
248
|
+
this.tokens.delete(sessionId);
|
|
249
|
+
throw new Error("Pairing token is missing or expired");
|
|
250
|
+
}
|
|
251
|
+
if (!tokensMatch(token, pairing.digest)) {
|
|
252
|
+
throw new Error("Invalid pairing token");
|
|
253
|
+
}
|
|
254
|
+
// Org policy is enforced only after the token proves the caller is invited,
|
|
255
|
+
// so a device-limit / relay-disabled message never leaks to unauthenticated
|
|
256
|
+
// probes. Existing (non-host) devices count against the limit.
|
|
257
|
+
const deviceCount = [...session.members.values()].filter(
|
|
258
|
+
(member) => member.clientId !== session.hostClientId,
|
|
259
|
+
).length;
|
|
260
|
+
this.policy.enforceJoin({ deviceCount, via });
|
|
261
|
+
// Pairing credentials are deliberately one-time. The host must explicitly
|
|
262
|
+
// issue another token for each additional device.
|
|
263
|
+
this.tokens.delete(sessionId);
|
|
264
|
+
const member = {
|
|
265
|
+
clientId,
|
|
266
|
+
scopes: pairing.scopes,
|
|
267
|
+
joinedAt: this.now(),
|
|
268
|
+
pushToken: pushToken || null,
|
|
269
|
+
pushProvider: pushToken ? pushProvider || null : null,
|
|
270
|
+
};
|
|
271
|
+
session.members.set(clientId, member);
|
|
272
|
+
return { session: publicSession(session), member: { ...member } };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* A device registers (or refreshes / clears) its own vendor push token after
|
|
277
|
+
* pairing — e.g. once FCM assigns one. Only an existing member may set its own
|
|
278
|
+
* token. A null token clears push for that device.
|
|
279
|
+
*/
|
|
280
|
+
registerPush(sessionId, clientId, { token = null, provider = null } = {}) {
|
|
281
|
+
const session = this.requireSession(sessionId);
|
|
282
|
+
const member = session.members.get(clientId);
|
|
283
|
+
if (!member) {
|
|
284
|
+
throw new Error("Device is not paired with this remote session");
|
|
285
|
+
}
|
|
286
|
+
member.pushToken = token || null;
|
|
287
|
+
member.pushProvider = token ? provider || null : null;
|
|
288
|
+
return {
|
|
289
|
+
clientId,
|
|
290
|
+
hasPush: Boolean(member.pushToken),
|
|
291
|
+
provider: member.pushProvider,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Non-host members that carry a push token, for wake-up dispatch. */
|
|
296
|
+
pushTargets(sessionId, { excludeClientId } = {}) {
|
|
297
|
+
const session = this.requireSession(sessionId);
|
|
298
|
+
const targets = [];
|
|
299
|
+
for (const member of session.members.values()) {
|
|
300
|
+
if (member.clientId === session.hostClientId) continue;
|
|
301
|
+
if (member.clientId === excludeClientId) continue;
|
|
302
|
+
if (!member.pushToken) continue;
|
|
303
|
+
targets.push({
|
|
304
|
+
clientId: member.clientId,
|
|
305
|
+
pushToken: member.pushToken,
|
|
306
|
+
pushProvider: member.pushProvider,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return targets;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
authorize(sessionId, clientId, scope) {
|
|
313
|
+
const session = this.requireSession(sessionId);
|
|
314
|
+
const member = session.members.get(clientId);
|
|
315
|
+
if (!member)
|
|
316
|
+
throw new Error("Client is not paired with this remote session");
|
|
317
|
+
if (!member.scopes.includes(scope)) {
|
|
318
|
+
throw new Error(`Remote session scope required: ${scope}`);
|
|
319
|
+
}
|
|
320
|
+
return { session, member };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
members(sessionId) {
|
|
324
|
+
const session = this.requireSession(sessionId);
|
|
325
|
+
return [...session.members.values()].map((member) => ({ ...member }));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Host-only view of every paired device on a session. The host flag lets the
|
|
330
|
+
* UI keep the host row un-revocable and label it distinctly.
|
|
331
|
+
*/
|
|
332
|
+
listDevices(sessionId, hostClientId) {
|
|
333
|
+
const session = this.requireSession(sessionId);
|
|
334
|
+
if (hostClientId && session.hostClientId !== hostClientId) {
|
|
335
|
+
throw new Error("Only the host can list paired devices");
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
session: publicSession(session),
|
|
339
|
+
devices: [...session.members.values()].map((member) => ({
|
|
340
|
+
clientId: member.clientId,
|
|
341
|
+
scopes: [...member.scopes],
|
|
342
|
+
joinedAt: member.joinedAt,
|
|
343
|
+
isHost: member.clientId === session.hostClientId,
|
|
344
|
+
hasPush: Boolean(member.pushToken),
|
|
345
|
+
pushProvider: member.pushProvider || null,
|
|
346
|
+
})),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Host-initiated revocation of a single paired device. The host cannot revoke
|
|
352
|
+
* itself (use close() to end the whole session). Returns the removed member so
|
|
353
|
+
* callers can push a revocation notice to that device.
|
|
354
|
+
*/
|
|
355
|
+
revokeMember(sessionId, hostClientId, clientId) {
|
|
356
|
+
if (!clientId) throw new Error("clientId is required");
|
|
357
|
+
const session = this.requireSession(sessionId);
|
|
358
|
+
if (session.hostClientId !== hostClientId) {
|
|
359
|
+
throw new Error("Only the host can revoke devices");
|
|
360
|
+
}
|
|
361
|
+
if (clientId === session.hostClientId) {
|
|
362
|
+
throw new Error("Cannot revoke the host device");
|
|
363
|
+
}
|
|
364
|
+
const member = session.members.get(clientId);
|
|
365
|
+
if (!member)
|
|
366
|
+
throw new Error("Device is not paired with this remote session");
|
|
367
|
+
session.members.delete(clientId);
|
|
368
|
+
return { session: publicSession(session), member: { ...member } };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
findHosted(agentSessionId, hostClientId) {
|
|
372
|
+
const matches = [];
|
|
373
|
+
for (const session of this.sessions.values()) {
|
|
374
|
+
if (
|
|
375
|
+
session.agentSessionId === agentSessionId &&
|
|
376
|
+
session.hostClientId === hostClientId &&
|
|
377
|
+
session.expiresAt > this.now()
|
|
378
|
+
) {
|
|
379
|
+
matches.push(session);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return matches;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
removeClient(clientId) {
|
|
386
|
+
const affected = [];
|
|
387
|
+
for (const [sessionId, session] of this.sessions) {
|
|
388
|
+
if (!session.members.delete(clientId)) continue;
|
|
389
|
+
if (session.hostClientId === clientId) {
|
|
390
|
+
this.sessions.delete(sessionId);
|
|
391
|
+
this.tokens.delete(sessionId);
|
|
392
|
+
affected.push({ sessionId, closed: true });
|
|
393
|
+
} else {
|
|
394
|
+
affected.push({ sessionId, closed: false });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return affected;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
close(sessionId, clientId) {
|
|
401
|
+
const session = this.requireSession(sessionId);
|
|
402
|
+
if (session.hostClientId !== clientId) {
|
|
403
|
+
throw new Error("Only the host can close a Remote Session");
|
|
404
|
+
}
|
|
405
|
+
this.sessions.delete(sessionId);
|
|
406
|
+
this.tokens.delete(sessionId);
|
|
407
|
+
return publicSession(session);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
requireSession(sessionId) {
|
|
411
|
+
const session = this.sessions.get(sessionId);
|
|
412
|
+
if (!session) throw new Error("Remote session not found");
|
|
413
|
+
if (session.expiresAt <= this.now()) {
|
|
414
|
+
this.sessions.delete(sessionId);
|
|
415
|
+
this.tokens.delete(sessionId);
|
|
416
|
+
throw new Error("Remote session expired");
|
|
417
|
+
}
|
|
418
|
+
return session;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** OS-isolated shell execution for the coding agent. */
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_SANDBOX_IMAGE = "node:22-bookworm-slim";
|
|
6
|
+
export const _deps = { spawnSync };
|
|
7
|
+
|
|
8
|
+
export function normalizeAgentSandbox(value, options = {}) {
|
|
9
|
+
if (!value) return null;
|
|
10
|
+
const image =
|
|
11
|
+
typeof value === "string" && value.trim() && value !== "true"
|
|
12
|
+
? value.trim()
|
|
13
|
+
: DEFAULT_SANDBOX_IMAGE;
|
|
14
|
+
return {
|
|
15
|
+
engine: "docker",
|
|
16
|
+
image,
|
|
17
|
+
cwd: path.resolve(options.cwd || process.cwd()),
|
|
18
|
+
network: options.network === true,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function executeSandboxedShell(command, sandbox, options = {}) {
|
|
23
|
+
if (!sandbox || sandbox.engine !== "docker") {
|
|
24
|
+
throw new Error("A supported agent sandbox configuration is required");
|
|
25
|
+
}
|
|
26
|
+
const hostCwd = path.resolve(options.cwd || sandbox.cwd);
|
|
27
|
+
const args = ["run", "--rm", "--init"];
|
|
28
|
+
if (!sandbox.network) args.push("--network", "none");
|
|
29
|
+
args.push("--mount", `type=bind,source=${hostCwd},target=/workspace`);
|
|
30
|
+
args.push("--workdir", "/workspace");
|
|
31
|
+
if (process.platform !== "win32" && process.getuid && process.getgid) {
|
|
32
|
+
args.push("--user", `${process.getuid()}:${process.getgid()}`);
|
|
33
|
+
}
|
|
34
|
+
const env = options.env || {};
|
|
35
|
+
for (const key of ["CLAUDECODE", "CC_SESSION_ID", "CLAUDE_CODE_SESSION_ID"]) {
|
|
36
|
+
if (env[key] != null) args.push("--env", `${key}=${env[key]}`);
|
|
37
|
+
}
|
|
38
|
+
args.push(sandbox.image, "sh", "-lc", String(command || ""));
|
|
39
|
+
const result = _deps.spawnSync("docker", args, {
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
timeout: options.timeout,
|
|
42
|
+
maxBuffer: options.maxBuffer,
|
|
43
|
+
windowsHide: true,
|
|
44
|
+
});
|
|
45
|
+
if (result.error) {
|
|
46
|
+
return {
|
|
47
|
+
stdout: result.stdout || "",
|
|
48
|
+
stderr:
|
|
49
|
+
result.error.code === "ENOENT"
|
|
50
|
+
? "Docker is not installed"
|
|
51
|
+
: result.error.message,
|
|
52
|
+
exitCode: typeof result.status === "number" ? result.status : 1,
|
|
53
|
+
failedToStart: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
stdout: result.stdout || "",
|
|
58
|
+
stderr: result.stderr || "",
|
|
59
|
+
exitCode: typeof result.status === "number" ? result.status : 1,
|
|
60
|
+
signal: result.signal || null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function sandboxSummary(sandbox) {
|
|
65
|
+
if (!sandbox) return null;
|
|
66
|
+
return {
|
|
67
|
+
engine: sandbox.engine,
|
|
68
|
+
image: sandbox.image,
|
|
69
|
+
network: sandbox.network ? "enabled" : "disabled",
|
|
70
|
+
workspace: "read-write",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
openSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { randomBytes } from "node:crypto";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { getHomeDir } from "./paths.js";
|
|
17
|
+
|
|
18
|
+
export const _deps = { spawn, spawnSync };
|
|
19
|
+
|
|
20
|
+
export function backgroundAgentsDir() {
|
|
21
|
+
const dir =
|
|
22
|
+
process.env.CC_BACKGROUND_AGENTS_DIR ||
|
|
23
|
+
join(getHomeDir(), "background-agents");
|
|
24
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createBackgroundAgentId() {
|
|
29
|
+
return `bg-${Date.now()}-${randomBytes(3).toString("hex")}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function safeId(id) {
|
|
33
|
+
if (!/^bg-[a-zA-Z0-9-]+$/.test(String(id || ""))) {
|
|
34
|
+
throw new Error(`Invalid background agent id: ${id}`);
|
|
35
|
+
}
|
|
36
|
+
return String(id);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function statePath(id) {
|
|
40
|
+
return join(backgroundAgentsDir(), `${safeId(id)}.json`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function logPath(id) {
|
|
44
|
+
return join(backgroundAgentsDir(), `${safeId(id)}.log`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function writeBackgroundAgentState(state) {
|
|
48
|
+
const target = statePath(state.id);
|
|
49
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
50
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
51
|
+
renameSync(tmp, target);
|
|
52
|
+
return state;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function readBackgroundAgentState(id) {
|
|
56
|
+
const file = statePath(id);
|
|
57
|
+
if (!existsSync(file)) return null;
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isProcessAlive(pid) {
|
|
66
|
+
if (!Number.isInteger(Number(pid)) || Number(pid) <= 0) return false;
|
|
67
|
+
try {
|
|
68
|
+
process.kill(Number(pid), 0);
|
|
69
|
+
return true;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error?.code === "EPERM") return true;
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function effectiveBackgroundAgentState(state) {
|
|
77
|
+
if (!state) return null;
|
|
78
|
+
if (state.status === "running" && !isProcessAlive(state.pid)) {
|
|
79
|
+
return { ...state, status: "lost", endedAt: state.endedAt || Date.now() };
|
|
80
|
+
}
|
|
81
|
+
return state;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function listBackgroundAgents(options = {}) {
|
|
85
|
+
return readdirSync(backgroundAgentsDir())
|
|
86
|
+
.filter((name) => name.endsWith(".json") && !name.includes(".job."))
|
|
87
|
+
.map((name) => readBackgroundAgentState(name.slice(0, -5)))
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
.map(effectiveBackgroundAgentState)
|
|
90
|
+
.filter((state) => options.all || state.status === "running")
|
|
91
|
+
.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function launchBackgroundAgent({
|
|
95
|
+
argv,
|
|
96
|
+
cwd,
|
|
97
|
+
sessionId,
|
|
98
|
+
title,
|
|
99
|
+
cliEntry,
|
|
100
|
+
}) {
|
|
101
|
+
const id = createBackgroundAgentId();
|
|
102
|
+
const dir = backgroundAgentsDir();
|
|
103
|
+
const jobFile = join(dir, `${id}.job.${process.pid}.json`);
|
|
104
|
+
const worker = fileURLToPath(
|
|
105
|
+
new URL("../workers/background-agent-worker.js", import.meta.url),
|
|
106
|
+
);
|
|
107
|
+
const job = {
|
|
108
|
+
id,
|
|
109
|
+
argv,
|
|
110
|
+
cwd,
|
|
111
|
+
sessionId,
|
|
112
|
+
title: title || "Background agent",
|
|
113
|
+
cliEntry: cliEntry || process.argv[1],
|
|
114
|
+
logFile: logPath(id),
|
|
115
|
+
};
|
|
116
|
+
writeFileSync(jobFile, JSON.stringify(job), { mode: 0o600 });
|
|
117
|
+
let child;
|
|
118
|
+
try {
|
|
119
|
+
child = _deps.spawn(process.execPath, [worker, jobFile], {
|
|
120
|
+
cwd,
|
|
121
|
+
detached: true,
|
|
122
|
+
stdio: "ignore",
|
|
123
|
+
windowsHide: true,
|
|
124
|
+
});
|
|
125
|
+
} catch (error) {
|
|
126
|
+
rmSync(jobFile, { force: true });
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
child.unref();
|
|
130
|
+
const state = {
|
|
131
|
+
id,
|
|
132
|
+
sessionId,
|
|
133
|
+
title: job.title,
|
|
134
|
+
cwd,
|
|
135
|
+
pid: child.pid,
|
|
136
|
+
status: "running",
|
|
137
|
+
startedAt: Date.now(),
|
|
138
|
+
endedAt: null,
|
|
139
|
+
exitCode: null,
|
|
140
|
+
logFile: job.logFile,
|
|
141
|
+
};
|
|
142
|
+
writeBackgroundAgentState(state);
|
|
143
|
+
return state;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function readBackgroundAgentLog(id, options = {}) {
|
|
147
|
+
const file = logPath(id);
|
|
148
|
+
if (!existsSync(file)) return "";
|
|
149
|
+
const text = readFileSync(file, "utf8");
|
|
150
|
+
const lines = text.split(/\r?\n/);
|
|
151
|
+
const limit = Math.max(1, Number(options.lines) || 100);
|
|
152
|
+
return lines.slice(-limit).join("\n");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function stopBackgroundAgent(id) {
|
|
156
|
+
const state = effectiveBackgroundAgentState(readBackgroundAgentState(id));
|
|
157
|
+
if (!state) throw new Error(`Background agent not found: ${id}`);
|
|
158
|
+
if (state.status !== "running") return { ...state, stopped: false };
|
|
159
|
+
if (process.platform === "win32") {
|
|
160
|
+
const killed = _deps.spawnSync(
|
|
161
|
+
"taskkill",
|
|
162
|
+
["/PID", String(state.pid), "/T", "/F"],
|
|
163
|
+
{
|
|
164
|
+
windowsHide: true,
|
|
165
|
+
encoding: "utf8",
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
if (killed.error || (killed.status !== 0 && isProcessAlive(state.pid))) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Failed to stop background agent ${id}: ${killed.error?.message || killed.stderr || `taskkill exited ${killed.status}`}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
try {
|
|
175
|
+
process.kill(-Number(state.pid), "SIGTERM");
|
|
176
|
+
} catch {
|
|
177
|
+
process.kill(Number(state.pid), "SIGTERM");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const next = {
|
|
181
|
+
...state,
|
|
182
|
+
status: "stopped",
|
|
183
|
+
endedAt: Date.now(),
|
|
184
|
+
stoppedByUser: true,
|
|
185
|
+
};
|
|
186
|
+
writeBackgroundAgentState(next);
|
|
187
|
+
return { ...next, stopped: true };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function openBackgroundLogFile(id) {
|
|
191
|
+
const fd = openSync(logPath(id), "a", 0o600);
|
|
192
|
+
return { fd, close: () => closeSync(fd) };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function removeJobFile(file) {
|
|
196
|
+
rmSync(file, { force: true });
|
|
197
|
+
}
|