mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
3e39e92f03
## Why
Executor-owned plugin roots are `PathUri`, but MCP config normalization
still converts them into a native `Path` using the app-server host's
rules. Relative `cwd` values can therefore resolve against the wrong
filesystem when host and executor path conventions differ.
This PR keeps executor MCP paths URI-native until the selected
environment launches the server, while retaining the existing host
parser behavior.
## What changed
- Keep one shared MCP normalization path with narrow host-`Path` and
executor-`PathUri` entrypoints.
- Preserve native host resolution for locally installed plugin MCP
configs.
- For executor configs, default `cwd` to the plugin root and resolve
relative working directories with the root URI's path convention.
- Accept explicit executor `file:` URIs only when they remain within the
selected plugin root.
- Preserve the selected environment id and existing remote
environment-variable ownership rules.
- Route the executor plugin provider through the URI-native entrypoint
without converting the root on the host.
- Ensure `codex doctor` does not probe executor-owned stdio commands or
foreign working directories on the host.
- Cover foreign Windows roots, relative and absolute executor working
directories, traversal rejection, runtime resolution, and doctor
behavior.
```text
plugin root: file:///C:/plugins/demo
configured cwd: scripts
|
v
resolved cwd: file:///C:/plugins/demo/scripts
|
v
launch through the selected executor
```
No new provider or filesystem abstraction is introduced.
## Stack
1. #29614 — add lexical `PathUri` containment.
2. #29620 — share URI-native manifest path resolution.
3. #28918 — keep selected plugin roots and resources URI-native.
4. #29626 — load executor skills without host path conversion.
5. **This PR** — resolve executor MCP working directories without host
path conversion.
273 lines
9.4 KiB
Rust
273 lines
9.4 KiB
Rust
//! Runtime support for Model Context Protocol (MCP) servers.
|
|
//!
|
|
//! This module contains data that describes the runtime environment in which MCP
|
|
//! servers execute, plus the sandbox state payload sent to capable servers and a
|
|
//! tiny shared metrics helper. Transport startup and orchestration live in
|
|
//! [`crate::rmcp_client`] and [`crate::connection_manager`].
|
|
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use codex_exec_server::Environment;
|
|
use codex_exec_server::EnvironmentManager;
|
|
use codex_protocol::models::PermissionProfile;
|
|
use codex_utils_path_uri::PathUri;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
|
|
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SandboxState {
|
|
pub permission_profile: PermissionProfile,
|
|
pub codex_linux_sandbox_exe: Option<PathBuf>,
|
|
pub sandbox_cwd: PathUri,
|
|
#[serde(default)]
|
|
pub use_legacy_landlock: bool,
|
|
}
|
|
|
|
/// Runtime context used when resolving per-server MCP environments.
|
|
///
|
|
/// `McpConfig` describes what servers exist. This value carries the canonical
|
|
/// environment registry plus the local stdio fallback cwd used when a local
|
|
/// stdio server omits its own working directory.
|
|
#[derive(Clone)]
|
|
pub struct McpRuntimeContext {
|
|
environment_manager: Arc<EnvironmentManager>,
|
|
local_stdio_fallback_cwd: PathBuf,
|
|
}
|
|
|
|
impl McpRuntimeContext {
|
|
pub fn new(
|
|
environment_manager: Arc<EnvironmentManager>,
|
|
local_stdio_fallback_cwd: PathBuf,
|
|
) -> Self {
|
|
Self {
|
|
environment_manager,
|
|
local_stdio_fallback_cwd,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn local_stdio_fallback_cwd(&self) -> PathBuf {
|
|
self.local_stdio_fallback_cwd.clone()
|
|
}
|
|
|
|
pub(crate) fn resolve_server_environment(
|
|
&self,
|
|
server_name: &str,
|
|
config: &codex_config::McpServerConfig,
|
|
) -> Result<Option<Arc<Environment>>, String> {
|
|
// Resolve `"local"` through the shared registry when available. Local
|
|
// HTTP is the one current exception: it can use the ambient HTTP client
|
|
// even when no local Environment is configured.
|
|
if let Some(environment) = self
|
|
.environment_manager
|
|
.get_environment(&config.environment_id)
|
|
{
|
|
return Ok(Some(environment));
|
|
}
|
|
|
|
if config.is_local_environment() {
|
|
return match config.transport {
|
|
codex_config::McpServerTransportConfig::Stdio { .. } => Err(format!(
|
|
"local stdio MCP server `{server_name}` requires a local environment"
|
|
)),
|
|
codex_config::McpServerTransportConfig::StreamableHttp { .. } => Ok(None),
|
|
};
|
|
}
|
|
|
|
Err(format!(
|
|
"MCP server `{server_name}` references unknown environment id `{}`",
|
|
config.environment_id
|
|
))
|
|
}
|
|
}
|
|
|
|
pub(crate) fn emit_duration(metric: &str, duration: Duration, tags: &[(&str, &str)]) {
|
|
if let Some(metrics) = codex_otel::global() {
|
|
let _ = metrics.record_duration(metric, duration, tags);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::collections::HashMap;
|
|
|
|
use codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID;
|
|
use codex_config::McpServerConfig;
|
|
use codex_config::McpServerTransportConfig;
|
|
use codex_exec_server::EnvironmentManager;
|
|
use codex_utils_path_uri::LegacyAppPathString;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use super::*;
|
|
|
|
fn stdio_server(environment_id: &str) -> McpServerConfig {
|
|
McpServerConfig {
|
|
transport: McpServerTransportConfig::Stdio {
|
|
command: "echo".to_string(),
|
|
args: Vec::new(),
|
|
env: None,
|
|
env_vars: Vec::new(),
|
|
cwd: None,
|
|
},
|
|
environment_id: environment_id.to_string(),
|
|
enabled: true,
|
|
required: false,
|
|
supports_parallel_tool_calls: false,
|
|
disabled_reason: None,
|
|
startup_timeout_sec: None,
|
|
tool_timeout_sec: None,
|
|
default_tools_approval_mode: None,
|
|
enabled_tools: None,
|
|
disabled_tools: None,
|
|
scopes: None,
|
|
oauth: None,
|
|
oauth_resource: None,
|
|
tools: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
fn http_server(environment_id: &str) -> McpServerConfig {
|
|
McpServerConfig {
|
|
transport: McpServerTransportConfig::StreamableHttp {
|
|
url: "http://127.0.0.1:1".to_string(),
|
|
bearer_token_env_var: None,
|
|
http_headers: None,
|
|
env_http_headers: None,
|
|
},
|
|
environment_id: environment_id.to_string(),
|
|
..stdio_server(environment_id)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn local_stdio_requires_local_stdio_availability() {
|
|
let runtime_context = McpRuntimeContext::new(
|
|
Arc::new(EnvironmentManager::without_environments()),
|
|
PathBuf::from("/tmp"),
|
|
);
|
|
|
|
let error = match runtime_context
|
|
.resolve_server_environment("stdio", &stdio_server(DEFAULT_MCP_SERVER_ENVIRONMENT_ID))
|
|
{
|
|
Ok(_) => panic!("local stdio MCP should require a local environment"),
|
|
Err(error) => error,
|
|
};
|
|
assert_eq!(
|
|
error,
|
|
"local stdio MCP server `stdio` requires a local environment"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn local_http_does_not_require_local_stdio_availability() {
|
|
let runtime_context = McpRuntimeContext::new(
|
|
Arc::new(EnvironmentManager::without_environments()),
|
|
PathBuf::from("/tmp"),
|
|
);
|
|
|
|
let resolved_runtime = match runtime_context
|
|
.resolve_server_environment("http", &http_server(DEFAULT_MCP_SERVER_ENVIRONMENT_ID))
|
|
{
|
|
Ok(resolved_runtime) => resolved_runtime,
|
|
Err(error) => panic!("local HTTP MCP should resolve: {error}"),
|
|
};
|
|
assert!(resolved_runtime.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_explicit_environment_is_rejected() {
|
|
let runtime_context = McpRuntimeContext::new(
|
|
Arc::new(EnvironmentManager::without_environments()),
|
|
PathBuf::from("/tmp"),
|
|
);
|
|
|
|
let error =
|
|
match runtime_context.resolve_server_environment("stdio", &stdio_server("remote")) {
|
|
Ok(_) => panic!("unknown MCP environment should fail"),
|
|
Err(error) => error,
|
|
};
|
|
assert_eq!(
|
|
error,
|
|
"MCP server `stdio` references unknown environment id `remote`"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn explicit_remote_stdio_and_http_accept_named_environment() {
|
|
let runtime_context = McpRuntimeContext::new(
|
|
Arc::new(
|
|
EnvironmentManager::create_for_tests(
|
|
Some("ws://127.0.0.1:8765".to_string()),
|
|
/*local_runtime_paths*/ None,
|
|
)
|
|
.await,
|
|
),
|
|
PathBuf::from("/tmp"),
|
|
);
|
|
|
|
let mut remote_stdio = stdio_server("remote");
|
|
let McpServerTransportConfig::Stdio { cwd, .. } = &mut remote_stdio.transport else {
|
|
unreachable!("stdio helper should build stdio transport");
|
|
};
|
|
*cwd = Some(LegacyAppPathString::from_path(&std::env::temp_dir()));
|
|
for resolved_runtime in [
|
|
runtime_context.resolve_server_environment("stdio", &remote_stdio),
|
|
runtime_context.resolve_server_environment("http", &http_server("remote")),
|
|
] {
|
|
let resolved_runtime = match resolved_runtime {
|
|
Ok(resolved_runtime) => resolved_runtime,
|
|
Err(error) => panic!("remote MCP should resolve: {error}"),
|
|
};
|
|
assert!(resolved_runtime.is_some());
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn remote_stdio_accepts_foreign_absolute_cwd() {
|
|
let runtime_context = McpRuntimeContext::new(
|
|
Arc::new(
|
|
EnvironmentManager::create_for_tests(
|
|
Some("ws://127.0.0.1:8765".to_string()),
|
|
/*local_runtime_paths*/ None,
|
|
)
|
|
.await,
|
|
),
|
|
PathBuf::from("/tmp"),
|
|
);
|
|
let mut remote_stdio = stdio_server("remote");
|
|
let McpServerTransportConfig::Stdio { cwd, .. } = &mut remote_stdio.transport else {
|
|
unreachable!("stdio helper should build stdio transport");
|
|
};
|
|
*cwd = Some(
|
|
PathUri::parse("file:///C:/plugins/demo")
|
|
.expect("foreign cwd URI")
|
|
.into(),
|
|
);
|
|
|
|
let resolved_runtime =
|
|
match runtime_context.resolve_server_environment("stdio", &remote_stdio) {
|
|
Ok(resolved_runtime) => resolved_runtime,
|
|
Err(error) => panic!("foreign cwd should resolve: {error}"),
|
|
};
|
|
assert!(resolved_runtime.is_some());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn local_stdio_accepts_local_environment_when_available() {
|
|
let runtime_context = McpRuntimeContext::new(
|
|
Arc::new(EnvironmentManager::default_for_tests()),
|
|
PathBuf::from("/tmp"),
|
|
);
|
|
|
|
let resolved_runtime = match runtime_context
|
|
.resolve_server_environment("stdio", &stdio_server(DEFAULT_MCP_SERVER_ENVIRONMENT_ID))
|
|
{
|
|
Ok(resolved_runtime) => resolved_runtime,
|
|
Err(error) => panic!("local stdio MCP should resolve: {error}"),
|
|
};
|
|
assert!(resolved_runtime.is_some());
|
|
}
|
|
}
|