From 8d3f3bca456b7a74293b07472e56a91dec9ef216 Mon Sep 17 00:00:00 2001 From: sharoneyal Date: Tue, 21 Oct 2025 19:06:18 +0300 Subject: [PATCH] Add support for SSL related interactions: (#2074) * Add support for SSL related interactions: 1. Inject SSL related env variables to git clone 2. Force LiteLLM to use aiohttp which honors SSL env. variables, this is done by setting configuration toml as follows: [litellm] disable_aiohttp = true * Update pr_agent/git_providers/git_provider.py Co-authored-by: qodo-merge-for-open-source[bot] <189517486+qodo-merge-for-open-source[bot]@users.noreply.github.com> * Guard get_git_ssl_env invocation with try-catch --------- Co-authored-by: qodo-merge-for-open-source[bot] <189517486+qodo-merge-for-open-source[bot]@users.noreply.github.com> --- .../algo/ai_handlers/litellm_ai_handler.py | 4 +- .../bitbucket_server_provider.py | 6 +- pr_agent/git_providers/git_provider.py | 70 ++++++++++++++++++- 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/pr_agent/algo/ai_handlers/litellm_ai_handler.py b/pr_agent/algo/ai_handlers/litellm_ai_handler.py index 31e3d12b..dd04591e 100644 --- a/pr_agent/algo/ai_handlers/litellm_ai_handler.py +++ b/pr_agent/algo/ai_handlers/litellm_ai_handler.py @@ -32,7 +32,9 @@ class LiteLLMAIHandler(BaseAiHandler): self.azure = False self.api_base = None self.repetition_penalty = None - + + if get_settings().get("LITELLM.DISABLE_AIOHTTP", False): + litellm.disable_aiohttp_transport = True if get_settings().get("OPENAI.KEY", None): openai.api_key = get_settings().openai.key litellm.openai_key = get_settings().openai.key diff --git a/pr_agent/git_providers/bitbucket_server_provider.py b/pr_agent/git_providers/bitbucket_server_provider.py index 6bb3c368..c929221a 100644 --- a/pr_agent/git_providers/bitbucket_server_provider.py +++ b/pr_agent/git_providers/bitbucket_server_provider.py @@ -18,7 +18,7 @@ from ..algo.utils import (find_line_number_of_relevant_line_in_file, load_large_diff) from ..config_loader import get_settings from ..log import get_logger -from .git_provider import GitProvider +from .git_provider import GitProvider, get_git_ssl_env class BitbucketServerProvider(GitProvider): @@ -561,5 +561,7 @@ class BitbucketServerProvider(GitProvider): cli_args = shlex.split(f"git clone -c http.extraHeader='Authorization: Bearer {bearer_token}' " f"--filter=blob:none --depth 1 {repo_url} {dest_folder}") - subprocess.run(cli_args, check=True, # check=True will raise an exception if the command fails + ssl_env = get_git_ssl_env() + + subprocess.run(cli_args, env=ssl_env, check=True, # check=True will raise an exception if the command fails stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=operation_timeout_in_seconds) diff --git a/pr_agent/git_providers/git_provider.py b/pr_agent/git_providers/git_provider.py index 7288f186..631e189c 100644 --- a/pr_agent/git_providers/git_provider.py +++ b/pr_agent/git_providers/git_provider.py @@ -12,6 +12,65 @@ from pr_agent.log import get_logger MAX_FILES_ALLOWED_FULL = 50 +def get_git_ssl_env() -> dict[str, str]: + """ + Get git SSL configuration arguments for per-command use. + This fixes SSL certificate issues when cloning repos with self-signed certificates. + Returns the current environment with the addition of SSL config changes if any such SSL certificates exist. + """ + ssl_cert_file = os.environ.get('SSL_CERT_FILE') + requests_ca_bundle = os.environ.get('REQUESTS_CA_BUNDLE') + git_ssl_ca_info = os.environ.get('GIT_SSL_CAINFO') + + chosen_cert_file = "" + + # Try SSL_CERT_FILE first + if ssl_cert_file: + if os.path.exists(ssl_cert_file): + if ((requests_ca_bundle and requests_ca_bundle != ssl_cert_file) + or (git_ssl_ca_info and git_ssl_ca_info != ssl_cert_file)): + get_logger().warning(f"Found mismatch among: SSL_CERT_FILE, REQUESTS_CA_BUNDLE, GIT_SSL_CAINFO. " + f"Using the SSL_CERT_FILE to resolve ambiguity.", + artifact={"ssl_cert_file": ssl_cert_file, "requests_ca_bundle": requests_ca_bundle, + 'git_ssl_ca_info': git_ssl_ca_info}) + else: + get_logger().info(f"Using SSL certificate bundle for git operations", artifact={"ssl_cert_file": ssl_cert_file}) + chosen_cert_file = ssl_cert_file + else: + get_logger().warning("SSL certificate bundle not found for git operations", artifact={"ssl_cert_file": ssl_cert_file}) + + # Fallback to REQUESTS_CA_BUNDLE + elif requests_ca_bundle: + if os.path.exists(requests_ca_bundle): + if (git_ssl_ca_info and git_ssl_ca_info != requests_ca_bundle): + get_logger().warning(f"Found mismatch between: REQUESTS_CA_BUNDLE, GIT_SSL_CAINFO. " + f"Using the REQUESTS_CA_BUNDLE to resolve ambiguity.", + artifact = {"requests_ca_bundle": requests_ca_bundle, 'git_ssl_ca_info': git_ssl_ca_info}) + else: + get_logger().info("Using SSL certificate bundle from REQUESTS_CA_BUNDLE for git operations", + artifact={"requests_ca_bundle": requests_ca_bundle}) + chosen_cert_file = requests_ca_bundle + else: + get_logger().warning("requests CA bundle not found for git operations", artifact={"requests_ca_bundle": requests_ca_bundle}) + + #Fallback to GIT CA: + elif git_ssl_ca_info: + if os.path.exists(git_ssl_ca_info): + get_logger().info("Using git SSL CA info from GIT_SSL_CAINFO for git operations", + artifact={"git_ssl_ca_info": git_ssl_ca_info}) + chosen_cert_file = git_ssl_ca_info + else: + get_logger().warning("git SSL CA info not found for git operations", artifact={"git_ssl_ca_info": git_ssl_ca_info}) + + else: + get_logger().warning("Neither SSL_CERT_FILE nor REQUESTS_CA_BUNDLE nor GIT_SSL_CAINFO are defined, or they are defined but not found. Returning environment without SSL configuration") + + returned_env = os.environ.copy() + if chosen_cert_file: + returned_env.update({"GIT_SSL_CAINFO": chosen_cert_file, "REQUESTS_CA_BUNDLE": chosen_cert_file}) + return returned_env + + class GitProvider(ABC): @abstractmethod def is_supported(self, capability: str) -> bool: @@ -57,12 +116,21 @@ class GitProvider(ABC): # #Repo.clone_from(repo_url, dest_folder) # , but with throwing an exception upon timeout. # Note: This can only be used in context that supports using pipes. + try: + ssl_env = get_git_ssl_env() + except Exception as e: + get_logger().exception( + "Failed to prepare SSL environment for git operations, falling back to default env", + artifact={"error": e} + ) + ssl_env = os.environ.copy() + subprocess.run([ "git", "clone", "--filter=blob:none", "--depth", "1", repo_url, dest_folder - ], check=True, # check=True will raise an exception if the command fails + ], env=ssl_env, check=True, # check=True will raise an exception if the command fails stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=operation_timeout_in_seconds) CLONE_TIMEOUT_SEC = 20