infraweaver 0.3.7 → 0.3.9
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.
- package/README.md +2 -2
- package/dist/agents/claude.d.ts +1 -1
- package/dist/agents/claudePretoolGate.d.ts +0 -36
- package/dist/agents/toolPlaneWatch.d.ts +31 -0
- package/dist/cli.mjs +31908 -18736
- package/dist/external.d.ts +2 -1
- package/dist/index.js +31992 -18749
- package/dist/internal.js +133 -52
- package/dist/mcp/baseBranch.d.ts +41 -0
- package/dist/mcp/changeSummary.d.ts +1 -1
- package/dist/mcp/findingAudit.d.ts +2 -0
- package/dist/mcp/guardrails.d.ts +38 -0
- package/dist/mcp/localContext.d.ts +1 -1
- package/dist/mcp/moduleExtraction.d.ts +5 -0
- package/dist/mcp/pr.d.ts +0 -31
- package/dist/mcp/reproveSuggestions.d.ts +27 -0
- package/dist/mcp/review.d.ts +126 -2
- package/dist/mcp/reviewCommentLimit.d.ts +17 -0
- package/dist/mcp/reviewComments.d.ts +9 -5
- package/dist/mcp/reviewDedup.d.ts +1 -0
- package/dist/mcp/reviewMarkers.d.ts +10 -0
- package/dist/mcp/reviewProvenance.d.ts +20 -6
- package/dist/mcp/shared.d.ts +15 -0
- package/dist/mcp/suggestionProver.d.ts +3 -2
- package/dist/mcp/terraform/checkovSeverities.d.ts +7 -0
- package/dist/mcp/terraform/consolidation.d.ts +26 -0
- package/dist/mcp/terraform/cost.d.ts +2 -2
- package/dist/mcp/terraform/currency.d.ts +11 -0
- package/dist/mcp/terraform/deltaSummary.d.ts +11 -1
- package/dist/mcp/terraform/emittedArtefacts.d.ts +11 -0
- package/dist/mcp/terraform/evidence.d.ts +10 -0
- package/dist/mcp/terraform/gateScan.d.ts +2 -16
- package/dist/mcp/terraform/hcl.d.ts +15 -0
- package/dist/mcp/terraform/initArtefacts.d.ts +21 -0
- package/dist/mcp/terraform/localModules.d.ts +29 -0
- package/dist/mcp/terraform/moduleFindings.d.ts +38 -0
- package/dist/mcp/terraform/refactor/attribution.d.ts +1 -1
- package/dist/mcp/terraform/refactor/equivalence.d.ts +7 -0
- package/dist/mcp/terraform/refactor/expressions.d.ts +62 -0
- package/dist/mcp/terraform/refactor/references.d.ts +15 -0
- package/dist/mcp/terraform/refactor/resources.d.ts +11 -0
- package/dist/mcp/terraform/refactor/substitution.d.ts +55 -37
- package/dist/mcp/terraform/scanDelta.d.ts +28 -0
- package/dist/mcp/terraform/scanSession.d.ts +13 -0
- package/dist/mcp/terraform/scanners.d.ts +67 -18
- package/dist/mcp/terraform/secretScan.d.ts +19 -0
- package/dist/mcp/terraform/standardsReport.d.ts +1 -1
- package/dist/mcp/terraform/tools/plan.d.ts +1 -1
- package/dist/mcp/terraform/tools/scan.d.ts +7 -7
- package/dist/mcp/terraform/tools/validate.d.ts +7 -2
- package/dist/mcp/terraform/tools/verifyRemediation.d.ts +1 -1
- package/dist/mcp/terraform/treeLayout.d.ts +57 -0
- package/dist/mcp/terraform/types.d.ts +1 -1
- package/dist/phases/applyPlan.d.ts +0 -2
- package/dist/toolState.d.ts +3 -0
- package/dist/utils/agent/codexOAuth.d.ts +1 -1
- package/dist/utils/cli.d.ts +1 -7
- package/dist/utils/cloud/cloudReport.d.ts +1 -1
- package/dist/utils/config/baseRefConfig.d.ts +1 -1
- package/dist/utils/config/infraweaverConfig.d.ts +1 -1
- package/dist/utils/config/payload.d.ts +5 -0
- package/dist/utils/markdownTable.d.ts +6 -0
- package/dist/utils/setup/toolLicensing.d.ts +1 -1
- package/dist/utils/unifiedDiff.d.ts +69 -0
- package/package.json +34 -28
- package/src/agents/claude.ts +48 -13
- package/src/agents/claudePretoolGate.ts +9 -17
- package/src/agents/sessionLabeler.ts +3 -3
- package/src/agents/toolPlaneWatch.ts +71 -0
- package/src/external.ts +3 -1
- package/src/mcp/assess.ts +8 -8
- package/src/mcp/baseBranch.ts +96 -0
- package/src/mcp/changeSummary.ts +10 -11
- package/src/mcp/checkSuite.ts +7 -10
- package/src/mcp/checkout.ts +13 -40
- package/src/mcp/crosswalk.ts +5 -0
- package/src/mcp/findingAudit.ts +1 -1
- package/src/mcp/git.ts +44 -2
- package/src/mcp/guardrails.ts +188 -36
- package/src/mcp/localContext.ts +2 -0
- package/src/mcp/localServer.ts +1 -0
- package/src/mcp/moduleExtraction.ts +17 -5
- package/src/mcp/moduleTests.ts +9 -2
- package/src/mcp/modules.ts +10 -7
- package/src/mcp/pr.ts +17 -65
- package/src/mcp/reproveSuggestions.ts +86 -0
- package/src/mcp/review.ts +249 -75
- package/src/mcp/reviewAnchor.ts +11 -23
- package/src/mcp/reviewCommentLimit.ts +79 -0
- package/src/mcp/reviewComments.ts +34 -81
- package/src/mcp/reviewDedup.ts +1 -1
- package/src/mcp/reviewMarkers.ts +25 -2
- package/src/mcp/reviewProvenance.ts +56 -21
- package/src/mcp/roots.ts +3 -2
- package/src/mcp/server.ts +2 -1
- package/src/mcp/shared.ts +40 -1
- package/src/mcp/shell.ts +27 -28
- package/src/mcp/suggestionProver.ts +8 -4
- package/src/mcp/terraform/checkovSeverities.ts +1156 -0
- package/src/mcp/terraform/concernResult.ts +2 -1
- package/src/mcp/terraform/consolidation.ts +232 -19
- package/src/mcp/terraform/cost.ts +16 -7
- package/src/mcp/terraform/cspm.ts +8 -4
- package/src/mcp/terraform/currency.ts +28 -12
- package/src/mcp/terraform/decisions.ts +1 -1
- package/src/mcp/terraform/deltaSummary.ts +50 -1
- package/src/mcp/terraform/emittedArtefacts.ts +63 -0
- package/src/mcp/terraform/evidence.ts +23 -3
- package/src/mcp/terraform/gateScan.ts +191 -128
- package/src/mcp/terraform/hcl.ts +27 -2
- package/src/mcp/terraform/idleFloor.ts +2 -1
- package/src/mcp/terraform/initArtefacts.ts +76 -0
- package/src/mcp/terraform/localModules.ts +112 -0
- package/src/mcp/terraform/moduleDocs.ts +2 -5
- package/src/mcp/terraform/moduleFindings.ts +102 -0
- package/src/mcp/terraform/moduleVersionConstraints.ts +1 -1
- package/src/mcp/terraform/planPairs.ts +2 -20
- package/src/mcp/terraform/policyGate.ts +0 -15
- package/src/mcp/terraform/refactor/attribution.ts +39 -5
- package/src/mcp/terraform/refactor/equivalence.ts +49 -10
- package/src/mcp/terraform/refactor/expressions.ts +323 -0
- package/src/mcp/terraform/refactor/references.ts +101 -0
- package/src/mcp/terraform/refactor/resources.ts +79 -7
- package/src/mcp/terraform/refactor/substitution.ts +852 -256
- package/src/mcp/terraform/scanDelta.ts +41 -0
- package/src/mcp/terraform/scanSession.ts +30 -0
- package/src/mcp/terraform/scannerCache.ts +4 -0
- package/src/mcp/terraform/scanners.ts +553 -233
- package/src/mcp/terraform/secretScan.ts +131 -0
- package/src/mcp/terraform/standardsReport.ts +14 -13
- package/src/mcp/terraform/tools/consolidationCandidates.ts +3 -0
- package/src/mcp/terraform/tools/emitEvidence.ts +13 -5
- package/src/mcp/terraform/tools/emitOscal.ts +3 -2
- package/src/mcp/terraform/tools/emitVex.ts +10 -4
- package/src/mcp/terraform/tools/equivalenceCheck.ts +139 -21
- package/src/mcp/terraform/tools/infracostDiff.ts +2 -2
- package/src/mcp/terraform/tools/plan.ts +30 -17
- package/src/mcp/terraform/tools/reviewScanDelta.ts +3 -0
- package/src/mcp/terraform/tools/scan.ts +5 -3
- package/src/mcp/terraform/tools/standardsReport.ts +1 -1
- package/src/mcp/terraform/tools/validate.ts +20 -5
- package/src/mcp/terraform/tools/verifyRemediation.ts +14 -7
- package/src/mcp/terraform/tools/versionCurrency.ts +3 -1
- package/src/mcp/terraform/treeLayout.ts +378 -0
- package/src/mcp/terraform/types.ts +4 -1
- package/src/mcp/terraform/versionRequirements.ts +11 -2
- package/src/modes/__snapshots__/assemblePrompt.test.ts.snap +2 -0
- package/src/modes/modernize-deprecated.ts +2 -2
- package/src/modes/refactor.ts +9 -9
- package/src/modes/remediate.ts +1 -1
- package/src/modes/terraform-code-review.ts +7 -3
- package/src/modes/update-dependencies.ts +6 -6
- package/src/phases/applyPlan.ts +33 -14
- package/src/phases/applySuggestions.ts +2 -2
- package/src/phases/runAgentWithWatchdogs.ts +2 -1
- package/src/toolState.ts +7 -0
- package/src/utils/agent/codexOAuth.ts +7 -7
- package/src/utils/agent/openCodeModels.ts +4 -6
- package/src/utils/cli.ts +1 -31
- package/src/utils/cloud/runContext.ts +1 -8
- package/src/utils/config/baseRefConfig.ts +6 -0
- package/src/utils/config/infraweaverConfig.ts +2 -0
- package/src/utils/config/payload.ts +41 -0
- package/src/utils/github/assets.ts +1 -6
- package/src/utils/github/github.ts +42 -150
- package/src/utils/github/token.ts +6 -11
- package/src/utils/log.ts +4 -5
- package/src/utils/markdownTable.ts +11 -0
- package/src/utils/prompt/changeImpact.ts +18 -26
- package/src/utils/prompt/instructions.ts +1 -1
- package/src/utils/prompt/promptDirectives.ts +1 -1
- package/src/utils/prompt/toolSelection.ts +11 -2
- package/src/utils/setup/toolLicensing.ts +7 -0
- package/src/utils/telemetry.ts +2 -1
- package/src/utils/unifiedDiff.ts +120 -0
|
@@ -5,20 +5,20 @@ export function updateDependenciesMode(t: ToolRef): Mode {
|
|
|
5
5
|
return {
|
|
6
6
|
name: "UpdateDependencies",
|
|
7
7
|
description:
|
|
8
|
-
"Upgrade outdated or unpinned Terraform provider and module versions to current, supported pins, then PROVE the bump is safe — config-preserving bumps via the equivalence check (same resource set + arguments), behaviour-affecting major bumps via a reviewed plan delta (or shipped
|
|
8
|
+
"Upgrade outdated or unpinned Terraform provider and module versions to current, supported pins, then PROVE the bump is safe — config-preserving bumps via the equivalence check (same resource set + arguments), behaviour-affecting major bumps via a reviewed plan delta (or shipped proposed — unproven). One scoped PR per provider/module.",
|
|
9
9
|
prompt: `### Checklist
|
|
10
10
|
|
|
11
11
|
This mode turns the version-currency ADVISORY into action: it bumps stale/unpinned provider + module versions and PROVES the bump is safe before a human sees it, reusing the Refactor proof machinery. A pin bump that needs no code change is behaviour-preserving (proven by \`${t("terraform_equivalence_check")}\`); a major bump that changes resource shape ALTERS behaviour and is proven by a reviewed plan delta (or shipped \`proposed — unproven\`) — never a false equivalence claim.
|
|
12
12
|
|
|
13
13
|
1. **task list**: create your task list for this run as your first action.
|
|
14
14
|
|
|
15
|
-
2. **detect**: call \`${t("terraform_version_currency")}\` — it returns provider/module pins that are outdated or unpinned, each with the current constraint, the latest version,
|
|
15
|
+
2. **detect**: call \`${t("terraform_version_currency")}\` — it returns provider/module pins that are outdated or unpinned, each with the current constraint, the latest version, how many majors behind it is, and \`latest_in_major\` — the newest release in the major it already sits on.
|
|
16
16
|
|
|
17
|
-
3. **pick scope — ONE provider or module per PR**: take the most impactful safe upgrade first (a security-relevant provider bump, or pinning an unpinned pin to an exact constraint). **Targeted directive override:** if YOUR TASK names a specific provider/module, act only on it. Unless the task asks for more, open **at most one PR this run**; list deferred upgrades in the final report (never skip silently).
|
|
17
|
+
3. **pick scope — ONE provider or module per PR**: take the most impactful safe upgrade first (a security-relevant provider bump, or pinning an unpinned pin to an exact constraint). A row with \`latest_in_major\` has an upgrade that crosses no major — take that one when the major is not allowed (\`allow_major_upgrade\`), rather than reporting that nothing could be done. **Targeted directive override:** if YOUR TASK names a specific provider/module, act only on it. Unless the task asks for more, open **at most one PR this run**; list deferred upgrades in the final report (never skip silently).
|
|
18
18
|
|
|
19
19
|
4. **classify the bump (this decides the proof)**:
|
|
20
|
-
- **config-preserving** — a version-constraint change only (a patch/minor bump, or pinning a floating version) with NO change to any resource or argument. This is behaviour-preserving: prove it with \`${t("terraform_equivalence_check")}\` exactly as Refactor does — it must return \`equivalent: true\` (same resource set, identical arguments, validate + fmt clean). The push hard-fails an unproven one.
|
|
21
|
-
- **behaviour-affecting** — a major bump that requires resource/argument changes (renamed args, new required blocks, changed defaults). Equivalence is FALSE and must never be claimed. Make the minimal code changes the new major requires — use \`${t("terraform_validate")}\`'s \`providers\` majors and \`${t("terraform_provider_schema")}\` to get the new argument shape — then prove via the reviewed-plan path in step 6.
|
|
20
|
+
- **config-preserving** — a PROVIDER version-constraint change only (a patch/minor bump, or pinning a floating version) with NO change to any resource or argument. This is behaviour-preserving: prove it with \`${t("terraform_equivalence_check")}\` exactly as Refactor does — it must return \`equivalent: true\` (same resource set, identical arguments, validate + fmt clean). The push hard-fails an unproven one.
|
|
21
|
+
- **behaviour-affecting** — a major bump that requires resource/argument changes (renamed args, new required blocks, changed defaults), AND every registry MODULE version change, patch included: the module's resources are its code, which the bump replaces outside this tree, so \`${t("terraform_equivalence_check")}\` cannot see what changed and will not claim it. Equivalence is FALSE and must never be claimed. Make the minimal code changes the new major requires — use \`${t("terraform_validate")}\`'s \`providers\` majors and \`${t("terraform_provider_schema")}\` to get the new argument shape — then prove via the reviewed-plan path in step 6.
|
|
22
22
|
|
|
23
23
|
4b. **for a provider MAJOR bump, call \`${t("terraform_provider_upgrade")}\` BEFORE editing.** It returns the codified breaking changes for that exact major boundary, each with the file it is in. Three things in its output decide what you do next, and none of them can be inferred from the diff:
|
|
24
24
|
- \`needs_human: true\` — the transform MOVES STATE (a removed resource type, or a rename the vendor marks manual), forces replacement, or needs the VALUE changed and not just the name. Never apply one autonomously: put it in its own PR, label it \`needs-human\` (\`${t("add_labels")}\`), and name the affected resources in the body. Read \`needs_human_reason\` — the three causes need different PR bodies.
|
|
@@ -30,7 +30,7 @@ This mode turns the version-currency ADVISORY into action: it bumps stale/unpinn
|
|
|
30
30
|
|
|
31
31
|
6. **prove safety**:
|
|
32
32
|
- config-preserving → \`${t("terraform_equivalence_check")}\` is the proof (\`equivalent: true\`).
|
|
33
|
-
- behaviour-affecting → call \`${t("terraform_plan")}\` when cloud credentials are present and attach the **reviewed plan delta** as the proof; treat \`has_destroy_or_replace\`/\`stateful_destructive\` exactly as Remediate (a stateful destroy/replace is hard-blocked at push unless \`allow_replace\` covers that address — abandon otherwise). With NO creds, do not guess: ship the PR labelled \`proposed
|
|
33
|
+
- behaviour-affecting → call \`${t("terraform_plan")}\` when cloud credentials are present and attach the **reviewed plan delta** as the proof; treat \`has_destroy_or_replace\`/\`stateful_destructive\` exactly as Remediate (a stateful destroy/replace is hard-blocked at push unless \`allow_replace\` covers that address — abandon otherwise). With NO creds, do not guess: ship the PR labelled \`proposed-unproven\` (exactly that name, \`${t("add_labels")}\`) with the proposed-unproven banner, stating the major bump ALTERS behaviour and the plan must be reviewed.
|
|
34
34
|
- record either proof with \`${t("terraform_emit_evidence")}\`.
|
|
35
35
|
|
|
36
36
|
7. **keep module tests consistent** (only if the bump changed a local module's interface): \`${t("terraform_module_tests")}\` — update the drifting \`examples/\`/tests, never weaken an assertion.
|
package/src/phases/applyPlan.ts
CHANGED
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
* - its marker says a re-scan PROVED the fix — never an unproven or a
|
|
8
8
|
* judgement suggestion, which the review showed as plain code on purpose,
|
|
9
9
|
* - it was proved against the PR's current head, so the lines it replaces are
|
|
10
|
-
* exactly the lines the proof saw — a push since then makes it stale,
|
|
10
|
+
* exactly the lines the proof saw — a push since then makes it stale, until
|
|
11
|
+
* the next review re-proves it there and replies with the lines it used,
|
|
11
12
|
* - its thread is still open: a resolved thread's finding is already handled.
|
|
12
13
|
*/
|
|
13
|
-
import { isOwnReviewComment, parseCommentMarker } from "#app/mcp/reviewMarkers";
|
|
14
|
+
import { isOwnReviewComment, parseCommentMarker, suggestionText } from "#app/mcp/reviewMarkers";
|
|
14
15
|
|
|
15
16
|
export interface ReviewCommentLike {
|
|
16
17
|
id: number;
|
|
@@ -39,14 +40,6 @@ export interface SkippedSuggestion {
|
|
|
39
40
|
reason: string;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
const SUGGESTION = /```suggestion\r?\n([\s\S]*?)```/;
|
|
43
|
-
|
|
44
|
-
/** The text of a comment's `suggestion` block, without its closing newline. */
|
|
45
|
-
export function suggestionText(body: string): string | undefined {
|
|
46
|
-
const m = SUGGESTION.exec(body);
|
|
47
|
-
return m ? (m[1] ?? "").replace(/\r?\n$/, "") : undefined;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
43
|
export function planApply(input: {
|
|
51
44
|
comments: readonly ReviewCommentLike[];
|
|
52
45
|
/** root comment id → whether its thread is still open */
|
|
@@ -66,12 +59,21 @@ export function planApply(input: {
|
|
|
66
59
|
skip("the comment carries no suggestion block");
|
|
67
60
|
continue;
|
|
68
61
|
}
|
|
62
|
+
// proved on this head, at the comment's own lines — or proved on an earlier
|
|
63
|
+
// one and re-proved here by the engine, at the lines the re-proof names
|
|
64
|
+
let line = c.original_line;
|
|
65
|
+
let startLine = c.original_start_line ?? line;
|
|
69
66
|
if (c.original_commit_id !== input.headSha) {
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
const reproof = reproofAt(input.comments, c.id, input.headSha);
|
|
68
|
+
if (!reproof) {
|
|
69
|
+
skip(
|
|
70
|
+
"it was proved against an earlier commit and does not hold on this one — the review re-proves it on each push while it still fits",
|
|
71
|
+
);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
line = reproof.line;
|
|
75
|
+
startLine = reproof.startLine;
|
|
72
76
|
}
|
|
73
|
-
const line = c.original_line;
|
|
74
|
-
const startLine = c.original_start_line ?? line;
|
|
75
77
|
if (c.side === "LEFT" || line === undefined || startLine === undefined || startLine > line) {
|
|
76
78
|
skip("it does not anchor to a line range of the new code");
|
|
77
79
|
continue;
|
|
@@ -88,6 +90,23 @@ export function planApply(input: {
|
|
|
88
90
|
return { edits, skipped };
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
/** The engine's latest re-proof of root `rootId`'s suggestion on `head`, if any. */
|
|
94
|
+
function reproofAt(
|
|
95
|
+
comments: readonly ReviewCommentLike[],
|
|
96
|
+
rootId: number,
|
|
97
|
+
head: string,
|
|
98
|
+
): { startLine: number; line: number } | null {
|
|
99
|
+
let found: { startLine: number; line: number } | null = null;
|
|
100
|
+
for (const r of comments) {
|
|
101
|
+
if (r.in_reply_to_id !== rootId || !isOwnReviewComment(r.user, r.body)) continue;
|
|
102
|
+
const marker = parseCommentMarker(r.body);
|
|
103
|
+
if (marker?.kind === "reproof" && marker.head === head) {
|
|
104
|
+
found = { startLine: marker.startLine, line: marker.line };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return found;
|
|
108
|
+
}
|
|
109
|
+
|
|
91
110
|
/**
|
|
92
111
|
* Replace each edit's line range, bottom-up so earlier ranges keep their line
|
|
93
112
|
* numbers. The file's line ending and final newline are kept.
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
listOpenInfraweaverThreads,
|
|
20
20
|
RESOLVE_REVIEW_THREAD_MUTATION,
|
|
21
21
|
} from "#app/mcp/reviewComments";
|
|
22
|
+
import { count } from "#app/mcp/terraform/prose";
|
|
22
23
|
import {
|
|
23
24
|
type ApplyEdit,
|
|
24
25
|
applyEdits,
|
|
@@ -156,14 +157,13 @@ async function apply(repo: Repo, comment: PrCommentRunContext): Promise<ApplyOut
|
|
|
156
157
|
base_tree: head.tree.sha,
|
|
157
158
|
tree,
|
|
158
159
|
});
|
|
159
|
-
const count = batch.length;
|
|
160
160
|
const { data: commit } = await octokit.rest.git.createCommit({
|
|
161
161
|
owner,
|
|
162
162
|
repo: name,
|
|
163
163
|
tree: newTree.sha,
|
|
164
164
|
parents: [headSha],
|
|
165
165
|
message:
|
|
166
|
-
`Apply ${count
|
|
166
|
+
`Apply ${count(batch.length, "proven review suggestion")}\n\n` +
|
|
167
167
|
`Each was proved by ${PRODUCT_NAME}'s re-scan of ${headSha.slice(0, 7)}; ` +
|
|
168
168
|
`requested by @${comment.author} in #${comment.number}.`,
|
|
169
169
|
});
|
|
@@ -15,6 +15,7 @@ import { buildRunQuotas, hasGraceReserve } from "#app/agents/quotaReserve";
|
|
|
15
15
|
import type { Agent, AgentResult } from "#app/agents/shared";
|
|
16
16
|
import { reportProgress } from "#app/mcp/comment";
|
|
17
17
|
import type { ToolContext } from "#app/mcp/server";
|
|
18
|
+
import { count } from "#app/mcp/terraform/prose";
|
|
18
19
|
import type { ToolState } from "#app/toolState";
|
|
19
20
|
import { raceAgentRun } from "#app/utils/agent/agentRace";
|
|
20
21
|
import { INFRAWEAVER_DATA_DIR } from "#app/utils/agent/codexHome";
|
|
@@ -236,7 +237,7 @@ export async function runAgentWithWatchdogs(params: RunAgentParams): Promise<Age
|
|
|
236
237
|
if (!wasTracked) return;
|
|
237
238
|
const trackedRanges = toolState.diffCoverage?.coveredRanges ?? [];
|
|
238
239
|
log.debug(
|
|
239
|
-
`» diff coverage tracked from tool ${event.toolName} (${trackedRanges.length
|
|
240
|
+
`» diff coverage tracked from tool ${event.toolName} (${count(trackedRanges.length, "merged range")})`,
|
|
240
241
|
);
|
|
241
242
|
},
|
|
242
243
|
});
|
package/src/toolState.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "@infraweaver-io/ci/progressComment";
|
|
7
7
|
import type { AgentUsage } from "@infraweaver-io/models/usage";
|
|
8
8
|
import { DispatchRecorder } from "#app/agents/dispatchMetrics";
|
|
9
|
+
import type { StagedReview } from "#app/mcp/review";
|
|
9
10
|
import { createScanSession, type ScanSession } from "#app/mcp/terraform/scanSession";
|
|
10
11
|
import type { PrepResult } from "#app/prep/types";
|
|
11
12
|
import type { AgentDiagnostic } from "#app/utils/agent/agentHangReport";
|
|
@@ -140,6 +141,10 @@ export interface ToolState {
|
|
|
140
141
|
// number of remediation PRs opened this run. set only in Remediate mode by
|
|
141
142
|
// create_pull_request; read by the max_prs guardrail (mcp/guardrails.ts).
|
|
142
143
|
remediationPrsOpened?: number;
|
|
144
|
+
// remote branch → the commit push_branch pushed there this run. read by the
|
|
145
|
+
// pushed-head guardrail: a write mode's PR may only present a commit this
|
|
146
|
+
// run pushed, because the push is where every content guardrail runs.
|
|
147
|
+
pushedHeads?: Map<string, string>;
|
|
143
148
|
// the terraform tool-to-tool session: the baseline/scan/plan/cost/refactor
|
|
144
149
|
// signals the terraform MCP tools hand to one another and to end-of-run
|
|
145
150
|
// reporting. Extracted from this god-bag into its own typed, documented
|
|
@@ -170,6 +175,8 @@ export interface ToolState {
|
|
|
170
175
|
nodeId: string;
|
|
171
176
|
reviewedSha: string | undefined;
|
|
172
177
|
};
|
|
178
|
+
// comments and dismissals stage_review holds for the next review submission
|
|
179
|
+
stagedReview?: StagedReview | undefined;
|
|
173
180
|
// dedupe key: parent review comment_id → most-recent reply written this
|
|
174
181
|
// session by reply_to_review_comment. used by duplicateReplyDecision to
|
|
175
182
|
// skip identical-body re-emissions of the same call (PR #610 root cause).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Codex OAuth refresh (fetch) + JWT exp decoding (jose, decode-only).
|
|
3
3
|
*
|
|
4
4
|
* Deliberately free of node:child_process / spawn / mkdtemp: the Cloud's
|
|
5
5
|
* Next.js server imports `refreshCodexAuthBody` through the
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* bundle. `src/utils/agent/codexHome.ts` is the in-runner consumer.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { decodeJwt } from "jose";
|
|
12
|
+
|
|
11
13
|
export interface CodexAuthBody {
|
|
12
14
|
auth_mode: "chatgpt";
|
|
13
15
|
tokens: {
|
|
@@ -98,16 +100,14 @@ export async function refreshCodexAuthBody(body: CodexAuthBody): Promise<CodexAu
|
|
|
98
100
|
* would just delay the inevitable 401 from OpenAI. No security boundary
|
|
99
101
|
* at this decode step. */
|
|
100
102
|
export function decodeJwtExpMs(token: string): number | null {
|
|
101
|
-
|
|
102
|
-
if (parts.length !== 3) return null;
|
|
103
|
-
let payload: { exp?: unknown };
|
|
103
|
+
let exp: unknown;
|
|
104
104
|
try {
|
|
105
|
-
|
|
105
|
+
exp = decodeJwt(token).exp;
|
|
106
106
|
} catch {
|
|
107
107
|
return null;
|
|
108
108
|
}
|
|
109
|
-
if (typeof
|
|
110
|
-
return
|
|
109
|
+
if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
|
|
110
|
+
return exp * 1000;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/** parse + validate a Codex auth.json body from its JSON-string form.
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
// (`selectFallbackModelIfNeeded` + `autoSelectModel`) share one shell-out.
|
|
30
30
|
|
|
31
31
|
import { spawnSync } from "node:child_process";
|
|
32
|
+
import { stripVTControlCharacters } from "node:util";
|
|
32
33
|
import { getModelEnvVars } from "@infraweaver-io/models/catalog";
|
|
33
34
|
import { log } from "#app/utils/cli";
|
|
34
35
|
|
|
@@ -62,11 +63,6 @@ let baseline: Set<string> | undefined;
|
|
|
62
63
|
let authorized: Set<string> | undefined;
|
|
63
64
|
let failure: string | undefined;
|
|
64
65
|
|
|
65
|
-
// the CLI paints its errors; these get re-rendered into a PR comment, where raw
|
|
66
|
-
// SGR escapes read as literal `[91m` noise.
|
|
67
|
-
// biome-ignore lint/suspicious/noControlCharactersInRegex: matching SGR escapes is the point
|
|
68
|
-
const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
|
|
69
|
-
|
|
70
66
|
function readModels(cliPath: string): Set<string> {
|
|
71
67
|
// spawnSync, not execFileSync: we want opencode's stderr as a VALUE rather
|
|
72
68
|
// than a throw, so `getModelsFailure` can hand the real reason to
|
|
@@ -84,7 +80,9 @@ function readModels(cliPath: string): Set<string> {
|
|
|
84
80
|
// `result.error` carries the only reason there is. without folding it in,
|
|
85
81
|
// `failure` stays undefined and the empty set reaches `validateAgentApiKey`
|
|
86
82
|
// as a bare "you have no key" verdict for what is really a runner problem.
|
|
87
|
-
|
|
83
|
+
// the CLI paints its errors; this gets re-rendered into a PR comment, where
|
|
84
|
+
// raw escapes read as literal `[91m` noise.
|
|
85
|
+
const stderr = stripVTControlCharacters(result.stderr ?? "").trim();
|
|
88
86
|
failure = stderr || result.error?.message;
|
|
89
87
|
log.debug(`» \`opencode models\` failed (${result.status}): ${failure}`);
|
|
90
88
|
return new Set();
|
package/src/utils/cli.ts
CHANGED
|
@@ -2,35 +2,5 @@
|
|
|
2
2
|
* CLI utilities
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
6
|
-
import { existsSync } from "node:fs";
|
|
7
|
-
|
|
8
5
|
// re-export logging utilities for backward compatibility
|
|
9
|
-
export {
|
|
10
|
-
formatIndentedField,
|
|
11
|
-
formatJsonValue,
|
|
12
|
-
formatUsageSummary,
|
|
13
|
-
log,
|
|
14
|
-
writeSummary,
|
|
15
|
-
} from "#app/utils/log";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Finds a CLI executable path by checking if it's installed globally
|
|
19
|
-
* @param name The name of the CLI executable to find
|
|
20
|
-
* @returns The path to the CLI executable, or null if not found
|
|
21
|
-
*/
|
|
22
|
-
export function findCliPath(name: string): string | null {
|
|
23
|
-
// `which` is POSIX-only. The action always runs on Linux, but this repo is
|
|
24
|
-
// developed on Windows, where `which` does not exist and the lookup fails
|
|
25
|
-
// silently as "not installed" — use `where` there. `where` can print several
|
|
26
|
-
// matches, one per line, so take the first.
|
|
27
|
-
const isWindows = process.platform === "win32";
|
|
28
|
-
const result = spawnSync(isWindows ? "where" : "which", [name], { encoding: "utf-8" });
|
|
29
|
-
if (result.status === 0 && result.stdout) {
|
|
30
|
-
const cliPath = result.stdout.split(/\r?\n/)[0]?.trim();
|
|
31
|
-
if (cliPath && existsSync(cliPath)) {
|
|
32
|
-
return cliPath;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
6
|
+
export { formatJsonValue, formatUsageSummary, log, writeSummary } from "#app/utils/log";
|
|
@@ -210,10 +210,7 @@ export async function fetchRunContext(params: {
|
|
|
210
210
|
repoContext: RepoContext;
|
|
211
211
|
oidcToken?: string | undefined;
|
|
212
212
|
}): Promise<RunContext> {
|
|
213
|
-
|
|
214
|
-
const controller = new AbortController();
|
|
215
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
216
|
-
|
|
213
|
+
// no `signal`: apiFetch applies the cloud transport's default timeout
|
|
217
214
|
try {
|
|
218
215
|
const headers: Record<string, string> = {
|
|
219
216
|
Authorization: `Bearer ${params.token}`,
|
|
@@ -225,11 +222,8 @@ export async function fetchRunContext(params: {
|
|
|
225
222
|
const response = await apiFetch({
|
|
226
223
|
path: `/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context`,
|
|
227
224
|
headers,
|
|
228
|
-
signal: controller.signal,
|
|
229
225
|
});
|
|
230
226
|
|
|
231
|
-
clearTimeout(timeoutId);
|
|
232
|
-
|
|
233
227
|
if (!response.ok) {
|
|
234
228
|
if ((response.status === 401 || response.status === 403) && isBackendConfigured()) {
|
|
235
229
|
log.warning(cloudAuthHint(response.status, params.oidcToken));
|
|
@@ -285,7 +279,6 @@ export async function fetchRunContext(params: {
|
|
|
285
279
|
...(moduleCredentials ? { moduleCredentials } : {}),
|
|
286
280
|
};
|
|
287
281
|
} catch (err) {
|
|
288
|
-
clearTimeout(timeoutId);
|
|
289
282
|
return fellBackToDefaults(errorMessageOr(err, "request failed"), true);
|
|
290
283
|
}
|
|
291
284
|
}
|
|
@@ -80,6 +80,12 @@ export const GATE_RELEVANT_CONFIG_KEYS = [
|
|
|
80
80
|
// silence it from the PR that set it, which is the shape this module exists
|
|
81
81
|
// to refuse. Read from the base ref; a PR cannot retune it for its own run.
|
|
82
82
|
"repeated_literal_threshold",
|
|
83
|
+
// which variable files the scanners load: a PR that unmaps its own var file
|
|
84
|
+
// would hide what that file sets.
|
|
85
|
+
"var_files",
|
|
86
|
+
// how many review comments post inline: lowering it moves findings out of
|
|
87
|
+
// the diff view and into the body, which a PR should not do for itself.
|
|
88
|
+
"review_comment_limit",
|
|
83
89
|
] as const satisfies readonly InfraweaverConfigKey[];
|
|
84
90
|
|
|
85
91
|
/**
|
|
@@ -58,6 +58,8 @@ export const INFRAWEAVER_CONFIG_KEYS = [
|
|
|
58
58
|
"module_catalogue",
|
|
59
59
|
"repeated_literal_threshold",
|
|
60
60
|
"required_controls",
|
|
61
|
+
"var_files",
|
|
62
|
+
"review_comment_limit",
|
|
61
63
|
] as const;
|
|
62
64
|
|
|
63
65
|
export type InfraweaverConfigKey = (typeof INFRAWEAVER_CONFIG_KEYS)[number];
|
|
@@ -6,6 +6,7 @@ import { type } from "arktype";
|
|
|
6
6
|
import { PRODUCT_SLUG } from "#app/brand";
|
|
7
7
|
import type { AuthorPermission, PayloadEvent } from "#app/external";
|
|
8
8
|
import { parseToolsNetwork } from "#app/mcp/terraform/scannerEgress";
|
|
9
|
+
import { parseVarFileMappings, type VarFileMapping } from "#app/mcp/terraform/treeLayout";
|
|
9
10
|
import { log } from "#app/utils/cli";
|
|
10
11
|
import type { RepoSettings } from "#app/utils/cloud/runContext";
|
|
11
12
|
import { applyBaseRefConfigPolicy } from "#app/utils/config/baseRefConfig";
|
|
@@ -115,6 +116,8 @@ export const Inputs = type({
|
|
|
115
116
|
"protected_paths?": type.string.or("undefined"),
|
|
116
117
|
"autonomy_threshold?": type.string.or("undefined"),
|
|
117
118
|
"repeated_literal_threshold?": type.string.or("undefined"),
|
|
119
|
+
"var_files?": type.string.or("undefined"),
|
|
120
|
+
"review_comment_limit?": type.string.or("undefined"),
|
|
118
121
|
"required_controls?": type.string.or("undefined"),
|
|
119
122
|
"gitleaks?": type.string.or("undefined"),
|
|
120
123
|
"cost_increase_block_usd?": type.string.or("undefined"),
|
|
@@ -216,6 +219,8 @@ function resolveNonPromptInputs() {
|
|
|
216
219
|
protected_paths: core.getInput("protected_paths") || undefined,
|
|
217
220
|
autonomy_threshold: core.getInput("autonomy_threshold") || undefined,
|
|
218
221
|
repeated_literal_threshold: core.getInput("repeated_literal_threshold") || undefined,
|
|
222
|
+
var_files: core.getInput("var_files") || undefined,
|
|
223
|
+
review_comment_limit: core.getInput("review_comment_limit") || undefined,
|
|
219
224
|
required_controls: core.getInput("required_controls") || undefined,
|
|
220
225
|
gitleaks: core.getInput("gitleaks") || undefined,
|
|
221
226
|
cost_increase_block_usd: core.getInput("cost_increase_block_usd") || undefined,
|
|
@@ -275,6 +280,35 @@ function parseRepeatThreshold(raw: string | undefined): number | undefined {
|
|
|
275
280
|
return n;
|
|
276
281
|
}
|
|
277
282
|
|
|
283
|
+
/** Inline comments one review posts when `review_comment_limit` is unset. */
|
|
284
|
+
const DEFAULT_REVIEW_COMMENT_LIMIT = 25;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The `review_comment_limit` setting: a whole number of inline comments, 0 for
|
|
288
|
+
* no limit. Unset or unusable falls back to the default rather than failing the
|
|
289
|
+
* run — a presentation knob set wrong is not worth losing a review over — and an
|
|
290
|
+
* unusable value says so, since silently ignoring it would look like a bug.
|
|
291
|
+
*/
|
|
292
|
+
function parseReviewCommentLimit(raw: string | undefined): number {
|
|
293
|
+
const text = raw?.trim();
|
|
294
|
+
if (!text) return DEFAULT_REVIEW_COMMENT_LIMIT;
|
|
295
|
+
const n = Number(text);
|
|
296
|
+
if (Number.isInteger(n) && n >= 0) return n;
|
|
297
|
+
log.warning(
|
|
298
|
+
`» review_comment_limit: "${text}" is not a whole number — using ${DEFAULT_REVIEW_COMMENT_LIMIT}`,
|
|
299
|
+
);
|
|
300
|
+
return DEFAULT_REVIEW_COMMENT_LIMIT;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** The `var_files` setting as mappings; a malformed entry is reported and left out. */
|
|
304
|
+
function parseVarFiles(raw: string | undefined): VarFileMapping[] | undefined {
|
|
305
|
+
const { mappings, invalid } = parseVarFileMappings(raw);
|
|
306
|
+
for (const entry of invalid) {
|
|
307
|
+
log.warning(`» var_files: "${entry}" is not root=file — ignored`);
|
|
308
|
+
}
|
|
309
|
+
return mappings.length > 0 ? mappings : undefined;
|
|
310
|
+
}
|
|
311
|
+
|
|
278
312
|
function parseSeverityThreshold(raw: string | undefined): string | undefined {
|
|
279
313
|
if (!raw) return undefined;
|
|
280
314
|
const v = raw.trim().toLowerCase();
|
|
@@ -569,6 +603,13 @@ export function resolvePayload(
|
|
|
569
603
|
repeatedLiteralThreshold: parseRepeatThreshold(
|
|
570
604
|
inputs.repeated_literal_threshold ?? fileConfig.repeated_literal_threshold,
|
|
571
605
|
),
|
|
606
|
+
// `-var-file` files and the root each belongs to, for the files no
|
|
607
|
+
// matching can place (`treeLayout.ts`).
|
|
608
|
+
varFiles: parseVarFiles(inputs.var_files ?? fileConfig.var_files),
|
|
609
|
+
// inline comments one review posts; the rest are listed in its body.
|
|
610
|
+
reviewCommentLimit: parseReviewCommentLimit(
|
|
611
|
+
inputs.review_comment_limit ?? fileConfig.review_comment_limit,
|
|
612
|
+
),
|
|
572
613
|
// compliance controls that must have NO open finding — a finding mapping to
|
|
573
614
|
// one fails the gate regardless of severity (the "required controls" policy:
|
|
574
615
|
// binds the crosswalk to the gate). Matched by framework / control /
|
|
@@ -83,11 +83,6 @@ export async function downloadAssetsInMarkdown(
|
|
|
83
83
|
* {@link MAX_ASSET_BYTES}). */
|
|
84
84
|
const ASSET_DOWNLOAD_CONCURRENCY = 5;
|
|
85
85
|
|
|
86
|
-
/** Escape a literal for embedding in a RegExp. */
|
|
87
|
-
function escapeRegExp(literal: string): string {
|
|
88
|
-
return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
86
|
/**
|
|
92
87
|
* Substitute every key of `replacements` in `text` in ONE pass, via a single
|
|
93
88
|
* alternation.
|
|
@@ -103,7 +98,7 @@ function replaceAll(text: string, replacements: Map<string, string>): string {
|
|
|
103
98
|
if (replacements.size === 0) return text;
|
|
104
99
|
const pattern = [...replacements.keys()]
|
|
105
100
|
.sort((a, b) => b.length - a.length)
|
|
106
|
-
.map(
|
|
101
|
+
.map((key) => RegExp.escape(key))
|
|
107
102
|
.join("|");
|
|
108
103
|
return text.replace(new RegExp(pattern, "g"), (match) => replacements.get(match) ?? match);
|
|
109
104
|
}
|