tinyagent-py 0.0.9__py3-none-any.whl → 0.0.12__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.
@@ -63,6 +63,7 @@ def create_sandbox(
63
63
  pip_install: Sequence[str] | None = None,
64
64
  image_name: str = "tinyagent-sandbox-image",
65
65
  app_name: str = "persistent-code-session",
66
+ force_build: bool = False,
66
67
  **sandbox_kwargs,
67
68
  ) -> Tuple[modal.Sandbox, modal.App]:
68
69
  """Create (or lookup) a `modal.Sandbox` pre-configured for code execution.
@@ -99,7 +100,7 @@ def create_sandbox(
99
100
 
100
101
  # Build image -----------------------------------------------------------
101
102
  agent_image = (
102
- modal.Image.debian_slim(python_version=python_version)
103
+ modal.Image.debian_slim(python_version=python_version,force_build=force_build)
103
104
  .apt_install(*apt_packages)
104
105
  .pip_install(*full_pip_list)
105
106
  )
@@ -196,6 +197,7 @@ class SandboxSession:
196
197
  modal_secrets: modal.Secret,
197
198
  *,
198
199
  timeout: int = 5 * 60,
200
+
199
201
  **create_kwargs,
200
202
  ) -> None:
201
203
  self.modal_secrets = modal_secrets
@@ -26,6 +26,7 @@ class ModalProvider(CodeExecutionProvider):
26
26
  default_packages: Optional[List[str]] = None,
27
27
  apt_packages: Optional[List[str]] = None,
28
28
  python_version: Optional[str] = None,
29
+ authorized_imports: list[str] | None = None,
29
30
  modal_secrets: Dict[str, Union[str, None]] | None = None,
30
31
  lazy_init: bool = True,
31
32
  sandbox_name: str = "tinycodeagent-sandbox",
@@ -48,6 +49,7 @@ class ModalProvider(CodeExecutionProvider):
48
49
  (git, curl, …) so you only need to specify the extras.
49
50
  python_version: Python version used for the sandbox image. If
50
51
  ``None`` the current interpreter version is used.
52
+ authorized_imports: Optional allow-list of modules the user code is permitted to import. Supports wildcard patterns (e.g. "pandas.*"). If ``None`` the safety layer blocks only the predefined dangerous modules.
51
53
  """
52
54
 
53
55
  # Resolve default values ------------------------------------------------
@@ -70,6 +72,7 @@ class ModalProvider(CodeExecutionProvider):
70
72
  self.default_packages: List[str] = default_packages
71
73
  self.apt_packages: List[str] = apt_packages
72
74
  self.python_version: str = python_version
75
+ self.authorized_imports = authorized_imports
73
76
 
74
77
  # ----------------------------------------------------------------------
75
78
  final_packages = list(set(self.default_packages + (pip_packages or [])))
@@ -89,6 +92,7 @@ class ModalProvider(CodeExecutionProvider):
89
92
  self.modal_secrets = modal.Secret.from_dict(self.secrets)
90
93
  self.app = None
91
94
  self._app_run_python = None
95
+ self.is_trusted_code = kwargs.get("trust_code", False)
92
96
 
93
97
  self._setup_modal_app()
94
98
 
@@ -164,18 +168,30 @@ class ModalProvider(CodeExecutionProvider):
164
168
  if self.executed_default_codes:
165
169
  print("✔️ default codes already executed")
166
170
  full_code = "\n".join(self.code_tools_definitions) +"\n\n"+code
171
+ # Code tools and default code are trusted, user code is not
167
172
  else:
168
173
  full_code = "\n".join(self.code_tools_definitions) +"\n\n"+ "\n".join(self.default_python_codes) + "\n\n" + code
169
174
  self.executed_default_codes = True
175
+ # First execution includes framework code which is trusted
170
176
 
171
177
  # Use Modal's native execution methods
172
178
  if self.local_execution:
173
- # Use Modal's .local() method for local execution
174
- return self._app_run_python.local(full_code, globals_dict or {}, locals_dict or {})
179
+ return self._app_run_python.local(
180
+ full_code,
181
+ globals_dict or {},
182
+ locals_dict or {},
183
+ self.authorized_imports,
184
+ self.is_trusted_code,
185
+ )
175
186
  else:
176
- # Use Modal's .remote() method for remote execution
177
187
  with self.app.run():
178
- return self._app_run_python.remote(full_code, globals_dict or {}, locals_dict or {})
188
+ return self._app_run_python.remote(
189
+ full_code,
190
+ globals_dict or {},
191
+ locals_dict or {},
192
+ self.authorized_imports,
193
+ self.is_trusted_code,
194
+ )
179
195
 
180
196
  def _log_response(self, response: Dict[str, Any]):
181
197
  """Log the response from code execution."""
@@ -184,15 +200,31 @@ class ModalProvider(CodeExecutionProvider):
184
200
  print("#########################<printed_output>#########################")
185
201
  print(response["printed_output"])
186
202
  print("#########################</printed_output>#########################")
187
- print("#########################<return_value>#########################")
188
- print(response["return_value"])
189
- print("#########################</return_value>#########################")
190
- print("#########################<stderr>#########################")
191
- print(response["stderr"])
192
- print("#########################</stderr>#########################")
193
- print("#########################<traceback>#########################")
194
- print(response["error_traceback"])
195
- print("#########################</traceback>#########################")
203
+ if response.get("return_value",None) not in [None,""]:
204
+ print("#########################<return_value>#########################")
205
+ print(response["return_value"])
206
+ print("#########################</return_value>#########################")
207
+ if response.get("stderr",None) not in [None,""]:
208
+ print("#########################<stderr>#########################")
209
+ print(response["stderr"])
210
+ print("#########################</stderr>#########################")
211
+ if response.get("error_traceback",None) not in [None,""]:
212
+ print("#########################<traceback>#########################")
213
+ # Check if this is a security exception and highlight it in red if so
214
+ error_text = response["error_traceback"]
215
+ 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
+ }
224
+ print(f"{COLOR['RED']}{error_text}{COLOR['ENDC']}")
225
+ else:
226
+ print(error_text)
227
+ print("#########################</traceback>#########################")
196
228
 
197
229
  async def cleanup(self):
198
230
  """Clean up Modal resources."""
