From fdf3101b39638634cd9cc0594298bb778f61b538 Mon Sep 17 00:00:00 2001 From: Matt Aitchison Date: Wed, 22 Apr 2026 15:21:35 -0500 Subject: [PATCH 1/4] feat(azure): fall back to DefaultAzureCredential when no API key Enables keyless Azure auth (OIDC Workload Identity Federation, Managed Identity, Azure CLI, env-configured Service Principal) without any crewAI-specific configuration. Customers whose deployment environment already sets the standard azure-identity env vars get keyless auth for free; the existing API-key path is unchanged. Linear: FAC-40 --- lib/crewai/pyproject.toml | 1 + .../crewai/llms/providers/azure/completion.py | 34 ++++++++++++++--- lib/crewai/tests/llms/azure/test_azure.py | 38 +++++++++++++++---- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index 001f2b8a6e..cbf0178010 100644 --- a/lib/crewai/pyproject.toml +++ b/lib/crewai/pyproject.toml @@ -94,6 +94,7 @@ google-genai = [ ] azure-ai-inference = [ "azure-ai-inference~=1.0.0b9", + "azure-identity>=1.17.0,<2", ] anthropic = [ "anthropic~=0.73.0", diff --git a/lib/crewai/src/crewai/llms/providers/azure/completion.py b/lib/crewai/src/crewai/llms/providers/azure/completion.py index 4b8d842a5e..714a7f0e9b 100644 --- a/lib/crewai/src/crewai/llms/providers/azure/completion.py +++ b/lib/crewai/src/crewai/llms/providers/azure/completion.py @@ -183,11 +183,6 @@ def _make_client_kwargs(self) -> dict[str, Any]: AzureCompletion._is_azure_openai_endpoint(self.endpoint) ) - if not self.api_key: - raise ValueError( - "Azure API key is required. Set AZURE_API_KEY environment " - "variable or pass api_key parameter." - ) if not self.endpoint: raise ValueError( "Azure endpoint is required. Set AZURE_ENDPOINT environment " @@ -195,12 +190,39 @@ def _make_client_kwargs(self) -> dict[str, Any]: ) client_kwargs: dict[str, Any] = { "endpoint": self.endpoint, - "credential": AzureKeyCredential(self.api_key), + "credential": self._resolve_credential(), } if self.api_version: client_kwargs["api_version"] = self.api_version return client_kwargs + def _resolve_credential(self) -> Any: + """Return an Azure credential, preferring the API key when set. + + Without an API key, fall back to ``DefaultAzureCredential`` from + ``azure-identity``. That chain auto-detects the standard keyless + paths the customer's environment may provide — OIDC Workload + Identity Federation (``AZURE_FEDERATED_TOKEN_FILE`` + + ``AZURE_TENANT_ID`` + ``AZURE_CLIENT_ID``), Managed Identity on + AKS/Azure VMs, environment-configured service principals, and + developer tools like the Azure CLI. Installing ``azure-identity`` + is what enables these paths; without it we raise the existing + API-key error. + """ + if self.api_key: + return AzureKeyCredential(self.api_key) + + try: + from azure.identity import DefaultAzureCredential + except ImportError: + raise ValueError( + "Azure API key is required when azure-identity is not " + "installed. Set AZURE_API_KEY, or install azure-identity " + 'for keyless auth: uv add "crewai[azure-ai-inference]"' + ) from None + + return DefaultAzureCredential() + def _get_sync_client(self) -> Any: if self._client is None: self._client = self._build_sync_client() diff --git a/lib/crewai/tests/llms/azure/test_azure.py b/lib/crewai/tests/llms/azure/test_azure.py index d42e2d7fe2..774d23f205 100644 --- a/lib/crewai/tests/llms/azure/test_azure.py +++ b/lib/crewai/tests/llms/azure/test_azure.py @@ -389,17 +389,41 @@ def test_azure_raises_error_when_endpoint_missing(): llm._get_sync_client() -def test_azure_raises_error_when_api_key_missing(): - """Credentials are validated lazily: construction succeeds, first +def test_azure_raises_error_when_api_key_missing_without_azure_identity(): + """Without an API key AND without ``azure-identity`` installed, client build raises the descriptive error.""" from crewai.llms.providers.azure.completion import AzureCompletion with patch.dict(os.environ, {}, clear=True): - llm = AzureCompletion( - model="gpt-4", endpoint="https://test.openai.azure.com" - ) - with pytest.raises(ValueError, match="Azure API key is required"): - llm._get_sync_client() + with patch.dict("sys.modules", {"azure.identity": None}): + llm = AzureCompletion( + model="gpt-4", endpoint="https://test.openai.azure.com" + ) + with pytest.raises(ValueError, match="Azure API key is required"): + llm._get_sync_client() + + +def test_azure_uses_default_credential_when_api_key_missing(): + """With ``azure-identity`` installed, a missing API key falls back to + ``DefaultAzureCredential`` instead of raising. This is the path that + enables keyless auth (OIDC WIF on EKS/AKS, Managed Identity, Azure + CLI) without any crewAI-specific config.""" + from unittest.mock import MagicMock + + from crewai.llms.providers.azure.completion import AzureCompletion + + sentinel = MagicMock(name="DefaultAzureCredential()") + with patch.dict(os.environ, {}, clear=True): + with patch( + "azure.identity.DefaultAzureCredential", return_value=sentinel + ) as mock_cls: + llm = AzureCompletion( + model="gpt-4", + endpoint="https://test-ai.services.example.com", + ) + kwargs = llm._make_client_kwargs() + assert kwargs["credential"] is sentinel + mock_cls.assert_called() @pytest.mark.asyncio From 3f7637455c41ea67b1e6192e8b7ac89688918394 Mon Sep 17 00:00:00 2001 From: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:36:33 -0700 Subject: [PATCH 2/4] feat: supporting e2b --- lib/crewai-tools/pyproject.toml | 5 + lib/crewai-tools/src/crewai_tools/__init__.py | 8 + .../src/crewai_tools/tools/__init__.py | 8 + .../tools/e2b_sandbox_tool/README.md | 120 ++++ .../tools/e2b_sandbox_tool/__init__.py | 12 + .../tools/e2b_sandbox_tool/e2b_base_tool.py | 197 ++++++ .../tools/e2b_sandbox_tool/e2b_exec_tool.py | 62 ++ .../tools/e2b_sandbox_tool/e2b_file_tool.py | 220 ++++++ .../tools/e2b_sandbox_tool/e2b_python_tool.py | 133 ++++ lib/crewai-tools/tool.specs.json | 662 ++++++++++++++++++ uv.lock | 75 +- 11 files changed, 1500 insertions(+), 2 deletions(-) create mode 100644 lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/README.md create mode 100644 lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/__init__.py create mode 100644 lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/e2b_base_tool.py create mode 100644 lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/e2b_exec_tool.py create mode 100644 lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/e2b_file_tool.py create mode 100644 lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/e2b_python_tool.py diff --git a/lib/crewai-tools/pyproject.toml b/lib/crewai-tools/pyproject.toml index 7cf64465d3..2d3b1bba2a 100644 --- a/lib/crewai-tools/pyproject.toml +++ b/lib/crewai-tools/pyproject.toml @@ -143,6 +143,11 @@ daytona = [ "daytona~=0.140.0", ] +e2b = [ + "e2b~=2.20.0", + "e2b-code-interpreter~=2.6.0", +] + [tool.uv] exclude-newer = "3 days" diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py index 996b63d579..e036874845 100644 --- a/lib/crewai-tools/src/crewai_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/__init__.py @@ -71,6 +71,11 @@ DirectorySearchTool, ) from crewai_tools.tools.docx_search_tool.docx_search_tool import DOCXSearchTool +from crewai_tools.tools.e2b_sandbox_tool import ( + E2BExecTool, + E2BFileTool, + E2BPythonTool, +) from crewai_tools.tools.exa_tools.exa_search_tool import EXASearchTool from crewai_tools.tools.file_read_tool.file_read_tool import FileReadTool from crewai_tools.tools.file_writer_tool.file_writer_tool import FileWriterTool @@ -242,6 +247,9 @@ "DaytonaPythonTool", "DirectoryReadTool", "DirectorySearchTool", + "E2BExecTool", + "E2BFileTool", + "E2BPythonTool", "EXASearchTool", "EnterpriseActionTool", "FileCompressorTool", diff --git a/lib/crewai-tools/src/crewai_tools/tools/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/__init__.py index 40fdb74eb6..7cf61c7a35 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/tools/__init__.py @@ -60,6 +60,11 @@ DirectorySearchTool, ) from crewai_tools.tools.docx_search_tool.docx_search_tool import DOCXSearchTool +from crewai_tools.tools.e2b_sandbox_tool import ( + E2BExecTool, + E2BFileTool, + E2BPythonTool, +) from crewai_tools.tools.exa_tools.exa_search_tool import EXASearchTool from crewai_tools.tools.file_read_tool.file_read_tool import FileReadTool from crewai_tools.tools.file_writer_tool.file_writer_tool import FileWriterTool @@ -227,6 +232,9 @@ "DaytonaPythonTool", "DirectoryReadTool", "DirectorySearchTool", + "E2BExecTool", + "E2BFileTool", + "E2BPythonTool", "EXASearchTool", "FileCompressorTool", "FileReadTool", diff --git a/lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/README.md b/lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/README.md new file mode 100644 index 0000000000..81f30996dc --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/README.md @@ -0,0 +1,120 @@ +# E2B Sandbox Tools + +Run shell commands, execute Python, and manage files inside an [E2B](https://e2b.dev/) sandbox. E2B provides isolated, ephemeral VMs suitable for agent-driven code execution, with a Jupyter-style code interpreter for rich Python results. + +Three tools are provided so you can pick what the agent actually needs: + +- **`E2BExecTool`** — run a shell command (`sandbox.commands.run`). +- **`E2BPythonTool`** — run a Python cell in the E2B code interpreter (`sandbox.run_code`), returning stdout/stderr and rich results (charts, dataframes). +- **`E2BFileTool`** — read / write / list / delete files (`sandbox.files.*`). + +## Installation + +```shell +uv add "crewai-tools[e2b]" +# or +pip install "crewai-tools[e2b]" +``` + +Set the API key: + +```shell +export E2B_API_KEY="..." +``` + +`E2B_DOMAIN` is also respected if set (for self-hosted or non-default deployments). + +## Sandbox lifecycle + +All three tools share the same lifecycle controls from `E2BBaseTool`: + +| Mode | When the sandbox is created | When it is killed | +| --- | --- | --- | +| **Ephemeral** (default, `persistent=False`) | On every `_run` call | At the end of that same call | +| **Persistent** (`persistent=True`) | Lazily on first use | At process exit (via `atexit`), or manually via `tool.close()` | +| **Attach** (`sandbox_id="…"`) | Never — the tool attaches to an existing sandbox | Never — the tool will not kill a sandbox it did not create | + +Ephemeral mode is the safe default: nothing leaks if the agent forgets to clean up. Use persistent mode when you want filesystem state or installed packages to carry across steps — this is typical when pairing `E2BFileTool` with `E2BExecTool`. + +E2B sandboxes also auto-expire after an idle timeout. Tune it via `sandbox_timeout` (seconds, default `300`). + +## Examples + +### One-shot Python execution (ephemeral) + +```python +from crewai_tools import E2BPythonTool + +tool = E2BPythonTool() +result = tool.run(code="print(sum(range(10)))") +``` + +### Multi-step shell session (persistent) + +```python +from crewai_tools import E2BExecTool, E2BFileTool + +exec_tool = E2BExecTool(persistent=True) +file_tool = E2BFileTool(persistent=True) + +# Each tool keeps its own persistent sandbox. If you need the *same* sandbox +# across two tools, create one tool, grab the sandbox id via +# `tool._persistent_sandbox.sandbox_id`, and pass it to the other via +# `sandbox_id=...`. +``` + +### Attach to an existing sandbox + +```python +from crewai_tools import E2BExecTool + +tool = E2BExecTool(sandbox_id="sbx_...") +``` + +### Custom create params + +```python +tool = E2BExecTool( + persistent=True, + template="my-custom-template", + sandbox_timeout=600, + envs={"MY_FLAG": "1"}, + metadata={"owner": "crewai-agent"}, +) +``` + +## Tool arguments + +### `E2BExecTool` +- `command: str` — shell command to run. +- `cwd: str | None` — working directory. +- `envs: dict[str, str] | None` — extra env vars for this command. +- `timeout: float | None` — seconds. + +### `E2BPythonTool` +- `code: str` — source to execute. +- `language: str | None` — override kernel language (default: Python). +- `envs: dict[str, str] | None` — env vars for the run. +- `timeout: float | None` — seconds. + +### `E2BFileTool` +- `action: "read" | "write" | "append" | "list" | "delete" | "mkdir" | "info" | "exists"` +- `path: str` — absolute path inside the sandbox. +- `content: str | None` — required for `append`; optional for `write`. +- `binary: bool` — if `True`, `content` is base64 on write / returned as base64 on read. +- `depth: int` — for `list`, how many levels to recurse (default 1). + +## Security considerations + +These tools hand the LLM arbitrary shell, Python, and filesystem access inside a remote VM. The threat model to keep in mind: + +- **Prompt-injection is a code-execution vector.** If the agent ingests untrusted content (web pages, scraped documents, user-supplied files, emails, search results), a malicious instruction hidden in that content can coerce the agent into issuing commands to `E2BExecTool` / `E2BPythonTool`. Treat any pipeline that feeds untrusted text into an agent that also has these tools as equivalent to remote code execution — the LLM is the attacker's shell. +- **Ephemeral mode (the default) is the main blast-radius control.** A fresh sandbox is created per call and killed at the end, so injected commands cannot persist state, exfiltrate long-lived secrets, or build up tooling across turns. Leave `persistent=False` unless you have a concrete reason to change it. +- **Avoid this specific combination:** + - untrusted content in the agent's context, **plus** + - `persistent=True` or an explicit long-lived `sandbox_id`, **plus** + - a large `sandbox_timeout` or credentials/secrets seeded into the sandbox via `envs`. + + That stack lets a single injection pivot into a long-running, credentialed shell that survives across turns. If you must run persistently, also keep `sandbox_timeout` short, scope `envs` to the minimum the task needs, and don't feed the same agent untrusted input. +- **Don't mount production credentials.** Anything you put into `envs`, `metadata`, or files written to the sandbox is reachable from the LLM. Use per-task scoped keys, not your personal API tokens. +- **E2B's VM isolation is the final backstop**, not a license to relax the above — isolation prevents escape to the host, but everything the sandbox can reach (the public internet, any service whose token you dropped in) is still fair game for an injected command. diff --git a/lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/__init__.py new file mode 100644 index 0000000000..8bb3b26b39 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/__init__.py @@ -0,0 +1,12 @@ +from crewai_tools.tools.e2b_sandbox_tool.e2b_base_tool import E2BBaseTool +from crewai_tools.tools.e2b_sandbox_tool.e2b_exec_tool import E2BExecTool +from crewai_tools.tools.e2b_sandbox_tool.e2b_file_tool import E2BFileTool +from crewai_tools.tools.e2b_sandbox_tool.e2b_python_tool import E2BPythonTool + + +__all__ = [ + "E2BBaseTool", + "E2BExecTool", + "E2BFileTool", + "E2BPythonTool", +] diff --git a/lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/e2b_base_tool.py b/lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/e2b_base_tool.py new file mode 100644 index 0000000000..e22680dfe0 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/e2b_base_tool.py @@ -0,0 +1,197 @@ +from __future__ import annotations + +import atexit +import logging +import os +import threading +from typing import Any, ClassVar + +from crewai.tools import BaseTool, EnvVar +from pydantic import ConfigDict, Field, PrivateAttr, SecretStr + + +logger = logging.getLogger(__name__) + + +class E2BBaseTool(BaseTool): + """Shared base for tools that act on an E2B sandbox. + + Lifecycle modes: + - persistent=False (default): create a fresh sandbox per `_run` call and + kill it when the call returns. Safer and stateless — nothing leaks if + the agent forgets cleanup. + - persistent=True: lazily create a single sandbox on first use, cache it + on the instance, and register an atexit hook to kill it at process + exit. Cheaper across many calls and lets files/state carry over. + - sandbox_id=: attach to a sandbox the caller already owns. + Never killed by the tool. + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + package_dependencies: list[str] = Field(default_factory=lambda: ["e2b"]) + + api_key: SecretStr | None = Field( + default_factory=lambda: ( + SecretStr(val) if (val := os.getenv("E2B_API_KEY")) else None + ), + description="E2B API key. Falls back to E2B_API_KEY env var.", + json_schema_extra={"required": False}, + repr=False, + ) + domain: str | None = Field( + default_factory=lambda: os.getenv("E2B_DOMAIN"), + description="E2B API domain override. Falls back to E2B_DOMAIN env var.", + json_schema_extra={"required": False}, + ) + + template: str | None = Field( + default=None, + description=( + "Optional template/snapshot name or id to create the sandbox from. " + "Defaults to E2B's base template when omitted." + ), + ) + persistent: bool = Field( + default=False, + description=( + "If True, reuse one sandbox across all calls to this tool instance " + "and kill it at process exit. Default False creates and kills a " + "fresh sandbox per call." + ), + ) + sandbox_id: str | None = Field( + default=None, + description=( + "Attach to an existing sandbox by id instead of creating a new " + "one. The tool will never kill a sandbox it did not create." + ), + ) + sandbox_timeout: int = Field( + default=300, + description=( + "Idle timeout in seconds after which E2B auto-kills the sandbox. " + "Applied at create time and when attaching via sandbox_id." + ), + ) + envs: dict[str, str] | None = Field( + default=None, + description="Environment variables to set inside the sandbox at create time.", + ) + metadata: dict[str, str] | None = Field( + default=None, + description="Metadata key-value pairs to attach to the sandbox at create time.", + ) + + env_vars: list[EnvVar] = Field( + default_factory=lambda: [ + EnvVar( + name="E2B_API_KEY", + description="API key for E2B sandbox service", + required=False, + ), + EnvVar( + name="E2B_DOMAIN", + description="E2B API domain (optional)", + required=False, + ), + ] + ) + + _persistent_sandbox: Any | None = PrivateAttr(default=None) + _lock: threading.Lock = PrivateAttr(default_factory=threading.Lock) + _cleanup_registered: bool = PrivateAttr(default=False) + + _sdk_cache: ClassVar[dict[str, Any]] = {} + + @classmethod + def _import_sandbox_class(cls) -> Any: + """Return the Sandbox class used by this tool. + + Subclasses override this to swap in a different SDK (e.g. the code + interpreter sandbox). The default uses plain `e2b.Sandbox`. + """ + cached = cls._sdk_cache.get("e2b.Sandbox") + if cached is not None: + return cached + try: + from e2b import Sandbox # type: ignore[import-untyped] + except ImportError as exc: + raise ImportError( + "The 'e2b' package is required for E2B sandbox tools. " + "Install it with: uv add e2b (or) pip install e2b" + ) from exc + cls._sdk_cache["e2b.Sandbox"] = Sandbox + return Sandbox + + def _connect_kwargs(self) -> dict[str, Any]: + kwargs: dict[str, Any] = {} + if self.api_key is not None: + kwargs["api_key"] = self.api_key.get_secret_value() + if self.domain: + kwargs["domain"] = self.domain + if self.sandbox_timeout is not None: + kwargs["timeout"] = self.sandbox_timeout + return kwargs + + def _create_kwargs(self) -> dict[str, Any]: + kwargs: dict[str, Any] = self._connect_kwargs() + if self.template is not None: + kwargs["template"] = self.template + if self.envs is not None: + kwargs["envs"] = self.envs + if self.metadata is not None: + kwargs["metadata"] = self.metadata + return kwargs + + def _acquire_sandbox(self) -> tuple[Any, bool]: + """Return (sandbox, should_kill_after_use).""" + sandbox_cls = self._import_sandbox_class() + + if self.sandbox_id: + return ( + sandbox_cls.connect(self.sandbox_id, **self._connect_kwargs()), + False, + ) + + if self.persistent: + with self._lock: + if self._persistent_sandbox is None: + self._persistent_sandbox = sandbox_cls.create( + **self._create_kwargs() + ) + if not self._cleanup_registered: + atexit.register(self.close) + self._cleanup_registered = True + return self._persistent_sandbox, False + + sandbox = sandbox_cls.create(**self._create_kwargs()) + return sandbox, True + + def _release_sandbox(self, sandbox: Any, should_kill: bool) -> None: + if not should_kill: + return + try: + sandbox.kill() + except Exception: + logger.debug( + "Best-effort sandbox cleanup failed after ephemeral use; " + "the sandbox may need manual termination.", + exc_info=True, + ) + + def close(self) -> None: + """Kill the cached persistent sandbox if one exists.""" + with self._lock: + sandbox = self._persistent_sandbox + self._persistent_sandbox = None + if sandbox is None: + return + try: + sandbox.kill() + except Exception: + logger.debug( + "Best-effort persistent sandbox cleanup failed at close(); " + "the sandbox may need manual termination.", + exc_info=True, + ) diff --git a/lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/e2b_exec_tool.py b/lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/e2b_exec_tool.py new file mode 100644 index 0000000000..571be33004 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/e2b_exec_tool.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from builtins import type as type_ +from typing import Any + +from pydantic import BaseModel, Field + +from crewai_tools.tools.e2b_sandbox_tool.e2b_base_tool import E2BBaseTool + + +class E2BExecToolSchema(BaseModel): + command: str = Field(..., description="Shell command to execute in the sandbox.") + cwd: str | None = Field( + default=None, + description="Working directory to run the command in. Defaults to the sandbox home dir.", + ) + envs: dict[str, str] | None = Field( + default=None, + description="Optional environment variables to set for this command.", + ) + timeout: float | None = Field( + default=None, + description="Maximum seconds to wait for the command to finish.", + ) + + +class E2BExecTool(E2BBaseTool): + """Run a shell command inside an E2B sandbox.""" + + name: str = "E2B Sandbox Exec" + description: str = ( + "Execute a shell command inside an E2B sandbox and return the exit " + "code, stdout, and stderr. Use this to run builds, package installs, " + "git operations, or any one-off shell command." + ) + args_schema: type_[BaseModel] = E2BExecToolSchema + + def _run( + self, + command: str, + cwd: str | None = None, + envs: dict[str, str] | None = None, + timeout: float | None = None, + ) -> Any: + sandbox, should_kill = self._acquire_sandbox() + try: + run_kwargs: dict[str, Any] = {} + if cwd is not None: + run_kwargs["cwd"] = cwd + if envs is not None: + run_kwargs["envs"] = envs + if timeout is not None: + run_kwargs["timeout"] = timeout + result = sandbox.commands.run(command, **run_kwargs) + return { + "exit_code": getattr(result, "exit_code", None), + "stdout": getattr(result, "stdout", None), + "stderr": getattr(result, "stderr", None), + "error": getattr(result, "error", None), + } + finally: + self._release_sandbox(sandbox, should_kill) diff --git a/lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/e2b_file_tool.py b/lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/e2b_file_tool.py new file mode 100644 index 0000000000..e39d348c24 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/e2b_file_tool.py @@ -0,0 +1,220 @@ +from __future__ import annotations + +import base64 +from builtins import type as type_ +import logging +import posixpath +from typing import Any, Literal + +from pydantic import BaseModel, Field, model_validator + +from crewai_tools.tools.e2b_sandbox_tool.e2b_base_tool import E2BBaseTool + + +logger = logging.getLogger(__name__) + + +FileAction = Literal[ + "read", "write", "append", "list", "delete", "mkdir", "info", "exists" +] + + +class E2BFileToolSchema(BaseModel): + action: FileAction = Field( + ..., + description=( + "The filesystem action to perform: 'read' (returns file contents), " + "'write' (create or replace a file with content), 'append' (append " + "content to an existing file — use this for writing large files in " + "chunks to avoid hitting tool-call size limits), 'list' (lists a " + "directory), 'delete' (removes a file/dir), 'mkdir' (creates a " + "directory), 'info' (returns file metadata), 'exists' (returns a " + "boolean for whether the path exists)." + ), + ) + path: str = Field(..., description="Absolute path inside the sandbox.") + content: str | None = Field( + default=None, + description=( + "Content to write or append. If omitted for 'write', an empty file " + "is created. For files larger than a few KB, prefer one 'write' " + "with empty content followed by multiple 'append' calls of ~4KB " + "each to stay within tool-call payload limits." + ), + ) + binary: bool = Field( + default=False, + description=( + "For 'write'/'append': treat content as base64 and upload raw " + "bytes. For 'read': return contents as base64 instead of decoded " + "utf-8." + ), + ) + depth: int = Field( + default=1, + description="For action='list': how many levels deep to recurse (default 1).", + ) + + @model_validator(mode="after") + def _validate_action_args(self) -> E2BFileToolSchema: + if self.action == "append" and self.content is None: + raise ValueError( + "action='append' requires 'content'. Pass the chunk to append " + "in the 'content' field." + ) + return self + + +class E2BFileTool(E2BBaseTool): + """Read, write, and manage files inside an E2B sandbox. + + Notes: + - Most useful with `persistent=True` or an explicit `sandbox_id`. With + the default ephemeral mode, files disappear when this tool call + finishes. + """ + + name: str = "E2B Sandbox Files" + description: str = ( + "Perform filesystem operations inside an E2B sandbox: read a file, " + "write content to a path, append content to an existing file, list a " + "directory, delete a path, make a directory, fetch file metadata, or " + "check whether a path exists. For files larger than a few KB, create " + "the file with action='write' and empty content, then send the body " + "via multiple 'append' calls of ~4KB each to stay within tool-call " + "payload limits." + ) + args_schema: type_[BaseModel] = E2BFileToolSchema + + def _run( + self, + action: FileAction, + path: str, + content: str | None = None, + binary: bool = False, + depth: int = 1, + ) -> Any: + sandbox, should_kill = self._acquire_sandbox() + try: + if action == "read": + return self._read(sandbox, path, binary=binary) + if action == "write": + return self._write(sandbox, path, content or "", binary=binary) + if action == "append": + return self._append(sandbox, path, content or "", binary=binary) + if action == "list": + return self._list(sandbox, path, depth=depth) + if action == "delete": + sandbox.files.remove(path) + return {"status": "deleted", "path": path} + if action == "mkdir": + created = sandbox.files.make_dir(path) + return {"status": "created", "path": path, "created": bool(created)} + if action == "info": + return self._info(sandbox, path) + if action == "exists": + return {"path": path, "exists": bool(sandbox.files.exists(path))} + raise ValueError(f"Unknown action: {action}") + finally: + self._release_sandbox(sandbox, should_kill) + + def _read(self, sandbox: Any, path: str, *, binary: bool) -> dict[str, Any]: + if binary: + data: bytes = sandbox.files.read(path, format="bytes") + return { + "path": path, + "encoding": "base64", + "content": base64.b64encode(data).decode("ascii"), + } + try: + content: str = sandbox.files.read(path) + return {"path": path, "encoding": "utf-8", "content": content} + except UnicodeDecodeError: + data = sandbox.files.read(path, format="bytes") + return { + "path": path, + "encoding": "base64", + "content": base64.b64encode(data).decode("ascii"), + "note": "File was not valid utf-8; returned as base64.", + } + + def _write( + self, sandbox: Any, path: str, content: str, *, binary: bool + ) -> dict[str, Any]: + payload: str | bytes = base64.b64decode(content) if binary else content + self._ensure_parent_dir(sandbox, path) + sandbox.files.write(path, payload) + size = ( + len(payload) + if isinstance(payload, (bytes, bytearray)) + else len(payload.encode("utf-8")) + ) + return {"status": "written", "path": path, "bytes": size} + + def _append( + self, sandbox: Any, path: str, content: str, *, binary: bool + ) -> dict[str, Any]: + chunk: bytes = base64.b64decode(content) if binary else content.encode("utf-8") + self._ensure_parent_dir(sandbox, path) + try: + existing: bytes = sandbox.files.read(path, format="bytes") + except Exception: + existing = b"" + payload = existing + chunk + sandbox.files.write(path, payload) + return { + "status": "appended", + "path": path, + "appended_bytes": len(chunk), + "total_bytes": len(payload), + } + + @staticmethod + def _ensure_parent_dir(sandbox: Any, path: str) -> None: + parent = posixpath.dirname(path) + if not parent or parent in ("/", "."): + return + try: + sandbox.files.make_dir(parent) + except Exception: + logger.debug( + "Best-effort parent-directory create failed for %s; " + "assuming it already exists and proceeding with the write.", + parent, + exc_info=True, + ) + + def _list(self, sandbox: Any, path: str, *, depth: int) -> dict[str, Any]: + entries = sandbox.files.list(path, depth=depth) + return { + "path": path, + "entries": [self._entry_to_dict(e) for e in entries], + } + + def _info(self, sandbox: Any, path: str) -> dict[str, Any]: + return self._entry_to_dict(sandbox.files.get_info(path)) + + @staticmethod + def _entry_to_dict(entry: Any) -> dict[str, Any]: + fields = ( + "name", + "path", + "type", + "size", + "mode", + "permissions", + "owner", + "group", + "modified_time", + "symlink_target", + ) + result: dict[str, Any] = {} + for field in fields: + value = getattr(entry, field, None) + if value is not None and field == "modified_time": + result[field] = ( + value.isoformat() if hasattr(value, "isoformat") else str(value) + ) + else: + result[field] = value + return result diff --git a/lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/e2b_python_tool.py b/lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/e2b_python_tool.py new file mode 100644 index 0000000000..724e92454b --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/e2b_sandbox_tool/e2b_python_tool.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +from builtins import type as type_ +from typing import Any, ClassVar + +from pydantic import BaseModel, Field + +from crewai_tools.tools.e2b_sandbox_tool.e2b_base_tool import E2BBaseTool + + +class E2BPythonToolSchema(BaseModel): + code: str = Field( + ..., + description="Python source to execute inside the sandbox.", + ) + language: str | None = Field( + default=None, + description=( + "Override the execution language (e.g. 'python', 'r', 'javascript'). " + "Defaults to Python when omitted." + ), + ) + envs: dict[str, str] | None = Field( + default=None, + description="Optional environment variables for the run.", + ) + timeout: float | None = Field( + default=None, + description="Maximum seconds to wait for the code to finish.", + ) + + +class E2BPythonTool(E2BBaseTool): + """Run Python code inside an E2B code interpreter sandbox. + + Uses `e2b_code_interpreter`, which runs cells in a persistent Jupyter-style + kernel so state (imports, variables) carries across calls when + `persistent=True`. + """ + + name: str = "E2B Sandbox Python" + description: str = ( + "Execute a block of Python code inside an E2B code interpreter sandbox " + "and return captured stdout, stderr, the final expression value, and " + "any rich results (charts, dataframes). Use this for data processing, " + "quick scripts, or analysis that should run in an isolated environment." + ) + args_schema: type_[BaseModel] = E2BPythonToolSchema + + package_dependencies: list[str] = Field( + default_factory=lambda: ["e2b_code_interpreter"], + ) + + _ci_cache: ClassVar[dict[str, Any]] = {} + + @classmethod + def _import_sandbox_class(cls) -> Any: + cached = cls._ci_cache.get("Sandbox") + if cached is not None: + return cached + try: + from e2b_code_interpreter import Sandbox # type: ignore[import-untyped] + except ImportError as exc: + raise ImportError( + "The 'e2b_code_interpreter' package is required for the E2B " + "Python tool. Install it with: " + "uv add e2b-code-interpreter (or) " + "pip install e2b-code-interpreter" + ) from exc + cls._ci_cache["Sandbox"] = Sandbox + return Sandbox + + def _run( + self, + code: str, + language: str | None = None, + envs: dict[str, str] | None = None, + timeout: float | None = None, + ) -> Any: + sandbox, should_kill = self._acquire_sandbox() + try: + run_kwargs: dict[str, Any] = {} + if language is not None: + run_kwargs["language"] = language + if envs is not None: + run_kwargs["envs"] = envs + if timeout is not None: + run_kwargs["timeout"] = timeout + execution = sandbox.run_code(code, **run_kwargs) + return self._serialize_execution(execution) + finally: + self._release_sandbox(sandbox, should_kill) + + @staticmethod + def _serialize_execution(execution: Any) -> dict[str, Any]: + logs = getattr(execution, "logs", None) + error = getattr(execution, "error", None) + results = getattr(execution, "results", None) or [] + return { + "text": getattr(execution, "text", None), + "stdout": list(getattr(logs, "stdout", []) or []) if logs else [], + "stderr": list(getattr(logs, "stderr", []) or []) if logs else [], + "error": ( + { + "name": getattr(error, "name", None), + "value": getattr(error, "value", None), + "traceback": getattr(error, "traceback", None), + } + if error + else None + ), + "results": [E2BPythonTool._serialize_result(r) for r in results], + "execution_count": getattr(execution, "execution_count", None), + } + + @staticmethod + def _serialize_result(result: Any) -> dict[str, Any]: + fields = ( + "text", + "html", + "markdown", + "svg", + "png", + "jpeg", + "pdf", + "latex", + "json", + "javascript", + "data", + "is_main_result", + "extra", + ) + return {field: getattr(result, field, None) for field in fields} diff --git a/lib/crewai-tools/tool.specs.json b/lib/crewai-tools/tool.specs.json index 6bd3747497..c78dc2c1f6 100644 --- a/lib/crewai-tools/tool.specs.json +++ b/lib/crewai-tools/tool.specs.json @@ -8734,6 +8734,668 @@ "type": "object" } }, + { + "description": "Execute a shell command inside an E2B sandbox and return the exit code, stdout, and stderr. Use this to run builds, package installs, git operations, or any one-off shell command.", + "env_vars": [ + { + "default": null, + "description": "API key for E2B sandbox service", + "name": "E2B_API_KEY", + "required": false + }, + { + "default": null, + "description": "E2B API domain (optional)", + "name": "E2B_DOMAIN", + "required": false + } + ], + "humanized_name": "E2B Sandbox Exec", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "Run a shell command inside an E2B sandbox.", + "properties": { + "api_key": { + "anyOf": [ + { + "format": "password", + "type": "string", + "writeOnly": true + }, + { + "type": "null" + } + ], + "description": "E2B API key. Falls back to E2B_API_KEY env var.", + "required": false, + "title": "Api Key" + }, + "domain": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "E2B API domain override. Falls back to E2B_DOMAIN env var.", + "required": false, + "title": "Domain" + }, + "envs": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Environment variables to set inside the sandbox at create time.", + "title": "Envs" + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Metadata key-value pairs to attach to the sandbox at create time.", + "title": "Metadata" + }, + "persistent": { + "default": false, + "description": "If True, reuse one sandbox across all calls to this tool instance and kill it at process exit. Default False creates and kills a fresh sandbox per call.", + "title": "Persistent", + "type": "boolean" + }, + "sandbox_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Attach to an existing sandbox by id instead of creating a new one. The tool will never kill a sandbox it did not create.", + "title": "Sandbox Id" + }, + "sandbox_timeout": { + "default": 300, + "description": "Idle timeout in seconds after which E2B auto-kills the sandbox. Applied at create time and when attaching via sandbox_id.", + "title": "Sandbox Timeout", + "type": "integer" + }, + "template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional template/snapshot name or id to create the sandbox from. Defaults to E2B's base template when omitted.", + "title": "Template" + } + }, + "required": [], + "title": "E2BExecTool", + "type": "object" + }, + "name": "E2BExecTool", + "package_dependencies": [ + "e2b" + ], + "run_params_schema": { + "properties": { + "command": { + "description": "Shell command to execute in the sandbox.", + "title": "Command", + "type": "string" + }, + "cwd": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Working directory to run the command in. Defaults to the sandbox home dir.", + "title": "Cwd" + }, + "envs": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional environment variables to set for this command.", + "title": "Envs" + }, + "timeout": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum seconds to wait for the command to finish.", + "title": "Timeout" + } + }, + "required": [ + "command" + ], + "title": "E2BExecToolSchema", + "type": "object" + } + }, + { + "description": "Perform filesystem operations inside an E2B sandbox: read a file, write content to a path, append content to an existing file, list a directory, delete a path, make a directory, fetch file metadata, or check whether a path exists. For files larger than a few KB, create the file with action='write' and empty content, then send the body via multiple 'append' calls of ~4KB each to stay within tool-call payload limits.", + "env_vars": [ + { + "default": null, + "description": "API key for E2B sandbox service", + "name": "E2B_API_KEY", + "required": false + }, + { + "default": null, + "description": "E2B API domain (optional)", + "name": "E2B_DOMAIN", + "required": false + } + ], + "humanized_name": "E2B Sandbox Files", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "Read, write, and manage files inside an E2B sandbox.\n\nNotes:\n - Most useful with `persistent=True` or an explicit `sandbox_id`. With\n the default ephemeral mode, files disappear when this tool call\n finishes.", + "properties": { + "api_key": { + "anyOf": [ + { + "format": "password", + "type": "string", + "writeOnly": true + }, + { + "type": "null" + } + ], + "description": "E2B API key. Falls back to E2B_API_KEY env var.", + "required": false, + "title": "Api Key" + }, + "domain": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "E2B API domain override. Falls back to E2B_DOMAIN env var.", + "required": false, + "title": "Domain" + }, + "envs": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Environment variables to set inside the sandbox at create time.", + "title": "Envs" + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Metadata key-value pairs to attach to the sandbox at create time.", + "title": "Metadata" + }, + "persistent": { + "default": false, + "description": "If True, reuse one sandbox across all calls to this tool instance and kill it at process exit. Default False creates and kills a fresh sandbox per call.", + "title": "Persistent", + "type": "boolean" + }, + "sandbox_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Attach to an existing sandbox by id instead of creating a new one. The tool will never kill a sandbox it did not create.", + "title": "Sandbox Id" + }, + "sandbox_timeout": { + "default": 300, + "description": "Idle timeout in seconds after which E2B auto-kills the sandbox. Applied at create time and when attaching via sandbox_id.", + "title": "Sandbox Timeout", + "type": "integer" + }, + "template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional template/snapshot name or id to create the sandbox from. Defaults to E2B's base template when omitted.", + "title": "Template" + } + }, + "required": [], + "title": "E2BFileTool", + "type": "object" + }, + "name": "E2BFileTool", + "package_dependencies": [ + "e2b" + ], + "run_params_schema": { + "properties": { + "action": { + "description": "The filesystem action to perform: 'read' (returns file contents), 'write' (create or replace a file with content), 'append' (append content to an existing file \u2014 use this for writing large files in chunks to avoid hitting tool-call size limits), 'list' (lists a directory), 'delete' (removes a file/dir), 'mkdir' (creates a directory), 'info' (returns file metadata), 'exists' (returns a boolean for whether the path exists).", + "enum": [ + "read", + "write", + "append", + "list", + "delete", + "mkdir", + "info", + "exists" + ], + "title": "Action", + "type": "string" + }, + "binary": { + "default": false, + "description": "For 'write'/'append': treat content as base64 and upload raw bytes. For 'read': return contents as base64 instead of decoded utf-8.", + "title": "Binary", + "type": "boolean" + }, + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Content to write or append. If omitted for 'write', an empty file is created. For files larger than a few KB, prefer one 'write' with empty content followed by multiple 'append' calls of ~4KB each to stay within tool-call payload limits.", + "title": "Content" + }, + "depth": { + "default": 1, + "description": "For action='list': how many levels deep to recurse (default 1).", + "title": "Depth", + "type": "integer" + }, + "path": { + "description": "Absolute path inside the sandbox.", + "title": "Path", + "type": "string" + } + }, + "required": [ + "action", + "path" + ], + "title": "E2BFileToolSchema", + "type": "object" + } + }, + { + "description": "Execute a block of Python code inside an E2B code interpreter sandbox and return captured stdout, stderr, the final expression value, and any rich results (charts, dataframes). Use this for data processing, quick scripts, or analysis that should run in an isolated environment.", + "env_vars": [ + { + "default": null, + "description": "API key for E2B sandbox service", + "name": "E2B_API_KEY", + "required": false + }, + { + "default": null, + "description": "E2B API domain (optional)", + "name": "E2B_DOMAIN", + "required": false + } + ], + "humanized_name": "E2B Sandbox Python", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "Run Python code inside an E2B code interpreter sandbox.\n\nUses `e2b_code_interpreter`, which runs cells in a persistent Jupyter-style\nkernel so state (imports, variables) carries across calls when\n`persistent=True`.", + "properties": { + "api_key": { + "anyOf": [ + { + "format": "password", + "type": "string", + "writeOnly": true + }, + { + "type": "null" + } + ], + "description": "E2B API key. Falls back to E2B_API_KEY env var.", + "required": false, + "title": "Api Key" + }, + "domain": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "E2B API domain override. Falls back to E2B_DOMAIN env var.", + "required": false, + "title": "Domain" + }, + "envs": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Environment variables to set inside the sandbox at create time.", + "title": "Envs" + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Metadata key-value pairs to attach to the sandbox at create time.", + "title": "Metadata" + }, + "persistent": { + "default": false, + "description": "If True, reuse one sandbox across all calls to this tool instance and kill it at process exit. Default False creates and kills a fresh sandbox per call.", + "title": "Persistent", + "type": "boolean" + }, + "sandbox_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Attach to an existing sandbox by id instead of creating a new one. The tool will never kill a sandbox it did not create.", + "title": "Sandbox Id" + }, + "sandbox_timeout": { + "default": 300, + "description": "Idle timeout in seconds after which E2B auto-kills the sandbox. Applied at create time and when attaching via sandbox_id.", + "title": "Sandbox Timeout", + "type": "integer" + }, + "template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional template/snapshot name or id to create the sandbox from. Defaults to E2B's base template when omitted.", + "title": "Template" + } + }, + "required": [], + "title": "E2BPythonTool", + "type": "object" + }, + "name": "E2BPythonTool", + "package_dependencies": [ + "e2b_code_interpreter" + ], + "run_params_schema": { + "properties": { + "code": { + "description": "Python source to execute inside the sandbox.", + "title": "Code", + "type": "string" + }, + "envs": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional environment variables for the run.", + "title": "Envs" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Override the execution language (e.g. 'python', 'r', 'javascript'). Defaults to Python when omitted.", + "title": "Language" + }, + "timeout": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum seconds to wait for the code to finish.", + "title": "Timeout" + } + }, + "required": [ + "code" + ], + "title": "E2BPythonToolSchema", + "type": "object" + } + }, { "description": "Search the internet using Exa", "env_vars": [ diff --git a/uv.lock b/uv.lock index de440cae25..768be7983f 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-04-22T16:00:00Z" +exclude-newer = "2026-04-23T07:00:00Z" [manifest] members = [ @@ -700,6 +700,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/76/cab7af7f16c0b09347f2ebe7ffda7101132f786acb767666dce43055faab/botocore_stubs-1.42.41-py3-none-any.whl", hash = "sha256:9423110fb0e391834bd2ed44ae5f879d8cb370a444703d966d30842ce2bcb5f0", size = 66759, upload-time = "2026-02-03T20:46:13.02Z" }, ] +[[package]] +name = "bracex" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, +] + [[package]] name = "browserbase" version = "1.8.0" @@ -1489,6 +1498,10 @@ databricks-sdk = [ daytona = [ { name = "daytona" }, ] +e2b = [ + { name = "e2b" }, + { name = "e2b-code-interpreter" }, +] exa-py = [ { name = "exa-py" }, ] @@ -1590,6 +1603,8 @@ requires-dist = [ { name = "cryptography", marker = "extra == 'snowflake'", specifier = ">=43.0.3" }, { name = "databricks-sdk", marker = "extra == 'databricks-sdk'", specifier = ">=0.46.0" }, { name = "daytona", marker = "extra == 'daytona'", specifier = "~=0.140.0" }, + { name = "e2b", marker = "extra == 'e2b'", specifier = "~=2.20.0" }, + { name = "e2b-code-interpreter", marker = "extra == 'e2b'", specifier = "~=2.6.0" }, { name = "exa-py", marker = "extra == 'exa-py'", specifier = ">=1.8.7" }, { name = "firecrawl-py", marker = "extra == 'firecrawl-py'", specifier = ">=1.8.0" }, { name = "gitpython", marker = "extra == 'github'", specifier = ">=3.1.41,<4" }, @@ -1632,7 +1647,7 @@ requires-dist = [ { name = "weaviate-client", marker = "extra == 'weaviate-client'", specifier = ">=4.10.2" }, { name = "youtube-transcript-api", specifier = "~=1.2.2" }, ] -provides-extras = ["apify", "beautifulsoup4", "bedrock", "browserbase", "composio-core", "contextual", "couchbase", "databricks-sdk", "daytona", "exa-py", "firecrawl-py", "github", "hyperbrowser", "linkup-sdk", "mcp", "mongodb", "multion", "mysql", "oxylabs", "patronus", "postgresql", "qdrant-client", "rag", "scrapegraph-py", "scrapfly-sdk", "selenium", "serpapi", "singlestore", "snowflake", "spider-client", "sqlalchemy", "stagehand", "tavily-python", "weaviate-client", "xml"] +provides-extras = ["apify", "beautifulsoup4", "bedrock", "browserbase", "composio-core", "contextual", "couchbase", "databricks-sdk", "daytona", "e2b", "exa-py", "firecrawl-py", "github", "hyperbrowser", "linkup-sdk", "mcp", "mongodb", "multion", "mysql", "oxylabs", "patronus", "postgresql", "qdrant-client", "rag", "scrapegraph-py", "scrapfly-sdk", "selenium", "serpapi", "singlestore", "snowflake", "spider-client", "sqlalchemy", "stagehand", "tavily-python", "weaviate-client", "xml"] [[package]] name = "cryptography" @@ -1975,6 +1990,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] +[[package]] +name = "dockerfile-parse" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/df/929ee0b5d2c8bd8d713c45e71b94ab57c7e11e322130724d54f469b2cd48/dockerfile-parse-2.0.1.tar.gz", hash = "sha256:3184ccdc513221983e503ac00e1aa504a2aa8f84e5de673c46b0b6eee99ec7bc", size = 24556, upload-time = "2023-07-18T13:36:07.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/6c/79cd5bc1b880d8c1a9a5550aa8dacd57353fa3bb2457227e1fb47383eb49/dockerfile_parse-2.0.1-py2.py3-none-any.whl", hash = "sha256:bdffd126d2eb26acf1066acb54cb2e336682e1d72b974a40894fac76a4df17f6", size = 14845, upload-time = "2023-07-18T13:36:06.052Z" }, +] + [[package]] name = "docling" version = "2.84.0" @@ -2125,6 +2149,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, ] +[[package]] +name = "e2b" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "dockerfile-parse" }, + { name = "httpcore" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "python-dateutil" }, + { name = "rich" }, + { name = "typing-extensions" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/87/e9b3bd252a4fe2b3fd6967ff985c7a5a15a31b2d5b8c37e50afb18797b17/e2b-2.20.0.tar.gz", hash = "sha256:52b3a00ac7015bbdce84913b2a57664d2def33d5a4069e34fa2354de31759173", size = 156575, upload-time = "2026-04-02T19:20:32.375Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/ce/e402e2ecebe40ed9af20cddb862386f2ce20336e35c0dea257812129020e/e2b-2.20.0-py3-none-any.whl", hash = "sha256:66f6edcf6b742ca180f3aadcff7966fda86d68430fa6b2becdfa0fcc72224988", size = 296483, upload-time = "2026-04-02T19:20:30.573Z" }, +] + +[[package]] +name = "e2b-code-interpreter" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "e2b" }, + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/dd/f90b56d1597abfcdabdc018ac184fa714066be93d24b97edc2bf0671d483/e2b_code_interpreter-2.6.0.tar.gz", hash = "sha256:67e66531e5cf65c9df6e82aa0bdb1e73223a1ab205f10d47c027eb2ea09b73f9", size = 10683, upload-time = "2026-03-23T17:01:07.327Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/79/f70d50604584df66064892f3fca7ab57b10ad40c826fd003be53a4cd5fa5/e2b_code_interpreter-2.6.0-py3-none-any.whl", hash = "sha256:a15f1d155566aef98cf2ccc0f8d9b07d15e07582d6cc8a128bc97de371bd617c", size = 13715, upload-time = "2026-03-23T17:01:06.111Z" }, +] + [[package]] name = "effdet" version = "0.4.1" @@ -9407,6 +9466,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, ] +[[package]] +name = "wcmatch" +version = "10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, +] + [[package]] name = "wcwidth" version = "0.6.0" From 3e9deaf9c02b365fde20a8ff7a860961734efe5e Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Thu, 23 Apr 2026 04:55:08 +0800 Subject: [PATCH 3/4] feat: bump versions to 1.14.3a3 --- .github/workflows/import-time.yml | 103 ------------------ lib/crewai-files/src/crewai_files/__init__.py | 2 +- lib/crewai-tools/pyproject.toml | 2 +- lib/crewai-tools/src/crewai_tools/__init__.py | 2 +- lib/crewai/pyproject.toml | 2 +- lib/crewai/src/crewai/__init__.py | 2 +- .../crewai/cli/templates/crew/pyproject.toml | 2 +- .../crewai/cli/templates/flow/pyproject.toml | 2 +- .../crewai/cli/templates/tool/pyproject.toml | 2 +- lib/devtools/src/crewai_devtools/__init__.py | 2 +- scripts/benchmark_import_time.py | 76 ------------- uv.lock | 46 +++++++- 12 files changed, 54 insertions(+), 189 deletions(-) delete mode 100644 .github/workflows/import-time.yml delete mode 100755 scripts/benchmark_import_time.py diff --git a/.github/workflows/import-time.yml b/.github/workflows/import-time.yml deleted file mode 100644 index 7c0126b239..0000000000 --- a/.github/workflows/import-time.yml +++ /dev/null @@ -1,103 +0,0 @@ -name: Import Time Guard - -on: - pull_request: - paths: - - "lib/crewai/src/**" - - "lib/crewai/pyproject.toml" - - "pyproject.toml" - -permissions: - contents: read - -jobs: - import-time: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.12"] - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: astral-sh/setup-uv@v6 - with: - version: "0.11.3" - enable-cache: true - - - name: Install the project - run: uv sync --all-extras --no-dev - env: - UV_PYTHON: ${{ matrix.python-version }} - - - name: Benchmark PR branch - id: pr - run: | - result=$(uv run python scripts/benchmark_import_time.py --runs 5 --json) - echo "result=$result" >> "$GITHUB_OUTPUT" - echo "pr_median=$(echo $result | python3 -c 'import sys,json; print(json.load(sys.stdin)["median_s"])')" >> "$GITHUB_OUTPUT" - echo "### PR Branch Import Time" >> "$GITHUB_STEP_SUMMARY" - echo "$result" | python3 -c " - import sys, json - d = json.load(sys.stdin) - print(f'- Median: {d[\"median_s\"]}s') - print(f'- Mean: {d[\"mean_s\"]}s ± {d[\"stdev_s\"]}s') - print(f'- Range: {d[\"min_s\"]}s – {d[\"max_s\"]}s') - " >> "$GITHUB_STEP_SUMMARY" - env: - UV_PYTHON: ${{ matrix.python-version }} - - - name: Checkout base branch - run: git checkout ${{ github.event.pull_request.base.sha }} - - - name: Install base branch - run: uv sync --all-extras --no-dev - env: - UV_PYTHON: ${{ matrix.python-version }} - - - name: Benchmark base branch - id: base - run: | - result=$(uv run python scripts/benchmark_import_time.py --runs 5 --json 2>/dev/null || echo '{"median_s": 0}') - echo "result=$result" >> "$GITHUB_OUTPUT" - echo "base_median=$(echo $result | python3 -c 'import sys,json; print(json.load(sys.stdin)["median_s"])')" >> "$GITHUB_OUTPUT" - echo "### Base Branch Import Time" >> "$GITHUB_STEP_SUMMARY" - echo "$result" | python3 -c " - import sys, json - d = json.load(sys.stdin) - if d.get('median_s', 0) > 0: - print(f'- Median: {d[\"median_s\"]}s') - else: - print('- Benchmark script not present on base branch (skip comparison)') - " >> "$GITHUB_STEP_SUMMARY" - env: - UV_PYTHON: ${{ matrix.python-version }} - - - name: Compare and gate - run: | - pr_median=${{ steps.pr.outputs.pr_median }} - base_median=${{ steps.base.outputs.base_median }} - - python3 -c " - pr = float('$pr_median') - base = float('$base_median') - - if base <= 0: - print('⏭️ No base benchmark available — skipping comparison.') - exit(0) - - change_pct = ((pr - base) / base) * 100 - print(f'Base: {base:.3f}s') - print(f'PR: {pr:.3f}s') - print(f'Change: {change_pct:+.1f}%') - print() - - if change_pct > 5: - print(f'❌ BLOCKED: Import time regressed by {change_pct:.1f}% (threshold: 5%)') - exit(1) - elif change_pct > 0: - print(f'⚠️ Slight regression ({change_pct:.1f}%) but within 5% threshold.') - else: - print(f'✅ Import time improved by {abs(change_pct):.1f}%') - " diff --git a/lib/crewai-files/src/crewai_files/__init__.py b/lib/crewai-files/src/crewai_files/__init__.py index 051eda5d45..8ed8d00532 100644 --- a/lib/crewai-files/src/crewai_files/__init__.py +++ b/lib/crewai-files/src/crewai_files/__init__.py @@ -152,4 +152,4 @@ "wrap_file_source", ] -__version__ = "1.14.3a2" +__version__ = "1.14.3a3" diff --git a/lib/crewai-tools/pyproject.toml b/lib/crewai-tools/pyproject.toml index 2d3b1bba2a..40e0fb9517 100644 --- a/lib/crewai-tools/pyproject.toml +++ b/lib/crewai-tools/pyproject.toml @@ -10,7 +10,7 @@ requires-python = ">=3.10, <3.14" dependencies = [ "pytube~=15.0.0", "requests>=2.33.0,<3", - "crewai==1.14.3a2", + "crewai==1.14.3a3", "tiktoken~=0.8.0", "beautifulsoup4~=4.13.4", "python-docx~=1.2.0", diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py index e036874845..af9de2437b 100644 --- a/lib/crewai-tools/src/crewai_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/__init__.py @@ -321,4 +321,4 @@ "ZapierActionTools", ] -__version__ = "1.14.3a2" +__version__ = "1.14.3a3" diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index cbf0178010..24f01ea908 100644 --- a/lib/crewai/pyproject.toml +++ b/lib/crewai/pyproject.toml @@ -55,7 +55,7 @@ Repository = "https://github.com/crewAIInc/crewAI" [project.optional-dependencies] tools = [ - "crewai-tools==1.14.3a2", + "crewai-tools==1.14.3a3", ] embeddings = [ "tiktoken~=0.8.0" diff --git a/lib/crewai/src/crewai/__init__.py b/lib/crewai/src/crewai/__init__.py index 8d1587056f..176b0ca617 100644 --- a/lib/crewai/src/crewai/__init__.py +++ b/lib/crewai/src/crewai/__init__.py @@ -48,7 +48,7 @@ def filtered_warn( _suppress_pydantic_deprecation_warnings() -__version__ = "1.14.3a2" +__version__ = "1.14.3a3" _LAZY_IMPORTS: dict[str, tuple[str, str]] = { "Memory": ("crewai.memory.unified_memory", "Memory"), diff --git a/lib/crewai/src/crewai/cli/templates/crew/pyproject.toml b/lib/crewai/src/crewai/cli/templates/crew/pyproject.toml index 93ee876919..65225c0fb0 100644 --- a/lib/crewai/src/crewai/cli/templates/crew/pyproject.toml +++ b/lib/crewai/src/crewai/cli/templates/crew/pyproject.toml @@ -5,7 +5,7 @@ description = "{{name}} using crewAI" authors = [{ name = "Your Name", email = "you@example.com" }] requires-python = ">=3.10,<3.14" dependencies = [ - "crewai[tools]==1.14.3a2" + "crewai[tools]==1.14.3a3" ] [project.scripts] diff --git a/lib/crewai/src/crewai/cli/templates/flow/pyproject.toml b/lib/crewai/src/crewai/cli/templates/flow/pyproject.toml index a7f5747bc7..0b0760f5d1 100644 --- a/lib/crewai/src/crewai/cli/templates/flow/pyproject.toml +++ b/lib/crewai/src/crewai/cli/templates/flow/pyproject.toml @@ -5,7 +5,7 @@ description = "{{name}} using crewAI" authors = [{ name = "Your Name", email = "you@example.com" }] requires-python = ">=3.10,<3.14" dependencies = [ - "crewai[tools]==1.14.3a2" + "crewai[tools]==1.14.3a3" ] [project.scripts] diff --git a/lib/crewai/src/crewai/cli/templates/tool/pyproject.toml b/lib/crewai/src/crewai/cli/templates/tool/pyproject.toml index cac3afab3b..4dd6b344c8 100644 --- a/lib/crewai/src/crewai/cli/templates/tool/pyproject.toml +++ b/lib/crewai/src/crewai/cli/templates/tool/pyproject.toml @@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}" readme = "README.md" requires-python = ">=3.10,<3.14" dependencies = [ - "crewai[tools]==1.14.3a2" + "crewai[tools]==1.14.3a3" ] [tool.crewai] diff --git a/lib/devtools/src/crewai_devtools/__init__.py b/lib/devtools/src/crewai_devtools/__init__.py index 14470c7426..9574be2db7 100644 --- a/lib/devtools/src/crewai_devtools/__init__.py +++ b/lib/devtools/src/crewai_devtools/__init__.py @@ -1,3 +1,3 @@ """CrewAI development tools.""" -__version__ = "1.14.3a2" +__version__ = "1.14.3a3" diff --git a/scripts/benchmark_import_time.py b/scripts/benchmark_import_time.py deleted file mode 100755 index e44b2272a0..0000000000 --- a/scripts/benchmark_import_time.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 -"""Benchmark `import crewai` cold start time. - -Usage: - python scripts/benchmark_import_time.py [--runs N] [--json] - -Spawns a fresh Python subprocess for each run to ensure cold imports. -Prints median, mean, min, max across all runs. -With --json, outputs machine-readable results for CI. -""" -import argparse -import json -import statistics -import subprocess -import sys - - -IMPORT_SCRIPT = "import time; t0 = time.perf_counter(); import crewai; print(time.perf_counter() - t0)" - - -def measure_import(python: str = sys.executable) -> float: - """Run a single cold-import measurement in a subprocess.""" - result = subprocess.run( - [python, "-c", IMPORT_SCRIPT], - capture_output=True, - text=True, - env={"PATH": "", "VIRTUAL_ENV": "", "PYTHONPATH": ""}, - timeout=30, - ) - if result.returncode != 0: - raise RuntimeError(f"Import failed: {result.stderr.strip()}") - return float(result.stdout.strip()) - - -def main(): - parser = argparse.ArgumentParser(description="Benchmark crewai import time") - parser.add_argument("--runs", type=int, default=5, help="Number of runs (default: 5)") - parser.add_argument("--json", action="store_true", help="Output JSON for CI") - parser.add_argument("--threshold", type=float, default=None, - help="Fail if median exceeds this value (seconds)") - args = parser.parse_args() - - times = [] - for i in range(args.runs): - t = measure_import() - times.append(t) - if not args.json: - print(f" Run {i + 1}: {t:.3f}s") - - median = statistics.median(times) - mean = statistics.mean(times) - stdev = statistics.stdev(times) if len(times) > 1 else 0.0 - - result = { - "runs": args.runs, - "median_s": round(median, 3), - "mean_s": round(mean, 3), - "stdev_s": round(stdev, 3), - "min_s": round(min(times), 3), - "max_s": round(max(times), 3), - } - - if args.json: - print(json.dumps(result)) - else: - print(f"\n Median: {median:.3f}s") - print(f" Mean: {mean:.3f}s ± {stdev:.3f}s") - print(f" Range: {min(times):.3f}s – {max(times):.3f}s") - - if args.threshold and median > args.threshold: - print(f"\n ❌ FAILED: median {median:.3f}s exceeds threshold {args.threshold:.3f}s") - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/uv.lock b/uv.lock index 768be7983f..461c859a46 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-04-23T07:00:00Z" +exclude-newer = "2026-04-22T16:00:00Z" [manifest] members = [ @@ -510,6 +510,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/d6/8ebcd05b01a580f086ac9a97fb9fac65c09a4b012161cc97c21a336e880b/azure_core-1.39.0-py3-none-any.whl", hash = "sha256:4ac7b70fab5438c3f68770649a78daf97833caa83827f91df9c14e0e0ea7d34f", size = 218318, upload-time = "2026-03-19T01:31:31.25Z" }, ] +[[package]] +name = "azure-identity" +version = "1.25.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "cryptography" }, + { name = "msal" }, + { name = "msal-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/0e/3a63efb48aa4a5ae2cfca61ee152fbcb668092134d3eb8bfda472dd5c617/azure_identity-1.25.3.tar.gz", hash = "sha256:ab23c0d63015f50b630ef6c6cf395e7262f439ce06e5d07a64e874c724f8d9e6", size = 286304, upload-time = "2026-03-13T01:12:20.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/9a/417b3a533e01953a7c618884df2cb05a71e7b68bdbce4fbdb62349d2a2e8/azure_identity-1.25.3-py3-none-any.whl", hash = "sha256:f4d0b956a8146f30333e071374171f3cfa7bdb8073adb8c3814b65567aa7447c", size = 192138, upload-time = "2026-03-13T01:12:22.951Z" }, +] + [[package]] name = "backoff" version = "2.2.1" @@ -1305,6 +1321,7 @@ aws = [ ] azure-ai-inference = [ { name = "azure-ai-inference" }, + { name = "azure-identity" }, ] bedrock = [ { name = "boto3" }, @@ -1359,6 +1376,7 @@ requires-dist = [ { name = "anthropic", marker = "extra == 'anthropic'", specifier = "~=0.73.0" }, { name = "appdirs", specifier = "~=1.4.4" }, { name = "azure-ai-inference", marker = "extra == 'azure-ai-inference'", specifier = "~=1.0.0b9" }, + { name = "azure-identity", marker = "extra == 'azure-ai-inference'", specifier = ">=1.17.0,<2" }, { name = "boto3", marker = "extra == 'aws'", specifier = "~=1.42.79" }, { name = "boto3", marker = "extra == 'bedrock'", specifier = "~=1.42.79" }, { name = "chromadb", specifier = "~=1.1.0" }, @@ -4484,6 +4502,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] +[[package]] +name = "msal" +version = "1.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/cb/b02b0f748ac668922364ccb3c3bff5b71628a05f5adfec2ba2a5c3031483/msal-1.36.0.tar.gz", hash = "sha256:3f6a4af2b036b476a4215111c4297b4e6e236ed186cd804faefba23e4990978b", size = 174217, upload-time = "2026-04-09T10:20:33.525Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/d3/414d1f0a5f6f4fe5313c2b002c54e78a3332970feb3f5fed14237aa17064/msal-1.36.0-py3-none-any.whl", hash = "sha256:36ecac30e2ff4322d956029aabce3c82301c29f0acb1ad89b94edcabb0e58ec4", size = 121547, upload-time = "2026-04-09T10:20:32.336Z" }, +] + +[[package]] +name = "msal-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, +] + [[package]] name = "msgpack" version = "1.1.2" From bc2fb715601dbee16fbc72beb4092038afc92a84 Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Thu, 23 Apr 2026 05:11:06 +0800 Subject: [PATCH 4/4] docs: update changelog and version for v1.14.3a3 --- docs/ar/changelog.mdx | 26 ++++++++++++++++++++++++++ docs/en/changelog.mdx | 26 ++++++++++++++++++++++++++ docs/ko/changelog.mdx | 26 ++++++++++++++++++++++++++ docs/pt-BR/changelog.mdx | 26 ++++++++++++++++++++++++++ 4 files changed, 104 insertions(+) diff --git a/docs/ar/changelog.mdx b/docs/ar/changelog.mdx index eb714117d1..acbb285bf2 100644 --- a/docs/ar/changelog.mdx +++ b/docs/ar/changelog.mdx @@ -4,6 +4,32 @@ description: "تحديثات المنتج والتحسينات وإصلاحات icon: "clock" mode: "wide" --- + + ## v1.14.3a3 + + [عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.3a3) + + ## ما الذي تغير + + ### الميزات + - إضافة دعم لـ e2b + - تنفيذ التراجع إلى DefaultAzureCredential عند عدم توفير مفتاح API + + ### إصلاحات الأخطاء + - ترقية lxml إلى >=6.1.0 لمعالجة مشكلة الأمان GHSA-vfmq-68hx-4jfw + + ### الوثائق + - إزالة الأسئلة الشائعة حول التسعير من صفحة البناء باستخدام الذكاء الاصطناعي عبر جميع اللغات + + ### الأداء + - تحسين وقت بدء التشغيل البارد بنسبة ~29% من خلال التحميل الكسول لمجموعة أدوات MCP وأنواع الأحداث + + ## المساهمون + + @alex-clawd, @github-advanced-security[bot], @greysonlalonde, @iris-clawd, @lorenzejay, @mattatcha + + + ## v1.14.3a2 diff --git a/docs/en/changelog.mdx b/docs/en/changelog.mdx index 5fdd624ff0..8fc991bbf0 100644 --- a/docs/en/changelog.mdx +++ b/docs/en/changelog.mdx @@ -4,6 +4,32 @@ description: "Product updates, improvements, and bug fixes for CrewAI" icon: "clock" mode: "wide" --- + + ## v1.14.3a3 + + [View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.3a3) + + ## What's Changed + + ### Features + - Add support for e2b + - Implement fallback to DefaultAzureCredential when no API key is provided + + ### Bug Fixes + - Upgrade lxml to >=6.1.0 to address security issue GHSA-vfmq-68hx-4jfw + + ### Documentation + - Remove pricing FAQ from build-with-ai page across all locales + + ### Performance + - Improve cold start time by ~29% through lazy-loading of MCP SDK and event types + + ## Contributors + + @alex-clawd, @github-advanced-security[bot], @greysonlalonde, @iris-clawd, @lorenzejay, @mattatcha + + + ## v1.14.3a2 diff --git a/docs/ko/changelog.mdx b/docs/ko/changelog.mdx index f744341ebc..26c63cad65 100644 --- a/docs/ko/changelog.mdx +++ b/docs/ko/changelog.mdx @@ -4,6 +4,32 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정" icon: "clock" mode: "wide" --- + + ## v1.14.3a3 + + [GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.3a3) + + ## 변경 사항 + + ### 기능 + - e2b 지원 추가 + - API 키가 제공되지 않을 경우 DefaultAzureCredential로 대체 구현 + + ### 버그 수정 + - 보안 문제 GHSA-vfmq-68hx-4jfw를 해결하기 위해 lxml을 >=6.1.0으로 업그레이드 + + ### 문서 + - 모든 지역에서 build-with-ai 페이지의 가격 FAQ 제거 + + ### 성능 + - MCP SDK 및 이벤트 유형의 지연 로딩을 통해 콜드 스타트 시간을 약 29% 개선 + + ## 기여자 + + @alex-clawd, @github-advanced-security[bot], @greysonlalonde, @iris-clawd, @lorenzejay, @mattatcha + + + ## v1.14.3a2 diff --git a/docs/pt-BR/changelog.mdx b/docs/pt-BR/changelog.mdx index ed14c66db3..4c7a0234cb 100644 --- a/docs/pt-BR/changelog.mdx +++ b/docs/pt-BR/changelog.mdx @@ -4,6 +4,32 @@ description: "Atualizações de produto, melhorias e correções do CrewAI" icon: "clock" mode: "wide" --- + + ## v1.14.3a3 + + [Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.3a3) + + ## O que Mudou + + ### Recursos + - Adicionar suporte para e2b + - Implementar fallback para DefaultAzureCredential quando nenhuma chave de API for fornecida + + ### Correções de Bugs + - Atualizar lxml para >=6.1.0 para resolver problema de segurança GHSA-vfmq-68hx-4jfw + + ### Documentação + - Remover FAQ de preços da página build-with-ai em todos os locais + + ### Desempenho + - Melhorar o tempo de inicialização a frio em ~29% através do carregamento preguiçoso do SDK MCP e tipos de eventos + + ## Contributors + + @alex-clawd, @github-advanced-security[bot], @greysonlalonde, @iris-clawd, @lorenzejay, @mattatcha + + + ## v1.14.3a2