Skip to content

fix(a2a-server): deep-merge user and workspace settings#28094

Open
AriaZhao-coder wants to merge 3 commits into
google-gemini:mainfrom
AriaZhao-coder:fix/a2a-deep-merge-settings
Open

fix(a2a-server): deep-merge user and workspace settings#28094
AriaZhao-coder wants to merge 3 commits into
google-gemini:mainfrom
AriaZhao-coder:fix/a2a-deep-merge-settings

Conversation

@AriaZhao-coder

Copy link
Copy Markdown

Summary

loadSettings() in packages/a2a-server/src/config/settings.ts combined user and workspace settings with a shallow object spread ({ ...userSettings, ...workspaceSettings }).

Any nested section (tools, telemetry, fileFiltering, experimental, ...) defined in workspace settings replaced the entire corresponding section from user settings, silently dropping unrelated user-level configuration.

Example

User settings: { "fileFiltering": { "respectGitIgnore": true, "enableRecursiveFileSearch": true } }
Workspace settings: { "fileFiltering": { "respectGitIgnore": false } }

  • Before: enableRecursiveFileSearch is silently lost.
  • After: it is preserved; only respectGitIgnore is overridden.

Fix

Replace the shallow spread with a self-contained recursive deep merge:

  • nested plain objects are merged key-by-key;
  • arrays and primitive values from workspace settings still override the user values;
  • __proto__ / constructor / prototype keys are skipped to prevent prototype pollution;
  • undefined source values are ignored.

