Skip to content

Commit daf85cb

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 daf85cb

5 files changed

Lines changed: 414 additions & 90 deletions

File tree

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

Lines changed: 90 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,20 @@
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;
1617
import org.a2aproject.sdk.server.util.CdiUtils;
1718
import org.a2aproject.sdk.spec.AgentCard;
18-
import org.jspecify.annotations.Nullable;
1919
import org.a2aproject.sdk.spec.AgentInterface;
2020
import org.a2aproject.sdk.spec.TransportProtocol;
21+
import org.jspecify.annotations.Nullable;
2122

2223
/**
2324
* Validates AgentCard transport configuration against available transport endpoints.
@@ -35,41 +36,52 @@ public class AgentCardValidator {
3536
public static final String SKIP_GRPC_PROPERTY = "org.a2aproject.sdk.transport.grpc.skipValidation";
3637
public static final String SKIP_REST_PROPERTY = "org.a2aproject.sdk.transport.rest.skipValidation";
3738

39+
/**
40+
* Creates a new thread-safe set for tracking which {@link AgentCard} instances have already
41+
* been validated. Each distinct card is validated at most once; subsequent requests for the
42+
* same card skip validation.
43+
*
44+
* @return a concurrent set suitable for use as the {@code validatedCards} parameter
45+
*/
46+
public static Set<AgentCard> newValidatedCardsSet() {
47+
return ConcurrentHashMap.newKeySet();
48+
}
49+
3850
/**
3951
* 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.
52+
* configuration once per distinct card using the default
53+
* {@link #validateTransportConfiguration} check. On failure the card is removed from the
54+
* set so validation can be retried on the next call.
4355
*
4456
* @param agentCardInstance the CDI instance holding the agent card
45-
* @param transportValidated atomic guard ensuring validation runs once
57+
* @param validatedCards set tracking which cards have already been validated
4658
* @return the resolved agent card
4759
*/
4860
public static AgentCard resolveAndValidateOnce(Instance<AgentCard> agentCardInstance,
49-
AtomicBoolean transportValidated) {
50-
return resolveAndValidateOnce(agentCardInstance::get, transportValidated,
61+
Set<AgentCard> validatedCards) {
62+
return resolveAndValidateOnce(agentCardInstance::get, validatedCards,
5163
AgentCardValidator::validateTransportConfiguration);
5264
}
5365

5466
/**
5567
* 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.
68+
* once per distinct card. On failure the card is removed from the set so validation can be
69+
* retried on the next call.
5870
*
5971
* @param agentCardSupplier supplier that produces the agent card
60-
* @param transportValidated atomic guard ensuring validation runs once
72+
* @param validatedCards set tracking which cards have already been validated
6173
* @param validator validation logic to apply on first access
6274
* @return the resolved agent card
6375
*/
6476
public static AgentCard resolveAndValidateOnce(Supplier<AgentCard> agentCardSupplier,
65-
AtomicBoolean transportValidated,
77+
Set<AgentCard> validatedCards,
6678
Consumer<AgentCard> validator) {
6779
AgentCard card = agentCardSupplier.get();
68-
if (transportValidated.compareAndSet(false, true)) {
80+
if (validatedCards.add(card)) {
6981
try {
7082
validator.accept(card);
7183
} catch (RuntimeException e) {
72-
transportValidated.set(false);
84+
validatedCards.remove(card);
7385
throw e;
7486
}
7587
}
@@ -82,23 +94,83 @@ public static AgentCard resolveAndValidateOnce(Supplier<AgentCard> agentCardSupp
8294
*
8395
* @param publicCard the CDI instance for the {@code @PublicAgentCard}
8496
* @param extendedCard the CDI instance for the {@code @ExtendedAgentCard}, may be {@code null}
85-
* @param transportValidated atomic guard ensuring validation runs once
97+
* @param validatedCards set tracking which cards have already been validated
8698
* @return the resolved agent card
8799
* @throws IllegalStateException if neither card is available
88100
*/
89101
public static AgentCard resolveWithFallback(Instance<AgentCard> publicCard,
90102
@Nullable Instance<AgentCard> extendedCard,
91-
AtomicBoolean transportValidated) {
103+
Set<AgentCard> validatedCards) {
104+
return resolveWithFallback(publicCard, extendedCard, null, null, validatedCards);
105+
}
106+
107+
/**
108+
* Resolves an agent card using the following priority order. When a non-blank tenant is
109+
* specified, the router's tenant-specific card is preferred over default CDI beans so that
110+
* capability, version, and extension checks match the card served for that tenant:
111+
* <ol>
112+
* <li>Tenant-specific public card via the {@link AgentCardRouter} (when tenant is specified)</li>
113+
* <li>Unqualified {@code @PublicAgentCard} bean (default)</li>
114+
* <li>Router's public card (when no tenant specified and no default bean)</li>
115+
* <li>Tenant-specific extended card via the {@link AgentCardRouter} (when tenant is specified)</li>
116+
* <li>Unqualified {@code @ExtendedAgentCard} bean (default)</li>
117+
* <li>Router's extended card (when no tenant specified and no default bean)</li>
118+
* </ol>
119+
*
120+
* @param publicCard the CDI instance for the {@code @PublicAgentCard}
121+
* @param extendedCard the CDI instance for the {@code @ExtendedAgentCard}, may be {@code null}
122+
* @param agentCardRouter optional router for tenant-specific card resolution
123+
* @param tenant the tenant identifier, may be {@code null}
124+
* @param validatedCards set tracking which cards have already been validated
125+
* @return the resolved agent card
126+
* @throws IllegalStateException if no card can be resolved
127+
*/
128+
public static AgentCard resolveWithFallback(Instance<AgentCard> publicCard,
129+
@Nullable Instance<AgentCard> extendedCard,
130+
@Nullable AgentCardRouter agentCardRouter,
131+
@Nullable String tenant,
132+
Set<AgentCard> validatedCards) {
133+
boolean hasTenant = tenant != null && !tenant.isBlank();
134+
// --- Public cards ---
135+
if (hasTenant && agentCardRouter != null) {
136+
AgentCard routerCard = agentCardRouter.resolvePublicCard(tenant);
137+
if (routerCard != null) {
138+
return resolveAndValidateOnce(() -> routerCard, validatedCards,
139+
AgentCardValidator::validateTransportConfiguration);
140+
}
141+
}
92142
AgentCard resolved = CdiUtils.resolveDefault(publicCard);
93143
if (resolved != null) {
94-
return resolveAndValidateOnce(() -> resolved, transportValidated,
144+
return resolveAndValidateOnce(() -> resolved, validatedCards,
95145
AgentCardValidator::validateTransportConfiguration);
96146
}
147+
if (!hasTenant && agentCardRouter != null) {
148+
AgentCard routerCard = agentCardRouter.resolvePublicCard(tenant);
149+
if (routerCard != null) {
150+
return resolveAndValidateOnce(() -> routerCard, validatedCards,
151+
AgentCardValidator::validateTransportConfiguration);
152+
}
153+
}
154+
// --- Extended cards ---
155+
if (hasTenant && agentCardRouter != null) {
156+
AgentCard routerExtCard = agentCardRouter.resolveExtendedCard(tenant);
157+
if (routerExtCard != null) {
158+
return resolveAndValidateOnce(() -> routerExtCard, validatedCards,
159+
AgentCardValidator::validateTransportConfiguration);
160+
}
161+
}
97162
AgentCard extResolved = CdiUtils.resolveDefault(extendedCard);
98163
if (extResolved != null) {
99-
return resolveAndValidateOnce(() -> extResolved, transportValidated,
164+
return resolveAndValidateOnce(() -> extResolved, validatedCards,
100165
AgentCardValidator::validateTransportConfiguration);
101166
}
167+
if (!hasTenant && agentCardRouter != null) {
168+
AgentCard routerExtCard = agentCardRouter.resolveExtendedCard(tenant);
169+
if (routerExtCard != null) {
170+
return resolveAndValidateOnce(() -> routerExtCard, validatedCards,
171+
AgentCardValidator::validateTransportConfiguration);
172+
}
173+
}
102174
throw new IllegalStateException(NO_AGENT_CARD_MESSAGE);
103175
}
104176

0 commit comments

Comments
 (0)