Skip to content

Commit 4eb0454

Browse files
committed
Add InvokeGuardrailChecks (inline checks) mode to the policy backend
Lets the policy backend evaluate tool-call context with inline guardrail checks (content filter, prompt attack, sensitive information) so no pre-created Bedrock guardrail is required, streamlining onboarding (design decision #12). - Bumps AWSSDK.BedrockRuntime to 4.0.101.1 (adds InvokeGuardrailChecks). - GuardrailChecksOptions: categories/entities to check plus severity/confidence thresholds. BedrockGuardrailsPolicyOptions.InlineChecks selects the mode. - BedrockGuardrailClient.InvokeChecksAsync builds/times the request; GuardrailResponseMapper.ChecksTripped maps per-check scores to a deny (fail-safe: a finding with no score is treated as tripped). - The backend uses ApplyGuardrail when GuardrailId is set, else inline checks; ctor and setup validation accept either. Detection only (no masking), so PII sanitization stays on ApplyGuardrail. Tests cover mode selection, allow/deny by threshold, and ctor validation.
1 parent caaab80 commit 4eb0454

8 files changed

Lines changed: 334 additions & 23 deletions

src/AWS.Bedrock.MAG/AWS.Bedrock.MAG.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
</PropertyGroup>
3030

3131
<ItemGroup>
32-
<PackageReference Include="AWSSDK.BedrockRuntime" Version="4.0.17.5" />
32+
<PackageReference Include="AWSSDK.BedrockRuntime" Version="4.0.101.1" />
3333
<PackageReference Include="AWS.Logger.Core" Version="4.0.3" />
3434
<PackageReference Include="AWSSDK.CloudWatch" Version="4.0.103.2" />
3535
<PackageReference Include="Microsoft.AgentGovernance" Version="5.0.0" />

src/AWS.Bedrock.MAG/Internal/BedrockGuardrailClient.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Diagnostics;
7+
using System.Linq;
78
using System.Threading;
89
using System.Threading.Tasks;
910
using Amazon.BedrockRuntime;
@@ -56,8 +57,71 @@ public async Task<GuardrailInvocation> ApplyAsync(
5657

5758
return new GuardrailInvocation(response, evaluationMs);
5859
}
60+
61+
/// <summary>
62+
/// Runs inline guardrail checks (InvokeGuardrailChecks) over a single block of text and returns the
63+
/// response plus the elapsed time. No pre-created guardrail is required.
64+
/// </summary>
65+
public async Task<GuardrailChecksInvocation> InvokeChecksAsync(
66+
GuardrailChecksOptions checks,
67+
string text,
68+
CancellationToken cancellationToken = default)
69+
{
70+
var config = new GuardrailChecksConfig();
71+
if (checks.ContentFilterCategories.Count > 0)
72+
{
73+
config.ContentFilter = new GuardrailChecksContentFilterConfig
74+
{
75+
Categories = checks.ContentFilterCategories
76+
.Select(c => new GuardrailChecksContentFilterCategoryConfig { Category = c }).ToList()
77+
};
78+
}
79+
80+
if (checks.PromptAttackCategories.Count > 0)
81+
{
82+
config.PromptAttack = new GuardrailChecksPromptAttackConfig
83+
{
84+
Categories = checks.PromptAttackCategories
85+
.Select(c => new GuardrailChecksPromptAttackCategoryConfig { Category = c }).ToList()
86+
};
87+
}
88+
89+
if (checks.SensitiveInformationEntities.Count > 0)
90+
{
91+
config.SensitiveInformation = new GuardrailChecksSensitiveInformationConfig
92+
{
93+
Entities = checks.SensitiveInformationEntities
94+
.Select(e => new GuardrailChecksSensitiveInformationEntityConfig { Type = e }).ToList()
95+
};
96+
}
97+
98+
var request = new InvokeGuardrailChecksRequest
99+
{
100+
Checks = config,
101+
Messages = new List<GuardrailChecksMessage>
102+
{
103+
new GuardrailChecksMessage
104+
{
105+
Role = "user",
106+
Content = new List<GuardrailChecksContentBlock>
107+
{
108+
new GuardrailChecksContentBlock { Text = text }
109+
}
110+
}
111+
}
112+
};
113+
114+
var start = Stopwatch.GetTimestamp();
115+
var response = await _client.InvokeGuardrailChecksAsync(request, cancellationToken).ConfigureAwait(false);
116+
var evaluationMs = Stopwatch.GetElapsedTime(start).TotalMilliseconds;
117+
118+
return new GuardrailChecksInvocation(response, evaluationMs);
119+
}
59120
}
60121

