feat: add multiple provider support (#3089)

* Remove `crewai signup` command, update docs

* Add `Settings.clear()` and clear settings before each login

* Add pyjwt

* Remove print statement from ToolCommand.login()

* Remove auth0 dependency

* Update docs
This commit is contained in:
Heitor Carvalho
2025-07-02 17:44:47 -03:00
committed by GitHub
parent 68f5bdf0d9
commit a77dcdd419
16 changed files with 760 additions and 213 deletions

View File

@@ -4,6 +4,8 @@ description: Learn how to use the CrewAI CLI to interact with CrewAI.
icon: terminal icon: terminal
--- ---
<Warning>Since release 0.140.0, CrewAI Enterprise started a process of migrating their login provider. As such, the authentication flow via CLI was updated. Users that use Google to login, or that created their account after July 3rd, 2025 will be unable to log in with older versions of the `crewai` library.</Warning>
## Overview ## Overview
The CrewAI CLI provides a set of commands to interact with CrewAI, allowing you to create, train, run, and manage crews & flows. The CrewAI CLI provides a set of commands to interact with CrewAI, allowing you to create, train, run, and manage crews & flows.
@@ -186,10 +188,7 @@ def crew(self) -> Crew:
Deploy the crew or flow to [CrewAI Enterprise](https://app.crewai.com). Deploy the crew or flow to [CrewAI Enterprise](https://app.crewai.com).
- **Authentication**: You need to be authenticated to deploy to CrewAI Enterprise. - **Authentication**: You need to be authenticated to deploy to CrewAI Enterprise.
```shell Terminal You can login or create an account with:
crewai signup
```
If you already have an account, you can login with:
```shell Terminal ```shell Terminal
crewai login crewai login
``` ```

View File

@@ -41,11 +41,8 @@ The CLI provides the fastest way to deploy locally developed crews to the Enterp
First, you need to authenticate your CLI with the CrewAI Enterprise platform: First, you need to authenticate your CLI with the CrewAI Enterprise platform:
```bash ```bash
# If you already have a CrewAI Enterprise account # If you already have a CrewAI Enterprise account, or want to create one:
crewai login crewai login
# If you're creating a new account
crewai signup
``` ```
When you run either command, the CLI will: When you run either command, the CLI will:

View File

@@ -3,6 +3,7 @@ title: CLI
description: Aprenda a usar o CLI do CrewAI para interagir com o CrewAI. description: Aprenda a usar o CLI do CrewAI para interagir com o CrewAI.
icon: terminal icon: terminal
--- ---
<Warning>A partir da versão 0.140.0, a plataforma CrewAI Enterprise iniciou um processo de migração de seu provedor de login. Como resultado, o fluxo de autenticação via CLI foi atualizado. Usuários que utlizam o Google para fazer login, ou que criaram conta após 3 de julho de 2025 não poderão fazer login com versões anteriores da biblioteca `crewai`.</Warning>
## Visão Geral ## Visão Geral

View File

@@ -41,11 +41,8 @@ A CLI fornece a maneira mais rápida de implantar crews desenvolvidos localmente
Primeiro, você precisa autenticar sua CLI com a plataforma CrewAI Enterprise: Primeiro, você precisa autenticar sua CLI com a plataforma CrewAI Enterprise:
```bash ```bash
# Se já possui uma conta CrewAI Enterprise # Se já possui uma conta CrewAI Enterprise, ou deseja criar uma:
crewai login crewai login
# Se vai criar uma nova conta
crewai signup
``` ```
Ao executar qualquer um dos comandos, a CLI irá: Ao executar qualquer um dos comandos, a CLI irá:

View File

@@ -27,8 +27,8 @@ dependencies = [
"openpyxl>=3.1.5", "openpyxl>=3.1.5",
"pyvis>=0.3.2", "pyvis>=0.3.2",
# Authentication and Security # Authentication and Security
"auth0-python>=4.7.1",
"python-dotenv>=1.0.0", "python-dotenv>=1.0.0",
"pyjwt>=2.9.0",
# Configuration and Utils # Configuration and Utils
"click>=8.1.7", "click>=8.1.7",
"appdirs>=1.4.4", "appdirs>=1.4.4",

View File

@@ -2,3 +2,7 @@ ALGORITHMS = ["RS256"]
AUTH0_DOMAIN = "crewai.us.auth0.com" AUTH0_DOMAIN = "crewai.us.auth0.com"
AUTH0_CLIENT_ID = "DEVC5Fw6NlRoSzmDCcOhVq85EfLBjKa8" AUTH0_CLIENT_ID = "DEVC5Fw6NlRoSzmDCcOhVq85EfLBjKa8"
AUTH0_AUDIENCE = "https://crewai.us.auth0.com/api/v2/" AUTH0_AUDIENCE = "https://crewai.us.auth0.com/api/v2/"
WORKOS_DOMAIN = "login.crewai.com"
WORKOS_CLI_CONNECT_APP_ID = "client_01JYT06R59SP0NXYGD994NFXXX"
WORKOS_ENVIRONMENT_ID = "client_01JNJQWB4HG8T5980R5VHP057C"

View File

@@ -5,37 +5,72 @@ from typing import Any, Dict
import requests import requests
from rich.console import Console from rich.console import Console
from .constants import AUTH0_AUDIENCE, AUTH0_CLIENT_ID, AUTH0_DOMAIN from .constants import (
from .utils import TokenManager, validate_token AUTH0_AUDIENCE,
AUTH0_CLIENT_ID,
AUTH0_DOMAIN,
WORKOS_DOMAIN,
WORKOS_CLI_CONNECT_APP_ID,
WORKOS_ENVIRONMENT_ID,
)
from .utils import TokenManager, validate_jwt_token
from urllib.parse import quote
from crewai.cli.plus_api import PlusAPI
from crewai.cli.config import Settings
console = Console() console = Console()
class AuthenticationCommand: class AuthenticationCommand:
DEVICE_CODE_URL = f"https://{AUTH0_DOMAIN}/oauth/device/code" AUTH0_DEVICE_CODE_URL = f"https://{AUTH0_DOMAIN}/oauth/device/code"
TOKEN_URL = f"https://{AUTH0_DOMAIN}/oauth/token" AUTH0_TOKEN_URL = f"https://{AUTH0_DOMAIN}/oauth/token"
WORKOS_DEVICE_CODE_URL = f"https://{WORKOS_DOMAIN}/oauth2/device_authorization"
WORKOS_TOKEN_URL = f"https://{WORKOS_DOMAIN}/oauth2/token"
def __init__(self): def __init__(self):
self.token_manager = TokenManager() self.token_manager = TokenManager()
# TODO: WORKOS - This variable is temporary until migration to WorkOS is complete.
self.user_provider = "workos"
def signup(self) -> None: def login(self) -> None:
"""Sign up to CrewAI+""" """Sign up to CrewAI+"""
console.print("Signing Up to CrewAI+ \n", style="bold blue")
device_code_data = self._get_device_code() device_code_url = self.WORKOS_DEVICE_CODE_URL
token_url = self.WORKOS_TOKEN_URL
client_id = WORKOS_CLI_CONNECT_APP_ID
audience = None
console.print("Signing in to CrewAI Enterprise...\n", style="bold blue")
# TODO: WORKOS - Next line and conditional are temporary until migration to WorkOS is complete.
user_provider = self._determine_user_provider()
if user_provider == "auth0":
device_code_url = self.AUTH0_DEVICE_CODE_URL
token_url = self.AUTH0_TOKEN_URL
client_id = AUTH0_CLIENT_ID
audience = AUTH0_AUDIENCE
self.user_provider = "auth0"
# End of temporary code.
device_code_data = self._get_device_code(client_id, device_code_url, audience)
self._display_auth_instructions(device_code_data) self._display_auth_instructions(device_code_data)
return self._poll_for_token(device_code_data) return self._poll_for_token(device_code_data, client_id, token_url)
def _get_device_code(self) -> Dict[str, Any]: def _get_device_code(
self, client_id: str, device_code_url: str, audience: str | None = None
) -> Dict[str, Any]:
"""Get the device code to authenticate the user.""" """Get the device code to authenticate the user."""
device_code_payload = { device_code_payload = {
"client_id": AUTH0_CLIENT_ID, "client_id": client_id,
"scope": "openid", "scope": "openid",
"audience": AUTH0_AUDIENCE, "audience": audience,
} }
response = requests.post( response = requests.post(
url=self.DEVICE_CODE_URL, data=device_code_payload, timeout=20 url=device_code_url, data=device_code_payload, timeout=20
) )
response.raise_for_status() response.raise_for_status()
return response.json() return response.json()
@@ -46,38 +81,33 @@ class AuthenticationCommand:
console.print("2. Enter the following code: ", device_code_data["user_code"]) console.print("2. Enter the following code: ", device_code_data["user_code"])
webbrowser.open(device_code_data["verification_uri_complete"]) webbrowser.open(device_code_data["verification_uri_complete"])
def _poll_for_token(self, device_code_data: Dict[str, Any]) -> None: def _poll_for_token(
"""Poll the server for the token.""" self, device_code_data: Dict[str, Any], client_id: str, token_poll_url: str
) -> None:
"""Polls the server for the token until it is received, or max attempts are reached."""
token_payload = { token_payload = {
"grant_type": "urn:ietf:params:oauth:grant-type:device_code", "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"device_code": device_code_data["device_code"], "device_code": device_code_data["device_code"],
"client_id": AUTH0_CLIENT_ID, "client_id": client_id,
} }
console.print("\nWaiting for authentication... ", style="bold blue", end="")
attempts = 0 attempts = 0
while True and attempts < 5: while True and attempts < 10:
response = requests.post(self.TOKEN_URL, data=token_payload, timeout=30) response = requests.post(token_poll_url, data=token_payload, timeout=30)
token_data = response.json() token_data = response.json()
if response.status_code == 200: if response.status_code == 200:
validate_token(token_data["id_token"]) self._validate_and_save_token(token_data)
expires_in = 360000 # Token expiration time in seconds
self.token_manager.save_tokens(token_data["access_token"], expires_in)
try: console.print(
from crewai.cli.tools.main import ToolCommand "Success!",
ToolCommand().login() style="bold green",
except Exception: )
console.print(
"\n[bold yellow]Warning:[/bold yellow] Authentication with the Tool Repository failed.", self._login_to_tool_repository()
style="yellow",
)
console.print(
"Other features will work normally, but you may experience limitations "
"with downloading and publishing tools."
"\nRun [bold]crewai login[/bold] to try logging in again.\n",
style="yellow",
)
console.print( console.print(
"\n[bold green]Welcome to CrewAI Enterprise![/bold green]\n" "\n[bold green]Welcome to CrewAI Enterprise![/bold green]\n"
@@ -93,3 +123,88 @@ class AuthenticationCommand:
console.print( console.print(
"Timeout: Failed to get the token. Please try again.", style="bold red" "Timeout: Failed to get the token. Please try again.", style="bold red"
) )
def _validate_and_save_token(self, token_data: Dict[str, Any]) -> None:
"""Validates the JWT token and saves the token to the token manager."""
jwt_token = token_data["access_token"]
jwt_token_data = {
"jwt_token": jwt_token,
"jwks_url": f"https://{WORKOS_DOMAIN}/oauth2/jwks",
"issuer": f"https://{WORKOS_DOMAIN}",
"audience": WORKOS_ENVIRONMENT_ID,
}
# TODO: WORKOS - The following conditional is temporary until migration to WorkOS is complete.
if self.user_provider == "auth0":
jwt_token_data["jwks_url"] = f"https://{AUTH0_DOMAIN}/.well-known/jwks.json"
jwt_token_data["issuer"] = f"https://{AUTH0_DOMAIN}/"
jwt_token_data["audience"] = AUTH0_AUDIENCE
decoded_token = validate_jwt_token(**jwt_token_data)
expires_at = decoded_token.get("exp", 0)
self.token_manager.save_tokens(jwt_token, expires_at)
def _login_to_tool_repository(self) -> None:
"""Login to the tool repository."""
from crewai.cli.tools.main import ToolCommand
try:
console.print(
"Now logging you in to the Tool Repository... ",
style="bold blue",
end="",
)
ToolCommand().login()
console.print(
"Success!\n",
style="bold green",
)
settings = Settings()
console.print(
f"You are authenticated to the tool repository as [bold cyan]'{settings.org_name}'[/bold cyan] ({settings.org_uuid})",
style="green",
)
except Exception:
console.print(
"\n[bold yellow]Warning:[/bold yellow] Authentication with the Tool Repository failed.",
style="yellow",
)
console.print(
"Other features will work normally, but you may experience limitations "
"with downloading and publishing tools."
"\nRun [bold]crewai login[/bold] to try logging in again.\n",
style="yellow",
)
# TODO: WORKOS - This method is temporary until migration to WorkOS is complete.
def _determine_user_provider(self) -> str:
"""Determine which provider to use for authentication."""
console.print(
"Enter your CrewAI Enterprise account email: ", style="bold blue", end=""
)
email = input()
email_encoded = quote(email)
# It's not correct to call this method directly, but it's temporary until migration is complete.
response = PlusAPI("")._make_request(
"GET", f"/crewai_plus/api/v1/me/provider?email={email_encoded}"
)
if response.status_code == 200:
if response.json().get("provider") == "auth0":
return "auth0"
else:
return "workos"
else:
console.print(
"Error: Failed to authenticate with crewai enterprise. Ensure that you are using the latest crewai version and please try again. If the problem persists, contact support@crewai.com.",
style="red",
)
raise SystemExit

View File

@@ -1,32 +1,63 @@
import json import json
import os import os
import sys import sys
from datetime import datetime, timedelta from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
import jwt
from auth0.authentication.token_verifier import ( from jwt import PyJWKClient
AsymmetricSignatureVerifier,
TokenVerifier,
)
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from .constants import AUTH0_CLIENT_ID, AUTH0_DOMAIN
def validate_jwt_token(
def validate_token(id_token: str) -> None: jwt_token: str, jwks_url: str, issuer: str, audience: str
) -> dict:
""" """
Verify the token and its precedence Verify the token's signature and claims using PyJWT.
:param jwt_token: The JWT (JWS) string to validate.
:param id_token: :param jwks_url: The URL of the JWKS endpoint.
:param issuer: The expected issuer of the token.
:param audience: The expected audience of the token.
:return: The decoded token.
:raises Exception: If the token is invalid for any reason (e.g., signature mismatch,
expired, incorrect issuer/audience, JWKS fetching error,
missing required claims).
""" """
jwks_url = f"https://{AUTH0_DOMAIN}/.well-known/jwks.json"
issuer = f"https://{AUTH0_DOMAIN}/" decoded_token = None
signature_verifier = AsymmetricSignatureVerifier(jwks_url)
token_verifier = TokenVerifier( try:
signature_verifier=signature_verifier, issuer=issuer, audience=AUTH0_CLIENT_ID jwk_client = PyJWKClient(jwks_url)
) signing_key = jwk_client.get_signing_key_from_jwt(jwt_token)
token_verifier.verify(id_token)
decoded_token = jwt.decode(
jwt_token,
signing_key.key,
algorithms=["RS256"],
audience=audience,
issuer=issuer,
options={
"verify_signature": True,
"verify_exp": True,
"verify_nbf": True,
"verify_iat": True,
"require": ["exp", "iat", "iss", "aud", "sub"],
},
)
return decoded_token
except jwt.ExpiredSignatureError:
raise Exception("Token has expired.")
except jwt.InvalidAudienceError:
raise Exception(f"Invalid token audience. Expected: '{audience}'")
except jwt.InvalidIssuerError:
raise Exception(f"Invalid token issuer. Expected: '{issuer}'")
except jwt.MissingRequiredClaimError as e:
raise Exception(f"Token is missing required claims: {str(e)}")
except jwt.exceptions.PyJWKClientError as e:
raise Exception(f"JWKS or key processing error: {str(e)}")
except jwt.InvalidTokenError as e:
raise Exception(f"Invalid token: {str(e)}")
class TokenManager: class TokenManager:
@@ -56,14 +87,14 @@ class TokenManager:
self.save_secure_file(key_filename, new_key) self.save_secure_file(key_filename, new_key)
return new_key return new_key
def save_tokens(self, access_token: str, expires_in: int) -> None: def save_tokens(self, access_token: str, expires_at: int) -> None:
""" """
Save the access token and its expiration time. Save the access token and its expiration time.
:param access_token: The access token to save. :param access_token: The access token to save.
:param expires_in: The expiration time of the access token in seconds. :param expires_at: The UNIX timestamp of the expiration time.
""" """
expiration_time = datetime.now() + timedelta(seconds=expires_in) expiration_time = datetime.fromtimestamp(expires_at)
data = { data = {
"access_token": access_token, "access_token": access_token,
"expiration": expiration_time.isoformat(), "expiration": expiration_time.isoformat(),

View File

@@ -2,7 +2,7 @@ from importlib.metadata import version as get_version
from typing import Optional from typing import Optional
import click import click
from crewai.cli.config import Settings
from crewai.cli.add_crew_to_flow import add_crew_to_flow from crewai.cli.add_crew_to_flow import add_crew_to_flow
from crewai.cli.create_crew import create_crew from crewai.cli.create_crew import create_crew
from crewai.cli.create_flow import create_flow from crewai.cli.create_flow import create_flow
@@ -138,8 +138,12 @@ def log_tasks_outputs() -> None:
@click.option("-s", "--short", is_flag=True, help="Reset SHORT TERM memory") @click.option("-s", "--short", is_flag=True, help="Reset SHORT TERM memory")
@click.option("-e", "--entities", is_flag=True, help="Reset ENTITIES memory") @click.option("-e", "--entities", is_flag=True, help="Reset ENTITIES memory")
@click.option("-kn", "--knowledge", is_flag=True, help="Reset KNOWLEDGE storage") @click.option("-kn", "--knowledge", is_flag=True, help="Reset KNOWLEDGE storage")
@click.option("-akn", "--agent-knowledge", is_flag=True, help="Reset AGENT KNOWLEDGE storage") @click.option(
@click.option("-k","--kickoff-outputs",is_flag=True,help="Reset LATEST KICKOFF TASK OUTPUTS") "-akn", "--agent-knowledge", is_flag=True, help="Reset AGENT KNOWLEDGE storage"
)
@click.option(
"-k", "--kickoff-outputs", is_flag=True, help="Reset LATEST KICKOFF TASK OUTPUTS"
)
@click.option("-a", "--all", is_flag=True, help="Reset ALL memories") @click.option("-a", "--all", is_flag=True, help="Reset ALL memories")
def reset_memories( def reset_memories(
long: bool, long: bool,
@@ -154,13 +158,23 @@ def reset_memories(
Reset the crew memories (long, short, entity, latest_crew_kickoff_ouputs, knowledge, agent_knowledge). This will delete all the data saved. Reset the crew memories (long, short, entity, latest_crew_kickoff_ouputs, knowledge, agent_knowledge). This will delete all the data saved.
""" """
try: try:
memory_types = [long, short, entities, knowledge, agent_knowledge, kickoff_outputs, all] memory_types = [
long,
short,
entities,
knowledge,
agent_knowledge,
kickoff_outputs,
all,
]
if not any(memory_types): if not any(memory_types):
click.echo( click.echo(
"Please specify at least one memory type to reset using the appropriate flags." "Please specify at least one memory type to reset using the appropriate flags."
) )
return return
reset_memories_command(long, short, entities, knowledge, agent_knowledge, kickoff_outputs, all) reset_memories_command(
long, short, entities, knowledge, agent_knowledge, kickoff_outputs, all
)
except Exception as e: except Exception as e:
click.echo(f"An error occurred while resetting memories: {e}", err=True) click.echo(f"An error occurred while resetting memories: {e}", err=True)
@@ -210,16 +224,11 @@ def update():
update_crew() update_crew()
@crewai.command()
def signup():
"""Sign Up/Login to CrewAI+."""
AuthenticationCommand().signup()
@crewai.command() @crewai.command()
def login(): def login():
"""Sign Up/Login to CrewAI+.""" """Sign Up/Login to CrewAI Enterprise."""
AuthenticationCommand().signup() Settings().clear()
AuthenticationCommand().login()
# DEPLOY CREWAI+ COMMANDS # DEPLOY CREWAI+ COMMANDS

View File

@@ -37,6 +37,10 @@ class Settings(BaseModel):
merged_data = {**file_data, **data} merged_data = {**file_data, **data}
super().__init__(config_path=config_path, **merged_data) super().__init__(config_path=config_path, **merged_data)
def clear(self) -> None:
"""Clear all settings"""
self.config_path.unlink(missing_ok=True)
def dump(self) -> None: def dump(self) -> None:
"""Save current settings to settings.json""" """Save current settings to settings.json"""
if self.config_path.is_file(): if self.config_path.is_file():

View File

@@ -156,7 +156,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
console.print(f"Successfully installed {handle}", style="bold green") console.print(f"Successfully installed {handle}", style="bold green")
def login(self): def login(self) -> None:
login_response = self.plus_api_client.login_to_tool_repository() login_response = self.plus_api_client.login_to_tool_repository()
if login_response.status_code != 200: if login_response.status_code != 200:
@@ -175,18 +175,10 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
settings.tool_repository_password = login_response_json["credential"][ settings.tool_repository_password = login_response_json["credential"][
"password" "password"
] ]
settings.org_uuid = login_response_json["current_organization"][ settings.org_uuid = login_response_json["current_organization"]["uuid"]
"uuid" settings.org_name = login_response_json["current_organization"]["name"]
]
settings.org_name = login_response_json["current_organization"][
"name"
]
settings.dump() settings.dump()
console.print(
f"Successfully authenticated to the tool repository as {settings.org_name} ({settings.org_uuid}).", style="bold green"
)
def _add_package(self, tool_details: dict[str, Any]): def _add_package(self, tool_details: dict[str, Any]):
is_from_pypi = tool_details.get("source", None) == "pypi" is_from_pypi = tool_details.get("source", None) == "pypi"
tool_handle = tool_details["handle"] tool_handle = tool_details["handle"]
@@ -243,9 +235,15 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
return env return env
def _print_current_organization(self): def _print_current_organization(self) -> None:
settings = Settings() settings = Settings()
if settings.org_uuid: if settings.org_uuid:
console.print(f"Current organization: {settings.org_name} ({settings.org_uuid})", style="bold blue") console.print(
f"Current organization: {settings.org_name} ({settings.org_uuid})",
style="bold blue",
)
else: else:
console.print("No organization currently set. We recommend setting one before using: `crewai org switch <org_id>` command.", style="yellow") console.print(
"No organization currently set. We recommend setting one before using: `crewai org switch <org_id>` command.",
style="yellow",
)

View File

@@ -1,103 +1,419 @@
import unittest import pytest
from unittest.mock import MagicMock, patch from datetime import datetime, timedelta
import requests import requests
from unittest.mock import MagicMock, patch, call
from crewai.cli.authentication.main import AuthenticationCommand from crewai.cli.authentication.main import AuthenticationCommand
from crewai.cli.authentication.constants import (
AUTH0_AUDIENCE,
AUTH0_CLIENT_ID,
AUTH0_DOMAIN,
WORKOS_DOMAIN,
WORKOS_CLI_CONNECT_APP_ID,
WORKOS_ENVIRONMENT_ID,
)
class TestAuthenticationCommand(unittest.TestCase): class TestAuthenticationCommand:
def setUp(self): def setup_method(self):
self.auth_command = AuthenticationCommand() self.auth_command = AuthenticationCommand()
@patch("crewai.cli.authentication.main.requests.post") @pytest.mark.parametrize(
def test_get_device_code(self, mock_post): "user_provider,expected_urls",
mock_response = MagicMock() [
mock_response.json.return_value = { (
"device_code": "123456", "auth0",
"user_code": "ABCDEF", {
"verification_uri_complete": "https://example.com", "device_code_url": f"https://{AUTH0_DOMAIN}/oauth/device/code",
"interval": 5, "token_url": f"https://{AUTH0_DOMAIN}/oauth/token",
} "client_id": AUTH0_CLIENT_ID,
mock_post.return_value = mock_response "audience": AUTH0_AUDIENCE,
},
device_code_data = self.auth_command._get_device_code() ),
(
self.assertEqual(device_code_data["device_code"], "123456") "workos",
self.assertEqual(device_code_data["user_code"], "ABCDEF") {
self.assertEqual( "device_code_url": f"https://{WORKOS_DOMAIN}/oauth2/device_authorization",
device_code_data["verification_uri_complete"], "https://example.com" "token_url": f"https://{WORKOS_DOMAIN}/oauth2/token",
) "client_id": WORKOS_CLI_CONNECT_APP_ID,
self.assertEqual(device_code_data["interval"], 5) },
),
],
)
@patch(
"crewai.cli.authentication.main.AuthenticationCommand._determine_user_provider"
)
@patch("crewai.cli.authentication.main.AuthenticationCommand._get_device_code")
@patch(
"crewai.cli.authentication.main.AuthenticationCommand._display_auth_instructions"
)
@patch("crewai.cli.authentication.main.AuthenticationCommand._poll_for_token")
@patch("crewai.cli.authentication.main.console.print") @patch("crewai.cli.authentication.main.console.print")
@patch("crewai.cli.authentication.main.webbrowser.open") def test_login(
def test_display_auth_instructions(self, mock_open, mock_print): self,
mock_console_print,
mock_poll,
mock_display,
mock_get_device,
mock_determine_provider,
user_provider,
expected_urls,
):
mock_determine_provider.return_value = user_provider
mock_get_device.return_value = {
"device_code": "test_code",
"user_code": "123456",
}
self.auth_command.login()
mock_console_print.assert_called_once_with(
"Signing in to CrewAI Enterprise...\n", style="bold blue"
)
mock_determine_provider.assert_called_once()
mock_get_device.assert_called_once_with(
expected_urls["client_id"],
expected_urls["device_code_url"],
expected_urls.get("audience", None),
)
mock_display.assert_called_once_with(
{"device_code": "test_code", "user_code": "123456"}
)
mock_poll.assert_called_once_with(
{"device_code": "test_code", "user_code": "123456"},
expected_urls["client_id"],
expected_urls["token_url"],
)
@patch("crewai.cli.authentication.main.webbrowser")
@patch("crewai.cli.authentication.main.console.print")
def test_display_auth_instructions(self, mock_console_print, mock_webbrowser):
device_code_data = { device_code_data = {
"verification_uri_complete": "https://example.com", "verification_uri_complete": "https://example.com/auth",
"user_code": "ABCDEF", "user_code": "123456",
} }
self.auth_command._display_auth_instructions(device_code_data) self.auth_command._display_auth_instructions(device_code_data)
mock_print.assert_any_call("1. Navigate to: ", "https://example.com") expected_calls = [
mock_print.assert_any_call("2. Enter the following code: ", "ABCDEF") call("1. Navigate to: ", "https://example.com/auth"),
mock_open.assert_called_once_with("https://example.com") call("2. Enter the following code: ", "123456"),
]
mock_console_print.assert_has_calls(expected_calls)
mock_webbrowser.open.assert_called_once_with("https://example.com/auth")
@pytest.mark.parametrize(
"user_provider,jwt_config",
[
(
"auth0",
{
"jwks_url": f"https://{AUTH0_DOMAIN}/.well-known/jwks.json",
"issuer": f"https://{AUTH0_DOMAIN}/",
"audience": AUTH0_AUDIENCE,
},
),
(
"workos",
{
"jwks_url": f"https://{WORKOS_DOMAIN}/oauth2/jwks",
"issuer": f"https://{WORKOS_DOMAIN}",
"audience": WORKOS_ENVIRONMENT_ID,
},
),
],
)
@pytest.mark.parametrize("has_expiration", [True, False])
@patch("crewai.cli.authentication.main.validate_jwt_token")
@patch("crewai.cli.authentication.main.TokenManager.save_tokens")
def test_validate_and_save_token(
self,
mock_save_tokens,
mock_validate_jwt,
user_provider,
jwt_config,
has_expiration,
):
self.auth_command.user_provider = user_provider
token_data = {"access_token": "test_access_token", "id_token": "test_id_token"}
if has_expiration:
future_timestamp = int((datetime.now() + timedelta(days=100)).timestamp())
decoded_token = {"exp": future_timestamp}
else:
decoded_token = {}
mock_validate_jwt.return_value = decoded_token
self.auth_command._validate_and_save_token(token_data)
mock_validate_jwt.assert_called_once_with(
jwt_token="test_access_token",
jwks_url=jwt_config["jwks_url"],
issuer=jwt_config["issuer"],
audience=jwt_config["audience"],
)
if has_expiration:
mock_save_tokens.assert_called_once_with(
"test_access_token", future_timestamp
)
else:
mock_save_tokens.assert_called_once_with("test_access_token", 0)
@patch("crewai.cli.tools.main.ToolCommand") @patch("crewai.cli.tools.main.ToolCommand")
@patch("crewai.cli.authentication.main.requests.post") @patch("crewai.cli.authentication.main.Settings")
@patch("crewai.cli.authentication.main.validate_token")
@patch("crewai.cli.authentication.main.console.print") @patch("crewai.cli.authentication.main.console.print")
def test_poll_for_token_success( def test_login_to_tool_repository_success(
self, mock_print, mock_validate_token, mock_post, mock_tool self, mock_console_print, mock_settings, mock_tool_command
): ):
mock_tool_instance = MagicMock()
mock_tool_command.return_value = mock_tool_instance
mock_settings_instance = MagicMock()
mock_settings_instance.org_name = "Test Org"
mock_settings_instance.org_uuid = "test-uuid-123"
mock_settings.return_value = mock_settings_instance
self.auth_command._login_to_tool_repository()
mock_tool_command.assert_called_once()
mock_tool_instance.login.assert_called_once()
expected_calls = [
call(
"Now logging you in to the Tool Repository... ",
style="bold blue",
end="",
),
call("Success!\n", style="bold green"),
call(
"You are authenticated to the tool repository as [bold cyan]'Test Org'[/bold cyan] (test-uuid-123)",
style="green",
),
]
mock_console_print.assert_has_calls(expected_calls)
@patch("crewai.cli.tools.main.ToolCommand")
@patch("crewai.cli.authentication.main.console.print")
def test_login_to_tool_repository_error(
self, mock_console_print, mock_tool_command
):
mock_tool_instance = MagicMock()
mock_tool_instance.login.side_effect = Exception("Tool repository error")
mock_tool_command.return_value = mock_tool_instance
self.auth_command._login_to_tool_repository()
mock_tool_command.assert_called_once()
mock_tool_instance.login.assert_called_once()
expected_calls = [
call(
"Now logging you in to the Tool Repository... ",
style="bold blue",
end="",
),
call(
"\n[bold yellow]Warning:[/bold yellow] Authentication with the Tool Repository failed.",
style="yellow",
),
call(
"Other features will work normally, but you may experience limitations with downloading and publishing tools.\nRun [bold]crewai login[/bold] to try logging in again.\n",
style="yellow",
),
]
mock_console_print.assert_has_calls(expected_calls)
@pytest.mark.parametrize(
"api_response,expected_provider",
[
({"provider": "auth0"}, "auth0"),
({"provider": "workos"}, "workos"),
({"provider": "none"}, "workos"), # Default to workos for any other value
(
{},
"workos",
), # Default to workos if no provider key is sent in the response
],
)
@patch("crewai.cli.authentication.main.PlusAPI")
@patch("crewai.cli.authentication.main.console.print")
@patch("builtins.input", return_value="test@example.com")
def test_determine_user_provider_success(
self,
mock_input,
mock_console_print,
mock_plus_api,
api_response,
expected_provider,
):
mock_api_instance = MagicMock()
mock_response = MagicMock() mock_response = MagicMock()
mock_response.status_code = 200 mock_response.status_code = 200
mock_response.json.return_value = { mock_response.json.return_value = api_response
"id_token": "TOKEN", mock_api_instance._make_request.return_value = mock_response
"access_token": "ACCESS_TOKEN", mock_plus_api.return_value = mock_api_instance
}
mock_post.return_value = mock_response
mock_instance = mock_tool.return_value result = self.auth_command._determine_user_provider()
mock_instance.login.return_value = None
self.auth_command._poll_for_token({"device_code": "123456"}) mock_input.assert_called_once()
mock_validate_token.assert_called_once_with("TOKEN") mock_plus_api.assert_called_once_with("")
mock_print.assert_called_once_with( mock_api_instance._make_request.assert_called_once_with(
"\n[bold green]Welcome to CrewAI Enterprise![/bold green]\n" "GET", "/crewai_plus/api/v1/me/provider?email=test%40example.com"
) )
@patch("crewai.cli.authentication.main.requests.post") assert result == expected_provider
@patch("crewai.cli.authentication.main.PlusAPI")
@patch("crewai.cli.authentication.main.console.print") @patch("crewai.cli.authentication.main.console.print")
def test_poll_for_token_error(self, mock_print, mock_post): @patch("builtins.input", return_value="test@example.com")
def test_determine_user_provider_error(
self, mock_input, mock_console_print, mock_plus_api
):
mock_api_instance = MagicMock()
mock_response = MagicMock()
mock_response.status_code = 500
mock_api_instance._make_request.return_value = mock_response
mock_plus_api.return_value = mock_api_instance
with pytest.raises(SystemExit):
self.auth_command._determine_user_provider()
mock_input.assert_called_once()
mock_plus_api.assert_called_once_with("")
mock_api_instance._make_request.assert_called_once_with(
"GET", "/crewai_plus/api/v1/me/provider?email=test%40example.com"
)
mock_console_print.assert_has_calls(
[
call(
"Enter your CrewAI Enterprise account email: ",
style="bold blue",
end="",
),
call(
"Error: Failed to authenticate with crewai enterprise. Ensure that you are using the latest crewai version and please try again. If the problem persists, contact support@crewai.com.",
style="red",
),
]
)
@patch("requests.post")
def test_get_device_code(self, mock_post):
mock_response = MagicMock() mock_response = MagicMock()
mock_response.status_code = 400
mock_response.json.return_value = { mock_response.json.return_value = {
"error": "invalid_request", "device_code": "test_device_code",
"error_description": "Invalid request", "user_code": "123456",
"verification_uri_complete": "https://example.com/auth",
} }
mock_post.return_value = mock_response mock_post.return_value = mock_response
with self.assertRaises(requests.HTTPError): result = self.auth_command._get_device_code(
self.auth_command._poll_for_token({"device_code": "123456"}) client_id="test_client",
device_code_url="https://example.com/device",
audience="test_audience",
)
mock_print.assert_not_called() mock_post.assert_called_once_with(
url="https://example.com/device",
data={
"client_id": "test_client",
"scope": "openid",
"audience": "test_audience",
},
timeout=20,
)
@patch("crewai.cli.authentication.main.requests.post") assert result == {
@patch("crewai.cli.authentication.main.console.print") "device_code": "test_device_code",
def test_poll_for_token_timeout(self, mock_print, mock_post): "user_code": "123456",
mock_response = MagicMock() "verification_uri_complete": "https://example.com/auth",
mock_response.status_code = 400
mock_response.json.return_value = {
"error": "authorization_pending",
"error_description": "Authorization pending",
} }
mock_post.return_value = mock_response
self.auth_command._poll_for_token({"device_code": "123456", "interval": 0.01}) @patch("requests.post")
@patch("crewai.cli.authentication.main.console.print")
def test_poll_for_token_success(self, mock_console_print, mock_post):
mock_response_success = MagicMock()
mock_response_success.status_code = 200
mock_response_success.json.return_value = {
"access_token": "test_access_token",
"id_token": "test_id_token",
}
mock_post.return_value = mock_response_success
mock_print.assert_called_once_with( device_code_data = {"device_code": "test_device_code", "interval": 1}
with (
patch.object(
self.auth_command, "_validate_and_save_token"
) as mock_validate,
patch.object(
self.auth_command, "_login_to_tool_repository"
) as mock_tool_login,
):
self.auth_command._poll_for_token(
device_code_data, "test_client", "https://example.com/token"
)
mock_post.assert_called_once_with(
"https://example.com/token",
data={
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"device_code": "test_device_code",
"client_id": "test_client",
},
timeout=30,
)
mock_validate.assert_called_once()
mock_tool_login.assert_called_once()
expected_calls = [
call("\nWaiting for authentication... ", style="bold blue", end=""),
call("Success!", style="bold green"),
call("\n[bold green]Welcome to CrewAI Enterprise![/bold green]\n"),
]
mock_console_print.assert_has_calls(expected_calls)
@patch("requests.post")
@patch("crewai.cli.authentication.main.console.print")
def test_poll_for_token_timeout(self, mock_console_print, mock_post):
mock_response_pending = MagicMock()
mock_response_pending.status_code = 400
mock_response_pending.json.return_value = {"error": "authorization_pending"}
mock_post.return_value = mock_response_pending
device_code_data = {
"device_code": "test_device_code",
"interval": 0.1, # Short interval for testing
}
self.auth_command._poll_for_token(
device_code_data, "test_client", "https://example.com/token"
)
mock_console_print.assert_any_call(
"Timeout: Failed to get the token. Please try again.", style="bold red" "Timeout: Failed to get the token. Please try again.", style="bold red"
) )
@patch("requests.post")
def test_poll_for_token_error(self, mock_post):
"""Test the method to poll for token (error path)."""
# Setup mock to return error
mock_response_error = MagicMock()
mock_response_error.status_code = 400
mock_response_error.json.return_value = {
"error": "access_denied",
"error_description": "User denied access",
}
mock_post.return_value = mock_response_error
device_code_data = {"device_code": "test_device_code", "interval": 1}
with pytest.raises(requests.HTTPError):
self.auth_command._poll_for_token(
device_code_data, "test_client", "https://example.com/token"
)

View File

@@ -1,31 +1,110 @@
import json import json
import jwt
import unittest import unittest
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from crewai.cli.authentication.utils import TokenManager, validate_token from crewai.cli.authentication.utils import TokenManager, validate_jwt_token
@patch("crewai.cli.authentication.utils.PyJWKClient", return_value=MagicMock())
@patch("crewai.cli.authentication.utils.jwt")
class TestValidateToken(unittest.TestCase): class TestValidateToken(unittest.TestCase):
@patch("crewai.cli.authentication.utils.AsymmetricSignatureVerifier") def test_validate_jwt_token(self, mock_jwt, mock_pyjwkclient):
@patch("crewai.cli.authentication.utils.TokenVerifier") mock_jwt.decode.return_value = {"exp": 1719859200}
def test_validate_token(self, mock_token_verifier, mock_asymmetric_verifier):
mock_verifier_instance = mock_token_verifier.return_value
mock_id_token = "mock_id_token"
validate_token(mock_id_token) # Create signing key object mock with a .key attribute
mock_pyjwkclient.return_value.get_signing_key_from_jwt.return_value = MagicMock(
key="mock_signing_key"
)
mock_asymmetric_verifier.assert_called_once_with( decoded_token = validate_jwt_token(
"https://crewai.us.auth0.com/.well-known/jwks.json" jwt_token="aaaaa.bbbbbb.cccccc",
jwks_url="https://mock_jwks_url",
issuer="https://mock_issuer",
audience="app_id_xxxx",
) )
mock_token_verifier.assert_called_once_with(
signature_verifier=mock_asymmetric_verifier.return_value, mock_jwt.decode.assert_called_once_with(
issuer="https://crewai.us.auth0.com/", "aaaaa.bbbbbb.cccccc",
audience="DEVC5Fw6NlRoSzmDCcOhVq85EfLBjKa8", "mock_signing_key",
algorithms=["RS256"],
audience="app_id_xxxx",
issuer="https://mock_issuer",
options={
"verify_signature": True,
"verify_exp": True,
"verify_nbf": True,
"verify_iat": True,
"require": ["exp", "iat", "iss", "aud", "sub"],
},
) )
mock_verifier_instance.verify.assert_called_once_with(mock_id_token) mock_pyjwkclient.assert_called_once_with("https://mock_jwks_url")
self.assertEqual(decoded_token, {"exp": 1719859200})
def test_validate_jwt_token_expired(self, mock_jwt, mock_pyjwkclient):
mock_jwt.decode.side_effect = jwt.ExpiredSignatureError
with self.assertRaises(Exception):
validate_jwt_token(
jwt_token="aaaaa.bbbbbb.cccccc",
jwks_url="https://mock_jwks_url",
issuer="https://mock_issuer",
audience="app_id_xxxx",
)
def test_validate_jwt_token_invalid_audience(self, mock_jwt, mock_pyjwkclient):
mock_jwt.decode.side_effect = jwt.InvalidAudienceError
with self.assertRaises(Exception):
validate_jwt_token(
jwt_token="aaaaa.bbbbbb.cccccc",
jwks_url="https://mock_jwks_url",
issuer="https://mock_issuer",
audience="app_id_xxxx",
)
def test_validate_jwt_token_invalid_issuer(self, mock_jwt, mock_pyjwkclient):
mock_jwt.decode.side_effect = jwt.InvalidIssuerError
with self.assertRaises(Exception):
validate_jwt_token(
jwt_token="aaaaa.bbbbbb.cccccc",
jwks_url="https://mock_jwks_url",
issuer="https://mock_issuer",
audience="app_id_xxxx",
)
def test_validate_jwt_token_missing_required_claims(
self, mock_jwt, mock_pyjwkclient
):
mock_jwt.decode.side_effect = jwt.MissingRequiredClaimError
with self.assertRaises(Exception):
validate_jwt_token(
jwt_token="aaaaa.bbbbbb.cccccc",
jwks_url="https://mock_jwks_url",
issuer="https://mock_issuer",
audience="app_id_xxxx",
)
def test_validate_jwt_token_jwks_error(self, mock_jwt, mock_pyjwkclient):
mock_jwt.decode.side_effect = jwt.exceptions.PyJWKClientError
with self.assertRaises(Exception):
validate_jwt_token(
jwt_token="aaaaa.bbbbbb.cccccc",
jwks_url="https://mock_jwks_url",
issuer="https://mock_issuer",
audience="app_id_xxxx",
)
def test_validate_jwt_token_invalid_token(self, mock_jwt, mock_pyjwkclient):
mock_jwt.decode.side_effect = jwt.InvalidTokenError
with self.assertRaises(Exception):
validate_jwt_token(
jwt_token="aaaaa.bbbbbb.cccccc",
jwks_url="https://mock_jwks_url",
issuer="https://mock_issuer",
audience="app_id_xxxx",
)
class TestTokenManager(unittest.TestCase): class TestTokenManager(unittest.TestCase):
@@ -62,9 +141,9 @@ class TestTokenManager(unittest.TestCase):
@patch("crewai.cli.authentication.utils.TokenManager.save_secure_file") @patch("crewai.cli.authentication.utils.TokenManager.save_secure_file")
def test_save_tokens(self, mock_save): def test_save_tokens(self, mock_save):
access_token = "test_token" access_token = "test_token"
expires_in = 3600 expires_at = int((datetime.now() + timedelta(seconds=3600)).timestamp())
self.token_manager.save_tokens(access_token, expires_in) self.token_manager.save_tokens(access_token, expires_at)
mock_save.assert_called_once() mock_save.assert_called_once()
args = mock_save.call_args[0] args = mock_save.call_args[0]
@@ -73,11 +152,7 @@ class TestTokenManager(unittest.TestCase):
data = json.loads(decrypted_data) data = json.loads(decrypted_data)
self.assertEqual(data["access_token"], access_token) self.assertEqual(data["access_token"], access_token)
expiration = datetime.fromisoformat(data["expiration"]) expiration = datetime.fromisoformat(data["expiration"])
self.assertAlmostEqual( self.assertEqual(expiration, datetime.fromtimestamp(expires_at))
expiration,
datetime.now() + timedelta(seconds=expires_in),
delta=timedelta(seconds=1),
)
@patch("crewai.cli.authentication.utils.TokenManager.read_secure_file") @patch("crewai.cli.authentication.utils.TokenManager.read_secure_file")
def test_get_token_valid(self, mock_read): def test_get_token_valid(self, mock_read):

View File

@@ -13,7 +13,7 @@ from crewai.cli.cli import (
deply_status, deply_status,
flow_add_crew, flow_add_crew,
reset_memories, reset_memories,
signup, login,
test, test,
train, train,
version, version,
@@ -261,12 +261,12 @@ def test_test_invalid_string_iterations(evaluate_crew, runner):
@mock.patch("crewai.cli.cli.AuthenticationCommand") @mock.patch("crewai.cli.cli.AuthenticationCommand")
def test_signup(command, runner): def test_login(command, runner):
mock_auth = command.return_value mock_auth = command.return_value
result = runner.invoke(signup) result = runner.invoke(login)
assert result.exit_code == 0 assert result.exit_code == 0
mock_auth.signup.assert_called_once() mock_auth.login.assert_called_once()
@mock.patch("crewai.cli.cli.DeployCommand") @mock.patch("crewai.cli.cli.DeployCommand")

View File

@@ -2,6 +2,7 @@ import os
import tempfile import tempfile
import unittest import unittest
import unittest.mock import unittest.mock
from datetime import datetime, timedelta
from contextlib import contextmanager from contextlib import contextmanager
from unittest import mock from unittest import mock
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@@ -26,7 +27,9 @@ def in_temp_dir():
@pytest.fixture @pytest.fixture
def tool_command(): def tool_command():
TokenManager().save_tokens("test-token", 36000) TokenManager().save_tokens(
"test-token", (datetime.now() + timedelta(seconds=36000)).timestamp()
)
tool_command = ToolCommand() tool_command = ToolCommand()
with patch.object(tool_command, "login"): with patch.object(tool_command, "login"):
yield tool_command yield tool_command
@@ -57,7 +60,9 @@ def test_create_success(mock_subprocess, capsys, tool_command):
@patch("crewai.cli.tools.main.subprocess.run") @patch("crewai.cli.tools.main.subprocess.run")
@patch("crewai.cli.plus_api.PlusAPI.get_tool") @patch("crewai.cli.plus_api.PlusAPI.get_tool")
@patch("crewai.cli.tools.main.ToolCommand._print_current_organization") @patch("crewai.cli.tools.main.ToolCommand._print_current_organization")
def test_install_success(mock_print_org, mock_get, mock_subprocess_run, capsys, tool_command): def test_install_success(
mock_print_org, mock_get, mock_subprocess_run, capsys, tool_command
):
mock_get_response = MagicMock() mock_get_response = MagicMock()
mock_get_response.status_code = 200 mock_get_response.status_code = 200
mock_get_response.json.return_value = { mock_get_response.json.return_value = {
@@ -89,6 +94,7 @@ def test_install_success(mock_print_org, mock_get, mock_subprocess_run, capsys,
# Verify _print_current_organization was called # Verify _print_current_organization was called
mock_print_org.assert_called_once() mock_print_org.assert_called_once()
@patch("crewai.cli.tools.main.subprocess.run") @patch("crewai.cli.tools.main.subprocess.run")
@patch("crewai.cli.plus_api.PlusAPI.get_tool") @patch("crewai.cli.plus_api.PlusAPI.get_tool")
def test_install_success_from_pypi(mock_get, mock_subprocess_run, capsys, tool_command): def test_install_success_from_pypi(mock_get, mock_subprocess_run, capsys, tool_command):
@@ -169,7 +175,10 @@ def test_publish_when_not_in_sync(mock_is_synced, capsys, tool_command):
) )
@patch("crewai.cli.plus_api.PlusAPI.publish_tool") @patch("crewai.cli.plus_api.PlusAPI.publish_tool")
@patch("crewai.cli.tools.main.git.Repository.is_synced", return_value=False) @patch("crewai.cli.tools.main.git.Repository.is_synced", return_value=False)
@patch("crewai.cli.tools.main.extract_available_exports", return_value=[{"name": "SampleTool"}]) @patch(
"crewai.cli.tools.main.extract_available_exports",
return_value=[{"name": "SampleTool"}],
)
@patch("crewai.cli.tools.main.ToolCommand._print_current_organization") @patch("crewai.cli.tools.main.ToolCommand._print_current_organization")
def test_publish_when_not_in_sync_and_force( def test_publish_when_not_in_sync_and_force(
mock_print_org, mock_print_org,
@@ -223,7 +232,10 @@ def test_publish_when_not_in_sync_and_force(
) )
@patch("crewai.cli.plus_api.PlusAPI.publish_tool") @patch("crewai.cli.plus_api.PlusAPI.publish_tool")
@patch("crewai.cli.tools.main.git.Repository.is_synced", return_value=True) @patch("crewai.cli.tools.main.git.Repository.is_synced", return_value=True)
@patch("crewai.cli.tools.main.extract_available_exports", return_value=[{"name": "SampleTool"}]) @patch(
"crewai.cli.tools.main.extract_available_exports",
return_value=[{"name": "SampleTool"}],
)
def test_publish_success( def test_publish_success(
mock_available_exports, mock_available_exports,
mock_is_synced, mock_is_synced,
@@ -273,7 +285,10 @@ def test_publish_success(
read_data=b"sample tarball content", read_data=b"sample tarball content",
) )
@patch("crewai.cli.plus_api.PlusAPI.publish_tool") @patch("crewai.cli.plus_api.PlusAPI.publish_tool")
@patch("crewai.cli.tools.main.extract_available_exports", return_value=[{"name": "SampleTool"}]) @patch(
"crewai.cli.tools.main.extract_available_exports",
return_value=[{"name": "SampleTool"}],
)
def test_publish_failure( def test_publish_failure(
mock_available_exports, mock_available_exports,
mock_publish, mock_publish,
@@ -311,7 +326,10 @@ def test_publish_failure(
read_data=b"sample tarball content", read_data=b"sample tarball content",
) )
@patch("crewai.cli.plus_api.PlusAPI.publish_tool") @patch("crewai.cli.plus_api.PlusAPI.publish_tool")
@patch("crewai.cli.tools.main.extract_available_exports", return_value=[{"name": "SampleTool"}]) @patch(
"crewai.cli.tools.main.extract_available_exports",
return_value=[{"name": "SampleTool"}],
)
def test_publish_api_error( def test_publish_api_error(
mock_available_exports, mock_available_exports,
mock_publish, mock_publish,
@@ -338,7 +356,6 @@ def test_publish_api_error(
mock_publish.assert_called_once() mock_publish.assert_called_once()
@patch("crewai.cli.tools.main.Settings") @patch("crewai.cli.tools.main.Settings")
def test_print_current_organization_with_org(mock_settings, capsys, tool_command): def test_print_current_organization_with_org(mock_settings, capsys, tool_command):
mock_settings_instance = MagicMock() mock_settings_instance = MagicMock()

20
uv.lock generated
View File

@@ -250,22 +250,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001, upload-time = "2024-08-06T14:37:36.958Z" }, { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001, upload-time = "2024-08-06T14:37:36.958Z" },
] ]
[[package]]
name = "auth0-python"
version = "4.7.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "cryptography" },
{ name = "pyjwt" },
{ name = "requests" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/12/81/3e867262f1f48fdacb1f8e9853497f6283274ba2c3c145e767bc0c7ed3c8/auth0_python-4.7.2.tar.gz", hash = "sha256:5d36b7f26defa946c0a548dddccf0451fc62e9f8e61fd0138c5025ad2506ba8b", size = 73261, upload-time = "2024-09-11T06:23:38.03Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/0e/38cb7b781371e79e9c697fb78f3ccd18fda8bd547d0a2e76e616561a3792/auth0_python-4.7.2-py3-none-any.whl", hash = "sha256:df2224f9b1e170b3aa12d8bc7ff02eadb7cc229307a09ec6b8a55fd1e0e05dc8", size = 131834, upload-time = "2024-09-11T06:23:36.619Z" },
]
[[package]] [[package]]
name = "autoflake" name = "autoflake"
version = "2.3.1" version = "2.3.1"
@@ -661,7 +645,6 @@ name = "crewai"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "appdirs" }, { name = "appdirs" },
{ name = "auth0-python" },
{ name = "blinker" }, { name = "blinker" },
{ name = "chromadb" }, { name = "chromadb" },
{ name = "click" }, { name = "click" },
@@ -678,6 +661,7 @@ dependencies = [
{ name = "opentelemetry-sdk" }, { name = "opentelemetry-sdk" },
{ name = "pdfplumber" }, { name = "pdfplumber" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pyjwt" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "pyvis" }, { name = "pyvis" },
{ name = "regex" }, { name = "regex" },
@@ -737,7 +721,6 @@ requires-dist = [
{ name = "agentops", marker = "extra == 'agentops'", specifier = ">=0.3.0" }, { name = "agentops", marker = "extra == 'agentops'", specifier = ">=0.3.0" },
{ name = "aisuite", marker = "extra == 'aisuite'", specifier = ">=0.1.10" }, { name = "aisuite", marker = "extra == 'aisuite'", specifier = ">=0.1.10" },
{ name = "appdirs", specifier = ">=1.4.4" }, { name = "appdirs", specifier = ">=1.4.4" },
{ name = "auth0-python", specifier = ">=4.7.1" },
{ name = "blinker", specifier = ">=1.9.0" }, { name = "blinker", specifier = ">=1.9.0" },
{ name = "chromadb", specifier = ">=0.5.23" }, { name = "chromadb", specifier = ">=0.5.23" },
{ name = "click", specifier = ">=8.1.7" }, { name = "click", specifier = ">=8.1.7" },
@@ -760,6 +743,7 @@ requires-dist = [
{ name = "pdfplumber", specifier = ">=0.11.4" }, { name = "pdfplumber", specifier = ">=0.11.4" },
{ name = "pdfplumber", marker = "extra == 'pdfplumber'", specifier = ">=0.11.4" }, { name = "pdfplumber", marker = "extra == 'pdfplumber'", specifier = ">=0.11.4" },
{ name = "pydantic", specifier = ">=2.4.2" }, { name = "pydantic", specifier = ">=2.4.2" },
{ name = "pyjwt", specifier = ">=2.9.0" },
{ name = "python-dotenv", specifier = ">=1.0.0" }, { name = "python-dotenv", specifier = ">=1.0.0" },
{ name = "pyvis", specifier = ">=0.3.2" }, { name = "pyvis", specifier = ">=0.3.2" },
{ name = "regex", specifier = ">=2024.9.11" }, { name = "regex", specifier = ">=2024.9.11" },