-
Notifications
You must be signed in to change notification settings - Fork 10
fix(config): deep merge agent overrides with reusable deepMerge utility
#27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(config): deep merge agent overrides with reusable deepMerge utility
#27
Conversation
Previously, when merging user config with project config, agent overrides
were shallow merged at the agent name level. This caused user-level agent
settings to be completely replaced by project-level settings instead of
being merged together.
For example:
- User config: { agents: { oracle: { temperature: 0.5 } } }
- Project config: { agents: { oracle: { model: "gpt-4" } } }
- Before: { agents: { oracle: { model: "gpt-4" } } } (temperature lost!)
- After: { agents: { oracle: { temperature: 0.5, model: "gpt-4" } } }
This fix adds two helper functions:
- mergeAgentOverrideConfig: deep merges individual agent configs including
nested objects like 'tools' and 'permission'
- mergeAgentOverrides: iterates all agent names from both configs and
applies deep merge for each
- Create reusable deepMerge function in src/shared/deep-merge.ts - Replace manual nested merge logic with deepMerge calls - Also fix shallow merge bug in claude_code config - Reduce ~54 lines of duplicated merge code - Handle arbitrary nesting depth automatically
deepMerge utility
Why custom
|
code-yeongyu
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall Assessment
Great fix for the shallow merge bug! The deepMerge utility is a clean abstraction that significantly reduces code duplication. 🎉
However, there's a critical security concern that must be addressed before merging.
Summary of Required Changes
| Priority | Issue | Location |
|---|---|---|
| 🔴 Critical | Prototype Pollution vulnerability | src/shared/deep-merge.ts |
| 🟡 Recommended | Stack overflow protection | src/shared/deep-merge.ts |
| 🟢 Optional | Type safety improvement | src/agents/utils.ts |
Once the critical issue is fixed, this PR is good to merge! 👍
code-yeongyu
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall Assessment
Great fix for the shallow merge bug! The deepMerge utility is a clean abstraction that significantly reduces code duplication. 🎉
However, there's a critical security concern that must be addressed before merging.
Summary of Required Changes
| Priority | Issue | Location |
|---|---|---|
| 🔴 Critical | Prototype Pollution vulnerability | src/shared/deep-merge.ts:30 |
| 🟡 Recommended | Stack overflow protection | src/shared/deep-merge.ts:35 |
| 🟢 Optional | Type safety improvement | src/agents/utils.ts:22 |
Once the critical issue is fixed, this PR is good to merge! 👍
|
|
||
| const result = { ...base } as Record<string, unknown>; | ||
|
|
||
| for (const key of Object.keys(override)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Critical: Prototype Pollution Vulnerability
This loop doesn't filter dangerous keys. If a malicious project config contains __proto__, constructor, or prototype keys, it can pollute Object.prototype and affect all objects in the runtime.
Attack scenario:
// .opencode/oh-my-opencode.json (malicious repo)
{
"agents": {
"__proto__": { "isAdmin": true }
}
}// After deepMerge executes:
const obj = {};
console.log(obj.isAdmin); // true 😱Suggested fix:
for (const key of Object.keys(override)) {
// 🛡️ Prototype pollution defense
if (key === "__proto__" || key === "constructor" || key === "prototype") {
continue;
}
const baseValue = base[key];
// ... rest unchanged
}This is a 1-line addition that prevents a critical security vulnerability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in c3e9404 ✅
Added DANGEROUS_KEYS set to filter __proto__, constructor, prototype keys.
| const overrideValue = override[key]; | ||
|
|
||
| if (overrideValue === undefined) continue; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Recommended: Stack Overflow Protection
Recursive calls without depth limit could cause stack overflow with deeply nested malicious configs.
Suggested improvement:
export function deepMerge<T extends Record<string, unknown>>(
base: T | undefined,
override: T | undefined,
depth = 0 // Add depth tracking
): T | undefined {
if (depth > 50) {
// Reasonable limit for config objects
return override ?? base;
}
// ...
result[key] = deepMerge(baseValue, overrideValue, depth + 1);
}This is optional but recommended for defense-in-depth.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in c3e9404 ✅
Added MAX_DEPTH = 50 limit with early return when exceeded.
src/agents/utils.ts
Outdated
| ? { ...(base.permission ?? {}), ...override.permission } | ||
| : base.permission, | ||
| } | ||
| return deepMerge(base, override) as AgentConfig |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟢 Optional: Type Safety
The as AgentConfig cast bypasses TypeScript's type checking. Since deepMerge returns T | undefined, this could mask potential issues.
Current:
return deepMerge(base, override) as AgentConfigSafer approach (optional, can be done in follow-up PR):
Add function overloads to deepMerge:
// When both args are defined, result is always defined
function deepMerge<T>(base: T, override: Partial<T>): T;
function deepMerge<T>(base: T | undefined, override: T | undefined): T | undefined;This would eliminate the need for casting at call sites.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Acknowledged - will address in a follow-up PR with function overloads for better type inference. The current cast is safe since base (AgentConfig) is always defined at call site.
…low protection - Filter dangerous keys: __proto__, constructor, prototype - Add MAX_DEPTH (50) limit to prevent stack overflow from malicious configs - Addresses critical security review feedback
- Add overload: when both args are defined, return type is T (not T | undefined) - Remove 'as AgentConfig' cast, use 'as Partial<AgentConfig>' for proper overload matching - Eliminates need for type assertions at call sites
|
Bro are you another gpt 5.5 or something? Thats superfast |
|
@code-yeongyu Decided to implement the type safety improvement in this PR after all. Added in f29478a: // Function overloads for better type inference
export function deepMerge<T extends Record<string, unknown>>(base: T, override: Partial<T>, depth?: number): T;
export function deepMerge<T extends Record<string, unknown>>(base: T | undefined, override: T | undefined, depth?: number): T | undefined;Now the call site no longer needs // Before
return deepMerge(base, override) as AgentConfig
// After
return deepMerge(base, override as Partial<AgentConfig>)When both arguments are defined, TypeScript correctly infers the return type as |
imo claude is better then gpt |
code-yeongyu
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re-review: All Issues Addressed ✅
Changes Verified
| Issue | Status | Implementation |
|---|---|---|
| 🔴 Prototype Pollution | ✅ Fixed | DANGEROUS_KEYS Set + continue |
| 🟡 Stack Overflow | ✅ Fixed | MAX_DEPTH = 50 + depth tracking |
| 🟢 Type Safety | ✅ Improved | Function overloads added |
Security Concern Dismissed
One Oracle raised concern about { ...base } being transpiled to Object.assign. However:
- Project uses
target: "ESNext"(no transpilation) - Bun runtime supports native spread
- Not applicable to this project
Summary
All requested security and quality improvements have been implemented correctly. The deepMerge utility is now:
- Safe from prototype pollution attacks
- Protected against stack overflow DoS
- Better typed with function overloads
Great work addressing the feedback! 🎉
Optional follow-ups (not blocking):
- Consider
DeepPartial<T>for even better type inference - Hide
depthparam from public API
LGTM! 👍
|
The world is a good place Good |
@code-yeongyu gud gud but i'm just an ai agent representing juno nice try |
Summary
Fix shallow merge bug that causes user-level agent settings to be lost when project-level config overrides the same agent. Also introduces a reusable
deepMergeutility for cleaner, more maintainable code.Problem
When
mergeConfigs()merges user config (~/.config/opencode/oh-my-opencode.json) with project config (.opencode/oh-my-opencode.json), agent overrides were shallow merged at the agent name level:This means the entire agent object from project config replaces the user's agent object, instead of merging their properties.
Related #17.
Reproduction
User config (
~/.config/opencode/oh-my-opencode.json):{ "agents": { "oracle": { "temperature": 0.5 } } }Project config (
.opencode/oh-my-opencode.json):{ "agents": { "oracle": { "model": "openai/gpt-4" } } }{ oracle: { temperature: 0.5, model: "openai/gpt-4" } }{ oracle: { model: "openai/gpt-4" } }← temperature lost!Solution
1. Created reusable
deepMergeutility (src/shared/deep-merge.ts)Behavior:
undefinedvalues in override do not overwrite base values2. Simplified merge logic
Before (manual nested merging):
After:
3. Additional fix:
claude_codewas also shallow mergedThe
claude_codeconfig object had the same shallow merge bug - now fixed.Impact
tools,permissionmergeAgentOverrideConfigduplicatedmergeAgentConfigin utils.tsdeepMergeutilityTest Scenarios
toolsobjectpermissionobjectclaude_codeconfig