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
|
@@ -27,6 +27,19 @@ import {
|
|
|
27
27
|
createRuntimeEvent,
|
|
28
28
|
} from "../../runtime/runtime-events.js";
|
|
29
29
|
import { createWsMessageDispatcher } from "./message-dispatcher.js";
|
|
30
|
+
import {
|
|
31
|
+
RemoteSessionRegistry,
|
|
32
|
+
RemoteSessionPolicy,
|
|
33
|
+
} from "../../harness/remote-session-registry.js";
|
|
34
|
+
import { RemoteSessionAuditLog } from "../../harness/remote-session-audit.js";
|
|
35
|
+
import { RemoteSessionAuditFileSink } from "../../harness/remote-session-audit-sink.js";
|
|
36
|
+
import {
|
|
37
|
+
RemoteSessionPushDispatcher,
|
|
38
|
+
isApprovalRequestEvent,
|
|
39
|
+
} from "../../harness/remote-session-push.js";
|
|
40
|
+
import { createRemoteSessionPushSender } from "../../harness/remote-session-push-fcm.js";
|
|
41
|
+
import { RemoteSessionRelay } from "../remote-session-relay.js";
|
|
42
|
+
import { handleRemoteSessionPublish } from "./remote-session-protocol.js";
|
|
30
43
|
import { handleTaskDetail, handleTaskHistory } from "./task-protocol.js";
|
|
31
44
|
import {
|
|
32
45
|
handleSessionCreate,
|
|
@@ -226,6 +239,63 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
226
239
|
|
|
227
240
|
/** Session handlers: sessionId → WSAgentHandler | WSChatHandler */
|
|
228
241
|
this.sessionHandlers = new Map();
|
|
242
|
+
/** Org-policy constraints for Remote Sessions (scopes, device cap, TTL). */
|
|
243
|
+
this.remoteSessionPolicy =
|
|
244
|
+
options.remoteSessionPolicy || RemoteSessionPolicy.fromEnv(process.env);
|
|
245
|
+
/** Local authorization state for multi-device Remote Sessions. */
|
|
246
|
+
this.remoteSessions =
|
|
247
|
+
options.remoteSessionRegistry ||
|
|
248
|
+
new RemoteSessionRegistry({ policy: this.remoteSessionPolicy });
|
|
249
|
+
/**
|
|
250
|
+
* Optional durable sink (JSONL) for the audit trail. Enabled only when a
|
|
251
|
+
* sink is injected or CHAINLESSCHAIN_REMOTE_SESSION_AUDIT_FILE is set; the
|
|
252
|
+
* in-memory ring stays the primary store and query surface.
|
|
253
|
+
*/
|
|
254
|
+
this.remoteSessionAuditSink =
|
|
255
|
+
options.remoteSessionAuditSink ||
|
|
256
|
+
RemoteSessionAuditFileSink.fromEnv(process.env);
|
|
257
|
+
/** Bounded in-memory audit trail for Remote Session lifecycle + control. */
|
|
258
|
+
this.remoteSessionAudit =
|
|
259
|
+
options.remoteSessionAudit ||
|
|
260
|
+
new RemoteSessionAuditLog(
|
|
261
|
+
this.remoteSessionAuditSink
|
|
262
|
+
? {
|
|
263
|
+
sink: this.remoteSessionAuditSink.handler,
|
|
264
|
+
// Hydrate so audit queries survive a restart; cap to the ring size.
|
|
265
|
+
initialEntries: this.remoteSessionAuditSink.readAll({
|
|
266
|
+
limit: 1000,
|
|
267
|
+
}),
|
|
268
|
+
}
|
|
269
|
+
: {},
|
|
270
|
+
);
|
|
271
|
+
/**
|
|
272
|
+
* Vendor-push dispatcher — wakes backgrounded paired devices for approval
|
|
273
|
+
* requests. Credential-free by default (a no-op until a `sender` is
|
|
274
|
+
* injected); FCM/APNs delivery is host-supplied.
|
|
275
|
+
*/
|
|
276
|
+
this.remoteSessionPush =
|
|
277
|
+
options.remoteSessionPush ||
|
|
278
|
+
RemoteSessionPushDispatcher.fromEnv(process.env, {
|
|
279
|
+
// Prefer an explicitly injected sender (tests / custom transport); else
|
|
280
|
+
// build a real FCM sender from env when service-account creds are
|
|
281
|
+
// configured. Stays a no-op (null) when neither is present.
|
|
282
|
+
sender:
|
|
283
|
+
options.remoteSessionPushSender ||
|
|
284
|
+
createRemoteSessionPushSender(process.env),
|
|
285
|
+
});
|
|
286
|
+
this.remoteSessionRelayUrl = options.remoteSessionRelayUrl || null;
|
|
287
|
+
this.remoteSessionPeerId = options.remoteSessionPeerId || null;
|
|
288
|
+
this.remoteSessionCrypto = new Map();
|
|
289
|
+
this.remoteSessionPairingSecrets = new Map();
|
|
290
|
+
this.remoteSessionRelay =
|
|
291
|
+
options.remoteSessionRelay ||
|
|
292
|
+
(this.remoteSessionRelayUrl && this.remoteSessionPeerId
|
|
293
|
+
? new RemoteSessionRelay({
|
|
294
|
+
relayUrl: this.remoteSessionRelayUrl,
|
|
295
|
+
peerId: this.remoteSessionPeerId,
|
|
296
|
+
})
|
|
297
|
+
: null);
|
|
298
|
+
this._attachRemoteSessionRelay();
|
|
229
299
|
this._dispatcher = createWsMessageDispatcher(this);
|
|
230
300
|
|
|
231
301
|
this._heartbeatTimer = null;
|
|
@@ -251,6 +321,12 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
251
321
|
this.port = addr.port;
|
|
252
322
|
}
|
|
253
323
|
this._startHeartbeat();
|
|
324
|
+
this.remoteSessionRelay?.connect().catch((error) => {
|
|
325
|
+
this.emit("client-error", {
|
|
326
|
+
clientId: "remote-session-relay",
|
|
327
|
+
error: error.message,
|
|
328
|
+
});
|
|
329
|
+
});
|
|
254
330
|
this.emit("listening", { port: this.port, host: this.host });
|
|
255
331
|
resolve();
|
|
256
332
|
});
|
|
@@ -285,6 +361,9 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
285
361
|
}
|
|
286
362
|
}
|
|
287
363
|
this.sessionHandlers.clear();
|
|
364
|
+
this.remoteSessionCrypto.clear();
|
|
365
|
+
this.remoteSessionPairingSecrets.clear();
|
|
366
|
+
this.remoteSessionRelay?.close();
|
|
288
367
|
|
|
289
368
|
// Kill all running child processes
|
|
290
369
|
for (const [id, child] of this.processes) {
|
|
@@ -391,6 +470,18 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
391
470
|
client.authTimer = null;
|
|
392
471
|
}
|
|
393
472
|
this.clients.delete(clientId);
|
|
473
|
+
for (const affected of this.remoteSessions.removeClient(clientId)) {
|
|
474
|
+
if (affected.closed)
|
|
475
|
+
this.remoteSessionCrypto.delete(affected.sessionId);
|
|
476
|
+
if (affected.closed)
|
|
477
|
+
this.remoteSessionPairingSecrets.delete(affected.sessionId);
|
|
478
|
+
this.remoteSessionAudit?.record({
|
|
479
|
+
sessionId: affected.sessionId,
|
|
480
|
+
actor: clientId,
|
|
481
|
+
action: affected.closed ? "session.closed" : "device.disconnected",
|
|
482
|
+
detail: { reason: "disconnect" },
|
|
483
|
+
});
|
|
484
|
+
}
|
|
394
485
|
this.emit("disconnection", { clientId });
|
|
395
486
|
});
|
|
396
487
|
|
|
@@ -975,12 +1066,220 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
975
1066
|
if (ws.readyState === ws.OPEN) {
|
|
976
1067
|
try {
|
|
977
1068
|
ws.send(JSON.stringify(data));
|
|
1069
|
+
this._mirrorRemoteSessionEvent(ws, data);
|
|
978
1070
|
} catch (_err) {
|
|
979
1071
|
// Connection may have just closed
|
|
980
1072
|
}
|
|
981
1073
|
}
|
|
982
1074
|
}
|
|
983
1075
|
|
|
1076
|
+
/** Mirror canonical Agent Session output to paired Remote Session clients. */
|
|
1077
|
+
_mirrorRemoteSessionEvent(hostWs, data) {
|
|
1078
|
+
if (
|
|
1079
|
+
!data ||
|
|
1080
|
+
typeof data !== "object" ||
|
|
1081
|
+
!data.sessionId ||
|
|
1082
|
+
String(data.type || "").startsWith("remote-session-")
|
|
1083
|
+
) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
let hostClientId = null;
|
|
1087
|
+
for (const [clientId, client] of this.clients) {
|
|
1088
|
+
if (client.ws === hostWs) {
|
|
1089
|
+
hostClientId = clientId;
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (!hostClientId) return;
|
|
1094
|
+
|
|
1095
|
+
for (const remoteSession of this.remoteSessions.findHosted(
|
|
1096
|
+
data.sessionId,
|
|
1097
|
+
hostClientId,
|
|
1098
|
+
)) {
|
|
1099
|
+
for (const member of remoteSession.members.values()) {
|
|
1100
|
+
if (
|
|
1101
|
+
member.clientId === hostClientId ||
|
|
1102
|
+
!member.scopes.includes("observe")
|
|
1103
|
+
) {
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
// Wake a backgrounded device via vendor push for approval requests.
|
|
1107
|
+
// Independent of the WS mirror below — a relay-paired mobile app may
|
|
1108
|
+
// report a live socket while its process is suspended and will never
|
|
1109
|
+
// read the mirrored event until the user taps the push.
|
|
1110
|
+
if (member.pushToken && isApprovalRequestEvent(data.type)) {
|
|
1111
|
+
this._dispatchApprovalPush(remoteSession, member, data);
|
|
1112
|
+
}
|
|
1113
|
+
const target = this.clients.get(member.clientId);
|
|
1114
|
+
if (!target && this.remoteSessionRelay) {
|
|
1115
|
+
const crypto = this.remoteSessionCrypto.get(remoteSession.sessionId);
|
|
1116
|
+
if (crypto) {
|
|
1117
|
+
this.remoteSessionRelay.sendEncrypted(
|
|
1118
|
+
member.clientId,
|
|
1119
|
+
crypto.encrypt(member.clientId, data),
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
if (!target) continue;
|
|
1125
|
+
this._send(target.ws, {
|
|
1126
|
+
type: "remote-session-event",
|
|
1127
|
+
remoteSessionId: remoteSession.sessionId,
|
|
1128
|
+
agentSessionId: remoteSession.agentSessionId,
|
|
1129
|
+
event: data,
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Fire-and-forget a vendor push to one member for an approval request, then
|
|
1137
|
+
* record the outcome in the audit trail. Never throws (the dispatcher is
|
|
1138
|
+
* best-effort); the WS mirror + in-app notification remain primary.
|
|
1139
|
+
*/
|
|
1140
|
+
_dispatchApprovalPush(remoteSession, member, data) {
|
|
1141
|
+
const dispatcher = this.remoteSessionPush;
|
|
1142
|
+
if (!dispatcher || !dispatcher.enabled) return;
|
|
1143
|
+
const requestId =
|
|
1144
|
+
data.requestId || data.approvalId || data.id || `approval:${data.type}`;
|
|
1145
|
+
dispatcher
|
|
1146
|
+
.dispatch({
|
|
1147
|
+
token: member.pushToken,
|
|
1148
|
+
provider: member.pushProvider,
|
|
1149
|
+
sessionId: remoteSession.sessionId,
|
|
1150
|
+
clientId: member.clientId,
|
|
1151
|
+
dedupeKey: requestId,
|
|
1152
|
+
notification: {
|
|
1153
|
+
title: "Approval requested",
|
|
1154
|
+
body: "A coding session needs your approval",
|
|
1155
|
+
},
|
|
1156
|
+
})
|
|
1157
|
+
.then((outcome) => {
|
|
1158
|
+
// A vendor that reports the token as retired means the app was
|
|
1159
|
+
// uninstalled / the token rotated — prune it so it is never retried.
|
|
1160
|
+
if (outcome.code === "PUSH_TOKEN_UNREGISTERED") {
|
|
1161
|
+
try {
|
|
1162
|
+
this.remoteSessions.registerPush(
|
|
1163
|
+
remoteSession.sessionId,
|
|
1164
|
+
member.clientId,
|
|
1165
|
+
{ token: null },
|
|
1166
|
+
);
|
|
1167
|
+
} catch {
|
|
1168
|
+
// Member may have been revoked meanwhile — nothing to prune.
|
|
1169
|
+
}
|
|
1170
|
+
this.remoteSessionAudit?.record({
|
|
1171
|
+
sessionId: remoteSession.sessionId,
|
|
1172
|
+
actor: member.clientId,
|
|
1173
|
+
action: "push.unregistered",
|
|
1174
|
+
detail: { provider: member.pushProvider || null },
|
|
1175
|
+
});
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
this.remoteSessionAudit?.record({
|
|
1179
|
+
sessionId: remoteSession.sessionId,
|
|
1180
|
+
actor: member.clientId,
|
|
1181
|
+
action:
|
|
1182
|
+
outcome.status === "sent"
|
|
1183
|
+
? "push.sent"
|
|
1184
|
+
: outcome.status === "failed"
|
|
1185
|
+
? "push.failed"
|
|
1186
|
+
: "push.skipped",
|
|
1187
|
+
detail: {
|
|
1188
|
+
provider: member.pushProvider || null,
|
|
1189
|
+
reason: outcome.reason || null,
|
|
1190
|
+
error: outcome.error || null,
|
|
1191
|
+
},
|
|
1192
|
+
});
|
|
1193
|
+
})
|
|
1194
|
+
.catch(() => {
|
|
1195
|
+
// Dispatcher never rejects, but guard anyway — a failed courtesy push
|
|
1196
|
+
// must not surface as an unhandled rejection.
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
_attachRemoteSessionRelay() {
|
|
1201
|
+
if (!this.remoteSessionRelay) return;
|
|
1202
|
+
this.remoteSessionRelay.on("relay-message", (message) => {
|
|
1203
|
+
this._handleRemoteRelayMessage(message).catch((error) => {
|
|
1204
|
+
this.emit("client-error", {
|
|
1205
|
+
clientId: "remote-session-relay",
|
|
1206
|
+
error: error.message,
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
});
|
|
1210
|
+
this.remoteSessionRelay.on("encrypted-message", ({ from, envelope }) => {
|
|
1211
|
+
this._handleRemoteEncryptedControl(from, envelope).catch((error) => {
|
|
1212
|
+
this.emit("client-error", { clientId: from, error: error.message });
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
async _handleRemoteRelayMessage(message) {
|
|
1218
|
+
if (
|
|
1219
|
+
message?.type !== "message" ||
|
|
1220
|
+
message.payload?.type !== "remote-session.pair"
|
|
1221
|
+
)
|
|
1222
|
+
return;
|
|
1223
|
+
const payload = message.payload;
|
|
1224
|
+
const sessionId = payload.envelope?.sessionId;
|
|
1225
|
+
const crypto = this.remoteSessionCrypto.get(sessionId);
|
|
1226
|
+
const token = this.remoteSessionPairingSecrets.get(sessionId);
|
|
1227
|
+
if (!crypto || !token)
|
|
1228
|
+
throw new Error("Remote Session pairing is unavailable or expired");
|
|
1229
|
+
crypto.pair(payload.mobilePeerId, payload.mobilePublicKey, token);
|
|
1230
|
+
const join = crypto.decrypt(payload.envelope);
|
|
1231
|
+
if (join.type !== "pair.join" || join.token !== token) {
|
|
1232
|
+
throw new Error("Invalid encrypted Remote Session pairing request");
|
|
1233
|
+
}
|
|
1234
|
+
const relayMember = this.remoteSessions.join({
|
|
1235
|
+
sessionId,
|
|
1236
|
+
clientId: payload.mobilePeerId,
|
|
1237
|
+
token: join.token,
|
|
1238
|
+
via: "relay",
|
|
1239
|
+
pushToken: join.pushToken,
|
|
1240
|
+
pushProvider: join.pushProvider,
|
|
1241
|
+
});
|
|
1242
|
+
this.remoteSessionAudit?.record({
|
|
1243
|
+
sessionId,
|
|
1244
|
+
actor: payload.mobilePeerId,
|
|
1245
|
+
action: "device.joined",
|
|
1246
|
+
detail: {
|
|
1247
|
+
via: "relay",
|
|
1248
|
+
hasPush: relayMember?.member?.pushToken ? true : false,
|
|
1249
|
+
},
|
|
1250
|
+
});
|
|
1251
|
+
this.remoteSessionPairingSecrets.delete(sessionId);
|
|
1252
|
+
this.remoteSessionRelay.sendEncrypted(
|
|
1253
|
+
payload.mobilePeerId,
|
|
1254
|
+
crypto.encrypt(payload.mobilePeerId, {
|
|
1255
|
+
type: "pair.accepted",
|
|
1256
|
+
remoteSessionId: sessionId,
|
|
1257
|
+
}),
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
async _handleRemoteEncryptedControl(from, envelope) {
|
|
1262
|
+
const crypto = this.remoteSessionCrypto.get(envelope?.sessionId);
|
|
1263
|
+
if (!crypto) throw new Error("Remote Session crypto context not found");
|
|
1264
|
+
const event = crypto.decrypt(envelope);
|
|
1265
|
+
const virtualWs = {
|
|
1266
|
+
OPEN: 1,
|
|
1267
|
+
readyState: 1,
|
|
1268
|
+
send: (raw) => {
|
|
1269
|
+
const response = JSON.parse(raw);
|
|
1270
|
+
this.remoteSessionRelay.sendEncrypted(
|
|
1271
|
+
from,
|
|
1272
|
+
crypto.encrypt(from, response),
|
|
1273
|
+
);
|
|
1274
|
+
},
|
|
1275
|
+
};
|
|
1276
|
+
await handleRemoteSessionPublish(this, from, virtualWs, {
|
|
1277
|
+
id: `remote-${Date.now()}`,
|
|
1278
|
+
remoteSessionId: envelope.sessionId,
|
|
1279
|
+
event,
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
|
|
984
1283
|
/** @private — broadcast a message to all connected, authenticated clients */
|
|
985
1284
|
_broadcast(data) {
|
|
986
1285
|
for (const [, client] of this.clients) {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize runtime events before comparing them with a golden transcript.
|
|
3
|
+
*
|
|
4
|
+
* Runtime envelopes intentionally contain volatile identity and timing fields.
|
|
5
|
+
* Golden files should describe observable behavior, not a particular run, so
|
|
6
|
+
* those fields are removed recursively and platform-specific workspace paths
|
|
7
|
+
* are replaced with a stable token.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const VOLATILE_KEYS = new Set([
|
|
11
|
+
"eventId",
|
|
12
|
+
"id",
|
|
13
|
+
"requestId",
|
|
14
|
+
"sessionId",
|
|
15
|
+
"sequence",
|
|
16
|
+
"timestamp",
|
|
17
|
+
"createdAt",
|
|
18
|
+
"updatedAt",
|
|
19
|
+
"duration",
|
|
20
|
+
"durationMs",
|
|
21
|
+
"toolTelemetryRecord",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function normalizeString(value, workspace) {
|
|
25
|
+
if (!workspace) return value;
|
|
26
|
+
const variants = new Set([
|
|
27
|
+
workspace,
|
|
28
|
+
workspace.replaceAll("\\", "/"),
|
|
29
|
+
workspace.replaceAll("/", "\\"),
|
|
30
|
+
]);
|
|
31
|
+
let normalized = value;
|
|
32
|
+
for (const variant of variants) {
|
|
33
|
+
normalized = normalized.split(variant).join("<WORKSPACE>");
|
|
34
|
+
}
|
|
35
|
+
return normalized.replaceAll("<WORKSPACE>\\", "<WORKSPACE>/");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeValue(value, workspace) {
|
|
39
|
+
if (typeof value === "string") return normalizeString(value, workspace);
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
return value.map((item) => normalizeValue(item, workspace));
|
|
42
|
+
}
|
|
43
|
+
if (!value || typeof value !== "object") return value;
|
|
44
|
+
|
|
45
|
+
return Object.fromEntries(
|
|
46
|
+
Object.entries(value)
|
|
47
|
+
.filter(([key]) => !VOLATILE_KEYS.has(key))
|
|
48
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
49
|
+
.map(([key, item]) => [key, normalizeValue(item, workspace)]),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {Array<object>} events
|
|
55
|
+
* @param {{workspace?: string, ignoredTypes?: string[]}} [options]
|
|
56
|
+
*/
|
|
57
|
+
export function normalizeGoldenTranscript(events, options = {}) {
|
|
58
|
+
if (!Array.isArray(events)) {
|
|
59
|
+
throw new TypeError("Golden transcript input must be an array");
|
|
60
|
+
}
|
|
61
|
+
const ignored = new Set(options.ignoredTypes || ["run-started", "run-ended"]);
|
|
62
|
+
return events
|
|
63
|
+
.filter((event) => !ignored.has(event?.type))
|
|
64
|
+
.map((event) => normalizeValue(event, options.workspace));
|
|
65
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { spawn } from "child_process";
|
|
12
12
|
import { EventEmitter } from "events";
|
|
13
|
+
import { safeJsonParse } from "../lib/safe-json.js";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Injectable dependencies — overridable from tests.
|
|
@@ -1156,8 +1157,11 @@ export class MCPServerConfig {
|
|
|
1156
1157
|
return {
|
|
1157
1158
|
name: row.name,
|
|
1158
1159
|
command: row.command || null,
|
|
1159
|
-
|
|
1160
|
-
|
|
1160
|
+
// safeJsonParse, not bare JSON.parse: list()/getAutoConnect() map every
|
|
1161
|
+
// row through here, so one corrupt cell must not take down the whole
|
|
1162
|
+
// MCP server list (`|| "[]"` only guards NULL, not a corrupt non-empty string).
|
|
1163
|
+
args: safeJsonParse(row.args, []),
|
|
1164
|
+
env: safeJsonParse(row.env, {}),
|
|
1161
1165
|
autoConnect: row.auto_connect === 1,
|
|
1162
1166
|
url: row.url || null,
|
|
1163
1167
|
transport:
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Remote Session audit log — durable JSONL sink.
|
|
2
|
+
//
|
|
3
|
+
// Attaches to RemoteSessionAuditLog's `sink` seam to persist the (already
|
|
4
|
+
// privacy-filtered) audit trail to a newline-delimited JSON file so it survives
|
|
5
|
+
// a host restart for forensics. Each entry is one JSON line; the file is
|
|
6
|
+
// size-bounded with simple rotation (audit.jsonl → audit.jsonl.1 → …) so it can
|
|
7
|
+
// never grow unbounded. Reads tolerate a torn last line (crash mid-append) by
|
|
8
|
+
// skipping any line that will not parse.
|
|
9
|
+
//
|
|
10
|
+
// Deliberately dependency-free (node:fs only) and injectable — a SQLite-backed
|
|
11
|
+
// sink could implement the same { handler, readAll } shape later without
|
|
12
|
+
// touching the audit log or its call sites.
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
const DEFAULT_MAX_BYTES = 5 * 1024 * 1024; // 5 MiB
|
|
18
|
+
const DEFAULT_BACKUPS = 1;
|
|
19
|
+
|
|
20
|
+
export class RemoteSessionAuditFileSink {
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
if (!options.path) {
|
|
23
|
+
throw new Error("RemoteSessionAuditFileSink requires a path");
|
|
24
|
+
}
|
|
25
|
+
this.path = options.path;
|
|
26
|
+
this.maxBytes = options.maxBytes || DEFAULT_MAX_BYTES;
|
|
27
|
+
this.backups = Number.isInteger(options.backups)
|
|
28
|
+
? options.backups
|
|
29
|
+
: DEFAULT_BACKUPS;
|
|
30
|
+
this.fs = options.fs || fs;
|
|
31
|
+
this._ensureDir();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_ensureDir() {
|
|
35
|
+
try {
|
|
36
|
+
this.fs.mkdirSync(path.dirname(this.path), { recursive: true });
|
|
37
|
+
} catch {
|
|
38
|
+
// Best effort — a later append will surface a real error if the dir is
|
|
39
|
+
// genuinely unwritable, and the audit log swallows sink throws anyway.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Bound function to pass as RemoteSessionAuditLog({ sink }). */
|
|
44
|
+
get handler() {
|
|
45
|
+
return (entry) => this.append(entry);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Append one entry as a JSON line, rotating first if it would overflow. */
|
|
49
|
+
append(entry) {
|
|
50
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
51
|
+
this._rotateIfNeeded(Buffer.byteLength(line, "utf8"));
|
|
52
|
+
this.fs.appendFileSync(this.path, line, "utf8");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_rotateIfNeeded(incomingBytes) {
|
|
56
|
+
let size = 0;
|
|
57
|
+
try {
|
|
58
|
+
size = this.fs.statSync(this.path).size;
|
|
59
|
+
} catch {
|
|
60
|
+
return; // No file yet — nothing to rotate.
|
|
61
|
+
}
|
|
62
|
+
if (size + incomingBytes <= this.maxBytes) return;
|
|
63
|
+
if (this.backups <= 0) {
|
|
64
|
+
// No history kept — truncate by removing the active file.
|
|
65
|
+
try {
|
|
66
|
+
this.fs.rmSync(this.path);
|
|
67
|
+
} catch {
|
|
68
|
+
/* ignore */
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Shift backups: .(<n-1>) → .(<n>), …, active → .1, dropping the oldest.
|
|
73
|
+
for (let i = this.backups; i >= 1; i -= 1) {
|
|
74
|
+
const src = i === 1 ? this.path : `${this.path}.${i - 1}`;
|
|
75
|
+
const dst = `${this.path}.${i}`;
|
|
76
|
+
try {
|
|
77
|
+
if (this.fs.existsSync(src)) this.fs.renameSync(src, dst);
|
|
78
|
+
} catch {
|
|
79
|
+
/* ignore a single failed rotation step */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Read persisted entries oldest-first across the active file and its backups.
|
|
86
|
+
* Corrupt/partial lines are skipped. `limit` keeps only the newest N.
|
|
87
|
+
*/
|
|
88
|
+
readAll({ limit } = {}) {
|
|
89
|
+
const files = [];
|
|
90
|
+
for (let i = this.backups; i >= 1; i -= 1) files.push(`${this.path}.${i}`);
|
|
91
|
+
files.push(this.path);
|
|
92
|
+
const entries = [];
|
|
93
|
+
for (const file of files) {
|
|
94
|
+
let raw;
|
|
95
|
+
try {
|
|
96
|
+
raw = this.fs.readFileSync(file, "utf8");
|
|
97
|
+
} catch {
|
|
98
|
+
continue; // Missing backup slot — expected.
|
|
99
|
+
}
|
|
100
|
+
for (const line of raw.split("\n")) {
|
|
101
|
+
const trimmed = line.trim();
|
|
102
|
+
if (!trimmed) continue;
|
|
103
|
+
try {
|
|
104
|
+
entries.push(JSON.parse(trimmed));
|
|
105
|
+
} catch {
|
|
106
|
+
// Torn write (crash mid-append) or manual edit — skip the line.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (limit && limit > 0 && entries.length > limit) {
|
|
111
|
+
return entries.slice(entries.length - limit);
|
|
112
|
+
}
|
|
113
|
+
return entries;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build a sink from env, or null when no audit file is configured. Enable with
|
|
118
|
+
* CHAINLESSCHAIN_REMOTE_SESSION_AUDIT_FILE (path); tune the rotation ceiling
|
|
119
|
+
* with CHAINLESSCHAIN_REMOTE_SESSION_AUDIT_MAX_BYTES.
|
|
120
|
+
*/
|
|
121
|
+
static fromEnv(env = {}, options = {}) {
|
|
122
|
+
const filePath = env.CHAINLESSCHAIN_REMOTE_SESSION_AUDIT_FILE;
|
|
123
|
+
if (!filePath) return null;
|
|
124
|
+
const maxBytes =
|
|
125
|
+
Number(env.CHAINLESSCHAIN_REMOTE_SESSION_AUDIT_MAX_BYTES) || undefined;
|
|
126
|
+
return new RemoteSessionAuditFileSink({
|
|
127
|
+
path: filePath,
|
|
128
|
+
maxBytes,
|
|
129
|
+
...options,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Remote Session audit log.
|
|
2
|
+
//
|
|
3
|
+
// A bounded, in-memory, privacy-respecting audit trail for one CLI host process
|
|
4
|
+
// — matched to the ephemeral (in-memory, 12h TTL) nature of remote sessions.
|
|
5
|
+
// Captures WHO did WHAT and WHEN across a session's lifecycle and remote control
|
|
6
|
+
// events, but deliberately does NOT record prompt/tool payloads (only shapes
|
|
7
|
+
// like content length), keeping the trail useful for forensics without
|
|
8
|
+
// hoarding session content. A pluggable `sink` seam lets a durable backend
|
|
9
|
+
// (JSONL file, SQLite audit_log) be attached later without touching call sites.
|
|
10
|
+
|
|
11
|
+
const DEFAULT_MAX_ENTRIES = 1000;
|
|
12
|
+
|
|
13
|
+
export const REMOTE_SESSION_AUDIT_ACTIONS = Object.freeze([
|
|
14
|
+
"session.created",
|
|
15
|
+
"pairing-token.issued",
|
|
16
|
+
"device.joined",
|
|
17
|
+
"device.revoked",
|
|
18
|
+
"device.disconnected",
|
|
19
|
+
"session.closed",
|
|
20
|
+
"control.prompt",
|
|
21
|
+
"control.approval",
|
|
22
|
+
"control.interrupt",
|
|
23
|
+
"push.registered",
|
|
24
|
+
"push.sent",
|
|
25
|
+
"push.failed",
|
|
26
|
+
"push.skipped",
|
|
27
|
+
"push.unregistered",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export class RemoteSessionAuditLog {
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.now = options.now || Date.now;
|
|
33
|
+
this.maxEntries = options.maxEntries || DEFAULT_MAX_ENTRIES;
|
|
34
|
+
this.sink = typeof options.sink === "function" ? options.sink : null;
|
|
35
|
+
this.entries = [];
|
|
36
|
+
this.seq = 0;
|
|
37
|
+
// Hydrate from a durable sink so audit queries survive a host restart. The
|
|
38
|
+
// persisted entries are NOT re-written to the sink (they are already
|
|
39
|
+
// there); we only pre-fill the in-memory ring and carry the seq high-water
|
|
40
|
+
// forward so new entries stay strictly monotonic.
|
|
41
|
+
if (Array.isArray(options.initialEntries)) {
|
|
42
|
+
this._hydrate(options.initialEntries);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_hydrate(entries) {
|
|
47
|
+
const recent = entries.slice(-this.maxEntries);
|
|
48
|
+
for (const entry of recent) {
|
|
49
|
+
if (!entry || !entry.action) continue;
|
|
50
|
+
this.entries.push({ ...entry });
|
|
51
|
+
if (typeof entry.seq === "number" && entry.seq > this.seq) {
|
|
52
|
+
this.seq = entry.seq;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Append an audit entry. Never throws into the caller — a broken sink or bad
|
|
59
|
+
* argument must not take down the live session it is auditing.
|
|
60
|
+
*/
|
|
61
|
+
record({ sessionId = null, actor = null, action, detail = null } = {}) {
|
|
62
|
+
if (!action) return null;
|
|
63
|
+
const entry = {
|
|
64
|
+
seq: (this.seq += 1),
|
|
65
|
+
timestamp: this.now(),
|
|
66
|
+
sessionId,
|
|
67
|
+
actor,
|
|
68
|
+
action,
|
|
69
|
+
detail: detail && typeof detail === "object" ? { ...detail } : detail,
|
|
70
|
+
};
|
|
71
|
+
this.entries.push(entry);
|
|
72
|
+
if (this.entries.length > this.maxEntries) {
|
|
73
|
+
this.entries.splice(0, this.entries.length - this.maxEntries);
|
|
74
|
+
}
|
|
75
|
+
if (this.sink) {
|
|
76
|
+
try {
|
|
77
|
+
this.sink(entry);
|
|
78
|
+
} catch {
|
|
79
|
+
// A durable-sink failure must not break the session being audited.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return entry;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Newest-first, optionally filtered by session and/or action. */
|
|
86
|
+
list({ sessionId, action, limit } = {}) {
|
|
87
|
+
let out = this.entries;
|
|
88
|
+
if (sessionId) out = out.filter((entry) => entry.sessionId === sessionId);
|
|
89
|
+
if (action) out = out.filter((entry) => entry.action === action);
|
|
90
|
+
out = [...out].reverse();
|
|
91
|
+
if (limit && limit > 0) out = out.slice(0, limit);
|
|
92
|
+
return out.map((entry) => ({ ...entry }));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
stats(sessionId) {
|
|
96
|
+
const scope = sessionId
|
|
97
|
+
? this.entries.filter((entry) => entry.sessionId === sessionId)
|
|
98
|
+
: this.entries;
|
|
99
|
+
const byAction = {};
|
|
100
|
+
for (const entry of scope) {
|
|
101
|
+
byAction[entry.action] = (byAction[entry.action] || 0) + 1;
|
|
102
|
+
}
|
|
103
|
+
return { total: scope.length, byAction };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
clear() {
|
|
107
|
+
this.entries = [];
|
|
108
|
+
this.seq = 0;
|
|
109
|
+
}
|
|
110
|
+
}
|