@@ -0,0 +1,546 @@
1
+ # TinyAgent code execution safety utilities
2
+ # -----------------------------------------
3
+ #
4
+ # This helper module defines *very* lightweight safeguards that are applied
5
+ # before running any user-supplied Python code inside the Modal sandbox.
6
+ # The goal is **not** to build a full blown secure interpreter (this would
7
+ # require a much more sophisticated setup à la Pyodide or the `python-secure`
8
+ # project). Instead we implement the following pragmatic defence layers:
9
+ #
10
+ # 1. Static AST inspection of the submitted code to detect direct `import` or
11
+ # `from … import …` statements that reference known dangerous modules
12
+ # (e.g. `os`, `subprocess`, …). This prevents the *most common* attack
13
+ # vector where an LLM attempts to read or modify the host file-system or
14
+ # spawn sub-processes.
15
+ # 2. Runtime patching of the built-in `__import__` hook so that *dynamic*
16
+ # imports carried out via `importlib` or `__import__(…)` are blocked at
17
+ # execution time as well.
18
+ # 3. Static AST inspection to detect calls to dangerous functions like `exec`,
19
+ # `eval`, `compile`, etc. that could be used to bypass security measures.
20
+ # 4. Runtime patching of built-in dangerous functions to prevent their use
21
+ # at execution time.
22
+ #
23
+ # The chosen approach keeps the TinyAgent runtime *fast* and *lean* while
24
+ # still providing a reasonable first line of defence against obviously
25
+ # malicious code.
26
+
27
+ from __future__ import annotations
28
+
29
+ import ast
30
+ import builtins
31
+ import warnings
32
+ from typing import Iterable, List, Set, Sequence, Any, Callable
33
+ import contextlib
34
+
35
+ __all__ = [
36
+ "DANGEROUS_MODULES",
37
+ "DANGEROUS_FUNCTIONS",
38
+ "RUNTIME_BLOCKED_FUNCTIONS",
39
+ "validate_code_safety",
40
+ "install_import_hook",
41
+ "function_safety_context",
42
+ ]
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Threat model / deny-list
46
+ # ---------------------------------------------------------------------------
47
+
48
+ # Non-exhaustive list of modules that grant (direct or indirect) access to the
49
+ # underlying operating system, spawn sub-processes, perform unrestricted I/O,
50
+ # or allow the user to circumvent the static import analysis performed below.
51
+ DANGEROUS_MODULES: Set[str] = {
52
+ "builtins", # Gives access to exec/eval etc.
53
+ "ctypes",
54
+ "importlib",
55
+ "io",
56
+ "multiprocessing",
57
+ "os",
58
+ "pathlib",
59
+ "pty",
60
+ "shlex",
61
+ "shutil",
62
+ "signal",
63
+ "socket",
64
+ "subprocess",
65
+ "sys",
66
+ "tempfile",
67
+ "threading",
68
+ "webbrowser",
69
+ }
70
+
71
+ # List of dangerous built-in functions that could be used to bypass security
72
+ # measures or execute arbitrary code
73
+ DANGEROUS_FUNCTIONS: Set[str] = {
74
+ "exec",
75
+ "eval",
76
+ "compile",
77
+ "__import__",
78
+ "open",
79
+ "input",
80
+ "breakpoint",
81
+ }
82
+
83
+ # Functions that should be blocked at runtime (a subset of DANGEROUS_FUNCTIONS)
84
+ RUNTIME_BLOCKED_FUNCTIONS: Set[str] = {
85
+ "exec",
86
+ "eval",
87
+ }
88
+
89
+ # Essential modules that are always allowed, even in untrusted code
90
+ # These are needed for the framework to function properly
91
+ ESSENTIAL_MODULES: Set[str] = {
92
+ "cloudpickle",
93
+ "tinyagent",
94
+ "json",
95
+ "time",
96
+ "datetime",
97
+ "requests",
98
+
99
+ }
100
+
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # Utility helpers
104
+ # ---------------------------------------------------------------------------
105
+
106
+ def _is_allowed(module_root: str, allowed: Sequence[str] | None) -> bool:
107
+ """Return ``True`` if *module_root* is within *allowed* specification."""
108
+
109
+ if allowed is None:
110
+ # No explicit allow-list means everything that is **not** in the
111
+ # dangerous list is considered fine.
112
+ return True
113
+
114
+ # Fast path – wildcard allows everything.
115
+ if "*" in allowed:
116
+ return True
117
+
118
+ for pattern in allowed:
119
+ if pattern.endswith(".*"):
120
+ if module_root == pattern[:-2]:
121
+ return True
122
+ elif module_root == pattern:
123
+ return True
124
+ return False
125
+
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Static analysis helpers
129
+ # ---------------------------------------------------------------------------
130
+
131
+ def _iter_import_nodes(tree: ast.AST) -> Iterable[ast.AST]:
132
+ """Yield all *import* related nodes from *tree*."""
133
+ for node in ast.walk(tree):
134
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
135
+ yield node
136
+
137
+
138
+ def _extract_module_roots(node: ast.AST) -> List[str]:
139
+ """Return the *top-level* module names referenced in an import node."""
140
+ roots: list[str] = []
141
+ if isinstance(node, ast.Import):
142
+ for alias in node.names:
143
+ roots.append(alias.name.split(".")[0])
144
+ elif isinstance(node, ast.ImportFrom):
145
+ if node.module is not None:
146
+ roots.append(node.module.split(".")[0])
147
+ return roots
148
+
149
+
150
+ def _check_for_dangerous_function_calls(tree: ast.AST, authorized_functions: Sequence[str] | None = None) -> Set[str]:
151
+ """
152
+ Check for calls to dangerous functions in the AST.
153
+
154
+ Parameters
155
+ ----------
156
+ tree
157
+ The AST to check
158
+ authorized_functions
159
+ Optional white-list of dangerous functions that are allowed
160
+
161
+ Returns
162
+ -------
163
+ Set[str]
164
+ Set of dangerous function names found in the code
165
+ """
166
+ dangerous_calls = set()
167
+
168
+ # Convert authorized_functions to a set if it's not None and not a boolean
169
+ authorized_set = set(authorized_functions) if authorized_functions is not None and not isinstance(authorized_functions, bool) else set()
170
+
171
+ for node in ast.walk(tree):
172
+ # Check for direct function calls: func()
173
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
174
+ func_name = node.func.id
175
+ # Only flag if it's a known dangerous function
176
+ if func_name in DANGEROUS_FUNCTIONS and func_name not in authorized_set:
177
+ dangerous_calls.add(func_name)
178
+
179
+ # Check for calls via string literals in exec/eval: exec("import os")
180
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in ["exec", "eval"]:
181
+ # Only check if the function itself is not authorized
182
+ if node.func.id not in authorized_set:
183
+ if node.args and isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
184
+ dangerous_calls.add(f"{node.func.id} with string literal")
185
+
186
+ # Check for attribute access: builtins.exec()
187
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
188
+ if node.func.attr in DANGEROUS_FUNCTIONS:
189
+ if isinstance(node.func.value, ast.Name):
190
+ module_name = node.func.value.id
191
+ # Focus on builtins module access
192
+ if module_name == "builtins" and node.func.attr not in authorized_set:
193
+ func_name = f"{module_name}.{node.func.attr}"
194
+ dangerous_calls.add(func_name)
195
+
196
+ # Check for string manipulation that could be used to bypass security
197
+ # For example: e = "e" + "x" + "e" + "c"; e("import os")
198
+ if isinstance(node, ast.Assign):
199
+ for target in node.targets:
200
+ if isinstance(target, ast.Name) and isinstance(node.value, ast.BinOp):
201
+ # Check if we're building a string that could be a dangerous function name
202
+ potential_name = _extract_string_from_binop(node.value)
203
+ if potential_name in DANGEROUS_FUNCTIONS and potential_name not in authorized_set:
204
+ dangerous_calls.add(f"string manipulation to create '{potential_name}'")
205
+
206
+ # Check for getattr(builtins, "exec") pattern
207
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "getattr":
208
+ if len(node.args) >= 2 and isinstance(node.args[1], ast.Constant) and isinstance(node.args[1].value, str):
209
+ attr_name = node.args[1].value
210
+ if attr_name in DANGEROUS_FUNCTIONS and attr_name not in authorized_set:
211
+ if isinstance(node.args[0], ast.Name) and node.args[0].id == "builtins":
212
+ module_name = node.args[0].id
213
+ dangerous_calls.add(f"getattr({module_name}, '{attr_name}')")
214
+
215
+ return dangerous_calls
216
+
217
+
218
+ def _extract_string_from_binop(node: ast.BinOp) -> str:
219
+ """
220
+ Attempt to extract a string from a binary operation node.
221
+ This helps detect string concatenation that builds dangerous function names.
222
+
223
+ For example: "e" + "x" + "e" + "c" -> "exec"
224
+
225
+ Parameters
226
+ ----------
227
+ node
228
+ The binary operation node
229
+
230
+ Returns
231
+ -------
232
+ str
233
+ The extracted string, or empty string if not extractable
234
+ """
235
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
236
+ return node.value
237
+
238
+ if not isinstance(node, ast.BinOp):
239
+ return ""
240
+
241
+ # Handle string concatenation
242
+ if isinstance(node.op, ast.Add):
243
+ left_str = _extract_string_from_binop(node.left)
244
+ right_str = _extract_string_from_binop(node.right)
245
+ return left_str + right_str
246
+
247
+ return ""
248
+
249
+
250
+ def _detect_string_obfuscation(tree: ast.AST) -> bool:
251
+ """
252
+ Detect common string obfuscation techniques that might be used to bypass security.
253
+
254
+ Parameters
255
+ ----------
256
+ tree
257
+ The AST to check
258
+
259
+ Returns
260
+ -------
261
+ bool
262
+ True if suspicious string manipulation is detected
263
+ """
264
+ suspicious_patterns = False
265
+
266
+ for node in ast.walk(tree):
267
+ # Check for chr() usage to build strings
268
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "chr":
269
+ suspicious_patterns = True
270
+ break
271
+
272
+ # Check for ord() usage in combination with string operations
273
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "ord":
274
+ suspicious_patterns = True
275
+ break
276
+
277
+ # Check for suspicious string joins with list comprehensions
278
+ if (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and
279
+ node.func.attr == "join" and isinstance(node.args[0], ast.ListComp)):
280
+ suspicious_patterns = True
281
+ break
282
+
283
+ # Check for base64 decoding
284
+ if (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and
285
+ node.func.attr in ["b64decode", "b64encode", "b32decode", "b32encode", "b16decode", "b16encode"]):
286
+ suspicious_patterns = True
287
+ break
288
+
289
+ # Check for string formatting that might be used to build dangerous code
290
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "format":
291
+ suspicious_patterns = True
292
+ break
293
+
294
+ return suspicious_patterns
295
+
296
+
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:
299
+ """Static validation of user code.
300
+
301
+ Parameters
302
+ ----------
303
+ code
304
+ The user supplied source code (single string or multi-line).
305
+ authorized_imports
306
+ Optional white-list restricting which modules may be imported. If
307
+ *None* every module that is not in :pydata:`DANGEROUS_MODULES` is
308
+ allowed. Wildcards are supported – e.g. ``["numpy.*"]`` allows any
309
+ sub-package of *numpy*.
310
+ authorized_functions
311
+ Optional white-list of dangerous functions that are allowed.
312
+ trusted_code
313
+ If True, skip security checks. This should only be used for code that is part of the
314
+ framework, developer-provided tools, or default executed code.
315
+ """
316
+ # Skip security checks for trusted code
317
+ if trusted_code:
318
+ return
319
+
320
+ try:
321
+ tree = ast.parse(code, mode="exec")
322
+ except SyntaxError:
323
+ # If the code does not even parse we leave the error handling to the
324
+ # caller (who will attempt to compile / execute the code later on).
325
+ return
326
+
327
+ blocked: set[str] = set()
328
+
329
+ # Convert authorized_imports to a set if it's not None and not a boolean
330
+ combined_allowed = None
331
+ if authorized_imports is not None and not isinstance(authorized_imports, bool):
332
+ combined_allowed = set(list(authorized_imports) + list(ESSENTIAL_MODULES))
333
+
334
+
335
+ for node in _iter_import_nodes(tree):
336
+ for root in _extract_module_roots(node):
337
+
338
+ # Check if module is explicitly allowed
339
+ if combined_allowed is not None:
340
+ allowed = _is_allowed(root, combined_allowed)
341
+ else:
342
+ # If no explicit allow-list, only allow if not in DANGEROUS_MODULES
343
+ allowed = root not in DANGEROUS_MODULES
344
+
345
+ if root in DANGEROUS_MODULES and allowed and combined_allowed is not None:
346
+ warnings.warn(
347
+ f"⚠️ Importing dangerous module '{root}' was allowed due to authorized_imports configuration.",
348
+ stacklevel=2,
349
+ )
350
+
351
+ # Block dangerous modules unless explicitly allowed
352
+ if root in DANGEROUS_MODULES and not allowed:
353
+ blocked.add(root)
354
+ # If there is an explicit allow-list, block everything not on it
355
+ elif authorized_imports is not None and not isinstance(authorized_imports, bool) and not allowed and root not in ESSENTIAL_MODULES:
356
+ blocked.add(root)
357
+
358
+ # ------------------------------------------------------------------
359
+ # Detect direct calls to __import__ (e.g. __import__("os")) in *untrusted* code
360
+ # ------------------------------------------------------------------
361
+ for _node in ast.walk(tree):
362
+ if isinstance(_node, ast.Call):
363
+ # Pattern: __import__(...)
364
+ if isinstance(_node.func, ast.Name) and _node.func.id == "__import__":
365
+ # Check if it's a direct call to the built-in __import__
366
+ raise ValueError("SECURITY VIOLATION: Usage of __import__ is not allowed in untrusted code.")
367
+ # Pattern: builtins.__import__(...)
368
+ if (
369
+ isinstance(_node.func, ast.Attribute)
370
+ and isinstance(_node.func.value, ast.Name)
371
+ and _node.func.attr == "__import__"
372
+ and _node.func.value.id == "builtins"
373
+ ):
374
+ raise ValueError("SECURITY VIOLATION: Usage of builtins.__import__ is not allowed in untrusted code.")
375
+
376
+ # ------------------------------------------------------------------
377
+ # Detect calls to dangerous functions (e.g. exec, eval) in *untrusted* code
378
+ # ------------------------------------------------------------------
379
+ dangerous_calls = _check_for_dangerous_function_calls(tree, authorized_functions)
380
+ if dangerous_calls:
381
+ offenders = ", ".join(sorted(dangerous_calls))
382
+ raise ValueError(f"SECURITY VIOLATION: Usage of dangerous function(s) {offenders} is not allowed in untrusted code.")
383
+
384
+ # ------------------------------------------------------------------
385
+ # Detect string obfuscation techniques that might be used to bypass security
386
+ # ------------------------------------------------------------------
387
+ if _detect_string_obfuscation(tree):
388
+ raise ValueError("SECURITY VIOLATION: Suspicious string manipulation detected that could be used to bypass security.")
389
+
390
+ if blocked:
391
+ offenders = ", ".join(sorted(blocked))
392
+ msg = f"SECURITY VIOLATION: Importing module(s) {offenders} is not allowed."
393
+ if authorized_imports is not None and not isinstance(authorized_imports, bool):
394
+ msg += " Allowed imports are: " + ", ".join(sorted(authorized_imports))
395
+ raise ValueError(msg)
396
+
397
+
398
+ # ---------------------------------------------------------------------------
399
+ # Runtime import hook
400
+ # ---------------------------------------------------------------------------
401
+
402
+ def install_import_hook(
403
+ *,
404
+ blocked_modules: Set[str] | None = None,
405
+ authorized_imports: Sequence[str] | None = None,
406
+ trusted_code: bool = False,
407
+ ) -> None:
408
+ """Monkey-patch the built-in ``__import__`` to deny run-time imports.
409
+
410
+ The hook is *process-wide* but extremely cheap to install. It simply
411
+ checks the *root* package name against the provided *blocked_modules*
412
+ (defaults to :pydata:`DANGEROUS_MODULES`) and raises ``ImportError`` if the
413
+ import should be denied.
414
+
415
+ Calling this function **multiple times** is safe – only the first call
416
+ installs the wrapper, subsequent calls are ignored.
417
+
418
+ Parameters
419
+ ----------
420
+ blocked_modules
421
+ Set of module names to block. Defaults to DANGEROUS_MODULES.
422
+ authorized_imports
423
+ Optional white-list restricting which modules may be imported.
424
+ trusted_code
425
+ If True, skip security checks. This should only be used for code that is part of the
426
+ framework, developer-provided tools, or default executed code.
427
+ """
428
+ # Skip security checks for trusted code
429
+ if trusted_code:
430
+ return
431
+
432
+ blocked_modules = blocked_modules or DANGEROUS_MODULES
433
+
434
+ # Convert authorized_imports to a set if it's not None and not a boolean
435
+ authorized_set = set(authorized_imports) if authorized_imports is not None and not isinstance(authorized_imports, bool) else None
436
+
437
+ # Create a combined set for allowed modules (essential + authorized)
438
+ combined_allowed = None
439
+ if authorized_set is not None:
440
+ combined_allowed = set(list(authorized_set) + list(ESSENTIAL_MODULES))
441
+
442
+ # Check if we have already installed the hook to avoid double-wrapping.
443
+ if getattr(builtins, "__tinyagent_import_hook_installed", False):
444
+ return
445
+
446
+ original_import = builtins.__import__
447
+
448
+ def _safe_import(
449
+ name: str,
450
+ globals=None,
451
+ locals=None,
452
+ fromlist=(),
453
+ level: int = 0,
454
+ ): # type: ignore[override]
455
+ root = name.split(".")[0]
456
+
457
+ # Check if module is explicitly allowed
458
+ if combined_allowed is not None:
459
+ allowed = _is_allowed(root, combined_allowed)
460
+ else:
461
+ # If no explicit allow-list, only allow if not in blocked_modules
462
+ allowed = root not in blocked_modules
463
+
464
+ if root in blocked_modules and allowed and authorized_set is not None:
465
+ warnings.warn(
466
+ f"⚠️ Importing dangerous module '{root}' was allowed due to authorized_imports configuration.",
467
+ stacklevel=2,
468
+ )
469
+ elif root in blocked_modules and not allowed:
470
+ error_msg = f"SECURITY VIOLATION: Import of module '{name}' is blocked by TinyAgent security policy"
471
+ if authorized_set is not None:
472
+ error_msg += f". Allowed imports are: {', '.join(sorted(authorized_set))}"
473
+ raise ImportError(error_msg)
474
+ elif authorized_set is not None and not allowed and root not in ESSENTIAL_MODULES:
475
+ error_msg = f"SECURITY VIOLATION: Import of module '{name}' is not in the authorized imports list"
476
+ if authorized_set:
477
+ error_msg += f": {', '.join(sorted(authorized_set))}"
478
+ raise ImportError(error_msg)
479
+
480
+ return original_import(name, globals, locals, fromlist, level)
481
+
482
+ builtins.__import__ = _safe_import # type: ignore[assignment]
483
+ setattr(builtins, "__tinyagent_import_hook_installed", True)
484
+
485
+
486
+ # ---------------------------------------------------------------------------
487
+ # Runtime function hook
488
+ # ---------------------------------------------------------------------------
489
+
490
+ @contextlib.contextmanager
491
+ def function_safety_context(
492
+ *,
493
+ blocked_functions: Set[str] | None = None,
494
+ authorized_functions: Sequence[str] | None = None,
495
+ trusted_code: bool = False,
496
+ ):
497
+ """
498
+ Context manager for safely executing code with dangerous functions blocked.
499
+
500
+ Parameters
501
+ ----------
502
+ blocked_functions
503
+ Set of function names to block. Defaults to RUNTIME_BLOCKED_FUNCTIONS.
504
+ authorized_functions
505
+ Optional white-list of dangerous functions that are allowed.
506
+ trusted_code
507
+ If True, skip security checks. This should only be used for code that is part of the
508
+ framework, developer-provided tools, or default executed code.
509
+ """
510
+ if trusted_code:
511
+ yield
512
+ return
513
+
514
+ # Install the function hook
515
+ blocked_functions = blocked_functions or RUNTIME_BLOCKED_FUNCTIONS
516
+
517
+ # Convert authorized_functions to a set if it's not None and not a boolean
518
+ authorized_set = set(authorized_functions) if authorized_functions is not None and not isinstance(authorized_functions, bool) else set()
519
+
520
+ # Store original functions
521
+ original_functions = {}
522
+
523
+ # Replace dangerous functions with safe versions
524
+ for func_name in blocked_functions:
525
+ # Only block functions that exist in builtins
526
+ if hasattr(builtins, func_name) and func_name not in authorized_set:
527
+ original_functions[func_name] = getattr(builtins, func_name)
528
+
529
+ # Create a closure to capture the function name
530
+ def make_safe_function(name):
531
+ def safe_function(*args, **kwargs):
532
+ error_msg = f"SECURITY VIOLATION: Function '{name}' is blocked by TinyAgent security policy"
533
+ if authorized_functions and not isinstance(authorized_functions, bool) and isinstance(authorized_functions, (list, tuple, set)):
534
+ error_msg += f". Allowed functions are: {', '.join(sorted(authorized_set))}"
535
+ raise RuntimeError(error_msg)
536
+ return safe_function
537
+
538
+ # Replace the function
539
+ setattr(builtins, func_name, make_safe_function(func_name))
540
+
541
+ try:
542
+ yield
543
+ finally:
544
+ # Restore original functions
545
+ for func_name, original_func in original_functions.items():
546
+ setattr(builtins, func_name, original_func)