Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: support cross-account Bedrock AssumeRole in AI Bridge - #26527

Merged
evgeniy-scherbina merged 22 commits into
mainfrom
yevhenii/bedrock-dynamic-assume-role
Jun 24, 2026
Merged

feat: support cross-account Bedrock AssumeRole in AI Bridge#26527
evgeniy-scherbina merged 22 commits into
mainfrom
yevhenii/bedrock-dynamic-assume-role

Conversation

@evgeniy-scherbina

@evgeniy-scherbina evgeniy-scherbina commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Support IAM role assumption for AWS Bedrock in AI Bridge

Summary

Implements https://linear.app/codercom/issue/AIGOV-371/support-dynamic-bedrock-assumerole-across-aws-accounts-for-ai-gateway

A Bedrock provider can now be configured with an IAM role to assume. Before calling Bedrock, the gateway assumes that role via STS and signs requests with the resulting temporary credentials. Whether the role lives in the same account or another one is entirely a matter of the role's trust policy.

Problem

Many organizations prohibit long-lived AWS access keys and expect workloads to authenticate through assumed IAM roles instead. A common case is an organization that runs Bedrock across several AWS accounts, one per business unit, and needs each unit's usage billed to its own account by assuming a role there. AI Bridge previously authenticated a Bedrock provider only with static keys or the gateway's own ambient AWS identity, which is shared by every provider, with no way to assume a role. These deployments had no clean path.

How it works

When a provider is configured with a role ARN, the gateway uses its base identity to assume that role via STS and signs Bedrock requests with the temporary credentials it returns. The base identity is whatever the AWS default credential chain resolves, IRSA, EKS Pod Identity, EC2 Instance Profile, or static keys.

Credentials are resolved once when the provider is set up and are then cached and rotated, so individual requests are served from the cache rather than triggering a new STS call. A deployment that needs several roles configures several providers, each pointing at its own role.

Configuration

The role ARN is part of the Bedrock provider settings and is set through the AI provider API. It is optional: a provider with no role ARN behaves exactly as before.

Scope and trade-offs

  • This PR is backend only. The settings UI for the role ARN ships in a follow-up.
  • Configuration is not exposed through environment variables. Environment-based provider configuration is being phased out in favor of database-managed providers, so the role ARN is intentionally database and API only.

Follow-up PR: #26578

@github-actions

Copy link
Copy Markdown
Contributor

Docs preview

📖 View docs preview for docs/reference/api/general.md

@datadog-coder

This comment has been minimized.

@evgeniy-scherbina
evgeniy-scherbina force-pushed the yevhenii/bedrock-dynamic-assume-role branch from a63a21b to dbf927d Compare June 20, 2026 16:55
@evgeniy-scherbina
evgeniy-scherbina force-pushed the yevhenii/bedrock-dynamic-assume-role branch 3 times, most recently from 8e7868d to 4ce147a Compare June 22, 2026 16:53
@evgeniy-scherbina
evgeniy-scherbina force-pushed the yevhenii/bedrock-dynamic-assume-role branch from 4ce147a to 65cb77c Compare June 22, 2026 17:03
@evgeniy-scherbina
evgeniy-scherbina marked this pull request as ready for review June 22, 2026 17:30
@evgeniy-scherbina

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-06-24 15:12 UTC by @evgeniy-scherbina
Spend: $69.40 / $100.00

Review history
  • R1 (2026-06-22): 12 reviewers, 5 Nit, 2 P2, 8 P3, COMMENT. Review
  • R2 (2026-06-24): 8 reviewers, 6 Nit, 2 P2, 11 P3, COMMENT. Review

deep-review v0.9.0 | Round 2 | d5ec26b..ed11c8c

Last posted: Round 2, 19 findings (2 P2, 11 P3, 6 Nit), COMMENT. Review

