This is an automated email from the ASF dual-hosted git repository.

oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new d81c8c493ab2 CAMEL-25404: camel-ai-tool - make an authorizationPolicy 
denial observable via a CamelEvent (#27480)
d81c8c493ab2 is described below

commit d81c8c493ab2d452a1a00834f40f350f8e7b470b
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Oct 7 13:56:17 2026 +0200

    CAMEL-25404: camel-ai-tool - make an authorizationPolicy denial observable 
via a CamelEvent (#27480)
    
    An authorizationPolicy denial in AiToolExecutor now fires an
    AiToolAuthorizationDeniedEvent (a CamelEvent) so route operators can observe
    refused tool calls through the regular EventNotifier mechanism. Notifier
    failures can never affect the refusal: the notification is guarded with
    catch (Throwable), consistent with EventHelper.doNotifyEvent(), so a 
notifier
    throwing an Error still leaves the caller with AuthorizationDenied.
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
---
 .../camel/catalog/docs/ai-tool-component.adoc      |  31 ++-
 .../src/main/docs/ai-tool-component.adoc           |  31 ++-
 .../ai/tool/AiToolAuthorizationDeniedEvent.java    |  97 ++++++++++
 .../camel/component/ai/tool/AiToolExecutor.java    |  34 +++-
 .../tool/AiToolAuthorizationDeniedEventTest.java   | 210 +++++++++++++++++++++
 5 files changed, 398 insertions(+), 5 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-tool-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-tool-component.adoc
index 9912e90bdf31..b5b934eb1a23 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-tool-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-tool-component.adoc
@@ -426,7 +426,8 @@ YAML::
 The guard runs in front of the route: it wraps the route's outer processor, so 
it executes before the route's unit
 of work, tracing and error handling. A denied call 
(`CamelAuthorizationException`) is returned to the model as a
 short refusal it can relay — not as a tool result and not as a stack trace — 
regardless of the tool-execution error
-strategy; the denial is logged at `WARN`, but it does not produce a route span 
or metric.
+strategy. Because the guard runs in front of the route, a denial never enters 
the route's unit of work, so it does
+not produce a route span. It is instead observable as described in <<Observing 
denials>> below.
 
 The policy authorizes on *trustworthy* input only:
 
@@ -448,6 +449,34 @@ Which runtimes carry the caller identity:
   that inspects `CamelMcpSecurityPrincipal`. Exposing a runtime-neutral 
principal name and roles for the shipped
   policies is tracked as a follow-up.
 
+=== Observing denials
+
+Because the guard runs in front of the route, a denied call is caught before 
the route's unit of work and so fires no
+exchange-lifecycle event or route span. Every denial is instead:
+
+* logged at `WARN`, and
+* published as an 
`org.apache.camel.component.ai.tool.AiToolAuthorizationDeniedEvent` — a 
`CamelEvent` of type
+  `Custom` carrying the denied tool name, the tool exchange (for correlation), 
and the `CamelAuthorizationException`
+  the policy raised.
+
+Register an `EventNotifier` to count, log or alert on denials — for example to 
drive a micrometer counter — without
+changing what the model sees (still the short refusal):
+
+[source,java]
+----
+context.getManagementStrategy().addEventNotifier(new EventNotifierSupport() {
+    @Override
+    public void notify(CamelEvent event) {
+        if (event instanceof AiToolAuthorizationDeniedEvent denied) {
+            meterRegistry.counter("ai.tool.authorization.denied", "tool", 
denied.getToolName()).increment();
+        }
+    }
+});
+----
+
+The event is emitted best-effort: when no `EventNotifier` is registered 
nothing is published, and a failure to notify
+never affects the refusal returned to the model.
+
 == See Also
 
 * xref:langchain4j-agent-component.adoc[LangChain4j Agent Component] — 
discovers ai-tool tools via the `tags` option
diff --git 
a/components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc 
b/components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc
index 9912e90bdf31..b5b934eb1a23 100644
--- a/components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc
+++ b/components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc
@@ -426,7 +426,8 @@ YAML::
 The guard runs in front of the route: it wraps the route's outer processor, so 
it executes before the route's unit
 of work, tracing and error handling. A denied call 
(`CamelAuthorizationException`) is returned to the model as a
 short refusal it can relay — not as a tool result and not as a stack trace — 
regardless of the tool-execution error
-strategy; the denial is logged at `WARN`, but it does not produce a route span 
or metric.
+strategy. Because the guard runs in front of the route, a denial never enters 
the route's unit of work, so it does
+not produce a route span. It is instead observable as described in <<Observing 
denials>> below.
 
 The policy authorizes on *trustworthy* input only:
 
@@ -448,6 +449,34 @@ Which runtimes carry the caller identity:
   that inspects `CamelMcpSecurityPrincipal`. Exposing a runtime-neutral 
principal name and roles for the shipped
   policies is tracked as a follow-up.
 
+=== Observing denials
+
+Because the guard runs in front of the route, a denied call is caught before 
the route's unit of work and so fires no
+exchange-lifecycle event or route span. Every denial is instead:
+
+* logged at `WARN`, and
+* published as an 
`org.apache.camel.component.ai.tool.AiToolAuthorizationDeniedEvent` — a 
`CamelEvent` of type
+  `Custom` carrying the denied tool name, the tool exchange (for correlation), 
and the `CamelAuthorizationException`
+  the policy raised.
+
+Register an `EventNotifier` to count, log or alert on denials — for example to 
drive a micrometer counter — without
+changing what the model sees (still the short refusal):
+
+[source,java]
+----
+context.getManagementStrategy().addEventNotifier(new EventNotifierSupport() {
+    @Override
+    public void notify(CamelEvent event) {
+        if (event instanceof AiToolAuthorizationDeniedEvent denied) {
+            meterRegistry.counter("ai.tool.authorization.denied", "tool", 
denied.getToolName()).increment();
+        }
+    }
+});
+----
+
+The event is emitted best-effort: when no `EventNotifier` is registered 
nothing is published, and a failure to notify
+never affects the refusal returned to the model.
+
 == See Also
 
 * xref:langchain4j-agent-component.adoc[LangChain4j Agent Component] — 
discovers ai-tool tools via the `tags` option
diff --git 
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolAuthorizationDeniedEvent.java
 
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolAuthorizationDeniedEvent.java
new file mode 100644
index 000000000000..09e5e4bfe1a7
--- /dev/null
+++ 
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolAuthorizationDeniedEvent.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.ai.tool;
+
+import java.io.Serial;
+import java.util.EventObject;
+
+import org.apache.camel.CamelAuthorizationException;
+import org.apache.camel.Exchange;
+import org.apache.camel.spi.CamelEvent;
+
+/**
+ * Fired when an {@code ai-tool} route's {@code authorizationPolicy} denies a 
tool call.
+ * <p>
+ * The policy guards the route's <em>outer</em> processor, so a denial is 
caught in front of the route: it never enters
+ * the route's unit of work and therefore produces no exchange-lifecycle event 
or route span. This event is the
+ * observable signal operators can subscribe to (with an {@code 
EventNotifier}, JMX, or a micrometer counter) to see and
+ * alert on authorization denials, without changing what the model sees — the 
call is still relayed back to the model as
+ * a short refusal.
+ * <p>
+ * It is a {@link CamelEvent.Type#Custom} event and deliberately not a {@link 
CamelEvent.ExchangeEvent}, so it does not
+ * pollute generic exchange lifecycle metrics; {@link #getExchange()} is 
public so listeners can still correlate it with
+ * the tool exchange (route id, exchange id). It is a {@link 
CamelEvent.FailureEvent}, so {@link #getCause()} returns
+ * the {@link CamelAuthorizationException} the policy raised.
+ *
+ * @since 4.23
+ */
+public final class AiToolAuthorizationDeniedEvent extends EventObject 
implements CamelEvent.FailureEvent {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    private final transient Exchange exchange;
+    private final String toolName;
+    private final transient CamelAuthorizationException cause;
+    private long timestamp;
+
+    public AiToolAuthorizationDeniedEvent(Exchange exchange, String toolName, 
CamelAuthorizationException cause) {
+        super(exchange);
+        this.exchange = exchange;
+        this.toolName = toolName;
+        this.cause = cause;
+    }
+
+    /**
+     * The tool exchange whose call was denied (for correlation, e.g. route id 
or exchange id).
+     */
+    public Exchange getExchange() {
+        return exchange;
+    }
+
+    /**
+     * The name (route id) of the tool whose call was denied.
+     */
+    public String getToolName() {
+        return toolName;
+    }
+
+    @Override
+    public CamelAuthorizationException getCause() {
+        return cause;
+    }
+
+    @Override
+    public Type getType() {
+        return Type.Custom;
+    }
+
+    @Override
+    public long getTimestamp() {
+        return timestamp;
+    }
+
+    @Override
+    public void setTimestamp(long timestamp) {
+        this.timestamp = timestamp;
+    }
+
+    @Override
+    public String toString() {
+        return "AiToolAuthorizationDeniedEvent{toolName='" + toolName + "'}";
+    }
+}
diff --git 
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolExecutor.java
 
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolExecutor.java
index 4e5a2f405d19..028d667f0a2b 100644
--- 
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolExecutor.java
+++ 
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolExecutor.java
@@ -24,6 +24,7 @@ import java.util.Set;
 import org.apache.camel.CamelAuthorizationException;
 import org.apache.camel.Exchange;
 import org.apache.camel.Processor;
+import org.apache.camel.spi.ManagementStrategy;
 import org.apache.camel.support.DefaultConsumer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -148,7 +149,7 @@ public final class AiToolExecutor {
 
             if (exchange.getException() != null) {
                 Exception routeError = exchange.getException();
-                AiToolResult denied = authorizationDenied(toolName, 
routeError);
+                AiToolResult denied = authorizationDenied(toolName, 
routeError, exchange);
                 if (denied != null) {
                     return denied;
                 }
@@ -161,7 +162,7 @@ public final class AiToolExecutor {
             LOG.debug("Tool '{}' execution completed successfully", toolName);
             return buildSuccessResult(spec, exchange, result);
         } catch (Exception e) {
-            AiToolResult denied = authorizationDenied(toolName, e);
+            AiToolResult denied = authorizationDenied(toolName, e, exchange);
             if (denied != null) {
                 return denied;
             }
@@ -176,17 +177,44 @@ public final class AiToolExecutor {
      * 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 (Throwable e) {
+            // ManagementStrategy.notify() does not isolate a failing notifier 
(unlike EventHelper.doNotifyEvent), so a
+            // notifier throwing anything - including an Error such as 
AssertionError - would otherwise escape and turn
+            // the denial into a propagating failure. Swallow it here (as 
EventHelper does) so a broken notifier never
+            // changes the AuthorizationDenied refusal returned to the model.
+            LOG.warn("Notifying {} for tool '{}' failed and was ignored; the 
authorization denial is unaffected",
+                    AiToolAuthorizationDeniedEvent.class.getSimpleName(), 
toolName, e);
+        }
+    }
+
     private static CamelAuthorizationException 
findAuthorizationException(Throwable error) {
         for (Throwable t = error; t != null; t = t.getCause()) {
             if (t instanceof CamelAuthorizationException cae) {
diff --git 
a/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolAuthorizationDeniedEventTest.java
 
b/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolAuthorizationDeniedEventTest.java
new file mode 100644
index 000000000000..888555f93849
--- /dev/null
+++ 
b/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolAuthorizationDeniedEventTest.java
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.ai.tool;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.camel.CamelAuthorizationException;
+import org.apache.camel.Exchange;
+import org.apache.camel.NamedNode;
+import org.apache.camel.Processor;
+import org.apache.camel.Route;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.AuthorizationPolicy;
+import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.support.EventNotifierSupport;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-25404: an {@code authorizationPolicy} denial on an {@code ai-tool} 
route is caught in front of the route, so it
+ * fires no exchange-lifecycle event or route span. This verifies the 
component instead emits an
+ * {@link AiToolAuthorizationDeniedEvent} on every denial (so operators can 
observe and alert), while the model-facing
+ * behaviour is unchanged (still an {@link AiToolResult.AuthorizationDenied} 
refusal), and that the event is
+ * <em>not</em> emitted for a normal route error or a successful call.
+ */
+class AiToolAuthorizationDeniedEventTest extends CamelTestSupport {
+
+    private final List<AiToolAuthorizationDeniedEvent> events = new 
CopyOnWriteArrayList<>();
+
+    /** Allows the call only when {@code subject == alice}; otherwise throws, 
as a real policy does. */
+    static final class SubjectPolicy implements AuthorizationPolicy {
+        @Override
+        public void beforeWrap(Route route, NamedNode definition) {
+        }
+
+        @Override
+        public Processor wrap(Route route, Processor processor) {
+            return exchange -> {
+                if (!"alice".equals(exchange.getProperty("subject", 
String.class))) {
+                    throw new CamelAuthorizationException("caller is not 
authorized", exchange);
+                }
+                processor.process(exchange);
+            };
+        }
+    }
+
+    /** Denies by setting the exception on the exchange rather than throwing 
(as Spring Security's policy does). */
+    static final class ExceptionDeny implements AuthorizationPolicy {
+        @Override
+        public void beforeWrap(Route route, NamedNode definition) {
+        }
+
+        @Override
+        public Processor wrap(Route route, Processor processor) {
+            return exchange -> exchange.setException(new 
CamelAuthorizationException("denied via exchange", exchange));
+        }
+    }
+
+    /** Allows every call (delegates straight to the route). */
+    static final class AllowAll implements AuthorizationPolicy {
+        @Override
+        public void beforeWrap(Route route, NamedNode definition) {
+        }
+
+        @Override
+        public Processor wrap(Route route, Processor processor) {
+            return processor::process;
+        }
+    }
+
+    @Override
+    protected void bindToRegistry(Registry registry) {
+        registry.bind("subjectPolicy", new SubjectPolicy());
+        registry.bind("exceptionDeny", new ExceptionDeny());
+        registry.bind("allowAll", new AllowAll());
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                from("ai-tool:guarded?tags=test&description=A guarded 
tool&authorizationPolicy=#subjectPolicy")
+                        .setBody(constant("ok"));
+
+                from("ai-tool:exceptionGuarded?tags=test&description=A tool 
whose policy sets the exception"
+                     + "&authorizationPolicy=#exceptionDeny")
+                        .setBody(constant("ok"));
+
+                from("ai-tool:allowedButFails?tags=test&description=A tool 
allowed but whose route fails"
+                     + "&authorizationPolicy=#allowAll")
+                        .throwException(new RuntimeException("route boom"));
+            }
+        };
+    }
+
+    @BeforeEach
+    void registerEventNotifier() {
+        events.clear();
+        context.getManagementStrategy().addEventNotifier(new 
EventNotifierSupport() {
+            @Override
+            public void notify(CamelEvent event) {
+                if (event instanceof AiToolAuthorizationDeniedEvent denied) {
+                    events.add(denied);
+                }
+            }
+        });
+    }
+
+    @Test
+    void firesAnEventWhenThePolicyThrows() {
+        AiToolSpec spec = findSpec("guarded");
+        Exchange exchange = new DefaultExchange(context);
+        exchange.setProperty("subject", "mallory");
+
+        AiToolResult result = AiToolExecutor.execute(spec, Map.of(), exchange);
+
+        // model-facing behaviour is unchanged: still a refusal
+        
assertThat(result).isInstanceOf(AiToolResult.AuthorizationDenied.class);
+        // ...and the denial is now observable as a single event
+        assertThat(events).hasSize(1);
+        AiToolAuthorizationDeniedEvent event = events.get(0);
+        assertThat(event.getType()).isEqualTo(CamelEvent.Type.Custom);
+        assertThat(event.getToolName()).isEqualTo("guarded");
+        assertThat(event.getExchange()).isSameAs(exchange);
+        
assertThat(event.getCause()).isInstanceOf(CamelAuthorizationException.class);
+    }
+
+    @Test
+    void firesAnEventWhenThePolicySetsTheExceptionInsteadOfThrowing() {
+        AiToolSpec spec = findSpec("exceptionGuarded");
+
+        AiToolResult result = AiToolExecutor.execute(spec, Map.of(), new 
DefaultExchange(context));
+
+        
assertThat(result).isInstanceOf(AiToolResult.AuthorizationDenied.class);
+        assertThat(events).hasSize(1);
+        assertThat(events.get(0).getToolName()).isEqualTo("exceptionGuarded");
+    }
+
+    @Test
+    void doesNotFireForANonAuthorizationRouteError() {
+        AiToolSpec spec = findSpec("allowedButFails");
+
+        AiToolResult result = AiToolExecutor.execute(spec, Map.of(), new 
DefaultExchange(context));
+
+        assertThat(result).isInstanceOf(AiToolResult.ExecutionError.class);
+        assertThat(events).isEmpty();
+    }
+
+    @Test
+    void doesNotFireWhenThePolicyAllows() {
+        AiToolSpec spec = findSpec("guarded");
+        Exchange exchange = new DefaultExchange(context);
+        exchange.setProperty("subject", "alice");
+
+        AiToolResult result = AiToolExecutor.execute(spec, Map.of(), exchange);
+
+        assertThat(result).isInstanceOf(AiToolResult.Success.class);
+        assertThat(events).isEmpty();
+    }
+
+    @Test
+    void aNotifierThrowingAnErrorDoesNotBreakTheRefusal() {
+        // ManagementStrategy.notify does not isolate a failing notifier, so 
without a Throwable guard a notifier that
+        // throws an Error would escape and turn the denial into a propagating 
failure. The refusal must survive.
+        context.getManagementStrategy().addEventNotifier(new 
EventNotifierSupport() {
+            @Override
+            public void notify(CamelEvent event) {
+                if (event instanceof AiToolAuthorizationDeniedEvent) {
+                    throw new AssertionError("broken notifier");
+                }
+            }
+        });
+
+        AiToolSpec spec = findSpec("guarded");
+        Exchange exchange = new DefaultExchange(context);
+        exchange.setProperty("subject", "mallory");
+
+        AiToolResult result = AiToolExecutor.execute(spec, Map.of(), exchange);
+
+        
assertThat(result).isInstanceOf(AiToolResult.AuthorizationDenied.class);
+    }
+
+    private AiToolSpec findSpec(String toolName) {
+        return 
AiToolRegistry.getOrCreate(context).getToolsByTag("test").stream()
+                .filter(s -> toolName.equals(s.getName()))
+                .findFirst()
+                .orElseThrow(() -> new AssertionError("Tool not found: " + 
toolName));
+    }
+}

Reply via email to