oscerd commented on code in PR #27480:
URL: https://github.com/apache/camel/pull/27480#discussion_r4204959725


##########
components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolExecutor.java:
##########
@@ -176,17 +177,42 @@ public static AiToolResult execute(AiToolSpec spec, 
Map<String, Object> argument
      * present in its cause chain (the route's {@link 
org.apache.camel.spi.AuthorizationPolicy} rejected the call).
      * Returns a caller-safe {@link AiToolResult.AuthorizationDenied} refusal 
that does not leak the policy's internal
      * message, or {@code null} when the error is not an authorization denial.
+     * <p>
+     * On a denial it also logs at {@code WARN} and emits an {@link 
AiToolAuthorizationDeniedEvent} so operators can
+     * observe and alert on denials; neither affects the refusal returned to 
the model.
      */
-    private static AiToolResult authorizationDenied(String toolName, Throwable 
error) {
+    private static AiToolResult authorizationDenied(String toolName, Throwable 
error, Exchange exchange) {
         CamelAuthorizationException denial = findAuthorizationException(error);
         if (denial == null) {
             return null;
         }
         LOG.warn("Tool '{}' call denied by authorization policy: {}", 
toolName, denial.getMessage());
+        fireAuthorizationDeniedEvent(exchange, toolName, denial);
         return new AiToolResult.AuthorizationDenied(
                 String.format("Access denied: not authorized to call tool 
'%s'", toolName), denial);
     }
 
+    /**
+     * Emits an {@link AiToolAuthorizationDeniedEvent} for the denied tool 
call. The policy guards the route's outer
+     * processor, so a denial never runs the route's unit of work and fires no 
exchange-lifecycle event or route span;
+     * this event is the observable signal. Best-effort: when no {@code 
EventNotifier} is registered nothing is emitted,
+     * and a failure to notify is swallowed so it never affects the refusal 
returned to the model.
+     */
+    private static void fireAuthorizationDeniedEvent(Exchange exchange, String 
toolName, CamelAuthorizationException denial) {
+        ManagementStrategy management = 
exchange.getContext().getManagementStrategy();
+        if (management.getEventNotifiers().isEmpty()) {
+            return;
+        }
+        try {
+            management.notify(new AiToolAuthorizationDeniedEvent(exchange, 
toolName, denial));
+        } catch (Exception e) {

Review Comment:
   Fixed in 572f019c6d7d. You're right — `ManagementStrategy.notify()` loops 
the notifiers without isolating a failure, so catching only `Exception` let an 
`Error` (e.g. `AssertionError`) thrown by a notifier escape 
`AiToolExecutor.execute()` and break the "never affects the refusal" guarantee. 
Now catching `Throwable` (consistent with `EventHelper.doNotifyEvent()`) and 
logging at `WARN`.
   
   Added 
`AiToolAuthorizationDeniedEventTest.aNotifierThrowingAnErrorDoesNotBreakTheRefusal`:
 a denying policy plus a notifier that throws an `AssertionError` for 
`AiToolAuthorizationDeniedEvent` — the result is still `AuthorizationDenied`. 
Verified it fails on the parent revision and passes now.
   
   (The sibling `camel-openai` `OpenAIAgenticObservability.notify()` uses the 
same catch-`Exception` pattern, but those are lifecycle events with no refusal 
guarantee; I've kept this fix scoped to ai-tool where the guarantee is explicit 
— happy to file a follow-up for openai if you think it's worth hardening there 
too.)
   
   _Claude Code on behalf of oscerd_



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to