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
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { createSign } from "node:crypto";
|
|
2
1
|
import { rename, writeFile } from "node:fs/promises";
|
|
3
2
|
import { dirname, join } from "node:path";
|
|
4
3
|
import * as core from "@actions/core";
|
|
5
4
|
import { errorMessage } from "@infraweaver-io/core/errors";
|
|
6
5
|
import { retry } from "@infraweaver-io/core/retry";
|
|
6
|
+
import { createAppAuth } from "@octokit/auth-app";
|
|
7
7
|
import { throttling } from "@octokit/plugin-throttling";
|
|
8
8
|
import { Octokit } from "@octokit/rest";
|
|
9
9
|
import { DOC_LINKS, PRODUCT_NAME, PRODUCT_SLUG } from "#app/brand";
|
|
10
10
|
import { apiFetch } from "#app/utils/cloud/apiFetch";
|
|
11
11
|
import { cloudOidcAudience } from "#app/utils/cloud/apiUrl";
|
|
12
|
+
import { DEFAULT_CLOUD_TIMEOUT_MS } from "#app/utils/cloud/cloudFetchCore";
|
|
12
13
|
|
|
13
14
|
function isObject(value: unknown) {
|
|
14
15
|
return typeof value === "object" && value !== null;
|
|
@@ -32,37 +33,6 @@ interface InstallationToken {
|
|
|
32
33
|
owner?: string;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
interface GitHubAppConfig {
|
|
36
|
-
appId: string;
|
|
37
|
-
privateKey: string;
|
|
38
|
-
repoOwner: string;
|
|
39
|
-
repoName: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface Installation {
|
|
43
|
-
id: number;
|
|
44
|
-
account: {
|
|
45
|
-
login: string;
|
|
46
|
-
type: string;
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface Repository {
|
|
51
|
-
owner: {
|
|
52
|
-
login: string;
|
|
53
|
-
};
|
|
54
|
-
name: string;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
interface InstallationTokenResponse {
|
|
58
|
-
token: string;
|
|
59
|
-
expires_at: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface RepositoriesResponse {
|
|
63
|
-
repositories: Repository[];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
36
|
function isOIDCAvailable(): boolean {
|
|
67
37
|
// OIDC requires both env vars to be set (only in real GitHub Actions with id-token permission)
|
|
68
38
|
return Boolean(
|
|
@@ -176,11 +146,8 @@ async function acquireTokenViaOIDC(opts?: AcquireTokenOptions): Promise<string>
|
|
|
176
146
|
}
|
|
177
147
|
const reposParam = repos.length ? `?repos=${repos.join(",")}` : "";
|
|
178
148
|
|
|
179
|
-
const timeoutMs = 30000;
|
|
180
|
-
const controller = new AbortController();
|
|
181
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
182
|
-
|
|
183
149
|
try {
|
|
150
|
+
// no `signal`: apiFetch applies the cloud transport's default timeout
|
|
184
151
|
const tokenResponse = await apiFetch({
|
|
185
152
|
path: `/api/github/installation-token${reposParam}`,
|
|
186
153
|
method: "POST",
|
|
@@ -189,11 +156,8 @@ async function acquireTokenViaOIDC(opts?: AcquireTokenOptions): Promise<string>
|
|
|
189
156
|
"Content-Type": "application/json",
|
|
190
157
|
},
|
|
191
158
|
body: opts?.permissions ? JSON.stringify({ permissions: opts.permissions }) : undefined,
|
|
192
|
-
signal: controller.signal,
|
|
193
159
|
});
|
|
194
160
|
|
|
195
|
-
clearTimeout(timeoutId);
|
|
196
|
-
|
|
197
161
|
if (!tokenResponse.ok) {
|
|
198
162
|
// prefer the server-side `error` field — it's the single source of
|
|
199
163
|
// truth for the install URL (uses GITHUB_APP_INSTALL_URL, or the
|
|
@@ -216,82 +180,23 @@ async function acquireTokenViaOIDC(opts?: AcquireTokenOptions): Promise<string>
|
|
|
216
180
|
const tokenData = (await tokenResponse.json()) as InstallationToken;
|
|
217
181
|
return tokenData.token;
|
|
218
182
|
} catch (error) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (error instanceof Error && error.name === "
|
|
222
|
-
throw new Error(`Token exchange timed out after ${
|
|
183
|
+
// the default timeout's AbortSignal.timeout() rejects with a TimeoutError;
|
|
184
|
+
// the message carries "timed out" so acquireNewToken's retry policy matches it
|
|
185
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
186
|
+
throw new Error(`Token exchange timed out after ${DEFAULT_CLOUD_TIMEOUT_MS}ms`);
|
|
223
187
|
}
|
|
224
188
|
throw error;
|
|
225
189
|
}
|
|
226
190
|
}
|
|
227
191
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const generateJWT = (appId: string, privateKey: string): string => {
|
|
237
|
-
const now = Math.floor(Date.now() / 1000);
|
|
238
|
-
const payload = {
|
|
239
|
-
iat: now - 60,
|
|
240
|
-
exp: now + 5 * 60,
|
|
241
|
-
iss: appId,
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
const header = {
|
|
245
|
-
alg: "RS256",
|
|
246
|
-
typ: "JWT",
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
250
|
-
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
|
251
|
-
const signaturePart = `${encodedHeader}.${encodedPayload}`;
|
|
252
|
-
|
|
253
|
-
const signature = createSign("RSA-SHA256")
|
|
254
|
-
.update(signaturePart)
|
|
255
|
-
.sign(privateKey, "base64")
|
|
256
|
-
.replace(/\+/g, "-")
|
|
257
|
-
.replace(/\//g, "_")
|
|
258
|
-
.replace(/=/g, "");
|
|
259
|
-
|
|
260
|
-
return `${signaturePart}.${signature}`;
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
const githubRequest = async <T>(
|
|
264
|
-
path: string,
|
|
265
|
-
options: {
|
|
266
|
-
method?: string;
|
|
267
|
-
headers?: Record<string, string>;
|
|
268
|
-
body?: string;
|
|
269
|
-
} = {},
|
|
270
|
-
): Promise<T> => {
|
|
271
|
-
const { method = "GET", headers = {}, body } = options;
|
|
272
|
-
|
|
273
|
-
const url = `https://api.github.com${path}`;
|
|
274
|
-
const requestHeaders = {
|
|
275
|
-
Accept: "application/vnd.github.v3+json",
|
|
276
|
-
"User-Agent": `${PRODUCT_NAME}-Installation-Token-Generator/1.0`,
|
|
277
|
-
...headers,
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
const response = await fetch(url, {
|
|
281
|
-
method,
|
|
282
|
-
headers: requestHeaders,
|
|
283
|
-
...(body && { body }),
|
|
284
|
-
signal: AbortSignal.timeout(30_000),
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
if (!response.ok) {
|
|
288
|
-
const errorText = await response.text();
|
|
289
|
-
throw new Error(
|
|
290
|
-
`GitHub API request failed: ${response.status} ${response.statusText}\n${errorText}`,
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return response.json() as T;
|
|
192
|
+
// the local-dev App path talks to api.github.com directly; bound every request
|
|
193
|
+
// the way the cloud transport bounds its own.
|
|
194
|
+
const appRequestDefaults = {
|
|
195
|
+
userAgent: `${PRODUCT_NAME}-Installation-Token-Generator/1.0`,
|
|
196
|
+
request: {
|
|
197
|
+
fetch: (url: string, init: RequestInit) =>
|
|
198
|
+
fetch(url, { ...init, signal: AbortSignal.timeout(30_000) }),
|
|
199
|
+
},
|
|
295
200
|
};
|
|
296
201
|
|
|
297
202
|
const checkRepositoryAccess = async (
|
|
@@ -300,13 +205,13 @@ const checkRepositoryAccess = async (
|
|
|
300
205
|
repoName: string,
|
|
301
206
|
): Promise<boolean> => {
|
|
302
207
|
try {
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
208
|
+
const { data } = await new Octokit({ ...appRequestDefaults, auth: token }).request(
|
|
209
|
+
"GET /installation/repositories",
|
|
210
|
+
);
|
|
306
211
|
|
|
307
212
|
const ownerLower = repoOwner.toLowerCase();
|
|
308
213
|
const nameLower = repoName.toLowerCase();
|
|
309
|
-
return
|
|
214
|
+
return data.repositories.some(
|
|
310
215
|
(repo) =>
|
|
311
216
|
repo.owner.login.toLowerCase() === ownerLower && repo.name.toLowerCase() === nameLower,
|
|
312
217
|
);
|
|
@@ -316,31 +221,18 @@ const checkRepositoryAccess = async (
|
|
|
316
221
|
};
|
|
317
222
|
|
|
318
223
|
const createInstallationToken = async (
|
|
319
|
-
|
|
224
|
+
app: Octokit,
|
|
320
225
|
installationId: number,
|
|
321
226
|
permissions?: GitHubAppPermissions,
|
|
322
227
|
): Promise<string> => {
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
headers: Record<string, string>;
|
|
326
|
-
body?: string;
|
|
327
|
-
} = {
|
|
328
|
-
method: "POST",
|
|
329
|
-
headers: { Authorization: `Bearer ${jwt}` },
|
|
228
|
+
const { token } = (await app.auth({ type: "installation", installationId, permissions })) as {
|
|
229
|
+
token: string;
|
|
330
230
|
};
|
|
331
|
-
|
|
332
|
-
requestOpts.body = JSON.stringify({ permissions });
|
|
333
|
-
}
|
|
334
|
-
const response = await githubRequest<InstallationTokenResponse>(
|
|
335
|
-
`/app/installations/${installationId}/access_tokens`,
|
|
336
|
-
requestOpts,
|
|
337
|
-
);
|
|
338
|
-
|
|
339
|
-
return response.token;
|
|
231
|
+
return token;
|
|
340
232
|
};
|
|
341
233
|
|
|
342
234
|
const findInstallationId = async (
|
|
343
|
-
|
|
235
|
+
app: Octokit,
|
|
344
236
|
repoOwner: string,
|
|
345
237
|
repoName: string,
|
|
346
238
|
): Promise<number> => {
|
|
@@ -352,10 +244,10 @@ const findInstallationId = async (
|
|
|
352
244
|
// repo, and the loop's per-installation errors are what produce the useful
|
|
353
245
|
// diagnosis in that case.
|
|
354
246
|
try {
|
|
355
|
-
const direct = await
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
);
|
|
247
|
+
const { data: direct } = await app.request("GET /repos/{owner}/{repo}/installation", {
|
|
248
|
+
owner: repoOwner,
|
|
249
|
+
repo: repoName,
|
|
250
|
+
});
|
|
359
251
|
if (Number.isInteger(direct?.id)) return direct.id;
|
|
360
252
|
} catch {
|
|
361
253
|
// Not installed on this repo, or the endpoint is unavailable — fall through
|
|
@@ -363,9 +255,7 @@ const findInstallationId = async (
|
|
|
363
255
|
// selection includes it and which produces the better error message.
|
|
364
256
|
}
|
|
365
257
|
|
|
366
|
-
const installations = await
|
|
367
|
-
headers: { Authorization: `Bearer ${jwt}` },
|
|
368
|
-
});
|
|
258
|
+
const { data: installations } = await app.request("GET /app/installations");
|
|
369
259
|
|
|
370
260
|
// Probing each installation is best-effort — one that legitimately can't mint a
|
|
371
261
|
// token (suspended, revoked) must not abort the search. But swallowing the error
|
|
@@ -376,7 +266,7 @@ const findInstallationId = async (
|
|
|
376
266
|
let lastError: unknown;
|
|
377
267
|
for (const installation of installations) {
|
|
378
268
|
try {
|
|
379
|
-
const tempToken = await createInstallationToken(
|
|
269
|
+
const tempToken = await createInstallationToken(app, installation.id);
|
|
380
270
|
const hasAccess = await checkRepositoryAccess(tempToken, repoOwner, repoName);
|
|
381
271
|
|
|
382
272
|
if (hasAccess) {
|
|
@@ -407,16 +297,18 @@ async function acquireTokenViaGitHubApp(opts?: AcquireTokenOptions): Promise<str
|
|
|
407
297
|
|
|
408
298
|
const repoContext = parseRepoContext();
|
|
409
299
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
300
|
+
// createAppAuth signs the App JWT for the /app/* and /repos/{owner}/{repo}/installation
|
|
301
|
+
// routes and mints installation tokens through `app.auth()`.
|
|
302
|
+
const app = new Octokit({
|
|
303
|
+
...appRequestDefaults,
|
|
304
|
+
authStrategy: createAppAuth,
|
|
305
|
+
auth: {
|
|
306
|
+
appId: process.env.GITHUB_APP_ID,
|
|
307
|
+
privateKey: process.env.GITHUB_PRIVATE_KEY.replace(/\\n/g, "\n"),
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
const installationId = await findInstallationId(app, repoContext.owner, repoContext.name);
|
|
311
|
+
return await createInstallationToken(app, installationId, opts?.permissions);
|
|
420
312
|
}
|
|
421
313
|
|
|
422
314
|
/**
|
|
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import * as core from "@actions/core";
|
|
3
3
|
import { errorMessage } from "@infraweaver-io/core/errors";
|
|
4
4
|
import { onExitSignal } from "@infraweaver-io/sandbox/exitHandler";
|
|
5
|
+
import { Octokit } from "@octokit/rest";
|
|
5
6
|
import type { PushPermission } from "#app/external";
|
|
6
7
|
import { log } from "#app/utils/cli";
|
|
7
8
|
import { stashKeylessOidcCreds } from "#app/utils/cloud/keylessOidc";
|
|
@@ -260,18 +261,12 @@ export function getGitHubInstallationToken(): string {
|
|
|
260
261
|
}
|
|
261
262
|
|
|
262
263
|
export async function revokeGitHubInstallationToken(token: string): Promise<void> {
|
|
263
|
-
const apiUrl = process.env.GITHUB_API_URL || "https://api.github.com";
|
|
264
|
-
|
|
265
264
|
try {
|
|
266
|
-
await
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
272
|
-
},
|
|
273
|
-
signal: AbortSignal.timeout(10_000),
|
|
274
|
-
});
|
|
265
|
+
await new Octokit({
|
|
266
|
+
auth: token,
|
|
267
|
+
baseUrl: process.env.GITHUB_API_URL || "https://api.github.com",
|
|
268
|
+
request: { signal: AbortSignal.timeout(10_000) },
|
|
269
|
+
}).rest.apps.revokeInstallationAccessToken();
|
|
275
270
|
log.debug("» installation token revoked");
|
|
276
271
|
} catch (error) {
|
|
277
272
|
log.info(`Failed to revoke installation token: ${errorMessage(error)}`);
|
package/src/utils/log.ts
CHANGED
|
@@ -6,8 +6,10 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
6
6
|
import * as core from "@actions/core";
|
|
7
7
|
import { setLogger } from "@infraweaver-io/core/logger";
|
|
8
8
|
import { type AgentUsage, formatCostUsd } from "@infraweaver-io/models/usage";
|
|
9
|
+
import pc from "picocolors";
|
|
9
10
|
import { table } from "table";
|
|
10
11
|
import { isGitHubActions, isInsideDocker } from "#app/utils/globals";
|
|
12
|
+
import { tableCell } from "#app/utils/markdownTable";
|
|
11
13
|
import { redactSecrets } from "#app/utils/redact";
|
|
12
14
|
|
|
13
15
|
// --- log prefix via AsyncLocalStorage ---
|
|
@@ -16,9 +18,6 @@ type LogContext = { prefix: string };
|
|
|
16
18
|
|
|
17
19
|
const logContext = new AsyncLocalStorage<LogContext>();
|
|
18
20
|
|
|
19
|
-
const MAGENTA = "\x1b[35m";
|
|
20
|
-
const RESET = "\x1b[0m";
|
|
21
|
-
|
|
22
21
|
/** run `fn` with every log line prefixed by `prefix` (e.g. "[task-label]") in magenta */
|
|
23
22
|
export function withLogPrefix<T>(prefix: string, fn: () => Promise<T>): Promise<T> {
|
|
24
23
|
return logContext.run({ prefix }, fn);
|
|
@@ -27,7 +26,7 @@ export function withLogPrefix<T>(prefix: string, fn: () => Promise<T>): Promise<
|
|
|
27
26
|
function prefixLines(message: string): string {
|
|
28
27
|
const ctx = logContext.getStore();
|
|
29
28
|
if (!ctx) return message;
|
|
30
|
-
const colored = `${
|
|
29
|
+
const colored = `${pc.magenta(ctx.prefix)} `;
|
|
31
30
|
return message
|
|
32
31
|
.split("\n")
|
|
33
32
|
.map((line) => `${colored}${line}`)
|
|
@@ -439,7 +438,7 @@ export function formatUsageSummary(entries: AgentUsage[]): string {
|
|
|
439
438
|
|
|
440
439
|
const rows = entries.map(
|
|
441
440
|
(e) =>
|
|
442
|
-
`| ${e.agent} | ${fmt(nonCachedInput(e))} | ${fmt(e.cacheReadTokens ?? 0)} | ${fmt(e.cacheWriteTokens ?? 0)} | ${fmt(e.outputTokens)} | ${fmt(totalFor(e))} | ${costCell(e)} |`,
|
|
441
|
+
`| ${tableCell(e.agent)} | ${fmt(nonCachedInput(e))} | ${fmt(e.cacheReadTokens ?? 0)} | ${fmt(e.cacheWriteTokens ?? 0)} | ${fmt(e.outputTokens)} | ${fmt(totalFor(e))} | ${costCell(e)} |`,
|
|
443
442
|
);
|
|
444
443
|
|
|
445
444
|
const totalsRows: string[] = [];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flatten text into one markdown table cell: escape pipes (an unescaped `|`
|
|
3
|
+
* starts a new column and shears the row) and fold newlines, which would end the
|
|
4
|
+
* row, into single spaces.
|
|
5
|
+
*/
|
|
6
|
+
export function tableCell(text: string): string {
|
|
7
|
+
return text
|
|
8
|
+
.replaceAll("|", "\\|")
|
|
9
|
+
.replaceAll(/\s*\n\s*/g, " ")
|
|
10
|
+
.trim();
|
|
11
|
+
}
|
|
@@ -2,6 +2,7 @@ import { writeFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { $ } from "@infraweaver-io/sandbox/shell";
|
|
4
4
|
import { log } from "#app/utils/cli";
|
|
5
|
+
import { parseDiffLines } from "#app/utils/unifiedDiff";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Deterministic change-impact leads for Terraform review.
|
|
@@ -75,27 +76,27 @@ const TF_DECL_PATTERNS: Array<{
|
|
|
75
76
|
}> = [
|
|
76
77
|
{
|
|
77
78
|
declaration: "resource",
|
|
78
|
-
re: /^resource\s+"([A-Za-z0-9_]+)"
|
|
79
|
+
re: /^resource\s+"?([A-Za-z0-9_]+)"?\s+"?([A-Za-z0-9_-]+)"?/,
|
|
79
80
|
build: (m) => ({ label: `${m[1]}.${m[2]}`, ref: `${m[1]}.${m[2]}` }),
|
|
80
81
|
},
|
|
81
82
|
{
|
|
82
83
|
declaration: "data",
|
|
83
|
-
re: /^data\s+"([A-Za-z0-9_]+)"
|
|
84
|
+
re: /^data\s+"?([A-Za-z0-9_]+)"?\s+"?([A-Za-z0-9_-]+)"?/,
|
|
84
85
|
build: (m) => ({ label: `data.${m[1]}.${m[2]}`, ref: `data.${m[1]}.${m[2]}` }),
|
|
85
86
|
},
|
|
86
87
|
{
|
|
87
88
|
declaration: "variable",
|
|
88
|
-
re: /^variable\s+"([A-Za-z0-9_-]+)"
|
|
89
|
+
re: /^variable\s+"?([A-Za-z0-9_-]+)"?/,
|
|
89
90
|
build: (m) => ({ label: `var.${m[1]}`, ref: `var.${m[1]}` }),
|
|
90
91
|
},
|
|
91
92
|
{
|
|
92
93
|
declaration: "module",
|
|
93
|
-
re: /^module\s+"([A-Za-z0-9_-]+)"
|
|
94
|
+
re: /^module\s+"?([A-Za-z0-9_-]+)"?/,
|
|
94
95
|
build: (m) => ({ label: `module.${m[1]}`, ref: `module.${m[1]}` }),
|
|
95
96
|
},
|
|
96
97
|
{
|
|
97
98
|
declaration: "output",
|
|
98
|
-
re: /^output\s+"([A-Za-z0-9_-]+)"
|
|
99
|
+
re: /^output\s+"?([A-Za-z0-9_-]+)"?/,
|
|
99
100
|
// an output is consumed as `module.<caller>.<name>`; grep that access form as
|
|
100
101
|
// a regex rather than the bare `.<name>`, which would match every attribute
|
|
101
102
|
// access named `<name>` in the tree (for common names like `id`/`arn` that is
|
|
@@ -143,34 +144,25 @@ export function extractTerraformAtoms(files: ChangedFile[]): {
|
|
|
143
144
|
if (!file.patch) continue;
|
|
144
145
|
// only Terraform declarations matter here.
|
|
145
146
|
if (!/\.(tf|tfvars)$/.test(file.filename)) continue;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (patchLine.startsWith("+")) {
|
|
156
|
-
const decl = declarationOf(patchLine.slice(1));
|
|
157
|
-
const where: ChangeLocation = { path: file.filename, line: newLine, kind: "added" };
|
|
158
|
-
addedLines.add(locationKey(file.filename, newLine));
|
|
147
|
+
for (const patchLine of parseDiffLines(file.patch)) {
|
|
148
|
+
if (patchLine.kind === "add") {
|
|
149
|
+
const decl = declarationOf(patchLine.text);
|
|
150
|
+
const where: ChangeLocation = {
|
|
151
|
+
path: file.filename,
|
|
152
|
+
line: patchLine.newLine,
|
|
153
|
+
kind: "added",
|
|
154
|
+
};
|
|
155
|
+
addedLines.add(locationKey(file.filename, patchLine.newLine));
|
|
159
156
|
if (decl && !byRef.has(decl.ref)) byRef.set(decl.ref, { ...decl, where });
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const decl = declarationOf(patchLine.slice(1));
|
|
157
|
+
} else if (patchLine.kind === "remove") {
|
|
158
|
+
const decl = declarationOf(patchLine.text);
|
|
163
159
|
const oldPath = file.previous_filename ?? file.filename;
|
|
164
160
|
if (decl && !byRef.has(decl.ref)) {
|
|
165
161
|
byRef.set(decl.ref, {
|
|
166
162
|
...decl,
|
|
167
|
-
where: { path: oldPath, line: oldLine, kind: "removed" },
|
|
163
|
+
where: { path: oldPath, line: patchLine.oldLine, kind: "removed" },
|
|
168
164
|
});
|
|
169
165
|
}
|
|
170
|
-
oldLine++;
|
|
171
|
-
} else if (!patchLine.startsWith("\\")) {
|
|
172
|
-
oldLine++;
|
|
173
|
-
newLine++;
|
|
174
166
|
}
|
|
175
167
|
}
|
|
176
168
|
}
|
|
@@ -421,7 +421,7 @@ When embedding images (e.g. uploaded screenshots) in comments or PR bodies, alwa
|
|
|
421
421
|
|
|
422
422
|
**Your raw assistant messages are never delivered** — they exist only in the run logs. Anything the user is meant to see (an answer to a question, a mention reply, a result) MUST go through \`report_progress\` (or another ${infraweaverMcpName} write tool). Do not rely on returning the answer as plain text — the harness makes a best-effort attempt to recover it into the progress comment, but that is a safety net, not a substitute for calling \`report_progress\`.
|
|
423
423
|
|
|
424
|
-
**\`report_progress\`**: call this exactly once at the end of every run with a brief final summary (1-3 sentences) unless the mode guidance instructs otherwise. Never call it for intermediate status updates (e.g., "Checking for changes...", "Starting review...") — the task list handles live progress automatically. Calling \`report_progress\` replaces the task list with your summary and preserves the current task list in a collapsible section. Keep the summary concise — do not repeat what the task list already shows. Focus on the outcome (what was accomplished, links to artifacts) rather than listing individual steps. If something failed, include the tool's error text even when that makes the summary longer.
|
|
424
|
+
**\`report_progress\`**: call this exactly once at the end of every run, with its \`body\` set to a brief final summary (1-3 sentences) unless the mode guidance instructs otherwise. Never call it for intermediate status updates (e.g., "Checking for changes...", "Starting review...") — the task list handles live progress automatically. Calling \`report_progress\` replaces the task list with your summary and preserves the current task list in a collapsible section. Keep the summary concise — do not repeat what the task list already shows. Focus on the outcome (what was accomplished, links to artifacts) rather than listing individual steps. If something failed, include the tool's error text even when that makes the summary longer.
|
|
425
425
|
|
|
426
426
|
Never use \`create_issue_comment\` for task progress — that creates duplicate comments and leaves the progress comment stuck in its initial state. \`create_issue_comment\` is only for standalone comments unrelated to your current task. Plan output (initial post AND revisions) goes through \`report_progress\` — see the Plan mode guidance for details.
|
|
427
427
|
|
|
@@ -116,7 +116,7 @@ const REFACTOR_SOURCE_DIRECTIVES: Readonly<Record<RefactorSource, string>> = {
|
|
|
116
116
|
].join("\n"),
|
|
117
117
|
"same-repo": [
|
|
118
118
|
"Use ONLY a module that already exists in THIS repository (variant b.2 — a local",
|
|
119
|
-
"
|
|
119
|
+
"module called by its `call_source`, candidate `kind: local`). Do not author a new module, reach into",
|
|
120
120
|
"another repo, or adopt a registry module. If no suitable in-repo module matches the",
|
|
121
121
|
"cluster, report that and stop — do not fall back to another variant.",
|
|
122
122
|
].join("\n"),
|
|
@@ -55,7 +55,7 @@ export interface ToolSelectionFlags {
|
|
|
55
55
|
* via its dedicated input. Listing keeps their long-standing behaviour intact
|
|
56
56
|
* while the unified list can also enable/disable them.
|
|
57
57
|
*/
|
|
58
|
-
const FLAG_OPT_IN: ReadonlySet<ToolId> = new Set<ToolId>(["gitleaks", "terratest"]);
|
|
58
|
+
const FLAG_OPT_IN: ReadonlySet<ToolId> = new Set<ToolId>(["gitleaks", "betterleaks", "terratest"]);
|
|
59
59
|
|
|
60
60
|
/** map a token to a canonical tool id (case-insensitive; common spellings). */
|
|
61
61
|
const TOKEN_ALIASES: Readonly<Record<string, ToolId>> = {
|
|
@@ -69,6 +69,7 @@ const TOKEN_ALIASES: Readonly<Record<string, ToolId>> = {
|
|
|
69
69
|
infracost: "infracost",
|
|
70
70
|
cost: "infracost",
|
|
71
71
|
gitleaks: "gitleaks",
|
|
72
|
+
betterleaks: "betterleaks",
|
|
72
73
|
conftest: "conftest",
|
|
73
74
|
opa: "conftest",
|
|
74
75
|
policy: "conftest",
|
|
@@ -177,7 +178,13 @@ function decide(id: ToolId, flags: ToolSelectionFlags): Verdict {
|
|
|
177
178
|
if (FLAG_OPT_IN.has(id)) {
|
|
178
179
|
return flagFor(id, flags)
|
|
179
180
|
? { on: true }
|
|
180
|
-
: {
|
|
181
|
+
: {
|
|
182
|
+
on: false,
|
|
183
|
+
reason:
|
|
184
|
+
id === "betterleaks"
|
|
185
|
+
? `opt-in — name "${id}" in tools_enabled`
|
|
186
|
+
: `opt-in — set the ${id} input or name "${id}" in tools_enabled`,
|
|
187
|
+
};
|
|
181
188
|
}
|
|
182
189
|
// permissive default-on (subject to the tool's own runtime presence checks).
|
|
183
190
|
return { on: true };
|
|
@@ -235,6 +242,8 @@ export function scannerToolId(source: string): ToolId | null {
|
|
|
235
242
|
return "trivy";
|
|
236
243
|
case "checkov":
|
|
237
244
|
return "checkov";
|
|
245
|
+
case "betterleaks":
|
|
246
|
+
return "betterleaks";
|
|
238
247
|
default:
|
|
239
248
|
return null;
|
|
240
249
|
}
|
|
@@ -20,6 +20,7 @@ export type ToolId =
|
|
|
20
20
|
| "checkov"
|
|
21
21
|
| "infracost"
|
|
22
22
|
| "gitleaks"
|
|
23
|
+
| "betterleaks"
|
|
23
24
|
| "conftest"
|
|
24
25
|
| "terratest"
|
|
25
26
|
| "terratag"
|
|
@@ -96,6 +97,12 @@ export const TOOL_LICENSES: Readonly<Record<ToolId, ToolLicense>> = {
|
|
|
96
97
|
license: "MIT",
|
|
97
98
|
class: "permissive",
|
|
98
99
|
},
|
|
100
|
+
betterleaks: {
|
|
101
|
+
id: "betterleaks",
|
|
102
|
+
name: "Betterleaks",
|
|
103
|
+
license: "MIT",
|
|
104
|
+
class: "permissive",
|
|
105
|
+
},
|
|
99
106
|
conftest: {
|
|
100
107
|
id: "conftest",
|
|
101
108
|
name: "Conftest (OPA)",
|
package/src/utils/telemetry.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { errorMessage } from "@infraweaver-io/core/errors";
|
|
2
3
|
import { PRODUCT_SLUG } from "#app/brand";
|
|
3
4
|
import { log } from "#app/utils/cli";
|
|
4
5
|
import { redactSecrets } from "#app/utils/redact";
|
|
@@ -356,6 +357,6 @@ export async function flushTelemetry(): Promise<void> {
|
|
|
356
357
|
}
|
|
357
358
|
log.debug(`telemetry exported ${armed.spans.length} span(s)`);
|
|
358
359
|
} catch (err) {
|
|
359
|
-
log.debug(`telemetry export failed: ${
|
|
360
|
+
log.debug(`telemetry export failed: ${errorMessage(err)}`);
|
|
360
361
|
}
|
|
361
362
|
}
|