{"record":{"id":"d467d41950a97359","repo":"Hmbown/CodeWhale","slug":"plugin-registry-workspace-does-not-match-mcp-pool","errorCode":null,"errorMessage":"plugin registry workspace does not match MCP pool workspace","messagePattern":"plugin registry workspace does not match MCP pool workspace","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/mcp.rs","lineNumber":2422,"sourceCode":"    /// Create a pool from global MCP config plus workspace-local\n    /// `.codewhale/mcp.json`. Project servers override same-name global\n    /// servers and default stdio `cwd` to the workspace root.\n    #[cfg(test)]\n    pub fn from_config_path_with_workspace(\n        path: &std::path::Path,\n        workspace: &Path,\n    ) -> Result<Self> {\n        let plugins = Arc::new(crate::plugins::PluginRegistry::empty(workspace));\n        Self::from_config_path_with_workspace_and_plugins(path, workspace, plugins)\n    }\n\n    pub fn from_config_path_with_workspace_and_plugins(\n        path: &std::path::Path,\n        workspace: &Path,\n        plugins: Arc<crate::plugins::PluginRegistry>,\n    ) -> Result<Self> {\n        if plugins.workspace() != workspace {\n            anyhow::bail!(\"plugin registry workspace does not match MCP pool workspace\");\n        }\n        let config = load_config_with_workspace_and_plugins(path, workspace, plugins.as_ref())?;\n        let workspace = checked_workspace_path(workspace)?;\n        let mut pool = Self::new(config);\n        pool.config_sources = vec![\n            path.to_path_buf(),\n            checked_workspace_mcp_config_path(&workspace)?,\n        ];\n        pool.config_sources\n            .extend(crate::config::workspace_trust_config_candidate_paths());\n        pool.last_mtimes = pool\n            .config_sources\n            .iter()\n            .map(|source| mcp_config_mtime(source))\n            .collect();\n        pool.workspace = Some(workspace);\n        pool.plugin_registry = Some(plugins);\n        Ok(pool)","sourceCodeStart":2404,"sourceCodeEnd":2440,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/mcp.rs#L2404-L2440","documentation":"McpPool::from_config_path_with_workspace_and_plugins requires that the PluginRegistry it is given was constructed for exactly the same workspace path as the pool's workspace. The check plugins.workspace() != workspace is a pure path-equality precondition violation — the config file is not even read before it fails. It exists to prevent a pool and its plugin registry from disagreeing about which workspace's reviewed plugins are trusted.","triggerScenarios":"Calling from_config_path_with_workspace_and_plugins with a PluginRegistry built via PluginRegistry::empty(dir_a) (or loaded from dir_a) while passing dir_b as the workspace, including cases where the two paths differ only by symlink resolution, trailing components, or relative-vs-absolute spelling.","commonSituations":"Refactors where the registry is created early from a config-derived path and the pool later from a canonicalized workspace root; tests building fixtures in temp dirs and passing mismatched paths; symlinked checkouts where one caller canonicalizes and the other does not.","solutions":["Construct the PluginRegistry from the exact same workspace value you pass to the pool constructor.","Canonicalize both paths once (std::fs::canonicalize or the crate's checked_workspace_path) before comparing/deriving either.","Check for symlinked or relative components in one of the paths; make both absolute and normalized.","If the mismatch is intentional, rethink the design — the API deliberately forbids split workspaces."],"exampleFix":"// before\nlet plugins = Arc::new(PluginRegistry::empty(Path::new(\"/repo\")));\nlet pool = McpPool::from_config_path_with_workspace_and_plugins(\n    &cfg, Path::new(\"/repo/symlink-target\"), plugins)?; // bail: workspace mismatch\n\n// after\nlet workspace = std::fs::canonicalize(\"/repo\")?;\nlet plugins = Arc::new(PluginRegistry::empty(workspace.clone()));\nlet pool = McpPool::from_config_path_with_workspace_and_plugins(\n    &cfg, &workspace, plugins)?;","handlingStrategy":"validation","validationCode":"// Rust: assert precondition before constructing the pool\nfn assert_same_workspace(plugins: &PluginRegistry, ws: &Path) -> Result<()> {\n    if plugins.workspace() != ws {\n        bail!(\"registry workspace {:?} != pool workspace {:?}\", plugins.workspace(), ws);\n    }\n    Ok(())\n}\nassert_same_workspace(&plugins, &workspace)?;\nlet pool = McpPool::from_config_path_with_workspace_and_plugins(&path, &workspace, plugins)?;","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Create the PluginRegistry and the pool from one shared, canonicalized workspace value.","Canonicalize with the same helper (checked_workspace_path) on both sides so symlinks can't split them.","Unit-test constructor pairs with mismatched paths to fail fast at the call site."],"tags":["mcp","workspace","validation","programming-error"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}