Finding inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P3 Author fixed (9b436d6) cli/aibridged.go:352 bedrockConfigFromRow unit test does not verify new RoleARN field mapping R1 Netero Yes
CRF-2 P2 Author fixed (0d6cb16) aibridge/intercept/messages/base.go:296 withAWSBedrockOptions constructs aws.Config with only explicit config region, losing SDK-resolved region R1 Meruem Yes
CRF-3 P2 Author fixed (7dcf5db) aibridge/intercept/messages/base.go:293 "no AWS credentials found" wraps AssumeRole denials, misleading operators R1 Hisoka Yes
CRF-4 P3 Author contested; panel closed R2 (5/7 close) aibridge/provider/bedrock.go:18 bedrockSessionName says "aigateway"; codebase consistently uses "aibridge" R1 Gon P3, Hisoka Note, Kurapika Note Yes
CRF-5 P3 Author fixed (eec5a92) codersdk/aiproviders_bedrock.go:37 RoleARN accepts any string with no format validation R1 Kurapika Yes
CRF-6 P3 Author contested; panel closed R2 (6/6 close) codersdk/aiproviders_bedrock.go:20 PATCH with partial bedrock settings silently clears RoleARN R1 Kurapika Yes
CRF-7 P3 Author contested; panel closed R2 (5/5 close) aibridge/provider/bedrock.go:71 Cross-account AssumeRole without ExternalId support R1 Kurapika Yes
CRF-8 P3 Author fixed (a4277e5) aibridge/provider/bedrock.go:70 RoleARN + BaseURL-only fails STS AssumeRole at runtime with no early validation R1 Meruem Yes
CRF-9 P3 Author contested; panel closed R2 (3/4 close) aibridge/provider/bedrock.go:20 buildBedrockCredentials doc comment is bloated R1 Gon Yes
CRF-10 P3 Author contested; panel closed R2 (3/4 close) aibridge/provider/anthropic.go:60 4-line inline comment restates code R1 Gon Yes
CRF-11 Nit Author fixed (4ea714e) coderd/ai_providers_test.go:1552 MigrateStaticToRole asserts AccessKey cleared but not AccessKeySecret R1 Bisky Yes
CRF-12 Nit Author fixed (comma -> period) aibridge/intercept/messages/base.go:272 Comma splice in doc comment R1 Leorio Yes
CRF-13 Nit Author contested; panel closed R2 (3/3 close) aibridge/provider/anthropic.go:48 Exported constructor NewAnthropic has no doc comment R1 Leorio Yes
CRF-14 Nit Author contested; panel closed R2 (2/2 close) aibridge/provider/bedrock.go:27 "Credential Provider" capitalized mid-sentence in doc comment R1 Leorio Yes
CRF-15 Nit Author fixed (8769c21, b07ef44) aibridge/bridge_test.go:25 mustNewAnthropicProvider / mustNewAnthropic inconsistent naming across files R1 Gon, Meruem Yes
CRF-16 P3 Open aibridge/intercept/messages/base_internal_test.go:180 Retrieve error path in withAWSBedrockOptions untested at this layer R2 Bisky Yes
CRF-17 P3 Open aibridge/provider/bedrock.go:73 5-line inline comment restates code flow; useful insight (only AssumeRoleProvider needs cache-wrapping) buried R2 Gon Yes
CRF-18 P3 Open aibridge/intercept/messages/base.go:294 3-line inline comment restates caching behavior documented on callee and two lines above R2 Gon Yes
CRF-19 Nit Open aibridge/provider/bedrock.go:32 Bare string return value ambiguous; named returns would clarify resolvedRegion R2 Gon Yes

Contested and acknowledged

CRF-4 (P3, aibridge/provider/bedrock.go:18) - bedrockSessionName naming

  • Finding: Session name uses "coder-aigateway" while every other identifier uses "coder-aibridge."
  • Author defense: "coder-aibridge was rebranded to coder-aigateway. So I prefer to use newer name."
  • Panel closure (R2, 5/7): Mafuuu traced external-facing identifiers (DB tables, API operations, Prometheus aliases) and found the public brand is "AI Gateway." Meruem confirmed the naming direction is a human call outside panel scope. Kurapika verified no security boundary violation. The codebase has not been renamed yet, so today the name is inconsistent, but the product brand supports the author's choice.

