mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-05 17:22:36 +00:00
refactor(core): lift Settings, TokenManager, and tool-credentials into crewai-core
- New crewai_core.{settings,token_manager,tool_credentials} absorb the previously
duplicated Settings class (~200 LOC), TokenManager (~185 LOC), and
build_env_with_*_credentials helpers from both crewai and crewai-cli.
- crewai_core.constants gains the OAuth2/enterprise URL constants.
- crewai.settings, crewai.auth.token_manager, crewai_cli.config, and
crewai_cli.shared.token_manager are now thin re-export shims (deprecation
warnings on the crewai.* paths; crewai_cli.* paths kept silent re-exports).
- Internal callers (plus_api, auth.token, auth.oauth2, agent_utils,
trace_batch_manager) migrated to crewai_core.* imports.
- Tests updated to patch crewai_core.{settings,token_manager}.* paths.
- crewai-core gains pydantic, cryptography, tomli deps; crewai-cli's redundant
cryptography dep can stay (still imported by crewai_cli.shared.token_manager
shim users) — no behavior change.
- Standalone CLI smoke still passes; crewai's full mypy (471 files) clean.
This commit is contained in:
@@ -1,221 +1,18 @@
|
||||
import json
|
||||
from logging import getLogger
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from typing import Any
|
||||
"""Re-exports of shared settings from ``crewai_core.settings``.
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
Kept as a stable import path for the CLI; new code should import from
|
||||
``crewai_core.settings`` directly.
|
||||
"""
|
||||
|
||||
from crewai_cli.constants import (
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_AUDIENCE,
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_CLIENT_ID,
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN,
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_PROVIDER,
|
||||
DEFAULT_CREWAI_ENTERPRISE_URL,
|
||||
from __future__ import annotations
|
||||
|
||||
from crewai_core.settings import (
|
||||
CLI_SETTINGS_KEYS as CLI_SETTINGS_KEYS,
|
||||
DEFAULT_CLI_SETTINGS as DEFAULT_CLI_SETTINGS,
|
||||
DEFAULT_CONFIG_PATH as DEFAULT_CONFIG_PATH,
|
||||
HIDDEN_SETTINGS_KEYS as HIDDEN_SETTINGS_KEYS,
|
||||
READONLY_SETTINGS_KEYS as READONLY_SETTINGS_KEYS,
|
||||
USER_SETTINGS_KEYS as USER_SETTINGS_KEYS,
|
||||
Settings as Settings,
|
||||
get_writable_config_path as get_writable_config_path,
|
||||
)
|
||||
from crewai_cli.shared.token_manager import TokenManager
|
||||
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
DEFAULT_CONFIG_PATH = Path.home() / ".config" / "crewai" / "settings.json"
|
||||
|
||||
|
||||
def get_writable_config_path() -> Path | None:
|
||||
"""
|
||||
Find a writable location for the config file with fallback options.
|
||||
|
||||
Tries in order:
|
||||
1. Default: ~/.config/crewai/settings.json
|
||||
2. Temp directory: /tmp/crewai_settings.json (or OS equivalent)
|
||||
3. Current directory: ./crewai_settings.json
|
||||
4. In-memory only (returns None)
|
||||
|
||||
Returns:
|
||||
Path object for writable config location, or None if no writable location found
|
||||
"""
|
||||
fallback_paths = [
|
||||
DEFAULT_CONFIG_PATH, # Default location
|
||||
Path(tempfile.gettempdir()) / "crewai_settings.json", # Temporary directory
|
||||
Path.cwd() / "crewai_settings.json", # Current working directory
|
||||
]
|
||||
|
||||
for config_path in fallback_paths:
|
||||
try:
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
test_file = config_path.parent / ".crewai_write_test"
|
||||
try:
|
||||
test_file.write_text("test")
|
||||
test_file.unlink() # Clean up test file
|
||||
logger.info(f"Using config path: {config_path}")
|
||||
return config_path
|
||||
except Exception: # noqa: S112
|
||||
continue
|
||||
|
||||
except Exception: # noqa: S112
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Settings that are related to the user's account
|
||||
USER_SETTINGS_KEYS = [
|
||||
"tool_repository_username",
|
||||
"tool_repository_password",
|
||||
"org_name",
|
||||
"org_uuid",
|
||||
]
|
||||
|
||||
# Settings that are related to the CLI
|
||||
CLI_SETTINGS_KEYS = [
|
||||
"enterprise_base_url",
|
||||
"oauth2_provider",
|
||||
"oauth2_audience",
|
||||
"oauth2_client_id",
|
||||
"oauth2_domain",
|
||||
"oauth2_extra",
|
||||
]
|
||||
|
||||
# Default values for CLI settings
|
||||
DEFAULT_CLI_SETTINGS: dict[str, Any] = {
|
||||
"enterprise_base_url": DEFAULT_CREWAI_ENTERPRISE_URL,
|
||||
"oauth2_provider": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_PROVIDER,
|
||||
"oauth2_audience": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_AUDIENCE,
|
||||
"oauth2_client_id": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_CLIENT_ID,
|
||||
"oauth2_domain": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN,
|
||||
"oauth2_extra": {},
|
||||
}
|
||||
|
||||
# Readonly settings - cannot be set by the user
|
||||
READONLY_SETTINGS_KEYS = [
|
||||
"org_name",
|
||||
"org_uuid",
|
||||
]
|
||||
|
||||
# Hidden settings - not displayed by the 'list' command and cannot be set by the user
|
||||
HIDDEN_SETTINGS_KEYS = [
|
||||
"config_path",
|
||||
"tool_repository_username",
|
||||
"tool_repository_password",
|
||||
]
|
||||
|
||||
|
||||
class Settings(BaseModel):
|
||||
enterprise_base_url: str | None = Field(
|
||||
default=DEFAULT_CLI_SETTINGS["enterprise_base_url"],
|
||||
description="Base URL of the CrewAI AMP instance",
|
||||
)
|
||||
tool_repository_username: str | None = Field(
|
||||
None, description="Username for interacting with the Tool Repository"
|
||||
)
|
||||
tool_repository_password: str | None = Field(
|
||||
None, description="Password for interacting with the Tool Repository"
|
||||
)
|
||||
org_name: str | None = Field(
|
||||
None, description="Name of the currently active organization"
|
||||
)
|
||||
org_uuid: str | None = Field(
|
||||
None, description="UUID of the currently active organization"
|
||||
)
|
||||
config_path: Path = Field(default=DEFAULT_CONFIG_PATH, frozen=True, exclude=True)
|
||||
|
||||
oauth2_provider: str = Field(
|
||||
description="OAuth2 provider used for authentication (e.g., workos, okta, auth0).",
|
||||
default=DEFAULT_CLI_SETTINGS["oauth2_provider"],
|
||||
)
|
||||
|
||||
oauth2_audience: str | None = Field(
|
||||
description="OAuth2 audience value, typically used to identify the target API or resource.",
|
||||
default=DEFAULT_CLI_SETTINGS["oauth2_audience"],
|
||||
)
|
||||
|
||||
oauth2_client_id: str = Field(
|
||||
default=DEFAULT_CLI_SETTINGS["oauth2_client_id"],
|
||||
description="OAuth2 client ID issued by the provider, used during authentication requests.",
|
||||
)
|
||||
|
||||
oauth2_domain: str = Field(
|
||||
description="OAuth2 provider's domain (e.g., your-org.auth0.com) used for issuing tokens.",
|
||||
default=DEFAULT_CLI_SETTINGS["oauth2_domain"],
|
||||
)
|
||||
|
||||
oauth2_extra: dict[str, Any] = Field(
|
||||
description="Extra configuration for the OAuth2 provider.",
|
||||
default={},
|
||||
)
|
||||
|
||||
def __init__(self, config_path: Path | None = None, **data: dict[str, Any]) -> None:
|
||||
"""Load Settings from config path with fallback support"""
|
||||
if config_path is None:
|
||||
config_path = get_writable_config_path()
|
||||
|
||||
# If config_path is None, we're in memory-only mode
|
||||
if config_path is None:
|
||||
merged_data = {**data}
|
||||
# Dummy path for memory-only mode
|
||||
super().__init__(config_path=Path("/dev/null"), **merged_data)
|
||||
return
|
||||
|
||||
try:
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
except Exception:
|
||||
merged_data = {**data}
|
||||
# Dummy path for memory-only mode
|
||||
super().__init__(config_path=Path("/dev/null"), **merged_data)
|
||||
return
|
||||
|
||||
file_data = {}
|
||||
if config_path.is_file():
|
||||
try:
|
||||
with config_path.open("r") as f:
|
||||
file_data = json.load(f)
|
||||
except Exception:
|
||||
file_data = {}
|
||||
|
||||
merged_data = {**file_data, **data}
|
||||
super().__init__(config_path=config_path, **merged_data)
|
||||
|
||||
def clear_user_settings(self) -> None:
|
||||
"""Clear all user settings"""
|
||||
self._reset_user_settings()
|
||||
self.dump()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset all settings to default values"""
|
||||
self._reset_user_settings()
|
||||
self._reset_cli_settings()
|
||||
self._clear_auth_tokens()
|
||||
self.dump()
|
||||
|
||||
def dump(self) -> None:
|
||||
"""Save current settings to settings.json"""
|
||||
if str(self.config_path) == "/dev/null":
|
||||
return
|
||||
|
||||
try:
|
||||
if self.config_path.is_file():
|
||||
with self.config_path.open("r") as f:
|
||||
existing_data = json.load(f)
|
||||
else:
|
||||
existing_data = {}
|
||||
|
||||
updated_data = {**existing_data, **self.model_dump(exclude_unset=True)}
|
||||
with self.config_path.open("w") as f:
|
||||
json.dump(updated_data, f, indent=4)
|
||||
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
def _reset_user_settings(self) -> None:
|
||||
"""Reset all user settings to default values"""
|
||||
for key in USER_SETTINGS_KEYS:
|
||||
setattr(self, key, None)
|
||||
|
||||
def _reset_cli_settings(self) -> None:
|
||||
"""Reset all CLI settings to default values"""
|
||||
for key in CLI_SETTINGS_KEYS:
|
||||
setattr(self, key, DEFAULT_CLI_SETTINGS.get(key))
|
||||
|
||||
def _clear_auth_tokens(self) -> None:
|
||||
"""Clear all authentication tokens"""
|
||||
TokenManager().clear_tokens()
|
||||
|
||||
@@ -1,186 +1,15 @@
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Final, Literal, cast
|
||||
"""Deprecated: use ``crewai_core.token_manager`` instead."""
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
|
||||
from crewai_core.token_manager import TokenManager as TokenManager
|
||||
|
||||
|
||||
_FERNET_KEY_LENGTH: Final[Literal[44]] = 44
|
||||
|
||||
|
||||
class TokenManager:
|
||||
"""Manages encrypted token storage."""
|
||||
|
||||
def __init__(self, file_path: str = "tokens.enc") -> None:
|
||||
"""Initialize the TokenManager.
|
||||
|
||||
Args:
|
||||
file_path: The file path to store encrypted tokens.
|
||||
"""
|
||||
self.file_path = file_path
|
||||
self.key = self._get_or_create_key()
|
||||
self.fernet = Fernet(self.key)
|
||||
|
||||
def _get_or_create_key(self) -> bytes:
|
||||
"""Get or create the encryption key.
|
||||
|
||||
Returns:
|
||||
The encryption key as bytes.
|
||||
"""
|
||||
key_filename: str = "secret.key"
|
||||
|
||||
key = self._read_secure_file(key_filename)
|
||||
if key is not None and len(key) == _FERNET_KEY_LENGTH:
|
||||
return key
|
||||
|
||||
new_key = Fernet.generate_key()
|
||||
if self._atomic_create_secure_file(key_filename, new_key):
|
||||
return new_key
|
||||
|
||||
key = self._read_secure_file(key_filename)
|
||||
if key is not None and len(key) == _FERNET_KEY_LENGTH:
|
||||
return key
|
||||
|
||||
raise RuntimeError("Failed to create or read encryption key")
|
||||
|
||||
def save_tokens(self, access_token: str, expires_at: int) -> None:
|
||||
"""Save the access token and its expiration time.
|
||||
|
||||
Args:
|
||||
access_token: The access token to save.
|
||||
expires_at: The UNIX timestamp of the expiration time.
|
||||
"""
|
||||
expiration_time = datetime.fromtimestamp(expires_at)
|
||||
data = {
|
||||
"access_token": access_token,
|
||||
"expiration": expiration_time.isoformat(),
|
||||
}
|
||||
encrypted_data = self.fernet.encrypt(json.dumps(data).encode())
|
||||
self._atomic_write_secure_file(self.file_path, encrypted_data)
|
||||
|
||||
def get_token(self) -> str | None:
|
||||
"""Get the access token if it is valid and not expired.
|
||||
|
||||
Returns:
|
||||
The access token if valid and not expired, otherwise None.
|
||||
"""
|
||||
encrypted_data = self._read_secure_file(self.file_path)
|
||||
if encrypted_data is None:
|
||||
return None
|
||||
|
||||
decrypted_data = self.fernet.decrypt(encrypted_data)
|
||||
data = json.loads(decrypted_data)
|
||||
|
||||
expiration = datetime.fromisoformat(data["expiration"])
|
||||
if expiration <= datetime.now():
|
||||
return None
|
||||
|
||||
return cast(str | None, data.get("access_token"))
|
||||
|
||||
def clear_tokens(self) -> None:
|
||||
"""Clear the stored tokens."""
|
||||
self._delete_secure_file(self.file_path)
|
||||
|
||||
@staticmethod
|
||||
def _get_secure_storage_path() -> Path:
|
||||
"""Get the secure storage path based on the operating system.
|
||||
|
||||
Returns:
|
||||
The secure storage path.
|
||||
"""
|
||||
if sys.platform == "win32":
|
||||
base_path = os.environ.get("LOCALAPPDATA")
|
||||
elif sys.platform == "darwin":
|
||||
base_path = os.path.expanduser("~/Library/Application Support")
|
||||
else:
|
||||
base_path = os.path.expanduser("~/.local/share")
|
||||
|
||||
app_name = "crewai/credentials"
|
||||
storage_path = Path(base_path) / app_name
|
||||
|
||||
storage_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
return storage_path
|
||||
|
||||
def _atomic_create_secure_file(self, filename: str, content: bytes) -> bool:
|
||||
"""Create a file only if it doesn't exist.
|
||||
|
||||
Args:
|
||||
filename: The name of the file.
|
||||
content: The content to write.
|
||||
|
||||
Returns:
|
||||
True if file was created, False if it already exists.
|
||||
"""
|
||||
storage_path = self._get_secure_storage_path()
|
||||
file_path = storage_path / filename
|
||||
|
||||
try:
|
||||
fd = os.open(file_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
||||
try:
|
||||
os.write(fd, content)
|
||||
finally:
|
||||
os.close(fd)
|
||||
return True
|
||||
except FileExistsError:
|
||||
return False
|
||||
|
||||
def _atomic_write_secure_file(self, filename: str, content: bytes) -> None:
|
||||
"""Write content to a secure file.
|
||||
|
||||
Args:
|
||||
filename: The name of the file.
|
||||
content: The content to write.
|
||||
"""
|
||||
storage_path = self._get_secure_storage_path()
|
||||
file_path = storage_path / filename
|
||||
|
||||
fd, temp_path = tempfile.mkstemp(dir=storage_path, prefix=f".{filename}.")
|
||||
fd_closed = False
|
||||
try:
|
||||
os.write(fd, content)
|
||||
os.close(fd)
|
||||
fd_closed = True
|
||||
os.chmod(temp_path, 0o600)
|
||||
os.replace(temp_path, file_path)
|
||||
except Exception:
|
||||
if not fd_closed:
|
||||
os.close(fd)
|
||||
if os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
raise
|
||||
|
||||
def _read_secure_file(self, filename: str) -> bytes | None:
|
||||
"""Read the content of a secure file.
|
||||
|
||||
Args:
|
||||
filename: The name of the file.
|
||||
|
||||
Returns:
|
||||
The content of the file if it exists, otherwise None.
|
||||
"""
|
||||
storage_path = self._get_secure_storage_path()
|
||||
file_path = storage_path / filename
|
||||
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
def _delete_secure_file(self, filename: str) -> None:
|
||||
"""Delete a secure file.
|
||||
|
||||
Args:
|
||||
filename: The name of the file.
|
||||
"""
|
||||
storage_path = self._get_secure_storage_path()
|
||||
file_path = storage_path / filename
|
||||
try:
|
||||
file_path.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
warnings.warn(
|
||||
"crewai_cli.shared.token_manager is deprecated; "
|
||||
"import from crewai_core.token_manager.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
@@ -15,9 +15,12 @@ from crewai_core.project import (
|
||||
parse_toml as parse_toml,
|
||||
read_toml as read_toml,
|
||||
)
|
||||
from crewai_core.tool_credentials import (
|
||||
build_env_with_all_tool_credentials as build_env_with_all_tool_credentials,
|
||||
build_env_with_tool_repository_credentials as build_env_with_tool_repository_credentials,
|
||||
)
|
||||
from rich.console import Console
|
||||
|
||||
from crewai_cli.config import Settings
|
||||
from crewai_cli.constants import ENV_VARS
|
||||
|
||||
|
||||
@@ -154,46 +157,3 @@ def write_env_file(folder_path: Path, env_vars: dict[str, Any]) -> None:
|
||||
with open(env_file_path, "w") as file:
|
||||
for key, value in env_vars.items():
|
||||
file.write(f"{key.upper()}={value}\n")
|
||||
|
||||
|
||||
def build_env_with_tool_repository_credentials(
|
||||
repository_handle: str,
|
||||
) -> dict[str, Any]:
|
||||
repository_handle = repository_handle.upper().replace("-", "_")
|
||||
settings = Settings()
|
||||
|
||||
env = os.environ.copy()
|
||||
env[f"UV_INDEX_{repository_handle}_USERNAME"] = str(
|
||||
settings.tool_repository_username or ""
|
||||
)
|
||||
env[f"UV_INDEX_{repository_handle}_PASSWORD"] = str(
|
||||
settings.tool_repository_password or ""
|
||||
)
|
||||
|
||||
return env
|
||||
|
||||
|
||||
def build_env_with_all_tool_credentials() -> dict[str, Any]:
|
||||
"""Build environment dict with credentials for all tool repository indexes.
|
||||
|
||||
Reads ``[tool.uv.sources]`` from ``pyproject.toml`` and merges credentials
|
||||
for each private index into a copy of the current environment.
|
||||
|
||||
Returns:
|
||||
Environment variables with credentials for all private indexes.
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
try:
|
||||
pyproject_data = read_toml()
|
||||
sources = pyproject_data.get("tool", {}).get("uv", {}).get("sources", {})
|
||||
|
||||
for source_config in sources.values():
|
||||
if isinstance(source_config, dict):
|
||||
index = source_config.get("index")
|
||||
if index:
|
||||
index_env = build_env_with_tool_repository_credentials(index)
|
||||
env.update(index_env)
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
return env
|
||||
|
||||
@@ -12,7 +12,7 @@ from crewai_cli.config import (
|
||||
USER_SETTINGS_KEYS,
|
||||
Settings,
|
||||
)
|
||||
from crewai_cli.shared.token_manager import TokenManager
|
||||
from crewai_core.token_manager import TokenManager
|
||||
|
||||
|
||||
class TestSettings(unittest.TestCase):
|
||||
@@ -69,7 +69,7 @@ class TestSettings(unittest.TestCase):
|
||||
for key in user_settings.keys():
|
||||
self.assertEqual(getattr(settings, key), None)
|
||||
|
||||
@patch("crewai_cli.config.TokenManager")
|
||||
@patch("crewai_core.settings.TokenManager")
|
||||
def test_reset_settings(self, mock_token_manager):
|
||||
user_settings = {key: f"value_for_{key}" for key in USER_SETTINGS_KEYS}
|
||||
cli_settings = {key: f"value_for_{key}" for key in CLI_SETTINGS_KEYS if key != "oauth2_extra"}
|
||||
|
||||
@@ -9,20 +9,20 @@ from unittest.mock import patch
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
from crewai_cli.shared.token_manager import TokenManager
|
||||
from crewai_core.token_manager import TokenManager
|
||||
|
||||
|
||||
class TestTokenManager(unittest.TestCase):
|
||||
"""Test cases for TokenManager."""
|
||||
|
||||
@patch("crewai_cli.shared.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def setUp(self, mock_get_key: unittest.mock.MagicMock) -> None:
|
||||
"""Set up test fixtures."""
|
||||
mock_get_key.return_value = Fernet.generate_key()
|
||||
self.token_manager = TokenManager()
|
||||
|
||||
@patch("crewai_cli.shared.token_manager.TokenManager._read_secure_file")
|
||||
@patch("crewai_cli.shared.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._read_secure_file")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_get_or_create_key_existing(
|
||||
self,
|
||||
mock_get_or_create: unittest.mock.MagicMock,
|
||||
@@ -44,7 +44,7 @@ class TestTokenManager(unittest.TestCase):
|
||||
with (
|
||||
patch.object(self.token_manager, "_read_secure_file", return_value=None) as mock_read,
|
||||
patch.object(self.token_manager, "_atomic_create_secure_file", return_value=True) as mock_atomic_create,
|
||||
patch("crewai_cli.shared.token_manager.Fernet.generate_key", return_value=mock_key) as mock_generate,
|
||||
patch("crewai_core.token_manager.Fernet.generate_key", return_value=mock_key) as mock_generate,
|
||||
):
|
||||
result = self.token_manager._get_or_create_key()
|
||||
|
||||
@@ -61,14 +61,14 @@ class TestTokenManager(unittest.TestCase):
|
||||
with (
|
||||
patch.object(self.token_manager, "_read_secure_file", side_effect=[None, their_key]) as mock_read,
|
||||
patch.object(self.token_manager, "_atomic_create_secure_file", return_value=False) as mock_atomic_create,
|
||||
patch("crewai_cli.shared.token_manager.Fernet.generate_key", return_value=our_key),
|
||||
patch("crewai_core.token_manager.Fernet.generate_key", return_value=our_key),
|
||||
):
|
||||
result = self.token_manager._get_or_create_key()
|
||||
|
||||
self.assertEqual(result, their_key)
|
||||
self.assertEqual(mock_read.call_count, 2)
|
||||
|
||||
@patch("crewai_cli.shared.token_manager.TokenManager._atomic_write_secure_file")
|
||||
@patch("crewai_core.token_manager.TokenManager._atomic_write_secure_file")
|
||||
def test_save_tokens(
|
||||
self, mock_write: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -87,7 +87,7 @@ class TestTokenManager(unittest.TestCase):
|
||||
expiration = datetime.fromisoformat(data["expiration"])
|
||||
self.assertEqual(expiration, datetime.fromtimestamp(expires_at))
|
||||
|
||||
@patch("crewai_cli.shared.token_manager.TokenManager._read_secure_file")
|
||||
@patch("crewai_core.token_manager.TokenManager._read_secure_file")
|
||||
def test_get_token_valid(
|
||||
self, mock_read: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -102,7 +102,7 @@ class TestTokenManager(unittest.TestCase):
|
||||
|
||||
self.assertEqual(result, access_token)
|
||||
|
||||
@patch("crewai_cli.shared.token_manager.TokenManager._read_secure_file")
|
||||
@patch("crewai_core.token_manager.TokenManager._read_secure_file")
|
||||
def test_get_token_expired(
|
||||
self, mock_read: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -117,7 +117,7 @@ class TestTokenManager(unittest.TestCase):
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
@patch("crewai_cli.shared.token_manager.TokenManager._read_secure_file")
|
||||
@patch("crewai_core.token_manager.TokenManager._read_secure_file")
|
||||
def test_get_token_not_found(
|
||||
self, mock_read: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -128,7 +128,7 @@ class TestTokenManager(unittest.TestCase):
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
@patch("crewai_cli.shared.token_manager.TokenManager._delete_secure_file")
|
||||
@patch("crewai_core.token_manager.TokenManager._delete_secure_file")
|
||||
def test_clear_tokens(
|
||||
self, mock_delete: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -158,7 +158,7 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
import shutil
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
@patch("crewai_cli.shared.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_atomic_create_new_file(
|
||||
self, mock_get_key: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -174,7 +174,7 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
self.assertEqual(file_path.read_bytes(), b"content")
|
||||
self.assertEqual(file_path.stat().st_mode & 0o777, 0o600)
|
||||
|
||||
@patch("crewai_cli.shared.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_atomic_create_existing_file(
|
||||
self, mock_get_key: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -191,7 +191,7 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
self.assertFalse(result)
|
||||
self.assertEqual(file_path.read_bytes(), b"original")
|
||||
|
||||
@patch("crewai_cli.shared.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_atomic_write_new_file(
|
||||
self, mock_get_key: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -206,7 +206,7 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
self.assertEqual(file_path.read_bytes(), b"content")
|
||||
self.assertEqual(file_path.stat().st_mode & 0o777, 0o600)
|
||||
|
||||
@patch("crewai_cli.shared.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_atomic_write_overwrites(
|
||||
self, mock_get_key: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -221,7 +221,7 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
|
||||
self.assertEqual(file_path.read_bytes(), b"new content")
|
||||
|
||||
@patch("crewai_cli.shared.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_atomic_write_no_temp_file_on_success(
|
||||
self, mock_get_key: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -235,7 +235,7 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
temp_files = list(Path(self.temp_dir).glob(".test.txt.*"))
|
||||
self.assertEqual(len(temp_files), 0)
|
||||
|
||||
@patch("crewai_cli.shared.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_read_secure_file_exists(
|
||||
self, mock_get_key: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -250,7 +250,7 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
|
||||
self.assertEqual(result, b"content")
|
||||
|
||||
@patch("crewai_cli.shared.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_read_secure_file_not_exists(
|
||||
self, mock_get_key: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -262,7 +262,7 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
@patch("crewai_cli.shared.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_delete_secure_file_exists(
|
||||
self, mock_get_key: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -277,7 +277,7 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
|
||||
self.assertFalse(file_path.exists())
|
||||
|
||||
@patch("crewai_cli.shared.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_delete_secure_file_not_exists(
|
||||
self, mock_get_key: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
|
||||
@@ -9,11 +9,14 @@ authors = [
|
||||
requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"appdirs~=1.4.4",
|
||||
"cryptography>=42.0",
|
||||
"portalocker~=2.7.0",
|
||||
"pydantic>=2.11.9,<2.13",
|
||||
"rich>=13.7.1",
|
||||
"opentelemetry-api~=1.34.0",
|
||||
"opentelemetry-sdk~=1.34.0",
|
||||
"opentelemetry-exporter-otlp-proto-http~=1.34.0",
|
||||
"tomli~=2.0.2",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -10,3 +10,13 @@ TRAINING_DATA_FILE: Final[str] = "training_data.pkl"
|
||||
TRAINED_AGENTS_DATA_FILE: Final[str] = "trained_agents_data.pkl"
|
||||
KNOWLEDGE_DIRECTORY: Final[str] = "knowledge"
|
||||
MAX_FILE_NAME_LENGTH: Final[int] = 255
|
||||
|
||||
DEFAULT_CREWAI_ENTERPRISE_URL: Final[str] = "https://app.crewai.com"
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_PROVIDER: Final[str] = "workos"
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_AUDIENCE: Final[str] = (
|
||||
"client_01JNJQWBJ4SPFN3SWJM5T7BDG8"
|
||||
)
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_CLIENT_ID: Final[str] = (
|
||||
"client_01JYT06R59SP0NXYGD994NFXXX"
|
||||
)
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN: Final[str] = "login.crewai.com"
|
||||
|
||||
215
lib/crewai-core/src/crewai_core/settings.py
Normal file
215
lib/crewai-core/src/crewai_core/settings.py
Normal file
@@ -0,0 +1,215 @@
|
||||
"""CrewAI platform settings — shared by crewai and crewai-cli."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from logging import getLogger
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from crewai_core.constants import (
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_AUDIENCE,
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_CLIENT_ID,
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN,
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_PROVIDER,
|
||||
DEFAULT_CREWAI_ENTERPRISE_URL,
|
||||
)
|
||||
from crewai_core.token_manager import TokenManager
|
||||
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
DEFAULT_CONFIG_PATH = Path.home() / ".config" / "crewai" / "settings.json"
|
||||
|
||||
|
||||
def get_writable_config_path() -> Path | None:
|
||||
"""Find a writable location for the config file with fallback options.
|
||||
|
||||
Tries in order:
|
||||
1. Default: ``~/.config/crewai/settings.json``
|
||||
2. Temp directory: ``/tmp/crewai_settings.json`` (or OS equivalent)
|
||||
3. Current directory: ``./crewai_settings.json``
|
||||
4. In-memory only (returns ``None``)
|
||||
"""
|
||||
fallback_paths = [
|
||||
DEFAULT_CONFIG_PATH,
|
||||
Path(tempfile.gettempdir()) / "crewai_settings.json",
|
||||
Path.cwd() / "crewai_settings.json",
|
||||
]
|
||||
|
||||
for config_path in fallback_paths:
|
||||
try:
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
test_file = config_path.parent / ".crewai_write_test"
|
||||
try:
|
||||
test_file.write_text("test")
|
||||
test_file.unlink()
|
||||
logger.info(f"Using config path: {config_path}")
|
||||
return config_path
|
||||
except Exception: # noqa: S112
|
||||
continue
|
||||
|
||||
except Exception: # noqa: S112
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
|
||||
USER_SETTINGS_KEYS = [
|
||||
"tool_repository_username",
|
||||
"tool_repository_password",
|
||||
"org_name",
|
||||
"org_uuid",
|
||||
]
|
||||
|
||||
CLI_SETTINGS_KEYS = [
|
||||
"enterprise_base_url",
|
||||
"oauth2_provider",
|
||||
"oauth2_audience",
|
||||
"oauth2_client_id",
|
||||
"oauth2_domain",
|
||||
"oauth2_extra",
|
||||
]
|
||||
|
||||
DEFAULT_CLI_SETTINGS: dict[str, Any] = {
|
||||
"enterprise_base_url": DEFAULT_CREWAI_ENTERPRISE_URL,
|
||||
"oauth2_provider": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_PROVIDER,
|
||||
"oauth2_audience": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_AUDIENCE,
|
||||
"oauth2_client_id": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_CLIENT_ID,
|
||||
"oauth2_domain": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN,
|
||||
"oauth2_extra": {},
|
||||
}
|
||||
|
||||
READONLY_SETTINGS_KEYS = [
|
||||
"org_name",
|
||||
"org_uuid",
|
||||
]
|
||||
|
||||
HIDDEN_SETTINGS_KEYS = [
|
||||
"config_path",
|
||||
"tool_repository_username",
|
||||
"tool_repository_password",
|
||||
]
|
||||
|
||||
|
||||
class Settings(BaseModel):
|
||||
"""CrewAI platform settings persisted to ``~/.config/crewai/settings.json``."""
|
||||
|
||||
enterprise_base_url: str | None = Field(
|
||||
default=DEFAULT_CLI_SETTINGS["enterprise_base_url"],
|
||||
description="Base URL of the CrewAI AMP instance",
|
||||
)
|
||||
tool_repository_username: str | None = Field(
|
||||
None, description="Username for interacting with the Tool Repository"
|
||||
)
|
||||
tool_repository_password: str | None = Field(
|
||||
None, description="Password for interacting with the Tool Repository"
|
||||
)
|
||||
org_name: str | None = Field(
|
||||
None, description="Name of the currently active organization"
|
||||
)
|
||||
org_uuid: str | None = Field(
|
||||
None, description="UUID of the currently active organization"
|
||||
)
|
||||
config_path: Path = Field(default=DEFAULT_CONFIG_PATH, frozen=True, exclude=True)
|
||||
|
||||
oauth2_provider: str = Field(
|
||||
description="OAuth2 provider used for authentication (e.g., workos, okta, auth0).",
|
||||
default=DEFAULT_CLI_SETTINGS["oauth2_provider"],
|
||||
)
|
||||
|
||||
oauth2_audience: str | None = Field(
|
||||
description="OAuth2 audience value, typically used to identify the target API or resource.",
|
||||
default=DEFAULT_CLI_SETTINGS["oauth2_audience"],
|
||||
)
|
||||
|
||||
oauth2_client_id: str = Field(
|
||||
default=DEFAULT_CLI_SETTINGS["oauth2_client_id"],
|
||||
description="OAuth2 client ID issued by the provider, used during authentication requests.",
|
||||
)
|
||||
|
||||
oauth2_domain: str = Field(
|
||||
description="OAuth2 provider's domain (e.g., your-org.auth0.com) used for issuing tokens.",
|
||||
default=DEFAULT_CLI_SETTINGS["oauth2_domain"],
|
||||
)
|
||||
|
||||
oauth2_extra: dict[str, Any] = Field(
|
||||
description="Extra configuration for the OAuth2 provider.",
|
||||
default={},
|
||||
)
|
||||
|
||||
def __init__(self, config_path: Path | None = None, **data: dict[str, Any]) -> None:
|
||||
"""Load Settings from config path with fallback support."""
|
||||
if config_path is None:
|
||||
config_path = get_writable_config_path()
|
||||
|
||||
if config_path is None:
|
||||
merged_data = {**data}
|
||||
super().__init__(config_path=Path("/dev/null"), **merged_data)
|
||||
return
|
||||
|
||||
try:
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
except Exception:
|
||||
merged_data = {**data}
|
||||
super().__init__(config_path=Path("/dev/null"), **merged_data)
|
||||
return
|
||||
|
||||
file_data = {}
|
||||
if config_path.is_file():
|
||||
try:
|
||||
with config_path.open("r") as f:
|
||||
file_data = json.load(f)
|
||||
except Exception:
|
||||
file_data = {}
|
||||
|
||||
merged_data = {**file_data, **data}
|
||||
super().__init__(config_path=config_path, **merged_data)
|
||||
|
||||
def clear_user_settings(self) -> None:
|
||||
"""Clear all user settings."""
|
||||
self._reset_user_settings()
|
||||
self.dump()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset all settings to default values."""
|
||||
self._reset_user_settings()
|
||||
self._reset_cli_settings()
|
||||
self._clear_auth_tokens()
|
||||
self.dump()
|
||||
|
||||
def dump(self) -> None:
|
||||
"""Save current settings to settings.json."""
|
||||
if str(self.config_path) == "/dev/null":
|
||||
return
|
||||
|
||||
try:
|
||||
if self.config_path.is_file():
|
||||
with self.config_path.open("r") as f:
|
||||
existing_data = json.load(f)
|
||||
else:
|
||||
existing_data = {}
|
||||
|
||||
updated_data = {**existing_data, **self.model_dump(exclude_unset=True)}
|
||||
with self.config_path.open("w") as f:
|
||||
json.dump(updated_data, f, indent=4)
|
||||
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
def _reset_user_settings(self) -> None:
|
||||
"""Reset all user settings to default values."""
|
||||
for key in USER_SETTINGS_KEYS:
|
||||
setattr(self, key, None)
|
||||
|
||||
def _reset_cli_settings(self) -> None:
|
||||
"""Reset all CLI settings to default values."""
|
||||
for key in CLI_SETTINGS_KEYS:
|
||||
setattr(self, key, DEFAULT_CLI_SETTINGS.get(key))
|
||||
|
||||
def _clear_auth_tokens(self) -> None:
|
||||
"""Clear all authentication tokens."""
|
||||
TokenManager().clear_tokens()
|
||||
151
lib/crewai-core/src/crewai_core/token_manager.py
Normal file
151
lib/crewai-core/src/crewai_core/token_manager.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""Encrypted token storage shared by crewai and crewai-cli."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Final, Literal, cast
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
|
||||
_FERNET_KEY_LENGTH: Final[Literal[44]] = 44
|
||||
|
||||
|
||||
class TokenManager:
|
||||
"""Manages encrypted token storage on disk under platform-appropriate paths."""
|
||||
|
||||
def __init__(self, file_path: str = "tokens.enc") -> None:
|
||||
"""Initialize the TokenManager.
|
||||
|
||||
Args:
|
||||
file_path: The file path to store encrypted tokens.
|
||||
"""
|
||||
self.file_path = file_path
|
||||
self.key = self._get_or_create_key()
|
||||
self.fernet = Fernet(self.key)
|
||||
|
||||
def _get_or_create_key(self) -> bytes:
|
||||
"""Get or create the encryption key."""
|
||||
key_filename: str = "secret.key"
|
||||
|
||||
key = self._read_secure_file(key_filename)
|
||||
if key is not None and len(key) == _FERNET_KEY_LENGTH:
|
||||
return key
|
||||
|
||||
new_key = Fernet.generate_key()
|
||||
if self._atomic_create_secure_file(key_filename, new_key):
|
||||
return new_key
|
||||
|
||||
key = self._read_secure_file(key_filename)
|
||||
if key is not None and len(key) == _FERNET_KEY_LENGTH:
|
||||
return key
|
||||
|
||||
raise RuntimeError("Failed to create or read encryption key")
|
||||
|
||||
def save_tokens(self, access_token: str, expires_at: int) -> None:
|
||||
"""Save the access token and its expiration time.
|
||||
|
||||
Args:
|
||||
access_token: The access token to save.
|
||||
expires_at: The UNIX timestamp of the expiration time.
|
||||
"""
|
||||
expiration_time = datetime.fromtimestamp(expires_at)
|
||||
data = {
|
||||
"access_token": access_token,
|
||||
"expiration": expiration_time.isoformat(),
|
||||
}
|
||||
encrypted_data = self.fernet.encrypt(json.dumps(data).encode())
|
||||
self._atomic_write_secure_file(self.file_path, encrypted_data)
|
||||
|
||||
def get_token(self) -> str | None:
|
||||
"""Return the access token if valid and not expired, else None."""
|
||||
encrypted_data = self._read_secure_file(self.file_path)
|
||||
if encrypted_data is None:
|
||||
return None
|
||||
|
||||
decrypted_data = self.fernet.decrypt(encrypted_data)
|
||||
data = json.loads(decrypted_data)
|
||||
|
||||
expiration = datetime.fromisoformat(data["expiration"])
|
||||
if expiration <= datetime.now():
|
||||
return None
|
||||
|
||||
return cast(str | None, data.get("access_token"))
|
||||
|
||||
def clear_tokens(self) -> None:
|
||||
"""Remove the stored token file (no-op if absent)."""
|
||||
self._delete_secure_file(self.file_path)
|
||||
|
||||
@staticmethod
|
||||
def _get_secure_storage_path() -> Path:
|
||||
"""Platform-appropriate per-user credential directory (mode 0o700)."""
|
||||
if sys.platform == "win32":
|
||||
base_path = os.environ.get("LOCALAPPDATA")
|
||||
elif sys.platform == "darwin":
|
||||
base_path = os.path.expanduser("~/Library/Application Support")
|
||||
else:
|
||||
base_path = os.path.expanduser("~/.local/share")
|
||||
|
||||
app_name = "crewai/credentials"
|
||||
storage_path = Path(base_path) / app_name
|
||||
|
||||
storage_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
return storage_path
|
||||
|
||||
def _atomic_create_secure_file(self, filename: str, content: bytes) -> bool:
|
||||
"""Create a file only if it doesn't already exist."""
|
||||
storage_path = self._get_secure_storage_path()
|
||||
file_path = storage_path / filename
|
||||
|
||||
try:
|
||||
fd = os.open(file_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
||||
try:
|
||||
os.write(fd, content)
|
||||
finally:
|
||||
os.close(fd)
|
||||
return True
|
||||
except FileExistsError:
|
||||
return False
|
||||
|
||||
def _atomic_write_secure_file(self, filename: str, content: bytes) -> None:
|
||||
"""Write content to a secure file via tempfile + os.replace."""
|
||||
storage_path = self._get_secure_storage_path()
|
||||
file_path = storage_path / filename
|
||||
|
||||
fd, temp_path = tempfile.mkstemp(dir=storage_path, prefix=f".{filename}.")
|
||||
fd_closed = False
|
||||
try:
|
||||
os.write(fd, content)
|
||||
os.close(fd)
|
||||
fd_closed = True
|
||||
os.chmod(temp_path, 0o600)
|
||||
os.replace(temp_path, file_path)
|
||||
except Exception:
|
||||
if not fd_closed:
|
||||
os.close(fd)
|
||||
if os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
raise
|
||||
|
||||
def _read_secure_file(self, filename: str) -> bytes | None:
|
||||
"""Read raw bytes from a secure file, or None if absent."""
|
||||
storage_path = self._get_secure_storage_path()
|
||||
file_path = storage_path / filename
|
||||
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
def _delete_secure_file(self, filename: str) -> None:
|
||||
"""Delete a secure file (no-op if absent)."""
|
||||
storage_path = self._get_secure_storage_path()
|
||||
file_path = storage_path / filename
|
||||
file_path.unlink(missing_ok=True)
|
||||
56
lib/crewai-core/src/crewai_core/tool_credentials.py
Normal file
56
lib/crewai-core/src/crewai_core/tool_credentials.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""Tool-repository credential helpers shared by crewai and crewai-cli."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from crewai_core.project import read_toml
|
||||
from crewai_core.settings import Settings
|
||||
|
||||
|
||||
def build_env_with_tool_repository_credentials(
|
||||
repository_handle: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Return a copy of ``os.environ`` augmented with UV_INDEX_* credentials
|
||||
for ``repository_handle``.
|
||||
|
||||
The handle is normalized to upper-case with hyphens replaced by underscores
|
||||
(matching ``uv``'s env-var convention).
|
||||
"""
|
||||
repository_handle = repository_handle.upper().replace("-", "_")
|
||||
settings = Settings()
|
||||
|
||||
env = os.environ.copy()
|
||||
env[f"UV_INDEX_{repository_handle}_USERNAME"] = str(
|
||||
settings.tool_repository_username or ""
|
||||
)
|
||||
env[f"UV_INDEX_{repository_handle}_PASSWORD"] = str(
|
||||
settings.tool_repository_password or ""
|
||||
)
|
||||
|
||||
return env
|
||||
|
||||
|
||||
def build_env_with_all_tool_credentials() -> dict[str, Any]:
|
||||
"""Return ``os.environ`` augmented with UV_INDEX_* credentials for every
|
||||
private index referenced under ``[tool.uv.sources]`` in ``pyproject.toml``.
|
||||
|
||||
Errors reading ``pyproject.toml`` are swallowed — the un-augmented
|
||||
environment is returned in that case.
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
try:
|
||||
pyproject_data = read_toml()
|
||||
sources = pyproject_data.get("tool", {}).get("uv", {}).get("sources", {})
|
||||
|
||||
for source_config in sources.values():
|
||||
if isinstance(source_config, dict):
|
||||
index = source_config.get("index")
|
||||
if index:
|
||||
index_env = build_env_with_tool_repository_credentials(index)
|
||||
env.update(index_env)
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
return env
|
||||
@@ -4,13 +4,13 @@ import time
|
||||
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
||||
import webbrowser
|
||||
|
||||
from crewai_core.settings import Settings
|
||||
from crewai_core.token_manager import TokenManager
|
||||
import httpx
|
||||
from pydantic import BaseModel, Field
|
||||
from rich.console import Console
|
||||
|
||||
from crewai.auth.token_manager import TokenManager
|
||||
from crewai.auth.utils import validate_jwt_token
|
||||
from crewai.settings import Settings
|
||||
|
||||
|
||||
console = Console()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Authentication token retrieval."""
|
||||
|
||||
from crewai.auth.token_manager import TokenManager
|
||||
from crewai_core.token_manager import TokenManager
|
||||
|
||||
|
||||
class AuthError(Exception):
|
||||
|
||||
@@ -1,185 +1,14 @@
|
||||
"""Manages encrypted token storage."""
|
||||
"""Deprecated: use ``crewai_core.token_manager`` instead."""
|
||||
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Final, Literal, cast
|
||||
from __future__ import annotations
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
import warnings
|
||||
|
||||
from crewai_core.token_manager import TokenManager as TokenManager
|
||||
|
||||
|
||||
_FERNET_KEY_LENGTH: Final[Literal[44]] = 44
|
||||
|
||||
|
||||
class TokenManager:
|
||||
"""Manages encrypted token storage."""
|
||||
|
||||
def __init__(self, file_path: str = "tokens.enc") -> None:
|
||||
"""Initialize the TokenManager.
|
||||
|
||||
Args:
|
||||
file_path: The file path to store encrypted tokens.
|
||||
"""
|
||||
self.file_path = file_path
|
||||
self.key = self._get_or_create_key()
|
||||
self.fernet = Fernet(self.key)
|
||||
|
||||
def _get_or_create_key(self) -> bytes:
|
||||
"""Get or create the encryption key.
|
||||
|
||||
Returns:
|
||||
The encryption key as bytes.
|
||||
"""
|
||||
key_filename: str = "secret.key"
|
||||
|
||||
key = self._read_secure_file(key_filename)
|
||||
if key is not None and len(key) == _FERNET_KEY_LENGTH:
|
||||
return key
|
||||
|
||||
new_key = Fernet.generate_key()
|
||||
if self._atomic_create_secure_file(key_filename, new_key):
|
||||
return new_key
|
||||
|
||||
key = self._read_secure_file(key_filename)
|
||||
if key is not None and len(key) == _FERNET_KEY_LENGTH:
|
||||
return key
|
||||
|
||||
raise RuntimeError("Failed to create or read encryption key")
|
||||
|
||||
def save_tokens(self, access_token: str, expires_at: int) -> None:
|
||||
"""Save the access token and its expiration time.
|
||||
|
||||
Args:
|
||||
access_token: The access token to save.
|
||||
expires_at: The UNIX timestamp of the expiration time.
|
||||
"""
|
||||
expiration_time = datetime.fromtimestamp(expires_at)
|
||||
data = {
|
||||
"access_token": access_token,
|
||||
"expiration": expiration_time.isoformat(),
|
||||
}
|
||||
encrypted_data = self.fernet.encrypt(json.dumps(data).encode())
|
||||
self._atomic_write_secure_file(self.file_path, encrypted_data)
|
||||
|
||||
def get_token(self) -> str | None:
|
||||
"""Get the access token if it is valid and not expired.
|
||||
|
||||
Returns:
|
||||
The access token if valid and not expired, otherwise None.
|
||||
"""
|
||||
encrypted_data = self._read_secure_file(self.file_path)
|
||||
if encrypted_data is None:
|
||||
return None
|
||||
|
||||
decrypted_data = self.fernet.decrypt(encrypted_data)
|
||||
data = json.loads(decrypted_data)
|
||||
|
||||
expiration = datetime.fromisoformat(data["expiration"])
|
||||
if expiration <= datetime.now():
|
||||
return None
|
||||
|
||||
return cast(str | None, data.get("access_token"))
|
||||
|
||||
def clear_tokens(self) -> None:
|
||||
"""Clear the stored tokens."""
|
||||
self._delete_secure_file(self.file_path)
|
||||
|
||||
@staticmethod
|
||||
def _get_secure_storage_path() -> Path:
|
||||
"""Get the secure storage path based on the operating system.
|
||||
|
||||
Returns:
|
||||
The secure storage path.
|
||||
"""
|
||||
if sys.platform == "win32":
|
||||
base_path = os.environ.get("LOCALAPPDATA")
|
||||
elif sys.platform == "darwin":
|
||||
base_path = os.path.expanduser("~/Library/Application Support")
|
||||
else:
|
||||
base_path = os.path.expanduser("~/.local/share")
|
||||
|
||||
app_name = "crewai/credentials"
|
||||
storage_path = Path(base_path) / app_name
|
||||
|
||||
storage_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
return storage_path
|
||||
|
||||
def _atomic_create_secure_file(self, filename: str, content: bytes) -> bool:
|
||||
"""Create a file only if it doesn't exist.
|
||||
|
||||
Args:
|
||||
filename: The name of the file.
|
||||
content: The content to write.
|
||||
|
||||
Returns:
|
||||
True if file was created, False if it already exists.
|
||||
"""
|
||||
storage_path = self._get_secure_storage_path()
|
||||
file_path = storage_path / filename
|
||||
|
||||
try:
|
||||
fd = os.open(file_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
||||
try:
|
||||
os.write(fd, content)
|
||||
finally:
|
||||
os.close(fd)
|
||||
return True
|
||||
except FileExistsError:
|
||||
return False
|
||||
|
||||
def _atomic_write_secure_file(self, filename: str, content: bytes) -> None:
|
||||
"""Write content to a secure file.
|
||||
|
||||
Args:
|
||||
filename: The name of the file.
|
||||
content: The content to write.
|
||||
"""
|
||||
storage_path = self._get_secure_storage_path()
|
||||
file_path = storage_path / filename
|
||||
|
||||
fd, temp_path = tempfile.mkstemp(dir=storage_path, prefix=f".{filename}.")
|
||||
fd_closed = False
|
||||
try:
|
||||
os.write(fd, content)
|
||||
os.close(fd)
|
||||
fd_closed = True
|
||||
os.chmod(temp_path, 0o600)
|
||||
os.replace(temp_path, file_path)
|
||||
except Exception:
|
||||
if not fd_closed:
|
||||
os.close(fd)
|
||||
if os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
raise
|
||||
|
||||
def _read_secure_file(self, filename: str) -> bytes | None:
|
||||
"""Read the content of a secure file.
|
||||
|
||||
Args:
|
||||
filename: The name of the file.
|
||||
|
||||
Returns:
|
||||
The content of the file if it exists, otherwise None.
|
||||
"""
|
||||
storage_path = self._get_secure_storage_path()
|
||||
file_path = storage_path / filename
|
||||
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
def _delete_secure_file(self, filename: str) -> None:
|
||||
"""Delete a secure file.
|
||||
|
||||
Args:
|
||||
filename: The name of the file.
|
||||
"""
|
||||
storage_path = self._get_secure_storage_path()
|
||||
file_path = storage_path / filename
|
||||
file_path.unlink(missing_ok=True)
|
||||
warnings.warn(
|
||||
"crewai.auth.token_manager is deprecated; import from crewai_core.token_manager.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from crewai_core.constants import (
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_AUDIENCE as CREWAI_ENTERPRISE_DEFAULT_OAUTH2_AUDIENCE,
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_CLIENT_ID as CREWAI_ENTERPRISE_DEFAULT_OAUTH2_CLIENT_ID,
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN as CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN,
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_PROVIDER as CREWAI_ENTERPRISE_DEFAULT_OAUTH2_PROVIDER,
|
||||
DEFAULT_CREWAI_ENTERPRISE_URL as DEFAULT_CREWAI_ENTERPRISE_URL,
|
||||
)
|
||||
|
||||
DEFAULT_CREWAI_ENTERPRISE_URL = "https://app.crewai.com"
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_PROVIDER = "workos"
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_AUDIENCE = "client_01JNJQWBJ4SPFN3SWJM5T7BDG8"
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_CLIENT_ID = "client_01JYT06R59SP0NXYGD994NFXXX"
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN = "login.crewai.com"
|
||||
|
||||
ENV_VARS: dict[str, list[dict[str, Any]]] = {
|
||||
"openai": [
|
||||
|
||||
@@ -6,6 +6,7 @@ import time
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
from crewai_core.settings import Settings
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
@@ -18,7 +19,6 @@ from crewai.events.listeners.tracing.utils import (
|
||||
should_auto_collect_first_time_traces,
|
||||
)
|
||||
from crewai.plus_api import PlusAPI
|
||||
from crewai.settings import Settings
|
||||
from crewai.version import get_crewai_version
|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import os
|
||||
from typing import Any
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from crewai_core.settings import Settings
|
||||
import httpx
|
||||
|
||||
from crewai.constants import DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
from crewai.settings import Settings
|
||||
from crewai.version import get_crewai_version
|
||||
|
||||
|
||||
|
||||
@@ -1,216 +1,18 @@
|
||||
"""CrewAI platform settings management."""
|
||||
"""Re-exports of shared settings from ``crewai_core.settings``.
|
||||
|
||||
import json
|
||||
from logging import getLogger
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from typing import Any
|
||||
Existing imports from ``crewai.settings`` continue to work; new code should
|
||||
import from ``crewai_core.settings`` directly.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from __future__ import annotations
|
||||
|
||||
from crewai.auth.token_manager import TokenManager
|
||||
from crewai.constants import (
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_AUDIENCE,
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_CLIENT_ID,
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN,
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_PROVIDER,
|
||||
DEFAULT_CREWAI_ENTERPRISE_URL,
|
||||
from crewai_core.settings import (
|
||||
CLI_SETTINGS_KEYS as CLI_SETTINGS_KEYS,
|
||||
DEFAULT_CLI_SETTINGS as DEFAULT_CLI_SETTINGS,
|
||||
DEFAULT_CONFIG_PATH as DEFAULT_CONFIG_PATH,
|
||||
HIDDEN_SETTINGS_KEYS as HIDDEN_SETTINGS_KEYS,
|
||||
READONLY_SETTINGS_KEYS as READONLY_SETTINGS_KEYS,
|
||||
USER_SETTINGS_KEYS as USER_SETTINGS_KEYS,
|
||||
Settings as Settings,
|
||||
get_writable_config_path as get_writable_config_path,
|
||||
)
|
||||
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
DEFAULT_CONFIG_PATH = Path.home() / ".config" / "crewai" / "settings.json"
|
||||
|
||||
|
||||
def get_writable_config_path() -> Path | None:
|
||||
"""Find a writable location for the config file with fallback options.
|
||||
|
||||
Tries in order:
|
||||
1. Default: ~/.config/crewai/settings.json
|
||||
2. Temp directory: /tmp/crewai_settings.json (or OS equivalent)
|
||||
3. Current directory: ./crewai_settings.json
|
||||
4. In-memory only (returns None)
|
||||
|
||||
Returns:
|
||||
Path object for writable config location, or None if no writable location found.
|
||||
"""
|
||||
fallback_paths = [
|
||||
DEFAULT_CONFIG_PATH,
|
||||
Path(tempfile.gettempdir()) / "crewai_settings.json",
|
||||
Path.cwd() / "crewai_settings.json",
|
||||
]
|
||||
|
||||
for config_path in fallback_paths:
|
||||
try:
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
test_file = config_path.parent / ".crewai_write_test"
|
||||
try:
|
||||
test_file.write_text("test")
|
||||
test_file.unlink()
|
||||
logger.info(f"Using config path: {config_path}")
|
||||
return config_path
|
||||
except Exception: # noqa: S112
|
||||
continue
|
||||
|
||||
except Exception: # noqa: S112
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
|
||||
USER_SETTINGS_KEYS = [
|
||||
"tool_repository_username",
|
||||
"tool_repository_password",
|
||||
"org_name",
|
||||
"org_uuid",
|
||||
]
|
||||
|
||||
CLI_SETTINGS_KEYS = [
|
||||
"enterprise_base_url",
|
||||
"oauth2_provider",
|
||||
"oauth2_audience",
|
||||
"oauth2_client_id",
|
||||
"oauth2_domain",
|
||||
"oauth2_extra",
|
||||
]
|
||||
|
||||
DEFAULT_CLI_SETTINGS = {
|
||||
"enterprise_base_url": DEFAULT_CREWAI_ENTERPRISE_URL,
|
||||
"oauth2_provider": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_PROVIDER,
|
||||
"oauth2_audience": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_AUDIENCE,
|
||||
"oauth2_client_id": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_CLIENT_ID,
|
||||
"oauth2_domain": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN,
|
||||
"oauth2_extra": {},
|
||||
}
|
||||
|
||||
READONLY_SETTINGS_KEYS = [
|
||||
"org_name",
|
||||
"org_uuid",
|
||||
]
|
||||
|
||||
HIDDEN_SETTINGS_KEYS = [
|
||||
"config_path",
|
||||
"tool_repository_username",
|
||||
"tool_repository_password",
|
||||
]
|
||||
|
||||
|
||||
class Settings(BaseModel):
|
||||
"""CrewAI platform settings."""
|
||||
|
||||
enterprise_base_url: str | None = Field(
|
||||
default=DEFAULT_CREWAI_ENTERPRISE_URL,
|
||||
description="Base URL of the CrewAI AMP instance",
|
||||
)
|
||||
tool_repository_username: str | None = Field(
|
||||
None, description="Username for interacting with the Tool Repository"
|
||||
)
|
||||
tool_repository_password: str | None = Field(
|
||||
None, description="Password for interacting with the Tool Repository"
|
||||
)
|
||||
org_name: str | None = Field(
|
||||
None, description="Name of the currently active organization"
|
||||
)
|
||||
org_uuid: str | None = Field(
|
||||
None, description="UUID of the currently active organization"
|
||||
)
|
||||
config_path: Path = Field(default=DEFAULT_CONFIG_PATH, frozen=True, exclude=True)
|
||||
|
||||
oauth2_provider: str = Field(
|
||||
description="OAuth2 provider used for authentication (e.g., workos, okta, auth0).",
|
||||
default=CREWAI_ENTERPRISE_DEFAULT_OAUTH2_PROVIDER,
|
||||
)
|
||||
|
||||
oauth2_audience: str | None = Field(
|
||||
description="OAuth2 audience value, typically used to identify the target API or resource.",
|
||||
default=CREWAI_ENTERPRISE_DEFAULT_OAUTH2_AUDIENCE,
|
||||
)
|
||||
|
||||
oauth2_client_id: str = Field(
|
||||
default=CREWAI_ENTERPRISE_DEFAULT_OAUTH2_CLIENT_ID,
|
||||
description="OAuth2 client ID issued by the provider, used during authentication requests.",
|
||||
)
|
||||
|
||||
oauth2_domain: str = Field(
|
||||
description="OAuth2 provider's domain (e.g., your-org.auth0.com) used for issuing tokens.",
|
||||
default=CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN,
|
||||
)
|
||||
|
||||
oauth2_extra: dict[str, Any] = Field(
|
||||
description="Extra configuration for the OAuth2 provider.",
|
||||
default={},
|
||||
)
|
||||
|
||||
def __init__(self, config_path: Path | None = None, **data: dict[str, Any]) -> None:
|
||||
"""Load Settings from config path with fallback support."""
|
||||
if config_path is None:
|
||||
config_path = get_writable_config_path()
|
||||
|
||||
if config_path is None:
|
||||
merged_data = {**data}
|
||||
super().__init__(config_path=Path("/dev/null"), **merged_data)
|
||||
return
|
||||
|
||||
try:
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
except Exception:
|
||||
merged_data = {**data}
|
||||
super().__init__(config_path=Path("/dev/null"), **merged_data)
|
||||
return
|
||||
|
||||
file_data = {}
|
||||
if config_path.is_file():
|
||||
try:
|
||||
with config_path.open("r") as f:
|
||||
file_data = json.load(f)
|
||||
except Exception:
|
||||
file_data = {}
|
||||
|
||||
merged_data = {**file_data, **data}
|
||||
super().__init__(config_path=config_path, **merged_data)
|
||||
|
||||
def clear_user_settings(self) -> None:
|
||||
"""Clear all user settings."""
|
||||
self._reset_user_settings()
|
||||
self.dump()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset all settings to default values."""
|
||||
self._reset_user_settings()
|
||||
self._reset_cli_settings()
|
||||
self._clear_auth_tokens()
|
||||
self.dump()
|
||||
|
||||
def dump(self) -> None:
|
||||
"""Save current settings to settings.json."""
|
||||
if str(self.config_path) == "/dev/null":
|
||||
return
|
||||
|
||||
try:
|
||||
if self.config_path.is_file():
|
||||
with self.config_path.open("r") as f:
|
||||
existing_data = json.load(f)
|
||||
else:
|
||||
existing_data = {}
|
||||
|
||||
updated_data = {**existing_data, **self.model_dump(exclude_unset=True)}
|
||||
with self.config_path.open("w") as f:
|
||||
json.dump(updated_data, f, indent=4)
|
||||
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
def _reset_user_settings(self) -> None:
|
||||
"""Reset all user settings to default values."""
|
||||
for key in USER_SETTINGS_KEYS:
|
||||
setattr(self, key, None)
|
||||
|
||||
def _reset_cli_settings(self) -> None:
|
||||
"""Reset all CLI settings to default values."""
|
||||
for key in CLI_SETTINGS_KEYS:
|
||||
setattr(self, key, DEFAULT_CLI_SETTINGS.get(key))
|
||||
|
||||
def _clear_auth_tokens(self) -> None:
|
||||
"""Clear all authentication tokens."""
|
||||
TokenManager().clear_tokens()
|
||||
|
||||
@@ -13,6 +13,7 @@ import re
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, TypedDict
|
||||
|
||||
from crewai_core.printer import PRINTER, ColoredText, Printer
|
||||
from crewai_core.settings import Settings
|
||||
from pydantic import BaseModel
|
||||
from rich.console import Console
|
||||
|
||||
@@ -24,7 +25,6 @@ from crewai.agents.parser import (
|
||||
parse,
|
||||
)
|
||||
from crewai.llms.base_llm import BaseLLM, call_stop_override
|
||||
from crewai.settings import Settings
|
||||
from crewai.tools import BaseTool as CrewAITool
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
from crewai.tools.structured_tool import CrewStructuredTool
|
||||
|
||||
@@ -22,11 +22,14 @@ from crewai_core.project import (
|
||||
parse_toml as parse_toml,
|
||||
read_toml as read_toml,
|
||||
)
|
||||
from crewai_core.tool_credentials import (
|
||||
build_env_with_all_tool_credentials as build_env_with_all_tool_credentials,
|
||||
build_env_with_tool_repository_credentials as build_env_with_tool_repository_credentials,
|
||||
)
|
||||
from rich.console import Console
|
||||
|
||||
from crewai.crew import Crew
|
||||
from crewai.flow import Flow
|
||||
from crewai.settings import Settings
|
||||
|
||||
|
||||
console = Console()
|
||||
@@ -289,49 +292,6 @@ def extract_available_exports(dir_path: str = "src") -> list[dict[str, Any]]:
|
||||
raise SystemExit(1) from e
|
||||
|
||||
|
||||
def build_env_with_tool_repository_credentials(
|
||||
repository_handle: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Build environment variables with tool repository credentials."""
|
||||
repository_handle = repository_handle.upper().replace("-", "_")
|
||||
settings = Settings()
|
||||
|
||||
env = os.environ.copy()
|
||||
env[f"UV_INDEX_{repository_handle}_USERNAME"] = str(
|
||||
settings.tool_repository_username or ""
|
||||
)
|
||||
env[f"UV_INDEX_{repository_handle}_PASSWORD"] = str(
|
||||
settings.tool_repository_password or ""
|
||||
)
|
||||
|
||||
return env
|
||||
|
||||
|
||||
def build_env_with_all_tool_credentials() -> dict[str, Any]:
|
||||
"""
|
||||
Build environment dict with credentials for all tool repository indexes
|
||||
found in pyproject.toml's [tool.uv.sources] section.
|
||||
|
||||
Returns:
|
||||
dict: Environment variables with credentials for all private indexes.
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
try:
|
||||
pyproject_data = read_toml()
|
||||
sources = pyproject_data.get("tool", {}).get("uv", {}).get("sources", {})
|
||||
|
||||
for source_config in sources.values():
|
||||
if isinstance(source_config, dict):
|
||||
index = source_config.get("index")
|
||||
if index:
|
||||
index_env = build_env_with_tool_repository_credentials(index)
|
||||
env.update(index_env)
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
return env
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _load_module_from_file(
|
||||
init_file: Path, module_name: str | None = None
|
||||
|
||||
@@ -12,7 +12,7 @@ from crewai.settings import (
|
||||
USER_SETTINGS_KEYS,
|
||||
Settings,
|
||||
)
|
||||
from crewai.auth.token_manager import TokenManager
|
||||
from crewai_core.token_manager import TokenManager
|
||||
|
||||
|
||||
class TestSettings(unittest.TestCase):
|
||||
@@ -69,7 +69,7 @@ class TestSettings(unittest.TestCase):
|
||||
for key in user_settings.keys():
|
||||
self.assertEqual(getattr(settings, key), None)
|
||||
|
||||
@patch("crewai.settings.TokenManager")
|
||||
@patch("crewai_core.settings.TokenManager")
|
||||
def test_reset_settings(self, mock_token_manager):
|
||||
user_settings = {key: f"value_for_{key}" for key in USER_SETTINGS_KEYS}
|
||||
cli_settings = {key: f"value_for_{key}" for key in CLI_SETTINGS_KEYS if key != "oauth2_extra"}
|
||||
|
||||
@@ -10,20 +10,20 @@ from unittest.mock import patch
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
from crewai.auth.token_manager import TokenManager
|
||||
from crewai_core.token_manager import TokenManager
|
||||
|
||||
|
||||
class TestTokenManager(unittest.TestCase):
|
||||
"""Test cases for TokenManager."""
|
||||
|
||||
@patch("crewai.auth.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def setUp(self, mock_get_key: unittest.mock.MagicMock) -> None:
|
||||
"""Set up test fixtures."""
|
||||
mock_get_key.return_value = Fernet.generate_key()
|
||||
self.token_manager = TokenManager()
|
||||
|
||||
@patch("crewai.auth.token_manager.TokenManager._read_secure_file")
|
||||
@patch("crewai.auth.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._read_secure_file")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_get_or_create_key_existing(
|
||||
self,
|
||||
mock_get_or_create: unittest.mock.MagicMock,
|
||||
@@ -45,7 +45,7 @@ class TestTokenManager(unittest.TestCase):
|
||||
with (
|
||||
patch.object(self.token_manager, "_read_secure_file", return_value=None) as mock_read,
|
||||
patch.object(self.token_manager, "_atomic_create_secure_file", return_value=True) as mock_atomic_create,
|
||||
patch("crewai.auth.token_manager.Fernet.generate_key", return_value=mock_key) as mock_generate,
|
||||
patch("crewai_core.token_manager.Fernet.generate_key", return_value=mock_key) as mock_generate,
|
||||
):
|
||||
result = self.token_manager._get_or_create_key()
|
||||
|
||||
@@ -62,14 +62,14 @@ class TestTokenManager(unittest.TestCase):
|
||||
with (
|
||||
patch.object(self.token_manager, "_read_secure_file", side_effect=[None, their_key]) as mock_read,
|
||||
patch.object(self.token_manager, "_atomic_create_secure_file", return_value=False) as mock_atomic_create,
|
||||
patch("crewai.auth.token_manager.Fernet.generate_key", return_value=our_key),
|
||||
patch("crewai_core.token_manager.Fernet.generate_key", return_value=our_key),
|
||||
):
|
||||
result = self.token_manager._get_or_create_key()
|
||||
|
||||
self.assertEqual(result, their_key)
|
||||
self.assertEqual(mock_read.call_count, 2)
|
||||
|
||||
@patch("crewai.auth.token_manager.TokenManager._atomic_write_secure_file")
|
||||
@patch("crewai_core.token_manager.TokenManager._atomic_write_secure_file")
|
||||
def test_save_tokens(
|
||||
self, mock_write: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -88,7 +88,7 @@ class TestTokenManager(unittest.TestCase):
|
||||
expiration = datetime.fromisoformat(data["expiration"])
|
||||
self.assertEqual(expiration, datetime.fromtimestamp(expires_at))
|
||||
|
||||
@patch("crewai.auth.token_manager.TokenManager._read_secure_file")
|
||||
@patch("crewai_core.token_manager.TokenManager._read_secure_file")
|
||||
def test_get_token_valid(
|
||||
self, mock_read: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -103,7 +103,7 @@ class TestTokenManager(unittest.TestCase):
|
||||
|
||||
self.assertEqual(result, access_token)
|
||||
|
||||
@patch("crewai.auth.token_manager.TokenManager._read_secure_file")
|
||||
@patch("crewai_core.token_manager.TokenManager._read_secure_file")
|
||||
def test_get_token_expired(
|
||||
self, mock_read: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -118,7 +118,7 @@ class TestTokenManager(unittest.TestCase):
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
@patch("crewai.auth.token_manager.TokenManager._read_secure_file")
|
||||
@patch("crewai_core.token_manager.TokenManager._read_secure_file")
|
||||
def test_get_token_not_found(
|
||||
self, mock_read: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -129,7 +129,7 @@ class TestTokenManager(unittest.TestCase):
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
@patch("crewai.auth.token_manager.TokenManager._delete_secure_file")
|
||||
@patch("crewai_core.token_manager.TokenManager._delete_secure_file")
|
||||
def test_clear_tokens(
|
||||
self, mock_delete: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -159,7 +159,7 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
import shutil
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
@patch("crewai.auth.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_atomic_create_new_file(
|
||||
self, mock_get_key: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -175,7 +175,7 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
self.assertEqual(file_path.read_bytes(), b"content")
|
||||
self.assertEqual(file_path.stat().st_mode & 0o777, 0o600)
|
||||
|
||||
@patch("crewai.auth.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_atomic_create_existing_file(
|
||||
self, mock_get_key: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -192,7 +192,7 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
self.assertFalse(result)
|
||||
self.assertEqual(file_path.read_bytes(), b"original")
|
||||
|
||||
@patch("crewai.auth.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_atomic_write_new_file(
|
||||
self, mock_get_key: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -207,7 +207,7 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
self.assertEqual(file_path.read_bytes(), b"content")
|
||||
self.assertEqual(file_path.stat().st_mode & 0o777, 0o600)
|
||||
|
||||
@patch("crewai.auth.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_atomic_write_overwrites(
|
||||
self, mock_get_key: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -222,7 +222,7 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
|
||||
self.assertEqual(file_path.read_bytes(), b"new content")
|
||||
|
||||
@patch("crewai.auth.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_atomic_write_no_temp_file_on_success(
|
||||
self, mock_get_key: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -236,7 +236,7 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
temp_files = list(Path(self.temp_dir).glob(".test.txt.*"))
|
||||
self.assertEqual(len(temp_files), 0)
|
||||
|
||||
@patch("crewai.auth.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_read_secure_file_exists(
|
||||
self, mock_get_key: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -251,7 +251,7 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
|
||||
self.assertEqual(result, b"content")
|
||||
|
||||
@patch("crewai.auth.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_read_secure_file_not_exists(
|
||||
self, mock_get_key: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -263,7 +263,7 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
@patch("crewai.auth.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_delete_secure_file_exists(
|
||||
self, mock_get_key: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
@@ -278,7 +278,7 @@ class TestAtomicFileOperations(unittest.TestCase):
|
||||
|
||||
self.assertFalse(file_path.exists())
|
||||
|
||||
@patch("crewai.auth.token_manager.TokenManager._get_or_create_key")
|
||||
@patch("crewai_core.token_manager.TokenManager._get_or_create_key")
|
||||
def test_delete_secure_file_not_exists(
|
||||
self, mock_get_key: unittest.mock.MagicMock
|
||||
) -> None:
|
||||
|
||||
6
uv.lock
generated
6
uv.lock
generated
@@ -1476,21 +1476,27 @@ name = "crewai-core"
|
||||
source = { editable = "lib/crewai-core" }
|
||||
dependencies = [
|
||||
{ name = "appdirs" },
|
||||
{ name = "cryptography" },
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "portalocker" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
{ name = "tomli" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "appdirs", specifier = "~=1.4.4" },
|
||||
{ name = "cryptography", specifier = ">=42.0" },
|
||||
{ name = "opentelemetry-api", specifier = "~=1.34.0" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http", specifier = "~=1.34.0" },
|
||||
{ name = "opentelemetry-sdk", specifier = "~=1.34.0" },
|
||||
{ name = "portalocker", specifier = "~=2.7.0" },
|
||||
{ name = "pydantic", specifier = ">=2.11.9,<2.13" },
|
||||
{ name = "rich", specifier = ">=13.7.1" },
|
||||
{ name = "tomli", specifier = "~=2.0.2" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user