diff --git a/reference/grpc/src/main/java/org/a2aproject/sdk/server/grpc/quarkus/QuarkusGrpcHandler.java b/reference/grpc/src/main/java/org/a2aproject/sdk/server/grpc/quarkus/QuarkusGrpcHandler.java index 8df6e0699..cae365e2e 100644 --- a/reference/grpc/src/main/java/org/a2aproject/sdk/server/grpc/quarkus/QuarkusGrpcHandler.java +++ b/reference/grpc/src/main/java/org/a2aproject/sdk/server/grpc/quarkus/QuarkusGrpcHandler.java @@ -74,8 +74,8 @@ @Blocking public class QuarkusGrpcHandler extends GrpcHandler { - private final AgentCard agentCard; - private final AgentCard extendedAgentCard; + private final Instance agentCard; + private final Instance extendedAgentCard; private final RequestHandler requestHandler; private final Instance callContextFactoryInstance; private final Executor executor; @@ -106,17 +106,13 @@ public class QuarkusGrpcHandler extends GrpcHandler { * @param executor the executor for async operations (qualified with {@code @Internal}) */ @Inject - public QuarkusGrpcHandler(@PublicAgentCard AgentCard agentCard, + public QuarkusGrpcHandler(@PublicAgentCard Instance agentCard, @ExtendedAgentCard Instance extendedAgentCard, RequestHandler requestHandler, Instance callContextFactoryInstance, @Internal Executor executor) { this.agentCard = agentCard; - if (extendedAgentCard != null && extendedAgentCard.isResolvable()) { - this.extendedAgentCard = extendedAgentCard.get(); - } else { - this.extendedAgentCard = null; - } + this.extendedAgentCard = extendedAgentCard; this.requestHandler = requestHandler; this.callContextFactoryInstance = callContextFactoryInstance; this.executor = executor; @@ -129,12 +125,15 @@ protected RequestHandler getRequestHandler() { @Override protected AgentCard getAgentCard() { - return agentCard; + return agentCard.get(); } @Override protected AgentCard getExtendedAgentCard() { - return extendedAgentCard; + if (extendedAgentCard != null && extendedAgentCard.isResolvable()) { + return extendedAgentCard.get(); + } + return null; } @Override diff --git a/reference/grpc/src/test/java/org/a2aproject/sdk/server/grpc/quarkus/QuarkusGrpcHandlerLazyResolutionTest.java b/reference/grpc/src/test/java/org/a2aproject/sdk/server/grpc/quarkus/QuarkusGrpcHandlerLazyResolutionTest.java new file mode 100644 index 000000000..57d44396e --- /dev/null +++ b/reference/grpc/src/test/java/org/a2aproject/sdk/server/grpc/quarkus/QuarkusGrpcHandlerLazyResolutionTest.java @@ -0,0 +1,44 @@ +package org.a2aproject.sdk.server.grpc.quarkus; + +import jakarta.enterprise.inject.Instance; + +import org.a2aproject.sdk.server.ResolvedInstance; +import org.a2aproject.sdk.spec.AgentCard; +import org.a2aproject.sdk.transport.grpc.handler.CallContextFactory; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +/** + * Verifies that {@link QuarkusGrpcHandler} does not eagerly resolve {@link Instance} parameters + * during construction. This guards against the regression described in + * issue #1033, where eager + * resolution prevented startup when the AgentCard producer depended on the HTTP server address. + */ +class QuarkusGrpcHandlerLazyResolutionTest { + + @Test + void constructorDoesNotResolveAgentCardInstances() { + Instance throwOnGet = new ResolvedInstance<>(null) { + @Override + public AgentCard get() { + throw new AssertionError("Instance.get() must not be called during construction"); + } + + @Override + public boolean isUnsatisfied() { + return false; + } + }; + + @SuppressWarnings("unchecked") + Instance emptyCallContextFactory = (Instance) (Instance) new ResolvedInstance<>(null); + + assertDoesNotThrow(() -> new QuarkusGrpcHandler( + throwOnGet, + throwOnGet, + null, + emptyCallContextFactory, + Runnable::run)); + } +} diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/ResolvedInstance.java b/server-common/src/main/java/org/a2aproject/sdk/server/ResolvedInstance.java new file mode 100644 index 000000000..3383858d9 --- /dev/null +++ b/server-common/src/main/java/org/a2aproject/sdk/server/ResolvedInstance.java @@ -0,0 +1,78 @@ +package org.a2aproject.sdk.server; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.Iterator; + +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.UnsatisfiedResolutionException; +import jakarta.enterprise.util.TypeLiteral; + +import org.jspecify.annotations.Nullable; + +/** + * An {@link Instance} wrapper for a pre-resolved value, for use in non-CDI contexts + * such as convenience constructors and tests. + * + * @param the bean type + */ +public class ResolvedInstance implements Instance { + + private final @Nullable T value; + + public ResolvedInstance(@Nullable T value) { + this.value = value; + } + + @Override + public T get() { + if (value == null) { + throw new UnsatisfiedResolutionException("No value available"); + } + return value; + } + + @Override + public boolean isUnsatisfied() { + return value == null; + } + + @Override + public boolean isAmbiguous() { + return false; + } + + @Override + public Iterator iterator() { + return value != null ? Collections.singleton(value).iterator() : Collections.emptyIterator(); + } + + @Override + public void destroy(T instance) { + } + + @Override + public Instance select(Annotation... qualifiers) { + throw new UnsupportedOperationException(); + } + + @Override + public Instance select(Class subtype, Annotation... qualifiers) { + throw new UnsupportedOperationException(); + } + + @Override + public Instance select(TypeLiteral subtype, Annotation... qualifiers) { + throw new UnsupportedOperationException(); + } + + @Override + public Handle getHandle() { + throw new UnsupportedOperationException(); + } + + @Override + public Iterable> handles() { + throw new UnsupportedOperationException(); + } +} diff --git a/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandler.java b/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandler.java index 3f739f182..b450f089c 100644 --- a/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandler.java +++ b/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandler.java @@ -838,8 +838,13 @@ private void handleInternalError(StreamObserver responseObserver, Throwab private AgentCard getAgentCardInternal() { AgentCard agentCard = getAgentCard(); if (initialised.compareAndSet(false, true)) { - // Validate transport configuration with proper classloader context - validateTransportConfigurationWithCorrectClassLoader(agentCard); + try { + // Validate transport configuration with proper classloader context + validateTransportConfigurationWithCorrectClassLoader(agentCard); + } catch (RuntimeException e) { + initialised.set(false); + throw e; + } } return agentCard; } diff --git a/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandler.java b/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandler.java index 3c0448179..2655534e7 100644 --- a/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandler.java +++ b/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandler.java @@ -5,6 +5,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; @@ -12,8 +13,11 @@ import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; +import mutiny.zero.ZeroPublisher; import org.a2aproject.sdk.jsonrpc.common.wrappers.CancelTaskRequest; import org.a2aproject.sdk.jsonrpc.common.wrappers.CancelTaskResponse; +import org.a2aproject.sdk.jsonrpc.common.wrappers.CreateTaskPushNotificationConfigRequest; +import org.a2aproject.sdk.jsonrpc.common.wrappers.CreateTaskPushNotificationConfigResponse; import org.a2aproject.sdk.jsonrpc.common.wrappers.DeleteTaskPushNotificationConfigRequest; import org.a2aproject.sdk.jsonrpc.common.wrappers.DeleteTaskPushNotificationConfigResponse; import org.a2aproject.sdk.jsonrpc.common.wrappers.GetExtendedAgentCardRequest; @@ -31,13 +35,13 @@ import org.a2aproject.sdk.jsonrpc.common.wrappers.SendMessageResponse; import org.a2aproject.sdk.jsonrpc.common.wrappers.SendStreamingMessageRequest; import org.a2aproject.sdk.jsonrpc.common.wrappers.SendStreamingMessageResponse; -import org.a2aproject.sdk.jsonrpc.common.wrappers.CreateTaskPushNotificationConfigRequest; -import org.a2aproject.sdk.jsonrpc.common.wrappers.CreateTaskPushNotificationConfigResponse; import org.a2aproject.sdk.jsonrpc.common.wrappers.SubscribeToTaskRequest; import org.a2aproject.sdk.server.AgentCardValidator; import org.a2aproject.sdk.server.ExtendedAgentCard; import org.a2aproject.sdk.server.PublicAgentCard; +import org.a2aproject.sdk.server.ResolvedInstance; import org.a2aproject.sdk.server.ServerCallContext; +import org.a2aproject.sdk.server.auth.TaskOperation; import org.a2aproject.sdk.server.extensions.A2AExtensions; import org.a2aproject.sdk.server.requesthandlers.RequestHandler; import org.a2aproject.sdk.server.util.async.Internal; @@ -45,19 +49,16 @@ import org.a2aproject.sdk.spec.A2AError; import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.CancelTaskParams; -import org.a2aproject.sdk.spec.ExtendedAgentCardNotConfiguredError; import org.a2aproject.sdk.spec.EventKind; +import org.a2aproject.sdk.spec.ExtendedAgentCardNotConfiguredError; import org.a2aproject.sdk.spec.InternalError; -import org.a2aproject.sdk.spec.UnsupportedOperationError; import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsResult; import org.a2aproject.sdk.spec.PushNotificationNotSupportedError; import org.a2aproject.sdk.spec.StreamingEventKind; import org.a2aproject.sdk.spec.Task; import org.a2aproject.sdk.spec.TaskNotFoundError; import org.a2aproject.sdk.spec.TaskPushNotificationConfig; - -import mutiny.zero.ZeroPublisher; -import org.a2aproject.sdk.server.auth.TaskOperation; +import org.a2aproject.sdk.spec.UnsupportedOperationError; import org.jspecify.annotations.Nullable; /** @@ -139,10 +140,11 @@ public class JSONRPCHandler { // Fields set by constructor injection cannot be final. We need a noargs constructor for // Jakarta compatibility, and it seems that making fields set by constructor injection // final, is not proxyable in all runtimes - private AgentCard agentCard; + private Instance agentCardInstance; private @Nullable Instance extendedAgentCard; private RequestHandler requestHandler; private Executor executor; + private final AtomicBoolean transportValidated = new AtomicBoolean(false); /** * No-args constructor for CDI proxy creation. @@ -152,7 +154,7 @@ public class JSONRPCHandler { @SuppressWarnings("NullAway") protected JSONRPCHandler() { // For CDI proxy creation - this.agentCard = null; + this.agentCardInstance = null; this.extendedAgentCard = null; this.requestHandler = null; this.executor = null; @@ -161,21 +163,19 @@ protected JSONRPCHandler() { /** * Creates a JSON-RPC handler with full CDI injection support. * - * @param agentCard the public agent card containing agent capabilities + * @param agentCardInstance the public agent card instance containing agent capabilities * @param extendedAgentCard optional extended agent card instance * @param requestHandler the handler for processing A2A requests * @param executor the executor for asynchronous operations */ @Inject - public JSONRPCHandler(@PublicAgentCard AgentCard agentCard, @Nullable @ExtendedAgentCard Instance extendedAgentCard, + public JSONRPCHandler(@PublicAgentCard Instance agentCardInstance, + @Nullable @ExtendedAgentCard Instance extendedAgentCard, RequestHandler requestHandler, @Internal Executor executor) { - this.agentCard = agentCard; + this.agentCardInstance = agentCardInstance; this.extendedAgentCard = extendedAgentCard; this.requestHandler = requestHandler; this.executor = executor; - - // Validate transport configuration - AgentCardValidator.validateTransportConfiguration(agentCard); } /** @@ -186,7 +186,7 @@ public JSONRPCHandler(@PublicAgentCard AgentCard agentCard, @Nullable @ExtendedA * @param executor the executor for asynchronous operations */ public JSONRPCHandler(@PublicAgentCard AgentCard agentCard, RequestHandler requestHandler, Executor executor) { - this(agentCard, null, requestHandler, executor); + this(new ResolvedInstance<>(agentCard), null, requestHandler, executor); } /** @@ -233,8 +233,8 @@ public JSONRPCHandler(@PublicAgentCard AgentCard agentCard, RequestHandler reque */ public SendMessageResponse onMessageSend(SendMessageRequest request, ServerCallContext context) { try { - A2AVersionValidator.validateProtocolVersion(agentCard, context); - A2AExtensions.validateRequiredExtensions(agentCard, context); + A2AVersionValidator.validateProtocolVersion(agentCard(), context); + A2AExtensions.validateRequiredExtensions(agentCard(), context); EventKind taskOrMessage = requestHandler.onMessageSend(request.getParams(), context); return new SendMessageResponse(request.getId(), taskOrMessage); } catch (A2AError e) { @@ -282,7 +282,7 @@ public SendMessageResponse onMessageSend(SendMessageRequest request, ServerCallC */ public Flow.Publisher onMessageSendStream( SendStreamingMessageRequest request, ServerCallContext context) { - if (!agentCard.capabilities().streaming()) { + if (!agentCard().capabilities().streaming()) { return ZeroPublisher.fromItems( new SendStreamingMessageResponse( request.getId(), @@ -290,8 +290,8 @@ public Flow.Publisher onMessageSendStream( } try { - A2AVersionValidator.validateProtocolVersion(agentCard, context); - A2AExtensions.validateRequiredExtensions(agentCard, context); + A2AVersionValidator.validateProtocolVersion(agentCard(), context); + A2AExtensions.validateRequiredExtensions(agentCard(), context); 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 @@ -379,7 +379,7 @@ public CancelTaskResponse onCancelTask(CancelTaskRequest request, ServerCallCont */ public Flow.Publisher onSubscribeToTask( SubscribeToTaskRequest request, ServerCallContext context) throws A2AError { - if (!agentCard.capabilities().streaming()) { + if (!agentCard().capabilities().streaming()) { return ZeroPublisher.fromItems( new SendStreamingMessageResponse( request.getId(), @@ -426,7 +426,7 @@ public Flow.Publisher onSubscribeToTask( */ public GetTaskPushNotificationConfigResponse getPushNotificationConfig( GetTaskPushNotificationConfigRequest request, ServerCallContext context) { - if (!agentCard.capabilities().pushNotifications()) { + if (!agentCard().capabilities().pushNotifications()) { return new GetTaskPushNotificationConfigResponse(request.getId(), new PushNotificationNotSupportedError()); } @@ -468,7 +468,7 @@ public GetTaskPushNotificationConfigResponse getPushNotificationConfig( */ public CreateTaskPushNotificationConfigResponse setPushNotificationConfig( CreateTaskPushNotificationConfigRequest request, ServerCallContext context) { - if (!agentCard.capabilities().pushNotifications()) { + if (!agentCard().capabilities().pushNotifications()) { return new CreateTaskPushNotificationConfigResponse(request.getId(), new PushNotificationNotSupportedError()); } @@ -591,7 +591,7 @@ public ListTasksResponse onListTasks(ListTasksRequest request, ServerCallContext */ public ListTaskPushNotificationConfigsResponse listPushNotificationConfigs( ListTaskPushNotificationConfigsRequest request, ServerCallContext context) { - if ( !agentCard.capabilities().pushNotifications()) { + if (!agentCard().capabilities().pushNotifications()) { return new ListTaskPushNotificationConfigsResponse(request.getId(), new PushNotificationNotSupportedError()); } @@ -633,7 +633,7 @@ public ListTaskPushNotificationConfigsResponse listPushNotificationConfigs( */ public DeleteTaskPushNotificationConfigResponse deletePushNotificationConfig( DeleteTaskPushNotificationConfigRequest request, ServerCallContext context) { - if ( !agentCard.capabilities().pushNotifications()) { + if (!agentCard().capabilities().pushNotifications()) { return new DeleteTaskPushNotificationConfigResponse(request.getId(), new PushNotificationNotSupportedError()); } @@ -673,7 +673,7 @@ public DeleteTaskPushNotificationConfigResponse deletePushNotificationConfig( // TODO: Add authentication (https://github.com/a2aproject/a2a-java/issues/77) public GetExtendedAgentCardResponse onGetExtendedCardRequest( GetExtendedAgentCardRequest request, ServerCallContext context) { - if (!agentCard.capabilities().extendedAgentCard()) { + if (!agentCard().capabilities().extendedAgentCard()) { return new GetExtendedAgentCardResponse(request.getId(), new UnsupportedOperationError()); } if (extendedAgentCard == null || !extendedAgentCard.isResolvable()) { @@ -700,7 +700,20 @@ public GetExtendedAgentCardResponse onGetExtendedCardRequest( * @see AgentCard */ public AgentCard getAgentCard() { - return agentCard; + return agentCard(); + } + + private AgentCard agentCard() { + AgentCard card = agentCardInstance.get(); + if (transportValidated.compareAndSet(false, true)) { + try { + AgentCardValidator.validateTransportConfiguration(card); + } catch (RuntimeException e) { + transportValidated.set(false); + throw e; + } + } + return card; } private Flow.Publisher convertToSendStreamingMessageResponse( diff --git a/transport/jsonrpc/src/test/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandlerTest.java b/transport/jsonrpc/src/test/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandlerTest.java index 42f09868a..b436de140 100644 --- a/transport/jsonrpc/src/test/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandlerTest.java +++ b/transport/jsonrpc/src/test/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandlerTest.java @@ -1,5 +1,6 @@ package org.a2aproject.sdk.transport.jsonrpc.handler; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -81,6 +82,7 @@ import org.a2aproject.sdk.spec.TextPart; import org.a2aproject.sdk.spec.UnsupportedOperationError; import org.a2aproject.sdk.spec.VersionNotSupportedError; +import jakarta.enterprise.inject.Instance; import mutiny.zero.ZeroPublisher; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; @@ -2024,4 +2026,13 @@ public void testListTasksEmptyResultIncludesAllFields() { assertEquals(0, result.pageSize(), "pageSize should be 0"); // nextPageToken can be null for empty results } + + @Test + void constructorDoesNotResolveAgentCardInstance() { + @SuppressWarnings("unchecked") + Instance throwOnGet = Mockito.mock(Instance.class); + Mockito.when(throwOnGet.get()).thenThrow(new AssertionError("Instance.get() must not be called during construction")); + + assertDoesNotThrow(() -> new JSONRPCHandler(throwOnGet, null, requestHandler, internalExecutor)); + } } diff --git a/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java b/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java index 20ce9afb8..e6d08a299 100644 --- a/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java +++ b/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java @@ -12,6 +12,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -34,6 +35,7 @@ import org.a2aproject.sdk.server.AgentCardValidator; import org.a2aproject.sdk.server.ExtendedAgentCard; import org.a2aproject.sdk.server.PublicAgentCard; +import org.a2aproject.sdk.server.ResolvedInstance; import org.a2aproject.sdk.server.ServerCallContext; import org.a2aproject.sdk.server.auth.TaskOperation; import org.a2aproject.sdk.server.extensions.A2AExtensions; @@ -124,11 +126,12 @@ public class RestHandler { // Fields set by constructor injection cannot be final. We need a noargs constructor for // Jakarta compatibility, and it seems that making fields set by constructor injection // final, is not proxyable in all runtimes - private AgentCard agentCard; + private Instance agentCardInstance; private @Nullable Instance extendedAgentCard; private AgentCardCacheMetadata cacheMetadata; private RequestHandler requestHandler; private Executor executor; + private final AtomicBoolean transportValidated = new AtomicBoolean(false); /** * No-args constructor for CDI proxy creation. @@ -144,23 +147,21 @@ protected RestHandler() { /** * Creates a REST handler with full CDI injection support. * - * @param agentCard the public agent card containing agent capabilities + * @param agentCardInstance the public agent card instance containing agent capabilities * @param extendedAgentCard optional extended agent card instance * @param cacheMetadata the agent card caching metadata * @param requestHandler the handler for processing A2A requests * @param executor the executor for asynchronous operations */ @Inject - public RestHandler(@PublicAgentCard AgentCard agentCard, @ExtendedAgentCard Instance extendedAgentCard, + public RestHandler(@PublicAgentCard Instance agentCardInstance, + @ExtendedAgentCard Instance extendedAgentCard, AgentCardCacheMetadata cacheMetadata, RequestHandler requestHandler, @Internal Executor executor) { - this.agentCard = agentCard; + this.agentCardInstance = agentCardInstance; this.extendedAgentCard = extendedAgentCard; this.cacheMetadata = cacheMetadata; this.requestHandler = requestHandler; this.executor = executor; - - // Validate transport configuration - AgentCardValidator.validateTransportConfiguration(agentCard); } /** @@ -173,7 +174,7 @@ public RestHandler(@PublicAgentCard AgentCard agentCard, @ExtendedAgentCard Inst */ public RestHandler(AgentCard agentCard, AgentCardCacheMetadata cacheMetadata, RequestHandler requestHandler, Executor executor) { - this.agentCard = agentCard; + this.agentCardInstance = new ResolvedInstance<>(agentCard); this.cacheMetadata = cacheMetadata; this.requestHandler = requestHandler; this.executor = executor; @@ -227,8 +228,8 @@ public RestHandler(AgentCard agentCard, AgentCardCacheMetadata cacheMetadata, */ public HTTPRestResponse sendMessage(ServerCallContext context, String tenant, String body) { try { - A2AVersionValidator.validateProtocolVersion(agentCard, context); - A2AExtensions.validateRequiredExtensions(agentCard, context); + A2AVersionValidator.validateProtocolVersion(agentCard(), context); + A2AExtensions.validateRequiredExtensions(agentCard(), context); org.a2aproject.sdk.grpc.SendMessageRequest.Builder request = org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(); parseRequestBody(body, request); request.setTenant(tenant); @@ -292,11 +293,11 @@ public HTTPRestResponse sendMessage(ServerCallContext context, String tenant, St */ public HTTPRestResponse sendStreamingMessage(ServerCallContext context, String tenant, String body) { try { - if (!agentCard.capabilities().streaming()) { + if (!agentCard().capabilities().streaming()) { return createErrorResponse(new UnsupportedOperationError(null, "Streaming is not supported by the agent", null)); } - A2AVersionValidator.validateProtocolVersion(agentCard, context); - A2AExtensions.validateRequiredExtensions(agentCard, context); + A2AVersionValidator.validateProtocolVersion(agentCard(), context); + A2AExtensions.validateRequiredExtensions(agentCard(), context); org.a2aproject.sdk.grpc.SendMessageRequest.Builder request = org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(); parseRequestBody(body, request); request.setTenant(tenant); @@ -369,7 +370,7 @@ public HTTPRestResponse cancelTask(ServerCallContext context, String tenant, Str */ public HTTPRestResponse createTaskPushNotificationConfiguration(ServerCallContext context, String tenant, String body, String taskId) { try { - if (!agentCard.capabilities().pushNotifications()) { + if (!agentCard().capabilities().pushNotifications()) { throw new PushNotificationNotSupportedError(); } org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder builder = org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder(); @@ -424,7 +425,7 @@ public HTTPRestResponse createTaskPushNotificationConfiguration(ServerCallContex */ public HTTPRestResponse subscribeToTask(ServerCallContext context, String tenant, String taskId) { try { - if (!agentCard.capabilities().streaming()) { + if (!agentCard().capabilities().streaming()) { return createErrorResponse(new UnsupportedOperationError(null, "Streaming is not supported by the agent", null)); } TaskIdParams params = TaskIdParams.builder().id(taskId).tenant(tenant).build(); @@ -581,7 +582,7 @@ public HTTPRestResponse listTasks(ServerCallContext context, String tenant, */ public HTTPRestResponse getTaskPushNotificationConfiguration(ServerCallContext context, String tenant, String taskId, String configId) { try { - if (!agentCard.capabilities().pushNotifications()) { + if (!agentCard().capabilities().pushNotifications()) { throw new PushNotificationNotSupportedError(); } GetTaskPushNotificationConfigParams params = new GetTaskPushNotificationConfigParams(taskId, configId, tenant); @@ -606,7 +607,7 @@ public HTTPRestResponse getTaskPushNotificationConfiguration(ServerCallContext c */ public HTTPRestResponse listTaskPushNotificationConfigurations(ServerCallContext context, String tenant, String taskId, int pageSize, String pageToken) { try { - if (!agentCard.capabilities().pushNotifications()) { + if (!agentCard().capabilities().pushNotifications()) { throw new PushNotificationNotSupportedError(); } ListTaskPushNotificationConfigsParams params = new ListTaskPushNotificationConfigsParams(taskId, pageSize, pageToken, tenant); @@ -630,7 +631,7 @@ public HTTPRestResponse listTaskPushNotificationConfigurations(ServerCallContext */ public HTTPRestResponse deleteTaskPushNotificationConfiguration(ServerCallContext context, String tenant, String taskId, String configId) { try { - if (!agentCard.capabilities().pushNotifications()) { + if (!agentCard().capabilities().pushNotifications()) { throw new PushNotificationNotSupportedError(); } DeleteTaskPushNotificationConfigParams params = new DeleteTaskPushNotificationConfigParams(taskId, configId, tenant); @@ -814,7 +815,7 @@ private static int mapErrorToHttpStatus(A2AError error) { */ public HTTPRestResponse getExtendedAgentCard(ServerCallContext context, String tenant) { try { - if (!agentCard.capabilities().extendedAgentCard()) { + if (!agentCard().capabilities().extendedAgentCard()) { throw new UnsupportedOperationError(); } if (extendedAgentCard == null || !extendedAgentCard.isResolvable()) { @@ -863,9 +864,22 @@ public HTTPRestResponse getExtendedAgentCard(ServerCallContext context, String t * @see AgentCard * @see #getExtendedAgentCard(ServerCallContext, String) */ + private AgentCard agentCard() { + AgentCard card = agentCardInstance.get(); + if (transportValidated.compareAndSet(false, true)) { + try { + AgentCardValidator.validateTransportConfiguration(card); + } catch (RuntimeException e) { + transportValidated.set(false); + throw e; + } + } + return card; + } + public HTTPRestResponse getAgentCard() { try { - return new HTTPRestResponse(200, APPLICATION_JSON, JsonUtil.toJson(agentCard), + return new HTTPRestResponse(200, APPLICATION_JSON, JsonUtil.toJson(agentCard()), cacheMetadata.getHttpHeadersMap()); } catch (Throwable t) { return createErrorResponse(500, internalError(t)); diff --git a/transport/rest/src/test/java/org/a2aproject/sdk/transport/rest/handler/RestHandlerTest.java b/transport/rest/src/test/java/org/a2aproject/sdk/transport/rest/handler/RestHandlerTest.java index 26e24f7b0..9e9668e8c 100644 --- a/transport/rest/src/test/java/org/a2aproject/sdk/transport/rest/handler/RestHandlerTest.java +++ b/transport/rest/src/test/java/org/a2aproject/sdk/transport/rest/handler/RestHandlerTest.java @@ -17,6 +17,7 @@ import com.google.gson.JsonParser; import com.google.protobuf.InvalidProtocolBufferException; +import jakarta.enterprise.inject.Instance; import org.a2aproject.sdk.common.MediaType; import org.a2aproject.sdk.server.AgentCardCacheMetadata; import org.a2aproject.sdk.server.ServerCallContext; @@ -556,7 +557,7 @@ public void testExtensionSupportRequiredErrorOnSendMessage() { AgentCard cardWithExtension = AgentCard.builder() .name("test-card") .description("Test card with required extension") - .supportedInterfaces(Collections.singletonList(new AgentInterface("REST", "http://localhost:9999"))) + .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) .version("1.0.0") .capabilities(AgentCapabilities.builder() .streaming(true) @@ -604,7 +605,7 @@ public void testExtensionSupportRequiredErrorOnSendStreamingMessage() { AgentCard cardWithExtension = AgentCard.builder() .name("test-card") .description("Test card with required extension") - .supportedInterfaces(Collections.singletonList(new AgentInterface("REST", "http://localhost:9999"))) + .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) .version("1.0.0") .capabilities(AgentCapabilities.builder() .streaming(true) @@ -699,7 +700,7 @@ public void testRequiredExtensionProvidedSuccess() { AgentCard cardWithExtension = AgentCard.builder() .name("test-card") .description("Test card with required extension") - .supportedInterfaces(Collections.singletonList(new AgentInterface("REST", "http://localhost:9999"))) + .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) .version("1.0.0") .capabilities(AgentCapabilities.builder() .streaming(true) @@ -762,7 +763,7 @@ public void testVersionNotSupportedErrorOnSendMessage() { AgentCard agentCard = AgentCard.builder() .name("test-card") .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("REST", "http://localhost:9999"))) + .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) .version("1.0.0") .capabilities(AgentCapabilities.builder() .streaming(true) @@ -812,7 +813,7 @@ public void testVersionNotSupportedErrorOnSendStreamingMessage() { AgentCard agentCard = AgentCard.builder() .name("test-card") .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("REST", "http://localhost:9999"))) + .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) .version("1.0.0") .capabilities(AgentCapabilities.builder() .streaming(true) @@ -908,7 +909,7 @@ public void testCompatibleVersionSuccess() { AgentCard agentCard = AgentCard.builder() .name("test-card") .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("REST", "http://localhost:9999"))) + .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) .version("1.0.0") .capabilities(AgentCapabilities.builder() .streaming(true) @@ -963,7 +964,7 @@ public void testNoVersionDefaultsTo0_3_RejectedByV10OnlyServer() { AgentCard agentCard = AgentCard.builder() .name("test-card") .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("REST", "http://localhost:9999"))) + .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) .version("1.0.0") .capabilities(AgentCapabilities.builder() .streaming(true) @@ -1079,6 +1080,16 @@ public void testListTasksEmptyResultIncludesAllFields() { "tasks should be empty array"); } + @Test + void constructorDoesNotResolveAgentCardInstance() { + @SuppressWarnings("unchecked") + Instance throwOnGet = Mockito.mock(Instance.class); + Mockito.when(throwOnGet.get()).thenThrow(new AssertionError("Instance.get() must not be called during construction")); + + Assertions.assertDoesNotThrow(() -> new RestHandler(throwOnGet, throwOnGet, + createCacheMetadata(), requestHandler, internalExecutor)); + } + private static void assertProblemDetail(RestHandler.HTTPRestResponse response, int expectedStatus, String expectedReason, String expectedMessage) { Assertions.assertEquals(expectedStatus, response.getStatusCode());