Skip to content

fix: validate bedrock protocol at provider construction - #27234

Merged
evgeniy-scherbina merged 6 commits into
mainfrom
yevhenii/bedrock-mantle-fixes
Jul 14, 2026
Merged

fix: validate bedrock protocol at provider construction#27234
evgeniy-scherbina merged 6 commits into
mainfrom
yevhenii/bedrock-mantle-fixes

Conversation

@evgeniy-scherbina

@evgeniy-scherbina evgeniy-scherbina commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Follow-up PR to #26745

@evgeniy-scherbina
evgeniy-scherbina marked this pull request as ready for review July 14, 2026 16:49
@evgeniy-scherbina

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-07-14 16:52 UTC by @evgeniy-scherbina
Spend: $12.37 / $100.00

Review history
  • R1 (2026-07-14): 13 reviewers, 2 Nit, 1 P2, 3 P3, COMMENT. Review

deep-review v0.9.0 | Round 1 | 5785211..f55d714

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

Finding inventory

Finding inventory: PR #27234

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P3 Open anthropic.go:72 Construction Validate() gate error path untested R1 Netero, Mafu-san (Bisky Note) Yes
CRF-2 P3 Open base.go:176 aws_bedrock_protocol trace branch + ResolvedProtocol untested (0% cov) R1 Bisky P3 (Netero Note) Yes
CRF-3 Note Open CI Failing windows-pg CI job appears unrelated to platform-independent diff R1 Netero No (body)
CRF-4 P2 Open anthropic.go:72 Construction validates unresolved config; breaks env-region mantle providers R1 Hisoka P2, Ryosuke P3 Yes
CRF-5 P3 Open config.go:76 ResolvedProtocol adoption gap: empty->InvokeModel invariant now in 3 places R1 Mafuuu P3, Ryosuke P3, Chopper Nit, Razor Nit (Pariston Note) Yes
CRF-6 Nit Open config.go:73 "legacy" appears twice in ResolvedProtocol doc comment R1 Gon Yes
CRF-7 Nit Open config.go:76 ResolvedProtocol collapses to cmp.Or R1 Ging-Go Yes
CRF-8 P3 Open PR description Empty description for a fix PR; undisclosed feat commit R1 Leorio P3, Mafu-san Nit No (body)

Contested and acknowledged

CRF-4 (P2, anthropic.go:72) - construction validates unresolved config

  • Finding (Hisoka P2, Ryosuke P3): Construction runs bedrockCfg.Validate() at anthropic.go:72 before buildBedrockCredentials resolves the region; the request-time gates validate the resolved runtimeCfg. For mantle, Validate() requires Region != "" unconditionally (config.go:97-99), so a mantle provider relying on AWS_REGION/IMDS for its region constructed and served pre-PR but now fails construction with bedrock config: region required.
  • Pariston dissent (Note, no regression): Argued no regression, but only traced the invoke-model branch, whose Region=="" && BaseURL=="" guard is duplicated in buildBedrockCredentials. The mantle branch requires Region != "" regardless of BaseURL, so Pariston's analysis does not cover the regressing case.
  • Orchestrator disposition: Verified against code. config.go:96-102 (mantle requires Region), anthropic.go:72/79-84 (Validate before region resolution; resolvedRegion lands in runtimeCfg only after), bedrock.go:35-37/108-110 (mantle+BaseURL passes the credentials guard and resolves base.Region from env), base.go:376-390 (request-time mantle path validates resolved runtimeCfg and signs with cfg.Region). Regression confirmed for env-region mantle. Higher severity (Hisoka P2) wins over Ryosuke's hedged P3.

Round log

Round 1

Netero first pass (1 P3, 2 Note), then 13-reviewer panel. Final: 1 P2, 4 P3, 2 Nit, 1 Note. Reviewed against 5785211..f55d714. Event COMMENT (no P0-P1).

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.

@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.

This is a small, coherent follow-up: it catches a bad Bedrock config at construction instead of on the first request, centralizes the empty-protocol default in ResolvedProtocol(), and adds an aws_bedrock_protocol trace attribute. The error wrapping (bedrock config: %w) reads cleanly and the trace attribute is correctly gated behind i.bedrock != nil.

Severity: 1 P2, 4 P3, 2 Nit, 1 Note.

