chainlesschain 0.162.159 → 0.162.160

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 (183) 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-CkRHjW8e.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-NlgSFZq5.js → ActionButton-BP3MeNi2.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-D17cOUcb.js → Analytics-CjbjQsOr.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-B4mEZcbl.js → AppLayout-i0adH1M8.js} +5 -5
  7. package/src/assets/web-panel/assets/{Artifacts-CASsmyQw.js → Artifacts-CCCG2GOU.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-DYAia2e2.js → Audit-DrsgvwHC.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-C1lvYUSu.js → BackgroundAgents-DkXNkbZM.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-B6agK6bA.js → Backup-DarTWCex.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-DM43R2vy.js → BaseInput-DaDzhcto.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-Dtdp3eXp.js → Chat-C4EzGw6V.js} +5 -5
  13. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BL5c_OQ1.js → ChatBubbleRenderer-lkavIWMS.js} +1 -1
  14. package/src/assets/web-panel/assets/{Checkbox-D2xf7uZt.js → Checkbox-gddNgaQi.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-D3j45jJL.js → Codegen-BKldFWf9.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-voPJ9fUZ.js → Col-ChR-brDs.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-DEsyTESF.js → Community-DlHtZX86.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-OHV_QXLD.js → Compact-BAhfcBpC.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-eDTJWC-J.js → Compliance-gIrKkm6m.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-BBvy-6d1.js → Cowork-BYnzLBO3.js} +2 -2
  21. package/src/assets/web-panel/assets/{Cron-Dw2eoJxu.js → Cron-ByUDIljI.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-DQHms_Bh.js → Crosschain-DOdNbHFp.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-Cyu7Id1c.js → DID-BJHaNWD2.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-sIDzkG92.js → Dashboard-Drm_tClz.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-DUW3T2ey.js → Dropdown-CA1WLzGN.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-CECELXW8.js → EmailListRenderer-Di1SKjNO.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BIZR37ov.js → FamilyGuardDashboard-44Ve7ZFe.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-BEZIQX84.js → Federation-CubPwv_N.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-BcdUMznL.js → FormItemContext-CsUFe5bU.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-DsN_fr5H.js → GenericCardRenderer-D7xRjPjZ.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-63LhjBx2.js → Git-D-Qx9dhO.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-CYtQtIcH.js → Governance-_YMYaU4n.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-BwR6UUox.js → Inference-J7Yzh-K_.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-D9lVNTUA.js → KnowledgeGraph-D37iI_Mo.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-C4q5h7Ut.js → Logs-oMTSYfGY.js} +2 -2
  36. package/src/assets/web-panel/assets/{MarkdownRenderer-DexLlXfN.js → MarkdownRenderer-BqaPmC0W.js} +1 -1
  37. package/src/assets/web-panel/assets/{Marketplace-Plksliuw.js → Marketplace-DKUqYDaF.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-DZUFfeuG.js → McpTools-BEv3Gx61.js} +3 -3
  39. package/src/assets/web-panel/assets/{Memory-NoDYt29V.js → Memory-CtA8XD2y.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-CEXGTkdh.js → MobileBridge-ug7c1mMj.js} +1 -1
  41. package/src/assets/web-panel/assets/MobileProjects-BpX5g9It.js +1 -0
  42. package/src/assets/web-panel/assets/{Mtc-DTAa0cwy.js → Mtc-WJaMs4i2.js} +5 -5
  43. package/src/assets/web-panel/assets/{MtcAudit-CVV7LnNS.js → MtcAudit-DXnK_-ru.js} +2 -2
  44. package/src/assets/web-panel/assets/{Multisig-BqqGrjt_.js → Multisig-C4Ev7726.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-C4WP8DRp.js → NLProgramming-BMFb9_zk.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-Zo4DJUVI.js → Notes-Bl93nvBX.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-DBcjRaz2.js → NotificationSettings-BN2W5cl6.js} +1 -1
  48. package/src/assets/web-panel/assets/OrderTableRenderer-Dxc9oN4o.js +1 -0
  49. package/src/assets/web-panel/assets/{Organization-BT9WPBOX.js → Organization-CAs48637.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-D6b4bZ3q.js → Overflow-Bhq27Zoc.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-Cw3OsYx-.js → P2P-D61-2542.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-C0nyiK2b.js → PdhVaultBrowser-C1j4iUxz.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-zVBZEB_C.js → Permissions-rJ2-fScM.js} +3 -3
  54. package/src/assets/web-panel/assets/{PersonalDataHub-BOspsFew.js → PersonalDataHub-DFIlTok6.js} +2 -2
  55. package/src/assets/web-panel/assets/{Pipeline-CQWfZ4XH.js → Pipeline-BjIgEWoZ.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-C2oGJ_WL.js → Privacy-BQoH5wcX.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-BZMR4qY1.js → ProjectInit-CYR_-_Ub.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-DAujoP63.js → ProjectSettings-nkatCq-9.js} +2 -2
  59. package/src/assets/web-panel/assets/Projects-SS9uZKCl.js +1 -0
  60. package/src/assets/web-panel/assets/{Providers-D_RGHM-v.js → Providers-BnKM9fsF.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-Mf6KfL2J.js → QrScannerModal-Cfe82dbz.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-5D8URoI_.js → QuickAsk-r1RbPufV.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-Df4kC8Gy.js → Recommend-CjCbM2WE.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-DGi3f3vO.js → RemoteSession-BuMGPH6V.js} +2 -2
  65. package/src/assets/web-panel/assets/{Reputation-jmkGFNWb.js → Reputation-Bmj3NY1F.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-DP6Pcu3m.js → Row-BIsNcE_1.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-D60hSuPO.js → RssFeed-BObmojD2.js} +3 -3
  68. package/src/assets/web-panel/assets/{Search-0mf1BQUK.js → Search-BRmbkQNS.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-CfZtLFYV.js → Security-_rF3DRpY.js} +4 -4
  70. package/src/assets/web-panel/assets/{Services-D9zpZLWz.js → Services-D0If9ZV4.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-D1_44eMD.js → Skeleton-o7zIksUg.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-cVFa8zkz.js → Skills-oUR7zi9u.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-Cinpzc3L.js → Sla-fqKyGi03.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-bqdiJE_P.js → SpeechSettings-B1wZ_gxx.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-Dgl_SLgj.js → SyncSettings-B3qJWYXq.js} +2 -2
  76. package/src/assets/web-panel/assets/{Tasks-DawWAEhy.js → Tasks-BH5_dUfr.js} +1 -1
  77. package/src/assets/web-panel/assets/{Templates-Ca0nY651.js → Templates-CCIVpjRP.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-CgaGEhLm.js → Tenant-2GZtTGnB.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-D-9EnpRx.js → Terminal-BlMkfs_u.js} +2 -2
  80. package/src/assets/web-panel/assets/{TimelineRenderer-CSOxDHZ6.js → TimelineRenderer-U6hi5OMc.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-BpwLZpL3.js → Tokens-C-_BerGJ.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-DgowwBK5.js → Trigger-DaKzBgN3.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-BZutapjE.js → Trust-CLvcsco3.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-QZPt8_Sp.js → UkeySign-BFn_Y1zZ.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-DlQx5UDJ.js → VideoEditing-B0V18dLT.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-ZfdTgtWA.js → Wallet-BbmP7Imu.js} +2 -2
  87. package/src/assets/web-panel/assets/{WebAuthn-CAp0y_Cr.js → WebAuthn-Dewj_0kt.js} +5 -5
  88. package/src/assets/web-panel/assets/{WorkflowEditor-BVITZEmr.js → WorkflowEditor-DOxz3Lwa.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-CC2tPaNg.js → chat-B1OIpZnh.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-31brc9CA.js → colors-CSGNVdri.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-DLMkC9C-.js → compact-item-8S0vcg0u.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-BMtveI6F.js → createContext-vZyc27TN.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-DYEZcY55.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-DbD6WHW2.js → hasIn-DWw3-UhA.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-Dm0byjm1.js → index-B4pP2ycr.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CEXa-4zd.js → index-BDHyovnK.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-C_eUKGYP.js → index-BKEmQQvR.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BOM-VFlv.js → index-BQssf0EQ.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-bdaJwTXJ.js → index-BZnauCqN.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Cwbqh0KZ.js → index-BdyokEau.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DJvxFyIz.js → index-BeBv896m.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-DndNnYc0.js → index-BjC-TrOb.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-CWgfipcC.js → index-BkWd6YL7.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-D7cV0tu0.js → index-BoIPHP4R.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CcVszn2b.js → index-BuB5P0TR.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-COHHQ9cU.js → index-BwKrTVV0.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-C1PfeI88.js → index-C6r27A-o.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-Cod97Urs.js → index-CB2MOS_8.js} +3 -3
  109. package/src/assets/web-panel/assets/{index-CEwjBA9q.js → index-CFQWZvak.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-D67kIagl.js → index-CI3slBKI.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-jG6h22Qg.js → index-CJgKP2cm.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-DZl-UXB6.js → index-CLPr2vC_.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-ntVQqSML.js → index-CbZqw7pg.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-C-cGlCDW.js → index-CfEvacz7.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BFRLmCR3.js → index-CwXBblm-.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-AaIKG60c.js → index-D1i_8oop.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BPpS-lGb.js → index-D8qXVfoV.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-WQUO2vvK.js → index-DKwl1AkQ.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CUmLMFsy.js → index-DLAvUtOH.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Bbc2JeQG.js → index-DLgkAhEE.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BKAB6-6V.js → index-DPgY5GLy.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-ALvoNTRF.js → index-Dc40pX3F.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-ep6JchNG.js → index-DcfLXAW7.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-C21pdBqs.js → index-DeCS12Mh.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-3oBsXZmF.js → index-Dg0Izipr.js} +1 -1
  126. package/src/assets/web-panel/assets/index-DmVFfcNR.js +1 -0
  127. package/src/assets/web-panel/assets/{index-BnFLzejT.js → index-DtHWHJnj.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-C7LHNw_N.js → index-Q6oyLnKC.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BoO4WEJB.js → index-dxocDVD_.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BiRQUoyE.js → index-t-q3rkVm.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-CFXKbOek.js → index-tr1x96W9.js} +1 -1
  132. package/src/assets/web-panel/assets/index-x7YyhMUa.js +1 -0
  133. package/src/assets/web-panel/assets/{index-CJhREOlG.js → index-xx5M5Rbj.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-D5UoBRhN.js → initDefaultProps-Di5YFId0.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-DltOw0CR.js → motion-BVSN0aeL.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-_oPkk5zj.js → move-DHNiaP2l.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-EES8ROCS.js → mtc-parser-xJPf3gQD.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-B-_39x_M.js → omit-C7qzUdkN.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-l6D0yJHz.js → pickAttrs-pSpjg3Le.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-DIeUK31L.js → placementArrow-cs_jL_UC.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-dG4EzUQo.js → responsiveObserve-DkS_Zs4r.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-DWFzYGdH.js → slide-B5O1Kpxo.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-CECdTA5H.js → statusUtils-BX02GnZp.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-D6kP-mMK.js → styleChecker-EmBYRftd.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-BWfna_rd.js → useFlexGapSupport-ZGzN1cU8.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-Bb-j6rzo.js → useFs-CfP7M1m7.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-D5OIsYM4.js → usePersonalDataHub-CbIPHgNh.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-DNpwzFTM.js → vnode-7xh0If_O.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-vNLpFQBW.js → zoom-DmitvTR2.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/commands/agent.js +65 -6
  152. package/src/commands/background-session.js +30 -0
  153. package/src/commands/mcp.js +46 -0
  154. package/src/commands/session.js +70 -0
  155. package/src/data/changelog.json +7 -2
  156. package/src/harness/jsonl-session-store.js +70 -1
  157. package/src/harness/mcp-client.js +50 -0
  158. package/src/lib/agent-sandbox.js +68 -0
  159. package/src/lib/agents.js +12 -4
  160. package/src/lib/api-key-helper.js +62 -0
  161. package/src/lib/background-agent-supervisor.js +32 -0
  162. package/src/lib/dependency-install-policy.js +130 -0
  163. package/src/lib/exit-codes.cjs +77 -0
  164. package/src/lib/headless-manifest.js +175 -0
  165. package/src/lib/hook-runner.cjs +70 -4
  166. package/src/lib/ide-context-redaction.js +130 -0
  167. package/src/lib/ide-context.js +89 -13
  168. package/src/lib/jsonl-session-store.js +2 -0
  169. package/src/lib/llm-config-defaults.js +24 -8
  170. package/src/lib/project-mcp-trust.js +87 -0
  171. package/src/lib/sensitive-file-guard.js +51 -0
  172. package/src/lib/sub-agent-context.js +7 -3
  173. package/src/runtime/agent-core.js +102 -6
  174. package/src/runtime/coding-agent-contract-shared.cjs +19 -2
  175. package/src/runtime/headless-runner.js +66 -7
  176. package/src/runtime/headless-stream.js +29 -1
  177. package/src/runtime/mcp-config.js +32 -1
  178. package/src/assets/web-panel/assets/MobileProjects-DnbJkbZo.js +0 -1
  179. package/src/assets/web-panel/assets/OrderTableRenderer-BCQel9bD.js +0 -1
  180. package/src/assets/web-panel/assets/Projects-DvH_P2Uq.js +0 -1
  181. package/src/assets/web-panel/assets/devWarning-CB8pPOtd.js +0 -1
  182. package/src/assets/web-panel/assets/index-BhXiaiev.js +0 -1
  183. package/src/assets/web-panel/assets/index-WpaYaE3D.js +0 -1
@@ -16,7 +16,10 @@ import {
16
16
  } from "../runtime/fallback-model.js";
17
17
  import { resolveImages, resolveVisionLlm } from "../lib/image-input.js";
18
18
  import { loadConfig } from "../lib/config-manager.js";
19
- import { normalizeAgentSandbox } from "../lib/agent-sandbox.js";
19
+ import {
20
+ normalizeAgentSandbox,
21
+ assertSandboxAvailable,
22
+ } from "../lib/agent-sandbox.js";
20
23
 
21
24
  /**
22
25
  * Resolve + validate `--add-dir` values into absolute, existing, de-duped
@@ -172,6 +175,14 @@ export function registerAgentCommand(program) {
172
175
  "--bare",
173
176
  "Everything --safe-mode disables PLUS skills, plugins and MCP/IDE auto-connect — minimal fast surface for scripted calls (explicit --mcp-config still loads; permission rules STAY active)",
174
177
  )
178
+ .option(
179
+ "--ephemeral",
180
+ "No session persistence: nothing is written to the session store (headless; a --resume id still replays history read-only)",
181
+ )
182
+ .option(
183
+ "--capabilities",
184
+ "Print a machine-readable capability manifest (JSON: protocol version, tools, permission modes, exit codes) and exit",
185
+ )
175
186
  .option(
176
187
  "--disable-slash-commands",
177
188
  "Interactive REPL: don't dispatch '/' input as slash commands — it reaches the model verbatim (/exit and /quit stay live)",
@@ -367,12 +378,31 @@ export function registerAgentCommand(program) {
367
378
  false,
368
379
  )
369
380
  .action(async (task, options, command) => {
381
+ // --capabilities: print the machine-readable manifest and exit — no
382
+ // config, no bootstrap, no network (gap-analysis 2026-07-11 快速收益 #6).
383
+ if (options.capabilities) {
384
+ const { buildAgentCapabilities } =
385
+ await import("../lib/headless-manifest.js");
386
+ console.log(JSON.stringify(buildAgentCapabilities(), null, 2));
387
+ return;
388
+ }
370
389
  // CC_API_KEY env fallback for --api-key: lets callers (IDE plugins,
371
390
  // scripts) pass the key via the environment instead of argv, where it
372
391
  // is visible to every local process (/proc/<pid>/cmdline, Win32_Process).
373
392
  // An explicit --api-key still wins.
374
393
  if (!options.apiKey && process.env.CC_API_KEY) {
375
394
  options.apiKey = process.env.CC_API_KEY;
395
+ } else if (
396
+ options.apiKey &&
397
+ process.argv.includes("--api-key") &&
398
+ !process.env.VITEST &&
399
+ !process.env.VITEST_WORKER_ID
400
+ ) {
401
+ // gap-analysis 2026-07-11: argv keys leak into shell history, process
402
+ // lists and logs — steer callers to the env/helper paths.
403
+ process.stderr.write(
404
+ "⚠️ --api-key on the command line is visible to every local process (shell history, ps). Prefer CC_API_KEY env or config llm.apiKeyHelper.\n",
405
+ );
376
406
  }
377
407
  const bypassPermissions = !!(
378
408
  options.allowDangerousBypass ||
@@ -830,6 +860,7 @@ export function registerAgentCommand(program) {
830
860
  outputStyle: options.outputStyle || null,
831
861
  strictMcpConfig: options.strictMcpConfig === true,
832
862
  replayUserMessages: options.replayUserMessages === true,
863
+ ephemeral: options.ephemeral === true,
833
864
  maxCostUsd,
834
865
  priceTable,
835
866
  chatFn: fallbackChatFn,
@@ -937,6 +968,15 @@ export function registerAgentCommand(program) {
937
968
  network: options.sandboxNetwork === true,
938
969
  settings: settingsSandbox,
939
970
  });
971
+ // Strict sandbox mode: failIfUnavailable refuses to START when the
972
+ // configured engine can't run — no silent per-command degradation.
973
+ try {
974
+ assertSandboxAvailable(agentSandbox);
975
+ } catch (err) {
976
+ process.stderr.write(`Error: ${err.message}\n`);
977
+ await _finishWorktree();
978
+ process.exit(6); // config error (see lib/exit-codes.cjs)
979
+ }
940
980
  // Resume requested onto a session with no headless (JSONL) transcript?
941
981
  // Warn instead of silently starting empty 鈥?headless resume rebuilds
942
982
  // from the JSONL store only (DB-only sessions are not replayable here).
@@ -1025,6 +1065,8 @@ export function registerAgentCommand(program) {
1025
1065
  // --settings: extra .claude/settings.json permission rules
1026
1066
  settingsFile: options.settings || null,
1027
1067
  outputStyle: options.outputStyle || null,
1068
+ // --ephemeral: no session persistence (resume replay stays read-only)
1069
+ ephemeral: options.ephemeral === true,
1028
1070
  // --max-budget-usd: hard spend cap (+ price table from config llm.pricing)
1029
1071
  maxCostUsd,
1030
1072
  priceTable,
@@ -1074,6 +1116,13 @@ export function registerAgentCommand(program) {
1074
1116
  return;
1075
1117
  }
1076
1118
 
1119
+ // --ephemeral is a headless contract (the interactive REPL owns its own
1120
+ // session lifecycle) — warn instead of silently dropping the flag.
1121
+ if (options.ephemeral) {
1122
+ process.stderr.write(
1123
+ "--ephemeral is only used in headless mode (-p / a task / piped stdin); ignoring for the interactive session.\n",
1124
+ );
1125
+ }
1077
1126
  // Reached only for an interactive session, where --image has no turn to
1078
1127
  // attach to 鈥?warn instead of silently dropping the attachment.
1079
1128
  if (images.length) {
@@ -1082,6 +1131,20 @@ export function registerAgentCommand(program) {
1082
1131
  );
1083
1132
  }
1084
1133
 
1134
+ // Strict sandbox mode also guards the interactive session: refuse to
1135
+ // start when failIfUnavailable is set and the engine can't run.
1136
+ const interactiveSandbox = normalizeAgentSandbox(options.sandbox, {
1137
+ cwd: process.cwd(),
1138
+ network: options.sandboxNetwork === true,
1139
+ settings: settingsSandbox,
1140
+ });
1141
+ try {
1142
+ assertSandboxAvailable(interactiveSandbox);
1143
+ } catch (err) {
1144
+ process.stderr.write(`Error: ${err.message}\n`);
1145
+ process.exit(6); // config error (see lib/exit-codes.cjs)
1146
+ }
1147
+
1085
1148
  const runtime = createAgentRuntimeFactory().createAgentRuntime({
1086
1149
  model: options.model,
1087
1150
  thinking,
@@ -1103,11 +1166,7 @@ export function registerAgentCommand(program) {
1103
1166
  parkOnExit: options.parkOnExit, // false when --no-park-on-exit
1104
1167
  bundlePath: options.bundle || null,
1105
1168
  additionalDirectories,
1106
- sandbox: normalizeAgentSandbox(options.sandbox, {
1107
- cwd: process.cwd(),
1108
- network: options.sandboxNetwork === true,
1109
- settings: settingsSandbox,
1110
- }),
1169
+ sandbox: interactiveSandbox,
1111
1170
  autoCheckpoint,
1112
1171
  // --vim: start the REPL in vim-mode editing (also CC_VIM=1 or /vim).
1113
1172
  vimMode: options.vim === true,
@@ -597,6 +597,36 @@ export function registerBackgroundSessionCommands(program) {
597
597
  }
598
598
  });
599
599
 
600
+ // daemon rm — record cleanup (gap-analysis 2026-07-11 P0 后台 Supervisor).
601
+ daemon
602
+ .command("rm <id>")
603
+ .description(
604
+ "Remove a finished background agent's record + log (running agents need --force; the conversation session itself is untouched)",
605
+ )
606
+ .option("--force", "Stop a still-running agent first, then remove")
607
+ .option("--keep-log", "Keep the log file, remove only the state record")
608
+ .option("--json", "Output as JSON")
609
+ .action(async (id, options) => {
610
+ try {
611
+ const { removeBackgroundAgent } = await loadSupervisor();
612
+ const result = removeBackgroundAgent(id, {
613
+ force: options.force === true,
614
+ keepLog: options.keepLog === true,
615
+ });
616
+ if (options.json) {
617
+ console.log(JSON.stringify(result, null, 2));
618
+ return;
619
+ }
620
+ logger.log(
621
+ chalk.green(`Removed background agent ${result.id}`) +
622
+ chalk.gray(` (was ${result.status})`),
623
+ );
624
+ } catch (error) {
625
+ logger.error(chalk.red(error.message));
626
+ process.exitCode = 1;
627
+ }
628
+ });
629
+
600
630
  daemon
601
631
  .command("stop [id]")
602
632
  .description("Stop one background agent, or all running agents with --any")
@@ -999,6 +999,52 @@ export function registerMcpCommand(program) {
999
999
  });
1000
1000
 
1001
1001
  // mcp scaffold — generate a boilerplate MCP server project
1002
+ mcp
1003
+ .command("trust-project")
1004
+ .description(
1005
+ "Re-trust the project .mcp.json after reviewing a content change (records its fingerprint for --project-mcp runs)",
1006
+ )
1007
+ .option("--json", "Output as JSON")
1008
+ .action(async (options) => {
1009
+ try {
1010
+ const fsMod = await import("node:fs");
1011
+ const { projectRootBase } = await import("../lib/project-root.cjs");
1012
+ const { recordProjectMcpTrust, projectMcpFingerprint } =
1013
+ await import("../lib/project-mcp-trust.js");
1014
+ const cwd = process.cwd();
1015
+ const candidates = [path.join(cwd, ".mcp.json")];
1016
+ const root = projectRootBase(cwd, { fs: fsMod.default, path });
1017
+ if (root) candidates.push(path.join(root, ".mcp.json"));
1018
+ const results = [];
1019
+ for (const file of [...new Set(candidates)]) {
1020
+ if (!fsMod.existsSync(file)) continue;
1021
+ const content = fsMod.readFileSync(file, "utf-8");
1022
+ const ok = recordProjectMcpTrust(file, content);
1023
+ results.push({
1024
+ file,
1025
+ trusted: ok,
1026
+ fingerprint: projectMcpFingerprint(content).slice(0, 16),
1027
+ });
1028
+ }
1029
+ if (options.json) {
1030
+ console.log(JSON.stringify(results, null, 2));
1031
+ return;
1032
+ }
1033
+ if (results.length === 0) {
1034
+ console.log("No .mcp.json found in this project.");
1035
+ return;
1036
+ }
1037
+ for (const r of results) {
1038
+ console.log(
1039
+ `${r.trusted ? "Trusted" : "FAILED to record"} ${r.file} (${r.fingerprint})`,
1040
+ );
1041
+ }
1042
+ } catch (err) {
1043
+ console.error(`Failed: ${err.message}`);
1044
+ process.exitCode = 1;
1045
+ }
1046
+ });
1047
+
1002
1048
  mcp
1003
1049
  .command("scaffold <name>")
1004
1050
  .description("Scaffold a new MCP server project (stdio or http+sse)")
@@ -534,6 +534,76 @@ export function registerSessionCommand(program) {
534
534
  }
535
535
  });
536
536
 
537
+ // session rename — named sessions (gap-analysis 2026-07-11 快速收益 #5).
538
+ // Appends a session_rename event (hash chain stays intact); last one wins.
539
+ session
540
+ .command("rename")
541
+ .description("Rename a headless (JSONL) session — shows up in session list")
542
+ .argument("<id>", "Session ID")
543
+ .argument("<title...>", "New title")
544
+ .option("--json", "Output as JSON")
545
+ .action(async (id, title, options) => {
546
+ try {
547
+ const { renameSession } =
548
+ await import("../harness/jsonl-session-store.js");
549
+ const result = renameSession(
550
+ id,
551
+ Array.isArray(title) ? title.join(" ") : title,
552
+ );
553
+ if (options.json) {
554
+ console.log(JSON.stringify(result, null, 2));
555
+ return;
556
+ }
557
+ logger.success(`Renamed session ${result.id}`);
558
+ logger.info(` ${result.title}`);
559
+ } catch (err) {
560
+ logger.error(`Failed: ${err.message}`);
561
+ process.exitCode = 1;
562
+ }
563
+ });
564
+
565
+ // session prune — retention sweep (gap-analysis 2026-07-11 P1 "保留期限").
566
+ session
567
+ .command("prune")
568
+ .description(
569
+ "Delete JSONL sessions whose last activity is older than a cutoff (newest N always kept)",
570
+ )
571
+ .requiredOption(
572
+ "--older-than <days>",
573
+ "Delete sessions idle for more than this many days",
574
+ )
575
+ .option("--keep <n>", "Always keep the newest N sessions (default 10)")
576
+ .option("--dry-run", "List what would be deleted without deleting")
577
+ .option("--json", "Output as JSON")
578
+ .action(async (options) => {
579
+ try {
580
+ const { pruneJsonlSessions } =
581
+ await import("../harness/jsonl-session-store.js");
582
+ const result = pruneJsonlSessions({
583
+ olderThanDays: Number(options.olderThan),
584
+ keep: options.keep != null ? Number(options.keep) : undefined,
585
+ dryRun: options.dryRun === true,
586
+ });
587
+ if (options.json) {
588
+ console.log(JSON.stringify(result, null, 2));
589
+ return;
590
+ }
591
+ const verb = result.dryRun ? "Would delete" : "Deleted";
592
+ logger.info(
593
+ `${verb} ${result.deleted.length} of ${result.scanned} session(s).`,
594
+ );
595
+ for (const id of result.deleted.slice(0, 50)) {
596
+ logger.info(` - ${id}`);
597
+ }
598
+ if (result.deleted.length > 50) {
599
+ logger.info(` … and ${result.deleted.length - 50} more`);
600
+ }
601
+ } catch (err) {
602
+ logger.error(`Failed: ${err.message}`);
603
+ process.exitCode = 1;
604
+ }
605
+ });
606
+
537
607
  session
538
608
  .command("migrate")
539
609
  .description("Migrate legacy JSON session files to JSONL")
@@ -3,7 +3,7 @@
3
3
  "releases": [
4
4
  {
5
5
  "productVersion": "Unreleased",
6
- "cliVersion": "0.162.159",
6
+ "cliVersion": "0.162.160",
7
7
  "cliVersions": [
8
8
  "0.4.46",
9
9
  "0.4.51",
@@ -17,11 +17,16 @@
17
17
  "0.162.156",
18
18
  "0.162.157",
19
19
  "0.162.158",
20
- "0.162.159"
20
+ "0.162.159",
21
+ "0.162.160"
21
22
  ],
22
23
  "date": null,
23
24
  "title": "",
24
25
  "sections": [
26
+ {
27
+ "heading": "Added — cc CLI 0.162.160:运行时安全与确定性 8 批(沙箱严格模式 + 依赖/凭据安全 + 确定性 Headless + Subagent 契约 + MCP 生命周期 + Hooks 硬化)(CLI-only npm 发版)",
28
+ "body": "> `chainlesschain` 0.162.159 → **0.162.160** 发 npm `latest`(经 `npm-publish.yml`,`--provenance --access public`)。纯 `packages/cli/src` 增量;未触 `pdh/lib` → 无 Android cc bundle rollover / 无 USR_VERSION 改动。命令面**只加子命令**(`cc session rename/prune`、`cc daemon rm`、`cc mcp trust-project`),**顶层命令数 175 不变**(manifest 无需重生)。对照 `docs/CLAUDE_CODE_CLI_GAP_ANALYSIS.md`(docs 根)审计确认仍缺的 8 项全落地(commit 链 `c4533ad848..4c2e297754`)。\n>\n> ⚠️ **行为变更(升级前须知)**:① `run_code` 的 pip 依赖**默认不再自动安装**(改 opt-in,见下);② agent 脚本默认写 OS 临时目录、不再落项目根(`persist:true` 才进项目);③ headless/agent **退出码细化**——最大轮次→3、预算超限→4、模型错误→5、配置错误→6(成功仍 0、通用失败仍 1、hook block 仍 2、SIGINT/SIGTERM 仍 130/143)。依赖固定 exit≠0 判失败的脚本不受影响;依赖\"失败必为 1\"的脚本需放宽。\n\n- **确定性 Headless(A)**:新增 `--ephemeral`(resume 只读回放、零落盘);init 事件补 `protocol_version`/`session_persistence`/`loaded_sources`/`policy_digest`/`tools_hash`/`isolation_level`(消费者可判定回放确定性与工具/策略指纹);退出码分类(`lib/exit-codes.cjs`);`cc agent --capabilities` 打印机读能力清单(协议版本、agent 工具集、权限模式、退出码、特性开关)后退出。\n- **依赖安装安全(B)**:`run_code` 的 pip 自动安装**默认关闭**——经 settings `runCode.autoInstall` 或 `CC_RUN_CODE_AUTO_INSTALL` 显式开启,且受 `runCode.installAllowlist`(PEP 503 归一化包名)约束,每次安装写审计 JSONL(`~/.chainlesschain/audit/`,`CC_AUDIT_DIR` 可覆盖);agent 脚本默认写 OS 临时目录(`persist:true` 才进项目根)。系统提示词/工具契约文案已同步。\n- **凭据卫生(C)**:config `llm.apiKeyHelper`——从外部命令(→ OS 凭据库)解析 API Key,5 分钟缓存,helper 失败 **fail-closed 并告警一次**(返回 null,绝不静默替换成别的凭据);argv `--api-key` 明文传参现给出告警(进程表/shell 历史泄露面)。\n- **沙箱严格模式(D)**:`failIfUnavailable` 此前只是声明、从不执行——现在启动期探测沙箱引擎(docker `version` / bwrap `--version`),不可用即拒绝启动(退出码 6),headless 与交互双路均生效;`isolation_level`(os-sandbox / container / policy-only)透出到 init 事件与 `sandboxSummary`,不再静默降级为\"只有工具规则\"。\n- **Subagent 契约(E)**:`spawn_sub_agent` 与 `.claude/agents/*.md` frontmatter 双入口新增 `disallowedTools`(从 allowedTools 减去)、`maxTurns`、`isolation:worktree`。**顺手修出一个真 bug**:`SubAgentContext._runCore` 早已算出工具白名单却从未传给 agentLoop——子代理在 LLM 面一直看到全量工具,白名单形同虚设(修=`enabledToolNames` 透传)。\n- **会话运营(F)**:`cc session rename <id> <title>`(append `session_rename` 事件,哈希链不破)+ `cc session prune --older-than <days> [--keep N] [--dry-run]`(按最后活动保留期清理,默认保最新 10)+ `cc daemon rm <id> [--force] [--keep-log]`(终态直接移除、运行中需 `--force`)。\n- **MCP 生命周期(G)**:mcp-client 处理 `notifications/tools/list_changed` + `notifications/resources/list_changed`——合并式重拉服务器工具/资源清单(刻意**不动 in-flight LLM 工具数组**,保 tool-search 缓存稳定性,追加式更新);项目 `.mcp.json` 首用记 sha256 指纹,文件变更后 **fail-closed 拒绝加载**,需 `CC_PROJECT_MCP_TRUST=1` 或 `cc mcp trust-project` 显式重信任(防被换过的项目配置静默拉起新 MCP server)。\n- **Hooks 硬化(H)**:每个 hook 的 stdin payload 打 `schema_version`(=1,additive 变更保号、破坏性 reshape 才 bump);连续失败熔断——同一 hook 命令连续 3 次非阻塞失败(spawn error / 超时 / 非 2 非零退出)开闸,60s 冷却后半开试跑一次,成功或真 block/ask 决策关闸(`CC_HOOK_BREAKER_THRESHOLD=0` 关闭、`CC_HOOK_BREAKER_COOLDOWN_MS` 可调);坏 hook 不再每轮拖慢 agent。"
29
+ },
25
30
  {
26
31
  "heading": "Added — cc CLI 0.162.159:Claude-Code 平价 gap-analysis 14 项收口(cloud handoff + /tui + /voice + /fast + plugin 治理 + session SQLite 索引)(CLI-only npm 发版)",
27
32
  "body": "> `chainlesschain` 0.162.158 → **0.162.159** 发 npm `latest`(经 `npm-publish.yml`,`--provenance --access public`)。纯 `packages/cli/src` 增量;未触 `pdh/lib` → 无 Android cc bundle rollover / 无 USR_VERSION 改动。顶层命令数 **173 → 175**(新增 `cc cloud` + `cc routine`;`cc session index` 是 `session` 子命令、`/tui` `/voice` `/fast` 为 REPL-only 均不计数)。对照 `docs/internal/cli-claude-code-gap-analysis-2026-07-11.md` 14 项全落地。\n\n- **`cc cloud`(P1#7)自托管 cloud handoff**:`cc cloud run/status/attach/list`——`git bundle`(base64)当前分支上传到配置的私有 runner(`cloud.baseUrl`/`cloud.token` 或 `CC_CLOUD_URL`/`CC_CLOUD_TOKEN`),runner 完成后 `cc cloud attach` 轮询到终态并 `git apply --3way` 回流 patch、落 plan/artifacts、显示 PR——无 Anthropic 云依赖。本地 job 台账 `~/.chainlesschain/cloud-jobs/`(穿越守卫),不落地的 patch 存旁不失败。\n- **`/tui`(P1#10)全屏无闪烁视图**:`/tui fullscreen` 进备用屏缓冲(`?1049h`,保滚动历史)+ 尾锚定视口窗口化 + diff 重绘(仅改动行光标寻址重写,无整屏清屏→无闪烁);`/tui default` 退回流式滚动;`CC_NO_FLICKER=1` 启动即全屏;屏幕阅读器模式强制 default。持久化 `cli.tuiMode`。\n- **`/voice`(P2#11)语音听写**:`/voice hold|tap|off`(hold=按住说话,tap=切换);严格 local-first STT 优先级(本地 Whisper > 系统 STT > 云,云需 `voice.allowCloud` 显式开);SSH/无 TTY/CI/`CC_NO_VOICE` 环境清晰降级不假装监听;真实采集是主机绑定 DI seam。\n- **`/fast`(P2#12)延迟档**:`/fast on|off|status`——下一轮最小化推理(thinking off)+ 除非用户 `/model` 钉过模型否则换到 provider 的低延迟模型(anthropic→haiku-4-5、openai→4o-mini、deepseek/dashscope/mistral/gemini),始终显示成本/质量权衡。持久化 `cli.fastMode`。\n- **插件治理(P2#13)三项**:manifest `dependencies`(插件名→semver 区间 + 保留 `host`/`cc` 约束 CLI 版本)经 `cc plugin validate` 校验缺失/越界依赖;LSP 注册表改**每 languageId 有序候选表**——一个插件 LSP server 二进制缺失不再遮蔽 builtin(回退到下一候选/内建,扩展名不再\"变黑\");`isPluginInUse` 把 LSP/MCP/hooks/monitors 被动组件视为 in-use,不再被\"未调用即弃用\"误判。\n- **`cc session index`(P2#14)外部 session 存储**:JSONL 仍是权威源,新增 SQLite 索引(`~/.chainlesschain/session-index.db`)——增量按 mtime+size 同步 + 内容/标题搜索(LIKE 转义通配符)+ stats + `--rebuild`,取代逐文件解析(1072 真 session 验过);可插拔 off-box mirror(`fs`/`http` 驱动,S3/WebDAV/Redis 同接口待配),`session.mirror.{kind,dir|baseUrl,token}` 配置,加密/访问控制由驱动独立负责。\n- **P0/P1 早前提交同随本版发出**:`cc bg view` 交互式 agent 面板(P0#1)+ background supervisor 稳定性加固(P0#2)+ `/doctor` 可修复 checkup + `/checkup` 别名(P0#3)+ transcript hash-chain 防篡改 `cc session verify`(P0#4)+ Inbound Channels(webhook/telegram allowlist,P0#5)+ computer-use MCP 显式开启(P1#6)+ Routines(cron/once/webhook/github,P1#8)+ PR/session linking(P1#9)。"
@@ -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`
@@ -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: {
package/src/lib/agents.js CHANGED
@@ -8,10 +8,13 @@
8
8
  * `review/security.md` is the agent `review:security`.
9
9
  *
10
10
  * Frontmatter (all optional):
11
- * name override the filename-derived name
12
- * description one-line summary (when to use this agent)
13
- * tools allow-list — comma string or YAML array; omit = inherit all
14
- * model model override for runs of this agent
11
+ * name override the filename-derived name
12
+ * description one-line summary (when to use this agent)
13
+ * tools allow-list — comma string or YAML array; omit = inherit all
14
+ * disallowedTools deny-list — removed from whatever the allow-list resolves to
15
+ * model model override for runs of this agent
16
+ * maxTurns per-run iteration cap for this agent (positive integer)
17
+ * isolation "worktree" → run in a fresh git worktree (gap 2026-07-11)
15
18
  *
16
19
  * Project scope shadows personal on a name clash. Discovery + parse are pure
17
20
  * (inject fs/path/home) so the whole thing is unit-testable.
@@ -132,13 +135,18 @@ export function parseAgentFile(file, scope, opts = {}) {
132
135
  return null;
133
136
  }
134
137
  const { data, body } = parseFrontmatter(content);
138
+ const maxTurns = Number(data.maxTurns);
135
139
  return {
136
140
  file,
137
141
  scope,
138
142
  name: data.name || null, // resolved against the path in discoverAgents
139
143
  description: data.description || "",
140
144
  tools: normalizeTools(data.tools),
145
+ disallowedTools: normalizeTools(data.disallowedTools),
141
146
  model: data.model || null,
147
+ maxTurns:
148
+ Number.isFinite(maxTurns) && maxTurns > 0 ? Math.floor(maxTurns) : null,
149
+ isolation: data.isolation === "worktree" ? "worktree" : null,
142
150
  systemPrompt: body || "",
143
151
  };
144
152
  }