Skip to content

feat: harden spec immutability#968

Open
Sh1bari wants to merge 2 commits into
a2aproject:mainfrom
Sh1bari:feat/spec-record-immutability
Open

feat: harden spec immutability#968
Sh1bari wants to merge 2 commits into
a2aproject:mainfrom
Sh1bari:feat/spec-record-immutability

Conversation

@Sh1bari

@Sh1bari Sh1bari commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Spec cleanup: immutable records and copy semantics

Build fails after actions/checkout was bumped from v6 to v7 in PR #948:
#948

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces defensive copying and immutability for collection fields across several record classes and builders in the org.a2aproject.sdk.spec package using List.copyOf and Map.copyOf. The review feedback suggests further refining these changes by replacing @Nullable Boolean with a primitive boolean in MessageSendConfiguration and utilizing explicit canonical constructors in CancelTaskParams to avoid redundant null checks on record accessors when non-null default values are guaranteed.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread spec/src/main/java/org/a2aproject/sdk/spec/MessageSendConfiguration.java Outdated
Comment thread spec/src/main/java/org/a2aproject/sdk/spec/CancelTaskParams.java Outdated
Comment thread spec/src/main/java/org/a2aproject/sdk/spec/CancelTaskParams.java
@Sh1bari Sh1bari force-pushed the feat/spec-record-immutability branch 5 times, most recently from be8709f to 1151c80 Compare July 4, 2026 03:35
@kabir

kabir commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces defensive copying of collections and maps across various specification records (such as AgentCard, AgentSkill, Artifact, and others) to ensure immutability, along with comprehensive unit tests to verify this behavior. The review feedback correctly points out a critical issue: using Map.copyOf on arbitrary metadata or parameter maps will throw a NullPointerException if any values are null, which is common in JSON-based protocols. It is recommended to use Collections.unmodifiableMap(new HashMap<>(...)) instead to safely preserve immutability while supporting null values.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

