diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index fd220ae86..b870b5b17 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -2095,6 +2095,7 @@ mod tests { use codex_protocol::items::build_hook_prompt_message; use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions; use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions; + use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -2110,7 +2111,6 @@ mod tests { use codex_protocol::protocol::RateLimitSnapshot; use codex_protocol::protocol::RateLimitWindow; use codex_protocol::protocol::RolloutItem; - use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::TokenUsage; use codex_protocol::protocol::TokenUsageInfo; @@ -2187,7 +2187,7 @@ mod tests { agent_path: None, git_info: None, approval_mode: AskForApproval::OnRequest, - sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: PermissionProfile::read_only(), token_usage: None, first_user_message: Some("before rollback".to_string()), history: Some(StoredThreadHistory { diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index b5d49cf7f..2a4b2722e 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -68,13 +68,13 @@ mod thread_processor_behavior_tests { use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY; use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; + use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; - use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_state::ThreadMetadataBuilder; @@ -412,7 +412,7 @@ mod thread_processor_behavior_tests { agent_path: None, git_info: None, approval_mode: AskForApproval::OnRequest, - sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: PermissionProfile::read_only(), token_usage: None, first_user_message: Some("first user message".to_string()), history: None, diff --git a/codex-rs/core/src/realtime_context_tests.rs b/codex-rs/core/src/realtime_context_tests.rs index f36239f2a..d60484d2e 100644 --- a/codex-rs/core/src/realtime_context_tests.rs +++ b/codex-rs/core/src/realtime_context_tests.rs @@ -13,10 +13,10 @@ use chrono::Utc; use codex_git_utils::GitSha; use codex_protocol::ThreadId; use codex_protocol::models::ContentItem; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::GitInfo; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_thread_store::StoredThread; use core_test_support::PathBufExt; @@ -59,7 +59,7 @@ fn stored_thread(cwd: &str, title: &str, first_user_message: &str) -> StoredThre repository_url: None, }), approval_mode: AskForApproval::Never, - sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: PermissionProfile::read_only(), token_usage: None, first_user_message: Some(first_user_message.to_string()), history: None, diff --git a/codex-rs/state/src/extract.rs b/codex-rs/state/src/extract.rs index 5f0734e33..0094e362e 100644 --- a/codex-rs/state/src/extract.rs +++ b/codex-rs/state/src/extract.rs @@ -76,7 +76,8 @@ fn apply_turn_context(metadata: &mut ThreadMetadata, turn_ctx: &TurnContextItem) } metadata.model = Some(turn_ctx.model.clone()); metadata.reasoning_effort = turn_ctx.effort; - metadata.sandbox_policy = enum_to_string(&turn_ctx.sandbox_policy); + metadata.sandbox_policy = + serde_json::to_string(&turn_ctx.permission_profile()).unwrap_or_default(); metadata.approval_mode = enum_to_string(&turn_ctx.approval_policy); } @@ -157,6 +158,7 @@ mod tests { use chrono::Utc; use codex_protocol::ThreadId; use codex_protocol::models::ContentItem; + use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; @@ -364,11 +366,46 @@ mod tests { assert_eq!(metadata.cwd, PathBuf::from("/child/worktree")); assert_eq!( metadata.sandbox_policy, - super::enum_to_string(&SandboxPolicy::DangerFullAccess) + serde_json::to_string(&PermissionProfile::Disabled) + .expect("serialize permission profile") ); assert_eq!(metadata.approval_mode, "never"); } + #[test] + fn turn_context_sets_permission_profile_metadata() { + let mut metadata = metadata_for_test(); + let permission_profile = PermissionProfile::workspace_write(); + + apply_rollout_item( + &mut metadata, + &RolloutItem::TurnContext(TurnContextItem { + turn_id: Some("turn-1".to_string()), + cwd: PathBuf::from("/workspace"), + workspace_roots: None, + current_date: None, + timezone: None, + approval_policy: AskForApproval::OnRequest, + sandbox_policy: SandboxPolicy::DangerFullAccess, + permission_profile: Some(permission_profile.clone()), + network: None, + file_system_sandbox_policy: None, + model: "gpt-5".to_string(), + personality: None, + collaboration_mode: None, + realtime_active: None, + effort: None, + summary: codex_protocol::config_types::ReasoningSummary::Auto, + }), + "test-provider", + ); + + assert_eq!( + metadata.sandbox_policy, + serde_json::to_string(&permission_profile).expect("serialize permission profile") + ); + } + #[test] fn turn_context_sets_cwd_when_session_cwd_missing() { let mut metadata = metadata_for_test(); diff --git a/codex-rs/thread-store/src/in_memory.rs b/codex-rs/thread-store/src/in_memory.rs index 5956b3dc4..5dade3a26 100644 --- a/codex-rs/thread-store/src/in_memory.rs +++ b/codex-rs/thread-store/src/in_memory.rs @@ -8,9 +8,9 @@ use std::sync::OnceLock; use async_trait::async_trait; use chrono::Utc; use codex_protocol::ThreadId; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::RolloutItem; -use codex_protocol::protocol::SandboxPolicy; use crate::AppendThreadItemsParams; use crate::ArchiveThreadParams; @@ -363,9 +363,9 @@ fn stored_thread_from_state( approval_mode: metadata .and_then(|metadata| metadata.approval_mode) .unwrap_or(AskForApproval::Never), - sandbox_policy: metadata - .and_then(|metadata| metadata.sandbox_policy.clone()) - .unwrap_or_else(SandboxPolicy::new_read_only_policy), + permission_profile: metadata + .and_then(|metadata| metadata.permission_profile.clone()) + .unwrap_or_else(PermissionProfile::read_only), token_usage: metadata.and_then(|metadata| metadata.token_usage.clone()), first_user_message: metadata.and_then(|metadata| metadata.first_user_message.clone()), history, diff --git a/codex-rs/thread-store/src/local/helpers.rs b/codex-rs/thread-store/src/local/helpers.rs index bfdc94fbf..75120eb56 100644 --- a/codex-rs/thread-store/src/local/helpers.rs +++ b/codex-rs/thread-store/src/local/helpers.rs @@ -9,8 +9,10 @@ use chrono::DateTime; use chrono::Utc; use codex_git_utils::GitSha; use codex_protocol::ThreadId; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::GitInfo; +use codex_protocol::protocol::NetworkAccess; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_rollout::ARCHIVED_SESSIONS_SUBDIR; @@ -140,13 +142,34 @@ pub(super) fn stored_thread_from_rollout_item( agent_path: None, git_info, approval_mode: AskForApproval::OnRequest, - sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: PermissionProfile::read_only(), token_usage: None, first_user_message: item.first_user_message, history: None, }) } +pub(super) fn permission_profile_from_metadata_value(value: &str, cwd: &Path) -> PermissionProfile { + serde_json::from_str::(value) + .or_else(|_| { + parse_legacy_sandbox_policy(value) + .map(|policy| PermissionProfile::from_legacy_sandbox_policy_for_cwd(&policy, cwd)) + }) + .unwrap_or_else(|_| PermissionProfile::read_only()) +} + +pub(super) fn permission_profile_to_metadata_value( + permission_profile: &PermissionProfile, +) -> String { + match serde_json::to_string(permission_profile) { + Ok(value) => value, + Err(err) => { + tracing::warn!("failed to serialize permission profile metadata: {err}"); + String::new() + } + } +} + pub(super) fn distinct_thread_metadata_title(metadata: &ThreadMetadata) -> Option { let title = metadata.title.trim(); if title.is_empty() || metadata.first_user_message.as_deref().map(str::trim) == Some(title) { @@ -169,6 +192,20 @@ fn parse_rfc3339(value: Option<&str>) -> Option> { .map(|dt| dt.with_timezone(&Utc)) } +fn parse_legacy_sandbox_policy(value: &str) -> serde_json::Result { + serde_json::from_str(value) + .or_else(|_| serde_json::from_value(serde_json::Value::String(value.to_string()))) + .or_else(|_| match value { + "danger-full-access" => Ok(SandboxPolicy::DangerFullAccess), + "read-only" => Ok(SandboxPolicy::new_read_only_policy()), + "workspace-write" => Ok(SandboxPolicy::new_workspace_write_policy()), + "external-sandbox" => Ok(SandboxPolicy::ExternalSandbox { + network_access: NetworkAccess::Restricted, + }), + _ => serde_json::from_value(serde_json::Value::String(value.to_string())), + }) +} + pub(super) fn git_info_from_parts( sha: Option, branch: Option, diff --git a/codex-rs/thread-store/src/local/read_thread.rs b/codex-rs/thread-store/src/local/read_thread.rs index 718527e7e..767dbda6c 100644 --- a/codex-rs/thread-store/src/local/read_thread.rs +++ b/codex-rs/thread-store/src/local/read_thread.rs @@ -1,7 +1,7 @@ use chrono::DateTime; use chrono::Utc; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionMetaLine; use codex_protocol::protocol::SessionSource; use codex_rollout::RolloutRecorder; @@ -15,6 +15,7 @@ use codex_state::ThreadMetadata; use super::LocalThreadStore; use super::helpers::distinct_thread_metadata_title; use super::helpers::git_info_from_parts; +use super::helpers::permission_profile_from_metadata_value; use super::helpers::rollout_path_is_archived; use super::helpers::set_thread_name_from_title; use super::helpers::stored_thread_from_rollout_item; @@ -45,6 +46,7 @@ pub(super) async fn read_thread( ) .await) { + let metadata_sandbox_policy = metadata.sandbox_policy.clone(); let mut thread = stored_thread_from_sqlite_metadata(store, metadata).await; if !params.include_history && let Some(rollout_path) = thread.rollout_path.clone() @@ -57,6 +59,10 @@ pub(super) async fn read_thread( rollout_thread.name = thread.name; } rollout_thread.git_info = thread.git_info; + rollout_thread.permission_profile = permission_profile_from_metadata_value( + &metadata_sandbox_policy, + rollout_thread.cwd.as_path(), + ); thread = rollout_thread; } attach_history_if_requested(&mut thread, params.include_history).await?; @@ -286,6 +292,8 @@ async fn stored_thread_from_sqlite_metadata( .clone() .or_else(|| metadata.first_user_message.clone()) .unwrap_or_default(); + let permission_profile = + permission_profile_from_metadata_value(&metadata.sandbox_policy, metadata.cwd.as_path()); StoredThread { thread_id: metadata.id, rollout_path: Some(metadata.rollout_path), @@ -315,10 +323,7 @@ async fn stored_thread_from_sqlite_metadata( metadata.git_origin_url, ), approval_mode: parse_or_default(&metadata.approval_mode, AskForApproval::OnRequest), - sandbox_policy: parse_or_default( - &metadata.sandbox_policy, - SandboxPolicy::new_read_only_policy(), - ), + permission_profile, token_usage: None, first_user_message: metadata.first_user_message, history: None, @@ -377,7 +382,7 @@ fn stored_thread_from_meta_line( agent_path: meta_line.meta.agent_path, git_info: meta_line.git, approval_mode: AskForApproval::OnRequest, - sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: PermissionProfile::read_only(), token_usage: None, first_user_message: None, history: None, @@ -412,6 +417,7 @@ mod tests { use chrono::Utc; use codex_protocol::ThreadId; + use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_state::ThreadMetadataBuilder; use pretty_assertions::assert_eq; @@ -671,6 +677,84 @@ mod tests { assert_eq!(thread.name, Some("Saved title".to_string())); } + #[tokio::test] + async fn read_thread_returns_permission_profile_from_sqlite_metadata() { + let home = TempDir::new().expect("temp dir"); + let config = test_config(home.path()); + let uuid = Uuid::from_u128(225); + let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id"); + let rollout_path = + write_session_file(home.path(), "2025-01-03T12-00-00", uuid).expect("session file"); + let runtime = codex_state::StateRuntime::init( + config.sqlite_home.clone(), + config.default_model_provider_id.clone(), + ) + .await + .expect("state db should initialize"); + let store = LocalThreadStore::new(config.clone(), Some(runtime.clone())); + let mut builder = + ThreadMetadataBuilder::new(thread_id, rollout_path, Utc::now(), SessionSource::Cli); + builder.model_provider = Some(config.default_model_provider_id.clone()); + builder.cwd = home.path().to_path_buf(); + let mut metadata = builder.build(config.default_model_provider_id.as_str()); + metadata.sandbox_policy = + serde_json::to_string(&PermissionProfile::Disabled).expect("serialize profile"); + runtime + .upsert_thread(&metadata) + .await + .expect("state db upsert should succeed"); + + let thread = store + .read_thread(ReadThreadParams { + thread_id, + include_archived: false, + include_history: false, + }) + .await + .expect("read thread"); + + assert_eq!(thread.preview, "Hello from user"); + assert_eq!(thread.permission_profile, PermissionProfile::Disabled); + } + + #[tokio::test] + async fn read_thread_accepts_legacy_sandbox_policy_metadata() { + let home = TempDir::new().expect("temp dir"); + let config = test_config(home.path()); + let uuid = Uuid::from_u128(226); + let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id"); + let rollout_path = + write_session_file(home.path(), "2025-01-03T12-00-00", uuid).expect("session file"); + let runtime = codex_state::StateRuntime::init( + config.sqlite_home.clone(), + config.default_model_provider_id.clone(), + ) + .await + .expect("state db should initialize"); + let store = LocalThreadStore::new(config.clone(), Some(runtime.clone())); + let mut builder = + ThreadMetadataBuilder::new(thread_id, rollout_path, Utc::now(), SessionSource::Cli); + builder.model_provider = Some(config.default_model_provider_id.clone()); + builder.cwd = home.path().to_path_buf(); + let mut metadata = builder.build(config.default_model_provider_id.as_str()); + metadata.sandbox_policy = "danger-full-access".to_string(); + runtime + .upsert_thread(&metadata) + .await + .expect("state db upsert should succeed"); + + let thread = store + .read_thread(ReadThreadParams { + thread_id, + include_archived: false, + include_history: true, + }) + .await + .expect("read thread"); + + assert_eq!(thread.permission_profile, PermissionProfile::Disabled); + } + #[tokio::test] async fn read_thread_preserves_rollout_cwd_when_sqlite_metadata_exists() { let home = TempDir::new().expect("temp dir"); @@ -725,6 +809,7 @@ mod tests { let mut metadata = builder.build(config.default_model_provider_id.as_str()); metadata.title = "Saved title".to_string(); metadata.first_user_message = Some("Hello from sqlite".to_string()); + metadata.sandbox_policy = "workspace-write".to_string(); runtime .upsert_thread(&metadata) .await @@ -745,6 +830,19 @@ mod tests { assert_eq!(thread.name, Some("Saved title".to_string())); assert_eq!(thread.model_provider, "rollout-provider"); assert_eq!(thread.cwd, rollout_cwd); + let legacy_policy = SandboxPolicy::WorkspaceWrite { + writable_roots: Vec::new(), + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }; + assert_eq!( + thread.permission_profile, + PermissionProfile::from_legacy_sandbox_policy_for_cwd( + &legacy_policy, + rollout_cwd.as_path() + ) + ); } #[tokio::test] diff --git a/codex-rs/thread-store/src/local/update_thread_metadata.rs b/codex-rs/thread-store/src/local/update_thread_metadata.rs index 612328f4c..3dd0b39a3 100644 --- a/codex-rs/thread-store/src/local/update_thread_metadata.rs +++ b/codex-rs/thread-store/src/local/update_thread_metadata.rs @@ -18,6 +18,7 @@ use tracing::warn; use super::LocalThreadStore; use super::helpers::git_info_from_parts; +use super::helpers::permission_profile_to_metadata_value; use super::live_writer; use crate::GitInfoPatch; use crate::ReadThreadParams; @@ -275,8 +276,8 @@ async fn apply_metadata_update( if let Some(approval_mode) = patch.approval_mode { metadata.approval_mode = enum_to_string(&approval_mode); } - if let Some(sandbox_policy) = patch.sandbox_policy { - metadata.sandbox_policy = enum_to_string(&sandbox_policy); + if let Some(permission_profile) = patch.permission_profile { + metadata.sandbox_policy = permission_profile_to_metadata_value(&permission_profile); } if let Some(token_usage) = patch.token_usage { metadata.tokens_used = token_usage.total_tokens.max(0); @@ -382,7 +383,7 @@ fn has_observed_metadata_facts(patch: &ThreadMetadataPatch) -> bool { || patch.cwd.is_some() || patch.cli_version.is_some() || patch.approval_mode.is_some() - || patch.sandbox_policy.is_some() + || patch.permission_profile.is_some() || patch.token_usage.is_some() || patch.first_user_message.is_some() } @@ -604,6 +605,7 @@ fn rollout_path_is_archived(store: &LocalThreadStore, path: &Path) -> bool { #[cfg(test)] mod tests { + use codex_protocol::models::PermissionProfile; use pretty_assertions::assert_eq; use serde_json::Value; use serde_json::json; @@ -845,6 +847,45 @@ mod tests { ); } + #[tokio::test] + async fn update_thread_metadata_sets_permission_profile() { + let home = TempDir::new().expect("temp dir"); + let config = test_config(home.path()); + let runtime = codex_state::StateRuntime::init( + config.sqlite_home.clone(), + config.default_model_provider_id.clone(), + ) + .await + .expect("state db should initialize"); + let store = LocalThreadStore::new(config, Some(runtime.clone())); + let uuid = Uuid::from_u128(317); + let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id"); + write_session_file(home.path(), "2025-01-03T20-30-00", uuid).expect("session file"); + + let thread = store + .update_thread_metadata(UpdateThreadMetadataParams { + thread_id, + patch: ThreadMetadataPatch { + permission_profile: Some(PermissionProfile::Disabled), + ..Default::default() + }, + include_archived: false, + }) + .await + .expect("set permission profile"); + + assert_eq!(thread.permission_profile, PermissionProfile::Disabled); + let metadata = runtime + .get_thread(thread_id) + .await + .expect("sqlite metadata read") + .expect("sqlite metadata"); + assert_eq!( + metadata.sandbox_policy, + serde_json::to_string(&PermissionProfile::Disabled).expect("serialize profile") + ); + } + #[tokio::test] async fn update_thread_metadata_partially_updates_git_info() { let home = TempDir::new().expect("temp dir"); diff --git a/codex-rs/thread-store/src/thread_metadata_sync.rs b/codex-rs/thread-store/src/thread_metadata_sync.rs index d25a0e65a..3e83d2f48 100644 --- a/codex-rs/thread-store/src/thread_metadata_sync.rs +++ b/codex-rs/thread-store/src/thread_metadata_sync.rs @@ -234,7 +234,7 @@ impl ThreadMetadataSync { update.model = Some(turn_ctx.model.clone()); update.reasoning_effort = turn_ctx.effort; update.approval_mode = Some(turn_ctx.approval_policy); - update.sandbox_policy = Some(turn_ctx.sandbox_policy.clone()); + update.permission_profile = Some(turn_ctx.permission_profile()); } RolloutItem::EventMsg(EventMsg::UserMessage(user)) => { if let Some(preview) = user_message_preview(user) { @@ -354,7 +354,7 @@ fn update_has_metadata_facts(update: &ThreadMetadataPatch) -> bool { || update.cwd.is_some() || update.cli_version.is_some() || update.approval_mode.is_some() - || update.sandbox_policy.is_some() + || update.permission_profile.is_some() || update.token_usage.is_some() || update.first_user_message.is_some() || update.git_info.is_some() diff --git a/codex-rs/thread-store/src/types.rs b/codex-rs/thread-store/src/types.rs index 1e26e5dc7..61763f10f 100644 --- a/codex-rs/thread-store/src/types.rs +++ b/codex-rs/thread-store/src/types.rs @@ -5,11 +5,11 @@ use chrono::Utc; use codex_protocol::ThreadId; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::models::BaseInstructions; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::GitInfo; use codex_protocol::protocol::RolloutItem; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::ThreadMemoryMode as MemoryMode; use codex_protocol::protocol::ThreadSource; @@ -396,8 +396,8 @@ pub struct StoredThread { pub git_info: Option, /// Approval mode captured for the thread. pub approval_mode: AskForApproval, - /// Sandbox policy captured for the thread. - pub sandbox_policy: SandboxPolicy, + /// Canonical runtime permissions captured for the thread. + pub permission_profile: PermissionProfile, /// Last observed token usage. pub token_usage: Option, /// First user message observed for this thread, if any. @@ -519,8 +519,8 @@ pub struct ThreadMetadataPatch { pub cli_version: Option, /// Approval mode. pub approval_mode: Option, - /// Sandbox policy. - pub sandbox_policy: Option, + /// Canonical runtime permissions. + pub permission_profile: Option, /// Last observed token usage. pub token_usage: Option, /// First user message observed for this thread. @@ -589,8 +589,8 @@ impl ThreadMetadataPatch { if next.approval_mode.is_some() { self.approval_mode = next.approval_mode; } - if next.sandbox_policy.is_some() { - self.sandbox_policy = next.sandbox_policy; + if next.permission_profile.is_some() { + self.permission_profile = next.permission_profile; } if next.token_usage.is_some() { self.token_usage = next.token_usage; @@ -626,7 +626,7 @@ impl ThreadMetadataPatch { && self.cwd.is_none() && self.cli_version.is_none() && self.approval_mode.is_none() - && self.sandbox_policy.is_none() + && self.permission_profile.is_none() && self.token_usage.is_none() && self.first_user_message.is_none() && self.git_info.is_none()