feat: add OpenSandbox tool for sandbox-based code execution#5755
feat: add OpenSandbox tool for sandbox-based code execution#5755iris-clawd wants to merge 1 commit intomainfrom
Conversation
📝 WalkthroughWalkthroughA 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. ChangesOpenSandbox Tool Integration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
lib/crewai/src/crewai/tools/opensandbox_tool.pylib/crewai/tests/test_opensandbox_tool.py
| 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 |
There was a problem hiding this comment.
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.
| 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}" |
There was a problem hiding this comment.
🧩 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.pyRepository: 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.
|
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
|
requested by Slack user <@U0773HGAUSJ> — friendly nudge from Iris: this PR is still open. Want to take a look or close it out? |
|
requested by Slack user <@U0773HGAUSJ> — friendly nudge from Iris: this PR is still open. Want to take a look or close it out? |
|
requested by Slack user <@U0773HGAUSJ> — friendly nudge from Iris: this PR is still open. Want to take a look or close it out? |
|
requested by Slack user <@U0773HGAUSJ> — friendly nudge from Iris: this PR is still open. Want to take a look or close it out? |
|
requested by Lorenze Jay — friendly nudge from Iris: this PR is still open. Want to take a look or close it out? |
Summary
Adds
OpenSandboxTool— aBaseToolsubclass 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
run_commandread_filewrite_filekillThe 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)OPENSANDBOX_DOMAINhost:portof the OpenSandbox serverOPENSANDBOX_PROTOCOLhttphttporhttpsOPENSANDBOX_IMAGEpython:3.12OPENSANDBOX_TIMEOUT_MINUTES30OPENSANDBOX_API_KEYFiles changed
lib/crewai/src/crewai/tools/opensandbox_tool.py— tool implementation (206 lines)lib/crewai/tests/test_opensandbox_tool.py— 13 unit tests, all passingTest results
Notes
pip install opensandbox(v0.1.8+). This is an optional dep — the tool raises a clearImportErrorif the package isn't installed._runviaasyncio.run()with aThreadPoolExecutorfallback for environments with a running event loop.OPENSANDBOX_DOMAINnot set → raisesValueErrorwith a descriptive message.ruff check+ruff formatclean.Usage example
Summary by CodeRabbit
Release Notes