@@ -3,6 +3,7 @@ use std::io::Write;
33use serde_json:: json;
44
55use crate :: client:: { ToolCall , ToolDefinition , ToolFunctionDefinition } ;
6+ use crate :: render:: sanitize_terminal_line;
67use crate :: shell_cmd;
78
89pub const MAX_TOOL_ROUNDS : usize = 8 ;
@@ -41,13 +42,55 @@ fn execute_shell(call: &ToolCall, no_interaction: bool) -> String {
4142}
4243
4344fn confirm ( command : & str ) -> bool {
44- print ! ( "Model wants to execute `{command}`. Execute? [y/N] " ) ;
45+ // The command is model-controlled; sanitize before display so control bytes
46+ // cannot rewrite the prompt and spoof what is being approved.
47+ print ! (
48+ "Model wants to execute `{}`. Execute? [y/N] " ,
49+ sanitize_terminal_line( command)
50+ ) ;
4551 std:: io:: stdout ( ) . flush ( ) . ok ( ) ;
4652 let mut answer = String :: new ( ) ;
4753 std:: io:: stdin ( ) . read_line ( & mut answer) . ok ( ) ;
54+ is_yes ( & answer)
55+ }
56+
57+ /// True only for an explicit yes; anything else (incl. empty/EOF) denies.
58+ pub fn is_yes ( answer : & str ) -> bool {
4859 matches ! ( answer. trim( ) . to_ascii_lowercase( ) . as_str( ) , "y" | "yes" )
4960}
5061
62+ /// Prompts on the controlling terminal (`/dev/tty`) — not stdin, which may be a
63+ /// pipe — to confirm executing a model-proposed `command`. Returns false,
64+ /// denying execution, when there is no controlling terminal to prompt on. The
65+ /// command is sanitized before display to prevent approval-prompt spoofing.
66+ pub fn confirm_tty ( command : & str ) -> bool {
67+ use std:: io:: { BufRead , BufReader } ;
68+
69+ let tty = match std:: fs:: OpenOptions :: new ( )
70+ . read ( true )
71+ . write ( true )
72+ . open ( "/dev/tty" )
73+ {
74+ Ok ( tty) => tty,
75+ Err ( _) => {
76+ eprintln ! ( "Refusing to execute: no controlling terminal available to confirm." ) ;
77+ return false ;
78+ }
79+ } ;
80+ let mut writer = & tty;
81+ let _ = write ! (
82+ writer,
83+ "Execute `{}`? [y/N] " ,
84+ sanitize_terminal_line( command)
85+ ) ;
86+ let _ = writer. flush ( ) ;
87+ let mut answer = String :: new ( ) ;
88+ if BufReader :: new ( & tty) . read_line ( & mut answer) . is_err ( ) {
89+ return false ;
90+ }
91+ is_yes ( & answer)
92+ }
93+
5194#[ cfg( test) ]
5295mod tests {
5396 use super :: * ;
@@ -64,4 +107,13 @@ mod tests {
64107 fn registry_exposes_the_shell_tool ( ) {
65108 assert_eq ! ( definitions( ) [ 0 ] . function. name, "execute_shell_command" ) ;
66109 }
110+ #[ test]
111+ fn confirmation_denies_by_default ( ) {
112+ for yes in [ "y" , "yes" , "Y" , "YES" , " yes \n " ] {
113+ assert ! ( is_yes( yes) , "{yes:?} should confirm" ) ;
114+ }
115+ for no in [ "" , "\n " , "n" , "no" , "sure" , "yep" , "yolo" ] {
116+ assert ! ( !is_yes( no) , "{no:?} should deny" ) ;
117+ }
118+ }
67119}
0 commit comments