From dbe4bd57669b9c109724780f0226a7d18c6e3354 Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Wed, 29 Oct 2025 13:37:34 -0400 Subject: [PATCH 01/37] chore: Next is 0.3.1 --- client/base/pom.xml | 2 +- client/transport/grpc/pom.xml | 2 +- client/transport/jsonrpc/pom.xml | 2 +- client/transport/rest/pom.xml | 2 +- client/transport/spi/pom.xml | 2 +- common/pom.xml | 2 +- examples/cloud-deployment/server/pom.xml | 2 +- examples/helloworld/client/pom.xml | 2 +- .../java/io/a2a/examples/helloworld/HelloWorldRunner.java | 4 ++-- examples/helloworld/pom.xml | 2 +- examples/helloworld/server/pom.xml | 2 +- extras/common/pom.xml | 2 +- extras/push-notification-config-store-database-jpa/pom.xml | 2 +- extras/queue-manager-replicated/core/pom.xml | 2 +- extras/queue-manager-replicated/pom.xml | 2 +- .../queue-manager-replicated/replication-mp-reactive/pom.xml | 2 +- extras/queue-manager-replicated/tests-multi-instance/pom.xml | 2 +- .../tests-multi-instance/quarkus-app-1/pom.xml | 2 +- .../tests-multi-instance/quarkus-app-2/pom.xml | 2 +- .../tests-multi-instance/quarkus-common/pom.xml | 2 +- .../tests-multi-instance/tests/pom.xml | 2 +- extras/queue-manager-replicated/tests-single-instance/pom.xml | 2 +- extras/task-store-database-jpa/pom.xml | 2 +- http-client/pom.xml | 2 +- pom.xml | 2 +- reference/common/pom.xml | 2 +- reference/grpc/pom.xml | 2 +- reference/jsonrpc/pom.xml | 2 +- reference/rest/pom.xml | 2 +- server-common/pom.xml | 2 +- spec-grpc/pom.xml | 2 +- spec/pom.xml | 2 +- tck/pom.xml | 2 +- tests/server-common/pom.xml | 2 +- transport/grpc/pom.xml | 2 +- transport/jsonrpc/pom.xml | 2 +- transport/rest/pom.xml | 2 +- 37 files changed, 38 insertions(+), 38 deletions(-) diff --git a/client/base/pom.xml b/client/base/pom.xml index 332ddd995..31ee4acee 100644 --- a/client/base/pom.xml +++ b/client/base/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-client diff --git a/client/transport/grpc/pom.xml b/client/transport/grpc/pom.xml index ae949cc12..90499597f 100644 --- a/client/transport/grpc/pom.xml +++ b/client/transport/grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-grpc diff --git a/client/transport/jsonrpc/pom.xml b/client/transport/jsonrpc/pom.xml index 0b8e5c819..e85dd3953 100644 --- a/client/transport/jsonrpc/pom.xml +++ b/client/transport/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-jsonrpc diff --git a/client/transport/rest/pom.xml b/client/transport/rest/pom.xml index 3658a0b45..810a8115f 100644 --- a/client/transport/rest/pom.xml +++ b/client/transport/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-rest diff --git a/client/transport/spi/pom.xml b/client/transport/spi/pom.xml index b07d672d4..d5f4f6f74 100644 --- a/client/transport/spi/pom.xml +++ b/client/transport/spi/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-spi diff --git a/common/pom.xml b/common/pom.xml index 4a762745c..fa25ae67b 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT a2a-java-sdk-common diff --git a/examples/cloud-deployment/server/pom.xml b/examples/cloud-deployment/server/pom.xml index 00572ed8d..e48af5851 100644 --- a/examples/cloud-deployment/server/pom.xml +++ b/examples/cloud-deployment/server/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../../pom.xml diff --git a/examples/helloworld/client/pom.xml b/examples/helloworld/client/pom.xml index 90da6e004..e17d7a80e 100644 --- a/examples/helloworld/client/pom.xml +++ b/examples/helloworld/client/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-examples-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT a2a-java-sdk-examples-client diff --git a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java index 24b0d6159..5909a268b 100644 --- a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java +++ b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java @@ -1,6 +1,6 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.0.Final -//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.0.Final +//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.1.Beta1-SNAPSHOT +//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.1.Beta1-SNAPSHOT //SOURCES HelloWorldClient.java /** diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 66940a387..c353009d3 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../pom.xml diff --git a/examples/helloworld/server/pom.xml b/examples/helloworld/server/pom.xml index 96001f9f4..fd5641175 100644 --- a/examples/helloworld/server/pom.xml +++ b/examples/helloworld/server/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-examples-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT a2a-java-sdk-examples-server diff --git a/extras/common/pom.xml b/extras/common/pom.xml index f69af2bbf..a14956814 100644 --- a/extras/common/pom.xml +++ b/extras/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../pom.xml diff --git a/extras/push-notification-config-store-database-jpa/pom.xml b/extras/push-notification-config-store-database-jpa/pom.xml index c8fefc5e4..145735003 100644 --- a/extras/push-notification-config-store-database-jpa/pom.xml +++ b/extras/push-notification-config-store-database-jpa/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../pom.xml a2a-java-extras-push-notification-config-store-database-jpa diff --git a/extras/queue-manager-replicated/core/pom.xml b/extras/queue-manager-replicated/core/pom.xml index 4fc4550b7..95a5059e6 100644 --- a/extras/queue-manager-replicated/core/pom.xml +++ b/extras/queue-manager-replicated/core/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/pom.xml b/extras/queue-manager-replicated/pom.xml index b8c3fef98..c9d9495a3 100644 --- a/extras/queue-manager-replicated/pom.xml +++ b/extras/queue-manager-replicated/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../pom.xml diff --git a/extras/queue-manager-replicated/replication-mp-reactive/pom.xml b/extras/queue-manager-replicated/replication-mp-reactive/pom.xml index 917ea52f8..d65ce0fa3 100644 --- a/extras/queue-manager-replicated/replication-mp-reactive/pom.xml +++ b/extras/queue-manager-replicated/replication-mp-reactive/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/pom.xml index 9993f83a9..e6eef3c52 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml index ad1935815..117b5c4c0 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml index a8d6e34e4..a68d5768e 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml index 35ddc799d..617d8a0ff 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml index 94d1438bf..20593593a 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-single-instance/pom.xml b/extras/queue-manager-replicated/tests-single-instance/pom.xml index dad8ef5e0..c4e2fa065 100644 --- a/extras/queue-manager-replicated/tests-single-instance/pom.xml +++ b/extras/queue-manager-replicated/tests-single-instance/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/task-store-database-jpa/pom.xml b/extras/task-store-database-jpa/pom.xml index f7b8816a6..efa41d0e6 100644 --- a/extras/task-store-database-jpa/pom.xml +++ b/extras/task-store-database-jpa/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../pom.xml a2a-java-extras-task-store-database-jpa diff --git a/http-client/pom.xml b/http-client/pom.xml index 56bc6db24..80022d381 100644 --- a/http-client/pom.xml +++ b/http-client/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT a2a-java-sdk-http-client diff --git a/pom.xml b/pom.xml index 46d29db66..47ad8ed1c 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT pom diff --git a/reference/common/pom.xml b/reference/common/pom.xml index 9c16f1158..a269309e8 100644 --- a/reference/common/pom.xml +++ b/reference/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-reference-common diff --git a/reference/grpc/pom.xml b/reference/grpc/pom.xml index 0e84ea936..e0bfcbeec 100644 --- a/reference/grpc/pom.xml +++ b/reference/grpc/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../pom.xml diff --git a/reference/jsonrpc/pom.xml b/reference/jsonrpc/pom.xml index 9ceb7800f..bfaec81cf 100644 --- a/reference/jsonrpc/pom.xml +++ b/reference/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-reference-jsonrpc diff --git a/reference/rest/pom.xml b/reference/rest/pom.xml index 3a8416335..d1df6f193 100644 --- a/reference/rest/pom.xml +++ b/reference/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-reference-rest diff --git a/server-common/pom.xml b/server-common/pom.xml index d48f69036..96ca0d0a2 100644 --- a/server-common/pom.xml +++ b/server-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT a2a-java-sdk-server-common diff --git a/spec-grpc/pom.xml b/spec-grpc/pom.xml index e7805eb4f..fefff3998 100644 --- a/spec-grpc/pom.xml +++ b/spec-grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT a2a-java-sdk-spec-grpc diff --git a/spec/pom.xml b/spec/pom.xml index afe9f76bb..32a249568 100644 --- a/spec/pom.xml +++ b/spec/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT a2a-java-sdk-spec diff --git a/tck/pom.xml b/tck/pom.xml index de276a2bc..977cdbb45 100644 --- a/tck/pom.xml +++ b/tck/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT a2a-tck-server diff --git a/tests/server-common/pom.xml b/tests/server-common/pom.xml index ac01d5aff..51bb51640 100644 --- a/tests/server-common/pom.xml +++ b/tests/server-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-tests-server-common diff --git a/transport/grpc/pom.xml b/transport/grpc/pom.xml index 4f428d1ff..48bfaaf74 100644 --- a/transport/grpc/pom.xml +++ b/transport/grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-transport-grpc diff --git a/transport/jsonrpc/pom.xml b/transport/jsonrpc/pom.xml index 0b4bd027b..64b8d86d7 100644 --- a/transport/jsonrpc/pom.xml +++ b/transport/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-transport-jsonrpc diff --git a/transport/rest/pom.xml b/transport/rest/pom.xml index 7dc589343..cb3b00e7e 100644 --- a/transport/rest/pom.xml +++ b/transport/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.0.Final + 0.3.1.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-transport-rest From 8c87e94cc96c9c47e9916e016c7ddd47e481a2f8 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Thu, 30 Oct 2025 17:03:43 +0000 Subject: [PATCH 02/37] fix: Clean up the k8s example pom, and the extras application properties (#416) Upstream: #415 --- examples/cloud-deployment/server/pom.xml | 29 ++++--------------- .../src/main/resources/application.properties | 3 -- .../src/main/resources/application.properties | 3 -- .../src/test/resources/application.properties | 3 -- 4 files changed, 6 insertions(+), 32 deletions(-) diff --git a/examples/cloud-deployment/server/pom.xml b/examples/cloud-deployment/server/pom.xml index e48af5851..53645e005 100644 --- a/examples/cloud-deployment/server/pom.xml +++ b/examples/cloud-deployment/server/pom.xml @@ -17,7 +17,7 @@ Example demonstrating A2A agent deployment in Kubernetes with database persistence and event replication - + io.github.a2asdk a2a-java-sdk-reference-jsonrpc @@ -45,14 +45,17 @@ ${project.version} - + io.github.a2asdk a2a-java-queue-manager-replication-mp-reactive ${project.version} - + io.quarkus quarkus-messaging-kafka @@ -70,32 +73,12 @@ quarkus-hibernate-orm - - - io.quarkus - quarkus-resteasy-jackson - - io.quarkus quarkus-smallrye-health - - - jakarta.enterprise - jakarta.enterprise.cdi-api - provided - - - - - org.slf4j - slf4j-api - - - io.github.a2asdk a2a-java-sdk-client diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/src/main/resources/application.properties b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/src/main/resources/application.properties index f2d484ef3..d0692ca53 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/src/main/resources/application.properties +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/src/main/resources/application.properties @@ -1,9 +1,6 @@ # Application HTTP Port quarkus.http.port=8081 -# Select our ReplicatedQueueManager and JpaDatabaseTaskStore as the active implementations -quarkus.arc.selected-alternatives=io.a2a.extras.queuemanager.replicated.core.ReplicatedQueueManager,io.a2a.extras.taskstore.database.jpa.JpaDatabaseTaskStore - # Configure PostgreSQL database (connection details will be provided by Testcontainers) quarkus.datasource."a2a-java".db-kind=postgresql quarkus.datasource."a2a-java".jdbc.url=${DATABASE_URL:jdbc:postgresql://localhost:5432/a2adb} diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/src/main/resources/application.properties b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/src/main/resources/application.properties index ca4698aa2..0b647f3a5 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/src/main/resources/application.properties +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/src/main/resources/application.properties @@ -1,9 +1,6 @@ # Application HTTP Port quarkus.http.port=8082 -# Select our ReplicatedQueueManager and JpaDatabaseTaskStore as the active implementations -quarkus.arc.selected-alternatives=io.a2a.extras.queuemanager.replicated.core.ReplicatedQueueManager,io.a2a.extras.taskstore.database.jpa.JpaDatabaseTaskStore - # Configure PostgreSQL database (connection details will be provided by Testcontainers) quarkus.datasource."a2a-java".db-kind=postgresql quarkus.datasource."a2a-java".jdbc.url=${DATABASE_URL:jdbc:postgresql://localhost:5432/a2adb} diff --git a/extras/queue-manager-replicated/tests-single-instance/src/test/resources/application.properties b/extras/queue-manager-replicated/tests-single-instance/src/test/resources/application.properties index 1e02c4559..15aed5bc3 100644 --- a/extras/queue-manager-replicated/tests-single-instance/src/test/resources/application.properties +++ b/extras/queue-manager-replicated/tests-single-instance/src/test/resources/application.properties @@ -1,6 +1,3 @@ -# Select our ReplicatedQueueManager as the active implementation -quarkus.arc.selected-alternatives=io.a2a.extras.queuemanager.replicated.core.ReplicatedQueueManager,io.a2a.extras.taskstore.database.jpa.JpaDatabaseTaskStore - # Configure in-memory H2 database for testing quarkus.datasource."a2a-java".db-kind=h2 quarkus.datasource."a2a-java".jdbc.url=jdbc:h2:mem:test From db2bfd02b1ebacd406e86da57c09ff80a810076d Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Thu, 30 Oct 2025 17:06:17 +0000 Subject: [PATCH 03/37] fix: Update to use pull_request_target --- .github/workflows/build-with-release-profile.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-with-release-profile.yml b/.github/workflows/build-with-release-profile.yml index 2eafdcafc..699e62ac5 100644 --- a/.github/workflows/build-with-release-profile.yml +++ b/.github/workflows/build-with-release-profile.yml @@ -6,7 +6,7 @@ name: Build with '-Prelease' on: # Handle all branches for now push: - pull_request: + pull_request_target: workflow_dispatch: # Only run the latest job From db1e26635bdf929b6b5801171747d956a25ae87b Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Thu, 30 Oct 2025 17:53:26 +0000 Subject: [PATCH 04/37] fix: Changes needed to have ConfigProperty injection working in Jakarta (#418) Upstream: #417 --- .../java/io/a2a/server/util/async/AsyncExecutorProducer.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server-common/src/main/java/io/a2a/server/util/async/AsyncExecutorProducer.java b/server-common/src/main/java/io/a2a/server/util/async/AsyncExecutorProducer.java index d6f0e996e..49e69f99e 100644 --- a/server-common/src/main/java/io/a2a/server/util/async/AsyncExecutorProducer.java +++ b/server-common/src/main/java/io/a2a/server/util/async/AsyncExecutorProducer.java @@ -12,6 +12,8 @@ import jakarta.annotation.PreDestroy; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; + import org.eclipse.microprofile.config.inject.ConfigProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,12 +23,15 @@ public class AsyncExecutorProducer { private static final Logger LOGGER = LoggerFactory.getLogger(AsyncExecutorProducer.class); + @Inject // Needed to work in standard Jakarta runtimes (Quarkus skips this) @ConfigProperty(name = "a2a.executor.core-pool-size", defaultValue = "5") int corePoolSize; + @Inject // Needed to work in standard Jakarta runtimes (Quarkus skips this) @ConfigProperty(name = "a2a.executor.max-pool-size", defaultValue = "50") int maxPoolSize; + @Inject // Needed to work in standard Jakarta runtimes (Quarkus skips this) @ConfigProperty(name = "a2a.executor.keep-alive-seconds", defaultValue = "60") long keepAliveSeconds; From a2b5aa5a17933d5050e23de70a59cf6d3bd4af94 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Thu, 30 Oct 2025 21:59:08 +0000 Subject: [PATCH 05/37] chore: Release 0.3.1.Final (#419) --- client/base/pom.xml | 2 +- client/transport/grpc/pom.xml | 2 +- client/transport/jsonrpc/pom.xml | 2 +- client/transport/rest/pom.xml | 2 +- client/transport/spi/pom.xml | 2 +- common/pom.xml | 2 +- examples/cloud-deployment/server/pom.xml | 2 +- examples/helloworld/client/pom.xml | 2 +- .../java/io/a2a/examples/helloworld/HelloWorldRunner.java | 4 ++-- examples/helloworld/pom.xml | 2 +- examples/helloworld/server/pom.xml | 2 +- extras/common/pom.xml | 2 +- extras/push-notification-config-store-database-jpa/pom.xml | 2 +- extras/queue-manager-replicated/core/pom.xml | 2 +- extras/queue-manager-replicated/pom.xml | 2 +- .../queue-manager-replicated/replication-mp-reactive/pom.xml | 2 +- extras/queue-manager-replicated/tests-multi-instance/pom.xml | 2 +- .../tests-multi-instance/quarkus-app-1/pom.xml | 2 +- .../tests-multi-instance/quarkus-app-2/pom.xml | 2 +- .../tests-multi-instance/quarkus-common/pom.xml | 2 +- .../tests-multi-instance/tests/pom.xml | 2 +- extras/queue-manager-replicated/tests-single-instance/pom.xml | 2 +- extras/task-store-database-jpa/pom.xml | 2 +- http-client/pom.xml | 2 +- pom.xml | 2 +- reference/common/pom.xml | 2 +- reference/grpc/pom.xml | 2 +- reference/jsonrpc/pom.xml | 2 +- reference/rest/pom.xml | 2 +- server-common/pom.xml | 2 +- spec-grpc/pom.xml | 2 +- spec/pom.xml | 2 +- tck/pom.xml | 2 +- tests/server-common/pom.xml | 2 +- transport/grpc/pom.xml | 2 +- transport/jsonrpc/pom.xml | 2 +- transport/rest/pom.xml | 2 +- 37 files changed, 38 insertions(+), 38 deletions(-) diff --git a/client/base/pom.xml b/client/base/pom.xml index 31ee4acee..ac57a15a1 100644 --- a/client/base/pom.xml +++ b/client/base/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../pom.xml a2a-java-sdk-client diff --git a/client/transport/grpc/pom.xml b/client/transport/grpc/pom.xml index 90499597f..2d1f1a54b 100644 --- a/client/transport/grpc/pom.xml +++ b/client/transport/grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../../pom.xml a2a-java-sdk-client-transport-grpc diff --git a/client/transport/jsonrpc/pom.xml b/client/transport/jsonrpc/pom.xml index e85dd3953..0fd8e1488 100644 --- a/client/transport/jsonrpc/pom.xml +++ b/client/transport/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../../pom.xml a2a-java-sdk-client-transport-jsonrpc diff --git a/client/transport/rest/pom.xml b/client/transport/rest/pom.xml index 810a8115f..06dbd1dce 100644 --- a/client/transport/rest/pom.xml +++ b/client/transport/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../../pom.xml a2a-java-sdk-client-transport-rest diff --git a/client/transport/spi/pom.xml b/client/transport/spi/pom.xml index d5f4f6f74..a41c2a2a6 100644 --- a/client/transport/spi/pom.xml +++ b/client/transport/spi/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../../pom.xml a2a-java-sdk-client-transport-spi diff --git a/common/pom.xml b/common/pom.xml index fa25ae67b..835d5c929 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final a2a-java-sdk-common diff --git a/examples/cloud-deployment/server/pom.xml b/examples/cloud-deployment/server/pom.xml index 53645e005..382629d0f 100644 --- a/examples/cloud-deployment/server/pom.xml +++ b/examples/cloud-deployment/server/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../../pom.xml diff --git a/examples/helloworld/client/pom.xml b/examples/helloworld/client/pom.xml index e17d7a80e..51616fa42 100644 --- a/examples/helloworld/client/pom.xml +++ b/examples/helloworld/client/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-examples-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final a2a-java-sdk-examples-client diff --git a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java index 5909a268b..548e89bb3 100644 --- a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java +++ b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java @@ -1,6 +1,6 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.1.Beta1-SNAPSHOT -//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.1.Beta1-SNAPSHOT +//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.1.Final +//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.1.Final //SOURCES HelloWorldClient.java /** diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index c353009d3..cf8970e63 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../pom.xml diff --git a/examples/helloworld/server/pom.xml b/examples/helloworld/server/pom.xml index fd5641175..ca969f42d 100644 --- a/examples/helloworld/server/pom.xml +++ b/examples/helloworld/server/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-examples-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final a2a-java-sdk-examples-server diff --git a/extras/common/pom.xml b/extras/common/pom.xml index a14956814..c6851890f 100644 --- a/extras/common/pom.xml +++ b/extras/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../pom.xml diff --git a/extras/push-notification-config-store-database-jpa/pom.xml b/extras/push-notification-config-store-database-jpa/pom.xml index 145735003..5bc9371dd 100644 --- a/extras/push-notification-config-store-database-jpa/pom.xml +++ b/extras/push-notification-config-store-database-jpa/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../pom.xml a2a-java-extras-push-notification-config-store-database-jpa diff --git a/extras/queue-manager-replicated/core/pom.xml b/extras/queue-manager-replicated/core/pom.xml index 95a5059e6..683304b91 100644 --- a/extras/queue-manager-replicated/core/pom.xml +++ b/extras/queue-manager-replicated/core/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../pom.xml diff --git a/extras/queue-manager-replicated/pom.xml b/extras/queue-manager-replicated/pom.xml index c9d9495a3..cd99c6872 100644 --- a/extras/queue-manager-replicated/pom.xml +++ b/extras/queue-manager-replicated/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../pom.xml diff --git a/extras/queue-manager-replicated/replication-mp-reactive/pom.xml b/extras/queue-manager-replicated/replication-mp-reactive/pom.xml index d65ce0fa3..08ad766f0 100644 --- a/extras/queue-manager-replicated/replication-mp-reactive/pom.xml +++ b/extras/queue-manager-replicated/replication-mp-reactive/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/pom.xml index e6eef3c52..d820ad14e 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml index 117b5c4c0..1dce1207d 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml index a68d5768e..1e2091199 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml index 617d8a0ff..ce959fe7d 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml index 20593593a..af4fb9b8e 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../pom.xml diff --git a/extras/queue-manager-replicated/tests-single-instance/pom.xml b/extras/queue-manager-replicated/tests-single-instance/pom.xml index c4e2fa065..5ca8e3806 100644 --- a/extras/queue-manager-replicated/tests-single-instance/pom.xml +++ b/extras/queue-manager-replicated/tests-single-instance/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../pom.xml diff --git a/extras/task-store-database-jpa/pom.xml b/extras/task-store-database-jpa/pom.xml index efa41d0e6..9c31a264d 100644 --- a/extras/task-store-database-jpa/pom.xml +++ b/extras/task-store-database-jpa/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../pom.xml a2a-java-extras-task-store-database-jpa diff --git a/http-client/pom.xml b/http-client/pom.xml index 80022d381..61297a25c 100644 --- a/http-client/pom.xml +++ b/http-client/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final a2a-java-sdk-http-client diff --git a/pom.xml b/pom.xml index 47ad8ed1c..bca36af62 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final pom diff --git a/reference/common/pom.xml b/reference/common/pom.xml index a269309e8..ec4c6686a 100644 --- a/reference/common/pom.xml +++ b/reference/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../pom.xml a2a-java-sdk-reference-common diff --git a/reference/grpc/pom.xml b/reference/grpc/pom.xml index e0bfcbeec..1569722e6 100644 --- a/reference/grpc/pom.xml +++ b/reference/grpc/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../pom.xml diff --git a/reference/jsonrpc/pom.xml b/reference/jsonrpc/pom.xml index bfaec81cf..30fc015c2 100644 --- a/reference/jsonrpc/pom.xml +++ b/reference/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../pom.xml a2a-java-sdk-reference-jsonrpc diff --git a/reference/rest/pom.xml b/reference/rest/pom.xml index d1df6f193..4c11e80bb 100644 --- a/reference/rest/pom.xml +++ b/reference/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../pom.xml a2a-java-sdk-reference-rest diff --git a/server-common/pom.xml b/server-common/pom.xml index 96ca0d0a2..d55ef7ec9 100644 --- a/server-common/pom.xml +++ b/server-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final a2a-java-sdk-server-common diff --git a/spec-grpc/pom.xml b/spec-grpc/pom.xml index fefff3998..933f2993e 100644 --- a/spec-grpc/pom.xml +++ b/spec-grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final a2a-java-sdk-spec-grpc diff --git a/spec/pom.xml b/spec/pom.xml index 32a249568..84088dc6a 100644 --- a/spec/pom.xml +++ b/spec/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final a2a-java-sdk-spec diff --git a/tck/pom.xml b/tck/pom.xml index 977cdbb45..4686cf29d 100644 --- a/tck/pom.xml +++ b/tck/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final a2a-tck-server diff --git a/tests/server-common/pom.xml b/tests/server-common/pom.xml index 51bb51640..c49ade3f0 100644 --- a/tests/server-common/pom.xml +++ b/tests/server-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../pom.xml a2a-java-sdk-tests-server-common diff --git a/transport/grpc/pom.xml b/transport/grpc/pom.xml index 48bfaaf74..b0f46b0df 100644 --- a/transport/grpc/pom.xml +++ b/transport/grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../pom.xml a2a-java-sdk-transport-grpc diff --git a/transport/jsonrpc/pom.xml b/transport/jsonrpc/pom.xml index 64b8d86d7..55c7b85b6 100644 --- a/transport/jsonrpc/pom.xml +++ b/transport/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../pom.xml a2a-java-sdk-transport-jsonrpc diff --git a/transport/rest/pom.xml b/transport/rest/pom.xml index cb3b00e7e..92167461a 100644 --- a/transport/rest/pom.xml +++ b/transport/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Beta1-SNAPSHOT + 0.3.1.Final ../../pom.xml a2a-java-sdk-transport-rest From 535a0f32d7963e27c0f1a2d0fb7942a2c3cb64f5 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Thu, 30 Oct 2025 22:10:37 +0000 Subject: [PATCH 06/37] chore: Next is 0.3.2 (#420) --- client/base/pom.xml | 2 +- client/transport/grpc/pom.xml | 2 +- client/transport/jsonrpc/pom.xml | 2 +- client/transport/rest/pom.xml | 2 +- client/transport/spi/pom.xml | 2 +- common/pom.xml | 2 +- examples/cloud-deployment/server/pom.xml | 2 +- examples/helloworld/client/pom.xml | 2 +- .../java/io/a2a/examples/helloworld/HelloWorldRunner.java | 4 ++-- examples/helloworld/pom.xml | 2 +- examples/helloworld/server/pom.xml | 2 +- extras/common/pom.xml | 2 +- extras/push-notification-config-store-database-jpa/pom.xml | 2 +- extras/queue-manager-replicated/core/pom.xml | 2 +- extras/queue-manager-replicated/pom.xml | 2 +- .../queue-manager-replicated/replication-mp-reactive/pom.xml | 2 +- extras/queue-manager-replicated/tests-multi-instance/pom.xml | 2 +- .../tests-multi-instance/quarkus-app-1/pom.xml | 2 +- .../tests-multi-instance/quarkus-app-2/pom.xml | 2 +- .../tests-multi-instance/quarkus-common/pom.xml | 2 +- .../tests-multi-instance/tests/pom.xml | 2 +- extras/queue-manager-replicated/tests-single-instance/pom.xml | 2 +- extras/task-store-database-jpa/pom.xml | 2 +- http-client/pom.xml | 2 +- pom.xml | 2 +- reference/common/pom.xml | 2 +- reference/grpc/pom.xml | 2 +- reference/jsonrpc/pom.xml | 2 +- reference/rest/pom.xml | 2 +- server-common/pom.xml | 2 +- spec-grpc/pom.xml | 2 +- spec/pom.xml | 2 +- tck/pom.xml | 2 +- tests/server-common/pom.xml | 2 +- transport/grpc/pom.xml | 2 +- transport/jsonrpc/pom.xml | 2 +- transport/rest/pom.xml | 2 +- 37 files changed, 38 insertions(+), 38 deletions(-) diff --git a/client/base/pom.xml b/client/base/pom.xml index ac57a15a1..2e9add628 100644 --- a/client/base/pom.xml +++ b/client/base/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-client diff --git a/client/transport/grpc/pom.xml b/client/transport/grpc/pom.xml index 2d1f1a54b..9818ae4f8 100644 --- a/client/transport/grpc/pom.xml +++ b/client/transport/grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-grpc diff --git a/client/transport/jsonrpc/pom.xml b/client/transport/jsonrpc/pom.xml index 0fd8e1488..4422a4ac3 100644 --- a/client/transport/jsonrpc/pom.xml +++ b/client/transport/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-jsonrpc diff --git a/client/transport/rest/pom.xml b/client/transport/rest/pom.xml index 06dbd1dce..8b40bad9f 100644 --- a/client/transport/rest/pom.xml +++ b/client/transport/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-rest diff --git a/client/transport/spi/pom.xml b/client/transport/spi/pom.xml index a41c2a2a6..3c340b70a 100644 --- a/client/transport/spi/pom.xml +++ b/client/transport/spi/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-spi diff --git a/common/pom.xml b/common/pom.xml index 835d5c929..1c5547e9e 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT a2a-java-sdk-common diff --git a/examples/cloud-deployment/server/pom.xml b/examples/cloud-deployment/server/pom.xml index 382629d0f..91bf77eb4 100644 --- a/examples/cloud-deployment/server/pom.xml +++ b/examples/cloud-deployment/server/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../../pom.xml diff --git a/examples/helloworld/client/pom.xml b/examples/helloworld/client/pom.xml index 51616fa42..7ef99f7f6 100644 --- a/examples/helloworld/client/pom.xml +++ b/examples/helloworld/client/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-examples-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT a2a-java-sdk-examples-client diff --git a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java index 548e89bb3..6a3415371 100644 --- a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java +++ b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java @@ -1,6 +1,6 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.1.Final -//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.1.Final +//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.2.Beta1-SNAPSHOT +//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.2.Beta1-SNAPSHOT //SOURCES HelloWorldClient.java /** diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index cf8970e63..35eede3bd 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../pom.xml diff --git a/examples/helloworld/server/pom.xml b/examples/helloworld/server/pom.xml index ca969f42d..581fa8b19 100644 --- a/examples/helloworld/server/pom.xml +++ b/examples/helloworld/server/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-examples-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT a2a-java-sdk-examples-server diff --git a/extras/common/pom.xml b/extras/common/pom.xml index c6851890f..5e2e6212c 100644 --- a/extras/common/pom.xml +++ b/extras/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../pom.xml diff --git a/extras/push-notification-config-store-database-jpa/pom.xml b/extras/push-notification-config-store-database-jpa/pom.xml index 5bc9371dd..a4520e963 100644 --- a/extras/push-notification-config-store-database-jpa/pom.xml +++ b/extras/push-notification-config-store-database-jpa/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../pom.xml a2a-java-extras-push-notification-config-store-database-jpa diff --git a/extras/queue-manager-replicated/core/pom.xml b/extras/queue-manager-replicated/core/pom.xml index 683304b91..ba22098ca 100644 --- a/extras/queue-manager-replicated/core/pom.xml +++ b/extras/queue-manager-replicated/core/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/pom.xml b/extras/queue-manager-replicated/pom.xml index cd99c6872..af259d689 100644 --- a/extras/queue-manager-replicated/pom.xml +++ b/extras/queue-manager-replicated/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../pom.xml diff --git a/extras/queue-manager-replicated/replication-mp-reactive/pom.xml b/extras/queue-manager-replicated/replication-mp-reactive/pom.xml index 08ad766f0..cacaaa843 100644 --- a/extras/queue-manager-replicated/replication-mp-reactive/pom.xml +++ b/extras/queue-manager-replicated/replication-mp-reactive/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/pom.xml index d820ad14e..e611e0f88 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml index 1dce1207d..90e288b60 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml index 1e2091199..70fbf329f 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml index ce959fe7d..cc4a8562d 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml index af4fb9b8e..51773af02 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-single-instance/pom.xml b/extras/queue-manager-replicated/tests-single-instance/pom.xml index 5ca8e3806..df44234c7 100644 --- a/extras/queue-manager-replicated/tests-single-instance/pom.xml +++ b/extras/queue-manager-replicated/tests-single-instance/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/task-store-database-jpa/pom.xml b/extras/task-store-database-jpa/pom.xml index 9c31a264d..319d3d277 100644 --- a/extras/task-store-database-jpa/pom.xml +++ b/extras/task-store-database-jpa/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../pom.xml a2a-java-extras-task-store-database-jpa diff --git a/http-client/pom.xml b/http-client/pom.xml index 61297a25c..0a35e7232 100644 --- a/http-client/pom.xml +++ b/http-client/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT a2a-java-sdk-http-client diff --git a/pom.xml b/pom.xml index bca36af62..316f69f59 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT pom diff --git a/reference/common/pom.xml b/reference/common/pom.xml index ec4c6686a..8becda134 100644 --- a/reference/common/pom.xml +++ b/reference/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-reference-common diff --git a/reference/grpc/pom.xml b/reference/grpc/pom.xml index 1569722e6..72ac58701 100644 --- a/reference/grpc/pom.xml +++ b/reference/grpc/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../pom.xml diff --git a/reference/jsonrpc/pom.xml b/reference/jsonrpc/pom.xml index 30fc015c2..48a3758d9 100644 --- a/reference/jsonrpc/pom.xml +++ b/reference/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-reference-jsonrpc diff --git a/reference/rest/pom.xml b/reference/rest/pom.xml index 4c11e80bb..ba83bed0e 100644 --- a/reference/rest/pom.xml +++ b/reference/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-reference-rest diff --git a/server-common/pom.xml b/server-common/pom.xml index d55ef7ec9..14ae048c3 100644 --- a/server-common/pom.xml +++ b/server-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT a2a-java-sdk-server-common diff --git a/spec-grpc/pom.xml b/spec-grpc/pom.xml index 933f2993e..159fef48c 100644 --- a/spec-grpc/pom.xml +++ b/spec-grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT a2a-java-sdk-spec-grpc diff --git a/spec/pom.xml b/spec/pom.xml index 84088dc6a..e1351828b 100644 --- a/spec/pom.xml +++ b/spec/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT a2a-java-sdk-spec diff --git a/tck/pom.xml b/tck/pom.xml index 4686cf29d..1bb66da05 100644 --- a/tck/pom.xml +++ b/tck/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT a2a-tck-server diff --git a/tests/server-common/pom.xml b/tests/server-common/pom.xml index c49ade3f0..399aa32ce 100644 --- a/tests/server-common/pom.xml +++ b/tests/server-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-tests-server-common diff --git a/transport/grpc/pom.xml b/transport/grpc/pom.xml index b0f46b0df..1f227c2fa 100644 --- a/transport/grpc/pom.xml +++ b/transport/grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-transport-grpc diff --git a/transport/jsonrpc/pom.xml b/transport/jsonrpc/pom.xml index 55c7b85b6..5bf75365b 100644 --- a/transport/jsonrpc/pom.xml +++ b/transport/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-transport-jsonrpc diff --git a/transport/rest/pom.xml b/transport/rest/pom.xml index 92167461a..e9d56ccb8 100644 --- a/transport/rest/pom.xml +++ b/transport/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.1.Final + 0.3.2.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-transport-rest From b64fd9e9244baf3bad6ced23a51b16a5e2d39f7b Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Mon, 3 Nov 2025 08:53:30 -0500 Subject: [PATCH 07/37] fix: use pull_request_trigger to get access to secrets (#406) (#412) Backporting https://github.com/a2aproject/a2a-java/pull/406 to 0.3.x Co-authored-by: Kabir Khan --- .github/workflows/build-with-release-profile.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-with-release-profile.yml b/.github/workflows/build-with-release-profile.yml index 699e62ac5..129833307 100644 --- a/.github/workflows/build-with-release-profile.yml +++ b/.github/workflows/build-with-release-profile.yml @@ -47,7 +47,7 @@ jobs: mkdir -p ~/.m2 echo "central-a2asdk-temp${{ secrets.CENTRAL_TOKEN_USERNAME }}${{ secrets.CENTRAL_TOKEN_PASSWORD }}" > ~/.m2/settings.xml - # Deploy to Maven Central + # Build with the same settings as the deploy job # -s uses the settings file we created. - name: Build with same arguments as deploy job run: > From fd474faf037dff0ddf24e30888ab2f15cfbee43d Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Mon, 3 Nov 2025 15:49:48 -0500 Subject: [PATCH 08/37] fix: Add missing extensions to Artifact and Message (#411) Backporting https://github.com/a2aproject/a2a-java/pull/409 to 0.3.x Depends on https://github.com/a2aproject/a2a-java/pull/412 --- .../core/EventSerializationTest.java | 2 +- .../io/a2a/server/util/ArtifactUtils.java | 1 + .../java/io/a2a/grpc/utils/ProtoUtils.java | 6 ++++-- spec/src/main/java/io/a2a/spec/Artifact.java | 12 +++++++++-- spec/src/main/java/io/a2a/spec/Message.java | 21 +++++++++++++++---- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/EventSerializationTest.java b/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/EventSerializationTest.java index f05201fb7..7e18ca4a8 100644 --- a/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/EventSerializationTest.java +++ b/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/EventSerializationTest.java @@ -139,7 +139,7 @@ public void testTaskStatusUpdateEventSerialization() throws JsonProcessingExcept public void testTaskArtifactUpdateEventSerialization() throws JsonProcessingException { // Create a TaskArtifactUpdateEvent List> parts = List.of(new TextPart("Test artifact content")); - Artifact artifact = new Artifact("test-artifact-123", "Test Artifact", "Test description", parts, null); + Artifact artifact = new Artifact("test-artifact-123", "Test Artifact", "Test description", parts, null, null); TaskArtifactUpdateEvent originalEvent = new TaskArtifactUpdateEvent.Builder() .taskId("test-task-xyz") .contextId("test-context-uvw") diff --git a/server-common/src/main/java/io/a2a/server/util/ArtifactUtils.java b/server-common/src/main/java/io/a2a/server/util/ArtifactUtils.java index 7a8637680..406113fd4 100644 --- a/server-common/src/main/java/io/a2a/server/util/ArtifactUtils.java +++ b/server-common/src/main/java/io/a2a/server/util/ArtifactUtils.java @@ -32,6 +32,7 @@ public static Artifact newArtifact(String name, List> parts, String desc name, description, parts, + null, null ); } diff --git a/spec-grpc/src/main/java/io/a2a/grpc/utils/ProtoUtils.java b/spec-grpc/src/main/java/io/a2a/grpc/utils/ProtoUtils.java index 701947b42..357016054 100644 --- a/spec-grpc/src/main/java/io/a2a/grpc/utils/ProtoUtils.java +++ b/spec-grpc/src/main/java/io/a2a/grpc/utils/ProtoUtils.java @@ -875,7 +875,8 @@ public static Message message(io.a2a.grpc.MessageOrBuilder message) { message.getContextId().isEmpty() ? null : message.getContextId(), message.getTaskId().isEmpty() ? null : message.getTaskId(), null, // referenceTaskIds is not in grpc message - struct(message.getMetadata()) + struct(message.getMetadata()), + message.getExtensionsList().isEmpty() ? null : message.getExtensionsList() ); } @@ -906,7 +907,8 @@ private static Artifact artifact(io.a2a.grpc.ArtifactOrBuilder artifact) { artifact.getName(), artifact.getDescription(), artifact.getPartsList().stream().map(item -> part(item)).collect(Collectors.toList()), - struct(artifact.getMetadata()) + struct(artifact.getMetadata()), + artifact.getExtensionsList().isEmpty() ? null : artifact.getExtensionsList() ); } diff --git a/spec/src/main/java/io/a2a/spec/Artifact.java b/spec/src/main/java/io/a2a/spec/Artifact.java index cb4d1504b..798ac5823 100644 --- a/spec/src/main/java/io/a2a/spec/Artifact.java +++ b/spec/src/main/java/io/a2a/spec/Artifact.java @@ -12,7 +12,8 @@ */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) -public record Artifact(String artifactId, String name, String description, List> parts, Map metadata) { +public record Artifact(String artifactId, String name, String description, List> parts, Map metadata, + List extensions) { public Artifact { Assert.checkNotNullParam("artifactId", artifactId); @@ -28,6 +29,7 @@ public static class Builder { private String description; private List> parts; private Map metadata; + private List extensions; public Builder(){ } @@ -38,6 +40,7 @@ public Builder(Artifact existingArtifact) { description = existingArtifact.description; parts = existingArtifact.parts; metadata = existingArtifact.metadata; + extensions = existingArtifact.extensions; } public Builder artifactId(String artifactId) { @@ -71,8 +74,13 @@ public Builder metadata(Map metadata) { return this; } + public Builder extensions(List extensions) { + this.extensions = (extensions == null) ? null : List.copyOf(extensions); + return this; + } + public Artifact build() { - return new Artifact(artifactId, name, description, parts, metadata); + return new Artifact(artifactId, name, description, parts, metadata, extensions); } } } diff --git a/spec/src/main/java/io/a2a/spec/Message.java b/spec/src/main/java/io/a2a/spec/Message.java index 8278bec7e..dd7e860a5 100644 --- a/spec/src/main/java/io/a2a/spec/Message.java +++ b/spec/src/main/java/io/a2a/spec/Message.java @@ -34,17 +34,18 @@ public final class Message implements EventKind, StreamingEventKind { private final Map metadata; private final String kind; private final List referenceTaskIds; + private final List extensions; public Message(Role role, List> parts, String messageId, String contextId, String taskId, - List referenceTaskIds, Map metadata) { - this(role, parts, messageId, contextId, taskId, referenceTaskIds, metadata, MESSAGE); + List referenceTaskIds, Map metadata, List extensions) { + this(role, parts, messageId, contextId, taskId, referenceTaskIds, metadata, extensions, MESSAGE); } @JsonCreator public Message(@JsonProperty("role") Role role, @JsonProperty("parts") List> parts, @JsonProperty("messageId") String messageId, @JsonProperty("contextId") String contextId, @JsonProperty("taskId") String taskId, @JsonProperty("referenceTaskIds") List referenceTaskIds, - @JsonProperty("metadata") Map metadata, + @JsonProperty("metadata") Map metadata, @JsonProperty("extensions") List extensions, @JsonProperty("kind") String kind) { Assert.checkNotNullParam("kind", kind); Assert.checkNotNullParam("parts", parts); @@ -63,6 +64,7 @@ public Message(@JsonProperty("role") Role role, @JsonProperty("parts") List getReferenceTaskIds() { return referenceTaskIds; } + public List getExtensions() { + return extensions; + } + @Override public String getKind() { return kind; @@ -132,6 +138,7 @@ public static class Builder { private String taskId; private List referenceTaskIds; private Map metadata; + private List extensions; public Builder() { } @@ -144,6 +151,7 @@ public Builder(Message message) { taskId = message.taskId; referenceTaskIds = message.referenceTaskIds; metadata = message.metadata; + extensions = message.extensions; } public Builder role(Role role) { @@ -186,9 +194,14 @@ public Builder metadata(Map metadata) { return this; } + public Builder extensions(List extensions) { + this.extensions = (extensions == null) ? null : List.copyOf(extensions); + return this; + } + public Message build() { return new Message(role, parts, messageId == null ? UUID.randomUUID().toString() : messageId, - contextId, taskId, referenceTaskIds, metadata); + contextId, taskId, referenceTaskIds, metadata, extensions); } } } From 183305325dad14b92aa1f6d5237fc2f193b998ef Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Thu, 30 Oct 2025 20:50:16 +0000 Subject: [PATCH 09/37] chore: Run TCK on 0.3.x branch too, and use latest TCK (#421) --- .github/workflows/run-tck.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-tck.yml b/.github/workflows/run-tck.yml index 8dddf8124..375d7f6d3 100644 --- a/.github/workflows/run-tck.yml +++ b/.github/workflows/run-tck.yml @@ -5,14 +5,17 @@ on: push: branches: - main + - 0.3.x pull_request: branches: - main + - 0.3.x workflow_dispatch: env: + # TODO once we have the TCK for 0.4.0 we will need to look at the branch to decide which TCK version to run. # Tag of the TCK - TCK_VERSION: 0.3.0.beta2 + TCK_VERSION: 0.3.0.beta3 # Tells uv to not need a venv, and instead use system UV_SYSTEM_PYTHON: 1 # SUT_JSONRPC_URL to use for the TCK and the server agent From b695641cc6427cd1a2bcdae327023f8e99ce6c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=AA=85=ED=98=84?= <117622409+flex-myeonghyeon@users.noreply.github.com> Date: Tue, 4 Nov 2025 01:54:43 +0900 Subject: [PATCH 10/37] fix: clear status.message after adding to history in TaskManager.updateWithMessage #424 (#425) --- .../java/io/a2a/server/tasks/TaskManager.java | 10 +++-- .../io/a2a/server/tasks/TaskManagerTest.java | 44 +++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/server-common/src/main/java/io/a2a/server/tasks/TaskManager.java b/server-common/src/main/java/io/a2a/server/tasks/TaskManager.java index 523b9346f..dff182af4 100644 --- a/server-common/src/main/java/io/a2a/server/tasks/TaskManager.java +++ b/server-common/src/main/java/io/a2a/server/tasks/TaskManager.java @@ -104,12 +104,16 @@ public Event process(Event event) throws A2AServerException { } public Task updateWithMessage(Message message, Task task) { - List history = task.getHistory() == null ? new ArrayList<>() : new ArrayList<>(task.getHistory()); - if (task.getStatus().message() != null) { - history.add(task.getStatus().message()); + List history = new ArrayList<>(task.getHistory()); + + TaskStatus status = task.getStatus(); + if (status.message() != null) { + history.add(status.message()); + status = new TaskStatus(status.state(), null, status.timestamp()); } history.add(message); task = new Task.Builder(task) + .status(status) .history(history) .build(); saveTask(task); diff --git a/server-common/src/test/java/io/a2a/server/tasks/TaskManagerTest.java b/server-common/src/test/java/io/a2a/server/tasks/TaskManagerTest.java index 25b5ead11..637509ccf 100644 --- a/server-common/src/test/java/io/a2a/server/tasks/TaskManagerTest.java +++ b/server-common/src/test/java/io/a2a/server/tasks/TaskManagerTest.java @@ -692,4 +692,48 @@ public void testSaveTaskInternal() throws A2AServerException { assertEquals("test-context", taskManagerWithoutId.getContextId()); assertSame(savedTask, taskManagerWithoutId.getTask()); } + + @Test + public void testUpdateWithMessage() throws A2AServerException { + Message initialMessage = new Message.Builder() + .role(Message.Role.USER) + .parts(Collections.singletonList(new TextPart("initial message"))) + .messageId("initial-msg-id") + .build(); + + TaskManager taskManagerWithInitialMessage = new TaskManager(null, null, taskStore, initialMessage); + + Message taskMessage = new Message.Builder() + .role(Message.Role.AGENT) + .parts(Collections.singletonList(new TextPart("task message"))) + .messageId("task-msg-id") + .build(); + + TaskStatusUpdateEvent event = new TaskStatusUpdateEvent.Builder() + .taskId("new-task-id") + .contextId("some-context") + .status(new TaskStatus(TaskState.SUBMITTED, taskMessage, null)) + .isFinal(false) + .build(); + + Task saved = taskManagerWithInitialMessage.saveTaskEvent(event); + + Message updateMessage = new Message.Builder() + .role(Message.Role.USER) + .parts(Collections.singletonList(new TextPart("update message"))) + .messageId("update-msg-id") + .build(); + + Task updated = taskManagerWithInitialMessage.updateWithMessage(updateMessage, saved); + + // There should now be a history containing the initialMessage, task message and update message + assertNotNull(updated.getHistory()); + assertEquals(3, updated.getHistory().size()); + assertEquals("initial message", ((TextPart) updated.getHistory().get(0).getParts().get(0)).getText()); + + // The message in the current state should be null + assertNull(updated.getStatus().message()); + assertEquals("task message", ((TextPart) updated.getHistory().get(1).getParts().get(0)).getText()); + assertEquals("update message", ((TextPart) updated.getHistory().get(2).getParts().get(0)).getText()); + } } From 038191a8dea27197fef41b5f86343090bf64c5cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=AA=85=ED=98=84?= <117622409+flex-myeonghyeon@users.noreply.github.com> Date: Tue, 4 Nov 2025 02:01:05 +0900 Subject: [PATCH 11/37] fix: merge metadata instead of replacing in TaskManager.TaskStatusUpdateEvent #426 (#427) --- .../src/main/java/io/a2a/server/tasks/TaskManager.java | 6 +++++- .../test/java/io/a2a/server/tasks/TaskManagerTest.java | 10 ++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/server-common/src/main/java/io/a2a/server/tasks/TaskManager.java b/server-common/src/main/java/io/a2a/server/tasks/TaskManager.java index dff182af4..4b1112e22 100644 --- a/server-common/src/main/java/io/a2a/server/tasks/TaskManager.java +++ b/server-common/src/main/java/io/a2a/server/tasks/TaskManager.java @@ -5,7 +5,9 @@ import static io.a2a.util.Utils.appendArtifactToTask; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import io.a2a.spec.A2AServerException; import io.a2a.spec.Artifact; @@ -78,7 +80,9 @@ Task saveTaskEvent(TaskStatusUpdateEvent event) throws A2AServerException { // Handle metadata from the event if (event.getMetadata() != null) { - builder.metadata(event.getMetadata()); + Map metadata = task.getMetadata() == null ? new HashMap<>() : new HashMap<>(task.getMetadata()); + metadata.putAll(event.getMetadata()); + builder.metadata(metadata); } task = builder.build(); diff --git a/server-common/src/test/java/io/a2a/server/tasks/TaskManagerTest.java b/server-common/src/test/java/io/a2a/server/tasks/TaskManagerTest.java index 637509ccf..1a4850bda 100644 --- a/server-common/src/test/java/io/a2a/server/tasks/TaskManagerTest.java +++ b/server-common/src/test/java/io/a2a/server/tasks/TaskManagerTest.java @@ -588,8 +588,8 @@ public void testSaveTaskEventMetadataUpdateNull() throws A2AServerException { } @Test - public void testSaveTaskEventMetadataUpdateOverwritesExisting() throws A2AServerException { - // Test that metadata update overwrites existing metadata + public void testSaveTaskEventMetadataMergeExisting() throws A2AServerException { + // Test that metadata update merges with existing metadata Map originalMetadata = new HashMap<>(); originalMetadata.put("original_key", "original_value"); @@ -612,8 +612,10 @@ public void testSaveTaskEventMetadataUpdateOverwritesExisting() throws A2AServer taskManager.saveTaskEvent(event); Task updatedTask = taskManager.getTask(); - assertEquals(newMetadata, updatedTask.getMetadata()); - assertNotEquals(originalMetadata, updatedTask.getMetadata()); + + Map mergedMetadata = new HashMap<>(originalMetadata); + mergedMetadata.putAll(newMetadata); + assertEquals(mergedMetadata, updatedTask.getMetadata()); } @Test From c29abdd315371fe5043f9bc85fc76d26bc0fb510 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Tue, 4 Nov 2025 11:54:39 +0000 Subject: [PATCH 12/37] fix: Change image for the multi instance replicated queuemanager test (#440) openjdk:17-slim has disappeared Upstream: #439 --- .../tests-multi-instance/quarkus-app-1/Dockerfile | 2 +- .../tests-multi-instance/quarkus-app-2/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/Dockerfile b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/Dockerfile index fb8d091ba..218da8c35 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/Dockerfile +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/Dockerfile @@ -1,4 +1,4 @@ -FROM openjdk:17-slim +FROM mirror.gcr.io/eclipse-temurin:17-jre-jammy WORKDIR /app diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/Dockerfile b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/Dockerfile index 920d34747..bc0b294be 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/Dockerfile +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/Dockerfile @@ -1,4 +1,4 @@ -FROM openjdk:17-slim +FROM mirror.gcr.io/eclipse-temurin:17-jre-jammy WORKDIR /app From 2e23b0b4abb60f2fef0ad3073f3521793bff7583 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Tue, 4 Nov 2025 13:54:37 +0000 Subject: [PATCH 13/37] fix: Make sure we use @Inject on @ConfigProperty (#438) Jakarta needs this or it ignores the config Upstream: #437 --- .../java/io/a2a/examples/cloud/CloudAgentCardProducer.java | 3 +++ .../extras/taskstore/database/jpa/JpaDatabaseTaskStore.java | 1 + 2 files changed, 4 insertions(+) diff --git a/examples/cloud-deployment/server/src/main/java/io/a2a/examples/cloud/CloudAgentCardProducer.java b/examples/cloud-deployment/server/src/main/java/io/a2a/examples/cloud/CloudAgentCardProducer.java index 3a35a6e8b..2894c84a9 100644 --- a/examples/cloud-deployment/server/src/main/java/io/a2a/examples/cloud/CloudAgentCardProducer.java +++ b/examples/cloud-deployment/server/src/main/java/io/a2a/examples/cloud/CloudAgentCardProducer.java @@ -6,6 +6,8 @@ import io.a2a.spec.AgentSkill; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; + import org.eclipse.microprofile.config.inject.ConfigProperty; import java.util.Collections; @@ -17,6 +19,7 @@ @ApplicationScoped public class CloudAgentCardProducer { + @Inject @ConfigProperty(name = "agent.url", defaultValue = "http://localhost:8080") String agentUrl; diff --git a/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java b/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java index a90cf6294..44837ae85 100644 --- a/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java +++ b/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java @@ -34,6 +34,7 @@ public class JpaDatabaseTaskStore implements TaskStore, TaskStateProvider { @Inject Event taskFinalizedEvent; + @Inject @ConfigProperty(name = "a2a.replication.grace-period-seconds", defaultValue = "15") long gracePeriodSeconds; From bd8fcd2721dcc55f20ca235fcf53b55e0352c422 Mon Sep 17 00:00:00 2001 From: Emmanuel Hugonnet Date: Tue, 4 Nov 2025 15:11:07 +0100 Subject: [PATCH 14/37] fix: fixing the handling of historyLength being set to 0 by default in gRPC. (#436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Using int instead of Integer and setting the default value to 0. * If historyLength == 0 then we should return the full history * Removing the handling of null Issue: https://github.com/a2aproject/a2a-java/issues/423 Fixes #423 🦕 Signed-off-by: Emmanuel Hugonnet --- .../client/transport/grpc/GrpcTransport.java | 8 +- .../client/transport/rest/RestTransport.java | 2 +- .../server/rest/quarkus/A2AServerRoutes.java | 4 +- .../rest/quarkus/A2AServerRoutesTest.java | 5 +- .../DefaultRequestHandler.java | 8 +- .../DefaultRequestHandlerUnitTest.java | 731 ++++++++++++++++++ .../java/io/a2a/spec/TaskQueryParams.java | 8 +- .../transport/rest/handler/RestHandler.java | 2 +- .../rest/handler/RestHandlerTest.java | 6 +- 9 files changed, 749 insertions(+), 25 deletions(-) create mode 100644 server-common/src/test/java/io/a2a/server/requesthandlers/DefaultRequestHandlerUnitTest.java diff --git a/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransport.java b/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransport.java index 5a8437679..224433393 100644 --- a/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransport.java +++ b/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransport.java @@ -1,9 +1,5 @@ package io.a2a.client.transport.grpc; -import static io.a2a.grpc.A2AServiceGrpc.A2AServiceBlockingV2Stub; -import static io.a2a.grpc.A2AServiceGrpc.A2AServiceStub; -import static io.a2a.grpc.utils.ProtoUtils.FromProto; -import static io.a2a.grpc.utils.ProtoUtils.ToProto; import static io.a2a.util.Assert.checkNotNullParam; import java.util.List; @@ -127,9 +123,7 @@ public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context GetTaskRequest.Builder requestBuilder = GetTaskRequest.newBuilder(); requestBuilder.setName("tasks/" + request.id()); - if (request.historyLength() != null) { - requestBuilder.setHistoryLength(request.historyLength()); - } + requestBuilder.setHistoryLength(request.historyLength()); GetTaskRequest getTaskRequest = requestBuilder.build(); PayloadAndHeaders payloadAndHeaders = applyInterceptors(io.a2a.spec.GetTaskRequest.METHOD, getTaskRequest, agentCard, context); diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java index f659589b7..5b67d97e0 100644 --- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java +++ b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java @@ -125,7 +125,7 @@ public Task getTask(TaskQueryParams taskQueryParams, @Nullable ClientCallContext agentCard, context); try { String url; - if (taskQueryParams.historyLength() != null) { + if (taskQueryParams.historyLength() > 0) { url = agentUrl + String.format("/v1/tasks/%1s?historyLength=%2d", taskQueryParams.id(), taskQueryParams.historyLength()); } else { url = agentUrl + String.format("/v1/tasks/%1s", taskQueryParams.id()); diff --git a/reference/rest/src/main/java/io/a2a/server/rest/quarkus/A2AServerRoutes.java b/reference/rest/src/main/java/io/a2a/server/rest/quarkus/A2AServerRoutes.java index b923dde5c..4f0173070 100644 --- a/reference/rest/src/main/java/io/a2a/server/rest/quarkus/A2AServerRoutes.java +++ b/reference/rest/src/main/java/io/a2a/server/rest/quarkus/A2AServerRoutes.java @@ -122,9 +122,9 @@ public void getTask(RoutingContext rc) { if (taskId == null || taskId.isEmpty()) { response = jsonRestHandler.createErrorResponse(new InvalidParamsError("bad task id")); } else { - Integer historyLength = null; + int historyLength = 0; if (rc.request().params().contains("history_length")) { - historyLength = Integer.valueOf(rc.request().params().get("history_length")); + historyLength = Integer.parseInt(rc.request().params().get("history_length")); } response = jsonRestHandler.getTask(taskId, historyLength, context); } diff --git a/reference/rest/src/test/java/io/a2a/server/rest/quarkus/A2AServerRoutesTest.java b/reference/rest/src/test/java/io/a2a/server/rest/quarkus/A2AServerRoutesTest.java index 83d54a40c..09cbc9926 100644 --- a/reference/rest/src/test/java/io/a2a/server/rest/quarkus/A2AServerRoutesTest.java +++ b/reference/rest/src/test/java/io/a2a/server/rest/quarkus/A2AServerRoutesTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -137,7 +138,7 @@ public void testGetTask_MethodNameSetInContext() { when(mockHttpResponse.getStatusCode()).thenReturn(200); when(mockHttpResponse.getContentType()).thenReturn("application/json"); when(mockHttpResponse.getBody()).thenReturn("{test:value}"); - when(mockRestHandler.getTask(anyString(), any(), any(ServerCallContext.class))).thenReturn(mockHttpResponse); + when(mockRestHandler.getTask(anyString(), anyInt(), any(ServerCallContext.class))).thenReturn(mockHttpResponse); ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(ServerCallContext.class); @@ -145,7 +146,7 @@ public void testGetTask_MethodNameSetInContext() { routes.getTask(mockRoutingContext); // Assert - verify(mockRestHandler).getTask(eq("task123"), eq(null), contextCaptor.capture()); + verify(mockRestHandler).getTask(eq("task123"), anyInt(), contextCaptor.capture()); ServerCallContext capturedContext = contextCaptor.getValue(); assertNotNull(capturedContext); assertEquals(GetTaskRequest.METHOD, capturedContext.getState().get(METHOD_NAME_KEY)); diff --git a/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java b/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java index c05dcfd24..f79e059a7 100644 --- a/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java +++ b/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java @@ -19,7 +19,6 @@ import java.util.function.Supplier; import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; import io.a2a.server.ServerCallContext; @@ -34,7 +33,6 @@ import io.a2a.server.events.TaskQueueExistsException; import io.a2a.server.tasks.PushNotificationConfigStore; import io.a2a.server.tasks.PushNotificationSender; -import io.a2a.server.tasks.TaskStateProvider; import io.a2a.server.tasks.ResultAggregator; import io.a2a.server.tasks.TaskManager; import io.a2a.server.tasks.TaskStore; @@ -103,14 +101,14 @@ public Task onGetTask(TaskQueryParams params, ServerCallContext context) throws LOGGER.debug("No task found for {}. Throwing TaskNotFoundError", params.id()); throw new TaskNotFoundError(); } - if (params.historyLength() != null && task.getHistory() != null && params.historyLength() < task.getHistory().size()) { + if (task.getHistory() != null && params.historyLength() < task.getHistory().size()) { List history; if (params.historyLength() <= 0) { - history = new ArrayList<>(); + history = task.getHistory(); } else { history = task.getHistory().subList( task.getHistory().size() - params.historyLength(), - task.getHistory().size() - 1); + task.getHistory().size()); } task = new Task.Builder(task) diff --git a/server-common/src/test/java/io/a2a/server/requesthandlers/DefaultRequestHandlerUnitTest.java b/server-common/src/test/java/io/a2a/server/requesthandlers/DefaultRequestHandlerUnitTest.java new file mode 100644 index 000000000..1e3da1f16 --- /dev/null +++ b/server-common/src/test/java/io/a2a/server/requesthandlers/DefaultRequestHandlerUnitTest.java @@ -0,0 +1,731 @@ +package io.a2a.server.requesthandlers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import io.a2a.server.ServerCallContext; +import io.a2a.server.agentexecution.AgentExecutor; +import io.a2a.server.auth.UnauthenticatedUser; +import io.a2a.server.events.EventQueue; +import io.a2a.server.events.QueueManager; +import io.a2a.server.tasks.PushNotificationConfigStore; +import io.a2a.server.tasks.PushNotificationSender; +import io.a2a.server.tasks.TaskStore; +import io.a2a.spec.DeleteTaskPushNotificationConfigParams; +import io.a2a.spec.GetTaskPushNotificationConfigParams; +import io.a2a.spec.InternalError; +import io.a2a.spec.ListTaskPushNotificationConfigParams; +import io.a2a.spec.Message; +import io.a2a.spec.PushNotificationConfig; +import io.a2a.spec.Task; +import io.a2a.spec.TaskIdParams; +import io.a2a.spec.TaskNotCancelableError; +import io.a2a.spec.TaskNotFoundError; +import io.a2a.spec.TaskPushNotificationConfig; +import io.a2a.spec.TaskQueryParams; +import io.a2a.spec.TaskState; +import io.a2a.spec.TaskStatus; +import io.a2a.spec.TextPart; +import io.a2a.spec.UnsupportedOperationError; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Unit tests for DefaultRequestHandler using Mockito. + * These tests focus on individual methods and edge cases, particularly the history truncation logic. + */ +public class DefaultRequestHandlerUnitTest { + + @Mock + private AgentExecutor agentExecutor; + + @Mock + private TaskStore taskStore; + + @Mock + private QueueManager queueManager; + + @Mock + private PushNotificationConfigStore pushConfigStore; + + @Mock + private PushNotificationSender pushSender; + + private Executor executor; + private DefaultRequestHandler requestHandler; + private ServerCallContext serverCallContext; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + executor = Executors.newCachedThreadPool(); + requestHandler = new DefaultRequestHandler( + agentExecutor, + taskStore, + queueManager, + pushConfigStore, + pushSender, + executor + ); + serverCallContext = new ServerCallContext(UnauthenticatedUser.INSTANCE, Map.of(), Set.of()); + } + + @Nested + class OnGetTaskTests { + + @Test + void testGetTask_TaskNotFound() { + // Given + TaskQueryParams params = new TaskQueryParams("non-existent-task", 0); + when(taskStore.get("non-existent-task")).thenReturn(null); + + // When/Then + assertThrows(TaskNotFoundError.class, () + -> requestHandler.onGetTask(params, serverCallContext) + ); + } + + @Test + void testGetTask_NoHistoryTruncation_WhenHistoryLengthIsZero() throws Exception { + // Given + List fullHistory = createMessageList(5); + Task task = createTaskWithHistory("task-1", fullHistory); + TaskQueryParams params = new TaskQueryParams("task-1", 0); + + when(taskStore.get("task-1")).thenReturn(task); + + // When + Task result = requestHandler.onGetTask(params, serverCallContext); + + // Then + assertNotNull(result); + assertEquals(5, result.getHistory().size()); + assertEquals(fullHistory, result.getHistory()); + } + + @Test + void testGetTask_NoHistoryTruncation_WhenHistoryLengthEqualsHistorySize() throws Exception { + // Given + List fullHistory = createMessageList(5); + Task task = createTaskWithHistory("task-1", fullHistory); + TaskQueryParams params = new TaskQueryParams("task-1", 5); + + when(taskStore.get("task-1")).thenReturn(task); + + // When + Task result = requestHandler.onGetTask(params, serverCallContext); + + // Then + assertNotNull(result); + assertEquals(5, result.getHistory().size()); + assertEquals(fullHistory, result.getHistory()); + } + + @Test + void testGetTask_NoHistoryTruncation_WhenHistoryLengthGreaterThanHistorySize() throws Exception { + // Given + List fullHistory = createMessageList(5); + Task task = createTaskWithHistory("task-1", fullHistory); + TaskQueryParams params = new TaskQueryParams("task-1", 10); + + when(taskStore.get("task-1")).thenReturn(task); + + // When + Task result = requestHandler.onGetTask(params, serverCallContext); + + // Then + assertNotNull(result); + assertEquals(5, result.getHistory().size()); + assertEquals(fullHistory, result.getHistory()); + } + + @Test + void testGetTask_HistoryTruncation_ReturnsLastNMessages() throws Exception { + // Given - 10 messages in history + List fullHistory = createMessageList(10); + Task task = createTaskWithHistory("task-1", fullHistory); + TaskQueryParams params = new TaskQueryParams("task-1", 3); + + when(taskStore.get("task-1")).thenReturn(task); + + // When - request last 3 messages + Task result = requestHandler.onGetTask(params, serverCallContext); + + // Then - should get messages at indices 7, 8, 9 (the last 3) + assertNotNull(result); + assertEquals(3, result.getHistory().size()); + assertEquals("msg-7", result.getHistory().get(0).getMessageId()); + assertEquals("msg-8", result.getHistory().get(1).getMessageId()); + assertEquals("msg-9", result.getHistory().get(2).getMessageId()); + } + + @Test + void testGetTask_HistoryTruncation_IncludesMostRecentMessage() throws Exception { + // Given - This tests the bug fix where size()-1 was excluding the last message + List fullHistory = createMessageList(10); + Task task = createTaskWithHistory("task-1", fullHistory); + TaskQueryParams params = new TaskQueryParams("task-1", 1); + + when(taskStore.get("task-1")).thenReturn(task); + + // When - request last 1 message + Task result = requestHandler.onGetTask(params, serverCallContext); + + // Then - should get the LAST message (msg-9), not msg-8 + assertNotNull(result); + assertEquals(1, result.getHistory().size()); + assertEquals("msg-9", result.getHistory().get(0).getMessageId()); + } + + @Test + void testGetTask_NullHistory_ReturnsTaskAsIs() throws Exception { + // Given + Task task = new Task.Builder() + .id("task-1") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.COMPLETED)) + .build(); + TaskQueryParams params = new TaskQueryParams("task-1", 3); + + when(taskStore.get("task-1")).thenReturn(task); + + // When + Task result = requestHandler.onGetTask(params, serverCallContext); + + // Then + assertNotNull(result); + assertEquals(task, result); + } + + @Test + void testGetTask_EmptyHistory_ReturnsTaskAsIs() throws Exception { + // Given + Task task = createTaskWithHistory("task-1", new ArrayList<>()); + TaskQueryParams params = new TaskQueryParams("task-1", 3); + + when(taskStore.get("task-1")).thenReturn(task); + + // When + Task result = requestHandler.onGetTask(params, serverCallContext); + + // Then + assertNotNull(result); + assertEquals(0, result.getHistory().size()); + } + + private List createMessageList(int count) { + List messages = new ArrayList<>(); + for (int i = 0; i < count; i++) { + messages.add(new Message.Builder() + .messageId("msg-" + i) + .role(Message.Role.USER) + .parts(new TextPart("Message " + i)) + .build()); + } + return messages; + } + + private Task createTaskWithHistory(String taskId, List history) { + return new Task.Builder() + .id(taskId) + .contextId("ctx-1") + .status(new TaskStatus(TaskState.COMPLETED)) + .history(history) + .build(); + } + } + + @Nested + class OnCancelTaskTests { + + @Test + void testCancelTask_TaskNotFound() { + // Given + TaskIdParams params = new TaskIdParams("non-existent-task"); + when(taskStore.get("non-existent-task")).thenReturn(null); + + // When/Then + assertThrows(TaskNotFoundError.class, () + -> requestHandler.onCancelTask(params, serverCallContext) + ); + } + + @Test + void testCancelTask_TaskAlreadyCompleted() { + // Given + Task completedTask = new Task.Builder() + .id("task-1") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.COMPLETED)) + .build(); + TaskIdParams params = new TaskIdParams("task-1"); + + when(taskStore.get("task-1")).thenReturn(completedTask); + + // When/Then + TaskNotCancelableError exception = assertThrows(TaskNotCancelableError.class, () + -> requestHandler.onCancelTask(params, serverCallContext) + ); + + assertEquals(-32002 , exception.getCode()); + assertTrue(exception.getMessage().contains("completed")); + } + + @Test + void testCancelTask_TaskAlreadyCanceled() { + // Given + Task canceledTask = new Task.Builder() + .id("task-1") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.CANCELED)) + .build(); + TaskIdParams params = new TaskIdParams("task-1"); + + when(taskStore.get("task-1")).thenReturn(canceledTask); + + // When/Then + TaskNotCancelableError exception = assertThrows(TaskNotCancelableError.class, () + -> requestHandler.onCancelTask(params, serverCallContext) + ); + + assertEquals(-32002 , exception.getCode()); + assertTrue(exception.getMessage().contains("canceled")); + } + + @Test + void testCancelTask_TaskAlreadyFailed() { + // Given + Task failedTask = new Task.Builder() + .id("task-1") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.FAILED)) + .build(); + TaskIdParams params = new TaskIdParams("task-1"); + + when(taskStore.get("task-1")).thenReturn(failedTask); + + // When/Then + TaskNotCancelableError exception = assertThrows(TaskNotCancelableError.class, () + -> requestHandler.onCancelTask(params, serverCallContext) + ); + + assertEquals(-32002 , exception.getCode()); + assertTrue(exception.getMessage().contains("failed")); + } + + @Test + void testCancelTask_TaskAlreadyRejected() { + // Given + Task rejectedTask = new Task.Builder() + .id("task-1") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.REJECTED)) + .build(); + TaskIdParams params = new TaskIdParams("task-1"); + + when(taskStore.get("task-1")).thenReturn(rejectedTask); + + // When/Then + TaskNotCancelableError exception = assertThrows(TaskNotCancelableError.class, () + -> requestHandler.onCancelTask(params, serverCallContext) + ); + + + assertEquals(-32002 , exception.getCode()); + assertTrue(exception.getMessage().contains("rejected")); + } + } + + @Nested + class PushNotificationConfigTests { + + @Test + void testSetTaskPushNotificationConfig_NoConfigStore() { + // Given + requestHandler = new DefaultRequestHandler( + agentExecutor, taskStore, queueManager, null, pushSender, executor + ); + TaskPushNotificationConfig params = new TaskPushNotificationConfig( + "task-1", + new PushNotificationConfig.Builder().id("config-1").url("http://localhost:8080").build() + ); + + // When/Then + assertThrows(UnsupportedOperationError.class, () + -> requestHandler.onSetTaskPushNotificationConfig(params, serverCallContext) + ); + } + + @Test + void testSetTaskPushNotificationConfig_TaskNotFound() { + // Given + TaskPushNotificationConfig params = new TaskPushNotificationConfig( + "non-existent-task", + new PushNotificationConfig.Builder().id("config-1").url("http://localhost:8080").build() + ); + when(taskStore.get("non-existent-task")).thenReturn(null); + + // When/Then + assertThrows(TaskNotFoundError.class, () + -> requestHandler.onSetTaskPushNotificationConfig(params, serverCallContext) + ); + } + + @Test + void testSetTaskPushNotificationConfig_Success() throws Exception { + // Given + Task task = new Task.Builder() + .id("task-1") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.WORKING)) + .build(); + PushNotificationConfig config = new PushNotificationConfig.Builder() + .id("config-1") + .url("http://localhost:8080") + .build(); + TaskPushNotificationConfig params = new TaskPushNotificationConfig("task-1", config); + + when(taskStore.get("task-1")).thenReturn(task); + when(pushConfigStore.setInfo("task-1", config)).thenReturn(config); + + // When + TaskPushNotificationConfig result = requestHandler.onSetTaskPushNotificationConfig(params, serverCallContext); + + // Then + assertNotNull(result); + assertEquals("task-1", result.taskId()); + assertEquals("config-1", result.pushNotificationConfig().id()); + verify(pushConfigStore).setInfo("task-1", config); + } + + @Test + void testGetTaskPushNotificationConfig_NoConfigStore() { + // Given + requestHandler = new DefaultRequestHandler( + agentExecutor, taskStore, queueManager, null, pushSender, executor + ); + GetTaskPushNotificationConfigParams params = new GetTaskPushNotificationConfigParams("task-1", null); + + // When/Then + assertThrows(UnsupportedOperationError.class, () + -> requestHandler.onGetTaskPushNotificationConfig(params, serverCallContext) + ); + } + + @Test + void testGetTaskPushNotificationConfig_TaskNotFound() { + // Given + GetTaskPushNotificationConfigParams params = new GetTaskPushNotificationConfigParams("non-existent-task", null); + when(taskStore.get("non-existent-task")).thenReturn(null); + + // When/Then + assertThrows(TaskNotFoundError.class, () + -> requestHandler.onGetTaskPushNotificationConfig(params, serverCallContext) + ); + } + + @Test + void testGetTaskPushNotificationConfig_NoConfigFound() { + // Given + Task task = new Task.Builder() + .id("task-1") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.WORKING)) + .build(); + GetTaskPushNotificationConfigParams params = new GetTaskPushNotificationConfigParams("task-1", null); + + when(taskStore.get("task-1")).thenReturn(task); + when(pushConfigStore.getInfo("task-1")).thenReturn(null); + + // When/Then + assertThrows(InternalError.class, () + -> requestHandler.onGetTaskPushNotificationConfig(params, serverCallContext) + ); + } + + @Test + void testGetTaskPushNotificationConfig_EmptyConfigList() { + // Given + Task task = new Task.Builder() + .id("task-1") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.WORKING)) + .build(); + GetTaskPushNotificationConfigParams params = new GetTaskPushNotificationConfigParams("task-1", null); + + when(taskStore.get("task-1")).thenReturn(task); + when(pushConfigStore.getInfo("task-1")).thenReturn(new ArrayList<>()); + + // When/Then + assertThrows(InternalError.class, () + -> requestHandler.onGetTaskPushNotificationConfig(params, serverCallContext) + ); + } + + @Test + void testGetTaskPushNotificationConfig_ReturnsFirstConfig_WhenNoIdSpecified() throws Exception { + // Given + Task task = new Task.Builder() + .id("task-1") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.WORKING)) + .build(); + PushNotificationConfig config1 = new PushNotificationConfig.Builder().id("config-1").url("http://localhost:8080").build(); + PushNotificationConfig config2 = new PushNotificationConfig.Builder().id("config-2").url("http://localhost:8080").build(); + List configs = List.of(config1, config2); + GetTaskPushNotificationConfigParams params = new GetTaskPushNotificationConfigParams("task-1", null); + + when(taskStore.get("task-1")).thenReturn(task); + when(pushConfigStore.getInfo("task-1")).thenReturn(configs); + + // When + TaskPushNotificationConfig result = requestHandler.onGetTaskPushNotificationConfig(params, serverCallContext); + + // Then + assertNotNull(result); + assertEquals("task-1", result.taskId()); + assertEquals("config-1", result.pushNotificationConfig().id()); + } + + @Test + void testGetTaskPushNotificationConfig_ReturnsSpecificConfig_WhenIdSpecified() throws Exception { + // Given + Task task = new Task.Builder() + .id("task-1") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.WORKING)) + .build(); + PushNotificationConfig config1 = new PushNotificationConfig.Builder().id("config-1").url("http://localhost:8080").build(); + PushNotificationConfig config2 = new PushNotificationConfig.Builder().id("config-2").url("http://localhost:8080").build(); + List configs = List.of(config1, config2); + GetTaskPushNotificationConfigParams params = new GetTaskPushNotificationConfigParams("task-1", "config-2"); + + when(taskStore.get("task-1")).thenReturn(task); + when(pushConfigStore.getInfo("task-1")).thenReturn(configs); + + // When + TaskPushNotificationConfig result = requestHandler.onGetTaskPushNotificationConfig(params, serverCallContext); + + // Then + assertNotNull(result); + assertEquals("task-1", result.taskId()); + assertEquals("config-2", result.pushNotificationConfig().id()); + } + + @Test + void testListTaskPushNotificationConfig_NoConfigStore() { + // Given + requestHandler = new DefaultRequestHandler( + agentExecutor, taskStore, queueManager, null, pushSender, executor + ); + ListTaskPushNotificationConfigParams params = new ListTaskPushNotificationConfigParams("task-1"); + + // When/Then + assertThrows(UnsupportedOperationError.class, () + -> requestHandler.onListTaskPushNotificationConfig(params, serverCallContext) + ); + } + + @Test + void testListTaskPushNotificationConfig_TaskNotFound() { + // Given + ListTaskPushNotificationConfigParams params = new ListTaskPushNotificationConfigParams("non-existent-task"); + when(taskStore.get("non-existent-task")).thenReturn(null); + + // When/Then + assertThrows(TaskNotFoundError.class, () + -> requestHandler.onListTaskPushNotificationConfig(params, serverCallContext) + ); + } + + @Test + void testListTaskPushNotificationConfig_ReturnsEmptyList_WhenNoConfigs() throws Exception { + // Given + Task task = new Task.Builder() + .id("task-1") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.WORKING)) + .build(); + ListTaskPushNotificationConfigParams params = new ListTaskPushNotificationConfigParams("task-1"); + + when(taskStore.get("task-1")).thenReturn(task); + when(pushConfigStore.getInfo("task-1")).thenReturn(null); + + // When + List result = requestHandler.onListTaskPushNotificationConfig(params, serverCallContext); + + // Then + assertNotNull(result); + assertEquals(0, result.size()); + } + + @Test + void testListTaskPushNotificationConfig_ReturnsAllConfigs() throws Exception { + // Given + Task task = new Task.Builder() + .id("task-1") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.WORKING)) + .build(); + PushNotificationConfig config1 = new PushNotificationConfig.Builder().id("config-1").url("http://localhost:8080").build(); + PushNotificationConfig config2 = new PushNotificationConfig.Builder().id("config-2").url("http://localhost:8080").build(); + List configs = List.of(config1, config2); + ListTaskPushNotificationConfigParams params = new ListTaskPushNotificationConfigParams("task-1"); + + when(taskStore.get("task-1")).thenReturn(task); + when(pushConfigStore.getInfo("task-1")).thenReturn(configs); + + // When + List result = requestHandler.onListTaskPushNotificationConfig(params, serverCallContext); + + // Then + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals("config-1", result.get(0).pushNotificationConfig().id()); + assertEquals("config-2", result.get(1).pushNotificationConfig().id()); + } + + @Test + void testDeleteTaskPushNotificationConfig_NoConfigStore() { + // Given + requestHandler = new DefaultRequestHandler( + agentExecutor, taskStore, queueManager, null, pushSender, executor + ); + DeleteTaskPushNotificationConfigParams params = new DeleteTaskPushNotificationConfigParams("task-1", "config-1"); + + // When/Then + assertThrows(UnsupportedOperationError.class, () + -> requestHandler.onDeleteTaskPushNotificationConfig(params, serverCallContext) + ); + } + + @Test + void testDeleteTaskPushNotificationConfig_TaskNotFound() { + // Given + DeleteTaskPushNotificationConfigParams params = new DeleteTaskPushNotificationConfigParams("non-existent-task", "config-1"); + when(taskStore.get("non-existent-task")).thenReturn(null); + + // When/Then + assertThrows(TaskNotFoundError.class, () + -> requestHandler.onDeleteTaskPushNotificationConfig(params, serverCallContext) + ); + } + + @Test + void testDeleteTaskPushNotificationConfig_Success() { + // Given + Task task = new Task.Builder() + .id("task-1") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.WORKING)) + .build(); + DeleteTaskPushNotificationConfigParams params = new DeleteTaskPushNotificationConfigParams("task-1", "config-1"); + + when(taskStore.get("task-1")).thenReturn(task); + + // When + requestHandler.onDeleteTaskPushNotificationConfig(params, serverCallContext); + + // Then + verify(pushConfigStore).deleteInfo("task-1", "config-1"); + } + } + + @Nested + class OnResubscribeToTaskTests { + + @Test + void testResubscribeToTask_TaskNotFound() { + // Given + TaskIdParams params = new TaskIdParams("non-existent-task"); + when(taskStore.get("non-existent-task")).thenReturn(null); + + // When/Then + assertThrows(TaskNotFoundError.class, () + -> requestHandler.onResubscribeToTask(params, serverCallContext) + ); + } + + @Test + void testResubscribeToTask_QueueNotFound_FinalTask() { + // Given + Task finalTask = new Task.Builder() + .id("task-1") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.COMPLETED)) + .build(); + TaskIdParams params = new TaskIdParams("task-1"); + + when(taskStore.get("task-1")).thenReturn(finalTask); + when(queueManager.tap("task-1")).thenReturn(null); + + // When/Then - Should throw TaskNotFoundError for final tasks with no queue + assertThrows(TaskNotFoundError.class, () + -> requestHandler.onResubscribeToTask(params, serverCallContext) + ); + } + + @Test + void testResubscribeToTask_QueueNotFound_NonFinalTask_CreatesNewQueue() throws Exception { + // Given + Task workingTask = new Task.Builder() + .id("task-1") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.WORKING)) + .build(); + TaskIdParams params = new TaskIdParams("task-1"); + EventQueue newQueue = mock(EventQueue.class); + + when(taskStore.get("task-1")).thenReturn(workingTask); + when(queueManager.tap("task-1")).thenReturn(null); + when(queueManager.createOrTap("task-1")).thenReturn(newQueue); + + // When + var result = requestHandler.onResubscribeToTask(params, serverCallContext); + + // Then + assertNotNull(result); + verify(queueManager).createOrTap("task-1"); + } + + @Test + void testResubscribeToTask_QueueExists_ReturnsPublisher() throws Exception { + // Given + Task workingTask = new Task.Builder() + .id("task-1") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.WORKING)) + .build(); + TaskIdParams params = new TaskIdParams("task-1"); + EventQueue existingQueue = mock(EventQueue.class); + + when(taskStore.get("task-1")).thenReturn(workingTask); + when(queueManager.tap("task-1")).thenReturn(existingQueue); + + // When + var result = requestHandler.onResubscribeToTask(params, serverCallContext); + + // Then + assertNotNull(result); + verify(queueManager).tap("task-1"); + verify(queueManager, never()).createOrTap(anyString()); + } + } +} diff --git a/spec/src/main/java/io/a2a/spec/TaskQueryParams.java b/spec/src/main/java/io/a2a/spec/TaskQueryParams.java index a6154d67d..92c2453c5 100644 --- a/spec/src/main/java/io/a2a/spec/TaskQueryParams.java +++ b/spec/src/main/java/io/a2a/spec/TaskQueryParams.java @@ -17,20 +17,20 @@ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) -public record TaskQueryParams(String id, @Nullable Integer historyLength, @Nullable Map metadata) { +public record TaskQueryParams(String id, int historyLength, @Nullable Map metadata) { public TaskQueryParams { Assert.checkNotNullParam("id", id); - if (historyLength != null && historyLength < 0) { + if (historyLength < 0) { throw new IllegalArgumentException("Invalid history length"); } } public TaskQueryParams(String id) { - this(id, null, null); + this(id, 0, null); } - public TaskQueryParams(String id, @Nullable Integer historyLength) { + public TaskQueryParams(String id, int historyLength) { this(id, historyLength, null); } } diff --git a/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java b/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java index 586149948..85d307f53 100644 --- a/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java +++ b/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java @@ -162,7 +162,7 @@ public HTTPRestResponse resubscribeTask(String taskId, ServerCallContext context } } - public HTTPRestResponse getTask(String taskId, @Nullable Integer historyLength, ServerCallContext context) { + public HTTPRestResponse getTask(String taskId, int historyLength, ServerCallContext context) { try { TaskQueryParams params = new TaskQueryParams(taskId, historyLength); Task task = requestHandler.onGetTask(params, context); diff --git a/transport/rest/src/test/java/io/a2a/transport/rest/handler/RestHandlerTest.java b/transport/rest/src/test/java/io/a2a/transport/rest/handler/RestHandlerTest.java index 58e74f85e..74a505236 100644 --- a/transport/rest/src/test/java/io/a2a/transport/rest/handler/RestHandlerTest.java +++ b/transport/rest/src/test/java/io/a2a/transport/rest/handler/RestHandlerTest.java @@ -26,7 +26,7 @@ public void testGetTaskSuccess() { RestHandler handler = new RestHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK); - RestHandler.HTTPRestResponse response = handler.getTask(MINIMAL_TASK.getId(),null, callContext); + RestHandler.HTTPRestResponse response = handler.getTask(MINIMAL_TASK.getId(), 0, callContext); Assertions.assertEquals(200, response.getStatusCode()); Assertions.assertEquals("application/json", response.getContentType()); @@ -43,7 +43,7 @@ public void testGetTaskSuccess() { public void testGetTaskNotFound() { RestHandler handler = new RestHandler(CARD, requestHandler, internalExecutor); - RestHandler.HTTPRestResponse response = handler.getTask("nonexistent", null, callContext); + RestHandler.HTTPRestResponse response = handler.getTask("nonexistent", 0, callContext); Assertions.assertEquals(404, response.getStatusCode()); Assertions.assertEquals("application/json", response.getContentType()); @@ -315,7 +315,7 @@ public void testHttpStatusCodeMapping() { Assertions.assertEquals(400, response.getStatusCode()); // Test 404 for not found - response = handler.getTask("nonexistent", null, callContext); + response = handler.getTask("nonexistent", 0, callContext); Assertions.assertEquals(404, response.getStatusCode()); } From d89a677569ed8ebf90b8b825956171658c287fa2 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Tue, 4 Nov 2025 15:09:20 +0000 Subject: [PATCH 15/37] =?UTF-8?q?fix:=20Wait=20for=20agent=20completion=20?= =?UTF-8?q?and=20ensure=20all=20events=20processed=20in=20blo=E2=80=A6=20(?= =?UTF-8?q?#441)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …cking calls Fixes race condition where DefaultRequestHandler.onMessageSend() returns before all task events are fully processed and persisted to TaskStore, resulting in incomplete Task objects being returned to clients (missing artifacts, incorrect state). Root Cause: - Blocking calls interrupted immediately after first event and returned to client before background event consumption completed - Agent execution and event processing happened asynchronously in background - No synchronization to ensure all events were consumed and persisted before returning Task to client Solution (4-step process): 1. Wait for agent to finish enqueueing events (5s timeout) 2. Close the queue to signal consumption can complete (breaks dependency) 3. Wait for consumption to finish processing events (2s timeout) 4. Fetch final task state from TaskStore (has all artifacts and correct state) This ensures blocking calls return complete Task objects with all artifacts and correct state, including support for fire-and-forget tasks that never emit final state events. Added unit tests: - testBlockingFireAndForgetReturnsNonFinalTask: Validates fire-and-forget pattern - testBlockingCallReturnsCompleteTaskWithArtifacts: Ensures all artifacts included Upstream: #431 --- .../DefaultRequestHandler.java | 111 ++++++++++- .../io/a2a/server/tasks/ResultAggregator.java | 39 ++-- .../AbstractA2ARequestHandlerTest.java | 3 +- .../DefaultRequestHandlerTest.java | 184 +++++++++++++++--- .../grpc/handler/GrpcHandlerTest.java | 12 +- .../jsonrpc/handler/JSONRPCHandlerTest.java | 23 ++- 6 files changed, 308 insertions(+), 64 deletions(-) diff --git a/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java b/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java index f79e059a7..a93b6238a 100644 --- a/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java +++ b/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java @@ -3,6 +3,7 @@ import static io.a2a.server.util.async.AsyncUtils.convertingProcessor; import static io.a2a.server.util.async.AsyncUtils.createTubeConfig; import static io.a2a.server.util.async.AsyncUtils.processor; +import static java.util.concurrent.TimeUnit.*; import java.util.ArrayList; import java.util.List; @@ -51,11 +52,12 @@ import io.a2a.spec.Task; import io.a2a.spec.TaskIdParams; import io.a2a.spec.TaskNotCancelableError; -import io.a2a.spec.TaskState; import io.a2a.spec.TaskNotFoundError; import io.a2a.spec.TaskPushNotificationConfig; import io.a2a.spec.TaskQueryParams; +import io.a2a.spec.TaskState; import io.a2a.spec.UnsupportedOperationError; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,6 +66,26 @@ public class DefaultRequestHandler implements RequestHandler { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultRequestHandler.class); + /** + * Timeout in seconds to wait for agent execution to complete in blocking calls. + * This allows slow agents (LLM-based, data processing, external APIs) sufficient time. + * Configurable via: a2a.blocking.agent.timeout.seconds + * Default: 30 seconds + */ + @Inject + @ConfigProperty(name = "a2a.blocking.agent.timeout.seconds", defaultValue = "30") + int agentCompletionTimeoutSeconds; + + /** + * Timeout in seconds to wait for event consumption to complete in blocking calls. + * This ensures all events are processed and persisted before returning to client. + * Configurable via: a2a.blocking.consumption.timeout.seconds + * Default: 5 seconds + */ + @Inject + @ConfigProperty(name = "a2a.blocking.consumption.timeout.seconds", defaultValue = "5") + int consumptionCompletionTimeoutSeconds; + private final AgentExecutor agentExecutor; private final TaskStore taskStore; private final QueueManager queueManager; @@ -93,6 +115,19 @@ public DefaultRequestHandler(AgentExecutor agentExecutor, TaskStore taskStore, this.requestContextBuilder = () -> new SimpleRequestContextBuilder(taskStore, false); } + /** + * For testing + */ + public static DefaultRequestHandler create(AgentExecutor agentExecutor, TaskStore taskStore, + QueueManager queueManager, PushNotificationConfigStore pushConfigStore, + PushNotificationSender pushSender, Executor executor) { + DefaultRequestHandler handler = + new DefaultRequestHandler(agentExecutor, taskStore, queueManager, pushConfigStore, pushSender, executor); + handler.agentCompletionTimeoutSeconds = 5; + handler.consumptionCompletionTimeoutSeconds = 2; + return handler; + } + @Override public Task onGetTask(TaskQueryParams params, ServerCallContext context) throws JSONRPCError { LOGGER.debug("onGetTask {}", params.id()); @@ -192,6 +227,7 @@ public EventKind onMessageSend(MessageSendParams params, ServerCallContext conte EnhancedRunnable producerRunnable = registerAndExecuteAgentAsync(taskId, mss.requestContext, queue); ResultAggregator.EventTypeAndInterrupt etai = null; + EventKind kind = null; // Declare outside try block so it's in scope for return try { // Create callback for push notifications during background event processing Runnable pushNotificationCallback = () -> sendPushNotification(taskId, resultAggregator); @@ -201,7 +237,10 @@ public EventKind onMessageSend(MessageSendParams params, ServerCallContext conte // This callback must be added before we start consuming. Otherwise, // any errors thrown by the producerRunnable are not picked up by the consumer producerRunnable.addDoneCallback(consumer.createAgentRunnableDoneCallback()); - etai = resultAggregator.consumeAndBreakOnInterrupt(consumer, blocking, pushNotificationCallback); + + // Get agent future before consuming (for blocking calls to wait for agent completion) + CompletableFuture agentFuture = runningAgents.get(taskId); + etai = resultAggregator.consumeAndBreakOnInterrupt(consumer, blocking); if (etai == null) { LOGGER.debug("No result, throwing InternalError"); @@ -210,7 +249,69 @@ public EventKind onMessageSend(MessageSendParams params, ServerCallContext conte interruptedOrNonBlocking = etai.interrupted(); LOGGER.debug("Was interrupted or non-blocking: {}", interruptedOrNonBlocking); - EventKind kind = etai.eventType(); + // For blocking calls that were interrupted (returned on first event), + // wait for agent execution and event processing BEFORE returning to client. + // This ensures the returned Task has all artifacts and current state. + // We do this HERE (not in ResultAggregator) to avoid blocking Vert.x worker threads + // during the consumption loop itself. + kind = etai.eventType(); + if (blocking && interruptedOrNonBlocking) { + // For blocking calls: ensure all events are processed before returning + // Order of operations is critical to avoid circular dependency: + // 1. Wait for agent to finish enqueueing events + // 2. Close the queue to signal consumption can complete + // 3. Wait for consumption to finish processing events + // 4. Fetch final task state from TaskStore + + try { + // Step 1: Wait for agent to finish (with configurable timeout) + if (agentFuture != null) { + try { + agentFuture.get(agentCompletionTimeoutSeconds, SECONDS); + LOGGER.debug("Agent completed for task {}", taskId); + } catch (java.util.concurrent.TimeoutException e) { + // Agent still running after timeout - that's fine, events already being processed + LOGGER.debug("Agent still running for task {} after {}s", taskId, agentCompletionTimeoutSeconds); + } + } + + // Step 2: Close the queue to signal consumption can complete + // For fire-and-forget tasks, there's no final event, so we need to close the queue + // This allows EventConsumer.consumeAll() to exit + queue.close(false, false); // graceful close, don't notify parent yet + LOGGER.debug("Closed queue for task {} to allow consumption completion", taskId); + + // Step 3: Wait for consumption to complete (now that queue is closed) + if (etai.consumptionFuture() != null) { + etai.consumptionFuture().get(consumptionCompletionTimeoutSeconds, SECONDS); + LOGGER.debug("Consumption completed for task {}", taskId); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + String msg = String.format("Error waiting for task %s completion", taskId); + LOGGER.warn(msg, e); + throw new InternalError(msg); + } catch (java.util.concurrent.ExecutionException e) { + String msg = String.format("Error during task %s execution", taskId); + LOGGER.warn(msg, e.getCause()); + throw new InternalError(msg); + } catch (java.util.concurrent.TimeoutException e) { + String msg = String.format("Timeout waiting for consumption to complete for task %s", taskId); + LOGGER.warn(msg, taskId); + throw new InternalError(msg); + } + + // Step 4: Fetch the final task state from TaskStore (all events have been processed) + Task updatedTask = taskStore.get(taskId); + if (updatedTask != null) { + kind = updatedTask; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Fetched final task for {} with state {} and {} artifacts", + taskId, updatedTask.getStatus().state(), + updatedTask.getArtifacts().size()); + } + } + } if (kind instanceof Task taskResult && !taskId.equals(taskResult.getId())) { throw new InternalError("Task ID mismatch in agent response"); } @@ -227,8 +328,8 @@ public EventKind onMessageSend(MessageSendParams params, ServerCallContext conte trackBackgroundTask(cleanupProducer(agentFuture, etai != null ? etai.consumptionFuture() : null, taskId, queue, false)); } - LOGGER.debug("Returning: {}", etai.eventType()); - return etai.eventType(); + LOGGER.debug("Returning: {}", kind); + return kind; } @Override diff --git a/server-common/src/main/java/io/a2a/server/tasks/ResultAggregator.java b/server-common/src/main/java/io/a2a/server/tasks/ResultAggregator.java index f73242491..26767a90b 100644 --- a/server-common/src/main/java/io/a2a/server/tasks/ResultAggregator.java +++ b/server-common/src/main/java/io/a2a/server/tasks/ResultAggregator.java @@ -11,20 +11,20 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import io.a2a.server.events.EventConsumer; import io.a2a.server.events.EventQueueItem; import io.a2a.spec.A2AServerException; import io.a2a.spec.Event; import io.a2a.spec.EventKind; +import io.a2a.spec.InternalError; import io.a2a.spec.JSONRPCError; import io.a2a.spec.Message; import io.a2a.spec.Task; import io.a2a.spec.TaskState; import io.a2a.spec.TaskStatusUpdateEvent; import io.a2a.util.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ResultAggregator { private static final Logger LOGGER = LoggerFactory.getLogger(ResultAggregator.class); @@ -106,10 +106,6 @@ public EventKind consumeAll(EventConsumer consumer) throws JSONRPCError { } public EventTypeAndInterrupt consumeAndBreakOnInterrupt(EventConsumer consumer, boolean blocking) throws JSONRPCError { - return consumeAndBreakOnInterrupt(consumer, blocking, null); - } - - public EventTypeAndInterrupt consumeAndBreakOnInterrupt(EventConsumer consumer, boolean blocking, Runnable eventCallback) throws JSONRPCError { Flow.Publisher allItems = consumer.consumeAll(); AtomicReference message = new AtomicReference<>(); AtomicBoolean interrupted = new AtomicBoolean(false); @@ -180,11 +176,11 @@ else if (!blocking) { shouldInterrupt = true; continueInBackground = true; } - else { - // For ALL blocking calls (both final and non-final events), use background consumption - // This ensures all events are processed and persisted to TaskStore in background - // Queue lifecycle is now managed by DefaultRequestHandler.cleanupProducer() - // which waits for BOTH agent and consumption futures before closing queues + else if (blocking) { + // For blocking calls: Interrupt to free Vert.x thread, but continue in background + // Python's async consumption doesn't block threads, but Java's does + // So we interrupt to return quickly, then rely on background consumption + // DefaultRequestHandler will fetch the final state from TaskStore shouldInterrupt = true; continueInBackground = true; if (LOGGER.isDebugEnabled()) { @@ -198,10 +194,17 @@ else if (!blocking) { interrupted.set(true); completionFuture.complete(null); - // Signal that cleanup can proceed while consumption continues in background. - // This prevents infinite hangs for fire-and-forget agents that never emit final events. - // Processing continues (return true below) and all events are still persisted to TaskStore. - consumptionCompletionFuture.complete(null); + // For blocking calls, DON'T complete consumptionCompletionFuture here. + // Let it complete naturally when subscription finishes (onComplete callback below). + // This ensures all events are processed and persisted to TaskStore before + // DefaultRequestHandler.cleanupProducer() proceeds with cleanup. + // + // For non-blocking and auth-required calls, complete immediately to allow + // cleanup to proceed while consumption continues in background. + if (!blocking) { + consumptionCompletionFuture.complete(null); + } + // else: blocking calls wait for actual consumption completion in onComplete // Continue consuming in background - keep requesting events // Note: continueInBackground is always true when shouldInterrupt is true @@ -244,8 +247,8 @@ else if (!blocking) { } } - // Background consumption continues automatically via the subscription - // returning true in the consumer function keeps the subscription alive + // Note: For blocking calls that were interrupted, the wait logic has been moved + // to DefaultRequestHandler.onMessageSend() to avoid blocking Vert.x worker threads. // Queue lifecycle is managed by DefaultRequestHandler.cleanupProducer() Throwable error = errorRef.get(); diff --git a/server-common/src/test/java/io/a2a/server/requesthandlers/AbstractA2ARequestHandlerTest.java b/server-common/src/test/java/io/a2a/server/requesthandlers/AbstractA2ARequestHandlerTest.java index 9447203e1..d654a83a6 100644 --- a/server-common/src/test/java/io/a2a/server/requesthandlers/AbstractA2ARequestHandlerTest.java +++ b/server-common/src/test/java/io/a2a/server/requesthandlers/AbstractA2ARequestHandlerTest.java @@ -98,7 +98,8 @@ public void cancel(RequestContext context, EventQueue eventQueue) throws JSONRPC PushNotificationConfigStore pushConfigStore = new InMemoryPushNotificationConfigStore(); PushNotificationSender pushSender = new BasePushNotificationSender(pushConfigStore, httpClient); - requestHandler = new DefaultRequestHandler(executor, taskStore, queueManager, pushConfigStore, pushSender, internalExecutor); + requestHandler = DefaultRequestHandler.create( + executor, taskStore, queueManager, pushConfigStore, pushSender, internalExecutor); } @AfterEach diff --git a/server-common/src/test/java/io/a2a/server/requesthandlers/DefaultRequestHandlerTest.java b/server-common/src/test/java/io/a2a/server/requesthandlers/DefaultRequestHandlerTest.java index 968c7812d..4fa3a2a15 100644 --- a/server-common/src/test/java/io/a2a/server/requesthandlers/DefaultRequestHandlerTest.java +++ b/server-common/src/test/java/io/a2a/server/requesthandlers/DefaultRequestHandlerTest.java @@ -1,7 +1,10 @@ package io.a2a.server.requesthandlers; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; @@ -16,6 +19,7 @@ import io.a2a.server.events.EventQueue; import io.a2a.server.events.InMemoryQueueManager; import io.a2a.server.tasks.InMemoryTaskStore; +import io.a2a.server.tasks.TaskUpdater; import io.a2a.spec.JSONRPCError; import io.a2a.spec.Message; import io.a2a.spec.MessageSendConfiguration; @@ -50,7 +54,7 @@ void setUp() { queueManager = new InMemoryQueueManager(taskStore); agentExecutor = new TestAgentExecutor(); - requestHandler = new DefaultRequestHandler( + requestHandler = DefaultRequestHandler.create( agentExecutor, taskStore, queueManager, @@ -418,35 +422,99 @@ void testDisconnectPersistsFinalTaskToStore() throws Exception { } /** - * Test that blocking message calls persist all events in background. - * This test proves the bug where blocking calls stop consuming events after - * the first event is returned, causing subsequent events to be lost. + * Test that blocking message call waits for agent to finish and returns complete Task + * even when agent does fire-and-forget (emits non-final state and returns). * * Expected behavior: - * 1. Blocking call returns immediately with first event (WORKING state) + * 1. Agent emits WORKING state with artifacts + * 2. Agent's execute() method returns WITHOUT emitting final state + * 3. Blocking onMessageSend() should wait for agent execution to complete + * 4. Blocking onMessageSend() should wait for all queued events to be processed + * 5. Returned Task should have WORKING state with all artifacts included + * + * This tests fire-and-forget pattern with blocking calls. + */ + @Test + @Timeout(15) + void testBlockingFireAndForgetReturnsNonFinalTask() throws Exception { + String taskId = "blocking-fire-forget-task"; + String contextId = "blocking-fire-forget-ctx"; + + Message message = new Message.Builder() + .messageId("msg-blocking-fire-forget") + .role(Message.Role.USER) + .parts(new TextPart("test message")) + .taskId(taskId) + .contextId(contextId) + .build(); + + MessageSendConfiguration config = new MessageSendConfiguration.Builder() + .blocking(true) + .build(); + + MessageSendParams params = new MessageSendParams(message, config, null); + + // Agent that does fire-and-forget: emits WORKING with artifact but never completes + agentExecutor.setExecuteCallback((context, queue) -> { + TaskUpdater updater = new TaskUpdater(context, queue); + + // Start work (WORKING state) + updater.startWork(); + + // Add artifact + updater.addArtifact( + List.of(new TextPart("Fire and forget artifact", null)), + "artifact-1", "FireForget", null); + + // Agent returns WITHOUT calling updater.complete() + // Task stays in WORKING state (non-final) + }); + + // Call blocking onMessageSend - should wait for agent to finish + Object result = requestHandler.onMessageSend(params, serverCallContext); + + // The returned result should be a Task in WORKING state with artifact + assertTrue(result instanceof Task, "Result should be a Task"); + Task returnedTask = (Task) result; + + // Verify task is in WORKING state (non-final, fire-and-forget) + assertEquals(TaskState.WORKING, returnedTask.getStatus().state(), + "Returned task should be WORKING (fire-and-forget), got: " + returnedTask.getStatus().state()); + + // Verify artifacts are included in the returned task + assertNotNull(returnedTask.getArtifacts(), + "Returned task should have artifacts"); + assertTrue(returnedTask.getArtifacts().size() >= 1, + "Returned task should have at least 1 artifact, got: " + + returnedTask.getArtifacts().size()); + } + + /** + * Test that non-blocking message call returns immediately and persists all events in background. + * + * Expected behavior: + * 1. Non-blocking call returns immediately with first event (WORKING state) * 2. Agent continues running in background and produces more events * 3. Background consumption continues and persists all events to TaskStore - * 4. Final task state (COMPLETED) is persisted despite blocking call having returned - * - * This test will FAIL before the fix is applied, demonstrating the bug. + * 4. Final task state (COMPLETED) is persisted in background */ @Test @Timeout(15) - void testBlockingMessagePersistsAllEventsInBackground() throws Exception { + void testNonBlockingMessagePersistsAllEventsInBackground() throws Exception { String taskId = "blocking-persist-task"; String contextId = "blocking-persist-ctx"; Message message = new Message.Builder() - .messageId("msg-blocking-persist") + .messageId("msg-nonblocking-persist") .role(Message.Role.USER) .parts(new TextPart("test message")) .taskId(taskId) .contextId(contextId) .build(); - // Explicitly set blocking=true (though it's the default) + // Set blocking=false for non-blocking behavior MessageSendConfiguration config = new MessageSendConfiguration.Builder() - .blocking(true) + .blocking(false) .build(); MessageSendParams params = new MessageSendParams(message, config, null); @@ -468,7 +536,7 @@ void testBlockingMessagePersistsAllEventsInBackground() throws Exception { queue.enqueueEvent(workingTask); firstEventEmitted.countDown(); - // Sleep to ensure the blocking call has returned before we emit more events + // Sleep to ensure the non-blocking call has returned before we emit more events try { Thread.sleep(1000); } catch (InterruptedException e) { @@ -485,8 +553,7 @@ void testBlockingMessagePersistsAllEventsInBackground() throws Exception { } // Emit final event (COMPLETED state) - // This event will be LOST with the current bug, because the consumer - // has already stopped after returning the first event + // This event should be persisted to TaskStore in background Task completedTask = new Task.Builder() .id(taskId) .contextId(contextId) @@ -495,21 +562,21 @@ void testBlockingMessagePersistsAllEventsInBackground() throws Exception { queue.enqueueEvent(completedTask); }); - // Call blocking onMessageSend + // Call non-blocking onMessageSend Object result = requestHandler.onMessageSend(params, serverCallContext); // Assertion 1: The immediate result should be the first event (WORKING) assertTrue(result instanceof Task, "Result should be a Task"); Task immediateTask = (Task) result; - assertTrue(immediateTask.getStatus().state() == TaskState.WORKING, - "Immediate return should show WORKING state, got: " + immediateTask.getStatus().state()); + assertEquals(TaskState.WORKING, immediateTask.getStatus().state(), + "Non-blocking should return immediately with WORKING state, got: " + immediateTask.getStatus().state()); - // At this point, the blocking call has returned, but the agent is still running + // At this point, the non-blocking call has returned, but the agent is still running // Allow the agent to emit the final COMPLETED event allowCompletion.countDown(); - // Assertion 2: Poll for the final task state to be persisted + // Assertion 2: Poll for the final task state to be persisted in background // Use polling loop instead of fixed sleep for faster and more reliable test long timeoutMs = 5000; long startTime = System.currentTimeMillis(); @@ -530,8 +597,7 @@ void testBlockingMessagePersistsAllEventsInBackground() throws Exception { completedStateFound, "Final task state should be COMPLETED (background consumption should have processed it), got: " + (persistedTask != null ? persistedTask.getStatus().state() : "null") + - " after " + (System.currentTimeMillis() - startTime) + "ms. " + - "This failure proves the bug - events after the first are lost." + " after " + (System.currentTimeMillis() - startTime) + "ms" ); } @@ -654,6 +720,80 @@ void testMainQueueClosesForFinalizedTasks() throws Exception { "Queue for finalized task should be null or closed"); } + /** + * Test that blocking message call returns a Task with ALL artifacts included. + * This reproduces the reported bug: blocking call returns before artifacts are processed. + * + * Expected behavior: + * 1. Agent emits multiple artifacts via TaskUpdater + * 2. Blocking onMessageSend() should wait for ALL events to be processed + * 3. Returned Task should have all artifacts included in COMPLETED state + * + * Bug manifestation: + * - onMessageSend() returns after first event + * - Artifacts are still being processed in background + * - Returned Task is incomplete + */ + @Test + @Timeout(15) + void testBlockingCallReturnsCompleteTaskWithArtifacts() throws Exception { + String taskId = "blocking-artifacts-task"; + String contextId = "blocking-artifacts-ctx"; + + Message message = new Message.Builder() + .messageId("msg-blocking-artifacts") + .role(Message.Role.USER) + .parts(new TextPart("test message")) + .taskId(taskId) + .contextId(contextId) + .build(); + + MessageSendConfiguration config = new MessageSendConfiguration.Builder() + .blocking(true) + .build(); + + MessageSendParams params = new MessageSendParams(message, config, null); + + // Agent that uses TaskUpdater to emit multiple artifacts (like real agents do) + agentExecutor.setExecuteCallback((context, queue) -> { + TaskUpdater updater = new TaskUpdater(context, queue); + + // Start work (WORKING state) + updater.startWork(); + + // Add first artifact + updater.addArtifact( + List.of(new TextPart("First artifact", null)), + "artifact-1", "First", null); + + // Add second artifact + updater.addArtifact( + List.of(new TextPart("Second artifact", null)), + "artifact-2", "Second", null); + + // Complete the task + updater.complete(); + }); + + // Call blocking onMessageSend - should wait for ALL events + Object result = requestHandler.onMessageSend(params, serverCallContext); + + // The returned result should be a Task with ALL artifacts + assertTrue(result instanceof Task, "Result should be a Task"); + Task returnedTask = (Task) result; + + // Verify task is completed + assertEquals(TaskState.COMPLETED, returnedTask.getStatus().state(), + "Returned task should be COMPLETED"); + + // Verify artifacts are included in the returned task + assertNotNull(returnedTask.getArtifacts(), + "Returned task should have artifacts"); + assertTrue(returnedTask.getArtifacts().size() >= 2, + "Returned task should have at least 2 artifacts, got: " + + returnedTask.getArtifacts().size()); + } + /** * Simple test agent executor that allows controlling execution timing */ diff --git a/transport/grpc/src/test/java/io/a2a/transport/grpc/handler/GrpcHandlerTest.java b/transport/grpc/src/test/java/io/a2a/transport/grpc/handler/GrpcHandlerTest.java index 50fa8979c..d7a2193bd 100644 --- a/transport/grpc/src/test/java/io/a2a/transport/grpc/handler/GrpcHandlerTest.java +++ b/transport/grpc/src/test/java/io/a2a/transport/grpc/handler/GrpcHandlerTest.java @@ -280,8 +280,8 @@ public void testOnGetPushNotificationNoPushNotifierConfig() throws Exception { @Test public void testOnSetPushNotificationNoPushNotifierConfig() throws Exception { // Create request handler without a push notifier - DefaultRequestHandler requestHandler = - new DefaultRequestHandler(executor, taskStore, queueManager, null, null, internalExecutor); + DefaultRequestHandler requestHandler = DefaultRequestHandler.create( + executor, taskStore, queueManager, null, null, internalExecutor); AgentCard card = AbstractA2ARequestHandlerTest.createAgentCard(false, true, false); GrpcHandler handler = new TestGrpcHandler(card, requestHandler, internalExecutor); String NAME = "tasks/" + AbstractA2ARequestHandlerTest.MINIMAL_TASK.getId() + "/pushNotificationConfigs/" + AbstractA2ARequestHandlerTest.MINIMAL_TASK.getId(); @@ -655,8 +655,8 @@ public void testListPushNotificationConfigNotSupported() throws Exception { @Test public void testListPushNotificationConfigNoPushConfigStore() { - DefaultRequestHandler requestHandler = - new DefaultRequestHandler(executor, taskStore, queueManager, null, null, internalExecutor); + DefaultRequestHandler requestHandler = DefaultRequestHandler.create( + executor, taskStore, queueManager, null, null, internalExecutor); GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK); agentExecutorExecute = (context, eventQueue) -> { @@ -728,8 +728,8 @@ public void testDeletePushNotificationConfigNotSupported() throws Exception { @Test public void testDeletePushNotificationConfigNoPushConfigStore() { - DefaultRequestHandler requestHandler = - new DefaultRequestHandler(executor, taskStore, queueManager, null, null, internalExecutor); + DefaultRequestHandler requestHandler = DefaultRequestHandler.create( + executor, taskStore, queueManager, null, null, internalExecutor); GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); String NAME = "tasks/" + AbstractA2ARequestHandlerTest.MINIMAL_TASK.getId() + "/pushNotificationConfigs/" + AbstractA2ARequestHandlerTest.MINIMAL_TASK.getId(); DeleteTaskPushNotificationConfigRequest request = DeleteTaskPushNotificationConfigRequest.newBuilder() diff --git a/transport/jsonrpc/src/test/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandlerTest.java b/transport/jsonrpc/src/test/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandlerTest.java index 45109cced..c2cf1f751 100644 --- a/transport/jsonrpc/src/test/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandlerTest.java +++ b/transport/jsonrpc/src/test/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandlerTest.java @@ -1112,8 +1112,8 @@ public void testPushNotificationsNotSupportedError() { @Test public void testOnGetPushNotificationNoPushNotifierConfig() { // Create request handler without a push notifier - DefaultRequestHandler requestHandler = - new DefaultRequestHandler(executor, taskStore, queueManager, null, null, internalExecutor); + DefaultRequestHandler requestHandler = DefaultRequestHandler.create( + executor, taskStore, queueManager, null, null, internalExecutor); AgentCard card = createAgentCard(false, true, false); JSONRPCHandler handler = new JSONRPCHandler(card, requestHandler, internalExecutor); @@ -1131,8 +1131,8 @@ public void testOnGetPushNotificationNoPushNotifierConfig() { @Test public void testOnSetPushNotificationNoPushNotifierConfig() { // Create request handler without a push notifier - DefaultRequestHandler requestHandler = - new DefaultRequestHandler(executor, taskStore, queueManager, null, null, internalExecutor); + DefaultRequestHandler requestHandler = DefaultRequestHandler.create( + executor, taskStore, queueManager, null, null, internalExecutor); AgentCard card = createAgentCard(false, true, false); JSONRPCHandler handler = new JSONRPCHandler(card, requestHandler, internalExecutor); @@ -1222,8 +1222,8 @@ public void testDefaultRequestHandlerWithCustomComponents() { @Test public void testOnMessageSendErrorHandling() { - DefaultRequestHandler requestHandler = - new DefaultRequestHandler(executor, taskStore, queueManager, null, null, internalExecutor); + DefaultRequestHandler requestHandler = DefaultRequestHandler.create( + executor, taskStore, queueManager, null, null, internalExecutor); AgentCard card = createAgentCard(false, true, false); JSONRPCHandler handler = new JSONRPCHandler(card, requestHandler, internalExecutor); @@ -1244,8 +1244,7 @@ public void testOnMessageSendErrorHandling() { new UnsupportedOperationError()) .when(mock).consumeAndBreakOnInterrupt( Mockito.any(EventConsumer.class), - Mockito.anyBoolean(), - Mockito.any()); + Mockito.anyBoolean()); })){ response = handler.onMessageSend(request, callContext); } @@ -1376,8 +1375,8 @@ public void testListPushNotificationConfigNotSupported() { @Test public void testListPushNotificationConfigNoPushConfigStore() { - DefaultRequestHandler requestHandler = - new DefaultRequestHandler(executor, taskStore, queueManager, null, null, internalExecutor); + DefaultRequestHandler requestHandler = DefaultRequestHandler.create( + executor, taskStore, queueManager, null, null, internalExecutor); JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK); agentExecutorExecute = (context, eventQueue) -> { @@ -1468,8 +1467,8 @@ public void testDeletePushNotificationConfigNotSupported() { @Test public void testDeletePushNotificationConfigNoPushConfigStore() { - DefaultRequestHandler requestHandler = - new DefaultRequestHandler(executor, taskStore, queueManager, null, null, internalExecutor); + DefaultRequestHandler requestHandler = DefaultRequestHandler.create( + executor, taskStore, queueManager, null, null, internalExecutor); JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK); agentExecutorExecute = (context, eventQueue) -> { From 35993575d7b2fed7d2ca312351a79bd147ec29c3 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Wed, 5 Nov 2025 11:25:30 +0000 Subject: [PATCH 16/37] =?UTF-8?q?fix:=20NPE=20with=20test=20JSONRPCHandler?= =?UTF-8?q?Test.testOnMessageNewMessageSuccessM=E2=80=A6=20(#446)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …ocks (#444) Simple fix for JSONRPCHandlerTest.testOnMessageNewMessageSuccessMocks ensuring that the DoneCallback is not null when mocking. Signed-off-by: Emmanuel Hugonnet Co-authored-by: Emmanuel Hugonnet --- .../src/main/java/io/a2a/server/events/EnhancedRunnable.java | 1 - .../io/a2a/transport/jsonrpc/handler/JSONRPCHandlerTest.java | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server-common/src/main/java/io/a2a/server/events/EnhancedRunnable.java b/server-common/src/main/java/io/a2a/server/events/EnhancedRunnable.java index 17d5b3e9a..380cb04f4 100644 --- a/server-common/src/main/java/io/a2a/server/events/EnhancedRunnable.java +++ b/server-common/src/main/java/io/a2a/server/events/EnhancedRunnable.java @@ -1,6 +1,5 @@ package io.a2a.server.events; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; diff --git a/transport/jsonrpc/src/test/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandlerTest.java b/transport/jsonrpc/src/test/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandlerTest.java index c2cf1f751..450c0e76d 100644 --- a/transport/jsonrpc/src/test/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandlerTest.java +++ b/transport/jsonrpc/src/test/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandlerTest.java @@ -186,7 +186,8 @@ public void testOnMessageNewMessageSuccessMocks() { SendMessageResponse response; try (MockedConstruction mocked = Mockito.mockConstruction( EventConsumer.class, - (mock, context) -> {Mockito.doReturn(ZeroPublisher.fromItems(wrapEvent(MINIMAL_TASK))).when(mock).consumeAll();})){ + (mock, context) -> {Mockito.doReturn(ZeroPublisher.fromItems(wrapEvent(MINIMAL_TASK))).when(mock).consumeAll(); + Mockito.doCallRealMethod().when(mock).createAgentRunnableDoneCallback();})){ response = handler.onMessageSend(request, callContext); } assertNull(response.getError()); From 41e73a812e9904d59f9c2047162787bf93323bbe Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Wed, 5 Nov 2025 16:30:24 +0000 Subject: [PATCH 17/37] chore: Release 0.3.2.Final (#450) --- client/base/pom.xml | 2 +- client/transport/grpc/pom.xml | 2 +- client/transport/jsonrpc/pom.xml | 2 +- client/transport/rest/pom.xml | 2 +- client/transport/spi/pom.xml | 2 +- common/pom.xml | 2 +- examples/cloud-deployment/server/pom.xml | 2 +- examples/helloworld/client/pom.xml | 2 +- .../java/io/a2a/examples/helloworld/HelloWorldRunner.java | 4 ++-- examples/helloworld/pom.xml | 2 +- examples/helloworld/server/pom.xml | 2 +- extras/common/pom.xml | 2 +- extras/push-notification-config-store-database-jpa/pom.xml | 2 +- extras/queue-manager-replicated/core/pom.xml | 2 +- extras/queue-manager-replicated/pom.xml | 2 +- .../queue-manager-replicated/replication-mp-reactive/pom.xml | 2 +- extras/queue-manager-replicated/tests-multi-instance/pom.xml | 2 +- .../tests-multi-instance/quarkus-app-1/pom.xml | 2 +- .../tests-multi-instance/quarkus-app-2/pom.xml | 2 +- .../tests-multi-instance/quarkus-common/pom.xml | 2 +- .../tests-multi-instance/tests/pom.xml | 2 +- extras/queue-manager-replicated/tests-single-instance/pom.xml | 2 +- extras/task-store-database-jpa/pom.xml | 2 +- http-client/pom.xml | 2 +- pom.xml | 2 +- reference/common/pom.xml | 2 +- reference/grpc/pom.xml | 2 +- reference/jsonrpc/pom.xml | 2 +- reference/rest/pom.xml | 2 +- server-common/pom.xml | 2 +- spec-grpc/pom.xml | 2 +- spec/pom.xml | 2 +- tck/pom.xml | 2 +- tests/server-common/pom.xml | 2 +- transport/grpc/pom.xml | 2 +- transport/jsonrpc/pom.xml | 2 +- transport/rest/pom.xml | 2 +- 37 files changed, 38 insertions(+), 38 deletions(-) diff --git a/client/base/pom.xml b/client/base/pom.xml index 2e9add628..5a28fa045 100644 --- a/client/base/pom.xml +++ b/client/base/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../pom.xml a2a-java-sdk-client diff --git a/client/transport/grpc/pom.xml b/client/transport/grpc/pom.xml index 9818ae4f8..9acd33759 100644 --- a/client/transport/grpc/pom.xml +++ b/client/transport/grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../../pom.xml a2a-java-sdk-client-transport-grpc diff --git a/client/transport/jsonrpc/pom.xml b/client/transport/jsonrpc/pom.xml index 4422a4ac3..9bc5266dd 100644 --- a/client/transport/jsonrpc/pom.xml +++ b/client/transport/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../../pom.xml a2a-java-sdk-client-transport-jsonrpc diff --git a/client/transport/rest/pom.xml b/client/transport/rest/pom.xml index 8b40bad9f..d7939e278 100644 --- a/client/transport/rest/pom.xml +++ b/client/transport/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../../pom.xml a2a-java-sdk-client-transport-rest diff --git a/client/transport/spi/pom.xml b/client/transport/spi/pom.xml index 3c340b70a..bcff63da7 100644 --- a/client/transport/spi/pom.xml +++ b/client/transport/spi/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../../pom.xml a2a-java-sdk-client-transport-spi diff --git a/common/pom.xml b/common/pom.xml index 1c5547e9e..809683bb7 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final a2a-java-sdk-common diff --git a/examples/cloud-deployment/server/pom.xml b/examples/cloud-deployment/server/pom.xml index 91bf77eb4..3d2084f30 100644 --- a/examples/cloud-deployment/server/pom.xml +++ b/examples/cloud-deployment/server/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../../pom.xml diff --git a/examples/helloworld/client/pom.xml b/examples/helloworld/client/pom.xml index 7ef99f7f6..20648b924 100644 --- a/examples/helloworld/client/pom.xml +++ b/examples/helloworld/client/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-examples-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final a2a-java-sdk-examples-client diff --git a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java index 6a3415371..8b2af9def 100644 --- a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java +++ b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java @@ -1,6 +1,6 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.2.Beta1-SNAPSHOT -//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.2.Beta1-SNAPSHOT +//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.2.Final +//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.2.Final //SOURCES HelloWorldClient.java /** diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 35eede3bd..f52c10bb7 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../pom.xml diff --git a/examples/helloworld/server/pom.xml b/examples/helloworld/server/pom.xml index 581fa8b19..696beda12 100644 --- a/examples/helloworld/server/pom.xml +++ b/examples/helloworld/server/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-examples-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final a2a-java-sdk-examples-server diff --git a/extras/common/pom.xml b/extras/common/pom.xml index 5e2e6212c..ff66ce183 100644 --- a/extras/common/pom.xml +++ b/extras/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../pom.xml diff --git a/extras/push-notification-config-store-database-jpa/pom.xml b/extras/push-notification-config-store-database-jpa/pom.xml index a4520e963..68881c91c 100644 --- a/extras/push-notification-config-store-database-jpa/pom.xml +++ b/extras/push-notification-config-store-database-jpa/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../pom.xml a2a-java-extras-push-notification-config-store-database-jpa diff --git a/extras/queue-manager-replicated/core/pom.xml b/extras/queue-manager-replicated/core/pom.xml index ba22098ca..1d645cc58 100644 --- a/extras/queue-manager-replicated/core/pom.xml +++ b/extras/queue-manager-replicated/core/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../pom.xml diff --git a/extras/queue-manager-replicated/pom.xml b/extras/queue-manager-replicated/pom.xml index af259d689..3d87549e3 100644 --- a/extras/queue-manager-replicated/pom.xml +++ b/extras/queue-manager-replicated/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../pom.xml diff --git a/extras/queue-manager-replicated/replication-mp-reactive/pom.xml b/extras/queue-manager-replicated/replication-mp-reactive/pom.xml index cacaaa843..e861fcbc9 100644 --- a/extras/queue-manager-replicated/replication-mp-reactive/pom.xml +++ b/extras/queue-manager-replicated/replication-mp-reactive/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/pom.xml index e611e0f88..77fbb0ad3 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml index 90e288b60..0a5726b83 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml index 70fbf329f..09e80a0b2 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml index cc4a8562d..7b5ec84fa 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml index 51773af02..0081c8c6a 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../pom.xml diff --git a/extras/queue-manager-replicated/tests-single-instance/pom.xml b/extras/queue-manager-replicated/tests-single-instance/pom.xml index df44234c7..ca55738b0 100644 --- a/extras/queue-manager-replicated/tests-single-instance/pom.xml +++ b/extras/queue-manager-replicated/tests-single-instance/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../pom.xml diff --git a/extras/task-store-database-jpa/pom.xml b/extras/task-store-database-jpa/pom.xml index 319d3d277..17bbb4a87 100644 --- a/extras/task-store-database-jpa/pom.xml +++ b/extras/task-store-database-jpa/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../pom.xml a2a-java-extras-task-store-database-jpa diff --git a/http-client/pom.xml b/http-client/pom.xml index 0a35e7232..3a069e579 100644 --- a/http-client/pom.xml +++ b/http-client/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final a2a-java-sdk-http-client diff --git a/pom.xml b/pom.xml index 316f69f59..423ffe9df 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final pom diff --git a/reference/common/pom.xml b/reference/common/pom.xml index 8becda134..a48499692 100644 --- a/reference/common/pom.xml +++ b/reference/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../pom.xml a2a-java-sdk-reference-common diff --git a/reference/grpc/pom.xml b/reference/grpc/pom.xml index 72ac58701..4655ecf02 100644 --- a/reference/grpc/pom.xml +++ b/reference/grpc/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../pom.xml diff --git a/reference/jsonrpc/pom.xml b/reference/jsonrpc/pom.xml index 48a3758d9..973cb0fcc 100644 --- a/reference/jsonrpc/pom.xml +++ b/reference/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../pom.xml a2a-java-sdk-reference-jsonrpc diff --git a/reference/rest/pom.xml b/reference/rest/pom.xml index ba83bed0e..8b37fb6c5 100644 --- a/reference/rest/pom.xml +++ b/reference/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../pom.xml a2a-java-sdk-reference-rest diff --git a/server-common/pom.xml b/server-common/pom.xml index 14ae048c3..4a5dde120 100644 --- a/server-common/pom.xml +++ b/server-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final a2a-java-sdk-server-common diff --git a/spec-grpc/pom.xml b/spec-grpc/pom.xml index 159fef48c..35a167634 100644 --- a/spec-grpc/pom.xml +++ b/spec-grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final a2a-java-sdk-spec-grpc diff --git a/spec/pom.xml b/spec/pom.xml index e1351828b..061b3cc07 100644 --- a/spec/pom.xml +++ b/spec/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final a2a-java-sdk-spec diff --git a/tck/pom.xml b/tck/pom.xml index 1bb66da05..a875330df 100644 --- a/tck/pom.xml +++ b/tck/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final a2a-tck-server diff --git a/tests/server-common/pom.xml b/tests/server-common/pom.xml index 399aa32ce..47257883d 100644 --- a/tests/server-common/pom.xml +++ b/tests/server-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../pom.xml a2a-java-sdk-tests-server-common diff --git a/transport/grpc/pom.xml b/transport/grpc/pom.xml index 1f227c2fa..cee14f61f 100644 --- a/transport/grpc/pom.xml +++ b/transport/grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../pom.xml a2a-java-sdk-transport-grpc diff --git a/transport/jsonrpc/pom.xml b/transport/jsonrpc/pom.xml index 5bf75365b..51fcd5f57 100644 --- a/transport/jsonrpc/pom.xml +++ b/transport/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../pom.xml a2a-java-sdk-transport-jsonrpc diff --git a/transport/rest/pom.xml b/transport/rest/pom.xml index e9d56ccb8..aeba89e87 100644 --- a/transport/rest/pom.xml +++ b/transport/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Beta1-SNAPSHOT + 0.3.2.Final ../../pom.xml a2a-java-sdk-transport-rest From 003a67bf09709a7c82956c49c0ff727988679229 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Wed, 5 Nov 2025 16:56:33 +0000 Subject: [PATCH 18/37] chore: Next is 0.3.3 (#451) --- client/base/pom.xml | 2 +- client/transport/grpc/pom.xml | 2 +- client/transport/jsonrpc/pom.xml | 2 +- client/transport/rest/pom.xml | 2 +- client/transport/spi/pom.xml | 2 +- common/pom.xml | 2 +- examples/cloud-deployment/server/pom.xml | 2 +- examples/helloworld/client/pom.xml | 2 +- .../java/io/a2a/examples/helloworld/HelloWorldRunner.java | 4 ++-- examples/helloworld/pom.xml | 2 +- examples/helloworld/server/pom.xml | 2 +- extras/common/pom.xml | 2 +- extras/push-notification-config-store-database-jpa/pom.xml | 2 +- extras/queue-manager-replicated/core/pom.xml | 2 +- extras/queue-manager-replicated/pom.xml | 2 +- .../queue-manager-replicated/replication-mp-reactive/pom.xml | 2 +- extras/queue-manager-replicated/tests-multi-instance/pom.xml | 2 +- .../tests-multi-instance/quarkus-app-1/pom.xml | 2 +- .../tests-multi-instance/quarkus-app-2/pom.xml | 2 +- .../tests-multi-instance/quarkus-common/pom.xml | 2 +- .../tests-multi-instance/tests/pom.xml | 2 +- extras/queue-manager-replicated/tests-single-instance/pom.xml | 2 +- extras/task-store-database-jpa/pom.xml | 2 +- http-client/pom.xml | 2 +- pom.xml | 2 +- reference/common/pom.xml | 2 +- reference/grpc/pom.xml | 2 +- reference/jsonrpc/pom.xml | 2 +- reference/rest/pom.xml | 2 +- server-common/pom.xml | 2 +- spec-grpc/pom.xml | 2 +- spec/pom.xml | 2 +- tck/pom.xml | 2 +- tests/server-common/pom.xml | 2 +- transport/grpc/pom.xml | 2 +- transport/jsonrpc/pom.xml | 2 +- transport/rest/pom.xml | 2 +- 37 files changed, 38 insertions(+), 38 deletions(-) diff --git a/client/base/pom.xml b/client/base/pom.xml index 5a28fa045..05d594414 100644 --- a/client/base/pom.xml +++ b/client/base/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-client diff --git a/client/transport/grpc/pom.xml b/client/transport/grpc/pom.xml index 9acd33759..16aefca3d 100644 --- a/client/transport/grpc/pom.xml +++ b/client/transport/grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-grpc diff --git a/client/transport/jsonrpc/pom.xml b/client/transport/jsonrpc/pom.xml index 9bc5266dd..6028a83e0 100644 --- a/client/transport/jsonrpc/pom.xml +++ b/client/transport/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-jsonrpc diff --git a/client/transport/rest/pom.xml b/client/transport/rest/pom.xml index d7939e278..a31b38b39 100644 --- a/client/transport/rest/pom.xml +++ b/client/transport/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-rest diff --git a/client/transport/spi/pom.xml b/client/transport/spi/pom.xml index bcff63da7..6de8ddaf3 100644 --- a/client/transport/spi/pom.xml +++ b/client/transport/spi/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-spi diff --git a/common/pom.xml b/common/pom.xml index 809683bb7..6e8222642 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT a2a-java-sdk-common diff --git a/examples/cloud-deployment/server/pom.xml b/examples/cloud-deployment/server/pom.xml index 3d2084f30..4170c3e5b 100644 --- a/examples/cloud-deployment/server/pom.xml +++ b/examples/cloud-deployment/server/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../../pom.xml diff --git a/examples/helloworld/client/pom.xml b/examples/helloworld/client/pom.xml index 20648b924..acffe5706 100644 --- a/examples/helloworld/client/pom.xml +++ b/examples/helloworld/client/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-examples-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT a2a-java-sdk-examples-client diff --git a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java index 8b2af9def..219d5044f 100644 --- a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java +++ b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java @@ -1,6 +1,6 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.2.Final -//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.2.Final +//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.3.Beta1-SNAPSHOT +//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.3.Beta1-SNAPSHOT //SOURCES HelloWorldClient.java /** diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index f52c10bb7..290cad120 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../pom.xml diff --git a/examples/helloworld/server/pom.xml b/examples/helloworld/server/pom.xml index 696beda12..44b895b74 100644 --- a/examples/helloworld/server/pom.xml +++ b/examples/helloworld/server/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-examples-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT a2a-java-sdk-examples-server diff --git a/extras/common/pom.xml b/extras/common/pom.xml index ff66ce183..4fdbcafac 100644 --- a/extras/common/pom.xml +++ b/extras/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../pom.xml diff --git a/extras/push-notification-config-store-database-jpa/pom.xml b/extras/push-notification-config-store-database-jpa/pom.xml index 68881c91c..63b12c4c4 100644 --- a/extras/push-notification-config-store-database-jpa/pom.xml +++ b/extras/push-notification-config-store-database-jpa/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../pom.xml a2a-java-extras-push-notification-config-store-database-jpa diff --git a/extras/queue-manager-replicated/core/pom.xml b/extras/queue-manager-replicated/core/pom.xml index 1d645cc58..752c126cb 100644 --- a/extras/queue-manager-replicated/core/pom.xml +++ b/extras/queue-manager-replicated/core/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/pom.xml b/extras/queue-manager-replicated/pom.xml index 3d87549e3..9f801ffdd 100644 --- a/extras/queue-manager-replicated/pom.xml +++ b/extras/queue-manager-replicated/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../pom.xml diff --git a/extras/queue-manager-replicated/replication-mp-reactive/pom.xml b/extras/queue-manager-replicated/replication-mp-reactive/pom.xml index e861fcbc9..923822e48 100644 --- a/extras/queue-manager-replicated/replication-mp-reactive/pom.xml +++ b/extras/queue-manager-replicated/replication-mp-reactive/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/pom.xml index 77fbb0ad3..273653c92 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml index 0a5726b83..86a1e24c0 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml index 09e80a0b2..8e591d84d 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml index 7b5ec84fa..48da517d8 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml index 0081c8c6a..45fd49427 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-single-instance/pom.xml b/extras/queue-manager-replicated/tests-single-instance/pom.xml index ca55738b0..8e707e80a 100644 --- a/extras/queue-manager-replicated/tests-single-instance/pom.xml +++ b/extras/queue-manager-replicated/tests-single-instance/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/task-store-database-jpa/pom.xml b/extras/task-store-database-jpa/pom.xml index 17bbb4a87..5112fd6de 100644 --- a/extras/task-store-database-jpa/pom.xml +++ b/extras/task-store-database-jpa/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../pom.xml a2a-java-extras-task-store-database-jpa diff --git a/http-client/pom.xml b/http-client/pom.xml index 3a069e579..32bd39099 100644 --- a/http-client/pom.xml +++ b/http-client/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT a2a-java-sdk-http-client diff --git a/pom.xml b/pom.xml index 423ffe9df..98dbe8bf4 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT pom diff --git a/reference/common/pom.xml b/reference/common/pom.xml index a48499692..64e15bcde 100644 --- a/reference/common/pom.xml +++ b/reference/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-reference-common diff --git a/reference/grpc/pom.xml b/reference/grpc/pom.xml index 4655ecf02..e11c110cc 100644 --- a/reference/grpc/pom.xml +++ b/reference/grpc/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../pom.xml diff --git a/reference/jsonrpc/pom.xml b/reference/jsonrpc/pom.xml index 973cb0fcc..7ca82c8c2 100644 --- a/reference/jsonrpc/pom.xml +++ b/reference/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-reference-jsonrpc diff --git a/reference/rest/pom.xml b/reference/rest/pom.xml index 8b37fb6c5..a8422ca43 100644 --- a/reference/rest/pom.xml +++ b/reference/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-reference-rest diff --git a/server-common/pom.xml b/server-common/pom.xml index 4a5dde120..1476e17a6 100644 --- a/server-common/pom.xml +++ b/server-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT a2a-java-sdk-server-common diff --git a/spec-grpc/pom.xml b/spec-grpc/pom.xml index 35a167634..0bddbc336 100644 --- a/spec-grpc/pom.xml +++ b/spec-grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT a2a-java-sdk-spec-grpc diff --git a/spec/pom.xml b/spec/pom.xml index 061b3cc07..c9838c801 100644 --- a/spec/pom.xml +++ b/spec/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT a2a-java-sdk-spec diff --git a/tck/pom.xml b/tck/pom.xml index a875330df..7f1679966 100644 --- a/tck/pom.xml +++ b/tck/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT a2a-tck-server diff --git a/tests/server-common/pom.xml b/tests/server-common/pom.xml index 47257883d..effa1f1df 100644 --- a/tests/server-common/pom.xml +++ b/tests/server-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-tests-server-common diff --git a/transport/grpc/pom.xml b/transport/grpc/pom.xml index cee14f61f..bfcb8be76 100644 --- a/transport/grpc/pom.xml +++ b/transport/grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-transport-grpc diff --git a/transport/jsonrpc/pom.xml b/transport/jsonrpc/pom.xml index 51fcd5f57..bf9bf4a82 100644 --- a/transport/jsonrpc/pom.xml +++ b/transport/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-transport-jsonrpc diff --git a/transport/rest/pom.xml b/transport/rest/pom.xml index aeba89e87..9f9eacea4 100644 --- a/transport/rest/pom.xml +++ b/transport/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.2.Final + 0.3.3.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-transport-rest From 19d5b401b01cf2be0657b8f4a519b70fb40eb476 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Thu, 20 Nov 2025 13:12:36 +0000 Subject: [PATCH 19/37] feat!: Remove hard dependency on MicroProfile Config from the core SDK (#469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Default values will now be used by default. You can supply your own by providing a CDI bean with a higher priority. Also, there is a new a2a-java-sdk-microprofile-config with the previous MicroProfile Config capabilities. If used, this will allow MicroProfile Config configurations of the properties. The reference implementations use this new module Fixes #467 🦕 Upstream: #468 --- README.md | 40 +++-- .../database/jpa/JpaDatabaseTaskStore.java | 20 ++- .../META-INF/a2a-defaults.properties | 6 + integrations/microprofile-config/README.md | 148 ++++++++++++++++++ integrations/microprofile-config/pom.xml | 59 +++++++ .../MicroProfileConfigProvider.java | 78 +++++++++ .../MicroProfileConfigProviderTest.java | 102 ++++++++++++ .../src/test/resources/application.properties | 14 ++ pom.xml | 6 + reference/common/pom.xml | 4 + server-common/pom.xml | 4 - .../a2a/server/config/A2AConfigProvider.java | 35 +++++ .../config/DefaultValuesConfigProvider.java | 96 ++++++++++++ .../DefaultRequestHandler.java | 32 ++-- .../util/async/AsyncExecutorProducer.java | 36 ++++- .../META-INF/a2a-defaults.properties | 21 +++ 16 files changed, 666 insertions(+), 35 deletions(-) create mode 100644 extras/task-store-database-jpa/src/main/resources/META-INF/a2a-defaults.properties create mode 100644 integrations/microprofile-config/README.md create mode 100644 integrations/microprofile-config/pom.xml create mode 100644 integrations/microprofile-config/src/main/java/io/a2a/integrations/microprofile/MicroProfileConfigProvider.java create mode 100644 integrations/microprofile-config/src/test/java/io/a2a/integrations/microprofile/MicroProfileConfigProviderTest.java create mode 100644 integrations/microprofile-config/src/test/resources/application.properties create mode 100644 server-common/src/main/java/io/a2a/server/config/A2AConfigProvider.java create mode 100644 server-common/src/main/java/io/a2a/server/config/DefaultValuesConfigProvider.java create mode 100644 server-common/src/main/resources/META-INF/a2a-defaults.properties diff --git a/README.md b/README.md index f5228e0c0..45c0fa1d9 100644 --- a/README.md +++ b/README.md @@ -232,11 +232,22 @@ public class WeatherAgentExecutorProducer { } ``` -### 4. Configure Executor Settings (Optional) +### 4. Configuration System -The A2A Java SDK uses a dedicated executor for handling asynchronous operations like streaming subscriptions. By default, this executor is configured with a core pool size of 5 threads and a maximum pool size of 50 threads, optimized for I/O-bound operations. +The A2A Java SDK uses a flexible configuration system that works across different frameworks. -You can customize the executor settings in your `application.properties`: +**Default behavior:** Configuration values come from `META-INF/a2a-defaults.properties` files on the classpath (provided by core modules and extras). These defaults work out of the box without any additional setup. + +**Customizing configuration:** +- **Quarkus/MicroProfile Config users**: Add the [`microprofile-config`](integrations/microprofile-config/README.md) integration to override defaults via `application.properties`, environment variables, or system properties +- **Spring/other frameworks**: See the [integration module README](integrations/microprofile-config/README.md#custom-config-providers) for how to implement a custom `A2AConfigProvider` +- **Reference implementations**: Already include the MicroProfile Config integration + +#### Configuration Properties + +**Executor Settings** (Optional) + +The SDK uses a dedicated executor for async operations like streaming. Default: 5 core threads, 50 max threads. ```properties # Core thread pool size for the @Internal executor (default: 5) @@ -249,20 +260,23 @@ a2a.executor.max-pool-size=50 a2a.executor.keep-alive-seconds=60 ``` -**Why this matters:** -- **Streaming Performance**: The executor handles streaming subscriptions. Too few threads can cause timeouts under concurrent load. -- **Resource Management**: The dedicated executor prevents streaming operations from competing with the ForkJoinPool used by other async tasks. -- **Concurrency**: In production environments with high concurrent streaming requests, increase the pool sizes accordingly. +**Blocking Call Timeouts** (Optional) -**Default Configuration:** ```properties -# These are the defaults - no need to set unless you want different values -a2a.executor.core-pool-size=5 -a2a.executor.max-pool-size=50 -a2a.executor.keep-alive-seconds=60 +# Timeout for agent execution in blocking calls (default: 30 seconds) +a2a.blocking.agent.timeout.seconds=30 + +# Timeout for event consumption in blocking calls (default: 5 seconds) +a2a.blocking.consumption.timeout.seconds=5 ``` -**Note:** The reference server implementations automatically configure this executor. If you're creating a custom server integration, ensure you provide an `@Internal Executor` bean for optimal streaming performance. +**Why this matters:** +- **Streaming Performance**: The executor handles streaming subscriptions. Too few threads can cause timeouts under concurrent load. +- **Resource Management**: The dedicated executor prevents streaming operations from competing with the ForkJoinPool. +- **Concurrency**: In production with high concurrent streaming, increase pool sizes accordingly. +- **Agent Timeouts**: LLM-based agents may need longer timeouts (60-120s) compared to simple agents. + +**Note:** The reference server implementations (Quarkus-based) automatically include the MicroProfile Config integration, so properties work out of the box in `application.properties`. ## A2A Client diff --git a/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java b/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java index 44837ae85..edfbfaf69 100644 --- a/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java +++ b/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java @@ -3,6 +3,7 @@ import java.time.Duration; import java.time.Instant; +import jakarta.annotation.PostConstruct; import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Event; @@ -14,10 +15,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import io.a2a.extras.common.events.TaskFinalizedEvent; +import io.a2a.server.config.A2AConfigProvider; import io.a2a.server.tasks.TaskStateProvider; import io.a2a.server.tasks.TaskStore; import io.a2a.spec.Task; -import org.eclipse.microprofile.config.inject.ConfigProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,9 +36,24 @@ public class JpaDatabaseTaskStore implements TaskStore, TaskStateProvider { Event taskFinalizedEvent; @Inject - @ConfigProperty(name = "a2a.replication.grace-period-seconds", defaultValue = "15") + A2AConfigProvider configProvider; + + /** + * Grace period for task finalization in replicated scenarios (seconds). + * After a task reaches a final state, this is the minimum time to wait before cleanup + * to allow replicated events to arrive and be processed. + *

+ * Property: {@code a2a.replication.grace-period-seconds}
+ * Default: 15
+ * Note: Property override requires a configurable {@link A2AConfigProvider} on the classpath. + */ long gracePeriodSeconds; + @PostConstruct + void initConfig() { + gracePeriodSeconds = Long.parseLong(configProvider.getValue("a2a.replication.grace-period-seconds")); + } + @Transactional @Override public void save(Task task) { diff --git a/extras/task-store-database-jpa/src/main/resources/META-INF/a2a-defaults.properties b/extras/task-store-database-jpa/src/main/resources/META-INF/a2a-defaults.properties new file mode 100644 index 000000000..c01c5e60a --- /dev/null +++ b/extras/task-store-database-jpa/src/main/resources/META-INF/a2a-defaults.properties @@ -0,0 +1,6 @@ +# A2A JPA Database Task Store Default Configuration + +# Grace period for task finalization in replicated scenarios (seconds) +# After a task reaches a final state, this is the minimum time to wait before cleanup +# to allow replicated events to arrive and be processed +a2a.replication.grace-period-seconds=15 diff --git a/integrations/microprofile-config/README.md b/integrations/microprofile-config/README.md new file mode 100644 index 000000000..501a0dd78 --- /dev/null +++ b/integrations/microprofile-config/README.md @@ -0,0 +1,148 @@ +# A2A Java SDK - MicroProfile Config Integration + +This optional integration module provides MicroProfile Config support for the A2A Java SDK configuration system. + +## Overview + +The A2A Java SDK core uses the `A2AConfigProvider` interface for configuration, with default values loaded from `META-INF/a2a-defaults.properties` files on the classpath. + +This module provides `MicroProfileConfigProvider`, which integrates with MicroProfile Config to allow configuration via: +- `application.properties` +- Environment variables +- System properties (`-D` flags) +- Custom ConfigSources + +## Quick Start + +### 1. Add Dependency + +```xml + + io.github.a2asdk + a2a-java-sdk-microprofile-config + ${io.a2a.sdk.version} + +``` + +### 2. Configure Properties + +Once the dependency is added, you can override any A2A configuration property: + +**application.properties:** +```properties +# Executor configuration +a2a.executor.core-pool-size=10 +a2a.executor.max-pool-size=100 + +# Timeout configuration +a2a.blocking.agent.timeout.seconds=60 +a2a.blocking.consumption.timeout.seconds=10 +``` + +**Environment variables:** +```bash +export A2A_EXECUTOR_CORE_POOL_SIZE=10 +export A2A_BLOCKING_AGENT_TIMEOUT_SECONDS=60 +``` + +**System properties:** +```bash +java -Da2a.executor.core-pool-size=10 -jar your-app.jar +``` + +## How It Works + +The `MicroProfileConfigProvider` implementation: + +1. **First tries MicroProfile Config** - Checks `application.properties`, environment variables, system properties, and custom ConfigSources +2. **Falls back to defaults** - If not found, uses values from `META-INF/a2a-defaults.properties` provided by core modules and extras +3. **Priority 50** - Can be overridden by custom providers with higher priority + +## Configuration Fallback Chain + +``` +MicroProfile Config Sources (application.properties, env vars, -D flags) + ↓ (not found?) +DefaultValuesConfigProvider + → Scans classpath for ALL META-INF/a2a-defaults.properties files + → Merges all discovered properties together + → Throws exception if duplicate keys found + ↓ (property exists?) +Return merged default value + ↓ (not found?) +IllegalArgumentException +``` + +**Note**: All `META-INF/a2a-defaults.properties` files (from server-common, extras modules, etc.) are loaded and merged together by `DefaultValuesConfigProvider` at startup. This is not a sequential fallback chain, but a single merged set of defaults. + +## Available Configuration Properties + +See the [main README](../../README.md#configuration-system) for a complete list of configuration properties. + +## Framework Compatibility + +This module works with any MicroProfile Config implementation: + +- **Quarkus** - Built-in MicroProfile Config support +- **Helidon** - Built-in MicroProfile Config support +- **Open Liberty** - Built-in MicroProfile Config support +- **WildFly/JBoss EAP** - Add `smallrye-config` dependency +- **Other Jakarta EE servers** - Add MicroProfile Config implementation + +## Custom Config Providers + +If you're using a different framework (Spring, Micronaut, etc.), you can implement your own `A2AConfigProvider`: + +```java +import io.a2a.server.config.A2AConfigProvider; +import io.a2a.server.config.DefaultValuesConfigProvider; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Alternative; +import jakarta.annotation.Priority; +import jakarta.inject.Inject; + +@ApplicationScoped +@Alternative +@Priority(100) // Higher than MicroProfileConfigProvider's priority of 50 +public class OtherEnvironmentConfigProvider implements A2AConfigProvider { + + @Inject + Environment env; + + @Inject + DefaultValuesConfigProvider defaultValues; + + @Override + public String getValue(String name) { + String value = env.getProperty(name); + if (value != null) { + return value; + } + // Fallback to defaults + return defaultValues.getValue(name); + } + + @Override + public Optional getOptionalValue(String name) { + String value = env.getProperty(name); + if (value != null) { + return Optional.of(value); + } + return defaultValues.getOptionalValue(name); + } +} +``` + +## Implementation Details + +- **Package**: `io.a2a.integrations.microprofile` +- **Class**: `MicroProfileConfigProvider` +- **Priority**: 50 (can be overridden) +- **Scope**: `@ApplicationScoped` +- **Dependencies**: MicroProfile Config API, A2A SDK server-common + +## Reference Implementations + +The A2A Java SDK reference implementations (Quarkus-based) automatically include this integration module, so MicroProfile Config properties work out of the box. + +If you're building a custom server implementation, add this dependency to enable property-based configuration. diff --git a/integrations/microprofile-config/pom.xml b/integrations/microprofile-config/pom.xml new file mode 100644 index 000000000..29b9f750d --- /dev/null +++ b/integrations/microprofile-config/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + + io.github.a2asdk + a2a-java-sdk-parent + 0.3.3.Beta1-SNAPSHOT + ../../pom.xml + + a2a-java-sdk-microprofile-config + + jar + + A2A Java SDK - MicroProfile Config Integration + MicroProfile Config integration for A2A Java SDK - provides A2AConfigProvider implementation + + + + ${project.groupId} + a2a-java-sdk-server-common + + + jakarta.enterprise + jakarta.enterprise.cdi-api + + + jakarta.inject + jakarta.inject-api + + + org.eclipse.microprofile.config + microprofile-config-api + + + org.slf4j + slf4j-api + + + + + io.quarkus + quarkus-arc + test + + + io.quarkus + quarkus-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + diff --git a/integrations/microprofile-config/src/main/java/io/a2a/integrations/microprofile/MicroProfileConfigProvider.java b/integrations/microprofile-config/src/main/java/io/a2a/integrations/microprofile/MicroProfileConfigProvider.java new file mode 100644 index 000000000..666c2d612 --- /dev/null +++ b/integrations/microprofile-config/src/main/java/io/a2a/integrations/microprofile/MicroProfileConfigProvider.java @@ -0,0 +1,78 @@ +package io.a2a.integrations.microprofile; + +import java.util.Optional; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Alternative; +import jakarta.annotation.Priority; +import jakarta.inject.Inject; + +import io.a2a.server.config.A2AConfigProvider; +import io.a2a.server.config.DefaultValuesConfigProvider; +import org.eclipse.microprofile.config.Config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * MicroProfile Config-based implementation of {@link A2AConfigProvider}. + *

+ * This provider integrates with MicroProfile Config (used by Quarkus and other Jakarta EE runtimes) + * to allow configuration via standard sources: + *

    + *
  • System properties (-D flags)
  • + *
  • Environment variables
  • + *
  • application.properties
  • + *
  • Custom ConfigSources
  • + *
+ *

+ * Falls back to {@link DefaultValuesConfigProvider} when a configuration value is not found + * in MicroProfile Config, ensuring that default values from {@code META-INF/a2a-defaults.properties} + * are always available. + *

+ * This provider is automatically enabled with {@code @Priority(50)}, but can be overridden by + * custom providers with higher priority. + *

+ * To use this provider, add the {@code a2a-java-sdk-microprofile-config} dependency to your project. + */ +@ApplicationScoped +@Alternative +@Priority(50) +public class MicroProfileConfigProvider implements A2AConfigProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(MicroProfileConfigProvider.class); + + @Inject + Config mpConfig; + + @Inject + DefaultValuesConfigProvider defaultValues; + + @Override + public String getValue(String name) { + Optional value = mpConfig.getOptionalValue(name, String.class); + if (value.isPresent()) { + LOGGER.trace("Config value '{}' = '{}' (from MicroProfile Config)", name, value.get()); + return value.get(); + } + + // Fallback to defaults + String defaultValue = defaultValues.getValue(name); + LOGGER.trace("Config value '{}' = '{}' (from DefaultValuesConfigProvider)", name, defaultValue); + return defaultValue; + } + + @Override + public Optional getOptionalValue(String name) { + Optional value = mpConfig.getOptionalValue(name, String.class); + if (value.isPresent()) { + LOGGER.trace("Optional config value '{}' = '{}' (from MicroProfile Config)", name, value.get()); + return value; + } + + // Fallback to defaults + Optional defaultValue = defaultValues.getOptionalValue(name); + LOGGER.trace("Optional config value '{}' = '{}' (from DefaultValuesConfigProvider)", + name, defaultValue.orElse("")); + return defaultValue; + } +} diff --git a/integrations/microprofile-config/src/test/java/io/a2a/integrations/microprofile/MicroProfileConfigProviderTest.java b/integrations/microprofile-config/src/test/java/io/a2a/integrations/microprofile/MicroProfileConfigProviderTest.java new file mode 100644 index 000000000..1c245baf7 --- /dev/null +++ b/integrations/microprofile-config/src/test/java/io/a2a/integrations/microprofile/MicroProfileConfigProviderTest.java @@ -0,0 +1,102 @@ +package io.a2a.integrations.microprofile; + +import io.a2a.server.config.A2AConfigProvider; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * CDI-based test to verify that MicroProfileConfigProvider is properly selected + * and works correctly with MicroProfile Config and fallback to defaults. + */ +@QuarkusTest +public class MicroProfileConfigProviderTest { + + @Inject + A2AConfigProvider configProvider; + + @Test + public void testIsMicroProfileConfigProvider() { + // Verify that when microprofile-config module is on classpath, + // the injected A2AConfigProvider is the MicroProfile implementation + assertInstanceOf(MicroProfileConfigProvider.class, configProvider, + "A2AConfigProvider should be MicroProfileConfigProvider when module is present"); + } + + @Test + public void testGetValueFromMicroProfileConfig() { + // Test that values from application.properties override defaults + // The test application.properties sets a2a.executor.core-pool-size=15 + String value = configProvider.getValue("a2a.executor.core-pool-size"); + assertEquals("15", value, "Should get value from MicroProfile Config (application.properties)"); + } + + @Test + public void testGetValueFallbackToDefaults() { + // Test that values not in application.properties fall back to META-INF/a2a-defaults.properties + // a2a.executor.max-pool-size is not in test application.properties, so should use default + String value = configProvider.getValue("a2a.executor.max-pool-size"); + assertEquals("50", value, "Should fall back to default value from META-INF/a2a-defaults.properties"); + } + + @Test + public void testGetValueAnotherDefault() { + // Test another default property to ensure fallback works + String value = configProvider.getValue("a2a.executor.keep-alive-seconds"); + assertEquals("60", value, "Should fall back to default value"); + } + + @Test + public void testGetOptionalValueFromMicroProfileConfig() { + // Test optional value that exists in application.properties + Optional value = configProvider.getOptionalValue("a2a.executor.core-pool-size"); + assertTrue(value.isPresent(), "Optional value should be present"); + assertEquals("15", value.get(), "Should get overridden value from MicroProfile Config"); + } + + @Test + public void testGetOptionalValueFallbackToDefaults() { + // Test optional value that falls back to defaults + Optional value = configProvider.getOptionalValue("a2a.executor.max-pool-size"); + assertTrue(value.isPresent(), "Optional value should be present from defaults"); + assertEquals("50", value.get(), "Should get default value"); + } + + @Test + public void testGetOptionalValueNotFound() { + // Test optional value that doesn't exist anywhere + Optional value = configProvider.getOptionalValue("non.existent.property"); + assertFalse(value.isPresent(), "Optional value should be empty for non-existent property"); + } + + @Test + public void testGetValueThrowsForNonExistent() { + // Test that required getValue() throws for non-existent property + assertThrows(IllegalArgumentException.class, + () -> configProvider.getValue("non.existent.property"), + "Should throw IllegalArgumentException for non-existent required property"); + } + + @Test + public void testSystemPropertyOverride() { + // System properties should have higher priority than application.properties + // Set a system property and verify it's used + String originalValue = System.getProperty("a2a.test.system.property"); + try { + System.setProperty("a2a.test.system.property", "from-system-property"); + String value = configProvider.getValue("a2a.test.system.property"); + assertEquals("from-system-property", value, + "System property should override application.properties"); + } finally { + if (originalValue != null) { + System.setProperty("a2a.test.system.property", originalValue); + } else { + System.clearProperty("a2a.test.system.property"); + } + } + } +} diff --git a/integrations/microprofile-config/src/test/resources/application.properties b/integrations/microprofile-config/src/test/resources/application.properties new file mode 100644 index 000000000..a79c9b843 --- /dev/null +++ b/integrations/microprofile-config/src/test/resources/application.properties @@ -0,0 +1,14 @@ +# Test configuration for MicroProfileConfigProviderTest +# This overrides the default value to verify MicroProfile Config integration works + +# Override default value (default is 5) +a2a.executor.core-pool-size=15 + +# Note: a2a.executor.max-pool-size is NOT set here to test fallback to defaults +# Default value should be 50 from META-INF/a2a-defaults.properties + +# Exclude beans that aren't needed for config testing +quarkus.arc.exclude-types=io.a2a.server.requesthandlers.*,io.a2a.server.agentexecution.*,io.a2a.server.tasks.*,io.a2a.server.events.*,io.a2a.server.util.* + +# Property that will be overridden by a system property +a2a.test.system.property=from-application-properties \ No newline at end of file diff --git a/pom.xml b/pom.xml index 98dbe8bf4..eba4b8985 100644 --- a/pom.xml +++ b/pom.xml @@ -125,6 +125,11 @@ a2a-java-sdk-server-common ${project.version} + + ${project.groupId} + a2a-java-sdk-microprofile-config + ${project.version} + ${project.groupId} a2a-java-extras-common @@ -445,6 +450,7 @@ extras/push-notification-config-store-database-jpa extras/queue-manager-replicated http-client + integrations/microprofile-config reference/common reference/grpc reference/jsonrpc diff --git a/reference/common/pom.xml b/reference/common/pom.xml index 64e15bcde..dcd5781f6 100644 --- a/reference/common/pom.xml +++ b/reference/common/pom.xml @@ -49,6 +49,10 @@ org.slf4j slf4j-api + + ${project.groupId} + a2a-java-sdk-microprofile-config + io.quarkus quarkus-junit5 diff --git a/server-common/pom.xml b/server-common/pom.xml index 1476e17a6..f14b471a8 100644 --- a/server-common/pom.xml +++ b/server-common/pom.xml @@ -91,10 +91,6 @@ logback-classic test - - org.eclipse.microprofile.config - microprofile-config-api - diff --git a/server-common/src/main/java/io/a2a/server/config/A2AConfigProvider.java b/server-common/src/main/java/io/a2a/server/config/A2AConfigProvider.java new file mode 100644 index 000000000..ccd442e1c --- /dev/null +++ b/server-common/src/main/java/io/a2a/server/config/A2AConfigProvider.java @@ -0,0 +1,35 @@ +package io.a2a.server.config; + +import java.util.Optional; + +/** + * Configuration provider interface for A2A SDK configuration values. + *

+ * Implementations can obtain configuration from various sources: + *

    + *
  • {@link DefaultValuesConfigProvider} - Loads from META-INF/a2a-defaults.properties on classpath
  • + *
  • MicroProfileConfigProvider - Delegates to MicroProfile Config (reference implementations)
  • + *
  • Custom implementations - Can integrate with any configuration system
  • + *
+ *

+ * All configuration values are returned as strings. Consumers are responsible for type conversion. + */ +public interface A2AConfigProvider { + + /** + * Get a required configuration value. + * + * @param name the configuration property name + * @return the configuration value + * @throws IllegalArgumentException if the configuration value is not found + */ + String getValue(String name); + + /** + * Get an optional configuration value. + * + * @param name the configuration property name + * @return an Optional containing the value if present, empty otherwise + */ + Optional getOptionalValue(String name); +} diff --git a/server-common/src/main/java/io/a2a/server/config/DefaultValuesConfigProvider.java b/server-common/src/main/java/io/a2a/server/config/DefaultValuesConfigProvider.java new file mode 100644 index 000000000..f2375ae57 --- /dev/null +++ b/server-common/src/main/java/io/a2a/server/config/DefaultValuesConfigProvider.java @@ -0,0 +1,96 @@ +package io.a2a.server.config; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Default configuration provider that loads values from {@code META-INF/a2a-defaults.properties} + * files on the classpath. + *

+ * Each module (server-common, extras, etc.) can contribute a {@code META-INF/a2a-defaults.properties} + * file with default configuration values. All files are discovered and merged at startup. + *

+ * If duplicate keys are found across different properties files, initialization will fail with + * an exception to prevent ambiguous configuration. + */ +@ApplicationScoped +public class DefaultValuesConfigProvider implements A2AConfigProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultValuesConfigProvider.class); + private static final String DEFAULTS_RESOURCE = "META-INF/a2a-defaults.properties"; + + private final Map defaults = new HashMap<>(); + + @PostConstruct + void init() { + loadDefaultsFromClasspath(); + } + + private void loadDefaultsFromClasspath() { + try { + Enumeration resources = Thread.currentThread() + .getContextClassLoader() + .getResources(DEFAULTS_RESOURCE); + + Map sourceTracker = new HashMap<>(); // Track which file each key came from + + while (resources.hasMoreElements()) { + URL url = resources.nextElement(); + LOGGER.debug("Loading A2A defaults from: {}", url); + + Properties props = new Properties(); + try (InputStream is = url.openStream()) { + props.load(is); + + // Check for duplicates and merge + for (String key : props.stringPropertyNames()) { + String value = props.getProperty(key); + String existingSource = sourceTracker.get(key); + + if (existingSource != null) { + throw new IllegalStateException(String.format( + "Duplicate configuration key '%s' found in multiple a2a-defaults.properties files: %s and %s", + key, existingSource, url)); + } + + defaults.put(key, value); + sourceTracker.put(key, url.toString()); + LOGGER.trace("Loaded default: {} = {}", key, value); + } + } + } + + LOGGER.info("Loaded {} A2A default configuration values from {} resource(s)", + defaults.size(), sourceTracker.values().stream().distinct().count()); + + } catch (IOException e) { + throw new RuntimeException("Failed to load A2A default configuration from classpath", e); + } + } + + @Override + public String getValue(String name) { + String value = defaults.get(name); + if (value == null) { + throw new IllegalArgumentException("No default configuration value found for: " + name); + } + return value; + } + + @Override + public Optional getOptionalValue(String name) { + return Optional.ofNullable(defaults.get(name)); + } +} diff --git a/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java b/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java index a93b6238a..577e571c9 100644 --- a/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java +++ b/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java @@ -57,7 +57,8 @@ import io.a2a.spec.TaskQueryParams; import io.a2a.spec.TaskState; import io.a2a.spec.UnsupportedOperationError; -import org.eclipse.microprofile.config.inject.ConfigProperty; +import io.a2a.server.config.A2AConfigProvider; +import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,24 +67,29 @@ public class DefaultRequestHandler implements RequestHandler { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultRequestHandler.class); + @Inject + A2AConfigProvider configProvider; + /** * Timeout in seconds to wait for agent execution to complete in blocking calls. * This allows slow agents (LLM-based, data processing, external APIs) sufficient time. - * Configurable via: a2a.blocking.agent.timeout.seconds - * Default: 30 seconds + *

+ * Property: {@code a2a.blocking.agent.timeout.seconds}
+ * Default: 30 seconds
+ * Note: Property override requires a configurable {@link A2AConfigProvider} on the classpath + * (e.g., MicroProfileConfigProvider in reference implementations). */ - @Inject - @ConfigProperty(name = "a2a.blocking.agent.timeout.seconds", defaultValue = "30") int agentCompletionTimeoutSeconds; /** * Timeout in seconds to wait for event consumption to complete in blocking calls. * This ensures all events are processed and persisted before returning to client. - * Configurable via: a2a.blocking.consumption.timeout.seconds - * Default: 5 seconds + *

+ * Property: {@code a2a.blocking.consumption.timeout.seconds}
+ * Default: 5 seconds
+ * Note: Property override requires a configurable {@link A2AConfigProvider} on the classpath + * (e.g., MicroProfileConfigProvider in reference implementations). */ - @Inject - @ConfigProperty(name = "a2a.blocking.consumption.timeout.seconds", defaultValue = "5") int consumptionCompletionTimeoutSeconds; private final AgentExecutor agentExecutor; @@ -115,6 +121,14 @@ public DefaultRequestHandler(AgentExecutor agentExecutor, TaskStore taskStore, this.requestContextBuilder = () -> new SimpleRequestContextBuilder(taskStore, false); } + @PostConstruct + void initConfig() { + agentCompletionTimeoutSeconds = Integer.parseInt( + configProvider.getValue("a2a.blocking.agent.timeout.seconds")); + consumptionCompletionTimeoutSeconds = Integer.parseInt( + configProvider.getValue("a2a.blocking.consumption.timeout.seconds")); + } + /** * For testing */ diff --git a/server-common/src/main/java/io/a2a/server/util/async/AsyncExecutorProducer.java b/server-common/src/main/java/io/a2a/server/util/async/AsyncExecutorProducer.java index 49e69f99e..d85cd4de3 100644 --- a/server-common/src/main/java/io/a2a/server/util/async/AsyncExecutorProducer.java +++ b/server-common/src/main/java/io/a2a/server/util/async/AsyncExecutorProducer.java @@ -14,7 +14,7 @@ import jakarta.enterprise.inject.Produces; import jakarta.inject.Inject; -import org.eclipse.microprofile.config.inject.ConfigProperty; +import io.a2a.server.config.A2AConfigProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,22 +23,44 @@ public class AsyncExecutorProducer { private static final Logger LOGGER = LoggerFactory.getLogger(AsyncExecutorProducer.class); - @Inject // Needed to work in standard Jakarta runtimes (Quarkus skips this) - @ConfigProperty(name = "a2a.executor.core-pool-size", defaultValue = "5") + @Inject + A2AConfigProvider configProvider; + + /** + * Core pool size for async agent execution thread pool. + *

+ * Property: {@code a2a.executor.core-pool-size}
+ * Default: 5
+ * Note: Property override requires a configurable {@link A2AConfigProvider} on the classpath. + */ int corePoolSize; - @Inject // Needed to work in standard Jakarta runtimes (Quarkus skips this) - @ConfigProperty(name = "a2a.executor.max-pool-size", defaultValue = "50") + /** + * Maximum pool size for async agent execution thread pool. + *

+ * Property: {@code a2a.executor.max-pool-size}
+ * Default: 50
+ * Note: Property override requires a configurable {@link A2AConfigProvider} on the classpath. + */ int maxPoolSize; - @Inject // Needed to work in standard Jakarta runtimes (Quarkus skips this) - @ConfigProperty(name = "a2a.executor.keep-alive-seconds", defaultValue = "60") + /** + * Keep-alive time for idle threads (seconds). + *

+ * Property: {@code a2a.executor.keep-alive-seconds}
+ * Default: 60
+ * Note: Property override requires a configurable {@link A2AConfigProvider} on the classpath. + */ long keepAliveSeconds; private ExecutorService executor; @PostConstruct public void init() { + corePoolSize = Integer.parseInt(configProvider.getValue("a2a.executor.core-pool-size")); + maxPoolSize = Integer.parseInt(configProvider.getValue("a2a.executor.max-pool-size")); + keepAliveSeconds = Long.parseLong(configProvider.getValue("a2a.executor.keep-alive-seconds")); + LOGGER.info("Initializing async executor: corePoolSize={}, maxPoolSize={}, keepAliveSeconds={}", corePoolSize, maxPoolSize, keepAliveSeconds); diff --git a/server-common/src/main/resources/META-INF/a2a-defaults.properties b/server-common/src/main/resources/META-INF/a2a-defaults.properties new file mode 100644 index 000000000..280fd943b --- /dev/null +++ b/server-common/src/main/resources/META-INF/a2a-defaults.properties @@ -0,0 +1,21 @@ +# A2A SDK Default Configuration Values +# These values are used when no other configuration source provides them + +# DefaultRequestHandler - Blocking call timeouts +# Timeout for agent execution to complete (seconds) +# Increase for slow agents: LLM-based, data processing, external APIs +a2a.blocking.agent.timeout.seconds=30 + +# Timeout for event consumption/persistence to complete (seconds) +# Ensures TaskStore is fully updated before returning to client +a2a.blocking.consumption.timeout.seconds=5 + +# AsyncExecutorProducer - Thread pool configuration +# Core pool size for async agent execution +a2a.executor.core-pool-size=5 + +# Maximum pool size for async agent execution +a2a.executor.max-pool-size=50 + +# Keep-alive time for idle threads (seconds) +a2a.executor.keep-alive-seconds=60 From ee777850a3c99341bd870b89b8d41544a6b58851 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Thu, 20 Nov 2025 14:15:39 +0000 Subject: [PATCH 20/37] chore: Release 0.3.3.Final (#471) --- client/base/pom.xml | 2 +- client/transport/grpc/pom.xml | 2 +- client/transport/jsonrpc/pom.xml | 2 +- client/transport/rest/pom.xml | 2 +- client/transport/spi/pom.xml | 2 +- common/pom.xml | 2 +- examples/cloud-deployment/server/pom.xml | 2 +- examples/helloworld/client/pom.xml | 2 +- .../java/io/a2a/examples/helloworld/HelloWorldRunner.java | 4 ++-- examples/helloworld/pom.xml | 2 +- examples/helloworld/server/pom.xml | 2 +- extras/common/pom.xml | 2 +- extras/push-notification-config-store-database-jpa/pom.xml | 2 +- extras/queue-manager-replicated/core/pom.xml | 2 +- extras/queue-manager-replicated/pom.xml | 2 +- .../queue-manager-replicated/replication-mp-reactive/pom.xml | 2 +- extras/queue-manager-replicated/tests-multi-instance/pom.xml | 2 +- .../tests-multi-instance/quarkus-app-1/pom.xml | 2 +- .../tests-multi-instance/quarkus-app-2/pom.xml | 2 +- .../tests-multi-instance/quarkus-common/pom.xml | 2 +- .../tests-multi-instance/tests/pom.xml | 2 +- extras/queue-manager-replicated/tests-single-instance/pom.xml | 2 +- extras/task-store-database-jpa/pom.xml | 2 +- http-client/pom.xml | 2 +- integrations/microprofile-config/pom.xml | 2 +- pom.xml | 2 +- reference/common/pom.xml | 2 +- reference/grpc/pom.xml | 2 +- reference/jsonrpc/pom.xml | 2 +- reference/rest/pom.xml | 2 +- server-common/pom.xml | 2 +- spec-grpc/pom.xml | 2 +- spec/pom.xml | 2 +- tck/pom.xml | 2 +- tests/server-common/pom.xml | 2 +- transport/grpc/pom.xml | 2 +- transport/jsonrpc/pom.xml | 2 +- transport/rest/pom.xml | 2 +- 38 files changed, 39 insertions(+), 39 deletions(-) diff --git a/client/base/pom.xml b/client/base/pom.xml index 05d594414..7c48bc076 100644 --- a/client/base/pom.xml +++ b/client/base/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../pom.xml a2a-java-sdk-client diff --git a/client/transport/grpc/pom.xml b/client/transport/grpc/pom.xml index 16aefca3d..b4e148f04 100644 --- a/client/transport/grpc/pom.xml +++ b/client/transport/grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../../pom.xml a2a-java-sdk-client-transport-grpc diff --git a/client/transport/jsonrpc/pom.xml b/client/transport/jsonrpc/pom.xml index 6028a83e0..8536e5ef5 100644 --- a/client/transport/jsonrpc/pom.xml +++ b/client/transport/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../../pom.xml a2a-java-sdk-client-transport-jsonrpc diff --git a/client/transport/rest/pom.xml b/client/transport/rest/pom.xml index a31b38b39..17f991604 100644 --- a/client/transport/rest/pom.xml +++ b/client/transport/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../../pom.xml a2a-java-sdk-client-transport-rest diff --git a/client/transport/spi/pom.xml b/client/transport/spi/pom.xml index 6de8ddaf3..a0b37f240 100644 --- a/client/transport/spi/pom.xml +++ b/client/transport/spi/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../../pom.xml a2a-java-sdk-client-transport-spi diff --git a/common/pom.xml b/common/pom.xml index 6e8222642..df3dda2ab 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final a2a-java-sdk-common diff --git a/examples/cloud-deployment/server/pom.xml b/examples/cloud-deployment/server/pom.xml index 4170c3e5b..61a478863 100644 --- a/examples/cloud-deployment/server/pom.xml +++ b/examples/cloud-deployment/server/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../../pom.xml diff --git a/examples/helloworld/client/pom.xml b/examples/helloworld/client/pom.xml index acffe5706..381c98021 100644 --- a/examples/helloworld/client/pom.xml +++ b/examples/helloworld/client/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-examples-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final a2a-java-sdk-examples-client diff --git a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java index 219d5044f..081d8c458 100644 --- a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java +++ b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java @@ -1,6 +1,6 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.3.Beta1-SNAPSHOT -//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.3.Beta1-SNAPSHOT +//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.3.Final +//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.3.Final //SOURCES HelloWorldClient.java /** diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 290cad120..afa00a8f1 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../pom.xml diff --git a/examples/helloworld/server/pom.xml b/examples/helloworld/server/pom.xml index 44b895b74..1403ebe5b 100644 --- a/examples/helloworld/server/pom.xml +++ b/examples/helloworld/server/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-examples-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final a2a-java-sdk-examples-server diff --git a/extras/common/pom.xml b/extras/common/pom.xml index 4fdbcafac..045c2eb3a 100644 --- a/extras/common/pom.xml +++ b/extras/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../pom.xml diff --git a/extras/push-notification-config-store-database-jpa/pom.xml b/extras/push-notification-config-store-database-jpa/pom.xml index 63b12c4c4..5ff483cc3 100644 --- a/extras/push-notification-config-store-database-jpa/pom.xml +++ b/extras/push-notification-config-store-database-jpa/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../pom.xml a2a-java-extras-push-notification-config-store-database-jpa diff --git a/extras/queue-manager-replicated/core/pom.xml b/extras/queue-manager-replicated/core/pom.xml index 752c126cb..526cea2b3 100644 --- a/extras/queue-manager-replicated/core/pom.xml +++ b/extras/queue-manager-replicated/core/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../pom.xml diff --git a/extras/queue-manager-replicated/pom.xml b/extras/queue-manager-replicated/pom.xml index 9f801ffdd..51970d986 100644 --- a/extras/queue-manager-replicated/pom.xml +++ b/extras/queue-manager-replicated/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../pom.xml diff --git a/extras/queue-manager-replicated/replication-mp-reactive/pom.xml b/extras/queue-manager-replicated/replication-mp-reactive/pom.xml index 923822e48..85bf57eb7 100644 --- a/extras/queue-manager-replicated/replication-mp-reactive/pom.xml +++ b/extras/queue-manager-replicated/replication-mp-reactive/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/pom.xml index 273653c92..7fb3c9532 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml index 86a1e24c0..d510ba647 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml index 8e591d84d..5354456fd 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml index 48da517d8..48abb1582 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml index 45fd49427..267b7b077 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../pom.xml diff --git a/extras/queue-manager-replicated/tests-single-instance/pom.xml b/extras/queue-manager-replicated/tests-single-instance/pom.xml index 8e707e80a..2944b326f 100644 --- a/extras/queue-manager-replicated/tests-single-instance/pom.xml +++ b/extras/queue-manager-replicated/tests-single-instance/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../pom.xml diff --git a/extras/task-store-database-jpa/pom.xml b/extras/task-store-database-jpa/pom.xml index 5112fd6de..b56ddd39e 100644 --- a/extras/task-store-database-jpa/pom.xml +++ b/extras/task-store-database-jpa/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../pom.xml a2a-java-extras-task-store-database-jpa diff --git a/http-client/pom.xml b/http-client/pom.xml index 32bd39099..0b72bd185 100644 --- a/http-client/pom.xml +++ b/http-client/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final a2a-java-sdk-http-client diff --git a/integrations/microprofile-config/pom.xml b/integrations/microprofile-config/pom.xml index 29b9f750d..7610b93e5 100644 --- a/integrations/microprofile-config/pom.xml +++ b/integrations/microprofile-config/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../pom.xml a2a-java-sdk-microprofile-config diff --git a/pom.xml b/pom.xml index eba4b8985..6c8f87b5a 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final pom diff --git a/reference/common/pom.xml b/reference/common/pom.xml index dcd5781f6..07446c19d 100644 --- a/reference/common/pom.xml +++ b/reference/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../pom.xml a2a-java-sdk-reference-common diff --git a/reference/grpc/pom.xml b/reference/grpc/pom.xml index e11c110cc..93d953a00 100644 --- a/reference/grpc/pom.xml +++ b/reference/grpc/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../pom.xml diff --git a/reference/jsonrpc/pom.xml b/reference/jsonrpc/pom.xml index 7ca82c8c2..ac1173e46 100644 --- a/reference/jsonrpc/pom.xml +++ b/reference/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../pom.xml a2a-java-sdk-reference-jsonrpc diff --git a/reference/rest/pom.xml b/reference/rest/pom.xml index a8422ca43..5a1194c1d 100644 --- a/reference/rest/pom.xml +++ b/reference/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../pom.xml a2a-java-sdk-reference-rest diff --git a/server-common/pom.xml b/server-common/pom.xml index f14b471a8..ef1b3f9ac 100644 --- a/server-common/pom.xml +++ b/server-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final a2a-java-sdk-server-common diff --git a/spec-grpc/pom.xml b/spec-grpc/pom.xml index 0bddbc336..802b844f0 100644 --- a/spec-grpc/pom.xml +++ b/spec-grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final a2a-java-sdk-spec-grpc diff --git a/spec/pom.xml b/spec/pom.xml index c9838c801..7cb1ce168 100644 --- a/spec/pom.xml +++ b/spec/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final a2a-java-sdk-spec diff --git a/tck/pom.xml b/tck/pom.xml index 7f1679966..fdba9c2de 100644 --- a/tck/pom.xml +++ b/tck/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final a2a-tck-server diff --git a/tests/server-common/pom.xml b/tests/server-common/pom.xml index effa1f1df..47fab51b8 100644 --- a/tests/server-common/pom.xml +++ b/tests/server-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../pom.xml a2a-java-sdk-tests-server-common diff --git a/transport/grpc/pom.xml b/transport/grpc/pom.xml index bfcb8be76..3c82a5373 100644 --- a/transport/grpc/pom.xml +++ b/transport/grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../pom.xml a2a-java-sdk-transport-grpc diff --git a/transport/jsonrpc/pom.xml b/transport/jsonrpc/pom.xml index bf9bf4a82..115af71ad 100644 --- a/transport/jsonrpc/pom.xml +++ b/transport/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../pom.xml a2a-java-sdk-transport-jsonrpc diff --git a/transport/rest/pom.xml b/transport/rest/pom.xml index 9f9eacea4..034e462f3 100644 --- a/transport/rest/pom.xml +++ b/transport/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Beta1-SNAPSHOT + 0.3.3.Final ../../pom.xml a2a-java-sdk-transport-rest From 717598332fd7b97ebc2423b1027e563a66b9bdec Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Thu, 20 Nov 2025 15:24:18 +0000 Subject: [PATCH 21/37] chore: Next is 0.3.4 (#472) --- client/base/pom.xml | 2 +- client/transport/grpc/pom.xml | 2 +- client/transport/jsonrpc/pom.xml | 2 +- client/transport/rest/pom.xml | 2 +- client/transport/spi/pom.xml | 2 +- common/pom.xml | 2 +- examples/cloud-deployment/server/pom.xml | 2 +- examples/helloworld/client/pom.xml | 2 +- .../java/io/a2a/examples/helloworld/HelloWorldRunner.java | 4 ++-- examples/helloworld/pom.xml | 2 +- examples/helloworld/server/pom.xml | 2 +- extras/common/pom.xml | 2 +- extras/push-notification-config-store-database-jpa/pom.xml | 2 +- extras/queue-manager-replicated/core/pom.xml | 2 +- extras/queue-manager-replicated/pom.xml | 2 +- .../queue-manager-replicated/replication-mp-reactive/pom.xml | 2 +- extras/queue-manager-replicated/tests-multi-instance/pom.xml | 2 +- .../tests-multi-instance/quarkus-app-1/pom.xml | 2 +- .../tests-multi-instance/quarkus-app-2/pom.xml | 2 +- .../tests-multi-instance/quarkus-common/pom.xml | 2 +- .../tests-multi-instance/tests/pom.xml | 2 +- extras/queue-manager-replicated/tests-single-instance/pom.xml | 2 +- extras/task-store-database-jpa/pom.xml | 2 +- http-client/pom.xml | 2 +- integrations/microprofile-config/pom.xml | 2 +- pom.xml | 2 +- reference/common/pom.xml | 2 +- reference/grpc/pom.xml | 2 +- reference/jsonrpc/pom.xml | 2 +- reference/rest/pom.xml | 2 +- server-common/pom.xml | 2 +- spec-grpc/pom.xml | 2 +- spec/pom.xml | 2 +- tck/pom.xml | 2 +- tests/server-common/pom.xml | 2 +- transport/grpc/pom.xml | 2 +- transport/jsonrpc/pom.xml | 2 +- transport/rest/pom.xml | 2 +- 38 files changed, 39 insertions(+), 39 deletions(-) diff --git a/client/base/pom.xml b/client/base/pom.xml index 7c48bc076..6a30adf20 100644 --- a/client/base/pom.xml +++ b/client/base/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-client diff --git a/client/transport/grpc/pom.xml b/client/transport/grpc/pom.xml index b4e148f04..eb996b98d 100644 --- a/client/transport/grpc/pom.xml +++ b/client/transport/grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-grpc diff --git a/client/transport/jsonrpc/pom.xml b/client/transport/jsonrpc/pom.xml index 8536e5ef5..e14025c5b 100644 --- a/client/transport/jsonrpc/pom.xml +++ b/client/transport/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-jsonrpc diff --git a/client/transport/rest/pom.xml b/client/transport/rest/pom.xml index 17f991604..754dff4be 100644 --- a/client/transport/rest/pom.xml +++ b/client/transport/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-rest diff --git a/client/transport/spi/pom.xml b/client/transport/spi/pom.xml index a0b37f240..a550244f0 100644 --- a/client/transport/spi/pom.xml +++ b/client/transport/spi/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-spi diff --git a/common/pom.xml b/common/pom.xml index df3dda2ab..e23d23ecc 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT a2a-java-sdk-common diff --git a/examples/cloud-deployment/server/pom.xml b/examples/cloud-deployment/server/pom.xml index 61a478863..6fa303112 100644 --- a/examples/cloud-deployment/server/pom.xml +++ b/examples/cloud-deployment/server/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../../pom.xml diff --git a/examples/helloworld/client/pom.xml b/examples/helloworld/client/pom.xml index 381c98021..2effa47aa 100644 --- a/examples/helloworld/client/pom.xml +++ b/examples/helloworld/client/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-examples-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT a2a-java-sdk-examples-client diff --git a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java index 081d8c458..82638ec81 100644 --- a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java +++ b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java @@ -1,6 +1,6 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.3.Final -//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.3.Final +//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.4.Beta1-SNAPSHOT +//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.4.Beta1-SNAPSHOT //SOURCES HelloWorldClient.java /** diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index afa00a8f1..c61721150 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../pom.xml diff --git a/examples/helloworld/server/pom.xml b/examples/helloworld/server/pom.xml index 1403ebe5b..3638d9252 100644 --- a/examples/helloworld/server/pom.xml +++ b/examples/helloworld/server/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-examples-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT a2a-java-sdk-examples-server diff --git a/extras/common/pom.xml b/extras/common/pom.xml index 045c2eb3a..90c58ffd9 100644 --- a/extras/common/pom.xml +++ b/extras/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../pom.xml diff --git a/extras/push-notification-config-store-database-jpa/pom.xml b/extras/push-notification-config-store-database-jpa/pom.xml index 5ff483cc3..cec16f4c2 100644 --- a/extras/push-notification-config-store-database-jpa/pom.xml +++ b/extras/push-notification-config-store-database-jpa/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../pom.xml a2a-java-extras-push-notification-config-store-database-jpa diff --git a/extras/queue-manager-replicated/core/pom.xml b/extras/queue-manager-replicated/core/pom.xml index 526cea2b3..d724d1773 100644 --- a/extras/queue-manager-replicated/core/pom.xml +++ b/extras/queue-manager-replicated/core/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/pom.xml b/extras/queue-manager-replicated/pom.xml index 51970d986..fcd25cac0 100644 --- a/extras/queue-manager-replicated/pom.xml +++ b/extras/queue-manager-replicated/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../pom.xml diff --git a/extras/queue-manager-replicated/replication-mp-reactive/pom.xml b/extras/queue-manager-replicated/replication-mp-reactive/pom.xml index 85bf57eb7..d5a23d61a 100644 --- a/extras/queue-manager-replicated/replication-mp-reactive/pom.xml +++ b/extras/queue-manager-replicated/replication-mp-reactive/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/pom.xml index 7fb3c9532..7aacf4eb9 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml index d510ba647..98b023799 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml index 5354456fd..03c60ca7d 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml index 48abb1582..ad9d7e5f5 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml index 267b7b077..c0ba840c8 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-single-instance/pom.xml b/extras/queue-manager-replicated/tests-single-instance/pom.xml index 2944b326f..9a6c0c34b 100644 --- a/extras/queue-manager-replicated/tests-single-instance/pom.xml +++ b/extras/queue-manager-replicated/tests-single-instance/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../pom.xml diff --git a/extras/task-store-database-jpa/pom.xml b/extras/task-store-database-jpa/pom.xml index b56ddd39e..55a3e619f 100644 --- a/extras/task-store-database-jpa/pom.xml +++ b/extras/task-store-database-jpa/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../pom.xml a2a-java-extras-task-store-database-jpa diff --git a/http-client/pom.xml b/http-client/pom.xml index 0b72bd185..9b7567af3 100644 --- a/http-client/pom.xml +++ b/http-client/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT a2a-java-sdk-http-client diff --git a/integrations/microprofile-config/pom.xml b/integrations/microprofile-config/pom.xml index 7610b93e5..4bb6de22f 100644 --- a/integrations/microprofile-config/pom.xml +++ b/integrations/microprofile-config/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-microprofile-config diff --git a/pom.xml b/pom.xml index 6c8f87b5a..4d2557a53 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT pom diff --git a/reference/common/pom.xml b/reference/common/pom.xml index 07446c19d..f779abd4b 100644 --- a/reference/common/pom.xml +++ b/reference/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-reference-common diff --git a/reference/grpc/pom.xml b/reference/grpc/pom.xml index 93d953a00..588d3a12e 100644 --- a/reference/grpc/pom.xml +++ b/reference/grpc/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../pom.xml diff --git a/reference/jsonrpc/pom.xml b/reference/jsonrpc/pom.xml index ac1173e46..22173625a 100644 --- a/reference/jsonrpc/pom.xml +++ b/reference/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-reference-jsonrpc diff --git a/reference/rest/pom.xml b/reference/rest/pom.xml index 5a1194c1d..951704c29 100644 --- a/reference/rest/pom.xml +++ b/reference/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-reference-rest diff --git a/server-common/pom.xml b/server-common/pom.xml index ef1b3f9ac..ff2cd8a1c 100644 --- a/server-common/pom.xml +++ b/server-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT a2a-java-sdk-server-common diff --git a/spec-grpc/pom.xml b/spec-grpc/pom.xml index 802b844f0..0ffac2fd3 100644 --- a/spec-grpc/pom.xml +++ b/spec-grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT a2a-java-sdk-spec-grpc diff --git a/spec/pom.xml b/spec/pom.xml index 7cb1ce168..ce100bf1e 100644 --- a/spec/pom.xml +++ b/spec/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT a2a-java-sdk-spec diff --git a/tck/pom.xml b/tck/pom.xml index fdba9c2de..6b4002b03 100644 --- a/tck/pom.xml +++ b/tck/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT a2a-tck-server diff --git a/tests/server-common/pom.xml b/tests/server-common/pom.xml index 47fab51b8..c7f671485 100644 --- a/tests/server-common/pom.xml +++ b/tests/server-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-tests-server-common diff --git a/transport/grpc/pom.xml b/transport/grpc/pom.xml index 3c82a5373..d535a3199 100644 --- a/transport/grpc/pom.xml +++ b/transport/grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-transport-grpc diff --git a/transport/jsonrpc/pom.xml b/transport/jsonrpc/pom.xml index 115af71ad..010caff77 100644 --- a/transport/jsonrpc/pom.xml +++ b/transport/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-transport-jsonrpc diff --git a/transport/rest/pom.xml b/transport/rest/pom.xml index 034e462f3..53344385d 100644 --- a/transport/rest/pom.xml +++ b/transport/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.3.Final + 0.3.4.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-transport-rest From c2551bce62d525b2f2295b20f7e58aee2e367d9f Mon Sep 17 00:00:00 2001 From: Emmanuel Hugonnet Date: Fri, 12 Dec 2025 16:53:54 +0100 Subject: [PATCH 22/37] fix: Making the event queue overridable (#511) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [X] Follow the [`CONTRIBUTING` Guide](../CONTRIBUTING.md). - [C] Make your Pull Request title in the specification. - Important Prefixes for [release-please](https://github.com/googleapis/release-please): - `fix:` which represents bug fixes, and correlates to a [SemVer](https://semver.org/) patch. - `feat:` represents a new feature, and correlates to a SemVer minor. - `feat!:`, or `fix!:`, `refactor!:`, etc., which represent a breaking change (indicated by the `!`) and will result in a SemVer major. - [X] Ensure the tests pass - [X] Appropriate READMEs were updated (if necessary) Fixes # 🦕 Signed-off-by: Emmanuel Hugonnet --- .../main/java/io/a2a/client/MessageEvent.java | 17 +++++++++++++++-- .../java/io/a2a/server/events/EventQueue.java | 12 ++++++------ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/client/base/src/main/java/io/a2a/client/MessageEvent.java b/client/base/src/main/java/io/a2a/client/MessageEvent.java index b5970ab78..94c8a1058 100644 --- a/client/base/src/main/java/io/a2a/client/MessageEvent.java +++ b/client/base/src/main/java/io/a2a/client/MessageEvent.java @@ -21,6 +21,19 @@ public MessageEvent(Message message) { public Message getMessage() { return message; } -} - + @Override + public String toString() { + String messageAsString = "{" + + "role=" + message.getRole() + + ", parts=" + message.getParts() + + ", messageId=" + message.getMessageId() + + ", contextId=" + message.getContextId() + + ", taskId=" + message.getTaskId() + + ", metadata=" + message.getMetadata() + + ", kind=" + message.getKind() + + ", referenceTaskIds=" + message.getReferenceTaskIds() + + ", extensions=" + message.getExtensions() + '}'; + return "MessageEvent{" + "message=" + messageAsString + '}'; + } +} diff --git a/server-common/src/main/java/io/a2a/server/events/EventQueue.java b/server-common/src/main/java/io/a2a/server/events/EventQueue.java index d590d8890..6a8a154ac 100644 --- a/server-common/src/main/java/io/a2a/server/events/EventQueue.java +++ b/server-common/src/main/java/io/a2a/server/events/EventQueue.java @@ -96,7 +96,7 @@ public int getQueueSize() { public abstract void awaitQueuePollerStart() throws InterruptedException ; - abstract void signalQueuePollerStarted(); + public abstract void signalQueuePollerStarted(); public void enqueueEvent(Event event) { enqueueItem(new LocalEventQueueItem(event)); @@ -119,7 +119,7 @@ public void enqueueItem(EventQueueItem item) { LOGGER.debug("Enqueued event {} {}", event instanceof Throwable ? event.toString() : event, this); } - abstract EventQueue tap(); + public abstract EventQueue tap(); /** * Dequeues an EventQueueItem from the queue. @@ -265,7 +265,7 @@ static class MainQueue extends EventQueue { taskId, onCloseCallbacks.size(), taskStateProvider != null); } - EventQueue tap() { + public EventQueue tap() { ChildQueue child = new ChildQueue(this); children.add(child); return child; @@ -310,7 +310,7 @@ public void awaitQueuePollerStart() throws InterruptedException { } @Override - void signalQueuePollerStarted() { + public void signalQueuePollerStarted() { if (pollingStarted.get()) { return; } @@ -415,7 +415,7 @@ private void internalEnqueueItem(EventQueueItem item) { } @Override - EventQueue tap() { + public EventQueue tap() { throw new IllegalStateException("Can only tap the main queue"); } @@ -425,7 +425,7 @@ public void awaitQueuePollerStart() throws InterruptedException { } @Override - void signalQueuePollerStarted() { + public void signalQueuePollerStarted() { parent.signalQueuePollerStarted(); } From e603fc3e644987691e471c20bbcae3811faf6b3b Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Tue, 28 Apr 2026 17:57:32 +0100 Subject: [PATCH 23/37] fix: Get cloud deployment example working with Strimzi Pin version of operator to current latest version --- examples/cloud-deployment/README.md | 4 +- examples/cloud-deployment/k8s/02-kafka.yaml | 8 +- .../cloud-deployment/k8s/03-kafka-topic.yaml | 2 +- examples/cloud-deployment/scripts/deploy.sh | 5 +- .../strimzi-cluster-operator-1.0.0.yaml | 18469 ++++++++++++++++ 5 files changed, 18480 insertions(+), 8 deletions(-) create mode 100644 examples/cloud-deployment/strimzi-1.0.0/strimzi-cluster-operator-1.0.0.yaml diff --git a/examples/cloud-deployment/README.md b/examples/cloud-deployment/README.md index bf1e4cd60..4f780f8e0 100644 --- a/examples/cloud-deployment/README.md +++ b/examples/cloud-deployment/README.md @@ -448,7 +448,7 @@ kubectl logs -n kafka kubectl get crd kafkas.kafka.strimzi.io # If missing, reinstall Strimzi -kubectl create -f 'https://strimzi.io/install/latest?namespace=kafka' -n kafka +kubectl create -f '../strimzi-1.0.0/strimzi-cluster-operator-1.0.0.yaml' -n kafka ``` ### Kind Resource Issues @@ -532,6 +532,8 @@ cloud-deployment/ │ ├── 03-kafka-topic.yaml # Kafka topic │ ├── 04-agent-configmap.yaml # Configuration │ └── 05-agent-deployment.yaml # Agent deployment + service +├── strimzi-1.0.0/ +│ └── strimzi-cluster-operator-1.0.0.yaml # Pinned from https://strimzi.io/install/latest?namespace=kafka ├── scripts/ │ ├── deploy.sh # Automated deployment │ ├── verify.sh # Health checks diff --git a/examples/cloud-deployment/k8s/02-kafka.yaml b/examples/cloud-deployment/k8s/02-kafka.yaml index 044aeb1ac..26e7f1862 100644 --- a/examples/cloud-deployment/k8s/02-kafka.yaml +++ b/examples/cloud-deployment/k8s/02-kafka.yaml @@ -1,6 +1,6 @@ --- # KafkaNodePool for KRaft mode -apiVersion: kafka.strimzi.io/v1beta2 +apiVersion: kafka.strimzi.io/v1 kind: KafkaNodePool metadata: name: broker @@ -23,7 +23,7 @@ spec: cpu: "500m" --- # Kafka cluster -apiVersion: kafka.strimzi.io/v1beta2 +apiVersion: kafka.strimzi.io/v1 kind: Kafka metadata: name: a2a-kafka @@ -33,8 +33,8 @@ metadata: strimzi.io/kraft: enabled spec: kafka: - version: 4.0.0 - metadataVersion: 4.0-IV0 + version: 4.2.0 + metadataVersion: 4.2-IV0 listeners: - name: plain port: 9092 diff --git a/examples/cloud-deployment/k8s/03-kafka-topic.yaml b/examples/cloud-deployment/k8s/03-kafka-topic.yaml index 2fa7fca9c..5373d544b 100644 --- a/examples/cloud-deployment/k8s/03-kafka-topic.yaml +++ b/examples/cloud-deployment/k8s/03-kafka-topic.yaml @@ -1,6 +1,6 @@ --- # Kafka topic for A2A event replication -apiVersion: kafka.strimzi.io/v1beta2 +apiVersion: kafka.strimzi.io/v1 kind: KafkaTopic metadata: name: a2a-replicated-events diff --git a/examples/cloud-deployment/scripts/deploy.sh b/examples/cloud-deployment/scripts/deploy.sh index e267f3302..98bcb35df 100755 --- a/examples/cloud-deployment/scripts/deploy.sh +++ b/examples/cloud-deployment/scripts/deploy.sh @@ -177,8 +177,9 @@ if ! kubectl get namespace kafka > /dev/null 2>&1; then fi if ! kubectl get crd kafkas.kafka.strimzi.io > /dev/null 2>&1; then - echo "Installing Strimzi operator..." - kubectl create -f 'https://strimzi.io/install/latest?namespace=kafka' -n kafka + # Pinned version of https://strimzi.io/install/latest?namespace=kafka + echo "Installing Strimzi operator (1.0.0)..." + kubectl create -f '../strimzi-1.0.0/strimzi-cluster-operator-1.0.0.yaml' -n kafka echo "Waiting for Strimzi operator deployment to be created..." for i in {1..30}; do diff --git a/examples/cloud-deployment/strimzi-1.0.0/strimzi-cluster-operator-1.0.0.yaml b/examples/cloud-deployment/strimzi-1.0.0/strimzi-cluster-operator-1.0.0.yaml new file mode 100644 index 000000000..2affcd27e --- /dev/null +++ b/examples/cloud-deployment/strimzi-1.0.0/strimzi-cluster-operator-1.0.0.yaml @@ -0,0 +1,18469 @@ + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: strimzi-cluster-operator-leader-election + labels: + app: strimzi + namespace: kafka +subjects: + - kind: ServiceAccount + name: strimzi-cluster-operator + namespace: kafka +roleRef: + kind: ClusterRole + name: strimzi-cluster-operator-leader-election + apiGroup: rbac.authorization.k8s.io + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: strimzi-entity-operator + labels: + app: strimzi +rules: + - apiGroups: + - kafka.strimzi.io + resources: + - kafkatopics + verbs: + - get + - list + - watch + - create + - patch + - update + - delete + - apiGroups: + - kafka.strimzi.io + resources: + - kafkausers + verbs: + - get + - list + - watch + - create + - patch + - update + - apiGroups: + - kafka.strimzi.io + resources: + - kafkatopics/status + - kafkausers/status + verbs: + - get + - patch + - update + - apiGroups: + - kafka.strimzi.io + resources: + - kafkatopics/finalizers + - kafkausers/finalizers + verbs: + - update + - apiGroups: + - '' + resources: + - secrets + verbs: + - get + - list + - watch + - create + - delete + - patch + - update + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: strimzi-cluster-operator + labels: + app: strimzi + namespace: kafka +subjects: + - kind: ServiceAccount + name: strimzi-cluster-operator + namespace: kafka +roleRef: + kind: ClusterRole + name: strimzi-cluster-operator-namespaced + apiGroup: rbac.authorization.k8s.io + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: strimzi-kafka-client + labels: + app: strimzi +rules: + - apiGroups: + - '' + resources: + - nodes + verbs: + - get + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: kafkausers.kafka.strimzi.io + labels: + app: strimzi + strimzi.io/crd-install: 'true' +spec: + group: kafka.strimzi.io + names: + kind: KafkaUser + listKind: KafkaUserList + singular: kafkauser + plural: kafkausers + shortNames: + - ku + categories: + - strimzi + scope: Namespaced + conversion: + strategy: None + versions: + - name: v1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Cluster + description: The name of the Kafka cluster this user belongs to + jsonPath: .metadata.labels.strimzi\.io/cluster + type: string + - name: Authentication + description: How the user is authenticated + jsonPath: .spec.authentication.type + type: string + - name: Authorization + description: How the user is authorised + jsonPath: .spec.authorization.type + type: string + - name: Ready + description: The state of the custom resource + jsonPath: '.status.conditions[?(@.type=="Ready")].status' + type: string + schema: + openAPIV3Schema: + type: object + properties: + apiVersion: + type: string + description: >- + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the + latest internal value, and may reject unrecognized values. More + info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + kind: + type: string + description: >- + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the + client submits requests to. Cannot be updated. In CamelCase. + More info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + metadata: + type: object + spec: + type: object + properties: + authentication: + type: object + properties: + password: + type: object + properties: + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: >- + Selects a key of a Secret in the resource's + namespace. + description: Secret from which the password should be read. + required: + - valueFrom + description: >- + Specify the password for the user. If not set, a new + password is generated by the User Operator. + type: + type: string + enum: + - tls + - tls-external + - scram-sha-512 + description: Authentication type. + required: + - type + description: >- + Authentication mechanism enabled for this Kafka user. The + supported authentication mechanisms are `scram-sha-512`, + `tls`, and `tls-external`. + + + * `scram-sha-512` generates a secret with SASL SCRAM-SHA-512 + credentials. + + * `tls` generates a secret with user certificate for mutual + TLS authentication. + + * `tls-external` does not generate a user certificate. But + prepares the user for using mutual TLS authentication using + a user certificate generated outside the User Operator. + ACLs and quotas set for this user are configured in the `CN=` format. + + Authentication is optional. If authentication is not + configured, no credentials are generated. ACLs and quotas + set for the user are configured in the `` format + suitable for SASL authentication. + authorization: + type: object + properties: + acls: + type: array + items: + type: object + properties: + type: + type: string + enum: + - allow + - deny + description: >- + The type of the rule. ACL rules with type `allow` + are used to allow user to execute the specified + operations. ACL rules with type `deny` are used to + deny user to execute the specified operations. + Default value is `allow`. + resource: + type: object + properties: + name: + type: string + description: >- + Name of resource for which given ACL rule + applies. Can be combined with `patternType` + field to use prefix pattern. + patternType: + type: string + enum: + - literal + - prefix + description: >- + Describes the pattern used in the resource + field. The supported types are `literal` and + `prefix`. With `literal` pattern type, the + resource field will be used as a definition of + a full name. With `prefix` pattern type, the + resource name will be used only as a prefix. + Default value is `literal`. + type: + type: string + enum: + - topic + - group + - cluster + - transactionalId + description: >- + Resource type. The available resource types + are `topic`, `group`, `cluster`, and + `transactionalId`. + required: + - type + description: >- + Indicates the resource for which given ACL rule + applies. + host: + type: string + description: >- + The host from which the action described in the + ACL rule is allowed or denied. If not set, it + defaults to `*`, allowing or denying the action + from any host. + operations: + type: array + items: + type: string + enum: + - Read + - Write + - Create + - Delete + - Alter + - Describe + - ClusterAction + - AlterConfigs + - DescribeConfigs + - IdempotentWrite + - All + description: >- + List of operations to allow or deny. Supported + operations are: Read, Write, Create, Delete, + Alter, Describe, ClusterAction, AlterConfigs, + DescribeConfigs, IdempotentWrite and All. Only + certain operations work with the specified + resource. + required: + - resource + - operations + description: List of ACL rules which should be applied to this user. + type: + type: string + enum: + - simple + description: >- + Authorization type. Currently the only supported type is + `simple`. `simple` authorization type uses the Kafka + Admin API for managing the ACL rules. + required: + - acls + - type + description: Authorization rules for this Kafka user. + quotas: + type: object + properties: + producerByteRate: + type: integer + minimum: 0 + description: >- + A quota on the maximum bytes per-second that each client + group can publish to a broker before the clients in the + group are throttled. Defined on a per-broker basis. + consumerByteRate: + type: integer + minimum: 0 + description: >- + A quota on the maximum bytes per-second that each client + group can fetch from a broker before the clients in the + group are throttled. Defined on a per-broker basis. + requestPercentage: + type: integer + minimum: 0 + description: >- + A quota on the maximum CPU utilization of each client + group as a percentage of network and I/O threads. + controllerMutationRate: + type: number + minimum: 0 + description: >- + A quota on the rate at which mutations are accepted for + the create topics request, the create partitions request + and the delete topics request. The rate is accumulated + by the number of partitions created or deleted. + description: >- + Quotas on requests to control the broker resources used by + clients. Network bandwidth and request rate quotas can be + enforced. For more information, see the Apache Kafka design + documentation about quotas. + template: + type: object + properties: + secret: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + description: >- + Template for KafkaUser resources. The template allows + users to specify how the `Secret` with password or TLS + certificates is generated. + description: Template to specify how Kafka User `Secrets` are generated. + description: The specification of the user. + status: + type: object + properties: + conditions: + type: array + items: + type: object + properties: + type: + type: string + description: >- + The unique identifier of a condition, used to + distinguish between other conditions in the resource. + status: + type: string + description: >- + The status of the condition, either True, False or + Unknown. + lastTransitionTime: + type: string + description: >- + Last time the condition of a type changed from one + status to another. The required format is + 'yyyy-MM-ddTHH:mm:ssZ', in the UTC time zone. + reason: + type: string + description: >- + The reason for the condition's last transition (a + single word in CamelCase). + message: + type: string + description: >- + Human-readable message indicating details about the + condition's last transition. + description: List of status conditions. + observedGeneration: + type: integer + description: >- + The generation of the CRD that was last reconciled by the + operator. + username: + type: string + description: Username. + secret: + type: string + description: The name of `Secret` where the credentials are stored. + description: The status of the Kafka User. + required: + - spec + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: strimzi-cluster-operator-watched + labels: + app: strimzi +rules: + - apiGroups: + - '' + resources: + - pods + verbs: + - watch + - list + - apiGroups: + - kafka.strimzi.io + resources: + - kafkas + - kafkanodepools + - kafkaconnects + - kafkaconnectors + - kafkabridges + - kafkamirrormaker2s + - kafkarebalances + verbs: + - get + - list + - watch + - create + - patch + - update + - apiGroups: + - kafka.strimzi.io + resources: + - kafkas/status + - kafkanodepools/status + - kafkaconnects/status + - kafkaconnectors/status + - kafkabridges/status + - kafkamirrormaker2s/status + - kafkarebalances/status + verbs: + - get + - patch + - update + - apiGroups: + - kafka.strimzi.io + resources: + - kafkas/finalizers + - kafkanodepools/finalizers + - kafkaconnects/finalizers + - kafkaconnectors/finalizers + - kafkabridges/finalizers + - kafkamirrormaker2s/finalizers + - kafkarebalances/finalizers + verbs: + - update + - apiGroups: + - core.strimzi.io + resources: + - strimzipodsets + verbs: + - get + - list + - watch + - create + - delete + - patch + - update + - apiGroups: + - core.strimzi.io + resources: + - strimzipodsets/status + verbs: + - get + - patch + - update + - apiGroups: + - core.strimzi.io + resources: + - strimzipodsets/finalizers + verbs: + - update + - apiGroups: + - kafka.strimzi.io + resources: + - kafkarebalances + verbs: + - delete + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: kafkaconnectors.kafka.strimzi.io + labels: + app: strimzi + strimzi.io/crd-install: 'true' +spec: + group: kafka.strimzi.io + names: + kind: KafkaConnector + listKind: KafkaConnectorList + singular: kafkaconnector + plural: kafkaconnectors + shortNames: + - kctr + categories: + - strimzi + scope: Namespaced + conversion: + strategy: None + versions: + - name: v1 + served: true + storage: true + subresources: + status: {} + scale: + specReplicasPath: .spec.tasksMax + statusReplicasPath: .status.tasksMax + additionalPrinterColumns: + - name: Cluster + description: The name of the Kafka Connect cluster this connector belongs to + jsonPath: .metadata.labels.strimzi\.io/cluster + type: string + - name: Connector class + description: The class used by this connector + jsonPath: .spec.class + type: string + - name: Max Tasks + description: Maximum number of tasks + jsonPath: .spec.tasksMax + type: integer + - name: Ready + description: The state of the custom resource + jsonPath: '.status.conditions[?(@.type=="Ready")].status' + type: string + schema: + openAPIV3Schema: + type: object + properties: + apiVersion: + type: string + description: >- + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the + latest internal value, and may reject unrecognized values. More + info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + kind: + type: string + description: >- + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the + client submits requests to. Cannot be updated. In CamelCase. + More info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + metadata: + type: object + spec: + type: object + properties: + class: + type: string + description: The Class for the Kafka Connector. + tasksMax: + type: integer + minimum: 1 + description: The maximum number of tasks for the Kafka Connector. + autoRestart: + type: object + properties: + enabled: + type: boolean + description: >- + Whether automatic restart for failed connectors and + tasks should be enabled or disabled. + maxRestarts: + type: integer + description: >- + The maximum number of connector restarts that the + operator will try. If the connector remains in a failed + state after reaching this limit, it must be restarted + manually by the user. Defaults to an unlimited number of + restarts. + description: Automatic restart of connector and tasks configuration. + version: + type: string + description: >- + Desired version or version range to respect when starting + the Kafka Connector. This is only supported when using Kafka + Connect version 4.1.0 and higher. + config: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + The Kafka Connector configuration. The following properties + cannot be set: name, connector.class, tasks.max, + connector.plugin.version. + state: + type: string + enum: + - paused + - stopped + - running + description: The state the connector should be in. Defaults to running. + listOffsets: + type: object + properties: + toConfigMap: + type: object + properties: + name: + type: string + description: >- + Reference to the ConfigMap where the list of offsets + will be written to. + required: + - toConfigMap + description: Configuration for listing offsets. + alterOffsets: + type: object + properties: + fromConfigMap: + type: object + properties: + name: + type: string + description: >- + Reference to the ConfigMap where the new offsets are + stored. + required: + - fromConfigMap + description: Configuration for altering offsets. + description: The specification of the Kafka Connector. + status: + type: object + properties: + conditions: + type: array + items: + type: object + properties: + type: + type: string + description: >- + The unique identifier of a condition, used to + distinguish between other conditions in the resource. + status: + type: string + description: >- + The status of the condition, either True, False or + Unknown. + lastTransitionTime: + type: string + description: >- + Last time the condition of a type changed from one + status to another. The required format is + 'yyyy-MM-ddTHH:mm:ssZ', in the UTC time zone. + reason: + type: string + description: >- + The reason for the condition's last transition (a + single word in CamelCase). + message: + type: string + description: >- + Human-readable message indicating details about the + condition's last transition. + description: List of status conditions. + observedGeneration: + type: integer + description: >- + The generation of the CRD that was last reconciled by the + operator. + autoRestart: + type: object + properties: + count: + type: integer + description: The number of times the connector or task is restarted. + connectorName: + type: string + description: The name of the connector being restarted. + lastRestartTimestamp: + type: string + description: >- + The last time the automatic restart was attempted. The + required format is 'yyyy-MM-ddTHH:mm:ssZ' in the UTC + time zone. + description: The auto restart status. + connectorStatus: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + The connector status, as reported by the Kafka Connect REST + API. + tasksMax: + type: integer + description: The maximum number of tasks for the Kafka Connector. + topics: + type: array + items: + type: string + description: The list of topics used by the Kafka Connector. + description: The status of the Kafka Connector. + required: + - spec + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: strimzi-cluster-operator-entity-operator-delegation + labels: + app: strimzi + namespace: kafka +subjects: + - kind: ServiceAccount + name: strimzi-cluster-operator + namespace: kafka +roleRef: + kind: ClusterRole + name: strimzi-entity-operator + apiGroup: rbac.authorization.k8s.io + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: kafkarebalances.kafka.strimzi.io + labels: + app: strimzi + strimzi.io/crd-install: 'true' +spec: + group: kafka.strimzi.io + names: + kind: KafkaRebalance + listKind: KafkaRebalanceList + singular: kafkarebalance + plural: kafkarebalances + shortNames: + - kr + categories: + - strimzi + scope: Namespaced + conversion: + strategy: None + versions: + - name: v1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Cluster + description: The name of the Kafka cluster this resource rebalances + jsonPath: .metadata.labels.strimzi\.io/cluster + type: string + - name: Template + description: If this rebalance resource is a template + jsonPath: .metadata.annotations.strimzi\.io/rebalance-template + type: string + - name: Status + description: Status of the current rebalancing operation + jsonPath: '.status.conditions[*].type' + type: string + schema: + openAPIV3Schema: + type: object + properties: + apiVersion: + type: string + description: >- + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the + latest internal value, and may reject unrecognized values. More + info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + kind: + type: string + description: >- + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the + client submits requests to. Cannot be updated. In CamelCase. + More info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + metadata: + type: object + spec: + type: object + properties: + mode: + type: string + enum: + - full + - add-brokers + - remove-brokers + - remove-disks + description: >- + Mode to run the rebalancing. The supported modes are `full`, + `add-brokers`, `remove-brokers`. + + If not specified, the `full` mode is used by default. + + + * `full` mode runs the rebalancing across all the brokers in + the cluster. + + * `add-brokers` mode can be used after scaling up the + cluster to move some replicas to the newly added brokers. + + * `remove-brokers` mode can be used before scaling down the + cluster to move replicas out of the brokers to be removed. + + * `remove-disks` mode can be used to move data across the + volumes within the same broker + + . + brokers: + type: array + items: + type: integer + description: >- + The list of newly added brokers in case of scaling up or the + ones to be removed in case of scaling down to use for + rebalancing. This list can be used only with rebalancing + mode `add-brokers` and `removed-brokers`. It is ignored with + `full` mode. + goals: + type: array + items: + type: string + description: >- + A list of goals, ordered by decreasing priority, to use for + generating and executing the rebalance proposal. The + supported goals are available at + https://github.com/linkedin/cruise-control#goals. If an + empty goals list is provided, the goals declared in the + default.goals Cruise Control configuration parameter are + used. + skipHardGoalCheck: + type: boolean + description: >- + Whether to allow the hard goals specified in the Kafka CR to + be skipped in optimization proposal generation. This can be + useful when some of those hard goals are preventing a + balance solution being found. Default is false. + rebalanceDisk: + type: boolean + description: >- + Enables intra-broker disk balancing, which balances disk + space utilization between disks on the same broker. Only + applies to Kafka deployments that use JBOD storage with + multiple disks. When enabled, inter-broker balancing is + disabled. Default is false. + excludedTopics: + type: string + description: >- + A regular expression where any matching topics will be + excluded from the calculation of optimization proposals. + This expression will be parsed by the + java.util.regex.Pattern class; for more information on the + supported format consult the documentation for that class. + concurrentPartitionMovementsPerBroker: + type: integer + minimum: 0 + description: >- + The upper bound of ongoing partition replica movements going + into/out of each broker. Default is 5. + concurrentIntraBrokerPartitionMovements: + type: integer + minimum: 0 + description: >- + The upper bound of ongoing partition replica movements + between disks within each broker. Default is 2. + concurrentLeaderMovements: + type: integer + minimum: 0 + description: >- + The upper bound of ongoing partition leadership movements. + Default is 1000. + replicationThrottle: + type: integer + minimum: 0 + description: >- + The upper bound, in bytes per second, on the bandwidth used + to move replicas. There is no limit by default. + replicaMovementStrategies: + type: array + items: + type: string + description: >- + A list of strategy class names used to determine the + execution order for the replica movements in the generated + optimization proposal. By default + BaseReplicaMovementStrategy is used, which will execute the + replica movements in the order that they were generated. + moveReplicasOffVolumes: + type: array + minItems: 1 + items: + type: object + properties: + brokerId: + type: integer + description: >- + ID of the broker that contains the disk from which you + want to move the partition replicas. + volumeIds: + type: array + minItems: 1 + items: + type: integer + description: >- + IDs of the disks from which the partition replicas + need to be moved. + description: >- + List of brokers and their corresponding volumes from which + replicas need to be moved. + description: The specification of the Kafka rebalance. + status: + type: object + properties: + conditions: + type: array + items: + type: object + properties: + type: + type: string + description: >- + The unique identifier of a condition, used to + distinguish between other conditions in the resource. + status: + type: string + description: >- + The status of the condition, either True, False or + Unknown. + lastTransitionTime: + type: string + description: >- + Last time the condition of a type changed from one + status to another. The required format is + 'yyyy-MM-ddTHH:mm:ssZ', in the UTC time zone. + reason: + type: string + description: >- + The reason for the condition's last transition (a + single word in CamelCase). + message: + type: string + description: >- + Human-readable message indicating details about the + condition's last transition. + description: List of status conditions. + observedGeneration: + type: integer + description: >- + The generation of the CRD that was last reconciled by the + operator. + sessionId: + type: string + description: >- + The session identifier for requests to Cruise Control + pertaining to this KafkaRebalance resource. This is used by + the Kafka Rebalance operator to track the status of ongoing + rebalancing operations. + progress: + type: object + properties: + rebalanceProgressConfigMap: + type: string + description: >- + The name of the `ConfigMap` containing information + related to the progress of a partition rebalance. + description: A reference to Config Map with the progress information. + optimizationResult: + x-kubernetes-preserve-unknown-fields: true + type: object + description: A JSON object describing the optimization result. + description: The status of the Kafka rebalance. + required: + - spec + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: strimzi-cluster-operator-kafka-client-delegation + labels: + app: strimzi +subjects: + - kind: ServiceAccount + name: strimzi-cluster-operator + namespace: kafka +roleRef: + kind: ClusterRole + name: strimzi-kafka-client + apiGroup: rbac.authorization.k8s.io + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: strimzi-cluster-operator-watched + labels: + app: strimzi + namespace: kafka +subjects: + - kind: ServiceAccount + name: strimzi-cluster-operator + namespace: kafka +roleRef: + kind: ClusterRole + name: strimzi-cluster-operator-watched + apiGroup: rbac.authorization.k8s.io + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: strimzi-cluster-operator-namespaced + labels: + app: strimzi +rules: + - apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + verbs: + - get + - list + - watch + - create + - delete + - patch + - update + - apiGroups: + - rbac.authorization.k8s.io + resources: + - roles + verbs: + - get + - list + - watch + - create + - delete + - patch + - update + - apiGroups: + - '' + resources: + - pods + - serviceaccounts + - configmaps + - services + - endpoints + - secrets + - persistentvolumeclaims + verbs: + - get + - list + - watch + - create + - delete + - patch + - update + - apiGroups: + - '' + resources: + - pods/resize + verbs: + - patch + - update + - apiGroups: + - apps + resources: + - deployments + - replicasets + verbs: + - get + - list + - watch + - create + - delete + - patch + - update + - apiGroups: + - apps + resources: + - deployments/scale + verbs: + - get + - patch + - update + - apiGroups: + - events.k8s.io + resources: + - events + verbs: + - create + - apiGroups: + - build.openshift.io + resources: + - buildconfigs + - buildconfigs/instantiate + - builds + verbs: + - get + - list + - watch + - create + - delete + - patch + - update + - apiGroups: + - networking.k8s.io + resources: + - networkpolicies + - ingresses + verbs: + - get + - list + - watch + - create + - delete + - patch + - update + - apiGroups: + - route.openshift.io + resources: + - routes + - routes/custom-host + verbs: + - get + - list + - watch + - create + - delete + - patch + - update + - apiGroups: + - image.openshift.io + resources: + - imagestreams + verbs: + - get + - apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - get + - list + - watch + - create + - delete + - patch + - update + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: strimzi-cluster-operator-leader-election + labels: + app: strimzi +rules: + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - apiGroups: + - coordination.k8s.io + resources: + - leases + resourceNames: + - strimzi-cluster-operator + verbs: + - get + - list + - watch + - delete + - patch + - update + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: strimzi-cluster-operator + labels: + app: strimzi + namespace: kafka +spec: + replicas: 1 + selector: + matchLabels: + name: strimzi-cluster-operator + strimzi.io/kind: cluster-operator + template: + metadata: + labels: + name: strimzi-cluster-operator + strimzi.io/kind: cluster-operator + spec: + serviceAccountName: strimzi-cluster-operator + volumes: + - name: strimzi-tmp + emptyDir: + medium: Memory + sizeLimit: 1Mi + - name: co-config-volume + configMap: + name: strimzi-cluster-operator + containers: + - name: strimzi-cluster-operator + image: 'quay.io/strimzi/operator:1.0.0' + ports: + - containerPort: 8080 + name: http + args: + - /opt/strimzi/bin/cluster_operator_run.sh + volumeMounts: + - name: strimzi-tmp + mountPath: /tmp + - name: co-config-volume + mountPath: /opt/strimzi/custom-config/ + env: + - name: STRIMZI_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: STRIMZI_FULL_RECONCILIATION_INTERVAL_MS + value: '120000' + - name: STRIMZI_OPERATION_TIMEOUT_MS + value: '300000' + - name: STRIMZI_DEFAULT_KAFKA_EXPORTER_IMAGE + value: 'quay.io/strimzi/kafka:1.0.0-kafka-4.2.0' + - name: STRIMZI_DEFAULT_CRUISE_CONTROL_IMAGE + value: 'quay.io/strimzi/kafka:1.0.0-kafka-4.2.0' + - name: STRIMZI_KAFKA_IMAGES + value: | + 4.1.0=quay.io/strimzi/kafka:1.0.0-kafka-4.1.0 + 4.1.1=quay.io/strimzi/kafka:1.0.0-kafka-4.1.1 + 4.1.2=quay.io/strimzi/kafka:1.0.0-kafka-4.1.2 + 4.2.0=quay.io/strimzi/kafka:1.0.0-kafka-4.2.0 + - name: STRIMZI_KAFKA_CONNECT_IMAGES + value: | + 4.1.0=quay.io/strimzi/kafka:1.0.0-kafka-4.1.0 + 4.1.1=quay.io/strimzi/kafka:1.0.0-kafka-4.1.1 + 4.1.2=quay.io/strimzi/kafka:1.0.0-kafka-4.1.2 + 4.2.0=quay.io/strimzi/kafka:1.0.0-kafka-4.2.0 + - name: STRIMZI_KAFKA_MIRROR_MAKER_2_IMAGES + value: | + 4.1.0=quay.io/strimzi/kafka:1.0.0-kafka-4.1.0 + 4.1.1=quay.io/strimzi/kafka:1.0.0-kafka-4.1.1 + 4.1.2=quay.io/strimzi/kafka:1.0.0-kafka-4.1.2 + 4.2.0=quay.io/strimzi/kafka:1.0.0-kafka-4.2.0 + - name: STRIMZI_DEFAULT_TOPIC_OPERATOR_IMAGE + value: 'quay.io/strimzi/operator:1.0.0' + - name: STRIMZI_DEFAULT_USER_OPERATOR_IMAGE + value: 'quay.io/strimzi/operator:1.0.0' + - name: STRIMZI_DEFAULT_KAFKA_INIT_IMAGE + value: 'quay.io/strimzi/operator:1.0.0' + - name: STRIMZI_DEFAULT_KAFKA_BRIDGE_IMAGE + value: 'quay.io/strimzi/kafka-bridge:1.0.0' + - name: STRIMZI_DEFAULT_KANIKO_EXECUTOR_IMAGE + value: 'quay.io/strimzi/kaniko-executor:1.0.0' + - name: STRIMZI_DEFAULT_BUILDAH_IMAGE + value: 'quay.io/strimzi/buildah:1.0.0' + - name: STRIMZI_DEFAULT_MAVEN_BUILDER + value: 'quay.io/strimzi/maven-builder:1.0.0' + - name: STRIMZI_OPERATOR_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: STRIMZI_FEATURE_GATES + value: '' + - name: STRIMZI_LEADER_ELECTION_ENABLED + value: 'true' + - name: STRIMZI_LEADER_ELECTION_LEASE_NAME + value: strimzi-cluster-operator + - name: STRIMZI_LEADER_ELECTION_LEASE_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: STRIMZI_LEADER_ELECTION_IDENTITY + valueFrom: + fieldRef: + fieldPath: metadata.name + livenessProbe: + httpGet: + path: /healthy + port: http + initialDelaySeconds: 10 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /ready + port: http + initialDelaySeconds: 10 + periodSeconds: 30 + resources: + limits: + cpu: 1000m + memory: 384Mi + requests: + cpu: 200m + memory: 384Mi + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: kafkabridges.kafka.strimzi.io + labels: + app: strimzi + strimzi.io/crd-install: 'true' +spec: + group: kafka.strimzi.io + names: + kind: KafkaBridge + listKind: KafkaBridgeList + singular: kafkabridge + plural: kafkabridges + shortNames: + - kb + categories: + - strimzi + scope: Namespaced + conversion: + strategy: None + versions: + - name: v1 + served: true + storage: true + subresources: + status: {} + scale: + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + labelSelectorPath: .status.labelSelector + additionalPrinterColumns: + - name: Desired replicas + description: The desired number of Kafka Bridge replicas + jsonPath: .spec.replicas + type: integer + - name: Bootstrap Servers + description: The boostrap servers + jsonPath: .spec.bootstrapServers + type: string + priority: 1 + - name: Ready + description: The state of the custom resource + jsonPath: '.status.conditions[?(@.type=="Ready")].status' + type: string + schema: + openAPIV3Schema: + type: object + properties: + apiVersion: + type: string + description: >- + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the + latest internal value, and may reject unrecognized values. More + info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + kind: + type: string + description: >- + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the + client submits requests to. Cannot be updated. In CamelCase. + More info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + metadata: + type: object + spec: + type: object + properties: + replicas: + type: integer + minimum: 0 + description: >- + The number of pods in the `Deployment`. Required in the `v1` + version of the Strimzi API. Defaults to `1` in the `v1beta2` + version of the Strimzi API. + image: + type: string + description: >- + The container image used for HTTP Bridge pods. If no image + name is explicitly specified, the image name corresponds to + the image specified in the Cluster Operator configuration. + If an image name is not defined in the Cluster Operator + configuration, a default value is used. + bootstrapServers: + type: string + description: >- + A list of host:port pairs for establishing the initial + connection to the Kafka cluster. + tls: + type: object + properties: + trustedCertificates: + type: array + items: + type: object + properties: + secretName: + type: string + description: The name of the Secret containing the certificate. + certificate: + type: string + description: The name of the file certificate in the secret. + pattern: + type: string + description: >- + Pattern for the certificate files in the secret. + Use the + link:https://en.wikipedia.org/wiki/Glob_(programming)[_glob + syntax_] for the pattern. All files in the secret + that match the pattern are used. + oneOf: + - properties: + certificate: {} + required: + - certificate + - properties: + pattern: {} + required: + - pattern + required: + - secretName + description: Trusted certificates for TLS connection. + description: TLS configuration for connecting HTTP Bridge to the cluster. + authentication: + type: object + properties: + certificateAndKey: + type: object + properties: + secretName: + type: string + description: The name of the Secret containing the certificate. + certificate: + type: string + description: The name of the file certificate in the Secret. + key: + type: string + description: >- + The name of the private key in the secret. The + private key must be in unencrypted PKCS #8 format. + For more information, see RFC 5208: + https://datatracker.ietf.org/doc/html/rfc5208. + required: + - secretName + - certificate + - key + description: >- + Reference to the `Secret` which holds the certificate + and private key pair. + config: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + Configuration for the custom authentication mechanism. + Only properties with the `sasl.` and `ssl.keystore.` + prefixes are allowed. Specify other options in the + regular configuration section of the custom resource. + passwordSecret: + type: object + properties: + secretName: + type: string + description: The name of the Secret containing the password. + password: + type: string + description: >- + The name of the key in the Secret under which the + password is stored. + required: + - secretName + - password + description: Reference to the `Secret` which holds the password. + sasl: + type: boolean + description: Enable or disable SASL on this authentication mechanism. + type: + type: string + enum: + - tls + - scram-sha-256 + - scram-sha-512 + - plain + - custom + description: >- + Specifies the authentication type. Supported types are + `tls`, `scram-sha-256`, `scram-sha-512`, `plain`, + 'oauth', and `custom`. `tls` uses TLS client + authentication and is supported only over TLS + connections. `scram-sha-256` and `scram-sha-512` use + SASL SCRAM-SHA-256 and SASL SCRAM-SHA-512 + authentication, respectively. `plain` uses SASL PLAIN + authentication. `oauth` uses SASL OAUTHBEARER + authentication. `custom` allows you to configure a + custom authentication mechanism. As of Strimzi 0.49.0, + `oauth` type is deprecated and will be removed in the + `v1` API version. Please use `custom` type instead. + username: + type: string + description: Username used for the authentication. + required: + - type + description: Authentication configuration for connecting to the cluster. + http: + type: object + properties: + port: + type: integer + minimum: 1023 + description: Port the server listens on. + tls: + type: object + properties: + certificateAndKey: + type: object + properties: + secretName: + type: string + description: >- + The name of the Secret containing the + certificate. + certificate: + type: string + description: The name of the file certificate in the Secret. + key: + type: string + description: >- + The name of the private key in the secret. The + private key must be in unencrypted PKCS #8 + format. For more information, see RFC 5208: + https://datatracker.ietf.org/doc/html/rfc5208. + required: + - secretName + - certificate + - key + description: >- + Reference to the `Secret` which holds the + certificate and private key pair. + config: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + Additional configuration for the HTTP server TLS. + Properties with the following prefixes cannot be + set: ssl. (with the exception of: + ssl.enabled.cipher.suites, ssl.enabled.protocols). + required: + - certificateAndKey + description: >- + TLS configuration for clients connections to the HTTP + Bridge. + cors: + type: object + properties: + allowedOrigins: + type: array + items: + type: string + description: >- + List of allowed origins. Java regular expressions + can be used. + allowedMethods: + type: array + items: + type: string + description: List of allowed HTTP methods. + required: + - allowedOrigins + - allowedMethods + description: CORS configuration for the HTTP Bridge. + description: The HTTP related configuration. + adminClient: + type: object + properties: + config: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + The Kafka AdminClient configuration used for AdminClient + instances created by the bridge. + description: Kafka AdminClient related configuration. + consumer: + type: object + properties: + enabled: + type: boolean + description: >- + Whether the HTTP consumer should be enabled or disabled. + The default is enabled (`true`). + timeoutSeconds: + type: integer + description: >- + The timeout in seconds for deleting inactive consumers, + default is -1 (disabled). + config: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + The Kafka consumer configuration used for consumer + instances created by the bridge. Properties with the + following prefixes cannot be set: ssl., + bootstrap.servers, group.id, sasl., security. (with the + exception of: ssl.endpoint.identification.algorithm, + ssl.cipher.suites, ssl.protocol, ssl.enabled.protocols). + description: Kafka consumer related configuration. + producer: + type: object + properties: + enabled: + type: boolean + description: >- + Whether the HTTP producer should be enabled or disabled. + The default is enabled (`true`). + config: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + The Kafka producer configuration used for producer + instances created by the bridge. Properties with the + following prefixes cannot be set: ssl., + bootstrap.servers, sasl., security. (with the exception + of: ssl.endpoint.identification.algorithm, + ssl.cipher.suites, ssl.protocol, ssl.enabled.protocols). + description: Kafka producer related configuration. + resources: + type: object + properties: + claims: + type: array + items: + type: object + properties: + name: + type: string + request: + type: string + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: >- + ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: >- + ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + description: CPU and memory resources to reserve. + jvmOptions: + type: object + properties: + '-XX': + additionalProperties: + type: string + type: object + description: A map of -XX options to the JVM. + '-Xmx': + type: string + pattern: '^[0-9]+[mMgG]?$' + description: '-Xmx option to to the JVM.' + '-Xms': + type: string + pattern: '^[0-9]+[mMgG]?$' + description: '-Xms option to to the JVM.' + gcLoggingEnabled: + type: boolean + description: >- + Specifies whether the Garbage Collection logging is + enabled. The default is false. + javaSystemProperties: + type: array + items: + type: object + properties: + name: + type: string + description: The system property name. + value: + type: string + description: The system property value. + description: >- + A map of additional system properties which will be + passed using the `-D` option to the JVM. + description: JVM Options for pods. + logging: + type: object + properties: + loggers: + additionalProperties: + type: string + type: object + description: A Map from logger name to logger level. + type: + type: string + enum: + - inline + - external + description: 'Logging type, must be either ''inline'' or ''external''.' + valueFrom: + type: object + properties: + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: >- + Reference to the key in the ConfigMap containing the + configuration. + description: >- + `ConfigMap` entry where the logging configuration is + stored. + required: + - type + description: Logging configuration for HTTP Bridge. + clientRackInitImage: + type: string + description: >- + The image of the init container used for initializing the + `client.rack`. + rack: + type: object + properties: + envVarName: + type: string + description: >- + The name of the environment variable that defines the + rack ID. Its value sets the `broker.rack` configuration + for Kafka brokers and the `client.rack` configuration + for Kafka Connect or MirrorMaker 2. + topologyKey: + type: string + example: topology.kubernetes.io/zone + description: >- + A key that matches labels assigned to the Kubernetes + cluster nodes. The value of the label is used to set a + broker's `broker.rack` config, and the `client.rack` + config for Kafka Connect or MirrorMaker 2. + type: + type: string + enum: + - topology-label + - environment-variable + description: >- + Specifies the rack awareness type. Supported types are + `topology-label` and `environment-variable`. + `topology-label` uses a Kubernetes worker node label to + set the `broker.rack` configuration for Kafka brokers + and the `client.rack` configuration for Kafka Connect + and MirrorMaker 2. `environment-variable` uses an + environment variable to set the `broker.rack` + configuration for Kafka brokers and the `client.rack` + configuration for Kafka Connect and MirrorMaker 2. When + not specified, `topology-label` type is used by default. + description: >- + Configuration of the node label which will be used as the + client.rack consumer configuration. + x-kubernetes-validations: + - rule: >- + (has(self.type) && self.type != "topology-label") || + self.topologyKey != "" + message: topologyKey property is required + - rule: >- + has(self.type) == false || self.type != + "environment-variable" || self.envVarName != "" + message: envVarName property is required + metricsConfig: + type: object + properties: + type: + type: string + enum: + - jmxPrometheusExporter + - strimziMetricsReporter + description: >- + Metrics type. The supported types are + `jmxPrometheusExporter` and `strimziMetricsReporter`. + Type `jmxPrometheusExporter` uses the Prometheus JMX + Exporter to expose Kafka JMX metrics in Prometheus + format through an HTTP endpoint. Type + `strimziMetricsReporter` uses the Strimzi Metrics + Reporter to directly expose Kafka metrics in Prometheus + format through an HTTP endpoint. + valueFrom: + type: object + properties: + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: >- + Reference to the key in the ConfigMap containing the + configuration. + description: >- + ConfigMap entry where the Prometheus JMX Exporter + configuration is stored. + values: + type: object + properties: + allowList: + type: array + items: + type: string + description: >- + A list of regex patterns to filter the metrics to + collect. Should contain at least one element. + description: Configuration values for the Strimzi Metrics Reporter. + required: + - type + description: Metrics configuration. + x-kubernetes-validations: + - rule: >- + self.type != 'jmxPrometheusExporter' || + has(self.valueFrom) + message: valueFrom property is required + livenessProbe: + type: object + properties: + initialDelaySeconds: + type: integer + minimum: 0 + description: >- + The initial delay before first the health is first + checked. Default to 15 seconds. Minimum value is 0. + timeoutSeconds: + type: integer + minimum: 1 + description: >- + The timeout for each attempted health check. Default to + 5 seconds. Minimum value is 1. + periodSeconds: + type: integer + minimum: 1 + description: >- + How often (in seconds) to perform the probe. Default to + 10 seconds. Minimum value is 1. + successThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive successes for the probe to be + considered successful after having failed. Defaults to + 1. Must be 1 for liveness. Minimum value is 1. + failureThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive failures for the probe to be + considered failed after having succeeded. Defaults to 3. + Minimum value is 1. + description: Pod liveness checking. + readinessProbe: + type: object + properties: + initialDelaySeconds: + type: integer + minimum: 0 + description: >- + The initial delay before first the health is first + checked. Default to 15 seconds. Minimum value is 0. + timeoutSeconds: + type: integer + minimum: 1 + description: >- + The timeout for each attempted health check. Default to + 5 seconds. Minimum value is 1. + periodSeconds: + type: integer + minimum: 1 + description: >- + How often (in seconds) to perform the probe. Default to + 10 seconds. Minimum value is 1. + successThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive successes for the probe to be + considered successful after having failed. Defaults to + 1. Must be 1 for liveness. Minimum value is 1. + failureThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive failures for the probe to be + considered failed after having succeeded. Defaults to 3. + Minimum value is 1. + description: Pod readiness checking. + template: + type: object + properties: + deployment: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + deploymentStrategy: + type: string + enum: + - RollingUpdate + - Recreate + description: >- + Pod replacement strategy for deployment + configuration changes. Valid values are + `RollingUpdate` and `Recreate`. Defaults to + `RollingUpdate`. + description: Template for HTTP Bridge `Deployment`. + pod: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + imagePullSecrets: + type: array + items: + type: object + properties: + name: + type: string + description: >- + List of references to secrets in the same namespace + to use for pulling any of the images used by this + Pod. When the `STRIMZI_IMAGE_PULL_SECRETS` + environment variable in Cluster Operator and the + `imagePullSecrets` option are specified, only the + `imagePullSecrets` variable is used and the + `STRIMZI_IMAGE_PULL_SECRETS` variable is ignored. + securityContext: + type: object + properties: + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + fsGroup: + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + supplementalGroups: + type: array + items: + type: integer + supplementalGroupsPolicy: + type: string + sysctls: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: >- + Configures pod-level security attributes and common + container settings. + terminationGracePeriodSeconds: + type: integer + minimum: 0 + description: >- + The grace period is the duration in seconds after + the processes running in the pod are sent a + termination signal, and the time when the processes + are forcibly halted with a kill signal. Set this + value to longer than the expected cleanup time for + your process. Value must be a non-negative integer. + A zero value indicates delete immediately. You might + need to increase the grace period for very large + Kafka clusters, so that the Kafka brokers have + enough time to transfer their work to another broker + before they are terminated. Defaults to 30 seconds. + affinity: + type: object + properties: + nodeAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + preference: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: object + properties: + nodeSelectorTerms: + type: array + items: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + podAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + podAntiAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + description: The pod's affinity rules. + tolerations: + type: array + items: + type: object + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + description: The pod's tolerations. + topologySpreadConstraints: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + maxSkew: + type: integer + minDomains: + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + description: The pod's topology spread constraints. + priorityClassName: + type: string + description: >- + The name of the priority class used to assign + priority to the pods. + schedulerName: + type: string + description: >- + The name of the scheduler used to dispatch this + `Pod`. If not specified, the default scheduler will + be used. + hostAliases: + type: array + items: + type: object + properties: + hostnames: + type: array + items: + type: string + ip: + type: string + description: >- + The pod's HostAliases. HostAliases is an optional + list of hosts and IPs that will be injected into the + Pod's hosts file if specified. + dnsPolicy: + type: string + enum: + - ClusterFirst + - ClusterFirstWithHostNet + - Default + - None + description: >- + The pod's DNSPolicy. Defaults to `ClusterFirst`. + Valid values are `ClusterFirstWithHostNet`, + `ClusterFirst`, `Default` or `None`. + dnsConfig: + type: object + properties: + nameservers: + type: array + items: + type: string + options: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + searches: + type: array + items: + type: string + description: >- + The pod's DNSConfig. If specified, it will be merged + to the generated DNS configuration based on the + DNSPolicy. + enableServiceLinks: + type: boolean + description: >- + Indicates whether information about services should + be injected into Pod's environment variables. + tmpDirSizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + Defines the total amount of pod memory allocated for + the temporary `EmptyDir` volume `/tmp`. Specify the + allocation in memory units, for example, `100Mi` for + 100 mebibytes. Default value is `5Mi`. The `/tmp` + volume is backed by pod memory, not disk storage, so + avoid setting a high value as it consumes pod memory + resources. + volumes: + type: array + items: + type: object + properties: + name: + type: string + description: Name to use for the volume. Required. + secret: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + optional: + type: boolean + secretName: + type: string + description: '`Secret` to use to populate the volume.' + configMap: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + name: + type: string + optional: + type: boolean + description: '`ConfigMap` to use to populate the volume.' + emptyDir: + type: object + properties: + medium: + type: string + enum: + - Memory + description: >- + Medium represents the type of storage + medium should back this volume. Valid + values are unset or `Memory`. When not + set, it will use the node's default + medium. + sizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + The total amount of local storage required + for this EmptyDir volume (for example + 1Gi). + description: '`EmptyDir` to use to populate the volume.' + persistentVolumeClaim: + type: object + properties: + claimName: + type: string + readOnly: + type: boolean + description: >- + `PersistentVolumeClaim` object to use to + populate the volume. + csi: + type: object + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + type: object + properties: + name: + type: string + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + description: >- + `CSIVolumeSource` object to use to populate + the volume. + image: + type: object + properties: + pullPolicy: + type: string + reference: + type: string + description: >- + `ImageVolumeSource` object to use to populate + the volume. + oneOf: + - properties: + secret: {} + configMap: {} + emptyDir: {} + persistentVolumeClaim: {} + csi: {} + image: {} + description: Additional volumes that can be mounted to the pod. + hostUsers: + type: boolean + description: >- + Use the host user namespace. Optional. Defaults to + `true`. When `true` or not set, the pod runs in the + host user namespace. This is required when the pod + needs features available only in the host namespace, + such as loading kernel modules with + `CAP_SYS_MODULE`.When set to `false`, the pod runs + in a new user namespace. Setting `false` helps + mitigate container breakout vulnerabilities and + allows containers to run as `root` without granting + `root` privileges on the host. This property is + alpha-level in Kubernetes and is supported only by + Kubernetes clusters that enable the + `UserNamespacesSupport` feature. + description: Template for HTTP Bridge `Pods`. + apiService: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + ipFamilyPolicy: + type: string + enum: + - SingleStack + - PreferDualStack + - RequireDualStack + description: >- + Specifies the IP Family Policy used by the service. + Available options are `SingleStack`, + `PreferDualStack` and `RequireDualStack`. + `SingleStack` is for a single IP family. + `PreferDualStack` is for two IP families on + dual-stack configured clusters or a single IP family + on single-stack clusters. `RequireDualStack` fails + unless there are two IP families on dual-stack + configured clusters. If unspecified, Kubernetes will + choose the default value based on the service type. + ipFamilies: + type: array + items: + type: string + enum: + - IPv4 + - IPv6 + description: >- + Specifies the IP Families used by the service. + Available options are `IPv4` and `IPv6`. If + unspecified, Kubernetes will choose the default + value based on the `ipFamilyPolicy` setting. + description: Template for HTTP Bridge API `Service`. + podDisruptionBudget: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: >- + Metadata to apply to the + `PodDisruptionBudgetTemplate` resource. + maxUnavailable: + type: integer + minimum: 0 + description: >- + Maximum number of unavailable pods to allow + automatic Pod eviction. A Pod eviction is allowed + when the `maxUnavailable` number of pods or fewer + are unavailable after the eviction. Setting this + value to 0 prevents all voluntary evictions, so the + pods must be evicted manually. Defaults to 1. + description: Template for HTTP Bridge `PodDisruptionBudget`. + bridgeContainer: + type: object + properties: + env: + type: array + items: + type: object + properties: + name: + type: string + description: The environment variable key. + value: + type: string + description: The environment variable value. + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a secret. + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a config map. + oneOf: + - properties: + secretKeyRef: {} + required: + - secretKeyRef + - properties: + configMapKeyRef: {} + required: + - configMapKeyRef + description: >- + Reference to the secret or config map property + to which the environment variable is set. + oneOf: + - properties: + value: {} + required: + - value + - properties: + valueFrom: {} + required: + - valueFrom + description: >- + Environment variables which should be applied to the + container. + securityContext: + type: object + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + capabilities: + type: object + properties: + add: + type: array + items: + type: string + drop: + type: array + items: + type: string + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: Security context for the container. + volumeMounts: + type: array + items: + type: object + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + description: >- + Additional volume mounts which should be applied to + the container. + description: Template for the HTTP Bridge container. + clusterRoleBinding: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + description: Template for the HTTP Bridge ClusterRoleBinding. + serviceAccount: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + description: Template for the HTTP Bridge service account. + initContainer: + type: object + properties: + env: + type: array + items: + type: object + properties: + name: + type: string + description: The environment variable key. + value: + type: string + description: The environment variable value. + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a secret. + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a config map. + oneOf: + - properties: + secretKeyRef: {} + required: + - secretKeyRef + - properties: + configMapKeyRef: {} + required: + - configMapKeyRef + description: >- + Reference to the secret or config map property + to which the environment variable is set. + oneOf: + - properties: + value: {} + required: + - value + - properties: + valueFrom: {} + required: + - valueFrom + description: >- + Environment variables which should be applied to the + container. + securityContext: + type: object + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + capabilities: + type: object + properties: + add: + type: array + items: + type: string + drop: + type: array + items: + type: string + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: Security context for the container. + volumeMounts: + type: array + items: + type: object + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + description: >- + Additional volume mounts which should be applied to + the container. + description: Template for the HTTP Bridge init container. + description: >- + Template for HTTP Bridge resources. The template allows + users to specify how a `Deployment` and `Pod` is generated. + tracing: + type: object + properties: + type: + type: string + enum: + - opentelemetry + description: >- + Type of the tracing used. Currently the only supported + type is `opentelemetry` for OpenTelemetry tracing. As of + Strimzi 0.37.0, `jaeger` type is not supported anymore + and this option is ignored. + required: + - type + description: The configuration of tracing in HTTP Bridge. + config: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + Additional configuration for the HTTP bridge. The following + prefixes cannot be set: kafka., http., bridge.metrics. The + following options cannot be set: bridge.id, bridge.tracing, + bridge.metrics. + required: + - replicas + - bootstrapServers + description: The specification of the HTTP Bridge. + status: + type: object + properties: + conditions: + type: array + items: + type: object + properties: + type: + type: string + description: >- + The unique identifier of a condition, used to + distinguish between other conditions in the resource. + status: + type: string + description: >- + The status of the condition, either True, False or + Unknown. + lastTransitionTime: + type: string + description: >- + Last time the condition of a type changed from one + status to another. The required format is + 'yyyy-MM-ddTHH:mm:ssZ', in the UTC time zone. + reason: + type: string + description: >- + The reason for the condition's last transition (a + single word in CamelCase). + message: + type: string + description: >- + Human-readable message indicating details about the + condition's last transition. + description: List of status conditions. + observedGeneration: + type: integer + description: >- + The generation of the CRD that was last reconciled by the + operator. + url: + type: string + description: >- + The URL at which external client applications can access the + HTTP Bridge. + replicas: + type: integer + description: >- + The current number of pods being used to provide this + resource. + labelSelector: + type: string + description: Label selector for pods providing this resource. + description: The status of the HTTP Bridge. + required: + - spec + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: strimzi-cluster-operator-kafka-broker-delegation + labels: + app: strimzi +subjects: + - kind: ServiceAccount + name: strimzi-cluster-operator + namespace: kafka +roleRef: + kind: ClusterRole + name: strimzi-kafka-broker + apiGroup: rbac.authorization.k8s.io + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: strimzi-kafka-broker + labels: + app: strimzi +rules: + - apiGroups: + - '' + resources: + - nodes + verbs: + - get + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: strimzipodsets.core.strimzi.io + labels: + app: strimzi + strimzi.io/crd-install: 'true' +spec: + group: core.strimzi.io + names: + kind: StrimziPodSet + listKind: StrimziPodSetList + singular: strimzipodset + plural: strimzipodsets + shortNames: + - sps + categories: + - strimzi + scope: Namespaced + conversion: + strategy: None + versions: + - name: v1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Pods + description: Number of pods managed by the StrimziPodSet + jsonPath: .status.pods + type: integer + - name: Ready Pods + description: Number of ready pods managed by the StrimziPodSet + jsonPath: .status.readyPods + type: integer + - name: Current Pods + description: Number of up-to-date pods managed by the StrimziPodSet + jsonPath: .status.currentPods + type: integer + - name: Age + description: Age of the StrimziPodSet + jsonPath: .metadata.creationTimestamp + type: date + schema: + openAPIV3Schema: + type: object + properties: + apiVersion: + type: string + description: >- + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the + latest internal value, and may reject unrecognized values. More + info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + kind: + type: string + description: >- + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the + client submits requests to. Cannot be updated. In CamelCase. + More info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + metadata: + type: object + spec: + type: object + properties: + selector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + description: >- + Selector is a label query which matches all the pods managed + by this `StrimziPodSet`. Only `matchLabels` is supported. If + `matchExpressions` is set, it will be ignored. + pods: + type: array + items: + x-kubernetes-preserve-unknown-fields: true + type: object + description: The Pods managed by this StrimziPodSet. + required: + - selector + - pods + description: The specification of the StrimziPodSet. + status: + type: object + properties: + conditions: + type: array + items: + type: object + properties: + type: + type: string + description: >- + The unique identifier of a condition, used to + distinguish between other conditions in the resource. + status: + type: string + description: >- + The status of the condition, either True, False or + Unknown. + lastTransitionTime: + type: string + description: >- + Last time the condition of a type changed from one + status to another. The required format is + 'yyyy-MM-ddTHH:mm:ssZ', in the UTC time zone. + reason: + type: string + description: >- + The reason for the condition's last transition (a + single word in CamelCase). + message: + type: string + description: >- + Human-readable message indicating details about the + condition's last transition. + description: List of status conditions. + observedGeneration: + type: integer + description: >- + The generation of the CRD that was last reconciled by the + operator. + pods: + type: integer + description: Number of pods managed by this `StrimziPodSet` resource. + readyPods: + type: integer + description: >- + Number of pods managed by this `StrimziPodSet` resource that + are ready. + currentPods: + type: integer + description: >- + Number of pods managed by this `StrimziPodSet` resource that + have the current revision. + description: The status of the StrimziPodSet. + required: + - spec + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: strimzi-cluster-operator + labels: + app: strimzi +subjects: + - kind: ServiceAccount + name: strimzi-cluster-operator + namespace: kafka +roleRef: + kind: ClusterRole + name: strimzi-cluster-operator-global + apiGroup: rbac.authorization.k8s.io + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: kafkas.kafka.strimzi.io + labels: + app: strimzi + strimzi.io/crd-install: 'true' +spec: + group: kafka.strimzi.io + names: + kind: Kafka + listKind: KafkaList + singular: kafka + plural: kafkas + shortNames: + - k + categories: + - strimzi + scope: Namespaced + conversion: + strategy: None + versions: + - name: v1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Ready + description: The state of the custom resource + jsonPath: '.status.conditions[?(@.type=="Ready")].status' + type: string + - name: Warnings + description: Warnings related to the custom resource + jsonPath: '.status.conditions[?(@.type=="Warning")].status' + type: string + - name: Kafka version + description: The Kafka version used by the cluster + jsonPath: .status.kafkaVersion + type: string + - name: Metadata version + description: The Kafka metadata version used by the cluster + jsonPath: .status.kafkaMetadataVersion + type: string + schema: + openAPIV3Schema: + type: object + properties: + apiVersion: + type: string + description: >- + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the + latest internal value, and may reject unrecognized values. More + info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + kind: + type: string + description: >- + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the + client submits requests to. Cannot be updated. In CamelCase. + More info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + metadata: + type: object + spec: + type: object + properties: + kafka: + type: object + properties: + version: + type: string + description: >- + The Kafka broker version. Defaults to the latest + version. Consult the user documentation to understand + the process required to upgrade or downgrade the + version. + metadataVersion: + type: string + description: >- + The KRaft metadata version used by the Kafka cluster. + This property is ignored when running in ZooKeeper mode. + If the property is not set, it defaults to the metadata + version that corresponds to the `version` property. + image: + type: string + description: >- + The container image used for Kafka pods. If the property + is not set, the default Kafka image version is + determined based on the `version` configuration. The + image names are specifically mapped to corresponding + versions in the Cluster Operator configuration. Changing + the Kafka image version does not automatically update + the image versions for other components, such as Kafka + Exporter. + listeners: + type: array + minItems: 1 + items: + type: object + properties: + name: + type: string + pattern: '^[a-z0-9]{1,11}$' + description: >- + Name of the listener. The name will be used to + identify the listener and the related Kubernetes + objects. The name has to be unique within given a + Kafka cluster. The name can consist of lowercase + characters and numbers and be up to 11 characters + long. + port: + type: integer + minimum: 9092 + description: >- + Port number used by the listener inside Kafka. The + port number has to be unique within a given Kafka + cluster. Allowed port numbers are 9092 and higher + with the exception of ports 9404 and 9999, which + are already used for Prometheus and JMX. Depending + on the listener type, the port number might not be + the same as the port number that connects Kafka + clients. + type: + type: string + enum: + - internal + - route + - loadbalancer + - nodeport + - ingress + - cluster-ip + description: > + Type of the listener. The supported types are as + follows: + + + * `internal` type exposes Kafka internally only + within the Kubernetes cluster. + + * `route` type uses OpenShift Routes to expose + Kafka. + + * `loadbalancer` type uses LoadBalancer type + services to expose Kafka. + + * `nodeport` type uses NodePort type services to + expose Kafka. + + * `ingress` (deprecated) type uses Kubernetes + Nginx Ingress to expose Kafka with TLS + passthrough. + + * `cluster-ip` type uses a per-broker `ClusterIP` + service. + tls: + type: boolean + description: >- + Enables TLS encryption on the listener. This is a + required property. For `route` and `ingress` type + listeners, TLS encryption must be always enabled. + authentication: + type: object + properties: + listenerConfig: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + Configuration to be used for a specific + listener. All values are prefixed with + `listener.name.`. + sasl: + type: boolean + description: Enable or disable SASL on this listener. + type: + type: string + enum: + - tls + - scram-sha-512 + - custom + description: >- + Authentication type. `oauth` type uses SASL + OAUTHBEARER Authentication. `scram-sha-512` + type uses SASL SCRAM-SHA-512 Authentication. + `tls` type uses TLS Client Authentication. + `tls` type is supported only on TLS listeners. + `custom` type allows for any authentication + type to be used. As of Strimzi 0.49.0, `oauth` + type is deprecated and will be removed in the + `v1` API version. Please use `custom` type + instead. + required: + - type + description: Authentication configuration for this listener. + configuration: + type: object + properties: + brokerCertChainAndKey: + type: object + properties: + secretName: + type: string + description: >- + The name of the Secret containing the + certificate. + certificate: + type: string + description: >- + The name of the file certificate in the + Secret. + key: + type: string + description: >- + The name of the private key in the secret. + The private key must be in unencrypted + PKCS #8 format. For more information, see + RFC 5208: + https://datatracker.ietf.org/doc/html/rfc5208. + required: + - secretName + - certificate + - key + description: >- + Reference to the `Secret` which holds the + certificate and private key pair which will be + used for this listener. The certificate can + optionally contain the whole chain. This field + can be used only with listeners with enabled + TLS encryption. + class: + type: string + description: >- + Configures a specific class for `Ingress` and + `LoadBalancer` that defines which controller + is used. If not specified, the default + controller is used. + + + * For an `ingress` listener, the operator uses + this property to set the `ingressClassName` + property in the `Ingress` resources. + + * For a `loadbalancer` listener, the operator + uses this property to set the + `loadBalancerClass` property in the `Service` + resources. + + + For `ingress` and `loadbalancer` listeners + only. + externalTrafficPolicy: + type: string + enum: + - Local + - Cluster + description: >- + Specifies whether the service routes external + traffic to cluster-wide or node-local + endpoints: + + + * `Cluster` may cause a second hop to another + node and obscures the client source IP. + + * `Local` avoids a second hop for + `LoadBalancer` and `Nodeport` type services + and preserves the client source IP (when + supported by the infrastructure). + + + If unspecified, Kubernetes uses `Cluster` as + the default. For `loadbalancer` or `nodeport` + listeners only. + loadBalancerSourceRanges: + type: array + items: + type: string + description: >- + A list of CIDR ranges (for example + `10.0.0.0/8` or `130.211.204.1/32`) from which + clients can connect to loadbalancer listeners. + If supported by the platform, traffic through + the loadbalancer is restricted to the + specified CIDR ranges. This field is + applicable only for loadbalancer type services + and is ignored if the cloud provider does not + support the feature. For `loadbalancer` + listeners only. + bootstrap: + type: object + properties: + alternativeNames: + type: array + items: + type: string + description: >- + Additional alternative names for the + bootstrap service. The alternative names + will be added to the list of subject + alternative names of the TLS certificates. + host: + type: string + description: >- + Specifies the hostname used for the + bootstrap resource. For `route` (optional) + or `ingress` (required) listeners only. + Ensure the hostname resolves to the + Ingress endpoints; no validation is + performed by Strimzi. + nodePort: + type: integer + description: >- + Node port for the bootstrap service. For + `nodeport` listeners only. + loadBalancerIP: + type: string + description: >- + The loadbalancer is requested with the IP + address specified in this property. This + feature depends on whether the underlying + cloud provider supports specifying the + `loadBalancerIP` when a load balancer is + created. This property is ignored if the + cloud provider does not support the + feature. For `loadbalancer` listeners + only. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to `Ingress`, `Route`, + or `Service` resources. You can use this + property to configure DNS providers such + as External DNS. For `loadbalancer`, + `nodeport`, `route`, or `ingress` + listeners only. + labels: + additionalProperties: + type: string + type: object + description: >- + Labels added to `Ingress`, `Route`, or + `Service` resources. For `loadbalancer`, + `nodeport`, `route`, or `ingress` + listeners only. + externalIPs: + type: array + items: + type: string + description: >- + External IPs associated to the nodeport + service. These IPs are used by clients + external to the Kubernetes cluster to + access the Kafka brokers. This property is + helpful when `nodeport` without + `externalIP` is not sufficient. For + example on bare-metal Kubernetes clusters + that do not support Loadbalancer service + types. For `nodeport` listeners only. + description: Bootstrap configuration. + brokers: + type: array + items: + type: object + properties: + broker: + type: integer + description: >- + ID of the kafka broker (broker + identifier). Broker IDs start from 0 and + correspond to the number of broker + replicas. + advertisedHost: + type: string + description: >- + The host name used in the brokers' + `advertised.listeners`. + advertisedPort: + type: integer + description: >- + The port number used in the brokers' + `advertised.listeners`. + host: + type: string + description: >- + The broker host. This field will be used + in the Ingress resource or in the Route + resource to specify the desired + hostname. This field can be used only + with `route` (optional) or `ingress` + (required) type listeners. + nodePort: + type: integer + description: >- + Node port for the per-broker service. + This field can be used only with + `nodeport` type listener. + loadBalancerIP: + type: string + description: >- + The loadbalancer is requested with the + IP address specified in this field. This + feature depends on whether the + underlying cloud provider supports + specifying the `loadBalancerIP` when a + load balancer is created. This field is + ignored if the cloud provider does not + support the feature.This field can be + used only with `loadbalancer` type + listener. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations that will be added to the + `Ingress` or `Service` resource. You can + use this field to configure DNS + providers such as External DNS. This + field can be used only with + `loadbalancer`, `nodeport`, or `ingress` + type listeners. + labels: + additionalProperties: + type: string + type: object + description: >- + Labels that will be added to the + `Ingress`, `Route`, or `Service` + resource. This field can be used only + with `loadbalancer`, `nodeport`, + `route`, or `ingress` type listeners. + externalIPs: + type: array + items: + type: string + description: >- + External IPs associated to the nodeport + service. These IPs are used by clients + external to the Kubernetes cluster to + access the Kafka brokers. This field is + helpful when `nodeport` without + `externalIP` is not sufficient. For + example on bare-metal Kubernetes + clusters that do not support + Loadbalancer service types. This field + can only be used with `nodeport` type + listener. + required: + - broker + description: Per-broker configurations. + ipFamilyPolicy: + type: string + enum: + - SingleStack + - PreferDualStack + - RequireDualStack + description: >- + Specifies the IP Family Policy used by the + service. Available options are `SingleStack`, + `PreferDualStack` and `RequireDualStack`: + + + * `SingleStack` is for a single IP family. + + * `PreferDualStack` is for two IP families on + dual-stack configured clusters or a single IP + family on single-stack clusters. + + * `RequireDualStack` fails unless there are + two IP families on dual-stack configured + clusters. + + + If unspecified, Kubernetes will choose the + default value based on the service type. + ipFamilies: + type: array + items: + type: string + enum: + - IPv4 + - IPv6 + description: >- + Specifies the IP Families used by the service. + Available options are `IPv4` and `IPv6`. If + unspecified, Kubernetes will choose the + default value based on the `ipFamilyPolicy` + setting. + createBootstrapService: + type: boolean + description: >- + Whether to create the bootstrap service or + not. The bootstrap service is created by + default (if not specified differently). This + field can be used with the `loadbalancer` + listener. + finalizers: + type: array + items: + type: string + description: >- + A list of finalizers configured for the + `LoadBalancer` type services created for this + listener. If supported by the platform, the + finalizer + `service.kubernetes.io/load-balancer-cleanup` + to make sure that the external load balancer + is deleted together with the service.For more + information, see + https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#garbage-collecting-load-balancers. + For `loadbalancer` listeners only. + useServiceDnsDomain: + type: boolean + description: >- + Configures whether the Kubernetes service DNS + domain should be included in the generated + addresses. + + + * If set to `false`, the generated addresses + do not contain the service DNS domain suffix. + For example, + `my-cluster-kafka-0.my-cluster-kafka-brokers.myproject.svc`. + + * If set to `true`, the generated addresses + contain the service DNS domain suffix. For + example, + `my-cluster-kafka-0.my-cluster-kafka-brokers.myproject.svc.cluster.local`. + + + The default is `.cluster.local`, but this is + customizable using the environment variable + `KUBERNETES_SERVICE_DNS_DOMAIN`. For + `internal` and `cluster-ip` listeners only. + maxConnections: + type: integer + description: >- + The maximum number of connections we allow for + this listener in the broker at any time. New + connections are blocked if the limit is + reached. + maxConnectionCreationRate: + type: integer + description: >- + The maximum connection creation rate we allow + in this listener at any time. New connections + will be throttled if the limit is reached. + preferredNodePortAddressType: + type: string + enum: + - ExternalIP + - ExternalDNS + - InternalIP + - InternalDNS + - Hostname + description: >- + Defines which address type should be used as + the node address. Available types are: + `ExternalDNS`, `ExternalIP`, `InternalDNS`, + `InternalIP` and `Hostname`. By default, the + addresses are used in the following order (the + first one found is used): + + + * `ExternalDNS` + + * `ExternalIP` + + * `InternalDNS` + + * `InternalIP` + + * `Hostname` + + + This property is used to select the preferred + address type, which is checked first. If no + address is found for this address type, the + other types are checked in the default order. + For `nodeport` listeners only. + publishNotReadyAddresses: + type: boolean + description: >- + Configures whether the service endpoints are + considered "ready" even if the Pods themselves + are not. Defaults to `false`. This field can + not be used with `internal` listeners. + hostTemplate: + type: string + description: >- + Configures the template for generating the + hostnames of the individual brokers. Valid + placeholders that you can use in the template + are `{nodeId}` and `{nodePodName}`. + advertisedHostTemplate: + type: string + description: >- + Configures the template for generating the + advertised hostnames of the individual + brokers. Valid placeholders that you can use + in the template are `{nodeId}` and + `{nodePodName}`. + advertisedPortTemplate: + type: string + description: >- + Configures the template for generating the + advertised ports of the individual brokers. It + allows to specify a simple mathematics formula + that will be used to calculate the port. The + only valid placeholder that you can use in the + template is `{nodeId}`. Supported operations + are `+`, `-`, and `*`. For example, `9000 + + {nodeId}` will generate ports `9000`, `9001`, + `9002`, and so on for the individual brokers. + You can also use a fixed port number in the + template, for example `9000`, which will + generate the same port for all brokers. + allocateLoadBalancerNodePorts: + type: boolean + description: >- + Configures whether to allocate NodePort + automatically for the `Service` with type + `LoadBalancer`. + + This is a one to one with the + `spec.allocateLoadBalancerNodePorts` + configuration in the `Service` type + + For `loadbalancer` listeners only. + description: Additional listener configuration. + networkPolicyPeers: + type: array + items: + type: object + properties: + ipBlock: + type: object + properties: + cidr: + type: string + except: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + podSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + description: >- + List of peers which should be able to connect to + this listener. Peers in this list are combined + using a logical OR operation. If this field is + empty or missing, all connections will be allowed + for this listener. If this field is present and + contains at least one item, the listener only + allows the traffic which matches at least one item + in this list. + required: + - name + - port + - type + - tls + description: Configures listeners to provide access to Kafka brokers. + config: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + Kafka broker config properties with certain prefixes + cannot be set unless it is in the exception list. + Consult the documentation for the list of forbidden + prefixes and exceptions. + authorization: + type: object + properties: + authorizerClass: + type: string + description: >- + Authorization implementation class, which must be + available in classpath. + superUsers: + type: array + items: + type: string + description: >- + List of super users, which are user principals with + unlimited access rights. + supportsAdminApi: + type: boolean + description: >- + Indicates whether the custom authorizer supports the + APIs for managing ACLs using the Kafka Admin API. + Defaults to `false`. + type: + type: string + enum: + - simple + - custom + description: >- + Authorization type. Currently, the supported types + are `simple`, `keycloak`, `opa` and `custom`. + `simple` authorization type uses Kafka's built-in + authorizer for authorization. `keycloak` + authorization type uses Keycloak Authorization + Services for authorization. `opa` authorization type + uses Open Policy Agent based authorization. `custom` + authorization type uses user-provided implementation + for authorization. `opa` (as of Strimzi 0.46.0) and + `keycloak` (as of Strimzi 0.49.0) types are + deprecated and will be removed in the `v1` API + version. Please use `custom` type instead. + required: + - type + description: Authorization configuration for Kafka brokers. + rack: + type: object + properties: + envVarName: + type: string + description: >- + The name of the environment variable that defines + the rack ID. Its value sets the `broker.rack` + configuration for Kafka brokers and the + `client.rack` configuration for Kafka Connect or + MirrorMaker 2. + topologyKey: + type: string + example: topology.kubernetes.io/zone + description: >- + A key that matches labels assigned to the Kubernetes + cluster nodes. The value of the label is used to set + a broker's `broker.rack` config, and the + `client.rack` config for Kafka Connect or + MirrorMaker 2. + type: + type: string + enum: + - topology-label + - environment-variable + description: >- + Specifies the rack awareness type. Supported types + are `topology-label` and `environment-variable`. + `topology-label` uses a Kubernetes worker node label + to set the `broker.rack` configuration for Kafka + brokers and the `client.rack` configuration for + Kafka Connect and MirrorMaker 2. + `environment-variable` uses an environment variable + to set the `broker.rack` configuration for Kafka + brokers and the `client.rack` configuration for + Kafka Connect and MirrorMaker 2. When not specified, + `topology-label` type is used by default. + description: Configuration of the `broker.rack` broker config. + x-kubernetes-validations: + - rule: >- + (has(self.type) && self.type != "topology-label") || + self.topologyKey != "" + message: topologyKey property is required + - rule: >- + has(self.type) == false || self.type != + "environment-variable" || self.envVarName != "" + message: envVarName property is required + brokerRackInitImage: + type: string + description: >- + The image of the init container used for initializing + the `broker.rack`. + livenessProbe: + type: object + properties: + initialDelaySeconds: + type: integer + minimum: 0 + description: >- + The initial delay before first the health is first + checked. Default to 15 seconds. Minimum value is 0. + timeoutSeconds: + type: integer + minimum: 1 + description: >- + The timeout for each attempted health check. Default + to 5 seconds. Minimum value is 1. + periodSeconds: + type: integer + minimum: 1 + description: >- + How often (in seconds) to perform the probe. Default + to 10 seconds. Minimum value is 1. + successThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive successes for the probe to be + considered successful after having failed. Defaults + to 1. Must be 1 for liveness. Minimum value is 1. + failureThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive failures for the probe to be + considered failed after having succeeded. Defaults + to 3. Minimum value is 1. + description: Pod liveness checking. + readinessProbe: + type: object + properties: + initialDelaySeconds: + type: integer + minimum: 0 + description: >- + The initial delay before first the health is first + checked. Default to 15 seconds. Minimum value is 0. + timeoutSeconds: + type: integer + minimum: 1 + description: >- + The timeout for each attempted health check. Default + to 5 seconds. Minimum value is 1. + periodSeconds: + type: integer + minimum: 1 + description: >- + How often (in seconds) to perform the probe. Default + to 10 seconds. Minimum value is 1. + successThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive successes for the probe to be + considered successful after having failed. Defaults + to 1. Must be 1 for liveness. Minimum value is 1. + failureThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive failures for the probe to be + considered failed after having succeeded. Defaults + to 3. Minimum value is 1. + description: Pod readiness checking. + jvmOptions: + type: object + properties: + '-XX': + additionalProperties: + type: string + type: object + description: A map of -XX options to the JVM. + '-Xmx': + type: string + pattern: '^[0-9]+[mMgG]?$' + description: '-Xmx option to to the JVM.' + '-Xms': + type: string + pattern: '^[0-9]+[mMgG]?$' + description: '-Xms option to to the JVM.' + gcLoggingEnabled: + type: boolean + description: >- + Specifies whether the Garbage Collection logging is + enabled. The default is false. + javaSystemProperties: + type: array + items: + type: object + properties: + name: + type: string + description: The system property name. + value: + type: string + description: The system property value. + description: >- + A map of additional system properties which will be + passed using the `-D` option to the JVM. + description: JVM Options for pods. + jmxOptions: + type: object + properties: + authentication: + type: object + properties: + type: + type: string + enum: + - password + description: >- + Authentication type. Currently the only + supported types are `password`.`password` type + creates a username and protected port with no + TLS. + required: + - type + description: >- + Authentication configuration for connecting to the + JMX port. + description: JMX Options for Kafka brokers. + metricsConfig: + type: object + properties: + type: + type: string + enum: + - jmxPrometheusExporter + - strimziMetricsReporter + description: >- + Metrics type. The supported types are + `jmxPrometheusExporter` and + `strimziMetricsReporter`. Type + `jmxPrometheusExporter` uses the Prometheus JMX + Exporter to expose Kafka JMX metrics in Prometheus + format through an HTTP endpoint. Type + `strimziMetricsReporter` uses the Strimzi Metrics + Reporter to directly expose Kafka metrics in + Prometheus format through an HTTP endpoint. + valueFrom: + type: object + properties: + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: >- + Reference to the key in the ConfigMap containing + the configuration. + description: >- + ConfigMap entry where the Prometheus JMX Exporter + configuration is stored. + values: + type: object + properties: + allowList: + type: array + items: + type: string + description: >- + A list of regex patterns to filter the metrics + to collect. Should contain at least one element. + description: >- + Configuration values for the Strimzi Metrics + Reporter. + required: + - type + description: Metrics configuration. + x-kubernetes-validations: + - rule: >- + self.type != 'jmxPrometheusExporter' || + has(self.valueFrom) + message: valueFrom property is required + logging: + type: object + properties: + loggers: + additionalProperties: + type: string + type: object + description: A Map from logger name to logger level. + type: + type: string + enum: + - inline + - external + description: 'Logging type, must be either ''inline'' or ''external''.' + valueFrom: + type: object + properties: + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: >- + Reference to the key in the ConfigMap containing + the configuration. + description: >- + `ConfigMap` entry where the logging configuration is + stored. + required: + - type + description: Logging configuration for Kafka. + template: + type: object + properties: + pod: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + imagePullSecrets: + type: array + items: + type: object + properties: + name: + type: string + description: >- + List of references to secrets in the same + namespace to use for pulling any of the images + used by this Pod. When the + `STRIMZI_IMAGE_PULL_SECRETS` environment + variable in Cluster Operator and the + `imagePullSecrets` option are specified, only + the `imagePullSecrets` variable is used and the + `STRIMZI_IMAGE_PULL_SECRETS` variable is + ignored. + securityContext: + type: object + properties: + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + fsGroup: + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + supplementalGroups: + type: array + items: + type: integer + supplementalGroupsPolicy: + type: string + sysctls: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: >- + Configures pod-level security attributes and + common container settings. + terminationGracePeriodSeconds: + type: integer + minimum: 0 + description: >- + The grace period is the duration in seconds + after the processes running in the pod are sent + a termination signal, and the time when the + processes are forcibly halted with a kill + signal. Set this value to longer than the + expected cleanup time for your process. Value + must be a non-negative integer. A zero value + indicates delete immediately. You might need to + increase the grace period for very large Kafka + clusters, so that the Kafka brokers have enough + time to transfer their work to another broker + before they are terminated. Defaults to 30 + seconds. + affinity: + type: object + properties: + nodeAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + preference: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: object + properties: + nodeSelectorTerms: + type: array + items: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + podAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + podAntiAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + description: The pod's affinity rules. + tolerations: + type: array + items: + type: object + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + description: The pod's tolerations. + topologySpreadConstraints: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + maxSkew: + type: integer + minDomains: + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + description: The pod's topology spread constraints. + priorityClassName: + type: string + description: >- + The name of the priority class used to assign + priority to the pods. + schedulerName: + type: string + description: >- + The name of the scheduler used to dispatch this + `Pod`. If not specified, the default scheduler + will be used. + hostAliases: + type: array + items: + type: object + properties: + hostnames: + type: array + items: + type: string + ip: + type: string + description: >- + The pod's HostAliases. HostAliases is an + optional list of hosts and IPs that will be + injected into the Pod's hosts file if specified. + dnsPolicy: + type: string + enum: + - ClusterFirst + - ClusterFirstWithHostNet + - Default + - None + description: >- + The pod's DNSPolicy. Defaults to `ClusterFirst`. + Valid values are `ClusterFirstWithHostNet`, + `ClusterFirst`, `Default` or `None`. + dnsConfig: + type: object + properties: + nameservers: + type: array + items: + type: string + options: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + searches: + type: array + items: + type: string + description: >- + The pod's DNSConfig. If specified, it will be + merged to the generated DNS configuration based + on the DNSPolicy. + enableServiceLinks: + type: boolean + description: >- + Indicates whether information about services + should be injected into Pod's environment + variables. + tmpDirSizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + Defines the total amount of pod memory allocated + for the temporary `EmptyDir` volume `/tmp`. + Specify the allocation in memory units, for + example, `100Mi` for 100 mebibytes. Default + value is `5Mi`. The `/tmp` volume is backed by + pod memory, not disk storage, so avoid setting a + high value as it consumes pod memory resources. + volumes: + type: array + items: + type: object + properties: + name: + type: string + description: Name to use for the volume. Required. + secret: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + optional: + type: boolean + secretName: + type: string + description: '`Secret` to use to populate the volume.' + configMap: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + name: + type: string + optional: + type: boolean + description: '`ConfigMap` to use to populate the volume.' + emptyDir: + type: object + properties: + medium: + type: string + enum: + - Memory + description: >- + Medium represents the type of storage + medium should back this volume. Valid + values are unset or `Memory`. When not + set, it will use the node's default + medium. + sizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + The total amount of local storage + required for this EmptyDir volume (for + example 1Gi). + description: '`EmptyDir` to use to populate the volume.' + persistentVolumeClaim: + type: object + properties: + claimName: + type: string + readOnly: + type: boolean + description: >- + `PersistentVolumeClaim` object to use to + populate the volume. + csi: + type: object + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + type: object + properties: + name: + type: string + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + description: >- + `CSIVolumeSource` object to use to + populate the volume. + image: + type: object + properties: + pullPolicy: + type: string + reference: + type: string + description: >- + `ImageVolumeSource` object to use to + populate the volume. + oneOf: + - properties: + secret: {} + configMap: {} + emptyDir: {} + persistentVolumeClaim: {} + csi: {} + image: {} + description: >- + Additional volumes that can be mounted to the + pod. + hostUsers: + type: boolean + description: >- + Use the host user namespace. Optional. Defaults + to `true`. When `true` or not set, the pod runs + in the host user namespace. This is required + when the pod needs features available only in + the host namespace, such as loading kernel + modules with `CAP_SYS_MODULE`.When set to + `false`, the pod runs in a new user namespace. + Setting `false` helps mitigate container + breakout vulnerabilities and allows containers + to run as `root` without granting `root` + privileges on the host. This property is + alpha-level in Kubernetes and is supported only + by Kubernetes clusters that enable the + `UserNamespacesSupport` feature. + description: Template for Kafka `Pods`. + bootstrapService: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + ipFamilyPolicy: + type: string + enum: + - SingleStack + - PreferDualStack + - RequireDualStack + description: >- + Specifies the IP Family Policy used by the + service. Available options are `SingleStack`, + `PreferDualStack` and `RequireDualStack`. + `SingleStack` is for a single IP family. + `PreferDualStack` is for two IP families on + dual-stack configured clusters or a single IP + family on single-stack clusters. + `RequireDualStack` fails unless there are two IP + families on dual-stack configured clusters. If + unspecified, Kubernetes will choose the default + value based on the service type. + ipFamilies: + type: array + items: + type: string + enum: + - IPv4 + - IPv6 + description: >- + Specifies the IP Families used by the service. + Available options are `IPv4` and `IPv6`. If + unspecified, Kubernetes will choose the default + value based on the `ipFamilyPolicy` setting. + description: Template for Kafka bootstrap `Service`. + brokersService: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + ipFamilyPolicy: + type: string + enum: + - SingleStack + - PreferDualStack + - RequireDualStack + description: >- + Specifies the IP Family Policy used by the + service. Available options are `SingleStack`, + `PreferDualStack` and `RequireDualStack`. + `SingleStack` is for a single IP family. + `PreferDualStack` is for two IP families on + dual-stack configured clusters or a single IP + family on single-stack clusters. + `RequireDualStack` fails unless there are two IP + families on dual-stack configured clusters. If + unspecified, Kubernetes will choose the default + value based on the service type. + ipFamilies: + type: array + items: + type: string + enum: + - IPv4 + - IPv6 + description: >- + Specifies the IP Families used by the service. + Available options are `IPv4` and `IPv6`. If + unspecified, Kubernetes will choose the default + value based on the `ipFamilyPolicy` setting. + description: Template for Kafka broker `Service`. + externalBootstrapService: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + description: Template for Kafka external bootstrap `Service`. + perPodService: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + description: >- + Template for Kafka per-pod `Services` used for + access from outside of Kubernetes. + externalBootstrapRoute: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + description: Template for Kafka external bootstrap `Route`. + perPodRoute: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + description: >- + Template for Kafka per-pod `Routes` used for access + from outside of OpenShift. + externalBootstrapIngress: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + description: Template for Kafka external bootstrap `Ingress`. + perPodIngress: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + description: >- + Template for Kafka per-pod `Ingress` used for access + from outside of Kubernetes. + persistentVolumeClaim: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + description: Template for all Kafka `PersistentVolumeClaims`. + podDisruptionBudget: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: >- + Metadata to apply to the + `PodDisruptionBudgetTemplate` resource. + maxUnavailable: + type: integer + minimum: 0 + description: >- + Maximum number of unavailable pods to allow + automatic Pod eviction. A Pod eviction is + allowed when the `maxUnavailable` number of pods + or fewer are unavailable after the eviction. + Setting this value to 0 prevents all voluntary + evictions, so the pods must be evicted manually. + Defaults to 1. + description: Template for Kafka `PodDisruptionBudget`. + kafkaContainer: + type: object + properties: + env: + type: array + items: + type: object + properties: + name: + type: string + description: The environment variable key. + value: + type: string + description: The environment variable value. + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a secret. + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a config map. + oneOf: + - properties: + secretKeyRef: {} + required: + - secretKeyRef + - properties: + configMapKeyRef: {} + required: + - configMapKeyRef + description: >- + Reference to the secret or config map + property to which the environment variable + is set. + oneOf: + - properties: + value: {} + required: + - value + - properties: + valueFrom: {} + required: + - valueFrom + description: >- + Environment variables which should be applied to + the container. + securityContext: + type: object + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + capabilities: + type: object + properties: + add: + type: array + items: + type: string + drop: + type: array + items: + type: string + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: Security context for the container. + volumeMounts: + type: array + items: + type: object + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + description: >- + Additional volume mounts which should be applied + to the container. + description: Template for the Kafka broker container. + initContainer: + type: object + properties: + env: + type: array + items: + type: object + properties: + name: + type: string + description: The environment variable key. + value: + type: string + description: The environment variable value. + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a secret. + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a config map. + oneOf: + - properties: + secretKeyRef: {} + required: + - secretKeyRef + - properties: + configMapKeyRef: {} + required: + - configMapKeyRef + description: >- + Reference to the secret or config map + property to which the environment variable + is set. + oneOf: + - properties: + value: {} + required: + - value + - properties: + valueFrom: {} + required: + - valueFrom + description: >- + Environment variables which should be applied to + the container. + securityContext: + type: object + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + capabilities: + type: object + properties: + add: + type: array + items: + type: string + drop: + type: array + items: + type: string + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: Security context for the container. + volumeMounts: + type: array + items: + type: object + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + description: >- + Additional volume mounts which should be applied + to the container. + description: Template for the Kafka init container. + clusterCaCert: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + description: >- + Template for Secret with Kafka Cluster certificate + public key. + serviceAccount: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + description: Template for the Kafka service account. + jmxSecret: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + description: >- + Template for Secret of the Kafka Cluster JMX + authentication. + clusterRoleBinding: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + description: Template for the Kafka ClusterRoleBinding. + podSet: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + description: Template for Kafka `StrimziPodSet` resource. + description: >- + Template for Kafka cluster resources. The template + allows users to specify how the Kubernetes resources are + generated. + tieredStorage: + type: object + properties: + remoteStorageManager: + type: object + properties: + className: + type: string + description: >- + The class name for the `RemoteStorageManager` + implementation. + classPath: + type: string + description: >- + The class path for the `RemoteStorageManager` + implementation. + config: + additionalProperties: + type: string + type: object + description: >- + The additional configuration map for the + `RemoteStorageManager` implementation. Keys will + be automatically prefixed with `rsm.config.`, + and added to Kafka broker configuration. + description: Configuration for the Remote Storage Manager. + type: + type: string + enum: + - custom + description: >- + Storage type, only 'custom' is supported at the + moment. + required: + - type + description: Configure the tiered storage feature for Kafka brokers. + quotas: + type: object + properties: + consumerByteRate: + type: integer + minimum: 0 + description: >- + A per-broker byte-rate quota for clients consuming + from a broker, independent of their number. If + clients consume at maximum speed, the quota is + shared equally between all non-excluded consumers. + Otherwise, the quota is divided based on each + client's consumption rate. + controllerMutationRate: + type: number + minimum: 0 + description: >- + The default client quota on the rate at which + mutations are accepted per second for create topic + requests, create partition requests, and delete + topic requests, defined for each broker. The + mutations rate is measured by the number of + partitions created or deleted. Applied on a + per-broker basis. + excludedPrincipals: + type: array + items: + type: string + description: >- + List of principals that are excluded from the quota. + The principals have to be prefixed with `User:`, for + example `User:my-user;User:CN=my-other-user`. + minAvailableBytesPerVolume: + type: integer + minimum: 0 + description: >- + Stop message production if the available size (in + bytes) of the storage is lower than or equal to this + specified value. This condition is mutually + exclusive with `minAvailableRatioPerVolume`. + minAvailableRatioPerVolume: + type: number + minimum: 0 + maximum: 1 + description: >- + Stop message production if the percentage of + available storage space falls below or equals the + specified ratio (set as a decimal representing a + percentage). This condition is mutually exclusive + with `minAvailableBytesPerVolume`. + producerByteRate: + type: integer + minimum: 0 + description: >- + A per-broker byte-rate quota for clients producing + to a broker, independent of their number. If clients + produce at maximum speed, the quota is shared + equally between all non-excluded producers. + Otherwise, the quota is divided based on each + client's production rate. + requestPercentage: + type: integer + minimum: 0 + description: >- + The default client quota limits the maximum CPU + utilization of each client as a percentage of the + network and I/O threads of each broker. Applied on a + per-broker basis. + type: + type: string + enum: + - kafka + - strimzi + description: >- + Quotas plugin type. Currently, the supported types + are `kafka` and `strimzi`. `kafka` quotas type uses + Kafka's built-in quotas plugin. `strimzi` quotas + type uses Strimzi quotas plugin. + required: + - type + description: >- + Quotas plugin configuration for Kafka brokers allows + setting quotas for disk usage, produce/fetch rates, and + more. Supported plugin types include `kafka` (default) + and `strimzi`. If not specified, the default `kafka` + quotas plugin is used. + required: + - listeners + description: Configuration of the Kafka cluster. + entityOperator: + type: object + properties: + topicOperator: + type: object + properties: + watchedNamespace: + type: string + description: The namespace the Topic Operator should watch. + image: + type: string + description: The image to use for the Topic Operator. + reconciliationIntervalMs: + type: integer + minimum: 0 + description: >- + Interval between periodic reconciliations in + milliseconds. + startupProbe: + type: object + properties: + initialDelaySeconds: + type: integer + minimum: 0 + description: >- + The initial delay before first the health is + first checked. Default to 15 seconds. Minimum + value is 0. + timeoutSeconds: + type: integer + minimum: 1 + description: >- + The timeout for each attempted health check. + Default to 5 seconds. Minimum value is 1. + periodSeconds: + type: integer + minimum: 1 + description: >- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + successThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive successes for the probe to + be considered successful after having failed. + Defaults to 1. Must be 1 for liveness. Minimum + value is 1. + failureThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive failures for the probe to be + considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + description: Pod startup checking. + livenessProbe: + type: object + properties: + initialDelaySeconds: + type: integer + minimum: 0 + description: >- + The initial delay before first the health is + first checked. Default to 15 seconds. Minimum + value is 0. + timeoutSeconds: + type: integer + minimum: 1 + description: >- + The timeout for each attempted health check. + Default to 5 seconds. Minimum value is 1. + periodSeconds: + type: integer + minimum: 1 + description: >- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + successThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive successes for the probe to + be considered successful after having failed. + Defaults to 1. Must be 1 for liveness. Minimum + value is 1. + failureThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive failures for the probe to be + considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + description: Pod liveness checking. + readinessProbe: + type: object + properties: + initialDelaySeconds: + type: integer + minimum: 0 + description: >- + The initial delay before first the health is + first checked. Default to 15 seconds. Minimum + value is 0. + timeoutSeconds: + type: integer + minimum: 1 + description: >- + The timeout for each attempted health check. + Default to 5 seconds. Minimum value is 1. + periodSeconds: + type: integer + minimum: 1 + description: >- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + successThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive successes for the probe to + be considered successful after having failed. + Defaults to 1. Must be 1 for liveness. Minimum + value is 1. + failureThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive failures for the probe to be + considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + description: Pod readiness checking. + resources: + type: object + properties: + claims: + type: array + items: + type: object + properties: + name: + type: string + request: + type: string + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: >- + ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: >- + ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + description: CPU and memory resources to reserve. + logging: + type: object + properties: + loggers: + additionalProperties: + type: string + type: object + description: A Map from logger name to logger level. + type: + type: string + enum: + - inline + - external + description: >- + Logging type, must be either 'inline' or + 'external'. + valueFrom: + type: object + properties: + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: >- + Reference to the key in the ConfigMap + containing the configuration. + description: >- + `ConfigMap` entry where the logging + configuration is stored. + required: + - type + description: Logging configuration. + jvmOptions: + type: object + properties: + '-XX': + additionalProperties: + type: string + type: object + description: A map of -XX options to the JVM. + '-Xmx': + type: string + pattern: '^[0-9]+[mMgG]?$' + description: '-Xmx option to to the JVM.' + '-Xms': + type: string + pattern: '^[0-9]+[mMgG]?$' + description: '-Xms option to to the JVM.' + gcLoggingEnabled: + type: boolean + description: >- + Specifies whether the Garbage Collection logging + is enabled. The default is false. + javaSystemProperties: + type: array + items: + type: object + properties: + name: + type: string + description: The system property name. + value: + type: string + description: The system property value. + description: >- + A map of additional system properties which will + be passed using the `-D` option to the JVM. + description: JVM Options for pods. + description: Configuration of the Topic Operator. + userOperator: + type: object + properties: + watchedNamespace: + type: string + description: The namespace the User Operator should watch. + image: + type: string + description: The image to use for the User Operator. + reconciliationIntervalMs: + type: integer + minimum: 0 + description: >- + Interval between periodic reconciliations in + milliseconds. + secretPrefix: + type: string + description: >- + The prefix that will be added to the KafkaUser name + to be used as the Secret name. + livenessProbe: + type: object + properties: + initialDelaySeconds: + type: integer + minimum: 0 + description: >- + The initial delay before first the health is + first checked. Default to 15 seconds. Minimum + value is 0. + timeoutSeconds: + type: integer + minimum: 1 + description: >- + The timeout for each attempted health check. + Default to 5 seconds. Minimum value is 1. + periodSeconds: + type: integer + minimum: 1 + description: >- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + successThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive successes for the probe to + be considered successful after having failed. + Defaults to 1. Must be 1 for liveness. Minimum + value is 1. + failureThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive failures for the probe to be + considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + description: Pod liveness checking. + readinessProbe: + type: object + properties: + initialDelaySeconds: + type: integer + minimum: 0 + description: >- + The initial delay before first the health is + first checked. Default to 15 seconds. Minimum + value is 0. + timeoutSeconds: + type: integer + minimum: 1 + description: >- + The timeout for each attempted health check. + Default to 5 seconds. Minimum value is 1. + periodSeconds: + type: integer + minimum: 1 + description: >- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + successThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive successes for the probe to + be considered successful after having failed. + Defaults to 1. Must be 1 for liveness. Minimum + value is 1. + failureThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive failures for the probe to be + considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + description: Pod readiness checking. + resources: + type: object + properties: + claims: + type: array + items: + type: object + properties: + name: + type: string + request: + type: string + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: >- + ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: >- + ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + description: CPU and memory resources to reserve. + logging: + type: object + properties: + loggers: + additionalProperties: + type: string + type: object + description: A Map from logger name to logger level. + type: + type: string + enum: + - inline + - external + description: >- + Logging type, must be either 'inline' or + 'external'. + valueFrom: + type: object + properties: + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: >- + Reference to the key in the ConfigMap + containing the configuration. + description: >- + `ConfigMap` entry where the logging + configuration is stored. + required: + - type + description: Logging configuration. + jvmOptions: + type: object + properties: + '-XX': + additionalProperties: + type: string + type: object + description: A map of -XX options to the JVM. + '-Xmx': + type: string + pattern: '^[0-9]+[mMgG]?$' + description: '-Xmx option to to the JVM.' + '-Xms': + type: string + pattern: '^[0-9]+[mMgG]?$' + description: '-Xms option to to the JVM.' + gcLoggingEnabled: + type: boolean + description: >- + Specifies whether the Garbage Collection logging + is enabled. The default is false. + javaSystemProperties: + type: array + items: + type: object + properties: + name: + type: string + description: The system property name. + value: + type: string + description: The system property value. + description: >- + A map of additional system properties which will + be passed using the `-D` option to the JVM. + description: JVM Options for pods. + description: Configuration of the User Operator. + template: + type: object + properties: + deployment: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + deploymentStrategy: + type: string + enum: + - RollingUpdate + - Recreate + description: >- + Pod replacement strategy for deployment + configuration changes. Valid values are + `RollingUpdate` and `Recreate`. Defaults to + `RollingUpdate`. + description: Template for Entity Operator `Deployment`. + pod: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + imagePullSecrets: + type: array + items: + type: object + properties: + name: + type: string + description: >- + List of references to secrets in the same + namespace to use for pulling any of the images + used by this Pod. When the + `STRIMZI_IMAGE_PULL_SECRETS` environment + variable in Cluster Operator and the + `imagePullSecrets` option are specified, only + the `imagePullSecrets` variable is used and the + `STRIMZI_IMAGE_PULL_SECRETS` variable is + ignored. + securityContext: + type: object + properties: + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + fsGroup: + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + supplementalGroups: + type: array + items: + type: integer + supplementalGroupsPolicy: + type: string + sysctls: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: >- + Configures pod-level security attributes and + common container settings. + terminationGracePeriodSeconds: + type: integer + minimum: 0 + description: >- + The grace period is the duration in seconds + after the processes running in the pod are sent + a termination signal, and the time when the + processes are forcibly halted with a kill + signal. Set this value to longer than the + expected cleanup time for your process. Value + must be a non-negative integer. A zero value + indicates delete immediately. You might need to + increase the grace period for very large Kafka + clusters, so that the Kafka brokers have enough + time to transfer their work to another broker + before they are terminated. Defaults to 30 + seconds. + affinity: + type: object + properties: + nodeAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + preference: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: object + properties: + nodeSelectorTerms: + type: array + items: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + podAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + podAntiAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + description: The pod's affinity rules. + tolerations: + type: array + items: + type: object + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + description: The pod's tolerations. + topologySpreadConstraints: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + maxSkew: + type: integer + minDomains: + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + description: The pod's topology spread constraints. + priorityClassName: + type: string + description: >- + The name of the priority class used to assign + priority to the pods. + schedulerName: + type: string + description: >- + The name of the scheduler used to dispatch this + `Pod`. If not specified, the default scheduler + will be used. + hostAliases: + type: array + items: + type: object + properties: + hostnames: + type: array + items: + type: string + ip: + type: string + description: >- + The pod's HostAliases. HostAliases is an + optional list of hosts and IPs that will be + injected into the Pod's hosts file if specified. + dnsPolicy: + type: string + enum: + - ClusterFirst + - ClusterFirstWithHostNet + - Default + - None + description: >- + The pod's DNSPolicy. Defaults to `ClusterFirst`. + Valid values are `ClusterFirstWithHostNet`, + `ClusterFirst`, `Default` or `None`. + dnsConfig: + type: object + properties: + nameservers: + type: array + items: + type: string + options: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + searches: + type: array + items: + type: string + description: >- + The pod's DNSConfig. If specified, it will be + merged to the generated DNS configuration based + on the DNSPolicy. + enableServiceLinks: + type: boolean + description: >- + Indicates whether information about services + should be injected into Pod's environment + variables. + tmpDirSizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + Defines the total amount of pod memory allocated + for the temporary `EmptyDir` volume `/tmp`. + Specify the allocation in memory units, for + example, `100Mi` for 100 mebibytes. Default + value is `5Mi`. The `/tmp` volume is backed by + pod memory, not disk storage, so avoid setting a + high value as it consumes pod memory resources. + volumes: + type: array + items: + type: object + properties: + name: + type: string + description: Name to use for the volume. Required. + secret: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + optional: + type: boolean + secretName: + type: string + description: '`Secret` to use to populate the volume.' + configMap: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + name: + type: string + optional: + type: boolean + description: '`ConfigMap` to use to populate the volume.' + emptyDir: + type: object + properties: + medium: + type: string + enum: + - Memory + description: >- + Medium represents the type of storage + medium should back this volume. Valid + values are unset or `Memory`. When not + set, it will use the node's default + medium. + sizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + The total amount of local storage + required for this EmptyDir volume (for + example 1Gi). + description: '`EmptyDir` to use to populate the volume.' + persistentVolumeClaim: + type: object + properties: + claimName: + type: string + readOnly: + type: boolean + description: >- + `PersistentVolumeClaim` object to use to + populate the volume. + csi: + type: object + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + type: object + properties: + name: + type: string + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + description: >- + `CSIVolumeSource` object to use to + populate the volume. + image: + type: object + properties: + pullPolicy: + type: string + reference: + type: string + description: >- + `ImageVolumeSource` object to use to + populate the volume. + oneOf: + - properties: + secret: {} + configMap: {} + emptyDir: {} + persistentVolumeClaim: {} + csi: {} + image: {} + description: >- + Additional volumes that can be mounted to the + pod. + hostUsers: + type: boolean + description: >- + Use the host user namespace. Optional. Defaults + to `true`. When `true` or not set, the pod runs + in the host user namespace. This is required + when the pod needs features available only in + the host namespace, such as loading kernel + modules with `CAP_SYS_MODULE`.When set to + `false`, the pod runs in a new user namespace. + Setting `false` helps mitigate container + breakout vulnerabilities and allows containers + to run as `root` without granting `root` + privileges on the host. This property is + alpha-level in Kubernetes and is supported only + by Kubernetes clusters that enable the + `UserNamespacesSupport` feature. + description: Template for Entity Operator `Pods`. + topicOperatorContainer: + type: object + properties: + env: + type: array + items: + type: object + properties: + name: + type: string + description: The environment variable key. + value: + type: string + description: The environment variable value. + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a secret. + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a config map. + oneOf: + - properties: + secretKeyRef: {} + required: + - secretKeyRef + - properties: + configMapKeyRef: {} + required: + - configMapKeyRef + description: >- + Reference to the secret or config map + property to which the environment variable + is set. + oneOf: + - properties: + value: {} + required: + - value + - properties: + valueFrom: {} + required: + - valueFrom + description: >- + Environment variables which should be applied to + the container. + securityContext: + type: object + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + capabilities: + type: object + properties: + add: + type: array + items: + type: string + drop: + type: array + items: + type: string + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: Security context for the container. + volumeMounts: + type: array + items: + type: object + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + description: >- + Additional volume mounts which should be applied + to the container. + description: Template for the Entity Topic Operator container. + userOperatorContainer: + type: object + properties: + env: + type: array + items: + type: object + properties: + name: + type: string + description: The environment variable key. + value: + type: string + description: The environment variable value. + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a secret. + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a config map. + oneOf: + - properties: + secretKeyRef: {} + required: + - secretKeyRef + - properties: + configMapKeyRef: {} + required: + - configMapKeyRef + description: >- + Reference to the secret or config map + property to which the environment variable + is set. + oneOf: + - properties: + value: {} + required: + - value + - properties: + valueFrom: {} + required: + - valueFrom + description: >- + Environment variables which should be applied to + the container. + securityContext: + type: object + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + capabilities: + type: object + properties: + add: + type: array + items: + type: string + drop: + type: array + items: + type: string + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: Security context for the container. + volumeMounts: + type: array + items: + type: object + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + description: >- + Additional volume mounts which should be applied + to the container. + description: Template for the Entity User Operator container. + serviceAccount: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + description: Template for the Entity Operator service account. + podDisruptionBudget: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: >- + Metadata to apply to the + `PodDisruptionBudgetTemplate` resource. + maxUnavailable: + type: integer + minimum: 0 + description: >- + Maximum number of unavailable pods to allow + automatic Pod eviction. A Pod eviction is + allowed when the `maxUnavailable` number of pods + or fewer are unavailable after the eviction. + Setting this value to 0 prevents all voluntary + evictions, so the pods must be evicted manually. + Defaults to 1. + description: >- + Template for the Entity Operator Pod Disruption + Budget. + entityOperatorRole: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + description: Template for the Entity Operator Role. + topicOperatorRoleBinding: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + description: Template for the Entity Topic Operator RoleBinding. + userOperatorRoleBinding: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + description: Template for the Entity Topic Operator RoleBinding. + description: >- + Template for Entity Operator resources. The template + allows users to specify how a `Deployment` and `Pod` is + generated. + description: Configuration of the Entity Operator. + clusterCa: + type: object + properties: + generateCertificateAuthority: + type: boolean + description: >- + If true then Certificate Authority certificates will be + generated automatically. Otherwise the user will need to + provide a Secret with the CA certificate. Default is + true. + generateSecretOwnerReference: + type: boolean + description: >- + If `true`, the Cluster and Client CA Secrets are + configured with the `ownerReference` set to the `Kafka` + resource. If the `Kafka` resource is deleted when + `true`, the CA Secrets are also deleted. If `false`, the + `ownerReference` is disabled. If the `Kafka` resource is + deleted when `false`, the CA Secrets are retained and + available for reuse. Default is `true`. + validityDays: + type: integer + minimum: 1 + description: >- + The number of days generated certificates should be + valid for. The default is 365. + renewalDays: + type: integer + minimum: 1 + description: >- + The number of days in the certificate renewal period. + This is the number of days before the a certificate + expires during which renewal actions may be performed. + When `generateCertificateAuthority` is true, this will + cause the generation of a new certificate. When + `generateCertificateAuthority` is true, this will cause + extra logging at WARN level about the pending + certificate expiry. Default is 30. + certificateExpirationPolicy: + type: string + enum: + - renew-certificate + - replace-key + description: >- + How should CA certificate expiration be handled when + `generateCertificateAuthority=true`. The default is for + a new CA certificate to be generated reusing the + existing private key. + description: Configuration of the cluster certificate authority. + clientsCa: + type: object + properties: + generateCertificateAuthority: + type: boolean + description: >- + If true then Certificate Authority certificates will be + generated automatically. Otherwise the user will need to + provide a Secret with the CA certificate. Default is + true. + generateSecretOwnerReference: + type: boolean + description: >- + If `true`, the Cluster and Client CA Secrets are + configured with the `ownerReference` set to the `Kafka` + resource. If the `Kafka` resource is deleted when + `true`, the CA Secrets are also deleted. If `false`, the + `ownerReference` is disabled. If the `Kafka` resource is + deleted when `false`, the CA Secrets are retained and + available for reuse. Default is `true`. + validityDays: + type: integer + minimum: 1 + description: >- + The number of days generated certificates should be + valid for. The default is 365. + renewalDays: + type: integer + minimum: 1 + description: >- + The number of days in the certificate renewal period. + This is the number of days before the a certificate + expires during which renewal actions may be performed. + When `generateCertificateAuthority` is true, this will + cause the generation of a new certificate. When + `generateCertificateAuthority` is true, this will cause + extra logging at WARN level about the pending + certificate expiry. Default is 30. + certificateExpirationPolicy: + type: string + enum: + - renew-certificate + - replace-key + description: >- + How should CA certificate expiration be handled when + `generateCertificateAuthority=true`. The default is for + a new CA certificate to be generated reusing the + existing private key. + description: Configuration of the clients certificate authority. + cruiseControl: + type: object + properties: + image: + type: string + description: >- + The container image used for Cruise Control pods. If no + image name is explicitly specified, the image name + corresponds to the name specified in the Cluster + Operator configuration. If an image name is not defined + in the Cluster Operator configuration, a default value + is used. + resources: + type: object + properties: + claims: + type: array + items: + type: object + properties: + name: + type: string + request: + type: string + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: >- + ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: >- + ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + description: >- + CPU and memory resources to reserve for the Cruise + Control container. + livenessProbe: + type: object + properties: + initialDelaySeconds: + type: integer + minimum: 0 + description: >- + The initial delay before first the health is first + checked. Default to 15 seconds. Minimum value is 0. + timeoutSeconds: + type: integer + minimum: 1 + description: >- + The timeout for each attempted health check. Default + to 5 seconds. Minimum value is 1. + periodSeconds: + type: integer + minimum: 1 + description: >- + How often (in seconds) to perform the probe. Default + to 10 seconds. Minimum value is 1. + successThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive successes for the probe to be + considered successful after having failed. Defaults + to 1. Must be 1 for liveness. Minimum value is 1. + failureThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive failures for the probe to be + considered failed after having succeeded. Defaults + to 3. Minimum value is 1. + description: Pod liveness checking for the Cruise Control container. + readinessProbe: + type: object + properties: + initialDelaySeconds: + type: integer + minimum: 0 + description: >- + The initial delay before first the health is first + checked. Default to 15 seconds. Minimum value is 0. + timeoutSeconds: + type: integer + minimum: 1 + description: >- + The timeout for each attempted health check. Default + to 5 seconds. Minimum value is 1. + periodSeconds: + type: integer + minimum: 1 + description: >- + How often (in seconds) to perform the probe. Default + to 10 seconds. Minimum value is 1. + successThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive successes for the probe to be + considered successful after having failed. Defaults + to 1. Must be 1 for liveness. Minimum value is 1. + failureThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive failures for the probe to be + considered failed after having succeeded. Defaults + to 3. Minimum value is 1. + description: Pod readiness checking for the Cruise Control container. + jvmOptions: + type: object + properties: + '-XX': + additionalProperties: + type: string + type: object + description: A map of -XX options to the JVM. + '-Xmx': + type: string + pattern: '^[0-9]+[mMgG]?$' + description: '-Xmx option to to the JVM.' + '-Xms': + type: string + pattern: '^[0-9]+[mMgG]?$' + description: '-Xms option to to the JVM.' + gcLoggingEnabled: + type: boolean + description: >- + Specifies whether the Garbage Collection logging is + enabled. The default is false. + javaSystemProperties: + type: array + items: + type: object + properties: + name: + type: string + description: The system property name. + value: + type: string + description: The system property value. + description: >- + A map of additional system properties which will be + passed using the `-D` option to the JVM. + description: JVM Options for the Cruise Control container. + logging: + type: object + properties: + loggers: + additionalProperties: + type: string + type: object + description: A Map from logger name to logger level. + type: + type: string + enum: + - inline + - external + description: 'Logging type, must be either ''inline'' or ''external''.' + valueFrom: + type: object + properties: + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: >- + Reference to the key in the ConfigMap containing + the configuration. + description: >- + `ConfigMap` entry where the logging configuration is + stored. + required: + - type + description: Logging configuration (Log4j 2) for Cruise Control. + template: + type: object + properties: + deployment: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + deploymentStrategy: + type: string + enum: + - RollingUpdate + - Recreate + description: >- + Pod replacement strategy for deployment + configuration changes. Valid values are + `RollingUpdate` and `Recreate`. Defaults to + `RollingUpdate`. + description: Template for Cruise Control `Deployment`. + pod: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + imagePullSecrets: + type: array + items: + type: object + properties: + name: + type: string + description: >- + List of references to secrets in the same + namespace to use for pulling any of the images + used by this Pod. When the + `STRIMZI_IMAGE_PULL_SECRETS` environment + variable in Cluster Operator and the + `imagePullSecrets` option are specified, only + the `imagePullSecrets` variable is used and the + `STRIMZI_IMAGE_PULL_SECRETS` variable is + ignored. + securityContext: + type: object + properties: + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + fsGroup: + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + supplementalGroups: + type: array + items: + type: integer + supplementalGroupsPolicy: + type: string + sysctls: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: >- + Configures pod-level security attributes and + common container settings. + terminationGracePeriodSeconds: + type: integer + minimum: 0 + description: >- + The grace period is the duration in seconds + after the processes running in the pod are sent + a termination signal, and the time when the + processes are forcibly halted with a kill + signal. Set this value to longer than the + expected cleanup time for your process. Value + must be a non-negative integer. A zero value + indicates delete immediately. You might need to + increase the grace period for very large Kafka + clusters, so that the Kafka brokers have enough + time to transfer their work to another broker + before they are terminated. Defaults to 30 + seconds. + affinity: + type: object + properties: + nodeAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + preference: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: object + properties: + nodeSelectorTerms: + type: array + items: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + podAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + podAntiAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + description: The pod's affinity rules. + tolerations: + type: array + items: + type: object + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + description: The pod's tolerations. + topologySpreadConstraints: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + maxSkew: + type: integer + minDomains: + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + description: The pod's topology spread constraints. + priorityClassName: + type: string + description: >- + The name of the priority class used to assign + priority to the pods. + schedulerName: + type: string + description: >- + The name of the scheduler used to dispatch this + `Pod`. If not specified, the default scheduler + will be used. + hostAliases: + type: array + items: + type: object + properties: + hostnames: + type: array + items: + type: string + ip: + type: string + description: >- + The pod's HostAliases. HostAliases is an + optional list of hosts and IPs that will be + injected into the Pod's hosts file if specified. + dnsPolicy: + type: string + enum: + - ClusterFirst + - ClusterFirstWithHostNet + - Default + - None + description: >- + The pod's DNSPolicy. Defaults to `ClusterFirst`. + Valid values are `ClusterFirstWithHostNet`, + `ClusterFirst`, `Default` or `None`. + dnsConfig: + type: object + properties: + nameservers: + type: array + items: + type: string + options: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + searches: + type: array + items: + type: string + description: >- + The pod's DNSConfig. If specified, it will be + merged to the generated DNS configuration based + on the DNSPolicy. + enableServiceLinks: + type: boolean + description: >- + Indicates whether information about services + should be injected into Pod's environment + variables. + tmpDirSizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + Defines the total amount of pod memory allocated + for the temporary `EmptyDir` volume `/tmp`. + Specify the allocation in memory units, for + example, `100Mi` for 100 mebibytes. Default + value is `5Mi`. The `/tmp` volume is backed by + pod memory, not disk storage, so avoid setting a + high value as it consumes pod memory resources. + volumes: + type: array + items: + type: object + properties: + name: + type: string + description: Name to use for the volume. Required. + secret: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + optional: + type: boolean + secretName: + type: string + description: '`Secret` to use to populate the volume.' + configMap: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + name: + type: string + optional: + type: boolean + description: '`ConfigMap` to use to populate the volume.' + emptyDir: + type: object + properties: + medium: + type: string + enum: + - Memory + description: >- + Medium represents the type of storage + medium should back this volume. Valid + values are unset or `Memory`. When not + set, it will use the node's default + medium. + sizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + The total amount of local storage + required for this EmptyDir volume (for + example 1Gi). + description: '`EmptyDir` to use to populate the volume.' + persistentVolumeClaim: + type: object + properties: + claimName: + type: string + readOnly: + type: boolean + description: >- + `PersistentVolumeClaim` object to use to + populate the volume. + csi: + type: object + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + type: object + properties: + name: + type: string + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + description: >- + `CSIVolumeSource` object to use to + populate the volume. + image: + type: object + properties: + pullPolicy: + type: string + reference: + type: string + description: >- + `ImageVolumeSource` object to use to + populate the volume. + oneOf: + - properties: + secret: {} + configMap: {} + emptyDir: {} + persistentVolumeClaim: {} + csi: {} + image: {} + description: >- + Additional volumes that can be mounted to the + pod. + hostUsers: + type: boolean + description: >- + Use the host user namespace. Optional. Defaults + to `true`. When `true` or not set, the pod runs + in the host user namespace. This is required + when the pod needs features available only in + the host namespace, such as loading kernel + modules with `CAP_SYS_MODULE`.When set to + `false`, the pod runs in a new user namespace. + Setting `false` helps mitigate container + breakout vulnerabilities and allows containers + to run as `root` without granting `root` + privileges on the host. This property is + alpha-level in Kubernetes and is supported only + by Kubernetes clusters that enable the + `UserNamespacesSupport` feature. + description: Template for Cruise Control `Pods`. + apiService: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + ipFamilyPolicy: + type: string + enum: + - SingleStack + - PreferDualStack + - RequireDualStack + description: >- + Specifies the IP Family Policy used by the + service. Available options are `SingleStack`, + `PreferDualStack` and `RequireDualStack`. + `SingleStack` is for a single IP family. + `PreferDualStack` is for two IP families on + dual-stack configured clusters or a single IP + family on single-stack clusters. + `RequireDualStack` fails unless there are two IP + families on dual-stack configured clusters. If + unspecified, Kubernetes will choose the default + value based on the service type. + ipFamilies: + type: array + items: + type: string + enum: + - IPv4 + - IPv6 + description: >- + Specifies the IP Families used by the service. + Available options are `IPv4` and `IPv6`. If + unspecified, Kubernetes will choose the default + value based on the `ipFamilyPolicy` setting. + description: Template for Cruise Control API `Service`. + podDisruptionBudget: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: >- + Metadata to apply to the + `PodDisruptionBudgetTemplate` resource. + maxUnavailable: + type: integer + minimum: 0 + description: >- + Maximum number of unavailable pods to allow + automatic Pod eviction. A Pod eviction is + allowed when the `maxUnavailable` number of pods + or fewer are unavailable after the eviction. + Setting this value to 0 prevents all voluntary + evictions, so the pods must be evicted manually. + Defaults to 1. + description: Template for Cruise Control `PodDisruptionBudget`. + cruiseControlContainer: + type: object + properties: + env: + type: array + items: + type: object + properties: + name: + type: string + description: The environment variable key. + value: + type: string + description: The environment variable value. + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a secret. + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a config map. + oneOf: + - properties: + secretKeyRef: {} + required: + - secretKeyRef + - properties: + configMapKeyRef: {} + required: + - configMapKeyRef + description: >- + Reference to the secret or config map + property to which the environment variable + is set. + oneOf: + - properties: + value: {} + required: + - value + - properties: + valueFrom: {} + required: + - valueFrom + description: >- + Environment variables which should be applied to + the container. + securityContext: + type: object + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + capabilities: + type: object + properties: + add: + type: array + items: + type: string + drop: + type: array + items: + type: string + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: Security context for the container. + volumeMounts: + type: array + items: + type: object + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + description: >- + Additional volume mounts which should be applied + to the container. + description: Template for the Cruise Control container. + serviceAccount: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + description: Template for the Cruise Control service account. + description: >- + Template to specify how Cruise Control resources, + `Deployments` and `Pods`, are generated. + brokerCapacity: + type: object + properties: + cpu: + type: string + pattern: '^[0-9]+([.][0-9]{0,3}|[m]?)$' + description: >- + Broker capacity for CPU resource in cores or + millicores. For example, 1, 1.500, 1500m. For more + information on valid CPU resource units see + https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-cpu. + inboundNetwork: + type: string + pattern: '^[0-9]+([KMG]i?)?B/s$' + description: >- + Broker capacity for inbound network throughput in + bytes per second. Use an integer value with standard + Kubernetes byte units (K, M, G) or their bibyte + (power of two) equivalents (Ki, Mi, Gi) per second. + For example, 10000KiB/s. + outboundNetwork: + type: string + pattern: '^[0-9]+([KMG]i?)?B/s$' + description: >- + Broker capacity for outbound network throughput in + bytes per second. Use an integer value with standard + Kubernetes byte units (K, M, G) or their bibyte + (power of two) equivalents (Ki, Mi, Gi) per second. + For example, 10000KiB/s. + overrides: + type: array + items: + type: object + properties: + brokers: + type: array + items: + type: integer + description: List of Kafka brokers (broker identifiers). + cpu: + type: string + pattern: '^[0-9]+([.][0-9]{0,3}|[m]?)$' + description: >- + Broker capacity for CPU resource in cores or + millicores. For example, 1, 1.500, 1500m. For + more information on valid CPU resource units + see + https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-cpu. + inboundNetwork: + type: string + pattern: '^[0-9]+([KMG]i?)?B/s$' + description: >- + Broker capacity for inbound network throughput + in bytes per second. Use an integer value with + standard Kubernetes byte units (K, M, G) or + their bibyte (power of two) equivalents (Ki, + Mi, Gi) per second. For example, 10000KiB/s. + outboundNetwork: + type: string + pattern: '^[0-9]+([KMG]i?)?B/s$' + description: >- + Broker capacity for outbound network + throughput in bytes per second. Use an integer + value with standard Kubernetes byte units (K, + M, G) or their bibyte (power of two) + equivalents (Ki, Mi, Gi) per second. For + example, 10000KiB/s. + required: + - brokers + description: >- + Overrides for individual brokers. The `overrides` + property lets you specify a different capacity + configuration for different brokers. + description: The Cruise Control `brokerCapacity` configuration. + config: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + The Cruise Control configuration. For a full list of + configuration options refer to + https://github.com/linkedin/cruise-control/wiki/Configurations. + Note that properties with the following prefixes cannot + be set: bootstrap.servers, client.id, zookeeper., + network., security., + failed.brokers.zk.path,webserver.http., + webserver.api.urlprefix, webserver.session.path, + webserver.accesslog., two.step., + request.reason.required,metric.reporter.sampler.bootstrap.servers, + capacity.config.file, self.healing., ssl., + kafka.broker.failure.detection.enable, + topic.config.provider.class (with the exception of: + ssl.cipher.suites, ssl.protocol, ssl.enabled.protocols, + webserver.http.cors.enabled, webserver.http.cors.origin, + webserver.http.cors.exposeheaders, + webserver.security.enable, webserver.ssl.enable). + metricsConfig: + type: object + properties: + type: + type: string + enum: + - jmxPrometheusExporter + - strimziMetricsReporter + description: >- + Metrics type. The supported types are + `jmxPrometheusExporter` and + `strimziMetricsReporter`. Type + `jmxPrometheusExporter` uses the Prometheus JMX + Exporter to expose Kafka JMX metrics in Prometheus + format through an HTTP endpoint. Type + `strimziMetricsReporter` uses the Strimzi Metrics + Reporter to directly expose Kafka metrics in + Prometheus format through an HTTP endpoint. + valueFrom: + type: object + properties: + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: >- + Reference to the key in the ConfigMap containing + the configuration. + description: >- + ConfigMap entry where the Prometheus JMX Exporter + configuration is stored. + values: + type: object + properties: + allowList: + type: array + items: + type: string + description: >- + A list of regex patterns to filter the metrics + to collect. Should contain at least one element. + description: >- + Configuration values for the Strimzi Metrics + Reporter. + required: + - type + description: >- + Metrics configuration. Only `jmxPrometheusExporter` can + be configured, as this component does not support + `strimziMetricsReporter`. + x-kubernetes-validations: + - rule: >- + self.type != 'jmxPrometheusExporter' || + has(self.valueFrom) + message: valueFrom property is required + - rule: self.type != 'strimziMetricsReporter' + message: value type not supported + apiUsers: + type: object + properties: + type: + type: string + enum: + - hashLoginService + description: >- + Type of the Cruise Control API users configuration. + Supported format is: `hashLoginService`. + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: >- + Selects a key of a Secret in the resource's + namespace. + description: >- + Secret from which the custom Cruise Control API + authentication credentials are read. + required: + - type + - valueFrom + description: Configuration of the Cruise Control REST API users. + autoRebalance: + type: array + minItems: 1 + items: + type: object + properties: + mode: + type: string + enum: + - add-brokers + - remove-brokers + description: > + Specifies the mode for automatically rebalancing + when brokers are added or removed. Supported modes + are `add-brokers` and `remove-brokers`. + template: + type: object + properties: + name: + type: string + description: >- + Reference to the KafkaRebalance custom resource to + be used as the configuration template for the + auto-rebalancing on scaling when running for the + corresponding mode. + required: + - mode + description: >- + Auto-rebalancing on scaling related configuration + listing the modes, when brokers are added or removed, + with the corresponding rebalance template + configurations.If this field is set, at least one mode + has to be defined. + description: >- + Configuration for Cruise Control deployment. Deploys a + Cruise Control instance when specified. + kafkaExporter: + type: object + properties: + image: + type: string + description: >- + The container image used for the Kafka Exporter pods. If + no image name is explicitly specified, the image name + corresponds to the version specified in the Cluster + Operator configuration. If an image name is not defined + in the Cluster Operator configuration, a default value + is used. + groupRegex: + type: string + description: >- + Regular expression to specify which consumer groups to + collect. Default value is `.*`. + topicRegex: + type: string + description: >- + Regular expression to specify which topics to collect. + Default value is `.*`. + groupExcludeRegex: + type: string + description: >- + Regular expression to specify which consumer groups to + exclude. + topicExcludeRegex: + type: string + description: Regular expression to specify which topics to exclude. + resources: + type: object + properties: + claims: + type: array + items: + type: object + properties: + name: + type: string + request: + type: string + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: >- + ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: >- + ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + description: CPU and memory resources to reserve. + logging: + type: string + description: >- + Only log messages with the given severity or above. + Valid levels: [`info`, `debug`, `trace`]. Default log + level is `info`. + livenessProbe: + type: object + properties: + initialDelaySeconds: + type: integer + minimum: 0 + description: >- + The initial delay before first the health is first + checked. Default to 15 seconds. Minimum value is 0. + timeoutSeconds: + type: integer + minimum: 1 + description: >- + The timeout for each attempted health check. Default + to 5 seconds. Minimum value is 1. + periodSeconds: + type: integer + minimum: 1 + description: >- + How often (in seconds) to perform the probe. Default + to 10 seconds. Minimum value is 1. + successThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive successes for the probe to be + considered successful after having failed. Defaults + to 1. Must be 1 for liveness. Minimum value is 1. + failureThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive failures for the probe to be + considered failed after having succeeded. Defaults + to 3. Minimum value is 1. + description: Pod liveness check. + readinessProbe: + type: object + properties: + initialDelaySeconds: + type: integer + minimum: 0 + description: >- + The initial delay before first the health is first + checked. Default to 15 seconds. Minimum value is 0. + timeoutSeconds: + type: integer + minimum: 1 + description: >- + The timeout for each attempted health check. Default + to 5 seconds. Minimum value is 1. + periodSeconds: + type: integer + minimum: 1 + description: >- + How often (in seconds) to perform the probe. Default + to 10 seconds. Minimum value is 1. + successThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive successes for the probe to be + considered successful after having failed. Defaults + to 1. Must be 1 for liveness. Minimum value is 1. + failureThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive failures for the probe to be + considered failed after having succeeded. Defaults + to 3. Minimum value is 1. + description: Pod readiness check. + enableSaramaLogging: + type: boolean + description: >- + Enable Sarama logging, a Go client library used by the + Kafka Exporter. + showAllOffsets: + type: boolean + description: >- + Whether show the offset/lag for all consumer group, + otherwise, only show connected consumer groups. + template: + type: object + properties: + deployment: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + deploymentStrategy: + type: string + enum: + - RollingUpdate + - Recreate + description: >- + Pod replacement strategy for deployment + configuration changes. Valid values are + `RollingUpdate` and `Recreate`. Defaults to + `RollingUpdate`. + description: Template for Kafka Exporter `Deployment`. + pod: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + imagePullSecrets: + type: array + items: + type: object + properties: + name: + type: string + description: >- + List of references to secrets in the same + namespace to use for pulling any of the images + used by this Pod. When the + `STRIMZI_IMAGE_PULL_SECRETS` environment + variable in Cluster Operator and the + `imagePullSecrets` option are specified, only + the `imagePullSecrets` variable is used and the + `STRIMZI_IMAGE_PULL_SECRETS` variable is + ignored. + securityContext: + type: object + properties: + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + fsGroup: + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + supplementalGroups: + type: array + items: + type: integer + supplementalGroupsPolicy: + type: string + sysctls: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: >- + Configures pod-level security attributes and + common container settings. + terminationGracePeriodSeconds: + type: integer + minimum: 0 + description: >- + The grace period is the duration in seconds + after the processes running in the pod are sent + a termination signal, and the time when the + processes are forcibly halted with a kill + signal. Set this value to longer than the + expected cleanup time for your process. Value + must be a non-negative integer. A zero value + indicates delete immediately. You might need to + increase the grace period for very large Kafka + clusters, so that the Kafka brokers have enough + time to transfer their work to another broker + before they are terminated. Defaults to 30 + seconds. + affinity: + type: object + properties: + nodeAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + preference: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: object + properties: + nodeSelectorTerms: + type: array + items: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + podAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + podAntiAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + description: The pod's affinity rules. + tolerations: + type: array + items: + type: object + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + description: The pod's tolerations. + topologySpreadConstraints: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + maxSkew: + type: integer + minDomains: + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + description: The pod's topology spread constraints. + priorityClassName: + type: string + description: >- + The name of the priority class used to assign + priority to the pods. + schedulerName: + type: string + description: >- + The name of the scheduler used to dispatch this + `Pod`. If not specified, the default scheduler + will be used. + hostAliases: + type: array + items: + type: object + properties: + hostnames: + type: array + items: + type: string + ip: + type: string + description: >- + The pod's HostAliases. HostAliases is an + optional list of hosts and IPs that will be + injected into the Pod's hosts file if specified. + dnsPolicy: + type: string + enum: + - ClusterFirst + - ClusterFirstWithHostNet + - Default + - None + description: >- + The pod's DNSPolicy. Defaults to `ClusterFirst`. + Valid values are `ClusterFirstWithHostNet`, + `ClusterFirst`, `Default` or `None`. + dnsConfig: + type: object + properties: + nameservers: + type: array + items: + type: string + options: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + searches: + type: array + items: + type: string + description: >- + The pod's DNSConfig. If specified, it will be + merged to the generated DNS configuration based + on the DNSPolicy. + enableServiceLinks: + type: boolean + description: >- + Indicates whether information about services + should be injected into Pod's environment + variables. + tmpDirSizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + Defines the total amount of pod memory allocated + for the temporary `EmptyDir` volume `/tmp`. + Specify the allocation in memory units, for + example, `100Mi` for 100 mebibytes. Default + value is `5Mi`. The `/tmp` volume is backed by + pod memory, not disk storage, so avoid setting a + high value as it consumes pod memory resources. + volumes: + type: array + items: + type: object + properties: + name: + type: string + description: Name to use for the volume. Required. + secret: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + optional: + type: boolean + secretName: + type: string + description: '`Secret` to use to populate the volume.' + configMap: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + name: + type: string + optional: + type: boolean + description: '`ConfigMap` to use to populate the volume.' + emptyDir: + type: object + properties: + medium: + type: string + enum: + - Memory + description: >- + Medium represents the type of storage + medium should back this volume. Valid + values are unset or `Memory`. When not + set, it will use the node's default + medium. + sizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + The total amount of local storage + required for this EmptyDir volume (for + example 1Gi). + description: '`EmptyDir` to use to populate the volume.' + persistentVolumeClaim: + type: object + properties: + claimName: + type: string + readOnly: + type: boolean + description: >- + `PersistentVolumeClaim` object to use to + populate the volume. + csi: + type: object + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + type: object + properties: + name: + type: string + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + description: >- + `CSIVolumeSource` object to use to + populate the volume. + image: + type: object + properties: + pullPolicy: + type: string + reference: + type: string + description: >- + `ImageVolumeSource` object to use to + populate the volume. + oneOf: + - properties: + secret: {} + configMap: {} + emptyDir: {} + persistentVolumeClaim: {} + csi: {} + image: {} + description: >- + Additional volumes that can be mounted to the + pod. + hostUsers: + type: boolean + description: >- + Use the host user namespace. Optional. Defaults + to `true`. When `true` or not set, the pod runs + in the host user namespace. This is required + when the pod needs features available only in + the host namespace, such as loading kernel + modules with `CAP_SYS_MODULE`.When set to + `false`, the pod runs in a new user namespace. + Setting `false` helps mitigate container + breakout vulnerabilities and allows containers + to run as `root` without granting `root` + privileges on the host. This property is + alpha-level in Kubernetes and is supported only + by Kubernetes clusters that enable the + `UserNamespacesSupport` feature. + description: Template for Kafka Exporter `Pods`. + container: + type: object + properties: + env: + type: array + items: + type: object + properties: + name: + type: string + description: The environment variable key. + value: + type: string + description: The environment variable value. + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a secret. + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a config map. + oneOf: + - properties: + secretKeyRef: {} + required: + - secretKeyRef + - properties: + configMapKeyRef: {} + required: + - configMapKeyRef + description: >- + Reference to the secret or config map + property to which the environment variable + is set. + oneOf: + - properties: + value: {} + required: + - value + - properties: + valueFrom: {} + required: + - valueFrom + description: >- + Environment variables which should be applied to + the container. + securityContext: + type: object + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + capabilities: + type: object + properties: + add: + type: array + items: + type: string + drop: + type: array + items: + type: string + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: Security context for the container. + volumeMounts: + type: array + items: + type: object + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + description: >- + Additional volume mounts which should be applied + to the container. + description: Template for the Kafka Exporter container. + serviceAccount: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: Metadata applied to the resource. + description: Template for the Kafka Exporter service account. + podDisruptionBudget: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: >- + Annotations added to the Kubernetes + resource. + description: >- + Metadata to apply to the + `PodDisruptionBudgetTemplate` resource. + maxUnavailable: + type: integer + minimum: 0 + description: >- + Maximum number of unavailable pods to allow + automatic Pod eviction. A Pod eviction is + allowed when the `maxUnavailable` number of pods + or fewer are unavailable after the eviction. + Setting this value to 0 prevents all voluntary + evictions, so the pods must be evicted manually. + Defaults to 1. + description: >- + Template for the Pod Disruption Budget for Kafka + Exporter pods. + description: Customization of deployment templates and pods. + description: >- + Configuration of the Kafka Exporter. Kafka Exporter can + provide additional metrics, for example lag of consumer + group at topic/partition. + maintenanceTimeWindows: + type: array + items: + type: string + description: >- + A list of time windows for maintenance tasks (that is, + certificates renewal). Each time window is defined by a cron + expression. + required: + - kafka + description: The specification of the Kafka cluster. + status: + type: object + properties: + conditions: + type: array + items: + type: object + properties: + type: + type: string + description: >- + The unique identifier of a condition, used to + distinguish between other conditions in the resource. + status: + type: string + description: >- + The status of the condition, either True, False or + Unknown. + lastTransitionTime: + type: string + description: >- + Last time the condition of a type changed from one + status to another. The required format is + 'yyyy-MM-ddTHH:mm:ssZ', in the UTC time zone. + reason: + type: string + description: >- + The reason for the condition's last transition (a + single word in CamelCase). + message: + type: string + description: >- + Human-readable message indicating details about the + condition's last transition. + description: List of status conditions. + observedGeneration: + type: integer + description: >- + The generation of the CRD that was last reconciled by the + operator. + listeners: + type: array + items: + type: object + properties: + name: + type: string + description: The name of the listener. + addresses: + type: array + items: + type: object + properties: + host: + type: string + description: >- + The DNS name or IP address of the Kafka + bootstrap service. + port: + type: integer + description: The port of the Kafka bootstrap service. + description: A list of the addresses for this listener. + bootstrapServers: + type: string + description: >- + A comma-separated list of `host:port` pairs for + connecting to the Kafka cluster using this listener. + certificates: + type: array + items: + type: string + description: >- + A list of TLS certificates which can be used to verify + the identity of the server when connecting to the + given listener. Set only for `tls` and `external` + listeners. + description: Addresses of the internal and external listeners. + kafkaNodePools: + type: array + items: + type: object + properties: + name: + type: string + description: >- + The name of the KafkaNodePool used by this Kafka + resource. + description: List of the KafkaNodePools used by this Kafka cluster. + clusterId: + type: string + description: Kafka cluster Id. + operatorLastSuccessfulVersion: + type: string + description: >- + The version of the Strimzi Cluster Operator which performed + the last successful reconciliation. + kafkaVersion: + type: string + description: The version of Kafka currently deployed in the cluster. + kafkaMetadataVersion: + type: string + description: >- + The KRaft metadata.version currently used by the Kafka + cluster. + autoRebalance: + type: object + properties: + state: + type: string + enum: + - Idle + - RebalanceOnScaleDown + - RebalanceOnScaleUp + description: >- + The current state of an auto-rebalancing operation. + Possible values are: + + + * `Idle` as the initial state when an auto-rebalancing + is requested or as final state when it completes or + fails. + + * `RebalanceOnScaleDown` if an auto-rebalance related to + a scale-down operation is running. + + * `RebalanceOnScaleUp` if an auto-rebalance related to a + scale-up operation is running. + lastTransitionTime: + type: string + description: >- + The timestamp of the latest auto-rebalancing state + update. + modes: + type: array + items: + type: object + properties: + mode: + type: string + enum: + - add-brokers + - remove-brokers + description: >- + Mode for which there is an auto-rebalancing + operation in progress or queued, when brokers are + added or removed. The possible modes are + `add-brokers` and `remove-brokers`. + brokers: + type: array + items: + type: integer + description: > + List of broker IDs involved in an auto-rebalancing + operation related to the current mode. + + The list contains one of the following: + + + * Broker IDs for a current auto-rebalance. + + * Broker IDs for a queued auto-rebalance (if a + previous auto-rebalance is still in progress). + description: >- + List of modes where an auto-rebalancing operation is + either running or queued. + + Each mode entry (`add-brokers` or `remove-brokers`) + includes one of the following: + + + * Broker IDs for a current auto-rebalance. + + * Broker IDs for a queued auto-rebalance (if a previous + rebalance is still in progress). + description: >- + The status of an auto-rebalancing triggered by a cluster + scaling request. + description: The status of the Kafka cluster. + required: + - spec + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: kafkanodepools.kafka.strimzi.io + labels: + app: strimzi + strimzi.io/crd-install: 'true' +spec: + group: kafka.strimzi.io + names: + kind: KafkaNodePool + listKind: KafkaNodePoolList + singular: kafkanodepool + plural: kafkanodepools + shortNames: + - knp + categories: + - strimzi + scope: Namespaced + conversion: + strategy: None + versions: + - name: v1 + served: true + storage: true + subresources: + status: {} + scale: + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + labelSelectorPath: .status.labelSelector + additionalPrinterColumns: + - name: Desired replicas + description: The desired number of replicas + jsonPath: .spec.replicas + type: integer + - name: Roles + description: Roles of the nodes in the pool + jsonPath: .status.roles + type: string + - name: NodeIds + description: Node IDs used by Kafka nodes in this pool + jsonPath: .status.nodeIds + type: string + schema: + openAPIV3Schema: + type: object + properties: + apiVersion: + type: string + description: >- + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the + latest internal value, and may reject unrecognized values. More + info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + kind: + type: string + description: >- + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the + client submits requests to. Cannot be updated. In CamelCase. + More info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + metadata: + type: object + spec: + type: object + properties: + replicas: + type: integer + minimum: 0 + description: The number of pods in the pool. + storage: + type: object + properties: + class: + type: string + description: The storage class to use for dynamic volume allocation. + deleteClaim: + type: boolean + description: >- + Specifies whether the persistent volume claim is deleted + when a Kafka node is deleted. Optional. Defaults to + `false`. + id: + type: integer + minimum: 0 + description: >- + Storage identification number. It is mandatory only for + storage volumes defined in a storage of type 'jbod'. + kraftMetadata: + type: string + enum: + - shared + description: >- + Specifies whether this volume should be used for storing + KRaft metadata. This property is optional. When set, the + only currently supported value is `shared`. At most one + volume can have this property set. + selector: + additionalProperties: + type: string + type: object + description: >- + Specifies a specific persistent volume to use. It + contains key:value pairs representing labels for + selecting such a volume. + size: + type: string + description: >- + When `type=persistent-claim`, defines the size of the + persistent volume claim, such as 100Gi. Mandatory when + `type=persistent-claim`. + sizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + When type=ephemeral, defines the total amount of local + storage required for this EmptyDir volume (for example + 1Gi). + type: + type: string + enum: + - ephemeral + - persistent-claim + - jbod + description: >- + Storage type, must be either 'ephemeral', + 'persistent-claim', or 'jbod'. + volumeAttributesClass: + type: string + description: >- + Specifies `VolumeAttributeClass` name for dynamically + configuring storage attributes. + volumes: + type: array + items: + type: object + properties: + class: + type: string + description: >- + The storage class to use for dynamic volume + allocation. + deleteClaim: + type: boolean + description: >- + Specifies whether the persistent volume claim is + deleted when a Kafka node is deleted. Optional. + Defaults to `false`. + id: + type: integer + minimum: 0 + description: >- + Storage identification number. Mandatory for + storage volumes defined with a `jbod` storage type + configuration. + kraftMetadata: + type: string + enum: + - shared + description: >- + Specifies whether this volume should be used for + storing KRaft metadata. This property is optional. + When set, the only currently supported value is + `shared`. At most one volume can have this + property set. + selector: + additionalProperties: + type: string + type: object + description: >- + Specifies a specific persistent volume to use. It + contains key:value pairs representing labels for + selecting such a volume. + size: + type: string + description: >- + When `type=persistent-claim`, defines the size of + the persistent volume claim, such as 100Gi. + Mandatory when `type=persistent-claim`. + sizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + When type=ephemeral, defines the total amount of + local storage required for this EmptyDir volume + (for example 1Gi). + type: + type: string + enum: + - ephemeral + - persistent-claim + description: >- + Storage type, must be either 'ephemeral' or + 'persistent-claim'. + volumeAttributesClass: + type: string + description: >- + Specifies `VolumeAttributeClass` name for + dynamically configuring storage attributes. + required: + - type + description: >- + List of volumes as Storage objects representing the JBOD + disks array. + required: + - type + description: Storage configuration (disk). Cannot be updated. + roles: + type: array + items: + type: string + enum: + - controller + - broker + description: >- + The roles assigned to the node pool. Supported values are + `broker` and `controller`. This property is required. + resources: + type: object + properties: + claims: + type: array + items: + type: object + properties: + name: + type: string + request: + type: string + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: >- + ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: >- + ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + description: CPU and memory resources to reserve. + jvmOptions: + type: object + properties: + '-XX': + additionalProperties: + type: string + type: object + description: A map of -XX options to the JVM. + '-Xmx': + type: string + pattern: '^[0-9]+[mMgG]?$' + description: '-Xmx option to to the JVM.' + '-Xms': + type: string + pattern: '^[0-9]+[mMgG]?$' + description: '-Xms option to to the JVM.' + gcLoggingEnabled: + type: boolean + description: >- + Specifies whether the Garbage Collection logging is + enabled. The default is false. + javaSystemProperties: + type: array + items: + type: object + properties: + name: + type: string + description: The system property name. + value: + type: string + description: The system property value. + description: >- + A map of additional system properties which will be + passed using the `-D` option to the JVM. + description: JVM Options for pods. + template: + type: object + properties: + podSet: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + description: Template for Kafka `StrimziPodSet` resource. + pod: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + imagePullSecrets: + type: array + items: + type: object + properties: + name: + type: string + description: >- + List of references to secrets in the same namespace + to use for pulling any of the images used by this + Pod. When the `STRIMZI_IMAGE_PULL_SECRETS` + environment variable in Cluster Operator and the + `imagePullSecrets` option are specified, only the + `imagePullSecrets` variable is used and the + `STRIMZI_IMAGE_PULL_SECRETS` variable is ignored. + securityContext: + type: object + properties: + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + fsGroup: + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + supplementalGroups: + type: array + items: + type: integer + supplementalGroupsPolicy: + type: string + sysctls: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: >- + Configures pod-level security attributes and common + container settings. + terminationGracePeriodSeconds: + type: integer + minimum: 0 + description: >- + The grace period is the duration in seconds after + the processes running in the pod are sent a + termination signal, and the time when the processes + are forcibly halted with a kill signal. Set this + value to longer than the expected cleanup time for + your process. Value must be a non-negative integer. + A zero value indicates delete immediately. You might + need to increase the grace period for very large + Kafka clusters, so that the Kafka brokers have + enough time to transfer their work to another broker + before they are terminated. Defaults to 30 seconds. + affinity: + type: object + properties: + nodeAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + preference: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: object + properties: + nodeSelectorTerms: + type: array + items: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + podAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + podAntiAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + description: The pod's affinity rules. + tolerations: + type: array + items: + type: object + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + description: The pod's tolerations. + topologySpreadConstraints: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + maxSkew: + type: integer + minDomains: + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + description: The pod's topology spread constraints. + priorityClassName: + type: string + description: >- + The name of the priority class used to assign + priority to the pods. + schedulerName: + type: string + description: >- + The name of the scheduler used to dispatch this + `Pod`. If not specified, the default scheduler will + be used. + hostAliases: + type: array + items: + type: object + properties: + hostnames: + type: array + items: + type: string + ip: + type: string + description: >- + The pod's HostAliases. HostAliases is an optional + list of hosts and IPs that will be injected into the + Pod's hosts file if specified. + dnsPolicy: + type: string + enum: + - ClusterFirst + - ClusterFirstWithHostNet + - Default + - None + description: >- + The pod's DNSPolicy. Defaults to `ClusterFirst`. + Valid values are `ClusterFirstWithHostNet`, + `ClusterFirst`, `Default` or `None`. + dnsConfig: + type: object + properties: + nameservers: + type: array + items: + type: string + options: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + searches: + type: array + items: + type: string + description: >- + The pod's DNSConfig. If specified, it will be merged + to the generated DNS configuration based on the + DNSPolicy. + enableServiceLinks: + type: boolean + description: >- + Indicates whether information about services should + be injected into Pod's environment variables. + tmpDirSizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + Defines the total amount of pod memory allocated for + the temporary `EmptyDir` volume `/tmp`. Specify the + allocation in memory units, for example, `100Mi` for + 100 mebibytes. Default value is `5Mi`. The `/tmp` + volume is backed by pod memory, not disk storage, so + avoid setting a high value as it consumes pod memory + resources. + volumes: + type: array + items: + type: object + properties: + name: + type: string + description: Name to use for the volume. Required. + secret: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + optional: + type: boolean + secretName: + type: string + description: '`Secret` to use to populate the volume.' + configMap: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + name: + type: string + optional: + type: boolean + description: '`ConfigMap` to use to populate the volume.' + emptyDir: + type: object + properties: + medium: + type: string + enum: + - Memory + description: >- + Medium represents the type of storage + medium should back this volume. Valid + values are unset or `Memory`. When not + set, it will use the node's default + medium. + sizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + The total amount of local storage required + for this EmptyDir volume (for example + 1Gi). + description: '`EmptyDir` to use to populate the volume.' + persistentVolumeClaim: + type: object + properties: + claimName: + type: string + readOnly: + type: boolean + description: >- + `PersistentVolumeClaim` object to use to + populate the volume. + csi: + type: object + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + type: object + properties: + name: + type: string + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + description: >- + `CSIVolumeSource` object to use to populate + the volume. + image: + type: object + properties: + pullPolicy: + type: string + reference: + type: string + description: >- + `ImageVolumeSource` object to use to populate + the volume. + oneOf: + - properties: + secret: {} + configMap: {} + emptyDir: {} + persistentVolumeClaim: {} + csi: {} + image: {} + description: Additional volumes that can be mounted to the pod. + hostUsers: + type: boolean + description: >- + Use the host user namespace. Optional. Defaults to + `true`. When `true` or not set, the pod runs in the + host user namespace. This is required when the pod + needs features available only in the host namespace, + such as loading kernel modules with + `CAP_SYS_MODULE`.When set to `false`, the pod runs + in a new user namespace. Setting `false` helps + mitigate container breakout vulnerabilities and + allows containers to run as `root` without granting + `root` privileges on the host. This property is + alpha-level in Kubernetes and is supported only by + Kubernetes clusters that enable the + `UserNamespacesSupport` feature. + description: Template for Kafka `Pods`. + perPodService: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + description: >- + Template for Kafka per-pod `Services` used for access + from outside of Kubernetes. + perPodRoute: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + description: >- + Template for Kafka per-pod `Routes` used for access from + outside of OpenShift. + perPodIngress: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + description: >- + Template for Kafka per-pod `Ingress` used for access + from outside of Kubernetes. + persistentVolumeClaim: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + description: Template for all Kafka `PersistentVolumeClaims`. + kafkaContainer: + type: object + properties: + env: + type: array + items: + type: object + properties: + name: + type: string + description: The environment variable key. + value: + type: string + description: The environment variable value. + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a secret. + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a config map. + oneOf: + - properties: + secretKeyRef: {} + required: + - secretKeyRef + - properties: + configMapKeyRef: {} + required: + - configMapKeyRef + description: >- + Reference to the secret or config map property + to which the environment variable is set. + oneOf: + - properties: + value: {} + required: + - value + - properties: + valueFrom: {} + required: + - valueFrom + description: >- + Environment variables which should be applied to the + container. + securityContext: + type: object + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + capabilities: + type: object + properties: + add: + type: array + items: + type: string + drop: + type: array + items: + type: string + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: Security context for the container. + volumeMounts: + type: array + items: + type: object + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + description: >- + Additional volume mounts which should be applied to + the container. + description: Template for the Kafka broker container. + initContainer: + type: object + properties: + env: + type: array + items: + type: object + properties: + name: + type: string + description: The environment variable key. + value: + type: string + description: The environment variable value. + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a secret. + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a config map. + oneOf: + - properties: + secretKeyRef: {} + required: + - secretKeyRef + - properties: + configMapKeyRef: {} + required: + - configMapKeyRef + description: >- + Reference to the secret or config map property + to which the environment variable is set. + oneOf: + - properties: + value: {} + required: + - value + - properties: + valueFrom: {} + required: + - valueFrom + description: >- + Environment variables which should be applied to the + container. + securityContext: + type: object + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + capabilities: + type: object + properties: + add: + type: array + items: + type: string + drop: + type: array + items: + type: string + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: Security context for the container. + volumeMounts: + type: array + items: + type: object + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + description: >- + Additional volume mounts which should be applied to + the container. + description: Template for the Kafka init container. + description: >- + Template for pool resources. The template allows users to + specify how the resources belonging to this pool are + generated. + required: + - replicas + - storage + - roles + description: The specification of the KafkaNodePool. + status: + type: object + properties: + conditions: + type: array + items: + type: object + properties: + type: + type: string + description: >- + The unique identifier of a condition, used to + distinguish between other conditions in the resource. + status: + type: string + description: >- + The status of the condition, either True, False or + Unknown. + lastTransitionTime: + type: string + description: >- + Last time the condition of a type changed from one + status to another. The required format is + 'yyyy-MM-ddTHH:mm:ssZ', in the UTC time zone. + reason: + type: string + description: >- + The reason for the condition's last transition (a + single word in CamelCase). + message: + type: string + description: >- + Human-readable message indicating details about the + condition's last transition. + description: List of status conditions. + observedGeneration: + type: integer + description: >- + The generation of the CRD that was last reconciled by the + operator. + nodeIds: + type: array + items: + type: integer + description: Node IDs used by Kafka nodes in this pool. + clusterId: + type: string + description: Kafka cluster ID. + roles: + type: array + items: + type: string + enum: + - controller + - broker + description: The roles currently assigned to this pool. + replicas: + type: integer + description: >- + The current number of pods being used to provide this + resource. + labelSelector: + type: string + description: Label selector for pods providing this resource. + description: The status of the KafkaNodePool. + required: + - spec + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: strimzi-cluster-operator-global + labels: + app: strimzi +rules: + - apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + verbs: + - get + - list + - watch + - create + - delete + - patch + - update + - apiGroups: + - storage.k8s.io + resources: + - storageclasses + verbs: + - get + - apiGroups: + - '' + resources: + - nodes + verbs: + - list + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: kafkaconnects.kafka.strimzi.io + labels: + app: strimzi + strimzi.io/crd-install: 'true' +spec: + group: kafka.strimzi.io + names: + kind: KafkaConnect + listKind: KafkaConnectList + singular: kafkaconnect + plural: kafkaconnects + shortNames: + - kc + categories: + - strimzi + scope: Namespaced + conversion: + strategy: None + versions: + - name: v1 + served: true + storage: true + subresources: + status: {} + scale: + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + labelSelectorPath: .status.labelSelector + additionalPrinterColumns: + - name: Desired replicas + description: The desired number of Kafka Connect replicas + jsonPath: .spec.replicas + type: integer + - name: Ready + description: The state of the custom resource + jsonPath: '.status.conditions[?(@.type=="Ready")].status' + type: string + schema: + openAPIV3Schema: + type: object + properties: + apiVersion: + type: string + description: >- + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the + latest internal value, and may reject unrecognized values. More + info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + kind: + type: string + description: >- + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the + client submits requests to. Cannot be updated. In CamelCase. + More info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + metadata: + type: object + spec: + type: object + properties: + version: + type: string + description: >- + The Kafka Connect version. Defaults to the latest version. + Consult the user documentation to understand the process + required to upgrade or downgrade the version. + replicas: + type: integer + description: >- + The number of pods in the Kafka Connect group. Required in + the `v1` version of the Strimzi API. Defaults to `3` in the + `v1beta2` version of the Strimzi API. + image: + type: string + description: >- + The container image used for Kafka Connect pods. If no image + name is explicitly specified, it is determined based on the + `spec.version` configuration. The image names are + specifically mapped to corresponding versions in the Cluster + Operator configuration. + bootstrapServers: + type: string + description: >- + Bootstrap servers to connect to. This should be given as a + comma separated list of __:__ pairs. + groupId: + type: string + description: A unique ID that identifies the Connect cluster group. + configStorageTopic: + type: string + description: >- + The name of the Kafka topic where connector configurations + are stored. + statusStorageTopic: + type: string + description: >- + The name of the Kafka topic where connector and task status + are stored. + offsetStorageTopic: + type: string + description: >- + The name of the Kafka topic where source connector offsets + are stored. + tls: + type: object + properties: + trustedCertificates: + type: array + items: + type: object + properties: + secretName: + type: string + description: The name of the Secret containing the certificate. + certificate: + type: string + description: The name of the file certificate in the secret. + pattern: + type: string + description: >- + Pattern for the certificate files in the secret. + Use the + link:https://en.wikipedia.org/wiki/Glob_(programming)[_glob + syntax_] for the pattern. All files in the secret + that match the pattern are used. + oneOf: + - properties: + certificate: {} + required: + - certificate + - properties: + pattern: {} + required: + - pattern + required: + - secretName + description: Trusted certificates for TLS connection. + description: TLS configuration. + authentication: + type: object + properties: + certificateAndKey: + type: object + properties: + secretName: + type: string + description: The name of the Secret containing the certificate. + certificate: + type: string + description: The name of the file certificate in the Secret. + key: + type: string + description: >- + The name of the private key in the secret. The + private key must be in unencrypted PKCS #8 format. + For more information, see RFC 5208: + https://datatracker.ietf.org/doc/html/rfc5208. + required: + - secretName + - certificate + - key + description: >- + Reference to the `Secret` which holds the certificate + and private key pair. + config: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + Configuration for the custom authentication mechanism. + Only properties with the `sasl.` and `ssl.keystore.` + prefixes are allowed. Specify other options in the + regular configuration section of the custom resource. + passwordSecret: + type: object + properties: + secretName: + type: string + description: The name of the Secret containing the password. + password: + type: string + description: >- + The name of the key in the Secret under which the + password is stored. + required: + - secretName + - password + description: Reference to the `Secret` which holds the password. + sasl: + type: boolean + description: Enable or disable SASL on this authentication mechanism. + type: + type: string + enum: + - tls + - scram-sha-256 + - scram-sha-512 + - plain + - custom + description: >- + Specifies the authentication type. Supported types are + `tls`, `scram-sha-256`, `scram-sha-512`, `plain`, + 'oauth', and `custom`. `tls` uses TLS client + authentication and is supported only over TLS + connections. `scram-sha-256` and `scram-sha-512` use + SASL SCRAM-SHA-256 and SASL SCRAM-SHA-512 + authentication, respectively. `plain` uses SASL PLAIN + authentication. `oauth` uses SASL OAUTHBEARER + authentication. `custom` allows you to configure a + custom authentication mechanism. As of Strimzi 0.49.0, + `oauth` type is deprecated and will be removed in the + `v1` API version. Please use `custom` type instead. + username: + type: string + description: Username used for the authentication. + required: + - type + description: Authentication configuration for Kafka Connect. + config: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + The Kafka Connect configuration. Properties with the + following prefixes cannot be set: group.id, + config.storage.topic, offset.storage.topic, + status.storage.topic, ssl., sasl., security., listeners, + plugin.path, rest., bootstrap.servers, + consumer.interceptor.classes, producer.interceptor.classes, + prometheus.metrics.reporter. (with the exception of: + ssl.endpoint.identification.algorithm, ssl.cipher.suites, + ssl.protocol, ssl.enabled.protocols). + resources: + type: object + properties: + claims: + type: array + items: + type: object + properties: + name: + type: string + request: + type: string + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: >- + ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: >- + ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + description: >- + The maximum limits for CPU and memory resources and the + requested initial resources. + livenessProbe: + type: object + properties: + initialDelaySeconds: + type: integer + minimum: 0 + description: >- + The initial delay before first the health is first + checked. Default to 15 seconds. Minimum value is 0. + timeoutSeconds: + type: integer + minimum: 1 + description: >- + The timeout for each attempted health check. Default to + 5 seconds. Minimum value is 1. + periodSeconds: + type: integer + minimum: 1 + description: >- + How often (in seconds) to perform the probe. Default to + 10 seconds. Minimum value is 1. + successThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive successes for the probe to be + considered successful after having failed. Defaults to + 1. Must be 1 for liveness. Minimum value is 1. + failureThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive failures for the probe to be + considered failed after having succeeded. Defaults to 3. + Minimum value is 1. + description: Pod liveness checking. + readinessProbe: + type: object + properties: + initialDelaySeconds: + type: integer + minimum: 0 + description: >- + The initial delay before first the health is first + checked. Default to 15 seconds. Minimum value is 0. + timeoutSeconds: + type: integer + minimum: 1 + description: >- + The timeout for each attempted health check. Default to + 5 seconds. Minimum value is 1. + periodSeconds: + type: integer + minimum: 1 + description: >- + How often (in seconds) to perform the probe. Default to + 10 seconds. Minimum value is 1. + successThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive successes for the probe to be + considered successful after having failed. Defaults to + 1. Must be 1 for liveness. Minimum value is 1. + failureThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive failures for the probe to be + considered failed after having succeeded. Defaults to 3. + Minimum value is 1. + description: Pod readiness checking. + jvmOptions: + type: object + properties: + '-XX': + additionalProperties: + type: string + type: object + description: A map of -XX options to the JVM. + '-Xmx': + type: string + pattern: '^[0-9]+[mMgG]?$' + description: '-Xmx option to to the JVM.' + '-Xms': + type: string + pattern: '^[0-9]+[mMgG]?$' + description: '-Xms option to to the JVM.' + gcLoggingEnabled: + type: boolean + description: >- + Specifies whether the Garbage Collection logging is + enabled. The default is false. + javaSystemProperties: + type: array + items: + type: object + properties: + name: + type: string + description: The system property name. + value: + type: string + description: The system property value. + description: >- + A map of additional system properties which will be + passed using the `-D` option to the JVM. + description: JVM Options for pods. + jmxOptions: + type: object + properties: + authentication: + type: object + properties: + type: + type: string + enum: + - password + description: >- + Authentication type. Currently the only supported + types are `password`.`password` type creates a + username and protected port with no TLS. + required: + - type + description: >- + Authentication configuration for connecting to the JMX + port. + description: JMX Options. + logging: + type: object + properties: + loggers: + additionalProperties: + type: string + type: object + description: A Map from logger name to logger level. + type: + type: string + enum: + - inline + - external + description: 'Logging type, must be either ''inline'' or ''external''.' + valueFrom: + type: object + properties: + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: >- + Reference to the key in the ConfigMap containing the + configuration. + description: >- + `ConfigMap` entry where the logging configuration is + stored. + required: + - type + description: Logging configuration for Kafka Connect. + clientRackInitImage: + type: string + description: >- + The image of the init container used for initializing the + `client.rack`. + rack: + type: object + properties: + envVarName: + type: string + description: >- + The name of the environment variable that defines the + rack ID. Its value sets the `broker.rack` configuration + for Kafka brokers and the `client.rack` configuration + for Kafka Connect or MirrorMaker 2. + topologyKey: + type: string + example: topology.kubernetes.io/zone + description: >- + A key that matches labels assigned to the Kubernetes + cluster nodes. The value of the label is used to set a + broker's `broker.rack` config, and the `client.rack` + config for Kafka Connect or MirrorMaker 2. + type: + type: string + enum: + - topology-label + - environment-variable + description: >- + Specifies the rack awareness type. Supported types are + `topology-label` and `environment-variable`. + `topology-label` uses a Kubernetes worker node label to + set the `broker.rack` configuration for Kafka brokers + and the `client.rack` configuration for Kafka Connect + and MirrorMaker 2. `environment-variable` uses an + environment variable to set the `broker.rack` + configuration for Kafka brokers and the `client.rack` + configuration for Kafka Connect and MirrorMaker 2. When + not specified, `topology-label` type is used by default. + description: >- + Configuration of the node label which will be used as the + `client.rack` consumer configuration. + x-kubernetes-validations: + - rule: >- + (has(self.type) && self.type != "topology-label") || + self.topologyKey != "" + message: topologyKey property is required + - rule: >- + has(self.type) == false || self.type != + "environment-variable" || self.envVarName != "" + message: envVarName property is required + metricsConfig: + type: object + properties: + type: + type: string + enum: + - jmxPrometheusExporter + - strimziMetricsReporter + description: >- + Metrics type. The supported types are + `jmxPrometheusExporter` and `strimziMetricsReporter`. + Type `jmxPrometheusExporter` uses the Prometheus JMX + Exporter to expose Kafka JMX metrics in Prometheus + format through an HTTP endpoint. Type + `strimziMetricsReporter` uses the Strimzi Metrics + Reporter to directly expose Kafka metrics in Prometheus + format through an HTTP endpoint. + valueFrom: + type: object + properties: + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: >- + Reference to the key in the ConfigMap containing the + configuration. + description: >- + ConfigMap entry where the Prometheus JMX Exporter + configuration is stored. + values: + type: object + properties: + allowList: + type: array + items: + type: string + description: >- + A list of regex patterns to filter the metrics to + collect. Should contain at least one element. + description: Configuration values for the Strimzi Metrics Reporter. + required: + - type + description: Metrics configuration. + x-kubernetes-validations: + - rule: >- + self.type != 'jmxPrometheusExporter' || + has(self.valueFrom) + message: valueFrom property is required + tracing: + type: object + properties: + type: + type: string + enum: + - opentelemetry + description: >- + Type of the tracing used. Currently the only supported + type is `opentelemetry` for OpenTelemetry tracing. As of + Strimzi 0.37.0, `jaeger` type is not supported anymore + and this option is ignored. + required: + - type + description: The configuration of tracing in Kafka Connect. + template: + type: object + properties: + podSet: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + description: Template for Kafka Connect `StrimziPodSet` resource. + pod: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + imagePullSecrets: + type: array + items: + type: object + properties: + name: + type: string + description: >- + List of references to secrets in the same namespace + to use for pulling any of the images used by this + Pod. When the `STRIMZI_IMAGE_PULL_SECRETS` + environment variable in Cluster Operator and the + `imagePullSecrets` option are specified, only the + `imagePullSecrets` variable is used and the + `STRIMZI_IMAGE_PULL_SECRETS` variable is ignored. + securityContext: + type: object + properties: + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + fsGroup: + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + supplementalGroups: + type: array + items: + type: integer + supplementalGroupsPolicy: + type: string + sysctls: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: >- + Configures pod-level security attributes and common + container settings. + terminationGracePeriodSeconds: + type: integer + minimum: 0 + description: >- + The grace period is the duration in seconds after + the processes running in the pod are sent a + termination signal, and the time when the processes + are forcibly halted with a kill signal. Set this + value to longer than the expected cleanup time for + your process. Value must be a non-negative integer. + A zero value indicates delete immediately. You might + need to increase the grace period for very large + Kafka clusters, so that the Kafka brokers have + enough time to transfer their work to another broker + before they are terminated. Defaults to 30 seconds. + affinity: + type: object + properties: + nodeAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + preference: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: object + properties: + nodeSelectorTerms: + type: array + items: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + podAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + podAntiAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + description: The pod's affinity rules. + tolerations: + type: array + items: + type: object + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + description: The pod's tolerations. + topologySpreadConstraints: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + maxSkew: + type: integer + minDomains: + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + description: The pod's topology spread constraints. + priorityClassName: + type: string + description: >- + The name of the priority class used to assign + priority to the pods. + schedulerName: + type: string + description: >- + The name of the scheduler used to dispatch this + `Pod`. If not specified, the default scheduler will + be used. + hostAliases: + type: array + items: + type: object + properties: + hostnames: + type: array + items: + type: string + ip: + type: string + description: >- + The pod's HostAliases. HostAliases is an optional + list of hosts and IPs that will be injected into the + Pod's hosts file if specified. + dnsPolicy: + type: string + enum: + - ClusterFirst + - ClusterFirstWithHostNet + - Default + - None + description: >- + The pod's DNSPolicy. Defaults to `ClusterFirst`. + Valid values are `ClusterFirstWithHostNet`, + `ClusterFirst`, `Default` or `None`. + dnsConfig: + type: object + properties: + nameservers: + type: array + items: + type: string + options: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + searches: + type: array + items: + type: string + description: >- + The pod's DNSConfig. If specified, it will be merged + to the generated DNS configuration based on the + DNSPolicy. + enableServiceLinks: + type: boolean + description: >- + Indicates whether information about services should + be injected into Pod's environment variables. + tmpDirSizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + Defines the total amount of pod memory allocated for + the temporary `EmptyDir` volume `/tmp`. Specify the + allocation in memory units, for example, `100Mi` for + 100 mebibytes. Default value is `5Mi`. The `/tmp` + volume is backed by pod memory, not disk storage, so + avoid setting a high value as it consumes pod memory + resources. + volumes: + type: array + items: + type: object + properties: + name: + type: string + description: Name to use for the volume. Required. + secret: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + optional: + type: boolean + secretName: + type: string + description: '`Secret` to use to populate the volume.' + configMap: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + name: + type: string + optional: + type: boolean + description: '`ConfigMap` to use to populate the volume.' + emptyDir: + type: object + properties: + medium: + type: string + enum: + - Memory + description: >- + Medium represents the type of storage + medium should back this volume. Valid + values are unset or `Memory`. When not + set, it will use the node's default + medium. + sizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + The total amount of local storage required + for this EmptyDir volume (for example + 1Gi). + description: '`EmptyDir` to use to populate the volume.' + persistentVolumeClaim: + type: object + properties: + claimName: + type: string + readOnly: + type: boolean + description: >- + `PersistentVolumeClaim` object to use to + populate the volume. + csi: + type: object + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + type: object + properties: + name: + type: string + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + description: >- + `CSIVolumeSource` object to use to populate + the volume. + image: + type: object + properties: + pullPolicy: + type: string + reference: + type: string + description: >- + `ImageVolumeSource` object to use to populate + the volume. + oneOf: + - properties: + secret: {} + configMap: {} + emptyDir: {} + persistentVolumeClaim: {} + csi: {} + image: {} + description: Additional volumes that can be mounted to the pod. + hostUsers: + type: boolean + description: >- + Use the host user namespace. Optional. Defaults to + `true`. When `true` or not set, the pod runs in the + host user namespace. This is required when the pod + needs features available only in the host namespace, + such as loading kernel modules with + `CAP_SYS_MODULE`.When set to `false`, the pod runs + in a new user namespace. Setting `false` helps + mitigate container breakout vulnerabilities and + allows containers to run as `root` without granting + `root` privileges on the host. This property is + alpha-level in Kubernetes and is supported only by + Kubernetes clusters that enable the + `UserNamespacesSupport` feature. + description: Template for Kafka Connect `Pods`. + apiService: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + ipFamilyPolicy: + type: string + enum: + - SingleStack + - PreferDualStack + - RequireDualStack + description: >- + Specifies the IP Family Policy used by the service. + Available options are `SingleStack`, + `PreferDualStack` and `RequireDualStack`. + `SingleStack` is for a single IP family. + `PreferDualStack` is for two IP families on + dual-stack configured clusters or a single IP family + on single-stack clusters. `RequireDualStack` fails + unless there are two IP families on dual-stack + configured clusters. If unspecified, Kubernetes will + choose the default value based on the service type. + ipFamilies: + type: array + items: + type: string + enum: + - IPv4 + - IPv6 + description: >- + Specifies the IP Families used by the service. + Available options are `IPv4` and `IPv6`. If + unspecified, Kubernetes will choose the default + value based on the `ipFamilyPolicy` setting. + description: Template for Kafka Connect API `Service`. + headlessService: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + ipFamilyPolicy: + type: string + enum: + - SingleStack + - PreferDualStack + - RequireDualStack + description: >- + Specifies the IP Family Policy used by the service. + Available options are `SingleStack`, + `PreferDualStack` and `RequireDualStack`. + `SingleStack` is for a single IP family. + `PreferDualStack` is for two IP families on + dual-stack configured clusters or a single IP family + on single-stack clusters. `RequireDualStack` fails + unless there are two IP families on dual-stack + configured clusters. If unspecified, Kubernetes will + choose the default value based on the service type. + ipFamilies: + type: array + items: + type: string + enum: + - IPv4 + - IPv6 + description: >- + Specifies the IP Families used by the service. + Available options are `IPv4` and `IPv6`. If + unspecified, Kubernetes will choose the default + value based on the `ipFamilyPolicy` setting. + description: Template for Kafka Connect headless `Service`. + connectContainer: + type: object + properties: + env: + type: array + items: + type: object + properties: + name: + type: string + description: The environment variable key. + value: + type: string + description: The environment variable value. + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a secret. + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a config map. + oneOf: + - properties: + secretKeyRef: {} + required: + - secretKeyRef + - properties: + configMapKeyRef: {} + required: + - configMapKeyRef + description: >- + Reference to the secret or config map property + to which the environment variable is set. + oneOf: + - properties: + value: {} + required: + - value + - properties: + valueFrom: {} + required: + - valueFrom + description: >- + Environment variables which should be applied to the + container. + securityContext: + type: object + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + capabilities: + type: object + properties: + add: + type: array + items: + type: string + drop: + type: array + items: + type: string + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: Security context for the container. + volumeMounts: + type: array + items: + type: object + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + description: >- + Additional volume mounts which should be applied to + the container. + description: Template for the Kafka Connect container. + initContainer: + type: object + properties: + env: + type: array + items: + type: object + properties: + name: + type: string + description: The environment variable key. + value: + type: string + description: The environment variable value. + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a secret. + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a config map. + oneOf: + - properties: + secretKeyRef: {} + required: + - secretKeyRef + - properties: + configMapKeyRef: {} + required: + - configMapKeyRef + description: >- + Reference to the secret or config map property + to which the environment variable is set. + oneOf: + - properties: + value: {} + required: + - value + - properties: + valueFrom: {} + required: + - valueFrom + description: >- + Environment variables which should be applied to the + container. + securityContext: + type: object + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + capabilities: + type: object + properties: + add: + type: array + items: + type: string + drop: + type: array + items: + type: string + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: Security context for the container. + volumeMounts: + type: array + items: + type: object + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + description: >- + Additional volume mounts which should be applied to + the container. + description: Template for the Kafka init container. + podDisruptionBudget: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: >- + Metadata to apply to the + `PodDisruptionBudgetTemplate` resource. + maxUnavailable: + type: integer + minimum: 0 + description: >- + Maximum number of unavailable pods to allow + automatic Pod eviction. A Pod eviction is allowed + when the `maxUnavailable` number of pods or fewer + are unavailable after the eviction. Setting this + value to 0 prevents all voluntary evictions, so the + pods must be evicted manually. Defaults to 1. + description: Template for Kafka Connect `PodDisruptionBudget`. + serviceAccount: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + description: Template for the Kafka Connect service account. + clusterRoleBinding: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + description: Template for the Kafka Connect ClusterRoleBinding. + buildPod: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + imagePullSecrets: + type: array + items: + type: object + properties: + name: + type: string + description: >- + List of references to secrets in the same namespace + to use for pulling any of the images used by this + Pod. When the `STRIMZI_IMAGE_PULL_SECRETS` + environment variable in Cluster Operator and the + `imagePullSecrets` option are specified, only the + `imagePullSecrets` variable is used and the + `STRIMZI_IMAGE_PULL_SECRETS` variable is ignored. + securityContext: + type: object + properties: + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + fsGroup: + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + supplementalGroups: + type: array + items: + type: integer + supplementalGroupsPolicy: + type: string + sysctls: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: >- + Configures pod-level security attributes and common + container settings. + terminationGracePeriodSeconds: + type: integer + minimum: 0 + description: >- + The grace period is the duration in seconds after + the processes running in the pod are sent a + termination signal, and the time when the processes + are forcibly halted with a kill signal. Set this + value to longer than the expected cleanup time for + your process. Value must be a non-negative integer. + A zero value indicates delete immediately. You might + need to increase the grace period for very large + Kafka clusters, so that the Kafka brokers have + enough time to transfer their work to another broker + before they are terminated. Defaults to 30 seconds. + affinity: + type: object + properties: + nodeAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + preference: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: object + properties: + nodeSelectorTerms: + type: array + items: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + podAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + podAntiAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + description: The pod's affinity rules. + tolerations: + type: array + items: + type: object + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + description: The pod's tolerations. + topologySpreadConstraints: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + maxSkew: + type: integer + minDomains: + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + description: The pod's topology spread constraints. + priorityClassName: + type: string + description: >- + The name of the priority class used to assign + priority to the pods. + schedulerName: + type: string + description: >- + The name of the scheduler used to dispatch this + `Pod`. If not specified, the default scheduler will + be used. + hostAliases: + type: array + items: + type: object + properties: + hostnames: + type: array + items: + type: string + ip: + type: string + description: >- + The pod's HostAliases. HostAliases is an optional + list of hosts and IPs that will be injected into the + Pod's hosts file if specified. + dnsPolicy: + type: string + enum: + - ClusterFirst + - ClusterFirstWithHostNet + - Default + - None + description: >- + The pod's DNSPolicy. Defaults to `ClusterFirst`. + Valid values are `ClusterFirstWithHostNet`, + `ClusterFirst`, `Default` or `None`. + dnsConfig: + type: object + properties: + nameservers: + type: array + items: + type: string + options: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + searches: + type: array + items: + type: string + description: >- + The pod's DNSConfig. If specified, it will be merged + to the generated DNS configuration based on the + DNSPolicy. + enableServiceLinks: + type: boolean + description: >- + Indicates whether information about services should + be injected into Pod's environment variables. + tmpDirSizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + Defines the total amount of pod memory allocated for + the temporary `EmptyDir` volume `/tmp`. Specify the + allocation in memory units, for example, `100Mi` for + 100 mebibytes. Default value is `5Mi`. The `/tmp` + volume is backed by pod memory, not disk storage, so + avoid setting a high value as it consumes pod memory + resources. + volumes: + type: array + items: + type: object + properties: + name: + type: string + description: Name to use for the volume. Required. + secret: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + optional: + type: boolean + secretName: + type: string + description: '`Secret` to use to populate the volume.' + configMap: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + name: + type: string + optional: + type: boolean + description: '`ConfigMap` to use to populate the volume.' + emptyDir: + type: object + properties: + medium: + type: string + enum: + - Memory + description: >- + Medium represents the type of storage + medium should back this volume. Valid + values are unset or `Memory`. When not + set, it will use the node's default + medium. + sizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + The total amount of local storage required + for this EmptyDir volume (for example + 1Gi). + description: '`EmptyDir` to use to populate the volume.' + persistentVolumeClaim: + type: object + properties: + claimName: + type: string + readOnly: + type: boolean + description: >- + `PersistentVolumeClaim` object to use to + populate the volume. + csi: + type: object + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + type: object + properties: + name: + type: string + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + description: >- + `CSIVolumeSource` object to use to populate + the volume. + image: + type: object + properties: + pullPolicy: + type: string + reference: + type: string + description: >- + `ImageVolumeSource` object to use to populate + the volume. + oneOf: + - properties: + secret: {} + configMap: {} + emptyDir: {} + persistentVolumeClaim: {} + csi: {} + image: {} + description: Additional volumes that can be mounted to the pod. + hostUsers: + type: boolean + description: >- + Use the host user namespace. Optional. Defaults to + `true`. When `true` or not set, the pod runs in the + host user namespace. This is required when the pod + needs features available only in the host namespace, + such as loading kernel modules with + `CAP_SYS_MODULE`.When set to `false`, the pod runs + in a new user namespace. Setting `false` helps + mitigate container breakout vulnerabilities and + allows containers to run as `root` without granting + `root` privileges on the host. This property is + alpha-level in Kubernetes and is supported only by + Kubernetes clusters that enable the + `UserNamespacesSupport` feature. + description: >- + Template for Kafka Connect Build `Pods`. The build pod + is used only on Kubernetes. + buildContainer: + type: object + properties: + env: + type: array + items: + type: object + properties: + name: + type: string + description: The environment variable key. + value: + type: string + description: The environment variable value. + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a secret. + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a config map. + oneOf: + - properties: + secretKeyRef: {} + required: + - secretKeyRef + - properties: + configMapKeyRef: {} + required: + - configMapKeyRef + description: >- + Reference to the secret or config map property + to which the environment variable is set. + oneOf: + - properties: + value: {} + required: + - value + - properties: + valueFrom: {} + required: + - valueFrom + description: >- + Environment variables which should be applied to the + container. + securityContext: + type: object + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + capabilities: + type: object + properties: + add: + type: array + items: + type: string + drop: + type: array + items: + type: string + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: Security context for the container. + volumeMounts: + type: array + items: + type: object + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + description: >- + Additional volume mounts which should be applied to + the container. + description: >- + Template for the Kafka Connect Build container. The + build container is used only on Kubernetes. + buildConfig: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: >- + Metadata to apply to the + `PodDisruptionBudgetTemplate` resource. + pullSecret: + type: string + description: >- + Container Registry Secret with the credentials for + pulling the base image. + description: >- + Template for the Kafka Connect BuildConfig used to build + new container images. The BuildConfig is used only on + OpenShift. + buildServiceAccount: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + description: Template for the Kafka Connect Build service account. + jmxSecret: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + description: >- + Template for Secret of the Kafka Connect Cluster JMX + authentication. + description: >- + Template for Kafka Connect and Kafka MirrorMaker 2 + resources. The template allows users to specify how the + `Pods`, `Service`, and other services are generated. + build: + type: object + properties: + output: + type: object + properties: + additionalBuildOptions: + type: array + items: + type: string + description: >- + Configures additional options to pass to the `build` + command of either Kaniko or Buildah (depending on + the feature gate setting) when building a new Kafka + Connect image. Allowed Kaniko options: + --customPlatform, --custom-platform, --insecure, + --insecure-pull, --insecure-registry, --log-format, + --log-timestamp, --registry-mirror, --reproducible, + --single-snapshot, --skip-tls-verify, + --skip-tls-verify-pull, --skip-tls-verify-registry, + --verbosity, --snapshotMode, --use-new-run, + --registry-certificate, --registry-client-cert, + --ignore-path. Allowed Buildah `build` options: + --authfile, --cert-dir, --creds, --decryption-key, + --retry, --retry-delay, --tls-verify. Those options + are used only on Kubernetes, where Kaniko and + Buildah are available. They are ignored on + OpenShift. For more information, see the + link:https://github.com/GoogleContainerTools/kaniko[Kaniko + GitHub repository^] or the + link:https://github.com/containers/buildah/blob/main/docs/buildah-build.1.md[Buildah + build document^]. Changing this field does not + trigger a rebuild of the Kafka Connect image. + additionalPushOptions: + type: array + items: + type: string + description: >- + Configures additional options to pass to the Buildah + `push` command when pushing a new Connect image. + Allowed options: --authfile, --cert-dir, --creds, + --quiet, --retry, --retry-delay, --tls-verify. Those + options are used only on Kubernetes, where Buildah + is available. They are ignored on OpenShift. For + more information, see the + link:https://github.com/containers/buildah/blob/main/docs/buildah-push.1.md[Buildah + push document^]. Changing this field does not + trigger a rebuild of the Kafka Connect image. + image: + type: string + description: The name of the image which will be built. Required. + pushSecret: + type: string + description: >- + Container Registry Secret with the credentials for + pushing the newly built image. + type: + type: string + enum: + - docker + - imagestream + description: >- + Output type. Must be either `docker` for pushing the + newly build image to Docker compatible registry or + `imagestream` for pushing the image to OpenShift + ImageStream. Required. + required: + - image + - type + description: >- + Configures where should the newly built image be stored. + Required. + plugins: + type: array + items: + type: object + properties: + name: + type: string + pattern: '^[a-z0-9][-_a-z0-9]*[a-z0-9]$' + description: >- + The unique name of the connector plugin. Will be + used to generate the path where the connector + artifacts will be stored. The name has to be + unique within the KafkaConnect resource. The name + has to follow the following pattern: + `^[a-z][-_a-z0-9]*[a-z]$`. Required. + artifacts: + type: array + items: + type: object + properties: + artifact: + type: string + description: >- + Maven artifact id. Applicable to the `maven` + artifact type only. + fileName: + type: string + description: >- + Name under which the artifact will be + stored. + group: + type: string + description: >- + Maven group id. Applicable to the `maven` + artifact type only. + insecure: + type: boolean + description: >- + By default, connections using TLS are + verified to check they are secure. The + server certificate used must be valid, + trusted, and contain the server name. By + setting this option to `true`, all TLS + verification is disabled and the artifact + will be downloaded, even when the server is + considered insecure. + repository: + type: string + description: >- + Maven repository to download the artifact + from. Applicable to the `maven` artifact + type only. + sha512sum: + type: string + description: >- + SHA512 checksum of the artifact. Optional. + If specified, the checksum will be verified + while building the new container. If not + specified, the downloaded artifact will not + be verified. Not applicable to the `maven` + artifact type. + type: + type: string + enum: + - jar + - tgz + - zip + - maven + - other + description: >- + Artifact type. Currently, the supported + artifact types are `tgz`, `jar`, `zip`, + `other` and `maven`. + url: + type: string + pattern: >- + ^(https?|ftp)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]$ + description: >- + URL of the artifact which will be + downloaded. Strimzi does not do any security + scanning of the downloaded artifacts. For + security reasons, you should first verify + the artifacts manually and configure the + checksum verification to make sure the same + artifact is used in the automated build. + Required for `jar`, `zip`, `tgz` and `other` + artifacts. Not applicable to the `maven` + artifact type. + version: + type: string + description: >- + Maven version number. Applicable to the + `maven` artifact type only. + required: + - type + description: >- + List of artifacts which belong to this connector + plugin. Required. + required: + - name + - artifacts + description: >- + List of connector plugins which should be added to the + Kafka Connect. Required. + resources: + type: object + properties: + claims: + type: array + items: + type: object + properties: + name: + type: string + request: + type: string + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: >- + ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: >- + ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + description: CPU and memory resources to reserve for the build. + required: + - output + - plugins + description: >- + Configures how the Connect container image should be built. + Optional. + plugins: + type: array + items: + type: object + properties: + name: + type: string + pattern: '^[a-z0-9][-_a-z0-9]*[a-z0-9]$' + description: >- + A unique name for the connector plugin. This name is + used to generate the mount path for the connector + artifacts. The name has to be unique within the + KafkaConnect resource. The name must be unique within + the `KafkaConnect` resource and match the pattern: + `^[a-z][-_a-z0-9]*[a-z]$`. Required. + artifacts: + type: array + items: + type: object + properties: + pullPolicy: + type: string + description: >- + Policy that determines when the container image + (OCI artifact) is pulled. + + + Possible values are: + + + * `Always`: Always pull the image. If the pull + fails, container creation fails. + + * `Never`: Never pull the image. Use only a + locally available image. Container creation + fails if the image isn’t present. + + * `IfNotPresent`: Pull the image only if it’s + not already available locally. Container + creation fails if the image isn’t present and + the pull fails. + + + Defaults to `Always` if `:latest` tag is + specified, or `IfNotPresent` otherwise. + reference: + type: string + description: >- + Reference to the container image (OCI artifact) + containing the Kafka Connect plugin. The image + is mounted as a volume and provides the plugin + binary. Required. + type: + type: string + enum: + - image + description: >- + Artifact type. Currently, the only supported + artifact type is `image`. + required: + - reference + - type + description: >- + List of artifacts associated with this connector + plugin. Required. + required: + - name + - artifacts + description: >- + List of connector plugins to mount into the `KafkaConnect` + pod. + required: + - replicas + - bootstrapServers + - groupId + - configStorageTopic + - statusStorageTopic + - offsetStorageTopic + description: The specification of the Kafka Connect cluster. + status: + type: object + properties: + conditions: + type: array + items: + type: object + properties: + type: + type: string + description: >- + The unique identifier of a condition, used to + distinguish between other conditions in the resource. + status: + type: string + description: >- + The status of the condition, either True, False or + Unknown. + lastTransitionTime: + type: string + description: >- + Last time the condition of a type changed from one + status to another. The required format is + 'yyyy-MM-ddTHH:mm:ssZ', in the UTC time zone. + reason: + type: string + description: >- + The reason for the condition's last transition (a + single word in CamelCase). + message: + type: string + description: >- + Human-readable message indicating details about the + condition's last transition. + description: List of status conditions. + observedGeneration: + type: integer + description: >- + The generation of the CRD that was last reconciled by the + operator. + url: + type: string + description: >- + The URL of the REST API endpoint for managing and monitoring + Kafka Connect connectors. + connectorPlugins: + type: array + items: + type: object + properties: + class: + type: string + description: The class of the connector plugin. + type: + type: string + description: >- + The type of the connector plugin. The available types + are `sink` and `source`. + version: + type: string + description: The version of the connector plugin. + description: >- + The list of connector plugins available in this Kafka + Connect deployment. + replicas: + type: integer + description: >- + The current number of pods being used to provide this + resource. + labelSelector: + type: string + description: Label selector for pods providing this resource. + description: The status of the Kafka Connect cluster. + required: + - spec + +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: strimzi-cluster-operator + labels: + app: strimzi + namespace: kafka +data: + log4j2.properties: > + name = COConfig + + monitorInterval = 30 + + + appender.console.type = Console + + appender.console.name = STDOUT + + appender.console.layout.type = PatternLayout + + appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - + %m%n + + + rootLogger.level = ${env:STRIMZI_LOG_LEVEL:-INFO} + + rootLogger.appenderRefs = stdout + + rootLogger.appenderRef.console.ref = STDOUT + + + # Kafka AdminClient logging is a bit noisy at INFO level + + logger.kafka.name = org.apache.kafka + + logger.kafka.level = WARN + + + # Keeps separate level for Netty logging -> to not be changed by the root + logger + + logger.netty.name = io.netty + + logger.netty.level = INFO + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: kafkamirrormaker2s.kafka.strimzi.io + labels: + app: strimzi + strimzi.io/crd-install: 'true' +spec: + group: kafka.strimzi.io + names: + kind: KafkaMirrorMaker2 + listKind: KafkaMirrorMaker2List + singular: kafkamirrormaker2 + plural: kafkamirrormaker2s + shortNames: + - kmm2 + categories: + - strimzi + scope: Namespaced + conversion: + strategy: None + versions: + - name: v1 + served: true + storage: true + subresources: + status: {} + scale: + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + labelSelectorPath: .status.labelSelector + additionalPrinterColumns: + - name: Desired replicas + description: The desired number of Kafka MirrorMaker 2 replicas + jsonPath: .spec.replicas + type: integer + - name: Ready + description: The state of the custom resource + jsonPath: '.status.conditions[?(@.type=="Ready")].status' + type: string + schema: + openAPIV3Schema: + type: object + properties: + apiVersion: + type: string + description: >- + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the + latest internal value, and may reject unrecognized values. More + info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + kind: + type: string + description: >- + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the + client submits requests to. Cannot be updated. In CamelCase. + More info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + metadata: + type: object + spec: + type: object + properties: + version: + type: string + description: >- + The Kafka Connect version. Defaults to the latest version. + Consult the user documentation to understand the process + required to upgrade or downgrade the version. + replicas: + type: integer + description: >- + The number of pods in the Kafka Connect group. Required in + the `v1` version of the Strimzi API. Defaults to `3` in the + `v1beta2` version of the Strimzi API. + image: + type: string + description: >- + The container image used for Kafka Connect pods. If no image + name is explicitly specified, it is determined based on the + `spec.version` configuration. The image names are + specifically mapped to corresponding versions in the Cluster + Operator configuration. + target: + type: object + properties: + alias: + type: string + pattern: '^[a-zA-Z0-9\._\-]{1,100}$' + description: Alias used to reference the Kafka cluster. + bootstrapServers: + type: string + description: >- + A comma-separated list of `host:port` pairs for + establishing the connection to the Kafka cluster. + groupId: + type: string + description: >- + A unique ID that identifies the Connect cluster group. + Required. + configStorageTopic: + type: string + description: >- + The name of the Kafka topic where connector + configurations are stored. Required. + statusStorageTopic: + type: string + description: >- + The name of the Kafka topic where connector and task + statuses are stored. Required. + offsetStorageTopic: + type: string + description: >- + The name of the Kafka topic where source connector + offsets are stored. Required. + tls: + type: object + properties: + trustedCertificates: + type: array + items: + type: object + properties: + secretName: + type: string + description: >- + The name of the Secret containing the + certificate. + certificate: + type: string + description: >- + The name of the file certificate in the + secret. + pattern: + type: string + description: >- + Pattern for the certificate files in the + secret. Use the + link:https://en.wikipedia.org/wiki/Glob_(programming)[_glob + syntax_] for the pattern. All files in the + secret that match the pattern are used. + oneOf: + - properties: + certificate: {} + required: + - certificate + - properties: + pattern: {} + required: + - pattern + required: + - secretName + description: Trusted certificates for TLS connection. + description: >- + TLS configuration for connecting MirrorMaker 2 + connectors to a cluster. + authentication: + type: object + properties: + certificateAndKey: + type: object + properties: + secretName: + type: string + description: >- + The name of the Secret containing the + certificate. + certificate: + type: string + description: The name of the file certificate in the Secret. + key: + type: string + description: >- + The name of the private key in the secret. The + private key must be in unencrypted PKCS #8 + format. For more information, see RFC 5208: + https://datatracker.ietf.org/doc/html/rfc5208. + required: + - secretName + - certificate + - key + description: >- + Reference to the `Secret` which holds the + certificate and private key pair. + config: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + Configuration for the custom authentication + mechanism. Only properties with the `sasl.` and + `ssl.keystore.` prefixes are allowed. Specify other + options in the regular configuration section of the + custom resource. + passwordSecret: + type: object + properties: + secretName: + type: string + description: The name of the Secret containing the password. + password: + type: string + description: >- + The name of the key in the Secret under which + the password is stored. + required: + - secretName + - password + description: Reference to the `Secret` which holds the password. + sasl: + type: boolean + description: >- + Enable or disable SASL on this authentication + mechanism. + type: + type: string + enum: + - tls + - scram-sha-256 + - scram-sha-512 + - plain + - custom + description: >- + Specifies the authentication type. Supported types + are `tls`, `scram-sha-256`, `scram-sha-512`, + `plain`, 'oauth', and `custom`. `tls` uses TLS + client authentication and is supported only over TLS + connections. `scram-sha-256` and `scram-sha-512` use + SASL SCRAM-SHA-256 and SASL SCRAM-SHA-512 + authentication, respectively. `plain` uses SASL + PLAIN authentication. `oauth` uses SASL OAUTHBEARER + authentication. `custom` allows you to configure a + custom authentication mechanism. As of Strimzi + 0.49.0, `oauth` type is deprecated and will be + removed in the `v1` API version. Please use `custom` + type instead. + username: + type: string + description: Username used for the authentication. + required: + - type + description: >- + Authentication configuration for connecting to the + cluster. + config: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + The MirrorMaker 2 cluster config. Properties with the + following prefixes cannot be set: group.id, + config.storage.topic, offset.storage.topic, + status.storage.topic, ssl., sasl., security., listeners, + plugin.path, rest., bootstrap.servers, + consumer.interceptor.classes, + producer.interceptor.classes (with the exception of: + ssl.endpoint.identification.algorithm, + ssl.cipher.suites, ssl.protocol, ssl.enabled.protocols). + required: + - alias + - bootstrapServers + - groupId + - configStorageTopic + - statusStorageTopic + - offsetStorageTopic + description: >- + The target Apache Kafka cluster. The target Kafka cluster is + used by the underlying Kafka Connect framework for its + internal topics. + mirrors: + type: array + items: + type: object + properties: + source: + type: object + properties: + alias: + type: string + pattern: '^[a-zA-Z0-9\._\-]{1,100}$' + description: Alias used to reference the Kafka cluster. + bootstrapServers: + type: string + description: >- + A comma-separated list of `host:port` pairs for + establishing the connection to the Kafka cluster. + tls: + type: object + properties: + trustedCertificates: + type: array + items: + type: object + properties: + secretName: + type: string + description: >- + The name of the Secret containing the + certificate. + certificate: + type: string + description: >- + The name of the file certificate in the + secret. + pattern: + type: string + description: >- + Pattern for the certificate files in the + secret. Use the + link:https://en.wikipedia.org/wiki/Glob_(programming)[_glob + syntax_] for the pattern. All files in + the secret that match the pattern are + used. + oneOf: + - properties: + certificate: {} + required: + - certificate + - properties: + pattern: {} + required: + - pattern + required: + - secretName + description: Trusted certificates for TLS connection. + description: >- + TLS configuration for connecting MirrorMaker 2 + connectors to a cluster. + authentication: + type: object + properties: + certificateAndKey: + type: object + properties: + secretName: + type: string + description: >- + The name of the Secret containing the + certificate. + certificate: + type: string + description: >- + The name of the file certificate in the + Secret. + key: + type: string + description: >- + The name of the private key in the secret. + The private key must be in unencrypted + PKCS #8 format. For more information, see + RFC 5208: + https://datatracker.ietf.org/doc/html/rfc5208. + required: + - secretName + - certificate + - key + description: >- + Reference to the `Secret` which holds the + certificate and private key pair. + config: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + Configuration for the custom authentication + mechanism. Only properties with the `sasl.` + and `ssl.keystore.` prefixes are allowed. + Specify other options in the regular + configuration section of the custom resource. + passwordSecret: + type: object + properties: + secretName: + type: string + description: >- + The name of the Secret containing the + password. + password: + type: string + description: >- + The name of the key in the Secret under + which the password is stored. + required: + - secretName + - password + description: >- + Reference to the `Secret` which holds the + password. + sasl: + type: boolean + description: >- + Enable or disable SASL on this authentication + mechanism. + type: + type: string + enum: + - tls + - scram-sha-256 + - scram-sha-512 + - plain + - custom + description: >- + Specifies the authentication type. Supported + types are `tls`, `scram-sha-256`, + `scram-sha-512`, `plain`, 'oauth', and + `custom`. `tls` uses TLS client authentication + and is supported only over TLS connections. + `scram-sha-256` and `scram-sha-512` use SASL + SCRAM-SHA-256 and SASL SCRAM-SHA-512 + authentication, respectively. `plain` uses + SASL PLAIN authentication. `oauth` uses SASL + OAUTHBEARER authentication. `custom` allows + you to configure a custom authentication + mechanism. As of Strimzi 0.49.0, `oauth` type + is deprecated and will be removed in the `v1` + API version. Please use `custom` type instead. + username: + type: string + description: Username used for the authentication. + required: + - type + description: >- + Authentication configuration for connecting to the + cluster. + config: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + The MirrorMaker 2 cluster config. Properties with + the following prefixes cannot be set: group.id, + config.storage.topic, offset.storage.topic, + status.storage.topic, ssl., sasl., security., + listeners, plugin.path, rest., bootstrap.servers, + consumer.interceptor.classes, + producer.interceptor.classes (with the exception + of: ssl.endpoint.identification.algorithm, + ssl.cipher.suites, ssl.protocol, + ssl.enabled.protocols). + required: + - alias + - bootstrapServers + description: >- + The source Apache Kafka cluster. The source Kafka + cluster is used by the Kafka MirrorMaker 2 connectors. + sourceConnector: + type: object + properties: + tasksMax: + type: integer + minimum: 1 + description: >- + The maximum number of tasks for the Kafka + Connector. + version: + type: string + description: >- + Desired version or version range to respect when + starting the Kafka Connector. This is only + supported when using Kafka Connect version 4.1.0 + and higher. + config: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + The Kafka Connector configuration. The following + properties cannot be set: name, connector.class, + tasks.max, connector.plugin.version. + state: + type: string + enum: + - paused + - stopped + - running + description: >- + The state the connector should be in. Defaults to + running. + autoRestart: + type: object + properties: + enabled: + type: boolean + description: >- + Whether automatic restart for failed + connectors and tasks should be enabled or + disabled. + maxRestarts: + type: integer + description: >- + The maximum number of connector restarts that + the operator will try. If the connector + remains in a failed state after reaching this + limit, it must be restarted manually by the + user. Defaults to an unlimited number of + restarts. + description: >- + Automatic restart of connector and tasks + configuration. + listOffsets: + type: object + properties: + toConfigMap: + type: object + properties: + name: + type: string + description: >- + Reference to the ConfigMap where the list of + offsets will be written to. + required: + - toConfigMap + description: Configuration for listing offsets. + alterOffsets: + type: object + properties: + fromConfigMap: + type: object + properties: + name: + type: string + description: >- + Reference to the ConfigMap where the new + offsets are stored. + required: + - fromConfigMap + description: Configuration for altering offsets. + description: >- + The specification of the Kafka MirrorMaker 2 source + connector. + checkpointConnector: + type: object + properties: + tasksMax: + type: integer + minimum: 1 + description: >- + The maximum number of tasks for the Kafka + Connector. + version: + type: string + description: >- + Desired version or version range to respect when + starting the Kafka Connector. This is only + supported when using Kafka Connect version 4.1.0 + and higher. + config: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + The Kafka Connector configuration. The following + properties cannot be set: name, connector.class, + tasks.max, connector.plugin.version. + state: + type: string + enum: + - paused + - stopped + - running + description: >- + The state the connector should be in. Defaults to + running. + autoRestart: + type: object + properties: + enabled: + type: boolean + description: >- + Whether automatic restart for failed + connectors and tasks should be enabled or + disabled. + maxRestarts: + type: integer + description: >- + The maximum number of connector restarts that + the operator will try. If the connector + remains in a failed state after reaching this + limit, it must be restarted manually by the + user. Defaults to an unlimited number of + restarts. + description: >- + Automatic restart of connector and tasks + configuration. + listOffsets: + type: object + properties: + toConfigMap: + type: object + properties: + name: + type: string + description: >- + Reference to the ConfigMap where the list of + offsets will be written to. + required: + - toConfigMap + description: Configuration for listing offsets. + alterOffsets: + type: object + properties: + fromConfigMap: + type: object + properties: + name: + type: string + description: >- + Reference to the ConfigMap where the new + offsets are stored. + required: + - fromConfigMap + description: Configuration for altering offsets. + description: >- + The specification of the Kafka MirrorMaker 2 + checkpoint connector. + topicsPattern: + type: string + description: >- + A regular expression matching the topics to be + mirrored, for example, "topic1\|topic2\|topic3". + Comma-separated lists are also supported. + topicsExcludePattern: + type: string + description: >- + A regular expression matching the topics to exclude + from mirroring. Comma-separated lists are also + supported. + groupsPattern: + type: string + description: >- + A regular expression matching the consumer groups to + be mirrored. Comma-separated lists are also supported. + groupsExcludePattern: + type: string + description: >- + A regular expression matching the consumer groups to + exclude from mirroring. Comma-separated lists are also + supported. + required: + - source + description: Configuration of the MirrorMaker 2 connectors. + resources: + type: object + properties: + claims: + type: array + items: + type: object + properties: + name: + type: string + request: + type: string + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: >- + ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: >- + ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + description: >- + The maximum limits for CPU and memory resources and the + requested initial resources. + livenessProbe: + type: object + properties: + initialDelaySeconds: + type: integer + minimum: 0 + description: >- + The initial delay before first the health is first + checked. Default to 15 seconds. Minimum value is 0. + timeoutSeconds: + type: integer + minimum: 1 + description: >- + The timeout for each attempted health check. Default to + 5 seconds. Minimum value is 1. + periodSeconds: + type: integer + minimum: 1 + description: >- + How often (in seconds) to perform the probe. Default to + 10 seconds. Minimum value is 1. + successThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive successes for the probe to be + considered successful after having failed. Defaults to + 1. Must be 1 for liveness. Minimum value is 1. + failureThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive failures for the probe to be + considered failed after having succeeded. Defaults to 3. + Minimum value is 1. + description: Pod liveness checking. + readinessProbe: + type: object + properties: + initialDelaySeconds: + type: integer + minimum: 0 + description: >- + The initial delay before first the health is first + checked. Default to 15 seconds. Minimum value is 0. + timeoutSeconds: + type: integer + minimum: 1 + description: >- + The timeout for each attempted health check. Default to + 5 seconds. Minimum value is 1. + periodSeconds: + type: integer + minimum: 1 + description: >- + How often (in seconds) to perform the probe. Default to + 10 seconds. Minimum value is 1. + successThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive successes for the probe to be + considered successful after having failed. Defaults to + 1. Must be 1 for liveness. Minimum value is 1. + failureThreshold: + type: integer + minimum: 1 + description: >- + Minimum consecutive failures for the probe to be + considered failed after having succeeded. Defaults to 3. + Minimum value is 1. + description: Pod readiness checking. + jvmOptions: + type: object + properties: + '-XX': + additionalProperties: + type: string + type: object + description: A map of -XX options to the JVM. + '-Xmx': + type: string + pattern: '^[0-9]+[mMgG]?$' + description: '-Xmx option to to the JVM.' + '-Xms': + type: string + pattern: '^[0-9]+[mMgG]?$' + description: '-Xms option to to the JVM.' + gcLoggingEnabled: + type: boolean + description: >- + Specifies whether the Garbage Collection logging is + enabled. The default is false. + javaSystemProperties: + type: array + items: + type: object + properties: + name: + type: string + description: The system property name. + value: + type: string + description: The system property value. + description: >- + A map of additional system properties which will be + passed using the `-D` option to the JVM. + description: JVM Options for pods. + jmxOptions: + type: object + properties: + authentication: + type: object + properties: + type: + type: string + enum: + - password + description: >- + Authentication type. Currently the only supported + types are `password`.`password` type creates a + username and protected port with no TLS. + required: + - type + description: >- + Authentication configuration for connecting to the JMX + port. + description: JMX Options. + logging: + type: object + properties: + loggers: + additionalProperties: + type: string + type: object + description: A Map from logger name to logger level. + type: + type: string + enum: + - inline + - external + description: 'Logging type, must be either ''inline'' or ''external''.' + valueFrom: + type: object + properties: + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: >- + Reference to the key in the ConfigMap containing the + configuration. + description: >- + `ConfigMap` entry where the logging configuration is + stored. + required: + - type + description: Logging configuration for Kafka Connect. + clientRackInitImage: + type: string + description: >- + The image of the init container used for initializing the + `client.rack`. + rack: + type: object + properties: + envVarName: + type: string + description: >- + The name of the environment variable that defines the + rack ID. Its value sets the `broker.rack` configuration + for Kafka brokers and the `client.rack` configuration + for Kafka Connect or MirrorMaker 2. + topologyKey: + type: string + example: topology.kubernetes.io/zone + description: >- + A key that matches labels assigned to the Kubernetes + cluster nodes. The value of the label is used to set a + broker's `broker.rack` config, and the `client.rack` + config for Kafka Connect or MirrorMaker 2. + type: + type: string + enum: + - topology-label + - environment-variable + description: >- + Specifies the rack awareness type. Supported types are + `topology-label` and `environment-variable`. + `topology-label` uses a Kubernetes worker node label to + set the `broker.rack` configuration for Kafka brokers + and the `client.rack` configuration for Kafka Connect + and MirrorMaker 2. `environment-variable` uses an + environment variable to set the `broker.rack` + configuration for Kafka brokers and the `client.rack` + configuration for Kafka Connect and MirrorMaker 2. When + not specified, `topology-label` type is used by default. + description: >- + Configuration of the node label which will be used as the + `client.rack` consumer configuration. + x-kubernetes-validations: + - rule: >- + (has(self.type) && self.type != "topology-label") || + self.topologyKey != "" + message: topologyKey property is required + - rule: >- + has(self.type) == false || self.type != + "environment-variable" || self.envVarName != "" + message: envVarName property is required + metricsConfig: + type: object + properties: + type: + type: string + enum: + - jmxPrometheusExporter + - strimziMetricsReporter + description: >- + Metrics type. The supported types are + `jmxPrometheusExporter` and `strimziMetricsReporter`. + Type `jmxPrometheusExporter` uses the Prometheus JMX + Exporter to expose Kafka JMX metrics in Prometheus + format through an HTTP endpoint. Type + `strimziMetricsReporter` uses the Strimzi Metrics + Reporter to directly expose Kafka metrics in Prometheus + format through an HTTP endpoint. + valueFrom: + type: object + properties: + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: >- + Reference to the key in the ConfigMap containing the + configuration. + description: >- + ConfigMap entry where the Prometheus JMX Exporter + configuration is stored. + values: + type: object + properties: + allowList: + type: array + items: + type: string + description: >- + A list of regex patterns to filter the metrics to + collect. Should contain at least one element. + description: Configuration values for the Strimzi Metrics Reporter. + required: + - type + description: Metrics configuration. + x-kubernetes-validations: + - rule: >- + self.type != 'jmxPrometheusExporter' || + has(self.valueFrom) + message: valueFrom property is required + tracing: + type: object + properties: + type: + type: string + enum: + - opentelemetry + description: >- + Type of the tracing used. Currently the only supported + type is `opentelemetry` for OpenTelemetry tracing. As of + Strimzi 0.37.0, `jaeger` type is not supported anymore + and this option is ignored. + required: + - type + description: The configuration of tracing in Kafka Connect. + template: + type: object + properties: + podSet: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + description: Template for Kafka Connect `StrimziPodSet` resource. + pod: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + imagePullSecrets: + type: array + items: + type: object + properties: + name: + type: string + description: >- + List of references to secrets in the same namespace + to use for pulling any of the images used by this + Pod. When the `STRIMZI_IMAGE_PULL_SECRETS` + environment variable in Cluster Operator and the + `imagePullSecrets` option are specified, only the + `imagePullSecrets` variable is used and the + `STRIMZI_IMAGE_PULL_SECRETS` variable is ignored. + securityContext: + type: object + properties: + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + fsGroup: + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + supplementalGroups: + type: array + items: + type: integer + supplementalGroupsPolicy: + type: string + sysctls: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: >- + Configures pod-level security attributes and common + container settings. + terminationGracePeriodSeconds: + type: integer + minimum: 0 + description: >- + The grace period is the duration in seconds after + the processes running in the pod are sent a + termination signal, and the time when the processes + are forcibly halted with a kill signal. Set this + value to longer than the expected cleanup time for + your process. Value must be a non-negative integer. + A zero value indicates delete immediately. You might + need to increase the grace period for very large + Kafka clusters, so that the Kafka brokers have + enough time to transfer their work to another broker + before they are terminated. Defaults to 30 seconds. + affinity: + type: object + properties: + nodeAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + preference: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: object + properties: + nodeSelectorTerms: + type: array + items: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + podAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + podAntiAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + description: The pod's affinity rules. + tolerations: + type: array + items: + type: object + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + description: The pod's tolerations. + topologySpreadConstraints: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + maxSkew: + type: integer + minDomains: + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + description: The pod's topology spread constraints. + priorityClassName: + type: string + description: >- + The name of the priority class used to assign + priority to the pods. + schedulerName: + type: string + description: >- + The name of the scheduler used to dispatch this + `Pod`. If not specified, the default scheduler will + be used. + hostAliases: + type: array + items: + type: object + properties: + hostnames: + type: array + items: + type: string + ip: + type: string + description: >- + The pod's HostAliases. HostAliases is an optional + list of hosts and IPs that will be injected into the + Pod's hosts file if specified. + dnsPolicy: + type: string + enum: + - ClusterFirst + - ClusterFirstWithHostNet + - Default + - None + description: >- + The pod's DNSPolicy. Defaults to `ClusterFirst`. + Valid values are `ClusterFirstWithHostNet`, + `ClusterFirst`, `Default` or `None`. + dnsConfig: + type: object + properties: + nameservers: + type: array + items: + type: string + options: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + searches: + type: array + items: + type: string + description: >- + The pod's DNSConfig. If specified, it will be merged + to the generated DNS configuration based on the + DNSPolicy. + enableServiceLinks: + type: boolean + description: >- + Indicates whether information about services should + be injected into Pod's environment variables. + tmpDirSizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + Defines the total amount of pod memory allocated for + the temporary `EmptyDir` volume `/tmp`. Specify the + allocation in memory units, for example, `100Mi` for + 100 mebibytes. Default value is `5Mi`. The `/tmp` + volume is backed by pod memory, not disk storage, so + avoid setting a high value as it consumes pod memory + resources. + volumes: + type: array + items: + type: object + properties: + name: + type: string + description: Name to use for the volume. Required. + secret: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + optional: + type: boolean + secretName: + type: string + description: '`Secret` to use to populate the volume.' + configMap: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + name: + type: string + optional: + type: boolean + description: '`ConfigMap` to use to populate the volume.' + emptyDir: + type: object + properties: + medium: + type: string + enum: + - Memory + description: >- + Medium represents the type of storage + medium should back this volume. Valid + values are unset or `Memory`. When not + set, it will use the node's default + medium. + sizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + The total amount of local storage required + for this EmptyDir volume (for example + 1Gi). + description: '`EmptyDir` to use to populate the volume.' + persistentVolumeClaim: + type: object + properties: + claimName: + type: string + readOnly: + type: boolean + description: >- + `PersistentVolumeClaim` object to use to + populate the volume. + csi: + type: object + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + type: object + properties: + name: + type: string + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + description: >- + `CSIVolumeSource` object to use to populate + the volume. + image: + type: object + properties: + pullPolicy: + type: string + reference: + type: string + description: >- + `ImageVolumeSource` object to use to populate + the volume. + oneOf: + - properties: + secret: {} + configMap: {} + emptyDir: {} + persistentVolumeClaim: {} + csi: {} + image: {} + description: Additional volumes that can be mounted to the pod. + hostUsers: + type: boolean + description: >- + Use the host user namespace. Optional. Defaults to + `true`. When `true` or not set, the pod runs in the + host user namespace. This is required when the pod + needs features available only in the host namespace, + such as loading kernel modules with + `CAP_SYS_MODULE`.When set to `false`, the pod runs + in a new user namespace. Setting `false` helps + mitigate container breakout vulnerabilities and + allows containers to run as `root` without granting + `root` privileges on the host. This property is + alpha-level in Kubernetes and is supported only by + Kubernetes clusters that enable the + `UserNamespacesSupport` feature. + description: Template for Kafka Connect `Pods`. + apiService: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + ipFamilyPolicy: + type: string + enum: + - SingleStack + - PreferDualStack + - RequireDualStack + description: >- + Specifies the IP Family Policy used by the service. + Available options are `SingleStack`, + `PreferDualStack` and `RequireDualStack`. + `SingleStack` is for a single IP family. + `PreferDualStack` is for two IP families on + dual-stack configured clusters or a single IP family + on single-stack clusters. `RequireDualStack` fails + unless there are two IP families on dual-stack + configured clusters. If unspecified, Kubernetes will + choose the default value based on the service type. + ipFamilies: + type: array + items: + type: string + enum: + - IPv4 + - IPv6 + description: >- + Specifies the IP Families used by the service. + Available options are `IPv4` and `IPv6`. If + unspecified, Kubernetes will choose the default + value based on the `ipFamilyPolicy` setting. + description: Template for Kafka Connect API `Service`. + headlessService: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + ipFamilyPolicy: + type: string + enum: + - SingleStack + - PreferDualStack + - RequireDualStack + description: >- + Specifies the IP Family Policy used by the service. + Available options are `SingleStack`, + `PreferDualStack` and `RequireDualStack`. + `SingleStack` is for a single IP family. + `PreferDualStack` is for two IP families on + dual-stack configured clusters or a single IP family + on single-stack clusters. `RequireDualStack` fails + unless there are two IP families on dual-stack + configured clusters. If unspecified, Kubernetes will + choose the default value based on the service type. + ipFamilies: + type: array + items: + type: string + enum: + - IPv4 + - IPv6 + description: >- + Specifies the IP Families used by the service. + Available options are `IPv4` and `IPv6`. If + unspecified, Kubernetes will choose the default + value based on the `ipFamilyPolicy` setting. + description: Template for Kafka Connect headless `Service`. + connectContainer: + type: object + properties: + env: + type: array + items: + type: object + properties: + name: + type: string + description: The environment variable key. + value: + type: string + description: The environment variable value. + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a secret. + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a config map. + oneOf: + - properties: + secretKeyRef: {} + required: + - secretKeyRef + - properties: + configMapKeyRef: {} + required: + - configMapKeyRef + description: >- + Reference to the secret or config map property + to which the environment variable is set. + oneOf: + - properties: + value: {} + required: + - value + - properties: + valueFrom: {} + required: + - valueFrom + description: >- + Environment variables which should be applied to the + container. + securityContext: + type: object + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + capabilities: + type: object + properties: + add: + type: array + items: + type: string + drop: + type: array + items: + type: string + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: Security context for the container. + volumeMounts: + type: array + items: + type: object + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + description: >- + Additional volume mounts which should be applied to + the container. + description: Template for the Kafka Connect container. + initContainer: + type: object + properties: + env: + type: array + items: + type: object + properties: + name: + type: string + description: The environment variable key. + value: + type: string + description: The environment variable value. + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a secret. + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a config map. + oneOf: + - properties: + secretKeyRef: {} + required: + - secretKeyRef + - properties: + configMapKeyRef: {} + required: + - configMapKeyRef + description: >- + Reference to the secret or config map property + to which the environment variable is set. + oneOf: + - properties: + value: {} + required: + - value + - properties: + valueFrom: {} + required: + - valueFrom + description: >- + Environment variables which should be applied to the + container. + securityContext: + type: object + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + capabilities: + type: object + properties: + add: + type: array + items: + type: string + drop: + type: array + items: + type: string + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: Security context for the container. + volumeMounts: + type: array + items: + type: object + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + description: >- + Additional volume mounts which should be applied to + the container. + description: Template for the Kafka init container. + podDisruptionBudget: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: >- + Metadata to apply to the + `PodDisruptionBudgetTemplate` resource. + maxUnavailable: + type: integer + minimum: 0 + description: >- + Maximum number of unavailable pods to allow + automatic Pod eviction. A Pod eviction is allowed + when the `maxUnavailable` number of pods or fewer + are unavailable after the eviction. Setting this + value to 0 prevents all voluntary evictions, so the + pods must be evicted manually. Defaults to 1. + description: Template for Kafka Connect `PodDisruptionBudget`. + serviceAccount: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + description: Template for the Kafka Connect service account. + clusterRoleBinding: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + description: Template for the Kafka Connect ClusterRoleBinding. + buildPod: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + imagePullSecrets: + type: array + items: + type: object + properties: + name: + type: string + description: >- + List of references to secrets in the same namespace + to use for pulling any of the images used by this + Pod. When the `STRIMZI_IMAGE_PULL_SECRETS` + environment variable in Cluster Operator and the + `imagePullSecrets` option are specified, only the + `imagePullSecrets` variable is used and the + `STRIMZI_IMAGE_PULL_SECRETS` variable is ignored. + securityContext: + type: object + properties: + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + fsGroup: + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + supplementalGroups: + type: array + items: + type: integer + supplementalGroupsPolicy: + type: string + sysctls: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: >- + Configures pod-level security attributes and common + container settings. + terminationGracePeriodSeconds: + type: integer + minimum: 0 + description: >- + The grace period is the duration in seconds after + the processes running in the pod are sent a + termination signal, and the time when the processes + are forcibly halted with a kill signal. Set this + value to longer than the expected cleanup time for + your process. Value must be a non-negative integer. + A zero value indicates delete immediately. You might + need to increase the grace period for very large + Kafka clusters, so that the Kafka brokers have + enough time to transfer their work to another broker + before they are terminated. Defaults to 30 seconds. + affinity: + type: object + properties: + nodeAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + preference: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: object + properties: + nodeSelectorTerms: + type: array + items: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchFields: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + podAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + podAntiAffinity: + type: object + properties: + preferredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + podAffinityTerm: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + weight: + type: integer + requiredDuringSchedulingIgnoredDuringExecution: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + mismatchLabelKeys: + type: array + items: + type: string + namespaceSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + namespaces: + type: array + items: + type: string + topologyKey: + type: string + description: The pod's affinity rules. + tolerations: + type: array + items: + type: object + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + description: The pod's tolerations. + topologySpreadConstraints: + type: array + items: + type: object + properties: + labelSelector: + type: object + properties: + matchExpressions: + type: array + items: + type: object + properties: + key: + type: string + operator: + type: string + values: + type: array + items: + type: string + matchLabels: + additionalProperties: + type: string + type: object + matchLabelKeys: + type: array + items: + type: string + maxSkew: + type: integer + minDomains: + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + description: The pod's topology spread constraints. + priorityClassName: + type: string + description: >- + The name of the priority class used to assign + priority to the pods. + schedulerName: + type: string + description: >- + The name of the scheduler used to dispatch this + `Pod`. If not specified, the default scheduler will + be used. + hostAliases: + type: array + items: + type: object + properties: + hostnames: + type: array + items: + type: string + ip: + type: string + description: >- + The pod's HostAliases. HostAliases is an optional + list of hosts and IPs that will be injected into the + Pod's hosts file if specified. + dnsPolicy: + type: string + enum: + - ClusterFirst + - ClusterFirstWithHostNet + - Default + - None + description: >- + The pod's DNSPolicy. Defaults to `ClusterFirst`. + Valid values are `ClusterFirstWithHostNet`, + `ClusterFirst`, `Default` or `None`. + dnsConfig: + type: object + properties: + nameservers: + type: array + items: + type: string + options: + type: array + items: + type: object + properties: + name: + type: string + value: + type: string + searches: + type: array + items: + type: string + description: >- + The pod's DNSConfig. If specified, it will be merged + to the generated DNS configuration based on the + DNSPolicy. + enableServiceLinks: + type: boolean + description: >- + Indicates whether information about services should + be injected into Pod's environment variables. + tmpDirSizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + Defines the total amount of pod memory allocated for + the temporary `EmptyDir` volume `/tmp`. Specify the + allocation in memory units, for example, `100Mi` for + 100 mebibytes. Default value is `5Mi`. The `/tmp` + volume is backed by pod memory, not disk storage, so + avoid setting a high value as it consumes pod memory + resources. + volumes: + type: array + items: + type: object + properties: + name: + type: string + description: Name to use for the volume. Required. + secret: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + optional: + type: boolean + secretName: + type: string + description: '`Secret` to use to populate the volume.' + configMap: + type: object + properties: + defaultMode: + type: integer + items: + type: array + items: + type: object + properties: + key: + type: string + mode: + type: integer + path: + type: string + name: + type: string + optional: + type: boolean + description: '`ConfigMap` to use to populate the volume.' + emptyDir: + type: object + properties: + medium: + type: string + enum: + - Memory + description: >- + Medium represents the type of storage + medium should back this volume. Valid + values are unset or `Memory`. When not + set, it will use the node's default + medium. + sizeLimit: + type: string + pattern: '^([0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$' + description: >- + The total amount of local storage required + for this EmptyDir volume (for example + 1Gi). + description: '`EmptyDir` to use to populate the volume.' + persistentVolumeClaim: + type: object + properties: + claimName: + type: string + readOnly: + type: boolean + description: >- + `PersistentVolumeClaim` object to use to + populate the volume. + csi: + type: object + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + type: object + properties: + name: + type: string + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + description: >- + `CSIVolumeSource` object to use to populate + the volume. + image: + type: object + properties: + pullPolicy: + type: string + reference: + type: string + description: >- + `ImageVolumeSource` object to use to populate + the volume. + oneOf: + - properties: + secret: {} + configMap: {} + emptyDir: {} + persistentVolumeClaim: {} + csi: {} + image: {} + description: Additional volumes that can be mounted to the pod. + hostUsers: + type: boolean + description: >- + Use the host user namespace. Optional. Defaults to + `true`. When `true` or not set, the pod runs in the + host user namespace. This is required when the pod + needs features available only in the host namespace, + such as loading kernel modules with + `CAP_SYS_MODULE`.When set to `false`, the pod runs + in a new user namespace. Setting `false` helps + mitigate container breakout vulnerabilities and + allows containers to run as `root` without granting + `root` privileges on the host. This property is + alpha-level in Kubernetes and is supported only by + Kubernetes clusters that enable the + `UserNamespacesSupport` feature. + description: >- + Template for Kafka Connect Build `Pods`. The build pod + is used only on Kubernetes. + buildContainer: + type: object + properties: + env: + type: array + items: + type: object + properties: + name: + type: string + description: The environment variable key. + value: + type: string + description: The environment variable value. + valueFrom: + type: object + properties: + secretKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a secret. + configMapKeyRef: + type: object + properties: + key: + type: string + name: + type: string + optional: + type: boolean + description: Reference to a key in a config map. + oneOf: + - properties: + secretKeyRef: {} + required: + - secretKeyRef + - properties: + configMapKeyRef: {} + required: + - configMapKeyRef + description: >- + Reference to the secret or config map property + to which the environment variable is set. + oneOf: + - properties: + value: {} + required: + - value + - properties: + valueFrom: {} + required: + - valueFrom + description: >- + Environment variables which should be applied to the + container. + securityContext: + type: object + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + capabilities: + type: object + properties: + add: + type: array + items: + type: string + drop: + type: array + items: + type: string + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + type: object + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + seccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string + windowsOptions: + type: object + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + description: Security context for the container. + volumeMounts: + type: array + items: + type: object + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + description: >- + Additional volume mounts which should be applied to + the container. + description: >- + Template for the Kafka Connect Build container. The + build container is used only on Kubernetes. + buildConfig: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: >- + Metadata to apply to the + `PodDisruptionBudgetTemplate` resource. + pullSecret: + type: string + description: >- + Container Registry Secret with the credentials for + pulling the base image. + description: >- + Template for the Kafka Connect BuildConfig used to build + new container images. The BuildConfig is used only on + OpenShift. + buildServiceAccount: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + description: Template for the Kafka Connect Build service account. + jmxSecret: + type: object + properties: + metadata: + type: object + properties: + labels: + additionalProperties: + type: string + type: object + description: Labels added to the Kubernetes resource. + annotations: + additionalProperties: + type: string + type: object + description: Annotations added to the Kubernetes resource. + description: Metadata applied to the resource. + description: >- + Template for Secret of the Kafka Connect Cluster JMX + authentication. + description: >- + Template for Kafka Connect and Kafka MirrorMaker 2 + resources. The template allows users to specify how the + `Pods`, `Service`, and other services are generated. + required: + - replicas + - target + - mirrors + description: The specification of the Kafka MirrorMaker 2 cluster. + status: + type: object + properties: + conditions: + type: array + items: + type: object + properties: + type: + type: string + description: >- + The unique identifier of a condition, used to + distinguish between other conditions in the resource. + status: + type: string + description: >- + The status of the condition, either True, False or + Unknown. + lastTransitionTime: + type: string + description: >- + Last time the condition of a type changed from one + status to another. The required format is + 'yyyy-MM-ddTHH:mm:ssZ', in the UTC time zone. + reason: + type: string + description: >- + The reason for the condition's last transition (a + single word in CamelCase). + message: + type: string + description: >- + Human-readable message indicating details about the + condition's last transition. + description: List of status conditions. + observedGeneration: + type: integer + description: >- + The generation of the CRD that was last reconciled by the + operator. + url: + type: string + description: >- + The URL of the REST API endpoint for managing and monitoring + Kafka Connect connectors. + connectors: + type: array + items: + x-kubernetes-preserve-unknown-fields: true + type: object + description: >- + List of MirrorMaker 2 connector statuses, as reported by the + Kafka Connect REST API. + autoRestartStatuses: + type: array + items: + type: object + properties: + count: + type: integer + description: >- + The number of times the connector or task is + restarted. + connectorName: + type: string + description: The name of the connector being restarted. + lastRestartTimestamp: + type: string + description: >- + The last time the automatic restart was attempted. The + required format is 'yyyy-MM-ddTHH:mm:ssZ' in the UTC + time zone. + description: List of MirrorMaker 2 connector auto restart statuses. + connectorPlugins: + type: array + items: + type: object + properties: + class: + type: string + description: The class of the connector plugin. + type: + type: string + description: >- + The type of the connector plugin. The available types + are `sink` and `source`. + version: + type: string + description: The version of the connector plugin. + description: >- + The list of connector plugins available in this Kafka + Connect deployment. + labelSelector: + type: string + description: Label selector for pods providing this resource. + replicas: + type: integer + description: >- + The current number of pods being used to provide this + resource. + description: The status of the Kafka MirrorMaker 2 cluster. + required: + - spec + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: kafkatopics.kafka.strimzi.io + labels: + app: strimzi + strimzi.io/crd-install: 'true' +spec: + group: kafka.strimzi.io + names: + kind: KafkaTopic + listKind: KafkaTopicList + singular: kafkatopic + plural: kafkatopics + shortNames: + - kt + categories: + - strimzi + scope: Namespaced + conversion: + strategy: None + versions: + - name: v1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Cluster + description: The name of the Kafka cluster this topic belongs to + jsonPath: .metadata.labels.strimzi\.io/cluster + type: string + - name: Partitions + description: The desired number of partitions in the topic + jsonPath: .spec.partitions + type: integer + - name: Replication factor + description: The desired number of replicas of each partition + jsonPath: .spec.replicas + type: integer + - name: Ready + description: The state of the custom resource + jsonPath: '.status.conditions[?(@.type=="Ready")].status' + type: string + schema: + openAPIV3Schema: + type: object + properties: + apiVersion: + type: string + description: >- + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the + latest internal value, and may reject unrecognized values. More + info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + kind: + type: string + description: >- + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the + client submits requests to. Cannot be updated. In CamelCase. + More info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + metadata: + type: object + spec: + type: object + properties: + topicName: + type: string + description: >- + The name of the topic. When absent this will default to the + metadata.name of the topic. It is recommended to not set + this unless the topic name is not a valid Kubernetes + resource name. + partitions: + type: integer + minimum: 1 + description: >- + The number of partitions the topic should have. This cannot + be decreased after topic creation. It can be increased after + topic creation, but it is important to understand the + consequences that has, especially for topics with semantic + partitioning. When absent this will default to the broker + configuration for `num.partitions`. + replicas: + type: integer + minimum: 1 + maximum: 32767 + description: >- + The number of replicas the topic should have. When absent + this will default to the broker configuration for + `default.replication.factor`. + config: + x-kubernetes-preserve-unknown-fields: true + type: object + description: The topic configuration. + description: The specification of the topic. + status: + type: object + properties: + conditions: + type: array + items: + type: object + properties: + type: + type: string + description: >- + The unique identifier of a condition, used to + distinguish between other conditions in the resource. + status: + type: string + description: >- + The status of the condition, either True, False or + Unknown. + lastTransitionTime: + type: string + description: >- + Last time the condition of a type changed from one + status to another. The required format is + 'yyyy-MM-ddTHH:mm:ssZ', in the UTC time zone. + reason: + type: string + description: >- + The reason for the condition's last transition (a + single word in CamelCase). + message: + type: string + description: >- + Human-readable message indicating details about the + condition's last transition. + description: List of status conditions. + observedGeneration: + type: integer + description: >- + The generation of the CRD that was last reconciled by the + operator. + topicName: + type: string + description: Topic name. + topicId: + type: string + description: >- + The topic's id. For a KafkaTopic with the ready condition, + this will change only if the topic gets deleted and + recreated with the same name. + replicasChange: + type: object + properties: + targetReplicas: + type: integer + description: >- + The target replicas value requested by the user. This + may be different from .spec.replicas when a change is + ongoing. + state: + type: string + enum: + - pending + - ongoing + description: >- + Current state of the replicas change operation. This can + be `pending`, when the change has been requested, or + `ongoing`, when the change has been successfully + submitted to Cruise Control. + message: + type: string + description: >- + Message for the user related to the replicas change + request. This may contain transient error messages that + would disappear on periodic reconciliations. + sessionId: + type: string + description: >- + The session identifier for replicas change requests + pertaining to this KafkaTopic resource. This is used by + the Topic Operator to track the status of `ongoing` + replicas change operations. + description: Replication factor change status. + description: The status of the topic. + required: + - spec + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: strimzi-cluster-operator + labels: + app: strimzi + namespace: kafka From 6dfe544a1981a907da337c63b302bec66d5a5e5d Mon Sep 17 00:00:00 2001 From: Emmanuel Hugonnet Date: Mon, 13 Apr 2026 13:22:34 +0200 Subject: [PATCH 24/37] feat: Replace Jackson with Gson for json (de) serialization (#789) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replacing Jackson with Gson for json (de)serialization Fixes # 🦕 --------- Signed-off-by: Emmanuel Hugonnet Co-authored-by: Kabir Khan --- boms/sdk/pom.xml | 257 +++++ boms/test-utils/pom.xml | 35 + client/transport/jsonrpc/pom.xml | 14 + .../transport/jsonrpc/JSONRPCTransport.java | 53 +- .../jsonrpc/sse/SSEEventListener.java | 49 +- .../jsonrpc/JsonStreamingMessages.java | 3 +- .../transport/rest/RestErrorMapper.java | 17 +- .../client/transport/rest/RestTransport.java | 19 +- .../examples/helloworld/HelloWorldClient.java | 7 +- examples/helloworld/server/pom.xml | 2 +- .../pom.xml | 2 +- ...paDatabasePushNotificationConfigStore.java | 2 +- .../jpa/JpaPushNotificationConfig.java | 10 +- .../core/ReplicatedEventQueueItem.java | 19 - .../core/EventSerializationTest.java | 44 +- .../core/ReplicatedQueueManagerTest.java | 10 +- .../ReactiveMessagingReplicationStrategy.java | 8 +- ...ctiveMessagingReplicationStrategyTest.java | 4 +- .../KafkaReplicationIntegrationTest.java | 6 +- .../tests/TestKafkaEventConsumer.java | 4 +- extras/task-store-database-jpa/pom.xml | 2 +- .../database/jpa/JpaDatabaseTaskStore.java | 2 +- .../taskstore/database/jpa/JpaTask.java | 10 +- .../jpa/JpaDatabaseTaskStoreTest.java | 2 +- .../io/a2a/client/http/A2ACardResolver.java | 10 +- .../a2a/client/http/A2ACardResolverTest.java | 11 +- pom.xml | 29 +- reference/common/pom.xml | 2 +- .../server/grpc/quarkus/A2ATestResource.java | 12 +- reference/jsonrpc/pom.xml | 2 +- .../server/apps/quarkus/A2AServerRoutes.java | 122 ++- .../server/apps/quarkus/A2ATestRoutes.java | 12 +- reference/rest/pom.xml | 2 +- .../server/rest/quarkus/A2ATestRoutes.java | 12 +- server-common/pom.xml | 8 - .../tasks/BasePushNotificationSender.java | 10 +- .../a2a/server/events/EventConsumerTest.java | 14 +- .../io/a2a/server/events/EventQueueTest.java | 22 +- .../AbstractA2ARequestHandlerTest.java | 12 +- ...MemoryPushNotificationConfigStoreTest.java | 2 +- .../server/tasks/InMemoryTaskStoreTest.java | 4 +- .../tasks/PushNotificationSenderTest.java | 11 +- .../io/a2a/server/tasks/TaskManagerTest.java | 2 +- spec-grpc/pom.xml | 4 + .../java/io/a2a/grpc/utils/JSONRPCUtils.java | 546 +++++++++++ .../io/a2a/grpc/utils/JSONRPCUtilsTest.java | 247 +++++ spec/pom.xml | 9 +- .../io/a2a/json/JsonMappingException.java | 102 ++ .../io/a2a/json/JsonProcessingException.java | 55 ++ spec/src/main/java/io/a2a/json/JsonUtil.java | 904 ++++++++++++++++++ .../main/java/io/a2a/json/package-info.java | 8 + .../java/io/a2a/spec/A2AClientJSONError.java | 25 + .../main/java/io/a2a/spec/A2AErrorCodes.java | 22 + .../io/a2a/spec/APIKeySecurityScheme.java | 17 +- .../java/io/a2a/spec/AgentCapabilities.java | 5 - spec/src/main/java/io/a2a/spec/AgentCard.java | 4 - .../java/io/a2a/spec/AgentCardSignature.java | 9 +- .../main/java/io/a2a/spec/AgentInterface.java | 7 +- .../main/java/io/a2a/spec/AgentProvider.java | 4 - .../src/main/java/io/a2a/spec/AgentSkill.java | 4 - spec/src/main/java/io/a2a/spec/Artifact.java | 4 - ...ticatedExtendedCardNotConfiguredError.java | 17 +- .../java/io/a2a/spec/AuthenticationInfo.java | 4 - .../a2a/spec/AuthorizationCodeOAuthFlow.java | 4 - .../java/io/a2a/spec/CancelTaskRequest.java | 10 +- .../java/io/a2a/spec/CancelTaskResponse.java | 12 +- .../a2a/spec/ClientCredentialsOAuthFlow.java | 4 - .../spec/ContentTypeNotSupportedError.java | 18 +- spec/src/main/java/io/a2a/spec/DataPart.java | 13 +- ...eleteTaskPushNotificationConfigParams.java | 4 - ...leteTaskPushNotificationConfigRequest.java | 12 +- ...eteTaskPushNotificationConfigResponse.java | 14 +- spec/src/main/java/io/a2a/spec/EventKind.java | 35 +- .../main/java/io/a2a/spec/FileContent.java | 27 +- .../io/a2a/spec/FileContentDeserializer.java | 38 - spec/src/main/java/io/a2a/spec/FilePart.java | 11 +- .../main/java/io/a2a/spec/FileWithBytes.java | 5 - .../main/java/io/a2a/spec/FileWithUri.java | 5 - .../GetAuthenticatedExtendedCardRequest.java | 10 +- .../GetAuthenticatedExtendedCardResponse.java | 12 +- .../GetTaskPushNotificationConfigParams.java | 4 - .../GetTaskPushNotificationConfigRequest.java | 10 +- ...GetTaskPushNotificationConfigResponse.java | 12 +- .../main/java/io/a2a/spec/GetTaskRequest.java | 10 +- .../java/io/a2a/spec/GetTaskResponse.java | 11 +- .../io/a2a/spec/HTTPAuthSecurityScheme.java | 15 +- .../io/a2a/spec/IdJsonMappingException.java | 6 +- .../java/io/a2a/spec/ImplicitOAuthFlow.java | 4 - .../main/java/io/a2a/spec/InternalError.java | 16 +- .../a2a/spec/InvalidAgentResponseError.java | 50 +- .../java/io/a2a/spec/InvalidParamsError.java | 43 +- .../java/io/a2a/spec/InvalidRequestError.java | 42 +- .../java/io/a2a/spec/JSONErrorResponse.java | 20 +- .../main/java/io/a2a/spec/JSONParseError.java | 35 +- .../main/java/io/a2a/spec/JSONRPCError.java | 26 +- .../io/a2a/spec/JSONRPCErrorDeserializer.java | 59 -- .../io/a2a/spec/JSONRPCErrorResponse.java | 22 +- .../io/a2a/spec/JSONRPCErrorSerializer.java | 29 - .../main/java/io/a2a/spec/JSONRPCRequest.java | 5 - .../spec/JSONRPCRequestDeserializerBase.java | 89 -- .../java/io/a2a/spec/JSONRPCResponse.java | 5 - .../spec/JSONRPCVoidResponseSerializer.java | 32 - .../ListTaskPushNotificationConfigParams.java | 5 - ...ListTaskPushNotificationConfigRequest.java | 12 +- ...istTaskPushNotificationConfigResponse.java | 12 +- spec/src/main/java/io/a2a/spec/Message.java | 29 +- .../io/a2a/spec/MessageSendConfiguration.java | 4 - .../java/io/a2a/spec/MessageSendParams.java | 4 - .../java/io/a2a/spec/MethodNotFoundError.java | 23 +- .../io/a2a/spec/MutualTLSSecurityScheme.java | 12 +- .../a2a/spec/NonStreamingJSONRPCRequest.java | 7 - ...onStreamingJSONRPCRequestDeserializer.java | 58 -- .../io/a2a/spec/OAuth2SecurityScheme.java | 13 +- .../src/main/java/io/a2a/spec/OAuthFlows.java | 5 - .../a2a/spec/OpenIdConnectSecurityScheme.java | 12 +- spec/src/main/java/io/a2a/spec/Part.java | 21 +- .../java/io/a2a/spec/PasswordOAuthFlow.java | 5 - .../PushNotificationAuthenticationInfo.java | 5 - .../io/a2a/spec/PushNotificationConfig.java | 6 - .../PushNotificationNotSupportedError.java | 14 +- .../main/java/io/a2a/spec/SecurityScheme.java | 16 - .../java/io/a2a/spec/SendMessageRequest.java | 23 +- .../java/io/a2a/spec/SendMessageResponse.java | 14 +- .../a2a/spec/SendStreamingMessageRequest.java | 10 +- .../spec/SendStreamingMessageResponse.java | 12 +- .../SetTaskPushNotificationConfigRequest.java | 11 +- ...SetTaskPushNotificationConfigResponse.java | 12 +- .../java/io/a2a/spec/StreamingEventKind.java | 49 +- .../io/a2a/spec/StreamingJSONRPCRequest.java | 8 +- .../StreamingJSONRPCRequestDeserializer.java | 41 - spec/src/main/java/io/a2a/spec/Task.java | 18 +- .../io/a2a/spec/TaskArtifactUpdateEvent.java | 16 +- .../main/java/io/a2a/spec/TaskIdParams.java | 4 - .../io/a2a/spec/TaskNotCancelableError.java | 16 +- .../java/io/a2a/spec/TaskNotFoundError.java | 18 +- .../a2a/spec/TaskPushNotificationConfig.java | 4 - .../java/io/a2a/spec/TaskQueryParams.java | 6 - .../a2a/spec/TaskResubscriptionRequest.java | 10 +- spec/src/main/java/io/a2a/spec/TaskState.java | 23 +- .../src/main/java/io/a2a/spec/TaskStatus.java | 8 +- .../io/a2a/spec/TaskStatusUpdateEvent.java | 16 +- spec/src/main/java/io/a2a/spec/TextPart.java | 11 +- .../java/io/a2a/spec/TransportProtocol.java | 21 +- .../a2a/spec/UnsupportedOperationError.java | 17 +- spec/src/main/java/io/a2a/util/Utils.java | 101 +- .../spec/JSONRPCErrorSerializationTest.java | 16 +- .../io/a2a/spec/SubTypeSerializationTest.java | 37 +- .../io/a2a/spec/TaskDeserializationTest.java | 90 -- .../io/a2a/spec/TaskSerializationTest.java | 713 ++++++++++++++ .../test/java/io/a2a/spec/TaskStatusTest.java | 94 -- tck/pom.xml | 2 +- .../apps/common/A2AGsonObjectMapper.java | 38 + .../apps/common/AbstractA2AServerTest.java | 26 +- .../server/apps/common/TestHttpClient.java | 9 +- .../jsonrpc/handler/JSONRPCHandler.java | 2 +- transport/rest/pom.xml | 4 - .../transport/rest/handler/RestHandler.java | 15 +- 157 files changed, 3753 insertions(+), 1681 deletions(-) create mode 100644 boms/sdk/pom.xml create mode 100644 boms/test-utils/pom.xml create mode 100644 spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java create mode 100644 spec-grpc/src/test/java/io/a2a/grpc/utils/JSONRPCUtilsTest.java create mode 100644 spec/src/main/java/io/a2a/json/JsonMappingException.java create mode 100644 spec/src/main/java/io/a2a/json/JsonProcessingException.java create mode 100644 spec/src/main/java/io/a2a/json/JsonUtil.java create mode 100644 spec/src/main/java/io/a2a/json/package-info.java create mode 100644 spec/src/main/java/io/a2a/spec/A2AErrorCodes.java delete mode 100644 spec/src/main/java/io/a2a/spec/FileContentDeserializer.java delete mode 100644 spec/src/main/java/io/a2a/spec/JSONRPCErrorDeserializer.java delete mode 100644 spec/src/main/java/io/a2a/spec/JSONRPCErrorSerializer.java delete mode 100644 spec/src/main/java/io/a2a/spec/JSONRPCRequestDeserializerBase.java delete mode 100644 spec/src/main/java/io/a2a/spec/JSONRPCVoidResponseSerializer.java delete mode 100644 spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequestDeserializer.java delete mode 100644 spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequestDeserializer.java delete mode 100644 spec/src/test/java/io/a2a/spec/TaskDeserializationTest.java create mode 100644 spec/src/test/java/io/a2a/spec/TaskSerializationTest.java delete mode 100644 spec/src/test/java/io/a2a/spec/TaskStatusTest.java create mode 100644 tests/server-common/src/test/java/io/a2a/server/apps/common/A2AGsonObjectMapper.java diff --git a/boms/sdk/pom.xml b/boms/sdk/pom.xml new file mode 100644 index 000000000..c0b51b73a --- /dev/null +++ b/boms/sdk/pom.xml @@ -0,0 +1,257 @@ + + + 4.0.0 + + + io.github.a2asdk + a2a-java-sdk-parent + 0.4.0.Alpha1-SNAPSHOT + ../../pom.xml + + + a2a-java-sdk-bom + pom + + A2A Java SDK - BOM + Bill of Materials (BOM) for A2A Java SDK core modules and dependencies + + + + + + + + ${project.groupId} + a2a-java-sdk-spec + ${project.version} + + + ${project.groupId} + a2a-java-sdk-spec-grpc + ${project.version} + + + + + ${project.groupId} + a2a-java-sdk-common + ${project.version} + + + ${project.groupId} + a2a-java-sdk-http-client + ${project.version} + + + + + ${project.groupId} + a2a-java-sdk-server-common + ${project.version} + + + + + ${project.groupId} + a2a-java-sdk-microprofile-config + ${project.version} + + + + + ${project.groupId} + a2a-java-sdk-client + ${project.version} + + + ${project.groupId} + a2a-java-sdk-client-transport-spi + ${project.version} + + + ${project.groupId} + a2a-java-sdk-client-transport-jsonrpc + ${project.version} + + + ${project.groupId} + a2a-java-sdk-client-transport-grpc + ${project.version} + + + ${project.groupId} + a2a-java-sdk-client-transport-rest + ${project.version} + + + + + ${project.groupId} + a2a-java-sdk-transport-grpc + ${project.version} + + + ${project.groupId} + a2a-java-sdk-transport-jsonrpc + ${project.version} + + + ${project.groupId} + a2a-java-sdk-transport-rest + ${project.version} + + + + + ${project.groupId} + a2a-java-sdk-tests-server-common + ${project.version} + test + + + ${project.groupId} + a2a-java-sdk-tests-server-common + test-jar + test + ${project.version} + + + ${project.groupId} + a2a-java-sdk-server-common + test-jar + test + ${project.version} + + + + + io.grpc + grpc-bom + ${grpc.version} + pom + import + + + org.slf4j + slf4j-bom + ${slf4j.version} + pom + import + + + + + com.google.protobuf + protobuf-java + ${protobuf-java.version} + + + io.smallrye.reactive + mutiny-zero + ${mutiny-zero.version} + + + + + jakarta.enterprise + jakarta.enterprise.cdi-api + ${jakarta.enterprise.cdi-api.version} + + + jakarta.inject + jakarta.inject-api + ${jakarta.inject.jakarta.inject-api.version} + + + jakarta.json + jakarta.json-api + ${jakarta.json-api.version} + provided + + + jakarta.ws.rs + jakarta.ws.rs-api + ${jakarta.ws.rs-api.version} + provided + + + + + org.jspecify + jspecify + 1.0.0 + provided + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + io.rest-assured + rest-assured + ${rest-assured.version} + test + + + org.mockito + mockito-core + ${mockito-core.version} + test + + + org.mock-server + mockserver-netty + ${mockserver.version} + test + + + ch.qos.logback + logback-classic + ${logback.version} + test + + + + + + + + org.apache.maven.plugins + maven-invoker-plugin + 3.8.0 + + ${project.build.directory}/it + ${project.build.directory}/local-repo + src/it/settings.xml + + clean + verify + + false + true + invoker.properties + + + io.github.a2asdk:a2a-java-bom-test-utils:${project.version}:jar + + + + + integration-test + + install + run + + + + + + + + diff --git a/boms/test-utils/pom.xml b/boms/test-utils/pom.xml new file mode 100644 index 000000000..5c0cdff0a --- /dev/null +++ b/boms/test-utils/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + + io.github.a2asdk + a2a-java-sdk-parent + 0.4.0.Alpha1-SNAPSHOT + ../../pom.xml + + + a2a-java-bom-test-utils + jar + + A2A Java SDK - BOM Test Utilities + Shared utilities for BOM integration tests + + + 17 + 17 + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.1 + + + + diff --git a/client/transport/jsonrpc/pom.xml b/client/transport/jsonrpc/pom.xml index e14025c5b..04e2d096f 100644 --- a/client/transport/jsonrpc/pom.xml +++ b/client/transport/jsonrpc/pom.xml @@ -33,6 +33,20 @@ ${project.groupId} a2a-java-sdk-spec + + ${project.groupId} + a2a-java-sdk-spec-grpc + + + com.google.protobuf + protobuf-java-util + provided + + + com.google.protobuf + protobuf-java + provided + org.junit.jupiter junit-jupiter-api diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java index 108c80558..92e6d86b9 100644 --- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java +++ b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java @@ -7,9 +7,8 @@ import java.util.Map; import java.util.function.Consumer; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; - +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import io.a2a.client.http.A2ACardResolver; import io.a2a.client.transport.spi.interceptors.ClientCallContext; import io.a2a.client.transport.spi.interceptors.ClientCallInterceptor; @@ -58,18 +57,16 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; -import io.a2a.util.Utils; - public class JSONRPCTransport implements ClientTransport { - private static final TypeReference SEND_MESSAGE_RESPONSE_REFERENCE = new TypeReference<>() {}; - private static final TypeReference GET_TASK_RESPONSE_REFERENCE = new TypeReference<>() {}; - private static final TypeReference CANCEL_TASK_RESPONSE_REFERENCE = new TypeReference<>() {}; - private static final TypeReference GET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = new TypeReference<>() {}; - private static final TypeReference SET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = new TypeReference<>() {}; - private static final TypeReference LIST_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = new TypeReference<>() {}; - private static final TypeReference DELETE_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = new TypeReference<>() {}; - private static final TypeReference GET_AUTHENTICATED_EXTENDED_CARD_RESPONSE_REFERENCE = new TypeReference<>() {}; + private static final Class SEND_MESSAGE_RESPONSE_REFERENCE = SendMessageResponse.class; + private static final Class GET_TASK_RESPONSE_REFERENCE = GetTaskResponse.class; + private static final Class CANCEL_TASK_RESPONSE_REFERENCE = CancelTaskResponse.class; + private static final Class GET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = GetTaskPushNotificationConfigResponse.class; + private static final Class SET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = SetTaskPushNotificationConfigResponse.class; + private static final Class LIST_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = ListTaskPushNotificationConfigResponse.class; + private static final Class DELETE_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = DeleteTaskPushNotificationConfigResponse.class; + private static final Class GET_AUTHENTICATED_EXTENDED_CARD_RESPONSE_REFERENCE = GetAuthenticatedExtendedCardResponse.class; private final A2AHttpClient httpClient; private final String agentUrl; @@ -112,7 +109,7 @@ public EventKind sendMessage(MessageSendParams request, ClientCallContext contex return response.getResult(); } catch (A2AClientException e) { throw e; - } catch (IOException | InterruptedException e) { + } catch (IOException | InterruptedException | JsonProcessingException e) { throw new A2AClientException("Failed to send message: " + e, e); } } @@ -147,6 +144,8 @@ public void sendMessageStreaming(MessageSendParams request, Consumer listTaskPushNotificationConfigurations( return response.getResult(); } catch (A2AClientException e) { throw e; - } catch (IOException | InterruptedException e) { + } catch (IOException | InterruptedException | JsonProcessingException e) { throw new A2AClientException("Failed to list task push notification configs: " + e, e); } } @@ -290,7 +289,7 @@ public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationC unmarshalResponse(httpResponseBody, DELETE_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE); } catch (A2AClientException e) { throw e; - } catch (IOException | InterruptedException e) { + } catch (IOException | InterruptedException | JsonProcessingException e) { throw new A2AClientException("Failed to delete task push notification configs: " + e, e); } } @@ -326,6 +325,8 @@ public void resubscribe(TaskIdParams request, Consumer event throw new A2AClientException("Failed to send task resubscription request: " + e, e); } catch (InterruptedException e) { throw new A2AClientException("Task resubscription request timed out: " + e, e); + } catch (JsonProcessingException e) { + throw new A2AClientException("Failed to process JSON for task resubscription request: " + e, e); } } @@ -357,7 +358,7 @@ public AgentCard getAgentCard(ClientCallContext context) throws A2AClientExcepti agentCard = response.getResult(); needsExtendedCard = false; return agentCard; - } catch (IOException | InterruptedException e) { + } catch (IOException | InterruptedException | JsonProcessingException e) { throw new A2AClientException("Failed to get authenticated extended agent card: " + e, e); } } catch(A2AClientError e){ @@ -382,7 +383,7 @@ private PayloadAndHeaders applyInterceptors(String methodName, Object payload, return payloadAndHeaders; } - private String sendPostRequest(PayloadAndHeaders payloadAndHeaders) throws IOException, InterruptedException { + private String sendPostRequest(PayloadAndHeaders payloadAndHeaders) throws IOException, InterruptedException, JsonProcessingException { A2AHttpClient.PostBuilder builder = createPostBuilder(payloadAndHeaders); A2AHttpResponse response = builder.post(); if (!response.success()) { @@ -395,7 +396,7 @@ private A2AHttpClient.PostBuilder createPostBuilder(PayloadAndHeaders payloadAnd A2AHttpClient.PostBuilder postBuilder = httpClient.createPost() .url(agentUrl) .addHeader("Content-Type", "application/json") - .body(Utils.OBJECT_MAPPER.writeValueAsString(payloadAndHeaders.getPayload())); + .body(JsonUtil.toJson(payloadAndHeaders.getPayload())); if (payloadAndHeaders.getHeaders() != null) { for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) { @@ -406,9 +407,9 @@ private A2AHttpClient.PostBuilder createPostBuilder(PayloadAndHeaders payloadAnd return postBuilder; } - private > T unmarshalResponse(String response, TypeReference typeReference) + private > T unmarshalResponse(String response, Class responseClass) throws A2AClientException, JsonProcessingException { - T value = Utils.unmarshalFrom(response, typeReference); + T value = JsonUtil.fromJson(response, responseClass); JSONRPCError error = value.getError(); if (error != null) { throw new A2AClientException(error.getMessage() + (error.getData() != null ? ": " + error.getData() : ""), error); @@ -419,4 +420,4 @@ private > T unmarshalResponse(String response, Type private Map getHttpHeaders(ClientCallContext context) { return context != null ? context.getHeaders() : null; } -} \ No newline at end of file +} diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java index ff4b93f3c..7ed6585f9 100644 --- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java +++ b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java @@ -1,7 +1,10 @@ package io.a2a.client.transport.jsonrpc.sse; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import io.a2a.spec.JSONRPCError; import io.a2a.spec.StreamingEventKind; import io.a2a.spec.TaskStatusUpdateEvent; @@ -10,8 +13,6 @@ import java.util.function.Consumer; import java.util.logging.Logger; -import static io.a2a.util.Utils.OBJECT_MAPPER; - public class SSEEventListener { private static final Logger log = Logger.getLogger(SSEEventListener.class.getName()); private final Consumer eventHandler; @@ -26,9 +27,11 @@ public SSEEventListener(Consumer eventHandler, public void onMessage(String message, Future completableFuture) { try { - handleMessage(OBJECT_MAPPER.readTree(message),completableFuture); - } catch (JsonProcessingException e) { + handleMessage(JsonParser.parseString(message).getAsJsonObject(), completableFuture); + } catch (JsonSyntaxException e) { log.warning("Failed to parse JSON message: " + message); + } catch (JsonProcessingException e) { + log.warning("Failed to process JSON message: " + message); } } @@ -57,26 +60,22 @@ public void onComplete() { } } - private void handleMessage(JsonNode jsonNode, Future future) { - try { - if (jsonNode.has("error")) { - JSONRPCError error = OBJECT_MAPPER.treeToValue(jsonNode.get("error"), JSONRPCError.class); - if (errorHandler != null) { - errorHandler.accept(error); - } - } else if (jsonNode.has("result")) { - // result can be a Task, Message, TaskStatusUpdateEvent, or TaskArtifactUpdateEvent - JsonNode result = jsonNode.path("result"); - StreamingEventKind event = OBJECT_MAPPER.treeToValue(result, StreamingEventKind.class); - eventHandler.accept(event); - if (event instanceof TaskStatusUpdateEvent && ((TaskStatusUpdateEvent) event).isFinal()) { - future.cancel(true); // close SSE channel - } - } else { - throw new IllegalArgumentException("Unknown message type"); + private void handleMessage(JsonObject jsonObject, Future future) throws JsonProcessingException { + if (jsonObject.has("error")) { + JSONRPCError error = JsonUtil.fromJson(jsonObject.get("error").toString(), JSONRPCError.class); + if (errorHandler != null) { + errorHandler.accept(error); } - } catch (JsonProcessingException e) { - throw new RuntimeException(e); + } else if (jsonObject.has("result")) { + // result can be a Task, Message, TaskStatusUpdateEvent, or TaskArtifactUpdateEvent + String resultJson = jsonObject.get("result").toString(); + StreamingEventKind event = JsonUtil.fromJson(resultJson, StreamingEventKind.class); + eventHandler.accept(event); + if (event instanceof TaskStatusUpdateEvent && ((TaskStatusUpdateEvent) event).isFinal()) { + future.cancel(true); // close SSE channel + } + } else { + throw new IllegalArgumentException("Unknown message type"); } } diff --git a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonStreamingMessages.java b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonStreamingMessages.java index 909955e81..5930af5ed 100644 --- a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonStreamingMessages.java +++ b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonStreamingMessages.java @@ -89,8 +89,7 @@ public class JsonStreamingMessages { ] } } - } - }"""; + }"""; public static final String STREAMING_ERROR_EVENT = """ data: { diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java index 965cc2962..c23eab4a0 100644 --- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java +++ b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java @@ -1,10 +1,9 @@ package io.a2a.client.transport.rest; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.gson.JsonObject; import io.a2a.client.http.A2AHttpResponse; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import io.a2a.spec.A2AClientException; import io.a2a.spec.AuthenticatedExtendedCardNotConfiguredError; import io.a2a.spec.ContentTypeNotSupportedError; @@ -26,8 +25,6 @@ */ public class RestErrorMapper { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new JavaTimeModule()); - public static A2AClientException mapRestError(A2AHttpResponse response) { return RestErrorMapper.mapRestError(response.body(), response.status()); } @@ -35,9 +32,9 @@ public static A2AClientException mapRestError(A2AHttpResponse response) { public static A2AClientException mapRestError(String body, int code) { try { if (body != null && !body.isBlank()) { - JsonNode node = OBJECT_MAPPER.readTree(body); - String className = node.findValue("error").asText(); - String errorMessage = node.findValue("message").asText(); + JsonObject node = JsonUtil.fromJson(body, JsonObject.class); + String className = node.has("error") ? node.get("error").getAsString() : ""; + String errorMessage = node.has("message") ? node.get("message").getAsString() : ""; return mapRestError(className, errorMessage, code); } return mapRestError("", "", code); @@ -50,7 +47,7 @@ public static A2AClientException mapRestError(String body, int code) { public static A2AClientException mapRestError(String className, String errorMessage, int code) { return switch (className) { case "io.a2a.spec.TaskNotFoundError" -> new A2AClientException(errorMessage, new TaskNotFoundError()); - case "io.a2a.spec.AuthenticatedExtendedCardNotConfiguredError" -> new A2AClientException(errorMessage, new AuthenticatedExtendedCardNotConfiguredError()); + case "io.a2a.spec.AuthenticatedExtendedCardNotConfiguredError" -> new A2AClientException(errorMessage, new AuthenticatedExtendedCardNotConfiguredError(null, errorMessage, null)); case "io.a2a.spec.ContentTypeNotSupportedError" -> new A2AClientException(errorMessage, new ContentTypeNotSupportedError(null, null, errorMessage)); case "io.a2a.spec.InternalError" -> new A2AClientException(errorMessage, new InternalError(errorMessage)); case "io.a2a.spec.InvalidAgentResponseError" -> new A2AClientException(errorMessage, new InvalidAgentResponseError(null, null, errorMessage)); diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java index 5b67d97e0..af2df8df2 100644 --- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java +++ b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java @@ -2,7 +2,7 @@ import static io.a2a.util.Assert.checkNotNullParam; -import com.fasterxml.jackson.core.JsonProcessingException; +import io.a2a.json.JsonProcessingException; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.MessageOrBuilder; import com.google.protobuf.util.JsonFormat; @@ -20,6 +20,7 @@ import io.a2a.grpc.GetTaskPushNotificationConfigRequest; import io.a2a.grpc.GetTaskRequest; import io.a2a.grpc.ListTaskPushNotificationConfigRequest; +import io.a2a.json.JsonUtil; import io.a2a.spec.TaskPushNotificationConfig; import io.a2a.spec.A2AClientException; import io.a2a.spec.AgentCard; @@ -86,7 +87,7 @@ public EventKind sendMessage(MessageSendParams messageSendParams, @Nullable Clie throw new A2AClientException("Failed to send message, wrong response:" + httpResponseBody); } catch (A2AClientException e) { throw e; - } catch (IOException | InterruptedException e) { + } catch (IOException | InterruptedException | JsonProcessingException e) { throw new A2AClientException("Failed to send message: " + e, e); } } @@ -113,6 +114,8 @@ public void sendMessageStreaming(MessageSendParams messageSendParams, Consumer event throw new A2AClientException("Failed to send streaming message request: " + e, e); } catch (InterruptedException e) { throw new A2AClientException("Send streaming message request timed out: " + e, e); + } catch (JsonProcessingException e) { + throw new A2AClientException("Failed to process JSON for streaming message request: " + e, e); } } @@ -329,10 +334,10 @@ public AgentCard getAgentCard(@Nullable ClientCallContext context) throws A2ACli throw RestErrorMapper.mapRestError(response); } String httpResponseBody = response.body(); - agentCard = Utils.OBJECT_MAPPER.readValue(httpResponseBody, AgentCard.class); + agentCard = JsonUtil.fromJson(httpResponseBody, AgentCard.class); needsExtendedCard = false; return agentCard; - } catch (IOException | InterruptedException e) { + } catch (IOException | InterruptedException | JsonProcessingException e) { throw new A2AClientException("Failed to get authenticated extended agent card: " + e, e); } catch (A2AClientError e) { throw new A2AClientException("Failed to get agent card: " + e, e); @@ -356,7 +361,7 @@ private PayloadAndHeaders applyInterceptors(String methodName, @Nullable Message return payloadAndHeaders; } - private String sendPostRequest(String url, PayloadAndHeaders payloadAndHeaders) throws IOException, InterruptedException { + private String sendPostRequest(String url, PayloadAndHeaders payloadAndHeaders) throws IOException, InterruptedException, JsonProcessingException { A2AHttpClient.PostBuilder builder = createPostBuilder(url, payloadAndHeaders); A2AHttpResponse response = builder.post(); if (!response.success()) { diff --git a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldClient.java b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldClient.java index a82438a35..17ce1aef7 100644 --- a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldClient.java +++ b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldClient.java @@ -8,7 +8,6 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; -import com.fasterxml.jackson.databind.ObjectMapper; import io.a2a.A2A; import io.a2a.client.Client; @@ -18,6 +17,7 @@ import io.a2a.client.http.A2ACardResolver; import io.a2a.client.transport.jsonrpc.JSONRPCTransport; import io.a2a.client.transport.jsonrpc.JSONRPCTransportConfig; +import io.a2a.json.JsonUtil; import io.a2a.spec.AgentCard; import io.a2a.spec.Message; import io.a2a.spec.Part; @@ -31,14 +31,13 @@ public class HelloWorldClient { private static final String SERVER_URL = "http://localhost:9999"; private static final String MESSAGE_TEXT = "how much is 10 USD in INR?"; - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); public static void main(String[] args) { try { AgentCard finalAgentCard = null; AgentCard publicAgentCard = new A2ACardResolver("http://localhost:9999").getAgentCard(); System.out.println("Successfully fetched public agent card:"); - System.out.println(OBJECT_MAPPER.writeValueAsString(publicAgentCard)); + System.out.println(JsonUtil.toJson(publicAgentCard)); System.out.println("Using public agent card for client initialization (default)."); finalAgentCard = publicAgentCard; @@ -48,7 +47,7 @@ public static void main(String[] args) { authHeaders.put("Authorization", "Bearer dummy-token-for-extended-card"); AgentCard extendedAgentCard = A2A.getAgentCard(SERVER_URL, "/agent/authenticatedExtendedCard", authHeaders); System.out.println("Successfully fetched authenticated extended agent card:"); - System.out.println(OBJECT_MAPPER.writeValueAsString(extendedAgentCard)); + System.out.println(JsonUtil.toJson(extendedAgentCard)); System.out.println("Using AUTHENTICATED EXTENDED agent card for client initialization."); finalAgentCard = extendedAgentCard; } else { diff --git a/examples/helloworld/server/pom.xml b/examples/helloworld/server/pom.xml index 3638d9252..345f927d9 100644 --- a/examples/helloworld/server/pom.xml +++ b/examples/helloworld/server/pom.xml @@ -26,7 +26,7 @@ io.quarkus - quarkus-resteasy-jackson + quarkus-resteasy provided diff --git a/extras/push-notification-config-store-database-jpa/pom.xml b/extras/push-notification-config-store-database-jpa/pom.xml index cec16f4c2..bdbf2f7f0 100644 --- a/extras/push-notification-config-store-database-jpa/pom.xml +++ b/extras/push-notification-config-store-database-jpa/pom.xml @@ -50,7 +50,7 @@ io.quarkus - quarkus-rest-client-jackson + quarkus-rest-client test diff --git a/extras/push-notification-config-store-database-jpa/src/main/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java b/extras/push-notification-config-store-database-jpa/src/main/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java index f002256cd..835ffea38 100644 --- a/extras/push-notification-config-store-database-jpa/src/main/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java +++ b/extras/push-notification-config-store-database-jpa/src/main/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java @@ -9,7 +9,7 @@ import jakarta.persistence.PersistenceContext; import jakarta.transaction.Transactional; -import com.fasterxml.jackson.core.JsonProcessingException; +import io.a2a.json.JsonProcessingException; import io.a2a.server.tasks.PushNotificationConfigStore; import io.a2a.spec.PushNotificationConfig; import org.slf4j.Logger; diff --git a/extras/push-notification-config-store-database-jpa/src/main/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfig.java b/extras/push-notification-config-store-database-jpa/src/main/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfig.java index a55e252b5..225936e47 100644 --- a/extras/push-notification-config-store-database-jpa/src/main/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfig.java +++ b/extras/push-notification-config-store-database-jpa/src/main/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfig.java @@ -6,9 +6,9 @@ import jakarta.persistence.Table; import jakarta.persistence.Transient; -import com.fasterxml.jackson.core.JsonProcessingException; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import io.a2a.spec.PushNotificationConfig; -import io.a2a.util.Utils; @Entity @Table(name = "a2a_push_notification_configs") @@ -46,7 +46,7 @@ public void setConfigJson(String configJson) { public PushNotificationConfig getConfig() throws JsonProcessingException { if (config == null) { - this.config = Utils.unmarshalFrom(configJson, PushNotificationConfig.TYPE_REFERENCE); + this.config = JsonUtil.fromJson(configJson, PushNotificationConfig.class); } return config; } @@ -56,12 +56,12 @@ public void setConfig(PushNotificationConfig config) throws JsonProcessingExcept throw new IllegalArgumentException("Mismatched config id. " + "Expected '" + id.getConfigId() + "'. Got: '" + config.id() + "'"); } - configJson = Utils.OBJECT_MAPPER.writeValueAsString(config); + configJson = JsonUtil.toJson(config); this.config = config; } static JpaPushNotificationConfig createFromConfig(String taskId, PushNotificationConfig config) throws JsonProcessingException { - String json = Utils.OBJECT_MAPPER.writeValueAsString(config); + String json = JsonUtil.toJson(config); JpaPushNotificationConfig jpaPushNotificationConfig = new JpaPushNotificationConfig(new TaskConfigId(taskId, config.id()), json); jpaPushNotificationConfig.config = config; diff --git a/extras/queue-manager-replicated/core/src/main/java/io/a2a/extras/queuemanager/replicated/core/ReplicatedEventQueueItem.java b/extras/queue-manager-replicated/core/src/main/java/io/a2a/extras/queuemanager/replicated/core/ReplicatedEventQueueItem.java index 5eac92d4f..092af8c46 100644 --- a/extras/queue-manager-replicated/core/src/main/java/io/a2a/extras/queuemanager/replicated/core/ReplicatedEventQueueItem.java +++ b/extras/queue-manager-replicated/core/src/main/java/io/a2a/extras/queuemanager/replicated/core/ReplicatedEventQueueItem.java @@ -1,10 +1,5 @@ package io.a2a.extras.queuemanager.replicated.core; -import com.fasterxml.jackson.annotation.JsonGetter; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonSetter; - import io.a2a.server.events.EventQueueItem; import io.a2a.spec.Event; import io.a2a.spec.JSONRPCError; @@ -12,11 +7,7 @@ public class ReplicatedEventQueueItem implements EventQueueItem { private String taskId; - - @JsonInclude(JsonInclude.Include.NON_NULL) private StreamingEventKind event; - - @JsonInclude(JsonInclude.Include.NON_NULL) private JSONRPCError error; private boolean closedEvent; @@ -72,13 +63,10 @@ public void setTaskId(String taskId) { * Get the StreamingEventKind event field (for JSON serialization). * @return the StreamingEventKind event or null */ - @JsonGetter("event") - @JsonInclude(JsonInclude.Include.NON_NULL) public StreamingEventKind getStreamingEvent() { return event; } - @JsonSetter("event") public void setEvent(StreamingEventKind event) { this.event = event; this.error = null; // Clear error when setting event @@ -88,13 +76,10 @@ public void setEvent(StreamingEventKind event) { * Get the JSONRPCError field (for JSON serialization). * @return the JSONRPCError or null */ - @JsonGetter("error") - @JsonInclude(JsonInclude.Include.NON_NULL) public JSONRPCError getErrorObject() { return error; } - @JsonSetter("error") public void setError(JSONRPCError error) { this.error = error; this.event = null; // Clear event when setting error @@ -105,7 +90,6 @@ public void setError(JSONRPCError error) { * This is the method required by the EventQueueItem interface. * @return the event (StreamingEventKind, JSONRPCError, or QueueClosedEvent) or null if none is set */ - @JsonIgnore @Override public Event getEvent() { if (closedEvent) { @@ -121,7 +105,6 @@ public Event getEvent() { * Indicates this is a replicated event (implements EventQueueItem). * @return always true for replicated events */ - @JsonIgnore @Override public boolean isReplicated() { return true; @@ -148,7 +131,6 @@ public boolean hasError() { * For JSON serialization. * @return true if this is a queue closed event */ - @JsonGetter("closedEvent") public boolean isClosedEvent() { return closedEvent; } @@ -157,7 +139,6 @@ public boolean isClosedEvent() { * Set the closed event flag (for JSON deserialization). * @param closedEvent true if this is a queue closed event */ - @JsonSetter("closedEvent") public void setClosedEvent(boolean closedEvent) { this.closedEvent = closedEvent; if (closedEvent) { diff --git a/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/EventSerializationTest.java b/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/EventSerializationTest.java index 7e18ca4a8..3f7759153 100644 --- a/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/EventSerializationTest.java +++ b/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/EventSerializationTest.java @@ -9,7 +9,7 @@ import java.util.List; -import com.fasterxml.jackson.core.JsonProcessingException; +import io.a2a.json.JsonProcessingException; import io.a2a.server.events.QueueClosedEvent; import io.a2a.spec.Artifact; import io.a2a.spec.Event; @@ -32,7 +32,7 @@ import io.a2a.spec.TaskStatusUpdateEvent; import io.a2a.spec.TextPart; import io.a2a.spec.UnsupportedOperationError; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; import org.junit.jupiter.api.Test; /** @@ -52,12 +52,12 @@ public void testTaskSerialization() throws JsonProcessingException { .build(); // Test serialization as Event - String json = Utils.OBJECT_MAPPER.writeValueAsString((Event) originalTask); + String json = JsonUtil.toJson(originalTask); assertTrue(json.contains("\"kind\":\"task\""), "JSON should contain task kind"); assertTrue(json.contains("\"id\":\"test-task-123\""), "JSON should contain task ID"); // Test deserialization back to StreamingEventKind - StreamingEventKind deserializedEvent = Utils.OBJECT_MAPPER.readValue(json, StreamingEventKind.class); + StreamingEventKind deserializedEvent = JsonUtil.fromJson(json, StreamingEventKind.class); assertInstanceOf(Task.class, deserializedEvent, "Should deserialize to Task"); Task deserializedTask = (Task) deserializedEvent; @@ -67,7 +67,7 @@ public void testTaskSerialization() throws JsonProcessingException { assertEquals(originalTask.getStatus().state(), deserializedTask.getStatus().state()); // Test as StreamingEventKind - StreamingEventKind deserializedAsStreaming = Utils.OBJECT_MAPPER.readValue(json, StreamingEventKind.class); + StreamingEventKind deserializedAsStreaming = JsonUtil.fromJson(json, StreamingEventKind.class); assertInstanceOf(Task.class, deserializedAsStreaming, "Should deserialize to Task as StreamingEventKind"); } @@ -83,12 +83,12 @@ public void testMessageSerialization() throws JsonProcessingException { .build(); // Test serialization as Event - String json = Utils.OBJECT_MAPPER.writeValueAsString((Event) originalMessage); + String json = JsonUtil.toJson(originalMessage); assertTrue(json.contains("\"kind\":\"message\""), "JSON should contain message kind"); assertTrue(json.contains("\"taskId\":\"test-task-789\""), "JSON should contain task ID"); // Test deserialization back to StreamingEventKind - StreamingEventKind deserializedEvent = Utils.OBJECT_MAPPER.readValue(json, StreamingEventKind.class); + StreamingEventKind deserializedEvent = JsonUtil.fromJson(json, StreamingEventKind.class); assertInstanceOf(Message.class, deserializedEvent, "Should deserialize to Message"); Message deserializedMessage = (Message) deserializedEvent; @@ -98,7 +98,7 @@ public void testMessageSerialization() throws JsonProcessingException { assertEquals(originalMessage.getParts().size(), deserializedMessage.getParts().size()); // Test as StreamingEventKind - StreamingEventKind deserializedAsStreaming = Utils.OBJECT_MAPPER.readValue(json, StreamingEventKind.class); + StreamingEventKind deserializedAsStreaming = JsonUtil.fromJson(json, StreamingEventKind.class); assertInstanceOf(Message.class, deserializedAsStreaming, "Should deserialize to Message as StreamingEventKind"); } @@ -114,13 +114,13 @@ public void testTaskStatusUpdateEventSerialization() throws JsonProcessingExcept .build(); // Test serialization as Event - String json = Utils.OBJECT_MAPPER.writeValueAsString((Event) originalEvent); + String json = JsonUtil.toJson((Event) originalEvent); assertTrue(json.contains("\"kind\":\"status-update\""), "JSON should contain status-update kind"); assertTrue(json.contains("\"taskId\":\"test-task-abc\""), "JSON should contain task ID"); assertTrue(json.contains("\"final\":true"), "JSON should contain final flag"); // Test deserialization back to StreamingEventKind - StreamingEventKind deserializedEvent = Utils.OBJECT_MAPPER.readValue(json, StreamingEventKind.class); + StreamingEventKind deserializedEvent = JsonUtil.fromJson(json, StreamingEventKind.class); assertInstanceOf(TaskStatusUpdateEvent.class, deserializedEvent, "Should deserialize to TaskStatusUpdateEvent"); TaskStatusUpdateEvent deserializedStatusEvent = (TaskStatusUpdateEvent) deserializedEvent; @@ -131,7 +131,7 @@ public void testTaskStatusUpdateEventSerialization() throws JsonProcessingExcept assertEquals(originalEvent.isFinal(), deserializedStatusEvent.isFinal()); // Test as StreamingEventKind - StreamingEventKind deserializedAsStreaming = Utils.OBJECT_MAPPER.readValue(json, StreamingEventKind.class); + StreamingEventKind deserializedAsStreaming = JsonUtil.fromJson(json, StreamingEventKind.class); assertInstanceOf(TaskStatusUpdateEvent.class, deserializedAsStreaming, "Should deserialize to TaskStatusUpdateEvent as StreamingEventKind"); } @@ -147,13 +147,13 @@ public void testTaskArtifactUpdateEventSerialization() throws JsonProcessingExce .build(); // Test serialization as Event - String json = Utils.OBJECT_MAPPER.writeValueAsString((Event) originalEvent); + String json = JsonUtil.toJson((Event) originalEvent); assertTrue(json.contains("\"kind\":\"artifact-update\""), "JSON should contain artifact-update kind"); assertTrue(json.contains("\"taskId\":\"test-task-xyz\""), "JSON should contain task ID"); assertTrue(json.contains("\"test-artifact-123\""), "JSON should contain artifact ID"); // Test deserialization back to StreamingEventKind - StreamingEventKind deserializedEvent = Utils.OBJECT_MAPPER.readValue(json, StreamingEventKind.class); + StreamingEventKind deserializedEvent = JsonUtil.fromJson(json, StreamingEventKind.class); assertInstanceOf(TaskArtifactUpdateEvent.class, deserializedEvent, "Should deserialize to TaskArtifactUpdateEvent"); TaskArtifactUpdateEvent deserializedArtifactEvent = (TaskArtifactUpdateEvent) deserializedEvent; @@ -164,7 +164,7 @@ public void testTaskArtifactUpdateEventSerialization() throws JsonProcessingExce assertEquals(originalEvent.getArtifact().name(), deserializedArtifactEvent.getArtifact().name()); // Test as StreamingEventKind - StreamingEventKind deserializedAsStreaming = Utils.OBJECT_MAPPER.readValue(json, StreamingEventKind.class); + StreamingEventKind deserializedAsStreaming = JsonUtil.fromJson(json, StreamingEventKind.class); assertInstanceOf(TaskArtifactUpdateEvent.class, deserializedAsStreaming, "Should deserialize to TaskArtifactUpdateEvent as StreamingEventKind"); } @@ -186,11 +186,11 @@ public void testJSONRPCErrorSubclassesSerialization() throws JsonProcessingExcep for (JSONRPCError originalError : errors) { // Test serialization - String json = Utils.OBJECT_MAPPER.writeValueAsString(originalError); + String json = JsonUtil.toJson(originalError); assertTrue(json.contains("\"message\""), "JSON should contain error message for " + originalError.getClass().getSimpleName()); // Test deserialization - it's acceptable to deserialize as base JSONRPCError - JSONRPCError deserializedError = Utils.OBJECT_MAPPER.readValue(json, JSONRPCError.class); + JSONRPCError deserializedError = JsonUtil.fromJson(json, JSONRPCError.class); assertNotNull(deserializedError, "Should deserialize successfully for " + originalError.getClass().getSimpleName()); assertEquals(originalError.getMessage(), deserializedError.getMessage(), "Error message should match for " + originalError.getClass().getSimpleName()); assertEquals(originalError.getCode(), deserializedError.getCode(), "Error code should match for " + originalError.getClass().getSimpleName()); @@ -213,14 +213,14 @@ public void testReplicatedEventWithStreamingEventSerialization() throws JsonProc ReplicatedEventQueueItem originalReplicatedEvent = new ReplicatedEventQueueItem("replicated-test-task", statusEvent); // Serialize the ReplicatedEventQueueItem - String json = Utils.OBJECT_MAPPER.writeValueAsString(originalReplicatedEvent); + String json = JsonUtil.toJson(originalReplicatedEvent); assertTrue(json.contains("\"taskId\":\"replicated-test-task\""), "JSON should contain task ID"); assertTrue(json.contains("\"event\""), "JSON should contain event field"); assertTrue(json.contains("\"kind\":\"status-update\""), "JSON should contain the event kind"); assertFalse(json.contains("\"error\""), "JSON should not contain error field"); // Deserialize the ReplicatedEventQueueItem - ReplicatedEventQueueItem deserializedReplicatedEvent = Utils.OBJECT_MAPPER.readValue(json, ReplicatedEventQueueItem.class); + ReplicatedEventQueueItem deserializedReplicatedEvent = JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); assertEquals(originalReplicatedEvent.getTaskId(), deserializedReplicatedEvent.getTaskId()); // Now we should get the proper type back! @@ -250,14 +250,14 @@ public void testReplicatedEventWithErrorSerialization() throws JsonProcessingExc ReplicatedEventQueueItem originalReplicatedEvent = new ReplicatedEventQueueItem("error-test-task", error); // Serialize the ReplicatedEventQueueItemQueueItem - String json = Utils.OBJECT_MAPPER.writeValueAsString(originalReplicatedEvent); + String json = JsonUtil.toJson(originalReplicatedEvent); assertTrue(json.contains("\"taskId\":\"error-test-task\""), "JSON should contain task ID"); assertTrue(json.contains("\"error\""), "JSON should contain error field"); assertTrue(json.contains("\"message\""), "JSON should contain error message"); assertFalse(json.contains("\"event\""), "JSON should not contain event field"); // Deserialize the ReplicatedEventQueueItem - ReplicatedEventQueueItem deserializedReplicatedEvent = Utils.OBJECT_MAPPER.readValue(json, ReplicatedEventQueueItem.class); + ReplicatedEventQueueItem deserializedReplicatedEvent = JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); assertEquals(originalReplicatedEvent.getTaskId(), deserializedReplicatedEvent.getTaskId()); // Should get the error back @@ -308,14 +308,14 @@ public void testQueueClosedEventSerialization() throws JsonProcessingException { assertFalse(originalReplicatedEvent.hasError(), "Should not have error"); // Serialize the ReplicatedEventQueueItem - String json = Utils.OBJECT_MAPPER.writeValueAsString(originalReplicatedEvent); + String json = JsonUtil.toJson(originalReplicatedEvent); assertTrue(json.contains("\"taskId\":\"" + taskId + "\""), "JSON should contain task ID"); assertTrue(json.contains("\"closedEvent\":true"), "JSON should contain closedEvent flag set to true"); assertFalse(json.contains("\"event\""), "JSON should not contain event field"); assertFalse(json.contains("\"error\""), "JSON should not contain error field"); // Deserialize the ReplicatedEventQueueItem - ReplicatedEventQueueItem deserializedReplicatedEvent = Utils.OBJECT_MAPPER.readValue(json, ReplicatedEventQueueItem.class); + ReplicatedEventQueueItem deserializedReplicatedEvent = JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); assertEquals(taskId, deserializedReplicatedEvent.getTaskId()); // Verify the deserialized item is marked as a closed event diff --git a/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/ReplicatedQueueManagerTest.java b/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/ReplicatedQueueManagerTest.java index 3e2da2f51..42454ea3b 100644 --- a/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/ReplicatedQueueManagerTest.java +++ b/extras/queue-manager-replicated/core/src/test/java/io/a2a/extras/queuemanager/replicated/core/ReplicatedQueueManagerTest.java @@ -27,7 +27,7 @@ import io.a2a.spec.TaskState; import io.a2a.spec.TaskStatus; import io.a2a.spec.TaskStatusUpdateEvent; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -199,14 +199,14 @@ void testReplicatedEventJsonSerialization() throws Exception { ReplicatedEventQueueItem original = new ReplicatedEventQueueItem("json-test-task", originalEvent); // Serialize to JSON - String json = Utils.OBJECT_MAPPER.writeValueAsString(original); + String json = JsonUtil.toJson(original); assertNotNull(json); assertTrue(json.contains("json-test-task")); assertTrue(json.contains("\"event\":{")); assertTrue(json.contains("\"kind\":\"status-update\"")); // Deserialize back - ReplicatedEventQueueItem deserialized = Utils.OBJECT_MAPPER.readValue(json, ReplicatedEventQueueItem.class); + ReplicatedEventQueueItem deserialized = JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); assertNotNull(deserialized); assertEquals("json-test-task", deserialized.getTaskId()); assertNotNull(deserialized.getEvent()); @@ -427,13 +427,13 @@ void testQueueClosedEventJsonSerialization() throws Exception { assertFalse(original.hasError(), "Should not have error"); // Serialize to JSON - String json = Utils.OBJECT_MAPPER.writeValueAsString(original); + String json = JsonUtil.toJson(original); assertNotNull(json); assertTrue(json.contains(taskId), "JSON should contain taskId"); assertTrue(json.contains("\"closedEvent\":true"), "JSON should contain closedEvent flag"); // Deserialize back - ReplicatedEventQueueItem deserialized = Utils.OBJECT_MAPPER.readValue(json, ReplicatedEventQueueItem.class); + ReplicatedEventQueueItem deserialized = JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); assertNotNull(deserialized); assertEquals(taskId, deserialized.getTaskId()); assertTrue(deserialized.isClosedEvent(), "Deserialized should be marked as closed event"); diff --git a/extras/queue-manager-replicated/replication-mp-reactive/src/main/java/io/a2a/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategy.java b/extras/queue-manager-replicated/replication-mp-reactive/src/main/java/io/a2a/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategy.java index 04238c27d..26797bc5f 100644 --- a/extras/queue-manager-replicated/replication-mp-reactive/src/main/java/io/a2a/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategy.java +++ b/extras/queue-manager-replicated/replication-mp-reactive/src/main/java/io/a2a/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategy.java @@ -4,10 +4,9 @@ import jakarta.enterprise.event.Event; import jakarta.inject.Inject; -import com.fasterxml.jackson.core.type.TypeReference; import io.a2a.extras.queuemanager.replicated.core.ReplicatedEventQueueItem; import io.a2a.extras.queuemanager.replicated.core.ReplicationStrategy; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; import org.eclipse.microprofile.reactive.messaging.Channel; import org.eclipse.microprofile.reactive.messaging.Emitter; import org.eclipse.microprofile.reactive.messaging.Incoming; @@ -18,7 +17,6 @@ public class ReactiveMessagingReplicationStrategy implements ReplicationStrategy { private static final Logger LOGGER = LoggerFactory.getLogger(ReactiveMessagingReplicationStrategy.class); - private static final TypeReference REPLICATED_EVENT_TYPE_REF = new TypeReference() {}; @Inject @Channel("replicated-events-out") @@ -33,7 +31,7 @@ public void send(String taskId, io.a2a.spec.Event event) { try { ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem(taskId, event); - String json = Utils.OBJECT_MAPPER.writeValueAsString(replicatedEvent); + String json = JsonUtil.toJson(replicatedEvent); emitter.send(json); LOGGER.debug("Successfully sent replicated event for task: {}", taskId); } catch (Exception e) { @@ -47,7 +45,7 @@ public void onReplicatedEvent(String jsonMessage) { LOGGER.debug("Received replicated event JSON: {}", jsonMessage); try { - ReplicatedEventQueueItem replicatedEvent = Utils.unmarshalFrom(jsonMessage, REPLICATED_EVENT_TYPE_REF); + ReplicatedEventQueueItem replicatedEvent = JsonUtil.fromJson(jsonMessage, ReplicatedEventQueueItem.class); LOGGER.debug("Deserialized replicated event for task: {}, event: {}", replicatedEvent.getTaskId(), replicatedEvent.getEvent()); diff --git a/extras/queue-manager-replicated/replication-mp-reactive/src/test/java/io/a2a/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategyTest.java b/extras/queue-manager-replicated/replication-mp-reactive/src/test/java/io/a2a/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategyTest.java index a10b019db..26976d1e3 100644 --- a/extras/queue-manager-replicated/replication-mp-reactive/src/test/java/io/a2a/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategyTest.java +++ b/extras/queue-manager-replicated/replication-mp-reactive/src/test/java/io/a2a/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategyTest.java @@ -19,7 +19,7 @@ import io.a2a.spec.TaskStatus; import io.a2a.spec.TaskState; import io.a2a.spec.TaskStatusUpdateEvent; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; @ExtendWith(MockitoExtension.class) class ReactiveMessagingReplicationStrategyTest { @@ -54,7 +54,7 @@ private String createValidJsonMessage(String taskId, String contextId) throws Ex .isFinal(false) .build(); ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem(taskId, event); - return Utils.OBJECT_MAPPER.writeValueAsString(replicatedEvent); + return JsonUtil.toJson(replicatedEvent); } @Test diff --git a/extras/queue-manager-replicated/tests-single-instance/src/test/java/io/a2a/extras/queuemanager/replicated/tests/KafkaReplicationIntegrationTest.java b/extras/queue-manager-replicated/tests-single-instance/src/test/java/io/a2a/extras/queuemanager/replicated/tests/KafkaReplicationIntegrationTest.java index 52f5676d3..08c198281 100644 --- a/extras/queue-manager-replicated/tests-single-instance/src/test/java/io/a2a/extras/queuemanager/replicated/tests/KafkaReplicationIntegrationTest.java +++ b/extras/queue-manager-replicated/tests-single-instance/src/test/java/io/a2a/extras/queuemanager/replicated/tests/KafkaReplicationIntegrationTest.java @@ -39,7 +39,7 @@ import io.a2a.spec.TaskStatus; import io.a2a.spec.TaskStatusUpdateEvent; import io.a2a.spec.TextPart; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; import io.quarkus.test.junit.QuarkusTest; import org.eclipse.microprofile.reactive.messaging.Channel; import org.eclipse.microprofile.reactive.messaging.Emitter; @@ -261,7 +261,7 @@ public void testKafkaEventReceivedByA2AServer() throws Exception { .build(); ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem(taskId, statusEvent); - String eventJson = Utils.OBJECT_MAPPER.writeValueAsString(replicatedEvent); + String eventJson = JsonUtil.toJson(replicatedEvent); // Send to Kafka using reactive messaging testEmitter.send(eventJson); @@ -361,7 +361,7 @@ public void testQueueClosedEventTerminatesRemoteSubscribers() throws Exception { // Now manually send a QueueClosedEvent to Kafka to simulate queue closure on another node QueueClosedEvent closedEvent = new QueueClosedEvent(taskId); ReplicatedEventQueueItem replicatedClosedEvent = new ReplicatedEventQueueItem(taskId, closedEvent); - String eventJson = Utils.OBJECT_MAPPER.writeValueAsString(replicatedClosedEvent); + String eventJson = JsonUtil.toJson(replicatedClosedEvent); // Send to Kafka using reactive messaging testEmitter.send(eventJson); diff --git a/extras/queue-manager-replicated/tests-single-instance/src/test/java/io/a2a/extras/queuemanager/replicated/tests/TestKafkaEventConsumer.java b/extras/queue-manager-replicated/tests-single-instance/src/test/java/io/a2a/extras/queuemanager/replicated/tests/TestKafkaEventConsumer.java index 7571a6ba9..9e308ec42 100644 --- a/extras/queue-manager-replicated/tests-single-instance/src/test/java/io/a2a/extras/queuemanager/replicated/tests/TestKafkaEventConsumer.java +++ b/extras/queue-manager-replicated/tests-single-instance/src/test/java/io/a2a/extras/queuemanager/replicated/tests/TestKafkaEventConsumer.java @@ -9,7 +9,7 @@ import org.eclipse.microprofile.reactive.messaging.Incoming; import io.a2a.extras.queuemanager.replicated.core.ReplicatedEventQueueItem; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; import io.quarkus.arc.profile.IfBuildProfile; /** @@ -26,7 +26,7 @@ public class TestKafkaEventConsumer { @Incoming("test-replicated-events-in") public void onTestReplicatedEvent(String jsonMessage) { try { - ReplicatedEventQueueItem event = Utils.OBJECT_MAPPER.readValue(jsonMessage, ReplicatedEventQueueItem.class); + ReplicatedEventQueueItem event = JsonUtil.fromJson(jsonMessage, ReplicatedEventQueueItem.class); receivedEvents.offer(event); // Signal any waiting threads diff --git a/extras/task-store-database-jpa/pom.xml b/extras/task-store-database-jpa/pom.xml index 55a3e619f..c078e3b57 100644 --- a/extras/task-store-database-jpa/pom.xml +++ b/extras/task-store-database-jpa/pom.xml @@ -54,7 +54,7 @@ io.quarkus - quarkus-rest-client-jackson + quarkus-rest-client test diff --git a/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java b/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java index edfbfaf69..23fc83759 100644 --- a/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java +++ b/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java @@ -13,7 +13,7 @@ import jakarta.persistence.PersistenceContext; import jakarta.transaction.Transactional; -import com.fasterxml.jackson.core.JsonProcessingException; +import io.a2a.json.JsonProcessingException; import io.a2a.extras.common.events.TaskFinalizedEvent; import io.a2a.server.config.A2AConfigProvider; import io.a2a.server.tasks.TaskStateProvider; diff --git a/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaTask.java b/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaTask.java index 2b3fc56f1..eb98cc096 100644 --- a/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaTask.java +++ b/extras/task-store-database-jpa/src/main/java/io/a2a/extras/taskstore/database/jpa/JpaTask.java @@ -8,9 +8,9 @@ import jakarta.persistence.Table; import jakarta.persistence.Transient; -import com.fasterxml.jackson.core.JsonProcessingException; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import io.a2a.spec.Task; -import io.a2a.util.Utils; @Entity @Table(name = "a2a_tasks") @@ -76,13 +76,13 @@ public void setFinalizedAt(Instant finalizedAt, boolean isFinalState) { public Task getTask() throws JsonProcessingException { if (task == null) { - this.task = Utils.unmarshalFrom(taskJson, Task.TYPE_REFERENCE); + this.task = JsonUtil.fromJson(taskJson, Task.class); } return task; } public void setTask(Task task) throws JsonProcessingException { - taskJson = Utils.OBJECT_MAPPER.writeValueAsString(task); + taskJson = JsonUtil.toJson(task); if (id == null) { id = task.getId(); } @@ -91,7 +91,7 @@ public void setTask(Task task) throws JsonProcessingException { } static JpaTask createFromTask(Task task) throws JsonProcessingException { - String json = Utils.OBJECT_MAPPER.writeValueAsString(task); + String json = JsonUtil.toJson(task); JpaTask jpaTask = new JpaTask(task.getId(), json); jpaTask.task = task; jpaTask.updateFinalizedTimestamp(task); diff --git a/extras/task-store-database-jpa/src/test/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTest.java b/extras/task-store-database-jpa/src/test/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTest.java index 8c091f87f..30cf0c75a 100644 --- a/extras/task-store-database-jpa/src/test/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTest.java +++ b/extras/task-store-database-jpa/src/test/java/io/a2a/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTest.java @@ -180,7 +180,7 @@ public void testTaskWithComplexMetadata() { assertEquals("test-task-5", retrieved.getId()); assertNotNull(retrieved.getMetadata()); assertEquals("value1", retrieved.getMetadata().get("key1")); - assertEquals(42, retrieved.getMetadata().get("key2")); + assertEquals(42, ((Number)retrieved.getMetadata().get("key2")).intValue()); assertEquals(true, retrieved.getMetadata().get("key3")); } diff --git a/http-client/src/main/java/io/a2a/client/http/A2ACardResolver.java b/http-client/src/main/java/io/a2a/client/http/A2ACardResolver.java index 539f3a741..22af7c615 100644 --- a/http-client/src/main/java/io/a2a/client/http/A2ACardResolver.java +++ b/http-client/src/main/java/io/a2a/client/http/A2ACardResolver.java @@ -1,14 +1,12 @@ package io.a2a.client.http; -import static io.a2a.util.Utils.unmarshalFrom; - import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.Map; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import io.a2a.spec.A2AClientError; import io.a2a.spec.A2AClientJSONError; import io.a2a.spec.AgentCard; @@ -21,8 +19,6 @@ public class A2ACardResolver { private static final String DEFAULT_AGENT_CARD_PATH = "/.well-known/agent-card.json"; - private static final TypeReference AGENT_CARD_TYPE_REFERENCE = new TypeReference<>() {}; - /** * Get the agent card for an A2A agent. * The {@code JdkA2AHttpClient} will be used to fetch the agent card. @@ -106,7 +102,7 @@ public AgentCard getAgentCard() throws A2AClientError, A2AClientJSONError { } try { - return unmarshalFrom(body, AGENT_CARD_TYPE_REFERENCE); + return JsonUtil.fromJson(body, AgentCard.class); } catch (JsonProcessingException e) { throw new A2AClientJSONError("Could not unmarshal agent card response", e); } diff --git a/http-client/src/test/java/io/a2a/client/http/A2ACardResolverTest.java b/http-client/src/test/java/io/a2a/client/http/A2ACardResolverTest.java index 99d26adad..9c2a177ec 100644 --- a/http-client/src/test/java/io/a2a/client/http/A2ACardResolverTest.java +++ b/http-client/src/test/java/io/a2a/client/http/A2ACardResolverTest.java @@ -1,7 +1,5 @@ package io.a2a.client.http; -import static io.a2a.util.Utils.OBJECT_MAPPER; -import static io.a2a.util.Utils.unmarshalFrom; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -10,7 +8,7 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; -import com.fasterxml.jackson.core.type.TypeReference; +import io.a2a.json.JsonUtil; import io.a2a.spec.A2AClientError; import io.a2a.spec.A2AClientJSONError; import io.a2a.spec.AgentCard; @@ -20,7 +18,6 @@ public class A2ACardResolverTest { private static final String AGENT_CARD_PATH = "/.well-known/agent-card.json"; - private static final TypeReference AGENT_CARD_TYPE_REFERENCE = new TypeReference<>() {}; @Test public void testConstructorStripsSlashes() throws Exception { @@ -72,10 +69,10 @@ public void testGetAgentCardSuccess() throws Exception { A2ACardResolver resolver = new A2ACardResolver(client, "http://example.com/"); AgentCard card = resolver.getAgentCard(); - AgentCard expectedCard = unmarshalFrom(JsonMessages.AGENT_CARD, AGENT_CARD_TYPE_REFERENCE); - String expected = OBJECT_MAPPER.writeValueAsString(expectedCard); + AgentCard expectedCard = JsonUtil.fromJson(JsonMessages.AGENT_CARD, AgentCard.class); + String expected = JsonUtil.toJson(expectedCard); - String requestCardString = OBJECT_MAPPER.writeValueAsString(card); + String requestCardString = JsonUtil.toJson(card); assertEquals(expected, requestCardString); } diff --git a/pom.xml b/pom.xml index 4d2557a53..83cff974b 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ 3.8.0 3.2.4 0.8.0 - 2.17.0 + 2.13.2 4.1.0 2.0.1 2.1.3 @@ -58,7 +58,7 @@ 5.15.0 1.1.1 1.7.1 - 4.31.1 + 4.33.1 0.6.1 3.28.2 5.5.1 @@ -192,19 +192,25 @@ import - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} + com.google.protobuf + protobuf-java + ${protobuf-java.version} + + + com.google.code.gson + gson + + - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - ${jackson.version} + com.google.protobuf + protobuf-java-util + ${protobuf-java.version} - com.google.protobuf - protobuf-java - ${protobuf.version} + com.google.code.gson + gson + ${gson.version} io.smallrye.reactive @@ -296,7 +302,6 @@ org.jspecify jspecify - diff --git a/reference/common/pom.xml b/reference/common/pom.xml index f779abd4b..47450b9b9 100644 --- a/reference/common/pom.xml +++ b/reference/common/pom.xml @@ -60,7 +60,7 @@ io.quarkus - quarkus-rest-client-jackson + quarkus-rest-client test diff --git a/reference/grpc/src/test/java/io/a2a/server/grpc/quarkus/A2ATestResource.java b/reference/grpc/src/test/java/io/a2a/server/grpc/quarkus/A2ATestResource.java index 6c24c5166..ef28ada75 100644 --- a/reference/grpc/src/test/java/io/a2a/server/grpc/quarkus/A2ATestResource.java +++ b/reference/grpc/src/test/java/io/a2a/server/grpc/quarkus/A2ATestResource.java @@ -24,7 +24,7 @@ import io.a2a.spec.TaskArtifactUpdateEvent; import io.a2a.spec.TaskStatusUpdateEvent; import io.a2a.transport.grpc.handler.GrpcHandler; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; @Path("/test") @ApplicationScoped @@ -44,7 +44,7 @@ public void init() { @Path("/task") @Consumes(MediaType.APPLICATION_JSON) public Response saveTask(String body) throws Exception { - Task task = Utils.OBJECT_MAPPER.readValue(body, Task.class); + Task task = JsonUtil.fromJson(body, Task.class); testUtilsBean.saveTask(task); return Response.ok().build(); } @@ -57,7 +57,7 @@ public Response getTask(@PathParam("taskId") String taskId) throws Exception { return Response.status(404).build(); } return Response.ok() - .entity(Utils.OBJECT_MAPPER.writeValueAsString(task)) + .entity(JsonUtil.toJson(task)) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) .build(); } @@ -85,7 +85,7 @@ public Response ensureQueue(@PathParam("taskId") String taskId) { @POST @Path("/queue/enqueueTaskStatusUpdateEvent/{taskId}") public Response enqueueTaskStatusUpdateEvent(@PathParam("taskId") String taskId, String body) throws Exception { - TaskStatusUpdateEvent event = Utils.OBJECT_MAPPER.readValue(body, TaskStatusUpdateEvent.class); + TaskStatusUpdateEvent event = JsonUtil.fromJson(body, TaskStatusUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); return Response.ok().build(); } @@ -93,7 +93,7 @@ public Response enqueueTaskStatusUpdateEvent(@PathParam("taskId") String taskId, @POST @Path("/queue/enqueueTaskArtifactUpdateEvent/{taskId}") public Response enqueueTaskArtifactUpdateEvent(@PathParam("taskId") String taskId, String body) throws Exception { - TaskArtifactUpdateEvent event = Utils.OBJECT_MAPPER.readValue(body, TaskArtifactUpdateEvent.class); + TaskArtifactUpdateEvent event = JsonUtil.fromJson(body, TaskArtifactUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); return Response.ok().build(); } @@ -130,7 +130,7 @@ public Response deleteTaskPushNotificationConfig(@PathParam("taskId") String tas @Path("/task/{taskId}") @Consumes(MediaType.APPLICATION_JSON) public Response savePushNotificationConfigInStore(@PathParam("taskId") String taskId, String body) throws Exception { - PushNotificationConfig notificationConfig = Utils.OBJECT_MAPPER.readValue(body, PushNotificationConfig.class); + PushNotificationConfig notificationConfig = JsonUtil.fromJson(body, PushNotificationConfig.class); if (notificationConfig == null) { return Response.status(404).build(); } diff --git a/reference/jsonrpc/pom.xml b/reference/jsonrpc/pom.xml index 22173625a..aa402bed1 100644 --- a/reference/jsonrpc/pom.xml +++ b/reference/jsonrpc/pom.xml @@ -64,7 +64,7 @@ io.quarkus - quarkus-rest-client-jackson + quarkus-rest-client test diff --git a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java index df726ad88..7a4f8e76f 100644 --- a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java +++ b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java @@ -19,10 +19,9 @@ import jakarta.inject.Inject; import jakarta.inject.Singleton; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.io.JsonEOFException; -import com.fasterxml.jackson.databind.JsonNode; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import io.a2a.json.JsonUtil; import io.a2a.common.A2AHeaders; import io.a2a.server.ServerCallContext; import io.a2a.server.auth.UnauthenticatedUser; @@ -35,19 +34,17 @@ import io.a2a.spec.GetAuthenticatedExtendedCardRequest; import io.a2a.spec.GetTaskPushNotificationConfigRequest; import io.a2a.spec.GetTaskRequest; -import io.a2a.spec.IdJsonMappingException; import io.a2a.spec.InternalError; import io.a2a.spec.InvalidParamsError; -import io.a2a.spec.InvalidParamsJsonMappingException; import io.a2a.spec.InvalidRequestError; import io.a2a.spec.JSONParseError; import io.a2a.spec.JSONRPCError; import io.a2a.spec.JSONRPCErrorResponse; +import io.a2a.spec.JSONRPCMessage; import io.a2a.spec.JSONRPCRequest; import io.a2a.spec.JSONRPCResponse; import io.a2a.spec.ListTaskPushNotificationConfigRequest; import io.a2a.spec.MethodNotFoundError; -import io.a2a.spec.MethodNotFoundJsonMappingException; import io.a2a.spec.NonStreamingJSONRPCRequest; import io.a2a.spec.SendMessageRequest; import io.a2a.spec.SendStreamingMessageRequest; @@ -94,26 +91,57 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) { JSONRPCResponse nonStreamingResponse = null; Multi> streamingResponse = null; JSONRPCErrorResponse error = null; + Object requestId = null; try { - JsonNode node = Utils.OBJECT_MAPPER.readTree(body); - JsonNode method = node != null ? node.get("method") : null; - streaming = method != null && (SendStreamingMessageRequest.METHOD.equals(method.asText()) - || TaskResubscriptionRequest.METHOD.equals(method.asText())); - String methodName = (method != null && method.isTextual()) ? method.asText() : null; - if (methodName != null) { - context.getState().put(METHOD_NAME_KEY, methodName); + com.google.gson.JsonObject node; + try { + node = JsonParser.parseString(body).getAsJsonObject(); + } catch (Exception e) { + throw new JSONParseError(e.getMessage()); } + + // Validate jsonrpc field + com.google.gson.JsonElement jsonrpcElement = node.get("jsonrpc"); + if (jsonrpcElement == null || !jsonrpcElement.isJsonPrimitive() + || !JSONRPCMessage.JSONRPC_VERSION.equals(jsonrpcElement.getAsString())) { + throw new InvalidRequestError("Invalid JSON-RPC request: missing or invalid 'jsonrpc' field"); + } + + // Validate id field (must be string, number, or null — not an object or array) + com.google.gson.JsonElement idElement = node.get("id"); + if (idElement != null && !idElement.isJsonNull() && !idElement.isJsonPrimitive()) { + throw new InvalidRequestError("Invalid JSON-RPC request: 'id' must be a string, number, or null"); + } + if (idElement != null && !idElement.isJsonNull() && idElement.isJsonPrimitive()) { + com.google.gson.JsonPrimitive idPrimitive = idElement.getAsJsonPrimitive(); + requestId = idPrimitive.isNumber() ? idPrimitive.getAsLong() : idPrimitive.getAsString(); + } + + // Validate method field + com.google.gson.JsonElement methodElement = node.get("method"); + if (methodElement == null || !methodElement.isJsonPrimitive()) { + throw new InvalidRequestError("Invalid JSON-RPC request: missing or invalid 'method' field"); + } + + String methodName = methodElement.getAsString(); + context.getState().put(METHOD_NAME_KEY, methodName); + + streaming = SendStreamingMessageRequest.METHOD.equals(methodName) + || TaskResubscriptionRequest.METHOD.equals(methodName); + if (streaming) { - StreamingJSONRPCRequest request = Utils.OBJECT_MAPPER.treeToValue(node, StreamingJSONRPCRequest.class); + StreamingJSONRPCRequest request = deserializeStreamingRequest(body, methodName); streamingResponse = processStreamingRequest(request, context); } else { - NonStreamingJSONRPCRequest request = Utils.OBJECT_MAPPER.treeToValue(node, NonStreamingJSONRPCRequest.class); + NonStreamingJSONRPCRequest request = deserializeNonStreamingRequest(body, methodName); nonStreamingResponse = processNonStreamingRequest(request, context); } - } catch (JsonProcessingException e) { - error = handleError(e); + } catch (JSONRPCError e) { + error = new JSONRPCErrorResponse(requestId, e); + } catch (JsonSyntaxException e) { + error = new JSONRPCErrorResponse(requestId, new JSONParseError(e.getMessage())); } catch (Throwable t) { - error = new JSONRPCErrorResponse(new InternalError(t.getMessage())); + error = new JSONRPCErrorResponse(requestId, new InternalError(t.getMessage())); } finally { if (error != null) { rc.response() @@ -136,28 +164,6 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) { } } - private JSONRPCErrorResponse handleError(JsonProcessingException exception) { - Object id = null; - JSONRPCError jsonRpcError = null; - if (exception.getCause() instanceof JsonParseException) { - jsonRpcError = new JSONParseError(); - } else if (exception instanceof JsonEOFException) { - jsonRpcError = new JSONParseError(exception.getMessage()); - } else if (exception instanceof MethodNotFoundJsonMappingException err) { - id = err.getId(); - jsonRpcError = new MethodNotFoundError(); - } else if (exception instanceof InvalidParamsJsonMappingException err) { - id = err.getId(); - jsonRpcError = new InvalidParamsError(); - } else if (exception instanceof IdJsonMappingException err) { - id = err.getId(); - jsonRpcError = new InvalidRequestError(); - } else { - jsonRpcError = new InvalidRequestError(); - } - return new JSONRPCErrorResponse(id, jsonRpcError); - } - /** * /** * Handles incoming GET requests to the agent card endpoint. @@ -170,6 +176,40 @@ public AgentCard getAgentCard() { return jsonRpcHandler.getAgentCard(); } + private NonStreamingJSONRPCRequest deserializeNonStreamingRequest(String body, String methodName) { + try { + return switch (methodName) { + case GetTaskRequest.METHOD -> JsonUtil.fromJson(body, GetTaskRequest.class); + case CancelTaskRequest.METHOD -> JsonUtil.fromJson(body, CancelTaskRequest.class); + case SendMessageRequest.METHOD -> JsonUtil.fromJson(body, SendMessageRequest.class); + case SetTaskPushNotificationConfigRequest.METHOD -> JsonUtil.fromJson(body, SetTaskPushNotificationConfigRequest.class); + case GetTaskPushNotificationConfigRequest.METHOD -> JsonUtil.fromJson(body, GetTaskPushNotificationConfigRequest.class); + case ListTaskPushNotificationConfigRequest.METHOD -> JsonUtil.fromJson(body, ListTaskPushNotificationConfigRequest.class); + case DeleteTaskPushNotificationConfigRequest.METHOD -> JsonUtil.fromJson(body, DeleteTaskPushNotificationConfigRequest.class); + case GetAuthenticatedExtendedCardRequest.METHOD -> JsonUtil.fromJson(body, GetAuthenticatedExtendedCardRequest.class); + default -> throw new MethodNotFoundError(); + }; + } catch (JSONRPCError e) { + throw e; + } catch (Exception e) { + throw new InvalidParamsError(e.getMessage()); + } + } + + private StreamingJSONRPCRequest deserializeStreamingRequest(String body, String methodName) { + try { + return switch (methodName) { + case SendStreamingMessageRequest.METHOD -> JsonUtil.fromJson(body, SendStreamingMessageRequest.class); + case TaskResubscriptionRequest.METHOD -> JsonUtil.fromJson(body, TaskResubscriptionRequest.class); + default -> throw new MethodNotFoundError(); + }; + } catch (JSONRPCError e) { + throw e; + } catch (Exception e) { + throw new InvalidParamsError(e.getMessage()); + } + } + private JSONRPCResponse processNonStreamingRequest( NonStreamingJSONRPCRequest request, ServerCallContext context) { if (request instanceof GetTaskRequest req) { diff --git a/reference/jsonrpc/src/test/java/io/a2a/server/apps/quarkus/A2ATestRoutes.java b/reference/jsonrpc/src/test/java/io/a2a/server/apps/quarkus/A2ATestRoutes.java index 3e6be1a31..d3bfca712 100644 --- a/reference/jsonrpc/src/test/java/io/a2a/server/apps/quarkus/A2ATestRoutes.java +++ b/reference/jsonrpc/src/test/java/io/a2a/server/apps/quarkus/A2ATestRoutes.java @@ -15,7 +15,7 @@ import io.a2a.spec.Task; import io.a2a.spec.TaskArtifactUpdateEvent; import io.a2a.spec.TaskStatusUpdateEvent; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; import io.quarkus.vertx.web.Body; import io.quarkus.vertx.web.Param; import io.quarkus.vertx.web.Route; @@ -43,7 +43,7 @@ public void init() { @Route(path = "/test/task", methods = {Route.HttpMethod.POST}, consumes = {APPLICATION_JSON}, type = Route.HandlerType.BLOCKING) public void saveTask(@Body String body, RoutingContext rc) { try { - Task task = Utils.OBJECT_MAPPER.readValue(body, Task.class); + Task task = JsonUtil.fromJson(body, Task.class); testUtilsBean.saveTask(task); rc.response() .setStatusCode(200) @@ -66,7 +66,7 @@ public void getTask(@Param String taskId, RoutingContext rc) { rc.response() .setStatusCode(200) .putHeader(CONTENT_TYPE, APPLICATION_JSON) - .end(Utils.OBJECT_MAPPER.writeValueAsString(task)); + .end(JsonUtil.toJson(task)); } catch (Throwable t) { errorResponse(t, rc); @@ -108,7 +108,7 @@ public void ensureTaskQueue(@Param String taskId, RoutingContext rc) { public void enqueueTaskStatusUpdateEvent(@Param String taskId, @Body String body, RoutingContext rc) { try { - TaskStatusUpdateEvent event = Utils.OBJECT_MAPPER.readValue(body, TaskStatusUpdateEvent.class); + TaskStatusUpdateEvent event = JsonUtil.fromJson(body, TaskStatusUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); rc.response() .setStatusCode(200) @@ -122,7 +122,7 @@ public void enqueueTaskStatusUpdateEvent(@Param String taskId, @Body String body public void enqueueTaskArtifactUpdateEvent(@Param String taskId, @Body String body, RoutingContext rc) { try { - TaskArtifactUpdateEvent event = Utils.OBJECT_MAPPER.readValue(body, TaskArtifactUpdateEvent.class); + TaskArtifactUpdateEvent event = JsonUtil.fromJson(body, TaskArtifactUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); rc.response() .setStatusCode(200) @@ -169,7 +169,7 @@ public void deleteTaskPushNotificationConfig(@Param String taskId, @Param String @Route(path = "/test/task/:taskId", methods = {Route.HttpMethod.POST}, type = Route.HandlerType.BLOCKING) public void saveTaskPushNotificationConfig(@Param String taskId, @Body String body, RoutingContext rc) { try { - PushNotificationConfig notificationConfig = Utils.OBJECT_MAPPER.readValue(body, PushNotificationConfig.class); + PushNotificationConfig notificationConfig = JsonUtil.fromJson(body, PushNotificationConfig.class); if (notificationConfig == null) { rc.response() .setStatusCode(404) diff --git a/reference/rest/pom.xml b/reference/rest/pom.xml index 951704c29..ffec66fda 100644 --- a/reference/rest/pom.xml +++ b/reference/rest/pom.xml @@ -74,7 +74,7 @@ io.quarkus - quarkus-rest-client-jackson + quarkus-rest-client test diff --git a/reference/rest/src/test/java/io/a2a/server/rest/quarkus/A2ATestRoutes.java b/reference/rest/src/test/java/io/a2a/server/rest/quarkus/A2ATestRoutes.java index 6cda8dc8f..ba5bd1cfd 100644 --- a/reference/rest/src/test/java/io/a2a/server/rest/quarkus/A2ATestRoutes.java +++ b/reference/rest/src/test/java/io/a2a/server/rest/quarkus/A2ATestRoutes.java @@ -17,7 +17,7 @@ import io.a2a.spec.Task; import io.a2a.spec.TaskArtifactUpdateEvent; import io.a2a.spec.TaskStatusUpdateEvent; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; import io.quarkus.vertx.web.Body; import io.quarkus.vertx.web.Param; import io.quarkus.vertx.web.Route; @@ -45,7 +45,7 @@ public void init() { @Route(path = "/test/task", methods = {Route.HttpMethod.POST}, consumes = {APPLICATION_JSON}, type = Route.HandlerType.BLOCKING) public void saveTask(@Body String body, RoutingContext rc) { try { - Task task = Utils.OBJECT_MAPPER.readValue(body, Task.class); + Task task = JsonUtil.fromJson(body, Task.class); testUtilsBean.saveTask(task); rc.response() .setStatusCode(200) @@ -68,7 +68,7 @@ public void getTask(@Param String taskId, RoutingContext rc) { rc.response() .setStatusCode(200) .putHeader(CONTENT_TYPE, APPLICATION_JSON) - .end(Utils.OBJECT_MAPPER.writeValueAsString(task)); + .end(JsonUtil.toJson(task)); } catch (Throwable t) { errorResponse(t, rc); @@ -110,7 +110,7 @@ public void ensureTaskQueue(@Param String taskId, RoutingContext rc) { public void enqueueTaskStatusUpdateEvent(@Param String taskId, @Body String body, RoutingContext rc) { try { - TaskStatusUpdateEvent event = Utils.OBJECT_MAPPER.readValue(body, TaskStatusUpdateEvent.class); + TaskStatusUpdateEvent event = JsonUtil.fromJson(body, TaskStatusUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); rc.response() .setStatusCode(200) @@ -124,7 +124,7 @@ public void enqueueTaskStatusUpdateEvent(@Param String taskId, @Body String body public void enqueueTaskArtifactUpdateEvent(@Param String taskId, @Body String body, RoutingContext rc) { try { - TaskArtifactUpdateEvent event = Utils.OBJECT_MAPPER.readValue(body, TaskArtifactUpdateEvent.class); + TaskArtifactUpdateEvent event = JsonUtil.fromJson(body, TaskArtifactUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); rc.response() .setStatusCode(200) @@ -171,7 +171,7 @@ public void deleteTaskPushNotificationConfig(@Param String taskId, @Param String @Route(path = "/test/task/:taskId", methods = {Route.HttpMethod.POST}, type = Route.HandlerType.BLOCKING) public void saveTaskPushNotificationConfig(@Param String taskId, @Body String body, RoutingContext rc) { try { - PushNotificationConfig notificationConfig = Utils.OBJECT_MAPPER.readValue(body, PushNotificationConfig.class); + PushNotificationConfig notificationConfig = JsonUtil.fromJson(body, PushNotificationConfig.class); if (notificationConfig == null) { rc.response() .setStatusCode(404) diff --git a/server-common/pom.xml b/server-common/pom.xml index ff2cd8a1c..a24035f52 100644 --- a/server-common/pom.xml +++ b/server-common/pom.xml @@ -29,14 +29,6 @@ ${project.groupId} a2a-java-sdk-client-transport-jsonrpc - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - io.smallrye.reactive mutiny-zero diff --git a/server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java b/server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java index 4afaf3b4e..9601e6b79 100644 --- a/server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java +++ b/server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java @@ -1,6 +1,7 @@ package io.a2a.server.tasks; import static io.a2a.common.A2AHeaders.X_A2A_NOTIFICATION_TOKEN; + import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -9,13 +10,13 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import com.fasterxml.jackson.core.JsonProcessingException; +import io.a2a.json.JsonProcessingException; import io.a2a.client.http.A2AHttpClient; import io.a2a.client.http.JdkA2AHttpClient; +import io.a2a.json.JsonUtil; import io.a2a.spec.PushNotificationConfig; import io.a2a.spec.Task; -import io.a2a.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -78,10 +79,7 @@ private boolean dispatchNotification(Task task, PushNotificationConfig pushInfo) String body; try { - body = Utils.OBJECT_MAPPER.writeValueAsString(task); - } catch (JsonProcessingException e) { - LOGGER.debug("Error writing value as string: {}", e.getMessage(), e); - return false; + body = JsonUtil.toJson(task); } catch (Throwable throwable) { LOGGER.debug("Error writing value as string: {}", throwable.getMessage(), throwable); return false; diff --git a/server-common/src/test/java/io/a2a/server/events/EventConsumerTest.java b/server-common/src/test/java/io/a2a/server/events/EventConsumerTest.java index 0cf633fff..3e5611d9e 100644 --- a/server-common/src/test/java/io/a2a/server/events/EventConsumerTest.java +++ b/server-common/src/test/java/io/a2a/server/events/EventConsumerTest.java @@ -14,7 +14,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import com.fasterxml.jackson.core.JsonProcessingException; +import io.a2a.json.JsonProcessingException; import io.a2a.spec.A2AError; import io.a2a.spec.A2AServerException; import io.a2a.spec.Artifact; @@ -63,13 +63,13 @@ public void init() { @Test public void testConsumeOneTaskEvent() throws Exception { - Task event = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); + Task event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); enqueueAndConsumeOneEvent(event); } @Test public void testConsumeOneMessageEvent() throws Exception { - Event event = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.class); enqueueAndConsumeOneEvent(event); } @@ -93,7 +93,7 @@ public void testConsumeOneQueueEmpty() throws A2AServerException { @Test public void testConsumeAllMultipleEvents() throws JsonProcessingException { List events = List.of( - Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE), + Utils.unmarshalFrom(MINIMAL_TASK, Task.class), new TaskArtifactUpdateEvent.Builder() .taskId("task-123") .contextId("session-xyz") @@ -154,7 +154,7 @@ public void onComplete() { @Test public void testConsumeUntilMessage() throws Exception { List events = List.of( - Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE), + Utils.unmarshalFrom(MINIMAL_TASK, Task.class), new TaskArtifactUpdateEvent.Builder() .taskId("task-123") .contextId("session-xyz") @@ -214,7 +214,7 @@ public void onComplete() { @Test public void testConsumeMessageEvents() throws Exception { - Message message = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.TYPE_REFERENCE); + Message message = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.class); Message message2 = new Message.Builder(message).build(); List events = List.of(message, message2); @@ -393,7 +393,7 @@ public void testConsumeAllHandlesQueueClosedException() throws Exception { EventConsumer consumer = new EventConsumer(queue); // Add a message event (which will complete the stream) - Event message = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.TYPE_REFERENCE); + Event message = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.class); queue.enqueueEvent(message); // Close the queue before consuming diff --git a/server-common/src/test/java/io/a2a/server/events/EventQueueTest.java b/server-common/src/test/java/io/a2a/server/events/EventQueueTest.java index 389c21d88..75888f151 100644 --- a/server-common/src/test/java/io/a2a/server/events/EventQueueTest.java +++ b/server-common/src/test/java/io/a2a/server/events/EventQueueTest.java @@ -100,7 +100,7 @@ public void testEnqueueEventPropagagesToChildren() throws Exception { EventQueue parentQueue = EventQueue.builder().build(); EventQueue childQueue = parentQueue.tap(); - Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); parentQueue.enqueueEvent(event); // Event should be available in both parent and child queues @@ -117,8 +117,8 @@ public void testMultipleChildQueuesReceiveEvents() throws Exception { EventQueue childQueue1 = parentQueue.tap(); EventQueue childQueue2 = parentQueue.tap(); - Event event1 = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); - Event event2 = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.TYPE_REFERENCE); + Event event1 = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); + Event event2 = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.class); parentQueue.enqueueEvent(event1); parentQueue.enqueueEvent(event2); @@ -140,7 +140,7 @@ public void testChildQueueDequeueIndependently() throws Exception { EventQueue childQueue1 = parentQueue.tap(); EventQueue childQueue2 = parentQueue.tap(); - Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); parentQueue.enqueueEvent(event); // Dequeue from child1 first @@ -163,7 +163,7 @@ public void testCloseImmediatePropagationToChildren() throws Exception { EventQueue childQueue = parentQueue.tap(); // Add events to both parent and child - Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); parentQueue.enqueueEvent(event); assertFalse(childQueue.isClosed()); @@ -190,7 +190,7 @@ public void testCloseImmediatePropagationToChildren() throws Exception { @Test public void testEnqueueEventWhenClosed() throws Exception { EventQueue queue = EventQueue.builder().build(); - Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); queue.close(); // Close the queue first assertTrue(queue.isClosed()); @@ -220,7 +220,7 @@ public void testDequeueEventWhenClosedAndEmpty() throws Exception { @Test public void testDequeueEventWhenClosedButHasEvents() throws Exception { EventQueue queue = EventQueue.builder().build(); - Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); queue.enqueueEvent(event); queue.close(); // Graceful close - events should remain @@ -236,7 +236,7 @@ public void testDequeueEventWhenClosedButHasEvents() throws Exception { @Test public void testEnqueueAndDequeueEvent() throws Exception { - Event event = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MESSAGE_PAYLOAD, Message.class); eventQueue.enqueueEvent(event); Event dequeuedEvent = eventQueue.dequeueEventItem(200).getEvent(); assertSame(event, dequeuedEvent); @@ -244,7 +244,7 @@ public void testEnqueueAndDequeueEvent() throws Exception { @Test public void testDequeueEventNoWait() throws Exception { - Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); eventQueue.enqueueEvent(event); Event dequeuedEvent = eventQueue.dequeueEventItem(-1).getEvent(); assertSame(event, dequeuedEvent); @@ -305,7 +305,7 @@ public void testEnqueueDifferentEventTypes() throws Exception { */ @Test public void testCloseGracefulSetsFlag() throws Exception { - Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); eventQueue.enqueueEvent(event); eventQueue.close(false); // Graceful close @@ -318,7 +318,7 @@ public void testCloseGracefulSetsFlag() throws Exception { */ @Test public void testCloseImmediateClearsQueue() throws Exception { - Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.TYPE_REFERENCE); + Event event = Utils.unmarshalFrom(MINIMAL_TASK, Task.class); eventQueue.enqueueEvent(event); eventQueue.close(true); // Immediate close diff --git a/server-common/src/test/java/io/a2a/server/requesthandlers/AbstractA2ARequestHandlerTest.java b/server-common/src/test/java/io/a2a/server/requesthandlers/AbstractA2ARequestHandlerTest.java index d654a83a6..429c27942 100644 --- a/server-common/src/test/java/io/a2a/server/requesthandlers/AbstractA2ARequestHandlerTest.java +++ b/server-common/src/test/java/io/a2a/server/requesthandlers/AbstractA2ARequestHandlerTest.java @@ -37,7 +37,8 @@ import io.a2a.spec.TaskStatus; import io.a2a.spec.Event; import io.a2a.spec.TextPart; -import io.a2a.util.Utils; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import io.quarkus.arc.profile.IfBuildProfile; import java.util.Map; @@ -199,8 +200,9 @@ public PostBuilder body(String body) { @Override public A2AHttpResponse post() throws IOException, InterruptedException { - tasks.add(Utils.OBJECT_MAPPER.readValue(body, Task.TYPE_REFERENCE)); try { + Task task = JsonUtil.fromJson(body, Task.class); + tasks.add(task); return new A2AHttpResponse() { @Override public int status() { @@ -217,8 +219,12 @@ public String body() { return ""; } }; + } catch (JsonProcessingException e) { + throw new IOException("Failed to parse task JSON", e); } finally { - latch.countDown(); + if (latch != null) { + latch.countDown(); + } } } diff --git a/server-common/src/test/java/io/a2a/server/tasks/InMemoryPushNotificationConfigStoreTest.java b/server-common/src/test/java/io/a2a/server/tasks/InMemoryPushNotificationConfigStoreTest.java index 9156f78b2..fc0ee6b1b 100644 --- a/server-common/src/test/java/io/a2a/server/tasks/InMemoryPushNotificationConfigStoreTest.java +++ b/server-common/src/test/java/io/a2a/server/tasks/InMemoryPushNotificationConfigStoreTest.java @@ -244,7 +244,7 @@ public void testSendNotificationSuccess() throws Exception { verify(mockPostBuilder).url(config.url()); verify(mockPostBuilder).body(bodyCaptor.capture()); verify(mockPostBuilder).post(); - + // Verify the request body contains the task data String sentBody = bodyCaptor.getValue(); assertTrue(sentBody.contains(task.getId())); diff --git a/server-common/src/test/java/io/a2a/server/tasks/InMemoryTaskStoreTest.java b/server-common/src/test/java/io/a2a/server/tasks/InMemoryTaskStoreTest.java index 1ae4174dc..d49f6de4f 100644 --- a/server-common/src/test/java/io/a2a/server/tasks/InMemoryTaskStoreTest.java +++ b/server-common/src/test/java/io/a2a/server/tasks/InMemoryTaskStoreTest.java @@ -19,7 +19,7 @@ public class InMemoryTaskStoreTest { @Test public void testSaveAndGet() throws Exception { InMemoryTaskStore store = new InMemoryTaskStore(); - Task task = Utils.unmarshalFrom(TASK_JSON, Task.TYPE_REFERENCE); + Task task = Utils.unmarshalFrom(TASK_JSON, Task.class); store.save(task); Task retrieved = store.get(task.getId()); assertSame(task, retrieved); @@ -35,7 +35,7 @@ public void testGetNonExistent() throws Exception { @Test public void testDelete() throws Exception { InMemoryTaskStore store = new InMemoryTaskStore(); - Task task = Utils.unmarshalFrom(TASK_JSON, Task.TYPE_REFERENCE); + Task task = Utils.unmarshalFrom(TASK_JSON, Task.class); store.save(task); store.delete(task.getId()); Task retrieved = store.get(task.getId()); diff --git a/server-common/src/test/java/io/a2a/server/tasks/PushNotificationSenderTest.java b/server-common/src/test/java/io/a2a/server/tasks/PushNotificationSenderTest.java index 2ab974edb..f73c2a7f5 100644 --- a/server-common/src/test/java/io/a2a/server/tasks/PushNotificationSenderTest.java +++ b/server-common/src/test/java/io/a2a/server/tasks/PushNotificationSenderTest.java @@ -19,7 +19,8 @@ import io.a2a.client.http.A2AHttpClient; import io.a2a.client.http.A2AHttpResponse; import io.a2a.common.A2AHeaders; -import io.a2a.util.Utils; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import io.a2a.spec.PushNotificationConfig; import io.a2a.spec.Task; import io.a2a.spec.TaskState; @@ -72,13 +73,13 @@ public A2AHttpResponse post() throws IOException, InterruptedException { if (shouldThrowException) { throw new IOException("Simulated network error"); } - + try { - Task task = Utils.OBJECT_MAPPER.readValue(body, Task.TYPE_REFERENCE); + Task task = JsonUtil.fromJson(body, Task.class); tasks.add(task); urls.add(url); headers.add(new java.util.HashMap<>(requestHeaders)); - + return new A2AHttpResponse() { @Override public int status() { @@ -95,6 +96,8 @@ public String body() { return ""; } }; + } catch (JsonProcessingException e) { + throw new IOException("Failed to parse task JSON", e); } finally { if (latch != null) { latch.countDown(); diff --git a/server-common/src/test/java/io/a2a/server/tasks/TaskManagerTest.java b/server-common/src/test/java/io/a2a/server/tasks/TaskManagerTest.java index 1a4850bda..0b30b0a71 100644 --- a/server-common/src/test/java/io/a2a/server/tasks/TaskManagerTest.java +++ b/server-common/src/test/java/io/a2a/server/tasks/TaskManagerTest.java @@ -42,7 +42,7 @@ public class TaskManagerTest { @BeforeEach public void init() throws Exception { - minimalTask = Utils.unmarshalFrom(TASK_JSON, Task.TYPE_REFERENCE); + minimalTask = Utils.unmarshalFrom(TASK_JSON, Task.class); taskStore = new InMemoryTaskStore(); taskManager = new TaskManager(minimalTask.getId(), minimalTask.getContextId(), taskStore, null); } diff --git a/spec-grpc/pom.xml b/spec-grpc/pom.xml index 0ffac2fd3..a7efca27f 100644 --- a/spec-grpc/pom.xml +++ b/spec-grpc/pom.xml @@ -25,6 +25,10 @@ com.google.protobuf protobuf-java + + com.google.protobuf + protobuf-java-util + io.grpc grpc-protobuf diff --git a/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java b/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java new file mode 100644 index 000000000..5014608a7 --- /dev/null +++ b/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java @@ -0,0 +1,546 @@ +package io.a2a.grpc.utils; + + +import io.a2a.json.JsonMappingException; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.Strictness; +import com.google.gson.stream.JsonWriter; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import io.a2a.grpc.StreamResponse; +import io.a2a.spec.CancelTaskRequest; +import io.a2a.spec.CancelTaskResponse; +import io.a2a.spec.ContentTypeNotSupportedError; +import io.a2a.spec.DeleteTaskPushNotificationConfigRequest; +import io.a2a.spec.DeleteTaskPushNotificationConfigResponse; +import io.a2a.spec.GetAuthenticatedExtendedCardRequest; +import io.a2a.spec.GetAuthenticatedExtendedCardResponse; +import io.a2a.spec.GetTaskPushNotificationConfigRequest; +import io.a2a.spec.GetTaskPushNotificationConfigResponse; +import io.a2a.spec.GetTaskRequest; +import io.a2a.spec.GetTaskResponse; +import io.a2a.spec.IdJsonMappingException; +import io.a2a.spec.InvalidAgentResponseError; +import io.a2a.spec.InvalidParamsError; +import io.a2a.spec.InvalidParamsJsonMappingException; +import io.a2a.spec.InvalidRequestError; +import io.a2a.spec.JSONParseError; +import io.a2a.spec.JSONRPCError; +import io.a2a.spec.JSONRPCMessage; +import io.a2a.spec.JSONRPCRequest; +import io.a2a.spec.JSONRPCResponse; +import io.a2a.spec.ListTaskPushNotificationConfigRequest; +import io.a2a.spec.ListTaskPushNotificationConfigResponse; +import io.a2a.spec.MethodNotFoundError; +import io.a2a.spec.MethodNotFoundJsonMappingException; +import io.a2a.spec.PushNotificationNotSupportedError; +import io.a2a.spec.SendMessageRequest; +import io.a2a.spec.SendMessageResponse; +import io.a2a.spec.SendStreamingMessageRequest; +import io.a2a.spec.SetTaskPushNotificationConfigRequest; +import io.a2a.spec.SetTaskPushNotificationConfigResponse; +import io.a2a.spec.TaskResubscriptionRequest; +import io.a2a.json.JsonUtil; +import io.a2a.json.JsonProcessingException; +import io.a2a.spec.AgentCard; +import io.a2a.spec.TaskNotCancelableError; +import io.a2a.spec.TaskNotFoundError; +import io.a2a.spec.UnsupportedOperationError; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.jspecify.annotations.Nullable; + +import static io.a2a.spec.A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.INTERNAL_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.INVALID_AGENT_RESPONSE_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.INVALID_PARAMS_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.INVALID_REQUEST_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.JSON_PARSE_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.METHOD_NOT_FOUND_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.TASK_NOT_CANCELABLE_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.TASK_NOT_FOUND_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.UNSUPPORTED_OPERATION_ERROR_CODE; + + +/** + * Utilities for converting between JSON-RPC 2.0 messages and Protocol Buffer objects. + *

+ * This class provides a unified strategy for handling JSON-RPC requests and responses in the A2A SDK + * by bridging the JSON-RPC transport layer with Protocol Buffer-based internal representations. + * + *

Conversion Strategy

+ * The conversion process follows a two-step approach: + *
    + *
  1. JSON → Proto: JSON-RPC messages are parsed using Gson, then converted to Protocol Buffer + * objects using Google's {@link JsonFormat} parser. This ensures consistent handling of field names, + * types, and nested structures according to the proto3 specification.
  2. + *
  3. Proto → Spec: Protocol Buffer objects are converted to A2A spec objects using + * {@link ProtoUtils.FromProto} converters, which handle type mappings and create immutable + * spec-compliant Java objects.
  4. + *
+ * + *

Request Processing Flow

+ *
+ * Incoming JSON-RPC Request
+ *   ↓ parseRequestBody(String)
+ * Validate version, id, method
+ *   ↓ parseMethodRequest()
+ * Parse params → Proto Builder
+ *   ↓ ProtoUtils.FromProto.*
+ * Create JSONRPCRequest<?> with spec objects
+ * 
+ * + *

Response Processing Flow

+ *
+ * Incoming JSON-RPC Response
+ *   ↓ parseResponseBody(String, String)
+ * Validate version, id, check for errors
+ *   ↓ Parse result/error
+ * Proto Builder → spec objects
+ *   ↓ ProtoUtils.FromProto.*
+ * Create JSONRPCResponse<?> with result or error
+ * 
+ * + *

Serialization Flow

+ *
+ * Proto MessageOrBuilder
+ *   ↓ JsonFormat.printer()
+ * Proto JSON string
+ *   ↓ Gson JsonWriter
+ * Complete JSON-RPC envelope
+ * 
+ * + *

Error Handling

+ * The class provides detailed error messages for common failure scenarios: + *
    + *
  • Missing/invalid method: Returns {@link MethodNotFoundError} with the invalid method name
  • + *
  • Invalid parameters: Returns {@link InvalidParamsError} with proto parsing details
  • + *
  • Protocol version mismatch: Returns {@link InvalidRequestError} with version info
  • + *
  • Missing/invalid id: Returns {@link InvalidRequestError} with id validation details
  • + *
+ * + *

Thread Safety

+ * This class is thread-safe. All methods are stateless and use immutable shared resources + * ({@link Gson} instance is thread-safe, proto builders are created per-invocation). + * + *

Usage Example

+ *
{@code
+ * // Parse incoming JSON-RPC request
+ * String jsonRequest = """
+ *     {"jsonrpc":"2.0","id":1,"method":"tasks.get","params":{"name":"tasks/task-123"}}
+ *     """;
+ * JSONRPCRequest request = JSONRPCUtils.parseRequestBody(jsonRequest);
+ *
+ * // Create JSON-RPC request from proto
+ * io.a2a.grpc.GetTaskRequest protoRequest = ...;
+ * String json = JSONRPCUtils.toJsonRPCRequest("req-1", "tasks.get", protoRequest);
+ *
+ * // Create JSON-RPC response from proto
+ * io.a2a.grpc.Task protoTask = ...;
+ * String response = JSONRPCUtils.toJsonRPCResultResponse("req-1", protoTask);
+ * }
+ * + * @see ProtoUtils + * @see JSONRPCRequest + * @see JSONRPCResponse + * @see JSON-RPC 2.0 Specification + */ +public class JSONRPCUtils { + + private static final Logger log = Logger.getLogger(JSONRPCUtils.class.getName()); + private static final Gson GSON = new GsonBuilder() + .setStrictness(Strictness.STRICT) + .create(); + + public static JSONRPCRequest parseRequestBody(String body) throws JsonMappingException { + JsonElement jelement = JsonParser.parseString(body); + JsonObject jsonRpc = jelement.getAsJsonObject(); + if (!jsonRpc.has("method")) { + throw new IdJsonMappingException( + "JSON-RPC request missing required 'method' field. Request must include: jsonrpc, id, method, and params.", + getIdIfPossible(jsonRpc)); + } + String version = getAndValidateJsonrpc(jsonRpc); + Object id = getAndValidateId(jsonRpc); + String method = jsonRpc.get("method").getAsString(); + JsonElement paramsNode = jsonRpc.get("params"); + + try { + return parseMethodRequest(version, id, method, paramsNode); + } catch (InvalidParamsError e) { + throw new InvalidParamsJsonMappingException(e.getMessage(), id); + } + } + + private static JSONRPCRequest parseMethodRequest(String version, Object id, String method, JsonElement paramsNode) throws InvalidParamsError, MethodNotFoundJsonMappingException { + switch (method) { + case GetTaskRequest.METHOD -> { + io.a2a.grpc.GetTaskRequest.Builder builder = io.a2a.grpc.GetTaskRequest.newBuilder(); + parseRequestBody(paramsNode, builder); + return new GetTaskRequest(version, id, method, ProtoUtils.FromProto.taskQueryParams(builder)); + } + case CancelTaskRequest.METHOD -> { + io.a2a.grpc.CancelTaskRequest.Builder builder = io.a2a.grpc.CancelTaskRequest.newBuilder(); + parseRequestBody(paramsNode, builder); + return new CancelTaskRequest(version, id, method, ProtoUtils.FromProto.taskIdParams(builder)); + } + case SetTaskPushNotificationConfigRequest.METHOD -> { + io.a2a.grpc.CreateTaskPushNotificationConfigRequest.Builder builder = io.a2a.grpc.CreateTaskPushNotificationConfigRequest.newBuilder(); + parseRequestBody(paramsNode, builder); + return new SetTaskPushNotificationConfigRequest(version, id, method, ProtoUtils.FromProto.taskPushNotificationConfig(builder)); + } + case GetTaskPushNotificationConfigRequest.METHOD -> { + io.a2a.grpc.GetTaskPushNotificationConfigRequest.Builder builder = io.a2a.grpc.GetTaskPushNotificationConfigRequest.newBuilder(); + parseRequestBody(paramsNode, builder); + return new GetTaskPushNotificationConfigRequest(version, id, method, ProtoUtils.FromProto.getTaskPushNotificationConfigParams(builder)); + } + case SendMessageRequest.METHOD -> { + io.a2a.grpc.SendMessageRequest.Builder builder = io.a2a.grpc.SendMessageRequest.newBuilder(); + parseRequestBody(paramsNode, builder); + return new SendMessageRequest(version, id, method, ProtoUtils.FromProto.messageSendParams(builder)); + } + case ListTaskPushNotificationConfigRequest.METHOD -> { + io.a2a.grpc.ListTaskPushNotificationConfigRequest.Builder builder = io.a2a.grpc.ListTaskPushNotificationConfigRequest.newBuilder(); + parseRequestBody(paramsNode, builder); + return new ListTaskPushNotificationConfigRequest(version, id, method, ProtoUtils.FromProto.listTaskPushNotificationConfigParams(builder)); + } + case DeleteTaskPushNotificationConfigRequest.METHOD -> { + io.a2a.grpc.DeleteTaskPushNotificationConfigRequest.Builder builder = io.a2a.grpc.DeleteTaskPushNotificationConfigRequest.newBuilder(); + parseRequestBody(paramsNode, builder); + return new DeleteTaskPushNotificationConfigRequest(version, id, method, ProtoUtils.FromProto.deleteTaskPushNotificationConfigParams(builder)); + } + case GetAuthenticatedExtendedCardRequest.METHOD -> { + return new GetAuthenticatedExtendedCardRequest(version, id, method, null); + } + case SendStreamingMessageRequest.METHOD -> { + io.a2a.grpc.SendMessageRequest.Builder builder = io.a2a.grpc.SendMessageRequest.newBuilder(); + parseRequestBody(paramsNode, builder); + return new SendStreamingMessageRequest(version, id, method, ProtoUtils.FromProto.messageSendParams(builder)); + } + case TaskResubscriptionRequest.METHOD -> { + io.a2a.grpc.TaskSubscriptionRequest.Builder builder = io.a2a.grpc.TaskSubscriptionRequest.newBuilder(); + parseRequestBody(paramsNode, builder); + return new TaskResubscriptionRequest(version, id, method, ProtoUtils.FromProto.taskIdParams(builder)); + } + default -> + throw new MethodNotFoundJsonMappingException("Unsupported JSON-RPC method: '" + method + "'", id); + } + } + + public static StreamResponse parseResponseEvent(String body) throws JsonMappingException { + JsonElement jelement = JsonParser.parseString(body); + JsonObject jsonRpc = jelement.getAsJsonObject(); + String version = getAndValidateJsonrpc(jsonRpc); + Object id = getAndValidateId(jsonRpc); + JsonElement paramsNode = jsonRpc.get("result"); + if (jsonRpc.has("error")) { + throw processError(jsonRpc.getAsJsonObject("error")); + } + StreamResponse.Builder builder = StreamResponse.newBuilder(); + parseRequestBody(paramsNode, builder); + return builder.build(); + } + + public static JSONRPCResponse parseResponseBody(String body, String method) throws JsonMappingException { + JsonElement jelement = JsonParser.parseString(body); + JsonObject jsonRpc = jelement.getAsJsonObject(); + String version = getAndValidateJsonrpc(jsonRpc); + Object id = getAndValidateId(jsonRpc); + JsonElement paramsNode = jsonRpc.get("result"); + if (jsonRpc.has("error")) { + return parseError(jsonRpc.getAsJsonObject("error"), id, method); + } + switch (method) { + case GetTaskRequest.METHOD -> { + io.a2a.grpc.Task.Builder builder = io.a2a.grpc.Task.newBuilder(); + parseRequestBody(paramsNode, builder); + return new GetTaskResponse(id, ProtoUtils.FromProto.task(builder)); + } + case CancelTaskRequest.METHOD -> { + io.a2a.grpc.Task.Builder builder = io.a2a.grpc.Task.newBuilder(); + parseRequestBody(paramsNode, builder); + return new CancelTaskResponse(id, ProtoUtils.FromProto.task(builder)); + } + case SetTaskPushNotificationConfigRequest.METHOD -> { + io.a2a.grpc.TaskPushNotificationConfig.Builder builder = io.a2a.grpc.TaskPushNotificationConfig.newBuilder(); + parseRequestBody(paramsNode, builder); + return new SetTaskPushNotificationConfigResponse(id, ProtoUtils.FromProto.taskPushNotificationConfig(builder)); + } + case GetTaskPushNotificationConfigRequest.METHOD -> { + io.a2a.grpc.TaskPushNotificationConfig.Builder builder = io.a2a.grpc.TaskPushNotificationConfig.newBuilder(); + parseRequestBody(paramsNode, builder); + return new GetTaskPushNotificationConfigResponse(id, ProtoUtils.FromProto.taskPushNotificationConfig(builder)); + } + case SendMessageRequest.METHOD -> { + io.a2a.grpc.SendMessageResponse.Builder builder = io.a2a.grpc.SendMessageResponse.newBuilder(); + parseRequestBody(paramsNode, builder); + if (builder.hasMsg()) { + return new SendMessageResponse(id, ProtoUtils.FromProto.message(builder.getMsg())); + } + return new SendMessageResponse(id, ProtoUtils.FromProto.task(builder.getTask())); + } + case ListTaskPushNotificationConfigRequest.METHOD -> { + io.a2a.grpc.ListTaskPushNotificationConfigResponse.Builder builder = io.a2a.grpc.ListTaskPushNotificationConfigResponse.newBuilder(); + parseRequestBody(paramsNode, builder); + return new ListTaskPushNotificationConfigResponse(id, ProtoUtils.FromProto.listTaskPushNotificationConfigParams(builder)); + } + case DeleteTaskPushNotificationConfigRequest.METHOD -> { + return new DeleteTaskPushNotificationConfigResponse(id); + } + case GetAuthenticatedExtendedCardRequest.METHOD -> { + try { + AgentCard card = JsonUtil.fromJson(GSON.toJson(paramsNode), AgentCard.class); + return new GetAuthenticatedExtendedCardResponse(id, card); + } catch (JsonProcessingException e) { + throw new InvalidParamsError("Failed to parse agent card response: " + e.getMessage()); + } + } + default -> + throw new MethodNotFoundJsonMappingException("Unsupported JSON-RPC method: '" + method + "' in response parsing.", getIdIfPossible(jsonRpc)); + } + } + + public static JSONRPCResponse parseError(JsonObject error, Object id, String method) throws JsonMappingException { + JSONRPCError rpcError = processError(error); + switch (method) { + case GetTaskRequest.METHOD -> { + return new GetTaskResponse(id, rpcError); + } + case CancelTaskRequest.METHOD -> { + return new CancelTaskResponse(id, rpcError); + } + case SetTaskPushNotificationConfigRequest.METHOD -> { + return new SetTaskPushNotificationConfigResponse(id, rpcError); + } + case GetTaskPushNotificationConfigRequest.METHOD -> { + return new GetTaskPushNotificationConfigResponse(id, rpcError); + } + case SendMessageRequest.METHOD -> { + return new SendMessageResponse(id, rpcError); + } + case ListTaskPushNotificationConfigRequest.METHOD -> { + return new ListTaskPushNotificationConfigResponse(id, rpcError); + } + case DeleteTaskPushNotificationConfigRequest.METHOD -> { + return new DeleteTaskPushNotificationConfigResponse(id, rpcError); + } + default -> + throw new MethodNotFoundJsonMappingException("Unsupported JSON-RPC method: '" + method + "'", id); + } + } + + private static JSONRPCError processError(JsonObject error) { + String message = error.has("message") ? error.get("message").getAsString() : null; + Integer code = error.has("code") ? error.get("code").getAsInt() : null; + String data = error.has("data") ? error.get("data").toString() : null; + if (code != null) { + switch (code) { + case JSON_PARSE_ERROR_CODE: + return new JSONParseError(code, message, data); + case INVALID_REQUEST_ERROR_CODE: + return new InvalidRequestError(code, message, data); + case METHOD_NOT_FOUND_ERROR_CODE: + return new MethodNotFoundError(code, message, data); + case INVALID_PARAMS_ERROR_CODE: + return new InvalidParamsError(code, message, data); + case INTERNAL_ERROR_CODE: + return new io.a2a.spec.InternalError(code, message, data); + case PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE: + return new PushNotificationNotSupportedError(code, message, data); + case UNSUPPORTED_OPERATION_ERROR_CODE: + return new UnsupportedOperationError(code, message, data); + case CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE: + return new ContentTypeNotSupportedError(code, message, data); + case INVALID_AGENT_RESPONSE_ERROR_CODE: + return new InvalidAgentResponseError(code, message, data); + case TASK_NOT_CANCELABLE_ERROR_CODE: + return new TaskNotCancelableError(code, message, data); + case TASK_NOT_FOUND_ERROR_CODE: + return new TaskNotFoundError(code, message, data); + default: + return new JSONRPCError(code, message, data); + } + } + return new JSONRPCError(code, message, data); + } + + protected static void parseRequestBody(JsonElement jsonRpc, com.google.protobuf.Message.Builder builder) throws JSONRPCError { + try (Writer writer = new StringWriter()) { + GSON.toJson(jsonRpc, writer); + parseJsonString(writer.toString(), builder); + } catch (IOException e) { + log.log(Level.SEVERE, "Failed to serialize JSON element to string during proto conversion. JSON: {0}", jsonRpc); + log.log(Level.SEVERE, "Serialization error details", e); + throw new InvalidParamsError( + "Failed to parse request content. " + + "This may indicate invalid JSON structure or unsupported field types. Error: " + e.getMessage()); + } + } + + public static void parseJsonString(String body, com.google.protobuf.Message.Builder builder) throws JSONRPCError { + try { + JsonFormat.parser().merge(body, builder); + } catch (InvalidProtocolBufferException e) { + log.log(Level.SEVERE, "Protocol buffer parsing failed for JSON: {0}", body); + log.log(Level.SEVERE, "Proto parsing error details", e); + throw new InvalidParamsError( + "Invalid request content: " + extractProtoErrorMessage(e) + + ". Please verify the request matches the expected schema for this method."); + } + } + + /** + * Extracts a user-friendly error message from Protocol Buffer parsing exceptions. + * + * @param e the InvalidProtocolBufferException + * @return a cleaned error message with field information + */ + private static String extractProtoErrorMessage(InvalidProtocolBufferException e) { + String message = e.getMessage(); + if (message == null) { + return "unknown parsing error"; + } + // Extract field name if present in error message + if (message.contains("Cannot find field:")) { + return message.substring(message.indexOf("Cannot find field:")); + } + if (message.contains("Invalid value for")) { + return message.substring(message.indexOf("Invalid value for")); + } + return message; + } + + protected static String getAndValidateJsonrpc(JsonObject jsonRpc) throws JsonMappingException { + if (!jsonRpc.has("jsonrpc")) { + throw new IdJsonMappingException( + "Missing required 'jsonrpc' field. All requests must include 'jsonrpc': '2.0'", + getIdIfPossible(jsonRpc)); + } + String version = jsonRpc.get("jsonrpc").getAsString(); + if (!JSONRPCMessage.JSONRPC_VERSION.equals(version)) { + throw new IdJsonMappingException( + "Unsupported JSON-RPC version: '" + version + "'. Expected version '2.0'", + getIdIfPossible(jsonRpc)); + } + return version; + } + + /** + * Try to get the request id if possible , returns "UNDETERMINED ID" otherwise. + * This should be only used for errors. + * @param jsonRpc the json rpc JSON. + * @return the request id if possible , "UNDETERMINED ID" otherwise. + */ + protected static Object getIdIfPossible(JsonObject jsonRpc) { + try { + return getAndValidateId(jsonRpc); + } catch (JsonMappingException e) { + // id can't be determined + return "UNDETERMINED ID"; + } + } + + protected static Object getAndValidateId(JsonObject jsonRpc) throws JsonMappingException { + Object id = null; + if (jsonRpc.has("id")) { + if (jsonRpc.get("id").isJsonPrimitive()) { + try { + id = jsonRpc.get("id").getAsInt(); + } catch (UnsupportedOperationException | NumberFormatException | IllegalStateException e) { + id = jsonRpc.get("id").getAsString(); + } + } else { + throw new JsonMappingException(null, "Invalid 'id' type: " + jsonRpc.get("id").getClass().getSimpleName() + + ". ID must be a JSON string or number, not an object or array."); + } + } + if (id == null) { + throw new JsonMappingException(null, "Request 'id' cannot be null. Use a string or number identifier."); + } + return id; + } + + public static String toJsonRPCRequest(@Nullable String requestId, String method, com.google.protobuf.@Nullable MessageOrBuilder payload) { + try (StringWriter result = new StringWriter(); JsonWriter output = GSON.newJsonWriter(result)) { + output.beginObject(); + output.name("jsonrpc").value("2.0"); + String id = requestId; + if (requestId == null) { + id = UUID.randomUUID().toString(); + } + output.name("id").value(id); + if (method != null) { + output.name("method").value(method); + } + if (payload != null) { + String resultValue = JsonFormat.printer().omittingInsignificantWhitespace().print(payload); + output.name("params").jsonValue(resultValue); + } + output.endObject(); + return result.toString(); + } catch (IOException ex) { + throw new RuntimeException( + "Failed to serialize JSON-RPC request for method '" + method + "'. " + + "This indicates an internal error in JSON generation. Request ID: " + requestId, ex); + } + } + + public static String toJsonRPCResultResponse(Object requestId, com.google.protobuf.MessageOrBuilder builder) { + try (StringWriter result = new StringWriter(); JsonWriter output = GSON.newJsonWriter(result)) { + output.beginObject(); + output.name("jsonrpc").value("2.0"); + if (requestId != null) { + if (requestId instanceof String string) { + output.name("id").value(string); + } else if (requestId instanceof Number number) { + output.name("id").value(number.longValue()); + } + } + String resultValue = JsonFormat.printer().omittingInsignificantWhitespace().print(builder); + output.name("result").jsonValue(resultValue); + output.endObject(); + return result.toString(); + } catch (IOException ex) { + throw new RuntimeException( + "Failed to serialize JSON-RPC success response. " + + "Proto type: " + builder.getClass().getSimpleName() + ", Request ID: " + requestId, ex); + } + } + + public static String toJsonRPCErrorResponse(Object requestId, JSONRPCError error) { + try (StringWriter result = new StringWriter(); JsonWriter output = GSON.newJsonWriter(result)) { + output.beginObject(); + output.name("jsonrpc").value("2.0"); + if (requestId != null) { + if (requestId instanceof String string) { + output.name("id").value(string); + } else if (requestId instanceof Number number) { + output.name("id").value(number.longValue()); + } + } + output.name("error"); + output.beginObject(); + output.name("code").value(error.getCode()); + output.name("message").value(error.getMessage()); + if (error.getData() != null) { + output.name("data").value(error.getData().toString()); + } + output.endObject(); + output.endObject(); + return result.toString(); + } catch (IOException ex) { + throw new RuntimeException( + "Failed to serialize JSON-RPC error response. " + + "Error code: " + error.getCode() + ", Request ID: " + requestId, ex); + } + } +} diff --git a/spec-grpc/src/test/java/io/a2a/grpc/utils/JSONRPCUtilsTest.java b/spec-grpc/src/test/java/io/a2a/grpc/utils/JSONRPCUtilsTest.java new file mode 100644 index 000000000..0482f5ab3 --- /dev/null +++ b/spec-grpc/src/test/java/io/a2a/grpc/utils/JSONRPCUtilsTest.java @@ -0,0 +1,247 @@ +package io.a2a.grpc.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import io.a2a.json.JsonProcessingException; +import com.google.gson.JsonSyntaxException; +import io.a2a.spec.GetTaskPushNotificationConfigRequest; +import io.a2a.spec.GetTaskPushNotificationConfigResponse; +import io.a2a.spec.InvalidParamsError; +import io.a2a.spec.InvalidParamsJsonMappingException; +import io.a2a.spec.JSONParseError; +import io.a2a.spec.JSONRPCRequest; +import io.a2a.spec.SetTaskPushNotificationConfigRequest; +import io.a2a.spec.SetTaskPushNotificationConfigResponse; +import io.a2a.spec.TaskPushNotificationConfig; +import org.junit.jupiter.api.Test; + +public class JSONRPCUtilsTest { + + @Test + public void testParseSetTaskPushNotificationConfigRequest_ValidProtoFormat() throws JsonProcessingException { + String validRequest = """ + { + "jsonrpc": "2.0", + "method": "%s", + "id": "1", + "params": { + "parent": "tasks/task-123", + "configId": "config-456", + "config": { + "name": "tasks/task-123/pushNotificationConfigs/config-456", + "pushNotificationConfig": { + "url": "https://example.com/callback", + "authentication": { + "schemes": ["jwt"] + } + } + } + } + } + """.formatted(SetTaskPushNotificationConfigRequest.METHOD); + + JSONRPCRequest request = JSONRPCUtils.parseRequestBody(validRequest); + + assertNotNull(request); + assertInstanceOf(SetTaskPushNotificationConfigRequest.class, request); + SetTaskPushNotificationConfigRequest setRequest = (SetTaskPushNotificationConfigRequest) request; + assertEquals("2.0", setRequest.getJsonrpc()); + assertEquals(1, setRequest.getId()); + assertEquals(SetTaskPushNotificationConfigRequest.METHOD, setRequest.getMethod()); + + TaskPushNotificationConfig config = setRequest.getParams(); + assertNotNull(config); + assertEquals("task-123", config.taskId()); + assertNotNull(config.pushNotificationConfig()); + assertEquals("https://example.com/callback", config.pushNotificationConfig().url()); + } + + @Test + public void testParseGetTaskPushNotificationConfigRequest_ValidProtoFormat() throws JsonProcessingException { + String validRequest = """ + { + "jsonrpc": "2.0", + "method": "%s", + "id": "2", + "params": { + "name": "tasks/task-123/pushNotificationConfigs/config-456" + } + } + """.formatted(GetTaskPushNotificationConfigRequest.METHOD); + + JSONRPCRequest request = JSONRPCUtils.parseRequestBody(validRequest); + + assertNotNull(request); + assertInstanceOf(GetTaskPushNotificationConfigRequest.class, request); + GetTaskPushNotificationConfigRequest getRequest = (GetTaskPushNotificationConfigRequest) request; + assertEquals("2.0", getRequest.getJsonrpc()); + assertEquals(2, getRequest.getId()); + assertEquals(GetTaskPushNotificationConfigRequest.METHOD, getRequest.getMethod()); + assertNotNull(getRequest.getParams()); + assertEquals("task-123", getRequest.getParams().id()); + } + + @Test + public void testParseMalformedJSON_ThrowsJsonSyntaxException() { + String malformedRequest = """ + { + "jsonrpc": "2.0", + "method": "%s", + "params": { + "parent": "tasks/task-123" + """.formatted(SetTaskPushNotificationConfigRequest.METHOD); // Missing closing braces + + assertThrows(JsonSyntaxException.class, () -> { + JSONRPCUtils.parseRequestBody(malformedRequest); + }); + } + + @Test + public void testParseInvalidParams_ThrowsInvalidParamsError() { + String invalidParamsRequest = """ + { + "jsonrpc": "2.0", + "method": "%s", + "id": "3", + "params": "not_a_dict" + } + """.formatted(SetTaskPushNotificationConfigRequest.METHOD); + + InvalidParamsJsonMappingException exception = assertThrows( + InvalidParamsJsonMappingException.class, + () -> JSONRPCUtils.parseRequestBody(invalidParamsRequest) + ); + assertEquals(3, exception.getId()); + } + + @Test + public void testParseInvalidProtoStructure_ThrowsInvalidParamsError() { + String invalidStructure = """ + { + "jsonrpc": "2.0", + "method": "%s", + "id": "4", + "params": { + "invalid_field": "value" + } + } + """.formatted(SetTaskPushNotificationConfigRequest.METHOD); + + InvalidParamsJsonMappingException exception = assertThrows( + InvalidParamsJsonMappingException.class, + () -> JSONRPCUtils.parseRequestBody(invalidStructure) + ); + assertEquals(4, exception.getId()); + } + + @Test + public void testGenerateSetTaskPushNotificationConfigResponse_Success() throws Exception { + TaskPushNotificationConfig config = new TaskPushNotificationConfig( + "task-123", + new io.a2a.spec.PushNotificationConfig.Builder() + .url("https://example.com/callback") + .id("config-456") + .build() + ); + + String responseJson = """ + { + "jsonrpc": "2.0", + "id": "1", + "result": { + "name": "tasks/task-123/pushNotificationConfigs/config-456", + "pushNotificationConfig": { + "url": "https://example.com/callback", + "id": "config-456" + } + } + } + """; + + SetTaskPushNotificationConfigResponse response = + (SetTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody(responseJson, SetTaskPushNotificationConfigRequest.METHOD); + + assertNotNull(response); + assertEquals(1, response.getId()); + assertNotNull(response.getResult()); + assertEquals("task-123", response.getResult().taskId()); + assertEquals("https://example.com/callback", response.getResult().pushNotificationConfig().url()); + } + + @Test + public void testGenerateGetTaskPushNotificationConfigResponse_Success() throws Exception { + String responseJson = """ + { + "jsonrpc": "2.0", + "id": "2", + "result": { + "name": "tasks/task-123/pushNotificationConfigs/config-456", + "pushNotificationConfig": { + "url": "https://example.com/callback", + "id": "config-456" + } + } + } + """; + + GetTaskPushNotificationConfigResponse response = + (GetTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody(responseJson, GetTaskPushNotificationConfigRequest.METHOD); + + assertNotNull(response); + assertEquals(2, response.getId()); + assertNotNull(response.getResult()); + assertEquals("task-123", response.getResult().taskId()); + assertEquals("https://example.com/callback", response.getResult().pushNotificationConfig().url()); + } + + @Test + public void testParseErrorResponse_InvalidParams() throws Exception { + String errorResponse = """ + { + "jsonrpc": "2.0", + "id": "5", + "error": { + "code": -32602, + "message": "Invalid params" + } + } + """; + + SetTaskPushNotificationConfigResponse response = + (SetTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody(errorResponse, SetTaskPushNotificationConfigRequest.METHOD); + + assertNotNull(response); + assertEquals(5, response.getId()); + assertNotNull(response.getError()); + assertInstanceOf(InvalidParamsError.class, response.getError()); + assertEquals(-32602, response.getError().getCode()); + assertEquals("Invalid params", response.getError().getMessage()); + } + + @Test + public void testParseErrorResponse_ParseError() throws Exception { + String errorResponse = """ + { + "jsonrpc": "2.0", + "id": 6, + "error": { + "code": -32700, + "message": "Parse error" + } + } + """; + + SetTaskPushNotificationConfigResponse response = + (SetTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody(errorResponse, SetTaskPushNotificationConfigRequest.METHOD); + + assertNotNull(response); + assertEquals(6, response.getId()); + assertNotNull(response.getError()); + assertInstanceOf(JSONParseError.class, response.getError()); + assertEquals(-32700, response.getError().getCode()); + assertEquals("Parse error", response.getError().getMessage()); + } +} diff --git a/spec/pom.xml b/spec/pom.xml index ce100bf1e..f7f1f4bb0 100644 --- a/spec/pom.xml +++ b/spec/pom.xml @@ -21,14 +21,9 @@ ${project.groupId} a2a-java-sdk-common
- - - com.fasterxml.jackson.core - jackson-databind - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 + com.google.code.gson + gson diff --git a/spec/src/main/java/io/a2a/json/JsonMappingException.java b/spec/src/main/java/io/a2a/json/JsonMappingException.java new file mode 100644 index 000000000..550b7827c --- /dev/null +++ b/spec/src/main/java/io/a2a/json/JsonMappingException.java @@ -0,0 +1,102 @@ +package io.a2a.json; + +import org.jspecify.annotations.Nullable; + +/** + * Exception for JSON mapping errors when converting between JSON and Java objects. + *

+ * This exception serves as a replacement for Jackson's JsonMappingException, allowing + * the A2A Java SDK to remain independent of any specific JSON library implementation. + * It represents errors that occur during the mapping phase of deserialization or + * serialization, such as type mismatches, invalid values, or constraint violations. + *

+ * This exception extends {@link JsonProcessingException} and is used for more specific + * mapping-related errors compared to general parsing errors. + *

+ * Usage example: + *

{@code
+ * try {
+ *     Task task = JsonUtil.fromJson(json, Task.class);
+ *     if (task.getId() == null) {
+ *         throw new JsonMappingException(null, "Task ID cannot be null");
+ *     }
+ * } catch (JsonProcessingException e) {
+ *     throw new JsonMappingException(null, "Invalid task format: " + e.getMessage(), e);
+ * }
+ * }
+ * + * @see JsonProcessingException for the base exception class + */ +public class JsonMappingException extends JsonProcessingException { + + /** + * Optional reference object that caused the mapping error (e.g., JsonParser or field path). + */ + private final @Nullable Object reference; + + /** + * Constructs a new JsonMappingException with the specified reference and message. + *

+ * The reference parameter can be used to provide context about where the mapping + * error occurred (e.g., a field name, path, or parser reference). It can be null + * if no specific reference is available. + * + * @param reference optional reference object providing context for the error (may be null) + * @param message the detail message explaining the mapping error + */ + public JsonMappingException(@Nullable Object reference, String message) { + super(message); + this.reference = reference; + } + + /** + * Constructs a new JsonMappingException with the specified reference, message, and cause. + *

+ * The reference parameter can be used to provide context about where the mapping + * error occurred (e.g., a field name, path, or parser reference). It can be null + * if no specific reference is available. + * + * @param reference optional reference object providing context for the error (may be null) + * @param message the detail message explaining the mapping error + * @param cause the underlying cause of the mapping error (may be null) + */ + public JsonMappingException(@Nullable Object reference, String message, @Nullable Throwable cause) { + super(message, cause); + this.reference = reference; + } + + /** + * Constructs a new JsonMappingException with the specified message and cause. + *

+ * This constructor is provided for compatibility when no reference object is needed. + * + * @param message the detail message explaining the mapping error + * @param cause the underlying cause of the mapping error (may be null) + */ + public JsonMappingException(String message, @Nullable Throwable cause) { + this(null, message, cause); + } + + /** + * Constructs a new JsonMappingException with the specified message. + *

+ * This constructor is provided for compatibility when no reference object is needed. + * + * @param message the detail message explaining the mapping error + */ + public JsonMappingException(String message) { + this(null, message); + } + + /** + * Returns the reference object that provides context for the mapping error. + *

+ * This may be null if no specific reference was available when the exception + * was created. + * + * @return the reference object, or null if not available + */ + public @Nullable Object getReference() { + return reference; + } +} diff --git a/spec/src/main/java/io/a2a/json/JsonProcessingException.java b/spec/src/main/java/io/a2a/json/JsonProcessingException.java new file mode 100644 index 000000000..9af50b7ce --- /dev/null +++ b/spec/src/main/java/io/a2a/json/JsonProcessingException.java @@ -0,0 +1,55 @@ +package io.a2a.json; + +import org.jspecify.annotations.Nullable; + +/** + * General exception for JSON processing errors during serialization or deserialization. + *

+ * This exception serves as a replacement for Jackson's JsonProcessingException, allowing + * the A2A Java SDK to remain independent of any specific JSON library implementation. + * It can be used with any JSON processing library (Gson, Jackson, etc.). + *

+ * This is the base class for more specific JSON processing exceptions like + * {@link JsonMappingException}. + *

+ * Usage example: + *

{@code
+ * try {
+ *     String json = gson.toJson(object);
+ * } catch (Exception e) {
+ *     throw new JsonProcessingException("Failed to serialize object", e);
+ * }
+ * }
+ * + * @see JsonMappingException for mapping-specific errors + */ +public class JsonProcessingException extends Exception { + + /** + * Constructs a new JsonProcessingException with the specified message. + * + * @param message the detail message explaining the cause of the exception + */ + public JsonProcessingException(String message) { + super(message); + } + + /** + * Constructs a new JsonProcessingException with the specified message and cause. + * + * @param message the detail message explaining the cause of the exception + * @param cause the underlying cause of the exception (may be null) + */ + public JsonProcessingException(String message, @Nullable Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new JsonProcessingException with the specified cause. + * + * @param cause the underlying cause of the exception + */ + public JsonProcessingException(Throwable cause) { + super(cause); + } +} diff --git a/spec/src/main/java/io/a2a/json/JsonUtil.java b/spec/src/main/java/io/a2a/json/JsonUtil.java new file mode 100644 index 000000000..56dd3f310 --- /dev/null +++ b/spec/src/main/java/io/a2a/json/JsonUtil.java @@ -0,0 +1,904 @@ +package io.a2a.json; + +import static com.google.gson.stream.JsonToken.BEGIN_ARRAY; +import static com.google.gson.stream.JsonToken.BEGIN_OBJECT; +import static com.google.gson.stream.JsonToken.BOOLEAN; +import static com.google.gson.stream.JsonToken.NULL; +import static com.google.gson.stream.JsonToken.NUMBER; +import static com.google.gson.stream.JsonToken.STRING; +import static io.a2a.spec.A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.INTERNAL_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.INVALID_AGENT_RESPONSE_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.INVALID_PARAMS_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.INVALID_REQUEST_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.JSON_PARSE_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.METHOD_NOT_FOUND_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.TASK_NOT_CANCELABLE_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.TASK_NOT_FOUND_ERROR_CODE; +import static io.a2a.spec.A2AErrorCodes.UNSUPPORTED_OPERATION_ERROR_CODE; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.ToNumberPolicy; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import io.a2a.spec.APIKeySecurityScheme; +import io.a2a.spec.EventKind; +import io.a2a.spec.ContentTypeNotSupportedError; +import io.a2a.spec.DataPart; +import io.a2a.spec.FileContent; +import io.a2a.spec.FilePart; +import io.a2a.spec.FileWithBytes; +import io.a2a.spec.FileWithUri; +import io.a2a.spec.HTTPAuthSecurityScheme; +import io.a2a.spec.InvalidAgentResponseError; +import io.a2a.spec.InvalidParamsError; +import io.a2a.spec.InvalidRequestError; +import io.a2a.spec.JSONParseError; +import io.a2a.spec.JSONRPCError; +import io.a2a.spec.Message; +import io.a2a.spec.MethodNotFoundError; +import io.a2a.spec.MutualTLSSecurityScheme; +import io.a2a.spec.OAuth2SecurityScheme; +import io.a2a.spec.OpenIdConnectSecurityScheme; +import io.a2a.spec.Part; +import io.a2a.spec.PushNotificationNotSupportedError; +import io.a2a.spec.SecurityScheme; +import io.a2a.spec.StreamingEventKind; +import io.a2a.spec.Task; +import io.a2a.spec.TaskArtifactUpdateEvent; +import io.a2a.spec.TaskNotCancelableError; +import io.a2a.spec.TaskNotFoundError; +import io.a2a.spec.TaskState; +import io.a2a.spec.TaskStatusUpdateEvent; +import io.a2a.spec.TextPart; +import io.a2a.spec.UnsupportedOperationError; +import java.io.StringReader; +import java.lang.reflect.Type; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import org.jspecify.annotations.Nullable; + +import static io.a2a.json.JsonUtil.JSONRPCErrorTypeAdapter.THROWABLE_MARKER_FIELD; + +public class JsonUtil { + + private static GsonBuilder createBaseGsonBuilder() { + return new GsonBuilder() + .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeTypeAdapter()) + .registerTypeHierarchyAdapter(JSONRPCError.class, new JSONRPCErrorTypeAdapter()) + .registerTypeAdapter(TaskState.class, new TaskStateTypeAdapter()) + .registerTypeAdapter(Message.Role.class, new RoleTypeAdapter()) + .registerTypeAdapter(Part.Kind.class, new PartKindTypeAdapter()) + .registerTypeHierarchyAdapter(FileContent.class, new FileContentTypeAdapter()) + .registerTypeHierarchyAdapter(SecurityScheme.class, new SecuritySchemeTypeAdapter()); + } + + /** + * Pre-configured {@link Gson} instance for JSON operations. + *

+ * This mapper is configured with strict parsing mode and all necessary custom TypeAdapters + * for A2A Protocol types including polymorphic types, enums, and date/time types. + *

+ * Used throughout the SDK for consistent JSON serialization and deserialization. + * + * @see GsonFactory#createGson() + */ + public static final Gson OBJECT_MAPPER = createBaseGsonBuilder() + .registerTypeHierarchyAdapter(Part.class, new PartTypeAdapter()) + .registerTypeHierarchyAdapter(StreamingEventKind.class, new StreamingEventKindTypeAdapter()) + .registerTypeAdapter(EventKind.class, new EventKindTypeAdapter()) + .create(); + + public static T fromJson(String json, Class classOfT) throws JsonProcessingException { + try { + return OBJECT_MAPPER.fromJson(json, classOfT); + } catch (JsonSyntaxException e) { + throw new JsonProcessingException("Failed to parse JSON", e); + } + } + + public static T fromJson(String json, Type type) throws JsonProcessingException { + try { + return OBJECT_MAPPER.fromJson(json, type); + } catch (JsonSyntaxException e) { + throw new JsonProcessingException("Failed to parse JSON", e); + } + } + + /** + * Serializes an object to a JSON string using Gson. + *

+ * This method uses the pre-configured {@link #OBJECT_MAPPER} to produce + * JSON representation of the provided object. + * + * @param data the object to serialize + * @return JSON string representation of the object + */ + public static String toJson(Object data) throws JsonProcessingException { + try { + return OBJECT_MAPPER.toJson(data); + } catch (JsonSyntaxException e) { + throw new JsonProcessingException("Failed to generate JSON", e); + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link OffsetDateTime} to/from ISO-8601 format. + *

+ * This adapter ensures that OffsetDateTime instances are serialized to ISO-8601 formatted strings + * (e.g., "2023-10-01T12:00:00.234-05:00") and deserialized from the same format. + * This is necessary because Gson cannot access private fields of java.time classes via reflection + * in Java 17+ due to module system restrictions. + *

+ * The adapter uses {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME} for both serialization and + * deserialization, which ensures proper handling of timezone offsets. + * + * @see OffsetDateTime + * @see DateTimeFormatter#ISO_OFFSET_DATE_TIME + */ + static class OffsetDateTimeTypeAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, OffsetDateTime value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); + } + } + + @Override + public @Nullable + OffsetDateTime read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + String dateTimeString = in.nextString(); + try { + return OffsetDateTime.parse(dateTimeString, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } catch (DateTimeParseException e) { + throw new JsonSyntaxException("Failed to parse OffsetDateTime: " + dateTimeString, e); + } + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link Throwable} and its subclasses. + *

+ * This adapter avoids reflection into {@link Throwable}'s private fields, which is not allowed + * in Java 17+ due to module system restrictions. Instead, it serializes Throwables as simple + * objects containing only the type (fully qualified class name) and message. + *

+ * Serialization: Converts a Throwable to a JSON object with: + *

    + *
  • "type": The fully qualified class name (e.g., "java.lang.IllegalArgumentException")
  • + *
  • "message": The exception message
  • + *
+ *

+ * Deserialization: Reads the JSON and reconstructs the Throwable using reflection to find + * a constructor that accepts a String message parameter. If no such constructor exists or if + * instantiation fails, returns a generic {@link RuntimeException} with the message. + * + * @see Throwable + */ + static class ThrowableTypeAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, Throwable value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + return; + } + out.beginObject(); + out.name("type").value(value.getClass().getName()); + out.name("message").value(value.getMessage()); + out.name(THROWABLE_MARKER_FIELD).value(true); + out.endObject(); + } + + @Override + public @Nullable + Throwable read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + + String type = null; + String message = null; + + in.beginObject(); + while (in.hasNext()) { + String fieldName = in.nextName(); + switch (fieldName) { + case "type" -> + type = in.nextString(); + case "message" -> + message = in.nextString(); + default -> + in.skipValue(); + } + } + in.endObject(); + + // Try to reconstruct the Throwable + if (type != null) { + try { + Class throwableClass = Class.forName(type); + if (Throwable.class.isAssignableFrom(throwableClass)) { + // Try to find a constructor that takes a String message + try { + var constructor = throwableClass.getConstructor(String.class); + return (Throwable) constructor.newInstance(message); + } catch (NoSuchMethodException e) { + // No String constructor, return a generic RuntimeException + return new RuntimeException(message); + } + } + } catch (Exception e) { + // If we can't reconstruct the exact type, return a generic RuntimeException + return new RuntimeException(message); + } + } + return new RuntimeException(message); + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link JSONRPCError} and its subclasses. + *

+ * This adapter handles polymorphic deserialization based on the error code, creating the + * appropriate subclass instance. + *

+ * The adapter maps error codes to their corresponding error classes: + *

    + *
  • -32700: {@link JSONParseError}
  • + *
  • -32600: {@link InvalidRequestError}
  • + *
  • -32601: {@link MethodNotFoundError}
  • + *
  • -32602: {@link InvalidParamsError}
  • + *
  • -32603: {@link InternalError}
  • + *
  • -32001: {@link TaskNotFoundError}
  • + *
  • -32002: {@link TaskNotCancelableError}
  • + *
  • -32003: {@link PushNotificationNotSupportedError}
  • + *
  • -32004: {@link UnsupportedOperationError}
  • + *
  • -32005: {@link ContentTypeNotSupportedError}
  • + *
  • -32006: {@link InvalidAgentResponseError}
  • + *
  • Other codes: {@link JSONRPCError}
  • + *
+ * + * @see JSONRPCError + */ + static class JSONRPCErrorTypeAdapter extends TypeAdapter { + + private static final ThrowableTypeAdapter THROWABLE_ADAPTER = new ThrowableTypeAdapter(); + static final String THROWABLE_MARKER_FIELD = "__throwable"; + private static final String CODE_FIELD = "code"; + private static final String DATA_FIELD = "data"; + private static final String MESSAGE_FIELD = "message"; + private static final String TYPE_FIELD = "type"; + + @Override + public void write(JsonWriter out, JSONRPCError value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + return; + } + out.beginObject(); + out.name(CODE_FIELD).value(value.getCode()); + out.name(MESSAGE_FIELD).value(value.getMessage()); + if (value.getData() != null) { + out.name(DATA_FIELD); + // If data is a Throwable, use ThrowableTypeAdapter to avoid reflection issues + if (value.getData() instanceof Throwable throwable) { + THROWABLE_ADAPTER.write(out, throwable); + } else { + // Use Gson to serialize the data field for non-Throwable types + OBJECT_MAPPER.toJson(value.getData(), Object.class, out); + } + } + out.endObject(); + } + + @Override + public @Nullable + JSONRPCError read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + + Integer code = null; + String message = null; + Object data = null; + + in.beginObject(); + while (in.hasNext()) { + String fieldName = in.nextName(); + switch (fieldName) { + case CODE_FIELD -> + code = in.nextInt(); + case MESSAGE_FIELD -> + message = in.nextString(); + case DATA_FIELD -> { + // Read data as a generic object (could be string, number, object, etc.) + data = readDataValue(in); + } + default -> + in.skipValue(); + } + } + in.endObject(); + + // Create the appropriate subclass based on the error code + return createErrorInstance(code, message, data); + } + + /** + * Reads the data field value, which can be of any JSON type. + */ + private @Nullable + Object readDataValue(JsonReader in) throws java.io.IOException { + return switch (in.peek()) { + case STRING -> + in.nextString(); + case NUMBER -> + in.nextDouble(); + case BOOLEAN -> + in.nextBoolean(); + case NULL -> { + in.nextNull(); + yield null; + } + case BEGIN_OBJECT -> { + // Parse as JsonElement to check if it's a Throwable + com.google.gson.JsonElement element = com.google.gson.JsonParser.parseReader(in); + if (element.isJsonObject()) { + com.google.gson.JsonObject obj = element.getAsJsonObject(); + // Check if it has the structure of a serialized Throwable (type + message) + if (obj.has(TYPE_FIELD) && obj.has(MESSAGE_FIELD) && obj.has(THROWABLE_MARKER_FIELD)) { + // Deserialize as Throwable using ThrowableTypeAdapter + yield THROWABLE_ADAPTER.read(new JsonReader(new StringReader(element.toString()))); + } + } + // Otherwise, deserialize as generic object + yield OBJECT_MAPPER.fromJson(element, Object.class); + } + case BEGIN_ARRAY -> + // For arrays, read as raw JSON using Gson + OBJECT_MAPPER.fromJson(in, Object.class); + default -> { + in.skipValue(); + yield null; + } + }; + } + + /** + * Creates the appropriate JSONRPCError subclass based on the error code. + */ + private JSONRPCError createErrorInstance(@Nullable Integer code, @Nullable String message, @Nullable Object data) { + if (code == null) { + throw new JsonSyntaxException("JSONRPCError must have a code field"); + } + + return switch (code) { + case JSON_PARSE_ERROR_CODE -> + new JSONParseError(code, message, data); + case INVALID_REQUEST_ERROR_CODE -> + new InvalidRequestError(code, message, data); + case METHOD_NOT_FOUND_ERROR_CODE -> + new MethodNotFoundError(code, message, data); + case INVALID_PARAMS_ERROR_CODE -> + new InvalidParamsError(code, message, data); + case INTERNAL_ERROR_CODE -> + new io.a2a.spec.InternalError(code, message, data); + case TASK_NOT_FOUND_ERROR_CODE -> + new TaskNotFoundError(code, message, data); + case TASK_NOT_CANCELABLE_ERROR_CODE -> + new TaskNotCancelableError(code, message, data); + case PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE -> + new PushNotificationNotSupportedError(code, message, data); + case UNSUPPORTED_OPERATION_ERROR_CODE -> + new UnsupportedOperationError(code, message, data); + case CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE -> + new ContentTypeNotSupportedError(code, message, data); + case INVALID_AGENT_RESPONSE_ERROR_CODE -> + new InvalidAgentResponseError(code, message, data); + default -> + new JSONRPCError(code, message, data); + }; + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link TaskState} enum. + *

+ * This adapter ensures that TaskState enum values are serialized using their + * wire format string representation (e.g., "completed", "working") rather than + * the Java enum constant name (e.g., "COMPLETED", "WORKING"). + *

+ * For serialization, it uses {@link TaskState#asString()} to get the wire format. + * For deserialization, it uses {@link TaskState#fromString(String)} to parse the + * wire format back to the enum constant. + * + * @see TaskState + * @see TaskState#asString() + * @see TaskState#fromString(String) + */ + static class TaskStateTypeAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, TaskState value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.asString()); + } + } + + @Override + public @Nullable + TaskState read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + String stateString = in.nextString(); + try { + return TaskState.fromString(stateString); + } catch (IllegalArgumentException e) { + throw new JsonSyntaxException("Invalid TaskState: " + stateString, e); + } + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link Message.Role} enum. + *

+ * This adapter ensures that Message.Role enum values are serialized using their + * wire format string representation (e.g., "user", "agent") rather than the Java + * enum constant name (e.g., "USER", "AGENT"). + *

+ * For serialization, it uses {@link Message.Role#asString()} to get the wire format. + * For deserialization, it parses the string to the enum constant. + * + * @see Message.Role + * @see Message.Role#asString() + */ + static class RoleTypeAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, Message.Role value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.asString()); + } + } + + @Override + public Message.@Nullable Role read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + String roleString = in.nextString(); + try { + return switch (roleString) { + case "user" -> + Message.Role.USER; + case "agent" -> + Message.Role.AGENT; + default -> + throw new IllegalArgumentException("Invalid Role: " + roleString); + }; + } catch (IllegalArgumentException e) { + throw new JsonSyntaxException("Invalid Message.Role: " + roleString, e); + } + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link Part.Kind} enum. + *

+ * This adapter ensures that Part.Kind enum values are serialized using their + * wire format string representation (e.g., "text", "file", "data") rather than + * the Java enum constant name (e.g., "TEXT", "FILE", "DATA"). + *

+ * For serialization, it uses {@link Part.Kind#asString()} to get the wire format. + * For deserialization, it parses the string to the enum constant. + * + * @see Part.Kind + * @see Part.Kind#asString() + */ + static class PartKindTypeAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, Part.Kind value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.asString()); + } + } + + @Override + public Part.@Nullable Kind read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + String kindString = in.nextString(); + try { + return switch (kindString) { + case "text" -> + Part.Kind.TEXT; + case "file" -> + Part.Kind.FILE; + case "data" -> + Part.Kind.DATA; + default -> + throw new IllegalArgumentException("Invalid Part.Kind: " + kindString); + }; + } catch (IllegalArgumentException e) { + throw new JsonSyntaxException("Invalid Part.Kind: " + kindString, e); + } + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link Part} and its subclasses. + *

+ * This adapter handles polymorphic deserialization based on the "kind" field, creating the + * appropriate subclass instance (TextPart, FilePart, or DataPart). + *

+ * The adapter uses a two-pass approach: first reads the JSON as a tree to inspect the "kind" + * field, then deserializes to the appropriate concrete type. + * + * @see Part + * @see TextPart + * @see FilePart + * @see DataPart + */ + static class PartTypeAdapter extends TypeAdapter> { + + // Create separate Gson instance without the Part adapter to avoid recursion + private final Gson delegateGson = createBaseGsonBuilder().create(); + + @Override + public void write(JsonWriter out, Part value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + return; + } + // Delegate to Gson's default serialization for the concrete type + if (value instanceof TextPart textPart) { + delegateGson.toJson(textPart, TextPart.class, out); + } else if (value instanceof FilePart filePart) { + delegateGson.toJson(filePart, FilePart.class, out); + } else if (value instanceof DataPart dataPart) { + delegateGson.toJson(dataPart, DataPart.class, out); + } else { + throw new JsonSyntaxException("Unknown Part subclass: " + value.getClass().getName()); + } + } + + @Override + public @Nullable + Part read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + + // Read the JSON as a tree so we can inspect the "kind" field + com.google.gson.JsonElement jsonElement = com.google.gson.JsonParser.parseReader(in); + if (!jsonElement.isJsonObject()) { + throw new JsonSyntaxException("Part must be a JSON object"); + } + + com.google.gson.JsonObject jsonObject = jsonElement.getAsJsonObject(); + com.google.gson.JsonElement kindElement = jsonObject.get("kind"); + if (kindElement == null || !kindElement.isJsonPrimitive()) { + throw new JsonSyntaxException("Part must have a 'kind' field"); + } + + String kind = kindElement.getAsString(); + // Use the delegate Gson to deserialize to the concrete type + return switch (kind) { + case "text" -> + delegateGson.fromJson(jsonElement, TextPart.class); + case "file" -> + delegateGson.fromJson(jsonElement, FilePart.class); + case "data" -> + delegateGson.fromJson(jsonElement, DataPart.class); + default -> + throw new JsonSyntaxException("Unknown Part kind: " + kind); + }; + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link EventKind} and its implementations. + *

+ * Discriminates based on the {@code "kind"} field: + *

    + *
  • {@code "task"} → {@link Task}
  • + *
  • {@code "message"} → {@link Message}
  • + *
+ */ + static class EventKindTypeAdapter extends TypeAdapter { + + private final Gson delegateGson = createBaseGsonBuilder() + .registerTypeHierarchyAdapter(Part.class, new PartTypeAdapter()) + .create(); + + @Override + public void write(JsonWriter out, EventKind value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + return; + } + if (value instanceof Task task) { + delegateGson.toJson(task, Task.class, out); + } else if (value instanceof Message message) { + delegateGson.toJson(message, Message.class, out); + } else { + throw new JsonSyntaxException("Unknown EventKind implementation: " + value.getClass().getName()); + } + } + + @Override + public @Nullable EventKind read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + + com.google.gson.JsonElement jsonElement = com.google.gson.JsonParser.parseReader(in); + if (!jsonElement.isJsonObject()) { + throw new JsonSyntaxException("EventKind must be a JSON object"); + } + + com.google.gson.JsonObject jsonObject = jsonElement.getAsJsonObject(); + com.google.gson.JsonElement kindElement = jsonObject.get("kind"); + if (kindElement == null || !kindElement.isJsonPrimitive()) { + throw new JsonSyntaxException("EventKind must have a 'kind' field"); + } + + String kind = kindElement.getAsString(); + return switch (kind) { + case Task.TASK -> delegateGson.fromJson(jsonElement, Task.class); + case Message.MESSAGE -> delegateGson.fromJson(jsonElement, Message.class); + default -> throw new JsonSyntaxException("Unknown EventKind kind: " + kind); + }; + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link StreamingEventKind} and its implementations. + *

+ * This adapter handles polymorphic deserialization based on the "kind" field, creating the + * appropriate implementation instance (Task, Message, TaskStatusUpdateEvent, or TaskArtifactUpdateEvent). + *

+ * The adapter uses a two-pass approach: first reads the JSON as a tree to inspect the "kind" + * field, then deserializes to the appropriate concrete type. + * + * @see StreamingEventKind + * @see Task + * @see Message + * @see TaskStatusUpdateEvent + * @see TaskArtifactUpdateEvent + */ + static class StreamingEventKindTypeAdapter extends TypeAdapter { + + // Create separate Gson instance without the StreamingEventKind adapter to avoid recursion + private final Gson delegateGson = createBaseGsonBuilder() + .registerTypeHierarchyAdapter(Part.class, new PartTypeAdapter()) + .create(); + + @Override + public void write(JsonWriter out, StreamingEventKind value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + return; + } + // Delegate to Gson's default serialization for the concrete type + if (value instanceof Task task) { + delegateGson.toJson(task, Task.class, out); + } else if (value instanceof Message message) { + delegateGson.toJson(message, Message.class, out); + } else if (value instanceof TaskStatusUpdateEvent event) { + delegateGson.toJson(event, TaskStatusUpdateEvent.class, out); + } else if (value instanceof TaskArtifactUpdateEvent event) { + delegateGson.toJson(event, TaskArtifactUpdateEvent.class, out); + } else { + throw new JsonSyntaxException("Unknown StreamingEventKind implementation: " + value.getClass().getName()); + } + } + + @Override + public @Nullable + StreamingEventKind read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + + // Read the JSON as a tree so we can inspect the "kind" field + com.google.gson.JsonElement jsonElement = com.google.gson.JsonParser.parseReader(in); + if (!jsonElement.isJsonObject()) { + throw new JsonSyntaxException("StreamingEventKind must be a JSON object"); + } + + com.google.gson.JsonObject jsonObject = jsonElement.getAsJsonObject(); + com.google.gson.JsonElement kindElement = jsonObject.get("kind"); + if (kindElement == null || !kindElement.isJsonPrimitive()) { + throw new JsonSyntaxException("StreamingEventKind must have a 'kind' field"); + } + + String kind = kindElement.getAsString(); + // Use the delegate Gson to deserialize to the concrete type + return switch (kind) { + case "task" -> + delegateGson.fromJson(jsonElement, Task.class); + case "message" -> + delegateGson.fromJson(jsonElement, Message.class); + case "status-update" -> + delegateGson.fromJson(jsonElement, TaskStatusUpdateEvent.class); + case "artifact-update" -> + delegateGson.fromJson(jsonElement, TaskArtifactUpdateEvent.class); + default -> + throw new JsonSyntaxException("Unknown StreamingEventKind kind: " + kind); + }; + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link SecurityScheme} and its implementations. + *

+ * Discriminates based on the {@code "type"} field: + *

    + *
  • {@code "apiKey"} → {@link APIKeySecurityScheme}
  • + *
  • {@code "http"} → {@link HTTPAuthSecurityScheme}
  • + *
  • {@code "oauth2"} → {@link OAuth2SecurityScheme}
  • + *
  • {@code "openIdConnect"} → {@link OpenIdConnectSecurityScheme}
  • + *
  • {@code "mutualTLS"} → {@link MutualTLSSecurityScheme}
  • + *
+ */ + static class SecuritySchemeTypeAdapter extends TypeAdapter { + + // Use a plain Gson to avoid circular initialization — SecurityScheme concrete types + // contain only simple fields (Strings, OAuthFlows) that need no custom adapters. + private final Gson delegateGson = new Gson(); + + @Override + public void write(JsonWriter out, SecurityScheme value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + return; + } + if (value instanceof APIKeySecurityScheme v) { + delegateGson.toJson(v, APIKeySecurityScheme.class, out); + } else if (value instanceof HTTPAuthSecurityScheme v) { + delegateGson.toJson(v, HTTPAuthSecurityScheme.class, out); + } else if (value instanceof OAuth2SecurityScheme v) { + delegateGson.toJson(v, OAuth2SecurityScheme.class, out); + } else if (value instanceof OpenIdConnectSecurityScheme v) { + delegateGson.toJson(v, OpenIdConnectSecurityScheme.class, out); + } else if (value instanceof MutualTLSSecurityScheme v) { + delegateGson.toJson(v, MutualTLSSecurityScheme.class, out); + } else { + throw new JsonSyntaxException("Unknown SecurityScheme implementation: " + value.getClass().getName()); + } + } + + @Override + public @Nullable SecurityScheme read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + + com.google.gson.JsonElement jsonElement = com.google.gson.JsonParser.parseReader(in); + if (!jsonElement.isJsonObject()) { + throw new JsonSyntaxException("SecurityScheme must be a JSON object"); + } + + com.google.gson.JsonObject jsonObject = jsonElement.getAsJsonObject(); + com.google.gson.JsonElement typeElement = jsonObject.get("type"); + if (typeElement == null || !typeElement.isJsonPrimitive()) { + throw new JsonSyntaxException("SecurityScheme must have a 'type' field"); + } + + String type = typeElement.getAsString(); + return switch (type) { + case APIKeySecurityScheme.API_KEY -> + delegateGson.fromJson(jsonElement, APIKeySecurityScheme.class); + case HTTPAuthSecurityScheme.HTTP -> + delegateGson.fromJson(jsonElement, HTTPAuthSecurityScheme.class); + case OAuth2SecurityScheme.OAUTH2 -> + delegateGson.fromJson(jsonElement, OAuth2SecurityScheme.class); + case OpenIdConnectSecurityScheme.OPENID_CONNECT -> + delegateGson.fromJson(jsonElement, OpenIdConnectSecurityScheme.class); + case MutualTLSSecurityScheme.MUTUAL_TLS -> + delegateGson.fromJson(jsonElement, MutualTLSSecurityScheme.class); + default -> + throw new JsonSyntaxException("Unknown SecurityScheme type: " + type); + }; + } + } + + /** + * Gson TypeAdapter for serializing and deserializing {@link FileContent} and its implementations. + *

+ * This adapter handles polymorphic deserialization for the sealed FileContent interface, + * which permits two implementations: + *

    + *
  • {@link FileWithBytes} - File content embedded as base64-encoded bytes
  • + *
  • {@link FileWithUri} - File content referenced by URI
  • + *
+ *

+ * The adapter distinguishes between the two types by checking for the presence of + * "bytes" or "uri" fields in the JSON object. + * + * @see FileContent + * @see FileWithBytes + * @see FileWithUri + */ + static class FileContentTypeAdapter extends TypeAdapter { + + // Create separate Gson instance without the FileContent adapter to avoid recursion + private final Gson delegateGson = new Gson(); + + @Override + public void write(JsonWriter out, FileContent value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + return; + } + // Delegate to Gson's default serialization for the concrete type + if (value instanceof FileWithBytes fileWithBytes) { + delegateGson.toJson(fileWithBytes, FileWithBytes.class, out); + } else if (value instanceof FileWithUri fileWithUri) { + delegateGson.toJson(fileWithUri, FileWithUri.class, out); + } else { + throw new JsonSyntaxException("Unknown FileContent implementation: " + value.getClass().getName()); + } + } + + @Override + public @Nullable + FileContent read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; + } + + // Read the JSON as a tree to inspect the fields + com.google.gson.JsonElement jsonElement = com.google.gson.JsonParser.parseReader(in); + if (!jsonElement.isJsonObject()) { + throw new JsonSyntaxException("FileContent must be a JSON object"); + } + + com.google.gson.JsonObject jsonObject = jsonElement.getAsJsonObject(); + + // Distinguish between FileWithBytes and FileWithUri by checking for "bytes" or "uri" field + if (jsonObject.has("bytes")) { + return delegateGson.fromJson(jsonElement, FileWithBytes.class); + } else if (jsonObject.has("uri")) { + return delegateGson.fromJson(jsonElement, FileWithUri.class); + } else { + throw new JsonSyntaxException("FileContent must have either 'bytes' or 'uri' field"); + } + } + } + +} diff --git a/spec/src/main/java/io/a2a/json/package-info.java b/spec/src/main/java/io/a2a/json/package-info.java new file mode 100644 index 000000000..81fe05a3a --- /dev/null +++ b/spec/src/main/java/io/a2a/json/package-info.java @@ -0,0 +1,8 @@ +/** + * JSON processing exceptions for the A2A Java SDK. + *

+ * This package provides custom exceptions that replace Jackson's JSON processing exceptions, + * allowing the SDK to be independent of any specific JSON library implementation. + */ +@org.jspecify.annotations.NullMarked +package io.a2a.json; diff --git a/spec/src/main/java/io/a2a/spec/A2AClientJSONError.java b/spec/src/main/java/io/a2a/spec/A2AClientJSONError.java index 75988da1c..06d04605c 100644 --- a/spec/src/main/java/io/a2a/spec/A2AClientJSONError.java +++ b/spec/src/main/java/io/a2a/spec/A2AClientJSONError.java @@ -1,5 +1,30 @@ package io.a2a.spec; +/** + * Client exception indicating a JSON serialization or deserialization error. + *

+ * This exception is thrown when the A2A client SDK encounters errors while + * parsing JSON responses from agents or serializing requests. This typically + * indicates: + *

    + *
  • Malformed JSON in agent responses
  • + *
  • Unexpected JSON structure or field types
  • + *
  • Missing required JSON fields
  • + *
  • JSON encoding/decoding errors
  • + *
+ *

+ * Usage example: + *

{@code
+ * try {
+ *     AgentCard card = objectMapper.readValue(json, AgentCard.class);
+ * } catch (io.a2a.json.JsonProcessingException e) {
+ *     throw new A2AClientJSONError("Failed to parse agent card", e);
+ * }
+ * }
+ * + * @see A2AClientError for the base client error class + * @see JSONParseError for protocol-level JSON errors + */ public class A2AClientJSONError extends A2AClientError { public A2AClientJSONError() { diff --git a/spec/src/main/java/io/a2a/spec/A2AErrorCodes.java b/spec/src/main/java/io/a2a/spec/A2AErrorCodes.java new file mode 100644 index 000000000..09175b744 --- /dev/null +++ b/spec/src/main/java/io/a2a/spec/A2AErrorCodes.java @@ -0,0 +1,22 @@ +package io.a2a.spec; + +/** + * All the error codes for A2A errors. + */ +public interface A2AErrorCodes { + + int TASK_NOT_FOUND_ERROR_CODE = -32001; + int TASK_NOT_CANCELABLE_ERROR_CODE = -32002; + int PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE = -32003; + int UNSUPPORTED_OPERATION_ERROR_CODE = -32004; + int CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE = -32005; + int INVALID_AGENT_RESPONSE_ERROR_CODE = -32006; + int AUTHENTICATED_EXTENDED_CARD_NOT_CONFIGURED_ERROR_CODE = -32007; + + int INVALID_REQUEST_ERROR_CODE = -32600; + int METHOD_NOT_FOUND_ERROR_CODE = -32601; + int INVALID_PARAMS_ERROR_CODE = -32602; + int INTERNAL_ERROR_CODE = -32603; + + int JSON_PARSE_ERROR_CODE = -32700; +} diff --git a/spec/src/main/java/io/a2a/spec/APIKeySecurityScheme.java b/spec/src/main/java/io/a2a/spec/APIKeySecurityScheme.java index 028fea303..a2dc48c4d 100644 --- a/spec/src/main/java/io/a2a/spec/APIKeySecurityScheme.java +++ b/spec/src/main/java/io/a2a/spec/APIKeySecurityScheme.java @@ -1,12 +1,5 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.annotation.JsonValue; - import io.a2a.util.Assert; import static io.a2a.spec.APIKeySecurityScheme.API_KEY; @@ -14,9 +7,6 @@ /** * Defines a security scheme using an API key. */ -@JsonTypeName(API_KEY) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class APIKeySecurityScheme implements SecurityScheme { public static final String API_KEY = "apiKey"; @@ -39,12 +29,10 @@ public enum Location { this.location = location; } - @JsonValue public String asString() { return location; } - @JsonCreator public static Location fromString(String location) { switch (location) { case "cookie" -> { @@ -65,9 +53,8 @@ public APIKeySecurityScheme(String in, String name, String description) { this(in, name, description, API_KEY); } - @JsonCreator - public APIKeySecurityScheme(@JsonProperty("in") String in, @JsonProperty("name") String name, - @JsonProperty("description") String description, @JsonProperty("type") String type) { + public APIKeySecurityScheme(String in, String name, + String description, String type) { Assert.checkNotNullParam("in", in); Assert.checkNotNullParam("name", name); Assert.checkNotNullParam("type", type); diff --git a/spec/src/main/java/io/a2a/spec/AgentCapabilities.java b/spec/src/main/java/io/a2a/spec/AgentCapabilities.java index 1c6fdf1b2..1de51d5f9 100644 --- a/spec/src/main/java/io/a2a/spec/AgentCapabilities.java +++ b/spec/src/main/java/io/a2a/spec/AgentCapabilities.java @@ -2,14 +2,9 @@ import java.util.List; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - /** * Defines optional capabilities supported by an agent. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record AgentCapabilities(boolean streaming, boolean pushNotifications, boolean stateTransitionHistory, List extensions) { diff --git a/spec/src/main/java/io/a2a/spec/AgentCard.java b/spec/src/main/java/io/a2a/spec/AgentCard.java index b59a9403b..2574f5425 100644 --- a/spec/src/main/java/io/a2a/spec/AgentCard.java +++ b/spec/src/main/java/io/a2a/spec/AgentCard.java @@ -4,8 +4,6 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** @@ -13,8 +11,6 @@ * metadata including the agent's identity, capabilities, skills, supported * communication methods, and security requirements. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record AgentCard(String name, String description, String url, AgentProvider provider, String version, String documentationUrl, AgentCapabilities capabilities, List defaultInputModes, List defaultOutputModes, List skills, diff --git a/spec/src/main/java/io/a2a/spec/AgentCardSignature.java b/spec/src/main/java/io/a2a/spec/AgentCardSignature.java index 4e383d998..70a92cd57 100644 --- a/spec/src/main/java/io/a2a/spec/AgentCardSignature.java +++ b/spec/src/main/java/io/a2a/spec/AgentCardSignature.java @@ -1,20 +1,15 @@ package io.a2a.spec; +import com.google.gson.annotations.SerializedName; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import io.a2a.util.Assert; /** * Represents a JWS signature of an AgentCard. * This follows the JSON format of an RFC 7515 JSON Web Signature (JWS). */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) -public record AgentCardSignature(Map header, @JsonProperty("protected") String protectedHeader, +public record AgentCardSignature(Map header, @SerializedName("protected")String protectedHeader, String signature) { public AgentCardSignature { diff --git a/spec/src/main/java/io/a2a/spec/AgentInterface.java b/spec/src/main/java/io/a2a/spec/AgentInterface.java index db81ce8f0..0b2e8d8b0 100644 --- a/spec/src/main/java/io/a2a/spec/AgentInterface.java +++ b/spec/src/main/java/io/a2a/spec/AgentInterface.java @@ -1,16 +1,13 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; + import io.a2a.util.Assert; /** * Declares a combination of a target URL and a transport protocol for interacting with the agent. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) -public record AgentInterface(String transport, String url) { +public record AgentInterface(String transport, String url) { public AgentInterface { Assert.checkNotNullParam("transport", transport); Assert.checkNotNullParam("url", url); diff --git a/spec/src/main/java/io/a2a/spec/AgentProvider.java b/spec/src/main/java/io/a2a/spec/AgentProvider.java index fa57b4478..1d50b699e 100644 --- a/spec/src/main/java/io/a2a/spec/AgentProvider.java +++ b/spec/src/main/java/io/a2a/spec/AgentProvider.java @@ -1,14 +1,10 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * Represents the service provider of an agent. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record AgentProvider(String organization, String url) { public AgentProvider { diff --git a/spec/src/main/java/io/a2a/spec/AgentSkill.java b/spec/src/main/java/io/a2a/spec/AgentSkill.java index 3802b3418..b397f6248 100644 --- a/spec/src/main/java/io/a2a/spec/AgentSkill.java +++ b/spec/src/main/java/io/a2a/spec/AgentSkill.java @@ -3,15 +3,11 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * The set of skills, or distinct capabilities, that the agent can perform. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record AgentSkill(String id, String name, String description, List tags, List examples, List inputModes, List outputModes, List>> security) { diff --git a/spec/src/main/java/io/a2a/spec/Artifact.java b/spec/src/main/java/io/a2a/spec/Artifact.java index 798ac5823..69d2f0581 100644 --- a/spec/src/main/java/io/a2a/spec/Artifact.java +++ b/spec/src/main/java/io/a2a/spec/Artifact.java @@ -3,15 +3,11 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * Represents a file, data structure, or other resource generated by an agent during a task. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record Artifact(String artifactId, String name, String description, List> parts, Map metadata, List extensions) { diff --git a/spec/src/main/java/io/a2a/spec/AuthenticatedExtendedCardNotConfiguredError.java b/spec/src/main/java/io/a2a/spec/AuthenticatedExtendedCardNotConfiguredError.java index 323cd147d..7ac3e9d9c 100644 --- a/spec/src/main/java/io/a2a/spec/AuthenticatedExtendedCardNotConfiguredError.java +++ b/spec/src/main/java/io/a2a/spec/AuthenticatedExtendedCardNotConfiguredError.java @@ -2,34 +2,19 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; /** * An A2A-specific error indicating that the agent does not have an * Authenticated Extended Card configured */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class AuthenticatedExtendedCardNotConfiguredError extends JSONRPCError { public final static Integer DEFAULT_CODE = -32007; - @JsonCreator - public AuthenticatedExtendedCardNotConfiguredError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + public AuthenticatedExtendedCardNotConfiguredError(Integer code, String message, Object data) { super( defaultIfNull(code, DEFAULT_CODE), defaultIfNull(message, "Authenticated Extended Card not configured"), data); } - - public AuthenticatedExtendedCardNotConfiguredError() { - this(null, null, null); - } - } diff --git a/spec/src/main/java/io/a2a/spec/AuthenticationInfo.java b/spec/src/main/java/io/a2a/spec/AuthenticationInfo.java index d28a1e173..4f24e3c4c 100644 --- a/spec/src/main/java/io/a2a/spec/AuthenticationInfo.java +++ b/spec/src/main/java/io/a2a/spec/AuthenticationInfo.java @@ -2,15 +2,11 @@ import java.util.List; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * The authentication info for an agent. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record AuthenticationInfo(List schemes, String credentials) { public AuthenticationInfo { diff --git a/spec/src/main/java/io/a2a/spec/AuthorizationCodeOAuthFlow.java b/spec/src/main/java/io/a2a/spec/AuthorizationCodeOAuthFlow.java index afa6fed72..cc5e0ee54 100644 --- a/spec/src/main/java/io/a2a/spec/AuthorizationCodeOAuthFlow.java +++ b/spec/src/main/java/io/a2a/spec/AuthorizationCodeOAuthFlow.java @@ -2,16 +2,12 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * Defines configuration details for the OAuth 2.0 Authorization Code flow. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record AuthorizationCodeOAuthFlow(String authorizationUrl, String refreshUrl, Map scopes, String tokenUrl) { diff --git a/spec/src/main/java/io/a2a/spec/CancelTaskRequest.java b/spec/src/main/java/io/a2a/spec/CancelTaskRequest.java index 39c370ae3..f8a1bb8bf 100644 --- a/spec/src/main/java/io/a2a/spec/CancelTaskRequest.java +++ b/spec/src/main/java/io/a2a/spec/CancelTaskRequest.java @@ -4,25 +4,17 @@ import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import io.a2a.util.Assert; /** * A request that can be used to cancel a task. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class CancelTaskRequest extends NonStreamingJSONRPCRequest { public static final String METHOD = "tasks/cancel"; - @JsonCreator - public CancelTaskRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, @JsonProperty("params") TaskIdParams params) { + public CancelTaskRequest(String jsonrpc, Object id, String method, TaskIdParams params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } diff --git a/spec/src/main/java/io/a2a/spec/CancelTaskResponse.java b/spec/src/main/java/io/a2a/spec/CancelTaskResponse.java index 9ef775118..d65091080 100644 --- a/spec/src/main/java/io/a2a/spec/CancelTaskResponse.java +++ b/spec/src/main/java/io/a2a/spec/CancelTaskResponse.java @@ -1,20 +1,12 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * A response to a cancel task request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) + public final class CancelTaskResponse extends JSONRPCResponse { - @JsonCreator - public CancelTaskResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") Task result, @JsonProperty("error") JSONRPCError error) { + public CancelTaskResponse(String jsonrpc, Object id, Task result, JSONRPCError error) { super(jsonrpc, id, result, error, Task.class); } diff --git a/spec/src/main/java/io/a2a/spec/ClientCredentialsOAuthFlow.java b/spec/src/main/java/io/a2a/spec/ClientCredentialsOAuthFlow.java index 372b57d78..18056681f 100644 --- a/spec/src/main/java/io/a2a/spec/ClientCredentialsOAuthFlow.java +++ b/spec/src/main/java/io/a2a/spec/ClientCredentialsOAuthFlow.java @@ -3,16 +3,12 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * Defines configuration details for the OAuth 2.0 Client Credentials flow. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record ClientCredentialsOAuthFlow(String refreshUrl, Map scopes, String tokenUrl) { public ClientCredentialsOAuthFlow { diff --git a/spec/src/main/java/io/a2a/spec/ContentTypeNotSupportedError.java b/spec/src/main/java/io/a2a/spec/ContentTypeNotSupportedError.java index 3aa245ca2..fc8c412d5 100644 --- a/spec/src/main/java/io/a2a/spec/ContentTypeNotSupportedError.java +++ b/spec/src/main/java/io/a2a/spec/ContentTypeNotSupportedError.java @@ -1,29 +1,19 @@ package io.a2a.spec; +import static io.a2a.spec.A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE; import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; /** * An A2A-specific error indicating an incompatibility between the requested * content types and the agent's capabilities. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class ContentTypeNotSupportedError extends JSONRPCError { - public final static Integer DEFAULT_CODE = -32005; + public final static Integer DEFAULT_CODE = CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE; - @JsonCreator - public ContentTypeNotSupportedError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { - super( - defaultIfNull(code, DEFAULT_CODE), + public ContentTypeNotSupportedError(Integer code, String message, Object data) { + super(defaultIfNull(code, CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE), defaultIfNull(message, "Incompatible content types"), data); } diff --git a/spec/src/main/java/io/a2a/spec/DataPart.java b/spec/src/main/java/io/a2a/spec/DataPart.java index 7ac244263..58e8ae412 100644 --- a/spec/src/main/java/io/a2a/spec/DataPart.java +++ b/spec/src/main/java/io/a2a/spec/DataPart.java @@ -2,21 +2,12 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; import io.a2a.util.Assert; -import static io.a2a.spec.DataPart.DATA; /** * Represents a structured data segment (e.g., JSON) within a message or artifact. */ -@JsonTypeName(DATA) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class DataPart extends Part> { public static final String DATA = "data"; @@ -28,9 +19,7 @@ public DataPart(Map data) { this(data, null); } - @JsonCreator - public DataPart(@JsonProperty("data") Map data, - @JsonProperty("metadata") Map metadata) { + public DataPart(Map data, Map metadata) { Assert.checkNotNullParam("data", data); this.data = data; this.metadata = metadata; diff --git a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java index a64421a4c..0cb34a38d 100644 --- a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java +++ b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java @@ -2,16 +2,12 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * Parameters for removing pushNotificationConfiguration associated with a Task. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record DeleteTaskPushNotificationConfigParams(String id, String pushNotificationConfigId, Map metadata) { public DeleteTaskPushNotificationConfigParams { diff --git a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigRequest.java b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigRequest.java index 99f50ebfd..dc3449ada 100644 --- a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigRequest.java +++ b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigRequest.java @@ -2,27 +2,17 @@ import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import io.a2a.util.Assert; import io.a2a.util.Utils; /** * A delete task push notification config request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class DeleteTaskPushNotificationConfigRequest extends NonStreamingJSONRPCRequest { public static final String METHOD = "tasks/pushNotificationConfig/delete"; - @JsonCreator - public DeleteTaskPushNotificationConfigRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, - @JsonProperty("params") DeleteTaskPushNotificationConfigParams params) { + public DeleteTaskPushNotificationConfigRequest(String jsonrpc, Object id, String method, DeleteTaskPushNotificationConfigParams params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } diff --git a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigResponse.java b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigResponse.java index 0f65b5ad5..89e4964db 100644 --- a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigResponse.java +++ b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigResponse.java @@ -1,23 +1,11 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - /** * A response for a delete task push notification config request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonSerialize(using = JSONRPCVoidResponseSerializer.class) public final class DeleteTaskPushNotificationConfigResponse extends JSONRPCResponse { - @JsonCreator - public DeleteTaskPushNotificationConfigResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") Void result, - @JsonProperty("error") JSONRPCError error) { + public DeleteTaskPushNotificationConfigResponse(String jsonrpc, Object id, Void result,JSONRPCError error) { super(jsonrpc, id, result, error, Void.class); } diff --git a/spec/src/main/java/io/a2a/spec/EventKind.java b/spec/src/main/java/io/a2a/spec/EventKind.java index a1ed7ef31..888611f81 100644 --- a/spec/src/main/java/io/a2a/spec/EventKind.java +++ b/spec/src/main/java/io/a2a/spec/EventKind.java @@ -1,21 +1,24 @@ package io.a2a.spec; -import static io.a2a.spec.Message.MESSAGE; -import static io.a2a.spec.Task.TASK; - -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; - -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "kind", - visible = true -) -@JsonSubTypes({ - @JsonSubTypes.Type(value = Task.class, name = TASK), - @JsonSubTypes.Type(value = Message.class, name = MESSAGE) -}) +/** + * Interface for events that can be returned from non-streaming A2A Protocol operations. + *

+ * EventKind represents events that are suitable for synchronous request-response patterns. + * These events provide complete state information and are typically returned as the final + * result of an operation. + *

+ * EventKind implementations use polymorphic JSON serialization with the "kind" discriminator + * to determine the concrete type during deserialization. + *

+ * Permitted implementations: + *

    + *
  • {@link Task} - Complete task state with status and artifacts
  • + *
  • {@link Message} - Full message with all content parts
  • + *
+ * + * @see StreamingEventKind + * @see Event + */ public interface EventKind { String getKind(); diff --git a/spec/src/main/java/io/a2a/spec/FileContent.java b/spec/src/main/java/io/a2a/spec/FileContent.java index f9609fb8b..c1d631e5c 100644 --- a/spec/src/main/java/io/a2a/spec/FileContent.java +++ b/spec/src/main/java/io/a2a/spec/FileContent.java @@ -1,8 +1,29 @@ package io.a2a.spec; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -@JsonDeserialize(using = FileContentDeserializer.class) +/** + * Sealed interface representing file content in the A2A Protocol. + *

+ * FileContent provides a polymorphic abstraction for file data, allowing files to be + * represented either as embedded binary content or as URI references. This flexibility + * enables different strategies for file transmission based on size, security, and + * accessibility requirements. + *

+ * The sealed interface permits only two implementations: + *

    + *
  • {@link FileWithBytes} - File content embedded as base64-encoded bytes (for small files or inline data)
  • + *
  • {@link FileWithUri} - File content referenced by URI (for large files or external resources)
  • + *
+ *

+ * Both implementations must provide: + *

    + *
  • MIME type - Describes the file format (e.g., "image/png", "application/pdf")
  • + *
  • File name - The original or display name for the file
  • + *
+ * + * @see FilePart + * @see FileWithBytes + * @see FileWithUri + */ public sealed interface FileContent permits FileWithBytes, FileWithUri { String mimeType(); diff --git a/spec/src/main/java/io/a2a/spec/FileContentDeserializer.java b/spec/src/main/java/io/a2a/spec/FileContentDeserializer.java deleted file mode 100644 index aa763db42..000000000 --- a/spec/src/main/java/io/a2a/spec/FileContentDeserializer.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.a2a.spec; - -import java.io.IOException; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; - -public class FileContentDeserializer extends StdDeserializer { - - public FileContentDeserializer() { - this(null); - } - - public FileContentDeserializer(Class vc) { - super(vc); - } - - @Override - public FileContent deserialize(JsonParser jsonParser, DeserializationContext context) - throws IOException, JsonProcessingException { - JsonNode node = jsonParser.getCodec().readTree(jsonParser); - JsonNode mimeType = node.get("mimeType"); - JsonNode name = node.get("name"); - JsonNode bytes = node.get("bytes"); - if (bytes != null) { - return new FileWithBytes(mimeType != null ? mimeType.asText() : null, - name != null ? name.asText() : null, bytes.asText()); - } else if (node.has("uri")) { - return new FileWithUri(mimeType != null ? mimeType.asText() : null, - name != null ? name.asText() : null, node.get("uri").asText()); - } else { - throw new IOException("Invalid file format: missing 'bytes' or 'uri'"); - } - } -} diff --git a/spec/src/main/java/io/a2a/spec/FilePart.java b/spec/src/main/java/io/a2a/spec/FilePart.java index 79bd2e5e8..fae1788e3 100644 --- a/spec/src/main/java/io/a2a/spec/FilePart.java +++ b/spec/src/main/java/io/a2a/spec/FilePart.java @@ -2,11 +2,6 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; import io.a2a.util.Assert; import static io.a2a.spec.FilePart.FILE; @@ -15,9 +10,6 @@ * Represents a file segment within a message or artifact. The file content can be * provided either directly as bytes or as a URI. */ -@JsonTypeName(FILE) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class FilePart extends Part { public static final String FILE = "file"; @@ -29,8 +21,7 @@ public FilePart(FileContent file) { this(file, null); } - @JsonCreator - public FilePart(@JsonProperty("file") FileContent file, @JsonProperty("metadata") Map metadata) { + public FilePart(FileContent file, Map metadata) { Assert.checkNotNullParam("file", file); this.file = file; this.metadata = metadata; diff --git a/spec/src/main/java/io/a2a/spec/FileWithBytes.java b/spec/src/main/java/io/a2a/spec/FileWithBytes.java index 782bc2c02..01ccef127 100644 --- a/spec/src/main/java/io/a2a/spec/FileWithBytes.java +++ b/spec/src/main/java/io/a2a/spec/FileWithBytes.java @@ -1,12 +1,7 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - /** * Represents a file with its content provided directly as a base64-encoded string. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record FileWithBytes(String mimeType, String name, String bytes) implements FileContent { } diff --git a/spec/src/main/java/io/a2a/spec/FileWithUri.java b/spec/src/main/java/io/a2a/spec/FileWithUri.java index afb3a87d8..e1edd4bd2 100644 --- a/spec/src/main/java/io/a2a/spec/FileWithUri.java +++ b/spec/src/main/java/io/a2a/spec/FileWithUri.java @@ -1,13 +1,8 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - /** * Represents a file with its content located at a specific URI. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record FileWithUri(String mimeType, String name, String uri) implements FileContent { } diff --git a/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardRequest.java b/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardRequest.java index 9d561c2d2..4afe2d029 100644 --- a/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardRequest.java +++ b/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardRequest.java @@ -2,10 +2,6 @@ import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import io.a2a.util.Assert; import io.a2a.util.Utils; @@ -13,15 +9,11 @@ /** * Represents a JSON-RPC request for the `agent/getAuthenticatedExtendedCard` method. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class GetAuthenticatedExtendedCardRequest extends NonStreamingJSONRPCRequest { public static final String METHOD = "agent/getAuthenticatedExtendedCard"; - @JsonCreator - public GetAuthenticatedExtendedCardRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, @JsonProperty("params") Void params) { + public GetAuthenticatedExtendedCardRequest(String jsonrpc, Object id, String method, Void params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } diff --git a/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardResponse.java b/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardResponse.java index ec624e77d..48f380f12 100644 --- a/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardResponse.java +++ b/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardResponse.java @@ -1,21 +1,11 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * A response for the `agent/getAuthenticatedExtendedCard` method. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class GetAuthenticatedExtendedCardResponse extends JSONRPCResponse { - @JsonCreator - public GetAuthenticatedExtendedCardResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") AgentCard result, - @JsonProperty("error") JSONRPCError error) { + public GetAuthenticatedExtendedCardResponse(String jsonrpc, Object id, AgentCard result, JSONRPCError error) { super(jsonrpc, id, result, error, AgentCard.class); } diff --git a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java index 627feff34..2836e2065 100644 --- a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java +++ b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java @@ -2,8 +2,6 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; import org.jspecify.annotations.Nullable; @@ -11,8 +9,6 @@ /** * Parameters for fetching a pushNotificationConfiguration associated with a Task. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record GetTaskPushNotificationConfigParams(String id, @Nullable String pushNotificationConfigId, @Nullable Map metadata) { public GetTaskPushNotificationConfigParams { diff --git a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigRequest.java b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigRequest.java index b353e0cc8..4f1ef9e88 100644 --- a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigRequest.java +++ b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigRequest.java @@ -1,9 +1,5 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import io.a2a.util.Assert; import io.a2a.util.Utils; @@ -12,15 +8,11 @@ /** * A get task push notification request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class GetTaskPushNotificationConfigRequest extends NonStreamingJSONRPCRequest { public static final String METHOD = "tasks/pushNotificationConfig/get"; - @JsonCreator - public GetTaskPushNotificationConfigRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, @JsonProperty("params") GetTaskPushNotificationConfigParams params) { + public GetTaskPushNotificationConfigRequest( String jsonrpc, Object id, String method, GetTaskPushNotificationConfigParams params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } diff --git a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigResponse.java b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigResponse.java index 116799a9e..83a125925 100644 --- a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigResponse.java +++ b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigResponse.java @@ -1,21 +1,11 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * A response for a get task push notification request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class GetTaskPushNotificationConfigResponse extends JSONRPCResponse { - @JsonCreator - public GetTaskPushNotificationConfigResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") TaskPushNotificationConfig result, - @JsonProperty("error") JSONRPCError error) { + public GetTaskPushNotificationConfigResponse(String jsonrpc, Object id, TaskPushNotificationConfig result, JSONRPCError error) { super(jsonrpc, id, result, error, TaskPushNotificationConfig.class); } diff --git a/spec/src/main/java/io/a2a/spec/GetTaskRequest.java b/spec/src/main/java/io/a2a/spec/GetTaskRequest.java index f31237af0..7b7fb8803 100644 --- a/spec/src/main/java/io/a2a/spec/GetTaskRequest.java +++ b/spec/src/main/java/io/a2a/spec/GetTaskRequest.java @@ -4,25 +4,17 @@ import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import io.a2a.util.Assert; /** * A get task request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class GetTaskRequest extends NonStreamingJSONRPCRequest { public static final String METHOD = "tasks/get"; - @JsonCreator - public GetTaskRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, @JsonProperty("params") TaskQueryParams params) { + public GetTaskRequest(String jsonrpc, Object id, String method, TaskQueryParams params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } diff --git a/spec/src/main/java/io/a2a/spec/GetTaskResponse.java b/spec/src/main/java/io/a2a/spec/GetTaskResponse.java index 0d27a8e68..f5e6fa2ee 100644 --- a/spec/src/main/java/io/a2a/spec/GetTaskResponse.java +++ b/spec/src/main/java/io/a2a/spec/GetTaskResponse.java @@ -1,20 +1,11 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * The response for a get task request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class GetTaskResponse extends JSONRPCResponse { - @JsonCreator - public GetTaskResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") Task result, @JsonProperty("error") JSONRPCError error) { + public GetTaskResponse(String jsonrpc, Object id, Task result, JSONRPCError error) { super(jsonrpc, id, result, error, Task.class); } diff --git a/spec/src/main/java/io/a2a/spec/HTTPAuthSecurityScheme.java b/spec/src/main/java/io/a2a/spec/HTTPAuthSecurityScheme.java index 408fd2605..0323d3fa9 100644 --- a/spec/src/main/java/io/a2a/spec/HTTPAuthSecurityScheme.java +++ b/spec/src/main/java/io/a2a/spec/HTTPAuthSecurityScheme.java @@ -1,11 +1,5 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import com.fasterxml.jackson.annotation.JsonTypeName; import io.a2a.util.Assert; import static io.a2a.spec.HTTPAuthSecurityScheme.HTTP; @@ -13,9 +7,6 @@ /** * Defines a security scheme using HTTP authentication. */ -@JsonTypeName(HTTP) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class HTTPAuthSecurityScheme implements SecurityScheme { public static final String HTTP = "http"; @@ -28,12 +19,10 @@ public HTTPAuthSecurityScheme(String bearerFormat, String scheme, String descrip this(bearerFormat, scheme, description, HTTP); } - @JsonCreator - public HTTPAuthSecurityScheme(@JsonProperty("bearerFormat") String bearerFormat, @JsonProperty("scheme") String scheme, - @JsonProperty("description") String description, @JsonProperty("type") String type) { + public HTTPAuthSecurityScheme(String bearerFormat, String scheme, String description, String type) { Assert.checkNotNullParam("scheme", scheme); Assert.checkNotNullParam("type", type); - if (! type.equals(HTTP)) { + if (! HTTP.equals(type)) { throw new IllegalArgumentException("Invalid type for HTTPAuthSecurityScheme"); } this.bearerFormat = bearerFormat; diff --git a/spec/src/main/java/io/a2a/spec/IdJsonMappingException.java b/spec/src/main/java/io/a2a/spec/IdJsonMappingException.java index 15e0b07b1..00270f909 100644 --- a/spec/src/main/java/io/a2a/spec/IdJsonMappingException.java +++ b/spec/src/main/java/io/a2a/spec/IdJsonMappingException.java @@ -1,18 +1,18 @@ package io.a2a.spec; -import com.fasterxml.jackson.databind.JsonMappingException; +import io.a2a.json.JsonMappingException; public class IdJsonMappingException extends JsonMappingException { Object id; public IdJsonMappingException(String msg, Object id) { - super(null, msg); + super(msg); this.id = id; } public IdJsonMappingException(String msg, Throwable cause, Object id) { - super(null, msg, cause); + super(msg, cause); this.id = id; } diff --git a/spec/src/main/java/io/a2a/spec/ImplicitOAuthFlow.java b/spec/src/main/java/io/a2a/spec/ImplicitOAuthFlow.java index 8e2d529ea..cd2ef6235 100644 --- a/spec/src/main/java/io/a2a/spec/ImplicitOAuthFlow.java +++ b/spec/src/main/java/io/a2a/spec/ImplicitOAuthFlow.java @@ -2,16 +2,12 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * Defines configuration details for the OAuth 2.0 Implicit flow. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record ImplicitOAuthFlow(String authorizationUrl, String refreshUrl, Map scopes) { public ImplicitOAuthFlow { diff --git a/spec/src/main/java/io/a2a/spec/InternalError.java b/spec/src/main/java/io/a2a/spec/InternalError.java index ae52ecb70..f319c43ea 100644 --- a/spec/src/main/java/io/a2a/spec/InternalError.java +++ b/spec/src/main/java/io/a2a/spec/InternalError.java @@ -2,27 +2,17 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; /** * An error indicating an internal error on the server. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class InternalError extends JSONRPCError { - public final static Integer DEFAULT_CODE = -32603; + public final static Integer DEFAULT_CODE = A2AErrorCodes.INTERNAL_ERROR_CODE; - @JsonCreator - public InternalError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + public InternalError(Integer code, String message, Object data) { super( - defaultIfNull(code, DEFAULT_CODE), + defaultIfNull(code, A2AErrorCodes.INTERNAL_ERROR_CODE), defaultIfNull(message, "Internal Error"), data); } diff --git a/spec/src/main/java/io/a2a/spec/InvalidAgentResponseError.java b/spec/src/main/java/io/a2a/spec/InvalidAgentResponseError.java index faab71a96..d96e2d870 100644 --- a/spec/src/main/java/io/a2a/spec/InvalidAgentResponseError.java +++ b/spec/src/main/java/io/a2a/spec/InvalidAgentResponseError.java @@ -2,28 +2,48 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; /** - * An A2A-specific error indicating that the agent returned a response that - * does not conform to the specification for the current method. + * A2A Protocol error indicating that an agent returned a response not conforming to protocol specifications. + *

+ * This error is typically raised by client implementations when validating agent responses. + * It indicates that the agent's response structure, content, or format violates the A2A Protocol + * requirements for the invoked method. + *

+ * Common violations: + *

    + *
  • Missing required fields in response objects
  • + *
  • Invalid field types or values
  • + *
  • Malformed event stream data
  • + *
  • Response doesn't match declared agent capabilities
  • + *
+ *

+ * Corresponds to A2A-specific error code {@code -32006}. + *

+ * Usage example: + *

{@code
+ * SendMessageResponse response = client.sendMessage(request);
+ * if (response.task() == null) {
+ *     throw new InvalidAgentResponseError(
+ *         null,
+ *         "Response missing required 'task' field",
+ *         null
+ *     );
+ * }
+ * }
+ * + * @see JSONRPCResponse for response structure + * @see SendMessageResponse for message send response + * @see A2A Protocol Specification + */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class InvalidAgentResponseError extends JSONRPCError { - public final static Integer DEFAULT_CODE = -32006; + public final static Integer DEFAULT_CODE = A2AErrorCodes.INVALID_AGENT_RESPONSE_ERROR_CODE; - @JsonCreator - public InvalidAgentResponseError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + public InvalidAgentResponseError(Integer code, String message, Object data) { super( - defaultIfNull(code, DEFAULT_CODE), + defaultIfNull(code, A2AErrorCodes.INVALID_AGENT_RESPONSE_ERROR_CODE), defaultIfNull(message, "Invalid agent response"), data); } diff --git a/spec/src/main/java/io/a2a/spec/InvalidParamsError.java b/spec/src/main/java/io/a2a/spec/InvalidParamsError.java index c71ea14bf..eb067eec5 100644 --- a/spec/src/main/java/io/a2a/spec/InvalidParamsError.java +++ b/spec/src/main/java/io/a2a/spec/InvalidParamsError.java @@ -2,27 +2,40 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** - * An error indicating that the method parameters are invalid. + * JSON-RPC error indicating that method parameters are invalid or missing required fields. + *

+ * This error is returned when a JSON-RPC method is called with parameters that fail validation. + * Common causes include: + *

    + *
  • Missing required parameters
  • + *
  • Parameters of incorrect type
  • + *
  • Parameter values outside acceptable ranges
  • + *
  • Malformed parameter structures
  • + *
+ *

+ * Corresponds to JSON-RPC 2.0 error code {@code -32602}. + *

+ * Usage example: + *

{@code
+ * // Default error with standard message
+ * throw new InvalidParamsError();
+ *
+ * // Custom error message
+ * throw new InvalidParamsError("taskId parameter is required");
+ * }
+ * + * @see JSONRPCError for the base error class + * @see A2AError for the error marker interface + * @see JSON-RPC 2.0 Error Codes */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class InvalidParamsError extends JSONRPCError { - public final static Integer DEFAULT_CODE = -32602; + public final static Integer DEFAULT_CODE = A2AErrorCodes.INVALID_PARAMS_ERROR_CODE; - @JsonCreator - public InvalidParamsError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + public InvalidParamsError(Integer code, String message, Object data) { super( - defaultIfNull(code, DEFAULT_CODE), + defaultIfNull(code, A2AErrorCodes.INVALID_PARAMS_ERROR_CODE), defaultIfNull(message, "Invalid parameters"), data); } diff --git a/spec/src/main/java/io/a2a/spec/InvalidRequestError.java b/spec/src/main/java/io/a2a/spec/InvalidRequestError.java index 4d9e50779..66d9ddb05 100644 --- a/spec/src/main/java/io/a2a/spec/InvalidRequestError.java +++ b/spec/src/main/java/io/a2a/spec/InvalidRequestError.java @@ -2,31 +2,45 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; /** - * An error indicating that the JSON sent is not a valid Request object. + * JSON-RPC error indicating that the request payload is not a valid JSON-RPC Request object. + *

+ * This error is returned when the JSON-RPC request fails structural validation. + * Common causes include: + *

    + *
  • Missing required JSON-RPC fields (jsonrpc, method, id)
  • + *
  • Invalid JSON-RPC version (must be "2.0")
  • + *
  • Malformed request structure
  • + *
  • Type mismatches in required fields
  • + *
+ *

+ * Corresponds to JSON-RPC 2.0 error code {@code -32600}. + *

+ * Usage example: + *

{@code
+ * // Default error with standard message
+ * throw new InvalidRequestError();
+ *
+ * // Custom error message
+ * throw new InvalidRequestError("Missing 'method' field in request");
+ * }
+ * + * @see JSONRPCError for the base error class + * @see A2AError for the error marker interface + * @see JSON-RPC 2.0 Error Codes */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class InvalidRequestError extends JSONRPCError { - public final static Integer DEFAULT_CODE = -32600; + public final static Integer DEFAULT_CODE = A2AErrorCodes.INVALID_REQUEST_ERROR_CODE; public InvalidRequestError() { this(null, null, null); } - @JsonCreator - public InvalidRequestError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + public InvalidRequestError(Integer code, String message, Object data) { super( - defaultIfNull(code, DEFAULT_CODE), + defaultIfNull(code, A2AErrorCodes.INVALID_REQUEST_ERROR_CODE), defaultIfNull(message, "Request payload validation error"), data); } diff --git a/spec/src/main/java/io/a2a/spec/JSONErrorResponse.java b/spec/src/main/java/io/a2a/spec/JSONErrorResponse.java index 3029f5394..8823f9bd3 100644 --- a/spec/src/main/java/io/a2a/spec/JSONErrorResponse.java +++ b/spec/src/main/java/io/a2a/spec/JSONErrorResponse.java @@ -1,9 +1,19 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) +/** + * A simplified error response wrapper for non-JSON-RPC error scenarios. + *

+ * This record provides a lightweight error response format for cases where + * a full JSON-RPC error structure is not appropriate, such as HTTP-level + * errors or transport-layer failures. + *

+ * Unlike {@link JSONRPCErrorResponse}, this is not part of the JSON-RPC 2.0 + * specification but serves as a utility for simpler error reporting in the + * A2A Java SDK implementation. + * + * @param error a human-readable error message + * @see JSONRPCErrorResponse + * @see JSONRPCError + */ public record JSONErrorResponse(String error) { } diff --git a/spec/src/main/java/io/a2a/spec/JSONParseError.java b/spec/src/main/java/io/a2a/spec/JSONParseError.java index d596c0701..acec50ca7 100644 --- a/spec/src/main/java/io/a2a/spec/JSONParseError.java +++ b/spec/src/main/java/io/a2a/spec/JSONParseError.java @@ -1,17 +1,31 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import static io.a2a.util.Utils.defaultIfNull; /** - * An error indicating that the server received invalid JSON. + * JSON-RPC error indicating that the server received invalid JSON that could not be parsed. + *

+ * This error is returned when the request payload is not valid JSON, such as malformed syntax, + * unexpected tokens, or encoding issues. This is distinct from {@link InvalidRequestError}, + * which indicates structurally valid JSON that doesn't conform to the JSON-RPC specification. + *

+ * Corresponds to JSON-RPC 2.0 error code {@code -32700}. + *

+ * Usage example: + *

{@code
+ * try {
+ *     objectMapper.readValue(payload, JSONRPCRequest.class);
+ * } catch (io.a2a.json.JsonProcessingException e) {
+ *     throw new JSONParseError("Malformed JSON: " + e.getMessage());
+ * }
+ * }
+ * + * @see JSONRPCError for the base error class + * @see A2AError for the error marker interface + * @see InvalidRequestError for structurally valid but invalid requests + * @see JSON-RPC 2.0 Error Codes */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class JSONParseError extends JSONRPCError implements A2AError { public final static Integer DEFAULT_CODE = -32700; @@ -24,11 +38,10 @@ public JSONParseError(String message) { this(null, message, null); } - @JsonCreator public JSONParseError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + Integer code, + String message, + Object data) { super( defaultIfNull(code, DEFAULT_CODE), defaultIfNull(message, "Invalid JSON payload"), diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCError.java b/spec/src/main/java/io/a2a/spec/JSONRPCError.java index 776b9c8c6..766e54fda 100644 --- a/spec/src/main/java/io/a2a/spec/JSONRPCError.java +++ b/spec/src/main/java/io/a2a/spec/JSONRPCError.java @@ -1,31 +1,27 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.a2a.util.Assert; /** * Represents a JSON-RPC 2.0 Error object, included in an error response. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonDeserialize(using = JSONRPCErrorDeserializer.class) -@JsonSerialize(using = JSONRPCErrorSerializer.class) -@JsonIgnoreProperties(ignoreUnknown = true) public class JSONRPCError extends Error implements Event, A2AError { private final Integer code; private final Object data; - @JsonCreator - public JSONRPCError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + /** + * Constructs a JSON-RPC error with the specified code, message, and optional data. + *

+ * This constructor is used by Jackson for JSON deserialization. + * + * @param code the numeric error code (required, see JSON-RPC 2.0 spec for standard codes) + * @param message the human-readable error message (required) + * @param data additional error information, structure defined by the error code (optional) + * @throws IllegalArgumentException if code or message is null + */ + public JSONRPCError(Integer code, String message, Object data) { super(message); Assert.checkNotNullParam("code", code); Assert.checkNotNullParam("message", message); diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCErrorDeserializer.java b/spec/src/main/java/io/a2a/spec/JSONRPCErrorDeserializer.java deleted file mode 100644 index 229abf55d..000000000 --- a/spec/src/main/java/io/a2a/spec/JSONRPCErrorDeserializer.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.a2a.spec; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; - -public class JSONRPCErrorDeserializer extends StdDeserializer { - - private static final Map> ERROR_MAP = new HashMap<>(); - - static { - ERROR_MAP.put(JSONParseError.DEFAULT_CODE, JSONParseError::new); - ERROR_MAP.put(InvalidRequestError.DEFAULT_CODE, InvalidRequestError::new); - ERROR_MAP.put(MethodNotFoundError.DEFAULT_CODE, MethodNotFoundError::new); - ERROR_MAP.put(InvalidParamsError.DEFAULT_CODE, InvalidParamsError::new); - ERROR_MAP.put(InternalError.DEFAULT_CODE, InternalError::new); - ERROR_MAP.put(PushNotificationNotSupportedError.DEFAULT_CODE, PushNotificationNotSupportedError::new); - ERROR_MAP.put(UnsupportedOperationError.DEFAULT_CODE, UnsupportedOperationError::new); - ERROR_MAP.put(ContentTypeNotSupportedError.DEFAULT_CODE, ContentTypeNotSupportedError::new); - ERROR_MAP.put(InvalidAgentResponseError.DEFAULT_CODE, InvalidAgentResponseError::new); - ERROR_MAP.put(TaskNotCancelableError.DEFAULT_CODE, TaskNotCancelableError::new); - ERROR_MAP.put(TaskNotFoundError.DEFAULT_CODE, TaskNotFoundError::new); - } - - public JSONRPCErrorDeserializer() { - this(null); - } - - public JSONRPCErrorDeserializer(Class vc) { - super(vc); - } - - @Override - public JSONRPCError deserialize(JsonParser jsonParser, DeserializationContext context) - throws IOException, JsonProcessingException { - JsonNode node = jsonParser.getCodec().readTree(jsonParser); - int code = node.get("code").asInt(); - String message = node.get("message").asText(); - JsonNode dataNode = node.get("data"); - Object data = dataNode != null ? jsonParser.getCodec().treeToValue(dataNode, Object.class) : null; - TriFunction constructor = ERROR_MAP.get(code); - if (constructor != null) { - return constructor.apply(code, message, data); - } else { - return new JSONRPCError(code, message, data); - } - } - - @FunctionalInterface - private interface TriFunction { - R apply(A a, B b, C c); - } -} diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCErrorResponse.java b/spec/src/main/java/io/a2a/spec/JSONRPCErrorResponse.java index ea7846655..f24271c46 100644 --- a/spec/src/main/java/io/a2a/spec/JSONRPCErrorResponse.java +++ b/spec/src/main/java/io/a2a/spec/JSONRPCErrorResponse.java @@ -1,22 +1,24 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import io.a2a.util.Assert; /** * A JSON RPC error response. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class JSONRPCErrorResponse extends JSONRPCResponse { - @JsonCreator - public JSONRPCErrorResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") Void result, @JsonProperty("error") JSONRPCError error) { + /** + * Constructs a JSON-RPC error response with all fields. + *

+ * This constructor is used for JSON deserialization. + * + * @param jsonrpc the JSON-RPC version (must be "2.0") + * @param id the request ID, or null if the ID could not be determined from the request + * @param result must be null for error responses + * @param error the error object describing what went wrong (required) + * @throws IllegalArgumentException if error is null + */ + public JSONRPCErrorResponse(String jsonrpc, Object id, Void result, JSONRPCError error) { super(jsonrpc, id, result, error, Void.class); Assert.checkNotNullParam("error", error); } diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCErrorSerializer.java b/spec/src/main/java/io/a2a/spec/JSONRPCErrorSerializer.java deleted file mode 100644 index 87b427548..000000000 --- a/spec/src/main/java/io/a2a/spec/JSONRPCErrorSerializer.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.a2a.spec; - -import java.io.IOException; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; - -public class JSONRPCErrorSerializer extends StdSerializer { - - public JSONRPCErrorSerializer() { - this(null); - } - - public JSONRPCErrorSerializer(Class vc) { - super(vc); - } - - @Override - public void serialize(JSONRPCError value, JsonGenerator gen, SerializerProvider provider) throws IOException { - gen.writeStartObject(); - gen.writeNumberField("code", value.getCode()); - gen.writeStringField("message", value.getMessage()); - if (value.getData() != null) { - gen.writeObjectField("data", value.getData()); - } - gen.writeEndObject(); - } -} diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java b/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java index a88de90f1..45e2b6883 100644 --- a/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java +++ b/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java @@ -2,16 +2,11 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - import io.a2a.util.Assert; /** * Represents a JSONRPC request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public abstract sealed class JSONRPCRequest implements JSONRPCMessage permits NonStreamingJSONRPCRequest, StreamingJSONRPCRequest { protected String jsonrpc; diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCRequestDeserializerBase.java b/spec/src/main/java/io/a2a/spec/JSONRPCRequestDeserializerBase.java deleted file mode 100644 index 2af7d0513..000000000 --- a/spec/src/main/java/io/a2a/spec/JSONRPCRequestDeserializerBase.java +++ /dev/null @@ -1,89 +0,0 @@ -package io.a2a.spec; - -import static io.a2a.util.Utils.OBJECT_MAPPER; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; - -public abstract class JSONRPCRequestDeserializerBase extends StdDeserializer> { - - public JSONRPCRequestDeserializerBase() { - this(null); - } - - public JSONRPCRequestDeserializerBase(Class vc) { - super(vc); - } - - protected T getAndValidateParams(JsonNode paramsNode, JsonParser jsonParser, JsonNode node, Class paramsType) throws JsonMappingException { - if (paramsNode == null) { - return null; - } - try { - return OBJECT_MAPPER.treeToValue(paramsNode, paramsType); - } catch (JsonProcessingException e) { - throw new InvalidParamsJsonMappingException("Invalid params", e, getIdIfPossible(node, jsonParser)); - } - } - - protected String getAndValidateJsonrpc(JsonNode treeNode, JsonParser jsonParser) throws JsonMappingException { - JsonNode jsonrpcNode = treeNode.get("jsonrpc"); - if (jsonrpcNode == null || ! jsonrpcNode.asText().equals(JSONRPCMessage.JSONRPC_VERSION)) { - throw new IdJsonMappingException("Invalid JSON-RPC protocol version", getIdIfPossible(treeNode, jsonParser)); - } - return jsonrpcNode.asText(); - } - - protected String getAndValidateMethod(JsonNode treeNode, JsonParser jsonParser) throws JsonMappingException { - JsonNode methodNode = treeNode.get("method"); - if (methodNode == null) { - throw new IdJsonMappingException("Missing method", getIdIfPossible(treeNode, jsonParser)); - } - String method = methodNode.asText(); - if (! isValidMethodName(method)) { - throw new MethodNotFoundJsonMappingException("Invalid method", getIdIfPossible(treeNode, jsonParser)); - } - return method; - } - - protected Object getAndValidateId(JsonNode treeNode, JsonParser jsonParser) throws JsonProcessingException { - JsonNode idNode = treeNode.get("id"); - Object id = null; - if (idNode != null) { - if (idNode.isTextual()) { - id = OBJECT_MAPPER.treeToValue(idNode, String.class); - } else if (idNode.isNumber()) { - id = OBJECT_MAPPER.treeToValue(idNode, Integer.class); - } else { - throw new JsonMappingException(jsonParser, "Invalid id"); - } - } - return id; - } - - protected Object getIdIfPossible(JsonNode treeNode, JsonParser jsonParser) { - try { - return getAndValidateId(treeNode, jsonParser); - } catch (JsonProcessingException e) { - // id can't be determined - return null; - } - } - - protected static boolean isValidMethodName(String methodName) { - return methodName != null && (methodName.equals(CancelTaskRequest.METHOD) - || methodName.equals(GetTaskRequest.METHOD) - || methodName.equals(GetTaskPushNotificationConfigRequest.METHOD) - || methodName.equals(SetTaskPushNotificationConfigRequest.METHOD) - || methodName.equals(TaskResubscriptionRequest.METHOD) - || methodName.equals(SendMessageRequest.METHOD) - || methodName.equals(SendStreamingMessageRequest.METHOD) - || methodName.equals(ListTaskPushNotificationConfigRequest.METHOD) - || methodName.equals(DeleteTaskPushNotificationConfigRequest.METHOD) - || methodName.equals(GetAuthenticatedExtendedCardRequest.METHOD)); - - } -} diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java b/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java index be67a1e24..d4330d843 100644 --- a/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java +++ b/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java @@ -2,16 +2,11 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - import io.a2a.util.Assert; /** * Represents a JSONRPC response. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public abstract sealed class JSONRPCResponse implements JSONRPCMessage permits SendStreamingMessageResponse, GetTaskResponse, CancelTaskResponse, SetTaskPushNotificationConfigResponse, GetTaskPushNotificationConfigResponse, SendMessageResponse, DeleteTaskPushNotificationConfigResponse, ListTaskPushNotificationConfigResponse, JSONRPCErrorResponse, diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCVoidResponseSerializer.java b/spec/src/main/java/io/a2a/spec/JSONRPCVoidResponseSerializer.java deleted file mode 100644 index 200bc4cd4..000000000 --- a/spec/src/main/java/io/a2a/spec/JSONRPCVoidResponseSerializer.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.a2a.spec; - -import java.io.IOException; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import com.fasterxml.jackson.databind.type.TypeFactory; - -public class JSONRPCVoidResponseSerializer extends StdSerializer> { - - private static final JSONRPCErrorSerializer JSON_RPC_ERROR_SERIALIZER = new JSONRPCErrorSerializer(); - - public JSONRPCVoidResponseSerializer() { - super(TypeFactory.defaultInstance().constructParametricType(JSONRPCResponse.class, - Void.class)); - } - - @Override - public void serialize(JSONRPCResponse value, JsonGenerator gen, SerializerProvider provider) throws IOException { - gen.writeStartObject(); - gen.writeStringField("jsonrpc", value.getJsonrpc()); - gen.writeObjectField("id", value.getId()); - if (value.getError() != null) { - gen.writeFieldName("error"); - JSON_RPC_ERROR_SERIALIZER.serialize(value.getError(), gen, provider); - } else { - gen.writeNullField("result"); - } - gen.writeEndObject(); - } -} diff --git a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java index 5ebb12f76..2d04f36ce 100644 --- a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java +++ b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java @@ -2,16 +2,11 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - import io.a2a.util.Assert; /** * Parameters for getting list of pushNotificationConfigurations associated with a Task. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record ListTaskPushNotificationConfigParams(String id, Map metadata) { public ListTaskPushNotificationConfigParams { diff --git a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigRequest.java b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigRequest.java index 90ba0f1f5..8a4b75a1f 100644 --- a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigRequest.java +++ b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigRequest.java @@ -2,27 +2,17 @@ import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import io.a2a.util.Assert; import io.a2a.util.Utils; /** * A list task push notification config request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class ListTaskPushNotificationConfigRequest extends NonStreamingJSONRPCRequest { public static final String METHOD = "tasks/pushNotificationConfig/list"; - @JsonCreator - public ListTaskPushNotificationConfigRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, - @JsonProperty("params") ListTaskPushNotificationConfigParams params) { + public ListTaskPushNotificationConfigRequest(String jsonrpc, Object id, String method, ListTaskPushNotificationConfigParams params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } diff --git a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigResponse.java b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigResponse.java index cc610416e..5546f148c 100644 --- a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigResponse.java +++ b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigResponse.java @@ -2,22 +2,12 @@ import java.util.List; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * A response for a list task push notification config request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class ListTaskPushNotificationConfigResponse extends JSONRPCResponse> { - @JsonCreator - public ListTaskPushNotificationConfigResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") List result, - @JsonProperty("error") JSONRPCError error) { + public ListTaskPushNotificationConfigResponse(String jsonrpc, Object id, List result, JSONRPCError error) { super(jsonrpc, id, result, error, (Class>) (Class) List.class); } diff --git a/spec/src/main/java/io/a2a/spec/Message.java b/spec/src/main/java/io/a2a/spec/Message.java index dd7e860a5..9d08cb56c 100644 --- a/spec/src/main/java/io/a2a/spec/Message.java +++ b/spec/src/main/java/io/a2a/spec/Message.java @@ -4,13 +4,6 @@ import java.util.Map; import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.annotation.JsonValue; -import com.fasterxml.jackson.core.type.TypeReference; import io.a2a.util.Assert; import static io.a2a.spec.Message.MESSAGE; @@ -18,13 +11,8 @@ /** * Represents a single message in the conversation between a user and an agent. */ -@JsonTypeName(MESSAGE) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class Message implements EventKind, StreamingEventKind { - public static final TypeReference TYPE_REFERENCE = new TypeReference<>() {}; - public static final String MESSAGE = "message"; private final Role role; private final List> parts; @@ -41,12 +29,11 @@ public Message(Role role, List> parts, String messageId, String contextI this(role, parts, messageId, contextId, taskId, referenceTaskIds, metadata, extensions, MESSAGE); } - @JsonCreator - public Message(@JsonProperty("role") Role role, @JsonProperty("parts") List> parts, - @JsonProperty("messageId") String messageId, @JsonProperty("contextId") String contextId, - @JsonProperty("taskId") String taskId, @JsonProperty("referenceTaskIds") List referenceTaskIds, - @JsonProperty("metadata") Map metadata, @JsonProperty("extensions") List extensions, - @JsonProperty("kind") String kind) { + public Message(Role role, List> parts, + String messageId, String contextId, + String taskId, List referenceTaskIds, + Map metadata, List extensions, + String kind) { Assert.checkNotNullParam("kind", kind); Assert.checkNotNullParam("parts", parts); if (parts.isEmpty()) { @@ -123,7 +110,11 @@ public enum Role { this.role = role; } - @JsonValue + /** + * Returns the string representation of the role for JSON serialization. + * + * @return the role as a string ("user" or "agent") + */ public String asString() { return this.role; } diff --git a/spec/src/main/java/io/a2a/spec/MessageSendConfiguration.java b/spec/src/main/java/io/a2a/spec/MessageSendConfiguration.java index cbe72cfb1..d44ce494f 100644 --- a/spec/src/main/java/io/a2a/spec/MessageSendConfiguration.java +++ b/spec/src/main/java/io/a2a/spec/MessageSendConfiguration.java @@ -2,16 +2,12 @@ import java.util.List; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; /** * Defines configuration options for a `message/send` or `message/stream` request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record MessageSendConfiguration(List acceptedOutputModes, Integer historyLength, PushNotificationConfig pushNotificationConfig, Boolean blocking) { diff --git a/spec/src/main/java/io/a2a/spec/MessageSendParams.java b/spec/src/main/java/io/a2a/spec/MessageSendParams.java index 7cba6f1ae..5914ef462 100644 --- a/spec/src/main/java/io/a2a/spec/MessageSendParams.java +++ b/spec/src/main/java/io/a2a/spec/MessageSendParams.java @@ -2,16 +2,12 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * Defines the parameters for a request to send a message to an agent. This can be used * to create a new task, continue an existing one, or restart a task. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record MessageSendParams(Message message, MessageSendConfiguration configuration, Map metadata) { diff --git a/spec/src/main/java/io/a2a/spec/MethodNotFoundError.java b/spec/src/main/java/io/a2a/spec/MethodNotFoundError.java index 5a46b336f..e8af3590a 100644 --- a/spec/src/main/java/io/a2a/spec/MethodNotFoundError.java +++ b/spec/src/main/java/io/a2a/spec/MethodNotFoundError.java @@ -1,33 +1,20 @@ package io.a2a.spec; +import static io.a2a.spec.A2AErrorCodes.METHOD_NOT_FOUND_ERROR_CODE; import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * An error indicating that the requested method does not exist or is not available. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class MethodNotFoundError extends JSONRPCError { - public final static Integer DEFAULT_CODE = -32601; + public final static Integer DEFAULT_CODE = METHOD_NOT_FOUND_ERROR_CODE; - @JsonCreator - public MethodNotFoundError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { - super( - defaultIfNull(code, DEFAULT_CODE), - defaultIfNull(message, "Method not found"), - data); + public MethodNotFoundError(Integer code, String message, Object data) { + super(defaultIfNull(code, METHOD_NOT_FOUND_ERROR_CODE), defaultIfNull(message, "Method not found"), data); } public MethodNotFoundError() { - this(DEFAULT_CODE, null, null); + this(METHOD_NOT_FOUND_ERROR_CODE, null, null); } } diff --git a/spec/src/main/java/io/a2a/spec/MutualTLSSecurityScheme.java b/spec/src/main/java/io/a2a/spec/MutualTLSSecurityScheme.java index 37f5bd755..e0b6fb8e0 100644 --- a/spec/src/main/java/io/a2a/spec/MutualTLSSecurityScheme.java +++ b/spec/src/main/java/io/a2a/spec/MutualTLSSecurityScheme.java @@ -1,10 +1,5 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; import io.a2a.util.Assert; import static io.a2a.spec.MutualTLSSecurityScheme.MUTUAL_TLS; @@ -12,9 +7,6 @@ /** * Defines a security scheme using mTLS authentication. */ -@JsonTypeName(MUTUAL_TLS) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class MutualTLSSecurityScheme implements SecurityScheme { public static final String MUTUAL_TLS = "mutualTLS"; @@ -29,9 +21,7 @@ public MutualTLSSecurityScheme() { this(null, MUTUAL_TLS); } - @JsonCreator - public MutualTLSSecurityScheme(@JsonProperty("description") String description, - @JsonProperty("type") String type) { + public MutualTLSSecurityScheme(String description, String type) { Assert.checkNotNullParam("type", type); if (!type.equals(MUTUAL_TLS)) { throw new IllegalArgumentException("Invalid type for MutualTLSSecurityScheme"); diff --git a/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java b/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java index f3ac7d2cb..e969ce08e 100644 --- a/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java +++ b/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java @@ -1,15 +1,8 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - /** * Represents a non-streaming JSON-RPC request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonDeserialize(using = NonStreamingJSONRPCRequestDeserializer.class) public abstract sealed class NonStreamingJSONRPCRequest extends JSONRPCRequest permits GetTaskRequest, CancelTaskRequest, SetTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, SendMessageRequest, DeleteTaskPushNotificationConfigRequest, ListTaskPushNotificationConfigRequest, diff --git a/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequestDeserializer.java b/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequestDeserializer.java deleted file mode 100644 index 37366b9b7..000000000 --- a/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequestDeserializer.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.a2a.spec; - -import java.io.IOException; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; - -public class NonStreamingJSONRPCRequestDeserializer extends JSONRPCRequestDeserializerBase> { - - public NonStreamingJSONRPCRequestDeserializer() { - this(null); - } - - public NonStreamingJSONRPCRequestDeserializer(Class vc) { - super(vc); - } - - @Override - public NonStreamingJSONRPCRequest deserialize(JsonParser jsonParser, DeserializationContext context) - throws IOException, JsonProcessingException { - JsonNode treeNode = jsonParser.getCodec().readTree(jsonParser); - String jsonrpc = getAndValidateJsonrpc(treeNode, jsonParser); - String method = getAndValidateMethod(treeNode, jsonParser); - Object id = getAndValidateId(treeNode, jsonParser); - JsonNode paramsNode = treeNode.get("params"); - - switch (method) { - case GetTaskRequest.METHOD: - return new GetTaskRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, TaskQueryParams.class)); - case CancelTaskRequest.METHOD: - return new CancelTaskRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, TaskIdParams.class)); - case SetTaskPushNotificationConfigRequest.METHOD: - return new SetTaskPushNotificationConfigRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, TaskPushNotificationConfig.class)); - case GetTaskPushNotificationConfigRequest.METHOD: - return new GetTaskPushNotificationConfigRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, GetTaskPushNotificationConfigParams.class)); - case SendMessageRequest.METHOD: - return new SendMessageRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, MessageSendParams.class)); - case ListTaskPushNotificationConfigRequest.METHOD: - return new ListTaskPushNotificationConfigRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, ListTaskPushNotificationConfigParams.class)); - case DeleteTaskPushNotificationConfigRequest.METHOD: - return new DeleteTaskPushNotificationConfigRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, DeleteTaskPushNotificationConfigParams.class)); - case GetAuthenticatedExtendedCardRequest.METHOD: - return new GetAuthenticatedExtendedCardRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, Void.class)); - default: - throw new MethodNotFoundJsonMappingException("Invalid method", getIdIfPossible(treeNode, jsonParser)); - } - } -} diff --git a/spec/src/main/java/io/a2a/spec/OAuth2SecurityScheme.java b/spec/src/main/java/io/a2a/spec/OAuth2SecurityScheme.java index 9094eee7c..cd456864a 100644 --- a/spec/src/main/java/io/a2a/spec/OAuth2SecurityScheme.java +++ b/spec/src/main/java/io/a2a/spec/OAuth2SecurityScheme.java @@ -1,11 +1,5 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import com.fasterxml.jackson.annotation.JsonTypeName; import io.a2a.util.Assert; import static io.a2a.spec.OAuth2SecurityScheme.OAUTH2; @@ -13,9 +7,6 @@ /** * Defines a security scheme using OAuth 2.0. */ -@JsonTypeName(OAUTH2) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class OAuth2SecurityScheme implements SecurityScheme { public static final String OAUTH2 = "oauth2"; @@ -28,9 +19,7 @@ public OAuth2SecurityScheme(OAuthFlows flows, String description, String oauth2M this(flows, description, oauth2MetadataUrl, OAUTH2); } - @JsonCreator - public OAuth2SecurityScheme(@JsonProperty("flows") OAuthFlows flows, @JsonProperty("description") String description, - @JsonProperty("oauth2MetadataUrl") String oauth2MetadataUrl, @JsonProperty("type") String type) { + public OAuth2SecurityScheme(OAuthFlows flows, String description, String oauth2MetadataUrl, String type) { Assert.checkNotNullParam("flows", flows); Assert.checkNotNullParam("type", type); if (!type.equals(OAUTH2)) { diff --git a/spec/src/main/java/io/a2a/spec/OAuthFlows.java b/spec/src/main/java/io/a2a/spec/OAuthFlows.java index 8c5015893..849f84d41 100644 --- a/spec/src/main/java/io/a2a/spec/OAuthFlows.java +++ b/spec/src/main/java/io/a2a/spec/OAuthFlows.java @@ -1,13 +1,8 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - /** * Defines the configuration for the supported OAuth 2.0 flows. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record OAuthFlows(AuthorizationCodeOAuthFlow authorizationCode, ClientCredentialsOAuthFlow clientCredentials, ImplicitOAuthFlow implicit, PasswordOAuthFlow password) { diff --git a/spec/src/main/java/io/a2a/spec/OpenIdConnectSecurityScheme.java b/spec/src/main/java/io/a2a/spec/OpenIdConnectSecurityScheme.java index 01726d2ea..949fd2a68 100644 --- a/spec/src/main/java/io/a2a/spec/OpenIdConnectSecurityScheme.java +++ b/spec/src/main/java/io/a2a/spec/OpenIdConnectSecurityScheme.java @@ -1,10 +1,5 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; import io.a2a.util.Assert; import static io.a2a.spec.OpenIdConnectSecurityScheme.OPENID_CONNECT; @@ -12,9 +7,6 @@ /** * Defines a security scheme using OpenID Connect. */ -@JsonTypeName(OPENID_CONNECT) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class OpenIdConnectSecurityScheme implements SecurityScheme { public static final String OPENID_CONNECT = "openIdConnect"; @@ -26,9 +18,7 @@ public OpenIdConnectSecurityScheme(String openIdConnectUrl, String description) this(openIdConnectUrl, description, OPENID_CONNECT); } - @JsonCreator - public OpenIdConnectSecurityScheme(@JsonProperty("openIdConnectUrl") String openIdConnectUrl, - @JsonProperty("description") String description, @JsonProperty("type") String type) { + public OpenIdConnectSecurityScheme(String openIdConnectUrl, String description, String type) { Assert.checkNotNullParam("type", type); Assert.checkNotNullParam("openIdConnectUrl", openIdConnectUrl); if (!type.equals(OPENID_CONNECT)) { diff --git a/spec/src/main/java/io/a2a/spec/Part.java b/spec/src/main/java/io/a2a/spec/Part.java index 2bdfb69d1..e55577622 100644 --- a/spec/src/main/java/io/a2a/spec/Part.java +++ b/spec/src/main/java/io/a2a/spec/Part.java @@ -2,25 +2,10 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonValue; - /** * A fundamental unit with a Message or Artifact. * @param the type of unit */ -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "kind", - visible = true -) -@JsonSubTypes({ - @JsonSubTypes.Type(value = TextPart.class, name = TextPart.TEXT), - @JsonSubTypes.Type(value = FilePart.class, name = FilePart.FILE), - @JsonSubTypes.Type(value = DataPart.class, name = DataPart.DATA) -}) public abstract class Part { public enum Kind { TEXT("text"), @@ -33,7 +18,11 @@ public enum Kind { this.kind = kind; } - @JsonValue + /** + * Returns the string representation of the kind for JSON serialization. + * + * @return the kind as a string + */ public String asString() { return this.kind; } diff --git a/spec/src/main/java/io/a2a/spec/PasswordOAuthFlow.java b/spec/src/main/java/io/a2a/spec/PasswordOAuthFlow.java index 424a4817f..e5de924cb 100644 --- a/spec/src/main/java/io/a2a/spec/PasswordOAuthFlow.java +++ b/spec/src/main/java/io/a2a/spec/PasswordOAuthFlow.java @@ -2,16 +2,11 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - import io.a2a.util.Assert; /** * Defines configuration details for the OAuth 2.0 Resource Owner Password flow. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record PasswordOAuthFlow(String refreshUrl, Map scopes, String tokenUrl) { public PasswordOAuthFlow { diff --git a/spec/src/main/java/io/a2a/spec/PushNotificationAuthenticationInfo.java b/spec/src/main/java/io/a2a/spec/PushNotificationAuthenticationInfo.java index 1ef29ccd4..6263ac990 100644 --- a/spec/src/main/java/io/a2a/spec/PushNotificationAuthenticationInfo.java +++ b/spec/src/main/java/io/a2a/spec/PushNotificationAuthenticationInfo.java @@ -1,16 +1,11 @@ package io.a2a.spec; import java.util.List; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * Defines authentication details for a push notification endpoint. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record PushNotificationAuthenticationInfo(List schemes, String credentials) { public PushNotificationAuthenticationInfo { diff --git a/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java b/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java index fcffa2c7f..b5a9e1131 100644 --- a/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java +++ b/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java @@ -1,17 +1,11 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.core.type.TypeReference; import io.a2a.util.Assert; /** * Defines the configuration for setting up push notifications for task updates. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record PushNotificationConfig(String url, String token, PushNotificationAuthenticationInfo authentication, String id) { - public static final TypeReference TYPE_REFERENCE = new TypeReference<>() {}; public PushNotificationConfig { Assert.checkNotNullParam("url", url); diff --git a/spec/src/main/java/io/a2a/spec/PushNotificationNotSupportedError.java b/spec/src/main/java/io/a2a/spec/PushNotificationNotSupportedError.java index c618e2b78..7397acd74 100644 --- a/spec/src/main/java/io/a2a/spec/PushNotificationNotSupportedError.java +++ b/spec/src/main/java/io/a2a/spec/PushNotificationNotSupportedError.java @@ -2,16 +2,9 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * An A2A-specific error indicating that the agent does not support push notifications. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class PushNotificationNotSupportedError extends JSONRPCError { public final static Integer DEFAULT_CODE = -32003; @@ -20,11 +13,10 @@ public PushNotificationNotSupportedError() { this(null, null, null); } - @JsonCreator public PushNotificationNotSupportedError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + Integer code, + String message, + Object data) { super( defaultIfNull(code, DEFAULT_CODE), defaultIfNull(message, "Push Notification is not supported"), diff --git a/spec/src/main/java/io/a2a/spec/SecurityScheme.java b/spec/src/main/java/io/a2a/spec/SecurityScheme.java index b3cc104e3..a39dfd314 100644 --- a/spec/src/main/java/io/a2a/spec/SecurityScheme.java +++ b/spec/src/main/java/io/a2a/spec/SecurityScheme.java @@ -2,22 +2,6 @@ import static io.a2a.spec.APIKeySecurityScheme.API_KEY; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; - -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true -) -@JsonSubTypes({ - @JsonSubTypes.Type(value = APIKeySecurityScheme.class, name = API_KEY), - @JsonSubTypes.Type(value = HTTPAuthSecurityScheme.class, name = HTTPAuthSecurityScheme.HTTP), - @JsonSubTypes.Type(value = OAuth2SecurityScheme.class, name = OAuth2SecurityScheme.OAUTH2), - @JsonSubTypes.Type(value = OpenIdConnectSecurityScheme.class, name = OpenIdConnectSecurityScheme.OPENID_CONNECT), - @JsonSubTypes.Type(value = MutualTLSSecurityScheme.class, name = MutualTLSSecurityScheme.MUTUAL_TLS) -}) /** * Defines a security scheme that can be used to secure an agent's endpoints. * This is a discriminated union type based on the OpenAPI 3.0 Security Scheme Object. diff --git a/spec/src/main/java/io/a2a/spec/SendMessageRequest.java b/spec/src/main/java/io/a2a/spec/SendMessageRequest.java index d31f364e4..a58ce0890 100644 --- a/spec/src/main/java/io/a2a/spec/SendMessageRequest.java +++ b/spec/src/main/java/io/a2a/spec/SendMessageRequest.java @@ -4,25 +4,28 @@ import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import io.a2a.util.Assert; /** * Used to send a message request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class SendMessageRequest extends NonStreamingJSONRPCRequest { public static final String METHOD = "message/send"; - @JsonCreator - public SendMessageRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, @JsonProperty("params") MessageSendParams params) { + /** + * Constructs a SendMessageRequest with the specified JSON-RPC fields. + *

+ * This constructor is used for JSON deserialization and validates + * that the method name is exactly "SendMessage". + * + * @param jsonrpc the JSON-RPC version (must be "2.0") + * @param id the request correlation identifier (String, Integer, or null) + * @param method the method name (must be {@value #METHOD}) + * @param params the message send parameters (required) + * @throws IllegalArgumentException if validation fails + */ + public SendMessageRequest(String jsonrpc, Object id, String method, MessageSendParams params) { if (jsonrpc == null || jsonrpc.isEmpty()) { throw new IllegalArgumentException("JSON-RPC protocol version cannot be null or empty"); } diff --git a/spec/src/main/java/io/a2a/spec/SendMessageResponse.java b/spec/src/main/java/io/a2a/spec/SendMessageResponse.java index 901beba90..14d3d5197 100644 --- a/spec/src/main/java/io/a2a/spec/SendMessageResponse.java +++ b/spec/src/main/java/io/a2a/spec/SendMessageResponse.java @@ -1,23 +1,11 @@ package io.a2a.spec; -import static io.a2a.util.Utils.defaultIfNull; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.a2a.util.Assert; - /** * The response after receiving a send message request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class SendMessageResponse extends JSONRPCResponse { - @JsonCreator - public SendMessageResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") EventKind result, @JsonProperty("error") JSONRPCError error) { + public SendMessageResponse(String jsonrpc, Object id, EventKind result, JSONRPCError error) { super(jsonrpc, id, result, error, EventKind.class); } diff --git a/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java b/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java index c4ebe8315..de3abf950 100644 --- a/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java +++ b/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java @@ -2,10 +2,6 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import io.a2a.util.Assert; import java.util.UUID; @@ -13,15 +9,11 @@ /** * Used to initiate a task with streaming. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class SendStreamingMessageRequest extends StreamingJSONRPCRequest { public static final String METHOD = "message/stream"; - @JsonCreator - public SendStreamingMessageRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, @JsonProperty("params") MessageSendParams params) { + public SendStreamingMessageRequest(String jsonrpc, Object id, String method, MessageSendParams params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } diff --git a/spec/src/main/java/io/a2a/spec/SendStreamingMessageResponse.java b/spec/src/main/java/io/a2a/spec/SendStreamingMessageResponse.java index 0ed6ca80d..d76ea9aef 100644 --- a/spec/src/main/java/io/a2a/spec/SendStreamingMessageResponse.java +++ b/spec/src/main/java/io/a2a/spec/SendStreamingMessageResponse.java @@ -1,21 +1,11 @@ package io.a2a.spec; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * The response after receiving a request to initiate a task with streaming. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class SendStreamingMessageResponse extends JSONRPCResponse { - @JsonCreator - public SendStreamingMessageResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") StreamingEventKind result, @JsonProperty("error") JSONRPCError error) { + public SendStreamingMessageResponse(String jsonrpc, Object id, StreamingEventKind result, JSONRPCError error) { super(jsonrpc, id, result, error, StreamingEventKind.class); } diff --git a/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigRequest.java b/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigRequest.java index 7d53083a1..3007112ff 100644 --- a/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigRequest.java +++ b/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigRequest.java @@ -4,25 +4,16 @@ import java.util.UUID; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import io.a2a.util.Assert; /** * Used to set a task push notification request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class SetTaskPushNotificationConfigRequest extends NonStreamingJSONRPCRequest { public static final String METHOD = "tasks/pushNotificationConfig/set"; - @JsonCreator - public SetTaskPushNotificationConfigRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, @JsonProperty("params") TaskPushNotificationConfig params) { + public SetTaskPushNotificationConfigRequest(String jsonrpc, Object id, String method, TaskPushNotificationConfig params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } diff --git a/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigResponse.java b/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigResponse.java index c40f18f18..1b5bd2e52 100644 --- a/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigResponse.java +++ b/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigResponse.java @@ -1,21 +1,11 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * The response after receiving a set task push notification request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class SetTaskPushNotificationConfigResponse extends JSONRPCResponse { - @JsonCreator - public SetTaskPushNotificationConfigResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("result") TaskPushNotificationConfig result, - @JsonProperty("error") JSONRPCError error) { + public SetTaskPushNotificationConfigResponse(String jsonrpc, Object id, TaskPushNotificationConfig result, JSONRPCError error) { super(jsonrpc, id, result, error, TaskPushNotificationConfig.class); } diff --git a/spec/src/main/java/io/a2a/spec/StreamingEventKind.java b/spec/src/main/java/io/a2a/spec/StreamingEventKind.java index b8c16b00a..73c141953 100644 --- a/spec/src/main/java/io/a2a/spec/StreamingEventKind.java +++ b/spec/src/main/java/io/a2a/spec/StreamingEventKind.java @@ -1,25 +1,34 @@ package io.a2a.spec; -import static io.a2a.spec.Message.MESSAGE; -import static io.a2a.spec.Task.TASK; -import static io.a2a.spec.TaskArtifactUpdateEvent.ARTIFACT_UPDATE; -import static io.a2a.spec.TaskStatusUpdateEvent.STATUS_UPDATE; - -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; - -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "kind", - visible = true -) -@JsonSubTypes({ - @JsonSubTypes.Type(value = Task.class, name = TASK), - @JsonSubTypes.Type(value = Message.class, name = MESSAGE), - @JsonSubTypes.Type(value = TaskStatusUpdateEvent.class, name = STATUS_UPDATE), - @JsonSubTypes.Type(value = TaskArtifactUpdateEvent.class, name = ARTIFACT_UPDATE) -}) +/** + * Sealed interface for events that can be emitted during streaming A2A Protocol operations. + *

+ * StreamingEventKind represents events suitable for asynchronous, progressive updates during + * agent task execution. Streaming allows agents to provide incremental feedback as work progresses, + * rather than waiting until task completion. + *

+ * Streaming events use polymorphic JSON serialization with the "kind" discriminator to determine + * the concrete type during deserialization. + *

+ * Permitted implementations: + *

    + *
  • {@link Task} - Complete task state (typically the final event in a stream)
  • + *
  • {@link Message} - Full message (complete message in the stream)
  • + *
  • {@link TaskStatusUpdateEvent} - Incremental status updates (e.g., SUBMITTED → WORKING → COMPLETED)
  • + *
  • {@link TaskArtifactUpdateEvent} - Incremental artifact updates (partial results, chunks)
  • + *
+ *

+ * Streaming events enable patterns like: + *

    + *
  • Progressive text generation (chunks of response as generated)
  • + *
  • Status notifications (task state transitions)
  • + *
  • Partial results (early artifacts before task completion)
  • + *
+ * + * @see Event + * @see EventKind + * @see UpdateEvent + */ public sealed interface StreamingEventKind extends Event permits Task, Message, TaskStatusUpdateEvent, TaskArtifactUpdateEvent { String getKind(); diff --git a/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequest.java b/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequest.java index 7642c41d2..9cbb5e4c6 100644 --- a/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequest.java +++ b/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequest.java @@ -1,15 +1,9 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - /** * Represents a streaming JSON-RPC request. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonDeserialize(using = StreamingJSONRPCRequestDeserializer.class) + public abstract sealed class StreamingJSONRPCRequest extends JSONRPCRequest permits TaskResubscriptionRequest, SendStreamingMessageRequest { diff --git a/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequestDeserializer.java b/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequestDeserializer.java deleted file mode 100644 index b66533620..000000000 --- a/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequestDeserializer.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.a2a.spec; - -import java.io.IOException; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; - -public class StreamingJSONRPCRequestDeserializer extends JSONRPCRequestDeserializerBase> { - - public StreamingJSONRPCRequestDeserializer() { - this(null); - } - - public StreamingJSONRPCRequestDeserializer(Class vc) { - super(vc); - } - - @Override - public StreamingJSONRPCRequest deserialize(JsonParser jsonParser, DeserializationContext context) - throws IOException, JsonProcessingException { - JsonNode treeNode = jsonParser.getCodec().readTree(jsonParser); - String jsonrpc = getAndValidateJsonrpc(treeNode, jsonParser); - String method = getAndValidateMethod(treeNode, jsonParser); - Object id = getAndValidateId(treeNode, jsonParser); - JsonNode paramsNode = treeNode.get("params"); - - switch (method) { - case TaskResubscriptionRequest.METHOD -> { - return new TaskResubscriptionRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, TaskIdParams.class)); - } - case SendStreamingMessageRequest.METHOD -> { - return new SendStreamingMessageRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, MessageSendParams.class)); - } - default -> throw new MethodNotFoundJsonMappingException("Invalid method", getIdIfPossible(treeNode, jsonParser)); - } - } -} diff --git a/spec/src/main/java/io/a2a/spec/Task.java b/spec/src/main/java/io/a2a/spec/Task.java index 873f23431..b3b886e5b 100644 --- a/spec/src/main/java/io/a2a/spec/Task.java +++ b/spec/src/main/java/io/a2a/spec/Task.java @@ -3,12 +3,6 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.core.type.TypeReference; import io.a2a.util.Assert; import static io.a2a.spec.Task.TASK; @@ -16,13 +10,8 @@ /** * Represents a single, stateful operation or conversation between a client and an agent. */ -@JsonTypeName(TASK) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class Task implements EventKind, StreamingEventKind { - public static final TypeReference TYPE_REFERENCE = new TypeReference<>() {}; - public static final String TASK = "task"; private final String id; private final String contextId; @@ -37,10 +26,9 @@ public Task(String id, String contextId, TaskStatus status, List artif this(id, contextId, status, artifacts, history, metadata, TASK); } - @JsonCreator - public Task(@JsonProperty("id") String id, @JsonProperty("contextId") String contextId, @JsonProperty("status") TaskStatus status, - @JsonProperty("artifacts") List artifacts, @JsonProperty("history") List history, - @JsonProperty("metadata") Map metadata, @JsonProperty("kind") String kind) { + public Task(String id, String contextId, TaskStatus status, + List artifacts, List history, + Map metadata, String kind) { Assert.checkNotNullParam("id", id); Assert.checkNotNullParam("contextId", contextId); Assert.checkNotNullParam("status", status); diff --git a/spec/src/main/java/io/a2a/spec/TaskArtifactUpdateEvent.java b/spec/src/main/java/io/a2a/spec/TaskArtifactUpdateEvent.java index 1e1cad947..426a3da92 100644 --- a/spec/src/main/java/io/a2a/spec/TaskArtifactUpdateEvent.java +++ b/spec/src/main/java/io/a2a/spec/TaskArtifactUpdateEvent.java @@ -2,11 +2,6 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; import io.a2a.util.Assert; import static io.a2a.spec.TaskArtifactUpdateEvent.ARTIFACT_UPDATE; @@ -15,9 +10,6 @@ * An event sent by the agent to notify the client that an artifact has been * generated or updated. This is typically used in streaming models. */ -@JsonTypeName(ARTIFACT_UPDATE) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class TaskArtifactUpdateEvent implements EventKind, StreamingEventKind, UpdateEvent { public static final String ARTIFACT_UPDATE = "artifact-update"; @@ -33,13 +25,7 @@ public TaskArtifactUpdateEvent(String taskId, Artifact artifact, String contextI this(taskId, artifact, contextId, append, lastChunk, metadata, ARTIFACT_UPDATE); } - @JsonCreator - public TaskArtifactUpdateEvent(@JsonProperty("taskId") String taskId, @JsonProperty("artifact") Artifact artifact, - @JsonProperty("contextId") String contextId, - @JsonProperty("append") Boolean append, - @JsonProperty("lastChunk") Boolean lastChunk, - @JsonProperty("metadata") Map metadata, - @JsonProperty("kind") String kind) { + public TaskArtifactUpdateEvent(String taskId, Artifact artifact, String contextId, Boolean append, Boolean lastChunk, Map metadata, String kind) { Assert.checkNotNullParam("taskId", taskId); Assert.checkNotNullParam("artifact", artifact); Assert.checkNotNullParam("contextId", contextId); diff --git a/spec/src/main/java/io/a2a/spec/TaskIdParams.java b/spec/src/main/java/io/a2a/spec/TaskIdParams.java index 237fe727a..096c9a8a0 100644 --- a/spec/src/main/java/io/a2a/spec/TaskIdParams.java +++ b/spec/src/main/java/io/a2a/spec/TaskIdParams.java @@ -2,15 +2,11 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * Defines parameters containing a task ID, used for simple task operations. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record TaskIdParams(String id, Map metadata) { public TaskIdParams { diff --git a/spec/src/main/java/io/a2a/spec/TaskNotCancelableError.java b/spec/src/main/java/io/a2a/spec/TaskNotCancelableError.java index 50d32c385..d9b77bca0 100644 --- a/spec/src/main/java/io/a2a/spec/TaskNotCancelableError.java +++ b/spec/src/main/java/io/a2a/spec/TaskNotCancelableError.java @@ -2,16 +2,9 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * An A2A-specific error indicating that the task is in a state where it cannot be canceled. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class TaskNotCancelableError extends JSONRPCError { public final static Integer DEFAULT_CODE = -32002; @@ -20,18 +13,17 @@ public TaskNotCancelableError() { this(null, null, null); } - @JsonCreator public TaskNotCancelableError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + Integer code, + String message, + Object data) { super( defaultIfNull(code, DEFAULT_CODE), defaultIfNull(message, "Task cannot be canceled"), data); } - public TaskNotCancelableError(@JsonProperty("message") String message) { + public TaskNotCancelableError(String message) { this(null, message, null); } diff --git a/spec/src/main/java/io/a2a/spec/TaskNotFoundError.java b/spec/src/main/java/io/a2a/spec/TaskNotFoundError.java index c21998d2d..40a1528f7 100644 --- a/spec/src/main/java/io/a2a/spec/TaskNotFoundError.java +++ b/spec/src/main/java/io/a2a/spec/TaskNotFoundError.java @@ -1,33 +1,21 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import static io.a2a.util.Utils.defaultIfNull; /** * An A2A-specific error indicating that the requested task ID was not found. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class TaskNotFoundError extends JSONRPCError { - public final static Integer DEFAULT_CODE = -32001; + public final static Integer DEFAULT_CODE = A2AErrorCodes.TASK_NOT_FOUND_ERROR_CODE; public TaskNotFoundError() { this(null, null, null); } - @JsonCreator - - public TaskNotFoundError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + public TaskNotFoundError(Integer code, String message, Object data) { super( - defaultIfNull(code, DEFAULT_CODE), + defaultIfNull(code, A2AErrorCodes.TASK_NOT_FOUND_ERROR_CODE), defaultIfNull(message, "Task not found"), data); } diff --git a/spec/src/main/java/io/a2a/spec/TaskPushNotificationConfig.java b/spec/src/main/java/io/a2a/spec/TaskPushNotificationConfig.java index 4cda7a8f3..23a7fc0c4 100644 --- a/spec/src/main/java/io/a2a/spec/TaskPushNotificationConfig.java +++ b/spec/src/main/java/io/a2a/spec/TaskPushNotificationConfig.java @@ -1,14 +1,10 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; /** * A container associating a push notification configuration with a specific task. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record TaskPushNotificationConfig(String taskId, PushNotificationConfig pushNotificationConfig) { public TaskPushNotificationConfig { diff --git a/spec/src/main/java/io/a2a/spec/TaskQueryParams.java b/spec/src/main/java/io/a2a/spec/TaskQueryParams.java index 92c2453c5..f119bd14b 100644 --- a/spec/src/main/java/io/a2a/spec/TaskQueryParams.java +++ b/spec/src/main/java/io/a2a/spec/TaskQueryParams.java @@ -1,8 +1,5 @@ package io.a2a.spec; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import io.a2a.util.Assert; import java.util.Map; import org.jspecify.annotations.Nullable; @@ -14,9 +11,6 @@ * @param historyLength the maximum number of items of history for the task to include in the response * @param metadata additional properties */ - -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record TaskQueryParams(String id, int historyLength, @Nullable Map metadata) { public TaskQueryParams { diff --git a/spec/src/main/java/io/a2a/spec/TaskResubscriptionRequest.java b/spec/src/main/java/io/a2a/spec/TaskResubscriptionRequest.java index de0c88c62..951615d53 100644 --- a/spec/src/main/java/io/a2a/spec/TaskResubscriptionRequest.java +++ b/spec/src/main/java/io/a2a/spec/TaskResubscriptionRequest.java @@ -2,10 +2,6 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import io.a2a.util.Assert; import java.util.UUID; @@ -13,15 +9,11 @@ /** * Used to resubscribe to a task. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class TaskResubscriptionRequest extends StreamingJSONRPCRequest { public static final String METHOD = "tasks/resubscribe"; - @JsonCreator - public TaskResubscriptionRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, @JsonProperty("params") TaskIdParams params) { + public TaskResubscriptionRequest(String jsonrpc, Object id, String method, TaskIdParams params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } diff --git a/spec/src/main/java/io/a2a/spec/TaskState.java b/spec/src/main/java/io/a2a/spec/TaskState.java index 655d20919..413044810 100644 --- a/spec/src/main/java/io/a2a/spec/TaskState.java +++ b/spec/src/main/java/io/a2a/spec/TaskState.java @@ -1,8 +1,5 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - /** * Defines the lifecycle states of a Task. */ @@ -29,7 +26,14 @@ public enum TaskState { this.isFinal = isFinal; } - @JsonValue + /** + * Returns the string representation of this task state for JSON serialization. + *

+ * This method is used to serialize TaskState values to their + * wire format (e.g., "working", "completed"). + * + * @return the string representation of this state + */ public String asString() { return state; } @@ -38,7 +42,16 @@ public boolean isFinal(){ return isFinal; } - @JsonCreator + /** + * Deserializes a string value into a TaskState enum constant. + *

+ * This method is used to deserialize TaskState values from their + * wire format during JSON parsing. + * + * @param state the string representation of the state + * @return the corresponding TaskState enum constant + * @throws IllegalArgumentException if the state string is not recognized + */ public static TaskState fromString(String state) { return switch (state) { case "submitted" -> SUBMITTED; diff --git a/spec/src/main/java/io/a2a/spec/TaskStatus.java b/spec/src/main/java/io/a2a/spec/TaskStatus.java index 0b9bed654..a94d54580 100644 --- a/spec/src/main/java/io/a2a/spec/TaskStatus.java +++ b/spec/src/main/java/io/a2a/spec/TaskStatus.java @@ -3,19 +3,13 @@ import java.time.OffsetDateTime; import java.time.ZoneOffset; -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - import io.a2a.util.Assert; /** * Represents the status of a task at a specific point in time. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public record TaskStatus(TaskState state, Message message, - @JsonFormat(shape = JsonFormat.Shape.STRING) OffsetDateTime timestamp) { + OffsetDateTime timestamp) { public TaskStatus { Assert.checkNotNullParam("state", state); diff --git a/spec/src/main/java/io/a2a/spec/TaskStatusUpdateEvent.java b/spec/src/main/java/io/a2a/spec/TaskStatusUpdateEvent.java index 25e2cd170..409510c54 100644 --- a/spec/src/main/java/io/a2a/spec/TaskStatusUpdateEvent.java +++ b/spec/src/main/java/io/a2a/spec/TaskStatusUpdateEvent.java @@ -1,12 +1,8 @@ package io.a2a.spec; +import com.google.gson.annotations.SerializedName; import java.util.Map; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; import io.a2a.util.Assert; import static io.a2a.spec.TaskStatusUpdateEvent.STATUS_UPDATE; @@ -15,15 +11,13 @@ * An event sent by the agent to notify the client of a change in a task's status. * This is typically used in streaming or subscription models. */ -@JsonTypeName(STATUS_UPDATE) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public final class TaskStatusUpdateEvent implements EventKind, StreamingEventKind, UpdateEvent { public static final String STATUS_UPDATE = "status-update"; private final String taskId; private final TaskStatus status; private final String contextId; + @SerializedName("final") private final boolean isFinal; private final Map metadata; private final String kind; @@ -34,10 +28,7 @@ public TaskStatusUpdateEvent(String taskId, TaskStatus status, String contextId, this(taskId, status, contextId, isFinal, metadata, STATUS_UPDATE); } - @JsonCreator - public TaskStatusUpdateEvent(@JsonProperty("taskId") String taskId, @JsonProperty("status") TaskStatus status, - @JsonProperty("contextId") String contextId, @JsonProperty("final") boolean isFinal, - @JsonProperty("metadata") Map metadata, @JsonProperty("kind") String kind) { + public TaskStatusUpdateEvent(String taskId, TaskStatus status, String contextId, boolean isFinal, Map metadata, String kind) { Assert.checkNotNullParam("taskId", taskId); Assert.checkNotNullParam("status", status); Assert.checkNotNullParam("contextId", contextId); @@ -65,7 +56,6 @@ public String getContextId() { return contextId; } - @JsonProperty("final") public boolean isFinal() { return isFinal; } diff --git a/spec/src/main/java/io/a2a/spec/TextPart.java b/spec/src/main/java/io/a2a/spec/TextPart.java index ade2047ee..0d44a3c6a 100644 --- a/spec/src/main/java/io/a2a/spec/TextPart.java +++ b/spec/src/main/java/io/a2a/spec/TextPart.java @@ -2,11 +2,6 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; import io.a2a.util.Assert; import static io.a2a.spec.TextPart.TEXT; @@ -14,9 +9,6 @@ /** * Represents a text segment within a message or artifact. */ -@JsonTypeName(TEXT) -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class TextPart extends Part { public static final String TEXT = "text"; @@ -28,8 +20,7 @@ public TextPart(String text) { this(text, null); } - @JsonCreator - public TextPart(@JsonProperty("text") String text, @JsonProperty("metadata") Map metadata) { + public TextPart(String text, Map metadata) { Assert.checkNotNullParam("text", text); this.text = text; this.metadata = metadata; diff --git a/spec/src/main/java/io/a2a/spec/TransportProtocol.java b/spec/src/main/java/io/a2a/spec/TransportProtocol.java index d6c2922d2..3b3446238 100644 --- a/spec/src/main/java/io/a2a/spec/TransportProtocol.java +++ b/spec/src/main/java/io/a2a/spec/TransportProtocol.java @@ -1,8 +1,5 @@ package io.a2a.spec; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - /** * Supported A2A transport protocols. */ @@ -17,12 +14,26 @@ public enum TransportProtocol { this.transport = transport; } - @JsonValue + /** + * Returns the string representation of this transport protocol. + *

+ * Used for JSON serialization. + * + * @return the transport protocol name as a string + */ public String asString() { return transport; } - @JsonCreator + /** + * Parses a string into a {@link TransportProtocol} enum constant. + *

+ * Used for JSON deserialization. + * + * @param transport the transport protocol string (e.g., "JSONRPC", "GRPC", "HTTP+JSON") + * @return the corresponding TransportProtocol enum constant + * @throws IllegalArgumentException if the transport string is not recognized + */ public static TransportProtocol fromString(String transport) { return switch (transport) { case "JSONRPC" -> JSONRPC; diff --git a/spec/src/main/java/io/a2a/spec/UnsupportedOperationError.java b/spec/src/main/java/io/a2a/spec/UnsupportedOperationError.java index 9fe055e9c..edcb381b0 100644 --- a/spec/src/main/java/io/a2a/spec/UnsupportedOperationError.java +++ b/spec/src/main/java/io/a2a/spec/UnsupportedOperationError.java @@ -2,27 +2,16 @@ import static io.a2a.util.Utils.defaultIfNull; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * An A2A-specific error indicating that the requested operation is not supported by the agent. */ -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonIgnoreProperties(ignoreUnknown = true) public class UnsupportedOperationError extends JSONRPCError { - public final static Integer DEFAULT_CODE = -32004; + public final static Integer DEFAULT_CODE = A2AErrorCodes.UNSUPPORTED_OPERATION_ERROR_CODE; - @JsonCreator - public UnsupportedOperationError( - @JsonProperty("code") Integer code, - @JsonProperty("message") String message, - @JsonProperty("data") Object data) { + public UnsupportedOperationError(Integer code, String message, Object data) { super( - defaultIfNull(code, DEFAULT_CODE), + defaultIfNull(code, A2AErrorCodes.UNSUPPORTED_OPERATION_ERROR_CODE), defaultIfNull(message, "This operation is not supported"), data); } diff --git a/spec/src/main/java/io/a2a/util/Utils.java b/spec/src/main/java/io/a2a/util/Utils.java index 07143ab6a..f374f302e 100644 --- a/spec/src/main/java/io/a2a/util/Utils.java +++ b/spec/src/main/java/io/a2a/util/Utils.java @@ -2,29 +2,63 @@ import java.util.ArrayList; import java.util.List; -import java.util.logging.Logger; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.gson.Gson; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import io.a2a.spec.Artifact; -import io.a2a.spec.Part; import io.a2a.spec.Task; import io.a2a.spec.TaskArtifactUpdateEvent; +import io.a2a.spec.Part; +import java.util.logging.Logger; + + +/** + * Utility class providing common helper methods for A2A Protocol operations. + *

+ * This class contains static utility methods for JSON serialization/deserialization, + * null-safe operations, artifact management, and other common tasks used throughout + * the A2A Java SDK. + *

+ * Key capabilities: + *

    + *
  • JSON processing with pre-configured {@link Gson}
  • + *
  • Null-safe value defaults via {@link #defaultIfNull(Object, Object)}
  • + *
  • Artifact streaming support via {@link #appendArtifactToTask(Task, TaskArtifactUpdateEvent, String)}
  • + *
  • Type-safe exception rethrowing via {@link #rethrow(Throwable)}
  • + *
+ * + * @see Gson for JSON processing + * @see TaskArtifactUpdateEvent for streaming artifact updates + */ public class Utils { - public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final Logger log = Logger.getLogger(Utils.class.getName()); - static { - // needed for date/time types - OBJECT_MAPPER.registerModule(new JavaTimeModule()); + + /** + * Deserializes JSON string into a typed object using Gson. + *

+ * This method uses the pre-configured {@link #OBJECT_MAPPER} to parse JSON. + * + * @param the target type + * @param data JSON string to deserialize + * @param typeRef class reference specifying the target type + * @return deserialized object of type T + * @throws JsonProcessingException if JSON parsing fails + */ + public static T unmarshalFrom(String data, Class typeRef) throws JsonProcessingException { + return JsonUtil.fromJson(data, typeRef); } - public static T unmarshalFrom(String data, TypeReference typeRef) throws JsonProcessingException { - return OBJECT_MAPPER.readValue(data, typeRef); + public static String toJsonString(Object data) { + try { + return JsonUtil.toJson(data); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to serialize to JSON", e); + } } public static T defaultIfNull(T value, T defaultValue) { @@ -38,6 +72,29 @@ public static void rethrow(Throwable t) throws T { throw (T) t; } + /** + * Appends or updates an artifact in a task based on a {@link TaskArtifactUpdateEvent}. + *

+ * This method handles streaming artifact updates, supporting both: + *

    + *
  • Adding new artifacts to the task
  • + *
  • Replacing existing artifacts (when {@code append=false})
  • + *
  • Appending parts to existing artifacts (when {@code append=true})
  • + *
+ *

+ * The {@code append} flag in the event determines the behavior: + *

    + *
  • {@code false} or {@code null}: Replace/add the entire artifact
  • + *
  • {@code true}: Append the new artifact's parts to an existing artifact with matching {@code artifactId}
  • + *
+ * + * @param task the current task to update + * @param event the artifact update event containing the new/updated artifact + * @param taskId the task ID (for logging purposes) + * @return a new Task instance with the updated artifacts list + * @see TaskArtifactUpdateEvent for streaming artifact updates + * @see Artifact for artifact structure + */ public static Task appendArtifactToTask(Task task, TaskArtifactUpdateEvent event, String taskId) { // Append artifacts List artifacts = task.getArtifacts() == null ? new ArrayList<>() : new ArrayList<>(task.getArtifacts()); @@ -94,12 +151,18 @@ public static Task appendArtifactToTask(Task task, TaskArtifactUpdateEvent event } - public static String toJsonString(Object o) { - try { - return OBJECT_MAPPER.writeValueAsString(o); - } catch (JsonProcessingException ex) { - throw new RuntimeException(ex); - } - } + /** + * Get the first defined URL in the supported interaces of the agent card. + * + * @param agentCard the agentcard where the interfaces are defined. + * @return the first defined URL in the supported interaces of the agent card. + * @throws A2AClientException + */ +// public static String getFavoriteInterface(AgentCard agentCard) throws A2AClientException { +// if (agentCard.supportedInterfaces() == null || agentCard.supportedInterfaces().isEmpty()) { +// throw new A2AClientException("No server interface available in the AgentCard"); +// } +// return agentCard.supportedInterfaces().get(0).url(); +// } } diff --git a/spec/src/test/java/io/a2a/spec/JSONRPCErrorSerializationTest.java b/spec/src/test/java/io/a2a/spec/JSONRPCErrorSerializationTest.java index 24251a879..9c83f0806 100644 --- a/spec/src/test/java/io/a2a/spec/JSONRPCErrorSerializationTest.java +++ b/spec/src/test/java/io/a2a/spec/JSONRPCErrorSerializationTest.java @@ -1,7 +1,5 @@ package io.a2a.spec; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import java.util.List; @@ -9,10 +7,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; + + public class JSONRPCErrorSerializationTest { @Test - public void shouldDeserializeToCorrectJSONRPCErrorSubclass() { - ObjectMapper objectMapper = new ObjectMapper(); + public void shouldDeserializeToCorrectJSONRPCErrorSubclass() throws JsonProcessingException { String jsonTemplate = """ {"code": %s, "message": "error", "data": "anything"} """; @@ -36,12 +37,7 @@ record ErrorCase(int code, Class clazz) {} for (ErrorCase errorCase : cases) { String json = jsonTemplate.formatted(errorCase.code()); - JSONRPCError error; - try { - error = objectMapper.readValue(json, JSONRPCError.class); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + JSONRPCError error = JsonUtil.fromJson(json, JSONRPCError.class); assertInstanceOf(errorCase.clazz(), error); assertEquals("error", error.getMessage()); assertEquals("anything", error.getData().toString()); diff --git a/spec/src/test/java/io/a2a/spec/SubTypeSerializationTest.java b/spec/src/test/java/io/a2a/spec/SubTypeSerializationTest.java index a4571f923..0a5a898d7 100644 --- a/spec/src/test/java/io/a2a/spec/SubTypeSerializationTest.java +++ b/spec/src/test/java/io/a2a/spec/SubTypeSerializationTest.java @@ -1,15 +1,11 @@ package io.a2a.spec; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; @@ -23,24 +19,12 @@ public class SubTypeSerializationTest { .status(new TaskStatus(TaskState.SUBMITTED)) .build(); - private static final TypeReference> MAP_TYPE_REFERENCE = new TypeReference<>() { - }; - - private static final ObjectMapper OBJECT_MAPPER; - - static { - OBJECT_MAPPER = new ObjectMapper(); - SimpleModule module = new SimpleModule(); - module.addAbstractTypeMapping(Map.class, SingleKeyHashMap.class); - OBJECT_MAPPER.registerModule(module); - OBJECT_MAPPER.registerModule(new JavaTimeModule()); - } - @ParameterizedTest @MethodSource("serializationTestCases") void testSubtypeSerialization(Object objectToSerialize, String typePropertyName, String expectedTypeValue) throws JsonProcessingException { - Map map = OBJECT_MAPPER.readValue(OBJECT_MAPPER.writeValueAsString(objectToSerialize), - MAP_TYPE_REFERENCE); + String json = JsonUtil.toJson(objectToSerialize); + @SuppressWarnings("unchecked") + Map map = JsonUtil.fromJson(json, Map.class); assertEquals(expectedTypeValue, map.get(typePropertyName)); } @@ -115,15 +99,4 @@ private static Stream serializationTestCases() { ); } - private static class SingleKeyHashMap extends HashMap { - @Override - public V put(K key, V value) { - if (containsKey(key)) { - throw new IllegalArgumentException("duplicate key " + key - + " with value " + get(key) + " and new value " + value); - } - return super.put(key, value); - } - } - } diff --git a/spec/src/test/java/io/a2a/spec/TaskDeserializationTest.java b/spec/src/test/java/io/a2a/spec/TaskDeserializationTest.java deleted file mode 100644 index fa67b59f5..000000000 --- a/spec/src/test/java/io/a2a/spec/TaskDeserializationTest.java +++ /dev/null @@ -1,90 +0,0 @@ -package io.a2a.spec; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -public class TaskDeserializationTest { - private final ObjectMapper objectMapper = new ObjectMapper(); - - @Test - void testTaskWithMissingHistoryAndArtifacts() throws Exception { - // JSON without history and artifacts fields (common server response) - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "completed" - }, - "kind": "task" - } - """; - - Task task = objectMapper.readValue(json, Task.class); - - assertNotNull(task.getHistory(), "history should not be null"); - assertNotNull(task.getArtifacts(), "artifacts should not be null"); - - assertTrue(task.getHistory().isEmpty(), "history should be empty list when not provided"); - assertTrue(task.getArtifacts().isEmpty(), "artifacts should be empty list when not provided"); - } - - @Test - void testTaskWithExplicitNullValues() throws Exception { - // JSON with explicit null values - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "completed" - }, - "history": null, - "artifacts": null, - "kind": "task" - } - """; - - Task task = objectMapper.readValue(json, Task.class); - - // Should never be null even with explicit null in JSON - assertNotNull(task.getHistory(), "history should not be null even when JSON contains null"); - assertNotNull(task.getArtifacts(), "artifacts should not be null even when JSON contains null"); - - assertTrue(task.getHistory().isEmpty()); - assertTrue(task.getArtifacts().isEmpty()); - } - - @Test - void testTaskWithPopulatedArrays() throws Exception { - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "completed" - }, - "history": [ - { - "role": "user", - "parts": [{"kind": "text", "text": "hello"}], - "messageId": "msg-1", - "kind": "message" - } - ], - "artifacts": [], - "kind": "task" - } - """; - - Task task = objectMapper.readValue(json, Task.class); - - assertNotNull(task.getHistory()); - assertEquals(1, task.getHistory().size()); - - assertNotNull(task.getArtifacts()); - assertTrue(task.getArtifacts().isEmpty()); - } -} diff --git a/spec/src/test/java/io/a2a/spec/TaskSerializationTest.java b/spec/src/test/java/io/a2a/spec/TaskSerializationTest.java new file mode 100644 index 000000000..12bfcf508 --- /dev/null +++ b/spec/src/test/java/io/a2a/spec/TaskSerializationTest.java @@ -0,0 +1,713 @@ +package io.a2a.spec; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; + +/** + * Tests for Task serialization and deserialization using Gson. + */ +class TaskSerializationTest { + + @Test + void testBasicTaskSerialization() throws JsonProcessingException { + // Create a basic task + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.SUBMITTED)) + .build(); + + // Serialize to JSON + String json = JsonUtil.toJson(task); + + // Verify JSON contains expected fields + assertNotNull(json); + assertTrue(json.contains("\"id\":\"task-123\"")); + assertTrue(json.contains("\"state\":\"submitted\"")); + + // Deserialize back to Task + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify deserialized task matches original + assertEquals(task.getId(), deserialized.getId()); + assertEquals(task.getStatus().state(), deserialized.getStatus().state()); + } + + @Test + void testTaskWithTimestamp() throws JsonProcessingException { + OffsetDateTime timestamp = OffsetDateTime.now(); + + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.WORKING, null, timestamp)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify OffsetDateTime timestamp is preserved + assertNotNull(deserialized.getStatus().timestamp()); + assertEquals(task.getStatus().timestamp(), deserialized.getStatus().timestamp()); + } + + @Test + void testTaskWithArtifacts() throws JsonProcessingException { + Artifact artifact = new Artifact.Builder() + .artifactId("artifact-1") + .name("Test Artifact") + .description("Description of artifact") + .parts(List.of( + new TextPart("Hello"), + new TextPart("World") + )) + .build(); + + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.COMPLETED)) + .artifacts(List.of(artifact)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Verify JSON contains artifact data + assertTrue(json.contains("\"artifactId\":\"artifact-1\"")); + assertTrue(json.contains("Hello")); + assertTrue(json.contains("World")); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify artifacts are preserved + assertNotNull(deserialized.getArtifacts()); + assertEquals(1, deserialized.getArtifacts().size()); + assertEquals("artifact-1", deserialized.getArtifacts().get(0).artifactId()); + assertEquals(2, deserialized.getArtifacts().get(0).parts().size()); + } + + @Test + void testTaskWithHistory() throws JsonProcessingException { + Message message = new Message.Builder() + .role(Message.Role.USER) + .parts(List.of(new TextPart("Test message"))) + .build(); + + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.WORKING)) + .history(List.of(message)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Verify JSON contains history data + assertTrue(json.contains("\"role\":\"user\"")); + assertTrue(json.contains("Test message")); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify history is preserved + assertNotNull(deserialized.getHistory()); + assertEquals(1, deserialized.getHistory().size()); + assertEquals(Message.Role.USER, deserialized.getHistory().get(0).getRole()); + assertEquals(1, deserialized.getHistory().get(0).getParts().size()); + } + + @Test + void testTaskWithAllFields() throws JsonProcessingException { + OffsetDateTime timestamp = OffsetDateTime.now(); + + Task task = new Task.Builder() + .id("task-123") + .contextId("context-789") + .status(new TaskStatus(TaskState.WORKING, null, timestamp)) + .history(List.of( + new Message.Builder() + .role(Message.Role.USER) + .parts(List.of(new TextPart("User message"))) + .build(), + new Message.Builder() + .role(Message.Role.AGENT) + .parts(List.of(new TextPart("Agent response"))) + .build() + )) + .artifacts(List.of( + new Artifact.Builder() + .artifactId("artifact-1") + .parts(List.of(new TextPart("Artifact content"))) + .build() + )) + .metadata(Map.of("key1", "value1", "key2", 42)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify all fields are preserved + assertEquals(task.getId(), deserialized.getId()); + assertEquals(task.getContextId(), deserialized.getContextId()); + assertEquals(task.getStatus().state(), deserialized.getStatus().state()); + assertEquals(task.getStatus().timestamp(), deserialized.getStatus().timestamp()); + assertEquals(task.getHistory().size(), deserialized.getHistory().size()); + assertEquals(task.getArtifacts().size(), deserialized.getArtifacts().size()); + assertNotNull(deserialized.getMetadata()); + assertEquals("value1", deserialized.getMetadata().get("key1")); + } + + @Test + void testTaskWithDifferentStates() throws JsonProcessingException { + for (TaskState state : TaskState.values()) { + Task task = new Task.Builder() + .id("task-" + state.asString()) + .contextId("context-123") + .status(new TaskStatus(state)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Verify state is serialized correctly + assertTrue(json.contains("\"state\":\"" + state.asString() + "\"")); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify state is preserved + assertEquals(state, deserialized.getStatus().state()); + } + } + + @Test + void testTaskWithNullOptionalFields() throws JsonProcessingException { + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.SUBMITTED)) + // artifacts, history, metadata not set + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify required fields are present + assertEquals("task-123", deserialized.getId()); + assertEquals("context-456", deserialized.getContextId()); + assertEquals(TaskState.SUBMITTED, deserialized.getStatus().state()); + + // Verify optional lists default to empty + assertNotNull(deserialized.getArtifacts()); + assertEquals(0, deserialized.getArtifacts().size()); + assertNotNull(deserialized.getHistory()); + assertEquals(0, deserialized.getHistory().size()); + } + + @Test + void testTaskWithFilePartBytes() throws JsonProcessingException { + FilePart filePart = new FilePart(new FileWithBytes("application/pdf", "document.pdf", "base64data")); + + Artifact artifact = new Artifact.Builder() + .artifactId("file-artifact") + .parts(List.of(filePart)) + .build(); + + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.COMPLETED)) + .artifacts(List.of(artifact)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Verify JSON contains file part data + assertTrue(json.contains("\"kind\":\"file\"")); + assertTrue(json.contains("document.pdf")); + assertTrue(json.contains("application/pdf")); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify file part is preserved + Part part = deserialized.getArtifacts().get(0).parts().get(0); + assertTrue(part instanceof FilePart); + FilePart deserializedFilePart = (FilePart) part; + assertTrue(deserializedFilePart.getFile() instanceof FileWithBytes); + FileWithBytes fileWithBytes = (FileWithBytes) deserializedFilePart.getFile(); + assertEquals("document.pdf", fileWithBytes.name()); + assertEquals("application/pdf", fileWithBytes.mimeType()); + } + + @Test + void testTaskWithFilePartUri() throws JsonProcessingException { + FilePart filePart = new FilePart(new FileWithUri("image/png", "photo.png", "https://example.com/photo.png")); + + Artifact artifact = new Artifact.Builder() + .artifactId("uri-artifact") + .parts(List.of(filePart)) + .build(); + + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.COMPLETED)) + .artifacts(List.of(artifact)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Verify JSON contains URI + assertTrue(json.contains("https://example.com/photo.png")); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify file part URI is preserved + Part part = deserialized.getArtifacts().get(0).parts().get(0); + assertTrue(part instanceof FilePart); + FilePart deserializedFilePart = (FilePart) part; + assertTrue(deserializedFilePart.getFile() instanceof FileWithUri); + FileWithUri fileWithUri = (FileWithUri) deserializedFilePart.getFile(); + assertEquals("https://example.com/photo.png", fileWithUri.uri()); + } + + @Test + void testTaskWithDataPart() throws JsonProcessingException { + DataPart dataPart = new DataPart(Map.of("temperature", 22.5, "humidity", 65)); + + Artifact artifact = new Artifact.Builder() + .artifactId("data-artifact") + .parts(List.of(dataPart)) + .build(); + + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.COMPLETED)) + .artifacts(List.of(artifact)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Verify JSON contains data part + assertTrue(json.contains("\"kind\":\"data\"")); + assertTrue(json.contains("temperature")); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify data part is preserved + Part part = deserialized.getArtifacts().get(0).parts().get(0); + assertTrue(part instanceof DataPart); + DataPart deserializedDataPart = (DataPart) part; + assertNotNull(deserializedDataPart.getData()); + } + + @Test + void testTaskRoundTrip() throws JsonProcessingException { + // Create a comprehensive task with all part types + OffsetDateTime timestamp = OffsetDateTime.now(); + + Task original = new Task.Builder() + .id("task-123") + .contextId("context-789") + .status(new TaskStatus(TaskState.WORKING, null, timestamp)) + .history(List.of( + new Message.Builder() + .role(Message.Role.USER) + .parts(List.of( + new TextPart("Text"), + new FilePart(new FileWithBytes("text/plain", "file.txt", "data")), + new DataPart(Map.of("key", "value")) + )) + .build() + )) + .artifacts(List.of( + new Artifact.Builder() + .artifactId("artifact-1") + .parts(List.of(new TextPart("Content"))) + .build() + )) + .metadata(Map.of("meta1", "value1")) + .build(); + + // Serialize to JSON + String json = JsonUtil.toJson(original); + + // Deserialize back to Task + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Serialize again + String json2 = JsonUtil.toJson(deserialized); + + // Deserialize again + Task deserialized2 = JsonUtil.fromJson(json2, Task.class); + + // Verify multiple round-trips produce identical results + assertEquals(deserialized.getId(), deserialized2.getId()); + assertEquals(deserialized.getContextId(), deserialized2.getContextId()); + assertEquals(deserialized.getStatus().state(), deserialized2.getStatus().state()); + assertEquals(deserialized.getHistory().size(), deserialized2.getHistory().size()); + assertEquals(deserialized.getArtifacts().size(), deserialized2.getArtifacts().size()); + } + + @Test + void testTaskStatusWithMessage() throws JsonProcessingException { + Message statusMessage = new Message.Builder() + .role(Message.Role.AGENT) + .parts(List.of(new TextPart("Processing complete"))) + .build(); + + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.COMPLETED, statusMessage, null)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Verify JSON contains status message + assertTrue(json.contains("\"state\":\"completed\"")); + assertTrue(json.contains("Processing complete")); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify status message is preserved + assertEquals(TaskState.COMPLETED, deserialized.getStatus().state()); + assertNotNull(deserialized.getStatus().message()); + assertEquals(Message.Role.AGENT, deserialized.getStatus().message().getRole()); + assertTrue(deserialized.getStatus().message().getParts().get(0) instanceof TextPart); + } + + @Test + void testDeserializeTaskFromJson() throws JsonProcessingException { + String json = """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "submitted" + }, + "kind": "task" + } + """; + + Task task = JsonUtil.fromJson(json, Task.class); + + assertEquals("task-123", task.getId()); + assertEquals("context-456", task.getContextId()); + assertEquals(TaskState.SUBMITTED, task.getStatus().state()); + assertNull(task.getStatus().message()); + // TaskStatus automatically sets timestamp to current time if not provided + assertNotNull(task.getStatus().timestamp()); + } + + @Test + void testDeserializeTaskWithArtifactsFromJson() throws JsonProcessingException { + String json = """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "completed" + }, + "artifacts": [ + { + "artifactId": "artifact-1", + "name": "Result", + "parts": [ + { + "kind": "text", + "text": "Hello World" + } + ] + } + ], + "kind": "task" + } + """; + + Task task = JsonUtil.fromJson(json, Task.class); + + assertEquals("task-123", task.getId()); + assertEquals(TaskState.COMPLETED, task.getStatus().state()); + assertEquals(1, task.getArtifacts().size()); + assertEquals("artifact-1", task.getArtifacts().get(0).artifactId()); + assertEquals("Result", task.getArtifacts().get(0).name()); + assertEquals(1, task.getArtifacts().get(0).parts().size()); + assertTrue(task.getArtifacts().get(0).parts().get(0) instanceof TextPart); + assertEquals("Hello World", ((TextPart) task.getArtifacts().get(0).parts().get(0)).getText()); + } + + @Test + void testDeserializeTaskWithFilePartBytesFromJson() throws JsonProcessingException { + String json = """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "completed" + }, + "artifacts": [ + { + "artifactId": "file-artifact", + "parts": [ + { + "kind": "file", + "file": { + "mimeType": "application/pdf", + "name": "document.pdf", + "bytes": "base64encodeddata" + } + } + ] + } + ], + "kind": "task" + } + """; + + Task task = JsonUtil.fromJson(json, Task.class); + + assertEquals("task-123", task.getId()); + assertEquals(1, task.getArtifacts().size()); + Part part = task.getArtifacts().get(0).parts().get(0); + assertTrue(part instanceof FilePart); + FilePart filePart = (FilePart) part; + assertTrue(filePart.getFile() instanceof FileWithBytes); + FileWithBytes fileWithBytes = (FileWithBytes) filePart.getFile(); + assertEquals("application/pdf", fileWithBytes.mimeType()); + assertEquals("document.pdf", fileWithBytes.name()); + assertEquals("base64encodeddata", fileWithBytes.bytes()); + } + + @Test + void testDeserializeTaskWithFilePartUriFromJson() throws JsonProcessingException { + String json = """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "completed" + }, + "artifacts": [ + { + "artifactId": "uri-artifact", + "parts": [ + { + "kind": "file", + "file": { + "mimeType": "image/png", + "name": "photo.png", + "uri": "https://example.com/photo.png" + } + } + ] + } + ], + "kind": "task" + } + """; + + Task task = JsonUtil.fromJson(json, Task.class); + + assertEquals("task-123", task.getId()); + Part part = task.getArtifacts().get(0).parts().get(0); + assertTrue(part instanceof FilePart); + FilePart filePart = (FilePart) part; + assertTrue(filePart.getFile() instanceof FileWithUri); + FileWithUri fileWithUri = (FileWithUri) filePart.getFile(); + assertEquals("image/png", fileWithUri.mimeType()); + assertEquals("photo.png", fileWithUri.name()); + assertEquals("https://example.com/photo.png", fileWithUri.uri()); + } + + @Test + void testDeserializeTaskWithDataPartFromJson() throws JsonProcessingException { + String json = """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "completed" + }, + "artifacts": [ + { + "artifactId": "data-artifact", + "parts": [ + { + "kind": "data", + "data": { + "temperature": 22.5, + "humidity": 65 + } + } + ] + } + ], + "kind": "task" + } + """; + + Task task = JsonUtil.fromJson(json, Task.class); + + assertEquals("task-123", task.getId()); + Part part = task.getArtifacts().get(0).parts().get(0); + assertTrue(part instanceof DataPart); + DataPart dataPart = (DataPart) part; + assertNotNull(dataPart.getData()); + } + + @Test + void testDeserializeTaskWithHistoryFromJson() throws JsonProcessingException { + String json = """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "working" + }, + "history": [ + { + "role": "user", + "parts": [ + { + "kind": "text", + "text": "User message" + } + ] + }, + { + "role": "agent", + "parts": [ + { + "kind": "text", + "text": "Agent response" + } + ] + } + ], + "kind": "task" + } + """; + + Task task = JsonUtil.fromJson(json, Task.class); + + assertEquals("task-123", task.getId()); + assertEquals(2, task.getHistory().size()); + assertEquals(Message.Role.USER, task.getHistory().get(0).getRole()); + assertEquals(Message.Role.AGENT, task.getHistory().get(1).getRole()); + assertTrue(task.getHistory().get(0).getParts().get(0) instanceof TextPart); + assertEquals("User message", ((TextPart) task.getHistory().get(0).getParts().get(0)).getText()); + } + + @Test + void testDeserializeTaskWithTimestampFromJson() throws JsonProcessingException { + String json = """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "working", + "timestamp": "2023-10-01T12:00:00.234-05:00" + }, + "kind": "task" + } + """; + + Task task = JsonUtil.fromJson(json, Task.class); + + assertEquals("task-123", task.getId()); + assertEquals(TaskState.WORKING, task.getStatus().state()); + assertNotNull(task.getStatus().timestamp()); + assertEquals("2023-10-01T12:00:00.234-05:00", task.getStatus().timestamp().toString()); + } + + @Test + void testDeserializeTaskWithMetadataFromJson() throws JsonProcessingException { + String json = """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "completed" + }, + "metadata": { + "key1": "value1", + "key2": 42 + }, + "kind": "task" + } + """; + + Task task = JsonUtil.fromJson(json, Task.class); + + assertEquals("task-123", task.getId()); + assertNotNull(task.getMetadata()); + assertEquals("value1", task.getMetadata().get("key1")); + } + + @Test + void testTaskWithMixedPartTypes() throws JsonProcessingException { + Artifact artifact = new Artifact.Builder() + .artifactId("mixed-artifact") + .parts(List.of( + new TextPart("Text content"), + new FilePart(new FileWithBytes("application/json", "data.json", "{}")), + new DataPart(Map.of("result", 42)), + new FilePart(new FileWithUri("image/png", "image.png", "https://example.com/img.png")) + )) + .build(); + + Task task = new Task.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.COMPLETED)) + .artifacts(List.of(artifact)) + .build(); + + // Serialize + String json = JsonUtil.toJson(task); + + // Deserialize + Task deserialized = JsonUtil.fromJson(json, Task.class); + + // Verify all part types are preserved + List> parts = deserialized.getArtifacts().get(0).parts(); + assertEquals(4, parts.size()); + assertTrue(parts.get(0) instanceof TextPart); + assertTrue(parts.get(1) instanceof FilePart); + assertTrue(parts.get(2) instanceof DataPart); + assertTrue(parts.get(3) instanceof FilePart); + } +} diff --git a/spec/src/test/java/io/a2a/spec/TaskStatusTest.java b/spec/src/test/java/io/a2a/spec/TaskStatusTest.java deleted file mode 100644 index 7c4a9db8a..000000000 --- a/spec/src/test/java/io/a2a/spec/TaskStatusTest.java +++ /dev/null @@ -1,94 +0,0 @@ -package io.a2a.spec; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import org.junit.jupiter.api.Test; - -import java.time.OffsetDateTime; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class TaskStatusTest { - - private static final ObjectMapper OBJECT_MAPPER; - - private static final String REPLACE_TIMESTAMP_PATTERN = ".*\"timestamp\":\"([^\"]+)\",?.*"; - - static { - OBJECT_MAPPER = new ObjectMapper(); - OBJECT_MAPPER.registerModule(new JavaTimeModule()); - OBJECT_MAPPER.configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false); - } - - @Test - public void testTaskStatusWithSetTimestamp() { - TaskState state = TaskState.WORKING; - OffsetDateTime offsetDateTime = OffsetDateTime.parse("2023-10-01T12:00:00Z"); - TaskStatus status = new TaskStatus(state, offsetDateTime); - - assertNotNull(status.timestamp()); - assertEquals(offsetDateTime, status.timestamp()); - } - - @Test - public void testTaskStatusWithProvidedTimestamp() { - OffsetDateTime providedTimestamp = OffsetDateTime.parse("2024-01-01T00:00:00Z"); - TaskState state = TaskState.COMPLETED; - TaskStatus status = new TaskStatus(state, providedTimestamp); - - assertEquals(providedTimestamp, status.timestamp()); - } - - @Test - public void testTaskStatusSerializationUsesISO8601Format() throws Exception { - OffsetDateTime expectedTimestamp = OffsetDateTime.parse("2023-10-01T12:00:00.234-05:00"); - TaskState state = TaskState.WORKING; - TaskStatus status = new TaskStatus(state, expectedTimestamp); - - String json = OBJECT_MAPPER.writeValueAsString(status); - - String expectedJson = "{\"state\":\"working\",\"timestamp\":\"2023-10-01T12:00:00.234-05:00\"}"; - assertEquals(expectedJson, json); - } - - @Test - public void testTaskStatusDeserializationWithValidISO8601Format() throws Exception { - String validJson = "{" - + "\"state\": \"auth-required\"," - + "\"timestamp\": \"2023-10-01T12:00:00.10+03:00\"" - + "}"; - - TaskStatus result = OBJECT_MAPPER.readValue(validJson, TaskStatus.class); - assertEquals(TaskState.AUTH_REQUIRED, result.state()); - assertNotNull(result.timestamp()); - assertEquals(OffsetDateTime.parse("2023-10-01T12:00:00.100+03:00"), result.timestamp()); - } - - @Test - public void testTaskStatusDeserializationWithInvalidISO8601FormatFails() { - String invalidJson = "{" - + "\"state\": \"completed\"," - + "\"timestamp\": \"2023/10/01 12:00:00\"" - + "}"; - - assertThrows( - com.fasterxml.jackson.databind.exc.InvalidFormatException.class, - () -> OBJECT_MAPPER.readValue(invalidJson, TaskStatus.class) - ); - } - - @Test - public void testTaskStatusJsonTimestampMatchesISO8601Regex() throws Exception { - TaskState state = TaskState.WORKING; - OffsetDateTime expectedTimestamp = OffsetDateTime.parse("2023-10-01T12:00:00.234Z"); - TaskStatus status = new TaskStatus(state, expectedTimestamp); - - String json = OBJECT_MAPPER.writeValueAsString(status); - - String timestampValue = json.replaceAll(REPLACE_TIMESTAMP_PATTERN, "$1"); - assertEquals(expectedTimestamp, OffsetDateTime.parse(timestampValue)); - } -} diff --git a/tck/pom.xml b/tck/pom.xml index 6b4002b03..06aab7b63 100644 --- a/tck/pom.xml +++ b/tck/pom.xml @@ -30,7 +30,7 @@
io.quarkus - quarkus-rest-jackson + quarkus-rest provided diff --git a/tests/server-common/src/test/java/io/a2a/server/apps/common/A2AGsonObjectMapper.java b/tests/server-common/src/test/java/io/a2a/server/apps/common/A2AGsonObjectMapper.java new file mode 100644 index 000000000..c456ff3b1 --- /dev/null +++ b/tests/server-common/src/test/java/io/a2a/server/apps/common/A2AGsonObjectMapper.java @@ -0,0 +1,38 @@ +/* + * Copyright The WildFly Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package io.a2a.server.apps.common; + +import io.a2a.json.JsonProcessingException; +import io.a2a.json.JsonUtil; +import io.restassured.mapper.ObjectMapper; +import io.restassured.mapper.ObjectMapperDeserializationContext; +import io.restassured.mapper.ObjectMapperSerializationContext; + + +public class A2AGsonObjectMapper implements ObjectMapper { + public static final A2AGsonObjectMapper INSTANCE = new A2AGsonObjectMapper(); + + private A2AGsonObjectMapper() { + } + + @Override + public Object deserialize(ObjectMapperDeserializationContext context) { + try { + return JsonUtil.fromJson(context.getDataToDeserialize().asString(), context.getType()); + } catch (JsonProcessingException ex) { + throw new RuntimeException(ex); + } + } + + @Override + public Object serialize(ObjectMapperSerializationContext context) { + try { + return JsonUtil.toJson(context.getObjectToSerialize()); + } catch (JsonProcessingException ex) { + + throw new RuntimeException(ex); + } + } +} diff --git a/tests/server-common/src/test/java/io/a2a/server/apps/common/AbstractA2AServerTest.java b/tests/server-common/src/test/java/io/a2a/server/apps/common/AbstractA2AServerTest.java index 7fcf32cb4..be5d8403a 100644 --- a/tests/server-common/src/test/java/io/a2a/server/apps/common/AbstractA2AServerTest.java +++ b/tests/server-common/src/test/java/io/a2a/server/apps/common/AbstractA2AServerTest.java @@ -1,6 +1,5 @@ package io.a2a.server.apps.common; -import static io.restassured.RestAssured.given; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -31,7 +30,7 @@ import jakarta.ws.rs.core.MediaType; -import com.fasterxml.jackson.core.JsonProcessingException; +import io.a2a.json.JsonProcessingException; import io.a2a.client.Client; import io.a2a.client.ClientBuilder; import io.a2a.client.ClientEvent; @@ -72,7 +71,11 @@ import io.a2a.spec.TextPart; import io.a2a.spec.TransportProtocol; import io.a2a.spec.UnsupportedOperationError; +import io.a2a.json.JsonUtil; import io.a2a.util.Utils; +import io.restassured.RestAssured; +import io.restassured.config.ObjectMapperConfig; +import io.restassured.specification.RequestSpecification; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -114,6 +117,13 @@ public abstract class AbstractA2AServerTest { .build(); public static final String APPLICATION_JSON = "application/json"; + public static RequestSpecification given() { + return RestAssured.given() + .config(RestAssured.config() + .objectMapperConfig(new ObjectMapperConfig(A2AGsonObjectMapper.INSTANCE))); +} + + protected final int serverPort; private Client client; private Client nonStreamingClient; @@ -1355,7 +1365,7 @@ private CompletableFuture>> initialiseStreamingReque // Create the request HttpRequest.Builder builder = HttpRequest.newBuilder() .uri(URI.create("http://localhost:" + serverPort + "/")) - .POST(HttpRequest.BodyPublishers.ofString(Utils.OBJECT_MAPPER.writeValueAsString(request))) + .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(request))) .header("Content-Type", APPLICATION_JSON); if (mediaType != null) { builder.header("Accept", mediaType); @@ -1370,7 +1380,7 @@ private CompletableFuture>> initialiseStreamingReque private SendStreamingMessageResponse extractJsonResponseFromSseLine(String line) throws JsonProcessingException { line = extractSseData(line); if (line != null) { - return Utils.OBJECT_MAPPER.readValue(line, SendStreamingMessageResponse.class); + return JsonUtil.fromJson(line, SendStreamingMessageResponse.class); } return null; } @@ -1407,7 +1417,7 @@ protected void saveTaskInTaskStore(Task task) throws Exception { .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:" + serverPort + "/test/task")) - .POST(HttpRequest.BodyPublishers.ofString(Utils.OBJECT_MAPPER.writeValueAsString(task))) + .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(task))) .header("Content-Type", APPLICATION_JSON) .build(); @@ -1433,7 +1443,7 @@ protected Task getTaskFromTaskStore(String taskId) throws Exception { if (response.statusCode() != 200) { throw new RuntimeException(String.format("Getting task failed! Status: %d, Body: %s", response.statusCode(), response.body())); } - return Utils.OBJECT_MAPPER.readValue(response.body(), Task.TYPE_REFERENCE); + return JsonUtil.fromJson(response.body(), Task.class); } protected void deleteTaskInTaskStore(String taskId) throws Exception { @@ -1480,7 +1490,7 @@ protected void enqueueEventOnServer(Event event) throws Exception { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:" + serverPort + "/" + path)) .header("Content-Type", APPLICATION_JSON) - .POST(HttpRequest.BodyPublishers.ofString(Utils.OBJECT_MAPPER.writeValueAsString(event))) + .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(event))) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); @@ -1569,7 +1579,7 @@ protected void savePushNotificationConfigInStore(String taskId, PushNotification .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:" + serverPort + "/test/task/" + taskId)) - .POST(HttpRequest.BodyPublishers.ofString(Utils.OBJECT_MAPPER.writeValueAsString(notificationConfig))) + .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(notificationConfig))) .header("Content-Type", APPLICATION_JSON) .build(); diff --git a/tests/server-common/src/test/java/io/a2a/server/apps/common/TestHttpClient.java b/tests/server-common/src/test/java/io/a2a/server/apps/common/TestHttpClient.java index f161307aa..7cc62dcfe 100644 --- a/tests/server-common/src/test/java/io/a2a/server/apps/common/TestHttpClient.java +++ b/tests/server-common/src/test/java/io/a2a/server/apps/common/TestHttpClient.java @@ -13,8 +13,9 @@ import io.a2a.client.http.A2AHttpClient; import io.a2a.client.http.A2AHttpResponse; +import io.a2a.json.JsonProcessingException; import io.a2a.spec.Task; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; import java.util.Map; @Dependent @@ -48,7 +49,11 @@ public PostBuilder body(String body) { @Override public A2AHttpResponse post() throws IOException, InterruptedException { - tasks.add(Utils.OBJECT_MAPPER.readValue(body, Task.TYPE_REFERENCE)); + try { + tasks.add(JsonUtil.fromJson(body, Task.class)); + } catch (JsonProcessingException e) { + throw new IOException("Failed to parse task JSON", e); + } try { return new A2AHttpResponse() { @Override diff --git a/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandler.java b/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandler.java index 9a9d5eef1..ef577aaa5 100644 --- a/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandler.java +++ b/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandler.java @@ -229,7 +229,7 @@ public GetAuthenticatedExtendedCardResponse onGetAuthenticatedExtendedCardReques GetAuthenticatedExtendedCardRequest request, ServerCallContext context) { if ( !agentCard.supportsAuthenticatedExtendedCard() || !extendedAgentCard.isResolvable()) { return new GetAuthenticatedExtendedCardResponse(request.getId(), - new AuthenticatedExtendedCardNotConfiguredError()); + new AuthenticatedExtendedCardNotConfiguredError(null, "Authenticated Extended Card not configured", null)); } try { return new GetAuthenticatedExtendedCardResponse(request.getId(), extendedAgentCard.get()); diff --git a/transport/rest/pom.xml b/transport/rest/pom.xml index 53344385d..c4d1c5ff6 100644 --- a/transport/rest/pom.xml +++ b/transport/rest/pom.xml @@ -51,10 +51,6 @@ mockito-core test - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - com.google.protobuf protobuf-java-util diff --git a/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java b/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java index 85d307f53..da4bcba42 100644 --- a/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java +++ b/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java @@ -2,7 +2,8 @@ import static io.a2a.server.util.async.AsyncUtils.createTubeConfig; -import com.fasterxml.jackson.core.JacksonException; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.util.JsonFormat; import io.a2a.grpc.utils.ProtoUtils; @@ -41,7 +42,7 @@ import io.a2a.spec.TaskQueryParams; import io.a2a.spec.UnsupportedOperationError; import io.a2a.server.util.async.Internal; -import io.a2a.util.Utils; +import io.a2a.json.JsonUtil; import jakarta.enterprise.inject.Instance; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; @@ -238,8 +239,8 @@ private void parseRequestBody(String body, com.google.protobuf.Message.Builder b private void validate(String json) { try { - Utils.OBJECT_MAPPER.readTree(json); - } catch (JacksonException e) { + JsonParser.parseString(json); + } catch (JsonSyntaxException e) { throw new JSONParseError(JSONParseError.DEFAULT_CODE, "Failed to parse json", e.getMessage()); } } @@ -346,9 +347,9 @@ private int mapErrorToHttpStatus(JSONRPCError error) { public HTTPRestResponse getAuthenticatedExtendedCard() { try { if (!agentCard.supportsAuthenticatedExtendedCard() || extendedAgentCard == null || !extendedAgentCard.isResolvable()) { - throw new AuthenticatedExtendedCardNotConfiguredError(); + throw new AuthenticatedExtendedCardNotConfiguredError(null, "Authenticated Extended Card not configured", null); } - return new HTTPRestResponse(200, "application/json", Utils.OBJECT_MAPPER.writeValueAsString(extendedAgentCard.get())); + return new HTTPRestResponse(200, "application/json", JsonUtil.toJson(extendedAgentCard.get())); } catch (JSONRPCError e) { return createErrorResponse(e); } catch (Throwable t) { @@ -358,7 +359,7 @@ public HTTPRestResponse getAuthenticatedExtendedCard() { public HTTPRestResponse getAgentCard() { try { - return new HTTPRestResponse(200, "application/json", Utils.OBJECT_MAPPER.writeValueAsString(agentCard)); + return new HTTPRestResponse(200, "application/json", JsonUtil.toJson(agentCard)); } catch (Throwable t) { return createErrorResponse(500, new InternalError(t.getMessage())); } From 183663979480048d702fb0cd1dc5aeb614a9cd8a Mon Sep 17 00:00:00 2001 From: Emmanuel Hugonnet Date: Mon, 20 Apr 2026 10:10:58 +0200 Subject: [PATCH 25/37] chore: updating the workflows (#796) * chore: updating the workflows Signed-off-by: Emmanuel Hugonnet * chore: fixing javadoc Signed-off-by: Emmanuel Hugonnet * chore: Updating kafka version Signed-off-by: Emmanuel Hugonnet * fix; Fixing the last issues to be able to pass the TCK again Signed-off-by: Emmanuel Hugonnet * fix: Fixing the missing id in the jsonrpc response Extract request id before jsonrpc validation so error responses include top-level id. Signed-off-by: Emmanuel Hugonnet --------- Signed-off-by: Emmanuel Hugonnet --- .github/workflows/build-and-test.yml | 6 +- .../workflows/build-with-release-profile.yml | 71 +++++------- .../workflows/cloud-deployment-example.yml | 8 +- examples/cloud-deployment/scripts/deploy.sh | 17 +++ .../server/apps/quarkus/A2AServerRoutes.java | 16 +-- spec/src/main/java/io/a2a/json/JsonUtil.java | 109 +++++++++++++++++- .../java/io/a2a/spec/AgentCapabilities.java | 5 + spec/src/main/java/io/a2a/spec/AgentCard.java | 19 +++ .../java/io/a2a/spec/AgentCardSignature.java | 4 + .../main/java/io/a2a/spec/AgentExtension.java | 5 + .../main/java/io/a2a/spec/AgentInterface.java | 4 +- .../main/java/io/a2a/spec/AgentProvider.java | 3 + .../src/main/java/io/a2a/spec/AgentSkill.java | 9 ++ spec/src/main/java/io/a2a/spec/Artifact.java | 7 ++ .../java/io/a2a/spec/AuthenticationInfo.java | 3 + .../a2a/spec/AuthorizationCodeOAuthFlow.java | 5 + .../a2a/spec/ClientCredentialsOAuthFlow.java | 4 + ...eleteTaskPushNotificationConfigParams.java | 4 + .../main/java/io/a2a/spec/FileWithBytes.java | 4 + .../main/java/io/a2a/spec/FileWithUri.java | 4 + .../GetTaskPushNotificationConfigParams.java | 4 + .../java/io/a2a/spec/ImplicitOAuthFlow.java | 4 + .../main/java/io/a2a/spec/JSONRPCRequest.java | 2 + .../java/io/a2a/spec/JSONRPCResponse.java | 2 + .../ListTaskPushNotificationConfigParams.java | 3 + spec/src/main/java/io/a2a/spec/Message.java | 13 +++ .../io/a2a/spec/MessageSendConfiguration.java | 7 +- .../java/io/a2a/spec/MessageSendParams.java | 10 ++ .../a2a/spec/NonStreamingJSONRPCRequest.java | 2 + .../src/main/java/io/a2a/spec/OAuthFlows.java | 5 + .../java/io/a2a/spec/PasswordOAuthFlow.java | 4 + .../PushNotificationAuthenticationInfo.java | 3 + .../io/a2a/spec/PushNotificationConfig.java | 5 + .../java/io/a2a/spec/SendMessageRequest.java | 16 +++ .../a2a/spec/SendStreamingMessageRequest.java | 14 +++ .../io/a2a/spec/StreamingJSONRPCRequest.java | 3 +- .../main/java/io/a2a/spec/TaskIdParams.java | 3 + .../a2a/spec/TaskPushNotificationConfig.java | 3 + spec/src/main/java/io/a2a/util/Utils.java | 5 +- .../spec/JSONRPCErrorSerializationTest.java | 34 ++++++ .../a2a/tck/server/AgentExecutorProducer.java | 2 +- .../jsonrpc/handler/JSONRPCHandler.java | 9 +- 42 files changed, 389 insertions(+), 71 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index e2fb7253d..69cc8a178 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -18,7 +18,7 @@ jobs: matrix: java-version: ['17', '21', '25'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up JDK ${{ matrix.java-version }} uses: actions/setup-java@v4 with: @@ -29,7 +29,7 @@ jobs: run: mvn -B package --file pom.xml -fae - name: Upload Test Reports if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: surefire-reports-java-${{ matrix.java-version }} path: | @@ -39,7 +39,7 @@ jobs: if-no-files-found: warn - name: Upload Build Logs if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: build-logs-java-${{ matrix.java-version }} path: | diff --git a/.github/workflows/build-with-release-profile.yml b/.github/workflows/build-with-release-profile.yml index 129833307..70737fca9 100644 --- a/.github/workflows/build-with-release-profile.yml +++ b/.github/workflows/build-with-release-profile.yml @@ -1,12 +1,13 @@ -name: Build with '-Prelease' - -# Simply runs the build with -Prelease to avoid nasty surprises when running the release-to-maven-central workflow. +name: Build with '-Prelease' (Trigger) +# Trigger workflow for release profile build verification. +# This workflow runs on PRs and uploads the PR info for the workflow_run job. +# The actual build with secrets happens in build-with-release-profile-run.yml +# See: https://securitylab.github.com/research/github-actions-preventing-pwn-requests on: - # Handle all branches for now + pull_request: # Changed from pull_request_target for security push: - pull_request_target: workflow_dispatch: # Only run the latest job @@ -15,7 +16,7 @@ concurrency: cancel-in-progress: true jobs: - build: + trigger: # Only run this job for the main repository, not for forks if: github.repository == 'a2aproject/a2a-java' runs-on: ubuntu-latest @@ -23,39 +24,27 @@ jobs: contents: read steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - cache: maven - - # Use secrets to import GPG key - - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v6 - with: - gpg_private_key: ${{ secrets.GPG_SIGNING_KEY }} - passphrase: ${{ secrets.GPG_SIGNING_PASSPHRASE }} - - # Create settings.xml for Maven since it needs the 'central-a2asdk-temp' server. - # Populate wqith username and password from secrets - - name: Create settings.xml + - name: Prepare PR info run: | - mkdir -p ~/.m2 - echo "central-a2asdk-temp${{ secrets.CENTRAL_TOKEN_USERNAME }}${{ secrets.CENTRAL_TOKEN_PASSWORD }}" > ~/.m2/settings.xml - - # Build with the same settings as the deploy job - # -s uses the settings file we created. - - name: Build with same arguments as deploy job - run: > - mvn -B install - -s ~/.m2/settings.xml - -P release - -DskipTests - -Drelease.auto.publish=true - env: - # GPG passphrase is set as an environment variable for the gpg plugin to use - GPG_PASSPHRASE: ${{ secrets.GPG_SIGNING_PASSPHRASE }} \ No newline at end of file + mkdir -p pr_info + + # Store PR number for workflow_run job + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo ${{ github.event.number }} > pr_info/pr_number + echo ${{ github.event.pull_request.head.sha }} > pr_info/pr_sha + echo ${{ github.event.pull_request.head.ref }} > pr_info/pr_ref + else + # For push events, store the commit sha + echo ${{ github.sha }} > pr_info/pr_sha + echo ${{ github.ref }} > pr_info/pr_ref + fi + + echo "Event: ${{ github.event_name }}" + cat pr_info/* + + - name: Upload PR info + uses: actions/upload-artifact@v6 + with: + name: pr-info + path: pr_info/ + retention-days: 1 diff --git a/.github/workflows/cloud-deployment-example.yml b/.github/workflows/cloud-deployment-example.yml index f52ea5111..57a97a638 100644 --- a/.github/workflows/cloud-deployment-example.yml +++ b/.github/workflows/cloud-deployment-example.yml @@ -16,8 +16,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v4 - + uses: actions/checkout@v6 - name: Set up JDK 17 uses: actions/setup-java@v5 with: @@ -27,7 +26,7 @@ jobs: - name: Install Kind run: | - curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64 + curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.31.0/kind-linux-amd64 chmod +x ./kind sudo mv ./kind /usr/local/bin/kind kind version @@ -58,7 +57,8 @@ jobs: mvn test-compile exec:java \ -Dexec.mainClass="io.a2a.examples.cloud.A2ACloudExampleClient" \ -Dexec.classpathScope=test \ - -Dagent.url=http://localhost:8080 + -Dagent.url=http://localhost:8080 \ + -Dci.mode=true - name: Show diagnostics on failure if: failure() diff --git a/examples/cloud-deployment/scripts/deploy.sh b/examples/cloud-deployment/scripts/deploy.sh index 98bcb35df..433f22dc3 100755 --- a/examples/cloud-deployment/scripts/deploy.sh +++ b/examples/cloud-deployment/scripts/deploy.sh @@ -181,6 +181,7 @@ if ! kubectl get crd kafkas.kafka.strimzi.io > /dev/null 2>&1; then echo "Installing Strimzi operator (1.0.0)..." kubectl create -f '../strimzi-1.0.0/strimzi-cluster-operator-1.0.0.yaml' -n kafka + echo "Waiting for Strimzi operator deployment to be created..." for i in {1..30}; do if kubectl get deployment strimzi-cluster-operator -n kafka > /dev/null 2>&1; then @@ -213,6 +214,22 @@ echo "" echo "Deploying PostgreSQL..." kubectl apply -f ../k8s/01-postgres.yaml echo "Waiting for PostgreSQL to be ready..." + +# Wait for pod to be created (StatefulSet takes time to create pod) +for i in {1..30}; do + if kubectl get pod -l app=postgres -n a2a-demo 2>/dev/null | grep -q postgres; then + echo "PostgreSQL pod found, waiting for ready state..." + break + fi + if [ $i -eq 30 ]; then + echo -e "${RED}ERROR: PostgreSQL pod not created after 30 seconds${NC}" + kubectl get statefulset -n a2a-demo + exit 1 + fi + sleep 1 +done + +# Now wait for pod to be ready kubectl wait --for=condition=Ready pod -l app=postgres -n a2a-demo --timeout=120s echo -e "${GREEN}✓ PostgreSQL deployed${NC}" diff --git a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java index 7a4f8e76f..1f14a61ae 100644 --- a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java +++ b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java @@ -100,14 +100,7 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) { throw new JSONParseError(e.getMessage()); } - // Validate jsonrpc field - com.google.gson.JsonElement jsonrpcElement = node.get("jsonrpc"); - if (jsonrpcElement == null || !jsonrpcElement.isJsonPrimitive() - || !JSONRPCMessage.JSONRPC_VERSION.equals(jsonrpcElement.getAsString())) { - throw new InvalidRequestError("Invalid JSON-RPC request: missing or invalid 'jsonrpc' field"); - } - - // Validate id field (must be string, number, or null — not an object or array) + // Extract id field early so error responses can include it com.google.gson.JsonElement idElement = node.get("id"); if (idElement != null && !idElement.isJsonNull() && !idElement.isJsonPrimitive()) { throw new InvalidRequestError("Invalid JSON-RPC request: 'id' must be a string, number, or null"); @@ -117,6 +110,13 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) { requestId = idPrimitive.isNumber() ? idPrimitive.getAsLong() : idPrimitive.getAsString(); } + // Validate jsonrpc field + com.google.gson.JsonElement jsonrpcElement = node.get("jsonrpc"); + if (jsonrpcElement == null || !jsonrpcElement.isJsonPrimitive() + || !JSONRPCMessage.JSONRPC_VERSION.equals(jsonrpcElement.getAsString())) { + throw new InvalidRequestError("Invalid JSON-RPC request: missing or invalid 'jsonrpc' field"); + } + // Validate method field com.google.gson.JsonElement methodElement = node.get("method"); if (methodElement == null || !methodElement.isJsonPrimitive()) { diff --git a/spec/src/main/java/io/a2a/json/JsonUtil.java b/spec/src/main/java/io/a2a/json/JsonUtil.java index 56dd3f310..ab8a67f84 100644 --- a/spec/src/main/java/io/a2a/json/JsonUtil.java +++ b/spec/src/main/java/io/a2a/json/JsonUtil.java @@ -23,10 +23,13 @@ import com.google.gson.JsonSyntaxException; import com.google.gson.ToNumberPolicy; import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import io.a2a.spec.APIKeySecurityScheme; import io.a2a.spec.EventKind; +import io.a2a.spec.JSONRPCResponse; import io.a2a.spec.ContentTypeNotSupportedError; import io.a2a.spec.DataPart; import io.a2a.spec.FileContent; @@ -76,7 +79,10 @@ private static GsonBuilder createBaseGsonBuilder() { .registerTypeAdapter(Message.Role.class, new RoleTypeAdapter()) .registerTypeAdapter(Part.Kind.class, new PartKindTypeAdapter()) .registerTypeHierarchyAdapter(FileContent.class, new FileContentTypeAdapter()) - .registerTypeHierarchyAdapter(SecurityScheme.class, new SecuritySchemeTypeAdapter()); + .registerTypeHierarchyAdapter(SecurityScheme.class, new SecuritySchemeTypeAdapter()) + .registerTypeAdapter(void.class, new VoidTypeAdapter()) + .registerTypeAdapter(Void.class, new VoidTypeAdapter()) + .registerTypeAdapterFactory(new JSONRPCResponseTypeAdapterFactory()); } /** @@ -87,7 +93,6 @@ private static GsonBuilder createBaseGsonBuilder() { *

* Used throughout the SDK for consistent JSON serialization and deserialization. * - * @see GsonFactory#createGson() */ public static final Gson OBJECT_MAPPER = createBaseGsonBuilder() .registerTypeHierarchyAdapter(Part.class, new PartTypeAdapter()) @@ -725,8 +730,7 @@ public void write(JsonWriter out, StreamingEventKind value) throws java.io.IOExc } @Override - public @Nullable - StreamingEventKind read(JsonReader in) throws java.io.IOException { + public @Nullable StreamingEventKind read(JsonReader in) throws java.io.IOException { if (in.peek() == com.google.gson.stream.JsonToken.NULL) { in.nextNull(); return null; @@ -875,8 +879,7 @@ public void write(JsonWriter out, FileContent value) throws java.io.IOException } @Override - public @Nullable - FileContent read(JsonReader in) throws java.io.IOException { + public @Nullable FileContent read(JsonReader in) throws java.io.IOException { if (in.peek() == com.google.gson.stream.JsonToken.NULL) { in.nextNull(); return null; @@ -901,4 +904,98 @@ FileContent read(JsonReader in) throws java.io.IOException { } } + static class VoidTypeAdapter extends TypeAdapter { + + + @Override + @SuppressWarnings("resource") + public void write(final JsonWriter out, final Void value) throws java.io.IOException { + out.nullValue(); + } + + @Override + public @Nullable Void read(final JsonReader in) throws java.io.IOException { + in.skipValue(); + return null; + } + + } + + /** + * Gson TypeAdapterFactory for serializing {@link JSONRPCResponse} subclasses. + *

+ * JSON-RPC 2.0 requires that: + *

    + *
  • {@code result} MUST be present (possibly null) on success, and MUST NOT be present on error
  • + *
  • {@code error} MUST be present on error, and MUST NOT be present on success
  • + *
+ * Gson's default null-field-skipping behavior would omit {@code "result": null} for Void responses, + * so this factory writes the fields explicitly to comply with the spec. + */ + static class JSONRPCResponseTypeAdapterFactory implements TypeAdapterFactory { + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public @Nullable TypeAdapter create(Gson gson, TypeToken type) { + if (!JSONRPCResponse.class.isAssignableFrom(type.getRawType())) { + return null; + } + + TypeAdapter delegateAdapter = gson.getDelegateAdapter(this, type); + TypeAdapter errorAdapter = gson.getAdapter(JSONRPCError.class); + + return new TypeAdapter() { + @Override + public void write(JsonWriter out, T value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + return; + } + + JSONRPCResponse response = (JSONRPCResponse) value; + + out.beginObject(); + out.name("jsonrpc").value(response.getJsonrpc()); + + Object id = response.getId(); + out.name("id"); + if (id == null) { + out.nullValue(); + } else if (id instanceof Number n) { + out.value(n.longValue()); + } else { + out.value(id.toString()); + } + + JSONRPCError error = response.getError(); + if (error != null) { + out.name("error"); + errorAdapter.write(out, error); + } else { + out.name("result"); + Object result = response.getResult(); + if (result == null) { + // JsonWriter.nullValue() skips both name+value when serializeNulls=false, + // so we must temporarily enable it to write "result":null as required + // by JSON-RPC 2.0. + boolean prev = out.getSerializeNulls(); + out.setSerializeNulls(true); + out.nullValue(); + out.setSerializeNulls(prev); + } else { + TypeAdapter resultAdapter = gson.getAdapter(result.getClass()); + resultAdapter.write(out, result); + } + } + + out.endObject(); + } + + @Override + public T read(JsonReader in) throws java.io.IOException { + return delegateAdapter.read(in); + } + }; + } + } } diff --git a/spec/src/main/java/io/a2a/spec/AgentCapabilities.java b/spec/src/main/java/io/a2a/spec/AgentCapabilities.java index 1de51d5f9..2b4fbcee5 100644 --- a/spec/src/main/java/io/a2a/spec/AgentCapabilities.java +++ b/spec/src/main/java/io/a2a/spec/AgentCapabilities.java @@ -4,6 +4,11 @@ /** * Defines optional capabilities supported by an agent. + * + * @param streaming whether the agent supports streaming responses + * @param pushNotifications whether the agent supports push notifications + * @param stateTransitionHistory whether the agent supports state transition history + * @param extensions optional list of protocol extensions supported by the agent */ public record AgentCapabilities(boolean streaming, boolean pushNotifications, boolean stateTransitionHistory, List extensions) { diff --git a/spec/src/main/java/io/a2a/spec/AgentCard.java b/spec/src/main/java/io/a2a/spec/AgentCard.java index 2574f5425..7963f6c57 100644 --- a/spec/src/main/java/io/a2a/spec/AgentCard.java +++ b/spec/src/main/java/io/a2a/spec/AgentCard.java @@ -10,6 +10,25 @@ * The AgentCard is a self-describing manifest for an agent. It provides essential * metadata including the agent's identity, capabilities, skills, supported * communication methods, and security requirements. + * + * @param name the human-readable name of the agent + * @param description a human-readable description of the agent + * @param url the URL of the agent's primary endpoint + * @param provider the organization or individual providing the agent + * @param version the version of the agent + * @param documentationUrl optional URL to the agent's documentation + * @param capabilities the capabilities supported by the agent + * @param defaultInputModes the default input content modes supported by the agent + * @param defaultOutputModes the default output content modes supported by the agent + * @param skills the list of skills the agent can perform + * @param supportsAuthenticatedExtendedCard whether the agent supports an authenticated extended card + * @param securitySchemes the security scheme definitions available for this agent + * @param security the security requirements for accessing the agent + * @param iconUrl optional URL to the agent's icon + * @param additionalInterfaces additional transport/URL combinations for interacting with the agent + * @param preferredTransport the preferred transport protocol + * @param protocolVersion the A2A protocol version supported by the agent + * @param signatures optional JWS signatures of the agent card */ public record AgentCard(String name, String description, String url, AgentProvider provider, String version, String documentationUrl, AgentCapabilities capabilities, diff --git a/spec/src/main/java/io/a2a/spec/AgentCardSignature.java b/spec/src/main/java/io/a2a/spec/AgentCardSignature.java index 70a92cd57..35714554e 100644 --- a/spec/src/main/java/io/a2a/spec/AgentCardSignature.java +++ b/spec/src/main/java/io/a2a/spec/AgentCardSignature.java @@ -8,6 +8,10 @@ /** * Represents a JWS signature of an AgentCard. * This follows the JSON format of an RFC 7515 JSON Web Signature (JWS). + * + * @param header the JWS unprotected header + * @param protectedHeader the JWS protected header (base64url-encoded) + * @param signature the JWS signature value (base64url-encoded) */ public record AgentCardSignature(Map header, @SerializedName("protected")String protectedHeader, String signature) { diff --git a/spec/src/main/java/io/a2a/spec/AgentExtension.java b/spec/src/main/java/io/a2a/spec/AgentExtension.java index 053855976..76f1c9579 100644 --- a/spec/src/main/java/io/a2a/spec/AgentExtension.java +++ b/spec/src/main/java/io/a2a/spec/AgentExtension.java @@ -6,6 +6,11 @@ /** * A declaration of a protocol extension supported by an Agent. + * + * @param description a human-readable description of the extension + * @param params optional parameters for configuring the extension + * @param required whether the extension is required for the agent to function + * @param uri the URI identifying the extension */ public record AgentExtension (String description, Map params, boolean required, String uri) { diff --git a/spec/src/main/java/io/a2a/spec/AgentInterface.java b/spec/src/main/java/io/a2a/spec/AgentInterface.java index 0b2e8d8b0..ff30c913d 100644 --- a/spec/src/main/java/io/a2a/spec/AgentInterface.java +++ b/spec/src/main/java/io/a2a/spec/AgentInterface.java @@ -5,8 +5,10 @@ /** * Declares a combination of a target URL and a transport protocol for interacting with the agent. + * + * @param transport the transport protocol identifier (e.g., "jsonrpc", "grpc") + * @param url the endpoint URL for this transport */ - public record AgentInterface(String transport, String url) { public AgentInterface { Assert.checkNotNullParam("transport", transport); diff --git a/spec/src/main/java/io/a2a/spec/AgentProvider.java b/spec/src/main/java/io/a2a/spec/AgentProvider.java index 1d50b699e..eea07b24e 100644 --- a/spec/src/main/java/io/a2a/spec/AgentProvider.java +++ b/spec/src/main/java/io/a2a/spec/AgentProvider.java @@ -4,6 +4,9 @@ /** * Represents the service provider of an agent. + * + * @param organization the name of the organization providing the agent + * @param url the URL of the provider's website or documentation */ public record AgentProvider(String organization, String url) { diff --git a/spec/src/main/java/io/a2a/spec/AgentSkill.java b/spec/src/main/java/io/a2a/spec/AgentSkill.java index b397f6248..4ec0c6911 100644 --- a/spec/src/main/java/io/a2a/spec/AgentSkill.java +++ b/spec/src/main/java/io/a2a/spec/AgentSkill.java @@ -7,6 +7,15 @@ /** * The set of skills, or distinct capabilities, that the agent can perform. + * + * @param id a unique identifier for the skill + * @param name the human-readable name of the skill + * @param description a human-readable description of the skill + * @param tags tags for categorizing or discovering the skill + * @param examples example prompts or use cases for the skill + * @param inputModes the content modes accepted as input by the skill + * @param outputModes the content modes produced as output by the skill + * @param security optional security requirements specific to this skill */ public record AgentSkill(String id, String name, String description, List tags, List examples, List inputModes, List outputModes, diff --git a/spec/src/main/java/io/a2a/spec/Artifact.java b/spec/src/main/java/io/a2a/spec/Artifact.java index 69d2f0581..bda976da3 100644 --- a/spec/src/main/java/io/a2a/spec/Artifact.java +++ b/spec/src/main/java/io/a2a/spec/Artifact.java @@ -7,6 +7,13 @@ /** * Represents a file, data structure, or other resource generated by an agent during a task. + * + * @param artifactId a unique identifier for the artifact within the task + * @param name optional human-readable name of the artifact + * @param description optional human-readable description of the artifact + * @param parts the content parts that make up the artifact + * @param metadata optional additional metadata associated with the artifact + * @param extensions optional list of protocol extension identifiers */ public record Artifact(String artifactId, String name, String description, List> parts, Map metadata, List extensions) { diff --git a/spec/src/main/java/io/a2a/spec/AuthenticationInfo.java b/spec/src/main/java/io/a2a/spec/AuthenticationInfo.java index 4f24e3c4c..3ecc368a4 100644 --- a/spec/src/main/java/io/a2a/spec/AuthenticationInfo.java +++ b/spec/src/main/java/io/a2a/spec/AuthenticationInfo.java @@ -6,6 +6,9 @@ /** * The authentication info for an agent. + * + * @param schemes the list of authentication scheme identifiers + * @param credentials optional credentials string for the authentication scheme */ public record AuthenticationInfo(List schemes, String credentials) { diff --git a/spec/src/main/java/io/a2a/spec/AuthorizationCodeOAuthFlow.java b/spec/src/main/java/io/a2a/spec/AuthorizationCodeOAuthFlow.java index cc5e0ee54..886360b7a 100644 --- a/spec/src/main/java/io/a2a/spec/AuthorizationCodeOAuthFlow.java +++ b/spec/src/main/java/io/a2a/spec/AuthorizationCodeOAuthFlow.java @@ -7,6 +7,11 @@ /** * Defines configuration details for the OAuth 2.0 Authorization Code flow. + * + * @param authorizationUrl the URL for the authorization endpoint + * @param refreshUrl optional URL for obtaining refresh tokens + * @param scopes the available scopes mapped to their descriptions + * @param tokenUrl the URL for the token endpoint */ public record AuthorizationCodeOAuthFlow(String authorizationUrl, String refreshUrl, Map scopes, String tokenUrl) { diff --git a/spec/src/main/java/io/a2a/spec/ClientCredentialsOAuthFlow.java b/spec/src/main/java/io/a2a/spec/ClientCredentialsOAuthFlow.java index 18056681f..9577649e0 100644 --- a/spec/src/main/java/io/a2a/spec/ClientCredentialsOAuthFlow.java +++ b/spec/src/main/java/io/a2a/spec/ClientCredentialsOAuthFlow.java @@ -8,6 +8,10 @@ /** * Defines configuration details for the OAuth 2.0 Client Credentials flow. + * + * @param refreshUrl optional URL for obtaining refresh tokens + * @param scopes the available scopes mapped to their descriptions + * @param tokenUrl the URL for the token endpoint */ public record ClientCredentialsOAuthFlow(String refreshUrl, Map scopes, String tokenUrl) { diff --git a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java index 0cb34a38d..4e300a59f 100644 --- a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java +++ b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java @@ -7,6 +7,10 @@ /** * Parameters for removing pushNotificationConfiguration associated with a Task. + * + * @param id the task ID + * @param pushNotificationConfigId the ID of the push notification configuration to delete + * @param metadata optional additional metadata */ public record DeleteTaskPushNotificationConfigParams(String id, String pushNotificationConfigId, Map metadata) { diff --git a/spec/src/main/java/io/a2a/spec/FileWithBytes.java b/spec/src/main/java/io/a2a/spec/FileWithBytes.java index 01ccef127..0a5df369b 100644 --- a/spec/src/main/java/io/a2a/spec/FileWithBytes.java +++ b/spec/src/main/java/io/a2a/spec/FileWithBytes.java @@ -2,6 +2,10 @@ /** * Represents a file with its content provided directly as a base64-encoded string. + * + * @param mimeType the MIME type of the file content + * @param name optional name of the file + * @param bytes the base64-encoded file content */ public record FileWithBytes(String mimeType, String name, String bytes) implements FileContent { } diff --git a/spec/src/main/java/io/a2a/spec/FileWithUri.java b/spec/src/main/java/io/a2a/spec/FileWithUri.java index e1edd4bd2..45514ae04 100644 --- a/spec/src/main/java/io/a2a/spec/FileWithUri.java +++ b/spec/src/main/java/io/a2a/spec/FileWithUri.java @@ -2,6 +2,10 @@ /** * Represents a file with its content located at a specific URI. + * + * @param mimeType the MIME type of the file content + * @param name optional name of the file + * @param uri the URI pointing to the file content */ public record FileWithUri(String mimeType, String name, String uri) implements FileContent { } diff --git a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java index 2836e2065..200a3c3d5 100644 --- a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java +++ b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java @@ -8,6 +8,10 @@ /** * Parameters for fetching a pushNotificationConfiguration associated with a Task. + * + * @param id the task ID + * @param pushNotificationConfigId optional ID of a specific push notification configuration to retrieve + * @param metadata optional additional metadata */ public record GetTaskPushNotificationConfigParams(String id, @Nullable String pushNotificationConfigId, @Nullable Map metadata) { diff --git a/spec/src/main/java/io/a2a/spec/ImplicitOAuthFlow.java b/spec/src/main/java/io/a2a/spec/ImplicitOAuthFlow.java index cd2ef6235..ec76ab318 100644 --- a/spec/src/main/java/io/a2a/spec/ImplicitOAuthFlow.java +++ b/spec/src/main/java/io/a2a/spec/ImplicitOAuthFlow.java @@ -7,6 +7,10 @@ /** * Defines configuration details for the OAuth 2.0 Implicit flow. + * + * @param authorizationUrl the URL for the authorization endpoint + * @param refreshUrl optional URL for obtaining refresh tokens + * @param scopes the available scopes mapped to their descriptions */ public record ImplicitOAuthFlow(String authorizationUrl, String refreshUrl, Map scopes) { diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java b/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java index 45e2b6883..69683a764 100644 --- a/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java +++ b/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java @@ -6,6 +6,8 @@ /** * Represents a JSONRPC request. + * + * @param the type of the request parameters */ public abstract sealed class JSONRPCRequest implements JSONRPCMessage permits NonStreamingJSONRPCRequest, StreamingJSONRPCRequest { diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java b/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java index d4330d843..395483596 100644 --- a/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java +++ b/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java @@ -6,6 +6,8 @@ /** * Represents a JSONRPC response. + * + * @param the type of the response result */ public abstract sealed class JSONRPCResponse implements JSONRPCMessage permits SendStreamingMessageResponse, GetTaskResponse, CancelTaskResponse, SetTaskPushNotificationConfigResponse, GetTaskPushNotificationConfigResponse, diff --git a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java index 2d04f36ce..179e86172 100644 --- a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java +++ b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java @@ -6,6 +6,9 @@ /** * Parameters for getting list of pushNotificationConfigurations associated with a Task. + * + * @param id the task ID + * @param metadata optional additional metadata */ public record ListTaskPushNotificationConfigParams(String id, Map metadata) { diff --git a/spec/src/main/java/io/a2a/spec/Message.java b/spec/src/main/java/io/a2a/spec/Message.java index 9d08cb56c..050e44c53 100644 --- a/spec/src/main/java/io/a2a/spec/Message.java +++ b/spec/src/main/java/io/a2a/spec/Message.java @@ -55,6 +55,19 @@ public Message(Role role, List> parts, this.kind = kind; } + public void check() { + Assert.checkNotNullParam("kind", kind); + Assert.checkNotNullParam("parts", parts); + if (parts.isEmpty()) { + throw new IllegalArgumentException("Parts cannot be empty"); + } + Assert.checkNotNullParam("role", role); + if (!kind.equals(MESSAGE)) { + throw new IllegalArgumentException("Invalid Message"); + } + Assert.checkNotNullParam("messageId", messageId); + } + public Role getRole() { return role; } diff --git a/spec/src/main/java/io/a2a/spec/MessageSendConfiguration.java b/spec/src/main/java/io/a2a/spec/MessageSendConfiguration.java index d44ce494f..cd4888ff4 100644 --- a/spec/src/main/java/io/a2a/spec/MessageSendConfiguration.java +++ b/spec/src/main/java/io/a2a/spec/MessageSendConfiguration.java @@ -6,7 +6,12 @@ import org.jspecify.annotations.Nullable; /** - * Defines configuration options for a `message/send` or `message/stream` request. + * Defines configuration options for a {@code message/send} or {@code message/stream} request. + * + * @param acceptedOutputModes the output content modes the client accepts + * @param historyLength optional maximum number of history messages to include + * @param pushNotificationConfig optional push notification configuration for task updates + * @param blocking whether the request should block until the task completes */ public record MessageSendConfiguration(List acceptedOutputModes, Integer historyLength, PushNotificationConfig pushNotificationConfig, Boolean blocking) { diff --git a/spec/src/main/java/io/a2a/spec/MessageSendParams.java b/spec/src/main/java/io/a2a/spec/MessageSendParams.java index 5914ef462..de7e4bfc7 100644 --- a/spec/src/main/java/io/a2a/spec/MessageSendParams.java +++ b/spec/src/main/java/io/a2a/spec/MessageSendParams.java @@ -1,5 +1,6 @@ package io.a2a.spec; + import java.util.Map; import io.a2a.util.Assert; @@ -7,6 +8,10 @@ /** * Defines the parameters for a request to send a message to an agent. This can be used * to create a new task, continue an existing one, or restart a task. + * + * @param message the message to send to the agent + * @param configuration optional configuration options for this send request + * @param metadata optional additional metadata */ public record MessageSendParams(Message message, MessageSendConfiguration configuration, Map metadata) { @@ -15,6 +20,11 @@ public record MessageSendParams(Message message, MessageSendConfiguration config Assert.checkNotNullParam("message", message); } + public void check() { + Assert.checkNotNullParam("message", message); + message.check(); + } + public static class Builder { Message message; MessageSendConfiguration configuration; diff --git a/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java b/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java index e969ce08e..66562662d 100644 --- a/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java +++ b/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java @@ -2,6 +2,8 @@ /** * Represents a non-streaming JSON-RPC request. + * + * @param the type of the request parameters */ public abstract sealed class NonStreamingJSONRPCRequest extends JSONRPCRequest permits GetTaskRequest, CancelTaskRequest, SetTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, diff --git a/spec/src/main/java/io/a2a/spec/OAuthFlows.java b/spec/src/main/java/io/a2a/spec/OAuthFlows.java index 849f84d41..31312b41b 100644 --- a/spec/src/main/java/io/a2a/spec/OAuthFlows.java +++ b/spec/src/main/java/io/a2a/spec/OAuthFlows.java @@ -2,6 +2,11 @@ /** * Defines the configuration for the supported OAuth 2.0 flows. + * + * @param authorizationCode configuration for the Authorization Code flow + * @param clientCredentials configuration for the Client Credentials flow + * @param implicit configuration for the Implicit flow + * @param password configuration for the Resource Owner Password flow */ public record OAuthFlows(AuthorizationCodeOAuthFlow authorizationCode, ClientCredentialsOAuthFlow clientCredentials, ImplicitOAuthFlow implicit, PasswordOAuthFlow password) { diff --git a/spec/src/main/java/io/a2a/spec/PasswordOAuthFlow.java b/spec/src/main/java/io/a2a/spec/PasswordOAuthFlow.java index e5de924cb..58d4b81dd 100644 --- a/spec/src/main/java/io/a2a/spec/PasswordOAuthFlow.java +++ b/spec/src/main/java/io/a2a/spec/PasswordOAuthFlow.java @@ -6,6 +6,10 @@ /** * Defines configuration details for the OAuth 2.0 Resource Owner Password flow. + * + * @param refreshUrl optional URL for obtaining refresh tokens + * @param scopes the available scopes mapped to their descriptions + * @param tokenUrl the URL for the token endpoint */ public record PasswordOAuthFlow(String refreshUrl, Map scopes, String tokenUrl) { diff --git a/spec/src/main/java/io/a2a/spec/PushNotificationAuthenticationInfo.java b/spec/src/main/java/io/a2a/spec/PushNotificationAuthenticationInfo.java index 6263ac990..66d01e523 100644 --- a/spec/src/main/java/io/a2a/spec/PushNotificationAuthenticationInfo.java +++ b/spec/src/main/java/io/a2a/spec/PushNotificationAuthenticationInfo.java @@ -5,6 +5,9 @@ /** * Defines authentication details for a push notification endpoint. + * + * @param schemes the list of authentication scheme identifiers + * @param credentials optional credentials string for the authentication scheme */ public record PushNotificationAuthenticationInfo(List schemes, String credentials) { diff --git a/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java b/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java index b5a9e1131..18eda5f08 100644 --- a/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java +++ b/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java @@ -4,6 +4,11 @@ /** * Defines the configuration for setting up push notifications for task updates. + * + * @param url the URL of the push notification endpoint + * @param token optional authentication token for the push notification endpoint + * @param authentication optional authentication details for the push notification endpoint + * @param id optional identifier for this push notification configuration */ public record PushNotificationConfig(String url, String token, PushNotificationAuthenticationInfo authentication, String id) { diff --git a/spec/src/main/java/io/a2a/spec/SendMessageRequest.java b/spec/src/main/java/io/a2a/spec/SendMessageRequest.java index a58ce0890..a8ec457e5 100644 --- a/spec/src/main/java/io/a2a/spec/SendMessageRequest.java +++ b/spec/src/main/java/io/a2a/spec/SendMessageRequest.java @@ -44,6 +44,22 @@ public SendMessageRequest(String jsonrpc, Object id, String method, MessageSendP this.params = params; } + public void check() { + if (jsonrpc == null || jsonrpc.isEmpty()) { + throw new IllegalArgumentException("JSON-RPC protocol version cannot be null or empty"); + } + if (jsonrpc != null && !jsonrpc.equals(JSONRPC_VERSION)) { + throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); + } + Assert.checkNotNullParam("method", method); + if (!method.equals(METHOD)) { + throw new IllegalArgumentException("Invalid SendMessageRequest method"); + } + Assert.checkNotNullParam("params", params); + Assert.isNullOrStringOrInteger(id); + params.check(); + } + public SendMessageRequest(Object id, MessageSendParams params) { this(JSONRPC_VERSION, id, METHOD, params); } diff --git a/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java b/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java index de3abf950..d0eba2dbc 100644 --- a/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java +++ b/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java @@ -1,5 +1,6 @@ package io.a2a.spec; +import static io.a2a.spec.JSONRPCMessage.JSONRPC_VERSION; import static io.a2a.util.Utils.defaultIfNull; import io.a2a.util.Assert; @@ -33,6 +34,19 @@ public SendStreamingMessageRequest(Object id, MessageSendParams params) { this(null, id, METHOD, params); } + public void check() { + if (jsonrpc != null && !jsonrpc.equals(JSONRPC_VERSION)) { + throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); + } + Assert.checkNotNullParam("method", method); + if (!method.equals(METHOD)) { + throw new IllegalArgumentException("Invalid SendStreamingMessageRequest method"); + } + Assert.checkNotNullParam("params", params); + Assert.isNullOrStringOrInteger(id); + params.check(); + } + public static class Builder { private String jsonrpc; private Object id; diff --git a/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequest.java b/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequest.java index 9cbb5e4c6..e0b2a6255 100644 --- a/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequest.java +++ b/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequest.java @@ -2,8 +2,9 @@ /** * Represents a streaming JSON-RPC request. + * + * @param the type of the request parameters */ - public abstract sealed class StreamingJSONRPCRequest extends JSONRPCRequest permits TaskResubscriptionRequest, SendStreamingMessageRequest { diff --git a/spec/src/main/java/io/a2a/spec/TaskIdParams.java b/spec/src/main/java/io/a2a/spec/TaskIdParams.java index 096c9a8a0..7a9e9b159 100644 --- a/spec/src/main/java/io/a2a/spec/TaskIdParams.java +++ b/spec/src/main/java/io/a2a/spec/TaskIdParams.java @@ -6,6 +6,9 @@ /** * Defines parameters containing a task ID, used for simple task operations. + * + * @param id the task ID + * @param metadata optional additional metadata */ public record TaskIdParams(String id, Map metadata) { diff --git a/spec/src/main/java/io/a2a/spec/TaskPushNotificationConfig.java b/spec/src/main/java/io/a2a/spec/TaskPushNotificationConfig.java index 23a7fc0c4..5dfdede6c 100644 --- a/spec/src/main/java/io/a2a/spec/TaskPushNotificationConfig.java +++ b/spec/src/main/java/io/a2a/spec/TaskPushNotificationConfig.java @@ -4,6 +4,9 @@ /** * A container associating a push notification configuration with a specific task. + * + * @param taskId the ID of the task this configuration is associated with + * @param pushNotificationConfig the push notification configuration for the task */ public record TaskPushNotificationConfig(String taskId, PushNotificationConfig pushNotificationConfig) { diff --git a/spec/src/main/java/io/a2a/util/Utils.java b/spec/src/main/java/io/a2a/util/Utils.java index f374f302e..004b1cdc4 100644 --- a/spec/src/main/java/io/a2a/util/Utils.java +++ b/spec/src/main/java/io/a2a/util/Utils.java @@ -24,13 +24,12 @@ *

* Key capabilities: *

    - *
  • JSON processing with pre-configured {@link Gson}
  • + *
  • JSON processing with pre-configured {@link io.a2a.json.JsonUtil#OBJECT_MAPPER}
  • *
  • Null-safe value defaults via {@link #defaultIfNull(Object, Object)}
  • *
  • Artifact streaming support via {@link #appendArtifactToTask(Task, TaskArtifactUpdateEvent, String)}
  • *
  • Type-safe exception rethrowing via {@link #rethrow(Throwable)}
  • *
* - * @see Gson for JSON processing * @see TaskArtifactUpdateEvent for streaming artifact updates */ public class Utils { @@ -41,7 +40,7 @@ public class Utils { /** * Deserializes JSON string into a typed object using Gson. *

- * This method uses the pre-configured {@link #OBJECT_MAPPER} to parse JSON. + * This method uses the pre-configured {@link io.a2a.json.JsonUtil#OBJECT_MAPPER} to parse JSON. * * @param the target type * @param data JSON string to deserialize diff --git a/spec/src/test/java/io/a2a/spec/JSONRPCErrorSerializationTest.java b/spec/src/test/java/io/a2a/spec/JSONRPCErrorSerializationTest.java index 9c83f0806..e95be5a5a 100644 --- a/spec/src/test/java/io/a2a/spec/JSONRPCErrorSerializationTest.java +++ b/spec/src/test/java/io/a2a/spec/JSONRPCErrorSerializationTest.java @@ -3,9 +3,12 @@ import org.junit.jupiter.api.Test; import java.util.List; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; import io.a2a.json.JsonProcessingException; import io.a2a.json.JsonUtil; @@ -44,5 +47,36 @@ record ErrorCase(int code, Class clazz) {} } } + @Test + @SuppressWarnings("unchecked") + void deleteTaskPushNotificationConfigSuccessResponseSerializesResultAsNull() throws JsonProcessingException { + DeleteTaskPushNotificationConfigResponse response = + new DeleteTaskPushNotificationConfigResponse("req-123"); + + String json = JsonUtil.toJson(response); + Map map = JsonUtil.fromJson(json, Map.class); + + assertEquals("2.0", map.get("jsonrpc")); + assertEquals("req-123", map.get("id")); + assertTrue(map.containsKey("result"), "result field must be present in success response"); + assertEquals(null, map.get("result"), "result must be null for delete response"); + assertFalse(map.containsKey("error"), "error field must not be present in success response"); + } + + @Test + @SuppressWarnings("unchecked") + void deleteTaskPushNotificationConfigErrorResponseSerializesErrorWithoutResult() throws JsonProcessingException { + DeleteTaskPushNotificationConfigResponse response = + new DeleteTaskPushNotificationConfigResponse("req-456", new TaskNotFoundError()); + + String json = JsonUtil.toJson(response); + Map map = JsonUtil.fromJson(json, Map.class); + + assertEquals("2.0", map.get("jsonrpc")); + assertEquals("req-456", map.get("id")); + assertTrue(map.containsKey("error"), "error field must be present in error response"); + assertFalse(map.containsKey("result"), "result field must not be present in error response"); + } + } diff --git a/tck/src/main/java/io/a2a/tck/server/AgentExecutorProducer.java b/tck/src/main/java/io/a2a/tck/server/AgentExecutorProducer.java index 5c085f981..560c4d7f2 100644 --- a/tck/src/main/java/io/a2a/tck/server/AgentExecutorProducer.java +++ b/tck/src/main/java/io/a2a/tck/server/AgentExecutorProducer.java @@ -39,7 +39,7 @@ public void execute(RequestContext context, EventQueue eventQueue) throws JSONRP } // Sleep to allow task state persistence before TCK resubscribe test - if (context.getMessage().getMessageId().startsWith("test-resubscribe-message-id")) { + if (context.getMessage().getMessageId() != null && context.getMessage().getMessageId().startsWith("test-resubscribe-message-id")) { int timeoutMs = Integer.parseInt(System.getenv().getOrDefault("RESUBSCRIBE_TIMEOUT_MS", "3000")); System.out.println("====> task id starts with test-resubscribe-message-id, sleeping for " + timeoutMs + " ms"); try { diff --git a/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandler.java b/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandler.java index ef577aaa5..853d6978b 100644 --- a/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandler.java +++ b/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandler.java @@ -1,6 +1,7 @@ package io.a2a.transport.jsonrpc.handler; import static io.a2a.server.util.async.AsyncUtils.createTubeConfig; + import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; @@ -46,6 +47,7 @@ import io.a2a.spec.TaskPushNotificationConfig; import io.a2a.spec.TaskResubscriptionRequest; import io.a2a.server.util.async.Internal; +import io.a2a.spec.InvalidParamsError; import mutiny.zero.ZeroPublisher; @ApplicationScoped @@ -78,11 +80,14 @@ public JSONRPCHandler(@PublicAgentCard AgentCard agentCard, RequestHandler reque public SendMessageResponse onMessageSend(SendMessageRequest request, ServerCallContext context) { try { + request.check(); EventKind taskOrMessage = requestHandler.onMessageSend(request.getParams(), context); return new SendMessageResponse(request.getId(), taskOrMessage); } catch (JSONRPCError e) { return new SendMessageResponse(request.getId(), e); - } catch (Throwable t) { + } catch (IllegalArgumentException t) { + return new SendMessageResponse(request.getId(), new InvalidParamsError(t.getMessage())); + }catch (Throwable t) { return new SendMessageResponse(request.getId(), new InternalError(t.getMessage())); } } @@ -96,8 +101,8 @@ public Flow.Publisher onMessageSendStream( request.getId(), new InvalidRequestError("Streaming is not supported by the agent"))); } - try { + request.check(); Flow.Publisher publisher = requestHandler.onMessageSendStream(request.getParams(), context); // We can't use the convertingProcessor convenience method since that propagates any errors as an error handled From eaee8da59b0aff210b6411801380c01876f0a4f0 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Wed, 22 Apr 2026 15:22:46 +0100 Subject: [PATCH 26/37] fix: Security hardening, JSON serialization correctness, and spec compliance This commit addresses critical review feedback across security, data correctness, thread safety, and A2A spec compliance: **Security (CWE-470):** - Add whitelist validation before Class.forName() in ThrowableTypeAdapter to prevent arbitrary class loading vulnerability. Only allow java.lang.*, java.io.*, and io.a2a.* packages plus specific safe exception types. **Data Correctness:** - Fix JSON-RPC ID truncation: use getAsLong() instead of getAsInt() to handle IDs exceeding Integer.MAX_VALUE - Fix error data serialization: use GSON.toJson() instead of toString() to preserve JSON structure in error responses - Use shared JsonUtil.OBJECT_MAPPER in JSONRPCUtils for consistent type adapter support (OffsetDateTime, etc.) across all serialization paths **Thread Safety:** - Implement double-checked locking in getAgentCard() methods across Client, JSONRPCTransport, and RestTransport to prevent race conditions during lazy initialization - Declare agentCard and needsExtendedCard fields as volatile to ensure visibility across threads and prevent instruction reordering in double-checked locking pattern **A2A Spec Compliance:** - Fix intermittent null field serialization: JSON-RPC server now uses JsonUtil.toJson() instead of returning AgentCard object (which caused Quarkus to use Jackson, serializing nulls as "field": null instead of omitting them) - Remove leading slash from pushNotificationConfig resource name pattern to match REST API conventions **Code Quality:** - Improve error messages in JdkA2AHttpClient streaming handler (body not available in streaming context) - Add verification tests for AgentCard null field omission - Fix javadoc reference to use JsonUtil.OBJECT_MAPPER instead of Gson Resolves intermittent TCK failures related to null field serialization. Co-Authored-By: Claude Sonnet 4.5 --- .../src/main/java/io/a2a/client/Client.java | 16 ++- .../transport/jsonrpc/JSONRPCTransport.java | 69 +++++----- .../client/transport/rest/RestTransport.java | 73 ++++++----- common/src/main/java/io/a2a/util/Assert.java | 12 +- .../io/a2a/client/http/JdkA2AHttpClient.java | 8 +- .../server/apps/quarkus/A2AServerRoutes.java | 9 +- .../java/io/a2a/grpc/utils/JSONRPCUtils.java | 23 ++-- .../io/a2a/grpc/utils/JSONRPCUtilsTest.java | 16 +-- spec/src/main/java/io/a2a/json/JsonUtil.java | 25 ++++ .../java/io/a2a/spec/CancelTaskRequest.java | 2 +- ...leteTaskPushNotificationConfigRequest.java | 2 +- .../GetAuthenticatedExtendedCardRequest.java | 2 +- .../GetTaskPushNotificationConfigRequest.java | 2 +- .../main/java/io/a2a/spec/GetTaskRequest.java | 2 +- .../main/java/io/a2a/spec/JSONRPCRequest.java | 2 +- .../java/io/a2a/spec/JSONRPCResponse.java | 2 +- ...ListTaskPushNotificationConfigRequest.java | 2 +- .../java/io/a2a/spec/SendMessageRequest.java | 4 +- .../a2a/spec/SendStreamingMessageRequest.java | 4 +- .../SetTaskPushNotificationConfigRequest.java | 2 +- .../json/JsonUtilNullSerializationTest.java | 124 ++++++++++++++++++ 21 files changed, 293 insertions(+), 108 deletions(-) create mode 100644 spec/src/test/java/io/a2a/json/JsonUtilNullSerializationTest.java diff --git a/client/base/src/main/java/io/a2a/client/Client.java b/client/base/src/main/java/io/a2a/client/Client.java index ab2222667..2bb28aff5 100644 --- a/client/base/src/main/java/io/a2a/client/Client.java +++ b/client/base/src/main/java/io/a2a/client/Client.java @@ -37,7 +37,7 @@ public class Client extends AbstractClient { private final ClientConfig clientConfig; private final ClientTransport clientTransport; - private AgentCard agentCard; + private volatile AgentCard agentCard; Client(AgentCard agentCard, ClientConfig clientConfig, ClientTransport clientTransport, List> consumers, @Nullable Consumer streamingErrorHandler) { @@ -127,8 +127,18 @@ public void resubscribe(TaskIdParams request, @Nullable List interceptors; - private AgentCard agentCard; - private boolean needsExtendedCard = false; + private volatile AgentCard agentCard; + private volatile boolean needsExtendedCard = false; public JSONRPCTransport(String agentUrl) { this(null, null, agentUrl, null); @@ -332,37 +332,46 @@ public void resubscribe(TaskIdParams request, Consumer event @Override public AgentCard getAgentCard(ClientCallContext context) throws A2AClientException { - A2ACardResolver resolver; - try { - if (agentCard == null) { - resolver = new A2ACardResolver(httpClient, agentUrl, null, getHttpHeaders(context)); - agentCard = resolver.getAgentCard(); - needsExtendedCard = agentCard.supportsAuthenticatedExtendedCard(); - } - if (!needsExtendedCard) { - return agentCard; - } - - GetAuthenticatedExtendedCardRequest getExtendedAgentCardRequest = new GetAuthenticatedExtendedCardRequest.Builder() - .jsonrpc(JSONRPCMessage.JSONRPC_VERSION) - .method(GetAuthenticatedExtendedCardRequest.METHOD) - .build(); // id will be randomly generated - - PayloadAndHeaders payloadAndHeaders = applyInterceptors(GetAuthenticatedExtendedCardRequest.METHOD, - getExtendedAgentCardRequest, agentCard, context); + // Fast path - avoid synchronization if already initialized + if (agentCard != null && !needsExtendedCard) { + return agentCard; + } + synchronized (this) { + // Double-check inside synchronized block + A2ACardResolver resolver; try { - String httpResponseBody = sendPostRequest(payloadAndHeaders); - GetAuthenticatedExtendedCardResponse response = unmarshalResponse(httpResponseBody, - GET_AUTHENTICATED_EXTENDED_CARD_RESPONSE_REFERENCE); - agentCard = response.getResult(); - needsExtendedCard = false; - return agentCard; - } catch (IOException | InterruptedException | JsonProcessingException e) { - throw new A2AClientException("Failed to get authenticated extended agent card: " + e, e); + if (agentCard == null) { + resolver = new A2ACardResolver(httpClient, agentUrl, null, getHttpHeaders(context)); + agentCard = resolver.getAgentCard(); + needsExtendedCard = agentCard.supportsAuthenticatedExtendedCard(); + } + if (!needsExtendedCard) { + return agentCard; + } + + // Extended card fetch logic remains inside synchronized block + GetAuthenticatedExtendedCardRequest getExtendedAgentCardRequest = new GetAuthenticatedExtendedCardRequest.Builder() + .jsonrpc(JSONRPCMessage.JSONRPC_VERSION) + .method(GetAuthenticatedExtendedCardRequest.METHOD) + .build(); // id will be randomly generated + + PayloadAndHeaders payloadAndHeaders = applyInterceptors(GetAuthenticatedExtendedCardRequest.METHOD, + getExtendedAgentCardRequest, agentCard, context); + + try { + String httpResponseBody = sendPostRequest(payloadAndHeaders); + GetAuthenticatedExtendedCardResponse response = unmarshalResponse(httpResponseBody, + GET_AUTHENTICATED_EXTENDED_CARD_RESPONSE_REFERENCE); + agentCard = response.getResult(); + needsExtendedCard = false; + return agentCard; + } catch (IOException | InterruptedException | JsonProcessingException e) { + throw new A2AClientException("Failed to get authenticated extended agent card: " + e, e); + } + } catch(A2AClientError e){ + throw new A2AClientException("Failed to get agent card: " + e, e); } - } catch(A2AClientError e){ - throw new A2AClientException("Failed to get agent card: " + e, e); } } diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java index af2df8df2..94fbdb5df 100644 --- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java +++ b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java @@ -54,8 +54,8 @@ public class RestTransport implements ClientTransport { private final A2AHttpClient httpClient; private final String agentUrl; private @Nullable final List interceptors; - private AgentCard agentCard; - private boolean needsExtendedCard = false; + private volatile AgentCard agentCard; + private volatile boolean needsExtendedCard = false; public RestTransport(AgentCard agentCard) { this(null, agentCard, agentCard.url(), null); @@ -199,7 +199,7 @@ public TaskPushNotificationConfig setTaskPushNotificationConfiguration(TaskPushN public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) throws A2AClientException { checkNotNullParam("request", request); GetTaskPushNotificationConfigRequest.Builder builder = GetTaskPushNotificationConfigRequest.newBuilder(); - builder.setName(String.format("/tasks/%1s/pushNotificationConfigs/%2s", request.id(), request.pushNotificationConfigId())); + builder.setName(String.format("tasks/%1s/pushNotificationConfigs/%2s", request.id(), request.pushNotificationConfigId())); PayloadAndHeaders payloadAndHeaders = applyInterceptors(io.a2a.spec.GetTaskPushNotificationConfigRequest.METHOD, builder, agentCard, context); try { @@ -310,37 +310,46 @@ public void resubscribe(TaskIdParams request, Consumer event @Override public AgentCard getAgentCard(@Nullable ClientCallContext context) throws A2AClientException { - A2ACardResolver resolver; - try { - if (agentCard == null) { - resolver = new A2ACardResolver(httpClient, agentUrl, null, getHttpHeaders(context)); - agentCard = resolver.getAgentCard(); - needsExtendedCard = agentCard.supportsAuthenticatedExtendedCard(); - } - if (!needsExtendedCard) { - return agentCard; - } - PayloadAndHeaders payloadAndHeaders = applyInterceptors(io.a2a.spec.GetTaskRequest.METHOD, null, - agentCard, context); - String url = agentUrl + String.format("/v1/card"); - A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(url); - if (payloadAndHeaders.getHeaders() != null) { - for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) { - getBuilder.addHeader(entry.getKey(), entry.getValue()); + // Fast path - avoid synchronization if already initialized + if (agentCard != null && !needsExtendedCard) { + return agentCard; + } + + synchronized (this) { + // Double-check inside synchronized block + A2ACardResolver resolver; + try { + if (agentCard == null) { + resolver = new A2ACardResolver(httpClient, agentUrl, null, getHttpHeaders(context)); + agentCard = resolver.getAgentCard(); + needsExtendedCard = agentCard.supportsAuthenticatedExtendedCard(); } + if (!needsExtendedCard) { + return agentCard; + } + // Extended card fetch logic remains inside synchronized block + PayloadAndHeaders payloadAndHeaders = applyInterceptors(io.a2a.spec.GetTaskRequest.METHOD, null, + agentCard, context); + String url = agentUrl + String.format("/v1/card"); + A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(url); + if (payloadAndHeaders.getHeaders() != null) { + for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) { + getBuilder.addHeader(entry.getKey(), entry.getValue()); + } + } + A2AHttpResponse response = getBuilder.get(); + if (!response.success()) { + throw RestErrorMapper.mapRestError(response); + } + String httpResponseBody = response.body(); + agentCard = JsonUtil.fromJson(httpResponseBody, AgentCard.class); + needsExtendedCard = false; + return agentCard; + } catch (IOException | InterruptedException | JsonProcessingException e) { + throw new A2AClientException("Failed to get authenticated extended agent card: " + e, e); + } catch (A2AClientError e) { + throw new A2AClientException("Failed to get agent card: " + e, e); } - A2AHttpResponse response = getBuilder.get(); - if (!response.success()) { - throw RestErrorMapper.mapRestError(response); - } - String httpResponseBody = response.body(); - agentCard = JsonUtil.fromJson(httpResponseBody, AgentCard.class); - needsExtendedCard = false; - return agentCard; - } catch (IOException | InterruptedException | JsonProcessingException e) { - throw new A2AClientException("Failed to get authenticated extended agent card: " + e, e); - } catch (A2AClientError e) { - throw new A2AClientException("Failed to get agent card: " + e, e); } } diff --git a/common/src/main/java/io/a2a/util/Assert.java b/common/src/main/java/io/a2a/util/Assert.java index b0077cd23..de611c6c2 100644 --- a/common/src/main/java/io/a2a/util/Assert.java +++ b/common/src/main/java/io/a2a/util/Assert.java @@ -22,9 +22,15 @@ private static void checkNotNullParamChecked(final String name, final T valu if (value == null) throw new IllegalArgumentException("Parameter '" + name + "' may not be null"); } - public static void isNullOrStringOrInteger(Object value) { - if (! (value == null || value instanceof String || value instanceof Integer)) { - throw new IllegalArgumentException("Id must be null, a String, or an Integer"); + /** + * Validates that a value is a valid JSON-RPC ID (null, String, Integer, or Long). + * + * @param value the value to validate + * @throws IllegalArgumentException if the value is not a valid JSON-RPC ID type + */ + public static void isValidJsonRpcId(Object value) { + if (! (value == null || value instanceof String || value instanceof Integer || value instanceof Long)) { + throw new IllegalArgumentException("Id must be null, a String, an Integer, or a Long"); } } diff --git a/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClient.java b/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClient.java index 9b8003741..cd7177050 100644 --- a/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClient.java +++ b/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClient.java @@ -185,10 +185,12 @@ public void onComplete() { return httpClient.sendAsync(request, bodyHandler) .thenAccept(response -> { // Handle non-authentication/non-authorization errors here - if (!isSuccessStatus(response.statusCode()) && - response.statusCode() != HTTP_UNAUTHORIZED && + if (!isSuccessStatus(response.statusCode()) && + response.statusCode() != HTTP_UNAUTHORIZED && response.statusCode() != HTTP_FORBIDDEN) { - subscriber.onError(new IOException("Request failed with status " + response.statusCode() + ":" + response.body())); + // Note: body is not available here as it's being streamed to the subscriber + subscriber.onError(new IOException("Request failed with status " + + response.statusCode())); } }); } diff --git a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java index 1f14a61ae..aa1329ea4 100644 --- a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java +++ b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java @@ -172,8 +172,13 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) { * @return the agent card */ @Route(path = "/.well-known/agent-card.json", methods = Route.HttpMethod.GET, produces = APPLICATION_JSON) - public AgentCard getAgentCard() { - return jsonRpcHandler.getAgentCard(); + public String getAgentCard() { + try { + return JsonUtil.toJson(jsonRpcHandler.getAgentCard()); + } catch (Exception e) { + // This should never happen with a valid AgentCard, but handle it just in case + throw new RuntimeException("Failed to serialize agent card: " + e.getMessage(), e); + } } private NonStreamingJSONRPCRequest deserializeNonStreamingRequest(String body, String methodName) { diff --git a/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java b/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java index 5014608a7..2780052ed 100644 --- a/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java +++ b/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java @@ -2,12 +2,9 @@ import io.a2a.json.JsonMappingException; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import com.google.gson.Strictness; import com.google.gson.stream.JsonWriter; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.util.JsonFormat; @@ -130,7 +127,7 @@ * *

Thread Safety

* This class is thread-safe. All methods are stateless and use immutable shared resources - * ({@link Gson} instance is thread-safe, proto builders are created per-invocation). + * ({@link io.a2a.json.JsonUtil#OBJECT_MAPPER} Gson instance is thread-safe, proto builders are created per-invocation). * *

Usage Example

*
{@code
@@ -157,9 +154,6 @@
 public class JSONRPCUtils {
 
     private static final Logger log = Logger.getLogger(JSONRPCUtils.class.getName());
-    private static final Gson GSON = new GsonBuilder()
-            .setStrictness(Strictness.STRICT)
-            .create();
 
     public static JSONRPCRequest parseRequestBody(String body) throws JsonMappingException {
         JsonElement jelement = JsonParser.parseString(body);
@@ -298,7 +292,7 @@ public static JSONRPCResponse parseResponseBody(String body, String method) t
             }
             case GetAuthenticatedExtendedCardRequest.METHOD -> {
                 try {
-                    AgentCard card = JsonUtil.fromJson(GSON.toJson(paramsNode), AgentCard.class);
+                    AgentCard card = JsonUtil.fromJson(JsonUtil.OBJECT_MAPPER.toJson(paramsNode), AgentCard.class);
                     return new GetAuthenticatedExtendedCardResponse(id, card);
                 } catch (JsonProcessingException e) {
                     throw new InvalidParamsError("Failed to parse agent card response: " + e.getMessage());
@@ -375,7 +369,7 @@ private static JSONRPCError processError(JsonObject error) {
 
     protected static void parseRequestBody(JsonElement jsonRpc, com.google.protobuf.Message.Builder builder) throws JSONRPCError {
         try (Writer writer = new StringWriter()) {
-            GSON.toJson(jsonRpc, writer);
+            JsonUtil.OBJECT_MAPPER.toJson(jsonRpc, writer);
             parseJsonString(writer.toString(), builder);
         } catch (IOException e) {
             log.log(Level.SEVERE, "Failed to serialize JSON element to string during proto conversion. JSON: {0}", jsonRpc);
@@ -454,7 +448,7 @@ protected static Object getAndValidateId(JsonObject jsonRpc) throws JsonMappingE
         if (jsonRpc.has("id")) {
             if (jsonRpc.get("id").isJsonPrimitive()) {
                 try {
-                    id = jsonRpc.get("id").getAsInt();
+                    id = jsonRpc.get("id").getAsLong();
                 } catch (UnsupportedOperationException | NumberFormatException | IllegalStateException e) {
                     id = jsonRpc.get("id").getAsString();
                 }
@@ -470,7 +464,7 @@ protected static Object getAndValidateId(JsonObject jsonRpc) throws JsonMappingE
     }
 
     public static String toJsonRPCRequest(@Nullable String requestId, String method, com.google.protobuf.@Nullable MessageOrBuilder payload) {
-        try (StringWriter result = new StringWriter(); JsonWriter output = GSON.newJsonWriter(result)) {
+        try (StringWriter result = new StringWriter(); JsonWriter output = JsonUtil.OBJECT_MAPPER.newJsonWriter(result)) {
             output.beginObject();
             output.name("jsonrpc").value("2.0");
             String id = requestId;
@@ -495,7 +489,7 @@ public static String toJsonRPCRequest(@Nullable String requestId, String method,
     }
 
     public static String toJsonRPCResultResponse(Object requestId, com.google.protobuf.MessageOrBuilder builder) {
-        try (StringWriter result = new StringWriter(); JsonWriter output = GSON.newJsonWriter(result)) {
+        try (StringWriter result = new StringWriter(); JsonWriter output = JsonUtil.OBJECT_MAPPER.newJsonWriter(result)) {
             output.beginObject();
             output.name("jsonrpc").value("2.0");
             if (requestId != null) {
@@ -517,7 +511,7 @@ public static String toJsonRPCResultResponse(Object requestId, com.google.protob
     }
 
     public static String toJsonRPCErrorResponse(Object requestId, JSONRPCError error) {
-        try (StringWriter result = new StringWriter(); JsonWriter output = GSON.newJsonWriter(result)) {
+        try (StringWriter result = new StringWriter(); JsonWriter output = JsonUtil.OBJECT_MAPPER.newJsonWriter(result)) {
             output.beginObject();
             output.name("jsonrpc").value("2.0");
             if (requestId != null) {
@@ -532,7 +526,8 @@ public static String toJsonRPCErrorResponse(Object requestId, JSONRPCError error
             output.name("code").value(error.getCode());
             output.name("message").value(error.getMessage());
             if (error.getData() != null) {
-                output.name("data").value(error.getData().toString());
+                output.name("data");
+                JsonUtil.OBJECT_MAPPER.toJson(error.getData(), Object.class, output);
             }
             output.endObject();
             output.endObject();
diff --git a/spec-grpc/src/test/java/io/a2a/grpc/utils/JSONRPCUtilsTest.java b/spec-grpc/src/test/java/io/a2a/grpc/utils/JSONRPCUtilsTest.java
index 0482f5ab3..accc66fbf 100644
--- a/spec-grpc/src/test/java/io/a2a/grpc/utils/JSONRPCUtilsTest.java
+++ b/spec-grpc/src/test/java/io/a2a/grpc/utils/JSONRPCUtilsTest.java
@@ -49,7 +49,7 @@ public void testParseSetTaskPushNotificationConfigRequest_ValidProtoFormat() thr
         assertInstanceOf(SetTaskPushNotificationConfigRequest.class, request);
         SetTaskPushNotificationConfigRequest setRequest = (SetTaskPushNotificationConfigRequest) request;
         assertEquals("2.0", setRequest.getJsonrpc());
-        assertEquals(1, setRequest.getId());
+        assertEquals(1L, setRequest.getId());
         assertEquals(SetTaskPushNotificationConfigRequest.METHOD, setRequest.getMethod());
 
         TaskPushNotificationConfig config = setRequest.getParams();
@@ -78,7 +78,7 @@ public void testParseGetTaskPushNotificationConfigRequest_ValidProtoFormat() thr
         assertInstanceOf(GetTaskPushNotificationConfigRequest.class, request);
         GetTaskPushNotificationConfigRequest getRequest = (GetTaskPushNotificationConfigRequest) request;
         assertEquals("2.0", getRequest.getJsonrpc());
-        assertEquals(2, getRequest.getId());
+        assertEquals(2L, getRequest.getId());
         assertEquals(GetTaskPushNotificationConfigRequest.METHOD, getRequest.getMethod());
         assertNotNull(getRequest.getParams());
         assertEquals("task-123", getRequest.getParams().id());
@@ -114,7 +114,7 @@ public void testParseInvalidParams_ThrowsInvalidParamsError() {
             InvalidParamsJsonMappingException.class,
             () -> JSONRPCUtils.parseRequestBody(invalidParamsRequest)
         );
-        assertEquals(3, exception.getId());
+        assertEquals(3L, exception.getId());
     }
 
     @Test
@@ -134,7 +134,7 @@ public void testParseInvalidProtoStructure_ThrowsInvalidParamsError() {
             InvalidParamsJsonMappingException.class,
             () -> JSONRPCUtils.parseRequestBody(invalidStructure)
         );
-        assertEquals(4, exception.getId());
+        assertEquals(4L, exception.getId());
     }
 
     @Test
@@ -165,7 +165,7 @@ public void testGenerateSetTaskPushNotificationConfigResponse_Success() throws E
             (SetTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody(responseJson, SetTaskPushNotificationConfigRequest.METHOD);
 
         assertNotNull(response);
-        assertEquals(1, response.getId());
+        assertEquals(1L, response.getId());
         assertNotNull(response.getResult());
         assertEquals("task-123", response.getResult().taskId());
         assertEquals("https://example.com/callback", response.getResult().pushNotificationConfig().url());
@@ -191,7 +191,7 @@ public void testGenerateGetTaskPushNotificationConfigResponse_Success() throws E
             (GetTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody(responseJson, GetTaskPushNotificationConfigRequest.METHOD);
 
         assertNotNull(response);
-        assertEquals(2, response.getId());
+        assertEquals(2L, response.getId());
         assertNotNull(response.getResult());
         assertEquals("task-123", response.getResult().taskId());
         assertEquals("https://example.com/callback", response.getResult().pushNotificationConfig().url());
@@ -214,7 +214,7 @@ public void testParseErrorResponse_InvalidParams() throws Exception {
             (SetTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody(errorResponse, SetTaskPushNotificationConfigRequest.METHOD);
 
         assertNotNull(response);
-        assertEquals(5, response.getId());
+        assertEquals(5L, response.getId());
         assertNotNull(response.getError());
         assertInstanceOf(InvalidParamsError.class, response.getError());
         assertEquals(-32602, response.getError().getCode());
@@ -238,7 +238,7 @@ public void testParseErrorResponse_ParseError() throws Exception {
             (SetTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody(errorResponse, SetTaskPushNotificationConfigRequest.METHOD);
 
         assertNotNull(response);
-        assertEquals(6, response.getId());
+        assertEquals(6L, response.getId());
         assertNotNull(response.getError());
         assertInstanceOf(JSONParseError.class, response.getError());
         assertEquals(-32700, response.getError().getCode());
diff --git a/spec/src/main/java/io/a2a/json/JsonUtil.java b/spec/src/main/java/io/a2a/json/JsonUtil.java
index ab8a67f84..348297a01 100644
--- a/spec/src/main/java/io/a2a/json/JsonUtil.java
+++ b/spec/src/main/java/io/a2a/json/JsonUtil.java
@@ -195,6 +195,22 @@ OffsetDateTime read(JsonReader in) throws java.io.IOException {
      */
     static class ThrowableTypeAdapter extends TypeAdapter {
 
+        private static final java.util.Set ALLOWED_THROWABLE_PACKAGES = java.util.Set.of(
+            "java.lang.",
+            "java.io.",
+            "io.a2a."
+        );
+
+        private static final java.util.Set ALLOWED_THROWABLE_CLASSES = java.util.Set.of(
+            "java.lang.Exception",
+            "java.lang.RuntimeException",
+            "java.lang.IllegalArgumentException",
+            "java.lang.IllegalStateException",
+            "java.lang.NullPointerException",
+            "java.lang.UnsupportedOperationException",
+            "java.io.IOException"
+        );
+
         @Override
         public void write(JsonWriter out, Throwable value) throws java.io.IOException {
             if (value == null) {
@@ -235,6 +251,15 @@ Throwable read(JsonReader in) throws java.io.IOException {
 
             // Try to reconstruct the Throwable
             if (type != null) {
+                // Validate class name before loading to prevent CWE-470 vulnerability
+                boolean allowed = ALLOWED_THROWABLE_CLASSES.contains(type) ||
+                                 ALLOWED_THROWABLE_PACKAGES.stream().anyMatch(type::startsWith);
+
+                if (!allowed) {
+                    // Return generic RuntimeException for untrusted types
+                    return new RuntimeException("Error type '" + type + "': " + message);
+                }
+
                 try {
                     Class throwableClass = Class.forName(type);
                     if (Throwable.class.isAssignableFrom(throwableClass)) {
diff --git a/spec/src/main/java/io/a2a/spec/CancelTaskRequest.java b/spec/src/main/java/io/a2a/spec/CancelTaskRequest.java
index f8a1bb8bf..1e2a0bde6 100644
--- a/spec/src/main/java/io/a2a/spec/CancelTaskRequest.java
+++ b/spec/src/main/java/io/a2a/spec/CancelTaskRequest.java
@@ -23,7 +23,7 @@ public CancelTaskRequest(String jsonrpc, Object id, String method, TaskIdParams
             throw new IllegalArgumentException("Invalid CancelTaskRequest method");
         }
         Assert.checkNotNullParam("params", params);
-        Assert.isNullOrStringOrInteger(id);
+        Assert.isValidJsonRpcId(id);
         this.jsonrpc = defaultIfNull(jsonrpc, JSONRPC_VERSION);
         this.id = id;
         this.method = method;
diff --git a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigRequest.java b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigRequest.java
index dc3449ada..5966cddac 100644
--- a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigRequest.java
+++ b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigRequest.java
@@ -20,7 +20,7 @@ public DeleteTaskPushNotificationConfigRequest(String jsonrpc, Object id, String
         if (! method.equals(METHOD)) {
             throw new IllegalArgumentException("Invalid DeleteTaskPushNotificationConfigRequest method");
         }
-        Assert.isNullOrStringOrInteger(id);
+        Assert.isValidJsonRpcId(id);
         this.jsonrpc = Utils.defaultIfNull(jsonrpc, JSONRPC_VERSION);
         this.id = id;
         this.method = method;
diff --git a/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardRequest.java b/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardRequest.java
index 4afe2d029..6b5e3240d 100644
--- a/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardRequest.java
+++ b/spec/src/main/java/io/a2a/spec/GetAuthenticatedExtendedCardRequest.java
@@ -21,7 +21,7 @@ public GetAuthenticatedExtendedCardRequest(String jsonrpc, Object id, String met
         if (! method.equals(METHOD)) {
             throw new IllegalArgumentException("Invalid GetAuthenticatedExtendedCardRequest method");
         }
-        Assert.isNullOrStringOrInteger(id);
+        Assert.isValidJsonRpcId(id);
         this.jsonrpc = Utils.defaultIfNull(jsonrpc, JSONRPC_VERSION);
         this.id = id;
         this.method = method;
diff --git a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigRequest.java b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigRequest.java
index 4f1ef9e88..882938bd2 100644
--- a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigRequest.java
+++ b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigRequest.java
@@ -20,7 +20,7 @@ public GetTaskPushNotificationConfigRequest( String jsonrpc, Object id, String m
         if (! method.equals(METHOD)) {
             throw new IllegalArgumentException("Invalid GetTaskPushNotificationRequest method");
         }
-        Assert.isNullOrStringOrInteger(id);
+        Assert.isValidJsonRpcId(id);
         this.jsonrpc = Utils.defaultIfNull(jsonrpc, JSONRPC_VERSION);
         this.id = id;
         this.method = method;
diff --git a/spec/src/main/java/io/a2a/spec/GetTaskRequest.java b/spec/src/main/java/io/a2a/spec/GetTaskRequest.java
index 7b7fb8803..3f25fb461 100644
--- a/spec/src/main/java/io/a2a/spec/GetTaskRequest.java
+++ b/spec/src/main/java/io/a2a/spec/GetTaskRequest.java
@@ -23,7 +23,7 @@ public GetTaskRequest(String jsonrpc, Object id, String method, TaskQueryParams
             throw new IllegalArgumentException("Invalid GetTaskRequest method");
         }
         Assert.checkNotNullParam("params", params);
-        Assert.isNullOrStringOrInteger(id);
+        Assert.isValidJsonRpcId(id);
         this.jsonrpc = defaultIfNull(jsonrpc, JSONRPC_VERSION);
         this.id = id;
         this.method = method;
diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java b/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java
index 69683a764..a566e0a70 100644
--- a/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java
+++ b/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java
@@ -22,7 +22,7 @@ public JSONRPCRequest() {
     public JSONRPCRequest(String jsonrpc, Object id, String method, T params) {
         Assert.checkNotNullParam("jsonrpc", jsonrpc);
         Assert.checkNotNullParam("method", method);
-        Assert.isNullOrStringOrInteger(id);
+        Assert.isValidJsonRpcId(id);
         this.jsonrpc = defaultIfNull(jsonrpc, JSONRPC_VERSION);
         this.id = id;
         this.method = method;
diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java b/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java
index 395483596..6d2a3f3b2 100644
--- a/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java
+++ b/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java
@@ -32,7 +32,7 @@ public JSONRPCResponse(String jsonrpc, Object id, T result, JSONRPCError error,
         if (error == null && result == null && ! Void.class.equals(resultType)) {
             throw new IllegalArgumentException("Invalid JSON-RPC success response");
         }
-        Assert.isNullOrStringOrInteger(id);
+        Assert.isValidJsonRpcId(id);
         this.jsonrpc = defaultIfNull(jsonrpc, JSONRPC_VERSION);
         this.id = id;
         this.result = result;
diff --git a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigRequest.java b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigRequest.java
index 8a4b75a1f..b5d849f38 100644
--- a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigRequest.java
+++ b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigRequest.java
@@ -20,7 +20,7 @@ public ListTaskPushNotificationConfigRequest(String jsonrpc, Object id, String m
         if (! method.equals(METHOD)) {
             throw new IllegalArgumentException("Invalid ListTaskPushNotificationConfigRequest method");
         }
-        Assert.isNullOrStringOrInteger(id);
+        Assert.isValidJsonRpcId(id);
         this.jsonrpc = Utils.defaultIfNull(jsonrpc, JSONRPC_VERSION);
         this.id = id;
         this.method = method;
diff --git a/spec/src/main/java/io/a2a/spec/SendMessageRequest.java b/spec/src/main/java/io/a2a/spec/SendMessageRequest.java
index a8ec457e5..4eb7c7d4c 100644
--- a/spec/src/main/java/io/a2a/spec/SendMessageRequest.java
+++ b/spec/src/main/java/io/a2a/spec/SendMessageRequest.java
@@ -37,7 +37,7 @@ public SendMessageRequest(String jsonrpc, Object id, String method, MessageSendP
             throw new IllegalArgumentException("Invalid SendMessageRequest method");
         }
         Assert.checkNotNullParam("params", params);
-        Assert.isNullOrStringOrInteger(id);
+        Assert.isValidJsonRpcId(id);
         this.jsonrpc = defaultIfNull(jsonrpc, JSONRPC_VERSION);
         this.id = id;
         this.method = method;
@@ -56,7 +56,7 @@ public void check() {
             throw new IllegalArgumentException("Invalid SendMessageRequest method");
         }
         Assert.checkNotNullParam("params", params);
-        Assert.isNullOrStringOrInteger(id);
+        Assert.isValidJsonRpcId(id);
         params.check();
     }
 
diff --git a/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java b/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java
index d0eba2dbc..f592f9f99 100644
--- a/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java
+++ b/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java
@@ -23,7 +23,7 @@ public SendStreamingMessageRequest(String jsonrpc, Object id, String method, Mes
             throw new IllegalArgumentException("Invalid SendStreamingMessageRequest method");
         }
         Assert.checkNotNullParam("params", params);
-        Assert.isNullOrStringOrInteger(id);
+        Assert.isValidJsonRpcId(id);
         this.jsonrpc = defaultIfNull(jsonrpc, JSONRPC_VERSION);
         this.id = id;
         this.method = method;
@@ -43,7 +43,7 @@ public void check() {
             throw new IllegalArgumentException("Invalid SendStreamingMessageRequest method");
         }
         Assert.checkNotNullParam("params", params);
-        Assert.isNullOrStringOrInteger(id);
+        Assert.isValidJsonRpcId(id);
         params.check();
     }
 
diff --git a/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigRequest.java b/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigRequest.java
index 3007112ff..d81dacd56 100644
--- a/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigRequest.java
+++ b/spec/src/main/java/io/a2a/spec/SetTaskPushNotificationConfigRequest.java
@@ -22,7 +22,7 @@ public SetTaskPushNotificationConfigRequest(String jsonrpc, Object id, String me
             throw new IllegalArgumentException("Invalid SetTaskPushNotificationRequest method");
         }
         Assert.checkNotNullParam("params", params);
-        Assert.isNullOrStringOrInteger(id);
+        Assert.isValidJsonRpcId(id);
         this.jsonrpc = defaultIfNull(jsonrpc, JSONRPC_VERSION);
         this.id = id;
         this.method = method;
diff --git a/spec/src/test/java/io/a2a/json/JsonUtilNullSerializationTest.java b/spec/src/test/java/io/a2a/json/JsonUtilNullSerializationTest.java
new file mode 100644
index 000000000..91833a258
--- /dev/null
+++ b/spec/src/test/java/io/a2a/json/JsonUtilNullSerializationTest.java
@@ -0,0 +1,124 @@
+package io.a2a.json;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import io.a2a.spec.AgentCapabilities;
+import io.a2a.spec.AgentCard;
+import io.a2a.spec.AgentProvider;
+
+/**
+ * Test to verify that JsonUtil.OBJECT_MAPPER does not serialize null fields.
+ * This is critical for A2A spec compliance, particularly for AgentCard where
+ * optional fields must be omitted (not present as "field": null) when they are null.
+ */
+public class JsonUtilNullSerializationTest {
+
+    @Test
+    public void testAgentCardOptionalFieldsOmittedInSerialization() throws Exception {
+        // Create an AgentCard with REQUIRED fields only, leaving OPTIONAL fields as null
+        // Per A2A spec 5.5, REQUIRED fields are:
+        // - name, description, url, version, capabilities, defaultInputModes, defaultOutputModes, skills
+        // - preferredTransport (has default), protocolVersion (has default), supportsAuthenticatedExtendedCard (has default)
+        //
+        // OPTIONAL fields that should be omitted when null:
+        // - provider, documentationUrl, securitySchemes, security, iconUrl, signatures, additionalInterfaces
+        AgentCard card = new AgentCard.Builder()
+                .name("Test Agent")
+                .description("Test Description")
+                .url("https://example.com")
+                .version("1.0.0")
+                .capabilities(new AgentCapabilities(true, false, false, null))
+                .defaultInputModes(List.of("text"))
+                .defaultOutputModes(List.of("text"))
+                .skills(List.of())
+                // OPTIONAL fields intentionally not set (will be null):
+                // - provider
+                // - documentationUrl
+                // - securitySchemes
+                // - security
+                // - iconUrl
+                // - signatures
+                // Note: additionalInterfaces gets a default value in the builder, so we don't test it here
+                .build();
+
+        // Serialize to JSON
+        String json = JsonUtil.toJson(card);
+
+        assertNotNull(json);
+
+        // Verify that OPTIONAL fields with null values are NOT present in the JSON
+        // (not even as "field": null)
+        assertFalse(json.contains("\"provider\""),
+            "OPTIONAL 'provider' field should be omitted when null, not serialized");
+        assertFalse(json.contains("\"documentationUrl\""),
+            "OPTIONAL 'documentationUrl' field should be omitted when null, not serialized");
+        assertFalse(json.contains("\"securitySchemes\""),
+            "OPTIONAL 'securitySchemes' field should be omitted when null, not serialized");
+        assertFalse(json.contains("\"security\""),
+            "OPTIONAL 'security' field should be omitted when null, not serialized");
+        assertFalse(json.contains("\"iconUrl\""),
+            "OPTIONAL 'iconUrl' field should be omitted when null, not serialized");
+        assertFalse(json.contains("\"signatures\""),
+            "OPTIONAL 'signatures' field should be omitted when null, not serialized");
+
+        // Verify that NO fields are serialized with explicit null values
+        assertFalse(json.contains(": null"),
+            "No fields should be serialized with explicit null values. Found in JSON: " + json);
+    }
+
+    @Test
+    public void testAgentCardWithProviderIsSerializedCorrectly() throws Exception {
+        // Create an AgentCard with provider set (non-null)
+        AgentProvider provider = new AgentProvider("Test Org", "https://testorg.com");
+
+        AgentCard card = new AgentCard.Builder()
+                .name("Test Agent")
+                .description("Test Description")
+                .url("https://example.com")
+                .version("1.0.0")
+                .capabilities(new AgentCapabilities(true, false, false, null))
+                .defaultInputModes(List.of("text"))
+                .defaultOutputModes(List.of("text"))
+                .skills(List.of())
+                .provider(provider)  // Set provider explicitly
+                .build();
+
+        // Serialize to JSON
+        String json = JsonUtil.toJson(card);
+
+        assertNotNull(json);
+
+        // Verify that provider IS present in the JSON when set
+        assertFalse(json.contains("\"provider\":null"),
+            "Provider should not be serialized as null when it has a value");
+        assertFalse(json.contains("\"provider\": null"),
+            "Provider should not be serialized as null when it has a value");
+        assertFalse(json.contains(": null"),
+            "No fields should be serialized with explicit null values. Found in JSON: " + json);
+    }
+
+    @Test
+    public void testAgentCapabilitiesOptionalFieldsOmitted() throws Exception {
+        // AgentCapabilities has all OPTIONAL fields per spec 5.5.2:
+        // - streaming, pushNotifications, stateTransitionHistory (booleans - always serialized)
+        // - extensions (List - should be omitted when null)
+        AgentCapabilities caps = new AgentCapabilities(false, false, false, null);
+
+        String json = JsonUtil.toJson(caps);
+
+        assertNotNull(json);
+
+        // Note: streaming, pushNotifications, stateTransitionHistory use primitive booleans,
+        // so they will always be serialized (even with default false values)
+        // But extensions should be omitted when null
+        assertFalse(json.contains("\"extensions\""),
+            "OPTIONAL 'extensions' field should be omitted when null");
+        assertFalse(json.contains(": null"),
+            "No fields should be serialized with explicit null values. Found in JSON: " + json);
+    }
+}

From 612d0716089945ce0df4bdcbaed0632cebdb1862 Mon Sep 17 00:00:00 2001
From: Kabir Khan 
Date: Wed, 22 Apr 2026 15:58:46 +0100
Subject: [PATCH 27/37] fix: Address remaining PR #808 review feedback -
 circular dependency, precision, and robustness
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This commit addresses the remaining HIGH and MEDIUM priority issues from the PR #808 review:

**HIGH Priority - Circular Dependency (JsonUtil.java:333):**
- Convert JSONRPCErrorTypeAdapter from TypeAdapter to TypeAdapterFactory to eliminate
  circular dependency during static initialization
- The adapter now receives the Gson instance via the factory's create() method instead
  of referencing OBJECT_MAPPER during class initialization
- Prevents potential NullPointerException if the adapter is invoked during initialization

**MEDIUM Priority - ID Precision Loss (JsonUtil.java:1002):**
- Fix JSON-RPC ID serialization to preserve fractional values (e.g., 1.5 remains 1.5)
- Check number type: use longValue() for integer types (Long, Integer, Short, Byte),
  but preserve full precision for Double/Float by passing Number directly to JsonWriter
- Prevents data loss when clients use fractional numbers as JSON-RPC IDs

**MEDIUM Priority - JSON Parsing Robustness:**
- RestErrorMapper.java:36-37: Add null and type checks before calling getAsString()
  to handle JsonNull and non-primitive types gracefully
- JSONRPCUtils.java:160: Validate JSON is an object before calling getAsJsonObject()
  to provide clear error message for arrays or primitives
- A2AServerRoutes.java:97-101: Remove redundant inner try-catch block that made outer
  JsonSyntaxException handler unreachable, simplifying error handling logic

**MEDIUM Priority - Performance Optimization:**
- JSONRPCUtils.java:299: Eliminate inefficient double conversion (serialize → deserialize)
- Deserialize AgentCard directly from JsonElement using Gson.fromJson(JsonElement, Class)
  instead of converting to string first

All changes maintain backward compatibility and improve error handling for malformed input.

Co-Authored-By: Claude Sonnet 4.5 
---
 .../transport/rest/RestErrorMapper.java       |  18 +-
 .../server/apps/quarkus/A2AServerRoutes.java  |   7 +-
 .../java/io/a2a/grpc/utils/JSONRPCUtils.java  |   9 +-
 spec/src/main/java/io/a2a/json/JsonUtil.java  | 257 ++++++++++--------
 4 files changed, 162 insertions(+), 129 deletions(-)

diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java
index c23eab4a0..8fc414d14 100644
--- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java
+++ b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java
@@ -1,5 +1,6 @@
 package io.a2a.client.transport.rest;
 
+import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import io.a2a.client.http.A2AHttpResponse;
 import io.a2a.json.JsonProcessingException;
@@ -33,8 +34,21 @@ public static A2AClientException mapRestError(String body, int code) {
         try {
             if (body != null && !body.isBlank()) {
                 JsonObject node = JsonUtil.fromJson(body, JsonObject.class);
-                String className = node.has("error") ? node.get("error").getAsString() : "";
-                String errorMessage = node.has("message") ? node.get("message").getAsString() : "";
+                // Safely extract string fields, handling null and non-string types
+                String className = "";
+                if (node.has("error")) {
+                    JsonElement errorElement = node.get("error");
+                    if (errorElement != null && errorElement.isJsonPrimitive() && errorElement.getAsJsonPrimitive().isString()) {
+                        className = errorElement.getAsString();
+                    }
+                }
+                String errorMessage = "";
+                if (node.has("message")) {
+                    JsonElement messageElement = node.get("message");
+                    if (messageElement != null && messageElement.isJsonPrimitive() && messageElement.getAsJsonPrimitive().isString()) {
+                        errorMessage = messageElement.getAsString();
+                    }
+                }
                 return mapRestError(className, errorMessage, code);
             }
             return mapRestError("", "", code);
diff --git a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java
index aa1329ea4..535dbb872 100644
--- a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java
+++ b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java
@@ -93,12 +93,7 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) {
         JSONRPCErrorResponse error = null;
         Object requestId = null;
         try {
-            com.google.gson.JsonObject node;
-            try {
-                node = JsonParser.parseString(body).getAsJsonObject();
-            } catch (Exception e) {
-                throw new JSONParseError(e.getMessage());
-            }
+            com.google.gson.JsonObject node = JsonParser.parseString(body).getAsJsonObject();
 
             // Extract id field early so error responses can include it
             com.google.gson.JsonElement idElement = node.get("id");
diff --git a/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java b/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java
index 2780052ed..2b9e73cb6 100644
--- a/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java
+++ b/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java
@@ -157,6 +157,10 @@ public class JSONRPCUtils {
 
     public static JSONRPCRequest parseRequestBody(String body) throws JsonMappingException {
         JsonElement jelement = JsonParser.parseString(body);
+        if (!jelement.isJsonObject()) {
+            throw new JsonMappingException(
+                    "JSON-RPC request must be a JSON object, not an array or primitive value");
+        }
         JsonObject jsonRpc = jelement.getAsJsonObject();
         if (!jsonRpc.has("method")) {
             throw new IdJsonMappingException(
@@ -292,9 +296,10 @@ public static JSONRPCResponse parseResponseBody(String body, String method) t
             }
             case GetAuthenticatedExtendedCardRequest.METHOD -> {
                 try {
-                    AgentCard card = JsonUtil.fromJson(JsonUtil.OBJECT_MAPPER.toJson(paramsNode), AgentCard.class);
+                    // Deserialize directly from JsonElement without double conversion
+                    AgentCard card = JsonUtil.OBJECT_MAPPER.fromJson(paramsNode, AgentCard.class);
                     return new GetAuthenticatedExtendedCardResponse(id, card);
-                } catch (JsonProcessingException e) {
+                } catch (Exception e) {
                     throw new InvalidParamsError("Failed to parse agent card response: " + e.getMessage());
                 }
             }
diff --git a/spec/src/main/java/io/a2a/json/JsonUtil.java b/spec/src/main/java/io/a2a/json/JsonUtil.java
index 348297a01..cc5f19c3b 100644
--- a/spec/src/main/java/io/a2a/json/JsonUtil.java
+++ b/spec/src/main/java/io/a2a/json/JsonUtil.java
@@ -66,7 +66,7 @@
 import java.time.format.DateTimeParseException;
 import org.jspecify.annotations.Nullable;
 
-import static io.a2a.json.JsonUtil.JSONRPCErrorTypeAdapter.THROWABLE_MARKER_FIELD;
+import static io.a2a.json.JsonUtil.JSONRPCErrorTypeAdapterFactory.THROWABLE_MARKER_FIELD;
 
 public class JsonUtil {
 
@@ -74,7 +74,7 @@ private static GsonBuilder createBaseGsonBuilder() {
         return new GsonBuilder()
                 .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
                 .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeTypeAdapter())
-                .registerTypeHierarchyAdapter(JSONRPCError.class, new JSONRPCErrorTypeAdapter())
+                .registerTypeAdapterFactory(new JSONRPCErrorTypeAdapterFactory())
                 .registerTypeAdapter(TaskState.class, new TaskStateTypeAdapter())
                 .registerTypeAdapter(Message.Role.class, new RoleTypeAdapter())
                 .registerTypeAdapter(Part.Kind.class, new PartKindTypeAdapter())
@@ -305,7 +305,7 @@ Throwable read(JsonReader in) throws java.io.IOException {
      *
      * @see JSONRPCError
      */
-    static class JSONRPCErrorTypeAdapter extends TypeAdapter {
+    static class JSONRPCErrorTypeAdapterFactory implements TypeAdapterFactory {
 
         private static final ThrowableTypeAdapter THROWABLE_ADAPTER = new ThrowableTypeAdapter();
         static final String THROWABLE_MARKER_FIELD = "__throwable";
@@ -315,135 +315,147 @@ static class JSONRPCErrorTypeAdapter extends TypeAdapter {
         private static final String TYPE_FIELD = "type";
 
         @Override
-        public void write(JsonWriter out, JSONRPCError value) throws java.io.IOException {
-            if (value == null) {
-                out.nullValue();
-                return;
-            }
-            out.beginObject();
-            out.name(CODE_FIELD).value(value.getCode());
-            out.name(MESSAGE_FIELD).value(value.getMessage());
-            if (value.getData() != null) {
-                out.name(DATA_FIELD);
-                // If data is a Throwable, use ThrowableTypeAdapter to avoid reflection issues
-                if (value.getData() instanceof Throwable throwable) {
-                    THROWABLE_ADAPTER.write(out, throwable);
-                } else {
-                    // Use Gson to serialize the data field for non-Throwable types
-                    OBJECT_MAPPER.toJson(value.getData(), Object.class, out);
-                }
-            }
-            out.endObject();
-        }
-
-        @Override
-        public @Nullable
-        JSONRPCError read(JsonReader in) throws java.io.IOException {
-            if (in.peek() == com.google.gson.stream.JsonToken.NULL) {
-                in.nextNull();
+        public @Nullable  TypeAdapter create(Gson gson, TypeToken type) {
+            if (!JSONRPCError.class.isAssignableFrom(type.getRawType())) {
                 return null;
             }
 
-            Integer code = null;
-            String message = null;
-            Object data = null;
-
-            in.beginObject();
-            while (in.hasNext()) {
-                String fieldName = in.nextName();
-                switch (fieldName) {
-                    case CODE_FIELD ->
-                        code = in.nextInt();
-                    case MESSAGE_FIELD ->
-                        message = in.nextString();
-                    case DATA_FIELD -> {
-                        // Read data as a generic object (could be string, number, object, etc.)
-                        data = readDataValue(in);
+            @SuppressWarnings("unchecked")
+            TypeAdapter adapter = (TypeAdapter) new TypeAdapter() {
+                @Override
+                public void write(JsonWriter out, JSONRPCError value) throws java.io.IOException {
+                    if (value == null) {
+                        out.nullValue();
+                        return;
                     }
-                    default ->
-                        in.skipValue();
+                    out.beginObject();
+                    out.name(CODE_FIELD).value(value.getCode());
+                    out.name(MESSAGE_FIELD).value(value.getMessage());
+                    if (value.getData() != null) {
+                        out.name(DATA_FIELD);
+                        // If data is a Throwable, use ThrowableTypeAdapter to avoid reflection issues
+                        if (value.getData() instanceof Throwable throwable) {
+                            THROWABLE_ADAPTER.write(out, throwable);
+                        } else {
+                            // Use the Gson instance passed to this factory instead of OBJECT_MAPPER
+                            gson.toJson(value.getData(), Object.class, out);
+                        }
+                    }
+                    out.endObject();
                 }
-            }
-            in.endObject();
 
-            // Create the appropriate subclass based on the error code
-            return createErrorInstance(code, message, data);
-        }
+                @Override
+                public @Nullable
+                JSONRPCError read(JsonReader in) throws java.io.IOException {
+                    if (in.peek() == com.google.gson.stream.JsonToken.NULL) {
+                        in.nextNull();
+                        return null;
+                    }
 
-        /**
-         * Reads the data field value, which can be of any JSON type.
-         */
-        private @Nullable
-        Object readDataValue(JsonReader in) throws java.io.IOException {
-            return switch (in.peek()) {
-                case STRING ->
-                    in.nextString();
-                case NUMBER ->
-                    in.nextDouble();
-                case BOOLEAN ->
-                    in.nextBoolean();
-                case NULL -> {
-                    in.nextNull();
-                    yield null;
-                }
-                case BEGIN_OBJECT -> {
-                    // Parse as JsonElement to check if it's a Throwable
-                    com.google.gson.JsonElement element = com.google.gson.JsonParser.parseReader(in);
-                    if (element.isJsonObject()) {
-                        com.google.gson.JsonObject obj = element.getAsJsonObject();
-                        // Check if it has the structure of a serialized Throwable (type + message)
-                        if (obj.has(TYPE_FIELD) && obj.has(MESSAGE_FIELD) && obj.has(THROWABLE_MARKER_FIELD)) {
-                            // Deserialize as Throwable using ThrowableTypeAdapter
-                            yield THROWABLE_ADAPTER.read(new JsonReader(new StringReader(element.toString())));
+                    Integer code = null;
+                    String message = null;
+                    Object data = null;
+
+                    in.beginObject();
+                    while (in.hasNext()) {
+                        String fieldName = in.nextName();
+                        switch (fieldName) {
+                            case CODE_FIELD ->
+                                code = in.nextInt();
+                            case MESSAGE_FIELD ->
+                                message = in.nextString();
+                            case DATA_FIELD -> {
+                                // Read data as a generic object (could be string, number, object, etc.)
+                                data = readDataValue(in, gson);
+                            }
+                            default ->
+                                in.skipValue();
                         }
                     }
-                    // Otherwise, deserialize as generic object
-                    yield OBJECT_MAPPER.fromJson(element, Object.class);
+                    in.endObject();
+
+                    // Create the appropriate subclass based on the error code
+                    return createErrorInstance(code, message, data);
                 }
-                case BEGIN_ARRAY ->
-                    // For arrays, read as raw JSON using Gson
-                    OBJECT_MAPPER.fromJson(in, Object.class);
-                default -> {
-                    in.skipValue();
-                    yield null;
+
+                /**
+                 * Reads the data field value, which can be of any JSON type.
+                 */
+                private @Nullable
+                Object readDataValue(JsonReader in, Gson gson) throws java.io.IOException {
+                    return switch (in.peek()) {
+                        case STRING ->
+                            in.nextString();
+                        case NUMBER ->
+                            in.nextDouble();
+                        case BOOLEAN ->
+                            in.nextBoolean();
+                        case NULL -> {
+                            in.nextNull();
+                            yield null;
+                        }
+                        case BEGIN_OBJECT -> {
+                            // Parse as JsonElement to check if it's a Throwable
+                            com.google.gson.JsonElement element = com.google.gson.JsonParser.parseReader(in);
+                            if (element.isJsonObject()) {
+                                com.google.gson.JsonObject obj = element.getAsJsonObject();
+                                // Check if it has the structure of a serialized Throwable (type + message)
+                                if (obj.has(TYPE_FIELD) && obj.has(MESSAGE_FIELD) && obj.has(THROWABLE_MARKER_FIELD)) {
+                                    // Deserialize as Throwable using ThrowableTypeAdapter
+                                    yield THROWABLE_ADAPTER.read(new JsonReader(new StringReader(element.toString())));
+                                }
+                            }
+                            // Otherwise, deserialize as generic object using the Gson instance
+                            yield gson.fromJson(element, Object.class);
+                        }
+                        case BEGIN_ARRAY ->
+                            // For arrays, read as raw JSON using the Gson instance
+                            gson.fromJson(in, Object.class);
+                        default -> {
+                            in.skipValue();
+                            yield null;
+                        }
+                    };
                 }
-            };
-        }
 
-        /**
-         * Creates the appropriate JSONRPCError subclass based on the error code.
-         */
-        private JSONRPCError createErrorInstance(@Nullable Integer code, @Nullable String message, @Nullable Object data) {
-            if (code == null) {
-                throw new JsonSyntaxException("JSONRPCError must have a code field");
-            }
+                /**
+                 * Creates the appropriate JSONRPCError subclass based on the error code.
+                 */
+                private JSONRPCError createErrorInstance(@Nullable Integer code, @Nullable String message, @Nullable Object data) {
+                    if (code == null) {
+                        throw new JsonSyntaxException("JSONRPCError must have a code field");
+                    }
 
-            return switch (code) {
-                case JSON_PARSE_ERROR_CODE ->
-                    new JSONParseError(code, message, data);
-                case INVALID_REQUEST_ERROR_CODE ->
-                    new InvalidRequestError(code, message, data);
-                case METHOD_NOT_FOUND_ERROR_CODE ->
-                    new MethodNotFoundError(code, message, data);
-                case INVALID_PARAMS_ERROR_CODE ->
-                    new InvalidParamsError(code, message, data);
-                case INTERNAL_ERROR_CODE ->
-                    new io.a2a.spec.InternalError(code, message, data);
-                case TASK_NOT_FOUND_ERROR_CODE ->
-                    new TaskNotFoundError(code, message, data);
-                case TASK_NOT_CANCELABLE_ERROR_CODE ->
-                    new TaskNotCancelableError(code, message, data);
-                case PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE ->
-                    new PushNotificationNotSupportedError(code, message, data);
-                case UNSUPPORTED_OPERATION_ERROR_CODE ->
-                    new UnsupportedOperationError(code, message, data);
-                case CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE ->
-                    new ContentTypeNotSupportedError(code, message, data);
-                case INVALID_AGENT_RESPONSE_ERROR_CODE ->
-                    new InvalidAgentResponseError(code, message, data);
-                default ->
-                    new JSONRPCError(code, message, data);
+                    return switch (code) {
+                        case JSON_PARSE_ERROR_CODE ->
+                            new JSONParseError(code, message, data);
+                        case INVALID_REQUEST_ERROR_CODE ->
+                            new InvalidRequestError(code, message, data);
+                        case METHOD_NOT_FOUND_ERROR_CODE ->
+                            new MethodNotFoundError(code, message, data);
+                        case INVALID_PARAMS_ERROR_CODE ->
+                            new InvalidParamsError(code, message, data);
+                        case INTERNAL_ERROR_CODE ->
+                            new io.a2a.spec.InternalError(code, message, data);
+                        case TASK_NOT_FOUND_ERROR_CODE ->
+                            new TaskNotFoundError(code, message, data);
+                        case TASK_NOT_CANCELABLE_ERROR_CODE ->
+                            new TaskNotCancelableError(code, message, data);
+                        case PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE ->
+                            new PushNotificationNotSupportedError(code, message, data);
+                        case UNSUPPORTED_OPERATION_ERROR_CODE ->
+                            new UnsupportedOperationError(code, message, data);
+                        case CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE ->
+                            new ContentTypeNotSupportedError(code, message, data);
+                        case INVALID_AGENT_RESPONSE_ERROR_CODE ->
+                            new InvalidAgentResponseError(code, message, data);
+                        default ->
+                            new JSONRPCError(code, message, data);
+                    };
+                }
             };
+
+            return adapter;
         }
     }
 
@@ -987,7 +999,14 @@ public void write(JsonWriter out, T value) throws java.io.IOException {
                     if (id == null) {
                         out.nullValue();
                     } else if (id instanceof Number n) {
-                        out.value(n.longValue());
+                        // Preserve precision for fractional IDs (e.g., 1.5 should remain 1.5, not become 1)
+                        // Check if the number is an integer type or has no fractional part
+                        if (id instanceof Long || id instanceof Integer || id instanceof Short || id instanceof Byte) {
+                            out.value(n.longValue());
+                        } else {
+                            // For Double, Float, or other number types, preserve full precision
+                            out.value(n);
+                        }
                     } else {
                         out.value(id.toString());
                     }

From a56d89a32f38c22bc9d3dc9a873eb3c1aa335e02 Mon Sep 17 00:00:00 2001
From: Kabir Khan 
Date: Wed, 22 Apr 2026 17:01:31 +0100
Subject: [PATCH 28/37] refactor: Improve JSON-RPC code quality and null safety

Extract duplicated id-writing code into ensureId() helper method to reduce
repetition in response serialization. Add @Nullable annotations to handle
null id values throughout the codebase per JSON-RPC 2.0 spec requirements.
Fix error data serialization to use fromJson() instead of toString() to
preserve proper JSON structure. Optimize params handling by using toJson()
directly instead of inefficient serialize-parse roundtrip.

Co-Authored-By: Claude Sonnet 4.5 
---
 .../java/io/a2a/grpc/utils/JSONRPCUtils.java  | 72 +++++++++++--------
 1 file changed, 41 insertions(+), 31 deletions(-)

diff --git a/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java b/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java
index 2b9e73cb6..02c8fe772 100644
--- a/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java
+++ b/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java
@@ -179,7 +179,7 @@ public static JSONRPCRequest parseRequestBody(String body) throws JsonMapping
         }
     }
 
-    private static JSONRPCRequest parseMethodRequest(String version, Object id, String method, JsonElement paramsNode) throws InvalidParamsError, MethodNotFoundJsonMappingException {
+    private static JSONRPCRequest parseMethodRequest(String version, @Nullable Object id, String method, JsonElement paramsNode) throws InvalidParamsError, MethodNotFoundJsonMappingException {
         switch (method) {
             case GetTaskRequest.METHOD -> {
                 io.a2a.grpc.GetTaskRequest.Builder builder = io.a2a.grpc.GetTaskRequest.newBuilder();
@@ -308,7 +308,7 @@ public static JSONRPCResponse parseResponseBody(String body, String method) t
         }
     }
 
-    public static JSONRPCResponse parseError(JsonObject error, Object id, String method) throws JsonMappingException {
+    public static JSONRPCResponse parseError(JsonObject error, @Nullable Object id, String method) throws JsonMappingException {
         JSONRPCError rpcError = processError(error);
         switch (method) {
             case GetTaskRequest.METHOD -> {
@@ -340,7 +340,8 @@ public static JSONRPCResponse parseError(JsonObject error, Object id, String
     private static JSONRPCError processError(JsonObject error) {
         String message = error.has("message") ? error.get("message").getAsString() : null;
         Integer code = error.has("code") ? error.get("code").getAsInt() : null;
-        String data = error.has("data") ? error.get("data").toString() : null;
+        // Deserialize data to preserve JSON structure, not double-encode with toString()
+        Object data = error.has("data") ? JsonUtil.OBJECT_MAPPER.fromJson(error.get("data"), Object.class) : null;
         if (code != null) {
             switch (code) {
                 case JSON_PARSE_ERROR_CODE:
@@ -373,10 +374,14 @@ private static JSONRPCError processError(JsonObject error) {
     }
 
     protected static void parseRequestBody(JsonElement jsonRpc, com.google.protobuf.Message.Builder builder) throws JSONRPCError {
-        try (Writer writer = new StringWriter()) {
-            JsonUtil.OBJECT_MAPPER.toJson(jsonRpc, writer);
-            parseJsonString(writer.toString(), builder);
-        } catch (IOException e) {
+        // JSON-RPC 2.0 spec: params field is optional, handle null/omitted gracefully
+        if (jsonRpc == null || jsonRpc.isJsonNull()) {
+            return;
+        }
+        try {
+            // Use toJson(Object) directly instead of inefficient serialize→parse roundtrip
+            parseJsonString(JsonUtil.OBJECT_MAPPER.toJson(jsonRpc), builder);
+        } catch (Exception e) {
             log.log(Level.SEVERE, "Failed to serialize JSON element to string during proto conversion. JSON: {0}", jsonRpc);
             log.log(Level.SEVERE, "Serialization error details", e);
             throw new InvalidParamsError(
@@ -439,7 +444,7 @@ protected static String getAndValidateJsonrpc(JsonObject jsonRpc) throws JsonMap
      * @param jsonRpc the json rpc JSON.
      * @return the request id if possible , "UNDETERMINED ID" otherwise.
      */
-    protected static Object getIdIfPossible(JsonObject jsonRpc) {
+    protected static @Nullable Object getIdIfPossible(JsonObject jsonRpc) {
         try {
             return getAndValidateId(jsonRpc);
         } catch (JsonMappingException e) {
@@ -448,23 +453,25 @@ protected static Object getIdIfPossible(JsonObject jsonRpc) {
         }
     }
 
-    protected static Object getAndValidateId(JsonObject jsonRpc) throws JsonMappingException {
+    protected static @Nullable Object getAndValidateId(JsonObject jsonRpc) throws JsonMappingException {
         Object id = null;
         if (jsonRpc.has("id")) {
-            if (jsonRpc.get("id").isJsonPrimitive()) {
+            JsonElement idElement = jsonRpc.get("id");
+            // JSON-RPC 2.0 allows null id (though discouraged for requests)
+            if (idElement.isJsonNull()) {
+                id = null;
+            } else if (idElement.isJsonPrimitive()) {
                 try {
-                    id = jsonRpc.get("id").getAsLong();
+                    id = idElement.getAsLong();
                 } catch (UnsupportedOperationException | NumberFormatException | IllegalStateException e) {
-                    id = jsonRpc.get("id").getAsString();
+                    id = idElement.getAsString();
                 }
             } else {
-                throw new JsonMappingException(null,  "Invalid 'id' type: " + jsonRpc.get("id").getClass().getSimpleName() +
-                    ". ID must be a JSON string or number, not an object or array.");
+                throw new JsonMappingException(null,  "Invalid 'id' type: " + idElement.getClass().getSimpleName() +
+                    ". ID must be a JSON string, number, or null, not an object or array.");
             }
         }
-        if (id == null) {
-            throw new JsonMappingException(null, "Request 'id' cannot be null. Use a string or number identifier.");
-        }
+        // JSON-RPC 2.0 spec allows null id, so don't throw exception
         return id;
     }
 
@@ -493,17 +500,26 @@ public static String toJsonRPCRequest(@Nullable String requestId, String method,
         }
     }
 
+    /**
+     * Writes the 'id' field to a JSON-RPC response.
+     * Per JSON-RPC 2.0 spec, the id field is REQUIRED in all responses, even if null.
+     */
+    private static void ensureId(JsonWriter output, @Nullable Object requestId) throws IOException {
+        output.name("id");
+        if (requestId == null) {
+            output.nullValue();
+        } else if (requestId instanceof String string) {
+            output.value(string);
+        } else if (requestId instanceof Number number) {
+            output.value(number.longValue());
+        }
+    }
+
     public static String toJsonRPCResultResponse(Object requestId, com.google.protobuf.MessageOrBuilder builder) {
         try (StringWriter result = new StringWriter(); JsonWriter output = JsonUtil.OBJECT_MAPPER.newJsonWriter(result)) {
             output.beginObject();
             output.name("jsonrpc").value("2.0");
-            if (requestId != null) {
-                if (requestId instanceof String string) {
-                    output.name("id").value(string);
-                } else if (requestId instanceof Number number) {
-                    output.name("id").value(number.longValue());
-                }
-            }
+            ensureId(output, requestId);
             String resultValue = JsonFormat.printer().omittingInsignificantWhitespace().print(builder);
             output.name("result").jsonValue(resultValue);
             output.endObject();
@@ -519,13 +535,7 @@ public static String toJsonRPCErrorResponse(Object requestId, JSONRPCError error
         try (StringWriter result = new StringWriter(); JsonWriter output = JsonUtil.OBJECT_MAPPER.newJsonWriter(result)) {
             output.beginObject();
             output.name("jsonrpc").value("2.0");
-            if (requestId != null) {
-                if (requestId instanceof String string) {
-                    output.name("id").value(string);
-                } else if (requestId instanceof Number number) {
-                    output.name("id").value(number.longValue());
-                }
-            }
+            ensureId(output, requestId);
             output.name("error");
             output.beginObject();
             output.name("code").value(error.getCode());

From 95cb971de697132e4d25effdad1f1625dbc52131 Mon Sep 17 00:00:00 2001
From: Kabir Khan 
Date: Wed, 22 Apr 2026 17:33:52 +0100
Subject: [PATCH 29/37] perf: Optimize JSON processing and improve error code
 compliance

Eliminate inefficient toString() serialization followed by fromJson() parsing
cycles by using Gson's direct JsonElement deserialization. Replace three
instances of element.toString() + fromJson(String) with direct fromJson(JsonElement)
calls in SSEEventListener and JsonUtil.

Fix JSON-RPC 2.0 protocol compliance in A2AServerRoutes: validate that request
body is a JSON object before calling getAsJsonObject(). Non-object JSON now
correctly returns InvalidRequestError (-32600) instead of InternalError (-32603).

Co-Authored-By: Claude Sonnet 4.5 
---
 .../a2a/client/transport/jsonrpc/sse/SSEEventListener.java  | 5 ++---
 .../java/io/a2a/server/apps/quarkus/A2AServerRoutes.java    | 6 +++++-
 spec/src/main/java/io/a2a/json/JsonUtil.java                | 2 +-
 3 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java
index 7ed6585f9..0ab70027a 100644
--- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java
+++ b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java
@@ -62,14 +62,13 @@ public void onComplete() {
 
     private void handleMessage(JsonObject jsonObject, Future future) throws JsonProcessingException {
         if (jsonObject.has("error")) {
-            JSONRPCError error = JsonUtil.fromJson(jsonObject.get("error").toString(), JSONRPCError.class);
+            JSONRPCError error = JsonUtil.OBJECT_MAPPER.fromJson(jsonObject.get("error"), JSONRPCError.class);
             if (errorHandler != null) {
                 errorHandler.accept(error);
             }
         } else if (jsonObject.has("result")) {
             // result can be a Task, Message, TaskStatusUpdateEvent, or TaskArtifactUpdateEvent
-            String resultJson = jsonObject.get("result").toString();
-            StreamingEventKind event = JsonUtil.fromJson(resultJson, StreamingEventKind.class);
+            StreamingEventKind event = JsonUtil.OBJECT_MAPPER.fromJson(jsonObject.get("result"), StreamingEventKind.class);
             eventHandler.accept(event);
             if (event instanceof TaskStatusUpdateEvent && ((TaskStatusUpdateEvent) event).isFinal()) {
                 future.cancel(true); // close SSE channel
diff --git a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java
index 535dbb872..87e0b214d 100644
--- a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java
+++ b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java
@@ -93,7 +93,11 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) {
         JSONRPCErrorResponse error = null;
         Object requestId = null;
         try {
-            com.google.gson.JsonObject node = JsonParser.parseString(body).getAsJsonObject();
+            com.google.gson.JsonElement element = JsonParser.parseString(body);
+            if (!element.isJsonObject()) {
+                throw new InvalidRequestError("Invalid JSON-RPC request: expected a JSON object");
+            }
+            com.google.gson.JsonObject node = element.getAsJsonObject();
 
             // Extract id field early so error responses can include it
             com.google.gson.JsonElement idElement = node.get("id");
diff --git a/spec/src/main/java/io/a2a/json/JsonUtil.java b/spec/src/main/java/io/a2a/json/JsonUtil.java
index cc5f19c3b..34c238358 100644
--- a/spec/src/main/java/io/a2a/json/JsonUtil.java
+++ b/spec/src/main/java/io/a2a/json/JsonUtil.java
@@ -402,7 +402,7 @@ Object readDataValue(JsonReader in, Gson gson) throws java.io.IOException {
                                 // Check if it has the structure of a serialized Throwable (type + message)
                                 if (obj.has(TYPE_FIELD) && obj.has(MESSAGE_FIELD) && obj.has(THROWABLE_MARKER_FIELD)) {
                                     // Deserialize as Throwable using ThrowableTypeAdapter
-                                    yield THROWABLE_ADAPTER.read(new JsonReader(new StringReader(element.toString())));
+                                    yield THROWABLE_ADAPTER.fromJsonTree(element);
                                 }
                             }
                             // Otherwise, deserialize as generic object using the Gson instance

From 4e7e8fc4334af3453695bb53df22f0d650736f0c Mon Sep 17 00:00:00 2001
From: Daria Wieliczko 
Date: Tue, 14 Apr 2026 18:36:10 +0000
Subject: [PATCH 30/37] feat(http-client): add Android support and implement
 SPI for A2AHttpClient

- Introduce `AndroidA2AHttpClient` using `HttpURLConnection` for Android compatibility.
- Implement `A2AHttpClientFactory` and `A2AHttpClientProvider` using `ServiceLoader` to decouple implementations.
- Update `A2A` and transport providers to use the factory instead of hardcoding `JdkA2AHttpClient`.
- Add `@JsonProperty` annotations to spec records to prevent parsing failures on Android.
---
 README.md                                     |   4 +-
 client/base/src/main/java/io/a2a/A2A.java     |   7 +-
 .../transport/jsonrpc/JSONRPCTransport.java   |   4 +-
 .../JSONRPCTransportConfigBuilder.java        |   3 +-
 .../jsonrpc/JSONRPCTransportProvider.java     |   4 +-
 .../client/transport/rest/RestTransport.java  |   4 +-
 .../rest/RestTransportConfigBuilder.java      |   4 +-
 .../transport/rest/RestTransportProvider.java |   6 +-
 extras/http-client-android/pom.xml            |  42 +++
 .../a2a/client/http/AndroidA2AHttpClient.java | 290 ++++++++++++++++++
 .../http/AndroidA2AHttpClientProvider.java    |  22 ++
 .../io.a2a.client.http.A2AHttpClientProvider  |   1 +
 .../http/AndroidA2AHttpClientFactoryTest.java |  41 +++
 .../AndroidA2AHttpClientIntegrationTest.java  | 214 +++++++++++++
 .../http/AndroidA2AHttpClientSSETest.java     | 254 +++++++++++++++
 .../client/http/AndroidA2AHttpClientTest.java |  59 ++++
 .../io/a2a/client/http/A2ACardResolver.java   |  45 +--
 .../a2a/client/http/A2AHttpClientFactory.java |  52 ++++
 .../client/http/A2AHttpClientProvider.java    |  23 ++
 .../client/http/JdkA2AHttpClientProvider.java |  22 ++
 .../io.a2a.client.http.A2AHttpClientProvider  |   1 +
 .../a2a/client/http/A2ACardResolverTest.java  |   5 +
 pom.xml                                       |   1 +
 .../tasks/BasePushNotificationSender.java     |  12 +-
 24 files changed, 1077 insertions(+), 43 deletions(-)
 create mode 100644 extras/http-client-android/pom.xml
 create mode 100644 extras/http-client-android/src/main/java/io/a2a/client/http/AndroidA2AHttpClient.java
 create mode 100644 extras/http-client-android/src/main/java/io/a2a/client/http/AndroidA2AHttpClientProvider.java
 create mode 100644 extras/http-client-android/src/main/resources/META-INF/services/io.a2a.client.http.A2AHttpClientProvider
 create mode 100644 extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientFactoryTest.java
 create mode 100644 extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientIntegrationTest.java
 create mode 100644 extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientSSETest.java
 create mode 100644 extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientTest.java
 create mode 100644 http-client/src/main/java/io/a2a/client/http/A2AHttpClientFactory.java
 create mode 100644 http-client/src/main/java/io/a2a/client/http/A2AHttpClientProvider.java
 create mode 100644 http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClientProvider.java
 create mode 100644 http-client/src/main/resources/META-INF/services/io.a2a.client.http.A2AHttpClientProvider

diff --git a/README.md b/README.md
index 45c0fa1d9..14289c27b 100644
--- a/README.md
+++ b/README.md
@@ -394,7 +394,7 @@ Different transport protocols can be configured with specific settings using spe
 
 ##### JSON-RPC Transport Configuration
 
-For the JSON-RPC transport, to use the default `JdkA2AHttpClient`, provide a `JSONRPCTransportConfig` created with its default constructor.
+For the JSON-RPC transport, to use the default HTTP client (resolved automatically by `A2AHttpClientFactory`), provide a `JSONRPCTransportConfig` created with its default constructor.
 
 To use a custom HTTP client implementation, simply create a `JSONRPCTransportConfig` as follows:
 
@@ -441,7 +441,7 @@ Client client = Client
 
 ##### HTTP+JSON/REST Transport Configuration
 
-For the HTTP+JSON/REST transport, if you'd like to use the default `JdkA2AHttpClient`, provide a `RestTransportConfig` created with its default constructor.
+For the HTTP+JSON/REST transport, to use the default HTTP client (resolved automatically by `A2AHttpClientFactory`), provide a `RestTransportConfig` created with its default constructor.
 
 To use a custom HTTP client implementation, simply create a `RestTransportConfig` as follows:
 
diff --git a/client/base/src/main/java/io/a2a/A2A.java b/client/base/src/main/java/io/a2a/A2A.java
index d64cdaa27..7762eb2d2 100644
--- a/client/base/src/main/java/io/a2a/A2A.java
+++ b/client/base/src/main/java/io/a2a/A2A.java
@@ -3,11 +3,10 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.UUID;
 
 import io.a2a.client.http.A2ACardResolver;
 import io.a2a.client.http.A2AHttpClient;
-import io.a2a.client.http.JdkA2AHttpClient;
+import io.a2a.client.http.A2AHttpClientFactory;
 import io.a2a.spec.A2AClientError;
 import io.a2a.spec.A2AClientJSONError;
 import io.a2a.spec.AgentCard;
@@ -139,7 +138,7 @@ private static Message toMessage(List> parts, Message.Role role, String
      * @throws A2AClientJSONError If the response body cannot be decoded as JSON or validated against the AgentCard schema
      */
     public static AgentCard getAgentCard(String agentUrl) throws A2AClientError, A2AClientJSONError {
-        return getAgentCard(new JdkA2AHttpClient(), agentUrl);
+        return getAgentCard(A2AHttpClientFactory.create(), agentUrl);
     }
 
     /**
@@ -167,7 +166,7 @@ public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl)
      * @throws A2AClientJSONError If the response body cannot be decoded as JSON or validated against the AgentCard schema
      */
     public static AgentCard getAgentCard(String agentUrl, String relativeCardPath, Map authHeaders) throws A2AClientError, A2AClientJSONError {
-        return getAgentCard(new JdkA2AHttpClient(), agentUrl, relativeCardPath, authHeaders);
+        return getAgentCard(A2AHttpClientFactory.create(), agentUrl, relativeCardPath, authHeaders);
     }
 
     /**
diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java
index df16d071e..b1c38afcc 100644
--- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java
+++ b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java
@@ -14,8 +14,8 @@
 import io.a2a.client.transport.spi.interceptors.ClientCallInterceptor;
 import io.a2a.client.transport.spi.interceptors.PayloadAndHeaders;
 import io.a2a.client.http.A2AHttpClient;
+import io.a2a.client.http.A2AHttpClientFactory;
 import io.a2a.client.http.A2AHttpResponse;
-import io.a2a.client.http.JdkA2AHttpClient;
 import io.a2a.client.transport.spi.ClientTransport;
 import io.a2a.spec.A2AClientError;
 import io.a2a.spec.A2AClientException;
@@ -84,7 +84,7 @@ public JSONRPCTransport(AgentCard agentCard) {
 
     public JSONRPCTransport(A2AHttpClient httpClient, AgentCard agentCard,
                             String agentUrl, List interceptors) {
-        this.httpClient = httpClient == null ? new JdkA2AHttpClient() : httpClient;
+        this.httpClient = httpClient == null ? A2AHttpClientFactory.create() : httpClient;
         this.agentCard = agentCard;
         this.agentUrl = agentUrl;
         this.interceptors = interceptors;
diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportConfigBuilder.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportConfigBuilder.java
index 64153620f..52d7e7f8c 100644
--- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportConfigBuilder.java
+++ b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportConfigBuilder.java
@@ -1,6 +1,7 @@
 package io.a2a.client.transport.jsonrpc;
 
 import io.a2a.client.http.A2AHttpClient;
+import io.a2a.client.http.A2AHttpClientFactory;
 import io.a2a.client.http.JdkA2AHttpClient;
 import io.a2a.client.transport.spi.ClientTransportConfigBuilder;
 
@@ -18,7 +19,7 @@ public JSONRPCTransportConfigBuilder httpClient(A2AHttpClient httpClient) {
     public JSONRPCTransportConfig build() {
         // No HTTP client provided, fallback to the default one (JDK-based implementation)
         if (httpClient == null) {
-            httpClient = new JdkA2AHttpClient();
+            httpClient = A2AHttpClientFactory.create();
         }
 
         JSONRPCTransportConfig config = new JSONRPCTransportConfig(httpClient);
diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportProvider.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportProvider.java
index 97c22866a..75403515c 100644
--- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportProvider.java
+++ b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportProvider.java
@@ -1,6 +1,6 @@
 package io.a2a.client.transport.jsonrpc;
 
-import io.a2a.client.http.JdkA2AHttpClient;
+import io.a2a.client.http.A2AHttpClientFactory;
 import io.a2a.client.transport.spi.ClientTransportProvider;
 import io.a2a.spec.A2AClientException;
 import io.a2a.spec.AgentCard;
@@ -11,7 +11,7 @@ public class JSONRPCTransportProvider implements ClientTransportProvider interceptors) {
-        this.httpClient = httpClient == null ? new JdkA2AHttpClient() : httpClient;
+        this.httpClient = httpClient == null ? A2AHttpClientFactory.create() : httpClient;
         this.agentCard = agentCard;
         this.agentUrl = agentUrl.endsWith("/") ? agentUrl.substring(0, agentUrl.length() - 1) : agentUrl;
         this.interceptors = interceptors;
diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportConfigBuilder.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportConfigBuilder.java
index 68150f189..96a01740a 100644
--- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportConfigBuilder.java
+++ b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportConfigBuilder.java
@@ -1,6 +1,7 @@
 package io.a2a.client.transport.rest;
 
 import io.a2a.client.http.A2AHttpClient;
+import io.a2a.client.http.A2AHttpClientFactory;
 import io.a2a.client.http.JdkA2AHttpClient;
 import io.a2a.client.transport.spi.ClientTransportConfigBuilder;
 import org.jspecify.annotations.Nullable;
@@ -16,9 +17,8 @@ public RestTransportConfigBuilder httpClient(A2AHttpClient httpClient) {
 
     @Override
     public RestTransportConfig build() {
-        // No HTTP client provided, fallback to the default one (JDK-based implementation)
         if (httpClient == null) {
-            httpClient = new JdkA2AHttpClient();
+            httpClient = A2AHttpClientFactory.create();
         }
 
         RestTransportConfig config = new RestTransportConfig(httpClient);
diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportProvider.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportProvider.java
index 99d155968..f9a178c5c 100644
--- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportProvider.java
+++ b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportProvider.java
@@ -1,6 +1,6 @@
 package io.a2a.client.transport.rest;
 
-import io.a2a.client.http.JdkA2AHttpClient;
+import io.a2a.client.http.A2AHttpClientFactory;
 import io.a2a.client.transport.spi.ClientTransportProvider;
 import io.a2a.spec.A2AClientException;
 import io.a2a.spec.AgentCard;
@@ -17,9 +17,9 @@ public String getTransportProtocol() {
     public RestTransport create(RestTransportConfig clientTransportConfig, AgentCard agentCard, String agentUrl) throws A2AClientException {
         RestTransportConfig transportConfig = clientTransportConfig;
          if (transportConfig == null) {
-            transportConfig = new RestTransportConfig(new JdkA2AHttpClient());
+            transportConfig = new RestTransportConfig(A2AHttpClientFactory.create());
         }
-        return new RestTransport(clientTransportConfig.getHttpClient(), agentCard, agentUrl, transportConfig.getInterceptors());
+        return new RestTransport(transportConfig.getHttpClient(), agentCard, agentUrl, transportConfig.getInterceptors());
     }
 
     @Override
diff --git a/extras/http-client-android/pom.xml b/extras/http-client-android/pom.xml
new file mode 100644
index 000000000..1b86001aa
--- /dev/null
+++ b/extras/http-client-android/pom.xml
@@ -0,0 +1,42 @@
+
+
+    4.0.0
+
+    
+        io.github.a2asdk
+        a2a-java-sdk-parent
+        0.3.4.Beta1-SNAPSHOT
+        ../../pom.xml
+    
+    a2a-java-sdk-http-client-android
+    jar
+
+    Java SDK A2A HTTP Client: Android
+    Java SDK for the Agent2Agent Protocol (A2A) - Android HTTP Client
+
+    
+        
+            ${project.groupId}
+            a2a-java-sdk-http-client
+        
+        
+            ${project.groupId}
+            a2a-java-sdk-spec
+        
+
+        
+            org.junit.jupiter
+            junit-jupiter-api
+            test
+        
+
+        
+            org.mock-server
+            mockserver-netty
+            test
+        
+    
+
+
diff --git a/extras/http-client-android/src/main/java/io/a2a/client/http/AndroidA2AHttpClient.java b/extras/http-client-android/src/main/java/io/a2a/client/http/AndroidA2AHttpClient.java
new file mode 100644
index 000000000..c023f083d
--- /dev/null
+++ b/extras/http-client-android/src/main/java/io/a2a/client/http/AndroidA2AHttpClient.java
@@ -0,0 +1,290 @@
+package io.a2a.client.http;
+
+import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
+import static java.net.HttpURLConnection.HTTP_MULT_CHOICE;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
+
+import io.a2a.common.A2AErrorMessages;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.function.Consumer;
+
+/** Android-specific implementation of {@link A2AHttpClient} using {@link HttpURLConnection}. */
+public class AndroidA2AHttpClient implements A2AHttpClient {
+
+  private static final Executor NET_EXECUTOR = Executors.newCachedThreadPool(r -> {
+    Thread t = new Thread(r, "A2A-Android-Net");
+    t.setDaemon(true);
+    return t;
+  });
+
+  @Override
+  public GetBuilder createGet() {
+    return new AndroidGetBuilder();
+  }
+
+  @Override
+  public PostBuilder createPost() {
+    return new AndroidPostBuilder();
+  }
+
+  @Override
+  public DeleteBuilder createDelete() {
+    return new AndroidDeleteBuilder();
+  }
+
+  private abstract static class AndroidBuilder> implements Builder {
+    protected String url = "";
+    protected Map headers = new HashMap<>();
+
+    @Override
+    public T url(String url) {
+      this.url = url;
+      return self();
+    }
+
+    @Override
+    public T addHeader(String name, String value) {
+      headers.put(name, value);
+      return self();
+    }
+
+    @Override
+    public T addHeaders(Map headers) {
+      if (headers != null) {
+        this.headers.putAll(headers);
+      }
+      return self();
+    }
+
+    @SuppressWarnings("unchecked")
+    protected T self() {
+      return (T) this;
+    }
+
+    protected HttpURLConnection createConnection(String method, boolean isSSE) throws IOException {
+      URL urlObj;
+      try {
+        urlObj = new URI(url).toURL();
+      } catch (URISyntaxException e) {
+        throw new MalformedURLException("Invalid URL: " + url);
+      }
+      HttpURLConnection connection = (HttpURLConnection) urlObj.openConnection();
+      connection.setRequestMethod(method);
+      connection.setConnectTimeout(15000); // 15 seconds
+      connection.setReadTimeout(60000);    // 60 seconds
+      for (Map.Entry header : headers.entrySet()) {
+        connection.setRequestProperty(header.getKey(), header.getValue());
+      }
+      if (isSSE) {
+        connection.setRequestProperty("Accept", "text/event-stream");
+      }
+      return connection;
+    }
+
+    protected static String readStreamWithLimit(InputStream is) throws IOException {
+      if (is == null) {
+        return "";
+      }
+      int maxResponseSize = 10 * 1024 * 1024; // 10 MB
+      try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
+        StringBuilder sb = new StringBuilder();
+        String line;
+        boolean first = true;
+        while ((line = reader.readLine()) != null) {
+          if (sb.length() + line.length() > maxResponseSize) {
+            throw new IOException("Response size exceeds limit");
+          }
+          if (!first) {
+            sb.append('\n');
+          }
+          sb.append(line);
+          first = false;
+        }
+        return sb.toString();
+      }
+    }
+
+    protected A2AHttpResponse execute(HttpURLConnection connection) throws IOException {
+      int status = connection.getResponseCode();
+      String body = "";
+      try (InputStream is =
+          (status >= HTTP_OK && status < HTTP_MULT_CHOICE)
+              ? connection.getInputStream()
+              : connection.getErrorStream()) {
+        body = readStreamWithLimit(is);
+      }
+
+      if (status == HTTP_UNAUTHORIZED) {
+        throw new IOException(A2AErrorMessages.AUTHENTICATION_FAILED);
+      } else if (status == HTTP_FORBIDDEN) {
+        throw new IOException(A2AErrorMessages.AUTHORIZATION_FAILED);
+      }
+
+      return new AndroidHttpResponse(status, body);
+    }
+
+    protected void processSSEResponse(
+        HttpURLConnection connection,
+        Consumer messageConsumer,
+        Consumer errorConsumer,
+        Runnable completeRunnable) {
+      try {
+        int status = connection.getResponseCode();
+        if (status != HTTP_OK) {
+          if (status == HTTP_UNAUTHORIZED) {
+            errorConsumer.accept(new IOException(A2AErrorMessages.AUTHENTICATION_FAILED));
+            return;
+          } else if (status == HTTP_FORBIDDEN) {
+            errorConsumer.accept(new IOException(A2AErrorMessages.AUTHORIZATION_FAILED));
+            return;
+          }
+
+          String errorBody = "";
+          try (InputStream es = connection.getErrorStream()) {
+            errorBody = readStreamWithLimit(es);
+          }
+          errorConsumer.accept(
+              new IOException("Request failed with status " + status + ":" + errorBody));
+          return;
+        }
+
+        try (InputStream is = connection.getInputStream();
+            BufferedReader reader =
+                new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
+          String line;
+          while ((line = reader.readLine()) != null) {
+            if (line.startsWith("data:")) {
+              String data = line.substring(5).trim();
+              if (!data.isEmpty()) {
+                messageConsumer.accept(data);
+              }
+            }
+          }
+          completeRunnable.run();
+        }
+      } catch (Exception e) {
+        errorConsumer.accept(e);
+      } finally {
+        connection.disconnect();
+      }
+    }
+
+    protected CompletableFuture executeAsyncSSE(
+        HttpURLConnection connection,
+        Consumer messageConsumer,
+        Consumer errorConsumer,
+        Runnable completeRunnable) {
+      return CompletableFuture.runAsync(
+          () -> processSSEResponse(connection, messageConsumer, errorConsumer, completeRunnable),
+          NET_EXECUTOR);
+    }
+  }
+
+  private static class AndroidGetBuilder extends AndroidBuilder implements GetBuilder {
+    @Override
+    public A2AHttpResponse get() throws IOException {
+      HttpURLConnection connection = createConnection("GET", false);
+      try {
+        return execute(connection);
+      } catch (IOException e) {
+        connection.disconnect();
+        throw e;
+      }
+    }
+
+    @Override
+    public CompletableFuture getAsyncSSE(
+        Consumer messageConsumer,
+        Consumer errorConsumer,
+        Runnable completeRunnable)
+        throws IOException {
+      HttpURLConnection connection = createConnection("GET", true);
+      return executeAsyncSSE(connection, messageConsumer, errorConsumer, completeRunnable);
+    }
+  }
+
+  private static class AndroidPostBuilder extends AndroidBuilder
+      implements PostBuilder {
+    private String body = "";
+
+    @Override
+    public PostBuilder body(String body) {
+      this.body = body;
+      return this;
+    }
+
+    @Override
+    public A2AHttpResponse post() throws IOException {
+      HttpURLConnection connection = createConnection("POST", false);
+      connection.setDoOutput(true);
+      try {
+        try (OutputStream os = connection.getOutputStream()) {
+          os.write(body.getBytes(StandardCharsets.UTF_8));
+        }
+        return execute(connection);
+      } catch (IOException e) {
+        connection.disconnect();
+        throw e;
+      }
+    }
+
+    @Override
+    public CompletableFuture postAsyncSSE(
+        Consumer messageConsumer,
+        Consumer errorConsumer,
+        Runnable completeRunnable)
+        throws IOException {
+      HttpURLConnection connection = createConnection("POST", true);
+      connection.setDoOutput(true);
+      
+      return CompletableFuture.runAsync(
+          () -> {
+            try {
+              try (OutputStream os = connection.getOutputStream()) {
+                os.write(body.getBytes(StandardCharsets.UTF_8));
+              }
+              processSSEResponse(connection, messageConsumer, errorConsumer, completeRunnable);
+            } catch (Exception e) {
+              errorConsumer.accept(e);
+            }
+          }, NET_EXECUTOR);
+    }
+  }
+
+  private static class AndroidDeleteBuilder extends AndroidBuilder
+      implements DeleteBuilder {
+    @Override
+    public A2AHttpResponse delete() throws IOException {
+      HttpURLConnection connection = createConnection("DELETE", false);
+      try {
+        return execute(connection);
+      } catch (IOException e) {
+        connection.disconnect();
+        throw e;
+      }
+    }
+  }
+
+  private record AndroidHttpResponse(int status, String body) implements A2AHttpResponse {
+    @Override
+    public boolean success() {
+      return status >= HTTP_OK && status < HTTP_MULT_CHOICE;
+    }
+  }
+}
diff --git a/extras/http-client-android/src/main/java/io/a2a/client/http/AndroidA2AHttpClientProvider.java b/extras/http-client-android/src/main/java/io/a2a/client/http/AndroidA2AHttpClientProvider.java
new file mode 100644
index 000000000..1a0f8d372
--- /dev/null
+++ b/extras/http-client-android/src/main/java/io/a2a/client/http/AndroidA2AHttpClientProvider.java
@@ -0,0 +1,22 @@
+package io.a2a.client.http;
+
+/**
+ * Service provider for {@link AndroidA2AHttpClient}.
+ */
+public final class AndroidA2AHttpClientProvider implements A2AHttpClientProvider {
+
+    @Override
+    public A2AHttpClient create() {
+        return new AndroidA2AHttpClient();
+    }
+
+    @Override
+    public int priority() {
+        return 100; // Higher priority than JDK
+    }
+
+    @Override
+    public String name() {
+        return "android";
+    }
+}
diff --git a/extras/http-client-android/src/main/resources/META-INF/services/io.a2a.client.http.A2AHttpClientProvider b/extras/http-client-android/src/main/resources/META-INF/services/io.a2a.client.http.A2AHttpClientProvider
new file mode 100644
index 000000000..7829103c4
--- /dev/null
+++ b/extras/http-client-android/src/main/resources/META-INF/services/io.a2a.client.http.A2AHttpClientProvider
@@ -0,0 +1 @@
+io.a2a.client.http.AndroidA2AHttpClientProvider
diff --git a/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientFactoryTest.java b/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientFactoryTest.java
new file mode 100644
index 000000000..afa39584f
--- /dev/null
+++ b/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientFactoryTest.java
@@ -0,0 +1,41 @@
+package io.a2a.client.http;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import org.junit.jupiter.api.Test;
+
+public class AndroidA2AHttpClientFactoryTest {
+
+    @Test
+    public void testCreateReturnsAndroidClient() {
+        // When both JDK and Android are on classpath, Android should be preferred due to higher priority (100)
+        A2AHttpClient client = A2AHttpClientFactory.create();
+        assertNotNull(client);
+        assertInstanceOf(AndroidA2AHttpClient.class, client,
+            "Factory should return AndroidA2AHttpClient when Android provider is available");
+    }
+
+    @Test
+    public void testCreateWithAndroidProviderName() {
+        A2AHttpClient client = A2AHttpClientFactory.create("android");
+        assertNotNull(client);
+        assertInstanceOf(AndroidA2AHttpClient.class, client,
+            "Factory should return AndroidA2AHttpClient when 'android' provider is requested");
+    }
+
+    @Test
+    public void testAndroidClientIsUsable() {
+        A2AHttpClient client = A2AHttpClientFactory.create("android");
+        assertNotNull(client);
+
+        // Verify we can create builders
+        A2AHttpClient.GetBuilder getBuilder = client.createGet();
+        assertNotNull(getBuilder, "Should be able to create GET builder");
+
+        A2AHttpClient.PostBuilder postBuilder = client.createPost();
+        assertNotNull(postBuilder, "Should be able to create POST builder");
+
+        A2AHttpClient.DeleteBuilder deleteBuilder = client.createDelete();
+        assertNotNull(deleteBuilder, "Should be able to create DELETE builder");
+    }
+}
diff --git a/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientIntegrationTest.java b/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientIntegrationTest.java
new file mode 100644
index 000000000..b33e718ac
--- /dev/null
+++ b/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientIntegrationTest.java
@@ -0,0 +1,214 @@
+package io.a2a.client.http;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockserver.model.HttpRequest.request;
+import static org.mockserver.model.HttpResponse.response;
+
+import io.a2a.common.A2AErrorMessages;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockserver.integration.ClientAndServer;
+
+import java.io.IOException;
+
+public class AndroidA2AHttpClientIntegrationTest {
+
+    private ClientAndServer mockServer;
+    private AndroidA2AHttpClient client;
+
+    @BeforeEach
+    public void setup() {
+        mockServer = ClientAndServer.startClientAndServer(0);  // Use random port
+        client = new AndroidA2AHttpClient();
+    }
+
+    @AfterEach
+    public void teardown() {
+        if (mockServer != null) {
+            mockServer.stop();
+        }
+    }
+
+    private String getBaseUrl() {
+        return "http://localhost:" + mockServer.getPort();
+    }
+
+    @Test
+    public void testGetRequestSuccess() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/test"))
+                .respond(response().withStatusCode(200).withBody("success"));
+
+        A2AHttpResponse response = client.createGet()
+                .url(getBaseUrl() + "/test")
+                .get();
+
+        assertEquals(200, response.status());
+        assertTrue(response.success());
+        assertEquals("success", response.body());
+    }
+
+    @Test
+    public void testPostRequestSuccess() throws Exception {
+        mockServer
+                .when(request()
+                        .withMethod("POST")
+                        .withPath("/test")
+                        .withBody("{\"key\":\"value\"}"))
+                .respond(response().withStatusCode(201).withBody("created"));
+
+        A2AHttpResponse response = client.createPost()
+                .url(getBaseUrl() + "/test")
+                .body("{\"key\":\"value\"}")
+                .post();
+
+        assertEquals(201, response.status());
+        assertTrue(response.success());
+        assertEquals("created", response.body());
+    }
+
+    @Test
+    public void testDeleteRequestSuccess() throws Exception {
+        mockServer
+                .when(request().withMethod("DELETE").withPath("/test"))
+                .respond(response().withStatusCode(204));
+
+        A2AHttpResponse response = client.createDelete()
+                .url(getBaseUrl() + "/test")
+                .delete();
+
+        assertEquals(204, response.status());
+        assertTrue(response.success());
+    }
+
+    @Test
+    public void test401AuthenticationErrorOnGet() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/test"))
+                .respond(response().withStatusCode(401));
+
+        Exception exception = assertThrows(IOException.class, () -> {
+            client.createGet()
+                    .url(getBaseUrl() + "/test")
+                    .get();
+        });
+
+        assertEquals(A2AErrorMessages.AUTHENTICATION_FAILED, exception.getMessage());
+    }
+
+    @Test
+    public void test403AuthorizationErrorOnGet() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/test"))
+                .respond(response().withStatusCode(403));
+
+        Exception exception = assertThrows(IOException.class, () -> {
+            client.createGet()
+                    .url(getBaseUrl() + "/test")
+                    .get();
+        });
+
+        assertEquals(A2AErrorMessages.AUTHORIZATION_FAILED, exception.getMessage());
+    }
+
+    @Test
+    public void test401AuthenticationErrorOnPost() throws Exception {
+        mockServer
+                .when(request().withMethod("POST").withPath("/test"))
+                .respond(response().withStatusCode(401));
+
+        Exception exception = assertThrows(IOException.class, () -> {
+            client.createPost()
+                    .url(getBaseUrl() + "/test")
+                    .body("{}")
+                    .post();
+        });
+
+        assertEquals(A2AErrorMessages.AUTHENTICATION_FAILED, exception.getMessage());
+    }
+
+    @Test
+    public void test403AuthorizationErrorOnPost() throws Exception {
+        mockServer
+                .when(request().withMethod("POST").withPath("/test"))
+                .respond(response().withStatusCode(403));
+
+        Exception exception = assertThrows(IOException.class, () -> {
+            client.createPost()
+                    .url(getBaseUrl() + "/test")
+                    .body("{}")
+                    .post();
+        });
+
+        assertEquals(A2AErrorMessages.AUTHORIZATION_FAILED, exception.getMessage());
+    }
+
+    @Test
+    public void test401AuthenticationErrorOnDelete() throws Exception {
+        mockServer
+                .when(request().withMethod("DELETE").withPath("/test"))
+                .respond(response().withStatusCode(401));
+
+        Exception exception = assertThrows(IOException.class, () -> {
+            client.createDelete()
+                    .url(getBaseUrl() + "/test")
+                    .delete();
+        });
+
+        assertEquals(A2AErrorMessages.AUTHENTICATION_FAILED, exception.getMessage());
+    }
+
+    @Test
+    public void testHeaderPropagation() throws Exception {
+        mockServer
+                .when(request()
+                        .withMethod("GET")
+                        .withPath("/test")
+                        .withHeader("Authorization", "Bearer token")
+                        .withHeader("X-Custom-Header", "custom-value"))
+                .respond(response().withStatusCode(200).withBody("ok"));
+
+        A2AHttpResponse response = client.createGet()
+                .url(getBaseUrl() + "/test")
+                .addHeader("Authorization", "Bearer token")
+                .addHeader("X-Custom-Header", "custom-value")
+                .get();
+
+        assertEquals(200, response.status());
+        assertEquals("ok", response.body());
+    }
+
+    @Test
+    public void testNonSuccessStatusCode() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/test"))
+                .respond(response().withStatusCode(500).withBody("Internal Server Error"));
+
+        A2AHttpResponse response = client.createGet()
+                .url(getBaseUrl() + "/test")
+                .get();
+
+        assertEquals(500, response.status());
+        assertFalse(response.success());
+        assertEquals("Internal Server Error", response.body());
+    }
+
+    @Test
+    public void test404NotFound() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/test"))
+                .respond(response().withStatusCode(404).withBody("Not Found"));
+
+        A2AHttpResponse response = client.createGet()
+                .url(getBaseUrl() + "/test")
+                .get();
+
+        assertEquals(404, response.status());
+        assertFalse(response.success());
+        assertEquals("Not Found", response.body());
+    }
+}
diff --git a/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientSSETest.java b/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientSSETest.java
new file mode 100644
index 000000000..b1422aff0
--- /dev/null
+++ b/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientSSETest.java
@@ -0,0 +1,254 @@
+package io.a2a.client.http;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockserver.model.HttpRequest.request;
+import static org.mockserver.model.HttpResponse.response;
+
+import io.a2a.common.A2AErrorMessages;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockserver.integration.ClientAndServer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class AndroidA2AHttpClientSSETest {
+
+    private ClientAndServer mockServer;
+    private AndroidA2AHttpClient client;
+
+    @BeforeEach
+    public void setup() {
+        mockServer = ClientAndServer.startClientAndServer(0);  // Use random port
+        client = new AndroidA2AHttpClient();
+    }
+
+    @AfterEach
+    public void teardown() {
+        if (mockServer != null) {
+            mockServer.stop();
+        }
+    }
+
+    private String getBaseUrl() {
+        return "http://localhost:" + mockServer.getPort();
+    }
+
+    @Test
+    public void testGetAsyncSSE() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/sse"))
+                .respond(response()
+                        .withStatusCode(200)
+                        .withHeader("Content-Type", "text/event-stream")
+                        .withBody("data: event1\n\ndata: event2\n\ndata: event3\n\n"));
+
+        CountDownLatch latch = new CountDownLatch(1);
+        List events = new ArrayList<>();
+        AtomicReference error = new AtomicReference<>();
+
+        client.createGet()
+                .url(getBaseUrl() + "/sse")
+                .getAsyncSSE(
+                        events::add,
+                        error::set,
+                        latch::countDown
+                );
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS), "Expected completion handler to be called");
+        assertNull(error.get(), "Expected no errors");
+        assertEquals(3, events.size(), "Expected to receive 3 events");
+        assertTrue(events.contains("event1"));
+        assertTrue(events.contains("event2"));
+        assertTrue(events.contains("event3"));
+    }
+
+    @Test
+    public void testPostAsyncSSE() throws Exception {
+        mockServer
+                .when(request()
+                        .withMethod("POST")
+                        .withPath("/sse")
+                        .withBody("{\"subscribe\":true}"))
+                .respond(response()
+                        .withStatusCode(200)
+                        .withHeader("Content-Type", "text/event-stream")
+                        .withBody("data: message1\n\ndata: message2\n\n"));
+
+        CountDownLatch latch = new CountDownLatch(1);
+        List events = new ArrayList<>();
+        AtomicReference error = new AtomicReference<>();
+
+        client.createPost()
+                .url(getBaseUrl() + "/sse")
+                .body("{\"subscribe\":true}")
+                .postAsyncSSE(
+                        events::add,
+                        error::set,
+                        latch::countDown
+                );
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS), "Expected completion handler to be called");
+        assertNull(error.get(), "Expected no errors");
+        assertEquals(2, events.size(), "Expected to receive 2 events");
+        assertTrue(events.contains("message1"));
+        assertTrue(events.contains("message2"));
+    }
+
+    @Test
+    public void testSSEDataPrefixStripping() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/sse"))
+                .respond(response()
+                        .withStatusCode(200)
+                        .withHeader("Content-Type", "text/event-stream")
+                        .withBody("data: content here\n\ndata:no space\n\ndata: extra spaces  \n\n"));
+
+        CountDownLatch latch = new CountDownLatch(1);
+        List events = new ArrayList<>();
+        AtomicReference error = new AtomicReference<>();
+
+        client.createGet()
+                .url(getBaseUrl() + "/sse")
+                .getAsyncSSE(
+                        events::add,
+                        error::set,
+                        latch::countDown
+                );
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+        assertNull(error.get());
+        assertTrue(events.contains("content here"), "Should have stripped 'data: ' prefix");
+        assertTrue(events.contains("no space"), "Should handle 'data:' without space");
+        assertTrue(events.contains("extra spaces"), "Should trim whitespace");
+    }
+
+    @Test
+    public void testSSEAuthenticationError() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/sse"))
+                .respond(response().withStatusCode(401));
+
+        CountDownLatch errorLatch = new CountDownLatch(1);
+        AtomicReference error = new AtomicReference<>();
+        AtomicBoolean completed = new AtomicBoolean(false);
+
+        client.createGet()
+                .url(getBaseUrl() + "/sse")
+                .getAsyncSSE(
+                        msg -> {},
+                        e -> {
+                            error.set(e);
+                            errorLatch.countDown();
+                        },
+                        () -> completed.set(true)
+                );
+
+        assertTrue(errorLatch.await(5, TimeUnit.SECONDS), "Expected error handler to be called");
+        assertNotNull(error.get(), "Expected an error");
+        assertTrue(error.get() instanceof IOException, "Expected IOException");
+        assertTrue(error.get().getMessage().contains(A2AErrorMessages.AUTHENTICATION_FAILED),
+            "Expected authentication error message but got: " + error.get().getMessage());
+        assertFalse(completed.get(), "Should not call completion handler on error");
+    }
+
+    @Test
+    public void testSSEAuthorizationError() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/sse"))
+                .respond(response().withStatusCode(403));
+
+        CountDownLatch errorLatch = new CountDownLatch(1);
+        AtomicReference error = new AtomicReference<>();
+        AtomicBoolean completed = new AtomicBoolean(false);
+
+        client.createGet()
+                .url(getBaseUrl() + "/sse")
+                .getAsyncSSE(
+                        msg -> {},
+                        e -> {
+                            error.set(e);
+                            errorLatch.countDown();
+                        },
+                        () -> completed.set(true)
+                );
+
+        assertTrue(errorLatch.await(5, TimeUnit.SECONDS), "Expected error handler to be called");
+        assertNotNull(error.get(), "Expected an error");
+        assertTrue(error.get() instanceof IOException, "Expected IOException");
+        assertTrue(error.get().getMessage().contains(A2AErrorMessages.AUTHORIZATION_FAILED),
+            "Expected authorization error message but got: " + error.get().getMessage());
+        assertFalse(completed.get(), "Should not call completion handler on error");
+    }
+
+    @Test
+    public void testSSEEmptyLinesIgnored() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/sse"))
+                .respond(response()
+                        .withStatusCode(200)
+                        .withHeader("Content-Type", "text/event-stream")
+                        .withBody("data: first\n\n\n\ndata: second\n\ndata: \n\ndata: third\n\n"));
+
+        CountDownLatch latch = new CountDownLatch(1);
+        List events = new ArrayList<>();
+        AtomicReference error = new AtomicReference<>();
+
+        client.createGet()
+                .url(getBaseUrl() + "/sse")
+                .getAsyncSSE(
+                        events::add,
+                        error::set,
+                        latch::countDown
+                );
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+        assertNull(error.get());
+        assertEquals(3, events.size(), "Should have received 3 non-empty events");
+        assertTrue(events.contains("first"));
+        assertTrue(events.contains("second"));
+        assertTrue(events.contains("third"));
+    }
+
+    @Test
+    public void testSSEHeaderPropagation() throws Exception {
+        mockServer
+                .when(request()
+                        .withMethod("GET")
+                        .withPath("/sse")
+                        .withHeader("Accept", "text/event-stream")
+                        .withHeader("Authorization", "Bearer token"))
+                .respond(response()
+                        .withStatusCode(200)
+                        .withHeader("Content-Type", "text/event-stream")
+                        .withBody("data: authenticated\n\n"));
+
+        CountDownLatch latch = new CountDownLatch(1);
+        List events = new ArrayList<>();
+        AtomicReference error = new AtomicReference<>();
+
+        client.createGet()
+                .url(getBaseUrl() + "/sse")
+                .addHeader("Authorization", "Bearer token")
+                .getAsyncSSE(
+                        events::add,
+                        error::set,
+                        latch::countDown
+                );
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+        assertNull(error.get());
+        assertTrue(events.contains("authenticated"));
+    }
+}
diff --git a/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientTest.java b/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientTest.java
new file mode 100644
index 000000000..e611fa541
--- /dev/null
+++ b/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientTest.java
@@ -0,0 +1,59 @@
+package io.a2a.client.http;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import org.junit.jupiter.api.Test;
+
+public class AndroidA2AHttpClientTest {
+
+    @Test
+    public void testNoArgsConstructor() {
+        AndroidA2AHttpClient client = new AndroidA2AHttpClient();
+        assertNotNull(client);
+    }
+
+    @Test
+    public void testCreateGet() {
+        AndroidA2AHttpClient client = new AndroidA2AHttpClient();
+        A2AHttpClient.GetBuilder builder = client.createGet();
+        assertNotNull(builder);
+    }
+
+    @Test
+    public void testCreatePost() {
+        AndroidA2AHttpClient client = new AndroidA2AHttpClient();
+        A2AHttpClient.PostBuilder builder = client.createPost();
+        assertNotNull(builder);
+    }
+
+    @Test
+    public void testCreateDelete() {
+        AndroidA2AHttpClient client = new AndroidA2AHttpClient();
+        A2AHttpClient.DeleteBuilder builder = client.createDelete();
+        assertNotNull(builder);
+    }
+
+    @Test
+    public void testBuilderUrlSetting() {
+        AndroidA2AHttpClient client = new AndroidA2AHttpClient();
+        A2AHttpClient.GetBuilder builder = client.createGet();
+        A2AHttpClient.GetBuilder result = builder.url("https://example.com");
+        assertSame(builder, result, "Builder should return itself for method chaining");
+    }
+
+    @Test
+    public void testBuilderHeaderSetting() {
+        AndroidA2AHttpClient client = new AndroidA2AHttpClient();
+        A2AHttpClient.GetBuilder builder = client.createGet();
+        A2AHttpClient.GetBuilder result = builder.addHeader("Accept", "application/json");
+        assertSame(builder, result, "Builder should return itself for method chaining");
+    }
+
+    @Test
+    public void testPostBuilderBody() {
+        AndroidA2AHttpClient client = new AndroidA2AHttpClient();
+        A2AHttpClient.PostBuilder builder = client.createPost();
+        A2AHttpClient.PostBuilder result = builder.body("{\"key\":\"value\"}");
+        assertSame(builder, result, "Builder should return itself for method chaining");
+    }
+}
diff --git a/http-client/src/main/java/io/a2a/client/http/A2ACardResolver.java b/http-client/src/main/java/io/a2a/client/http/A2ACardResolver.java
index 22af7c615..4d4449e13 100644
--- a/http-client/src/main/java/io/a2a/client/http/A2ACardResolver.java
+++ b/http-client/src/main/java/io/a2a/client/http/A2ACardResolver.java
@@ -21,20 +21,23 @@ public class A2ACardResolver {
 
     /**
      * Get the agent card for an A2A agent.
-     * The {@code JdkA2AHttpClient} will be used to fetch the agent card.
+     * The {@link A2AHttpClientFactory#create()} will be used to fetch the agent
+     * card if available.
      *
-     * @param baseUrl the base URL for the agent whose agent card we want to retrieve
+     * @param baseUrl the base URL for the agent whose agent card we want to
+     *                retrieve
      * @throws A2AClientError if the URL for the agent is invalid
      */
     public A2ACardResolver(String baseUrl) throws A2AClientError {
-        this(new JdkA2AHttpClient(), baseUrl, null, null);
+        this(A2AHttpClientFactory.create(), baseUrl, null, null);
     }
 
     /**
      * Constructs an A2ACardResolver with a specific HTTP client and base URL.
      *
      * @param httpClient the http client to use
-     * @param baseUrl the base URL for the agent whose agent card we want to retrieve
+     * @param baseUrl    the base URL for the agent whose agent card we want to
+     *                   retrieve
      * @throws A2AClientError if the URL for the agent is invalid
      */
     public A2ACardResolver(A2AHttpClient httpClient, String baseUrl) throws A2AClientError {
@@ -42,10 +45,12 @@ public A2ACardResolver(A2AHttpClient httpClient, String baseUrl) throws A2AClien
     }
 
     /**
-     * @param httpClient the http client to use
-     * @param baseUrl the base URL for the agent whose agent card we want to retrieve
-     * @param agentCardPath optional path to the agent card endpoint relative to the base
-     *                         agent URL, defaults to ".well-known/agent-card.json"
+     * @param httpClient    the http client to use
+     * @param baseUrl       the base URL for the agent whose agent card we want to
+     *                      retrieve
+     * @param agentCardPath optional path to the agent card endpoint relative to the
+     *                      base
+     *                      agent URL, defaults to ".well-known/agent-card.json"
      * @throws A2AClientError if the URL for the agent is invalid
      */
     public A2ACardResolver(A2AHttpClient httpClient, String baseUrl, String agentCardPath) throws A2AClientError {
@@ -53,17 +58,21 @@ public A2ACardResolver(A2AHttpClient httpClient, String baseUrl, String agentCar
     }
 
     /**
-     * @param httpClient the http client to use
-     * @param baseUrl the base URL for the agent whose agent card we want to retrieve
-     * @param agentCardPath optional path to the agent card endpoint relative to the base
-     *                         agent URL, defaults to ".well-known/agent-card.json"
-     * @param authHeaders the HTTP authentication headers to use. May be {@code null}
+     * @param httpClient    the http client to use
+     * @param baseUrl       the base URL for the agent whose agent card we want to
+     *                      retrieve
+     * @param agentCardPath optional path to the agent card endpoint relative to the
+     *                      base
+     *                      agent URL, defaults to ".well-known/agent-card.json"
+     * @param authHeaders   the HTTP authentication headers to use. May be
+     *                      {@code null}
      * @throws A2AClientError if the URL for the agent is invalid
      */
     public A2ACardResolver(A2AHttpClient httpClient, String baseUrl, @Nullable String agentCardPath,
-                           @Nullable Map authHeaders) throws A2AClientError {
+            @Nullable Map authHeaders) throws A2AClientError {
         this.httpClient = httpClient;
-        String effectiveAgentCardPath = agentCardPath == null || agentCardPath.isEmpty() ? DEFAULT_AGENT_CARD_PATH : agentCardPath;
+        String effectiveAgentCardPath = agentCardPath == null || agentCardPath.isEmpty() ? DEFAULT_AGENT_CARD_PATH
+                : agentCardPath;
         try {
             this.url = new URI(baseUrl).resolve(effectiveAgentCardPath).toString();
         } catch (URISyntaxException e) {
@@ -76,8 +85,9 @@ public A2ACardResolver(A2AHttpClient httpClient, String baseUrl, @Nullable Strin
      * Get the agent card for the configured A2A agent.
      *
      * @return the agent card
-     * @throws A2AClientError If an HTTP error occurs fetching the card
-     * @throws A2AClientJSONError If the response body cannot be decoded as JSON or validated against the AgentCard schema
+     * @throws A2AClientError     If an HTTP error occurs fetching the card
+     * @throws A2AClientJSONError If the response body cannot be decoded as JSON or
+     *                            validated against the AgentCard schema
      */
     public AgentCard getAgentCard() throws A2AClientError, A2AClientJSONError {
         A2AHttpClient.GetBuilder builder = httpClient.createGet()
@@ -109,5 +119,4 @@ public AgentCard getAgentCard() throws A2AClientError, A2AClientJSONError {
 
     }
 
-
 }
diff --git a/http-client/src/main/java/io/a2a/client/http/A2AHttpClientFactory.java b/http-client/src/main/java/io/a2a/client/http/A2AHttpClientFactory.java
new file mode 100644
index 000000000..0ea791b6e
--- /dev/null
+++ b/http-client/src/main/java/io/a2a/client/http/A2AHttpClientFactory.java
@@ -0,0 +1,52 @@
+package io.a2a.client.http;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.ServiceLoader;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+/**
+ * Factory for creating {@link A2AHttpClient} instances using the ServiceLoader mechanism.
+ */
+public final class A2AHttpClientFactory {
+
+    private static final List PROVIDERS;
+
+    static {
+        ServiceLoader loader = ServiceLoader.load(A2AHttpClientProvider.class);
+        PROVIDERS = StreamSupport.stream(loader.spliterator(), false)
+                .collect(Collectors.toList());
+    }
+
+    private A2AHttpClientFactory() {
+        // Utility class
+    }
+
+    /**
+     * Creates a new A2AHttpClient instance using the highest priority provider available.
+     * If no providers are found, it throws an {@link IllegalStateException}.
+     */
+    public static A2AHttpClient create() {
+        return PROVIDERS.stream()
+                .max(Comparator.comparingInt(A2AHttpClientProvider::priority))
+                .map(A2AHttpClientProvider::create)
+                .orElseThrow(() -> new IllegalStateException("No A2AHttpClientProvider found"));
+    }
+
+    /**
+     * Creates a new A2AHttpClient instance using a specific provider by name.
+     */
+    public static A2AHttpClient create(String providerName) {
+        if (providerName == null || providerName.isEmpty()) {
+            throw new IllegalArgumentException("Provider name must not be null or empty");
+        }
+
+        return PROVIDERS.stream()
+                .filter(provider -> providerName.equals(provider.name()))
+                .findFirst()
+                .map(A2AHttpClientProvider::create)
+                .orElseThrow(() -> new IllegalArgumentException(
+                        "No A2AHttpClientProvider found with name: " + providerName));
+    }
+}
diff --git a/http-client/src/main/java/io/a2a/client/http/A2AHttpClientProvider.java b/http-client/src/main/java/io/a2a/client/http/A2AHttpClientProvider.java
new file mode 100644
index 000000000..0ededf4b6
--- /dev/null
+++ b/http-client/src/main/java/io/a2a/client/http/A2AHttpClientProvider.java
@@ -0,0 +1,23 @@
+package io.a2a.client.http;
+
+/**
+ * Provider interface for creating {@link A2AHttpClient} instances.
+ */
+public interface A2AHttpClientProvider {
+    /**
+     * Creates a new A2AHttpClient instance.
+     */
+    A2AHttpClient create();
+
+    /**
+     * Returns the priority of this provider. Higher priority providers are preferred.
+     */
+    default int priority() {
+        return 0;
+    }
+
+    /**
+     * Returns the name of this provider.
+     */
+    String name();
+}
diff --git a/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClientProvider.java b/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClientProvider.java
new file mode 100644
index 000000000..b512e49ef
--- /dev/null
+++ b/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClientProvider.java
@@ -0,0 +1,22 @@
+package io.a2a.client.http;
+
+/**
+ * Service provider for {@link JdkA2AHttpClient}.
+ */
+public final class JdkA2AHttpClientProvider implements A2AHttpClientProvider {
+
+    @Override
+    public A2AHttpClient create() {
+        return new JdkA2AHttpClient();
+    }
+
+    @Override
+    public int priority() {
+        return 0; // Lowest priority - fallback
+    }
+
+    @Override
+    public String name() {
+        return "jdk";
+    }
+}
diff --git a/http-client/src/main/resources/META-INF/services/io.a2a.client.http.A2AHttpClientProvider b/http-client/src/main/resources/META-INF/services/io.a2a.client.http.A2AHttpClientProvider
new file mode 100644
index 000000000..78dbb361e
--- /dev/null
+++ b/http-client/src/main/resources/META-INF/services/io.a2a.client.http.A2AHttpClientProvider
@@ -0,0 +1 @@
+io.a2a.client.http.JdkA2AHttpClientProvider
diff --git a/http-client/src/test/java/io/a2a/client/http/A2ACardResolverTest.java b/http-client/src/test/java/io/a2a/client/http/A2ACardResolverTest.java
index 9c2a177ec..3acad3b4f 100644
--- a/http-client/src/test/java/io/a2a/client/http/A2ACardResolverTest.java
+++ b/http-client/src/test/java/io/a2a/client/http/A2ACardResolverTest.java
@@ -174,4 +174,9 @@ public GetBuilder addHeaders(Map headers) {
         }
     }
 
+    @Test
+    public void testFactoryCreate() {
+        A2AHttpClient client = A2AHttpClientFactory.create();
+        assertTrue(client instanceof JdkA2AHttpClient);
+    }
 }
diff --git a/pom.xml b/pom.xml
index 83cff974b..f949c7804 100644
--- a/pom.xml
+++ b/pom.xml
@@ -454,6 +454,7 @@
         extras/task-store-database-jpa
         extras/push-notification-config-store-database-jpa
         extras/queue-manager-replicated
+        extras/http-client-android
         http-client
         integrations/microprofile-config
         reference/common
diff --git a/server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java b/server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java
index 9601e6b79..ca9092890 100644
--- a/server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java
+++ b/server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java
@@ -10,11 +10,9 @@
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 
-import io.a2a.json.JsonProcessingException;
-
 import io.a2a.client.http.A2AHttpClient;
-import io.a2a.client.http.JdkA2AHttpClient;
 import io.a2a.json.JsonUtil;
+import io.a2a.client.http.A2AHttpClientFactory;
 import io.a2a.spec.PushNotificationConfig;
 import io.a2a.spec.Task;
 
@@ -31,8 +29,7 @@ public class BasePushNotificationSender implements PushNotificationSender {
 
     @Inject
     public BasePushNotificationSender(PushNotificationConfigStore configStore) {
-        this.httpClient = new JdkA2AHttpClient();
-        this.configStore = configStore;
+        this(configStore, A2AHttpClientFactory.create());
     }
 
     public BasePushNotificationSender(PushNotificationConfigStore configStore, A2AHttpClient httpClient) {
@@ -56,11 +53,12 @@ public void sendNotification(Task task) {
                 .allMatch(CompletableFuture::join));
         try {
             boolean allSent = dispatchResult.get();
-            if (! allSent) {
+            if (!allSent) {
                 LOGGER.warn("Some push notifications failed to send for taskId: " + task.getId());
             }
         } catch (InterruptedException | ExecutionException e) {
-            LOGGER.warn("Some push notifications failed to send for taskId " + task.getId() + ": {}", e.getMessage(), e);
+            LOGGER.warn("Some push notifications failed to send for taskId " + task.getId() + ": {}", e.getMessage(),
+                    e);
         }
     }
 

From 527927eaec7cce23995b6968d1ea0e67116c33ca Mon Sep 17 00:00:00 2001
From: Emmanuel Hugonnet 
Date: Tue, 28 Apr 2026 17:41:54 +0200
Subject: [PATCH 31/37] test: unifying testing across client implementation

Signed-off-by: Emmanuel Hugonnet 
---
 .../JSONRPCTransportConfigBuilder.java        |   1 -
 .../rest/RestTransportConfigBuilder.java      |   1 -
 extras/http-client-android/pom.xml            |   6 +
 .../AndroidA2AHttpClientIntegrationTest.java  | 213 +--------------
 .../http/AndroidA2AHttpClientSSETest.java     | 253 +----------------
 .../http/JdkA2AHttpClientIntegrationTest.java |   9 +
 .../client/http/JdkA2AHttpClientSSETest.java  |   9 +
 http-client/pom.xml                           |  20 ++
 .../io/a2a/client/http/JdkA2AHttpClient.java  |  16 +-
 .../AbstractA2AHttpClientIntegrationTest.java | 216 +++++++++++++++
 .../http/AbstractA2AHttpClientSSETest.java    | 255 ++++++++++++++++++
 .../http/JdkA2AHttpClientIntegrationTest.java |   9 +
 .../client/http/JdkA2AHttpClientSSETest.java  |   9 +
 pom.xml                                       |   7 +
 14 files changed, 559 insertions(+), 465 deletions(-)
 create mode 100644 extras/http-client-android/src/test/java/io/a2a/client/http/JdkA2AHttpClientIntegrationTest.java
 create mode 100644 extras/http-client-android/src/test/java/io/a2a/client/http/JdkA2AHttpClientSSETest.java
 create mode 100644 http-client/src/test/java/io/a2a/client/http/AbstractA2AHttpClientIntegrationTest.java
 create mode 100644 http-client/src/test/java/io/a2a/client/http/AbstractA2AHttpClientSSETest.java
 create mode 100644 http-client/src/test/java/io/a2a/client/http/JdkA2AHttpClientIntegrationTest.java
 create mode 100644 http-client/src/test/java/io/a2a/client/http/JdkA2AHttpClientSSETest.java

diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportConfigBuilder.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportConfigBuilder.java
index 52d7e7f8c..ef27ac43c 100644
--- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportConfigBuilder.java
+++ b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportConfigBuilder.java
@@ -2,7 +2,6 @@
 
 import io.a2a.client.http.A2AHttpClient;
 import io.a2a.client.http.A2AHttpClientFactory;
-import io.a2a.client.http.JdkA2AHttpClient;
 import io.a2a.client.transport.spi.ClientTransportConfigBuilder;
 
 public class JSONRPCTransportConfigBuilder extends ClientTransportConfigBuilder {
diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportConfigBuilder.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportConfigBuilder.java
index 96a01740a..01d97f27c 100644
--- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportConfigBuilder.java
+++ b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportConfigBuilder.java
@@ -2,7 +2,6 @@
 
 import io.a2a.client.http.A2AHttpClient;
 import io.a2a.client.http.A2AHttpClientFactory;
-import io.a2a.client.http.JdkA2AHttpClient;
 import io.a2a.client.transport.spi.ClientTransportConfigBuilder;
 import org.jspecify.annotations.Nullable;
 
diff --git a/extras/http-client-android/pom.xml b/extras/http-client-android/pom.xml
index 1b86001aa..0b87a5466 100644
--- a/extras/http-client-android/pom.xml
+++ b/extras/http-client-android/pom.xml
@@ -26,6 +26,12 @@
             a2a-java-sdk-spec
         
 
+        
+            ${project.groupId}
+            a2a-java-sdk-http-client
+            test-jar
+            test
+        
         
             org.junit.jupiter
             junit-jupiter-api
diff --git a/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientIntegrationTest.java b/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientIntegrationTest.java
index b33e718ac..b5a7dac85 100644
--- a/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientIntegrationTest.java
+++ b/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientIntegrationTest.java
@@ -1,214 +1,9 @@
 package io.a2a.client.http;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockserver.model.HttpRequest.request;
-import static org.mockserver.model.HttpResponse.response;
+public class AndroidA2AHttpClientIntegrationTest extends AbstractA2AHttpClientIntegrationTest {
 
-import io.a2a.common.A2AErrorMessages;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.mockserver.integration.ClientAndServer;
-
-import java.io.IOException;
-
-public class AndroidA2AHttpClientIntegrationTest {
-
-    private ClientAndServer mockServer;
-    private AndroidA2AHttpClient client;
-
-    @BeforeEach
-    public void setup() {
-        mockServer = ClientAndServer.startClientAndServer(0);  // Use random port
-        client = new AndroidA2AHttpClient();
-    }
-
-    @AfterEach
-    public void teardown() {
-        if (mockServer != null) {
-            mockServer.stop();
-        }
-    }
-
-    private String getBaseUrl() {
-        return "http://localhost:" + mockServer.getPort();
-    }
-
-    @Test
-    public void testGetRequestSuccess() throws Exception {
-        mockServer
-                .when(request().withMethod("GET").withPath("/test"))
-                .respond(response().withStatusCode(200).withBody("success"));
-
-        A2AHttpResponse response = client.createGet()
-                .url(getBaseUrl() + "/test")
-                .get();
-
-        assertEquals(200, response.status());
-        assertTrue(response.success());
-        assertEquals("success", response.body());
-    }
-
-    @Test
-    public void testPostRequestSuccess() throws Exception {
-        mockServer
-                .when(request()
-                        .withMethod("POST")
-                        .withPath("/test")
-                        .withBody("{\"key\":\"value\"}"))
-                .respond(response().withStatusCode(201).withBody("created"));
-
-        A2AHttpResponse response = client.createPost()
-                .url(getBaseUrl() + "/test")
-                .body("{\"key\":\"value\"}")
-                .post();
-
-        assertEquals(201, response.status());
-        assertTrue(response.success());
-        assertEquals("created", response.body());
-    }
-
-    @Test
-    public void testDeleteRequestSuccess() throws Exception {
-        mockServer
-                .when(request().withMethod("DELETE").withPath("/test"))
-                .respond(response().withStatusCode(204));
-
-        A2AHttpResponse response = client.createDelete()
-                .url(getBaseUrl() + "/test")
-                .delete();
-
-        assertEquals(204, response.status());
-        assertTrue(response.success());
-    }
-
-    @Test
-    public void test401AuthenticationErrorOnGet() throws Exception {
-        mockServer
-                .when(request().withMethod("GET").withPath("/test"))
-                .respond(response().withStatusCode(401));
-
-        Exception exception = assertThrows(IOException.class, () -> {
-            client.createGet()
-                    .url(getBaseUrl() + "/test")
-                    .get();
-        });
-
-        assertEquals(A2AErrorMessages.AUTHENTICATION_FAILED, exception.getMessage());
-    }
-
-    @Test
-    public void test403AuthorizationErrorOnGet() throws Exception {
-        mockServer
-                .when(request().withMethod("GET").withPath("/test"))
-                .respond(response().withStatusCode(403));
-
-        Exception exception = assertThrows(IOException.class, () -> {
-            client.createGet()
-                    .url(getBaseUrl() + "/test")
-                    .get();
-        });
-
-        assertEquals(A2AErrorMessages.AUTHORIZATION_FAILED, exception.getMessage());
-    }
-
-    @Test
-    public void test401AuthenticationErrorOnPost() throws Exception {
-        mockServer
-                .when(request().withMethod("POST").withPath("/test"))
-                .respond(response().withStatusCode(401));
-
-        Exception exception = assertThrows(IOException.class, () -> {
-            client.createPost()
-                    .url(getBaseUrl() + "/test")
-                    .body("{}")
-                    .post();
-        });
-
-        assertEquals(A2AErrorMessages.AUTHENTICATION_FAILED, exception.getMessage());
-    }
-
-    @Test
-    public void test403AuthorizationErrorOnPost() throws Exception {
-        mockServer
-                .when(request().withMethod("POST").withPath("/test"))
-                .respond(response().withStatusCode(403));
-
-        Exception exception = assertThrows(IOException.class, () -> {
-            client.createPost()
-                    .url(getBaseUrl() + "/test")
-                    .body("{}")
-                    .post();
-        });
-
-        assertEquals(A2AErrorMessages.AUTHORIZATION_FAILED, exception.getMessage());
-    }
-
-    @Test
-    public void test401AuthenticationErrorOnDelete() throws Exception {
-        mockServer
-                .when(request().withMethod("DELETE").withPath("/test"))
-                .respond(response().withStatusCode(401));
-
-        Exception exception = assertThrows(IOException.class, () -> {
-            client.createDelete()
-                    .url(getBaseUrl() + "/test")
-                    .delete();
-        });
-
-        assertEquals(A2AErrorMessages.AUTHENTICATION_FAILED, exception.getMessage());
-    }
-
-    @Test
-    public void testHeaderPropagation() throws Exception {
-        mockServer
-                .when(request()
-                        .withMethod("GET")
-                        .withPath("/test")
-                        .withHeader("Authorization", "Bearer token")
-                        .withHeader("X-Custom-Header", "custom-value"))
-                .respond(response().withStatusCode(200).withBody("ok"));
-
-        A2AHttpResponse response = client.createGet()
-                .url(getBaseUrl() + "/test")
-                .addHeader("Authorization", "Bearer token")
-                .addHeader("X-Custom-Header", "custom-value")
-                .get();
-
-        assertEquals(200, response.status());
-        assertEquals("ok", response.body());
-    }
-
-    @Test
-    public void testNonSuccessStatusCode() throws Exception {
-        mockServer
-                .when(request().withMethod("GET").withPath("/test"))
-                .respond(response().withStatusCode(500).withBody("Internal Server Error"));
-
-        A2AHttpResponse response = client.createGet()
-                .url(getBaseUrl() + "/test")
-                .get();
-
-        assertEquals(500, response.status());
-        assertFalse(response.success());
-        assertEquals("Internal Server Error", response.body());
-    }
-
-    @Test
-    public void test404NotFound() throws Exception {
-        mockServer
-                .when(request().withMethod("GET").withPath("/test"))
-                .respond(response().withStatusCode(404).withBody("Not Found"));
-
-        A2AHttpResponse response = client.createGet()
-                .url(getBaseUrl() + "/test")
-                .get();
-
-        assertEquals(404, response.status());
-        assertFalse(response.success());
-        assertEquals("Not Found", response.body());
+    @Override
+    protected A2AHttpClient createClient() {
+        return new AndroidA2AHttpClient();
     }
 }
diff --git a/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientSSETest.java b/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientSSETest.java
index b1422aff0..cd842f9c4 100644
--- a/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientSSETest.java
+++ b/extras/http-client-android/src/test/java/io/a2a/client/http/AndroidA2AHttpClientSSETest.java
@@ -1,254 +1,9 @@
 package io.a2a.client.http;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockserver.model.HttpRequest.request;
-import static org.mockserver.model.HttpResponse.response;
+public class AndroidA2AHttpClientSSETest extends AbstractA2AHttpClientSSETest {
 
-import io.a2a.common.A2AErrorMessages;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.mockserver.integration.ClientAndServer;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-
-public class AndroidA2AHttpClientSSETest {
-
-    private ClientAndServer mockServer;
-    private AndroidA2AHttpClient client;
-
-    @BeforeEach
-    public void setup() {
-        mockServer = ClientAndServer.startClientAndServer(0);  // Use random port
-        client = new AndroidA2AHttpClient();
-    }
-
-    @AfterEach
-    public void teardown() {
-        if (mockServer != null) {
-            mockServer.stop();
-        }
-    }
-
-    private String getBaseUrl() {
-        return "http://localhost:" + mockServer.getPort();
-    }
-
-    @Test
-    public void testGetAsyncSSE() throws Exception {
-        mockServer
-                .when(request().withMethod("GET").withPath("/sse"))
-                .respond(response()
-                        .withStatusCode(200)
-                        .withHeader("Content-Type", "text/event-stream")
-                        .withBody("data: event1\n\ndata: event2\n\ndata: event3\n\n"));
-
-        CountDownLatch latch = new CountDownLatch(1);
-        List events = new ArrayList<>();
-        AtomicReference error = new AtomicReference<>();
-
-        client.createGet()
-                .url(getBaseUrl() + "/sse")
-                .getAsyncSSE(
-                        events::add,
-                        error::set,
-                        latch::countDown
-                );
-
-        assertTrue(latch.await(5, TimeUnit.SECONDS), "Expected completion handler to be called");
-        assertNull(error.get(), "Expected no errors");
-        assertEquals(3, events.size(), "Expected to receive 3 events");
-        assertTrue(events.contains("event1"));
-        assertTrue(events.contains("event2"));
-        assertTrue(events.contains("event3"));
-    }
-
-    @Test
-    public void testPostAsyncSSE() throws Exception {
-        mockServer
-                .when(request()
-                        .withMethod("POST")
-                        .withPath("/sse")
-                        .withBody("{\"subscribe\":true}"))
-                .respond(response()
-                        .withStatusCode(200)
-                        .withHeader("Content-Type", "text/event-stream")
-                        .withBody("data: message1\n\ndata: message2\n\n"));
-
-        CountDownLatch latch = new CountDownLatch(1);
-        List events = new ArrayList<>();
-        AtomicReference error = new AtomicReference<>();
-
-        client.createPost()
-                .url(getBaseUrl() + "/sse")
-                .body("{\"subscribe\":true}")
-                .postAsyncSSE(
-                        events::add,
-                        error::set,
-                        latch::countDown
-                );
-
-        assertTrue(latch.await(5, TimeUnit.SECONDS), "Expected completion handler to be called");
-        assertNull(error.get(), "Expected no errors");
-        assertEquals(2, events.size(), "Expected to receive 2 events");
-        assertTrue(events.contains("message1"));
-        assertTrue(events.contains("message2"));
-    }
-
-    @Test
-    public void testSSEDataPrefixStripping() throws Exception {
-        mockServer
-                .when(request().withMethod("GET").withPath("/sse"))
-                .respond(response()
-                        .withStatusCode(200)
-                        .withHeader("Content-Type", "text/event-stream")
-                        .withBody("data: content here\n\ndata:no space\n\ndata: extra spaces  \n\n"));
-
-        CountDownLatch latch = new CountDownLatch(1);
-        List events = new ArrayList<>();
-        AtomicReference error = new AtomicReference<>();
-
-        client.createGet()
-                .url(getBaseUrl() + "/sse")
-                .getAsyncSSE(
-                        events::add,
-                        error::set,
-                        latch::countDown
-                );
-
-        assertTrue(latch.await(5, TimeUnit.SECONDS));
-        assertNull(error.get());
-        assertTrue(events.contains("content here"), "Should have stripped 'data: ' prefix");
-        assertTrue(events.contains("no space"), "Should handle 'data:' without space");
-        assertTrue(events.contains("extra spaces"), "Should trim whitespace");
-    }
-
-    @Test
-    public void testSSEAuthenticationError() throws Exception {
-        mockServer
-                .when(request().withMethod("GET").withPath("/sse"))
-                .respond(response().withStatusCode(401));
-
-        CountDownLatch errorLatch = new CountDownLatch(1);
-        AtomicReference error = new AtomicReference<>();
-        AtomicBoolean completed = new AtomicBoolean(false);
-
-        client.createGet()
-                .url(getBaseUrl() + "/sse")
-                .getAsyncSSE(
-                        msg -> {},
-                        e -> {
-                            error.set(e);
-                            errorLatch.countDown();
-                        },
-                        () -> completed.set(true)
-                );
-
-        assertTrue(errorLatch.await(5, TimeUnit.SECONDS), "Expected error handler to be called");
-        assertNotNull(error.get(), "Expected an error");
-        assertTrue(error.get() instanceof IOException, "Expected IOException");
-        assertTrue(error.get().getMessage().contains(A2AErrorMessages.AUTHENTICATION_FAILED),
-            "Expected authentication error message but got: " + error.get().getMessage());
-        assertFalse(completed.get(), "Should not call completion handler on error");
-    }
-
-    @Test
-    public void testSSEAuthorizationError() throws Exception {
-        mockServer
-                .when(request().withMethod("GET").withPath("/sse"))
-                .respond(response().withStatusCode(403));
-
-        CountDownLatch errorLatch = new CountDownLatch(1);
-        AtomicReference error = new AtomicReference<>();
-        AtomicBoolean completed = new AtomicBoolean(false);
-
-        client.createGet()
-                .url(getBaseUrl() + "/sse")
-                .getAsyncSSE(
-                        msg -> {},
-                        e -> {
-                            error.set(e);
-                            errorLatch.countDown();
-                        },
-                        () -> completed.set(true)
-                );
-
-        assertTrue(errorLatch.await(5, TimeUnit.SECONDS), "Expected error handler to be called");
-        assertNotNull(error.get(), "Expected an error");
-        assertTrue(error.get() instanceof IOException, "Expected IOException");
-        assertTrue(error.get().getMessage().contains(A2AErrorMessages.AUTHORIZATION_FAILED),
-            "Expected authorization error message but got: " + error.get().getMessage());
-        assertFalse(completed.get(), "Should not call completion handler on error");
-    }
-
-    @Test
-    public void testSSEEmptyLinesIgnored() throws Exception {
-        mockServer
-                .when(request().withMethod("GET").withPath("/sse"))
-                .respond(response()
-                        .withStatusCode(200)
-                        .withHeader("Content-Type", "text/event-stream")
-                        .withBody("data: first\n\n\n\ndata: second\n\ndata: \n\ndata: third\n\n"));
-
-        CountDownLatch latch = new CountDownLatch(1);
-        List events = new ArrayList<>();
-        AtomicReference error = new AtomicReference<>();
-
-        client.createGet()
-                .url(getBaseUrl() + "/sse")
-                .getAsyncSSE(
-                        events::add,
-                        error::set,
-                        latch::countDown
-                );
-
-        assertTrue(latch.await(5, TimeUnit.SECONDS));
-        assertNull(error.get());
-        assertEquals(3, events.size(), "Should have received 3 non-empty events");
-        assertTrue(events.contains("first"));
-        assertTrue(events.contains("second"));
-        assertTrue(events.contains("third"));
-    }
-
-    @Test
-    public void testSSEHeaderPropagation() throws Exception {
-        mockServer
-                .when(request()
-                        .withMethod("GET")
-                        .withPath("/sse")
-                        .withHeader("Accept", "text/event-stream")
-                        .withHeader("Authorization", "Bearer token"))
-                .respond(response()
-                        .withStatusCode(200)
-                        .withHeader("Content-Type", "text/event-stream")
-                        .withBody("data: authenticated\n\n"));
-
-        CountDownLatch latch = new CountDownLatch(1);
-        List events = new ArrayList<>();
-        AtomicReference error = new AtomicReference<>();
-
-        client.createGet()
-                .url(getBaseUrl() + "/sse")
-                .addHeader("Authorization", "Bearer token")
-                .getAsyncSSE(
-                        events::add,
-                        error::set,
-                        latch::countDown
-                );
-
-        assertTrue(latch.await(5, TimeUnit.SECONDS));
-        assertNull(error.get());
-        assertTrue(events.contains("authenticated"));
+    @Override
+    protected A2AHttpClient createClient() {
+        return new AndroidA2AHttpClient();
     }
 }
diff --git a/extras/http-client-android/src/test/java/io/a2a/client/http/JdkA2AHttpClientIntegrationTest.java b/extras/http-client-android/src/test/java/io/a2a/client/http/JdkA2AHttpClientIntegrationTest.java
new file mode 100644
index 000000000..ed68795ea
--- /dev/null
+++ b/extras/http-client-android/src/test/java/io/a2a/client/http/JdkA2AHttpClientIntegrationTest.java
@@ -0,0 +1,9 @@
+package io.a2a.client.http;
+
+public class JdkA2AHttpClientIntegrationTest extends AbstractA2AHttpClientIntegrationTest {
+
+    @Override
+    protected A2AHttpClient createClient() {
+        return new JdkA2AHttpClient();
+    }
+}
diff --git a/extras/http-client-android/src/test/java/io/a2a/client/http/JdkA2AHttpClientSSETest.java b/extras/http-client-android/src/test/java/io/a2a/client/http/JdkA2AHttpClientSSETest.java
new file mode 100644
index 000000000..19dfd5f84
--- /dev/null
+++ b/extras/http-client-android/src/test/java/io/a2a/client/http/JdkA2AHttpClientSSETest.java
@@ -0,0 +1,9 @@
+package io.a2a.client.http;
+
+public class JdkA2AHttpClientSSETest extends AbstractA2AHttpClientSSETest {
+
+    @Override
+    protected A2AHttpClient createClient() {
+        return new JdkA2AHttpClient();
+    }
+}
diff --git a/http-client/pom.xml b/http-client/pom.xml
index 9b7567af3..7909ba46f 100644
--- a/http-client/pom.xml
+++ b/http-client/pom.xml
@@ -35,4 +35,24 @@
         
     
 
+    
+        
+            
+                org.apache.maven.plugins
+                maven-jar-plugin
+                
+                    
+                        
+                            test-jar
+                        
+                        
+                            
+                                **/Abstract*.class
+                            
+                        
+                    
+                
+            
+        
+    
 
\ No newline at end of file
diff --git a/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClient.java b/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClient.java
index cd7177050..0759ede26 100644
--- a/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClient.java
+++ b/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClient.java
@@ -91,6 +91,14 @@ protected HttpRequest.Builder createRequestBuilder() throws IOException {
             return builder;
         }
 
+        protected void checkAuthErrors(HttpResponse response) throws IOException {
+            if (response.statusCode() == HTTP_UNAUTHORIZED) {
+                throw new IOException(A2AErrorMessages.AUTHENTICATION_FAILED);
+            } else if (response.statusCode() == HTTP_FORBIDDEN) {
+                throw new IOException(A2AErrorMessages.AUTHORIZATION_FAILED);
+            }
+        }
+
         protected CompletableFuture asyncRequest(
                 HttpRequest request,
                 Consumer messageConsumer,
@@ -212,6 +220,7 @@ public A2AHttpResponse get() throws IOException, InterruptedException {
                     .build();
             HttpResponse response =
                     httpClient.send(request, BodyHandlers.ofString(StandardCharsets.UTF_8));
+            checkAuthErrors(response);
             return new JdkHttpResponse(response);
         }
 
@@ -234,6 +243,7 @@ public A2AHttpResponse delete() throws IOException, InterruptedException {
             HttpRequest request = super.createRequestBuilder().DELETE().build();
             HttpResponse response =
                     httpClient.send(request, BodyHandlers.ofString(StandardCharsets.UTF_8));
+            checkAuthErrors(response);
             return new JdkHttpResponse(response);
         }
 
@@ -265,11 +275,7 @@ public A2AHttpResponse post() throws IOException, InterruptedException {
             HttpResponse response =
                     httpClient.send(request, BodyHandlers.ofString(StandardCharsets.UTF_8));
 
-            if (response.statusCode() == HTTP_UNAUTHORIZED) {
-                throw new IOException(A2AErrorMessages.AUTHENTICATION_FAILED);
-            } else if (response.statusCode() == HTTP_FORBIDDEN) {
-                throw new IOException(A2AErrorMessages.AUTHORIZATION_FAILED);
-            }
+            checkAuthErrors(response);
 
             return new JdkHttpResponse(response);
         }
diff --git a/http-client/src/test/java/io/a2a/client/http/AbstractA2AHttpClientIntegrationTest.java b/http-client/src/test/java/io/a2a/client/http/AbstractA2AHttpClientIntegrationTest.java
new file mode 100644
index 000000000..90cd899e8
--- /dev/null
+++ b/http-client/src/test/java/io/a2a/client/http/AbstractA2AHttpClientIntegrationTest.java
@@ -0,0 +1,216 @@
+package io.a2a.client.http;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockserver.model.HttpRequest.request;
+import static org.mockserver.model.HttpResponse.response;
+
+import io.a2a.common.A2AErrorMessages;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockserver.integration.ClientAndServer;
+
+import java.io.IOException;
+
+public abstract class AbstractA2AHttpClientIntegrationTest {
+
+    private ClientAndServer mockServer;
+    private A2AHttpClient client;
+
+    protected abstract A2AHttpClient createClient();
+
+    @BeforeEach
+    public void setup() {
+        mockServer = ClientAndServer.startClientAndServer(0);
+        client = createClient();
+    }
+
+    @AfterEach
+    public void teardown() {
+        if (mockServer != null) {
+            mockServer.stop();
+        }
+    }
+
+    private String getBaseUrl() {
+        return "http://localhost:" + mockServer.getPort();
+    }
+
+    @Test
+    public void testGetRequestSuccess() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/test"))
+                .respond(response().withStatusCode(200).withBody("success"));
+
+        A2AHttpResponse response = client.createGet()
+                .url(getBaseUrl() + "/test")
+                .get();
+
+        assertEquals(200, response.status());
+        assertTrue(response.success());
+        assertEquals("success", response.body());
+    }
+
+    @Test
+    public void testPostRequestSuccess() throws Exception {
+        mockServer
+                .when(request()
+                        .withMethod("POST")
+                        .withPath("/test")
+                        .withBody("{\"key\":\"value\"}"))
+                .respond(response().withStatusCode(201).withBody("created"));
+
+        A2AHttpResponse response = client.createPost()
+                .url(getBaseUrl() + "/test")
+                .body("{\"key\":\"value\"}")
+                .post();
+
+        assertEquals(201, response.status());
+        assertTrue(response.success());
+        assertEquals("created", response.body());
+    }
+
+    @Test
+    public void testDeleteRequestSuccess() throws Exception {
+        mockServer
+                .when(request().withMethod("DELETE").withPath("/test"))
+                .respond(response().withStatusCode(204));
+
+        A2AHttpResponse response = client.createDelete()
+                .url(getBaseUrl() + "/test")
+                .delete();
+
+        assertEquals(204, response.status());
+        assertTrue(response.success());
+    }
+
+    @Test
+    public void test401AuthenticationErrorOnGet() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/test"))
+                .respond(response().withStatusCode(401));
+
+        Exception exception = assertThrows(IOException.class, () -> {
+            client.createGet()
+                    .url(getBaseUrl() + "/test")
+                    .get();
+        });
+
+        assertEquals(A2AErrorMessages.AUTHENTICATION_FAILED, exception.getMessage());
+    }
+
+    @Test
+    public void test403AuthorizationErrorOnGet() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/test"))
+                .respond(response().withStatusCode(403));
+
+        Exception exception = assertThrows(IOException.class, () -> {
+            client.createGet()
+                    .url(getBaseUrl() + "/test")
+                    .get();
+        });
+
+        assertEquals(A2AErrorMessages.AUTHORIZATION_FAILED, exception.getMessage());
+    }
+
+    @Test
+    public void test401AuthenticationErrorOnPost() throws Exception {
+        mockServer
+                .when(request().withMethod("POST").withPath("/test"))
+                .respond(response().withStatusCode(401));
+
+        Exception exception = assertThrows(IOException.class, () -> {
+            client.createPost()
+                    .url(getBaseUrl() + "/test")
+                    .body("{}")
+                    .post();
+        });
+
+        assertEquals(A2AErrorMessages.AUTHENTICATION_FAILED, exception.getMessage());
+    }
+
+    @Test
+    public void test403AuthorizationErrorOnPost() throws Exception {
+        mockServer
+                .when(request().withMethod("POST").withPath("/test"))
+                .respond(response().withStatusCode(403));
+
+        Exception exception = assertThrows(IOException.class, () -> {
+            client.createPost()
+                    .url(getBaseUrl() + "/test")
+                    .body("{}")
+                    .post();
+        });
+
+        assertEquals(A2AErrorMessages.AUTHORIZATION_FAILED, exception.getMessage());
+    }
+
+    @Test
+    public void test401AuthenticationErrorOnDelete() throws Exception {
+        mockServer
+                .when(request().withMethod("DELETE").withPath("/test"))
+                .respond(response().withStatusCode(401));
+
+        Exception exception = assertThrows(IOException.class, () -> {
+            client.createDelete()
+                    .url(getBaseUrl() + "/test")
+                    .delete();
+        });
+
+        assertEquals(A2AErrorMessages.AUTHENTICATION_FAILED, exception.getMessage());
+    }
+
+    @Test
+    public void testHeaderPropagation() throws Exception {
+        mockServer
+                .when(request()
+                        .withMethod("GET")
+                        .withPath("/test")
+                        .withHeader("Authorization", "Bearer token")
+                        .withHeader("X-Custom-Header", "custom-value"))
+                .respond(response().withStatusCode(200).withBody("ok"));
+
+        A2AHttpResponse response = client.createGet()
+                .url(getBaseUrl() + "/test")
+                .addHeader("Authorization", "Bearer token")
+                .addHeader("X-Custom-Header", "custom-value")
+                .get();
+
+        assertEquals(200, response.status());
+        assertEquals("ok", response.body());
+    }
+
+    @Test
+    public void testNonSuccessStatusCode() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/test"))
+                .respond(response().withStatusCode(500).withBody("Internal Server Error"));
+
+        A2AHttpResponse response = client.createGet()
+                .url(getBaseUrl() + "/test")
+                .get();
+
+        assertEquals(500, response.status());
+        assertFalse(response.success());
+        assertEquals("Internal Server Error", response.body());
+    }
+
+    @Test
+    public void test404NotFound() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/test"))
+                .respond(response().withStatusCode(404).withBody("Not Found"));
+
+        A2AHttpResponse response = client.createGet()
+                .url(getBaseUrl() + "/test")
+                .get();
+
+        assertEquals(404, response.status());
+        assertFalse(response.success());
+        assertEquals("Not Found", response.body());
+    }
+}
diff --git a/http-client/src/test/java/io/a2a/client/http/AbstractA2AHttpClientSSETest.java b/http-client/src/test/java/io/a2a/client/http/AbstractA2AHttpClientSSETest.java
new file mode 100644
index 000000000..2b9a0f349
--- /dev/null
+++ b/http-client/src/test/java/io/a2a/client/http/AbstractA2AHttpClientSSETest.java
@@ -0,0 +1,255 @@
+package io.a2a.client.http;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockserver.model.HttpRequest.request;
+import static org.mockserver.model.HttpResponse.response;
+
+import io.a2a.common.A2AErrorMessages;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockserver.integration.ClientAndServer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+public abstract class AbstractA2AHttpClientSSETest {
+
+    private ClientAndServer mockServer;
+    private A2AHttpClient client;
+
+    protected abstract A2AHttpClient createClient();
+
+    @BeforeEach
+    public void setup() {
+        mockServer = ClientAndServer.startClientAndServer(0);
+        client = createClient();
+    }
+
+    @AfterEach
+    public void teardown() {
+        if (mockServer != null) {
+            mockServer.stop();
+        }
+    }
+
+    private String getBaseUrl() {
+        return "http://localhost:" + mockServer.getPort();
+    }
+
+    @Test
+    public void testGetAsyncSSE() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/sse"))
+                .respond(response()
+                        .withStatusCode(200)
+                        .withHeader("Content-Type", "text/event-stream")
+                        .withBody("data: event1\n\ndata: event2\n\ndata: event3\n\n"));
+
+        CountDownLatch latch = new CountDownLatch(1);
+        List events = new ArrayList<>();
+        AtomicReference error = new AtomicReference<>();
+
+        client.createGet()
+                .url(getBaseUrl() + "/sse")
+                .getAsyncSSE(
+                        events::add,
+                        error::set,
+                        latch::countDown
+                );
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS), "Expected completion handler to be called");
+        assertNull(error.get(), "Expected no errors");
+        assertEquals(3, events.size(), "Expected to receive 3 events");
+        assertTrue(events.contains("event1"));
+        assertTrue(events.contains("event2"));
+        assertTrue(events.contains("event3"));
+    }
+
+    @Test
+    public void testPostAsyncSSE() throws Exception {
+        mockServer
+                .when(request()
+                        .withMethod("POST")
+                        .withPath("/sse")
+                        .withBody("{\"subscribe\":true}"))
+                .respond(response()
+                        .withStatusCode(200)
+                        .withHeader("Content-Type", "text/event-stream")
+                        .withBody("data: message1\n\ndata: message2\n\n"));
+
+        CountDownLatch latch = new CountDownLatch(1);
+        List events = new ArrayList<>();
+        AtomicReference error = new AtomicReference<>();
+
+        client.createPost()
+                .url(getBaseUrl() + "/sse")
+                .body("{\"subscribe\":true}")
+                .postAsyncSSE(
+                        events::add,
+                        error::set,
+                        latch::countDown
+                );
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS), "Expected completion handler to be called");
+        assertNull(error.get(), "Expected no errors");
+        assertEquals(2, events.size(), "Expected to receive 2 events");
+        assertTrue(events.contains("message1"));
+        assertTrue(events.contains("message2"));
+    }
+
+    @Test
+    public void testSSEDataPrefixStripping() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/sse"))
+                .respond(response()
+                        .withStatusCode(200)
+                        .withHeader("Content-Type", "text/event-stream")
+                        .withBody("data: content here\n\ndata:no space\n\ndata: extra spaces  \n\n"));
+
+        CountDownLatch latch = new CountDownLatch(1);
+        List events = new ArrayList<>();
+        AtomicReference error = new AtomicReference<>();
+
+        client.createGet()
+                .url(getBaseUrl() + "/sse")
+                .getAsyncSSE(
+                        events::add,
+                        error::set,
+                        latch::countDown
+                );
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+        assertNull(error.get());
+        assertTrue(events.contains("content here"), "Should have stripped 'data: ' prefix");
+        assertTrue(events.contains("no space"), "Should handle 'data:' without space");
+        assertTrue(events.contains("extra spaces"), "Should trim whitespace");
+    }
+
+    @Test
+    public void testSSEAuthenticationError() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/sse"))
+                .respond(response().withStatusCode(401));
+
+        CountDownLatch errorLatch = new CountDownLatch(1);
+        AtomicReference error = new AtomicReference<>();
+        AtomicBoolean completed = new AtomicBoolean(false);
+
+        client.createGet()
+                .url(getBaseUrl() + "/sse")
+                .getAsyncSSE(
+                        msg -> {},
+                        e -> {
+                            error.set(e);
+                            errorLatch.countDown();
+                        },
+                        () -> completed.set(true)
+                );
+
+        assertTrue(errorLatch.await(5, TimeUnit.SECONDS), "Expected error handler to be called");
+        assertNotNull(error.get(), "Expected an error");
+        assertTrue(error.get() instanceof IOException, "Expected IOException");
+        assertTrue(error.get().getMessage().contains(A2AErrorMessages.AUTHENTICATION_FAILED),
+            "Expected authentication error message but got: " + error.get().getMessage());
+        assertFalse(completed.get(), "Should not call completion handler on error");
+    }
+
+    @Test
+    public void testSSEAuthorizationError() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/sse"))
+                .respond(response().withStatusCode(403));
+
+        CountDownLatch errorLatch = new CountDownLatch(1);
+        AtomicReference error = new AtomicReference<>();
+        AtomicBoolean completed = new AtomicBoolean(false);
+
+        client.createGet()
+                .url(getBaseUrl() + "/sse")
+                .getAsyncSSE(
+                        msg -> {},
+                        e -> {
+                            error.set(e);
+                            errorLatch.countDown();
+                        },
+                        () -> completed.set(true)
+                );
+
+        assertTrue(errorLatch.await(5, TimeUnit.SECONDS), "Expected error handler to be called");
+        assertNotNull(error.get(), "Expected an error");
+        assertTrue(error.get() instanceof IOException, "Expected IOException");
+        assertTrue(error.get().getMessage().contains(A2AErrorMessages.AUTHORIZATION_FAILED),
+            "Expected authorization error message but got: " + error.get().getMessage());
+        assertFalse(completed.get(), "Should not call completion handler on error");
+    }
+
+    @Test
+    public void testSSEEmptyLinesIgnored() throws Exception {
+        mockServer
+                .when(request().withMethod("GET").withPath("/sse"))
+                .respond(response()
+                        .withStatusCode(200)
+                        .withHeader("Content-Type", "text/event-stream")
+                        .withBody("data: first\n\n\n\ndata: second\n\ndata: \n\ndata: third\n\n"));
+
+        CountDownLatch latch = new CountDownLatch(1);
+        List events = new ArrayList<>();
+        AtomicReference error = new AtomicReference<>();
+
+        client.createGet()
+                .url(getBaseUrl() + "/sse")
+                .getAsyncSSE(
+                        events::add,
+                        error::set,
+                        latch::countDown
+                );
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+        assertNull(error.get());
+        assertEquals(3, events.size(), "Should have received 3 non-empty events");
+        assertTrue(events.contains("first"));
+        assertTrue(events.contains("second"));
+        assertTrue(events.contains("third"));
+    }
+
+    @Test
+    public void testSSEHeaderPropagation() throws Exception {
+        mockServer
+                .when(request()
+                        .withMethod("GET")
+                        .withPath("/sse")
+                        .withHeader("Accept", "text/event-stream")
+                        .withHeader("Authorization", "Bearer token"))
+                .respond(response()
+                        .withStatusCode(200)
+                        .withHeader("Content-Type", "text/event-stream")
+                        .withBody("data: authenticated\n\n"));
+
+        CountDownLatch latch = new CountDownLatch(1);
+        List events = new ArrayList<>();
+        AtomicReference error = new AtomicReference<>();
+
+        client.createGet()
+                .url(getBaseUrl() + "/sse")
+                .addHeader("Authorization", "Bearer token")
+                .getAsyncSSE(
+                        events::add,
+                        error::set,
+                        latch::countDown
+                );
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+        assertNull(error.get());
+        assertTrue(events.contains("authenticated"));
+    }
+}
diff --git a/http-client/src/test/java/io/a2a/client/http/JdkA2AHttpClientIntegrationTest.java b/http-client/src/test/java/io/a2a/client/http/JdkA2AHttpClientIntegrationTest.java
new file mode 100644
index 000000000..ed68795ea
--- /dev/null
+++ b/http-client/src/test/java/io/a2a/client/http/JdkA2AHttpClientIntegrationTest.java
@@ -0,0 +1,9 @@
+package io.a2a.client.http;
+
+public class JdkA2AHttpClientIntegrationTest extends AbstractA2AHttpClientIntegrationTest {
+
+    @Override
+    protected A2AHttpClient createClient() {
+        return new JdkA2AHttpClient();
+    }
+}
diff --git a/http-client/src/test/java/io/a2a/client/http/JdkA2AHttpClientSSETest.java b/http-client/src/test/java/io/a2a/client/http/JdkA2AHttpClientSSETest.java
new file mode 100644
index 000000000..19dfd5f84
--- /dev/null
+++ b/http-client/src/test/java/io/a2a/client/http/JdkA2AHttpClientSSETest.java
@@ -0,0 +1,9 @@
+package io.a2a.client.http;
+
+public class JdkA2AHttpClientSSETest extends AbstractA2AHttpClientSSETest {
+
+    @Override
+    protected A2AHttpClient createClient() {
+        return new JdkA2AHttpClient();
+    }
+}
diff --git a/pom.xml b/pom.xml
index f949c7804..110e27ee7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -288,6 +288,13 @@
                 test
                 ${project.version}
             
+            
+                ${project.groupId}
+                a2a-java-sdk-http-client
+                test-jar
+                test
+                ${project.version}
+            
             
                 org.jspecify
                 jspecify

From 9311fbc221c9cb33e4cf655710da213974bf82b4 Mon Sep 17 00:00:00 2001
From: Daria Wieliczko 
Date: Fri, 17 Apr 2026 18:16:50 +0000
Subject: [PATCH 32/37] fix(tck): resolve capability test failures for
 extensions and push notifications

- Default `extensions` to an empty list in `AgentCapabilities` to ensure it serializes as an array, fixing `test_capabilities_structure`.
- Save `pushNotificationConfig` in `onMessageSend` for non-streaming messages, fixing `test_send_message_with_push_notification_config`
---
 .../server/requesthandlers/DefaultRequestHandler.java    | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java b/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java
index 577e571c9..45d6fc88d 100644
--- a/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java
+++ b/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java
@@ -326,8 +326,13 @@ public EventKind onMessageSend(MessageSendParams params, ServerCallContext conte
                     }
                 }
             }
-            if (kind instanceof Task taskResult && !taskId.equals(taskResult.getId())) {
-                throw new InternalError("Task ID mismatch in agent response");
+            if (kind instanceof Task taskResult) {
+                if (!Objects.equals(taskId, taskResult.getId())) {
+                    throw new InternalError("Task ID mismatch in agent response");
+                }
+                if (shouldAddPushInfo(params)) {
+                    pushConfigStore.setInfo(taskResult.getId(), params.configuration().pushNotificationConfig());
+                }
             }
 
             // Send push notification after initial return (for both blocking and non-blocking)

From 5a8a07ddedaccb3c48f8df9f10f83b6216c0c044 Mon Sep 17 00:00:00 2001
From: Emmanuel Hugonnet 
Date: Wed, 29 Apr 2026 10:36:50 +0200
Subject: [PATCH 33/37] refactor: centralize JSON-RPC helpers and fix push
 notification config URL path

- Extract writeJsonRpcId() into JsonUtil as shared utility
- Refactor anonymous TypeAdapter into named JSONRPCErrorTypeAdapter class
- Add safeGetString helper in RestErrorMapper
- Add AuthenticatedExtendedCardNotConfiguredError error code mapping
- Fix leading slash in listTaskPushNotificationConfigurations URLCleaning stuff

Signed-off-by: Emmanuel Hugonnet 
---
 .../transport/rest/RestErrorMapper.java       |  27 +-
 .../client/transport/rest/RestTransport.java  |   2 +-
 .../java/io/a2a/grpc/utils/JSONRPCUtils.java  |  17 +-
 spec/src/main/java/io/a2a/json/JsonUtil.java  | 286 +++++++++---------
 4 files changed, 158 insertions(+), 174 deletions(-)

diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java
index 8fc414d14..60e1c4197 100644
--- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java
+++ b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java
@@ -34,21 +34,8 @@ public static A2AClientException mapRestError(String body, int code) {
         try {
             if (body != null && !body.isBlank()) {
                 JsonObject node = JsonUtil.fromJson(body, JsonObject.class);
-                // Safely extract string fields, handling null and non-string types
-                String className = "";
-                if (node.has("error")) {
-                    JsonElement errorElement = node.get("error");
-                    if (errorElement != null && errorElement.isJsonPrimitive() && errorElement.getAsJsonPrimitive().isString()) {
-                        className = errorElement.getAsString();
-                    }
-                }
-                String errorMessage = "";
-                if (node.has("message")) {
-                    JsonElement messageElement = node.get("message");
-                    if (messageElement != null && messageElement.isJsonPrimitive() && messageElement.getAsJsonPrimitive().isString()) {
-                        errorMessage = messageElement.getAsString();
-                    }
-                }
+                String className = safeGetString(node, "error");
+                String errorMessage = safeGetString(node, "message");
                 return mapRestError(className, errorMessage, code);
             }
             return mapRestError("", "", code);
@@ -58,6 +45,16 @@ public static A2AClientException mapRestError(String body, int code) {
         }
     }
 
+    private static String safeGetString(JsonObject obj, String fieldName) {
+        if (obj.has(fieldName)) {
+            JsonElement element = obj.get(fieldName);
+            if (element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) {
+                return element.getAsString();
+            }
+        }
+        return "";
+    }
+
     public static A2AClientException mapRestError(String className, String errorMessage, int code) {
         return switch (className) {
             case "io.a2a.spec.TaskNotFoundError" -> new A2AClientException(errorMessage, new TaskNotFoundError());
diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java
index b8cf4f33b..c7a792527 100644
--- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java
+++ b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java
@@ -229,7 +229,7 @@ public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPu
     public List listTaskPushNotificationConfigurations(ListTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) throws A2AClientException {
         checkNotNullParam("request", request);
         ListTaskPushNotificationConfigRequest.Builder builder = ListTaskPushNotificationConfigRequest.newBuilder();
-        builder.setParent(String.format("/tasks/%1s/pushNotificationConfigs", request.id()));
+        builder.setParent(String.format("tasks/%1s/pushNotificationConfigs", request.id()));
         PayloadAndHeaders payloadAndHeaders = applyInterceptors(io.a2a.spec.ListTaskPushNotificationConfigRequest.METHOD, builder,
                 agentCard, context);
         try {
diff --git a/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java b/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java
index 02c8fe772..61a77c9bb 100644
--- a/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java
+++ b/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java
@@ -500,22 +500,11 @@ public static String toJsonRPCRequest(@Nullable String requestId, String method,
         }
     }
 
-    /**
-     * Writes the 'id' field to a JSON-RPC response.
-     * Per JSON-RPC 2.0 spec, the id field is REQUIRED in all responses, even if null.
-     */
     private static void ensureId(JsonWriter output, @Nullable Object requestId) throws IOException {
-        output.name("id");
-        if (requestId == null) {
-            output.nullValue();
-        } else if (requestId instanceof String string) {
-            output.value(string);
-        } else if (requestId instanceof Number number) {
-            output.value(number.longValue());
-        }
+        JsonUtil.writeJsonRpcId(output, requestId);
     }
 
-    public static String toJsonRPCResultResponse(Object requestId, com.google.protobuf.MessageOrBuilder builder) {
+    public static String toJsonRPCResultResponse(@Nullable Object requestId, com.google.protobuf.MessageOrBuilder builder) {
         try (StringWriter result = new StringWriter(); JsonWriter output = JsonUtil.OBJECT_MAPPER.newJsonWriter(result)) {
             output.beginObject();
             output.name("jsonrpc").value("2.0");
@@ -531,7 +520,7 @@ public static String toJsonRPCResultResponse(Object requestId, com.google.protob
         }
     }
 
-    public static String toJsonRPCErrorResponse(Object requestId, JSONRPCError error) {
+    public static String toJsonRPCErrorResponse(@Nullable Object requestId, JSONRPCError error) {
         try (StringWriter result = new StringWriter(); JsonWriter output = JsonUtil.OBJECT_MAPPER.newJsonWriter(result)) {
             output.beginObject();
             output.name("jsonrpc").value("2.0");
diff --git a/spec/src/main/java/io/a2a/json/JsonUtil.java b/spec/src/main/java/io/a2a/json/JsonUtil.java
index 34c238358..07cd1b212 100644
--- a/spec/src/main/java/io/a2a/json/JsonUtil.java
+++ b/spec/src/main/java/io/a2a/json/JsonUtil.java
@@ -6,6 +6,7 @@
 import static com.google.gson.stream.JsonToken.NULL;
 import static com.google.gson.stream.JsonToken.NUMBER;
 import static com.google.gson.stream.JsonToken.STRING;
+import static io.a2a.spec.A2AErrorCodes.AUTHENTICATED_EXTENDED_CARD_NOT_CONFIGURED_ERROR_CODE;
 import static io.a2a.spec.A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE;
 import static io.a2a.spec.A2AErrorCodes.INTERNAL_ERROR_CODE;
 import static io.a2a.spec.A2AErrorCodes.INVALID_AGENT_RESPONSE_ERROR_CODE;
@@ -28,6 +29,7 @@
 import com.google.gson.stream.JsonReader;
 import com.google.gson.stream.JsonWriter;
 import io.a2a.spec.APIKeySecurityScheme;
+import io.a2a.spec.AuthenticatedExtendedCardNotConfiguredError;
 import io.a2a.spec.EventKind;
 import io.a2a.spec.JSONRPCResponse;
 import io.a2a.spec.ContentTypeNotSupportedError;
@@ -59,17 +61,16 @@
 import io.a2a.spec.TaskStatusUpdateEvent;
 import io.a2a.spec.TextPart;
 import io.a2a.spec.UnsupportedOperationError;
-import java.io.StringReader;
 import java.lang.reflect.Type;
 import java.time.OffsetDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
 import org.jspecify.annotations.Nullable;
 
-import static io.a2a.json.JsonUtil.JSONRPCErrorTypeAdapterFactory.THROWABLE_MARKER_FIELD;
-
 public class JsonUtil {
 
+    private static final String THROWABLE_MARKER_FIELD = "__throwable";
+
     private static GsonBuilder createBaseGsonBuilder() {
         return new GsonBuilder()
                 .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
@@ -133,6 +134,25 @@ public static String toJson(Object data) throws JsonProcessingException {
         }
     }
 
+    /**
+     * Writes a JSON-RPC {@code id} field. Handles null, String, and Number values,
+     * preserving fractional precision for non-integer numeric IDs.
+     */
+    public static void writeJsonRpcId(JsonWriter out, @Nullable Object id) throws java.io.IOException {
+        out.name("id");
+        if (id == null) {
+            out.nullValue();
+        } else if (id instanceof Number n) {
+            if (id instanceof Long || id instanceof Integer || id instanceof Short || id instanceof Byte) {
+                out.value(n.longValue());
+            } else {
+                out.value(n);
+            }
+        } else {
+            out.value(id.toString());
+        }
+    }
+
     /**
      * Gson TypeAdapter for serializing and deserializing {@link OffsetDateTime} to/from ISO-8601 format.
      * 

@@ -198,7 +218,9 @@ static class ThrowableTypeAdapter extends TypeAdapter { private static final java.util.Set ALLOWED_THROWABLE_PACKAGES = java.util.Set.of( "java.lang.", "java.io.", - "io.a2a." + "io.a2a.spec.", + "io.a2a.json.", + "io.a2a.server." ); private static final java.util.Set ALLOWED_THROWABLE_CLASSES = java.util.Set.of( @@ -307,8 +329,7 @@ Throwable read(JsonReader in) throws java.io.IOException { */ static class JSONRPCErrorTypeAdapterFactory implements TypeAdapterFactory { - private static final ThrowableTypeAdapter THROWABLE_ADAPTER = new ThrowableTypeAdapter(); - static final String THROWABLE_MARKER_FIELD = "__throwable"; + static final ThrowableTypeAdapter THROWABLE_ADAPTER = new ThrowableTypeAdapter(); private static final String CODE_FIELD = "code"; private static final String DATA_FIELD = "data"; private static final String MESSAGE_FIELD = "message"; @@ -321,141 +342,133 @@ static class JSONRPCErrorTypeAdapterFactory implements TypeAdapterFactory { } @SuppressWarnings("unchecked") - TypeAdapter adapter = (TypeAdapter) new TypeAdapter() { - @Override - public void write(JsonWriter out, JSONRPCError value) throws java.io.IOException { - if (value == null) { - out.nullValue(); - return; - } - out.beginObject(); - out.name(CODE_FIELD).value(value.getCode()); - out.name(MESSAGE_FIELD).value(value.getMessage()); - if (value.getData() != null) { - out.name(DATA_FIELD); - // If data is a Throwable, use ThrowableTypeAdapter to avoid reflection issues - if (value.getData() instanceof Throwable throwable) { - THROWABLE_ADAPTER.write(out, throwable); - } else { - // Use the Gson instance passed to this factory instead of OBJECT_MAPPER - gson.toJson(value.getData(), Object.class, out); - } - } - out.endObject(); - } + TypeAdapter adapter = (TypeAdapter) new JSONRPCErrorTypeAdapter(gson); + return adapter; + } - @Override - public @Nullable - JSONRPCError read(JsonReader in) throws java.io.IOException { - if (in.peek() == com.google.gson.stream.JsonToken.NULL) { - in.nextNull(); - return null; - } + private static class JSONRPCErrorTypeAdapter extends TypeAdapter { - Integer code = null; - String message = null; - Object data = null; - - in.beginObject(); - while (in.hasNext()) { - String fieldName = in.nextName(); - switch (fieldName) { - case CODE_FIELD -> - code = in.nextInt(); - case MESSAGE_FIELD -> - message = in.nextString(); - case DATA_FIELD -> { - // Read data as a generic object (could be string, number, object, etc.) - data = readDataValue(in, gson); - } - default -> - in.skipValue(); - } + private final Gson gson; + + JSONRPCErrorTypeAdapter(Gson gson) { + this.gson = gson; + } + + @Override + public void write(JsonWriter out, JSONRPCError value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + return; + } + out.beginObject(); + out.name(CODE_FIELD).value(value.getCode()); + out.name(MESSAGE_FIELD).value(value.getMessage()); + if (value.getData() != null) { + out.name(DATA_FIELD); + if (value.getData() instanceof Throwable throwable) { + THROWABLE_ADAPTER.write(out, throwable); + } else { + gson.toJson(value.getData(), Object.class, out); } - in.endObject(); + } + out.endObject(); + } - // Create the appropriate subclass based on the error code - return createErrorInstance(code, message, data); + @Override + public @Nullable JSONRPCError read(JsonReader in) throws java.io.IOException { + if (in.peek() == com.google.gson.stream.JsonToken.NULL) { + in.nextNull(); + return null; } - /** - * Reads the data field value, which can be of any JSON type. - */ - private @Nullable - Object readDataValue(JsonReader in, Gson gson) throws java.io.IOException { - return switch (in.peek()) { - case STRING -> - in.nextString(); - case NUMBER -> - in.nextDouble(); - case BOOLEAN -> - in.nextBoolean(); - case NULL -> { - in.nextNull(); - yield null; - } - case BEGIN_OBJECT -> { - // Parse as JsonElement to check if it's a Throwable - com.google.gson.JsonElement element = com.google.gson.JsonParser.parseReader(in); - if (element.isJsonObject()) { - com.google.gson.JsonObject obj = element.getAsJsonObject(); - // Check if it has the structure of a serialized Throwable (type + message) - if (obj.has(TYPE_FIELD) && obj.has(MESSAGE_FIELD) && obj.has(THROWABLE_MARKER_FIELD)) { - // Deserialize as Throwable using ThrowableTypeAdapter - yield THROWABLE_ADAPTER.fromJsonTree(element); - } - } - // Otherwise, deserialize as generic object using the Gson instance - yield gson.fromJson(element, Object.class); - } - case BEGIN_ARRAY -> - // For arrays, read as raw JSON using the Gson instance - gson.fromJson(in, Object.class); - default -> { + Integer code = null; + String message = null; + Object data = null; + + in.beginObject(); + while (in.hasNext()) { + String fieldName = in.nextName(); + switch (fieldName) { + case CODE_FIELD -> + code = in.nextInt(); + case MESSAGE_FIELD -> + message = in.nextString(); + case DATA_FIELD -> + data = readDataValue(in); + default -> in.skipValue(); - yield null; - } - }; + } } + in.endObject(); + + return createErrorInstance(code, message, data); + } - /** - * Creates the appropriate JSONRPCError subclass based on the error code. - */ - private JSONRPCError createErrorInstance(@Nullable Integer code, @Nullable String message, @Nullable Object data) { - if (code == null) { - throw new JsonSyntaxException("JSONRPCError must have a code field"); + private @Nullable Object readDataValue(JsonReader in) throws java.io.IOException { + return switch (in.peek()) { + case STRING -> + in.nextString(); + case NUMBER -> + in.nextDouble(); + case BOOLEAN -> + in.nextBoolean(); + case NULL -> { + in.nextNull(); + yield null; + } + case BEGIN_OBJECT -> { + com.google.gson.JsonElement element = com.google.gson.JsonParser.parseReader(in); + if (element.isJsonObject()) { + com.google.gson.JsonObject obj = element.getAsJsonObject(); + if (obj.has(TYPE_FIELD) && obj.has(MESSAGE_FIELD) && obj.has(THROWABLE_MARKER_FIELD)) { + yield THROWABLE_ADAPTER.fromJsonTree(element); + } + } + yield gson.fromJson(element, Object.class); } + case BEGIN_ARRAY -> + gson.fromJson(in, Object.class); + default -> { + in.skipValue(); + yield null; + } + }; + } - return switch (code) { - case JSON_PARSE_ERROR_CODE -> - new JSONParseError(code, message, data); - case INVALID_REQUEST_ERROR_CODE -> - new InvalidRequestError(code, message, data); - case METHOD_NOT_FOUND_ERROR_CODE -> - new MethodNotFoundError(code, message, data); - case INVALID_PARAMS_ERROR_CODE -> - new InvalidParamsError(code, message, data); - case INTERNAL_ERROR_CODE -> - new io.a2a.spec.InternalError(code, message, data); - case TASK_NOT_FOUND_ERROR_CODE -> - new TaskNotFoundError(code, message, data); - case TASK_NOT_CANCELABLE_ERROR_CODE -> - new TaskNotCancelableError(code, message, data); - case PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE -> - new PushNotificationNotSupportedError(code, message, data); - case UNSUPPORTED_OPERATION_ERROR_CODE -> - new UnsupportedOperationError(code, message, data); - case CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE -> - new ContentTypeNotSupportedError(code, message, data); - case INVALID_AGENT_RESPONSE_ERROR_CODE -> - new InvalidAgentResponseError(code, message, data); - default -> - new JSONRPCError(code, message, data); - }; + private static JSONRPCError createErrorInstance(@Nullable Integer code, @Nullable String message, @Nullable Object data) { + if (code == null) { + throw new JsonSyntaxException("JSONRPCError must have a code field"); } - }; - return adapter; + return switch (code) { + case JSON_PARSE_ERROR_CODE -> + new JSONParseError(code, message, data); + case INVALID_REQUEST_ERROR_CODE -> + new InvalidRequestError(code, message, data); + case METHOD_NOT_FOUND_ERROR_CODE -> + new MethodNotFoundError(code, message, data); + case INVALID_PARAMS_ERROR_CODE -> + new InvalidParamsError(code, message, data); + case INTERNAL_ERROR_CODE -> + new io.a2a.spec.InternalError(code, message, data); + case TASK_NOT_FOUND_ERROR_CODE -> + new TaskNotFoundError(code, message, data); + case TASK_NOT_CANCELABLE_ERROR_CODE -> + new TaskNotCancelableError(code, message, data); + case PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE -> + new PushNotificationNotSupportedError(code, message, data); + case UNSUPPORTED_OPERATION_ERROR_CODE -> + new UnsupportedOperationError(code, message, data); + case CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE -> + new ContentTypeNotSupportedError(code, message, data); + case INVALID_AGENT_RESPONSE_ERROR_CODE -> + new InvalidAgentResponseError(code, message, data); + case AUTHENTICATED_EXTENDED_CARD_NOT_CONFIGURED_ERROR_CODE -> + new AuthenticatedExtendedCardNotConfiguredError(code, message, data); + default -> + new JSONRPCError(code, message, data); + }; + } } } @@ -994,22 +1007,7 @@ public void write(JsonWriter out, T value) throws java.io.IOException { out.beginObject(); out.name("jsonrpc").value(response.getJsonrpc()); - Object id = response.getId(); - out.name("id"); - if (id == null) { - out.nullValue(); - } else if (id instanceof Number n) { - // Preserve precision for fractional IDs (e.g., 1.5 should remain 1.5, not become 1) - // Check if the number is an integer type or has no fractional part - if (id instanceof Long || id instanceof Integer || id instanceof Short || id instanceof Byte) { - out.value(n.longValue()); - } else { - // For Double, Float, or other number types, preserve full precision - out.value(n); - } - } else { - out.value(id.toString()); - } + writeJsonRpcId(out, response.getId()); JSONRPCError error = response.getError(); if (error != null) { From 1c558b47763a0808bba5487f2c1c75775417e913 Mon Sep 17 00:00:00 2001 From: Emmanuel Hugonnet Date: Wed, 29 Apr 2026 12:06:28 +0200 Subject: [PATCH 34/37] chore: Updating all the modules to 0.3.4-SNAPSHOT Preparing the release Signed-off-by: Emmanuel Hugonnet --- boms/sdk/pom.xml | 2 +- boms/test-utils/pom.xml | 2 +- client/base/pom.xml | 2 +- client/transport/grpc/pom.xml | 2 +- client/transport/jsonrpc/pom.xml | 2 +- client/transport/rest/pom.xml | 2 +- client/transport/spi/pom.xml | 2 +- common/pom.xml | 2 +- examples/cloud-deployment/server/pom.xml | 2 +- examples/helloworld/client/pom.xml | 2 +- examples/helloworld/pom.xml | 2 +- examples/helloworld/server/pom.xml | 2 +- extras/common/pom.xml | 2 +- extras/http-client-android/pom.xml | 2 +- extras/push-notification-config-store-database-jpa/pom.xml | 2 +- extras/queue-manager-replicated/core/pom.xml | 2 +- extras/queue-manager-replicated/pom.xml | 2 +- extras/queue-manager-replicated/replication-mp-reactive/pom.xml | 2 +- extras/queue-manager-replicated/tests-multi-instance/pom.xml | 2 +- .../tests-multi-instance/quarkus-app-1/pom.xml | 2 +- .../tests-multi-instance/quarkus-app-2/pom.xml | 2 +- .../tests-multi-instance/quarkus-common/pom.xml | 2 +- .../queue-manager-replicated/tests-multi-instance/tests/pom.xml | 2 +- extras/queue-manager-replicated/tests-single-instance/pom.xml | 2 +- extras/task-store-database-jpa/pom.xml | 2 +- http-client/pom.xml | 2 +- integrations/microprofile-config/pom.xml | 2 +- pom.xml | 2 +- reference/common/pom.xml | 2 +- reference/grpc/pom.xml | 2 +- reference/jsonrpc/pom.xml | 2 +- reference/rest/pom.xml | 2 +- server-common/pom.xml | 2 +- spec-grpc/pom.xml | 2 +- spec/pom.xml | 2 +- tck/pom.xml | 2 +- tests/server-common/pom.xml | 2 +- transport/grpc/pom.xml | 2 +- transport/jsonrpc/pom.xml | 2 +- transport/rest/pom.xml | 2 +- 40 files changed, 40 insertions(+), 40 deletions(-) diff --git a/boms/sdk/pom.xml b/boms/sdk/pom.xml index c0b51b73a..6a4520302 100644 --- a/boms/sdk/pom.xml +++ b/boms/sdk/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.4.0.Alpha1-SNAPSHOT + 0.3.4-SNAPSHOT ../../pom.xml diff --git a/boms/test-utils/pom.xml b/boms/test-utils/pom.xml index 5c0cdff0a..a5bad5ba3 100644 --- a/boms/test-utils/pom.xml +++ b/boms/test-utils/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.4.0.Alpha1-SNAPSHOT + 0.3.4-SNAPSHOT ../../pom.xml diff --git a/client/base/pom.xml b/client/base/pom.xml index 6a30adf20..1f02211d9 100644 --- a/client/base/pom.xml +++ b/client/base/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../pom.xml a2a-java-sdk-client diff --git a/client/transport/grpc/pom.xml b/client/transport/grpc/pom.xml index eb996b98d..baee53f0a 100644 --- a/client/transport/grpc/pom.xml +++ b/client/transport/grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-grpc diff --git a/client/transport/jsonrpc/pom.xml b/client/transport/jsonrpc/pom.xml index 04e2d096f..0693230a6 100644 --- a/client/transport/jsonrpc/pom.xml +++ b/client/transport/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-jsonrpc diff --git a/client/transport/rest/pom.xml b/client/transport/rest/pom.xml index 754dff4be..c4c97eb92 100644 --- a/client/transport/rest/pom.xml +++ b/client/transport/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-rest diff --git a/client/transport/spi/pom.xml b/client/transport/spi/pom.xml index a550244f0..a9751de43 100644 --- a/client/transport/spi/pom.xml +++ b/client/transport/spi/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../../pom.xml a2a-java-sdk-client-transport-spi diff --git a/common/pom.xml b/common/pom.xml index e23d23ecc..ad0082267 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT a2a-java-sdk-common diff --git a/examples/cloud-deployment/server/pom.xml b/examples/cloud-deployment/server/pom.xml index 6fa303112..c2c4215b5 100644 --- a/examples/cloud-deployment/server/pom.xml +++ b/examples/cloud-deployment/server/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../../pom.xml diff --git a/examples/helloworld/client/pom.xml b/examples/helloworld/client/pom.xml index 2effa47aa..bd694b8ac 100644 --- a/examples/helloworld/client/pom.xml +++ b/examples/helloworld/client/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-examples-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT a2a-java-sdk-examples-client diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index c61721150..344c513e3 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../pom.xml diff --git a/examples/helloworld/server/pom.xml b/examples/helloworld/server/pom.xml index 345f927d9..38c7f0cbd 100644 --- a/examples/helloworld/server/pom.xml +++ b/examples/helloworld/server/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-examples-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT a2a-java-sdk-examples-server diff --git a/extras/common/pom.xml b/extras/common/pom.xml index 90c58ffd9..e90712aa9 100644 --- a/extras/common/pom.xml +++ b/extras/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../pom.xml diff --git a/extras/http-client-android/pom.xml b/extras/http-client-android/pom.xml index 0b87a5466..ca5d64c00 100644 --- a/extras/http-client-android/pom.xml +++ b/extras/http-client-android/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../pom.xml a2a-java-sdk-http-client-android diff --git a/extras/push-notification-config-store-database-jpa/pom.xml b/extras/push-notification-config-store-database-jpa/pom.xml index bdbf2f7f0..6f6abad8a 100644 --- a/extras/push-notification-config-store-database-jpa/pom.xml +++ b/extras/push-notification-config-store-database-jpa/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../pom.xml a2a-java-extras-push-notification-config-store-database-jpa diff --git a/extras/queue-manager-replicated/core/pom.xml b/extras/queue-manager-replicated/core/pom.xml index d724d1773..4af5c1857 100644 --- a/extras/queue-manager-replicated/core/pom.xml +++ b/extras/queue-manager-replicated/core/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/pom.xml b/extras/queue-manager-replicated/pom.xml index fcd25cac0..321960821 100644 --- a/extras/queue-manager-replicated/pom.xml +++ b/extras/queue-manager-replicated/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../pom.xml diff --git a/extras/queue-manager-replicated/replication-mp-reactive/pom.xml b/extras/queue-manager-replicated/replication-mp-reactive/pom.xml index d5a23d61a..abdc1f4fc 100644 --- a/extras/queue-manager-replicated/replication-mp-reactive/pom.xml +++ b/extras/queue-manager-replicated/replication-mp-reactive/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/pom.xml index 7aacf4eb9..215bb1d7d 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml index 98b023799..0fa89d4e8 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml index 03c60ca7d..31991b790 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml index ad9d7e5f5..e6a0abcb1 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml index c0ba840c8..29dabaa3f 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-tests-multi-instance-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../pom.xml diff --git a/extras/queue-manager-replicated/tests-single-instance/pom.xml b/extras/queue-manager-replicated/tests-single-instance/pom.xml index 9a6c0c34b..607d4441a 100644 --- a/extras/queue-manager-replicated/tests-single-instance/pom.xml +++ b/extras/queue-manager-replicated/tests-single-instance/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-queue-manager-replicated-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../pom.xml diff --git a/extras/task-store-database-jpa/pom.xml b/extras/task-store-database-jpa/pom.xml index c078e3b57..9e930c762 100644 --- a/extras/task-store-database-jpa/pom.xml +++ b/extras/task-store-database-jpa/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../pom.xml a2a-java-extras-task-store-database-jpa diff --git a/http-client/pom.xml b/http-client/pom.xml index 7909ba46f..a962cf920 100644 --- a/http-client/pom.xml +++ b/http-client/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT a2a-java-sdk-http-client diff --git a/integrations/microprofile-config/pom.xml b/integrations/microprofile-config/pom.xml index 4bb6de22f..857bede79 100644 --- a/integrations/microprofile-config/pom.xml +++ b/integrations/microprofile-config/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../pom.xml a2a-java-sdk-microprofile-config diff --git a/pom.xml b/pom.xml index 110e27ee7..0122d9031 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT pom diff --git a/reference/common/pom.xml b/reference/common/pom.xml index 47450b9b9..4711e2eac 100644 --- a/reference/common/pom.xml +++ b/reference/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../pom.xml a2a-java-sdk-reference-common diff --git a/reference/grpc/pom.xml b/reference/grpc/pom.xml index 588d3a12e..f4a1a2e9a 100644 --- a/reference/grpc/pom.xml +++ b/reference/grpc/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../pom.xml diff --git a/reference/jsonrpc/pom.xml b/reference/jsonrpc/pom.xml index aa402bed1..ad977fd89 100644 --- a/reference/jsonrpc/pom.xml +++ b/reference/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../pom.xml a2a-java-sdk-reference-jsonrpc diff --git a/reference/rest/pom.xml b/reference/rest/pom.xml index ffec66fda..d4aab0a29 100644 --- a/reference/rest/pom.xml +++ b/reference/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../pom.xml a2a-java-sdk-reference-rest diff --git a/server-common/pom.xml b/server-common/pom.xml index a24035f52..c34e64471 100644 --- a/server-common/pom.xml +++ b/server-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT a2a-java-sdk-server-common diff --git a/spec-grpc/pom.xml b/spec-grpc/pom.xml index a7efca27f..efe0cc0ee 100644 --- a/spec-grpc/pom.xml +++ b/spec-grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT a2a-java-sdk-spec-grpc diff --git a/spec/pom.xml b/spec/pom.xml index f7f1f4bb0..842e59429 100644 --- a/spec/pom.xml +++ b/spec/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT a2a-java-sdk-spec diff --git a/tck/pom.xml b/tck/pom.xml index 06aab7b63..1cc331637 100644 --- a/tck/pom.xml +++ b/tck/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT a2a-tck-server diff --git a/tests/server-common/pom.xml b/tests/server-common/pom.xml index c7f671485..08f677202 100644 --- a/tests/server-common/pom.xml +++ b/tests/server-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../pom.xml a2a-java-sdk-tests-server-common diff --git a/transport/grpc/pom.xml b/transport/grpc/pom.xml index d535a3199..8f64120a4 100644 --- a/transport/grpc/pom.xml +++ b/transport/grpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../pom.xml a2a-java-sdk-transport-grpc diff --git a/transport/jsonrpc/pom.xml b/transport/jsonrpc/pom.xml index 010caff77..a4f7f74a5 100644 --- a/transport/jsonrpc/pom.xml +++ b/transport/jsonrpc/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../pom.xml a2a-java-sdk-transport-jsonrpc diff --git a/transport/rest/pom.xml b/transport/rest/pom.xml index c4d1c5ff6..eaab72b26 100644 --- a/transport/rest/pom.xml +++ b/transport/rest/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.3.4.Beta1-SNAPSHOT + 0.3.4-SNAPSHOT ../../pom.xml a2a-java-sdk-transport-rest From e4ade680b1c84b458e636b5a85893fe2a1084347 Mon Sep 17 00:00:00 2001 From: Emmanuel Hugonnet Date: Tue, 5 May 2026 12:30:12 +0200 Subject: [PATCH 35/37] fix: replace raw SSE string handling with SSEEvent model and SSEParser Introduce SSEEvent (data, id, event type, retry) and a spec-compliant SSEParser with buffer and line-length guards. Update postAsyncSSE and all SSE listeners across JDK, Android, JSONRPC, and REST transports to consume SSEEvent instead of raw strings. Using proper SSE Events Signed-off-by: Emmanuel Hugonnet --- .../transport/jsonrpc/JSONRPCTransport.java | 4 +- .../jsonrpc/sse/SSEEventListener.java | 9 +- .../jsonrpc/sse/SSEEventListenerTest.java | 13 +- .../client/transport/rest/RestTransport.java | 4 +- .../rest/sse/RestSSEEventListener.java | 9 +- .../a2a/client/http/AndroidA2AHttpClient.java | 454 ++++++++-------- .../io/a2a/client/http/A2AHttpClient.java | 4 +- .../io/a2a/client/http/JdkA2AHttpClient.java | 17 +- .../io/a2a/client/http/ServerSentEvent.java | 32 ++ .../client/http/ServerSentEventParser.java | 192 +++++++ .../a2a/client/http/A2ACardResolverTest.java | 2 +- .../http/AbstractA2AHttpClientSSETest.java | 139 ++++- .../http/ServerSentEventParserTest.java | 512 ++++++++++++++++++ .../AbstractA2ARequestHandlerTest.java | 3 +- .../tasks/PushNotificationSenderTest.java | 3 +- .../server/apps/common/TestHttpClient.java | 3 +- 16 files changed, 1135 insertions(+), 265 deletions(-) create mode 100644 http-client/src/main/java/io/a2a/client/http/ServerSentEvent.java create mode 100644 http-client/src/main/java/io/a2a/client/http/ServerSentEventParser.java create mode 100644 http-client/src/test/java/io/a2a/client/http/ServerSentEventParserTest.java diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java index b1c38afcc..3a12cf8a5 100644 --- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java +++ b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java @@ -134,7 +134,7 @@ public void sendMessageStreaming(MessageSendParams request, Consumer sseEventListener.onMessage(msg, ref.get()), + event -> sseEventListener.onMessage(event, ref.get()), throwable -> sseEventListener.onError(throwable, ref.get()), () -> { // Signal normal stream completion to error handler (null error means success) @@ -315,7 +315,7 @@ public void resubscribe(TaskIdParams request, Consumer event try { A2AHttpClient.PostBuilder builder = createPostBuilder(payloadAndHeaders); ref.set(builder.postAsyncSSE( - msg -> sseEventListener.onMessage(msg, ref.get()), + event -> sseEventListener.onMessage(event, ref.get()), throwable -> sseEventListener.onError(throwable, ref.get()), () -> { // Signal normal stream completion to error handler (null error means success) diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java index 0ab70027a..eef1a5d39 100644 --- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java +++ b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListener.java @@ -3,6 +3,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; +import io.a2a.client.http.ServerSentEvent; import io.a2a.json.JsonProcessingException; import io.a2a.json.JsonUtil; import io.a2a.spec.JSONRPCError; @@ -25,13 +26,13 @@ public SSEEventListener(Consumer eventHandler, this.errorHandler = errorHandler; } - public void onMessage(String message, Future completableFuture) { + public void onMessage(ServerSentEvent event, Future completableFuture) { try { - handleMessage(JsonParser.parseString(message).getAsJsonObject(), completableFuture); + handleMessage(JsonParser.parseString(event.data()).getAsJsonObject(), completableFuture); } catch (JsonSyntaxException e) { - log.warning("Failed to parse JSON message: " + message); + log.warning("Failed to parse JSON message: " + event.data()); } catch (JsonProcessingException e) { - log.warning("Failed to process JSON message: " + message); + log.warning("Failed to process JSON message: " + event.data()); } } diff --git a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListenerTest.java b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListenerTest.java index 8c4c1495e..b826d1671 100644 --- a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListenerTest.java +++ b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListenerTest.java @@ -13,6 +13,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import io.a2a.client.http.ServerSentEvent; import io.a2a.client.transport.jsonrpc.JsonStreamingMessages; import io.a2a.spec.Artifact; import io.a2a.spec.JSONRPCError; @@ -43,7 +44,7 @@ public void testOnEventWithTaskResult() throws Exception { JsonStreamingMessages.STREAMING_TASK_EVENT.indexOf("{")); // Call the onEvent method directly - listener.onMessage(eventData, null); + listener.onMessage(new ServerSentEvent(eventData), null); // Verify the event was processed correctly assertNotNull(receivedEvent.get()); @@ -68,7 +69,7 @@ public void testOnEventWithMessageResult() throws Exception { JsonStreamingMessages.STREAMING_MESSAGE_EVENT.indexOf("{")); // Call onEvent method - listener.onMessage(eventData, null); + listener.onMessage(new ServerSentEvent(eventData), null); // Verify the event was processed correctly assertNotNull(receivedEvent.get()); @@ -96,7 +97,7 @@ public void testOnEventWithTaskStatusUpdateEventEvent() throws Exception { JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT.indexOf("{")); // Call onEvent method - listener.onMessage(eventData, null); + listener.onMessage(new ServerSentEvent(eventData), null); // Verify the event was processed correctly assertNotNull(receivedEvent.get()); @@ -122,7 +123,7 @@ public void testOnEventWithTaskArtifactUpdateEventEvent() throws Exception { JsonStreamingMessages.STREAMING_ARTIFACT_UPDATE_EVENT.indexOf("{")); // Call onEvent method - listener.onMessage(eventData, null); + listener.onMessage(new ServerSentEvent(eventData), null); // Verify the event was processed correctly assertNotNull(receivedEvent.get()); @@ -154,7 +155,7 @@ public void testOnEventWithError() throws Exception { JsonStreamingMessages.STREAMING_ERROR_EVENT.indexOf("{")); // Call onEvent method - listener.onMessage(eventData, null); + listener.onMessage(new ServerSentEvent(eventData), null); // Verify the error was processed correctly assertNotNull(receivedError.get()); @@ -217,7 +218,7 @@ public void testOnEventWithFinalTaskStatusUpdateEventEventCancels() throws Excep // Call onEvent method CancelCapturingFuture future = new CancelCapturingFuture(); - listener.onMessage(eventData, future); + listener.onMessage(new ServerSentEvent(eventData), future); // Verify the event was processed correctly assertNotNull(receivedEvent.get()); diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java index c7a792527..b30b4c54b 100644 --- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java +++ b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java @@ -105,7 +105,7 @@ public void sendMessageStreaming(MessageSendParams messageSendParams, Consumer sseEventListener.onMessage(msg, ref.get()), + event -> sseEventListener.onMessage(event, ref.get()), throwable -> sseEventListener.onError(throwable, ref.get()), () -> { // We don't need to do anything special on completion @@ -294,7 +294,7 @@ public void resubscribe(TaskIdParams request, Consumer event String url = agentUrl + String.format("/v1/tasks/%1s:subscribe", request.id()); A2AHttpClient.PostBuilder postBuilder = createPostBuilder(url, payloadAndHeaders); ref.set(postBuilder.postAsyncSSE( - msg -> sseEventListener.onMessage(msg, ref.get()), + event -> sseEventListener.onMessage(event, ref.get()), throwable -> sseEventListener.onError(throwable, ref.get()), () -> { // We don't need to do anything special on completion diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/sse/RestSSEEventListener.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/sse/RestSSEEventListener.java index d0b130eee..91a3187e8 100644 --- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/sse/RestSSEEventListener.java +++ b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/sse/RestSSEEventListener.java @@ -11,6 +11,7 @@ import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.util.JsonFormat; +import io.a2a.client.http.ServerSentEvent; import io.a2a.client.transport.rest.RestErrorMapper; import io.a2a.grpc.StreamResponse; import io.a2a.grpc.utils.ProtoUtils; @@ -29,14 +30,14 @@ public RestSSEEventListener(Consumer eventHandler, this.errorHandler = errorHandler; } - public void onMessage(String message, @Nullable Future completableFuture) { + public void onMessage(ServerSentEvent event, @Nullable Future completableFuture) { try { - log.fine("Streaming message received: " + message); + log.fine("Streaming message received: " + event.data()); io.a2a.grpc.StreamResponse.Builder builder = io.a2a.grpc.StreamResponse.newBuilder(); - JsonFormat.parser().merge(message, builder); + JsonFormat.parser().merge(event.data(), builder); handleMessage(builder.build()); } catch (InvalidProtocolBufferException e) { - errorHandler.accept(RestErrorMapper.mapRestError(message, 500)); + errorHandler.accept(RestErrorMapper.mapRestError(event.data(), 500)); } } diff --git a/extras/http-client-android/src/main/java/io/a2a/client/http/AndroidA2AHttpClient.java b/extras/http-client-android/src/main/java/io/a2a/client/http/AndroidA2AHttpClient.java index c023f083d..1ff7b78ad 100644 --- a/extras/http-client-android/src/main/java/io/a2a/client/http/AndroidA2AHttpClient.java +++ b/extras/http-client-android/src/main/java/io/a2a/client/http/AndroidA2AHttpClient.java @@ -24,267 +24,271 @@ import java.util.concurrent.Executors; import java.util.function.Consumer; -/** Android-specific implementation of {@link A2AHttpClient} using {@link HttpURLConnection}. */ +/** + * Android-specific implementation of {@link A2AHttpClient} using {@link HttpURLConnection}. + */ public class AndroidA2AHttpClient implements A2AHttpClient { - private static final Executor NET_EXECUTOR = Executors.newCachedThreadPool(r -> { - Thread t = new Thread(r, "A2A-Android-Net"); - t.setDaemon(true); - return t; - }); - - @Override - public GetBuilder createGet() { - return new AndroidGetBuilder(); - } - - @Override - public PostBuilder createPost() { - return new AndroidPostBuilder(); - } - - @Override - public DeleteBuilder createDelete() { - return new AndroidDeleteBuilder(); - } - - private abstract static class AndroidBuilder> implements Builder { - protected String url = ""; - protected Map headers = new HashMap<>(); + private static final Executor NET_EXECUTOR = Executors.newCachedThreadPool(r -> { + Thread t = new Thread(r, "A2A-Android-Net"); + t.setDaemon(true); + return t; + }); @Override - public T url(String url) { - this.url = url; - return self(); + public GetBuilder createGet() { + return new AndroidGetBuilder(); } @Override - public T addHeader(String name, String value) { - headers.put(name, value); - return self(); + public PostBuilder createPost() { + return new AndroidPostBuilder(); } @Override - public T addHeaders(Map headers) { - if (headers != null) { - this.headers.putAll(headers); - } - return self(); + public DeleteBuilder createDelete() { + return new AndroidDeleteBuilder(); } - @SuppressWarnings("unchecked") - protected T self() { - return (T) this; - } + private abstract static class AndroidBuilder> implements Builder { - protected HttpURLConnection createConnection(String method, boolean isSSE) throws IOException { - URL urlObj; - try { - urlObj = new URI(url).toURL(); - } catch (URISyntaxException e) { - throw new MalformedURLException("Invalid URL: " + url); - } - HttpURLConnection connection = (HttpURLConnection) urlObj.openConnection(); - connection.setRequestMethod(method); - connection.setConnectTimeout(15000); // 15 seconds - connection.setReadTimeout(60000); // 60 seconds - for (Map.Entry header : headers.entrySet()) { - connection.setRequestProperty(header.getKey(), header.getValue()); - } - if (isSSE) { - connection.setRequestProperty("Accept", "text/event-stream"); - } - return connection; - } + protected String url = ""; + protected Map headers = new HashMap<>(); - protected static String readStreamWithLimit(InputStream is) throws IOException { - if (is == null) { - return ""; - } - int maxResponseSize = 10 * 1024 * 1024; // 10 MB - try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { - StringBuilder sb = new StringBuilder(); - String line; - boolean first = true; - while ((line = reader.readLine()) != null) { - if (sb.length() + line.length() > maxResponseSize) { - throw new IOException("Response size exceeds limit"); - } - if (!first) { - sb.append('\n'); - } - sb.append(line); - first = false; + @Override + public T url(String url) { + this.url = url; + return self(); } - return sb.toString(); - } - } - protected A2AHttpResponse execute(HttpURLConnection connection) throws IOException { - int status = connection.getResponseCode(); - String body = ""; - try (InputStream is = - (status >= HTTP_OK && status < HTTP_MULT_CHOICE) - ? connection.getInputStream() - : connection.getErrorStream()) { - body = readStreamWithLimit(is); - } - - if (status == HTTP_UNAUTHORIZED) { - throw new IOException(A2AErrorMessages.AUTHENTICATION_FAILED); - } else if (status == HTTP_FORBIDDEN) { - throw new IOException(A2AErrorMessages.AUTHORIZATION_FAILED); - } - - return new AndroidHttpResponse(status, body); - } + @Override + public T addHeader(String name, String value) { + headers.put(name, value); + return self(); + } + + @Override + public T addHeaders(Map headers) { + if (headers != null) { + this.headers.putAll(headers); + } + return self(); + } - protected void processSSEResponse( - HttpURLConnection connection, - Consumer messageConsumer, - Consumer errorConsumer, - Runnable completeRunnable) { - try { - int status = connection.getResponseCode(); - if (status != HTTP_OK) { - if (status == HTTP_UNAUTHORIZED) { - errorConsumer.accept(new IOException(A2AErrorMessages.AUTHENTICATION_FAILED)); - return; - } else if (status == HTTP_FORBIDDEN) { - errorConsumer.accept(new IOException(A2AErrorMessages.AUTHORIZATION_FAILED)); - return; - } - - String errorBody = ""; - try (InputStream es = connection.getErrorStream()) { - errorBody = readStreamWithLimit(es); - } - errorConsumer.accept( - new IOException("Request failed with status " + status + ":" + errorBody)); - return; + @SuppressWarnings("unchecked") + protected T self() { + return (T) this; } - try (InputStream is = connection.getInputStream(); - BufferedReader reader = - new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { - String line; - while ((line = reader.readLine()) != null) { - if (line.startsWith("data:")) { - String data = line.substring(5).trim(); - if (!data.isEmpty()) { - messageConsumer.accept(data); - } + protected HttpURLConnection createConnection(String method, boolean isSSE) throws IOException { + URL urlObj; + try { + urlObj = new URI(url).toURL(); + } catch (URISyntaxException e) { + throw new MalformedURLException("Invalid URL: " + url); + } + HttpURLConnection connection = (HttpURLConnection) urlObj.openConnection(); + connection.setRequestMethod(method); + connection.setConnectTimeout(15000); // 15 seconds + connection.setReadTimeout(60000); // 60 seconds + for (Map.Entry header : headers.entrySet()) { + connection.setRequestProperty(header.getKey(), header.getValue()); } - } - completeRunnable.run(); + if (isSSE) { + connection.setRequestProperty("Accept", "text/event-stream"); + } + return connection; } - } catch (Exception e) { - errorConsumer.accept(e); - } finally { - connection.disconnect(); - } - } - protected CompletableFuture executeAsyncSSE( - HttpURLConnection connection, - Consumer messageConsumer, - Consumer errorConsumer, - Runnable completeRunnable) { - return CompletableFuture.runAsync( - () -> processSSEResponse(connection, messageConsumer, errorConsumer, completeRunnable), - NET_EXECUTOR); - } - } + protected static String readStreamWithLimit(InputStream is) throws IOException { + if (is == null) { + return ""; + } + int maxResponseSize = 10 * 1024 * 1024; // 10 MB + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + StringBuilder sb = new StringBuilder(); + String line; + boolean first = true; + while ((line = reader.readLine()) != null) { + if (sb.length() + line.length() > maxResponseSize) { + throw new IOException("Response size exceeds limit"); + } + if (!first) { + sb.append('\n'); + } + sb.append(line); + first = false; + } + return sb.toString(); + } + } - private static class AndroidGetBuilder extends AndroidBuilder implements GetBuilder { - @Override - public A2AHttpResponse get() throws IOException { - HttpURLConnection connection = createConnection("GET", false); - try { - return execute(connection); - } catch (IOException e) { - connection.disconnect(); - throw e; - } - } + protected A2AHttpResponse execute(HttpURLConnection connection) throws IOException { + int status = connection.getResponseCode(); + String body = ""; + try (InputStream is + = (status >= HTTP_OK && status < HTTP_MULT_CHOICE) + ? connection.getInputStream() + : connection.getErrorStream()) { + body = readStreamWithLimit(is); + } - @Override - public CompletableFuture getAsyncSSE( - Consumer messageConsumer, - Consumer errorConsumer, - Runnable completeRunnable) - throws IOException { - HttpURLConnection connection = createConnection("GET", true); - return executeAsyncSSE(connection, messageConsumer, errorConsumer, completeRunnable); - } - } + if (status == HTTP_UNAUTHORIZED) { + throw new IOException(A2AErrorMessages.AUTHENTICATION_FAILED); + } else if (status == HTTP_FORBIDDEN) { + throw new IOException(A2AErrorMessages.AUTHORIZATION_FAILED); + } - private static class AndroidPostBuilder extends AndroidBuilder - implements PostBuilder { - private String body = ""; + return new AndroidHttpResponse(status, body); + } - @Override - public PostBuilder body(String body) { - this.body = body; - return this; + protected void processSSEResponse( + HttpURLConnection connection, + Consumer messageConsumer, + Consumer errorConsumer, + Runnable completeRunnable) { + try { + int status = connection.getResponseCode(); + if (status != HTTP_OK) { + if (status == HTTP_UNAUTHORIZED) { + errorConsumer.accept(new IOException(A2AErrorMessages.AUTHENTICATION_FAILED)); + return; + } else if (status == HTTP_FORBIDDEN) { + errorConsumer.accept(new IOException(A2AErrorMessages.AUTHORIZATION_FAILED)); + return; + } + + String errorBody = ""; + try (InputStream es = connection.getErrorStream()) { + errorBody = readStreamWithLimit(es); + } + errorConsumer.accept( + new IOException("Request failed with status " + status + ":" + errorBody)); + return; + } + + ServerSentEventParser sseParser = new ServerSentEventParser(messageConsumer, errorConsumer); + + try (InputStream is = connection.getInputStream(); BufferedReader reader + = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + sseParser.processLine(line); + } + sseParser.flush(); + completeRunnable.run(); + } + } catch (Exception e) { + errorConsumer.accept(e); + } finally { + connection.disconnect(); + } + } + + protected CompletableFuture executeAsyncSSE( + HttpURLConnection connection, + Consumer messageConsumer, + Consumer errorConsumer, + Runnable completeRunnable) { + return CompletableFuture.runAsync( + () -> processSSEResponse(connection, messageConsumer, errorConsumer, completeRunnable), + NET_EXECUTOR); + } } - @Override - public A2AHttpResponse post() throws IOException { - HttpURLConnection connection = createConnection("POST", false); - connection.setDoOutput(true); - try { - try (OutputStream os = connection.getOutputStream()) { - os.write(body.getBytes(StandardCharsets.UTF_8)); + private static class AndroidGetBuilder extends AndroidBuilder implements GetBuilder { + + @Override + public A2AHttpResponse get() throws IOException { + HttpURLConnection connection = createConnection("GET", false); + try { + return execute(connection); + } catch (IOException e) { + connection.disconnect(); + throw e; + } + } + + @Override + public CompletableFuture getAsyncSSE( + Consumer messageConsumer, + Consumer errorConsumer, + Runnable completeRunnable) + throws IOException, InterruptedException { + HttpURLConnection connection = createConnection("GET", true); + return executeAsyncSSE(connection, messageConsumer, errorConsumer, completeRunnable); } - return execute(connection); - } catch (IOException e) { - connection.disconnect(); - throw e; - } } - @Override - public CompletableFuture postAsyncSSE( - Consumer messageConsumer, - Consumer errorConsumer, - Runnable completeRunnable) - throws IOException { - HttpURLConnection connection = createConnection("POST", true); - connection.setDoOutput(true); - - return CompletableFuture.runAsync( - () -> { + private static class AndroidPostBuilder extends AndroidBuilder + implements PostBuilder { + + private String body = ""; + + @Override + public PostBuilder body(String body) { + this.body = body; + return this; + } + + @Override + public A2AHttpResponse post() throws IOException { + HttpURLConnection connection = createConnection("POST", false); + connection.setDoOutput(true); try { - try (OutputStream os = connection.getOutputStream()) { - os.write(body.getBytes(StandardCharsets.UTF_8)); - } - processSSEResponse(connection, messageConsumer, errorConsumer, completeRunnable); - } catch (Exception e) { - errorConsumer.accept(e); + try (OutputStream os = connection.getOutputStream()) { + os.write(body.getBytes(StandardCharsets.UTF_8)); + } + return execute(connection); + } catch (IOException e) { + connection.disconnect(); + throw e; } - }, NET_EXECUTOR); + } + + @Override + public CompletableFuture postAsyncSSE( + Consumer messageConsumer, + Consumer errorConsumer, + Runnable completeRunnable) + throws IOException, InterruptedException { + HttpURLConnection connection = createConnection("POST", true); + connection.setDoOutput(true); + + return CompletableFuture.runAsync( + () -> { + try { + try (OutputStream os = connection.getOutputStream()) { + os.write(body.getBytes(StandardCharsets.UTF_8)); + } + processSSEResponse(connection, messageConsumer, errorConsumer, completeRunnable); + } catch (Exception e) { + errorConsumer.accept(e); + } + }, NET_EXECUTOR); + } } - } - private static class AndroidDeleteBuilder extends AndroidBuilder - implements DeleteBuilder { - @Override - public A2AHttpResponse delete() throws IOException { - HttpURLConnection connection = createConnection("DELETE", false); - try { - return execute(connection); - } catch (IOException e) { - connection.disconnect(); - throw e; - } + private static class AndroidDeleteBuilder extends AndroidBuilder + implements DeleteBuilder { + + @Override + public A2AHttpResponse delete() throws IOException { + HttpURLConnection connection = createConnection("DELETE", false); + try { + return execute(connection); + } catch (IOException e) { + connection.disconnect(); + throw e; + } + } } - } - private record AndroidHttpResponse(int status, String body) implements A2AHttpResponse { - @Override - public boolean success() { - return status >= HTTP_OK && status < HTTP_MULT_CHOICE; + private record AndroidHttpResponse(int status, String body) implements A2AHttpResponse { + + @Override + public boolean success() { + return status >= HTTP_OK && status < HTTP_MULT_CHOICE; + } } - } } diff --git a/http-client/src/main/java/io/a2a/client/http/A2AHttpClient.java b/http-client/src/main/java/io/a2a/client/http/A2AHttpClient.java index 52c252a8f..cccfe38ec 100644 --- a/http-client/src/main/java/io/a2a/client/http/A2AHttpClient.java +++ b/http-client/src/main/java/io/a2a/client/http/A2AHttpClient.java @@ -22,7 +22,7 @@ interface Builder> { interface GetBuilder extends Builder { A2AHttpResponse get() throws IOException, InterruptedException; CompletableFuture getAsyncSSE( - Consumer messageConsumer, + Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) throws IOException, InterruptedException; } @@ -31,7 +31,7 @@ interface PostBuilder extends Builder { PostBuilder body(String body); A2AHttpResponse post() throws IOException, InterruptedException; CompletableFuture postAsyncSSE( - Consumer messageConsumer, + Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) throws IOException, InterruptedException; } diff --git a/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClient.java b/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClient.java index 0759ede26..30b7a6cf9 100644 --- a/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClient.java +++ b/http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClient.java @@ -101,10 +101,12 @@ protected void checkAuthErrors(HttpResponse response) throws IOException protected CompletableFuture asyncRequest( HttpRequest request, - Consumer messageConsumer, + Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable ) { + ServerSentEventParser sseParser = new ServerSentEventParser(messageConsumer, errorConsumer); + Flow.Subscriber subscriber = new Flow.Subscriber() { private Flow.@Nullable Subscription subscription; private volatile boolean errorRaised = false; @@ -117,12 +119,8 @@ public void onSubscribe(Flow.Subscription subscription) { @Override public void onNext(String item) { - // SSE messages sometimes start with "data:". Strip that off - if (item != null && item.startsWith("data:")) { - item = item.substring(5).trim(); - if (!item.isEmpty()) { - messageConsumer.accept(item); - } + if (item != null) { + sseParser.processLine(item); } if (subscription != null) { subscription.request(1); @@ -143,6 +141,7 @@ public void onError(Throwable throwable) { @Override public void onComplete() { if (!errorRaised) { + sseParser.flush(); completeRunnable.run(); } if (subscription != null) { @@ -226,7 +225,7 @@ public A2AHttpResponse get() throws IOException, InterruptedException { @Override public CompletableFuture getAsyncSSE( - Consumer messageConsumer, + Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) throws IOException, InterruptedException { HttpRequest request = createRequestBuilder(true) @@ -282,7 +281,7 @@ public A2AHttpResponse post() throws IOException, InterruptedException { @Override public CompletableFuture postAsyncSSE( - Consumer messageConsumer, + Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) throws IOException, InterruptedException { HttpRequest request = createRequestBuilder(true) diff --git a/http-client/src/main/java/io/a2a/client/http/ServerSentEvent.java b/http-client/src/main/java/io/a2a/client/http/ServerSentEvent.java new file mode 100644 index 000000000..9b490b4b3 --- /dev/null +++ b/http-client/src/main/java/io/a2a/client/http/ServerSentEvent.java @@ -0,0 +1,32 @@ +package io.a2a.client.http; + +import io.a2a.util.Assert; +import org.jspecify.annotations.Nullable; + +/** + * Represents a parsed Server-Sent Event (SSE). + *

+ * Each instance carries the fields defined by the SSE specification: + *

    + *
  • {@code data} — the event payload, already concatenated from one or more {@code data:} lines
  • + *
  • {@code eventType} — the event type from the {@code event:} field; never null, defaults to {@code "message"}
  • + *
  • {@code id} — the event ID from the {@code id:} field; null if absent
  • + *
  • {@code retry} — the reconnection interval in milliseconds from the {@code retry:} field; null if absent
  • + *
+ */ +public record ServerSentEvent(String data, String eventType, @Nullable String id, @Nullable Long retry) { + + /** + * Default event type per the SSE specification when no {@code event:} field is present. + */ + public static final String DEFAULT_EVENT_TYPE = "message"; + + public ServerSentEvent { + Assert.checkNotNullParam("data", data); + Assert.checkNotNullParam("eventType", eventType); + } + + public ServerSentEvent(String data) { + this(data, DEFAULT_EVENT_TYPE, null, null); + } +} diff --git a/http-client/src/main/java/io/a2a/client/http/ServerSentEventParser.java b/http-client/src/main/java/io/a2a/client/http/ServerSentEventParser.java new file mode 100644 index 000000000..a4815806b --- /dev/null +++ b/http-client/src/main/java/io/a2a/client/http/ServerSentEventParser.java @@ -0,0 +1,192 @@ +package io.a2a.client.http; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.logging.Logger; +import org.jspecify.annotations.Nullable; + +/** + * Streaming parser for Server-Sent Events (SSE). + *

+ * Feed lines one at a time via {@link #processLine}; call {@link #flush} when the stream ends. + * Not thread-safe — each connection should use its own instance. + */ +public class ServerSentEventParser { + private static final Logger log = Logger.getLogger(ServerSentEventParser.class.getName()); + + private static final int MAX_BUFFER_SIZE = 1000; + private static final int MAX_BUFFER_BYTES = 1024 * 1024; // 1 MB + private static final int MAX_LINE_LENGTH = 65536; // 64 KB + + private final Consumer eventConsumer; + private final @Nullable Consumer errorConsumer; + private final List dataBuffer = new ArrayList<>(); + private int dataBufferBytes = 0; + private @Nullable String eventType; + private @Nullable String currentEventId; + private @Nullable String lastEventId; + private @Nullable Long retry; + // Set when the current event block is corrupt (line too long, buffer overflow). + // All further fields are ignored until the next empty-line boundary. + private boolean skippingCurrentEvent = false; + + public ServerSentEventParser(Consumer eventConsumer) { + this(eventConsumer, null); + } + + public ServerSentEventParser(Consumer eventConsumer, @Nullable Consumer errorConsumer) { + this.eventConsumer = eventConsumer; + this.errorConsumer = errorConsumer; + } + + /** + * Processes a single line from the SSE stream. An empty line dispatches any buffered event. + * A {@code null} line is routed to the error consumer (or logged) without affecting the current event block. + */ + public void processLine(@Nullable String line) { + if (line == null) { + handleError(new IllegalArgumentException("Line cannot be null")); + return; + } + + // Check line length to prevent DoS; corrupt the current event so it is not dispatched + if (line.length() > MAX_LINE_LENGTH) { + handleError(new IllegalArgumentException("Line exceeds maximum length of " + MAX_LINE_LENGTH + " characters")); + skippingCurrentEvent = true; + dataBuffer.clear(); + dataBufferBytes = 0; + return; + } + + if (skippingCurrentEvent && !line.isEmpty()) { + return; + } + + // Empty line - dispatch the buffered event + if (line.isEmpty()) { + dispatchEvent(); + return; + } + + // Comment line - ignore + if (line.startsWith(":")) { + return; + } + + // Parse field and value + int colonIndex = line.indexOf(':'); + if (colonIndex == -1) { + // Field with no value + processField(line, ""); + } else { + String field = line.substring(0, colonIndex); + String value = line.substring(colonIndex + 1); + + // Remove optional leading space from value + if (value.startsWith(" ")) { + value = value.substring(1); + } + + processField(field, value); + } + } + + private void processField(String field, String value) { + switch (field) { + case "data" -> { + // Check line count to prevent DoS; corrupt and skip the rest of this event block + if (dataBuffer.size() >= MAX_BUFFER_SIZE) { + handleError(new IllegalStateException("SSE data buffer exceeded maximum size of " + MAX_BUFFER_SIZE + " lines")); + skippingCurrentEvent = true; + dataBuffer.clear(); + dataBufferBytes = 0; + return; + } + // Check total byte size to prevent OOM on large streams + if (dataBufferBytes + value.length() > MAX_BUFFER_BYTES) { + handleError(new IllegalStateException("SSE data buffer exceeded maximum byte size of " + MAX_BUFFER_BYTES + " bytes")); + skippingCurrentEvent = true; + dataBuffer.clear(); + dataBufferBytes = 0; + return; + } + dataBuffer.add(value); + dataBufferBytes += value.length(); + } + case "event" -> eventType = value; + case "id" -> { + // Per SSE spec: ignore the id field if the value contains a U+0000 NULL character. + // An empty value is valid and clears the last event ID buffer on dispatch. + if (value.indexOf('\0') == -1) { + currentEventId = value; + } + } + case "retry" -> { + // Per SSE spec: ignore the retry field unless the value consists entirely of ASCII digits. + if (!value.isEmpty() && value.chars().allMatch(c -> c >= '0' && c <= '9')) { + try { + retry = Long.parseLong(value); + } catch (NumberFormatException e) { + // Value is all digits but too large for long; log and ignore per spec. + log.fine("Ignoring retry value out of long range: " + value); + } + } else { + log.fine("Ignoring non-digit retry value: " + value); + } + } + default -> { + // Unknown field - ignore per spec + log.fine("Ignoring unknown SSE field: " + field); + } + } + } + + private void dispatchEvent() { + // Per SSE spec: update lastEventId before checking data, so ID-only events (e.g. heartbeats) are tracked + if (currentEventId != null) { + lastEventId = currentEventId; + } + + String data = String.join("\n", dataBuffer); + String type = eventType; + String id = currentEventId; + + // Always reset at event boundary, regardless of whether an event is dispatched + dataBuffer.clear(); + dataBufferBytes = 0; + eventType = null; + skippingCurrentEvent = false; + // currentEventId is NOT reset — it persists per the SSE specification + + // Per SSE spec: don't dispatch if data is empty (also covers limit-violation blocks, whose buffer was cleared) + if (data.isEmpty()) { + return; + } + + eventConsumer.accept(new ServerSentEvent(data, type != null ? type : ServerSentEvent.DEFAULT_EVENT_TYPE, id, retry)); + } + + /** Dispatches any buffered data not yet followed by an empty line. Call when the stream ends. */ + public void flush() { + dispatchEvent(); + } + + /** Returns the last event ID received, or {@code null} if none. */ + public @Nullable String getLastEventId() { + return lastEventId; + } + + /** Returns the reconnection interval in milliseconds from the last {@code retry:} field, or {@code null} if none. */ + public @Nullable Long getRetry() { + return retry; + } + + private void handleError(Throwable error) { + if (errorConsumer != null) { + errorConsumer.accept(error); + } else { + log.warning("SSE parsing error: " + error.getMessage()); + } + } +} diff --git a/http-client/src/test/java/io/a2a/client/http/A2ACardResolverTest.java b/http-client/src/test/java/io/a2a/client/http/A2ACardResolverTest.java index 3acad3b4f..497adc4b2 100644 --- a/http-client/src/test/java/io/a2a/client/http/A2ACardResolverTest.java +++ b/http-client/src/test/java/io/a2a/client/http/A2ACardResolverTest.java @@ -152,7 +152,7 @@ public String body() { } @Override - public CompletableFuture getAsyncSSE(Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) throws IOException, InterruptedException { + public CompletableFuture getAsyncSSE(Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) throws IOException, InterruptedException { return null; } diff --git a/http-client/src/test/java/io/a2a/client/http/AbstractA2AHttpClientSSETest.java b/http-client/src/test/java/io/a2a/client/http/AbstractA2AHttpClientSSETest.java index 2b9a0f349..fc36a3339 100644 --- a/http-client/src/test/java/io/a2a/client/http/AbstractA2AHttpClientSSETest.java +++ b/http-client/src/test/java/io/a2a/client/http/AbstractA2AHttpClientSSETest.java @@ -62,7 +62,7 @@ public void testGetAsyncSSE() throws Exception { client.createGet() .url(getBaseUrl() + "/sse") .getAsyncSSE( - events::add, + event -> events.add(event.data()), error::set, latch::countDown ); @@ -95,7 +95,7 @@ public void testPostAsyncSSE() throws Exception { .url(getBaseUrl() + "/sse") .body("{\"subscribe\":true}") .postAsyncSSE( - events::add, + event -> events.add(event.data()), error::set, latch::countDown ); @@ -114,7 +114,7 @@ public void testSSEDataPrefixStripping() throws Exception { .respond(response() .withStatusCode(200) .withHeader("Content-Type", "text/event-stream") - .withBody("data: content here\n\ndata:no space\n\ndata: extra spaces \n\n")); + .withBody("data: content here\n\ndata:no space\n\ndata: extra spaces \n\n")); CountDownLatch latch = new CountDownLatch(1); List events = new ArrayList<>(); @@ -123,7 +123,7 @@ public void testSSEDataPrefixStripping() throws Exception { client.createGet() .url(getBaseUrl() + "/sse") .getAsyncSSE( - events::add, + event -> events.add(event.data()), error::set, latch::countDown ); @@ -132,7 +132,8 @@ public void testSSEDataPrefixStripping() throws Exception { assertNull(error.get()); assertTrue(events.contains("content here"), "Should have stripped 'data: ' prefix"); assertTrue(events.contains("no space"), "Should handle 'data:' without space"); - assertTrue(events.contains("extra spaces"), "Should trim whitespace"); + // SSE spec: only first space after colon is removed, rest is preserved + assertTrue(events.contains(" extra spaces "), "Should preserve whitespace after first space"); } @Test @@ -209,7 +210,7 @@ public void testSSEEmptyLinesIgnored() throws Exception { client.createGet() .url(getBaseUrl() + "/sse") .getAsyncSSE( - events::add, + event -> events.add(event.data()), error::set, latch::countDown ); @@ -243,7 +244,7 @@ public void testSSEHeaderPropagation() throws Exception { .url(getBaseUrl() + "/sse") .addHeader("Authorization", "Bearer token") .getAsyncSSE( - events::add, + event -> events.add(event.data()), error::set, latch::countDown ); @@ -252,4 +253,128 @@ public void testSSEHeaderPropagation() throws Exception { assertNull(error.get()); assertTrue(events.contains("authenticated")); } + + @Test + public void testSSETypedEvents() throws Exception { + mockServer + .when(request().withMethod("GET").withPath("/sse")) + .respond(response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody("event: update\ndata: payload1\n\nevent: delete\ndata: payload2\n\ndata: no-type\n\n")); + + CountDownLatch latch = new CountDownLatch(1); + List events = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + + client.createGet() + .url(getBaseUrl() + "/sse") + .getAsyncSSE(events::add, error::set, latch::countDown); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertNull(error.get()); + assertEquals(3, events.size()); + assertEquals("update", events.get(0).eventType()); + assertEquals("payload1", events.get(0).data()); + assertEquals("delete", events.get(1).eventType()); + assertEquals("payload2", events.get(1).data()); + assertEquals("message", events.get(2).eventType(), "Event without 'event:' field should default to 'message'"); + } + + @Test + public void testSSEEventIdAndLastEventId() throws Exception { + mockServer + .when(request().withMethod("GET").withPath("/sse")) + .respond(response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody("id: 42\ndata: identified\n\ndata: no-id\n\n")); + + CountDownLatch latch = new CountDownLatch(1); + List events = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + + client.createGet() + .url(getBaseUrl() + "/sse") + .getAsyncSSE(events::add, error::set, latch::countDown); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertNull(error.get()); + assertEquals(2, events.size()); + assertEquals("42", events.get(0).id()); + assertEquals("42", events.get(1).id(), "Event without 'id:' field inherits last event ID per SSE spec"); + } + + @Test + public void testSSEMultiLineData() throws Exception { + mockServer + .when(request().withMethod("GET").withPath("/sse")) + .respond(response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody("data: line1\ndata: line2\ndata: line3\n\n")); + + CountDownLatch latch = new CountDownLatch(1); + List events = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + + client.createGet() + .url(getBaseUrl() + "/sse") + .getAsyncSSE(events::add, error::set, latch::countDown); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertNull(error.get()); + assertEquals(1, events.size()); + assertEquals("line1\nline2\nline3", events.get(0).data()); + } + + @Test + public void testSSEStreamEndingWithoutTrailingEmptyLine() throws Exception { + // Stream ends mid-event with no terminal empty line — flush() must dispatch the buffered data + mockServer + .when(request().withMethod("GET").withPath("/sse")) + .respond(response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody("data: flushed-event")); + + CountDownLatch latch = new CountDownLatch(1); + List events = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + + client.createGet() + .url(getBaseUrl() + "/sse") + .getAsyncSSE(events::add, error::set, latch::countDown); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertNull(error.get()); + assertEquals(1, events.size(), "Buffered event must be dispatched on stream end"); + assertEquals("flushed-event", events.get(0).data()); + } + + @Test + public void testPostSSETypedEvents() throws Exception { + mockServer + .when(request().withMethod("POST").withPath("/sse")) + .respond(response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody("event: result\nid: 99\ndata: done\n\n")); + + CountDownLatch latch = new CountDownLatch(1); + List events = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + + client.createPost() + .url(getBaseUrl() + "/sse") + .body("{}") + .postAsyncSSE(events::add, error::set, latch::countDown); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertNull(error.get()); + assertEquals(1, events.size()); + assertEquals("result", events.get(0).eventType()); + assertEquals("99", events.get(0).id()); + assertEquals("done", events.get(0).data()); + } } diff --git a/http-client/src/test/java/io/a2a/client/http/ServerSentEventParserTest.java b/http-client/src/test/java/io/a2a/client/http/ServerSentEventParserTest.java new file mode 100644 index 000000000..456b54bce --- /dev/null +++ b/http-client/src/test/java/io/a2a/client/http/ServerSentEventParserTest.java @@ -0,0 +1,512 @@ +package io.a2a.client.http; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; + +public class ServerSentEventParserTest { + + @Test + public void testSimpleDataEvent() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine("data: Hello World"); + parser.processLine(""); + + assertEquals(1, events.size()); + assertEquals("Hello World", events.get(0).data()); + assertEquals("message", events.get(0).eventType()); + assertNull(events.get(0).id()); + assertNull(events.get(0).retry()); + } + + @Test + public void testMultiLineDataEvent() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine("data: First line"); + parser.processLine("data: Second line"); + parser.processLine("data: Third line"); + parser.processLine(""); + + assertEquals(1, events.size()); + assertEquals("First line\nSecond line\nThird line", events.get(0).data()); + } + + @Test + public void testEventWithType() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine("event: custom"); + parser.processLine("data: Custom event data"); + parser.processLine(""); + + assertEquals(1, events.size()); + assertEquals("Custom event data", events.get(0).data()); + assertEquals("custom", events.get(0).eventType()); + } + + @Test + public void testEventWithId() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine("id: 123"); + parser.processLine("data: Event with ID"); + parser.processLine(""); + + assertEquals(1, events.size()); + assertEquals("Event with ID", events.get(0).data()); + assertEquals("123", events.get(0).id()); + assertEquals("123", parser.getLastEventId()); + } + + @Test + public void testEventWithRetry() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine("retry: 5000"); + parser.processLine("data: Event with retry"); + parser.processLine(""); + + assertEquals(1, events.size()); + assertEquals("Event with retry", events.get(0).data()); + assertEquals(5000L, events.get(0).retry()); + assertEquals(5000L, parser.getRetry()); + } + + @Test + public void testCompleteEvent() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine("event: notification"); + parser.processLine("id: msg-001"); + parser.processLine("retry: 3000"); + parser.processLine("data: Complete event"); + parser.processLine(""); + + assertEquals(1, events.size()); + ServerSentEvent event = events.get(0); + assertEquals("Complete event", event.data()); + assertEquals("notification", event.eventType()); + assertEquals("msg-001", event.id()); + assertEquals(3000L, event.retry()); + } + + @Test + public void testMultipleEvents() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + // First event + parser.processLine("event: type1"); + parser.processLine("data: First"); + parser.processLine(""); + + // Second event + parser.processLine("event: type2"); + parser.processLine("data: Second"); + parser.processLine(""); + + assertEquals(2, events.size()); + assertEquals("First", events.get(0).data()); + assertEquals("type1", events.get(0).eventType()); + assertEquals("Second", events.get(1).data()); + assertEquals("type2", events.get(1).eventType()); + } + + @Test + public void testEmptyIdClearsLastEventId() { + // Per WHATWG SSE spec: id: with an empty value sets lastEventId to "" (clears it). + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine("id: initial"); + parser.processLine("data: First"); + parser.processLine(""); + + parser.processLine("id:"); + parser.processLine("data: Second"); + parser.processLine(""); + + assertEquals(2, events.size()); + assertEquals("initial", events.get(0).id()); + assertEquals("", events.get(1).id(), "Empty id: should set currentEventId to empty string"); + assertEquals("", parser.getLastEventId(), "Empty id: should clear lastEventId to empty string"); + } + + @Test + public void testInvalidRetryIsIgnored() { + // Per SSE spec: non-digit retry values are silently ignored; the event is still dispatched. + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + assertDoesNotThrow(() -> parser.processLine("retry: not-a-number")); + assertDoesNotThrow(() -> parser.processLine("retry: +100")); + assertDoesNotThrow(() -> parser.processLine("retry: -1")); + assertDoesNotThrow(() -> parser.processLine("retry: 1.5")); + parser.processLine("data: Test"); + parser.processLine(""); + + assertEquals(1, events.size()); + assertNull(events.get(0).retry(), "Retry should remain null after invalid values"); + } + + @Test + public void testCommentLinesIgnored() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine(": This is a comment"); + parser.processLine("data: Real data"); + parser.processLine(": Another comment"); + parser.processLine(""); + + assertEquals(1, events.size()); + assertEquals("Real data", events.get(0).data()); + } + + @Test + public void testDataPrefixStripping() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine("data: with space"); + parser.processLine(""); + parser.processLine("data:no space"); + parser.processLine(""); + parser.processLine("data: extra spaces "); + parser.processLine(""); + + assertEquals(3, events.size()); + assertEquals("with space", events.get(0).data()); + assertEquals("no space", events.get(1).data()); + // SSE spec: remove only the first space after colon, preserve the rest + assertEquals(" extra spaces ", events.get(2).data()); + } + + @Test + public void testEmptyDataFieldIgnored() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine("data:"); + parser.processLine(""); + + assertEquals(0, events.size(), "Empty data field should not dispatch event"); + } + + @Test + public void testMultipleEmptyLinesIgnored() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine("data: first"); + parser.processLine(""); + parser.processLine(""); + parser.processLine(""); + parser.processLine("data: second"); + parser.processLine(""); + + assertEquals(2, events.size()); + assertEquals("first", events.get(0).data()); + assertEquals("second", events.get(1).data()); + } + + @Test + public void testFieldWithoutColon() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine("data"); + parser.processLine(""); + + assertEquals(0, events.size(), "Field without value should result in empty data"); + } + + @Test + public void testFlush() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine("data: Unflushed"); + assertEquals(0, events.size(), "Event should not be dispatched yet"); + + parser.flush(); + assertEquals(1, events.size(), "Flush should dispatch buffered event"); + assertEquals("Unflushed", events.get(0).data()); + } + + @Test + public void testNullLineIgnored() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine(null); + parser.processLine("data: Valid"); + parser.processLine(""); + + assertEquals(1, events.size()); + assertEquals("Valid", events.get(0).data()); + } + + @Test + public void testEventTypeResetBetweenEvents() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine("event: custom"); + parser.processLine("data: First"); + parser.processLine(""); + + parser.processLine("data: Second"); + parser.processLine(""); + + assertEquals(2, events.size()); + assertEquals("custom", events.get(0).eventType()); + assertEquals("message", events.get(1).eventType(), "Event type should reset to 'message' after dispatch"); + } + + @Test + public void testIdPersistsAcrossEvents() { + // Per SSE spec, the "last event ID buffer" is never reset between events; + // it persists until explicitly changed by another id: field. + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine("id: 100"); + parser.processLine("data: First"); + parser.processLine(""); + + parser.processLine("data: Second"); + parser.processLine(""); + + assertEquals(2, events.size()); + assertEquals("100", events.get(0).id()); + assertEquals("100", events.get(1).id(), "ID should carry over to subsequent events per SSE spec"); + assertEquals("100", parser.getLastEventId(), "lastEventId should persist"); + } + + @Test + public void testIdWithNullCharacterIsIgnored() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + // id containing U+0000 must be ignored per SSE spec + parser.processLine("id: before"); + parser.processLine("data: First"); + parser.processLine(""); + + parser.processLine("id: invalid\u0000id"); + parser.processLine("data: Second"); + parser.processLine(""); + + assertEquals(2, events.size()); + assertEquals("before", events.get(0).id()); + // The null-containing id is ignored; currentEventId stays "before" (it persists) + assertEquals("before", events.get(1).id()); + // lastEventId should still be "before" since the null id was discarded + assertEquals("before", parser.getLastEventId()); + } + + @Test + public void testRetryPersistsAcrossEvents() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine("retry: 2000"); + parser.processLine("data: First"); + parser.processLine(""); + + parser.processLine("data: Second"); + parser.processLine(""); + + assertEquals(2, events.size()); + assertEquals(2000L, events.get(0).retry()); + assertEquals(2000L, events.get(1).retry()); + assertEquals(2000L, parser.getRetry(), "Retry should persist"); + } + + @Test + public void testUnknownFieldIgnored() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine("unknown: field"); + parser.processLine("data: Valid"); + parser.processLine(""); + + assertEquals(1, events.size()); + assertEquals("Valid", events.get(0).data()); + } + + // --- errorConsumer tests --- + + @Test + public void testErrorConsumerCalledForNullLine() { + List events = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add, error::set); + + parser.processLine(null); + + assertNotNull(error.get(), "errorConsumer should be called for null line"); + assertTrue(error.get() instanceof IllegalArgumentException); + assertEquals(0, events.size(), "No events should be dispatched"); + } + + @Test + public void testErrorConsumerCalledForLineTooLong() { + List events = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add, error::set); + + // Oversized line mid-event: the whole event block is discarded + parser.processLine("data: before overflow"); + String longLine = "data: " + "x".repeat(65537); + parser.processLine(longLine); + // Subsequent lines in the same block are skipped + parser.processLine("data: should be skipped"); + parser.processLine(""); // end of corrupted block — nothing dispatched + + assertNotNull(error.get(), "errorConsumer should be called for oversized line"); + assertTrue(error.get() instanceof IllegalArgumentException); + assertTrue(error.get().getMessage().contains("maximum length")); + assertEquals(0, events.size(), "Corrupted event block must not be dispatched"); + + // Parser recovers cleanly at the next event boundary + parser.processLine("data: recovered"); + parser.processLine(""); + assertEquals(1, events.size(), "Parser should recover after oversized line"); + assertEquals("recovered", events.get(0).data()); + } + + @Test + public void testErrorConsumerCalledForBufferOverflow() { + List events = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add, error::set); + + for (int i = 0; i < 1000; i++) { + parser.processLine("data: line" + i); + } + assertNull(error.get(), "No error expected before limit"); + + parser.processLine("data: overflow"); + assertNotNull(error.get(), "errorConsumer should be called when buffer limit exceeded"); + assertTrue(error.get() instanceof IllegalStateException); + assertTrue(error.get().getMessage().contains("maximum size")); + + // Lines in the same event block after the overflow are skipped + parser.processLine("data: skipped in same block"); + parser.processLine(""); // end of corrupted block — nothing dispatched + assertEquals(0, events.size(), "Corrupted event block must not be dispatched"); + + // Parser recovers cleanly at the next event boundary + parser.processLine("data: recovered"); + parser.processLine(""); + assertEquals(1, events.size(), "Parser should recover after buffer overflow"); + assertEquals("recovered", events.get(0).data()); + } + + @Test + public void testErrorConsumerCalledForBufferByteOverflow() { + List events = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add, error::set); + + // Value is 65530 chars so the full line ("data: " + value = 65536) stays within the per-line + // limit; 17 such lines (17 * 65530 = 1,114,010 bytes) exceed the 1MB buffer byte limit. + String bigValue = "x".repeat(65530); + for (int i = 0; i < 17; i++) { + parser.processLine("data: " + bigValue); + } + + assertNotNull(error.get(), "errorConsumer should be called when byte limit exceeded"); + assertTrue(error.get() instanceof IllegalStateException); + assertTrue(error.get().getMessage().contains("maximum byte size")); + + // Lines in the same event block after the overflow are skipped + parser.processLine("data: skipped in same block"); + parser.processLine(""); // end of corrupted block — nothing dispatched + assertEquals(0, events.size(), "Corrupted event block must not be dispatched"); + + // Parser recovers cleanly at the next event boundary + parser.processLine("data: recovered"); + parser.processLine(""); + assertEquals(1, events.size(), "Parser should recover after byte overflow"); + assertEquals("recovered", events.get(0).data()); + } + + @Test + public void testInvalidRetryDoesNotCallErrorConsumer() { + // Per SSE spec: non-digit retry values are silently ignored, not errors. + List events = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add, error::set); + + parser.processLine("retry: not-a-number"); + parser.processLine("retry: +100"); + parser.processLine("data: Test"); + parser.processLine(""); + + assertNull(error.get(), "errorConsumer must not be called for non-digit retry values"); + assertEquals(1, events.size(), "Event should still be dispatched"); + assertNull(events.get(0).retry(), "Retry should remain null"); + } + + @Test + public void testProcessingContinuesAfterErrorConsumerInvocation() { + List events = new ArrayList<>(); + List errors = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add, errors::add); + + parser.processLine(null); + parser.processLine("data: recovered"); + parser.processLine(""); + + assertEquals(1, errors.size(), "Should have one error from null line"); + assertEquals(1, events.size(), "Should still dispatch the event after error"); + assertEquals("recovered", events.get(0).data()); + } + + @Test + public void testNullLineWithoutErrorConsumerLogsAndContinues() { + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + // Without errorConsumer: null is logged, not thrown + assertDoesNotThrow(() -> parser.processLine(null)); + + parser.processLine("data: still works"); + parser.processLine(""); + + assertEquals(1, events.size()); + assertEquals("still works", events.get(0).data()); + } + + @Test + public void testCRLFLineTerminatorsPreservedInValue() { + // SSEParser processes individual lines after line-splitting by the HTTP client. + // Callers (e.g., BufferedReader.readLine()) strip the \r\n terminator before passing + // the line here, so processLine never receives a bare \r from a CRLF stream. + // If a caller passes a line with a trailing \r (e.g., a non-standard source), it is + // preserved in the data value — stripping is the caller's responsibility. + List events = new ArrayList<>(); + ServerSentEventParser parser = new ServerSentEventParser(events::add); + + parser.processLine("data: value\r"); + parser.processLine(""); + + assertEquals(1, events.size()); + assertEquals("value\r", events.get(0).data()); + } +} diff --git a/server-common/src/test/java/io/a2a/server/requesthandlers/AbstractA2ARequestHandlerTest.java b/server-common/src/test/java/io/a2a/server/requesthandlers/AbstractA2ARequestHandlerTest.java index 429c27942..32ed93bbb 100644 --- a/server-common/src/test/java/io/a2a/server/requesthandlers/AbstractA2ARequestHandlerTest.java +++ b/server-common/src/test/java/io/a2a/server/requesthandlers/AbstractA2ARequestHandlerTest.java @@ -17,6 +17,7 @@ import io.a2a.client.http.A2AHttpClient; import io.a2a.client.http.A2AHttpResponse; +import io.a2a.client.http.ServerSentEvent; import io.a2a.server.agentexecution.AgentExecutor; import io.a2a.server.agentexecution.RequestContext; import io.a2a.server.events.EventQueue; @@ -229,7 +230,7 @@ public String body() { } @Override - public CompletableFuture postAsyncSSE(Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) throws IOException, InterruptedException { + public CompletableFuture postAsyncSSE(Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) throws IOException, InterruptedException { return null; } diff --git a/server-common/src/test/java/io/a2a/server/tasks/PushNotificationSenderTest.java b/server-common/src/test/java/io/a2a/server/tasks/PushNotificationSenderTest.java index f73c2a7f5..cd42daabf 100644 --- a/server-common/src/test/java/io/a2a/server/tasks/PushNotificationSenderTest.java +++ b/server-common/src/test/java/io/a2a/server/tasks/PushNotificationSenderTest.java @@ -18,6 +18,7 @@ import io.a2a.client.http.A2AHttpClient; import io.a2a.client.http.A2AHttpResponse; +import io.a2a.client.http.ServerSentEvent; import io.a2a.common.A2AHeaders; import io.a2a.json.JsonProcessingException; import io.a2a.json.JsonUtil; @@ -106,7 +107,7 @@ public String body() { } @Override - public CompletableFuture postAsyncSSE(Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) throws IOException, InterruptedException { + public CompletableFuture postAsyncSSE(Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) throws IOException, InterruptedException { return null; } diff --git a/tests/server-common/src/test/java/io/a2a/server/apps/common/TestHttpClient.java b/tests/server-common/src/test/java/io/a2a/server/apps/common/TestHttpClient.java index 7cc62dcfe..37e0a35e4 100644 --- a/tests/server-common/src/test/java/io/a2a/server/apps/common/TestHttpClient.java +++ b/tests/server-common/src/test/java/io/a2a/server/apps/common/TestHttpClient.java @@ -13,6 +13,7 @@ import io.a2a.client.http.A2AHttpClient; import io.a2a.client.http.A2AHttpResponse; +import io.a2a.client.http.ServerSentEvent; import io.a2a.json.JsonProcessingException; import io.a2a.spec.Task; import io.a2a.json.JsonUtil; @@ -77,7 +78,7 @@ public String body() { } @Override - public CompletableFuture postAsyncSSE(Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) throws IOException, InterruptedException { + public CompletableFuture postAsyncSSE(Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) throws IOException, InterruptedException { return null; } From 6ba8e6f26b7d5b161dd8121470fa49ab1b6fd359 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Tue, 5 May 2026 15:48:43 +0100 Subject: [PATCH 36/37] chore: use TCK 0.3.0.beta4 (#849) --- .github/workflows/run-tck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tck.yml b/.github/workflows/run-tck.yml index 375d7f6d3..2ec8e58d8 100644 --- a/.github/workflows/run-tck.yml +++ b/.github/workflows/run-tck.yml @@ -15,7 +15,7 @@ on: env: # TODO once we have the TCK for 0.4.0 we will need to look at the branch to decide which TCK version to run. # Tag of the TCK - TCK_VERSION: 0.3.0.beta3 + TCK_VERSION: 0.3.0.beta4 # Tells uv to not need a venv, and instead use system UV_SYSTEM_PYTHON: 1 # SUT_JSONRPC_URL to use for the TCK and the server agent From c2cfd7c8d07c81b9c19dd5849443b553bccc4337 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Wed, 6 May 2026 07:21:21 +0100 Subject: [PATCH 37/37] feat: support authentication field in push notification webhooks (#851) Co-authored-by: Claude Opus 4.6 --- .../tasks/BasePushNotificationSender.java | 7 + .../tasks/PushNotificationSenderTest.java | 134 ++++++++++++++++++ 2 files changed, 141 insertions(+) diff --git a/server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java b/server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java index ca9092890..98a1821bd 100644 --- a/server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java +++ b/server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java @@ -13,6 +13,7 @@ import io.a2a.client.http.A2AHttpClient; import io.a2a.json.JsonUtil; import io.a2a.client.http.A2AHttpClientFactory; +import io.a2a.spec.PushNotificationAuthenticationInfo; import io.a2a.spec.PushNotificationConfig; import io.a2a.spec.Task; @@ -75,6 +76,12 @@ private boolean dispatchNotification(Task task, PushNotificationConfig pushInfo) postBuilder.addHeader(X_A2A_NOTIFICATION_TOKEN, token); } + PushNotificationAuthenticationInfo auth = pushInfo.authentication(); + if (auth != null && !auth.schemes().isEmpty() && auth.credentials() != null && !auth.credentials().isBlank()) { + String scheme = auth.schemes().get(0); + postBuilder.addHeader("Authorization", scheme + " " + auth.credentials()); + } + String body; try { body = JsonUtil.toJson(task); diff --git a/server-common/src/test/java/io/a2a/server/tasks/PushNotificationSenderTest.java b/server-common/src/test/java/io/a2a/server/tasks/PushNotificationSenderTest.java index cd42daabf..20ecaee39 100644 --- a/server-common/src/test/java/io/a2a/server/tasks/PushNotificationSenderTest.java +++ b/server-common/src/test/java/io/a2a/server/tasks/PushNotificationSenderTest.java @@ -22,6 +22,7 @@ import io.a2a.common.A2AHeaders; import io.a2a.json.JsonProcessingException; import io.a2a.json.JsonUtil; +import io.a2a.spec.PushNotificationAuthenticationInfo; import io.a2a.spec.PushNotificationConfig; import io.a2a.spec.Task; import io.a2a.spec.TaskState; @@ -289,6 +290,139 @@ public void testSendNotificationMultipleConfigs() throws InterruptedException { } } + @Test + public void testSendNotificationWithAuthentication() throws InterruptedException { + String taskId = "task_send_with_auth"; + Task taskData = createSampleTask(taskId, TaskState.COMPLETED); + PushNotificationAuthenticationInfo authInfo = new PushNotificationAuthenticationInfo( + List.of("Bearer"), "my-secret-credentials"); + PushNotificationConfig config = new PushNotificationConfig.Builder() + .url("http://notify.me/here") + .id("cfg1") + .authenticationInfo(authInfo) + .build(); + + configStore.setInfo(taskId, config); + testHttpClient.latch = new CountDownLatch(1); + + sender.sendNotification(taskData); + + assertTrue(testHttpClient.latch.await(5, TimeUnit.SECONDS)); + assertEquals(1, testHttpClient.headers.size()); + Map sentHeaders = testHttpClient.headers.get(0); + assertEquals("Bearer my-secret-credentials", sentHeaders.get("Authorization")); + } + + @Test + public void testSendNotificationWithTokenAndAuthentication() throws InterruptedException { + String taskId = "task_send_token_and_auth"; + Task taskData = createSampleTask(taskId, TaskState.COMPLETED); + PushNotificationAuthenticationInfo authInfo = new PushNotificationAuthenticationInfo( + List.of("Bearer"), "my-secret-credentials"); + PushNotificationConfig config = new PushNotificationConfig.Builder() + .url("http://notify.me/here") + .id("cfg1") + .token("my-token") + .authenticationInfo(authInfo) + .build(); + + configStore.setInfo(taskId, config); + testHttpClient.latch = new CountDownLatch(1); + + sender.sendNotification(taskData); + + assertTrue(testHttpClient.latch.await(5, TimeUnit.SECONDS)); + assertEquals(1, testHttpClient.headers.size()); + Map sentHeaders = testHttpClient.headers.get(0); + assertEquals("my-token", sentHeaders.get(A2AHeaders.X_A2A_NOTIFICATION_TOKEN)); + assertEquals("Bearer my-secret-credentials", sentHeaders.get("Authorization")); + } + + @Test + public void testSendNotificationWithNullAuthSchemes() throws InterruptedException { + String taskId = "task_send_null_schemes"; + Task taskData = createSampleTask(taskId, TaskState.COMPLETED); + PushNotificationConfig config = new PushNotificationConfig.Builder() + .url("http://notify.me/here") + .id("cfg1") + .build(); + + configStore.setInfo(taskId, config); + testHttpClient.latch = new CountDownLatch(1); + + sender.sendNotification(taskData); + + assertTrue(testHttpClient.latch.await(5, TimeUnit.SECONDS)); + assertEquals(1, testHttpClient.headers.size()); + assertTrue(testHttpClient.headers.get(0).isEmpty()); + } + + @Test + public void testSendNotificationWithEmptyAuthSchemes() throws InterruptedException { + String taskId = "task_send_empty_schemes"; + Task taskData = createSampleTask(taskId, TaskState.COMPLETED); + PushNotificationAuthenticationInfo authInfo = new PushNotificationAuthenticationInfo( + List.of(), "my-secret-credentials"); + PushNotificationConfig config = new PushNotificationConfig.Builder() + .url("http://notify.me/here") + .id("cfg1") + .authenticationInfo(authInfo) + .build(); + + configStore.setInfo(taskId, config); + testHttpClient.latch = new CountDownLatch(1); + + sender.sendNotification(taskData); + + assertTrue(testHttpClient.latch.await(5, TimeUnit.SECONDS)); + assertEquals(1, testHttpClient.headers.size()); + assertTrue(testHttpClient.headers.get(0).isEmpty()); + } + + @Test + public void testSendNotificationWithNullAuthCredentials() throws InterruptedException { + String taskId = "task_send_null_creds"; + Task taskData = createSampleTask(taskId, TaskState.COMPLETED); + PushNotificationAuthenticationInfo authInfo = new PushNotificationAuthenticationInfo( + List.of("Bearer"), null); + PushNotificationConfig config = new PushNotificationConfig.Builder() + .url("http://notify.me/here") + .id("cfg1") + .authenticationInfo(authInfo) + .build(); + + configStore.setInfo(taskId, config); + testHttpClient.latch = new CountDownLatch(1); + + sender.sendNotification(taskData); + + assertTrue(testHttpClient.latch.await(5, TimeUnit.SECONDS)); + assertEquals(1, testHttpClient.headers.size()); + assertTrue(testHttpClient.headers.get(0).isEmpty()); + } + + @Test + public void testSendNotificationWithBlankAuthCredentials() throws InterruptedException { + String taskId = "task_send_blank_creds"; + Task taskData = createSampleTask(taskId, TaskState.COMPLETED); + PushNotificationAuthenticationInfo authInfo = new PushNotificationAuthenticationInfo( + List.of("Bearer"), " "); + PushNotificationConfig config = new PushNotificationConfig.Builder() + .url("http://notify.me/here") + .id("cfg1") + .authenticationInfo(authInfo) + .build(); + + configStore.setInfo(taskId, config); + testHttpClient.latch = new CountDownLatch(1); + + sender.sendNotification(taskData); + + assertTrue(testHttpClient.latch.await(5, TimeUnit.SECONDS)); + assertEquals(1, testHttpClient.headers.size()); + assertTrue(testHttpClient.headers.get(0).isEmpty()); + } + @Test public void testSendNotificationHttpError() { String taskId = "task_send_http_err";