tinyagent-py 0.0.13__py3-none-any.whl → 0.0.15__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.
@@ -47,13 +47,13 @@ You are an Agent, You need to solve the task, not suggesting user about how to s
47
47
 
48
48
  """)
49
49
 
50
- def load_template(path: str) -> str:
50
+ def load_template(path: str,key:str="system_prompt") -> str:
51
51
  """
52
52
  Load the YAML file and extract its 'system_prompt' field.
53
53
  """
54
54
  with open(path, "r") as f:
55
55
  data = yaml.safe_load(f)
56
- return data["system_prompt"]
56
+ return data[key]
57
57
 
58
58
  def render_system_prompt(template_str: str,
59
59
  tools: dict,
@@ -78,7 +78,7 @@ def create_sandbox(
78
78
 
79
79
  if apt_packages is None:
80
80
  # Always install the basics required for most workflows
81
- apt_packages = ("git", "curl", "nodejs", "npm")
81
+ apt_packages = ("git", "curl", "nodejs", "npm","ripgrep","tree")
82
82
 
83
83
  if default_packages is None:
84
84
  default_packages = (
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Dict, List, Any, Optional
2
+ from typing import Dict, List, Any, Optional, Set
3
3
  from tinyagent.hooks.logging_manager import LoggingManager
4
4
  import cloudpickle
5
5
 
@@ -35,6 +35,15 @@ class CodeExecutionProvider(ABC):
35
35
  self._locals_dict = kwargs.get("locals_dict", {})
36
36
  self._user_variables = {}
37
37
  self.code_tools_definitions = []
38
+ # Safe shell commands that don't modify the system or access sensitive data
39
+ self.safe_shell_commands: Set[str] = {
40
+ "ls", "cat", "grep", "find", "echo", "pwd", "whoami", "date",
41
+ "head", "tail", "wc", "sort", "uniq", "tr", "cut", "sed", "awk",
42
+ "ps", "df", "du", "uname", "which", "type", "file", "stat","rg","if",
43
+ "tree"
44
+ }
45
+ # Safe control operators for shell commands
46
+ self.safe_control_operators: Set[str] = {"&&", "||", ";", "|"}
38
47
 
39
48
  @abstractmethod
40
49
  async def execute_python(
@@ -58,6 +67,129 @@ class CodeExecutionProvider(ABC):
58
67
  """
59
68
  pass
60
69
 
70
+ @abstractmethod
71
+ async def execute_shell(
72
+ self,
73
+ command: List[str],
74
+ timeout: int = 10,
75
+ workdir: Optional[str] = None
76
+ ) -> Dict[str, Any]:
77
+ """
78
+ Execute a shell command securely and return the result.
79
+
80
+ Args:
81
+ command: List of command parts to execute
82
+ timeout: Maximum execution time in seconds
83
+ workdir: Working directory for command execution
84
+
85
+ Returns:
86
+ Dictionary containing execution results with keys:
87
+ - stdout: stdout from the execution
88
+ - stderr: stderr from the execution
89
+ - exit_code: exit code from the command
90
+ """
91
+ pass
92
+
93
+ def is_safe_command(self, command: List[str]) -> Dict[str, Any]:
94
+ """
95
+ Check if a shell command is safe to execute.
96
+
97
+ Args:
98
+ command: List of command parts to check
99
+
100
+ Returns:
101
+ Dictionary with:
102
+ - safe: Boolean indicating if command is safe
103
+ - reason: Reason why command is not safe (if applicable)
104
+ """
105
+ if type(command) == str:
106
+ command = command.split(" ")
107
+ if not command or not isinstance(command, list) or len(command) == 0:
108
+ return {"safe": False, "reason": "Empty or invalid command"}
109
+
110
+ # Special handling for bash -c or bash -lc commands
111
+ if len(command) >= 3 and command[0] == "bash" and command[1] in ["-c", "-lc"]:
112
+ # For bash -c or bash -lc, we need to parse the command string that follows
113
+ # We'll extract commands from the bash command string and check them
114
+ bash_cmd_str = command[2]
115
+
116
+ # Simple parsing of the bash command to extract command names
117
+ # This is a basic implementation and might not cover all edge cases
118
+ import shlex
119
+ import re
120
+
121
+ try:
122
+ # Shell script keywords that should be allowed
123
+ shell_keywords = {
124
+ "if", "then", "else", "elif", "fi", "for", "do", "done",
125
+ "while", "until", "case", "esac", "in", "function", "select",
126
+ "time", "coproc", "true", "false"
127
+ }
128
+
129
+ # Split the command by common shell operators
130
+ cmd_parts = re.split(r'(\||;|&&|\|\||>|>>|<|<<)', bash_cmd_str)
131
+ commands_to_check = []
132
+
133
+ for part in cmd_parts:
134
+ part = part.strip()
135
+ if part and part not in ['|', ';', '&&', '||', '>', '>>', '<', '<<']:
136
+ # Get the first word which is typically the command
137
+ try:
138
+ words = shlex.split(part)
139
+ if words:
140
+ cmd_name = words[0].split('/')[-1] # Extract binary name
141
+
142
+ # Skip shell keywords
143
+ if cmd_name in shell_keywords:
144
+ continue
145
+
146
+ # Skip variable assignments (e.g., VAR=value)
147
+ if re.match(r'^[A-Za-z_][A-Za-z0-9_]*=', cmd_name):
148
+ continue
149
+
150
+ if cmd_name not in self.safe_shell_commands and '*' not in cmd_name and '?' not in cmd_name:
151
+ return {"safe": False, "reason": f"Unsafe command in bash script: {cmd_name}"}
152
+ except Exception:
153
+ # If parsing fails, be cautious and reject
154
+ return {"safe": False, "reason": "Could not parse bash command safely"}
155
+
156
+ # All commands in the bash script are safe
157
+ return {"safe": True}
158
+ except Exception as e:
159
+ return {"safe": False, "reason": f"Error parsing bash command: {str(e)}"}
160
+
161
+ # Normal command processing for non-bash -c commands
162
+ # Shell operators that might be passed as separate arguments
163
+ shell_operators = ['|', '>', '<', '>>', '<<', '&&', '||', ';']
164
+
165
+ # Extract actual commands from the command list, ignoring shell operators
166
+ commands_to_check = []
167
+ i = 0
168
+ while i < len(command):
169
+ if command[i] in shell_operators:
170
+ i += 1
171
+ continue
172
+
173
+ # Extract the binary name
174
+ bin_name = command[i].split("/")[-1]
175
+ commands_to_check.append(bin_name)
176
+
177
+ # Skip to next command after an operator
178
+ i += 1
179
+ while i < len(command) and command[i] not in shell_operators:
180
+ i += 1
181
+
182
+ # Check if all commands are in the safe list
183
+ for cmd in commands_to_check:
184
+ # Handle wildcards in command names (e.g., *.py)
185
+ if '*' in cmd or '?' in cmd:
186
+ continue
187
+
188
+ if cmd not in self.safe_shell_commands:
189
+ return {"safe": False, "reason": f"Unsafe command: {cmd}"}
190
+
191
+ return {"safe": True}
192
+
61
193
  @abstractmethod
62
194
  async def cleanup(self):
63
195
  """Clean up any resources used by the provider."""
@@ -129,14 +261,14 @@ class CodeExecutionProvider(ABC):
129
261
  if variables_str_list:
130
262
  # Find where to insert (after tools section if it exists)
131
263
  insert_index = 0
132
- for i, code in enumerate(self.default_python_codes):
264
+ for i, code in enumerate(self.code_tools_definitions):
133
265
  if "###########</tools>###########" in code:
134
266
  insert_index = i + 1
135
267
  break
136
268
 
137
269
  # Insert the variables code
138
270
  for j, var_code in enumerate(variables_str_list):
139
- self.default_python_codes.insert(insert_index + j, var_code)
271
+ self.code_tools_definitions.insert(insert_index + j, var_code)
140
272
 
141
273
  def _remove_existing_user_variables(self) -> None:
142
274
  """Remove existing user variables from default python codes."""
@@ -144,16 +276,16 @@ class CodeExecutionProvider(ABC):
144
276
  start_index = None
145
277
  end_index = None
146
278
 
147
- for i, code in enumerate(self.default_python_codes):
279
+ for i, code in enumerate(self.code_tools_definitions):
148
280
  if "###########<user_variables>###########" in code:
149
- start_index = i - 1 if i > 0 and "import cloudpickle" in self.default_python_codes[i-1] else i
281
+ start_index = i - 1 if i > 0 and "import cloudpickle" in self.code_tools_definitions[i-1] else i
150
282
  elif "###########</user_variables>###########" in code:
151
283
  end_index = i + 2 # Include the newline after
152
284
  break
153
285
 
154
286
  if start_index is not None and end_index is not None:
155
287
  # Remove the old variables section
156
- del self.default_python_codes[start_index:end_index]
288
+ del self.code_tools_definitions[start_index:end_index]
157
289
 
158
290
  def get_user_variables(self) -> Dict[str, Any]:
159
291
  """
@@ -204,4 +336,18 @@ class CodeExecutionProvider(ABC):
204
336
  self._user_variables[var_name] = var_value
205
337
  except Exception:
206
338
  # If serialization fails, skip this variable
207
- pass
339
+ pass
340
+
341
+ def shell_response_to_llm_understandable(self, response: Dict[str, Any]) -> str:
342
+ """
343
+ Convert a shell command response to a format that is understandable by the LLM.
344
+ """
345
+ if response.get('stderr',None) not in [None,""]:
346
+ error_message = "Bash Error: " + response['stderr']
347
+ if "No such file or directory" in response['stderr']:
348
+ error_message.replace("No such file or directory", "No such file or directory, Have you provided the correct absolute path? If you are unsure use ls first to make sure the path exists")
349
+ if "Command timed out after" in response['stderr']:
350
+ error_message += ", Make sure your command is specific enough. And only if it is the most specific and optimized command then try to increase the timeout parameter if you need to more time for this command."
351
+ return error_message
352
+ else:
353
+ return response['stdout']
@@ -1,9 +1,22 @@
1
1
  import sys
2
2
  import modal
3
3
  import cloudpickle
4
+ from pprint import pprint
4
5
  from typing import Dict, List, Any, Optional, Union
5
6
  from .base import CodeExecutionProvider
6
- from ..utils import clean_response, make_session_blob, _run_python
7
+ from ..utils import clean_response, make_session_blob, _run_python, _run_shell
8
+ try:
9
+ from ..modal_sandbox import COLOR
10
+ except ImportError:
11
+ # Fallback colors if modal_sandbox is not available
12
+ COLOR = {
13
+ "HEADER": "\033[95m",
14
+ "BLUE": "\033[94m",
15
+ "GREEN": "\033[92m",
16
+ "RED": "\033[91m",
17
+ "ENDC": "\033[0m",
18
+ }
19
+
7
20
 
8
21
 
9
22
  class ModalProvider(CodeExecutionProvider):
@@ -16,6 +29,7 @@ class ModalProvider(CodeExecutionProvider):
16
29
  """
17
30
 
18
31
  PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"
32
+ TIMEOUT_MAX = 120
19
33
 