public AgentCardSignature {
Assert.checkNotNullParam("protectedHeader", protectedHeader);
Assert.checkNotNullParam("signature", signature);
header = header == null ? null : Map.copyOf(header);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using Map.copyOf on arbitrary header maps (like Map<String, Object>) is risky because Map.copyOf throws a NullPointerException if the map contains any null values. In JSON-based protocols, null values are common and valid in metadata/header maps. If a client sends a payload with a null value in the header, deserialization will fail with a NullPointerException. Using Collections.unmodifiableMap(new HashMap<>(...)) is a safer alternative that preserves immutability while supporting null values.

Suggested change
header = header == null ? null : Map.copyOf(header);
header = header == null ? null : java.util.Collections.unmodifiableMap(new java.util.HashMap<>(header));

*/
public AgentExtension {
Assert.checkNotNullParam("uri", uri);
params = params == null ? null : Map.copyOf(params);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using Map.copyOf on arbitrary parameter maps (like Map<String, Object>) is risky because Map.copyOf throws a NullPointerException if the map contains any null values. In JSON-based protocols, null values are common and valid in parameters. If a client sends a payload with a null value in the parameters, deserialization will fail with a NullPointerException. Using Collections.unmodifiableMap(new HashMap<>(...)) is a safer alternative that preserves immutability while supporting null values.

Suggested change
params = params == null ? null : Map.copyOf(params);
params = params == null ? null : java.util.Collections.unmodifiableMap(new java.util.HashMap<>(params));

throw new IllegalArgumentException("Parts cannot be empty");
}
parts = List.copyOf(parts);
metadata = metadata == null ? null : Map.copyOf(metadata);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using Map.copyOf on arbitrary metadata maps (like Map<String, Object>) is risky because Map.copyOf throws a NullPointerException if the map contains any null values. In JSON-based protocols, null values are common and valid in metadata. If a client sends a payload with a null value in the metadata, deserialization will fail with a NullPointerException. Using Collections.unmodifiableMap(new HashMap<>(...)) is a safer alternative that preserves immutability while supporting null values.

Suggested change
metadata = metadata == null ? null : Map.copyOf(metadata);
metadata = metadata == null ? null : java.util.Collections.unmodifiableMap(new java.util.HashMap<>(metadata));

*/
public CancelTaskParams {
Assert.checkNotNullParam("id", id);
metadata = Map.copyOf(metadata);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using Map.copyOf on arbitrary metadata maps (like Map<String, Object>) is risky because Map.copyOf throws a NullPointerException if the map contains any null values. In JSON-based protocols, null values are common and valid in metadata. If a client sends a payload with a null value in the metadata, deserialization will fail with a NullPointerException. Using Collections.unmodifiableMap(new HashMap<>(...)) is a safer alternative that preserves immutability while supporting null values.

Suggested change
metadata = Map.copyOf(metadata);
metadata = java.util.Collections.unmodifiableMap(new java.util.HashMap<>(metadata));

*/
public MessageSendParams {
Assert.checkNotNullParam("message", message);
metadata = metadata == null ? null : Map.copyOf(metadata);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using Map.copyOf on arbitrary metadata maps (like Map<String, Object>) is risky because Map.copyOf throws a NullPointerException if the map contains any null values. In JSON-based protocols, null values are common and valid in metadata. If a client sends a payload with a null value in the metadata, deserialization will fail with a NullPointerException. Using Collections.unmodifiableMap(new HashMap<>(...)) is a safer alternative that preserves immutability while supporting null values.

Suggested change
metadata = metadata == null ? null : Map.copyOf(metadata);
metadata = metadata == null ? null : java.util.Collections.unmodifiableMap(new java.util.HashMap<>(metadata));

Assert.checkNotNullParam("taskId", taskId);
Assert.checkNotNullParam("artifact", artifact);
Assert.checkNotNullParam("contextId", contextId);
metadata = metadata == null ? null : Map.copyOf(metadata);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using Map.copyOf on arbitrary metadata maps (like Map<String, Object>) is risky because Map.copyOf throws a NullPointerException if the map contains any null values. In JSON-based protocols, null values are common and valid in metadata. If a client sends a payload with a null value in the metadata, deserialization will fail with a NullPointerException. Using Collections.unmodifiableMap(new HashMap<>(...)) is a safer alternative that preserves immutability while supporting null values.

Suggested change
metadata = metadata == null ? null : Map.copyOf(metadata);
metadata = metadata == null ? null : java.util.Collections.unmodifiableMap(new java.util.HashMap<>(metadata));

Assert.checkNotNullParam("taskId", taskId);
Assert.checkNotNullParam("status", status);
Assert.checkNotNullParam("contextId", contextId);
metadata = metadata != null ? Map.copyOf(metadata) : null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using Map.copyOf on arbitrary metadata maps (like Map<String, Object>) is risky because Map.copyOf throws a NullPointerException if the map contains any null values. In JSON-based protocols, null values are common and valid in metadata. If a client sends a payload with a null value in the metadata, deserialization will fail with a NullPointerException. Using Collections.unmodifiableMap(new HashMap<>(...)) is a safer alternative that preserves immutability while supporting null values.

Suggested change
metadata = metadata != null ? Map.copyOf(metadata) : null;
metadata = metadata != null ? java.util.Collections.unmodifiableMap(new java.util.HashMap<>(metadata)) : null;

@Sh1bari

Sh1bari commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

i will fix it

@Sh1bari

Sh1bari commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author
image or no what do you think? @kabir

@kabir

kabir commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

@Sh1bari I see this in the javadoc for Map.copyOf so it might have a point

Screenshot 2026-07-06 at 14 59 15

However, my local Claude instance says that there are other occurrences of Map.copyOIf in Message, Task, TextPart, FilePart and DataPart which are not touched by this PR. But I think you can update the ones you changed, and I can do the rest at some point if it becomes an issue

@Sh1bari Sh1bari force-pushed the feat/spec-record-immutability branch from 1151c80 to 231dfc5 Compare July 6, 2026 15:32
@Sh1bari

Sh1bari commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

@kabir done

@kabir

kabir commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces defensive copying and immutability enhancements across various record classes in the SDK specification (such as AgentCard, AgentCapabilities, and CancelTaskParams) to prevent external mutation of internal collections and maps. It also adds a comprehensive suite of unit tests to verify these immutability behaviors. The review feedback highlights potential NullPointerException risks in CancelTaskParams when metadata is null, recommending graceful handling by defaulting to empty maps in both the compact constructor and the builder.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread spec/src/main/java/org/a2aproject/sdk/spec/CancelTaskParams.java
Comment thread spec/src/main/java/org/a2aproject/sdk/spec/CancelTaskParams.java
@kabir kabir requested a review from jmesnil July 6, 2026 16:17
@kabir

kabir commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

@jmesnil Can you please take a look?

@@ -0,0 +1,109 @@
package org.a2aproject.sdk.server.tasks;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this test related to the spec hardening?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this test was accidentally deleted in feat: Implement MainEventBus architecture for event queue processing (#611). I just rollback it
image

defaultInputModes = List.copyOf(defaultInputModes);
defaultOutputModes = List.copyOf(defaultOutputModes);
skills = List.copyOf(skills);
securitySchemes = securitySchemes == null ? null : Map.copyOf(securitySchemes);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are cases where you use nullability?: + Map.copyOf, others with Collections.unmodifiableMap(new HashMap<> with or without nullabillity?: and it's not clear why these choices were made.

The code would be more readable if these was using helper methods that would describe the hardening choices.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it? I was not asking to use Collections.unmodifiableMap instead of copyOf.
I was suggesting to use helper methods to give more helpful information on the initialization of the data structures of the spec (eg this field can be safely copied, this one requires to use an unmodifiable map because null entry and/or values are allowed).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to using helper methods.

FYI Map.copyOf() has some issues if any of the keys/values are null, so we were steered towards using Collections.unmodifiableMap()

@Sh1bari Sh1bari Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec/src/main/java/org/a2aproject/sdk/util/CollectionCopies.java

Pushed a utility class with helper methods for the defensive-copy strategies. Could you please take a look?

I propose using this helper to make the intent explicit at call sites. If the approach looks good to you, I’ll update the usages in this PR to use it.
@jmesnil @kabir

Comment thread spec/src/main/java/org/a2aproject/sdk/spec/TaskStatusUpdateEvent.java Outdated
@Sh1bari Sh1bari force-pushed the feat/spec-record-immutability branch from 231dfc5 to 19693d3 Compare July 7, 2026 08:45
@Sh1bari Sh1bari requested a review from jmesnil July 7, 2026 09:05
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.*;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

star imports are not allowed

@Sh1bari Sh1bari force-pushed the feat/spec-record-immutability branch from d478368 to 48d517f Compare July 7, 2026 18:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants