Skip to content

Commit 93fa8ca

Browse files
committed
feat: harden spec immutability
1 parent 8a101ca commit 93fa8ca

14 files changed

Lines changed: 82 additions & 46 deletions

spec/src/main/java/org/a2aproject/sdk/spec/AgentCapabilities.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package org.a2aproject.sdk.spec;
22

3-
import java.util.List;
43
import org.jspecify.annotations.Nullable;
54

5+
import java.util.List;
6+
67
/**
78
* Defines optional capabilities supported by an agent in the A2A Protocol.
89
* <p>
@@ -35,6 +36,10 @@ public record AgentCapabilities(boolean streaming,
3536
boolean extendedAgentCard,
3637
@Nullable List<AgentExtension> extensions) {
3738

39+
public AgentCapabilities {
40+
extensions = extensions == null ? null : List.copyOf(extensions);
41+
}
42+
3843
/**
3944
* Create a new Builder
4045
*
@@ -121,7 +126,7 @@ public Builder extendedAgentCard(boolean extendedAgentCard) {
121126
* @see AgentExtension
122127
*/
123128
public Builder extensions(List<AgentExtension> extensions) {
124-
this.extensions = extensions;
129+
this.extensions = List.copyOf(extensions);
125130
return this;
126131
}
127132

spec/src/main/java/org/a2aproject/sdk/spec/AgentCard.java

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
package org.a2aproject.sdk.spec;
22

3-
import java.util.ArrayList;
4-
import java.util.Collections;
5-
import java.util.List;
6-
import java.util.Map;
7-
83
import org.a2aproject.sdk.util.Assert;
94
import org.jspecify.annotations.Nullable;
105

6+
import java.util.List;
7+
import java.util.Map;
8+
119
/**
1210
* The AgentCard is a self-describing manifest for an agent in the A2A Protocol.
1311
* <p>
@@ -89,6 +87,14 @@ public record AgentCard(
8987
Assert.checkNotNullParam("skills", skills);
9088
Assert.checkNotNullParam("supportedInterfaces", supportedInterfaces);
9189
Assert.checkNotNullParam("version", version);
90+
defaultInputModes = List.copyOf(defaultInputModes);
91+
defaultOutputModes = List.copyOf(defaultOutputModes);
92+
skills = List.copyOf(skills);
93+
securitySchemes = securitySchemes == null ? null : Map.copyOf(securitySchemes);
94+
securityRequirements = securityRequirements == null ? null : List.copyOf(securityRequirements);
95+
supportedInterfaces = List.copyOf(supportedInterfaces);
96+
signatures = signatures == null ? null : List.copyOf(signatures);
97+
additionalInterfaces = additionalInterfaces == null ? null : List.copyOf(additionalInterfaces);
9298
}
9399

94100
/**
@@ -184,17 +190,17 @@ private Builder(AgentCard card) {
184190
this.version = card.version();
185191
this.documentationUrl = card.documentationUrl();
186192
this.capabilities = card.capabilities();
187-
this.defaultInputModes = card.defaultInputModes() != null ? new ArrayList<>(card.defaultInputModes()) : Collections.emptyList();
188-
this.defaultOutputModes = card.defaultOutputModes() != null ? new ArrayList<>(card.defaultOutputModes()) : Collections.emptyList();
189-
this.skills = card.skills() != null ? new ArrayList<>(card.skills()) : Collections.emptyList();
190-
this.securitySchemes = card.securitySchemes() != null ? Map.copyOf(card.securitySchemes()) : Collections.emptyMap();
191-
this.securityRequirements = card.securityRequirements() != null ? new ArrayList<>(card.securityRequirements()) : Collections.emptyList();
193+
this.defaultInputModes = List.copyOf(card.defaultInputModes());
194+
this.defaultOutputModes = List.copyOf(card.defaultOutputModes());
195+
this.skills = List.copyOf(card.skills());
196+
this.securitySchemes = card.securitySchemes() != null ? Map.copyOf(card.securitySchemes()) : null;
197+
this.securityRequirements = card.securityRequirements() != null ? List.copyOf(card.securityRequirements()) : null;
192198
this.iconUrl = card.iconUrl();
193-
this.supportedInterfaces = card.supportedInterfaces() != null ? new ArrayList<>(card.supportedInterfaces()) : Collections.emptyList();
194-
this.signatures = card.signatures() != null ? new ArrayList<>(card.signatures()) : null;
199+
this.supportedInterfaces = List.copyOf(card.supportedInterfaces());
200+
this.signatures = card.signatures() != null ? List.copyOf(card.signatures()) : null;
195201
this.url = card.url();
196202
this.preferredTransport= card.preferredTransport();
197-
this.additionalInterfaces = card.additionalInterfaces() != null ? new ArrayList<>(card.additionalInterfaces()) : null;
203+
this.additionalInterfaces = card.additionalInterfaces() != null ? List.copyOf(card.additionalInterfaces()) : null;
198204
}
199205

200206
/**
@@ -276,7 +282,7 @@ public Builder capabilities(AgentCapabilities capabilities) {
276282
* @return this builder for method chaining
277283
*/
278284
public Builder defaultInputModes(List<String> defaultInputModes) {
279-
this.defaultInputModes = defaultInputModes;
285+
this.defaultInputModes = List.copyOf(defaultInputModes);
280286
return this;
281287
}
282288

