Skip to content

Commit 7a61e6a

Browse files
committed
Support AWS profile to configure Bedrock Authentication
Added support for configurations under ~/.aws/credentials or ~/.aws/config.
1 parent 084599c commit 7a61e6a

File tree

6 files changed

+145
-27
lines changed

6 files changed

+145
-27
lines changed

.changeset/afraid-pillows-kiss.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": minor
3+
---
4+
5+
Added suport for configuring Bedrock provider with AWS Profiles. Useful for users with SSO or other integrations who don't have access to long term credentials.

src/api/providers/__tests__/bedrock.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
// Mock AWS SDK credential providers
2+
jest.mock("@aws-sdk/credential-providers", () => ({
3+
fromIni: jest.fn().mockReturnValue({
4+
accessKeyId: "profile-access-key",
5+
secretAccessKey: "profile-secret-key",
6+
}),
7+
}))
8+
19
import { AwsBedrockHandler } from "../bedrock"
210
import { MessageContent } from "../../../shared/api"
311
import { BedrockRuntimeClient } from "@aws-sdk/client-bedrock-runtime"
412
import { Anthropic } from "@anthropic-ai/sdk"
13+
import { fromIni } from "@aws-sdk/credential-providers"
514

615
describe("AwsBedrockHandler", () => {
716
let handler: AwsBedrockHandler
@@ -30,6 +39,65 @@ describe("AwsBedrockHandler", () => {
3039
})
3140
expect(handlerWithoutCreds).toBeInstanceOf(AwsBedrockHandler)
3241
})
42+
43+
it("should initialize with AWS profile credentials", () => {
44+
const handlerWithProfile = new AwsBedrockHandler({
45+
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
46+
awsRegion: "us-east-1",
47+
awsUseProfile: true,
48+
awsProfile: "test-profile",
49+
})
50+
expect(handlerWithProfile).toBeInstanceOf(AwsBedrockHandler)
51+
expect(handlerWithProfile["options"].awsUseProfile).toBe(true)
52+
expect(handlerWithProfile["options"].awsProfile).toBe("test-profile")
53+
})
54+
55+
it("should initialize with AWS profile enabled but no profile set", () => {
56+
const handlerWithoutProfile = new AwsBedrockHandler({
57+
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
58+
awsRegion: "us-east-1",
59+
awsUseProfile: true,
60+
})
61+
expect(handlerWithoutProfile).toBeInstanceOf(AwsBedrockHandler)
62+
expect(handlerWithoutProfile["options"].awsUseProfile).toBe(true)
63+
expect(handlerWithoutProfile["options"].awsProfile).toBeUndefined()
64+
})
65+
})
66+
67+
describe("AWS SDK client configuration", () => {
68+
it("should configure client with profile credentials when profile mode is enabled", async () => {
69+
// Import the fromIni function to mock it
70+
jest.mock("@aws-sdk/credential-providers", () => ({
71+
fromIni: jest.fn().mockReturnValue({
72+
accessKeyId: "profile-access-key",
73+
secretAccessKey: "profile-secret-key",
74+
}),
75+
}))
76+
77+
const handlerWithProfile = new AwsBedrockHandler({
78+
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
79+
awsRegion: "us-east-1",
80+
awsUseProfile: true,
81+
awsProfile: "test-profile",
82+
})
83+
84+
// Mock a simple API call to verify credentials are used
85+
const mockResponse = {
86+
output: new TextEncoder().encode(JSON.stringify({ content: "test" })),
87+
}
88+
const mockSend = jest.fn().mockResolvedValue(mockResponse)
89+
handlerWithProfile["client"] = {
90+
send: mockSend,
91+
} as unknown as BedrockRuntimeClient
92+
93+
await handlerWithProfile.completePrompt("test")
94+
95+
// Verify the client was configured with profile credentials
96+
expect(mockSend).toHaveBeenCalled()
97+
expect(fromIni).toHaveBeenCalledWith({
98+
profile: "test-profile",
99+
})
100+
})
33101
})
34102

35103
describe("createMessage", () => {

src/api/providers/bedrock.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ConverseCommand,
55
BedrockRuntimeClientConfig,
66
} from "@aws-sdk/client-bedrock-runtime"
7+
import { fromIni } from "@aws-sdk/credential-providers"
78
import { Anthropic } from "@anthropic-ai/sdk"
89
import { ApiHandler, SingleCompletionHandler } from "../"
910
import { ApiHandlerOptions, BedrockModelId, ModelInfo, bedrockDefaultModelId, bedrockModels } from "../../shared/api"
@@ -50,13 +51,17 @@ export class AwsBedrockHandler implements ApiHandler, SingleCompletionHandler {
5051
constructor(options: ApiHandlerOptions) {
5152
this.options = options
5253

53-
// Only include credentials if they actually exist
5454
const clientConfig: BedrockRuntimeClientConfig = {
5555
region: this.options.awsRegion || "us-east-1",
5656
}
5757

58-
if (this.options.awsAccessKey && this.options.awsSecretKey) {
59-
// Create credentials object with all properties at once
58+
if (this.options.awsUseProfile && this.options.awsProfile) {
59+
// Use profile-based credentials if enabled and profile is set
60+
clientConfig.credentials = fromIni({
61+
profile: this.options.awsProfile,
62+
})
63+
} else if (this.options.awsAccessKey && this.options.awsSecretKey) {
64+
// Use direct credentials if provided
6065
clientConfig.credentials = {
6166
accessKeyId: this.options.awsAccessKey,
6267
secretAccessKey: this.options.awsSecretKey,

src/core/webview/ClineProvider.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ type GlobalStateKey =
5656
| "glamaModelInfo"
5757
| "awsRegion"
5858
| "awsUseCrossRegionInference"
59+
| "awsProfile"
60+
| "awsUseProfile"
5961
| "vertexProjectId"
6062
| "vertexRegion"
6163
| "lastShownAnnouncementId"
@@ -1147,6 +1149,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
11471149
awsSessionToken,
11481150
awsRegion,
11491151
awsUseCrossRegionInference,
1152+
awsProfile,
1153+
awsUseProfile,
11501154
vertexProjectId,
11511155
vertexRegion,
11521156
openAiBaseUrl,
@@ -1180,6 +1184,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
11801184
await this.storeSecret("awsSessionToken", awsSessionToken)
11811185
await this.updateGlobalState("awsRegion", awsRegion)
11821186
await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference)
1187+
await this.updateGlobalState("awsProfile", awsProfile)
1188+
await this.updateGlobalState("awsUseProfile", awsUseProfile)
11831189
await this.updateGlobalState("vertexProjectId", vertexProjectId)
11841190
await this.updateGlobalState("vertexRegion", vertexRegion)
11851191
await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
@@ -1795,6 +1801,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
17951801
awsSessionToken,
17961802
awsRegion,
17971803
awsUseCrossRegionInference,
1804+
awsProfile,
1805+
awsUseProfile,
17981806
vertexProjectId,
17991807
vertexRegion,
18001808
openAiBaseUrl,
@@ -1857,6 +1865,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
18571865
this.getSecret("awsSessionToken") as Promise<string | undefined>,
18581866
this.getGlobalState("awsRegion") as Promise<string | undefined>,
18591867
this.getGlobalState("awsUseCrossRegionInference") as Promise<boolean | undefined>,
1868+
this.getGlobalState("awsProfile") as Promise<string | undefined>,
1869+
this.getGlobalState("awsUseProfile") as Promise<boolean | undefined>,
18601870
this.getGlobalState("vertexProjectId") as Promise<string | undefined>,
18611871
this.getGlobalState("vertexRegion") as Promise<string | undefined>,
18621872
this.getGlobalState("openAiBaseUrl") as Promise<string | undefined>,
@@ -1936,6 +1946,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
19361946
awsSessionToken,
19371947
awsRegion,
19381948
awsUseCrossRegionInference,
1949+
awsProfile,
1950+
awsUseProfile,
19391951
vertexProjectId,
19401952
vertexRegion,
19411953
openAiBaseUrl,

src/shared/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export interface ApiHandlerOptions {
3333
awsUseCrossRegionInference?: boolean
3434
awsUsePromptCache?: boolean
3535
awspromptCacheId?: string
36+
awsProfile?: string
37+
awsUseProfile?: boolean
3638
vertexProjectId?: string
3739
vertexRegion?: string
3840
openAiBaseUrl?: string

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -342,30 +342,56 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
342342

343343
{selectedProvider === "bedrock" && (
344344
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
345-
<VSCodeTextField
346-
value={apiConfiguration?.awsAccessKey || ""}
347-
style={{ width: "100%" }}
348-
type="password"
349-
onInput={handleInputChange("awsAccessKey")}
350-
placeholder="Enter Access Key...">
351-
<span style={{ fontWeight: 500 }}>AWS Access Key</span>
352-
</VSCodeTextField>
353-
<VSCodeTextField
354-
value={apiConfiguration?.awsSecretKey || ""}
355-
style={{ width: "100%" }}
356-
type="password"
357-
onInput={handleInputChange("awsSecretKey")}
358-
placeholder="Enter Secret Key...">
359-
<span style={{ fontWeight: 500 }}>AWS Secret Key</span>
360-
</VSCodeTextField>
361-
<VSCodeTextField
362-
value={apiConfiguration?.awsSessionToken || ""}
363-
style={{ width: "100%" }}
364-
type="password"
365-
onInput={handleInputChange("awsSessionToken")}
366-
placeholder="Enter Session Token...">
367-
<span style={{ fontWeight: 500 }}>AWS Session Token</span>
368-
</VSCodeTextField>
345+
<VSCodeRadioGroup
346+
value={apiConfiguration?.awsUseProfile ? "profile" : "credentials"}
347+
onChange={(e) => {
348+
const value = (e.target as HTMLInputElement)?.value
349+
const useProfile = value === "profile"
350+
handleInputChange("awsUseProfile")({
351+
target: { value: useProfile },
352+
})
353+
}}>
354+
<VSCodeRadio value="credentials">AWS Credentials</VSCodeRadio>
355+
<VSCodeRadio value="profile">AWS Profile</VSCodeRadio>
356+
</VSCodeRadioGroup>
357+
{/* AWS Profile Config Block */}
358+
{apiConfiguration?.awsUseProfile ? (
359+
<VSCodeTextField
360+
value={apiConfiguration?.awsProfile || ""}
361+
style={{ width: "100%" }}
362+
onInput={handleInputChange("awsProfile")}
363+
placeholder="Enter profile name">
364+
<span style={{ fontWeight: 500 }}>AWS Profile Name</span>
365+
</VSCodeTextField>
366+
) : (
367+
<>
368+
{/* AWS Credentials Config Block */}
369+
<VSCodeTextField
370+
value={apiConfiguration?.awsAccessKey || ""}
371+
style={{ width: "100%" }}
372+
type="password"
373+
onInput={handleInputChange("awsAccessKey")}
374+
placeholder="Enter Access Key...">
375+
<span style={{ fontWeight: 500 }}>AWS Access Key</span>
376+
</VSCodeTextField>
377+
<VSCodeTextField
378+
value={apiConfiguration?.awsSecretKey || ""}
379+
style={{ width: "100%" }}
380+
type="password"
381+
onInput={handleInputChange("awsSecretKey")}
382+
placeholder="Enter Secret Key...">
383+
<span style={{ fontWeight: 500 }}>AWS Secret Key</span>
384+
</VSCodeTextField>
385+
<VSCodeTextField
386+
value={apiConfiguration?.awsSessionToken || ""}
387+
style={{ width: "100%" }}
388+
type="password"
389+
onInput={handleInputChange("awsSessionToken")}
390+
placeholder="Enter Session Token...">
391+
<span style={{ fontWeight: 500 }}>AWS Session Token</span>
392+
</VSCodeTextField>
393+
</>
394+
)}
369395
<div className="dropdown-container">
370396
<label htmlFor="aws-region-dropdown">
371397
<span style={{ fontWeight: 500 }}>AWS Region</span>

0 commit comments

Comments
 (0)