fix(a2a-server): deep-merge user and workspace settings#28094
fix(a2a-server): deep-merge user and workspace settings#28094AriaZhao-coder wants to merge 1 commit into
Conversation
`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
|
📊 PR Size: size/M
|
Summary of ChangesHello, 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 Highlights
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
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.
| 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)) { |
There was a problem hiding this comment.
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.
| 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)) { |
| const targetValue = output[key]; | ||
| if (isPlainObject(targetValue) && isPlainObject(sourceValue)) { | ||
| output[key] = deepMergeSettings(targetValue, sourceValue); | ||
| } else { | ||
| output[key] = sourceValue; | ||
| } |
There was a problem hiding this comment.
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;
}
Summary
loadSettings()inpackages/a2a-server/src/config/settings.tscombined 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 } }enableRecursiveFileSearchis silently lost.respectGitIgnoreis overridden.Fix
Replace the shallow spread with a self-contained recursive deep merge:
__proto__/constructor/prototypekeys are skipped to prevent prototype pollution;undefinedsource values are ignored.The a2a-server only depends on
core, so this intentionally avoids the cli-onlycustomDeepMerge(which is coupled to cli'sMergeStrategy/settingsSchema). ThepolicyPaths/adminPolicyPathssecurity override is unchanged.Tests
undefined).Verified locally (Node 20):
a2a-serversuite — all 140 tests pass.prettier --check,eslint --max-warnings 0,tsc --noEmit— all clean.Fixes #25747