Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
fix: default missing A2A-Version to 0.3 per spec Section 3.6.2
The spec requires that agents interpret empty/missing A2A-Version
as 0.3 for backward compatibility. Previously defaulted to the
current protocol version (1.0). All transport handler tests updated
to explicitly pass version "1.0" in their test contexts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  • Loading branch information
kabir and claude committed May 6, 2026
commit 301aab81e4162158077a211650a4b472406adf4d
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
* <li>Minor versions are compatible (1.0 client can talk to 1.1 server and vice versa)</li>
* </ul>
*
* <p>If the client does not specify a version, the current protocol version
* ({@link AgentInterface#CURRENT_PROTOCOL_VERSION}) is assumed as the default.
* <p>Per A2A spec Section 3.6.2, if the client does not specify a version,
* version "0.3" is assumed for backward compatibility.
*/
public class A2AVersionValidator {

Expand All @@ -34,9 +34,9 @@ public static void validateProtocolVersion(AgentCard agentCard, ServerCallContex
throws VersionNotSupportedError {
String requestedVersion = context.getRequestedProtocolVersion();

// If client didn't specify a version, default to current version
// Per spec Section 3.6.2: empty/missing A2A-Version defaults to 0.3
if (requestedVersion == null || requestedVersion.trim().isEmpty()) {
requestedVersion = AgentInterface.CURRENT_PROTOCOL_VERSION;
requestedVersion = "0.3";
}

// Collect all unique protocol versions from all supported interfaces
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,46 @@ public void testIsVersionCompatible_InvalidFormat() {
}

@Test
public void testValidateProtocolVersion_NoVersionProvided_DefaultsTo1_0() {
// When no version is provided, should default to 1.0 and succeed
public void testValidateProtocolVersion_NoVersionProvided_DefaultsTo0_3() {
// Per spec Section 3.6.2: missing A2A-Version defaults to 0.3
// A v1.0-only server should reject this
AgentCard agentCard = createAgentCard("1.0");
ServerCallContext context = createContext(null);

// Should not throw - defaults to 1.0 which matches
assertDoesNotThrow(() -> A2AVersionValidator.validateProtocolVersion(agentCard, context));
VersionNotSupportedError error = assertThrows(VersionNotSupportedError.class,
() -> A2AVersionValidator.validateProtocolVersion(agentCard, context));
assertTrue(error.getMessage().contains("0.3"));
assertTrue(error.getMessage().contains("not supported"));
}

@Test
public void testValidateProtocolVersion_EmptyVersionProvided_DefaultsTo1_0() {
// When empty version is provided, should default to 1.0 and succeed
public void testValidateProtocolVersion_EmptyVersionProvided_DefaultsTo0_3() {
// Per spec Section 3.6.2: empty A2A-Version defaults to 0.3
// A v1.0-only server should reject this
AgentCard agentCard = createAgentCard("1.0");
ServerCallContext context = createContext("");

// Should not throw - defaults to 1.0 which matches
VersionNotSupportedError error = assertThrows(VersionNotSupportedError.class,
() -> A2AVersionValidator.validateProtocolVersion(agentCard, context));
assertTrue(error.getMessage().contains("0.3"));
assertTrue(error.getMessage().contains("not supported"));
}

@Test
public void testValidateProtocolVersion_NoVersionProvided_DualVersionServer() {
// A server supporting both 1.0 and 0.3 should accept missing version (defaults to 0.3)
AgentCard agentCard = createAgentCardWithVersions("1.0", "0.3");
ServerCallContext context = createContext(null);

assertDoesNotThrow(() -> A2AVersionValidator.validateProtocolVersion(agentCard, context));
}

@Test
public void testValidateProtocolVersion_NoVersionProvided_V03OnlyServer() {
// A v0.3-only server should accept missing version (defaults to 0.3)
AgentCard agentCard = createAgentCard("0.3");
ServerCallContext context = createContext(null);

assertDoesNotThrow(() -> A2AVersionValidator.validateProtocolVersion(agentCard, context));
}

Expand Down Expand Up @@ -141,10 +165,18 @@ public void testValidateProtocolVersion_InvalidVersionFormat() {
}

private AgentCard createAgentCard(String protocolVersion) {
return createAgentCardWithVersions(protocolVersion);
}

private AgentCard createAgentCardWithVersions(String... protocolVersions) {
List<AgentInterface> interfaces = new java.util.ArrayList<>();
for (String version : protocolVersions) {
interfaces.add(new AgentInterface("GRPC", "http://localhost:9999", "", version));
}
return AgentCard.builder()
.name("test-card")
.description("Test card")
.supportedInterfaces(List.of(new AgentInterface("GRPC", "http://localhost:9999", "", protocolVersion)))
.supportedInterfaces(interfaces)
.version("1.0.0")
.capabilities(AgentCapabilities.builder()
.streaming(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -942,7 +942,8 @@ public <V> ServerCallContext create(StreamObserver<V> streamObserver) {
return new ServerCallContext(
UnauthenticatedUser.INSTANCE,
Map.of("grpc_response_observer", streamObserver),
requestedExtensions
requestedExtensions,
"1.0"
);
}
};
Expand Down Expand Up @@ -1109,8 +1110,8 @@ public <V> ServerCallContext create(StreamObserver<V> streamObserver) {
}

@Test
public void testNoVersionDefaultsToCurrentVersionSuccess() throws Exception {
// Create AgentCard with protocol version 1.0 (current version)
public void testNoVersionDefaultsTo0_3_RejectedByV10OnlyServer() throws Exception {
// Per spec Section 3.6.2: missing A2A-Version defaults to 0.3
AgentCard agentCard = AgentCard.builder()
.name("test-card")
.description("Test card with version 1.0")
Expand All @@ -1125,7 +1126,7 @@ public void testNoVersionDefaultsToCurrentVersionSuccess() throws Exception {
.skills(List.of())
.build();

// Create handler that provides null version (should default to 1.0)
// Create handler that provides null version — defaults to 0.3, incompatible with v1.0-only
GrpcHandler handler = new TestGrpcHandler(agentCard, requestHandler, internalExecutor) {
@Override
protected CallContextFactory getCallContextFactory() {
Expand All @@ -1136,7 +1137,7 @@ public <V> ServerCallContext create(StreamObserver<V> streamObserver) {
UnauthenticatedUser.INSTANCE,
Map.of("grpc_response_observer", streamObserver),
new HashSet<>(),
null // No version - should default to 1.0
null
);
}
};
Expand All @@ -1155,9 +1156,9 @@ public <V> ServerCallContext create(StreamObserver<V> streamObserver) {
handler.sendMessage(request, streamRecorder);
streamRecorder.awaitCompletion(5, TimeUnit.SECONDS);

// Should succeed without error (defaults to 1.0)
Assertions.assertNull(streamRecorder.getError());
Assertions.assertFalse(streamRecorder.getValues().isEmpty());
// Should fail with VersionNotSupportedError (0.3 is not supported by v1.0-only server)
Assertions.assertNotNull(streamRecorder.getError());
Assertions.assertInstanceOf(io.grpc.StatusRuntimeException.class, streamRecorder.getError());
}

private StreamRecorder<SendMessageResponse> sendMessageRequest(GrpcHandler handler) throws Exception {
Expand Down Expand Up @@ -1308,7 +1309,17 @@ protected AgentCard getExtendedAgentCard() {

@Override
protected CallContextFactory getCallContextFactory() {
return null;
return new CallContextFactory() {
@Override
public <V> ServerCallContext create(StreamObserver<V> streamObserver) {
return new ServerCallContext(
UnauthenticatedUser.INSTANCE,
Map.of("grpc_response_observer", streamObserver),
new HashSet<>(),
"1.0"
);
}
};
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
Expand Down Expand Up @@ -91,7 +92,7 @@
@Timeout(value = 1, unit = TimeUnit.MINUTES)
public class JSONRPCHandlerTest extends AbstractA2ARequestHandlerTest {

private final ServerCallContext callContext = new ServerCallContext(UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>());
private final ServerCallContext callContext = new ServerCallContext(UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>(), "1.0");

private static MessageSendConfiguration defaultConfiguration() {
return MessageSendConfiguration.builder()
Expand Down Expand Up @@ -998,7 +999,7 @@ public void testOnSubscribeNoExistingTaskError() {
TaskNotFoundError.class,
() -> handler.onSubscribeToTask(request, callContext));

Assertions.assertNotNull(thrown);
assertNotNull(thrown);
}

@Test
Expand Down Expand Up @@ -1135,7 +1136,7 @@ public void testOnGetPushNotificationNoPushNotifierConfig() {
MINIMAL_TASK.id(),"c295ea44-7543-4f78-b524-7a38915ad6e4"));
GetTaskPushNotificationConfigResponse response = handler.getPushNotificationConfig(request, callContext);

Assertions.assertNotNull(response.getError());
assertNotNull(response.getError());
assertInstanceOf(UnsupportedOperationError.class, response.getError());
assertEquals("This operation is not supported", response.getError().getMessage());
}
Expand Down Expand Up @@ -1742,7 +1743,8 @@ public void testRequiredExtensionProvidedSuccess() {
ServerCallContext contextWithExtension = new ServerCallContext(
UnauthenticatedUser.INSTANCE,
Map.of("foo", "bar"),
requestedExtensions
requestedExtensions,
"1.0"
);

agentExecutorExecute = (context, agentEmitter) -> {
Expand Down Expand Up @@ -1921,8 +1923,8 @@ public void testCompatibleVersionSuccess() {
}

@Test
public void testNoVersionDefaultsToCurrentVersionSuccess() {
// Create AgentCard with protocol version 1.0 (current version)
public void testNoVersionDefaultsTo0_3_RejectedByV10OnlyServer() {
// Per spec Section 3.6.2: missing A2A-Version defaults to 0.3
AgentCard agentCard = AgentCard.builder()
.name("test-card")
.description("Test card with version 1.0")
Expand All @@ -1940,21 +1942,25 @@ public void testNoVersionDefaultsToCurrentVersionSuccess() {
JSONRPCHandler handler = new JSONRPCHandler(agentCard, requestHandler, internalExecutor);
taskStore.save(MINIMAL_TASK, false);

// Use default callContext (no version - should default to 1.0)
agentExecutorExecute = (context, agentEmitter) -> {
agentEmitter.sendMessage(context.getMessage());
};

// Context with no version — defaults to 0.3, which is incompatible with v1.0-only server
ServerCallContext noVersionContext = new ServerCallContext(
UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>());

Message message = Message.builder(MESSAGE)
.taskId(MINIMAL_TASK.id())
.contextId(MINIMAL_TASK.contextId())
.build();
SendMessageRequest request = new SendMessageRequest("1", new MessageSendParams(message, null, null));
SendMessageResponse response = handler.onMessageSend(request, callContext);
SendMessageResponse response = handler.onMessageSend(request, noVersionContext);

// Should succeed without error (defaults to 1.0)
assertNull(response.getError());
Assertions.assertSame(message, response.getResult());
// Should return VersionNotSupportedError
assertNotNull(response.getError());
assertInstanceOf(VersionNotSupportedError.class, response.getError());
assertTrue(response.getError().getMessage().contains("0.3"));
}

@Test
Expand All @@ -1972,8 +1978,8 @@ public void testListTasksEmptyResultIncludesAllFields() {
// Should return success with all fields present (not null)
assertNull(response.getError());
ListTasksResult result = response.getResult();
Assertions.assertNotNull(result);
Assertions.assertNotNull(result.tasks(), "tasks field should not be null");
assertNotNull(result);
assertNotNull(result.tasks(), "tasks field should not be null");
assertEquals(0, result.tasks().size(), "tasks should be empty list");
assertEquals(0, result.totalSize(), "totalSize should be 0");
assertEquals(0, result.pageSize(), "pageSize should be 0");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
@Timeout(value = 1, unit = TimeUnit.MINUTES)
public class RestHandlerTest extends AbstractA2ARequestHandlerTest {

private final ServerCallContext callContext = new ServerCallContext(UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>());
private final ServerCallContext callContext = new ServerCallContext(UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>(), "1.0");

private static AgentCardCacheMetadata createCacheMetadata() {
return createCacheMetadata(CARD);
Expand Down Expand Up @@ -722,7 +722,8 @@ public void testRequiredExtensionProvidedSuccess() {
ServerCallContext contextWithExtension = new ServerCallContext(
UnauthenticatedUser.INSTANCE,
Map.of("foo", "bar"),
requestedExtensions
requestedExtensions,
"1.0"
);

agentExecutorExecute = (context, agentEmitter) -> {
Expand Down Expand Up @@ -955,8 +956,8 @@ public void testCompatibleVersionSuccess() {
}

@Test
public void testNoVersionDefaultsToCurrentVersionSuccess() {
// Create AgentCard with protocol version 1.0 (current version)
public void testNoVersionDefaultsTo0_3_RejectedByV10OnlyServer() {
// Per spec Section 3.6.2: missing A2A-Version defaults to 0.3
AgentCard agentCard = AgentCard.builder()
.name("test-card")
.description("Test card with version 1.0")
Expand All @@ -973,11 +974,14 @@ public void testNoVersionDefaultsToCurrentVersionSuccess() {

RestHandler handler = new RestHandler(agentCard, createCacheMetadata(agentCard), requestHandler, internalExecutor);

// Use default callContext (no version - should default to 1.0)
agentExecutorExecute = (context, agentEmitter) -> {
agentEmitter.sendMessage(context.getMessage());
};

// Context with no version — defaults to 0.3, incompatible with v1.0-only server
ServerCallContext noVersionContext = new ServerCallContext(
UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>());

String requestBody = """
{
"message": {
Expand All @@ -994,12 +998,12 @@ public void testNoVersionDefaultsToCurrentVersionSuccess() {
}
}""";

RestHandler.HTTPRestResponse response = handler.sendMessage(callContext, "", requestBody);
RestHandler.HTTPRestResponse response = handler.sendMessage(noVersionContext, "", requestBody);

// Should succeed without error (defaults to 1.0)
Assertions.assertEquals(200, response.getStatusCode());
Assertions.assertEquals(APPLICATION_JSON, response.getContentType());
Assertions.assertNotNull(response.getBody());
// Should return error (0.3 is not supported by v1.0-only server)
Assertions.assertEquals(400, response.getStatusCode());
Assertions.assertTrue(response.getBody().contains("0.3"));
Assertions.assertTrue(response.getBody().contains("not supported"));
}

@Test
Expand Down