mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
## What changed - Skip folder trust prompts for locally discovered projectless directories without a saved trust decision. Apply workspace-write permissions and granular approval defaults only when local execution, configuration, and managed policy allow them. - Prompt for Windows sandbox setup when needed before enabling implicit workspace-write permissions. - Restore saved task permissions on resume unless explicitly overridden, and keep ordinary turns from overwriting the server's saved permission profile. Track explicit approval reviewer choices separately from restored permissions. - Let `/cd` request folder trust with a “Keep current directory” option. Recheck task activity and background terminals before switching, and enforce destination permission requirements. ## Testing Add regression coverage for projectless defaults and session transitions, explicit permission overrides, saved profile preservation, trust discovery, destination policy conflicts, and the directory consent and Windows sandbox prompts. GitOrigin-RevId: cf74f5a90245f2aee96caa80d76de64da6ec1ebd
170 lines
5.6 KiB
Rust
170 lines
5.6 KiB
Rust
//! Interactive recovery when starting an archived session.
|
|
//! Retries preserve the launch-resolved local presentation settings from the first attempt.
|
|
|
|
use crate::app::AppExitInfo;
|
|
use crate::app::ExitReason;
|
|
use crate::app_server_session::AppServerSession;
|
|
use crate::app_server_session::AppServerStartedThread;
|
|
use crate::app_server_session::ForkPermissionMode;
|
|
use crate::app_server_session::ResumeModelSettings;
|
|
use crate::legacy_core::config::Config;
|
|
use crate::local_settings::LocalSettings;
|
|
use crate::resume_picker::SessionTarget;
|
|
use crate::unarchive_prompt::UnarchiveChoice;
|
|
use color_eyre::Result;
|
|
use color_eyre::eyre::WrapErr;
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub(crate) enum SessionStartAction {
|
|
Resume(
|
|
ResumeModelSettings,
|
|
crate::resume_permissions::ResumePermissions,
|
|
),
|
|
Fork(ForkPermissionMode),
|
|
}
|
|
|
|
impl SessionStartAction {
|
|
pub(crate) fn verb(self) -> &'static str {
|
|
match self {
|
|
Self::Resume(..) => "resume",
|
|
Self::Fork(_) => "fork",
|
|
}
|
|
}
|
|
|
|
async fn start(
|
|
self,
|
|
app_server: &mut AppServerSession,
|
|
config: &Config,
|
|
local_settings: &LocalSettings,
|
|
target: &SessionTarget,
|
|
) -> Result<AppServerStartedThread> {
|
|
match self {
|
|
Self::Resume(settings, permissions) => {
|
|
app_server
|
|
.resume_thread_with_permission_overrides(
|
|
local_settings,
|
|
config.clone(),
|
|
target.thread_id,
|
|
settings,
|
|
permissions,
|
|
)
|
|
.await
|
|
}
|
|
Self::Fork(permission_mode) => {
|
|
app_server
|
|
.fork_thread_with_permission_mode(
|
|
local_settings,
|
|
config.clone(),
|
|
target.thread_id,
|
|
permission_mode,
|
|
)
|
|
.await
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) enum SessionStartOutcome {
|
|
Started(Box<AppServerStartedThread>),
|
|
CommandCenter,
|
|
Exit,
|
|
}
|
|
|
|
/// The first attempt's session configuration and launch-resolved presentation settings.
|
|
/// Archived-session retries must reuse both so thread configuration cannot change ownership.
|
|
pub(crate) struct SessionStartConfig<'a> {
|
|
pub(crate) config: &'a Config,
|
|
pub(crate) local_settings: &'a LocalSettings,
|
|
}
|
|
|
|
pub(crate) async fn complete_session_start(
|
|
app_server: &mut AppServerSession,
|
|
config: SessionStartConfig<'_>,
|
|
app_server_target: &crate::AppServerTarget,
|
|
target: &SessionTarget,
|
|
action: SessionStartAction,
|
|
initial_result: Result<AppServerStartedThread>,
|
|
confirm: impl AsyncFnOnce() -> Result<UnarchiveChoice>,
|
|
) -> Result<SessionStartOutcome> {
|
|
match initial_result {
|
|
Ok(started) => return Ok(SessionStartOutcome::Started(Box::new(started))),
|
|
Err(err) => {
|
|
// Match the requested ID as well as the server's archive guidance: an unrelated
|
|
// startup failure must never cause us to unarchive a session.
|
|
let archived_prefix = format!("session {} is archived. ", target.thread_id);
|
|
if !archived_session_guidance(&err)
|
|
.is_some_and(|message| message.starts_with(&archived_prefix))
|
|
{
|
|
return Err(session_start_error(action.verb(), target, err));
|
|
}
|
|
}
|
|
}
|
|
|
|
match confirm().await? {
|
|
UnarchiveChoice::Cancel => {
|
|
return Ok(match app_server_target {
|
|
crate::AppServerTarget::LocalDaemon { .. }
|
|
| crate::AppServerTarget::Remote { .. } => SessionStartOutcome::CommandCenter,
|
|
crate::AppServerTarget::Embedded => SessionStartOutcome::Exit,
|
|
});
|
|
}
|
|
UnarchiveChoice::Quit => return Ok(SessionStartOutcome::Exit),
|
|
UnarchiveChoice::Unarchive => {}
|
|
}
|
|
|
|
app_server
|
|
.thread_unarchive(target.thread_id)
|
|
.await
|
|
.wrap_err_with(|| format!("Failed to unarchive session {}", target.thread_id))?;
|
|
// Retry by ID, not by the old rollout path, which unarchiving may have moved.
|
|
action
|
|
.start(app_server, config.config, config.local_settings, target)
|
|
.await
|
|
.map(|started| SessionStartOutcome::Started(Box::new(started)))
|
|
.map_err(|err| session_start_error(action.verb(), target, err))
|
|
}
|
|
|
|
pub(crate) async fn cancel_session_start(app_server: AppServerSession) -> AppExitInfo {
|
|
if let Err(err) = app_server.shutdown().await {
|
|
tracing::warn!("app-server shutdown failed: {err}");
|
|
}
|
|
AppExitInfo {
|
|
token_usage: Default::default(),
|
|
thread_id: None,
|
|
resume_hint: None,
|
|
disconnect_info: None,
|
|
update_action: None,
|
|
exit_reason: ExitReason::UserRequested,
|
|
}
|
|
}
|
|
|
|
fn session_start_error(
|
|
action: &str,
|
|
target_session: &SessionTarget,
|
|
err: color_eyre::Report,
|
|
) -> color_eyre::Report {
|
|
if let Some(message) = archived_session_guidance(&err) {
|
|
return color_eyre::eyre::eyre!("{message}");
|
|
}
|
|
|
|
let target_label = target_session.display_label();
|
|
color_eyre::eyre::eyre!("Failed to {action} session from {target_label}: {err}")
|
|
}
|
|
|
|
fn archived_session_guidance(err: &color_eyre::Report) -> Option<String> {
|
|
let err = err.to_string();
|
|
let message = &err[err.find("session ")?..];
|
|
if !message.contains(" is archived. Run `codex unarchive ") {
|
|
return None;
|
|
}
|
|
let message = message
|
|
.split_once(" (code ")
|
|
.map_or(message, |(message, _)| message);
|
|
Some(message.to_string())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "session_start_tests.rs"]
|
|
mod tests;
|