infraweaver 0.1.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 (524) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +244 -0
  3. package/dist/agents/claude.d.ts +113 -0
  4. package/dist/agents/claudePretoolGate.d.ts +137 -0
  5. package/dist/agents/dispatchMetrics.d.ts +77 -0
  6. package/dist/agents/gateServer.d.ts +7 -0
  7. package/dist/agents/index.d.ts +6 -0
  8. package/dist/agents/nativeFsDenies.d.ts +46 -0
  9. package/dist/agents/opencode.d.ts +284 -0
  10. package/dist/agents/opencodePlugin.d.ts +96 -0
  11. package/dist/agents/opencodeShared.d.ts +40 -0
  12. package/dist/agents/postRun.d.ts +126 -0
  13. package/dist/agents/reviewer.d.ts +41 -0
  14. package/dist/agents/sessionLabeler.d.ts +97 -0
  15. package/dist/agents/shared.d.ts +246 -0
  16. package/dist/agents/subagentModels.d.ts +19 -0
  17. package/dist/agents/tokenQuota.d.ts +118 -0
  18. package/dist/agents/writeGateSource.d.ts +22 -0
  19. package/dist/agents/writePolicy.d.ts +79 -0
  20. package/dist/brand.d.ts +67 -0
  21. package/dist/cli.mjs +247846 -0
  22. package/dist/external.d.ts +227 -0
  23. package/dist/i18n/scaffolding.d.ts +59 -0
  24. package/dist/index.d.ts +6 -0
  25. package/dist/index.js +247061 -0
  26. package/dist/internal/index.d.ts +18 -0
  27. package/dist/internal.js +2398 -0
  28. package/dist/lifecycle.d.ts +2 -0
  29. package/dist/main.d.ts +8 -0
  30. package/dist/mcp/arkConfig.d.ts +1 -0
  31. package/dist/mcp/assess.d.ts +157 -0
  32. package/dist/mcp/capabilityContext.d.ts +71 -0
  33. package/dist/mcp/changeSummary.d.ts +52 -0
  34. package/dist/mcp/checkSuite.d.ts +27 -0
  35. package/dist/mcp/checkout.d.ts +92 -0
  36. package/dist/mcp/comment.d.ts +127 -0
  37. package/dist/mcp/commitInfo.d.ts +11 -0
  38. package/dist/mcp/crosswalk.d.ts +178 -0
  39. package/dist/mcp/crosswalkDigest.d.ts +1 -0
  40. package/dist/mcp/cyberEssentials.d.ts +24 -0
  41. package/dist/mcp/dashboard.d.ts +125 -0
  42. package/dist/mcp/dependencies.d.ts +12 -0
  43. package/dist/mcp/frameworks.d.ts +74 -0
  44. package/dist/mcp/geminiSanitizer.d.ts +28 -0
  45. package/dist/mcp/git.d.ts +60 -0
  46. package/dist/mcp/guardrails.d.ts +222 -0
  47. package/dist/mcp/issue.d.ts +20 -0
  48. package/dist/mcp/issueComments.d.ts +11 -0
  49. package/dist/mcp/issueEvents.d.ts +11 -0
  50. package/dist/mcp/issueInfo.d.ts +11 -0
  51. package/dist/mcp/labels.d.ts +14 -0
  52. package/dist/mcp/localContext.d.ts +26 -0
  53. package/dist/mcp/moduleExtraction.d.ts +79 -0
  54. package/dist/mcp/moduleTests.d.ts +106 -0
  55. package/dist/mcp/modules.d.ts +198 -0
  56. package/dist/mcp/output.d.ts +16 -0
  57. package/dist/mcp/pathSafety.d.ts +14 -0
  58. package/dist/mcp/policy.d.ts +50 -0
  59. package/dist/mcp/pr.d.ts +64 -0
  60. package/dist/mcp/prInfo.d.ts +11 -0
  61. package/dist/mcp/providerSchema.d.ts +52 -0
  62. package/dist/mcp/review.d.ts +212 -0
  63. package/dist/mcp/reviewComments.d.ts +245 -0
  64. package/dist/mcp/roots.d.ts +60 -0
  65. package/dist/mcp/scope.d.ts +26 -0
  66. package/dist/mcp/selectMode.d.ts +20 -0
  67. package/dist/mcp/server.d.ts +61 -0
  68. package/dist/mcp/shared.d.ts +62 -0
  69. package/dist/mcp/shell.d.ts +58 -0
  70. package/dist/mcp/staleFix.d.ts +138 -0
  71. package/dist/mcp/terraform/azurePrices.d.ts +8 -0
  72. package/dist/mcp/terraform/baseline.d.ts +57 -0
  73. package/dist/mcp/terraform/concernResult.d.ts +38 -0
  74. package/dist/mcp/terraform/cost.d.ts +55 -0
  75. package/dist/mcp/terraform/cspm.d.ts +80 -0
  76. package/dist/mcp/terraform/currency.d.ts +110 -0
  77. package/dist/mcp/terraform/decisions.d.ts +178 -0
  78. package/dist/mcp/terraform/eidas.d.ts +100 -0
  79. package/dist/mcp/terraform/evidence.d.ts +343 -0
  80. package/dist/mcp/terraform/findings.d.ts +76 -0
  81. package/dist/mcp/terraform/fixMemory.d.ts +81 -0
  82. package/dist/mcp/terraform/governance.d.ts +56 -0
  83. package/dist/mcp/terraform/hcl.d.ts +115 -0
  84. package/dist/mcp/terraform/iacLanguages.d.ts +120 -0
  85. package/dist/mcp/terraform/idleFloor.d.ts +78 -0
  86. package/dist/mcp/terraform/keyless.d.ts +68 -0
  87. package/dist/mcp/terraform/moduleDocs.d.ts +56 -0
  88. package/dist/mcp/terraform/nativeRules.d.ts +38 -0
  89. package/dist/mcp/terraform/nativeScan.d.ts +11 -0
  90. package/dist/mcp/terraform/noiseProfile.d.ts +100 -0
  91. package/dist/mcp/terraform/normalization.d.ts +52 -0
  92. package/dist/mcp/terraform/oscal.d.ts +103 -0
  93. package/dist/mcp/terraform/packs/azurerm-3-4.d.ts +2 -0
  94. package/dist/mcp/terraform/packs/cloudflare-4-5.d.ts +2 -0
  95. package/dist/mcp/terraform/paths.d.ts +28 -0
  96. package/dist/mcp/terraform/plan.d.ts +157 -0
  97. package/dist/mcp/terraform/planDrift.d.ts +199 -0
  98. package/dist/mcp/terraform/policyAuthor.d.ts +191 -0
  99. package/dist/mcp/terraform/prowlerOcsf.d.ts +136 -0
  100. package/dist/mcp/terraform/refactor.d.ts +183 -0
  101. package/dist/mcp/terraform/registry.d.ts +108 -0
  102. package/dist/mcp/terraform/risk.d.ts +41 -0
  103. package/dist/mcp/terraform/scanSession.d.ts +116 -0
  104. package/dist/mcp/terraform/scannerCache.d.ts +15 -0
  105. package/dist/mcp/terraform/scannerEgress.d.ts +72 -0
  106. package/dist/mcp/terraform/scannerJson.d.ts +13 -0
  107. package/dist/mcp/terraform/scanners.d.ts +205 -0
  108. package/dist/mcp/terraform/signing.d.ts +61 -0
  109. package/dist/mcp/terraform/stateLocking.d.ts +18 -0
  110. package/dist/mcp/terraform/subprocess.d.ts +43 -0
  111. package/dist/mcp/terraform/suppressions.d.ts +90 -0
  112. package/dist/mcp/terraform/taxonomy.d.ts +39 -0
  113. package/dist/mcp/terraform/tfquery.d.ts +47 -0
  114. package/dist/mcp/terraform/toolchainIntegrity.d.ts +82 -0
  115. package/dist/mcp/terraform/tools/authorPolicy.d.ts +59 -0
  116. package/dist/mcp/terraform/tools/detectPlanDrift.d.ts +20 -0
  117. package/dist/mcp/terraform/tools/emitCost.d.ts +11 -0
  118. package/dist/mcp/terraform/tools/emitSarif.d.ts +14 -0
  119. package/dist/mcp/terraform/tools/fixMemory.d.ts +19 -0
  120. package/dist/mcp/terraform/tools/infracostDiff.d.ts +22 -0
  121. package/dist/mcp/terraform/tools/ingestExternalFindings.d.ts +21 -0
  122. package/dist/mcp/terraform/tools/moduleLookup.d.ts +14 -0
  123. package/dist/mcp/terraform/tools/moduleSearch.d.ts +23 -0
  124. package/dist/mcp/terraform/tools/plan.d.ts +59 -0
  125. package/dist/mcp/terraform/tools/providerUpgrade.d.ts +55 -0
  126. package/dist/mcp/terraform/tools/readFindings.d.ts +17 -0
  127. package/dist/mcp/terraform/tools/scan.d.ts +114 -0
  128. package/dist/mcp/terraform/tools/validate.d.ts +31 -0
  129. package/dist/mcp/terraform/tools/verifyRemediation.d.ts +31 -0
  130. package/dist/mcp/terraform/tools/versionCurrency.d.ts +5 -0
  131. package/dist/mcp/terraform/tools.d.ts +16 -0
  132. package/dist/mcp/terraform/transformPacks.d.ts +267 -0
  133. package/dist/mcp/terraform/types.d.ts +226 -0
  134. package/dist/mcp/terraform/verification.d.ts +81 -0
  135. package/dist/mcp/terraform/vex.d.ts +115 -0
  136. package/dist/mcp/terraform/writeOnly.d.ts +67 -0
  137. package/dist/mcp/terraform.d.ts +51 -0
  138. package/dist/mcp/terratest.d.ts +85 -0
  139. package/dist/mcp/untrustedContent.d.ts +76 -0
  140. package/dist/mcp/upload.d.ts +8 -0
  141. package/dist/models.d.ts +181 -0
  142. package/dist/modes/address-reviews.d.ts +2 -0
  143. package/dist/modes/assess.d.ts +2 -0
  144. package/dist/modes/build.d.ts +2 -0
  145. package/dist/modes/compliance-audit.d.ts +2 -0
  146. package/dist/modes/cost-optimization.d.ts +2 -0
  147. package/dist/modes/drift-detect.d.ts +2 -0
  148. package/dist/modes/fix.d.ts +2 -0
  149. package/dist/modes/incremental-review.d.ts +2 -0
  150. package/dist/modes/index.d.ts +24 -0
  151. package/dist/modes/modernize-deprecated.d.ts +2 -0
  152. package/dist/modes/plan.d.ts +2 -0
  153. package/dist/modes/policy-gate.d.ts +2 -0
  154. package/dist/modes/prFormats.d.ts +3 -0
  155. package/dist/modes/refactor.d.ts +2 -0
  156. package/dist/modes/refresh-remediation.d.ts +2 -0
  157. package/dist/modes/remediate-and-refactor.d.ts +2 -0
  158. package/dist/modes/remediate.d.ts +2 -0
  159. package/dist/modes/resolve-conflicts.d.ts +2 -0
  160. package/dist/modes/review.d.ts +2 -0
  161. package/dist/modes/summarize-pr.d.ts +2 -0
  162. package/dist/modes/task.d.ts +2 -0
  163. package/dist/modes/terraform-code-review.d.ts +2 -0
  164. package/dist/modes/types.d.ts +9 -0
  165. package/dist/modes/update-dependencies.d.ts +2 -0
  166. package/dist/prep/index.d.ts +7 -0
  167. package/dist/prep/installNodeDependencies.d.ts +2 -0
  168. package/dist/prep/installPythonDependencies.d.ts +2 -0
  169. package/dist/prep/types.d.ts +31 -0
  170. package/dist/reviewQuality.d.ts +107 -0
  171. package/dist/skills/terraform-best-practices/SKILL.md +379 -0
  172. package/dist/toolState.d.ts +123 -0
  173. package/dist/utils/activity.d.ts +40 -0
  174. package/dist/utils/agent.d.ts +47 -0
  175. package/dist/utils/agentHangReport.d.ts +38 -0
  176. package/dist/utils/annotations.d.ts +17 -0
  177. package/dist/utils/apiFetch.d.ts +11 -0
  178. package/dist/utils/apiKeys.d.ts +48 -0
  179. package/dist/utils/apiUrl.d.ts +28 -0
  180. package/dist/utils/assets.d.ts +8 -0
  181. package/dist/utils/baseRefConfig.d.ts +121 -0
  182. package/dist/utils/billingErrors.d.ts +85 -0
  183. package/dist/utils/body.d.ts +34 -0
  184. package/dist/utils/buildInfraweaverFooter.d.ts +29 -0
  185. package/dist/utils/byokFallback.d.ts +85 -0
  186. package/dist/utils/changeImpact.d.ts +101 -0
  187. package/dist/utils/claudeSubscription.d.ts +30 -0
  188. package/dist/utils/cli.d.ts +10 -0
  189. package/dist/utils/cloudClient.d.ts +33 -0
  190. package/dist/utils/cloudFetchCore.d.ts +42 -0
  191. package/dist/utils/cloudReport.d.ts +114 -0
  192. package/dist/utils/codexHome.d.ts +29 -0
  193. package/dist/utils/codexOAuth.d.ts +60 -0
  194. package/dist/utils/diffCoverage.d.ts +63 -0
  195. package/dist/utils/env.d.ts +46 -0
  196. package/dist/utils/errorReport.d.ts +17 -0
  197. package/dist/utils/exitHandler.d.ts +8 -0
  198. package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
  199. package/dist/utils/gitAuth.d.ts +84 -0
  200. package/dist/utils/gitAuthServer.d.ts +24 -0
  201. package/dist/utils/github.d.ts +104 -0
  202. package/dist/utils/globals.d.ts +3 -0
  203. package/dist/utils/infraweaverConfig.d.ts +56 -0
  204. package/dist/utils/install.d.ts +37 -0
  205. package/dist/utils/instructions.d.ts +48 -0
  206. package/dist/utils/keylessOidc.d.ts +28 -0
  207. package/dist/utils/leapingComment.d.ts +11 -0
  208. package/dist/utils/learnings.d.ts +62 -0
  209. package/dist/utils/learningsTruncate.d.ts +25 -0
  210. package/dist/utils/lifecycle.d.ts +57 -0
  211. package/dist/utils/log.d.ts +111 -0
  212. package/dist/utils/modelResolver.d.ts +63 -0
  213. package/dist/utils/moduleFetch.d.ts +39 -0
  214. package/dist/utils/normalizeEnv.d.ts +30 -0
  215. package/dist/utils/openCodeModels.d.ts +11 -0
  216. package/dist/utils/openaiCompatible.d.ts +26 -0
  217. package/dist/utils/opentofuMcp.d.ts +50 -0
  218. package/dist/utils/overrides.d.ts +40 -0
  219. package/dist/utils/packageManager.d.ts +49 -0
  220. package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
  221. package/dist/utils/payload.d.ts +249 -0
  222. package/dist/utils/prSummary.d.ts +61 -0
  223. package/dist/utils/presets.d.ts +36 -0
  224. package/dist/utils/progressComment.d.ts +146 -0
  225. package/dist/utils/providerErrors.d.ts +31 -0
  226. package/dist/utils/proxyModel.d.ts +76 -0
  227. package/dist/utils/rangeDiff.d.ts +51 -0
  228. package/dist/utils/redact.d.ts +33 -0
  229. package/dist/utils/registryAuth.d.ts +52 -0
  230. package/dist/utils/remediationCommand.d.ts +58 -0
  231. package/dist/utils/retry.d.ts +38 -0
  232. package/dist/utils/reviewCleanup.d.ts +14 -0
  233. package/dist/utils/run.d.ts +9 -0
  234. package/dist/utils/runContext.d.ts +112 -0
  235. package/dist/utils/runContextData.d.ts +42 -0
  236. package/dist/utils/runErrorRenderer.d.ts +64 -0
  237. package/dist/utils/runLifecycle.d.ts +89 -0
  238. package/dist/utils/runStartupLog.d.ts +15 -0
  239. package/dist/utils/secrets.d.ts +34 -0
  240. package/dist/utils/setup.d.ts +90 -0
  241. package/dist/utils/shell.d.ts +44 -0
  242. package/dist/utils/skills.d.ts +10 -0
  243. package/dist/utils/subprocess.d.ts +81 -0
  244. package/dist/utils/terraformMcp.d.ts +46 -0
  245. package/dist/utils/time.d.ts +15 -0
  246. package/dist/utils/timer.d.ts +23 -0
  247. package/dist/utils/todoTracking.d.ts +16 -0
  248. package/dist/utils/token.d.ts +52 -0
  249. package/dist/utils/toolLicensing.d.ts +56 -0
  250. package/dist/utils/toolSelection.d.ts +73 -0
  251. package/dist/utils/toon.d.ts +16 -0
  252. package/dist/utils/version.d.ts +2 -0
  253. package/dist/utils/versioning.d.ts +7 -0
  254. package/dist/utils/vertex.d.ts +16 -0
  255. package/dist/utils/workflow.d.ts +13 -0
  256. package/package.json +132 -0
  257. package/src/agents/claude.ts +1588 -0
  258. package/src/agents/claudePretoolGate.ts +286 -0
  259. package/src/agents/dispatchMetrics.ts +162 -0
  260. package/src/agents/gateServer.ts +178 -0
  261. package/src/agents/index.ts +10 -0
  262. package/src/agents/nativeFsDenies.ts +164 -0
  263. package/src/agents/opencode.ts +1600 -0
  264. package/src/agents/opencodePlugin.ts +304 -0
  265. package/src/agents/opencodeShared.ts +134 -0
  266. package/src/agents/postRun.ts +615 -0
  267. package/src/agents/reviewer.ts +134 -0
  268. package/src/agents/sessionLabeler.ts +185 -0
  269. package/src/agents/shared.ts +377 -0
  270. package/src/agents/subagentModels.ts +40 -0
  271. package/src/agents/tokenQuota.ts +203 -0
  272. package/src/agents/writeGateSource.ts +148 -0
  273. package/src/agents/writePolicy.ts +132 -0
  274. package/src/brand.ts +84 -0
  275. package/src/cli.ts +117 -0
  276. package/src/commands/gha.ts +188 -0
  277. package/src/commands/mcp.ts +126 -0
  278. package/src/commands/verifyEvidence.ts +100 -0
  279. package/src/entry.ts +7 -0
  280. package/src/entryPost.ts +109 -0
  281. package/src/external.ts +308 -0
  282. package/src/i18n/scaffolding.ts +191 -0
  283. package/src/index.ts +7 -0
  284. package/src/internal/index.ts +74 -0
  285. package/src/lifecycle.ts +2 -0
  286. package/src/main.ts +815 -0
  287. package/src/mcp/__fixtures__/infraweaver-scratch-pr-49-review-3485940013.json +110 -0
  288. package/src/mcp/__fixtures__/infraweaver-scratch-pr-64-review-3531000326.json +14 -0
  289. package/src/mcp/__fixtures__/infraweaver-test-repo-pr-1.diff.json +67 -0
  290. package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
  291. package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
  292. package/src/mcp/arkConfig.ts +7 -0
  293. package/src/mcp/assess.ts +723 -0
  294. package/src/mcp/capabilityContext.ts +80 -0
  295. package/src/mcp/changeSummary.ts +145 -0
  296. package/src/mcp/checkSuite.ts +261 -0
  297. package/src/mcp/checkout.ts +1015 -0
  298. package/src/mcp/comment.ts +683 -0
  299. package/src/mcp/commitInfo.ts +57 -0
  300. package/src/mcp/crosswalk.ts +1253 -0
  301. package/src/mcp/crosswalkDigest.ts +5 -0
  302. package/src/mcp/cyberEssentials.ts +60 -0
  303. package/src/mcp/dashboard.ts +232 -0
  304. package/src/mcp/dependencies.ts +190 -0
  305. package/src/mcp/frameworks.ts +236 -0
  306. package/src/mcp/geminiSanitizer.ts +212 -0
  307. package/src/mcp/git.ts +1116 -0
  308. package/src/mcp/guardrails.ts +771 -0
  309. package/src/mcp/issue.ts +74 -0
  310. package/src/mcp/issueComments.ts +48 -0
  311. package/src/mcp/issueEvents.ts +100 -0
  312. package/src/mcp/issueInfo.ts +72 -0
  313. package/src/mcp/labels.ts +35 -0
  314. package/src/mcp/localContext.ts +65 -0
  315. package/src/mcp/localServer.ts +217 -0
  316. package/src/mcp/moduleExtraction.ts +368 -0
  317. package/src/mcp/moduleTests.ts +421 -0
  318. package/src/mcp/modules.ts +752 -0
  319. package/src/mcp/output.ts +71 -0
  320. package/src/mcp/pathSafety.ts +28 -0
  321. package/src/mcp/policy.ts +226 -0
  322. package/src/mcp/pr.ts +238 -0
  323. package/src/mcp/prInfo.ts +91 -0
  324. package/src/mcp/providerSchema.ts +175 -0
  325. package/src/mcp/review.ts +1081 -0
  326. package/src/mcp/reviewComments.ts +1137 -0
  327. package/src/mcp/roots.ts +217 -0
  328. package/src/mcp/scope.ts +82 -0
  329. package/src/mcp/selectMode.ts +207 -0
  330. package/src/mcp/server.ts +498 -0
  331. package/src/mcp/shared.ts +120 -0
  332. package/src/mcp/shell.ts +631 -0
  333. package/src/mcp/staleFix.ts +557 -0
  334. package/src/mcp/terraform/__snapshots__/moduleDocs.test.ts.snap +40 -0
  335. package/src/mcp/terraform/azurePrices.ts +38 -0
  336. package/src/mcp/terraform/baseline.ts +172 -0
  337. package/src/mcp/terraform/concernResult.ts +111 -0
  338. package/src/mcp/terraform/cost.ts +175 -0
  339. package/src/mcp/terraform/cspm.ts +326 -0
  340. package/src/mcp/terraform/currency.ts +354 -0
  341. package/src/mcp/terraform/decisions.ts +590 -0
  342. package/src/mcp/terraform/eidas.ts +134 -0
  343. package/src/mcp/terraform/evidence.ts +771 -0
  344. package/src/mcp/terraform/findings.ts +383 -0
  345. package/src/mcp/terraform/fixMemory.ts +253 -0
  346. package/src/mcp/terraform/governance.ts +231 -0
  347. package/src/mcp/terraform/hcl.ts +273 -0
  348. package/src/mcp/terraform/iacLanguages.ts +431 -0
  349. package/src/mcp/terraform/idleFloor.ts +304 -0
  350. package/src/mcp/terraform/keyless.ts +123 -0
  351. package/src/mcp/terraform/moduleDocs.ts +303 -0
  352. package/src/mcp/terraform/nativeRules.ts +152 -0
  353. package/src/mcp/terraform/nativeScan.ts +67 -0
  354. package/src/mcp/terraform/noiseProfile.ts +183 -0
  355. package/src/mcp/terraform/normalization.ts +265 -0
  356. package/src/mcp/terraform/oscal.ts +283 -0
  357. package/src/mcp/terraform/packs/azurerm-3-4.ts +382 -0
  358. package/src/mcp/terraform/packs/cloudflare-4-5.ts +114 -0
  359. package/src/mcp/terraform/paths.ts +59 -0
  360. package/src/mcp/terraform/plan.ts +377 -0
  361. package/src/mcp/terraform/planDrift.ts +520 -0
  362. package/src/mcp/terraform/policyAuthor.ts +886 -0
  363. package/src/mcp/terraform/prowlerOcsf.ts +380 -0
  364. package/src/mcp/terraform/refactor.ts +879 -0
  365. package/src/mcp/terraform/registry.ts +317 -0
  366. package/src/mcp/terraform/risk.ts +99 -0
  367. package/src/mcp/terraform/scanSession.ts +242 -0
  368. package/src/mcp/terraform/scannerCache.ts +86 -0
  369. package/src/mcp/terraform/scannerEgress.ts +141 -0
  370. package/src/mcp/terraform/scannerJson.ts +22 -0
  371. package/src/mcp/terraform/scanners.ts +1134 -0
  372. package/src/mcp/terraform/signing.ts +151 -0
  373. package/src/mcp/terraform/stateLocking.ts +106 -0
  374. package/src/mcp/terraform/subprocess.ts +102 -0
  375. package/src/mcp/terraform/suppressions.ts +551 -0
  376. package/src/mcp/terraform/taxonomy.ts +0 -0
  377. package/src/mcp/terraform/tfquery.ts +159 -0
  378. package/src/mcp/terraform/toolchainIntegrity.ts +151 -0
  379. package/src/mcp/terraform/tools/authorPolicy.ts +185 -0
  380. package/src/mcp/terraform/tools/detectPlanDrift.ts +279 -0
  381. package/src/mcp/terraform/tools/emitCost.ts +130 -0
  382. package/src/mcp/terraform/tools/emitSarif.ts +108 -0
  383. package/src/mcp/terraform/tools/fixMemory.ts +82 -0
  384. package/src/mcp/terraform/tools/infracostDiff.ts +110 -0
  385. package/src/mcp/terraform/tools/ingestExternalFindings.ts +259 -0
  386. package/src/mcp/terraform/tools/moduleLookup.ts +86 -0
  387. package/src/mcp/terraform/tools/moduleSearch.ts +46 -0
  388. package/src/mcp/terraform/tools/plan.ts +384 -0
  389. package/src/mcp/terraform/tools/providerUpgrade.ts +217 -0
  390. package/src/mcp/terraform/tools/readFindings.ts +138 -0
  391. package/src/mcp/terraform/tools/scan.ts +303 -0
  392. package/src/mcp/terraform/tools/validate.ts +160 -0
  393. package/src/mcp/terraform/tools/verifyRemediation.ts +190 -0
  394. package/src/mcp/terraform/tools/versionCurrency.ts +64 -0
  395. package/src/mcp/terraform/tools.ts +62 -0
  396. package/src/mcp/terraform/transformPacks.ts +594 -0
  397. package/src/mcp/terraform/types.ts +382 -0
  398. package/src/mcp/terraform/verification.ts +150 -0
  399. package/src/mcp/terraform/vex.ts +367 -0
  400. package/src/mcp/terraform/writeOnly.ts +252 -0
  401. package/src/mcp/terraform.ts +52 -0
  402. package/src/mcp/terratest.ts +215 -0
  403. package/src/mcp/untrustedContent.ts +126 -0
  404. package/src/mcp/upload.ts +123 -0
  405. package/src/models.ts +753 -0
  406. package/src/modes/address-reviews.ts +35 -0
  407. package/src/modes/assess.ts +27 -0
  408. package/src/modes/build.ts +86 -0
  409. package/src/modes/compliance-audit.ts +25 -0
  410. package/src/modes/cost-optimization.ts +36 -0
  411. package/src/modes/drift-detect.ts +31 -0
  412. package/src/modes/fix.ts +29 -0
  413. package/src/modes/incremental-review.ts +103 -0
  414. package/src/modes/index.ts +94 -0
  415. package/src/modes/modernize-deprecated.ts +36 -0
  416. package/src/modes/plan.ts +21 -0
  417. package/src/modes/policy-gate.ts +27 -0
  418. package/src/modes/prFormats.ts +325 -0
  419. package/src/modes/refactor.ts +74 -0
  420. package/src/modes/refresh-remediation.ts +40 -0
  421. package/src/modes/remediate-and-refactor.ts +42 -0
  422. package/src/modes/remediate.ts +78 -0
  423. package/src/modes/resolve-conflicts.ts +32 -0
  424. package/src/modes/review.ts +135 -0
  425. package/src/modes/summarize-pr.ts +42 -0
  426. package/src/modes/task.ts +26 -0
  427. package/src/modes/terraform-code-review.ts +68 -0
  428. package/src/modes/types.ts +17 -0
  429. package/src/modes/update-dependencies.ts +44 -0
  430. package/src/prep/index.ts +96 -0
  431. package/src/prep/installNodeDependencies.ts +251 -0
  432. package/src/prep/installPythonDependencies.ts +235 -0
  433. package/src/prep/types.ts +38 -0
  434. package/src/reviewQuality.ts +237 -0
  435. package/src/runCli.ts +335 -0
  436. package/src/skills/terraform-best-practices/SKILL.md +379 -0
  437. package/src/toolState.ts +241 -0
  438. package/src/utils/activity.ts +210 -0
  439. package/src/utils/agent.ts +245 -0
  440. package/src/utils/agentHangReport.ts +180 -0
  441. package/src/utils/annotations.ts +56 -0
  442. package/src/utils/apiFetch.ts +19 -0
  443. package/src/utils/apiKeys.ts +305 -0
  444. package/src/utils/apiUrl.ts +42 -0
  445. package/src/utils/assets.ts +123 -0
  446. package/src/utils/baseRefConfig.ts +254 -0
  447. package/src/utils/billingErrors.ts +210 -0
  448. package/src/utils/body.ts +168 -0
  449. package/src/utils/buildInfraweaverFooter.ts +104 -0
  450. package/src/utils/byokFallback.ts +137 -0
  451. package/src/utils/changeImpact.ts +330 -0
  452. package/src/utils/claudeSubscription.ts +93 -0
  453. package/src/utils/cli.ts +36 -0
  454. package/src/utils/cloudClient.ts +57 -0
  455. package/src/utils/cloudFetchCore.ts +139 -0
  456. package/src/utils/cloudReport.ts +316 -0
  457. package/src/utils/codexHome.ts +204 -0
  458. package/src/utils/codexOAuth.ts +154 -0
  459. package/src/utils/codexRefreshDetect.ts +36 -0
  460. package/src/utils/diffCoverage.ts +404 -0
  461. package/src/utils/env.ts +84 -0
  462. package/src/utils/errorReport.ts +94 -0
  463. package/src/utils/exitHandler.ts +35 -0
  464. package/src/utils/fixDoubleEscapedString.ts +9 -0
  465. package/src/utils/ghaCore.ts +13 -0
  466. package/src/utils/gitAuth.ts +268 -0
  467. package/src/utils/gitAuthServer.ts +186 -0
  468. package/src/utils/github.ts +676 -0
  469. package/src/utils/globals.ts +9 -0
  470. package/src/utils/humanEditCapture.ts +198 -0
  471. package/src/utils/infraweaverConfig.ts +202 -0
  472. package/src/utils/install.ts +279 -0
  473. package/src/utils/instructions.ts +640 -0
  474. package/src/utils/keylessOidc.ts +47 -0
  475. package/src/utils/leapingComment.ts +20 -0
  476. package/src/utils/learnings.ts +145 -0
  477. package/src/utils/learningsTruncate.ts +42 -0
  478. package/src/utils/lifecycle.ts +198 -0
  479. package/src/utils/log.ts +452 -0
  480. package/src/utils/modelResolver.ts +105 -0
  481. package/src/utils/moduleFetch.ts +138 -0
  482. package/src/utils/normalizeEnv.ts +106 -0
  483. package/src/utils/openCodeModels.ts +86 -0
  484. package/src/utils/openaiCompatible.ts +52 -0
  485. package/src/utils/opentofuMcp.ts +61 -0
  486. package/src/utils/overrides.ts +100 -0
  487. package/src/utils/packageManager.ts +257 -0
  488. package/src/utils/patchWorkflowRunFields.ts +173 -0
  489. package/src/utils/payload.ts +1206 -0
  490. package/src/utils/prSummary.ts +148 -0
  491. package/src/utils/presets.ts +105 -0
  492. package/src/utils/progressComment.ts +266 -0
  493. package/src/utils/providerErrors.ts +189 -0
  494. package/src/utils/proxyModel.ts +239 -0
  495. package/src/utils/rangeDiff.ts +182 -0
  496. package/src/utils/redact.ts +86 -0
  497. package/src/utils/registryAuth.ts +115 -0
  498. package/src/utils/remediationCommand.ts +144 -0
  499. package/src/utils/retry.ts +180 -0
  500. package/src/utils/reviewCleanup.ts +116 -0
  501. package/src/utils/run.ts +100 -0
  502. package/src/utils/runContext.ts +276 -0
  503. package/src/utils/runContextData.ts +121 -0
  504. package/src/utils/runErrorRenderer.ts +282 -0
  505. package/src/utils/runFixture.ts +91 -0
  506. package/src/utils/runLifecycle.ts +360 -0
  507. package/src/utils/runStartupLog.ts +67 -0
  508. package/src/utils/secrets.ts +208 -0
  509. package/src/utils/setup.ts +368 -0
  510. package/src/utils/shell.ts +132 -0
  511. package/src/utils/skills.ts +67 -0
  512. package/src/utils/subprocess.ts +474 -0
  513. package/src/utils/terraformMcp.ts +99 -0
  514. package/src/utils/time.ts +59 -0
  515. package/src/utils/timer.ts +72 -0
  516. package/src/utils/todoTracking.ts +168 -0
  517. package/src/utils/token.ts +294 -0
  518. package/src/utils/toolLicensing.ts +152 -0
  519. package/src/utils/toolSelection.ts +239 -0
  520. package/src/utils/toon.ts +76 -0
  521. package/src/utils/version.ts +10 -0
  522. package/src/utils/versioning.ts +44 -0
  523. package/src/utils/vertex.ts +94 -0
  524. package/src/utils/workflow.ts +25 -0
@@ -0,0 +1,1206 @@
1
+ import { isAbsolute, resolve } from "node:path";
2
+ import * as core from "@actions/core";
3
+ import { type } from "arktype";
4
+ import { parseRunTokenQuota } from "#app/agents/tokenQuota";
5
+ import { PRODUCT_NAME, PRODUCT_SLUG } from "#app/brand";
6
+ import type { AuthorPermission, PayloadEvent } from "#app/external";
7
+ import { parseToolsNetwork } from "#app/mcp/terraform/scannerEgress";
8
+ import { BUILTIN_MODE_NAMES } from "#app/modes";
9
+ import { applyBaseRefConfigPolicy } from "#app/utils/baseRefConfig";
10
+ import { log } from "#app/utils/cli";
11
+ import { parseRemediationCommand } from "#app/utils/remediationCommand";
12
+ import type { RepoSettings } from "#app/utils/runContext";
13
+ import { loadInfraweaverConfig } from "#app/utils/infraweaverConfig";
14
+ import { parseToolSelection } from "#app/utils/toolSelection";
15
+ import { validateCompatibility } from "#app/utils/versioning";
16
+ import packageJson from "#package.json" with { type: "json" };
17
+
18
+ // tool permission enum types for inputs
19
+ const ShellPermissionInput = type.enumerated(
20
+ "disabled",
21
+ "restricted",
22
+ "enabled",
23
+ );
24
+ const PushPermissionInput = type.enumerated(
25
+ "disabled",
26
+ "restricted",
27
+ "enabled",
28
+ );
29
+
30
+ // schema for JSON payload passed via prompt (internal dispatch invocation)
31
+ // note: permissions are intentionally NOT included here to prevent injection attacks
32
+ // permissions are derived from event.authorPermission instead
33
+ export const JsonPayload = type({
34
+ "~infraweaver": "true",
35
+ version: "string",
36
+ "model?": "string | undefined",
37
+ prompt: "string",
38
+ "triggerer?": "string | undefined",
39
+
40
+ "eventInstructions?": "string",
41
+ "previousRunsNote?": "string",
42
+ "event?": "object",
43
+ "timeout?": "string | undefined",
44
+ "progressComment?": type({
45
+ id: "string",
46
+ type: "'issue' | 'review'",
47
+ }).or("undefined"),
48
+ "generateSummary?": "boolean | undefined",
49
+ });
50
+
51
+ // permission levels that indicate collaborator status (have push access)
52
+ const COLLABORATOR_PERMISSIONS: AuthorPermission[] = [
53
+ "admin",
54
+ "maintain",
55
+ "write",
56
+ ];
57
+
58
+ // check if the event author has collaborator-level permissions
59
+ function isCollaborator(event: PayloadEvent): boolean {
60
+ const perm = event.authorPermission;
61
+ return perm !== undefined && COLLABORATOR_PERMISSIONS.includes(perm);
62
+ }
63
+
64
+ // inputs schema - action inputs from core.getInput()
65
+ // note: tool permissions use .or("undefined") because getInput() || undefined
66
+ // explicitly sets the property to undefined when empty, which is different from
67
+ // the property being absent. arktype's "prop?" means "optional to include" but
68
+ // if included, must match the type - so we need to explicitly allow undefined.
69
+ export const Inputs = type({
70
+ prompt: "string",
71
+ "model?": type.string.or("undefined"),
72
+ "mode?": type.string.or("undefined"),
73
+ "timeout?": type.string.or("undefined"),
74
+ "push?": PushPermissionInput.or("undefined"),
75
+ "shell?": ShellPermissionInput.or("undefined"),
76
+ "cwd?": type.string.or("undefined"),
77
+ "output_schema?": type.string.or("undefined"),
78
+ // Terraform remediation config (all optional; defaults applied downstream)
79
+ "scan_scope?": type.string.or("undefined"),
80
+ "iac_languages?": type.string.or("undefined"),
81
+ // language for user-facing prose (PR bodies, review text, summaries). en (default)
82
+ // | de | es | pl | ro — the LLM-authored explanations only; never the Terraform.
83
+ "language?": type.string.or("undefined"),
84
+ "severity_threshold?": type.string.or("undefined"),
85
+ "concern_ids?": type.string.or("undefined"),
86
+ "grouping?": type.string.or("undefined"),
87
+ "max_prs?": type.string.or("undefined"),
88
+ "max_run_tokens?": type.string.or("undefined"),
89
+ "pr_concurrent_limit?": type.string.or("undefined"),
90
+ "pr_hourly_limit?": type.string.or("undefined"),
91
+ "allowed_paths?": type.string.or("undefined"),
92
+ "base_branch?": type.string.or("undefined"),
93
+ "allow_replace?": type.string.or("undefined"),
94
+ "protected_paths?": type.string.or("undefined"),
95
+ "autonomy_threshold?": type.string.or("undefined"),
96
+ "required_controls?": type.string.or("undefined"),
97
+ "gitleaks?": type.string.or("undefined"),
98
+ "cost_increase_block_usd?": type.string.or("undefined"),
99
+ "module_catalogue?": type.string.or("undefined"),
100
+ "terratest?": type.string.or("undefined"),
101
+ "docs?": type.string.or("undefined"),
102
+ "terraform_mcp?": type.string.or("undefined"),
103
+ "opentofu_mcp?": type.string.or("undefined"),
104
+ "fix_memory?": type.string.or("undefined"),
105
+ "tools_enabled?": type.string.or("undefined"),
106
+ "tools_network?": type.string.or("undefined"),
107
+ "module_fetch_token?": type.string.or("undefined"),
108
+ "module_fetch_ssh_key?": type.string.or("undefined"),
109
+ "registry_token?": type.string.or("undefined"),
110
+ "registry_host?": type.string.or("undefined"),
111
+ // Refactor: which module-source variant the operator wants (auto | same-repo |
112
+ // org-repo | author | third-party). Structured scope, not prompt text — see
113
+ // parseRefactorSource / withRefactorSource. Gates the behaviour-altering b.4.
114
+ "refactor_source?": type.string.or("undefined"),
115
+ "review_instructions?": type.string.or("undefined"),
116
+ "fp_filtering_instructions?": type.string.or("undefined"),
117
+ "remediation_instructions?": type.string.or("undefined"),
118
+ "refactor_instructions?": type.string.or("undefined"),
119
+ // Infraweaver Cloud org token (tmnd_...) — when present, the run reports its
120
+ // findings/posture to Cloud after success (utils/cloudReport.ts). Optional.
121
+ "api_token?": type.string.or("undefined"),
122
+ });
123
+
124
+ export type Inputs = typeof Inputs.infer;
125
+
126
+ function isPayloadEvent(value: unknown): value is PayloadEvent {
127
+ return typeof value === "object" && value !== null && "trigger" in value;
128
+ }
129
+
130
+ function resolveCwd(cwd: string | undefined): string | undefined {
131
+ const workspace = process.env.GITHUB_WORKSPACE;
132
+ if (!cwd) return workspace;
133
+ if (isAbsolute(cwd)) return cwd;
134
+ return workspace ? resolve(workspace, cwd) : cwd;
135
+ }
136
+
137
+ export type ResolvedPromptInput = string | typeof JsonPayload.infer;
138
+
139
+ /**
140
+ * Built-in, VETTED prompts for the deterministic Terraform CI modes. When one of
141
+ * these modes is pinned via the action `mode:` input, the engine OWNS the prompt —
142
+ * a workflow-supplied prompt is ignored (see {@link resolveEffectivePrompt}) so a
143
+ * managed / Cloud-dispatched run can never be steered by free-form prompt text.
144
+ * Keyed by the canonical mode name returned by {@link parseMode}.
145
+ */
146
+ export const VETTED_MODE_PROMPTS: Readonly<Record<string, string>> = {
147
+ Remediate: [
148
+ "Bring this repository's Terraform up to best practice and open ONE scoped,",
149
+ "reviewable pull request for the highest-severity group of issues. The",
150
+ "deterministic scanners decide what is wrong; you apply the minimal, correct fix",
151
+ "and prove it.",
152
+ "",
153
+ "Priorities, in order:",
154
+ "1. SECURITY — encryption at rest and in transit; no unintended public exposure;",
155
+ " least-privilege IAM (no wildcard actions/resources); host hardening (e.g.",
156
+ " IMDSv2); no plaintext secrets in .tf/.tfvars.",
157
+ "2. CORRECTNESS — must pass `terraform validate`; no deprecated syntax; pin",
158
+ " provider and module versions.",
159
+ "3. IDIOMATIC STYLE — clear, consistent naming; prefer variables/locals and",
160
+ " well-maintained modules over copy-pasted blocks. Match the file's existing",
161
+ " formatting in your edit; do NOT run `terraform fmt` to reformat pre-existing",
162
+ " style drift (it rewrites the whole file and buries the fix).",
163
+ "4. TAGGING & GOVERNANCE — consistent owner / environment / cost-center tags.",
164
+ "5. COST — reach best practice the cost-effective way; surface any meaningful",
165
+ " monthly cost change.",
166
+ "",
167
+ "Discipline: only modify Terraform files (*.tf, *.tfvars) — never CI/workflow",
168
+ "files, application code, or docs. Make the smallest change that resolves each",
169
+ "concern; do not refactor unrelated code. Prove every fix by re-scanning the",
170
+ "branch (✗ → ✓). The PR body must explain, per issue, what was wrong, what",
171
+ "changed, and why it is safe. Never auto-merge — always leave the PR for human",
172
+ "review.",
173
+ ].join("\n"),
174
+ Assess: [
175
+ "Assess this repository's Terraform and report its security and best-practice",
176
+ "posture. Run the deterministic scanners, summarise the findings by severity, and",
177
+ "explain the highest-priority risks in priority order (security → correctness →",
178
+ "idiomatic style → tagging → cost). Do NOT modify any files and do NOT open a",
179
+ "pull request — this is a read-only assessment.",
180
+ ].join("\n"),
181
+ TerraformCodeReview: [
182
+ "Review the Terraform changes in this pull request and submit ONE PR review. Scope",
183
+ "is *.tf / *.tfvars / *.tf.json / *.tfvars.json ONLY — note any non-Terraform files",
184
+ "as out of scope, do not review them.",
185
+ "",
186
+ "Look for, in priority order: SECURITY misconfig (wildcard IAM, public exposure,",
187
+ "missing encryption, plaintext secrets, IMDSv1); STATE SAFETY (a change that forces a",
188
+ "destroy/replace of a stateful resource, a relocated address missing a `moved {}`",
189
+ "block, a flipped force_destroy / deletion_protection / prevent_destroy); CORRECTNESS",
190
+ "(would it pass `terraform validate`? deprecated syntax, wrong types, count/for_each",
191
+ "churn); VERSION & SUPPLY-CHAIN (unpinned or major-bumped provider/module pins, an",
192
+ "unpinned remote module source); and MODULE/INTERFACE hygiene (insecure variable",
193
+ "defaults, unmarked sensitive outputs).",
194
+ "",
195
+ "This is READ-ONLY — do NOT modify any files, commit, push, or open a PR. The PR",
196
+ "review is the only deliverable. Anchor findings to changed lines; raise a",
197
+ "destroy/replace or a major version bump for explicit human sign-off rather than",
198
+ "treating it as routine.",
199
+ ].join("\n"),
200
+ Refactor: [
201
+ "Standardise this repository's Terraform WITHOUT changing behaviour, and open ONE scoped,",
202
+ "reviewable pull request that PROVES the change is a no-op on live infrastructure. There are",
203
+ "two kinds of behaviour-preserving refactor — do whichever has the most value here:",
204
+ " (a) MODULARISE — lift a cohesive cluster of raw resources behind a module boundary (an",
205
+ " existing module, same repo or a pinned cross-repo one, or one you mechanically",
206
+ " extract), with a `moved {}` block for every relocated address.",
207
+ " (b) NORMALISE in place — apply provably-equivalent idiomatic cleanups that change SYNTAX,",
208
+ " not behaviour: drop redundant whole-string interpolation wrapping, convert legacy",
209
+ " HCL0.11 block-assignment (`config {` / `vars {`) to `= {`, split an overgrown file.",
210
+ " These relocate nothing, so they need no `moved {}` blocks and are proven the same",
211
+ " way (same resource set, same argument names, validate + fmt clean).",
212
+ "Use `module_extraction_candidates` and `terraform_normalization_candidates` to find both.",
213
+ "",
214
+ "Discipline:",
215
+ "1. EQUIVALENCE, not alteration — the resource set, every resource's arguments, and",
216
+ " behaviour must be identical before and after. Generate a `moved {}` block for",
217
+ " EVERY relocated address; a missing one silently destroys and recreates a resource.",
218
+ "2. PROVE it — terraform_equivalence_check must report `equivalent: true` (zero",
219
+ " uncovered moves, resource set + arguments preserved, validate + fmt clean) before",
220
+ " you push. When cloud credentials exist, a `refactor_safe` plan corroborates it.",
221
+ "3. SECURE defaults — when you parameterise a hardcoded value into a variable, its",
222
+ " default must be the secure choice or absent; never preserve an insecure default.",
223
+ "4. Only modify Terraform files (*.tf, *.tfvars). A module in a separate repo is",
224
+ " READ-ONLY — never open a write PR against it; fix at the call site via inputs.",
225
+ "5. NOT a refactor — swapping a resource TYPE or adding/removing a managed resource",
226
+ " (e.g. aws_launch_configuration→aws_launch_template, aws_elb→aws_lb, the template_file",
227
+ " data source→the templatefile() function) ALTERS behaviour. Leave it to Remediate and",
228
+ " never claim equivalence for it. If that is the only work left, say so — do not report",
229
+ " the repository as already idiomatic.",
230
+ "",
231
+ "Do NOT ship a refactor you cannot prove equivalent — abandon or escalate it. Never",
232
+ "auto-merge; always leave the PR for human review. The PR body must carry the",
233
+ "equivalence proof (resource-set, argument, and moved-block coverage).",
234
+ ].join("\n"),
235
+ RemediateAndRefactor: [
236
+ "Do BOTH a best-practice fix AND a behaviour-preserving modularisation of this repository's",
237
+ "Terraform, on ONE branch, and open ONE pull request that carries TWO distinct, un-entangled",
238
+ "proofs. The two halves are different categories of change and an auditor reads them",
239
+ "differently — never blur them together.",
240
+ "",
241
+ "Run them in this ORDER (the order is load-bearing — the equivalence proof depends on it):",
242
+ "1. REMEDIATE FIRST. Scan with the deterministic tools, take the highest-severity group (or",
243
+ " the TARGETED FINDINGS group if this run named concern ids), apply the minimal correct fix,",
244
+ " `terraform validate`, and prove it by re-scanning (✗ → ✓). Then COMMIT the fix on the",
245
+ " branch — this committed state is the baseline the refactor's equivalence is measured",
246
+ " against, so the fix's argument changes are NOT counted against the refactor.",
247
+ "2. REFACTOR SECOND, on top of the committed fix. Modularise a cohesive cluster (or apply an",
248
+ " idiomatic in-place normalisation), authoring a `moved {}` block for EVERY relocated",
249
+ " address. Run terraform_equivalence_check while the refactor edits are still UNCOMMITTED —",
250
+ " it diffs the working tree against the committed fix and must return `equivalent: true`",
251
+ " (same resource set, same arguments, zero uncovered moves, validate + fmt clean) before you",
252
+ " commit and push. A refactor you cannot prove equivalent is abandoned, never pushed.",
253
+ "",
254
+ "Discipline: only modify Terraform files (*.tf, *.tfvars). The fix is proven by the ✗→✓",
255
+ "re-scan; the refactor is proven by equivalence — keep them as SEPARATE PR sections (a",
256
+ "`## Validation` block for the fix and a `## Equivalence proof` block for the refactor), never",
257
+ "one narrative. A module in a separate repo is READ-ONLY — never write to it. Respect the",
258
+ "pinned refactor_source if one was supplied. Never auto-merge; leave the PR for human review.",
259
+ ].join("\n"),
260
+ PolicyGate: [
261
+ "Run this repository's own policy-as-code (OPA/conftest Rego under policy/, policies/, or",
262
+ ".conftest) as a READ-ONLY gate and report whether it passes. conftest evaluates a Terraform",
263
+ "plan, so produce a plan first; when no cloud credentials are available the gate is",
264
+ "INCONCLUSIVE, not passing — say so rather than reporting a false green. Report PASS, FAIL",
265
+ "(list each violation: policy, resource, message), or INCONCLUSIVE, and emit a structured",
266
+ "result so CI can fail on a policy violation. Do NOT modify any files, commit, push, or open a",
267
+ "pull request — this is a read-only gate.",
268
+ ].join("\n"),
269
+ UpdateDependencies: [
270
+ "Upgrade this repository's outdated or unpinned Terraform provider and module versions to",
271
+ "current, supported pins, and open ONE scoped, reviewable pull request per provider/module that",
272
+ "PROVES the bump is safe.",
273
+ "",
274
+ "Discipline:",
275
+ "1. CLASSIFY each bump. A version-constraint-only change (patch/minor, or pinning a floating",
276
+ " version) is behaviour-preserving — prove it with terraform_equivalence_check (equivalent:",
277
+ " true; same resource set + arguments, validate + fmt clean). A major bump that needs code",
278
+ " changes ALTERS behaviour — never claim equivalence for it.",
279
+ "2. PROVE a behaviour-affecting bump with a reviewed terraform plan when cloud credentials",
280
+ " exist; with none, ship the PR labelled `proposed — unproven` and say the plan must be",
281
+ " reviewed.",
282
+ "3. A stateful destroy/replace is blocked at push unless allow_replace covers it — abandon",
283
+ " rather than force it. Keep a changed module's examples/tests consistent.",
284
+ "4. Only modify Terraform files (*.tf, *.tfvars). Pin to a CURRENT supported major, never an",
285
+ " ancient one. The PR body states old → new and links the changelog. Never auto-merge.",
286
+ ].join("\n"),
287
+ ModernizeDeprecated: [
288
+ "Migrate this repository's DEPRECATED Terraform patterns to their modern equivalents —",
289
+ "aws_launch_configuration → aws_launch_template, aws_elb → aws_lb, the template_file data",
290
+ "source → the templatefile() function, EOL provider pins — and open ONE scoped pull request per",
291
+ "pattern class.",
292
+ "",
293
+ "Discipline:",
294
+ "1. These swaps ALTER the resource set, so equivalence is FALSE and must NEVER be claimed.",
295
+ " Preserve the effective configuration; author a `moved {}` block wherever an address carries",
296
+ " over, and use create_before_destroy where a replace is unavoidable.",
297
+ "2. PROVE with a reviewed terraform plan when cloud credentials exist; with none, ship the PR",
298
+ " labelled `proposed — unproven` and state plainly that it must be reviewed before merge.",
299
+ "3. A stateful destroy/replace is blocked at push unless allow_replace covers it — abandon",
300
+ " rather than force it. A high blast radius escalates to needs-human.",
301
+ "4. Only modify Terraform files (*.tf, *.tfvars); one pattern class per PR. Never auto-merge.",
302
+ ].join("\n"),
303
+ DriftDetect: [
304
+ "Detect whether this repository's deployed infrastructure has DRIFTED from its committed",
305
+ "Terraform, and report it. Drift is any change / replace / destroy a plan would make to reconcile",
306
+ "live state with the unchanged code. Prefer the zero-credential path: read a `terraform show -json`",
307
+ `plan artifact the CI already produced (${PRODUCT_NAME} never holds cloud creds); otherwise run a`,
308
+ "read-only terraform plan against live state.",
309
+ "",
310
+ "Discipline:",
311
+ "1. Drift is measured from a plan — a CI-produced plan artifact (no credentials needed) or a",
312
+ " read-only plan against live state (a read-only cloud role / OIDC is enough). Only when NEITHER",
313
+ " a plan artifact NOR cloud credentials are available is the result INCONCLUSIVE, not 'no drift' —",
314
+ " report that plainly so a green check never masks an unchecked stack.",
315
+ "2. Report NO DRIFT (clean plan), DRIFT (list the drifted addresses, the blast radius, and call",
316
+ " out any stateful resource whose reconciliation could destroy data), or INCONCLUSIVE. Treat a",
317
+ " non-deterministic plan as a perpetual-diff smell, not true drift.",
318
+ `3. Do NOT modify any files, commit, push, open a PR, or apply — ${PRODUCT_NAME} never reconciles drift`,
319
+ " itself; the human decides whether to revert the out-of-band change, apply, or update the code.",
320
+ " Emit a structured output so a scheduled workflow can alert on drift.",
321
+ ].join("\n"),
322
+ ComplianceAudit: [
323
+ "Audit this repository's Terraform against the operator's required compliance controls and emit a",
324
+ "machine-readable OSCAL assessment-results document. This is READ-ONLY — do NOT modify any files,",
325
+ "commit, push, or open a PR.",
326
+ "",
327
+ "Discipline:",
328
+ "1. Build the audit from tool evidence, never your own judgement: assess the Terraform, then map",
329
+ " the findings to controls across the supported frameworks (NCSC Cloud Principles, Cyber",
330
+ " Essentials, NHS DSPT, Secure by Design, CIS, SOC 2). When required_controls are supplied, lead",
331
+ " with them — report each as met, at-risk, or not-code-verifiable (honestly, never implying a",
332
+ " pass for a control Terraform alone can't evidence).",
333
+ "2. The crosswalk is an INDICATIVE alignment, not a certified audit verdict — label it as such.",
334
+ "3. Emit the OSCAL assessment-results document as the audit artefact, and a structured output so",
335
+ " CI can gate on an at-risk required control. The report + the OSCAL document are the only",
336
+ " deliverables.",
337
+ ].join("\n"),
338
+ CostOptimization: [
339
+ "Reduce this repository's Terraform cost and open ONE scoped, reviewable pull request per",
340
+ "opportunity, each QUANTIFIED by the infracost monthly delta. Cost analysis needs infracost,",
341
+ `which ${PRODUCT_NAME} does not bundle — when INFRACOST_API_KEY / the infracost CLI is absent, report`,
342
+ "that cost data is unavailable and stop; never guess costs.",
343
+ "",
344
+ "Discipline:",
345
+ "1. Prefer savings with NO capacity loss (gp2 → gp3, idle/unattached resources, oversized",
346
+ " instances rightsized within stated needs). Any change that reduces capacity is a human",
347
+ " decision — surface the trade-off, never silently shrink production.",
348
+ "2. QUANTIFY every change with infracost (the PR headline is the monthly saving); validate, and",
349
+ " plan when cloud credentials exist. A stateful destroy/replace is blocked at push unless",
350
+ " allow_replace covers it — abandon rather than force it.",
351
+ "3. Only modify Terraform files (*.tf, *.tfvars); one opportunity per PR. The body is",
352
+ " evidence-built from infracost + plan, never self-reported. Never auto-merge.",
353
+ ].join("\n"),
354
+ };
355
+
356
+ export function resolvePromptInput(): ResolvedPromptInput {
357
+ // Two surfaces resolve here. `prompt` is the PUBLIC input (declared in
358
+ // action.yml): the free-form task for an un-pinned OSS/self-hosted run — the
359
+ // only way to task the action when no vetted mode is pinned and there is no
360
+ // dispatch UI. `command` is internal plumbing (NOT declared): the
361
+ // `@infraweaver fix …` comment flow (comment-fix.yml) forwards the comment
362
+ // body through it. A pinned vetted mode (remediate/assess/…) supplies its own
363
+ // task and IGNORES a free-form prompt — see resolveEffectivePrompt, the
364
+ // security boundary — so those runs send neither; resolvePayload validates
365
+ // one is present otherwise.
366
+ const command = core.getInput("command") || core.getInput("prompt");
367
+ if (!command.trim()) return "";
368
+
369
+ let parsed: unknown;
370
+ try {
371
+ parsed = JSON.parse(command);
372
+ } catch {
373
+ // JSON parse error is fine (plain text command)
374
+ return command;
375
+ }
376
+
377
+ if (!parsed || typeof parsed !== "object" || !("~infraweaver" in parsed)) {
378
+ // if it doesn't look like a infraweaver payload, return the plain text command
379
+ return command;
380
+ }
381
+
382
+ // validation errors should propagate
383
+ const jsonPayload = JsonPayload.assert(parsed);
384
+ validateCompatibility(jsonPayload.version, packageJson.version);
385
+ return jsonPayload;
386
+ }
387
+
388
+ /**
389
+ * The prompt a run actually uses. For a pinned mode that ships a vetted standard
390
+ * (the Terraform CI modes in {@link VETTED_MODE_PROMPTS}), the engine owns the
391
+ * prompt: a FREE-FORM workflow prompt is IGNORED (and warned) so it can't steer a
392
+ * managed/CI run — this is the security boundary. Two things are deliberately NOT
393
+ * overridden: the internal JSON dispatch payload (trusted), and a recognised
394
+ * `@infraweaver fix …` command (`isRemediationCommand`) — the comment-driven flow
395
+ * needs the structured command to reach the agent. Otherwise the workflow prompt is
396
+ * used, and is required when no vetted mode supplies one.
397
+ */
398
+ export function resolveEffectivePrompt(
399
+ rawPrompt: string,
400
+ fromJsonPayload: boolean,
401
+ mode: string | undefined,
402
+ isRemediationCommand: boolean,
403
+ ): string {
404
+ const vetted = mode ? VETTED_MODE_PROMPTS[mode] : undefined;
405
+ if (!fromJsonPayload && vetted && !isRemediationCommand) {
406
+ if (rawPrompt.trim()) {
407
+ log.warning(
408
+ `» ignoring the free-form workflow prompt for pinned mode ${mode} — ${PRODUCT_NAME} uses its built-in standard for this mode (the prompt is engine-owned).`,
409
+ );
410
+ }
411
+ return vetted;
412
+ }
413
+ if (!rawPrompt.trim()) {
414
+ const vettedModes = Object.keys(VETTED_MODE_PROMPTS)
415
+ .map((m) => `mode: ${m.toLowerCase()}`)
416
+ .join(" or ");
417
+ throw new Error(
418
+ `This run has no mode pinned and no task to run. Pin a mode that ships a built-in standard (${vettedModes}).`,
419
+ );
420
+ }
421
+ return rawPrompt;
422
+ }
423
+
424
+ function resolveNonPromptInputs() {
425
+ return Inputs.omit("prompt").assert({
426
+ model: core.getInput("model") || undefined,
427
+ mode: core.getInput("mode") || undefined,
428
+ timeout: core.getInput("timeout") || undefined,
429
+ cwd: core.getInput("cwd") || undefined,
430
+ push: core.getInput("push") || undefined,
431
+ shell: core.getInput("shell") || undefined,
432
+ scan_scope: core.getInput("scan_scope") || undefined,
433
+ iac_languages: core.getInput("iac_languages") || undefined,
434
+ language: core.getInput("language") || undefined,
435
+ severity_threshold: core.getInput("severity_threshold") || undefined,
436
+ concern_ids: core.getInput("concern_ids") || undefined,
437
+ grouping: core.getInput("grouping") || undefined,
438
+ max_prs: core.getInput("max_prs") || undefined,
439
+ max_run_tokens: core.getInput("max_run_tokens") || undefined,
440
+ pr_concurrent_limit: core.getInput("pr_concurrent_limit") || undefined,
441
+ pr_hourly_limit: core.getInput("pr_hourly_limit") || undefined,
442
+ allowed_paths: core.getInput("allowed_paths") || undefined,
443
+ base_branch: core.getInput("base_branch") || undefined,
444
+ allow_replace: core.getInput("allow_replace") || undefined,
445
+ protected_paths: core.getInput("protected_paths") || undefined,
446
+ autonomy_threshold: core.getInput("autonomy_threshold") || undefined,
447
+ required_controls: core.getInput("required_controls") || undefined,
448
+ gitleaks: core.getInput("gitleaks") || undefined,
449
+ cost_increase_block_usd:
450
+ core.getInput("cost_increase_block_usd") || undefined,
451
+ module_catalogue: core.getInput("module_catalogue") || undefined,
452
+ terratest: core.getInput("terratest") || undefined,
453
+ docs: core.getInput("docs") || undefined,
454
+ terraform_mcp: core.getInput("terraform_mcp") || undefined,
455
+ opentofu_mcp: core.getInput("opentofu_mcp") || undefined,
456
+ fix_memory: core.getInput("fix_memory") || undefined,
457
+ tools_enabled: core.getInput("tools_enabled") || undefined,
458
+ tools_network: core.getInput("tools_network") || undefined,
459
+ module_fetch_token: core.getInput("module_fetch_token") || undefined,
460
+ module_fetch_ssh_key: core.getInput("module_fetch_ssh_key") || undefined,
461
+ registry_token: core.getInput("registry_token") || undefined,
462
+ registry_host: core.getInput("registry_host") || undefined,
463
+ refactor_source: core.getInput("refactor_source") || undefined,
464
+ review_instructions: core.getInput("review_instructions") || undefined,
465
+ fp_filtering_instructions:
466
+ core.getInput("fp_filtering_instructions") || undefined,
467
+ remediation_instructions:
468
+ core.getInput("remediation_instructions") || undefined,
469
+ refactor_instructions: core.getInput("refactor_instructions") || undefined,
470
+ api_token: core.getInput("api_token") || undefined,
471
+ });
472
+ }
473
+
474
+ /**
475
+ * Short aliases the control plane may send for a mode, mapped to the canonical
476
+ * built-in name. The Cloud dispatch contract uses the terse `refresh` for the
477
+ * RefreshRemediation sweep (see infraweaver-ui `remediation-dispatch.ts`'s
478
+ * `DispatchMode`); accepting it here lets a listener workflow forward
479
+ * `client_payload.mode` verbatim instead of every consumer having to know the
480
+ * engine's exact casing. Keyed lowercase.
481
+ */
482
+ const MODE_ALIASES: Readonly<Record<string, string>> = {
483
+ refresh: "RefreshRemediation",
484
+ moduleupgrade: "UpdateDependencies",
485
+ modernize: "ModernizeDeprecated",
486
+ costoptimisation: "CostOptimization",
487
+ tfreview: "TerraformCodeReview",
488
+ terraformreview: "TerraformCodeReview",
489
+ drift: "DriftDetect",
490
+ driftcheck: "DriftDetect",
491
+ compliance: "ComplianceAudit",
492
+ };
493
+
494
+ /**
495
+ * Canonicalize the `mode` input against the built-in mode names
496
+ * (case-insensitive), resolving any control-plane alias first. Returns the
497
+ * canonical name (e.g. "remediate" → "Remediate", "refresh" →
498
+ * "RefreshRemediation") when it matches a built-in mode, letting CI pin a mode
499
+ * deterministically. Unknown non-empty values warn and return undefined so the
500
+ * agent falls back to prompt-driven `select_mode` — mirroring how an unknown
501
+ * `model` slug degrades to auto-select rather than hard-failing the run.
502
+ */
503
+ export function parseMode(raw: string | undefined): string | undefined {
504
+ const v = raw?.trim();
505
+ if (!v) return undefined;
506
+ const canonical = MODE_ALIASES[v.toLowerCase()] ?? v;
507
+ const match = BUILTIN_MODE_NAMES.find(
508
+ (name) => name.toLowerCase() === canonical.toLowerCase(),
509
+ );
510
+ if (!match) {
511
+ log.warning(
512
+ `» unknown mode "${v}" — agent will select a mode (valid: ${BUILTIN_MODE_NAMES.join(", ")})`,
513
+ );
514
+ return undefined;
515
+ }
516
+ return match;
517
+ }
518
+
519
+ /** parse scan_scope; "diff" or "full" (default). */
520
+ function parseScanScope(raw: string | undefined): "diff" | "full" | undefined {
521
+ const v = raw?.trim().toLowerCase();
522
+ return v === "diff" || v === "full" ? v : undefined;
523
+ }
524
+
525
+ /** parse the remediation grouping strategy; "file" (default — one PR per file,
526
+ * smaller blast radius) or "rule" (one PR per rule across files — for sweeping,
527
+ * low-risk fixes). undefined when unset/invalid, so the consumer default applies. */
528
+ function parseGrouping(raw: string | undefined): "file" | "rule" | undefined {
529
+ const v = raw?.trim().toLowerCase();
530
+ return v === "file" || v === "rule" ? v : undefined;
531
+ }
532
+
533
+ /** parse the comma-separated `concern_ids` focus list — the specific concern ids a
534
+ * dashboard "Remediate" dispatch targets. Structured scope (like severity/paths),
535
+ * NOT task text. undefined when unset. */
536
+ export function parseConcernIds(raw: string | undefined): string[] | undefined {
537
+ if (!raw) return undefined;
538
+ const ids = raw
539
+ .split(",")
540
+ .map((s) => s.trim())
541
+ .filter(Boolean);
542
+ return ids.length > 0 ? ids : undefined;
543
+ }
544
+
545
+ /**
546
+ * Append an engine-owned "fix exactly these concern ids" directive to a Remediate
547
+ * run's task when the dispatch targeted specific findings (the Cloud "Remediate"
548
+ * button). The task text stays engine-owned — only the id list comes from the
549
+ * trusted dispatch — so this narrows scope without letting a customer author the
550
+ * prompt. A no-op when there are no targeted ids.
551
+ */
552
+ export function withConcernFocus(
553
+ prompt: string,
554
+ concernIds: string[] | undefined,
555
+ ): string {
556
+ if (!concernIds?.length) return prompt;
557
+ return [
558
+ prompt,
559
+ "",
560
+ "TARGETED FINDINGS — this run was dispatched to remediate specific concern(s).",
561
+ `Act ONLY on the group(s) that contain these concern ids: ${concernIds.join(", ")}.`,
562
+ "Do not act on any other group — not even a higher-severity one — and do not widen",
563
+ "the scope. Fix exactly these concerns, prove each ✗ → ✓ by re-scanning, and open",
564
+ "one scoped PR for them.",
565
+ ].join("\n");
566
+ }
567
+
568
+ /**
569
+ * The five module-source variants a Refactor run can be pinned to. `auto` (the
570
+ * default) lets the engine pick the lowest-risk BEHAVIOUR-PRESERVING source
571
+ * (b.2 → b.1 → b.3) and deliberately excludes the behaviour-altering b.4. The
572
+ * other four map 1:1 onto the spec's b-variants and let the operator force one:
573
+ * - `same-repo` → b.2 (a module already in this repo, `./modules/x`)
574
+ * - `org-repo` → b.1 (a module in a separate org repo, fetched read-only)
575
+ * - `author` → b.3 (mechanically extract a new module)
576
+ * - `third-party` → b.4 (adopt a registry/community module — ALTERS behaviour)
577
+ * The value is structured SCOPE, never task text: the engine still owns the
578
+ * Refactor prompt (the security boundary), and this only narrows which source
579
+ * the agent may resolve to. See {@link withRefactorSource}.
580
+ */
581
+ export type RefactorSource =
582
+ | "auto"
583
+ | "same-repo"
584
+ | "org-repo"
585
+ | "author"
586
+ | "third-party";
587
+
588
+ const REFACTOR_SOURCE_VALUES = new Set<RefactorSource>([
589
+ "auto",
590
+ "same-repo",
591
+ "org-repo",
592
+ "author",
593
+ "third-party",
594
+ ]);
595
+
596
+ /** Aliases the control plane / a workflow may use for a refactor_source value,
597
+ * mapped to the canonical token. Lets the Cloud picker and a human workflow use
598
+ * the variant ids (b1/b2/…) or friendlier words interchangeably. Keyed lowercase. */
599
+ const REFACTOR_SOURCE_ALIASES: Readonly<Record<string, RefactorSource>> = {
600
+ b2: "same-repo",
601
+ "same-repo-module": "same-repo",
602
+ local: "same-repo",
603
+ b1: "org-repo",
604
+ "org-module-repo": "org-repo",
605
+ git: "org-repo",
606
+ "cross-repo": "org-repo",
607
+ b3: "author",
608
+ "author-module": "author",
609
+ new: "author",
610
+ extract: "author",
611
+ b4: "third-party",
612
+ thirdparty: "third-party",
613
+ registry: "third-party",
614
+ external: "third-party",
615
+ };
616
+
617
+ /** parse the `refactor_source` input into a canonical {@link RefactorSource}.
618
+ * Unset → undefined (the consumer treats that as `auto`). An unknown value
619
+ * warns and falls back to `auto` rather than hard-failing the run (mirrors how
620
+ * an unknown `mode`/`model` degrades). */
621
+ export function parseRefactorSource(
622
+ raw: string | undefined,
623
+ ): RefactorSource | undefined {
624
+ const v = raw?.trim().toLowerCase();
625
+ if (!v) return undefined;
626
+ if (REFACTOR_SOURCE_VALUES.has(v as RefactorSource))
627
+ return v as RefactorSource;
628
+ const aliased = REFACTOR_SOURCE_ALIASES[v];
629
+ if (aliased) return aliased;
630
+ log.warning(
631
+ `» unknown refactor_source "${raw}" — defaulting to auto (valid: auto, same-repo, org-repo, author, third-party)`,
632
+ );
633
+ return "auto";
634
+ }
635
+
636
+ /** the engine-owned directive appended for each pinned variant — the structured
637
+ * counterpart of the variant text already in the vetted Refactor prompt. */
638
+ const REFACTOR_SOURCE_DIRECTIVES: Readonly<Record<RefactorSource, string>> = {
639
+ auto: [
640
+ "Resolve the module source automatically, lowest risk first: a module that already",
641
+ "exists in THIS repo (b.2), else one in a separate org repo fetched READ-ONLY (b.1),",
642
+ "else author a new one by mechanical extraction (b.3). Do NOT adopt a third-party /",
643
+ "registry module (b.4): that ALTERS behaviour and is only permitted when the operator",
644
+ "explicitly sets refactor_source to `third-party`. Ship only a refactor proven equivalent.",
645
+ ].join("\n"),
646
+ "same-repo": [
647
+ "Use ONLY a module that already exists in THIS repository (variant b.2 — a local",
648
+ "`./modules/x` path, candidate `kind: local`). Do not author a new module, reach into",
649
+ "another repo, or adopt a registry module. If no suitable in-repo module matches the",
650
+ "cluster, report that and stop — do not fall back to another variant.",
651
+ ].join("\n"),
652
+ "org-repo": [
653
+ "Use ONLY a module from the organisation's SEPARATE module repository (variant b.1 —",
654
+ "a `git::…?ref=<pinned-sha>` source, candidate `kind: git`), fetched READ-ONLY via the",
655
+ "supplied module-fetch credential. ⚠️ NEVER open a write PR against that module repo and",
656
+ "NEVER edit its vendored code — fix any module-internal concern at the CALL SITE via",
657
+ "inputs, or flag `fix needed in module repo <repo>`. If no org module matches, report and stop.",
658
+ ].join("\n"),
659
+ author: [
660
+ "AUTHOR a new module by MECHANICAL extraction (variant b.3): move the cluster's existing",
661
+ "resource blocks verbatim into `./modules/<name>/`, parameterise each hardcoded value into",
662
+ "a `variable` with a SECURE default (or no default), expose the needed `output`s, and",
663
+ "replace the root resources with a `module` call passing the original values. Do not adopt",
664
+ "an external module; the equivalence check is the guardrail that proves you changed nothing.",
665
+ ].join("\n"),
666
+ "third-party": [
667
+ "Adopt a THIRD-PARTY / registry module (variant b.4). This ALTERS behaviour: equivalence",
668
+ "is FALSE and must NEVER be claimed. Follow the b.4 path — scan with external-module",
669
+ "resolution ON (no finding-laundering), remediate module-sourced findings via INPUTS",
670
+ "(`needs-fork` where no knob exists), pin the EXACT version (provenance), and ship",
671
+ "`proposed — unproven` by default (banner + label), or a reviewed plan delta with opt-in creds.",
672
+ ].join("\n"),
673
+ };
674
+
675
+ /**
676
+ * Append the engine-owned module-source directive to a Refactor run's task when
677
+ * the operator pinned a `refactor_source` variant (the Cloud refactor picker, or
678
+ * the `refactor_source` action input). The task text stays engine-owned — only
679
+ * the enum comes from the trusted dispatch/input — so this narrows WHICH source
680
+ * the agent may resolve to without letting a customer author the prompt (the same
681
+ * boundary as {@link withConcernFocus}). A no-op when the variant is unset (the
682
+ * vetted prompt's own risk-ordered guidance applies).
683
+ */
684
+ export function withRefactorSource(
685
+ prompt: string,
686
+ source: RefactorSource | undefined,
687
+ ): string {
688
+ if (!source) return prompt;
689
+ return [
690
+ prompt,
691
+ "",
692
+ `PINNED MODULE SOURCE — this run was dispatched with refactor_source: ${source}.`,
693
+ REFACTOR_SOURCE_DIRECTIVES[source],
694
+ ].join("\n");
695
+ }
696
+
697
+ /**
698
+ * The languages a run's user-facing PROSE can be written in. English is the
699
+ * source/default; the others are the L1 marketing languages (see
700
+ * `docs/I18N-AND-MULTI-REGION-PLAN.md` A.5). This governs LLM-authored
701
+ * explanations only — never the Terraform, the identifiers, or the evidence.
702
+ */
703
+ export type UserFacingLanguage = "en" | "de" | "es" | "pl" | "ro";
704
+
705
+ /** English display name of each language, used in the vetted directive. */
706
+ const LANGUAGE_NAMES: Readonly<Record<UserFacingLanguage, string>> = {
707
+ en: "English",
708
+ de: "German",
709
+ es: "Spanish",
710
+ pl: "Polish",
711
+ ro: "Romanian",
712
+ };
713
+
714
+ /** Friendly aliases the control plane / a workflow may send, mapped to the code.
715
+ * Keyed lowercase; an `en-*`/`de-*` regional tag collapses to its base language. */
716
+ const LANGUAGE_ALIASES: Readonly<Record<string, UserFacingLanguage>> = {
717
+ english: "en",
718
+ german: "de",
719
+ deutsch: "de",
720
+ spanish: "es",
721
+ espanol: "es",
722
+ castellano: "es",
723
+ polish: "pl",
724
+ polski: "pl",
725
+ romanian: "ro",
726
+ romana: "ro",
727
+ };
728
+
729
+ /** parse the `language:` input into a {@link UserFacingLanguage}. Unset → "en".
730
+ * Accepts a code (`de`), a regional tag (`de-DE`), or a name (`German`). An
731
+ * unknown value warns and falls back to "en" rather than hard-failing the run —
732
+ * mirroring how an unknown `mode`/`model`/`refactor_source` degrades. */
733
+ export function parseLanguage(raw: string | undefined): UserFacingLanguage {
734
+ const v = raw?.trim().toLowerCase();
735
+ if (!v) return "en";
736
+ const base = v.split(/[-_]/)[0] ?? v;
737
+ if (base in LANGUAGE_NAMES) return base as UserFacingLanguage;
738
+ const aliased = LANGUAGE_ALIASES[v] ?? LANGUAGE_ALIASES[base];
739
+ if (aliased) return aliased;
740
+ log.warning(
741
+ `» unknown language "${raw}" — writing user-facing prose in English (valid: ${Object.keys(LANGUAGE_NAMES).join(", ")})`,
742
+ );
743
+ return "en";
744
+ }
745
+
746
+ /**
747
+ * Append the engine-owned "write user-facing prose in <language>" directive to a
748
+ * run's task. Structured enum input, never workflow prose — the engine still owns
749
+ * the prompt (the security boundary, same as {@link withConcernFocus} /
750
+ * {@link withRefactorSource}). A no-op for English (the default), so existing runs
751
+ * are byte-for-byte unchanged. The directive is glossary-aware: it localizes the
752
+ * EXPLANATIONS a human reads and holds every identifier, code block, path, rule
753
+ * id, control name, and the product terms verbatim in English — so a translated
754
+ * PR body never corrupts the Terraform it describes.
755
+ */
756
+ export function withLanguageDirective(
757
+ prompt: string,
758
+ language: UserFacingLanguage,
759
+ ): string {
760
+ if (language === "en") return prompt;
761
+ const name = LANGUAGE_NAMES[language];
762
+ return [
763
+ prompt,
764
+ "",
765
+ `USER-FACING LANGUAGE — write every explanation meant for a human reader (PR titles and`,
766
+ `bodies, review-comment text, issue / dashboard notes, and the run summary) in ${name}.`,
767
+ "This governs PROSE ONLY. Keep verbatim, in English: all Terraform identifiers and resource",
768
+ "addresses, every code block / diff / file path / CLI command / log excerpt, scanner rule",
769
+ "ids, severity tokens, and compliance control and framework names. Never translate the",
770
+ `product names ${PRODUCT_NAME}, Remediate, Assess, or Refactor. Where ${name} has no established`,
771
+ "technical term, keep the English one rather than inventing a translation.",
772
+ ].join("\n");
773
+ }
774
+
775
+ const SEVERITY_VALUES = new Set(["critical", "high", "medium", "low", "info"]);
776
+
777
+ /** parse the severity_threshold input; undefined when unset or not a valid level. */
778
+ function parseSeverityThreshold(raw: string | undefined): string | undefined {
779
+ if (!raw) return undefined;
780
+ const v = raw.trim().toLowerCase();
781
+ return SEVERITY_VALUES.has(v) ? v : undefined;
782
+ }
783
+
784
+ /** parse max_prs; a positive integer, else undefined (downstream default is 1). */
785
+ function parseMaxPrs(raw: string | undefined): number | undefined {
786
+ if (!raw) return undefined;
787
+ const n = Number.parseInt(raw.trim(), 10);
788
+ return Number.isInteger(n) && n > 0 ? n : undefined;
789
+ }
790
+
791
+ /** parse a PR-flood limit (pr_concurrent_limit / pr_hourly_limit): a non-negative
792
+ * integer where 0 means "unlimited". undefined when unset or invalid, so the
793
+ * downstream default applies. */
794
+ function parsePrLimit(raw: string | undefined): number | undefined {
795
+ if (!raw) return undefined;
796
+ const n = Number.parseInt(raw.trim(), 10);
797
+ return Number.isInteger(n) && n >= 0 ? n : undefined;
798
+ }
799
+
800
+ /** parse cost_increase_block_usd; a positive number of dollars/month, else
801
+ * undefined (no cost escalation). */
802
+ function parseCostIncreaseBlock(raw: string | undefined): number | undefined {
803
+ if (!raw) return undefined;
804
+ const n = Number.parseFloat(raw.trim());
805
+ return Number.isFinite(n) && n > 0 ? n : undefined;
806
+ }
807
+
808
+ /** parse a comma- or newline-separated glob list (allowed_paths /
809
+ * protected_paths); undefined when unset or empty after trimming. Newlines are
810
+ * accepted so a YAML block scalar in the workflow — and a list in
811
+ * `.infraweaver.yml` (joined with newlines) — parse the same as a comma list. */
812
+ function parseGlobList(raw: string | undefined): string[] | undefined {
813
+ if (!raw) return undefined;
814
+ const globs = raw
815
+ .split(/[\n,]/)
816
+ .map((g) => g.trim())
817
+ .filter(Boolean);
818
+ return globs.length > 0 ? globs : undefined;
819
+ }
820
+
821
+ /** parse a boolean-ish action input ("true"/"1"/"yes" → true). undefined/unset
822
+ * and any other value → false. */
823
+ function parseBooleanInput(raw: string | undefined): boolean {
824
+ if (!raw) return false;
825
+ return ["true", "1", "yes", "on"].includes(raw.trim().toLowerCase());
826
+ }
827
+
828
+ /** parse the base_branch override; trims and strips a leading `refs/heads/`,
829
+ * undefined when unset (downstream resolves the run-start branch / default). */
830
+ export function parseBaseBranch(raw: string | undefined): string | undefined {
831
+ const v = raw?.trim().replace(/^refs\/heads\//, "");
832
+ return v || undefined;
833
+ }
834
+
835
+ /** parse the comma-separated allow_replace list — resource addresses (or globs,
836
+ * or `*`/`all`) permitted to be destroyed/replaced; undefined when unset. */
837
+ export function parseAllowReplace(
838
+ raw: string | undefined,
839
+ ): string[] | undefined {
840
+ if (!raw) return undefined;
841
+ const entries = raw
842
+ .split(",")
843
+ .map((a) => a.trim())
844
+ .filter(Boolean);
845
+ return entries.length > 0 ? entries : undefined;
846
+ }
847
+
848
+ /**
849
+ * true when `actor` is Infraweaver's own GitHub identity — the bot login
850
+ * (`infraweaver[bot]` / `infraweaverdev[bot]`) or the bare account. used to skip
851
+ * self-triggered events and to recognise Infraweaver-originated review threads
852
+ * (the never-approve-with-outstanding-findings gate). matches with or without
853
+ * the `[bot]` suffix since GraphQL and REST report the login differently.
854
+ */
855
+ export const isInfraweaver = (actor: string | null | undefined): boolean => {
856
+ actor = actor?.replace("[bot]", "");
857
+ return !!actor && (actor === PRODUCT_SLUG || actor === `${PRODUCT_SLUG}dev`);
858
+ };
859
+
860
+ /** Union the locally-configured required controls (action input / `.infraweaver.yml`)
861
+ * with the org's Cloud "required controls" policy (delivered over run-context).
862
+ * Either source can mandate a control; both are the org's own settings. Undefined
863
+ * when both are empty, to preserve the "no policy" shape the gate treats as a no-op. */
864
+ function mergeRequiredControls(
865
+ local: string[] | undefined,
866
+ org: string[],
867
+ ): string[] | undefined {
868
+ const merged = [...new Set([...(local ?? []), ...org])];
869
+ return merged.length ? merged : undefined;
870
+ }
871
+
872
+ export function resolvePayload(
873
+ resolvedPromptInput: ResolvedPromptInput,
874
+ repoSettings: RepoSettings,
875
+ /** The org's mandatory pack-control ids from the Cloud run-context (org-scoped).
876
+ * Merged with the local `required_controls` so the dashboard policy gates CI. */
877
+ orgRequiredControls: string[] = [],
878
+ /** Cloud-managed module credentials from the run-context (the dashboard Refactor
879
+ * dialog). A FALLBACK only — an explicit `module_fetch_token` / `registry_token`
880
+ * input always wins. undefined when none are stored for the repo. */
881
+ managedCredentials?: { moduleFetchToken?: string; registryToken?: string },
882
+ ) {
883
+ const [rawPrompt, jsonPayload] =
884
+ typeof resolvedPromptInput !== "string"
885
+ ? [resolvedPromptInput.prompt, resolvedPromptInput]
886
+ : [resolvedPromptInput, undefined];
887
+
888
+ const inputs = resolveNonPromptInputs();
889
+
890
+ // Deterministic mode pin for CI (action input only). Resolved here so the prompt
891
+ // can be locked to the mode's vetted standard below.
892
+ const mode = parseMode(inputs.mode);
893
+
894
+ // A `@infraweaver fix …` command parsed from the (raw) prompt — parse from rawPrompt
895
+ // so the comment-driven flow survives the vetted-prompt override below.
896
+ const remediationCommand = parseRemediationCommand(rawPrompt);
897
+
898
+ // For a pinned vetted mode the engine owns the prompt (free-form workflow prompt
899
+ // ignored); a recognised fix-command and the trusted JSON dispatch payload pass
900
+ // through. Otherwise the workflow prompt is used and required.
901
+ const basePrompt = resolveEffectivePrompt(
902
+ rawPrompt,
903
+ jsonPayload !== undefined,
904
+ mode,
905
+ remediationCommand !== null,
906
+ );
907
+ // A dashboard "Remediate" dispatch targets specific findings via the structured
908
+ // `concern_ids` input (NOT prose). Narrow the engine-owned Remediate task to
909
+ // those concerns; the comment-command path carries its own scope, so skip it there.
910
+ const concernIds = parseConcernIds(inputs.concern_ids);
911
+ // a pinned module-source variant (the Cloud refactor picker / `refactor_source`
912
+ // input) narrows the engine-owned Refactor task to one b-variant — structured
913
+ // scope, not prose. Applies to Refactor and the combined Remediate+Refactor mode.
914
+ const refactorSource = parseRefactorSource(inputs.refactor_source);
915
+ const wantsConcernFocus =
916
+ (mode === "Remediate" || mode === "RemediateAndRefactor") &&
917
+ remediationCommand === null;
918
+ const wantsRefactorSource =
919
+ mode === "Refactor" || mode === "RemediateAndRefactor";
920
+ let prompt = basePrompt;
921
+ if (wantsConcernFocus) prompt = withConcernFocus(prompt, concernIds);
922
+ if (wantsRefactorSource) prompt = withRefactorSource(prompt, refactorSource);
923
+
924
+ // Mask credential inputs in logs the moment we read them — GitHub only auto-masks
925
+ // registered secrets, not arbitrary input values, so a token passed inline (or
926
+ // echoed by a debug line) would otherwise leak. Covers the Cloud org token and the
927
+ // private-module credentials (git-transport + private-registry).
928
+ for (const secret of [
929
+ inputs.api_token,
930
+ inputs.module_fetch_token,
931
+ inputs.module_fetch_ssh_key,
932
+ inputs.registry_token,
933
+ ]) {
934
+ if (secret) core.setSecret(secret);
935
+ }
936
+
937
+ // resolve event - use type guard for jsonPayload.event, fallback to unknown trigger
938
+ const rawEvent = jsonPayload?.event;
939
+ const event: PayloadEvent = isPayloadEvent(rawEvent)
940
+ ? rawEvent
941
+ : { trigger: "unknown" };
942
+
943
+ const model =
944
+ jsonPayload?.model ?? inputs.model ?? repoSettings.model ?? undefined;
945
+
946
+ // determine shell permission - strictest setting wins
947
+ // precedence: disabled > restricted > enabled
948
+ // non-collaborators always get at least "restricted"
949
+ const isNonCollaborator = !isCollaborator(event);
950
+ const repoShell = repoSettings.shell ?? "restricted";
951
+ const inputShell = inputs.shell;
952
+
953
+ // resolve shell: start with repo setting, then apply restrictions
954
+ let resolvedShell = repoShell;
955
+
956
+ // input can only make it stricter (disabled > restricted > enabled)
957
+ if (inputShell === "disabled") {
958
+ resolvedShell = "disabled";
959
+ } else if (inputShell === "restricted" && resolvedShell === "enabled") {
960
+ resolvedShell = "restricted";
961
+ }
962
+
963
+ // non-collaborators get at least "restricted" (can't have "enabled")
964
+ if (isNonCollaborator && resolvedShell === "enabled") {
965
+ resolvedShell = "restricted";
966
+ }
967
+
968
+ const cwd = resolveCwd(inputs.cwd);
969
+ // repo-committed `.infraweaver.yml` policy, layered UNDER the
970
+ // action inputs: a workflow input always wins; the file fills the gaps. Read
971
+ // from the checked-out repo root; a missing file is a silent no-op. See
972
+ // utils/infraweaverConfig.ts for the trust boundary + supported keys.
973
+ //
974
+ // On a PULL-REQUEST run the checked-out ref is the PR's own head, so the
975
+ // GATE-relevant keys are re-resolved from the base ref: a PR must not be able
976
+ // to widen `allowed_paths`, empty `required_controls`, raise
977
+ // `severity_threshold` or disable a scanner in the very run that reviews it.
978
+ // Presentational keys still come from the head. Degrades honestly (keeps the
979
+ // head value + logs) when the base ref isn't fetched. See utils/baseRefConfig.ts.
980
+ const fileConfig = applyBaseRefConfigPolicy({
981
+ cwd,
982
+ headValues: loadInfraweaverConfig(cwd),
983
+ baseRef: process.env.GITHUB_BASE_REF?.trim() || undefined,
984
+ });
985
+
986
+ // Language for user-facing prose (input wins, then `.infraweaver.yml`). Appended
987
+ // as an engine-owned directive to the run's task (all modes + free-form), since
988
+ // it only governs the language of the explanations the run writes back. A no-op
989
+ // for the default English, so existing runs are byte-for-byte unchanged.
990
+ const language = parseLanguage(inputs.language ?? fileConfig.language);
991
+ prompt = withLanguageDirective(prompt, language);
992
+
993
+ // build payload - precedence: inputs > .infraweaver.yml > repoSettings > fallbacks
994
+ // note: modes are NOT in payload - they come from repoSettings in main()
995
+ return {
996
+ "~infraweaver": true as const,
997
+ version: jsonPayload?.version ?? packageJson.version,
998
+ model,
999
+ // deterministic mode pin for CI (action input only — not accepted from the
1000
+ // JSON payload, which is the internal dispatch surface). undefined → the
1001
+ // agent chooses via select_mode as before.
1002
+ mode,
1003
+ prompt,
1004
+ triggerer:
1005
+ jsonPayload?.triggerer ??
1006
+ // it's not a common use case but GITHUB_ACTOR can be a user when the workflow is manually triggered by a user through GitHub Actions UI
1007
+ (!isInfraweaver(process.env.GITHUB_ACTOR)
1008
+ ? process.env.GITHUB_ACTOR
1009
+ : undefined),
1010
+ eventInstructions: jsonPayload?.eventInstructions,
1011
+ previousRunsNote: jsonPayload?.previousRunsNote,
1012
+ event,
1013
+ timeout: inputs.timeout ?? jsonPayload?.timeout,
1014
+ cwd,
1015
+ progressComment: jsonPayload?.progressComment,
1016
+ generateSummary: jsonPayload?.generateSummary,
1017
+
1018
+ // permissions: inputs > repoSettings > fallbacks
1019
+ push: inputs.push ?? repoSettings.push ?? "restricted",
1020
+ shell: resolvedShell,
1021
+
1022
+ // Terraform remediation config — consumed by mcp/terraform.ts + the
1023
+ // Remediate mode. Defaults are applied at the consumer, not here, so
1024
+ // "unset" stays distinguishable from an explicit value.
1025
+ // Terraform policy fields accept a `.infraweaver.yml` fallback (input wins).
1026
+ scanScope: parseScanScope(inputs.scan_scope ?? fileConfig.scan_scope),
1027
+ // multi-language Assess scope for the READ-ONLY surfaces only. Raw string
1028
+ // ("auto" | "terraform" | comma list); resolved against the tree by
1029
+ // resolveLanguageScope at scan time. Input wins, then `.infraweaver.yml`.
1030
+ // undefined → "auto" at the consumer. The fix loop ignores this (Terraform-first).
1031
+ iacLanguages:
1032
+ (inputs.iac_languages ?? fileConfig.iac_languages)?.trim() || undefined,
1033
+ severityThreshold: parseSeverityThreshold(
1034
+ inputs.severity_threshold ?? fileConfig.severity_threshold,
1035
+ ),
1036
+ // repo-wide default remediation grouping (input wins, then the
1037
+ // `.infraweaver.yml`/`.json` file). A per-scan `group_by` tool arg still wins
1038
+ // over this. undefined → the consumer default ("file").
1039
+ grouping: parseGrouping(inputs.grouping ?? fileConfig.grouping),
1040
+ // specific concern ids a dashboard "Remediate" dispatch targets — narrows the
1041
+ // run to the group(s) containing them (folded into the task by withConcernFocus
1042
+ // above). Action-input only (not the untrusted repo file). undefined → the
1043
+ // engine acts on the highest-severity group as usual.
1044
+ concernIds,
1045
+ maxPrs: parseMaxPrs(inputs.max_prs),
1046
+ // LLM10 per-run token ceiling. Action-input only, like the PR-flood guards
1047
+ // below and TC-0.2's `tools_network`: a safety limit a pull request could
1048
+ // raise from `.infraweaver.yml` in the run that reviews it is not a limit.
1049
+ maxRunTokens: parseRunTokenQuota(inputs.max_run_tokens),
1050
+ // Cross-run PR-flood guards (action-input only, like max_prs — deliberately
1051
+ // NOT read from the untrusted repo file, so a malicious PR can't loosen them).
1052
+ // prConcurrentLimit caps simultaneously-open remediation PRs (consumer default
1053
+ // 10); prHourlyLimit caps creation velocity (consumer default 0 = off). 0 =
1054
+ // unlimited for both. See guardrails.assertUnderOpenPrCaps.
1055
+ prConcurrentLimit: parsePrLimit(inputs.pr_concurrent_limit),
1056
+ prHourlyLimit: parsePrLimit(inputs.pr_hourly_limit),
1057
+ allowedPaths: parseGlobList(
1058
+ inputs.allowed_paths ?? fileConfig.allowed_paths,
1059
+ ),
1060
+ // globs the fixer must never auto-modify (inverse of allowed_paths).
1061
+ protectedPaths: parseGlobList(
1062
+ inputs.protected_paths ?? fileConfig.protected_paths,
1063
+ ),
1064
+ // minimum severity at which a security concern escalates to a human.
1065
+ autonomyThreshold: parseSeverityThreshold(
1066
+ inputs.autonomy_threshold ?? fileConfig.autonomy_threshold,
1067
+ ),
1068
+ // compliance controls that must have NO open finding — a finding mapping to
1069
+ // one fails the gate regardless of severity (the "required controls" policy:
1070
+ // binds the crosswalk to the gate). Matched by framework / control /
1071
+ // `framework|control` / title / theme / Cloud pack id. The local config (input
1072
+ // > `.infraweaver.yml`) is UNIONED with the org's Cloud policy (run-context).
1073
+ requiredControls: mergeRequiredControls(
1074
+ parseGlobList(inputs.required_controls ?? fileConfig.required_controls),
1075
+ orgRequiredControls,
1076
+ ),
1077
+ // opt in to the external gitleaks engine on top of the built-in
1078
+ // secret scanner (best-effort; degrades to built-in only when absent).
1079
+ gitleaks: parseBooleanInput(inputs.gitleaks),
1080
+ // monthly $ increase at/above which a fix is escalated to a
1081
+ // human (needs-human). undefined disables cost escalation.
1082
+ costIncreaseBlockUsd: parseCostIncreaseBlock(
1083
+ inputs.cost_increase_block_usd,
1084
+ ),
1085
+ // operator-approved modules a fix/generation
1086
+ // should prefer; raw string, structured by `parseModuleCatalogue` in the
1087
+ // `list_modules` tool.
1088
+ moduleCatalogue: inputs.module_catalogue ?? fileConfig.module_catalogue,
1089
+ // opt in to scaffolding a Go Terratest smoke test + a native
1090
+ // `*.tftest.hcl` (both plan the module directly) when generating a reusable
1091
+ // module; also widens the push allow-list so the test files can be written.
1092
+ terratest: parseBooleanInput(inputs.terratest),
1093
+ // opt in to evidence-backed module README generation on the refactor
1094
+ // flow (terraform_module_docs); also widens the push allow-list to the
1095
+ // DOCS_ALLOWED_PATHS carve-out (module READMEs + docs/infraweaver only).
1096
+ docs: parseBooleanInput(inputs.docs),
1097
+ // opt in to HashiCorp's terraform-mcp-server as a second MCP server
1098
+ // for the agent (live Terraform Registry knowledge: current module versions
1099
+ // and provider argument shapes). Requires docker on the runner; degrades
1100
+ // green with a note when absent. See utils/terraformMcp.ts.
1101
+ terraformMcp: parseBooleanInput(inputs.terraform_mcp),
1102
+ // opt in to the OpenTofu Registry MCP server (registry knowledge for an
1103
+ // OpenTofu repo) — the permissively-licensed sibling to terraform_mcp,
1104
+ // reached at OpenTofu's hosted endpoint (no docker, no credentials). See
1105
+ // utils/opentofuMcp.ts.
1106
+ opentofuMcp: parseBooleanInput(inputs.opentofu_mcp),
1107
+ // language for user-facing prose the run authors (PR bodies, review text, run
1108
+ // summary, dashboard note). Folded into the engine-owned task by
1109
+ // withLanguageDirective above; also threaded to the fixed-scaffolding renderers
1110
+ // (buildDashboardBody) so section headers match. Input wins, then `.infraweaver.yml`.
1111
+ // "en" default → no behaviour change. Never governs the Terraform itself.
1112
+ language,
1113
+ // §5.12 opt in to reusable proven-fix memory: capture the transformation of
1114
+ // each finding-type that goes ✗→✓, and offer it as a proven prior when the
1115
+ // same finding-type recurs (still re-scanning ✗→✓ per target, never
1116
+ // blind-applying). The fleet library rides the Cloud report/run-context;
1117
+ // a local `.infraweaver.fix-memory.json` is also read when present.
1118
+ fixMemory: parseBooleanInput(inputs.fix_memory),
1119
+ // the unified tool-selection allow/deny list. Parsed once here into a
1120
+ // ToolDirective; resolveToolSelection(payload) combines it with the dedicated
1121
+ // booleans + the licence gate so every consumer agrees on which tools run.
1122
+ toolsEnabled: parseToolSelection(
1123
+ inputs.tools_enabled ?? fileConfig.tools_enabled,
1124
+ ),
1125
+ // per-scanner egress policy (supply-chain 0.2). Deliberately INPUT-ONLY,
1126
+ // never `.infraweaver.yml`: a security posture must belong to the workflow
1127
+ // that dispatches the run, not to a file the scanned ref can edit.
1128
+ toolsNetwork: inputs.tools_network
1129
+ ? parseToolsNetwork(inputs.tools_network)
1130
+ : undefined,
1131
+ // scoped credential to FETCH private cross-repo `git::` modules at
1132
+ // terraform init/plan (the HepCare shape). Injected per-subprocess via
1133
+ // GIT_CONFIG_* by resolveModuleFetchEnv; never the action's own git. The
1134
+ // action input wins; a Cloud-managed credential (from the run-context, set in
1135
+ // the dashboard Refactor dialog) is the fallback when no input was supplied.
1136
+ moduleFetchToken:
1137
+ inputs.module_fetch_token ?? managedCredentials?.moduleFetchToken,
1138
+ // private-key counterpart for `git::ssh://…` / deploy-key module sources;
1139
+ // written 0600 into the run tmpdir and exposed via a scoped GIT_SSH_COMMAND
1140
+ // (resolveModuleFetchEnv), never the home dir or a committed path.
1141
+ moduleFetchSshKey: inputs.module_fetch_ssh_key,
1142
+ // scoped bearer token to fetch a private Terraform REGISTRY module (the
1143
+ // refactor b.4 shape) at terraform init/plan. Re-admitted per-subprocess as
1144
+ // `TF_TOKEN_<host>` by resolveRegistryTokenEnv (utils/registryAuth.ts); never
1145
+ // persisted. `registryHost` names the host(s) it authorises (default: HCP).
1146
+ // Action input wins; the Cloud-managed credential is the fallback.
1147
+ registryToken: inputs.registry_token ?? managedCredentials?.registryToken,
1148
+ registryHost: inputs.registry_host,
1149
+ // which module-source variant a Refactor (or combined Remediate+Refactor) run
1150
+ // is pinned to (auto | same-repo | org-repo | author | third-party). Folded
1151
+ // into the engine-owned task by withRefactorSource above; also read by the
1152
+ // Refactor mode for the b.4 consent gate. undefined → auto. Action-input/
1153
+ // trusted-dispatch only (not the untrusted repo file).
1154
+ refactorSource,
1155
+ // a `@infraweaver fix …` command parsed from the triggering comment
1156
+ // body (the raw prompt), scoping the run to a specific concern/severity/file.
1157
+ // null when the prompt isn't a recognised command.
1158
+ remediationCommand,
1159
+ // explicit base-branch override; when unset the effective base is resolved
1160
+ // at PR time (run-start branch → repo default) — see resolveBaseBranch.
1161
+ baseBranch: parseBaseBranch(inputs.base_branch),
1162
+ // resource addresses the operator allows the remediation to destroy/replace
1163
+ // — consumed by the destroy-block guardrail (mcp/guardrails.ts). Unset means
1164
+ // no destructive change to a stateful resource is permitted.
1165
+ allowReplace: parseAllowReplace(inputs.allow_replace),
1166
+ // org-specific review policy + FP precedents from the workflow file
1167
+ // (owner-controlled, same trust surface as repo settings). merged into the
1168
+ // Review mode instructions in main() via mergeReviewModeInstructions.
1169
+ reviewInstructions: inputs.review_instructions,
1170
+ fpFilteringInstructions: inputs.fp_filtering_instructions,
1171
+ // org-specific remediation/refactor policy — the same composable pattern,
1172
+ // for the pinned Terraform-write modes: appended under a fixed heading via
1173
+ // mergeRemediationModeInstructions in main(); NEVER replaces the vetted
1174
+ // standard and does not reopen the free-form-prompt path.
1175
+ remediationInstructions: inputs.remediation_instructions,
1176
+ refactorInstructions: inputs.refactor_instructions,
1177
+ // Infraweaver Cloud org token — read by finalizeSuccessRun to report this run
1178
+ // to Cloud (utils/cloudReport.ts). Already masked above.
1179
+ cloudApiToken: inputs.api_token,
1180
+ };
1181
+ }
1182
+
1183
+ export type ResolvedPayload = ReturnType<typeof resolvePayload>;
1184
+
1185
+ /**
1186
+ * Parse and validate the optional `output_schema` action input. Returns the
1187
+ * parsed object when present, or `undefined` when absent. Throws on invalid
1188
+ * JSON or non-object payloads — these are workflow-author errors that should
1189
+ * surface immediately, not silently degrade to "no schema".
1190
+ */
1191
+ export function resolveOutputSchema(): Record<string, unknown> | undefined {
1192
+ const raw = core.getInput("output_schema");
1193
+ if (!raw) return undefined;
1194
+
1195
+ let parsed: unknown;
1196
+ try {
1197
+ parsed = JSON.parse(raw);
1198
+ } catch {
1199
+ throw new Error(`invalid output_schema: not valid JSON`);
1200
+ }
1201
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1202
+ throw new Error(`invalid output_schema: must be a JSON object`);
1203
+ }
1204
+ log.info("» structured output schema provided — output will be required");
1205
+ return parsed as Record<string, unknown>;
1206
+ }