Skip to content

feat: add OpenSandbox tool for sandbox-based code execution#5755

Open
iris-clawd wants to merge 1 commit intomainfrom
feat/opensandbox-tool
Open

feat: add OpenSandbox tool for sandbox-based code execution#5755
iris-clawd wants to merge 1 commit intomainfrom
feat/opensandbox-tool

Conversation

@iris-clawd
Copy link
Copy Markdown
Contributor

@iris-clawd iris-clawd commented May 8, 2026

Summary

Adds OpenSandboxTool — a BaseTool subclass that lets CrewAI agents create and interact with isolated OpenSandbox containers. OpenSandbox is a CNCF-listed, self-hosted sandbox platform (Apache-2.0) supporting Docker and Kubernetes runtimes.

What agents can do with this tool

Action Description
run_command Execute a shell command inside the sandbox
read_file Read a file's contents from the sandbox filesystem
write_file Write content to a file in the sandbox
kill Terminate and release the sandbox

The same sandbox instance is reused across calls (lazy-created on first use), so agents can build up state across multiple tool invocations.

Configuration (env vars via EnvVar)

Variable Required Default Description
OPENSANDBOX_DOMAIN host:port of the OpenSandbox server
OPENSANDBOX_PROTOCOL http http or https
OPENSANDBOX_IMAGE python:3.12 Container image to launch
OPENSANDBOX_TIMEOUT_MINUTES 30 Sandbox idle timeout
OPENSANDBOX_API_KEY Auth key if server requires it

Files changed

  • lib/crewai/src/crewai/tools/opensandbox_tool.py — tool implementation (206 lines)
  • lib/crewai/tests/test_opensandbox_tool.py — 13 unit tests, all passing

Test results

13 passed in 3.48s

Notes

  • Requires pip install opensandbox (v0.1.8+). This is an optional dep — the tool raises a clear ImportError if the package isn't installed.
  • Async SDK bridged to sync _run via asyncio.run() with a ThreadPoolExecutor fallback for environments with a running event loop.
  • OPENSANDBOX_DOMAIN not set → raises ValueError with a descriptive message.
  • Lint: ruff check + ruff format clean.

Usage example

from crewai import Agent, Task, Crew
from crewai.tools.opensandbox_tool import OpenSandboxTool
import os

os.environ["OPENSANDBOX_DOMAIN"] = "localhost:8080"

sandbox_tool = OpenSandboxTool()

coder = Agent(
    role="Python Developer",
    goal="Write and run Python code in an isolated sandbox",
    tools=[sandbox_tool],
)

task = Task(
    description="Write a Python script that computes the first 10 Fibonacci numbers and run it.",
    agent=coder,
)

Crew(agents=[coder], tasks=[task]).kickoff()

Requested by: Lorenze Jay lorenze@crewai.com

Summary by CodeRabbit

Release Notes

  • New Features
    • Introduced OpenSandbox integration tool enabling command execution, file operations (read/write), and container lifecycle management.
    • Automatically provisions and reuses sandbox instances across multiple operations for efficiency.
    • Simple environment variable-based configuration for seamless integration.
    • Includes robust error handling with detailed error messages for troubleshooting.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

A new OpenSandbox tool integration for CrewAI enables sandboxed command execution, file I/O, and lifecycle management. The implementation includes async-to-sync bridging, lazy sandbox creation with caching, per-action handlers (run_command, read_file, write_file, kill), and comprehensive pytest tests with mocked SDK dependencies.

Changes

OpenSandbox Tool Integration

