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
@@ -1,17 +1,16 @@
package io.a2a.server.rest.quarkus;

import static io.a2a.server.apps.common.AbstractA2AServerTest.APPLICATION_JSON;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import io.a2a.client.ClientBuilder;
import io.a2a.client.transport.rest.RestTransport;
import io.a2a.client.transport.rest.RestTransportConfigBuilder;
import io.a2a.server.apps.common.AbstractA2AServerTest;
import io.a2a.spec.TransportProtocol;
import io.quarkus.test.junit.QuarkusTest;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

Expand Down
59 changes: 51 additions & 8 deletions server-common/src/main/java/io/a2a/server/AgentCardValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,21 @@

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

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

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


// Properties to turn off validation globally, or per known transport
public static final String SKIP_PROPERTY = "io.a2a.transport.skipValidation";
public static final String SKIP_JSONRPC_PROPERTY = "io.a2a.transport.jsonrpc.skipValidation";
public static final String SKIP_GRPC_PROPERTY = "io.a2a.transport.grpc.skipValidation";
public static final String SKIP_REST_PROPERTY = "io.a2a.transport.rest.skipValidation";

/**
* Validates the transport configuration of an AgentCard against available transports found on the classpath.
* Logs warnings for missing transports and errors for unsupported transports.
Expand All @@ -36,34 +43,41 @@ public static void validateTransportConfiguration(AgentCard agentCard) {
* @param availableTransports the set of available transport protocols
*/
static void validateTransportConfiguration(AgentCard agentCard, Set<String> availableTransports) {
boolean skip = Boolean.getBoolean(SKIP_PROPERTY);
if (skip) {
return;
}

Set<String> agentCardTransports = getAgentCardTransports(agentCard);
Set<String> filteredAvailableTransports = filterSkippedTransports(availableTransports);
Set<String> filteredAgentCardTransports = filterSkippedTransports(agentCardTransports);

// Check for missing transports (warn if AgentCard doesn't include all available transports)
Set<String> missingTransports = availableTransports.stream()
.filter(transport -> !agentCardTransports.contains(transport))
Set<String> missingTransports = filteredAvailableTransports.stream()
.filter(transport -> !filteredAgentCardTransports.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)
formatTransports(filteredAvailableTransports),
formatTransports(filteredAgentCardTransports)
));
}

// Check for unsupported transports (error if AgentCard specifies unavailable transports)
Set<String> unsupportedTransports = agentCardTransports.stream()
.filter(transport -> !availableTransports.contains(transport))
Set<String> unsupportedTransports = filteredAgentCardTransports.stream()
.filter(transport -> !filteredAvailableTransports.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)
formatTransports(filteredAvailableTransports)
);
LOGGER.severe(errorMessage);

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

/**
* Filters out transports that have been configured to skip validation.
*
* @param transports the set of transport protocols to filter
* @return filtered set with skipped transports removed
*/
private static Set<String> filterSkippedTransports(Set<String> transports) {
return transports.stream()
.filter(transport -> !isTransportSkipped(transport))
.collect(Collectors.toSet());
}

/**
* Checks if validation should be skipped for a specific transport.
*
* @param transport the transport protocol to check
* @return true if validation should be skipped for this transport
*/
private static boolean isTransportSkipped(String transport) {
if (transport.equals(TransportProtocol.JSONRPC.asString())) {
return Boolean.getBoolean(SKIP_JSONRPC_PROPERTY);
} else if (transport.equals(TransportProtocol.GRPC.asString())){
return Boolean.getBoolean(SKIP_GRPC_PROPERTY);
} else if (transport.equals(TransportProtocol.HTTP_JSON.asString())) {
return Boolean.getBoolean(SKIP_REST_PROPERTY);
}
return false;
}
Comment on lines +145 to +154
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The if-else if chain can be simplified by using a switch statement. This improves readability and is slightly more efficient, making the code easier to maintain.

    private static boolean isTransportSkipped(String transport) {
        switch (transport) {
            case "JSONRPC":
                return Boolean.getBoolean(SKIP_JSONRPC_PROPERTY);
            case "GRPC":
                return Boolean.getBoolean(SKIP_GRPC_PROPERTY);
            case "HTTP+JSON":
                return Boolean.getBoolean(SKIP_REST_PROPERTY);
            default:
                return false;
        }
    }

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to use constants


/**
* Discovers available transport endpoints using ServiceLoader.
* This searches the classpath for implementations of TransportMetadata.
Expand Down
168 changes: 136 additions & 32 deletions server-common/src/test/java/io/a2a/server/AgentCardValidatorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,26 @@
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;

public class AgentCardValidatorTest {

@Test
void testValidationWithSimpleAgentCard() {
// Create a simple AgentCard (uses default JSONRPC transport)
AgentCard agentCard = new AgentCard.Builder()
private AgentCard.Builder createTestAgentCardBuilder() {
return new AgentCard.Builder()
.name("Test Agent")
.description("Test Description")
.url("http://localhost:9999")
.version("1.0.0")
.capabilities(new AgentCapabilities.Builder().build())
.defaultInputModes(Collections.singletonList("text"))
.defaultOutputModes(Collections.singletonList("text"))
.skills(Collections.emptyList())
.skills(Collections.emptyList());
}

@Test
void testValidationWithSimpleAgentCard() {
// Create a simple AgentCard (uses default JSONRPC transport)
AgentCard agentCard = createTestAgentCardBuilder()
.build();

// Define available transports
Expand All @@ -43,15 +48,7 @@ void testValidationWithSimpleAgentCard() {
@Test
void testValidationWithMultipleTransports() {
// Create AgentCard that specifies multiple transports
AgentCard agentCard = new AgentCard.Builder()
.name("Test Agent")
.description("Test Description")
.url("http://localhost:9999")
.version("1.0.0")
.capabilities(new AgentCapabilities.Builder().build())
.defaultInputModes(Collections.singletonList("text"))
.defaultOutputModes(Collections.singletonList("text"))
.skills(Collections.emptyList())
AgentCard agentCard = createTestAgentCardBuilder()
.preferredTransport(TransportProtocol.JSONRPC.asString())
.additionalInterfaces(List.of(
new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999"),
Expand All @@ -70,15 +67,7 @@ void testValidationWithMultipleTransports() {
@Test
void testLogWarningWhenExtraTransportsFound() {
// Create an AgentCard with only JSONRPC
AgentCard agentCard = new AgentCard.Builder()
.name("Test Agent")
.description("Test Description")
.url("http://localhost:9999")
.version("1.0.0")
.capabilities(new AgentCapabilities.Builder().build())
.defaultInputModes(Collections.singletonList("text"))
.defaultOutputModes(Collections.singletonList("text"))
.skills(Collections.emptyList())
AgentCard agentCard = createTestAgentCardBuilder()
.preferredTransport(TransportProtocol.JSONRPC.asString())
.build();

Expand All @@ -105,15 +94,7 @@ void testLogWarningWhenExtraTransportsFound() {
@Test
void testValidationWithUnavailableTransport() {
// Create a simple AgentCard (uses default JSONRPC transport)
AgentCard agentCard = new AgentCard.Builder()
.name("Test Agent")
.description("Test Description")
.url("http://localhost:9999")
.version("1.0.0")
.capabilities(new AgentCapabilities.Builder().build())
.defaultInputModes(Collections.singletonList("text"))
.defaultOutputModes(Collections.singletonList("text"))
.skills(Collections.emptyList())
AgentCard agentCard = createTestAgentCardBuilder()
.build();

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

@Test
void testGlobalSkipProperty() {
System.setProperty(AgentCardValidator.SKIP_PROPERTY, "true");
try {
AgentCard agentCard = createTestAgentCardBuilder()
.build();

Set<String> availableTransports = Collections.emptySet();

assertDoesNotThrow(() -> AgentCardValidator.validateTransportConfiguration(agentCard, availableTransports));
} finally {
System.clearProperty(AgentCardValidator.SKIP_PROPERTY);
}
}

@Test
void testSkipJsonrpcProperty() {
System.setProperty(AgentCardValidator.SKIP_JSONRPC_PROPERTY, "true");
try {
AgentCard agentCard = createTestAgentCardBuilder()
.preferredTransport(TransportProtocol.JSONRPC.asString())
.build();

Set<String> availableTransports = Set.of(TransportProtocol.GRPC.asString());

assertDoesNotThrow(() -> AgentCardValidator.validateTransportConfiguration(agentCard, availableTransports));
} finally {
System.clearProperty(AgentCardValidator.SKIP_JSONRPC_PROPERTY);
}
}

@Test
void testSkipGrpcProperty() {
System.setProperty(AgentCardValidator.SKIP_GRPC_PROPERTY, "true");
try {
AgentCard agentCard = createTestAgentCardBuilder()
.preferredTransport(TransportProtocol.GRPC.asString())
.build();

Set<String> availableTransports = Set.of(TransportProtocol.JSONRPC.asString());

assertDoesNotThrow(() -> AgentCardValidator.validateTransportConfiguration(agentCard, availableTransports));
} finally {
System.clearProperty(AgentCardValidator.SKIP_GRPC_PROPERTY);
}
}

@Test
void testSkipRestProperty() {
System.setProperty(AgentCardValidator.SKIP_REST_PROPERTY, "true");
try {
AgentCard agentCard = createTestAgentCardBuilder()
.additionalInterfaces(List.of(
new AgentInterface(TransportProtocol.HTTP_JSON.asString(), "http://localhost:8080")
))
.build();

Set<String> availableTransports = Set.of(TransportProtocol.JSONRPC.asString());

assertDoesNotThrow(() -> AgentCardValidator.validateTransportConfiguration(agentCard, availableTransports));
} finally {
System.clearProperty(AgentCardValidator.SKIP_REST_PROPERTY);
}
}

@Test
void testMultipleTransportsWithMixedSkipProperties() {
System.setProperty(AgentCardValidator.SKIP_GRPC_PROPERTY, "true");
try {
AgentCard agentCard = createTestAgentCardBuilder()
.preferredTransport(TransportProtocol.JSONRPC.asString())
.additionalInterfaces(List.of(
new AgentInterface(TransportProtocol.GRPC.asString(), "http://localhost:9000"),
new AgentInterface(TransportProtocol.HTTP_JSON.asString(), "http://localhost:8080")
))
.build();

Set<String> availableTransports = Set.of(TransportProtocol.JSONRPC.asString());

IllegalStateException exception = assertThrows(IllegalStateException.class,
() -> AgentCardValidator.validateTransportConfiguration(agentCard, availableTransports));
assertTrue(exception.getMessage().contains("unavailable transports: [HTTP+JSON]"));
} finally {
System.clearProperty(AgentCardValidator.SKIP_GRPC_PROPERTY);
}
}

@Test
void testSkipPropertiesFilterWarnings() {
System.setProperty(AgentCardValidator.SKIP_GRPC_PROPERTY, "true");
try {
AgentCard agentCard = createTestAgentCardBuilder()
.preferredTransport(TransportProtocol.JSONRPC.asString())
.build();

Set<String> availableTransports = Set.of(
TransportProtocol.JSONRPC.asString(),
TransportProtocol.GRPC.asString(),
TransportProtocol.HTTP_JSON.asString()
);

Logger logger = Logger.getLogger(AgentCardValidator.class.getName());
TestLogHandler testLogHandler = new TestLogHandler();
logger.addHandler(testLogHandler);

try {
AgentCardValidator.validateTransportConfiguration(agentCard, availableTransports);
} finally {
logger.removeHandler(testLogHandler);
}

boolean foundWarning = testLogHandler.getLogMessages().stream()
.anyMatch(msg -> msg.contains("Missing: [HTTP+JSON]"));
assertTrue(foundWarning);

boolean grpcMentioned = testLogHandler.getLogMessages().stream()
.anyMatch(msg -> msg.contains("GRPC"));
assertFalse(grpcMentioned);
} finally {
System.clearProperty(AgentCardValidator.SKIP_GRPC_PROPERTY);
}
}

// A simple log handler for testing
private static class TestLogHandler extends Handler {
private final List<String> logMessages = new java.util.ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import io.a2a.grpc.utils.ProtoUtils;
import io.a2a.server.AgentCardValidator;
import io.a2a.server.ExtendedAgentCard;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
Expand Down Expand Up @@ -64,6 +65,9 @@ public RestHandler(@PublicAgentCard AgentCard agentCard, @ExtendedAgentCard Inst
this.agentCard = agentCard;
this.extendedAgentCard = extendedAgentCard;
this.requestHandler = requestHandler;

// Validate transport configuration
AgentCardValidator.validateTransportConfiguration(agentCard);
}

public RestHandler(AgentCard agentCard, RequestHandler requestHandler) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
preferred-transport=HTTP_JSON
preferred-transport=HTTP+JSON