Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
133 changes: 130 additions & 3 deletions codex-rs/core/src/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::terminal;
use crate::user_notification::UserNotifier;
use async_channel::Receiver;
use async_channel::Sender;
use async_trait::async_trait;
use codex_apply_patch::ApplyPatchAction;
use codex_protocol::ConversationId;
use codex_protocol::protocol::ConversationPathResponseEvent;
Expand Down Expand Up @@ -55,14 +56,19 @@ use crate::conversation_history::ConversationHistory;
use crate::environment_context::EnvironmentContext;
use crate::error::CodexErr;
use crate::error::Result as CodexResult;
use crate::exec::ExecParams;
use crate::exec::ExecToolCallOutput;
use crate::exec::StdoutStream;
#[cfg(test)]
use crate::exec::StreamOutput;
use crate::exec_command::ExecCommandParams;
use crate::exec_command::ExecSessionManager;
use crate::exec_command::WriteStdinParams;
use crate::exec_env::create_env;
use crate::executor::ExecutionMode;
use crate::executor::Executor;
use crate::executor::ExecutorConfig;
use crate::executor::linkers::PreparedExec;
use crate::executor::normalize_exec_result;
use crate::mcp::auth::compute_auth_statuses;
use crate::mcp_connection_manager::McpConnectionManager;
Expand Down Expand Up @@ -110,7 +116,11 @@ use crate::state::TaskKind;
use crate::tasks::CompactTask;
use crate::tasks::RegularTask;
use crate::tasks::ReviewTask;
use crate::tasks::SessionTask;
use crate::tasks::SessionTaskContext;
use crate::tools::ToolRouter;
use crate::tools::context::ApplyPatchCommandContext;
use crate::tools::context::ExecCommandContext;
use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::format_exec_output_str;
use crate::tools::parallel::ToolCallRuntime;
Expand All @@ -128,6 +138,7 @@ use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::InitialHistory;
use uuid::Uuid;

pub mod compact;
use self::compact::build_compacted_history;
Expand All @@ -151,6 +162,7 @@ pub struct CodexSpawnOk {

pub(crate) const INITIAL_SUBMIT_ID: &str = "";
pub(crate) const SUBMISSION_CHANNEL_CAPACITY: usize = 64;
const USER_SHELL_TOOL_NAME: &str = "user_shell";

impl Codex {
/// Spawn a new [`Codex`] and initialize the session.
Expand Down Expand Up @@ -869,6 +881,7 @@ impl Session {
command_for_display,
cwd,
apply_patch,
is_user_shell_command,
..
} = exec_command_context;
let msg = match apply_patch {
Expand All @@ -892,6 +905,7 @@ impl Session {
command: command_for_display.clone(),
cwd,
parsed_cmd: parse_command(&command_for_display),
is_user_shell_command,
}),
};
let event = Event {
Expand Down Expand Up @@ -1177,6 +1191,21 @@ impl Session {
&self.services.user_shell
}

pub async fn spawn_user_shell_command(
self: &Arc<Self>,
turn_context: Arc<TurnContext>,
sub_id: String,
command: String,
) {
self.spawn_task(
Arc::clone(&turn_context),
sub_id,
Vec::new(),
UserShellCommandTask::new(command),
)
.await;
}

fn show_raw_agent_reasoning(&self) -> bool {
self.services.show_raw_agent_reasoning
}
Expand Down Expand Up @@ -1511,6 +1540,10 @@ async fn submission_loop(
.await;
}
}
Op::RunUserShellCommand { command } => {
sess.spawn_user_shell_command(Arc::clone(&turn_context), sub.id, command)
.await;
}
Op::Shutdown => {
sess.abort_all_tasks(TurnAbortReason::Interrupted).await;
info!("Shutting down Codex instance");
Expand Down Expand Up @@ -1586,6 +1619,103 @@ async fn submission_loop(
debug!("Agent loop exited");
}

#[derive(Clone)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is a task, can you move it into the task module?

struct UserShellCommandTask {
command: String,
}

impl UserShellCommandTask {
fn new(command: String) -> Self {
Self { command }
}
}

#[async_trait]
impl SessionTask for UserShellCommandTask {
fn kind(&self) -> TaskKind {
TaskKind::Regular
}

async fn run(
self: Arc<Self>,
session: Arc<SessionTaskContext>,
turn_context: Arc<TurnContext>,
sub_id: String,
_input: Vec<InputItem>,
) -> Option<String> {
let session = session.clone_session();
run_user_shell_command(session, turn_context, sub_id, self.command.clone()).await;
None
}
}

async fn run_user_shell_command(
session: Arc<Session>,
turn_context: Arc<TurnContext>,
sub_id: String,
command: String,
) {
let event = Event {
id: sub_id.clone(),
msg: EventMsg::TaskStarted(TaskStartedEvent {
model_context_window: turn_context.client.get_model_context_window(),
}),
};
session.send_event(event).await;

let shell_invocation = session
.user_shell()
.format_user_shell_script(&command)
.unwrap_or_else(|| vec![command.clone()]);

let params = ExecParams {
command: shell_invocation.clone(),
cwd: turn_context.cwd.clone(),
timeout_ms: None,
env: create_env(&turn_context.shell_environment_policy),
with_escalated_permissions: None,
justification: None,
};

session
.services
.executor
.update_environment(SandboxPolicy::DangerFullAccess, turn_context.cwd.clone());

let call_id = Uuid::new_v4().to_string();
let otel_event_manager = turn_context.client.get_otel_event_manager();

let exec_context = ExecCommandContext {
sub_id: sub_id.clone(),
call_id: call_id.clone(),
command_for_display: shell_invocation.clone(),
cwd: params.cwd.clone(),
apply_patch: None,
tool_name: USER_SHELL_TOOL_NAME.to_string(),
otel_event_manager,
is_user_shell_command: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be necessary if we have the tool_name

};

let prepared_exec = PreparedExec::new(
exec_context,
params,
shell_invocation,
ExecutionMode::Shell,
Some(StdoutStream {
sub_id: sub_id.clone(),
call_id: call_id.clone(),
tx_event: session.get_tx_event(),
}),
turn_context.shell_environment_policy.use_profile,
);

let turn_diff_tracker = Arc::new(Mutex::new(TurnDiffTracker::new()));

let _ = session
.run_exec_with_events(turn_diff_tracker, prepared_exec, AskForApproval::Never)
.await;
}

/// Spawn a review thread using the given prompt.
async fn spawn_review_thread(
sess: Arc<Session>,
Expand Down Expand Up @@ -2511,9 +2641,6 @@ pub(crate) async fn exit_review_mode(
}

use crate::executor::errors::ExecError;
use crate::executor::linkers::PreparedExec;
use crate::tools::context::ApplyPatchCommandContext;
use crate::tools::context::ExecCommandContext;
#[cfg(test)]
pub(crate) use tests::make_session_and_context;

Expand Down
61 changes: 54 additions & 7 deletions codex-rs/core/src/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,35 @@ pub enum Shell {
}

impl Shell {
pub fn format_user_shell_script(&self, script: &str) -> Option<Vec<String>> {
match self {
Shell::Zsh(zsh) => Some(format_shell_script_with_rc(
&zsh.shell_path,
&zsh.zshrc_path,
script,
)),
Shell::Bash(bash) => Some(format_shell_script_with_rc(
&bash.shell_path,
&bash.bashrc_path,
script,
)),
Shell::PowerShell(ps) => Some(vec![
ps.exe.clone(),
"-NoProfile".to_string(),
"-Command".to_string(),
script.to_string(),
]),
Shell::Unknown => {
// In CI we sometimes cannot determine the user's default shell (e.g. musl builds
// running under minimal passwd entries). Returning `None` here would make callers
// treat the entire script as a single argv[0], which prevents simple commands like
// `python3 -c "..."` from spawning. Instead, fall back to a POSIX-style split so we
// still run straightforward commands even without shell discovery.
shlex::split(script)
}
}
}

pub fn format_default_shell_invocation(&self, command: Vec<String>) -> Option<Vec<String>> {
match self {
Shell::Zsh(zsh) => format_shell_invocation_with_rc(
Expand Down Expand Up @@ -113,13 +142,7 @@ fn format_shell_invocation_with_rc(
let joined = strip_bash_lc(command)
.or_else(|| shlex::try_join(command.iter().map(String::as_str)).ok())?;

let rc_command = if std::path::Path::new(rc_path).exists() {
format!("source {rc_path} && ({joined})")
} else {
joined
};

Some(vec![shell_path.to_string(), "-lc".to_string(), rc_command])
Some(format_shell_script_with_rc(shell_path, rc_path, &joined))
}

fn strip_bash_lc(command: &[String]) -> Option<String> {
Expand All @@ -135,6 +158,16 @@ fn strip_bash_lc(command: &[String]) -> Option<String> {
}
}

fn format_shell_script_with_rc(shell_path: &str, rc_path: &str, script: &str) -> Vec<String> {
let rc_command = if std::path::Path::new(rc_path).exists() {
format!("source {rc_path} && ({script})")
} else {
script.to_string()
};

vec![shell_path.to_string(), "-lc".to_string(), rc_command]
}

#[cfg(unix)]
fn detect_default_user_shell() -> Shell {
use libc::getpwuid;
Expand Down Expand Up @@ -366,6 +399,20 @@ mod tests {
}
}
}

#[test]
fn format_user_shell_script_unknown_splits_command() {
let script = "python3 -c \"print('hi')\"";
let invocation = Shell::Unknown.format_user_shell_script(script);
assert_eq!(
invocation,
Some(vec![
"python3".to_string(),
"-c".to_string(),
"print('hi')".to_string(),
])
);
}
}

#[cfg(test)]
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/tools/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ pub(crate) struct ExecCommandContext {
pub(crate) apply_patch: Option<ApplyPatchCommandContext>,
pub(crate) tool_name: String,
pub(crate) otel_event_manager: OtelEventManager,
pub(crate) is_user_shell_command: bool,
}

#[derive(Clone, Debug)]
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ pub(crate) async fn handle_container_exec_with_params(
),
tool_name: tool_name.to_string(),
otel_event_manager,
is_user_shell_command: false,
};

let mode = match apply_patch_exec {
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/tests/suite/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ mod tool_parallelism;
mod tools;
mod unified_exec;
mod user_notification;
mod user_shell_cmd;
mod view_image;
Loading
Loading