Layer / File(s) Summary
Input Schema and Tool Metadata
lib/crewai/src/crewai/tools/opensandbox_tool.py
OpenSandboxToolSchema defines action selector (run_command, read_file, write_file, kill) with optional parameters. OpenSandboxTool declares name, description, and environment variable configuration for domain, protocol, image, timeout, and API key.
Execution Dispatcher and Async Bridge
lib/crewai/src/crewai/tools/opensandbox_tool.py
_run validates required inputs per action and dispatches to async handlers, returning stringified results or error messages. _run_async bridges sync and async contexts via asyncio.run or single-thread executor.
Sandbox Lifecycle Management
lib/crewai/src/crewai/tools/opensandbox_tool.py
_build_connection_config constructs OpenSandbox config from environment variables (raises ValueError if domain is unset). _ensure_sandbox lazily creates and caches sandbox instance with image and timeout settings.
Action Handlers
lib/crewai/src/crewai/tools/opensandbox_tool.py
_run_command executes shell commands and formats stdout/stderr; _read_file retrieves file contents; _write_file writes files with mode 0o644; _kill terminates sandbox and clears cached instance. All handlers return error strings on exceptions.
Test Infrastructure
lib/crewai/tests/test_opensandbox_tool.py
Stubs opensandbox SDK modules to support lazy imports. Fixtures provide environment variables, mocked async sandbox with command/file/kill operations, and patched Sandbox.create. Helper _make_execution constructs mocked execution results.
Configuration and Input Validation Tests
lib/crewai/tests/test_opensandbox_tool.py
Test missing domain raises ValueError. Validate required parameters: command for run_command, path for read_file, both path and content for write_file.
Action Behavior Tests
lib/crewai/tests/test_opensandbox_tool.py
write_file formats "Wrote …" output and verifies entry data structure. kill reports "No sandbox to kill." when absent and clears state after use.
Integration and Error Handling Tests
lib/crewai/tests/test_opensandbox_tool.py
Verify sandbox reuse (SDK create called once per tool). SDK exceptions from commands.run wrapped as "OpenSandbox error" messages. Unknown action returns error message from _run.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A sandbox tool hops into view,
Commands run safe, both old and new,
Files read and written with care,
Async bridges everywhere,
Tests mock the SDK so true! 🏗️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately and concisely describes the main change: adding a new OpenSandbox tool for sandbox-based code execution, which is the primary objective of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/opensandbox-tool

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/crewai/src/crewai/tools/opensandbox_tool.py`:
- Around line 141-154: The _ensure_sandbox method has a race: multiple
concurrent callers can pass the initial if and each call will create its own
Sandbox; fix by introducing an asyncio.Lock on the instance (e.g.,
self._sandbox_lock initialized in __init__) and wrap the create logic in an
async with self._sandbox_lock: block, then re-check self._sandbox inside the
lock before calling Sandbox.create (use the same image/timeout/connection_config
logic and self._build_connection_config), so only one Sandbox.create is invoked
and the rest return the created self._sandbox.
- Around line 185-194: In _write_file, move the from opensandbox.models import
WriteEntry into the try block (so the ImportError from missing opensandbox is
caught) and add a dedicated except ImportError handler that returns a clear
error string; keep the existing general except Exception exc for runtime errors
from sandbox.files.write_files or _ensure_sandbox, and reference the same
symbols (_write_file, WriteEntry, sandbox.files.write_files, _ensure_sandbox)
when applying the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 27786ff5-130e-4403-8278-411b01ccc8a8

📥 Commits

Reviewing files that changed from the base of the PR and between e4a91cd and 516e4fd.

📒 Files selected for processing (2)
  • lib/crewai/src/crewai/tools/opensandbox_tool.py
  • lib/crewai/tests/test_opensandbox_tool.py

Comment on lines +141 to +154
async def _ensure_sandbox(self) -> Any:
if self._sandbox is not None:
return self._sandbox
from opensandbox import Sandbox

image = (os.getenv("OPENSANDBOX_IMAGE") or "python:3.12").strip()
timeout_minutes = int(os.getenv("OPENSANDBOX_TIMEOUT_MINUTES") or "30")
connection_config = self._build_connection_config()
self._sandbox = await Sandbox.create(
image,
timeout=timedelta(minutes=timeout_minutes),
connection_config=connection_config,
)
return self._sandbox
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Protect lazy sandbox creation from concurrent races.

_ensure_sandbox does a check-then-create without a lock, so concurrent calls can create multiple sandboxes and leak resources while only one is retained.

Suggested fix
@@
 import asyncio
 import concurrent.futures
 from datetime import timedelta
 import os
+import threading
 from typing import Any, Literal
@@
     _sandbox: Any = PrivateAttr(default=None)
+    _sandbox_init_lock: Any = PrivateAttr(default_factory=threading.Lock)
@@
     async def _ensure_sandbox(self) -> Any:
         if self._sandbox is not None:
             return self._sandbox
-        from opensandbox import Sandbox
-
-        image = (os.getenv("OPENSANDBOX_IMAGE") or "python:3.12").strip()
-        timeout_minutes = int(os.getenv("OPENSANDBOX_TIMEOUT_MINUTES") or "30")
-        connection_config = self._build_connection_config()
-        self._sandbox = await Sandbox.create(
-            image,
-            timeout=timedelta(minutes=timeout_minutes),
-            connection_config=connection_config,
-        )
+        with self._sandbox_init_lock:
+            if self._sandbox is not None:
+                return self._sandbox
+            from opensandbox import Sandbox
+
+            image = (os.getenv("OPENSANDBOX_IMAGE") or "python:3.12").strip()
+            timeout_minutes = int(os.getenv("OPENSANDBOX_TIMEOUT_MINUTES") or "30")
+            connection_config = self._build_connection_config()
+            self._sandbox = await Sandbox.create(
+                image,
+                timeout=timedelta(minutes=timeout_minutes),
+                connection_config=connection_config,
+            )
         return self._sandbox
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/tools/opensandbox_tool.py` around lines 141 - 154, The
_ensure_sandbox method has a race: multiple concurrent callers can pass the
initial if and each call will create its own Sandbox; fix by introducing an
asyncio.Lock on the instance (e.g., self._sandbox_lock initialized in __init__)
and wrap the create logic in an async with self._sandbox_lock: block, then
re-check self._sandbox inside the lock before calling Sandbox.create (use the
same image/timeout/connection_config logic and self._build_connection_config),
so only one Sandbox.create is invoked and the rest return the created
self._sandbox.

