fix(provider): isolate plugin hook mutations from internal provider state#1130
Conversation
…tate Plugin provider.models() hooks received the live internal provider Info object. A plugin mutating its argument (e.g. provider.name / provider.options) corrupted the provider state the rest of the engine reads, since the final provider is built from that same object via mergeDeep. Add a toPublicInfo() JSON-safe deep clone (drops functions/symbols/undefined, stringifies bigint; Info is plain-data so it round-trips) and pass the clone into both plugin entry points that hand out the internal Info: the provider.models() hook and the plugin auth loader. The auth-loader site keeps its existing nullable behavior (clone only when the provider is present), since in PawWork database[provider] can be undefined there and cloning undefined would throw. Adapted from upstream anomalyco/opencode 5e49029e70 (PR #26561, thanks Kit Langton). dev and upstream/dev share no common ancestor; reimplemented, not cherry-picked. Test: added "plugin provider.models hook cannot mutate internal provider state" (regression), proven red (leaked name = mutated-by-plugin) then green.
|
Warning Review limit reached
More reviews will be available in 51 minutes and 59 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a toPublicInfo utility function to deep-clone provider information before passing it to plugin hooks (such as models and auth loaders), preventing plugins from mutating internal provider state. It also adds a corresponding test suite to verify this behavior. Review feedback points out that using JSON.parse(JSON.stringify(...)) for deep cloning silently drops functions (like custom fetch functions) and symbols, which could lead to runtime errors. A custom recursive deep clone function is suggested to preserve these non-serializable properties while still preventing mutation.
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.
Summary
Plugin
provider.models()hooks were handed the live internalproviderInfo object. A plugin that mutates its argument (e.g.provider.name,provider.options, or model fields) corrupted the provider state the rest of the engine reads — the final provider returned bygetProvider/listis built from that same object viamergeDeep, so the mutation leaks straight through.Fix
Add a
toPublicInfo()JSON-safe deep clone (drops functions/symbols/undefined, stringifies bigint;Infois plain-data so it round-trips) and pass the clone into both plugin entry points that expose the internalInfo:provider.models()hook (models(toPublicInfo(provider), …)), andloader(toPublicInfo(authProvider)).The auth-loader site keeps its existing nullable behavior — it clones only when the provider is present, because in PawWork
database[plugin.auth.provider]can beundefinedthere andtoPublicInfo(undefined)would throw. Themodels()site is already guarded (if (!provider) continue), so it clones unconditionally.PawWork gap vs upstream
Adapted from upstream
anomalyco/opencode5e49029e70(PR #26561 — thanks Kit Langton). Upstream already hadtoPublicInfoand applied it to themodels()hook; PawWork had neithertoPublicInfonor the protection on either site, so this ports the helper and wires both.devandupstream/devshare no common ancestor; this is a re-implementation, not a cherry-pick.Test
Added
plugin provider.models hook cannot mutate internal provider state: loads a.opencode/pluginwhoseprovider.modelshook mutates its argument, then asserts the resolved provider'sname/optionsare untouched and models still resolve. Proven red → green (without the clone the mutated name leaks into provider state).Verification
bun test test/provider/provider.test.ts— 97 pass, 0 failbun test test/plugin/auth-override.test.ts test/plugin/codex.test.ts test/plugin/github-copilot-models.test.ts— 31 pass (auth-loader path unaffected)bun run typecheck— clean