mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
## What changed - Add the disabled-by-default `output_token_replay` feature to request `output.encrypted_content` from OpenAI providers. - Preserve encrypted message and tool-call output, message and function-call status, and output-text `annotations` and `logprobs` through history and replay. Update the generated protocol schemas and SDK types. - Strip output ciphertext from requests to other providers while retaining it in stored history, including after replay is disabled. Remove encrypted content from memory extraction inputs. ## Testing Add integration coverage for the feature/provider combinations, metadata replay across follow-up requests, and history preservation when resuming with replay disabled. GitOrigin-RevId: 77729a021b6771933402aebe74ece8eb39c057e2
1991 lines
94 KiB
Rust
1991 lines
94 KiB
Rust
use super::input_queue::InputQueue;
|
||
use super::mcp_refresh::McpRefresh;
|
||
use super::retained_context::CodeModeMessageTasks;
|
||
use super::step_context::StepContext;
|
||
use super::step_settings::ModelInfoOverrides;
|
||
use super::step_settings::StepSettings;
|
||
use super::step_settings::StepSettingsConstraints;
|
||
use super::step_settings::StepSettingsUpdate;
|
||
use super::*;
|
||
use crate::agent::api::AgentControl;
|
||
use crate::agents_md_manager::AgentsMdManager;
|
||
use crate::agents_md_manager::SessionInstructions;
|
||
use crate::config::ConstraintError;
|
||
use crate::environment_selection::ThreadEnvironments;
|
||
use crate::environment_selection::TurnEnvironmentSnapshot;
|
||
use crate::hook_mcp_executor::CoreHookMcpExecutor;
|
||
use crate::mcp_tool_call::McpToolApprovalContext;
|
||
use crate::responses_metadata::CodexResponsesMetadata;
|
||
use crate::responses_metadata::CodexResponsesRequestKind;
|
||
use crate::responses_metadata::CompactionTurnMetadata;
|
||
use crate::shell_snapshot::ShellSnapshot;
|
||
use crate::shell_snapshot::SnapshotCredentialBrokerState;
|
||
use crate::state::ActiveTurn;
|
||
use crate::turn_metadata::ExecutionMetadata;
|
||
use codex_analytics::ThreadProductUpdate;
|
||
use codex_attachment_store::AttachmentStore;
|
||
use codex_extension_api::ExtensionDataInit;
|
||
use codex_http_client::ClientRouteClass;
|
||
use codex_http_client::RouteAwareClientPool;
|
||
use codex_login::auth::AgentIdentityAuthPolicy;
|
||
use codex_model_provider::SharedModelProvider;
|
||
use codex_prompts::render_model_instructions;
|
||
use codex_protocol::SessionId;
|
||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||
use codex_protocol::mcp::ClientMcpExtensions;
|
||
use codex_protocol::models::ProfileWorkspaceRoot;
|
||
use codex_protocol::permissions::FileSystemPath;
|
||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||
use codex_protocol::protocol::EnvironmentConfig;
|
||
use codex_protocol::protocol::HookCompletedEvent;
|
||
use codex_protocol::protocol::MultiAgentVersion;
|
||
use codex_protocol::protocol::ThreadHistoryMode;
|
||
use codex_protocol::protocol::ThreadSource;
|
||
use codex_protocol::protocol::TurnEnvironmentRequests;
|
||
use codex_protocol::protocol::TurnEnvironmentSelections;
|
||
use codex_sandboxing::SandboxType;
|
||
use codex_skills::SkillError;
|
||
use codex_utils_git_discovery::GitRootDiscovery;
|
||
use codex_utils_path::replace_path_and_deduplicate;
|
||
use std::sync::OnceLock;
|
||
use tokio::sync::Semaphore;
|
||
|
||
type McpToolApprovalMetadataMap =
|
||
HashMap<(String, String), std::sync::Weak<McpToolApprovalContext>>;
|
||
|
||
/// Context for an initialized model agent
|
||
///
|
||
/// A session has at most 1 running task at a time, and can be interrupted by user input.
|
||
pub(crate) struct Session {
|
||
pub(crate) thread_id: ThreadId,
|
||
pub(crate) installation_id: String,
|
||
pub(super) tx_event: Sender<Event>,
|
||
pub(super) agent_status: watch::Sender<AgentStatus>,
|
||
pub(super) state: Mutex<SessionState>,
|
||
/// Orders accepted settings commits and their persisted events with compaction checkpoints.
|
||
/// Keep this separate from `state` so storage I/O does not block runtime state access.
|
||
pub(super) thread_settings_persistence: Semaphore,
|
||
pub(super) code_mode_message_tasks: CodeModeMessageTasks,
|
||
/// Serializes rebuild/apply cycles for the running proxy; each cycle
|
||
/// rebuilds from the current SessionState while holding this lock.
|
||
pub(super) managed_network_proxy_refresh_lock: Semaphore,
|
||
/// The set of enabled features should be invariant for the lifetime of the
|
||
/// session.
|
||
pub(super) features: ManagedFeatures,
|
||
pub(super) isolation: codex_extension_api::SessionIsolation,
|
||
pub(crate) tool_policy: Arc<codex_extension_api::ToolPolicy>,
|
||
pub(crate) windows_sandbox_proxy_settings_mode:
|
||
codex_sandboxing::WindowsSandboxProxySettingsMode,
|
||
pub(super) multi_agent_version: OnceLock<MultiAgentVersion>,
|
||
/// Owns invalidation and serializes refreshes without blocking captured calls.
|
||
pub(super) mcp_refresh: McpRefresh,
|
||
/// Non-owning lookup for approval data retained by running MCP invocations.
|
||
pub(crate) mcp_tool_approval_metadata: std::sync::Mutex<McpToolApprovalMetadataMap>,
|
||
pub(super) mcp_elicitation_reviewer_handle: OnceLock<codex_mcp::ElicitationReviewerHandle>,
|
||
pub(super) mcp_elicitation_lifecycle_handle: OnceLock<codex_mcp::ElicitationLifecycle>,
|
||
pub(super) mcp_prewarm_tx: async_channel::Sender<()>,
|
||
pub(super) mcp_prewarm_shutdown: CancellationToken,
|
||
pub(super) mcp_prewarm_task: std::sync::Mutex<Option<JoinHandle<()>>>,
|
||
pub(crate) conversation: Arc<RealtimeConversationManager>,
|
||
pub(crate) realtime_history: Option<Mutex<crate::realtime_history::RealtimeHistoryState>>,
|
||
pub(crate) active_turn: Mutex<Option<ActiveTurn>>,
|
||
pub(crate) async_hook_results: async_channel::Receiver<HookCompletedEvent>,
|
||
pub(crate) input_queue: InputQueue,
|
||
pub(crate) services: SessionServices,
|
||
pub(super) git_enrichment_policy: GitEnrichmentPolicy,
|
||
pub(super) fork_persistence: ForkPersistence,
|
||
pub(super) history_initialization: HistoryInitialization,
|
||
pub(super) forked_from_ordinal_exclusive: Option<u64>,
|
||
pub(super) next_internal_sub_id: AtomicU64,
|
||
}
|
||
|
||
#[derive(Clone)]
|
||
pub(crate) struct SessionConfiguration {
|
||
/// Runtime provider and its provider-specific execution policy.
|
||
pub(super) provider: SharedModelProvider,
|
||
|
||
/// Desired configured inputs inherited by future turns.
|
||
pub(super) step_settings: Arc<StepSettings>,
|
||
/// Host-supplied extension data inherited by future turns.
|
||
pub(super) turn_extension_init: ExtensionDataInit,
|
||
/// Ordered environments inherited by future turns.
|
||
pub(super) environments: Vec<TurnEnvironmentSelection>,
|
||
/// Explicit startup overrides used when resolving effective model metadata.
|
||
pub(super) model_info_overrides: ModelInfoOverrides,
|
||
|
||
/// Developer instructions that supplement the base instructions.
|
||
pub(super) developer_instructions: Option<String>,
|
||
|
||
/// Base instructions for the session.
|
||
pub(super) base_instructions: String,
|
||
|
||
/// Permission profile state for the session. Keep the constrained profile,
|
||
/// active profile id, and profile-defined workspace roots in sync by using
|
||
/// the methods below instead of mutating the fields independently.
|
||
pub(super) permission_profile_state: PermissionProfileState,
|
||
pub(super) allow_login_shell: bool,
|
||
pub(super) shell_environment_policy: ShellEnvironmentPolicy,
|
||
// TODO(anp): Reconcile these legacy thread defaults with TurnEnvironment::sandbox_context;
|
||
// internal sandbox decisions should use the selected environment's configuration.
|
||
pub(super) windows_sandbox_level: WindowsSandboxLevel,
|
||
pub(super) windows_sandbox_type: SandboxType,
|
||
pub(super) use_legacy_landlock: bool,
|
||
|
||
/// Legacy thread cwd used when a turn does not select an environment.
|
||
pub(super) legacy_fallback_cwd: AbsolutePathBuf,
|
||
/// Top-level runtime workspace roots, independent of explicit environment selections.
|
||
pub(super) runtime_workspace_roots: Vec<AbsolutePathBuf>,
|
||
/// Directory containing all Codex state for this session.
|
||
pub(super) codex_home: AbsolutePathBuf,
|
||
/// Optional user-facing name for the thread, updated during the session.
|
||
pub(super) thread_name: Option<String>,
|
||
/// Thread-owned plugin selection inherited by future turns.
|
||
pub(super) disabled_plugin_ids: Vec<String>,
|
||
|
||
// TODO(pakrym): Remove config from here
|
||
pub(super) original_config_do_not_use: Arc<Config>,
|
||
/// Optional service name tag for session metrics.
|
||
pub(super) metrics_service_name: Option<String>,
|
||
pub(super) app_server_client_name: Option<String>,
|
||
pub(super) app_server_client_version: Option<String>,
|
||
/// Guardian reviewer identity is trusted only when established during an in-memory spawn.
|
||
pub(super) trusted_guardian_reviewer: bool,
|
||
/// Source of the session (cli, vscode, exec, mcp, ...)
|
||
pub(super) session_source: SessionSource,
|
||
/// Persisted thread history contract selected when this thread was created.
|
||
pub(super) history_mode: ThreadHistoryMode,
|
||
/// Immediate history source copied into this thread, when this thread was forked.
|
||
pub(super) forked_from_thread_id: Option<ThreadId>,
|
||
/// Immediate control/spawn parent for this thread, when it has one.
|
||
pub(super) parent_thread_id: Option<ThreadId>,
|
||
/// Optional analytics source classification for this thread.
|
||
pub(super) thread_source: Option<ThreadSource>,
|
||
/// Effective originator used for this thread's Responses requests and analytics events.
|
||
pub(super) originator: String,
|
||
pub(super) dynamic_tools: Vec<DynamicToolSpec>,
|
||
pub(super) user_shell_override: Option<shell::Shell>,
|
||
}
|
||
|
||
impl SessionConfiguration {
|
||
pub(super) fn cwd(&self) -> &AbsolutePathBuf {
|
||
&self.legacy_fallback_cwd
|
||
}
|
||
|
||
pub(crate) fn codex_home(&self) -> &AbsolutePathBuf {
|
||
&self.codex_home
|
||
}
|
||
|
||
pub(super) fn inferred_environment_config(&self) -> EnvironmentConfig {
|
||
EnvironmentConfig {
|
||
allow_login_shell: self.allow_login_shell,
|
||
workspace_roots: Vec::new(),
|
||
permission_profile: self.permission_profile_state.snapshot(),
|
||
shell_environment_policy: self.shell_environment_policy.clone(),
|
||
windows_sandbox_level: self.windows_sandbox_level,
|
||
windows_sandbox_type: self
|
||
.original_config_do_not_use
|
||
.windows_sandbox_type_from_config(),
|
||
use_legacy_landlock: self.use_legacy_landlock,
|
||
exec_policy: None,
|
||
mcp_policy: None,
|
||
network_policy: None,
|
||
selected_capability_roots: Vec::new(),
|
||
}
|
||
}
|
||
|
||
pub(super) fn permission_profile(&self) -> PermissionProfile {
|
||
self.permission_profile_state.permission_profile().clone()
|
||
}
|
||
|
||
fn materialized_permission_profile(
|
||
&self,
|
||
environments: &[TurnEnvironmentSelection],
|
||
) -> PermissionProfile {
|
||
let workspace_roots = ThreadEnvironments::primary_workspace_roots_for(environments);
|
||
self.permission_profile()
|
||
.materialize_project_roots_with_workspace_roots(&workspace_roots)
|
||
}
|
||
|
||
fn effective_permission_profile(
|
||
&self,
|
||
environments: &[TurnEnvironmentSelection],
|
||
) -> PermissionProfile {
|
||
ThreadEnvironments::primary_config_for(environments)
|
||
.map(|config| {
|
||
config
|
||
.permission_profile
|
||
.permission_profile()
|
||
.clone()
|
||
.materialize_project_roots_with_path_uris(&config.workspace_roots)
|
||
})
|
||
.unwrap_or_else(|| self.materialized_permission_profile(environments))
|
||
}
|
||
|
||
pub(super) fn active_permission_profile(&self) -> Option<ActivePermissionProfile> {
|
||
self.permission_profile_state.active_permission_profile()
|
||
}
|
||
|
||
pub(super) fn apply_permission_profile_to_permissions(
|
||
&self,
|
||
permissions: &mut crate::config::Permissions,
|
||
) {
|
||
permissions.set_permission_profile_state(self.permission_profile_state.clone());
|
||
}
|
||
|
||
#[cfg(test)]
|
||
pub(super) fn set_permission_profile_for_tests(
|
||
&mut self,
|
||
permission_profile: PermissionProfile,
|
||
) -> ConstraintResult<()> {
|
||
self.permission_profile_state
|
||
.set_legacy_permission_profile(permission_profile)
|
||
}
|
||
|
||
pub(super) fn sandbox_policy(
|
||
&self,
|
||
environments: &[TurnEnvironmentSelection],
|
||
) -> SandboxPolicy {
|
||
let permission_profile = self.materialized_permission_profile(environments);
|
||
codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
|
||
&permission_profile,
|
||
self.cwd(),
|
||
)
|
||
}
|
||
|
||
pub(super) fn file_system_sandbox_policy(
|
||
&self,
|
||
environments: &[TurnEnvironmentSelection],
|
||
) -> FileSystemSandboxPolicy {
|
||
self.materialized_permission_profile(environments)
|
||
.file_system_sandbox_policy()
|
||
}
|
||
|
||
pub(super) fn network_sandbox_policy(&self) -> NetworkSandboxPolicy {
|
||
self.permission_profile_state
|
||
.permission_profile()
|
||
.network_sandbox_policy()
|
||
}
|
||
|
||
pub(super) fn thread_config_snapshot(
|
||
&self,
|
||
environment_selections: Vec<TurnEnvironmentSelection>,
|
||
) -> ThreadConfigSnapshot {
|
||
let workspace_roots =
|
||
ThreadEnvironments::primary_workspace_roots_for(&environment_selections);
|
||
let permission_profile = ThreadEnvironments::primary_config_for(&environment_selections)
|
||
.map(|config| config.permission_profile.clone())
|
||
.unwrap_or_else(|| self.permission_profile_state.snapshot());
|
||
ThreadConfigSnapshot {
|
||
model: self.step_settings.collaboration_mode.model().to_string(),
|
||
model_provider_id: self.original_config_do_not_use.model_provider_id.clone(),
|
||
service_tier: self.step_settings.service_tier.clone(),
|
||
approval_policy: self.step_settings.approval_policy.value(),
|
||
approvals_reviewer: self.step_settings.approvals_reviewer,
|
||
permission_profile: self.effective_permission_profile(&environment_selections),
|
||
full_access: codex_protocol::protocol::has_full_access(
|
||
self.step_settings.approval_policy.value(),
|
||
&self.permission_profile(),
|
||
environment_selections
|
||
.iter()
|
||
.map(|environment| &environment.config),
|
||
),
|
||
active_permission_profile: permission_profile.active_permission_profile(),
|
||
environments: TurnEnvironmentSelections::new(
|
||
self.legacy_fallback_cwd.clone(),
|
||
environment_selections,
|
||
),
|
||
workspace_roots,
|
||
profile_workspace_roots: permission_profile.profile_workspace_roots().to_vec(),
|
||
ephemeral: self.original_config_do_not_use.ephemeral,
|
||
reasoning_effort: self.step_settings.collaboration_mode.reasoning_effort(),
|
||
reasoning_summary: self.step_settings.reasoning_summary,
|
||
personality: self.step_settings.personality,
|
||
collaboration_mode: self.step_settings.collaboration_mode.clone(),
|
||
session_source: self.session_source.clone(),
|
||
history_mode: self.history_mode,
|
||
forked_from_thread_id: self.forked_from_thread_id,
|
||
parent_thread_id: self.parent_thread_id,
|
||
thread_source: self.thread_source.clone(),
|
||
originator: self.originator.clone(),
|
||
disabled_plugin_ids: self.disabled_plugin_ids.clone(),
|
||
turn_extension_init: self.turn_extension_init.clone(),
|
||
}
|
||
}
|
||
|
||
/// Captures thread-owned settings for persistence and resume.
|
||
pub(super) fn thread_settings_snapshot(
|
||
&self,
|
||
environment_selections: &[TurnEnvironmentSelection],
|
||
) -> ThreadSettingsSnapshot {
|
||
ThreadSettingsSnapshot {
|
||
model: self.step_settings.collaboration_mode.model().to_string(),
|
||
model_provider_id: self.original_config_do_not_use.model_provider_id.clone(),
|
||
service_tier: self.step_settings.service_tier.clone(),
|
||
approval_policy: self.step_settings.approval_policy.value(),
|
||
approvals_reviewer: self.step_settings.approvals_reviewer,
|
||
permission_profile: self.materialized_permission_profile(environment_selections),
|
||
active_permission_profile: self.active_permission_profile(),
|
||
cwd: self.legacy_fallback_cwd.clone(),
|
||
runtime_workspace_roots: Some(self.runtime_workspace_roots.clone()),
|
||
reasoning_effort: self.step_settings.collaboration_mode.reasoning_effort(),
|
||
reasoning_summary: self.step_settings.reasoning_summary,
|
||
personality: self.step_settings.personality,
|
||
collaboration_mode: self.step_settings.collaboration_mode.clone(),
|
||
disabled_plugin_ids: self.disabled_plugin_ids.clone(),
|
||
}
|
||
}
|
||
|
||
/// Captures thread-owned settings and their separately owned environments.
|
||
pub(super) fn restorable_thread_settings(
|
||
&self,
|
||
environment_selections: Vec<TurnEnvironmentSelection>,
|
||
) -> CodexThreadSettingsOverrides {
|
||
CodexThreadSettingsOverrides {
|
||
environments: Some(TurnEnvironmentRequests::new(
|
||
self.legacy_fallback_cwd.clone(),
|
||
environment_selections
|
||
.into_iter()
|
||
.map(TurnEnvironmentSelection::into_request)
|
||
.collect(),
|
||
)),
|
||
runtime_workspace_roots: Some(self.runtime_workspace_roots.clone()),
|
||
profile_workspace_roots: Some(
|
||
self.permission_profile_state
|
||
.profile_workspace_roots()
|
||
.to_vec(),
|
||
),
|
||
approval_policy: Some(self.step_settings.approval_policy.value()),
|
||
approvals_reviewer: Some(self.step_settings.approvals_reviewer),
|
||
permission_profile: Some(self.permission_profile()),
|
||
active_permission_profile: self.active_permission_profile(),
|
||
windows_sandbox_level: Some(self.windows_sandbox_level),
|
||
summary: self.step_settings.reasoning_summary,
|
||
service_tier: Some(self.step_settings.service_tier.clone()),
|
||
collaboration_mode: Some(self.step_settings.collaboration_mode.clone()),
|
||
personality: self.step_settings.personality,
|
||
disabled_plugin_ids: Some(self.disabled_plugin_ids.clone()),
|
||
turn_extension_init: Some(self.turn_extension_init.clone()),
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
pub(super) fn validate(
|
||
&self,
|
||
environments: &[TurnEnvironmentSelection],
|
||
) -> ConstraintResult<()> {
|
||
self.step_settings
|
||
.validate(&self.step_settings_constraints(environments))?;
|
||
super::environment::validate_environment_configs(environments)
|
||
}
|
||
|
||
pub(super) fn step_settings_constraints(
|
||
&self,
|
||
environments: &[TurnEnvironmentSelection],
|
||
) -> StepSettingsConstraints<'_> {
|
||
let permission_profile = self.effective_permission_profile(environments);
|
||
StepSettingsConstraints {
|
||
requirements: self
|
||
.original_config_do_not_use
|
||
.config_layer_stack
|
||
.requirements(),
|
||
guardian_approval_enabled: self
|
||
.original_config_do_not_use
|
||
.features
|
||
.enabled(Feature::GuardianApproval),
|
||
trusted_guardian_reviewer: self.trusted_guardian_reviewer,
|
||
has_full_disk_write_access: permission_profile
|
||
.file_system_sandbox_policy()
|
||
.has_full_disk_write_access(),
|
||
}
|
||
}
|
||
|
||
pub(super) fn apply(
|
||
&self,
|
||
updates: &SessionSettingsUpdate,
|
||
current_environments: &[TurnEnvironmentSelection],
|
||
) -> ConstraintResult<Self> {
|
||
let mut next_configuration = self.clone();
|
||
if let Some(turn_extension_init) = &updates.turn_extension_init {
|
||
next_configuration.turn_extension_init = turn_extension_init.clone();
|
||
}
|
||
if let Some(disabled_plugin_ids) = &updates.disabled_plugin_ids {
|
||
next_configuration.disabled_plugin_ids = disabled_plugin_ids.clone();
|
||
}
|
||
let current_file_system_sandbox_policy =
|
||
self.file_system_sandbox_policy(current_environments);
|
||
let file_system_policy_has_rebindable_project_root_write =
|
||
current_file_system_sandbox_policy
|
||
.entries
|
||
.iter()
|
||
.any(|entry| {
|
||
entry.access.can_write()
|
||
&& matches!(
|
||
&entry.path,
|
||
FileSystemPath::Special {
|
||
value: FileSystemSpecialPath::ProjectRoots { subpath: None },
|
||
}
|
||
)
|
||
});
|
||
if let Some(windows_sandbox_level) = updates.windows_sandbox_level {
|
||
next_configuration.windows_sandbox_level = windows_sandbox_level;
|
||
}
|
||
|
||
let current_cwd = self.cwd().clone();
|
||
if let Some(environments) = &updates.environments {
|
||
next_configuration.legacy_fallback_cwd = environments.legacy_fallback_cwd.clone();
|
||
next_configuration.environments = environments.environments.clone();
|
||
}
|
||
let cwd_changed = next_configuration.legacy_fallback_cwd != current_cwd;
|
||
if let Some(runtime_workspace_roots) = &updates.runtime_workspace_roots {
|
||
next_configuration.runtime_workspace_roots = runtime_workspace_roots.clone();
|
||
} else if cwd_changed {
|
||
next_configuration.runtime_workspace_roots = replace_path_and_deduplicate(
|
||
next_configuration.runtime_workspace_roots,
|
||
current_cwd.as_path(),
|
||
next_configuration.legacy_fallback_cwd.clone(),
|
||
);
|
||
}
|
||
|
||
if let Some(permission_profile) = updates.permission_profile.clone() {
|
||
let active_permission_profile =
|
||
updates.active_permission_profile.clone().or_else(|| {
|
||
if permission_profile == self.permission_profile() {
|
||
self.active_permission_profile()
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
next_configuration.set_permission_profile_projection(
|
||
permission_profile,
|
||
active_permission_profile,
|
||
updates.profile_workspace_roots.clone().unwrap_or_default(),
|
||
Some(¤t_file_system_sandbox_policy),
|
||
)?;
|
||
if let Some(active_permission_profile) = next_configuration.active_permission_profile()
|
||
{
|
||
let mut config = (*next_configuration.original_config_do_not_use).clone();
|
||
let permission_profile = next_configuration.permission_profile();
|
||
config.permissions.network = config
|
||
.network_proxy_spec_for_active_permission_profile(
|
||
&active_permission_profile,
|
||
&permission_profile,
|
||
)
|
||
.map_err(|err| ConstraintError::InvalidValue {
|
||
field_name: "default_permissions",
|
||
candidate: active_permission_profile.id.clone(),
|
||
allowed: format!(
|
||
"configured permission profile with valid network policy ({err})"
|
||
),
|
||
requirement_source: codex_config::RequirementSource::Unknown,
|
||
})?;
|
||
config
|
||
.permissions
|
||
.set_permission_profile_from_session_snapshot(
|
||
PermissionProfileSnapshot::active(
|
||
permission_profile,
|
||
active_permission_profile,
|
||
),
|
||
)?;
|
||
next_configuration.original_config_do_not_use = Arc::new(config);
|
||
}
|
||
} else if let Some(sandbox_policy) = updates.sandbox_policy.clone() {
|
||
let file_system_sandbox_policy =
|
||
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
|
||
&sandbox_policy,
|
||
next_configuration.cwd(),
|
||
¤t_file_system_sandbox_policy,
|
||
);
|
||
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
|
||
next_configuration
|
||
.permission_profile_state
|
||
.set_legacy_permission_profile(
|
||
PermissionProfile::from_runtime_permissions_with_enforcement(
|
||
SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy),
|
||
&file_system_sandbox_policy,
|
||
network_sandbox_policy,
|
||
),
|
||
)?;
|
||
} else if cwd_changed && file_system_policy_has_rebindable_project_root_write {
|
||
// Compatibility projection can resolve filesystem paths. Only compute it
|
||
// when a cwd-bound legacy policy might need rebinding.
|
||
let current_sandbox_policy = self.sandbox_policy(current_environments);
|
||
if current_file_system_sandbox_policy.is_semantically_equivalent_to(
|
||
&FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
|
||
¤t_sandbox_policy,
|
||
self.cwd(),
|
||
¤t_file_system_sandbox_policy,
|
||
),
|
||
self.cwd(),
|
||
) {
|
||
// Preserve richer split policies across cwd-only updates; only
|
||
// rederive when the session is already using a structurally
|
||
// cwd-bound legacy bridge.
|
||
let file_system_sandbox_policy =
|
||
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
|
||
¤t_sandbox_policy,
|
||
next_configuration.cwd(),
|
||
¤t_file_system_sandbox_policy,
|
||
);
|
||
next_configuration
|
||
.permission_profile_state
|
||
.set_legacy_permission_profile(
|
||
PermissionProfile::from_runtime_permissions_with_enforcement(
|
||
SandboxEnforcement::from_legacy_sandbox_policy(¤t_sandbox_policy),
|
||
&file_system_sandbox_policy,
|
||
self.network_sandbox_policy(),
|
||
),
|
||
)?;
|
||
}
|
||
}
|
||
if let Some(app_server_client_name) = updates.app_server_client_name.clone() {
|
||
next_configuration.app_server_client_name = Some(app_server_client_name);
|
||
}
|
||
if let Some(app_server_client_version) = updates.app_server_client_version.clone() {
|
||
next_configuration.app_server_client_version = Some(app_server_client_version);
|
||
}
|
||
let next_environments = updates
|
||
.environments
|
||
.as_ref()
|
||
.map_or(current_environments, |environments| {
|
||
environments.environments.as_slice()
|
||
});
|
||
super::environment::validate_environment_configs(next_environments)?;
|
||
// Apply step settings last: the proposed permissions and environment
|
||
// selections must be complete before deriving their validation constraints.
|
||
next_configuration.step_settings = Arc::new(self.step_settings.apply(
|
||
&updates.step_settings,
|
||
&next_configuration.step_settings_constraints(next_environments),
|
||
)?);
|
||
Ok(next_configuration)
|
||
}
|
||
|
||
fn set_permission_profile_projection(
|
||
&mut self,
|
||
permission_profile: PermissionProfile,
|
||
active_permission_profile: Option<ActivePermissionProfile>,
|
||
profile_workspace_roots: Vec<ProfileWorkspaceRoot>,
|
||
preserve_deny_reads_from: Option<&FileSystemSandboxPolicy>,
|
||
) -> ConstraintResult<()> {
|
||
let enforcement = permission_profile.enforcement();
|
||
let (mut file_system_sandbox_policy, network_sandbox_policy) =
|
||
permission_profile.to_runtime_permissions();
|
||
if let Some(existing_file_system_policy) = preserve_deny_reads_from {
|
||
file_system_sandbox_policy
|
||
.preserve_deny_read_restrictions_from(existing_file_system_policy);
|
||
}
|
||
let effective_permission_profile =
|
||
PermissionProfile::from_runtime_permissions_with_enforcement(
|
||
enforcement,
|
||
&file_system_sandbox_policy,
|
||
network_sandbox_policy,
|
||
);
|
||
|
||
let permission_snapshot = match active_permission_profile {
|
||
Some(active_permission_profile) => {
|
||
PermissionProfileSnapshot::active_with_profile_workspace_roots(
|
||
effective_permission_profile,
|
||
active_permission_profile,
|
||
profile_workspace_roots,
|
||
)
|
||
}
|
||
None => PermissionProfileSnapshot::legacy(effective_permission_profile),
|
||
};
|
||
|
||
self.permission_profile_state
|
||
.set_permission_profile_snapshot(permission_snapshot)
|
||
}
|
||
}
|
||
|
||
/// The configuration and public snapshot published by one settings commit.
|
||
pub(crate) struct SessionSettingsCommit {
|
||
pub(crate) configuration: SessionConfiguration,
|
||
pub(crate) snapshot: ThreadSettingsSnapshot,
|
||
}
|
||
|
||
#[derive(Default, Clone)]
|
||
pub(crate) struct SessionSettingsUpdate {
|
||
pub(crate) step_settings: StepSettingsUpdate,
|
||
/// Omission preserves the current data; an empty initializer clears it.
|
||
pub(crate) turn_extension_init: Option<ExtensionDataInit>,
|
||
pub(crate) environments: Option<TurnEnvironmentSelections>,
|
||
pub(crate) runtime_workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||
pub(crate) profile_workspace_roots: Option<Vec<ProfileWorkspaceRoot>>,
|
||
pub(crate) sandbox_policy: Option<SandboxPolicy>,
|
||
pub(crate) permission_profile: Option<PermissionProfile>,
|
||
pub(crate) active_permission_profile: Option<ActivePermissionProfile>,
|
||
pub(crate) windows_sandbox_level: Option<WindowsSandboxLevel>,
|
||
pub(crate) service_tier_for_turn: Option<String>,
|
||
pub(crate) app_server_client_name: Option<String>,
|
||
pub(crate) app_server_client_version: Option<String>,
|
||
pub(crate) disabled_plugin_ids: Option<Vec<String>>,
|
||
}
|
||
|
||
pub(crate) struct AppServerClientMetadata {
|
||
pub(crate) client_name: Option<String>,
|
||
pub(crate) client_version: Option<String>,
|
||
}
|
||
|
||
async fn warm_plugins_and_skills_for_session_init(
|
||
config: Arc<Config>,
|
||
plugins_manager: Arc<PluginsManager>,
|
||
skills_service: Arc<HostSkillsService>,
|
||
turn_environments: &TurnEnvironmentSnapshot,
|
||
extensions: &codex_extension_api::ExtensionRegistry<Config>,
|
||
session_source: &SessionSource,
|
||
) -> Vec<SkillError> {
|
||
// Guardian does not consume skills, including the legacy empty-registry catalog.
|
||
if crate::guardian::is_basic_session_source(session_source) {
|
||
return Vec::new();
|
||
}
|
||
let plugins_input = config.plugins_config_input();
|
||
let plugin_outcome = plugins_manager.plugins_for_config(&plugins_input).await;
|
||
if config.features.enabled(Feature::SkipHostSkillDiscovery)
|
||
&& !extensions.requires_host_skill_discovery()
|
||
{
|
||
return Vec::new();
|
||
}
|
||
|
||
let fs = turn_environments.primary_filesystem();
|
||
let effective_skill_roots = plugin_outcome.effective_plugin_skill_roots();
|
||
let plugin_skill_snapshots = plugins_manager.plugin_skill_snapshots_for_config(&plugins_input);
|
||
let skills_input = skills_load_input_from_config(config.as_ref(), effective_skill_roots)
|
||
.with_plugin_skill_snapshots(plugin_skill_snapshots);
|
||
skills_service
|
||
.snapshot_for_config(&skills_input, fs)
|
||
.await
|
||
.outcome()
|
||
.errors
|
||
.clone()
|
||
}
|
||
|
||
impl Session {
|
||
/// Returns the concrete identity for this thread.
|
||
pub(crate) fn thread_id(&self) -> ThreadId {
|
||
self.thread_id
|
||
}
|
||
|
||
/// Returns the identity shared by the root thread and all descendant threads.
|
||
pub(crate) fn session_id(&self) -> SessionId {
|
||
self.services.agent_control.identity()
|
||
}
|
||
|
||
pub(crate) async fn originator(&self) -> String {
|
||
let state = self.state.lock().await;
|
||
state.session_configuration.originator.clone()
|
||
}
|
||
|
||
pub(crate) async fn dynamic_tools(&self) -> Vec<DynamicToolSpec> {
|
||
let state = self.state.lock().await;
|
||
state.session_configuration.dynamic_tools.clone()
|
||
}
|
||
|
||
pub(crate) async fn responses_metadata(
|
||
&self,
|
||
step_context: &StepContext,
|
||
request_kind: CodexResponsesRequestKind,
|
||
) -> CodexResponsesMetadata {
|
||
let (window_id, window_number, context_window_id) = self.current_window().await;
|
||
let mut responses_metadata = step_context.turn.turn_metadata_state.to_responses_metadata(
|
||
self.installation_id.clone(),
|
||
window_id,
|
||
request_kind,
|
||
);
|
||
ExecutionMetadata::from_settings(&step_context.settings).apply_to(&mut responses_metadata);
|
||
responses_metadata.tool_namespaces_info = if step_context
|
||
.turn
|
||
.config
|
||
.tool_registry
|
||
.turn_metadata_includes_tool_info
|
||
&& step_context.settings.model_info.use_responses_lite
|
||
{
|
||
step_context.tool_router.tool_namespaces_info().cloned()
|
||
} else {
|
||
None
|
||
};
|
||
self.with_window_and_fork_metadata(
|
||
&step_context.turn,
|
||
responses_metadata,
|
||
window_number,
|
||
context_window_id,
|
||
)
|
||
}
|
||
|
||
// TODO(CDXENT-454): Build the compaction request and metadata from the captured execution.
|
||
// Remote compaction currently attaches only finalized tool inventory because the rest of the
|
||
// request remains turn-backed; local compaction does not have a finalized request inventory.
|
||
pub(crate) async fn compaction_responses_metadata(
|
||
&self,
|
||
turn_context: &TurnContext,
|
||
compaction_metadata: CompactionTurnMetadata,
|
||
) -> CodexResponsesMetadata {
|
||
let (window_id, window_number, context_window_id) = self.current_window().await;
|
||
let responses_metadata = turn_context.turn_metadata_state.to_responses_metadata(
|
||
self.installation_id.clone(),
|
||
window_id,
|
||
CodexResponsesRequestKind::Compaction(compaction_metadata),
|
||
);
|
||
self.with_window_and_fork_metadata(
|
||
turn_context,
|
||
responses_metadata,
|
||
window_number,
|
||
context_window_id,
|
||
)
|
||
}
|
||
|
||
pub(crate) fn with_window_and_fork_metadata(
|
||
&self,
|
||
turn_context: &TurnContext,
|
||
responses_metadata: CodexResponsesMetadata,
|
||
window_number: u64,
|
||
context_window_id: uuid::Uuid,
|
||
) -> CodexResponsesMetadata {
|
||
CodexResponsesMetadata {
|
||
history_initialization: Some(self.history_initialization),
|
||
window_number: Some(window_number),
|
||
context_window_id: Some(context_window_id),
|
||
mcp_attribution: Some(self.services.executed_tool_calls.mcp_attribution_snapshot()),
|
||
analytics_enabled: Some(self.services.analytics_events_client.is_enabled()),
|
||
history_ingest_requested: turn_context
|
||
.config
|
||
.token_budget
|
||
.as_ref()
|
||
.is_some_and(|config| config.use_history_notes_extension)
|
||
.then_some(true),
|
||
forked_from_ordinal_exclusive: self
|
||
.forked_from_ordinal_exclusive
|
||
.filter(|_| responses_metadata.forked_from_thread_id.is_some()),
|
||
..responses_metadata
|
||
}
|
||
}
|
||
|
||
#[instrument(name = "session_init", level = "info", skip_all)]
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub(crate) async fn new(
|
||
startup: Option<Arc<super::startup::SessionStartup>>,
|
||
mut session_configuration: SessionConfiguration,
|
||
environment_selections: &[TurnEnvironmentSelection],
|
||
config: Arc<Config>,
|
||
mut instructions: SessionInstructions,
|
||
installation_id: String,
|
||
auth_manager: Arc<AuthManager>,
|
||
models_manager: SharedModelsManager,
|
||
git_root_discovery: Arc<GitRootDiscovery>,
|
||
model_info: ModelInfo,
|
||
exec_policy: Arc<ExecPolicyManager>,
|
||
tx_event: Sender<Event>,
|
||
agent_status: watch::Sender<AgentStatus>,
|
||
mut initial_history: InitialHistory,
|
||
history_initialization: HistoryInitialization,
|
||
fork_persistence: ForkPersistence,
|
||
session_source: SessionSource,
|
||
skills_service: Arc<HostSkillsService>,
|
||
plugins_manager: Arc<PluginsManager>,
|
||
mcp_manager: Arc<McpManager>,
|
||
code_mode_session_provider: Arc<dyn codex_code_mode::CodeModeSessionProvider>,
|
||
extensions: Arc<codex_extension_api::ExtensionRegistry<crate::config::Config>>,
|
||
mut thread_extension_init: ExtensionDataInit,
|
||
client_mcp_extensions: ClientMcpExtensions,
|
||
agent_control: AgentControlInit,
|
||
reserved_thread_id: Option<ThreadId>,
|
||
environment_manager: Arc<EnvironmentManager>,
|
||
inherited_environments: Option<TurnEnvironmentSnapshot>,
|
||
analytics_events_client: Option<AnalyticsEventsClient>,
|
||
image_store: Arc<dyn AttachmentStore>,
|
||
thread_store: Arc<dyn ThreadStore>,
|
||
parent_rollout_thread_trace: ThreadTraceContext,
|
||
attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
||
external_time_provider: Option<Arc<dyn TimeProvider>>,
|
||
multi_agent_version: Option<MultiAgentVersion>,
|
||
git_enrichment_policy: GitEnrichmentPolicy,
|
||
windows_sandbox_proxy_settings_mode: codex_sandboxing::WindowsSandboxProxySettingsMode,
|
||
) -> anyhow::Result<Arc<Self>> {
|
||
debug!(
|
||
"Configuring session: model={}; provider={:?}",
|
||
session_configuration
|
||
.step_settings
|
||
.collaboration_mode
|
||
.model(),
|
||
session_configuration.provider
|
||
);
|
||
let base_instructions_provenance = if config.base_instructions.is_some() {
|
||
Some(
|
||
config
|
||
.base_instructions_provenance
|
||
.clone()
|
||
.unwrap_or(BaseInstructionsProvenance::Custom),
|
||
)
|
||
} else if let Some(inherited_base_instructions) = initial_history.get_base_instructions() {
|
||
let BaseInstructions { text, provenance } = inherited_base_instructions;
|
||
provenance.or_else(|| {
|
||
(text == render_model_instructions(&model_info)).then(|| {
|
||
BaseInstructionsProvenance::Model {
|
||
model: model_info.slug.clone(),
|
||
}
|
||
})
|
||
})
|
||
} else {
|
||
Some(BaseInstructionsProvenance::Model {
|
||
model: model_info.slug.clone(),
|
||
})
|
||
};
|
||
let forked_from_id = session_configuration
|
||
.forked_from_thread_id
|
||
.or_else(|| initial_history.forked_from_id());
|
||
session_configuration.forked_from_thread_id = forked_from_id;
|
||
let forked_from_ordinal_exclusive = match &fork_persistence {
|
||
ForkPersistence::Referenced { history_base, .. } => {
|
||
history_base.map(|position| position.end_ordinal_exclusive)
|
||
}
|
||
ForkPersistence::Copied | ForkPersistence::CopiedDeferred => match &initial_history {
|
||
InitialHistory::Resumed(resumed) => {
|
||
// Both local and CCA thread stores place the resumed thread's
|
||
// canonical SessionMeta first. Never inspect inherited metadata:
|
||
// an ancestor's history_base describes a different fork boundary.
|
||
resumed.history.first().and_then(|item| match item {
|
||
RolloutItem::SessionMeta(meta)
|
||
if meta.meta.id == resumed.conversation_id =>
|
||
{
|
||
codex_rollout::forked_from_ordinal_exclusive(
|
||
&meta.meta,
|
||
resumed.rollout_path.as_deref(),
|
||
)
|
||
}
|
||
_ => None,
|
||
})
|
||
}
|
||
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => None,
|
||
},
|
||
}
|
||
.filter(|_| forked_from_id.is_some());
|
||
let parent_thread_id = session_configuration
|
||
.parent_thread_id
|
||
.or_else(|| initial_history.get_resumed_parent_thread_id());
|
||
session_configuration.parent_thread_id = parent_thread_id;
|
||
let is_paginated_subagent = matches!(
|
||
session_configuration.history_mode,
|
||
ThreadHistoryMode::Paginated
|
||
) && matches!(
|
||
session_configuration.thread_source.as_ref(),
|
||
Some(ThreadSource::Subagent | ThreadSource::GuardianReview)
|
||
);
|
||
if let InitialHistory::Forked(items) = &mut initial_history {
|
||
Self::assign_missing_rollout_response_item_ids(items);
|
||
}
|
||
let multi_agent_version = multi_agent_version.map(OnceLock::from).unwrap_or_default();
|
||
let initial_multi_agent_version = multi_agent_version.get().copied();
|
||
|
||
let thread_id = match (&initial_history, reserved_thread_id) {
|
||
(
|
||
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_),
|
||
Some(thread_id),
|
||
) => thread_id,
|
||
(InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_), None) => {
|
||
agent_control.runtime().generate_thread_id()
|
||
}
|
||
(InitialHistory::Resumed(resumed_history), None) => resumed_history.conversation_id,
|
||
(InitialHistory::Resumed(_), Some(_)) => {
|
||
return Err(anyhow::anyhow!(
|
||
"reserved thread ID cannot be used when resuming a thread"
|
||
));
|
||
}
|
||
};
|
||
let isolation = thread_extension_init
|
||
.get::<codex_extension_api::SessionIsolation>()
|
||
.map(|policy| *policy)
|
||
.unwrap_or_default();
|
||
if !session_configuration.session_source.is_non_root_agent()
|
||
&& isolation != codex_extension_api::SessionIsolation::Isolated
|
||
{
|
||
instructions.thread_provider = agent_control
|
||
.runtime()
|
||
.root_thread_instructions_provider(thread_id, instructions.thread_provider);
|
||
}
|
||
// Ephemeral forks reuse cache routing, without sharing storage or lifecycle identity.
|
||
let fork_cache_key = match &initial_history {
|
||
InitialHistory::Forked(items)
|
||
if config.ephemeral
|
||
&& !session_configuration.session_source.is_non_root_agent() =>
|
||
{
|
||
items.iter().find_map(|item| match item {
|
||
RolloutItem::SessionMeta(meta) => Some(meta.meta.session_id.to_string()),
|
||
_ => None,
|
||
})
|
||
}
|
||
InitialHistory::New
|
||
| InitialHistory::Cleared
|
||
| InitialHistory::Resumed(_)
|
||
| InitialHistory::Forked(_) => None,
|
||
};
|
||
let resumed_session_id = match &initial_history {
|
||
InitialHistory::Resumed(resumed) => {
|
||
resumed.history.iter().find_map(|item| match item {
|
||
RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.session_id),
|
||
_ => None,
|
||
})
|
||
}
|
||
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => None,
|
||
};
|
||
// Legacy subagent rollouts synthesized session_id from their own thread ID.
|
||
let resumed_session_id = resumed_session_id.filter(|session_id| {
|
||
!session_configuration.session_source.is_non_root_agent()
|
||
|| *session_id != SessionId::from(thread_id)
|
||
});
|
||
// session_id is equal to the root thread's ID.
|
||
let session_id = resumed_session_id.unwrap_or_else(|| {
|
||
if session_configuration.session_source.is_non_root_agent() {
|
||
agent_control.control().identity()
|
||
} else {
|
||
SessionId::from(thread_id)
|
||
}
|
||
});
|
||
let initial_auto_compact_window_ids = AutoCompactWindowIds::new_initial();
|
||
let restore_child_window = matches!(&initial_history, InitialHistory::Forked(_))
|
||
&& session_configuration.session_source.is_non_root_agent()
|
||
&& config.features.enabled(Feature::TokenBudget);
|
||
if restore_child_window && let InitialHistory::Forked(items) = &mut initial_history {
|
||
let child_window_id = initial_auto_compact_window_ids.window_id.to_string();
|
||
for item in items {
|
||
if let RolloutItem::Compacted(checkpoint) = item {
|
||
checkpoint.window_number = Some(0);
|
||
checkpoint.first_window_id = Some(child_window_id.clone());
|
||
checkpoint.previous_window_id = None;
|
||
checkpoint.window_id = Some(child_window_id.clone());
|
||
}
|
||
}
|
||
}
|
||
let (agent_control, local_agent_runtime): (Arc<dyn AgentControl>, _) = match agent_control {
|
||
AgentControlInit::Local(control) => {
|
||
let control = control.with_session_id(
|
||
session_id,
|
||
config
|
||
.effective_agent_max_threads(MultiAgentVersion::V2)
|
||
.unwrap_or(usize::MAX),
|
||
);
|
||
if parent_thread_id.is_none() {
|
||
control.propagate_config_update(AgentConfigUpdate::ServiceTier(
|
||
session_configuration
|
||
.step_settings
|
||
.service_tier
|
||
.clone()
|
||
.or_else(|| config.service_tier.clone()),
|
||
));
|
||
}
|
||
let runtime = control.runtime.clone();
|
||
(Arc::new(control), runtime)
|
||
}
|
||
AgentControlInit::Provided { control, runtime } => {
|
||
let controller_id = control.identity();
|
||
if controller_id != session_id {
|
||
return Err(CodexErr::InvalidRequest(format!(
|
||
"agent controller identity {controller_id} does not match session identity {session_id}"
|
||
))
|
||
.into());
|
||
}
|
||
(control, runtime)
|
||
}
|
||
};
|
||
let time_provider = crate::current_time::resolve_time_provider(
|
||
config.current_time_reminder.as_ref(),
|
||
external_time_provider,
|
||
)?;
|
||
let selected_capability_roots = thread_extension_init
|
||
.get::<Vec<SelectedCapabilityRoot>>()
|
||
.map(|roots| roots.as_ref().clone())
|
||
.unwrap_or_default();
|
||
thread_extension_init.insert(codex_extension_api::ThreadOriginator(
|
||
session_configuration.originator.clone(),
|
||
));
|
||
// Publish the already resolved model before extensions make startup decisions.
|
||
// Turn construction refreshes this attachment when the selected model changes.
|
||
thread_extension_init.insert(model_info);
|
||
let tool_policy = thread_extension_init
|
||
.get::<codex_extension_api::ToolPolicy>()
|
||
.unwrap_or_else(|| {
|
||
// Older reviewer rollouts predate the explicit startup policy.
|
||
if crate::guardian::is_basic_session_source(&session_configuration.session_source) {
|
||
Arc::new(codex_guardian_reviewer::reviewer_tool_policy())
|
||
} else {
|
||
Arc::default()
|
||
}
|
||
});
|
||
let mcp_thread_init = thread_extension_init.clone();
|
||
let thread_extension_data = codex_extension_api::ExtensionData::new_with_init(
|
||
thread_id.to_string(),
|
||
thread_extension_init,
|
||
);
|
||
thread_extension_data.insert(crate::context::GuardianReviewEvidence::default());
|
||
// Kick off independent async setup tasks in parallel to reduce startup latency.
|
||
//
|
||
// - initialize thread persistence with new or resumed session info
|
||
// - perform default shell discovery
|
||
// - load history metadata (skipped for subagents)
|
||
let persistence_auth = futures::FutureExt::shared(auth_manager.auth());
|
||
let mcp_auth = persistence_auth.clone();
|
||
let thread_persistence_fut = async {
|
||
if config.ephemeral {
|
||
Ok::<_, anyhow::Error>((None, LiveThreadInitGuard::new(/*live_thread*/ None), None))
|
||
} else {
|
||
let mut local_guard = LiveThreadInitGuard::default();
|
||
let mut resume_context = None;
|
||
let mut managed_guard = match &startup {
|
||
Some(startup) => Some(startup.persistence.lock().await),
|
||
None => None,
|
||
};
|
||
let guard = managed_guard.as_deref_mut().unwrap_or(&mut local_guard);
|
||
let live_thread = match &initial_history {
|
||
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => {
|
||
let auth = persistence_auth.await;
|
||
let params = CreateThreadParams {
|
||
creator_user_id: auth.as_ref().and_then(CodexAuth::get_chatgpt_user_id),
|
||
creator_account_id: auth.as_ref().and_then(CodexAuth::get_account_id),
|
||
session_id,
|
||
thread_id,
|
||
extra_config: config.extra_config.clone(),
|
||
forked_from_id,
|
||
parent_thread_id,
|
||
source: session_source,
|
||
thread_source: session_configuration.thread_source.clone(),
|
||
originator: session_configuration.originator.clone(),
|
||
base_instructions: BaseInstructions {
|
||
text: session_configuration.base_instructions.clone(),
|
||
provenance: base_instructions_provenance.clone(),
|
||
},
|
||
dynamic_tools: session_configuration.dynamic_tools.clone(),
|
||
selected_capability_roots: selected_capability_roots.clone(),
|
||
multi_agent_version: initial_multi_agent_version,
|
||
history_mode: session_configuration.history_mode,
|
||
history_base: match &fork_persistence {
|
||
ForkPersistence::Copied | ForkPersistence::CopiedDeferred => None,
|
||
ForkPersistence::Referenced { history_base, .. } => *history_base,
|
||
},
|
||
subagent_history_start_ordinal: None,
|
||
initial_window_id: initial_auto_compact_window_ids
|
||
.window_id
|
||
.to_string(),
|
||
runtime_workspace_roots: Some(config.workspace_roots.clone()),
|
||
metadata: ThreadPersistenceMetadata {
|
||
cwd: Some(config.cwd.to_path_buf()),
|
||
model_provider: config.model_provider_id.clone(),
|
||
memory_mode: if config.memories.generate_memories {
|
||
ThreadMemoryMode::Enabled
|
||
} else {
|
||
ThreadMemoryMode::Disabled
|
||
},
|
||
},
|
||
};
|
||
if is_paginated_subagent
|
||
&& matches!(
|
||
&fork_persistence,
|
||
ForkPersistence::Copied | ForkPersistence::CopiedDeferred
|
||
)
|
||
&& let InitialHistory::Forked(items) = &initial_history
|
||
{
|
||
LiveThread::create_with_inherited_model_context(
|
||
Arc::clone(&thread_store),
|
||
params,
|
||
items,
|
||
guard,
|
||
)
|
||
.await?
|
||
} else {
|
||
guard
|
||
.acquire(LiveThread::create(Arc::clone(&thread_store), params))
|
||
.await?
|
||
}
|
||
}
|
||
InitialHistory::Resumed(resumed_history) => {
|
||
let params = ResumeThreadParams {
|
||
history_revision: resumed_history.history_revision.clone(),
|
||
thread_id: resumed_history.conversation_id,
|
||
rollout_path: resumed_history.rollout_path.clone(),
|
||
history: Some(resumed_history.history.clone()),
|
||
include_archived: true,
|
||
metadata: ThreadPersistenceMetadata {
|
||
cwd: Some(config.cwd.to_path_buf()),
|
||
model_provider: config.model_provider_id.clone(),
|
||
memory_mode: if config.memories.generate_memories {
|
||
ThreadMemoryMode::Enabled
|
||
} else {
|
||
ThreadMemoryMode::Disabled
|
||
},
|
||
},
|
||
};
|
||
let store = Arc::clone(&thread_store);
|
||
let history_mode = session_configuration.history_mode;
|
||
let (context_tx, context_rx) = tokio::sync::oneshot::channel();
|
||
let live_thread = guard
|
||
.acquire(async move {
|
||
let (live_thread, history) =
|
||
LiveThread::resume(store, history_mode, params).await?;
|
||
// Cancellation still leaves the writer with the acquisition guard.
|
||
let _ = context_tx.send(history);
|
||
Ok(live_thread)
|
||
})
|
||
.await?;
|
||
resume_context = Some(context_rx.await?);
|
||
live_thread
|
||
}
|
||
};
|
||
Ok((Some(live_thread), local_guard, resume_context))
|
||
}
|
||
}
|
||
.instrument(info_span!(
|
||
"session_init.thread_persistence",
|
||
otel.name = "session_init.thread_persistence",
|
||
session_init.ephemeral = config.ephemeral,
|
||
));
|
||
let state_db_fut = async {
|
||
if config.ephemeral {
|
||
None
|
||
} else if let Some(local_store) =
|
||
thread_store.as_any().downcast_ref::<LocalThreadStore>()
|
||
{
|
||
local_store.state_db().await
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
.instrument(info_span!(
|
||
"session_init.state_db",
|
||
otel.name = "session_init.state_db",
|
||
session_init.ephemeral = config.ephemeral,
|
||
));
|
||
|
||
let mut mcp_auth_changes = auth_manager.auth_change_receiver();
|
||
let plugins_manager_for_prewarm = Arc::clone(&plugins_manager);
|
||
let config_for_mcp = Arc::clone(&config);
|
||
let mcp_manager_for_mcp = Arc::clone(&mcp_manager);
|
||
let mcp_thread_init_for_startup = &mcp_thread_init;
|
||
let thread_extension_data_for_mcp = &thread_extension_data;
|
||
let mcp_originator = session_configuration.originator.clone();
|
||
let mcp_session_source = session_configuration.session_source.clone();
|
||
let mcp_disabled_plugin_ids = session_configuration.disabled_plugin_ids.clone();
|
||
let mcp_runtime_cwd = environment_selections
|
||
.first()
|
||
.and_then(|environment| environment.cwd.to_abs_path().ok())
|
||
.map(|cwd| cwd.to_path_buf())
|
||
.unwrap_or_else(|| session_configuration.cwd().to_path_buf());
|
||
let auth_and_mcp_fut = async move {
|
||
let auth = mcp_auth.await;
|
||
if config_for_mcp.features.plugin_recommendations_enabled() {
|
||
let plugins_config = config_for_mcp.plugins_config_input();
|
||
let auth_for_prewarm = auth.clone();
|
||
// Fetch the catalog while MCP and plugin/skill initialization continue.
|
||
// Context construction still handles filtering and prompt insertion.
|
||
tokio::spawn(async move {
|
||
plugins_manager_for_prewarm
|
||
.recommended_plugins_mode_for_config(
|
||
&plugins_config,
|
||
auth_for_prewarm.as_ref(),
|
||
)
|
||
.await;
|
||
});
|
||
}
|
||
let mcp_projection = mcp_manager_for_mcp
|
||
.runtime_config_for_step(
|
||
&config_for_mcp,
|
||
mcp_thread_init_for_startup,
|
||
thread_extension_data_for_mcp,
|
||
McpThreadIdentity {
|
||
auth_changed: false,
|
||
session_source: &mcp_session_source,
|
||
originator: &mcp_originator,
|
||
disabled_plugin_ids: &mcp_disabled_plugin_ids,
|
||
environments: McpEnvironmentScope::Selected(environment_selections),
|
||
},
|
||
/*ready_selected_capability_roots*/ &[],
|
||
/*executor_capability_discovery*/ None,
|
||
)
|
||
.await;
|
||
(auth, mcp_projection)
|
||
}
|
||
.instrument(info_span!(
|
||
"session_init.auth_mcp",
|
||
otel.name = "session_init.auth_mcp",
|
||
));
|
||
|
||
// Join all independent futures.
|
||
let (thread_persistence_result, state_db_ctx, (auth, mcp_projection)) =
|
||
tokio::join!(thread_persistence_fut, state_db_fut, auth_and_mcp_fut);
|
||
|
||
let (live_thread, mut live_thread_init, resume_context) = thread_persistence_result
|
||
.map_err(|e| {
|
||
error!("failed to initialize thread persistence: {e:#}");
|
||
e
|
||
})?;
|
||
let session_result: anyhow::Result<Arc<Self>> = async {
|
||
if let InitialHistory::Resumed(resumed) = &mut initial_history
|
||
&& let Some(history) = resume_context
|
||
{
|
||
resumed.history = history;
|
||
resumed.history_revision = None;
|
||
}
|
||
let rollout_path = if let Some(live_thread) = live_thread.as_ref() {
|
||
live_thread.local_rollout_path().await?
|
||
} else {
|
||
None
|
||
};
|
||
let trace_agent_path = session_configuration
|
||
.session_source
|
||
.get_agent_path()
|
||
.unwrap_or_else(codex_protocol::AgentPath::root);
|
||
let trace_task_name =
|
||
(!trace_agent_path.is_root()).then(|| trace_agent_path.name().to_string());
|
||
let trace_metadata = ThreadStartedTraceMetadata {
|
||
thread_id: thread_id.to_string(),
|
||
agent_path: trace_agent_path.to_string(),
|
||
task_name: trace_task_name,
|
||
nickname: session_configuration.session_source.get_nickname(),
|
||
agent_role: session_configuration.session_source.get_agent_role(),
|
||
session_source: session_configuration.session_source.clone(),
|
||
cwd: session_configuration.cwd().to_path_buf(),
|
||
rollout_path: rollout_path.clone(),
|
||
model: session_configuration
|
||
.step_settings
|
||
.collaboration_mode
|
||
.model()
|
||
.to_string(),
|
||
provider_name: config.model_provider_id.clone(),
|
||
approval_policy: session_configuration
|
||
.step_settings
|
||
.approval_policy
|
||
.value()
|
||
.to_string(),
|
||
sandbox_policy: format!(
|
||
"{:?}",
|
||
session_configuration.sandbox_policy(environment_selections)
|
||
),
|
||
};
|
||
let rollout_thread_trace = if matches!(
|
||
session_configuration.session_source,
|
||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. })
|
||
) {
|
||
// Spawned child threads are part of their root rollout tree. If the
|
||
// parent had no trace bundle, do not create an orphan child bundle
|
||
// that looks like an independent rollout.
|
||
parent_rollout_thread_trace.start_child_thread_trace_or_disabled(trace_metadata)
|
||
} else {
|
||
ThreadTraceContext::start_root_or_disabled(trace_metadata)
|
||
};
|
||
|
||
let mut post_session_configured_events = Vec::<Event>::new();
|
||
|
||
for usage in config.features.legacy_feature_usages() {
|
||
post_session_configured_events.push(Event {
|
||
id: INITIAL_SUBMIT_ID.to_owned(),
|
||
msg: EventMsg::DeprecationNotice(DeprecationNoticeEvent {
|
||
summary: usage.summary.clone(),
|
||
details: usage.details.clone(),
|
||
}),
|
||
});
|
||
}
|
||
for message in &config.startup_warnings {
|
||
post_session_configured_events.push(Event {
|
||
id: "".to_owned(),
|
||
msg: EventMsg::Warning(WarningEvent {
|
||
message: message.clone(),
|
||
}),
|
||
});
|
||
}
|
||
let effective_config = config.config_layer_stack.effective_config();
|
||
let config_path = config.codex_home.join(CONFIG_TOML_FILE);
|
||
if let Some(event) = unstable_features_warning_event(
|
||
effective_config
|
||
.get("features")
|
||
.and_then(TomlValue::as_table),
|
||
config.suppress_unstable_features_warning,
|
||
&config.features,
|
||
&config_path.display().to_string(),
|
||
) {
|
||
post_session_configured_events.push(event);
|
||
}
|
||
let telemetry_auth = auth.as_ref();
|
||
let auth_mode = telemetry_auth
|
||
.map(CodexAuth::auth_mode)
|
||
.map(TelemetryAuthMode::from);
|
||
let account_id = telemetry_auth.and_then(CodexAuth::get_account_id);
|
||
let account_email = telemetry_auth.and_then(CodexAuth::get_account_email);
|
||
let originator = session_configuration.originator.clone();
|
||
let terminal_type = user_agent();
|
||
let session_model = session_configuration
|
||
.step_settings
|
||
.collaboration_mode
|
||
.model()
|
||
.to_string();
|
||
let auth_env_telemetry = collect_auth_env_telemetry(
|
||
session_configuration.provider.info(),
|
||
auth_manager.codex_api_key_env_enabled(),
|
||
);
|
||
let mut session_telemetry = SessionTelemetry::new(
|
||
thread_id,
|
||
session_model.as_str(),
|
||
session_model.as_str(),
|
||
account_id.clone(),
|
||
account_email.clone(),
|
||
auth_mode,
|
||
originator.clone(),
|
||
config.otel.log_user_prompt,
|
||
terminal_type.clone(),
|
||
session_configuration.session_source.clone(),
|
||
)
|
||
.with_auth_env(auth_env_telemetry.to_otel_metadata())
|
||
.with_user_id(telemetry_auth.and_then(CodexAuth::get_chatgpt_user_id))
|
||
.with_tool_result_log_config(config.otel.tool_result);
|
||
if let Some(metrics) = thread_extension_data.get::<codex_otel::MetricsClient>() {
|
||
session_telemetry = session_telemetry.with_metrics(metrics.as_ref().clone());
|
||
}
|
||
if let Some(service_name) = session_configuration.metrics_service_name.as_deref() {
|
||
session_telemetry = session_telemetry.with_metrics_service_name(service_name);
|
||
}
|
||
let network_proxy_audit_metadata = NetworkProxyAuditMetadata {
|
||
conversation_id: Some(thread_id.to_string()),
|
||
app_version: Some(env!("CARGO_PKG_VERSION").to_string()),
|
||
user_account_id: account_id,
|
||
auth_mode: auth_mode.map(|mode| mode.to_string()),
|
||
originator: Some(originator),
|
||
user_email: account_email,
|
||
terminal_type: Some(terminal_type),
|
||
model: Some(session_model.clone()),
|
||
slug: Some(session_model),
|
||
};
|
||
crate::config::emit_session_start_metrics(config.as_ref(), &session_telemetry);
|
||
let is_worktree = session_configuration
|
||
.cwd()
|
||
.canonicalize()
|
||
.ok()
|
||
.and_then(|cwd| {
|
||
codex_git_utils::repository_identity(&cwd).and_then(|_| {
|
||
get_git_repo_root(&cwd).map(|root| root.join(".git").is_file())
|
||
})
|
||
});
|
||
let is_worktree_tag = match is_worktree {
|
||
Some(true) => "true",
|
||
Some(false) => "false",
|
||
None => "unknown",
|
||
};
|
||
let is_git_tag = if get_git_repo_root(session_configuration.cwd()).is_some() {
|
||
"true"
|
||
} else {
|
||
"false"
|
||
};
|
||
session_telemetry.counter(
|
||
THREAD_STARTED_METRIC,
|
||
/*inc*/ 1,
|
||
&[("is_git", is_git_tag), ("is_worktree", is_worktree_tag)],
|
||
);
|
||
|
||
let mcp_server_names =
|
||
codex_mcp::effective_mcp_servers(&mcp_projection.config, auth.as_ref())
|
||
.into_iter()
|
||
.filter_map(|(name, server)| server.enabled().then_some(name))
|
||
.collect::<Vec<_>>();
|
||
session_telemetry.conversation_starts(
|
||
config.model_provider.name.as_str(),
|
||
session_configuration
|
||
.step_settings
|
||
.collaboration_mode
|
||
.reasoning_effort(),
|
||
config
|
||
.model_reasoning_summary
|
||
.unwrap_or(ReasoningSummaryConfig::Auto),
|
||
config.model_context_window,
|
||
config.model_auto_compact_token_limit,
|
||
config.permissions.approval_policy.value(),
|
||
config
|
||
.permissions
|
||
.legacy_sandbox_policy(session_configuration.cwd().as_path()),
|
||
mcp_server_names.iter().map(String::as_str).collect(),
|
||
);
|
||
|
||
let default_shell = session_configuration
|
||
.user_shell_override
|
||
.clone()
|
||
.unwrap_or_else(shell::default_user_shell);
|
||
let credential_broker_available = (config.features.enabled(Feature::NetworkProxy)
|
||
|| config.features.enabled(Feature::CredentialMasking))
|
||
&& config
|
||
.config_layer_stack
|
||
.requirements()
|
||
.network
|
||
.as_ref()
|
||
.is_none_or(|network| network.value.enabled != Some(false));
|
||
let credential_broker_configured = credential_broker_available
|
||
&& (config.features.enabled(Feature::CredentialMasking)
|
||
|| effective_config
|
||
.get("features")
|
||
.and_then(|features| features.get("network_proxy"))
|
||
.and_then(|network_proxy| network_proxy.get("credential_broker"))
|
||
.and_then(TomlValue::as_bool)
|
||
.unwrap_or(false));
|
||
let credential_broker_active = credential_broker_configured
|
||
&& config
|
||
.permissions
|
||
.network
|
||
.as_ref()
|
||
.is_some_and(crate::config::NetworkProxySpec::credential_broker_enabled);
|
||
let prefer_executor_shell_snapshots = config.features.enabled(Feature::ShellSnapshotV2)
|
||
&& config.features.enabled(Feature::ShellTool)
|
||
&& config.features.enabled(Feature::UnifiedExec);
|
||
let use_executor_shell_snapshots =
|
||
prefer_executor_shell_snapshots && !credential_broker_active;
|
||
let shell_snapshot = if config.features.enabled(Feature::ShellSnapshot)
|
||
&& (!use_executor_shell_snapshots || credential_broker_available)
|
||
{
|
||
let snapshot_credential_broker = credential_broker_available.then(|| {
|
||
let state = if credential_broker_active {
|
||
SnapshotCredentialBrokerState::Starting
|
||
} else {
|
||
SnapshotCredentialBrokerState::Inactive
|
||
};
|
||
watch::channel(state).0
|
||
});
|
||
ShellSnapshot::new(
|
||
config.codex_home.clone(),
|
||
thread_id,
|
||
session_telemetry.clone(),
|
||
state_db_ctx.clone(),
|
||
snapshot_credential_broker,
|
||
prefer_executor_shell_snapshots,
|
||
)
|
||
} else {
|
||
ShellSnapshot::disabled()
|
||
};
|
||
let inherited_environments = inherited_environments.unwrap_or_default();
|
||
let turn_environments = Arc::new(ThreadEnvironments::new(
|
||
environment_manager,
|
||
default_shell.clone(),
|
||
ThreadEnvironmentDefaults::new(
|
||
session_configuration.inferred_environment_config(),
|
||
session_configuration.windows_sandbox_type,
|
||
),
|
||
shell_snapshot,
|
||
inherited_environments.clone(),
|
||
config.features.enabled(Feature::DeferredExecutor),
|
||
));
|
||
turn_environments.update_selections(environment_selections);
|
||
session_configuration.environments = turn_environments.selections();
|
||
let resolved_environments = turn_environments.snapshot().await;
|
||
let agents_md_manager = Arc::new(AgentsMdManager::new(instructions));
|
||
let plugin_skill_warmup = warm_plugins_and_skills_for_session_init(
|
||
Arc::clone(&config),
|
||
Arc::clone(&plugins_manager),
|
||
Arc::clone(&skills_service),
|
||
&resolved_environments,
|
||
extensions.as_ref(),
|
||
&session_configuration.session_source,
|
||
)
|
||
.instrument(info_span!(
|
||
"session_init.plugin_skill_warmup",
|
||
otel.name = "session_init.plugin_skill_warmup",
|
||
));
|
||
let thread_name_lookup = async {
|
||
if config.ephemeral && matches!(&initial_history, InitialHistory::Forked(_)) {
|
||
None
|
||
} else {
|
||
thread_title_from_thread_store(live_thread.as_ref(), &thread_store, thread_id)
|
||
.await
|
||
}
|
||
}
|
||
.instrument(info_span!(
|
||
"session_init.thread_name_lookup",
|
||
otel.name = "session_init.thread_name_lookup",
|
||
));
|
||
let (instruction_refresh, plugin_skill_errors, thread_name) = tokio::join!(
|
||
agents_md_manager.refresh(config.as_ref(), &resolved_environments),
|
||
plugin_skill_warmup,
|
||
thread_name_lookup,
|
||
);
|
||
let (agents_md_result, instruction_warnings) = instruction_refresh;
|
||
// TODO(anp): Present AGENTS.md discovery errors more clearly to the user.
|
||
agents_md_result?;
|
||
post_session_configured_events.extend(instruction_warnings.into_iter().map(
|
||
|message| Event {
|
||
id: INITIAL_SUBMIT_ID.to_owned(),
|
||
msg: EventMsg::Warning(WarningEvent { message }),
|
||
},
|
||
));
|
||
for err in &plugin_skill_errors {
|
||
error!(
|
||
"failed to load skill {}: {}",
|
||
err.path.display(),
|
||
err.message
|
||
);
|
||
}
|
||
session_configuration.thread_name = thread_name.clone();
|
||
let mut state = SessionState::new_with_auto_compact_window_ids(
|
||
session_configuration.clone(),
|
||
initial_auto_compact_window_ids,
|
||
ContextManager::for_session(
|
||
&session_configuration.session_source,
|
||
&config.features,
|
||
),
|
||
);
|
||
state.base_instructions_provenance = base_instructions_provenance.clone();
|
||
// Restore a provided effort baseline before startup prewarm can establish a new one.
|
||
if let Some(pin) = thread_extension_data.remove::<ReasoningEffortPin>() {
|
||
state.reasoning_effort_pin = pin.as_ref().clone();
|
||
}
|
||
state.active_disabled_plugin_ids = session_configuration.disabled_plugin_ids.clone();
|
||
let managed_network_requirements_configured = config
|
||
.config_layer_stack
|
||
.requirements_toml()
|
||
.network
|
||
.is_some();
|
||
let managed_network_requirements_enabled =
|
||
config.managed_network_requirements_enabled();
|
||
let network_approval = Arc::new(NetworkApprovalService::default());
|
||
// The managed proxy can call back into core for allowlist-miss decisions.
|
||
let network_policy_decider_session = if managed_network_requirements_configured {
|
||
config
|
||
.permissions
|
||
.network
|
||
.as_ref()
|
||
.map(|_| Arc::new(RwLock::new(std::sync::Weak::<Session>::new())))
|
||
} else {
|
||
None
|
||
};
|
||
let blocked_request_observer = config
|
||
.permissions
|
||
.network
|
||
.as_ref()
|
||
.map(|_| build_blocked_request_observer(Arc::clone(&network_approval)));
|
||
let network_policy_decider =
|
||
network_policy_decider_session
|
||
.as_ref()
|
||
.map(|network_policy_decider_session| {
|
||
build_network_policy_decider(
|
||
Arc::clone(&network_approval),
|
||
Arc::clone(network_policy_decider_session),
|
||
)
|
||
});
|
||
let (network_proxy, session_network_proxy) = if let Some(spec) = config
|
||
.permissions
|
||
.network
|
||
.as_ref()
|
||
.filter(|spec| spec.enabled())
|
||
{
|
||
let current_exec_policy = exec_policy.current();
|
||
let (network_proxy, session_network_proxy) = Self::start_managed_network_proxy(
|
||
spec,
|
||
current_exec_policy.as_ref(),
|
||
config.permissions.permission_profile(),
|
||
config.effective_local_windows_sandbox_type(),
|
||
network_policy_decider.as_ref().map(Arc::clone),
|
||
blocked_request_observer.as_ref().map(Arc::clone),
|
||
managed_network_requirements_configured,
|
||
network_proxy_audit_metadata.clone(),
|
||
)
|
||
.instrument(info_span!(
|
||
"session_init.network_proxy",
|
||
otel.name = "session_init.network_proxy",
|
||
session_init.managed_network_requirements_enabled =
|
||
managed_network_requirements_enabled,
|
||
))
|
||
.await?;
|
||
(Some(network_proxy), Some(session_network_proxy))
|
||
} else {
|
||
(None, None)
|
||
};
|
||
if let Some(network_proxy) = network_proxy.as_ref()
|
||
&& config
|
||
.permissions
|
||
.network
|
||
.as_ref()
|
||
.is_some_and(crate::config::NetworkProxySpec::credential_broker_enabled)
|
||
{
|
||
turn_environments.set_snapshot_credential_broker(
|
||
SnapshotCredentialBrokerState::Ready(network_proxy.proxy()),
|
||
);
|
||
}
|
||
|
||
// Hooks and extensions share one stable thread-owned MCP runtime handle.
|
||
let mcp_runtime = Arc::new(McpRuntime::empty(
|
||
mcp_projection.config.prefix_mcp_tool_names,
|
||
));
|
||
let hooks_config = build_hooks_config(
|
||
&config,
|
||
plugins_manager.as_ref(),
|
||
resolved_environments.single_local_environment(),
|
||
&session_configuration.disabled_plugin_ids,
|
||
)
|
||
.await;
|
||
let (hooks, async_hook_results) = Hooks::new(
|
||
hooks_config,
|
||
thread_id,
|
||
Arc::new(CoreHookMcpExecutor {
|
||
runtime: Arc::clone(&mcp_runtime),
|
||
thread_id,
|
||
}),
|
||
)?;
|
||
for warning in hooks.startup_warnings() {
|
||
post_session_configured_events.push(Event {
|
||
id: INITIAL_SUBMIT_ID.to_owned(),
|
||
msg: EventMsg::Warning(WarningEvent {
|
||
message: warning.clone(),
|
||
}),
|
||
});
|
||
}
|
||
|
||
let analytics_events_client = if config.analytics_enabled == Some(false) {
|
||
if let Some(client) = &analytics_events_client {
|
||
client.update_thread_product_sku(thread_id, ThreadProductUpdate::Clear);
|
||
}
|
||
AnalyticsEventsClient::disabled()
|
||
} else {
|
||
analytics_events_client.unwrap_or_else(|| {
|
||
AnalyticsEventsClient::new(
|
||
Arc::clone(&auth_manager),
|
||
config.chatgpt_base_url.trim_end_matches('/').to_string(),
|
||
config.analytics_enabled,
|
||
)
|
||
})
|
||
};
|
||
analytics_events_client.update_thread_product_sku(
|
||
thread_id,
|
||
match &config.apps_mcp_product_sku {
|
||
Some(product) => ThreadProductUpdate::Set(product.clone()),
|
||
None => ThreadProductUpdate::Clear,
|
||
},
|
||
);
|
||
for item in initial_history.get_rollout_items() {
|
||
match item {
|
||
RolloutItem::Compacted(compacted) => {
|
||
if let Some(checkpoint) = &compacted.mcp_resource_origins {
|
||
mcp_runtime.restore_resource_origin_checkpoint(checkpoint);
|
||
}
|
||
}
|
||
RolloutItem::EventMsg(event) => mcp_runtime.observe_event(event),
|
||
RolloutItem::SessionMeta(_)
|
||
| RolloutItem::ResponseItem(_)
|
||
| RolloutItem::InterAgentCommunication(_)
|
||
| RolloutItem::InterAgentCommunicationMetadata { .. }
|
||
| RolloutItem::TurnContext(_)
|
||
| RolloutItem::WorldState(_)
|
||
| RolloutItem::RealtimeItem(_)
|
||
| RolloutItem::TokenUsageRecord(_)
|
||
| RolloutItem::RetainedContext(_)
|
||
| RolloutItem::SecurityRiskScore(_) => {}
|
||
}
|
||
}
|
||
let session_extension_data =
|
||
codex_extension_api::ExtensionData::new(session_id.to_string());
|
||
session_extension_data.insert(analytics_events_client.clone());
|
||
session_extension_data.insert(session_telemetry.clone());
|
||
let mcp_resource_client = Arc::new(McpResourceClient::new(Arc::clone(&mcp_runtime)));
|
||
let extension_metrics =
|
||
extension_metrics::from_session_telemetry(session_telemetry.clone());
|
||
let workspace_routing =
|
||
thread_extension_data.get_or_init(|| config.workspace_routing_context());
|
||
for contributor in extensions.thread_lifecycle_contributors() {
|
||
contributor
|
||
.on_thread_start(codex_extension_api::ThreadStartInput {
|
||
config: config.as_ref(),
|
||
session_source: &session_configuration.session_source,
|
||
persistent_thread_state_available: state_db_ctx.is_some(),
|
||
environments: environment_selections,
|
||
mcp_resource_client: Some(Arc::clone(&mcp_resource_client)),
|
||
extension_metrics: Some(Arc::clone(&extension_metrics)),
|
||
session_store: &session_extension_data,
|
||
thread_store: &thread_extension_data,
|
||
})
|
||
.await;
|
||
}
|
||
|
||
let executed_tool_calls =
|
||
crate::state::ExecutedToolCalls::new(&config.features, &initial_history);
|
||
let codex_responses_headers =
|
||
thread_extension_data.get::<crate::CodexResponsesHeaders>();
|
||
// Ephemeral title requests use request-level effort even when managed settings
|
||
// enable overrides. The client-supplied tag selects cache behavior, not permissions.
|
||
let title_request = config.ephemeral
|
||
&& matches!(
|
||
session_configuration.thread_source.as_ref(),
|
||
Some(ThreadSource::Feature(feature)) if feature == "thread_title"
|
||
);
|
||
let reasoning_effort_override_enabled =
|
||
config.features.enabled(Feature::ReasoningEffortOverride) && !title_request;
|
||
let services = SessionServices {
|
||
// Start with an empty connection set. The initialized set is
|
||
// published after SessionConfigured so MCP events follow it.
|
||
mcp_runtime,
|
||
mcp_handler_cache: Default::default(),
|
||
unified_exec_manager: UnifiedExecProcessManager::new(
|
||
config.background_terminal_max_timeout,
|
||
),
|
||
elicitations: crate::elicitation::ElicitationService::new(),
|
||
analytics_events_client,
|
||
hooks: arc_swap::ArcSwap::from_pointee(hooks),
|
||
rollout_thread_trace,
|
||
user_shell: Arc::new(default_shell),
|
||
show_raw_agent_reasoning: config.show_raw_agent_reasoning,
|
||
exec_policy,
|
||
auth_manager: Arc::clone(&auth_manager),
|
||
openai_file_upload_client_pool: RouteAwareClientPool::new_without_request_logging(
|
||
config.http_client_factory(),
|
||
ClientRouteClass::Api,
|
||
)
|
||
.with_legacy_custom_ca_fallback(),
|
||
session_telemetry,
|
||
models_manager: Arc::clone(&models_manager),
|
||
git_root_discovery,
|
||
tool_approvals: Mutex::new(ApprovalStore::default()),
|
||
granted_permissions_by_environment_id: Arc::default(),
|
||
runtime_handle: tokio::runtime::Handle::current(),
|
||
skills_service,
|
||
agents_md_manager,
|
||
plugins_manager: Arc::clone(&plugins_manager),
|
||
mcp_manager: Arc::clone(&mcp_manager),
|
||
extensions: Arc::clone(&extensions),
|
||
// TODO(jif): extract session to share between sub-agents
|
||
session_extension_data,
|
||
thread_extension_data,
|
||
selected_capability_roots,
|
||
mcp_thread_init,
|
||
client_mcp_extensions,
|
||
local_agent_runtime,
|
||
agent_control,
|
||
network_proxy: arc_swap::ArcSwapOption::from(network_proxy.map(Arc::new)),
|
||
network_proxy_audit_metadata,
|
||
managed_network_requirements_configured,
|
||
network_approval: Arc::clone(&network_approval),
|
||
state_db: state_db_ctx.clone(),
|
||
live_thread: live_thread.clone(),
|
||
image_store,
|
||
thread_store: Arc::clone(&thread_store),
|
||
attestation_provider: attestation_provider.clone(),
|
||
time_provider,
|
||
model_client: ModelClient::new(
|
||
Some(Arc::clone(&auth_manager)),
|
||
if config.features.enabled(Feature::UseAgentIdentity) {
|
||
AgentIdentityAuthPolicy::ChatGptAuth
|
||
} else {
|
||
AgentIdentityAuthPolicy::JwtOnly
|
||
},
|
||
thread_id,
|
||
session_configuration.provider.info().clone(),
|
||
session_configuration.session_source.clone(),
|
||
session_configuration.originator.clone(),
|
||
config.model_verbosity,
|
||
config.features.enabled(Feature::ContentItemKinds),
|
||
reasoning_effort_override_enabled,
|
||
config.features.enabled(Feature::EnableRequestCompression),
|
||
config.features.enabled(Feature::RuntimeMetrics),
|
||
Self::build_model_client_beta_features_header(config.as_ref()),
|
||
/*concurrent_reasoning_summaries_enabled*/
|
||
config
|
||
.features
|
||
.enabled(Feature::ConcurrentReasoningSummaries),
|
||
config.features.enabled(Feature::OutputTokenReplay),
|
||
attestation_provider,
|
||
config.http_client_factory(),
|
||
workspace_routing.as_ref().clone(),
|
||
extensions.model_request_contributors().to_vec(),
|
||
)
|
||
.with_executed_tool_calls(executed_tool_calls.clone())
|
||
.with_restored_history(matches!(
|
||
&initial_history,
|
||
InitialHistory::Resumed(_) | InitialHistory::Forked(_)
|
||
))
|
||
.with_session_context(
|
||
crate::guardian::prompt_cache_key_override_for_review_session(
|
||
&session_configuration.session_source,
|
||
session_configuration.parent_thread_id,
|
||
)
|
||
.or(fork_cache_key),
|
||
tx_event.clone(),
|
||
codex_responses_headers,
|
||
crate::cyber_access_program::ApiKeyCyberAccessPrograms::from_config(&config),
|
||
),
|
||
executed_tool_calls: executed_tool_calls.clone(),
|
||
code_mode_service: crate::tools::code_mode::CodeModeService::new(
|
||
thread_id,
|
||
Arc::clone(&code_mode_session_provider),
|
||
&config.code_mode,
|
||
executed_tool_calls,
|
||
),
|
||
tool_search_handler_cache: Default::default(),
|
||
turn_environments: Arc::clone(&turn_environments),
|
||
};
|
||
let (mcp_prewarm_tx, mcp_prewarm_rx) = async_channel::bounded(1);
|
||
let sess = Arc::new(Session {
|
||
thread_id,
|
||
installation_id,
|
||
tx_event: tx_event.clone(),
|
||
agent_status,
|
||
state: Mutex::new(state),
|
||
thread_settings_persistence: Semaphore::new(/*permits*/ 1),
|
||
code_mode_message_tasks: CodeModeMessageTasks::default(),
|
||
managed_network_proxy_refresh_lock: Semaphore::new(/*permits*/ 1),
|
||
features: config.features.clone(),
|
||
isolation,
|
||
tool_policy,
|
||
windows_sandbox_proxy_settings_mode,
|
||
multi_agent_version,
|
||
mcp_refresh: McpRefresh::new(),
|
||
mcp_tool_approval_metadata: Default::default(),
|
||
mcp_elicitation_reviewer_handle: OnceLock::new(),
|
||
mcp_elicitation_lifecycle_handle: OnceLock::new(),
|
||
mcp_prewarm_tx,
|
||
mcp_prewarm_shutdown: CancellationToken::new(),
|
||
mcp_prewarm_task: std::sync::Mutex::new(None),
|
||
conversation: Arc::new(RealtimeConversationManager::new()),
|
||
realtime_history: (session_configuration.history_mode
|
||
== ThreadHistoryMode::Paginated
|
||
&& services.live_thread.is_some())
|
||
.then(|| Mutex::new(Default::default())),
|
||
active_turn: Mutex::new(None),
|
||
async_hook_results,
|
||
input_queue: InputQueue::with_controller(
|
||
thread_id,
|
||
Arc::clone(&services.agent_control),
|
||
),
|
||
services,
|
||
git_enrichment_policy,
|
||
fork_persistence,
|
||
history_initialization,
|
||
forked_from_ordinal_exclusive,
|
||
next_internal_sub_id: AtomicU64::new(0),
|
||
});
|
||
if let Some(startup) = &startup {
|
||
startup.set_session(Arc::clone(&sess));
|
||
}
|
||
if let Some(network_policy_decider_session) = network_policy_decider_session {
|
||
let mut guard = network_policy_decider_session.write().await;
|
||
*guard = Arc::downgrade(&sess);
|
||
}
|
||
// Dispatch the SessionConfiguredEvent first and then report any errors.
|
||
let thread_config =
|
||
session_configuration.thread_config_snapshot(turn_environments.selections());
|
||
let events = std::iter::once(Event {
|
||
id: INITIAL_SUBMIT_ID.to_owned(),
|
||
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
|
||
session_id,
|
||
thread_id,
|
||
cwd: thread_config.cwd().clone(),
|
||
forked_from_id: thread_config.forked_from_thread_id,
|
||
parent_thread_id: thread_config.parent_thread_id,
|
||
thread_source: thread_config.thread_source,
|
||
thread_name: session_configuration.thread_name.clone(),
|
||
model: thread_config.model,
|
||
model_provider_id: thread_config.model_provider_id,
|
||
service_tier: thread_config.service_tier,
|
||
approval_policy: thread_config.approval_policy,
|
||
approvals_reviewer: thread_config.approvals_reviewer,
|
||
network_proxy: session_network_proxy.filter(|_| {
|
||
Self::managed_network_proxy_active_for_permission_profile(
|
||
&thread_config.permission_profile,
|
||
)
|
||
}),
|
||
permission_profile: thread_config.permission_profile,
|
||
active_permission_profile: thread_config.active_permission_profile,
|
||
reasoning_effort: thread_config.reasoning_effort,
|
||
rollout_path,
|
||
}),
|
||
})
|
||
.chain(post_session_configured_events.into_iter());
|
||
for event in events {
|
||
sess.send_event_raw(event).await;
|
||
}
|
||
turn_environments.start_connection_event_forwarding(tx_event.clone());
|
||
|
||
let startup_auth_changed = mcp_auth_changes.has_changed().unwrap_or(false);
|
||
if startup_auth_changed {
|
||
mcp_auth_changes.mark_unchanged();
|
||
}
|
||
let latest_auth = sess.services.auth_manager.auth().await;
|
||
let resolved_environment_selections = resolved_environments.all_selections();
|
||
let mcp_projection = if startup_auth_changed
|
||
|| mcp_auth_changes.has_changed().unwrap_or(false)
|
||
|| resolved_environment_selections.as_slice() != environment_selections
|
||
{
|
||
sess.services
|
||
.mcp_manager
|
||
.runtime_config_for_step(
|
||
config.as_ref(),
|
||
&sess.services.mcp_thread_init,
|
||
&sess.services.thread_extension_data,
|
||
McpThreadIdentity {
|
||
auth_changed: !sess
|
||
.services
|
||
.mcp_runtime
|
||
.current_auth_matches(latest_auth.as_ref()),
|
||
session_source: &session_configuration.session_source,
|
||
originator: &session_configuration.originator,
|
||
disabled_plugin_ids: &session_configuration.disabled_plugin_ids,
|
||
environments: McpEnvironmentScope::Selected(
|
||
&resolved_environment_selections,
|
||
),
|
||
},
|
||
/*ready_selected_capability_roots*/ &[],
|
||
/*executor_capability_discovery*/ None,
|
||
)
|
||
.await
|
||
} else {
|
||
mcp_projection
|
||
};
|
||
sess.install_initial_mcp_runtime(
|
||
&session_configuration,
|
||
latest_auth,
|
||
mcp_projection,
|
||
&resolved_environments,
|
||
mcp_runtime_cwd,
|
||
)
|
||
.await?;
|
||
sess.start_mcp_prewarm_worker(mcp_prewarm_rx, mcp_auth_changes);
|
||
sess.follow_inherited_environment_configurations(
|
||
&inherited_environments,
|
||
&session_configuration.environments,
|
||
);
|
||
sess.schedule_startup_prewarm(super::startup_prewarm::PrewarmInput::Base)
|
||
.await;
|
||
let session_start_source = match &initial_history {
|
||
InitialHistory::Forked(_) if forked_from_id.is_some() => {
|
||
codex_hooks::SessionStartSource::Fork
|
||
}
|
||
// `thread/resume` with supplied history uses `Forked` internally
|
||
// without a fork parent, so it should still report `resume`.
|
||
InitialHistory::Resumed(_) | InitialHistory::Forked(_) => {
|
||
codex_hooks::SessionStartSource::Resume
|
||
}
|
||
InitialHistory::New => codex_hooks::SessionStartSource::Startup,
|
||
InitialHistory::Cleared => codex_hooks::SessionStartSource::Clear,
|
||
};
|
||
|
||
// record_initial_history can emit events. We record only after the SessionConfiguredEvent is emitted.
|
||
Box::pin(sess.record_initial_history(initial_history)).await;
|
||
if restore_child_window {
|
||
sess.state.lock().await.restore_auto_compact_window(
|
||
/*window_number*/ 0,
|
||
initial_auto_compact_window_ids,
|
||
);
|
||
}
|
||
if matches!(&sess.fork_persistence, ForkPersistence::Referenced { .. }) {
|
||
// Keep the source reserved until the child's history reference is durable.
|
||
sess.try_ensure_rollout_materialized(PersistContext::Standard)
|
||
.await?;
|
||
}
|
||
{
|
||
let mut state = sess.state.lock().await;
|
||
state.queue_pending_session_start_source(session_start_source);
|
||
}
|
||
Ok(sess)
|
||
}
|
||
.await;
|
||
match session_result {
|
||
Ok(sess) => {
|
||
live_thread_init.commit();
|
||
Ok(sess)
|
||
}
|
||
Err(err) => {
|
||
if let Err(error) = live_thread_init.discard().await {
|
||
tracing::warn!(
|
||
"failed to discard thread persistence for failed session init: {error}"
|
||
);
|
||
}
|
||
Err(err)
|
||
}
|
||
}
|
||
}
|
||
}
|