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 ;
17+ import org .a2aproject .sdk .server .multitenancy .TenantNotFoundException ;
1618import org .a2aproject .sdk .server .util .CdiUtils ;
1719import org .a2aproject .sdk .spec .AgentCard ;
18- import org .jspecify .annotations .Nullable ;
1920import org .a2aproject .sdk .spec .AgentInterface ;
2021import 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,22 +95,118 @@ 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+ * Convenience overload that uses the default {@link #validateTransportConfiguration} validator.
110+ *
111+ * @see #resolveWithFallback(Instance, Instance, AgentCardRouter, String, Set, Consumer)
112+ */
113+ public static AgentCard resolveWithFallback (Instance <AgentCard > publicCard ,
114+ @ Nullable Instance <AgentCard > extendedCard ,
115+ @ Nullable AgentCardRouter agentCardRouter ,
116+ @ Nullable String tenant ,
117+ Set <AgentCard > validatedCards ) {
118+ return resolveWithFallback (publicCard , extendedCard , agentCardRouter , tenant , validatedCards ,
119+ AgentCardValidator ::validateTransportConfiguration );
120+ }
121+
122+ /**
123+ * Resolves an agent card using one of two resolution strategies depending on whether a
124+ * tenant-scoped request is being made, and validates using the supplied validator.
125+ *
126+ * @param publicCard the CDI instance for the {@code @PublicAgentCard}
127+ * @param extendedCard the CDI instance for the {@code @ExtendedAgentCard}, may be {@code null}
128+ * @param agentCardRouter optional router for tenant-specific card resolution
129+ * @param tenant the tenant identifier, may be {@code null}
130+ * @param validatedCards set tracking which cards have already been validated
131+ * @param validator validation logic to apply on first access of each distinct card
132+ * @return the resolved agent card
133+ * @throws TenantNotFoundException if a non-blank tenant is specified, a router is available,
134+ * but neither a public nor extended card is registered for that tenant
135+ * @throws IllegalStateException if no card can be resolved (non-tenant-scoped path)
136+ * @see #resolveTenantScoped(AgentCardRouter, String, Set, Consumer)
137+ * @see #resolveDefaultWithRouterFallback(Instance, Instance, AgentCardRouter, String, Set, Consumer)
138+ */
139+ public static AgentCard resolveWithFallback (Instance <AgentCard > publicCard ,
140+ @ Nullable Instance <AgentCard > extendedCard ,
141+ @ Nullable AgentCardRouter agentCardRouter ,
142+ @ Nullable String tenant ,
143+ Set <AgentCard > validatedCards ,
144+ Consumer <AgentCard > validator ) {
145+ if (tenant != null && !tenant .isBlank () && agentCardRouter != null ) {
146+ return resolveTenantScoped (agentCardRouter , tenant , validatedCards , validator );
147+ }
148+ return resolveDefaultWithRouterFallback (publicCard , extendedCard , agentCardRouter , tenant ,
149+ validatedCards , validator );
150+ }
151+
152+ /**
153+ * Tenant-scoped resolution: only the {@link AgentCardRouter} is consulted. CDI default beans
154+ * are <em>not</em> used as fallbacks — doing so would let the request proceed against the
155+ * wrong tenant's card.
156+ * <ol>
157+ * <li>Tenant-specific public card via the router</li>
158+ * <li>Tenant-specific extended card via the router</li>
159+ * <li>{@link TenantNotFoundException} if neither is registered</li>
160+ * </ol>
161+ */
162+ private static AgentCard resolveTenantScoped (AgentCardRouter agentCardRouter , String tenant ,
163+ Set <AgentCard > validatedCards , Consumer <AgentCard > validator ) {
164+ AgentCard routerCard = agentCardRouter .resolvePublicCard (tenant );
165+ if (routerCard != null ) {
166+ return resolveAndValidateOnce (() -> routerCard , validatedCards , validator );
167+ }
168+ AgentCard routerExtCard = agentCardRouter .resolveExtendedCard (tenant );
169+ if (routerExtCard != null ) {
170+ return resolveAndValidateOnce (() -> routerExtCard , validatedCards , validator );
171+ }
172+ throw new TenantNotFoundException (tenant );
173+ }
174+
175+ /**
176+ * Non-tenant-scoped resolution with optional router fallback:
177+ * <ol>
178+ * <li>Unqualified {@code @PublicAgentCard} CDI bean</li>
179+ * <li>Router's public card (when no default bean exists)</li>
180+ * <li>Unqualified {@code @ExtendedAgentCard} CDI bean</li>
181+ * <li>Router's extended card (when no default bean exists)</li>
182+ * <li>{@link IllegalStateException} if nothing resolves</li>
183+ * </ol>
184+ */
185+ private static AgentCard resolveDefaultWithRouterFallback (Instance <AgentCard > publicCard ,
186+ @ Nullable Instance <AgentCard > extendedCard ,
187+ @ Nullable AgentCardRouter agentCardRouter ,
188+ @ Nullable String tenant ,
189+ Set <AgentCard > validatedCards ,
190+ Consumer <AgentCard > validator ) {
92191 AgentCard resolved = CdiUtils .resolveDefault (publicCard );
93192 if (resolved != null ) {
94- return resolveAndValidateOnce (() -> resolved , transportValidated ,
95- AgentCardValidator ::validateTransportConfiguration );
193+ return resolveAndValidateOnce (() -> resolved , validatedCards , validator );
194+ }
195+ if (agentCardRouter != null ) {
196+ AgentCard routerCard = agentCardRouter .resolvePublicCard (tenant );
197+ if (routerCard != null ) {
198+ return resolveAndValidateOnce (() -> routerCard , validatedCards , validator );
199+ }
96200 }
97201 AgentCard extResolved = CdiUtils .resolveDefault (extendedCard );
98202 if (extResolved != null ) {
99- return resolveAndValidateOnce (() -> extResolved , transportValidated ,
100- AgentCardValidator ::validateTransportConfiguration );
203+ return resolveAndValidateOnce (() -> extResolved , validatedCards , validator );
204+ }
205+ if (agentCardRouter != null ) {
206+ AgentCard routerExtCard = agentCardRouter .resolveExtendedCard (tenant );
207+ if (routerExtCard != null ) {
208+ return resolveAndValidateOnce (() -> routerExtCard , validatedCards , validator );
209+ }
101210 }
102211 throw new IllegalStateException (NO_AGENT_CARD_MESSAGE );
103212 }
0 commit comments