Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion codex-rs/apply-patch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ fn classify_shell_name(shell: &str) -> Option<String> {

fn classify_shell(shell: &str, flag: &str) -> Option<ApplyPatchShell> {
classify_shell_name(shell).and_then(|name| match name.as_str() {
"bash" | "zsh" | "sh" if flag == "-lc" => Some(ApplyPatchShell::Unix),
"bash" | "zsh" | "sh" if matches!(flag, "-lc" | "-c") => Some(ApplyPatchShell::Unix),
"pwsh" | "powershell" if flag.eq_ignore_ascii_case("-command") => {
Some(ApplyPatchShell::PowerShell)
}
Expand Down Expand Up @@ -1097,6 +1097,13 @@ mod tests {
assert_match(&heredoc_script(""), None);
}

#[test]
fn test_heredoc_non_login_shell() {
let script = heredoc_script("");
let args = strs_to_strings(&["bash", "-c", &script]);
assert_match_args(args, None);
}

#[test]
fn test_heredoc_applypatch() {
let args = strs_to_strings(&[
Expand Down
28 changes: 19 additions & 9 deletions codex-rs/core/src/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ use crate::rollout::RolloutRecorder;
use crate::rollout::RolloutRecorderParams;
use crate::rollout::map_session_init_error;
use crate::shell;
use crate::shell_snapshot::ShellSnapshot;
use crate::state::ActiveTurn;
use crate::state::SessionServices;
use crate::state::SessionState;
Expand Down Expand Up @@ -515,7 +516,6 @@ impl Session {
// - load history metadata
let rollout_fut = RolloutRecorder::new(&config, rollout_params);

let default_shell = shell::default_user_shell();
let history_meta_fut = crate::message_history::history_metadata(&config);
let auth_statuses_fut = compute_auth_statuses(
config.mcp_servers.iter(),
Expand Down Expand Up @@ -577,7 +577,14 @@ impl Session {
config.active_profile.clone(),
);

let mut default_shell = shell::default_user_shell();
// Create the mutable state for the Session.
if config.features.enabled(Feature::ShellSnapshot) {
default_shell.shell_snapshot =
ShellSnapshot::try_new(&config.codex_home, &default_shell)
.await
.map(Arc::new);
}
let state = SessionState::new(session_configuration.clone());

let services = SessionServices {
Expand All @@ -586,7 +593,7 @@ impl Session {
unified_exec_manager: UnifiedExecSessionManager::default(),
notifier: UserNotifier::new(config.notify.clone()),
rollout: Mutex::new(Some(rollout_recorder)),
user_shell: default_shell,
user_shell: Arc::new(default_shell),
show_raw_agent_reasoning: config.show_raw_agent_reasoning,
auth_manager: Arc::clone(&auth_manager),
otel_event_manager,
Expand Down Expand Up @@ -804,14 +811,16 @@ impl Session {
) -> Option<ResponseItem> {
let prev = previous?;

let prev_context = EnvironmentContext::from(prev.as_ref());
let next_context = EnvironmentContext::from(next);
let shell = self.user_shell();
let prev_context = EnvironmentContext::from_turn_context(prev.as_ref(), shell.as_ref());
let next_context = EnvironmentContext::from_turn_context(next, shell.as_ref());
if prev_context.equals_except_shell(&next_context) {
return None;
}
Some(ResponseItem::from(EnvironmentContext::diff(
prev.as_ref(),
next,
shell.as_ref(),
)))
}

Expand Down Expand Up @@ -1161,6 +1170,7 @@ impl Session {

pub(crate) fn build_initial_context(&self, turn_context: &TurnContext) -> Vec<ResponseItem> {
let mut items = Vec::<ResponseItem>::with_capacity(3);
let shell = self.user_shell();
if let Some(developer_instructions) = turn_context.developer_instructions.as_deref() {
items.push(DeveloperInstructions::new(developer_instructions.to_string()).into());
}
Expand All @@ -1177,7 +1187,7 @@ impl Session {
Some(turn_context.cwd.clone()),
Some(turn_context.approval_policy),
Some(turn_context.sandbox_policy.clone()),
self.user_shell().clone(),
shell.as_ref().clone(),
)));
items
}
Expand Down Expand Up @@ -1452,8 +1462,8 @@ impl Session {
&self.services.notifier
}

pub(crate) fn user_shell(&self) -> &shell::Shell {
&self.services.user_shell
pub(crate) fn user_shell(&self) -> Arc<shell::Shell> {
Arc::clone(&self.services.user_shell)
}

fn show_raw_agent_reasoning(&self) -> bool {
Expand Down Expand Up @@ -2895,7 +2905,7 @@ mod tests {
unified_exec_manager: UnifiedExecSessionManager::default(),
notifier: UserNotifier::new(None),
rollout: Mutex::new(None),
user_shell: default_user_shell(),
user_shell: Arc::new(default_user_shell()),
show_raw_agent_reasoning: config.show_raw_agent_reasoning,
auth_manager: auth_manager.clone(),
otel_event_manager: otel_event_manager.clone(),
Expand Down Expand Up @@ -2977,7 +2987,7 @@ mod tests {
unified_exec_manager: UnifiedExecSessionManager::default(),
notifier: UserNotifier::new(None),
rollout: Mutex::new(None),
user_shell: default_user_shell(),
user_shell: Arc::new(default_user_shell()),
show_raw_agent_reasoning: config.show_raw_agent_reasoning,
auth_manager: Arc::clone(&auth_manager),
otel_event_manager: otel_event_manager.clone(),
Expand Down
15 changes: 7 additions & 8 deletions codex-rs/core/src/environment_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use crate::codex::TurnContext;
use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy;
use crate::shell::Shell;
use crate::shell::default_user_shell;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
Expand Down Expand Up @@ -95,7 +94,7 @@ impl EnvironmentContext {
&& self.writable_roots == *writable_roots
}

pub fn diff(before: &TurnContext, after: &TurnContext) -> Self {
pub fn diff(before: &TurnContext, after: &TurnContext, shell: &Shell) -> Self {
let cwd = if before.cwd != after.cwd {
Some(after.cwd.clone())
} else {
Expand All @@ -111,18 +110,15 @@ impl EnvironmentContext {
} else {
None
};
EnvironmentContext::new(cwd, approval_policy, sandbox_policy, default_user_shell())
EnvironmentContext::new(cwd, approval_policy, sandbox_policy, shell.clone())
}
}

impl From<&TurnContext> for EnvironmentContext {
fn from(turn_context: &TurnContext) -> Self {
pub fn from_turn_context(turn_context: &TurnContext, shell: &Shell) -> Self {
Self::new(
Some(turn_context.cwd.clone()),
Some(turn_context.approval_policy),
Some(turn_context.sandbox_policy.clone()),
// Shell is not configurable from turn to turn
default_user_shell(),
shell.clone(),
)
}
}
Expand Down Expand Up @@ -201,6 +197,7 @@ mod tests {
Shell {
shell_type: ShellType::Bash,
shell_path: PathBuf::from("/bin/bash"),
shell_snapshot: None,
}
}

Expand Down Expand Up @@ -338,6 +335,7 @@ mod tests {
Shell {
shell_type: ShellType::Bash,
shell_path: "/bin/bash".into(),
shell_snapshot: None,
},
);
let context2 = EnvironmentContext::new(
Expand All @@ -347,6 +345,7 @@ mod tests {
Shell {
shell_type: ShellType::Zsh,
shell_path: "/bin/zsh".into(),
shell_snapshot: None,
},
);

Expand Down
8 changes: 8 additions & 0 deletions codex-rs/core/src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ pub enum Feature {
ParallelToolCalls,
/// Experimental skills injection (CLI flag-driven).
Skills,
/// Experimental shell snapshotting.
ShellSnapshot,
}

impl Feature {
Expand Down Expand Up @@ -353,4 +355,10 @@ pub const FEATURES: &[FeatureSpec] = &[
stage: Stage::Experimental,
default_enabled: false,
},
FeatureSpec {
id: Feature::ShellSnapshot,
key: "shell_snapshot",
stage: Stage::Experimental,
default_enabled: false,
},
];
1 change: 1 addition & 0 deletions codex-rs/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ mod rollout;
pub(crate) mod safety;
pub mod seatbelt;
pub mod shell;
pub mod shell_snapshot;
pub mod skills;
pub mod spawn;
pub mod terminal;
Expand Down
43 changes: 43 additions & 0 deletions codex-rs/core/src/shell.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use serde::Deserialize;
use serde::Serialize;
use std::path::PathBuf;
use std::sync::Arc;

use crate::shell_snapshot::ShellSnapshot;

#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub enum ShellType {
Expand All @@ -15,6 +18,8 @@ pub enum ShellType {
pub struct Shell {
pub(crate) shell_type: ShellType,
pub(crate) shell_path: PathBuf,
#[serde(skip_serializing, skip_deserializing, default)]
pub(crate) shell_snapshot: Option<Arc<ShellSnapshot>>,
}

impl Shell {
Expand Down Expand Up @@ -58,6 +63,33 @@ impl Shell {
}
}
}

pub(crate) fn wrap_command_with_snapshot(&self, command: &[String]) -> Vec<String> {
let Some(snapshot) = &self.shell_snapshot else {
return command.to_vec();
};

if command.is_empty() {
return command.to_vec();
}

match self.shell_type {
ShellType::Zsh | ShellType::Bash | ShellType::Sh => {
let mut args = self.derive_exec_args(". \"$0\" && exec \"$@\"", false);
args.push(snapshot.path.to_string_lossy().to_string());
args.extend_from_slice(command);
args
}
ShellType::PowerShell => {
let mut args =
self.derive_exec_args("param($snapshot) . $snapshot; & @args", false);
args.push(snapshot.path.to_string_lossy().to_string());
args.extend_from_slice(command);
args
}
ShellType::Cmd => command.to_vec(),
}
}
}

#[cfg(unix)]
Expand Down Expand Up @@ -134,6 +166,7 @@ fn get_zsh_shell(path: Option<&PathBuf>) -> Option<Shell> {
shell_path.map(|shell_path| Shell {
shell_type: ShellType::Zsh,
shell_path,
shell_snapshot: None,
})
}

Expand All @@ -143,6 +176,7 @@ fn get_bash_shell(path: Option<&PathBuf>) -> Option<Shell> {
shell_path.map(|shell_path| Shell {
shell_type: ShellType::Bash,
shell_path,
shell_snapshot: None,
})
}

Expand All @@ -152,6 +186,7 @@ fn get_sh_shell(path: Option<&PathBuf>) -> Option<Shell> {
shell_path.map(|shell_path| Shell {
shell_type: ShellType::Sh,
shell_path,
shell_snapshot: None,
})
}

Expand All @@ -167,6 +202,7 @@ fn get_powershell_shell(path: Option<&PathBuf>) -> Option<Shell> {
shell_path.map(|shell_path| Shell {
shell_type: ShellType::PowerShell,
shell_path,
shell_snapshot: None,
})
}

Expand All @@ -176,6 +212,7 @@ fn get_cmd_shell(path: Option<&PathBuf>) -> Option<Shell> {
shell_path.map(|shell_path| Shell {
shell_type: ShellType::Cmd,
shell_path,
shell_snapshot: None,
})
}

Expand All @@ -184,11 +221,13 @@ fn ultimate_fallback_shell() -> Shell {
Shell {
shell_type: ShellType::Cmd,
shell_path: PathBuf::from("cmd.exe"),
shell_snapshot: None,
}
} else {
Shell {
shell_type: ShellType::Sh,
shell_path: PathBuf::from("/bin/sh"),
shell_snapshot: None,
}
}
}
Expand Down Expand Up @@ -413,6 +452,7 @@ mod tests {
let test_bash_shell = Shell {
shell_type: ShellType::Bash,
shell_path: PathBuf::from("/bin/bash"),
shell_snapshot: None,
};
assert_eq!(
test_bash_shell.derive_exec_args("echo hello", false),
Expand All @@ -426,6 +466,7 @@ mod tests {
let test_zsh_shell = Shell {
shell_type: ShellType::Zsh,
shell_path: PathBuf::from("/bin/zsh"),
shell_snapshot: None,
};
assert_eq!(
test_zsh_shell.derive_exec_args("echo hello", false),
Expand All @@ -439,6 +480,7 @@ mod tests {
let test_powershell_shell = Shell {
shell_type: ShellType::PowerShell,
shell_path: PathBuf::from("pwsh.exe"),
shell_snapshot: None,
};
assert_eq!(
test_powershell_shell.derive_exec_args("echo hello", false),
Expand All @@ -465,6 +507,7 @@ mod tests {
Shell {
shell_type: ShellType::Zsh,
shell_path: PathBuf::from(shell_path),
shell_snapshot: None,
}
);
}
Expand Down
Loading
Loading