Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public class AgentCardProducer {
@Produces
@PublicAgentCard
public AgentCard agentCard() {
// NOTE: Transport validation will automatically check that transports specified
// in this AgentCard match those available on the classpath when handlers are initialized
return new AgentCard.Builder()
.name("Hello World Agent")
.description("Just a hello world agent")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;

import io.a2a.transport.grpc.handler.CallContextFactory;
import io.a2a.transport.grpc.handler.GrpcHandler;
import io.a2a.server.PublicAgentCard;
import io.a2a.server.requesthandlers.RequestHandler;
import io.a2a.spec.AgentCard;
import io.a2a.transport.grpc.handler.CallContextFactory;
import io.a2a.transport.grpc.handler.GrpcHandler;
import io.quarkus.grpc.GrpcService;

@GrpcService
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.a2a.server.grpc.quarkus;

import io.a2a.server.TransportMetadata;
import io.a2a.spec.TransportProtocol;

public class QuarkusGrpcTransportMetadata implements TransportMetadata {
@Override
public String getTransportProtocol() {
return TransportProtocol.GRPC.asString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.a2a.server.grpc.quarkus.QuarkusGrpcTransportMetadata
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,11 @@
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.ws.rs.core.Response;

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 io.a2a.transport.jsonrpc.handler.JSONRPCHandler;
import io.a2a.server.ExtendedAgentCard;
import io.a2a.server.ServerCallContext;
import io.a2a.server.auth.UnauthenticatedUser;
import io.a2a.server.auth.User;
Expand All @@ -37,7 +34,6 @@
import io.a2a.spec.InvalidParamsError;
import io.a2a.spec.InvalidParamsJsonMappingException;
import io.a2a.spec.InvalidRequestError;
import io.a2a.spec.JSONErrorResponse;
import io.a2a.spec.JSONParseError;
import io.a2a.spec.JSONRPCError;
import io.a2a.spec.JSONRPCErrorResponse;
Expand All @@ -53,11 +49,11 @@
import io.a2a.spec.StreamingJSONRPCRequest;
import io.a2a.spec.TaskResubscriptionRequest;
import io.a2a.spec.UnsupportedOperationError;
import io.a2a.transport.jsonrpc.handler.JSONRPCHandler;
import io.a2a.util.Utils;
import io.quarkus.vertx.web.Body;
import io.quarkus.vertx.web.ReactiveRoutes;
import io.quarkus.vertx.web.Route;
import io.quarkus.vertx.web.RoutingExchange;
import io.smallrye.mutiny.Multi;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
Expand Down Expand Up @@ -344,6 +340,5 @@ private static void endOfStream(HttpServerResponse response) {
response.end();
}
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.a2a.server.apps.quarkus;

import io.a2a.server.TransportMetadata;
import io.a2a.spec.TransportProtocol;

public class QuarkusJSONRPCTransportMetadata implements TransportMetadata {

@Override
public String getTransportProtocol() {
return TransportProtocol.JSONRPC.asString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.a2a.server.apps.quarkus.QuarkusJSONRPCTransportMetadata
127 changes: 127 additions & 0 deletions server-common/src/main/java/io/a2a/server/AgentCardValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package io.a2a.server;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import io.a2a.spec.AgentCard;
import io.a2a.spec.AgentInterface;

/**
* Validates AgentCard transport configuration against available transport endpoints.
*/
public class AgentCardValidator {

private static final Logger LOGGER = Logger.getLogger(AgentCardValidator.class.getName());

/**
* Validates the transport configuration of an AgentCard against available transports found on the classpath.
* Logs warnings for missing transports and errors for unsupported transports.
*
* @param agentCard the agent card to validate
*/
public static void validateTransportConfiguration(AgentCard agentCard) {
validateTransportConfiguration(agentCard, getAvailableTransports());
}

/**
* Validates the transport configuration of an AgentCard against a given set of available transports.
* This method is package-private for testability.
*
* @param agentCard the agent card to validate
* @param availableTransports the set of available transport protocols
*/
static void validateTransportConfiguration(AgentCard agentCard, Set<String> availableTransports) {
Set<String> agentCardTransports = getAgentCardTransports(agentCard);

// Check for missing transports (warn if AgentCard doesn't include all available transports)
Set<String> missingTransports = availableTransports.stream()
.filter(transport -> !agentCardTransports.contains(transport))
.collect(Collectors.toSet());

if (!missingTransports.isEmpty()) {
LOGGER.warning(String.format(
"AgentCard does not include all available transports. Missing: %s. " +
"Available transports: %s. AgentCard transports: %s",
formatTransports(missingTransports),
formatTransports(availableTransports),
formatTransports(agentCardTransports)
));
}

// Check for unsupported transports (error if AgentCard specifies unavailable transports)
Set<String> unsupportedTransports = agentCardTransports.stream()
.filter(transport -> !availableTransports.contains(transport))
.collect(Collectors.toSet());

if (!unsupportedTransports.isEmpty()) {
String errorMessage = String.format(
"AgentCard specifies transport interfaces for unavailable transports: %s. " +
"Available transports: %s. Consider removing these interfaces or adding the required transport dependencies.",
formatTransports(unsupportedTransports),
formatTransports(availableTransports)
);
LOGGER.severe(errorMessage);

// Following the GitHub issue suggestion to use an error instead of warning
throw new IllegalStateException(errorMessage);
}
}

/**
* Extracts all transport protocols specified in the AgentCard.
* Includes both the preferred transport and additional interface transports.
*
* @param agentCard the agent card to analyze
* @return set of transport protocols specified in the agent card
*/
private static Set<String> getAgentCardTransports(AgentCard agentCard) {
List<String> transportStrings = new ArrayList<>();

// Add preferred transport
if (agentCard.preferredTransport() != null) {
transportStrings.add(agentCard.preferredTransport());
}

// Add additional interface transports
if (agentCard.additionalInterfaces() != null) {
for (AgentInterface agentInterface : agentCard.additionalInterfaces()) {
if (agentInterface.transport() != null) {
transportStrings.add(agentInterface.transport());
}
}
}

return new HashSet<>(transportStrings);
}

/**
* Formats a set of transport protocols for logging.
*
* @param transports the transport protocols to format
* @return formatted string representation
*/
private static String formatTransports(Set<String> transports) {
return transports.stream()
.collect(Collectors.joining(", ", "[", "]"));
}

/**
* Discovers available transport endpoints using ServiceLoader.
* This searches the classpath for implementations of TransportMetadata.
*
* @return set of available transport protocols
*/
private static Set<String> getAvailableTransports() {
return ServiceLoader.load(TransportMetadata.class)
.stream()
.map(ServiceLoader.Provider::get)
.filter(TransportMetadata::isAvailable)
.map(TransportMetadata::getTransportProtocol)
.collect(Collectors.toSet());
}
}
27 changes: 27 additions & 0 deletions server-common/src/main/java/io/a2a/server/TransportMetadata.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.a2a.server;

import io.a2a.spec.TransportProtocol;

/**
* Interface for transport endpoint implementations to provide metadata about their transport.
* This is used by the validation system to discover available transports on the classpath.
*/
public interface TransportMetadata {

/**
* Returns the transport protocol this endpoint supports.
*
* @return the transport protocol
*/
String getTransportProtocol();

/**
* Checks if this transport endpoint is currently available/functional.
* This can be used for runtime availability checks beyond just classpath presence.
*
* @return true if the transport is available, false otherwise
*/
default boolean isAvailable() {
return true;
}
}
Loading