diff --git a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java index 18e18a2f1..ae15f415f 100644 --- a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java +++ b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java @@ -2,6 +2,7 @@ import static io.a2a.transport.jsonrpc.context.JSONRPCContextKeys.HEADERS_KEY; import static io.a2a.transport.jsonrpc.context.JSONRPCContextKeys.METHOD_NAME_KEY; +import static io.a2a.transport.jsonrpc.context.JSONRPCContextKeys.TENANT_KEY; import static io.vertx.core.http.HttpHeaders.CONTENT_TYPE; import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; import static jakarta.ws.rs.core.MediaType.SERVER_SENT_EVENTS; @@ -101,7 +102,7 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) { Multi> streamingResponse = null; A2AErrorResponse error = null; try { - A2ARequest request = JSONRPCUtils.parseRequestBody(body); + A2ARequest request = JSONRPCUtils.parseRequestBody(body, extractTenant(rc)); context.getState().put(METHOD_NAME_KEY, request.getMethod()); if (request instanceof NonStreamingJSONRPCRequest nonStreamingRequest) { nonStreamingResponse = processNonStreamingRequest(nonStreamingRequest, context); @@ -213,7 +214,6 @@ static void setStreamingMultiSseSupportSubscribedRunnable(Runnable runnable) { } private ServerCallContext createCallContext(RoutingContext rc) { - if (callContextFactory.isUnsatisfied()) { User user; if (rc.user() == null) { @@ -240,6 +240,7 @@ public String getUsername() { Set headerNames = rc.request().headers().names(); headerNames.forEach(name -> headers.put(name, rc.request().getHeader(name))); state.put(HEADERS_KEY, headers); + state.put(TENANT_KEY, extractTenant(rc)); // Extract requested protocol version from X-A2A-Version header String requestedVersion = rc.request().getHeader(A2AHeaders.X_A2A_VERSION); @@ -255,6 +256,20 @@ public String getUsername() { } } + private String extractTenant(RoutingContext rc) { + String tenantPath = rc.normalizedPath(); + if (tenantPath == null || tenantPath.isBlank()) { + return ""; + } + if (tenantPath.startsWith("/")) { + tenantPath = tenantPath.substring(1); + } + if(tenantPath.endsWith("/")) { + tenantPath = tenantPath.substring(0, tenantPath.length() -1); + } + return tenantPath; + } + private static String serializeResponse(A2AResponse response) { // For error responses, use Jackson serialization (errors are standardized) if (response instanceof A2AErrorResponse error) { diff --git a/reference/jsonrpc/src/test/java/io/a2a/server/apps/quarkus/A2AServerRoutesTest.java b/reference/jsonrpc/src/test/java/io/a2a/server/apps/quarkus/A2AServerRoutesTest.java index 3f28d564d..429eff9e1 100644 --- a/reference/jsonrpc/src/test/java/io/a2a/server/apps/quarkus/A2AServerRoutesTest.java +++ b/reference/jsonrpc/src/test/java/io/a2a/server/apps/quarkus/A2AServerRoutesTest.java @@ -5,6 +5,7 @@ import static io.a2a.spec.A2AMethods.SEND_STREAMING_MESSAGE_METHOD; import static io.a2a.spec.AgentCard.CURRENT_PROTOCOL_VERSION; import static io.a2a.transport.jsonrpc.context.JSONRPCContextKeys.METHOD_NAME_KEY; +import static io.a2a.transport.jsonrpc.context.JSONRPCContextKeys.TENANT_KEY; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -108,6 +109,7 @@ public void setUp() { when(mockRoutingContext.user()).thenReturn(null); when(mockRequest.headers()).thenReturn(mockHeaders); when(mockRoutingContext.body()).thenReturn(mockRequestBody); + when(mockRoutingContext.normalizedPath()).thenReturn("/"); // Chain the response methods properly when(mockHttpResponse.setStatusCode(any(Integer.class))).thenReturn(mockHttpResponse); @@ -520,6 +522,202 @@ public void testGetExtendedCard_MethodNameSetInContext() { assertEquals(GET_EXTENDED_AGENT_CARD_METHOD, capturedContext.getState().get(METHOD_NAME_KEY)); } + @Test + public void testTenantExtraction_MultiSegmentPath() { + // Arrange - simulate request to /test/titi + when(mockRoutingContext.normalizedPath()).thenReturn("/test/titi"); + String jsonRpcRequest = """ + { + "jsonrpc": "2.0", + "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64", + "method": "GetTask", + "params": { + "name": "tasks/de38c76d-d54c-436c-8b9f-4c2703648d64", + "historyLength": 10 + } + }"""; + when(mockRequestBody.asString()).thenReturn(jsonRpcRequest); + + Task responseTask = Task.builder() + .id("de38c76d-d54c-436c-8b9f-4c2703648d64") + .contextId("context-1234") + .status(new TaskStatus(TaskState.SUBMITTED)) + .build(); + GetTaskResponse realResponse = new GetTaskResponse("1", responseTask); + when(mockJsonRpcHandler.onGetTask(any(GetTaskRequest.class), any(ServerCallContext.class))) + .thenReturn(realResponse); + + ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(ServerCallContext.class); + + // Act + routes.invokeJSONRPCHandler(jsonRpcRequest, mockRoutingContext); + + // Assert + verify(mockJsonRpcHandler).onGetTask(any(GetTaskRequest.class), contextCaptor.capture()); + ServerCallContext capturedContext = contextCaptor.getValue(); + assertNotNull(capturedContext); + assertEquals("test/titi", capturedContext.getState().get(TENANT_KEY)); + } + + @Test + public void testTenantExtraction_RootPath() { + // Arrange - simulate request to / + when(mockRoutingContext.normalizedPath()).thenReturn("/"); + String jsonRpcRequest = """ + { + "jsonrpc": "2.0", + "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64", + "method": "GetTask", + "params": { + "name": "tasks/de38c76d-d54c-436c-8b9f-4c2703648d64", + "historyLength": 10 + } + }"""; + when(mockRequestBody.asString()).thenReturn(jsonRpcRequest); + + Task responseTask = Task.builder() + .id("de38c76d-d54c-436c-8b9f-4c2703648d64") + .contextId("context-1234") + .status(new TaskStatus(TaskState.SUBMITTED)) + .build(); + GetTaskResponse realResponse = new GetTaskResponse("1", responseTask); + when(mockJsonRpcHandler.onGetTask(any(GetTaskRequest.class), any(ServerCallContext.class))) + .thenReturn(realResponse); + + ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(ServerCallContext.class); + + // Act + routes.invokeJSONRPCHandler(jsonRpcRequest, mockRoutingContext); + + // Assert + verify(mockJsonRpcHandler).onGetTask(any(GetTaskRequest.class), contextCaptor.capture()); + ServerCallContext capturedContext = contextCaptor.getValue(); + assertNotNull(capturedContext); + assertEquals("", capturedContext.getState().get(TENANT_KEY)); + } + + @Test + public void testTenantExtraction_SingleSegmentPath() { + // Arrange - simulate request to /tenant1 + when(mockRoutingContext.normalizedPath()).thenReturn("/tenant1"); + String jsonRpcRequest = """ + { + "jsonrpc": "2.0", + "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64", + "method": "GetTask", + "params": { + "name": "tasks/de38c76d-d54c-436c-8b9f-4c2703648d64", + "historyLength": 10 + } + }"""; + when(mockRequestBody.asString()).thenReturn(jsonRpcRequest); + + Task responseTask = Task.builder() + .id("de38c76d-d54c-436c-8b9f-4c2703648d64") + .contextId("context-1234") + .status(new TaskStatus(TaskState.SUBMITTED)) + .build(); + GetTaskResponse realResponse = new GetTaskResponse("1", responseTask); + when(mockJsonRpcHandler.onGetTask(any(GetTaskRequest.class), any(ServerCallContext.class))) + .thenReturn(realResponse); + + ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(ServerCallContext.class); + + // Act + routes.invokeJSONRPCHandler(jsonRpcRequest, mockRoutingContext); + + // Assert + verify(mockJsonRpcHandler).onGetTask(any(GetTaskRequest.class), contextCaptor.capture()); + ServerCallContext capturedContext = contextCaptor.getValue(); + assertNotNull(capturedContext); + assertEquals("tenant1", capturedContext.getState().get(TENANT_KEY)); + } + + @Test + public void testTenantExtraction_ThreeSegmentPath() { + // Arrange - simulate request to /tenant1/api/v1 + when(mockRoutingContext.normalizedPath()).thenReturn("/tenant1/api/v1"); + String jsonRpcRequest = """ + { + "jsonrpc": "2.0", + "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64", + "method": "GetTask", + "params": { + "name": "tasks/de38c76d-d54c-436c-8b9f-4c2703648d64", + "historyLength": 10 + } + }"""; + when(mockRequestBody.asString()).thenReturn(jsonRpcRequest); + + Task responseTask = Task.builder() + .id("de38c76d-d54c-436c-8b9f-4c2703648d64") + .contextId("context-1234") + .status(new TaskStatus(TaskState.SUBMITTED)) + .build(); + GetTaskResponse realResponse = new GetTaskResponse("1", responseTask); + when(mockJsonRpcHandler.onGetTask(any(GetTaskRequest.class), any(ServerCallContext.class))) + .thenReturn(realResponse); + + ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(ServerCallContext.class); + + // Act + routes.invokeJSONRPCHandler(jsonRpcRequest, mockRoutingContext); + + // Assert + verify(mockJsonRpcHandler).onGetTask(any(GetTaskRequest.class), contextCaptor.capture()); + ServerCallContext capturedContext = contextCaptor.getValue(); + assertNotNull(capturedContext); + assertEquals("tenant1/api/v1", capturedContext.getState().get(TENANT_KEY)); + } + + @Test + public void testTenantExtraction_StreamingRequest() { + // Arrange - simulate streaming request to /myTenant/api + when(mockRoutingContext.normalizedPath()).thenReturn("/myTenant/api"); + String jsonRpcRequest = """ + { + "jsonrpc": "2.0", + "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64", + "method": "SendStreamingMessage", + "params": { + "message": { + "messageId": "message-1234", + "contextId": "context-1234", + "role": "ROLE_USER", + "parts": [ + { + "text": "tell me a joke" + } + ], + "metadata": {} + }, + "configuration": { + "acceptedOutputModes": ["text"], + "blocking": true + }, + "metadata": {} + } + }"""; + when(mockRequestBody.asString()).thenReturn(jsonRpcRequest); + + @SuppressWarnings("unchecked") + Flow.Publisher mockPublisher = mock(Flow.Publisher.class); + when(mockJsonRpcHandler.onMessageSendStream(any(SendStreamingMessageRequest.class), + any(ServerCallContext.class))).thenReturn(mockPublisher); + + ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(ServerCallContext.class); + + // Act + routes.invokeJSONRPCHandler(jsonRpcRequest, mockRoutingContext); + + // Assert + verify(mockJsonRpcHandler).onMessageSendStream(any(SendStreamingMessageRequest.class), + contextCaptor.capture()); + ServerCallContext capturedContext = contextCaptor.getValue(); + assertNotNull(capturedContext); + assertEquals("myTenant/api", capturedContext.getState().get(TENANT_KEY)); + } + /** * Helper method to set a field via reflection for testing purposes. */ diff --git a/reference/rest/src/main/java/io/a2a/server/rest/quarkus/A2AServerRoutes.java b/reference/rest/src/main/java/io/a2a/server/rest/quarkus/A2AServerRoutes.java index 46d0d38e6..6c714b6fd 100644 --- a/reference/rest/src/main/java/io/a2a/server/rest/quarkus/A2AServerRoutes.java +++ b/reference/rest/src/main/java/io/a2a/server/rest/quarkus/A2AServerRoutes.java @@ -58,6 +58,8 @@ import static io.a2a.spec.A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD; import static io.a2a.spec.A2AMethods.SUBSCRIBE_TO_TASK_METHOD; +import static io.a2a.transport.rest.context.RestContextKeys.TENANT_KEY; + @Singleton @Authenticated public class A2AServerRoutes { @@ -435,6 +437,7 @@ public String getUsername() { headerNames.forEach(name -> headers.put(name, rc.request().getHeader(name))); state.put(HEADERS_KEY, headers); state.put(METHOD_NAME_KEY, jsonRpcMethodName); + state.put(TENANT_KEY, extractTenant(rc)); // Extract requested protocol version from X-A2A-Version header String requestedVersion = rc.request().getHeader(A2AHeaders.X_A2A_VERSION); diff --git a/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java b/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java index 1692ae3a5..e49bcbeec 100644 --- a/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java +++ b/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java @@ -183,7 +183,7 @@ public class JSONRPCUtils { private static final Pattern EXTRACT_WRONG_TYPE = Pattern.compile("Expected (.*) but found \".*\""); static final String ERROR_MESSAGE = "Invalid request content: %s. Please verify the request matches the expected schema for this method."; - public static A2ARequest parseRequestBody(String body) throws JsonMappingException, JsonProcessingException { + public static A2ARequest parseRequestBody(String body, @Nullable String tenant) throws JsonMappingException, JsonProcessingException { JsonElement jelement = JsonParser.parseString(body); JsonObject jsonRpc = jelement.getAsJsonObject(); if (!jsonRpc.has("method")) { @@ -196,52 +196,76 @@ public static A2ARequest parseRequestBody(String body) throws JsonMappingExce String method = jsonRpc.get("method").getAsString(); JsonElement paramsNode = jsonRpc.get("params"); try { - return parseMethodRequest(version, id, method, paramsNode); + return parseMethodRequest(version, id, method, paramsNode, tenant); } catch (InvalidParamsError e) { throw new InvalidParamsJsonMappingException(Utils.defaultIfNull(e.getMessage(), "Invalid parameters"), id); } } - private static A2ARequest parseMethodRequest(String version, Object id, String method, JsonElement paramsNode) throws InvalidParamsError, MethodNotFoundJsonMappingException, JsonProcessingException { + private static A2ARequest parseMethodRequest(String version, Object id, String method, JsonElement paramsNode, @Nullable String tenant) throws InvalidParamsError, MethodNotFoundJsonMappingException, JsonProcessingException { switch (method) { case GET_TASK_METHOD -> { io.a2a.grpc.GetTaskRequest.Builder builder = io.a2a.grpc.GetTaskRequest.newBuilder(); parseRequestBody(paramsNode, builder, id); + if (tenant != null && !tenant.isBlank() && (builder.getTenant() == null || builder.getTenant().isBlank())) { + builder.setTenant(tenant); + } return new GetTaskRequest(version, id, ProtoUtils.FromProto.taskQueryParams(builder)); } case CANCEL_TASK_METHOD -> { io.a2a.grpc.CancelTaskRequest.Builder builder = io.a2a.grpc.CancelTaskRequest.newBuilder(); parseRequestBody(paramsNode, builder, id); + if (tenant != null && !tenant.isBlank() && (builder.getTenant() == null || builder.getTenant().isBlank())) { + builder.setTenant(tenant); + } return new CancelTaskRequest(version, id, ProtoUtils.FromProto.taskIdParams(builder)); } case LIST_TASK_METHOD -> { io.a2a.grpc.ListTasksRequest.Builder builder = io.a2a.grpc.ListTasksRequest.newBuilder(); parseRequestBody(paramsNode, builder, id); + if (tenant != null && !tenant.isBlank() && (builder.getTenant() == null || builder.getTenant().isBlank())) { + builder.setTenant(tenant); + } return new ListTasksRequest(version, id, ProtoUtils.FromProto.listTasksParams(builder)); } case SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD -> { io.a2a.grpc.SetTaskPushNotificationConfigRequest.Builder builder = io.a2a.grpc.SetTaskPushNotificationConfigRequest.newBuilder(); parseRequestBody(paramsNode, builder, id); + if (tenant != null && !tenant.isBlank() && (builder.getTenant() == null || builder.getTenant().isBlank())) { + builder.setTenant(tenant); + } return new SetTaskPushNotificationConfigRequest(version, id, ProtoUtils.FromProto.setTaskPushNotificationConfig(builder)); } case GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD -> { io.a2a.grpc.GetTaskPushNotificationConfigRequest.Builder builder = io.a2a.grpc.GetTaskPushNotificationConfigRequest.newBuilder(); parseRequestBody(paramsNode, builder, id); + if (tenant != null && !tenant.isBlank() && (builder.getTenant() == null || builder.getTenant().isBlank())) { + builder.setTenant(tenant); + } return new GetTaskPushNotificationConfigRequest(version, id, ProtoUtils.FromProto.getTaskPushNotificationConfigParams(builder)); } case SEND_MESSAGE_METHOD -> { io.a2a.grpc.SendMessageRequest.Builder builder = io.a2a.grpc.SendMessageRequest.newBuilder(); parseRequestBody(paramsNode, builder, id); + if (tenant != null && !tenant.isBlank() && (builder.getTenant() == null || builder.getTenant().isBlank())) { + builder.setTenant(tenant); + } return new SendMessageRequest(version, id, ProtoUtils.FromProto.messageSendParams(builder)); } case LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD -> { io.a2a.grpc.ListTaskPushNotificationConfigRequest.Builder builder = io.a2a.grpc.ListTaskPushNotificationConfigRequest.newBuilder(); parseRequestBody(paramsNode, builder, id); + if (tenant != null && !tenant.isBlank() && (builder.getTenant() == null || builder.getTenant().isBlank())) { + builder.setTenant(tenant); + } return new ListTaskPushNotificationConfigRequest(version, id, ProtoUtils.FromProto.listTaskPushNotificationConfigParams(builder)); } case DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD -> { io.a2a.grpc.DeleteTaskPushNotificationConfigRequest.Builder builder = io.a2a.grpc.DeleteTaskPushNotificationConfigRequest.newBuilder(); parseRequestBody(paramsNode, builder, id); + if (tenant != null && !tenant.isBlank() && (builder.getTenant() == null || builder.getTenant().isBlank())) { + builder.setTenant(tenant); + } return new DeleteTaskPushNotificationConfigRequest(version, id, ProtoUtils.FromProto.deleteTaskPushNotificationConfigParams(builder)); } case GET_EXTENDED_AGENT_CARD_METHOD -> { @@ -250,11 +274,17 @@ private static A2ARequest parseMethodRequest(String version, Object id, Strin case SEND_STREAMING_MESSAGE_METHOD -> { io.a2a.grpc.SendMessageRequest.Builder builder = io.a2a.grpc.SendMessageRequest.newBuilder(); parseRequestBody(paramsNode, builder, id); + if (tenant != null && !tenant.isBlank() && (builder.getTenant() == null || builder.getTenant().isBlank())) { + builder.setTenant(tenant); + } return new SendStreamingMessageRequest(version, id, ProtoUtils.FromProto.messageSendParams(builder)); } case SUBSCRIBE_TO_TASK_METHOD -> { io.a2a.grpc.SubscribeToTaskRequest.Builder builder = io.a2a.grpc.SubscribeToTaskRequest.newBuilder(); parseRequestBody(paramsNode, builder, id); + if (tenant != null && !tenant.isBlank() && (builder.getTenant() == null || builder.getTenant().isBlank())) { + builder.setTenant(tenant); + } return new SubscribeToTaskRequest(version, id, ProtoUtils.FromProto.taskIdParams(builder)); } default -> diff --git a/spec-grpc/src/test/java/io/a2a/grpc/utils/JSONRPCUtilsTest.java b/spec-grpc/src/test/java/io/a2a/grpc/utils/JSONRPCUtilsTest.java index 61506c88e..380353dea 100644 --- a/spec-grpc/src/test/java/io/a2a/grpc/utils/JSONRPCUtilsTest.java +++ b/spec-grpc/src/test/java/io/a2a/grpc/utils/JSONRPCUtilsTest.java @@ -49,7 +49,7 @@ public void testParseSetTaskPushNotificationConfigRequest_ValidProtoFormat() thr } """; - A2ARequest request = JSONRPCUtils.parseRequestBody(validRequest); + A2ARequest request = JSONRPCUtils.parseRequestBody(validRequest, null); assertNotNull(request); assertInstanceOf(SetTaskPushNotificationConfigRequest.class, request); @@ -78,7 +78,7 @@ public void testParseGetTaskPushNotificationConfigRequest_ValidProtoFormat() thr } """; - A2ARequest request = JSONRPCUtils.parseRequestBody(validRequest); + A2ARequest request = JSONRPCUtils.parseRequestBody(validRequest, null); assertNotNull(request); assertInstanceOf(GetTaskPushNotificationConfigRequest.class, request); @@ -101,7 +101,7 @@ public void testParseMalformedJSON_ThrowsJsonSyntaxException() { """; // Missing closing braces JsonSyntaxException exception = assertThrows(JsonSyntaxException.class, () -> { - JSONRPCUtils.parseRequestBody(malformedRequest); + JSONRPCUtils.parseRequestBody(malformedRequest, null); }); assertEquals("java.io.EOFException: End of input at line 6 column 1 path $.params.parent", exception.getMessage()); } @@ -119,7 +119,7 @@ public void testParseInvalidParams_ThrowsInvalidParamsJsonMappingException() { InvalidParamsJsonMappingException exception = assertThrows( InvalidParamsJsonMappingException.class, - () -> JSONRPCUtils.parseRequestBody(invalidParamsRequest) + () -> JSONRPCUtils.parseRequestBody(invalidParamsRequest, null) ); assertEquals(3, exception.getId()); } @@ -139,7 +139,7 @@ public void testParseInvalidProtoStructure_ThrowsInvalidParamsJsonMappingExcepti InvalidParamsJsonMappingException exception = assertThrows( InvalidParamsJsonMappingException.class, - () -> JSONRPCUtils.parseRequestBody(invalidStructure) + () -> JSONRPCUtils.parseRequestBody(invalidStructure, null) ); assertEquals(4, exception.getId()); assertEquals(ERROR_MESSAGE.formatted("invalid_field in message a2a.v1.SetTaskPushNotificationConfigRequest"), exception.getMessage()); @@ -169,7 +169,7 @@ public void testParseMissingField_ThrowsInvalidParamsError() throws JsonMappingE }"""; InvalidParamsJsonMappingException exception = assertThrows( InvalidParamsJsonMappingException.class, - () -> JSONRPCUtils.parseRequestBody(missingRoleMessage) + () -> JSONRPCUtils.parseRequestBody(missingRoleMessage, null) ); assertEquals(18, exception.getId()); } @@ -199,7 +199,7 @@ public void testParseUnknownField_ThrowsJsonMappingException() throws JsonMappin }"""; JsonMappingException exception = assertThrows( JsonMappingException.class, - () -> JSONRPCUtils.parseRequestBody(unkownFieldMessage) + () -> JSONRPCUtils.parseRequestBody(unkownFieldMessage, null) ); assertEquals(ERROR_MESSAGE.formatted("unknown in message a2a.v1.Message"), exception.getMessage()); } diff --git a/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/context/JSONRPCContextKeys.java b/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/context/JSONRPCContextKeys.java index 015e3860a..fbed22192 100644 --- a/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/context/JSONRPCContextKeys.java +++ b/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/context/JSONRPCContextKeys.java @@ -18,6 +18,11 @@ public final class JSONRPCContextKeys { */ public static final String METHOD_NAME_KEY = "method"; + /** + * Context key for storing the tenant identifier extracted from the normalized path. + */ + public static final String TENANT_KEY = "tenant"; + private JSONRPCContextKeys() { // Utility class } diff --git a/transport/rest/src/main/java/io/a2a/transport/rest/context/RestContextKeys.java b/transport/rest/src/main/java/io/a2a/transport/rest/context/RestContextKeys.java index de35ca0cf..f822607ef 100644 --- a/transport/rest/src/main/java/io/a2a/transport/rest/context/RestContextKeys.java +++ b/transport/rest/src/main/java/io/a2a/transport/rest/context/RestContextKeys.java @@ -17,6 +17,10 @@ public final class RestContextKeys { * Context key for storing the method name being called. */ public static final String METHOD_NAME_KEY = "method"; + /** + * Context key for storing the tenant identifier extracted from the request path. + */ + public static final String TENANT_KEY = "tenant"; private RestContextKeys() { // Utility class