Comment on lines +185 to +194
async def _write_file(self, path: str, content: str) -> str:
from opensandbox.models import WriteEntry

try:
sandbox = await self._ensure_sandbox()
await sandbox.files.write_files(
[WriteEntry(path=path, data=content, mode=0o644)]
)
except Exception as exc:
return f"OpenSandbox error writing {path}: {exc}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify import location and exception handling in write_file
rg -n -C3 'async def _write_file|from opensandbox.models import WriteEntry|except ImportError|except Exception' lib/crewai/src/crewai/tools/opensandbox_tool.py

# Verify there is no current test covering missing opensandbox during write_file
rg -n -C2 'write_file|ImportError|ModuleNotFoundError' lib/crewai/tests/test_opensandbox_tool.py

Repository: crewAIInc/crewAI

Length of output: 2595


Move the WriteEntry import inside the try block to handle missing opensandbox gracefully.

The import at line 186 is outside the try block. If opensandbox is not installed, the ImportError will occur before entering the try block and won't be caught by the except handler, causing an unhandled exception instead of returning the controlled error message.

Move the import to line 189 (inside the try block) and add an except ImportError handler:

Suggested fix
 async def _write_file(self, path: str, content: str) -> str:
-    from opensandbox.models import WriteEntry
-
     try:
+        from opensandbox.models import WriteEntry
         sandbox = await self._ensure_sandbox()
         await sandbox.files.write_files(
             [WriteEntry(path=path, data=content, mode=0o644)]
         )
+    except ImportError as exc:
+        return f"OpenSandbox dependency error: {exc}"
     except Exception as exc:
         return f"OpenSandbox error writing {path}: {exc}"
     return f"Wrote {len(content)} bytes to {path}."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/tools/opensandbox_tool.py` around lines 185 - 194, In
_write_file, move the from opensandbox.models import WriteEntry into the try
block (so the ImportError from missing opensandbox is caught) and add a
dedicated except ImportError handler that returns a clear error string; keep the
existing general except Exception exc for runtime errors from
sandbox.files.write_files or _ensure_sandbox, and reference the same symbols
(_write_file, WriteEntry, sandbox.files.write_files, _ensure_sandbox) when
applying the change.

@iris-clawd
Copy link
Copy Markdown
Contributor Author

requested by Slack user <@U0773HGAUSJ> — friendly nudge from Iris: this PR is still open. Want to take a look or close it out?

4 similar comments
@iris-clawd
Copy link
Copy Markdown
Contributor Author

requested by Slack user <@U0773HGAUSJ> — friendly nudge from Iris: this PR is still open. Want to take a look or close it out?

@iris-clawd
Copy link
Copy Markdown
Contributor Author

requested by Slack user <@U0773HGAUSJ> — friendly nudge from Iris: this PR is still open. Want to take a look or close it out?

@iris-clawd
Copy link
Copy Markdown
Contributor Author

requested by Slack user <@U0773HGAUSJ> — friendly nudge from Iris: this PR is still open. Want to take a look or close it out?

@iris-clawd
Copy link
Copy Markdown
Contributor Author

requested by Slack user <@U0773HGAUSJ> — friendly nudge from Iris: this PR is still open. Want to take a look or close it out?

@iris-clawd
Copy link
Copy Markdown
Contributor Author

requested by Lorenze Jay — friendly nudge from Iris: this PR is still open. Want to take a look or close it out?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant