terramend 0.2.0

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 (406) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +145 -0
  3. package/dist/agents/claude.d.ts +73 -0
  4. package/dist/agents/claudePretoolGate.d.ts +99 -0
  5. package/dist/agents/gateServer.d.ts +7 -0
  6. package/dist/agents/index.d.ts +6 -0
  7. package/dist/agents/nativeFsDenies.d.ts +28 -0
  8. package/dist/agents/opencode.d.ts +231 -0
  9. package/dist/agents/opencodePlugin.d.ts +85 -0
  10. package/dist/agents/opencodeShared.d.ts +40 -0
  11. package/dist/agents/postRun.d.ts +132 -0
  12. package/dist/agents/reviewer.d.ts +38 -0
  13. package/dist/agents/sessionLabeler.d.ts +97 -0
  14. package/dist/agents/shared.d.ts +189 -0
  15. package/dist/agents/subagentModels.d.ts +19 -0
  16. package/dist/agents/subagentToolGates.d.ts +55 -0
  17. package/dist/cli.mjs +197426 -0
  18. package/dist/external.d.ts +227 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.js +196783 -0
  21. package/dist/internal/index.d.ts +18 -0
  22. package/dist/internal.js +1714 -0
  23. package/dist/lifecycle.d.ts +2 -0
  24. package/dist/main.d.ts +8 -0
  25. package/dist/mcp/arkConfig.d.ts +1 -0
  26. package/dist/mcp/checkSuite.d.ts +25 -0
  27. package/dist/mcp/checkout.d.ts +77 -0
  28. package/dist/mcp/comment.d.ts +119 -0
  29. package/dist/mcp/commitInfo.d.ts +9 -0
  30. package/dist/mcp/crosswalk.d.ts +105 -0
  31. package/dist/mcp/dependencies.d.ts +8 -0
  32. package/dist/mcp/geminiSanitizer.d.ts +28 -0
  33. package/dist/mcp/git.d.ts +46 -0
  34. package/dist/mcp/guardrails.d.ts +104 -0
  35. package/dist/mcp/issue.d.ts +18 -0
  36. package/dist/mcp/issueComments.d.ts +9 -0
  37. package/dist/mcp/issueEvents.d.ts +9 -0
  38. package/dist/mcp/issueInfo.d.ts +9 -0
  39. package/dist/mcp/labels.d.ts +12 -0
  40. package/dist/mcp/localContext.d.ts +19 -0
  41. package/dist/mcp/moduleExtraction.d.ts +71 -0
  42. package/dist/mcp/moduleTests.d.ts +104 -0
  43. package/dist/mcp/modules.d.ts +179 -0
  44. package/dist/mcp/output.d.ts +12 -0
  45. package/dist/mcp/pathSafety.d.ts +14 -0
  46. package/dist/mcp/policy.d.ts +48 -0
  47. package/dist/mcp/pr.d.ts +49 -0
  48. package/dist/mcp/prInfo.d.ts +9 -0
  49. package/dist/mcp/providerSchema.d.ts +50 -0
  50. package/dist/mcp/review.d.ts +199 -0
  51. package/dist/mcp/reviewComments.d.ts +178 -0
  52. package/dist/mcp/roots.d.ts +58 -0
  53. package/dist/mcp/scope.d.ts +15 -0
  54. package/dist/mcp/selectMode.d.ts +18 -0
  55. package/dist/mcp/server.d.ts +48 -0
  56. package/dist/mcp/shared.d.ts +47 -0
  57. package/dist/mcp/shell.d.ts +37 -0
  58. package/dist/mcp/staleFix.d.ts +51 -0
  59. package/dist/mcp/terraform/cost.d.ts +55 -0
  60. package/dist/mcp/terraform/currency.d.ts +94 -0
  61. package/dist/mcp/terraform/decisions.d.ts +178 -0
  62. package/dist/mcp/terraform/findings.d.ts +75 -0
  63. package/dist/mcp/terraform/plan.d.ts +157 -0
  64. package/dist/mcp/terraform/scanners.d.ts +131 -0
  65. package/dist/mcp/terraform/tools.d.ts +63 -0
  66. package/dist/mcp/terraform/types.d.ts +172 -0
  67. package/dist/mcp/terraform.d.ts +22 -0
  68. package/dist/mcp/terratest.d.ts +83 -0
  69. package/dist/mcp/upload.d.ts +6 -0
  70. package/dist/models.d.ts +171 -0
  71. package/dist/modes.d.ts +26 -0
  72. package/dist/prep/index.d.ts +7 -0
  73. package/dist/prep/installNodeDependencies.d.ts +2 -0
  74. package/dist/prep/installPythonDependencies.d.ts +2 -0
  75. package/dist/prep/types.d.ts +31 -0
  76. package/dist/reviewQuality.d.ts +64 -0
  77. package/dist/skills/terraform-best-practices/SKILL.md +369 -0
  78. package/dist/toolState.d.ts +135 -0
  79. package/dist/utils/activity.d.ts +40 -0
  80. package/dist/utils/agent.d.ts +20 -0
  81. package/dist/utils/agentHangReport.d.ts +38 -0
  82. package/dist/utils/apiFetch.d.ts +19 -0
  83. package/dist/utils/apiKeys.d.ts +41 -0
  84. package/dist/utils/apiUrl.d.ts +20 -0
  85. package/dist/utils/assets.d.ts +8 -0
  86. package/dist/utils/billingErrors.d.ts +85 -0
  87. package/dist/utils/body.d.ts +34 -0
  88. package/dist/utils/buildTerramendFooter.d.ts +25 -0
  89. package/dist/utils/byokFallback.d.ts +85 -0
  90. package/dist/utils/claudeSubscription.d.ts +30 -0
  91. package/dist/utils/cli.d.ts +10 -0
  92. package/dist/utils/codexHome.d.ts +29 -0
  93. package/dist/utils/codexOAuth.d.ts +60 -0
  94. package/dist/utils/diffCoverage.d.ts +63 -0
  95. package/dist/utils/errorReport.d.ts +17 -0
  96. package/dist/utils/exitHandler.d.ts +8 -0
  97. package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
  98. package/dist/utils/gitAuth.d.ts +84 -0
  99. package/dist/utils/gitAuthServer.d.ts +24 -0
  100. package/dist/utils/github.d.ts +78 -0
  101. package/dist/utils/globals.d.ts +3 -0
  102. package/dist/utils/install.d.ts +60 -0
  103. package/dist/utils/instructions.d.ts +48 -0
  104. package/dist/utils/leapingComment.d.ts +11 -0
  105. package/dist/utils/learnings.d.ts +62 -0
  106. package/dist/utils/learningsTruncate.d.ts +25 -0
  107. package/dist/utils/lifecycle.d.ts +57 -0
  108. package/dist/utils/log.d.ts +111 -0
  109. package/dist/utils/normalizeEnv.d.ts +30 -0
  110. package/dist/utils/openCodeModels.d.ts +11 -0
  111. package/dist/utils/overrides.d.ts +40 -0
  112. package/dist/utils/packageManager.d.ts +49 -0
  113. package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
  114. package/dist/utils/payload.d.ts +105 -0
  115. package/dist/utils/prSummary.d.ts +61 -0
  116. package/dist/utils/progressComment.d.ts +146 -0
  117. package/dist/utils/providerErrors.d.ts +31 -0
  118. package/dist/utils/rangeDiff.d.ts +51 -0
  119. package/dist/utils/remediationCommand.d.ts +55 -0
  120. package/dist/utils/retry.d.ts +13 -0
  121. package/dist/utils/reviewCleanup.d.ts +14 -0
  122. package/dist/utils/run.d.ts +9 -0
  123. package/dist/utils/runContext.d.ts +60 -0
  124. package/dist/utils/runContextData.d.ts +23 -0
  125. package/dist/utils/runErrorRenderer.d.ts +64 -0
  126. package/dist/utils/runLifecycle.d.ts +86 -0
  127. package/dist/utils/runStartupLog.d.ts +15 -0
  128. package/dist/utils/secrets.d.ts +22 -0
  129. package/dist/utils/setup.d.ts +90 -0
  130. package/dist/utils/shell.d.ts +32 -0
  131. package/dist/utils/skills.d.ts +10 -0
  132. package/dist/utils/subprocess.d.ts +80 -0
  133. package/dist/utils/terraformMcp.d.ts +42 -0
  134. package/dist/utils/time.d.ts +15 -0
  135. package/dist/utils/timer.d.ts +23 -0
  136. package/dist/utils/todoTracking.d.ts +16 -0
  137. package/dist/utils/token.d.ts +39 -0
  138. package/dist/utils/version.d.ts +2 -0
  139. package/dist/utils/versioning.d.ts +7 -0
  140. package/dist/utils/vertex.d.ts +16 -0
  141. package/dist/utils/workflow.d.ts +13 -0
  142. package/package.json +119 -0
  143. package/src/agents/claude.test.ts +1016 -0
  144. package/src/agents/claude.ts +1246 -0
  145. package/src/agents/claudePretoolGate.test.ts +28 -0
  146. package/src/agents/claudePretoolGate.ts +173 -0
  147. package/src/agents/gateServer.test.ts +204 -0
  148. package/src/agents/gateServer.ts +124 -0
  149. package/src/agents/index.ts +10 -0
  150. package/src/agents/nativeFsDenies.ts +82 -0
  151. package/src/agents/opencode.test.ts +1440 -0
  152. package/src/agents/opencode.ts +1312 -0
  153. package/src/agents/opencodePlugin.ts +222 -0
  154. package/src/agents/opencodeShared.test.ts +34 -0
  155. package/src/agents/opencodeShared.ts +121 -0
  156. package/src/agents/postRun.test.ts +549 -0
  157. package/src/agents/postRun.ts +535 -0
  158. package/src/agents/reviewer.ts +104 -0
  159. package/src/agents/sessionLabeler.test.ts +247 -0
  160. package/src/agents/sessionLabeler.ts +178 -0
  161. package/src/agents/shared.test.ts +76 -0
  162. package/src/agents/shared.ts +292 -0
  163. package/src/agents/subagentModels.test.ts +113 -0
  164. package/src/agents/subagentModels.ts +40 -0
  165. package/src/agents/subagentRegistration.test.ts +41 -0
  166. package/src/agents/subagentToolGates.ts +114 -0
  167. package/src/cli.test.ts +129 -0
  168. package/src/cli.ts +105 -0
  169. package/src/commands/gha.test.ts +192 -0
  170. package/src/commands/gha.ts +188 -0
  171. package/src/commands/mcp.ts +122 -0
  172. package/src/config.ts +1 -0
  173. package/src/entry.ts +7 -0
  174. package/src/entryPost.stdlibOnly.test.ts +109 -0
  175. package/src/entryPost.ts +99 -0
  176. package/src/external.test.ts +16 -0
  177. package/src/external.ts +302 -0
  178. package/src/index.ts +11 -0
  179. package/src/internal/index.ts +71 -0
  180. package/src/lifecycle.ts +2 -0
  181. package/src/main.test.ts +873 -0
  182. package/src/main.ts +712 -0
  183. package/src/mcp/__fixtures__/terramend-scratch-pr-49-review-3485940013.json +110 -0
  184. package/src/mcp/__fixtures__/terramend-scratch-pr-64-review-3531000326.json +14 -0
  185. package/src/mcp/__fixtures__/terramend-test-repo-pr-1.diff.json +67 -0
  186. package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
  187. package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
  188. package/src/mcp/arkConfig.ts +7 -0
  189. package/src/mcp/checkSuite.test.ts +245 -0
  190. package/src/mcp/checkSuite.ts +255 -0
  191. package/src/mcp/checkout.test.ts +752 -0
  192. package/src/mcp/checkout.ts +886 -0
  193. package/src/mcp/comment.test.ts +772 -0
  194. package/src/mcp/comment.ts +582 -0
  195. package/src/mcp/commitInfo.test.ts +127 -0
  196. package/src/mcp/commitInfo.ts +61 -0
  197. package/src/mcp/crosswalk.test.ts +106 -0
  198. package/src/mcp/crosswalk.ts +339 -0
  199. package/src/mcp/dependencies.test.ts +309 -0
  200. package/src/mcp/dependencies.ts +189 -0
  201. package/src/mcp/geminiSanitizer.test.ts +287 -0
  202. package/src/mcp/geminiSanitizer.ts +207 -0
  203. package/src/mcp/git.test.ts +1083 -0
  204. package/src/mcp/git.ts +890 -0
  205. package/src/mcp/guardrails.test.ts +705 -0
  206. package/src/mcp/guardrails.ts +465 -0
  207. package/src/mcp/issue.test.ts +113 -0
  208. package/src/mcp/issue.ts +73 -0
  209. package/src/mcp/issueComments.test.ts +69 -0
  210. package/src/mcp/issueComments.ts +48 -0
  211. package/src/mcp/issueEvents.test.ts +134 -0
  212. package/src/mcp/issueEvents.ts +100 -0
  213. package/src/mcp/issueInfo.test.ts +104 -0
  214. package/src/mcp/issueInfo.ts +72 -0
  215. package/src/mcp/labels.test.ts +52 -0
  216. package/src/mcp/labels.ts +34 -0
  217. package/src/mcp/localContext.ts +28 -0
  218. package/src/mcp/localServer.test.ts +75 -0
  219. package/src/mcp/localServer.ts +131 -0
  220. package/src/mcp/moduleExtraction.test.ts +261 -0
  221. package/src/mcp/moduleExtraction.ts +313 -0
  222. package/src/mcp/moduleTests.test.ts +269 -0
  223. package/src/mcp/moduleTests.ts +421 -0
  224. package/src/mcp/modules.test.ts +640 -0
  225. package/src/mcp/modules.ts +696 -0
  226. package/src/mcp/output.test.ts +96 -0
  227. package/src/mcp/output.ts +70 -0
  228. package/src/mcp/pathSafety.test.ts +44 -0
  229. package/src/mcp/pathSafety.ts +28 -0
  230. package/src/mcp/policy.test.ts +282 -0
  231. package/src/mcp/policy.ts +199 -0
  232. package/src/mcp/pr.test.ts +387 -0
  233. package/src/mcp/pr.ts +194 -0
  234. package/src/mcp/prInfo.test.ts +96 -0
  235. package/src/mcp/prInfo.ts +91 -0
  236. package/src/mcp/providerSchema.test.ts +85 -0
  237. package/src/mcp/providerSchema.ts +175 -0
  238. package/src/mcp/review.test.ts +936 -0
  239. package/src/mcp/review.ts +923 -0
  240. package/src/mcp/reviewComments.test.ts +549 -0
  241. package/src/mcp/reviewComments.ts +896 -0
  242. package/src/mcp/roots.test.ts +175 -0
  243. package/src/mcp/roots.ts +217 -0
  244. package/src/mcp/scope.test.ts +59 -0
  245. package/src/mcp/scope.ts +65 -0
  246. package/src/mcp/security.test.ts +720 -0
  247. package/src/mcp/selectMode.test.ts +210 -0
  248. package/src/mcp/selectMode.ts +181 -0
  249. package/src/mcp/server.test.ts +292 -0
  250. package/src/mcp/server.ts +403 -0
  251. package/src/mcp/shared.ts +100 -0
  252. package/src/mcp/shell.test.ts +520 -0
  253. package/src/mcp/shell.ts +505 -0
  254. package/src/mcp/staleFix.test.ts +237 -0
  255. package/src/mcp/staleFix.ts +277 -0
  256. package/src/mcp/terraform/cost.ts +163 -0
  257. package/src/mcp/terraform/currency.test.ts +338 -0
  258. package/src/mcp/terraform/currency.ts +336 -0
  259. package/src/mcp/terraform/decisions.ts +527 -0
  260. package/src/mcp/terraform/findings.ts +333 -0
  261. package/src/mcp/terraform/plan.ts +348 -0
  262. package/src/mcp/terraform/scanners.ts +809 -0
  263. package/src/mcp/terraform/tools.test.ts +1071 -0
  264. package/src/mcp/terraform/tools.ts +908 -0
  265. package/src/mcp/terraform/types.ts +305 -0
  266. package/src/mcp/terraform.test.ts +1957 -0
  267. package/src/mcp/terraform.ts +23 -0
  268. package/src/mcp/terratest.test.ts +105 -0
  269. package/src/mcp/terratest.ts +196 -0
  270. package/src/mcp/toolFiltering.test.ts +85 -0
  271. package/src/mcp/upload.test.ts +180 -0
  272. package/src/mcp/upload.ts +112 -0
  273. package/src/models.test.ts +300 -0
  274. package/src/models.ts +708 -0
  275. package/src/modes.test.ts +107 -0
  276. package/src/modes.ts +880 -0
  277. package/src/prep/index.ts +43 -0
  278. package/src/prep/installNodeDependencies.test.ts +298 -0
  279. package/src/prep/installNodeDependencies.ts +196 -0
  280. package/src/prep/installPythonDependencies.test.ts +268 -0
  281. package/src/prep/installPythonDependencies.ts +199 -0
  282. package/src/prep/types.ts +38 -0
  283. package/src/reviewQuality.test.ts +63 -0
  284. package/src/reviewQuality.ts +134 -0
  285. package/src/runCli.test.ts +214 -0
  286. package/src/runCli.ts +282 -0
  287. package/src/skills/terraform-best-practices/SKILL.md +369 -0
  288. package/src/toolState.test.ts +45 -0
  289. package/src/toolState.ts +252 -0
  290. package/src/utils/activity.test.ts +188 -0
  291. package/src/utils/activity.ts +210 -0
  292. package/src/utils/agent.test.ts +251 -0
  293. package/src/utils/agent.ts +139 -0
  294. package/src/utils/agentHangReport.test.ts +203 -0
  295. package/src/utils/agentHangReport.ts +170 -0
  296. package/src/utils/apiFetch.test.ts +115 -0
  297. package/src/utils/apiFetch.ts +62 -0
  298. package/src/utils/apiKeys.test.ts +344 -0
  299. package/src/utils/apiKeys.ts +206 -0
  300. package/src/utils/apiUrl.test.ts +30 -0
  301. package/src/utils/apiUrl.ts +59 -0
  302. package/src/utils/assets.test.ts +153 -0
  303. package/src/utils/assets.ts +107 -0
  304. package/src/utils/billingErrors.test.ts +121 -0
  305. package/src/utils/billingErrors.ts +189 -0
  306. package/src/utils/body.test.ts +217 -0
  307. package/src/utils/body.ts +168 -0
  308. package/src/utils/buildTerramendFooter.test.ts +38 -0
  309. package/src/utils/buildTerramendFooter.ts +82 -0
  310. package/src/utils/byokFallback.test.ts +205 -0
  311. package/src/utils/byokFallback.ts +128 -0
  312. package/src/utils/claudeSubscription.test.ts +179 -0
  313. package/src/utils/claudeSubscription.ts +93 -0
  314. package/src/utils/cli.ts +31 -0
  315. package/src/utils/codexHome.test.ts +190 -0
  316. package/src/utils/codexHome.ts +191 -0
  317. package/src/utils/codexOAuth.ts +147 -0
  318. package/src/utils/codexRefreshDetect.test.ts +85 -0
  319. package/src/utils/codexRefreshDetect.ts +35 -0
  320. package/src/utils/diffCoverage.test.ts +468 -0
  321. package/src/utils/diffCoverage.ts +404 -0
  322. package/src/utils/errorReport.test.ts +135 -0
  323. package/src/utils/errorReport.ts +83 -0
  324. package/src/utils/exitHandler.ts +35 -0
  325. package/src/utils/fixDoubleEscapedString.ts +9 -0
  326. package/src/utils/ghaCore.ts +13 -0
  327. package/src/utils/gitAuth.test.ts +322 -0
  328. package/src/utils/gitAuth.ts +263 -0
  329. package/src/utils/gitAuthServer.test.ts +260 -0
  330. package/src/utils/gitAuthServer.ts +182 -0
  331. package/src/utils/github.test.ts +615 -0
  332. package/src/utils/github.ts +538 -0
  333. package/src/utils/globals.ts +9 -0
  334. package/src/utils/humanEditCapture.test.ts +100 -0
  335. package/src/utils/humanEditCapture.ts +193 -0
  336. package/src/utils/install.test.ts +768 -0
  337. package/src/utils/install.ts +492 -0
  338. package/src/utils/instructions.test.ts +240 -0
  339. package/src/utils/instructions.ts +543 -0
  340. package/src/utils/leapingComment.test.ts +51 -0
  341. package/src/utils/leapingComment.ts +18 -0
  342. package/src/utils/learnings.test.ts +87 -0
  343. package/src/utils/learnings.ts +138 -0
  344. package/src/utils/learningsTocRender.test.ts +116 -0
  345. package/src/utils/learningsTruncate.test.ts +39 -0
  346. package/src/utils/learningsTruncate.ts +42 -0
  347. package/src/utils/lifecycle.test.ts +195 -0
  348. package/src/utils/lifecycle.ts +198 -0
  349. package/src/utils/log.test.ts +402 -0
  350. package/src/utils/log.ts +432 -0
  351. package/src/utils/normalizeEnv.test.ts +91 -0
  352. package/src/utils/normalizeEnv.ts +106 -0
  353. package/src/utils/openCodeModels.ts +82 -0
  354. package/src/utils/overrides.test.ts +89 -0
  355. package/src/utils/overrides.ts +98 -0
  356. package/src/utils/packageManager.test.ts +321 -0
  357. package/src/utils/packageManager.ts +257 -0
  358. package/src/utils/patchWorkflowRunFields.test.ts +92 -0
  359. package/src/utils/patchWorkflowRunFields.ts +150 -0
  360. package/src/utils/payload.test.ts +497 -0
  361. package/src/utils/payload.ts +371 -0
  362. package/src/utils/postApiFetch.ts +51 -0
  363. package/src/utils/prSummary.test.ts +224 -0
  364. package/src/utils/prSummary.ts +147 -0
  365. package/src/utils/progressComment.ts +261 -0
  366. package/src/utils/providerErrors.test.ts +315 -0
  367. package/src/utils/providerErrors.ts +172 -0
  368. package/src/utils/rangeDiff.test.ts +236 -0
  369. package/src/utils/rangeDiff.ts +182 -0
  370. package/src/utils/remediationCommand.test.ts +163 -0
  371. package/src/utils/remediationCommand.ts +119 -0
  372. package/src/utils/retry.test.ts +153 -0
  373. package/src/utils/retry.ts +58 -0
  374. package/src/utils/reviewCleanup.ts +106 -0
  375. package/src/utils/run.ts +99 -0
  376. package/src/utils/runContext.ts +145 -0
  377. package/src/utils/runContextData.ts +58 -0
  378. package/src/utils/runErrorRenderer.test.ts +95 -0
  379. package/src/utils/runErrorRenderer.ts +259 -0
  380. package/src/utils/runFixture.ts +76 -0
  381. package/src/utils/runLifecycle.ts +237 -0
  382. package/src/utils/runStartupLog.ts +60 -0
  383. package/src/utils/secrets.test.ts +103 -0
  384. package/src/utils/secrets.ts +177 -0
  385. package/src/utils/setup.test.ts +509 -0
  386. package/src/utils/setup.ts +352 -0
  387. package/src/utils/shell.ts +103 -0
  388. package/src/utils/skills.test.ts +46 -0
  389. package/src/utils/skills.ts +67 -0
  390. package/src/utils/subprocess.test.ts +170 -0
  391. package/src/utils/subprocess.ts +438 -0
  392. package/src/utils/terraformMcp.test.ts +63 -0
  393. package/src/utils/terraformMcp.ts +83 -0
  394. package/src/utils/time.test.ts +105 -0
  395. package/src/utils/time.ts +59 -0
  396. package/src/utils/timer.test.ts +91 -0
  397. package/src/utils/timer.ts +72 -0
  398. package/src/utils/todoTracking.test.ts +223 -0
  399. package/src/utils/todoTracking.ts +167 -0
  400. package/src/utils/token.test.ts +239 -0
  401. package/src/utils/token.ts +186 -0
  402. package/src/utils/version.ts +10 -0
  403. package/src/utils/versioning.test.ts +34 -0
  404. package/src/utils/versioning.ts +44 -0
  405. package/src/utils/vertex.ts +85 -0
  406. package/src/utils/workflow.ts +25 -0
@@ -0,0 +1,28 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { describe, expect, it } from "vitest";
3
+ import { CLAUDE_CODE_AGENT_ID_VERIFIED_VERSION } from "#app/agents/claudePretoolGate";
4
+
5
+ // Tripwire for the subagent gate's load-bearing assumption: claude-code
6
+ // populates `agent_id` in the PreToolUse hook payload for subagent tool calls
7
+ // (the gate fails OPEN for subagents otherwise). claude-code ships as a native
8
+ // binary, so we can't assert the behavior statically — instead we pin the
9
+ // verified version and fail CI on any bump, forcing a human to re-verify
10
+ // `createBaseHookInput` before updating the pin + the constant together.
11
+ describe("subagent gate ↔ claude-code agent_id contract", () => {
12
+ const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf-8")) as {
13
+ devDependencies?: Record<string, string>;
14
+ dependencies?: Record<string, string>;
15
+ };
16
+
17
+ it("pinned @anthropic-ai/claude-code matches the verified version", () => {
18
+ const pinned =
19
+ pkg.devDependencies?.["@anthropic-ai/claude-code"] ??
20
+ pkg.dependencies?.["@anthropic-ai/claude-code"];
21
+ expect(
22
+ pinned,
23
+ "claude-code was bumped: re-verify that subagent tool calls still populate " +
24
+ "`agent_id` in the PreToolUse hook (createBaseHookInput) BEFORE updating " +
25
+ "CLAUDE_CODE_AGENT_ID_VERIFIED_VERSION — the subagent gate fails OPEN otherwise.",
26
+ ).toBe(CLAUDE_CODE_AGENT_ID_VERIFIED_VERSION);
27
+ });
28
+ });
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Claude Code `PreToolUse` hook source — written into `ctx.tmpdir` at runtime
3
+ * and registered via a tmpdir-scoped `settings.json` referenced by
4
+ * `--settings <path>` (see action/agents/claude.ts).
5
+ *
6
+ * Closes the subagent → state-mutating MCP tool path that motivated the
7
+ * 2026-05-18 zed-industries/cloud incident (`reviewfrog` lens called
8
+ * `checkout_pr` mid-review and the orchestrator's next push clobbered an
9
+ * unrelated branch). Pairs with the `tool.execute.before` hook in
10
+ * action/agents/opencodePlugin.ts; both runtimes share the deny list at
11
+ * action/agents/subagentToolGates.ts.
12
+ *
13
+ * PreToolUse hook contract (verified against yasasbanukaofficial/claude-code
14
+ * `src/utils/hooks/hooksConfigManager.ts` and `src/utils/hooks.ts`):
15
+ * - stdin: JSON with `hook_event_name: "PreToolUse"`, `tool_name`,
16
+ * `tool_input`, `tool_use_id`, `session_id`, `cwd`, `transcript_path`,
17
+ * and crucially `agent_id` / `agent_type` populated when the call
18
+ * originates from a subagent (set by the SDK when a Task/Agent
19
+ * dispatches a tool — see `createBaseHookInput` in claude-code source).
20
+ * - exit 0 → allow, no output shown
21
+ * - exit 2 → block tool call AND show stderr to model (this is the path
22
+ * we want for the deny case — the subagent gets a clear refusal it can
23
+ * reason about and pick a different action)
24
+ * - other → show stderr to user only, continue with tool call
25
+ *
26
+ * The hook itself is intentionally tiny: stdin → JSON → check `agent_id`
27
+ * presence + `tool_name` against the deny list → exit 0 or 2. No deps.
28
+ *
29
+ * Why the script source is a string template, not a separate `.ts` file
30
+ * shipped with the action: the action runs as a published npm package; at
31
+ * install time we don't have the source on disk in a stable place. Embedding
32
+ * the source into `dist/main.mjs` and writing it out per-run keeps the path
33
+ * inside `ctx.tmpdir` (where `--settings` can find it) and survives bundle
34
+ * minification.
35
+ */
36
+
37
+ import { SUBAGENT_DENIED_TOOLS } from "#app/agents/subagentToolGates";
38
+
39
+ /**
40
+ * The pinned `@anthropic-ai/claude-code` version against which the subagent
41
+ * gate's `agent_id` discriminator was last verified (see the contract notes in
42
+ * the gate source below). The gate fails OPEN for subagents if claude-code ever
43
+ * stops populating `agent_id` in the PreToolUse hook payload, so a version bump
44
+ * must be paired with a re-verification of `createBaseHookInput`.
45
+ *
46
+ * `claudePretoolGate.test.ts` asserts this equals the version pinned in
47
+ * `package.json` — that test fails on any bump, forcing the re-verification
48
+ * before the pin and this constant are updated together.
49
+ *
50
+ * 2.1.170 verified 2026-06-10 against the schema embedded in the shipped
51
+ * binary: the base hook input declares `agent_id` as optional with the
52
+ * describe-text "Present only when the hook fires from within a subagent
53
+ * (e.g., a tool called by an AgentTool worker). Absent for the main thread,
54
+ * even in --agent sessions." — exactly the discriminator the gate relies on.
55
+ */
56
+ export const CLAUDE_CODE_AGENT_ID_VERIFIED_VERSION = "2.1.170" as const;
57
+
58
+ /**
59
+ * Source written to `<ctx.tmpdir>/terramend-pretool-gate.mjs`. Plain ESM,
60
+ * no TypeScript, no dependencies — node executes it directly via the
61
+ * `#!/usr/bin/env node` shebang and the executable bit set by the harness.
62
+ */
63
+ export const CLAUDE_PRETOOL_GATE_FILENAME = "terramend-pretool-gate.mjs" as const;
64
+
65
+ export const CLAUDE_PRETOOL_GATE_SOURCE = `#!/usr/bin/env node
66
+ // AUTOGENERATED by Terramend. PreToolUse hook that hard-blocks state-mutating
67
+ // MCP tool calls from subagents. See action/agents/claudePretoolGate.ts.
68
+
69
+ const SUBAGENT_DENIED_TOOLS = new Set(${JSON.stringify(SUBAGENT_DENIED_TOOLS)});
70
+
71
+ function stripMcpPrefix(toolName) {
72
+ if (toolName.startsWith("mcp__terramend__")) return toolName.slice("mcp__terramend__".length);
73
+ return toolName;
74
+ }
75
+
76
+ let stdin = "";
77
+ process.stdin.setEncoding("utf8");
78
+ process.stdin.on("data", (chunk) => {
79
+ stdin += chunk;
80
+ });
81
+ process.stdin.on("end", () => {
82
+ let payload;
83
+ try {
84
+ payload = JSON.parse(stdin);
85
+ } catch {
86
+ // malformed input — exit non-blocking so the run continues. user-only
87
+ // stderr per the hook contract.
88
+ process.stderr.write("terramend-pretool-gate: malformed stdin JSON\\n");
89
+ process.exit(1);
90
+ }
91
+ const toolName = typeof payload?.tool_name === "string" ? payload.tool_name : "";
92
+ // claude-code populates agent_id whenever a tool call originates inside
93
+ // a Task/Agent subagent dispatch (createBaseHookInput in claude-code
94
+ // source). on the orchestrator's main thread agent_id is undefined.
95
+ // agent_type can be set on the orchestrator itself via --agent, so it's
96
+ // not a reliable subagent discriminator on its own; agent_id is.
97
+ // contract verified against @anthropic-ai/claude-code 2.1.170 (pinned in
98
+ // package.json; see CLAUDE_CODE_AGENT_ID_VERIFIED_VERSION + the version
99
+ // tripwire in claudePretoolGate.test.ts); re-verify createBaseHookInput if
100
+ // that bumps — if agent_id ever stops being populated the gate fails OPEN.
101
+ const agentId = typeof payload?.agent_id === "string" ? payload.agent_id : "";
102
+ if (!agentId) process.exit(0);
103
+ const bare = stripMcpPrefix(toolName);
104
+ if (!SUBAGENT_DENIED_TOOLS.has(bare)) process.exit(0);
105
+ process.stderr.write(
106
+ "subagent attempted to call denied tool '" + bare + "'. " +
107
+ "subagents share the orchestrator's in-process working tree and toolState; " +
108
+ "state-changing MCP tools (checkout_pr, push_branch, create_pull_request_review, " +
109
+ "report_progress, etc.) are reserved for the orchestrator. " +
110
+ "report findings back to the orchestrator and let it perform the mutation.\\n"
111
+ );
112
+ process.exit(2);
113
+ });
114
+ `;
115
+
116
+ /**
117
+ * Settings JSON shape registered via `claude --settings <path>`. The
118
+ * matcher `^mcp__terramend__` is treated as a regex by claude-code's
119
+ * `matchesPattern` helper (anything outside `[a-zA-Z0-9_|]` triggers the
120
+ * regex branch — verified in src/utils/hooks.ts), so this anchors at the
121
+ * start of the tool name and fires for every Terramend MCP tool. We narrow
122
+ * inside the script itself rather than declaring per-tool matchers because
123
+ * the deny list is the source of truth.
124
+ *
125
+ * The hook process inherits the parent's PATH, so `node` resolves to the
126
+ * runner's node binary; the `--settings` flag accepts either a path or a
127
+ * literal JSON string per claude-code source `src/main.tsx` (`Path to a
128
+ * settings JSON file or a JSON string`), but we use a path so the script
129
+ * and its config sit side-by-side under `ctx.tmpdir`.
130
+ *
131
+ * `execToolDenyRules` are the native exec tools (Bash/Monitor/REPL/Workflow +
132
+ * their `Agent(...)` forms) to deny at a settings-source rule — the
133
+ * authoritative, bypass-immune layer. `--disallowedTools` alone (a `cliArg`
134
+ * deny) was observed to leak under `--dangerously-skip-permissions`, so the
135
+ * deny is carried here too. Both consumers use both returned fields: the flag
136
+ * `--settings` JSON (covers non-CI runs) writes the whole object, and
137
+ * `buildManagedSettings` (CI, /etc managed settings) spreads `hooks` and folds
138
+ * `permissions.deny` into its richer deny list.
139
+ */
140
+ export function buildClaudePretoolGateSettings(
141
+ scriptAbsolutePath: string,
142
+ execToolDenyRules: string[],
143
+ ): {
144
+ hooks: {
145
+ PreToolUse: Array<{
146
+ matcher: string;
147
+ hooks: Array<{ type: "command"; command: string; timeout?: number }>;
148
+ }>;
149
+ };
150
+ permissions: { deny: string[] };
151
+ } {
152
+ return {
153
+ hooks: {
154
+ PreToolUse: [
155
+ {
156
+ matcher: "^mcp__terramend__",
157
+ hooks: [
158
+ {
159
+ type: "command",
160
+ // shell-quote-safe because tmpdir paths created via
161
+ // node:fs.mkdtempSync don't contain spaces, but pass via a
162
+ // literal that node parses as a single argv entry to be
163
+ // defensive against future tmpdir layout changes.
164
+ command: `node ${JSON.stringify(scriptAbsolutePath)}`,
165
+ timeout: 5,
166
+ },
167
+ ],
168
+ },
169
+ ],
170
+ },
171
+ permissions: { deny: execToolDenyRules },
172
+ };
173
+ }
@@ -0,0 +1,204 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { type GateServerHandle, startGateServer } from "#app/agents/gateServer";
3
+ import type { AgentRunContext, PostRunIssues } from "#app/agents/shared";
4
+
5
+ const collectPostRunIssuesMock = vi.fn();
6
+ const shouldRunReflectionMock = vi.fn();
7
+
8
+ vi.mock("#app/agents/postRun", () => ({
9
+ collectPostRunIssues: (ctx: unknown, opts: unknown) => collectPostRunIssuesMock(ctx, opts),
10
+ buildPostRunPrompt: (issues: PostRunIssues) => `post-run prompt: ${JSON.stringify(issues)}`,
11
+ buildLearningsReflectionPrompt: (path: string) => `reflection prompt for ${path}`,
12
+ shouldRunReflection: (mode: string | undefined) => shouldRunReflectionMock(mode),
13
+ }));
14
+
15
+ const noIssues: PostRunIssues = {};
16
+ const dirtyIssues: PostRunIssues = { dirtyTree: "M src/main.tf" };
17
+
18
+ function makeCtx(
19
+ toolState: { learningsFilePath?: string; selectedMode?: string } = {},
20
+ ): AgentRunContext {
21
+ return { toolState } as unknown as AgentRunContext;
22
+ }
23
+
24
+ let handle: GateServerHandle | undefined;
25
+
26
+ async function startServer(ctx: AgentRunContext): Promise<GateServerHandle> {
27
+ handle = await startGateServer(ctx);
28
+ return handle;
29
+ }
30
+
31
+ function gateFetch(server: GateServerHandle, init?: RequestInit): Promise<Response> {
32
+ return fetch(server.url, {
33
+ headers: { authorization: `Bearer ${server.token}` },
34
+ ...init,
35
+ });
36
+ }
37
+
38
+ beforeEach(() => {
39
+ collectPostRunIssuesMock.mockResolvedValue(noIssues);
40
+ shouldRunReflectionMock.mockReturnValue(false);
41
+ });
42
+
43
+ afterEach(async () => {
44
+ if (handle) {
45
+ await handle[Symbol.asyncDispose]();
46
+ handle = undefined;
47
+ }
48
+ vi.clearAllMocks();
49
+ });
50
+
51
+ describe("gate server routing and auth", () => {
52
+ it("binds a loopback url and a uuid bearer token", async () => {
53
+ const server = await startServer(makeCtx());
54
+ expect(server.url).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/gates$/);
55
+ expect(server.token).toMatch(/^[0-9a-f-]{36}$/);
56
+ });
57
+
58
+ it("returns 404 for unknown paths and non-GET methods", async () => {
59
+ const server = await startServer(makeCtx());
60
+
61
+ const wrongPath = await fetch(server.url.replace("/gates", "/other"), {
62
+ headers: { authorization: `Bearer ${server.token}` },
63
+ });
64
+ expect(wrongPath.status).toBe(404);
65
+
66
+ const wrongMethod = await gateFetch(server, {
67
+ method: "POST",
68
+ headers: { authorization: `Bearer ${server.token}` },
69
+ });
70
+ expect(wrongMethod.status).toBe(404);
71
+ });
72
+
73
+ it("rejects missing or wrong bearer tokens without consuming budget", async () => {
74
+ collectPostRunIssuesMock.mockResolvedValue(dirtyIssues);
75
+ const server = await startServer(makeCtx());
76
+
77
+ const noAuth = await fetch(server.url);
78
+ expect(noAuth.status).toBe(403);
79
+
80
+ const wrongAuth = await fetch(server.url, {
81
+ headers: { authorization: "Bearer not-the-token" },
82
+ });
83
+ expect(wrongAuth.status).toBe(403);
84
+
85
+ // gate state was never read — the budget is untouched
86
+ expect(collectPostRunIssuesMock).not.toHaveBeenCalled();
87
+
88
+ // all MAX_POST_RUN_RETRIES blocks are still available afterwards
89
+ for (let i = 0; i < 3; i++) {
90
+ const res = await gateFetch(server);
91
+ expect(await res.json()).toMatchObject({ block: true });
92
+ }
93
+ });
94
+ });
95
+
96
+ describe("gate decisions", () => {
97
+ it("allows the stop when gates are clean and no reflection is pending", async () => {
98
+ const server = await startServer(makeCtx());
99
+ const res = await gateFetch(server);
100
+ expect(res.status).toBe(200);
101
+ expect(res.headers.get("content-type")).toBe("application/json");
102
+ expect(await res.json()).toEqual({ block: false });
103
+ });
104
+
105
+ it("blocks with the combined gate prompt while issues persist", async () => {
106
+ collectPostRunIssuesMock.mockResolvedValue(dirtyIssues);
107
+ const server = await startServer(makeCtx());
108
+
109
+ const res = await gateFetch(server);
110
+ const body = (await res.json()) as { block: boolean; reason: string };
111
+ expect(body.block).toBe(true);
112
+ expect(body.reason).toContain("post-run prompt:");
113
+ expect(body.reason).toContain("M src/main.tf");
114
+ });
115
+
116
+ it("allows the stop once the retry budget is exhausted (terminal hard-fail)", async () => {
117
+ collectPostRunIssuesMock.mockResolvedValue(dirtyIssues);
118
+ const server = await startServer(makeCtx());
119
+
120
+ for (let i = 0; i < 3; i++) {
121
+ const res = await gateFetch(server);
122
+ expect(await res.json()).toMatchObject({ block: true });
123
+ }
124
+
125
+ const exhausted = await gateFetch(server);
126
+ expect(await exhausted.json()).toEqual({ block: false });
127
+ });
128
+
129
+ it("nudges summaryStale exactly once, then skips it on later collections", async () => {
130
+ collectPostRunIssuesMock
131
+ .mockResolvedValueOnce({ summaryStale: { filePath: "/tmp/summary.md" } })
132
+ .mockResolvedValue(noIssues);
133
+ const server = await startServer(makeCtx());
134
+
135
+ const first = await gateFetch(server);
136
+ expect(await first.json()).toMatchObject({ block: true });
137
+ expect(collectPostRunIssuesMock).toHaveBeenLastCalledWith(expect.anything(), {
138
+ skipSummaryStale: false,
139
+ });
140
+
141
+ const second = await gateFetch(server);
142
+ expect(await second.json()).toEqual({ block: false });
143
+ expect(collectPostRunIssuesMock).toHaveBeenLastCalledWith(expect.anything(), {
144
+ skipSummaryStale: true,
145
+ });
146
+ });
147
+
148
+ it("delivers the reflection nudge once when gates are clean", async () => {
149
+ shouldRunReflectionMock.mockReturnValue(true);
150
+ const server = await startServer(
151
+ makeCtx({ learningsFilePath: "/tmp/learnings.md", selectedMode: "Remediate" }),
152
+ );
153
+
154
+ const first = await gateFetch(server);
155
+ const body = (await first.json()) as { block: boolean; reason: string };
156
+ expect(body.block).toBe(true);
157
+ expect(body.reason).toBe("reflection prompt for /tmp/learnings.md");
158
+ expect(shouldRunReflectionMock).toHaveBeenCalledWith("Remediate");
159
+
160
+ // one-shot: the second stop is allowed
161
+ const second = await gateFetch(server);
162
+ expect(await second.json()).toEqual({ block: false });
163
+ });
164
+
165
+ it("skips reflection when no learnings file was seeded", async () => {
166
+ shouldRunReflectionMock.mockReturnValue(true);
167
+ const server = await startServer(makeCtx({ selectedMode: "Remediate" }));
168
+
169
+ const res = await gateFetch(server);
170
+ expect(await res.json()).toEqual({ block: false });
171
+ expect(shouldRunReflectionMock).not.toHaveBeenCalled();
172
+ });
173
+
174
+ it("skips reflection when the selected mode opts out", async () => {
175
+ shouldRunReflectionMock.mockReturnValue(false);
176
+ const server = await startServer(
177
+ makeCtx({ learningsFilePath: "/tmp/learnings.md", selectedMode: "Review" }),
178
+ );
179
+
180
+ const res = await gateFetch(server);
181
+ expect(await res.json()).toEqual({ block: false });
182
+ });
183
+
184
+ it("allows the stop when the gate collection itself throws", async () => {
185
+ collectPostRunIssuesMock.mockRejectedValue(new Error("github 500"));
186
+ const server = await startServer(makeCtx());
187
+
188
+ const res = await gateFetch(server);
189
+ expect(res.status).toBe(200);
190
+ expect(await res.json()).toEqual({ block: false });
191
+ });
192
+ });
193
+
194
+ describe("gate server disposal", () => {
195
+ it("stops accepting connections after dispose", async () => {
196
+ const server = await startServer(makeCtx());
197
+ const url = server.url;
198
+ await server[Symbol.asyncDispose]();
199
+ handle = undefined;
200
+
201
+ const err = await fetch(url).catch((e: unknown) => e);
202
+ expect(err).toBeInstanceOf(Error);
203
+ });
204
+ });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * gate server — localhost HTTP sidecar that returns Stop-hook decisions for
3
+ * the Claude harness. the managed Stop hook (see `action/agents/claude.ts`)
4
+ * curls this server on every stop and writes back the resulting
5
+ * `{decision: "block", reason}` to inject a follow-up turn inside the live
6
+ * `queryLoop`, replacing the pre-PR-#795 `--resume <sessionId>` subprocess.
7
+ *
8
+ * the server captures `ctx` by closure so every request reads the latest
9
+ * mutations from MCP tool callbacks (which run in the same process). the
10
+ * counters live here, not on `toolState`, because gate-retry budget and
11
+ * reflection one-shot are harness concerns the literal `toolState` design
12
+ * rule (see `action/toolState.ts`) explicitly excludes.
13
+ *
14
+ * decision policy mirrors the old `runPostRunRetryLoop`:
15
+ * - gates dirty + budget remaining → block with combined gate prompt
16
+ * - gates clean + reflection pending → block with reflection prompt
17
+ * - otherwise → allow stop
18
+ * `summaryStale` is a one-shot nudge; once delivered the gate is suppressed
19
+ * so a deliberately-unchanged file doesn't burn the retry budget. when the
20
+ * budget is exhausted with hard-fail gates still failing, we allow the stop
21
+ * so `finalizeAgentResult` can render the terminal `AgentResult.success =
22
+ * false` — that state cannot be set from a stdout-only hook.
23
+ */
24
+ import { randomUUID } from "node:crypto";
25
+ import { createServer } from "node:http";
26
+ import {
27
+ buildLearningsReflectionPrompt,
28
+ buildPostRunPrompt,
29
+ collectPostRunIssues,
30
+ shouldRunReflection,
31
+ } from "#app/agents/postRun";
32
+ import { type AgentRunContext, hasPostRunIssues, MAX_POST_RUN_RETRIES } from "#app/agents/shared";
33
+ import { log } from "#app/utils/cli";
34
+
35
+ export interface GateServerHandle {
36
+ url: string;
37
+ // bearer token the Stop hook must present. Passed to the agent process via a
38
+ // `_TOKEN`-suffixed env var so filterEnv() strips it from the agent's shell
39
+ // sandbox — only the real Stop hook (a child of the agent process, inheriting
40
+ // its full env) can read it. Without this, any loopback caller (incl. the
41
+ // agent) could poison the retry budget by hitting GET /gates.
42
+ token: string;
43
+ [Symbol.asyncDispose]: () => Promise<void>;
44
+ }
45
+
46
+ export async function startGateServer(ctx: AgentRunContext): Promise<GateServerHandle> {
47
+ let blockCount = 0;
48
+ let reflectionDelivered = false;
49
+ let summaryStaleNudged = false;
50
+ const token = randomUUID();
51
+
52
+ const server = createServer((req, res) => {
53
+ void (async () => {
54
+ if (req.method !== "GET" || req.url !== "/gates") {
55
+ res.writeHead(404).end();
56
+ return;
57
+ }
58
+ // Authenticate before reading state or mutating counters — an unauthorized
59
+ // request must not consume the retry budget.
60
+ if (req.headers.authorization !== `Bearer ${token}`) {
61
+ res.writeHead(403).end();
62
+ return;
63
+ }
64
+ try {
65
+ const issues = await collectPostRunIssues(ctx, { skipSummaryStale: summaryStaleNudged });
66
+ if (hasPostRunIssues(issues)) {
67
+ if (blockCount >= MAX_POST_RUN_RETRIES) {
68
+ log.info("» gate-server: retry budget exhausted, allowing stop for terminal hard-fail");
69
+ res.writeHead(200, { "content-type": "application/json" }).end('{"block":false}');
70
+ return;
71
+ }
72
+ if (issues.summaryStale) summaryStaleNudged = true;
73
+ blockCount++;
74
+ const reason = buildPostRunPrompt(issues);
75
+ log.info(`» gate-server: blocking (attempt ${blockCount}/${MAX_POST_RUN_RETRIES})`);
76
+ res
77
+ .writeHead(200, { "content-type": "application/json" })
78
+ .end(JSON.stringify({ block: true, reason }));
79
+ return;
80
+ }
81
+ const learningsPath = ctx.toolState.learningsFilePath;
82
+ if (
83
+ !reflectionDelivered &&
84
+ learningsPath &&
85
+ shouldRunReflection(ctx.toolState.selectedMode)
86
+ ) {
87
+ reflectionDelivered = true;
88
+ const reason = buildLearningsReflectionPrompt(learningsPath);
89
+ log.info("» gate-server: delivering reflection nudge");
90
+ res
91
+ .writeHead(200, { "content-type": "application/json" })
92
+ .end(JSON.stringify({ block: true, reason }));
93
+ return;
94
+ }
95
+ res.writeHead(200, { "content-type": "application/json" }).end('{"block":false}');
96
+ } catch (err) {
97
+ const msg = err instanceof Error ? err.message : String(err);
98
+ log.warning(`» gate-server handler error: ${msg}`);
99
+ // never block claude on a server-side fault — allow the stop and
100
+ // let the harness's terminal hard-fail path own the decision.
101
+ res.writeHead(200, { "content-type": "application/json" }).end('{"block":false}');
102
+ }
103
+ })();
104
+ });
105
+
106
+ await new Promise<void>((resolve, reject) => {
107
+ server.once("error", reject);
108
+ server.listen(0, "127.0.0.1", () => resolve());
109
+ });
110
+ const addr = server.address();
111
+ if (!addr || typeof addr === "string") {
112
+ throw new Error("gate-server: failed to bind localhost port");
113
+ }
114
+ const url = `http://127.0.0.1:${addr.port}/gates`;
115
+ log.debug(`» gate-server listening at ${url}`);
116
+
117
+ return {
118
+ url,
119
+ token,
120
+ [Symbol.asyncDispose]: async () => {
121
+ await new Promise<void>((resolve) => server.close(() => resolve()));
122
+ },
123
+ };
124
+ }
@@ -0,0 +1,10 @@
1
+ import { claude } from "#app/agents/claude";
2
+ // In-process OpenCode harness, adapted to opencode-ai >=1.14.x SDK-v2 /
3
+ // Effect-ts CLI rewrite. (The legacy CLI-subprocess harness was removed; see
4
+ // git history if a revert is ever needed.)
5
+ import { opencode } from "#app/agents/opencode";
6
+ import type { Agent } from "#app/agents/shared";
7
+
8
+ export type { Agent, AgentUsage } from "#app/agents/shared";
9
+
10
+ export const agents = { claude, opencode } satisfies Record<string, Agent>;
@@ -0,0 +1,82 @@
1
+ // Canonical native-FS-tool deny set shared by the OpenCode and Claude harnesses.
2
+ //
3
+ // The agent's NATIVE FS tools (Read/Write/Edit/Glob/Grep) run in the agent
4
+ // process OUTSIDE the bash mount-namespace sandbox (FS_MOUNTS in
5
+ // action/mcp/shell.ts), so they need an independent deny — without it a
6
+ // prompt-injected agent could plant a git filter or hook via the native edit
7
+ // tool, bypassing the shell sandbox entirely. See wiki/security.md "Native
8
+ // Tool Filesystem Sandbox".
9
+ //
10
+ // WRITES are denied across ALL of .git, READS only on .git/config:
11
+ // - nothing legitimately WRITES under .git via native tools — real commits
12
+ // go through the MCP git tools, which run in the action process OUTSIDE
13
+ // this permission gate. so a blanket write-deny is free, and it robustly
14
+ // covers every code-exec surface an enumerated list would miss:
15
+ // .git/config, .git/config.worktree, .git/modules/*/config (all carry the
16
+ // same core.hooksPath / clean+smudge filter / alias / credential.helper
17
+ // exec vectors) plus .git/hooks/* and .git/info/attributes. the deny also
18
+ // covers the `.git` gitfile itself and nested `*/.git` gitfiles (worktree /
19
+ // submodule layouts), whose `gitdir:` pointer is the same exec surface.
20
+ // - READS stay narrow (.git/config only) because over-blocking .git reads
21
+ // would break legit native orientation reads (.git/HEAD, refs), and the
22
+ // read threat is low — ASKPASS keeps live tokens out of .git/config.
23
+ //
24
+ // The two agents express denies differently, so the surfaces are encoded once
25
+ // here and formatted per-agent:
26
+ // - OpenCode: worktree-relative patterns under the per-tool `read`/`edit`
27
+ // permission keys (Wildcard dialect: `*` is regex `.*`, matching `/`
28
+ // recursively). The `external_directory` gate can't restrict within-project
29
+ // paths (it short-circuits inside the repo root via Instance.containsPath),
30
+ // and the `grep`/`glob` permissions match the search pattern rather than a
31
+ // filepath — so only `read` and `edit` can path-deny within the project.
32
+ // - Claude: gitignore-style globs (`**` = recursive) per (tool, path) in
33
+ // managed-settings `permissions.deny`.
34
+
35
+ /** worktree-relative blanket WRITE deny for the entire `.git` tree, in
36
+ * OpenCode Wildcard dialect (`*` compiles to regex `.*`, matching `/`
37
+ * recursively — see packages/core/src/util/wildcard.ts). spread into the
38
+ * `edit` ruleset after a `"*": "allow"` baseline — `evaluate` is
39
+ * last-match-wins by key order, so the deny keys must follow the wildcard
40
+ * allow.
41
+ *
42
+ * four patterns, because the root-anchored descendants glob only matches
43
+ * paths under a root `.git` *directory* — it misses `.git` when it's a gitfile
44
+ * (worktree / submodule layouts: a regular file whose `gitdir:` line redirects
45
+ * git metadata) and misses nested gitfiles (a `.git` inside a subdirectory).
46
+ * rewriting either pointer is the same code-exec surface (`core.hooksPath`,
47
+ * clean/smudge filters, credential.helper) the blanket deny exists to seal, so
48
+ * we cover the gitfile itself and any nested `.git` too. */
49
+ export const GIT_NATIVE_WRITE_DENY_OPENCODE: Record<string, "deny"> = {
50
+ ".git": "deny",
51
+ ".git/*": "deny",
52
+ "*/.git": "deny",
53
+ "*/.git/*": "deny",
54
+ };
55
+
56
+ /** worktree-relative narrow READ deny (`.git/config` only), in OpenCode
57
+ * Wildcard dialect. spread into the `read` ruleset after the `"*": "allow"`
58
+ * baseline. */
59
+ export const GIT_NATIVE_READ_DENY_OPENCODE: Record<string, "deny"> = {
60
+ ".git/config": "deny",
61
+ };
62
+
63
+ /** native FS tools Claude exposes that can read or enumerate files. */
64
+ const CLAUDE_READ_TOOLS = ["Read", "Grep", "Glob"] as const;
65
+
66
+ /** Claude `permissions.deny` entries for the blanket `.git` WRITE deny —
67
+ * mirrors {@link GIT_NATIVE_WRITE_DENY_OPENCODE}. `**` is recursive. the exact
68
+ * `.git` entry plus the recursive-prefix gitfile entry cover the gitfile
69
+ * pointer (root + nested) that the root-anchored descendants glob alone misses;
70
+ * the recursive-prefix descendants entry covers nested gitdirs. */
71
+ export const GIT_NATIVE_WRITE_DENY_CLAUDE: string[] = [
72
+ "Edit(.git)",
73
+ "Edit(.git/**)",
74
+ "Edit(**/.git)",
75
+ "Edit(**/.git/**)",
76
+ ];
77
+
78
+ /** Claude `permissions.deny` entries for the narrow `.git/config` READ deny,
79
+ * one per read/enumerate tool — mirrors {@link GIT_NATIVE_READ_DENY_OPENCODE}. */
80
+ export const GIT_NATIVE_READ_DENY_CLAUDE: string[] = CLAUDE_READ_TOOLS.map(
81
+ (tool) => `${tool}(.git/config)`,
82
+ );