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:
Channing Conger
2026-07-30 20:24:29 +00:00
committed by copyberry
parent acd540f158
commit 97576b1794
51 changed files with 627 additions and 424 deletions
+1 -1
View File
@@ -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": {
+1
View File
@@ -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,
}
+1
View File
@@ -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(),
+5 -5
View File
@@ -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,
+19
View File
@@ -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())
{
+11 -34
View File
@@ -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,
+1 -1
View File
@@ -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(),
+25 -44
View File
@@ -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");
}
}
+13 -1
View File
@@ -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));
+139 -59
View File
@@ -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(()));