diff --git a/AGENTS.md b/AGENTS.md index c8d989fe9d..297fb0fe15 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,6 +21,7 @@ In the codex-rs folder where the rust code lives: - Newly added traits should include doc comments that explain their role and how implementations are expected to use them. - When writing tests, prefer comparing the equality of entire objects over fields one by one. - When making a change that adds or changes an API, ensure that the documentation in the `docs/` folder is up to date if applicable. +- Prefer private modules and explicitly exported public crate API. - If you change `ConfigToml` or nested config types, run `just write-config-schema` to update `codex-rs/core/config.schema.json`. - If you change Rust dependencies (`Cargo.toml` or `Cargo.lock`), run `just bazel-lock-update` from the repo root to refresh `MODULE.bazel.lock`, and include that lockfile update in the same change. diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 3ca0079ab5..2f450afa49 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -248,12 +248,12 @@ use codex_login::default_client::set_default_client_residency_requirement; use codex_login::login_with_api_key; use codex_login::request_device_code; use codex_login::run_login_server; -use codex_mcp::mcp::McpSnapshotDetail; -use codex_mcp::mcp::auth::discover_supported_scopes; -use codex_mcp::mcp::auth::resolve_oauth_scopes; -use codex_mcp::mcp::collect_mcp_snapshot_with_detail; -use codex_mcp::mcp::effective_mcp_servers; -use codex_mcp::mcp::qualified_mcp_tool_name_prefix; +use codex_mcp::McpSnapshotDetail; +use codex_mcp::collect_mcp_snapshot_with_detail; +use codex_mcp::discover_supported_scopes; +use codex_mcp::effective_mcp_servers; +use codex_mcp::qualified_mcp_tool_name_prefix; +use codex_mcp::resolve_oauth_scopes; use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_protocol::ThreadId; use codex_protocol::config_types::CollaborationMode; @@ -5223,7 +5223,7 @@ impl CodexMessageProcessor { request_id: ConnectionRequestId, params: ListMcpServerStatusParams, config: Config, - mcp_config: codex_mcp::mcp::McpConfig, + mcp_config: codex_mcp::McpConfig, auth: Option, ) { let detail = match params.detail.unwrap_or(McpServerStatusDetail::Full) { @@ -7720,42 +7720,101 @@ impl CodexMessageProcessor { } let snapshot = self.feedback.snapshot(conversation_id); let thread_id = snapshot.thread_id.clone(); - let sqlite_feedback_logs = if include_logs { + let (feedback_thread_ids, sqlite_feedback_logs, state_db_ctx) = if include_logs { if let Some(log_db) = self.log_db.as_ref() { log_db.flush().await; } let state_db_ctx = get_state_db(&self.config).await; - match (state_db_ctx.as_ref(), conversation_id) { - (Some(state_db_ctx), Some(conversation_id)) => { - let thread_id_text = conversation_id.to_string(); - match state_db_ctx.query_feedback_logs(&thread_id_text).await { - Ok(logs) if logs.is_empty() => None, - Ok(logs) => Some(logs), - Err(err) => { - warn!( - "failed to query feedback logs from sqlite for thread_id={thread_id_text}: {err}" - ); - None + let feedback_thread_ids = match conversation_id { + Some(conversation_id) => match self + .thread_manager + .list_agent_subtree_thread_ids(conversation_id) + .await + { + Ok(thread_ids) => thread_ids, + Err(err) => { + warn!( + "failed to list feedback subtree for thread_id={conversation_id}: {err}" + ); + let mut thread_ids = vec![conversation_id]; + if let Some(state_db_ctx) = state_db_ctx.as_ref() { + for status in [ + codex_state::DirectionalThreadSpawnEdgeStatus::Open, + codex_state::DirectionalThreadSpawnEdgeStatus::Closed, + ] { + match state_db_ctx + .list_thread_spawn_descendants_with_status( + conversation_id, + status, + ) + .await + { + Ok(descendant_ids) => thread_ids.extend(descendant_ids), + Err(err) => warn!( + "failed to list persisted feedback subtree for thread_id={conversation_id}: {err}" + ), + } + } } + thread_ids + } + }, + None => Vec::new(), + }; + let sqlite_feedback_logs = if let Some(state_db_ctx) = state_db_ctx.as_ref() + && !feedback_thread_ids.is_empty() + { + let thread_id_texts = feedback_thread_ids + .iter() + .map(ToString::to_string) + .collect::>(); + let thread_id_refs = thread_id_texts + .iter() + .map(String::as_str) + .collect::>(); + match state_db_ctx + .query_feedback_logs_for_threads(&thread_id_refs) + .await + { + Ok(logs) if logs.is_empty() => None, + Ok(logs) => Some(logs), + Err(err) => { + let thread_ids = thread_id_texts.join(", "); + warn!( + "failed to query feedback logs from sqlite for thread_ids=[{thread_ids}]: {err}" + ); + None } } - _ => None, - } + } else { + None + }; + (feedback_thread_ids, sqlite_feedback_logs, state_db_ctx) } else { - None + (Vec::new(), None, None) }; - let validated_rollout_path = if include_logs { - match conversation_id { - Some(conv_id) => self.resolve_rollout_path(conv_id).await, - None => None, + let mut attachment_paths = Vec::new(); + let mut seen_attachment_paths = HashSet::new(); + if include_logs { + for feedback_thread_id in &feedback_thread_ids { + let Some(rollout_path) = self + .resolve_rollout_path(*feedback_thread_id, state_db_ctx.as_ref()) + .await + else { + continue; + }; + if seen_attachment_paths.insert(rollout_path.clone()) { + attachment_paths.push(rollout_path); + } } - } else { - None - }; - let mut attachment_paths = validated_rollout_path.into_iter().collect::>(); + } if let Some(extra_log_files) = extra_log_files { - attachment_paths.extend(extra_log_files); + for extra_log_file in extra_log_files { + if seen_attachment_paths.insert(extra_log_file.clone()) { + attachment_paths.push(extra_log_file); + } + } } let session_source = self.thread_manager.session_source(); @@ -7871,11 +7930,25 @@ impl CodexMessageProcessor { }); } - async fn resolve_rollout_path(&self, conversation_id: ThreadId) -> Option { - match self.thread_manager.get_thread(conversation_id).await { - Ok(conv) => conv.rollout_path(), - Err(_) => None, + async fn resolve_rollout_path( + &self, + conversation_id: ThreadId, + state_db_ctx: Option<&StateDbHandle>, + ) -> Option { + if let Ok(conversation) = self.thread_manager.get_thread(conversation_id).await + && let Some(rollout_path) = conversation.rollout_path() + { + return Some(rollout_path); } + + let state_db_ctx = state_db_ctx?; + state_db_ctx + .find_rollout_path_by_id(conversation_id, /*archived_only*/ None) + .await + .unwrap_or_else(|err| { + warn!("failed to resolve rollout path for thread_id={conversation_id}: {err}"); + None + }) } } diff --git a/codex-rs/app-server/src/codex_message_processor/plugin_mcp_oauth.rs b/codex-rs/app-server/src/codex_message_processor/plugin_mcp_oauth.rs index 58f4a5e8dd..b027aef453 100644 --- a/codex-rs/app-server/src/codex_message_processor/plugin_mcp_oauth.rs +++ b/codex-rs/app-server/src/codex_message_processor/plugin_mcp_oauth.rs @@ -5,10 +5,10 @@ use codex_app_server_protocol::McpServerOauthLoginCompletedNotification; use codex_app_server_protocol::ServerNotification; use codex_config::types::McpServerConfig; use codex_core::config::Config; -use codex_mcp::mcp::auth::McpOAuthLoginSupport; -use codex_mcp::mcp::auth::oauth_login_support; -use codex_mcp::mcp::auth::resolve_oauth_scopes; -use codex_mcp::mcp::auth::should_retry_without_scopes; +use codex_mcp::McpOAuthLoginSupport; +use codex_mcp::oauth_login_support; +use codex_mcp::resolve_oauth_scopes; +use codex_mcp::should_retry_without_scopes; use codex_rmcp_client::perform_oauth_login_silent; use tracing::warn; diff --git a/codex-rs/app-server/tests/common/rollout.rs b/codex-rs/app-server/tests/common/rollout.rs index b67390154e..efbed53255 100644 --- a/codex-rs/app-server/tests/common/rollout.rs +++ b/codex-rs/app-server/tests/common/rollout.rs @@ -84,6 +84,7 @@ pub fn create_fake_rollout_with_source( agent_role: None, model_provider: model_provider.map(str::to_string), base_instructions: None, + developer_instructions: None, dynamic_tools: None, memory_mode: None, }; @@ -167,6 +168,7 @@ pub fn create_fake_rollout_with_text_elements( agent_role: None, model_provider: model_provider.map(str::to_string), base_instructions: None, + developer_instructions: None, dynamic_tools: None, memory_mode: None, }; diff --git a/codex-rs/app-server/tests/suite/v2/thread_fork.rs b/codex-rs/app-server/tests/suite/v2/thread_fork.rs index b4e80d7816..1137f09848 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_fork.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_fork.rs @@ -200,6 +200,16 @@ async fn thread_fork_honors_explicit_null_thread_instructions() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; + let config_path = codex_home.path().join("config.toml"); + let mut config_toml = std::fs::read_to_string(&config_path)?; + let first_table_index = config_toml + .find("\n[") + .expect("test config must include a table header"); + config_toml.insert_str( + first_table_index, + "\ndeveloper_instructions = \"Config developer instructions sentinel\"\n", + ); + std::fs::write(config_path, config_toml)?; let conversation_id = create_fake_rollout( codex_home.path(), @@ -323,6 +333,13 @@ async fn thread_fork_honors_explicit_null_thread_instructions() -> Result<()> { "unexpected instructions field in payload: {payload:?}" ); let developer_texts = request.message_input_texts("developer"); + assert_eq!( + developer_texts + .iter() + .any(|text| { text.contains("Config developer instructions sentinel") }), + expect_instructions, + "unexpected config developer instruction presence: {developer_texts:?}" + ); assert!( developer_texts.iter().all(|text| !text.is_empty()), "did not expect empty developer instruction messages: {developer_texts:?}" diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 80ae756888..4c92b61c25 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -377,6 +377,7 @@ stream_max_retries = 0 agent_role: None, model_provider: Some("mock_provider".to_string()), base_instructions: None, + developer_instructions: None, dynamic_tools: None, memory_mode: None, }; diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index e8f3e1df2c..70c388a19d 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -166,6 +166,16 @@ async fn turn_start_honors_explicit_null_thread_instructions() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never", &BTreeMap::new())?; + let config_path = codex_home.path().join("config.toml"); + let mut config_toml = std::fs::read_to_string(&config_path)?; + let first_table_index = config_toml + .find("\n[") + .expect("test config must include a table header"); + config_toml.insert_str( + first_table_index, + "\ndeveloper_instructions = \"Config developer instructions sentinel\"\n", + ); + std::fs::write(config_path, config_toml)?; let mut mcp = McpProcess::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; @@ -240,6 +250,13 @@ async fn turn_start_honors_explicit_null_thread_instructions() -> Result<()> { "unexpected instructions field in payload: {payload:?}" ); let developer_texts = request.message_input_texts("developer"); + assert_eq!( + developer_texts + .iter() + .any(|text| { text.contains("Config developer instructions sentinel") }), + expect_instructions, + "unexpected config developer instruction presence: {developer_texts:?}" + ); assert!( developer_texts.iter().all(|text| !text.is_empty()), "did not expect empty developer instruction messages: {developer_texts:?}" diff --git a/codex-rs/backend-client/src/client.rs b/codex-rs/backend-client/src/client.rs index 3d5beb9d15..6049c1697b 100644 --- a/codex-rs/backend-client/src/client.rs +++ b/codex-rs/backend-client/src/client.rs @@ -503,6 +503,7 @@ impl Client { #[cfg(test)] mod tests { use super::*; + use codex_backend_openapi_models::models::AdditionalRateLimitDetails; use pretty_assertions::assert_eq; #[test] @@ -536,7 +537,7 @@ mod tests { }))), ..Default::default() }))), - additional_rate_limits: Some(Some(vec![crate::types::AdditionalRateLimitDetails { + additional_rate_limits: Some(Some(vec![AdditionalRateLimitDetails { limit_name: "codex_other".to_string(), metered_feature: "codex_other".to_string(), rate_limit: Some(Some(Box::new(crate::types::RateLimitStatusDetails { @@ -596,7 +597,7 @@ mod tests { let payload = RateLimitStatusPayload { plan_type: crate::types::PlanType::Plus, rate_limit: None, - additional_rate_limits: Some(Some(vec![crate::types::AdditionalRateLimitDetails { + additional_rate_limits: Some(Some(vec![AdditionalRateLimitDetails { limit_name: "codex_other".to_string(), metered_feature: "codex_other".to_string(), rate_limit: None, diff --git a/codex-rs/backend-client/src/lib.rs b/codex-rs/backend-client/src/lib.rs index a3b2f37608..397c2a2cd4 100644 --- a/codex-rs/backend-client/src/lib.rs +++ b/codex-rs/backend-client/src/lib.rs @@ -1,5 +1,5 @@ mod client; -pub mod types; +pub(crate) mod types; pub use client::Client; pub use client::RequestError; diff --git a/codex-rs/backend-client/src/types.rs b/codex-rs/backend-client/src/types.rs index 4ef0fb8462..22e0984cda 100644 --- a/codex-rs/backend-client/src/types.rs +++ b/codex-rs/backend-client/src/types.rs @@ -1,4 +1,3 @@ -pub use codex_backend_openapi_models::models::AdditionalRateLimitDetails; pub use codex_backend_openapi_models::models::ConfigFileResponse; pub use codex_backend_openapi_models::models::CreditStatusDetails; pub use codex_backend_openapi_models::models::PaginatedListTaskListItem; diff --git a/codex-rs/cli/src/lib.rs b/codex-rs/cli/src/lib.rs index b6174efa3a..848391293c 100644 --- a/codex-rs/cli/src/lib.rs +++ b/codex-rs/cli/src/lib.rs @@ -1,10 +1,21 @@ -pub mod debug_sandbox; +pub(crate) mod debug_sandbox; mod exit_status; -pub mod login; +pub(crate) mod login; use clap::Parser; use codex_utils_cli::CliConfigOverrides; +pub use debug_sandbox::run_command_under_landlock; +pub use debug_sandbox::run_command_under_seatbelt; +pub use debug_sandbox::run_command_under_windows; +pub use login::read_api_key_from_stdin; +pub use login::run_login_status; +pub use login::run_login_with_api_key; +pub use login::run_login_with_chatgpt; +pub use login::run_login_with_device_code; +pub use login::run_login_with_device_code_fallback_to_browser; +pub use login::run_logout; + #[derive(Debug, Parser)] pub struct SeatbeltCommand { /// Convenience alias for low-friction sandboxed automatic execution (network-disabled sandbox that can write to cwd and TMPDIR) diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 507fbf25ca..d8bcfc22cb 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -10,12 +10,12 @@ use codex_chatgpt::apply_command::run_apply_command; use codex_cli::LandlockCommand; use codex_cli::SeatbeltCommand; use codex_cli::WindowsCommand; -use codex_cli::login::read_api_key_from_stdin; -use codex_cli::login::run_login_status; -use codex_cli::login::run_login_with_api_key; -use codex_cli::login::run_login_with_chatgpt; -use codex_cli::login::run_login_with_device_code; -use codex_cli::login::run_logout; +use codex_cli::read_api_key_from_stdin; +use codex_cli::run_login_status; +use codex_cli::run_login_with_api_key; +use codex_cli::run_login_with_chatgpt; +use codex_cli::run_login_with_device_code; +use codex_cli::run_logout; use codex_cloud_tasks::Cli as CloudTasksCli; use codex_exec::Cli as ExecCli; use codex_exec::Command as ExecCommand; @@ -27,7 +27,7 @@ use codex_state::state_db_path; use codex_tui::AppExitInfo; use codex_tui::Cli as TuiCli; use codex_tui::ExitReason; -use codex_tui::update_action::UpdateAction; +use codex_tui::UpdateAction; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_cli::CliConfigOverrides; use owo_colors::OwoColorize; @@ -439,15 +439,14 @@ fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec anyhow::Result<()> { &mut seatbelt_cli.config_overrides, root_config_overrides.clone(), ); - codex_cli::debug_sandbox::run_command_under_seatbelt( + codex_cli::run_command_under_seatbelt( seatbelt_cli, arg0_paths.codex_linux_sandbox_exe.clone(), ) @@ -900,7 +899,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { &mut landlock_cli.config_overrides, root_config_overrides.clone(), ); - codex_cli::debug_sandbox::run_command_under_landlock( + codex_cli::run_command_under_landlock( landlock_cli, arg0_paths.codex_linux_sandbox_exe.clone(), ) @@ -916,7 +915,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { &mut windows_cli.config_overrides, root_config_overrides.clone(), ); - codex_cli::debug_sandbox::run_command_under_windows( + codex_cli::run_command_under_windows( windows_cli, arg0_paths.codex_linux_sandbox_exe.clone(), ) @@ -1179,7 +1178,7 @@ async fn run_debug_prompt_input_command( }); } - let prompt_input = codex_core::prompt_debug::build_prompt_input(config, input).await?; + let prompt_input = codex_core::build_prompt_input(config, input).await?; println!("{}", serde_json::to_string_pretty(&prompt_input)?); Ok(()) diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index 027a88a7c5..f544ca82b7 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -8,24 +8,24 @@ use anyhow::bail; use clap::ArgGroup; use codex_config::types::McpServerConfig; use codex_config::types::McpServerTransportConfig; +use codex_core::McpManager; use codex_core::config::Config; use codex_core::config::edit::ConfigEditsBuilder; use codex_core::config::find_codex_home; use codex_core::config::load_global_mcp_servers; -use codex_core::mcp::McpManager; use codex_core::plugins::PluginsManager; -use codex_mcp::mcp::auth::McpOAuthLoginSupport; -use codex_mcp::mcp::auth::ResolvedMcpOAuthScopes; -use codex_mcp::mcp::auth::compute_auth_statuses; -use codex_mcp::mcp::auth::discover_supported_scopes; -use codex_mcp::mcp::auth::oauth_login_support; -use codex_mcp::mcp::auth::resolve_oauth_scopes; -use codex_mcp::mcp::auth::should_retry_without_scopes; +use codex_mcp::McpOAuthLoginSupport; +use codex_mcp::ResolvedMcpOAuthScopes; +use codex_mcp::compute_auth_statuses; +use codex_mcp::discover_supported_scopes; +use codex_mcp::oauth_login_support; +use codex_mcp::resolve_oauth_scopes; +use codex_mcp::should_retry_without_scopes; use codex_protocol::protocol::McpAuthStatus; use codex_rmcp_client::delete_oauth_tokens; use codex_rmcp_client::perform_oauth_login; use codex_utils_cli::CliConfigOverrides; -use codex_utils_cli::format_env_display::format_env_display; +use codex_utils_cli::format_env_display; /// Subcommands: /// - `list` — list configured servers (with `--json`) diff --git a/codex-rs/cli/src/wsl_paths.rs b/codex-rs/cli/src/wsl_paths.rs index 28366579e5..b90dc1e283 100644 --- a/codex-rs/cli/src/wsl_paths.rs +++ b/codex-rs/cli/src/wsl_paths.rs @@ -1,7 +1,7 @@ use std::ffi::OsStr; /// Returns true if the current process is running under WSL. -pub use codex_utils_path::env::is_wsl; +pub use codex_utils_path::is_wsl; /// Convert a Windows absolute path (`C:\foo\bar` or `C:/foo/bar`) to a WSL mount path (`/mnt/c/foo/bar`). /// Returns `None` if the input does not look like a Windows drive path. diff --git a/codex-rs/cloud-requirements/src/lib.rs b/codex-rs/cloud-requirements/src/lib.rs index 04463a7dd4..ad4ebed12f 100644 --- a/codex-rs/cloud-requirements/src/lib.rs +++ b/codex-rs/cloud-requirements/src/lib.rs @@ -806,7 +806,7 @@ fn status_code_tag(status_code: Option) -> String { } fn emit_metric(metric_name: &str, tags: Vec<(&str, String)>) { - if let Some(metrics) = codex_otel::metrics::global() { + if let Some(metrics) = codex_otel::global() { let tag_refs = tags .iter() .map(|(key, value)| (*key, value.as_str())) diff --git a/codex-rs/cloud-tasks/src/lib.rs b/codex-rs/cloud-tasks/src/lib.rs index f96ac62ed9..7006d52b92 100644 --- a/codex-rs/cloud-tasks/src/lib.rs +++ b/codex-rs/cloud-tasks/src/lib.rs @@ -1,10 +1,10 @@ mod app; mod cli; -pub mod env_detect; +pub(crate) mod env_detect; mod new_task; -pub mod scrollable_diff; +pub(crate) mod scrollable_diff; mod ui; -pub mod util; +pub(crate) mod util; pub use cli::Cli; use anyhow::anyhow; @@ -1596,7 +1596,7 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option) -> an let total = ov.attempt_display_total(); let current = ov.selected_attempt + 1; app.status = format!("Viewing attempt {current} of {total}"); - ov.sd.to_top(); + ov.sd.scroll_to_top(); needs_redraw = true; } }; @@ -1672,7 +1672,7 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option) -> an let has_diff = ov.current_attempt().is_some_and(app::AttemptView::has_diff) || ov.base_can_apply; if has_text && has_diff { ov.set_view(app::DetailView::Prompt); - ov.sd.to_top(); + ov.sd.scroll_to_top(); needs_redraw = true; } } @@ -1683,7 +1683,7 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option) -> an let has_diff = ov.current_attempt().is_some_and(app::AttemptView::has_diff) || ov.base_can_apply; if has_text && has_diff { ov.set_view(app::DetailView::Diff); - ov.sd.to_top(); + ov.sd.scroll_to_top(); needs_redraw = true; } } @@ -1714,8 +1714,8 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option) -> an if let Some(ov) = &mut app.diff_overlay { let step = ov.sd.state.viewport_h.saturating_sub(1) as i16; ov.sd.page_by(-step); } needs_redraw = true; } - KeyCode::Home => { if let Some(ov) = &mut app.diff_overlay { ov.sd.to_top(); } needs_redraw = true; } - KeyCode::End => { if let Some(ov) = &mut app.diff_overlay { ov.sd.to_bottom(); } needs_redraw = true; } + KeyCode::Home => { if let Some(ov) = &mut app.diff_overlay { ov.sd.scroll_to_top(); } needs_redraw = true; } + KeyCode::End => { if let Some(ov) = &mut app.diff_overlay { ov.sd.scroll_to_bottom(); } needs_redraw = true; } _ => {} } } else if app.env_modal.is_some() { diff --git a/codex-rs/cloud-tasks/src/scrollable_diff.rs b/codex-rs/cloud-tasks/src/scrollable_diff.rs index 97dfb24895..59dd076b36 100644 --- a/codex-rs/cloud-tasks/src/scrollable_diff.rs +++ b/codex-rs/cloud-tasks/src/scrollable_diff.rs @@ -86,11 +86,11 @@ impl ScrollableDiff { self.scroll_by(delta); } - pub fn to_top(&mut self) { + pub fn scroll_to_top(&mut self) { self.state.scroll = 0; } - pub fn to_bottom(&mut self) { + pub fn scroll_to_bottom(&mut self) { self.state.scroll = self.max_scroll(); } diff --git a/codex-rs/codex-api/src/common.rs b/codex-rs/codex-api/src/common.rs index a2a2c1d478..6a88cd1f47 100644 --- a/codex-rs/codex-api/src/common.rs +++ b/codex-rs/codex-api/src/common.rs @@ -169,6 +169,8 @@ pub struct ResponsesApiRequest { pub prompt_cache_key: Option, #[serde(skip_serializing_if = "Option::is_none")] pub text: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub client_metadata: Option>, } impl From<&ResponsesApiRequest> for ResponseCreateWsRequest { @@ -189,7 +191,7 @@ impl From<&ResponsesApiRequest> for ResponseCreateWsRequest { prompt_cache_key: request.prompt_cache_key.clone(), text: request.text.clone(), generate: None, - client_metadata: None, + client_metadata: request.client_metadata.clone(), } } } diff --git a/codex-rs/codex-api/src/endpoint/mod.rs b/codex-rs/codex-api/src/endpoint/mod.rs index 6a748e533d..a6c2603f70 100644 --- a/codex-rs/codex-api/src/endpoint/mod.rs +++ b/codex-rs/codex-api/src/endpoint/mod.rs @@ -1,7 +1,22 @@ -pub mod compact; -pub mod memories; -pub mod models; -pub mod realtime_websocket; -pub mod responses; -pub mod responses_websocket; +pub(crate) mod compact; +pub(crate) mod memories; +pub(crate) mod models; +pub(crate) mod realtime_websocket; +pub(crate) mod responses; +pub(crate) mod responses_websocket; mod session; + +pub use compact::CompactClient; +pub use memories::MemoriesClient; +pub use models::ModelsClient; +pub use realtime_websocket::RealtimeEventParser; +pub use realtime_websocket::RealtimeSessionConfig; +pub use realtime_websocket::RealtimeSessionMode; +pub use realtime_websocket::RealtimeWebsocketClient; +pub use realtime_websocket::RealtimeWebsocketConnection; +pub use realtime_websocket::RealtimeWebsocketEvents; +pub use realtime_websocket::RealtimeWebsocketWriter; +pub use responses::ResponsesClient; +pub use responses::ResponsesOptions; +pub use responses_websocket::ResponsesWebsocketClient; +pub use responses_websocket::ResponsesWebsocketConnection; diff --git a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs index eed0e470a4..544c7ad7a5 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs @@ -625,9 +625,9 @@ fn normalize_realtime_path(url: &mut Url) { #[cfg(test)] mod tests { use super::*; - use crate::endpoint::realtime_websocket::protocol::RealtimeHandoffRequested; use crate::endpoint::realtime_websocket::protocol::RealtimeTranscriptDelta; use crate::endpoint::realtime_websocket::protocol::RealtimeTranscriptEntry; + use codex_protocol::protocol::RealtimeHandoffRequested; use codex_protocol::protocol::RealtimeInputAudioSpeechStarted; use codex_protocol::protocol::RealtimeResponseCancelled; use http::HeaderValue; diff --git a/codex-rs/codex-api/src/endpoint/realtime_websocket/mod.rs b/codex-rs/codex-api/src/endpoint/realtime_websocket/mod.rs index d13585034a..867ebdff65 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_websocket/mod.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/mod.rs @@ -1,14 +1,12 @@ -pub mod methods; +pub(crate) mod methods; mod methods_common; mod methods_v1; mod methods_v2; -pub mod protocol; +pub(crate) mod protocol; mod protocol_common; mod protocol_v1; mod protocol_v2; -pub use codex_protocol::protocol::RealtimeAudioFrame; -pub use codex_protocol::protocol::RealtimeEvent; pub use methods::RealtimeWebsocketClient; pub use methods::RealtimeWebsocketConnection; pub use methods::RealtimeWebsocketEvents; diff --git a/codex-rs/codex-api/src/endpoint/realtime_websocket/protocol.rs b/codex-rs/codex-api/src/endpoint/realtime_websocket/protocol.rs index 2c629249fa..4941740556 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_websocket/protocol.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/protocol.rs @@ -2,7 +2,6 @@ use crate::endpoint::realtime_websocket::protocol_v1::parse_realtime_event_v1; use crate::endpoint::realtime_websocket::protocol_v2::parse_realtime_event_v2; pub use codex_protocol::protocol::RealtimeAudioFrame; pub use codex_protocol::protocol::RealtimeEvent; -pub use codex_protocol::protocol::RealtimeHandoffRequested; pub use codex_protocol::protocol::RealtimeTranscriptDelta; pub use codex_protocol::protocol::RealtimeTranscriptEntry; use serde::Serialize; diff --git a/codex-rs/codex-api/src/endpoint/responses.rs b/codex-rs/codex-api/src/endpoint/responses.rs index 57a44d2e27..8e0e7384fd 100644 --- a/codex-rs/codex-api/src/endpoint/responses.rs +++ b/codex-rs/codex-api/src/endpoint/responses.rs @@ -4,11 +4,11 @@ use crate::common::ResponsesApiRequest; use crate::endpoint::session::EndpointSession; use crate::error::ApiError; use crate::provider::Provider; +use crate::requests::Compression; +use crate::requests::attach_item_ids; use crate::requests::headers::build_conversation_headers; use crate::requests::headers::insert_header; use crate::requests::headers::subagent_header; -use crate::requests::responses::Compression; -use crate::requests::responses::attach_item_ids; use crate::sse::spawn_response_stream; use crate::telemetry::SseTelemetry; use codex_client::HttpTransport; diff --git a/codex-rs/codex-api/src/endpoint/responses_websocket.rs b/codex-rs/codex-api/src/endpoint/responses_websocket.rs index d3b578db69..a60d188bda 100644 --- a/codex-rs/codex-api/src/endpoint/responses_websocket.rs +++ b/codex-rs/codex-api/src/endpoint/responses_websocket.rs @@ -6,8 +6,8 @@ use crate::common::ResponsesWsRequest; use crate::error::ApiError; use crate::provider::Provider; use crate::rate_limits::parse_rate_limit_event; -use crate::sse::responses::ResponsesStreamEvent; -use crate::sse::responses::process_responses_event; +use crate::sse::ResponsesStreamEvent; +use crate::sse::process_responses_event; use crate::telemetry::WebsocketTelemetry; use codex_client::TransportError; use codex_client::maybe_build_rustls_client_config_with_custom_ca; diff --git a/codex-rs/codex-api/src/lib.rs b/codex-rs/codex-api/src/lib.rs index c7bb607059..01a5637356 100644 --- a/codex-rs/codex-api/src/lib.rs +++ b/codex-rs/codex-api/src/lib.rs @@ -1,48 +1,58 @@ -pub mod api_bridge; -pub mod auth; -pub mod common; -pub mod endpoint; -pub mod error; -pub mod provider; -pub mod rate_limits; -pub mod requests; -pub mod sse; -pub mod telemetry; +pub(crate) mod api_bridge; +pub(crate) mod auth; +pub(crate) mod common; +pub(crate) mod endpoint; +pub(crate) mod error; +pub(crate) mod provider; +pub(crate) mod rate_limits; +pub(crate) mod requests; +pub(crate) mod sse; +pub(crate) mod telemetry; pub use crate::requests::headers::build_conversation_headers; pub use codex_client::RequestTelemetry; pub use codex_client::ReqwestTransport; pub use codex_client::TransportError; +pub use crate::api_bridge::CoreAuthProvider; +pub use crate::api_bridge::map_api_error; pub use crate::auth::AuthProvider; pub use crate::common::CompactionInput; pub use crate::common::MemorySummarizeInput; pub use crate::common::MemorySummarizeOutput; +pub use crate::common::OpenAiVerbosity; pub use crate::common::RawMemory; pub use crate::common::RawMemoryMetadata; +pub use crate::common::Reasoning; pub use crate::common::ResponseCreateWsRequest; pub use crate::common::ResponseEvent; pub use crate::common::ResponseStream; pub use crate::common::ResponsesApiRequest; +pub use crate::common::ResponsesWsRequest; +pub use crate::common::TextControls; pub use crate::common::WS_REQUEST_HEADER_TRACEPARENT_CLIENT_METADATA_KEY; pub use crate::common::WS_REQUEST_HEADER_TRACESTATE_CLIENT_METADATA_KEY; pub use crate::common::create_text_param_for_request; pub use crate::common::response_create_client_metadata; -pub use crate::endpoint::compact::CompactClient; -pub use crate::endpoint::memories::MemoriesClient; -pub use crate::endpoint::models::ModelsClient; -pub use crate::endpoint::realtime_websocket::RealtimeEventParser; -pub use crate::endpoint::realtime_websocket::RealtimeSessionConfig; -pub use crate::endpoint::realtime_websocket::RealtimeSessionMode; -pub use crate::endpoint::realtime_websocket::RealtimeWebsocketClient; -pub use crate::endpoint::realtime_websocket::RealtimeWebsocketConnection; -pub use crate::endpoint::responses::ResponsesClient; -pub use crate::endpoint::responses::ResponsesOptions; -pub use crate::endpoint::responses_websocket::ResponsesWebsocketClient; -pub use crate::endpoint::responses_websocket::ResponsesWebsocketConnection; +pub use crate::endpoint::CompactClient; +pub use crate::endpoint::MemoriesClient; +pub use crate::endpoint::ModelsClient; +pub use crate::endpoint::RealtimeEventParser; +pub use crate::endpoint::RealtimeSessionConfig; +pub use crate::endpoint::RealtimeSessionMode; +pub use crate::endpoint::RealtimeWebsocketClient; +pub use crate::endpoint::RealtimeWebsocketConnection; +pub use crate::endpoint::RealtimeWebsocketEvents; +pub use crate::endpoint::RealtimeWebsocketWriter; +pub use crate::endpoint::ResponsesClient; +pub use crate::endpoint::ResponsesOptions; +pub use crate::endpoint::ResponsesWebsocketClient; +pub use crate::endpoint::ResponsesWebsocketConnection; pub use crate::error::ApiError; pub use crate::provider::Provider; +pub use crate::provider::RetryConfig; pub use crate::provider::is_azure_responses_wire_base_url; +pub use crate::requests::Compression; pub use crate::sse::stream_from_fixture; pub use crate::telemetry::SseTelemetry; pub use crate::telemetry::WebsocketTelemetry; diff --git a/codex-rs/codex-api/src/requests/mod.rs b/codex-rs/codex-api/src/requests/mod.rs index 4e98bb8cac..1c357b2a61 100644 --- a/codex-rs/codex-api/src/requests/mod.rs +++ b/codex-rs/codex-api/src/requests/mod.rs @@ -1,2 +1,5 @@ pub(crate) mod headers; -pub mod responses; +pub(crate) mod responses; + +pub use responses::Compression; +pub(crate) use responses::attach_item_ids; diff --git a/codex-rs/codex-api/src/sse/mod.rs b/codex-rs/codex-api/src/sse/mod.rs index cb689afc1d..06b9855890 100644 --- a/codex-rs/codex-api/src/sse/mod.rs +++ b/codex-rs/codex-api/src/sse/mod.rs @@ -1,5 +1,6 @@ -pub mod responses; +pub(crate) mod responses; -pub use responses::process_sse; +pub(crate) use responses::ResponsesStreamEvent; +pub(crate) use responses::process_responses_event; pub use responses::spawn_response_stream; pub use responses::stream_from_fixture; diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs index 2f3bc17e2c..2e8411dd81 100644 --- a/codex-rs/codex-api/tests/clients.rs +++ b/codex-rs/codex-api/tests/clients.rs @@ -6,11 +6,11 @@ use anyhow::Result; use async_trait::async_trait; use bytes::Bytes; use codex_api::AuthProvider; +use codex_api::Compression; use codex_api::Provider; use codex_api::ResponsesApiRequest; use codex_api::ResponsesClient; use codex_api::ResponsesOptions; -use codex_api::requests::responses::Compression; use codex_client::HttpTransport; use codex_client::Request; use codex_client::Response; @@ -126,7 +126,7 @@ fn provider(name: &str) -> Provider { base_url: "https://example.com/v1".to_string(), query_params: None, headers: HeaderMap::new(), - retry: codex_api::provider::RetryConfig { + retry: codex_api::RetryConfig { max_attempts: 1, base_delay: Duration::from_millis(1), retry_429: false, @@ -278,6 +278,7 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> { service_tier: None, prompt_cache_key: None, text: None, + client_metadata: None, }; let client = ResponsesClient::new(transport.clone(), provider, NoAuth); @@ -320,6 +321,7 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> { service_tier: None, prompt_cache_key: None, text: None, + client_metadata: None, }; let mut extra_headers = HeaderMap::new(); diff --git a/codex-rs/codex-api/tests/models_integration.rs b/codex-rs/codex-api/tests/models_integration.rs index 3ffb2496b4..c6b28fa71a 100644 --- a/codex-rs/codex-api/tests/models_integration.rs +++ b/codex-rs/codex-api/tests/models_integration.rs @@ -1,7 +1,7 @@ use codex_api::AuthProvider; use codex_api::ModelsClient; -use codex_api::provider::Provider; -use codex_api::provider::RetryConfig; +use codex_api::Provider; +use codex_api::RetryConfig; use codex_client::ReqwestTransport; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::openai_models::ConfigShellToolType; diff --git a/codex-rs/codex-api/tests/realtime_websocket_e2e.rs b/codex-rs/codex-api/tests/realtime_websocket_e2e.rs index 130ab6fd35..42c7a92e2d 100644 --- a/codex-rs/codex-api/tests/realtime_websocket_e2e.rs +++ b/codex-rs/codex-api/tests/realtime_websocket_e2e.rs @@ -2,14 +2,14 @@ use std::collections::HashMap; use std::future::Future; use std::time::Duration; +use codex_api::Provider; use codex_api::RealtimeAudioFrame; use codex_api::RealtimeEvent; use codex_api::RealtimeEventParser; use codex_api::RealtimeSessionConfig; use codex_api::RealtimeSessionMode; use codex_api::RealtimeWebsocketClient; -use codex_api::provider::Provider; -use codex_api::provider::RetryConfig; +use codex_api::RetryConfig; use codex_protocol::protocol::RealtimeHandoffRequested; use futures::SinkExt; use futures::StreamExt; diff --git a/codex-rs/codex-api/tests/sse_end_to_end.rs b/codex-rs/codex-api/tests/sse_end_to_end.rs index 80972340dd..b15de296a8 100644 --- a/codex-rs/codex-api/tests/sse_end_to_end.rs +++ b/codex-rs/codex-api/tests/sse_end_to_end.rs @@ -4,10 +4,10 @@ use anyhow::Result; use async_trait::async_trait; use bytes::Bytes; use codex_api::AuthProvider; +use codex_api::Compression; use codex_api::Provider; use codex_api::ResponseEvent; use codex_api::ResponsesClient; -use codex_api::requests::responses::Compression; use codex_client::HttpTransport; use codex_client::Request; use codex_client::Response; @@ -64,7 +64,7 @@ fn provider(name: &str) -> Provider { base_url: "https://example.com/v1".to_string(), query_params: None, headers: HeaderMap::new(), - retry: codex_api::provider::RetryConfig { + retry: codex_api::RetryConfig { max_attempts: 1, base_delay: Duration::from_millis(1), retry_429: false, diff --git a/codex-rs/codex-backend-openapi-models/src/models/mod.rs b/codex-rs/codex-backend-openapi-models/src/models/mod.rs index 90bc058334..2140c83f91 100644 --- a/codex-rs/codex-backend-openapi-models/src/models/mod.rs +++ b/codex-rs/codex-backend-openapi-models/src/models/mod.rs @@ -4,41 +4,41 @@ // The process for this will change // Config -pub mod config_file_response; +pub(crate) mod config_file_response; pub use self::config_file_response::ConfigFileResponse; // Cloud Tasks -pub mod code_task_details_response; +pub(crate) mod code_task_details_response; pub use self::code_task_details_response::CodeTaskDetailsResponse; -pub mod task_response; +pub(crate) mod task_response; pub use self::task_response::TaskResponse; -pub mod external_pull_request_response; +pub(crate) mod external_pull_request_response; pub use self::external_pull_request_response::ExternalPullRequestResponse; -pub mod git_pull_request; +pub(crate) mod git_pull_request; pub use self::git_pull_request::GitPullRequest; -pub mod task_list_item; +pub(crate) mod task_list_item; pub use self::task_list_item::TaskListItem; -pub mod paginated_list_task_list_item_; +pub(crate) mod paginated_list_task_list_item_; pub use self::paginated_list_task_list_item_::PaginatedListTaskListItem; // Rate Limits -pub mod additional_rate_limit_details; +pub(crate) mod additional_rate_limit_details; pub use self::additional_rate_limit_details::AdditionalRateLimitDetails; -pub mod rate_limit_status_payload; +pub(crate) mod rate_limit_status_payload; pub use self::rate_limit_status_payload::PlanType; pub use self::rate_limit_status_payload::RateLimitStatusPayload; -pub mod rate_limit_status_details; +pub(crate) mod rate_limit_status_details; pub use self::rate_limit_status_details::RateLimitStatusDetails; -pub mod rate_limit_window_snapshot; +pub(crate) mod rate_limit_window_snapshot; pub use self::rate_limit_window_snapshot::RateLimitWindowSnapshot; -pub mod credit_status_details; +pub(crate) mod credit_status_details; pub use self::credit_status_details::CreditStatusDetails; diff --git a/codex-rs/codex-mcp/src/lib.rs b/codex-rs/codex-mcp/src/lib.rs index 149578c967..48969841c4 100644 --- a/codex-rs/codex-mcp/src/lib.rs +++ b/codex-rs/codex-mcp/src/lib.rs @@ -1,2 +1,40 @@ -pub mod mcp; -pub mod mcp_connection_manager; +pub(crate) mod mcp; +pub(crate) mod mcp_connection_manager; + +pub use mcp::CODEX_APPS_MCP_SERVER_NAME; +pub use mcp::McpAuthStatusEntry; +pub use mcp::McpConfig; +pub use mcp::McpManager; +pub use mcp::McpOAuthLoginConfig; +pub use mcp::McpOAuthLoginSupport; +pub use mcp::McpOAuthScopesSource; +pub use mcp::McpSnapshotDetail; +pub use mcp::ResolvedMcpOAuthScopes; +pub use mcp::ToolPluginProvenance; +pub use mcp::canonical_mcp_server_key; +pub use mcp::collect_mcp_snapshot; +pub use mcp::collect_mcp_snapshot_from_manager; +pub use mcp::collect_mcp_snapshot_from_manager_with_detail; +pub use mcp::collect_mcp_snapshot_with_detail; +pub use mcp::collect_missing_mcp_dependencies; +pub use mcp::compute_auth_statuses; +pub use mcp::configured_mcp_servers; +pub use mcp::discover_supported_scopes; +pub use mcp::effective_mcp_servers; +pub use mcp::group_tools_by_server; +pub use mcp::oauth_login_support; +pub use mcp::qualified_mcp_tool_name_prefix; +pub use mcp::resolve_oauth_scopes; +pub use mcp::should_retry_without_scopes; +pub use mcp::split_qualified_tool_name; +pub use mcp::tool_plugin_provenance; +pub use mcp::with_codex_apps_mcp; +pub use mcp_connection_manager::CodexAppsToolsCacheKey; +pub use mcp_connection_manager::DEFAULT_STARTUP_TIMEOUT; +pub use mcp_connection_manager::MCP_SANDBOX_STATE_CAPABILITY; +pub use mcp_connection_manager::MCP_SANDBOX_STATE_METHOD; +pub use mcp_connection_manager::McpConnectionManager; +pub use mcp_connection_manager::SandboxState; +pub use mcp_connection_manager::ToolInfo; +pub use mcp_connection_manager::codex_apps_tools_cache_key; +pub use mcp_connection_manager::filter_non_codex_apps_mcp_tools_only; diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index faa8ecc2fc..9301ff1008 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -1,5 +1,15 @@ -pub mod auth; +pub(crate) mod auth; mod skill_dependencies; +pub use auth::McpAuthStatusEntry; +pub use auth::McpOAuthLoginConfig; +pub use auth::McpOAuthLoginSupport; +pub use auth::McpOAuthScopesSource; +pub use auth::ResolvedMcpOAuthScopes; +pub use auth::compute_auth_statuses; +pub use auth::discover_supported_scopes; +pub use auth::oauth_login_support; +pub use auth::resolve_oauth_scopes; +pub use auth::should_retry_without_scopes; pub use skill_dependencies::canonical_mcp_server_key; pub use skill_dependencies::collect_missing_mcp_dependencies; @@ -23,7 +33,6 @@ use codex_protocol::protocol::McpListToolsResponseEvent; use codex_protocol::protocol::SandboxPolicy; use serde_json::Value; -use crate::mcp::auth::compute_auth_statuses; use crate::mcp_connection_manager::McpConnectionManager; use crate::mcp_connection_manager::SandboxState; use crate::mcp_connection_manager::codex_apps_tools_cache_key; @@ -388,7 +397,7 @@ pub fn group_tools_by_server( pub async fn collect_mcp_snapshot_from_manager( mcp_connection_manager: &McpConnectionManager, - auth_status_entries: HashMap, + auth_status_entries: HashMap, ) -> McpListToolsResponseEvent { collect_mcp_snapshot_from_manager_with_detail( mcp_connection_manager, @@ -400,7 +409,7 @@ pub async fn collect_mcp_snapshot_from_manager( pub async fn collect_mcp_snapshot_from_manager_with_detail( mcp_connection_manager: &McpConnectionManager, - auth_status_entries: HashMap, + auth_status_entries: HashMap, detail: McpSnapshotDetail, ) -> McpListToolsResponseEvent { let (tools, resources, resource_templates) = tokio::join!( diff --git a/codex-rs/codex-mcp/src/mcp_connection_manager.rs b/codex-rs/codex-mcp/src/mcp_connection_manager.rs index b4de70ee95..c90088c7dc 100644 --- a/codex-rs/codex-mcp/src/mcp_connection_manager.rs +++ b/codex-rs/codex-mcp/src/mcp_connection_manager.rs @@ -19,10 +19,10 @@ use std::sync::atomic::Ordering; use std::time::Duration; use std::time::Instant; +use crate::McpAuthStatusEntry; use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; use crate::mcp::McpConfig; use crate::mcp::ToolPluginProvenance; -use crate::mcp::auth::McpAuthStatusEntry; use crate::mcp::configured_mcp_servers; use crate::mcp::effective_mcp_servers; use crate::mcp::sanitize_responses_api_tool_name; @@ -1568,7 +1568,7 @@ fn filter_disallowed_codex_apps_tools(tools: Vec) -> Vec { } fn emit_duration(metric: &str, duration: Duration, tags: &[(&str, &str)]) { - if let Some(metrics) = codex_otel::metrics::global() { + if let Some(metrics) = codex_otel::global() { let _ = metrics.record_duration(metric, duration, tags); } } diff --git a/codex-rs/config/src/requirements_exec_policy.rs b/codex-rs/config/src/requirements_exec_policy.rs index 64d60f8814..30a1a0e694 100644 --- a/codex-rs/config/src/requirements_exec_policy.rs +++ b/codex-rs/config/src/requirements_exec_policy.rs @@ -1,9 +1,9 @@ use codex_execpolicy::Decision; +use codex_execpolicy::PatternToken; use codex_execpolicy::Policy; -use codex_execpolicy::rule::PatternToken; -use codex_execpolicy::rule::PrefixPattern; -use codex_execpolicy::rule::PrefixRule; -use codex_execpolicy::rule::RuleRef; +use codex_execpolicy::PrefixPattern; +use codex_execpolicy::PrefixRule; +use codex_execpolicy::RuleRef; use multimap::MultiMap; use serde::Deserialize; use std::sync::Arc; diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 8da02dc803..e4e7ff85b7 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -362,6 +362,9 @@ "connectors": { "type": "boolean" }, + "debug_hide_spawn_agent_metadata": { + "type": "boolean" + }, "default_mode_request_user_input": { "type": "boolean" }, @@ -2072,6 +2075,9 @@ "connectors": { "type": "boolean" }, + "debug_hide_spawn_agent_metadata": { + "type": "boolean" + }, "default_mode_request_user_input": { "type": "boolean" }, diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 255c6bc2c3..529615a7a5 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -730,6 +730,15 @@ impl AgentControl { self.state.agent_metadata_for_thread(agent_id) } + pub(crate) async fn list_live_agent_subtree_thread_ids( + &self, + agent_id: ThreadId, + ) -> CodexResult> { + let mut thread_ids = vec![agent_id]; + thread_ids.extend(self.live_thread_spawn_descendants(agent_id).await?); + Ok(thread_ids) + } + pub(crate) async fn get_agent_config_snapshot( &self, agent_id: ThreadId, diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index a64a11bf98..54693228fa 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -1687,6 +1687,132 @@ async fn resume_agent_from_rollout_reads_archived_rollout_path() { .expect("resumed child shutdown should succeed"); } +#[tokio::test] +async fn list_agent_subtree_thread_ids_includes_anonymous_and_closed_descendants() { + let harness = AgentControlHarness::new().await; + let (parent_thread_id, _parent_thread) = harness.start_thread().await; + let worker_path = AgentPath::root().join("worker").expect("worker path"); + let reviewer_path = AgentPath::root().join("reviewer").expect("reviewer path"); + + let worker_thread_id = harness + .control + .spawn_agent( + harness.config.clone(), + text_input("hello worker"), + Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth: 1, + agent_path: Some(worker_path.clone()), + agent_nickname: None, + agent_role: Some("worker".to_string()), + })), + ) + .await + .expect("worker spawn should succeed"); + let worker_child_thread_id = harness + .control + .spawn_agent( + harness.config.clone(), + text_input("hello worker child"), + Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id: worker_thread_id, + depth: 2, + agent_path: Some( + worker_path + .join("child") + .expect("worker child path should be valid"), + ), + agent_nickname: None, + agent_role: Some("worker".to_string()), + })), + ) + .await + .expect("worker child spawn should succeed"); + let no_path_child_thread_id = harness + .control + .spawn_agent( + harness.config.clone(), + text_input("hello anonymous child"), + Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id: worker_thread_id, + depth: 2, + agent_path: None, + agent_nickname: None, + agent_role: Some("worker".to_string()), + })), + ) + .await + .expect("no-path child spawn should succeed"); + let no_path_grandchild_thread_id = harness + .control + .spawn_agent( + harness.config.clone(), + text_input("hello anonymous grandchild"), + Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id: no_path_child_thread_id, + depth: 3, + agent_path: None, + agent_nickname: None, + agent_role: Some("worker".to_string()), + })), + ) + .await + .expect("no-path grandchild spawn should succeed"); + let _reviewer_thread_id = harness + .control + .spawn_agent( + harness.config.clone(), + text_input("hello reviewer"), + Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth: 1, + agent_path: Some(reviewer_path), + agent_nickname: None, + agent_role: Some("reviewer".to_string()), + })), + ) + .await + .expect("reviewer spawn should succeed"); + + let _ = harness + .control + .shutdown_live_agent(no_path_grandchild_thread_id) + .await + .expect("no-path grandchild shutdown should succeed"); + + let mut worker_subtree_thread_ids = harness + .manager + .list_agent_subtree_thread_ids(worker_thread_id) + .await + .expect("worker subtree thread ids should load"); + worker_subtree_thread_ids.sort_by_key(ToString::to_string); + let mut expected_worker_subtree_thread_ids = vec![ + worker_thread_id, + worker_child_thread_id, + no_path_child_thread_id, + no_path_grandchild_thread_id, + ]; + expected_worker_subtree_thread_ids.sort_by_key(ToString::to_string); + assert_eq!( + worker_subtree_thread_ids, + expected_worker_subtree_thread_ids + ); + + let mut no_path_child_subtree_thread_ids = harness + .manager + .list_agent_subtree_thread_ids(no_path_child_thread_id) + .await + .expect("no-path subtree thread ids should load"); + no_path_child_subtree_thread_ids.sort_by_key(ToString::to_string); + let mut expected_no_path_child_subtree_thread_ids = + vec![no_path_child_thread_id, no_path_grandchild_thread_id]; + expected_no_path_child_subtree_thread_ids.sort_by_key(ToString::to_string); + assert_eq!( + no_path_child_subtree_thread_ids, + expected_no_path_child_subtree_thread_ids + ); +} + #[tokio::test] async fn shutdown_agent_tree_closes_live_descendants() { let harness = AgentControlHarness::new().await; diff --git a/codex-rs/core/src/agent/registry.rs b/codex-rs/core/src/agent/registry.rs index c600d5c64b..1acd73085f 100644 --- a/codex-rs/core/src/agent/registry.rs +++ b/codex-rs/core/src/agent/registry.rs @@ -220,7 +220,7 @@ impl AgentRegistry { } else { active_agents.used_agent_nicknames.clear(); active_agents.nickname_reset_count += 1; - if let Some(metrics) = codex_otel::metrics::global() { + if let Some(metrics) = codex_otel::global() { let _ = metrics.counter( "codex.multi_agent.nickname_pool_reset", /*inc*/ 1, diff --git a/codex-rs/core/src/agent/role.rs b/codex-rs/core/src/agent/role.rs index 0f22691c56..f2067565a0 100644 --- a/codex-rs/core/src/agent/role.rs +++ b/codex-rs/core/src/agent/role.rs @@ -64,6 +64,12 @@ async fn apply_role_to_config_inner( return Ok(()); }; let role_layer_toml = load_role_layer_toml(config, config_file, is_built_in, role_name).await?; + if role_layer_toml + .as_table() + .is_some_and(toml::map::Map::is_empty) + { + return Ok(()); + } let (preserve_current_profile, preserve_current_provider) = preservation_policy(config, &role_layer_toml); diff --git a/codex-rs/core/src/agent/role_tests.rs b/codex-rs/core/src/agent/role_tests.rs index b0ada4754e..9357b6917c 100644 --- a/codex-rs/core/src/agent/role_tests.rs +++ b/codex-rs/core/src/agent/role_tests.rs @@ -89,6 +89,22 @@ async fn apply_explorer_role_sets_model_and_adds_session_flags_layer() { assert_eq!(session_flags_layer_count(&config), before_layers + 1); } +#[tokio::test] +async fn apply_empty_explorer_role_preserves_current_model_and_reasoning_effort() { + let (_home, mut config) = test_config_with_cli_overrides(Vec::new()).await; + let before_layers = session_flags_layer_count(&config); + config.model = Some("gpt-5.4-mini".to_string()); + config.model_reasoning_effort = Some(ReasoningEffort::High); + + apply_role_to_config(&mut config, Some("explorer")) + .await + .expect("explorer role should apply"); + + assert_eq!(config.model.as_deref(), Some("gpt-5.4-mini")); + assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::High)); + assert_eq!(session_flags_layer_count(&config), before_layers); +} + #[tokio::test] async fn apply_role_returns_unavailable_for_missing_user_role_file() { let (_home, mut config) = test_config_with_cli_overrides(Vec::new()).await; diff --git a/codex-rs/core/src/apps/render.rs b/codex-rs/core/src/apps/render.rs index 9b93913d78..fe23a09f8d 100644 --- a/codex-rs/core/src/apps/render.rs +++ b/codex-rs/core/src/apps/render.rs @@ -1,5 +1,5 @@ use codex_app_server_protocol::AppInfo; -use codex_mcp::mcp::CODEX_APPS_MCP_SERVER_NAME; +use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_protocol::protocol::APPS_INSTRUCTIONS_CLOSE_TAG; use codex_protocol::protocol::APPS_INSTRUCTIONS_OPEN_TAG; diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index b77dda2b75..80ad24be5b 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -31,12 +31,15 @@ use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; +use codex_api::ApiError; use codex_api::CompactClient as ApiCompactClient; use codex_api::CompactionInput as ApiCompactionInput; +use codex_api::Compression; use codex_api::MemoriesClient as ApiMemoriesClient; use codex_api::MemorySummarizeInput as ApiMemorySummarizeInput; use codex_api::MemorySummarizeOutput as ApiMemorySummarizeOutput; use codex_api::RawMemory as ApiRawMemory; +use codex_api::Reasoning; use codex_api::RequestTelemetry; use codex_api::ReqwestTransport; use codex_api::ResponseCreateWsRequest; @@ -45,15 +48,12 @@ use codex_api::ResponsesClient as ApiResponsesClient; use codex_api::ResponsesOptions as ApiResponsesOptions; use codex_api::ResponsesWebsocketClient as ApiWebSocketResponsesClient; use codex_api::ResponsesWebsocketConnection as ApiWebSocketConnection; +use codex_api::ResponsesWsRequest; use codex_api::SseTelemetry; use codex_api::TransportError; use codex_api::WebsocketTelemetry; use codex_api::build_conversation_headers; -use codex_api::common::Reasoning; -use codex_api::common::ResponsesWsRequest; use codex_api::create_text_param_for_request; -use codex_api::error::ApiError; -use codex_api::requests::responses::Compression; use codex_api::response_create_client_metadata; use codex_app_server_protocol::AuthMode; use codex_login::AuthManager; @@ -98,8 +98,8 @@ use crate::client_common::ResponseEvent; use crate::client_common::ResponseStream; use crate::flags::CODEX_RS_SSE_FIXTURE; use crate::util::emit_feedback_auth_recovery_tags; -use codex_api::api_bridge::CoreAuthProvider; -use codex_api::api_bridge::map_api_error; +use codex_api::CoreAuthProvider; +use codex_api::map_api_error; use codex_feedback::FeedbackRequestTags; use codex_feedback::emit_feedback_request_tags_with_auth_env; use codex_login::api_bridge::auth_provider_from_auth; @@ -118,6 +118,7 @@ use codex_response_debug_context::telemetry_api_error_message; use codex_response_debug_context::telemetry_transport_error_message; pub const OPENAI_BETA_HEADER: &str = "OpenAI-Beta"; +pub const X_CODEX_INSTALLATION_ID_HEADER: &str = "x-codex-installation-id"; pub const X_CODEX_TURN_STATE_HEADER: &str = "x-codex-turn-state"; pub const X_CODEX_TURN_METADATA_HEADER: &str = "x-codex-turn-metadata"; pub const X_CODEX_PARENT_THREAD_ID_HEADER: &str = "x-codex-parent-thread-id"; @@ -142,6 +143,7 @@ struct ModelClientState { auth_manager: Option>, conversation_id: ThreadId, window_generation: AtomicU64, + installation_id: String, provider: ModelProviderInfo, auth_env_telemetry: AuthEnvTelemetry, session_source: SessionSource, @@ -263,6 +265,7 @@ impl ModelClient { pub fn new( auth_manager: Option>, conversation_id: ThreadId, + installation_id: String, provider: ModelProviderInfo, session_source: SessionSource, model_verbosity: Option, @@ -280,6 +283,7 @@ impl ModelClient { auth_manager, conversation_id, window_generation: AtomicU64::new(0), + installation_id, provider, auth_env_telemetry, session_source, @@ -429,7 +433,11 @@ impl ModelClient { text, }; - let mut extra_headers = self.build_responses_identity_headers(); + let mut extra_headers = ApiHeaderMap::new(); + if let Ok(header_value) = HeaderValue::from_str(&self.state.installation_id) { + extra_headers.insert(X_CODEX_INSTALLATION_ID_HEADER, header_value); + } + extra_headers.extend(self.build_responses_identity_headers()); extra_headers.extend(build_conversation_headers(Some( self.state.conversation_id.to_string(), ))); @@ -515,6 +523,10 @@ impl ModelClient { turn_metadata_header: Option<&str>, ) -> HashMap { let mut client_metadata = HashMap::new(); + client_metadata.insert( + X_CODEX_INSTALLATION_ID_HEADER.to_string(), + self.state.installation_id.clone(), + ); client_metadata.insert( X_CODEX_WINDOW_ID_HEADER.to_string(), self.current_window_id(), @@ -817,6 +829,10 @@ impl ModelClientSession { }, prompt_cache_key, text, + client_metadata: Some(HashMap::from([( + X_CODEX_INSTALLATION_ID_HEADER.to_string(), + self.client.state.installation_id.clone(), + )])), }; Ok(request) } diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 828839ae77..a90d75bf01 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -1,4 +1,4 @@ -pub use codex_api::common::ResponseEvent; +pub use codex_api::ResponseEvent; use codex_config::types::Personality; use codex_protocol::error::Result; use codex_protocol::models::BaseInstructions; diff --git a/codex-rs/core/src/client_common_tests.rs b/codex-rs/core/src/client_common_tests.rs index 968703046e..4d4b5af4ff 100644 --- a/codex-rs/core/src/client_common_tests.rs +++ b/codex-rs/core/src/client_common_tests.rs @@ -1,6 +1,6 @@ +use codex_api::OpenAiVerbosity; use codex_api::ResponsesApiRequest; -use codex_api::common::OpenAiVerbosity; -use codex_api::common::TextControls; +use codex_api::TextControls; use codex_api::create_text_param_for_request; use codex_protocol::config_types::ServiceTier; use codex_protocol::models::FunctionCallOutputPayload; @@ -29,6 +29,7 @@ fn serializes_text_verbosity_when_set() { verbosity: Some(OpenAiVerbosity::Low), format: None, }), + client_metadata: None, }; let v = serde_json::to_value(&req).expect("json"); @@ -69,6 +70,7 @@ fn serializes_text_schema_with_strict_format() { prompt_cache_key: None, service_tier: None, text: Some(text_controls), + client_metadata: None, }; let v = serde_json::to_value(&req).expect("json"); @@ -106,6 +108,7 @@ fn omits_text_when_not_set() { prompt_cache_key: None, service_tier: None, text: None, + client_metadata: None, }; let v = serde_json::to_value(&req).expect("json"); @@ -128,6 +131,7 @@ fn serializes_flex_service_tier_when_set() { prompt_cache_key: None, service_tier: Some(ServiceTier::Flex.to_string()), text: None, + client_metadata: None, }; let v = serde_json::to_value(&req).expect("json"); diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index a4fb681201..2fd5f04f95 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -2,11 +2,12 @@ use super::AuthRequestTelemetryContext; use super::ModelClient; use super::PendingUnauthorizedRetry; use super::UnauthorizedRecoveryExecution; +use super::X_CODEX_INSTALLATION_ID_HEADER; use super::X_CODEX_PARENT_THREAD_ID_HEADER; use super::X_CODEX_TURN_METADATA_HEADER; use super::X_CODEX_WINDOW_ID_HEADER; use super::X_OPENAI_SUBAGENT_HEADER; -use codex_api::api_bridge::CoreAuthProvider; +use codex_api::CoreAuthProvider; use codex_app_server_protocol::AuthMode; use codex_model_provider_info::WireApi; use codex_model_provider_info::create_oss_provider_with_base_url; @@ -23,6 +24,7 @@ fn test_model_client(session_source: SessionSource) -> ModelClient { ModelClient::new( /*auth_manager*/ None, ThreadId::new(), + /*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(), provider, session_source, /*model_verbosity*/ None, @@ -107,6 +109,10 @@ fn build_ws_client_metadata_includes_window_lineage_and_turn_metadata() { assert_eq!( client_metadata, std::collections::HashMap::from([ + ( + X_CODEX_INSTALLATION_ID_HEADER.to_string(), + "11111111-1111-4111-8111-111111111111".to_string(), + ), ( X_CODEX_WINDOW_ID_HEADER.to_string(), format!("{conversation_id}:1"), diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 0da24f4ca2..e7c8f6114f 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -24,6 +24,7 @@ use crate::compact_remote::run_inline_remote_auto_compact_task; use crate::config::ManagedFeatures; use crate::connectors; use crate::exec_policy::ExecPolicyManager; +use crate::installation_id::resolve_installation_id; use crate::parse_turn_item; use crate::path_utils::normalize_for_native_workdir; use crate::realtime_conversation::RealtimeConversationManager; @@ -70,11 +71,11 @@ use codex_login::AuthManager; use codex_login::CodexAuth; use codex_login::auth_env_telemetry::collect_auth_env_telemetry; use codex_login::default_client::originator; -use codex_mcp::mcp_connection_manager::McpConnectionManager; -use codex_mcp::mcp_connection_manager::SandboxState; -use codex_mcp::mcp_connection_manager::ToolInfo as McpToolInfo; -use codex_mcp::mcp_connection_manager::codex_apps_tools_cache_key; -use codex_mcp::mcp_connection_manager::filter_non_codex_apps_mcp_tools_only; +use codex_mcp::McpConnectionManager; +use codex_mcp::SandboxState; +use codex_mcp::ToolInfo as McpToolInfo; +use codex_mcp::codex_apps_tools_cache_key; +use codex_mcp::filter_non_codex_apps_mcp_tools_only; #[cfg(test)] use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_models_manager::manager::ModelsManager; @@ -149,7 +150,6 @@ use rmcp::model::PaginatedRequestParams; use rmcp::model::ReadResourceRequestParams; use rmcp::model::ReadResourceResult; use rmcp::model::RequestId; -use serde_json; use serde_json::Value; use tokio::sync::Mutex; use tokio::sync::RwLock; @@ -322,12 +322,12 @@ use crate::util::backoff; use crate::windows_sandbox::WindowsSandboxLevelExt; use codex_async_utils::OrCancelExt; use codex_git_utils::get_git_repo_root; -use codex_mcp::mcp::CODEX_APPS_MCP_SERVER_NAME; -use codex_mcp::mcp::auth::compute_auth_statuses; -use codex_mcp::mcp::with_codex_apps_mcp; +use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; +use codex_mcp::compute_auth_statuses; +use codex_mcp::with_codex_apps_mcp; use codex_otel::SessionTelemetry; +use codex_otel::THREAD_STARTED_METRIC; use codex_otel::TelemetryAuthMode; -use codex_otel::metrics::names::THREAD_STARTED_METRIC; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; @@ -407,8 +407,6 @@ pub(crate) type SessionLoopTermination = Shared>; pub struct CodexSpawnOk { pub codex: Codex, pub thread_id: ThreadId, - #[deprecated(note = "use thread_id")] - pub conversation_id: ThreadId, } pub(crate) struct CodexSpawnArgs { @@ -618,6 +616,14 @@ impl Codex { dynamic_tools }; + let developer_instructions_override = config + .developer_instructions_override + .clone() + .or_else(|| conversation_history.get_developer_instructions()); + let developer_instructions = developer_instructions_override + .clone() + .unwrap_or_else(|| config.developer_instructions.clone()); + // TODO (aibrahim): Consolidate config.model and config.model_reasoning_effort into config.collaboration_mode // to avoid extracting these fields separately and constructing CollaborationMode here. let collaboration_mode = CollaborationMode { @@ -633,7 +639,8 @@ impl Codex { collaboration_mode, model_reasoning_summary: config.model_reasoning_summary, service_tier: config.service_tier, - developer_instructions: config.developer_instructions.clone(), + developer_instructions, + developer_instructions_override, user_instructions, personality: config.personality, base_instructions, @@ -701,12 +708,7 @@ impl Codex { session_loop_termination: session_loop_termination_from_handle(session_loop_handle), }; - #[allow(deprecated)] - Ok(CodexSpawnOk { - codex, - thread_id, - conversation_id: thread_id, - }) + Ok(CodexSpawnOk { codex, thread_id }) } /// Submit the `op` wrapped in a `Submission` with a unique ID. @@ -1110,6 +1112,10 @@ pub(crate) struct SessionConfiguration { /// Developer instructions that supplement the base instructions. developer_instructions: Option, + /// Explicit developer instructions override, preserving `null` as distinct + /// from a missing override. + developer_instructions_override: Option>, + /// Model instructions that are appended to the base instructions. user_instructions: Option, @@ -1560,6 +1566,9 @@ impl Session { .base_instructions .clone() .map(|text| BaseInstructions { text }), + session_configuration + .developer_instructions_override + .clone(), session_configuration.dynamic_tools.clone(), if session_configuration.persist_extended_history { EventPersistenceMode::Extended @@ -1926,6 +1935,7 @@ impl Session { }); } + let installation_id = resolve_installation_id(&config.codex_home).await?; let services = SessionServices { // Initialize the MCP connection manager with an uninitialized // instance. It will be replaced with one created via @@ -1969,6 +1979,7 @@ impl Session { model_client: ModelClient::new( Some(Arc::clone(&auth_manager)), conversation_id, + installation_id, session_configuration.provider.clone(), session_configuration.session_source.clone(), config.model_verbosity, @@ -4806,8 +4817,8 @@ mod handlers { use crate::tasks::UserShellCommandMode; use crate::tasks::UserShellCommandTask; use crate::tasks::execute_user_shell_command; - use codex_mcp::mcp::auth::compute_auth_statuses; - use codex_mcp::mcp::collect_mcp_snapshot_from_manager; + use codex_mcp::collect_mcp_snapshot_from_manager; + use codex_mcp::compute_auth_statuses; use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::ErrorEvent; use codex_protocol::protocol::Event; diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index f93e32bcb7..2e3396aeba 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -15,7 +15,7 @@ use crate::tools::format_exec_output_str; use codex_features::Features; use codex_login::CodexAuth; -use codex_mcp::mcp_connection_manager::ToolInfo; +use codex_mcp::ToolInfo; use codex_model_provider_info::ModelProviderInfo; use codex_models_manager::bundled_models_response; use codex_models_manager::model_info; @@ -36,9 +36,9 @@ use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::RequestPermissionProfile; use tracing::Span; +use crate::RolloutRecorderParams; use crate::rollout::policy::EventPersistenceMode; use crate::rollout::recorder::RolloutRecorder; -use crate::rollout::recorder::RolloutRecorderParams; use crate::state::TaskKind; use crate::tasks::SessionTask; use crate::tasks::SessionTaskContext; @@ -255,6 +255,7 @@ fn test_model_client_session() -> crate::client::ModelClientSession { /*auth_manager*/ None, ThreadId::try_from("00000000-0000-4000-8000-000000000001") .expect("test thread id should be valid"), + /*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(), ModelProviderInfo::create_openai_provider(/* base_url */ /*base_url*/ None), codex_protocol::protocol::SessionSource::Exec, /*model_verbosity*/ None, @@ -1853,6 +1854,7 @@ async fn set_rate_limits_retains_previous_credits() { collaboration_mode, model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), + developer_instructions_override: config.developer_instructions_override.clone(), user_instructions: config.user_instructions.clone(), service_tier: None, personality: config.personality, @@ -1955,6 +1957,7 @@ async fn set_rate_limits_updates_plan_type_when_present() { collaboration_mode, model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), + developer_instructions_override: config.developer_instructions_override.clone(), user_instructions: config.user_instructions.clone(), service_tier: None, personality: config.personality, @@ -2227,6 +2230,7 @@ async fn attach_rollout_recorder(session: &Arc) -> PathBuf { /*forked_from_id*/ None, SessionSource::Exec, Some(BaseInstructions::default()), + /*developer_instructions*/ None, Vec::new(), EventPersistenceMode::Limited, ), @@ -2304,6 +2308,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati collaboration_mode, model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), + developer_instructions_override: config.developer_instructions_override.clone(), user_instructions: config.user_instructions.clone(), service_tier: None, personality: config.personality, @@ -2570,6 +2575,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() { collaboration_mode, model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), + developer_instructions_override: config.developer_instructions_override.clone(), user_instructions: config.user_instructions.clone(), service_tier: None, personality: config.personality, @@ -2673,6 +2679,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { collaboration_mode, model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), + developer_instructions_override: config.developer_instructions_override.clone(), user_instructions: config.user_instructions.clone(), service_tier: None, personality: config.personality, @@ -2766,6 +2773,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { model_client: ModelClient::new( Some(auth_manager.clone()), conversation_id, + /*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(), session_configuration.provider.clone(), session_configuration.session_source.clone(), config.model_verbosity, @@ -3513,6 +3521,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( collaboration_mode, model_reasoning_summary: config.model_reasoning_summary, developer_instructions: config.developer_instructions.clone(), + developer_instructions_override: config.developer_instructions_override.clone(), user_instructions: config.user_instructions.clone(), service_tier: None, personality: config.personality, @@ -3606,6 +3615,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( model_client: ModelClient::new( Some(Arc::clone(&auth_manager)), conversation_id, + /*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(), session_configuration.provider.clone(), session_configuration.session_source.clone(), config.model_verbosity, @@ -4089,18 +4099,13 @@ async fn handle_output_item_done_records_image_save_history_message() { let image_output_dir = image_output_path .parent() .expect("generated image path should have a parent"); - let save_message: ResponseItem = DeveloperInstructions::new(format!( - "Generated images are saved to {} as {} by default.", + let image_message: ResponseItem = DeveloperInstructions::new(format!( + "Generated images are saved to {} as {} by default.\nIf you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it.", image_output_dir.display(), image_output_path.display(), )) .into(); - let copy_message: ResponseItem = DeveloperInstructions::new( - "If you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it." - .to_string(), - ) - .into(); - assert_eq!(history.raw_items(), &[save_message, copy_message, item]); + assert_eq!(history.raw_items(), &[image_message, item]); assert_eq!( std::fs::read(&expected_saved_path).expect("saved file"), b"foo" @@ -4269,6 +4274,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline /*forked_from_id*/ None, SessionSource::Exec, Some(BaseInstructions::default()), + /*developer_instructions*/ None, Vec::new(), EventPersistenceMode::Limited, ), @@ -4366,6 +4372,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_full_rei /*forked_from_id*/ None, SessionSource::Exec, Some(BaseInstructions::default()), + /*developer_instructions*/ None, Vec::new(), EventPersistenceMode::Limited, ), diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 8ccf7ffc11..dfb8733646 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -4506,6 +4506,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { experimental_realtime_ws_startup_context: None, base_instructions: None, developer_instructions: None, + developer_instructions_override: None, guardian_developer_instructions: None, include_permissions_instructions: true, include_apps_instructions: true, @@ -4651,6 +4652,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { experimental_realtime_ws_startup_context: None, base_instructions: None, developer_instructions: None, + developer_instructions_override: None, guardian_developer_instructions: None, include_permissions_instructions: true, include_apps_instructions: true, @@ -4794,6 +4796,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { experimental_realtime_ws_startup_context: None, base_instructions: None, developer_instructions: None, + developer_instructions_override: None, guardian_developer_instructions: None, include_permissions_instructions: true, include_apps_instructions: true, @@ -4923,6 +4926,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { experimental_realtime_ws_startup_context: None, base_instructions: None, developer_instructions: None, + developer_instructions_override: None, guardian_developer_instructions: None, include_permissions_instructions: true, include_apps_instructions: true, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 10435fb22f..e9e939b0b6 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -54,7 +54,7 @@ use codex_features::FeatureConfigSource; use codex_features::FeatureOverrides; use codex_features::Features; use codex_login::AuthManagerConfig; -use codex_mcp::mcp::McpConfig; +use codex_mcp::McpConfig; use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID; use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::OLLAMA_CHAT_PROVIDER_REMOVED_ERROR; @@ -100,7 +100,7 @@ mod network_proxy_spec; mod permissions; #[cfg(test)] mod schema; -pub mod service; +pub(crate) mod service; pub use codex_config::Constrained; pub use codex_config::ConstraintError; pub use codex_config::ConstraintResult; @@ -248,6 +248,10 @@ pub struct Config { /// Developer instructions override injected as a separate message. pub developer_instructions: Option, + /// Explicit developer instructions override, preserving `null` as distinct + /// from a missing override. + pub developer_instructions_override: Option>, + /// Guardian-specific developer instructions override from requirements.toml. pub guardian_developer_instructions: Option, @@ -1750,6 +1754,7 @@ impl Config { let file_base_instructions = Self::try_read_non_empty_file(model_instructions_path, "model instructions file")?; let base_instructions = base_instructions.or_else(|| file_base_instructions.map(Some)); + let developer_instructions_override = developer_instructions.clone(); let developer_instructions = developer_instructions.unwrap_or_else(|| cfg.developer_instructions.clone()); let include_permissions_instructions = config_profile @@ -1934,6 +1939,7 @@ impl Config { base_instructions, personality, developer_instructions, + developer_instructions_override, compact_prompt, commit_attribution, include_permissions_instructions, diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 334e160f12..ad3447eaf3 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -40,14 +40,14 @@ use codex_login::CodexAuth; use codex_login::default_client::create_client; use codex_login::default_client::is_first_party_chat_originator; use codex_login::default_client::originator; -use codex_mcp::mcp::CODEX_APPS_MCP_SERVER_NAME; -use codex_mcp::mcp::ToolPluginProvenance; -use codex_mcp::mcp::auth::compute_auth_statuses; -use codex_mcp::mcp::with_codex_apps_mcp; -use codex_mcp::mcp_connection_manager::McpConnectionManager; -use codex_mcp::mcp_connection_manager::SandboxState; -use codex_mcp::mcp_connection_manager::ToolInfo; -use codex_mcp::mcp_connection_manager::codex_apps_tools_cache_key; +use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; +use codex_mcp::McpConnectionManager; +use codex_mcp::SandboxState; +use codex_mcp::ToolInfo; +use codex_mcp::ToolPluginProvenance; +use codex_mcp::codex_apps_tools_cache_key; +use codex_mcp::compute_auth_statuses; +use codex_mcp::with_codex_apps_mcp; pub use codex_connectors::CONNECTORS_CACHE_TTL; const CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS: Duration = Duration::from_secs(30); diff --git a/codex-rs/core/src/connectors_tests.rs b/codex-rs/core/src/connectors_tests.rs index 43311790d3..c691368543 100644 --- a/codex-rs/core/src/connectors_tests.rs +++ b/codex-rs/core/src/connectors_tests.rs @@ -12,8 +12,8 @@ use codex_config::types::AppToolConfig; use codex_config::types::AppToolsConfig; use codex_config::types::AppsDefaultConfig; use codex_features::Feature; -use codex_mcp::mcp::CODEX_APPS_MCP_SERVER_NAME; -use codex_mcp::mcp_connection_manager::ToolInfo; +use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; +use codex_mcp::ToolInfo; use codex_utils_absolute_path::AbsolutePathBuf; use core_test_support::TempDirExt; use pretty_assertions::assert_eq; diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 0696263f4d..870e281f23 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -417,7 +417,7 @@ fn record_windows_sandbox_spawn_failure( } else { "legacy" }; - if let Some(metrics) = codex_otel::metrics::global() { + if let Some(metrics) = codex_otel::global() { let _ = metrics.counter( "codex.windows_sandbox.createprocessasuserw_failed", /*inc*/ 1, diff --git a/codex-rs/core/src/external_agent_config.rs b/codex-rs/core/src/external_agent_config.rs index 862a4a28ba..47218a22af 100644 --- a/codex-rs/core/src/external_agent_config.rs +++ b/codex-rs/core/src/external_agent_config.rs @@ -677,7 +677,7 @@ fn emit_migration_metric( item_type: ExternalAgentConfigMigrationItemType, skills_count: Option, ) { - let Some(metrics) = codex_otel::metrics::global() else { + let Some(metrics) = codex_otel::global() else { return; }; let tags = migration_metric_tags(item_type, skills_count); diff --git a/codex-rs/core/src/installation_id.rs b/codex-rs/core/src/installation_id.rs new file mode 100644 index 0000000000..940e17eb01 --- /dev/null +++ b/codex-rs/core/src/installation_id.rs @@ -0,0 +1,145 @@ +use std::fs::OpenOptions; +use std::io::Read; +use std::io::Result; +use std::io::Seek; +use std::io::SeekFrom; +use std::io::Write; +use std::path::Path; + +#[cfg(unix)] +use std::os::unix::fs::OpenOptionsExt; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; + +use tokio::fs; +use uuid::Uuid; + +pub(crate) const INSTALLATION_ID_FILENAME: &str = "installation_id"; + +pub(crate) async fn resolve_installation_id(codex_home: &Path) -> Result { + let path = codex_home.join(INSTALLATION_ID_FILENAME); + fs::create_dir_all(codex_home).await?; + tokio::task::spawn_blocking(move || { + let mut options = OpenOptions::new(); + options.read(true).write(true).create(true); + + #[cfg(unix)] + { + options.mode(0o644); + } + + let mut file = options.open(&path)?; + file.lock()?; + + #[cfg(unix)] + { + let metadata = file.metadata()?; + let current_mode = metadata.permissions().mode() & 0o777; + if current_mode != 0o644 { + let mut permissions = metadata.permissions(); + permissions.set_mode(0o644); + file.set_permissions(permissions)?; + } + } + + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + let trimmed = contents.trim(); + if !trimmed.is_empty() + && let Ok(existing) = Uuid::parse_str(trimmed) + { + return Ok(existing.to_string()); + } + + let installation_id = Uuid::new_v4().to_string(); + file.set_len(0)?; + file.seek(SeekFrom::Start(0))?; + file.write_all(installation_id.as_bytes())?; + file.flush()?; + file.sync_all()?; + + Ok(installation_id) + }) + .await? +} + +#[cfg(test)] +mod tests { + use super::INSTALLATION_ID_FILENAME; + use super::resolve_installation_id; + use pretty_assertions::assert_eq; + use tempfile::TempDir; + use uuid::Uuid; + + #[cfg(unix)] + use std::os::unix::fs::PermissionsExt; + + #[tokio::test] + async fn resolve_installation_id_generates_and_persists_uuid() { + let codex_home = TempDir::new().expect("create temp dir"); + let persisted_path = codex_home.path().join(INSTALLATION_ID_FILENAME); + + let installation_id = resolve_installation_id(codex_home.path()) + .await + .expect("resolve installation id"); + + assert_eq!( + std::fs::read_to_string(&persisted_path).expect("read persisted installation id"), + installation_id + ); + assert!(Uuid::parse_str(&installation_id).is_ok()); + + #[cfg(unix)] + { + let mode = std::fs::metadata(&persisted_path) + .expect("read installation id metadata") + .permissions() + .mode() + & 0o777; + assert_eq!(mode, 0o644); + } + } + + #[tokio::test] + async fn resolve_installation_id_reuses_existing_uuid() { + let codex_home = TempDir::new().expect("create temp dir"); + let existing = Uuid::new_v4().to_string().to_uppercase(); + std::fs::write( + codex_home.path().join(INSTALLATION_ID_FILENAME), + existing.clone(), + ) + .expect("write installation id"); + + let resolved = resolve_installation_id(codex_home.path()) + .await + .expect("resolve installation id"); + + assert_eq!( + resolved, + Uuid::parse_str(existing.as_str()) + .expect("parse existing installation id") + .to_string() + ); + } + + #[tokio::test] + async fn resolve_installation_id_rewrites_invalid_file_contents() { + let codex_home = TempDir::new().expect("create temp dir"); + std::fs::write( + codex_home.path().join(INSTALLATION_ID_FILENAME), + "not-a-uuid", + ) + .expect("write invalid installation id"); + + let resolved = resolve_installation_id(codex_home.path()) + .await + .expect("resolve installation id"); + + assert!(Uuid::parse_str(&resolved).is_ok()); + assert_eq!( + std::fs::read_to_string(codex_home.path().join(INSTALLATION_ID_FILENAME)) + .expect("read rewritten installation id"), + resolved + ); + } +} diff --git a/codex-rs/core/src/instructions/mod.rs b/codex-rs/core/src/instructions/mod.rs index 5af9a6e890..a1f77ba5e8 100644 --- a/codex-rs/core/src/instructions/mod.rs +++ b/codex-rs/core/src/instructions/mod.rs @@ -1,2 +1 @@ -pub use codex_instructions::USER_INSTRUCTIONS_PREFIX; pub(crate) use codex_instructions::UserInstructions; diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index d68c2b2992..83232de734 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -10,7 +10,7 @@ mod apps; mod arc_monitor; mod client; mod client_common; -pub mod codex; +pub(crate) mod codex; mod realtime_context; mod realtime_conversation; pub use codex::SteerInputError; @@ -38,28 +38,41 @@ mod flags; mod git_info_tests; mod guardian; mod hook_runtime; -pub mod instructions; -pub mod landlock; -pub mod mcp; +mod installation_id; +pub(crate) mod instructions; +pub(crate) mod landlock; +pub use landlock::spawn_command_under_linux_sandbox; +pub(crate) mod mcp; mod mcp_skill_dependencies; mod mcp_tool_approval_templates; mod network_policy_decision; -pub mod network_proxy_loader; +pub(crate) mod network_proxy_loader; +pub use mcp::McpManager; +pub use network_proxy_loader::MtimeConfigReloader; +pub use network_proxy_loader::build_network_proxy_state; +pub use network_proxy_loader::build_network_proxy_state_and_reloader; mod original_image_detail; -pub use codex_mcp::mcp_connection_manager; -pub use codex_mcp::mcp_connection_manager::MCP_SANDBOX_STATE_CAPABILITY; -pub use codex_mcp::mcp_connection_manager::MCP_SANDBOX_STATE_METHOD; -pub use codex_mcp::mcp_connection_manager::SandboxState; +pub use codex_mcp::MCP_SANDBOX_STATE_CAPABILITY; +pub use codex_mcp::MCP_SANDBOX_STATE_METHOD; +pub use codex_mcp::SandboxState; mod mcp_tool_call; mod memories; -pub mod mention_syntax; -pub mod message_history; -pub mod utils; +pub(crate) mod mention_syntax; +pub(crate) mod message_history; +pub(crate) mod utils; +pub use mention_syntax::PLUGIN_TEXT_MENTION_SIGIL; +pub use mention_syntax::TOOL_MENTION_SIGIL; +pub use message_history::HistoryEntry as MessageHistoryEntry; +pub use message_history::append_entry as append_message_history_entry; +pub use message_history::history_metadata as message_history_metadata; +pub use message_history::lookup as lookup_message_history_entry; pub use utils::path_utils; pub mod personality_migration; pub mod plugins; #[doc(hidden)] -pub mod prompt_debug; +pub(crate) mod prompt_debug; +#[doc(hidden)] +pub use prompt_debug::build_prompt_input; pub(crate) mod mentions { pub(crate) use crate::plugins::build_connector_slug_counts; pub(crate) use crate::plugins::build_skill_name_counts; @@ -102,54 +115,63 @@ mod event_mapping; pub mod review_format; pub mod review_prompts; mod thread_manager; -pub mod web_search; -pub mod windows_sandbox_read_grants; +pub(crate) mod web_search; +pub(crate) mod windows_sandbox_read_grants; pub use thread_manager::ForkSnapshot; pub use thread_manager::NewThread; pub use thread_manager::ThreadManager; +pub use web_search::web_search_action_detail; +pub use web_search::web_search_detail; +pub use windows_sandbox_read_grants::grant_read_root_non_elevated; #[deprecated(note = "use ThreadManager")] pub type ConversationManager = ThreadManager; #[deprecated(note = "use NewThread")] pub type NewConversation = NewThread; #[deprecated(note = "use CodexThread")] pub type CodexConversation = CodexThread; -pub mod project_doc; +pub(crate) mod project_doc; +pub use project_doc::DEFAULT_PROJECT_DOC_FILENAME; +pub use project_doc::LOCAL_PROJECT_DOC_FILENAME; +pub use project_doc::discover_project_doc_paths; +pub use project_doc::read_project_docs; mod rollout; pub(crate) mod safety; pub mod seatbelt; mod session_rollout_init_error; pub mod shell; -pub mod shell_snapshot; +pub(crate) mod shell_snapshot; pub mod spawn; -pub mod state_db_bridge; +pub(crate) mod state_db_bridge; +pub use state_db_bridge::StateDbHandle; +pub use state_db_bridge::get_state_db; mod thread_rollout_truncation; mod tools; -pub mod turn_diff_tracker; +pub(crate) mod turn_diff_tracker; mod turn_metadata; mod turn_timing; pub use rollout::ARCHIVED_SESSIONS_SUBDIR; +pub use rollout::Cursor; +pub use rollout::EventPersistenceMode; pub use rollout::INTERACTIVE_SESSION_SOURCES; pub use rollout::RolloutRecorder; pub use rollout::RolloutRecorderParams; pub use rollout::SESSIONS_SUBDIR; pub use rollout::SessionMeta; +pub use rollout::ThreadItem; +pub use rollout::ThreadSortKey; +pub use rollout::ThreadsPage; pub use rollout::append_thread_name; pub use rollout::find_archived_thread_path_by_id_str; #[deprecated(note = "use find_thread_path_by_id_str")] pub use rollout::find_conversation_path_by_id_str; pub use rollout::find_thread_name_by_id; +pub use rollout::find_thread_names_by_ids; pub use rollout::find_thread_path_by_id_str; pub use rollout::find_thread_path_by_name_str; -pub use rollout::list::Cursor; -pub use rollout::list::ThreadItem; -pub use rollout::list::ThreadSortKey; -pub use rollout::list::ThreadsPage; -pub use rollout::list::parse_cursor; -pub use rollout::list::read_head_for_summary; -pub use rollout::list::read_session_meta_line; -pub use rollout::policy::EventPersistenceMode; +pub use rollout::parse_cursor; +pub use rollout::read_head_for_summary; +pub use rollout::read_session_meta_line; pub use rollout::rollout_date_parts; -pub use rollout::session_index::find_thread_names_by_ids; mod function_tool; mod state; mod tasks; @@ -158,6 +180,7 @@ pub mod util; pub use client::ModelClient; pub use client::ModelClientSession; +pub use client::X_CODEX_INSTALLATION_ID_HEADER; pub use client::X_CODEX_TURN_METADATA_HEADER; pub use client_common::Prompt; pub use client_common::REVIEW_PROMPT; @@ -172,5 +195,7 @@ pub use exec_policy::load_exec_policy; pub use file_watcher::FileWatcherEvent; pub use turn_metadata::build_turn_metadata_header; pub mod compact; -pub mod memory_trace; +pub(crate) mod memory_trace; +pub use memory_trace::BuiltMemory; +pub use memory_trace::build_memories_from_trace_files; pub mod otel_init; diff --git a/codex-rs/core/src/mcp.rs b/codex-rs/core/src/mcp.rs index d82452c54d..83becdc07e 100644 --- a/codex-rs/core/src/mcp.rs +++ b/codex-rs/core/src/mcp.rs @@ -5,10 +5,10 @@ use crate::config::Config; use crate::plugins::PluginsManager; use codex_config::McpServerConfig; use codex_login::CodexAuth; -use codex_mcp::mcp::ToolPluginProvenance; -use codex_mcp::mcp::configured_mcp_servers; -use codex_mcp::mcp::effective_mcp_servers; -use codex_mcp::mcp::tool_plugin_provenance as collect_tool_plugin_provenance; +use codex_mcp::ToolPluginProvenance; +use codex_mcp::configured_mcp_servers; +use codex_mcp::effective_mcp_servers; +use codex_mcp::tool_plugin_provenance as collect_tool_plugin_provenance; #[derive(Clone)] pub struct McpManager { diff --git a/codex-rs/core/src/mcp_skill_dependencies.rs b/codex-rs/core/src/mcp_skill_dependencies.rs index dfd8a4c5c3..fca38919a0 100644 --- a/codex-rs/core/src/mcp_skill_dependencies.rs +++ b/codex-rs/core/src/mcp_skill_dependencies.rs @@ -21,10 +21,10 @@ use crate::SkillMetadata; use crate::codex::Session; use crate::codex::TurnContext; use crate::skills::model::SkillToolDependency; -use codex_mcp::mcp::auth::McpOAuthLoginSupport; -use codex_mcp::mcp::auth::oauth_login_support; -use codex_mcp::mcp::auth::resolve_oauth_scopes; -use codex_mcp::mcp::auth::should_retry_without_scopes; +use codex_mcp::McpOAuthLoginSupport; +use codex_mcp::oauth_login_support; +use codex_mcp::resolve_oauth_scopes; +use codex_mcp::should_retry_without_scopes; const SKILL_MCP_DEPENDENCY_PROMPT_ID: &str = "skill_mcp_dependency_install"; const MCP_DEPENDENCY_OPTION_INSTALL: &str = "Install"; diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 344e46d0be..2a94d13f89 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -32,7 +32,7 @@ use codex_analytics::InvocationType; use codex_analytics::build_track_events_context; use codex_config::types::AppToolApproval; use codex_features::Feature; -use codex_mcp::mcp::CODEX_APPS_MCP_SERVER_NAME; +use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_otel::sanitize_metric_tag_value; use codex_protocol::mcp::CallToolResult; use codex_protocol::openai_models::InputModality; diff --git a/codex-rs/core/src/otel_init.rs b/codex-rs/core/src/otel_init.rs index c732692d62..4a0e7cd984 100644 --- a/codex-rs/core/src/otel_init.rs +++ b/codex-rs/core/src/otel_init.rs @@ -3,11 +3,11 @@ use codex_config::types::OtelExporterKind as Kind; use codex_config::types::OtelHttpProtocol as Protocol; use codex_features::Feature; use codex_login::default_client::originator; +use codex_otel::OtelExporter; +use codex_otel::OtelHttpProtocol; use codex_otel::OtelProvider; -use codex_otel::config::OtelExporter; -use codex_otel::config::OtelHttpProtocol; -use codex_otel::config::OtelSettings; -use codex_otel::config::OtelTlsConfig as OtelTlsSettings; +use codex_otel::OtelSettings; +use codex_otel::OtelTlsConfig as OtelTlsSettings; use std::error::Error; /// Build an OpenTelemetry provider from the app Config. diff --git a/codex-rs/core/src/personality_migration_tests.rs b/codex-rs/core/src/personality_migration_tests.rs index de1070ad34..3c8bb077fb 100644 --- a/codex-rs/core/src/personality_migration_tests.rs +++ b/codex-rs/core/src/personality_migration_tests.rs @@ -43,6 +43,7 @@ async fn write_session_with_user_event(codex_home: &Path) -> io::Result<()> { agent_role: None, model_provider: None, base_instructions: None, + developer_instructions: None, dynamic_tools: None, memory_mode: None, }, diff --git a/codex-rs/core/src/plugins/injection.rs b/codex-rs/core/src/plugins/injection.rs index e482010b2e..3e5bb6ecb5 100644 --- a/codex-rs/core/src/plugins/injection.rs +++ b/codex-rs/core/src/plugins/injection.rs @@ -7,8 +7,8 @@ use codex_protocol::models::ResponseItem; use crate::connectors; use crate::plugins::PluginCapabilitySummary; use crate::plugins::render_explicit_plugin_instructions; -use codex_mcp::mcp::CODEX_APPS_MCP_SERVER_NAME; -use codex_mcp::mcp_connection_manager::ToolInfo; +use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; +use codex_mcp::ToolInfo; pub(crate) fn build_plugin_injections( mentioned_plugins: &[PluginCapabilitySummary], diff --git a/codex-rs/core/src/plugins/startup_sync.rs b/codex-rs/core/src/plugins/startup_sync.rs index 14608d1313..5bca32de13 100644 --- a/codex-rs/core/src/plugins/startup_sync.rs +++ b/codex-rs/core/src/plugins/startup_sync.rs @@ -6,8 +6,8 @@ use std::process::Stdio; use std::sync::Arc; use std::time::Duration; -use codex_otel::metrics::names::CURATED_PLUGINS_STARTUP_SYNC_FINAL_METRIC; -use codex_otel::metrics::names::CURATED_PLUGINS_STARTUP_SYNC_METRIC; +use codex_otel::CURATED_PLUGINS_STARTUP_SYNC_FINAL_METRIC; +use codex_otel::CURATED_PLUGINS_STARTUP_SYNC_METRIC; use reqwest::Client; use serde::Deserialize; use tempfile::TempDir; @@ -451,7 +451,7 @@ fn emit_curated_plugins_startup_sync_counter( transport: &'static str, status: &'static str, ) { - let Some(metrics) = codex_otel::metrics::global() else { + let Some(metrics) = codex_otel::global() else { return; }; let tags = [("transport", transport), ("status", status)]; diff --git a/codex-rs/core/src/realtime_conversation.rs b/codex-rs/core/src/realtime_conversation.rs index 4eb4278048..16f88650a4 100644 --- a/codex-rs/core/src/realtime_conversation.rs +++ b/codex-rs/core/src/realtime_conversation.rs @@ -12,9 +12,9 @@ use codex_api::RealtimeEventParser; use codex_api::RealtimeSessionConfig; use codex_api::RealtimeSessionMode; use codex_api::RealtimeWebsocketClient; -use codex_api::api_bridge::map_api_error; -use codex_api::endpoint::realtime_websocket::RealtimeWebsocketEvents; -use codex_api::endpoint::realtime_websocket::RealtimeWebsocketWriter; +use codex_api::RealtimeWebsocketEvents; +use codex_api::RealtimeWebsocketWriter; +use codex_api::map_api_error; use codex_app_server_protocol::AuthMode; use codex_config::config_toml::RealtimeWsMode; use codex_config::config_toml::RealtimeWsVersion; diff --git a/codex-rs/core/src/rollout.rs b/codex-rs/core/src/rollout.rs index c3a7218710..492b0bc058 100644 --- a/codex-rs/core/src/rollout.rs +++ b/codex-rs/core/src/rollout.rs @@ -1,17 +1,26 @@ use crate::config::Config; pub use codex_rollout::ARCHIVED_SESSIONS_SUBDIR; +pub use codex_rollout::Cursor; +pub use codex_rollout::EventPersistenceMode; pub use codex_rollout::INTERACTIVE_SESSION_SOURCES; pub use codex_rollout::RolloutRecorder; pub use codex_rollout::RolloutRecorderParams; pub use codex_rollout::SESSIONS_SUBDIR; pub use codex_rollout::SessionMeta; +pub use codex_rollout::ThreadItem; +pub use codex_rollout::ThreadSortKey; +pub use codex_rollout::ThreadsPage; pub use codex_rollout::append_thread_name; pub use codex_rollout::find_archived_thread_path_by_id_str; #[deprecated(note = "use find_thread_path_by_id_str")] pub use codex_rollout::find_conversation_path_by_id_str; pub use codex_rollout::find_thread_name_by_id; +pub use codex_rollout::find_thread_names_by_ids; pub use codex_rollout::find_thread_path_by_id_str; pub use codex_rollout::find_thread_path_by_name_str; +pub use codex_rollout::parse_cursor; +pub use codex_rollout::read_head_for_summary; +pub use codex_rollout::read_session_meta_line; pub use codex_rollout::rollout_date_parts; impl codex_rollout::RolloutConfigView for Config { @@ -36,24 +45,30 @@ impl codex_rollout::RolloutConfigView for Config { } } -pub mod list { - pub use codex_rollout::list::*; +pub(crate) mod list { + pub use codex_rollout::ThreadListConfig; + pub use codex_rollout::ThreadListLayout; + pub use codex_rollout::ThreadSortKey; + pub use codex_rollout::find_thread_path_by_id_str; + pub use codex_rollout::get_threads_in_root; } pub(crate) mod metadata { - pub(crate) use codex_rollout::metadata::builder_from_items; + pub(crate) use codex_rollout::builder_from_items; } -pub mod policy { - pub use codex_rollout::policy::*; +pub(crate) mod policy { + pub use codex_rollout::EventPersistenceMode; + pub use codex_rollout::should_persist_response_item_for_memories; } -pub mod recorder { - pub use codex_rollout::recorder::*; +pub(crate) mod recorder { + pub use codex_rollout::RolloutRecorder; } -pub mod session_index { - pub use codex_rollout::session_index::*; +pub(crate) mod session_index { + pub use codex_rollout::append_thread_name; + pub use codex_rollout::find_thread_name_by_id; } pub(crate) use crate::session_rollout_init_error::map_session_init_error; diff --git a/codex-rs/core/src/safety.rs b/codex-rs/core/src/safety.rs index dec7a00876..4ed88783a6 100644 --- a/codex-rs/core/src/safety.rs +++ b/codex-rs/core/src/safety.rs @@ -12,6 +12,11 @@ use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxType; use codex_sandboxing::get_platform_sandbox; +const PATCH_REJECTED_OUTSIDE_PROJECT_REASON: &str = + "writing outside of the project; rejected by user approval settings"; +const PATCH_REJECTED_READ_ONLY_REASON: &str = + "writing is blocked by read-only sandbox; rejected by user approval settings"; + #[derive(Debug, PartialEq)] pub enum SafetyCheck { AutoApprove { @@ -85,9 +90,7 @@ pub fn assess_patch_safety( None => { if rejects_sandbox_approval { SafetyCheck::Reject { - reason: - "writing outside of the project; rejected by user approval settings" - .to_string(), + reason: patch_rejection_reason(sandbox_policy).to_string(), } } else { SafetyCheck::AskUser @@ -97,14 +100,22 @@ pub fn assess_patch_safety( } } else if rejects_sandbox_approval { SafetyCheck::Reject { - reason: "writing outside of the project; rejected by user approval settings" - .to_string(), + reason: patch_rejection_reason(sandbox_policy).to_string(), } } else { SafetyCheck::AskUser } } +fn patch_rejection_reason(sandbox_policy: &SandboxPolicy) -> &'static str { + match sandbox_policy { + SandboxPolicy::ReadOnly { .. } => PATCH_REJECTED_READ_ONLY_REASON, + SandboxPolicy::WorkspaceWrite { .. } + | SandboxPolicy::DangerFullAccess + | SandboxPolicy::ExternalSandbox { .. } => PATCH_REJECTED_OUTSIDE_PROJECT_REASON, + } +} + fn is_write_patch_constrained_to_writable_paths( action: &ApplyPatchAction, file_system_sandbox_policy: &FileSystemSandboxPolicy, diff --git a/codex-rs/core/src/safety_tests.rs b/codex-rs/core/src/safety_tests.rs index e7f55693d1..5ebeb64e0e 100644 --- a/codex-rs/core/src/safety_tests.rs +++ b/codex-rs/core/src/safety_tests.rs @@ -162,8 +162,36 @@ fn granular_sandbox_approval_false_rejects_out_of_root_patch() { WindowsSandboxLevel::Disabled, ), SafetyCheck::Reject { - reason: "writing outside of the project; rejected by user approval settings" - .to_string(), + reason: PATCH_REJECTED_OUTSIDE_PROJECT_REASON.to_string(), + }, + ); +} + +#[test] +fn read_only_policy_rejects_patch_with_read_only_reason() { + let tmp = TempDir::new().unwrap(); + let cwd = tmp.path().to_path_buf(); + let action = ApplyPatchAction::new_add_for_test(&cwd.join("inside.txt"), "".to_string()); + let sandbox_policy = SandboxPolicy::new_read_only_policy(); + let file_system_sandbox_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, &cwd); + + assert!(!is_write_patch_constrained_to_writable_paths( + &action, + &file_system_sandbox_policy, + &cwd, + )); + assert_eq!( + assess_patch_safety( + &action, + AskForApproval::Never, + &sandbox_policy, + &file_system_sandbox_policy, + &cwd, + WindowsSandboxLevel::Disabled, + ), + SafetyCheck::Reject { + reason: PATCH_REJECTED_READ_ONLY_REASON.to_string(), }, ); } diff --git a/codex-rs/core/src/session_startup_prewarm.rs b/codex-rs/core/src/session_startup_prewarm.rs index 708401aedd..d3ab0e6198 100644 --- a/codex-rs/core/src/session_startup_prewarm.rs +++ b/codex-rs/core/src/session_startup_prewarm.rs @@ -13,9 +13,9 @@ use crate::codex::INITIAL_SUBMIT_ID; use crate::codex::Session; use crate::codex::build_prompt; use crate::codex::built_tools; +use codex_otel::STARTUP_PREWARM_AGE_AT_FIRST_TURN_METRIC; +use codex_otel::STARTUP_PREWARM_DURATION_METRIC; use codex_otel::SessionTelemetry; -use codex_otel::metrics::names::STARTUP_PREWARM_AGE_AT_FIRST_TURN_METRIC; -use codex_otel::metrics::names::STARTUP_PREWARM_DURATION_METRIC; use codex_protocol::error::Result as CodexResult; use codex_protocol::models::BaseInstructions; diff --git a/codex-rs/core/src/state/service.rs b/codex-rs/core/src/state/service.rs index a0600ddcec..e5c3a00cb2 100644 --- a/codex-rs/core/src/state/service.rs +++ b/codex-rs/core/src/state/service.rs @@ -17,7 +17,7 @@ use codex_analytics::AnalyticsEventsClient; use codex_exec_server::Environment; use codex_hooks::Hooks; use codex_login::AuthManager; -use codex_mcp::mcp_connection_manager::McpConnectionManager; +use codex_mcp::McpConnectionManager; use codex_models_manager::manager::ModelsManager; use codex_otel::SessionTelemetry; use codex_rollout::state_db::StateDbHandle; diff --git a/codex-rs/core/src/state_db_bridge.rs b/codex-rs/core/src/state_db_bridge.rs index f073c498b5..c588f039d2 100644 --- a/codex-rs/core/src/state_db_bridge.rs +++ b/codex-rs/core/src/state_db_bridge.rs @@ -1,18 +1,5 @@ use codex_rollout::state_db as rollout_state_db; pub use codex_rollout::state_db::StateDbHandle; -pub use codex_rollout::state_db::apply_rollout_items; -pub use codex_rollout::state_db::find_rollout_path_by_id; -pub use codex_rollout::state_db::get_dynamic_tools; -pub use codex_rollout::state_db::list_thread_ids_db; -pub use codex_rollout::state_db::list_threads_db; -pub use codex_rollout::state_db::mark_thread_memory_mode_polluted; -pub use codex_rollout::state_db::normalize_cwd_for_state_db; -pub use codex_rollout::state_db::open_if_present; -pub use codex_rollout::state_db::persist_dynamic_tools; -pub use codex_rollout::state_db::read_repair_rollout_path; -pub use codex_rollout::state_db::reconcile_rollout; -pub use codex_rollout::state_db::touch_thread_updated_at; -pub use codex_state::LogEntry; use crate::config::Config; diff --git a/codex-rs/core/src/stream_events_utils.rs b/codex-rs/core/src/stream_events_utils.rs index 75eb1a8c22..ac4334d683 100644 --- a/codex-rs/core/src/stream_events_utils.rs +++ b/codex-rs/core/src/stream_events_utils.rs @@ -379,17 +379,12 @@ pub(crate) async fn handle_non_tool_response_item( .parent() .unwrap_or(turn_context.config.codex_home.as_path()); let message: ResponseItem = DeveloperInstructions::new(format!( - "Generated images are saved to {} as {} by default.", + "Generated images are saved to {} as {} by default.\nIf you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it.", image_output_dir.display(), image_output_path.display(), )) .into(); - let copy_message: ResponseItem = DeveloperInstructions::new( - "If you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it." - .to_string(), - ) - .into(); - sess.record_conversation_items(turn_context, &[message, copy_message]) + sess.record_conversation_items(turn_context, &[message]) .await; } Err(err) => { diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index f33e6886f4..5da3650bf8 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -33,10 +33,10 @@ use crate::state::TaskKind; use codex_login::AuthManager; use codex_models_manager::manager::ModelsManager; use codex_otel::SessionTelemetry; -use codex_otel::metrics::names::TURN_E2E_DURATION_METRIC; -use codex_otel::metrics::names::TURN_NETWORK_PROXY_METRIC; -use codex_otel::metrics::names::TURN_TOKEN_USAGE_METRIC; -use codex_otel::metrics::names::TURN_TOOL_CALL_METRIC; +use codex_otel::TURN_E2E_DURATION_METRIC; +use codex_otel::TURN_NETWORK_PROXY_METRIC; +use codex_otel::TURN_TOKEN_USAGE_METRIC; +use codex_otel::TURN_TOOL_CALL_METRIC; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; diff --git a/codex-rs/core/src/tasks/mod_tests.rs b/codex-rs/core/src/tasks/mod_tests.rs index 6573da5d4c..22bf0a99c8 100644 --- a/codex-rs/core/src/tasks/mod_tests.rs +++ b/codex-rs/core/src/tasks/mod_tests.rs @@ -1,8 +1,8 @@ use super::emit_turn_network_proxy_metric; +use codex_otel::MetricsClient; +use codex_otel::MetricsConfig; use codex_otel::SessionTelemetry; -use codex_otel::metrics::MetricsClient; -use codex_otel::metrics::MetricsConfig; -use codex_otel::metrics::names::TURN_NETWORK_PROXY_METRIC; +use codex_otel::TURN_NETWORK_PROXY_METRIC; use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; use opentelemetry::KeyValue; diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 87502eb721..d872116a87 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -124,11 +124,16 @@ pub(crate) async fn execute_user_shell_command( let use_login_shell = true; let session_shell = session.user_shell(); let display_command = session_shell.derive_exec_args(&command, use_login_shell); + let exec_env_map = create_env( + &turn_context.shell_environment_policy, + Some(session.conversation_id), + ); let exec_command = maybe_wrap_shell_lc_with_snapshot( &display_command, session_shell.as_ref(), turn_context.cwd.as_path(), &turn_context.shell_environment_policy.r#set, + &exec_env_map, ); let call_id = Uuid::new_v4().to_string(); @@ -156,10 +161,7 @@ pub(crate) async fn execute_user_shell_command( let exec_env = ExecRequest { command: exec_command.clone(), cwd: cwd.clone(), - env: create_env( - &turn_context.shell_environment_policy, - Some(session.conversation_id), - ), + env: exec_env_map, network: turn_context.network.clone(), // TODO(zhao-oai): Now that we have ExecExpiration::Cancellation, we // should use that instead of an "arbitrarily large" timeout here. diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index 6d93a1a329..2dd4ad5532 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -43,9 +43,11 @@ use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::TurnAbortedEvent; use codex_protocol::protocol::W3cTraceContext; +use codex_state::DirectionalThreadSpawnEdgeStatus; use futures::StreamExt; use futures::stream::FuturesUnordered; use std::collections::HashMap; +use std::collections::HashSet; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; @@ -401,6 +403,53 @@ impl ThreadManager { self.state.get_thread(thread_id).await } + /// List `thread_id` plus all known descendants in its spawn subtree. + pub async fn list_agent_subtree_thread_ids( + &self, + thread_id: ThreadId, + ) -> CodexResult> { + let thread = self.state.get_thread(thread_id).await?; + + let mut subtree_thread_ids = Vec::new(); + let mut seen_thread_ids = HashSet::new(); + subtree_thread_ids.push(thread_id); + seen_thread_ids.insert(thread_id); + + if let Some(state_db_ctx) = thread.state_db() { + for status in [ + DirectionalThreadSpawnEdgeStatus::Open, + DirectionalThreadSpawnEdgeStatus::Closed, + ] { + for descendant_id in state_db_ctx + .list_thread_spawn_descendants_with_status(thread_id, status) + .await + .map_err(|err| { + CodexErr::Fatal(format!("failed to load thread-spawn descendants: {err}")) + })? + { + if seen_thread_ids.insert(descendant_id) { + subtree_thread_ids.push(descendant_id); + } + } + } + } + + for descendant_id in thread + .codex + .session + .services + .agent_control + .list_live_agent_subtree_thread_ids(thread_id) + .await? + { + if seen_thread_ids.insert(descendant_id) { + subtree_thread_ids.push(descendant_id); + } + } + + Ok(subtree_thread_ids) + } + pub async fn start_thread(&self, config: Config) -> CodexResult { // Box delegated thread-spawn futures so these convenience wrappers do // not inline the full spawn path into every caller's async state. diff --git a/codex-rs/core/src/tools/handlers/mod.rs b/codex-rs/core/src/tools/handlers/mod.rs index bd37f47f65..1877060e66 100644 --- a/codex-rs/core/src/tools/handlers/mod.rs +++ b/codex-rs/core/src/tools/handlers/mod.rs @@ -1,5 +1,5 @@ pub(crate) mod agent_jobs; -pub mod apply_patch; +pub(crate) mod apply_patch; mod dynamic; mod js_repl; mod list_dir; diff --git a/codex-rs/core/src/tools/handlers/multi_agents.rs b/codex-rs/core/src/tools/handlers/multi_agents.rs index df66543da6..984e09a968 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents.rs @@ -63,7 +63,7 @@ pub(crate) use send_input::Handler as SendInputHandler; pub(crate) use spawn::Handler as SpawnAgentHandler; pub(crate) use wait::Handler as WaitAgentHandler; -pub mod close_agent; +pub(crate) mod close_agent; mod resume_agent; mod send_input; mod spawn; diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index 03f4b0780b..5f09c248a4 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -1,4 +1,5 @@ use super::*; +use crate::CodexThread; use crate::ThreadManager; use crate::codex::make_session_and_context; use crate::config::DEFAULT_AGENT_MAX_DEPTH; @@ -112,6 +113,74 @@ fn history_contains_inter_agent_communication( }) } +async fn wait_for_turn_aborted( + thread: &Arc, + expected_turn_id: &str, + expected_reason: TurnAbortReason, +) { + timeout(Duration::from_secs(5), async { + loop { + let event = thread + .next_event() + .await + .expect("child thread should emit events"); + if matches!( + event.msg, + EventMsg::TurnAborted(TurnAbortedEvent { + turn_id: Some(ref turn_id), + ref reason, + .. + }) if turn_id == expected_turn_id && *reason == expected_reason + ) { + break; + } + } + }) + .await + .expect("expected child turn to be interrupted"); +} + +async fn wait_for_redirected_envelope_in_history( + thread: &Arc, + expected: &InterAgentCommunication, +) { + timeout(Duration::from_secs(5), async { + loop { + let history_items = thread + .codex + .session + .clone_history() + .await + .raw_items() + .to_vec(); + let saw_envelope = + history_contains_inter_agent_communication(&history_items, expected); + let saw_user_message = history_items.iter().any(|item| { + matches!( + item, + ResponseItem::Message { role, content, .. } + if role == "user" + && content.iter().any(|content_item| matches!( + content_item, + ContentItem::InputText { text } + if text == &expected.content + )) + ) + }); + if saw_envelope { + assert!( + !saw_user_message, + "redirected followup should be stored as an assistant envelope, not a plain user message" + ); + break; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .expect("redirected followup envelope should appear in history"); +} + #[derive(Clone, Copy)] struct NeverEndingTask; @@ -757,7 +826,7 @@ async fn multi_agent_v2_followup_task_rejects_root_target_from_child() { agent_role: None, }); - let err = FollowupTaskHandlerV2 + let Err(err) = FollowupTaskHandlerV2 .handle(invocation( Arc::new(session), Arc::new(turn), @@ -769,7 +838,9 @@ async fn multi_agent_v2_followup_task_rejects_root_target_from_child() { })), )) .await - .expect_err("followup_task should reject the root target"); + else { + panic!("followup_task should reject the root target"); + }; assert_eq!( err, @@ -1201,6 +1272,7 @@ async fn multi_agent_v2_followup_task_interrupts_busy_child_without_losing_messa .expect("worker thread should exist"); let active_turn = thread.codex.session.new_default_turn().await; + let interrupted_turn_id = active_turn.sub_id.clone(); thread .codex .session @@ -1245,44 +1317,18 @@ async fn multi_agent_v2_followup_task_interrupts_busy_child_without_losing_messa ) })); - timeout(Duration::from_secs(5), async { - loop { - let history_items = thread - .codex - .session - .clone_history() - .await - .raw_items() - .to_vec(); - let saw_envelope = history_contains_inter_agent_communication( - &history_items, - &InterAgentCommunication::new( - AgentPath::root(), - AgentPath::try_from("/root/worker").expect("agent path"), - Vec::new(), - "continue".to_string(), - /*trigger_turn*/ true, - ), - ); - let saw_user_message = history_items.iter().any(|item| { - matches!( - item, - ResponseItem::Message { role, content, .. } - if role == "user" - && content.iter().any(|content_item| matches!( - content_item, - ContentItem::InputText { text } if text == "continue" - )) - ) - }); - if saw_envelope && !saw_user_message { - break; - } - tokio::time::sleep(Duration::from_millis(10)).await; - } - }) - .await - .expect("interrupting v2 followup_task should preserve the redirected message"); + wait_for_turn_aborted(&thread, &interrupted_turn_id, TurnAbortReason::Interrupted).await; + wait_for_redirected_envelope_in_history( + &thread, + &InterAgentCommunication::new( + AgentPath::root(), + AgentPath::try_from("/root/worker").expect("agent path"), + Vec::new(), + "continue".to_string(), + /*trigger_turn*/ true, + ), + ) + .await; let _ = thread .submit(Op::Shutdown {}) @@ -1554,7 +1600,7 @@ async fn multi_agent_v2_interrupted_turn_does_not_notify_parent() { } #[tokio::test] -async fn multi_agent_v2_spawn_includes_agent_id_key_when_named() { +async fn multi_agent_v2_spawn_omits_agent_id_when_named() { let (mut session, mut turn) = make_session_and_context().await; let manager = thread_manager(); let root = manager @@ -1586,7 +1632,7 @@ async fn multi_agent_v2_spawn_includes_agent_id_key_when_named() { let result: serde_json::Value = serde_json::from_str(&content).expect("spawn_agent result should be json"); - assert_eq!(result["agent_id"], serde_json::Value::Null); + assert!(result.get("agent_id").is_none()); assert_eq!(result["task_name"], "/root/test_process"); assert!(result.get("nickname").is_some()); assert_eq!(success, Some(true)); diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/followup_task.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/followup_task.rs index 312e17a316..e618f5da2b 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/followup_task.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/followup_task.rs @@ -1,13 +1,13 @@ use super::message_tool::FollowupTaskArgs; use super::message_tool::MessageDeliveryMode; -use super::message_tool::MessageToolResult; use super::message_tool::handle_message_string_tool; use super::*; +use crate::tools::context::FunctionToolOutput; pub(crate) struct Handler; impl ToolHandler for Handler { - type Output = MessageToolResult; + type Output = FunctionToolOutput; fn kind(&self) -> ToolKind { ToolKind::Function diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs index 3123e93f4c..c9c2fda51e 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs @@ -4,6 +4,7 @@ //! resulting `InterAgentCommunication` should wake the target immediately. use super::*; +use crate::tools::context::FunctionToolOutput; use codex_protocol::protocol::InterAgentCommunication; #[derive(Clone, Copy, PartialEq, Eq)] @@ -46,30 +47,6 @@ pub(crate) struct FollowupTaskArgs { pub(crate) interrupt: bool, } -#[derive(Debug, Serialize)] -/// Tool result shared by the MultiAgentV2 message-delivery tools. -pub(crate) struct MessageToolResult { - submission_id: String, -} - -impl ToolOutput for MessageToolResult { - fn log_preview(&self) -> String { - tool_output_json_text(self, "multi_agent_message") - } - - fn success_for_logging(&self) -> bool { - true - } - - fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem { - tool_output_response_item(call_id, payload, self, Some(true), "multi_agent_message") - } - - fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue { - tool_output_code_mode_result(self, "multi_agent_message") - } -} - fn message_content(message: String) -> Result { if message.trim().is_empty() { return Err(FunctionCallError::RespondToModel( @@ -86,7 +63,7 @@ pub(crate) async fn handle_message_string_tool( target: String, message: String, interrupt: bool, -) -> Result { +) -> Result { handle_message_submission( invocation, mode, @@ -103,15 +80,13 @@ async fn handle_message_submission( target: String, prompt: String, interrupt: bool, -) -> Result { +) -> Result { let ToolInvocation { session, turn, - payload, call_id, .. } = invocation; - let _ = payload; let receiver_thread_id = resolve_agent_target(&session, &turn, &target).await?; let receiver_agent = session .services @@ -186,7 +161,7 @@ async fn handle_message_submission( .into(), ) .await; - let submission_id = result?; + result?; - Ok(MessageToolResult { submission_id }) + Ok(FunctionToolOutput::from_text(String::new(), Some(true))) } diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/send_message.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/send_message.rs index a490d1100d..0d142623a9 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/send_message.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/send_message.rs @@ -1,13 +1,13 @@ use super::message_tool::MessageDeliveryMode; -use super::message_tool::MessageToolResult; use super::message_tool::SendMessageArgs; use super::message_tool::handle_message_string_tool; use super::*; +use crate::tools::context::FunctionToolOutput; pub(crate) struct Handler; impl ToolHandler for Handler { - type Output = MessageToolResult; + type Output = FunctionToolOutput; fn kind(&self) -> ToolKind { ToolKind::Function diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs index a1cdb121d1..0d460b1a04 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs @@ -5,6 +5,7 @@ use crate::agent::control::render_input_preview; use crate::agent::next_thread_spawn_depth; use crate::agent::role::DEFAULT_ROLE_NAME; use crate::agent::role::apply_role_to_config; +use codex_features::Feature; use codex_protocol::AgentPath; use codex_protocol::models::DeveloperInstructions; use codex_protocol::protocol::InterAgentCommunication; @@ -208,11 +209,18 @@ impl ToolHandler for Handler { ) })?; - Ok(SpawnAgentResult { - agent_id: None, - task_name, - nickname, - }) + let hide_agent_metadata = turn + .config + .features + .enabled(Feature::DebugHideSpawnAgentMetadata); + if hide_agent_metadata { + Ok(SpawnAgentResult::HiddenMetadata { task_name }) + } else { + Ok(SpawnAgentResult::WithNickname { + task_name, + nickname, + }) + } } } @@ -268,10 +276,15 @@ impl SpawnAgentArgs { } #[derive(Debug, Serialize)] -pub(crate) struct SpawnAgentResult { - agent_id: Option, - task_name: String, - nickname: Option, +#[serde(untagged)] +pub(crate) enum SpawnAgentResult { + WithNickname { + task_name: String, + nickname: Option, + }, + HiddenMetadata { + task_name: String, + }, } impl ToolOutput for SpawnAgentResult { diff --git a/codex-rs/core/src/tools/handlers/request_user_input.rs b/codex-rs/core/src/tools/handlers/request_user_input.rs index dcc9445a94..69d222aa87 100644 --- a/codex-rs/core/src/tools/handlers/request_user_input.rs +++ b/codex-rs/core/src/tools/handlers/request_user_input.rs @@ -5,6 +5,7 @@ use crate::tools::context::ToolPayload; use crate::tools::handlers::parse_arguments; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; +use codex_protocol::protocol::SessionSource; use codex_protocol::request_user_input::RequestUserInputArgs; use codex_tools::REQUEST_USER_INPUT_TOOL_NAME; use codex_tools::normalize_request_user_input_args; @@ -39,6 +40,12 @@ impl ToolHandler for RequestUserInputHandler { } }; + if matches!(turn.session_source, SessionSource::SubAgent(_)) { + return Err(FunctionCallError::RespondToModel( + "request_user_input can only be used by the root thread".to_string(), + )); + } + let mode = session.collaboration_mode().await.mode; if let Some(message) = request_user_input_unavailable_message(mode, self.default_mode_request_user_input) @@ -67,3 +74,7 @@ impl ToolHandler for RequestUserInputHandler { Ok(FunctionToolOutput::from_text(content, Some(true))) } } + +#[cfg(test)] +#[path = "request_user_input_tests.rs"] +mod tests; diff --git a/codex-rs/core/src/tools/handlers/request_user_input_tests.rs b/codex-rs/core/src/tools/handlers/request_user_input_tests.rs new file mode 100644 index 0000000000..0275a12860 --- /dev/null +++ b/codex-rs/core/src/tools/handlers/request_user_input_tests.rs @@ -0,0 +1,66 @@ +use super::*; +use crate::codex::make_session_and_context; +use crate::tools::context::ToolInvocation; +use crate::tools::context::ToolPayload; +use crate::turn_diff_tracker::TurnDiffTracker; +use codex_protocol::ThreadId; +use codex_protocol::protocol::SubAgentSource; +use pretty_assertions::assert_eq; +use serde_json::json; +use std::sync::Arc; +use tokio::sync::Mutex; + +#[tokio::test] +async fn multi_agent_v2_request_user_input_rejects_subagent_threads() { + let (session, mut turn) = make_session_and_context().await; + turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id: ThreadId::new(), + depth: 1, + agent_path: None, + agent_nickname: None, + agent_role: None, + }); + + let result = RequestUserInputHandler { + default_mode_request_user_input: true, + } + .handle(ToolInvocation { + session: Arc::new(session), + turn: Arc::new(turn), + tracker: Arc::new(Mutex::new(TurnDiffTracker::default())), + call_id: "call-1".to_string(), + tool_name: REQUEST_USER_INPUT_TOOL_NAME.to_string(), + tool_namespace: None, + payload: ToolPayload::Function { + arguments: json!({ + "questions": [{ + "header": "Hdr", + "question": "Pick one", + "id": "pick_one", + "options": [ + { + "label": "A", + "description": "A" + }, + { + "label": "B", + "description": "B" + } + ] + }] + }) + .to_string(), + }, + }) + .await; + + let Err(err) = result else { + panic!("sub-agent request_user_input should fail"); + }; + assert_eq!( + err, + FunctionCallError::RespondToModel( + "request_user_input can only be used by the root thread".to_string(), + ) + ); +} diff --git a/codex-rs/core/src/tools/handlers/tool_search.rs b/codex-rs/core/src/tools/handlers/tool_search.rs index dd203e93b5..d1033c6135 100644 --- a/codex-rs/core/src/tools/handlers/tool_search.rs +++ b/codex-rs/core/src/tools/handlers/tool_search.rs @@ -7,7 +7,7 @@ use crate::tools::registry::ToolKind; use bm25::Document; use bm25::Language; use bm25::SearchEngineBuilder; -use codex_mcp::mcp_connection_manager::ToolInfo; +use codex_mcp::ToolInfo; use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT; use codex_tools::TOOL_SEARCH_TOOL_NAME; use codex_tools::ToolSearchResultSource; diff --git a/codex-rs/core/src/tools/handlers/tool_suggest.rs b/codex-rs/core/src/tools/handlers/tool_suggest.rs index 380aebd51b..9975f5131f 100644 --- a/codex-rs/core/src/tools/handlers/tool_suggest.rs +++ b/codex-rs/core/src/tools/handlers/tool_suggest.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use codex_app_server_protocol::AppInfo; -use codex_mcp::mcp::CODEX_APPS_MCP_SERVER_NAME; +use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_rmcp_client::ElicitationAction; use codex_tools::DiscoverableTool; use codex_tools::DiscoverableToolAction; diff --git a/codex-rs/core/src/tools/handlers/unified_exec.rs b/codex-rs/core/src/tools/handlers/unified_exec.rs index dfe8c42752..4486cd7917 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec.rs @@ -24,7 +24,7 @@ use crate::unified_exec::UnifiedExecProcessManager; use crate::unified_exec::WriteStdinRequest; use codex_features::Feature; use codex_otel::SessionTelemetry; -use codex_otel::metrics::names::TOOL_CALL_UNIFIED_EXEC_METRIC; +use codex_otel::TOOL_CALL_UNIFIED_EXEC_METRIC; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::TerminalInteractionEvent; diff --git a/codex-rs/core/src/tools/mod.rs b/codex-rs/core/src/tools/mod.rs index f94f896d20..06c20d6922 100644 --- a/codex-rs/core/src/tools/mod.rs +++ b/codex-rs/core/src/tools/mod.rs @@ -1,16 +1,16 @@ -pub mod code_mode; -pub mod context; -pub mod events; +pub(crate) mod code_mode; +pub(crate) mod context; +pub(crate) mod events; pub(crate) mod handlers; -pub mod js_repl; +pub(crate) mod js_repl; pub(crate) mod network_approval; -pub mod orchestrator; -pub mod parallel; -pub mod registry; -pub mod router; -pub mod runtimes; -pub mod sandboxing; -pub mod spec; +pub(crate) mod orchestrator; +pub(crate) mod parallel; +pub(crate) mod registry; +pub(crate) mod router; +pub(crate) mod runtimes; +pub(crate) mod sandboxing; +pub(crate) mod spec; use codex_protocol::exec_output::ExecToolCallOutput; use codex_utils_output_truncation::TruncationPolicy; diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs index 4fd64715bb..aad8c1b7dc 100644 --- a/codex-rs/core/src/tools/router.rs +++ b/codex-rs/core/src/tools/router.rs @@ -8,7 +8,7 @@ use crate::tools::context::ToolPayload; use crate::tools::registry::AnyToolResult; use crate::tools::registry::ToolRegistry; use crate::tools::spec::build_specs_with_discoverable_tools; -use codex_mcp::mcp_connection_manager::ToolInfo; +use codex_mcp::ToolInfo; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::models::LocalShellAction; use codex_protocol::models::ResponseItem; diff --git a/codex-rs/core/src/tools/runtimes/mod.rs b/codex-rs/core/src/tools/runtimes/mod.rs index 9b8c4a5804..b22800e94c 100644 --- a/codex-rs/core/src/tools/runtimes/mod.rs +++ b/codex-rs/core/src/tools/runtimes/mod.rs @@ -4,6 +4,7 @@ Module: runtimes Concrete ToolRuntime implementations for specific tools. Each runtime stays small and focused and reuses the orchestrator for approvals + sandbox + retry. */ +use crate::exec_env::CODEX_THREAD_ID_ENV_VAR; use crate::path_utils; use crate::shell::Shell; use crate::tools::sandboxing::ToolError; @@ -12,9 +13,9 @@ use codex_sandboxing::SandboxCommand; use std::collections::HashMap; use std::path::Path; -pub mod apply_patch; -pub mod shell; -pub mod unified_exec; +pub(crate) mod apply_patch; +pub(crate) mod shell; +pub(crate) mod unified_exec; /// Shared helper to construct sandbox transform inputs from a tokenized command line. /// Validates that at least a program is present. @@ -48,11 +49,19 @@ pub(crate) fn build_sandbox_command( /// This wrapper script uses POSIX constructs (`if`, `.`, `exec`) so it can /// be run by Bash/Zsh/sh. On non-matching commands, or when command cwd does /// not match the snapshot cwd, this is a no-op. +/// +/// `explicit_env_overrides` and `env` are intentionally separate inputs. +/// `explicit_env_overrides` contains policy-driven shell env overrides that +/// should win after the snapshot is sourced, while `env` is the full live exec +/// environment. We need access to both so snapshot restore logic can preserve +/// runtime-only vars like `CODEX_THREAD_ID` without pretending they came from +/// the explicit override policy. pub(crate) fn maybe_wrap_shell_lc_with_snapshot( command: &[String], session_shell: &Shell, cwd: &Path, explicit_env_overrides: &HashMap, + env: &HashMap, ) -> Vec { if cfg!(windows) { return command.to_vec(); @@ -95,7 +104,11 @@ pub(crate) fn maybe_wrap_shell_lc_with_snapshot( .iter() .map(|arg| format!(" '{}'", shell_single_quote(arg))) .collect::(); - let (override_captures, override_exports) = build_override_exports(explicit_env_overrides); + let mut override_env = explicit_env_overrides.clone(); + if let Some(thread_id) = env.get(CODEX_THREAD_ID_ENV_VAR) { + override_env.insert(CODEX_THREAD_ID_ENV_VAR.to_string(), thread_id.clone()); + } + let (override_captures, override_exports) = build_override_exports(&override_env); let rewritten_script = if override_exports.is_empty() { format!( "if . '{snapshot_path}' >/dev/null 2>&1; then :; fi\n\nexec '{original_shell}' -c '{original_script}'{trailing_args}" diff --git a/codex-rs/core/src/tools/runtimes/mod_tests.rs b/codex-rs/core/src/tools/runtimes/mod_tests.rs index edd2c521fc..082e55ce2a 100644 --- a/codex-rs/core/src/tools/runtimes/mod_tests.rs +++ b/codex-rs/core/src/tools/runtimes/mod_tests.rs @@ -40,8 +40,13 @@ fn maybe_wrap_shell_lc_with_snapshot_bootstraps_in_user_shell() { "echo hello".to_string(), ]; - let rewritten = - maybe_wrap_shell_lc_with_snapshot(&command, &session_shell, dir.path(), &HashMap::new()); + let rewritten = maybe_wrap_shell_lc_with_snapshot( + &command, + &session_shell, + dir.path(), + &HashMap::new(), + &HashMap::new(), + ); assert_eq!(rewritten[0], "/bin/zsh"); assert_eq!(rewritten[1], "-c"); @@ -62,8 +67,13 @@ fn maybe_wrap_shell_lc_with_snapshot_escapes_single_quotes() { "echo 'hello'".to_string(), ]; - let rewritten = - maybe_wrap_shell_lc_with_snapshot(&command, &session_shell, dir.path(), &HashMap::new()); + let rewritten = maybe_wrap_shell_lc_with_snapshot( + &command, + &session_shell, + dir.path(), + &HashMap::new(), + &HashMap::new(), + ); assert!(rewritten[2].contains(r#"exec '/bin/bash' -c 'echo '"'"'hello'"'"''"#)); } @@ -85,8 +95,13 @@ fn maybe_wrap_shell_lc_with_snapshot_uses_bash_bootstrap_shell() { "echo hello".to_string(), ]; - let rewritten = - maybe_wrap_shell_lc_with_snapshot(&command, &session_shell, dir.path(), &HashMap::new()); + let rewritten = maybe_wrap_shell_lc_with_snapshot( + &command, + &session_shell, + dir.path(), + &HashMap::new(), + &HashMap::new(), + ); assert_eq!(rewritten[0], "/bin/bash"); assert_eq!(rewritten[1], "-c"); @@ -107,8 +122,13 @@ fn maybe_wrap_shell_lc_with_snapshot_uses_sh_bootstrap_shell() { "echo hello".to_string(), ]; - let rewritten = - maybe_wrap_shell_lc_with_snapshot(&command, &session_shell, dir.path(), &HashMap::new()); + let rewritten = maybe_wrap_shell_lc_with_snapshot( + &command, + &session_shell, + dir.path(), + &HashMap::new(), + &HashMap::new(), + ); assert_eq!(rewritten[0], "/bin/sh"); assert_eq!(rewritten[1], "-c"); @@ -131,8 +151,13 @@ fn maybe_wrap_shell_lc_with_snapshot_preserves_trailing_args() { "arg1".to_string(), ]; - let rewritten = - maybe_wrap_shell_lc_with_snapshot(&command, &session_shell, dir.path(), &HashMap::new()); + let rewritten = maybe_wrap_shell_lc_with_snapshot( + &command, + &session_shell, + dir.path(), + &HashMap::new(), + &HashMap::new(), + ); assert!( rewritten[2] @@ -161,8 +186,13 @@ fn maybe_wrap_shell_lc_with_snapshot_skips_when_cwd_mismatch() { "echo hello".to_string(), ]; - let rewritten = - maybe_wrap_shell_lc_with_snapshot(&command, &session_shell, &command_cwd, &HashMap::new()); + let rewritten = maybe_wrap_shell_lc_with_snapshot( + &command, + &session_shell, + &command_cwd, + &HashMap::new(), + &HashMap::new(), + ); assert_eq!(rewritten, command); } @@ -181,8 +211,13 @@ fn maybe_wrap_shell_lc_with_snapshot_accepts_dot_alias_cwd() { ]; let command_cwd = dir.path().join("."); - let rewritten = - maybe_wrap_shell_lc_with_snapshot(&command, &session_shell, &command_cwd, &HashMap::new()); + let rewritten = maybe_wrap_shell_lc_with_snapshot( + &command, + &session_shell, + &command_cwd, + &HashMap::new(), + &HashMap::new(), + ); assert_eq!(rewritten[0], "/bin/zsh"); assert_eq!(rewritten[1], "-c"); @@ -217,6 +252,7 @@ fn maybe_wrap_shell_lc_with_snapshot_restores_explicit_override_precedence() { &session_shell, dir.path(), &explicit_env_overrides, + &HashMap::from([("TEST_ENV_SNAPSHOT".to_string(), "worktree".to_string())]), ); let output = Command::new(&rewritten[0]) .args(&rewritten[1..]) @@ -231,6 +267,43 @@ fn maybe_wrap_shell_lc_with_snapshot_restores_explicit_override_precedence() { ); } +#[test] +fn maybe_wrap_shell_lc_with_snapshot_restores_codex_thread_id_from_env() { + let dir = tempdir().expect("create temp dir"); + let snapshot_path = dir.path().join("snapshot.sh"); + std::fs::write( + &snapshot_path, + "# Snapshot file\nexport CODEX_THREAD_ID='parent-thread'\n", + ) + .expect("write snapshot"); + let session_shell = shell_with_snapshot( + ShellType::Bash, + "/bin/bash", + snapshot_path, + dir.path().abs(), + ); + let command = vec![ + "/bin/bash".to_string(), + "-lc".to_string(), + "printf '%s' \"$CODEX_THREAD_ID\"".to_string(), + ]; + let rewritten = maybe_wrap_shell_lc_with_snapshot( + &command, + &session_shell, + dir.path(), + &HashMap::new(), + &HashMap::from([("CODEX_THREAD_ID".to_string(), "nested-thread".to_string())]), + ); + let output = Command::new(&rewritten[0]) + .args(&rewritten[1..]) + .env("CODEX_THREAD_ID", "nested-thread") + .output() + .expect("run rewritten command"); + + assert!(output.status.success(), "command failed: {output:?}"); + assert_eq!(String::from_utf8_lossy(&output.stdout), "nested-thread"); +} + #[test] fn maybe_wrap_shell_lc_with_snapshot_keeps_snapshot_path_without_override() { let dir = tempdir().expect("create temp dir"); @@ -251,8 +324,13 @@ fn maybe_wrap_shell_lc_with_snapshot_keeps_snapshot_path_without_override() { "-lc".to_string(), "printf '%s' \"$PATH\"".to_string(), ]; - let rewritten = - maybe_wrap_shell_lc_with_snapshot(&command, &session_shell, dir.path(), &HashMap::new()); + let rewritten = maybe_wrap_shell_lc_with_snapshot( + &command, + &session_shell, + dir.path(), + &HashMap::new(), + &HashMap::new(), + ); let output = Command::new(&rewritten[0]) .args(&rewritten[1..]) .output() @@ -288,6 +366,7 @@ fn maybe_wrap_shell_lc_with_snapshot_applies_explicit_path_override() { &session_shell, dir.path(), &explicit_env_overrides, + &HashMap::from([("PATH".to_string(), "/worktree/bin".to_string())]), ); let output = Command::new(&rewritten[0]) .args(&rewritten[1..]) @@ -328,6 +407,10 @@ fn maybe_wrap_shell_lc_with_snapshot_does_not_embed_override_values_in_argv() { &session_shell, dir.path(), &explicit_env_overrides, + &HashMap::from([( + "OPENAI_API_KEY".to_string(), + "super-secret-value".to_string(), + )]), ); assert!(!rewritten[2].contains("super-secret-value")); @@ -372,6 +455,7 @@ fn maybe_wrap_shell_lc_with_snapshot_preserves_unset_override_variables() { &session_shell, dir.path(), &explicit_env_overrides, + &HashMap::new(), ); let output = Command::new(&rewritten[0]) diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index a546d8edd7..2276b72f62 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -226,6 +226,7 @@ impl ToolRuntime for ShellRuntime { session_shell.as_ref(), &req.cwd, &req.explicit_env_overrides, + &req.env, ); let command = if matches!(session_shell.shell_type, ShellType::PowerShell) { prefix_powershell_script_with_utf8(&command) diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index a9c3effd5b..ebf25467bd 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -207,6 +207,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt session_shell.as_ref(), &req.cwd, &req.explicit_env_overrides, + &req.env, ); let command = if matches!(session_shell.shell_type, ShellType::PowerShell) { prefix_powershell_script_with_utf8(&command) diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 90b9a382aa..37a68d4081 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -5,8 +5,8 @@ use crate::tools::handlers::multi_agents_common::DEFAULT_WAIT_TIMEOUT_MS; use crate::tools::handlers::multi_agents_common::MAX_WAIT_TIMEOUT_MS; use crate::tools::handlers::multi_agents_common::MIN_WAIT_TIMEOUT_MS; use crate::tools::registry::ToolRegistryBuilder; -use codex_mcp::mcp::CODEX_APPS_MCP_SERVER_NAME; -use codex_mcp::mcp_connection_manager::ToolInfo; +use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; +use codex_mcp::ToolInfo; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_tools::DiscoverableTool; use codex_tools::ToolHandlerKind; diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index 4615b45319..13fa4930db 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -8,7 +8,7 @@ use crate::tools::router::ToolRouterParams; use codex_app_server_protocol::AppInfo; use codex_features::Feature; use codex_features::Features; -use codex_mcp::mcp::CODEX_APPS_MCP_SERVER_NAME; +use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_models_manager::bundled_models_response; use codex_models_manager::model_info::with_config_overrides; use codex_protocol::config_types::WebSearchMode; diff --git a/codex-rs/core/src/turn_timing.rs b/codex-rs/core/src/turn_timing.rs index a4451a52f9..6c47d3b528 100644 --- a/codex-rs/core/src/turn_timing.rs +++ b/codex-rs/core/src/turn_timing.rs @@ -3,8 +3,8 @@ use std::time::Instant; use std::time::SystemTime; use std::time::UNIX_EPOCH; -use codex_otel::metrics::names::TURN_TTFM_DURATION_METRIC; -use codex_otel::metrics::names::TURN_TTFT_DURATION_METRIC; +use codex_otel::TURN_TTFM_DURATION_METRIC; +use codex_otel::TURN_TTFT_DURATION_METRIC; use codex_protocol::items::TurnItem; use codex_protocol::models::ResponseItem; use tokio::sync::Mutex; diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 202a9f8c08..d9705ea41c 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -582,12 +582,12 @@ impl UnifiedExecProcessManager { pub(crate) async fn open_session_with_exec_env( &self, process_id: i32, - env: &ExecRequest, + request: &ExecRequest, tty: bool, mut spawn_lifecycle: SpawnLifecycleHandle, environment: &codex_exec_server::Environment, ) -> Result { - let (program, args) = env + let (program, args) = request .command .split_first() .ok_or(UnifiedExecError::MissingCommandLine)?; @@ -604,24 +604,24 @@ impl UnifiedExecProcessManager { .get_exec_backend() .start(codex_exec_server::ExecParams { process_id: exec_server_process_id(process_id).into(), - argv: env.command.clone(), - cwd: env.cwd.to_path_buf(), - env: env.env.clone(), + argv: request.command.clone(), + cwd: request.cwd.to_path_buf(), + env: request.env.clone(), tty, - arg0: env.arg0.clone(), + arg0: request.arg0.clone(), }) .await .map_err(|err| UnifiedExecError::create_process(err.to_string()))?; - return UnifiedExecProcess::from_remote_started(started, env.sandbox).await; + return UnifiedExecProcess::from_remote_started(started, request.sandbox).await; } let spawn_result = if tty { codex_utils_pty::pty::spawn_process_with_inherited_fds( program, args, - env.cwd.as_path(), - &env.env, - &env.arg0, + request.cwd.as_path(), + &request.env, + &request.arg0, codex_utils_pty::TerminalSize::default(), &inherited_fds, ) @@ -630,9 +630,9 @@ impl UnifiedExecProcessManager { codex_utils_pty::pipe::spawn_process_no_stdin_with_inherited_fds( program, args, - env.cwd.as_path(), - &env.env, - &env.arg0, + request.cwd.as_path(), + &request.env, + &request.arg0, &inherited_fds, ) .await @@ -640,7 +640,7 @@ impl UnifiedExecProcessManager { let spawned = spawn_result.map_err(|err| UnifiedExecError::create_process(err.to_string()))?; spawn_lifecycle.after_spawn(); - UnifiedExecProcess::from_spawned(spawned, env.sandbox, spawn_lifecycle).await + UnifiedExecProcess::from_spawned(spawned, request.sandbox, spawn_lifecycle).await } pub(super) async fn open_session_with_sandbox( diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index b3f445bfa1..0b48e9cb71 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -364,7 +364,7 @@ fn emit_windows_sandbox_setup_success_metrics( originator_tag: &str, duration: std::time::Duration, ) { - let Some(metrics) = codex_otel::metrics::global() else { + let Some(metrics) = codex_otel::global() else { return; }; let mode_tag = windows_sandbox_setup_mode_tag(mode); @@ -390,7 +390,7 @@ fn emit_windows_sandbox_setup_failure_metrics( duration: std::time::Duration, _err: &anyhow::Error, ) { - let Some(metrics) = codex_otel::metrics::global() else { + let Some(metrics) = codex_otel::global() else { return; }; let mode_tag = windows_sandbox_setup_mode_tag(mode); diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 7a6e73ed08..03870beab2 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -31,6 +31,8 @@ fn normalize_git_remote_url(url: &str) -> String { .to_string() } +const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111"; + #[tokio::test] async fn responses_stream_includes_subagent_header_on_review() { core_test_support::skip_if_no_network!(); @@ -98,6 +100,7 @@ async fn responses_stream_includes_subagent_header_on_review() { let client = ModelClient::new( /*auth_manager*/ None, conversation_id, + /*installation_id*/ TEST_INSTALLATION_ID.to_string(), provider.clone(), session_source, config.model_verbosity, @@ -147,6 +150,10 @@ async fn responses_stream_includes_subagent_header_on_review() { Some(expected_window_id.as_str()) ); assert_eq!(request.header("x-codex-parent-thread-id"), None); + assert_eq!( + request.body_json()["client_metadata"]["x-codex-installation-id"].as_str(), + Some(TEST_INSTALLATION_ID) + ); assert_eq!(request.header("x-codex-sandbox"), None); } @@ -218,6 +225,7 @@ async fn responses_stream_includes_subagent_header_on_other() { let client = ModelClient::new( /*auth_manager*/ None, conversation_id, + /*installation_id*/ TEST_INSTALLATION_ID.to_string(), provider.clone(), session_source, config.model_verbosity, @@ -331,6 +339,7 @@ async fn responses_respects_model_info_overrides_from_config() { let client = ModelClient::new( /*auth_manager*/ None, conversation_id, + /*installation_id*/ TEST_INSTALLATION_ID.to_string(), provider.clone(), session_source, config.model_verbosity, diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index cb97e4a3b3..15303a58ff 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -81,6 +81,8 @@ use wiremock::matchers::method; use wiremock::matchers::path; use wiremock::matchers::query_param; +const INSTALLATION_ID_FILENAME: &str = "installation_id"; + #[expect(clippy::unwrap_used)] fn assert_message_role(request_body: &serde_json::Value, role: &str) { assert_eq!(request_body["role"].as_str().unwrap(), role); @@ -760,10 +762,18 @@ async fn includes_conversation_id_and_model_headers_in_request() { .header("authorization") .expect("authorization header"); let request_originator = request.header("originator").expect("originator header"); + let request_body = request.body_json(); + let installation_id = + std::fs::read_to_string(test.codex_home_path().join(INSTALLATION_ID_FILENAME)) + .expect("read installation id"); assert_eq!(request_session_id, session_id.to_string()); assert_eq!(request_originator, originator().value); assert_eq!(request_authorization, "Bearer Test API Key"); + assert_eq!( + request_body["client_metadata"]["x-codex-installation-id"].as_str(), + Some(installation_id.as_str()) + ); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -868,6 +878,7 @@ async fn send_provider_auth_request(server: &MockServer, auth: ModelProviderAuth "unused-api-key", ))), conversation_id, + /*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(), provider, SessionSource::Exec, config.model_verbosity, @@ -1048,11 +1059,18 @@ async fn chatgpt_auth_sends_correct_request() { let request_body = request.body_json(); let session_id = request.header("session_id").expect("session_id header"); + let installation_id = + std::fs::read_to_string(test.codex_home_path().join(INSTALLATION_ID_FILENAME)) + .expect("read installation id"); assert_eq!(session_id, thread_id.to_string()); assert_eq!(request_originator, originator().value); assert_eq!(request_authorization, "Bearer Access Token"); assert_eq!(request_chatgpt_account_id, "account_id"); + assert_eq!( + request_body["client_metadata"]["x-codex-installation-id"].as_str(), + Some(installation_id.as_str()) + ); assert!(request_body["stream"].as_bool().unwrap()); assert_eq!( request_body["include"][0].as_str().unwrap(), @@ -2179,6 +2197,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { let client = ModelClient::new( /*auth_manager*/ None, conversation_id, + /*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(), provider.clone(), SessionSource::Exec, config.model_verbosity, diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index 0a1195a11b..7ff4a9f88d 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -10,11 +10,11 @@ use codex_features::Feature; use codex_login::CodexAuth; use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::WireApi; +use codex_otel::MetricsClient; +use codex_otel::MetricsConfig; use codex_otel::SessionTelemetry; use codex_otel::TelemetryAuthMode; use codex_otel::current_span_w3c_trace_context; -use codex_otel::metrics::MetricsClient; -use codex_otel::metrics::MetricsConfig; use codex_protocol::ThreadId; use codex_protocol::account::PlanType; use codex_protocol::config_types::ReasoningSummary; @@ -55,6 +55,7 @@ const MODEL: &str = "gpt-5.2-codex"; const OPENAI_BETA_HEADER: &str = "OpenAI-Beta"; const WS_V2_BETA_HEADER_VALUE: &str = "responses_websockets=2026-02-06"; const X_CLIENT_REQUEST_ID_HEADER: &str = "x-client-request-id"; +const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111"; fn assert_request_trace_matches(body: &serde_json::Value, expected_trace: &W3cTraceContext) { let client_metadata = body["client_metadata"] @@ -125,6 +126,10 @@ async fn responses_websocket_streams_request() { handshake.header(X_CLIENT_REQUEST_ID_HEADER), Some(harness.conversation_id.to_string()) ); + assert_eq!( + body["client_metadata"]["x-codex-installation-id"].as_str(), + Some(TEST_INSTALLATION_ID) + ); server.shutdown().await; } @@ -1756,6 +1761,7 @@ async fn websocket_harness_with_provider_options( let client = ModelClient::new( /*auth_manager*/ None, conversation_id, + /*installation_id*/ TEST_INSTALLATION_ID.to_string(), provider.clone(), SessionSource::Exec, config.model_verbosity, diff --git a/codex-rs/core/tests/suite/pending_input.rs b/codex-rs/core/tests/suite/pending_input.rs index 2fc5bf4e34..25a08120ae 100644 --- a/codex-rs/core/tests/suite/pending_input.rs +++ b/codex-rs/core/tests/suite/pending_input.rs @@ -101,6 +101,19 @@ async fn submit_user_input(codex: &CodexThread, text: &str) { .unwrap_or_else(|err| panic!("submit user input: {err}")); } +async fn steer_user_input(codex: &CodexThread, text: &str) { + codex + .steer_input( + vec![UserInput::Text { + text: text.to_string(), + text_elements: Vec::new(), + }], + /*expected_turn_id*/ None, + ) + .await + .unwrap_or_else(|err| panic!("steer user input: {err:?}")); +} + async fn submit_queue_only_agent_mail(codex: &CodexThread, text: &str) { codex .submit(Op::InterAgentCommunication { @@ -410,7 +423,7 @@ async fn user_input_does_not_preempt_after_reasoning_item() { wait_for_reasoning_item_started(&codex).await; - submit_user_input(&codex, "second prompt").await; + steer_user_input(&codex, "second prompt").await; let _ = gate_reasoning_done_tx.send(()); diff --git a/codex-rs/core/tests/suite/personality_migration.rs b/codex-rs/core/tests/suite/personality_migration.rs index 3f19d55eff..3ce6925f41 100644 --- a/codex-rs/core/tests/suite/personality_migration.rs +++ b/codex-rs/core/tests/suite/personality_migration.rs @@ -71,6 +71,7 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R agent_role: None, model_provider: None, base_instructions: None, + developer_instructions: None, dynamic_tools: None, memory_mode: None, }, @@ -116,6 +117,7 @@ async fn write_rollout_with_meta_only(dir: &Path, thread_id: ThreadId) -> io::Re agent_role: None, model_provider: None, base_instructions: None, + developer_instructions: None, dynamic_tools: None, memory_mode: None, }, diff --git a/codex-rs/core/tests/suite/rollout_list_find.rs b/codex-rs/core/tests/suite/rollout_list_find.rs index 93edd1621a..1954654266 100644 --- a/codex-rs/core/tests/suite/rollout_list_find.rs +++ b/codex-rs/core/tests/suite/rollout_list_find.rs @@ -174,6 +174,7 @@ async fn find_locates_rollout_file_written_by_recorder() -> std::io::Result<()> /*forked_from_id*/ None, SessionSource::Exec, Some(BaseInstructions::default()), + /*developer_instructions*/ None, Vec::new(), EventPersistenceMode::Limited, ), diff --git a/codex-rs/core/tests/suite/sqlite_state.rs b/codex-rs/core/tests/suite/sqlite_state.rs index f35152e185..d99c3865c7 100644 --- a/codex-rs/core/tests/suite/sqlite_state.rs +++ b/codex-rs/core/tests/suite/sqlite_state.rs @@ -146,6 +146,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> { agent_role: None, model_provider: None, base_instructions: None, + developer_instructions: None, dynamic_tools: Some(dynamic_tools_for_hook), memory_mode: None, }, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 6b0ba03c85..4f53aee2e5 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -7,8 +7,8 @@ mod cli; mod event_processor; mod event_processor_with_human_output; -pub mod event_processor_with_jsonl_output; -pub mod exec_events; +pub(crate) mod event_processor_with_jsonl_output; +pub(crate) mod exec_events; pub use cli::Cli; pub use cli::Command; @@ -85,7 +85,42 @@ use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_oss::ensure_oss_provider_ready; use codex_utils_oss::get_default_model_for_oss_provider; use event_processor_with_human_output::EventProcessorWithHumanOutput; -use event_processor_with_jsonl_output::EventProcessorWithJsonOutput; +pub use event_processor_with_jsonl_output::CodexStatus; +pub use event_processor_with_jsonl_output::CollectedThreadEvents; +pub use event_processor_with_jsonl_output::EventProcessorWithJsonOutput; +pub use exec_events::AgentMessageItem; +pub use exec_events::CollabAgentState; +pub use exec_events::CollabAgentStatus; +pub use exec_events::CollabTool; +pub use exec_events::CollabToolCallItem; +pub use exec_events::CollabToolCallStatus; +pub use exec_events::CommandExecutionItem; +pub use exec_events::CommandExecutionStatus; +pub use exec_events::ErrorItem; +pub use exec_events::FileChangeItem; +pub use exec_events::FileUpdateChange; +pub use exec_events::ItemCompletedEvent; +pub use exec_events::ItemStartedEvent; +pub use exec_events::ItemUpdatedEvent; +pub use exec_events::McpToolCallItem; +pub use exec_events::McpToolCallItemError; +pub use exec_events::McpToolCallItemResult; +pub use exec_events::McpToolCallStatus; +pub use exec_events::PatchApplyStatus; +pub use exec_events::PatchChangeKind; +pub use exec_events::ReasoningItem; +pub use exec_events::ThreadErrorEvent; +pub use exec_events::ThreadEvent; +pub use exec_events::ThreadItem as ExecThreadItem; +pub use exec_events::ThreadItemDetails; +pub use exec_events::ThreadStartedEvent; +pub use exec_events::TodoItem; +pub use exec_events::TodoListItem; +pub use exec_events::TurnCompletedEvent; +pub use exec_events::TurnFailedEvent; +pub use exec_events::TurnStartedEvent; +pub use exec_events::Usage; +pub use exec_events::WebSearchItem; use serde_json::Value; use std::collections::HashMap; use std::io::IsTerminal; @@ -105,7 +140,6 @@ use tracing_subscriber::prelude::*; use uuid::Uuid; use crate::cli::Command as ExecCommand; -use crate::event_processor::CodexStatus; use crate::event_processor::EventProcessor; const DEFAULT_ANALYTICS_ENABLED: bool = true; diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index c22207c914..a6b25b66b8 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -37,42 +37,42 @@ use codex_protocol::protocol::SessionConfiguredEvent; use pretty_assertions::assert_eq; use serde_json::json; -use codex_exec::event_processor_with_jsonl_output::CodexStatus; -use codex_exec::event_processor_with_jsonl_output::CollectedThreadEvents; -use codex_exec::event_processor_with_jsonl_output::EventProcessorWithJsonOutput; -use codex_exec::exec_events::AgentMessageItem; -use codex_exec::exec_events::CollabAgentState; -use codex_exec::exec_events::CollabAgentStatus; -use codex_exec::exec_events::CollabTool; -use codex_exec::exec_events::CollabToolCallItem; -use codex_exec::exec_events::CollabToolCallStatus; -use codex_exec::exec_events::CommandExecutionItem; -use codex_exec::exec_events::CommandExecutionStatus; -use codex_exec::exec_events::ErrorItem; -use codex_exec::exec_events::FileChangeItem; -use codex_exec::exec_events::FileUpdateChange as ExecFileUpdateChange; -use codex_exec::exec_events::ItemCompletedEvent; -use codex_exec::exec_events::ItemStartedEvent; -use codex_exec::exec_events::ItemUpdatedEvent; -use codex_exec::exec_events::McpToolCallItem; -use codex_exec::exec_events::McpToolCallItemError; -use codex_exec::exec_events::McpToolCallItemResult; -use codex_exec::exec_events::McpToolCallStatus; -use codex_exec::exec_events::PatchApplyStatus; -use codex_exec::exec_events::PatchChangeKind; -use codex_exec::exec_events::ReasoningItem; -use codex_exec::exec_events::ThreadErrorEvent; -use codex_exec::exec_events::ThreadEvent; -use codex_exec::exec_events::ThreadItem as ExecThreadItem; -use codex_exec::exec_events::ThreadItemDetails; -use codex_exec::exec_events::ThreadStartedEvent; -use codex_exec::exec_events::TodoItem; -use codex_exec::exec_events::TodoListItem; -use codex_exec::exec_events::TurnCompletedEvent; -use codex_exec::exec_events::TurnFailedEvent; -use codex_exec::exec_events::TurnStartedEvent; -use codex_exec::exec_events::Usage; -use codex_exec::exec_events::WebSearchItem; +use codex_exec::AgentMessageItem; +use codex_exec::CodexStatus; +use codex_exec::CollabAgentState; +use codex_exec::CollabAgentStatus; +use codex_exec::CollabTool; +use codex_exec::CollabToolCallItem; +use codex_exec::CollabToolCallStatus; +use codex_exec::CollectedThreadEvents; +use codex_exec::CommandExecutionItem; +use codex_exec::CommandExecutionStatus; +use codex_exec::ErrorItem; +use codex_exec::EventProcessorWithJsonOutput; +use codex_exec::ExecThreadItem; +use codex_exec::FileChangeItem; +use codex_exec::FileUpdateChange as ExecFileUpdateChange; +use codex_exec::ItemCompletedEvent; +use codex_exec::ItemStartedEvent; +use codex_exec::ItemUpdatedEvent; +use codex_exec::McpToolCallItem; +use codex_exec::McpToolCallItemError; +use codex_exec::McpToolCallItemResult; +use codex_exec::McpToolCallStatus; +use codex_exec::PatchApplyStatus; +use codex_exec::PatchChangeKind; +use codex_exec::ReasoningItem; +use codex_exec::ThreadErrorEvent; +use codex_exec::ThreadEvent; +use codex_exec::ThreadItemDetails; +use codex_exec::ThreadStartedEvent; +use codex_exec::TodoItem; +use codex_exec::TodoListItem; +use codex_exec::TurnCompletedEvent; +use codex_exec::TurnFailedEvent; +use codex_exec::TurnStartedEvent; +use codex_exec::Usage; +use codex_exec::WebSearchItem; #[test] fn map_todo_items_preserves_text_and_completion_state() { diff --git a/codex-rs/exec/tests/suite/sandbox.rs b/codex-rs/exec/tests/suite/sandbox.rs index 65eecb6bbf..990e1650c9 100644 --- a/codex-rs/exec/tests/suite/sandbox.rs +++ b/codex-rs/exec/tests/suite/sandbox.rs @@ -44,7 +44,7 @@ async fn spawn_command_under_sandbox( stdio_policy: StdioPolicy, env: HashMap, ) -> std::io::Result { - use codex_core::landlock::spawn_command_under_linux_sandbox; + use codex_core::spawn_command_under_linux_sandbox; let codex_linux_sandbox_exe = core_test_support::find_codex_linux_sandbox_exe() .map_err(|err| io::Error::new(io::ErrorKind::NotFound, err))?; let command_cwd = AbsolutePathBuf::try_from(command_cwd) diff --git a/codex-rs/execpolicy/src/amend.rs b/codex-rs/execpolicy/src/amend.rs index 7909fc14a4..e25fd1bd91 100644 --- a/codex-rs/execpolicy/src/amend.rs +++ b/codex-rs/execpolicy/src/amend.rs @@ -9,7 +9,6 @@ use std::path::PathBuf; use crate::decision::Decision; use crate::rule::NetworkRuleProtocol; use crate::rule::normalize_network_rule_host; -use serde_json; use thiserror::Error; #[derive(Debug, Error)] diff --git a/codex-rs/execpolicy/src/lib.rs b/codex-rs/execpolicy/src/lib.rs index 8cb89c36e4..128c9dd41e 100644 --- a/codex-rs/execpolicy/src/lib.rs +++ b/codex-rs/execpolicy/src/lib.rs @@ -1,11 +1,11 @@ -pub mod amend; -pub mod decision; -pub mod error; -pub mod execpolicycheck; +pub(crate) mod amend; +pub(crate) mod decision; +pub(crate) mod error; +pub(crate) mod execpolicycheck; mod executable_name; -pub mod parser; -pub mod policy; -pub mod rule; +pub(crate) mod parser; +pub(crate) mod policy; +pub(crate) mod rule; pub use amend::AmendError; pub use amend::blocking_append_allow_prefix_rule; @@ -22,6 +22,9 @@ pub use policy::Evaluation; pub use policy::MatchOptions; pub use policy::Policy; pub use rule::NetworkRuleProtocol; +pub use rule::PatternToken; +pub use rule::PrefixPattern; +pub use rule::PrefixRule; pub use rule::Rule; pub use rule::RuleMatch; pub use rule::RuleRef; diff --git a/codex-rs/execpolicy/src/main.rs b/codex-rs/execpolicy/src/main.rs index d3b34a3307..82e1bc12c4 100644 --- a/codex-rs/execpolicy/src/main.rs +++ b/codex-rs/execpolicy/src/main.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Parser; -use codex_execpolicy::execpolicycheck::ExecPolicyCheckCommand; +use codex_execpolicy::ExecPolicyCheckCommand; /// CLI for evaluating exec policies #[derive(Parser)] diff --git a/codex-rs/execpolicy/src/parser.rs b/codex-rs/execpolicy/src/parser.rs index 46764d0e07..5d01df18bb 100644 --- a/codex-rs/execpolicy/src/parser.rs +++ b/codex-rs/execpolicy/src/parser.rs @@ -1,6 +1,5 @@ use codex_utils_absolute_path::AbsolutePathBuf; use multimap::MultiMap; -use shlex; use starlark::any::ProvidesStaticType; use starlark::codemap::FileSpan; use starlark::environment::GlobalsBuilder; diff --git a/codex-rs/execpolicy/tests/basic.rs b/codex-rs/execpolicy/tests/basic.rs index bef845da67..50c3f5361f 100644 --- a/codex-rs/execpolicy/tests/basic.rs +++ b/codex-rs/execpolicy/tests/basic.rs @@ -10,14 +10,14 @@ use codex_execpolicy::Error; use codex_execpolicy::Evaluation; use codex_execpolicy::MatchOptions; use codex_execpolicy::NetworkRuleProtocol; +use codex_execpolicy::PatternToken; use codex_execpolicy::Policy; use codex_execpolicy::PolicyParser; +use codex_execpolicy::PrefixPattern; +use codex_execpolicy::PrefixRule; use codex_execpolicy::RuleMatch; use codex_execpolicy::RuleRef; use codex_execpolicy::blocking_append_allow_prefix_rule; -use codex_execpolicy::rule::PatternToken; -use codex_execpolicy::rule::PrefixPattern; -use codex_execpolicy::rule::PrefixRule; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use tempfile::tempdir; diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index f49e0edc05..23c23c9916 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -138,6 +138,8 @@ pub enum Feature { Collab, /// Enable task-path-based multi-agent routing. MultiAgentV2, + /// Hide spawn_agent agent/model override fields from the model-visible tool schema. + DebugHideSpawnAgentMetadata, /// Enable CSV-backed agent job tools. SpawnCsv, /// Enable apps. @@ -706,6 +708,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::UnderDevelopment, default_enabled: false, }, + FeatureSpec { + id: Feature::DebugHideSpawnAgentMetadata, + key: "debug_hide_spawn_agent_metadata", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::SpawnCsv, key: "enable_fanout", diff --git a/codex-rs/feedback/src/lib.rs b/codex-rs/feedback/src/lib.rs index 8d9489dbe6..712aeee219 100644 --- a/codex-rs/feedback/src/lib.rs +++ b/codex-rs/feedback/src/lib.rs @@ -14,8 +14,6 @@ use anyhow::anyhow; use codex_login::AuthEnvTelemetry; use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; -use feedback_diagnostics::FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME; -use feedback_diagnostics::FeedbackDiagnostics; use tracing::Event; use tracing::Level; use tracing::field::Visit; @@ -24,7 +22,10 @@ use tracing_subscriber::filter::Targets; use tracing_subscriber::fmt::writer::MakeWriter; use tracing_subscriber::registry::LookupSpan; -pub mod feedback_diagnostics; +pub(crate) mod feedback_diagnostics; +pub use feedback_diagnostics::FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME; +pub use feedback_diagnostics::FeedbackDiagnostic; +pub use feedback_diagnostics::FeedbackDiagnostics; const DEFAULT_MAX_BYTES: usize = 4 * 1024 * 1024; // 4 MiB const SENTRY_DSN: &str = @@ -609,7 +610,7 @@ mod tests { use std::fs; use super::*; - use feedback_diagnostics::FeedbackDiagnostic; + use crate::FeedbackDiagnostic; use pretty_assertions::assert_eq; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; diff --git a/codex-rs/hooks/src/lib.rs b/codex-rs/hooks/src/lib.rs index 16334a6476..784b414427 100644 --- a/codex-rs/hooks/src/lib.rs +++ b/codex-rs/hooks/src/lib.rs @@ -1,5 +1,5 @@ mod engine; -pub mod events; +pub(crate) mod events; mod legacy_notify; mod registry; mod schema; diff --git a/codex-rs/login/src/api_bridge.rs b/codex-rs/login/src/api_bridge.rs index 4c432ff18d..d8b9dbb77c 100644 --- a/codex-rs/login/src/api_bridge.rs +++ b/codex-rs/login/src/api_bridge.rs @@ -1,4 +1,4 @@ -use codex_api::api_bridge::CoreAuthProvider; +use codex_api::CoreAuthProvider; use codex_model_provider_info::ModelProviderInfo; use crate::CodexAuth; diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index f786884b34..1b52223bc3 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -9,6 +9,7 @@ mod pkce; mod server; pub use codex_client::BuildCustomCaTransportError as BuildLoginHttpClientError; +pub use codex_config::types::AuthCredentialsStoreMode; pub use device_code_auth::DeviceCode; pub use device_code_auth::complete_device_code_login; pub use device_code_auth::request_device_code; diff --git a/codex-rs/model-provider-info/src/lib.rs b/codex-rs/model-provider-info/src/lib.rs index 968bcc8ed0..f39ff0c7fe 100644 --- a/codex-rs/model-provider-info/src/lib.rs +++ b/codex-rs/model-provider-info/src/lib.rs @@ -6,7 +6,7 @@ //! key. These override or extend the defaults at runtime. use codex_api::Provider as ApiProvider; -use codex_api::provider::RetryConfig as ApiRetryConfig; +use codex_api::RetryConfig as ApiRetryConfig; use codex_app_server_protocol::AuthMode; use codex_protocol::config_types::ModelProviderAuthInfo; use codex_protocol::error::CodexErr; diff --git a/codex-rs/models-manager/src/lib.rs b/codex-rs/models-manager/src/lib.rs index a9c0d489d3..e99c33edb9 100644 --- a/codex-rs/models-manager/src/lib.rs +++ b/codex-rs/models-manager/src/lib.rs @@ -1,6 +1,6 @@ -pub mod cache; +pub(crate) mod cache; pub mod collaboration_mode_presets; -pub mod config; +pub(crate) mod config; pub mod manager; pub mod model_info; pub mod model_presets; diff --git a/codex-rs/models-manager/src/manager.rs b/codex-rs/models-manager/src/manager.rs index 984c612296..a7d4aa12ab 100644 --- a/codex-rs/models-manager/src/manager.rs +++ b/codex-rs/models-manager/src/manager.rs @@ -7,7 +7,7 @@ use codex_api::ModelsClient; use codex_api::RequestTelemetry; use codex_api::ReqwestTransport; use codex_api::TransportError; -use codex_api::api_bridge::map_api_error; +use codex_api::map_api_error; use codex_app_server_protocol::AuthMode; use codex_feedback::FeedbackRequestTags; use codex_feedback::emit_feedback_request_tags_with_auth_env; diff --git a/codex-rs/otel/src/events/session_telemetry.rs b/codex-rs/otel/src/events/session_telemetry.rs index 42fd6e50c3..b1bf9b0947 100644 --- a/codex-rs/otel/src/events/session_telemetry.rs +++ b/codex-rs/otel/src/events/session_telemetry.rs @@ -3,29 +3,29 @@ use crate::ToolDecisionSource; use crate::events::shared::log_and_trace_event; use crate::events::shared::log_event; use crate::events::shared::trace_event; +use crate::metrics::API_CALL_COUNT_METRIC; +use crate::metrics::API_CALL_DURATION_METRIC; use crate::metrics::MetricsClient; use crate::metrics::MetricsConfig; use crate::metrics::MetricsError; +use crate::metrics::PROFILE_USAGE_METRIC; +use crate::metrics::RESPONSES_API_ENGINE_IAPI_TBT_DURATION_METRIC; +use crate::metrics::RESPONSES_API_ENGINE_IAPI_TTFT_DURATION_METRIC; +use crate::metrics::RESPONSES_API_ENGINE_SERVICE_TBT_DURATION_METRIC; +use crate::metrics::RESPONSES_API_ENGINE_SERVICE_TTFT_DURATION_METRIC; +use crate::metrics::RESPONSES_API_INFERENCE_TIME_DURATION_METRIC; +use crate::metrics::RESPONSES_API_OVERHEAD_DURATION_METRIC; use crate::metrics::Result as MetricsResult; -use crate::metrics::names::API_CALL_COUNT_METRIC; -use crate::metrics::names::API_CALL_DURATION_METRIC; -use crate::metrics::names::PROFILE_USAGE_METRIC; -use crate::metrics::names::RESPONSES_API_ENGINE_IAPI_TBT_DURATION_METRIC; -use crate::metrics::names::RESPONSES_API_ENGINE_IAPI_TTFT_DURATION_METRIC; -use crate::metrics::names::RESPONSES_API_ENGINE_SERVICE_TBT_DURATION_METRIC; -use crate::metrics::names::RESPONSES_API_ENGINE_SERVICE_TTFT_DURATION_METRIC; -use crate::metrics::names::RESPONSES_API_INFERENCE_TIME_DURATION_METRIC; -use crate::metrics::names::RESPONSES_API_OVERHEAD_DURATION_METRIC; -use crate::metrics::names::SSE_EVENT_COUNT_METRIC; -use crate::metrics::names::SSE_EVENT_DURATION_METRIC; -use crate::metrics::names::TOOL_CALL_COUNT_METRIC; -use crate::metrics::names::TOOL_CALL_DURATION_METRIC; -use crate::metrics::names::WEBSOCKET_EVENT_COUNT_METRIC; -use crate::metrics::names::WEBSOCKET_EVENT_DURATION_METRIC; -use crate::metrics::names::WEBSOCKET_REQUEST_COUNT_METRIC; -use crate::metrics::names::WEBSOCKET_REQUEST_DURATION_METRIC; +use crate::metrics::SSE_EVENT_COUNT_METRIC; +use crate::metrics::SSE_EVENT_DURATION_METRIC; +use crate::metrics::SessionMetricTagValues; +use crate::metrics::TOOL_CALL_COUNT_METRIC; +use crate::metrics::TOOL_CALL_DURATION_METRIC; +use crate::metrics::WEBSOCKET_EVENT_COUNT_METRIC; +use crate::metrics::WEBSOCKET_EVENT_DURATION_METRIC; +use crate::metrics::WEBSOCKET_REQUEST_COUNT_METRIC; +use crate::metrics::WEBSOCKET_REQUEST_DURATION_METRIC; use crate::metrics::runtime_metrics::RuntimeMetricsSummary; -use crate::metrics::tags::SessionMetricTagValues; use crate::metrics::timer::Timer; use crate::provider::OtelProvider; use crate::sanitize_metric_tag_value; diff --git a/codex-rs/otel/src/lib.rs b/codex-rs/otel/src/lib.rs index ea13ad9b96..c7d0b7c419 100644 --- a/codex-rs/otel/src/lib.rs +++ b/codex-rs/otel/src/lib.rs @@ -1,23 +1,27 @@ -pub mod config; +pub(crate) mod config; mod events; -pub mod metrics; -pub mod provider; -pub mod trace_context; +pub(crate) mod metrics; +pub(crate) mod provider; +pub(crate) mod trace_context; mod otlp; mod targets; -use crate::metrics::MetricsError; use crate::metrics::Result as MetricsResult; use serde::Serialize; use strum_macros::Display; +pub use crate::config::OtelExporter; +pub use crate::config::OtelHttpProtocol; +pub use crate::config::OtelSettings; +pub use crate::config::OtelTlsConfig; pub use crate::events::session_telemetry::AuthEnvTelemetryMetadata; pub use crate::events::session_telemetry::SessionTelemetry; pub use crate::events::session_telemetry::SessionTelemetryMetadata; pub use crate::metrics::runtime_metrics::RuntimeMetricTotals; pub use crate::metrics::runtime_metrics::RuntimeMetricsSummary; pub use crate::metrics::timer::Timer; +pub use crate::metrics::*; pub use crate::provider::OtelProvider; pub use crate::trace_context::context_from_w3c_trace_context; pub use crate::trace_context::current_span_trace_id; diff --git a/codex-rs/otel/src/metrics/mod.rs b/codex-rs/otel/src/metrics/mod.rs index 1d1195e5fe..bcbb85d35c 100644 --- a/codex-rs/otel/src/metrics/mod.rs +++ b/codex-rs/otel/src/metrics/mod.rs @@ -1,9 +1,9 @@ mod client; mod config; mod error; -pub mod names; +pub(crate) mod names; pub(crate) mod runtime_metrics; -pub mod tags; +pub(crate) mod tags; pub(crate) mod timer; pub(crate) mod validation; @@ -12,7 +12,9 @@ pub use crate::metrics::config::MetricsConfig; pub use crate::metrics::config::MetricsExporter; pub use crate::metrics::error::MetricsError; pub use crate::metrics::error::Result; +pub use names::*; use std::sync::OnceLock; +pub use tags::SessionMetricTagValues; static GLOBAL_METRICS: OnceLock = OnceLock::new(); diff --git a/codex-rs/otel/tests/harness/mod.rs b/codex-rs/otel/tests/harness/mod.rs index acdba0b7e1..fbba56411c 100644 --- a/codex-rs/otel/tests/harness/mod.rs +++ b/codex-rs/otel/tests/harness/mod.rs @@ -1,6 +1,6 @@ -use codex_otel::metrics::MetricsClient; -use codex_otel::metrics::MetricsConfig; -use codex_otel::metrics::Result; +use codex_otel::MetricsClient; +use codex_otel::MetricsConfig; +use codex_otel::Result; use opentelemetry::KeyValue; use opentelemetry_sdk::metrics::InMemoryMetricExporter; use opentelemetry_sdk::metrics::data::AggregatedMetrics; diff --git a/codex-rs/otel/tests/suite/manager_metrics.rs b/codex-rs/otel/tests/suite/manager_metrics.rs index 8a61403c77..d95f42dfc5 100644 --- a/codex-rs/otel/tests/suite/manager_metrics.rs +++ b/codex-rs/otel/tests/suite/manager_metrics.rs @@ -2,9 +2,9 @@ use crate::harness::attributes_to_map; use crate::harness::build_metrics_with_defaults; use crate::harness::find_metric; use crate::harness::latest_metrics; +use codex_otel::Result; use codex_otel::SessionTelemetry; use codex_otel::TelemetryAuthMode; -use codex_otel::metrics::Result; use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; use opentelemetry_sdk::metrics::data::AggregatedMetrics; diff --git a/codex-rs/otel/tests/suite/otlp_http_loopback.rs b/codex-rs/otel/tests/suite/otlp_http_loopback.rs index eb84dfdf5b..fd4e3531d8 100644 --- a/codex-rs/otel/tests/suite/otlp_http_loopback.rs +++ b/codex-rs/otel/tests/suite/otlp_http_loopback.rs @@ -1,10 +1,10 @@ +use codex_otel::MetricsClient; +use codex_otel::MetricsConfig; +use codex_otel::OtelExporter; +use codex_otel::OtelHttpProtocol; use codex_otel::OtelProvider; -use codex_otel::config::OtelExporter; -use codex_otel::config::OtelHttpProtocol; -use codex_otel::config::OtelSettings; -use codex_otel::metrics::MetricsClient; -use codex_otel::metrics::MetricsConfig; -use codex_otel::metrics::Result; +use codex_otel::OtelSettings; +use codex_otel::Result; use std::collections::HashMap; use std::io::Read as _; use std::io::Write as _; diff --git a/codex-rs/otel/tests/suite/runtime_summary.rs b/codex-rs/otel/tests/suite/runtime_summary.rs index 7ffa03e026..21fc7bf76c 100644 --- a/codex-rs/otel/tests/suite/runtime_summary.rs +++ b/codex-rs/otel/tests/suite/runtime_summary.rs @@ -1,10 +1,10 @@ +use codex_otel::MetricsClient; +use codex_otel::MetricsConfig; +use codex_otel::Result; use codex_otel::RuntimeMetricTotals; use codex_otel::RuntimeMetricsSummary; use codex_otel::SessionTelemetry; use codex_otel::TelemetryAuthMode; -use codex_otel::metrics::MetricsClient; -use codex_otel::metrics::MetricsConfig; -use codex_otel::metrics::Result; use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; use eventsource_stream::Event as StreamEvent; diff --git a/codex-rs/otel/tests/suite/send.rs b/codex-rs/otel/tests/suite/send.rs index a50c579089..fc382bf88a 100644 --- a/codex-rs/otel/tests/suite/send.rs +++ b/codex-rs/otel/tests/suite/send.rs @@ -3,7 +3,7 @@ use crate::harness::build_metrics_with_defaults; use crate::harness::find_metric; use crate::harness::histogram_data; use crate::harness::latest_metrics; -use codex_otel::metrics::Result; +use codex_otel::Result; use pretty_assertions::assert_eq; use std::collections::BTreeMap; diff --git a/codex-rs/otel/tests/suite/snapshot.rs b/codex-rs/otel/tests/suite/snapshot.rs index 4686e79d3d..e631100004 100644 --- a/codex-rs/otel/tests/suite/snapshot.rs +++ b/codex-rs/otel/tests/suite/snapshot.rs @@ -1,10 +1,10 @@ use crate::harness::attributes_to_map; use crate::harness::find_metric; +use codex_otel::MetricsClient; +use codex_otel::MetricsConfig; +use codex_otel::Result; use codex_otel::SessionTelemetry; use codex_otel::TelemetryAuthMode; -use codex_otel::metrics::MetricsClient; -use codex_otel::metrics::MetricsConfig; -use codex_otel::metrics::Result; use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; use opentelemetry_sdk::metrics::InMemoryMetricExporter; diff --git a/codex-rs/otel/tests/suite/timing.rs b/codex-rs/otel/tests/suite/timing.rs index 8398e1a0d7..0cf73b9a56 100644 --- a/codex-rs/otel/tests/suite/timing.rs +++ b/codex-rs/otel/tests/suite/timing.rs @@ -2,7 +2,7 @@ use crate::harness::attributes_to_map; use crate::harness::build_metrics_with_defaults; use crate::harness::histogram_data; use crate::harness::latest_metrics; -use codex_otel::metrics::Result; +use codex_otel::Result; use pretty_assertions::assert_eq; use std::time::Duration; diff --git a/codex-rs/otel/tests/suite/validation.rs b/codex-rs/otel/tests/suite/validation.rs index 81f07ed147..6308506112 100644 --- a/codex-rs/otel/tests/suite/validation.rs +++ b/codex-rs/otel/tests/suite/validation.rs @@ -1,7 +1,7 @@ -use codex_otel::metrics::MetricsClient; -use codex_otel::metrics::MetricsConfig; -use codex_otel::metrics::MetricsError; -use codex_otel::metrics::Result; +use codex_otel::MetricsClient; +use codex_otel::MetricsConfig; +use codex_otel::MetricsError; +use codex_otel::Result; use opentelemetry_sdk::metrics::InMemoryMetricExporter; fn build_in_memory_client() -> Result { diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index d4e568fea9..0f18b68393 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -27,7 +27,7 @@ use crate::user_input::UserInput; use codex_execpolicy::Policy; use codex_git_utils::GhostCommit; use codex_utils_absolute_path::AbsolutePathBuf; -use codex_utils_image::error::ImageProcessingError; +use codex_utils_image::ImageProcessingError; use schemars::JsonSchema; use crate::mcp::CallToolResult; diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 05926a9544..4be5620fc4 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -2351,6 +2351,26 @@ impl InitialHistory { } } + pub fn get_developer_instructions(&self) -> Option> { + match self { + InitialHistory::New => None, + InitialHistory::Resumed(resumed) => { + resumed.history.iter().find_map(|item| match item { + RolloutItem::SessionMeta(meta_line) => { + meta_line.meta.developer_instructions.clone() + } + _ => None, + }) + } + InitialHistory::Forked(items) => items.iter().find_map(|item| match item { + RolloutItem::SessionMeta(meta_line) => { + meta_line.meta.developer_instructions.clone() + } + _ => None, + }), + } + } + pub fn get_dynamic_tools(&self) -> Option> { match self { InitialHistory::New => None, @@ -2549,6 +2569,13 @@ pub struct SessionMeta { skip_serializing_if = "Option::is_none" )] pub base_instructions: Option>, + #[serde( + default, + deserialize_with = "deserialize_double_option", + serialize_with = "serialize_double_option", + skip_serializing_if = "Option::is_none" + )] + pub developer_instructions: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub dynamic_tools: Option>, #[serde(skip_serializing_if = "Option::is_none")] @@ -2570,6 +2597,7 @@ impl Default for SessionMeta { agent_path: None, model_provider: None, base_instructions: None, + developer_instructions: None, dynamic_tools: None, memory_mode: None, } diff --git a/codex-rs/response-debug-context/src/lib.rs b/codex-rs/response-debug-context/src/lib.rs index ed5a6bdc6f..f7c90b7533 100644 --- a/codex-rs/response-debug-context/src/lib.rs +++ b/codex-rs/response-debug-context/src/lib.rs @@ -1,6 +1,6 @@ use base64::Engine; +use codex_api::ApiError; use codex_api::TransportError; -use codex_api::error::ApiError; const REQUEST_ID_HEADER: &str = "x-request-id"; const OAI_REQUEST_ID_HEADER: &str = "x-oai-request-id"; @@ -91,8 +91,8 @@ mod tests { use super::extract_response_debug_context; use super::telemetry_api_error_message; use super::telemetry_transport_error_message; + use codex_api::ApiError; use codex_api::TransportError; - use codex_api::error::ApiError; use http::HeaderMap; use http::HeaderValue; use http::StatusCode; diff --git a/codex-rs/rollout/src/lib.rs b/codex-rs/rollout/src/lib.rs index 160792a390..6acf2c3be4 100644 --- a/codex-rs/rollout/src/lib.rs +++ b/codex-rs/rollout/src/lib.rs @@ -4,12 +4,12 @@ use std::sync::LazyLock; use codex_protocol::protocol::SessionSource; -pub mod config; -pub mod list; -pub mod metadata; -pub mod policy; -pub mod recorder; -pub mod session_index; +pub(crate) mod config; +pub(crate) mod list; +pub(crate) mod metadata; +pub(crate) mod policy; +pub(crate) mod recorder; +pub(crate) mod session_index; pub mod state_db; pub(crate) mod default_client { @@ -30,14 +30,28 @@ pub static INTERACTIVE_SESSION_SOURCES: LazyLock> = LazyLock: }); pub use codex_protocol::protocol::SessionMeta; +pub use config::Config; pub use config::RolloutConfig; pub use config::RolloutConfigView; +pub use list::Cursor; +pub use list::ThreadItem; +pub use list::ThreadListConfig; +pub use list::ThreadListLayout; +pub use list::ThreadSortKey; +pub use list::ThreadsPage; pub use list::find_archived_thread_path_by_id_str; pub use list::find_thread_path_by_id_str; #[deprecated(note = "use find_thread_path_by_id_str")] pub use list::find_thread_path_by_id_str as find_conversation_path_by_id_str; +pub use list::get_threads; +pub use list::get_threads_in_root; +pub use list::parse_cursor; +pub use list::read_head_for_summary; +pub use list::read_session_meta_line; pub use list::rollout_date_parts; +pub use metadata::builder_from_items; pub use policy::EventPersistenceMode; +pub use policy::should_persist_response_item_for_memories; pub use recorder::RolloutRecorder; pub use recorder::RolloutRecorderParams; pub use session_index::append_thread_name; diff --git a/codex-rs/rollout/src/metadata.rs b/codex-rs/rollout/src/metadata.rs index 51ebb5ef1e..e8a95839d7 100644 --- a/codex-rs/rollout/src/metadata.rs +++ b/codex-rs/rollout/src/metadata.rs @@ -137,7 +137,7 @@ pub(crate) async fn backfill_sessions( runtime: &codex_state::StateRuntime, config: &impl RolloutConfigView, ) { - let metric_client = codex_otel::metrics::global(); + let metric_client = codex_otel::global(); let timer = metric_client .as_ref() .and_then(|otel| otel.start_timer(DB_METRIC_BACKFILL_DURATION_MS, &[]).ok()); diff --git a/codex-rs/rollout/src/metadata_tests.rs b/codex-rs/rollout/src/metadata_tests.rs index 8a149313dc..fdea29206a 100644 --- a/codex-rs/rollout/src/metadata_tests.rs +++ b/codex-rs/rollout/src/metadata_tests.rs @@ -56,6 +56,7 @@ async fn extract_metadata_from_rollout_uses_session_meta() { agent_role: None, model_provider: Some("openai".to_string()), base_instructions: None, + developer_instructions: None, dynamic_tools: None, memory_mode: None, }; @@ -107,6 +108,7 @@ async fn extract_metadata_from_rollout_returns_latest_memory_mode() { agent_role: None, model_provider: Some("openai".to_string()), base_instructions: None, + developer_instructions: None, dynamic_tools: None, memory_mode: None, }; @@ -369,6 +371,7 @@ fn write_rollout_in_sessions_with_cwd( agent_role: None, model_provider: Some("test-provider".to_string()), base_instructions: None, + developer_instructions: None, dynamic_tools: None, memory_mode: None, }; diff --git a/codex-rs/rollout/src/recorder.rs b/codex-rs/rollout/src/recorder.rs index aa7b485b0c..b589ffac97 100644 --- a/codex-rs/rollout/src/recorder.rs +++ b/codex-rs/rollout/src/recorder.rs @@ -82,6 +82,7 @@ pub enum RolloutRecorderParams { forked_from_id: Option, source: SessionSource, base_instructions: Option, + developer_instructions: Option>, dynamic_tools: Vec, event_persistence_mode: EventPersistenceMode, }, @@ -111,6 +112,7 @@ impl RolloutRecorderParams { forked_from_id: Option, source: SessionSource, base_instructions: Option, + developer_instructions: Option>, dynamic_tools: Vec, event_persistence_mode: EventPersistenceMode, ) -> Self { @@ -119,6 +121,7 @@ impl RolloutRecorderParams { forked_from_id, source, base_instructions, + developer_instructions, dynamic_tools, event_persistence_mode, } @@ -380,6 +383,7 @@ impl RolloutRecorder { forked_from_id, source, base_instructions, + developer_instructions, dynamic_tools, event_persistence_mode, } => { @@ -409,6 +413,7 @@ impl RolloutRecorder { source, model_provider: Some(config.model_provider_id().to_string()), base_instructions: Some(base_instructions), + developer_instructions, dynamic_tools: if dynamic_tools.is_empty() { None } else { diff --git a/codex-rs/rollout/src/recorder_tests.rs b/codex-rs/rollout/src/recorder_tests.rs index 6ee55672e7..44e8ecb8c8 100644 --- a/codex-rs/rollout/src/recorder_tests.rs +++ b/codex-rs/rollout/src/recorder_tests.rs @@ -74,6 +74,7 @@ async fn recorder_materializes_only_after_explicit_persist() -> std::io::Result< /*forked_from_id*/ None, SessionSource::Exec, Some(BaseInstructions::default()), + /*developer_instructions*/ None, Vec::new(), EventPersistenceMode::Limited, ), @@ -167,6 +168,7 @@ async fn metadata_irrelevant_events_touch_state_db_updated_at() -> std::io::Resu /*forked_from_id*/ None, SessionSource::Cli, Some(BaseInstructions::default()), + /*developer_instructions*/ None, Vec::new(), EventPersistenceMode::Limited, ), diff --git a/codex-rs/rollout/src/tests.rs b/codex-rs/rollout/src/tests.rs index aeda089a25..4fbd58c14b 100644 --- a/codex-rs/rollout/src/tests.rs +++ b/codex-rs/rollout/src/tests.rs @@ -1140,6 +1140,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> { agent_role: None, model_provider: Some("test-provider".into()), base_instructions: None, + developer_instructions: None, dynamic_tools: None, memory_mode: None, }, diff --git a/codex-rs/shell-command/src/command_safety/mod.rs b/codex-rs/shell-command/src/command_safety/mod.rs index 12e467bdbe..31fa64f8af 100644 --- a/codex-rs/shell-command/src/command_safety/mod.rs +++ b/codex-rs/shell-command/src/command_safety/mod.rs @@ -2,4 +2,4 @@ mod powershell_parser; pub mod is_dangerous_command; pub mod is_safe_command; -pub mod windows_safe_commands; +pub(crate) mod windows_safe_commands; diff --git a/codex-rs/shell-command/src/lib.rs b/codex-rs/shell-command/src/lib.rs index 215c30fd7d..1d9e302a4e 100644 --- a/codex-rs/shell-command/src/lib.rs +++ b/codex-rs/shell-command/src/lib.rs @@ -3,7 +3,7 @@ mod shell_detect; pub mod bash; -pub mod command_safety; +pub(crate) mod command_safety; pub mod parse_command; pub mod powershell; diff --git a/codex-rs/shell-escalation/src/lib.rs b/codex-rs/shell-escalation/src/lib.rs index edad193afd..6c7e1ec605 100644 --- a/codex-rs/shell-escalation/src/lib.rs +++ b/codex-rs/shell-escalation/src/lib.rs @@ -1,6 +1,8 @@ #[cfg(unix)] mod unix; +#[cfg(unix)] +pub use unix::ESCALATE_SOCKET_ENV_VAR; #[cfg(unix)] pub use unix::EscalateAction; #[cfg(unix)] @@ -28,8 +30,6 @@ pub use unix::ShellCommandExecutor; #[cfg(unix)] pub use unix::Stopwatch; #[cfg(unix)] -pub use unix::escalate_protocol::ESCALATE_SOCKET_ENV_VAR; -#[cfg(unix)] pub use unix::main_execve_wrapper; #[cfg(unix)] pub use unix::run_shell_escalation_execve_wrapper; diff --git a/codex-rs/shell-escalation/src/unix/mod.rs b/codex-rs/shell-escalation/src/unix/mod.rs index 13b7146ab4..b5d999411c 100644 --- a/codex-rs/shell-escalation/src/unix/mod.rs +++ b/codex-rs/shell-escalation/src/unix/mod.rs @@ -53,15 +53,16 @@ //! | | //! o<-----x //! -pub mod escalate_client; -pub mod escalate_protocol; -pub mod escalate_server; -pub mod escalation_policy; -pub mod execve_wrapper; -pub mod socket; -pub mod stopwatch; +pub(crate) mod escalate_client; +pub(crate) mod escalate_protocol; +pub(crate) mod escalate_server; +pub(crate) mod escalation_policy; +pub(crate) mod execve_wrapper; +pub(crate) mod socket; +pub(crate) mod stopwatch; pub use self::escalate_client::run_shell_escalation_execve_wrapper; +pub use self::escalate_protocol::ESCALATE_SOCKET_ENV_VAR; pub use self::escalate_protocol::EscalateAction; pub use self::escalate_protocol::EscalationDecision; pub use self::escalate_protocol::EscalationExecution; diff --git a/codex-rs/state/src/extract.rs b/codex-rs/state/src/extract.rs index 8d35d393a8..31545e3f6c 100644 --- a/codex-rs/state/src/extract.rs +++ b/codex-rs/state/src/extract.rs @@ -257,6 +257,7 @@ mod tests { agent_role: None, model_provider: Some("openai".to_string()), base_instructions: None, + developer_instructions: None, dynamic_tools: None, memory_mode: None, }, @@ -384,6 +385,7 @@ mod tests { agent_role: None, model_provider: Some("openai".to_string()), base_instructions: None, + developer_instructions: None, dynamic_tools: None, memory_mode: None, }, diff --git a/codex-rs/state/src/runtime/threads.rs b/codex-rs/state/src/runtime/threads.rs index 09b23a4319..06fd9a7b42 100644 --- a/codex-rs/state/src/runtime/threads.rs +++ b/codex-rs/state/src/runtime/threads.rs @@ -1030,6 +1030,7 @@ mod tests { agent_role: None, model_provider: None, base_instructions: None, + developer_instructions: None, dynamic_tools: None, memory_mode: Some("polluted".to_string()), }, @@ -1088,6 +1089,7 @@ mod tests { agent_role: None, model_provider: None, base_instructions: None, + developer_instructions: None, dynamic_tools: None, memory_mode: None, }, diff --git a/codex-rs/tools/src/agent_tool.rs b/codex-rs/tools/src/agent_tool.rs index 017cc6e5ee..1a1eeb19dc 100644 --- a/codex-rs/tools/src/agent_tool.rs +++ b/codex-rs/tools/src/agent_tool.rs @@ -10,6 +10,7 @@ use std::collections::BTreeMap; pub struct SpawnAgentToolOptions<'a> { pub available_models: &'a [ModelPreset], pub agent_type_description: String, + pub hide_agent_type_model_reasoning: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -20,15 +21,19 @@ pub struct WaitAgentTimeoutOptions { } pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions<'_>) -> ToolSpec { - let available_models_description = spawn_agent_models_description(options.available_models); + let available_models_description = (!options.hide_agent_type_model_reasoning) + .then(|| spawn_agent_models_description(options.available_models)); let return_value_description = "Returns the spawned agent id plus the user-facing nickname when available."; - let properties = spawn_agent_common_properties_v1(&options.agent_type_description); + let mut properties = spawn_agent_common_properties_v1(&options.agent_type_description); + if options.hide_agent_type_model_reasoning { + hide_spawn_agent_metadata_options(&mut properties); + } ToolSpec::Function(ResponsesApiTool { name: "spawn_agent".to_string(), description: spawn_agent_tool_description( - &available_models_description, + available_models_description.as_deref(), return_value_description, ), strict: false, @@ -43,9 +48,17 @@ pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions<'_>) -> ToolSpe } pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions<'_>) -> ToolSpec { - let available_models_description = spawn_agent_models_description(options.available_models); - let return_value_description = "Returns the canonical task name for the spawned agent, plus the user-facing nickname when available."; + let available_models_description = (!options.hide_agent_type_model_reasoning) + .then(|| spawn_agent_models_description(options.available_models)); + let return_value_description = if options.hide_agent_type_model_reasoning { + "Returns the canonical task name for the spawned agent." + } else { + "Returns the canonical task name for the spawned agent, plus the user-facing nickname when available." + }; let mut properties = spawn_agent_common_properties_v2(&options.agent_type_description); + if options.hide_agent_type_model_reasoning { + hide_spawn_agent_metadata_options(&mut properties); + } properties.insert( "task_name".to_string(), JsonSchema::String { @@ -59,7 +72,7 @@ pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions<'_>) -> ToolSpe ToolSpec::Function(ResponsesApiTool { name: "spawn_agent".to_string(), description: spawn_agent_tool_description( - &available_models_description, + available_models_description.as_deref(), return_value_description, ), strict: false, @@ -69,7 +82,9 @@ pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions<'_>) -> ToolSpe required: Some(vec!["task_name".to_string(), "message".to_string()]), additional_properties: Some(false.into()), }, - output_schema: Some(spawn_agent_output_schema_v2()), + output_schema: Some(spawn_agent_output_schema_v2( + options.hide_agent_type_model_reasoning, + )), }) } @@ -146,7 +161,7 @@ pub fn create_send_message_tool() -> ToolSpec { required: Some(vec!["target".to_string(), "message".to_string()]), additional_properties: Some(false.into()), }, - output_schema: Some(send_input_output_schema()), + output_schema: None, }) } @@ -188,7 +203,7 @@ pub fn create_followup_task_tool() -> ToolSpec { required: Some(vec!["target".to_string(), "message".to_string()]), additional_properties: Some(false.into()), }, - output_schema: Some(send_input_output_schema()), + output_schema: None, }) } @@ -362,14 +377,24 @@ fn spawn_agent_output_schema_v1() -> Value { }) } -fn spawn_agent_output_schema_v2() -> Value { +fn spawn_agent_output_schema_v2(hide_agent_metadata: bool) -> Value { + if hide_agent_metadata { + return json!({ + "type": "object", + "properties": { + "task_name": { + "type": "string", + "description": "Canonical task name for the spawned agent." + } + }, + "required": ["task_name"], + "additionalProperties": false + }); + } + json!({ "type": "object", "properties": { - "agent_id": { - "type": ["string", "null"], - "description": "Legacy thread identifier for the spawned agent." - }, "task_name": { "type": "string", "description": "Canonical task name for the spawned agent." @@ -379,7 +404,7 @@ fn spawn_agent_output_schema_v2() -> Value { "description": "User-facing nickname for the spawned agent when available." } }, - "required": ["agent_id", "task_name", "nickname"], + "required": ["task_name", "nickname"], "additionalProperties": false }) } @@ -637,18 +662,30 @@ fn spawn_agent_common_properties_v2(agent_type_description: &str) -> BTreeMap) { + properties.remove("agent_type"); + properties.remove("model"); + properties.remove("reasoning_effort"); +} + fn spawn_agent_tool_description( - available_models_description: &str, + available_models_description: Option<&str>, return_value_description: &str, ) -> String { + let agent_role_guidance = available_models_description + .map(|description| { + format!( + "Agent-role guidance below only helps choose which agent to use after spawning is already authorized; it never authorizes spawning by itself.\n{description}" + ) + }) + .unwrap_or_default(); format!( r#" Only use `spawn_agent` if and only if the user explicitly asks for sub-agents, delegation, or parallel agent work. Requests for depth, thoroughness, research, investigation, or detailed codebase analysis do not count as permission to spawn. - Agent-role guidance below only helps choose which agent to use after spawning is already authorized; it never authorizes spawning by itself. + {agent_role_guidance} Spawn a sub-agent for a well-scoped task. {return_value_description} This spawn_agent tool provides you access to smaller but more efficient sub-agents. A mini model can solve many tasks faster than the main model. You should follow the rules and guidelines below to use this tool. -{available_models_description} ### When to delegate vs. do the subtask yourself - First, quickly analyze the overall user task and form a succinct high-level plan. Identify which tasks are immediate blockers on the critical path, and which tasks are sidecar tasks that are needed but can run in parallel without blocking the next local step. As part of that plan, explicitly decide what immediate task you should do locally right now. Do this planning step before delegating to agents so you do not hand off the immediate blocking task to a submodel and then waste time waiting on it. - Use the smaller subagent when a subtask is easy enough for it to handle and can run in parallel with your local work. Prefer delegating concrete, bounded sidecar tasks that materially advance the main task without blocking your immediate next local step. diff --git a/codex-rs/tools/src/agent_tool_tests.rs b/codex-rs/tools/src/agent_tool_tests.rs index cd68fbd13a..fc1de7bc76 100644 --- a/codex-rs/tools/src/agent_tool_tests.rs +++ b/codex-rs/tools/src/agent_tool_tests.rs @@ -34,6 +34,7 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() { model_preset("hidden", /*show_in_picker*/ false), ], agent_type_description: "role help".to_string(), + hide_agent_type_model_reasoning: false, }); let ToolSpec::Function(ResponsesApiTool { @@ -72,7 +73,7 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() { ); assert_eq!( output_schema.expect("spawn_agent output schema")["required"], - json!(["agent_id", "task_name", "nickname"]) + json!(["task_name", "nickname"]) ); } @@ -81,6 +82,7 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() { let tool = create_spawn_agent_tool_v1(SpawnAgentToolOptions { available_models: &[], agent_type_description: "role help".to_string(), + hide_agent_type_model_reasoning: false, }); let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = tool else { @@ -95,7 +97,7 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() { } #[test] -fn send_message_tool_requires_message_and_uses_submission_output() { +fn send_message_tool_requires_message_and_has_no_output_schema() { let ToolSpec::Function(ResponsesApiTool { parameters, output_schema, @@ -120,14 +122,11 @@ fn send_message_tool_requires_message_and_uses_submission_output() { required, Some(vec!["target".to_string(), "message".to_string()]) ); - assert_eq!( - output_schema.expect("send_message output schema")["required"], - json!(["submission_id"]) - ); + assert_eq!(output_schema, None); } #[test] -fn followup_task_tool_requires_message_and_uses_submission_output() { +fn followup_task_tool_requires_message_and_has_no_output_schema() { let ToolSpec::Function(ResponsesApiTool { parameters, output_schema, @@ -152,10 +151,7 @@ fn followup_task_tool_requires_message_and_uses_submission_output() { required, Some(vec!["target".to_string(), "message".to_string()]) ); - assert_eq!( - output_schema.expect("followup_task output schema")["required"], - json!(["submission_id"]) - ); + assert_eq!(output_schema, None); } #[test] diff --git a/codex-rs/tools/src/tool_config.rs b/codex-rs/tools/src/tool_config.rs index 6e9e0d1f57..1b55d2c4f4 100644 --- a/codex-rs/tools/src/tool_config.rs +++ b/codex-rs/tools/src/tool_config.rs @@ -104,7 +104,7 @@ pub struct ToolsConfig { pub can_request_original_image_detail: bool, pub collab_tools: bool, pub multi_agent_v2: bool, - pub request_user_input: bool, + pub hide_spawn_agent_metadata: bool, pub default_mode_request_user_input: bool, pub experimental_supported_tools: Vec, pub agent_jobs_tools: bool, @@ -141,10 +141,10 @@ impl ToolsConfig { include_js_repl && features.enabled(Feature::JsReplToolsOnly); let include_collab_tools = features.enabled(Feature::Collab); let include_multi_agent_v2 = features.enabled(Feature::MultiAgentV2); + let hide_spawn_agent_metadata = features.enabled(Feature::DebugHideSpawnAgentMetadata); let include_agent_jobs = features.enabled(Feature::SpawnCsv); - let include_request_user_input = !matches!(session_source, SessionSource::SubAgent(_)); let include_default_mode_request_user_input = - include_request_user_input && features.enabled(Feature::DefaultModeRequestUserInput); + features.enabled(Feature::DefaultModeRequestUserInput); let include_search_tool = model_info.supports_search_tool && features.enabled(Feature::ToolSearch); let include_tool_suggest = features.enabled(Feature::ToolSuggest) @@ -219,7 +219,7 @@ impl ToolsConfig { can_request_original_image_detail: include_original_image_detail, collab_tools: include_collab_tools, multi_agent_v2: include_multi_agent_v2, - request_user_input: include_request_user_input, + hide_spawn_agent_metadata, default_mode_request_user_input: include_default_mode_request_user_input, experimental_supported_tools: model_info.experimental_supported_tools.clone(), agent_jobs_tools: include_agent_jobs, diff --git a/codex-rs/tools/src/tool_config_tests.rs b/codex-rs/tools/src/tool_config_tests.rs index 168c8e80a0..5f121ff020 100644 --- a/codex-rs/tools/src/tool_config_tests.rs +++ b/codex-rs/tools/src/tool_config_tests.rs @@ -131,9 +131,10 @@ fn shell_zsh_fork_prefers_shell_command_over_unified_exec() { } #[test] -fn subagents_disable_request_user_input_and_agent_jobs_workers_opt_in_by_label() { +fn subagents_keep_request_user_input_mode_config_and_agent_jobs_workers_opt_in_by_label() { let model_info = model_info(); let mut features = Features::with_defaults(); + features.enable(Feature::DefaultModeRequestUserInput); features.enable(Feature::SpawnCsv); let available_models = Vec::new(); @@ -149,8 +150,7 @@ fn subagents_disable_request_user_input_and_agent_jobs_workers_opt_in_by_label() windows_sandbox_level: WindowsSandboxLevel::Disabled, }); - assert!(!tools_config.request_user_input); - assert!(!tools_config.default_mode_request_user_input); + assert!(tools_config.default_mode_request_user_input); assert!(tools_config.agent_jobs_tools); assert!(tools_config.agent_jobs_worker_tools); } diff --git a/codex-rs/tools/src/tool_registry_plan.rs b/codex-rs/tools/src/tool_registry_plan.rs index 8985e2ee87..dc76c12d36 100644 --- a/codex-rs/tools/src/tool_registry_plan.rs +++ b/codex-rs/tools/src/tool_registry_plan.rs @@ -206,19 +206,17 @@ pub fn build_tool_registry_plan( plan.register_handler("js_repl_reset", ToolHandlerKind::JsReplReset); } - if config.request_user_input { - plan.push_spec( - create_request_user_input_tool(request_user_input_tool_description( - config.default_mode_request_user_input, - )), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - plan.register_handler( - REQUEST_USER_INPUT_TOOL_NAME, - ToolHandlerKind::RequestUserInput, - ); - } + plan.push_spec( + create_request_user_input_tool(request_user_input_tool_description( + config.default_mode_request_user_input, + )), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.register_handler( + REQUEST_USER_INPUT_TOOL_NAME, + ToolHandlerKind::RequestUserInput, + ); if config.request_permissions_tool_enabled { plan.push_spec( @@ -355,6 +353,7 @@ pub fn build_tool_registry_plan( create_spawn_agent_tool_v2(SpawnAgentToolOptions { available_models: &config.available_models, agent_type_description, + hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata, }), /*supports_parallel_tool_calls*/ false, config.code_mode_enabled, @@ -397,6 +396,7 @@ pub fn build_tool_registry_plan( create_spawn_agent_tool_v1(SpawnAgentToolOptions { available_models: &config.available_models, agent_type_description, + hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata, }), /*supports_parallel_tool_calls*/ false, config.code_mode_enabled, diff --git a/codex-rs/tools/src/tool_registry_plan_tests.rs b/codex-rs/tools/src/tool_registry_plan_tests.rs index c73302d1be..d8d264e2e8 100644 --- a/codex-rs/tools/src/tool_registry_plan_tests.rs +++ b/codex-rs/tools/src/tool_registry_plan_tests.rs @@ -243,15 +243,18 @@ fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() { let output_schema = output_schema .as_ref() .expect("spawn_agent should define output schema"); - assert_eq!( - output_schema["required"], - json!(["agent_id", "task_name", "nickname"]) - ); + assert_eq!(output_schema["required"], json!(["task_name", "nickname"])); let send_message = find_tool(&tools, "send_message"); - let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &send_message.spec else { + let ToolSpec::Function(ResponsesApiTool { + parameters, + output_schema, + .. + }) = &send_message.spec + else { panic!("send_message should be a function tool"); }; + assert_eq!(output_schema, &None); let JsonSchema::Object { properties, required, @@ -270,9 +273,15 @@ fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() { ); let followup_task = find_tool(&tools, "followup_task"); - let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &followup_task.spec else { + let ToolSpec::Function(ResponsesApiTool { + parameters, + output_schema, + .. + }) = &followup_task.spec + else { panic!("followup_task should be a function tool"); }; + assert_eq!(output_schema, &None); let JsonSchema::Object { properties, required, @@ -525,9 +534,9 @@ fn test_build_specs_agent_job_worker_tools_enabled() { "close_agent", "spawn_agents_on_csv", "report_agent_job_result", + REQUEST_USER_INPUT_TOOL_NAME, ], ); - assert_lacks_tool_name(&tools, "request_user_input"); } #[test] @@ -1856,6 +1865,7 @@ fn spawn_agent_tool_options(config: &ToolsConfig) -> SpawnAgentToolOptions<'_> { SpawnAgentToolOptions { available_models: &config.available_models, agent_type_description: agent_type_description(config, DEFAULT_AGENT_TYPE_DESCRIPTION), + hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata, } } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 96758137b7..10da99a354 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -85,13 +85,14 @@ use codex_app_server_protocol::TurnError as AppServerTurnError; use codex_app_server_protocol::TurnStatus; use codex_config::types::ApprovalsReviewer; use codex_config::types::ModelAvailabilityNuxConfig; +use codex_core::append_message_history_entry; use codex_core::config::Config; use codex_core::config::ConfigBuilder; use codex_core::config::ConfigOverrides; use codex_core::config::edit::ConfigEdit; use codex_core::config::edit::ConfigEditsBuilder; use codex_core::config_loader::ConfigLayerStackOrdering; -use codex_core::message_history; +use codex_core::lookup_message_history_entry; #[cfg(target_os = "windows")] use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_features::Feature; @@ -315,12 +316,13 @@ fn session_summary( thread_id: Option, thread_name: Option, ) -> Option { - if token_usage.is_zero() { + let usage_line = (!token_usage.is_zero()).then(|| FinalOutput::from(token_usage).to_string()); + let resume_command = codex_core::util::resume_command(thread_name.as_deref(), thread_id); + + if usage_line.is_none() && resume_command.is_none() { return None; } - let usage_line = FinalOutput::from(token_usage).to_string(); - let resume_command = codex_core::util::resume_command(thread_name.as_deref(), thread_id); Some(SessionSummary { usage_line, resume_command, @@ -485,7 +487,7 @@ fn emit_system_bwrap_warning(app_event_tx: &AppEventSender, config: &Config) { #[derive(Debug, Clone, PartialEq, Eq)] struct SessionSummary { - usage_line: String, + usage_line: Option, resume_command: Option, } @@ -2160,8 +2162,7 @@ impl App { let text = text.clone(); let config = self.chat_widget.config_ref().clone(); tokio::spawn(async move { - if let Err(err) = - message_history::append_entry(&text, &thread_id, &config).await + if let Err(err) = append_message_history_entry(&text, &thread_id, &config).await { tracing::warn!( thread_id = %thread_id, @@ -2179,7 +2180,7 @@ impl App { let app_event_tx = self.app_event_tx.clone(); tokio::spawn(async move { let entry_opt = tokio::task::spawn_blocking(move || { - message_history::lookup(log_id, offset, &config) + lookup_message_history_entry(log_id, offset, &config) }) .await .unwrap_or_else(|err| { @@ -3302,7 +3303,10 @@ impl App { "Failed to attach to fresh app-server thread: {err}" )); } else if let Some(summary) = summary { - let mut lines: Vec> = vec![summary.usage_line.clone().into()]; + let mut lines: Vec> = Vec::new(); + if let Some(usage_line) = summary.usage_line { + lines.push(usage_line.into()); + } if let Some(command) = summary.resume_command { let spans = vec!["To continue this session, run ".into(), command.cyan()]; lines.push(spans.into()); @@ -4115,8 +4119,10 @@ impl App { { Ok(()) => { if let Some(summary) = summary { - let mut lines: Vec> = - vec![summary.usage_line.clone().into()]; + let mut lines: Vec> = Vec::new(); + if let Some(usage_line) = summary.usage_line { + lines.push(usage_line.into()); + } if let Some(command) = summary.resume_command { let spans = vec![ "To continue this session, run ".into(), @@ -4175,8 +4181,10 @@ impl App { { Ok(()) => { if let Some(summary) = summary { - let mut lines: Vec> = - vec![summary.usage_line.clone().into()]; + let mut lines: Vec> = Vec::new(); + if let Some(usage_line) = summary.usage_line { + lines.push(usage_line.into()); + } if let Some(command) = summary.resume_command { let spans = vec![ "To continue this session, run ".into(), @@ -4694,7 +4702,7 @@ impl App { tokio::task::spawn_blocking(move || { let requested_path = PathBuf::from(path); - let event = match codex_core::windows_sandbox_read_grants::grant_read_root_non_elevated( + let event = match codex_core::grant_read_root_non_elevated( &policy, policy_cwd.as_path(), command_cwd.as_path(), @@ -10898,7 +10906,7 @@ guardian_approval = true } #[tokio::test] - async fn session_summary_skip_zero_usage() { + async fn session_summary_skips_when_no_usage_or_resume_hint() { assert!( session_summary( TokenUsage::default(), @@ -10923,7 +10931,7 @@ guardian_approval = true session_summary(usage, Some(conversation), /*thread_name*/ None).expect("summary"); assert_eq!( summary.usage_line, - "Token usage: total=12 input=10 output=2" + Some("Token usage: total=12 input=10 output=2".to_string()) ); assert_eq!( summary.resume_command, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index d152e99f68..03072632bc 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -63,8 +63,10 @@ use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnSteerParams; use codex_app_server_protocol::TurnSteerResponse; +#[cfg(test)] +use codex_core::append_message_history_entry; use codex_core::config::Config; -use codex_core::message_history; +use codex_core::message_history_metadata; use codex_otel::TelemetryAuthMode; use codex_protocol::ThreadId; use codex_protocol::openai_models::ModelAvailabilityNux; @@ -1076,7 +1078,7 @@ async fn thread_session_state_from_thread_response( .map(ThreadId::from_string) .transpose() .map_err(|err| format!("forked_from_id is invalid: {err}"))?; - let (history_log_id, history_entry_count) = message_history::history_metadata(config).await; + let (history_log_id, history_entry_count) = message_history_metadata(config).await; let history_entry_count = u64::try_from(history_entry_count).unwrap_or(u64::MAX); Ok(ThreadSessionState { @@ -1316,10 +1318,10 @@ mod tests { let config = build_config(&temp_dir).await; let thread_id = ThreadId::new(); - message_history::append_entry("older", &thread_id, &config) + append_message_history_entry("older", &thread_id, &config) .await .expect("history append should succeed"); - message_history::append_entry("newer", &thread_id, &config) + append_message_history_entry("newer", &thread_id, &config) .await .expect("history append should succeed"); diff --git a/codex-rs/tui/src/bottom_pane/feedback_view.rs b/codex-rs/tui/src/bottom_pane/feedback_view.rs index f4f641c5d5..b1889abca0 100644 --- a/codex-rs/tui/src/bottom_pane/feedback_view.rs +++ b/codex-rs/tui/src/bottom_pane/feedback_view.rs @@ -1,5 +1,5 @@ -use codex_feedback::feedback_diagnostics::FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME; -use codex_feedback::feedback_diagnostics::FeedbackDiagnostics; +use codex_feedback::FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME; +use codex_feedback::FeedbackDiagnostics; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyModifiers; @@ -556,7 +556,7 @@ mod tests { use super::*; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; - use codex_feedback::feedback_diagnostics::FeedbackDiagnostic; + use codex_feedback::FeedbackDiagnostic; use pretty_assertions::assert_eq; fn render(view: &FeedbackNoteView, width: u16) -> String { diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index da5e2421f7..cbb698e3a2 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -76,7 +76,7 @@ pub(crate) struct MentionBinding { mod chat_composer; mod chat_composer_history; mod command_popup; -pub mod custom_prompt_view; +pub(crate) mod custom_prompt_view; mod experimental_features_view; mod file_search_popup; mod footer; @@ -108,7 +108,7 @@ pub(crate) use title_setup::TerminalTitleSetupView; mod paste_burst; mod pending_input_preview; mod pending_thread_approvals; -pub mod popup_consts; +pub(crate) mod popup_consts; mod scroll_state; mod selection_popup_common; mod textarea; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index f942308aa7..e8713704d2 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -95,13 +95,13 @@ use codex_chatgpt::connectors; use codex_config::types::ApprovalsReviewer; use codex_config::types::Notifications; use codex_config::types::WindowsSandboxModeToml; +use codex_core::DEFAULT_PROJECT_DOC_FILENAME; use codex_core::config::Config; use codex_core::config::Constrained; use codex_core::config::ConstraintResult; use codex_core::config_loader::ConfigLayerStackOrdering; use codex_core::find_thread_name_by_id; use codex_core::plugins::PluginsManager; -use codex_core::project_doc::DEFAULT_PROJECT_DOC_FILENAME; use codex_core::skills::model::SkillMetadata; #[cfg(target_os = "windows")] use codex_core::windows_sandbox::WindowsSandboxLevelExt; diff --git a/codex-rs/tui/src/chatwidget/skills.rs b/codex-rs/tui/src/chatwidget/skills.rs index 53dd7e8b8e..b5794f6c4b 100644 --- a/codex-rs/tui/src/chatwidget/skills.rs +++ b/codex-rs/tui/src/chatwidget/skills.rs @@ -13,8 +13,8 @@ use crate::bottom_pane::popup_consts::standard_popup_hint_line; use crate::skills_helpers::skill_description; use crate::skills_helpers::skill_display_name; use codex_chatgpt::connectors::AppInfo; +use codex_core::TOOL_MENTION_SIGIL; use codex_core::connectors::connector_mention_slug; -use codex_core::mention_syntax::TOOL_MENTION_SIGIL; use codex_core::skills::model::SkillDependencies; use codex_core::skills::model::SkillInterface; use codex_core::skills::model::SkillMetadata; diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index f899267858..8d14d4dda6 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -1761,13 +1761,11 @@ async fn feedback_upload_consent_popup_snapshot() { chat.app_event_tx.clone(), crate::app_event::FeedbackCategory::Bug, chat.current_rollout_path.clone(), - &codex_feedback::feedback_diagnostics::FeedbackDiagnostics::new(vec![ - codex_feedback::feedback_diagnostics::FeedbackDiagnostic { - headline: "Proxy environment variables are set and may affect connectivity." - .to_string(), - details: vec!["HTTPS_PROXY = hello".to_string()], - }, - ]), + &codex_feedback::FeedbackDiagnostics::new(vec![codex_feedback::FeedbackDiagnostic { + headline: "Proxy environment variables are set and may affect connectivity." + .to_string(), + details: vec!["HTTPS_PROXY = hello".to_string()], + }]), )); let popup = render_bottom_popup(&chat, /*width*/ 80); @@ -1782,13 +1780,11 @@ async fn feedback_good_result_consent_popup_includes_connectivity_diagnostics_fi chat.app_event_tx.clone(), crate::app_event::FeedbackCategory::GoodResult, chat.current_rollout_path.clone(), - &codex_feedback::feedback_diagnostics::FeedbackDiagnostics::new(vec![ - codex_feedback::feedback_diagnostics::FeedbackDiagnostic { - headline: "Proxy environment variables are set and may affect connectivity." - .to_string(), - details: vec!["HTTPS_PROXY = hello".to_string()], - }, - ]), + &codex_feedback::FeedbackDiagnostics::new(vec![codex_feedback::FeedbackDiagnostic { + headline: "Proxy environment variables are set and may affect connectivity." + .to_string(), + details: vec!["HTTPS_PROXY = hello".to_string()], + }]), )); let popup = render_bottom_popup(&chat, /*width*/ 80); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 6821adb5a8..67c7e9f98b 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -42,14 +42,14 @@ use base64::Engine; use codex_app_server_protocol::McpServerStatus; use codex_app_server_protocol::McpServerStatusDetail; use codex_config::types::McpServerTransportConfig; +#[cfg(test)] +use codex_core::McpManager; use codex_core::config::Config; #[cfg(test)] -use codex_core::mcp::McpManager; -#[cfg(test)] use codex_core::plugins::PluginsManager; -use codex_core::web_search::web_search_detail; +use codex_core::web_search_detail; #[cfg(test)] -use codex_mcp::mcp::qualified_mcp_tool_name_prefix; +use codex_mcp::qualified_mcp_tool_name_prefix; use codex_otel::RuntimeMetricsSummary; use codex_protocol::account::PlanType; use codex_protocol::config_types::ServiceTier; @@ -70,7 +70,7 @@ use codex_protocol::protocol::SessionConfiguredEvent; use codex_protocol::request_user_input::RequestUserInputAnswer; use codex_protocol::request_user_input::RequestUserInputQuestion; use codex_protocol::user_input::TextElement; -use codex_utils_cli::format_env_display::format_env_display; +use codex_utils_cli::format_env_display; use image::DynamicImage; use image::ImageReader; use ratatui::prelude::*; diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index d953319a40..4bda3817c6 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -102,7 +102,8 @@ mod clipboard_paste; mod clipboard_text; mod collaboration_modes; mod color; -pub mod custom_terminal; +pub(crate) mod custom_terminal; +pub use custom_terminal::Terminal; mod cwd_prompt; mod debug_config; mod diff_render; @@ -113,10 +114,12 @@ mod file_search; mod frames; mod get_git_diff; mod history_cell; -pub mod insert_history; +pub(crate) mod insert_history; +pub use insert_history::insert_history_lines; mod key_hint; mod line_truncation; -pub mod live_wrap; +pub(crate) mod live_wrap; +pub use live_wrap::RowBuilder; mod local_chatgpt_auth; mod markdown; mod markdown_render; @@ -126,10 +129,10 @@ mod model_catalog; mod model_migration; mod multi_agents; mod notifications; -pub mod onboarding; +pub(crate) mod onboarding; mod oss_selection; mod pager_overlay; -pub mod public_widgets; +pub(crate) mod public_widgets; mod render; mod resume_picker; mod selection_list; @@ -148,7 +151,8 @@ mod theme_picker; mod tooltips; mod tui; mod ui_consts; -pub mod update_action; +pub(crate) mod update_action; +pub use update_action::UpdateAction; mod update_prompt; mod updates; mod version; @@ -212,7 +216,7 @@ mod voice { mod wrapping; #[cfg(test)] -pub mod test_backend; +pub(crate) mod test_backend; #[cfg(test)] pub(crate) mod test_support; diff --git a/codex-rs/tui/src/mention_codec.rs b/codex-rs/tui/src/mention_codec.rs index 6e95067ba4..d3d1ead4de 100644 --- a/codex-rs/tui/src/mention_codec.rs +++ b/codex-rs/tui/src/mention_codec.rs @@ -1,8 +1,8 @@ use std::collections::HashMap; use std::collections::VecDeque; -use codex_core::mention_syntax::PLUGIN_TEXT_MENTION_SIGIL; -use codex_core::mention_syntax::TOOL_MENTION_SIGIL; +use codex_core::PLUGIN_TEXT_MENTION_SIGIL; +use codex_core::TOOL_MENTION_SIGIL; #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct LinkedMention { diff --git a/codex-rs/tui/src/onboarding/mod.rs b/codex-rs/tui/src/onboarding/mod.rs index 8a4dc760fa..0db5204d31 100644 --- a/codex-rs/tui/src/onboarding/mod.rs +++ b/codex-rs/tui/src/onboarding/mod.rs @@ -1,6 +1,5 @@ mod auth; -pub mod onboarding_screen; +pub(crate) mod onboarding_screen; mod trust_directory; pub(crate) use auth::mark_url_hyperlink; -pub use trust_directory::TrustDirectorySelection; mod welcome; diff --git a/codex-rs/tui/src/public_widgets/mod.rs b/codex-rs/tui/src/public_widgets/mod.rs index c752189f3b..b8db1c6bff 100644 --- a/codex-rs/tui/src/public_widgets/mod.rs +++ b/codex-rs/tui/src/public_widgets/mod.rs @@ -1 +1 @@ -pub mod composer_input; +pub(crate) mod composer_input; diff --git a/codex-rs/tui/src/render/mod.rs b/codex-rs/tui/src/render/mod.rs index a2e4920586..02c19fc9f7 100644 --- a/codex-rs/tui/src/render/mod.rs +++ b/codex-rs/tui/src/render/mod.rs @@ -1,8 +1,8 @@ use ratatui::layout::Rect; -pub mod highlight; -pub mod line_utils; -pub mod renderable; +pub(crate) mod highlight; +pub(crate) mod line_utils; +pub(crate) mod renderable; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Insets { diff --git a/codex-rs/tui/src/status/helpers.rs b/codex-rs/tui/src/status/helpers.rs index 9f4c8f793b..76aaa3da1a 100644 --- a/codex-rs/tui/src/status/helpers.rs +++ b/codex-rs/tui/src/status/helpers.rs @@ -4,7 +4,7 @@ use crate::text_formatting; use chrono::DateTime; use chrono::Local; use codex_core::config::Config; -use codex_core::project_doc::discover_project_doc_paths; +use codex_core::discover_project_doc_paths; use codex_exec_server::LOCAL_FS; use codex_protocol::account::PlanType; use codex_utils_absolute_path::AbsolutePathBuf; diff --git a/codex-rs/tui/tests/suite/vt100_history.rs b/codex-rs/tui/tests/suite/vt100_history.rs index 2b89363e66..609671cc35 100644 --- a/codex-rs/tui/tests/suite/vt100_history.rs +++ b/codex-rs/tui/tests/suite/vt100_history.rs @@ -22,20 +22,20 @@ macro_rules! assert_contains { } struct TestScenario { - term: codex_tui::custom_terminal::Terminal, + term: codex_tui::Terminal, } impl TestScenario { fn new(width: u16, height: u16, viewport: Rect) -> Self { let backend = VT100Backend::new(width, height); - let mut term = codex_tui::custom_terminal::Terminal::with_options(backend) - .expect("failed to construct terminal"); + let mut term = + codex_tui::Terminal::with_options(backend).expect("failed to construct terminal"); term.set_viewport_area(viewport); Self { term } } fn run_insert(&mut self, lines: Vec>) { - codex_tui::insert_history::insert_history_lines(&mut self.term, lines) + codex_tui::insert_history_lines(&mut self.term, lines) .expect("Failed to insert history lines in test"); } } diff --git a/codex-rs/tui/tests/suite/vt100_live_commit.rs b/codex-rs/tui/tests/suite/vt100_live_commit.rs index 4c13bb402d..01b3f72578 100644 --- a/codex-rs/tui/tests/suite/vt100_live_commit.rs +++ b/codex-rs/tui/tests/suite/vt100_live_commit.rs @@ -5,7 +5,7 @@ use ratatui::text::Line; #[test] fn live_001_commit_on_overflow() { let backend = VT100Backend::new(/*width*/ 20, /*height*/ 6); - let mut term = match codex_tui::custom_terminal::Terminal::with_options(backend) { + let mut term = match codex_tui::Terminal::with_options(backend) { Ok(t) => t, Err(e) => panic!("failed to construct terminal: {e}"), }; @@ -15,7 +15,7 @@ fn live_001_commit_on_overflow() { term.set_viewport_area(area); // Build 5 explicit rows at width 20. - let mut rb = codex_tui::live_wrap::RowBuilder::new(/*target_width*/ 20); + let mut rb = codex_tui::RowBuilder::new(/*target_width*/ 20); rb.push_fragment("one\n"); rb.push_fragment("two\n"); rb.push_fragment("three\n"); @@ -26,7 +26,7 @@ fn live_001_commit_on_overflow() { let commit_rows = rb.drain_commit_ready(/*max_keep*/ 3); let lines: Vec> = commit_rows.into_iter().map(|r| r.text.into()).collect(); - codex_tui::insert_history::insert_history_lines(&mut term, lines) + codex_tui::insert_history_lines(&mut term, lines) .expect("Failed to insert history lines in test"); let screen = term.backend().vt100().screen(); diff --git a/codex-rs/utils/cli/src/lib.rs b/codex-rs/utils/cli/src/lib.rs index b3ce23b908..ed00d683f9 100644 --- a/codex-rs/utils/cli/src/lib.rs +++ b/codex-rs/utils/cli/src/lib.rs @@ -1,8 +1,9 @@ mod approval_mode_cli_arg; mod config_override; -pub mod format_env_display; +pub(crate) mod format_env_display; mod sandbox_mode_cli_arg; pub use approval_mode_cli_arg::ApprovalModeCliArg; pub use config_override::CliConfigOverrides; +pub use format_env_display::format_env_display; pub use sandbox_mode_cli_arg::SandboxModeCliArg; diff --git a/codex-rs/utils/image/src/lib.rs b/codex-rs/utils/image/src/lib.rs index b150f76a18..9d491bc107 100644 --- a/codex-rs/utils/image/src/lib.rs +++ b/codex-rs/utils/image/src/lib.rs @@ -2,7 +2,6 @@ use std::num::NonZeroUsize; use std::path::Path; use std::sync::LazyLock; -use crate::error::ImageProcessingError; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use codex_utils_cache::BlockingLruCache; @@ -21,7 +20,9 @@ pub const MAX_WIDTH: u32 = 2048; /// Maximum height used when resizing images before uploading. pub const MAX_HEIGHT: u32 = 768; -pub mod error; +pub(crate) mod error; + +pub use crate::error::ImageProcessingError; #[derive(Debug, Clone)] pub struct EncodedImage { diff --git a/codex-rs/utils/path-utils/src/lib.rs b/codex-rs/utils/path-utils/src/lib.rs index a6dc6f8b3a..4e053c0398 100644 --- a/codex-rs/utils/path-utils/src/lib.rs +++ b/codex-rs/utils/path-utils/src/lib.rs @@ -1,6 +1,8 @@ //! Path normalization, symlink resolution, and atomic writes shared across Codex crates. -pub mod env; +pub(crate) mod env; +pub use env::is_headless_environment; +pub use env::is_wsl; use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashSet; diff --git a/codex-rs/utils/pty/src/win/mod.rs b/codex-rs/utils/pty/src/win/mod.rs index 78f5af2ce7..33b6e52fbd 100644 --- a/codex-rs/utils/pty/src/win/mod.rs +++ b/codex-rs/utils/pty/src/win/mod.rs @@ -44,7 +44,7 @@ use winapi::um::processthreadsapi::*; use winapi::um::synchapi::WaitForSingleObject; use winapi::um::winbase::INFINITE; -pub mod conpty; +pub(crate) mod conpty; mod procthreadattr; mod psuedocon; diff --git a/codex-rs/windows-sandbox-rs/src/elevated/command_runner_win.rs b/codex-rs/windows-sandbox-rs/src/elevated/command_runner_win.rs index 7777055e28..9cb59e18dc 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated/command_runner_win.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated/command_runner_win.rs @@ -12,31 +12,33 @@ use anyhow::Context; use anyhow::Result; +use codex_windows_sandbox::ErrorPayload; +use codex_windows_sandbox::ExitPayload; +use codex_windows_sandbox::FramedMessage; +use codex_windows_sandbox::Message; +use codex_windows_sandbox::OutputPayload; +use codex_windows_sandbox::OutputStream; use codex_windows_sandbox::PipeSpawnHandles; use codex_windows_sandbox::SandboxPolicy; +use codex_windows_sandbox::SpawnReady; +use codex_windows_sandbox::SpawnRequest; use codex_windows_sandbox::StderrMode; use codex_windows_sandbox::StdinMode; use codex_windows_sandbox::allow_null_device; use codex_windows_sandbox::convert_string_sid_to_sid; use codex_windows_sandbox::create_readonly_token_with_caps_from; use codex_windows_sandbox::create_workspace_write_token_with_caps_from; +use codex_windows_sandbox::decode_bytes; +use codex_windows_sandbox::encode_bytes; use codex_windows_sandbox::get_current_token_for_restriction; use codex_windows_sandbox::hide_current_user_profile_dir; -use codex_windows_sandbox::ipc_framed::ErrorPayload; -use codex_windows_sandbox::ipc_framed::ExitPayload; -use codex_windows_sandbox::ipc_framed::FramedMessage; -use codex_windows_sandbox::ipc_framed::Message; -use codex_windows_sandbox::ipc_framed::OutputPayload; -use codex_windows_sandbox::ipc_framed::OutputStream; -use codex_windows_sandbox::ipc_framed::decode_bytes; -use codex_windows_sandbox::ipc_framed::encode_bytes; -use codex_windows_sandbox::ipc_framed::read_frame; -use codex_windows_sandbox::ipc_framed::write_frame; use codex_windows_sandbox::log_note; use codex_windows_sandbox::parse_policy; +use codex_windows_sandbox::read_frame; use codex_windows_sandbox::read_handle_loop; use codex_windows_sandbox::spawn_process_with_pipes; use codex_windows_sandbox::to_wide; +use codex_windows_sandbox::write_frame; use std::ffi::c_void; use std::fs::File; use std::os::windows::io::FromRawHandle; @@ -144,9 +146,7 @@ fn send_error(writer: &Arc>, code: &str, message: String) -> Resu } /// Read and validate the initial spawn request frame. -fn read_spawn_request( - reader: &mut File, -) -> Result { +fn read_spawn_request(reader: &mut File) -> Result { let Some(msg) = read_frame(reader)? else { anyhow::bail!("runner: pipe closed before spawn_request"); }; @@ -184,9 +184,7 @@ fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf { } } -fn spawn_ipc_process( - req: &codex_windows_sandbox::ipc_framed::SpawnRequest, -) -> Result { +fn spawn_ipc_process(req: &SpawnRequest) -> Result { let log_dir = req.codex_home.clone(); hide_current_user_profile_dir(req.codex_home.as_path()); log_note( @@ -466,7 +464,7 @@ pub fn main() -> Result<()> { let msg = FramedMessage { version: 1, message: Message::SpawnReady { - payload: codex_windows_sandbox::ipc_framed::SpawnReady { + payload: SpawnReady { process_id: unsafe { GetProcessId(pi.hProcess) }, }, }, diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 9dea9414ba..dd8f23d00f 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -34,7 +34,7 @@ mod conpty; #[cfg(target_os = "windows")] #[path = "elevated/ipc_framed.rs"] -pub mod ipc_framed; +pub(crate) mod ipc_framed; #[cfg(target_os = "windows")] #[path = "setup_orchestrator.rs"] @@ -88,6 +88,30 @@ pub use identity::require_logon_sandbox_creds; #[cfg(target_os = "windows")] pub use identity::sandbox_setup_is_complete; #[cfg(target_os = "windows")] +pub use ipc_framed::ErrorPayload; +#[cfg(target_os = "windows")] +pub use ipc_framed::ExitPayload; +#[cfg(target_os = "windows")] +pub use ipc_framed::FramedMessage; +#[cfg(target_os = "windows")] +pub use ipc_framed::Message; +#[cfg(target_os = "windows")] +pub use ipc_framed::OutputPayload; +#[cfg(target_os = "windows")] +pub use ipc_framed::OutputStream; +#[cfg(target_os = "windows")] +pub use ipc_framed::SpawnReady; +#[cfg(target_os = "windows")] +pub use ipc_framed::SpawnRequest; +#[cfg(target_os = "windows")] +pub use ipc_framed::decode_bytes; +#[cfg(target_os = "windows")] +pub use ipc_framed::encode_bytes; +#[cfg(target_os = "windows")] +pub use ipc_framed::read_frame; +#[cfg(target_os = "windows")] +pub use ipc_framed::write_frame; +#[cfg(target_os = "windows")] pub use logging::LOG_FILE_NAME; #[cfg(target_os = "windows")] pub use logging::log_note;