terramend 0.2.0 → 0.2.1

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 (60) hide show
  1. package/dist/agents/claudePretoolGate.d.ts +2 -2
  2. package/dist/cli.mjs +16554 -8100
  3. package/dist/index.js +13484 -5037
  4. package/dist/internal.js +75 -11
  5. package/dist/mcp/assess.d.ts +86 -0
  6. package/dist/mcp/changeSummary.d.ts +50 -0
  7. package/dist/mcp/crosswalk.d.ts +5 -0
  8. package/dist/mcp/localContext.d.ts +1 -1
  9. package/dist/mcp/terraform/evidence.d.ts +99 -0
  10. package/dist/mcp/terraform/scanners.d.ts +38 -3
  11. package/dist/mcp/terraform/types.d.ts +16 -0
  12. package/dist/mcp/terraform/verification.d.ts +74 -0
  13. package/dist/mcp/terraform.d.ts +4 -0
  14. package/dist/modes.d.ts +1 -1
  15. package/dist/toolState.d.ts +1 -0
  16. package/dist/utils/moduleFetch.d.ts +42 -0
  17. package/dist/utils/payload.d.ts +4 -0
  18. package/dist/utils/remediationCommand.d.ts +3 -0
  19. package/dist/utils/terraformMcp.d.ts +2 -2
  20. package/dist/utils/terramendConfig.d.ts +51 -0
  21. package/dist/utils/toolLicensing.d.ts +56 -0
  22. package/dist/utils/toolSelection.d.ts +72 -0
  23. package/package.json +9 -8
  24. package/src/agents/claudePretoolGate.ts +3 -3
  25. package/src/mcp/assess.test.ts +135 -0
  26. package/src/mcp/assess.ts +341 -0
  27. package/src/mcp/changeSummary.test.ts +94 -0
  28. package/src/mcp/changeSummary.ts +145 -0
  29. package/src/mcp/crosswalk.ts +15 -1
  30. package/src/mcp/guardrails.ts +11 -6
  31. package/src/mcp/localContext.ts +7 -0
  32. package/src/mcp/localServer.test.ts +2 -0
  33. package/src/mcp/localServer.ts +14 -0
  34. package/src/mcp/server.ts +6 -0
  35. package/src/mcp/terraform/evidence.test.ts +72 -0
  36. package/src/mcp/terraform/evidence.ts +187 -0
  37. package/src/mcp/terraform/scanners.ts +86 -9
  38. package/src/mcp/terraform/tools.test.ts +96 -1
  39. package/src/mcp/terraform/tools.ts +115 -32
  40. package/src/mcp/terraform/types.ts +24 -0
  41. package/src/mcp/terraform/verification.test.ts +85 -0
  42. package/src/mcp/terraform/verification.ts +133 -0
  43. package/src/mcp/terraform.test.ts +108 -0
  44. package/src/mcp/terraform.ts +4 -0
  45. package/src/modes.test.ts +9 -1
  46. package/src/modes.ts +81 -11
  47. package/src/toolState.ts +6 -0
  48. package/src/utils/moduleFetch.test.ts +68 -0
  49. package/src/utils/moduleFetch.ts +86 -0
  50. package/src/utils/payload.test.ts +66 -1
  51. package/src/utils/payload.ts +39 -11
  52. package/src/utils/remediationCommand.test.ts +32 -0
  53. package/src/utils/remediationCommand.ts +11 -0
  54. package/src/utils/terraformMcp.ts +6 -5
  55. package/src/utils/terramendConfig.test.ts +98 -0
  56. package/src/utils/terramendConfig.ts +143 -0
  57. package/src/utils/toolLicensing.test.ts +54 -0
  58. package/src/utils/toolLicensing.ts +103 -0
  59. package/src/utils/toolSelection.test.ts +140 -0
  60. package/src/utils/toolSelection.ts +231 -0
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Unified tool-selection config (§1.5 "unified tool-selection config") + the
3
+ * non-permissive-tool confirmation gate (§1.5, built on [[toolLicensing]]).
4
+ *
5
+ * One declarative `tools_enabled` list replaces the previous mix of per-flag
6
+ * inputs and silent presence-detection: an operator names the scanners/engines
7
+ * they want, prefixing `-` to turn one off. The same list is how a non-permissive
8
+ * tool (tflint, terraform-mcp-server) is opted into — naming it is the explicit,
9
+ * licence-aware acknowledgement the gate requires.
10
+ *
11
+ * Resolution precedence, per tool (highest first):
12
+ * 1. the required substrate (`terraform`) → ON (exempt; never disablable)
13
+ * 2. an explicit `-tool` in the list → OFF (always wins over the rest)
14
+ * 3. an explicit `tool` / `+tool` → ON (this is the licence opt-in)
15
+ * 4. `all` base → ON (operator accepts every tool)
16
+ * 5. `none` base → only the explicitly/flag-enabled
17
+ * 6. default base:
18
+ * - licence-gated tool → ON only if its dedicated flag is set, else OFF
19
+ * - flag-opt-in tool → ON only if its dedicated flag is set, else OFF
20
+ * - permissive tool → ON
21
+ *
22
+ * The dedicated booleans (`gitleaks`, `terratest`, `terraform_mcp`) still work
23
+ * and count as an opt-in, so existing workflows keep running unchanged. Pure.
24
+ */
25
+
26
+ import { isLicenseGated, isPermissive, TOOL_LICENSES, type ToolId } from "#app/utils/toolLicensing";
27
+
28
+ /** the parsed `tools_enabled` input: a base posture + per-tool overrides. */
29
+ export interface ToolDirective {
30
+ /** `all` (enable everything) / `none` (enable nothing) / undefined (defaults). */
31
+ base?: "all" | "none" | undefined;
32
+ /** explicit per-tool overrides: true = enable, false = disable. */
33
+ explicit: Map<ToolId, boolean>;
34
+ /** tokens that matched no known tool — surfaced as a warning, never fatal. */
35
+ unknown: string[];
36
+ }
37
+
38
+ /** the dedicated booleans that pre-date the unified list. */
39
+ export interface ToolSelectionFlags {
40
+ toolsEnabled?: ToolDirective | undefined;
41
+ gitleaks?: boolean;
42
+ terratest?: boolean;
43
+ terraformMcp?: boolean;
44
+ }
45
+
46
+ /**
47
+ * Permissive tools that still default OFF — not for licence reasons but because
48
+ * each is an extra/heavier engine (or writes extra files) the operator opts into
49
+ * via its dedicated input. Listing keeps their long-standing behaviour intact
50
+ * while the unified list can also enable/disable them.
51
+ */
52
+ const FLAG_OPT_IN: ReadonlySet<ToolId> = new Set<ToolId>(["gitleaks", "terratest"]);
53
+
54
+ /** map a token to a canonical tool id (case-insensitive; common spellings). */
55
+ const TOKEN_ALIASES: Readonly<Record<string, ToolId>> = {
56
+ terraform: "terraform",
57
+ tf: "terraform",
58
+ "terraform-fmt": "terraform",
59
+ "terraform-validate": "terraform",
60
+ tflint: "tflint",
61
+ trivy: "trivy",
62
+ checkov: "checkov",
63
+ infracost: "infracost",
64
+ cost: "infracost",
65
+ gitleaks: "gitleaks",
66
+ conftest: "conftest",
67
+ opa: "conftest",
68
+ policy: "conftest",
69
+ terratest: "terratest",
70
+ terraform_mcp: "terraform_mcp",
71
+ "terraform-mcp": "terraform_mcp",
72
+ "terraform-mcp-server": "terraform_mcp",
73
+ } as const;
74
+
75
+ /**
76
+ * Parse the `tools_enabled` input (comma- or newline-separated). Recognises the
77
+ * `all` / `none` bases and `tool` / `+tool` / `-tool` (also `!tool`) overrides.
78
+ * Returns undefined for an empty input so "unset" stays distinguishable from an
79
+ * explicit list (the consumer then applies the licence-aware defaults).
80
+ */
81
+ export function parseToolSelection(raw: string | undefined): ToolDirective | undefined {
82
+ if (!raw?.trim()) return undefined;
83
+ const explicit = new Map<ToolId, boolean>();
84
+ const unknown: string[] = [];
85
+ let base: "all" | "none" | undefined;
86
+
87
+ for (const token of raw.split(/[\n,]/)) {
88
+ const tok = token.trim();
89
+ if (!tok) continue;
90
+ const lower = tok.toLowerCase();
91
+ if (lower === "all" || lower === "*") {
92
+ base = "all";
93
+ continue;
94
+ }
95
+ if (lower === "none") {
96
+ base = "none";
97
+ continue;
98
+ }
99
+ let enable = true;
100
+ let name = lower;
101
+ if (name.startsWith("-") || name.startsWith("!")) {
102
+ enable = false;
103
+ name = name.slice(1).trim();
104
+ } else if (name.startsWith("+")) {
105
+ name = name.slice(1).trim();
106
+ }
107
+ const id = TOKEN_ALIASES[name];
108
+ if (!id) {
109
+ unknown.push(tok);
110
+ continue;
111
+ }
112
+ explicit.set(id, enable);
113
+ }
114
+ return { base, explicit, unknown };
115
+ }
116
+
117
+ function flagFor(id: ToolId, flags: ToolSelectionFlags): boolean {
118
+ switch (id) {
119
+ case "gitleaks":
120
+ return !!flags.gitleaks;
121
+ case "terratest":
122
+ return !!flags.terratest;
123
+ case "terraform_mcp":
124
+ return !!flags.terraformMcp;
125
+ default:
126
+ return false;
127
+ }
128
+ }
129
+
130
+ interface Verdict {
131
+ on: boolean;
132
+ /** why a tool is OFF (for reporting); undefined when ON. */
133
+ reason?: string;
134
+ }
135
+
136
+ function decide(id: ToolId, flags: ToolSelectionFlags): Verdict {
137
+ const dir = flags.toolsEnabled;
138
+ const explicit = dir?.explicit.get(id);
139
+
140
+ // 1. the required substrate is the engine's reason to exist — never gated,
141
+ // never disablable (a Terraform fixer always has Terraform).
142
+ if (TOOL_LICENSES[id].required) return { on: true };
143
+ // 2. an explicit disable always wins.
144
+ if (explicit === false) return { on: false, reason: "disabled via tools_enabled" };
145
+ // 3. an explicit enable is the licence-aware opt-in.
146
+ if (explicit === true) return { on: true };
147
+ // 4 / 5. an `all` / `none` base.
148
+ if (dir?.base === "all") return { on: true };
149
+ if (dir?.base === "none") {
150
+ return flagFor(id, flags)
151
+ ? { on: true }
152
+ : { on: false, reason: "not in the tools_enabled allow-list" };
153
+ }
154
+ // 6. licence-aware defaults.
155
+ if (isLicenseGated(id)) {
156
+ if (flagFor(id, flags)) return { on: true };
157
+ const { license, name } = TOOL_LICENSES[id];
158
+ return {
159
+ on: false,
160
+ reason: `licence-gated (${name}, ${license}) — enable explicitly by naming "${id}" in tools_enabled`,
161
+ };
162
+ }
163
+ if (FLAG_OPT_IN.has(id)) {
164
+ return flagFor(id, flags)
165
+ ? { on: true }
166
+ : { on: false, reason: `opt-in — set the ${id} input or name "${id}" in tools_enabled` };
167
+ }
168
+ // permissive default-on (subject to the tool's own runtime presence checks).
169
+ return { on: true };
170
+ }
171
+
172
+ /** the resolved selection for a run — a deterministic verdict per tool. */
173
+ export interface ResolvedToolSelection {
174
+ enabled(id: ToolId): boolean;
175
+ /** the reason a tool is OFF (for the scan report / logs); undefined when ON. */
176
+ offReason(id: ToolId): string | undefined;
177
+ /** licence-gated tools that are OFF because they weren't opted into. */
178
+ gated: ToolId[];
179
+ /** tools explicitly turned off via `tools_enabled`. */
180
+ disabled: ToolId[];
181
+ /** unrecognised `tools_enabled` tokens (warn, never fatal). */
182
+ unknownTokens: string[];
183
+ }
184
+
185
+ /**
186
+ * Resolve the per-tool selection from the run's payload (the parsed
187
+ * `tools_enabled` directive + the dedicated booleans). Pure; safe to call from
188
+ * any consumer (the scan tool, the secret guardrail, the terraform-mcp resolver,
189
+ * the plan tool) so they all agree on which tools may run this run.
190
+ */
191
+ export function resolveToolSelection(flags: ToolSelectionFlags): ResolvedToolSelection {
192
+ const verdicts = new Map<ToolId, Verdict>();
193
+ const gated: ToolId[] = [];
194
+ const disabled: ToolId[] = [];
195
+ for (const id of Object.keys(TOOL_LICENSES) as ToolId[]) {
196
+ const verdict = decide(id, flags);
197
+ verdicts.set(id, verdict);
198
+ if (verdict.on) continue;
199
+ if (flags.toolsEnabled?.explicit.get(id) === false) disabled.push(id);
200
+ else if (isLicenseGated(id)) gated.push(id);
201
+ }
202
+ return {
203
+ enabled: (id) => verdicts.get(id)?.on ?? false,
204
+ offReason: (id) => verdicts.get(id)?.reason,
205
+ gated,
206
+ disabled,
207
+ unknownTokens: flags.toolsEnabled?.unknown ?? [],
208
+ };
209
+ }
210
+
211
+ /** map a `terraform_scan` scanner source to the tool id it belongs to (null for
212
+ * the `reviewer` pseudo-source, which the gate never governs). */
213
+ export function scannerToolId(source: string): ToolId | null {
214
+ switch (source) {
215
+ case "terraform-fmt":
216
+ case "terraform-validate":
217
+ return "terraform";
218
+ case "tflint":
219
+ return "tflint";
220
+ case "trivy":
221
+ return "trivy";
222
+ case "checkov":
223
+ return "checkov";
224
+ default:
225
+ return null;
226
+ }
227
+ }
228
+
229
+ export type { ToolId };
230
+ // re-exported so callers need only this module for the common path.
231
+ export { isPermissive, TOOL_LICENSES };