chainlesschain 0.162.159 → 0.162.161
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 +1 -1
- package/src/assets/web-panel/assets/{AIOps-ByIiF5bT.js → AIOps-DMn528q-.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-NlgSFZq5.js → ActionButton-D9ZcDCJL.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-D17cOUcb.js → Analytics-A_K-8PX0.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-B4mEZcbl.js → AppLayout-D4pFW3X6.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CASsmyQw.js → Artifacts-Dz46COAd.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DYAia2e2.js → Audit-cbMjbyg0.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-C1lvYUSu.js → BackgroundAgents-Dtf88GAj.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-B6agK6bA.js → Backup-C5KRxn52.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DM43R2vy.js → BaseInput-Dd2UK3BN.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-Dtdp3eXp.js → Chat-lptKTFdG.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-BL5c_OQ1.js → ChatBubbleRenderer-C2KLtwHz.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-D2xf7uZt.js → Checkbox-_yspG6rG.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-D3j45jJL.js → Codegen-BGq_GAha.js} +1 -1
- package/src/assets/web-panel/assets/{Col-voPJ9fUZ.js → Col-CePor71g.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DEsyTESF.js → Community-CLOWhIT7.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-OHV_QXLD.js → Compact-CivJt2vT.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-eDTJWC-J.js → Compliance-Btmo1GGE.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BBvy-6d1.js → Cowork-Ct35OlG4.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-Dw2eoJxu.js → Cron-CTc2GmhB.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DQHms_Bh.js → Crosschain-ZtiBETk1.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Cyu7Id1c.js → DID-BNqMQQXw.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-sIDzkG92.js → Dashboard-BmzZd-9V.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DUW3T2ey.js → Dropdown-CMVgvP2M.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CECELXW8.js → EmailListRenderer-CcqQFAfT.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BIZR37ov.js → FamilyGuardDashboard-CTqB1_4V.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BEZIQX84.js → Federation-CiKOOzlb.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BcdUMznL.js → FormItemContext-Wk-OT0gr.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DsN_fr5H.js → GenericCardRenderer-DbEQT6ue.js} +1 -1
- package/src/assets/web-panel/assets/{Git-63LhjBx2.js → Git-0ZvCPSD9.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CYtQtIcH.js → Governance-CmVuKkPb.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BwR6UUox.js → Inference-CTWE3Jwg.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-D9lVNTUA.js → KnowledgeGraph-CBEIF00L.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-C4q5h7Ut.js → Logs-DLhxDwVG.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-DxywvhTU.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-Plksliuw.js → Marketplace-BgsCBrnc.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DZUFfeuG.js → McpTools-ClBUGDS2.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-NoDYt29V.js → Memory-CnG_ZnYw.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CEXGTkdh.js → MobileBridge-BUOAbR1Z.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-DnbJkbZo.js → MobileProjects-Dnt_9-WB.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-DTAa0cwy.js → Mtc-CLjt4kCD.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-CVV7LnNS.js → MtcAudit-f24U9Aes.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-BqqGrjt_.js → Multisig-GrmGQbSq.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-C4WP8DRp.js → NLProgramming-DADaWn-n.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Zo4DJUVI.js → Notes-xtf5RX6C.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-DBcjRaz2.js → NotificationSettings-Cd27Rgtx.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-fXRxlDjl.js +1 -0
- package/src/assets/web-panel/assets/{Organization-BT9WPBOX.js → Organization-joCFlNB0.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-D6b4bZ3q.js → Overflow-B1HoURq0.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Cw3OsYx-.js → P2P-DiwsKbT7.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-C0nyiK2b.js → PdhVaultBrowser-Xe-BsW6S.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-zVBZEB_C.js → Permissions-C23qi8kl.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-BOspsFew.js → PersonalDataHub-BS1syX2f.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-CQWfZ4XH.js → Pipeline-DRYwwDyh.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-C2oGJ_WL.js → Privacy-BQKaDHPH.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BZMR4qY1.js → ProjectInit-BC6F9xiG.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DAujoP63.js → ProjectSettings-ClFPc1Nd.js} +2 -2
- package/src/assets/web-panel/assets/Projects-Dn-f5aYe.js +1 -0
- package/src/assets/web-panel/assets/{Providers-D_RGHM-v.js → Providers-CLWuOUNt.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-Mf6KfL2J.js → QrScannerModal-DiqULTxb.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-5D8URoI_.js → QuickAsk-9A03P-E_.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Df4kC8Gy.js → Recommend-CJZAnbq_.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-DGi3f3vO.js → RemoteSession-C9A1R77h.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-jmkGFNWb.js → Reputation-BIZc6Rky.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DP6Pcu3m.js → Row-DfIPNtuG.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-D60hSuPO.js → RssFeed-C5znFcQ1.js} +3 -3
- package/src/assets/web-panel/assets/{Search-0mf1BQUK.js → Search-DSs2z3OX.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CfZtLFYV.js → Security-CV6LcrLY.js} +4 -4
- package/src/assets/web-panel/assets/{Services-D9zpZLWz.js → Services-cmhoX0Xj.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-D1_44eMD.js → Skeleton-tCsg2po7.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-cVFa8zkz.js → Skills-BK1KUDiy.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-Cinpzc3L.js → Sla-yRgyeA82.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-bqdiJE_P.js → SpeechSettings-c1mo1mlp.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-Dgl_SLgj.js → SyncSettings-koIAfuT3.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-DawWAEhy.js → Tasks-DkkreAaB.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Ca0nY651.js → Templates-fy5Ctf9M.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CgaGEhLm.js → Tenant-Bw70Jf6S.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-D-9EnpRx.js → Terminal-BKh6Rx-4.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CSOxDHZ6.js → TimelineRenderer-Dg54WP0c.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BpwLZpL3.js → Tokens-BvvD5SxJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DgowwBK5.js → Trigger-B7Ug9x4e.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BZutapjE.js → Trust-BjvuMORY.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-QZPt8_Sp.js → UkeySign-CZqhkx8p.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DlQx5UDJ.js → VideoEditing-ClR50lhX.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-ZfdTgtWA.js → Wallet-DlfppYjx.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-CAp0y_Cr.js → WebAuthn-BirgVr8B.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-BVITZEmr.js → WorkflowEditor-B8DP_bax.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CC2tPaNg.js → chat-DA8AUZzL.js} +1 -1
- package/src/assets/web-panel/assets/{colors-31brc9CA.js → colors-C38pLGp1.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DLMkC9C-.js → compact-item-81rtr98o.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BMtveI6F.js → createContext-D34ixq8t.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CHDIEOeN.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DbD6WHW2.js → hasIn-D4bzqTrZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoO4WEJB.js → index-9qoVpPt6.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJhREOlG.js → index-B0QArPQt.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnFLzejT.js → index-B8mkLM4x.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bbc2JeQG.js → index-BIP62a6r.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFXKbOek.js → index-BP-zkXK8.js} +1 -1
- package/src/assets/web-panel/assets/{index-C-cGlCDW.js → index-BS3M5H9V.js} +1 -1
- package/src/assets/web-panel/assets/{index-AaIKG60c.js → index-BXd2_LmH.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEXa-4zd.js → index-BlxEphl7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZl-UXB6.js → index-BqIhLAbA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOM-VFlv.js → index-BtGxuQrO.js} +1 -1
- package/src/assets/web-panel/assets/{index-CcVszn2b.js → index-BvD7b2IK.js} +1 -1
- package/src/assets/web-panel/assets/{index-C21pdBqs.js → index-BvL_3J_n.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJvxFyIz.js → index-COe_cOLJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-ALvoNTRF.js → index-CP5alL4t.js} +1 -1
- package/src/assets/web-panel/assets/index-CY1NURWZ.js +1 -0
- package/src/assets/web-panel/assets/{index-Dm0byjm1.js → index-CbhE3j5j.js} +1 -1
- package/src/assets/web-panel/assets/{index-WQUO2vvK.js → index-Cd3KuNK5.js} +1 -1
- package/src/assets/web-panel/assets/{index-bdaJwTXJ.js → index-CoVgZw_b.js} +1 -1
- package/src/assets/web-panel/assets/{index-COHHQ9cU.js → index-CuS-UPoj.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7LHNw_N.js → index-D0Ih7Hwn.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cod97Urs.js → index-D3zCCZRS.js} +3 -3
- package/src/assets/web-panel/assets/{index-Cwbqh0KZ.js → index-D6GfvEQG.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKAB6-6V.js → index-DBFSo1NF.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEwjBA9q.js → index-DFHa1HIg.js} +1 -1
- package/src/assets/web-panel/assets/{index-DndNnYc0.js → index-DFyWKOBD.js} +1 -1
- package/src/assets/web-panel/assets/{index-3oBsXZmF.js → index-DMgnRwrK.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_eUKGYP.js → index-DUQLXLQM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CUmLMFsy.js → index-D_2VYPr1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BPpS-lGb.js → index-DakfFZ-k.js} +1 -1
- package/src/assets/web-panel/assets/{index-ep6JchNG.js → index-Ddk0e_Pi.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7cV0tu0.js → index-DkHqMnn3.js} +1 -1
- package/src/assets/web-panel/assets/{index-jG6h22Qg.js → index-Dmo3dFHb.js} +1 -1
- package/src/assets/web-panel/assets/index-DokUYqpq.js +1 -0
- package/src/assets/web-panel/assets/{index-C1PfeI88.js → index-DvfCEJG-.js} +1 -1
- package/src/assets/web-panel/assets/{index-ntVQqSML.js → index-E1OJRXs4.js} +1 -1
- package/src/assets/web-panel/assets/{index-CWgfipcC.js → index-F76w1NiL.js} +1 -1
- package/src/assets/web-panel/assets/{index-BiRQUoyE.js → index-ZRJRItm0.js} +1 -1
- package/src/assets/web-panel/assets/{index-BFRLmCR3.js → index-fI5sXMLg.js} +1 -1
- package/src/assets/web-panel/assets/{index-D67kIagl.js → index-tKiC8T-4.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-D5UoBRhN.js → initDefaultProps-ByyvflIR.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DltOw0CR.js → motion-B3FQm2By.js} +1 -1
- package/src/assets/web-panel/assets/{move-_oPkk5zj.js → move-BSfns6Ef.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-EES8ROCS.js → mtc-parser-CTG1Ieri.js} +1 -1
- package/src/assets/web-panel/assets/{omit-B-_39x_M.js → omit-DObM_2j6.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-l6D0yJHz.js → pickAttrs-DOfelQ1t.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DIeUK31L.js → placementArrow-CHZILds_.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-dG4EzUQo.js → responsiveObserve-REbucKSx.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DWFzYGdH.js → slide-C6EZzD7o.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CECdTA5H.js → statusUtils-BxkWS-g3.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-D6kP-mMK.js → styleChecker-Chklyoq6.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-BWfna_rd.js → useFlexGapSupport-CRVwKrrI.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-Bb-j6rzo.js → useFs-DV24ik_J.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-D5OIsYM4.js → usePersonalDataHub-C7vIo4Pg.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DNpwzFTM.js → vnode-By-v5GlF.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-vNLpFQBW.js → zoom-B1w9Am3r.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +15 -1
- package/src/commands/agent.js +72 -6
- package/src/commands/background-session.js +46 -2
- package/src/commands/mcp.js +46 -0
- package/src/commands/session.js +70 -0
- package/src/data/changelog.json +12 -2
- package/src/gateways/ws/background-agent-protocol.js +88 -14
- package/src/harness/jsonl-session-store.js +70 -1
- package/src/harness/mcp-client.js +50 -0
- package/src/lib/agent-authority.js +251 -0
- package/src/lib/agent-sandbox.js +68 -0
- package/src/lib/agents.js +12 -4
- package/src/lib/api-key-helper.js +62 -0
- package/src/lib/background-agent-phase.js +100 -0
- package/src/lib/background-agent-supervisor.js +320 -8
- package/src/lib/backpressure-policy.js +105 -0
- package/src/lib/capability-negotiation.js +212 -0
- package/src/lib/credential-guard.js +11 -0
- package/src/lib/credential-proxy.js +182 -0
- package/src/lib/dependency-install-policy.js +130 -0
- package/src/lib/event-seq-replay.js +167 -0
- package/src/lib/exit-codes.cjs +77 -0
- package/src/lib/goal-condition-engine.js +369 -0
- package/src/lib/headless-manifest.js +188 -0
- package/src/lib/hook-runner.cjs +70 -4
- package/src/lib/ide-context-redaction.js +130 -0
- package/src/lib/ide-context.js +128 -13
- package/src/lib/jsonl-session-store.js +2 -0
- package/src/lib/llm-config-defaults.js +24 -8
- package/src/lib/project-instructions.js +98 -1
- package/src/lib/project-mcp-trust.js +87 -0
- package/src/lib/remote-path-mapping.js +229 -0
- package/src/lib/schedule-planner.js +136 -0
- package/src/lib/sensitive-file-guard.js +51 -0
- package/src/lib/sub-agent-context.js +7 -3
- package/src/repl/bg-dashboard.js +13 -12
- package/src/runtime/agent-core.js +113 -10
- package/src/runtime/coding-agent-contract-shared.cjs +19 -2
- package/src/runtime/headless-runner.js +66 -7
- package/src/runtime/headless-stream.js +224 -12
- package/src/runtime/mcp-config.js +32 -1
- package/src/runtime/system-prompt.js +3 -0
- package/src/workers/background-agent-worker.js +37 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-DexLlXfN.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-BCQel9bD.js +0 -1
- package/src/assets/web-panel/assets/Projects-DvH_P2Uq.js +0 -1
- package/src/assets/web-panel/assets/devWarning-CB8pPOtd.js +0 -1
- package/src/assets/web-panel/assets/index-BhXiaiev.js +0 -1
- package/src/assets/web-panel/assets/index-WpaYaE3D.js +0 -1
|
@@ -8,17 +8,37 @@
|
|
|
8
8
|
* Routes (request → reply):
|
|
9
9
|
* bg-list {all?} → {type:"bg-list", sessions}
|
|
10
10
|
* bg-view {bgId, lines?} → {type:"bg-view", session, log}
|
|
11
|
-
* bg-attach {bgId, lines?}
|
|
11
|
+
* bg-attach {bgId, lines?, sinceSeq?} → {type:"bg-attach", bgId, hello,
|
|
12
|
+
* log, latestSeq, [reattached, replayed,
|
|
13
|
+
* replayTruncated]}
|
|
12
14
|
* bg-prompt {bgId, text} → {type:"bg-prompt", bgId, sent}
|
|
13
15
|
* bg-stop-turn {bgId} → {type:"bg-stop-turn", bgId, sent}
|
|
14
16
|
* bg-detach {bgId} → {type:"bg-detach", bgId}
|
|
15
17
|
* bg-stop {bgId} → {type:"bg-stop", session} (kill whole session)
|
|
16
18
|
*
|
|
17
|
-
* While attached, the server pushes unsolicited frames to that client
|
|
18
|
-
*
|
|
19
|
+
* While attached, the server pushes unsolicited frames to that client, each
|
|
20
|
+
* stamped with a per-attachment monotonic `seq` (additive; older clients
|
|
21
|
+
* ignore it) so the client can detect gaps and request a replay:
|
|
22
|
+
* {type:"bg-event", bgId, seq, event} worker lifecycle events (turn-started /
|
|
19
23
|
* turn-ended / idle / accepted / error /
|
|
20
24
|
* transport-closed)
|
|
21
|
-
* {type:"bg-log", bgId, chunk}
|
|
25
|
+
* {type:"bg-log", bgId, seq, chunk} log-file delta (500ms poll)
|
|
26
|
+
*
|
|
27
|
+
* Reconnect resilience: a panel whose WS blipped re-sends bg-attach with the
|
|
28
|
+
* last `seq` it saw (`sinceSeq`); the still-live relay re-pushes the frames it
|
|
29
|
+
* missed (bounded buffer) and reports `latestSeq`. If the missed range was
|
|
30
|
+
* already evicted, `replayTruncated:true` tells the client to full-resync from
|
|
31
|
+
* the log. If the relay was fully torn down (WS closed → cleanup), the fresh
|
|
32
|
+
* attach returns hello + log tail as before and the client resets its tracker.
|
|
33
|
+
*
|
|
34
|
+
* Backpressure: if the WS client drains too slowly (ws.bufferedAmount over the
|
|
35
|
+
* high water), DROPPABLE frames (bg-log) are shed while CRITICAL ones (bg-event
|
|
36
|
+
* lifecycle) are always delivered. Dropped frames are still recorded (seq
|
|
37
|
+
* advances), so the gap on the next delivered frame trips the client's
|
|
38
|
+
* replaySince recovery. A one-shot {type:"bg-lag", bgId, lagging:true} marks
|
|
39
|
+
* the start and {type:"bg-lag", bgId, lagging:false, dropped, latestSeq} the
|
|
40
|
+
* recovery — the UI shows "catching up" instead of freezing. bg-lag frames
|
|
41
|
+
* carry no seq (out-of-band control; the gap tracker ignores them).
|
|
22
42
|
*
|
|
23
43
|
* The state file's `transport.token` is the local takeover capability — it is
|
|
24
44
|
* NEVER sent over WS. Sessions are serialized with `interactive: true/false`
|
|
@@ -26,6 +46,12 @@
|
|
|
26
46
|
*/
|
|
27
47
|
|
|
28
48
|
import { existsSync, readFileSync } from "node:fs";
|
|
49
|
+
import { EventReplayBuffer } from "../../lib/event-seq-replay.js";
|
|
50
|
+
import {
|
|
51
|
+
BackpressurePolicy,
|
|
52
|
+
CRITICAL,
|
|
53
|
+
DROPPABLE,
|
|
54
|
+
} from "../../lib/backpressure-policy.js";
|
|
29
55
|
|
|
30
56
|
const LOG_POLL_MS = 500;
|
|
31
57
|
|
|
@@ -139,6 +165,20 @@ export async function handleBgAttach(server, clientId, id, ws, message) {
|
|
|
139
165
|
if (existing) {
|
|
140
166
|
// Idempotent re-attach: reply with the live handshake instead of
|
|
141
167
|
// stacking a second relay (a reconnecting panel re-sends bg-attach).
|
|
168
|
+
// If the panel reports the last seq it saw (`sinceSeq`), replay the
|
|
169
|
+
// push frames it missed while the WS was blipped — re-sent as normal
|
|
170
|
+
// bg-event/bg-log frames (their original seq), so the client's gap
|
|
171
|
+
// tracker sees a contiguous run. `replayTruncated` tells the client the
|
|
172
|
+
// hole predates our buffer and it must full-resync (re-read the log).
|
|
173
|
+
const sinceSeq = Number(message.sinceSeq);
|
|
174
|
+
let replayTruncated = false;
|
|
175
|
+
let replayed = 0;
|
|
176
|
+
if (Number.isFinite(sinceSeq)) {
|
|
177
|
+
const { frames, truncated } = existing.buffer.replaySince(sinceSeq);
|
|
178
|
+
replayTruncated = truncated;
|
|
179
|
+
replayed = frames.length;
|
|
180
|
+
for (const frame of frames) server._send(ws, frame);
|
|
181
|
+
}
|
|
142
182
|
server._send(ws, {
|
|
143
183
|
id,
|
|
144
184
|
type: "bg-attach",
|
|
@@ -146,6 +186,9 @@ export async function handleBgAttach(server, clientId, id, ws, message) {
|
|
|
146
186
|
hello: existing.hello,
|
|
147
187
|
log: "",
|
|
148
188
|
reattached: true,
|
|
189
|
+
latestSeq: existing.buffer.latestSeq,
|
|
190
|
+
replayed,
|
|
191
|
+
replayTruncated,
|
|
149
192
|
});
|
|
150
193
|
return;
|
|
151
194
|
}
|
|
@@ -172,6 +215,42 @@ export async function handleBgAttach(server, clientId, id, ws, message) {
|
|
|
172
215
|
);
|
|
173
216
|
}
|
|
174
217
|
|
|
218
|
+
// Per-attachment replay buffer: stamps a monotonic seq on every push frame
|
|
219
|
+
// and retains a bounded tail so a reconnecting client can fill gaps
|
|
220
|
+
// (bg-attach { sinceSeq }). One buffer per relay instance — seq restarts
|
|
221
|
+
// at 1 on a fresh attach, so the client resets its tracker then.
|
|
222
|
+
const buffer = new EventReplayBuffer();
|
|
223
|
+
// Backpressure: if the WS client drains too slowly (ws.bufferedAmount grows
|
|
224
|
+
// past the high water), shed DROPPABLE frames (bg-log — recoverable) while
|
|
225
|
+
// still delivering CRITICAL ones (bg-event lifecycle). Dropped frames are
|
|
226
|
+
// still RECORDED (seq advances), so the seq gap on the next delivered frame
|
|
227
|
+
// trips the client's replaySince recovery; a one-shot bg-lag notice lets
|
|
228
|
+
// the UI show "catching up" instead of freezing. Buffer stays 0 in the
|
|
229
|
+
// normal case → byte-identical legacy behavior.
|
|
230
|
+
const backpressure = new BackpressurePolicy();
|
|
231
|
+
const bufferedBytes = () =>
|
|
232
|
+
ws && Number.isFinite(ws.bufferedAmount) ? ws.bufferedAmount : 0;
|
|
233
|
+
const pushFrame = (base, frameClass) => {
|
|
234
|
+
const frame = buffer.record(base); // record always → seq advances, replay recovers drops
|
|
235
|
+
const d = backpressure.evaluate(bufferedBytes(), frameClass);
|
|
236
|
+
if (d.lagStarted)
|
|
237
|
+
server._send(ws, { type: "bg-lag", bgId, lagging: true });
|
|
238
|
+
if (d.send) server._send(ws, frame);
|
|
239
|
+
if (d.lagEnded) {
|
|
240
|
+
server._send(ws, {
|
|
241
|
+
type: "bg-lag",
|
|
242
|
+
bgId,
|
|
243
|
+
lagging: false,
|
|
244
|
+
dropped: d.dropped,
|
|
245
|
+
latestSeq: buffer.latestSeq,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
const pushEvent = (event) =>
|
|
250
|
+
pushFrame({ type: "bg-event", bgId, event }, CRITICAL);
|
|
251
|
+
const pushLog = (chunk) =>
|
|
252
|
+
pushFrame({ type: "bg-log", bgId, chunk }, DROPPABLE);
|
|
253
|
+
|
|
175
254
|
const { connectBackgroundSession } =
|
|
176
255
|
await import("../../lib/background-session-transport.js");
|
|
177
256
|
let conn;
|
|
@@ -179,16 +258,10 @@ export async function handleBgAttach(server, clientId, id, ws, message) {
|
|
|
179
258
|
conn = await connectBackgroundSession({
|
|
180
259
|
pipePath: session.transport.pipe,
|
|
181
260
|
token: session.transport.token,
|
|
182
|
-
onEvent: (event) =>
|
|
183
|
-
server._send(ws, { type: "bg-event", bgId, event });
|
|
184
|
-
},
|
|
261
|
+
onEvent: (event) => pushEvent(event),
|
|
185
262
|
onClose: () => {
|
|
186
263
|
if (dropAttachment(client, bgId)) {
|
|
187
|
-
|
|
188
|
-
type: "bg-event",
|
|
189
|
-
bgId,
|
|
190
|
-
event: { type: "transport-closed" },
|
|
191
|
-
});
|
|
264
|
+
pushEvent({ type: "transport-closed" });
|
|
192
265
|
}
|
|
193
266
|
},
|
|
194
267
|
});
|
|
@@ -212,7 +285,7 @@ export async function handleBgAttach(server, clientId, id, ws, message) {
|
|
|
212
285
|
if (text.length > offset) {
|
|
213
286
|
const chunk = text.slice(offset);
|
|
214
287
|
offset = text.length;
|
|
215
|
-
|
|
288
|
+
pushLog(chunk);
|
|
216
289
|
}
|
|
217
290
|
} catch {
|
|
218
291
|
/* transient read failures skip one tick */
|
|
@@ -220,7 +293,7 @@ export async function handleBgAttach(server, clientId, id, ws, message) {
|
|
|
220
293
|
}, LOG_POLL_MS);
|
|
221
294
|
poll.unref?.();
|
|
222
295
|
|
|
223
|
-
attachments(client).set(bgId, { conn, poll, hello: conn.hello });
|
|
296
|
+
attachments(client).set(bgId, { conn, poll, hello: conn.hello, buffer });
|
|
224
297
|
server._send(ws, {
|
|
225
298
|
id,
|
|
226
299
|
type: "bg-attach",
|
|
@@ -229,6 +302,7 @@ export async function handleBgAttach(server, clientId, id, ws, message) {
|
|
|
229
302
|
log: readBackgroundAgentLog(bgId, {
|
|
230
303
|
lines: Number(message.lines) || 100,
|
|
231
304
|
}),
|
|
305
|
+
latestSeq: buffer.latestSeq,
|
|
232
306
|
});
|
|
233
307
|
} catch (err) {
|
|
234
308
|
sendError(server, id, ws, "BG_ATTACH_FAILED", err.message);
|
|
@@ -271,10 +271,17 @@ export function listJsonlSessions(options = {}) {
|
|
|
271
271
|
const messageCount = events.filter(
|
|
272
272
|
(e) => e.type === "user_message" || e.type === "assistant_message",
|
|
273
273
|
).length;
|
|
274
|
+
// cc session rename appends session_rename events; the LAST one wins.
|
|
275
|
+
let renamedTitle = null;
|
|
276
|
+
for (const e of events) {
|
|
277
|
+
if (e.type === "session_rename" && e.data?.title) {
|
|
278
|
+
renamedTitle = e.data.title;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
274
281
|
|
|
275
282
|
return {
|
|
276
283
|
id,
|
|
277
|
-
title: startEvent?.data?.title || "Untitled",
|
|
284
|
+
title: renamedTitle || startEvent?.data?.title || "Untitled",
|
|
278
285
|
provider: startEvent?.data?.provider || "",
|
|
279
286
|
model: startEvent?.data?.model || "",
|
|
280
287
|
message_count: messageCount,
|
|
@@ -296,6 +303,68 @@ export function listJsonlSessions(options = {}) {
|
|
|
296
303
|
return files;
|
|
297
304
|
}
|
|
298
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Rename a session (gap-analysis 2026-07-11 P1 "命名会话"): appends a
|
|
308
|
+
* `session_rename` event so the hash chain stays intact (no rewrite). The
|
|
309
|
+
* LAST rename wins when listing/showing.
|
|
310
|
+
*/
|
|
311
|
+
export function renameSession(sessionId, title) {
|
|
312
|
+
if (!sessionExists(sessionId)) {
|
|
313
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
314
|
+
}
|
|
315
|
+
const normalized = String(title || "").trim();
|
|
316
|
+
if (!normalized) throw new Error("A non-empty title is required");
|
|
317
|
+
appendEvent(sessionId, "session_rename", { title: normalized.slice(0, 200) });
|
|
318
|
+
return { id: sessionId, title: normalized.slice(0, 200) };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Prune old sessions (gap-analysis 2026-07-11 P1 "保留期限"): delete session
|
|
323
|
+
* transcripts whose LAST activity is older than `olderThanDays`, always
|
|
324
|
+
* keeping the newest `keep` (default 10) regardless of age. Dry-run returns
|
|
325
|
+
* the same shape without deleting.
|
|
326
|
+
*/
|
|
327
|
+
export function pruneJsonlSessions(options = {}) {
|
|
328
|
+
const olderThanDays = Number(options.olderThanDays);
|
|
329
|
+
if (!Number.isFinite(olderThanDays) || olderThanDays < 0) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
"prune requires --older-than <days> (a non-negative number)",
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
const keep = Number.isFinite(Number(options.keep))
|
|
335
|
+
? Math.max(0, Math.floor(Number(options.keep)))
|
|
336
|
+
: 10;
|
|
337
|
+
const now = typeof options.now === "number" ? options.now : Date.now();
|
|
338
|
+
const cutoff = now - olderThanDays * 24 * 60 * 60 * 1000;
|
|
339
|
+
// listJsonlSessions sorts by last activity DESC — the first `keep` entries
|
|
340
|
+
// survive unconditionally.
|
|
341
|
+
const all = listJsonlSessions({ limit: 100000 });
|
|
342
|
+
const candidates = all.slice(keep).filter((s) => {
|
|
343
|
+
const last = Date.parse(s.updated_at || s.created_at || "");
|
|
344
|
+
return Number.isFinite(last) && last < cutoff;
|
|
345
|
+
});
|
|
346
|
+
const deleted = [];
|
|
347
|
+
for (const s of candidates) {
|
|
348
|
+
if (options.dryRun === true) {
|
|
349
|
+
deleted.push(s.id);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
rmSync(sessionPath(s.id), { force: true });
|
|
354
|
+
_chainTailCache.delete(s.id);
|
|
355
|
+
deleted.push(s.id);
|
|
356
|
+
} catch {
|
|
357
|
+
/* per-file failures never abort the sweep */
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
scanned: all.length,
|
|
362
|
+
kept: all.length - deleted.length,
|
|
363
|
+
deleted,
|
|
364
|
+
dryRun: options.dryRun === true,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
299
368
|
export function forkSession(sourceId) {
|
|
300
369
|
const events = readEvents(sourceId);
|
|
301
370
|
if (events.length === 0) return null;
|
|
@@ -1051,6 +1051,17 @@ export class MCPClient extends EventEmitter {
|
|
|
1051
1051
|
|
|
1052
1052
|
// Server notification
|
|
1053
1053
|
if (msg.method) {
|
|
1054
|
+
// tools/resources list_changed (gap 2026-07-11 MCP 生命周期): refetch
|
|
1055
|
+
// the changed list so entry.tools/entry.resources stay live —
|
|
1056
|
+
// `listTools()`, `/mcp` status and callTool routing all see the update.
|
|
1057
|
+
// (The LLM tool array of an in-flight turn is deliberately NOT mutated:
|
|
1058
|
+
// tool-search's prompt-cache stability depends on an append-only,
|
|
1059
|
+
// stable-prefix tool list.)
|
|
1060
|
+
if (msg.method === "notifications/tools/list_changed") {
|
|
1061
|
+
this._refreshServerList(serverName, "tools");
|
|
1062
|
+
} else if (msg.method === "notifications/resources/list_changed") {
|
|
1063
|
+
this._refreshServerList(serverName, "resources");
|
|
1064
|
+
}
|
|
1054
1065
|
this.emit("notification", {
|
|
1055
1066
|
server: serverName,
|
|
1056
1067
|
method: msg.method,
|
|
@@ -1059,6 +1070,45 @@ export class MCPClient extends EventEmitter {
|
|
|
1059
1070
|
}
|
|
1060
1071
|
}
|
|
1061
1072
|
|
|
1073
|
+
/**
|
|
1074
|
+
* Re-fetch a server's tools / resources list after a `*_list_changed`
|
|
1075
|
+
* notification. Coalesced per server+kind: a burst of notifications folds
|
|
1076
|
+
* into the in-flight refetch plus at most one trailing pass. Best-effort —
|
|
1077
|
+
* a failed refetch keeps the previous list and waits for the next
|
|
1078
|
+
* notification. Emits "tools-changed" / "resources-changed" on update.
|
|
1079
|
+
*/
|
|
1080
|
+
async _refreshServerList(serverName, kind) {
|
|
1081
|
+
const entry = this.servers.get(serverName);
|
|
1082
|
+
if (!entry) return;
|
|
1083
|
+
const flags = (entry._listRefresh = entry._listRefresh || {});
|
|
1084
|
+
if (flags[`${kind}Running`]) {
|
|
1085
|
+
flags[`${kind}Dirty`] = true;
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
flags[`${kind}Running`] = true;
|
|
1089
|
+
try {
|
|
1090
|
+
do {
|
|
1091
|
+
flags[`${kind}Dirty`] = false;
|
|
1092
|
+
try {
|
|
1093
|
+
const method = kind === "tools" ? "tools/list" : "resources/list";
|
|
1094
|
+
const result = await this._sendRequest(serverName, method, {});
|
|
1095
|
+
const list =
|
|
1096
|
+
(kind === "tools" ? result?.tools : result?.resources) || [];
|
|
1097
|
+
entry[kind] = list;
|
|
1098
|
+
if (kind === "tools") entry.toolsError = null;
|
|
1099
|
+
this.emit(`${kind}-changed`, {
|
|
1100
|
+
server: serverName,
|
|
1101
|
+
count: list.length,
|
|
1102
|
+
});
|
|
1103
|
+
} catch {
|
|
1104
|
+
break; // keep the previous list; the next notification retries
|
|
1105
|
+
}
|
|
1106
|
+
} while (flags[`${kind}Dirty`]);
|
|
1107
|
+
} finally {
|
|
1108
|
+
flags[`${kind}Running`] = false;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1062
1112
|
/**
|
|
1063
1113
|
* Answer a server-initiated JSON-RPC request. `roots/list` returns the
|
|
1064
1114
|
* session's workspace roots (we advertise the roots capability); `ping`
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-agent authority envelope + approval binding (P0 security slice —
|
|
3
|
+
* CLAUDE_CODE_CLI_INCREMENTAL_GAP_ANALYSIS_2026-07-12 §"权限来源与跨 Agent
|
|
4
|
+
* 授权边界").
|
|
5
|
+
*
|
|
6
|
+
* When Subagent, Team, Remote Control, Inbound Channel, Hook and MCP all feed
|
|
7
|
+
* text into one session, "who said this" is a security boundary: another
|
|
8
|
+
* agent's "the user approved" MUST NOT gain the user's authority, and an
|
|
9
|
+
* external channel message MUST NOT silently widen the session's permissions.
|
|
10
|
+
*
|
|
11
|
+
* This module is the single source of truth for two invariants the runtime
|
|
12
|
+
* enforces at every trust seam:
|
|
13
|
+
*
|
|
14
|
+
* 1. **Who may approve.** Only the local user (UI-owned stdin), an explicit
|
|
15
|
+
* permission tool, or a *paired + authenticated + approve-scoped* remote
|
|
16
|
+
* device may answer a permission gate. Model / subagent / teammate /
|
|
17
|
+
* channel / hook messages top out at `steer` — the string "approved" in
|
|
18
|
+
* their payload is just text.
|
|
19
|
+
*
|
|
20
|
+
* 2. **What an approval authorizes.** An approval is bound to
|
|
21
|
+
* `tool_call_id + normalized_arguments + policy_digest`. A replayed or
|
|
22
|
+
* mis-routed approval, or one issued for arguments that have since
|
|
23
|
+
* changed, fails the binding check — so a stale "yes" can never green-light
|
|
24
|
+
* a different (or tampered) tool call.
|
|
25
|
+
*
|
|
26
|
+
* `origin` is always assigned by trusted dispatch code from HOW a message
|
|
27
|
+
* arrived (which socket / which authenticated device / which internal caller)
|
|
28
|
+
* — it is never read from untrusted message *content*, which is the whole
|
|
29
|
+
* point: a model cannot label its own text `origin:"user"`.
|
|
30
|
+
*
|
|
31
|
+
* Pure + dependency-light (only node:crypto) so every seam — remote-session
|
|
32
|
+
* protocol, channels, hooks, subagent dispatch — can share it.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
36
|
+
|
|
37
|
+
/** Where a message / approval entered the runtime. Assigned by trusted code. */
|
|
38
|
+
export const ORIGIN = Object.freeze({
|
|
39
|
+
USER: "user", // the human at the local UI (owns stdin)
|
|
40
|
+
MODEL: "model", // the assistant's own output
|
|
41
|
+
SUBAGENT: "subagent", // a spawned sub-agent
|
|
42
|
+
TEAMMATE: "teammate", // another agent in a team run
|
|
43
|
+
CHANNEL: "channel", // an inbound channel (webhook / telegram)
|
|
44
|
+
HOOK: "hook", // a settings/plugin hook result
|
|
45
|
+
REMOTE: "remote", // a paired remote-control device
|
|
46
|
+
SYSTEM: "system", // internal runtime event
|
|
47
|
+
PERMISSION_TOOL: "permission_tool", // an explicit permission-answering tool
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** What a message is allowed to do. Ordered least → most privileged. */
|
|
51
|
+
export const AUTHORITY = Object.freeze({
|
|
52
|
+
NONE: "none", // ignored for control purposes
|
|
53
|
+
STEER: "steer", // may add a user turn / guide the conversation
|
|
54
|
+
APPROVE: "approve", // may answer a permission gate (approve/deny a tool)
|
|
55
|
+
MANAGE: "manage", // may also manage session lifecycle
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const AUTHORITY_RANK = Object.freeze({
|
|
59
|
+
none: 0,
|
|
60
|
+
steer: 1,
|
|
61
|
+
approve: 2,
|
|
62
|
+
manage: 3,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/** Numeric rank for an authority string (unknown → 0). */
|
|
66
|
+
export function authorityRank(authority) {
|
|
67
|
+
return AUTHORITY_RANK[authority] ?? 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Remote scopes that already imply the ability to answer a permission gate. */
|
|
71
|
+
function scopeGrantsApprove(context) {
|
|
72
|
+
const scopes = normalizeScopes(context);
|
|
73
|
+
return scopes.has("approve") || scopes.has("manage");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function scopeGrantsSteer(context) {
|
|
77
|
+
const scopes = normalizeScopes(context);
|
|
78
|
+
return (
|
|
79
|
+
scopes.has("prompt") ||
|
|
80
|
+
scopes.has("interrupt") ||
|
|
81
|
+
scopes.has("observe") ||
|
|
82
|
+
scopeGrantsApprove(context)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeScopes(context = {}) {
|
|
87
|
+
const raw = Array.isArray(context.scopes)
|
|
88
|
+
? context.scopes
|
|
89
|
+
: context.scope
|
|
90
|
+
? [context.scope]
|
|
91
|
+
: [];
|
|
92
|
+
return new Set(raw.map((s) => String(s || "").toLowerCase()));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The MAXIMUM authority an origin may exercise given its authentication
|
|
97
|
+
* context. This is a ceiling — a caller still passes the specific action
|
|
98
|
+
* through `canApprove` / `assertCanApprove`.
|
|
99
|
+
*
|
|
100
|
+
* The only paths to `approve` are: the local user (`manage`), an explicit
|
|
101
|
+
* permission tool, or a remote device that is BOTH authenticated AND was
|
|
102
|
+
* granted the approve scope at pairing. Everything an agent or external
|
|
103
|
+
* message can say tops out at `steer`.
|
|
104
|
+
*/
|
|
105
|
+
export function authorityForOrigin(origin, context = {}) {
|
|
106
|
+
switch (origin) {
|
|
107
|
+
case ORIGIN.USER:
|
|
108
|
+
return AUTHORITY.MANAGE;
|
|
109
|
+
case ORIGIN.PERMISSION_TOOL:
|
|
110
|
+
return AUTHORITY.APPROVE;
|
|
111
|
+
case ORIGIN.REMOTE:
|
|
112
|
+
// A paired Remote Approval Bridge acts for the user ONLY when the pairing
|
|
113
|
+
// is authenticated AND carries the approve scope. Never on the strength
|
|
114
|
+
// of the message alone.
|
|
115
|
+
if (context.authenticated === true && scopeGrantsApprove(context)) {
|
|
116
|
+
return normalizeScopes(context).has("manage")
|
|
117
|
+
? AUTHORITY.MANAGE
|
|
118
|
+
: AUTHORITY.APPROVE;
|
|
119
|
+
}
|
|
120
|
+
if (context.authenticated === true && scopeGrantsSteer(context)) {
|
|
121
|
+
return AUTHORITY.STEER;
|
|
122
|
+
}
|
|
123
|
+
return AUTHORITY.NONE;
|
|
124
|
+
case ORIGIN.MODEL:
|
|
125
|
+
case ORIGIN.SUBAGENT:
|
|
126
|
+
case ORIGIN.TEAMMATE:
|
|
127
|
+
case ORIGIN.CHANNEL:
|
|
128
|
+
case ORIGIN.HOOK:
|
|
129
|
+
case ORIGIN.SYSTEM:
|
|
130
|
+
// May guide the conversation, never speak with the user's approval
|
|
131
|
+
// authority. "The user approved" in their text is just text.
|
|
132
|
+
return AUTHORITY.STEER;
|
|
133
|
+
default:
|
|
134
|
+
return AUTHORITY.NONE;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* True when this envelope may answer a permission gate. An envelope is
|
|
140
|
+
* `{ origin, authenticated?, scope?/scopes?, principalId?, sessionId?, … }`.
|
|
141
|
+
*/
|
|
142
|
+
export function canApprove(envelope = {}) {
|
|
143
|
+
return (
|
|
144
|
+
authorityRank(authorityForOrigin(envelope.origin, envelope)) >=
|
|
145
|
+
AUTHORITY_RANK.approve
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** True when this envelope may manage session lifecycle. */
|
|
150
|
+
export function canManageSession(envelope = {}) {
|
|
151
|
+
return (
|
|
152
|
+
authorityRank(authorityForOrigin(envelope.origin, envelope)) >=
|
|
153
|
+
AUTHORITY_RANK.manage
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Throw a clear, log-safe error when an envelope is NOT allowed to approve.
|
|
159
|
+
* Callers gate every "resolve a permission" seam through this so the rule
|
|
160
|
+
* lives in exactly one place.
|
|
161
|
+
*/
|
|
162
|
+
export function assertCanApprove(envelope = {}) {
|
|
163
|
+
if (canApprove(envelope)) return;
|
|
164
|
+
const who = envelope.origin || "unknown";
|
|
165
|
+
const got = authorityForOrigin(envelope.origin, envelope);
|
|
166
|
+
throw new Error(
|
|
167
|
+
`origin "${who}" (authority "${got}") is not authorized to approve tool calls — ` +
|
|
168
|
+
"only the local user, a paired approve-scoped remote device, or an explicit permission tool may",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Approval binding ─────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Deterministic, key-order-independent serialization of tool arguments, so
|
|
176
|
+
* two structurally-equal argument objects hash identically and ANY real value
|
|
177
|
+
* change hashes differently. Non-objects serialize via JSON.
|
|
178
|
+
*/
|
|
179
|
+
export function normalizeToolArgs(args) {
|
|
180
|
+
return stableStringify(args === undefined ? null : args);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function stableStringify(value) {
|
|
184
|
+
if (value === null || typeof value !== "object") {
|
|
185
|
+
return JSON.stringify(value === undefined ? null : value);
|
|
186
|
+
}
|
|
187
|
+
if (Array.isArray(value)) {
|
|
188
|
+
return "[" + value.map(stableStringify).join(",") + "]";
|
|
189
|
+
}
|
|
190
|
+
const keys = Object.keys(value).sort();
|
|
191
|
+
return (
|
|
192
|
+
"{" +
|
|
193
|
+
keys
|
|
194
|
+
.map((k) => JSON.stringify(k) + ":" + stableStringify(value[k]))
|
|
195
|
+
.join(",") +
|
|
196
|
+
"}"
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The identity an approval authorizes: the exact tool call (`toolCallId`), its
|
|
202
|
+
* normalized arguments, and the policy in force (`policyDigest`). Change any
|
|
203
|
+
* one and the digest changes, so a "yes" for one call can never be replayed
|
|
204
|
+
* onto another or onto tampered arguments.
|
|
205
|
+
*/
|
|
206
|
+
export function approvalBindingDigest({ toolCallId, args, policyDigest } = {}) {
|
|
207
|
+
const h = createHash("sha256");
|
|
208
|
+
h.update("cc-approval-binding-v1\n");
|
|
209
|
+
h.update(String(toolCallId ?? "") + "\n");
|
|
210
|
+
h.update(normalizeToolArgs(args) + "\n");
|
|
211
|
+
h.update(String(policyDigest ?? "") + "\n");
|
|
212
|
+
return "ab_" + h.digest("hex").slice(0, 32);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function timingSafeEqualStr(a, b) {
|
|
216
|
+
const ba = Buffer.from(String(a), "utf8");
|
|
217
|
+
const bb = Buffer.from(String(b), "utf8");
|
|
218
|
+
if (ba.length !== bb.length) return false;
|
|
219
|
+
return timingSafeEqual(ba, bb);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Verify an incoming approval targets the expected pending request. Each side
|
|
224
|
+
* may be a precomputed digest string or a `{toolCallId, args, policyDigest}`
|
|
225
|
+
* descriptor. Returns false on any mismatch or missing input (fail closed).
|
|
226
|
+
* Comparison is constant-time.
|
|
227
|
+
*/
|
|
228
|
+
export function verifyApprovalBinding(expected, provided) {
|
|
229
|
+
if (!expected || !provided) return false;
|
|
230
|
+
const e =
|
|
231
|
+
typeof expected === "string" ? expected : approvalBindingDigest(expected);
|
|
232
|
+
const p =
|
|
233
|
+
typeof provided === "string" ? provided : approvalBindingDigest(provided);
|
|
234
|
+
if (!e || !p) return false;
|
|
235
|
+
return timingSafeEqualStr(e, p);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* A compact, log-safe provenance string for the permission/audit log so it is
|
|
240
|
+
* always clear WHICH principal, over WHICH session, with WHICH authority
|
|
241
|
+
* requested or answered a permission. Carries no secrets.
|
|
242
|
+
*/
|
|
243
|
+
export function describeAuthorityChain(envelope = {}) {
|
|
244
|
+
const parts = [`origin=${envelope.origin || "unknown"}`];
|
|
245
|
+
if (envelope.principalId) parts.push(`principal=${envelope.principalId}`);
|
|
246
|
+
if (envelope.sessionId) parts.push(`session=${envelope.sessionId}`);
|
|
247
|
+
if (envelope.parentAgentId) parts.push(`parent=${envelope.parentAgentId}`);
|
|
248
|
+
if (envelope.correlationId) parts.push(`corr=${envelope.correlationId}`);
|
|
249
|
+
parts.push(`authority=${authorityForOrigin(envelope.origin, envelope)}`);
|
|
250
|
+
return parts.join(" ");
|
|
251
|
+
}
|
package/src/lib/agent-sandbox.js
CHANGED
|
@@ -215,11 +215,79 @@ function executeBubblewrapShell(command, sandbox, options, hostCwd, policy) {
|
|
|
215
215
|
};
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* The run's TRUE isolation level (gap-analysis 2026-07-11 P0 "OS 级沙箱"):
|
|
220
|
+
* - "os-sandbox" : bubblewrap — kernel namespaces confine the child
|
|
221
|
+
* - "container" : docker — container boundary confines the child
|
|
222
|
+
* - "policy-only" : no sandbox — permission rules/shell policy are advisory
|
|
223
|
+
* for already-spawned subprocesses
|
|
224
|
+
* Surfaced in the headless init event and sandboxSummary so a caller can see
|
|
225
|
+
* what actually confines tool subprocesses instead of assuming.
|
|
226
|
+
*/
|
|
227
|
+
export function isolationLevel(sandbox) {
|
|
228
|
+
if (!sandbox) return "policy-only";
|
|
229
|
+
if (sandbox.engine === "bubblewrap") return "os-sandbox";
|
|
230
|
+
if (sandbox.engine === "docker") return "container";
|
|
231
|
+
return "policy-only";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Probe whether the configured sandbox ENGINE is actually runnable on this
|
|
236
|
+
* host (binary present + responds to a version query). Cheap: one spawnSync
|
|
237
|
+
* with a short timeout; no container is started.
|
|
238
|
+
* @returns {{available:boolean, reason:string|null}}
|
|
239
|
+
*/
|
|
240
|
+
export function probeSandboxAvailability(sandbox, deps = _deps) {
|
|
241
|
+
if (!sandbox) return { available: true, reason: null };
|
|
242
|
+
const probeArgs =
|
|
243
|
+
sandbox.engine === "bubblewrap"
|
|
244
|
+
? ["bwrap", ["--version"]]
|
|
245
|
+
: ["docker", ["version", "--format", "{{.Server.Version}}"]];
|
|
246
|
+
const result = deps.spawnSync(probeArgs[0], probeArgs[1], {
|
|
247
|
+
encoding: "utf8",
|
|
248
|
+
timeout: 10000,
|
|
249
|
+
windowsHide: true,
|
|
250
|
+
});
|
|
251
|
+
if (result.error) {
|
|
252
|
+
return {
|
|
253
|
+
available: false,
|
|
254
|
+
reason:
|
|
255
|
+
result.error.code === "ENOENT"
|
|
256
|
+
? `${probeArgs[0]} is not installed`
|
|
257
|
+
: result.error.message,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
261
|
+
return {
|
|
262
|
+
available: false,
|
|
263
|
+
reason: `${probeArgs[0]} probe exited ${result.status}: ${(result.stderr || "").trim().slice(0, 200)}`,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return { available: true, reason: null };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Strict mode (`sandbox.failIfUnavailable: true` in settings): refuse to
|
|
271
|
+
* START the agent when the configured sandbox engine is unavailable, instead
|
|
272
|
+
* of silently degrading per command. Throws with an actionable message; a
|
|
273
|
+
* no-op when the flag is unset or the sandbox is fine.
|
|
274
|
+
*/
|
|
275
|
+
export function assertSandboxAvailable(sandbox, deps = _deps) {
|
|
276
|
+
if (!sandbox || sandbox.policy?.failIfUnavailable !== true) return;
|
|
277
|
+
const probe = probeSandboxAvailability(sandbox, deps);
|
|
278
|
+
if (!probe.available) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
`sandbox.failIfUnavailable: ${sandbox.engine} sandbox is unavailable (${probe.reason}) — refusing to start. Install/start ${sandbox.engine === "bubblewrap" ? "bubblewrap" : "Docker"}, or unset failIfUnavailable to allow per-command degradation.`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
218
285
|
export function sandboxSummary(sandbox) {
|
|
219
286
|
if (!sandbox) return null;
|
|
220
287
|
return {
|
|
221
288
|
engine: sandbox.engine,
|
|
222
289
|
image: sandbox.image,
|
|
290
|
+
isolationLevel: isolationLevel(sandbox),
|
|
223
291
|
network: sandbox.network ? "enabled" : "disabled",
|
|
224
292
|
workspace: "read-write",
|
|
225
293
|
policy: {
|