61122
/// <summary>A guardrail response paired with how long the round-trip took, in milliseconds.</summary>
62123
internal readonly record struct GuardrailInvocation(ApplyGuardrailResponse Response, double EvaluationMs);
124+
125+
/// <summary>An inline-checks response paired with how long the round-trip took, in milliseconds.</summary>
126+
internal readonly record struct GuardrailChecksInvocation(InvokeGuardrailChecksResponse Response, double EvaluationMs);
63127
}

src/AWS.Bedrock.MAG/Internal/GuardrailResponseMapper.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,51 @@ public static string SummarizeAssessment(ApplyGuardrailResponse response)
5757
? $"action={action}"
5858
: $"action={action}; pii=[{string.Join(",", pii)}]";
5959
}
60+
61+
/// <summary>
62+
/// Evaluates an inline-checks (InvokeGuardrailChecks) response. A content-filter or prompt-attack
63+
/// finding trips when its severity meets <paramref name="severityThreshold"/>; a PII finding trips
64+
/// when its confidence meets <paramref name="confidenceThreshold"/>. A finding with no score is
65+
/// treated as tripped (fail-safe). Returns true when any check tripped, with a summary of which.
66+
/// </summary>
67+
public static bool ChecksTripped(
68+
InvokeGuardrailChecksResponse response,
69+
double severityThreshold,
70+
double confidenceThreshold,
71+
out string summary)
72+
{
73+
var tripped = new List<string>();
74+
var results = response.Results;
75+
76+
if (results?.ContentFilter?.Results is { } contentFilter)
77+
{
78+
foreach (var entry in contentFilter.Where(e => Meets(e.SeverityScore, severityThreshold)))
79+
{
80+
tripped.Add($"content:{entry.Category}");
81+
}
82+
}
83+
84+
if (results?.PromptAttack?.Results is { } promptAttack)
85+
{
86+
foreach (var entry in promptAttack.Where(e => Meets(e.SeverityScore, severityThreshold)))
87+
{
88+
tripped.Add($"promptAttack:{entry.Category}");
89+
}
90+
}
91+
92+
if (results?.SensitiveInformation?.Results is { } sensitive)
93+
{
94+
foreach (var entry in sensitive.Where(e => Meets(e.ConfidenceScore, confidenceThreshold)))
95+
{
96+
tripped.Add($"pii:{entry.Type}");
97+
}
98+
}
99+
100+
summary = tripped.Count == 0 ? "no checks tripped" : string.Join(",", tripped);
101+
return tripped.Count > 0;
102+
}
103+
104+
// A finding with no score is treated as meeting the threshold (fail-safe toward deny).
105+
private static bool Meets(double? score, double threshold) => !score.HasValue || score.Value >= threshold;
60106
}
61107
}