20
34
  def __init__(
21
35
  self,
@@ -27,25 +41,39 @@ class ModalProvider(CodeExecutionProvider):
27
41
  apt_packages: Optional[List[str]] = None,
28
42
  python_version: Optional[str] = None,
29
43
  authorized_imports: list[str] | None = None,
44
+ authorized_functions: list[str] | None = None,
30
45
  modal_secrets: Dict[str, Union[str, None]] | None = None,
31
46
  lazy_init: bool = True,
32
47
  sandbox_name: str = "tinycodeagent-sandbox",
33
48
  local_execution: bool = False,
49
+ check_string_obfuscation: bool = True,
34
50
  **kwargs
35
51
  ):
36
- """Create a ModalProvider instance.
37
-
38
- Additional keyword arguments (passed via **kwargs) are ignored by the
39
- base class but accepted here for forward-compatibility.
40
-
52
+ """
53
+ Initialize Modal-based code execution provider.
54
+
41
55
  Args:
42
- default_packages: Base set of Python packages installed into the
43
- sandbox image. If ``None`` a sane default list is used. The
44
- final set of installed packages is the union of
45
- ``default_packages`` and ``pip_packages``.
46
- apt_packages: Debian/Ubuntu APT packages to install into the image
47
- prior to ``pip install``. Defaults to an empty list. Always
48
- installed *in addition to* the basics required by TinyAgent
56
+ log_manager: Log manager instance
57
+ default_python_codes: List of Python code snippets to execute before user code
58
+ code_tools: List of code tools to make available
59
+ pip_packages: List of pip packages to install in the sandbox
60
+ default_packages: List of default pip packages to install in the sandbox
61
+ apt_packages: List of apt packages to install in the sandbox
62
+ python_version: Python version to use in the sandbox
63
+ authorized_imports: Optional allow-list of modules the user code is permitted to import
64
+ authorized_functions: Optional allow-list of dangerous functions the user code is permitted to use
65
+ modal_secrets: Dictionary of secrets to make available to the sandbox
66
+ lazy_init: Whether to initialize Modal app lazily
67
+ sandbox_name: Name of the Modal sandbox
68
+ local_execution: Whether to execute code locally
69
+ check_string_obfuscation: If True (default), check for string obfuscation techniques. Set to False to allow legitimate use of base64 encoding and other string manipulations.
70
+ **kwargs: Additional keyword arguments
71
+
72
+ Note:
73
+ The Modal sandbox is a secure environment for executing untrusted code.
74
+ It provides isolation from the host system and other sandboxes.
75
+
76
+ Default packages are always installed, while pip_packages are added to
49
77
  (git, curl, …) so you only need to specify the extras.
50
78
  python_version: Python version used for the sandbox image. If
51
79
  ``None`` the current interpreter version is used.
@@ -63,7 +91,7 @@ class ModalProvider(CodeExecutionProvider):
63
91
  ]
64
92
 
65
93
  if apt_packages is None:
66
- apt_packages = ["git", "curl", "nodejs", "npm"]
94
+ apt_packages = ["git", "curl", "nodejs", "npm","ripgrep"]
67
95
 
68
96
  if python_version is None:
69
97
  python_version = self.PYTHON_VERSION
@@ -74,6 +102,8 @@ class ModalProvider(CodeExecutionProvider):
74
102
  self.python_version: str = python_version
75
103
  self.authorized_imports = authorized_imports
76
104
 
105
+ self.authorized_functions = authorized_functions or []
106
+ self.check_string_obfuscation = check_string_obfuscation
77
107
  # ----------------------------------------------------------------------
78
108
  final_packages = list(set(self.default_packages + (pip_packages or [])))
79
109
 
@@ -92,6 +122,7 @@ class ModalProvider(CodeExecutionProvider):
92
122
  self.modal_secrets = modal.Secret.from_dict(self.secrets)
93
123
  self.app = None
94
124
  self._app_run_python = None
125
+ self._app_run_shell = None
95
126
  self.is_trusted_code = kwargs.get("trust_code", False)
96
127
 
97
128
  self._setup_modal_app()
