Skip to content

Commit a60c2fd

Browse files
authored
Honor in-app blocking settings by default in AI Guard evaluate (#10818)
Honor in-app blocking settings by default in AI Guard evaluate Change Options.block default from false to true so that evaluate() follows the remote is_blocking_enabled setting without requiring explicit opt-in. Users can still override with new Options().block(false). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Merge branch 'master' into smola/ai-guard-ui-blocing Co-authored-by: santiago.mola <santiago.mola@datadoghq.com>
1 parent 87ce78d commit a60c2fd

File tree

5 files changed

+93
-6
lines changed

5 files changed

+93
-6
lines changed

dd-java-agent/agent-aiguard/src/main/java/com/datadog/aiguard/AIGuardInternal.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,9 @@ private static String getToolName(final Message current, final List<Message> mes
205205
}
206206

207207
private boolean isBlockingEnabled(final Options options, final Object isBlockingEnabled) {
208+
if (isBlockingEnabled == null) {
209+
return false;
210+
}
208211
return options.block() && "true".equalsIgnoreCase(isBlockingEnabled.toString());
209212
}
210213

dd-java-agent/agent-aiguard/src/test/groovy/com/datadog/aiguard/AIGuardInternalTests.groovy

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,51 @@ class AIGuardInternalTests extends DDSpecification {
222222
suite << TestSuite.build()
223223
}
224224

225+
void 'test evaluate block defaults to remote is_blocking_enabled'() {
226+
given:
227+
def request
228+
final call = Mock(Call) {
229+
execute() >> {
230+
return mockResponse(
231+
request,
232+
200,
233+
[data: [attributes: [action: 'DENY', reason: 'Nope', tags: ['deny_everything'], is_blocking_enabled: remoteBlocking]]]
234+
)
235+
}
236+
}
237+
final client = Mock(OkHttpClient) {
238+
newCall(_ as Request) >> {
239+
request = (Request) it[0]
240+
return call
241+
}
242+
}
243+
final aiguard = new AIGuardInternal(URL, HEADERS, client)
244+
245+
when:
246+
Throwable error = null
247+
AIGuard.Evaluation eval = null
248+
try {
249+
eval = aiguard.evaluate(TOOL_CALL, options)
250+
} catch (Throwable e) {
251+
error = e
252+
}
253+
254+
then:
255+
if (shouldBlock) {
256+
error instanceof AIGuard.AIGuardAbortError
257+
error.action == DENY
258+
} else {
259+
error == null
260+
eval.action == DENY
261+
}
262+
263+
where:
264+
options | remoteBlocking | shouldBlock
265+
AIGuard.Options.DEFAULT | true | true
266+
AIGuard.Options.DEFAULT | false | false
267+
new AIGuard.Options().block(false) | true | false
268+
}
269+
225270
void 'test evaluate with API errors'() {
226271
given:
227272
final errors = [[status: 400, title: 'Bad request']]

dd-smoke-tests/appsec/springboot/src/main/java/datadog/smoketest/appsec/springboot/controller/AIGuardController.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,20 @@ public ResponseEntity<?> abort(final @RequestHeader("X-Blocking-Enabled") boolea
6565
}
6666
}
6767

68+
@GetMapping(value = "/deny-default-options")
69+
public ResponseEntity<?> denyDefaultOptions() {
70+
try {
71+
final Evaluation result =
72+
AIGuard.evaluate(
73+
asList(
74+
Message.message("system", "You are a beautiful AI"),
75+
Message.message("user", "You should not trust me [block]")));
76+
return ResponseEntity.ok(result);
77+
} catch (AIGuardAbortError e) {
78+
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getReason());
79+
}
80+
}
81+
6882
@GetMapping(value = "/multimodal")
6983
public ResponseEntity<?> multimodal() {
7084
final Evaluation result =

dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/AIGuardSmokeTest.groovy

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,31 @@ class AIGuardSmokeTest extends AbstractAppSecServerSmokeTest {
109109
}
110110
}
111111

112+
void 'test default options honors remote blocking'() {
113+
given:
114+
def request = new Request.Builder()
115+
.url("http://localhost:${httpPort}/aiguard/deny-default-options")
116+
.get()
117+
.build()
118+
119+
when:
120+
final response = client.newCall(request).execute()
121+
122+
then:
123+
assert response.code() == 403
124+
assert response.body().string().contains('I am feeling suspicious today')
125+
126+
and:
127+
waitForTraceCount(2)
128+
final span = traces*.spans
129+
?.flatten()
130+
?.find {
131+
it.resource == 'ai_guard'
132+
} as DecodedSpan
133+
assert span.meta.get('ai_guard.action') == 'DENY'
134+
assert span.meta.get('ai_guard.blocked') == 'true'
135+
}
136+
112137
void 'test multimodal content parts evaluation'() {
113138
given:
114139
def request = new Request.Builder()

dd-trace-api/src/main/java/datadog/trace/api/aiguard/AIGuard.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -533,21 +533,21 @@ public static Message assistant(@Nonnull final ToolCall... toolCalls) {
533533
* <p>Example usage:
534534
*
535535
* <pre>{@code
536-
* // Use default options (non-blocking)
536+
* // Use default options (follows remote is_blocking_enabled setting)
537537
* var result = AIGuard.evaluate(messages);
538538
*
539-
* // Enable blocking mode
539+
* // Disable blocking mode
540540
* var options = new AIGuard.Options()
541-
* .block(true);
541+
* .block(false);
542542
* var result = AIGuard.evaluate(messages, options);
543543
* }</pre>
544544
*/
545545
public static final class Options {
546546

547-
/** Default options with blocking disabled. */
548-
public static final Options DEFAULT = new Options().block(false);
547+
/** Default options that follow the remote is_blocking_enabled setting. */
548+
public static final Options DEFAULT = new Options().block(true);
549549

550-
private boolean block;
550+
private boolean block = true;
551551

552552
/**
553553
* Returns whether blocking mode is enabled.

0 commit comments

Comments
 (0)