mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
32ace07ac5
Adds the model-facing goal tools on top of the app-server API from PR 2. ## Why Once goals are persisted and exposed to clients, the model needs a small, constrained tool surface for goal workflows. The tool contract should let the model inspect goals, create them only when explicitly requested, and mark them complete without giving it broad control over user/runtime-owned state. ## What changed - Added `get_goal`, `create_goal`, and `update_goal` tool specs behind the `goals` feature flag. - Added core goal tool handlers that validate objectives and token budgets before mutating persisted state. - Constrained `create_goal` to create only when no goal exists, with optional `token_budget` only when a budget is explicitly provided. - Tightened the `create_goal` instructions so the model does not infer goals from ordinary task requests. - Constrained `update_goal` to expose only goal completion; pause, resume, clear, and budget-limited transitions remain user- or runtime-controlled. - Registered the goal tools in the tool registry and kept them out of review contexts where they should not appear. ## Verification - Added tool-registry coverage for feature gating and tool availability. - Added core session tests for create/get/update behavior, duplicate goal rejection, budget validation, and completion-only updates.
741 lines
31 KiB
Rust
741 lines
31 KiB
Rust
use super::*;
|
|
use codex_model_provider::SharedModelProvider;
|
|
use codex_model_provider::create_model_provider;
|
|
use codex_protocol::models::AdditionalPermissionProfile;
|
|
use codex_protocol::models::SandboxEnforcement;
|
|
use codex_protocol::protocol::TurnEnvironmentSelection;
|
|
use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;
|
|
use codex_sandboxing::policy_transforms::effective_network_sandbox_policy;
|
|
use std::sync::atomic::AtomicBool;
|
|
use std::sync::atomic::Ordering;
|
|
|
|
pub(super) fn image_generation_tool_auth_allowed(auth_manager: Option<&AuthManager>) -> bool {
|
|
auth_manager.is_some_and(AuthManager::current_auth_uses_codex_backend)
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct TurnSkillsContext {
|
|
pub(crate) outcome: Arc<SkillLoadOutcome>,
|
|
pub(crate) implicit_invocation_seen_skills: Arc<Mutex<HashSet<String>>>,
|
|
}
|
|
|
|
impl TurnSkillsContext {
|
|
pub(crate) fn new(outcome: Arc<SkillLoadOutcome>) -> Self {
|
|
Self {
|
|
outcome,
|
|
implicit_invocation_seen_skills: Arc::new(Mutex::new(HashSet::new())),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct TurnEnvironment {
|
|
pub(crate) environment_id: String,
|
|
pub(crate) environment: Arc<Environment>,
|
|
pub(crate) cwd: AbsolutePathBuf,
|
|
}
|
|
|
|
impl TurnEnvironment {
|
|
pub(crate) fn selection(&self) -> TurnEnvironmentSelection {
|
|
TurnEnvironmentSelection {
|
|
environment_id: self.environment_id.clone(),
|
|
cwd: self.cwd.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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<String>,
|
|
pub(crate) realtime_active: bool,
|
|
pub(crate) config: Arc<Config>,
|
|
pub(crate) auth_manager: Option<Arc<AuthManager>>,
|
|
pub(crate) model_info: ModelInfo,
|
|
pub(crate) session_telemetry: SessionTelemetry,
|
|
pub(crate) provider: SharedModelProvider,
|
|
pub(crate) reasoning_effort: Option<ReasoningEffortConfig>,
|
|
pub(crate) reasoning_summary: ReasoningSummaryConfig,
|
|
pub(crate) session_source: SessionSource,
|
|
pub(crate) environment: Option<Arc<Environment>>,
|
|
pub(crate) environments: Vec<TurnEnvironment>,
|
|
/// 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<String>,
|
|
pub(crate) timezone: Option<String>,
|
|
pub(crate) app_server_client_name: Option<String>,
|
|
pub(crate) developer_instructions: Option<String>,
|
|
pub(crate) compact_prompt: Option<String>,
|
|
pub(crate) user_instructions: Option<String>,
|
|
pub(crate) collaboration_mode: CollaborationMode,
|
|
pub(crate) personality: Option<Personality>,
|
|
pub(crate) approval_policy: Constrained<AskForApproval>,
|
|
pub(crate) sandbox_policy: Constrained<SandboxPolicy>,
|
|
pub(crate) file_system_sandbox_policy: FileSystemSandboxPolicy,
|
|
pub(crate) network_sandbox_policy: NetworkSandboxPolicy,
|
|
pub(crate) network: Option<NetworkProxy>,
|
|
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<Value>,
|
|
pub(crate) codex_self_exe: Option<PathBuf>,
|
|
pub(crate) codex_linux_sandbox_exe: Option<PathBuf>,
|
|
pub(crate) tool_call_gate: Arc<ReadinessFlag>,
|
|
pub(crate) truncation_policy: TruncationPolicy,
|
|
pub(crate) dynamic_tools: Vec<DynamicToolSpec>,
|
|
pub(crate) turn_metadata_state: Arc<TurnMetadataState>,
|
|
pub(crate) turn_skills: TurnSkillsContext,
|
|
pub(crate) turn_timing_state: Arc<TurnTimingState>,
|
|
pub(crate) server_model_warning_emitted: AtomicBool,
|
|
pub(crate) model_verification_emitted: AtomicBool,
|
|
}
|
|
impl TurnContext {
|
|
pub(crate) fn permission_profile(&self) -> PermissionProfile {
|
|
PermissionProfile::from_runtime_permissions_with_enforcement(
|
|
SandboxEnforcement::from_legacy_sandbox_policy(&self.sandbox_policy),
|
|
&self.file_system_sandbox_policy,
|
|
self.network_sandbox_policy,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn model_context_window(&self) -> Option<i64> {
|
|
let effective_context_window_percent = self.model_info.effective_context_window_percent;
|
|
self.model_info
|
|
.resolved_context_window()
|
|
.map(|context_window| {
|
|
context_window.saturating_mul(effective_context_window_percent) / 100
|
|
})
|
|
}
|
|
|
|
pub(crate) fn apps_enabled(&self) -> bool {
|
|
let uses_codex_backend = self
|
|
.auth_manager
|
|
.as_deref()
|
|
.is_some_and(AuthManager::current_auth_uses_codex_backend);
|
|
self.features.apps_enabled_for_auth(uses_codex_backend)
|
|
}
|
|
|
|
pub(crate) async fn with_model(
|
|
&self,
|
|
model: String,
|
|
models_manager: &SharedModelsManager,
|
|
) -> 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::<Vec<_>>();
|
|
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_goal_tools_allowed(self.tools_config.goal_tools)
|
|
.with_max_concurrent_threads_per_session(config.agent_max_threads)
|
|
.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(),
|
|
environments: self.environments.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,
|
|
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),
|
|
server_model_warning_emitted: AtomicBool::new(
|
|
self.server_model_warning_emitted.load(Ordering::Relaxed),
|
|
),
|
|
model_verification_emitted: AtomicBool::new(
|
|
self.model_verification_emitted.load(Ordering::Relaxed),
|
|
),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn resolve_path(&self, path: Option<String>) -> 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<AdditionalPermissionProfile>,
|
|
) -> FileSystemSandboxContext {
|
|
let file_system_sandbox_policy = effective_file_system_sandbox_policy(
|
|
&self.file_system_sandbox_policy,
|
|
additional_permissions.as_ref(),
|
|
);
|
|
let network_sandbox_policy = effective_network_sandbox_policy(
|
|
self.network_sandbox_policy,
|
|
additional_permissions.as_ref(),
|
|
);
|
|
let permissions = PermissionProfile::from_runtime_permissions_with_enforcement(
|
|
SandboxEnforcement::from_legacy_sandbox_policy(&self.sandbox_policy),
|
|
&file_system_sandbox_policy,
|
|
network_sandbox_policy,
|
|
);
|
|
FileSystemSandboxContext {
|
|
permissions,
|
|
cwd: Some(self.cwd.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(),
|
|
}
|
|
}
|
|
|
|
fn non_legacy_file_system_sandbox_policy(&self) -> Option<FileSystemSandboxPolicy> {
|
|
// 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 legacy_file_system_sandbox_policy =
|
|
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
|
|
self.sandbox_policy.get(),
|
|
&self.cwd,
|
|
);
|
|
(self.file_system_sandbox_policy != legacy_file_system_sandbox_policy)
|
|
.then(|| self.file_system_sandbox_policy.clone())
|
|
}
|
|
|
|
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 {
|
|
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(),
|
|
permission_profile: Some(self.permission_profile()),
|
|
network: self.turn_context_network_item(),
|
|
file_system_sandbox_policy: self.non_legacy_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<TurnContextNetworkItem> {
|
|
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,
|
|
cwd: AbsolutePathBuf,
|
|
) -> 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 = cwd;
|
|
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<Arc<AuthManager>>,
|
|
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: &SharedModelsManager,
|
|
network: Option<NetworkProxy>,
|
|
environment: Option<Arc<Environment>>,
|
|
environments: Vec<TurnEnvironment>,
|
|
cwd: AbsolutePathBuf,
|
|
sub_id: String,
|
|
skills_outcome: Arc<SkillLoadOutcome>,
|
|
goal_tools_supported: bool,
|
|
) -> 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_goal_tools_allowed(goal_tools_supported)
|
|
.with_max_concurrent_threads_per_session(per_turn_config.agent_max_threads)
|
|
.with_agent_type_description(crate::agent::role::spawn_tool_spec::build(
|
|
&per_turn_config.agent_roles,
|
|
));
|
|
|
|
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,
|
|
environments,
|
|
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(),
|
|
dynamic_tools: session_configuration.dynamic_tools.clone(),
|
|
turn_metadata_state,
|
|
turn_skills: TurnSkillsContext::new(skills_outcome),
|
|
turn_timing_state: Arc::new(TurnTimingState::default()),
|
|
server_model_warning_emitted: AtomicBool::new(false),
|
|
model_verification_emitted: AtomicBool::new(false),
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn new_turn_with_sub_id(
|
|
&self,
|
|
sub_id: String,
|
|
updates: SessionSettingsUpdate,
|
|
) -> CodexResult<Arc<TurnContext>> {
|
|
let update_result: CodexResult<_> = {
|
|
let mut state = self.state.lock().await;
|
|
match state.session_configuration.clone().apply(&updates) {
|
|
Ok(next) => {
|
|
let effective_environments = updates
|
|
.environments
|
|
.clone()
|
|
.unwrap_or_else(|| next.environments.clone());
|
|
let turn_environments =
|
|
self.resolve_turn_environments(&effective_environments)?;
|
|
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();
|
|
Ok((
|
|
next,
|
|
turn_environments,
|
|
sandbox_policy_changed,
|
|
previous_cwd,
|
|
codex_home,
|
|
session_source,
|
|
))
|
|
}
|
|
Err(err) => Err(CodexErr::InvalidRequest(err.to_string())),
|
|
}
|
|
};
|
|
|
|
let (
|
|
session_configuration,
|
|
turn_environments,
|
|
sandbox_policy_changed,
|
|
previous_cwd,
|
|
codex_home,
|
|
session_source,
|
|
) = match update_result {
|
|
Ok(update) => update,
|
|
Err(err) => {
|
|
let message = err.to_string();
|
|
self.send_event_raw(Event {
|
|
id: sub_id.clone(),
|
|
msg: EventMsg::Error(ErrorEvent {
|
|
message: message.clone(),
|
|
codex_error_info: Some(CodexErrorInfo::BadRequest),
|
|
}),
|
|
})
|
|
.await;
|
|
return Err(CodexErr::InvalidRequest(message));
|
|
}
|
|
};
|
|
|
|
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,
|
|
turn_environments,
|
|
)
|
|
.await)
|
|
}
|
|
|
|
fn resolve_turn_environments(
|
|
&self,
|
|
environments: &[TurnEnvironmentSelection],
|
|
) -> CodexResult<Vec<TurnEnvironment>> {
|
|
let mut turn_environments = Vec::with_capacity(environments.len());
|
|
for selected_environment in environments {
|
|
let environment_id = selected_environment.environment_id.clone();
|
|
let environment = self
|
|
.services
|
|
.environment_manager
|
|
.get_environment(&environment_id)
|
|
.ok_or_else(|| {
|
|
CodexErr::InvalidRequest(format!(
|
|
"unknown turn environment id `{environment_id}`"
|
|
))
|
|
})?;
|
|
let cwd = selected_environment.cwd.clone();
|
|
turn_environments.push(TurnEnvironment {
|
|
environment_id,
|
|
environment,
|
|
cwd,
|
|
});
|
|
}
|
|
|
|
Ok(turn_environments)
|
|
}
|
|
|
|
async fn new_turn_from_configuration(
|
|
&self,
|
|
sub_id: String,
|
|
session_configuration: SessionConfiguration,
|
|
final_output_json_schema: Option<Option<Value>>,
|
|
turn_environments: Vec<TurnEnvironment>,
|
|
) -> Arc<TurnContext> {
|
|
let primary_turn_environment = turn_environments.first();
|
|
let environment = primary_turn_environment
|
|
.map(|turn_environment| Arc::clone(&turn_environment.environment));
|
|
let cwd = primary_turn_environment
|
|
.map(|turn_environment| turn_environment.cwd.clone())
|
|
.unwrap_or_else(|| session_configuration.cwd.clone());
|
|
let per_turn_config = Self::build_per_turn_config(&session_configuration, cwd.clone());
|
|
{
|
|
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(session_configuration.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 = 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 goal_tools_supported = !per_turn_config.ephemeral && self.state_db().is_some();
|
|
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())
|
|
}),
|
|
environment,
|
|
turn_environments,
|
|
cwd,
|
|
sub_id,
|
|
skills_outcome,
|
|
goal_tools_supported,
|
|
);
|
|
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<TurnContext> {
|
|
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<TurnContext> {
|
|
let session_configuration = {
|
|
let state = self.state.lock().await;
|
|
state.session_configuration.clone()
|
|
};
|
|
let turn_environments =
|
|
match self.resolve_turn_environments(&session_configuration.environments) {
|
|
Ok(turn_environments) => turn_environments,
|
|
Err(err) => {
|
|
warn!("failed to resolve stored session environments: {err}");
|
|
Vec::new()
|
|
}
|
|
};
|
|
|
|
self.new_turn_from_configuration(
|
|
sub_id,
|
|
session_configuration,
|
|
/*final_output_json_schema*/ None,
|
|
turn_environments,
|
|
)
|
|
.await
|
|
}
|
|
}
|