Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ codex-config = { path = "config" }
codex-core = { path = "core" }
codex-environment = { path = "environment" }
codex-exec = { path = "exec" }
codex-exec-server = { path = "exec-server" }
codex-execpolicy = { path = "execpolicy" }
codex-experimental-api-macros = { path = "codex-experimental-api-macros" }
codex-feedback = { path = "feedback" }
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/app-server/src/fs_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub(crate) struct FsApi {
impl Default for FsApi {
fn default() -> Self {
Self {
file_system: Arc::new(Environment.get_filesystem()),
file_system: Environment::default().get_filesystem(),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ codex-client = { workspace = true }
codex-connectors = { workspace = true }
codex-config = { workspace = true }
codex-environment = { workspace = true }
codex-exec-server = { workspace = true }
codex-shell-command = { workspace = true }
codex-skills = { workspace = true }
codex-execpolicy = { workspace = true }
Expand Down
12 changes: 12 additions & 0 deletions codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1915,6 +1915,18 @@
"description": "Experimental / do not use. Replaces the synthesized realtime startup context appended to websocket session instructions. An empty string disables startup context injection entirely.",
"type": "string"
},
"experimental_unified_exec_exec_server_websocket_url": {
"description": "Optional websocket URL for connecting to an existing `codex-exec-server`.",
"type": "string"
},
"experimental_unified_exec_exec_server_workspace_root": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
}
],
"description": "Optional absolute path to the executor-visible workspace root that corresponds to the local session cwd."
},
"experimental_use_freeform_apply_patch": {
"type": "boolean"
},
Expand Down
13 changes: 11 additions & 2 deletions codex-rs/core/src/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ use crate::turn_diff_tracker::TurnDiffTracker;
use crate::turn_timing::TurnTimingState;
use crate::turn_timing::record_turn_ttfm_metric;
use crate::turn_timing::record_turn_ttft_metric;
use crate::unified_exec::RemoteExecServerBackend;
use crate::unified_exec::UnifiedExecProcessManager;
use crate::util::backoff;
use crate::windows_sandbox::WindowsSandboxLevelExt;
Expand Down Expand Up @@ -1772,6 +1773,13 @@ impl Session {
});
}

let remote_exec_server =
RemoteExecServerBackend::connect_for_config(config.as_ref()).await?;
let environment = remote_exec_server
.as_ref()
.map(|backend| Environment::with_file_system(Arc::new(backend.file_system())))
.unwrap_or_default();

let services = SessionServices {
// Initialize the MCP connection manager with an uninitialized
// instance. It will be replaced with one created via
Expand All @@ -1784,8 +1792,9 @@ impl Session {
&config.permissions.approval_policy,
))),
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
unified_exec_manager: UnifiedExecProcessManager::new(
unified_exec_manager: UnifiedExecProcessManager::with_remote_exec_server(
config.background_terminal_max_timeout,
remote_exec_server,
),
shell_zsh_path: config.zsh_path.clone(),
main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(),
Expand Down Expand Up @@ -1825,7 +1834,7 @@ impl Session {
code_mode_service: crate::tools::code_mode::CodeModeService::new(
config.js_repl_node_path.clone(),
),
environment: Arc::new(Environment),
environment: Arc::new(environment),
};
let js_repl = Arc::new(JsReplHandle::with_node_path(
config.js_repl_node_path.clone(),
Expand Down
4 changes: 2 additions & 2 deletions codex-rs/core/src/codex_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2465,7 +2465,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
true,
));
let network_approval = Arc::new(NetworkApprovalService::default());
let environment = Arc::new(codex_environment::Environment);
let environment = Arc::new(codex_environment::Environment::default());

let file_watcher = Arc::new(FileWatcher::noop());
let services = SessionServices {
Expand Down Expand Up @@ -3259,7 +3259,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
true,
));
let network_approval = Arc::new(NetworkApprovalService::default());
let environment = Arc::new(codex_environment::Environment);
let environment = Arc::new(codex_environment::Environment::default());

