mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Run code mode exclusively through the standalone host (#36217)
## What changed - Move the V8 implementation into a dedicated `codex-code-mode-runtime` crate used by `codex-code-mode-host`, removing the embedded runtime fallback from the Codex process. - Resolve the host executable from the active installation layout and check its availability before selecting tools. - Fall back to direct tools with a one-time warning when optional code mode is unavailable. Keep `code_mode_only` and `disable_in_process_fallback` configurations fail-closed. ## Testing - Cover host discovery for standalone and package layouts, including missing hosts and symlinks. - Verify direct-tool fallback, one-time warnings, and fail-closed code-mode-only behavior. GitOrigin-RevId: 5aa3c6f1db148b2231fc24089a2ee0e2b00dbddb
This commit is contained in:
committed by
copyberry
parent
acd540f158
commit
97576b1794
Generated
+18
-3
@@ -2538,17 +2538,15 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-code-mode-protocol",
|
||||
"codex-http-client",
|
||||
"codex-install-context",
|
||||
"codex-protocol",
|
||||
"codex-websocket-client",
|
||||
"deno_core_icudata",
|
||||
"futures",
|
||||
"pretty_assertions",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"v8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2560,6 +2558,7 @@ dependencies = [
|
||||
"clap",
|
||||
"codex-code-mode",
|
||||
"codex-code-mode-protocol",
|
||||
"codex-code-mode-runtime",
|
||||
"codex-protocol",
|
||||
"codex-utils-cargo-bin",
|
||||
"futures",
|
||||
@@ -2585,6 +2584,22 @@ dependencies = [
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-code-mode-runtime"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-code-mode-protocol",
|
||||
"codex-protocol",
|
||||
"deno_core_icudata",
|
||||
"futures",
|
||||
"pretty_assertions",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"v8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-collaboration-mode-templates"
|
||||
version = "0.0.0"
|
||||
|
||||
@@ -24,6 +24,7 @@ members = [
|
||||
"code-mode",
|
||||
"code-mode-host",
|
||||
"code-mode-protocol",
|
||||
"code-mode-runtime",
|
||||
"codex-home",
|
||||
"cloud-config",
|
||||
"cloud-tasks",
|
||||
@@ -168,6 +169,7 @@ codex-cloud-tasks-client = { path = "cloud-tasks-client" }
|
||||
codex-cloud-tasks-mock-client = { path = "cloud-tasks-mock-client" }
|
||||
codex-code-mode = { path = "code-mode" }
|
||||
codex-code-mode-protocol = { path = "code-mode-protocol" }
|
||||
codex-code-mode-runtime = { path = "code-mode-runtime" }
|
||||
codex-home = { path = "codex-home" }
|
||||
codex-http-client = { path = "http-client" }
|
||||
codex-websocket-client = { path = "websocket-client" }
|
||||
|
||||
@@ -157,13 +157,13 @@ pub struct TestAppServer {
|
||||
// removing an owned CODEX_HOME that may still be its cwd on Windows.
|
||||
_delayed_exec_server: Option<(LocalWebsocketExecServer, WebsocketDelayInterposer)>,
|
||||
_attribution_settings_server: Option<MockServer>,
|
||||
_owned_install_dir: Option<TempDir>,
|
||||
_owned_codex_home: Option<TempDir>,
|
||||
}
|
||||
|
||||
pub const DEFAULT_CLIENT_NAME: &str = "codex-app-server-tests";
|
||||
pub const DISABLE_PLUGIN_STARTUP_TASKS_ARG: &str = "--disable-plugin-startup-tasks-for-tests";
|
||||
const DISABLE_MANAGED_CONFIG_ENV_VAR: &str = "CODEX_APP_SERVER_DISABLE_MANAGED_CONFIG";
|
||||
const CODE_MODE_HOST_PATH_ENV_VAR: &str = "CODEX_CODE_MODE_HOST_PATH";
|
||||
#[cfg(windows)]
|
||||
const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(25);
|
||||
#[cfg(not(windows))]
|
||||
@@ -293,6 +293,7 @@ impl TestAppServer {
|
||||
json_logs,
|
||||
_delayed_exec_server: None,
|
||||
_attribution_settings_server: None,
|
||||
_owned_install_dir: None,
|
||||
_owned_codex_home: None,
|
||||
})
|
||||
}
|
||||
@@ -1997,25 +1998,42 @@ impl TestAppServerBuilder {
|
||||
(None, None)
|
||||
}
|
||||
};
|
||||
if !env_overrides
|
||||
.iter()
|
||||
.any(|(key, _)| key == CODE_MODE_HOST_PATH_ENV_VAR)
|
||||
&& let Ok(code_mode_host_program) =
|
||||
codex_utils_cargo_bin::cargo_bin("codex-code-mode-host")
|
||||
{
|
||||
env_overrides.insert(
|
||||
0,
|
||||
(
|
||||
CODE_MODE_HOST_PATH_ENV_VAR.to_string(),
|
||||
Some(code_mode_host_program.to_string_lossy().into_owned()),
|
||||
),
|
||||
);
|
||||
}
|
||||
let program = match program {
|
||||
let custom_program = program.is_some();
|
||||
let mut program = match program {
|
||||
Some(program) => program,
|
||||
None => codex_utils_cargo_bin::cargo_bin("codex-app-server")
|
||||
.context("should find binary for codex-app-server")?,
|
||||
};
|
||||
let mut owned_install_dir = None;
|
||||
if !custom_program
|
||||
&& codex_utils_cargo_bin::runfiles_available()
|
||||
&& let Ok(code_mode_host_program) =
|
||||
codex_utils_cargo_bin::cargo_bin("codex-code-mode-host")
|
||||
{
|
||||
// Bazel keeps binary targets in separate package directories.
|
||||
// Recreate the installed sibling layout without a path override.
|
||||
let install_dir = TempDir::new()?;
|
||||
let staged_program = install_dir.path().join(
|
||||
program
|
||||
.file_name()
|
||||
.context("app-server executable should have a filename")?,
|
||||
);
|
||||
let staged_host = install_dir.path().join(
|
||||
code_mode_host_program
|
||||
.file_name()
|
||||
.context("code-mode host executable should have a filename")?,
|
||||
);
|
||||
for (source, destination) in [
|
||||
(&program, &staged_program),
|
||||
(&code_mode_host_program, &staged_host),
|
||||
] {
|
||||
std::fs::hard_link(source, destination)
|
||||
.or_else(|_| std::fs::copy(source, destination).map(|_| ()))
|
||||
.with_context(|| format!("stage executable {}", source.display()))?;
|
||||
}
|
||||
program = staged_program;
|
||||
owned_install_dir = Some(install_dir);
|
||||
}
|
||||
let env_overrides = env_overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_deref()))
|
||||
@@ -2029,6 +2047,7 @@ impl TestAppServerBuilder {
|
||||
)
|
||||
.await?;
|
||||
app_server.auto_env = auto_env;
|
||||
app_server._owned_install_dir = owned_install_dir;
|
||||
app_server._owned_codex_home = owned_codex_home;
|
||||
app_server._delayed_exec_server = delayed_exec_server;
|
||||
app_server._attribution_settings_server = attribution_settings_server;
|
||||
|
||||
@@ -20,8 +20,8 @@ workspace = true
|
||||
anyhow = { workspace = true }
|
||||
axum = { workspace = true, features = ["http1", "tokio", "ws"] }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-code-mode = { workspace = true }
|
||||
codex-code-mode-protocol = { workspace = true }
|
||||
codex-code-mode-runtime = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
tokio = { workspace = true, features = ["io-std", "io-util", "macros", "net", "process", "rt", "sync", "time"] }
|
||||
tokio-util = { workspace = true, features = ["rt"] }
|
||||
@@ -29,6 +29,7 @@ tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
codex-code-mode = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
@@ -10,7 +10,6 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use codex_code_mode::InProcessCodeModeSession;
|
||||
use codex_code_mode_protocol::host::CapabilitySet;
|
||||
use codex_code_mode_protocol::host::ClientToHost;
|
||||
use codex_code_mode_protocol::host::EncodedFrame;
|
||||
@@ -23,6 +22,7 @@ use codex_code_mode_protocol::host::ProtocolVersion;
|
||||
use codex_code_mode_protocol::host::RequestId;
|
||||
use codex_code_mode_protocol::host::SessionId;
|
||||
use codex_code_mode_protocol::host::SupportedProtocolVersions;
|
||||
use codex_code_mode_runtime::InProcessCodeModeSession;
|
||||
use tokio::io::AsyncRead;
|
||||
use tokio::io::AsyncWrite;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
@@ -38,6 +38,7 @@ pub use session::CodeModeSessionDelegate;
|
||||
pub use session::CodeModeSessionProvider;
|
||||
pub use session::CodeModeSessionProviderFuture;
|
||||
pub use session::CodeModeSessionResultFuture;
|
||||
pub use session::NoopCodeModeSessionDelegate;
|
||||
pub use session::NotificationFuture;
|
||||
pub use session::StartedCell;
|
||||
pub use session::ToolInvocationFuture;
|
||||
|
||||
@@ -104,6 +104,34 @@ pub trait CodeModeSessionDelegate: Send + Sync {
|
||||
fn cell_closed(&self, cell_id: &CellId);
|
||||
}
|
||||
|
||||
/// A session delegate for clients that do not expose nested tools or notifications.
|
||||
pub struct NoopCodeModeSessionDelegate;
|
||||
|
||||
impl CodeModeSessionDelegate for NoopCodeModeSessionDelegate {
|
||||
fn invoke_tool<'a>(
|
||||
&'a self,
|
||||
_invocation: CodeModeNestedToolCall,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> ToolInvocationFuture<'a> {
|
||||
Box::pin(async move {
|
||||
cancellation_token.cancelled().await;
|
||||
Err("code mode nested tools are unavailable".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
fn notify<'a>(
|
||||
&'a self,
|
||||
_call_id: String,
|
||||
_cell_id: CellId,
|
||||
_text: String,
|
||||
_cancellation_token: CancellationToken,
|
||||
) -> NotificationFuture<'a> {
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn cell_closed(&self, _cell_id: &CellId) {}
|
||||
}
|
||||
|
||||
/// A durable code-mode session owned by one Codex thread.
|
||||
///
|
||||
/// Cells executed in the same session share stored values. Separate sessions
|
||||
@@ -127,6 +155,11 @@ pub trait CodeModeSession: Send + Sync {
|
||||
/// Implementations may share a remote host process across all sessions created
|
||||
/// by one provider.
|
||||
pub trait CodeModeSessionProvider: Send + Sync {
|
||||
/// Reports whether this provider can execute code without starting its host.
|
||||
fn availability(&self) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_session<'a>(
|
||||
&'a self,
|
||||
delegate: Arc<dyn CodeModeSessionDelegate>,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "code-mode-runtime",
|
||||
crate_name = "codex_code_mode_runtime",
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
name = "codex-code-mode-runtime"
|
||||
version.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
name = "codex_code_mode_runtime"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
sandbox = ["v8/v8_enable_sandbox"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codex-code-mode-protocol = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
deno_core_icudata = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] }
|
||||
tokio-util = { workspace = true, features = ["rt"] }
|
||||
tracing = { workspace = true }
|
||||
v8 = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
tokio = { workspace = true, features = ["test-util"] }
|
||||
@@ -0,0 +1,12 @@
|
||||
mod cell_actor;
|
||||
mod runtime;
|
||||
mod service;
|
||||
mod session_runtime;
|
||||
mod v8_init;
|
||||
|
||||
pub(crate) type TaskFailureHandler = std::sync::Arc<dyn Fn(String) + Send + Sync>;
|
||||
|
||||
pub use codex_code_mode_protocol::*;
|
||||
pub use service::InProcessCodeModeSession;
|
||||
pub use v8_init::V8JitMode;
|
||||
pub use v8_init::initialize_v8;
|
||||
@@ -5,8 +5,6 @@ use codex_code_mode_protocol::CellId;
|
||||
use codex_code_mode_protocol::CodeModeNestedToolCall;
|
||||
use codex_code_mode_protocol::CodeModeSession;
|
||||
use codex_code_mode_protocol::CodeModeSessionDelegate;
|
||||
use codex_code_mode_protocol::CodeModeSessionProvider;
|
||||
use codex_code_mode_protocol::CodeModeSessionProviderFuture;
|
||||
use codex_code_mode_protocol::CodeModeSessionResultFuture;
|
||||
use codex_code_mode_protocol::CodeModeToolKind;
|
||||
use codex_code_mode_protocol::DEFAULT_EXEC_YIELD_TIME_MS;
|
||||
@@ -14,10 +12,9 @@ use codex_code_mode_protocol::ExecuteRequest;
|
||||
use codex_code_mode_protocol::ExecuteToPendingOutcome;
|
||||
use codex_code_mode_protocol::FunctionCallOutputContentItem;
|
||||
use codex_code_mode_protocol::ImageDetail;
|
||||
use codex_code_mode_protocol::NotificationFuture;
|
||||
use codex_code_mode_protocol::NoopCodeModeSessionDelegate;
|
||||
use codex_code_mode_protocol::RuntimeResponse;
|
||||
use codex_code_mode_protocol::StartedCell;
|
||||
use codex_code_mode_protocol::ToolInvocationFuture;
|
||||
use codex_code_mode_protocol::WaitOutcome;
|
||||
use codex_code_mode_protocol::WaitRequest;
|
||||
use codex_code_mode_protocol::WaitToPendingOutcome;
|
||||
@@ -41,49 +38,6 @@ fn yield_timeout(yield_time_ms: u64) -> Duration {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NoopCodeModeSessionDelegate;
|
||||
|
||||
impl CodeModeSessionDelegate for NoopCodeModeSessionDelegate {
|
||||
fn invoke_tool<'a>(
|
||||
&'a self,
|
||||
_invocation: CodeModeNestedToolCall,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> ToolInvocationFuture<'a> {
|
||||
Box::pin(async move {
|
||||
cancellation_token.cancelled().await;
|
||||
Err("code mode nested tools are unavailable".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
fn notify<'a>(
|
||||
&'a self,
|
||||
_call_id: String,
|
||||
_cell_id: CellId,
|
||||
_text: String,
|
||||
_cancellation_token: CancellationToken,
|
||||
) -> NotificationFuture<'a> {
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn cell_closed(&self, _cell_id: &CellId) {}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct InProcessCodeModeSessionProvider;
|
||||
|
||||
impl CodeModeSessionProvider for InProcessCodeModeSessionProvider {
|
||||
fn create_session<'a>(
|
||||
&'a self,
|
||||
delegate: Arc<dyn CodeModeSessionDelegate>,
|
||||
) -> CodeModeSessionProviderFuture<'a> {
|
||||
Box::pin(async move {
|
||||
let session: Arc<dyn CodeModeSession> =
|
||||
Arc::new(InProcessCodeModeSession::with_delegate(delegate));
|
||||
Ok(session)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InProcessCodeModeSession {
|
||||
runtime: SessionRuntime<ProtocolDelegate>,
|
||||
}
|
||||
+2
@@ -3,6 +3,8 @@ use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_code_mode_protocol::NotificationFuture;
|
||||
use codex_code_mode_protocol::ToolInvocationFuture;
|
||||
use codex_protocol::ToolName;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tokio::sync::Notify;
|
||||
+2
-2
@@ -7,9 +7,7 @@ use super::CellId;
|
||||
use super::CodeModeNestedToolCall;
|
||||
use super::CodeModeSessionDelegate;
|
||||
use super::InProcessCodeModeSession;
|
||||
use super::NotificationFuture;
|
||||
use super::RuntimeResponse;
|
||||
use super::ToolInvocationFuture;
|
||||
use super::WaitOutcome;
|
||||
use super::WaitRequest;
|
||||
use super::WaitToPendingOutcome;
|
||||
@@ -20,6 +18,8 @@ use crate::ExecuteRequest;
|
||||
use crate::ExecuteToPendingOutcome;
|
||||
use crate::FunctionCallOutputContentItem;
|
||||
use crate::ToolDefinition;
|
||||
use codex_code_mode_protocol::NotificationFuture;
|
||||
use codex_code_mode_protocol::ToolInvocationFuture;
|
||||
use codex_protocol::ToolName;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value as JsonValue;
|
||||
@@ -1,8 +1,8 @@
|
||||
use codex_code_mode::ExecuteRequest;
|
||||
use codex_code_mode::InProcessCodeModeSession;
|
||||
use codex_code_mode::RuntimeResponse;
|
||||
use codex_code_mode::V8JitMode;
|
||||
use codex_code_mode::initialize_v8;
|
||||
use codex_code_mode_runtime::ExecuteRequest;
|
||||
use codex_code_mode_runtime::InProcessCodeModeSession;
|
||||
use codex_code_mode_runtime::RuntimeResponse;
|
||||
use codex_code_mode_runtime::V8JitMode;
|
||||
use codex_code_mode_runtime::initialize_v8;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
@@ -9,26 +9,21 @@ doctest = false
|
||||
name = "codex_code_mode"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
sandbox = ["v8/v8_enable_sandbox"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codex-code-mode-protocol = { workspace = true }
|
||||
codex-http-client = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-install-context = { workspace = true }
|
||||
codex-websocket-client = { workspace = true }
|
||||
deno_core_icudata = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["io-util", "macros", "net", "process", "rt", "sync", "time"] }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
tokio-util = { workspace = true, features = ["rt"] }
|
||||
tracing = { workspace = true }
|
||||
v8 = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
codex-protocol = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
tokio = { workspace = true, features = ["test-util"] }
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
mod cell_actor;
|
||||
mod remote_session;
|
||||
mod runtime;
|
||||
mod service;
|
||||
mod session_runtime;
|
||||
mod v8_init;
|
||||
|
||||
pub(crate) type TaskFailureHandler = std::sync::Arc<dyn Fn(String) + Send + Sync>;
|
||||
|
||||
pub use codex_code_mode_protocol::*;
|
||||
pub use remote_session::DisabledCodeModeSessionProvider;
|
||||
pub use remote_session::ProcessOwnedCodeModeSession;
|
||||
pub use remote_session::ProcessOwnedCodeModeSessionProvider;
|
||||
pub use remote_session::WebSocketCodeModeSessionProvider;
|
||||
pub use service::InProcessCodeModeSession;
|
||||
pub use service::InProcessCodeModeSessionProvider;
|
||||
pub use service::NoopCodeModeSessionDelegate;
|
||||
pub use v8_init::V8JitMode;
|
||||
pub use v8_init::initialize_v8;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::ffi::OsString;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
@@ -20,6 +19,7 @@ use codex_code_mode_protocol::WaitRequest;
|
||||
use codex_code_mode_protocol::host::SessionId;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_http_client::OutboundProxyPolicy;
|
||||
use codex_install_context::InstallContext;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::sync::watch;
|
||||
|
||||
@@ -31,87 +31,74 @@ use crate::NoopCodeModeSessionDelegate;
|
||||
|
||||
mod connection;
|
||||
|
||||
const CODE_MODE_HOST_PATH_ENV: &str = "CODEX_CODE_MODE_HOST_PATH";
|
||||
|
||||
type ShutdownResultReceiver = watch::Receiver<Option<Result<(), String>>>;
|
||||
|
||||
/// Creates code-mode sessions backed by one lazily spawned process host.
|
||||
pub struct ProcessOwnedCodeModeSessionProvider {
|
||||
state: StdMutex<ProviderState>,
|
||||
allow_in_process_fallback: bool,
|
||||
host: Arc<OwnedCodeModeHost>,
|
||||
}
|
||||
|
||||
/// Rejects code-mode sessions when the standalone host is disabled.
|
||||
#[derive(Default)]
|
||||
pub struct DisabledCodeModeSessionProvider;
|
||||
|
||||
/// Creates code-mode sessions backed by one shared remote WebSocket connection.
|
||||
pub struct WebSocketCodeModeSessionProvider {
|
||||
host: Arc<OwnedCodeModeHost>,
|
||||
}
|
||||
|
||||
enum ProviderState {
|
||||
OwnedProcess(Arc<OwnedCodeModeHost>),
|
||||
InProcess,
|
||||
}
|
||||
|
||||
impl ProcessOwnedCodeModeSessionProvider {
|
||||
pub fn with_host_program(host_program: PathBuf) -> Self {
|
||||
Self {
|
||||
state: StdMutex::new(ProviderState::OwnedProcess(Arc::new(
|
||||
OwnedCodeModeHost::new(host_program),
|
||||
))),
|
||||
allow_in_process_fallback: true,
|
||||
host: Arc::new(OwnedCodeModeHost::new(host_program)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn without_in_process_fallback(mut self) -> Self {
|
||||
self.allow_in_process_fallback = false;
|
||||
self
|
||||
}
|
||||
|
||||
fn process_host(&self) -> Option<Arc<OwnedCodeModeHost>> {
|
||||
match &*self
|
||||
.state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
{
|
||||
ProviderState::OwnedProcess(process_host) => Some(Arc::clone(process_host)),
|
||||
ProviderState::InProcess => None,
|
||||
}
|
||||
fn process_host(&self) -> Arc<OwnedCodeModeHost> {
|
||||
Arc::clone(&self.host)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProcessOwnedCodeModeSessionProvider {
|
||||
fn default() -> Self {
|
||||
Self::with_host_program(default_host_program())
|
||||
Self::with_host_program(InstallContext::current().code_mode_host_program())
|
||||
}
|
||||
}
|
||||
|
||||
impl CodeModeSessionProvider for ProcessOwnedCodeModeSessionProvider {
|
||||
fn availability(&self) -> Result<(), String> {
|
||||
let HostEndpoint::Process(host_program) = &self.host.endpoint else {
|
||||
unreachable!("a process-owned provider always has a process endpoint");
|
||||
};
|
||||
if host_program.is_file() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ConnectionError::Spawn {
|
||||
host_program: host_program.clone(),
|
||||
error: io::Error::new(io::ErrorKind::NotFound, "host executable was not found"),
|
||||
}
|
||||
.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_session<'a>(
|
||||
&'a self,
|
||||
delegate: Arc<dyn CodeModeSessionDelegate>,
|
||||
) -> CodeModeSessionProviderFuture<'a> {
|
||||
Box::pin(async move {
|
||||
let Some(process_host) = self.process_host() else {
|
||||
let session: Arc<dyn CodeModeSession> =
|
||||
Arc::new(crate::InProcessCodeModeSession::with_delegate(delegate));
|
||||
return Ok(session);
|
||||
};
|
||||
Box::pin(create_host_session(delegate, self.process_host()))
|
||||
}
|
||||
}
|
||||
|
||||
match process_host.connection().await {
|
||||
Ok(_) => {}
|
||||
Err(error) if error.host_program_not_found() && self.allow_in_process_fallback => {
|
||||
*self
|
||||
.state
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) =
|
||||
ProviderState::InProcess;
|
||||
let session: Arc<dyn CodeModeSession> =
|
||||
Arc::new(crate::InProcessCodeModeSession::with_delegate(delegate));
|
||||
return Ok(session);
|
||||
}
|
||||
Err(error) => return Err(error.to_string()),
|
||||
}
|
||||
create_host_session(delegate, process_host).await
|
||||
})
|
||||
impl CodeModeSessionProvider for DisabledCodeModeSessionProvider {
|
||||
fn availability(&self) -> Result<(), String> {
|
||||
Err("code-mode host is disabled".to_string())
|
||||
}
|
||||
|
||||
fn create_session<'a>(
|
||||
&'a self,
|
||||
_delegate: Arc<dyn CodeModeSessionDelegate>,
|
||||
) -> CodeModeSessionProviderFuture<'a> {
|
||||
Box::pin(async { Err("code-mode host is disabled".to_string()) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +260,9 @@ impl ProcessOwnedCodeModeSession {
|
||||
pub fn new() -> Self {
|
||||
Self::with_host(
|
||||
Arc::new(NoopCodeModeSessionDelegate),
|
||||
Arc::new(OwnedCodeModeHost::new(default_host_program())),
|
||||
Arc::new(OwnedCodeModeHost::new(
|
||||
InstallContext::current().code_mode_host_program(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -569,33 +558,6 @@ impl CodeModeSession for ProcessOwnedCodeModeSession {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_host_program() -> PathBuf {
|
||||
resolve_host_program(
|
||||
std::env::var_os(CODE_MODE_HOST_PATH_ENV),
|
||||
std::env::current_exe(),
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_host_program(
|
||||
override_path: Option<OsString>,
|
||||
current_exe: io::Result<PathBuf>,
|
||||
) -> PathBuf {
|
||||
if let Some(path) = override_path {
|
||||
return PathBuf::from(path);
|
||||
}
|
||||
let executable_name = if cfg!(windows) {
|
||||
"codex-code-mode-host.exe"
|
||||
} else {
|
||||
"codex-code-mode-host"
|
||||
};
|
||||
if let Ok(current_exe) = current_exe
|
||||
&& let Some(parent) = current_exe.parent()
|
||||
{
|
||||
return parent.join(executable_name);
|
||||
}
|
||||
PathBuf::from(executable_name)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "remote_session_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -71,15 +71,6 @@ pub(super) enum ConnectionError {
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl ConnectionError {
|
||||
pub(super) fn host_program_not_found(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Spawn { error, .. } if error.kind() == io::ErrorKind::NotFound
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ConnectionError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
|
||||
@@ -33,67 +33,18 @@ use super::ProcessOwnedCodeModeSession;
|
||||
use super::ProcessOwnedCodeModeSessionProvider;
|
||||
use super::WebSocketCodeModeSessionProvider;
|
||||
use super::connection::ConnectionError;
|
||||
use super::resolve_host_program;
|
||||
use crate::NoopCodeModeSessionDelegate;
|
||||
|
||||
#[test]
|
||||
fn provider_reuses_its_live_process_host() {
|
||||
let provider = ProcessOwnedCodeModeSessionProvider::default();
|
||||
|
||||
let first = provider.process_host().expect("owned process host");
|
||||
let second = provider.process_host().expect("owned process host");
|
||||
let first = provider.process_host();
|
||||
let second = provider.process_host();
|
||||
|
||||
assert!(Arc::ptr_eq(&first, &second));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_program_override_takes_precedence() {
|
||||
assert_eq!(
|
||||
resolve_host_program(
|
||||
Some("custom-code-mode-host".into()),
|
||||
Ok(PathBuf::from("/opt/codex/bin/codex")),
|
||||
),
|
||||
PathBuf::from("custom-code-mode-host")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_program_is_next_to_the_main_executable_even_when_missing() {
|
||||
let executable_name = if cfg!(windows) {
|
||||
"codex-code-mode-host.exe"
|
||||
} else {
|
||||
"codex-code-mode-host"
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
resolve_host_program(
|
||||
/*override_path*/ None,
|
||||
Ok(PathBuf::from("/opt/codex/bin/codex")),
|
||||
),
|
||||
PathBuf::from("/opt/codex/bin").join(executable_name)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_program_falls_back_to_its_name_when_main_executable_is_unknown() {
|
||||
let executable_name = if cfg!(windows) {
|
||||
"codex-code-mode-host.exe"
|
||||
} else {
|
||||
"codex-code-mode-host"
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
resolve_host_program(
|
||||
/*override_path*/ None,
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"missing executable"
|
||||
)),
|
||||
),
|
||||
PathBuf::from(executable_name)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_host_error_limits_the_displayed_path_to_512_bytes() {
|
||||
let executable = "codex-code-mode-host-does-not-exist";
|
||||
@@ -129,42 +80,6 @@ fn missing_host_error_preserves_utf8_boundaries_when_truncating_the_path() {
|
||||
assert!(displayed_path.len() <= 512);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn provider_falls_back_to_in_process_session_when_host_is_missing() {
|
||||
let provider = ProcessOwnedCodeModeSessionProvider::with_host_program(
|
||||
"codex-code-mode-host-does-not-exist".into(),
|
||||
);
|
||||
|
||||
let session = provider
|
||||
.create_session(Arc::new(NoopCodeModeSessionDelegate))
|
||||
.await
|
||||
.expect("missing host should fall back to an in-process session");
|
||||
let response = session
|
||||
.execute(ExecuteRequest {
|
||||
tool_call_id: "call-1".to_string(),
|
||||
enabled_tools: Vec::new(),
|
||||
source: "text('fallback')".to_string(),
|
||||
yield_time_ms: None,
|
||||
max_output_tokens: None,
|
||||
})
|
||||
.await
|
||||
.expect("execute fallback session")
|
||||
.initial_response()
|
||||
.await
|
||||
.expect("read fallback response");
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
RuntimeResponse::Result {
|
||||
cell_id: codex_code_mode_protocol::CellId::new("1".to_string()),
|
||||
content_items: vec![FunctionCallOutputContentItem::InputText {
|
||||
text: "fallback".to_string(),
|
||||
}],
|
||||
error_text: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_provider_executes_over_shared_connector() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
@@ -310,20 +225,18 @@ async fn websocket_provider_executes_over_shared_connector() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn provider_returns_missing_host_error_when_in_process_fallback_is_disabled() {
|
||||
async fn provider_returns_missing_host_error() {
|
||||
let provider = ProcessOwnedCodeModeSessionProvider::with_host_program(
|
||||
"codex-code-mode-host-does-not-exist".into(),
|
||||
)
|
||||
.without_in_process_fallback();
|
||||
);
|
||||
|
||||
let error = provider
|
||||
.create_session(Arc::new(NoopCodeModeSessionDelegate))
|
||||
.await
|
||||
.err()
|
||||
.expect("missing host should fail when in-process fallback is disabled");
|
||||
.expect("missing host should fail");
|
||||
|
||||
assert!(error.contains("failed to spawn code-mode host codex-code-mode-host-does-not-exist"));
|
||||
assert!(provider.process_host().is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -389,7 +389,7 @@
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"disable_in_process_fallback": {
|
||||
"description": "Fail instead of running embedded V8 when the standalone host is unavailable.",
|
||||
"description": "Keep code mode fail-closed when the standalone host is unavailable.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled": {
|
||||
|
||||
@@ -1096,6 +1096,7 @@ pub struct Config {
|
||||
pub struct CodeModeConfig {
|
||||
pub excluded_tool_namespaces: Vec<String>,
|
||||
pub direct_only_tool_namespaces: Vec<String>,
|
||||
/// Keep code mode fail-closed when the standalone host is unavailable.
|
||||
pub disable_in_process_fallback: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ pub(super) async fn spawn_review_thread(
|
||||
sub_id: review_turn_id.clone(),
|
||||
trace_id: current_span_trace_id(),
|
||||
realtime_active: parent_turn_context.realtime_active,
|
||||
code_mode_available: parent_turn_context.code_mode_available,
|
||||
config: per_turn_config,
|
||||
auth_manager: auth_manager_for_context,
|
||||
model_info: model_info.clone(),
|
||||
|
||||
@@ -5579,7 +5579,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
|
||||
skills_service,
|
||||
plugins_manager,
|
||||
mcp_manager,
|
||||
Arc::new(codex_code_mode::InProcessCodeModeSessionProvider),
|
||||
Arc::new(codex_code_mode::DisabledCodeModeSessionProvider),
|
||||
Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
|
||||
codex_extension_api::ExtensionDataInit::default(),
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
@@ -5809,7 +5809,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
),
|
||||
executed_tool_calls,
|
||||
code_mode_service: crate::tools::code_mode::CodeModeService::new(
|
||||
Arc::new(codex_code_mode::InProcessCodeModeSessionProvider),
|
||||
Arc::new(codex_code_mode::DisabledCodeModeSessionProvider),
|
||||
&config.features,
|
||||
),
|
||||
tool_search_handler_cache: Default::default(),
|
||||
@@ -5985,7 +5985,7 @@ async fn make_session_with_config_and_rx(
|
||||
skills_service,
|
||||
plugins_manager,
|
||||
mcp_manager,
|
||||
Arc::new(codex_code_mode::InProcessCodeModeSessionProvider),
|
||||
Arc::new(codex_code_mode::DisabledCodeModeSessionProvider),
|
||||
Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
|
||||
codex_extension_api::ExtensionDataInit::default(),
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
@@ -6095,7 +6095,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
skills_service,
|
||||
plugins_manager,
|
||||
mcp_manager,
|
||||
Arc::new(codex_code_mode::InProcessCodeModeSessionProvider),
|
||||
Arc::new(codex_code_mode::DisabledCodeModeSessionProvider),
|
||||
Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
|
||||
codex_extension_api::ExtensionDataInit::default(),
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
@@ -7997,7 +7997,7 @@ where
|
||||
),
|
||||
executed_tool_calls,
|
||||
code_mode_service: crate::tools::code_mode::CodeModeService::new(
|
||||
Arc::new(codex_code_mode::InProcessCodeModeSessionProvider),
|
||||
Arc::new(codex_code_mode::DisabledCodeModeSessionProvider),
|
||||
&config.features,
|
||||
),
|
||||
tool_search_handler_cache: Default::default(),
|
||||
|
||||
@@ -743,7 +743,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
|
||||
skills_service,
|
||||
plugins_manager,
|
||||
mcp_manager,
|
||||
code_mode_session_provider: Arc::new(codex_code_mode::InProcessCodeModeSessionProvider),
|
||||
code_mode_session_provider: Arc::new(codex_code_mode::DisabledCodeModeSessionProvider),
|
||||
extensions: codex_extension_api::empty_extension_registry(),
|
||||
conversation_history: InitialHistory::New,
|
||||
requested_history_mode: None,
|
||||
|
||||
@@ -115,6 +115,7 @@ pub struct TurnContext {
|
||||
pub(crate) sub_id: String,
|
||||
pub(crate) trace_id: Option<String>,
|
||||
pub(crate) realtime_active: bool,
|
||||
pub(crate) code_mode_available: bool,
|
||||
pub config: Arc<Config>,
|
||||
pub(crate) auth_manager: Option<Arc<AuthManager>>,
|
||||
pub(crate) model_info: ModelInfo,
|
||||
@@ -280,6 +281,7 @@ impl TurnContext {
|
||||
sub_id: self.sub_id.clone(),
|
||||
trace_id: self.trace_id.clone(),
|
||||
realtime_active: self.realtime_active,
|
||||
code_mode_available: self.code_mode_available,
|
||||
config: Arc::new(config),
|
||||
auth_manager: self.auth_manager.clone(),
|
||||
model_info: model_info.clone(),
|
||||
@@ -551,6 +553,7 @@ impl Session {
|
||||
sub_id,
|
||||
trace_id: current_span_trace_id(),
|
||||
realtime_active: false,
|
||||
code_mode_available: true,
|
||||
config: per_turn_config,
|
||||
auth_manager: auth_manager_for_context,
|
||||
model_info,
|
||||
@@ -798,6 +801,7 @@ impl Session {
|
||||
sub_id,
|
||||
skills_snapshot,
|
||||
);
|
||||
turn_context.code_mode_available = self.services.code_mode_service.is_available();
|
||||
turn_context.extension_data.insert(trusted_plugin_roots);
|
||||
turn_context.realtime_active = self.conversation.running_state().await.is_some();
|
||||
|
||||
@@ -830,6 +834,21 @@ impl Session {
|
||||
.await;
|
||||
}
|
||||
|
||||
if !tc.code_mode_available
|
||||
&& matches!(
|
||||
crate::tools::requested_tool_mode(tc),
|
||||
codex_protocol::openai_models::ToolMode::CodeMode
|
||||
| codex_protocol::openai_models::ToolMode::CodeModeOnly
|
||||
)
|
||||
&& let Some(message) = self
|
||||
.services
|
||||
.code_mode_service
|
||||
.take_unavailable_warning(crate::tools::effective_tool_mode(tc))
|
||||
{
|
||||
self.send_event(tc, EventMsg::Warning(WarningEvent { message }))
|
||||
.await;
|
||||
}
|
||||
|
||||
if let Some(message) =
|
||||
unsupported_code_mode_warning(&tc.model_info, tc.config.features.get())
|
||||
{
|
||||
|
||||
@@ -24,10 +24,8 @@ use codex_agent_graph_store::LocalAgentGraphStore;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_app_server_protocol::ThreadHistoryBuilder;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_code_mode::CodeModeSessionDelegate;
|
||||
use codex_code_mode::CodeModeSessionProvider;
|
||||
use codex_code_mode::CodeModeSessionProviderFuture;
|
||||
use codex_code_mode::InProcessCodeModeSessionProvider;
|
||||
use codex_code_mode::DisabledCodeModeSessionProvider;
|
||||
use codex_code_mode::ProcessOwnedCodeModeSessionProvider;
|
||||
use codex_core_plugins::PluginsManager;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
@@ -99,19 +97,6 @@ use tracing::warn;
|
||||
|
||||
const THREAD_CREATED_CHANNEL_CAPACITY: usize = 1024;
|
||||
|
||||
struct DisabledCodeModeSessionProvider;
|
||||
|
||||
impl CodeModeSessionProvider for DisabledCodeModeSessionProvider {
|
||||
fn create_session<'a>(
|
||||
&'a self,
|
||||
_delegate: Arc<dyn CodeModeSessionDelegate>,
|
||||
) -> CodeModeSessionProviderFuture<'a> {
|
||||
Box::pin(async {
|
||||
Err("code-mode host is disabled and in-process fallback is disabled".to_string())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Test-only override for enabling thread-manager behaviors used by integration
|
||||
/// tests.
|
||||
///
|
||||
@@ -392,17 +377,12 @@ impl ThreadManager {
|
||||
restriction_product,
|
||||
));
|
||||
let code_mode_session_provider: Arc<dyn CodeModeSessionProvider> =
|
||||
if config.features.enabled(Feature::CodeModeHost) {
|
||||
let provider = ProcessOwnedCodeModeSessionProvider::default();
|
||||
if config.code_mode.disable_in_process_fallback {
|
||||
Arc::new(provider.without_in_process_fallback())
|
||||
} else {
|
||||
Arc::new(provider)
|
||||
}
|
||||
} else if config.code_mode.disable_in_process_fallback {
|
||||
Arc::new(DisabledCodeModeSessionProvider)
|
||||
if config.features.enabled(Feature::CodeModeHost)
|
||||
|| config.code_mode.disable_in_process_fallback
|
||||
{
|
||||
Arc::new(ProcessOwnedCodeModeSessionProvider::default())
|
||||
} else {
|
||||
Arc::new(InProcessCodeModeSessionProvider)
|
||||
Arc::new(DisabledCodeModeSessionProvider)
|
||||
};
|
||||
Self {
|
||||
state: Arc::new(ThreadManagerState {
|
||||
@@ -447,17 +427,14 @@ impl ThreadManager {
|
||||
pub(crate) fn with_code_mode_host_program_for_tests(
|
||||
mut self,
|
||||
host_program: PathBuf,
|
||||
config: &Config,
|
||||
_config: &Config,
|
||||
) -> Self {
|
||||
let Some(state) = Arc::get_mut(&mut self.state) else {
|
||||
unreachable!("new thread manager state should not be shared");
|
||||
};
|
||||
let provider = ProcessOwnedCodeModeSessionProvider::with_host_program(host_program);
|
||||
state.code_mode_session_provider = if config.code_mode.disable_in_process_fallback {
|
||||
Arc::new(provider.without_in_process_fallback())
|
||||
} else {
|
||||
Arc::new(provider)
|
||||
};
|
||||
state.code_mode_session_provider = Arc::new(
|
||||
ProcessOwnedCodeModeSessionProvider::with_host_program(host_program),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -550,7 +527,7 @@ impl ThreadManager {
|
||||
skills_service,
|
||||
plugins_manager,
|
||||
mcp_manager,
|
||||
code_mode_session_provider: Arc::new(InProcessCodeModeSessionProvider),
|
||||
code_mode_session_provider: Arc::new(DisabledCodeModeSessionProvider),
|
||||
extensions: empty_extension_registry(),
|
||||
user_instructions_provider: Arc::new(
|
||||
crate::test_support::EmptyUserInstructionsProvider,
|
||||
|
||||
@@ -429,7 +429,7 @@ async fn code_mode_session_provider_is_shared_across_threads() {
|
||||
config.cwd = config.codex_home.abs();
|
||||
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
|
||||
|
||||
let provider: Arc<dyn CodeModeSessionProvider> = Arc::new(InProcessCodeModeSessionProvider);
|
||||
let provider: Arc<dyn CodeModeSessionProvider> = Arc::new(DisabledCodeModeSessionProvider);
|
||||
let manager = ThreadManager::with_models_provider_and_home_for_tests(
|
||||
CodexAuth::from_api_key("dummy"),
|
||||
config.model_provider.clone(),
|
||||
|
||||
@@ -76,9 +76,11 @@ pub(crate) struct ExecContext {
|
||||
pub(crate) struct CodeModeService {
|
||||
session: OnceCell<Arc<dyn CodeModeSession>>,
|
||||
session_provider: Arc<dyn CodeModeSessionProvider>,
|
||||
availability: Result<(), String>,
|
||||
dispatch_broker: Arc<CodeModeDispatchBroker>,
|
||||
default_exec_yield_time_override_ms: Option<u64>,
|
||||
shutting_down: AtomicBool,
|
||||
unavailable_warning_emitted: AtomicBool,
|
||||
}
|
||||
|
||||
impl CodeModeService {
|
||||
@@ -87,15 +89,38 @@ impl CodeModeService {
|
||||
features: &Features,
|
||||
) -> Self {
|
||||
let dispatch_broker = Arc::new(CodeModeDispatchBroker::new());
|
||||
let availability = session_provider.availability();
|
||||
Self {
|
||||
session: OnceCell::new(),
|
||||
session_provider,
|
||||
availability,
|
||||
dispatch_broker,
|
||||
default_exec_yield_time_override_ms: default_exec_yield_time_override_ms(features),
|
||||
shutting_down: AtomicBool::new(false),
|
||||
unavailable_warning_emitted: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_available(&self) -> bool {
|
||||
self.availability.is_ok()
|
||||
}
|
||||
|
||||
pub(crate) fn take_unavailable_warning(&self, tool_mode: ToolMode) -> Option<String> {
|
||||
let error = self.availability.as_ref().err()?;
|
||||
let behavior = match tool_mode {
|
||||
ToolMode::Direct => "Falling back to direct tools",
|
||||
ToolMode::CodeMode | ToolMode::CodeModeOnly => "Code mode will fail closed",
|
||||
};
|
||||
(!self
|
||||
.unavailable_warning_emitted
|
||||
.swap(true, Ordering::Relaxed))
|
||||
.then(|| {
|
||||
format!(
|
||||
"Code Mode is unavailable because {error}. {behavior}; enable `features.code_mode_host` and install `codex-code-mode-host`."
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn session_provider(&self) -> Arc<dyn CodeModeSessionProvider> {
|
||||
Arc::clone(&self.session_provider)
|
||||
}
|
||||
@@ -378,18 +403,10 @@ fn build_freeform_tool_payload(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::CodeModeService;
|
||||
use super::build_nested_tool_payload;
|
||||
use super::truncate_code_mode_result;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use codex_code_mode::CodeModeToolKind;
|
||||
use codex_code_mode::ExecuteRequest;
|
||||
use codex_code_mode::FunctionCallOutputContentItem as CodeModeOutputContentItem;
|
||||
use codex_code_mode::ProcessOwnedCodeModeSessionProvider;
|
||||
use codex_code_mode::RuntimeResponse;
|
||||
use codex_features::Features;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_tools::ToolName;
|
||||
use serde_json::json;
|
||||
@@ -460,40 +477,4 @@ mod tests {
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_process_host_falls_back_to_in_process_session() {
|
||||
let service = CodeModeService::new(
|
||||
Arc::new(ProcessOwnedCodeModeSessionProvider::with_host_program(
|
||||
"codex-code-mode-host-does-not-exist".into(),
|
||||
)),
|
||||
&Features::with_defaults(),
|
||||
);
|
||||
|
||||
let response = service
|
||||
.execute(ExecuteRequest {
|
||||
tool_call_id: "call-1".to_string(),
|
||||
enabled_tools: Vec::new(),
|
||||
source: "text('fallback')".to_string(),
|
||||
yield_time_ms: None,
|
||||
max_output_tokens: None,
|
||||
})
|
||||
.await
|
||||
.expect("missing host should fall back to an in-process session")
|
||||
.initial_response()
|
||||
.await
|
||||
.expect("read fallback response");
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
RuntimeResponse::Result {
|
||||
cell_id: codex_code_mode::CellId::new("1".to_string()),
|
||||
content_items: vec![CodeModeOutputContentItem::InputText {
|
||||
text: "fallback".to_string(),
|
||||
}],
|
||||
error_text: None,
|
||||
}
|
||||
);
|
||||
service.shutdown().await.expect("shutdown service");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ pub(crate) fn tool_user_shell_type(
|
||||
}
|
||||
}
|
||||
|
||||
fn effective_tool_mode(turn_context: &TurnContext) -> ToolMode {
|
||||
pub(crate) fn requested_tool_mode(turn_context: &TurnContext) -> ToolMode {
|
||||
turn_context.model_info.tool_mode.unwrap_or_else(|| {
|
||||
if turn_context.config.features.enabled(Feature::CodeModeOnly) {
|
||||
ToolMode::CodeModeOnly
|
||||
@@ -75,6 +75,18 @@ fn effective_tool_mode(turn_context: &TurnContext) -> ToolMode {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn effective_tool_mode(turn_context: &TurnContext) -> ToolMode {
|
||||
let requested_tool_mode = requested_tool_mode(turn_context);
|
||||
if !turn_context.code_mode_available
|
||||
&& requested_tool_mode == ToolMode::CodeMode
|
||||
&& !turn_context.config.code_mode.disable_in_process_fallback
|
||||
{
|
||||
ToolMode::Direct
|
||||
} else {
|
||||
requested_tool_mode
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the combined exec output for sending back to the model.
|
||||
/// Includes exit code and duration metadata; truncates large bodies safely.
|
||||
pub fn format_exec_output_for_model(
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::session::session::Session;
|
||||
use crate::session::step_context::StepContext;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::tools::code_mode::CodeModeService;
|
||||
use crate::tools::code_mode::CodeModeWaitHandler;
|
||||
use crate::tools::code_mode::WAIT_TOOL_NAME;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
@@ -59,6 +60,56 @@ impl ToolExecutor<ToolInvocation> for TestHandler {
|
||||
|
||||
impl CoreToolRuntime for TestHandler {}
|
||||
|
||||
struct MissingCellCodeModeSessionProvider;
|
||||
|
||||
impl codex_code_mode::CodeModeSessionProvider for MissingCellCodeModeSessionProvider {
|
||||
fn create_session<'a>(
|
||||
&'a self,
|
||||
_delegate: Arc<dyn codex_code_mode::CodeModeSessionDelegate>,
|
||||
) -> codex_code_mode::CodeModeSessionProviderFuture<'a> {
|
||||
Box::pin(async {
|
||||
Ok(Arc::new(MissingCellCodeModeSession) as Arc<dyn codex_code_mode::CodeModeSession>)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct MissingCellCodeModeSession;
|
||||
|
||||
impl codex_code_mode::CodeModeSession for MissingCellCodeModeSession {
|
||||
fn execute<'a>(
|
||||
&'a self,
|
||||
_request: codex_code_mode::ExecuteRequest,
|
||||
) -> codex_code_mode::CodeModeSessionResultFuture<'a, codex_code_mode::StartedCell> {
|
||||
Box::pin(async { Err("test session cannot execute cells".to_string()) })
|
||||
}
|
||||
|
||||
fn wait<'a>(
|
||||
&'a self,
|
||||
request: codex_code_mode::WaitRequest,
|
||||
) -> codex_code_mode::CodeModeSessionResultFuture<'a, codex_code_mode::WaitOutcome> {
|
||||
self.terminate(request.cell_id)
|
||||
}
|
||||
|
||||
fn terminate<'a>(
|
||||
&'a self,
|
||||
cell_id: codex_code_mode::CellId,
|
||||
) -> codex_code_mode::CodeModeSessionResultFuture<'a, codex_code_mode::WaitOutcome> {
|
||||
Box::pin(async move {
|
||||
Ok(codex_code_mode::WaitOutcome::MissingCell(
|
||||
codex_code_mode::RuntimeResponse::Result {
|
||||
error_text: Some(format!("exec cell {cell_id} not found")),
|
||||
cell_id,
|
||||
content_items: Vec::new(),
|
||||
},
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn shutdown<'a>(&'a self) -> codex_code_mode::CodeModeSessionResultFuture<'a, ()> {
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dispatch_lifecycle_trace_records_direct_and_code_mode_requesters() -> anyhow::Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
@@ -226,6 +277,10 @@ async fn dispatch_lifecycle_trace_records_incompatible_payload_failures() -> any
|
||||
async fn missing_code_mode_wait_traces_only_the_wait_tool_call() -> anyhow::Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
let (mut session, turn) = make_session_and_context().await;
|
||||
session.services.code_mode_service = CodeModeService::new(
|
||||
Arc::new(MissingCellCodeModeSessionProvider),
|
||||
&turn.config.features,
|
||||
);
|
||||
attach_test_trace(&mut session, &turn, temp.path())?;
|
||||
|
||||
let registry = ToolRegistry::with_handler_for_test(Arc::new(CodeModeWaitHandler));
|
||||
|
||||
@@ -232,8 +232,89 @@ async fn run_code_mode_turn_with_builder(
|
||||
Ok((test, second_mock))
|
||||
}
|
||||
|
||||
async fn run_unavailable_code_mode_turn(
|
||||
server: &MockServer,
|
||||
test: &TestCodex,
|
||||
) -> Result<(Value, Vec<String>)> {
|
||||
let response_mock = responses::mount_sse_once(
|
||||
server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "list available tools".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
loop {
|
||||
match wait_for_event(&test.codex, |_| true).await {
|
||||
EventMsg::Warning(warning) => warnings.push(warning.message),
|
||||
EventMsg::TurnComplete(_) => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((response_mock.single_request().body_json(), warnings))
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn missing_process_host_falls_back_to_in_process_code_mode() -> Result<()> {
|
||||
async fn missing_process_host_falls_back_to_direct_tools_and_warns_once() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mut builder = test_codex()
|
||||
.with_model("test-gpt-5.1-codex")
|
||||
.with_code_mode_host_program("codex-code-mode-host-does-not-exist".into())
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodeMode)
|
||||
.expect("code mode should be enabled");
|
||||
});
|
||||
let test = builder.build_with_auto_env(&server).await?;
|
||||
let (first_body, first_warnings) = run_unavailable_code_mode_turn(&server, &test).await?;
|
||||
let first_tools = tool_names(&first_body);
|
||||
assert!(
|
||||
first_tools
|
||||
.iter()
|
||||
.all(|name| name != "exec" && name != "wait"),
|
||||
"unavailable code mode must not expose code-mode tools: {first_tools:?}"
|
||||
);
|
||||
assert!(
|
||||
first_warnings.iter().any(|warning| {
|
||||
warning.contains("Code Mode is unavailable")
|
||||
&& warning.contains("codex-code-mode-host-does-not-exist")
|
||||
}),
|
||||
"missing host should produce an actionable warning: {first_warnings:?}"
|
||||
);
|
||||
|
||||
let (_, second_warnings) = run_unavailable_code_mode_turn(&server, &test).await?;
|
||||
assert!(
|
||||
second_warnings
|
||||
.iter()
|
||||
.all(|warning| !warning.contains("Code Mode is unavailable")),
|
||||
"host availability warning should be emitted once per thread: {second_warnings:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn missing_process_host_keeps_code_mode_only_and_fails_closed() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
@@ -243,26 +324,39 @@ async fn missing_process_host_falls_back_to_in_process_code_mode() -> Result<()>
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodeMode)
|
||||
.enable(Feature::CodeModeOnly)
|
||||
.expect("code mode should be enabled");
|
||||
});
|
||||
let (_test, follow_up_mock) =
|
||||
run_code_mode_turn_with_builder(&server, "Run code mode", "text('fallback')", builder)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
text_item(
|
||||
&custom_tool_output_items(&follow_up_mock.single_request(), "call-1"),
|
||||
/*index*/ 1,
|
||||
),
|
||||
"fallback"
|
||||
let (_test, follow_up_mock) = run_code_mode_turn_with_builder(
|
||||
&server,
|
||||
"Run required code mode",
|
||||
"text('unreachable')",
|
||||
builder,
|
||||
)
|
||||
.await?;
|
||||
let request = follow_up_mock.single_request();
|
||||
let tools = tool_names(&request.body_json());
|
||||
assert!(
|
||||
tools.iter().any(|name| name == "exec") && tools.iter().any(|name| name == "wait"),
|
||||
"code-mode-only must retain code-mode tools: {tools:?}"
|
||||
);
|
||||
assert!(
|
||||
tools
|
||||
.iter()
|
||||
.all(|name| { !matches!(name.as_str(), "shell" | "shell_command" | "exec_command") }),
|
||||
"code-mode-only must never expose direct shell tools: {tools:?}"
|
||||
);
|
||||
let (output, _) = custom_tool_output_body_and_success(&request, "call-1");
|
||||
assert!(
|
||||
output.contains("codex-code-mode-host-does-not-exist"),
|
||||
"code-mode-only must report the host failure: {output}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn missing_process_host_fails_when_in_process_fallback_is_disabled() -> Result<()> {
|
||||
async fn missing_process_host_fails_closed_when_direct_fallback_is_disabled() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
@@ -276,19 +370,30 @@ async fn missing_process_host_fails_when_in_process_fallback_is_disabled() -> Re
|
||||
.expect("code mode should be enabled");
|
||||
config.code_mode.disable_in_process_fallback = true;
|
||||
});
|
||||
let (_test, follow_up_mock) =
|
||||
run_code_mode_turn_with_builder(&server, "Run code mode", "text('unreachable')", builder)
|
||||
.await?;
|
||||
|
||||
let (output, _) =
|
||||
custom_tool_output_body_and_success(&follow_up_mock.single_request(), "call-1");
|
||||
assert!(output.contains("failed to spawn code-mode host codex-code-mode-host-does-not-exist"));
|
||||
let (_test, follow_up_mock) = run_code_mode_turn_with_builder(
|
||||
&server,
|
||||
"Run required code mode",
|
||||
"text('unreachable')",
|
||||
builder,
|
||||
)
|
||||
.await?;
|
||||
let request = follow_up_mock.single_request();
|
||||
let tools = tool_names(&request.body_json());
|
||||
assert!(
|
||||
tools.iter().any(|name| name == "exec") && tools.iter().any(|name| name == "wait"),
|
||||
"disabled fallback must retain code-mode tools: {tools:?}"
|
||||
);
|
||||
let (output, _) = custom_tool_output_body_and_success(&request, "call-1");
|
||||
assert!(
|
||||
output.contains("codex-code-mode-host-does-not-exist"),
|
||||
"disabled fallback must report the host failure: {output}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn disabled_process_host_fails_when_in_process_fallback_is_disabled() -> Result<()> {
|
||||
async fn disabled_process_host_with_fallback_disabled_attempts_the_host() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
@@ -305,44 +410,19 @@ async fn disabled_process_host_fails_when_in_process_fallback_is_disabled() -> R
|
||||
.expect("code-mode host should be disabled");
|
||||
config.code_mode.disable_in_process_fallback = true;
|
||||
});
|
||||
let (_test, follow_up_mock) =
|
||||
run_code_mode_turn_with_builder(&server, "Run code mode", "text('unreachable')", builder)
|
||||
.await?;
|
||||
|
||||
let (output, _) =
|
||||
custom_tool_output_body_and_success(&follow_up_mock.single_request(), "call-1");
|
||||
assert!(output.contains("code-mode host is disabled and in-process fallback is disabled"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn missing_process_host_error_is_bounded_when_in_process_fallback_is_disabled() -> Result<()>
|
||||
{
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let executable = "codex-code-mode-host-does-not-exist";
|
||||
let host_program = format!("{}{executable}", "missing-directory/".repeat(/*n*/ 64));
|
||||
let builder = test_codex()
|
||||
.with_model("test-gpt-5.1-codex")
|
||||
.with_code_mode_host_program(host_program.into())
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodeMode)
|
||||
.expect("code mode should be enabled");
|
||||
config.code_mode.disable_in_process_fallback = true;
|
||||
});
|
||||
let (_test, follow_up_mock) =
|
||||
run_code_mode_turn_with_builder(&server, "Run code mode", "text('unreachable')", builder)
|
||||
.await?;
|
||||
|
||||
let (output, _) =
|
||||
custom_tool_output_body_and_success(&follow_up_mock.single_request(), "call-1");
|
||||
assert!(output.contains("failed to spawn code-mode host ..."));
|
||||
assert!(output.contains(executable));
|
||||
assert!(output.len() <= 1024, "host error must remain bounded");
|
||||
let (_test, follow_up_mock) = run_code_mode_turn_with_builder(
|
||||
&server,
|
||||
"Run required code mode",
|
||||
"text('unreachable')",
|
||||
builder,
|
||||
)
|
||||
.await?;
|
||||
let request = follow_up_mock.single_request();
|
||||
let (output, _) = custom_tool_output_body_and_success(&request, "call-1");
|
||||
assert!(
|
||||
output.contains("failed to spawn code-mode host"),
|
||||
"disabled fallback must still attempt the standalone host: {output}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -231,6 +231,48 @@ async fn remote_tool_mode_selector_overrides_feature_flags() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn remote_code_mode_only_selector_fails_closed_when_host_is_disabled() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let mut model = remote_model("test-tool-mode-code-mode-only-host-disabled");
|
||||
model.tool_mode = Some(ToolMode::CodeModeOnly);
|
||||
let response = response_for_remote_model(model, |config| {
|
||||
config
|
||||
.features
|
||||
.disable(Feature::CodeModeHost)
|
||||
.expect("code-mode host should be disabled");
|
||||
})
|
||||
.await?;
|
||||
|
||||
let tools = tool_names(&response.body);
|
||||
assert!(
|
||||
tools
|
||||
.iter()
|
||||
.any(|name| name == codex_code_mode::PUBLIC_TOOL_NAME)
|
||||
&& tools
|
||||
.iter()
|
||||
.any(|name| name == codex_code_mode::WAIT_TOOL_NAME),
|
||||
"code-mode-only must retain code-mode tools: {tools:?}"
|
||||
);
|
||||
assert!(
|
||||
tools
|
||||
.iter()
|
||||
.all(|name| { !matches!(name.as_str(), "shell" | "shell_command" | "exec_command") }),
|
||||
"code-mode-only must never expose direct shell tools: {tools:?}"
|
||||
);
|
||||
assert!(
|
||||
response
|
||||
.warnings
|
||||
.iter()
|
||||
.any(|warning| warning.contains("Code mode will fail closed")),
|
||||
"code-mode-only should explain that it fails closed: {:?}",
|
||||
response.warnings
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn unsupported_code_mode_warning_is_emitted_each_turn() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -34,7 +34,7 @@ impl FeatureConfig for CodeModeConfigToml {
|
||||
pub struct CodeModeHostConfigToml {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
/// Fail instead of running embedded V8 when the standalone host is unavailable.
|
||||
/// Keep code mode fail-closed when the standalone host is unavailable.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub disable_in_process_fallback: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ pub enum Feature {
|
||||
// Experimental
|
||||
/// Record model-attempted tool calls in internal Responses metadata.
|
||||
ExecutedToolCallMetadata,
|
||||
/// Enable JavaScript code mode backed by the in-process V8 runtime.
|
||||
/// Enable JavaScript code mode backed by the standalone host process.
|
||||
CodeMode,
|
||||
/// Use a 30-second default yield timeout for code mode exec calls.
|
||||
CodeModeBufferedExec,
|
||||
|
||||
@@ -6,6 +6,11 @@ use std::sync::OnceLock;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
const BIN_DIRNAME: &str = "bin";
|
||||
const CODE_MODE_HOST_EXECUTABLE_NAME: &str = if cfg!(windows) {
|
||||
"codex-code-mode-host.exe"
|
||||
} else {
|
||||
"codex-code-mode-host"
|
||||
};
|
||||
const PACKAGE_METADATA_FILENAME: &str = "codex-package.json";
|
||||
const PATH_DIRNAME: &str = "codex-path";
|
||||
const RELEASES_DIRNAME: &str = "releases";
|
||||
@@ -147,6 +152,33 @@ impl InstallContext {
|
||||
default_rg_command()
|
||||
}
|
||||
|
||||
pub fn code_mode_host_program(&self) -> PathBuf {
|
||||
self.code_mode_host_program_from_exe(std::env::current_exe().ok().as_deref())
|
||||
}
|
||||
|
||||
fn code_mode_host_program_from_exe(&self, current_exe: Option<&Path>) -> PathBuf {
|
||||
let executable_dir = if let Some(package_layout) = &self.package_layout {
|
||||
Some(package_layout.bin_dir.clone())
|
||||
} else if let InstallMethod::Standalone { release_dir, .. } = &self.method {
|
||||
Some(release_dir.clone())
|
||||
} else {
|
||||
current_exe
|
||||
.and_then(Path::parent)
|
||||
.and_then(canonical_absolute_path)
|
||||
};
|
||||
if let Some(executable_dir) = executable_dir {
|
||||
let executable = executable_dir.join(CODE_MODE_HOST_EXECUTABLE_NAME);
|
||||
if executable.is_file() {
|
||||
return executable.into_path_buf();
|
||||
}
|
||||
}
|
||||
|
||||
current_exe
|
||||
.and_then(Path::parent)
|
||||
.map(|parent| parent.join(CODE_MODE_HOST_EXECUTABLE_NAME))
|
||||
.unwrap_or_else(|| PathBuf::from(CODE_MODE_HOST_EXECUTABLE_NAME))
|
||||
}
|
||||
|
||||
pub fn bundled_resource(&self, file_name: impl AsRef<Path>) -> Option<AbsolutePathBuf> {
|
||||
if let Some(package_layout) = &self.package_layout
|
||||
&& let Some(resources_dir) = &package_layout.resources_dir
|
||||
@@ -301,6 +333,7 @@ mod tests {
|
||||
fs::create_dir_all(&resources_dir)?;
|
||||
let exe_path = release_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" });
|
||||
fs::write(&exe_path, "")?;
|
||||
fs::write(release_dir.join(CODE_MODE_HOST_EXECUTABLE_NAME), "")?;
|
||||
fs::write(resources_dir.join(default_rg_command()), "")?;
|
||||
fs::write(resources_dir.join(TEST_RESOURCE_NAME), "")?;
|
||||
let canonical_release_dir =
|
||||
@@ -318,13 +351,19 @@ mod tests {
|
||||
context,
|
||||
InstallContext {
|
||||
method: InstallMethod::Standalone {
|
||||
release_dir: canonical_release_dir,
|
||||
release_dir: canonical_release_dir.clone(),
|
||||
resources_dir: Some(canonical_resources_dir.clone()),
|
||||
platform: standalone_platform(),
|
||||
},
|
||||
package_layout: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
context.code_mode_host_program_from_exe(Some(&exe_path)),
|
||||
canonical_release_dir
|
||||
.join(CODE_MODE_HOST_EXECUTABLE_NAME)
|
||||
.into_path_buf()
|
||||
);
|
||||
assert_eq!(
|
||||
context.bundled_resource(TEST_RESOURCE_NAME),
|
||||
Some(canonical_resources_dir.join(TEST_RESOURCE_NAME))
|
||||
@@ -364,6 +403,7 @@ mod tests {
|
||||
fs::write(package_dir.path().join(PACKAGE_METADATA_FILENAME), "{}")?;
|
||||
let exe_path = bin_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" });
|
||||
fs::write(&exe_path, "")?;
|
||||
fs::write(bin_dir.join(CODE_MODE_HOST_EXECUTABLE_NAME), "")?;
|
||||
fs::write(resources_dir.join(TEST_RESOURCE_NAME), "")?;
|
||||
fs::write(path_dir.join(default_rg_command()), "")?;
|
||||
if !cfg!(windows) {
|
||||
@@ -379,7 +419,7 @@ mod tests {
|
||||
let canonical_path_dir = AbsolutePathBuf::from_absolute_path(path_dir.canonicalize()?)?;
|
||||
let package_layout = CodexPackageLayout {
|
||||
package_dir: canonical_package_dir,
|
||||
bin_dir: canonical_bin_dir,
|
||||
bin_dir: canonical_bin_dir.clone(),
|
||||
resources_dir: Some(canonical_resources_dir.clone()),
|
||||
path_dir: Some(canonical_path_dir.clone()),
|
||||
};
|
||||
@@ -397,6 +437,12 @@ mod tests {
|
||||
package_layout: Some(package_layout),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
context.code_mode_host_program_from_exe(Some(&exe_path)),
|
||||
canonical_bin_dir
|
||||
.join(CODE_MODE_HOST_EXECUTABLE_NAME)
|
||||
.into_path_buf()
|
||||
);
|
||||
assert_eq!(
|
||||
context.rg_command(),
|
||||
canonical_path_dir
|
||||
@@ -423,6 +469,36 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn code_mode_host_program_accepts_symlinks_to_files() -> std::io::Result<()> {
|
||||
let package_dir = tempfile::tempdir()?;
|
||||
let bin_dir = package_dir.path().join(BIN_DIRNAME);
|
||||
fs::create_dir_all(&bin_dir)?;
|
||||
fs::write(package_dir.path().join(PACKAGE_METADATA_FILENAME), "{}")?;
|
||||
let exe_path = bin_dir.join("codex");
|
||||
let executable_target = package_dir.path().join("host-target");
|
||||
let executable_path = bin_dir.join(CODE_MODE_HOST_EXECUTABLE_NAME);
|
||||
fs::write(&exe_path, "")?;
|
||||
fs::write(&executable_target, "")?;
|
||||
std::os::unix::fs::symlink(&executable_target, &executable_path)?;
|
||||
let canonical_bin_dir = AbsolutePathBuf::from_absolute_path(bin_dir.canonicalize()?)?;
|
||||
|
||||
let context = InstallContext::from_exe(
|
||||
/*is_macos*/ false,
|
||||
/*current_exe*/ Some(&exe_path),
|
||||
/*method_override*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
context.code_mode_host_program_from_exe(Some(&exe_path)),
|
||||
canonical_bin_dir
|
||||
.join(CODE_MODE_HOST_EXECUTABLE_NAME)
|
||||
.into_path_buf()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standalone_package_layout_keeps_standalone_install_method() -> std::io::Result<()> {
|
||||
let codex_home = tempfile::tempdir()?;
|
||||
@@ -537,11 +613,14 @@ mod tests {
|
||||
let resources_dir = package_dir.path().join(RESOURCES_DIRNAME);
|
||||
let path_dir = package_dir.path().join(PATH_DIRNAME);
|
||||
fs::create_dir_all(&bin_dir)?;
|
||||
fs::create_dir_all(bin_dir.join(CODE_MODE_HOST_EXECUTABLE_NAME))?;
|
||||
fs::create_dir_all(resources_dir.join(TEST_RESOURCE_NAME))?;
|
||||
fs::create_dir_all(path_dir.join(default_rg_command()))?;
|
||||
fs::write(package_dir.path().join(PACKAGE_METADATA_FILENAME), "{}")?;
|
||||
let exe_path = bin_dir.join(if cfg!(windows) { "codex.exe" } else { "codex" });
|
||||
fs::write(&exe_path, "")?;
|
||||
let fallback_exe_path = package_dir.path().join("fallback-codex");
|
||||
fs::write(&fallback_exe_path, "")?;
|
||||
|
||||
let context = InstallContext::from_exe_with_codex_home(
|
||||
/*is_macos*/ false,
|
||||
@@ -549,11 +628,41 @@ mod tests {
|
||||
/*method_override*/ None,
|
||||
/*codex_home*/ None,
|
||||
);
|
||||
assert_eq!(
|
||||
context.code_mode_host_program_from_exe(Some(&fallback_exe_path)),
|
||||
package_dir.path().join(CODE_MODE_HOST_EXECUTABLE_NAME)
|
||||
);
|
||||
assert_eq!(context.rg_command(), default_rg_command());
|
||||
assert_eq!(context.bundled_resource(TEST_RESOURCE_NAME), None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_mode_host_program_is_next_to_the_executable_even_when_missing() {
|
||||
let context = InstallContext {
|
||||
method: InstallMethod::Other,
|
||||
package_layout: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
context.code_mode_host_program_from_exe(Some(Path::new("/opt/codex/bin/codex"))),
|
||||
PathBuf::from("/opt/codex/bin").join(CODE_MODE_HOST_EXECUTABLE_NAME)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_mode_host_program_falls_back_to_its_name_when_executable_is_unknown() {
|
||||
let context = InstallContext {
|
||||
method: InstallMethod::Other,
|
||||
package_layout: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
context.code_mode_host_program_from_exe(/*current_exe*/ None),
|
||||
PathBuf::from(CODE_MODE_HOST_EXECUTABLE_NAME)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn package_manager_method_overrides_take_precedence() {
|
||||
let pnpm_context = InstallContext::from_exe(
|
||||
|
||||
Reference in New Issue
Block a user