[codex] Use local environment for user shell commands (#28163)

## Why

User shell commands still read the legacy turn cwd and session shell
even though execution context is now owned by selected turn
environments. App-server also defines `thread/shellCommand` as a
local-host escape hatch, so it must use an available local environment
even when a remote environment is primary.

## What changed

- Add `ResolvedTurnEnvironments::local()` to find the selected local
environment.
- Resolve the user shell command cwd and shell from that local
`TurnEnvironment`.
- Emit the standard `shell is unavailable in this session` error when no
selected local environment or resolved local shell is available.
- Add an integration test covering `/shell` without a local environment.

## Test plan

- `just test -p codex-core
user_shell_command_without_local_environment_emits_error`
This commit is contained in:
pakrym-oai
2026-06-15 21:55:20 -07:00
committed by GitHub
Unverified
parent e752f7b4ae
commit 1e015884c5
6 changed files with 101 additions and 30 deletions
+37 -19
View File
@@ -29,6 +29,7 @@ use crate::turn_timing::now_unix_timestamp_ms;
use crate::user_shell_command::user_shell_command_record_item;
use codex_protocol::exec_output::ExecToolCallOutput;
use codex_protocol::exec_output::StreamOutput;
use codex_protocol::protocol::ErrorEvent;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecCommandBeginEvent;
use codex_protocol::protocol::ExecCommandEndEvent;
@@ -124,18 +125,26 @@ pub(crate) async fn execute_user_shell_command(
session.send_event(turn_context.as_ref(), event).await;
}
// Execute the user's script under their default shell when known; this
let Some((turn_environment, environment_shell)) = turn_context
.environments
.local()
.and_then(|environment| environment.shell.as_ref().map(|shell| (environment, shell)))
else {
send_user_shell_error(
&session,
turn_context.as_ref(),
"shell is unavailable in this session",
)
.await;
return;
};
// Execute the user's script under the environment's shell; this
// allows commands that use shell features (pipes, &&, redirects, etc.).
// We do not source rc files or otherwise reformat the script.
let use_login_shell = true;
let session_shell = session.user_shell();
let environment = turn_context.environments.single_local_environment();
let shell = environment
.and_then(|environment| environment.shell.as_ref())
.unwrap_or(session_shell.as_ref());
let shell_snapshot_location =
environment.and_then(|environment| environment.shell_snapshot(environment.cwd()));
let display_command = shell.derive_exec_args(&command, use_login_shell);
let display_command = environment_shell.derive_exec_args(&command, use_login_shell);
let shell_snapshot_location = turn_environment.shell_snapshot(turn_environment.cwd());
let mut exec_env_map = create_env(
&turn_context.shell_environment_policy,
Some(session.thread_id),
@@ -145,7 +154,7 @@ pub(crate) async fn execute_user_shell_command(
}
let exec_command = prepare_user_shell_exec_command(
&display_command,
shell,
environment_shell,
shell_snapshot_location.as_ref(),
&turn_context.shell_environment_policy.r#set,
&mut exec_env_map,
@@ -153,10 +162,7 @@ pub(crate) async fn execute_user_shell_command(
let call_id = Uuid::new_v4().to_string();
let raw_command = command;
#[allow(deprecated)]
let cwd = environment
.map(|environment| environment.cwd().clone())
.unwrap_or_else(|| turn_context.cwd.clone());
let cwd = turn_environment.cwd().clone();
let parsed_cmd = parse_command(&display_command);
session
@@ -341,9 +347,21 @@ pub(crate) async fn execute_user_shell_command(
}
}
async fn send_user_shell_error(session: &Session, turn_context: &TurnContext, message: &str) {
session
.send_event(
turn_context,
EventMsg::Error(ErrorEvent {
message: message.to_string(),
codex_error_info: None,
}),
)
.await;
}
fn prepare_user_shell_exec_command(
display_command: &[String],
session_shell: &Shell,
shell: &Shell,
shell_snapshot: Option<&AbsolutePathBuf>,
shell_environment_set: &HashMap<String, String>,
exec_env_map: &mut HashMap<String, String>,
@@ -352,7 +370,7 @@ fn prepare_user_shell_exec_command(
{
prepare_user_shell_exec_command_with_path_prepend(
display_command,
session_shell,
shell,
shell_snapshot,
shell_environment_set,
exec_env_map,
@@ -364,7 +382,7 @@ fn prepare_user_shell_exec_command(
{
maybe_wrap_shell_lc_with_snapshot(
display_command,
session_shell,
shell,
shell_snapshot,
shell_environment_set,
exec_env_map,
@@ -384,7 +402,7 @@ fn prepare_user_shell_exec_command(
#[cfg(unix)]
fn prepare_user_shell_exec_command_with_path_prepend(
display_command: &[String],
session_shell: &Shell,
shell: &Shell,
shell_snapshot: Option<&AbsolutePathBuf>,
shell_environment_set: &HashMap<String, String>,
exec_env_map: &mut HashMap<String, String>,
@@ -395,7 +413,7 @@ fn prepare_user_shell_exec_command_with_path_prepend(
prepend_runtime_path(exec_env_map, &mut runtime_path_prepends);
maybe_wrap_shell_lc_with_snapshot(
display_command,
session_shell,
shell,
shell_snapshot,
&explicit_env_overrides,
exec_env_map,