55import java .util .List ;
66import java .util .ServiceLoader ;
77import java .util .Set ;
8- import java .util .concurrent .atomic . AtomicBoolean ;
8+ import java .util .concurrent .ConcurrentHashMap ;
99import java .util .function .Consumer ;
1010import java .util .function .Supplier ;
1111import java .util .logging .Logger ;
1212import java .util .stream .Collectors ;
1313
1414import jakarta .enterprise .inject .Instance ;
1515
16+ import org .a2aproject .sdk .server .multitenancy .AgentCardRouter ;
1617import org .a2aproject .sdk .server .util .CdiUtils ;
1718import org .a2aproject .sdk .spec .AgentCard ;
18- import org .jspecify .annotations .Nullable ;
1919import org .a2aproject .sdk .spec .AgentInterface ;
2020import 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