mirror of
https://github.com/openai/codex.git
synced 2026-10-28 16:06:15 +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
@@ -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(()));
|
||||
|
||||
Reference in New Issue
Block a user