Skip to content

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

Open
AriaZhao-coder wants to merge 1 commit 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 1 commit 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

Copy link
Copy Markdown

📊 PR Size: size/M

  • Lines changed: 108
  • Additions: +100
  • Deletions: -8
  • 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;
    }

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

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