chainlesschain 0.162.125 → 0.162.127

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 (192) hide show
  1. package/package.json +2 -2
  2. package/src/assets/web-panel/assets/{AIOps-DcqNhZhA.js → AIOps-CsHJh6tT.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BigtmPoq.js → ActionButton-ClDr78Wl.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-1J_X_HF4.js → Analytics-CQQBX5mv.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-DlcDG_a_.js → AppLayout-DIofBNeG.js} +5 -5
  6. package/src/assets/web-panel/assets/{Audit-Do-y8_3w.js → Audit-d8pxGJLK.js} +1 -1
  7. package/src/assets/web-panel/assets/{Backup-S2Df-50Y.js → Backup-ChZb31ek.js} +1 -1
  8. package/src/assets/web-panel/assets/{BaseInput-D5kgWJfk.js → BaseInput-CQsSXb5v.js} +1 -1
  9. package/src/assets/web-panel/assets/{Chat-aUcp3kN1.js → Chat-C_mjILx8.js} +6 -6
  10. package/src/assets/web-panel/assets/ChatBubbleRenderer-B545kWla.js +1 -0
  11. package/src/assets/web-panel/assets/{Checkbox-DTg1U7Xs.js → Checkbox-CWFmVuo8.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen-YO9ZZ4Ky.js → Codegen-DwicePOQ.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-BHonE9fU.js → Col-B36SnRdL.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-DA2AlEFh.js → Community-C_sZ2HhK.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-DULxLRNg.js → Compact-B2jzDcUl.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-BZqfuuZL.js → Compliance-BD58_IHe.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-CJe3vyMS.js → Cowork-VIBdGc4D.js} +2 -2
  18. package/src/assets/web-panel/assets/{Cron-CeV8AtRu.js → Cron-CDTVWUmV.js} +2 -2
  19. package/src/assets/web-panel/assets/{Crosschain-CAg66zS5.js → Crosschain-D5DkGNKU.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-Dtup5JZm.js → DID-B23ae8PQ.js} +2 -2
  21. package/src/assets/web-panel/assets/{Dashboard-DAR_8PNy.js → Dashboard-DTG5MsSx.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dropdown-K5bTaCYN.js → Dropdown-D35RRnMZ.js} +1 -1
  23. package/src/assets/web-panel/assets/{EmailListRenderer-C890uErx.js → EmailListRenderer-DRvY2nPk.js} +1 -1
  24. package/src/assets/web-panel/assets/{FamilyGuardDashboard-B-Tj_8yM.js → FamilyGuardDashboard-BT_eESkk.js} +1 -1
  25. package/src/assets/web-panel/assets/{Federation-C6WZ98ni.js → Federation-CxX9F7IL.js} +1 -1
  26. package/src/assets/web-panel/assets/{FormItemContext-D-LTfGWd.js → FormItemContext-C1xDz6D8.js} +1 -1
  27. package/src/assets/web-panel/assets/{GenericCardRenderer-C80DK4W7.js → GenericCardRenderer-CjBOAXcO.js} +1 -1
  28. package/src/assets/web-panel/assets/{Git-Cjvvv3zW.js → Git-I9I9NkPO.js} +2 -2
  29. package/src/assets/web-panel/assets/{Governance-C0BND77H.js → Governance-BiigWxQW.js} +1 -1
  30. package/src/assets/web-panel/assets/{Inference-BL9kTtSu.js → Inference-C0KrSCgh.js} +1 -1
  31. package/src/assets/web-panel/assets/{KnowledgeGraph-CnVTBmJf.js → KnowledgeGraph-B1Nd54ow.js} +1 -1
  32. package/src/assets/web-panel/assets/{Logs-CbCbg7K6.js → Logs-CeOtvYIo.js} +2 -2
  33. package/src/assets/web-panel/assets/{Marketplace-CG5On-fH.js → Marketplace-CRhgvAVQ.js} +1 -1
  34. package/src/assets/web-panel/assets/{McpTools-AYYDZZK7.js → McpTools-BIJNah-H.js} +5 -5
  35. package/src/assets/web-panel/assets/{Memory-tMWbMDQq.js → Memory-D6gsbo4e.js} +2 -2
  36. package/src/assets/web-panel/assets/{MobileBridge-BAOsf4wB.js → MobileBridge-DDkIKA6U.js} +1 -1
  37. package/src/assets/web-panel/assets/{MobileProjects-BT-2komQ.js → MobileProjects-BBkI02Lf.js} +1 -1
  38. package/src/assets/web-panel/assets/{Mtc-BFwfC63n.js → Mtc-BziHK7HS.js} +5 -5
  39. package/src/assets/web-panel/assets/{MtcAudit-DYG3CWdH.js → MtcAudit-CWXjbkeF.js} +2 -2
  40. package/src/assets/web-panel/assets/{Multisig-KJwd0yWT.js → Multisig-9jNF_VWa.js} +3 -3
  41. package/src/assets/web-panel/assets/{NLProgramming-BmL5dNWm.js → NLProgramming-BUdF7I2G.js} +1 -1
  42. package/src/assets/web-panel/assets/{Notes-DmkaVaMc.js → Notes-D5ls1r1T.js} +3 -3
  43. package/src/assets/web-panel/assets/{NotificationSettings-tmVQszDZ.js → NotificationSettings-B2WX_TMX.js} +1 -1
  44. package/src/assets/web-panel/assets/{OrderTableRenderer-BtrR7anf.js → OrderTableRenderer-COSzdTcE.js} +1 -1
  45. package/src/assets/web-panel/assets/{Organization-pppktQyW.js → Organization-tzWIgnbM.js} +4 -4
  46. package/src/assets/web-panel/assets/{Overflow-D3lBoQDA.js → Overflow-Ca1gp_3S.js} +1 -1
  47. package/src/assets/web-panel/assets/{P2P-BgE2qGlZ.js → P2P-XnfDfRK9.js} +2 -2
  48. package/src/assets/web-panel/assets/{PdhVaultBrowser-D26Bz5gS.js → PdhVaultBrowser-23Susb25.js} +3 -3
  49. package/src/assets/web-panel/assets/{Permissions-BR8no2Xe.js → Permissions-CnkhYPqt.js} +4 -4
  50. package/src/assets/web-panel/assets/{PersonalDataHub-DabbyQEF.js → PersonalDataHub-CZ1UasJc.js} +2 -2
  51. package/src/assets/web-panel/assets/{Pipeline-n4sNpEz8.js → Pipeline-DC1oC84K.js} +1 -1
  52. package/src/assets/web-panel/assets/{Privacy-CGNp4n8M.js → Privacy-CdqayaCE.js} +1 -1
  53. package/src/assets/web-panel/assets/{ProjectInit-CAVA5VsX.js → ProjectInit-DS7NbGQL.js} +2 -2
  54. package/src/assets/web-panel/assets/{ProjectSettings-GCgkfM7A.js → ProjectSettings-fdzwup3n.js} +2 -2
  55. package/src/assets/web-panel/assets/{Projects-BYK17p52.js → Projects-FvodeZob.js} +1 -1
  56. package/src/assets/web-panel/assets/{Providers-DpUT5W-d.js → Providers-IxYyzeSI.js} +1 -1
  57. package/src/assets/web-panel/assets/{QuickAsk-ZJxTb7vi.js → QuickAsk-BAtFET-B.js} +1 -1
  58. package/src/assets/web-panel/assets/{Recommend-CIzFRDk1.js → Recommend-DCNRV9Vr.js} +1 -1
  59. package/src/assets/web-panel/assets/{Reputation-lQ_WDNZn.js → Reputation-lg2JmIai.js} +1 -1
  60. package/src/assets/web-panel/assets/{Row-1MEtrgtF.js → Row-BAhyDuri.js} +1 -1
  61. package/src/assets/web-panel/assets/{RssFeed-SSgcPPl4.js → RssFeed-BA_3issD.js} +2 -2
  62. package/src/assets/web-panel/assets/{Search-CcTMOGNY.js → Search-CJheUJDl.js} +1 -1
  63. package/src/assets/web-panel/assets/{Security-BA1KMaDx.js → Security-D2VczQDD.js} +3 -3
  64. package/src/assets/web-panel/assets/{Services-_aj7czZn.js → Services-7ZFAzqVR.js} +2 -2
  65. package/src/assets/web-panel/assets/{Skeleton-BWgg_0Zr.js → Skeleton-CsBoTasD.js} +1 -1
  66. package/src/assets/web-panel/assets/{Skills-C2ZIziYM.js → Skills-DT_j_gj0.js} +1 -1
  67. package/src/assets/web-panel/assets/{Sla-CYxp4axY.js → Sla-C9D5geJx.js} +1 -1
  68. package/src/assets/web-panel/assets/{SpeechSettings-C7Csp1jV.js → SpeechSettings-CWCNVW42.js} +1 -1
  69. package/src/assets/web-panel/assets/{SyncSettings-svGeswiw.js → SyncSettings-TYCbf2kk.js} +2 -2
  70. package/src/assets/web-panel/assets/{Tasks-ClISj6oK.js → Tasks-CbRoXu1G.js} +1 -1
  71. package/src/assets/web-panel/assets/{Templates-CmO-Fp7r.js → Templates-CxBjIrkJ.js} +1 -1
  72. package/src/assets/web-panel/assets/{Tenant-a3Ac3lxz.js → Tenant-NKVyFsPM.js} +1 -1
  73. package/src/assets/web-panel/assets/{Terminal-DyJIRh81.js → Terminal-D5aAL5_H.js} +2 -2
  74. package/src/assets/web-panel/assets/TimelineRenderer-CD1JE0Rb.js +1 -0
  75. package/src/assets/web-panel/assets/{Tokens-DJW0KqV4.js → Tokens-CdepBSYe.js} +1 -1
  76. package/src/assets/web-panel/assets/{Trigger-0HeTi4r6.js → Trigger-Br3n_PEh.js} +1 -1
  77. package/src/assets/web-panel/assets/{Trust-aFym34eo.js → Trust-CSac0pMf.js} +1 -1
  78. package/src/assets/web-panel/assets/{UkeySign-CYhszHJv.js → UkeySign-BiWo43zc.js} +1 -1
  79. package/src/assets/web-panel/assets/{VideoEditing-BbJxPF9X.js → VideoEditing-Dj50GuoF.js} +1 -1
  80. package/src/assets/web-panel/assets/{Wallet-DDthEwiu.js → Wallet-CQK_g4_U.js} +4 -4
  81. package/src/assets/web-panel/assets/{WebAuthn-DQ6McYPg.js → WebAuthn-D8mBl1NC.js} +3 -3
  82. package/src/assets/web-panel/assets/{WorkflowEditor-CnF8zRsi.js → WorkflowEditor-CHeS_vab.js} +1 -1
  83. package/src/assets/web-panel/assets/{chat-Dzkx2gTP.js → chat-CbeOf_lH.js} +1 -1
  84. package/src/assets/web-panel/assets/{colors-35T51Oo5.js → colors-CTFiyfed.js} +1 -1
  85. package/src/assets/web-panel/assets/{compact-item-TtzNrlu1.js → compact-item-CZsh8FXE.js} +1 -1
  86. package/src/assets/web-panel/assets/{createContext-Y1LkWrYb.js → createContext-UPhnXsap.js} +1 -1
  87. package/src/assets/web-panel/assets/devWarning-BFRqfgsk.js +1 -0
  88. package/src/assets/web-panel/assets/{hasIn-BOYw1BgS.js → hasIn-CCqQtSY-.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-D2od7Bgx.js → index-1d4pat4L.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-NM9Oke2k.js → index-86dmMEZY.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-Dof7lWA_.js → index-B-OcSTNA.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-D7dCBw_M.js → index-B1tylGKq.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-CqhwG1ox.js → index-BAcem_Qs.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-CZ16GlOs.js → index-BKADY9Iw.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-CPGNc2tF.js → index-Be0uH7m1.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DAizH273.js → index-Bga0UuWA.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-DFhlelzN.js → index-Blgzobgm.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-D6tG4B25.js → index-BwPULPuj.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-DjeUZxE5.js → index-CIQVlE9u.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CLqC2sRT.js → index-CMi91z74.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-hyqEKkBT.js → index-CNteMctn.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Cs82BHAj.js → index-CQHQiYAO.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-zF77jnI2.js → index-CVKFSFkX.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-G4ClSmJc.js → index-CctoAd9I.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DhMSOd_2.js → index-CdiuRSqw.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BWgNn9As.js → index-Cy83zglX.js} +3 -3
  107. package/src/assets/web-panel/assets/{index-Bw1iOGhM.js → index-DBJjqjxL.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-D4BgCBa3.js → index-DFbyobqi.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-BKRlWHUp.js → index-DcJxrM1F.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-B7aKAZzS.js → index-DgXn2sWa.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-D47robkX.js → index-Dkn1r09O.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CVrUs6rf.js → index-DmScDPXc.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-Bs69SI96.js → index-DpTaXoo4.js} +1 -1
  114. package/src/assets/web-panel/assets/index-DqADH38Q.js +1 -0
  115. package/src/assets/web-panel/assets/{index-BxWUEFKy.js → index-DvaDSd9k.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BtlJWaSP.js → index-LIFKNtNq.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BGPEaeB1.js → index-LyzbfmcN.js} +1 -1
  118. package/src/assets/web-panel/assets/index-Pbx8iJt_.js +1 -0
  119. package/src/assets/web-panel/assets/{index-KgXrlfNF.js → index-WGmri9tu.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-XIuZV9by.js → index-Yd1x-xhs.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-Dhfw-BXs.js → index-_OOipSkX.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-Dx0hIfC-.js → index-dwBroN51.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-C7Wt5feN.js → index-e6tj_vqt.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CdyFg4FI.js → index-lOxvi-gf.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-8jffdb6b.js → index-qTrskpW-.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DmkG479D.js → index-uH-glcG6.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BSpFe6j5.js → index-z5pyzCXj.js} +1 -1
  128. package/src/assets/web-panel/assets/{initDefaultProps-DD5y5msp.js → initDefaultProps-K95PVa57.js} +1 -1
  129. package/src/assets/web-panel/assets/{motion-BuoutMia.js → motion-DwTSyvvW.js} +1 -1
  130. package/src/assets/web-panel/assets/{move-DkpjqOXg.js → move-BuRSogkN.js} +1 -1
  131. package/src/assets/web-panel/assets/{mtc-parser-BmGJD-v-.js → mtc-parser-twEnPl1V.js} +1 -1
  132. package/src/assets/web-panel/assets/{omit-DftUE0Sz.js → omit-CdPhebkX.js} +1 -1
  133. package/src/assets/web-panel/assets/{pickAttrs-DDRb9FIu.js → pickAttrs-CGcIpdPi.js} +1 -1
  134. package/src/assets/web-panel/assets/{placementArrow-DPgtxOOD.js → placementArrow-DixkX9VV.js} +1 -1
  135. package/src/assets/web-panel/assets/{responsiveObserve-BQFRzFeZ.js → responsiveObserve-C74t3rB5.js} +1 -1
  136. package/src/assets/web-panel/assets/{slide-DQ4lmUSz.js → slide-TcNt6K6l.js} +1 -1
  137. package/src/assets/web-panel/assets/{statusUtils-C1TR4ZiQ.js → statusUtils-mpTN9mrS.js} +1 -1
  138. package/src/assets/web-panel/assets/{styleChecker-B-Suj978.js → styleChecker-Bwg8zmIj.js} +1 -1
  139. package/src/assets/web-panel/assets/{useFlexGapSupport-Q7bDZ3EI.js → useFlexGapSupport-Cz4mN4sW.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFs-DHjRLyQH.js → useFs-DF9zRfbI.js} +1 -1
  141. package/src/assets/web-panel/assets/{usePersonalDataHub-lQOvCvCr.js → usePersonalDataHub-BcQ4O2UK.js} +1 -1
  142. package/src/assets/web-panel/assets/{vnode-Ie0g4-p3.js → vnode-juM2dgAV.js} +1 -1
  143. package/src/assets/web-panel/assets/{zoom-CHP0YEoH.js → zoom-c4aJj-vC.js} +1 -1
  144. package/src/assets/web-panel/index.html +1 -1
  145. package/src/commands/agent.js +41 -2
  146. package/src/commands/compliance.js +3 -1
  147. package/src/commands/review.js +47 -17
  148. package/src/commands/session.js +11 -2
  149. package/src/gateways/ws/message-dispatcher.js +145 -165
  150. package/src/gateways/ws/ws-session-gateway.js +39 -5
  151. package/src/harness/jsonl-session-store.js +39 -12
  152. package/src/harness/worktree-isolator.js +5 -0
  153. package/src/lib/agent-core.js +1 -0
  154. package/src/lib/audit-logger.js +3 -1
  155. package/src/lib/chat-core.js +5 -2
  156. package/src/lib/chat-intent-service.js +68 -21
  157. package/src/lib/config-manager.js +25 -1
  158. package/src/lib/credential-guard.js +43 -1
  159. package/src/lib/git-integration.js +5 -0
  160. package/src/lib/interaction-adapter.js +32 -11
  161. package/src/lib/jsonl-session-store.js +1 -0
  162. package/src/lib/llm-config-defaults.js +37 -0
  163. package/src/lib/mcp-oauth.js +76 -10
  164. package/src/lib/permission-engine.js +4 -1
  165. package/src/lib/personal-data-hub-wiring.js +14 -0
  166. package/src/lib/pipeline-orchestrator.js +20 -2
  167. package/src/lib/project-detector.js +44 -3
  168. package/src/lib/project-instructions.js +13 -0
  169. package/src/lib/repl-denials.js +137 -0
  170. package/src/lib/safe-json.js +22 -0
  171. package/src/lib/sandbox-v2.js +7 -6
  172. package/src/lib/search-command.js +65 -0
  173. package/src/lib/settings-hooks.cjs +133 -0
  174. package/src/lib/settings-loader.cjs +16 -7
  175. package/src/lib/social-graph.js +3 -1
  176. package/src/lib/stream-retry.js +27 -0
  177. package/src/lib/turn-context.js +15 -5
  178. package/src/repl/agent-repl.js +81 -8
  179. package/src/runtime/__tests__/message-roles.test.js +36 -3
  180. package/src/runtime/agent-core.js +175 -50
  181. package/src/runtime/coding-agent-contract-shared.cjs +9 -2
  182. package/src/runtime/coding-agent-shell-policy.cjs +23 -2
  183. package/src/runtime/file-ref-expander.js +51 -7
  184. package/src/runtime/headless-runner.js +83 -2
  185. package/src/runtime/headless-stream.js +69 -3
  186. package/src/runtime/mcp-config.js +42 -1
  187. package/src/runtime/message-roles.js +14 -1
  188. package/src/assets/web-panel/assets/ChatBubbleRenderer-Pg7dIsiE.js +0 -1
  189. package/src/assets/web-panel/assets/TimelineRenderer-acXS5N5h.js +0 -1
  190. package/src/assets/web-panel/assets/devWarning-D_lwLZ6O.js +0 -1
  191. package/src/assets/web-panel/assets/index-CEwuCM44.js +0 -1
  192. package/src/assets/web-panel/assets/index-DzzgU4IU.js +0 -1
