From 5a81b5d2f9ec348f0c7489ba9ca679c22a975e68 Mon Sep 17 00:00:00 2001 From: allen-munsch Date: Tue, 16 Dec 2025 20:33:26 -0600 Subject: [PATCH 1/4] add bubbleproc sandboxing Signed-off-by: allen-munsch --- aider/args.py | 42 ++++++- aider/main.py | 23 ++++ aider/sandbox.py | 217 +++++++++++++++++++++++++++++++++++ requirements/requirements.in | 1 + 4 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 aider/sandbox.py diff --git a/aider/args.py b/aider/args.py index b16d14c4bc5..33c429034ce 100644 --- a/aider/args.py +++ b/aider/args.py @@ -987,7 +987,47 @@ def get_parser(default_config_files, git_root): f" {', '.join(supported_shells_list)}. Example: aider --shell-completions bash" ), ) - + # In the argument parser setup + group = parser.add_argument_group("Sandbox") + group.add_argument( + "--sandbox", + action="store_true", + default=True, + help="Enable subprocess sandboxing (default: True)", + ) + group.add_argument( + "--no-sandbox", + action="store_false", + dest="sandbox", + help="Disable subprocess sandboxing", + ) + group.add_argument( + "--sandbox-verbose", + action="store_true", + help="Show sandbox configuration on startup", + ) + group.add_argument( + "--sandbox-allow-gpg", + action="store_true", + help="Allow access to GPG keys for signed commits", + ) + group.add_argument( + "--sandbox-allow-ssh", + action="store_true", + help="Allow access to SSH keys for git operations", + ) + group.add_argument( + "--sandbox-no-network", + action="store_true", + help="Disable network access in sandbox (breaks API calls!)", + ) + group.add_argument( + "--sandbox-rw", + action="append", + default=[], + metavar="PATH", + help="Additional read-write paths for sandbox (can be repeated)", + ) ########## group = parser.add_argument_group("Deprecated model settings") # Add deprecated model shortcut arguments diff --git a/aider/main.py b/aider/main.py index 723cdc8c668..ffef6773013 100644 --- a/aider/main.py +++ b/aider/main.py @@ -53,6 +53,7 @@ from aider.onboarding import offer_openrouter_oauth, select_default_model from aider.repo import ANY_GIT_ERROR, GitRepo from aider.report import report_uncaught_exceptions, set_args_error_data +from aider.sandbox import enable_sandbox, SANDBOX_AVAILABLE from aider.versioncheck import check_version, install_from_main_branch, install_upgrade from aider.watch import FileWatcher @@ -886,6 +887,28 @@ def get_io(pretty): if right_repo_root: return await main_async(argv, input, output, right_repo_root, return_coder=return_coder) + # ========================================================================= + # SANDBOX INITIALIZATION - Protect against dangerous subprocess commands + # ========================================================================= + if getattr(args, 'sandbox', True) and not getattr(args, 'no_sandbox', False): + # Determine the project directory for sandboxing + sandbox_project_dir = git_root or git_dname or str(Path.cwd()) + + sandbox_enabled = enable_sandbox( + project_dir=sandbox_project_dir, + io=io, + network=not getattr(args, 'sandbox_no_network', False), + allow_gpg=getattr(args, 'sandbox_allow_gpg', False), + allow_ssh=getattr(args, 'sandbox_allow_ssh', False), + extra_rw=getattr(args, 'sandbox_rw', []), + verbose=getattr(args, 'sandbox_verbose', False), + ) + + if not sandbox_enabled and SANDBOX_AVAILABLE: + io.tool_warning("Subprocess sandboxing failed to initialize") + elif getattr(args, 'sandbox_verbose', False): + io.tool_output("Sandbox disabled by --no-sandbox flag") + # ========================================================================= if args.just_check_update: update_available = await check_version(io, just_check=True, verbose=args.verbose) diff --git a/aider/sandbox.py b/aider/sandbox.py new file mode 100644 index 00000000000..5d529a1e710 --- /dev/null +++ b/aider/sandbox.py @@ -0,0 +1,217 @@ +""" +Subprocess sandboxing for aider. + +Protects against accidental damage from AI-generated shell commands +by restricting filesystem access, network, and secrets. +""" + +import os +import sys +from typing import Optional + +# Try to import bubbleproc - it's optional +try: + import bubbleproc + SANDBOX_AVAILABLE = True +except ImportError: + SANDBOX_AVAILABLE = False + +_enabled = False + + +def get_env_passthrough() -> list[str]: + """Environment variables to pass through to sandboxed processes.""" + return [ + # API Keys (all providers aider supports) + "ANTHROPIC_API_KEY", + "OPENAI_API_KEY", + "OPENROUTER_API_KEY", + "AZURE_API_KEY", + "AZURE_API_BASE", + "AZURE_API_VERSION", + "GEMINI_API_KEY", + "GOOGLE_API_KEY", + "VERTEX_PROJECT", + "VERTEX_LOCATION", + "DEEPSEEK_API_KEY", + "GROQ_API_KEY", + "COHERE_API_KEY", + "MISTRAL_API_KEY", + "OLLAMA_API_BASE", + "OPENAI_API_BASE", + "OPENAI_API_TYPE", + "OPENAI_API_VERSION", + "OPENAI_ORGANIZATION", + + # Git configuration + "GIT_AUTHOR_NAME", + "GIT_AUTHOR_EMAIL", + "GIT_COMMITTER_NAME", + "GIT_COMMITTER_EMAIL", + "GIT_SSH_COMMAND", + "GIT_ASKPASS", + "GIT_TERMINAL_PROMPT", + "GIT_DIR", + "GIT_WORK_TREE", + "GIT_EXEC_PATH", + + # Terminal/Display + "TERM", + "COLORTERM", + "CLICOLOR", + "FORCE_COLOR", + "NO_COLOR", + "COLUMNS", + "LINES", + + # Aider configuration + "AIDER_MODEL", + "AIDER_OPUS", + "AIDER_SONNET", + "AIDER_DARK_MODE", + "AIDER_LIGHT_MODE", + "AIDER_AUTO_COMMITS", + "AIDER_DIRTY_COMMITS", + "AIDER_GITIGNORE", + "AIDER_LINT_CMD", + "AIDER_TEST_CMD", + "AIDER_AUTO_LINT", + "AIDER_AUTO_TEST", + "AIDER_VERBOSE", + "AIDER_SHOW_DIFFS", + + # Python/System + "PATH", + "HOME", + "USER", + "LOGNAME", + "SHELL", + "LANG", + "LC_ALL", + "LC_CTYPE", + "PYTHONPATH", + "PYTHONHOME", + "VIRTUAL_ENV", + "CONDA_DEFAULT_ENV", + "CONDA_PREFIX", + + # Editor + "EDITOR", + "VISUAL", + + # Proxy settings + "HTTP_PROXY", + "HTTPS_PROXY", + "NO_PROXY", + "http_proxy", + "https_proxy", + "no_proxy", + + # SSL + "SSL_CERT_FILE", + "SSL_CERT_DIR", + "REQUESTS_CA_BUNDLE", + "CURL_CA_BUNDLE", + ] + + +def enable_sandbox( + project_dir: str, + io=None, + network: bool = True, + allow_gpg: bool = False, + allow_ssh: bool = False, + extra_rw: Optional[list[str]] = None, + verbose: bool = False, +) -> bool: + """ + Enable subprocess sandboxing. + + Args: + project_dir: The project directory (git root) to allow writes to + io: Aider IO object for output (optional) + network: Allow network access (required for API calls) + allow_gpg: Allow access to ~/.gnupg for signed commits + allow_ssh: Allow access to ~/.ssh for git operations + extra_rw: Additional read-write paths + verbose: Show sandbox configuration + + Returns: + True if sandbox was enabled, False otherwise + """ + global _enabled + + if _enabled: + return True + + if not SANDBOX_AVAILABLE: + if io: + io.tool_warning( + "Sandboxing requested but 'bubbleproc' is not installed. " + "Install with: pip install bubbleproc" + ) + return False + + # Build list of read-write paths + rw_paths = [project_dir] + + # Add /tmp for temp files + rw_paths.append("/tmp") + + # Add virtualenv if active + if venv := os.environ.get("VIRTUAL_ENV"): + rw_paths.append(venv) + + # Add any extra paths + if extra_rw: + rw_paths.extend(extra_rw) + + # Build list of allowed secret paths + allow_secrets = [] + if allow_gpg: + allow_secrets.append(".gnupg") + if allow_ssh: + allow_secrets.append(".ssh") + + try: + bubbleproc.patch_subprocess( + rw=rw_paths, + network=network, + share_home=True, + env_passthrough=get_env_passthrough(), + allow_secrets=allow_secrets, + ) + _enabled = True + + if verbose and io: + io.tool_output("Sandbox enabled:") + io.tool_output(f" Project dir: {project_dir}") + io.tool_output(f" Network: {'enabled' if network else 'disabled'}") + io.tool_output(f" RW paths: {rw_paths}") + if allow_secrets: + io.tool_output(f" Allowed secrets: {allow_secrets}") + elif io: + io.tool_output(f"Sandbox enabled for: {project_dir}") + + return True + + except Exception as e: + if io: + io.tool_error(f"Failed to enable sandbox: {e}") + return False + + +def disable_sandbox(): + """Disable subprocess sandboxing.""" + global _enabled + + if not _enabled or not SANDBOX_AVAILABLE: + return + + bubbleproc.unpatch_subprocess() + _enabled = False + + +def is_enabled() -> bool: + """Check if sandboxing is currently enabled.""" + return _enabled \ No newline at end of file diff --git a/requirements/requirements.in b/requirements/requirements.in index d5195b87d9c..7b1ffa586a2 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -31,6 +31,7 @@ google-generativeai>=0.8.5 mcp>=1.12.3 textual>=6.0.0 truststore +bubbleproc==1.0.1 # The proper dependency is networkx[default], but this brings # in matplotlib and a bunch of other deps From 5815cd2734192afd927c98d0086cd2acf03be7d4 Mon Sep 17 00:00:00 2001 From: allen-munsch Date: Tue, 16 Dec 2025 21:06:15 -0600 Subject: [PATCH 2/4] bump bubbleproc to 1.0.2 Signed-off-by: allen-munsch --- requirements/requirements.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.in b/requirements/requirements.in index 7b1ffa586a2..f282892acb7 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -31,7 +31,7 @@ google-generativeai>=0.8.5 mcp>=1.12.3 textual>=6.0.0 truststore -bubbleproc==1.0.1 +bubbleproc==1.0.2 # The proper dependency is networkx[default], but this brings # in matplotlib and a bunch of other deps From 1d3b5a39a2b0b051fb445bf555e68722296e967e Mon Sep 17 00:00:00 2001 From: allen-munsch Date: Tue, 16 Dec 2025 21:49:36 -0600 Subject: [PATCH 3/4] bump version of bubbleproc to 1.0.3 Signed-off-by: allen-munsch --- requirements/requirements.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.in b/requirements/requirements.in index f282892acb7..1be7e501820 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -31,7 +31,7 @@ google-generativeai>=0.8.5 mcp>=1.12.3 textual>=6.0.0 truststore -bubbleproc==1.0.2 +bubbleproc==1.0.3 # The proper dependency is networkx[default], but this brings # in matplotlib and a bunch of other deps From 5e644ce33a5a023cd1b56388b824a772710a4ed7 Mon Sep 17 00:00:00 2001 From: allen-munsch Date: Tue, 16 Dec 2025 22:17:11 -0600 Subject: [PATCH 4/4] bump bubbleproc to 1.0.4, enhances core, releases GIL on bubblewrapped Signed-off-by: allen-munsch --- requirements/requirements.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.in b/requirements/requirements.in index 1be7e501820..882d374cff4 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -31,7 +31,7 @@ google-generativeai>=0.8.5 mcp>=1.12.3 textual>=6.0.0 truststore -bubbleproc==1.0.3 +bubbleproc==1.0.4 # The proper dependency is networkx[default], but this brings # in matplotlib and a bunch of other deps