From 732143ef385ad759b8d68a258e1ca099bc02cfbe Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 17 Apr 2026 10:49:22 -0700 Subject: [PATCH] core: derive active permission profiles --- .../app-server/src/codex_message_processor.rs | 5 + codex-rs/core/src/codex_thread.rs | 2 + codex-rs/core/src/config/mod.rs | 12 ++ codex-rs/core/src/session/mod.rs | 197 +++++++++++++++++- 4 files changed, 212 insertions(+), 4 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 04a066e88f..b54338e50b 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -9911,6 +9911,11 @@ mod tests { approval_policy: codex_protocol::protocol::AskForApproval::OnRequest, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, + permission_profile: + codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( + &codex_protocol::protocol::SandboxPolicy::DangerFullAccess, + std::path::Path::new("/tmp"), + ), cwd: test_path_buf("/tmp").abs(), ephemeral: false, reasoning_effort: None, diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 6513f09d07..7c8e9fa68a 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -11,6 +11,7 @@ use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; use codex_protocol::mcp::CallToolResult; use codex_protocol::models::ContentItem; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ReasoningEffort; @@ -42,6 +43,7 @@ pub struct ThreadConfigSnapshot { pub approval_policy: AskForApproval, pub approvals_reviewer: ApprovalsReviewer, pub sandbox_policy: SandboxPolicy, + pub permission_profile: PermissionProfile, pub cwd: AbsolutePathBuf, pub ephemeral: bool, pub reasoning_effort: Option, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 6320bff2e3..c48ac2db60 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -75,6 +75,7 @@ use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::WebSearchConfig; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::permissions::FileSystemSandboxPolicy; @@ -215,6 +216,17 @@ pub struct Permissions { pub windows_sandbox_private_desktop: bool, } +impl Permissions { + /// Effective runtime permissions after config requirements and runtime + /// readable-root additions have been applied. + pub fn permission_profile(&self) -> PermissionProfile { + PermissionProfile::from_runtime_permissions( + &self.file_system_sandbox_policy, + self.network_sandbox_policy, + ) + } +} + /// Application configuration loaded from disk and merged with overrides. #[derive(Debug, Clone, PartialEq)] pub struct Config { diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 4176abd47e..1ac02d0d63 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -879,6 +879,13 @@ pub(crate) struct TurnContext { pub(crate) turn_timing_state: Arc, } impl TurnContext { + pub(crate) fn permission_profile(&self) -> PermissionProfile { + PermissionProfile::from_runtime_permissions( + &self.file_system_sandbox_policy, + self.network_sandbox_policy, + ) + } + pub(crate) fn model_context_window(&self) -> Option { let effective_context_window_percent = self.model_info.effective_context_window_percent; self.model_info.context_window.map(|context_window| { @@ -1013,10 +1020,7 @@ impl TurnContext { &self, additional_permissions: Option, ) -> FileSystemSandboxContext { - let base_permissions = PermissionProfile::from_runtime_permissions( - &self.file_system_sandbox_policy, - self.network_sandbox_policy, - ); + let base_permissions = self.permission_profile(); let permissions = merge_permission_profiles(Some(&base_permissions), additional_permissions.as_ref()) .unwrap_or(base_permissions); @@ -1125,6 +1129,191 @@ async fn thread_title_from_state_db( .flatten() } +#[derive(Clone)] +pub(crate) struct SessionConfiguration { + /// Provider identifier ("openai", "openrouter", ...). + provider: ModelProviderInfo, + + collaboration_mode: CollaborationMode, + model_reasoning_summary: Option, + service_tier: Option, + + /// Developer instructions that supplement the base instructions. + developer_instructions: Option, + + /// Model instructions that are appended to the base instructions. + user_instructions: Option, + + /// Personality preference for the model. + personality: Option, + + /// Base instructions for the session. + base_instructions: String, + + /// Compact prompt override. + compact_prompt: Option, + + /// When to escalate for approval for execution + approval_policy: Constrained, + approvals_reviewer: ApprovalsReviewer, + /// How to sandbox commands executed in the system + sandbox_policy: Constrained, + file_system_sandbox_policy: FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, + windows_sandbox_level: WindowsSandboxLevel, + + /// Absolute working directory that should be treated as the *root* of the + /// session. All relative paths supplied by the model as well as the + /// execution sandbox are resolved against this directory **instead** of + /// the process-wide current working directory. + cwd: AbsolutePathBuf, + /// Directory containing all Codex state for this session. + codex_home: AbsolutePathBuf, + /// Optional user-facing name for the thread, updated during the session. + thread_name: Option, + + // TODO(pakrym): Remove config from here + original_config_do_not_use: Arc, + /// Optional service name tag for session metrics. + metrics_service_name: Option, + app_server_client_name: Option, + app_server_client_version: Option, + /// Source of the session (cli, vscode, exec, mcp, ...) + session_source: SessionSource, + dynamic_tools: Vec, + persist_extended_history: bool, + inherited_shell_snapshot: Option>, + user_shell_override: Option, +} + +impl SessionConfiguration { + pub(crate) fn codex_home(&self) -> &AbsolutePathBuf { + &self.codex_home + } + + fn permission_profile(&self) -> PermissionProfile { + PermissionProfile::from_runtime_permissions( + &self.file_system_sandbox_policy, + self.network_sandbox_policy, + ) + } + + fn thread_config_snapshot(&self) -> ThreadConfigSnapshot { + ThreadConfigSnapshot { + model: self.collaboration_mode.model().to_string(), + model_provider_id: self.original_config_do_not_use.model_provider_id.clone(), + service_tier: self.service_tier, + approval_policy: self.approval_policy.value(), + approvals_reviewer: self.approvals_reviewer, + sandbox_policy: self.sandbox_policy.get().clone(), + permission_profile: self.permission_profile(), + cwd: self.cwd.clone(), + ephemeral: self.original_config_do_not_use.ephemeral, + reasoning_effort: self.collaboration_mode.reasoning_effort(), + personality: self.personality, + session_source: self.session_source.clone(), + } + } + + pub(crate) fn apply(&self, updates: &SessionSettingsUpdate) -> ConstraintResult { + let mut next_configuration = self.clone(); + let file_system_policy_matches_legacy = self.file_system_sandbox_policy + == FileSystemSandboxPolicy::from_legacy_sandbox_policy( + self.sandbox_policy.get(), + &self.cwd, + ); + if let Some(collaboration_mode) = updates.collaboration_mode.clone() { + next_configuration.collaboration_mode = collaboration_mode; + } + if let Some(summary) = updates.reasoning_summary { + next_configuration.model_reasoning_summary = Some(summary); + } + if let Some(service_tier) = updates.service_tier { + next_configuration.service_tier = service_tier; + } + if let Some(personality) = updates.personality { + next_configuration.personality = Some(personality); + } + if let Some(approval_policy) = updates.approval_policy { + next_configuration.approval_policy.set(approval_policy)?; + } + if let Some(approvals_reviewer) = updates.approvals_reviewer { + next_configuration.approvals_reviewer = approvals_reviewer; + } + let mut sandbox_policy_changed = false; + if let Some(sandbox_policy) = updates.sandbox_policy.clone() { + next_configuration.sandbox_policy.set(sandbox_policy)?; + next_configuration.network_sandbox_policy = + NetworkSandboxPolicy::from(next_configuration.sandbox_policy.get()); + sandbox_policy_changed = true; + } + if let Some(windows_sandbox_level) = updates.windows_sandbox_level { + next_configuration.windows_sandbox_level = windows_sandbox_level; + } + + let absolute_cwd = updates + .cwd + .as_ref() + .map(|cwd| { + AbsolutePathBuf::relative_to_current_dir(normalize_for_native_workdir( + cwd.as_path(), + )) + .unwrap_or_else(|e| { + warn!("failed to normalize update cwd: {cwd:?}: {e}"); + self.cwd.clone() + }) + }) + .unwrap_or_else(|| self.cwd.clone()); + + let cwd_changed = absolute_cwd.as_path() != self.cwd.as_path(); + next_configuration.cwd = absolute_cwd; + if sandbox_policy_changed { + next_configuration.file_system_sandbox_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries( + next_configuration.sandbox_policy.get(), + &next_configuration.cwd, + &self.file_system_sandbox_policy, + ); + } else if cwd_changed && file_system_policy_matches_legacy { + // Preserve richer split policies across cwd-only updates; only + // rederive when the session is already using the legacy bridge. + next_configuration.file_system_sandbox_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy( + next_configuration.sandbox_policy.get(), + &next_configuration.cwd, + ); + } + 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); + } + Ok(next_configuration) + } +} + +#[derive(Default, Clone)] +pub(crate) struct SessionSettingsUpdate { + pub(crate) cwd: Option, + pub(crate) approval_policy: Option, + pub(crate) approvals_reviewer: Option, + pub(crate) sandbox_policy: Option, + pub(crate) windows_sandbox_level: Option, + pub(crate) collaboration_mode: Option, + pub(crate) reasoning_summary: Option, + pub(crate) service_tier: Option>, + pub(crate) final_output_json_schema: Option>, + pub(crate) personality: Option, + pub(crate) app_server_client_name: Option, + pub(crate) app_server_client_version: Option, +} + +pub(crate) struct AppServerClientMetadata { + pub(crate) client_name: Option, + pub(crate) client_version: Option, +} + impl Session { pub(crate) async fn app_server_client_metadata(&self) -> AppServerClientMetadata { let state = self.state.lock().await;