tinyagent-py 0.0.12__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,6 +1,7 @@
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
+ import cloudpickle
4
5
 
5
6
 
6
7
  class CodeExecutionProvider(ABC):
@@ -34,6 +35,15 @@ class CodeExecutionProvider(ABC):
34
35
  self._locals_dict = kwargs.get("locals_dict", {})
35
36
  self._user_variables = {}
36
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] = {"&&", "||", ";", "|"}
37
47
 
38
48
  @abstractmethod
39
49
  async def execute_python(
@@ -57,6 +67,129 @@ class CodeExecutionProvider(ABC):
57
67
  """
58
68
  pass
59
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
+
60
193
  @abstractmethod
61
194
  async def cleanup(self):
62
195
  """Clean up any resources used by the provider."""
@@ -69,8 +202,6 @@ class CodeExecutionProvider(ABC):
69
202
  Args:
70
203
  tools: List of tool objects to add
71
204
  """
72
- import cloudpickle
73
-
74
205
  tools_str_list = ["import cloudpickle"]
75
206
  tools_str_list.append("###########<tools>###########\n")
76
207
  for tool in tools:
@@ -82,6 +213,22 @@ class CodeExecutionProvider(ABC):
82
213
  tools_str_list.append("\n\n")
83
214
  self.code_tools_definitions.extend(tools_str_list)
84
215
 
216
+ def set_code_tools(self, tools: List[Any]) -> None:
217
+ """
218
+ Set the code tools available in the execution environment.
219
+ Replaces any existing tools with the new list.
220
+
221
+ Args:
222
+ tools: List of tool objects to set
223
+ """
224
+ # Clear existing tools
225
+ self.code_tools = tools.copy()
226
+ self.code_tools_definitions = []
227
+
228
+ # Add the new tools
229
+ if tools:
230
+ self.add_tools(tools)
231
+
85
232
  def set_user_variables(self, variables: Dict[str, Any]) -> None:
86
233
  """
87
234
  Set user variables that will be available in the Python environment.
@@ -89,8 +236,6 @@ class CodeExecutionProvider(ABC):
89
236
  Args:
90
237
  variables: Dictionary of variable name -> value pairs
91
238
  """
92
- import cloudpickle
93
-
94
239
  self._user_variables = variables.copy()
95
240
 
96
241
  # Add variables to the execution environment by serializing them
@@ -116,14 +261,14 @@ class CodeExecutionProvider(ABC):
116
261
  if variables_str_list:
117
262
  # Find where to insert (after tools section if it exists)
118
263
  insert_index = 0
119
- for i, code in enumerate(self.default_python_codes):
264
+ for i, code in enumerate(self.code_tools_definitions):
120
265
  if "###########</tools>###########" in code:
121
266
  insert_index = i + 1
122
267
  break
123
268
 
124
269
  # Insert the variables code
125
270
  for j, var_code in enumerate(variables_str_list):
126
- self.default_python_codes.insert(insert_index + j, var_code)
271
+ self.code_tools_definitions.insert(insert_index + j, var_code)
127
272
 
128
273
  def _remove_existing_user_variables(self) -> None:
129
274
  """Remove existing user variables from default python codes."""
@@ -131,16 +276,16 @@ class CodeExecutionProvider(ABC):
131
276
  start_index = None
132
277
  end_index = None
133
278
 
134
- for i, code in enumerate(self.default_python_codes):
279
+ for i, code in enumerate(self.code_tools_definitions):
135
280
  if "###########<user_variables>###########" in code:
136
- 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
137
282
  elif "###########</user_variables>###########" in code:
138
283
  end_index = i + 2 # Include the newline after
139
284
  break
140
285
 
141
286
  if start_index is not None and end_index is not None:
142
287
  # Remove the old variables section
143
- del self.default_python_codes[start_index:end_index]
288
+ del self.code_tools_definitions[start_index:end_index]
144
289
 
145
290
  def get_user_variables(self) -> Dict[str, Any]:
146
291
  """
@@ -149,4 +294,60 @@ class CodeExecutionProvider(ABC):
149
294
  Returns:
150
295
  Dictionary of current user variables
151
296
  """
152
- return self._user_variables.copy()
297
+ return self._user_variables.copy()
298
+
299
+ def update_user_variables_from_globals(self, globals_dict: Dict[str, Any]) -> None:
300
+ """
301
+ Extract and update user variables from the globals dictionary after code execution.
302
+ This ensures that any modifications to user variables during code execution are preserved.
303
+
304
+ Args:
305
+ globals_dict: The globals dictionary after code execution
306
+ """
307
+ if not globals_dict or not self._user_variables:
308
+ return
309
+
310
+ # Update user variables with values from globals
311
+ for var_name in list(self._user_variables.keys()):
312
+ if var_name in globals_dict:
313
+ try:
314
+ # Try to serialize the value to ensure it's valid
315
+ cloudpickle.dumps(globals_dict[var_name])
316
+ # Update the user variable with the new value
317
+ self._user_variables[var_name] = globals_dict[var_name]
318
+ except Exception:
319
+ # If serialization fails, keep the old value
320
+ pass
321
+
322
+ # Check for new variables that might have been created
323
+ # This handles cases where LLM creates new variables that should be preserved
324
+ for var_name, var_value in globals_dict.items():
325
+ # Skip special variables, modules, and functions
326
+ if (var_name.startswith('__') or
327
+ var_name in ['builtins', 'cloudpickle'] or
328
+ callable(var_value) or
329
+ var_name in self._user_variables):
330
+ continue
331
+
332
+ try:
333
+ # Try to serialize the value to ensure it's valid
334
+ cloudpickle.dumps(var_value)
335
+ # Add the new variable to user variables
336
+ self._user_variables[var_name] = var_value
337
+ except Exception:
338
+ # If serialization fails, skip this variable
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,26 +171,122 @@ 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
 
146
-
147
178
 
148
179
  # Use Modal's native execution methods
149
180
  response = self._python_executor(full_code, self._globals_dict, self._locals_dict)
150
181
 
151
182
  print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!<response>!!!!!!!!!!!!!!!!!!!!!!!!!")
152
183
 
153
- # Update the instance globals and locals with the execution results
154
- self._globals_dict = cloudpickle.loads(make_session_blob(response["updated_globals"]))
155
- self._locals_dict = cloudpickle.loads(make_session_blob(response["updated_locals"]))
156
-
184
+ # Always update globals and locals dictionaries, regardless of whether there was an error
185
+ # This ensures variables are preserved even when code execution fails
186
+ try:
187
+ # Update globals and locals from the response
188
+ if "updated_globals" in response:
189
+ self._globals_dict = cloudpickle.loads(make_session_blob(response["updated_globals"]))
190
+
191
+ if "updated_locals" in response:
192
+ self._locals_dict = cloudpickle.loads(make_session_blob(response["updated_locals"]))
193
+
194
+ # Update user variables from the updated globals and locals
195
+ # This preserves any changes made to variables by the LLM
196
+ self.update_user_variables_from_globals(self._globals_dict)
197
+ self.update_user_variables_from_globals(self._locals_dict)
198
+ except Exception as e:
199
+ print(f"Warning: Failed to update globals/locals after execution: {str(e)}")
157
200
 
158
201
  self._log_response(response)
159
202
 
160
203
  return clean_response(response)
161
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
+
162
290
  def _python_executor(self, code: str, globals_dict: Dict[str, Any] = None, locals_dict: Dict[str, Any] = None):
163
291
  """Execute Python code using Modal's native .local() or .remote() methods."""
164
292
  execution_mode = "🏠 LOCALLY" if self.local_execution else "☁️ REMOTELY"
@@ -180,8 +308,10 @@ class ModalProvider(CodeExecutionProvider):
180
308
  full_code,
181
309
  globals_dict or {},
182
310
  locals_dict or {},
183
- self.authorized_imports,
184
- 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,
185
315
  )
186
316
  else:
187
317
  with self.app.run():
@@ -189,8 +319,10 @@ class ModalProvider(CodeExecutionProvider):
189
319
  full_code,
190
320
  globals_dict or {},
191
321
  locals_dict or {},
192
- self.authorized_imports,
193
- 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,
194
326
  )
195
327
 
196
328
  def _log_response(self, response: Dict[str, Any]):
@@ -213,14 +345,7 @@ class ModalProvider(CodeExecutionProvider):
213
345
  # Check if this is a security exception and highlight it in red if so
214
346
  error_text = response["error_traceback"]
215
347
  if "SECURITY" in error_text:
216
- try:
217
- from ..modal_sandbox import COLOR
218
- except ImportError:
219
- # Fallback colors if modal_sandbox is not available
220
- COLOR = {
221
- "RED": "\033[91m",
222
- "ENDC": "\033[0m",
223
- }
348
+
224
349
  print(f"{COLOR['RED']}{error_text}{COLOR['ENDC']}")
225
350
  else:
226
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: