feat: add config allow_login_shell (#12312)
This commit is contained in:
parent
67e802e26b
commit
5034d4bd89
9 changed files with 244 additions and 56 deletions
|
|
@ -1392,6 +1392,10 @@
|
|||
],
|
||||
"description": "Agent-related settings (thread limits, etc.)."
|
||||
},
|
||||
"allow_login_shell": {
|
||||
"description": "Whether the model may request a login shell for shell-based tools. Default to `true`\n\nIf `true`, the model may request a login shell (`login = true`), and omitting `login` defaults to using a login shell. If `false`, the model can never use a login shell: `login = true` requests are rejected, and omitting `login` defaults to a non-login shell.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"analytics": {
|
||||
"allOf": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -609,6 +609,7 @@ impl TurnContext {
|
|||
features: &features,
|
||||
web_search_mode: self.tools_config.web_search_mode,
|
||||
})
|
||||
.with_allow_login_shell(self.tools_config.allow_login_shell)
|
||||
.with_agent_roles(config.agent_roles.clone());
|
||||
|
||||
Self {
|
||||
|
|
@ -941,6 +942,7 @@ impl Session {
|
|||
features: &per_turn_config.features,
|
||||
web_search_mode: Some(per_turn_config.web_search_mode.value()),
|
||||
})
|
||||
.with_allow_login_shell(per_turn_config.permissions.allow_login_shell)
|
||||
.with_agent_roles(per_turn_config.agent_roles.clone());
|
||||
|
||||
let cwd = session_configuration.cwd.clone();
|
||||
|
|
@ -4122,6 +4124,7 @@ async fn spawn_review_thread(
|
|||
features: &review_features,
|
||||
web_search_mode: Some(review_web_search_mode),
|
||||
})
|
||||
.with_allow_login_shell(config.permissions.allow_login_shell)
|
||||
.with_agent_roles(config.agent_roles.clone());
|
||||
|
||||
let review_prompt = resolved.prompt.clone();
|
||||
|
|
|
|||
|
|
@ -139,6 +139,15 @@ pub struct Permissions {
|
|||
pub sandbox_policy: Constrained<SandboxPolicy>,
|
||||
/// Effective network configuration applied to all spawned processes.
|
||||
pub network: Option<NetworkProxySpec>,
|
||||
/// Whether the model may request a login shell for shell-based tools.
|
||||
/// Default to `true`
|
||||
///
|
||||
/// If `true`, the model may request a login shell (`login = true`), and
|
||||
/// omitting `login` defaults to using a login shell.
|
||||
/// If `false`, the model can never use a login shell: `login = true`
|
||||
/// requests are rejected, and omitting `login` defaults to a non-login
|
||||
/// shell.
|
||||
pub allow_login_shell: bool,
|
||||
/// Policy used to build process environments for shell/unified exec.
|
||||
pub shell_environment_policy: ShellEnvironmentPolicy,
|
||||
/// Effective Windows sandbox mode derived from `[windows].sandbox` or
|
||||
|
|
@ -952,6 +961,16 @@ pub struct ConfigToml {
|
|||
#[serde(default)]
|
||||
pub shell_environment_policy: ShellEnvironmentPolicyToml,
|
||||
|
||||
/// Whether the model may request a login shell for shell-based tools.
|
||||
/// Default to `true`
|
||||
///
|
||||
/// If `true`, the model may request a login shell (`login = true`), and
|
||||
/// omitting `login` defaults to using a login shell.
|
||||
/// If `false`, the model can never use a login shell: `login = true`
|
||||
/// requests are rejected, and omitting `login` defaults to a non-login
|
||||
/// shell.
|
||||
pub allow_login_shell: Option<bool>,
|
||||
|
||||
/// Sandbox mode to use.
|
||||
pub sandbox_mode: Option<SandboxMode>,
|
||||
|
||||
|
|
@ -1710,6 +1729,7 @@ impl Config {
|
|||
.clone();
|
||||
|
||||
let shell_environment_policy = cfg.shell_environment_policy.into();
|
||||
let allow_login_shell = cfg.allow_login_shell.unwrap_or(true);
|
||||
|
||||
let history = cfg.history.unwrap_or_default();
|
||||
|
||||
|
|
@ -1950,6 +1970,7 @@ impl Config {
|
|||
approval_policy: constrained_approval_policy.value,
|
||||
sandbox_policy: constrained_sandbox_policy.value,
|
||||
network,
|
||||
allow_login_shell,
|
||||
shell_environment_policy,
|
||||
windows_sandbox_mode,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
|
|
@ -4519,6 +4540,7 @@ model_verbosity = "high"
|
|||
approval_policy: Constrained::allow_any(AskForApproval::Never),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
|
|
@ -4637,6 +4659,7 @@ model_verbosity = "high"
|
|||
approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
|
|
@ -4753,6 +4776,7 @@ model_verbosity = "high"
|
|||
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
|
|
@ -4855,6 +4879,7 @@ model_verbosity = "high"
|
|||
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
|
|
@ -5429,6 +5454,27 @@ mcp_oauth_callback_port = 5678
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_loads_allow_login_shell_from_toml() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cfg: ConfigToml = toml::from_str(
|
||||
r#"
|
||||
model = "gpt-5.1"
|
||||
allow_login_shell = false
|
||||
"#,
|
||||
)
|
||||
.expect("TOML deserialization should succeed for allow_login_shell");
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
cfg,
|
||||
ConfigOverrides::default(),
|
||||
codex_home.path().to_path_buf(),
|
||||
)?;
|
||||
|
||||
assert!(!config.permissions.allow_login_shell);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_loads_mcp_oauth_callback_url_from_toml() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
|
|
|||
|
|
@ -84,22 +84,28 @@ fn shell_command_for_invocation(invocation: &ToolInvocation) -> Option<(Vec<Stri
|
|||
"shell_command" => serde_json::from_str::<ShellCommandToolCallParams>(arguments)
|
||||
.ok()
|
||||
.map(|params| {
|
||||
if !invocation.turn.tools_config.allow_login_shell && params.login == Some(true) {
|
||||
return (Vec::new(), invocation.turn.resolve_path(params.workdir));
|
||||
}
|
||||
let use_login_shell = params
|
||||
.login
|
||||
.unwrap_or(invocation.turn.tools_config.allow_login_shell);
|
||||
let command = invocation
|
||||
.session
|
||||
.user_shell()
|
||||
.derive_exec_args(¶ms.command, params.login.unwrap_or(true));
|
||||
.derive_exec_args(¶ms.command, use_login_shell);
|
||||
(command, invocation.turn.resolve_path(params.workdir))
|
||||
}),
|
||||
"exec_command" => serde_json::from_str::<ExecCommandArgs>(arguments)
|
||||
.ok()
|
||||
.map(|params| {
|
||||
(
|
||||
crate::tools::handlers::unified_exec::get_command(
|
||||
¶ms,
|
||||
invocation.session.user_shell(),
|
||||
),
|
||||
invocation.turn.resolve_path(params.workdir),
|
||||
.and_then(|params| {
|
||||
let command = crate::tools::handlers::unified_exec::get_command(
|
||||
¶ms,
|
||||
invocation.session.user_shell(),
|
||||
invocation.turn.tools_config.allow_login_shell,
|
||||
)
|
||||
.ok()?;
|
||||
Some((command, invocation.turn.resolve_path(params.workdir)))
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1372,6 +1372,7 @@ permissions:
|
|||
}
|
||||
),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
macos_seatbelt_profile_extensions,
|
||||
|
|
@ -1408,6 +1409,7 @@ permissions: {}
|
|||
crate::protocol::SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
macos_seatbelt_profile_extensions: Some(
|
||||
|
|
@ -1421,6 +1423,7 @@ permissions: {}
|
|||
crate::protocol::SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
|
|
@ -1515,6 +1518,7 @@ permissions:
|
|||
crate::protocol::SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ pub(crate) fn compile_permission_profile(
|
|||
approval_policy: Constrained::allow_any(AskForApproval::Never),
|
||||
sandbox_policy: Constrained::allow_any(sandbox_policy),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
macos_seatbelt_profile_extensions,
|
||||
|
|
@ -347,6 +348,7 @@ mod tests {
|
|||
exclude_slash_tmp: false,
|
||||
}),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
|
|
@ -391,6 +393,7 @@ mod tests {
|
|||
approval_policy: Constrained::allow_any(AskForApproval::Never),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
|
|
@ -439,6 +442,7 @@ mod tests {
|
|||
},
|
||||
}),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
windows_sandbox_mode: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
|
|
|
|||
|
|
@ -63,8 +63,20 @@ impl ShellHandler {
|
|||
}
|
||||
|
||||
impl ShellCommandHandler {
|
||||
fn base_command(shell: &Shell, command: &str, login: Option<bool>) -> Vec<String> {
|
||||
let use_login_shell = login.unwrap_or(true);
|
||||
fn resolve_use_login_shell(
|
||||
login: Option<bool>,
|
||||
allow_login_shell: bool,
|
||||
) -> Result<bool, FunctionCallError> {
|
||||
if !allow_login_shell && login == Some(true) {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"login shell is disabled by config; omit `login` or set it to false.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(login.unwrap_or(allow_login_shell))
|
||||
}
|
||||
|
||||
fn base_command(shell: &Shell, command: &str, use_login_shell: bool) -> Vec<String> {
|
||||
shell.derive_exec_args(command, use_login_shell)
|
||||
}
|
||||
|
||||
|
|
@ -73,11 +85,13 @@ impl ShellCommandHandler {
|
|||
session: &crate::codex::Session,
|
||||
turn_context: &TurnContext,
|
||||
thread_id: ThreadId,
|
||||
) -> ExecParams {
|
||||
allow_login_shell: bool,
|
||||
) -> Result<ExecParams, FunctionCallError> {
|
||||
let shell = session.user_shell();
|
||||
let command = Self::base_command(shell.as_ref(), ¶ms.command, params.login);
|
||||
let use_login_shell = Self::resolve_use_login_shell(params.login, allow_login_shell)?;
|
||||
let command = Self::base_command(shell.as_ref(), ¶ms.command, use_login_shell);
|
||||
|
||||
ExecParams {
|
||||
Ok(ExecParams {
|
||||
command,
|
||||
cwd: turn_context.resolve_path(params.workdir.clone()),
|
||||
expiration: params.timeout_ms.into(),
|
||||
|
|
@ -87,7 +101,7 @@ impl ShellCommandHandler {
|
|||
windows_sandbox_level: turn_context.windows_sandbox_level,
|
||||
justification: params.justification.clone(),
|
||||
arg0: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -183,8 +197,15 @@ impl ToolHandler for ShellCommandHandler {
|
|||
|
||||
serde_json::from_str::<ShellCommandToolCallParams>(arguments)
|
||||
.map(|params| {
|
||||
let use_login_shell = match Self::resolve_use_login_shell(
|
||||
params.login,
|
||||
invocation.turn.tools_config.allow_login_shell,
|
||||
) {
|
||||
Ok(use_login_shell) => use_login_shell,
|
||||
Err(_) => return true,
|
||||
};
|
||||
let shell = invocation.session.user_shell();
|
||||
let command = Self::base_command(shell.as_ref(), ¶ms.command, params.login);
|
||||
let command = Self::base_command(shell.as_ref(), ¶ms.command, use_login_shell);
|
||||
!is_known_safe_command(&command)
|
||||
})
|
||||
.unwrap_or(true)
|
||||
|
|
@ -213,7 +234,8 @@ impl ToolHandler for ShellCommandHandler {
|
|||
session.as_ref(),
|
||||
turn.as_ref(),
|
||||
session.conversation_id,
|
||||
);
|
||||
turn.tools_config.allow_login_shell,
|
||||
)?;
|
||||
ShellHandler::run_exec_like(RunExecLikeArgs {
|
||||
tool_name,
|
||||
exec_params,
|
||||
|
|
@ -439,7 +461,9 @@ mod tests {
|
|||
&session,
|
||||
&turn_context,
|
||||
session.conversation_id,
|
||||
);
|
||||
true,
|
||||
)
|
||||
.expect("login shells should be allowed");
|
||||
|
||||
// ExecParams cannot derive Eq due to the CancellationToken field, so we manually compare the fields.
|
||||
assert_eq!(exec_params.command, expected_command);
|
||||
|
|
@ -464,18 +488,57 @@ mod tests {
|
|||
shell_snapshot,
|
||||
};
|
||||
|
||||
let login_command =
|
||||
ShellCommandHandler::base_command(&shell, "echo login shell", Some(true));
|
||||
let login_command = ShellCommandHandler::base_command(&shell, "echo login shell", true);
|
||||
assert_eq!(
|
||||
login_command,
|
||||
shell.derive_exec_args("echo login shell", true)
|
||||
);
|
||||
|
||||
let non_login_command =
|
||||
ShellCommandHandler::base_command(&shell, "echo non login shell", Some(false));
|
||||
ShellCommandHandler::base_command(&shell, "echo non login shell", false);
|
||||
assert_eq!(
|
||||
non_login_command,
|
||||
shell.derive_exec_args("echo non login shell", false)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn shell_command_handler_defaults_to_non_login_when_disallowed() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let params = ShellCommandToolCallParams {
|
||||
command: "echo hello".to_string(),
|
||||
workdir: None,
|
||||
login: None,
|
||||
timeout_ms: None,
|
||||
sandbox_permissions: None,
|
||||
prefix_rule: None,
|
||||
justification: None,
|
||||
};
|
||||
|
||||
let exec_params = ShellCommandHandler::to_exec_params(
|
||||
¶ms,
|
||||
&session,
|
||||
&turn_context,
|
||||
session.conversation_id,
|
||||
false,
|
||||
)
|
||||
.expect("non-login shells should still be allowed");
|
||||
|
||||
assert_eq!(
|
||||
exec_params.command,
|
||||
session.user_shell().derive_exec_args("echo hello", false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_command_handler_rejects_login_when_disallowed() {
|
||||
let err = ShellCommandHandler::resolve_use_login_shell(Some(true), false)
|
||||
.expect_err("explicit login should be rejected");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("login shell is disabled by config"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ pub(crate) struct ExecCommandArgs {
|
|||
pub(crate) workdir: Option<String>,
|
||||
#[serde(default)]
|
||||
shell: Option<String>,
|
||||
#[serde(default = "default_login")]
|
||||
login: bool,
|
||||
#[serde(default)]
|
||||
login: Option<bool>,
|
||||
#[serde(default = "default_tty")]
|
||||
tty: bool,
|
||||
#[serde(default = "default_exec_yield_time_ms")]
|
||||
|
|
@ -68,10 +68,6 @@ fn default_write_stdin_yield_time_ms() -> u64 {
|
|||
250
|
||||
}
|
||||
|
||||
fn default_login() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_tty() -> bool {
|
||||
false
|
||||
}
|
||||
|
|
@ -98,7 +94,14 @@ impl ToolHandler for UnifiedExecHandler {
|
|||
let Ok(params) = serde_json::from_str::<ExecCommandArgs>(arguments) else {
|
||||
return true;
|
||||
};
|
||||
let command = get_command(¶ms, invocation.session.user_shell());
|
||||
let command = match get_command(
|
||||
¶ms,
|
||||
invocation.session.user_shell(),
|
||||
invocation.turn.tools_config.allow_login_shell,
|
||||
) {
|
||||
Ok(command) => command,
|
||||
Err(_) => return true,
|
||||
};
|
||||
!is_known_safe_command(&command)
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +132,12 @@ impl ToolHandler for UnifiedExecHandler {
|
|||
"exec_command" => {
|
||||
let args: ExecCommandArgs = parse_arguments(&arguments)?;
|
||||
let process_id = manager.allocate_process_id().await;
|
||||
let command = get_command(&args, session.user_shell());
|
||||
let command = get_command(
|
||||
&args,
|
||||
session.user_shell(),
|
||||
turn.tools_config.allow_login_shell,
|
||||
)
|
||||
.map_err(FunctionCallError::RespondToModel)?;
|
||||
|
||||
let ExecCommandArgs {
|
||||
workdir,
|
||||
|
|
@ -238,7 +246,11 @@ impl ToolHandler for UnifiedExecHandler {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_command(args: &ExecCommandArgs, session_shell: Arc<Shell>) -> Vec<String> {
|
||||
pub(crate) fn get_command(
|
||||
args: &ExecCommandArgs,
|
||||
session_shell: Arc<Shell>,
|
||||
allow_login_shell: bool,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let model_shell = args.shell.as_ref().map(|shell_str| {
|
||||
let mut shell = get_shell_by_model_provided_path(&PathBuf::from(shell_str));
|
||||
shell.shell_snapshot = crate::shell::empty_shell_snapshot_receiver();
|
||||
|
|
@ -246,8 +258,17 @@ pub(crate) fn get_command(args: &ExecCommandArgs, session_shell: Arc<Shell>) ->
|
|||
});
|
||||
|
||||
let shell = model_shell.as_ref().unwrap_or(session_shell.as_ref());
|
||||
let use_login_shell = match args.login {
|
||||
Some(true) if !allow_login_shell => {
|
||||
return Err(
|
||||
"login shell is disabled by config; omit `login` or set it to false.".to_string(),
|
||||
);
|
||||
}
|
||||
Some(use_login_shell) => use_login_shell,
|
||||
None => allow_login_shell,
|
||||
};
|
||||
|
||||
shell.derive_exec_args(&args.cmd, args.login)
|
||||
Ok(shell.derive_exec_args(&args.cmd, use_login_shell))
|
||||
}
|
||||
|
||||
fn format_response(response: &UnifiedExecResponse) -> String {
|
||||
|
|
@ -283,6 +304,7 @@ fn format_response(response: &UnifiedExecResponse) -> String {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use crate::shell::default_user_shell;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
|
|
@ -293,7 +315,8 @@ mod tests {
|
|||
|
||||
assert!(args.shell.is_none());
|
||||
|
||||
let command = get_command(&args, Arc::new(default_user_shell()));
|
||||
let command =
|
||||
get_command(&args, Arc::new(default_user_shell()), true).map_err(anyhow::Error::msg)?;
|
||||
|
||||
assert_eq!(command.len(), 3);
|
||||
assert_eq!(command[2], "echo hello");
|
||||
|
|
@ -308,7 +331,8 @@ mod tests {
|
|||
|
||||
assert_eq!(args.shell.as_deref(), Some("/bin/bash"));
|
||||
|
||||
let command = get_command(&args, Arc::new(default_user_shell()));
|
||||
let command =
|
||||
get_command(&args, Arc::new(default_user_shell()), true).map_err(anyhow::Error::msg)?;
|
||||
|
||||
assert_eq!(command.last(), Some(&"echo hello".to_string()));
|
||||
if command
|
||||
|
|
@ -328,7 +352,8 @@ mod tests {
|
|||
|
||||
assert_eq!(args.shell.as_deref(), Some("powershell"));
|
||||
|
||||
let command = get_command(&args, Arc::new(default_user_shell()));
|
||||
let command =
|
||||
get_command(&args, Arc::new(default_user_shell()), true).map_err(anyhow::Error::msg)?;
|
||||
|
||||
assert_eq!(command[2], "echo hello");
|
||||
Ok(())
|
||||
|
|
@ -342,9 +367,25 @@ mod tests {
|
|||
|
||||
assert_eq!(args.shell.as_deref(), Some("cmd"));
|
||||
|
||||
let command = get_command(&args, Arc::new(default_user_shell()));
|
||||
let command =
|
||||
get_command(&args, Arc::new(default_user_shell()), true).map_err(anyhow::Error::msg)?;
|
||||
|
||||
assert_eq!(command[2], "echo hello");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_command_rejects_explicit_login_when_disallowed() -> anyhow::Result<()> {
|
||||
let json = r#"{"cmd": "echo hello", "login": true}"#;
|
||||
|
||||
let args: ExecCommandArgs = parse_arguments(json)?;
|
||||
let err = get_command(&args, Arc::new(default_user_shell()), false)
|
||||
.expect_err("explicit login should be rejected");
|
||||
|
||||
assert!(
|
||||
err.contains("login shell is disabled by config"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ const SEARCH_TOOL_BM25_DESCRIPTION_TEMPLATE: &str =
|
|||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ToolsConfig {
|
||||
pub shell_type: ConfigShellToolType,
|
||||
pub allow_login_shell: bool,
|
||||
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
|
||||
pub web_search_mode: Option<WebSearchMode>,
|
||||
pub agent_roles: BTreeMap<String, AgentRoleConfig>,
|
||||
|
|
@ -96,6 +97,7 @@ impl ToolsConfig {
|
|||
|
||||
Self {
|
||||
shell_type,
|
||||
allow_login_shell: true,
|
||||
apply_patch_tool_type,
|
||||
web_search_mode: *web_search_mode,
|
||||
agent_roles: BTreeMap::new(),
|
||||
|
|
@ -112,6 +114,11 @@ impl ToolsConfig {
|
|||
self.agent_roles = agent_roles;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_allow_login_shell(mut self, allow_login_shell: bool) -> Self {
|
||||
self.allow_login_shell = allow_login_shell;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic JSON‑Schema subset needed for our tool definitions
|
||||
|
|
@ -211,7 +218,7 @@ fn create_approval_parameters() -> BTreeMap<String, JsonSchema> {
|
|||
properties
|
||||
}
|
||||
|
||||
fn create_exec_command_tool() -> ToolSpec {
|
||||
fn create_exec_command_tool(allow_login_shell: bool) -> ToolSpec {
|
||||
let mut properties = BTreeMap::from([
|
||||
(
|
||||
"cmd".to_string(),
|
||||
|
|
@ -234,14 +241,6 @@ fn create_exec_command_tool() -> ToolSpec {
|
|||
description: Some("Shell binary to launch. Defaults to the user's default shell.".to_string()),
|
||||
},
|
||||
),
|
||||
(
|
||||
"login".to_string(),
|
||||
JsonSchema::Boolean {
|
||||
description: Some(
|
||||
"Whether to run the shell with -l/-i semantics. Defaults to true.".to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"tty".to_string(),
|
||||
JsonSchema::Boolean {
|
||||
|
|
@ -269,6 +268,16 @@ fn create_exec_command_tool() -> ToolSpec {
|
|||
},
|
||||
),
|
||||
]);
|
||||
if allow_login_shell {
|
||||
properties.insert(
|
||||
"login".to_string(),
|
||||
JsonSchema::Boolean {
|
||||
description: Some(
|
||||
"Whether to run the shell with -l/-i semantics. Defaults to true.".to_string(),
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
properties.extend(create_approval_parameters());
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
|
|
@ -385,7 +394,7 @@ Examples of valid command strings:
|
|||
})
|
||||
}
|
||||
|
||||
fn create_shell_command_tool() -> ToolSpec {
|
||||
fn create_shell_command_tool(allow_login_shell: bool) -> ToolSpec {
|
||||
let mut properties = BTreeMap::from([
|
||||
(
|
||||
"command".to_string(),
|
||||
|
|
@ -402,6 +411,14 @@ fn create_shell_command_tool() -> ToolSpec {
|
|||
},
|
||||
),
|
||||
(
|
||||
"timeout_ms".to_string(),
|
||||
JsonSchema::Number {
|
||||
description: Some("The timeout for the command in milliseconds".to_string()),
|
||||
},
|
||||
),
|
||||
]);
|
||||
if allow_login_shell {
|
||||
properties.insert(
|
||||
"login".to_string(),
|
||||
JsonSchema::Boolean {
|
||||
description: Some(
|
||||
|
|
@ -409,14 +426,8 @@ fn create_shell_command_tool() -> ToolSpec {
|
|||
.to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"timeout_ms".to_string(),
|
||||
JsonSchema::Number {
|
||||
description: Some("The timeout for the command in milliseconds".to_string()),
|
||||
},
|
||||
),
|
||||
]);
|
||||
);
|
||||
}
|
||||
properties.extend(create_approval_parameters());
|
||||
|
||||
let description = if cfg!(windows) {
|
||||
|
|
@ -1462,7 +1473,10 @@ pub(crate) fn build_specs(
|
|||
builder.push_spec_with_parallel_support(ToolSpec::LocalShell {}, true);
|
||||
}
|
||||
ConfigShellToolType::UnifiedExec => {
|
||||
builder.push_spec_with_parallel_support(create_exec_command_tool(), true);
|
||||
builder.push_spec_with_parallel_support(
|
||||
create_exec_command_tool(config.allow_login_shell),
|
||||
true,
|
||||
);
|
||||
builder.push_spec(create_write_stdin_tool());
|
||||
builder.register_handler("exec_command", unified_exec_handler.clone());
|
||||
builder.register_handler("write_stdin", unified_exec_handler);
|
||||
|
|
@ -1471,7 +1485,10 @@ pub(crate) fn build_specs(
|
|||
// Do nothing.
|
||||
}
|
||||
ConfigShellToolType::ShellCommand => {
|
||||
builder.push_spec_with_parallel_support(create_shell_command_tool(), true);
|
||||
builder.push_spec_with_parallel_support(
|
||||
create_shell_command_tool(config.allow_login_shell),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1816,7 +1833,7 @@ mod tests {
|
|||
// Build expected from the same helpers used by the builder.
|
||||
let mut expected: BTreeMap<String, ToolSpec> = BTreeMap::from([]);
|
||||
for spec in [
|
||||
create_exec_command_tool(),
|
||||
create_exec_command_tool(true),
|
||||
create_write_stdin_tool(),
|
||||
PLAN_TOOL.clone(),
|
||||
create_request_user_input_tool(),
|
||||
|
|
@ -2790,7 +2807,7 @@ Examples of valid command strings:
|
|||
|
||||
#[test]
|
||||
fn test_shell_command_tool() {
|
||||
let tool = super::create_shell_command_tool();
|
||||
let tool = super::create_shell_command_tool(true);
|
||||
let ToolSpec::Function(ResponsesApiTool {
|
||||
description, name, ..
|
||||
}) = &tool
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue