tinyagent-py 0.0.13__py3-none-any.whl → 0.0.16__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,4 +1,17 @@
1
1
  from .base import CodeExecutionProvider
2
2
  from .modal_provider import ModalProvider
3
3
 
4
- __all__ = ["CodeExecutionProvider", "ModalProvider"]
4
+ # Import SeatbeltProvider conditionally to avoid errors on non-macOS systems
5
+ import platform
6
+ if platform.system() == "Darwin":
7
+ try:
8
+ from .seatbelt_provider import SeatbeltProvider
9
+ except ImportError:
10
+ # If there's an issue importing, just don't make it available
11
+ pass
12
+
13
+ __all__ = ["CodeExecutionProvider", "ModalProvider"]
14
+
15
+ # Add SeatbeltProvider to __all__ if it was successfully imported
16
+ if platform.system() == "Darwin" and "SeatbeltProvider" in globals():
17
+ __all__.append("SeatbeltProvider")
@@ -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
 
@@ -21,6 +21,9 @@ class CodeExecutionProvider(ABC):
21
21
  pip_packages: List[str] = None,
22
22
  secrets: Dict[str, Any] = None,
23
23
  lazy_init: bool = True,
24
+ bypass_shell_safety: bool = False,
25
+ additional_safe_shell_commands: Optional[List[str]] = None,
26
+ additional_safe_control_operators: Optional[List[str]] = None,
24
27
  **kwargs
25
28
  ):
26
29
  self.log_manager = log_manager
@@ -35,6 +38,36 @@ class CodeExecutionProvider(ABC):
35
38
  self._locals_dict = kwargs.get("locals_dict", {})
36
39
  self._user_variables = {}
37
40
  self.code_tools_definitions = []
41
+
42
+ # Shell safety configuration
43
+ self.bypass_shell_safety = bypass_shell_safety
44
+
45
+ # Safe shell commands that don't modify the system or access sensitive data
46
+ self.safe_shell_commands: Set[str] = {
47
+ "ls", "cat", "grep", "find", "echo", "pwd", "whoami", "date",
48
+ "head", "tail", "wc", "sort", "uniq", "tr", "cut", "sed", "awk",
49
+ "ps", "df", "du", "uname", "which", "type", "file", "stat", "rg", "if",
50
+ "tree"
51
+ }
52
+
53
+ # Add additional safe shell commands if provided
54
+ if additional_safe_shell_commands:
55
+ if "*" in additional_safe_shell_commands:
56
+ # If wildcard is provided, allow all commands (effectively bypassing the check)
57
+ self.bypass_shell_safety = True
58
+ else:
59
+ self.safe_shell_commands.update(additional_safe_shell_commands)
60
+
61
+ # Safe control operators for shell commands
62
+ self.safe_control_operators: Set[str] = {"&&", "||", ";", "|"}
63
+
64
+ # Add additional safe control operators if provided
65
+ if additional_safe_control_operators:
66
+ if "*" in additional_safe_control_operators:
67
+ # If wildcard is provided, allow all operators
68
+ self.safe_control_operators = set("*")
69
+ else:
70
+ self.safe_control_operators.update(additional_safe_control_operators)
38
71
 
39
72
  @abstractmethod
