mirror of
https://github.com/openai/codex.git
synced 2026-09-17 12:23:33 +00:00
feature: Add "!cmd" user shell execution
- protocol: add Op::RunUserShellCommand to model a user-initiated one-off command - core: handle new Op by spawning a cancellable task that runs the command using the user’s default shell; stream output via ExecCommand* events; send TaskStarted/TaskComplete; track as current_task so Interrupt works - tui: detect leading '!' in composer submission and dispatch Op::RunUserShellCommand instead of sending a user message No changes to sandbox env var behavior; uses existing exec pipeline and event types.
This commit is contained in:
@@ -135,6 +135,7 @@ use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::models::ShellToolCallParams;
|
||||
use codex_protocol::protocol::InitialHistory;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod compact;
|
||||
use self::compact::build_compacted_history;
|
||||
@@ -812,6 +813,7 @@ impl Session {
|
||||
command_for_display,
|
||||
cwd,
|
||||
apply_patch,
|
||||
is_user_shell_command,
|
||||
} = exec_command_context;
|
||||
let msg = match apply_patch {
|
||||
Some(ApplyPatchCommandContext {
|
||||
@@ -834,6 +836,7 @@ impl Session {
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
is_user_shell_command,
|
||||
}),
|
||||
};
|
||||
let event = Event {
|
||||
@@ -1063,6 +1066,7 @@ pub(crate) struct ExecCommandContext {
|
||||
pub(crate) command_for_display: Vec<String>,
|
||||
pub(crate) cwd: PathBuf,
|
||||
pub(crate) apply_patch: Option<ApplyPatchCommandContext>,
|
||||
pub(crate) is_user_shell_command: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -1518,6 +1522,9 @@ async fn submission_loop(
|
||||
};
|
||||
sess.send_event(event).await;
|
||||
}
|
||||
Op::RunUserShellCommand { command } => {
|
||||
spawn_user_shell_command_task(sess.clone(), &turn_context, sub.id, command).await;
|
||||
}
|
||||
Op::Review { review_request } => {
|
||||
spawn_review_thread(
|
||||
sess.clone(),
|
||||
@@ -1536,6 +1543,101 @@ async fn submission_loop(
|
||||
debug!("Agent loop exited");
|
||||
}
|
||||
|
||||
async fn spawn_user_shell_command_task(
|
||||
sess: Arc<Session>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
sub_id: String,
|
||||
command: String,
|
||||
) {
|
||||
let handle = {
|
||||
let sess = sess.clone();
|
||||
let turn_context = turn_context.clone();
|
||||
let spawn_sub_id = sub_id.clone();
|
||||
tokio::spawn(async move {
|
||||
run_user_shell_command(sess, turn_context, spawn_sub_id, command).await;
|
||||
})
|
||||
.abort_handle()
|
||||
};
|
||||
|
||||
sess.set_task(AgentTask {
|
||||
sess: sess.clone(),
|
||||
sub_id,
|
||||
handle,
|
||||
kind: AgentTaskKind::Regular,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn run_user_shell_command(
|
||||
sess: 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(),
|
||||
}),
|
||||
};
|
||||
sess.send_event(event).await;
|
||||
|
||||
let shell_invocation = sess
|
||||
.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,
|
||||
};
|
||||
|
||||
let mut turn_diff_tracker = TurnDiffTracker::new();
|
||||
let call_id = Uuid::new_v4().to_string();
|
||||
|
||||
let exec_ctx = ExecCommandContext {
|
||||
sub_id: sub_id.clone(),
|
||||
call_id: call_id.clone(),
|
||||
command_for_display: shell_invocation,
|
||||
cwd: params.cwd.clone(),
|
||||
apply_patch: None,
|
||||
is_user_shell_command: true,
|
||||
};
|
||||
|
||||
let sandbox_policy = SandboxPolicy::DangerFullAccess;
|
||||
|
||||
let _ = sess
|
||||
.run_exec_with_events(
|
||||
&mut turn_diff_tracker,
|
||||
exec_ctx,
|
||||
ExecInvokeArgs {
|
||||
params,
|
||||
sandbox_type: SandboxType::None,
|
||||
sandbox_policy: &sandbox_policy,
|
||||
sandbox_cwd: &turn_context.cwd,
|
||||
codex_linux_sandbox_exe: &sess.codex_linux_sandbox_exe,
|
||||
stdout_stream: Some(StdoutStream {
|
||||
sub_id: sub_id.clone(),
|
||||
call_id: call_id.clone(),
|
||||
tx_event: sess.tx_event.clone(),
|
||||
}),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let complete = Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::TaskComplete(TaskCompleteEvent {
|
||||
last_agent_message: None,
|
||||
}),
|
||||
};
|
||||
sess.send_event(complete).await;
|
||||
}
|
||||
|
||||
/// Spawn a review thread using the given prompt.
|
||||
async fn spawn_review_thread(
|
||||
sess: Arc<Session>,
|
||||
@@ -2800,6 +2902,7 @@ async fn handle_container_exec_with_params(
|
||||
changes: convert_apply_patch_to_protocol(&action),
|
||||
},
|
||||
),
|
||||
is_user_shell_command: false,
|
||||
};
|
||||
|
||||
let params = maybe_translate_shell_command(params, sess, turn_context);
|
||||
|
||||
@@ -30,6 +30,28 @@ 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 => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_default_shell_invocation(&self, command: Vec<String>) -> Option<Vec<String>> {
|
||||
match self {
|
||||
Shell::Zsh(zsh) => format_shell_invocation_with_rc(
|
||||
@@ -113,13 +135,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> {
|
||||
@@ -135,6 +151,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;
|
||||
|
||||
Reference in New Issue
Block a user