@@ -117,6 +148,7 @@ class ModalProvider(CodeExecutionProvider):
117
148
  )
118
149
 
119
150
  self._app_run_python = self.app.function()(_run_python)
151
+ self._app_run_shell = self.app.function()(_run_shell)
120
152
 
121
153
  # Add tools if provided
122
154
  if self.code_tools:
@@ -139,7 +171,7 @@ class ModalProvider(CodeExecutionProvider):
139
171
  full_code = "\n".join(code_lines)
140
172
 
141
173
  print("#" * 100)
142
- print("#########################code#########################")
174
+ print("##########################################code##########################################")
143
175
  print(full_code)
144
176
  print("#" * 100)
145
177
 
@@ -170,6 +202,91 @@ class ModalProvider(CodeExecutionProvider):
170
202
 
171
203
  return clean_response(response)
172
204
 
205
+ async def execute_shell(
206
+ self,
207
+ command: List[str],
208
+ timeout: int = 30,
209
+ workdir: Optional[str] = None
210
+ ) -> Dict[str, Any]:
211
+ """
212
+ Execute a shell command securely using Modal.
213
+
214
+ Args:
215
+ command: List of command parts to execute
216
+ timeout: Maximum execution time in seconds
217
+ workdir: Working directory for command execution
218
+
219
+ Returns:
220
+ Dictionary containing execution results with keys:
221
+ - stdout: stdout from the execution
222
+ - stderr: stderr from the execution
223
+ - exit_code: exit code from the command
224
+ """
225
+ # First, check if the command is safe to execute
226
+ timeout = min(timeout, self.TIMEOUT_MAX)
227
+ if type(command) == str:
228
+ command = command.split(" ")
229
+
230
+ print("#########################<Bash>#########################")
231
+ print(f"{COLOR['BLUE']}>{command}{COLOR['ENDC']}")
232
+ safety_check = self.is_safe_command(command)
233
+ if not safety_check["safe"]:
234
+
235
+ response = {
236
+ "stdout": "",
237
+ "stderr": f"Command rejected for security reasons: {safety_check.get('reason', 'Unsafe command')}",
238
+ "exit_code": 1
239
+ }
240
+ print(f"{COLOR['RED']}{response['stderr']}{COLOR['ENDC']}")
241
+ return response
242
+ #execution_mode = "🏠 LOCALLY" if self.local_execution else "☁️ REMOTELY"
243
+ #print(f"Executing shell command {execution_mode} via Modal: {' '.join(command)}")
244
+
245
+ # Show working directory information
246
+ if workdir:
247
+ print(f"Working directory: {workdir}")
248
+
249
+ # If using Modal for remote execution
250
+ if not self.local_execution:
251
+ try:
252
+ with self.app.run():
253
+ result = self._app_run_shell.remote(
254
+ command=command,
255
+ timeout=timeout,
256
+ workdir=workdir
257
+ )
258
+
259
+
260
+ print(f"{COLOR['GREEN']}{result}{COLOR['ENDC']}")
261
+ return result
262
+ except Exception as e:
263
+ response = {
264
+ "stdout": "",
265
+ "stderr": f"Error executing shell command: {str(e)}",
266
+ "exit_code": 1
267
+ }
268
+
269
+ print(f"{COLOR['RED']}{response['stderr']}{COLOR['ENDC']}")
270
+ return response
271
+ # If executing locally
272
+ else:
273
+ try:
274
+ result = self._app_run_shell.local(
275
+ command=command,
276
+ timeout=timeout,
277
+ workdir=workdir
278
+ )
279
+ print(f"{COLOR['GREEN']}{result}{COLOR['ENDC']}")
280
+ return result
281
+ except Exception as e:
282
+ response = {
283
+ "stdout": "",
284
+ "stderr": f"Error executing shell command: {str(e)}",
285
+ "exit_code": 1
286
+ }
287
+ print(f"{COLOR['RED']}{response['stderr']}{COLOR['ENDC']}")
288
+ return response
289
+
173
290
  def _python_executor(self, code: str, globals_dict: Dict[str, Any] = None, locals_dict: Dict[str, Any] = None):
174
291
  """Execute Python code using Modal's native .local() or .remote() methods."""
175
292
  execution_mode = "🏠 LOCALLY" if self.local_execution else "☁️ REMOTELY"
@@ -191,8 +308,10 @@ class ModalProvider(CodeExecutionProvider):
191
308
  full_code,
192
309
  globals_dict or {},
193
310
  locals_dict or {},
194
- self.authorized_imports,
195
- self.is_trusted_code,
311
+ authorized_imports=self.authorized_imports,
312
+ authorized_functions=self.authorized_functions,
313
+ trusted_code=self.is_trusted_code,
314
+ check_string_obfuscation=self.check_string_obfuscation,
196
315
  )
197
316
  else:
198
317
  with self.app.run():
@@ -200,8 +319,10 @@ class ModalProvider(CodeExecutionProvider):
200
319
  full_code,
201
320
  globals_dict or {},
202
321
  locals_dict or {},
203
- self.authorized_imports,
204
- self.is_trusted_code,
322
+ authorized_imports=self.authorized_imports,
323
+ authorized_functions=self.authorized_functions,
324
+ trusted_code=self.is_trusted_code,
325
+ check_string_obfuscation=self.check_string_obfuscation,
205
326
  )
206
327
 
207
328
  def _log_response(self, response: Dict[str, Any]):
@@ -224,14 +345,7 @@ class ModalProvider(CodeExecutionProvider):
224
345
  # Check if this is a security exception and highlight it in red if so
225
346
  error_text = response["error_traceback"]
226
347
  if "SECURITY" in error_text:
227
- try:
228
- from ..modal_sandbox import COLOR
229
- except ImportError:
230
- # Fallback colors if modal_sandbox is not available
231
- COLOR = {
232
- "RED": "\033[91m",
233
- "ENDC": "\033[0m",
234
- }
348
+
235
349
  print(f"{COLOR['RED']}{error_text}{COLOR['ENDC']}")
236
350
  else:
237
351
  print(error_text)
@@ -295,7 +295,8 @@ def _detect_string_obfuscation(tree: ast.AST) -> bool:
295
295
 
296
296
 
297
297
  def validate_code_safety(code: str, *, authorized_imports: Sequence[str] | None = None,
298
- authorized_functions: Sequence[str] | None = None, trusted_code: bool = False) -> None:
298
+ authorized_functions: Sequence[str] | None = None, trusted_code: bool = False,
299
+ check_string_obfuscation: bool = True) -> None:
299
300
  """Static validation of user code.
300
301
 
301
302
  Parameters
@@ -312,6 +313,9 @@ def validate_code_safety(code: str, *, authorized_imports: Sequence[str] | None
312
313
  trusted_code
313
314
  If True, skip security checks. This should only be used for code that is part of the
314
315
  framework, developer-provided tools, or default executed code.
316
+ check_string_obfuscation
317
+ If True (default), check for string obfuscation techniques. Set to False to allow
318
+ legitimate use of base64 encoding and other string manipulations.
315
319
  """
316
320
  # Skip security checks for trusted code
317
321
  if trusted_code:
@@ -384,7 +388,7 @@ def validate_code_safety(code: str, *, authorized_imports: Sequence[str] | None
384
388
  # ------------------------------------------------------------------
385
389
  # Detect string obfuscation techniques that might be used to bypass security
386
390
  # ------------------------------------------------------------------
387
- if _detect_string_obfuscation(tree):
391
+ if check_string_obfuscation and _detect_string_obfuscation(tree):
388
392
  raise ValueError("SECURITY VIOLATION: Suspicious string manipulation detected that could be used to bypass security.")
389
393
 
390
394
  if blocked: