exec-server: carry cwd as PathUri

This commit is contained in:
Adam Perry
2026-06-13 06:26:43 +00:00
parent f5dfe0cb26
commit fe560a0e13
12 changed files with 82 additions and 33 deletions
+1
View File
@@ -3667,6 +3667,7 @@ dependencies = [
"codex-secrets",
"codex-utils-cargo-bin",
"codex-utils-home-dir",
"codex-utils-path-uri",
"codex-utils-pty",
"futures",
"keyring",
@@ -156,7 +156,7 @@ fn exec_server_params_for_request(
codex_exec_server::ExecParams {
process_id: exec_server_process_id(process_id).into(),
argv: request.command.clone(),
cwd: request.cwd.to_path_buf(),
cwd: codex_utils_path_uri::PathUri::from_abs_path(&request.cwd),
env_policy,
env,
tty,
@@ -101,7 +101,7 @@ fn exec_server_params_use_env_policy_overlay_contract() {
capture_policy: crate::exec::ExecCapturePolicy::ShellTool,
sandbox: codex_sandboxing::SandboxType::None,
windows_sandbox_policy_cwd: cwd.clone(),
windows_sandbox_workspace_roots: vec![cwd],
windows_sandbox_workspace_roots: vec![cwd.clone()],
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
permission_profile,
@@ -115,6 +115,10 @@ fn exec_server_params_use_env_policy_overlay_contract() {
exec_server_params_for_request(/*process_id*/ 123, &request, /*tty*/ true);
assert_eq!(params.process_id.as_str(), "123");
assert_eq!(
params.cwd,
codex_utils_path_uri::PathUri::from_abs_path(&cwd)
);
assert!(params.env_policy.is_some());
assert_eq!(
params.env,
+3 -3
View File
@@ -157,7 +157,7 @@ Request params:
{
"processId": "proc-1",
"argv": ["bash", "-lc", "printf 'hello\\n'"],
"cwd": "/absolute/working/directory",
"cwd": "file:///absolute/working/directory",
"env": {
"PATH": "/usr/bin:/bin"
},
@@ -171,7 +171,7 @@ Field definitions:
- `processId`: caller-chosen stable id for this process within the connection.
- `argv`: command vector. It must be non-empty.
- `cwd`: absolute working directory used for the child process.
- `cwd`: absolute working-directory file URI, converted to a native path by the executing server. Legacy native absolute strings are accepted on same-OS connections.
- `env`: environment variables passed to the child process.
- `tty`: when `true`, spawn a PTY-backed interactive process.
- `pipeStdin`: when `true`, keep non-PTY stdin writable via `process/write`.
@@ -409,7 +409,7 @@ Initialize:
Start a process:
```json
{"id":2,"method":"process/start","params":{"processId":"proc-1","argv":["bash","-lc","printf 'ready\\n'; while IFS= read -r line; do printf 'echo:%s\\n' \"$line\"; done"],"cwd":"/tmp","env":{"PATH":"/usr/bin:/bin"},"tty":true,"pipeStdin":false,"arg0":null}}
{"id":2,"method":"process/start","params":{"processId":"proc-1","argv":["bash","-lc","printf 'ready\\n'; while IFS= read -r line; do printf 'echo:%s\\n' \"$line\"; done"],"cwd":"file:///tmp","env":{"PATH":"/usr/bin:/bin"},"tty":true,"pipeStdin":false,"arg0":null}}
{"id":2,"result":{"processId":"proc-1"}}
{"method":"process/output","params":{"processId":"proc-1","seq":1,"stream":"stdout","chunk":"cmVhZHkK"}}
```
+2 -1
View File
@@ -887,7 +887,8 @@ mod tests {
.start(crate::ExecParams {
process_id: ProcessId::from("default-env-proc"),
argv: vec!["true".to_string()],
cwd: std::env::current_dir().expect("read current dir"),
cwd: PathUri::from_path(std::env::current_dir().expect("read current dir"))
.expect("cwd URI"),
env_policy: None,
env: Default::default(),
tty: false,
+13 -11
View File
@@ -156,6 +156,12 @@ impl LocalProcess {
.argv
.split_first()
.ok_or_else(|| invalid_params("argv must not be empty".to_string()))?;
let cwd = params.cwd.to_abs_path().map_err(|err| {
invalid_params(format!(
"cwd `{}` is not native to this exec-server host: {err}",
params.cwd
))
})?;
{
let mut process_map = self.inner.processes.lock().await;
@@ -172,26 +178,20 @@ impl LocalProcess {
codex_utils_pty::spawn_pty_process(
program,
args,
params.cwd.as_path(),
cwd.as_path(),
&env,
&params.arg0,
TerminalSize::default(),
)
.await
} else if params.pipe_stdin {
codex_utils_pty::spawn_pipe_process(
program,
args,
params.cwd.as_path(),
&env,
&params.arg0,
)
.await
codex_utils_pty::spawn_pipe_process(program, args, cwd.as_path(), &env, &params.arg0)
.await
} else {
codex_utils_pty::spawn_pipe_process_no_stdin(
program,
args,
params.cwd.as_path(),
cwd.as_path(),
&env,
&params.arg0,
)
@@ -783,16 +783,18 @@ fn notification_sender(inner: &Inner) -> Option<RpcNotificationSender> {
mod tests {
use super::*;
use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
use codex_utils_path_uri::PathUri;
use codex_utils_pty::ProcessDriver;
use pretty_assertions::assert_eq;
use tokio::sync::oneshot;
use tokio::time::timeout;
fn test_exec_params(env: HashMap<String, String>) -> ExecParams {
let cwd = std::env::current_dir().expect("current directory");
ExecParams {
process_id: ProcessId::from("env-test"),
argv: vec!["true".to_string()],
cwd: std::path::PathBuf::from("/tmp"),
cwd: PathUri::from_path(cwd).expect("cwd URI"),
env_policy: None,
env,
tty: false,
+36 -2
View File
@@ -1,5 +1,4 @@
use std::collections::HashMap;
use std::path::PathBuf;
use crate::FileSystemSandboxContext;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
@@ -103,7 +102,8 @@ pub struct ExecParams {
/// This is a protocol key, not an OS pid.
pub process_id: ProcessId,
pub argv: Vec<String>,
pub cwd: PathBuf,
/// Absolute working directory, converted to a native path by the executing server.
pub cwd: PathUri,
#[serde(default)]
pub env_policy: Option<ExecEnvPolicy>,
pub env: HashMap<String, String>,
@@ -465,15 +465,49 @@ mod base64_bytes {
#[cfg(test)]
mod tests {
use super::EnvironmentInfo;
use super::ExecParams;
use super::FsReadFileParams;
use super::HttpRequestParams;
use super::ShellInfo;
use crate::FileSystemSandboxContext;
use crate::ProcessId;
use codex_protocol::models::PermissionProfile;
use codex_utils_path_uri::PathConvention;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
#[test]
fn exec_protocol_accepts_legacy_native_cwd_and_serializes_path_uri() {
let legacy_cwd = std::env::current_dir().expect("current directory");
let expected_cwd = PathUri::from_path(&legacy_cwd).expect("cwd URI");
let params: ExecParams = serde_json::from_value(serde_json::json!({
"processId": "legacy-cwd",
"argv": ["example"],
"cwd": legacy_cwd.to_string_lossy(),
"env": {},
"tty": false,
"pipeStdin": false,
"arg0": null,
}))
.expect("legacy native cwd should deserialize");
let expected = ExecParams {
process_id: ProcessId::from("legacy-cwd"),
argv: vec!["example".to_string()],
cwd: expected_cwd.clone(),
env_policy: None,
env: Default::default(),
tty: false,
pipe_stdin: false,
arg0: None,
};
assert_eq!(params, expected);
assert_eq!(
serde_json::to_value(params).expect("params should serialize")["cwd"],
expected_cwd.to_string()
);
}
#[test]
fn filesystem_protocol_accepts_legacy_absolute_paths_and_serializes_path_uris() {
let legacy_path = std::env::current_dir()
@@ -2,6 +2,7 @@ use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use tokio::sync::mpsc;
use uuid::Uuid;
@@ -26,7 +27,7 @@ fn exec_params_with_argv(process_id: &str, argv: Vec<String>) -> ExecParams {
ExecParams {
process_id: ProcessId::from(process_id),
argv,
cwd: std::env::current_dir().expect("cwd"),
cwd: PathUri::from_path(std::env::current_dir().expect("cwd")).expect("cwd URI"),
env_policy: None,
env: inherited_path_env(),
tty: false,
+2 -1
View File
@@ -195,6 +195,7 @@ mod tests {
use codex_app_server_protocol::JSONRPCRequest;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_utils_path_uri::PathUri;
use serde::Serialize;
use serde::de::DeserializeOwned;
use tokio::io::AsyncBufReadExt;
@@ -396,7 +397,7 @@ mod tests {
ExecParams {
process_id,
argv: sleep_then_print_argv(),
cwd: std::env::current_dir().expect("cwd"),
cwd: PathUri::from_path(std::env::current_dir().expect("cwd")).expect("cwd URI"),
env_policy: None,
env,
tty: false,
+13 -12
View File
@@ -15,6 +15,7 @@ use codex_exec_server::ProcessSignal;
use codex_exec_server::ReadResponse;
use codex_exec_server::StartedExecProcess;
use codex_exec_server::WriteStatus;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use test_case::test_case;
@@ -72,7 +73,7 @@ async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> {
.start(ExecParams {
process_id: ProcessId::from("proc-1"),
argv: vec!["true".to_string()],
cwd: std::env::current_dir()?,
cwd: PathUri::from_path(std::env::current_dir()?)?,
env_policy: /*env_policy*/ None,
env: Default::default(),
tty: false,
@@ -213,7 +214,7 @@ async fn assert_exec_process_streams_output(use_remote: bool) -> Result<()> {
"-c".to_string(),
"sleep 0.05; printf 'session output\\n'".to_string(),
],
cwd: std::env::current_dir()?,
cwd: PathUri::from_path(std::env::current_dir()?)?,
env_policy: /*env_policy*/ None,
env: Default::default(),
tty: false,
@@ -244,7 +245,7 @@ async fn assert_exec_process_pushes_events(use_remote: bool) -> Result<()> {
"-c".to_string(),
"printf 'event output\\n'; sleep 0.1; printf 'event err\\n' >&2; sleep 0.1; exit 7".to_string(),
],
cwd: std::env::current_dir()?,
cwd: PathUri::from_path(std::env::current_dir()?)?,
env_policy: /*env_policy*/ None,
env: Default::default(),
tty: false,
@@ -291,7 +292,7 @@ async fn assert_exec_process_replays_events_after_close(use_remote: bool) -> Res
"-c".to_string(),
"printf 'late one\\n'; printf 'late two\\n'".to_string(),
],
cwd: std::env::current_dir()?,
cwd: PathUri::from_path(std::env::current_dir()?)?,
env_policy: /*env_policy*/ None,
env: Default::default(),
tty: false,
@@ -339,7 +340,7 @@ async fn assert_exec_process_retains_output_after_exit_until_streams_close(
DELAYED_OUTPUT_AFTER_EXIT_PARENT_ARG.to_string(),
release_path.to_string_lossy().into_owned(),
],
cwd: std::env::current_dir()?,
cwd: PathUri::from_path(std::env::current_dir()?)?,
env_policy: /*env_policy*/ None,
env: Default::default(),
tty: false,
@@ -412,7 +413,7 @@ async fn assert_exec_process_write_then_read(use_remote: bool) -> Result<()> {
"-c".to_string(),
"IFS= read line; printf 'from-stdin:%s\\n' \"$line\"".to_string(),
],
cwd: std::env::current_dir()?,
cwd: PathUri::from_path(std::env::current_dir()?)?,
env_policy: /*env_policy*/ None,
env: Default::default(),
tty: true,
@@ -449,7 +450,7 @@ async fn assert_exec_process_write_then_read_without_tty(use_remote: bool) -> Re
"-c".to_string(),
"IFS= read line; printf 'from-stdin:%s\\n' \"$line\"".to_string(),
],
cwd: std::env::current_dir()?,
cwd: PathUri::from_path(std::env::current_dir()?)?,
env_policy: /*env_policy*/ None,
env: Default::default(),
tty: false,
@@ -482,7 +483,7 @@ async fn assert_exec_process_rejects_write_without_pipe_stdin(use_remote: bool)
"-c".to_string(),
"sleep 0.3; if IFS= read -r line; then printf 'read:%s\\n' \"$line\"; else printf 'eof\\n'; fi".to_string(),
],
cwd: std::env::current_dir()?,
cwd: PathUri::from_path(std::env::current_dir()?)?,
env_policy: /*env_policy*/ None,
env: Default::default(),
tty: false,
@@ -516,7 +517,7 @@ async fn assert_exec_process_signal_interrupts_process(use_remote: bool) -> Resu
"-c".to_string(),
"trap 'printf \"signal:2\\n\"; exit 7' INT; printf 'ready\\n'; while :; do :; done".to_string(),
],
cwd: std::env::current_dir()?,
cwd: PathUri::from_path(std::env::current_dir()?)?,
env_policy: /*env_policy*/ None,
env: Default::default(),
tty: false,
@@ -569,7 +570,7 @@ async fn assert_exec_process_signal_reports_unsupported_on_windows(use_remote: b
"/C".to_string(),
"echo ready && ping -n 30 127.0.0.1 >NUL".to_string(),
],
cwd: std::env::current_dir()?,
cwd: PathUri::from_path(std::env::current_dir()?)?,
env_policy: /*env_policy*/ None,
env: Default::default(),
tty: false,
@@ -609,7 +610,7 @@ async fn assert_exec_process_preserves_queued_events_before_subscribe(
"-c".to_string(),
"printf 'queued output\\n'".to_string(),
],
cwd: std::env::current_dir()?,
cwd: PathUri::from_path(std::env::current_dir()?)?,
env_policy: /*env_policy*/ None,
env: Default::default(),
tty: false,
@@ -644,7 +645,7 @@ async fn remote_exec_process_reports_transport_disconnect() -> Result<()> {
"-c".to_string(),
"sleep 10".to_string(),
],
cwd: std::env::current_dir()?,
cwd: PathUri::from_path(std::env::current_dir()?)?,
env_policy: /*env_policy*/ None,
env: Default::default(),
tty: false,
+1
View File
@@ -23,6 +23,7 @@ codex-keyring-store = { workspace = true }
codex-protocol = { workspace = true }
codex-secrets = { workspace = true }
codex-utils-pty = { workspace = true }
codex-utils-path-uri = { workspace = true }
codex-utils-home-dir = { workspace = true }
bytes = { workspace = true }
futures = { workspace = true, default-features = false, features = ["std"] }
@@ -35,6 +35,7 @@ use codex_exec_server::ExecEnvPolicy;
use codex_exec_server::ExecParams;
use codex_exec_server::ExecProcess;
use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
use codex_utils_path_uri::PathUri;
#[cfg(unix)]
use codex_utils_pty::process_group::kill_process_group;
#[cfg(unix)]
@@ -478,6 +479,8 @@ impl ExecutorStdioServerLauncher {
"executor stdio server requires an explicit cwd",
));
};
let cwd = PathUri::from_path(&cwd)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
let program_name = program.to_string_lossy().into_owned();
let envs = create_env_overlay_for_remote_mcp_server(env, &env_vars);
let remote_env_vars = remote_mcp_env_var_names(&env_vars);