terramend 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +145 -0
- package/dist/agents/claude.d.ts +73 -0
- package/dist/agents/claudePretoolGate.d.ts +99 -0
- package/dist/agents/gateServer.d.ts +7 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/nativeFsDenies.d.ts +28 -0
- package/dist/agents/opencode.d.ts +231 -0
- package/dist/agents/opencodePlugin.d.ts +85 -0
- package/dist/agents/opencodeShared.d.ts +40 -0
- package/dist/agents/postRun.d.ts +132 -0
- package/dist/agents/reviewer.d.ts +38 -0
- package/dist/agents/sessionLabeler.d.ts +97 -0
- package/dist/agents/shared.d.ts +189 -0
- package/dist/agents/subagentModels.d.ts +19 -0
- package/dist/agents/subagentToolGates.d.ts +55 -0
- package/dist/cli.mjs +197426 -0
- package/dist/external.d.ts +227 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +196783 -0
- package/dist/internal/index.d.ts +18 -0
- package/dist/internal.js +1714 -0
- package/dist/lifecycle.d.ts +2 -0
- package/dist/main.d.ts +8 -0
- package/dist/mcp/arkConfig.d.ts +1 -0
- package/dist/mcp/checkSuite.d.ts +25 -0
- package/dist/mcp/checkout.d.ts +77 -0
- package/dist/mcp/comment.d.ts +119 -0
- package/dist/mcp/commitInfo.d.ts +9 -0
- package/dist/mcp/crosswalk.d.ts +105 -0
- package/dist/mcp/dependencies.d.ts +8 -0
- package/dist/mcp/geminiSanitizer.d.ts +28 -0
- package/dist/mcp/git.d.ts +46 -0
- package/dist/mcp/guardrails.d.ts +104 -0
- package/dist/mcp/issue.d.ts +18 -0
- package/dist/mcp/issueComments.d.ts +9 -0
- package/dist/mcp/issueEvents.d.ts +9 -0
- package/dist/mcp/issueInfo.d.ts +9 -0
- package/dist/mcp/labels.d.ts +12 -0
- package/dist/mcp/localContext.d.ts +19 -0
- package/dist/mcp/moduleExtraction.d.ts +71 -0
- package/dist/mcp/moduleTests.d.ts +104 -0
- package/dist/mcp/modules.d.ts +179 -0
- package/dist/mcp/output.d.ts +12 -0
- package/dist/mcp/pathSafety.d.ts +14 -0
- package/dist/mcp/policy.d.ts +48 -0
- package/dist/mcp/pr.d.ts +49 -0
- package/dist/mcp/prInfo.d.ts +9 -0
- package/dist/mcp/providerSchema.d.ts +50 -0
- package/dist/mcp/review.d.ts +199 -0
- package/dist/mcp/reviewComments.d.ts +178 -0
- package/dist/mcp/roots.d.ts +58 -0
- package/dist/mcp/scope.d.ts +15 -0
- package/dist/mcp/selectMode.d.ts +18 -0
- package/dist/mcp/server.d.ts +48 -0
- package/dist/mcp/shared.d.ts +47 -0
- package/dist/mcp/shell.d.ts +37 -0
- package/dist/mcp/staleFix.d.ts +51 -0
- package/dist/mcp/terraform/cost.d.ts +55 -0
- package/dist/mcp/terraform/currency.d.ts +94 -0
- package/dist/mcp/terraform/decisions.d.ts +178 -0
- package/dist/mcp/terraform/findings.d.ts +75 -0
- package/dist/mcp/terraform/plan.d.ts +157 -0
- package/dist/mcp/terraform/scanners.d.ts +131 -0
- package/dist/mcp/terraform/tools.d.ts +63 -0
- package/dist/mcp/terraform/types.d.ts +172 -0
- package/dist/mcp/terraform.d.ts +22 -0
- package/dist/mcp/terratest.d.ts +83 -0
- package/dist/mcp/upload.d.ts +6 -0
- package/dist/models.d.ts +171 -0
- package/dist/modes.d.ts +26 -0
- package/dist/prep/index.d.ts +7 -0
- package/dist/prep/installNodeDependencies.d.ts +2 -0
- package/dist/prep/installPythonDependencies.d.ts +2 -0
- package/dist/prep/types.d.ts +31 -0
- package/dist/reviewQuality.d.ts +64 -0
- package/dist/skills/terraform-best-practices/SKILL.md +369 -0
- package/dist/toolState.d.ts +135 -0
- package/dist/utils/activity.d.ts +40 -0
- package/dist/utils/agent.d.ts +20 -0
- package/dist/utils/agentHangReport.d.ts +38 -0
- package/dist/utils/apiFetch.d.ts +19 -0
- package/dist/utils/apiKeys.d.ts +41 -0
- package/dist/utils/apiUrl.d.ts +20 -0
- package/dist/utils/assets.d.ts +8 -0
- package/dist/utils/billingErrors.d.ts +85 -0
- package/dist/utils/body.d.ts +34 -0
- package/dist/utils/buildTerramendFooter.d.ts +25 -0
- package/dist/utils/byokFallback.d.ts +85 -0
- package/dist/utils/claudeSubscription.d.ts +30 -0
- package/dist/utils/cli.d.ts +10 -0
- package/dist/utils/codexHome.d.ts +29 -0
- package/dist/utils/codexOAuth.d.ts +60 -0
- package/dist/utils/diffCoverage.d.ts +63 -0
- package/dist/utils/errorReport.d.ts +17 -0
- package/dist/utils/exitHandler.d.ts +8 -0
- package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
- package/dist/utils/gitAuth.d.ts +84 -0
- package/dist/utils/gitAuthServer.d.ts +24 -0
- package/dist/utils/github.d.ts +78 -0
- package/dist/utils/globals.d.ts +3 -0
- package/dist/utils/install.d.ts +60 -0
- package/dist/utils/instructions.d.ts +48 -0
- package/dist/utils/leapingComment.d.ts +11 -0
- package/dist/utils/learnings.d.ts +62 -0
- package/dist/utils/learningsTruncate.d.ts +25 -0
- package/dist/utils/lifecycle.d.ts +57 -0
- package/dist/utils/log.d.ts +111 -0
- package/dist/utils/normalizeEnv.d.ts +30 -0
- package/dist/utils/openCodeModels.d.ts +11 -0
- package/dist/utils/overrides.d.ts +40 -0
- package/dist/utils/packageManager.d.ts +49 -0
- package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
- package/dist/utils/payload.d.ts +105 -0
- package/dist/utils/prSummary.d.ts +61 -0
- package/dist/utils/progressComment.d.ts +146 -0
- package/dist/utils/providerErrors.d.ts +31 -0
- package/dist/utils/rangeDiff.d.ts +51 -0
- package/dist/utils/remediationCommand.d.ts +55 -0
- package/dist/utils/retry.d.ts +13 -0
- package/dist/utils/reviewCleanup.d.ts +14 -0
- package/dist/utils/run.d.ts +9 -0
- package/dist/utils/runContext.d.ts +60 -0
- package/dist/utils/runContextData.d.ts +23 -0
- package/dist/utils/runErrorRenderer.d.ts +64 -0
- package/dist/utils/runLifecycle.d.ts +86 -0
- package/dist/utils/runStartupLog.d.ts +15 -0
- package/dist/utils/secrets.d.ts +22 -0
- package/dist/utils/setup.d.ts +90 -0
- package/dist/utils/shell.d.ts +32 -0
- package/dist/utils/skills.d.ts +10 -0
- package/dist/utils/subprocess.d.ts +80 -0
- package/dist/utils/terraformMcp.d.ts +42 -0
- package/dist/utils/time.d.ts +15 -0
- package/dist/utils/timer.d.ts +23 -0
- package/dist/utils/todoTracking.d.ts +16 -0
- package/dist/utils/token.d.ts +39 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/versioning.d.ts +7 -0
- package/dist/utils/vertex.d.ts +16 -0
- package/dist/utils/workflow.d.ts +13 -0
- package/package.json +119 -0
- package/src/agents/claude.test.ts +1016 -0
- package/src/agents/claude.ts +1246 -0
- package/src/agents/claudePretoolGate.test.ts +28 -0
- package/src/agents/claudePretoolGate.ts +173 -0
- package/src/agents/gateServer.test.ts +204 -0
- package/src/agents/gateServer.ts +124 -0
- package/src/agents/index.ts +10 -0
- package/src/agents/nativeFsDenies.ts +82 -0
- package/src/agents/opencode.test.ts +1440 -0
- package/src/agents/opencode.ts +1312 -0
- package/src/agents/opencodePlugin.ts +222 -0
- package/src/agents/opencodeShared.test.ts +34 -0
- package/src/agents/opencodeShared.ts +121 -0
- package/src/agents/postRun.test.ts +549 -0
- package/src/agents/postRun.ts +535 -0
- package/src/agents/reviewer.ts +104 -0
- package/src/agents/sessionLabeler.test.ts +247 -0
- package/src/agents/sessionLabeler.ts +178 -0
- package/src/agents/shared.test.ts +76 -0
- package/src/agents/shared.ts +292 -0
- package/src/agents/subagentModels.test.ts +113 -0
- package/src/agents/subagentModels.ts +40 -0
- package/src/agents/subagentRegistration.test.ts +41 -0
- package/src/agents/subagentToolGates.ts +114 -0
- package/src/cli.test.ts +129 -0
- package/src/cli.ts +105 -0
- package/src/commands/gha.test.ts +192 -0
- package/src/commands/gha.ts +188 -0
- package/src/commands/mcp.ts +122 -0
- package/src/config.ts +1 -0
- package/src/entry.ts +7 -0
- package/src/entryPost.stdlibOnly.test.ts +109 -0
- package/src/entryPost.ts +99 -0
- package/src/external.test.ts +16 -0
- package/src/external.ts +302 -0
- package/src/index.ts +11 -0
- package/src/internal/index.ts +71 -0
- package/src/lifecycle.ts +2 -0
- package/src/main.test.ts +873 -0
- package/src/main.ts +712 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-49-review-3485940013.json +110 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-64-review-3531000326.json +14 -0
- package/src/mcp/__fixtures__/terramend-test-repo-pr-1.diff.json +67 -0
- package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
- package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
- package/src/mcp/arkConfig.ts +7 -0
- package/src/mcp/checkSuite.test.ts +245 -0
- package/src/mcp/checkSuite.ts +255 -0
- package/src/mcp/checkout.test.ts +752 -0
- package/src/mcp/checkout.ts +886 -0
- package/src/mcp/comment.test.ts +772 -0
- package/src/mcp/comment.ts +582 -0
- package/src/mcp/commitInfo.test.ts +127 -0
- package/src/mcp/commitInfo.ts +61 -0
- package/src/mcp/crosswalk.test.ts +106 -0
- package/src/mcp/crosswalk.ts +339 -0
- package/src/mcp/dependencies.test.ts +309 -0
- package/src/mcp/dependencies.ts +189 -0
- package/src/mcp/geminiSanitizer.test.ts +287 -0
- package/src/mcp/geminiSanitizer.ts +207 -0
- package/src/mcp/git.test.ts +1083 -0
- package/src/mcp/git.ts +890 -0
- package/src/mcp/guardrails.test.ts +705 -0
- package/src/mcp/guardrails.ts +465 -0
- package/src/mcp/issue.test.ts +113 -0
- package/src/mcp/issue.ts +73 -0
- package/src/mcp/issueComments.test.ts +69 -0
- package/src/mcp/issueComments.ts +48 -0
- package/src/mcp/issueEvents.test.ts +134 -0
- package/src/mcp/issueEvents.ts +100 -0
- package/src/mcp/issueInfo.test.ts +104 -0
- package/src/mcp/issueInfo.ts +72 -0
- package/src/mcp/labels.test.ts +52 -0
- package/src/mcp/labels.ts +34 -0
- package/src/mcp/localContext.ts +28 -0
- package/src/mcp/localServer.test.ts +75 -0
- package/src/mcp/localServer.ts +131 -0
- package/src/mcp/moduleExtraction.test.ts +261 -0
- package/src/mcp/moduleExtraction.ts +313 -0
- package/src/mcp/moduleTests.test.ts +269 -0
- package/src/mcp/moduleTests.ts +421 -0
- package/src/mcp/modules.test.ts +640 -0
- package/src/mcp/modules.ts +696 -0
- package/src/mcp/output.test.ts +96 -0
- package/src/mcp/output.ts +70 -0
- package/src/mcp/pathSafety.test.ts +44 -0
- package/src/mcp/pathSafety.ts +28 -0
- package/src/mcp/policy.test.ts +282 -0
- package/src/mcp/policy.ts +199 -0
- package/src/mcp/pr.test.ts +387 -0
- package/src/mcp/pr.ts +194 -0
- package/src/mcp/prInfo.test.ts +96 -0
- package/src/mcp/prInfo.ts +91 -0
- package/src/mcp/providerSchema.test.ts +85 -0
- package/src/mcp/providerSchema.ts +175 -0
- package/src/mcp/review.test.ts +936 -0
- package/src/mcp/review.ts +923 -0
- package/src/mcp/reviewComments.test.ts +549 -0
- package/src/mcp/reviewComments.ts +896 -0
- package/src/mcp/roots.test.ts +175 -0
- package/src/mcp/roots.ts +217 -0
- package/src/mcp/scope.test.ts +59 -0
- package/src/mcp/scope.ts +65 -0
- package/src/mcp/security.test.ts +720 -0
- package/src/mcp/selectMode.test.ts +210 -0
- package/src/mcp/selectMode.ts +181 -0
- package/src/mcp/server.test.ts +292 -0
- package/src/mcp/server.ts +403 -0
- package/src/mcp/shared.ts +100 -0
- package/src/mcp/shell.test.ts +520 -0
- package/src/mcp/shell.ts +505 -0
- package/src/mcp/staleFix.test.ts +237 -0
- package/src/mcp/staleFix.ts +277 -0
- package/src/mcp/terraform/cost.ts +163 -0
- package/src/mcp/terraform/currency.test.ts +338 -0
- package/src/mcp/terraform/currency.ts +336 -0
- package/src/mcp/terraform/decisions.ts +527 -0
- package/src/mcp/terraform/findings.ts +333 -0
- package/src/mcp/terraform/plan.ts +348 -0
- package/src/mcp/terraform/scanners.ts +809 -0
- package/src/mcp/terraform/tools.test.ts +1071 -0
- package/src/mcp/terraform/tools.ts +908 -0
- package/src/mcp/terraform/types.ts +305 -0
- package/src/mcp/terraform.test.ts +1957 -0
- package/src/mcp/terraform.ts +23 -0
- package/src/mcp/terratest.test.ts +105 -0
- package/src/mcp/terratest.ts +196 -0
- package/src/mcp/toolFiltering.test.ts +85 -0
- package/src/mcp/upload.test.ts +180 -0
- package/src/mcp/upload.ts +112 -0
- package/src/models.test.ts +300 -0
- package/src/models.ts +708 -0
- package/src/modes.test.ts +107 -0
- package/src/modes.ts +880 -0
- package/src/prep/index.ts +43 -0
- package/src/prep/installNodeDependencies.test.ts +298 -0
- package/src/prep/installNodeDependencies.ts +196 -0
- package/src/prep/installPythonDependencies.test.ts +268 -0
- package/src/prep/installPythonDependencies.ts +199 -0
- package/src/prep/types.ts +38 -0
- package/src/reviewQuality.test.ts +63 -0
- package/src/reviewQuality.ts +134 -0
- package/src/runCli.test.ts +214 -0
- package/src/runCli.ts +282 -0
- package/src/skills/terraform-best-practices/SKILL.md +369 -0
- package/src/toolState.test.ts +45 -0
- package/src/toolState.ts +252 -0
- package/src/utils/activity.test.ts +188 -0
- package/src/utils/activity.ts +210 -0
- package/src/utils/agent.test.ts +251 -0
- package/src/utils/agent.ts +139 -0
- package/src/utils/agentHangReport.test.ts +203 -0
- package/src/utils/agentHangReport.ts +170 -0
- package/src/utils/apiFetch.test.ts +115 -0
- package/src/utils/apiFetch.ts +62 -0
- package/src/utils/apiKeys.test.ts +344 -0
- package/src/utils/apiKeys.ts +206 -0
- package/src/utils/apiUrl.test.ts +30 -0
- package/src/utils/apiUrl.ts +59 -0
- package/src/utils/assets.test.ts +153 -0
- package/src/utils/assets.ts +107 -0
- package/src/utils/billingErrors.test.ts +121 -0
- package/src/utils/billingErrors.ts +189 -0
- package/src/utils/body.test.ts +217 -0
- package/src/utils/body.ts +168 -0
- package/src/utils/buildTerramendFooter.test.ts +38 -0
- package/src/utils/buildTerramendFooter.ts +82 -0
- package/src/utils/byokFallback.test.ts +205 -0
- package/src/utils/byokFallback.ts +128 -0
- package/src/utils/claudeSubscription.test.ts +179 -0
- package/src/utils/claudeSubscription.ts +93 -0
- package/src/utils/cli.ts +31 -0
- package/src/utils/codexHome.test.ts +190 -0
- package/src/utils/codexHome.ts +191 -0
- package/src/utils/codexOAuth.ts +147 -0
- package/src/utils/codexRefreshDetect.test.ts +85 -0
- package/src/utils/codexRefreshDetect.ts +35 -0
- package/src/utils/diffCoverage.test.ts +468 -0
- package/src/utils/diffCoverage.ts +404 -0
- package/src/utils/errorReport.test.ts +135 -0
- package/src/utils/errorReport.ts +83 -0
- package/src/utils/exitHandler.ts +35 -0
- package/src/utils/fixDoubleEscapedString.ts +9 -0
- package/src/utils/ghaCore.ts +13 -0
- package/src/utils/gitAuth.test.ts +322 -0
- package/src/utils/gitAuth.ts +263 -0
- package/src/utils/gitAuthServer.test.ts +260 -0
- package/src/utils/gitAuthServer.ts +182 -0
- package/src/utils/github.test.ts +615 -0
- package/src/utils/github.ts +538 -0
- package/src/utils/globals.ts +9 -0
- package/src/utils/humanEditCapture.test.ts +100 -0
- package/src/utils/humanEditCapture.ts +193 -0
- package/src/utils/install.test.ts +768 -0
- package/src/utils/install.ts +492 -0
- package/src/utils/instructions.test.ts +240 -0
- package/src/utils/instructions.ts +543 -0
- package/src/utils/leapingComment.test.ts +51 -0
- package/src/utils/leapingComment.ts +18 -0
- package/src/utils/learnings.test.ts +87 -0
- package/src/utils/learnings.ts +138 -0
- package/src/utils/learningsTocRender.test.ts +116 -0
- package/src/utils/learningsTruncate.test.ts +39 -0
- package/src/utils/learningsTruncate.ts +42 -0
- package/src/utils/lifecycle.test.ts +195 -0
- package/src/utils/lifecycle.ts +198 -0
- package/src/utils/log.test.ts +402 -0
- package/src/utils/log.ts +432 -0
- package/src/utils/normalizeEnv.test.ts +91 -0
- package/src/utils/normalizeEnv.ts +106 -0
- package/src/utils/openCodeModels.ts +82 -0
- package/src/utils/overrides.test.ts +89 -0
- package/src/utils/overrides.ts +98 -0
- package/src/utils/packageManager.test.ts +321 -0
- package/src/utils/packageManager.ts +257 -0
- package/src/utils/patchWorkflowRunFields.test.ts +92 -0
- package/src/utils/patchWorkflowRunFields.ts +150 -0
- package/src/utils/payload.test.ts +497 -0
- package/src/utils/payload.ts +371 -0
- package/src/utils/postApiFetch.ts +51 -0
- package/src/utils/prSummary.test.ts +224 -0
- package/src/utils/prSummary.ts +147 -0
- package/src/utils/progressComment.ts +261 -0
- package/src/utils/providerErrors.test.ts +315 -0
- package/src/utils/providerErrors.ts +172 -0
- package/src/utils/rangeDiff.test.ts +236 -0
- package/src/utils/rangeDiff.ts +182 -0
- package/src/utils/remediationCommand.test.ts +163 -0
- package/src/utils/remediationCommand.ts +119 -0
- package/src/utils/retry.test.ts +153 -0
- package/src/utils/retry.ts +58 -0
- package/src/utils/reviewCleanup.ts +106 -0
- package/src/utils/run.ts +99 -0
- package/src/utils/runContext.ts +145 -0
- package/src/utils/runContextData.ts +58 -0
- package/src/utils/runErrorRenderer.test.ts +95 -0
- package/src/utils/runErrorRenderer.ts +259 -0
- package/src/utils/runFixture.ts +76 -0
- package/src/utils/runLifecycle.ts +237 -0
- package/src/utils/runStartupLog.ts +60 -0
- package/src/utils/secrets.test.ts +103 -0
- package/src/utils/secrets.ts +177 -0
- package/src/utils/setup.test.ts +509 -0
- package/src/utils/setup.ts +352 -0
- package/src/utils/shell.ts +103 -0
- package/src/utils/skills.test.ts +46 -0
- package/src/utils/skills.ts +67 -0
- package/src/utils/subprocess.test.ts +170 -0
- package/src/utils/subprocess.ts +438 -0
- package/src/utils/terraformMcp.test.ts +63 -0
- package/src/utils/terraformMcp.ts +83 -0
- package/src/utils/time.test.ts +105 -0
- package/src/utils/time.ts +59 -0
- package/src/utils/timer.test.ts +91 -0
- package/src/utils/timer.ts +72 -0
- package/src/utils/todoTracking.test.ts +223 -0
- package/src/utils/todoTracking.ts +167 -0
- package/src/utils/token.test.ts +239 -0
- package/src/utils/token.ts +186 -0
- package/src/utils/version.ts +10 -0
- package/src/utils/versioning.test.ts +34 -0
- package/src/utils/versioning.ts +44 -0
- package/src/utils/vertex.ts +85 -0
- package/src/utils/workflow.ts +25 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
formatApiKeyErrorSummary,
|
|
4
|
+
isApiKeyAuthError,
|
|
5
|
+
validateAgentApiKey,
|
|
6
|
+
} from "#app/utils/apiKeys";
|
|
7
|
+
|
|
8
|
+
const savedEnv = { ...process.env };
|
|
9
|
+
|
|
10
|
+
const ENV_KEYS_TO_STRIP = [
|
|
11
|
+
/_API_KEY$/,
|
|
12
|
+
/^CLAUDE_CODE_OAUTH_TOKEN$/,
|
|
13
|
+
/^CODEX_AUTH_JSON$/,
|
|
14
|
+
/^AWS_BEARER_TOKEN_BEDROCK$/,
|
|
15
|
+
/^AWS_ACCESS_KEY_ID$/,
|
|
16
|
+
/^AWS_SECRET_ACCESS_KEY$/,
|
|
17
|
+
/^AWS_SESSION_TOKEN$/,
|
|
18
|
+
/^AWS_REGION$/,
|
|
19
|
+
/^BEDROCK_MODEL_ID$/,
|
|
20
|
+
/^GOOGLE_APPLICATION_CREDENTIALS$/,
|
|
21
|
+
/^GOOGLE_CLOUD_PROJECT$/,
|
|
22
|
+
/^VERTEX_SERVICE_ACCOUNT_JSON$/,
|
|
23
|
+
/^VERTEX_LOCATION$/,
|
|
24
|
+
/^VERTEX_MODEL_ID$/,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
for (const key of Object.keys(process.env)) {
|
|
29
|
+
if (ENV_KEYS_TO_STRIP.some((re) => re.test(key))) delete process.env[key];
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
process.env = { ...savedEnv };
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const opencode = { name: "opencode" };
|
|
38
|
+
const claude = { name: "claude" };
|
|
39
|
+
const owner = "test-owner";
|
|
40
|
+
const name = "test-repo";
|
|
41
|
+
|
|
42
|
+
describe("validateAgentApiKey — opencode", () => {
|
|
43
|
+
it("passes when the resolved model is in the authorized set", () => {
|
|
44
|
+
expect(() =>
|
|
45
|
+
validateAgentApiKey({
|
|
46
|
+
agent: opencode,
|
|
47
|
+
model: "anthropic/claude-opus-4-7",
|
|
48
|
+
authorized: new Set(["anthropic/claude-opus-4-7"]),
|
|
49
|
+
owner,
|
|
50
|
+
name,
|
|
51
|
+
}),
|
|
52
|
+
).not.toThrow();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("throws when the resolved model is absent from the authorized set", () => {
|
|
56
|
+
expect(() =>
|
|
57
|
+
validateAgentApiKey({
|
|
58
|
+
agent: opencode,
|
|
59
|
+
model: "anthropic/claude-opus-4-7",
|
|
60
|
+
authorized: new Set(),
|
|
61
|
+
owner,
|
|
62
|
+
name,
|
|
63
|
+
}),
|
|
64
|
+
).toThrow("no API key found");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("passes the auto-select path when the authorized set is non-empty", () => {
|
|
68
|
+
expect(() =>
|
|
69
|
+
validateAgentApiKey({
|
|
70
|
+
agent: opencode,
|
|
71
|
+
model: undefined,
|
|
72
|
+
authorized: new Set(["opencode/big-pickle"]),
|
|
73
|
+
owner,
|
|
74
|
+
name,
|
|
75
|
+
}),
|
|
76
|
+
).not.toThrow();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("throws the auto-select path when the authorized set is empty", () => {
|
|
80
|
+
expect(() =>
|
|
81
|
+
validateAgentApiKey({
|
|
82
|
+
agent: opencode,
|
|
83
|
+
model: undefined,
|
|
84
|
+
authorized: new Set(),
|
|
85
|
+
owner,
|
|
86
|
+
name,
|
|
87
|
+
}),
|
|
88
|
+
).toThrow("no API key found");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("validateAgentApiKey — claude (static Anthropic check)", () => {
|
|
93
|
+
it("passes when ANTHROPIC_API_KEY is set", () => {
|
|
94
|
+
process.env.ANTHROPIC_API_KEY = "sk-test";
|
|
95
|
+
expect(() =>
|
|
96
|
+
validateAgentApiKey({
|
|
97
|
+
agent: claude,
|
|
98
|
+
model: "anthropic/claude-opus-4-7",
|
|
99
|
+
authorized: new Set(),
|
|
100
|
+
owner,
|
|
101
|
+
name,
|
|
102
|
+
}),
|
|
103
|
+
).not.toThrow();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("passes when CLAUDE_CODE_OAUTH_TOKEN is set", () => {
|
|
107
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-test";
|
|
108
|
+
expect(() =>
|
|
109
|
+
validateAgentApiKey({
|
|
110
|
+
agent: claude,
|
|
111
|
+
model: "anthropic/claude-opus-4-7",
|
|
112
|
+
authorized: new Set(),
|
|
113
|
+
owner,
|
|
114
|
+
name,
|
|
115
|
+
}),
|
|
116
|
+
).not.toThrow();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("throws when neither Anthropic credential is set", () => {
|
|
120
|
+
expect(() =>
|
|
121
|
+
validateAgentApiKey({
|
|
122
|
+
agent: claude,
|
|
123
|
+
model: "anthropic/claude-opus-4-7",
|
|
124
|
+
authorized: new Set(),
|
|
125
|
+
owner,
|
|
126
|
+
name,
|
|
127
|
+
}),
|
|
128
|
+
).toThrow("no API key found");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("validateAgentApiKey — Bedrock routing", () => {
|
|
133
|
+
const params = { agent: opencode, authorized: new Set<string>(), owner, name };
|
|
134
|
+
|
|
135
|
+
it("passes with AWS_BEARER_TOKEN_BEDROCK + AWS_REGION + BEDROCK_MODEL_ID", () => {
|
|
136
|
+
process.env.AWS_BEARER_TOKEN_BEDROCK = "bedrock-token";
|
|
137
|
+
process.env.AWS_REGION = "eu-west-2";
|
|
138
|
+
process.env.BEDROCK_MODEL_ID = "eu.anthropic.claude-opus-4-7";
|
|
139
|
+
expect(() => validateAgentApiKey({ ...params, model: "bedrock/byok" })).not.toThrow();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("passes with AWS access keys + region + model id", () => {
|
|
143
|
+
process.env.AWS_ACCESS_KEY_ID = "AKIA-test";
|
|
144
|
+
process.env.AWS_SECRET_ACCESS_KEY = "secret-test";
|
|
145
|
+
process.env.AWS_REGION = "eu-west-2";
|
|
146
|
+
process.env.BEDROCK_MODEL_ID = "amazon.nova-pro-v1:0";
|
|
147
|
+
expect(() => validateAgentApiKey({ ...params, model: "bedrock/byok" })).not.toThrow();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("throws when BEDROCK_MODEL_ID is missing", () => {
|
|
151
|
+
process.env.AWS_BEARER_TOKEN_BEDROCK = "bedrock-token";
|
|
152
|
+
process.env.AWS_REGION = "eu-west-2";
|
|
153
|
+
expect(() => validateAgentApiKey({ ...params, model: "bedrock/byok" })).toThrow(
|
|
154
|
+
"BEDROCK_MODEL_ID",
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// regression: main.ts passes the post-resolveModel value into
|
|
159
|
+
// validateAgentApiKey, which for Bedrock is the raw AWS model ID (no `/`).
|
|
160
|
+
it("accepts a raw Bedrock model ID without throwing", () => {
|
|
161
|
+
process.env.AWS_BEARER_TOKEN_BEDROCK = "bedrock-token";
|
|
162
|
+
process.env.AWS_REGION = "eu-west-2";
|
|
163
|
+
process.env.BEDROCK_MODEL_ID = "eu.anthropic.claude-opus-4-6-v1";
|
|
164
|
+
expect(() =>
|
|
165
|
+
validateAgentApiKey({ ...params, model: "eu.anthropic.claude-opus-4-6-v1" }),
|
|
166
|
+
).not.toThrow();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("throws on raw Bedrock model ID when AWS auth is missing", () => {
|
|
170
|
+
process.env.AWS_REGION = "eu-west-2";
|
|
171
|
+
process.env.BEDROCK_MODEL_ID = "eu.anthropic.claude-opus-4-6-v1";
|
|
172
|
+
expect(() =>
|
|
173
|
+
validateAgentApiKey({ ...params, model: "eu.anthropic.claude-opus-4-6-v1" }),
|
|
174
|
+
).toThrow("AWS_BEARER_TOKEN_BEDROCK");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("validateAgentApiKey — Vertex routing", () => {
|
|
179
|
+
const params = { agent: opencode, authorized: new Set<string>(), owner, name };
|
|
180
|
+
|
|
181
|
+
it("passes with service-account JSON + project + location + model id", () => {
|
|
182
|
+
process.env.VERTEX_SERVICE_ACCOUNT_JSON = "{}";
|
|
183
|
+
process.env.GOOGLE_CLOUD_PROJECT = "test-project";
|
|
184
|
+
process.env.VERTEX_LOCATION = "europe-west2";
|
|
185
|
+
process.env.VERTEX_MODEL_ID = "claude-opus-4-1@20250805";
|
|
186
|
+
expect(() => validateAgentApiKey({ ...params, model: "vertex/byok" })).not.toThrow();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("throws when VERTEX_MODEL_ID is missing", () => {
|
|
190
|
+
process.env.VERTEX_SERVICE_ACCOUNT_JSON = "{}";
|
|
191
|
+
process.env.GOOGLE_CLOUD_PROJECT = "test-project";
|
|
192
|
+
process.env.VERTEX_LOCATION = "europe-west2";
|
|
193
|
+
expect(() => validateAgentApiKey({ ...params, model: "vertex/byok" })).toThrow(
|
|
194
|
+
"VERTEX_MODEL_ID",
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("accepts a raw Vertex model ID without throwing", () => {
|
|
199
|
+
process.env.VERTEX_SERVICE_ACCOUNT_JSON = "{}";
|
|
200
|
+
process.env.GOOGLE_CLOUD_PROJECT = "test-project";
|
|
201
|
+
process.env.VERTEX_LOCATION = "europe-west2";
|
|
202
|
+
process.env.VERTEX_MODEL_ID = "gemini-2.5-pro";
|
|
203
|
+
expect(() => validateAgentApiKey({ ...params, model: "gemini-2.5-pro" })).not.toThrow();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("throws on raw Vertex model ID when auth is missing", () => {
|
|
207
|
+
process.env.GOOGLE_CLOUD_PROJECT = "test-project";
|
|
208
|
+
process.env.VERTEX_LOCATION = "europe-west2";
|
|
209
|
+
process.env.VERTEX_MODEL_ID = "gemini-2.5-pro";
|
|
210
|
+
expect(() => validateAgentApiKey({ ...params, model: "gemini-2.5-pro" })).toThrow(
|
|
211
|
+
"VERTEX_SERVICE_ACCOUNT_JSON",
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("validateAgentApiKey — claude auto-select (no model)", () => {
|
|
217
|
+
it("passes when ANTHROPIC_API_KEY is set", () => {
|
|
218
|
+
process.env.ANTHROPIC_API_KEY = "sk-test";
|
|
219
|
+
expect(() =>
|
|
220
|
+
validateAgentApiKey({ agent: claude, model: undefined, authorized: new Set(), owner, name }),
|
|
221
|
+
).not.toThrow();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("passes when CLAUDE_CODE_OAUTH_TOKEN is set", () => {
|
|
225
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-test";
|
|
226
|
+
expect(() =>
|
|
227
|
+
validateAgentApiKey({ agent: claude, model: undefined, authorized: new Set(), owner, name }),
|
|
228
|
+
).not.toThrow();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("throws when neither Anthropic credential is set", () => {
|
|
232
|
+
expect(() =>
|
|
233
|
+
validateAgentApiKey({ agent: claude, model: undefined, authorized: new Set(), owner, name }),
|
|
234
|
+
).toThrow("no API key found");
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("validateAgentApiKey — Bedrock auth shape edge cases", () => {
|
|
239
|
+
const params = { agent: opencode, authorized: new Set<string>(), owner, name };
|
|
240
|
+
|
|
241
|
+
it("treats an access key without its secret as missing auth", () => {
|
|
242
|
+
process.env.AWS_ACCESS_KEY_ID = "AKIA-test";
|
|
243
|
+
process.env.AWS_REGION = "eu-west-2";
|
|
244
|
+
process.env.BEDROCK_MODEL_ID = "eu.anthropic.claude-opus-4-7";
|
|
245
|
+
expect(() => validateAgentApiKey({ ...params, model: "bedrock/byok" })).toThrow(
|
|
246
|
+
"AWS_BEARER_TOKEN_BEDROCK (or AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY)",
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("lists AWS_REGION when only the region is missing", () => {
|
|
251
|
+
process.env.AWS_BEARER_TOKEN_BEDROCK = "bedrock-token";
|
|
252
|
+
process.env.BEDROCK_MODEL_ID = "eu.anthropic.claude-opus-4-7";
|
|
253
|
+
expect(() => validateAgentApiKey({ ...params, model: "bedrock/byok" })).toThrow("AWS_REGION");
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("validateAgentApiKey — Vertex project resolution", () => {
|
|
258
|
+
const params = { agent: opencode, authorized: new Set<string>(), owner, name };
|
|
259
|
+
|
|
260
|
+
it("accepts the project id embedded in the service-account JSON", () => {
|
|
261
|
+
process.env.VERTEX_SERVICE_ACCOUNT_JSON = '{"project_id":"embedded-project"}';
|
|
262
|
+
process.env.VERTEX_LOCATION = "europe-west2";
|
|
263
|
+
process.env.VERTEX_MODEL_ID = "claude-opus-4-1@20250805";
|
|
264
|
+
expect(() => validateAgentApiKey({ ...params, model: "vertex/byok" })).not.toThrow();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("lists VERTEX_LOCATION when only the location is missing", () => {
|
|
268
|
+
process.env.VERTEX_SERVICE_ACCOUNT_JSON = '{"project_id":"embedded-project"}';
|
|
269
|
+
process.env.VERTEX_MODEL_ID = "claude-opus-4-1@20250805";
|
|
270
|
+
expect(() => validateAgentApiKey({ ...params, model: "vertex/byok" })).toThrow(
|
|
271
|
+
"VERTEX_LOCATION",
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("lists GOOGLE_CLOUD_PROJECT when neither env nor the JSON carries a project id", () => {
|
|
276
|
+
process.env.VERTEX_SERVICE_ACCOUNT_JSON = '{"client_email":"sa@example.iam"}';
|
|
277
|
+
process.env.VERTEX_LOCATION = "europe-west2";
|
|
278
|
+
process.env.VERTEX_MODEL_ID = "claude-opus-4-1@20250805";
|
|
279
|
+
expect(() => validateAgentApiKey({ ...params, model: "vertex/byok" })).toThrow(
|
|
280
|
+
"GOOGLE_CLOUD_PROJECT",
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("isApiKeyAuthError", () => {
|
|
286
|
+
it("matches the missing-key marker thrown by validateAgentApiKey", () => {
|
|
287
|
+
expect(isApiKeyAuthError("no API key found. Terramend needs ...")).toBe(true);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("matches Claude CLI 401 strings", () => {
|
|
291
|
+
expect(isApiKeyAuthError("Invalid API key · Fix external API key")).toBe(true);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("matches OpenAI / OpenRouter 401 phrasings", () => {
|
|
295
|
+
expect(isApiKeyAuthError("ProviderAuthError: User not found")).toBe(true);
|
|
296
|
+
expect(isApiKeyAuthError("401 Invalid authentication")).toBe(true);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// see #782 — direct-Anthropic 401 shape (revoked / mistyped / rotated
|
|
300
|
+
// ANTHROPIC_API_KEY) reaches us via Claude CLI as a JSON dump, not as
|
|
301
|
+
// any of the canonical "Invalid API key" strings.
|
|
302
|
+
it("matches direct-Anthropic 401 shapes", () => {
|
|
303
|
+
expect(
|
|
304
|
+
isApiKeyAuthError(
|
|
305
|
+
'Failed to authenticate. API Error: 401 {"type":"error","error":{"type":"authentication_error","message":"Invalid bearer token"}}',
|
|
306
|
+
),
|
|
307
|
+
).toBe(true);
|
|
308
|
+
expect(
|
|
309
|
+
isApiKeyAuthError(
|
|
310
|
+
"» Terramend result error: subtype=success, api_error_status=401, message=Failed to authenticate.",
|
|
311
|
+
),
|
|
312
|
+
).toBe(true);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("ignores unrelated errors", () => {
|
|
316
|
+
expect(isApiKeyAuthError("git fetch failed")).toBe(false);
|
|
317
|
+
expect(isApiKeyAuthError("")).toBe(false);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe("formatApiKeyErrorSummary", () => {
|
|
322
|
+
it("renders the missing-key body when the raw error contains the marker", () => {
|
|
323
|
+
const msg = formatApiKeyErrorSummary({
|
|
324
|
+
owner: "acme",
|
|
325
|
+
name: "repo",
|
|
326
|
+
raw: "no API key found in this run",
|
|
327
|
+
});
|
|
328
|
+
expect(msg).toContain("no API key found");
|
|
329
|
+
expect(msg).toContain("https://github.com/acme/repo/settings/secrets/actions");
|
|
330
|
+
expect(msg).toContain("/console/acme/repo");
|
|
331
|
+
expect(msg).toContain("https://discord.gg/8y96raFg8e");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("renders the invalid-key body for any other auth error", () => {
|
|
335
|
+
const msg = formatApiKeyErrorSummary({
|
|
336
|
+
owner: "acme",
|
|
337
|
+
name: "repo",
|
|
338
|
+
raw: "Invalid API key · Fix external API key",
|
|
339
|
+
});
|
|
340
|
+
expect(msg).toContain("rejected (401)");
|
|
341
|
+
expect(msg).toContain("https://github.com/acme/repo/settings/secrets/actions");
|
|
342
|
+
expect(msg).toContain("https://discord.gg/8y96raFg8e");
|
|
343
|
+
});
|
|
344
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { BEDROCK_MODEL_ID_ENV, resolveDisplayAlias, VERTEX_MODEL_ID_ENV } from "#app/models";
|
|
2
|
+
import { getApiUrl } from "#app/utils/apiUrl";
|
|
3
|
+
import {
|
|
4
|
+
GOOGLE_CLOUD_PROJECT_ENV,
|
|
5
|
+
readProjectIdFromVertexServiceAccountJson,
|
|
6
|
+
VERTEX_LOCATION_ENV,
|
|
7
|
+
VERTEX_SERVICE_ACCOUNT_JSON_ENV,
|
|
8
|
+
} from "#app/utils/vertex";
|
|
9
|
+
|
|
10
|
+
/** marker prefix on the throw message for the catch-side reclassification path */
|
|
11
|
+
const MISSING_KEY_MARKER = "no API key found";
|
|
12
|
+
|
|
13
|
+
/** Markdown body used for both the thrown error and the formatted PR comment summary. */
|
|
14
|
+
function buildMissingApiKeyError(params: { owner: string; name: string }): string {
|
|
15
|
+
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
16
|
+
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
17
|
+
|
|
18
|
+
return [
|
|
19
|
+
`**${MISSING_KEY_MARKER}** — Terramend needs at least one LLM provider API key (e.g. \`ANTHROPIC_API_KEY\`, \`OPENAI_API_KEY\`, \`GEMINI_API_KEY\`) configured as a GitHub Actions secret.`,
|
|
20
|
+
"",
|
|
21
|
+
`[Open repo secrets →](${githubSecretsUrl}) · [Configure model →](${settingsUrl}) · [Setup docs →](https://docs.terramend.com/keys) · [Ask in Discord →](https://discord.gg/8y96raFg8e)`,
|
|
22
|
+
].join("\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildBedrockSetupError(params: {
|
|
26
|
+
owner: string;
|
|
27
|
+
name: string;
|
|
28
|
+
missing: string[];
|
|
29
|
+
}): string {
|
|
30
|
+
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
31
|
+
|
|
32
|
+
return `Bedrock model selected but required configuration is missing: ${params.missing.join(", ")}.
|
|
33
|
+
|
|
34
|
+
add the missing secret(s) to your GitHub repository at ${githubSecretsUrl}, then reference them in your workflow's \`env:\` block:
|
|
35
|
+
|
|
36
|
+
AWS_BEARER_TOKEN_BEDROCK: \${{ secrets.AWS_BEARER_TOKEN_BEDROCK }}
|
|
37
|
+
AWS_REGION: \${{ secrets.AWS_REGION }}
|
|
38
|
+
${BEDROCK_MODEL_ID_ENV}: \${{ secrets.${BEDROCK_MODEL_ID_ENV} }}
|
|
39
|
+
|
|
40
|
+
\`AWS_BEARER_TOKEN_BEDROCK\` may be substituted with \`AWS_ACCESS_KEY_ID\` + \`AWS_SECRET_ACCESS_KEY\` (and optional \`AWS_SESSION_TOKEN\`) if you prefer access keys.
|
|
41
|
+
|
|
42
|
+
for full setup instructions, see https://docs.terramend.com/bedrock`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildVertexSetupError(params: { owner: string; name: string; missing: string[] }): string {
|
|
46
|
+
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
47
|
+
|
|
48
|
+
return `Google Vertex AI model selected but required configuration is missing: ${params.missing.join(", ")}.
|
|
49
|
+
|
|
50
|
+
add the missing secret(s) to your GitHub repository at ${githubSecretsUrl}, then reference them in your workflow's \`env:\` block:
|
|
51
|
+
|
|
52
|
+
${VERTEX_SERVICE_ACCOUNT_JSON_ENV}: \${{ secrets.${VERTEX_SERVICE_ACCOUNT_JSON_ENV} }}
|
|
53
|
+
${GOOGLE_CLOUD_PROJECT_ENV}: my-project
|
|
54
|
+
${VERTEX_LOCATION_ENV}: global
|
|
55
|
+
${VERTEX_MODEL_ID_ENV}: <vertex-model-id>
|
|
56
|
+
|
|
57
|
+
for full setup instructions, see https://docs.terramend.com/vertex`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function hasEnvVar(name: string): boolean {
|
|
61
|
+
const value = process.env[name];
|
|
62
|
+
return typeof value === "string" && value.length > 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validateBedrockSetup(params: { owner: string; name: string }): void {
|
|
66
|
+
const hasAuth =
|
|
67
|
+
hasEnvVar("AWS_BEARER_TOKEN_BEDROCK") ||
|
|
68
|
+
(hasEnvVar("AWS_ACCESS_KEY_ID") && hasEnvVar("AWS_SECRET_ACCESS_KEY"));
|
|
69
|
+
|
|
70
|
+
const missing: string[] = [];
|
|
71
|
+
if (!hasAuth)
|
|
72
|
+
missing.push("AWS_BEARER_TOKEN_BEDROCK (or AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY)");
|
|
73
|
+
if (!hasEnvVar("AWS_REGION")) missing.push("AWS_REGION");
|
|
74
|
+
if (!hasEnvVar(BEDROCK_MODEL_ID_ENV)) missing.push(BEDROCK_MODEL_ID_ENV);
|
|
75
|
+
|
|
76
|
+
if (missing.length > 0) {
|
|
77
|
+
throw new Error(buildBedrockSetupError({ owner: params.owner, name: params.name, missing }));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function validateVertexSetup(params: { owner: string; name: string }): void {
|
|
82
|
+
const hasAuth = hasEnvVar(VERTEX_SERVICE_ACCOUNT_JSON_ENV);
|
|
83
|
+
const hasProject =
|
|
84
|
+
hasEnvVar(GOOGLE_CLOUD_PROJECT_ENV) ||
|
|
85
|
+
readProjectIdFromVertexServiceAccountJson() !== undefined;
|
|
86
|
+
|
|
87
|
+
const missing: string[] = [];
|
|
88
|
+
if (!hasAuth) missing.push(VERTEX_SERVICE_ACCOUNT_JSON_ENV);
|
|
89
|
+
if (!hasProject) missing.push(GOOGLE_CLOUD_PROJECT_ENV);
|
|
90
|
+
if (!hasEnvVar(VERTEX_LOCATION_ENV)) missing.push(VERTEX_LOCATION_ENV);
|
|
91
|
+
if (!hasEnvVar(VERTEX_MODEL_ID_ENV)) missing.push(VERTEX_MODEL_ID_ENV);
|
|
92
|
+
|
|
93
|
+
if (missing.length > 0) {
|
|
94
|
+
throw new Error(buildVertexSetupError({ owner: params.owner, name: params.name, missing }));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Validate that the resolved model can actually be served by the chosen
|
|
100
|
+
* agent. For routing slugs (Bedrock / Vertex) the auth shape is multi-var
|
|
101
|
+
* (auth + region/location + model-id) and `opencode models` doesn't catch
|
|
102
|
+
* gaps in the latter two — keep dedicated setup validators. For the
|
|
103
|
+
* opencode path, the authoritative answer comes from OpenCode's own model
|
|
104
|
+
* introspection (`authorized` set captured in `openCodeModels.ts`). For
|
|
105
|
+
* the claude path, fall back to the static check (`ANTHROPIC_API_KEY` /
|
|
106
|
+
* `CLAUDE_CODE_OAUTH_TOKEN`).
|
|
107
|
+
*/
|
|
108
|
+
export function validateAgentApiKey(params: {
|
|
109
|
+
agent: { name: string };
|
|
110
|
+
model: string | undefined;
|
|
111
|
+
authorized: Set<string>;
|
|
112
|
+
owner: string;
|
|
113
|
+
name: string;
|
|
114
|
+
}): void {
|
|
115
|
+
if (params.model) {
|
|
116
|
+
const alias = resolveDisplayAlias(params.model);
|
|
117
|
+
if (alias?.routing === "bedrock") {
|
|
118
|
+
validateBedrockSetup({ owner: params.owner, name: params.name });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (alias?.routing === "vertex") {
|
|
122
|
+
validateVertexSetup({ owner: params.owner, name: params.name });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// raw backend model IDs (post-resolveModel for routing slugs) have no
|
|
127
|
+
// `/`. discriminate by the env-var sentinel, then run the matching
|
|
128
|
+
// setup validator — `opencode models` doesn't help here because the
|
|
129
|
+
// Bedrock/Vertex provider plugins need region/location/model-id wired
|
|
130
|
+
// through env regardless of CLI-side auth.
|
|
131
|
+
if (!params.model.includes("/")) {
|
|
132
|
+
if (process.env[VERTEX_MODEL_ID_ENV]?.trim() === params.model) {
|
|
133
|
+
validateVertexSetup({ owner: params.owner, name: params.name });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
validateBedrockSetup({ owner: params.owner, name: params.name });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (params.agent.name === "opencode") {
|
|
141
|
+
if (params.authorized.has(params.model)) return;
|
|
142
|
+
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// claude: single-provider check on the Anthropic auth shapes.
|
|
146
|
+
if (hasEnvVar("ANTHROPIC_API_KEY") || hasEnvVar("CLAUDE_CODE_OAUTH_TOKEN")) return;
|
|
147
|
+
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// no model configured (auto-select path).
|
|
151
|
+
if (params.agent.name === "opencode") {
|
|
152
|
+
if (params.authorized.size > 0) return;
|
|
153
|
+
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
|
154
|
+
}
|
|
155
|
+
if (hasEnvVar("ANTHROPIC_API_KEY") || hasEnvVar("CLAUDE_CODE_OAUTH_TOKEN")) return;
|
|
156
|
+
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Detect agent-runtime auth failures that should be reformatted as an actionable
|
|
161
|
+
* key-fix CTA before being shown to the user. Covers the shapes we see:
|
|
162
|
+
* - missing key (validateAgentApiKey throw): contains MISSING_KEY_MARKER
|
|
163
|
+
* - revoked / invalid key (Claude CLI 401 surfaced via api_error_status):
|
|
164
|
+
* "Invalid API key · Fix external API key" + similar provider variants
|
|
165
|
+
* - direct-Anthropic 401 (`Failed to authenticate. API Error: 401 ...
|
|
166
|
+
* {"type":"error","error":{"type":"authentication_error", ...
|
|
167
|
+
* "Invalid bearer token"}}`) emitted by the Claude CLI for revoked /
|
|
168
|
+
* mistyped / rotated `ANTHROPIC_API_KEY`. see #782.
|
|
169
|
+
*/
|
|
170
|
+
export function isApiKeyAuthError(text: string): boolean {
|
|
171
|
+
if (!text) return false;
|
|
172
|
+
return (
|
|
173
|
+
text.includes(MISSING_KEY_MARKER) ||
|
|
174
|
+
/Invalid API key/i.test(text) ||
|
|
175
|
+
/\bUser not found\b/i.test(text) ||
|
|
176
|
+
/\bInvalid authentication\b/i.test(text) ||
|
|
177
|
+
/authentication_error/i.test(text) ||
|
|
178
|
+
/Invalid bearer token/i.test(text) ||
|
|
179
|
+
/api_error_status\s*=\s*401/i.test(text) ||
|
|
180
|
+
/API Error:\s*401/i.test(text)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Friendly Markdown summary for both the missing-key and invalid-key cases.
|
|
186
|
+
* Used in the catch / result-failure paths in `main.ts` to overwrite the raw
|
|
187
|
+
* agent error before it's posted to the PR progress comment.
|
|
188
|
+
*/
|
|
189
|
+
export function formatApiKeyErrorSummary(params: {
|
|
190
|
+
owner: string;
|
|
191
|
+
name: string;
|
|
192
|
+
raw: string;
|
|
193
|
+
}): string {
|
|
194
|
+
if (params.raw.includes(MISSING_KEY_MARKER)) {
|
|
195
|
+
return buildMissingApiKeyError({ owner: params.owner, name: params.name });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
199
|
+
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
200
|
+
|
|
201
|
+
return [
|
|
202
|
+
`**Your LLM provider API key was rejected (401).** Rotate the key in your provider dashboard, then update the matching GitHub Actions secret.`,
|
|
203
|
+
"",
|
|
204
|
+
`[Update repo secret →](${githubSecretsUrl}) · [Model settings →](${settingsUrl}) · [Setup docs →](https://docs.terramend.com/keys) · [Ask in Discord →](https://discord.gg/8y96raFg8e)`,
|
|
205
|
+
].join("\n");
|
|
206
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { isBackendConfigured } from "#app/utils/apiUrl";
|
|
3
|
+
|
|
4
|
+
describe("isBackendConfigured", () => {
|
|
5
|
+
let saved: string | undefined;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
saved = process.env.API_URL;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
if (saved === undefined) delete process.env.API_URL;
|
|
13
|
+
else process.env.API_URL = saved;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("is false when API_URL is unset (standalone BYOK — dormant seams no-op)", () => {
|
|
17
|
+
delete process.env.API_URL;
|
|
18
|
+
expect(isBackendConfigured()).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("is false for an empty API_URL", () => {
|
|
22
|
+
process.env.API_URL = "";
|
|
23
|
+
expect(isBackendConfigured()).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("is true when API_URL points at a real backend (hosted SaaS / local dev)", () => {
|
|
27
|
+
process.env.API_URL = "http://localhost:3000";
|
|
28
|
+
expect(isBackendConfigured()).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { log } from "#app/utils/cli";
|
|
2
|
+
|
|
3
|
+
function isLocalUrl(url: URL): boolean {
|
|
4
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// The Terramend JWT (apiToken) and codex write-back secret travel as
|
|
8
|
+
// `Authorization: Bearer` to this host. Pin it to a known allowlist so a
|
|
9
|
+
// misconfigured/hostile `API_URL` can't redirect those bearer credentials to
|
|
10
|
+
// an attacker-controlled host (https alone is not enough — it must be the right
|
|
11
|
+
// host). localhost stays exempt for local dev.
|
|
12
|
+
function isAllowedApiHost(url: URL): boolean {
|
|
13
|
+
if (isLocalUrl(url)) return true;
|
|
14
|
+
const host = url.hostname.toLowerCase();
|
|
15
|
+
return host === "terramend.dev" || host.endsWith(".terramend.com");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* resolve the Terramend API base URL.
|
|
20
|
+
*
|
|
21
|
+
* in the action: API_URL is not explicitly set, so this falls back to https://terramend.com.
|
|
22
|
+
* in local dev: API_URL=http://localhost:3000 (from .env).
|
|
23
|
+
*
|
|
24
|
+
* enforces https:// for non-local URLs to prevent cleartext credential transmission.
|
|
25
|
+
*/
|
|
26
|
+
export function getApiUrl(): string {
|
|
27
|
+
const raw = process.env.API_URL || "https://terramend.dev";
|
|
28
|
+
const parsed = new URL(raw);
|
|
29
|
+
|
|
30
|
+
if (parsed.protocol !== "https:" && !isLocalUrl(parsed)) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`API_URL must use https:// (got ${parsed.protocol}). only localhost is exempt.`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!isAllowedApiHost(parsed)) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`API_URL host '${parsed.hostname}' is not allowed. ` +
|
|
39
|
+
`Bearer credentials are only sent to terramend.dev (or a *.terramend.com host) or localhost.`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
log.debug(`resolved API_URL: ${raw}`);
|
|
44
|
+
return raw;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether a real Terramend backend is configured.
|
|
49
|
+
*
|
|
50
|
+
* Standalone BYOK runs (the OSS default) leave `API_URL` unset — `getApiUrl`
|
|
51
|
+
* then falls back to the marketing host, which serves no API. The dormant
|
|
52
|
+
* open-core persistence seams (repo learnings, run-field PATCHes) must no-op
|
|
53
|
+
* in that case rather than POST into the void and surface the host's 404 as a
|
|
54
|
+
* CI warning. A real backend — the hosted SaaS, or local dev with
|
|
55
|
+
* `API_URL=http://localhost:3000` — sets `API_URL` explicitly.
|
|
56
|
+
*/
|
|
57
|
+
export function isBackendConfigured(): boolean {
|
|
58
|
+
return Boolean(process.env.API_URL);
|
|
59
|
+
}
|