CRF-6 (P3, codersdk/aiproviders_bedrock.go:20) - PATCH clears RoleARN

  • Finding: RoleARN as plain string means PATCH silently clears it; proposed *string.
  • Author defense: Pointer approach only for write-only fields. RoleARN is returned on read, same pattern as Region/Model.
  • Panel closure (R2, 6/6): Hisoka, Kurapika, Mafuuu, Meruem, Pariston, and Melody all independently verified the merge logic. Plain string fields are replaced on PATCH because clients always have the current value to echo back. Consistent design across all non-secret fields.

CRF-7 (P3, aibridge/provider/bedrock.go:71) - No ExternalId

  • Finding: Cross-account AssumeRole without ExternalId removes a confused-deputy defense layer.
  • Author defense: ExternalId is for multi-tenant third-party; this is first-party self-hosted.
  • Panel closure (R2, 5/5): Hisoka, Kurapika, Mafuuu, Meruem, Pariston all confirmed that the confused-deputy threat model requires mutually-distrusting tenants sharing a service identity, which does not exist in a self-hosted gateway where the deployer controls both the base identity and target roles.

CRF-9 (P3, aibridge/provider/bedrock.go:20) - Doc comment bloat

  • Finding: 12-line doc comment; 4 deliver contract. Proposed 4-line replacement.
  • Author defense: Comment emphasizes STS workflow, cache mechanism, per-provider call pattern.
  • Panel closure (R2, 3/4): Mafuuu verified each point addresses a distinct concern. Meruem confirmed the length is proportional to the number of structural assumptions documented. Pariston agreed it is proportional. Gon dissented with a trimmed version.

CRF-10 (P3, aibridge/provider/anthropic.go:60) - Inline comment restates code

  • Finding: 4-line comment restates what code and called function show.
  • Author defense: Emphasizes new separation between lazy construction and expensive retrieval.
  • Panel closure (R2, 3/4): Mafuuu confirmed the comment documents a non-obvious design contract (no-network-at-construction). Meruem agreed the code alone doesn't reveal this property. Pariston agreed. Gon dissented, noting it near-verbatim duplicates the callee doc.

CRF-13 (Nit, aibridge/provider/anthropic.go:48) - Missing doc comment on NewAnthropic

  • Finding: Exported constructor lacks doc comment; signature change was moment to add.
  • Author defense: Pre-existing, not introduced by this PR.
  • Panel closure (R2, 3/3): Mafuuu, Meruem, Pariston all confirmed pre-existing absence is not a regression.

CRF-14 (Nit, aibridge/provider/bedrock.go:27) - Mid-sentence capitalization

  • Finding: "Credential Provider" and "Bedrock Provider" capitalized mid-sentence.
  • Author defense: "I think it's fine."
  • Panel closure (R2, 2/2): Mafuuu verified the capitalization matches AWS SDK type names. Meruem confirmed defensible under Go doc conventions.

Round log

Round 1

Panel (12 reviewers). 2 P2, 8 P3, 5 Nit. Reviewed against d5ec26b..511e209.

Round 2

Churn guard: 8 addressed, 7 contested. PROCEED. Panel (8 reviewers). All 7 contested findings closed by panel. 3 P3, 1 Nit new. Reviewed against d5ec26b..ed11c8c.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coderagents

coderagents Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Documentation Check

Updates Needed

  • docs/ai-coder/ai-gateway/providers.md - The Amazon Bedrock section (lines 84-134) describes two credential strategies: the AWS SDK default chain and static credentials. This PR adds a third: IAM role assumption via STS (configured with the new role_arn field in AIProviderBedrockSettings). The docs should describe when and how to use it, e.g. cross-account Bedrock access by assuming a role ARN. The field is already exposed in the API (role_arn in the Bedrock settings object), so users can configure it even before the UI ships.
  • docs/reference/api/schemas.md - The AIProviderBedrockSettings schema reference should include the new role_arn field. This is typically auto-generated; verify make gen picks it up.

Automated review via Coder Agents

@linear-code

linear-code Bot commented Jun 22, 2026

Copy link
Copy Markdown

AIGOV-371

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The credential resolution refactoring is well-structured: lifting AWS config loading from per-request into provider construction eliminates repeated SDK config parsing and makes credential failures visible at startup. The aws.NewCredentialsCache wrapping stscreds.AssumeRoleProvider is the standard AWS pattern, and the test suite covers the new paths thoroughly, including a mock STS endpoint for AssumeRole.

Severity summary: 2 P2, 8 P3, 5 Nit.

The two P2s are a region regression (the new manual aws.Config assembly loses the SDK-resolved region that LoadDefaultConfig previously carried) and a misleading error message ("no AWS credentials found" now also wraps STS AssumeRole denials, sending operators down the wrong debugging path).

"Someone searching CloudTrail for this session name will grep the codebase and find nothing; someone grepping for 'aigateway' will find only this constant."
-- Gon, on the naming outlier

🤖 This review was automatically generated with Coder Agents.

Comment thread aibridge/intercept/messages/base.go
Comment thread aibridge/intercept/messages/base.go Outdated
Comment thread aibridge/provider/bedrock.go
Comment thread codersdk/aiproviders_bedrock.go
Comment thread codersdk/aiproviders_bedrock.go
require.NotNil(t, persisted.Bedrock)
require.Equal(t, "arn:aws:iam::123456789012:role/target", persisted.Bedrock.RoleARN)
require.NotNil(t, persisted.Bedrock.AccessKey)
require.Equal(t, "", *persisted.Bedrock.AccessKey)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-11] MigrateStaticToRole sends AccessKeySecret: ptr.Ref("") in the PATCH and reads the DB row, but only asserts AccessKey == "". For a test titled "MigrateStaticToRole" the full migration should be shown:

require.NotNil(t, persisted.Bedrock.AccessKeySecret)
require.Equal(t, "", *persisted.Bedrock.AccessKeySecret)

(Bisky)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified fixed. Both AccessKey and AccessKeySecret are now asserted cleared.

🤖

Comment thread aibridge/intercept/messages/base.go Outdated
func (*interceptionBase) withAWSBedrockOptions(ctx context.Context, cfg *aibconfig.AWSBedrock) ([]option.RequestOption, error) {
if cfg == nil {
return nil, xerrors.New("nil config given")
// Credentials come from i.bedrock.Creds, it is a shared credentials cache, so the per-request

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-12] Comma splice: "Credentials come from i.bedrock.Creds, it is a shared credentials cache" is two independent clauses joined by a comma. Use a semicolon or split into two sentences.

(Leorio)

🤖

}

func NewAnthropic(cfg config.Anthropic, bedrockCfg *config.AWSBedrock) *Anthropic {
func NewAnthropic(ctx context.Context, cfg config.Anthropic, bedrockCfg *config.AWSBedrock) (*Anthropic, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-13] Exported constructor NewAnthropic has no doc comment. The signature change (added ctx, now returns error) makes this the moment to document when it errors, what ctx is used for, and why bedrockCfg is a pointer (nil = non-Bedrock).

(Leorio)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It existed before PR, I think it's okay.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Panel evaluated (3/3 close). The missing doc comment pre-dates this PR and is not a regression introduced by the signature change. Closing.

🤖

//
// The result is wrapped in aws.NewCredentialsCache, which caches and rotates
// the resolved temporary credentials. buildBedrockCredentials should be called
// once when the Bedrock provider is constructed, and the returned Credential

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-14] "Credential Provider" and "Bedrock Provider" are capitalized mid-sentence. Go doc convention is lowercase unless it is a proper noun or a type reference. Write "credentials provider" and "Bedrock provider".

(Leorio)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Panel evaluated (2/2 close). The capitalization matches the AWS SDK type names (aws.CredentialsProvider), which is defensible under Go doc convention. Closing.

🤖

Comment thread aibridge/bridge_test.go
)

var bridgeTestTracer = otel.Tracer("bridge_test")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-15] Two names for the same helper across files: mustNewAnthropicProvider in bridge_test.go, aibridged_test.go, aibridged_integration_test.go vs mustNewAnthropic in anthropic_internal_test.go, setupbridge.go. mustNewAnthropic matches the constructor NewAnthropic; the Provider suffix is redundant. Also, passthrough_internal_test.go avoids the helper and uses require.NoError(t, err) directly, which is the better pattern when t is available.

(Gon, Meruem)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's outdated.

@dannykopping dannykopping left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, but added a few minor points.

Have you tested this using a real AWS account?

