use super::*; use codex_model_provider::SharedModelProvider; use codex_model_provider::create_model_provider; pub(super) fn image_generation_tool_auth_allowed(auth_manager: Option<&AuthManager>) -> bool { matches!( auth_manager.and_then(AuthManager::auth_mode), Some(AuthMode::Chatgpt) ) } #[derive(Clone, Debug)] pub(crate) struct TurnSkillsContext { pub(crate) outcome: Arc, pub(crate) implicit_invocation_seen_skills: Arc>>, } impl TurnSkillsContext { pub(crate) fn new(outcome: Arc) -> Self { Self { outcome, implicit_invocation_seen_skills: Arc::new(Mutex::new(HashSet::new())), } } } /// The context needed for a single turn of the thread. #[derive(Debug)] pub(crate) struct TurnContext { pub(crate) sub_id: String, pub(crate) trace_id: Option, pub(crate) realtime_active: bool, pub(crate) config: Arc, pub(crate) auth_manager: Option>, pub(crate) model_info: ModelInfo, pub(crate) session_telemetry: SessionTelemetry, pub(crate) provider: SharedModelProvider, pub(crate) reasoning_effort: Option, pub(crate) reasoning_summary: ReasoningSummaryConfig, pub(crate) session_source: SessionSource, pub(crate) environment: Option>, /// The session's absolute working directory. All relative paths provided /// by the model as well as sandbox policies are resolved against this path /// instead of `std::env::current_dir()`. pub(crate) cwd: AbsolutePathBuf, pub(crate) current_date: Option, pub(crate) timezone: Option, pub(crate) app_server_client_name: Option, pub(crate) developer_instructions: Option, pub(crate) compact_prompt: Option, pub(crate) user_instructions: Option, pub(crate) collaboration_mode: CollaborationMode, pub(crate) personality: Option, pub(crate) approval_policy: Constrained, pub(crate) sandbox_policy: Constrained, pub(crate) file_system_sandbox_policy: FileSystemSandboxPolicy, pub(crate) network_sandbox_policy: NetworkSandboxPolicy, pub(crate) network: Option, pub(crate) windows_sandbox_level: WindowsSandboxLevel, pub(crate) shell_environment_policy: ShellEnvironmentPolicy, pub(crate) tools_config: ToolsConfig, pub(crate) features: ManagedFeatures, pub(crate) ghost_snapshot: GhostSnapshotConfig, pub(crate) final_output_json_schema: Option, pub(crate) codex_self_exe: Option, pub(crate) codex_linux_sandbox_exe: Option, pub(crate) tool_call_gate: Arc, pub(crate) truncation_policy: TruncationPolicy, pub(crate) js_repl: Arc, pub(crate) dynamic_tools: Vec, pub(crate) turn_metadata_state: Arc, pub(crate) turn_skills: TurnSkillsContext, pub(crate) turn_timing_state: Arc, } impl TurnContext { pub(crate) fn model_context_window(&self) -> Option { let effective_context_window_percent = self.model_info.effective_context_window_percent; self.model_info.context_window.map(|context_window| { context_window.saturating_mul(effective_context_window_percent) / 100 }) } pub(crate) fn apps_enabled(&self) -> bool { let is_chatgpt_auth = self .auth_manager .as_deref() .and_then(AuthManager::auth_cached) .as_ref() .is_some_and(CodexAuth::is_chatgpt_auth); self.features.apps_enabled_for_auth(is_chatgpt_auth) } pub(crate) async fn with_model(&self, model: String, models_manager: &ModelsManager) -> Self { let mut config = (*self.config).clone(); config.model = Some(model.clone()); let model_info = models_manager .get_model_info(model.as_str(), &config.to_models_manager_config()) .await; let truncation_policy = model_info.truncation_policy.into(); let supported_reasoning_levels = model_info .supported_reasoning_levels .iter() .map(|preset| preset.effort) .collect::>(); let reasoning_effort = if let Some(current_reasoning_effort) = self.reasoning_effort { if supported_reasoning_levels.contains(¤t_reasoning_effort) { Some(current_reasoning_effort) } else { supported_reasoning_levels .get(supported_reasoning_levels.len().saturating_sub(1) / 2) .copied() .or(model_info.default_reasoning_level) } } else { supported_reasoning_levels .get(supported_reasoning_levels.len().saturating_sub(1) / 2) .copied() .or(model_info.default_reasoning_level) }; config.model_reasoning_effort = reasoning_effort; let collaboration_mode = self.collaboration_mode.with_updates( Some(model.clone()), Some(reasoning_effort), /*developer_instructions*/ None, ); let features = self.features.clone(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, available_models: &models_manager .list_models(RefreshStrategy::OnlineIfUncached) .await, features: &features, image_generation_tool_auth_allowed: image_generation_tool_auth_allowed( self.auth_manager.as_deref(), ), web_search_mode: self.tools_config.web_search_mode, session_source: self.session_source.clone(), sandbox_policy: self.sandbox_policy.get(), windows_sandbox_level: self.windows_sandbox_level, }) .with_unified_exec_shell_mode(self.tools_config.unified_exec_shell_mode.clone()) .with_web_search_config(self.tools_config.web_search_config.clone()) .with_allow_login_shell(self.tools_config.allow_login_shell) .with_has_environment(self.tools_config.has_environment) .with_spawn_agent_usage_hint(config.multi_agent_v2.usage_hint_enabled) .with_spawn_agent_usage_hint_text(config.multi_agent_v2.usage_hint_text.clone()) .with_hide_spawn_agent_metadata(config.multi_agent_v2.hide_spawn_agent_metadata) .with_agent_type_description(crate::agent::role::spawn_tool_spec::build( &config.agent_roles, )); Self { sub_id: self.sub_id.clone(), trace_id: self.trace_id.clone(), realtime_active: self.realtime_active, config: Arc::new(config), auth_manager: self.auth_manager.clone(), model_info: model_info.clone(), session_telemetry: self .session_telemetry .clone() .with_model(model.as_str(), model_info.slug.as_str()), provider: self.provider.clone(), reasoning_effort, reasoning_summary: self.reasoning_summary, session_source: self.session_source.clone(), environment: self.environment.clone(), cwd: self.cwd.clone(), current_date: self.current_date.clone(), timezone: self.timezone.clone(), app_server_client_name: self.app_server_client_name.clone(), developer_instructions: self.developer_instructions.clone(), compact_prompt: self.compact_prompt.clone(), user_instructions: self.user_instructions.clone(), collaboration_mode, personality: self.personality, approval_policy: self.approval_policy.clone(), sandbox_policy: self.sandbox_policy.clone(), file_system_sandbox_policy: self.file_system_sandbox_policy.clone(), network_sandbox_policy: self.network_sandbox_policy, network: self.network.clone(), windows_sandbox_level: self.windows_sandbox_level, shell_environment_policy: self.shell_environment_policy.clone(), tools_config, features, ghost_snapshot: self.ghost_snapshot.clone(), final_output_json_schema: self.final_output_json_schema.clone(), codex_self_exe: self.codex_self_exe.clone(), codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(), tool_call_gate: Arc::new(ReadinessFlag::new()), truncation_policy, js_repl: Arc::clone(&self.js_repl), dynamic_tools: self.dynamic_tools.clone(), turn_metadata_state: self.turn_metadata_state.clone(), turn_skills: self.turn_skills.clone(), turn_timing_state: Arc::clone(&self.turn_timing_state), } } pub(crate) fn resolve_path(&self, path: Option) -> AbsolutePathBuf { path.as_ref() .map_or_else(|| self.cwd.clone(), |path| self.cwd.join(path)) } pub(crate) fn file_system_sandbox_context( &self, additional_permissions: Option, ) -> FileSystemSandboxContext { FileSystemSandboxContext { sandbox_policy: self.sandbox_policy.get().clone(), windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: self .config .permissions .windows_sandbox_private_desktop, use_legacy_landlock: self.features.use_legacy_landlock(), additional_permissions, } } pub(crate) fn compact_prompt(&self) -> &str { self.compact_prompt .as_deref() .unwrap_or(compact::SUMMARIZATION_PROMPT) } pub(crate) fn to_turn_context_item(&self) -> TurnContextItem { let legacy_file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy( self.sandbox_policy.get(), &self.cwd, ); // Omit the derived split filesystem policy when it is equivalent to // the legacy sandbox policy. This keeps turn-context payloads stable // while both fields exist; once callers consume only the split policy, // this comparison and the legacy projection should go away. let file_system_sandbox_policy = (self.file_system_sandbox_policy != legacy_file_system_sandbox_policy) .then(|| self.file_system_sandbox_policy.clone()); TurnContextItem { turn_id: Some(self.sub_id.clone()), trace_id: self.trace_id.clone(), cwd: self.cwd.to_path_buf(), current_date: self.current_date.clone(), timezone: self.timezone.clone(), approval_policy: self.approval_policy.value(), sandbox_policy: self.sandbox_policy.get().clone(), network: self.turn_context_network_item(), file_system_sandbox_policy, model: self.model_info.slug.clone(), personality: self.personality, collaboration_mode: Some(self.collaboration_mode.clone()), realtime_active: Some(self.realtime_active), effort: self.reasoning_effort, summary: self.reasoning_summary, user_instructions: self.user_instructions.clone(), developer_instructions: self.developer_instructions.clone(), final_output_json_schema: self.final_output_json_schema.clone(), truncation_policy: Some(self.truncation_policy), } } fn turn_context_network_item(&self) -> Option { let network = self .config .config_layer_stack .requirements() .network .as_ref()?; Some(TurnContextNetworkItem { allowed_domains: network .domains .as_ref() .and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains) .unwrap_or_default(), denied_domains: network .domains .as_ref() .and_then(codex_config::NetworkDomainPermissionsToml::denied_domains) .unwrap_or_default(), }) } } fn local_time_context() -> (String, String) { match iana_time_zone::get_timezone() { Ok(timezone) => (Local::now().format("%Y-%m-%d").to_string(), timezone), Err(_) => ( Utc::now().format("%Y-%m-%d").to_string(), "Etc/UTC".to_string(), ), } } impl Session { /// Don't expand the number of mutated arguments on config. We are in the process of getting rid of it. pub(crate) fn build_per_turn_config(session_configuration: &SessionConfiguration) -> Config { // todo(aibrahim): store this state somewhere else so we don't need to mut config let config = session_configuration.original_config_do_not_use.clone(); let mut per_turn_config = (*config).clone(); per_turn_config.cwd = session_configuration.cwd.clone(); per_turn_config.model_reasoning_effort = session_configuration.collaboration_mode.reasoning_effort(); per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary; per_turn_config.service_tier = session_configuration.service_tier; per_turn_config.personality = session_configuration.personality; per_turn_config.approvals_reviewer = session_configuration.approvals_reviewer; let resolved_web_search_mode = resolve_web_search_mode_for_turn( &per_turn_config.web_search_mode, session_configuration.sandbox_policy.get(), ); if let Err(err) = per_turn_config .web_search_mode .set(resolved_web_search_mode) { let fallback_value = per_turn_config.web_search_mode.value(); tracing::warn!( error = %err, ?resolved_web_search_mode, ?fallback_value, "resolved web_search_mode is disallowed by requirements; keeping constrained value" ); } per_turn_config.features = config.features.clone(); per_turn_config } #[allow(clippy::too_many_arguments)] pub(crate) fn make_turn_context( conversation_id: ThreadId, auth_manager: Option>, session_telemetry: &SessionTelemetry, provider: ModelProviderInfo, session_configuration: &SessionConfiguration, user_shell: &shell::Shell, shell_zsh_path: Option<&PathBuf>, main_execve_wrapper_exe: Option<&PathBuf>, per_turn_config: Config, model_info: ModelInfo, models_manager: &ModelsManager, network: Option, environment: Option>, sub_id: String, js_repl: Arc, skills_outcome: Arc, ) -> TurnContext { let reasoning_effort = session_configuration.collaboration_mode.reasoning_effort(); let reasoning_summary = session_configuration .model_reasoning_summary .unwrap_or(model_info.default_reasoning_summary); let session_telemetry = session_telemetry.clone().with_model( session_configuration.collaboration_mode.model(), model_info.slug.as_str(), ); let session_source = session_configuration.session_source.clone(); let image_generation_tool_auth_allowed = image_generation_tool_auth_allowed(auth_manager.as_deref()); let auth_manager_for_context = auth_manager.clone(); let provider_for_context = create_model_provider(provider, auth_manager); let session_telemetry_for_context = session_telemetry; let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, available_models: &models_manager.try_list_models().unwrap_or_default(), features: &per_turn_config.features, image_generation_tool_auth_allowed, web_search_mode: Some(per_turn_config.web_search_mode.value()), session_source: session_source.clone(), sandbox_policy: session_configuration.sandbox_policy.get(), windows_sandbox_level: session_configuration.windows_sandbox_level, }) .with_unified_exec_shell_mode_for_session( crate::tools::spec::tool_user_shell_type(user_shell), shell_zsh_path, main_execve_wrapper_exe, ) .with_web_search_config(per_turn_config.web_search_config.clone()) .with_allow_login_shell(per_turn_config.permissions.allow_login_shell) .with_has_environment(environment.is_some()) .with_spawn_agent_usage_hint(per_turn_config.multi_agent_v2.usage_hint_enabled) .with_spawn_agent_usage_hint_text(per_turn_config.multi_agent_v2.usage_hint_text.clone()) .with_hide_spawn_agent_metadata(per_turn_config.multi_agent_v2.hide_spawn_agent_metadata) .with_agent_type_description(crate::agent::role::spawn_tool_spec::build( &per_turn_config.agent_roles, )); let cwd = session_configuration.cwd.clone(); let per_turn_config = Arc::new(per_turn_config); let turn_metadata_state = Arc::new(TurnMetadataState::new( conversation_id.to_string(), &session_source, sub_id.clone(), cwd.clone(), session_configuration.sandbox_policy.get(), session_configuration.windows_sandbox_level, )); let (current_date, timezone) = local_time_context(); TurnContext { sub_id, trace_id: current_span_trace_id(), realtime_active: false, config: per_turn_config.clone(), auth_manager: auth_manager_for_context, model_info: model_info.clone(), session_telemetry: session_telemetry_for_context, provider: provider_for_context, reasoning_effort, reasoning_summary, session_source, environment, cwd, current_date: Some(current_date), timezone: Some(timezone), app_server_client_name: session_configuration.app_server_client_name.clone(), developer_instructions: session_configuration.developer_instructions.clone(), compact_prompt: session_configuration.compact_prompt.clone(), user_instructions: session_configuration.user_instructions.clone(), collaboration_mode: session_configuration.collaboration_mode.clone(), personality: session_configuration.personality, approval_policy: session_configuration.approval_policy.clone(), sandbox_policy: session_configuration.sandbox_policy.clone(), file_system_sandbox_policy: session_configuration.file_system_sandbox_policy.clone(), network_sandbox_policy: session_configuration.network_sandbox_policy, network, windows_sandbox_level: session_configuration.windows_sandbox_level, shell_environment_policy: per_turn_config.permissions.shell_environment_policy.clone(), tools_config, features: per_turn_config.features.clone(), ghost_snapshot: per_turn_config.ghost_snapshot.clone(), final_output_json_schema: None, codex_self_exe: per_turn_config.codex_self_exe.clone(), codex_linux_sandbox_exe: per_turn_config.codex_linux_sandbox_exe.clone(), tool_call_gate: Arc::new(ReadinessFlag::new()), truncation_policy: model_info.truncation_policy.into(), js_repl, dynamic_tools: session_configuration.dynamic_tools.clone(), turn_metadata_state, turn_skills: TurnSkillsContext::new(skills_outcome), turn_timing_state: Arc::new(TurnTimingState::default()), } } pub(crate) async fn new_turn_with_sub_id( &self, sub_id: String, updates: SessionSettingsUpdate, ) -> ConstraintResult> { let ( session_configuration, sandbox_policy_changed, previous_cwd, codex_home, session_source, ) = { let mut state = self.state.lock().await; match state.session_configuration.clone().apply(&updates) { Ok(next) => { let previous_cwd = state.session_configuration.cwd.clone(); let sandbox_policy_changed = state.session_configuration.sandbox_policy != next.sandbox_policy; let codex_home = next.codex_home.clone(); let session_source = next.session_source.clone(); state.session_configuration = next.clone(); ( next, sandbox_policy_changed, previous_cwd, codex_home, session_source, ) } Err(err) => { drop(state); self.send_event_raw(Event { id: sub_id.clone(), msg: EventMsg::Error(ErrorEvent { message: err.to_string(), codex_error_info: Some(CodexErrorInfo::BadRequest), }), }) .await; return Err(err); } } }; self.maybe_refresh_shell_snapshot_for_cwd( &previous_cwd, &session_configuration.cwd, &codex_home, &session_source, ); if sandbox_policy_changed { self.refresh_managed_network_proxy_for_current_sandbox_policy() .await; } Ok(self .new_turn_from_configuration( sub_id, session_configuration, updates.final_output_json_schema, ) .await) } async fn new_turn_from_configuration( &self, sub_id: String, session_configuration: SessionConfiguration, final_output_json_schema: Option>, ) -> Arc { let per_turn_config = Self::build_per_turn_config(&session_configuration); { let mcp_connection_manager = self.services.mcp_connection_manager.read().await; mcp_connection_manager.set_approval_policy(&session_configuration.approval_policy); mcp_connection_manager .set_sandbox_policy(per_turn_config.permissions.sandbox_policy.get()); } let model_info = self .services .models_manager .get_model_info( session_configuration.collaboration_mode.model(), &per_turn_config.to_models_manager_config(), ) .await; let plugin_outcome = self .services .plugins_manager .plugins_for_config(&per_turn_config) .await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); let skills_input = skills_load_input_from_config(&per_turn_config, effective_skill_roots); let fs = self .services .environment .as_ref() .map(|environment| environment.get_filesystem()); let skills_outcome = Arc::new( self.services .skills_manager .skills_for_config(&skills_input, fs) .await, ); let mut turn_context: TurnContext = Self::make_turn_context( self.conversation_id, Some(Arc::clone(&self.services.auth_manager)), &self.services.session_telemetry, session_configuration.provider.clone(), &session_configuration, self.services.user_shell.as_ref(), self.services.shell_zsh_path.as_ref(), self.services.main_execve_wrapper_exe.as_ref(), per_turn_config, model_info, &self.services.models_manager, self.services .network_proxy .as_ref() .and_then(|started_proxy| { Self::managed_network_proxy_active_for_sandbox_policy( session_configuration.sandbox_policy.get(), ) .then(|| started_proxy.proxy()) }), self.services.environment.clone(), sub_id, Arc::clone(&self.js_repl), skills_outcome, ); turn_context.realtime_active = self.conversation.running_state().await.is_some(); if let Some(final_schema) = final_output_json_schema { turn_context.final_output_json_schema = final_schema; } let turn_context = Arc::new(turn_context); turn_context.turn_metadata_state.spawn_git_enrichment_task(); turn_context } pub(crate) async fn maybe_emit_unknown_model_warning_for_turn(&self, tc: &TurnContext) { if tc.model_info.used_fallback_model_metadata { self.send_event( tc, EventMsg::Warning(WarningEvent { message: format!( "Model metadata for `{}` not found. Defaulting to fallback metadata; this can degrade performance and cause issues.", tc.model_info.slug ), }), ) .await; } } pub(crate) async fn new_default_turn(&self) -> Arc { self.new_default_turn_with_sub_id(self.next_internal_sub_id()) .await } pub(crate) async fn new_default_turn_with_sub_id(&self, sub_id: String) -> Arc { let session_configuration = { let state = self.state.lock().await; state.session_configuration.clone() }; self.new_turn_from_configuration( sub_id, session_configuration, /*final_output_json_schema*/ None, ) .await } }