Skip to content

feat: full AWS Bedrock coverage (#214) - #298

Merged
thzgajendra merged 6 commits into
stackshy:developmentfrom
Satyam-Trivedi-ZS:feat/214-full-bedrock-coverage
Jul 29, 2026
Merged

feat: full AWS Bedrock coverage (#214)#298
thzgajendra merged 6 commits into
stackshy:developmentfrom
Satyam-Trivedi-ZS:feat/214-full-bedrock-coverage

Conversation

@Satyam-Trivedi-ZS

Copy link
Copy Markdown
Contributor

Summary

Completes #214 — Full AWS Bedrock coverage end-to-end, so the real aws-sdk-go-v2 bedrock, bedrockruntime, bedrockagent, and bedrockagentruntime clients 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 + InvokeModelWithResponseStream over vnd.amazon.eventstream (framed with the AWS eventstream encoder)
  • CountTokens, ApplyGuardrail
  • Async invoke: StartAsyncInvoke / GetAsyncInvoke / ListAsyncInvokes

Control plane (bedrock)

  • Guardrail policy configs (topic / content / word / sensitive-info / contextual-grounding) + guardrail versions (CreateGuardrailVersion, version-addressed Get/Delete/List)
  • Model import jobs, model copy jobs, model evaluation jobs (+ StopEvaluationJob)
  • Inference profiles, prompt routers
  • Marketplace model endpoints (incl. Register/Deregister)
  • Foundation model agreements (Create/Delete/ListOffers/GetAvailability)
  • Automated reasoning policies (CRUD)
  • Tagging: TagResource / UntagResource / ListTagsForResource (also fixes tags previously accepted-but-dropped on guardrail/provisioned create)

Agents — new bedrock-agent + bedrock-agent-runtime services (new SDK deps)

  • Agents, knowledge bases, data sources, flows, prompts (CRUD + lifecycle: PrepareAgent / StartIngestionJob / PrepareFlow)
  • Runtime: InvokeAgent (eventstream), Retrieve, RetrieveAndGenerate

Routing / registration

The two agent services register before the S3 catch-all. The bedrock-agent-runtime handler registers before the bedrock-agent control plane and matches only POST on the runtime suffixes (/…/text, /…/retrieve, /retrieveAndGenerate), so the two never collide on the shared /agents and /knowledgebases roots.

Testing

  • go build ./..., go vet, gofmt — clean
  • golangci-lint run — clean on all new code
  • go test ./... — passing (whole repo)
  • Real-user end-to-end: booted the full cloudemu AWS server on a live socket and drove every subsystem with the genuine SDK clients (control plane, streaming, count-tokens on both union paths, apply-guardrail, async invoke, agent CRUD + lifecycle, InvokeAgent streaming / Retrieve / RetrieveAndGenerate) — all pass.

Notes

  • Emulated responses are deterministic simulations (e.g. "This is a simulated response from …"); jobs complete synchronously in a terminal state, matching the existing repo convention for long-running resources.
  • Adds aws-sdk-go-v2/service/bedrockagent and .../bedrockagentruntime to go.mod.

🤖 Generated with Claude Code

Satyam-Trivedi-ZS and others added 2 commits July 28, 2026 16:09
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>

@NitinKumar004 NitinKumar004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 bedrockbedrock-agent-runtimebedrock-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/eventstream encoder; 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 verify clean, tidy no-op. Factory wiring additive, no nil-panic. Tags-on-create fix safe (response bytes unchanged; ListTagsForResource is net-new). Layering/error-mapping/determinism clean.

🟠 MEDIUM

  1. Concurrent-map-write panic — HyperParameters returned by reference. providers/aws/bedrock/bedrock.go GetModelCustomizationJob/ListModelCustomizationJobs/GetCustomModel/ListCustomModels do result := *job — the copy's HyperParameters map aliases the stored map. Create copyMaps in but reads don't copy out. Concurrent read+mutate → fatal error: concurrent map read and map write under serve — same class as the LB Extra bug in #296. Fix: copyMap on return.
  2. S3 shadowing via unanchored prefixes. server/aws/bedrockagent/handler.go:84-87/knowledgebases, /flows, /prompts use HasPrefix with 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 named agents. Inconsistent with the sibling bedrock handler's underPrefix(p,pre)=p==pre||HasPrefix(p,pre+"/"). Fix: reuse underPrefix.
  3. Guardrail aliasing (no copy at all). management.go stores GuardrailPolicies: cfg.GuardrailPolicies and 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.
  4. guardrailRecord.versions slice mutated without a lock. guardrail_versions.go (append, nextVer++) and management.go (reslice on delete) mutate after the transient RLock is released; ListGuardrails ranges it concurrently → data race on the slice header (append can realloc). Compound mutation, beyond the accepted scalar-flip convention.
  5. Nondeterministic list ordering. New List* endpoints (jobs, inference profiles, prompt routers, AR policies, marketplace) use memstore.All() instead of the documented SortedValues() — violates the deterministic-ordering convention; flaky-test risk.
  6. Fidelity + tests bake in wrong behavior: StopEvaluationJob silently rewrites a terminal (Completed) job to Stopped (real AWS → ConflictException); DeregisterMarketplaceModelEndpoint is a success no-op leaving Status=REGISTERED. Both have tests asserting the incorrect behavior.
  7. No concurrency/-race tests anywhere in the new bedrock code — so #1/#3/#4 are entirely uncaught.

🟡 LOW

  • ApplyGuardrail ignores GuardrailVersion (a version-aware lookup exists but is unused).
  • []byte payloads (EvaluationConfig, InferenceConfig, PolicyDefinition) stored by direct alias — inconsistent with marketplace's copyBytes; slice fields (Models, modalities) returned by reference.
  • List* (async invoke etc.) ignore pagination/filters; streaming write-errors swallowed (no abort); ConverseStream omits contentBlockStart.
  • CountTokens returns InvalidArgument/400 for an unknown model while GetFoundationModel returns NotFound/404 for the same condition.
  • bedrock-agent: no parent-child delete cascade (orphaned data sources/aliases), lifecycle states (Preparing) and their FailedPrecondition paths are dead/unvalidated, json.RawMessage bodies aliased, fragile string EOF check (vs errors.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>
@Satyam-Trivedi-ZS

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough, blast-radius-focused review — really helpful. I verified every point against the code and pushed fixes in 68a5b5c. Point by point:

🟠 MEDIUM

1. HyperParameters returned by reference (concurrent-map-write). Verified the aliasing is real, but the panic scenario isn't reproducible on the current code: the map is copyMap-ed in at create and never mutated post-create (a duplicate create returns AlreadyExists rather than overwriting), and these methods are pre-existing (present at the merge base). Hardened anyway — added copyMap on the Get*/List* return paths so the alias is gone, and added a -race test (below) so the whole class is covered.

2. S3 shadowing via unanchored prefixes. ✅ Fixed. Added underPrefix(p, pre) to the bedrock-agent handler and anchored /knowledgebases, /flows, /prompts in both Matches and ServeHTTP (/agents was already anchored). Added a test asserting /flows-prod, /promptsdb, /knowledgebases-archive fall through to S3 while the documented item/nested shapes still route.

3. Guardrail aliasing. ✅ Fixed. Added deepCopyGuardrailPolicies (fresh pointers + cloned slices, incl. nested Examples) applied on Create, Update, and the version snapshot. A version is now immutable against later draft edits — covered by a new test and confirmed in the e2e (v1 stays VIOLENCE after the draft is edited to HATE).

4. guardrailRecord.versions slice race. ✅ Fixed. Added a sync.RWMutex to guardrailRecord guarding draft/versions/nextVer; readers copy under the lock. go vet is copylocks-clean.

5. Nondeterministic list ordering. ✅ Fixed. Switched all eight new List* (async invoke, import/copy/eval jobs, inference profiles, prompt routers, AR policies, marketplace) from All() to SortedValues(), matching the store.go directive and the sibling bedrock-agent handlers.

6. Fidelity — StopEvaluationJob / DeregisterMarketplaceModelEndpoint.

  • StopEvaluationJob flipping a terminal job: this is the repo-wide synchronous-completion convention — providers/aws/sagemaker/jobs.go does the identical unconditional flip. I kept bedrock consistent rather than adding a one-off ConflictException guard; happy to do a cross-provider terminal-state guard as a follow-up if you'd prefer. (I did fix the in-place-mutation data race in Stop via copy-on-write.)
  • DeregisterMarketplaceModelEndpoint no-op: the SDK types.Status enum only has REGISTERED/INCOMPATIBLE_ENDPOINT (no "deregistered" value), and real AWS leaves the underlying SageMaker endpoint describable/deletable after deregister — so removing the record would mismodel it and break the register→deregister→delete flow. Kept as a validated no-op and tightened the doc comment to note the limitation.

7. No -race/concurrency tests. ✅ Fixed. Added concurrency tests that hammer create + mutate + read from N goroutines for guardrails and evaluation jobs. They earned their keep — they surfaced an additional in-place-mutation race in StopEvaluationJob (and the same pattern in marketplace Update/Register and AR-policy Update), which I fixed with copy-on-write. go test -race ./providers/aws/bedrock/... ./server/aws/bedrock/... is clean.

🟡 LOW

  • []byte/json.RawMessage aliasing → ✅ Fixed: copyBytes on Evaluation/Inference/PolicyDefinition, copyRaw on the bedrock-agent configs. (Models/modalities were already copied.)
  • Dead *Preparing constants → ✅ removed. Fragile err.Error()=="EOF" → ✅ now errors.Is(err, io.EOF).
  • Duplicate-create → ✅ Fixed for marketplace endpoints (was a silent overwrite) and prompt routers (name conflict → AlreadyExists). Left inference profiles as-is since real Bedrock allows duplicate profile names (distinct ARNs).
  • ApplyGuardrail ignores version → kept: emulator output is version-invariant (always NONE/echo), so resolving the version adds no behavioral fidelity.
  • CountTokens 400 vs GetFoundationModel 404 → intentional and AWS-correct: runtime ops (InvokeModel/Converse/CountTokens) return ValidationException/400 for a bad modelId; the catalog lookup returns ResourceNotFoundException/404. CountTokens already matches its data-plane siblings.
  • Pagination / swallowed stream write-errors / omitted contentBlockStart → left as emulator simplifications (headers are already flushed before streaming; only text blocks are produced). Can add if you'd like uniformity.
  • bedrock-agent cascade delete → left: orphaned children are unreachable (reads re-check the parent), and real AWS rejects rather than cascades; happy to add a FailedPrecondition-on-non-empty check as a follow-up if preferred.

Verification

go build/go vet/gofmt/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 (control plane + runtime + streaming + async + agents CP/runtime + both typed-error paths) all passing.

@NitinKumar004 NitinKumar004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review @ 68a5b5c — all MEDIUMs fixed; a few fidelity items remain

Verified each fix directly against the code.

✅ Fixed & verified

  • HyperParameters concurrent-map-write paniccopyMap now on every read path (GetModelCustomizationJob/List, GetCustomModel/List). The block-on item is resolved.
  • S3 shadowingMatches now anchors all four roots via underPrefix(p, pre), so /flows-prod, /promptsdb, /knowledgebases-archive fall through to S3; test added.
  • Guardrail aliasing + racedeepCopyGuardrailPolicies on create/update and on version snapshot (snapshots are now truly immutable); guardrailRecord guarded by a sync.RWMutex with reads copying under the lock.
  • List orderingSortedValues() across the new List* endpoints (deterministic).
  • Copy disciplinecopyBytes on Evaluation/Inference/PolicyDefinition []byte; copyRaw on bedrock-agent json.RawMessage.
  • Duplicate-create — marketplace / prompt-router / inference-profile now return AlreadyExists.
  • bedrock-agent — decode uses errors.Is(io.EOF); dead *Preparing constants removed.
  • New -race concurrency tests (guardrail + eval job). Nice.

🟡 Remaining (LOW / fidelity — only the concurrency aspect was fixed)

  1. StopEvaluationJob still rewrites a terminal job. jobs.go now does copy-on-write (good, fixes the race) but still unconditionally sets Status=JobStopped. Since eval jobs complete synchronously to Completed, Stop can only ever hit a terminal job — real AWS returns ConflictException. The tests still assert success (asyncinvoke_jobs_test.go:157, sdk_roundtrip_asyncjobs_test.go:261), so they bake in the non-AWS behavior.
  2. DeregisterMarketplaceModelEndpoint still a no-op — validates existence, returns nil; Status stays REGISTERED after deregister.
  3. ApplyGuardrail still ignores GuardrailVersion (counttokens_applyguardrail.go:40 only findGuardrailRecord(identifier)); an unknown version succeeds with Action=NONE.
  4. Slice-field read-aliasing. GetInferenceProfile/GetPromptRouter/GetAutomatedReasoningPolicy return result := *p whose .Models slice 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.
  5. Prior LOWs out of this commit's scope still stand: streaming write-error swallowing (no abort), List* pagination gaps, CountTokens vs GetFoundationModel returning 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 NitinKumar004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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's do() is the canonical storage/ pattern (error-injection → rate-limit → latency → call → metrics → recorder), same With* options. All three services carry the full recorder/metrics/limiter/injector/latency set.
  • internal/memstore.Store[V] backing (*memstore.Store[*driver.CustomizationJob] …), sync.RWMutex discipline (guardrail-record mutex added in the fix), copy-on-read/write applied.
  • ARNs via idgen — 33 idgen.* calls, zero hand-built arn:aws: strings.
  • cerrors → typed restJson1 errors (X-Amzn-Errortype), server/wire/* helpers, and the official aws/protocol/eventstream encoder for streaming.
  • Registration most-specific-first (agent-runtime → agent-control → S3) with underPrefix anchoring 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)

  1. Missing portable-layer test for services/bedrockagent. services/bedrock and services/bedrockagentruntime each have a _test.go; services/bedrockagent has none (convention: portable API test per service).
  2. 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).
  3. No SetMonitoring/CloudWatch-metrics wiring. Real Bedrock emits Invocations/InvocationLatency; the mock doesn't push auto-metrics like S3/EC2/Lambda/SQS. Optional, low.
  4. Docs likely name only bedrock/bedrock-runtime, not the two new bedrock-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 → copyMap on all read paths.
  • S3 route shadowing → underPrefix anchoring.

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>
@Satyam-Trivedi-ZS

Copy link
Copy Markdown
Contributor Author

Thanks for the re-review and the architecture pass — pushed ce48796 addressing the remaining fidelity items and the pattern gaps. I verified each against the code and re-ran the full suite (go test ./... 174 pkgs, -race on the bedrock packages, and a 107-op exhaustive real-SDK e2e that asserts each changed behavior).

Remaining fidelity items (from the re-review)

  1. StopEvaluationJob on a terminal job — fixed properly. Evaluation is genuinely long-running (unlike import/copy, which produce an artifact synchronously and have no Stop op), so CreateEvaluationJob now records the job InProgress. StopEvaluationJob transitions it to Stopped, and rejects a job that is no longer in progress with FailedPrecondition — which the bedrock error mapper now surfaces as ConflictException (409), matching AWS. Tests updated to assert InProgress on create and a ConflictException on a second Stop.
  2. DeregisterMarketplaceModelEndpoint no-op — fixed. Deregister now removes the Bedrock registration record, so a subsequent Get/Delete returns ResourceNotFoundException (the underlying, unmodeled SageMaker endpoint is unaffected) — matching AWS rather than leaving Status=REGISTERED. The roundtrip/unit tests were restructured accordingly (deregister is terminal; Delete is exercised on a fresh endpoint).
  3. ApplyGuardrail ignores version — fixed. A requested numbered version is now validated (ResourceNotFoundException if it doesn't exist); ""/"DRAFT" resolve to the working draft.
  4. Slice-field read-aliasing — fixed. GetInferenceProfile/GetPromptRouter/GetAutomatedReasoningPolicy (and their List) now copy the Models slice / PolicyDefinition bytes out on read via clone helpers, closing the same class as the map fix.
  5. Other standing LOWsCountTokens 400 vs GetFoundationModel 404 is intentional/AWS-correct (runtime vs catalog), and List* pagination / stream write-error abort / contentBlockStart / bedrock-agent cascade-delete remain deliberate emulator simplifications; happy to pick any up if you'd like.

Architecture review

  • Missing services/bedrockagent portable test — added (bedrockagent_test.go, mirroring the runtime one).
  • Docsdocs/services.md now names bedrock-agent (+ bedrock-agent-runtime) in the service table and the operation-count table reflects the full surface. (docs/sdk-server.md already covered them.)
  • AWS-only + no SetMonitoring — acknowledged as documented/optional; left as-is.

Appreciate the careful passes — this made the PR meaningfully better.

@thzgajendra thzgajendra left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.NewEncoder with correct :message-type/:event-type/:content-type headers, 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 typed ResourceNotFoundException.
  • Deps clean, roundtrip tests genuine (typed assertions), and there's a -race-targeted concurrency test.

Substantive (MEDIUM — fidelity/consistency, none blocking)

  1. bedrock-agent has no versions/aliases model. PrepareAgent only flips status→PREPARED; there's no AgentVersion type, CreateAgentAlias takes no version, and Agent.Version stays "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).
  2. KB delete doesn't cascadeproviders/aws/bedrockagent/knowledgebases.go:100 DeleteKnowledgeBase removes only the KB entry; data sources (keyed by KnowledgeBaseID) and ingestion jobs are left orphaned in the store. Real data-consistency gap.
  3. Copy-out immutability gap — several Get*/List* return shallow result := *x (e.g. providers/aws/bedrockagent/datasources.go:42,54; also bedrock marketplace-endpoint + eval-job byte fields) where json.RawMessage/[]byte/slice fields still alias the store. copyRaw/copyBytes are 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.
  4. Error taxonomy incompleteserver/aws/bedrock/errors.go:29 writeErr doesn't map PermissionDenied→403 AccessDeniedException or ResourceExhaustedServiceQuotaExceededException; they fall to the default (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=exception path (and none tested).
  • ListAsyncInvokes (and import/copy/eval lists) ignore filters + pagination (NextToken always empty); no ClientRequestToken idempotency.
  • 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>
@Satyam-Trivedi-ZS

Copy link
Copy Markdown
Contributor Author

Thanks @thzgajendra — excellent, careful review (and the note ruling out the findGuardrailRecord race is spot on: the loop reads only the write-once ID/ARN, and UpdateGuardrail writes other fields under the record lock without reassigning rec.draft). Pushed a842411 addressing the MEDIUM items; verified each against the code + re-ran the full suite (go test ./... 204 pkgs, -race on the bedrock packages, and an exhaustive real-SDK e2e).

MEDIUM

  1. bedrock-agent versions/aliases/action-groups — confirmed intentionally out of scope for this iteration, and documented it: docs/sdk-server.md now states the coverage is the core resource lifecycle (agents, KBs, data sources, flows, prompts) + runtime (InvokeAgent/Retrieve/RetrieveAndGenerate), and that agent versioning/aliases beyond basic create, action groups, and collaborators are out of scope. PrepareAgent flips status→PREPARED as the deliberate simplification.
  2. KB delete cascade — fixed. DeleteKnowledgeBase now cascades to its data sources and ingestion jobs; DeleteDataSource removes its ingestion jobs; DeleteAgent removes its aliases. No orphans. Tests added.
  3. Copy-out immutability — fixed. Get/List (and Update/Create-return) now clone json.RawMessage/[]byte fields out via helpers, applied to bedrock-agent (KB/DS/flow/prompt configs) and bedrock (marketplace endpointConfig, evaluation-job evaluationConfig/inferenceConfig) — consistent with the guardrail/AR-policy clone-out paths. Tests assert a mutated return doesn't corrupt the store.
  4. Error taxonomy — fixed in all three writeErr maps (bedrock, bedrock-agent, bedrock-agent-runtime): PermissionDenied → 403 AccessDeniedException, ResourceExhausted → 400 ServiceQuotaExceededException.

Minor

  • CI -race — good catch: CI ran go test ./... without -race, so the concurrency tests were inert there. Added a scoped go test -race CI step for the Bedrock packages (enabling -race repo-wide risks surfacing pre-existing races outside this PR, so I kept it scoped).
  • Docs caveats — added an "Emulation caveats" note (long-running jobs complete synchronously; inference/agent responses are deterministic simulations).
  • In-stream exception frame — results are computed before the 200/eventstream opens, so errors surface pre-stream with the correct typed error; a mid-stream :message-type=exception path isn't modeled (noted as a known simplification).
  • List pagination/filters + ClientRequestToken idempotency — deliberate simplification for small in-memory datasets; happy to add if you'd like uniformity.

Appreciate the depth here — the cascade + copy-out fixes in particular are real improvements.

@thzgajendra thzgajendra left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed at a842411every item from my review is resolved. Verified against source, not just the commit message.

  • KB cascade delete (MEDIUM #2) — fixed. DeleteKnowledgeBase now calls deleteDataSourcesForKnowledgeBase + deleteJobsForKnowledgeBase; DeleteDataSource removes its jobs; DeleteAgent removes its aliases. No orphaned children. Covered by TestDeleteKnowledgeBaseCascade, TestDeleteDataSourceCascadesJobs, TestDeleteAgentCascadesAliases.
  • Copy-out immutability (MEDIUM #3) — fixed. Get/List/Update/Create now clone the json.RawMessage/[]byte fields out via cloneDataSource/copyRaw (and the equivalents for KB/flow/prompt/marketplace/eval-job) — the shallow result := *x is gone, consistent with the guardrail/AR-policy paths. TestDataSourceCopyOutImmutable asserts a caller can't corrupt stored state.
  • Error taxonomy (MEDIUM #4) — fixed. writeErr (all three services) now maps PermissionDenied → 403 AccessDeniedException and ResourceExhausted → 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 scoped go test -race step 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 NitinKumar004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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) → copyMap on all read paths (-race clean).
  • S3 route shadowingunderPrefix anchoring.
  • StopEvaluationJob returns FailedPrecondition for a non-InProgress job (real-AWS ConflictException semantics), copy-on-write.
  • ApplyGuardrail validates the numbered GuardrailVersion via versionSnapshot ("" / "DRAFT" → draft).
  • Slice read-aliasingcloneInferenceProfile/clonePromptRouter/cloneARPolicy copy Models on 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/DeletePrompt correctly don't cascade (no child stores modeled — confirmed).
  • Error taxonomyPermissionDenied → 403 AccessDeniedException and ResourceExhausted → ServiceQuotaExceededException across all three handlers.

Remaining (LOW — fidelity only)

  1. Deregister and Delete marketplace endpoint are now byte-identical (both HasDelete). The no-op is fixed, but it swung the other way — two distinct AWS ops are collapsed, so after Deregister a Get returns NotFound; real AWS keeps the endpoint in a deregistered state and only Delete removes it. Cosmetic unless a caller models the two-step lifecycle.
  2. StopEvaluationJob happy-path is unreachable — jobs complete synchronously to a terminal state, so Stop always 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 NitinKumar004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 solidDeleteKnowledgeBase→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 + -race test correct.
  • Error taxonomy correct (incl. PermissionDenied→403, ResourceExhausted→400 ServiceQuotaExceeded); CountTokens ValidationException vs GetFoundationModel ResourceNotFound divergence is intentional and matches real AWS; tagging defensive-copies.
  • Eventstream framing (official SDK encoder), 200-header-before-driver-call ordering, MaxBytesReader body bounds, length-guarded path parsing, errors.Is(io.EOF) — all correct. idgen ARNs, 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 whose InputModalities/OutputModalities/etc. alias the seed — and seedFoundationModels reuses the same backing slices across models, so a caller mutating one model's modalities corrupts every model's. Read-only today; latent. Fix: a cloneFoundationModel copying the four slices.
  • LoggingConfig pointer aliasing (NEW). Put/GetModelInvocationLoggingConfiguration (management.go:311,331) share the *S3LoggingConfig/*CloudWatchLoggingConfig (and nested LargeDataDeliveryS3) pointers with the caller and the store; logMu guards only the pointer swap. Deep-copy on store + read.
  • CreateInferenceProfile (and CreateAutomatedReasoningPolicy) lack a duplicate-name guard — inconsistent with CreatePromptRouter/provisioned-throughput which return AlreadyExists; real Bedrock uses ConflictException.
  • UpdateGuardrail rename can clobber a different guardrail (management.go:127): Delete(old)+Set(new) with no collision check silently overwrites an existing new.
  • RegisterMarketplaceModelEndpoint semantic inversion (marketplace_agreements.go:122): requires the endpoint to already exist (else NotFound); real AWS registers an externally-created SageMaker endpoint. Redundant as written.
  • CreateFoundationModelAgreement accepts any modelId (marketplace_agreements.go:156) with no catalog check → later GetFoundationModelAvailability reports AVAILABLE for a nonexistent model.
  • Copy/import job target ARN uses non-standard resource types (model-copy-target/…, imported-model/<name>) rather than a custom-model/… ARN. Cosmetic (ARNs still via idgen).
  • CountTokens union laxity: both members present → InvokeModel silently wins; neither → returns 0 instead of ValidationException.
  • bedrockagent silently drops tagsAgentConfig.Tags/KnowledgeBaseConfig.Tags are accepted but there's no tag store / TagResource for 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/DeletePrompt guard can exist. StartAsyncInvoke doesn't dedupe on ClientRequestToken. Streaming ignores r.Context() on client disconnect (bounded event count → safe). Marketplace/AR-policy copy-on-write mutators lack a dedicated -race test.

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>
@Satyam-Trivedi-ZS

Copy link
Copy Markdown
Contributor Author

Thanks @NitinKumar004 — the deep multi-agent pass was really valuable, especially catching the UTF-8 streaming bug. Pushed 44715f2; verified each item in code and re-ran the full suite (go test ./... 204 pkgs, -race on the bedrock/agent packages, multi-byte streaming roundtrip).

🟠 MEDIUM

  1. ConverseStream UTF-8 corruption — fixed. chunkText now advances the split to a rune boundary (utf8.RuneStart), so neither delta can contain a truncated rune. Added TestSDKConverseStreamMultibyteRuneBoundary (a café ☕ 日本語 🎉 naïve résumé-based completion whose byte-midpoint lands mid-rune) asserting the reassembled stream is valid UTF-8, U+FFFD-free, and byte-identical to non-streaming Converse. Great catch — the ASCII-only test genuinely missed it.
  2. Nondeterministic List* — fixed. ListModelCustomizationJobs, ListCustomModels, ListGuardrails (no-identifier), ListProvisionedModelThroughputs now use SortedValues(), matching the memstore contract and the other 9 lists.
  3. Bucket-name shadowing — documented on the bedrock-agent handler. An exact bucket name (agents/flows/prompts/knowledgebases) under path-style addressing is claimed before the S3 catch-all — the same inherent REST-vs-catch-all tradeoff as /custom-models and EKS /clusters. A robust shape guard isn't feasible (GET /prompts is genuinely ambiguous between ListPrompts and ListObjects on a bucket named prompts), so I added the doc note + the virtual-host-addressing workaround rather than a false guard.

🟡 LOW — fixed

  • FoundationModel slice aliasingcloneFoundationModel copies the modality/customization/inference slices out (the shared-seed-slice angle made this worth doing).
  • LoggingConfig pointer aliasing — deep-copied on store + read.
  • Duplicate-name guardCreateInferenceProfile + CreateAutomatedReasoningPolicy now return AlreadyExists (consistent with prompt routers / provisioned throughput). (Reversing my earlier "allow dups" note in favour of package consistency.)
  • UpdateGuardrail rename clobber — now errors instead of overwriting an existing guardrail with the target name.
  • RegisterMarketplaceModelEndpoint inversion — now upserts (registers an externally-created endpoint) instead of requiring pre-existence.
  • CreateFoundationModelAgreement — validates the modelId against the catalog, so GetFoundationModelAvailability can't report AVAILABLE for a nonexistent model.
  • CountTokens empty union — neither member → ValidationException.
  • Marketplace/AR-policy -race tests — added.

Notes / reply-only

  • Deregister == Delete — acknowledged. The SDK Status enum only has REGISTERED/INCOMPATIBLE_ENDPOINT (no "deregistered"), so a distinct deregistered-but-present state isn't representable; both remove the record. Kept as a documented simplification.
  • "StopEvaluationJob happy-path unreachable" — this was resolved earlier in ce48796: evaluation jobs are now created InProgress (they're long-running, unlike import/copy), so Stop legitimately transitions InProgress → Stopped, and only a second Stop hits a terminal job → ConflictException.
  • Cosmetic ARNs (imported-model/… is actually the real AWS type; model-copy-target/…), agent-resource tagging, and action groups / flow-prompt-agent versions & aliases remain out of scope for this iteration (documented). StartAsyncInvoke ClientRequestToken dedup and mid-stream context cancellation are likewise deliberate simplifications (event counts are bounded).

Appreciate the thoroughness — the UTF-8 fix in particular is a real correctness improvement.

@Satyam-Trivedi-ZS

Copy link
Copy Markdown
Contributor Author

✅ All review rounds resolved — full-flow e2e verified at 44715f2

Thanks everyone for the thorough multi-pass review. Below is a consolidated record of every fix across the rounds, followed by the final end-to-end verification.

Concurrency / immutability

  • HyperParameters concurrent-map-write → copyMap on all read paths (+ -race test).
  • Guardrail policies deep-copied on create/update/version-snapshot; guardrailRecord guarded by sync.RWMutex; versions are immutable snapshots.
  • Copy-on-write for in-place mutators (StopEvaluationJob, marketplace Update/Register, AR-policy Update).
  • Copy-out on read for []byte/json.RawMessage/slice fields everywhere: registries (Models), marketplace (endpointConfig), eval job (evaluation/inferenceConfig), bedrock-agent configs, FoundationModel modality slices (shared-seed), and LoggingConfig nested pointers.
  • -race tests for guardrail, eval-job, marketplace, and AR-policy mutators; CI now runs a scoped go test -race on the Bedrock packages.

Correctness

  • ConverseStream UTF-8: chunkText now splits on a rune boundary (was a byte offset that corrupted multi-byte responses into U+FFFD). Multi-byte streaming roundtrip test added.
  • Nondeterministic lists: all List* use SortedValues() per the memstore contract.
  • S3 route shadowing: underPrefix anchoring; bucket-name overlap documented (inherent REST-vs-catch-all tradeoff).

Fidelity

  • StopEvaluationJob: eval jobs start InProgress → Stop works → terminal Stop returns ConflictException.
  • DeregisterMarketplaceModelEndpoint removes the registration (Get→NotFound).
  • ApplyGuardrail validates the numbered GuardrailVersion.
  • Duplicate-name guards → AlreadyExists on inference profiles, prompt routers, AR policies, marketplace endpoints.
  • UpdateGuardrail rename no longer clobbers an existing guardrail.
  • RegisterMarketplaceModelEndpoint upserts an externally-created endpoint.
  • CreateFoundationModelAgreement validates the modelId against the catalog.
  • CountTokens with no union member → ValidationException.
  • Error taxonomy: PermissionDenied → 403 AccessDeniedException, ResourceExhausted → 400 ServiceQuotaExceededException across all three handlers.
  • bedrock-agent parent→child cascade delete (KB→data sources+ingestion jobs, DS→jobs, agent→aliases).

Pattern / docs

  • Added services/bedrockagent portable-layer test; errors.Is(io.EOF); removed dead constants.
  • Docs: services.md + sdk-server.md list bedrock-agent(+runtime); scope note (agent versioning/aliases-beyond-create, action groups, collaborators out of scope) + emulation caveats (synchronous jobs, simulated inference).

Final full-flow e2e (as a user, real SDK over a socket)

Booted the complete server (awsserver.New(awsserver.DriversFrom(cloudemu.NewAWS()))) on a real TCP listener and drove all four real aws-sdk-go-v2 clients (bedrock, bedrockruntime, bedrockagent, bedrockagentruntime) through 113 checks — every operation plus explicit regression assertions for each fix above (multi-byte UTF-8 streaming == non-stream, deterministic list order, all dup-name conflicts, Register upsert, FM-agreement catalog check, UpdateGuardrail rename conflict, guardrail version immutability, eval InProgress→Stop→terminal-conflict, marketplace deregister→NotFound, KB cascade delete). All 113 passed on the first run, no product bugs.

Gate: go build/go vet/gofmt/golangci-lint clean; go test ./... — 0 failures; go test -race on the Bedrock/agent packages — clean. HEAD 44715f2.

@NitinKumar004 NitinKumar004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 corruptionchunkText now advances the split to the next utf8.RuneStart; the all-one-multibyte-rune edge returns a single intact chunk (traced "é"/"😀"). New TestSDKConverseStreamMultibyteRuneBoundary asserts valid UTF-8, no U+FFFD, and equality to non-streaming Converse. The reproducible bug is gone.
  • List determinismListModelCustomizationJobs/ListCustomModels/ListGuardrails(no-id)/ListProvisionedModelThroughputs now use SortedValues().
  • FoundationModel aliasingcloneFoundationModel deep-copies all 4 slices, used by both Get and List (load-bearing: the seed shares backing slices across models).
  • LoggingConfig aliasingdeepCopyLoggingConfig copies S3, CloudWatch, and nested CloudWatch.LargeDataDeliveryS3, applied on both store and read; TestModelInvocationLoggingCopyOut verifies both directions.
  • Fidelity/consistencyCreateInferenceProfile/CreateAutomatedReasoningPolicy dup-name → AlreadyExists; UpdateGuardrail rename guard correctly allows self-rename (newName != oldName short-circuit) and errors only on a real collision; RegisterMarketplaceModelEndpoint upserts and preserves CreatedAt on re-register; CreateFoundationModelAgreement validates modelId against the catalog (seeded models still pass, ordering preserved); CountTokens neither-member → ValidationException while single-member still works.
  • Docs — bucket-name-shadowing tradeoff documented on the bedrock-agent handler.
  • New -race tests 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)

  1. clonePromptRouter misses the ResponseQualityDifference *float64 pointer (registries.go:322). The clone reallocates Models but keeps the stored pointer, so GetPromptRouter/ListPromptRouters hand 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 }.
  2. 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 (PolicyDefinition bytes, :228), CreateModelCustomizationJob (HyperParameters map, 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 -race tests; InferenceProfile/PromptRouter have no -race test at all.
  3. Benign (note-only): async-invoke / import-job / cloneEvaluationJob omit some []string/tag fields, but those are never populated in the synchronous mock — no live leak, worth a one-line hardening comment. Dup-name SortedValues-scan-then-Set is 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 NitinKumar004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 use SortedValues().
  • AliasingcloneFoundationModel (all 4 slices) and deepCopyLoggingConfig (nested S3/CloudWatch pointers) on both store and read; new -race tests run mutators concurrently with Get and List.
  • Fidelity — dup-name guards, UpdateGuardrail self-rename correctly allowed / collision rejected, marketplace upsert preserving CreatedAt, CreateFoundationModelAgreement catalog validation, CountTokens neither-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.

@thzgajendra
thzgajendra merged commit cf99a74 into stackshy:development Jul 29, 2026
11 checks passed
Satyam-Trivedi-ZS added a commit to Satyam-Trivedi-ZS/cloudemu that referenced this pull request Aug 3, 2026
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.
Satyam-Trivedi-ZS added a commit to Satyam-Trivedi-ZS/cloudemu that referenced this pull request Aug 3, 2026
…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.
thzgajendra pushed a commit that referenced this pull request Aug 5, 2026
…#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants