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.
- tinyagent/code_agent/helper.py +2 -2
- tinyagent/code_agent/modal_sandbox.py +1 -1
- tinyagent/code_agent/providers/base.py +153 -7
- tinyagent/code_agent/providers/modal_provider.py +141 -27
- tinyagent/code_agent/safety.py +6 -2
- tinyagent/code_agent/tiny_code_agent.py +303 -11
- tinyagent/code_agent/utils.py +97 -1
- tinyagent/hooks/__init__.py +3 -1
- 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.13.dist-info → tinyagent_py-0.0.15.dist-info}/METADATA +1 -1
- {tinyagent_py-0.0.13.dist-info → tinyagent_py-0.0.15.dist-info}/RECORD +17 -14
- {tinyagent_py-0.0.13.dist-info → tinyagent_py-0.0.15.dist-info}/WHEEL +0 -0
- {tinyagent_py-0.0.13.dist-info → tinyagent_py-0.0.15.dist-info}/licenses/LICENSE +0 -0
- {tinyagent_py-0.0.13.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,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.
|
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.
|
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.
|
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.
|
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.
|
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
|
-
"""
|
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,7 +171,7 @@ 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
|
|
@@ -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.
|
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.
|
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
|
-
|
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)
|
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:
|