let file_watcher = Arc::new(FileWatcher::noop());
let services = SessionServices {
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/core/src/config/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4328,6 +4328,8 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
web_search_config: None,
use_experimental_unified_exec_tool: !cfg!(windows),
experimental_unified_exec_exec_server_websocket_url: None,
experimental_unified_exec_exec_server_workspace_root: None,
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
features: Features::with_defaults().into(),
Expand Down Expand Up @@ -4469,6 +4471,8 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
web_search_config: None,
use_experimental_unified_exec_tool: !cfg!(windows),
experimental_unified_exec_exec_server_websocket_url: None,
experimental_unified_exec_exec_server_workspace_root: None,
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
features: Features::with_defaults().into(),
Expand Down Expand Up @@ -4608,6 +4612,8 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
web_search_config: None,
use_experimental_unified_exec_tool: !cfg!(windows),
experimental_unified_exec_exec_server_websocket_url: None,
experimental_unified_exec_exec_server_workspace_root: None,
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
features: Features::with_defaults().into(),
Expand Down Expand Up @@ -4733,6 +4739,8 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
web_search_config: None,
use_experimental_unified_exec_tool: !cfg!(windows),
experimental_unified_exec_exec_server_websocket_url: None,
experimental_unified_exec_exec_server_workspace_root: None,
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
features: Features::with_defaults().into(),
Expand Down
31 changes: 31 additions & 0 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,14 @@ pub struct Config {
/// If set to `true`, used only the experimental unified exec tool.
pub use_experimental_unified_exec_tool: bool,

/// When set, connect unified-exec process launches to an existing remote
/// `codex-exec-server` websocket endpoint.
pub experimental_unified_exec_exec_server_websocket_url: Option<String>,

/// When set, map local session cwd to this executor-visible workspace root
/// for remote unified-exec process launches.
pub experimental_unified_exec_exec_server_workspace_root: Option<AbsolutePathBuf>,

/// Maximum poll window for background terminal output (`write_stdin`), in milliseconds.
/// Default: `300000` (5 minutes).
pub background_terminal_max_timeout: u64,
Expand Down Expand Up @@ -1325,6 +1333,13 @@ pub struct ConfigToml {
/// Default: `300000` (5 minutes).
pub background_terminal_max_timeout: Option<u64>,

/// Optional websocket URL for connecting to an existing `codex-exec-server`.
pub experimental_unified_exec_exec_server_websocket_url: Option<String>,

/// Optional absolute path to the executor-visible workspace root that
/// corresponds to the local session cwd.
pub experimental_unified_exec_exec_server_workspace_root: Option<AbsolutePathBuf>,

/// Optional absolute path to the Node runtime used by `js_repl`.
pub js_repl_node_path: Option<AbsolutePathBuf>,

Expand Down Expand Up @@ -2474,6 +2489,20 @@ impl Config {

let include_apply_patch_tool_flag = features.enabled(Feature::ApplyPatchFreeform);
let use_experimental_unified_exec_tool = features.enabled(Feature::UnifiedExec);
let experimental_unified_exec_exec_server_websocket_url = cfg
.experimental_unified_exec_exec_server_websocket_url
.clone()
.and_then(|value: String| {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
});
let experimental_unified_exec_exec_server_workspace_root = cfg
.experimental_unified_exec_exec_server_workspace_root
.clone();

let forced_chatgpt_workspace_id =
cfg.forced_chatgpt_workspace_id.as_ref().and_then(|value| {
Expand Down Expand Up @@ -2768,6 +2797,8 @@ impl Config {
web_search_mode: constrained_web_search_mode.value,
web_search_config,
use_experimental_unified_exec_tool,
experimental_unified_exec_exec_server_websocket_url,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be exec_server_url

experimental_unified_exec_exec_server_workspace_root,
background_terminal_max_timeout,
ghost_snapshot,
features,
Expand Down
1 change: 0 additions & 1 deletion codex-rs/core/src/tools/handlers/view_image.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use async_trait::async_trait;
use codex_environment::ExecutorFileSystem;
use codex_protocol::models::ContentItem;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::ImageDetail;
Expand Down
9 changes: 8 additions & 1 deletion codex-rs/core/src/tools/runtimes/unified_exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ use std::path::PathBuf;

#[derive(Clone, Debug)]
pub struct UnifiedExecRequest {
pub process_id: i32,
pub command: Vec<String>,
pub cwd: PathBuf,
pub env: HashMap<String, String>,
Expand Down Expand Up @@ -239,6 +240,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
return self
.manager
.open_session_with_exec_env(
req.process_id,
&prepared.exec_request,
req.tty,
prepared.spawn_lifecycle,
Expand Down Expand Up @@ -275,7 +277,12 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
.env_for(spec, req.network.as_ref())
.map_err(|err| ToolError::Codex(err.into()))?;
self.manager
.open_session_with_exec_env(&exec_env, req.tty, Box::new(NoopSpawnLifecycle))
.open_session_with_exec_env(
req.process_id,
&exec_env,
req.tty,
Box::new(NoopSpawnLifecycle),
)
.await
.map_err(|err| match err {
UnifiedExecError::SandboxDenied { output, .. } => {
Expand Down
116 changes: 116 additions & 0 deletions codex-rs/core/src/unified_exec/backend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use std::path::PathBuf;

use codex_exec_server::ExecServerClient;
use codex_exec_server::RemoteExecServerConnectArgs;

use crate::config::Config;
use crate::exec::SandboxType;
use crate::sandboxing::ExecRequest;
use crate::unified_exec::RemoteExecServerFileSystem;
use crate::unified_exec::SpawnLifecycleHandle;
use crate::unified_exec::UnifiedExecError;
use crate::unified_exec::UnifiedExecProcess;

#[derive(Clone)]
pub(crate) struct RemoteExecServerBackend {
client: ExecServerClient,
local_workspace_root: PathBuf,
remote_workspace_root: Option<PathBuf>,
}

impl std::fmt::Debug for RemoteExecServerBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RemoteExecServerBackend")
.field("local_workspace_root", &self.local_workspace_root)
.field("remote_workspace_root", &self.remote_workspace_root)
.finish_non_exhaustive()
}
}

impl RemoteExecServerBackend {
pub(crate) async fn connect_for_config(
config: &Config,
) -> Result<Option<Self>, UnifiedExecError> {
let Some(websocket_url) = config
.experimental_unified_exec_exec_server_websocket_url
.clone()
else {
return Ok(None);
};

let client = ExecServerClient::connect_websocket(RemoteExecServerConnectArgs::new(
websocket_url,
"codex-core".to_string(),
))
.await
.map_err(|err| UnifiedExecError::create_process(err.to_string()))?;

Ok(Some(Self {
client,
local_workspace_root: config.cwd.clone(),
remote_workspace_root: config
.experimental_unified_exec_exec_server_workspace_root
.clone()
.map(PathBuf::from),
}))
}

pub(crate) async fn open_session(
&self,
process_id: i32,
env: &ExecRequest,
tty: bool,
spawn_lifecycle: SpawnLifecycleHandle,
) -> Result<UnifiedExecProcess, UnifiedExecError> {
if !spawn_lifecycle.inherited_fds().is_empty() {
return Err(UnifiedExecError::create_process(
"remote exec-server mode does not support inherited file descriptors".to_string(),
));
}

if env.sandbox != SandboxType::None {
return Err(UnifiedExecError::create_process(format!(
"remote exec-server mode does not support sandboxed execution yet: {:?}",
env.sandbox
)));
}

let remote_cwd = self.map_remote_cwd(env.cwd.as_path())?;
UnifiedExecProcess::from_exec_server(
self.client.clone(),
process_id,
env,
remote_cwd,
tty,
spawn_lifecycle,
)
.await
}

pub(crate) fn file_system(&self) -> RemoteExecServerFileSystem {
RemoteExecServerFileSystem::new(self.client.clone())
}

fn map_remote_cwd(&self, local_cwd: &std::path::Path) -> Result<PathBuf, UnifiedExecError> {
let Some(remote_root) = self.remote_workspace_root.as_ref() else {
if local_cwd == self.local_workspace_root.as_path() {
return Ok(PathBuf::from("."));
}
return Err(UnifiedExecError::create_process(format!(
"remote exec-server mode needs `experimental_unified_exec_exec_server_workspace_root` for non-root cwd `{}`",
local_cwd.display()
)));
};

let relative =
UnifiedExecProcess::relative_cwd_under(local_cwd, &self.local_workspace_root)
.ok_or_else(|| {
UnifiedExecError::create_process(format!(
"cwd `{}` is not under local workspace root `{}`",
local_cwd.display(),
self.local_workspace_root.display()
))
})?;
Ok(remote_root.join(relative))
}
}
Loading
Loading