mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
Restore thread-owned permissions on cold resume (#53150)
## Why Cold resume can replace saved unnamed permissions with current configuration defaults or recover an executor's permissions from turn context. A first-turn context can also predate an acknowledged settings update. ## What changed - Restore eligible unnamed permission profiles from settings snapshots owned by the resumed thread, honoring explicit caller overrides and current managed requirements. - Use owned snapshots as the authority for named permission profiles and reapply workspace permissions for the resumed workspace roots. - Persist a thread-owned settings checkpoint after the first turn's context, serialized with settings updates so older checkpoints cannot supersede newer settings. ## Testing Extend regression coverage for cold resume with changed defaults, managed permission restrictions, workspace-root overrides, foreign snapshots, and settings updates during first-turn processing. GitOrigin-RevId: e411d9fd3d586a4162498c9c77c4184459071bd1
This commit is contained in:
@@ -137,6 +137,7 @@ use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::SandboxEnforcement;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicyContext;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
@@ -459,6 +460,7 @@ pub(crate) struct SessionSpawnArgs {
|
||||
pub(crate) conversation_history: InitialHistory,
|
||||
pub(crate) history_initialization: HistoryInitialization,
|
||||
pub(crate) disabled_plugin_ids: Option<Vec<String>>,
|
||||
pub(crate) persisted_permission_profile: Option<PermissionProfile>,
|
||||
pub(crate) requested_history_mode: Option<ThreadHistoryMode>,
|
||||
pub(crate) fork_persistence: ForkPersistence,
|
||||
pub(crate) session_source: SessionSource,
|
||||
@@ -574,6 +576,7 @@ impl Session {
|
||||
conversation_history,
|
||||
history_initialization,
|
||||
disabled_plugin_ids,
|
||||
persisted_permission_profile,
|
||||
requested_history_mode,
|
||||
fork_persistence,
|
||||
session_source,
|
||||
@@ -869,7 +872,7 @@ impl Session {
|
||||
let service_tier =
|
||||
get_service_tier(config.service_tier.clone(), &config.features, &model_info);
|
||||
let storage_originator = AuthStorageOriginator::from_client_name(&originator);
|
||||
let session_configuration = SessionConfiguration {
|
||||
let mut session_configuration = SessionConfiguration {
|
||||
turn_extension_init,
|
||||
provider: create_model_provider(
|
||||
config.model_provider.clone(),
|
||||
@@ -912,6 +915,35 @@ impl Session {
|
||||
dynamic_tools,
|
||||
user_shell_override,
|
||||
};
|
||||
// Restore the thread's base policy, not an executor's enforcement or
|
||||
// materialized grants that might outlive the workspace they came from.
|
||||
if let Some(profile) = persisted_permission_profile
|
||||
&& !matches!(profile, PermissionProfile::External { .. })
|
||||
&& !profile.file_system_sandbox_policy().entries.iter().any(|entry| {
|
||||
entry.access.can_read()
|
||||
&& matches!(
|
||||
entry.path,
|
||||
FileSystemPath::Path { .. } | FileSystemPath::GlobPattern { .. }
|
||||
)
|
||||
})
|
||||
&& config
|
||||
.config_layer_stack
|
||||
.requirements_toml()
|
||||
.allowed_permission_profiles
|
||||
.is_none()
|
||||
// Reject a disallowed saved mode before the live-update path can
|
||||
// narrow it into a different policy (for example, Full + denies).
|
||||
&& config.permissions.can_set_permission_profile(&profile).is_ok()
|
||||
&& let Ok(restored) = session_configuration.apply(
|
||||
&SessionSettingsUpdate {
|
||||
permission_profile: Some(profile),
|
||||
..Default::default()
|
||||
},
|
||||
&environment_selections,
|
||||
)
|
||||
{
|
||||
session_configuration = restored;
|
||||
}
|
||||
session_configuration
|
||||
.validate(&environment_selections)
|
||||
.map_err(|err| CodexErr::InvalidRequest(err.to_string()))?;
|
||||
@@ -4804,8 +4836,21 @@ impl Session {
|
||||
}
|
||||
// Persist one `TurnContextItem` per real user turn so resume/lazy replay can recover the
|
||||
// latest durable baseline even when this turn emitted no model-visible context diffs.
|
||||
self.persist_rollout_items(&[RolloutItem::TurnContext(turn_context_item.clone())])
|
||||
.await;
|
||||
// Checkpoint current thread-owned settings after the first turn's context:
|
||||
// the step may predate an acknowledged update or use executor-owned permissions.
|
||||
// Serialize with settings updates so an older checkpoint cannot supersede a newer one.
|
||||
let _settings_guard = if should_inject_full_context {
|
||||
Some(thread_settings::acquire_persistence_lock(self).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut rollout_items = vec![RolloutItem::TurnContext(turn_context_item.clone())];
|
||||
if should_inject_full_context {
|
||||
rollout_items.push(RolloutItem::EventMsg(
|
||||
thread_settings::applied_event(self).await,
|
||||
));
|
||||
}
|
||||
self.persist_rollout_items(&rollout_items).await;
|
||||
|
||||
// Advance the persisted-settings baseline even when this turn emitted no model-visible
|
||||
// context items.
|
||||
|
||||
@@ -1305,6 +1305,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
|
||||
conversation_history: InitialHistory::New,
|
||||
history_initialization: HistoryInitialization::New,
|
||||
disabled_plugin_ids: None,
|
||||
persisted_permission_profile: None,
|
||||
requested_history_mode: None,
|
||||
fork_persistence: ForkPersistence::Copied,
|
||||
session_source: SessionSource::SubAgent(SubAgentSource::Other(
|
||||
|
||||
Reference in New Issue
Block a user