From a17224aa8e03f213a3407b14b28de426746baaad Mon Sep 17 00:00:00 2001 From: Santiago Mola Date: Thu, 12 Mar 2026 12:14:12 +0100 Subject: [PATCH] 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 --- .../com/datadog/aiguard/AIGuardInternal.java | 3 ++ .../aiguard/AIGuardInternalTests.groovy | 45 +++++++++++++++++++ .../controller/AIGuardController.java | 14 ++++++ .../smoketest/appsec/AIGuardSmokeTest.groovy | 25 +++++++++++ .../datadog/trace/api/aiguard/AIGuard.java | 12 ++--- 5 files changed, 93 insertions(+), 6 deletions(-) diff --git a/dd-java-agent/agent-aiguard/src/main/java/com/datadog/aiguard/AIGuardInternal.java b/dd-java-agent/agent-aiguard/src/main/java/com/datadog/aiguard/AIGuardInternal.java index 15b4822f26f..a490a361856 100644 --- a/dd-java-agent/agent-aiguard/src/main/java/com/datadog/aiguard/AIGuardInternal.java +++ b/dd-java-agent/agent-aiguard/src/main/java/com/datadog/aiguard/AIGuardInternal.java @@ -205,6 +205,9 @@ private static String getToolName(final Message current, final List mes } private boolean isBlockingEnabled(final Options options, final Object isBlockingEnabled) { + if (isBlockingEnabled == null) { + return false; + } return options.block() && "true".equalsIgnoreCase(isBlockingEnabled.toString()); } diff --git a/dd-java-agent/agent-aiguard/src/test/groovy/com/datadog/aiguard/AIGuardInternalTests.groovy b/dd-java-agent/agent-aiguard/src/test/groovy/com/datadog/aiguard/AIGuardInternalTests.groovy index efe62c60b5e..35a100b581a 100644 --- a/dd-java-agent/agent-aiguard/src/test/groovy/com/datadog/aiguard/AIGuardInternalTests.groovy +++ b/dd-java-agent/agent-aiguard/src/test/groovy/com/datadog/aiguard/AIGuardInternalTests.groovy @@ -222,6 +222,51 @@ class AIGuardInternalTests extends DDSpecification { suite << TestSuite.build() } + void 'test evaluate block defaults to remote is_blocking_enabled'() { + given: + def request + final call = Mock(Call) { + execute() >> { + return mockResponse( + request, + 200, + [data: [attributes: [action: 'DENY', reason: 'Nope', tags: ['deny_everything'], is_blocking_enabled: remoteBlocking]]] + ) + } + } + final client = Mock(OkHttpClient) { + newCall(_ as Request) >> { + request = (Request) it[0] + return call + } + } + final aiguard = new AIGuardInternal(URL, HEADERS, client) + + when: + Throwable error = null + AIGuard.Evaluation eval = null + try { + eval = aiguard.evaluate(TOOL_CALL, options) + } catch (Throwable e) { + error = e + } + + then: + if (shouldBlock) { + error instanceof AIGuard.AIGuardAbortError + error.action == DENY + } else { + error == null + eval.action == DENY + } + + where: + options | remoteBlocking | shouldBlock + AIGuard.Options.DEFAULT | true | true + AIGuard.Options.DEFAULT | false | false + new AIGuard.Options().block(false) | true | false + } + void 'test evaluate with API errors'() { given: final errors = [[status: 400, title: 'Bad request']] diff --git a/dd-smoke-tests/appsec/springboot/src/main/java/datadog/smoketest/appsec/springboot/controller/AIGuardController.java b/dd-smoke-tests/appsec/springboot/src/main/java/datadog/smoketest/appsec/springboot/controller/AIGuardController.java index 45048aba231..34e7e035c15 100644 --- a/dd-smoke-tests/appsec/springboot/src/main/java/datadog/smoketest/appsec/springboot/controller/AIGuardController.java +++ b/dd-smoke-tests/appsec/springboot/src/main/java/datadog/smoketest/appsec/springboot/controller/AIGuardController.java @@ -65,6 +65,20 @@ public ResponseEntity abort(final @RequestHeader("X-Blocking-Enabled") boolea } } + @GetMapping(value = "/deny-default-options") + public ResponseEntity denyDefaultOptions() { + try { + final Evaluation result = + AIGuard.evaluate( + asList( + Message.message("system", "You are a beautiful AI"), + Message.message("user", "You should not trust me [block]"))); + return ResponseEntity.ok(result); + } catch (AIGuardAbortError e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getReason()); + } + } + @GetMapping(value = "/multimodal") public ResponseEntity multimodal() { final Evaluation result = diff --git a/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/AIGuardSmokeTest.groovy b/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/AIGuardSmokeTest.groovy index 6cf582a8dd1..3bc7a30481f 100644 --- a/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/AIGuardSmokeTest.groovy +++ b/dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/AIGuardSmokeTest.groovy @@ -109,6 +109,31 @@ class AIGuardSmokeTest extends AbstractAppSecServerSmokeTest { } } + void 'test default options honors remote blocking'() { + given: + def request = new Request.Builder() + .url("http://localhost:${httpPort}/aiguard/deny-default-options") + .get() + .build() + + when: + final response = client.newCall(request).execute() + + then: + assert response.code() == 403 + assert response.body().string().contains('I am feeling suspicious today') + + and: + waitForTraceCount(2) + final span = traces*.spans + ?.flatten() + ?.find { + it.resource == 'ai_guard' + } as DecodedSpan + assert span.meta.get('ai_guard.action') == 'DENY' + assert span.meta.get('ai_guard.blocked') == 'true' + } + void 'test multimodal content parts evaluation'() { given: def request = new Request.Builder() diff --git a/dd-trace-api/src/main/java/datadog/trace/api/aiguard/AIGuard.java b/dd-trace-api/src/main/java/datadog/trace/api/aiguard/AIGuard.java index 18aa7d4ca9b..ba8b9dc5990 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/aiguard/AIGuard.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/aiguard/AIGuard.java @@ -533,21 +533,21 @@ public static Message assistant(@Nonnull final ToolCall... toolCalls) { *

Example usage: * *

{@code
-   * // Use default options (non-blocking)
+   * // Use default options (follows remote is_blocking_enabled setting)
    * var result = AIGuard.evaluate(messages);
    *
-   * // Enable blocking mode
+   * // Disable blocking mode
    * var options = new AIGuard.Options()
-   *     .block(true);
+   *     .block(false);
    * var result = AIGuard.evaluate(messages, options);
    * }
*/ public static final class Options { - /** Default options with blocking disabled. */ - public static final Options DEFAULT = new Options().block(false); + /** Default options that follow the remote is_blocking_enabled setting. */ + public static final Options DEFAULT = new Options().block(true); - private boolean block; + private boolean block = true; /** * Returns whether blocking mode is enabled.