infraweaver 0.3.11 → 0.3.13
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 +1 -1
- package/dist/agents/opencode.d.ts +6 -0
- package/dist/agents/quotaTokens.d.ts +6 -0
- package/dist/cli.mjs +846 -412
- package/dist/index.js +806 -372
- package/dist/internal.js +4 -4
- package/dist/mcp/git.d.ts +6 -4
- package/dist/mcp/localContext.d.ts +7 -0
- package/dist/mcp/reviewCommentLimit.d.ts +55 -5
- package/dist/mcp/reviewMarkers.d.ts +5 -0
- package/dist/mcp/staleFix.d.ts +36 -1
- package/dist/mcp/terraform/awsPrices.d.ts +1 -1
- package/dist/mcp/terraform/azurePrices.d.ts +1 -1
- package/dist/mcp/terraform/concernResult.d.ts +9 -0
- package/dist/mcp/terraform/deltaSummary.d.ts +3 -0
- package/dist/mcp/terraform/hardcodedSecrets.d.ts +24 -10
- package/dist/mcp/terraform/refactorScope.d.ts +14 -2
- package/dist/mcp/terraform/ruleContext.d.ts +3 -0
- package/dist/mcp/terraform/scanSession.d.ts +2 -0
- package/dist/mcp/terraform/tree.d.ts +5 -0
- package/dist/mcp/terraform/types.d.ts +2 -1
- package/dist/mcp/terraform/verifySites.d.ts +56 -0
- package/dist/vendor/@infraweaver-io/models/tokenQuota.d.ts +7 -6
- package/package.json +2 -2
- package/src/agents/opencode.ts +8 -1
- package/src/agents/quotaTokens.ts +7 -1
- package/src/mcp/git.ts +64 -12
- package/src/mcp/localContext.ts +6 -1
- package/src/mcp/moduleExtraction.ts +5 -4
- package/src/mcp/review.ts +52 -3
- package/src/mcp/reviewCommentLimit.ts +169 -26
- package/src/mcp/reviewMarkers.ts +11 -0
- package/src/mcp/reviewProvenance.ts +1 -1
- package/src/mcp/staleFix.ts +73 -2
- package/src/mcp/terraform/awsPrices.ts +1 -1
- package/src/mcp/terraform/azurePrices.ts +23 -23
- package/src/mcp/terraform/concernResult.ts +18 -9
- package/src/mcp/terraform/deltaSummary.ts +4 -0
- package/src/mcp/terraform/hardcodedSecrets.ts +148 -27
- package/src/mcp/terraform/nativeRuleScanners.ts +2 -1
- package/src/mcp/terraform/nativeScan.ts +4 -0
- package/src/mcp/terraform/refactor/equivalence.ts +42 -9
- package/src/mcp/terraform/refactorScope.ts +90 -21
- package/src/mcp/terraform/ruleContext.ts +4 -0
- package/src/mcp/terraform/scanDelta.ts +67 -26
- package/src/mcp/terraform/scanSession.ts +6 -0
- package/src/mcp/terraform/tools/detectPlanDrift.ts +2 -0
- package/src/mcp/terraform/tools/ingestExternalFindings.ts +2 -0
- package/src/mcp/terraform/tools/normalizationCandidates.ts +5 -5
- package/src/mcp/terraform/tools/readFindings.ts +2 -0
- package/src/mcp/terraform/tools/scan.ts +3 -0
- package/src/mcp/terraform/tools/verifyRemediation.ts +23 -1
- package/src/mcp/terraform/tree.ts +8 -0
- package/src/mcp/terraform/types.ts +2 -1
- package/src/mcp/terraform/verifySites.ts +124 -0
- package/src/modes/refactor.ts +1 -1
- package/src/modes/remediate-and-refactor.ts +1 -1
- package/src/modes/remediate.ts +1 -1
- package/src/phases/runAgentWithWatchdogs.ts +3 -2
package/dist/internal.js
CHANGED
|
@@ -110,7 +110,7 @@ function isValidTimeString(input) {
|
|
|
110
110
|
return parseTimeString(input) !== null;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
// node_modules/.pnpm/@infraweaver-io+models@0.1.
|
|
113
|
+
// node_modules/.pnpm/@infraweaver-io+models@0.1.7/node_modules/@infraweaver-io/models/dist/catalog.js
|
|
114
114
|
function provider(config) {
|
|
115
115
|
return config;
|
|
116
116
|
}
|
|
@@ -1781,7 +1781,7 @@ This mode standardises STRUCTURE while preserving BEHAVIOUR \u2014 the equivalen
|
|
|
1781
1781
|
- \`${t("terraform_normalization_candidates")}\` \u2192 in-place idiomatic cleanups that change SYNTAX, not behaviour (redundant whole-string \`"\${expr}"\` interpolation; legacy HCL0.11 map-argument block syntax like \`vars {\` on a \`template_file\`). These relocate NOTHING \u2014 zero \`moved {}\` blocks \u2014 and are proven by the same equivalence check.
|
|
1782
1782
|
- \`${t("terraform_consolidation_candidates")}\` \u2192 the SAME resource shape declared in several root directories (\`env/dev\` + \`env/staging\` + \`env/prod\`), with the single parameterised module they could all call already derived: which attributes are identical (module content) and which differ (per-environment inputs). A member root of a group will ALSO show up as a zero-candidate cluster in \`${t("module_extraction_candidates")}\` \u2014 prefer the consolidation, which does that work once for every environment instead of once per environment. Its \`near_groups\` are the opposite case: roots recognisably the same stack that DIFFER (one carries a resource the other does not), where folding them together is a design decision this mode has no basis for. Do NOT extract a near-group root's shared resources into a module on its own either \u2014 a module one twin calls and the other does not widens exactly the drift the near-group reports. Leave those roots alone and name the near-group and its \`differences\` in your report.
|
|
1783
1783
|
|
|
1784
|
-
**A
|
|
1784
|
+
**A candidate marked \`report_only\` is an observation, not work.** It sits in the code of a module other configurations call (\`report_only_reason\` says why), and restructuring that is churn for every caller: name those candidates in your \`${t("report_progress")}\` summary and open no PR for them. \`examples/\` roots are left out of every candidate list unless the operator set \`refactor_examples\`.
|
|
1785
1785
|
|
|
1786
1786
|
**Before picking, check the interface.** \`${t("module_extraction_candidates")}\` reports \`unmapped_attributes\` per candidate and a top-level \`partial_interfaces\` list: arguments the raw resources SET that the target module does not. Adopting such a module DROPS them silently \u2014 and the equivalence check cannot catch it, because once the argument is gone it is absent from both sides. Two of these are unrecoverable rather than merely wrong (\`object_lock_enabled\` cannot be set after creation; \`force_destroy\` changes deletion semantics). So: **decline the candidate, or extend the module to expose the argument and say so in the PR body.** Deleting the argument to make the shapes match is never the fix. A candidate whose \`unmapped_attributes\` is \`null\` was NOT checked \u2014 the module is external and its code is not in this repo \u2014 which is not the same as clean.
|
|
1787
1787
|
|
|
@@ -1914,7 +1914,7 @@ function remediateMode(t) {
|
|
|
1914
1914
|
|
|
1915
1915
|
4. **for the chosen group**:
|
|
1916
1916
|
- **base branch**: this run's base branch is resolved deterministically \u2014 \`${t("create_pull_request")}\` targets the \`base_branch\` input if set, else the branch the run started on, else the repository's default branch (\`main\`, or \`master\`). You do not choose it; just **omit** the \`base\` argument when opening the PR (below) and it is filled in.
|
|
1917
|
-
- **idempotency**: the remediation branch is the group's \`branch\` (\`remediate/<group-id>\`, with a \`--<base>\` suffix when the PR targets a non-default branch) \u2014 use it exactly, never a name of your own. Before doing anything, check whether that branch or an open PR for it already exists (\`${t("git")}\` / \`${t("get_pull_request")}\`). If one exists, update it rather than opening a duplicate.
|
|
1917
|
+
- **idempotency**: the remediation branch is the group's \`branch\` (\`remediate/<group-id>\`, with a \`--<base>\` suffix when the PR targets a non-default branch, or the branch of the PR already open for this group) \u2014 use it exactly, never a name of your own. Before doing anything, check whether that branch or an open PR for it already exists (\`${t("git")}\` / \`${t("get_pull_request")}\`). If one exists, update it rather than opening a duplicate.
|
|
1918
1918
|
- **branch**: create the group's \`branch\` from the **current HEAD** (the checkout that was just scanned) via \`${t("git")}\` (\`git({ command: "checkout", args: ["-b", <branch>] })\`). Do NOT switch to a different base first \u2014 branching from the scanned checkout keeps the PR diff to exactly your fix.
|
|
1919
1919
|
- **honest refusal (decide BEFORE fixing)**: if the group's concerns appear in the scan's \`refusal_candidates\` (the fix needs a human decision \u2014 narrowing an IAM wildcard, a KMS key policy, a real ingress CIDR), do **not** guess a fix that could break the stack. Instead open a structured issue (\`${t("create_issue")}\`) describing the concern, why it isn't auto-fixed, and what a human should do, and skip the PR for that group. A proven fix or an honest refusal \u2014 never a guessed, unverifiable PR.
|
|
1920
1920
|
- **propose, then let me steer (when there's no single right fix)**: distinct from honest refusal (which refuses a fix a human must *decide*), this is for a finding with **2\u20133 genuinely distinct, defensible fixes** that differ in trade-offs, not correctness (e.g. encrypt with an AWS-managed key **vs** a customer-managed KMS key; a narrow security-group rule **vs** a prefix list **vs** a VPC endpoint). When such a fork exists **and the triggering comment did not already select a strategy**, do **not** silently pick for the reviewer: via \`${t("create_issue_comment")}\` post one short comment listing the options as **A / B / C** \u2014 each a single line (what it does + its trade-off) \u2014 and ask the reviewer to reply \`${COMMENT_COMMAND} fix #<concern-id> with strategy <A|B|C>\`. Then **skip the PR for this group** this run and note it in your final report (it resumes when the reviewer replies). When the comment **did** select one (\`fix #<id> with strategy B\`, or a bare \`strategy B\` reply on the proposal thread), apply **exactly** that strategy \u2014 don't second-guess it. Reserve this for real forks in the road; a fix with one obvious correct answer just gets made.
|
|
@@ -1975,7 +1975,7 @@ This mode composes the **Remediate** and **Refactor** verbs into a single run. I
|
|
|
1975
1975
|
- **commit the fix** (\`git add\` only the changed \`*.tf\`/\`*.tfvars\`, a \`fix(tf): \u2026\` message). Do NOT push yet. This committed state is the refactor's equivalence BASELINE.
|
|
1976
1976
|
|
|
1977
1977
|
3. **REFACTOR (phase 2) \u2014 modularise on top of the committed fix**: follow the **Refactor** mode's flow:
|
|
1978
|
-
- \`${t("module_extraction_candidates")}\` / \`${t("terraform_normalization_candidates")}\` \u2192 pick one behaviour-preserving refactor; a
|
|
1978
|
+
- \`${t("module_extraction_candidates")}\` / \`${t("terraform_normalization_candidates")}\` \u2192 pick one behaviour-preserving refactor; a candidate marked \`report_only\` (it sits in the code of a module others call) is named in the summary, never proposed. Respect a pinned \`refactor_source\` if one was supplied (it constrains which module source you may use; the behaviour-altering b.4 path is only available when the operator pinned \`third-party\`).
|
|
1979
1979
|
- resolve the module source, wire the \`module\` call against its real interface (\`${t("terraform_module_interface")}\` for a local module dir, \`${t("terraform_module_lookup")}\` for a registry-sourced one), and emit a \`moved {}\` block for EVERY relocated address (\`${t("terraform_generate_moved")}\`).
|
|
1980
1980
|
- \`terraform fmt\` + \`${t("terraform_validate")}\`, then \u2014 **with the refactor edits still UNCOMMITTED** \u2014 call \`${t("terraform_equivalence_check")}\`. In this mode it diffs the working tree against the COMMITTED fix (not the run-start commit), so it measures only the refactor. It must return \`equivalent: true\` (zero uncovered moves, resource set + arguments preserved, validate + fmt clean) before you proceed; \`${t("push_branch")}\` hard-blocks an unproven one \u2014 and, once this run has added a \`moved {}\` block, it also hard-blocks a push that never ran the check at all, so abandoning the refactor means REVERTING its edits rather than leaving them in with the fix. If it can't be proven equivalent, abandon the refactor and ship the fix alone.
|
|
1981
1981
|
- **document the module (only when the \`docs\` input is enabled)**: after the equivalence check passes, call \`${t("terraform_module_docs")}\` for the module you created/adopted (pass the relocated addresses as \`moves\`). Do not hand-write docs \u2014 the tool builds from the interface, moved blocks, and equivalence verdict.
|
package/dist/mcp/git.d.ts
CHANGED
|
@@ -51,10 +51,12 @@ export declare const Git: import("arktype/internal/variants/object.ts").ObjectTy
|
|
|
51
51
|
* The subcommand and arguments of a call, however the model wrote it: the
|
|
52
52
|
* subcommand alone (the documented form), a whole `git …` command line, or the
|
|
53
53
|
* subcommand as the first of `args`. Models wrote the other two often enough
|
|
54
|
-
* to cost a turn per run, and each is unambiguous.
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
54
|
+
* to cost a turn per run, and each is unambiguous. Leading `git` words are
|
|
55
|
+
* dropped, so `{ command: "git", args: ["status"] }` is not `git git status`.
|
|
56
|
+
* The subcommand is checked against the same pattern the schema uses — in
|
|
57
|
+
* every form, the documented one included, so the check does not rest on the
|
|
58
|
+
* schema alone — and a flag (`-c`, `-C`, `--exec-path`) can no more arrive
|
|
59
|
+
* through `args[0]` or a command line than through `command`.
|
|
58
60
|
*/
|
|
59
61
|
export declare function normalizeGitCall(params: {
|
|
60
62
|
command?: string | undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ToolState } from "../toolState.js";
|
|
2
2
|
import type { ResolvedPayload } from "../utils/config/payload.js";
|
|
3
|
+
import type { OctokitWithPlugins } from "../utils/github/github.js";
|
|
3
4
|
/**
|
|
4
5
|
* The cwd-scoped, GitHub-free subset of `ToolContext` that the read-only
|
|
5
6
|
* Terraform tools depend on. Two providers exist:
|
|
@@ -27,8 +28,14 @@ export interface LocalToolContext {
|
|
|
27
28
|
* carries it; a local session has none, so its remediation branches keep the
|
|
28
29
|
* plain `remediate/<id>` name. */
|
|
29
30
|
repo?: {
|
|
31
|
+
owner?: string;
|
|
32
|
+
name?: string;
|
|
30
33
|
data: {
|
|
31
34
|
default_branch?: string | null;
|
|
32
35
|
};
|
|
33
36
|
} | undefined;
|
|
37
|
+
/** the Action's GitHub client, read only to find the remediation PRs already
|
|
38
|
+
* open, so a group that has one keeps that PR's branch. A local session has
|
|
39
|
+
* none and names its branches from the group and base alone. */
|
|
40
|
+
octokit?: OctokitWithPlugins | undefined;
|
|
34
41
|
}
|
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many inline comments one review posts.
|
|
3
|
+
*
|
|
4
|
+
* A review of forty inline comments is read by no one, and every one of them is
|
|
5
|
+
* a thread the author must resolve. Past the limit the most important comments
|
|
6
|
+
* still post inline — ranked by the scanner severity of the finding a comment
|
|
7
|
+
* answers, or else by its own 🚨/⚠️/ℹ️ marker (the marker is optional, so an
|
|
8
|
+
* unmarked comment ranks level with ⚠️, not last), a proven (one-click
|
|
9
|
+
* committable) fix ahead of an equal one without, otherwise in the order the
|
|
10
|
+
* reviewer wrote them — and the rest are listed in the review body in full, so
|
|
11
|
+
* nothing the review found is lost, only folded. The gate is not involved: it
|
|
12
|
+
* judges the scan delta, never the comments.
|
|
13
|
+
*
|
|
14
|
+
* A folded comment opens no thread, so the approval check that counts open
|
|
15
|
+
* threads cannot see it. A folded scanner finding keeps its hidden marker in
|
|
16
|
+
* the review body instead, and a later approval is refused while the head
|
|
17
|
+
* still introduces it (`foldedFindingsStillIntroduced`).
|
|
18
|
+
*
|
|
19
|
+
* The limit is a repo setting (`review_comment_limit`), read from the base
|
|
20
|
+
* branch on a pull request: lowering it moves findings out of the diff view,
|
|
21
|
+
* which is a way to make them easier to miss, so a PR does not get to set it for
|
|
22
|
+
* itself.
|
|
23
|
+
*/
|
|
24
|
+
import type { Dismissal } from "./reviewProvenance.js";
|
|
25
|
+
import { type DeltaFinding, type GateDelta } from "./terraform/scanDelta.js";
|
|
26
|
+
import type { Severity } from "./terraform/types.js";
|
|
1
27
|
interface LimitableComment {
|
|
2
28
|
path: string;
|
|
3
29
|
line?: number | undefined;
|
|
@@ -5,13 +31,37 @@ interface LimitableComment {
|
|
|
5
31
|
}
|
|
6
32
|
/**
|
|
7
33
|
* Split `comments` into those posted inline and those folded into the body.
|
|
8
|
-
* `limit` 0 or undefined means no limit.
|
|
9
|
-
*
|
|
34
|
+
* `limit` 0 or undefined means no limit. `severityOf` names the severity of the
|
|
35
|
+
* finding a comment answers, when it answers one. Both halves keep the input
|
|
36
|
+
* order, so the posted review reads in the order the reviewer wrote it.
|
|
10
37
|
*/
|
|
11
|
-
export declare function limitInlineComments<T extends LimitableComment>(comments: readonly T[], limit: number | undefined): {
|
|
38
|
+
export declare function limitInlineComments<T extends LimitableComment>(comments: readonly T[], limit: number | undefined, severityOf?: (c: T) => Severity | undefined): {
|
|
12
39
|
kept: T[];
|
|
13
40
|
overflow: T[];
|
|
14
41
|
};
|
|
15
|
-
/**
|
|
16
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The review-body note carrying the comments the limit folded, or "" for none.
|
|
44
|
+
* Each is kept whole while the note stays within its share of GitHub's body
|
|
45
|
+
* limit; past that, by place and first line (and its finding marker), and the
|
|
46
|
+
* note says so.
|
|
47
|
+
*/
|
|
48
|
+
export declare function formatOverflowNote<T extends LimitableComment>(overflow: readonly T[], limit: number, linkOf?: (c: T) => string | undefined): string;
|
|
49
|
+
/**
|
|
50
|
+
* The introduced findings an earlier review of this PR folded into its body —
|
|
51
|
+
* so they never became threads — that this head still introduces and this
|
|
52
|
+
* review does not dismiss. An approval past one would pass a finding the
|
|
53
|
+
* open-thread check cannot see. Markers count only in the engine's own reviews;
|
|
54
|
+
* one pasted elsewhere can only add a finding to this list, never remove one.
|
|
55
|
+
*/
|
|
56
|
+
export declare function foldedFindingsStillIntroduced(input: {
|
|
57
|
+
reviews: readonly {
|
|
58
|
+
user?: {
|
|
59
|
+
login?: string | null;
|
|
60
|
+
type?: string | null;
|
|
61
|
+
} | null;
|
|
62
|
+
body?: string | null;
|
|
63
|
+
}[];
|
|
64
|
+
delta: GateDelta;
|
|
65
|
+
dismissed: readonly Dismissal[];
|
|
66
|
+
}): DeltaFinding[];
|
|
17
67
|
export {};
|
|
@@ -15,6 +15,11 @@ export type CommentMarker = {
|
|
|
15
15
|
};
|
|
16
16
|
export declare function renderCommentMarker(marker: CommentMarker): string;
|
|
17
17
|
export declare function parseCommentMarker(body: string | null | undefined): CommentMarker | null;
|
|
18
|
+
/**
|
|
19
|
+
* The key of every finding marker in `body`. A review BODY carries one per
|
|
20
|
+
* comment the comment limit folded into it, where a comment carries one.
|
|
21
|
+
*/
|
|
22
|
+
export declare function findingKeysIn(body: string): string[];
|
|
18
23
|
/** The text of a comment's `suggestion` block, without its closing newline. */
|
|
19
24
|
export declare function suggestionText(body: string): string | undefined;
|
|
20
25
|
/**
|
package/dist/mcp/staleFix.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GitHubToolContext } from "./capabilityContext.js";
|
|
2
|
+
import type { OctokitWithPlugins } from "../utils/github/github.js";
|
|
2
3
|
/** true when `branch` is a Infraweaver remediation/generation branch. */
|
|
3
4
|
export declare function isRemediationBranch(branch: string): boolean;
|
|
4
5
|
/** the `<group-id>` of a `remediate/<group-id>` branch (the scan group id that
|
|
@@ -7,7 +8,8 @@ export declare function isRemediationBranch(branch: string): boolean;
|
|
|
7
8
|
export declare function groupIdFromBranch(branch: string): string | null;
|
|
8
9
|
/**
|
|
9
10
|
* What a remediation branch name carries after its group id: nothing when the
|
|
10
|
-
* PR targets the repository default branch, `--<base>` otherwise
|
|
11
|
+
* PR targets the repository default branch, `--<base>` otherwise — slugged for
|
|
12
|
+
* a ref, with a hash of the raw name when the slug changed it.
|
|
11
13
|
*
|
|
12
14
|
* The group id is a hash of the file (or rule) alone, so the same fix aimed at
|
|
13
15
|
* two base branches — or at two repos whose roots are both `main.tf` — named
|
|
@@ -16,6 +18,39 @@ export declare function groupIdFromBranch(branch: string): string | null;
|
|
|
16
18
|
* plain name, so the PRs already open under it are still found and refreshed.
|
|
17
19
|
*/
|
|
18
20
|
export declare function remediationBranchSuffix(base: string | undefined, defaultBranch: string | undefined): string;
|
|
21
|
+
/** an open remediation PR, as far as naming a group's branch needs it. */
|
|
22
|
+
export interface OpenRemediationPr {
|
|
23
|
+
head: string;
|
|
24
|
+
base: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The branch a remediation group's fix is pushed to: the head of the PR already
|
|
28
|
+
* open for this group into this exact base, when there is one, else
|
|
29
|
+
* `remediate/<id>` plus [[remediationBranchSuffix]].
|
|
30
|
+
*
|
|
31
|
+
* The open PR wins because its name may predate the current naming — opened
|
|
32
|
+
* before the base suffix existed, or under an unhashed suffix — and Remediate
|
|
33
|
+
* keys idempotency on the name, so a newly computed one opened a duplicate PR.
|
|
34
|
+
* A head that another open PR also proposes into a different base is never
|
|
35
|
+
* reused: pushing to it would rewrite that PR too.
|
|
36
|
+
*/
|
|
37
|
+
export declare function remediationBranchFor(id: string, base: string | undefined, defaultBranch: string | undefined, open: readonly OpenRemediationPr[] | undefined): string;
|
|
38
|
+
/**
|
|
39
|
+
* This repository's open `remediate/*` PRs, for [[remediationBranchFor]]. Empty
|
|
40
|
+
* without a GitHub client (a local session) or when the API call fails: the
|
|
41
|
+
* group then gets the computed name, as it did before open PRs were consulted.
|
|
42
|
+
* Only Remediate pushes to a group's branch, so no other mode pays for the call.
|
|
43
|
+
*/
|
|
44
|
+
export declare function listOpenRemediationPrs(ctx: {
|
|
45
|
+
octokit?: OctokitWithPlugins | undefined;
|
|
46
|
+
repo?: {
|
|
47
|
+
owner?: string;
|
|
48
|
+
name?: string;
|
|
49
|
+
} | undefined;
|
|
50
|
+
toolState?: {
|
|
51
|
+
selectedMode?: string | undefined;
|
|
52
|
+
} | undefined;
|
|
53
|
+
}): Promise<OpenRemediationPr[]>;
|
|
19
54
|
/** true when a commit-author LOGIN is Infraweaver's bot (so a commit by it is NOT
|
|
20
55
|
* a human edit). A null/absent login is treated as non-human here for backward
|
|
21
56
|
* compatibility of this login-only helper — but prefer {@link isBotCommit}, which
|
|
@@ -3,7 +3,7 @@ export declare const AWS_PRICE_REGION = "eu-west-2";
|
|
|
3
3
|
/** Currency of every price below. The bulk API quotes USD for every region. */
|
|
4
4
|
export declare const AWS_PRICE_CURRENCY = "USD";
|
|
5
5
|
/** ISO date the snapshot was captured — surfaced in cost.md so staleness is visible. */
|
|
6
|
-
export declare const AWS_PRICE_FETCHED_AT = "2026-
|
|
6
|
+
export declare const AWS_PRICE_FETCHED_AT = "2026-10-03";
|
|
7
7
|
/** meter key → list price per HOUR. */
|
|
8
8
|
export declare const AWS_HOURLY_PRICES: Record<string, number>;
|
|
9
9
|
/** meter key → list price per GB processed. Usage, so never part of a floor. */
|
|
@@ -3,6 +3,6 @@ export declare const AZURE_PRICE_REGION = "westeurope";
|
|
|
3
3
|
/** Currency of every price below. */
|
|
4
4
|
export declare const AZURE_PRICE_CURRENCY = "EUR";
|
|
5
5
|
/** ISO date the snapshot was captured — surfaced in cost.md so staleness is visible. */
|
|
6
|
-
export declare const AZURE_PRICE_FETCHED_AT = "2026-
|
|
6
|
+
export declare const AZURE_PRICE_FETCHED_AT = "2026-10-03";
|
|
7
7
|
/** meter key → list price per HOUR. */
|
|
8
8
|
export declare const AZURE_HOURLY_PRICES: Record<string, number>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LocalToolContext } from "../localContext.js";
|
|
2
|
+
import { type OpenRemediationPr } from "../staleFix.js";
|
|
2
3
|
import { type PrioritySignals, type SeedCapResult, type SeedCaps } from "./coverage.js";
|
|
3
4
|
import { annotateGroups, planBatches } from "./decisions.js";
|
|
4
5
|
import { type Concern, type Severity } from "./types.js";
|
|
@@ -59,4 +60,12 @@ export declare function buildConcernResult(ctx: LocalToolContext, rawConcerns: C
|
|
|
59
60
|
* the full raw set — so a capped concern the re-scan still sees can never
|
|
60
61
|
* read as a regression. */
|
|
61
62
|
seedCaps?: SeedCaps;
|
|
63
|
+
/** The repository's open remediation PRs (see `listOpenRemediationPrs`), so
|
|
64
|
+
* a group that already has one keeps its branch. */
|
|
65
|
+
openRemediationPrs?: readonly OpenRemediationPr[];
|
|
66
|
+
/** The concerns were scanned off the tree in `cwd` as it is now, so their
|
|
67
|
+
* lines name today's resources (see `recordConcernSites`). Only
|
|
68
|
+
* `terraform_scan` sets it: a findings file or external report may
|
|
69
|
+
* describe another commit. */
|
|
70
|
+
linesMatchTree?: boolean;
|
|
62
71
|
}): ConcernResult;
|
|
@@ -14,6 +14,9 @@ export interface DeltaVerdict {
|
|
|
14
14
|
gating: number;
|
|
15
15
|
/** introduced findings on a required control, any severity */
|
|
16
16
|
required: number;
|
|
17
|
+
/** introduced findings that fail either half, each counted once: a high
|
|
18
|
+
* finding on a required control is in both `gating` and `required`. */
|
|
19
|
+
blocking: number;
|
|
17
20
|
}
|
|
18
21
|
/** The gate's verdict on a delta: the same two halves `findings-gate` applies. */
|
|
19
22
|
export declare function deltaVerdict(delta: GateDelta, requiredControls: string[]): DeltaVerdict;
|
|
@@ -12,15 +12,21 @@
|
|
|
12
12
|
* WHAT IS READ. Every `name = "…"` line inside a `resource`, `data`,
|
|
13
13
|
* `provider`, `module` or `locals` block, at any depth, so
|
|
14
14
|
* `environment { variables = { auth = "…" } }` and `jsonencode({ … })` objects
|
|
15
|
-
* are reached;
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* are reached; the `default` of a `variable` whose name is a secret's; and
|
|
16
|
+
* every assignment in a `.tfvars` file, which is where a value kept out of the
|
|
17
|
+
* `.tf` source usually lands instead. Not read: comments, heredoc bodies, a
|
|
18
|
+
* name/value pair split across two keys (`[{ name = "PASSWORD", value = "…" }]`),
|
|
19
|
+
* and a value that starts on a later line than its name.
|
|
19
20
|
*
|
|
20
|
-
* WHAT IS NOT A FINDING. A reference, a function call, an interpolation
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* WHAT IS NOT A FINDING. A reference, a function call, an interpolation or a
|
|
22
|
+
* template directive, an empty string, `true` / `false` / `null`, a number, or
|
|
23
|
+
* a placeholder made only of `*` / `x` / `<…>`. Nor is a literal that only POINTS
|
|
24
|
+
* at a secret — an ARN, a GCP `projects/…/secrets/…` name, a Key Vault secret
|
|
25
|
+
* URI or an SSM parameter path — since that is the fix, not the fault. Neither
|
|
26
|
+
* is an argument whose name only mentions a secret (`password_length`,
|
|
27
|
+
* `secret_name`, `token_validity_units`, `creation_token`): the last word of the
|
|
28
|
+
* name decides, and a name that is `secret` and nothing else names a Secret
|
|
29
|
+
* Manager secret (`secret_key_ref { secret = "db-password" }`), not its value.
|
|
24
30
|
*
|
|
25
31
|
* Arguments the provider can take write-only (`aws_db_instance.password`) may
|
|
26
32
|
* also be reported by `secret-persisted-to-state`, which says something else:
|
|
@@ -45,5 +51,13 @@ export interface HardcodedSecret {
|
|
|
45
51
|
* parser is shared by every scanner and budgeted (see nativeScan.perf.test).
|
|
46
52
|
*/
|
|
47
53
|
export declare function findHardcodedSecrets(hcl: string): HardcodedSecret[];
|
|
48
|
-
/**
|
|
49
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Every secret-named literal in a `.tfvars` file. A tfvars file is nothing but
|
|
56
|
+
* assignments, so every one is read, at any depth, under the file's own name.
|
|
57
|
+
*/
|
|
58
|
+
export declare function findTfvarsSecrets(hcl: string, path: string): HardcodedSecret[];
|
|
59
|
+
/**
|
|
60
|
+
* Scan an already-read tree into native concerns. Pure; no I/O of its own.
|
|
61
|
+
* `tfvarsFiles` are the `.tfvars` inputs, read as assignments rather than blocks.
|
|
62
|
+
*/
|
|
63
|
+
export declare function scanHardcodedSecrets(files: readonly SourceFile[], tfvarsFiles?: readonly SourceFile[]): Concern[];
|
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* true for a path inside an `examples/` directory at any depth, or an `example/`
|
|
3
|
+
* one at the repository root. The registry's convention is `examples/`; the
|
|
4
|
+
* singular is accepted only where that convention puts it, because a module
|
|
5
|
+
* NAMED `example` (`modules/example/`) is code someone calls.
|
|
6
|
+
*/
|
|
2
7
|
export declare function isExamplePath(path: string): boolean;
|
|
3
8
|
/**
|
|
4
9
|
* Why the repository at `cwd` reads as a reusable module rather than a
|
|
5
10
|
* deployment, or null when it does not.
|
|
6
11
|
*/
|
|
7
12
|
export declare function libraryRepoReason(cwd: string): string | null;
|
|
8
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* For a library repository, why a candidate in `file` (repo-relative) is
|
|
15
|
+
* reported rather than restructured — or null when the file belongs to a
|
|
16
|
+
* directory, or sits under one, that deploys on its own. Outside a library,
|
|
17
|
+
* always null.
|
|
18
|
+
*/
|
|
19
|
+
export declare function libraryScope(cwd: string): (file: string) => string | null;
|
|
20
|
+
/** what a detector tool adds to a candidate in a library's own code. */
|
|
9
21
|
export declare function reportOnly(reason: string | null): {
|
|
10
22
|
report_only?: true;
|
|
11
23
|
report_only_reason?: string;
|
|
@@ -44,6 +44,8 @@ export interface RuleContext {
|
|
|
44
44
|
readonly files: readonly SourceFile[];
|
|
45
45
|
/** the `.tofu` half. Only the dialect rule needs it; empty on a repo with none. */
|
|
46
46
|
readonly tofuFiles: readonly SourceFile[];
|
|
47
|
+
/** the `.tfvars` inputs. Only the hardcoded-secret rule reads them; empty on a repo with none. */
|
|
48
|
+
readonly tfvarsFiles: readonly SourceFile[];
|
|
47
49
|
/** non-null when the walk was bounded before it finished — see [[tree]]. */
|
|
48
50
|
readonly truncated: TreeTruncation;
|
|
49
51
|
/** how many times a literal must repeat in one directory before it is reported. */
|
|
@@ -75,6 +77,7 @@ export interface RuleContextInput {
|
|
|
75
77
|
cwd: string;
|
|
76
78
|
files: readonly SourceFile[];
|
|
77
79
|
tofuFiles?: readonly SourceFile[];
|
|
80
|
+
tfvarsFiles?: readonly SourceFile[];
|
|
78
81
|
truncated?: TreeTruncation;
|
|
79
82
|
repeatedLiteralThreshold: number;
|
|
80
83
|
}
|
|
@@ -44,6 +44,8 @@ export interface ScanSession {
|
|
|
44
44
|
baselineFmtDriftFiles?: string[];
|
|
45
45
|
lastScanConcerns?: Concern[];
|
|
46
46
|
concernsById?: Map<string, Concern>;
|
|
47
|
+
siteById?: Map<string, string | null>;
|
|
48
|
+
firstSitesByKey?: Map<string, Set<string | null>>;
|
|
47
49
|
gateConcerns?: Concern[];
|
|
48
50
|
policyRules?: unknown[] | null;
|
|
49
51
|
policyGate?: PolicyGateResult;
|
|
@@ -102,6 +102,11 @@ export declare function readTfTree(cwd: string, limits?: TreeReadLimits): Source
|
|
|
102
102
|
* one is a fact about the PAIR.
|
|
103
103
|
*/
|
|
104
104
|
export declare function readTofuTree(cwd: string, limits?: TreeReadLimits): SourceTree;
|
|
105
|
+
/**
|
|
106
|
+
* The `.tfvars` inputs (`.auto.tfvars` included). Not configuration, so no rule
|
|
107
|
+
* reads them as blocks; the hardcoded-secret rule reads their assignments.
|
|
108
|
+
*/
|
|
109
|
+
export declare function readTfvarsTree(cwd: string, limits?: TreeReadLimits): SourceTree;
|
|
105
110
|
/** how a declined-for-size file reads in a coverage report. Names the files,
|
|
106
111
|
* because unlike a truncated walk this gap knows exactly what went unread. */
|
|
107
112
|
export declare function describeOversize(paths: readonly string[]): string;
|
|
@@ -243,7 +243,8 @@ export interface ConcernGroup {
|
|
|
243
243
|
* the file (by-file grouping) or the rule (by-rule grouping). */
|
|
244
244
|
id: string;
|
|
245
245
|
/** the branch its fix is pushed to: `remediate/<id>`, plus `--<base>` when the
|
|
246
|
-
* PR targets a branch other than the repository default
|
|
246
|
+
* PR targets a branch other than the repository default — or the head of the
|
|
247
|
+
* remediation PR already open for this group into the same base. */
|
|
247
248
|
branch?: string;
|
|
248
249
|
/** the group's primary file (by-file) or a human label like "3 files"
|
|
249
250
|
* (by-rule); `files` carries the full list for by-rule groups. */
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which findings a fix resolved where its rule still fires elsewhere in the file.
|
|
3
|
+
*
|
|
4
|
+
* terraform_verify_remediation keys a finding on (scanner, rule, file), so a
|
|
5
|
+
* rule fixed on one resource but still firing on another in the same file read
|
|
6
|
+
* as unfixed for both: AWSGoat's open-egress fix on two of four security groups
|
|
7
|
+
* verified as 0 of 5. Counting by resource instead risks the opposite error —
|
|
8
|
+
* a fix that moves the defect onto a new resource would look resolved — so a
|
|
9
|
+
* finding counts as resolved at its own site only when ALL of these hold:
|
|
10
|
+
*
|
|
11
|
+
* - it was placed on a resource when it was reported, by a scan of the tree
|
|
12
|
+
* as it then stood, and no later scan placed the same id elsewhere;
|
|
13
|
+
* - that resource is still declared in the file (a renamed or deleted one
|
|
14
|
+
* proves nothing);
|
|
15
|
+
* - the rule no longer fires on it;
|
|
16
|
+
* - every place the rule still fires in the file is a resource it already
|
|
17
|
+
* fired on before the fix (no new site, and no finding outside a resource).
|
|
18
|
+
*
|
|
19
|
+
* Anything else keeps the file-level answer, which under-claims and never
|
|
20
|
+
* over-claims.
|
|
21
|
+
*/
|
|
22
|
+
import type { ScanSession } from "./scanSession.js";
|
|
23
|
+
import { type Concern } from "./types.js";
|
|
24
|
+
/** A reader that returns each file under `cwd` once, or null when unreadable. */
|
|
25
|
+
export declare function fileReader(cwd: string): (file: string) => string | null;
|
|
26
|
+
/** The resource a concern sits in, read from the file as it is now. */
|
|
27
|
+
export declare function siteOf(c: Concern, read: (file: string) => string | null): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Remember where each reported concern sat, and the first sites per key.
|
|
30
|
+
*
|
|
31
|
+
* `linesMatchTree` says the concerns were scanned off the tree in `cwd` as it
|
|
32
|
+
* is now. Without it (a findings file or external report, which may describe
|
|
33
|
+
* another commit) their lines are not trusted to name today's resources: their
|
|
34
|
+
* ids get no site and their keys no first sites.
|
|
35
|
+
*/
|
|
36
|
+
export declare function recordConcernSites(session: ScanSession, concerns: Concern[], cwd: string, linesMatchTree: boolean): void;
|
|
37
|
+
export interface SiteRequest {
|
|
38
|
+
id: string;
|
|
39
|
+
/** the (scanner, rule, file) key */
|
|
40
|
+
key: string;
|
|
41
|
+
/** the resource address the finding sat in when reported, or null */
|
|
42
|
+
site: string | null;
|
|
43
|
+
file: string | undefined;
|
|
44
|
+
}
|
|
45
|
+
export declare function resolvedAtSite(params: {
|
|
46
|
+
requested: readonly SiteRequest[];
|
|
47
|
+
/** every finding the re-scan reports, with the resource it sits in */
|
|
48
|
+
current: readonly {
|
|
49
|
+
key: string;
|
|
50
|
+
site: string | null;
|
|
51
|
+
}[];
|
|
52
|
+
/** per key, the resources the rule fired on in the first scan of the run */
|
|
53
|
+
firstSites: ReadonlyMap<string, ReadonlySet<string | null>>;
|
|
54
|
+
/** the resource addresses a file declares now */
|
|
55
|
+
declared: (file: string) => ReadonlySet<string>;
|
|
56
|
+
}): string[];
|
|
@@ -44,10 +44,11 @@ export declare class TokenQuotaExceededError extends Error {
|
|
|
44
44
|
}
|
|
45
45
|
/** The workflow input that overrides {@link DEFAULT_RUN_TOKEN_QUOTA}. */
|
|
46
46
|
export declare const RUN_TOKEN_QUOTA_INPUT = "max_run_tokens";
|
|
47
|
-
/** The four token components a run is charged for. Cache reads
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
47
|
+
/** The four token components a run is charged for. Cache reads still count —
|
|
48
|
+
* a runaway loop is visible mostly in cache reads, and excluding them would
|
|
49
|
+
* leave the loudest signal of the very failure this bounds outside the count —
|
|
50
|
+
* but at {@link CACHE_READ_WEIGHT}, their price, so a long healthy run that
|
|
51
|
+
* re-reads a large cached prompt every turn is not stopped for it. */
|
|
51
52
|
interface TokenComponents {
|
|
52
53
|
input?: number | undefined;
|
|
53
54
|
output?: number | undefined;
|
|
@@ -55,9 +56,9 @@ interface TokenComponents {
|
|
|
55
56
|
cacheWrite?: number | undefined;
|
|
56
57
|
}
|
|
57
58
|
export interface RunTokenQuota {
|
|
58
|
-
/** The ceiling, in
|
|
59
|
+
/** The ceiling, in quota tokens (a cache read counts as a tenth). */
|
|
59
60
|
readonly limit: number;
|
|
60
|
-
/**
|
|
61
|
+
/** Quota tokens charged so far across every source. */
|
|
61
62
|
used(): number;
|
|
62
63
|
/**
|
|
63
64
|
* Report a source's running TOTAL (not a delta) and return whether the quota
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infraweaver",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
4
4
|
"description": "GitHub Action that remediates Terraform to best practices and opens one scoped pull request per concern.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-coding-agent",
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"@infraweaver-io/ci": "^0.1.8",
|
|
104
104
|
"@infraweaver-io/core": "^0.1.5",
|
|
105
105
|
"@infraweaver-io/crosswalk": "^1.2.0",
|
|
106
|
-
"@infraweaver-io/models": "^0.1.
|
|
106
|
+
"@infraweaver-io/models": "^0.1.7",
|
|
107
107
|
"@infraweaver-io/policy-spec": "^1.2.0",
|
|
108
108
|
"@infraweaver-io/report-contract": "^0.1.5",
|
|
109
109
|
"@infraweaver-io/sandbox": "^0.1.8",
|
package/src/agents/opencode.ts
CHANGED
|
@@ -417,6 +417,12 @@ export interface TurnAccumulator {
|
|
|
417
417
|
tokens: {
|
|
418
418
|
input: number;
|
|
419
419
|
output: number;
|
|
420
|
+
/**
|
|
421
|
+
* opencode reports reasoning apart from `output` (which excludes it). Kept
|
|
422
|
+
* separate so it reaches the token quota without changing the reported
|
|
423
|
+
* output count; cost already prices it, via opencode's per-step `cost`.
|
|
424
|
+
*/
|
|
425
|
+
reasoning: number;
|
|
420
426
|
cacheRead: number;
|
|
421
427
|
cacheWrite: number;
|
|
422
428
|
};
|
|
@@ -439,7 +445,7 @@ export function newTurn(): TurnAccumulator {
|
|
|
439
445
|
turnCounter += 1;
|
|
440
446
|
return {
|
|
441
447
|
finalText: "",
|
|
442
|
-
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
448
|
+
tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0 },
|
|
443
449
|
costUsd: 0,
|
|
444
450
|
sessionError: null,
|
|
445
451
|
lastToolError: null,
|
|
@@ -739,6 +745,7 @@ async function onPartUpdated(ctx: RunnerContext, part: Part): Promise<void> {
|
|
|
739
745
|
if (t) {
|
|
740
746
|
ctx.currentTurn.tokens.input += t.input || 0;
|
|
741
747
|
ctx.currentTurn.tokens.output += t.output || 0;
|
|
748
|
+
ctx.currentTurn.tokens.reasoning += t.reasoning || 0;
|
|
742
749
|
ctx.currentTurn.tokens.cacheRead += t.cache?.read || 0;
|
|
743
750
|
ctx.currentTurn.tokens.cacheWrite += t.cache?.write || 0;
|
|
744
751
|
}
|
|
@@ -15,11 +15,17 @@ const CACHE_READ_WEIGHT = 0.1;
|
|
|
15
15
|
export interface TokenUsage {
|
|
16
16
|
input: number;
|
|
17
17
|
output: number;
|
|
18
|
+
/**
|
|
19
|
+
* Reasoning tokens a harness reports apart from `output`. opencode does (its
|
|
20
|
+
* `output` excludes them, its cost prices them as output); Anthropic's
|
|
21
|
+
* `output_tokens` already include thinking, so the Claude harness has none.
|
|
22
|
+
*/
|
|
23
|
+
reasoning?: number;
|
|
18
24
|
cacheRead: number;
|
|
19
25
|
cacheWrite: number;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
/** The pre-summed total the run's token quota observes. */
|
|
23
29
|
export function quotaTokens(t: TokenUsage): number {
|
|
24
|
-
return t.input + t.output + t.cacheWrite + t.cacheRead * CACHE_READ_WEIGHT;
|
|
30
|
+
return t.input + t.output + (t.reasoning ?? 0) + t.cacheWrite + t.cacheRead * CACHE_READ_WEIGHT;
|
|
25
31
|
}
|