diff --git a/lib/crewai-tools/src/crewai_tools/tools/invoke_crewai_automation_tool/invoke_crewai_automation_tool.py b/lib/crewai-tools/src/crewai_tools/tools/invoke_crewai_automation_tool/invoke_crewai_automation_tool.py index 2398ddd41..e1907b1ed 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/invoke_crewai_automation_tool/invoke_crewai_automation_tool.py +++ b/lib/crewai-tools/src/crewai_tools/tools/invoke_crewai_automation_tool/invoke_crewai_automation_tool.py @@ -25,7 +25,7 @@ class InvokeCrewAIAutomationTool(BaseTool): Basic usage: >>> tool = InvokeCrewAIAutomationTool( ... crew_api_url="https://api.example.com", - ... crew_bearer_token="your_token", + ... crew_bearer_token=os.environ["CREWAI_BEARER_TOKEN"], ... crew_name="My Crew", ... crew_description="Description of what the crew does", ... ) @@ -39,7 +39,7 @@ class InvokeCrewAIAutomationTool(BaseTool): ... } >>> tool = InvokeCrewAIAutomationTool( ... crew_api_url="https://api.example.com", - ... crew_bearer_token="your_token", + ... crew_bearer_token=os.environ["CREWAI_BEARER_TOKEN"], ... crew_name="My Crew", ... crew_description="Description of what the crew does", ... crew_inputs=custom_inputs, @@ -49,7 +49,7 @@ class InvokeCrewAIAutomationTool(BaseTool): >>> tools = [ ... InvokeCrewAIAutomationTool( ... crew_api_url="https://canary-crew-[...].crewai.com", - ... crew_bearer_token="[Your token: abcdef012345]", + ... crew_bearer_token=os.environ["CREWAI_BEARER_TOKEN"], ... crew_name="State of AI Report", ... crew_description="Retrieves a report on state of AI for a given year.", ... crew_inputs={ diff --git a/lib/crewai-tools/src/crewai_tools/tools/multion_tool/example.py b/lib/crewai-tools/src/crewai_tools/tools/multion_tool/example.py index 00646a0d4..784b3d7a6 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/multion_tool/example.py +++ b/lib/crewai-tools/src/crewai_tools/tools/multion_tool/example.py @@ -4,9 +4,10 @@ from crewai import Agent, Crew, Task from multion_tool import MultiOnTool # type: ignore[import-not-found] -os.environ["OPENAI_API_KEY"] = "Your Key" +if not os.environ.get("OPENAI_API_KEY"): + raise ValueError("Please set the OPENAI_API_KEY environment variable") -multion_browse_tool = MultiOnTool(api_key="Your Key") +multion_browse_tool = MultiOnTool(api_key=os.environ.get("MULTION_API_KEY", "")) # Create a new agent Browser = Agent( diff --git a/lib/crewai/src/crewai/cli/create_flow.py b/lib/crewai/src/crewai/cli/create_flow.py index f349d7452..70d63a3d8 100644 --- a/lib/crewai/src/crewai/cli/create_flow.py +++ b/lib/crewai/src/crewai/cli/create_flow.py @@ -28,9 +28,9 @@ def create_flow(name: str) -> None: (project_root / "src" / folder_name / "tools").mkdir(parents=True) (project_root / "tests").mkdir(exist_ok=True) - # Create .env file + # Create .env file with placeholder with open(project_root / ".env", "w") as file: - file.write("OPENAI_API_KEY=YOUR_API_KEY") + file.write("OPENAI_API_KEY=\n") package_dir = Path(__file__).parent templates_dir = package_dir / "templates" / "flow" diff --git a/lib/crewai/src/crewai/llms/providers/openai_compatible/completion.py b/lib/crewai/src/crewai/llms/providers/openai_compatible/completion.py index 293e73ff0..aa755bb20 100644 --- a/lib/crewai/src/crewai/llms/providers/openai_compatible/completion.py +++ b/lib/crewai/src/crewai/llms/providers/openai_compatible/completion.py @@ -59,21 +59,21 @@ OPENAI_COMPATIBLE_PROVIDERS: dict[str, ProviderConfig] = { api_key_env="OLLAMA_API_KEY", base_url_env="OLLAMA_HOST", api_key_required=False, - default_api_key="ollama", + default_api_key=os.getenv("OLLAMA_DEFAULT_API_KEY", "ollama"), ), "ollama_chat": ProviderConfig( base_url="http://localhost:11434/v1", api_key_env="OLLAMA_API_KEY", base_url_env="OLLAMA_HOST", api_key_required=False, - default_api_key="ollama", + default_api_key=os.getenv("OLLAMA_DEFAULT_API_KEY", "ollama"), ), "hosted_vllm": ProviderConfig( base_url="http://localhost:8000/v1", api_key_env="VLLM_API_KEY", base_url_env="VLLM_BASE_URL", api_key_required=False, - default_api_key="dummy", + default_api_key=os.getenv("VLLM_DEFAULT_API_KEY", "no-key-required"), ), "cerebras": ProviderConfig( base_url="https://api.cerebras.ai/v1", diff --git a/lib/crewai/src/crewai/rag/embeddings/factory.py b/lib/crewai/src/crewai/rag/embeddings/factory.py index 802779320..83d3afac3 100644 --- a/lib/crewai/src/crewai/rag/embeddings/factory.py +++ b/lib/crewai/src/crewai/rag/embeddings/factory.py @@ -363,11 +363,11 @@ def build_embedder(spec): # type: ignore[no-untyped-def] # From dictionary specification embedder = build_embedder({ "provider": "openai", - "config": {"api_key": "sk-..."} + "config": {"api_key": os.environ["OPENAI_API_KEY"]} }) # From provider instance - provider = OpenAIProvider(api_key="sk-...") + provider = OpenAIProvider(api_key=os.environ["OPENAI_API_KEY"]) embedder = build_embedder(provider) """ if isinstance(spec, BaseEmbeddingsProvider): diff --git a/lib/crewai/src/crewai/rag/embeddings/providers/google/genai_vertex_embedding.py b/lib/crewai/src/crewai/rag/embeddings/providers/google/genai_vertex_embedding.py index 4c245280b..1a87f562a 100644 --- a/lib/crewai/src/crewai/rag/embeddings/providers/google/genai_vertex_embedding.py +++ b/lib/crewai/src/crewai/rag/embeddings/providers/google/genai_vertex_embedding.py @@ -45,9 +45,9 @@ class GoogleGenAIVertexEmbeddingFunction(EmbeddingFunction[Documents]): model_name="gemini-embedding-001" ) - # Using API key (new SDK only) + # Using API key from environment variable (new SDK only) embedder = GoogleGenAIVertexEmbeddingFunction( - api_key="your-api-key", + api_key=os.environ["GOOGLE_API_KEY"], model_name="gemini-embedding-001" ) """ diff --git a/lib/crewai/src/crewai/rag/embeddings/providers/google/vertex.py b/lib/crewai/src/crewai/rag/embeddings/providers/google/vertex.py index 8f44d7d59..e869c0f77 100644 --- a/lib/crewai/src/crewai/rag/embeddings/providers/google/vertex.py +++ b/lib/crewai/src/crewai/rag/embeddings/providers/google/vertex.py @@ -49,9 +49,9 @@ class VertexAIProvider(BaseEmbeddingsProvider[GoogleGenAIVertexEmbeddingFunction model_name="gemini-embedding-001" ) - # New model with API key + # New model with API key (from environment variable) provider = VertexAIProvider( - api_key="your-api-key", + api_key=os.environ["GOOGLE_API_KEY"], model_name="gemini-embedding-001" ) """ diff --git a/lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py b/lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py index fd5970299..c3d3f4179 100644 --- a/lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py +++ b/lib/crewai/tests/llms/openai_compatible/test_openai_compatible.py @@ -79,7 +79,7 @@ class TestProviderRegistry: assert config.base_url == "http://localhost:8000/v1" assert config.api_key_env == "VLLM_API_KEY" assert config.api_key_required is False - assert config.default_api_key == "dummy" + assert config.default_api_key == "no-key-required" def test_cerebras_config(self): """Test Cerebras provider configuration.""" diff --git a/lib/crewai/tests/security/test_hardcoded_secrets.py b/lib/crewai/tests/security/test_hardcoded_secrets.py new file mode 100644 index 000000000..c1d080a42 --- /dev/null +++ b/lib/crewai/tests/security/test_hardcoded_secrets.py @@ -0,0 +1,186 @@ +"""Tests to detect and prevent hardcoded secrets in the codebase. + +These tests scan source files for patterns that look like hardcoded secrets +(API keys, tokens, passwords) to prevent accidental credential leaks. +""" + +import os +import re +import tempfile +from pathlib import Path +from unittest.mock import patch + +import pytest + +from crewai.cli.create_flow import create_flow +from crewai.llms.providers.openai_compatible.completion import ( + OPENAI_COMPATIBLE_PROVIDERS, +) + +# Root of the workspace +WORKSPACE_ROOT = Path(__file__).resolve().parents[4] +CREWAI_SRC = WORKSPACE_ROOT / "lib" / "crewai" / "src" +CREWAI_TOOLS_SRC = WORKSPACE_ROOT / "lib" / "crewai-tools" / "src" + +# Patterns that indicate hardcoded secrets in source code (not docs/tests) +SECRET_PATTERNS = [ + # Actual API key formats + re.compile(r'''["']sk-proj-[a-zA-Z0-9_-]{20,}["']'''), + re.compile(r'''["']sk-ant-api[a-zA-Z0-9_-]{20,}["']'''), + re.compile(r'''["']ghp_[a-zA-Z0-9]{36}["']'''), + re.compile(r'''["']gho_[a-zA-Z0-9]{36}["']'''), + re.compile(r'''["']xox[bpas]-[a-zA-Z0-9-]{10,}["']'''), + re.compile(r'''["']AKIA[A-Z0-9]{16}["']'''), + # os.environ assignment with hardcoded non-empty value + re.compile(r'''os\.environ\[["'][A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD)["']\]\s*=\s*["'][^"']+["']'''), +] + +# Files/directories to skip (tests, docs, examples patterns in docstrings are OK) +SKIP_DIRS = { + "tests", + "test", + "__pycache__", + ".git", + "cassettes", + "node_modules", + ".venv", +} + + +def _get_python_source_files(root: Path) -> list[Path]: + """Get all Python source files, excluding test directories.""" + files = [] + for path in root.rglob("*.py"): + parts = set(path.parts) + if parts & SKIP_DIRS: + continue + files.append(path) + return files + + +class TestNoHardcodedSecrets: + """Test that source code does not contain hardcoded secrets.""" + + def test_no_real_api_keys_in_source(self): + """Verify no real API key patterns exist in source code.""" + violations = [] + + for src_root in [CREWAI_SRC, CREWAI_TOOLS_SRC]: + if not src_root.exists(): + continue + for filepath in _get_python_source_files(src_root): + content = filepath.read_text(errors="ignore") + for pattern in SECRET_PATTERNS: + for match in pattern.finditer(content): + # Get the line number + line_num = content[: match.start()].count("\n") + 1 + violations.append( + f"{filepath.relative_to(WORKSPACE_ROOT)}:{line_num}: {match.group()}" + ) + + assert not violations, ( + f"Found {len(violations)} potential hardcoded secret(s):\n" + + "\n".join(violations) + ) + + def test_no_env_assignment_with_hardcoded_keys(self): + """Verify no os.environ['KEY'] = 'hardcoded-value' patterns in source (non-test) code.""" + pattern = re.compile( + r'''os\.environ\[["'](\w*(?:KEY|TOKEN|SECRET|PASSWORD)\w*)["']\]\s*=\s*["']([^"']+)["']''' + ) + # Config flags that are not secrets + ALLOWED_ENV_ASSIGNMENTS = { + "TOKENIZERS_PARALLELISM", + } + + violations = [] + for src_root in [CREWAI_SRC, CREWAI_TOOLS_SRC]: + if not src_root.exists(): + continue + for filepath in _get_python_source_files(src_root): + content = filepath.read_text(errors="ignore") + for match in pattern.finditer(content): + env_var_name = match.group(1) + if env_var_name in ALLOWED_ENV_ASSIGNMENTS: + continue + line_num = content[: match.start()].count("\n") + 1 + violations.append( + f"{filepath.relative_to(WORKSPACE_ROOT)}:{line_num}: " + f"os.environ['{match.group(1)}'] = '{match.group(2)}'" + ) + + assert not violations, ( + f"Found {len(violations)} hardcoded environment variable assignment(s):\n" + + "\n".join(violations) + + "\n\nUse os.environ.get() or read from .env files instead." + ) + + +class TestCreateFlowEnvFile: + """Test that create_flow generates .env files without hardcoded secret values.""" + + def test_create_flow_env_file_has_no_hardcoded_api_key(self): + """Verify create_flow does not write a hardcoded API key value.""" + with tempfile.TemporaryDirectory() as temp_dir: + original_cwd = os.getcwd() + try: + os.chdir(temp_dir) + create_flow("test_flow") + + env_file = Path(temp_dir) / "test_flow" / ".env" + assert env_file.exists(), ".env file should be created" + + content = env_file.read_text() + assert "YOUR_API_KEY" not in content, ( + ".env should not contain hardcoded placeholder 'YOUR_API_KEY'" + ) + # The key name should be present but without a hardcoded value + assert "OPENAI_API_KEY" in content, ( + ".env should contain the OPENAI_API_KEY variable name" + ) + finally: + os.chdir(original_cwd) + + +class TestProviderDefaultApiKeys: + """Test that provider default API keys use environment variable lookups.""" + + def test_ollama_default_api_key_from_env(self): + """Verify Ollama default API key can be overridden via environment variable.""" + with patch.dict(os.environ, {"OLLAMA_DEFAULT_API_KEY": "custom-ollama-key"}, clear=False): + # Re-import to pick up new env var - but since module-level dict is already + # evaluated, we test the env var pattern is used in the config + config = OPENAI_COMPATIBLE_PROVIDERS["ollama"] + # The default_api_key should be set (either from env or fallback) + assert config.default_api_key is not None + + def test_vllm_default_api_key_not_dummy(self): + """Verify hosted_vllm default API key is not the literal string 'dummy'.""" + config = OPENAI_COMPATIBLE_PROVIDERS["hosted_vllm"] + assert config.default_api_key != "dummy", ( + "hosted_vllm should not use 'dummy' as a hardcoded default API key" + ) + assert config.default_api_key is not None + + def test_ollama_default_api_key_fallback(self): + """Verify Ollama uses 'ollama' as fallback when env var is not set.""" + # When OLLAMA_DEFAULT_API_KEY is not set, should fall back to "ollama" + env = os.environ.copy() + env.pop("OLLAMA_DEFAULT_API_KEY", None) + with patch.dict(os.environ, env, clear=True): + # The config was already created at module load time, so we check + # the current value + config = OPENAI_COMPATIBLE_PROVIDERS["ollama"] + assert config.default_api_key is not None + + def test_all_providers_have_valid_config(self): + """Verify all providers have properly configured API key settings.""" + for provider_name, config in OPENAI_COMPATIBLE_PROVIDERS.items(): + assert config.api_key_env, ( + f"Provider '{provider_name}' must have api_key_env configured" + ) + if not config.api_key_required: + assert config.default_api_key is not None, ( + f"Provider '{provider_name}' with api_key_required=False " + "must have a default_api_key" + )