@@ -289,7 +295,7 @@ public Builder defaultInputModes(List<String> defaultInputModes) {
289295
* @return this builder for method chaining
290296
*/
291297
public Builder defaultOutputModes(List<String> defaultOutputModes) {
292-
this.defaultOutputModes = defaultOutputModes;
298+
this.defaultOutputModes = List.copyOf(defaultOutputModes);
293299
return this;
294300
}
295301

@@ -304,7 +310,7 @@ public Builder defaultOutputModes(List<String> defaultOutputModes) {
304310
* @see AgentSkill
305311
*/
306312
public Builder skills(List<AgentSkill> skills) {
307-
this.skills = skills;
313+
this.skills = List.copyOf(skills);
308314
return this;
309315
}
310316

@@ -319,7 +325,7 @@ public Builder skills(List<AgentSkill> skills) {
319325
* @see SecurityScheme
320326
*/
321327
public Builder securitySchemes(Map<String, SecurityScheme> securitySchemes) {
322-
this.securitySchemes = securitySchemes;
328+
this.securitySchemes = Map.copyOf(securitySchemes);
323329
return this;
324330
}
325331

@@ -331,7 +337,7 @@ public Builder securitySchemes(Map<String, SecurityScheme> securitySchemes) {
331337
* @see SecurityRequirement
332338
*/
333339
public Builder securityRequirements(List<SecurityRequirement> securityRequirements) {
334-
this.securityRequirements = securityRequirements;
340+
this.securityRequirements = List.copyOf(securityRequirements);
335341
return this;
336342
}
337343

@@ -366,7 +372,7 @@ public Builder iconUrl(String iconUrl) {
366372
* @see AgentInterface
367373
*/
368374
public Builder supportedInterfaces(List<AgentInterface> supportedInterfaces) {
369-
this.supportedInterfaces = supportedInterfaces;
375+
this.supportedInterfaces = List.copyOf(supportedInterfaces);
370376
return this;
371377
}
372378

@@ -381,7 +387,7 @@ public Builder supportedInterfaces(List<AgentInterface> supportedInterfaces) {
381387
* @see AgentCardSignature
382388
*/
383389
public Builder signatures(List<AgentCardSignature> signatures) {
384-
this.signatures = signatures;
390+
this.signatures = List.copyOf(signatures);
385391
return this;
386392
}
387393

@@ -396,7 +402,7 @@ public Builder url(String url) {
396402
}
397403

398404
public Builder additionalInterfaces(List<Legacy_0_3_AgentInterface> additionalInterfaces) {
399-
this.additionalInterfaces = additionalInterfaces;
405+
this.additionalInterfaces = List.copyOf(additionalInterfaces);
400406
return this;
401407
}
402408

spec/src/main/java/org/a2aproject/sdk/spec/AgentCardSignature.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package org.a2aproject.sdk.spec;
22

3-
import java.util.Map;
4-
53
import com.google.gson.annotations.SerializedName;
64
import org.a2aproject.sdk.util.Assert;
75
import org.jspecify.annotations.Nullable;
86

7+
import java.util.Map;
8+
99
/**
1010
* Represents a digital signature for an {@link AgentCard} using JSON Web Signature (JWS) format.
1111
* <p>
@@ -46,6 +46,7 @@ public record AgentCardSignature(@Nullable Map<String, Object> header, @Serializ
4646
public AgentCardSignature {
4747
Assert.checkNotNullParam("protectedHeader", protectedHeader);
4848
Assert.checkNotNullParam("signature", signature);
49+
header = header == null ? null : Map.copyOf(header);
4950
}
5051

5152
/**
@@ -87,7 +88,7 @@ private Builder() {
8788
* @return this builder for method chaining
8889
*/
8990
public Builder header(Map<String, Object> header) {
90-
this.header = header;
91+
this.header = Map.copyOf(header);
9192
return this;
9293
}
9394

spec/src/main/java/org/a2aproject/sdk/spec/AgentExtension.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public record AgentExtension (@Nullable String description, @Nullable Map<String
3737
*/
3838
public AgentExtension {
3939
Assert.checkNotNullParam("uri", uri);
40+
params = params == null ? null : Map.copyOf(params);
4041
}
4142

4243
/**
@@ -91,7 +92,7 @@ public Builder description(String description) {
9192
* @return this builder for method chaining
9293
*/
9394
public Builder params(Map<String, Object> params) {
94-
this.params = params;
95+
this.params = Map.copyOf(params);
9596
return this;
9697
}
9798

spec/src/main/java/org/a2aproject/sdk/spec/AgentSkill.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ public record AgentSkill(String id, String name, String description, List<String
6161
Assert.checkNotNullParam("name", name);
6262
Assert.checkNotNullParam("description", description);
6363
Assert.checkNotNullParam("tags", tags);
64+
tags = List.copyOf(tags);
65+
examples = examples == null ? null : List.copyOf(examples);
66+
inputModes = inputModes == null ? null : List.copyOf(inputModes);
67+
outputModes = outputModes == null ? null : List.copyOf(outputModes);
68+
securityRequirements = securityRequirements == null ? null : List.copyOf(securityRequirements);
6469
}
6570

6671
/**
@@ -163,7 +168,7 @@ public Builder description(String description) {
163168
* @return this builder for method chaining
164169
*/
165170
public Builder tags(List<String> tags) {
166-
this.tags = tags;
171+
this.tags = List.copyOf(tags);
167172
return this;
168173
}
169174

@@ -177,7 +182,7 @@ public Builder tags(List<String> tags) {
177182
* @return this builder for method chaining
178183
*/
179184
public Builder examples(List<String> examples) {
180-
this.examples = examples;
185+
this.examples = List.copyOf(examples);
181186
return this;
182187
}
183188

@@ -191,7 +196,7 @@ public Builder examples(List<String> examples) {
191196
* @return this builder for method chaining
192197
*/
193198
public Builder inputModes(List<String> inputModes) {
194-
this.inputModes = inputModes;
199+
this.inputModes = List.copyOf(inputModes);
195200
return this;
196201
}
197202

@@ -205,7 +210,7 @@ public Builder inputModes(List<String> inputModes) {
205210
* @return this builder for method chaining
206211
*/
207212
public Builder outputModes(List<String> outputModes) {
208-
this.outputModes = outputModes;
213+
this.outputModes = List.copyOf(outputModes);
209214
return this;
210215
}
211216

@@ -222,7 +227,7 @@ public Builder outputModes(List<String> outputModes) {
222227
* @see SecurityRequirement
223228
*/
224229
public Builder securityRequirements(List<SecurityRequirement> securityRequirements) {
225-
this.securityRequirements = securityRequirements;
230+
this.securityRequirements = List.copyOf(securityRequirements);
226231
return this;
227232
}
228233

spec/src/main/java/org/a2aproject/sdk/spec/Artifact.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ public record Artifact(String artifactId, @Nullable String name, @Nullable Strin
5050
if (parts.isEmpty()) {
5151
throw new IllegalArgumentException("Parts cannot be empty");
5252
}
53+
parts = List.copyOf(parts);
54+
metadata = metadata == null ? null : Map.copyOf(metadata);
55+
extensions = extensions == null ? null : List.copyOf(extensions);
5356
}
5457

5558
/**
@@ -113,7 +116,7 @@ private Builder(Artifact existingArtifact) {
113116
artifactId = existingArtifact.artifactId;
114117
name = existingArtifact.name;
115118
description = existingArtifact.description;
116-
parts = existingArtifact.parts;
119+
parts = List.copyOf(existingArtifact.parts);
117120
metadata = existingArtifact.metadata;
118121
extensions = existingArtifact.extensions;
119122
}
@@ -158,7 +161,7 @@ public Builder description(@Nullable String description) {
158161
* @return this builder for method chaining
159162
*/
160163
public Builder parts(List<Part<?>> parts) {
161-
this.parts = parts;
164+
this.parts = List.copyOf(parts);
162165
return this;
163166
}
164167

@@ -180,7 +183,7 @@ public Builder parts(Part<?>... parts) {
180183
* @return this builder for method chaining
181184
*/
182185
public Builder metadata(@Nullable Map<String, Object> metadata) {
183-
this.metadata = metadata;
186+
this.metadata = metadata == null ? null : Map.copyOf(metadata);
184187
return this;
185188
}
186189

spec/src/main/java/org/a2aproject/sdk/spec/AuthorizationCodeOAuthFlow.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,6 @@ public record AuthorizationCodeOAuthFlow(String authorizationUrl, String refresh
4242
Assert.checkNotNullParam("authorizationUrl", authorizationUrl);
4343
Assert.checkNotNullParam("scopes", scopes);
4444
Assert.checkNotNullParam("tokenUrl", tokenUrl);
45+
scopes = Map.copyOf(scopes);
4546
}
4647
}

spec/src/main/java/org/a2aproject/sdk/spec/CancelTaskParams.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package org.a2aproject.sdk.spec;
22

3-
import org.a2aproject.sdk.util.Assert;
43
import java.util.Collections;
54
import java.util.Map;
5+
6+
import org.a2aproject.sdk.util.Assert;
67
import org.jspecify.annotations.Nullable;
78

89
/**
@@ -13,20 +14,23 @@
1314
*
1415
* @param id the unique task identifier (required)
1516
* @param tenant optional tenant, provided as a path parameter
16-
* @param metadata optional arbitrary key-value metadata (e.g. cancellation reason)
17+
* @param metadata arbitrary key-value metadata (e.g. cancellation reason)
1718
* @see <a href="https://a2a-protocol.org/latest/">A2A Protocol Specification</a>
1819
*/
1920
public record CancelTaskParams(String id, @Nullable String tenant, Map<String, Object> metadata) {
2021

2122
/**
22-
* Compact constructor for validation.
23-
* Validates that required parameters are not null.
23+
* Canonical constructor for validation and normalization.
2424
*
2525
* @param id the task identifier
2626
* @param tenant the tenant identifier
27+
* @param metadata arbitrary request metadata
2728
*/
28-
public CancelTaskParams {
29+
public CancelTaskParams(String id, @Nullable String tenant, @Nullable Map<String, Object> metadata) {
2930
Assert.checkNotNullParam("id", id);
31+
this.id = id;
32+
this.tenant = tenant;
33+
this.metadata = metadata == null ? Map.of() : Map.copyOf(metadata);
3034
}
3135

3236
/**

spec/src/main/java/org/a2aproject/sdk/spec/ClientCredentialsOAuthFlow.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public record ClientCredentialsOAuthFlow(String refreshUrl, Map<String, String>
3838
public ClientCredentialsOAuthFlow {
3939
Assert.checkNotNullParam("scopes", scopes);
4040
Assert.checkNotNullParam("tokenUrl", tokenUrl);
41+
scopes = Map.copyOf(scopes);
4142
}
4243

4344
}

spec/src/main/java/org/a2aproject/sdk/spec/MessageSendConfiguration.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
* @see <a href="https://a2a-protocol.org/latest/">A2A Protocol Specification</a>
2424
*/
2525
public record MessageSendConfiguration(@Nullable List<String> acceptedOutputModes, @Nullable Integer historyLength,
26-
@Nullable TaskPushNotificationConfig taskPushNotificationConfig, Boolean returnImmediately) {
26+
@Nullable TaskPushNotificationConfig taskPushNotificationConfig, @Nullable Boolean returnImmediately) {
2727

2828
/**
2929
* Compact constructor for validation.
@@ -39,6 +39,8 @@ public record MessageSendConfiguration(@Nullable List<String> acceptedOutputMode
3939
if (historyLength != null && historyLength < 0) {
4040
throw new IllegalArgumentException("Invalid history length");
4141
}
42+
acceptedOutputModes = acceptedOutputModes == null ? null : List.copyOf(acceptedOutputModes);
43+
returnImmediately = returnImmediately != null && returnImmediately;
4244
}
4345

4446
/**
@@ -75,7 +77,7 @@ private Builder() {
7577
* @return this builder
7678
*/
7779
public Builder acceptedOutputModes(List<String> acceptedOutputModes) {
78-
this.acceptedOutputModes = acceptedOutputModes;
80+
this.acceptedOutputModes = List.copyOf(acceptedOutputModes);
7981
return this;
8082
}
8183

0 commit comments

Comments
 (0)