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.
Files changed (199) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-ByIiF5bT.js → AIOps-DMn528q-.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-NlgSFZq5.js → ActionButton-D9ZcDCJL.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-D17cOUcb.js → Analytics-A_K-8PX0.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-B4mEZcbl.js → AppLayout-D4pFW3X6.js} +5 -5
  7. package/src/assets/web-panel/assets/{Artifacts-CASsmyQw.js → Artifacts-Dz46COAd.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-DYAia2e2.js → Audit-cbMjbyg0.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-C1lvYUSu.js → BackgroundAgents-Dtf88GAj.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-B6agK6bA.js → Backup-C5KRxn52.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-DM43R2vy.js → BaseInput-Dd2UK3BN.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-Dtdp3eXp.js → Chat-lptKTFdG.js} +6 -6
  13. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BL5c_OQ1.js → ChatBubbleRenderer-C2KLtwHz.js} +1 -1
  14. package/src/assets/web-panel/assets/{Checkbox-D2xf7uZt.js → Checkbox-_yspG6rG.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-D3j45jJL.js → Codegen-BGq_GAha.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-voPJ9fUZ.js → Col-CePor71g.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-DEsyTESF.js → Community-CLOWhIT7.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-OHV_QXLD.js → Compact-CivJt2vT.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-eDTJWC-J.js → Compliance-Btmo1GGE.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-BBvy-6d1.js → Cowork-Ct35OlG4.js} +2 -2
  21. package/src/assets/web-panel/assets/{Cron-Dw2eoJxu.js → Cron-CTc2GmhB.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-DQHms_Bh.js → Crosschain-ZtiBETk1.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-Cyu7Id1c.js → DID-BNqMQQXw.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-sIDzkG92.js → Dashboard-BmzZd-9V.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-DUW3T2ey.js → Dropdown-CMVgvP2M.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-CECELXW8.js → EmailListRenderer-CcqQFAfT.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BIZR37ov.js → FamilyGuardDashboard-CTqB1_4V.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-BEZIQX84.js → Federation-CiKOOzlb.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-BcdUMznL.js → FormItemContext-Wk-OT0gr.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-DsN_fr5H.js → GenericCardRenderer-DbEQT6ue.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-63LhjBx2.js → Git-0ZvCPSD9.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-CYtQtIcH.js → Governance-CmVuKkPb.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-BwR6UUox.js → Inference-CTWE3Jwg.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-D9lVNTUA.js → KnowledgeGraph-CBEIF00L.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-C4q5h7Ut.js → Logs-DLhxDwVG.js} +2 -2
  36. package/src/assets/web-panel/assets/MarkdownRenderer-DxywvhTU.js +1 -0
  37. package/src/assets/web-panel/assets/{Marketplace-Plksliuw.js → Marketplace-BgsCBrnc.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-DZUFfeuG.js → McpTools-ClBUGDS2.js} +3 -3
  39. package/src/assets/web-panel/assets/{Memory-NoDYt29V.js → Memory-CnG_ZnYw.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-CEXGTkdh.js → MobileBridge-BUOAbR1Z.js} +1 -1
  41. package/src/assets/web-panel/assets/{MobileProjects-DnbJkbZo.js → MobileProjects-Dnt_9-WB.js} +1 -1
  42. package/src/assets/web-panel/assets/{Mtc-DTAa0cwy.js → Mtc-CLjt4kCD.js} +5 -5
  43. package/src/assets/web-panel/assets/{MtcAudit-CVV7LnNS.js → MtcAudit-f24U9Aes.js} +2 -2
  44. package/src/assets/web-panel/assets/{Multisig-BqqGrjt_.js → Multisig-GrmGQbSq.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-C4WP8DRp.js → NLProgramming-DADaWn-n.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-Zo4DJUVI.js → Notes-xtf5RX6C.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-DBcjRaz2.js → NotificationSettings-Cd27Rgtx.js} +1 -1
  48. package/src/assets/web-panel/assets/OrderTableRenderer-fXRxlDjl.js +1 -0
  49. package/src/assets/web-panel/assets/{Organization-BT9WPBOX.js → Organization-joCFlNB0.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-D6b4bZ3q.js → Overflow-B1HoURq0.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-Cw3OsYx-.js → P2P-DiwsKbT7.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-C0nyiK2b.js → PdhVaultBrowser-Xe-BsW6S.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-zVBZEB_C.js → Permissions-C23qi8kl.js} +3 -3
  54. package/src/assets/web-panel/assets/{PersonalDataHub-BOspsFew.js → PersonalDataHub-BS1syX2f.js} +2 -2
  55. package/src/assets/web-panel/assets/{Pipeline-CQWfZ4XH.js → Pipeline-DRYwwDyh.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-C2oGJ_WL.js → Privacy-BQKaDHPH.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-BZMR4qY1.js → ProjectInit-BC6F9xiG.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-DAujoP63.js → ProjectSettings-ClFPc1Nd.js} +2 -2
  59. package/src/assets/web-panel/assets/Projects-Dn-f5aYe.js +1 -0
  60. package/src/assets/web-panel/assets/{Providers-D_RGHM-v.js → Providers-CLWuOUNt.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-Mf6KfL2J.js → QrScannerModal-DiqULTxb.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-5D8URoI_.js → QuickAsk-9A03P-E_.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-Df4kC8Gy.js → Recommend-CJZAnbq_.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-DGi3f3vO.js → RemoteSession-C9A1R77h.js} +2 -2
  65. package/src/assets/web-panel/assets/{Reputation-jmkGFNWb.js → Reputation-BIZc6Rky.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-DP6Pcu3m.js → Row-DfIPNtuG.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-D60hSuPO.js → RssFeed-C5znFcQ1.js} +3 -3
  68. package/src/assets/web-panel/assets/{Search-0mf1BQUK.js → Search-DSs2z3OX.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-CfZtLFYV.js → Security-CV6LcrLY.js} +4 -4
  70. package/src/assets/web-panel/assets/{Services-D9zpZLWz.js → Services-cmhoX0Xj.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-D1_44eMD.js → Skeleton-tCsg2po7.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-cVFa8zkz.js → Skills-BK1KUDiy.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-Cinpzc3L.js → Sla-yRgyeA82.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-bqdiJE_P.js → SpeechSettings-c1mo1mlp.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-Dgl_SLgj.js → SyncSettings-koIAfuT3.js} +2 -2
  76. package/src/assets/web-panel/assets/{Tasks-DawWAEhy.js → Tasks-DkkreAaB.js} +1 -1
  77. package/src/assets/web-panel/assets/{Templates-Ca0nY651.js → Templates-fy5Ctf9M.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-CgaGEhLm.js → Tenant-Bw70Jf6S.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-D-9EnpRx.js → Terminal-BKh6Rx-4.js} +2 -2
  80. package/src/assets/web-panel/assets/{TimelineRenderer-CSOxDHZ6.js → TimelineRenderer-Dg54WP0c.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-BpwLZpL3.js → Tokens-BvvD5SxJ.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-DgowwBK5.js → Trigger-B7Ug9x4e.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-BZutapjE.js → Trust-BjvuMORY.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-QZPt8_Sp.js → UkeySign-CZqhkx8p.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-DlQx5UDJ.js → VideoEditing-ClR50lhX.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-ZfdTgtWA.js → Wallet-DlfppYjx.js} +4 -4
  87. package/src/assets/web-panel/assets/{WebAuthn-CAp0y_Cr.js → WebAuthn-BirgVr8B.js} +5 -5
  88. package/src/assets/web-panel/assets/{WorkflowEditor-BVITZEmr.js → WorkflowEditor-B8DP_bax.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-CC2tPaNg.js → chat-DA8AUZzL.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-31brc9CA.js → colors-C38pLGp1.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-DLMkC9C-.js → compact-item-81rtr98o.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-BMtveI6F.js → createContext-D34ixq8t.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-CHDIEOeN.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-DbD6WHW2.js → hasIn-D4bzqTrZ.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BoO4WEJB.js → index-9qoVpPt6.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CJhREOlG.js → index-B0QArPQt.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BnFLzejT.js → index-B8mkLM4x.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-Bbc2JeQG.js → index-BIP62a6r.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-CFXKbOek.js → index-BP-zkXK8.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-C-cGlCDW.js → index-BS3M5H9V.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-AaIKG60c.js → index-BXd2_LmH.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-CEXa-4zd.js → index-BlxEphl7.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DZl-UXB6.js → index-BqIhLAbA.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BOM-VFlv.js → index-BtGxuQrO.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CcVszn2b.js → index-BvD7b2IK.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-C21pdBqs.js → index-BvL_3J_n.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DJvxFyIz.js → index-COe_cOLJ.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-ALvoNTRF.js → index-CP5alL4t.js} +1 -1
  109. package/src/assets/web-panel/assets/index-CY1NURWZ.js +1 -0
  110. package/src/assets/web-panel/assets/{index-Dm0byjm1.js → index-CbhE3j5j.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-WQUO2vvK.js → index-Cd3KuNK5.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-bdaJwTXJ.js → index-CoVgZw_b.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-COHHQ9cU.js → index-CuS-UPoj.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-C7LHNw_N.js → index-D0Ih7Hwn.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Cod97Urs.js → index-D3zCCZRS.js} +3 -3
  116. package/src/assets/web-panel/assets/{index-Cwbqh0KZ.js → index-D6GfvEQG.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BKAB6-6V.js → index-DBFSo1NF.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CEwjBA9q.js → index-DFHa1HIg.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DndNnYc0.js → index-DFyWKOBD.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-3oBsXZmF.js → index-DMgnRwrK.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-C_eUKGYP.js → index-DUQLXLQM.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-CUmLMFsy.js → index-D_2VYPr1.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BPpS-lGb.js → index-DakfFZ-k.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-ep6JchNG.js → index-Ddk0e_Pi.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-D7cV0tu0.js → index-DkHqMnn3.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-jG6h22Qg.js → index-Dmo3dFHb.js} +1 -1
  127. package/src/assets/web-panel/assets/index-DokUYqpq.js +1 -0
  128. package/src/assets/web-panel/assets/{index-C1PfeI88.js → index-DvfCEJG-.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-ntVQqSML.js → index-E1OJRXs4.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-CWgfipcC.js → index-F76w1NiL.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BiRQUoyE.js → index-ZRJRItm0.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-BFRLmCR3.js → index-fI5sXMLg.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-D67kIagl.js → index-tKiC8T-4.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-D5UoBRhN.js → initDefaultProps-ByyvflIR.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-DltOw0CR.js → motion-B3FQm2By.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-_oPkk5zj.js → move-BSfns6Ef.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-EES8ROCS.js → mtc-parser-CTG1Ieri.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-B-_39x_M.js → omit-DObM_2j6.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-l6D0yJHz.js → pickAttrs-DOfelQ1t.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-DIeUK31L.js → placementArrow-CHZILds_.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-dG4EzUQo.js → responsiveObserve-REbucKSx.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-DWFzYGdH.js → slide-C6EZzD7o.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-CECdTA5H.js → statusUtils-BxkWS-g3.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-D6kP-mMK.js → styleChecker-Chklyoq6.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-BWfna_rd.js → useFlexGapSupport-CRVwKrrI.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-Bb-j6rzo.js → useFs-DV24ik_J.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-D5OIsYM4.js → usePersonalDataHub-C7vIo4Pg.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-DNpwzFTM.js → vnode-By-v5GlF.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-vNLpFQBW.js → zoom-B1w9Am3r.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/commands/agenda.js +15 -1
  152. package/src/commands/agent.js +72 -6
  153. package/src/commands/background-session.js +46 -2
  154. package/src/commands/mcp.js +46 -0
  155. package/src/commands/session.js +70 -0
  156. package/src/data/changelog.json +12 -2
  157. package/src/gateways/ws/background-agent-protocol.js +88 -14
  158. package/src/harness/jsonl-session-store.js +70 -1
  159. package/src/harness/mcp-client.js +50 -0
  160. package/src/lib/agent-authority.js +251 -0
  161. package/src/lib/agent-sandbox.js +68 -0
  162. package/src/lib/agents.js +12 -4
  163. package/src/lib/api-key-helper.js +62 -0
  164. package/src/lib/background-agent-phase.js +100 -0
  165. package/src/lib/background-agent-supervisor.js +320 -8
  166. package/src/lib/backpressure-policy.js +105 -0
  167. package/src/lib/capability-negotiation.js +212 -0
  168. package/src/lib/credential-guard.js +11 -0
  169. package/src/lib/credential-proxy.js +182 -0
  170. package/src/lib/dependency-install-policy.js +130 -0
  171. package/src/lib/event-seq-replay.js +167 -0
  172. package/src/lib/exit-codes.cjs +77 -0
  173. package/src/lib/goal-condition-engine.js +369 -0
  174. package/src/lib/headless-manifest.js +188 -0
  175. package/src/lib/hook-runner.cjs +70 -4
  176. package/src/lib/ide-context-redaction.js +130 -0
  177. package/src/lib/ide-context.js +128 -13
  178. package/src/lib/jsonl-session-store.js +2 -0
  179. package/src/lib/llm-config-defaults.js +24 -8
  180. package/src/lib/project-instructions.js +98 -1
  181. package/src/lib/project-mcp-trust.js +87 -0
  182. package/src/lib/remote-path-mapping.js +229 -0
  183. package/src/lib/schedule-planner.js +136 -0
  184. package/src/lib/sensitive-file-guard.js +51 -0
  185. package/src/lib/sub-agent-context.js +7 -3
  186. package/src/repl/bg-dashboard.js +13 -12
  187. package/src/runtime/agent-core.js +113 -10
  188. package/src/runtime/coding-agent-contract-shared.cjs +19 -2
  189. package/src/runtime/headless-runner.js +66 -7
  190. package/src/runtime/headless-stream.js +224 -12
  191. package/src/runtime/mcp-config.js +32 -1
  192. package/src/runtime/system-prompt.js +3 -0
  193. package/src/workers/background-agent-worker.js +37 -2
  194. package/src/assets/web-panel/assets/MarkdownRenderer-DexLlXfN.js +0 -1
  195. package/src/assets/web-panel/assets/OrderTableRenderer-BCQel9bD.js +0 -1
  196. package/src/assets/web-panel/assets/Projects-DvH_P2Uq.js +0 -1
  197. package/src/assets/web-panel/assets/devWarning-CB8pPOtd.js +0 -1
  198. package/src/assets/web-panel/assets/index-BhXiaiev.js +0 -1
  199. 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?} → {type:"bg-attach", bgId, hello, log}
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
- * {type:"bg-event", bgId, event} worker lifecycle events (turn-started /
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} log-file delta (500ms poll)
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
- server._send(ws, {
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
- server._send(ws, { type: "bg-log", bgId, chunk });
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
+ }
@@ -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: {