Skip to content

Commit e5dcb4d

Browse files
authored
feat: Add ability to skip transport validation (#266)
Also add missing validation to the RestHandler. Fixes #264 🦕
1 parent 0e27821 commit e5dcb4d

File tree

5 files changed

+196
-46
lines changed

5 files changed

+196
-46
lines changed

reference/rest/src/test/java/io/a2a/server/rest/quarkus/QuarkusA2ARestTest.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
package io.a2a.server.rest.quarkus;
22

3-
import static io.a2a.server.apps.common.AbstractA2AServerTest.APPLICATION_JSON;
3+
import java.net.URI;
4+
import java.net.http.HttpClient;
5+
import java.net.http.HttpRequest;
6+
import java.net.http.HttpResponse;
47

58
import io.a2a.client.ClientBuilder;
69
import io.a2a.client.transport.rest.RestTransport;
710
import io.a2a.client.transport.rest.RestTransportConfigBuilder;
811
import io.a2a.server.apps.common.AbstractA2AServerTest;
912
import io.a2a.spec.TransportProtocol;
1013
import io.quarkus.test.junit.QuarkusTest;
11-
import java.net.URI;
12-
import java.net.http.HttpClient;
13-
import java.net.http.HttpRequest;
14-
import java.net.http.HttpResponse;
1514
import org.junit.jupiter.api.Assertions;
1615
import org.junit.jupiter.api.Test;
1716

server-common/src/main/java/io/a2a/server/AgentCardValidator.java

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,21 @@
1010

1111
import io.a2a.spec.AgentCard;
1212
import io.a2a.spec.AgentInterface;
13+
import io.a2a.spec.TransportProtocol;
1314

1415
/**
1516
* Validates AgentCard transport configuration against available transport endpoints.
1617
*/
1718
public class AgentCardValidator {
1819

1920
private static final Logger LOGGER = Logger.getLogger(AgentCardValidator.class.getName());
20-
21+
22+
// Properties to turn off validation globally, or per known transport
23+
public static final String SKIP_PROPERTY = "io.a2a.transport.skipValidation";
24+
public static final String SKIP_JSONRPC_PROPERTY = "io.a2a.transport.jsonrpc.skipValidation";
25+
public static final String SKIP_GRPC_PROPERTY = "io.a2a.transport.grpc.skipValidation";
26+
public static final String SKIP_REST_PROPERTY = "io.a2a.transport.rest.skipValidation";
27+
2128
/**
2229
* Validates the transport configuration of an AgentCard against available transports found on the classpath.
2330
* Logs warnings for missing transports and errors for unsupported transports.
@@ -36,34 +43,41 @@ public static void validateTransportConfiguration(AgentCard agentCard) {
3643
* @param availableTransports the set of available transport protocols
3744
*/
3845
static void validateTransportConfiguration(AgentCard agentCard, Set<String> availableTransports) {
46+
boolean skip = Boolean.getBoolean(SKIP_PROPERTY);
47+
if (skip) {
48+
return;
49+
}
50+
3951
Set<String> agentCardTransports = getAgentCardTransports(agentCard);
52+
Set<String> filteredAvailableTransports = filterSkippedTransports(availableTransports);
53+
Set<String> filteredAgentCardTransports = filterSkippedTransports(agentCardTransports);
4054

4155
// 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))
56+
Set<String> missingTransports = filteredAvailableTransports.stream()
57+
.filter(transport -> !filteredAgentCardTransports.contains(transport))
4458
.collect(Collectors.toSet());
4559

4660
if (!missingTransports.isEmpty()) {
4761
LOGGER.warning(String.format(
4862
"AgentCard does not include all available transports. Missing: %s. " +
4963
"Available transports: %s. AgentCard transports: %s",
5064
formatTransports(missingTransports),
51-
formatTransports(availableTransports),
52-
formatTransports(agentCardTransports)
65+
formatTransports(filteredAvailableTransports),
66+
formatTransports(filteredAgentCardTransports)
5367
));
5468
}
5569

5670
// Check for unsupported transports (error if AgentCard specifies unavailable transports)
57-
Set<String> unsupportedTransports = agentCardTransports.stream()
58-
.filter(transport -> !availableTransports.contains(transport))
71+
Set<String> unsupportedTransports = filteredAgentCardTransports.stream()
72+
.filter(transport -> !filteredAvailableTransports.contains(transport))
5973
.collect(Collectors.toSet());
6074

6175
if (!unsupportedTransports.isEmpty()) {
6276
String errorMessage = String.format(
6377
"AgentCard specifies transport interfaces for unavailable transports: %s. " +
6478
"Available transports: %s. Consider removing these interfaces or adding the required transport dependencies.",
6579
formatTransports(unsupportedTransports),
66-
formatTransports(availableTransports)
80+
formatTransports(filteredAvailableTransports)
6781
);
6882
LOGGER.severe(errorMessage);
6983

@@ -110,6 +124,35 @@ private static String formatTransports(Set<String> transports) {
110124
.collect(Collectors.joining(", ", "[", "]"));
111125
}
112126

127+
/**
128+
* Filters out transports that have been configured to skip validation.
129+
*
130+
* @param transports the set of transport protocols to filter
131+
* @return filtered set with skipped transports removed
132+
*/
133+
private static Set<String> filterSkippedTransports(Set<String> transports) {
134+
return transports.stream()
135+
.filter(transport -> !isTransportSkipped(transport))
136+
.collect(Collectors.toSet());
137+
}
138+
139+
/**
140+
* Checks if validation should be skipped for a specific transport.
141+
*
142+
* @param transport the transport protocol to check
143+
* @return true if validation should be skipped for this transport
144+
*/
145+
private static boolean isTransportSkipped(String transport) {
146+
if (transport.equals(TransportProtocol.JSONRPC.asString())) {
147+
return Boolean.getBoolean(SKIP_JSONRPC_PROPERTY);
148+
} else if (transport.equals(TransportProtocol.GRPC.asString())){
149+
return Boolean.getBoolean(SKIP_GRPC_PROPERTY);
150+
} else if (transport.equals(TransportProtocol.HTTP_JSON.asString())) {
151+
return Boolean.getBoolean(SKIP_REST_PROPERTY);
152+
}
153+
return false;
154+
}
155+
113156
/**
114157
* Discovers available transport endpoints using ServiceLoader.
115158
* This searches the classpath for implementations of TransportMetadata.

server-common/src/test/java/io/a2a/server/AgentCardValidatorTest.java

Lines changed: 136 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,26 @@
1616
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
1717
import static org.junit.jupiter.api.Assertions.assertThrows;
1818
import static org.junit.jupiter.api.Assertions.assertTrue;
19+
import static org.junit.jupiter.api.Assertions.assertFalse;
1920

2021
public class AgentCardValidatorTest {
2122

22-
@Test
23-
void testValidationWithSimpleAgentCard() {
24-
// Create a simple AgentCard (uses default JSONRPC transport)
25-
AgentCard agentCard = new AgentCard.Builder()
23+
private AgentCard.Builder createTestAgentCardBuilder() {
24+
return new AgentCard.Builder()
2625
.name("Test Agent")
2726
.description("Test Description")
2827
.url("http://localhost:9999")
2928
.version("1.0.0")
3029
.capabilities(new AgentCapabilities.Builder().build())
3130
.defaultInputModes(Collections.singletonList("text"))
3231
.defaultOutputModes(Collections.singletonList("text"))
33-
.skills(Collections.emptyList())
32+
.skills(Collections.emptyList());
33+
}
34+
35+
@Test
36+
void testValidationWithSimpleAgentCard() {
37+
// Create a simple AgentCard (uses default JSONRPC transport)
38+
AgentCard agentCard = createTestAgentCardBuilder()
3439
.build();
3540

3641
// Define available transports
@@ -43,15 +48,7 @@ void testValidationWithSimpleAgentCard() {
4348
@Test
4449
void testValidationWithMultipleTransports() {
4550
// Create AgentCard that specifies multiple transports
46-
AgentCard agentCard = new AgentCard.Builder()
47-
.name("Test Agent")
48-
.description("Test Description")
49-
.url("http://localhost:9999")
50-
.version("1.0.0")
51-
.capabilities(new AgentCapabilities.Builder().build())
52-
.defaultInputModes(Collections.singletonList("text"))
53-
.defaultOutputModes(Collections.singletonList("text"))
54-
.skills(Collections.emptyList())
51+
AgentCard agentCard = createTestAgentCardBuilder()
5552
.preferredTransport(TransportProtocol.JSONRPC.asString())
5653
.additionalInterfaces(List.of(
5754
new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999"),
@@ -70,15 +67,7 @@ void testValidationWithMultipleTransports() {
7067
@Test
7168
void testLogWarningWhenExtraTransportsFound() {
7269
// Create an AgentCard with only JSONRPC
73-
AgentCard agentCard = new AgentCard.Builder()
74-
.name("Test Agent")
75-
.description("Test Description")
76-
.url("http://localhost:9999")
77-
.version("1.0.0")
78-
.capabilities(new AgentCapabilities.Builder().build())
79-
.defaultInputModes(Collections.singletonList("text"))
80-
.defaultOutputModes(Collections.singletonList("text"))
81-
.skills(Collections.emptyList())
70+
AgentCard agentCard = createTestAgentCardBuilder()
8271
.preferredTransport(TransportProtocol.JSONRPC.asString())
8372
.build();
8473

@@ -105,15 +94,7 @@ void testLogWarningWhenExtraTransportsFound() {
10594
@Test
10695
void testValidationWithUnavailableTransport() {
10796
// Create a simple AgentCard (uses default JSONRPC transport)
108-
AgentCard agentCard = new AgentCard.Builder()
109-
.name("Test Agent")
110-
.description("Test Description")
111-
.url("http://localhost:9999")
112-
.version("1.0.0")
113-
.capabilities(new AgentCapabilities.Builder().build())
114-
.defaultInputModes(Collections.singletonList("text"))
115-
.defaultOutputModes(Collections.singletonList("text"))
116-
.skills(Collections.emptyList())
97+
AgentCard agentCard = createTestAgentCardBuilder()
11798
.build();
11899

119100
// Define available transports (empty)
@@ -125,6 +106,129 @@ void testValidationWithUnavailableTransport() {
125106
assertTrue(exception.getMessage().contains("unavailable transports: [JSONRPC]"));
126107
}
127108

109+
@Test
110+
void testGlobalSkipProperty() {
111+
System.setProperty(AgentCardValidator.SKIP_PROPERTY, "true");
112+
try {
113+
AgentCard agentCard = createTestAgentCardBuilder()
114+
.build();
115+
116+
Set<String> availableTransports = Collections.emptySet();
117+
118+
assertDoesNotThrow(() -> AgentCardValidator.validateTransportConfiguration(agentCard, availableTransports));
119+
} finally {
120+
System.clearProperty(AgentCardValidator.SKIP_PROPERTY);
121+
}
122+
}
123+
124+
@Test
125+
void testSkipJsonrpcProperty() {
126+
System.setProperty(AgentCardValidator.SKIP_JSONRPC_PROPERTY, "true");
127+
try {
128+
AgentCard agentCard = createTestAgentCardBuilder()
129+
.preferredTransport(TransportProtocol.JSONRPC.asString())
130+
.build();
131+
132+
Set<String> availableTransports = Set.of(TransportProtocol.GRPC.asString());
133+
134+
assertDoesNotThrow(() -> AgentCardValidator.validateTransportConfiguration(agentCard, availableTransports));
135+
} finally {
136+
System.clearProperty(AgentCardValidator.SKIP_JSONRPC_PROPERTY);
137+
}
138+
}
139+
140+
@Test
141+
void testSkipGrpcProperty() {
142+
System.setProperty(AgentCardValidator.SKIP_GRPC_PROPERTY, "true");
143+
try {
144+
AgentCard agentCard = createTestAgentCardBuilder()
145+
.preferredTransport(TransportProtocol.GRPC.asString())
146+
.build();
147+
148+
Set<String> availableTransports = Set.of(TransportProtocol.JSONRPC.asString());
149+
150+
assertDoesNotThrow(() -> AgentCardValidator.validateTransportConfiguration(agentCard, availableTransports));
151+
} finally {
152+
System.clearProperty(AgentCardValidator.SKIP_GRPC_PROPERTY);
153+
}
154+
}
155+
156+
@Test
157+
void testSkipRestProperty() {
158+
System.setProperty(AgentCardValidator.SKIP_REST_PROPERTY, "true");
159+
try {
160+
AgentCard agentCard = createTestAgentCardBuilder()
161+
.additionalInterfaces(List.of(
162+
new AgentInterface(TransportProtocol.HTTP_JSON.asString(), "http://localhost:8080")
163+
))
164+
.build();
165+
166+
Set<String> availableTransports = Set.of(TransportProtocol.JSONRPC.asString());
167+
168+
assertDoesNotThrow(() -> AgentCardValidator.validateTransportConfiguration(agentCard, availableTransports));
169+
} finally {
170+
System.clearProperty(AgentCardValidator.SKIP_REST_PROPERTY);
171+
}
172+
}
173+
174+
@Test
175+
void testMultipleTransportsWithMixedSkipProperties() {
176+
System.setProperty(AgentCardValidator.SKIP_GRPC_PROPERTY, "true");
177+
try {
178+
AgentCard agentCard = createTestAgentCardBuilder()
179+
.preferredTransport(TransportProtocol.JSONRPC.asString())
180+
.additionalInterfaces(List.of(
181+
new AgentInterface(TransportProtocol.GRPC.asString(), "http://localhost:9000"),
182+
new AgentInterface(TransportProtocol.HTTP_JSON.asString(), "http://localhost:8080")
183+
))
184+
.build();
185+
186+
Set<String> availableTransports = Set.of(TransportProtocol.JSONRPC.asString());
187+
188+
IllegalStateException exception = assertThrows(IllegalStateException.class,
189+
() -> AgentCardValidator.validateTransportConfiguration(agentCard, availableTransports));
190+
assertTrue(exception.getMessage().contains("unavailable transports: [HTTP+JSON]"));
191+
} finally {
192+
System.clearProperty(AgentCardValidator.SKIP_GRPC_PROPERTY);
193+
}
194+
}
195+
196+
@Test
197+
void testSkipPropertiesFilterWarnings() {
198+
System.setProperty(AgentCardValidator.SKIP_GRPC_PROPERTY, "true");
199+
try {
200+
AgentCard agentCard = createTestAgentCardBuilder()
201+
.preferredTransport(TransportProtocol.JSONRPC.asString())
202+
.build();
203+
204+
Set<String> availableTransports = Set.of(
205+
TransportProtocol.JSONRPC.asString(),
206+
TransportProtocol.GRPC.asString(),
207+
TransportProtocol.HTTP_JSON.asString()
208+
);
209+
210+
Logger logger = Logger.getLogger(AgentCardValidator.class.getName());
211+
TestLogHandler testLogHandler = new TestLogHandler();
212+
logger.addHandler(testLogHandler);
213+
214+
try {
215+
AgentCardValidator.validateTransportConfiguration(agentCard, availableTransports);
216+
} finally {
217+
logger.removeHandler(testLogHandler);
218+
}
219+
220+
boolean foundWarning = testLogHandler.getLogMessages().stream()
221+
.anyMatch(msg -> msg.contains("Missing: [HTTP+JSON]"));
222+
assertTrue(foundWarning);
223+
224+
boolean grpcMentioned = testLogHandler.getLogMessages().stream()
225+
.anyMatch(msg -> msg.contains("GRPC"));
226+
assertFalse(grpcMentioned);
227+
} finally {
228+
System.clearProperty(AgentCardValidator.SKIP_GRPC_PROPERTY);
229+
}
230+
}
231+
128232
// A simple log handler for testing
129233
private static class TestLogHandler extends Handler {
130234
private final List<String> logMessages = new java.util.ArrayList<>();

transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.google.protobuf.InvalidProtocolBufferException;
77
import com.google.protobuf.util.JsonFormat;
88
import io.a2a.grpc.utils.ProtoUtils;
9+
import io.a2a.server.AgentCardValidator;
910
import io.a2a.server.ExtendedAgentCard;
1011
import jakarta.enterprise.context.ApplicationScoped;
1112
import jakarta.inject.Inject;
@@ -64,6 +65,9 @@ public RestHandler(@PublicAgentCard AgentCard agentCard, @ExtendedAgentCard Inst
6465
this.agentCard = agentCard;
6566
this.extendedAgentCard = extendedAgentCard;
6667
this.requestHandler = requestHandler;
68+
69+
// Validate transport configuration
70+
AgentCardValidator.validateTransportConfiguration(agentCard);
6771
}
6872

6973
public RestHandler(AgentCard agentCard, RequestHandler requestHandler) {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
preferred-transport=HTTP_JSON
1+
preferred-transport=HTTP+JSON

0 commit comments

Comments
 (0)