The a2a-server only depends on core, so this intentionally avoids the cli-only customDeepMerge (which is coupled to cli's MergeStrategy/settingsSchema). The policyPaths/adminPolicyPaths security override is unchanged.

Tests

  • Updated the existing test that encoded the shallow-merge bug (it previously asserted a nested key becomes undefined).
  • Added: multi-section deep merge with array replacement, and prototype-pollution safety.

Verified locally (Node 20):

  • The updated/added merge tests fail without the fix and pass with it (red/green).
  • a2a-server suite — all 140 tests pass.
  • prettier --check, eslint --max-warnings 0, tsc --noEmit — all clean.

Fixes #25747

`loadSettings()` combined user and workspace settings with a shallow
object spread, so any nested section (`tools`, `telemetry`,
`fileFiltering`, `experimental`, ...) defined in workspace settings
replaced the entire corresponding section from user settings, silently
dropping unrelated user-level configuration.

Replace the shallow spread with a self-contained recursive deep merge:
nested plain objects are merged key-by-key, while arrays and primitive
values from workspace settings still override the user values. The merge
skips `__proto__`/`constructor`/`prototype` keys to prevent prototype
pollution and ignores `undefined` source values. The a2a-server only
depends on core, so this avoids reaching for the cli-only
`customDeepMerge`.

Update the existing test that encoded the shallow-merge bug, and add
coverage for multi-section deep merge, array replacement, and prototype
pollution safety.

Fixes google-gemini#25747
@AriaZhao-coder AriaZhao-coder requested a review from a team as a code owner June 22, 2026 14:31
@github-actions github-actions Bot added the size/m A medium sized PR label Jun 22, 2026
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown

📊 PR Size: size/M

  • Lines changed: 237
  • Additions: +226
  • Deletions: -11
  • Files changed: 2

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a configuration merging issue in a2a-server where workspace settings were performing a shallow overwrite of user settings, causing nested configuration blocks to be lost. By implementing a robust deep-merge strategy, the system now correctly preserves nested user-defined keys while allowing workspace-specific overrides. The implementation includes security hardening against prototype pollution and is supported by comprehensive unit tests.

Highlights

  • Deep Merge Implementation: Replaced the shallow object spread in loadSettings with a recursive deepMergeSettings function to ensure nested configuration sections are merged rather than overwritten.
  • Prototype Pollution Protection: Added explicit checks to skip 'proto', 'constructor', and 'prototype' keys during the merge process to prevent potential prototype pollution vulnerabilities.
  • Test Coverage: Updated existing tests to reflect correct deep-merge behavior and added new test cases for multi-section merging and prototype pollution security.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-cli gemini-cli Bot added priority/p2 Important but can be addressed in a future release. area/core Issues related to User Interface, OS Support, Core Functionality labels Jun 22, 2026

@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 a custom recursive deep merge function (deepMergeSettings) to merge user and workspace settings, along with corresponding unit tests. The review feedback identifies two critical issues in the implementation: a potential server crash if settings are parsed as null, and a reference-sharing issue where nested source objects are assigned directly instead of being deeply cloned.

Comment on lines +217 to +221
function deepMergeSettings<T extends object>(target: T, source: T): T {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const output = { ...target } as Record<string, unknown>;

for (const [key, sourceValue] of Object.entries(source)) {

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

If either target or source is null (which can happen if a settings file is empty or contains null, as JSON.parse("null") returns null), calling Object.entries(source) will throw a TypeError: Cannot convert undefined or null to object and crash the server.

To prevent this, we should safely default target and source to empty objects if they are not plain objects.

Suggested change
function deepMergeSettings<T extends object>(target: T, source: T): T {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const output = { ...target } as Record<string, unknown>;
for (const [key, sourceValue] of Object.entries(source)) {
function deepMergeSettings<T extends object>(target: T, source: T): T {
const t = isPlainObject(target) ? target : {};
const s = isPlainObject(source) ? source : {};
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const output = { ...t } as Record<string, unknown>;
for (const [key, sourceValue] of Object.entries(s)) {

Comment on lines +230 to +235
const targetValue = output[key];
if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
output[key] = deepMergeSettings(targetValue, sourceValue);
} else {
output[key] = sourceValue;
}

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

When sourceValue is a plain object but targetValue is not (e.g., it is undefined), the code currently assigns sourceValue directly by reference: output[key] = sourceValue;.

This means the merged settings object will share references with the original source (workspace settings) object. Any subsequent mutations to the merged settings could unintentionally mutate the workspace settings object.

To align with the repository's established deep-merge behavior (as seen in packages/cli/src/utils/deepMerge.ts), we should recursively clone sourceValue when targetValue is not a plain object.

    const targetValue = output[key];
    if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
      output[key] = deepMergeSettings(targetValue, sourceValue);
    } else if (isPlainObject(sourceValue)) {
      output[key] = deepMergeSettings({}, sourceValue);
    } else {
      output[key] = sourceValue;
    }

AriaZhao-coder added a commit to AriaZhao-coder/gemini-cli that referenced this pull request Jun 25, 2026
…ted values

Address review feedback on google-gemini#28094:

- Normalize parsed user/workspace settings to `{}` when a file is empty
  or contains `null`/a primitive (JSON.parse("null") === null). This
  prevents a server crash both in the deep merge (Object.entries(null))
  and at the earlier folderTrust / later policyPaths property accesses.
- During the merge, deep-clone a nested source object when the target
  value is not a plain object, instead of assigning it by reference. This
  stops the merged result from sharing references with the workspace
  settings object, matching the behavior in cli/src/utils/deepMerge.ts.

Adds unit tests for deepMergeSettings (null-safety, reference cloning)
and loadSettings (null settings files do not crash).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ted values

Address review feedback on google-gemini#28094:

- Normalize parsed user/workspace settings to `{}` when a file is empty
  or contains `null`/a primitive (JSON.parse("null") === null). This
  prevents a server crash both in the deep merge (Object.entries(null))
  and at the earlier folderTrust / later policyPaths property accesses.
- During the merge, deep-clone a nested source object when the target
  value is not a plain object, instead of assigning it by reference. This
  stops the merged result from sharing references with the workspace
  settings object, matching the behavior in cli/src/utils/deepMerge.ts.

Adds unit tests for deepMergeSettings (null-safety, reference cloning)
and loadSettings (null settings files do not crash).
@AriaZhao-coder AriaZhao-coder force-pushed the fix/a2a-deep-merge-settings branch from 4d99c73 to 7b08194 Compare June 25, 2026 15:26
@AriaZhao-coder

Copy link
Copy Markdown
Author

Thanks for the review! I've addressed both high-priority points in the latest commit (7b081945d):

  • Null-safety: parsed user/workspace settings are now normalized to {} when a file is empty or contains null/a primitive (JSON.parse("null") === null). This guards both the deep merge (Object.entries(null)) and the earlier folderTrust / later policyPaths accesses in loadSettings, so a malformed settings file can no longer crash the server.
  • Reference sharing: nested source objects are now deep-cloned during the merge instead of being assigned by reference, so the merged result no longer shares references with the workspace settings object — matching the behavior in packages/cli/src/utils/deepMerge.ts.

Added unit tests covering both (null-safety + reference cloning) plus loadSettings no longer crashing on null settings files. PTAL 🙏

/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 replaces the shallow merge of user and workspace settings with a recursive deep merge implementation (deepMergeSettings) to preserve unrelated nested user configurations. It also introduces normalization to safely handle null or empty settings files and adds protection against prototype pollution. Feedback is provided regarding the deep merge implementation, which currently copies array references directly; cloning arrays using structuredClone is recommended to prevent reference sharing and unintended mutations.

Comment on lines +254 to +262
} else if (isPlainObject(sourceValue)) {
// The source value is a nested object but the target value is not, so
// recursively clone it. Assigning `sourceValue` directly would make the
// merged result share references with the original `source` object,
// allowing later mutations to leak back into it.
output[key] = deepMergeSettings({}, sourceValue);
} else {
output[key] = sourceValue;
}

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.

security-high high

While the deep merge implementation correctly clones nested plain objects to prevent reference sharing, it currently misses cloning arrays. If a workspace setting contains an array (such as fileFiltering.customIgnoreFilePaths), the array reference is copied directly from the source object to the merged target. Any subsequent mutations to the merged settings will leak back and mutate the original source object, which can cause unexpected side effects or test leakage.

Additionally, security-sensitive settings (such as tools.allowed) must not use a merge strategy that allows less-trusted configuration scopes (like a workspace) to override more-trusted scopes (like global user settings). Ensure that these sensitive settings are protected from being overridden by workspace configurations.

For cloning arrays safely, we can use structuredClone to avoid reference sharing.

    } else if (isPlainObject(sourceValue)) {
      // The source value is a nested object but the target value is not, so
      // recursively clone it. Assigning sourceValue directly would make the
      // merged result share references with the original source object,
      // allowing later mutations to leak back into it.
      output[key] = deepMergeSettings({}, sourceValue);
    } else if (Array.isArray(sourceValue)) {
      // Clone arrays to prevent reference sharing.
      output[key] = structuredClone(sourceValue);
    } else {
      output[key] = sourceValue;
    }
References
  1. Security-sensitive settings should not use a merge strategy (e.g., MergeStrategy.REPLACE) that allows less-trusted configuration scopes (like a workspace) to completely override more-trusted scopes (like global user settings).

Follow-up to the deep-merge review on google-gemini#28094: arrays were still copied by
reference from the source (workspace) settings into the merged result, so
mutating a merged array (e.g. fileFiltering.customIgnoreFilePaths) would
leak back and mutate the original source object.

Clone arrays with structuredClone when they replace the target value,
mirroring the nested-object cloning already in place. Adds a unit test
asserting the merged array is a distinct reference and mutation-isolated.
@AriaZhao-coder

Copy link
Copy Markdown
Author

Thanks for the follow-up review! Addressed in a407905d4:

  • Array cloning: arrays are now cloned with structuredClone when they replace the target value, mirroring the nested-object cloning. The merged result no longer shares array references with the source (workspace) settings, so mutations can't leak back. Added a unit test covering reference identity + mutation isolation for customIgnoreFilePaths.

Regarding the note on security-sensitive settings (e.g. tools.allowed): I've intentionally left the merge semantics as "workspace replaces user" for these, for a few reasons:

  1. Workspace settings are only merged when the workspace is trusted (isTrusted gate in loadSettings); untrusted workspaces never contribute.
  2. The genuinely sensitive policy settings — policyPaths and adminPolicyPaths — are already explicitly protected and can only come from user-level config, even for a trusted workspace.
  3. Changing tools.allowed to be non-overridable would be a behavior change beyond this PR's scope (it preserves the pre-existing shallow-merge precedence) and would contradict the existing test that asserts a trusted workspace can override it.

Happy to revisit the trust model for tool settings in a dedicated PR if maintainers feel it's warranted. PTAL 🙏

@AriaZhao-coder

Copy link
Copy Markdown
Author

/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 a deep-merge capability for settings in the a2a-server package, replacing the previous shallow merge behavior. It adds a robust deepMergeSettings utility that recursively merges nested configuration objects, prevents prototype pollution by filtering out dangerous keys like __proto__, and avoids shared references by cloning nested objects and arrays. Additionally, it introduces normalizeSettings to safely handle empty or null settings files, preventing server crashes. Comprehensive unit tests have been added to verify these merging behaviors, cloning safety, and security protections. There are no review comments to address, and I have no further feedback to provide as the implementation is solid and well-tested.

@gemini-cli

gemini-cli Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Hi there! Thank you for your interest in contributing to Gemini CLI.

To ensure we maintain high code quality and focus on our prioritized roadmap, we only guarantee review and consideration of pull requests for issues that are explicitly labeled as 'help wanted'.

This PR will be closed in 7 days if it remains without that designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core Issues related to User Interface, OS Support, Core Functionality priority/p2 Important but can be addressed in a future release. size/m A medium sized PR status/pr-nudge-sent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A2A server shallow-merges user and workspace settings, causing nested config to be lost

1 participant