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,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