mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
Standardize shell execution on unified exec (#39757)
## What changed - Remove the legacy `shell_command` handler and runtime, leaving `exec_command` and `write_stdin` as the shell execution tools. - Treat legacy `shell_command` model metadata as `unified_exec`, and normalize legacy user opt-outs so they do not disable command execution. Managed feature requirements and `shell_tool` can still disable it. - Preserve shell approvals, sandboxing, zsh-fork support, and output truncation through the unified execution path. ## Testing - Cover legacy configuration and model-metadata compatibility. - Exercise unified shell execution, approvals, truncation, and `apply_patch` serialization across the app-server and core test suites. GitOrigin-RevId: 5c2fd6164fc3519cdae4944cb9db276b8467311c
This commit is contained in:
@@ -1257,7 +1257,7 @@ async fn danger_full_access_tool_attempts_do_not_enforce_managed_network() -> an
|
||||
_req: &TurnEnvironment,
|
||||
call_id: &str,
|
||||
) -> std::io::Result<crate::tools::sandboxing::ApprovalAction> {
|
||||
Ok(crate::tools::sandboxing::ApprovalAction::Shell {
|
||||
Ok(crate::tools::sandboxing::ApprovalAction::ExecCommand {
|
||||
id: call_id.to_string(),
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
command: Vec::new(),
|
||||
@@ -1266,6 +1266,7 @@ async fn danger_full_access_tool_attempts_do_not_enforce_managed_network() -> an
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: None,
|
||||
tty: false,
|
||||
proposed_execpolicy_amendment: None,
|
||||
})
|
||||
}
|
||||
@@ -11089,7 +11090,7 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
|
||||
id: None,
|
||||
status: None,
|
||||
call_id: "call-1".to_string(),
|
||||
name: "shell_command".to_string(),
|
||||
name: "exec_command".to_string(),
|
||||
namespace: None,
|
||||
input: "{}".to_string(),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
@@ -11116,7 +11117,7 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
|
||||
FunctionCallError::Fatal(message) => {
|
||||
assert_eq!(
|
||||
message,
|
||||
"tool shell_command invoked with incompatible payload"
|
||||
"tool exec_command invoked with incompatible payload"
|
||||
);
|
||||
}
|
||||
other => panic!("expected FunctionCallError::Fatal, got {other:?}"),
|
||||
@@ -11300,77 +11301,6 @@ async fn sample_rollout(
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn shell_tool_cancellation_waits_for_runtime_cleanup() -> anyhow::Result<()> {
|
||||
let session = make_session_with_config(|config| {
|
||||
let cwd = config.cwd.clone();
|
||||
config
|
||||
.permissions
|
||||
.set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess, cwd.as_path())
|
||||
.expect("test setup should allow sandbox policy");
|
||||
})
|
||||
.await?;
|
||||
let turn_context = session.new_default_turn().await;
|
||||
let session = Arc::new(session);
|
||||
let turn_context = Arc::new(turn_context);
|
||||
let temp_dir = tempfile::TempDir::new()?;
|
||||
let ready_marker = temp_dir.path().join("ready");
|
||||
let cleanup_marker = temp_dir.path().join("cleanup");
|
||||
// Interrupt after the shell starts, then verify dispatch waits for its TERM cleanup trap.
|
||||
let command = format!(
|
||||
r#"trap 'printf cleaned > "{}"; exit 0' TERM
|
||||
printf ready > "{}"
|
||||
while :; do sleep 1; done"#,
|
||||
cleanup_marker.display(),
|
||||
ready_marker.display(),
|
||||
);
|
||||
let item = ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "shell_command".to_string(),
|
||||
namespace: None,
|
||||
arguments: serde_json::json!({
|
||||
"command": command,
|
||||
"timeout_ms": 60_000,
|
||||
})
|
||||
.to_string(),
|
||||
call_id: "shell-cleanup-call".to_string(),
|
||||
encrypted_function_args: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
let call = ToolRouter::build_tool_call(item)?
|
||||
.expect("shell command response item should build a tool call");
|
||||
let cancellation_token = CancellationToken::new();
|
||||
let cancellation_tx = cancellation_token.clone();
|
||||
let handle = tokio::spawn(
|
||||
test_tool_runtime(Arc::clone(&session), Arc::clone(&turn_context))
|
||||
.handle_tool_call(call, cancellation_token),
|
||||
);
|
||||
|
||||
let mut ready = false;
|
||||
for _ in 0..50 {
|
||||
if ready_marker.exists() {
|
||||
ready = true;
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
}
|
||||
if !ready {
|
||||
cancellation_tx.cancel();
|
||||
let _ = timeout(Duration::from_secs(5), handle).await;
|
||||
anyhow::bail!("shell command should reach the ready marker");
|
||||
}
|
||||
|
||||
cancellation_tx.cancel();
|
||||
timeout(Duration::from_secs(5), handle)
|
||||
.await
|
||||
.expect("cancelled shell tool should finish promptly")
|
||||
.expect("shell tool task should join")
|
||||
.expect("cancelled shell tool should return a response item");
|
||||
assert_eq!(std::fs::read_to_string(cleanup_marker)?, "cleaned");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unified_exec_rejects_escalated_permissions_when_policy_not_on_request() {
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
|
||||
@@ -308,7 +308,7 @@ async fn request_permissions_guardian_review_stops_when_cancelled() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn guardian_allows_shell_command_additional_permissions_requests_past_policy_validation() {
|
||||
async fn guardian_allows_exec_command_additional_permissions_requests_past_policy_validation() {
|
||||
let server = start_mock_server().await;
|
||||
let _request_log = mount_sse_once(
|
||||
&server,
|
||||
@@ -371,11 +371,9 @@ async fn guardian_allows_shell_command_additional_permissions_requests_past_poli
|
||||
);
|
||||
let session = Arc::new(session);
|
||||
let turn_context = Arc::new(turn_context_raw);
|
||||
let expiration_ms: u64 = if cfg!(windows) { 2_500 } else { 1_000 };
|
||||
let yield_time_ms: u64 = 10_000;
|
||||
|
||||
let handler = crate::tools::handlers::ShellCommandHandler::from(
|
||||
codex_tools::ShellCommandBackendConfig::Classic,
|
||||
);
|
||||
let handler = crate::tools::handlers::ExecCommandHandler::default();
|
||||
#[allow(deprecated)]
|
||||
let workdir = Some(turn_context.cwd.to_string_lossy().to_string());
|
||||
let step_context = StepContext::for_test(Arc::clone(&turn_context));
|
||||
@@ -387,14 +385,14 @@ async fn guardian_allows_shell_command_additional_permissions_requests_past_poli
|
||||
cancellation_token: CancellationToken::new(),
|
||||
tracker: Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
|
||||
call_id: "test-call".to_string(),
|
||||
tool_name: codex_tools::ToolName::plain("shell_command"),
|
||||
tool_name: codex_tools::ToolName::plain("exec_command"),
|
||||
source: crate::tools::context::ToolCallSource::Direct,
|
||||
payload: ToolPayload::Function {
|
||||
arguments: serde_json::json!({
|
||||
"command": "echo hi",
|
||||
"cmd": "echo hi",
|
||||
"login": false,
|
||||
"workdir": workdir,
|
||||
"timeout_ms": expiration_ms,
|
||||
"yield_time_ms": yield_time_ms,
|
||||
"sandbox_permissions": SandboxPermissions::WithAdditionalPermissions,
|
||||
"additional_permissions": PermissionProfile {
|
||||
network: Some(NetworkPermissions {
|
||||
@@ -414,7 +412,7 @@ async fn guardian_allows_shell_command_additional_permissions_requests_past_poli
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn strict_auto_review_turn_grant_forces_guardian_for_shell_command_policy_skip() {
|
||||
async fn strict_auto_review_turn_grant_forces_guardian_for_exec_command_policy_skip() {
|
||||
let server = start_mock_server().await;
|
||||
let guardian_request_log = mount_sse_once(
|
||||
&server,
|
||||
@@ -501,9 +499,7 @@ async fn strict_auto_review_turn_grant_forces_guardian_for_shell_command_policy_
|
||||
)
|
||||
.await;
|
||||
|
||||
let handler = crate::tools::handlers::ShellCommandHandler::from(
|
||||
codex_tools::ShellCommandBackendConfig::Classic,
|
||||
);
|
||||
let handler = crate::tools::handlers::ExecCommandHandler::default();
|
||||
#[allow(deprecated)]
|
||||
let workdir = Some(turn_context.cwd.to_string_lossy().to_string());
|
||||
let step_context = StepContext::for_test(Arc::clone(&turn_context));
|
||||
@@ -515,14 +511,14 @@ async fn strict_auto_review_turn_grant_forces_guardian_for_shell_command_policy_
|
||||
cancellation_token: CancellationToken::new(),
|
||||
tracker: Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
|
||||
call_id: "strict-shell-command-call".to_string(),
|
||||
tool_name: codex_tools::ToolName::plain("shell_command"),
|
||||
tool_name: codex_tools::ToolName::plain("exec_command"),
|
||||
source: ToolCallSource::Direct,
|
||||
payload: ToolPayload::Function {
|
||||
arguments: serde_json::json!({
|
||||
"command": "echo hi",
|
||||
"cmd": "echo hi",
|
||||
"login": false,
|
||||
"workdir": workdir,
|
||||
"timeout_ms": 1_000_u64,
|
||||
"yield_time_ms": 10_000_u64,
|
||||
})
|
||||
.to_string(),
|
||||
},
|
||||
@@ -665,7 +661,7 @@ async fn process_compacted_history_preserves_separate_guardian_developer_message
|
||||
clippy::await_holding_invalid_type,
|
||||
reason = "test mutates active turn state directly to seed granted permissions"
|
||||
)]
|
||||
async fn shell_command_allows_sticky_turn_permissions_without_inline_request_permissions_feature() {
|
||||
async fn exec_command_allows_sticky_turn_permissions_without_inline_request_permissions_feature() {
|
||||
let (mut session, turn_context_raw) = make_session_and_context().await;
|
||||
session
|
||||
.features
|
||||
@@ -690,9 +686,7 @@ async fn shell_command_allows_sticky_turn_permissions_without_inline_request_per
|
||||
let session = Arc::new(session);
|
||||
let turn_context = Arc::new(turn_context_raw);
|
||||
|
||||
let handler = crate::tools::handlers::ShellCommandHandler::from(
|
||||
codex_tools::ShellCommandBackendConfig::Classic,
|
||||
);
|
||||
let handler = crate::tools::handlers::ExecCommandHandler::default();
|
||||
#[allow(deprecated)]
|
||||
let workdir = Some(turn_context.cwd.to_string_lossy().to_string());
|
||||
let step_context = StepContext::for_test(Arc::clone(&turn_context));
|
||||
@@ -704,13 +698,13 @@ async fn shell_command_allows_sticky_turn_permissions_without_inline_request_per
|
||||
cancellation_token: CancellationToken::new(),
|
||||
tracker: Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
|
||||
call_id: "sticky-turn-grant".to_string(),
|
||||
tool_name: codex_tools::ToolName::plain("shell_command"),
|
||||
tool_name: codex_tools::ToolName::plain("exec_command"),
|
||||
source: crate::tools::context::ToolCallSource::Direct,
|
||||
payload: ToolPayload::Function {
|
||||
arguments: serde_json::json!({
|
||||
"command": "echo hi",
|
||||
"cmd": "echo hi",
|
||||
"login": false,
|
||||
"timeout_ms": 1_000_u64,
|
||||
"yield_time_ms": 10_000_u64,
|
||||
"workdir": workdir,
|
||||
})
|
||||
.to_string(),
|
||||
|
||||
Reference in New Issue
Block a user