This is an automated email from the ASF dual-hosted git repository.
jamesnetherton pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
The following commit(s) were added to refs/heads/main by this push:
new b4e96412b5 Add a2a extension
b4e96412b5 is described below
commit b4e96412b5a6c3caaade4ac2edca1cbbea2cac3c
Author: James Netherton <[email protected]>
AuthorDate: Fri Jul 3 12:57:41 2026 +0100
Add a2a extension
Fixes #8780
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
catalog/pom.xml | 13 +
docs/modules/ROOT/examples/components/a2a.yml | 13 +
docs/modules/ROOT/nav.adoc | 1 +
.../ROOT/pages/reference/extensions/a2a.adoc | 61 +++
extensions/a2a/deployment/pom.xml | 71 +++
.../component/a2a/deployment/A2aProcessor.java | 65 +++
extensions/a2a/pom.xml | 39 ++
extensions/a2a/runtime/pom.xml | 110 +++++
extensions/a2a/runtime/src/main/doc/usage.adoc | 11 +
.../camel/quarkus/component/a2a/A2aRecorder.java | 52 ++
.../main/resources/META-INF/quarkus-extension.yaml | 36 ++
extensions/pom.xml | 1 +
integration-tests/a2a/pom.xml | 167 +++++++
.../quarkus/component/a2a/it/A2aResource.java | 322 ++++++++++++
.../camel/quarkus/component/a2a/it/A2aRoutes.java | 151 ++++++
.../component/a2a/it/TestExtensionHandler.java | 38 ++
.../a2a/src/main/resources/application.properties | 24 +
.../main/resources/cards/apikey-agent-card.json | 28 ++
.../src/main/resources/cards/emit-agent-card.json | 14 +
.../main/resources/cards/extension-agent-card.json | 19 +
.../src/main/resources/cards/full-agent-card.json | 36 ++
.../src/main/resources/cards/oauth-agent-card.json | 27 ++
.../src/main/resources/cards/push-agent-card.json | 14 +
.../main/resources/cards/streaming-agent-card.json | 14 +
.../camel/quarkus/component/a2a/it/A2aIT.java | 24 +
.../component/a2a/it/A2aKeycloakTestResource.java | 63 +++
.../camel/quarkus/component/a2a/it/A2aTest.java | 538 +++++++++++++++++++++
.../a2a/src/test/resources/camel-realm.json | 84 ++++
integration-tests/pom.xml | 1 +
poms/bom/pom.xml | 15 +
poms/bom/src/main/generated/flattened-full-pom.xml | 15 +
.../src/main/generated/flattened-reduced-pom.xml | 15 +
.../generated/flattened-reduced-verbose-pom.xml | 15 +
tooling/scripts/test-categories.yaml | 1 +
34 files changed, 2098 insertions(+)
diff --git a/catalog/pom.xml b/catalog/pom.xml
index 3eac29e9ac..bbaf3e869d 100644
--- a/catalog/pom.xml
+++ b/catalog/pom.xml
@@ -84,6 +84,19 @@
</dependency>
<!-- The following dependencies guarantee that this module is built
after them. You can update them by running `mvn process-resources -Pformat -N`
from the source tree root directory -->
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-a2a</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>*</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-activemq</artifactId>
diff --git a/docs/modules/ROOT/examples/components/a2a.yml
b/docs/modules/ROOT/examples/components/a2a.yml
new file mode 100644
index 0000000000..4ada35c7ca
--- /dev/null
+++ b/docs/modules/ROOT/examples/components/a2a.yml
@@ -0,0 +1,13 @@
+# Do not edit directly!
+# This file was generated by
camel-quarkus-maven-plugin:update-extension-doc-page
+cqArtifactId: camel-quarkus-a2a
+cqArtifactIdBase: a2a
+cqNativeSupported: true
+cqStatus: Stable
+cqDeprecated: false
+cqJvmSince: 3.37.0
+cqNativeSince: 3.37.0
+cqCamelPartName: a2a
+cqCamelPartTitle: A2A
+cqCamelPartDescription: A2A endpoint for agent-to-agent communication.
+cqExtensionPageTitle: A2A
diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc
index 1a5629540b..1e808479ee 100644
--- a/docs/modules/ROOT/nav.adoc
+++ b/docs/modules/ROOT/nav.adoc
@@ -30,6 +30,7 @@
* xref:reference/index.adoc[Reference]
** xref:reference/index.adoc[Extensions]
// extensions: START
+*** xref:reference/extensions/a2a.adoc[A2A]
*** xref:reference/extensions/amqp.adoc[AMQP]
*** xref:reference/extensions/as2.adoc[AS2]
*** xref:reference/extensions/asn1.adoc[ASN.1 File]
diff --git a/docs/modules/ROOT/pages/reference/extensions/a2a.adoc
b/docs/modules/ROOT/pages/reference/extensions/a2a.adoc
new file mode 100644
index 0000000000..7ff4ebd1e2
--- /dev/null
+++ b/docs/modules/ROOT/pages/reference/extensions/a2a.adoc
@@ -0,0 +1,61 @@
+// Do not edit directly!
+// This file was generated by
camel-quarkus-maven-plugin:update-extension-doc-page
+[id="extensions-a2a"]
+= A2A
+:linkattrs:
+:cq-artifact-id: camel-quarkus-a2a
+:cq-native-supported: true
+:cq-status: Preview
+:cq-status-deprecation: Preview
+:cq-description: A2A endpoint for agent-to-agent communication.
+:cq-deprecated: false
+:cq-jvm-since: 3.37.0
+:cq-native-since: 3.37.0
+
+ifeval::[{doc-show-badges} == true]
+[.badges]
+[.badge-key]##JVM since##[.badge-supported]##3.37.0## [.badge-key]##Native
since##[.badge-supported]##3.37.0##
+endif::[]
+
+A2A endpoint for agent-to-agent communication.
+
+[id="extensions-a2a-whats-inside"]
+== What's inside
+
+* xref:{cq-camel-components}::a2a-component.adoc[A2A component], URI syntax:
`a2a:agentCardSource`
+
+Please refer to the above link for usage and configuration details.
+
+[id="extensions-a2a-maven-coordinates"]
+== Maven coordinates
+
+https://{link-quarkus-code-generator}/?extension-search=camel-quarkus-a2a[Create
a new project with this extension on {link-quarkus-code-generator},
window="_blank"]
+
+Or add the coordinates to your existing project:
+
+[source,xml]
+----
+<dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-a2a</artifactId>
+</dependency>
+----
+ifeval::[{doc-show-user-guide-link} == true]
+Check the xref:user-guide/index.adoc[User guide] for more information about
writing Camel Quarkus applications.
+endif::[]
+
+[id="extensions-a2a-usage"]
+== Usage
+[id="extensions-a2a-usage-agent-card-json-files-in-native-mode"]
+=== Agent Card JSON files in native mode
+
+If you use JSON files to define agent cards that are loaded from the
classpath, you must ensure they are included in the native image.
+Add the `quarkus.native.resources.includes` configuration property to
`application.properties`. For example:
+
+[source,properties]
+----
+quarkus.native.resources.includes=cards/*.json
+----
+
+More information about selecting resources for inclusion in the native
executable can be found at
xref:user-guide/native-mode.adoc#embedding-resource-in-native-executable[Embedding
resources in native executable].
+
diff --git a/extensions/a2a/deployment/pom.xml
b/extensions/a2a/deployment/pom.xml
new file mode 100644
index 0000000000..09a1907a71
--- /dev/null
+++ b/extensions/a2a/deployment/pom.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-a2a-parent</artifactId>
+ <version>3.37.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>camel-quarkus-a2a-deployment</artifactId>
+ <name>Camel Quarkus :: A2A :: Deployment</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-core-deployment</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-platform-http-deployment</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-jackson-deployment</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-a2a</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <annotationProcessorPaths>
+ <path>
+ <groupId>io.quarkus</groupId>
+
<artifactId>quarkus-extension-processor</artifactId>
+ <version>${quarkus.version}</version>
+ </path>
+ </annotationProcessorPaths>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git
a/extensions/a2a/deployment/src/main/java/org/apache/camel/quarkus/component/a2a/deployment/A2aProcessor.java
b/extensions/a2a/deployment/src/main/java/org/apache/camel/quarkus/component/a2a/deployment/A2aProcessor.java
new file mode 100644
index 0000000000..85d6c331ea
--- /dev/null
+++
b/extensions/a2a/deployment/src/main/java/org/apache/camel/quarkus/component/a2a/deployment/A2aProcessor.java
@@ -0,0 +1,65 @@
+/*
+ * 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.quarkus.component.a2a.deployment;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.annotations.ExecutionTime;
+import io.quarkus.deployment.annotations.Record;
+import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
+import io.quarkus.deployment.builditem.FeatureBuildItem;
+import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
+import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild;
+import org.apache.camel.component.a2a.A2AComponent;
+import org.apache.camel.quarkus.component.a2a.A2aRecorder;
+import org.apache.camel.quarkus.core.deployment.spi.CamelRuntimeBeanBuildItem;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.DotName;
+
+class A2aProcessor {
+
+ private static final String FEATURE = "camel-a2a";
+
+ @BuildStep
+ FeatureBuildItem feature() {
+ return new FeatureBuildItem(FEATURE);
+ }
+
+ @BuildStep
+ @Record(ExecutionTime.RUNTIME_INIT)
+ CamelRuntimeBeanBuildItem configureA2aComponent(A2aRecorder recorder) {
+ return new CamelRuntimeBeanBuildItem("a2a",
A2AComponent.class.getName(),
+ recorder.createA2aComponent());
+ }
+
+ @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class)
+ ReflectiveClassBuildItem
registerModelClassesForReflection(CombinedIndexBuildItem combinedIndex) {
+ Set<String> modelClasses = combinedIndex.getIndex()
+ .getClassesInPackage("org.apache.camel.component.a2a.model")
+ .stream()
+ .map(ClassInfo::name)
+ .map(DotName::toString)
+ .collect(Collectors.toSet());
+
+ return ReflectiveClassBuildItem
+ .builder(modelClasses.toArray(new String[0]))
+ .methods(true)
+ .build();
+ }
+}
diff --git a/extensions/a2a/pom.xml b/extensions/a2a/pom.xml
new file mode 100644
index 0000000000..04cf09954c
--- /dev/null
+++ b/extensions/a2a/pom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-extensions</artifactId>
+ <version>3.37.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>camel-quarkus-a2a-parent</artifactId>
+ <name>Camel Quarkus :: A2A</name>
+ <packaging>pom</packaging>
+
+ <modules>
+ <module>deployment</module>
+ <module>runtime</module>
+ </modules>
+</project>
diff --git a/extensions/a2a/runtime/pom.xml b/extensions/a2a/runtime/pom.xml
new file mode 100644
index 0000000000..151dd1a4c0
--- /dev/null
+++ b/extensions/a2a/runtime/pom.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-a2a-parent</artifactId>
+ <version>3.37.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>camel-quarkus-a2a</artifactId>
+ <name>Camel Quarkus :: A2A :: Runtime</name>
+ <description>A2A endpoint for agent-to-agent communication.</description>
+
+ <properties>
+ <camel.quarkus.jvmSince>3.37.0</camel.quarkus.jvmSince>
+ <camel.quarkus.nativeSince>3.37.0</camel.quarkus.nativeSince>
+ <quarkus.metadata.status>preview</quarkus.metadata.status>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-a2a</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-platform-http</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-jackson</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-extension-maven-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <annotationProcessorPaths>
+ <path>
+ <groupId>io.quarkus</groupId>
+
<artifactId>quarkus-extension-processor</artifactId>
+ <version>${quarkus.version}</version>
+ </path>
+ </annotationProcessorPaths>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+
+ <profiles>
+ <profile>
+ <id>full</id>
+ <activation>
+ <property>
+ <name>!quickly</name>
+ </property>
+ </activation>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>update-extension-doc-page</id>
+ <goals>
+ <goal>update-extension-doc-page</goal>
+ </goals>
+ <phase>process-classes</phase>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
diff --git a/extensions/a2a/runtime/src/main/doc/usage.adoc
b/extensions/a2a/runtime/src/main/doc/usage.adoc
new file mode 100644
index 0000000000..8f74d7a9cc
--- /dev/null
+++ b/extensions/a2a/runtime/src/main/doc/usage.adoc
@@ -0,0 +1,11 @@
+=== Agent Card JSON files in native mode
+
+If you use JSON files to define agent cards that are loaded from the
classpath, you must ensure they are included in the native image.
+Add the `quarkus.native.resources.includes` configuration property to
`application.properties`. For example:
+
+[source,properties]
+----
+quarkus.native.resources.includes=cards/*.json
+----
+
+More information about selecting resources for inclusion in the native
executable can be found at
xref:user-guide/native-mode.adoc#embedding-resource-in-native-executable[Embedding
resources in native executable].
diff --git
a/extensions/a2a/runtime/src/main/java/org/apache/camel/quarkus/component/a2a/A2aRecorder.java
b/extensions/a2a/runtime/src/main/java/org/apache/camel/quarkus/component/a2a/A2aRecorder.java
new file mode 100644
index 0000000000..355b23e63c
--- /dev/null
+++
b/extensions/a2a/runtime/src/main/java/org/apache/camel/quarkus/component/a2a/A2aRecorder.java
@@ -0,0 +1,52 @@
+/*
+ * 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.quarkus.component.a2a;
+
+import java.util.Map;
+
+import io.quarkus.runtime.RuntimeValue;
+import io.quarkus.runtime.annotations.Recorder;
+import org.apache.camel.Endpoint;
+import org.apache.camel.component.a2a.A2AComponent;
+import org.apache.camel.component.a2a.A2AConstants;
+import org.apache.camel.spi.annotations.Component;
+
+@Recorder
+public class A2aRecorder {
+
+ public RuntimeValue<A2AComponent> createA2aComponent() {
+ return new RuntimeValue<>(new CamelQuarkusA2AComponent());
+ }
+
+ @Component("a2a")
+ static final class CamelQuarkusA2AComponent extends A2AComponent {
+ @Override
+ protected Endpoint createEndpoint(String uri, String remaining,
Map<String, Object> parameters) throws Exception {
+ Object binding = parameters.get("protocolBinding");
+ if (binding != null) {
+ String value = binding.toString();
+ if (A2AConstants.PROTOCOL_REST.equalsIgnoreCase(value)
+ ||
A2AConstants.PROTOCOL_REST_ALIAS.equalsIgnoreCase(value)) {
+ throw new IllegalArgumentException(
+ "The A2A REST (HTTP+JSON) protocol binding is not
supported on Quarkus. "
+ + "Use protocolBinding=JSONRPC instead.");
+ }
+ }
+ return super.createEndpoint(uri, remaining, parameters);
+ }
+ }
+}
diff --git
a/extensions/a2a/runtime/src/main/resources/META-INF/quarkus-extension.yaml
b/extensions/a2a/runtime/src/main/resources/META-INF/quarkus-extension.yaml
new file mode 100644
index 0000000000..fcaef15c73
--- /dev/null
+++ b/extensions/a2a/runtime/src/main/resources/META-INF/quarkus-extension.yaml
@@ -0,0 +1,36 @@
+#
+# 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.
+#
+
+# This is a generated file. Do not edit directly!
+# To re-generate, run the following command from the top level directory:
+#
+# mvn -N cq:update-quarkus-metadata
+#
+---
+name: "Camel A2A"
+description: "A2A endpoint for agent-to-agent communication"
+metadata:
+ icon-url:
"https://raw.githubusercontent.com/apache/camel-website/main/antora-ui-camel/src/img/logo-d.svg"
+ sponsor: "Apache Software Foundation"
+ guide:
"https://camel.apache.org/camel-quarkus/latest/reference/extensions/a2a.html"
+ categories:
+ - "integration"
+ status: "preview"
+ integrates:
+ - name: "Camel"
+ artifact: "org.apache.camel:camel-base"
+ version: "${camel.version}"
\ No newline at end of file
diff --git a/extensions/pom.xml b/extensions/pom.xml
index 58a1390b6b..76282ca973 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -34,6 +34,7 @@
<modules>
<!-- extensions a..z; do not remove this comment, it is important when
sorting via mvn process-resources -Pformat -->
+ <module>a2a</module>
<module>activemq</module>
<module>activemq6</module>
<module>amqp</module>
diff --git a/integration-tests/a2a/pom.xml b/integration-tests/a2a/pom.xml
new file mode 100644
index 0000000000..b4e6e5894a
--- /dev/null
+++ b/integration-tests/a2a/pom.xml
@@ -0,0 +1,167 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-build-parent-it</artifactId>
+ <version>3.37.0-SNAPSHOT</version>
+ <relativePath>../../poms/build-parent-it/pom.xml</relativePath>
+ </parent>
+
+ <artifactId>camel-quarkus-integration-test-a2a</artifactId>
+ <name>Camel Quarkus :: Integration Tests :: A2A</name>
+ <description>Integration tests for Camel Quarkus A2A
extension</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-a2a</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-direct</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-oauth</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-resteasy</artifactId>
+ </dependency>
+
+ <!-- test dependencies -->
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.rest-assured</groupId>
+ <artifactId>rest-assured</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-test-keycloak-server</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>testcontainers</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <profiles>
+ <profile>
+ <id>native</id>
+ <activation>
+ <property>
+ <name>native</name>
+ </property>
+ </activation>
+ <properties>
+ <quarkus.native.enabled>true</quarkus.native.enabled>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>integration-test</goal>
+ <goal>verify</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ <profile>
+ <id>virtualDependencies</id>
+ <activation>
+ <property>
+ <name>!noVirtualDependencies</name>
+ </property>
+ </activation>
+ <dependencies>
+ <!-- The following dependencies guarantee that this module is
built after them. You can update them by running `mvn process-resources
-Pformat -N` from the source tree root directory -->
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-a2a-deployment</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>*</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-oauth-deployment</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>*</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-direct-deployment</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>*</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ </dependencies>
+ </profile>
+ <profile>
+ <id>skip-testcontainers-tests</id>
+ <activation>
+ <property>
+ <name>skip-testcontainers-tests</name>
+ </property>
+ </activation>
+ <properties>
+ <skipTests>true</skipTests>
+ </properties>
+ </profile>
+ </profiles>
+
+</project>
diff --git
a/integration-tests/a2a/src/main/java/org/apache/camel/quarkus/component/a2a/it/A2aResource.java
b/integration-tests/a2a/src/main/java/org/apache/camel/quarkus/component/a2a/it/A2aResource.java
new file mode 100644
index 0000000000..3bc3de190b
--- /dev/null
+++
b/integration-tests/a2a/src/main/java/org/apache/camel/quarkus/component/a2a/it/A2aResource.java
@@ -0,0 +1,322 @@
+/*
+ * 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.quarkus.component.a2a.it;
+
+import java.util.Iterator;
+import java.util.List;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.MediaType;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.component.a2a.A2AConstants;
+import org.apache.camel.component.a2a.model.StreamResponse;
+import org.apache.camel.component.a2a.model.Task;
+import org.apache.camel.component.a2a.model.TaskPushNotificationConfig;
+import org.apache.camel.component.a2a.model.TextPart;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+@Path("/a2a")
+@ApplicationScoped
+public class A2aResource {
+
+ @Inject
+ CamelContext camelContext;
+
+ @Inject
+ ProducerTemplate producerTemplate;
+
+ @ConfigProperty(name = "quarkus.http.test-port", defaultValue = "8081")
+ int httpPort;
+
+ @Path("/send")
+ @POST
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.APPLICATION_JSON)
+ public String sendMessage(String message) {
+ Exchange result = producerTemplate.request("direct:send-message",
exchange -> {
+ exchange.getMessage().setBody(message);
+ exchange.getMessage().setHeader("CamelA2APort", httpPort);
+ });
+ Task task = result.getMessage().getBody(Task.class);
+ return
String.format("{\"taskId\":\"%s\",\"contextId\":\"%s\",\"state\":\"%s\"}",
+ task.id(), task.contextId(), task.status().state().name());
+ }
+
+ @Path("/send-payload")
+ @POST
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.TEXT_PLAIN)
+ public String sendMessagePayload(String message) {
+ Exchange result =
producerTemplate.request("direct:send-message-payload", exchange -> {
+ exchange.getMessage().setBody(message);
+ exchange.getMessage().setHeader("CamelA2APort", httpPort);
+ });
+ return result.getMessage().getBody(String.class);
+ }
+
+ @Path("/send-raw")
+ @POST
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.TEXT_PLAIN)
+ public String sendMessageRaw(String message) {
+ Exchange result = producerTemplate.request("direct:send-message-raw",
exchange -> {
+ exchange.getMessage().setBody(message);
+ exchange.getMessage().setHeader("CamelA2APort", httpPort);
+ });
+ return result.getMessage().getBody(String.class);
+ }
+
+ @Path("/get-task")
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ public String getTask(@QueryParam("taskId") String taskId) {
+ Exchange result = producerTemplate.request("direct:get-task", exchange
-> {
+ exchange.getMessage().setHeader(A2AConstants.TASK_ID, taskId);
+ exchange.getMessage().setHeader("CamelA2APort", httpPort);
+ });
+ if (result.getException() != null) {
+ return String.format("{\"error\":\"%s\"}",
result.getException().getMessage());
+ }
+ Task task = result.getMessage().getBody(Task.class);
+ return String.format("{\"taskId\":\"%s\",\"state\":\"%s\"}",
+ task.id(), task.status().state().name());
+ }
+
+ @Path("/cancel-task")
+ @POST
+ @Produces(MediaType.APPLICATION_JSON)
+ public String cancelTask(@QueryParam("taskId") String taskId) {
+ Exchange result = producerTemplate.request("direct:cancel-task",
exchange -> {
+ exchange.getMessage().setHeader(A2AConstants.TASK_ID, taskId);
+ exchange.getMessage().setHeader("CamelA2APort", httpPort);
+ });
+ if (result.getException() != null) {
+ return String.format("{\"error\":\"%s\"}",
result.getException().getMessage());
+ }
+ Task task = result.getMessage().getBody(Task.class);
+ return String.format("{\"taskId\":\"%s\",\"state\":\"%s\"}",
+ task.id(), task.status().state().name());
+ }
+
+ @Path("/create-rest-endpoint")
+ @GET
+ @Produces(MediaType.TEXT_PLAIN)
+ public String createRestEndpoint() {
+ try {
+
camelContext.getEndpoint("a2a:test?protocolBinding=REST&validateAuth=false");
+ return "unexpected-success";
+ } catch (Exception e) {
+ Throwable cause = e;
+ while (cause.getCause() != null) {
+ cause = cause.getCause();
+ }
+ return cause.getMessage();
+ }
+ }
+
+ @Path("/send-stream")
+ @POST
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.APPLICATION_JSON)
+ public String sendMessageStream(String message) {
+ Exchange result =
producerTemplate.request("direct:send-message-stream", exchange -> {
+ exchange.getMessage().setBody(message);
+ exchange.getMessage().setHeader("CamelA2APort", httpPort);
+ });
+ if (result.getException() != null) {
+ return String.format("{\"error\":\"%s\"}",
result.getException().getMessage());
+ }
+ Iterator<StreamResponse> events =
result.getMessage().getBody(Iterator.class);
+ int statusUpdates = 0;
+ int artifactUpdates = 0;
+ while (events.hasNext()) {
+ StreamResponse event = events.next();
+ if (event.getStatusUpdate() != null) {
+ statusUpdates++;
+ }
+ if (event.getArtifactUpdate() != null) {
+ artifactUpdates++;
+ }
+ }
+ return String.format("{\"statusUpdates\":%d,\"artifactUpdates\":%d}",
statusUpdates, artifactUpdates);
+ }
+
+ @Path("/list-tasks")
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @SuppressWarnings("unchecked")
+ public String listTasks(@QueryParam("contextId") String contextId) {
+ Exchange result = producerTemplate.request("direct:list-tasks",
exchange -> {
+ if (contextId != null) {
+ exchange.getMessage().setHeader(A2AConstants.LIST_CONTEXT_ID,
contextId);
+ }
+ exchange.getMessage().setHeader("CamelA2APort", httpPort);
+ });
+ if (result.getException() != null) {
+ return String.format("{\"error\":\"%s\"}",
result.getException().getMessage());
+ }
+ Object body = result.getMessage().getBody();
+ if (body instanceof List) {
+ return String.format("{\"count\":%d}", ((List<Task>) body).size());
+ }
+ return "{\"count\":0}";
+ }
+
+ @Path("/send-context")
+ @POST
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.APPLICATION_JSON)
+ public String sendMessageWithContext(String message,
@QueryParam("contextId") String contextId) {
+ Exchange result = producerTemplate.request("direct:send-message",
exchange -> {
+ exchange.getMessage().setBody(message);
+ exchange.getMessage().setHeader("CamelA2APort", httpPort);
+ if (contextId != null) {
+ exchange.getMessage().setHeader(A2AConstants.CONTEXT_ID,
contextId);
+ }
+ });
+ if (result.getException() != null) {
+ return String.format("{\"error\":\"%s\"}",
result.getException().getMessage());
+ }
+ Task task = result.getMessage().getBody(Task.class);
+ return
String.format("{\"taskId\":\"%s\",\"contextId\":\"%s\",\"state\":\"%s\"}",
+ task.id(), task.contextId(), task.status().state().name());
+ }
+
+ @Path("/send-pojo")
+ @POST
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.APPLICATION_JSON)
+ public String sendToPojo(String message) {
+ Exchange result = producerTemplate.request("direct:send-to-pojo",
exchange -> {
+ exchange.getMessage().setBody(message);
+ exchange.getMessage().setHeader("CamelA2APort", httpPort);
+ });
+ if (result.getException() != null) {
+ return String.format("{\"error\":\"%s\"}",
result.getException().getMessage());
+ }
+ Task task = result.getMessage().getBody(Task.class);
+ String latestText = "";
+ if (task.latest() != null && task.latest().parts() != null) {
+ for (Object part : task.latest().parts()) {
+ if (part instanceof TextPart tp) {
+ latestText = tp.text();
+ break;
+ }
+ }
+ }
+ return
String.format("{\"taskId\":\"%s\",\"state\":\"%s\",\"response\":\"%s\"}",
+ task.id(), task.status().state().name(), latestText);
+ }
+
+ @Path("/send-to-push")
+ @POST
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.APPLICATION_JSON)
+ public String sendToPush(String message) {
+ Exchange result = producerTemplate.request("direct:send-to-push",
exchange -> {
+ exchange.getMessage().setBody(message);
+ exchange.getMessage().setHeader("CamelA2APort", httpPort);
+ });
+ if (result.getException() != null) {
+ return String.format("{\"error\":\"%s\"}",
result.getException().getMessage());
+ }
+ Task task = result.getMessage().getBody(Task.class);
+ return
String.format("{\"taskId\":\"%s\",\"contextId\":\"%s\",\"state\":\"%s\"}",
+ task.id(), task.contextId(), task.status().state().name());
+ }
+
+ @Path("/push-config/create")
+ @POST
+ @Produces(MediaType.APPLICATION_JSON)
+ public String createPushConfig(@QueryParam("taskId") String taskId,
@QueryParam("url") String url) {
+ Exchange result =
producerTemplate.request("direct:push-config-create", exchange -> {
+ exchange.getMessage().setHeader(A2AConstants.TASK_ID, taskId);
+ exchange.getMessage().setHeader("CamelA2APort", httpPort);
+ TaskPushNotificationConfig config = new
TaskPushNotificationConfig();
+ config.setTaskId(taskId);
+ config.setUrl(url);
+ exchange.getMessage().setBody(config);
+ });
+ if (result.getException() != null) {
+ return String.format("{\"error\":\"%s\"}",
result.getException().getMessage());
+ }
+ TaskPushNotificationConfig config =
result.getMessage().getBody(TaskPushNotificationConfig.class);
+ return
String.format("{\"id\":\"%s\",\"taskId\":\"%s\",\"url\":\"%s\"}",
+ config.getId(), config.getTaskId(), config.getUrl());
+ }
+
+ @Path("/push-config/get")
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ public String getPushConfig(@QueryParam("taskId") String taskId,
@QueryParam("configId") String configId) {
+ Exchange result = producerTemplate.request("direct:push-config-get",
exchange -> {
+ exchange.getMessage().setHeader(A2AConstants.TASK_ID, taskId);
+ exchange.getMessage().setHeader(A2AConstants.PUSH_CONFIG_ID,
configId);
+ exchange.getMessage().setHeader("CamelA2APort", httpPort);
+ });
+ if (result.getException() != null) {
+ return String.format("{\"error\":\"%s\"}",
result.getException().getMessage());
+ }
+ TaskPushNotificationConfig config =
result.getMessage().getBody(TaskPushNotificationConfig.class);
+ return
String.format("{\"id\":\"%s\",\"taskId\":\"%s\",\"url\":\"%s\"}",
+ config.getId(), config.getTaskId(), config.getUrl());
+ }
+
+ @Path("/push-config/list")
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @SuppressWarnings("unchecked")
+ public String getPushConfigCount(@QueryParam("taskId") String taskId) {
+ Exchange result = producerTemplate.request("direct:push-config-list",
exchange -> {
+ exchange.getMessage().setHeader(A2AConstants.TASK_ID, taskId);
+ exchange.getMessage().setHeader("CamelA2APort", httpPort);
+ });
+ if (result.getException() != null) {
+ return String.format("{\"error\":\"%s\"}",
result.getException().getMessage());
+ }
+ Object body = result.getMessage().getBody();
+ if (body instanceof List) {
+ return String.format("{\"count\":%d}",
((List<TaskPushNotificationConfig>) body).size());
+ }
+ return "{\"count\":0}";
+ }
+
+ @Path("/push-config/delete")
+ @DELETE
+ @Produces(MediaType.APPLICATION_JSON)
+ public String deletePushConfig(@QueryParam("taskId") String taskId,
@QueryParam("configId") String configId) {
+ Exchange result =
producerTemplate.request("direct:push-config-delete", exchange -> {
+ exchange.getMessage().setHeader(A2AConstants.TASK_ID, taskId);
+ exchange.getMessage().setHeader(A2AConstants.PUSH_CONFIG_ID,
configId);
+ exchange.getMessage().setHeader("CamelA2APort", httpPort);
+ });
+ if (result.getException() != null) {
+ return String.format("{\"error\":\"%s\"}",
result.getException().getMessage());
+ }
+ return "{\"deleted\":true}";
+ }
+}
diff --git
a/integration-tests/a2a/src/main/java/org/apache/camel/quarkus/component/a2a/it/A2aRoutes.java
b/integration-tests/a2a/src/main/java/org/apache/camel/quarkus/component/a2a/it/A2aRoutes.java
new file mode 100644
index 0000000000..62f2e13da4
--- /dev/null
+++
b/integration-tests/a2a/src/main/java/org/apache/camel/quarkus/component/a2a/it/A2aRoutes.java
@@ -0,0 +1,151 @@
+/*
+ * 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.quarkus.component.a2a.it;
+
+import java.util.List;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.a2a.A2AConstants;
+import org.apache.camel.component.a2a.A2AProgress;
+import org.apache.camel.component.a2a.model.Artifact;
+import org.apache.camel.component.a2a.model.Message;
+import org.apache.camel.component.a2a.model.TextPart;
+
+@ApplicationScoped
+public class A2aRoutes extends RouteBuilder {
+
+ @Override
+ public void configure() throws Exception {
+ // Consumer: expose an A2A agent using JSON-RPC binding (recommended
for platform-http)
+ from("a2a:test-agent?name=Test Agent&description=A Quarkus test
agent&version=1.0.0"
+ + "&protocolBinding=JSONRPC&validateAuth=false")
+ .setBody(simple("Echo: ${body}"));
+
+ // Producer routes that call the local consumer agent
+ from("direct:send-message")
+ .toD("a2a:http://localhost:${header.CamelA2APort}"
+ + "?protocolBinding=JSONRPC&dataFormat=POJO");
+
+ from("direct:send-message-payload")
+ .toD("a2a:http://localhost:${header.CamelA2APort}"
+ + "?protocolBinding=JSONRPC&dataFormat=PAYLOAD");
+
+ from("direct:send-message-raw")
+ .toD("a2a:http://localhost:${header.CamelA2APort}"
+ + "?protocolBinding=JSONRPC&dataFormat=RAW");
+
+ from("direct:get-task")
+ .setHeader(A2AConstants.OPERATION, constant("TASK_GET"))
+ .toD("a2a:http://localhost:${header.CamelA2APort}"
+ + "?protocolBinding=JSONRPC&dataFormat=POJO");
+
+ from("direct:cancel-task")
+ .setHeader(A2AConstants.OPERATION, constant("TASK_CANCEL"))
+ .toD("a2a:http://localhost:${header.CamelA2APort}"
+ + "?protocolBinding=JSONRPC&dataFormat=POJO");
+
+ // Streaming consumer: emits SSE progress events and an artifact
(separate basePath to avoid route collision)
+
from("a2a:classpath:cards/streaming-agent-card.json?protocolBinding=JSONRPC&validateAuth=false&basePath=/streaming")
+ .process(exchange -> {
+ A2AProgress.emit(exchange, "Step 1 complete");
+ A2AProgress.emit(exchange, "Step 2 complete");
+ Artifact artifact = Artifact.builder()
+ .artifactId("art-1")
+ .name("test-artifact")
+ .parts(List.of(new TextPart("artifact content")))
+ .build();
+ A2AProgress.emitArtifact(exchange, artifact, false, true);
+ })
+ .setBody(constant("Streaming done"));
+
+ // Push notification consumer: enables push config CRUD testing
+
from("a2a:classpath:cards/push-agent-card.json?protocolBinding=JSONRPC&validateAuth=false"
+ + "&basePath=/push&allowLocalWebhookUrls=true")
+ .setBody(simple("Push Echo: ${body}"));
+
+ // Full-feature consumer: rich agent card with all fields for
serialization testing
+
from("a2a:classpath:cards/full-agent-card.json?protocolBinding=JSONRPC&validateAuth=false&basePath=/full")
+ .setBody(simple("Full Echo: ${body}"));
+
+ // POJO data format consumer: receives Message object body
+ from("a2a:pojo-agent?name=POJO Agent&description=POJO data format
test&version=1.0.0"
+ +
"&protocolBinding=JSONRPC&validateAuth=false&basePath=/pojo&dataFormat=POJO")
+ .process(exchange -> {
+ Message msg = exchange.getMessage().getBody(Message.class);
+ exchange.getMessage().setBody(
+ String.format("role=%s,parts=%d",
msg.role().getValue(), msg.parts().size()));
+ });
+
+ // Streaming consumer using ${a2a:emit()} Simple language function
+
from("a2a:classpath:cards/emit-agent-card.json?protocolBinding=JSONRPC&validateAuth=false&basePath=/emit")
+ .script(simple("${a2a:emit('Simple emit step 1')}"))
+ .script(simple("${a2a:emit('Simple emit step 2')}"))
+ .setBody(constant("Emit done"));
+
+ // API key authenticated consumer
+
from("a2a:classpath:cards/apikey-agent-card.json?protocolBinding=JSONRPC"
+ +
"&basePath=/apikey&apiKey=test-secret-key&apiKeyHeader=X-API-Key")
+ .setBody(simple("Authenticated: ${body}"));
+
+ // OAuth authenticated consumer
+
from("a2a:classpath:cards/oauth-agent-card.json?protocolBinding=JSONRPC&basePath=/oauth&oauthProfile=a2a-test")
+ .setBody(simple("OAuth: ${body}"));
+
+ // Extension-aware consumer
+
from("a2a:classpath:cards/extension-agent-card.json?protocolBinding=JSONRPC&validateAuth=false&basePath=/ext")
+ .setBody(simple("Extension: ${header.X-Extension-Tracking}
${body}"));
+
+ from("direct:list-tasks")
+ .setHeader(A2AConstants.OPERATION, constant("TASK_LIST"))
+ .toD("a2a:http://localhost:${header.CamelA2APort}"
+ + "?protocolBinding=JSONRPC&dataFormat=POJO");
+
+ from("direct:send-to-push")
+ .toD("a2a:http://localhost:${header.CamelA2APort}/push"
+ + "?protocolBinding=JSONRPC&dataFormat=POJO");
+
+ from("direct:send-to-pojo")
+ .toD("a2a:http://localhost:${header.CamelA2APort}/pojo"
+ + "?protocolBinding=JSONRPC&dataFormat=POJO");
+
+ from("direct:push-config-create")
+ .setHeader(A2AConstants.OPERATION,
constant("PUSH_CONFIG_CREATE"))
+ .toD("a2a:http://localhost:${header.CamelA2APort}/push"
+ + "?protocolBinding=JSONRPC&dataFormat=POJO");
+
+ from("direct:push-config-get")
+ .setHeader(A2AConstants.OPERATION, constant("PUSH_CONFIG_GET"))
+ .toD("a2a:http://localhost:${header.CamelA2APort}/push"
+ + "?protocolBinding=JSONRPC&dataFormat=POJO");
+
+ from("direct:push-config-list")
+ .setHeader(A2AConstants.OPERATION,
constant("PUSH_CONFIG_LIST"))
+ .toD("a2a:http://localhost:${header.CamelA2APort}/push"
+ + "?protocolBinding=JSONRPC&dataFormat=POJO");
+
+ from("direct:push-config-delete")
+ .setHeader(A2AConstants.OPERATION,
constant("PUSH_CONFIG_DELETE"))
+ .toD("a2a:http://localhost:${header.CamelA2APort}/push"
+ + "?protocolBinding=JSONRPC&dataFormat=POJO");
+
+ from("direct:send-message-stream")
+ .setHeader(A2AConstants.OPERATION, constant("MESSAGE_STREAM"))
+ .toD("a2a:http://localhost:${header.CamelA2APort}/streaming"
+ + "?protocolBinding=JSONRPC&dataFormat=POJO");
+ }
+}
diff --git
a/integration-tests/a2a/src/main/java/org/apache/camel/quarkus/component/a2a/it/TestExtensionHandler.java
b/integration-tests/a2a/src/main/java/org/apache/camel/quarkus/component/a2a/it/TestExtensionHandler.java
new file mode 100644
index 0000000000..91c518e3db
--- /dev/null
+++
b/integration-tests/a2a/src/main/java/org/apache/camel/quarkus/component/a2a/it/TestExtensionHandler.java
@@ -0,0 +1,38 @@
+/*
+ * 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.quarkus.component.a2a.it;
+
+import io.smallrye.common.annotation.Identifier;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.apache.camel.Exchange;
+import org.apache.camel.component.a2a.extension.A2AExtensionHandler;
+import org.apache.camel.component.a2a.model.AgentExtension;
+
+@ApplicationScoped
+@Identifier("testTrackingExtension")
+public class TestExtensionHandler implements A2AExtensionHandler {
+
+ @Override
+ public String extensionUri() {
+ return "urn:test:tracking";
+ }
+
+ @Override
+ public void beforeRoute(Exchange exchange, AgentExtension extension)
throws Exception {
+ exchange.getMessage().setHeader("X-Extension-Tracking", "active");
+ }
+}
diff --git a/integration-tests/a2a/src/main/resources/application.properties
b/integration-tests/a2a/src/main/resources/application.properties
new file mode 100644
index 0000000000..975c00a35e
--- /dev/null
+++ b/integration-tests/a2a/src/main/resources/application.properties
@@ -0,0 +1,24 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+
+quarkus.native.resources.includes=cards/*.json
+
+camel.oauth.a2a-test.base-uri=${cq.a2a.test.keycloak.url}
+camel.oauth.a2a-test.client-id=camel-client
+camel.oauth.a2a-test.client-secret=camel-client-secret
+camel.oauth.a2a-test.allow-insecure-http=true
+camel.oauth.a2a-test.allow-missing-audience=true
diff --git
a/integration-tests/a2a/src/main/resources/cards/apikey-agent-card.json
b/integration-tests/a2a/src/main/resources/cards/apikey-agent-card.json
new file mode 100644
index 0000000000..49253ea3ec
--- /dev/null
+++ b/integration-tests/a2a/src/main/resources/cards/apikey-agent-card.json
@@ -0,0 +1,28 @@
+{
+ "name": "API Key Agent",
+ "description": "A test agent with API key authentication",
+ "version": "1.0.0",
+ "securitySchemes": {
+ "apikey": {
+ "apiKeySecurityScheme": {
+ "location": "header",
+ "name": "X-API-Key"
+ }
+ }
+ },
+ "securityRequirements": [
+ {
+ "schemes": {
+ "apikey": {
+ "list": []
+ }
+ }
+ }
+ ],
+ "supportedInterfaces": [
+ {
+ "url": "http://localhost:8080",
+ "protocolBinding": "JSONRPC"
+ }
+ ]
+}
diff --git
a/integration-tests/a2a/src/main/resources/cards/emit-agent-card.json
b/integration-tests/a2a/src/main/resources/cards/emit-agent-card.json
new file mode 100644
index 0000000000..98fd8985b2
--- /dev/null
+++ b/integration-tests/a2a/src/main/resources/cards/emit-agent-card.json
@@ -0,0 +1,14 @@
+{
+ "name": "Emit Test Agent",
+ "description": "A test agent using a2a:emit Simple language function",
+ "version": "1.0.0",
+ "capabilities": {
+ "streaming": true
+ },
+ "supportedInterfaces": [
+ {
+ "url": "http://localhost:8080",
+ "protocolBinding": "JSONRPC"
+ }
+ ]
+}
diff --git
a/integration-tests/a2a/src/main/resources/cards/extension-agent-card.json
b/integration-tests/a2a/src/main/resources/cards/extension-agent-card.json
new file mode 100644
index 0000000000..f262e86b39
--- /dev/null
+++ b/integration-tests/a2a/src/main/resources/cards/extension-agent-card.json
@@ -0,0 +1,19 @@
+{
+ "name": "Extension Test Agent",
+ "description": "A test agent with A2A protocol extensions",
+ "version": "1.0.0",
+ "capabilities": {
+ "extensions": [
+ {
+ "uri": "urn:test:tracking",
+ "description": "Test tracking extension"
+ }
+ ]
+ },
+ "supportedInterfaces": [
+ {
+ "url": "http://localhost:8080",
+ "protocolBinding": "JSONRPC"
+ }
+ ]
+}
diff --git
a/integration-tests/a2a/src/main/resources/cards/full-agent-card.json
b/integration-tests/a2a/src/main/resources/cards/full-agent-card.json
new file mode 100644
index 0000000000..78e0a67c54
--- /dev/null
+++ b/integration-tests/a2a/src/main/resources/cards/full-agent-card.json
@@ -0,0 +1,36 @@
+{
+ "name": "Full Feature Agent",
+ "description": "A Quarkus test agent with all features",
+ "version": "2.0.0",
+ "provider": {
+ "name": "Test Provider",
+ "url": "https://example.com/provider"
+ },
+ "capabilities": {
+ "streaming": true,
+ "pushNotifications": true
+ },
+ "skills": [
+ {
+ "id": "echo",
+ "name": "Echo Skill",
+ "description": "Echoes back the input message",
+ "tags": ["test", "echo"],
+ "inputModes": ["text/plain", "application/json"],
+ "outputModes": ["text/plain"]
+ },
+ {
+ "id": "transform",
+ "name": "Transform Skill",
+ "description": "Transforms input data"
+ }
+ ],
+ "supportedInterfaces": [
+ {
+ "url": "http://localhost:8080",
+ "protocolBinding": "JSONRPC"
+ }
+ ],
+ "defaultInputModes": ["text/plain"],
+ "defaultOutputModes": ["text/plain"]
+}
diff --git
a/integration-tests/a2a/src/main/resources/cards/oauth-agent-card.json
b/integration-tests/a2a/src/main/resources/cards/oauth-agent-card.json
new file mode 100644
index 0000000000..1f7ee109d1
--- /dev/null
+++ b/integration-tests/a2a/src/main/resources/cards/oauth-agent-card.json
@@ -0,0 +1,27 @@
+{
+ "name": "OAuth Agent",
+ "description": "A test agent with OAuth2 authentication",
+ "version": "1.0.0",
+ "securitySchemes": {
+ "oauth2": {
+ "openIdConnectSecurityScheme": {
+ "openIdConnectUrl":
"http://placeholder/.well-known/openid-configuration"
+ }
+ }
+ },
+ "securityRequirements": [
+ {
+ "schemes": {
+ "oauth2": {
+ "list": []
+ }
+ }
+ }
+ ],
+ "supportedInterfaces": [
+ {
+ "url": "http://localhost:8080",
+ "protocolBinding": "JSONRPC"
+ }
+ ]
+}
diff --git
a/integration-tests/a2a/src/main/resources/cards/push-agent-card.json
b/integration-tests/a2a/src/main/resources/cards/push-agent-card.json
new file mode 100644
index 0000000000..41bfc307be
--- /dev/null
+++ b/integration-tests/a2a/src/main/resources/cards/push-agent-card.json
@@ -0,0 +1,14 @@
+{
+ "name": "Push Test Agent",
+ "description": "A Quarkus test agent with push notification support",
+ "version": "1.0.0",
+ "capabilities": {
+ "pushNotifications": true
+ },
+ "supportedInterfaces": [
+ {
+ "url": "http://localhost:8080",
+ "protocolBinding": "JSONRPC"
+ }
+ ]
+}
diff --git
a/integration-tests/a2a/src/main/resources/cards/streaming-agent-card.json
b/integration-tests/a2a/src/main/resources/cards/streaming-agent-card.json
new file mode 100644
index 0000000000..4d44c8b2eb
--- /dev/null
+++ b/integration-tests/a2a/src/main/resources/cards/streaming-agent-card.json
@@ -0,0 +1,14 @@
+{
+ "name": "Streaming Test Agent",
+ "description": "A Quarkus test agent with streaming support",
+ "version": "1.0.0",
+ "capabilities": {
+ "streaming": true
+ },
+ "supportedInterfaces": [
+ {
+ "url": "http://localhost:8081/streaming",
+ "protocolBinding": "JSONRPC"
+ }
+ ]
+}
diff --git
a/integration-tests/a2a/src/test/java/org/apache/camel/quarkus/component/a2a/it/A2aIT.java
b/integration-tests/a2a/src/test/java/org/apache/camel/quarkus/component/a2a/it/A2aIT.java
new file mode 100644
index 0000000000..716c749b03
--- /dev/null
+++
b/integration-tests/a2a/src/test/java/org/apache/camel/quarkus/component/a2a/it/A2aIT.java
@@ -0,0 +1,24 @@
+/*
+ * 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.quarkus.component.a2a.it;
+
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+
+@QuarkusIntegrationTest
+class A2aIT extends A2aTest {
+
+}
diff --git
a/integration-tests/a2a/src/test/java/org/apache/camel/quarkus/component/a2a/it/A2aKeycloakTestResource.java
b/integration-tests/a2a/src/test/java/org/apache/camel/quarkus/component/a2a/it/A2aKeycloakTestResource.java
new file mode 100644
index 0000000000..783d84734f
--- /dev/null
+++
b/integration-tests/a2a/src/test/java/org/apache/camel/quarkus/component/a2a/it/A2aKeycloakTestResource.java
@@ -0,0 +1,63 @@
+/*
+ * 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.quarkus.component.a2a.it;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Map;
+
+import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
+import io.quarkus.test.keycloak.server.KeycloakContainer;
+import org.eclipse.microprofile.config.ConfigProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.output.Slf4jLogConsumer;
+import org.testcontainers.utility.MountableFile;
+
+public class A2aKeycloakTestResource implements
QuarkusTestResourceLifecycleManager {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(A2aKeycloakTestResource.class);
+ private static final String REALM_NAME = "camel";
+
+ private KeycloakContainer keycloak;
+
+ @Override
+ public Map<String, String> start() {
+ System.setProperty("keycloak.docker.image",
+
ConfigProvider.getConfig().getValue("keycloak.container.image", String.class));
+
+ keycloak = new KeycloakContainer()
+ .withStartupTimeout(Duration.ofMinutes(5))
+ .withLogConsumer(new Slf4jLogConsumer(LOG))
+ .withCopyFileToContainer(
+
MountableFile.forClasspathResource("/camel-realm.json"),
+ "/opt/keycloak/data/import/camel-realm.json")
+ .withCommand("start", "--import-realm", "--verbose");
+
+ keycloak.start();
+
+ return Collections.singletonMap("cq.a2a.test.keycloak.url",
+ keycloak.getServerUrl() + "/realms/" + REALM_NAME);
+ }
+
+ @Override
+ public void stop() {
+ if (keycloak != null) {
+ keycloak.stop();
+ }
+ }
+}
diff --git
a/integration-tests/a2a/src/test/java/org/apache/camel/quarkus/component/a2a/it/A2aTest.java
b/integration-tests/a2a/src/test/java/org/apache/camel/quarkus/component/a2a/it/A2aTest.java
new file mode 100644
index 0000000000..3cae2712c6
--- /dev/null
+++
b/integration-tests/a2a/src/test/java/org/apache/camel/quarkus/component/a2a/it/A2aTest.java
@@ -0,0 +1,538 @@
+/*
+ * 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.quarkus.component.a2a.it;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import org.apache.camel.component.a2a.A2AConstants;
+import org.eclipse.microprofile.config.ConfigProvider;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@QuarkusTest
+@QuarkusTestResource(A2aKeycloakTestResource.class)
+class A2aTest {
+
+ @Test
+ void agentCardServed() {
+ RestAssured.get("/.well-known/agent-card.json")
+ .then()
+ .statusCode(200)
+ .body(
+ "name", is("Test Agent"),
+ "version", is("1.0.0"),
+ "description", is("A Quarkus test agent"));
+ }
+
+ @Test
+ void sendMessageJsonRpc() {
+ RestAssured.given()
+ .contentType("application/json")
+ .body(jsonRpcSendMessage("msg-1", "req-1", "{\"text\":\"Hello
A2A\"}"))
+ .post("/")
+ .then()
+ .statusCode(200)
+ .body(
+ "jsonrpc", is("2.0"),
+ "result.task.id", notNullValue(),
+ "result.task.contextId", notNullValue(),
+ "id", is("req-1"));
+ }
+
+ @Test
+ void producerSendMessagePojo() {
+ RestAssured.given()
+ .contentType(ContentType.TEXT)
+ .body("Hello from producer")
+ .post("/a2a/send")
+ .then()
+ .statusCode(200)
+ .body(
+ "taskId", notNullValue(),
+ "contextId", notNullValue(),
+ "state", is("COMPLETED"));
+ }
+
+ @Test
+ void producerSendMessagePayloadDataFormat() {
+ String response = RestAssured.given()
+ .contentType(ContentType.TEXT)
+ .body("Hello payload")
+ .post("/a2a/send-payload")
+ .then()
+ .statusCode(200)
+ .extract().asString();
+
+ assertTrue(
+ response.contains("Echo:"),
+ "Expected PAYLOAD response to contain echoed text, got: " +
response);
+ }
+
+ @Test
+ void producerSendMessageRawDataFormat() {
+ String response = RestAssured.given()
+ .contentType(ContentType.TEXT)
+ .body("Hello raw")
+ .post("/a2a/send-raw")
+ .then()
+ .statusCode(200)
+ .extract().asString();
+
+ assertTrue(
+ response.contains("\"task\"") ||
response.contains("\"result\""),
+ "Expected RAW response to contain JSON structure, got: " +
response);
+ }
+
+ @Test
+ void producerGetTask() {
+ String taskId = RestAssured.given()
+ .contentType(ContentType.TEXT)
+ .body("Create task for get")
+ .post("/a2a/send")
+ .then()
+ .statusCode(200)
+ .extract()
+ .jsonPath()
+ .getString("taskId");
+
+ RestAssured.given()
+ .queryParam("taskId", taskId)
+ .get("/a2a/get-task")
+ .then()
+ .statusCode(200)
+ .body(
+ "taskId", is(taskId),
+ "state", is("COMPLETED"));
+ }
+
+ @Test
+ void cancelCompletedTaskReturnsError() {
+ // The echo agent completes tasks immediately, so the task is already
in COMPLETED state
+ // when we attempt to cancel it — the A2A protocol rejects canceling a
completed task
+ String taskId = RestAssured.given()
+ .contentType(ContentType.TEXT)
+ .body("Create task for cancel")
+ .post("/a2a/send")
+ .then()
+ .statusCode(200)
+ .extract()
+ .jsonPath()
+ .getString("taskId");
+
+ RestAssured.given()
+ .queryParam("taskId", taskId)
+ .post("/a2a/cancel-task")
+ .then()
+ .statusCode(200)
+ .body("error", notNullValue());
+ }
+
+ @Test
+ void sendStreamingMessageJsonRpc() {
+ String response = RestAssured.given()
+ .contentType("application/json")
+ .body(jsonRpcStreamMessage("msg-stream", "req-stream", "Stream
this"))
+ .post("/streaming/")
+ .then()
+ .statusCode(200)
+ .header("Content-Type", "text/event-stream")
+ .extract().asString();
+
+ String[] events = response.split("\n\n");
+ assertTrue(events.length >= 3,
+ "Expected at least 3 SSE events (submitted + 2 progress +
completed), got "
+ + events.length + ": " + response);
+
+ for (String event : events) {
+ assertTrue(event.startsWith("data: "),
+ "SSE event should start with 'data: ', got: " + event);
+ }
+
+ assertTrue(response.contains("Step 1 complete"),
+ "Expected progress event 'Step 1 complete' in response: " +
response);
+ assertTrue(response.contains("Step 2 complete"),
+ "Expected progress event 'Step 2 complete' in response: " +
response);
+ }
+
+ @Test
+ void restProtocolBindingRejected() {
+ RestAssured.get("/a2a/create-rest-endpoint")
+ .then()
+ .statusCode(200)
+ .body(containsString("REST (HTTP+JSON) protocol binding is not
supported on Quarkus"));
+ }
+
+ @Test
+ void sendMessageWithDataPart() {
+ RestAssured.given()
+ .contentType("application/json")
+ .body(jsonRpcSendMessage("msg-data", "req-data",
+
"{\"kind\":\"data\",\"data\":{\"key\":\"value\",\"count\":42}}"))
+ .post("/")
+ .then()
+ .statusCode(200)
+ .body(
+ "jsonrpc", is("2.0"),
+ "result.task.id", notNullValue(),
+ "result.task.status.state", is("TASK_STATE_COMPLETED"),
+ "id", is("req-data"));
+ }
+
+ @Test
+ void sendMessageWithFilePart() {
+ RestAssured.given()
+ .contentType("application/json")
+ .body(jsonRpcSendMessage("msg-file", "req-file",
+
"{\"kind\":\"file\",\"raw\":\"aGVsbG8=\",\"mediaType\":\"text/plain\",\"filename\":\"test.txt\"}"))
+ .post("/")
+ .then()
+ .statusCode(200)
+ .body(
+ "jsonrpc", is("2.0"),
+ "result.task.id", notNullValue(),
+ "result.task.status.state", is("TASK_STATE_COMPLETED"),
+ "id", is("req-file"));
+ }
+
+ @Test
+ void fullAgentCardServed() {
+ RestAssured.get("/full/.well-known/agent-card.json")
+ .then()
+ .statusCode(200)
+ .body(
+ "name", is("Full Feature Agent"),
+ "version", is("2.0.0"),
+ "description", is("A Quarkus test agent with all
features"),
+ "provider.name", is("Test Provider"),
+ "provider.url", is("https://example.com/provider"),
+ "capabilities.streaming", is(true),
+ "capabilities.pushNotifications", is(true),
+ "skills[0].id", is("echo"),
+ "skills[0].name", is("Echo Skill"),
+ "skills[0].tags[0]", is("test"),
+ "skills[0].inputModes[0]", is("text/plain"),
+ "skills[1].id", is("transform"),
+ "defaultInputModes[0]", is("text/plain"),
+ "defaultOutputModes[0]", is("text/plain"));
+ }
+
+ @Test
+ void producerListTasks() {
+ RestAssured.given()
+ .contentType(ContentType.TEXT)
+ .body("Task for list 1")
+ .post("/a2a/send")
+ .then()
+ .statusCode(200);
+ RestAssured.given()
+ .contentType(ContentType.TEXT)
+ .body("Task for list 2")
+ .post("/a2a/send")
+ .then()
+ .statusCode(200);
+
+ RestAssured.get("/a2a/list-tasks")
+ .then()
+ .statusCode(200)
+ .body("count", greaterThanOrEqualTo(2));
+ }
+
+ @Test
+ void multiTurnConversation() {
+ String contextId = RestAssured.given()
+ .contentType(ContentType.TEXT)
+ .body("Turn 1")
+ .post("/a2a/send-context")
+ .then()
+ .statusCode(200)
+ .extract()
+ .jsonPath()
+ .getString("contextId");
+ assertNotNull(contextId, "First turn should return a contextId");
+
+ String contextId2 = RestAssured.given()
+ .contentType(ContentType.TEXT)
+ .body("Turn 2")
+ .queryParam("contextId", contextId)
+ .post("/a2a/send-context")
+ .then()
+ .statusCode(200)
+ .extract()
+ .jsonPath()
+ .getString("contextId");
+
+ assertEquals(contextId, contextId2, "contextId should be preserved
across turns");
+ }
+
+ @Test
+ void pushNotificationConfigCrud() {
+ String taskId = RestAssured.given()
+ .contentType("application/json")
+ .body(jsonRpcSendMessage("msg-push", "req-push",
"{\"text\":\"Push test\"}"))
+ .post("/push/")
+ .then()
+ .statusCode(200)
+ .body("result.task.id", notNullValue())
+ .extract()
+ .jsonPath()
+ .getString("result.task.id");
+ assertNotNull(taskId, "Task ID should be returned");
+
+ String configId = RestAssured.given()
+ .contentType("application/json")
+ .body(jsonRpcRequest("CreateTaskPushNotificationConfig",
"req-pc-create",
+ "\"taskId\":\"" + taskId +
"\",\"url\":\"http://localhost:9999/webhook\""))
+ .post("/push/")
+ .then()
+ .statusCode(200)
+ .body("result.url", is("http://localhost:9999/webhook"))
+ .extract()
+ .jsonPath()
+ .getString("result.id");
+ assertNotNull(configId, "Push config ID should be returned");
+
+ String listRequest = jsonRpcRequest("ListTaskPushNotificationConfigs",
"req-pc-list",
+ "\"taskId\":\"" + taskId + "\"");
+
+ RestAssured.given()
+ .contentType("application/json")
+ .body(listRequest)
+ .post("/push/")
+ .then()
+ .statusCode(200)
+ .body("result.configs.size()", is(1));
+
+ RestAssured.given()
+ .contentType("application/json")
+ .body(jsonRpcRequest("GetTaskPushNotificationConfig",
"req-pc-get",
+ "\"taskId\":\"" + taskId + "\",\"id\":\"" + configId +
"\""))
+ .post("/push/")
+ .then()
+ .statusCode(200)
+ .body("result.url", is("http://localhost:9999/webhook"));
+
+ RestAssured.given()
+ .contentType("application/json")
+ .body(jsonRpcRequest("DeleteTaskPushNotificationConfig",
"req-pc-delete",
+ "\"taskId\":\"" + taskId + "\",\"id\":\"" + configId +
"\""))
+ .post("/push/")
+ .then()
+ .statusCode(200);
+
+ RestAssured.given()
+ .contentType("application/json")
+ .body(listRequest)
+ .post("/push/")
+ .then()
+ .statusCode(200)
+ .body("result.configs.size()", is(0));
+ }
+
+ @Test
+ void streamingWithArtifactEmission() {
+ String response = RestAssured.given()
+ .contentType("application/json")
+ .body(jsonRpcStreamMessage("msg-art", "req-art", "Stream with
artifact"))
+ .post("/streaming/")
+ .then()
+ .statusCode(200)
+ .header("Content-Type", "text/event-stream")
+ .extract().asString();
+
+ assertTrue(response.contains("artifactUpdate"),
+ "Expected artifact update event in SSE response: " + response);
+ assertTrue(response.contains("test-artifact"),
+ "Expected artifact name 'test-artifact' in SSE response: " +
response);
+ }
+
+ @Test
+ void producerStreamMessage() {
+ RestAssured.given()
+ .contentType(ContentType.TEXT)
+ .body("Stream from producer")
+ .post("/a2a/send-stream")
+ .then()
+ .statusCode(200)
+ .body(
+ "statusUpdates", greaterThanOrEqualTo(2),
+ "artifactUpdates", greaterThanOrEqualTo(1));
+ }
+
+ @Test
+ void consumerPojoDataFormat() {
+ RestAssured.given()
+ .contentType(ContentType.TEXT)
+ .body("Hello POJO")
+ .post("/a2a/send-pojo")
+ .then()
+ .statusCode(200)
+ .body(
+ "state", is("COMPLETED"),
+ "response", containsString("role=ROLE_USER"));
+ }
+
+ @Test
+ void sendMessageAsync() {
+ RestAssured.given()
+ .contentType("application/json")
+ .body("{\"jsonrpc\":\"2.0\",\"method\":\"SendMessage\","
+ +
"\"params\":{\"message\":{\"messageId\":\"msg-async\","
+ + "\"role\":\"user\",\"parts\":[{\"text\":\"Async
request\"}]},"
+ + "\"configuration\":{\"returnImmediately\":true}},"
+ + "\"id\":\"req-async\"}")
+ .post("/")
+ .then()
+ .statusCode(200)
+ .body(
+ "jsonrpc", is("2.0"),
+ "result.task.id", notNullValue(),
+ "result.task.status.state", is("TASK_STATE_SUBMITTED"),
+ "id", is("req-async"));
+ }
+
+ @Test
+ void cardFromParametersServed() {
+ RestAssured.get("/pojo/.well-known/agent-card.json")
+ .then()
+ .statusCode(200)
+ .body(
+ "name", is("POJO Agent"),
+ "description", is("POJO data format test"),
+ "version", is("1.0.0"));
+ }
+
+ @Test
+ void simpleLanguageEmitFunction() {
+ String response = RestAssured.given()
+ .contentType("application/json")
+ .body(jsonRpcStreamMessage("msg-emit", "req-emit", "Test
emit"))
+ .post("/emit/")
+ .then()
+ .statusCode(200)
+ .header("Content-Type", "text/event-stream")
+ .extract().asString();
+
+ assertTrue(response.contains("Simple emit step 1"),
+ "Expected 'Simple emit step 1' in SSE response: " + response);
+ assertTrue(response.contains("Simple emit step 2"),
+ "Expected 'Simple emit step 2' in SSE response: " + response);
+ }
+
+ @Test
+ void apiKeyAuthenticationRequired() {
+ RestAssured.given()
+ .contentType("application/json")
+ .body(jsonRpcSendMessage("msg-noauth", "req-noauth",
"{\"text\":\"No auth\"}"))
+ .post("/apikey/")
+ .then()
+ .statusCode(200)
+ .body("error", notNullValue());
+ }
+
+ @Test
+ void apiKeyAuthenticationSuccess() {
+ RestAssured.given()
+ .contentType("application/json")
+ .header("X-API-Key", "test-secret-key")
+ .body(jsonRpcSendMessage("msg-auth", "req-auth",
"{\"text\":\"With auth\"}"))
+ .post("/apikey/")
+ .then()
+ .statusCode(200)
+ .body(
+ "result.task.id", notNullValue(),
+ "id", is("req-auth"));
+ }
+
+ @Test
+ void a2aExtensionNegotiation() {
+ RestAssured.given()
+ .contentType("application/json")
+ .header(A2AConstants.HEADER_A2A_EXTENSIONS,
"urn:test:tracking")
+ .body(jsonRpcSendMessage("msg-ext", "req-ext",
"{\"text\":\"Extension test\"}"))
+ .post("/ext/")
+ .then()
+ .statusCode(200)
+ .header(A2AConstants.HEADER_A2A_EXTENSIONS,
containsString("urn:test:tracking"))
+ .body("result.task.id", notNullValue());
+ }
+
+ @Test
+ void oauthAuthenticationRequired() {
+ RestAssured.given()
+ .contentType("application/json")
+ .body(jsonRpcSendMessage("msg-notoken", "req-notoken",
"{\"text\":\"No token\"}"))
+ .post("/oauth/")
+ .then()
+ .statusCode(200)
+ .body("error", notNullValue());
+ }
+
+ @Test
+ void oauthAuthenticationSuccess() {
+ String keycloakUrl =
ConfigProvider.getConfig().getValue("cq.a2a.test.keycloak.url", String.class);
+ String tokenEndpoint = keycloakUrl + "/protocol/openid-connect/token";
+
+ String accessToken = RestAssured.given()
+ .formParam("grant_type", "client_credentials")
+ .formParam("client_id", "camel-client")
+ .formParam("client_secret", "camel-client-secret")
+ .post(tokenEndpoint)
+ .then()
+ .statusCode(200)
+ .extract()
+ .jsonPath()
+ .getString("access_token");
+ assertNotNull(accessToken, "Should obtain access token from Keycloak");
+
+ RestAssured.given()
+ .contentType("application/json")
+ .header("Authorization", "Bearer " + accessToken)
+ .body(jsonRpcSendMessage("msg-oauth", "req-oauth",
"{\"text\":\"With OAuth\"}"))
+ .post("/oauth/")
+ .then()
+ .statusCode(200)
+ .body(
+ "result.task.id", notNullValue(),
+ "id", is("req-oauth"));
+ }
+
+ private static String jsonRpcSendMessage(String messageId, String
requestId, String partJson) {
+ return jsonRpcRequest("SendMessage", requestId,
+ "\"message\":{\"messageId\":\"" + messageId + "\","
+ + "\"role\":\"user\",\"parts\":[" + partJson + "]}");
+ }
+
+ private static String jsonRpcStreamMessage(String messageId, String
requestId, String text) {
+ return jsonRpcRequest("SendStreamingMessage", requestId,
+ "\"message\":{\"messageId\":\"" + messageId + "\","
+ + "\"role\":\"user\",\"parts\":[{\"text\":\"" + text +
"\"}]}");
+ }
+
+ private static String jsonRpcRequest(String method, String requestId,
String params) {
+ return "{\"jsonrpc\":\"2.0\",\"method\":\"" + method + "\","
+ + "\"params\":{" + params + "},\"id\":\"" + requestId + "\"}";
+ }
+}
diff --git a/integration-tests/a2a/src/test/resources/camel-realm.json
b/integration-tests/a2a/src/test/resources/camel-realm.json
new file mode 100644
index 0000000000..1e2ff6d9be
--- /dev/null
+++ b/integration-tests/a2a/src/test/resources/camel-realm.json
@@ -0,0 +1,84 @@
+{
+ "realm": "camel",
+ "accessTokenLifespan": 300,
+ "ssoSessionMaxLifespan": 32140800,
+ "ssoSessionIdleTimeout": 32140800,
+ "enabled": true,
+ "sslRequired": "external",
+ "users": [
+ {
+ "username": "alice",
+ "enabled": true,
+ "firstName": "Alice",
+ "lastName": "Brown",
+ "email": "[email protected]",
+ "credentials": [
+ {
+ "type": "password",
+ "value": "alice"
+ }
+ ],
+ "realmRoles": [
+ "user"
+ ]
+ },
+ {
+ "username": "admin",
+ "enabled": true,
+ "email": "[email protected]",
+ "credentials": [
+ {
+ "type": "password",
+ "value": "admin-password"
+ }
+ ],
+ "realmRoles": [
+ "admin"
+ ],
+ "clientRoles": {
+ "realm-management": [
+ "realm-admin"
+ ]
+ }
+ }
+ ],
+ "roles": {
+ "realm": [
+ {
+ "name": "user",
+ "description": "User privileges"
+ },
+ {
+ "name": "admin",
+ "description": "Administrator privileges"
+ }
+ ],
+ "client": {}
+ },
+ "scopeMappings": [],
+ "clientScopeMappings": {},
+ "clients": [
+ {
+ "clientId": "camel-client",
+ "enabled": true,
+ "clientAuthenticatorType": "client-secret",
+ "secret": "camel-client-secret",
+ "publicClient": false,
+ "bearerOnly": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": false,
+ "serviceAccountsEnabled": true,
+ "consentRequired" : false,
+ "fullScopeAllowed" : false,
+ "redirectUris": [
+ "http://127.0.0.1:8080/auth",
+ "https://example.local/auth",
+ "https://example.k3s/auth"
+ ],
+ "attributes": {
+ "post.logout.redirect.uris":
"http://127.0.0.1:8080/##https://example.local/##https://example.k3s/"
+ }
+ }
+ ]
+}
diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml
index 9249f0365b..903d7ef9d2 100644
--- a/integration-tests/pom.xml
+++ b/integration-tests/pom.xml
@@ -48,6 +48,7 @@
<module>main-yaml</module>
<!-- extensions a..z; do not remove this comment, it is important when
sorting via mvn process-resources -Pformat -->
+ <module>a2a</module>
<module>activemq</module>
<module>activemq6</module>
<module>amqp</module>
diff --git a/poms/bom/pom.xml b/poms/bom/pom.xml
index aa9502dcd8..90d6b700d6 100644
--- a/poms/bom/pom.xml
+++ b/poms/bom/pom.xml
@@ -124,6 +124,11 @@
<!-- Dependencies a..z; do not remove this comment, it is
important when sorting via mvn process-resources -Pformat -->
<!--$ org.apache.camel $-->
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-a2a</artifactId>
+ <version>${camel.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-activemq</artifactId>
@@ -3507,6 +3512,16 @@
</dependency>
<!--$ org.apache.camel.quarkus $-->
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-a2a</artifactId>
+ <version>${camel-quarkus.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-a2a-deployment</artifactId>
+ <version>${camel-quarkus.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-activemq</artifactId>
diff --git a/poms/bom/src/main/generated/flattened-full-pom.xml
b/poms/bom/src/main/generated/flattened-full-pom.xml
index 1295116b09..5385c2ed6f 100644
--- a/poms/bom/src/main/generated/flattened-full-pom.xml
+++ b/poms/bom/src/main/generated/flattened-full-pom.xml
@@ -47,6 +47,11 @@
</distributionManagement>
<dependencyManagement>
<dependencies>
+ <dependency>
+ <groupId>org.apache.camel</groupId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
+ <artifactId>camel-a2a</artifactId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
+ <version>4.21.0</version><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
+ </dependency>
<dependency>
<groupId>org.apache.camel</groupId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
<artifactId>camel-activemq</artifactId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
@@ -3402,6 +3407,16 @@
</exclusion>
</exclusions>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
+ <artifactId>camel-quarkus-a2a</artifactId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
+ <version>3.37.0-SNAPSHOT</version><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
+ <artifactId>camel-quarkus-a2a-deployment</artifactId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
+ <version>3.37.0-SNAPSHOT</version><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
+ </dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
<artifactId>camel-quarkus-activemq</artifactId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
diff --git a/poms/bom/src/main/generated/flattened-reduced-pom.xml
b/poms/bom/src/main/generated/flattened-reduced-pom.xml
index a1a7519dc6..73e0632330 100644
--- a/poms/bom/src/main/generated/flattened-reduced-pom.xml
+++ b/poms/bom/src/main/generated/flattened-reduced-pom.xml
@@ -47,6 +47,11 @@
</distributionManagement>
<dependencyManagement>
<dependencies>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-a2a</artifactId>
+ <version>4.21.0</version>
+ </dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-activemq</artifactId>
@@ -3376,6 +3381,16 @@
</exclusion>
</exclusions>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-a2a</artifactId>
+ <version>3.37.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-a2a-deployment</artifactId>
+ <version>3.37.0-SNAPSHOT</version>
+ </dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-activemq</artifactId>
diff --git a/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml
b/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml
index 17d0d306aa..2609463e66 100644
--- a/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml
+++ b/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml
@@ -47,6 +47,11 @@
</distributionManagement>
<dependencyManagement>
<dependencies>
+ <dependency>
+ <groupId>org.apache.camel</groupId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
+ <artifactId>camel-a2a</artifactId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
+ <version>4.21.0</version><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
+ </dependency>
<dependency>
<groupId>org.apache.camel</groupId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
<artifactId>camel-activemq</artifactId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
@@ -3376,6 +3381,16 @@
</exclusion>
</exclusions>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
+ <artifactId>camel-quarkus-a2a</artifactId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
+ <version>3.37.0-SNAPSHOT</version><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
+ <artifactId>camel-quarkus-a2a-deployment</artifactId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
+ <version>3.37.0-SNAPSHOT</version><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
+ </dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
<artifactId>camel-quarkus-activemq</artifactId><!--
org.apache.camel.quarkus:camel-quarkus-bom:${project.version} -->
diff --git a/tooling/scripts/test-categories.yaml
b/tooling/scripts/test-categories.yaml
index 4ddaeb144f..d477e1b68e 100644
--- a/tooling/scripts/test-categories.yaml
+++ b/tooling/scripts/test-categories.yaml
@@ -97,6 +97,7 @@ group-04:
- observability-services
- sjms-artemis-client
group-05:
+ - a2a
- avro
- base64
- bindy