src/AWS.Bedrock.MAG/Policy/BedrockGuardrailsPolicyBackend.cs

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,14 @@ public sealed class BedrockGuardrailsPolicyBackend : IExternalPolicyBackend
3737
public BedrockGuardrailsPolicyBackend(BedrockGuardrailsPolicyOptions options, IAmazonBedrockRuntime? client = null)
3838
{
3939
_options = options ?? throw new ArgumentNullException(nameof(options));
40-
if (string.IsNullOrWhiteSpace(_options.GuardrailId))
40+
41+
var hasGuardrail = !string.IsNullOrWhiteSpace(_options.GuardrailId);
42+
var hasInlineChecks = _options.InlineChecks?.HasAnyCheck == true;
43+
if (!hasGuardrail && !hasInlineChecks)
4144
{
42-
throw new ArgumentException($"{nameof(BedrockGuardrailsPolicyOptions)}.{nameof(BedrockGuardrailsPolicyOptions.GuardrailId)} must be set.", nameof(options));
45+
throw new ArgumentException(
46+
"Set either GuardrailId (ApplyGuardrail) or InlineChecks with at least one category/entity (InvokeGuardrailChecks).",
47+
nameof(options));
4348
}
4449

4550
_client = new BedrockGuardrailClient(client ?? CreateClient(_options.Region, _options.Credentials));
@@ -67,30 +72,57 @@ public async Task<ExternalPolicyDecision> EvaluateAsync(IReadOnlyDictionary<stri
6772
// instead of escaping to the (sync) PolicyEngine and breaking the whole governance call.
6873
var text = (_options.ContextSerializer ?? DefaultContextSerializer)(context);
6974

70-
var invocation = await _client
71-
.ApplyAsync(_options.GuardrailId!, _options.GuardrailVersion, GuardrailContentSource.INPUT, text, cancellationToken)
72-
.ConfigureAwait(false);
73-
74-
var intervened = GuardrailResponseMapper.Intervened(invocation.Response);
75-
var summary = GuardrailResponseMapper.SummarizeAssessment(invocation.Response);
76-
77-
return new ExternalPolicyDecision
78-
{
79-
Backend = Name,
80-
Allowed = !intervened,
81-
Reason = intervened
82-
? $"Denied by Bedrock guardrail ({summary})."
83-
: "Allowed by Bedrock guardrail.",
84-
EvaluationMs = invocation.EvaluationMs,
85-
Metadata = new Dictionary<string, object> { ["assessment"] = summary }
86-
};
75+
// A configured guardrail (ApplyGuardrail) wins; otherwise run inline checks.
76+
return string.IsNullOrWhiteSpace(_options.GuardrailId)
77+
? await EvaluateWithInlineChecksAsync(text, cancellationToken).ConfigureAwait(false)
78+
: await EvaluateWithGuardrailAsync(text, cancellationToken).ConfigureAwait(false);
8779
}
8880
catch (Exception ex)
8981
{
9082
return BuildErrorDecision(ex);
9183
}
9284
}
9385

86+
private async Task<ExternalPolicyDecision> EvaluateWithGuardrailAsync(string text, CancellationToken cancellationToken)
87+
{
88+
var invocation = await _client
89+
.ApplyAsync(_options.GuardrailId!, _options.GuardrailVersion, GuardrailContentSource.INPUT, text, cancellationToken)
90+
.ConfigureAwait(false);
91+
92+
var intervened = GuardrailResponseMapper.Intervened(invocation.Response);
93+
var summary = GuardrailResponseMapper.SummarizeAssessment(invocation.Response);
94+
95+
return new ExternalPolicyDecision
96+
{
97+
Backend = Name,
98+
Allowed = !intervened,
99+
Reason = intervened
100+
? $"Denied by Bedrock guardrail ({summary})."
101+
: "Allowed by Bedrock guardrail.",
102+
EvaluationMs = invocation.EvaluationMs,
103+
Metadata = new Dictionary<string, object> { ["assessment"] = summary }
104+
};
105+
}
106+
107+
private async Task<ExternalPolicyDecision> EvaluateWithInlineChecksAsync(string text, CancellationToken cancellationToken)
108+
{
109+
var checks = _options.InlineChecks!;
110+
var invocation = await _client.InvokeChecksAsync(checks, text, cancellationToken).ConfigureAwait(false);
111+
var tripped = GuardrailResponseMapper.ChecksTripped(
112+
invocation.Response, checks.SeverityThreshold, checks.ConfidenceThreshold, out var summary);
113+
114+
return new ExternalPolicyDecision
115+
{
116+
Backend = Name,
117+
Allowed = !tripped,
118+
Reason = tripped
119+
? $"Denied by Bedrock inline checks ({summary})."
120+
: "Allowed by Bedrock inline checks.",
121+
EvaluationMs = invocation.EvaluationMs,
122+
Metadata = new Dictionary<string, object> { ["checks"] = summary }
123+
};
124+
}
125+
94126
// Fail-closed: set BOTH Error and Allowed=false so the engine denies (engine denies when
95127
// !IsNullOrWhiteSpace(Error) || !Allowed). Fail-open: Allowed=true and leave Error EMPTY, or the
96128
// engine would still deny; the error is kept in Metadata for the audit.

src/AWS.Bedrock.MAG/Policy/BedrockGuardrailsPolicyOptions.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,22 @@ namespace AWS.Bedrock.MAG
1313
/// </summary>
1414
public sealed class BedrockGuardrailsPolicyOptions
1515
{
16-
/// <summary>The guardrail identifier (ID or ARN) to evaluate tool-call context against. Required.</summary>
16+
/// <summary>
17+
/// The guardrail identifier (ID or ARN) to evaluate tool-call context against via ApplyGuardrail.
18+
/// Set this or <see cref="InlineChecks"/>. When both are set, the configured guardrail wins.
19+
/// </summary>
1720
public string? GuardrailId { get; set; }
1821

1922
/// <summary>The guardrail version to apply. Defaults to the mutable working draft.</summary>
2023
public string GuardrailVersion { get; set; } = "DRAFT";
2124

25+
/// <summary>
26+
/// Inline guardrail checks (InvokeGuardrailChecks), used when <see cref="GuardrailId"/> is not set.
27+
/// The checks are defined in the request, so no pre-created guardrail is required. Detection only:
28+
/// a tripped check denies the call (this mode does not mask text).
29+
/// </summary>
30+
public GuardrailChecksOptions? InlineChecks { get; set; }
31+
2232
/// <summary>
2333
/// Serializes the tool-call context (tool name and arguments the toolkit passes) into the text
2434
/// handed to the guardrail. Defaults to a compact JSON object, matching the toolkit's OPA and
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System.Collections.Generic;
5+
6+
namespace AWS.Bedrock.MAG
7+
{
8+
/// <summary>
9+
/// Configures inline guardrail checks for the policy backend (InvokeGuardrailChecks). The checks are
10+
/// supplied in the request, so no pre-created Bedrock guardrail is needed. At least one category or
11+
/// entity must be set. A check trips the deny when its score meets the matching threshold.
12+
/// </summary>
13+
public sealed class GuardrailChecksOptions
14+
{
15+
/// <summary>Content-filter categories to evaluate (e.g. HATE, INSULTS, SEXUAL, VIOLENCE, MISCONDUCT).</summary>
16+
public IList<string> ContentFilterCategories { get; } = new List<string>();
17+
18+
/// <summary>Prompt-attack categories to evaluate (e.g. PROMPT_ATTACK).</summary>
19+
public IList<string> PromptAttackCategories { get; } = new List<string>();
20+
21+
/// <summary>PII entity types to detect (e.g. US_SSN, EMAIL, NAME).</summary>
22+
public IList<string> SensitiveInformationEntities { get; } = new List<string>();
23+
24+
/// <summary>
25+
/// Content-filter and prompt-attack severity at or above which the call is denied (0.0 to 1.0).
26+
/// </summary>
27+
public double SeverityThreshold { get; set; } = 0.5;
28+
29+
/// <summary>PII confidence at or above which the call is denied (0.0 to 1.0).</summary>
30+
public double ConfidenceThreshold { get; set; } = 0.5;
31+
32+
/// <summary>True when at least one category or entity is configured.</summary>
33+
public bool HasAnyCheck =>
34+
ContentFilterCategories.Count > 0 || PromptAttackCategories.Count > 0 || SensitiveInformationEntities.Count > 0;
35+
}
36+
}

src/AWS.Bedrock.MAG/Setup/BedrockGovernanceServiceCollectionExtensions.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,12 @@ internal static void Normalize(BedrockGovernanceOptions options)
7171

7272
internal static void Validate(BedrockGovernanceOptions options)
7373
{
74-
if (options.EnablePolicy && string.IsNullOrWhiteSpace(options.Policy.GuardrailId))
74+
if (options.EnablePolicy
75+
&& string.IsNullOrWhiteSpace(options.Policy.GuardrailId)
76+
&& options.Policy.InlineChecks?.HasAnyCheck != true)
7577
{
76-
throw new InvalidOperationException("EnablePolicy is true but Policy.GuardrailId is not set.");
78+
throw new InvalidOperationException(
79+
"EnablePolicy is true but neither Policy.GuardrailId nor Policy.InlineChecks is configured.");
7780
}
7881

7982
if (options.EnablePiiSanitization && string.IsNullOrWhiteSpace(options.Sanitization.GuardrailId))

0 commit comments

Comments
 (0)