-
Notifications
You must be signed in to change notification settings - Fork 169
feat(server)!: add multitenancy support with per-tenant routing #1084
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| --- | ||
| title: Multi-Tenancy | ||
| description: CDI-based multitenancy support for A2A Java SDK servers — per-tenant AgentExecutor and AgentCard routing with the @Tenant qualifier. | ||
| layout: page | ||
| --- | ||
|
|
||
| # Multi-Tenancy | ||
|
|
||
| Lets a single A2A server serve multiple tenants with different agent behavior — each tenant gets its own `AgentExecutor` and `AgentCard`, while requests without a recognized tenant automatically fall back to the default beans. | ||
|
|
||
| ## Module | ||
|
|
||
| | Artifact ID | Description | | ||
| |-------------|-------------| | ||
| | `a2a-java-extras-multitenancy` | `@Tenant` qualifier, `CdiAgentExecutorRouter`, `CdiAgentCardRouter` | | ||
|
|
||
| ### Add Dependency | ||
|
|
||
| ```xml | ||
| <dependency> | ||
| <groupId>org.a2aproject.sdk</groupId> | ||
| <artifactId>a2a-java-extras-multitenancy</artifactId> | ||
| </dependency> | ||
| ``` | ||
|
|
||
| > **Tip:** Use the [extras BOM](../boms) to manage the version without specifying it explicitly. | ||
|
|
||
| The module activates automatically via CDI when present on the classpath — no additional server configuration is required. | ||
|
|
||
| ## Architecture | ||
|
|
||
| <pre class="mermaid"> | ||
| flowchart TD | ||
| A["Incoming Request\ntenant field"] --> B["CdiAgentExecutorRouter"] | ||
| B -->|"@Tenant match found"| C["@Tenant AgentExecutor"] | ||
| B -->|"null / blank / unknown"| D["Default AgentExecutor"] | ||
| C --> E["RequestContext.getTenant()"] | ||
| D --> E | ||
|
|
||
| F["getExtendedAgentCard\npublic card URL"] --> G["CdiAgentCardRouter"] | ||
| G -->|"@Tenant match found"| H["@Tenant AgentCard"] | ||
| G -->|"null / blank / unknown"| I["Default AgentCard"] | ||
| </pre> | ||
|
|
||
| Two CDI routers are registered automatically: | ||
|
|
||
| - **`CdiAgentExecutorRouter`** — looks up an `AgentExecutor` bean qualified with `@Tenant(value)` for each request. Falls back to the unqualified default executor when no match is found. | ||
| - **`CdiAgentCardRouter`** — resolves the appropriate `AgentCard` for `getExtendedAgentCard` and tenant-specific public card endpoints. | ||
|
|
||
| ## Declaring Per-Tenant Beans | ||
|
|
||
| Use the `@Tenant` qualifier on CDI producer methods: | ||
|
|
||
| ### AgentExecutor | ||
|
|
||
| ```java | ||
| @ApplicationScoped | ||
| public class MyExecutors { | ||
|
|
||
| @Produces | ||
| public AgentExecutor defaultExecutor() { | ||
| return new DefaultAgentExecutor(); | ||
| } | ||
|
|
||
| @Produces | ||
| @Tenant("acme") | ||
| public AgentExecutor acmeExecutor() { | ||
| return new AcmeAgentExecutor(); | ||
| } | ||
|
|
||
| @Produces | ||
| @Tenant("beta") | ||
| public AgentExecutor betaExecutor() { | ||
| return new BetaAgentExecutor(); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### AgentCard | ||
|
|
||
| ```java | ||
| @ApplicationScoped | ||
| public class MyAgentCards { | ||
|
|
||
| // Default public card — used by /.well-known/agent-card.json | ||
| @Produces | ||
| @PublicAgentCard | ||
| public AgentCard publicCard() { | ||
| return AgentCard.builder().name("My Agent")...build(); | ||
| } | ||
|
|
||
| // Default extended card — used when no tenant match | ||
| @Produces | ||
| @ExtendedAgentCard | ||
| public AgentCard defaultExtendedCard() { | ||
| return AgentCard.builder().name("My Agent (extended)")...build(); | ||
| } | ||
|
|
||
| // Tenant-specific extended card | ||
| @Produces | ||
| @Tenant("acme") | ||
| @ExtendedAgentCard | ||
| public AgentCard acmeExtendedCard() { | ||
| return AgentCard.builder().name("Acme Agent")...build(); | ||
| } | ||
|
|
||
| // Tenant-specific public card — no @PublicAgentCard qualifier (see note below) | ||
| @Produces | ||
| @Tenant("acme") | ||
| public AgentCard acmePublicCard() { | ||
| return AgentCard.builder().name("Acme Agent")...build(); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| > **Note:** Tenant-specific public cards must **not** carry `@PublicAgentCard` — that qualifier is reserved for the single default public card. Adding it to a `@Tenant` bean causes CDI ambiguity. | ||
|
|
||
| ## Routing Rules | ||
|
|
||
| ### AgentExecutor routing | ||
|
|
||
| | Request tenant | Result | | ||
| |----------------|--------| | ||
| | `null` or blank | Default (unqualified) executor | | ||
| | Known tenant (e.g. `"acme"`) | `@Tenant("acme")` executor | | ||
| | Unknown tenant | Default (unqualified) executor | | ||
|
|
||
| ### AgentCard routing | ||
|
|
||
| | Request / URL | Card returned | | ||
| |---------------|---------------| | ||
| | `getExtendedAgentCard` with no tenant | Default `@ExtendedAgentCard` | | ||
| | `getExtendedAgentCard` with `tenant: "acme"` | `@Tenant("acme") @ExtendedAgentCard`, or default | | ||
| | `GET /.well-known/agent-card.json` | Default `@PublicAgentCard` | | ||
| | `GET /.well-known/acme/agent-card.json` | `@Tenant("acme")` card (no `@PublicAgentCard`), or default | | ||
|
|
||
| ## Accessing the Tenant in AgentExecutor | ||
|
|
||
| The resolved tenant is available in `RequestContext`: | ||
|
|
||
| ```java | ||
| @Override | ||
| public void execute(RequestContext context, AgentEmitter emitter) throws A2AError { | ||
| String tenant = context.getTenant(); // null for the default tenant | ||
| // ... | ||
| } | ||
| ``` | ||
|
|
||
| ## Tenant Source | ||
|
|
||
| The tenant is read from the `tenant` field in the request payload (e.g. `MessageSendParams.tenant()`, `CancelTaskParams.tenant()`). For the REST transport the tenant can also come from the URL path (e.g. `/\{tenant}/extendedAgentCard`); the payload value takes precedence when both are present. | ||
|
|
||
| Tenant identifiers are restricted to `a-zA-Z0-9_-.` characters — path segments containing `/` or `?` are rejected. | ||
|
|
||
| ## Limitations | ||
|
|
||
| - **TaskStore and QueueManager are shared** across all tenants — tasks are keyed by UUID, not partitioned per tenant. | ||
| - **Per-tenant TaskAuthorizationProvider** is not yet supported — a single provider applies to all tenants. | ||
|
|
||
| ## Without the Module | ||
|
|
||
| When `a2a-java-extras-multitenancy` is **not** on the classpath, the server behaves as a single-tenant deployment: the default `AgentExecutor` handles all requests, the default cards are returned, and the `tenant` field in request payloads is silently ignored. Existing single-tenant code requires no changes when the module is added. | ||
|
|
||
| ## See Also | ||
|
|
||
| - [Multi-Tenancy concept page](../../multi-tenancy) — conceptual overview and setup guide | ||
| - [Extras BOM](../boms) — version management for all extras modules |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| --- | ||
| title: Multi-Tenancy | ||
| description: Serve multiple tenants from a single A2A server — per-tenant AgentExecutor and AgentCard routing with CDI qualifiers. | ||
| layout: page | ||
| --- | ||
|
|
||
| # Multi-Tenancy | ||
|
|
||
| Multi-tenancy lets a single A2A server provide different agent behavior per tenant. Each tenant can have its own `AgentExecutor` (business logic) and `AgentCard` (capabilities, skills, metadata). Requests without a recognized tenant automatically fall back to the default beans. | ||
|
|
||
| This feature is provided by the `a2a-java-extras-multitenancy` extras module, which registers two CDI routers — `CdiAgentExecutorRouter` and `CdiAgentCardRouter` — that dispatch each request to the matching `@Tenant`-qualified bean. | ||
|
|
||
| For setup instructions, configuration reference, and code examples, see the **[Multi-Tenancy extras page](../extra/multi-tenancy)**. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.