From f66df86aed2c66c81655533b3a4a841f1912dabc Mon Sep 17 00:00:00 2001
From: Yevhenii Shcherbina
Date: Wed, 24 Jun 2026 21:48:33 -0400
Subject: [PATCH] feat: add Bedrock Role ARN field in UI (#26578)
Adds a Role ARN field for the Bedrock provider in the UI. When set, the
gateway assumes that IAM role (using the base identity) before calling
Bedrock. The field is optional and non-secret, so it round-trips back
into the form on edit and clears when left blank.
Related PR: https://github.com/coder/coder/pull/26527
Related issue:
https://linear.app/codercom/issue/AIGOV-371/support-dynamic-bedrock-assumerole-across-aws-accounts-for-ai-gateway
---
.../ProvidersPage/components/ProviderForm.tsx | 12 ++++
.../components/providerFormApiMap.test.ts | 70 +++++++++++++++++++
.../components/providerFormApiMap.ts | 5 ++
3 files changed, 87 insertions(+)
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderForm.tsx b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderForm.tsx
index 99a1d24a92..48b560add7 100644
--- a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderForm.tsx
+++ b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderForm.tsx
@@ -25,6 +25,7 @@ export type ProviderFormValues = {
smallFastModel: string;
accessKey: string;
accessKeySecret: string;
+ roleArn: string;
apiKey: string;
enabled: boolean;
};
@@ -66,6 +67,7 @@ const defaultInitialValues: ProviderFormValues = {
smallFastModel: "",
accessKey: "",
accessKeySecret: "",
+ roleArn: "",
apiKey: "",
enabled: true,
};
@@ -526,6 +528,16 @@ export const ProviderForm: FC = ({
View docs
+
+
+ Optional. When a role ARN is set, the gateway assumes that role
+ (using the base identity) before calling Bedrock.
+
>
)}
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.test.ts b/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.test.ts
index 1ae786008c..a721fd6e82 100644
--- a/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.test.ts
+++ b/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.test.ts
@@ -29,6 +29,7 @@ const baseOpenAIFormValues: ProviderFormValues = {
smallFastModel: "",
accessKey: "",
accessKeySecret: "",
+ roleArn: "",
apiKey: "sk-test",
enabled: true,
};
@@ -42,6 +43,7 @@ const baseBedrockFormValues: ProviderFormValues = {
smallFastModel: "anthropic.claude-haiku-4-5",
accessKey: "AKIA-test",
accessKeySecret: "secret",
+ roleArn: "",
apiKey: "",
enabled: true,
};
@@ -55,6 +57,7 @@ const baseCopilotFormValues: ProviderFormValues = {
smallFastModel: "",
accessKey: "",
accessKeySecret: "",
+ roleArn: "",
apiKey: "",
enabled: true,
};
@@ -407,6 +410,30 @@ describe("providerFormValuesToCreate", () => {
});
expect(req.api_keys).toBeUndefined();
});
+
+ it("includes role_arn when a role ARN is provided", () => {
+ const req = providerFormValuesToCreate({
+ ...baseBedrockFormValues,
+ roleArn: "arn:aws:iam::123456789012:role/BedrockRole",
+ });
+ const s = req.settings as unknown as Record;
+ expect(s.role_arn).toBe("arn:aws:iam::123456789012:role/BedrockRole");
+ });
+
+ it("omits role_arn when the form value is blank", () => {
+ const req = providerFormValuesToCreate(baseBedrockFormValues);
+ const s = req.settings as unknown as Record;
+ expect(s.role_arn).toBeUndefined();
+ });
+
+ it("trims whitespace around the role ARN", () => {
+ const req = providerFormValuesToCreate({
+ ...baseBedrockFormValues,
+ roleArn: " arn:aws:iam::123456789012:role/BedrockRole ",
+ });
+ const s = req.settings as unknown as Record;
+ expect(s.role_arn).toBe("arn:aws:iam::123456789012:role/BedrockRole");
+ });
});
describe("Copilot", () => {
@@ -559,6 +586,32 @@ describe("providerFormValuesToUpdate", () => {
expect(s.access_key).toBeUndefined();
expect(s.access_key_secret).toBeUndefined();
});
+
+ it("sends role_arn even when the access keys are kept", () => {
+ const req = providerFormValuesToUpdate(
+ {
+ ...baseBedrockFormValues,
+ accessKey: SAVED_CREDENTIAL_MASK,
+ accessKeySecret: SAVED_CREDENTIAL_MASK,
+ roleArn: "arn:aws:iam::123456789012:role/BedrockRole",
+ },
+ MockAIProviderBedrock,
+ );
+ const s = req.settings as unknown as Record;
+ expect(s.role_arn).toBe("arn:aws:iam::123456789012:role/BedrockRole");
+ });
+
+ it("omits role_arn when the field is cleared", () => {
+ const req = providerFormValuesToUpdate(
+ {
+ ...baseBedrockFormValues,
+ roleArn: "",
+ },
+ MockAIProviderBedrock,
+ );
+ const s = req.settings as unknown as Record;
+ expect(s.role_arn).toBeUndefined();
+ });
});
describe("Copilot", () => {
@@ -635,6 +688,23 @@ describe("aiProviderToFormValues", () => {
expect(values.accessKeySecret).toBe("");
});
+ it("round-trips role_arn back into the form", () => {
+ const provider: AIProvider = {
+ ...MockAIProviderBedrock,
+ settings: settings({
+ _type: "bedrock",
+ role_arn: "arn:aws:iam::123456789012:role/BedrockRole",
+ }),
+ };
+ const values = aiProviderToFormValues(provider);
+ expect(values.roleArn).toBe("arn:aws:iam::123456789012:role/BedrockRole");
+ });
+
+ it("seeds an empty role ARN when the provider has none", () => {
+ const values = aiProviderToFormValues(MockAIProviderBedrock);
+ expect(values.roleArn).toBe("");
+ });
+
it("seeds Copilot form values without a credential field", () => {
const values = aiProviderToFormValues(MockAIProviderCopilot);
expect(values.type).toBe("copilot");
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.ts b/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.ts
index 67eec7e4d9..2eaaa65a4f 100644
--- a/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.ts
+++ b/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.ts
@@ -109,6 +109,7 @@ const buildBedrockSettings = (
smallFastModel: string,
accessKey: string,
accessKeySecret: string,
+ roleArn: string,
): BedrockSettingsWire => ({
_type: BEDROCK_SETTINGS_TYPE,
_version: BEDROCK_SETTINGS_VERSION,
@@ -117,6 +118,7 @@ const buildBedrockSettings = (
small_fast_model: smallFastModel,
...(accessKey ? { access_key: accessKey } : {}),
...(accessKeySecret ? { access_key_secret: accessKeySecret } : {}),
+ ...(roleArn ? { role_arn: roleArn } : {}),
});
// Bedrock credentials live in `settings`; openai/anthropic keys go in
@@ -141,6 +143,7 @@ export const providerFormValuesToCreate = (
values.smallFastModel.trim(),
sanitizeCredential(values.accessKey),
sanitizeCredential(values.accessKeySecret),
+ values.roleArn.trim(),
);
return {
type: "anthropic",
@@ -215,6 +218,7 @@ export const providerFormValuesToUpdate = (
values.smallFastModel.trim(),
credentialsChanged ? newAccessKey : "",
credentialsChanged ? newAccessKeySecret : "",
+ values.roleArn.trim(),
);
return { ...base, settings: settings as AIProviderSettings };
@@ -238,6 +242,7 @@ export const aiProviderToFormValues = (
smallFastModel: s.small_fast_model ?? "",
accessKey: "",
accessKeySecret: "",
+ roleArn: s.role_arn ?? "",
enabled: provider.enabled,
};
}