fix(engine): bind MCP callers to the canonical workspace [skip ci]

Canonicalize the selected plugin workspace before pool comparison, storage
and fork construction. Equivalent symlink paths retain the existing caller
and attachment guards instead of rejecting a real session.

Adapt the merged PR #7138 existing shutdown test to the extracted app-server
and runtime crate paths; production shutdown behavior is unchanged.

Verified on native23 ordinary macOS ARM64 debug build with all 4,249 source
inputs unchanged: sealed selected checks 389 passed, 0 failed; merged shutdown
completion cases 11 passed, 0 failed. Native22 real DeepSeek Flash symlink
workspace read completed successfully; production MCP postimage is identical.
Native23 P04 v4 and actual PostgreSQL pg7 joined observers passed. Actual
Stats observer passed on native22. These are local/source/provider receipts,
not Windows/package, release, or deployment qualification.

Refs CW-7764. Evidence: CW/artifacts/takeover-20261103/ and
CW/artifacts/engine-0103-execution-20261102/coherent-native23-existing-v1/.
This commit is contained in:
CodeWhale Bot committed 2026-11-03 01:38:52 -08:00
1 parent 004632acae
commit d7ff89d16c
2 files changed
+8 -7

No files matched your search

+3 -3
View File
@@ -1920,9 +1920,9 @@ mod tests {
#[test]
fn run_completion_exec_shutdown_consumes_real_spawned_engine() {
let _env = crate::test_support::lock_test_env();
let _env = codewhale_engine::test_support::lock_test_env();
let home = tempfile::tempdir().unwrap();
let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", home.path());
let _home = codewhale_engine::test_support::EnvVarGuard::set("CODEWHALE_HOME", home.path());
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
@@ -1940,7 +1940,7 @@ mod tests {
.disable(codewhale_runtime::features::Feature::Mcp);
let handle = codewhale_engine::core::engine::spawn_engine(
config,
&crate::config::Config::default(),
&codewhale_runtime::config::Config::default(),
None,
)
.await;
+5 -4
View File
@@ -4062,10 +4062,11 @@ impl McpPool {
&mut self,
plugins: Arc<codewhale_plugins::PluginRegistry>,
) -> Result<()> {
let workspace = checked_workspace_path(plugins.workspace())?;
anyhow::ensure!(
self.workspace
.as_deref()
.is_none_or(|workspace| workspace == plugins.workspace()),
.is_none_or(|current| current == workspace),
"another MCP workspace must use its own selected pool fork"
);
let next = plugins.caller_selection();
@@ -4073,7 +4074,7 @@ impl McpPool {
.plugin_registry
.as_deref()
.and_then(codewhale_plugins::PluginRegistry::caller_selection);
if next == previous && self.workspace.as_deref() == Some(plugins.workspace()) {
if next == previous && self.workspace.as_deref() == Some(workspace.as_path()) {
return Ok(());
}
anyhow::ensure!(
@@ -4100,7 +4101,7 @@ impl McpPool {
self.drop_all_connections("caller selection changed");
self.config_hash = hash_mcp_config(&config);
self.config = config;
self.workspace = Some(plugins.workspace().to_path_buf());
self.workspace = Some(workspace);
self.plugin_registry = Some(plugins);
self.catalog_generation.fetch_add(1, Ordering::SeqCst);
}
@@ -4114,7 +4115,7 @@ impl McpPool {
&self,
plugins: Arc<codewhale_plugins::PluginRegistry>,
) -> Result<Self> {
let workspace = plugins.workspace().to_path_buf();
let workspace = checked_workspace_path(plugins.workspace())?;
let config = match self.config_sources.first() {
Some(primary) => {
load_config_with_workspace_and_plugins(primary, &workspace, plugins.as_ref())?