40
73
  async def execute_python(
@@ -58,6 +91,133 @@ class CodeExecutionProvider(ABC):
58
91
  """
59
92
  pass
60
93
 
94
+ @abstractmethod
95
+ async def execute_shell(
96
+ self,
97
+ command: List[str],
98
+ timeout: int = 10,
99
+ workdir: Optional[str] = None
100
+ ) -> Dict[str, Any]:
101
+ """
102
+ Execute a shell command securely and return the result.
103
+
104
+ Args:
105
+ command: List of command parts to execute
106
+ timeout: Maximum execution time in seconds
107
+ workdir: Working directory for command execution
108
+
109
+ Returns:
110
+ Dictionary containing execution results with keys:
111
+ - stdout: stdout from the execution
112
+ - stderr: stderr from the execution
113
+ - exit_code: exit code from the command
114
+ """
115
+ pass
116
+
117
+ def is_safe_command(self, command: List[str]) -> Dict[str, Any]:
118
+ """
119
+ Check if a shell command is safe to execute.
120
+
121
+ Args:
122
+ command: List of command parts to check
123
+
124
+ Returns:
125
+ Dictionary with:
126
+ - safe: Boolean indicating if command is safe
127
+ - reason: Reason why command is not safe (if applicable)
128
+ """
129
+ # If shell safety checks are bypassed, consider all commands safe
130
+ if self.bypass_shell_safety:
131
+ return {"safe": True}
132
+
133
+ if type(command) == str:
134
+ command = command.split(" ")
135
+ if not command or not isinstance(command, list) or len(command) == 0:
136
+ return {"safe": False, "reason": "Empty or invalid command"}
137
+
138
+ # Special handling for bash -c or bash -lc commands
139
+ if len(command) >= 3 and command[0] == "bash" and command[1] in ["-c", "-lc"]:
140
+ # For bash -c or bash -lc, we need to parse the command string that follows
141
+ # We'll extract commands from the bash command string and check them
142
+ bash_cmd_str = command[2]
143
+
144
+ # Simple parsing of the bash command to extract command names
145
+ # This is a basic implementation and might not cover all edge cases
146
+ import shlex
147
+ import re
148
+
149
+ try:
150
+ # Shell script keywords that should be allowed
151
+ shell_keywords = {
152
+ "if", "then", "else", "elif", "fi", "for", "do", "done",
153
+ "while", "until", "case", "esac", "in", "function", "select",
154
+ "time", "coproc", "true", "false"
155
+ }
156
+
157
+ # Split the command by common shell operators
158
+ cmd_parts = re.split(r'(\||;|&&|\|\||>|>>|<|<<)', bash_cmd_str)
159
+ commands_to_check = []
160
+
161
+ for part in cmd_parts:
162
+ part = part.strip()
163
+ if part and part not in ['|', ';', '&&', '||', '>', '>>', '<', '<<']:
164
+ # Get the first word which is typically the command
165
+ try:
166
+ words = shlex.split(part)
167
+ if words:
168
+ cmd_name = words[0].split('/')[-1] # Extract binary name
169
+
170
+ # Skip shell keywords
171
+ if cmd_name in shell_keywords:
172
+ continue
173
+
174
+ # Skip variable assignments (e.g., VAR=value)
175
+ if re.match(r'^[A-Za-z_][A-Za-z0-9_]*=', cmd_name):
176
+ continue
177
+
178
+ if cmd_name not in self.safe_shell_commands and '*' not in cmd_name and '?' not in cmd_name:
179
+ return {"safe": False, "reason": f"Unsafe command in bash script: {cmd_name}"}
180
+ except Exception:
181
+ # If parsing fails, be cautious and reject
182
+ return {"safe": False, "reason": "Could not parse bash command safely"}
183
+
184
+ # All commands in the bash script are safe
185
+ return {"safe": True}
186
+ except Exception as e:
187
+ return {"safe": False, "reason": f"Error parsing bash command: {str(e)}"}
188
+
189
+ # Normal command processing for non-bash -c commands
190
+ # Shell operators that might be passed as separate arguments
191
+ shell_operators = ['|', '>', '<', '>>', '<<', '&&', '||', ';']
192
+
193
+ # Extract actual commands from the command list, ignoring shell operators
194
+ commands_to_check = []
195
+ i = 0
196
+ while i < len(command):
197
+ if command[i] in shell_operators:
198
+ i += 1
199
+ continue
200
+
201
+ # Extract the binary name
202
+ bin_name = command[i].split("/")[-1]
203
+ commands_to_check.append(bin_name)
204
+
205
+ # Skip to next command after an operator
206
+ i += 1
207
+ while i < len(command) and command[i] not in shell_operators:
208
+ i += 1
209
+
210
+ # Check if all commands are in the safe list
211
+ for cmd in commands_to_check:
212
+ # Handle wildcards in command names (e.g., *.py)
213
+ if '*' in cmd or '?' in cmd:
214
+ continue
215
+
216
+ if cmd not in self.safe_shell_commands:
217
+ return {"safe": False, "reason": f"Unsafe command: {cmd}"}
218
+
219
+ return {"safe": True}
220
+
61
221
  @abstractmethod
62
222
  async def cleanup(self):
63
223
  """Clean up any resources used by the provider."""
@@ -129,14 +289,14 @@ class CodeExecutionProvider(ABC):
129
289
  if variables_str_list:
130
290
  # Find where to insert (after tools section if it exists)
131
291
  insert_index = 0
132
- for i, code in enumerate(self.default_python_codes):
292
+ for i, code in enumerate(self.code_tools_definitions):
133
293
  if "###########</tools>###########" in code:
134
294
  insert_index = i + 1
135
295
  break
136
296
 
137
297
  # Insert the variables code
138
298
  for j, var_code in enumerate(variables_str_list):
139
- self.default_python_codes.insert(insert_index + j, var_code)
299
+ self.code_tools_definitions.insert(insert_index + j, var_code)
140
300
 
141
301
  def _remove_existing_user_variables(self) -> None:
142
302
  """Remove existing user variables from default python codes."""
@@ -144,16 +304,16 @@ class CodeExecutionProvider(ABC):
144
304
  start_index = None
145
305
  end_index = None
146
306
 
147
- for i, code in enumerate(self.default_python_codes):
307
+ for i, code in enumerate(self.code_tools_definitions):
148
308
  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
309
+ start_index = i - 1 if i > 0 and "import cloudpickle" in self.code_tools_definitions[i-1] else i
150
310
  elif "###########</user_variables>###########" in code:
151
311
  end_index = i + 2 # Include the newline after
152
312
  break
153
313
 
154
314
  if start_index is not None and end_index is not None:
155
315
  # Remove the old variables section
156
- del self.default_python_codes[start_index:end_index]
316
+ del self.code_tools_definitions[start_index:end_index]
157
317
 
158
318
  def get_user_variables(self) -> Dict[str, Any]:
159
319
  """
@@ -204,4 +364,18 @@ class CodeExecutionProvider(ABC):
204
364
  self._user_variables[var_name] = var_value
205
365
  except Exception:
206
366
  # If serialization fails, skip this variable
207
- pass
367
+ pass
368
+
369
+ def shell_response_to_llm_understandable(self, response: Dict[str, Any]) -> str:
370
+ """
371
+ Convert a shell command response to a format that is understandable by the LLM.
372
+ """
373
+ if response.get('stderr',None) not in [None,""]:
374
+ error_message = "Bash Error: " + response['stderr']
375
+ if "No such file or directory" in response['stderr']:
376
+ 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")
377
+ if "Command timed out after" in response['stderr']:
378
+ 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."
379
+ return error_message
380
+ else:
381
+ 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,45 @@ 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,
50
+ bypass_shell_safety: bool = False, # Default to False for ModalProvider
51
+ additional_safe_shell_commands: Optional[List[str]] = None,
52
+ additional_safe_control_operators: Optional[List[str]] = None,
34
53
  **kwargs
35
54
  ):
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
-
55
+ """
56
+ Initialize Modal-based code execution provider.
57
+
41
58
  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
59
+ log_manager: Log manager instance
60
+ default_python_codes: List of Python code snippets to execute before user code
61
+ code_tools: List of code tools to make available
62
+ pip_packages: List of pip packages to install in the sandbox
63
+ default_packages: List of default pip packages to install in the sandbox
64
+ apt_packages: List of apt packages to install in the sandbox
65
+ python_version: Python version to use in the sandbox
66
+ authorized_imports: Optional allow-list of modules the user code is permitted to import
67
+ authorized_functions: Optional allow-list of dangerous functions the user code is permitted to use
68
+ modal_secrets: Dictionary of secrets to make available to the sandbox
69
+ lazy_init: Whether to initialize Modal app lazily
70
+ sandbox_name: Name of the Modal sandbox
71
+ local_execution: Whether to execute code locally
72
+ 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.
73
+ bypass_shell_safety: If True, bypass shell command safety checks (default: False for modal)
74
+ additional_safe_shell_commands: Additional shell commands to consider safe
75
+ additional_safe_control_operators: Additional shell control operators to consider safe
76
+ **kwargs: Additional keyword arguments
77
+
78
+ Note:
79
+ The Modal sandbox is a secure environment for executing untrusted code.
80
+ It provides isolation from the host system and other sandboxes.
81
+
82
+ Default packages are always installed, while pip_packages are added to
49
83
  (git, curl, …) so you only need to specify the extras.
50
84
  python_version: Python version used for the sandbox image. If
51
85
  ``None`` the current interpreter version is used.
@@ -63,7 +97,7 @@ class ModalProvider(CodeExecutionProvider):
63
97
  ]
64
98
 
65
99
  if apt_packages is None:
66
- apt_packages = ["git", "curl", "nodejs", "npm"]
100
+ apt_packages = ["git", "curl", "nodejs", "npm","ripgrep"]
67
101
 
68
102
  if python_version is None:
69
103
  python_version = self.PYTHON_VERSION
@@ -74,6 +108,8 @@ class ModalProvider(CodeExecutionProvider):
74
108
  self.python_version: str = python_version
75
109
  self.authorized_imports = authorized_imports
76
110
 
111
+ self.authorized_functions = authorized_functions or []
112
+ self.check_string_obfuscation = check_string_obfuscation
77
113
  # ----------------------------------------------------------------------
78
114
  final_packages = list(set(self.default_packages + (pip_packages or [])))
79
115
 
@@ -84,6 +120,9 @@ class ModalProvider(CodeExecutionProvider):
84
120
  pip_packages=final_packages,
85
121
  secrets=modal_secrets or {},
86
122
  lazy_init=lazy_init,
123
+ bypass_shell_safety=bypass_shell_safety,
124
+ additional_safe_shell_commands=additional_safe_shell_commands,
125
+ additional_safe_control_operators=additional_safe_control_operators,
87
126
  **kwargs
88
127
  )
89
128
 
@@ -92,6 +131,7 @@ class ModalProvider(CodeExecutionProvider):
92
131
  self.modal_secrets = modal.Secret.from_dict(self.secrets)
93
132
  self.app = None
94
133
  self._app_run_python = None
134
+ self._app_run_shell = None
95
135
  self.is_trusted_code = kwargs.get("trust_code", False)
96
136
 
97
137
  self._setup_modal_app()
@@ -117,6 +157,7 @@ class ModalProvider(CodeExecutionProvider):
117
157
  )
118
158
 
119
159
  self._app_run_python = self.app.function()(_run_python)
160
+ self._app_run_shell = self.app.function()(_run_shell)
120
161
 
121
162
  # Add tools if provided
122
163
  if self.code_tools:
@@ -139,7 +180,7 @@ class ModalProvider(CodeExecutionProvider):
139
180
  full_code = "\n".join(code_lines)
140
181
 
141
182
  print("#" * 100)
142
- print("#########################code#########################")
183
+ print("##########################################code##########################################")
143
184
  print(full_code)
144
185
  print("#" * 100)
145
186
 
@@ -170,6 +211,91 @@ class ModalProvider(CodeExecutionProvider):
170
211
 
171
212
  return clean_response(response)
172
213
 
214
+ async def execute_shell(
215
+ self,
216
+ command: List[str],
217
+ timeout: int = 30,
218
+ workdir: Optional[str] = None
219
+ ) -> Dict[str, Any]:
220
+ """
221
+ Execute a shell command securely using Modal.
222
+
223
+ Args:
224
+ command: List of command parts to execute
225
+ timeout: Maximum execution time in seconds
226
+ workdir: Working directory for command execution
227
+
228
+ Returns:
229
+ Dictionary containing execution results with keys:
230
+ - stdout: stdout from the execution
231
+ - stderr: stderr from the execution
232
+ - exit_code: exit code from the command
233
+ """
234
+ # First, check if the command is safe to execute
235
+ timeout = min(timeout, self.TIMEOUT_MAX)
236
+ if type(command) == str:
237
+ command = command.split(" ")
238
+
239
+ print("#########################<Bash>#########################")
240
+ print(f"{COLOR['BLUE']}>{command}{COLOR['ENDC']}")
241
+ safety_check = self.is_safe_command(command)
242
+ if not safety_check["safe"]:
243
+
244
+ response = {
245
+ "stdout": "",
246
+ "stderr": f"Command rejected for security reasons: {safety_check.get('reason', 'Unsafe command')}",
247
+ "exit_code": 1
248
+ }
249
+ print(f"{COLOR['RED']}{response['stderr']}{COLOR['ENDC']}")
250
+ return response
251
+ #execution_mode = "🏠 LOCALLY" if self.local_execution else "☁️ REMOTELY"
252
+ #print(f"Executing shell command {execution_mode} via Modal: {' '.join(command)}")
253
+
254
+ # Show working directory information
255
+ if workdir:
256
+ print(f"Working directory: {workdir}")
257
+
258
+ # If using Modal for remote execution
259
+ if not self.local_execution:
260
+ try:
261
+ with self.app.run():
262
+ result = self._app_run_shell.remote(
263
+ command=command,
264
+ timeout=timeout,
265
+ workdir=workdir
266
+ )
267
+
268
+
269
+ print(f"{COLOR['GREEN']}{result}{COLOR['ENDC']}")
270
+ return result
271
+ except Exception as e:
272
+ response = {
273
+ "stdout": "",
274
+ "stderr": f"Error executing shell command: {str(e)}",
275
+ "exit_code": 1
276
+ }
277
+
278
+ print(f"{COLOR['RED']}{response['stderr']}{COLOR['ENDC']}")
279
+ return response
280
+ # If executing locally
281
+ else:
282
+ try:
283
+ result = self._app_run_shell.local(
284
+ command=command,
285
+ timeout=timeout,
286
+ workdir=workdir
287
+ )
288
+ print(f"{COLOR['GREEN']}{result}{COLOR['ENDC']}")
289
+ return result
290
+ except Exception as e:
291
+ response = {
292
+ "stdout": "",
293
+ "stderr": f"Error executing shell command: {str(e)}",
294
+ "exit_code": 1
295
+ }
296
+ print(f"{COLOR['RED']}{response['stderr']}{COLOR['ENDC']}")
297
+ return response
298
+
173
299
  def _python_executor(self, code: str, globals_dict: Dict[str, Any] = None, locals_dict: Dict[str, Any] = None):
174
300
  """Execute Python code using Modal's native .local() or .remote() methods."""
175
301
  execution_mode = "🏠 LOCALLY" if self.local_execution else "☁️ REMOTELY"
@@ -191,8 +317,10 @@ class ModalProvider(CodeExecutionProvider):
191
317
  full_code,
192
318
  globals_dict or {},
193
319
  locals_dict or {},
194
- self.authorized_imports,
195
- self.is_trusted_code,
320
+ authorized_imports=self.authorized_imports,
321
+ authorized_functions=self.authorized_functions,
322
+ trusted_code=self.is_trusted_code,
323
+ check_string_obfuscation=self.check_string_obfuscation,
196
324
  )
197
325
  else:
198
326
  with self.app.run():
@@ -200,8 +328,10 @@ class ModalProvider(CodeExecutionProvider):
200
328
  full_code,
201
329
  globals_dict or {},
202
330
  locals_dict or {},
203
- self.authorized_imports,
204
- self.is_trusted_code,
331
+ authorized_imports=self.authorized_imports,
332
+ authorized_functions=self.authorized_functions,
333
+ trusted_code=self.is_trusted_code,
334
+ check_string_obfuscation=self.check_string_obfuscation,
205
335
  )
206
336
 
207
337
  def _log_response(self, response: Dict[str, Any]):
@@ -224,14 +354,7 @@ class ModalProvider(CodeExecutionProvider):
224
354
  # Check if this is a security exception and highlight it in red if so
225
355
  error_text = response["error_traceback"]
226
356
  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
- }
357
+
235
358
  print(f"{COLOR['RED']}{error_text}{COLOR['ENDC']}")
236
359
  else:
237
360
  print(error_text)