diff --git a/lib/crewai-tools/pyproject.toml b/lib/crewai-tools/pyproject.toml index bd2e638f5a..eb78f01d48 100644 --- a/lib/crewai-tools/pyproject.toml +++ b/lib/crewai-tools/pyproject.toml @@ -139,6 +139,10 @@ contextual = [ "contextual-client>=0.1.0", "nest-asyncio>=1.6.0", ] +boxlite = [ + "boxlite[sync]>=0.8.2,<1.0", +] + daytona = [ "daytona~=0.140.0", ] diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py index cb71de1d64..a421573259 100644 --- a/lib/crewai-tools/src/crewai_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/__init__.py @@ -10,6 +10,11 @@ from crewai_tools.tools.ai_mind_tool.ai_mind_tool import AIMindTool from crewai_tools.tools.apify_actors_tool.apify_actors_tool import ApifyActorsTool from crewai_tools.tools.arxiv_paper_tool.arxiv_paper_tool import ArxivPaperTool +from crewai_tools.tools.boxlite_sandbox_tool import ( + BoxLiteExecTool, + BoxLiteFileTool, + BoxLitePythonTool, +) from crewai_tools.tools.brave_search_tool.brave_image_tool import BraveImageSearchTool from crewai_tools.tools.brave_search_tool.brave_llm_context_tool import ( BraveLLMContextTool, @@ -224,6 +229,9 @@ "ArxivPaperTool", "BedrockInvokeAgentTool", "BedrockKBRetrieverTool", + "BoxLiteExecTool", + "BoxLiteFileTool", + "BoxLitePythonTool", "BraveImageSearchTool", "BraveLLMContextTool", "BraveLocalPOIsDescriptionTool", diff --git a/lib/crewai-tools/src/crewai_tools/tools/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/__init__.py index 18bf4e5638..971e9e7dac 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/tools/__init__.py @@ -1,6 +1,11 @@ from crewai_tools.tools.ai_mind_tool.ai_mind_tool import AIMindTool from crewai_tools.tools.apify_actors_tool.apify_actors_tool import ApifyActorsTool from crewai_tools.tools.arxiv_paper_tool.arxiv_paper_tool import ArxivPaperTool +from crewai_tools.tools.boxlite_sandbox_tool import ( + BoxLiteExecTool, + BoxLiteFileTool, + BoxLitePythonTool, +) from crewai_tools.tools.brave_search_tool.brave_image_tool import BraveImageSearchTool from crewai_tools.tools.brave_search_tool.brave_llm_context_tool import ( BraveLLMContextTool, @@ -209,6 +214,9 @@ "AIMindTool", "ApifyActorsTool", "ArxivPaperTool", + "BoxLiteExecTool", + "BoxLiteFileTool", + "BoxLitePythonTool", "BraveImageSearchTool", "BraveLLMContextTool", "BraveLocalPOIsDescriptionTool", diff --git a/lib/crewai-tools/src/crewai_tools/tools/boxlite_sandbox_tool/README.md b/lib/crewai-tools/src/crewai_tools/tools/boxlite_sandbox_tool/README.md new file mode 100644 index 0000000000..680546af98 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/boxlite_sandbox_tool/README.md @@ -0,0 +1,82 @@ +# BoxLite Sandbox Tools + +Run shell commands, execute Python, and manage files inside a [BoxLite](https://boxlite.ai/) micro-VM. BoxLite boots an OCI image inside a hardware-isolated micro-VM **on the local host**, so — unlike the E2B and Daytona sandbox tools — there is no API key, account, or cloud round-trip. It is a self-hosted option for agent-driven code execution with a VM-strength isolation boundary. + +Three tools are provided so you can pick what the agent actually needs: + +- **`BoxLiteExecTool`** — run a shell command, returning exit code, stdout, and stderr. +- **`BoxLitePythonTool`** — run a block of Python (`python3`) and return its stdout, stderr, and exit code. +- **`BoxLiteFileTool`** — read / write / append / list / delete / mkdir / info / exists. + +## Installation + +```shell +uv add "crewai-tools[boxlite]" +# or +pip install "crewai-tools[boxlite]" +``` + +No API key is required. The host must support micro-VMs: + +| Platform | Requirement | +| --- | --- | +| macOS 12+ | Apple Silicon (`Hypervisor.framework`) | +| Linux | KVM enabled (`/dev/kvm` accessible) | + +## Box lifecycle + +All three tools share the same lifecycle controls from `BoxLiteBaseTool`: + +| Mode | When the box is created | When it is removed | +| --- | --- | --- | +| **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()` | + +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 — typical when pairing `BoxLiteFileTool` with `BoxLiteExecTool`. + +Configure the box with `image` (default `python:slim`), `cpus`, and `memory_mib`. + +## Examples + +### One-shot Python execution (ephemeral) + +```python +from crewai_tools import BoxLitePythonTool + +tool = BoxLitePythonTool() +result = tool.run(code="print(sum(range(10)))") +# {"exit_code": 0, "stdout": "45\n", "stderr": ""} +``` + +### Multi-step shell session (persistent) + +```python +from crewai_tools import BoxLiteExecTool, BoxLiteFileTool + +exec_tool = BoxLiteExecTool(persistent=True, image="python:slim") +file_tool = BoxLiteFileTool(persistent=True, image="python:slim") + +# Each tool keeps its own persistent box for the life of the process. +file_tool.run(action="write", path="/tmp/data.txt", content="1\n2\n3\n") +exec_tool.run(command="wc -l < /tmp/data.txt") +``` + +### Give the tools to an agent + +```python +from crewai import Agent +from crewai_tools import BoxLiteExecTool, BoxLitePythonTool + +analyst = Agent( + role="Data Analyst", + goal="Run code safely in an isolated micro-VM", + backstory="Executes untrusted, model-generated code without touching the host.", + tools=[BoxLitePythonTool(), BoxLiteExecTool()], +) +``` + +## Notes and differences from E2B/Daytona + +- **Local only.** Boxes run on the host; there is no remote/attach-by-id mode, because BoxLite's synchronous API does not expose one with the rich exec (cwd/env/timeout) these tools use. +- **Text results only.** `BoxLitePythonTool` returns stdout/stderr/exit code; there is no Jupyter kernel, so rich results (charts, dataframes) are not returned. +- **Files over shell.** `BoxLiteFileTool` implements filesystem operations via the box's shell (content is transferred as base64), since BoxLite exposes command execution rather than a files API. diff --git a/lib/crewai-tools/src/crewai_tools/tools/boxlite_sandbox_tool/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/boxlite_sandbox_tool/__init__.py new file mode 100644 index 0000000000..bf29823189 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/boxlite_sandbox_tool/__init__.py @@ -0,0 +1,14 @@ +from crewai_tools.tools.boxlite_sandbox_tool.boxlite_base_tool import BoxLiteBaseTool +from crewai_tools.tools.boxlite_sandbox_tool.boxlite_exec_tool import BoxLiteExecTool +from crewai_tools.tools.boxlite_sandbox_tool.boxlite_file_tool import BoxLiteFileTool +from crewai_tools.tools.boxlite_sandbox_tool.boxlite_python_tool import ( + BoxLitePythonTool, +) + + +__all__ = [ + "BoxLiteBaseTool", + "BoxLiteExecTool", + "BoxLiteFileTool", + "BoxLitePythonTool", +] diff --git a/lib/crewai-tools/src/crewai_tools/tools/boxlite_sandbox_tool/boxlite_base_tool.py b/lib/crewai-tools/src/crewai_tools/tools/boxlite_sandbox_tool/boxlite_base_tool.py new file mode 100644 index 0000000000..016f075ff4 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/boxlite_sandbox_tool/boxlite_base_tool.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +import atexit +import logging +import threading +from typing import Any, ClassVar + +from crewai.tools import BaseTool +from pydantic import ConfigDict, Field, PrivateAttr + + +logger = logging.getLogger(__name__) + + +class BoxLiteBaseTool(BaseTool): + """Shared base for tools that act on a BoxLite micro-VM box. + + BoxLite boots an OCI image inside a hardware-isolated micro-VM on the local + host, so — unlike the E2B and Daytona sandbox tools — no API key or cloud + account is required. The host must support micro-VMs: macOS 12+ on Apple + Silicon, or Linux with KVM (``/dev/kvm``). + + Lifecycle modes (mirrors the E2B/Daytona sandbox tools): + - ``persistent=False`` (default): create a fresh box per ``_run`` call and + remove it when the call returns. Stateless and safe — nothing leaks if + the agent forgets cleanup. + - ``persistent=True``: lazily create one box on first use, cache it on the + instance, and remove it at process exit. Cheaper across many calls and + lets files or installed packages carry over between them. + + Boxes are addressed only through this tool instance; there is no attach to a + box created elsewhere, because BoxLite's synchronous API does not expose + one with the rich exec (cwd/env/timeout) these tools rely on. + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + package_dependencies: list[str] = Field(default_factory=lambda: ["boxlite[sync]"]) + + image: str = Field( + default="python:slim", + description=( + "OCI image the micro-VM boots from. Any Docker/OCI reference works " + "(e.g. 'python:slim', 'ubuntu:24.04')." + ), + ) + cpus: int | None = Field( + default=None, + description="vCPUs for the box. Defaults to the BoxLite runtime default.", + ) + memory_mib: int | None = Field( + default=None, + description="Memory in MiB for the box. Defaults to the BoxLite runtime default.", + ) + persistent: bool = Field( + default=False, + description=( + "If True, reuse one box across all calls to this tool instance and " + "remove it at process exit. Default False creates and removes a " + "fresh box per call." + ), + ) + + _persistent_box: 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_boxlite(cls) -> Any: + """Return the ``boxlite`` module. + + Imported lazily and cached so that importing this tool (e.g. for spec + generation or ``from crewai_tools import ...``) never requires the + optional ``boxlite`` dependency to be installed. + """ + cached = cls._sdk_cache.get("boxlite") + if cached is not None: + return cached + try: + import boxlite # type: ignore[import-untyped] + except ImportError as exc: + raise ImportError( + "The 'boxlite' package is required for BoxLite sandbox tools. " + "Install it with: uv add 'boxlite[sync]' (or) " + "pip install 'boxlite[sync]'" + ) from exc + cls._sdk_cache["boxlite"] = boxlite + return boxlite + + def _new_box(self) -> Any: + """Create and start a fresh box from this tool's configuration.""" + boxlite = self._import_boxlite() + box = boxlite.SyncSimpleBox( + image=self.image, + cpus=self.cpus, + memory_mib=self.memory_mib, + auto_remove=True, + ) + # SyncSimpleBox exposes its lifecycle only via the context-manager + # protocol; enter it here and exit in _release_box/close so the box can + # outlive a single ``with`` block (required for persistent mode). + box.__enter__() + return box + + def _acquire_box(self) -> tuple[Any, bool]: + """Return ``(box, should_remove_after_use)``.""" + if self.persistent: + with self._lock: + if self._persistent_box is None: + self._persistent_box = self._new_box() + if not self._cleanup_registered: + atexit.register(self.close) + self._cleanup_registered = True + return self._persistent_box, False + return self._new_box(), True + + def _release_box(self, box: Any, should_remove: bool) -> None: + if not should_remove: + return + try: + box.__exit__(None, None, None) + except Exception: + logger.debug( + "Best-effort box cleanup failed after ephemeral use; " + "the micro-VM may need manual removal.", + exc_info=True, + ) + + def close(self) -> None: + """Remove the cached persistent box if one exists.""" + with self._lock: + box = self._persistent_box + self._persistent_box = None + if box is None: + return + try: + box.__exit__(None, None, None) + except Exception: + logger.debug( + "Best-effort persistent box cleanup failed at close(); " + "the micro-VM may need manual removal.", + exc_info=True, + ) + + @staticmethod + def _result_dict(result: Any) -> dict[str, Any]: + """Normalize a BoxLite ExecResult into a plain JSON-friendly dict.""" + return { + "exit_code": result.exit_code, + "stdout": result.stdout, + "stderr": result.stderr, + } diff --git a/lib/crewai-tools/src/crewai_tools/tools/boxlite_sandbox_tool/boxlite_exec_tool.py b/lib/crewai-tools/src/crewai_tools/tools/boxlite_sandbox_tool/boxlite_exec_tool.py new file mode 100644 index 0000000000..1b125bbf9a --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/boxlite_sandbox_tool/boxlite_exec_tool.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from builtins import type as type_ +from typing import Any + +from pydantic import BaseModel, Field + +from crewai_tools.tools.boxlite_sandbox_tool.boxlite_base_tool import BoxLiteBaseTool + + +class BoxLiteExecToolSchema(BaseModel): + command: str = Field(..., description="Shell command to execute in the box.") + cwd: str | None = Field( + default=None, + description="Working directory to run the command in. Defaults to the box's configured workdir.", + ) + 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 BoxLiteExecTool(BoxLiteBaseTool): + """Run a shell command inside a BoxLite micro-VM.""" + + name: str = "BoxLite Sandbox Exec" + description: str = ( + "Execute a shell command inside a BoxLite micro-VM 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] = BoxLiteExecToolSchema + + def _run( + self, + command: str, + cwd: str | None = None, + envs: dict[str, str] | None = None, + timeout: float | None = None, + ) -> Any: + box, should_remove = self._acquire_box() + try: + result = box.exec( + "/bin/sh", "-c", command, env=envs, cwd=cwd, timeout=timeout + ) + return self._result_dict(result) + finally: + self._release_box(box, should_remove) diff --git a/lib/crewai-tools/src/crewai_tools/tools/boxlite_sandbox_tool/boxlite_file_tool.py b/lib/crewai-tools/src/crewai_tools/tools/boxlite_sandbox_tool/boxlite_file_tool.py new file mode 100644 index 0000000000..f9379facd8 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/boxlite_sandbox_tool/boxlite_file_tool.py @@ -0,0 +1,191 @@ +from __future__ import annotations + +import base64 +from builtins import type as type_ +from typing import Any, Literal + +from pydantic import BaseModel, Field, model_validator + +from crewai_tools.tools.boxlite_sandbox_tool.boxlite_base_tool import BoxLiteBaseTool + + +FileAction = Literal[ + "read", "write", "append", "list", "delete", "mkdir", "info", "exists" +] + + +class BoxLiteFileToolSchema(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 box.") + 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 write the decoded " + "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) -> BoxLiteFileToolSchema: + 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 BoxLiteFileTool(BoxLiteBaseTool): + """Read, write, and manage files inside a BoxLite micro-VM. + + BoxLite exposes command execution rather than a files API, so these + operations run over the box's shell, with content transferred as base64 so + binary data and arbitrary text survive intact. Most useful with + ``persistent=True``; with the default ephemeral mode the box — and its + files — disappear when the call finishes. + """ + + name: str = "BoxLite Sandbox Files" + description: str = ( + "Perform filesystem operations inside a BoxLite micro-VM: 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] = BoxLiteFileToolSchema + + def _run( + self, + action: FileAction, + path: str, + content: str | None = None, + binary: bool = False, + depth: int = 1, + ) -> Any: + box, should_remove = self._acquire_box() + try: + if action == "read": + return self._read(box, path, binary=binary) + if action == "write": + return self._write( + box, path, content or "", binary=binary, append=False + ) + if action == "append": + return self._write(box, path, content or "", binary=binary, append=True) + if action == "list": + return self._list(box, path, depth=depth) + if action == "delete": + return self._simple(box, "deleted", path, 'rm -rf -- "$1"') + if action == "mkdir": + return self._simple(box, "created", path, 'mkdir -p -- "$1"') + if action == "info": + return self._info(box, path) + if action == "exists": + return self._exists(box, path) + raise ValueError(f"Unknown action: {action}") + finally: + self._release_box(box, should_remove) + + @staticmethod + def _sh(box: Any, script: str, *sh_args: str) -> Any: + # $0 is a placeholder name ("sh"); the caller's values arrive as the + # positional parameters "$1", "$2", ... so nothing is re-parsed by the + # shell and paths/content need no escaping. + return box.exec("/bin/sh", "-c", script, "sh", *sh_args) + + def _read(self, box: Any, path: str, *, binary: bool) -> dict[str, Any]: + if binary: + result = self._sh(box, 'base64 -- "$1"', path) + if result.exit_code != 0: + return {"path": path, "error": (result.stderr or "read failed").strip()} + return { + "path": path, + "encoding": "base64", + "content": result.stdout.strip(), + } + result = self._sh(box, 'cat -- "$1"', path) + if result.exit_code != 0: + return {"path": path, "error": (result.stderr or "read failed").strip()} + return {"path": path, "encoding": "utf-8", "content": result.stdout} + + def _write( + self, box: Any, path: str, content: str, *, binary: bool, append: bool + ) -> dict[str, Any]: + payload = ( + content + if binary + else base64.b64encode(content.encode("utf-8")).decode("ascii") + ) + redirect = ">>" if append else ">" + script = ( + 'mkdir -p -- "$(dirname -- "$1")" && ' + f'printf %s "$2" | base64 -d {redirect} "$1"' + ) + result = self._sh(box, script, path, payload) + if result.exit_code != 0: + return {"path": path, "error": (result.stderr or "write failed").strip()} + try: + written = len(base64.b64decode(payload)) + except (ValueError, TypeError): + written = 0 + return { + "status": "appended" if append else "written", + "path": path, + "bytes": written, + } + + def _list(self, box: Any, path: str, *, depth: int) -> dict[str, Any]: + result = self._sh( + box, 'find -- "$1" -maxdepth "$2" -mindepth 1', path, str(depth) + ) + if result.exit_code != 0: + return {"path": path, "error": (result.stderr or "list failed").strip()} + entries = [line for line in result.stdout.splitlines() if line] + return {"path": path, "entries": entries} + + def _info(self, box: Any, path: str) -> dict[str, Any]: + result = self._sh(box, 'ls -ldn -- "$1"', path) + if result.exit_code != 0: + return {"path": path, "error": (result.stderr or "info failed").strip()} + return {"path": path, "info": result.stdout.strip()} + + def _exists(self, box: Any, path: str) -> dict[str, Any]: + result = self._sh(box, '[ -e "$1" ] && echo true || echo false', path) + return {"path": path, "exists": result.stdout.strip() == "true"} + + def _simple(self, box: Any, status: str, path: str, script: str) -> dict[str, Any]: + result = self._sh(box, script, path) + if result.exit_code != 0: + return { + "path": path, + "error": (result.stderr or "operation failed").strip(), + } + return {"status": status, "path": path} diff --git a/lib/crewai-tools/src/crewai_tools/tools/boxlite_sandbox_tool/boxlite_python_tool.py b/lib/crewai-tools/src/crewai_tools/tools/boxlite_sandbox_tool/boxlite_python_tool.py new file mode 100644 index 0000000000..2f52bbba10 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/boxlite_sandbox_tool/boxlite_python_tool.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from builtins import type as type_ +from typing import Any + +from pydantic import BaseModel, Field + +from crewai_tools.tools.boxlite_sandbox_tool.boxlite_base_tool import BoxLiteBaseTool + + +class BoxLitePythonToolSchema(BaseModel): + code: str = Field(..., description="Python source to execute inside the box.") + 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 BoxLitePythonTool(BoxLiteBaseTool): + """Run Python code inside a BoxLite micro-VM. + + Executes the code with the box image's ``python3`` and returns captured + stdout, stderr, and the process exit code. Pair with ``persistent=True`` + (and a Python image) to let imports and installed packages carry across + calls. Unlike the E2B code interpreter there is no Jupyter kernel, so rich + results (charts, dataframes) are not returned — only text streams. + """ + + name: str = "BoxLite Sandbox Python" + description: str = ( + "Execute a block of Python code inside a BoxLite micro-VM and return " + "captured stdout, stderr, and the process exit code. Use this for data " + "processing, quick scripts, or analysis that should run in an isolated " + "environment. The box image must provide python3 (the default " + "'python:slim' does)." + ) + args_schema: type_[BaseModel] = BoxLitePythonToolSchema + + def _run( + self, + code: str, + envs: dict[str, str] | None = None, + timeout: float | None = None, + ) -> Any: + box, should_remove = self._acquire_box() + try: + # Run through /bin/sh so python3 resolves on PATH regardless of the + # image's layout, and pass the source as a positional arg ($1) so no + # shell quoting is applied to the user's code. + result = box.exec( + "/bin/sh", + "-c", + 'python3 -c "$1"', + "sh", + code, + env=envs, + timeout=timeout, + ) + return self._result_dict(result) + finally: + self._release_box(box, should_remove) diff --git a/lib/crewai-tools/tool.specs.json b/lib/crewai-tools/tool.specs.json index 795fa932c4..937734d6f0 100644 --- a/lib/crewai-tools/tool.specs.json +++ b/lib/crewai-tools/tool.specs.json @@ -198,6 +198,436 @@ "type": "object" } }, + { + "description": "Execute a shell command inside a BoxLite micro-VM 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": [], + "humanized_name": "BoxLite 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 a BoxLite micro-VM.", + "properties": { + "cpus": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "vCPUs for the box. Defaults to the BoxLite runtime default.", + "title": "Cpus" + }, + "image": { + "default": "python:slim", + "description": "OCI image the micro-VM boots from. Any Docker/OCI reference works (e.g. 'python:slim', 'ubuntu:24.04').", + "title": "Image", + "type": "string" + }, + "memory_mib": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Memory in MiB for the box. Defaults to the BoxLite runtime default.", + "title": "Memory Mib" + }, + "persistent": { + "default": false, + "description": "If True, reuse one box across all calls to this tool instance and remove it at process exit. Default False creates and removes a fresh box per call.", + "title": "Persistent", + "type": "boolean" + } + }, + "required": [], + "title": "BoxLiteExecTool", + "type": "object" + }, + "name": "BoxLiteExecTool", + "package_dependencies": [ + "boxlite[sync]" + ], + "run_params_schema": { + "properties": { + "command": { + "description": "Shell command to execute in the box.", + "title": "Command", + "type": "string" + }, + "cwd": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Working directory to run the command in. Defaults to the box's configured workdir.", + "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": "BoxLiteExecToolSchema", + "type": "object" + } + }, + { + "description": "Perform filesystem operations inside a BoxLite micro-VM: 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": [], + "humanized_name": "BoxLite 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 a BoxLite micro-VM.\n\nBoxLite exposes command execution rather than a files API, so these\noperations run over the box's shell, with content transferred as base64 so\nbinary data and arbitrary text survive intact. Most useful with\n``persistent=True``; with the default ephemeral mode the box \u2014 and its\nfiles \u2014 disappear when the call finishes.", + "properties": { + "cpus": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "vCPUs for the box. Defaults to the BoxLite runtime default.", + "title": "Cpus" + }, + "image": { + "default": "python:slim", + "description": "OCI image the micro-VM boots from. Any Docker/OCI reference works (e.g. 'python:slim', 'ubuntu:24.04').", + "title": "Image", + "type": "string" + }, + "memory_mib": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Memory in MiB for the box. Defaults to the BoxLite runtime default.", + "title": "Memory Mib" + }, + "persistent": { + "default": false, + "description": "If True, reuse one box across all calls to this tool instance and remove it at process exit. Default False creates and removes a fresh box per call.", + "title": "Persistent", + "type": "boolean" + } + }, + "required": [], + "title": "BoxLiteFileTool", + "type": "object" + }, + "name": "BoxLiteFileTool", + "package_dependencies": [ + "boxlite[sync]" + ], + "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 write the decoded 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 box.", + "title": "Path", + "type": "string" + } + }, + "required": [ + "action", + "path" + ], + "title": "BoxLiteFileToolSchema", + "type": "object" + } + }, + { + "description": "Execute a block of Python code inside a BoxLite micro-VM and return captured stdout, stderr, and the process exit code. Use this for data processing, quick scripts, or analysis that should run in an isolated environment. The box image must provide python3 (the default 'python:slim' does).", + "env_vars": [], + "humanized_name": "BoxLite 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 a BoxLite micro-VM.\n\nExecutes the code with the box image's ``python3`` and returns captured\nstdout, stderr, and the process exit code. Pair with ``persistent=True``\n(and a Python image) to let imports and installed packages carry across\ncalls. Unlike the E2B code interpreter there is no Jupyter kernel, so rich\nresults (charts, dataframes) are not returned \u2014 only text streams.", + "properties": { + "cpus": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "vCPUs for the box. Defaults to the BoxLite runtime default.", + "title": "Cpus" + }, + "image": { + "default": "python:slim", + "description": "OCI image the micro-VM boots from. Any Docker/OCI reference works (e.g. 'python:slim', 'ubuntu:24.04').", + "title": "Image", + "type": "string" + }, + "memory_mib": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Memory in MiB for the box. Defaults to the BoxLite runtime default.", + "title": "Memory Mib" + }, + "persistent": { + "default": false, + "description": "If True, reuse one box across all calls to this tool instance and remove it at process exit. Default False creates and removes a fresh box per call.", + "title": "Persistent", + "type": "boolean" + } + }, + "required": [], + "title": "BoxLitePythonTool", + "type": "object" + }, + "name": "BoxLitePythonTool", + "package_dependencies": [ + "boxlite[sync]" + ], + "run_params_schema": { + "properties": { + "code": { + "description": "Python source to execute inside the box.", + "title": "Code", + "type": "string" + }, + "envs": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional environment variables for the run.", + "title": "Envs" + }, + "timeout": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum seconds to wait for the code to finish.", + "title": "Timeout" + } + }, + "required": [ + "code" + ], + "title": "BoxLitePythonToolSchema", + "type": "object" + } + }, { "description": "A tool that performs image searches using the Brave Search API. Results are returned as structured JSON data.", "env_vars": [