mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
Preserve acknowledged permissions when resuming and forking threads (#57227)
## Why A running turn can record stale permissions after the thread accepts a settings update. Resuming or forking from that history should preserve the accepted approval and permission settings. ## What changed - Prefer settings snapshots owned by the source thread over stale turn contexts, including later legacy updates associated with that thread's checkpoint. - Restore unnamed permission profiles from attributable settings updates while honoring explicit request overrides. Avoid restoring raw permission grants from history without an identifiable owner. - Share permission restoration logic between resume and fork. ## Testing Add regression coverage for stale turn contexts, legacy updates across thread ownership boundaries, and unnamed permission restoration on resume and fork. GitOrigin-RevId: 450bc1688be78c7ec685f6dc56bcc5e5dd2d130a
This commit is contained in:
@@ -5,8 +5,8 @@ mod daemon_continuation;
|
||||
mod daemon_snapshot;
|
||||
|
||||
use super::persisted_resume_settings::PersistedResumeSettings;
|
||||
use super::persisted_resume_settings::latest_owned_thread_settings;
|
||||
use super::persisted_resume_settings::latest_persisted_model_settings;
|
||||
use super::persisted_resume_settings::latest_persisted_resume_settings;
|
||||
use super::thread_enrichment::enrich_loaded_threads;
|
||||
use super::thread_fork_goal::inherit_thread_goal_snapshot;
|
||||
use super::thread_input::can_accept_direct_input;
|
||||
@@ -38,7 +38,6 @@ use codex_protocol::SanitizedGitUrl;
|
||||
use codex_protocol::config_types::MultiAgentMode;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::ThreadHistoryMode;
|
||||
use codex_thread_store::PersistContext;
|
||||
@@ -105,7 +104,6 @@ struct ResumeConfigState {
|
||||
workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
persisted_metadata: Option<ThreadMetadata>,
|
||||
persisted_settings: Option<PersistedResumeSettings>,
|
||||
persisted_permission_profile: Option<PermissionProfile>,
|
||||
}
|
||||
|
||||
struct PreparedResumeConfig {
|
||||
@@ -309,6 +307,29 @@ fn has_permission_override(
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_persisted_permission_profile(
|
||||
settings: &PersistedResumeSettings,
|
||||
request_overrides: Option<&HashMap<String, serde_json::Value>>,
|
||||
config_overrides: &mut ConfigOverrides,
|
||||
) {
|
||||
if has_permission_override(request_overrides, config_overrides) {
|
||||
return;
|
||||
}
|
||||
config_overrides.persisted_permission_profile_id = settings
|
||||
.active_permission_profile
|
||||
.as_ref()
|
||||
.map(|profile| profile.id.clone());
|
||||
if !request_overrides.is_some_and(|overrides| {
|
||||
overrides.keys().any(|key| {
|
||||
key == "sandbox_workspace_write" || key.starts_with("sandbox_workspace_write.")
|
||||
})
|
||||
}) {
|
||||
config_overrides
|
||||
.persisted_permission_profile
|
||||
.clone_from(&settings.unnamed_permission_profile);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_dynamic_tools(tools: &[DynamicToolSpec]) -> Result<(), String> {
|
||||
const DYNAMIC_TOOL_NAME_MAX_LEN: usize = 128;
|
||||
const DYNAMIC_TOOL_NAMESPACE_MAX_LEN: usize = 64;
|
||||
@@ -4139,16 +4160,9 @@ impl ThreadRequestProcessor {
|
||||
}
|
||||
|
||||
// Copied or referenced history can contain another thread's settings. Only snapshots
|
||||
// explicitly owned by this thread can restore its permissions, cwd, and workspace folders.
|
||||
// explicitly owned by this thread can restore its cwd and workspace folders.
|
||||
let history_settings = if let InitialHistory::Resumed(resumed) = &thread_history {
|
||||
resumed.history.iter().rev().find_map(|item| match item {
|
||||
RolloutItem::EventMsg(EventMsg::ThreadSettingsApplied(event))
|
||||
if event.thread_id == Some(resumed.conversation_id) =>
|
||||
{
|
||||
Some(&event.thread_settings)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
latest_owned_thread_settings(&resumed.history, resumed.conversation_id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -4242,27 +4256,12 @@ impl ThreadRequestProcessor {
|
||||
let restore_summary = !has_override("model_reasoning_summary");
|
||||
let restore_personality =
|
||||
typesafe_overrides.personality.is_none() && !has_override("personality");
|
||||
typesafe_overrides.persisted_permission_profile = history_settings
|
||||
.filter(|settings| {
|
||||
settings.active_permission_profile.is_none()
|
||||
&& !has_permission_override(request_overrides.as_ref(), &typesafe_overrides)
|
||||
&& !request_overrides.as_ref().is_some_and(|overrides| {
|
||||
overrides.keys().any(|key| {
|
||||
key == "sandbox_workspace_write"
|
||||
|| key.starts_with("sandbox_workspace_write.")
|
||||
})
|
||||
})
|
||||
})
|
||||
.map(|settings| settings.permission_profile.clone());
|
||||
let mut persisted_settings = match &thread_history {
|
||||
InitialHistory::Resumed(resumed) => latest_persisted_resume_settings(&resumed.history),
|
||||
let persisted_settings = match &thread_history {
|
||||
InitialHistory::Resumed(resumed) => {
|
||||
PersistedResumeSettings::from_history(&resumed.history, resumed.conversation_id)
|
||||
}
|
||||
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => None,
|
||||
};
|
||||
// A TurnContext can describe an executor's profile, so an owned snapshot
|
||||
// is authoritative for named permissions too.
|
||||
if let (Some(settings), Some(snapshot)) = (&mut persisted_settings, history_settings) {
|
||||
settings.active_permission_profile = snapshot.active_permission_profile.clone();
|
||||
}
|
||||
let persisted_metadata = self
|
||||
.load_and_apply_persisted_resume_metadata(
|
||||
&thread_history,
|
||||
@@ -4281,7 +4280,6 @@ impl ThreadRequestProcessor {
|
||||
workspace_roots: typesafe_overrides.workspace_roots.clone(),
|
||||
persisted_metadata,
|
||||
persisted_settings,
|
||||
persisted_permission_profile: typesafe_overrides.persisted_permission_profile.clone(),
|
||||
};
|
||||
let mut config = match prepared_config.take() {
|
||||
Some(prepared) if prepared.state == config_state => prepared.config,
|
||||
@@ -4604,12 +4602,11 @@ impl ThreadRequestProcessor {
|
||||
{
|
||||
typesafe_overrides.approvals_reviewer = persisted_settings.approvals_reviewer;
|
||||
}
|
||||
if !has_permission_override(request_overrides.as_ref(), typesafe_overrides) {
|
||||
typesafe_overrides.persisted_permission_profile_id = persisted_settings
|
||||
.active_permission_profile
|
||||
.as_ref()
|
||||
.map(|profile| profile.id.clone());
|
||||
}
|
||||
apply_persisted_permission_profile(
|
||||
persisted_settings,
|
||||
request_overrides.as_ref(),
|
||||
typesafe_overrides,
|
||||
);
|
||||
}
|
||||
let state_db_ctx = self.state_db.clone()?;
|
||||
let persisted_metadata = state_db_ctx
|
||||
@@ -5526,11 +5523,7 @@ impl ThreadRequestProcessor {
|
||||
let loaded_parent_settings = if paginated_source && needs_latest_settings {
|
||||
if let Some(parent) = loaded_parent.as_ref() {
|
||||
let snapshot = parent.thread_settings_snapshot().await;
|
||||
Some(PersistedResumeSettings {
|
||||
approval_policy: snapshot.approval_policy,
|
||||
approvals_reviewer: Some(snapshot.approvals_reviewer),
|
||||
active_permission_profile: snapshot.active_permission_profile,
|
||||
})
|
||||
Some(PersistedResumeSettings::from(&snapshot))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -5570,11 +5563,10 @@ impl ThreadRequestProcessor {
|
||||
.and_then(|parent| parent.multi_agent_version())
|
||||
});
|
||||
let persisted_settings = loaded_parent_settings.or_else(|| {
|
||||
latest_persisted_resume_settings(
|
||||
latest_context
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| source_history_items.as_ref()),
|
||||
)
|
||||
let history = latest_context
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| source_history_items.as_ref());
|
||||
PersistedResumeSettings::from_history(history, source_thread_id)
|
||||
});
|
||||
if let Some(persisted_settings) = persisted_settings {
|
||||
if restore_approval_policy {
|
||||
@@ -5583,11 +5575,11 @@ impl ThreadRequestProcessor {
|
||||
if restore_approvals_reviewer {
|
||||
typesafe_overrides.approvals_reviewer = persisted_settings.approvals_reviewer;
|
||||
}
|
||||
if restore_permission_profile {
|
||||
typesafe_overrides.persisted_permission_profile_id = persisted_settings
|
||||
.active_permission_profile
|
||||
.map(|profile| profile.id);
|
||||
}
|
||||
apply_persisted_permission_profile(
|
||||
&persisted_settings,
|
||||
request_overrides.as_ref(),
|
||||
&mut typesafe_overrides,
|
||||
);
|
||||
}
|
||||
let (inherited_options, inherited_settings) = inherited_fork.unzip();
|
||||
let options = if let Some(options) = inherited_options {
|
||||
|
||||
Reference in New Issue
Block a user