Skip to content

fix(config): clone global cache to prevent cross-workspace mutation leak - #41950

Open
weiconghe wants to merge 1 commit into
anomalyco:devfrom
weiconghe:fix/config-global-cache-leak
Open

fix(config): clone global cache to prevent cross-workspace mutation leak#41950
weiconghe wants to merge 1 commit into
anomalyco:devfrom
weiconghe:fix/config-global-cache-leak

Conversation

@weiconghe

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #41916

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Config.loadInstanceState merges the process-global config cache into the per-workspace result via remeda mergeDeep. When the workspace has no project-level value for a nested key (e.g. mcp), mergeDeep copies the same object reference from the global cache into the result — verified against remeda 2.26.0:

mergeDeep({}, { mcp: { server1: {...} } }).mcp === global.mcp  // true

Plugin config hooks receive the workspace's live config-state object (packages/opencode/src/plugin/index.ts:245) and commonly mutate it in place (config.mcp[name] ??= {...}). When the workspace config shares a nested reference with the global cache, those writes leak back into cachedGlobal — which has Duration.infinity TTL. Every other workspace attached to the same opencode serve process then sees the leaked MCP servers (or any other nested config) in its /mcp status.

Fix: structuredClone the cached global config before merging it into the per-workspace result. The loadGlobal(authEnv) path already returns freshly parsed objects (no caching), so only the getGlobal() path needs cloning.

How did you verify your code works?

Added a regression test in test/config/config.test.ts that:

  1. Sets up a global-only config with mcp (no project-level override)
  2. Loads the instance config, simulates a plugin config hook mutation (cfg.mcp.injected = {...})
  3. Asserts Config.use.getGlobal() does not see the mutation

The test fails without the fix (globalCfg.mcp.injected is the injected object) and passes with it. Full config test suite: 93 pass, 4 pre-existing Windows-specific failures (unrelated — ENOENT on .jsonc auto-creation, Git Bash/Cygwin path timeout, permission key-order on Windows).

Note: the test clears OPENCODE_CONFIG_DIR because a developer's local config may contain mcp: {}, which incidentally breaks the shared reference chain via mergeDeep and masks the bug.

Screenshots / recordings

N/A

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plugin config hooks can mutate process-shared config state via shallow-merged nested objects

1 participant