Class: Clacky::Tools::SafeShell
- Defined in:
- lib/clacky/tools/safe_shell.rb
Constant Summary collapse
- SAFE_READONLY_COMMANDS =
Safe read-only commands that don’t modify system state
%w[ ls pwd cat less more head tail grep find which whereis whoami ps top htop df du git echo printf wc date file stat env printenv curl wget ].freeze
Constants inherited from Shell
Clacky::Tools::Shell::INTERACTION_PATTERNS, Clacky::Tools::Shell::MAX_LINE_CHARS, Clacky::Tools::Shell::MAX_LLM_OUTPUT_CHARS, Clacky::Tools::Shell::SLOW_COMMANDS
Class Method Summary collapse
-
.command_safe_for_auto_execution?(command) ⇒ Boolean
Class method to check if a command is safe to execute automatically.
Instance Method Summary collapse
- #enhance_result(result, original_command, safe_command, safety_replacer = nil) ⇒ Object
- #execute(command:, timeout: nil, max_output_lines: 1000, skip_safety_check: false, on_output: nil, working_dir: nil) ⇒ Object
- #format_call(args) ⇒ Object
- #format_result(result) ⇒ Object
-
#format_result_for_llm(result) ⇒ Object
Override format_result_for_llm to preserve security fields.
-
#xcode_tools_missing?(stderr) ⇒ Boolean
Returns true if stderr contains the macOS xcode-select shim message, which appears when Xcode Command Line Tools are not installed and the user (or LLM) tries to run python3, git, make, gcc, etc.
Methods inherited from Shell
#detect_interaction, #detect_sudo_waiting, #determine_timeouts, #extract_command_name, #format_timeout_result, #format_waiting_input_result, #format_waiting_message, #output_truncated?, #truncate_and_save, #truncate_long_lines, #truncate_output, #wrap_with_shell
Methods inherited from Base
#category, #description, #name, #parameters, #to_function_definition
Class Method Details
.command_safe_for_auto_execution?(command) ⇒ Boolean
Class method to check if a command is safe to execute automatically
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/clacky/tools/safe_shell.rb', line 147 def self.command_safe_for_auto_execution?(command) return false unless command # Check if it's a known safe read-only command cmd_name = command.strip.split.first return true if SAFE_READONLY_COMMANDS.include?(cmd_name) begin project_root = Dir.pwd safety_replacer = CommandSafetyReplacer.new(project_root) safe_command = safety_replacer.make_command_safe(command) # If the command wasn't changed by the safety replacer, it's considered safe # This means it doesn't need any modifications to be secure command.strip == safe_command.strip rescue SecurityError # If SecurityError is raised, the command is definitely not safe false end end |
Instance Method Details
#enhance_result(result, original_command, safe_command, safety_replacer = nil) ⇒ Object
168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/clacky/tools/safe_shell.rb', line 168 def enhance_result(result, original_command, safe_command, safety_replacer = nil) # If command was replaced, add security information if safety_replacer && original_command != safe_command result[:security_enhanced] = true result[:original_command] = original_command result[:safe_command] = safe_command # Add security note to stdout security_note = "[Safe] Command was automatically made safe\n" result[:stdout] = security_note + (result[:stdout] || "") end result end |
#execute(command:, timeout: nil, max_output_lines: 1000, skip_safety_check: false, on_output: nil, working_dir: nil) ⇒ Object
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/clacky/tools/safe_shell.rb', line 36 def execute(command:, timeout: nil, max_output_lines: 1000, skip_safety_check: false, on_output: nil, working_dir: nil) # Use provided working_dir or fall back to current process directory project_root = working_dir || Dir.pwd begin # 1. Extract timeout from command if it starts with "timeout N" command, extracted_timeout = extract_timeout_from_command(command) # Use extracted timeout if not explicitly provided timeout ||= extracted_timeout # 2. Use safety replacer to process command (skip if user already confirmed) if skip_safety_check # User has confirmed, execute command as-is (no safety modifications) safe_command = command safety_replacer = nil else safety_replacer = CommandSafetyReplacer.new(project_root) safe_command = safety_replacer.make_command_safe(command) end # 3. Calculate timeouts: soft_timeout is fixed at 5s, hard_timeout from timeout parameter soft_timeout = 5 hard_timeout = calculate_hard_timeout(command, timeout) # 4. Call parent class execution method result = super(command: safe_command, soft_timeout: soft_timeout, hard_timeout: hard_timeout, max_output_lines: max_output_lines, on_output: on_output, working_dir: working_dir) # 4a. If macOS xcode-select shim detected, replace stderr with actionable message if xcode_tools_missing?(result[:stderr]) result = result.merge( stderr: "Xcode Command Line Tools are not installed.\nRun: bash ~/.clacky/scripts/install_system_deps.sh\nThen retry the original command.", exit_code: 1, success: false ) end # 5. Enhance result information enhance_result(result, command, safe_command, safety_replacer) rescue SecurityError => e # Security error, return friendly error message { command: command, stdout: "", stderr: "[Security Protection] #{e.}", exit_code: 126, success: false, security_blocked: true } end end |
#format_call(args) ⇒ Object
183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/clacky/tools/safe_shell.rb', line 183 def format_call(args) cmd = args[:command] || args['command'] || '' return "safe_shell(<no command>)" if cmd.empty? # Truncate long commands intelligently if cmd.length > 150 "safe_shell(\"#{cmd[0..147]}...\")" else "safe_shell(\"#{cmd}\")" end end |
#format_result(result) ⇒ Object
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/clacky/tools/safe_shell.rb', line 195 def format_result(result) exit_code = result[:exit_code] || result['exit_code'] || 0 stdout = result[:stdout] || result['stdout'] || "" stderr = result[:stderr] || result['stderr'] || "" if result[:security_blocked] "[Blocked] Security protection" elsif result[:security_enhanced] lines = stdout.lines.size "[Safe] Completed#{lines > 0 ? " (#{lines} lines)" : ''}" elsif exit_code == 0 lines = stdout.lines.size "[OK] Completed#{lines > 0 ? " (#{lines} lines)" : ''}" else format_non_zero_exit(exit_code, stdout, stderr) end end |
#format_result_for_llm(result) ⇒ Object
Override format_result_for_llm to preserve security fields
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 |
# File 'lib/clacky/tools/safe_shell.rb', line 214 def format_result_for_llm(result) # If security blocked, return as-is (small and important) return result if result[:security_blocked] # Call parent's format_result_for_llm to truncate output compact = super(result) # Add security enhancement fields if present (they're small and important for LLM to understand) if result[:security_enhanced] compact[:security_enhanced] = true compact[:original_command] = result[:original_command] compact[:safe_command] = result[:safe_command] end compact end |
#xcode_tools_missing?(stderr) ⇒ Boolean
Returns true if stderr contains the macOS xcode-select shim message, which appears when Xcode Command Line Tools are not installed and the user (or LLM) tries to run python3, git, make, gcc, etc.
310 311 312 313 |
# File 'lib/clacky/tools/safe_shell.rb', line 310 def xcode_tools_missing?(stderr) return false if stderr.nil? || stderr.empty? stderr.include?("xcode-select") && stderr.include?("No developer tools were found") end |