fix: validate bedrock protocol at provider construction - #27234
Conversation
|
/coder-agents-review |
|
Chat: Review posted | View chat Review history
deep-review v0.9.0 | Round 1 | Last posted: Round 1, 6 findings (1 P2, 3 P3, 2 Nit), COMMENT. Review Finding inventoryFinding inventory: PR #27234Findings
Contested and acknowledgedCRF-4 (P2, anthropic.go:72) - construction validates unresolved config
Round logRound 1Netero 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-reviewCRF = Coder Review Finding (P0-P4, Nit, Note)
|
There was a problem hiding this comment.
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 afeat: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.
| // so it is cheap to run at construction. | ||
| var bedrock *messages.BedrockRuntime | ||
| if bedrockCfg != nil { | ||
| if err := bedrockCfg.Validate(); err != nil { |
There was a problem hiding this comment.
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.
🤖
| // so it is cheap to run at construction. | ||
| var bedrock *messages.BedrockRuntime | ||
| if bedrockCfg != nil { | ||
| if err := bedrockCfg.Validate(); err != nil { |
There was a problem hiding this comment.
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.
🤖
| attribute.Bool(tracing.Streaming, streaming), | ||
| attribute.Bool(tracing.IsBedrock, i.bedrock != nil), | ||
| } | ||
| if i.bedrock != nil { |
There was a problem hiding this comment.
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 coverreports bothbaseTraceAttributes(base.go:166) andResolvedProtocol(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.
🤖
| // 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 { |
There was a problem hiding this comment.
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.
🤖
| Protocol BedrockProtocol | ||
| } | ||
|
|
||
| // ResolvedProtocol returns the configured protocol, mapping the empty value to |
There was a problem hiding this comment.
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."
🤖
| // 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 { |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
It passed CI
| return BedrockProtocolInvokeModel | ||
| } | ||
| return c.Protocol | ||
| } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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
}
Follow-up PR to #26745