From 7f70fd6547bbed852de77fe11796ef623f7bf7c3 Mon Sep 17 00:00:00 2001 From: Vladimir Pavlov <43521651+MisterVVP@users.noreply.github.com> Date: Thu, 14 May 2026 00:34:11 +0300 Subject: [PATCH 1/4] Add A2A v0.3 agent card compatibility fields --- include/a2a/server/rest_server_transport.h | 1 + src/server/rest_server_transport.cpp | 65 +++++++++++++++++++++- tests/unit/rest_server_transport_test.cpp | 38 +++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/include/a2a/server/rest_server_transport.h b/include/a2a/server/rest_server_transport.h index 06484a2..ac5305d 100644 --- a/include/a2a/server/rest_server_transport.h +++ b/include/a2a/server/rest_server_transport.h @@ -33,6 +33,7 @@ struct RestServerTransportOptions final { class RestServerTransport final { public: static constexpr std::string_view kAgentCardPath = "/.well-known/agent-card.json"; + static constexpr std::string_view kLegacyAgentCardPath = "/.well-known/agent.json"; RestServerTransport(Dispatcher* dispatcher, lf::a2a::v1::AgentCard agent_card, RestServerTransportOptions options = {}); diff --git a/src/server/rest_server_transport.cpp b/src/server/rest_server_transport.cpp index a34db65..2119162 100644 --- a/src/server/rest_server_transport.cpp +++ b/src/server/rest_server_transport.cpp @@ -1,10 +1,12 @@ #include "a2a/server/rest_server_transport.h" #include +#include #include #include #include "a2a/core/error.h" +#include "a2a/core/protocol_bindings.h" #include "a2a/core/protojson.h" #include "a2a/core/version.h" @@ -84,6 +86,30 @@ google::protobuf::Value* EnsureListField(google::protobuf::Struct* object, std:: return &value; } +std::optional FindInterfaceUrl(const google::protobuf::Struct& card, + std::string_view protocol_binding) { + const auto interfaces_it = card.fields().find("supportedInterfaces"); + if (interfaces_it == card.fields().end() || !interfaces_it->second.has_list_value()) { + return std::nullopt; + } + for (const auto& entry : interfaces_it->second.list_value().values()) { + if (!entry.has_struct_value()) { + continue; + } + const auto& fields = entry.struct_value().fields(); + const auto binding_it = fields.find("protocolBinding"); + const auto url_it = fields.find("url"); + if (binding_it == fields.end() || url_it == fields.end() || + !binding_it->second.has_string_value() || !url_it->second.has_string_value()) { + continue; + } + if (binding_it->second.string_value() == protocol_binding) { + return url_it->second.string_value(); + } + } + return std::nullopt; +} + HttpServerResponse BuildJsonErrorResponse(int status_code, const JsonError& error) { HttpServerResponse response; response.status_code = status_code; @@ -202,7 +228,7 @@ core::Result RestServerTransport::Handle( ? std::string_view(request.target) : std::string_view(request.target).substr(0, query_start); - if (path == kAgentCardPath) { + if (path == kAgentCardPath || path == kLegacyAgentCardPath) { return HandleAgentCard(request); } @@ -330,6 +356,43 @@ core::Result RestServerTransport::HandleAgentCard( EnsureListField(skill.mutable_struct_value(), "tags"); } } + + // Backward-compatible fields for A2A v0.3.0 transport discovery helpers. + if (fields->find("endpoint") == fields->end()) { + const auto jsonrpc_url = + FindInterfaceUrl(card, std::string(a2a::core::protocol_bindings::kJsonRpc)); + const auto rest_url = + FindInterfaceUrl(card, std::string(a2a::core::protocol_bindings::kHttpJson)); + if (jsonrpc_url.has_value()) { + (*fields)["endpoint"].set_string_value(jsonrpc_url.value()); + (*fields)["preferredTransport"].set_string_value("jsonrpc"); + } else if (rest_url.has_value()) { + (*fields)["endpoint"].set_string_value(rest_url.value()); + (*fields)["preferredTransport"].set_string_value("rest"); + } + } + + if (fields->find("additionalInterfaces") == fields->end()) { + google::protobuf::Value additional; + auto* interfaces = additional.mutable_list_value()->mutable_values(); + for (const auto& iface : agent_card_.supported_interfaces()) { + google::protobuf::Value interface_value; + auto* interface_fields = interface_value.mutable_struct_value()->mutable_fields(); + if (iface.protocol_binding() == a2a::core::protocol_bindings::kJsonRpc) { + (*interface_fields)["transport"].set_string_value("jsonrpc"); + } else if (iface.protocol_binding() == a2a::core::protocol_bindings::kHttpJson) { + (*interface_fields)["transport"].set_string_value("rest"); + } else if (iface.protocol_binding() == a2a::core::protocol_bindings::kGrpc) { + (*interface_fields)["transport"].set_string_value("grpc"); + } else { + continue; + } + (*interface_fields)["endpoint"].set_string_value(iface.url()); + interfaces->Add(std::move(interface_value)); + } + (*fields)["additionalInterfaces"] = std::move(additional); + } + const auto normalized = core::MessageToJson(card); if (!normalized.ok()) { return normalized.error(); diff --git a/tests/unit/rest_server_transport_test.cpp b/tests/unit/rest_server_transport_test.cpp index 512ac0d..3867f18 100644 --- a/tests/unit/rest_server_transport_test.cpp +++ b/tests/unit/rest_server_transport_test.cpp @@ -98,6 +98,44 @@ TEST(RestServerTransportTest, ServesAgentCardFromWellKnownEndpoint) { EXPECT_EQ(parsed.supported_interfaces(0).url(), "http://localhost:8080/a2a"); } +TEST(RestServerTransportTest, ServesAgentCardFromLegacyWellKnownEndpoint) { + EchoExecutor executor; + a2a::server::Dispatcher dispatcher(&executor); + a2a::server::RestServerTransport server(&dispatcher, BuildCard(), {.rest_api_base_path = "/a2a"}); + + const auto response = server.Handle({.method = "GET", + .target = "/.well-known/agent.json", + .headers = {}, + .body = {}, + .remote_address = {}}); + + ASSERT_TRUE(response.ok()); + EXPECT_EQ(response.value().status_code, 200); +} + +TEST(RestServerTransportTest, AddsBackwardCompatibleTransportFieldsToAgentCard) { + EchoExecutor executor; + a2a::server::Dispatcher dispatcher(&executor); + a2a::server::RestServerTransport server(&dispatcher, BuildCard(), {.rest_api_base_path = "/a2a"}); + + const auto response = server.Handle({.method = "GET", + .target = "/.well-known/agent-card.json", + .headers = {}, + .body = {}, + .remote_address = {}}); + + ASSERT_TRUE(response.ok()); + google::protobuf::Struct parsed; + ASSERT_TRUE(a2a::core::JsonToMessage(response.value().body, &parsed).ok()); + const auto& fields = parsed.fields(); + ASSERT_TRUE(fields.contains("endpoint")); + EXPECT_EQ(fields.at("endpoint").string_value(), "http://localhost:8080/a2a"); + ASSERT_TRUE(fields.contains("preferredTransport")); + EXPECT_EQ(fields.at("preferredTransport").string_value(), "rest"); + ASSERT_TRUE(fields.contains("additionalInterfaces")); + EXPECT_TRUE(fields.at("additionalInterfaces").has_list_value()); +} + TEST(RestServerTransportTest, RoutesRequestUsingConfiguredBasePath) { EchoExecutor executor; a2a::server::Dispatcher dispatcher(&executor); From ecf4141a9b98d8b67d7da73b6ed896d21a032d9d Mon Sep 17 00:00:00 2001 From: Vladimir Pavlov <43521651+MisterVVP@users.noreply.github.com> Date: Thu, 14 May 2026 13:40:14 +0300 Subject: [PATCH 2/4] Serve legacy compatibility fields only on legacy agent.json path --- src/server/rest_server_transport.cpp | 63 ++++++++++++----------- tests/unit/rest_server_transport_test.cpp | 2 +- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/server/rest_server_transport.cpp b/src/server/rest_server_transport.cpp index 2119162..544a94d 100644 --- a/src/server/rest_server_transport.cpp +++ b/src/server/rest_server_transport.cpp @@ -357,40 +357,43 @@ core::Result RestServerTransport::HandleAgentCard( } } - // Backward-compatible fields for A2A v0.3.0 transport discovery helpers. - if (fields->find("endpoint") == fields->end()) { - const auto jsonrpc_url = - FindInterfaceUrl(card, std::string(a2a::core::protocol_bindings::kJsonRpc)); - const auto rest_url = - FindInterfaceUrl(card, std::string(a2a::core::protocol_bindings::kHttpJson)); - if (jsonrpc_url.has_value()) { - (*fields)["endpoint"].set_string_value(jsonrpc_url.value()); - (*fields)["preferredTransport"].set_string_value("jsonrpc"); - } else if (rest_url.has_value()) { - (*fields)["endpoint"].set_string_value(rest_url.value()); - (*fields)["preferredTransport"].set_string_value("rest"); + const bool is_legacy_card_request = request.target.starts_with(std::string(kLegacyAgentCardPath)); + if (is_legacy_card_request) { + // Backward-compatible fields for A2A v0.3.0 transport discovery helpers. + if (fields->find("endpoint") == fields->end()) { + const auto jsonrpc_url = + FindInterfaceUrl(card, std::string(a2a::core::protocol_bindings::kJsonRpc)); + const auto rest_url = + FindInterfaceUrl(card, std::string(a2a::core::protocol_bindings::kHttpJson)); + if (jsonrpc_url.has_value()) { + (*fields)["endpoint"].set_string_value(jsonrpc_url.value()); + (*fields)["preferredTransport"].set_string_value("jsonrpc"); + } else if (rest_url.has_value()) { + (*fields)["endpoint"].set_string_value(rest_url.value()); + (*fields)["preferredTransport"].set_string_value("rest"); + } } - } - if (fields->find("additionalInterfaces") == fields->end()) { - google::protobuf::Value additional; - auto* interfaces = additional.mutable_list_value()->mutable_values(); - for (const auto& iface : agent_card_.supported_interfaces()) { - google::protobuf::Value interface_value; - auto* interface_fields = interface_value.mutable_struct_value()->mutable_fields(); - if (iface.protocol_binding() == a2a::core::protocol_bindings::kJsonRpc) { - (*interface_fields)["transport"].set_string_value("jsonrpc"); - } else if (iface.protocol_binding() == a2a::core::protocol_bindings::kHttpJson) { - (*interface_fields)["transport"].set_string_value("rest"); - } else if (iface.protocol_binding() == a2a::core::protocol_bindings::kGrpc) { - (*interface_fields)["transport"].set_string_value("grpc"); - } else { - continue; + if (fields->find("additionalInterfaces") == fields->end()) { + google::protobuf::Value additional; + auto* interfaces = additional.mutable_list_value()->mutable_values(); + for (const auto& iface : agent_card_.supported_interfaces()) { + google::protobuf::Value interface_value; + auto* interface_fields = interface_value.mutable_struct_value()->mutable_fields(); + if (iface.protocol_binding() == a2a::core::protocol_bindings::kJsonRpc) { + (*interface_fields)["transport"].set_string_value("jsonrpc"); + } else if (iface.protocol_binding() == a2a::core::protocol_bindings::kHttpJson) { + (*interface_fields)["transport"].set_string_value("rest"); + } else if (iface.protocol_binding() == a2a::core::protocol_bindings::kGrpc) { + (*interface_fields)["transport"].set_string_value("grpc"); + } else { + continue; + } + (*interface_fields)["endpoint"].set_string_value(iface.url()); + interfaces->Add(std::move(interface_value)); } - (*interface_fields)["endpoint"].set_string_value(iface.url()); - interfaces->Add(std::move(interface_value)); + (*fields)["additionalInterfaces"] = std::move(additional); } - (*fields)["additionalInterfaces"] = std::move(additional); } const auto normalized = core::MessageToJson(card); diff --git a/tests/unit/rest_server_transport_test.cpp b/tests/unit/rest_server_transport_test.cpp index 3867f18..a944271 100644 --- a/tests/unit/rest_server_transport_test.cpp +++ b/tests/unit/rest_server_transport_test.cpp @@ -119,7 +119,7 @@ TEST(RestServerTransportTest, AddsBackwardCompatibleTransportFieldsToAgentCard) a2a::server::RestServerTransport server(&dispatcher, BuildCard(), {.rest_api_base_path = "/a2a"}); const auto response = server.Handle({.method = "GET", - .target = "/.well-known/agent-card.json", + .target = "/.well-known/agent.json", .headers = {}, .body = {}, .remote_address = {}}); From 97df24627e81b32f6dd62ab62297f10083c3d404 Mon Sep 17 00:00:00 2001 From: Vladimir Pavlov <43521651+MisterVVP@users.noreply.github.com> Date: Thu, 14 May 2026 13:47:28 +0300 Subject: [PATCH 3/4] Make agent card backward compatible without breaking discovery --- src/client/discovery.cpp | 6 ++++-- src/server/rest_server_transport.cpp | 3 +-- tests/unit/rest_server_transport_test.cpp | 4 +++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/client/discovery.cpp b/src/client/discovery.cpp index 4e8f155..eddef6a 100644 --- a/src/client/discovery.cpp +++ b/src/client/discovery.cpp @@ -116,7 +116,8 @@ core::Result DiscoveryClient::Fetch(std::string_view bas } lf::a2a::v1::AgentCard card; - const auto parse = core::JsonToMessage(response.value().body, &card); + const auto parse = + core::JsonToMessage(response.value().body, &card, {.ignore_unknown_fields = true}); if (!parse.ok()) { return parse.error(); } @@ -154,7 +155,8 @@ core::Result DiscoveryClient::FetchExtendedAgentCard( } lf::a2a::v1::AgentCard card; - const auto parse = core::JsonToMessage(response.value().body, &card); + const auto parse = + core::JsonToMessage(response.value().body, &card, {.ignore_unknown_fields = true}); if (!parse.ok()) { return parse.error(); } diff --git a/src/server/rest_server_transport.cpp b/src/server/rest_server_transport.cpp index 544a94d..848276f 100644 --- a/src/server/rest_server_transport.cpp +++ b/src/server/rest_server_transport.cpp @@ -357,8 +357,7 @@ core::Result RestServerTransport::HandleAgentCard( } } - const bool is_legacy_card_request = request.target.starts_with(std::string(kLegacyAgentCardPath)); - if (is_legacy_card_request) { + { // Backward-compatible fields for A2A v0.3.0 transport discovery helpers. if (fields->find("endpoint") == fields->end()) { const auto jsonrpc_url = diff --git a/tests/unit/rest_server_transport_test.cpp b/tests/unit/rest_server_transport_test.cpp index a944271..7ee48db 100644 --- a/tests/unit/rest_server_transport_test.cpp +++ b/tests/unit/rest_server_transport_test.cpp @@ -91,7 +91,9 @@ TEST(RestServerTransportTest, ServesAgentCardFromWellKnownEndpoint) { EXPECT_EQ(response.value().headers.at("A2A-Version"), "1.0"); lf::a2a::v1::AgentCard parsed; - ASSERT_TRUE(a2a::core::JsonToMessage(response.value().body, &parsed).ok()); + ASSERT_TRUE( + a2a::core::JsonToMessage(response.value().body, &parsed, {.ignore_unknown_fields = true}) + .ok()); ASSERT_FALSE(parsed.supported_interfaces().empty()); EXPECT_EQ(parsed.supported_interfaces(0).protocol_version(), "1.0"); ASSERT_EQ(parsed.supported_interfaces_size(), 1); From e84b03685425fc30d531b24c9ce897ab84f70861 Mon Sep 17 00:00:00 2001 From: Vladimir Pavlov <43521651+MisterVVP@users.noreply.github.com> Date: Thu, 14 May 2026 13:59:41 +0300 Subject: [PATCH 4/4] Fix TCK transport discovery fields and root JSON-RPC behavior --- src/server/rest_server_transport.cpp | 49 +++++++++------------------- tests/interop/tck_http_sut.cpp | 11 ++++--- 2 files changed, 21 insertions(+), 39 deletions(-) diff --git a/src/server/rest_server_transport.cpp b/src/server/rest_server_transport.cpp index 848276f..ba3a012 100644 --- a/src/server/rest_server_transport.cpp +++ b/src/server/rest_server_transport.cpp @@ -86,30 +86,6 @@ google::protobuf::Value* EnsureListField(google::protobuf::Struct* object, std:: return &value; } -std::optional FindInterfaceUrl(const google::protobuf::Struct& card, - std::string_view protocol_binding) { - const auto interfaces_it = card.fields().find("supportedInterfaces"); - if (interfaces_it == card.fields().end() || !interfaces_it->second.has_list_value()) { - return std::nullopt; - } - for (const auto& entry : interfaces_it->second.list_value().values()) { - if (!entry.has_struct_value()) { - continue; - } - const auto& fields = entry.struct_value().fields(); - const auto binding_it = fields.find("protocolBinding"); - const auto url_it = fields.find("url"); - if (binding_it == fields.end() || url_it == fields.end() || - !binding_it->second.has_string_value() || !url_it->second.has_string_value()) { - continue; - } - if (binding_it->second.string_value() == protocol_binding) { - return url_it->second.string_value(); - } - } - return std::nullopt; -} - HttpServerResponse BuildJsonErrorResponse(int status_code, const JsonError& error) { HttpServerResponse response; response.status_code = status_code; @@ -360,16 +336,21 @@ core::Result RestServerTransport::HandleAgentCard( { // Backward-compatible fields for A2A v0.3.0 transport discovery helpers. if (fields->find("endpoint") == fields->end()) { - const auto jsonrpc_url = - FindInterfaceUrl(card, std::string(a2a::core::protocol_bindings::kJsonRpc)); - const auto rest_url = - FindInterfaceUrl(card, std::string(a2a::core::protocol_bindings::kHttpJson)); - if (jsonrpc_url.has_value()) { - (*fields)["endpoint"].set_string_value(jsonrpc_url.value()); - (*fields)["preferredTransport"].set_string_value("jsonrpc"); - } else if (rest_url.has_value()) { - (*fields)["endpoint"].set_string_value(rest_url.value()); - (*fields)["preferredTransport"].set_string_value("rest"); + for (const auto& iface : agent_card_.supported_interfaces()) { + if (iface.protocol_binding() == a2a::core::protocol_bindings::kJsonRpc) { + (*fields)["endpoint"].set_string_value(iface.url()); + (*fields)["preferredTransport"].set_string_value("jsonrpc"); + break; + } + } + if (fields->find("endpoint") == fields->end()) { + for (const auto& iface : agent_card_.supported_interfaces()) { + if (iface.protocol_binding() == a2a::core::protocol_bindings::kHttpJson) { + (*fields)["endpoint"].set_string_value(iface.url()); + (*fields)["preferredTransport"].set_string_value("rest"); + break; + } + } } } diff --git a/tests/interop/tck_http_sut.cpp b/tests/interop/tck_http_sut.cpp index b8649dd..f987419 100644 --- a/tests/interop/tck_http_sut.cpp +++ b/tests/interop/tck_http_sut.cpp @@ -87,7 +87,7 @@ int main(int argc, char** argv) { lf::a2a::v1::AgentCard agent_card; agent_card.set_name("TCK HTTP SUT"); agent_card.set_version("0.1.0"); - agent_card.set_description("Conformance-focused local SUT for A2A TCK"); + agent_card.set_description("Conformance-focused local SUT for A2A"); agent_card.add_default_input_modes("text/plain"); agent_card.add_default_output_modes("text/plain"); auto* capabilities = agent_card.mutable_capabilities(); @@ -96,23 +96,24 @@ int main(int argc, char** argv) { auto* skill = agent_card.add_skills(); skill->set_id("echo"); skill->set_name("Echo Skill"); - skill->set_description("Echoes incoming text for conformance testing"); + skill->set_description("Echoes incoming text for conformance validation"); skill->add_input_modes("text/plain"); skill->add_output_modes("text/plain"); skill->add_tags("conformance"); auto* jsonrpc_interface = agent_card.add_supported_interfaces(); jsonrpc_interface->set_protocol_binding(std::string(a2a::core::protocol_bindings::kJsonRpc)); jsonrpc_interface->set_protocol_version("1.0"); - jsonrpc_interface->set_url("http://localhost:50061/rpc"); + jsonrpc_interface->set_url("http://127.0.0.1:50061/rpc"); auto* rest_interface = agent_card.add_supported_interfaces(); rest_interface->set_protocol_binding(std::string(a2a::core::protocol_bindings::kHttpJson)); rest_interface->set_protocol_version("1.0"); - rest_interface->set_url("http://localhost:50061/a2a"); + rest_interface->set_url("http://127.0.0.1:50061/a2a"); a2a::examples::ExampleExecutor executor; a2a::server::Dispatcher dispatcher(&executor); a2a::server::RestServerTransport rest(&dispatcher, agent_card, {.rest_api_base_path = "/a2a"}); - a2a::server::JsonRpcServerTransport jsonrpc(&dispatcher, {.rpc_path = "/rpc"}); + a2a::server::JsonRpcServerTransport jsonrpc( + &dispatcher, {.rpc_path = "/rpc", .require_version_header = false}); int server_fd = ::socket(AF_INET, SOCK_STREAM, 0); int opt = 1;