Skip to content

Commit 63aa435

Browse files
committed
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.)*
1 parent d07729f commit 63aa435

3 files changed

Lines changed: 237 additions & 25 deletions

File tree

apps/cli/src/commands/cli/run.ts

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { JsonEventEmitter } from "@/agent/json-event-emitter.js"
2020

2121
import { loadSettings } from "@/lib/storage/index.js"
2222
import { readWorkspaceTaskSessions, resolveWorkspaceResumeSessionId } from "@/lib/task-history/index.js"
23-
import { getEnvVarName, getApiKeyFromEnv } from "@/lib/utils/provider.js"
23+
import { getEnvVarName, getApiKeyFromEnv, providerRequiresApiKey } from "@/lib/utils/provider.js"
2424
import { validateTerminalShellPath } from "@/lib/utils/shell.js"
2525
import { getDefaultExtensionPath } from "@/lib/utils/extension.js"
2626
import { isValidSessionId } from "@/lib/utils/session-id.js"
@@ -189,29 +189,18 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
189189

190190
extensionHostOptions.apiKey = flagOptions.apiKey || getApiKeyFromEnv(extensionHostOptions.provider)
191191

192-
if (!extensionHostOptions.apiKey) {
193-
if (extensionHostOptions.provider === "bedrock") {
194-
// Bedrock can authenticate via AWS credential chain without an explicit API key.
195-
// Validate that at least one credential source is available.
196-
const hasProfile = !!process.env.AWS_PROFILE
197-
const hasDirectCreds = !!(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY)
198-
if (!hasProfile && !hasDirectCreds) {
199-
console.error(`[CLI] Error: No credentials found for Bedrock. Provide one of:`)
200-
console.error(` --api-key or AWS_BEDROCK_API_KEY (bearer token / API key mode)`)
201-
console.error(` AWS_PROFILE (profile-based auth)`)
202-
console.error(` AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY (direct credentials)`)
203-
console.error(` Or ensure a default credential chain is available (IMDS, ECS task role, etc.)`)
204-
process.exit(1)
205-
}
206-
} else {
207-
console.error(
208-
`[CLI] Error: No API key provided. Use --api-key or set the appropriate environment variable.`,
209-
)
210-
console.error(
211-
`[CLI] For ${extensionHostOptions.provider}, set ${getEnvVarName(extensionHostOptions.provider)}`,
212-
)
213-
process.exit(1)
214-
}
192+
// Every provider except Bedrock requires an explicit API key up front. Bedrock
193+
// is intentionally exempt: beyond a bearer token / API key, it can authenticate
194+
// via AWS_PROFILE, direct AWS access/secret keys, or the AWS SDK default
195+
// credential chain (IMDS / EC2 instance profile, ECS task role, IRSA, SSO,
196+
// shared-config default). Those chain-based sources set none of our recognised
197+
// environment variables, so we must not hard-fail here — we let execution
198+
// continue and allow the downstream AwsBedrockHandler to resolve credentials and
199+
// surface a real error only if resolution actually fails.
200+
if (!extensionHostOptions.apiKey && providerRequiresApiKey(extensionHostOptions.provider)) {
201+
console.error(`[CLI] Error: No API key provided. Use --api-key or set the appropriate environment variable.`)
202+
console.error(`[CLI] For ${extensionHostOptions.provider}, set ${getEnvVarName(extensionHostOptions.provider)}`)
203+
process.exit(1)
215204
}
216205

217206
if (!fs.existsSync(extensionHostOptions.workspacePath)) {

apps/cli/src/lib/utils/__tests__/provider.test.ts

Lines changed: 190 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
1-
import { getApiKeyFromEnv } from "../provider.js"
1+
import { getApiKeyFromEnv, getProviderSettings, providerRequiresApiKey } from "../provider.js"
2+
3+
// Bedrock-relevant AWS environment variables. Cleared before each test so the
4+
// suite is hermetic regardless of the host machine's ambient AWS configuration.
5+
const AWS_ENV_KEYS = [
6+
"AWS_REGION",
7+
"AWS_DEFAULT_REGION",
8+
"AWS_PROFILE",
9+
"AWS_ACCESS_KEY_ID",
10+
"AWS_SECRET_ACCESS_KEY",
11+
"AWS_SESSION_TOKEN",
12+
"AWS_BEDROCK_API_KEY",
13+
]
214

315
describe("getApiKeyFromEnv", () => {
416
const originalEnv = process.env
@@ -32,3 +44,180 @@ describe("getApiKeyFromEnv", () => {
3244
expect(getApiKeyFromEnv("anthropic")).toBeUndefined()
3345
})
3446
})
47+
48+
describe("providerRequiresApiKey", () => {
49+
it("returns false for bedrock (AWS credential chain / profile / direct creds are valid)", () => {
50+
expect(providerRequiresApiKey("bedrock")).toBe(false)
51+
})
52+
53+
it.each(["anthropic", "openai-native", "gemini", "openrouter", "vercel-ai-gateway"] as const)(
54+
"returns true for '%s'",
55+
(provider) => {
56+
expect(providerRequiresApiKey(provider)).toBe(true)
57+
},
58+
)
59+
})
60+
61+
describe("getProviderSettings", () => {
62+
const originalEnv = process.env
63+
64+
beforeEach(() => {
65+
process.env = { ...originalEnv }
66+
for (const key of AWS_ENV_KEYS) {
67+
delete process.env[key]
68+
}
69+
})
70+
71+
afterEach(() => {
72+
process.env = originalEnv
73+
})
74+
75+
it("sets apiProvider for the selected provider", () => {
76+
const config = getProviderSettings("anthropic", undefined, undefined)
77+
expect(config.apiProvider).toBe("anthropic")
78+
})
79+
80+
describe("bedrock authentication modes", () => {
81+
it("mode 1: bearer token / API key sets awsUseApiKey and awsApiKey", () => {
82+
const config = getProviderSettings("bedrock", "bearer-token-123", undefined)
83+
84+
expect(config.apiProvider).toBe("bedrock")
85+
expect(config.awsUseApiKey).toBe(true)
86+
expect(config.awsApiKey).toBe("bearer-token-123")
87+
// API-key mode must not enable profile or direct-credential fields.
88+
expect(config.awsUseProfile).toBeUndefined()
89+
expect(config.awsProfile).toBeUndefined()
90+
expect(config.awsAccessKey).toBeUndefined()
91+
expect(config.awsSecretKey).toBeUndefined()
92+
})
93+
94+
it("mode 2: AWS_PROFILE sets awsUseProfile and awsProfile", () => {
95+
process.env.AWS_PROFILE = "my-sso-profile"
96+
97+
const config = getProviderSettings("bedrock", undefined, undefined)
98+
99+
expect(config.awsUseProfile).toBe(true)
100+
expect(config.awsProfile).toBe("my-sso-profile")
101+
expect(config.awsUseApiKey).toBeUndefined()
102+
expect(config.awsApiKey).toBeUndefined()
103+
expect(config.awsAccessKey).toBeUndefined()
104+
})
105+
106+
it("mode 3: direct credentials map AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY", () => {
107+
process.env.AWS_ACCESS_KEY_ID = "AKIAEXAMPLE"
108+
process.env.AWS_SECRET_ACCESS_KEY = "secret-example"
109+
110+
const config = getProviderSettings("bedrock", undefined, undefined)
111+
112+
expect(config.awsAccessKey).toBe("AKIAEXAMPLE")
113+
expect(config.awsSecretKey).toBe("secret-example")
114+
expect(config.awsSessionToken).toBeUndefined()
115+
expect(config.awsUseApiKey).toBeUndefined()
116+
expect(config.awsUseProfile).toBeUndefined()
117+
})
118+
119+
it("mode 3: direct credentials include AWS_SESSION_TOKEN when present", () => {
120+
process.env.AWS_ACCESS_KEY_ID = "AKIAEXAMPLE"
121+
process.env.AWS_SECRET_ACCESS_KEY = "secret-example"
122+
process.env.AWS_SESSION_TOKEN = "session-token-example"
123+
124+
const config = getProviderSettings("bedrock", undefined, undefined)
125+
126+
expect(config.awsAccessKey).toBe("AKIAEXAMPLE")
127+
expect(config.awsSecretKey).toBe("secret-example")
128+
expect(config.awsSessionToken).toBe("session-token-example")
129+
})
130+
131+
it("mode 4: default credential chain — no creds set does NOT throw and sets no explicit auth fields", () => {
132+
expect(() => getProviderSettings("bedrock", undefined, undefined)).not.toThrow()
133+
134+
const config = getProviderSettings("bedrock", undefined, undefined)
135+
136+
// Region is always resolved, but none of the explicit auth modes engage,
137+
// leaving the AWS SDK to resolve credentials via its default chain.
138+
expect(config.awsUseApiKey).toBeUndefined()
139+
expect(config.awsApiKey).toBeUndefined()
140+
expect(config.awsUseProfile).toBeUndefined()
141+
expect(config.awsProfile).toBeUndefined()
142+
expect(config.awsAccessKey).toBeUndefined()
143+
expect(config.awsSecretKey).toBeUndefined()
144+
})
145+
146+
it("prioritises API key over AWS_PROFILE and direct credentials", () => {
147+
process.env.AWS_PROFILE = "my-profile"
148+
process.env.AWS_ACCESS_KEY_ID = "AKIAEXAMPLE"
149+
process.env.AWS_SECRET_ACCESS_KEY = "secret-example"
150+
151+
const config = getProviderSettings("bedrock", "bearer-token-123", undefined)
152+
153+
expect(config.awsUseApiKey).toBe(true)
154+
expect(config.awsApiKey).toBe("bearer-token-123")
155+
expect(config.awsUseProfile).toBeUndefined()
156+
expect(config.awsAccessKey).toBeUndefined()
157+
})
158+
159+
it("prioritises AWS_PROFILE over direct credentials when no API key is present", () => {
160+
process.env.AWS_PROFILE = "my-profile"
161+
process.env.AWS_ACCESS_KEY_ID = "AKIAEXAMPLE"
162+
process.env.AWS_SECRET_ACCESS_KEY = "secret-example"
163+
164+
const config = getProviderSettings("bedrock", undefined, undefined)
165+
166+
expect(config.awsUseProfile).toBe(true)
167+
expect(config.awsProfile).toBe("my-profile")
168+
expect(config.awsAccessKey).toBeUndefined()
169+
expect(config.awsSecretKey).toBeUndefined()
170+
})
171+
})
172+
173+
describe("bedrock region resolution", () => {
174+
it("defaults awsRegion to us-east-1 when no region env is set", () => {
175+
const config = getProviderSettings("bedrock", undefined, undefined)
176+
expect(config.awsRegion).toBe("us-east-1")
177+
})
178+
179+
it("uses AWS_REGION when set", () => {
180+
process.env.AWS_REGION = "eu-west-1"
181+
const config = getProviderSettings("bedrock", undefined, undefined)
182+
expect(config.awsRegion).toBe("eu-west-1")
183+
})
184+
185+
it("falls back to AWS_DEFAULT_REGION when AWS_REGION is unset", () => {
186+
process.env.AWS_DEFAULT_REGION = "ap-southeast-2"
187+
const config = getProviderSettings("bedrock", undefined, undefined)
188+
expect(config.awsRegion).toBe("ap-southeast-2")
189+
})
190+
191+
it("prefers AWS_REGION over AWS_DEFAULT_REGION", () => {
192+
process.env.AWS_REGION = "eu-west-1"
193+
process.env.AWS_DEFAULT_REGION = "ap-southeast-2"
194+
const config = getProviderSettings("bedrock", undefined, undefined)
195+
expect(config.awsRegion).toBe("eu-west-1")
196+
})
197+
})
198+
199+
describe("bedrock cross-region inference auto-enable", () => {
200+
it.each([
201+
"us.anthropic.claude-3-5-sonnet-20241022-v2:0",
202+
"eu.anthropic.claude-3-5-sonnet",
203+
"apac.amazon.nova-pro",
204+
])("enables awsUseCrossRegionInference for regional-prefixed model '%s'", (model) => {
205+
const config = getProviderSettings("bedrock", undefined, model)
206+
expect(config.apiModelId).toBe(model)
207+
expect(config.awsUseCrossRegionInference).toBe(true)
208+
})
209+
210+
it("does NOT enable cross-region inference for a non-prefixed model", () => {
211+
const model = "anthropic.claude-3-5-sonnet-20241022-v2:0"
212+
const config = getProviderSettings("bedrock", undefined, model)
213+
expect(config.apiModelId).toBe(model)
214+
expect(config.awsUseCrossRegionInference).toBeUndefined()
215+
})
216+
217+
it("does not set apiModelId or cross-region flag when no model is given", () => {
218+
const config = getProviderSettings("bedrock", undefined, undefined)
219+
expect(config.apiModelId).toBeUndefined()
220+
expect(config.awsUseCrossRegionInference).toBeUndefined()
221+
})
222+
})
223+
})

apps/cli/src/lib/utils/provider.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,40 @@ export function getApiKeyFromEnv(provider: SupportedProvider): string | undefine
2020
return process.env[envVar]
2121
}
2222

23+
/**
24+
* Whether a provider requires an explicit API key before a task can run.
25+
*
26+
* Every provider except Bedrock authenticates solely via an API key
27+
* (`--api-key` or its provider-specific environment variable). Bedrock is the
28+
* exception: in addition to a bearer token / API key, it can authenticate via
29+
* an AWS profile, direct AWS access/secret keys, OR the AWS SDK default
30+
* credential chain (IMDS / EC2 instance profile, ECS task role, IRSA /
31+
* web-identity, SSO, shared-config default). Those chain-based sources set none
32+
* of our recognised environment variables, so Bedrock must never be hard-failed
33+
* for a "missing" API key — the downstream `AwsBedrockHandler` resolves the
34+
* credentials and surfaces a real error only if resolution actually fails.
35+
*/
36+
export function providerRequiresApiKey(provider: SupportedProvider): boolean {
37+
return provider !== "bedrock"
38+
}
39+
40+
/**
41+
* Build the provider-specific `RooCodeSettings` used to configure the extension
42+
* host for a CLI run.
43+
*
44+
* The Bedrock case supports four authentication modes, resolved in priority
45+
* order:
46+
* 1. Bearer token / API key — `--api-key` or `AWS_BEDROCK_API_KEY`
47+
* (`awsUseApiKey` + `awsApiKey`).
48+
* 2. AWS profile — `AWS_PROFILE` (`awsUseProfile` + `awsProfile`).
49+
* 3. Direct credentials — `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`
50+
* (+ optional `AWS_SESSION_TOKEN`).
51+
* 4. Default credential chain — none of the above set; the AWS SDK resolves
52+
* credentials (IMDS, ECS task role, IRSA, SSO, shared-config default).
53+
* The region is resolved from `AWS_REGION` / `AWS_DEFAULT_REGION` (defaulting to
54+
* `us-east-1`), and cross-region inference is auto-enabled for model IDs that
55+
* carry a regional prefix (`us.` / `eu.` / `apac.`).
56+
*/
2357
export function getProviderSettings(
2458
provider: SupportedProvider,
2559
apiKey: string | undefined,

0 commit comments

Comments
 (0)