The one that matters (CRF-4): the construction gate validates the raw bedrockCfg before buildBedrockCredentials resolves the region, while the request-time gate validates the resolved runtimeCfg. The same Validate() now judges two different snapshots. For mantle, which requires a non-empty region unconditionally, a provider that relied on AWS_REGION/IMDS for its region constructed and served before this PR and now fails to construct with bedrock config: region required. Validating runtimeCfg after region resolution fixes this and, as a bonus, makes the two per-request Validate() calls redundant, collapsing three validation sites across two snapshots into one gate.

The PR also ships zero tests for its entire behavioral payload. Two P3 coverage findings are inline (the construction error path and the trace branch); the harness for both already exists, so both are cheap.

Process, three items outside the diff:

  • The description is one line ("Follow-up PR to #26745") for a fix: PR. A reviewer can't tell what was broken or what an operator saw without leaving the PR. Two sentences (what was broken, what changed) would fix it. (Leorio P3)
  • The PR is typed fix: but carries a feat: commit adding the trace attribute, and the metadata never mentions it. Consider splitting the feat into its own PR or retitling and adding a sentence. (Mafu-san)
  • CI: the one failing job is test-go-pg (windows-2022). This diff is platform-independent Go with no Windows or Postgres surface, so it looks like flake or infra, but confirm before merge (unverified on Linux). (Netero)

Ryosuke, on the unused helper: "As written, the helper is a spare part, not a manifold."

🤖 This review was automatically generated with Coder Agents.

Comment thread aibridge/provider/anthropic.go Outdated
// so it is cheap to run at construction.
var bedrock *messages.BedrockRuntime
if bedrockCfg != nil {
if err := bedrockCfg.Validate(); err != nil {

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.

P2 [CRF-4] Construction validates the raw bedrockCfg before region resolution, so it judges a different config snapshot than the two request-time gates. (Hisoka P2, Ryosuke P3)

Two Validate() calls that look identical but judge different config.

Traced and confirmed: Validate() runs here at line 72 on bedrockCfg (Region == ""), but buildBedrockCredentials (line 75) resolves the region from the AWS environment and it lands in runtimeCfg.Region only afterward (lines 82-84). The request-time gate (base.go:377) validates that resolved runtimeCfg. For mantle, Validate() requires Region != "" unconditionally (config.go:97-99), so a mantle provider that relied on AWS_REGION/IMDS for its region constructed and served before this PR and now fails construction with bedrock config: region required. Invoke-model escapes only because its Region=="" && BaseURL=="" guard is duplicated in buildBedrockCredentials; mantle has no such escape.

Fix: move the Validate() call to run on runtimeCfg after line 84. Both gates then read the same config, and the per-request Validate() calls at base.go:336/:377 become redundant and removable.

🤖

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

Comment thread aibridge/provider/anthropic.go Outdated
// so it is cheap to run at construction.
var bedrock *messages.BedrockRuntime
if bedrockCfg != nil {
if err := bedrockCfg.Validate(); err != nil {

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-1] The construction-time Validate() gate, the PR's stated purpose, ships with no test exercising its error path. (Netero, Mafu-san; Bisky Note)

Per-block coverage reports anthropic.go:72.47,74.4 at count 0. Every existing caller passes a valid config and asserts require.NoError; nothing asserts NewAnthropic rejects an invalid bedrock config. The white-box harness in anthropic_internal_test.go already builds config.AWSBedrock cases, so a table row passing an invalid config and asserting bedrock config: ... is a few lines. Consequence is bounded (request-time Validate() still catches it on first request), hence P3, but the fix's stated value, catching it at construction, is exactly what nothing guards.

🤖

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

attribute.Bool(tracing.Streaming, streaming),
attribute.Bool(tracing.IsBedrock, i.bedrock != nil),
}
if i.bedrock != nil {

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-2] The new aws_bedrock_protocol trace branch, the PR's only observable behavior, has 0% coverage, and ResolvedProtocol()'s empty->invoke-model mapping never runs under test. (Bisky P3; Netero Note)

go tool cover reports both baseTraceAttributes (base.go:166) and ResolvedProtocol (config.go:76) at 0.0%

If the mapping regresses (emits "", or the branch stops firing), spans silently carry a missing or wrong protocol and nobody notices until someone debugs a Bedrock incident with bad telemetry. Blast radius is observability only, hence P3, but the gap is permanent absent a test. Bisky wrote and ran a green sketch: build an interceptionBase with a bedrock config, assert baseTraceAttributes emits invoke-model for the empty protocol and omits the attribute when bedrock == nil. One test covers the resolver, the append branch, and the nil-guard.

🤖

Comment thread aibridge/config/config.go
// ResolvedProtocol returns the configured protocol, mapping the empty value to
// the legacy InvokeModel protocol so existing providers keep the legacy
// behavior.
func (c AWSBedrock) ResolvedProtocol() BedrockProtocol {

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-5] ResolvedProtocol() centralizes the empty->InvokeModel mapping but is wired to only the trace attribute, leaving the same invariant hand-rolled in two other places. (Mafuuu P3, Ryosuke P3, Chopper Nit, Razor Nit)

The empty-value default now lives in three spots: here, isBedrockInvokeModel() (base.go:143, Protocol == "" || Protocol == BedrockProtocolInvokeModel), and Validate() (config.go:86, case "", BedrockProtocolInvokeModel). Behavior is identical today, so nothing breaks now. The consequence is latent divergence: if the zero-value default ever changes, a maintainer updating ResolvedProtocol() leaves the routing predicates disagreeing, so Model()/dispatch route down one protocol while the trace attribute reports the other. Route isBedrockInvokeModel()/isBedrockMantle() through ResolvedProtocol() so the invariant lives in one place.

🤖

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

Comment thread aibridge/config/config.go
Protocol BedrockProtocol
}

// ResolvedProtocol returns the configured protocol, mapping the empty value to

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-6] The ResolvedProtocol doc says "legacy" twice in one sentence. (Gon)

"mapping the empty value to the legacy InvokeModel protocol so existing providers keep the legacy behavior." Drop the first: "returns the configured protocol, or BedrockProtocolInvokeModel when unset, so existing providers keep legacy behavior."

🤖

Comment thread aibridge/config/config.go
// ResolvedProtocol returns the configured protocol, mapping the empty value to
// the legacy InvokeModel protocol so existing providers keep the legacy
// behavior.
func (c AWSBedrock) ResolvedProtocol() BedrockProtocol {

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-7] ResolvedProtocol collapses to return cmp.Or(c.Protocol, BedrockProtocolInvokeModel). (Ging-Go)

BedrockProtocol is a string type, so cmp.Or (Go 1.22+) returns c.Protocol when non-empty else the default, identical behavior with no side effects. go.mod is on 1.26, and cmp.Or is already used in the tree (e.g. aibridge/internal/testutil/mockupstream.go:214).

🤖

}
}

// NOTE: no t.Parallel() because the subtests use t.Setenv.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

needs to be a nolint

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 passed CI

Comment thread aibridge/config/config.go
return BedrockProtocolInvokeModel
}
return c.Protocol
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Potentially a larger refactor, so take it or leave it: unexport c.Protocol and rename this method Protocol() so that callers are forced to use this method that handles the zero value.

Alternatively, make BedrockProtocolInvokeModel the empty string?

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.

Potentially a larger refactor, so take it or leave it: unexport c.Protocol and rename this method Protocol() so that callers are forced to use this method that handles the zero value.

I think this requires too many changes. We'd need to add a constructor and update dozens of call sites.
It also goes against the pattern we use in config/config.go, where all fields are exported.

Alternatively, make BedrockProtocolInvokeModel the empty string?

That's an interesting idea—I hadn't considered it when I first implemented this.
At this point, though, it would require additional work, since we'd also need to update the codersdk layer.
I'm also not fully convinced it's an improvement. An empty string can be confusing at the API level.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wouldn't consider this confusing:

type BedrockProtocol string
const (
  BedrockProtocolDefault     = ""
  BedrockProtocolInvokeModel = "invoke-model"
  BedrockProtocolMantle      = "mantle"
)

...

  switch (proto) {
    case BedrockProtocolDefault, BedrockProtocolInvokeModel:
      // handle invoke-model
    case BedrockProtocolMantle:
      // handle mantle
    default:
      // unknown, error
  }

But it's fair enough to leave it as a follow-up. We do still have a window to change the bedrock protocol stuff around before the next release though!

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 you mean:

type BedrockProtocol string
const (
  BedrockProtocolInvokeModel = ""
  BedrockProtocolMantle      = "mantle"
)

...

  switch (proto) {
    case BedrockProtocolInvokeModel:
      // handle invoke-model
    case BedrockProtocolMantle:
      // handle mantle
    default:
      // unknown, error
  }

@evgeniy-scherbina
evgeniy-scherbina merged commit 4d884c3 into main Jul 14, 2026
28 of 29 checks passed
@evgeniy-scherbina
evgeniy-scherbina deleted the yevhenii/bedrock-mantle-fixes branch July 14, 2026 19:30
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 14, 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