mirror of
https://github.com/crewAIInc/crewAI.git
synced 2025-12-15 20:08:29 +00:00
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:
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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á:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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",
|
||||||
|
)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
20
uv.lock
generated
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user