[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-16 04:55:20 +00:00
committed by GitHub
parent e752f7b4ae
commit 1e015884c5
6 changed files with 101 additions and 30 deletions
@@ -21,6 +21,7 @@ use core_test_support::responses::mount_sse_once;
use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network;
use core_test_support::submit_thread_settings;
use core_test_support::test_codex::local_selections;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields;
@@ -99,6 +100,40 @@ async fn user_shell_cmd_ls_and_cat_in_temp_dir() {
assert_eq!(stdout, contents);
}
#[tokio::test]
async fn user_shell_command_without_local_environment_emits_error() -> anyhow::Result<()> {
let server = start_mock_server().await;
let mut builder = test_codex();
let test = builder.build(&server).await?;
submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(codex_protocol::protocol::TurnEnvironmentSelections::new(
test.config.cwd.clone(),
vec![],
)),
..Default::default()
},
)
.await?;
test.codex
.submit(Op::RunUserShellCommand {
command: "echo shell".to_string(),
})
.await?;
let EventMsg::Error(error) =
wait_for_event(&test.codex, |event| matches!(event, EventMsg::Error(_))).await
else {
unreachable!()
};
assert_eq!(error.message, "shell is unavailable in this session");
assert_eq!(error.codex_error_info, None);
Ok(())
}
#[tokio::test]
async fn user_shell_cmd_can_be_interrupted() {
// Set up isolated config and conversation.