Comment thread aibridge/bridge_test.go Outdated
Comment thread aibridge/provider/bedrock.go Outdated
Comment thread aibridge/provider/bedrock_internal_test.go
Comment thread aibridge/provider/anthropic.go
Comment thread aibridge/provider/bedrock_internal_test.go
Comment thread aibridge/intercept/messages/base_internal_test.go
Comment thread codersdk/aiproviders.go Outdated
Comment thread cli/aibridged.go
enabledCount++
}
prov, err := buildAIProviderFromRow(row, keysByProvider[row.ID], cfg, metrics)
prov, err := buildAIProviderFromRow(ctx, row, keysByProvider[row.ID], cfg, metrics)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given we're now passing ctx through, do we need to reconsider timeouts etc?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildBedrockCredentials is supposed to be lazy, expensive credential resolution (e.g. STS AssumeRole call) happens in withAWSBedrockOptions function.

But I'll consider adding 30secs timeout.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it more, looks like buildBedrockCredentials and LoadDefaultConfig don't make network calls, unless specifically configured to do so.
So it should be fast, lazy construction calls. So I think timeout is unnecessary.

Comment thread coderd/aibridged/aibridged_test.go Outdated
"github.com/coder/quartz"
)

func mustNewAnthropicProvider(cfg aibridge.AnthropicConfig, bedrockCfg *aibridge.AWSBedrockConfig) aibridge.Provider {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repeated pattern in a few places; can we abstract?

@evgeniy-scherbina evgeniy-scherbina Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It requires creating a new pkg to avoid import cycles, e.g.: #26636.
I don't mind to merge this example, if you're okay with that.

Helper usage is split across multiple places:

  ┌─────┬──────────────────────────────────────────────────┬─────────────────┬───────────────────┬──────────────────────┐
  │  #  │                       File                       │       Pkg       │      Returns      │   Consolidatable?    │
  ├─────┼──────────────────────────────────────────────────┼─────────────────┼───────────────────┼──────────────────────┤
  │ 1   │ aibridge/bridge_test.go                          │ aibridge_test   │ aibridge.Provider │ ✅                   │
  ├─────┼──────────────────────────────────────────────────┼─────────────────┼───────────────────┼──────────────────────┤
  │ 2   │ coderd/aibridged/aibridged_test.go               │ aibridged_test  │ aibridge.Provider │ ✅                   │
  ├─────┼──────────────────────────────────────────────────┼─────────────────┼───────────────────┼──────────────────────┤
  │ 3   │ enterprise/aibridged_integration_test.go         │ enterprise_test │ aibridge.Provider │ ✅                   │
  ├─────┼──────────────────────────────────────────────────┼─────────────────┼───────────────────┼──────────────────────┤
  │ 4   │ aibridge/internal/integrationtest/setupbridge.go │ integrationtest │ aibridge.Provider │ ✅                   │
  ├─────┼──────────────────────────────────────────────────┼─────────────────┼───────────────────┼──────────────────────┤
  │ 5   │ aibridge/provider/anthropic_internal_test.go     │ provider        │ *Anthropic        │ ❌                   │
  └─────┴──────────────────────────────────────────────────┴─────────────────┴───────────────────┴──────────────────────┘

Our existing testutil pkg is under internal path.

UPD:

  • I included this fix for now, let me know if I need to revert it.
  • Also Note we still have one mustNewAnthropic duplicate helper in provider package. There is no good way to remove it. Happy to discuss this part.

},
expectKind: intercept.CredentialKindCentralized,
expectAuthHeader: "",
expectHint: "<aws chain credentials>",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change? It was more clear before.

@evgeniy-scherbina evgeniy-scherbina Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hint should be under 15 chars, according to DB constraint to prevent accidentally storing real credentials.
So this bug existed before.

But I think this bug wasn't released yet, probably I need to add test for it as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated test here: 5ef859f to prevent it from happening in the future.

@evgeniy-scherbina

evgeniy-scherbina commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

@dannykopping

Have you tested this using a real AWS account?

Yes, but I'll double check before merge, considering I made changes.

I tested with only one AWS account. Both User (Base Creds) and Role in the same account. But it should work the same way for cross-account. From Coder PoV it doesn't matter, it only concerns TrustPolicy of IAM Role.

