From 2573afab2529f1fcd96763dcc51e0456c70f5836 Mon Sep 17 00:00:00 2001
From: evgeniy-scherbina
Date: Tue, 30 Jun 2026 08:44:32 -0400
Subject: [PATCH 1/6] feat: generate STS external ID for Bedrock role
assumption
---
aibridge/config/config.go | 4 +
aibridge/provider/bedrock.go | 3 +
aibridge/provider/bedrock_internal_test.go | 55 ++++++
cli/aibridged.go | 2 +
coderd/ai_providers.go | 55 ++++++
coderd/ai_providers_internal_test.go | 86 ++++++++++
coderd/ai_providers_test.go | 185 +++++++++++++++++++++
coderd/aibridged/proto/aibridged.pb.go | 142 ++++++++--------
coderd/aibridged/proto/aibridged.proto | 1 +
coderd/aibridgedserver/aibridgedserver.go | 1 +
codersdk/aiproviders.go | 6 +
codersdk/aiproviders_bedrock.go | 5 +
site/src/api/typesGenerated.ts | 10 ++
13 files changed, 489 insertions(+), 66 deletions(-)
create mode 100644 coderd/ai_providers_internal_test.go
diff --git a/aibridge/config/config.go b/aibridge/config/config.go
index 3dc76841aa3..b01282b0cc7 100644
--- a/aibridge/config/config.go
+++ b/aibridge/config/config.go
@@ -38,6 +38,10 @@ type AWSBedrock struct {
// IRSA / EKS Pod Identity / EC2 Instance Profile) signs the AssumeRole
// call, and the resulting temporary credentials sign Bedrock requests.
RoleARN string
+ // ExternalID is sent as the STS external ID on the AssumeRole call.
+ // It is meaningful only alongside RoleARN and must match the
+ // sts:ExternalId condition on the target role's trust policy.
+ ExternalID string
}
// OpenAI carries configuration for an OpenAI provider.
diff --git a/aibridge/provider/bedrock.go b/aibridge/provider/bedrock.go
index 93bbf7038a0..013430f1d7a 100644
--- a/aibridge/provider/bedrock.go
+++ b/aibridge/provider/bedrock.go
@@ -79,6 +79,9 @@ func buildBedrockCredentials(ctx context.Context, cfg config.AWSBedrock) (aws.Cr
if cfg.RoleARN != "" {
credsProvider = stscreds.NewAssumeRoleProvider(sts.NewFromConfig(base), cfg.RoleARN, func(o *stscreds.AssumeRoleOptions) {
o.RoleSessionName = bedrockSessionName
+ if cfg.ExternalID != "" {
+ o.ExternalID = aws.String(cfg.ExternalID)
+ }
})
credsProvider = aws.NewCredentialsCache(credsProvider)
}
diff --git a/aibridge/provider/bedrock_internal_test.go b/aibridge/provider/bedrock_internal_test.go
index 0ecae6c780b..1cb7764c894 100644
--- a/aibridge/provider/bedrock_internal_test.go
+++ b/aibridge/provider/bedrock_internal_test.go
@@ -207,6 +207,61 @@ func TestBuildBedrockCredentialsAssumeRole(t *testing.T) {
require.Equal(t, bedrockSessionName, gotSessionName)
}
+// TestBuildBedrockCredentialsAssumeRoleExternalID verifies that a configured
+// external ID is sent on the STS AssumeRole call, and that omitting it sends
+// no ExternalId parameter.
+// NOTE: no t.Parallel() because it uses t.Setenv.
+func TestBuildBedrockCredentialsAssumeRoleExternalID(t *testing.T) {
+ tests := []struct {
+ name string
+ externalID string
+ wantExternalID string
+ }{
+ {name: "with external id", externalID: "trust-policy-id-123", wantExternalID: "trust-policy-id-123"},
+ {name: "without external id", externalID: "", wantExternalID: ""},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var gotExternalID string
+ // Mock the AWS STS AssumeRole API.
+ // https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
+ sts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ require.NoError(t, r.ParseForm())
+ gotExternalID = r.Form.Get("ExternalId")
+
+ w.Header().Set("Content-Type", "text/xml")
+ _, _ = w.Write([]byte(`
+
+
+ ASIAASSUMED
+ assumed-secret
+ assumed-token
+ 2999-01-01T00:00:00Z
+
+
+`))
+ }))
+ defer sts.Close()
+
+ t.Setenv("AWS_ENDPOINT_URL_STS", sts.URL)
+ t.Setenv("AWS_ACCESS_KEY_ID", "base-key")
+ t.Setenv("AWS_SECRET_ACCESS_KEY", "base-secret")
+
+ creds, _, err := buildBedrockCredentials(context.Background(), config.AWSBedrock{
+ Region: "us-east-1",
+ RoleARN: "arn:aws:iam::123456789012:role/target",
+ ExternalID: tt.externalID,
+ })
+ require.NoError(t, err)
+
+ _, err = creds.Retrieve(context.Background())
+ require.NoError(t, err)
+ require.Equal(t, tt.wantExternalID, gotExternalID)
+ })
+ }
+}
+
// TestBuildBedrockCredentialsAssumeRoleError verifies that when STS rejects the
// AssumeRole call (e.g. a trust-policy or IAM denial), the failure surfaces to
// the caller on Retrieve with enough detail to diagnose it, rather than being
diff --git a/cli/aibridged.go b/cli/aibridged.go
index 87e583b5736..addeb3f9598 100644
--- a/cli/aibridged.go
+++ b/cli/aibridged.go
@@ -211,6 +211,7 @@ func protoToProviderSpec(pp *proto.AIProvider) aiProviderSpec {
b.GetSmallFastModel(),
)
bedrock.RoleARN = b.GetRoleArn()
+ bedrock.ExternalID = b.GetExternalId()
spec.Bedrock = ptr.Ref(bedrock)
}
return spec
@@ -352,6 +353,7 @@ func bedrockConfig(baseURL string, bedrock *codersdk.AIProviderBedrockSettings)
Model: bedrockSettings.Model,
SmallFastModel: bedrockSettings.SmallFastModel,
RoleARN: bedrockSettings.RoleARN,
+ ExternalID: bedrockSettings.ExternalID,
}
}
diff --git a/coderd/ai_providers.go b/coderd/ai_providers.go
index 49e5bb87504..a00e0268006 100644
--- a/coderd/ai_providers.go
+++ b/coderd/ai_providers.go
@@ -2,6 +2,7 @@ package coderd
import (
"context"
+ "crypto/rand"
"database/sql"
"encoding/json"
"errors"
@@ -178,6 +179,9 @@ func (api *API) aiProvidersCreate(rw http.ResponseWriter, r *http.Request) {
return
}
+ // Generate the server-owned external ID when the provider assumes a role.
+ ensureBedrockExternalID(&req.Settings)
+
settings, err := encodeAIProviderSettings(req.Settings)
if err != nil {
api.Logger.Error(ctx, "encode AI provider settings", slog.Error(err))
@@ -318,6 +322,9 @@ func (api *API) aiProvidersUpdate(rw http.ResponseWriter, r *http.Request) {
return xerrors.Errorf("decode existing settings: %w", err)
}
if req.Settings != nil {
+ if err := validateBedrockExternalIDUnchanged(existing, *req.Settings); err != nil {
+ return err
+ }
existing = mergeAIProviderSettings(existing, *req.Settings)
}
// Bedrock settings are only meaningful for anthropic- or
@@ -329,6 +336,9 @@ func (api *API) aiProvidersUpdate(rw http.ResponseWriter, r *http.Request) {
old.Type != database.AIProviderTypeBedrock {
return errAIProviderBedrockTypeMismatch
}
+ // Generate the server-owned external ID when the provider assumes a role
+ // and lacks one.
+ ensureBedrockExternalID(&existing)
settings, err := encodeAIProviderSettings(existing)
if err != nil {
return xerrors.Errorf("encode settings: %w", err)
@@ -400,6 +410,12 @@ func (api *API) aiProvidersUpdate(rw http.ResponseWriter, r *http.Request) {
})
return
}
+ if errors.Is(err, errAIProviderExternalIDReadOnly) {
+ httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
+ Message: "The Bedrock external ID is server-generated and cannot be changed.",
+ })
+ return
+ }
if errors.Is(err, errAIProviderKeyUnknown) {
// Use the sentinel directly so the response message does not
// leak the "execute transaction:" wrapper xerrors added on the
@@ -506,6 +522,12 @@ var errCopilotRejectsAPIKeys = xerrors.New("copilot providers do not accept api_
// the outer handler translates it into a 400.
var errAIProviderBedrockTypeMismatch = xerrors.New("bedrock settings are only valid for type=anthropic or type=bedrock")
+// errAIProviderExternalIDReadOnly is the sentinel returned from inside
+// the update transaction when a patch tries to change the server-owned
+// Bedrock external ID; the outer handler translates it into a 400. A
+// patch may echo the stored value but not set a different one.
+var errAIProviderExternalIDReadOnly = xerrors.New("external_id is server-generated and cannot be changed")
+
// errAIProviderInvalidName is returned from lookupAIProvider when the
// idOrName parameter is neither a UUID nor a syntactically-valid name.
// The handler translates this into a 400 so an integrator gets a hint
@@ -772,6 +794,39 @@ func mergeAIProviderSettings(existing, patch codersdk.AIProviderSettings) coders
if merged.AccessKeySecret == nil {
merged.AccessKeySecret = existing.Bedrock.AccessKeySecret
}
+ // The external ID is server-owned and stable: carry the stored value
+ // forward so a patch can't change it. A patch that sets a different
+ // value is rejected upstream.
+ merged.ExternalID = existing.Bedrock.ExternalID
}
return codersdk.AIProviderSettings{Bedrock: &merged}
}
+
+// validateBedrockExternalIDUnchanged rejects a patch that sets a Bedrock
+// external ID different from the stored one. A patch may echo the stored
+// value (read-modify-write resends it) but not change it; the value is
+// server-owned.
+func validateBedrockExternalIDUnchanged(existing, patch codersdk.AIProviderSettings) error {
+ stored := ""
+ if existing.Bedrock != nil {
+ stored = existing.Bedrock.ExternalID
+ }
+
+ provided := ""
+ if patch.Bedrock != nil {
+ provided = patch.Bedrock.ExternalID
+ }
+
+ if provided != "" && provided != stored {
+ return errAIProviderExternalIDReadOnly
+ }
+ return nil
+}
+
+// ensureBedrockExternalID assigns a server-owned STS external ID when the
+// Bedrock provider assumes a role and none is set yet.
+func ensureBedrockExternalID(s *codersdk.AIProviderSettings) {
+ if s.Bedrock != nil && s.Bedrock.RoleARN != "" && s.Bedrock.ExternalID == "" {
+ s.Bedrock.ExternalID = rand.Text()
+ }
+}
diff --git a/coderd/ai_providers_internal_test.go b/coderd/ai_providers_internal_test.go
new file mode 100644
index 00000000000..665a4dbfa0a
--- /dev/null
+++ b/coderd/ai_providers_internal_test.go
@@ -0,0 +1,86 @@
+package coderd
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/coder/coder/v2/codersdk"
+)
+
+// TestEnsureBedrockExternalID covers the server-owned external ID generation:
+// it generates only when a role is configured and none is set, and never
+// overwrites an existing value.
+func TestEnsureBedrockExternalID(t *testing.T) {
+ t.Parallel()
+
+ t.Run("NilBedrockIsNoOp", func(t *testing.T) {
+ t.Parallel()
+ s := codersdk.AIProviderSettings{}
+ ensureBedrockExternalID(&s)
+ require.Nil(t, s.Bedrock)
+ })
+
+ t.Run("NoRoleLeavesEmpty", func(t *testing.T) {
+ t.Parallel()
+ s := codersdk.AIProviderSettings{Bedrock: &codersdk.AIProviderBedrockSettings{Region: "us-east-1"}}
+ ensureBedrockExternalID(&s)
+ require.Empty(t, s.Bedrock.ExternalID)
+ })
+
+ t.Run("GeneratesWhenRoleSet", func(t *testing.T) {
+ t.Parallel()
+ s := codersdk.AIProviderSettings{Bedrock: &codersdk.AIProviderBedrockSettings{
+ RoleARN: "arn:aws:iam::123456789012:role/BedrockRole",
+ }}
+ ensureBedrockExternalID(&s)
+ require.NotEmpty(t, s.Bedrock.ExternalID)
+ // crypto/rand.Text returns a 26-character base32 string.
+ require.Len(t, s.Bedrock.ExternalID, 26)
+ })
+
+ t.Run("DoesNotOverwriteExisting", func(t *testing.T) {
+ t.Parallel()
+ s := codersdk.AIProviderSettings{Bedrock: &codersdk.AIProviderBedrockSettings{
+ RoleARN: "arn:aws:iam::123456789012:role/BedrockRole",
+ ExternalID: "existing-value",
+ }}
+ ensureBedrockExternalID(&s)
+ require.Equal(t, "existing-value", s.Bedrock.ExternalID)
+ })
+
+ t.Run("GeneratesUniqueValues", func(t *testing.T) {
+ t.Parallel()
+ seen := make(map[string]struct{})
+ for range 10 {
+ s := codersdk.AIProviderSettings{Bedrock: &codersdk.AIProviderBedrockSettings{
+ RoleARN: "arn:aws:iam::123456789012:role/BedrockRole",
+ }}
+ ensureBedrockExternalID(&s)
+ _, dup := seen[s.Bedrock.ExternalID]
+ require.False(t, dup, "external IDs must be unique per provider")
+ seen[s.Bedrock.ExternalID] = struct{}{}
+ }
+ })
+}
+
+// TestMergeAIProviderSettingsExternalID verifies the external ID is treated as
+// server-owned during a PATCH merge: a stored value is carried forward and
+// overrides the patch so it can't be changed.
+func TestMergeAIProviderSettingsExternalID(t *testing.T) {
+ t.Parallel()
+
+ roleARN := "arn:aws:iam::123456789012:role/BedrockRole"
+ existing := codersdk.AIProviderSettings{Bedrock: &codersdk.AIProviderBedrockSettings{
+ RoleARN: roleARN,
+ ExternalID: "stored-value",
+ }}
+ patch := codersdk.AIProviderSettings{Bedrock: &codersdk.AIProviderBedrockSettings{
+ RoleARN: roleARN,
+ ExternalID: "client-supplied-value",
+ }}
+ merged := mergeAIProviderSettings(existing, patch)
+ require.NotNil(t, merged.Bedrock)
+ require.Equal(t, roleARN, merged.Bedrock.RoleARN)
+ require.Equal(t, "stored-value", merged.Bedrock.ExternalID)
+}
diff --git a/coderd/ai_providers_test.go b/coderd/ai_providers_test.go
index 4fcc1dd63bc..6897ff7088d 100644
--- a/coderd/ai_providers_test.go
+++ b/coderd/ai_providers_test.go
@@ -1554,3 +1554,188 @@ func TestAIProviderSettingsMerge(t *testing.T) {
require.Equal(t, "", *persisted.Bedrock.AccessKeySecret)
})
}
+
+// TestAIProvidersBedrockExternalID covers the server-owned STS external ID:
+// it is generated when (and only when) the provider assumes a role, is
+// rejected when a client tries to set or change it, and is stable across
+// PATCHes that echo the stored value.
+func TestAIProvidersBedrockExternalID(t *testing.T) {
+ t.Parallel()
+
+ const (
+ roleARN = "arn:aws:iam::123456789012:role/BedrockRole"
+ externalIDReadOnlyMsg = "The Bedrock external ID is server-generated and cannot be changed."
+ )
+
+ createBedrock := func(t *testing.T, client *codersdk.Client, name string, b codersdk.AIProviderBedrockSettings) (codersdk.AIProvider, error) {
+ t.Helper()
+ ctx := testutil.Context(t, testutil.WaitLong)
+ //nolint:gocritic // Owner role is the audience for this endpoint.
+ return client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{
+ Type: codersdk.AIProviderTypeBedrock,
+ Name: name,
+ Enabled: true,
+ BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com",
+ Settings: codersdk.AIProviderSettings{Bedrock: &b},
+ })
+ }
+
+ t.Run("GeneratedWhenRoleSet", func(t *testing.T) {
+ t.Parallel()
+ client := coderdtest.New(t, nil)
+ _ = coderdtest.CreateFirstUser(t, client)
+ ctx := testutil.Context(t, testutil.WaitLong)
+
+ created, err := createBedrock(t, client, "bedrock-role", codersdk.AIProviderBedrockSettings{
+ Region: "us-east-1",
+ RoleARN: roleARN,
+ })
+ require.NoError(t, err)
+ require.NotNil(t, created.Settings.Bedrock)
+ require.NotEmpty(t, created.Settings.Bedrock.ExternalID, "external ID must be generated when a role is set")
+
+ // GET returns the same external ID.
+ got, err := client.AIProvider(ctx, created.ID.String())
+ require.NoError(t, err)
+ require.Equal(t, created.Settings.Bedrock.ExternalID, got.Settings.Bedrock.ExternalID)
+ })
+
+ t.Run("AbsentWithoutRole", func(t *testing.T) {
+ t.Parallel()
+ client := coderdtest.New(t, nil)
+ _ = coderdtest.CreateFirstUser(t, client)
+
+ created, err := createBedrock(t, client, "bedrock-no-role", codersdk.AIProviderBedrockSettings{Region: "us-east-1"})
+ require.NoError(t, err)
+ require.NotNil(t, created.Settings.Bedrock)
+ require.Empty(t, created.Settings.Bedrock.ExternalID, "no external ID without a role to assume")
+ })
+
+ t.Run("RejectsClientValueOnCreate", func(t *testing.T) {
+ t.Parallel()
+ client := coderdtest.New(t, nil)
+ _ = coderdtest.CreateFirstUser(t, client)
+
+ _, err := createBedrock(t, client, "bedrock-client-id", codersdk.AIProviderBedrockSettings{
+ Region: "us-east-1",
+ RoleARN: roleARN,
+ ExternalID: "client-supplied-value",
+ })
+ sdkErr := requireSDKError(t, err, http.StatusBadRequest)
+ require.Contains(t, sdkErr.Validations, codersdk.ValidationError{
+ Field: "settings.external_id",
+ Detail: "external_id is server-generated and cannot be set",
+ })
+ })
+
+ t.Run("StableWhenPatchOmitsValue", func(t *testing.T) {
+ t.Parallel()
+ client := coderdtest.New(t, nil)
+ _ = coderdtest.CreateFirstUser(t, client)
+ ctx := testutil.Context(t, testutil.WaitLong)
+
+ created, err := createBedrock(t, client, "bedrock-stable", codersdk.AIProviderBedrockSettings{
+ Region: "us-east-1",
+ RoleARN: roleARN,
+ })
+ require.NoError(t, err)
+ original := created.Settings.Bedrock.ExternalID
+ require.NotEmpty(t, original)
+
+ updated, err := client.UpdateAIProvider(ctx, created.Name, codersdk.UpdateAIProviderRequest{
+ Settings: &codersdk.AIProviderSettings{
+ Bedrock: &codersdk.AIProviderBedrockSettings{Region: "us-west-2", RoleARN: roleARN},
+ },
+ })
+ require.NoError(t, err)
+ require.Equal(t, "us-west-2", updated.Settings.Bedrock.Region)
+ require.Equal(t, original, updated.Settings.Bedrock.ExternalID, "external ID must be stable across PATCH")
+ })
+
+ t.Run("AllowsEchoedValueOnPatch", func(t *testing.T) {
+ t.Parallel()
+ client := coderdtest.New(t, nil)
+ _ = coderdtest.CreateFirstUser(t, client)
+ ctx := testutil.Context(t, testutil.WaitLong)
+
+ created, err := createBedrock(t, client, "bedrock-echo", codersdk.AIProviderBedrockSettings{
+ Region: "us-east-1",
+ RoleARN: roleARN,
+ })
+ require.NoError(t, err)
+ original := created.Settings.Bedrock.ExternalID
+ require.NotEmpty(t, original)
+
+ // Read-modify-write resends the whole settings, including the stored
+ // external ID. Echoing the same value is allowed.
+ updated, err := client.UpdateAIProvider(ctx, created.Name, codersdk.UpdateAIProviderRequest{
+ Settings: &codersdk.AIProviderSettings{
+ Bedrock: &codersdk.AIProviderBedrockSettings{Region: "us-west-2", RoleARN: roleARN, ExternalID: original},
+ },
+ })
+ require.NoError(t, err)
+ require.Equal(t, "us-west-2", updated.Settings.Bedrock.Region)
+ require.Equal(t, original, updated.Settings.Bedrock.ExternalID)
+ })
+
+ t.Run("RejectsChangedValueOnPatch", func(t *testing.T) {
+ t.Parallel()
+ client := coderdtest.New(t, nil)
+ _ = coderdtest.CreateFirstUser(t, client)
+ ctx := testutil.Context(t, testutil.WaitLong)
+
+ created, err := createBedrock(t, client, "bedrock-change", codersdk.AIProviderBedrockSettings{
+ Region: "us-east-1",
+ RoleARN: roleARN,
+ })
+ require.NoError(t, err)
+ require.NotEmpty(t, created.Settings.Bedrock.ExternalID)
+
+ _, err = client.UpdateAIProvider(ctx, created.Name, codersdk.UpdateAIProviderRequest{
+ Settings: &codersdk.AIProviderSettings{
+ Bedrock: &codersdk.AIProviderBedrockSettings{Region: "us-east-1", RoleARN: roleARN, ExternalID: "client-tries-to-change-it"},
+ },
+ })
+ sdkErr := requireSDKError(t, err, http.StatusBadRequest)
+ require.Equal(t, externalIDReadOnlyMsg, sdkErr.Message)
+ })
+
+ t.Run("GeneratedWhenRoleAddedByPatch", func(t *testing.T) {
+ t.Parallel()
+ client := coderdtest.New(t, nil)
+ _ = coderdtest.CreateFirstUser(t, client)
+ ctx := testutil.Context(t, testutil.WaitLong)
+
+ created, err := createBedrock(t, client, "bedrock-add-role", codersdk.AIProviderBedrockSettings{Region: "us-east-1"})
+ require.NoError(t, err)
+ require.Empty(t, created.Settings.Bedrock.ExternalID)
+
+ updated, err := client.UpdateAIProvider(ctx, created.Name, codersdk.UpdateAIProviderRequest{
+ Settings: &codersdk.AIProviderSettings{
+ Bedrock: &codersdk.AIProviderBedrockSettings{Region: "us-east-1", RoleARN: roleARN},
+ },
+ })
+ require.NoError(t, err)
+ require.NotEmpty(t, updated.Settings.Bedrock.ExternalID, "external ID must be generated when a role is added by PATCH")
+ })
+
+ t.Run("RejectsClientValueWhenRoleAddedByPatch", func(t *testing.T) {
+ t.Parallel()
+ client := coderdtest.New(t, nil)
+ _ = coderdtest.CreateFirstUser(t, client)
+ ctx := testutil.Context(t, testutil.WaitLong)
+
+ created, err := createBedrock(t, client, "bedrock-add-role-id", codersdk.AIProviderBedrockSettings{Region: "us-east-1"})
+ require.NoError(t, err)
+ require.Empty(t, created.Settings.Bedrock.ExternalID)
+
+ // No value is stored yet, so any client value is a change and is rejected.
+ _, err = client.UpdateAIProvider(ctx, created.Name, codersdk.UpdateAIProviderRequest{
+ Settings: &codersdk.AIProviderSettings{
+ Bedrock: &codersdk.AIProviderBedrockSettings{Region: "us-east-1", RoleARN: roleARN, ExternalID: "client-supplied-value"},
+ },
+ })
+ sdkErr := requireSDKError(t, err, http.StatusBadRequest)
+ require.Equal(t, externalIDReadOnlyMsg, sdkErr.Message)
+ })
+}
diff --git a/coderd/aibridged/proto/aibridged.pb.go b/coderd/aibridged/proto/aibridged.pb.go
index f503aaa8fb7..b4be124d972 100644
--- a/coderd/aibridged/proto/aibridged.pb.go
+++ b/coderd/aibridged/proto/aibridged.pb.go
@@ -1454,6 +1454,7 @@ type AIProviderKindBedrock struct {
Model string `protobuf:"bytes,4,opt,name=model,proto3" json:"model,omitempty"`
SmallFastModel string `protobuf:"bytes,5,opt,name=small_fast_model,json=smallFastModel,proto3" json:"small_fast_model,omitempty"`
RoleArn string `protobuf:"bytes,6,opt,name=role_arn,json=roleArn,proto3" json:"role_arn,omitempty"`
+ ExternalId string `protobuf:"bytes,7,opt,name=external_id,json=externalId,proto3" json:"external_id,omitempty"`
}
func (x *AIProviderKindBedrock) Reset() {
@@ -1530,6 +1531,13 @@ func (x *AIProviderKindBedrock) GetRoleArn() string {
return ""
}
+func (x *AIProviderKindBedrock) GetExternalId() string {
+ if x != nil {
+ return x.ExternalId
+ }
+ return ""
+}
+
var File_coderd_aibridged_proto_aibridged_proto protoreflect.FileDescriptor
var file_coderd_aibridged_proto_aibridged_proto_rawDesc = []byte{
@@ -1802,7 +1810,7 @@ var file_coderd_aibridged_proto_aibridged_proto_rawDesc = []byte{
0x6b, 0x65, 0x79, 0x73, 0x12, 0x36, 0x0a, 0x07, 0x62, 0x65, 0x64, 0x72, 0x6f, 0x63, 0x6b, 0x18,
0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x49,
0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4b, 0x69, 0x6e, 0x64, 0x42, 0x65, 0x64, 0x72,
- 0x6f, 0x63, 0x6b, 0x52, 0x07, 0x62, 0x65, 0x64, 0x72, 0x6f, 0x63, 0x6b, 0x22, 0xd5, 0x01, 0x0a,
+ 0x6f, 0x63, 0x6b, 0x52, 0x07, 0x62, 0x65, 0x64, 0x72, 0x6f, 0x63, 0x6b, 0x22, 0xf6, 0x01, 0x0a,
0x15, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4b, 0x69, 0x6e, 0x64, 0x42,
0x65, 0x64, 0x72, 0x6f, 0x63, 0x6b, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x12, 0x1d,
@@ -1816,72 +1824,74 @@ var file_coderd_aibridged_proto_aibridged_proto_rawDesc = []byte{
0x64, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x6d, 0x61, 0x6c, 0x6c,
0x46, 0x61, 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x6f, 0x6c,
0x65, 0x5f, 0x61, 0x72, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x6f, 0x6c,
- 0x65, 0x41, 0x72, 0x6e, 0x32, 0xa9, 0x04, 0x0a, 0x08, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x65,
- 0x72, 0x12, 0x59, 0x0a, 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72,
- 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e,
- 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69,
- 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74,
- 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70,
- 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x17,
- 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69,
- 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x12, 0x25, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e,
- 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69,
- 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26,
- 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74,
- 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65,
- 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x10, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
- 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f,
- 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73,
- 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f,
- 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73,
- 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x11, 0x52,
- 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65,
- 0x12, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50,
- 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
- 0x74, 0x1a, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
- 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
- 0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a, 0x0f, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f,
- 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52,
+ 0x65, 0x41, 0x72, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c,
+ 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x72,
+ 0x6e, 0x61, 0x6c, 0x49, 0x64, 0x32, 0xa9, 0x04, 0x0a, 0x08, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
+ 0x65, 0x72, 0x12, 0x59, 0x0a, 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65,
+ 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74,
+ 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f,
+ 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65,
+ 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a,
+ 0x17, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74,
+ 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x12, 0x25, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74,
+ 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
+ 0x26, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e,
+ 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x10, 0x52, 0x65, 0x63, 0x6f, 0x72,
+ 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x2e, 0x70, 0x72,
+ 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55,
+ 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72,
+ 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55,
+ 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x11,
+ 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67,
+ 0x65, 0x12, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
+ 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72,
+ 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a, 0x0f, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f,
+ 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e,
+ 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52,
0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65,
- 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65,
- 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73,
- 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x59, 0x0a, 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d,
- 0x6f, 0x64, 0x65, 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x12, 0x20, 0x2e, 0x70, 0x72,
- 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x54,
- 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e,
- 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65,
- 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
- 0x32, 0xeb, 0x01, 0x0a, 0x0f, 0x4d, 0x43, 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72,
- 0x61, 0x74, 0x6f, 0x72, 0x12, 0x5c, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65,
- 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x12, 0x21, 0x2e, 0x70, 0x72,
- 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
- 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22,
- 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72,
- 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
- 0x73, 0x65, 0x12, 0x7a, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76,
- 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61,
- 0x74, 0x63, 0x68, 0x12, 0x2b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d,
- 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f,
- 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
- 0x1a, 0x2c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53,
- 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
- 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x55,
- 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x72, 0x12, 0x47, 0x0a, 0x0c,
- 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x12, 0x1a, 0x2e, 0x70,
- 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65,
- 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
- 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, 0x73,
- 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x65, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65,
- 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x4d, 0x0a,
- 0x0e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12,
- 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f,
- 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e,
- 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69,
- 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x32, 0x5a, 0x30,
- 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72,
- 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x64,
- 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
- 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x59, 0x0a, 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
+ 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x12, 0x20, 0x2e, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c,
+ 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21,
+ 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64,
+ 0x65, 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+ 0x65, 0x32, 0xeb, 0x01, 0x0a, 0x0f, 0x4d, 0x43, 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75,
+ 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x5c, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53,
+ 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x12, 0x21, 0x2e, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65,
+ 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
+ 0x22, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65,
+ 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x12, 0x7a, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72,
+ 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42,
+ 0x61, 0x74, 0x63, 0x68, 0x12, 0x2b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74,
+ 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54,
+ 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50,
+ 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65,
+ 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32,
+ 0x55, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x72, 0x12, 0x47, 0x0a,
+ 0x0c, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x12, 0x1a, 0x2e,
+ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a,
+ 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74,
+ 0x6f, 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x65, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64,
+ 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x4d,
+ 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73,
+ 0x12, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72,
+ 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d,
+ 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76,
+ 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x32, 0x5a,
+ 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65,
+ 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72,
+ 0x64, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74,
+ 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
diff --git a/coderd/aibridged/proto/aibridged.proto b/coderd/aibridged/proto/aibridged.proto
index 1d22ccdaa81..8dc78e7a894 100644
--- a/coderd/aibridged/proto/aibridged.proto
+++ b/coderd/aibridged/proto/aibridged.proto
@@ -193,4 +193,5 @@ message AIProviderKindBedrock {
string model = 4;
string small_fast_model = 5;
string role_arn = 6;
+ string external_id = 7;
}
diff --git a/coderd/aibridgedserver/aibridgedserver.go b/coderd/aibridgedserver/aibridgedserver.go
index 78f11cd9cc8..8b539153e98 100644
--- a/coderd/aibridgedserver/aibridgedserver.go
+++ b/coderd/aibridgedserver/aibridgedserver.go
@@ -883,6 +883,7 @@ func aiProviderToProto(row database.AIProvider, keys []database.AIProviderKey) (
Model: settings.Bedrock.Model,
SmallFastModel: settings.Bedrock.SmallFastModel,
RoleArn: settings.Bedrock.RoleARN,
+ ExternalId: settings.Bedrock.ExternalID,
}
}
diff --git a/codersdk/aiproviders.go b/codersdk/aiproviders.go
index 6cf8fb3359e..bca27f13610 100644
--- a/codersdk/aiproviders.go
+++ b/codersdk/aiproviders.go
@@ -249,6 +249,12 @@ func (req CreateAIProviderRequest) Validate() []ValidationError {
}
if req.Settings.Bedrock != nil {
validations = append(validations, validateAIProviderRoleARN(req.Settings.Bedrock.RoleARN)...)
+ if req.Settings.Bedrock.ExternalID != "" {
+ validations = append(validations, ValidationError{
+ Field: "settings.external_id",
+ Detail: "external_id is server-generated and cannot be set",
+ })
+ }
}
if req.Type == AIProviderTypeCopilot && len(req.APIKeys) > 0 {
validations = append(validations, ValidationError{
diff --git a/codersdk/aiproviders_bedrock.go b/codersdk/aiproviders_bedrock.go
index 360b763333c..be17f6c95d0 100644
--- a/codersdk/aiproviders_bedrock.go
+++ b/codersdk/aiproviders_bedrock.go
@@ -35,6 +35,11 @@ type AIProviderBedrockSettings struct {
// IRSA / EKS Pod Identity / EC2 Instance Profile) signs the AssumeRole
// call, and the resulting temporary credentials sign Bedrock requests.
RoleARN string `json:"role_arn,omitempty"`
+ // ExternalID is the STS external ID sent on the AssumeRole call when
+ // RoleARN is set. The server generates and owns it: create and update
+ // reject any client-supplied value that differs from the stored one (an
+ // update may echo the stored value back).
+ ExternalID string `json:"external_id,omitempty"`
}
// IsConfigured reports whether any load-bearing Bedrock field is set,
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index cd7ca431860..94de690172d 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -335,6 +335,16 @@ export interface AIProviderBedrockSettings {
* call, and the resulting temporary credentials sign Bedrock requests.
*/
readonly role_arn?: string;
+ /**
+ * ExternalID is the STS external ID sent on the AssumeRole call when
+ * RoleARN is set. The server generates and owns it: create and update
+ * reject any client-supplied value that differs from the stored one (an
+ * update may echo the stored value back). Operators add it to the target
+ * role's trust policy as an sts:ExternalId condition to constrain the
+ * assumption to this deployment. It is not a secret and is returned on
+ * GET.
+ */
+ readonly external_id?: string;
}
// From codersdk/aiproviders_bedrock.go
From f7178f552977739a0148fa6040e947fe73c364e7 Mon Sep 17 00:00:00 2001
From: Yevhenii Shcherbina
Date: Wed, 1 Jul 2026 13:54:41 +0000
Subject: [PATCH 2/6] fix: CR's fixes
---
coderd/ai_providers_internal_test.go | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/coderd/ai_providers_internal_test.go b/coderd/ai_providers_internal_test.go
index 665a4dbfa0a..ac86e45fbdf 100644
--- a/coderd/ai_providers_internal_test.go
+++ b/coderd/ai_providers_internal_test.go
@@ -34,9 +34,8 @@ func TestEnsureBedrockExternalID(t *testing.T) {
RoleARN: "arn:aws:iam::123456789012:role/BedrockRole",
}}
ensureBedrockExternalID(&s)
- require.NotEmpty(t, s.Bedrock.ExternalID)
- // crypto/rand.Text returns a 26-character base32 string.
- require.Len(t, s.Bedrock.ExternalID, 26)
+ require.GreaterOrEqual(t, len(s.Bedrock.ExternalID), 26)
+ require.LessOrEqual(t, len(s.Bedrock.ExternalID), 52)
})
t.Run("DoesNotOverwriteExisting", func(t *testing.T) {
From ab9b1100a08f02bc0fb4e7cceb6c58a4715646fc Mon Sep 17 00:00:00 2001
From: Yevhenii Shcherbina
Date: Wed, 1 Jul 2026 13:59:40 +0000
Subject: [PATCH 3/6] fix: CR's fixes
---
site/src/api/typesGenerated.ts | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index 94de690172d..2c6d5717eaf 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -339,10 +339,7 @@ export interface AIProviderBedrockSettings {
* ExternalID is the STS external ID sent on the AssumeRole call when
* RoleARN is set. The server generates and owns it: create and update
* reject any client-supplied value that differs from the stored one (an
- * update may echo the stored value back). Operators add it to the target
- * role's trust policy as an sts:ExternalId condition to constrain the
- * assumption to this deployment. It is not a secret and is returned on
- * GET.
+ * update may echo the stored value back).
*/
readonly external_id?: string;
}
From d2d0bceea64c0374fc7ed8520af695802033eef0 Mon Sep 17 00:00:00 2001
From: Yevhenii Shcherbina
Date: Wed, 1 Jul 2026 14:09:59 +0000
Subject: [PATCH 4/6] fix: CR's fixes
---
coderd/ai_providers_test.go | 38 +++++++++++++++++++++++++++++++++++++
1 file changed, 38 insertions(+)
diff --git a/coderd/ai_providers_test.go b/coderd/ai_providers_test.go
index 6897ff7088d..9ebd787a7c7 100644
--- a/coderd/ai_providers_test.go
+++ b/coderd/ai_providers_test.go
@@ -1652,6 +1652,44 @@ func TestAIProvidersBedrockExternalID(t *testing.T) {
require.Equal(t, original, updated.Settings.Bedrock.ExternalID, "external ID must be stable across PATCH")
})
+ t.Run("StableAcrossRoleRemovalAndReadd", func(t *testing.T) {
+ t.Parallel()
+ client := coderdtest.New(t, nil)
+ _ = coderdtest.CreateFirstUser(t, client)
+ ctx := testutil.Context(t, testutil.WaitLong)
+
+ const roleB = "arn:aws:iam::123456789012:role/BedrockRoleB"
+
+ created, err := createBedrock(t, client, "bedrock-toggle", codersdk.AIProviderBedrockSettings{
+ Region: "us-east-1",
+ RoleARN: roleARN,
+ })
+ require.NoError(t, err)
+ original := created.Settings.Bedrock.ExternalID
+ require.NotEmpty(t, original)
+
+ // Removing the role retains the external ID.
+ cleared, err := client.UpdateAIProvider(ctx, created.Name, codersdk.UpdateAIProviderRequest{
+ Settings: &codersdk.AIProviderSettings{
+ Bedrock: &codersdk.AIProviderBedrockSettings{Region: "us-east-1"},
+ },
+ })
+ require.NoError(t, err)
+ require.Empty(t, cleared.Settings.Bedrock.RoleARN)
+ require.Equal(t, original, cleared.Settings.Bedrock.ExternalID)
+
+ // Adding a different role reuses the retained ID rather than
+ // regenerating it, so a trust policy referencing it keeps working.
+ readded, err := client.UpdateAIProvider(ctx, created.Name, codersdk.UpdateAIProviderRequest{
+ Settings: &codersdk.AIProviderSettings{
+ Bedrock: &codersdk.AIProviderBedrockSettings{Region: "us-east-1", RoleARN: roleB},
+ },
+ })
+ require.NoError(t, err)
+ require.Equal(t, roleB, readded.Settings.Bedrock.RoleARN)
+ require.Equal(t, original, readded.Settings.Bedrock.ExternalID, "external ID must survive role removal and re-add")
+ })
+
t.Run("AllowsEchoedValueOnPatch", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
From a3b92b788cb6b6bae9fae7ca7ce56b3dff0d3189 Mon Sep 17 00:00:00 2001
From: Yevhenii Shcherbina
Date: Wed, 1 Jul 2026 16:07:58 +0000
Subject: [PATCH 5/6] feat: show Bedrock external ID in the provider edit form
---
.../UpdateProviderPageView.tsx | 2 ++
.../ProvidersPage/components/ProviderForm.tsx | 16 ++++++++++
.../components/providerFormApiMap.test.ts | 31 +++++++++++++++++++
.../components/providerFormApiMap.ts | 11 +++++++
4 files changed, 60 insertions(+)
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/UpdateProviderPage/UpdateProviderPageView.tsx b/site/src/pages/AISettingsPage/ProvidersPage/UpdateProviderPage/UpdateProviderPageView.tsx
index dd4dd96599a..97ed87b975e 100644
--- a/site/src/pages/AISettingsPage/ProvidersPage/UpdateProviderPage/UpdateProviderPageView.tsx
+++ b/site/src/pages/AISettingsPage/ProvidersPage/UpdateProviderPage/UpdateProviderPageView.tsx
@@ -23,6 +23,7 @@ import { ProviderForm } from "../components/ProviderForm";
import { getProviderIcon } from "../components/ProviderIcon";
import {
aiProviderToFormValues,
+ bedrockExternalId,
getProviderDisplayType,
hasBedrockStoredCredentials,
isBedrockProvider,
@@ -196,6 +197,7 @@ const UpdateProviderPageView: React.FC = () => {
bedrockSavedAccessCredentials={hasBedrockStoredCredentials(
provider,
)}
+ bedrockExternalId={bedrockExternalId(provider)}
openAiAnthropicSavedApiKey={openAiAnthropicSavedApiKey}
openAiAnthropicMaskedApiKey={openAiAnthropicMaskedApiKey}
initialValues={aiProviderToFormValues(provider)}
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderForm.tsx b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderForm.tsx
index 6a2b1dfc4ac..7a87b06d14a 100644
--- a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderForm.tsx
+++ b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderForm.tsx
@@ -6,9 +6,11 @@ import * as Yup from "yup";
import type { AIProviderType } from "#/api/typesGenerated";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Button } from "#/components/Button/Button";
+import { CodeExample } from "#/components/CodeExample/CodeExample";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { Form, FormFields } from "#/components/Form/Form";
import { FormField } from "#/components/FormField/FormField";
+import { Label } from "#/components/Label/Label";
import { Link as DocsLink } from "#/components/Link/Link";
import { Spinner } from "#/components/Spinner/Spinner";
import { useUnsavedChangesPrompt } from "#/hooks/useUnsavedChangesPrompt";
@@ -242,6 +244,8 @@ type ProviderFormProps = {
editing?: boolean;
/** When editing Bedrock and the API already has keys, show masked placeholders until cleared. */
bedrockSavedAccessCredentials?: boolean;
+ /** Server-generated STS external ID, shown read-only when a role is assumed. */
+ bedrockExternalId?: string;
/** When editing openai/anthropic and a key is on file, show a masked placeholder until cleared. */
openAiAnthropicSavedApiKey?: boolean;
/** Masked rendering of the saved openai/anthropic key (e.g. `sk-***...ABCD`). Falls back to a generic mask when omitted. */
@@ -271,6 +275,7 @@ const baseUrlPlaceholder = (provider: string) =>
export const ProviderForm: FC = ({
editing = false,
bedrockSavedAccessCredentials = false,
+ bedrockExternalId,
openAiAnthropicSavedApiKey = false,
openAiAnthropicMaskedApiKey,
initialValues,
@@ -538,6 +543,17 @@ export const ProviderForm: FC = ({
Optional. When a role ARN is set, the gateway assumes that role
(using the base identity) before calling Bedrock.
+ {editing && bedrockExternalId && (
+
+
+
+
+ Server-generated. Add it to the assumed role's trust policy as
+ an sts:ExternalId condition so only this
+ deployment can assume the role.
+
+
+ )}
>
)}
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.test.ts b/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.test.ts
index 43f1c767624..ab71d513590 100644
--- a/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.test.ts
+++ b/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.test.ts
@@ -13,6 +13,7 @@ import {
} from "./ProviderForm";
import {
aiProviderToFormValues,
+ bedrockExternalId,
getProviderDisplayType,
hasBedrockStoredCredentials,
isBedrockProvider,
@@ -189,6 +190,36 @@ describe("hasBedrockStoredCredentials", () => {
});
});
+describe("bedrockExternalId", () => {
+ it("returns the external ID from a role-based Bedrock provider", () => {
+ const provider: AIProvider = {
+ ...MockAIProviderBedrock,
+ settings: settings({
+ _type: "bedrock",
+ role_arn: "arn:aws:iam::123456789012:role/BedrockRole",
+ external_id: "7QF3ZK2MLP4RS6TUVWXY2ABCDE",
+ }),
+ };
+ expect(bedrockExternalId(provider)).toBe("7QF3ZK2MLP4RS6TUVWXY2ABCDE");
+ });
+
+ it("returns undefined when the provider has no external ID", () => {
+ expect(bedrockExternalId(MockAIProviderBedrock)).toBeUndefined();
+ });
+
+ it("returns undefined when the external ID is an empty string", () => {
+ const provider: AIProvider = {
+ ...MockAIProviderBedrock,
+ settings: settings({ _type: "bedrock", external_id: "" }),
+ };
+ expect(bedrockExternalId(provider)).toBeUndefined();
+ });
+
+ it("returns undefined for a non-Bedrock provider", () => {
+ expect(bedrockExternalId(MockAIProviderOpenAI)).toBeUndefined();
+ });
+});
+
describe("getProviderDisplayType", () => {
it("returns bedrock for a Bedrock provider", () => {
expect(getProviderDisplayType(MockAIProviderBedrock)).toBe("bedrock");
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.ts b/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.ts
index 92df168a449..acb32c19753 100644
--- a/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.ts
+++ b/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.ts
@@ -55,6 +55,17 @@ export const isBedrockProvider = (provider: AIProvider): boolean => {
return s !== null && s._type === BEDROCK_SETTINGS_TYPE;
};
+// The external ID is server-generated and returned on read when the Bedrock
+// provider assumes a role. It is read-only: the form displays it but never
+// submits it.
+export const bedrockExternalId = (provider: AIProvider): string | undefined => {
+ if (!isBedrockProvider(provider)) {
+ return undefined;
+ }
+ const s = provider.settings as SettingsWire | null;
+ return s?.external_id || undefined;
+};
+
export const hasBedrockStoredCredentials = (provider: AIProvider): boolean => {
if (!isBedrockProvider(provider)) {
return false;
From d70b941f74b07c00ef6115fa9c71ea64a673c22c Mon Sep 17 00:00:00 2001
From: Jake Howell
Date: Thu, 2 Jul 2026 04:37:34 +0000
Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=A4=96=20fix(site/src/pages/AISetting?=
=?UTF-8?q?sPage/ProvidersPage):=20address=20Bedrock=20external=20ID=20rev?=
=?UTF-8?q?iew?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add EditBedrockWithExternalId Storybook story so the read-only external ID
block on the Bedrock edit form has coverage (CRF-1).
- Trim the bedrockExternalId comment to describe the function's own contract
instead of restating the generated JSDoc and consumer behavior (CRF-2).
Modified by Coder Agents on behalf of Jake Howell.
---
.../components/ProviderForm.stories.tsx | 34 +++++++++++++++++++
.../components/providerFormApiMap.ts | 4 +--
2 files changed, 35 insertions(+), 3 deletions(-)
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderForm.stories.tsx b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderForm.stories.tsx
index 7851332862c..af492374278 100644
--- a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderForm.stories.tsx
+++ b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderForm.stories.tsx
@@ -293,6 +293,40 @@ export const EditBedrockKeepCredentials: Story = {
},
};
+// The external ID is server-generated when a role is assumed. The edit form
+// surfaces it read-only next to Role ARN so operators can add the value to
+// their role's trust policy as an sts:ExternalId condition.
+export const EditBedrockWithExternalId: Story = {
+ args: {
+ editing: true,
+ bedrockSavedAccessCredentials: true,
+ bedrockExternalId: "7QF3ZK2MLP4RS6TUVWXY2ABCDE",
+ initialValues: {
+ type: "bedrock",
+ name: "bedrock",
+ displayName: "Bedrock",
+ baseUrl: "https://bedrock-runtime.us-east-2.amazonaws.com",
+ model: "anthropic.claude-opus-4-7",
+ smallFastModel: "anthropic.claude-haiku-4-5",
+ accessKey: "",
+ accessKeySecret: "",
+ roleArn: "arn:aws:iam::123456789012:role/BedrockRole",
+ enabled: true,
+ },
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await expect(canvas.findByText("External ID")).resolves.toBeVisible();
+ await expect(
+ canvas.findByText("7QF3ZK2MLP4RS6TUVWXY2ABCDE"),
+ ).resolves.toBeVisible();
+ await expect(canvas.findByText("sts:ExternalId")).resolves.toBeVisible();
+ await expect(
+ canvas.findByRole("button", { name: /copy code/i }),
+ ).resolves.toBeVisible();
+ },
+};
+
export const AddCopilot: Story = {
args: {
// The real add flow passes only the type; the form fills name and
diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.ts b/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.ts
index acb32c19753..71ca37dcbef 100644
--- a/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.ts
+++ b/site/src/pages/AISettingsPage/ProvidersPage/components/providerFormApiMap.ts
@@ -55,9 +55,7 @@ export const isBedrockProvider = (provider: AIProvider): boolean => {
return s !== null && s._type === BEDROCK_SETTINGS_TYPE;
};
-// The external ID is server-generated and returned on read when the Bedrock
-// provider assumes a role. It is read-only: the form displays it but never
-// submits it.
+// Server-generated STS external ID; read-only.
export const bedrockExternalId = (provider: AIProvider): string | undefined => {
if (!isBedrockProvider(provider)) {
return undefined;