Skip to content

Commit 80fc9f2

Browse files
committed
fix(multitenancy): resolve agent card via router when no default bean exists
When all @PublicAgentCard beans carry a @tenant qualifier, resolveAgentCard() threw IllegalStateException because CdiUtils.resolveDefault() skips tenant-qualified beans. Thread the tenant and AgentCardRouter through the resolution chain so the router is consulted before throwing. Fixes #1182 Signed-off-by: Emmanuel Hugonnet <ehugonne@redhat.com>
1 parent 7e73cab commit 80fc9f2

8 files changed

Lines changed: 584 additions & 150 deletions

File tree

‎server-common/src/main/java/org/a2aproject/sdk/server/AgentCardValidator.java‎

Lines changed: 104 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,21 @@
55
import java.util.List;
66
import java.util.ServiceLoader;
77
import java.util.Set;
8-
import java.util.concurrent.atomic.AtomicBoolean;
8+
import java.util.concurrent.ConcurrentHashMap;
99
import java.util.function.Consumer;
1010
import java.util.function.Supplier;
1111
import java.util.logging.Logger;
1212
import java.util.stream.Collectors;
1313

1414
import jakarta.enterprise.inject.Instance;
1515

16+
import org.a2aproject.sdk.server.multitenancy.AgentCardRouter;
17+
import org.a2aproject.sdk.server.multitenancy.TenantNotFoundException;
1618
import org.a2aproject.sdk.server.util.CdiUtils;
1719
import org.a2aproject.sdk.spec.AgentCard;
18-
import org.jspecify.annotations.Nullable;
1920
import org.a2aproject.sdk.spec.AgentInterface;
2021
import org.a2aproject.sdk.spec.TransportProtocol;
22+
import org.jspecify.annotations.Nullable;
2123

2224
/**
2325
* Validates AgentCard transport configuration against available transport endpoints.
@@ -35,41 +37,52 @@ public class AgentCardValidator {
3537
public static final String SKIP_GRPC_PROPERTY = "org.a2aproject.sdk.transport.grpc.skipValidation";
3638
public static final String SKIP_REST_PROPERTY = "org.a2aproject.sdk.transport.rest.skipValidation";
3739

40+
/**
41+
* Creates a new thread-safe set for tracking which {@link AgentCard} instances have already
42+
* been validated. Each distinct card is validated at most once; subsequent requests for the
43+
* same card skip validation.
44+
*
45+
* @return a concurrent set suitable for use as the {@code validatedCards} parameter
46+
*/
47+
public static Set<AgentCard> newValidatedCardsSet() {
48+
return ConcurrentHashMap.newKeySet();
49+
}
50+
3851
/**
3952
* Resolves an {@link AgentCard} from the given {@link Instance} and validates its transport
40-
* configuration exactly once using the default {@link #validateTransportConfiguration} check.
41-
* The {@code transportValidated} guard is reset on failure so validation can be retried on
42-
* the next call.
53+
* configuration once per distinct card using the default
54+
* {@link #validateTransportConfiguration} check. On failure the card is removed from the
55+
* set so validation can be retried on the next call.
4356
*
4457
* @param agentCardInstance the CDI instance holding the agent card
45-
* @param transportValidated atomic guard ensuring validation runs once
58+
* @param validatedCards set tracking which cards have already been validated
4659
* @return the resolved agent card
4760
*/
4861
public static AgentCard resolveAndValidateOnce(Instance<AgentCard> agentCardInstance,
49-
AtomicBoolean transportValidated) {
50-
return resolveAndValidateOnce(agentCardInstance::get, transportValidated,
62+
Set<AgentCard> validatedCards) {
63+
return resolveAndValidateOnce(agentCardInstance::get, validatedCards,
5164
AgentCardValidator::validateTransportConfiguration);
5265
}
5366

5467
/**
5568
* Obtains an {@link AgentCard} from the given supplier and applies the provided validator
56-
* exactly once. The {@code transportValidated} guard is reset on failure so validation can
57-
* be retried on the next call.
69+
* once per distinct card. On failure the card is removed from the set so validation can be
70+
* retried on the next call.
5871
*
5972
* @param agentCardSupplier supplier that produces the agent card
60-
* @param transportValidated atomic guard ensuring validation runs once
73+
* @param validatedCards set tracking which cards have already been validated
6174
* @param validator validation logic to apply on first access
6275
* @return the resolved agent card
6376
*/
6477
public static AgentCard resolveAndValidateOnce(Supplier<AgentCard> agentCardSupplier,
65-
AtomicBoolean transportValidated,
78+
Set<AgentCard> validatedCards,
6679
Consumer<AgentCard> validator) {
6780
AgentCard card = agentCardSupplier.get();
68-
if (transportValidated.compareAndSet(false, true)) {
81+
if (validatedCards.add(card)) {
6982
try {
7083
validator.accept(card);
7184
} catch (RuntimeException e) {
72-
transportValidated.set(false);
85+
validatedCards.remove(card);
7386
throw e;
7487
}
7588
}
@@ -82,23 +95,96 @@ public static AgentCard resolveAndValidateOnce(Supplier<AgentCard> agentCardSupp
8295
*
8396
* @param publicCard the CDI instance for the {@code @PublicAgentCard}
8497
* @param extendedCard the CDI instance for the {@code @ExtendedAgentCard}, may be {@code null}
85-
* @param transportValidated atomic guard ensuring validation runs once
98+
* @param validatedCards set tracking which cards have already been validated
8699
* @return the resolved agent card
87100
* @throws IllegalStateException if neither card is available
88101
*/
89102
public static AgentCard resolveWithFallback(Instance<AgentCard> publicCard,
90103
@Nullable Instance<AgentCard> extendedCard,
91-
AtomicBoolean transportValidated) {
104+
Set<AgentCard> validatedCards) {
105+
return resolveWithFallback(publicCard, extendedCard, null, null, validatedCards);
106+
}
107+
108+
/**
109+
* Resolves an agent card using one of two resolution strategies depending on whether a
110+
* tenant-scoped request is being made.
111+
*
112+
* <p><strong>Tenant-scoped</strong> (non-blank tenant with a router): only the
113+
* {@link AgentCardRouter} is consulted. CDI default beans are <em>not</em> used as
114+
* fallbacks — doing so would let the request proceed against the wrong tenant's card.
115+
* <ol>
116+
* <li>Tenant-specific public card via the router</li>
117+
* <li>Tenant-specific extended card via the router</li>
118+
* <li>{@link TenantNotFoundException} if neither is registered</li>
119+
* </ol>
120+
*
121+
* <p><strong>Non-tenant-scoped</strong> (no tenant, blank tenant, or no router):
122+
* <ol>
123+
* <li>Unqualified {@code @PublicAgentCard} CDI bean</li>
124+
* <li>Router's public card (when no default bean exists)</li>
125+
* <li>Unqualified {@code @ExtendedAgentCard} CDI bean</li>
126+
* <li>Router's extended card (when no default bean exists)</li>
127+
* <li>{@link IllegalStateException} if nothing resolves</li>
128+
* </ol>
129+
*
130+
* @param publicCard the CDI instance for the {@code @PublicAgentCard}
131+
* @param extendedCard the CDI instance for the {@code @ExtendedAgentCard}, may be {@code null}
132+
* @param agentCardRouter optional router for tenant-specific card resolution
133+
* @param tenant the tenant identifier, may be {@code null}
134+
* @param validatedCards set tracking which cards have already been validated
135+
* @return the resolved agent card
136+
* @throws TenantNotFoundException if a non-blank tenant is specified, a router is available,
137+
* but neither a public nor extended card is registered for that tenant
138+
* @throws IllegalStateException if no card can be resolved (non-tenant-scoped path)
139+
*/
140+
public static AgentCard resolveWithFallback(Instance<AgentCard> publicCard,
141+
@Nullable Instance<AgentCard> extendedCard,
142+
@Nullable AgentCardRouter agentCardRouter,
143+
@Nullable String tenant,
144+
Set<AgentCard> validatedCards) {
145+
// When a tenant is specified and a router is available, only the router
146+
// determines resolution. A null result means the tenant is unknown per
147+
// the AgentCardRouter contract — falling through to default beans would
148+
// let the request proceed against the wrong tenant's card.
149+
if (tenant != null && !tenant.isBlank() && agentCardRouter != null) {
150+
AgentCard routerCard = agentCardRouter.resolvePublicCard(tenant);
151+
if (routerCard != null) {
152+
return resolveAndValidateOnce(() -> routerCard, validatedCards,
153+
AgentCardValidator::validateTransportConfiguration);
154+
}
155+
AgentCard routerExtCard = agentCardRouter.resolveExtendedCard(tenant);
156+
if (routerExtCard != null) {
157+
return resolveAndValidateOnce(() -> routerExtCard, validatedCards,
158+
AgentCardValidator::validateTransportConfiguration);
159+
}
160+
throw new TenantNotFoundException(tenant);
161+
}
162+
// --- Public cards ---
92163
AgentCard resolved = CdiUtils.resolveDefault(publicCard);
93164
if (resolved != null) {
94-
return resolveAndValidateOnce(() -> resolved, transportValidated,
165+
return resolveAndValidateOnce(() -> resolved, validatedCards,
95166
AgentCardValidator::validateTransportConfiguration);
96167
}
168+
if (agentCardRouter != null) {
169+
AgentCard routerCard = agentCardRouter.resolvePublicCard(tenant);
170+
if (routerCard != null) {
171+
return resolveAndValidateOnce(() -> routerCard, validatedCards,
172+
AgentCardValidator::validateTransportConfiguration);
173+
}
174+
}
175+
// --- Extended cards ---
97176
AgentCard extResolved = CdiUtils.resolveDefault(extendedCard);
98177
if (extResolved != null) {
99-
return resolveAndValidateOnce(() -> extResolved, transportValidated,
178+
return resolveAndValidateOnce(() -> extResolved, validatedCards,
100179
AgentCardValidator::validateTransportConfiguration);
101180
}
181+
if (agentCardRouter != null) {
182+
AgentCard routerExtCard = agentCardRouter.resolveExtendedCard(tenant);
183+
if (routerExtCard != null) {
184+
return resolveAndValidateOnce(() -> routerExtCard, validatedCards,
185+
AgentCardValidator::validateTransportConfiguration);
186+
}
187+
}
102188
throw new IllegalStateException(NO_AGENT_CARD_MESSAGE);
103189
}
104190

0 commit comments

Comments
 (0)