mirror of
https://github.com/openai/codex.git
synced 2026-11-09 00:39:32 +00:00
Gate unified exec TTY support behind a feature flag (#42718)
## What changed - Add the stable, default-on `unified_exec_tty` feature. - When disabled, omit `tty` from the `exec_command` tool schema and reject calls that explicitly request a TTY. - Continue to support commands that omit `tty` or set it to `false`. ## Testing - Add unified exec coverage for the disabled tool schema, explicit TTY rejection, and non-TTY command execution. GitOrigin-RevId: c5934fe9db1423170b891c7ecd0188792cd03f42
This commit is contained in:
1 parent
d13aeb77ea
commit
3c837e568c
6 files changed
+131
-1
No files matched your search
@@ -1020,6 +1020,9 @@
|
|||||||
"unified_exec": {
|
"unified_exec": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"unified_exec_tty": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"unified_exec_zsh_fork": {
|
"unified_exec_zsh_fork": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@@ -6378,6 +6381,9 @@
|
|||||||
"unified_exec": {
|
"unified_exec": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"unified_exec_tty": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"unified_exec_zsh_fork": {
|
"unified_exec_zsh_fork": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ const EXEC_COMMAND_REJECTION_MAX_BYTES: usize = 900;
|
|||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub(crate) struct ExecCommandHandlerOptions {
|
pub(crate) struct ExecCommandHandlerOptions {
|
||||||
pub(crate) allow_login_shell: bool,
|
pub(crate) allow_login_shell: bool,
|
||||||
|
pub(crate) allow_tty: bool,
|
||||||
pub(crate) exec_permission_approvals_enabled: bool,
|
pub(crate) exec_permission_approvals_enabled: bool,
|
||||||
pub(crate) include_environment_id: bool,
|
pub(crate) include_environment_id: bool,
|
||||||
pub(crate) include_shell_parameter: bool,
|
pub(crate) include_shell_parameter: bool,
|
||||||
@@ -82,6 +83,7 @@ impl Default for ExecCommandHandler {
|
|||||||
lifetime: ExecCommandLifetime::Interactive,
|
lifetime: ExecCommandLifetime::Interactive,
|
||||||
options: ExecCommandHandlerOptions {
|
options: ExecCommandHandlerOptions {
|
||||||
allow_login_shell: false,
|
allow_login_shell: false,
|
||||||
|
allow_tty: true,
|
||||||
exec_permission_approvals_enabled: false,
|
exec_permission_approvals_enabled: false,
|
||||||
include_environment_id: false,
|
include_environment_id: false,
|
||||||
include_shell_parameter: true,
|
include_shell_parameter: true,
|
||||||
@@ -122,10 +124,19 @@ impl ToolExecutor<ToolInvocation> for ExecCommandHandler {
|
|||||||
self.options.include_shell_parameter,
|
self.options.include_shell_parameter,
|
||||||
self.options.include_windows_shell_guidance,
|
self.options.include_windows_shell_guidance,
|
||||||
);
|
);
|
||||||
match self.lifetime {
|
let mut spec = match self.lifetime {
|
||||||
ExecCommandLifetime::Interactive => spec,
|
ExecCommandLifetime::Interactive => spec,
|
||||||
ExecCommandLifetime::OneShot => one_shot_exec_command_spec(spec),
|
ExecCommandLifetime::OneShot => one_shot_exec_command_spec(spec),
|
||||||
|
};
|
||||||
|
if !self.options.allow_tty
|
||||||
|
&& let ToolSpec::Function(spec) = &mut spec
|
||||||
|
{
|
||||||
|
spec.parameters
|
||||||
|
.properties
|
||||||
|
.get_or_insert_default()
|
||||||
|
.remove("tty");
|
||||||
}
|
}
|
||||||
|
spec
|
||||||
}
|
}
|
||||||
|
|
||||||
fn supports_parallel_tool_calls(&self) -> bool {
|
fn supports_parallel_tool_calls(&self) -> bool {
|
||||||
@@ -230,6 +241,11 @@ impl ExecCommandHandler {
|
|||||||
parse_arguments(&arguments)?
|
parse_arguments(&arguments)?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if args.tty && !session.features().enabled(Feature::UnifiedExecTty) {
|
||||||
|
return Err(FunctionCallError::RespondToModel(
|
||||||
|
"TTY execution is disabled by config; omit `tty` or set it to false.".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
let sandbox_permissions =
|
let sandbox_permissions =
|
||||||
resolve_sandbox_permissions(args.sandbox_permissions, args.justification.as_deref())?;
|
resolve_sandbox_permissions(args.sandbox_permissions, args.justification.as_deref())?;
|
||||||
let hook_command = args.cmd.clone();
|
let hook_command = args.cmd.clone();
|
||||||
|
|||||||
@@ -997,6 +997,10 @@ fn add_core_tool_sources(context: &CoreToolPlanContext<'_>, registry: &mut ToolR
|
|||||||
{
|
{
|
||||||
registry.add(ExecCommandHandler::new(ExecCommandHandlerOptions {
|
registry.add(ExecCommandHandler::new(ExecCommandHandlerOptions {
|
||||||
allow_login_shell: any_environment_allows_login_shell(context.environments),
|
allow_login_shell: any_environment_allows_login_shell(context.environments),
|
||||||
|
allow_tty: turn_context
|
||||||
|
.config
|
||||||
|
.features
|
||||||
|
.enabled(Feature::UnifiedExecTty),
|
||||||
exec_permission_approvals_enabled: false,
|
exec_permission_approvals_enabled: false,
|
||||||
include_environment_id,
|
include_environment_id,
|
||||||
include_shell_parameter: unified_exec_should_include_shell_parameter(
|
include_shell_parameter: unified_exec_should_include_shell_parameter(
|
||||||
@@ -1088,6 +1092,7 @@ fn add_shell_tools(context: &CoreToolPlanContext<'_>, registry: &mut ToolRegistr
|
|||||||
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
|
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
|
||||||
let options = ExecCommandHandlerOptions {
|
let options = ExecCommandHandlerOptions {
|
||||||
allow_login_shell,
|
allow_login_shell,
|
||||||
|
allow_tty: features.enabled(Feature::UnifiedExecTty),
|
||||||
exec_permission_approvals_enabled,
|
exec_permission_approvals_enabled,
|
||||||
include_environment_id,
|
include_environment_id,
|
||||||
include_shell_parameter: unified_exec_should_include_shell_parameter(
|
include_shell_parameter: unified_exec_should_include_shell_parameter(
|
||||||
|
|||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
source: core/tests/suite/unified_exec.rs
|
||||||
|
expression: rejection
|
||||||
|
---
|
||||||
|
TTY execution is disabled by config; omit `tty` or set it to false.
|
||||||
@@ -306,6 +306,96 @@ async fn exec_command_hides_and_rejects_login_when_disabled() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn exec_command_hides_and_rejects_tty_when_disabled() -> Result<()> {
|
||||||
|
skip_if_no_network!(Ok(()));
|
||||||
|
|
||||||
|
let builder = test_codex().with_model("gpt-5.4").with_cloud_config_bundle(
|
||||||
|
CloudConfigBundleFixture::loader_with_enterprise_requirement(
|
||||||
|
"[features]\nunified_exec_tty = false\n",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let harness = TestCodexHarness::with_auto_env_builder(builder).await?;
|
||||||
|
let call_id = "tty-denied";
|
||||||
|
let arguments = json!({"cmd": "echo should-not-run > tty-disabled-rejected", "tty": true});
|
||||||
|
let request_log = mount_sse_sequence(
|
||||||
|
harness.server(),
|
||||||
|
vec![
|
||||||
|
sse(vec![
|
||||||
|
ev_response_created(call_id),
|
||||||
|
ev_function_call(call_id, "exec_command", &serde_json::to_string(&arguments)?),
|
||||||
|
ev_completed(call_id),
|
||||||
|
]),
|
||||||
|
sse(vec![ev_completed("done")]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
harness
|
||||||
|
.submit("run commands with and without a terminal")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let rejection = harness.function_call_stdout("tty-denied").await;
|
||||||
|
insta::assert_snapshot!("exec_command_tty_disabled", rejection);
|
||||||
|
assert!(!harness.path_exists("tty-disabled-rejected").await?);
|
||||||
|
|
||||||
|
let request = request_log.requests()[0].body_json();
|
||||||
|
let tools = request["tools"]
|
||||||
|
.as_array()
|
||||||
|
.expect("tools should be an array");
|
||||||
|
let exec_tool = tools
|
||||||
|
.iter()
|
||||||
|
.find(|tool| tool["name"] == "exec_command")
|
||||||
|
.expect("exec_command should be available");
|
||||||
|
assert!(exec_tool["parameters"]["properties"].get("tty").is_none());
|
||||||
|
assert!(tools.iter().any(|tool| tool["name"] == "write_stdin"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn exec_command_runs_without_tty_when_tty_disabled() -> Result<()> {
|
||||||
|
skip_if_no_network!(Ok(()));
|
||||||
|
skip_if_wine_exec!(
|
||||||
|
Ok(()),
|
||||||
|
"basic PowerShell execution through Wine is unavailable"
|
||||||
|
);
|
||||||
|
|
||||||
|
let builder = test_codex().with_model("gpt-5.4").with_cloud_config_bundle(
|
||||||
|
CloudConfigBundleFixture::loader_with_enterprise_requirement(
|
||||||
|
"[features]\nunified_exec_tty = false\n",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let harness = TestCodexHarness::with_auto_env_builder(builder).await?;
|
||||||
|
let mut responses = Vec::new();
|
||||||
|
for (call_id, arguments) in [
|
||||||
|
("tty-false", json!({"cmd": "echo pipe-ok", "tty": false})),
|
||||||
|
("tty-omitted", json!({"cmd": "echo pipe-ok"})),
|
||||||
|
] {
|
||||||
|
responses.push(sse(vec![
|
||||||
|
ev_response_created(call_id),
|
||||||
|
ev_function_call(call_id, "exec_command", &serde_json::to_string(&arguments)?),
|
||||||
|
ev_completed(call_id),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
responses.push(sse(vec![ev_completed("done")]));
|
||||||
|
mount_sse_sequence(harness.server(), responses).await;
|
||||||
|
|
||||||
|
harness
|
||||||
|
.submit("run commands with and without a terminal")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for call_id in ["tty-false", "tty-omitted"] {
|
||||||
|
let output = parse_unified_exec_output(&harness.function_call_stdout(call_id).await)?;
|
||||||
|
assert_eq!(
|
||||||
|
(output.exit_code, output.output.trim()),
|
||||||
|
(Some(0), "pipe-ok")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn exec_command_does_not_expose_configured_noise_auth_token() -> Result<()> {
|
async fn exec_command_does_not_expose_configured_noise_auth_token() -> Result<()> {
|
||||||
skip_if_no_network!(Ok(()));
|
skip_if_no_network!(Ok(()));
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ pub enum Feature {
|
|||||||
CodeModeOnly,
|
CodeModeOnly,
|
||||||
/// Use the single unified PTY-backed exec tool.
|
/// Use the single unified PTY-backed exec tool.
|
||||||
UnifiedExec,
|
UnifiedExec,
|
||||||
|
/// Allow unified exec commands to allocate an interactive terminal.
|
||||||
|
UnifiedExecTty,
|
||||||
/// Route shell tool execution through the zsh exec bridge.
|
/// Route shell tool execution through the zsh exec bridge.
|
||||||
ShellZshFork,
|
ShellZshFork,
|
||||||
/// Allow unified exec to compose with the zsh exec bridge.
|
/// Allow unified exec to compose with the zsh exec bridge.
|
||||||
@@ -932,6 +934,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
|||||||
stage: Stage::Stable,
|
stage: Stage::Stable,
|
||||||
default_enabled: true,
|
default_enabled: true,
|
||||||
},
|
},
|
||||||
|
FeatureSpec {
|
||||||
|
id: Feature::UnifiedExecTty,
|
||||||
|
key: "unified_exec_tty",
|
||||||
|
stage: Stage::Stable,
|
||||||
|
default_enabled: true,
|
||||||
|
},
|
||||||
FeatureSpec {
|
FeatureSpec {
|
||||||
id: Feature::ShellZshFork,
|
id: Feature::ShellZshFork,
|
||||||
key: "shell_zsh_fork",
|
key: "shell_zsh_fork",
|
||||||
|
|||||||
Reference in new issue
Block a user