tunacode-cli 0.0.34__py3-none-any.whl → 0.0.35__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tunacode-cli might be problematic. Click here for more details.

tunacode/cli/commands.py CHANGED
@@ -181,26 +181,29 @@ class IterationsCommand(SimpleCommand):
181
181
 
182
182
  async def execute(self, args: List[str], context: CommandContext) -> None:
183
183
  state = context.state_manager.session
184
- if args:
185
- try:
186
- new_limit = int(args[0])
187
- if new_limit < 1 or new_limit > 100:
188
- await ui.error("Iterations must be between 1 and 100")
189
- return
190
-
191
- # Update the user config
192
- if "settings" not in state.user_config:
193
- state.user_config["settings"] = {}
194
- state.user_config["settings"]["max_iterations"] = new_limit
195
-
196
- await ui.success(f"Maximum iterations set to {new_limit}")
197
- await ui.muted("Higher values allow more complex reasoning but may be slower")
198
- except ValueError:
199
- await ui.error("Please provide a valid number")
200
- else:
184
+
185
+ # Guard clause - handle "no args" case first and return early
186
+ if not args:
201
187
  current = state.user_config.get("settings", {}).get("max_iterations", 40)
202
188
  await ui.info(f"Current maximum iterations: {current}")
203
189
  await ui.muted("Usage: /iterations <number> (1-100)")
190
+ return
191
+
192
+ # update the logic to not be as nested messely, the above guars needing to get as messy
193
+ try:
194
+ new_limit = int(args[0])
195
+ if new_limit < 1 or new_limit > 100:
196
+ await ui.error("Iterations must be between 1 and 100")
197
+ return
198
+
199
+ # Update the user config
200
+ if "settings" not in state.user_config:
201
+ state.user_config["settings"] = {}
202
+ state.user_config["settings"]["max_iterations"] = new_limit
203
+
204
+ await ui.success(f"Maximum iterations set to {new_limit}")
205
+ except ValueError:
206
+ await ui.error("Please provide a valid number")
204
207
 
205
208
 
206
209
  class ClearCommand(SimpleCommand):
@@ -288,7 +291,9 @@ class ParseToolsCommand(SimpleCommand):
288
291
 
289
292
  try:
290
293
  await extract_and_execute_tool_calls(
291
- part.content, tool_callback_with_state, context.state_manager
294
+ part.content,
295
+ tool_callback_with_state,
296
+ context.state_manager,
292
297
  )
293
298
  await ui.success("JSON tool parsing completed")
294
299
  found_content = True
@@ -524,7 +529,8 @@ class UpdateCommand(SimpleCommand):
524
529
  result = subprocess.run(
525
530
  ["pipx", "list"], capture_output=True, text=True, timeout=10
526
531
  )
527
- if "tunacode" in result.stdout.lower():
532
+ pipx_installed = "tunacode" in result.stdout.lower()
533
+ if pipx_installed:
528
534
  installation_method = "pipx"
529
535
  except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
530
536
  pass
@@ -555,12 +561,22 @@ class UpdateCommand(SimpleCommand):
555
561
  if installation_method == "pipx":
556
562
  await ui.info("Updating via pipx...")
557
563
  result = subprocess.run(
558
- ["pipx", "upgrade", "tunacode"], capture_output=True, text=True, timeout=60
564
+ ["pipx", "upgrade", "tunacode"],
565
+ capture_output=True,
566
+ text=True,
567
+ timeout=60,
559
568
  )
560
569
  else: # pip
561
570
  await ui.info("Updating via pip...")
562
571
  result = subprocess.run(
563
- [sys.executable, "-m", "pip", "install", "--upgrade", "tunacode-cli"],
572
+ [
573
+ sys.executable,
574
+ "-m",
575
+ "pip",
576
+ "install",
577
+ "--upgrade",
578
+ "tunacode-cli",
579
+ ],
564
580
  capture_output=True,
565
581
  text=True,
566
582
  timeout=60,
tunacode/cli/repl.py CHANGED
@@ -22,6 +22,7 @@ from tunacode.core.tool_handler import ToolHandler
22
22
  from tunacode.exceptions import AgentError, UserAbortError, ValidationError
23
23
  from tunacode.ui import console as ui
24
24
  from tunacode.ui.tool_ui import ToolUI
25
+ from tunacode.utils.security import CommandSecurityError, safe_subprocess_run
25
26
 
26
27
  from ..types import CommandContext, CommandResult, StateManager, ToolArgs
27
28
  from .commands import CommandRegistry
@@ -320,13 +321,24 @@ async def repl(state_manager: StateManager):
320
321
  def run_shell():
321
322
  try:
322
323
  if command:
323
- result = subprocess.run(command, shell=True, capture_output=False)
324
- if result.returncode != 0:
325
- # Use print directly since we're in a terminal context
326
- print(f"\nCommand exited with code {result.returncode}")
324
+ # Use secure subprocess execution for shell commands
325
+ # Note: User shell commands are inherently risky but this is by design
326
+ # We validate but allow shell features since it's explicit user intent
327
+ try:
328
+ result = safe_subprocess_run(
329
+ command,
330
+ shell=True,
331
+ validate=True, # Still validate for basic safety
332
+ capture_output=False,
333
+ )
334
+ if result.returncode != 0:
335
+ print(f"\nCommand exited with code {result.returncode}")
336
+ except CommandSecurityError as e:
337
+ print(f"\nSecurity validation failed: {str(e)}")
338
+ print("If you need to run this command, please ensure it's safe.")
327
339
  else:
328
340
  shell = os.environ.get("SHELL", "bash")
329
- subprocess.run(shell)
341
+ subprocess.run(shell) # Interactive shell is safe
330
342
  except Exception as e:
331
343
  print(f"\nShell command failed: {str(e)}")
332
344
 
tunacode/constants.py CHANGED
@@ -7,7 +7,7 @@ Centralizes all magic strings, UI text, error messages, and application constant
7
7
 
8
8
  # Application info
9
9
  APP_NAME = "TunaCode"
10
- APP_VERSION = "0.0.34"
10
+ APP_VERSION = "0.0.35"
11
11
 
12
12
  # File patterns
13
13
  GUIDE_FILE_PATTERN = "{name}.md"
@@ -14,6 +14,7 @@ from tunacode.constants import (CMD_OUTPUT_FORMAT, CMD_OUTPUT_NO_ERRORS, CMD_OUT
14
14
  from tunacode.exceptions import ToolExecutionError
15
15
  from tunacode.tools.base import BaseTool
16
16
  from tunacode.types import ToolResult
17
+ from tunacode.utils.security import CommandSecurityError, safe_subprocess_popen
17
18
 
18
19
 
19
20
  class RunCommandTool(BaseTool):
@@ -34,16 +35,23 @@ class RunCommandTool(BaseTool):
34
35
 
35
36
  Raises:
36
37
  FileNotFoundError: If command not found
38
+ CommandSecurityError: If command fails security validation
37
39
  Exception: Any command execution errors
38
40
  """
39
- process = subprocess.Popen(
40
- command,
41
- shell=True,
42
- stdout=subprocess.PIPE,
43
- stderr=subprocess.PIPE,
44
- text=True,
45
- )
46
- stdout, stderr = process.communicate()
41
+ try:
42
+ # Use secure subprocess execution with validation
43
+ process = safe_subprocess_popen(
44
+ command,
45
+ shell=True, # CLI tool requires shell features
46
+ validate=True, # Enable security validation
47
+ stdout=subprocess.PIPE,
48
+ stderr=subprocess.PIPE,
49
+ text=True,
50
+ )
51
+ stdout, stderr = process.communicate()
52
+ except CommandSecurityError as e:
53
+ # Security validation failed - return error without execution
54
+ return f"Security validation failed: {str(e)}"
47
55
  output = stdout.strip() or CMD_OUTPUT_NO_OUTPUT
48
56
  error = stderr.strip() or CMD_OUTPUT_NO_ERRORS
49
57
  resp = CMD_OUTPUT_FORMAT.format(output=output, error=error).strip()
@@ -70,6 +78,8 @@ class RunCommandTool(BaseTool):
70
78
  """
71
79
  if isinstance(error, FileNotFoundError):
72
80
  err_msg = ERROR_COMMAND_EXECUTION.format(command=command, error=error)
81
+ elif isinstance(error, CommandSecurityError):
82
+ err_msg = f"Command blocked for security: {str(error)}"
73
83
  else:
74
84
  # Use parent class handling for other errors
75
85
  await super()._handle_error(error, command)
@@ -0,0 +1,208 @@
1
+ """
2
+ Security utilities for safe command execution and input validation.
3
+ Provides defensive measures against command injection attacks.
4
+ """
5
+
6
+ import logging
7
+ import re
8
+ import shlex
9
+ import subprocess
10
+ from typing import List, Optional
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Dangerous shell metacharacters that indicate potential injection
15
+ DANGEROUS_CHARS = [
16
+ ";",
17
+ "&",
18
+ "|",
19
+ "`",
20
+ "$",
21
+ "(",
22
+ ")",
23
+ "{",
24
+ "}",
25
+ "<",
26
+ ">",
27
+ "\n",
28
+ "\r",
29
+ "\\",
30
+ '"',
31
+ "'",
32
+ ]
33
+
34
+ # Common injection patterns
35
+ INJECTION_PATTERNS = [
36
+ r";\s*\w+", # Command chaining with semicolon
37
+ r"&&\s*\w+", # Command chaining with &&
38
+ r"\|\s*\w+", # Piping to another command
39
+ r"`[^`]+`", # Command substitution with backticks
40
+ r"\$\([^)]+\)", # Command substitution with $()
41
+ r">\s*[/\w]", # Output redirection
42
+ r"<\s*[/\w]", # Input redirection
43
+ ]
44
+
45
+
46
+ class CommandSecurityError(Exception):
47
+ """Raised when a command fails security validation."""
48
+
49
+ pass
50
+
51
+
52
+ def validate_command_safety(command: str, allow_shell_features: bool = False) -> None:
53
+ """
54
+ Validate that a command is safe to execute.
55
+
56
+ Args:
57
+ command: The command string to validate
58
+ allow_shell_features: If True, allows some shell features like pipes
59
+
60
+ Raises:
61
+ CommandSecurityError: If the command contains potentially dangerous patterns
62
+ """
63
+ if not command or not command.strip():
64
+ raise CommandSecurityError("Empty command not allowed")
65
+
66
+ # Log the command being validated
67
+ logger.info(f"Validating command: {command[:100]}...")
68
+
69
+ # Always check for the most dangerous patterns regardless of shell features
70
+ dangerous_patterns = [
71
+ r"rm\s+-rf\s+/", # Dangerous rm commands
72
+ r"sudo\s+rm", # Sudo rm commands
73
+ r">\s*/dev/sd[a-z]", # Writing to disk devices
74
+ r"dd\s+.*of=/dev/", # DD to devices
75
+ r"mkfs\.", # Format filesystem
76
+ r"fdisk", # Partition manipulation
77
+ r":\(\)\{.*\}\;", # Fork bomb pattern
78
+ ]
79
+
80
+ for pattern in dangerous_patterns:
81
+ if re.search(pattern, command, re.IGNORECASE):
82
+ logger.error(f"Highly dangerous pattern '{pattern}' detected in command")
83
+ raise CommandSecurityError("Command contains dangerous pattern and is blocked")
84
+
85
+ if not allow_shell_features:
86
+ # Check for dangerous characters (but allow some for CLI tools)
87
+ restricted_chars = [";", "&", "`", "$", "{", "}"] # More permissive for CLI
88
+ for char in restricted_chars:
89
+ if char in command:
90
+ logger.warning(f"Potentially dangerous character '{char}' detected in command")
91
+ raise CommandSecurityError(f"Potentially unsafe character '{char}' in command")
92
+
93
+ # Check for injection patterns (more selective)
94
+ strict_patterns = [
95
+ r";\s*rm\s+", # Command chaining to rm
96
+ r"&&\s*rm\s+", # Command chaining to rm
97
+ r"`[^`]*rm[^`]*`", # Command substitution with rm
98
+ r"\$\([^)]*rm[^)]*\)", # Command substitution with rm
99
+ ]
100
+
101
+ for pattern in strict_patterns:
102
+ if re.search(pattern, command):
103
+ logger.warning(f"Dangerous injection pattern '{pattern}' detected in command")
104
+ raise CommandSecurityError("Potentially unsafe pattern detected in command")
105
+
106
+
107
+ def sanitize_command_args(args: List[str]) -> List[str]:
108
+ """
109
+ Sanitize command arguments by shell-quoting them.
110
+
111
+ Args:
112
+ args: List of command arguments
113
+
114
+ Returns:
115
+ List of sanitized arguments
116
+ """
117
+ return [shlex.quote(arg) for arg in args]
118
+
119
+
120
+ def safe_subprocess_run(
121
+ command: str,
122
+ shell: bool = False,
123
+ validate: bool = True,
124
+ timeout: Optional[int] = None,
125
+ **kwargs,
126
+ ) -> subprocess.CompletedProcess:
127
+ """
128
+ Safely execute a subprocess with security validation.
129
+
130
+ Args:
131
+ command: Command to execute (string if shell=True, list if shell=False)
132
+ shell: Whether to use shell execution (discouraged)
133
+ validate: Whether to validate command safety
134
+ timeout: Timeout in seconds
135
+ **kwargs: Additional subprocess arguments
136
+
137
+ Returns:
138
+ CompletedProcess result
139
+
140
+ Raises:
141
+ CommandSecurityError: If command fails security validation
142
+ """
143
+ if validate and shell and isinstance(command, str):
144
+ validate_command_safety(command, allow_shell_features=shell)
145
+
146
+ # Log the command execution
147
+ logger.info(f"Executing command: {str(command)[:100]}...")
148
+
149
+ try:
150
+ if shell:
151
+ # When using shell=True, command should be a string
152
+ result = subprocess.run(command, shell=True, timeout=timeout, **kwargs)
153
+ else:
154
+ # When shell=False, command should be a list
155
+ if isinstance(command, str):
156
+ # Parse the string into a list
157
+ command_list = shlex.split(command)
158
+ else:
159
+ command_list = command
160
+
161
+ result = subprocess.run(command_list, shell=False, timeout=timeout, **kwargs)
162
+
163
+ logger.info(f"Command completed with return code: {result.returncode}")
164
+ return result
165
+
166
+ except subprocess.TimeoutExpired:
167
+ logger.error(f"Command timed out after {timeout} seconds")
168
+ raise
169
+ except Exception as e:
170
+ logger.error(f"Command execution failed: {str(e)}")
171
+ raise
172
+
173
+
174
+ def safe_subprocess_popen(
175
+ command: str, shell: bool = False, validate: bool = True, **kwargs
176
+ ) -> subprocess.Popen:
177
+ """
178
+ Safely create a subprocess.Popen with security validation.
179
+
180
+ Args:
181
+ command: Command to execute
182
+ shell: Whether to use shell execution (discouraged)
183
+ validate: Whether to validate command safety
184
+ **kwargs: Additional Popen arguments
185
+
186
+ Returns:
187
+ Popen process object
188
+
189
+ Raises:
190
+ CommandSecurityError: If command fails security validation
191
+ """
192
+ if validate and shell and isinstance(command, str):
193
+ validate_command_safety(command, allow_shell_features=shell)
194
+
195
+ # Log the command execution
196
+ logger.info(f"Creating Popen for command: {str(command)[:100]}...")
197
+
198
+ if shell:
199
+ # When using shell=True, command should be a string
200
+ return subprocess.Popen(command, shell=True, **kwargs)
201
+ else:
202
+ # When shell=False, command should be a list
203
+ if isinstance(command, str):
204
+ command_list = shlex.split(command)
205
+ else:
206
+ command_list = command
207
+
208
+ return subprocess.Popen(command_list, shell=False, **kwargs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tunacode-cli
3
- Version: 0.0.34
3
+ Version: 0.0.35
4
4
  Summary: Your agentic CLI developer.
5
5
  Author-email: larock22 <noreply@github.com>
6
6
  License-Expression: MIT
@@ -1,14 +1,14 @@
1
1
  tunacode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- tunacode/constants.py,sha256=SIhhV5x1ESvfVaAki0nJ9XVlKZOwwomqIMT73FLyjPY,4074
2
+ tunacode/constants.py,sha256=OnQYL4TeNFuMCo_7x9FGWmjQCSDOB544wPPs9oOKk-8,4074
3
3
  tunacode/context.py,sha256=6sterdRvPOyG3LU0nEAXpBsEPZbO3qtPyTlJBi-_VXE,2612
4
4
  tunacode/exceptions.py,sha256=mTWXuWyr1k16CGLWN2tsthDGi7lbx1JK0ekIqogYDP8,3105
5
5
  tunacode/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  tunacode/setup.py,sha256=dYn0NeAxtNIDSogWEmGSyjb9wsr8AonZ8vAo5sw9NIw,1909
7
7
  tunacode/types.py,sha256=BciT-uxnQ44iC-4QiDY72OD23LOtqSyMOuK_N0ttlaA,7676
8
8
  tunacode/cli/__init__.py,sha256=zgs0UbAck8hfvhYsWhWOfBe5oK09ug2De1r4RuQZREA,55
9
- tunacode/cli/commands.py,sha256=DEEfDVNzDmMRUhA7JfWGh8antnMeb3G6LmaJLqNVsoI,31293
9
+ tunacode/cli/commands.py,sha256=r-WYp5ajzkZfFjiLXuK9pfB5ugq3HWQyhRB8Usr567k,31668
10
10
  tunacode/cli/main.py,sha256=PIcFnfmIoI_pmK2y-zB_ouJbzR5fbSI7zsKQNPB_J8o,2406
11
- tunacode/cli/repl.py,sha256=o3bn9BYQsy3TFCWJq-fzeHKLrM2KInSrMF5E5_RqSOY,13736
11
+ tunacode/cli/repl.py,sha256=ELnJBk3Vn2almXmmCIjGfgi7J5kNNVnO0o4KNYGXF9Q,14556
12
12
  tunacode/cli/textual_app.py,sha256=14-Nt0IIETmyHBrNn9uwSF3EwCcutwTp6gdoKgNm0sY,12593
13
13
  tunacode/cli/textual_bridge.py,sha256=LvqiTtF0hu3gNujzpKaW9h-m6xzEP3OH2M8KL2pCwRc,6333
14
14
  tunacode/configuration/__init__.py,sha256=MbVXy8bGu0yKehzgdgZ_mfWlYGvIdb1dY2Ly75nfuPE,17
@@ -42,7 +42,7 @@ tunacode/tools/grep.py,sha256=jboEVA2ATv0YI8zg9dF89emZ_HWy2vVtsQ_-hDhlr7g,26337
42
42
  tunacode/tools/list_dir.py,sha256=1kNqzYCNlcA5rqXIEVqcjQy6QxlLZLj5AG6YIECfwio,7217
43
43
  tunacode/tools/read_file.py,sha256=BqHxPspZBYotz5wtjuP-zve61obsx98z5TU-aw5BJHg,3273
44
44
  tunacode/tools/read_file_async_poc.py,sha256=0rSfYCmoNcvWk8hB1z86l32-tomSc9yOM4tR4nrty_o,6267
45
- tunacode/tools/run_command.py,sha256=kYg_Re397OmZdKtUSjpNfYYNDBjd0vsS1xMK0yP181I,3776
45
+ tunacode/tools/run_command.py,sha256=2TtndMIeOWHQRC2XwkxUDVb06Ob-RHKSheBdluH3QgQ,4433
46
46
  tunacode/tools/update_file.py,sha256=bW1MhTzRjBDjJzqQ6A1yCVEbkr1oIqtEC8uqcg_rfY4,3957
47
47
  tunacode/tools/write_file.py,sha256=prL6u8XOi9ZyPU-YNlG9YMLbSLrDJXDRuDX73ncXh-k,2699
48
48
  tunacode/ui/__init__.py,sha256=aRNE2pS50nFAX6y--rSGMNYwhz905g14gRd6g4BolYU,13
@@ -64,13 +64,14 @@ tunacode/utils/diff_utils.py,sha256=V9QqQ0q4MfabVTnWptF3IXDp3estnfOKcJtDe_Sj14I,
64
64
  tunacode/utils/file_utils.py,sha256=AXiAJ_idtlmXEi9pMvwtfPy9Ys3yK-F4K7qb_NpwonU,923
65
65
  tunacode/utils/import_cache.py,sha256=q_xjJbtju05YbFopLDSkIo1hOtCx3DOTl3GQE5FFDgs,295
66
66
  tunacode/utils/ripgrep.py,sha256=AXUs2FFt0A7KBV996deS8wreIlUzKOlAHJmwrcAr4No,583
67
+ tunacode/utils/security.py,sha256=e_zo9VmcOKFjgFMr9GOBIFhAmND4PBlJZgY7zqnsGjI,6548
67
68
  tunacode/utils/system.py,sha256=FSoibTIH0eybs4oNzbYyufIiV6gb77QaeY2yGqW39AY,11381
68
69
  tunacode/utils/text_utils.py,sha256=zRBaorvtyd7HBEWtIfCH1Wce1L6rhsQwpORUEGBFMjA,2981
69
70
  tunacode/utils/token_counter.py,sha256=nGCWwrHHFbKywqeDCEuJnADCkfJuzysWiB6cCltJOKI,648
70
71
  tunacode/utils/user_configuration.py,sha256=Ilz8dpGVJDBE2iLWHAPT0xR8D51VRKV3kIbsAz8Bboc,3275
71
- tunacode_cli-0.0.34.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
72
- tunacode_cli-0.0.34.dist-info/METADATA,sha256=uZKr-BnaxT_7vB1gTSIMEnWx1tfOSbBtPAt7gY4pibY,4943
73
- tunacode_cli-0.0.34.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
74
- tunacode_cli-0.0.34.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
75
- tunacode_cli-0.0.34.dist-info/top_level.txt,sha256=lKy2P6BWNi5XSA4DHFvyjQ14V26lDZctwdmhEJrxQbU,9
76
- tunacode_cli-0.0.34.dist-info/RECORD,,
72
+ tunacode_cli-0.0.35.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
73
+ tunacode_cli-0.0.35.dist-info/METADATA,sha256=4ck_-g8eF10l6md95_EaEK1Sd4UmqmRAUEJacINtDA8,4943
74
+ tunacode_cli-0.0.35.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
75
+ tunacode_cli-0.0.35.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
76
+ tunacode_cli-0.0.35.dist-info/top_level.txt,sha256=lKy2P6BWNi5XSA4DHFvyjQ14V26lDZctwdmhEJrxQbU,9
77
+ tunacode_cli-0.0.35.dist-info/RECORD,,