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.
- tinyagent/code_agent/helper.py +2 -2
- tinyagent/code_agent/modal_sandbox.py +1 -1
- tinyagent/code_agent/providers/base.py +212 -11
- tinyagent/code_agent/providers/modal_provider.py +157 -32
- tinyagent/code_agent/safety.py +6 -2
- tinyagent/code_agent/tiny_code_agent.py +317 -11
- tinyagent/code_agent/utils.py +129 -9
- tinyagent/hooks/__init__.py +3 -1
- tinyagent/hooks/gradio_callback.py +3 -2
- tinyagent/hooks/jupyter_notebook_callback.py +1464 -0
- tinyagent/hooks/token_tracker.py +564 -0
- tinyagent/prompts/summarize.yaml +96 -0
- tinyagent/tiny_agent.py +426 -17
- {tinyagent_py-0.0.12.dist-info → tinyagent_py-0.0.15.dist-info}/METADATA +11 -1
- {tinyagent_py-0.0.12.dist-info → tinyagent_py-0.0.15.dist-info}/RECORD +18 -15
- {tinyagent_py-0.0.12.dist-info → tinyagent_py-0.0.15.dist-info}/WHEEL +0 -0
- {tinyagent_py-0.0.12.dist-info → tinyagent_py-0.0.15.dist-info}/licenses/LICENSE +0 -0
- {tinyagent_py-0.0.12.dist-info → tinyagent_py-0.0.15.dist-info}/top_level.txt +0 -0
tinyagent/code_agent/helper.py
CHANGED
@@ -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[
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
-
"""
|
37
|
-
|
38
|
-
|
39
|
-
base class but accepted here for forward-compatibility.
|
40
|
-
|
52
|
+
"""
|
53
|
+
Initialize Modal-based code execution provider.
|
54
|
+
|
41
55
|
Args:
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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("
|
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
|
-
#
|
154
|
-
|
155
|
-
|
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.
|
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.
|
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
|
-
|
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)
|
tinyagent/code_agent/safety.py
CHANGED
@@ -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
|
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:
|