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,505 @@
1
+ // changes to shell security (filterEnv, spawnShell) should be reflected in wiki/security.md and docs/security.mdx
2
+ import { type ChildProcess, type StdioOptions, spawn, spawnSync } from "node:child_process";
3
+ import { randomUUID } from "node:crypto";
4
+ import { closeSync, openSync, writeFileSync } from "node:fs";
5
+ import { userInfo } from "node:os";
6
+ import { join } from "node:path";
7
+ import { setTimeout as sleep } from "node:timers/promises";
8
+ import { type } from "arktype";
9
+ import type { ToolContext } from "#app/mcp/server";
10
+ import { execute, tool } from "#app/mcp/shared";
11
+ import { log } from "#app/utils/log";
12
+ import { resolveEnv } from "#app/utils/secrets";
13
+
14
+ export const ShellParams = type({
15
+ command: "string",
16
+ description: "string",
17
+ "timeout?": type.number.describe(
18
+ "Timeout in MILLISECONDS (not seconds). Default 30000 (30s), max 120000 (2m). e.g. timeout: 180000 for 3 minutes; timeout: 180 means 180ms and will kill the process almost immediately.",
19
+ ),
20
+ "working_directory?": "string",
21
+ "background?": "boolean",
22
+ });
23
+
24
+ type SpawnParams = {
25
+ command: string;
26
+ env: Record<string, string | undefined>;
27
+ cwd: string;
28
+ stdio: StdioOptions;
29
+ };
30
+
31
+ export type SandboxMethod = "unshare" | "sudo-unshare" | "none";
32
+
33
+ /** cached result of sandbox capability check */
34
+ let detectedSandboxMethod: SandboxMethod | undefined;
35
+
36
+ /** get the current sandbox method (for testing/diagnostics) */
37
+ export function getSandboxMethod(): SandboxMethod {
38
+ return detectSandboxMethod();
39
+ }
40
+
41
+ /** detect which sandbox method is available on this system */
42
+ function detectSandboxMethod(): SandboxMethod {
43
+ if (detectedSandboxMethod !== undefined) {
44
+ return detectedSandboxMethod;
45
+ }
46
+
47
+ // only attempt in CI environments - sandbox has overhead and is primarily for untrusted code
48
+ if (process.env.CI !== "true") {
49
+ detectedSandboxMethod = "none";
50
+ log.debug("sandbox disabled (CI !== true)");
51
+ return "none";
52
+ }
53
+
54
+ // try unprivileged unshare first (works on some systems)
55
+ try {
56
+ const result = spawnSync("unshare", ["--pid", "--fork", "--mount-proc", "true"], {
57
+ timeout: 5000,
58
+ stdio: "ignore",
59
+ });
60
+ if (result.status === 0) {
61
+ detectedSandboxMethod = "unshare";
62
+ log.debug("PID namespace isolation enabled (unprivileged unshare)");
63
+ return "unshare";
64
+ }
65
+ } catch {
66
+ // continue to try sudo
67
+ }
68
+
69
+ // sudo unshare (works on GHA runners)
70
+ try {
71
+ const result = spawnSync("sudo", ["unshare", "--pid", "--fork", "--mount-proc", "true"], {
72
+ timeout: 5000,
73
+ stdio: "ignore",
74
+ });
75
+ if (result.status === 0) {
76
+ detectedSandboxMethod = "sudo-unshare";
77
+ log.debug("PID namespace isolation enabled (sudo unshare)");
78
+ return "sudo-unshare";
79
+ }
80
+ } catch {
81
+ // no sandbox available
82
+ }
83
+
84
+ detectedSandboxMethod = "none";
85
+ log.info("PID namespace isolation not available");
86
+ return "none";
87
+ }
88
+
89
+ // strip inherited proc mount that sits underneath --mount-proc's overlay.
90
+ // --mount-proc mounts fresh proc on top, but `umount /proc` peels it off and exposes the
91
+ // host's proc with all host PIDs — allowing /proc/<pid>/environ exfiltration.
92
+ // double-umount removes both layers, then a clean mount gives only sandbox PIDs.
93
+ // on unprivileged systems where umount fails, --mount-proc still provides isolation
94
+ // (the agent also can't umount in that case).
95
+ const PROC_CLEANUP =
96
+ "umount /proc 2>/dev/null; umount /proc 2>/dev/null; mount -t proc proc /proc 2>/dev/null;";
97
+
98
+ // block container-runtime sockets that would otherwise grant a PID-namespace
99
+ // escape: `docker run --pid=host --privileged busybox cat /proc/<pid>/environ`
100
+ // reads the parent action process's env (which contains user secrets) even
101
+ // though the sandbox itself is unsharing PIDs. GHA `ubuntu-latest` puts the
102
+ // `runner` user in the `docker` group by default, so the socket is reachable
103
+ // without sudo. bind-mounting /dev/null on top inside the sandbox's mount
104
+ // namespace makes the socket unreachable from sandboxed shells without
105
+ // touching the host runner (so it doesn't break user workflow steps that
106
+ // run before/after terramend and legitimately need docker). same trick for
107
+ // podman/containerd/cri-o sockets — all silent-fail if the path is missing.
108
+ const SOCKET_CLEANUP = [
109
+ "/var/run/docker.sock",
110
+ "/run/docker.sock",
111
+ "/var/run/podman/podman.sock",
112
+ "/run/podman/podman.sock",
113
+ "/run/containerd/containerd.sock",
114
+ "/var/run/crio/crio.sock",
115
+ ]
116
+ .map((path) => `mount --bind /dev/null ${path} 2>/dev/null;`)
117
+ .join(" ");
118
+
119
+ // extend the mount-namespace isolation that PROC_CLEANUP and SOCKET_CLEANUP
120
+ // already establish. these mounts hide terramend-managed on-disk secrets,
121
+ // block env-injection into subsequent workflow steps, and make git's
122
+ // code-execution config read-only inside the bash subprocess.
123
+ //
124
+ // 1. tmpfs over /var/lib/terramend/ — codex auth.json and any future
125
+ // terramend-managed on-disk secret live here (see action/utils/codexHome.ts
126
+ // TERRAMEND_DATA_DIR). opencode's internal auth module runs in the agent
127
+ // process outside this namespace and reads the real file via bypass of
128
+ // external_directory; bash sees an empty tmpfs. mkdir -p the path
129
+ // first so the tmpfs always engages — without that, runs without
130
+ // CODEX_AUTH_JSON wouldn't have bootstrapped the dir, the mountpoint
131
+ // wouldn't exist, and `mount -t tmpfs` would silent-fail. precreate
132
+ // keeps the overlay active for any future on-disk secret that lands
133
+ // under /var/lib/terramend regardless of which install path created it.
134
+ // 2. tmpfs over $RUNNER_TEMP/_runner_file_commands/ — anything bash writes
135
+ // to $GITHUB_ENV / $GITHUB_PATH / $GITHUB_OUTPUT / $GITHUB_STATE lands in
136
+ // a per-namespace tmpfs that the GHA runner never sees. our own action
137
+ // process writes core.setOutput / core.saveState outside the namespace,
138
+ // so legitimate outputs are unaffected. requires RUNNER_TEMP to be set.
139
+ // 3. self-bind + remount-ro on the ENTIRE <repoRoot>/.git directory.
140
+ // A blanket ro-bind is free: nothing legitimately writes .git from
141
+ // bash (commits go through our $git(), whose binary runs OUTSIDE this
142
+ // namespace, so it's unaffected; bash `git` is already blocked). It
143
+ // robustly covers every code-exec surface an enumerated list would miss
144
+ // — .git/config, .git/config.worktree, .git/modules/*/config (all carry
145
+ // core.hooksPath / filter / alias / credential.helper exec vectors),
146
+ // plus .git/hooks/* and .git/info/attributes. Prevents agent-planted git
147
+ // filters / hooks from firing in downstream workflow steps (the threat
148
+ // survives ASKPASS because hooks fire after auth and our $git() uses -c
149
+ // core.hooksPath to override its own hooks, but downstream `git`
150
+ // invocations in later steps DON'T get that protection — see
151
+ // wiki/security.md "Filesystem Sandbox"). CONSEQUENCE: .git/info/exclude
152
+ // (a legit per-repo ignore file) is now read-only too — accepted, the
153
+ // narrow earlier bind list left it writable. Does not cover `~/.gitconfig`
154
+ // or `/etc/gitconfig` — see "Scope and Limitations" in wiki/security.md.
155
+ //
156
+ // these mounts run as root inside the namespace (before `exec su -p` drops
157
+ // to runner). after the drop, runner has no CAP_SYS_ADMIN in the host, so
158
+ // can't undo from outside. intra-namespace sudo undo is theoretically
159
+ // possible — same risk profile as SOCKET_CLEANUP, accepted per wiki/security.md
160
+ // "why sudo inside sandbox doesn't break security".
161
+ //
162
+ // in the unprivileged-unshare path (Docker --privileged test environments),
163
+ // the user retains CAP_SYS_ADMIN inside the user namespace and could
164
+ // `umount` these. production uses sudo-unshare where the drop seals them.
165
+ //
166
+ // repoDir is interpolated by the action process from resolveRepoRoot() —
167
+ // NOT $PWD — because spawnShell's cwd is agent-controllable via
168
+ // `working_directory` AND the action's process.cwd() may have been chdir'd
169
+ // to payload.cwd (monorepo subdir support in main.ts). using either would
170
+ // let the agent bypass the .git/* binds. resolveRepoRoot pins the actual
171
+ // repo root once at startup via $GITHUB_WORKSPACE or `git rev-parse`.
172
+ function buildFsMounts(repoDir: string): string {
173
+ // shell-escape via single-quote wrap; bash interprets \' as the escape
174
+ // for a single quote inside a single-quoted string by closing-and-reopening.
175
+ // repoDir paths in practice are GHA workspace paths (no quotes), but the
176
+ // escape keeps us correct against arbitrary user-configured workspaces.
177
+ const escaped = repoDir.replace(/'/g, "'\\''");
178
+ return [
179
+ `mkdir -p /var/lib/terramend 2>/dev/null;`,
180
+ `mount -t tmpfs tmpfs /var/lib/terramend 2>/dev/null;`,
181
+ `[ -n "$RUNNER_TEMP" ] && [ -d "$RUNNER_TEMP/_runner_file_commands" ] && mount -t tmpfs tmpfs "$RUNNER_TEMP/_runner_file_commands" 2>/dev/null;`,
182
+ `[ -e '${escaped}/.git' ] && mount --bind '${escaped}/.git' '${escaped}/.git' 2>/dev/null && mount -o remount,bind,ro '${escaped}/.git' 2>/dev/null;`,
183
+ ].join(" ");
184
+ }
185
+
186
+ /** locate the repo root once at action startup. process.cwd() is unreliable
187
+ * because main.ts may `process.chdir(payload.cwd)` for monorepo subdirs;
188
+ * the agent's `working_directory` shell param also moves spawn cwd. we need
189
+ * the actual git working tree root for the .git/* binds. memoized for the
190
+ * lifetime of the action process. */
191
+ let _repoRoot: string | undefined;
192
+ function resolveRepoRoot(): string {
193
+ if (_repoRoot) return _repoRoot;
194
+ const fromEnv = process.env.GITHUB_WORKSPACE;
195
+ if (fromEnv) {
196
+ _repoRoot = fromEnv;
197
+ return _repoRoot;
198
+ }
199
+ // fallback: `git rev-parse --show-toplevel` from process.cwd(). only used
200
+ // outside GHA (local dev, custom runners). swallow errors and fall back
201
+ // to process.cwd() so we never throw from the shell-tool init path.
202
+ try {
203
+ _repoRoot = spawnSync("git", ["rev-parse", "--show-toplevel"], {
204
+ cwd: process.cwd(),
205
+ stdio: ["ignore", "pipe", "ignore"],
206
+ encoding: "utf-8",
207
+ }).stdout?.trim();
208
+ } catch {
209
+ // intentionally empty — fall through to process.cwd()
210
+ }
211
+ if (!_repoRoot) _repoRoot = process.cwd();
212
+ return _repoRoot;
213
+ }
214
+
215
+ function spawnShell(params: SpawnParams): ChildProcess {
216
+ const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
217
+ const sandboxMethod = detectSandboxMethod();
218
+ const ci = process.env.CI === "true";
219
+
220
+ if (ci && sandboxMethod === "none") {
221
+ throw new Error(
222
+ "pid namespace isolation is required in CI but unavailable (both unshare and sudo unshare failed)",
223
+ );
224
+ }
225
+
226
+ // resolve the actual git repo root (NOT params.cwd which is
227
+ // agent-controllable via `working_directory`, NOT process.cwd() which may
228
+ // have been chdir'd to payload.cwd in main.ts). resolveRepoRoot prefers
229
+ // $GITHUB_WORKSPACE in CI and falls back to `git rev-parse`.
230
+ const repoRoot = resolveRepoRoot();
231
+ const fsMounts = buildFsMounts(repoRoot);
232
+
233
+ if (sandboxMethod === "unshare") {
234
+ return spawn(
235
+ "unshare",
236
+ [
237
+ "--pid",
238
+ "--fork",
239
+ "--mount-proc",
240
+ "bash",
241
+ "-c",
242
+ `${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} ${params.command}`,
243
+ ],
244
+ spawnOpts,
245
+ );
246
+ }
247
+
248
+ if (sandboxMethod === "sudo-unshare") {
249
+ const envArgs: string[] = [];
250
+ for (const [k, v] of Object.entries(params.env)) {
251
+ if (v !== undefined) {
252
+ envArgs.push(`${k}=${v}`);
253
+ }
254
+ }
255
+ // drop back to original user after PROC_CLEANUP / FS_MOUNTS so files aren't
256
+ // owned by root. sudo is only needed for unshare + the mount setup; the
257
+ // actual command should run as the normal user to avoid ownership
258
+ // mismatches with files created by the Node.js parent process.
259
+ const username = userInfo().username;
260
+ // su -p resets PATH on many Linux systems (ALWAYS_SET_PATH in /etc/login.defs).
261
+ // restore it from the SANDBOX_PATH env var that survives the su transition.
262
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: we need to restore the PATH variable
263
+ const pathRestore = 'export PATH="${SANDBOX_PATH:-$PATH}"; ';
264
+ const escaped = (pathRestore + params.command).replace(/'/g, "'\\''");
265
+ envArgs.push(`SANDBOX_PATH=${params.env.PATH ?? ""}`);
266
+ return spawn(
267
+ "sudo",
268
+ [
269
+ "env",
270
+ ...envArgs,
271
+ "unshare",
272
+ "--pid",
273
+ "--fork",
274
+ "--mount-proc",
275
+ "bash",
276
+ "-c",
277
+ `${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} exec su -p -s /bin/bash ${username} -c '${escaped}'`,
278
+ ],
279
+ { ...spawnOpts, env: {} },
280
+ );
281
+ }
282
+
283
+ return spawn("bash", ["-c", params.command], spawnOpts);
284
+ }
285
+
286
+ /** kill process and its entire process group */
287
+ async function killProcessGroup(proc: ChildProcess): Promise<void> {
288
+ if (!proc.pid) return;
289
+ try {
290
+ process.kill(-proc.pid, "SIGTERM");
291
+ await new Promise((r) => setTimeout(r, 200));
292
+ process.kill(-proc.pid, "SIGKILL");
293
+ } catch {
294
+ try {
295
+ proc.kill("SIGKILL");
296
+ } catch {
297
+ /* already dead */
298
+ }
299
+ }
300
+ }
301
+
302
+ function getTempDir(): string {
303
+ const tempDir = process.env.TERRAMEND_TEMP_DIR;
304
+ if (!tempDir) {
305
+ throw new Error("TERRAMEND_TEMP_DIR not set");
306
+ }
307
+ return tempDir;
308
+ }
309
+
310
+ /** chars of shell output kept inline in the agent reply. anything past this
311
+ * blows the agent's context budget on commands that dump big logs (test
312
+ * runners, build tools, grep on large trees), so the overflow is spilled
313
+ * to a tempfile the agent can re-read selectively (cat/tail/grep). */
314
+ export const MAX_OUTPUT_CHARS = 5000;
315
+
316
+ /** if `output` exceeds `MAX_OUTPUT_CHARS`, persist the full body to a
317
+ * tempfile and return the last `MAX_OUTPUT_CHARS` prefixed with a sentinel
318
+ * pointing at the saved path. otherwise return as-is. */
319
+ function capOutput(output: string): string {
320
+ if (output.length <= MAX_OUTPUT_CHARS) return output;
321
+ const fullPath = join(getTempDir(), `shell-${randomUUID().slice(0, 8)}.log`);
322
+ writeFileSync(fullPath, output);
323
+ const elided = output.length - MAX_OUTPUT_CHARS;
324
+ return `... [${elided} chars truncated; full output saved to ${fullPath}] ...\n${output.slice(-MAX_OUTPUT_CHARS)}`;
325
+ }
326
+
327
+ // detect `git` as a command invocation in any position a shell would start a new
328
+ // command: at the start, after a separator (`;`, `&`, `|`, newline, carriage
329
+ // return), or inside a subshell / command substitution / brace group (`(`,
330
+ // backtick, `$(`, `{`). Tolerates one or more launcher prefixes
331
+ // (`sudo`/`env`/`command`/`exec`/`nohup`). Matches `git` only when followed by
332
+ // whitespace or end-of-string, so it never fires on `.gitignore`, `digit`,
333
+ // `legit`, `git-lfs`, etc.
334
+ //
335
+ // NOTE: this is a UX redirect to the dedicated git tools, NOT a security
336
+ // boundary — in restricted mode the shell runs in a stripped, token-free sandbox
337
+ // with `.git` mounted read-only, so a `git` the agent slips past this still has
338
+ // no credentials and can't tamper with repo config. The detection is kept broad
339
+ // so the redirect isn't trivially defeated, but it is NOT relied on to contain a
340
+ // hostile `git` — that is the sandbox's job.
341
+ const GIT_INVOCATION =
342
+ /(?:^|\$\(|[\n\r;&|`({])\s*(?:(?:sudo|env|command|exec|nohup)\s+)*git(?:\s|$)/;
343
+
344
+ function isGitCommand(command: string): boolean {
345
+ return GIT_INVOCATION.test(command.trim());
346
+ }
347
+
348
+ export function ShellTool(ctx: ToolContext) {
349
+ return tool({
350
+ name: "shell",
351
+ timeoutMs: 120_000,
352
+ description: `Execute shell commands securely. Environment is filtered to remove API keys and secrets.
353
+
354
+ Example: \`shell({ command: "pnpm test", description: "run the test suite" })\`.
355
+
356
+ Use this tool to:
357
+ - Run shell commands (ls, cat, grep, find, etc.)
358
+ - Execute build tools (npm, pnpm, cargo, make, etc.)
359
+ - Run tests and linters
360
+
361
+ Output is capped at ${MAX_OUTPUT_CHARS} chars: if exceeded, only the tail is returned and the full body is saved to a tempfile (path included in the response). Re-read the tempfile with cat/tail/grep when you need more.
362
+
363
+ Do NOT use this tool for git commands — use the dedicated git tools instead.`,
364
+ parameters: ShellParams,
365
+ execute: execute(async (params) => {
366
+ if (isGitCommand(params.command)) {
367
+ throw new Error(
368
+ "git commands are not allowed in the shell tool. use the dedicated git tools instead:\n" +
369
+ "- git: local operations (status, log, diff, add, commit, checkout, merge, rebase, etc.)\n" +
370
+ "- push_branch: push to remote (handles authentication)\n" +
371
+ "- git_fetch: fetch from remote (handles authentication)\n" +
372
+ "- checkout_pr: check out PR branches",
373
+ );
374
+ }
375
+
376
+ const timeout = Math.min(params.timeout ?? 30000, 120000);
377
+ const cwd = params.working_directory ?? process.cwd();
378
+ const env = resolveEnv(ctx.payload.shell === "enabled" ? "inherit" : "restricted");
379
+
380
+ if (params.background) {
381
+ const tempDir = getTempDir();
382
+ const handle = `bg-${randomUUID().slice(0, 8)}`;
383
+ const outputPath = join(tempDir, `${handle}.log`);
384
+ const pidPath = join(tempDir, `${handle}.pid`);
385
+ const logFd = openSync(outputPath, "a");
386
+ let proc: ChildProcess;
387
+ try {
388
+ proc = spawnShell({
389
+ command: params.command,
390
+ env,
391
+ cwd,
392
+ stdio: ["ignore", logFd, logFd],
393
+ });
394
+ } finally {
395
+ closeSync(logFd);
396
+ }
397
+ if (!proc.pid) {
398
+ throw new Error("failed to start background process");
399
+ }
400
+ proc.unref();
401
+ writeFileSync(pidPath, `${proc.pid}\n`);
402
+ ctx.toolState.backgroundProcesses.set(handle, { pid: proc.pid, outputPath, pidPath });
403
+ return {
404
+ handle,
405
+ outputPath,
406
+ pidPath,
407
+ message: `started background process ${handle} (pid ${proc.pid})`,
408
+ };
409
+ }
410
+
411
+ const proc = spawnShell({
412
+ command: params.command,
413
+ env,
414
+ cwd,
415
+ stdio: ["ignore", "pipe", "pipe"],
416
+ });
417
+
418
+ let stdout = "",
419
+ stderr = "",
420
+ timedOut = false,
421
+ exited = false;
422
+ proc.stdout?.on("data", (chunk: Buffer) => {
423
+ stdout += chunk.toString();
424
+ });
425
+ proc.stderr?.on("data", (chunk: Buffer) => {
426
+ stderr += chunk.toString();
427
+ });
428
+
429
+ const timeoutId = setTimeout(async () => {
430
+ if (!exited) {
431
+ timedOut = true;
432
+ await killProcessGroup(proc);
433
+ }
434
+ }, timeout);
435
+
436
+ const exitCode = await new Promise<number | null>((resolve) => {
437
+ const done = (code: number | null) => {
438
+ exited = true;
439
+ clearTimeout(timeoutId);
440
+ resolve(code);
441
+ };
442
+ proc.on("exit", done);
443
+ proc.on("error", () => done(null));
444
+ });
445
+
446
+ let output = stderr ? (stdout ? `${stdout}\n${stderr}` : stderr) : stdout;
447
+ if (timedOut)
448
+ output = output
449
+ ? `${output}\n[timed out after ${timeout}ms]`
450
+ : `[timed out after ${timeout}ms]`;
451
+
452
+ const finalExitCode = exitCode ?? (timedOut ? 124 : -1);
453
+ const trimmed = output.trim();
454
+ if (finalExitCode !== 0) {
455
+ log.info(`shell command failed with exit code ${finalExitCode}: ${params.command}`);
456
+ if (trimmed) log.info(`output: ${trimmed}`);
457
+ }
458
+
459
+ return {
460
+ output: capOutput(trimmed),
461
+ exit_code: finalExitCode,
462
+ timed_out: timedOut,
463
+ };
464
+ }),
465
+ });
466
+ }
467
+
468
+ export const KillBackgroundParams = type({
469
+ handle: type.string.describe("The handle of the background process to kill (e.g., bg-a1b2c3d4)"),
470
+ });
471
+
472
+ export function KillBackgroundTool(ctx: ToolContext) {
473
+ return tool({
474
+ name: "kill_background",
475
+ description: `Kill a background process by its handle. Use this to stop dev servers or other long-running processes started with shell({ background: true }).`,
476
+ parameters: KillBackgroundParams,
477
+ execute: execute(async (params) => {
478
+ const proc = ctx.toolState.backgroundProcesses.get(params.handle);
479
+ if (!proc) {
480
+ return {
481
+ success: false,
482
+ message: `no background process with handle ${params.handle}`,
483
+ };
484
+ }
485
+
486
+ try {
487
+ process.kill(-proc.pid, "SIGTERM");
488
+ } catch {
489
+ // already dead
490
+ }
491
+ await sleep(200);
492
+ try {
493
+ process.kill(-proc.pid, "SIGKILL");
494
+ } catch {
495
+ // already dead
496
+ }
497
+
498
+ ctx.toolState.backgroundProcesses.delete(params.handle);
499
+ return {
500
+ success: true,
501
+ message: `killed background process ${params.handle} (pid ${proc.pid})`,
502
+ };
503
+ }),
504
+ });
505
+ }