@@ -33,6 +33,7 @@
33
33
  import fsDefault from "fs";
34
34
  import pathDefault from "path";
35
35
  import osDefault from "os";
36
+ import { credentialFileReason } from "./credential-guard.js";
36
37
 
37
38
  export const DEFAULT_MAX_FILE_BYTES = 48 * 1024; // per instruction/import file
38
39
  export const DEFAULT_MAX_TOTAL_BYTES = 192 * 1024; // whole block budget
@@ -321,6 +322,18 @@ export function loadProjectInstructions(opts = {}) {
321
322
  const resolved = path.resolve(baseDir, target);
322
323
  if (visited.has(resolved) || !isFile(fs, resolved)) continue; // silent:
323
324
  // non-files are decorative @tokens (npm scopes, emails), not imports.
325
+ // Refuse to import a credential / secret file. `cc agent` auto-loads
326
+ // project memory, so a cloned/untrusted repo's cc.md must NOT be able to
327
+ // pull `@~/.ssh/id_rsa` / `@../.env` into the LLM-bound system prompt
328
+ // (mirrors the read_file/run_shell credential guard).
329
+ const credReason = credentialFileReason(resolved);
330
+ if (credReason) {
331
+ visited.add(resolved); // don't re-warn on a second reference
332
+ warnings.push(
333
+ `refused @import of ${resolved} — ${credReason}; project memory must not pull secrets into the prompt`,
334
+ );
335
+ continue;
336
+ }
324
337
  visited.add(resolved);
325
338
  queue.push({ abs: resolved, scope: "import", depth: depth + 1 });
326
339
  }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * repl-denials — record + render the policy denials seen during an agent run,
3
+ * so the interactive REPL's `/permissions denials` can review what got blocked
4
+ * AND a headless run (`cc agent -p`) can print an end-of-run summary (Claude-
5
+ * Code 2.1.193 parity: "auto-mode denial reasons … /permissions recent
6
+ * denials"). The helpers are generic (not REPL-specific) and shared by both.
7
+ *
8
+ * A *denial* is a tool call the agent was NOT allowed to run — blocked by the
9
+ * shell policy, an ApprovalGate tier, a settings permission rule, or a hook. It
10
+ * is distinct from a tool that ran and *failed* (non-zero exit, missing file):
11
+ * those carry an `error` but none of the denial markers below, so they are not
12
+ * recorded here.
13
+ *
14
+ * Pure + dependency-free so it unit-tests without the REPL runtime.
15
+ */
16
+
17
+ /** Keep the most recent N denials per session (bounded ring buffer). */
18
+ export const MAX_RECENT_DENIALS = 20;
19
+
20
+ /** Error-message prefixes agent-core attaches to a blocked tool call. */
21
+ const DENIAL_PREFIX = /^\s*\[(Shell Policy|Permission|ApprovalGate|Hook)\]/;
22
+
23
+ /** Map a `[Prefix]` to a short `via` label when no structured field carries one. */
24
+ function viaFromPrefix(errStr) {
25
+ const m = DENIAL_PREFIX.exec(errStr);
26
+ if (!m) return null;
27
+ return (
28
+ {
29
+ "Shell Policy": "shell-policy",
30
+ Permission: "settings",
31
+ ApprovalGate: "gate",
32
+ Hook: "hook",
33
+ }[m[1]] || "policy"
34
+ );
35
+ }
36
+
37
+ /**
38
+ * Classify a tool-result as a policy denial and extract a structured record,
39
+ * or return null when it is not a denial. Detection prefers the structured
40
+ * markers agent-core attaches (`approval.decision` / `policy.decision` /
41
+ * `shellCommandPolicy.allowed`) and falls back to the error-message prefix.
42
+ *
43
+ * @param {object} ev { tool, result, error, argsSummary }
44
+ * @returns {{tool:string,summary:string,reason:string,via:string,rule:(string|null)}|null}
45
+ */
46
+ export function classifyDenial(ev = {}) {
47
+ const { tool, result, error, argsSummary } = ev;
48
+ const errStr = String(error || (result && result.error) || "").trim();
49
+ const approval = result && result.approval;
50
+ const policy = result && result.policy;
51
+ const shellPol = result && result.shellCommandPolicy;
52
+ const denied =
53
+ (approval && approval.decision === "deny") ||
54
+ (policy && policy.decision === "deny") ||
55
+ (shellPol && shellPol.allowed === false) ||
56
+ DENIAL_PREFIX.test(errStr);
57
+ if (!denied) return null;
58
+ const via =
59
+ (approval && approval.via) ||
60
+ (policy && policy.via) ||
61
+ (shellPol ? "shell-policy" : viaFromPrefix(errStr)) ||
62
+ "policy";
63
+ const rule = (policy && policy.rule) || (shellPol && shellPol.ruleId) || null;
64
+ return {
65
+ tool: tool || "?",
66
+ summary: String(
67
+ argsSummary || (shellPol && shellPol.normalizedCommand) || "",
68
+ ).slice(0, 200),
69
+ reason: errStr || "(denied)",
70
+ via,
71
+ rule,
72
+ };
73
+ }
74
+
75
+ /** Two denials are "the same" attempt when tool + command + rule + via match. */
76
+ function sameDenial(a, b) {
77
+ return (
78
+ a &&
79
+ b &&
80
+ a.tool === b.tool &&
81
+ a.summary === b.summary &&
82
+ a.via === b.via &&
83
+ a.rule === b.rule
84
+ );
85
+ }
86
+
87
+ /**
88
+ * Append a denial to a bounded most-recent-last ring buffer (mutates + returns
89
+ * it). Drops the oldest entries beyond `max`. No-op on a bad log/record.
90
+ *
91
+ * Consecutive identical denials are COALESCED: a model that hits a policy block
92
+ * usually retries the same command several times, which would otherwise flood
93
+ * the cap and evict other distinct denials. A repeat bumps the last entry's
94
+ * `count` and refreshes its `at` instead of appending.
95
+ */
96
+ export function recordDenial(log, record, max = MAX_RECENT_DENIALS) {
97
+ if (!Array.isArray(log) || !record) return log;
98
+ const last = log[log.length - 1];
99
+ if (sameDenial(last, record)) {
100
+ last.count = (last.count || 1) + 1;
101
+ if (typeof record.at === "number") last.at = record.at;
102
+ return log;
103
+ }
104
+ log.push(record);
105
+ while (log.length > max) log.shift();
106
+ return log;
107
+ }
108
+
109
+ /**
110
+ * Render the denial log most-recent-first for `/permissions denials`. Times are
111
+ * relative ("12s ago") when a record carries a numeric `at`; `now` is injectable
112
+ * for deterministic tests.
113
+ *
114
+ * @param {Array} log
115
+ * @param {{now?:number}} [opts]
116
+ * @returns {string}
117
+ */
118
+ export function formatDenials(log, opts = {}) {
119
+ const now = typeof opts.now === "number" ? opts.now : Date.now();
120
+ if (!Array.isArray(log) || log.length === 0) {
121
+ return " (no tool calls were denied this session)";
122
+ }
123
+ const lines = [` Recent denials (most recent first, ${log.length}):`];
124
+ for (let i = log.length - 1; i >= 0; i--) {
125
+ const d = log[i] || {};
126
+ const ago =
127
+ typeof d.at === "number"
128
+ ? ` · ${Math.max(0, Math.round((now - d.at) / 1000))}s ago`
129
+ : "";
130
+ const where = d.rule ? `${d.via}:${d.rule}` : d.via || "policy";
131
+ const what = d.summary ? `${d.tool} ${d.summary}` : d.tool || "?";
132
+ const times = d.count > 1 ? ` ×${d.count}` : "";
133
+ lines.push(` • ${what}${times} [${where}${ago}]`);
134
+ if (d.reason) lines.push(` ${d.reason}`);
135
+ }
136
+ return lines.join("\n");
137
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * safeJsonParse — JSON.parse that returns `fallback` instead of throwing on
3
+ * malformed (or null/empty) input.
4
+ *
5
+ * Use it for DB columns parsed inside a list `.map()` / loop: an unguarded
6
+ * `JSON.parse(row.col)` lets ONE corrupt cell throw out of the whole function,
7
+ * crashing the entire list load (the unguarded-JSON.parse-in-list-loop bug
8
+ * class — a single bad row should be skipped, not fail every other row).
9
+ *
10
+ * @param {unknown} text the raw string (or null/undefined)
11
+ * @param {*} [fallback=null] returned on null/empty/parse failure
12
+ * @returns {*}
13
+ */
14
+ export function safeJsonParse(text, fallback = null) {
15
+ if (text == null || text === "") return fallback;
16
+ try {
17
+ const v = JSON.parse(text);
18
+ return v == null ? fallback : v;
19
+ } catch {
20
+ return fallback;
21
+ }
22
+ }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import crypto from "crypto";
7
+ import { safeJsonParse } from "./safe-json.js";
7
8
  import { createRequire } from "module";
8
9
 
9
10
  const _require = createRequire(import.meta.url);
@@ -429,9 +430,9 @@ export function getSandbox(db, sandboxId) {
429
430
  id: row.id,
430
431
  agentId: row.agent_id,
431
432
  status: row.status,
432
- permissions: JSON.parse(row.permissions || "{}"),
433
- quota: JSON.parse(row.quota || "{}"),
434
- resourceUsage: JSON.parse(row.resource_usage || "{}"),
433
+ permissions: safeJsonParse(row.permissions, {}),
434
+ quota: safeJsonParse(row.quota, {}),
435
+ resourceUsage: safeJsonParse(row.resource_usage, {}),
435
436
  createdAt: row.created_at,
436
437
  };
437
438
  }
@@ -609,9 +610,9 @@ export function restoreFromDb(db) {
609
610
  id: row.id,
610
611
  agentId: row.agent_id,
611
612
  status: row.status || "active",
612
- permissions: JSON.parse(row.permissions || "{}"),
613
- quota: JSON.parse(row.quota || "{}"),
614
- resourceUsage: JSON.parse(row.resource_usage || "{}"),
613
+ permissions: safeJsonParse(row.permissions, {}),
614
+ quota: safeJsonParse(row.quota, {}),
615
+ resourceUsage: safeJsonParse(row.resource_usage, {}),
615
616
  createdAt: row.created_at || new Date(now).toISOString(),
616
617
  policy,
617
618
  createdAtMs: row.created_at_ms || now,
@@ -0,0 +1,65 @@
1
+ /**
2
+ * search-command — build the shell command for the agent `search_files` tool
3
+ * with the model/user-supplied pattern SAFELY embedded.
4
+ *
5
+ * The pattern flows into execSync (a real shell), so a raw interpolation
6
+ * (`grep -i "${pattern}"`) is a command-injection vector: a pattern like
7
+ * `x" ; curl evil ; "` runs arbitrary commands — and `search_files` is
8
+ * read-only + NOT confirm-gated, so it would bypass the `run_shell`
9
+ * ApprovalGate + credential guards entirely (reachable via prompt-injection).
10
+ * Raw interpolation is also semantically wrong: the shell would expand `$HOME`,
11
+ * backticks, globs, etc. in the pattern instead of searching for them literally.
12
+ *
13
+ * Fix: quote the pattern so every shell metacharacter is inert. The emitted
14
+ * commands (and therefore the output format `_ingestSearchOutput` parses) are
15
+ * otherwise unchanged.
16
+ */
17
+
18
+ /** POSIX single-quote: wrap in '…' and escape any embedded ' as '\''. Single
19
+ * quotes neutralize EVERY shell metacharacter, so nothing inside is expanded
20
+ * or can break out. */
21
+ export function shquotePosix(s) {
22
+ return `'${String(s).replace(/'/g, "'\\''")}'`;
23
+ }
24
+
25
+ /**
26
+ * Build `{ cmd }` for a search, or `{ error }` when the pattern can't be made
27
+ * safe on the target platform.
28
+ *
29
+ * @param {object} a
30
+ * @param {string} a.pattern the raw search pattern
31
+ * @param {boolean} a.isContent content search (grep/findstr) vs filename
32
+ * @param {string} [a.platform=process.platform]
33
+ * @returns {{cmd:string}|{error:string}}
34
+ */
35
+ export function buildSearchCommand({
36
+ pattern,
37
+ isContent,
38
+ platform = process.platform,
39
+ }) {
40
+ const pat = String(pattern ?? "");
41
+
42
+ if (platform === "win32") {
43
+ // cmd.exe has no reliable way to escape an embedded double-quote, and the
44
+ // pattern is double-quoted below — so reject a literal `"` (rare in a search
45
+ // pattern; run_shell handles complex cases). Every OTHER metacharacter
46
+ // (& | < > ^ ( )) is literal INSIDE cmd double quotes, so quoting the
47
+ // pattern closes the injection while leaving the redirect (2>NUL) outside.
48
+ if (pat.includes('"')) {
49
+ return {
50
+ error: 'search pattern may not contain a double-quote (") on Windows',
51
+ };
52
+ }
53
+ return {
54
+ cmd: isContent
55
+ ? `findstr /s /i /n "${pat}" *`
56
+ : `dir /s /b "*${pat}*" 2>NUL`,
57
+ };
58
+ }
59
+
60
+ return {
61
+ cmd: isContent
62
+ ? `grep -r -l -i ${shquotePosix(pat)} . --include="*" 2>/dev/null | head -20`
63
+ : `find . -name ${shquotePosix(`*${pat}*`)} -type f 2>/dev/null | head -20`,
64
+ };
65
+ }
@@ -27,6 +27,7 @@
27
27
  const path = require("node:path");
28
28
  const os = require("node:os");
29
29
  const fsDefault = require("node:fs");
30
+ const crypto = require("node:crypto");
30
31
  const { SUGGEST_UMBRELLA } = require("./permission-rules.cjs");
31
32
  const { projectRootBase } = require("./project-root.cjs");
32
33
 
@@ -179,8 +180,140 @@ function collectHooks(hooksBlock, event, toolName) {
179
180
  return out;
180
181
  }
181
182
 
183
+ /**
184
+ * Every command-hook in a parsed settings object, tagged with the event +
185
+ * matcher it fires under (so a fingerprint reflects WHEN it runs, not just the
186
+ * command string). Mirrors loadHooks' command-hook filter.
187
+ */
188
+ function extractCommandHooks(data) {
189
+ const out = [];
190
+ const block =
191
+ data && data.hooks && typeof data.hooks === "object" ? data.hooks : null;
192
+ if (!block) return out;
193
+ for (const [event, groups] of Object.entries(block)) {
194
+ if (!Array.isArray(groups)) continue;
195
+ for (const g of groups) {
196
+ if (!g || typeof g !== "object" || !Array.isArray(g.hooks)) continue;
197
+ for (const h of g.hooks) {
198
+ if (h && h.type === "command" && h.command) {
199
+ out.push({
200
+ event,
201
+ matcher: g.matcher ?? null,
202
+ command: String(h.command),
203
+ });
204
+ }
205
+ }
206
+ }
207
+ }
208
+ return out;
209
+ }
210
+
211
+ /** `~/.chainlesschain/hook-trust.json` — remembered first-run acknowledgments. */
212
+ function hookTrustStorePath() {
213
+ return path.join(_deps.homedir(), ".chainlesschain", "hook-trust.json");
214
+ }
215
+
216
+ function readHookTrustStore() {
217
+ try {
218
+ const f = hookTrustStorePath();
219
+ if (!_deps.fs.existsSync(f)) return {};
220
+ const parsed = JSON.parse(_deps.fs.readFileSync(f, "utf-8"));
221
+ return parsed && typeof parsed === "object" ? parsed : {};
222
+ } catch {
223
+ return {}; // unreadable store → treat as nothing acknowledged
224
+ }
225
+ }
226
+
227
+ function writeHookTrustStore(store) {
228
+ try {
229
+ const f = hookTrustStorePath();
230
+ _deps.fs.mkdirSync(path.dirname(f), { recursive: true });
231
+ _deps.fs.writeFileSync(f, JSON.stringify(store, null, 2), "utf-8");
232
+ return true;
233
+ } catch {
234
+ return false; // best-effort — a failed write just re-shows the notice
235
+ }
236
+ }
237
+
238
+ /**
239
+ * First-run trust notice for project-sourced settings.json hooks (which run
240
+ * shell). The user's own `~/.claude/settings.json` and an explicit `--settings`
241
+ * file are trusted; only the project's `.claude/settings{,.local}.json` (project
242
+ * root + cwd) carry the cloned-repo auto-execution risk that Claude-Code 2.1.195
243
+ * gated ("untrusted project config must require explicit consent").
244
+ *
245
+ * Returns a one-line notice (and records an acknowledgment hash) the FIRST time
246
+ * a project's shell-running hooks are seen — and again only if those hooks
247
+ * change. Returns null when there are no project hooks, the hooks are unchanged
248
+ * since last acknowledged, or the notice is disabled (`CC_HOOK_TRUST_NOTICE=0`,
249
+ * or hooks themselves are off via `CC_SETTINGS_HOOKS=0`). Best-effort: a failed
250
+ * store write still returns the notice (shown again next run) rather than
251
+ * throwing — it must never abort agent startup.
252
+ *
253
+ * @returns {string|null}
254
+ */
255
+ function projectHookTrustNotice({ cwd = process.cwd(), settingsFile } = {}) {
256
+ if (process.env.CC_HOOK_TRUST_NOTICE === "0") return null;
257
+ if (process.env.CC_SETTINGS_HOOKS === "0") return null; // hooks won't run anyway
258
+ const home = path.join(_deps.homedir(), ".claude", "settings.json");
259
+ const explicit = settingsFile ? path.resolve(cwd, settingsFile) : null;
260
+ const trusted = new Set([home, explicit].filter(Boolean));
261
+ const seen = new Set();
262
+ const projectFiles = settingsFiles(cwd, settingsFile).filter((f) => {
263
+ if (trusted.has(f) || seen.has(f)) return false;
264
+ seen.add(f);
265
+ return true;
266
+ });
267
+
268
+ const entries = [];
269
+ const contributing = [];
270
+ for (const f of projectFiles) {
271
+ let data;
272
+ try {
273
+ if (!_deps.fs.existsSync(f)) continue;
274
+ data = JSON.parse(_deps.fs.readFileSync(f, "utf-8"));
275
+ } catch {
276
+ continue; // malformed JSON is surfaced by loadHooks' own warn path
277
+ }
278
+ const cmds = extractCommandHooks(data);
279
+ if (cmds.length) {
280
+ entries.push(...cmds);
281
+ contributing.push(f);
282
+ }
283
+ }
284
+ if (entries.length === 0) return null;
285
+
286
+ const fingerprint = entries
287
+ .map((e) => `${e.event}${e.matcher ?? ""}${e.command}`)
288
+ .sort();
289
+ const hash = crypto
290
+ .createHash("sha256")
291
+ .update(JSON.stringify(fingerprint))
292
+ .digest("hex");
293
+ const key = projectRootBase(cwd, { fs: _deps.fs, path }) || cwd;
294
+
295
+ const store = readHookTrustStore();
296
+ if (store[key] && store[key].hash === hash) return null; // already acknowledged
297
+ store[key] = {
298
+ hash,
299
+ files: contributing,
300
+ count: entries.length,
301
+ at: new Date().toISOString(),
302
+ };
303
+ writeHookTrustStore(store); // best-effort acknowledgment
304
+
305
+ const fileList = contributing.map((f) => ` ${f}`).join("\n");
306
+ return (
307
+ `⚠ This project's .claude/settings.json defines ${entries.length} ` +
308
+ `shell-running hook(s) that auto-run on agent activity:\n${fileList}\n` +
309
+ ` Review them before trusting this repo. Shown once per change; ` +
310
+ `disable all hooks with CC_SETTINGS_HOOKS=0.`
311
+ );
312
+ }
313
+
182
314
  module.exports = {
183
315
  loadHooks,
316
+ projectHookTrustNotice,
184
317
  collectHooks,
185
318
  compileMatcher,
186
319
  umbrellaFor,
@@ -240,12 +240,16 @@ function loadSettingsConfig(opts = {}) {
240
240
  }
241
241
 
242
242
  /**
243
- * Read a top-level boolean setting across the layered `.claude/settings.json`
244
- * files (last/closest layer wins — same precedence as the permission rules).
245
- * Non-boolean values are ignored. Returns `undefined` when no layer sets it, so
246
- * a caller can distinguish "unset" from an explicit `false`.
243
+ * Read a boolean setting across the layered `.claude/settings.json` files
244
+ * (last/closest layer wins — same precedence as the permission rules). The key
245
+ * may be a dotted path for nested settings (e.g. `autoMode.classifyAllShell`);
246
+ * a plain key walks a one-element path, so existing callers are unaffected.
247
+ * Non-boolean values (and partial paths through a non-object) are ignored.
248
+ * Returns `undefined` when no layer sets it, so a caller can distinguish
249
+ * "unset" from an explicit `false`.
247
250
  *
248
- * @param {string} key top-level settings key (e.g. "respondToBashCommands")
251
+ * @param {string} key settings key or dotted path (e.g. "respondToBashCommands"
252
+ * or "autoMode.classifyAllShell")
249
253
  * @param {object} [opts]
250
254
  * @param {string} [opts.cwd=process.cwd()]
251
255
  * @param {string} [opts.settingsFile]
@@ -254,11 +258,16 @@ function loadSettingsConfig(opts = {}) {
254
258
  */
255
259
  function readBooleanSetting(key, opts = {}) {
256
260
  const cwd = opts.cwd || process.cwd();
261
+ const parts = String(key).split(".");
257
262
  let value;
258
263
  for (const file of settingsPaths(cwd, opts.settingsFile)) {
259
264
  const data = readSettingsFile(file, { onWarn: opts.onWarn });
260
- if (data && typeof data[key] === "boolean") {
261
- value = data[key]; // last (closest) layer wins
265
+ let node = data;
266
+ for (const p of parts) {
267
+ node = node && typeof node === "object" ? node[p] : undefined;
268
+ }
269
+ if (typeof node === "boolean") {
270
+ value = node; // last (closest) layer wins
262
271
  }
263
272
  }
264
273
  return value;
@@ -22,6 +22,7 @@
22
22
  */
23
23
 
24
24
  import { EventEmitter } from "events";
25
+ import { safeJsonParse } from "./safe-json.js";
25
26
 
26
27
  /* ── Edge types ────────────────────────────────────────────── */
27
28
 
@@ -325,7 +326,8 @@ export function loadFromDb(db) {
325
326
  )
326
327
  .all();
327
328
  for (const row of rows) {
328
- const metadata = row.metadata ? JSON.parse(row.metadata) : null;
329
+ // A single corrupt metadata cell must not crash the whole graph load.
330
+ const metadata = safeJsonParse(row.metadata, null);
329
331
  // Hydrate without re-emitting events (bulk load).
330
332
  _hydrateEdge({
331
333
  sourceDid: row.source_did,
@@ -18,6 +18,33 @@ import { isAbortError } from "./abort-utils.js";
18
18
  export const STREAM_RETRY_MAX = 2;
19
19
  export const STREAM_RETRY_BASE_MS = 400;
20
20
 
21
+ /**
22
+ * Hard ceiling on the *configurable* retry budget (Claude-Code 2.1.186:
23
+ * "CLAUDE_CODE_MAX_RETRIES capped at 15") — keeps a stray huge value from
24
+ * turning a dead endpoint into a multi-minute exponential-backoff hang.
25
+ */
26
+ export const STREAM_RETRY_MAX_CAP = 15;
27
+
28
+ /**
29
+ * Resolve the streaming-retry budget. Users can raise (or lower) the default
30
+ * via `CC_MAX_RETRIES` (native) or `CLAUDE_CODE_MAX_RETRIES` (Claude-Code
31
+ * parity); the value is clamped to `[0, STREAM_RETRY_MAX_CAP]`. Unset / blank /
32
+ * non-numeric falls back to the default `STREAM_RETRY_MAX`; a negative value
33
+ * clamps to 0 (retries disabled). `CC_MAX_RETRIES` wins when both are set.
34
+ *
35
+ * @param {Record<string,string|undefined>} [env=process.env]
36
+ * @returns {number}
37
+ */
38
+ export function resolveStreamRetryMax(env = process.env) {
39
+ const raw = env.CC_MAX_RETRIES ?? env.CLAUDE_CODE_MAX_RETRIES;
40
+ if (raw == null || String(raw).trim() === "") return STREAM_RETRY_MAX;
41
+ const n = Number.parseInt(String(raw).trim(), 10);
42
+ if (!Number.isFinite(n)) return STREAM_RETRY_MAX;
43
+ if (n < 0) return 0;
44
+ if (n > STREAM_RETRY_MAX_CAP) return STREAM_RETRY_MAX_CAP;
45
+ return n;
46
+ }
47
+
21
48
  /**
22
49
  * Is this error from a streaming chat request a transient API CONNECTION drop
23
50
  * that is safe to retry? True only for genuine network failures (reset /
@@ -30,6 +30,9 @@ function _git(cmd, cwd) {
30
30
  encoding: "utf-8",
31
31
  stdio: ["ignore", "pipe", "ignore"],
32
32
  timeout: 1500,
33
+ // `status --porcelain` on a dirty repo with many files can exceed the
34
+ // 1 MB execSync default → ENOBUFS → null → "(clean)" misreport below.
35
+ maxBuffer: 16 * 1024 * 1024,
33
36
  })
34
37
  .trim();
35
38
  } catch (_e) {
@@ -61,11 +64,18 @@ export function buildTurnContext({
61
64
  if (branch) {
62
65
  const head = _git("rev-parse --short HEAD", cwd);
63
66
  const status = _git("status --porcelain", cwd);
64
- const dirty = status && status.length > 0;
65
- const fileCount = dirty ? status.split("\n").filter(Boolean).length : 0;
66
- lines.push(
67
- `- git: ${branch}@${head || "?"}${dirty ? ` (${fileCount} uncommitted)` : " (clean)"}`,
68
- );
67
+ // Distinguish "command failed" (null timeout / huge output) from "" (truly
68
+ // clean): the old `status && …` reported a repo as "(clean)" whenever status
69
+ // couldn't be determined, which is a wrong signal to the model.
70
+ let stateLabel;
71
+ if (status === null) {
72
+ stateLabel = " (status unknown)";
73
+ } else if (status.length > 0) {
74
+ stateLabel = ` (${status.split("\n").filter(Boolean).length} uncommitted)`;
75
+ } else {
76
+ stateLabel = " (clean)";
77
+ }
78
+ lines.push(`- git: ${branch}@${head || "?"}${stateLabel}`);
69
79
  }
70
80
 
71
81
  if (sessionId) {