44import asyncio
55import logging
66import os
7+ import re
78import sys
89import tempfile
910import warnings
1011from hashlib import sha256
1112from pathlib import Path
1213from string import Template
1314from types import SimpleNamespace
14- from typing import Any , Callable , ClassVar , List , Optional , Sequence , Union
15+ from typing import Any , Callable , ClassVar , Dict , List , Optional , Sequence , Tuple , Union
1516
1617from autogen_core import CancellationToken , Component
1718from autogen_core .code_executor import CodeBlock , CodeExecutor , FunctionWithRequirements , FunctionWithRequirementsStr
3233
3334A = ParamSpec ("A" )
3435
36+ logger = logging .getLogger (__name__ )
37+
38+ # Default hard memory cap applied via RLIMIT_AS when sandbox=True (POSIX only).
39+ # 512 MiB is enough for most small Python scripts while still bounding runaway
40+ # LLM-generated code. Callers that need more can override via the
41+ # ``sandbox_memory_bytes`` constructor argument.
42+ _DEFAULT_SANDBOX_MEMORY_BYTES : int = 512 * 1024 * 1024
43+
44+ # Env-var name patterns scrubbed when sandbox=True. Matched case-insensitively
45+ # against variable *names* (not values). Best-effort only: this is not a
46+ # substitute for a real sandbox, and secrets stored under non-matching names
47+ # will still leak to the subprocess.
48+ _CREDENTIAL_ENV_PATTERNS : Tuple [re .Pattern [str ], ...] = (
49+ re .compile (r".*_API_KEY$" , re .IGNORECASE ),
50+ re .compile (r".*_TOKEN$" , re .IGNORECASE ),
51+ re .compile (r".*_SECRET$" , re .IGNORECASE ),
52+ re .compile (r".*PASSWORD.*" , re .IGNORECASE ),
53+ re .compile (r"^AWS_.*" , re .IGNORECASE ),
54+ re .compile (r"^AZURE_.*" , re .IGNORECASE ),
55+ re .compile (r"^GCP_.*" , re .IGNORECASE ),
56+ re .compile (r"^GOOGLE_.*" , re .IGNORECASE ),
57+ re .compile (r"^OPENAI_.*" , re .IGNORECASE ),
58+ re .compile (r"^ANTHROPIC_.*" , re .IGNORECASE ),
59+ re .compile (r"^HF_.*" , re .IGNORECASE ),
60+ re .compile (r"^HUGGINGFACE_.*" , re .IGNORECASE ),
61+ re .compile (r"^GITHUB_TOKEN$" , re .IGNORECASE ),
62+ re .compile (r"^GH_TOKEN$" , re .IGNORECASE ),
63+ re .compile (r"^NPM_TOKEN$" , re .IGNORECASE ),
64+ re .compile (r"^PYPI_.*" , re .IGNORECASE ),
65+ )
66+
67+
68+ def _scrub_credentials_from_env (env : Dict [str , str ]) -> Dict [str , str ]:
69+ """Return a copy of ``env`` with credential-shaped variable names removed."""
70+ scrubbed : Dict [str , str ] = {}
71+ for key , value in env .items ():
72+ if any (pattern .match (key ) for pattern in _CREDENTIAL_ENV_PATTERNS ):
73+ continue
74+ scrubbed [key ] = value
75+ return scrubbed
76+
77+
78+ def _make_sandbox_preexec_fn (cpu_seconds : int , memory_bytes : int ) -> Optional [Callable [[], None ]]:
79+ """Build a ``preexec_fn`` that applies POSIX resource limits before ``exec``.
80+
81+ Returns ``None`` on platforms where the ``resource`` module is unavailable
82+ (notably Windows). The returned closure is intended to be passed as
83+ ``preexec_fn`` to :func:`asyncio.create_subprocess_exec`.
84+ """
85+ try :
86+ import resource # type: ignore[import-not-found]
87+ except ImportError :
88+ return None
89+
90+ def _apply_limits () -> None : # pragma: no cover - runs in child process
91+ resource .setrlimit (resource .RLIMIT_CPU , (cpu_seconds , cpu_seconds ))
92+ try :
93+ resource .setrlimit (resource .RLIMIT_AS , (memory_bytes , memory_bytes ))
94+ except (ValueError , OSError ):
95+ # Some platforms (e.g. macOS) refuse RLIMIT_AS; continue so
96+ # sandbox=True still provides env-scrub + CPU capping.
97+ pass
98+
99+ return _apply_limits
100+
35101
36102class LocalCommandLineCodeExecutorConfig (BaseModel ):
37103 """Configuration for LocalCommandLineCodeExecutor"""
@@ -40,6 +106,8 @@ class LocalCommandLineCodeExecutorConfig(BaseModel):
40106 work_dir : Optional [str ] = None
41107 functions_module : str = "functions"
42108 cleanup_temp_files : bool = True
109+ sandbox : Optional [bool ] = None
110+ sandbox_memory_bytes : int = _DEFAULT_SANDBOX_MEMORY_BYTES
43111
44112
45113class LocalCommandLineCodeExecutor (CodeExecutor , Component [LocalCommandLineCodeExecutorConfig ]):
@@ -81,6 +149,25 @@ class LocalCommandLineCodeExecutor(CodeExecutor, Component[LocalCommandLineCodeE
81149 functions_module (str, optional): The name of the module that will be created to store the functions. Defaults to "functions".
82150 cleanup_temp_files (bool, optional): Whether to automatically clean up temporary files after execution. Defaults to True.
83151 virtual_env_context (Optional[SimpleNamespace], optional): The virtual environment context. Defaults to None.
152+ sandbox (Optional[bool], optional): Opt-in best-effort hardening for LLM-generated code.
153+ This is **NOT** a substitute for :class:`~autogen_ext.code_executors.docker.DockerCommandLineCodeExecutor`
154+ and provides no strong isolation guarantees; it is only a defense-in-depth layer for
155+ users who cannot run Docker.
156+
157+ * ``None`` (default): backward-compatible behavior. Emits a :class:`DeprecationWarning`
158+ noting that a future release will make this parameter required, and executes code
159+ without any added hardening.
160+ * ``False``: explicit acknowledgement that no sandboxing should be applied. No
161+ warning is emitted.
162+ * ``True``: applies POSIX ``RLIMIT_CPU`` (= ``timeout + 5`` seconds) and
163+ ``RLIMIT_AS`` (default 512 MiB, see ``sandbox_memory_bytes``) via ``preexec_fn``
164+ on code-execution and pip-install subprocesses, and scrubs credential-shaped
165+ environment variables (``*_API_KEY``, ``*_TOKEN``, ``*_SECRET``, ``AWS_*``,
166+ ``OPENAI_*``, etc.) from the subprocess environment. On Windows, resource limits
167+ are unavailable and a warning is logged; env scrub still applies.
168+ sandbox_memory_bytes (int, optional): Address-space cap (``RLIMIT_AS``) applied when
169+ ``sandbox=True`` on POSIX. Defaults to 512 MiB. Ignored when ``sandbox`` is ``None``
170+ or ``False``, and on platforms without the ``resource`` module.
84171
85172 .. note::
86173 Using the current directory (".") as working directory is deprecated. Using it will raise a deprecation warning.
@@ -158,15 +245,38 @@ def __init__(
158245 functions_module : str = "functions" ,
159246 cleanup_temp_files : bool = True ,
160247 virtual_env_context : Optional [SimpleNamespace ] = None ,
248+ sandbox : Optional [bool ] = None ,
249+ sandbox_memory_bytes : int = _DEFAULT_SANDBOX_MEMORY_BYTES ,
161250 ):
162- # Issue warning about using LocalCommandLineCodeExecutor
163- warnings .warn (
164- "Using LocalCommandLineCodeExecutor may execute code on the local machine which can be unsafe. "
165- "For security, it is recommended to use DockerCommandLineCodeExecutor instead. "
166- "To install Docker, visit: https://docs.docker.com/get-docker/" ,
167- UserWarning ,
168- stacklevel = 2 ,
169- )
251+ # Warn based on the caller's choice of ``sandbox``. When ``sandbox`` is
252+ # left at its default (``None``), we emit a ``DeprecationWarning`` so
253+ # callers are nudged toward making an explicit decision before a future
254+ # release makes the parameter required. An explicit ``False`` opts out
255+ # of the warning (but preserves the insecure behavior), while ``True``
256+ # enables best-effort hardening. See issue #7462.
257+ if sandbox is None :
258+ deprecation_msg = (
259+ "LocalCommandLineCodeExecutor was constructed without an explicit "
260+ "`sandbox` argument. A future release will require this parameter. "
261+ "Pass `sandbox=True` for best-effort in-process hardening (POSIX "
262+ "resource limits + credential env-var scrub), or `sandbox=False` to "
263+ "explicitly acknowledge unsandboxed execution. For real isolation, "
264+ "prefer DockerCommandLineCodeExecutor. See "
265+ "https://github.com/microsoft/autogen/issues/7462."
266+ )
267+ warnings .warn (deprecation_msg , DeprecationWarning , stacklevel = 2 )
268+ logger .warning (deprecation_msg )
269+
270+ self ._sandbox : Optional [bool ] = sandbox
271+ self ._sandbox_memory_bytes : int = sandbox_memory_bytes
272+ if sandbox is True and sys .platform == "win32" :
273+ logger .warning (
274+ "LocalCommandLineCodeExecutor(sandbox=True) on Windows: POSIX "
275+ "resource limits (RLIMIT_CPU, RLIMIT_AS) are not available on "
276+ "this platform. Credential env-var scrubbing still applies, but "
277+ "no CPU/memory caps will be enforced. Use "
278+ "DockerCommandLineCodeExecutor for real isolation."
279+ )
170280
171281 if timeout < 1 :
172282 raise ValueError ("Timeout must be greater than or equal to 1." )
@@ -270,6 +380,33 @@ def cleanup_temp_files(self) -> bool:
270380 """(Experimental) Whether to automatically clean up temporary files after execution."""
271381 return self ._cleanup_temp_files
272382
383+ @property
384+ def sandbox (self ) -> Optional [bool ]:
385+ """(Experimental) Whether sandbox hardening is enabled. See ``__init__`` docs."""
386+ return self ._sandbox
387+
388+ def _prepare_sandbox_subprocess_kwargs (self , env : Dict [str , str ]) -> Tuple [Dict [str , str ], Dict [str , Any ]]:
389+ """Apply sandbox policy to a subprocess env + kwargs pair.
390+
391+ Returns ``(env, extra_kwargs)``. When ``sandbox`` is not ``True``, the
392+ env is returned unchanged and extra_kwargs is empty. When ``sandbox`` is
393+ ``True``, credential-shaped env vars are stripped and (on POSIX) a
394+ ``preexec_fn`` applying RLIMIT_CPU and RLIMIT_AS is attached.
395+ """
396+ if self ._sandbox is not True :
397+ return env , {}
398+
399+ scrubbed = _scrub_credentials_from_env (env )
400+ extra_kwargs : Dict [str , Any ] = {}
401+ if sys .platform != "win32" :
402+ preexec = _make_sandbox_preexec_fn (
403+ cpu_seconds = self ._timeout + 5 ,
404+ memory_bytes = self ._sandbox_memory_bytes ,
405+ )
406+ if preexec is not None :
407+ extra_kwargs ["preexec_fn" ] = preexec
408+ return scrubbed , extra_kwargs
409+
273410 async def _setup_functions (self , cancellation_token : CancellationToken ) -> None :
274411 func_file_content = build_python_functions_file (self ._functions )
275412 func_file = self .work_dir / f"{ self ._functions_module } .py"
@@ -290,13 +427,17 @@ async def _setup_functions(self, cancellation_token: CancellationToken) -> None:
290427 else :
291428 py_executable = sys .executable
292429
430+ pip_env , pip_extra_kwargs = self ._prepare_sandbox_subprocess_kwargs (os .environ .copy ())
431+
293432 task = asyncio .create_task (
294433 asyncio .create_subprocess_exec (
295434 py_executable ,
296435 * cmd_args ,
297436 cwd = self .work_dir ,
298437 stdout = asyncio .subprocess .PIPE ,
299438 stderr = asyncio .subprocess .PIPE ,
439+ env = pip_env ,
440+ ** pip_extra_kwargs ,
300441 )
301442 )
302443 cancellation_token .link_future (task )
@@ -422,6 +563,9 @@ async def _execute_code_dont_check_setup(
422563 # Shell commands (bash, sh, etc.)
423564 extra_args = [str (written_file .absolute ())]
424565
566+ # Apply sandbox policy (env scrub + POSIX rlimits) if enabled.
567+ env , sandbox_kwargs = self ._prepare_sandbox_subprocess_kwargs (env )
568+
425569 # Create a subprocess and run
426570 task = asyncio .create_task (
427571 asyncio .create_subprocess_exec (
@@ -431,6 +575,7 @@ async def _execute_code_dont_check_setup(
431575 stdout = asyncio .subprocess .PIPE ,
432576 stderr = asyncio .subprocess .PIPE ,
433577 env = env ,
578+ ** sandbox_kwargs ,
434579 )
435580 )
436581 cancellation_token .link_future (task )
@@ -514,6 +659,8 @@ def _to_config(self) -> LocalCommandLineCodeExecutorConfig:
514659 work_dir = str (self .work_dir ),
515660 functions_module = self ._functions_module ,
516661 cleanup_temp_files = self ._cleanup_temp_files ,
662+ sandbox = self ._sandbox ,
663+ sandbox_memory_bytes = self ._sandbox_memory_bytes ,
517664 )
518665
519666 @classmethod
@@ -523,4 +670,6 @@ def _from_config(cls, config: LocalCommandLineCodeExecutorConfig) -> Self:
523670 work_dir = Path (config .work_dir ) if config .work_dir is not None else None ,
524671 functions_module = config .functions_module ,
525672 cleanup_temp_files = config .cleanup_temp_files ,
673+ sandbox = config .sandbox ,
674+ sandbox_memory_bytes = config .sandbox_memory_bytes ,
526675 )
0 commit comments