chainlesschain 0.162.151 → 0.162.152

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 (177) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-DuEOzebi.js → AIOps-CVLVJS-Q.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-C6GiJa5H.js → ActionButton-CJ2_GJsI.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-BBIiL_pO.js → Analytics-CZsmlTF8.js} +2 -2
  7. package/src/assets/web-panel/assets/{AppLayout-CuI2gZRy.js → AppLayout-DREvMZ62.js} +5 -5
  8. package/src/assets/web-panel/assets/{Audit-CbUjfr-K.js → Audit-B4NR42w_.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-CsSW9ljf.js → Backup-CJFmf3In.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-kM3rLe7F.js → BaseInput-Bh8_iUZY.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-kfE51YN3.js → Chat-DEwKjIxV.js} +5 -5
  12. package/src/assets/web-panel/assets/ChatBubbleRenderer-BOqHkxK_.js +1 -0
  13. package/src/assets/web-panel/assets/{Checkbox-jYb1uj-M.js → Checkbox-DYWCqYI9.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-X9XiD5Lj.js → Codegen-BlXrfZAJ.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-BfbHu1xI.js → Col-DLPM22eX.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-c4oJMFS-.js → Community-PFyzqijs.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-BFmnZpH7.js → Compact-7DIfY5Wp.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-s9shVHBS.js → Compliance-DF4CinPU.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-C5da--TC.js → Cowork-B1J80Vcj.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-9kNbyNE4.js → Cron-B7sx2OXX.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-C1k3NJD4.js → Crosschain-omLkxmu-.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DkheEvNK.js → DID-B_7r7b5F.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-C3FuSGmJ.js → Dashboard-Cp7k3GZg.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-CAV6FMFD.js → Dropdown-BCKO89Y8.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-Cc-Ulc4l.js → EmailListRenderer-CNgTSsFZ.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-8CitHEtv.js → FamilyGuardDashboard-DobM83JP.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-DFSZ5KDt.js → Federation-XVAYQIeu.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-CEWCtnG2.js → FormItemContext-DV5ka1KQ.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-CwRzNzmX.js → GenericCardRenderer-Dg5q-KpC.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-0rgDM7cJ.js → Git-Dsn3Xhxf.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CfBKtK9F.js → Governance-D06S4Qow.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CzcUVvud.js → Inference-nAvwxUXy.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-Dwc4YL4A.js → KnowledgeGraph-CtqRP_rF.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-DgFPwR2F.js → Logs-J5aGAyfY.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-DnpPTbGU.js → Marketplace-CaeY23Kj.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-DRxxINyW.js → McpTools-C2HHnwnE.js} +4 -4
  37. package/src/assets/web-panel/assets/{Memory-OhmW_6Z3.js → Memory-CWBrdKlR.js} +2 -2
  38. package/src/assets/web-panel/assets/{MobileBridge-DkTW-RK5.js → MobileBridge-CBLQ8uCY.js} +1 -1
  39. package/src/assets/web-panel/assets/{MobileProjects-DpCGV_lI.js → MobileProjects-Q_Cy_W4_.js} +1 -1
  40. package/src/assets/web-panel/assets/{Mtc--vmkL0AS.js → Mtc-BXur-euo.js} +2 -2
  41. package/src/assets/web-panel/assets/{MtcAudit-DGoFWjD4.js → MtcAudit-CpQSlOQC.js} +5 -5
  42. package/src/assets/web-panel/assets/{Multisig-B4kWgq95.js → Multisig-DlFwdEOA.js} +3 -3
  43. package/src/assets/web-panel/assets/{NLProgramming-YQwvommE.js → NLProgramming-DSh7fevc.js} +1 -1
  44. package/src/assets/web-panel/assets/{Notes-CgjtUADJ.js → Notes-Bv4NOVxg.js} +3 -3
  45. package/src/assets/web-panel/assets/{NotificationSettings-6JDrpBG5.js → NotificationSettings-CzHNnIfa.js} +1 -1
  46. package/src/assets/web-panel/assets/{OrderTableRenderer-PM6IlxjO.js → OrderTableRenderer-D7pHfKK4.js} +1 -1
  47. package/src/assets/web-panel/assets/{Organization-8McOT_OF.js → Organization-COzV01ed.js} +4 -4
  48. package/src/assets/web-panel/assets/{Overflow-DAZpSCGc.js → Overflow-CYRZJb2_.js} +1 -1
  49. package/src/assets/web-panel/assets/{P2P-B7l2ZWLy.js → P2P-DFm49FE0.js} +2 -2
  50. package/src/assets/web-panel/assets/{PdhVaultBrowser-W1Ud-NAL.js → PdhVaultBrowser-Ctr-6k5B.js} +3 -3
  51. package/src/assets/web-panel/assets/{Permissions-Czm_w9u_.js → Permissions-DXVrhFyv.js} +4 -4
  52. package/src/assets/web-panel/assets/{PersonalDataHub-B4F0_ZVM.js → PersonalDataHub-BO5utpLK.js} +2 -2
  53. package/src/assets/web-panel/assets/{Pipeline-B37aFiWv.js → Pipeline-C9irYCg7.js} +1 -1
  54. package/src/assets/web-panel/assets/{Privacy-94KBnRrD.js → Privacy-r13hrvoF.js} +1 -1
  55. package/src/assets/web-panel/assets/{ProjectInit-B5h8_dlh.js → ProjectInit-PRKNPZZC.js} +2 -2
  56. package/src/assets/web-panel/assets/{ProjectSettings-lmeI3Lpm.js → ProjectSettings-QvxM1ZDO.js} +2 -2
  57. package/src/assets/web-panel/assets/{Projects-D5AYOasl.js → Projects-DfgmsN3j.js} +1 -1
  58. package/src/assets/web-panel/assets/{Providers-B3cNWwJg.js → Providers-DxbrccZ7.js} +1 -1
  59. package/src/assets/web-panel/assets/{QrScannerModal-CHRvOw1R.js → QrScannerModal-BMHVRxBX.js} +1 -1
  60. package/src/assets/web-panel/assets/{QuickAsk-D1JMgFEd.js → QuickAsk-Co3_ndAH.js} +1 -1
  61. package/src/assets/web-panel/assets/{Recommend-EYWGR79p.js → Recommend-Cv3UWR6s.js} +1 -1
  62. package/src/assets/web-panel/assets/{RemoteSession-BAMC2pJt.js → RemoteSession-CSDsO2zA.js} +2 -2
  63. package/src/assets/web-panel/assets/{Reputation-X2Hk1XG_.js → Reputation-Bdj59vzM.js} +1 -1
  64. package/src/assets/web-panel/assets/{Row-Eq98LRtU.js → Row-CejfUjmo.js} +1 -1
  65. package/src/assets/web-panel/assets/{RssFeed-i76s6z_o.js → RssFeed-C83yKkvU.js} +2 -2
  66. package/src/assets/web-panel/assets/{Search-D_Es7CAI.js → Search-Ck4l_VI3.js} +1 -1
  67. package/src/assets/web-panel/assets/{Security-kayD2e94.js → Security-DGIIEx6K.js} +3 -3
  68. package/src/assets/web-panel/assets/{Services-Ctsm6ul8.js → Services-Cj2aNAy3.js} +2 -2
  69. package/src/assets/web-panel/assets/{Skeleton-CRNYlfdf.js → Skeleton-Dlyy8akb.js} +1 -1
  70. package/src/assets/web-panel/assets/{Skills-DkZYpF4-.js → Skills-C4aDa9Zh.js} +1 -1
  71. package/src/assets/web-panel/assets/{Sla-DXkoBcOP.js → Sla-tHd_Bauo.js} +1 -1
  72. package/src/assets/web-panel/assets/{SpeechSettings-DXi_GI1d.js → SpeechSettings-DI7R0Qgd.js} +1 -1
  73. package/src/assets/web-panel/assets/{SyncSettings-rNKpfomn.js → SyncSettings-OOxiPdsE.js} +2 -2
  74. package/src/assets/web-panel/assets/{Tasks-XF4uPtG-.js → Tasks-Nro_Ltg5.js} +1 -1
  75. package/src/assets/web-panel/assets/{Templates-i0Hzfeyu.js → Templates-Bm7eSMWH.js} +1 -1
  76. package/src/assets/web-panel/assets/{Tenant-sMpHAi27.js → Tenant-B8F8eFvD.js} +1 -1
  77. package/src/assets/web-panel/assets/Terminal-2oWhR7QF.js +3 -0
  78. package/src/assets/web-panel/assets/{TimelineRenderer-yuC6K7Rf.js → TimelineRenderer-GLnecmpd.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tokens-B8O8i0s6.js → Tokens-CtpFyati.js} +1 -1
  80. package/src/assets/web-panel/assets/{Trigger-Bv6yrlCr.js → Trigger-DGkXZzbN.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trust-C4mW95Ev.js → Trust-ClYZMkXN.js} +1 -1
  82. package/src/assets/web-panel/assets/{UkeySign-DV6yKaHn.js → UkeySign-C1AuEW5-.js} +1 -1
  83. package/src/assets/web-panel/assets/{VideoEditing-UI479-sD.js → VideoEditing-DHa9ekSK.js} +1 -1
  84. package/src/assets/web-panel/assets/{Wallet-M5oV9EVP.js → Wallet-CyQv8ZAN.js} +4 -4
  85. package/src/assets/web-panel/assets/{WebAuthn-DDyDHPDR.js → WebAuthn-DPxEEb5Y.js} +4 -4
  86. package/src/assets/web-panel/assets/{WorkflowEditor-RoBv4rqk.js → WorkflowEditor-C-fCM8Si.js} +1 -1
  87. package/src/assets/web-panel/assets/{chat-Cw1mE5LS.js → chat-BMu1Pk8B.js} +1 -1
  88. package/src/assets/web-panel/assets/{colors-BFP62Gwf.js → colors-DYPOXyWQ.js} +1 -1
  89. package/src/assets/web-panel/assets/{compact-item-17HL6pyC.js → compact-item-C5r_plPk.js} +1 -1
  90. package/src/assets/web-panel/assets/{createContext-D55X4fZX.js → createContext-9fWrWmuJ.js} +1 -1
  91. package/src/assets/web-panel/assets/devWarning-DmRz5Pet.js +1 -0
  92. package/src/assets/web-panel/assets/{hasIn-D3E5mNsP.js → hasIn-CjZM2-qE.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-KtLJXn4s.js → index-0DVMoDzF.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-DzG7mkgW.js → index-B1iUe3Ba.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-Da27u6j0.js → index-B56CI20h.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-uSsiwgZ2.js → index-BBVCXmJV.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-tUH_6bdJ.js → index-BPucc_ES.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-DD_qGBrt.js → index-BVFtY8Y4.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-CjCwi7we.js → index-BW-x3mUK.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-gAVxDiTZ.js → index-B_bTdeou.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CW6LLoJO.js → index-Bglw_kmu.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-DkBVYlE4.js → index-BsHmE__1.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BY_xZ2jy.js → index-Bti4lpw5.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-H9_X1Cg_.js → index-ByVDaARk.js} +1 -1
  105. package/src/assets/web-panel/assets/{index--44J50mc.js → index-C3E7xVl1.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CR9rNUvi.js → index-CDaA4A0b.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BgN5G5vF.js → index-CGqrVLWe.js} +27 -27
  108. package/src/assets/web-panel/assets/{index-D4LHSde4.js → index-CJJx0kbR.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Cdvk2_2q.js → index-CTnaRErh.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DCLu84x-.js → index-CYoS2cwK.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-paVubXzn.js → index-CcDGi9U1.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BQmZg5si.js → index-Cd9_KCPs.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-tVRPgYGr.js → index-Co9kBFxY.js} +1 -1
  114. package/src/assets/web-panel/assets/index-CrsL7YBW.js +1 -0
  115. package/src/assets/web-panel/assets/{index-DIRvdwBX.js → index-CvUR6l7C.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-Bq83UBDr.js → index-CyUtBxnC.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CvCFKojj.js → index-D0rFX-xN.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-Bzpp2z2S.js → index-D6zyUpD6.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BnGpft-Z.js → index-DJk9cPrs.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-ITeEHb8L.js → index-DLCoKst9.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CFYGn88j.js → index-DNxR1CHs.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-DOjA8bvj.js → index-DWwI0Jjd.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-RYZgFIwo.js → index-DjTB4wPn.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CLEzsZ8A.js → index-Du-w2i6G.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DtAnPBWP.js → index-HJdfPSMz.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BEa1RW4c.js → index-I3Qdrfcf.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-ASPYTzMK.js → index-Mipa7XgU.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BC6zxOlU.js → index-jO4lIpFE.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-CGyTos4p.js → index-oerotzoq.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-C_MfcrLf.js → index-phHPSXlV.js} +1 -1
  131. package/src/assets/web-panel/assets/index-zW69WSDA.js +1 -0
  132. package/src/assets/web-panel/assets/{initDefaultProps-Ckp7xVZS.js → initDefaultProps-B7tpABc_.js} +1 -1
  133. package/src/assets/web-panel/assets/{motion-DUZgCpn6.js → motion-DmvaRoVv.js} +1 -1
  134. package/src/assets/web-panel/assets/{move-B2rttglH.js → move-DF4X29Wf.js} +1 -1
  135. package/src/assets/web-panel/assets/{mtc-parser-BYWcLTuN.js → mtc-parser-C0cbg4PO.js} +1 -1
  136. package/src/assets/web-panel/assets/{omit-DhHqKeFx.js → omit-DZzcYy0l.js} +1 -1
  137. package/src/assets/web-panel/assets/{pickAttrs-ANpJaWMO.js → pickAttrs-DaZWfzuH.js} +1 -1
  138. package/src/assets/web-panel/assets/{placementArrow-CfZWe7CA.js → placementArrow-CBxOEyvn.js} +1 -1
  139. package/src/assets/web-panel/assets/{responsiveObserve-D7Xz2y-R.js → responsiveObserve-CGl8NIlO.js} +1 -1
  140. package/src/assets/web-panel/assets/{slide-DekqCBY9.js → slide-Bv330MSZ.js} +1 -1
  141. package/src/assets/web-panel/assets/{statusUtils-BET6XU7h.js → statusUtils-DJVy5oEM.js} +1 -1
  142. package/src/assets/web-panel/assets/{styleChecker-qHmJjV62.js → styleChecker-BiY1H5ST.js} +1 -1
  143. package/src/assets/web-panel/assets/{useFlexGapSupport-BoMCTNyu.js → useFlexGapSupport-D1sN2OlQ.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFs-TdW-NgFC.js → useFs-DxxHawzW.js} +1 -1
  145. package/src/assets/web-panel/assets/{usePersonalDataHub-Dyox7wCu.js → usePersonalDataHub-C1HblLrD.js} +1 -1
  146. package/src/assets/web-panel/assets/{vnode-XUn5S8CP.js → vnode-Dex0jy4T.js} +1 -1
  147. package/src/assets/web-panel/assets/{zoom-Ls77i8hn.js → zoom-Bk1MScAD.js} +1 -1
  148. package/src/assets/web-panel/index.html +1 -1
  149. package/src/commands/agent.js +8 -1
  150. package/src/commands/eval.js +88 -23
  151. package/src/commands/plugin.js +10 -0
  152. package/src/commands/team.js +26 -3
  153. package/src/data/changelog.json +12 -2
  154. package/src/harness/remote-command-ledger.js +60 -10
  155. package/src/lib/agent-sandbox.js +11 -5
  156. package/src/lib/agent-team/task-lease.js +28 -0
  157. package/src/lib/agent-team/team-runner.js +25 -0
  158. package/src/lib/eval/tasks.js +62 -41
  159. package/src/lib/eval/trend.js +7 -1
  160. package/src/lib/lsp/__tests__/lsp-client.test.js +12 -6
  161. package/src/lib/lsp/code-intelligence.js +55 -17
  162. package/src/lib/lsp/lsp-client.js +7 -1
  163. package/src/lib/plugin-runtime/install.js +20 -1
  164. package/src/lib/plugin-runtime/policy.js +13 -0
  165. package/src/lib/plugin-runtime/remote-source.js +34 -0
  166. package/src/lib/plugin-runtime/signature.js +79 -22
  167. package/src/lib/plugin-security.js +13 -14
  168. package/src/lib/telemetry/span-recorder.js +17 -6
  169. package/src/repl/agent-repl.js +12 -8
  170. package/src/runtime/__tests__/auto-pin.test.js +27 -4
  171. package/src/runtime/auto-pin.js +19 -0
  172. package/src/runtime/headless-runner.js +23 -5
  173. package/src/assets/web-panel/assets/ChatBubbleRenderer-Bg2FTW6A.js +0 -1
  174. package/src/assets/web-panel/assets/Terminal-BoZvaYAl.js +0 -3
  175. package/src/assets/web-panel/assets/devWarning-CqbHYuRc.js +0 -1
  176. package/src/assets/web-panel/assets/index-CKBI1U5K.js +0 -1
  177. package/src/assets/web-panel/assets/index-CkLp6DTO.js +0 -1
@@ -30,6 +30,8 @@ export class TeamRunner {
30
30
  this.runTask = opts.runTask;
31
31
  this.teammates = opts.teammates > 0 ? Math.floor(opts.teammates) : 2;
32
32
  this.ttlMs = opts.ttlMs;
33
+ // Lease-renewal heartbeat cadence while a task runs (default ttl/3).
34
+ this.renewEveryMs = opts.renewEveryMs;
33
35
  this.onEvent = typeof opts.onEvent === "function" ? opts.onEvent : () => {};
34
36
  this.maxTasks = opts.maxTasks > 0 ? opts.maxTasks : 1000;
35
37
  this._now = opts.now || registry._now || (() => Date.now());
@@ -198,6 +200,28 @@ export class TeamRunner {
198
200
  this._setState(holder, "running", { key });
199
201
  this._emit("task:claimed", { key, holder, attempts: task.attempts });
200
202
  const renew = () => this.registry.renew(key, { holder, ttlMs: this.ttlMs });
203
+ // Heartbeat: the built-in executors (shell / agent / worktree) never call
204
+ // `renew` themselves, so before this ANY task outliving the lease TTL was
205
+ // indistinguishable from a crash — a peer could steal it and the SAME task
206
+ // would run twice concurrently. The runner owns the TTL, so it renews here
207
+ // for every executor. Stops itself once the lease is lost (renew rejected —
208
+ // keeping a lost lease alive would be lying to the registry).
209
+ const effectiveTtl =
210
+ this.ttlMs > 0 ? this.ttlMs : this.registry.defaultTtlMs || 60000;
211
+ const every = Math.max(
212
+ 25,
213
+ Math.floor(this.renewEveryMs > 0 ? this.renewEveryMs : effectiveTtl / 3),
214
+ );
215
+ const heartbeat = setInterval(() => {
216
+ try {
217
+ const r = renew();
218
+ if (!r || r.ok !== true) clearInterval(heartbeat);
219
+ } catch {
220
+ clearInterval(heartbeat);
221
+ }
222
+ }, every);
223
+ // Never hold the process open just for a heartbeat.
224
+ if (typeof heartbeat.unref === "function") heartbeat.unref();
201
225
  // A teammate-scoped messaging handle: post to a peer / broadcast, and read
202
226
  // its own inbox (direct messages + unseen broadcasts).
203
227
  const inbox = this.mailbox ? this.mailbox.drain(holder) : [];
@@ -271,6 +295,7 @@ export class TeamRunner {
271
295
  attempts: outcome?.attempts,
272
296
  });
273
297
  } finally {
298
+ clearInterval(heartbeat);
274
299
  this._inFlight--;
275
300
  }
276
301
  }
@@ -30,6 +30,39 @@ const STRINGUTIL_V2 =
30
30
  " return str.charAt(0).toUpperCase() + str.slice(1);\n" +
31
31
  "}\n";
32
32
 
33
+ // Harness files the agent is told NOT to edit, shared by setup and check so the
34
+ // check can byte-compare them (same anti-cheat as STRINGUTIL_V2 above). A mere
35
+ // presence-regex is neuterable: prepending `console.log('ALL OK');
36
+ // process.exit(0)` keeps the assertion text present while never running it.
37
+ const RUN_CHECKS_MJS = [
38
+ "import { sum } from './calc.js';",
39
+ "const cases = [[1,2,3],[10,5,15],[0,0,0]];",
40
+ "for (const [a,b,want] of cases) {",
41
+ " if (sum(a,b) !== want) {",
42
+ " console.error(`FAIL sum(${a},${b})=${sum(a,b)} want ${want}`);",
43
+ " process.exit(1);",
44
+ " }",
45
+ "}",
46
+ "console.log('ALL OK');",
47
+ "",
48
+ ].join("\n");
49
+
50
+ const BUILD_MJS =
51
+ 'import { banner } from "./app.mjs";\n' +
52
+ 'if (banner() !== "v1.0.0") {\n' +
53
+ ' console.error("BUILD FAIL: " + banner());\n' +
54
+ " process.exit(1);\n" +
55
+ "}\n" +
56
+ 'console.log("BUILD OK");\n';
57
+
58
+ const RUN_MJS =
59
+ 'import { label } from "./app.mjs";\n' +
60
+ 'if (label("hello") !== "Hello") {\n' +
61
+ ' console.error("FAIL: " + label("hello"));\n' +
62
+ " process.exit(1);\n" +
63
+ "}\n" +
64
+ 'console.log("OK");\n';
65
+
33
66
  export const BUILTIN_TASKS = [
34
67
  {
35
68
  id: "create-file",
@@ -134,32 +167,22 @@ export const BUILTIN_TASKS = [
134
167
  );
135
168
  fs.writeFileSync(
136
169
  path.join(dir, "run-checks.mjs"),
137
- [
138
- "import { sum } from './calc.js';",
139
- "const cases = [[1,2,3],[10,5,15],[0,0,0]];",
140
- "for (const [a,b,want] of cases) {",
141
- " if (sum(a,b) !== want) {",
142
- " console.error(`FAIL sum(${a},${b})=${sum(a,b)} want ${want}`);",
143
- " process.exit(1);",
144
- " }",
145
- "}",
146
- "console.log('ALL OK');",
147
- "",
148
- ].join("\n"),
170
+ RUN_CHECKS_MJS,
149
171
  "utf8",
150
172
  );
151
173
  },
152
174
  check: (dir) => {
153
- // Objective: the test harness must actually pass. Guard against the agent
154
- // "fixing" it by neutering run-checks.mjs require the assertions to
155
- // still be present.
175
+ // Objective: the test harness must actually pass. Byte-compare the
176
+ // harness a presence-regex alone is neuterable (prepend an early
177
+ // `console.log('ALL OK'); process.exit(0)` and the assertion text is
178
+ // still there but never runs).
156
179
  const checks = read(dir, "run-checks.mjs");
157
180
  if (checks == null)
158
181
  return { pass: false, detail: "run-checks.mjs missing" };
159
- if (!/sum\(a,\s*b\)\s*!==\s*want/.test(checks)) {
182
+ if (checks !== RUN_CHECKS_MJS) {
160
183
  return {
161
184
  pass: false,
162
- detail: "run-checks.mjs assertions were altered",
185
+ detail: "run-checks.mjs was edited (fix calc.js instead)",
163
186
  };
164
187
  }
165
188
  try {
@@ -487,23 +510,19 @@ export const BUILTIN_TASKS = [
487
510
  'export function banner() {\n return "v" + VERSION;\n}\n',
488
511
  "utf8",
489
512
  );
490
- fs.writeFileSync(
491
- path.join(dir, "build.mjs"),
492
- 'import { banner } from "./app.mjs";\n' +
493
- 'if (banner() !== "v1.0.0") {\n' +
494
- ' console.error("BUILD FAIL: " + banner());\n' +
495
- " process.exit(1);\n" +
496
- "}\n" +
497
- 'console.log("BUILD OK");\n',
498
- "utf8",
499
- );
513
+ fs.writeFileSync(path.join(dir, "build.mjs"), BUILD_MJS, "utf8");
500
514
  },
501
515
  check: (dir) => {
502
516
  const build = read(dir, "build.mjs");
503
517
  if (build == null) return { pass: false, detail: "build.mjs missing" };
504
- // Guard: fix the modules, don't neuter the build harness.
505
- if (!/banner\(\)\s*!==\s*"v1\.0\.0"/.test(build)) {
506
- return { pass: false, detail: "build.mjs assertion was altered" };
518
+ // Guard: fix the modules, don't neuter the build harness. Byte-compare —
519
+ // a presence-regex is satisfiable by a rewritten build.mjs that defines
520
+ // its own banner() and never imports app.mjs.
521
+ if (build !== BUILD_MJS) {
522
+ return {
523
+ pass: false,
524
+ detail: "build.mjs was edited (reconcile config.mjs/app.mjs instead)",
525
+ };
507
526
  }
508
527
  try {
509
528
  const out = execFileSync(process.execPath, ["build.mjs"], {
@@ -534,7 +553,7 @@ export const BUILTIN_TASKS = [
534
553
  "`capitalize` export was renamed to `toTitle` (same behavior). Do all of: " +
535
554
  "(1) bump stringutil to ^2.0.0 in package.json; (2) update app.mjs to " +
536
555
  "import and use `toTitle` instead of `capitalize`; (3) do NOT edit " +
537
- "stringutil.mjs. Afterwards `node run.mjs` must print OK.",
556
+ "stringutil.mjs or run.mjs. Afterwards `node run.mjs` must print OK.",
538
557
  setup: (dir) => {
539
558
  fs.writeFileSync(
540
559
  path.join(dir, "package.json"),
@@ -558,16 +577,7 @@ export const BUILTIN_TASKS = [
558
577
  "export function label(s) {\n return capitalize(s);\n}\n",
559
578
  "utf8",
560
579
  );
561
- fs.writeFileSync(
562
- path.join(dir, "run.mjs"),
563
- 'import { label } from "./app.mjs";\n' +
564
- 'if (label("hello") !== "Hello") {\n' +
565
- ' console.error("FAIL: " + label("hello"));\n' +
566
- " process.exit(1);\n" +
567
- "}\n" +
568
- 'console.log("OK");\n',
569
- "utf8",
570
- );
580
+ fs.writeFileSync(path.join(dir, "run.mjs"), RUN_MJS, "utf8");
571
581
  },
572
582
  check: (dir) => {
573
583
  // The dependency is vendored — the agent must adapt the caller, NOT edit
@@ -580,6 +590,17 @@ export const BUILTIN_TASKS = [
580
590
  detail: "the stringutil dependency was edited (adapt the caller)",
581
591
  };
582
592
  }
593
+ // The run harness is equally off-limits: without this guard an agent
594
+ // could overwrite run.mjs with `console.log("OK")` and pass the textual
595
+ // checks below without app.mjs ever being executed.
596
+ const runner = read(dir, "run.mjs");
597
+ if (runner == null) return { pass: false, detail: "run.mjs missing" };
598
+ if (runner !== RUN_MJS) {
599
+ return {
600
+ pass: false,
601
+ detail: "run.mjs was edited (adapt app.mjs/package.json instead)",
602
+ };
603
+ }
583
604
  const pkg = read(dir, "package.json");
584
605
  if (pkg == null || !/"stringutil"\s*:\s*"[\^~]?2\./.test(pkg)) {
585
606
  return {
@@ -84,7 +84,13 @@ function ordered(records) {
84
84
  * }}
85
85
  */
86
86
  export function computeTrend(runs, { regressionThreshold = 0 } = {}) {
87
- const norm = ordered((runs || []).map((r) => summarizeRun(r)));
87
+ // A dry-run record is always 0% (the no-op agent creates nothing) folding
88
+ // it in would flag every previously-passing task as a regression (or, in the
89
+ // other direction, fake an "all fixed" jump). Smoke-testing with
90
+ // `--dry-run --history` must not poison the release gate.
91
+ const norm = ordered(
92
+ (runs || []).filter((r) => r?.dryRun !== true).map((r) => summarizeRun(r)),
93
+ );
88
94
  const history = norm.map((r) => r.passRate);
89
95
  if (norm.length === 0) {
90
96
  return {
@@ -225,14 +225,20 @@ describe("uri <-> path", () => {
225
225
  describe("normalizeUri", () => {
226
226
  it("lowercases the windows drive letter so client/server keys match", () => {
227
227
  // path.resolve yields C:, servers echo c: — must collapse to one key
228
- expect(normalizeUri("file:///C%3A/proj/a.ts")).toBe(
229
- "file:///c%3A/proj/a.ts",
230
- );
231
- expect(normalizeUri("file:///c%3A/proj/a.ts")).toBe(
232
- "file:///c%3A/proj/a.ts",
233
- );
228
+ expect(normalizeUri("file:///C%3A/proj/a.ts")).toBe("file:///c:/proj/a.ts");
229
+ expect(normalizeUri("file:///c%3A/proj/a.ts")).toBe("file:///c:/proj/a.ts");
234
230
  expect(normalizeUri("file:///C:/proj/a.ts")).toBe("file:///c:/proj/a.ts");
235
231
  });
232
+ it("collapses the %3A and bare-colon drive forms to ONE key (gopls)", () => {
233
+ // The client sends percent-encoded didOpen URIs (file:///C%3A/…);
234
+ // tsserver/pyright echo that form back, but gopls REWRITES to a bare
235
+ // colon (file:///C:/…). Before this canonicalization the two forms were
236
+ // distinct diagnostics-Map keys, so every gopls publish missed the
237
+ // didOpen key and `cc code-intel diag` on Go was permanently empty.
238
+ const ours = normalizeUri("file:///C%3A/Users/u/goproj/broken.go");
239
+ const goplsPublished = normalizeUri("file:///C:/Users/u/goproj/broken.go");
240
+ expect(ours).toBe(goplsPublished);
241
+ });
236
242
  it("leaves POSIX file URIs unchanged", () => {
237
243
  expect(normalizeUri("file:///home/u/a.ts")).toBe("file:///home/u/a.ts");
238
244
  });
@@ -85,11 +85,15 @@ export class CodeIntelligence {
85
85
  async references(filePath, line, col, { includeDeclaration = true } = {}) {
86
86
  const ready = await this._ensure(filePath);
87
87
  if (ready.unavailable) return unavailable(ready.reason);
88
- const result = await ready.client.request("textDocument/references", {
89
- textDocument: { uri: ready.uri },
90
- position: toLspPosition({ line, col }),
91
- context: { includeDeclaration },
92
- });
88
+ const result = await this._readRequest(
89
+ ready.client,
90
+ "textDocument/references",
91
+ {
92
+ textDocument: { uri: ready.uri },
93
+ position: toLspPosition({ line, col }),
94
+ context: { includeDeclaration },
95
+ },
96
+ );
93
97
  return { available: true, locations: this._normalizeLocations(result) };
94
98
  }
95
99
 
@@ -97,7 +101,7 @@ export class CodeIntelligence {
97
101
  async hover(filePath, line, col) {
98
102
  const ready = await this._ensure(filePath);
99
103
  if (ready.unavailable) return unavailable(ready.reason);
100
- const result = await ready.client.request("textDocument/hover", {
104
+ const result = await this._readRequest(ready.client, "textDocument/hover", {
101
105
  textDocument: { uri: ready.uri },
102
106
  position: toLspPosition({ line, col }),
103
107
  });
@@ -108,9 +112,13 @@ export class CodeIntelligence {
108
112
  async documentSymbols(filePath) {
109
113
  const ready = await this._ensure(filePath);
110
114
  if (ready.unavailable) return unavailable(ready.reason);
111
- const result = await ready.client.request("textDocument/documentSymbol", {
112
- textDocument: { uri: ready.uri },
113
- });
115
+ const result = await this._readRequest(
116
+ ready.client,
117
+ "textDocument/documentSymbol",
118
+ {
119
+ textDocument: { uri: ready.uri },
120
+ },
121
+ );
114
122
  return {
115
123
  available: true,
116
124
  symbols: this._normalizeSymbols(result, ready.uri),
@@ -124,7 +132,9 @@ export class CodeIntelligence {
124
132
  if (!seed) return unavailable("no indexable file found to start a server");
125
133
  const ready = await this.manager.ensureFor(seed, this._ensureOpts);
126
134
  if (ready.unavailable) return unavailable(ready.reason);
127
- const result = await ready.client.request("workspace/symbol", { query });
135
+ const result = await this._readRequest(ready.client, "workspace/symbol", {
136
+ query,
137
+ });
128
138
  return { available: true, symbols: this._normalizeSymbols(result) };
129
139
  }
130
140
 
@@ -160,7 +170,8 @@ export class CodeIntelligence {
160
170
  const caps = ready.client.serverCapabilities || {};
161
171
  if (caps.diagnosticProvider) {
162
172
  try {
163
- const report = await ready.client.request(
173
+ const report = await this._readRequest(
174
+ ready.client,
164
175
  "textDocument/diagnostic",
165
176
  { textDocument: { uri: ready.uri } },
166
177
  { timeoutMs },
@@ -182,11 +193,15 @@ export class CodeIntelligence {
182
193
  async renamePreview(filePath, line, col, newName) {
183
194
  const ready = await this._ensure(filePath);
184
195
  if (ready.unavailable) return unavailable(ready.reason);
185
- const result = await ready.client.request("textDocument/rename", {
186
- textDocument: { uri: ready.uri },
187
- position: toLspPosition({ line, col }),
188
- newName,
189
- });
196
+ const result = await this._readRequest(
197
+ ready.client,
198
+ "textDocument/rename",
199
+ {
200
+ textDocument: { uri: ready.uri },
201
+ position: toLspPosition({ line, col }),
202
+ newName,
203
+ },
204
+ );
190
205
  return { available: true, edits: this._normalizeWorkspaceEdit(result) };
191
206
  }
192
207
 
@@ -196,10 +211,33 @@ export class CodeIntelligence {
196
211
 
197
212
  // ---- internals ----
198
213
 
214
+ /**
215
+ * Issue an idempotent READ request with a bounded ContentModified retry.
216
+ * LSP -32801 (ContentModified) means "server state changed mid-request —
217
+ * the result would be stale, ask again"; rust-analyzer emits it freely
218
+ * while its index is still settling after didOpen. Read-only queries can
219
+ * always be retried; mutating requests must NOT go through here.
220
+ */
221
+ async _readRequest(client, method, params, opts) {
222
+ let lastErr;
223
+ for (let attempt = 0; attempt < 4; attempt++) {
224
+ try {
225
+ return await client.request(method, params, opts);
226
+ } catch (err) {
227
+ const contentModified =
228
+ err?.code === -32801 || /content modified/i.test(err?.message || "");
229
+ if (!contentModified) throw err;
230
+ lastErr = err;
231
+ await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
232
+ }
233
+ }
234
+ throw lastErr;
235
+ }
236
+
199
237
  async _locationQuery(method, filePath, line, col) {
200
238
  const ready = await this._ensure(filePath);
201
239
  if (ready.unavailable) return unavailable(ready.reason);
202
- const result = await ready.client.request(method, {
240
+ const result = await this._readRequest(ready.client, method, {
203
241
  textDocument: { uri: ready.uri },
204
242
  position: toLspPosition({ line, col }),
205
243
  });
@@ -297,9 +297,15 @@ export function pathToFileUri(p) {
297
297
  */
298
298
  export function normalizeUri(uri) {
299
299
  if (!uri) return uri;
300
+ // Canonicalize BOTH the drive-letter case AND the colon encoding. Servers
301
+ // differ in the form they publish back: tsserver/pyright echo the client's
302
+ // percent-encoded `c%3A/…`, but gopls rewrites to a bare `C:/…` — keeping
303
+ // the incoming colon form made the two forms distinct Map keys, so every
304
+ // gopls publishDiagnostics missed the didOpen key and Go diagnostics were
305
+ // permanently empty regardless of timeout.
300
306
  return String(uri).replace(
301
307
  /^(file:\/\/\/)([A-Za-z])(%3[Aa]|:)/,
302
- (_m, p, drive, colon) => p + drive.toLowerCase() + colon,
308
+ (_m, p, drive) => p + drive.toLowerCase() + ":",
303
309
  );
304
310
  }
305
311
 
@@ -29,7 +29,7 @@ import {
29
29
  discoverPlugins,
30
30
  } from "./scopes.js";
31
31
  import { verifyPluginManifest } from "../plugin-security.js";
32
- import { writePluginLock } from "./signature.js";
32
+ import { writePluginLock, LOCK_FILENAME } from "./signature.js";
33
33
 
34
34
  export const _deps = {
35
35
  existsSync: fs.existsSync,
@@ -97,6 +97,12 @@ export function installFromDirectory(srcDir, opts = {}) {
97
97
  _deps.mkdirSync(dest, { recursive: true });
98
98
  copyDirGuarded(src, dest, dest);
99
99
 
100
+ // A lock may ONLY exist if THIS installer wrote it. The source is untrusted
101
+ // and may ship its own `.plugin-lock.json` (copied verbatim above) — on an
102
+ // unsigned install that forged lock would otherwise survive into the
103
+ // "immutable" version dir and defeat load-time requireSignedPlugins.
104
+ _deps.rmSync(path.join(dest, LOCK_FILENAME), { force: true });
105
+
100
106
  // Record the verified signature into the installed (immutable) version dir so
101
107
  // load-time enforcement can re-check it. The manifest was copied verbatim, so
102
108
  // its bytes/sha in `dest` match what we verified in `src`.
@@ -110,6 +116,8 @@ export function installFromDirectory(srcDir, opts = {}) {
110
116
  sha256: verification.sha256,
111
117
  publicKeySha256: verification.publicKeySha256,
112
118
  signatureVerified: verification.signatureVerified === true,
119
+ signatureBase64: verification.signatureBase64,
120
+ publicKeyPem: verification.publicKeyPem,
113
121
  });
114
122
  }
115
123
 
@@ -245,6 +253,17 @@ export function parseGitSource(raw) {
245
253
  * command line — no injection). Caller removes the temp dir's parent.
246
254
  */
247
255
  export function fetchGitRepo(url, ref) {
256
+ // git argv-injection guard: a value starting with "-" is parsed by git as an
257
+ // OPTION, not a URL/ref — e.g. a registry-supplied ref "-f" reaches
258
+ // `git checkout <ref>` on the full-clone retry path, and an option-looking
259
+ // url reaches `git clone`. Real git URLs/refs never start with "-"
260
+ // (check-ref-format forbids it), so reject instead of trying to escape.
261
+ if (String(url).startsWith("-")) {
262
+ throw new Error(`refusing git source that looks like an option: ${url}`);
263
+ }
264
+ if (ref != null && String(ref).startsWith("-")) {
265
+ throw new Error(`refusing git ref that looks like an option: ${ref}`);
266
+ }
248
267
  const base = _deps.mkdtempSync(path.join(os.tmpdir(), "cc-plugin-git-"));
249
268
  const dir = path.join(base, "repo");
250
269
  const run = (args) =>
@@ -115,6 +115,19 @@ export function filterByManagedPolicy(plugins, managed) {
115
115
  continue;
116
116
  }
117
117
  if (requireSigned) {
118
+ // Fail closed when no signing keys are pinned: a lock's signature can be
119
+ // SELF-signed by whoever authored the plugin (the lock lives in a
120
+ // writable dir), so "signed by anyone" is not a guarantee. Without
121
+ // trustedPluginKeySha256 the gate would accept any self-signed drop-in.
122
+ if (!trustedKeys || trustedKeys.size === 0) {
123
+ dropped.push({
124
+ name: p.name,
125
+ reason:
126
+ "requireSignedPlugins: no trustedPluginKeySha256 fingerprints " +
127
+ "configured (fail-closed — pin the org's signing keys)",
128
+ });
129
+ continue;
130
+ }
118
131
  const v = verifyInstalledSignature(p, { trustedKeys });
119
132
  if (!v.signed) {
120
133
  dropped.push({
@@ -46,6 +46,39 @@ export function isRemoteSource(raw) {
46
46
  return /^https?:\/\//i.test(s) && /\.json(\?.*)?(#.*)?$/i.test(s);
47
47
  }
48
48
 
49
+ /**
50
+ * Enforce HTTPS for registry URLs. The registry supplies BOTH the git source
51
+ * and its claimed sha256 — over plain HTTP a network MITM controls the two
52
+ * together, so the integrity check verifies nothing. Loopback is exempt (a
53
+ * local dev registry isn't MITM-able off-host); anything else needs the
54
+ * explicit opt-in (opts.allowInsecure / CC_PLUGIN_REGISTRY_ALLOW_HTTP=1).
55
+ */
56
+ export function assertRegistryUrlSafe(url, { allowInsecure = false } = {}) {
57
+ let u;
58
+ try {
59
+ u = new URL(String(url));
60
+ } catch {
61
+ throw new Error(`invalid registry URL: ${url}`);
62
+ }
63
+ if (u.protocol === "https:") return;
64
+ if (u.protocol !== "http:") {
65
+ throw new Error(`registry URL must be http(s): ${url}`);
66
+ }
67
+ // WHATWG URL keeps the brackets on IPv6 hostnames — strip before comparing.
68
+ const host = u.hostname.replace(/^\[|\]$/g, "").toLowerCase();
69
+ const loopback =
70
+ host === "localhost" || host === "127.0.0.1" || host === "::1";
71
+ const optIn =
72
+ allowInsecure === true || process.env.CC_PLUGIN_REGISTRY_ALLOW_HTTP === "1";
73
+ if (loopback || optIn) return;
74
+ throw new Error(
75
+ `plain-HTTP registry rejected: ${url} — a network MITM controls both the ` +
76
+ `source and its sha256, so the integrity check verifies nothing. Use ` +
77
+ `https, or opt in with --allow-insecure-registry / ` +
78
+ `CC_PLUGIN_REGISTRY_ALLOW_HTTP=1 on a trusted network.`,
79
+ );
80
+ }
81
+
49
82
  /** Where a fetched registry is cached (content-addressed by its URL). */
50
83
  export function registryCachePath(url, cacheDir) {
51
84
  const dir =
@@ -93,6 +126,7 @@ export async function fetchRegistry(url, opts = {}) {
93
126
  timeoutMs = DEFAULT_TIMEOUT_MS,
94
127
  allowCache = true,
95
128
  } = opts;
129
+ assertRegistryUrlSafe(url, { allowInsecure: opts.allowInsecure });
96
130
  const cachePath = registryCachePath(url, cacheDir);
97
131
  const headers = { Accept: "application/json" };
98
132
  if (token) headers.Authorization = `Bearer ${token}`;