@evgeniy-scherbina

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@dannykopping dannykopping left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 2. All 8 R1 fixes verified by panel. All 7 contested findings evaluated and closed by panel vote.

R1 P2 fixes (CRF-2 region loss, CRF-3 misleading error) are solid. The ResolvedRegion fallback and neutral error message both address root causes. The new ARN validation (CRF-5 fix) covers standard, govcloud, and China partitions with clean test coverage. The AssumeRole caching, refresh, and error tests are genuine, including the atomic-counter verification that 5 retrievals produce exactly 1 STS call.

Contested findings: CRF-4 (naming) closed because the external-facing brand is "AI Gateway" and the session name is for CloudTrail consumers, not code readers. CRF-6 (PATCH clears RoleARN) closed; consistent with Region/Model as non-secret readable fields. CRF-7 (no ExternalId) closed; confused-deputy doesn't apply to first-party self-hosted. CRF-9, CRF-10 (comment style) closed by majority; the comments document non-obvious contracts that the code alone doesn't reveal.

4 new findings below (3 P3, 1 Nit).

"This code earned [silence]."
-- Hisoka, on the credential lifecycle design

🤖 This review was automatically generated with Coder Agents.


base := &interceptionBase{}
opts, err := base.withAWSBedrockOptions(context.Background(), tt.cfg)
base := &interceptionBase{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-16] TestAWSBedrockValidation hardcodes a successful credential provider in every row, leaving the Retrieve error path in withAWSBedrockOptions (line 297) untested at this layer.

The old TestAWSBedrockCredentialChain tested the "no credential source configured" error path through withAWSBedrockOptions. That test was removed when credential resolution moved to buildBedrockCredentials, which has its own equivalent. But the wrapping error path in withAWSBedrockOptions ("resolve AWS credentials: %w") is now uncovered here. A test case with a Creds provider that returns an error on Retrieve would pin the new error message and ensure future refactors don't silently change the error surface.

(Bisky)

🤖

return nil, "", xerrors.New("region is required to assume a role, but was not specified")
}

// The base identity signs Bedrock requests directly unless a target role is

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-17] The comment narrates what the if/else does ("The base identity signs requests directly unless a target RoleARN is configured..."). The code shows this: credsProvider := base.Credentials followed by if cfg.RoleARN != "" { ... }. The one useful observation, that base.Credentials from LoadDefaultConfig is already cache-wrapped so only the AssumeRoleProvider needs explicit wrapping, is buried in mechanism narration.

Suggested: // Only the AssumeRoleProvider needs cache-wrapping; base.Credentials from LoadDefaultConfig is already cached.

(Gon)

🤖

return nil, xerrors.New("both access key and access key secret must be provided together")
// Neither set: SDK default credential chain resolves credentials.
default:
// Fail fast: ensure credentials can be resolved before signing. Served from

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-18] "Served from the shared cache on most requests (no network); on the cold or refresh path this performs the actual STS/IMDS call." This restates the caching behavior already documented on buildBedrockCredentials and on the withAWSBedrockOptions doc comment two lines above. The intent (fail fast before signing) fits in one line.

Suggested: // Fail fast: surface credential errors before signing.

(Gon)

🤖

// so per-request credential retrieval is served from this cache rather than
// re-resolving (and re-assuming) on every request. No network call is made here:
// the base identity and any AssumeRole are resolved lazily on first retrieval.
func buildBedrockCredentials(ctx context.Context, cfg config.AWSBedrock) (aws.CredentialsProvider, string, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-19] The bare string second return is ambiguous. Callers must read the body to discover it is the SDK-resolved region. Named returns would clarify:

func buildBedrockCredentials(ctx context.Context, cfg config.AWSBedrock) (creds aws.CredentialsProvider, resolvedRegion string, err error) {

(Gon)

🤖

@evgeniy-scherbina
evgeniy-scherbina merged commit 8bf6f43 into main Jun 24, 2026
30 of 33 checks passed
@evgeniy-scherbina
evgeniy-scherbina deleted the yevhenii/bedrock-dynamic-assume-role branch June 24, 2026 16:03
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants