Skip to content

[FEATURE] Native AWS Bedrock Guardrails support in ModelConfig #2320

Description

@GiGi-cloud-76

📋 Prerequisites

📝 Feature Summary

Add an optional guardrail block to the Bedrock provider in ModelConfig so the controller can pass guardrailIdentifier and guardrailVersion to the Converse API natively, without requiring an external proxy.

❓ Problem Statement / Motivation

The Bedrock provider in ModelConfig only exposes region today.

If you want to apply AWS Bedrock Guardrails (content filtering, topic denial, PII redaction) to your agents, you're forced to deploy a separate OpenAI-compatible proxy that injects the guardrail config at the gateway level. This adds an extra service to maintain and forces you to use provider: "OpenAI" with a custom baseUrl instead of the native Bedrock path.

The Bedrock Converse API already accepts guardrailIdentifier and guardrailVersion in the request body — kagent just doesn't expose them.

💡 Proposed Solution

Add an optional guardrail block to the bedrock spec in ModelConfig:

bedrock:
  region: "eu-central-1"
  guardrail:
    identifier: "abc123def456"  # guardrail ID or ARN
    version: "1"                 # guardrail version
    trace: "enabled"            # optional, defaults to disabled

The controller should pass these values as GuardrailConfiguration in the Converse API call (or as headers for InvokeModel). When a guardrail triggers INTERVENED, surface the blocked reason to the agent/user.

Ref: https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-use-converse-api.html

🔄 Alternatives Considered

  • External proxy (what we do today): works but adds operational complexity and breaks native Bedrock provider usage
  • Annotation-based config on the ModelConfig resource: not type-safe, hard to validate
  • Separate GuardrailPolicy CRD referenced by ModelConfig: over-engineered for what's essentially two fields

🎯 Affected Service(s)

Controller Service

📚 Additional Context

Tested on kagent v0.9.0 with provider: Bedrock. The BedrockConfig struct currently only has Region — no guardrail-related fields exist in the CRD.

The AWS SDK Go v2 already exposes the relevant types in github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types:

  • GuardrailConfiguration{GuardrailIdentifier, GuardrailVersion, Trace}

So it's mostly wiring: add the field to the CRD, read it in the controller, pass it to the SDK call.

We're running kagent in production on AWS and would be happy to validate any implementation against real Bedrock Guardrails in our environment.

🙋 Are you willing to contribute?

  • I am willing to submit a PR for this feature

Activity

  1. sanskar-singh-2403 commented on Jul 23, 2026

    @sanskar-singh-2403
    Contributor

    @dmitri-d @davidkarlsen

    Happy to pick this up. I traced the wiring path on main and it looks clean. Wanted to confirm the CRD shape before writing code so we don't churn on the public API.

    Where it plugs in today:

    • go/api/v1alpha2/modelconfig_types.go: BedrockConfig currently exposes Region, AdditionalModelRequestFields, PromptCaching, CacheTTL. This is where the new Guardrail field plus kubebuilder validation would go.
    • go/api/adk/types.go: the Bedrock transport struct mirrors those fields (region, additional_model_request_fields, prompt_caching, cache_ttl) and would need the matching guardrail field for the ADK JSON contract.
    • go/adk/pkg/models/bedrock.go: the runtime BedrockConfig and the actual m.Client.Converse(ctx, &bedrockruntime.ConverseInput{...}) call (plus the ConverseStream path). ConverseInput takes a GuardrailConfig *types.GuardrailConfiguration which isn't set today, so that's the one-line addition on the SDK side.

    So the flow is: add the field to the CRD, map it through the ADK types, build a types.GuardrailConfiguration{GuardrailIdentifier, GuardrailVersion, Trace} and pass it into both Converse and ConverseStream. Plus deepcopy/CRD manifest regen and tests.

    Before I start, a couple of API shape questions so this lands the way you'd want:

    1. Nested block vs flat fields. The proposed guardrail: {identifier, version, trace} sub-block reads cleanly and matches the existing style. Good to lock that in? I'd make the whole block optional, with identifier and version required within it via a CEL XValidation rule, consistent with the existing Bedrock validations in this file.
    2. trace. Expose it as an enum (enabled, enabled_full, disabled, default disabled) mapping to types.GuardrailTrace, rather than a free string? Keeps it type safe.
    3. INTERVENED handling. When a guardrail blocks, do you want the intervention reason surfaced as a normal assistant response so the agent sees it, or as an error/stopReason? I'd lean toward surfacing it in the response content with the block reason attached, but happy to follow whatever convention you prefer.
    4. Converse only, or InvokeModel too? The current Bedrock path is entirely Converse/ConverseStream, so I'd scope this to GuardrailConfig on those two and leave InvokeModel out unless there's a reason to add it.

    If the shape above looks right, assign it to me and I'll open a PR with the CRD change, ADK mapping, both Converse paths wired, regenerated manifests, and unit tests. I can also validate against real Bedrock Guardrails since @GiGi-cloud-76 offered a prod environment, which would be great for an integration check.

  2. GiGi-cloud-76 commented on Jul 23, 2026

    @GiGi-cloud-76
    Author

    Thanks for picking this up, the analysis looks spot on.

    On your questions:

    1. Nested block vs flat — Yes, guardrail: {identifier, version, trace} as an optional sub-block with identifier+version required within it is exactly what I had in mind. CEL validation consistent with the existing Bedrock rules makes sense.

    2. trace as enum — Agreed, typed enum is better than a free string. enabled, enabled_full, disabled (default disabled) maps cleanly to the SDK.

    3. INTERVENED handling — I'd go with surfacing it in the response content (not as an error). The agent should see why the guardrail blocked so it can adjust or inform the user. A hard error would break agent loops unnecessarily. If you can include the guardrail trace output when trace is enabled, even better for debugging.

    4. Converse only — Yes, scoping to Converse/ConverseStream is the right call. The current Bedrock path doesn't use InvokeModel, no reason to add it.

    Shape looks good to me. Happy to validate against real guardrails once you have a branch — we have multiple guardrail configurations in production (content filters, topic denial, PII redaction) so we can cover a few scenarios.

  3. added 4 commits that reference this issue on Jul 23, 2026
    66094cc
    ff7ec17
    932fbc4
    222c46b
  4. added a commit that references this issue on Jul 28, 2026
    3fb5a9d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions