From d07729f4ab403fdafdda21eeb38500c1f1f27688 Mon Sep 17 00:00:00 2001 From: Rowena Day <16017744+rutgoff@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:47:33 -0400 Subject: [PATCH 1/3] feat(cli): add AWS Bedrock as a supported provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Bedrock to the CLI's supported providers with full AWS authentication stack support: - Bearer token / API key (AWS_BEDROCK_API_KEY or --api-key) - AWS Profile (AWS_PROFILE env var) - Direct credentials (AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY) - Default credential chain fallthrough (IMDS, ECS task role, etc.) Also makes the --provider help text dynamically list all supported providers from the supportedProviders array instead of a hardcoded string. The existing AwsBedrockHandler in src/api/providers/bedrock.ts already handles all auth modes — the CLI just needed to populate the RooCodeSettings fields correctly and relax the API key requirement for Bedrock (which can auth without an explicit key). *(Content added by Kenzie, LLM-based artificial assistant, on behalf of Rowena Day.)* --- apps/cli/src/commands/cli/run.ts | 26 +++++++++++++++++++++---- apps/cli/src/index.ts | 3 ++- apps/cli/src/lib/utils/provider.ts | 31 ++++++++++++++++++++++++++++++ apps/cli/src/types/types.ts | 1 + 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/apps/cli/src/commands/cli/run.ts b/apps/cli/src/commands/cli/run.ts index 908df9938b..4dde57f781 100644 --- a/apps/cli/src/commands/cli/run.ts +++ b/apps/cli/src/commands/cli/run.ts @@ -190,10 +190,28 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption extensionHostOptions.apiKey = flagOptions.apiKey || getApiKeyFromEnv(extensionHostOptions.provider) if (!extensionHostOptions.apiKey) { - console.error(`[CLI] Error: No API key provided. Use --api-key or set the appropriate environment variable.`) - console.error(`[CLI] For ${extensionHostOptions.provider}, set ${getEnvVarName(extensionHostOptions.provider)}`) - - process.exit(1) + if (extensionHostOptions.provider === "bedrock") { + // Bedrock can authenticate via AWS credential chain without an explicit API key. + // Validate that at least one credential source is available. + const hasProfile = !!process.env.AWS_PROFILE + const hasDirectCreds = !!(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) + if (!hasProfile && !hasDirectCreds) { + console.error(`[CLI] Error: No credentials found for Bedrock. Provide one of:`) + console.error(` --api-key or AWS_BEDROCK_API_KEY (bearer token / API key mode)`) + console.error(` AWS_PROFILE (profile-based auth)`) + console.error(` AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY (direct credentials)`) + console.error(` Or ensure a default credential chain is available (IMDS, ECS task role, etc.)`) + process.exit(1) + } + } else { + console.error( + `[CLI] Error: No API key provided. Use --api-key or set the appropriate environment variable.`, + ) + console.error( + `[CLI] For ${extensionHostOptions.provider}, set ${getEnvVarName(extensionHostOptions.provider)}`, + ) + process.exit(1) + } } if (!fs.existsSync(extensionHostOptions.workspacePath)) { diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 8c368cc233..9a6360973c 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -1,6 +1,7 @@ import { Command } from "commander" import { DEFAULT_FLAGS } from "@/types/constants.js" +import { supportedProviders } from "@/types/index.js" import { VERSION } from "@/lib/utils/version.js" import { run, logout, status, listCommands, listModes, listModels, listSessions, upgrade } from "@/commands/index.js" @@ -35,7 +36,7 @@ program .option("-d, --debug", "Enable debug output (includes detailed debug information)", false) .option("-a, --require-approval", "Require manual approval for actions", false) .option("-k, --api-key ", "API key for the LLM provider") - .option("--provider ", "API provider (anthropic, openai-native, gemini, openrouter, etc.)") + .option("--provider ", `API provider (${supportedProviders.join(", ")})`) .option("-m, --model ", "Model to use", DEFAULT_FLAGS.model) .option("--mode ", "Mode to start in (code, architect, ask, debug, etc.)", DEFAULT_FLAGS.mode) .option("--terminal-shell ", "Absolute path to shell executable for inline terminal commands") diff --git a/apps/cli/src/lib/utils/provider.ts b/apps/cli/src/lib/utils/provider.ts index 26beaf90c4..89f6afd07a 100644 --- a/apps/cli/src/lib/utils/provider.ts +++ b/apps/cli/src/lib/utils/provider.ts @@ -4,6 +4,7 @@ import type { SupportedProvider } from "@/types/index.js" const envVarMap: Record = { anthropic: "ANTHROPIC_API_KEY", + bedrock: "AWS_BEDROCK_API_KEY", "openai-native": "OPENAI_API_KEY", gemini: "GOOGLE_API_KEY", openrouter: "OPENROUTER_API_KEY", @@ -31,6 +32,36 @@ export function getProviderSettings( if (apiKey) config.apiKey = apiKey if (model) config.apiModelId = model break + case "bedrock": + config.awsRegion = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || "us-east-1" + if (model) { + config.apiModelId = model + // Auto-enable cross-region inference when model ID has a regional prefix + // (e.g. "us.", "eu.", "apac.") — these are cross-region inference profiles + // that require awsUseCrossRegionInference to be set. + if (/^(us|eu|apac)\./.test(model)) { + config.awsUseCrossRegionInference = true + } + } + + if (apiKey) { + // Bearer token / API key mode (LiteLLM proxy, Bedrock gateway) + config.awsUseApiKey = true + config.awsApiKey = apiKey + } else if (process.env.AWS_PROFILE) { + // Profile-based auth + config.awsUseProfile = true + config.awsProfile = process.env.AWS_PROFILE + } else if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) { + // Direct credentials from env + config.awsAccessKey = process.env.AWS_ACCESS_KEY_ID + config.awsSecretKey = process.env.AWS_SECRET_ACCESS_KEY + if (process.env.AWS_SESSION_TOKEN) { + config.awsSessionToken = process.env.AWS_SESSION_TOKEN + } + } + // else: fall through to default credential chain (SDK handles IMDS, ECS task role, etc.) + break case "openai-native": if (apiKey) config.openAiNativeApiKey = apiKey if (model) config.apiModelId = model diff --git a/apps/cli/src/types/types.ts b/apps/cli/src/types/types.ts index 0a9f3d2259..dec740ddb2 100644 --- a/apps/cli/src/types/types.ts +++ b/apps/cli/src/types/types.ts @@ -3,6 +3,7 @@ import type { OutputFormat } from "./json-events.js" export const supportedProviders = [ "anthropic", + "bedrock", "openai-native", "gemini", "openrouter", From 63aa4356b5ae121eb0fe66f4f21c63c1b239c744 Mon Sep 17 00:00:00 2001 From: Rowena Day <16017744+rutgoff@users.noreply.github.com> Date: Sat, 25 Jul 2026 02:18:31 -0400 Subject: [PATCH 2/3] fix(cli): allow AWS default credential chain for Bedrock + add tests Address PR #1015 review feedback. FIX 1 (CodeRabbit): the Bedrock branch in run.ts previously hard-failed with process.exit(1) when neither AWS_PROFILE nor static AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY were set. That rejected the AWS SDK default credential chain (IMDS / EC2 instance profile, ECS task role, IRSA/web-identity, SSO, shared-config default) before the resolver ever ran, defeating advertised auth mode #4. Extract a providerRequiresApiKey() helper (false for bedrock, true otherwise) and gate the API-key requirement on it, so Bedrock proceeds and AwsBedrockHandler surfaces a real error only if credential resolution actually fails. Non-bedrock providers keep the identical missing-API-key error and exit. FIX 2 (codecov): add hermetic vitest coverage for getProviderSettings bedrock path (all 4 auth modes, priority ordering, region resolution, cross-region inference auto-enable) and providerRequiresApiKey. provider.test.ts goes from 4 to 27 tests. Also add JSDoc to providerRequiresApiKey and getProviderSettings describing the four Bedrock auth modes. *(Content added by Kenzie, LLM-based artificial assistant, on behalf of Rowena Day.)* --- apps/cli/src/commands/cli/run.ts | 37 ++-- .../src/lib/utils/__tests__/provider.test.ts | 191 +++++++++++++++++- apps/cli/src/lib/utils/provider.ts | 34 ++++ 3 files changed, 237 insertions(+), 25 deletions(-) diff --git a/apps/cli/src/commands/cli/run.ts b/apps/cli/src/commands/cli/run.ts index 4dde57f781..d0f7f8ead8 100644 --- a/apps/cli/src/commands/cli/run.ts +++ b/apps/cli/src/commands/cli/run.ts @@ -20,7 +20,7 @@ import { JsonEventEmitter } from "@/agent/json-event-emitter.js" import { loadSettings } from "@/lib/storage/index.js" import { readWorkspaceTaskSessions, resolveWorkspaceResumeSessionId } from "@/lib/task-history/index.js" -import { getEnvVarName, getApiKeyFromEnv } from "@/lib/utils/provider.js" +import { getEnvVarName, getApiKeyFromEnv, providerRequiresApiKey } from "@/lib/utils/provider.js" import { validateTerminalShellPath } from "@/lib/utils/shell.js" import { getDefaultExtensionPath } from "@/lib/utils/extension.js" import { isValidSessionId } from "@/lib/utils/session-id.js" @@ -189,29 +189,18 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption extensionHostOptions.apiKey = flagOptions.apiKey || getApiKeyFromEnv(extensionHostOptions.provider) - if (!extensionHostOptions.apiKey) { - if (extensionHostOptions.provider === "bedrock") { - // Bedrock can authenticate via AWS credential chain without an explicit API key. - // Validate that at least one credential source is available. - const hasProfile = !!process.env.AWS_PROFILE - const hasDirectCreds = !!(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) - if (!hasProfile && !hasDirectCreds) { - console.error(`[CLI] Error: No credentials found for Bedrock. Provide one of:`) - console.error(` --api-key or AWS_BEDROCK_API_KEY (bearer token / API key mode)`) - console.error(` AWS_PROFILE (profile-based auth)`) - console.error(` AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY (direct credentials)`) - console.error(` Or ensure a default credential chain is available (IMDS, ECS task role, etc.)`) - process.exit(1) - } - } else { - console.error( - `[CLI] Error: No API key provided. Use --api-key or set the appropriate environment variable.`, - ) - console.error( - `[CLI] For ${extensionHostOptions.provider}, set ${getEnvVarName(extensionHostOptions.provider)}`, - ) - process.exit(1) - } + // Every provider except Bedrock requires an explicit API key up front. Bedrock + // is intentionally exempt: beyond a bearer token / API key, it can authenticate + // via AWS_PROFILE, direct AWS access/secret keys, or the AWS SDK default + // credential chain (IMDS / EC2 instance profile, ECS task role, IRSA, SSO, + // shared-config default). Those chain-based sources set none of our recognised + // environment variables, so we must not hard-fail here — we let execution + // continue and allow the downstream AwsBedrockHandler to resolve credentials and + // surface a real error only if resolution actually fails. + if (!extensionHostOptions.apiKey && providerRequiresApiKey(extensionHostOptions.provider)) { + console.error(`[CLI] Error: No API key provided. Use --api-key or set the appropriate environment variable.`) + console.error(`[CLI] For ${extensionHostOptions.provider}, set ${getEnvVarName(extensionHostOptions.provider)}`) + process.exit(1) } if (!fs.existsSync(extensionHostOptions.workspacePath)) { diff --git a/apps/cli/src/lib/utils/__tests__/provider.test.ts b/apps/cli/src/lib/utils/__tests__/provider.test.ts index 70d8a2a555..c3e2156466 100644 --- a/apps/cli/src/lib/utils/__tests__/provider.test.ts +++ b/apps/cli/src/lib/utils/__tests__/provider.test.ts @@ -1,4 +1,16 @@ -import { getApiKeyFromEnv } from "../provider.js" +import { getApiKeyFromEnv, getProviderSettings, providerRequiresApiKey } from "../provider.js" + +// Bedrock-relevant AWS environment variables. Cleared before each test so the +// suite is hermetic regardless of the host machine's ambient AWS configuration. +const AWS_ENV_KEYS = [ + "AWS_REGION", + "AWS_DEFAULT_REGION", + "AWS_PROFILE", + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "AWS_SESSION_TOKEN", + "AWS_BEDROCK_API_KEY", +] describe("getApiKeyFromEnv", () => { const originalEnv = process.env @@ -32,3 +44,180 @@ describe("getApiKeyFromEnv", () => { expect(getApiKeyFromEnv("anthropic")).toBeUndefined() }) }) + +describe("providerRequiresApiKey", () => { + it("returns false for bedrock (AWS credential chain / profile / direct creds are valid)", () => { + expect(providerRequiresApiKey("bedrock")).toBe(false) + }) + + it.each(["anthropic", "openai-native", "gemini", "openrouter", "vercel-ai-gateway"] as const)( + "returns true for '%s'", + (provider) => { + expect(providerRequiresApiKey(provider)).toBe(true) + }, + ) +}) + +describe("getProviderSettings", () => { + const originalEnv = process.env + + beforeEach(() => { + process.env = { ...originalEnv } + for (const key of AWS_ENV_KEYS) { + delete process.env[key] + } + }) + + afterEach(() => { + process.env = originalEnv + }) + + it("sets apiProvider for the selected provider", () => { + const config = getProviderSettings("anthropic", undefined, undefined) + expect(config.apiProvider).toBe("anthropic") + }) + + describe("bedrock authentication modes", () => { + it("mode 1: bearer token / API key sets awsUseApiKey and awsApiKey", () => { + const config = getProviderSettings("bedrock", "bearer-token-123", undefined) + + expect(config.apiProvider).toBe("bedrock") + expect(config.awsUseApiKey).toBe(true) + expect(config.awsApiKey).toBe("bearer-token-123") + // API-key mode must not enable profile or direct-credential fields. + expect(config.awsUseProfile).toBeUndefined() + expect(config.awsProfile).toBeUndefined() + expect(config.awsAccessKey).toBeUndefined() + expect(config.awsSecretKey).toBeUndefined() + }) + + it("mode 2: AWS_PROFILE sets awsUseProfile and awsProfile", () => { + process.env.AWS_PROFILE = "my-sso-profile" + + const config = getProviderSettings("bedrock", undefined, undefined) + + expect(config.awsUseProfile).toBe(true) + expect(config.awsProfile).toBe("my-sso-profile") + expect(config.awsUseApiKey).toBeUndefined() + expect(config.awsApiKey).toBeUndefined() + expect(config.awsAccessKey).toBeUndefined() + }) + + it("mode 3: direct credentials map AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY", () => { + process.env.AWS_ACCESS_KEY_ID = "AKIAEXAMPLE" + process.env.AWS_SECRET_ACCESS_KEY = "secret-example" + + const config = getProviderSettings("bedrock", undefined, undefined) + + expect(config.awsAccessKey).toBe("AKIAEXAMPLE") + expect(config.awsSecretKey).toBe("secret-example") + expect(config.awsSessionToken).toBeUndefined() + expect(config.awsUseApiKey).toBeUndefined() + expect(config.awsUseProfile).toBeUndefined() + }) + + it("mode 3: direct credentials include AWS_SESSION_TOKEN when present", () => { + process.env.AWS_ACCESS_KEY_ID = "AKIAEXAMPLE" + process.env.AWS_SECRET_ACCESS_KEY = "secret-example" + process.env.AWS_SESSION_TOKEN = "session-token-example" + + const config = getProviderSettings("bedrock", undefined, undefined) + + expect(config.awsAccessKey).toBe("AKIAEXAMPLE") + expect(config.awsSecretKey).toBe("secret-example") + expect(config.awsSessionToken).toBe("session-token-example") + }) + + it("mode 4: default credential chain — no creds set does NOT throw and sets no explicit auth fields", () => { + expect(() => getProviderSettings("bedrock", undefined, undefined)).not.toThrow() + + const config = getProviderSettings("bedrock", undefined, undefined) + + // Region is always resolved, but none of the explicit auth modes engage, + // leaving the AWS SDK to resolve credentials via its default chain. + expect(config.awsUseApiKey).toBeUndefined() + expect(config.awsApiKey).toBeUndefined() + expect(config.awsUseProfile).toBeUndefined() + expect(config.awsProfile).toBeUndefined() + expect(config.awsAccessKey).toBeUndefined() + expect(config.awsSecretKey).toBeUndefined() + }) + + it("prioritises API key over AWS_PROFILE and direct credentials", () => { + process.env.AWS_PROFILE = "my-profile" + process.env.AWS_ACCESS_KEY_ID = "AKIAEXAMPLE" + process.env.AWS_SECRET_ACCESS_KEY = "secret-example" + + const config = getProviderSettings("bedrock", "bearer-token-123", undefined) + + expect(config.awsUseApiKey).toBe(true) + expect(config.awsApiKey).toBe("bearer-token-123") + expect(config.awsUseProfile).toBeUndefined() + expect(config.awsAccessKey).toBeUndefined() + }) + + it("prioritises AWS_PROFILE over direct credentials when no API key is present", () => { + process.env.AWS_PROFILE = "my-profile" + process.env.AWS_ACCESS_KEY_ID = "AKIAEXAMPLE" + process.env.AWS_SECRET_ACCESS_KEY = "secret-example" + + const config = getProviderSettings("bedrock", undefined, undefined) + + expect(config.awsUseProfile).toBe(true) + expect(config.awsProfile).toBe("my-profile") + expect(config.awsAccessKey).toBeUndefined() + expect(config.awsSecretKey).toBeUndefined() + }) + }) + + describe("bedrock region resolution", () => { + it("defaults awsRegion to us-east-1 when no region env is set", () => { + const config = getProviderSettings("bedrock", undefined, undefined) + expect(config.awsRegion).toBe("us-east-1") + }) + + it("uses AWS_REGION when set", () => { + process.env.AWS_REGION = "eu-west-1" + const config = getProviderSettings("bedrock", undefined, undefined) + expect(config.awsRegion).toBe("eu-west-1") + }) + + it("falls back to AWS_DEFAULT_REGION when AWS_REGION is unset", () => { + process.env.AWS_DEFAULT_REGION = "ap-southeast-2" + const config = getProviderSettings("bedrock", undefined, undefined) + expect(config.awsRegion).toBe("ap-southeast-2") + }) + + it("prefers AWS_REGION over AWS_DEFAULT_REGION", () => { + process.env.AWS_REGION = "eu-west-1" + process.env.AWS_DEFAULT_REGION = "ap-southeast-2" + const config = getProviderSettings("bedrock", undefined, undefined) + expect(config.awsRegion).toBe("eu-west-1") + }) + }) + + describe("bedrock cross-region inference auto-enable", () => { + it.each([ + "us.anthropic.claude-3-5-sonnet-20241022-v2:0", + "eu.anthropic.claude-3-5-sonnet", + "apac.amazon.nova-pro", + ])("enables awsUseCrossRegionInference for regional-prefixed model '%s'", (model) => { + const config = getProviderSettings("bedrock", undefined, model) + expect(config.apiModelId).toBe(model) + expect(config.awsUseCrossRegionInference).toBe(true) + }) + + it("does NOT enable cross-region inference for a non-prefixed model", () => { + const model = "anthropic.claude-3-5-sonnet-20241022-v2:0" + const config = getProviderSettings("bedrock", undefined, model) + expect(config.apiModelId).toBe(model) + expect(config.awsUseCrossRegionInference).toBeUndefined() + }) + + it("does not set apiModelId or cross-region flag when no model is given", () => { + const config = getProviderSettings("bedrock", undefined, undefined) + expect(config.apiModelId).toBeUndefined() + expect(config.awsUseCrossRegionInference).toBeUndefined() + }) + }) +}) diff --git a/apps/cli/src/lib/utils/provider.ts b/apps/cli/src/lib/utils/provider.ts index 89f6afd07a..f122da317d 100644 --- a/apps/cli/src/lib/utils/provider.ts +++ b/apps/cli/src/lib/utils/provider.ts @@ -20,6 +20,40 @@ export function getApiKeyFromEnv(provider: SupportedProvider): string | undefine return process.env[envVar] } +/** + * Whether a provider requires an explicit API key before a task can run. + * + * Every provider except Bedrock authenticates solely via an API key + * (`--api-key` or its provider-specific environment variable). Bedrock is the + * exception: in addition to a bearer token / API key, it can authenticate via + * an AWS profile, direct AWS access/secret keys, OR the AWS SDK default + * credential chain (IMDS / EC2 instance profile, ECS task role, IRSA / + * web-identity, SSO, shared-config default). Those chain-based sources set none + * of our recognised environment variables, so Bedrock must never be hard-failed + * for a "missing" API key — the downstream `AwsBedrockHandler` resolves the + * credentials and surfaces a real error only if resolution actually fails. + */ +export function providerRequiresApiKey(provider: SupportedProvider): boolean { + return provider !== "bedrock" +} + +/** + * Build the provider-specific `RooCodeSettings` used to configure the extension + * host for a CLI run. + * + * The Bedrock case supports four authentication modes, resolved in priority + * order: + * 1. Bearer token / API key — `--api-key` or `AWS_BEDROCK_API_KEY` + * (`awsUseApiKey` + `awsApiKey`). + * 2. AWS profile — `AWS_PROFILE` (`awsUseProfile` + `awsProfile`). + * 3. Direct credentials — `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` + * (+ optional `AWS_SESSION_TOKEN`). + * 4. Default credential chain — none of the above set; the AWS SDK resolves + * credentials (IMDS, ECS task role, IRSA, SSO, shared-config default). + * The region is resolved from `AWS_REGION` / `AWS_DEFAULT_REGION` (defaulting to + * `us-east-1`), and cross-region inference is auto-enabled for model IDs that + * carry a regional prefix (`us.` / `eu.` / `apac.`). + */ export function getProviderSettings( provider: SupportedProvider, apiKey: string | undefined, From 226281bf8f4a6efdd8eb3866d8b5ef74fd1ab1c5 Mon Sep 17 00:00:00 2001 From: Rowena Day <16017744+rutgoff@users.noreply.github.com> Date: Sat, 25 Jul 2026 02:32:53 -0400 Subject: [PATCH 3/3] fix: add bedrock case to getApiKeyFromEnv test coverage --- apps/cli/src/lib/utils/__tests__/provider.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/cli/src/lib/utils/__tests__/provider.test.ts b/apps/cli/src/lib/utils/__tests__/provider.test.ts index c3e2156466..480e2300ff 100644 --- a/apps/cli/src/lib/utils/__tests__/provider.test.ts +++ b/apps/cli/src/lib/utils/__tests__/provider.test.ts @@ -39,6 +39,11 @@ describe("getApiKeyFromEnv", () => { expect(getApiKeyFromEnv("openai-native")).toBe("test-openai-key") }) + it("should return API key from environment variable for bedrock", () => { + process.env.AWS_BEDROCK_API_KEY = "test-bedrock-key" + expect(getApiKeyFromEnv("bedrock")).toBe("test-bedrock-key") + }) + it("should return undefined when API key is not set", () => { delete process.env.ANTHROPIC_API_KEY expect(getApiKeyFromEnv("anthropic")).toBeUndefined()