feat: full AWS Bedrock coverage (#214) - #298
Conversation
Complete the Bedrock SDK-compat surface end-to-end so the real aws-sdk-go-v2 bedrock, bedrockruntime, bedrockagent, and bedrockagentruntime clients all work against the in-memory backend. Runtime (bedrock-runtime): - ConverseStream + InvokeModelWithResponseStream over vnd.amazon.eventstream - CountTokens, ApplyGuardrail - Async invoke: StartAsyncInvoke / GetAsyncInvoke / ListAsyncInvokes Control plane (bedrock): - Guardrail policy configs (topic/content/word/sensitive-info/contextual-grounding) and guardrail versions (CreateGuardrailVersion + version-addressed Get/Delete/List) - Model import jobs, model copy jobs, model evaluation jobs (+ Stop) - Inference profiles, prompt routers - Marketplace model endpoints (incl. Register/Deregister) - Foundation model agreements (Create/Delete/ListOffers/GetAvailability) - Automated reasoning policies (CRUD) - Resource tagging: TagResource / UntagResource / ListTagsForResource (also persists tags previously accepted-but-dropped on guardrail/provisioned create) Agents (new bedrock-agent / bedrock-agent-runtime services + SDK deps): - Agents, knowledge bases, data sources, flows, prompts (CRUD + lifecycle) - Runtime: InvokeAgent (eventstream), Retrieve, RetrieveAndGenerate Implemented across all layers (driver interface, in-memory provider, portable API, restJson1 SDK-compat handler) with unit tests and real-SDK roundtrip tests per subsystem. The two agent services register before the S3 catch-all, and the agent-runtime handler registers before the agent control plane and matches only POST so the shared /agents and /knowledgebases roots never collide. Verified: go build/vet/gofmt clean, golangci-lint clean, go test ./... passing, and a full-server end-to-end exercise with the real SDK clients over a socket. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…l-bedrock-coverage
NitinKumar004
left a comment
There was a problem hiding this comment.
Review — full AWS Bedrock coverage (end-to-end, blast-radius focus)
Impressively complete and well-layered, with real-SDK roundtrip coverage. Blast radius on existing code is tightly contained (only server/aws/aws.go, providers/aws/aws.go, go.mod/go.sum change). The load-bearing pieces check out; a few things are worth addressing.
✅ Verified clean
- Routing order & agent collision — registration is
bedrock→bedrock-agent-runtime→bedrock-agent→ … → S3 last; runtime is POST-only on/…/text,/…/retrieve,/retrieveAndGenerate, and no control-plane path ends in those, so they never collide (traced path-by-path). - Eventstream framing — official
aws-sdk-go-v2/aws/protocol/eventstreamencoder; roundtrip tests decode end-to-end. Bodies bounded (MaxBytesReader); path parsing length-guarded. go.mod/go.sum— core SDK v1.42.1→v1.43.0 (minor),go mod verifyclean,tidyno-op. Factory wiring additive, no nil-panic. Tags-on-create fix safe (response bytes unchanged;ListTagsForResourceis net-new). Layering/error-mapping/determinism clean.
🟠 MEDIUM
- Concurrent-map-write panic —
HyperParametersreturned by reference.providers/aws/bedrock/bedrock.goGetModelCustomizationJob/ListModelCustomizationJobs/GetCustomModel/ListCustomModelsdoresult := *job— the copy'sHyperParametersmap aliases the stored map. CreatecopyMaps in but reads don't copy out. Concurrent read+mutate →fatal error: concurrent map read and map writeunderserve— same class as the LBExtrabug in #296. Fix:copyMapon return. - S3 shadowing via unanchored prefixes.
server/aws/bedrockagent/handler.go:84-87—/knowledgebases,/flows,/promptsuseHasPrefixwith no trailing slash, so (registering before the S3 catch-all) they intercept S3 requests for buckets named or prefixed with those words (flows-prod,promptsdb,knowledgebases-archive) and objects under a bucket namedagents. Inconsistent with the siblingbedrockhandler'sunderPrefix(p,pre)=p==pre||HasPrefix(p,pre+"/"). Fix: reuseunderPrefix. - Guardrail aliasing (no copy at all).
management.gostoresGuardrailPolicies: cfg.GuardrailPoliciesand returns*g— the caller's config becomes internal state (post-create mutation rewrites the stored guardrail); readers share policy pointers. Unlike HyperParameters there's no defensive copy anywhere. guardrailRecord.versionsslice mutated without a lock.guardrail_versions.go(append,nextVer++) andmanagement.go(reslice on delete) mutate after the transient RLock is released;ListGuardrailsranges it concurrently → data race on the slice header (append can realloc). Compound mutation, beyond the accepted scalar-flip convention.- Nondeterministic list ordering. New
List*endpoints (jobs, inference profiles, prompt routers, AR policies, marketplace) usememstore.All()instead of the documentedSortedValues()— violates the deterministic-ordering convention; flaky-test risk. - Fidelity + tests bake in wrong behavior:
StopEvaluationJobsilently rewrites a terminal (Completed) job toStopped(real AWS →ConflictException);DeregisterMarketplaceModelEndpointis a success no-op leavingStatus=REGISTERED. Both have tests asserting the incorrect behavior. - No concurrency/
-racetests anywhere in the new bedrock code — so #1/#3/#4 are entirely uncaught.
🟡 LOW
ApplyGuardrailignoresGuardrailVersion(a version-aware lookup exists but is unused).[]bytepayloads (EvaluationConfig,InferenceConfig,PolicyDefinition) stored by direct alias — inconsistent with marketplace'scopyBytes; slice fields (Models, modalities) returned by reference.List*(async invoke etc.) ignore pagination/filters; streaming write-errors swallowed (no abort);ConverseStreamomitscontentBlockStart.CountTokensreturnsInvalidArgument/400 for an unknown model whileGetFoundationModelreturnsNotFound/404 for the same condition.- bedrock-agent: no parent-child delete cascade (orphaned data sources/aliases), lifecycle states (
Preparing) and theirFailedPreconditionpaths are dead/unvalidated,json.RawMessagebodies aliased, fragile string EOF check (vserrors.Is), missing portable-layer test; duplicate-create not detected for marketplace/inference-profile/prompt-router.
Bottom line
Sound feature. The one to block on is #1 (concurrent-map-write panic — a repeat of the #296 class); #2 (S3-shadowing regression) is trivially fixable by anchoring the prefixes. The rest are consistency/fidelity/test-gap items best swept together, ideally with a -race test that mutates a returned map/slice.
… fidelity Applies the review feedback on stackshy#214. Concurrency / immutability (data-race + aliasing): - Guardrail policies deep-copied on create/update and on version snapshot, so version snapshots are truly immutable and callers can't mutate stored state. - guardrailRecord guarded by a sync.RWMutex (draft/versions/nextVer); reads copy under the lock. go vet copylocks-clean. - Copy-on-write for in-place mutators that a new -race test surfaced: StopEvaluationJob, marketplace Update/Register, AR-policy Update. - copyMap on customization-job/custom-model read paths; copyBytes on Evaluation/Inference/PolicyDefinition []byte stores; copyRaw on bedrock-agent json.RawMessage configs. - New -race concurrency tests (guardrail + evaluation job). Routing: - bedrock-agent handler anchors /knowledgebases, /flows, /prompts via underPrefix so bucket paths like /flows-prod fall through to S3 (no shadowing); test added. Fidelity / consistency: - List endpoints (async invoke, import/copy/eval jobs, inference profiles, prompt routers, AR policies, marketplace) use SortedValues() for deterministic order. - Duplicate CreateMarketplaceModelEndpoint / CreatePromptRouter now return AlreadyExists instead of silently overwriting. - bedrock-agent-runtime decode uses errors.Is(io.EOF); dead *Preparing constants removed. Verified: go build/vet/gofmt clean, golangci-lint clean, go test ./... passing, go test -race on the bedrock packages clean, and an exhaustive 102-operation real-SDK end-to-end run against the full server passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the thorough, blast-radius-focused review — really helpful. I verified every point against the code and pushed fixes in 🟠 MEDIUM1. 2. S3 shadowing via unanchored prefixes. ✅ Fixed. Added 3. Guardrail aliasing. ✅ Fixed. Added 4. 5. Nondeterministic list ordering. ✅ Fixed. Switched all eight new 6. Fidelity —
7. No 🟡 LOW
Verification
|
NitinKumar004
left a comment
There was a problem hiding this comment.
Re-review @ 68a5b5c — all MEDIUMs fixed; a few fidelity items remain
Verified each fix directly against the code.
✅ Fixed & verified
- HyperParameters concurrent-map-write panic —
copyMapnow on every read path (GetModelCustomizationJob/List,GetCustomModel/List). The block-on item is resolved. - S3 shadowing —
Matchesnow anchors all four roots viaunderPrefix(p, pre), so/flows-prod,/promptsdb,/knowledgebases-archivefall through to S3; test added. - Guardrail aliasing + race —
deepCopyGuardrailPolicieson create/update and on version snapshot (snapshots are now truly immutable);guardrailRecordguarded by async.RWMutexwith reads copying under the lock. - List ordering —
SortedValues()across the newList*endpoints (deterministic). - Copy discipline —
copyByteson Evaluation/Inference/PolicyDefinition[]byte;copyRawon bedrock-agentjson.RawMessage. - Duplicate-create — marketplace / prompt-router / inference-profile now return
AlreadyExists. - bedrock-agent — decode uses
errors.Is(io.EOF); dead*Preparingconstants removed. - New
-raceconcurrency tests (guardrail + eval job). Nice.
🟡 Remaining (LOW / fidelity — only the concurrency aspect was fixed)
StopEvaluationJobstill rewrites a terminal job.jobs.gonow does copy-on-write (good, fixes the race) but still unconditionally setsStatus=JobStopped. Since eval jobs complete synchronously toCompleted, Stop can only ever hit a terminal job — real AWS returnsConflictException. The tests still assert success (asyncinvoke_jobs_test.go:157,sdk_roundtrip_asyncjobs_test.go:261), so they bake in the non-AWS behavior.DeregisterMarketplaceModelEndpointstill a no-op — validates existence, returns nil;StatusstaysREGISTEREDafter deregister.ApplyGuardrailstill ignoresGuardrailVersion(counttokens_applyguardrail.go:40onlyfindGuardrailRecord(identifier)); an unknown version succeeds withAction=NONE.- Slice-field read-aliasing.
GetInferenceProfile/GetPromptRouter/GetAutomatedReasoningPolicyreturnresult := *pwhose.Modelsslice still aliases stored state. Create copies in (append([]string(nil), …)) but reads don't copy out — the same class the map fix addressed, not applied to slices. Read-mostly, so low, but inconsistent. - Prior LOWs out of this commit's scope still stand: streaming write-error swallowing (no abort),
List*pagination gaps,CountTokensvsGetFoundationModelreturning different error codes for an unknown model, and bedrock-agent lacking parent-child delete cascade.
Bottom line
The important set — the concurrent-map-write panic, the S3-shadowing regression, and the guardrail aliasing/race — are all correctly fixed and now covered by -race tests. What's left is LOW fidelity; the two worth a quick follow-up are #1 and #2, since a test currently asserts non-AWS StopEvaluationJob behavior.
NitinKumar004
left a comment
There was a problem hiding this comment.
Architecture & pattern-conformance review @ 68a5b5c
Verdict: follows cloudemu's documented end-to-end pattern faithfully — one of the more idiomatic large PRs. No outstanding major bugs.
✅ Follows the pattern (checked each layer against the docs)
- Full 4-layer structure for all three new services (
bedrock,bedrock-agent,bedrock-agent-runtime):services/*/driver(interface) →providers/aws/*(in-memory Mock) →services/*(portable API) →server/aws/*(restJson1 handler). Matches the "Adding a New Service" recipe. - Portable layer applies the real cross-cutting concerns, not a passthrough:
services/bedrock/bedrock.go'sdo()is the canonicalstorage/pattern (error-injection → rate-limit → latency → call → metrics → recorder), sameWith*options. All three services carry the fullrecorder/metrics/limiter/injector/latencyset. internal/memstore.Store[V]backing (*memstore.Store[*driver.CustomizationJob]…),sync.RWMutexdiscipline (guardrail-record mutex added in the fix), copy-on-read/write applied.- ARNs via
idgen— 33idgen.*calls, zero hand-builtarn:aws:strings. cerrors→ typed restJson1 errors (X-Amzn-Errortype),server/wire/*helpers, and the officialaws/protocol/eventstreamencoder for streaming.- Registration most-specific-first (agent-runtime → agent-control → S3) with
underPrefixanchoring so S3 bucket paths aren't shadowed. - docs/services.md updated — Bedrock in the service table (row 20), AWS-only status explicitly documented (
Azure: — GCP: —), so that's a recorded design choice.
⚠️ Pattern deviations / gaps (minor)
- Missing portable-layer test for
services/bedrockagent.services/bedrockandservices/bedrockagentruntimeeach have a_test.go;services/bedrockagenthas none (convention: portable API test per service). - AWS-only (no Azure/GCP mirror). Legitimate for a provider-specific AI service and documented, but a deviation from the "all 3 providers mirror" rule; the portable wrapper is somewhat vestigial with a single provider (kept for consistency — fine).
- No
SetMonitoring/CloudWatch-metrics wiring. Real Bedrock emitsInvocations/InvocationLatency; the mock doesn't push auto-metrics like S3/EC2/Lambda/SQS. Optional, low. - Docs likely name only
bedrock/bedrock-runtime, not the two newbedrock-agent*services — worth adding a docs entry for them.
Major bugs — none outstanding
The two majors are fixed and race-tested in 68a5b5c:
- HyperParameters concurrent-map-write panic →
copyMapon all read paths. - S3 route shadowing →
underPrefixanchoring.
Remaining are all LOW fidelity (not correctness/architecture): StopEvaluationJob still rewrites a terminal job (its test asserts that), DeregisterMarketplaceModelEndpoint no-op, ApplyGuardrail ignores version, slice-field read-aliasing (maps copied, slices not).
Bottom line
Architecturally proper, end-to-end — mirrors the established service layout, portable wrapper, memstore, idgen, error-mapping, and routing conventions. The only true convention gap is the missing services/bedrockagent portable test; AWS-only and absent metrics-wiring are documented/acceptable deviations; leftover fidelity items are low.
Follow-up to the second/third PR reviews on stackshy#214. Fidelity: - Evaluation jobs are now created InProgress (evaluation is long-running with no synchronous artifact, unlike import/copy), so StopEvaluationJob is meaningful; Stop rejects a non-InProgress job with FailedPrecondition, which the bedrock error mapper now surfaces as ConflictException (409), matching real AWS. - DeregisterMarketplaceModelEndpoint removes the Bedrock registration record, so a subsequent Get returns NotFound (the underlying SageMaker endpoint is unmodeled) — matching AWS instead of a success no-op. - ApplyGuardrail validates a requested numbered GuardrailVersion (NotFound if the version doesn't exist); "" / "DRAFT" resolve to the working draft. Aliasing/consistency: - GetInferenceProfile / GetPromptRouter / GetAutomatedReasoningPolicy (and their List) copy the Models slice / PolicyDefinition bytes out on read via clone helpers, so callers can't mutate stored state through a returned value. Pattern conformance: - Added the missing services/bedrockagent portable-layer test. - docs/services.md now lists bedrock-agent (+ bedrock-agent-runtime) and the operation-count table reflects the full surface. Tests updated for the corrected eval-job lifecycle and marketplace deregister semantics. Verified: go build/vet/gofmt/golangci-lint clean, go test ./... passing (174 pkgs), go test -race on the bedrock packages clean, and an exhaustive 107-operation real-SDK end-to-end run against the full server passing (including the changed behaviors). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the re-review and the architecture pass — pushed Remaining fidelity items (from the re-review)
Architecture review
Appreciate the careful passes — this made the PR meaningfully better. |
thzgajendra
left a comment
There was a problem hiding this comment.
Reviewed in depth across concurrency, eventstream framing, routing, restJson1 fidelity, control-plane + agent lifecycle, layering/immutability, and tests/deps/docs. Strong, well-structured PR — no blocker. The hard wire-level parts are solid and real-SDK-roundtrip-tested. The substantive items are completeness/fidelity gaps in the agent subsystem plus a copy-out immutability gap — none correctness-critical.
Verified sound
- Eventstream framing — ConverseStream / InvokeModelWithResponseStream / InvokeAgent use the real
eventstream.NewEncoderwith correct:message-type/:event-type/:content-typeheaders,vnd.amazon.eventstream, flush-per-event, and are decoded by genuine aws-sdk-go-v2 clients in roundtrip tests. - Routing — first-match by (method+path); agent-runtime registers before agent-control-plane before the S3 catch-all; the runtime predicate (POST +
/…/text|/…/retrieve|/retrieveAndGenerate) can't steal a control-plane request and vice versa. No collision/shadowing. - Guardrail version immutability (a prior re-review item) — versions are deep-cloned snapshots; a draft edit can't mutate a prior version. Tested.
- Tag round-trip fix confirmed (create→list→untag with a real SDK client).
- restJson1 errors —
__type+x-amzn-errortype+ correct status; not-found is typedResourceNotFoundException. - Deps clean, roundtrip tests genuine (typed assertions), and there's a
-race-targeted concurrency test.
Substantive (MEDIUM — fidelity/consistency, none blocking)
- bedrock-agent has no versions/aliases model.
PrepareAgentonly flips status→PREPARED; there's noAgentVersiontype,CreateAgentAliastakes no version, andAgent.Versionstays"DRAFT". Real Bedrock creates an immutable numbered version at Prepare that aliases route to. Action groups are also absent entirely. For a PR titled "full coverage," worth confirming these are intentionally out of scope (a note in the PR/docs would do). - KB delete doesn't cascade —
providers/aws/bedrockagent/knowledgebases.go:100DeleteKnowledgeBaseremoves only the KB entry; data sources (keyed byKnowledgeBaseID) and ingestion jobs are left orphaned in the store. Real data-consistency gap. - Copy-out immutability gap — several
Get*/List*return shallowresult := *x(e.g.providers/aws/bedrockagent/datasources.go:42,54; also bedrock marketplace-endpoint + eval-job byte fields) wherejson.RawMessage/[]byte/slice fields still alias the store.copyRaw/copyBytesare applied on the way IN but not OUT — inconsistent with the sibling guardrail/AR-policy paths that clone out. A caller mutating a returned slice corrupts stored state. Low-effort fix: apply the existing helpers on the read path. - Error taxonomy incomplete —
server/aws/bedrock/errors.go:29writeErrdoesn't mapPermissionDenied→403AccessDeniedExceptionorResourceExhausted→ServiceQuotaExceededException; they fall to thedefault(generic 500). Latent (no handler emits those today), but a fidelity trap the moment a quota/authz path is added.
Minor
- No in-stream exception frame — all errors fire before the 200 eventstream opens. Acceptable since results are computed up-front, but there's no mid-stream
:message-type=exceptionpath (and none tested). ListAsyncInvokes(and import/copy/eval lists) ignore filters + pagination (NextTokenalways empty); noClientRequestTokenidempotency.- Docs list the ops but omit the "jobs complete synchronously / responses are simulated" caveats (only in code comments).
- Confirm CI runs
go test -race, else the concurrency test is inert.
Note
I looked hard at a potential guardrail data race in findGuardrailRecord (unlocked read while UpdateGuardrail mutates) — it's not a race: the loop reads only the write-once ID/ARN, while UpdateGuardrail writes different fields under lock and never reassigns rec.draft. A defensive RLock in the loop would harden it against future field changes, but -race won't flag it today.
Nice work on the streaming + routing — those are the parts most likely to break a real SDK client, and they hold up. The agent-versioning/action-group scope and the KB-cascade + copy-out fixes are the ones I'd prioritise.
…or taxonomy Follow-up to the fourth PR review on stackshy#214. - bedrock-agent cascade delete: DeleteKnowledgeBase now also removes its data sources and ingestion jobs; DeleteDataSource removes its ingestion jobs; DeleteAgent removes its aliases. No more orphaned children in the store. - Copy-out immutability on read: Get/List/Update/Create-return paths now clone json.RawMessage / []byte fields out (knowledge-base + data-source + flow + prompt configs; marketplace endpointConfig; evaluation-job evaluation/inference configs), so a caller mutating a returned value can't corrupt stored state — consistent with the guardrail/AR-policy paths. - Error taxonomy: writeErr in bedrock, bedrock-agent, and bedrock-agent-runtime now maps PermissionDenied -> 403 AccessDeniedException and ResourceExhausted -> 400 ServiceQuotaExceededException instead of falling through to 500. - CI: added a scoped `go test -race` step for the Bedrock packages so their concurrency tests actually exercise the race detector in CI. - Docs: recorded that bedrock-agent versioning/aliases-beyond-create, action groups, and collaborators are out of scope for this iteration, and that jobs complete synchronously / inference responses are deterministic simulations. Verified: go build/vet/gofmt/golangci-lint clean, go test ./... (204 pkgs) passing, and go test -race on the bedrock packages clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @thzgajendra — excellent, careful review (and the note ruling out the MEDIUM
Minor
Appreciate the depth here — the cascade + copy-out fixes in particular are real improvements. |
thzgajendra
left a comment
There was a problem hiding this comment.
Re-reviewed at a842411 — every item from my review is resolved. Verified against source, not just the commit message.
- KB cascade delete (MEDIUM #2) — fixed.
DeleteKnowledgeBasenow callsdeleteDataSourcesForKnowledgeBase+deleteJobsForKnowledgeBase;DeleteDataSourceremoves its jobs;DeleteAgentremoves its aliases. No orphaned children. Covered byTestDeleteKnowledgeBaseCascade,TestDeleteDataSourceCascadesJobs,TestDeleteAgentCascadesAliases. - Copy-out immutability (MEDIUM #3) — fixed. Get/List/Update/Create now clone the
json.RawMessage/[]bytefields out viacloneDataSource/copyRaw(and the equivalents for KB/flow/prompt/marketplace/eval-job) — the shallowresult := *xis gone, consistent with the guardrail/AR-policy paths.TestDataSourceCopyOutImmutableasserts a caller can't corrupt stored state. - Error taxonomy (MEDIUM #4) — fixed.
writeErr(all three services) now mapsPermissionDenied → 403 AccessDeniedExceptionandResourceExhausted → 400 ServiceQuotaExceededException(plus FailedPrecondition→Conflict, Throttled→429), no longer falling through to 500. - Agent versioning / aliases-beyond-create / action groups (MEDIUM #1) — resolved by scoping: now explicitly documented as out of scope for this iteration in
sdk-server.md, alongside the synchronous-terminal-job + simulated-response caveats. That's the right call — the confirm-scope ask is answered. - CI
-race(minor) — added: a scopedgo test -racestep over all five bedrock packages, so the concurrency tests actually exercise the detector.
The deliberately-unchanged minors (no in-stream exception frame — fine since results are computed up-front; list filters/pagination + ClientRequestToken idempotency — repo convention) are acceptable. And to close the loop on my earlier note: the findGuardrailRecord "race" was never a race, so nothing to do there.
From my side everything's cleared — thorough turnaround. Nice work.
NitinKumar004
left a comment
There was a problem hiding this comment.
Bug hunt @ a842411 — no major bugs
Verified each item directly. go build / go vet / golangci-lint / go test -race on the bedrock packages are all clean.
Verified fixed
- Concurrent-map-write panic (HyperParameters) →
copyMapon all read paths (-raceclean). - S3 route shadowing →
underPrefixanchoring. StopEvaluationJobreturnsFailedPreconditionfor a non-InProgressjob (real-AWSConflictExceptionsemantics), copy-on-write.ApplyGuardrailvalidates the numberedGuardrailVersionviaversionSnapshot("" / "DRAFT" → draft).- Slice read-aliasing →
cloneInferenceProfile/clonePromptRouter/cloneARPolicycopyModelson read. - Cascade delete (new) —
DeleteKnowledgeBase→ data sources + ingestion jobs;DeleteAgent→ aliases. Correct parent-field matching (KnowledgeBaseID/AgentID),All()-snapshot makes delete-during-range safe.DeleteFlow/DeletePromptcorrectly don't cascade (no child stores modeled — confirmed). - Error taxonomy —
PermissionDenied → 403 AccessDeniedExceptionandResourceExhausted → ServiceQuotaExceededExceptionacross all three handlers.
Remaining (LOW — fidelity only)
DeregisterandDeletemarketplace endpoint are now byte-identical (bothHas→Delete). The no-op is fixed, but it swung the other way — two distinct AWS ops are collapsed, so afterDeregisteraGetreturnsNotFound; real AWS keeps the endpoint in a deregistered state and onlyDeleteremoves it. Cosmetic unless a caller models the two-step lifecycle.StopEvaluationJobhappy-path is unreachable — jobs complete synchronously to a terminal state, soStopalways conflicts. Repo-wide synchronous-completion tradeoff (documented), not a defect.
Pattern conformance
Still fully idiomatic — 4-layer structure, portable do() cross-cutting wrapper, memstore+RWMutex, idgen ARNs, typed cerrors→restJson1, correct registration/underPrefix. The earlier gaps (missing services/bedrockagent portable test, docs for the agent services) were closed in ce48796.
Bottom line
Clean — no bug that would block merge. Everything from the prior rounds is resolved and race/lint-tested. Only the Deregister==Delete collapse is worth a glance (LOW fidelity). Strong shape.
NitinKumar004
left a comment
There was a problem hiding this comment.
Deep re-review (multi-agent: blast radius, cascade, edge cases, concurrency) @ a842411
Fresh 5-agent pass dimensioned like the #301 review. The earlier majors stay fixed and the map-panic aliasing class is essentially eradicated — but this deeper sweep surfaced one new real correctness bug plus a determinism inconsistency worth fixing, and refines the S3-shadowing picture.
✅ Verified clean (re-confirmed)
- Cascade/lifecycle solid —
DeleteKnowledgeBase→data sources + ingestion jobs,DeleteDataSource→its jobs,DeleteAgent→aliases, all matching the correct parent field;All()-snapshot delete-safe; guardrail-version delete cascades. No orphaning, no wrong-field matches. - Concurrency — HyperParameters/Extra/GuardrailPolicies/RawMessage/Models all deep-copied on read + copy-on-write on mutate; guardrail-record mutex +
-racetest correct. - Error taxonomy correct (incl.
PermissionDenied→403,ResourceExhausted→400 ServiceQuotaExceeded); CountTokensValidationExceptionvs GetFoundationModelResourceNotFounddivergence is intentional and matches real AWS; tagging defensive-copies. - Eventstream framing (official SDK encoder), 200-header-before-driver-call ordering,
MaxBytesReaderbody bounds, length-guarded path parsing,errors.Is(io.EOF)— all correct.idgenARNs, 3-layer conformance,go.mod/deps, factory wiring — all clean/additive.
🟠 MEDIUM
1. ConverseStream corrupts non-ASCII responses (NEW). chunkText (server/aws/bedrock/streaming.go) splits the completion at len(s)/2 — a byte offset, not a rune boundary. When the midpoint lands inside a multi-byte UTF-8 sequence each half is invalid UTF-8, and encoding/json replaces the broken bytes with U+FFFD, so the client reassembles corrupted text. Reproduced: a 183-byte 3-byte-rune string → equal=false, contains U+FFFD=true (a rune-aligned length passes, which is why it's intermittent). Non-streaming Converse emits the block whole and is correct, so the two paths disagree; TestSDKConverseStream only uses ASCII and misses it. Fix: split on a rune boundary (or don't split).
2. Four List* endpoints are nondeterministic (NEW). ListModelCustomizationJobs/ListCustomModels (bedrock.go:200,214), ListGuardrails (no-identifier branch, management.go:92), ListProvisionedModelThroughputs (management.go:257) build results from .All() (random map order) while 9 sibling lists correctly use SortedValues(). Contradicts the documented memstore contract ("List endpoints must iterate SortedValues") → flaky-test risk and non-AWS-like ordering.
3. S3 bucket-name shadowing (refines the earlier item). underPrefix correctly fixed /flows-prod, but a bucket named exactly agents/flows/prompts/knowledgebases under path-style addressing is still claimed by the bedrock-agent handler before the S3 catch-all — e.g. GET /prompts (ListObjects) now returns ListPrompts JSON; PUT /agents/config.json now hits UpdateAgent. Same REST-vs-catch-all tradeoff as /custom-models/EKS /clusters, but these are far more plausible bucket names and the misroute is silent. At minimum a doc note in New(); ideally a shape guard.
🟡 LOW
- FoundationModel Get/List slice aliasing (NEW).
GetFoundationModel/ListFoundationModels(bedrock.go:82,95) return shallow copies whoseInputModalities/OutputModalities/etc. alias the seed — andseedFoundationModelsreuses the same backing slices across models, so a caller mutating one model's modalities corrupts every model's. Read-only today; latent. Fix: acloneFoundationModelcopying the four slices. - LoggingConfig pointer aliasing (NEW).
Put/GetModelInvocationLoggingConfiguration(management.go:311,331) share the*S3LoggingConfig/*CloudWatchLoggingConfig(and nestedLargeDataDeliveryS3) pointers with the caller and the store;logMuguards only the pointer swap. Deep-copy on store + read. CreateInferenceProfile(andCreateAutomatedReasoningPolicy) lack a duplicate-name guard — inconsistent withCreatePromptRouter/provisioned-throughput which returnAlreadyExists; real Bedrock usesConflictException.UpdateGuardrailrename can clobber a different guardrail (management.go:127):Delete(old)+Set(new)with no collision check silently overwrites an existingnew.RegisterMarketplaceModelEndpointsemantic inversion (marketplace_agreements.go:122): requires the endpoint to already exist (else NotFound); real AWS registers an externally-created SageMaker endpoint. Redundant as written.CreateFoundationModelAgreementaccepts any modelId (marketplace_agreements.go:156) with no catalog check → laterGetFoundationModelAvailabilityreports AVAILABLE for a nonexistent model.- Copy/import job target ARN uses non-standard resource types (
model-copy-target/…,imported-model/<name>) rather than acustom-model/…ARN. Cosmetic (ARNs still via idgen). CountTokensunion laxity: both members present → InvokeModel silently wins; neither → returns 0 instead ofValidationException.bedrockagentsilently drops tags —AgentConfig.Tags/KnowledgeBaseConfig.Tagsare accepted but there's no tag store /TagResourcefor agent resources.- Coverage gaps (fidelity, "full coverage"): agent action groups, and flow/prompt/agent versions & aliases aren't modeled — so nothing cascades and no in-use
DeleteFlow/DeletePromptguard can exist.StartAsyncInvokedoesn't dedupe onClientRequestToken. Streaming ignoresr.Context()on client disconnect (bounded event count → safe). Marketplace/AR-policy copy-on-write mutators lack a dedicated-racetest.
Bottom line
No CRITICAL/HIGH regressions — blast radius is contained and the concurrency work holds. The two to fix before merge are #1 (ConverseStream UTF-8 corruption — a real, reproducible correctness bug) and #2 (nondeterministic list ordering — contradicts an explicit codebase contract); #3 (bucket-name shadowing) deserves at least a doc note. The rest are LOW fidelity/robustness. A ConverseStream test with a multi-byte-UTF-8 completion would have caught #1.
…rminism, aliasing Follow-up to the deep multi-agent re-review on stackshy#214. Correctness: - ConverseStream: chunkText split the completion on a byte offset, corrupting multi-byte UTF-8 responses (broken halves marshalled as U+FFFD). Now advances the split to a rune boundary. Added a multi-byte-UTF-8 streaming roundtrip test (café/日本語/emoji) asserting the reassembled text is valid UTF-8, U+FFFD-free, and equal to non-streaming Converse. Determinism: - ListModelCustomizationJobs, ListCustomModels, ListGuardrails (no-identifier), and ListProvisionedModelThroughputs now use SortedValues() instead of All(), matching the documented memstore contract and the sibling lists. Aliasing / immutability: - FoundationModel Get/List clone the modality/customization/inference slices out (the seed reuses shared backing slices, so a mutated return would corrupt all). - Model-invocation LoggingConfig is deep-copied on store and read (nested S3/CloudWatch pointers no longer shared with caller/store). Fidelity / consistency: - CreateInferenceProfile and CreateAutomatedReasoningPolicy reject duplicate names with AlreadyExists (consistent with prompt routers / provisioned throughput). - UpdateGuardrail rename now errors instead of silently clobbering an existing guardrail with the target name. - RegisterMarketplaceModelEndpoint upserts (registers an externally-created endpoint) instead of requiring the record to pre-exist. - CreateFoundationModelAgreement validates the modelId against the catalog. - CountTokens with neither union member returns ValidationException instead of 0. Docs/tests: documented the bedrock-agent bucket-name-shadowing tradeoff on the handler; added -race tests for the marketplace + AR-policy copy-on-write mutators. Verified: go build/vet/gofmt/golangci-lint clean, go test ./... (204 pkgs) passing, and go test -race on the bedrock/agent packages clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @NitinKumar004 — the deep multi-agent pass was really valuable, especially catching the UTF-8 streaming bug. Pushed 🟠 MEDIUM
🟡 LOW — fixed
Notes / reply-only
Appreciate the thoroughness — the UTF-8 fix in particular is a real correctness improvement. |
✅ All review rounds resolved — full-flow e2e verified at
|
NitinKumar004
left a comment
There was a problem hiding this comment.
Re-review @ 44715f2 — all deep-review items fixed; two narrow aliasing leftovers
Verified each fix directly and via a two-agent pass (fix-completeness + a fresh aliasing/concurrency sweep). go test -race ./providers/aws/bedrock/... ./server/aws/bedrock/... is green.
✅ Fixed, verified complete & correct (with edge cases)
- ConverseStream UTF-8 corruption —
chunkTextnow advances the split to the nextutf8.RuneStart; the all-one-multibyte-rune edge returns a single intact chunk (traced "é"/"😀"). NewTestSDKConverseStreamMultibyteRuneBoundaryasserts valid UTF-8, no U+FFFD, and equality to non-streaming Converse. The reproducible bug is gone. - List determinism —
ListModelCustomizationJobs/ListCustomModels/ListGuardrails(no-id)/ListProvisionedModelThroughputsnow useSortedValues(). - FoundationModel aliasing —
cloneFoundationModeldeep-copies all 4 slices, used by both Get and List (load-bearing: the seed shares backing slices across models). - LoggingConfig aliasing —
deepCopyLoggingConfigcopiesS3,CloudWatch, and nestedCloudWatch.LargeDataDeliveryS3, applied on both store and read;TestModelInvocationLoggingCopyOutverifies both directions. - Fidelity/consistency —
CreateInferenceProfile/CreateAutomatedReasoningPolicydup-name →AlreadyExists;UpdateGuardrailrename guard correctly allows self-rename (newName != oldNameshort-circuit) and errors only on a real collision;RegisterMarketplaceModelEndpointupserts and preservesCreatedAton re-register;CreateFoundationModelAgreementvalidates modelId against the catalog (seeded models still pass, ordering preserved);CountTokensneither-member →ValidationExceptionwhile single-member still works. - Docs — bucket-name-shadowing tradeoff documented on the bedrock-agent handler.
- New
-racetests run the mutator concurrently with both Get and List (the real reader-vs-writer vector), not mutator-vs-itself. Good.
🟡 Remaining (LOW — narrow, not panic vectors, read-only in the server path)
clonePromptRoutermisses theResponseQualityDifference *float64pointer (registries.go:322). The clone reallocatesModelsbut keeps the stored pointer, soGetPromptRouter/ListPromptRoutershand every caller the same*float64— a de-aliasing function with a read-path hole. Fix:if p.ResponseQualityDifference != nil { v := *p.ResponseQualityDifference; out.ResponseQualityDifference = &v }.- Create-return paths shallow-copy reference fields — the fix de-aliased Get/List but left the create returns aliased:
CreateInferenceProfile(Models, registries.go:49),CreatePromptRouter(Models+ the*float64, :147),CreateAutomatedReasoningPolicy(PolicyDefinitionbytes, :228),CreateModelCustomizationJob(HyperParametersmap, management.go:188),CreateEvaluationJob(Eval/Inference bytes, jobs.go:197). Same class, on create instead of Get/List. LOW (needs a Go-library caller to mutate the returned reference).- Neither R1 nor R2 is exercised by the new
-racetests;InferenceProfile/PromptRouterhave no-racetest at all.
- Neither R1 nor R2 is exercised by the new
- Benign (note-only): async-invoke / import-job /
cloneEvaluationJobomit some[]string/tag fields, but those are never populated in the synchronous mock — no live leak, worth a one-line hardening comment. Dup-nameSortedValues-scan-then-Setis a non-atomic check-then-act (two concurrent same-name creates could both pass) — the codebase-wide memstore-has-no-CAS pattern, LOW.
Bottom line
Every item from the deep review — the UTF-8 correctness bug, list determinism, the FoundationModel/LoggingConfig aliasing, and all the fidelity/validation fixes — is correctly and completely addressed, with strong tests and no regressions. What's left is a single read-path pointer miss (clonePromptRouter) and the symmetric create-return shallow copies — all LOW and none reachable through the HTTP server. Clean pass.
NitinKumar004
left a comment
There was a problem hiding this comment.
Approving @ 44715f2
Every item from the deep multi-agent re-review is fixed, verified complete, and race-tested — with no regressions:
- ConverseStream UTF-8 corruption — split now advances to a rune boundary; new multibyte roundtrip test asserts valid UTF-8, no U+FFFD, and equality to non-streaming Converse.
- List determinism — the four
List*endpoints useSortedValues(). - Aliasing —
cloneFoundationModel(all 4 slices) anddeepCopyLoggingConfig(nested S3/CloudWatch pointers) on both store and read; new-racetests run mutators concurrently with Get and List. - Fidelity — dup-name guards,
UpdateGuardrailself-rename correctly allowed / collision rejected, marketplace upsert preservingCreatedAt,CreateFoundationModelAgreementcatalog validation,CountTokensneither-member →ValidationException. Bucket-shadowing tradeoff documented.
Cascade wiring, the concurrent-map-write class, error taxonomy, eventstream framing, routing/blast-radius, and 3-layer/idgen conformance were all verified clean across the review rounds. -race green on the bedrock packages; golangci-lint clean on all touched packages.
Remaining are two LOW read-path aliasing nits (unreachable through the HTTP server): clonePromptRouter doesn't deep-copy the ResponseQualityDifference *float64, and the create-return paths shallow-copy reference fields the fix de-aliased on Get/List. Worth a quick follow-up for consistency, but not blocking.
LGTM.
The prior CI run failed only on an unrelated, pre-existing flaky test:
server/aws/bedrock TestSDKConverseStream ('use of closed network
connection') — a load-sensitive httptest streaming-connection race from
stackshy#298, not touched by this PR. All other checks pass and the test is green
locally (30/30, incl. -race). No admin rights to re-run the job, so this
empty commit re-triggers the pull_request workflow.
…ponse CI's `go test -race` intermittently failed TestSDKConverseStream with "use of closed network connection" — a pre-existing, load-sensitive flake (from stackshy#298) surfaced by the race detector's slowdown on the 2-core runner. Root cause: converseStream decodes the request via json.Decoder, which stops at the end of the JSON value and leaves the body unread (e.g. a trailing newline). With an unread request body, net/http cannot finish the connection gracefully once the chunked event-stream response has started, so it tears the TCP connection down when the handler returns — racing the SDK client's in-flight read of the stream. invokeModelStream already reads the whole body via io.ReadAll, which is why only converse-stream flaked. Fix: io.Copy(io.Discard, r.Body) after decoding, before switching to the streamed response, so the connection is finished cleanly and the client reads a normal end-of-stream instead of a reset. Full bedrock suite green under -race (20x on the streaming tests); build, vet, go test ./..., and golangci-lint clean.
…#306) * feat(databricks): remaining Microsoft.Databricks ARM resources (#209) Finish the Microsoft.Databricks ARM control-plane surface beyond workspaces (built in #164), all reachable over the real armdatabricks SDK: - Access Connectors (accessConnectors): createOrUpdate, get, update, delete, list by resource group, list by subscription. System-assigned identities get synthesized principal/tenant IDs. - Private Endpoint Connections (workspaces/{w}/privateEndpointConnections): create, get, list, delete. - Private Link Resources (workspaces/{w}/privateLinkResources): get, list (synthesized databricks_ui_api / browser_authentication group set). - VNet Peering (workspaces/{w}/virtualNetworkPeerings): createOrUpdate, get, list, delete. - Outbound Network Dependencies Endpoints (workspaces/{w}/outboundNetworkDependenciesEndpoints): list (bare-array response, matching the SDK deserializer). - Operations (/providers/Microsoft.Databricks/operations): list — served via a subscription-less path special-case since azurearm.ParsePath requires a /subscriptions prefix. Built across all four layers: driver interface + types, in-memory provider Mock (memstore-backed, copy-on-write), portable service with the do() cross-cutting pipeline, and the SDK-compat ARM HTTP handler (routing extended for the new top-level type, workspace sub-resources, and the operations path). Modeled store-and-echo: the ARM resources round-trip faithfully over the SDK, but the underlying Azure networking side effects (real private endpoints, live VNet peering, outbound reachability) are not simulated — see docs/services.md. Tests: 21 SDK round-trip tests driving the real armdatabricks clients against an httptest server, plus 9 provider-level unit tests, covering happy paths and edge cases (not found, missing parent workspace, empty list, PATCH semantics, rejected PEC status, the no-subscription operations path). * fix(databricks): address review — case-insensitive routing, idempotent delete Addresses the re-review of #306: - Case-insensitive ARM routing (Medium): the handler matched the provider namespace and resource-type/sub-resource segments with ==, but ARM treats them case-insensitively (and the same file's isOperationsPath / the shared parseResourceGroup already use EqualFold). A lowercased path such as .../providers/microsoft.databricks/accessconnectors would 404. Now uses strings.EqualFold throughout Matches/ServeHTTP/serveWorkspaceChild. - Idempotent DELETE (Medium): DeleteAccessConnector / DeletePrivateEndpoint / DeleteVNetPeering returned 404 on a missing resource and 200 on success. Real ARM DELETE is idempotent — now 204 on success and 204 on a NotFound (matching the resourcegroups handler precedent), so teardown retries / delete-then-delete succeed. Pre-existing workspace delete (#164) left untouched. - Access-connector identity realism (Low): system-assigned principalId is now keyed on (resourceGroup, name) so same-named connectors in different RGs get distinct principals; tenantId is a single fixed emulator-wide directory GUID rather than a per-name synthesized value. - Docs (Low): §21 op-count total and the coverage-summary row bumped 52 -> 70 (this PR adds 18 driver operations). Tests: new SDK round-trip tests for case-insensitive routing (raw HTTP, since the SDK emits canonical casing), idempotent delete-of-missing across the three resources, and the PATCH-identity=None transition; all via the real armdatabricks clients. go build/vet/test/-race and golangci-lint (0 issues) green. * chore: re-trigger CI The prior CI run failed only on an unrelated, pre-existing flaky test: server/aws/bedrock TestSDKConverseStream ('use of closed network connection') — a load-sensitive httptest streaming-connection race from #298, not touched by this PR. All other checks pass and the test is green locally (30/30, incl. -race). No admin rights to re-run the job, so this empty commit re-triggers the pull_request workflow. * fix(bedrock): drain request body before streaming converse-stream response CI's `go test -race` intermittently failed TestSDKConverseStream with "use of closed network connection" — a pre-existing, load-sensitive flake (from #298) surfaced by the race detector's slowdown on the 2-core runner. Root cause: converseStream decodes the request via json.Decoder, which stops at the end of the JSON value and leaves the body unread (e.g. a trailing newline). With an unread request body, net/http cannot finish the connection gracefully once the chunked event-stream response has started, so it tears the TCP connection down when the handler returns — racing the SDK client's in-flight read of the stream. invokeModelStream already reads the whole body via io.ReadAll, which is why only converse-stream flaked. Fix: io.Copy(io.Discard, r.Body) after decoding, before switching to the streamed response, so the connection is finished cleanly and the client reads a normal end-of-stream instead of a reset. Full bedrock suite green under -race (20x on the streaming tests); build, vet, go test ./..., and golangci-lint clean.
Summary
Completes #214 — Full AWS Bedrock coverage end-to-end, so the real
aws-sdk-go-v2bedrock,bedrockruntime,bedrockagent, andbedrockagentruntimeclients all work against the in-memory backend over HTTP.Builds on the catalog+runtime (#160) and management (#207) work; adds the entire remaining surface across all layers (driver interface → in-memory provider → portable API → restJson1 SDK-compat handler), with unit tests and real-SDK roundtrip tests per subsystem.
What's added
Runtime (
bedrock-runtime)ConverseStream+InvokeModelWithResponseStreamovervnd.amazon.eventstream(framed with the AWS eventstream encoder)CountTokens,ApplyGuardrailStartAsyncInvoke/GetAsyncInvoke/ListAsyncInvokesControl plane (
bedrock)CreateGuardrailVersion, version-addressed Get/Delete/List)StopEvaluationJob)TagResource/UntagResource/ListTagsForResource(also fixes tags previously accepted-but-dropped on guardrail/provisioned create)Agents — new
bedrock-agent+bedrock-agent-runtimeservices (new SDK deps)InvokeAgent(eventstream),Retrieve,RetrieveAndGenerateRouting / registration
The two agent services register before the S3 catch-all. The
bedrock-agent-runtimehandler registers before thebedrock-agentcontrol plane and matches onlyPOSTon the runtime suffixes (/…/text,/…/retrieve,/retrieveAndGenerate), so the two never collide on the shared/agentsand/knowledgebasesroots.Testing
go build ./...,go vet,gofmt— cleangolangci-lint run— clean on all new codego test ./...— passing (whole repo)Notes
"This is a simulated response from …"); jobs complete synchronously in a terminal state, matching the existing repo convention for long-running resources.aws-sdk-go-v2/service/bedrockagentand.../bedrockagentruntimetogo.mod.🤖 Generated with Claude Code