shell: reject foreign environments before host execution

This commit is contained in:
Adam Perry
2026-06-13 06:26:43 +00:00
parent 4d05d4e172
commit f5dfe0cb26
2 changed files with 79 additions and 0 deletions
@@ -166,6 +166,15 @@ impl ShellCommandHandler {
"unsupported payload for shell_command handler: {tool_name}"
)));
};
if turn
.environments
.primary()
.is_some_and(|environment| environment.compatible_cwd().is_none())
{
return Err(FunctionCallError::RespondToModel(
"shell_command does not support cross-platform execution environments".to_string(),
));
}
#[allow(deprecated)]
let cwd = resolve_workdir_base_path(&arguments, &turn.cwd)?;
@@ -2,6 +2,9 @@ use std::path::PathBuf;
use std::sync::Arc;
use codex_protocol::models::ShellCommandToolCallParams;
use codex_tools::ToolExecutor;
use codex_utils_path_uri::PathConvention;
use codex_utils_path_uri::PathUri;
use core_test_support::PathBufExt;
use core_test_support::test_path_buf;
use pretty_assertions::assert_eq;
@@ -9,6 +12,7 @@ use pretty_assertions::assert_eq;
use crate::exec_env::create_env;
use crate::sandboxing::SandboxPermissions;
use crate::session::tests::make_session_and_context;
use crate::session::turn_context::TurnEnvironment;
use crate::shell::Shell;
use crate::shell::ShellType;
use crate::shell_snapshot::ShellSnapshot;
@@ -190,6 +194,72 @@ async fn shell_command_handler_defaults_to_non_login_when_disallowed() {
);
}
#[tokio::test]
async fn shell_command_handler_rejects_foreign_environment_before_host_execution() {
let (session, mut turn) = make_session_and_context().await;
let selected = turn.environments.turn_environments[0].clone();
let (environment_id, cwd, path_convention, shell) = match PathConvention::native() {
PathConvention::Posix => (
"windows",
PathUri::parse("file:///C:/workspace").expect("Windows cwd URI"),
PathConvention::Windows,
Shell {
shell_type: ShellType::PowerShell,
shell_path: PathBuf::from(r"C:\Program Files\PowerShell\7\pwsh.exe"),
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
},
),
PathConvention::Windows => (
"posix",
PathUri::parse("file:///workspace").expect("POSIX cwd URI"),
PathConvention::Posix,
Shell {
shell_type: ShellType::Sh,
shell_path: PathBuf::from("/bin/sh"),
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
},
),
};
turn.environments.turn_environments[0] = TurnEnvironment::new_with_uri(
environment_id.to_string(),
selected.environment,
cwd,
path_convention,
shell,
)
.expect("foreign environment");
let marker_dir = tempfile::tempdir().expect("marker directory");
let marker = marker_dir.path().join("should-not-exist");
let handler = ShellCommandHandler::from(codex_tools::ShellCommandBackendConfig::Classic);
let result = handler
.handle(ToolInvocation {
session: Arc::new(session),
turn: Arc::new(turn),
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
call_id: "foreign-shell".to_string(),
tool_name: codex_tools::ToolName::plain("shell_command"),
source: ToolCallSource::Direct,
payload: ToolPayload::Function {
arguments: json!({
"command": format!("touch {}", marker.display()),
})
.to_string(),
},
})
.await;
let Err(crate::function_tool::FunctionCallError::RespondToModel(message)) = result else {
panic!("expected cross-platform shell rejection");
};
assert_eq!(
message,
"shell_command does not support cross-platform execution environments"
);
assert!(!marker.exists());
}
#[test]
fn shell_command_handler_rejects_login_when_disallowed() {
let err =