-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Description
Is your feature request related to a problem? Please describe.
Currently, for HTTP+JSON and JSONRPC transports, multi-tenant servers are possible by attaching the A2A api to a nested URL, like www.example.com/agent1/user2/. This allows for natural multi-tenancy and multi-single tenant support. This is not possible in gRPC, however.
Describe the solution you'd like
Here I propose a few tweaks to the gRPC definitions to support a true multi-tenant deployment.
The concept is to introduce an equivalent but slightly different service, ScopedA2AService. This would have the following definition
service ScopedA2AService {
// Send a message to the agent. This is a blocking call that will return the
// task once it is completed, or a LRO if requested.
rpc SendMessage(SendMessageRequest) returns (SendMessageResponse) {
option (google.api.http) = {
post: "/{scope=*}/v1/message:send"
body: "*"
};
}
// SendStreamingMessage is a streaming call that will return a stream of
// task update events until the Task is in an interrupted or terminal state.
rpc SendStreamingMessage(SendMessageRequest) returns (stream StreamResponse) {
option (google.api.http) = {
post: "/{scope=*}/v1/message:stream"
body: "*"
};
option (google.api.method_signature) = "scope";
}
// Get the current state of a task from the agent.
rpc GetTask(GetTaskRequest) returns (Task) {
option (google.api.http) = {
get: "/{scope=*}/v1/{name=tasks/*}"
};
option (google.api.method_signature) = "scope,name";
}
// Cancel a task from the agent. If supported one should expect no
// more task updates for the task.
rpc CancelTask(CancelTaskRequest) returns (Task) {
option (google.api.http) = {
post: "/{scope=*}/v1/{name=tasks/*}:cancel"
body: "*"
};
option (google.api.method_signature) = "scope,name";
}
// TaskSubscription is a streaming call that will return a stream of task
// update events. This attaches the stream to an existing in process task.
// If the task is complete the stream will return the completed task (like
// GetTask) and close the stream.
rpc TaskSubscription(TaskSubscriptionRequest)
returns (stream StreamResponse) {
option (google.api.http) = {
get: "/{scope=*}/v1/{name=tasks/*}:subscribe"
};
option (google.api.method_signature) = "scope,name";
}
// Set a push notification config for a task.
rpc CreateTaskPushNotificationConfig(CreateTaskPushNotificationConfigRequest)
returns (TaskPushNotificationConfig) {
option (google.api.http) = {
post: "/{scope=*}/v1/{parent=tasks/*/pushNotificationConfigs}"
body: "config"
};
option (google.api.method_signature) = "scope,parent,config";
}
// Get a push notification config for a task.
rpc GetTaskPushNotificationConfig(GetTaskPushNotificationConfigRequest)
returns (TaskPushNotificationConfig) {
option (google.api.http) = {
get: "/{scope=*}/v1/{name=tasks/*/pushNotificationConfigs/*}"
};
option (google.api.method_signature) = "scope,name";
}
// Get a list of push notifications configured for a task.
rpc ListTaskPushNotificationConfig(ListTaskPushNotificationConfigRequest)
returns (ListTaskPushNotificationConfigResponse) {
option (google.api.http) = {
get: "/{scope=*}/v1/{parent=tasks/*}/pushNotificationConfigs"
};
option (google.api.method_signature) = "scope,parent";
}
// GetAgentCard returns the agent card for the agent.
rpc GetAgentCard(GetAgentCardRequest) returns (AgentCard) {
option (google.api.http) = {
get: "/{scope=*}/v1/card"
};
}
// Delete a push notification config for a task.
rpc DeleteTaskPushNotificationConfig(DeleteTaskPushNotificationConfigRequest)
returns (google.protobuf.Empty) {
option (google.api.http) = {
delete: "/{scope=*}/v1/{name=tasks/*/pushNotificationConfigs/*}"
};
option (google.api.method_signature) = "scope,name";
}
}
Simultaneously all the *Request objects have a string scope field added.
This way the same data model is used for both service contracts, and everything is identical except the mapping used (and the requirement of) the scope variable being set for the ScopedA2AService. Addition of this field will have no change to the wire payloads of the existing single tenant solution, and add minimal memory overhead to the runtime of a single empty string variable.
Finally, a new field map<string, TransportOptions> transport_options needs to be added to the Agent Card, where the TransportOptions message contains the information about any transport specific information. For the gRPC transport, this would mean inclusion of a) whether the service is A2AService or ScopedA2AService, and for the scoped A2A service case, the scope value associated to a specific agent.
This would allow deployment of multi-tenant solutions, for example, allow customer scoping of a single agent, and support multi-signal tenant cases where the API can act as a proxy to a specific A2A agent.
Describe alternatives you've considered
The alternative is to couple the ScopedA2AService with the existing A2AService definition. This has two main drawbacks:
- The developer would be responsible for rejecting non-scoped requests. This is especially important for cases where transcoding is supported and the HTTP+JSON and gRPC interfaces are provided. In this case, the unscoped URLs would be valid urls that would route to the agent. Such a validation can be automatically supported in the SDKs for the scoped service - this isn't possible with a merged service
- The scope field will be ignored/dropped for the non scoped service. Making this explicit will reduce the risk of misuse of the field
Additional context
Please let me know your thoughts. I believe this to be the most minimally invasive, backward compatible solution to bring parity in the multi-tenant support for gRPC compared to already supported approaches in HTTP+JSON and JSONRPC. This does not, however, address the discovery of multiple agent cards on a single host - that is a different problem
Code of Conduct
- I agree to follow this project's Code of Conduct