Skip to content

Commit 8e70e48

Browse files
authored
feat: Check transports in AgentCard match those on classpath (#251)
Fixes #246 🦕
1 parent 6ab3680 commit 8e70e48

File tree

20 files changed

+455
-26
lines changed

20 files changed

+455
-26
lines changed

examples/helloworld/server/src/main/java/io/a2a/examples/helloworld/AgentCardProducer.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public class AgentCardProducer {
1717
@Produces
1818
@PublicAgentCard
1919
public AgentCard agentCard() {
20+
// NOTE: Transport validation will automatically check that transports specified
21+
// in this AgentCard match those available on the classpath when handlers are initialized
2022
return new AgentCard.Builder()
2123
.name("Hello World Agent")
2224
.description("Just a hello world agent")

reference/grpc/src/main/java/io/a2a/server/grpc/quarkus/QuarkusGrpcHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import jakarta.enterprise.inject.Instance;
44
import jakarta.inject.Inject;
55

6-
import io.a2a.transport.grpc.handler.CallContextFactory;
7-
import io.a2a.transport.grpc.handler.GrpcHandler;
86
import io.a2a.server.PublicAgentCard;
97
import io.a2a.server.requesthandlers.RequestHandler;
108
import io.a2a.spec.AgentCard;
9+
import io.a2a.transport.grpc.handler.CallContextFactory;
10+
import io.a2a.transport.grpc.handler.GrpcHandler;
1111
import io.quarkus.grpc.GrpcService;
1212

1313
@GrpcService
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.a2a.server.grpc.quarkus;
2+
3+
import io.a2a.server.TransportMetadata;
4+
import io.a2a.spec.TransportProtocol;
5+
6+
public class QuarkusGrpcTransportMetadata implements TransportMetadata {
7+
@Override
8+
public String getTransportProtocol() {
9+
return TransportProtocol.GRPC.asString();
10+
}
11+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
io.a2a.server.grpc.quarkus.QuarkusGrpcTransportMetadata

reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,11 @@
1414
import jakarta.enterprise.inject.Instance;
1515
import jakarta.inject.Inject;
1616
import jakarta.inject.Singleton;
17-
import jakarta.ws.rs.core.Response;
1817

1918
import com.fasterxml.jackson.core.JsonParseException;
2019
import com.fasterxml.jackson.core.JsonProcessingException;
2120
import com.fasterxml.jackson.core.io.JsonEOFException;
2221
import com.fasterxml.jackson.databind.JsonNode;
23-
import io.a2a.transport.jsonrpc.handler.JSONRPCHandler;
24-
import io.a2a.server.ExtendedAgentCard;
2522
import io.a2a.server.ServerCallContext;
2623
import io.a2a.server.auth.UnauthenticatedUser;
2724
import io.a2a.server.auth.User;
@@ -37,7 +34,6 @@
3734
import io.a2a.spec.InvalidParamsError;
3835
import io.a2a.spec.InvalidParamsJsonMappingException;
3936
import io.a2a.spec.InvalidRequestError;
40-
import io.a2a.spec.JSONErrorResponse;
4137
import io.a2a.spec.JSONParseError;
4238
import io.a2a.spec.JSONRPCError;
4339
import io.a2a.spec.JSONRPCErrorResponse;
@@ -53,11 +49,11 @@
5349
import io.a2a.spec.StreamingJSONRPCRequest;
5450
import io.a2a.spec.TaskResubscriptionRequest;
5551
import io.a2a.spec.UnsupportedOperationError;
52+
import io.a2a.transport.jsonrpc.handler.JSONRPCHandler;
5653
import io.a2a.util.Utils;
5754
import io.quarkus.vertx.web.Body;
5855
import io.quarkus.vertx.web.ReactiveRoutes;
5956
import io.quarkus.vertx.web.Route;
60-
import io.quarkus.vertx.web.RoutingExchange;
6157
import io.smallrye.mutiny.Multi;
6258
import io.vertx.core.AsyncResult;
6359
import io.vertx.core.Handler;
@@ -344,6 +340,5 @@ private static void endOfStream(HttpServerResponse response) {
344340
response.end();
345341
}
346342
}
347-
348343
}
349344

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.a2a.server.apps.quarkus;
2+
3+
import io.a2a.server.TransportMetadata;
4+
import io.a2a.spec.TransportProtocol;
5+
6+
public class QuarkusJSONRPCTransportMetadata implements TransportMetadata {
7+
8+
@Override
9+
public String getTransportProtocol() {
10+
return TransportProtocol.JSONRPC.asString();
11+
}
12+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
io.a2a.server.apps.quarkus.QuarkusJSONRPCTransportMetadata
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package io.a2a.server;
2+
3+
import java.util.ArrayList;
4+
import java.util.HashSet;
5+
import java.util.List;
6+
import java.util.ServiceLoader;
7+
import java.util.Set;
8+
import java.util.logging.Logger;
9+
import java.util.stream.Collectors;
10+
11+
import io.a2a.spec.AgentCard;
12+
import io.a2a.spec.AgentInterface;
13+
14+
/**
15+
* Validates AgentCard transport configuration against available transport endpoints.
16+
*/
17+
public class AgentCardValidator {
18+
19+
private static final Logger LOGGER = Logger.getLogger(AgentCardValidator.class.getName());
20+
21+
/**
22+
* Validates the transport configuration of an AgentCard against available transports found on the classpath.
23+
* Logs warnings for missing transports and errors for unsupported transports.
24+
*
25+
* @param agentCard the agent card to validate
26+
*/
27+
public static void validateTransportConfiguration(AgentCard agentCard) {
28+
validateTransportConfiguration(agentCard, getAvailableTransports());
29+
}
30+
31+
/**
32+
* Validates the transport configuration of an AgentCard against a given set of available transports.
33+
* This method is package-private for testability.
34+
*
35+
* @param agentCard the agent card to validate
36+
* @param availableTransports the set of available transport protocols
37+
*/
38+
static void validateTransportConfiguration(AgentCard agentCard, Set<String> availableTransports) {
39+
Set<String> agentCardTransports = getAgentCardTransports(agentCard);
40+
41+
// Check for missing transports (warn if AgentCard doesn't include all available transports)
42+
Set<String> missingTransports = availableTransports.stream()
43+
.filter(transport -> !agentCardTransports.contains(transport))
44+
.collect(Collectors.toSet());
45+
46+
if (!missingTransports.isEmpty()) {
47+
LOGGER.warning(String.format(
48+
"AgentCard does not include all available transports. Missing: %s. " +
49+
"Available transports: %s. AgentCard transports: %s",
50+
formatTransports(missingTransports),
51+
formatTransports(availableTransports),
52+
formatTransports(agentCardTransports)
53+
));
54+
}
55+
56+
// Check for unsupported transports (error if AgentCard specifies unavailable transports)
57+
Set<String> unsupportedTransports = agentCardTransports.stream()
58+
.filter(transport -> !availableTransports.contains(transport))
59+
.collect(Collectors.toSet());
60+
61+
if (!unsupportedTransports.isEmpty()) {
62+
String errorMessage = String.format(
63+
"AgentCard specifies transport interfaces for unavailable transports: %s. " +
64+
"Available transports: %s. Consider removing these interfaces or adding the required transport dependencies.",
65+
formatTransports(unsupportedTransports),
66+
formatTransports(availableTransports)
67+
);
68+
LOGGER.severe(errorMessage);
69+
70+
// Following the GitHub issue suggestion to use an error instead of warning
71+
throw new IllegalStateException(errorMessage);
72+
}
73+
}
74+
75+
/**
76+
* Extracts all transport protocols specified in the AgentCard.
77+
* Includes both the preferred transport and additional interface transports.
78+
*
79+
* @param agentCard the agent card to analyze
80+
* @return set of transport protocols specified in the agent card
81+
*/
82+
private static Set<String> getAgentCardTransports(AgentCard agentCard) {
83+
List<String> transportStrings = new ArrayList<>();
84+
85+
// Add preferred transport
86+
if (agentCard.preferredTransport() != null) {
87+
transportStrings.add(agentCard.preferredTransport());
88+
}
89+
90+
// Add additional interface transports
91+
if (agentCard.additionalInterfaces() != null) {
92+
for (AgentInterface agentInterface : agentCard.additionalInterfaces()) {
93+
if (agentInterface.transport() != null) {
94+
transportStrings.add(agentInterface.transport());
95+
}
96+
}
97+
}
98+
99+
return new HashSet<>(transportStrings);
100+
}
101+
102+
/**
103+
* Formats a set of transport protocols for logging.
104+
*
105+
* @param transports the transport protocols to format
106+
* @return formatted string representation
107+
*/
108+
private static String formatTransports(Set<String> transports) {
109+
return transports.stream()
110+
.collect(Collectors.joining(", ", "[", "]"));
111+
}
112+
113+
/**
114+
* Discovers available transport endpoints using ServiceLoader.
115+
* This searches the classpath for implementations of TransportMetadata.
116+
*
117+
* @return set of available transport protocols
118+
*/
119+
private static Set<String> getAvailableTransports() {
120+
return ServiceLoader.load(TransportMetadata.class)
121+
.stream()
122+
.map(ServiceLoader.Provider::get)
123+
.filter(TransportMetadata::isAvailable)
124+
.map(TransportMetadata::getTransportProtocol)
125+
.collect(Collectors.toSet());
126+
}
127+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.a2a.server;
2+
3+
import io.a2a.spec.TransportProtocol;
4+
5+
/**
6+
* Interface for transport endpoint implementations to provide metadata about their transport.
7+
* This is used by the validation system to discover available transports on the classpath.
8+
*/
9+
public interface TransportMetadata {
10+
11+
/**
12+
* Returns the transport protocol this endpoint supports.
13+
*
14+
* @return the transport protocol
15+
*/
16+
String getTransportProtocol();
17+
18+
/**
19+
* Checks if this transport endpoint is currently available/functional.
20+
* This can be used for runtime availability checks beyond just classpath presence.
21+
*
22+
* @return true if the transport is available, false otherwise
23+
*/
24+
default boolean isAvailable() {
25+
return true;
26+
}
27+
}

0 commit comments

Comments
 (0)