mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-05 06:59:23 +00:00
feat: improve Daytona sandbox tools
Signed-off-by: Mislav Ivanda <mislavivanda454@gmail.com> Co-authored-by: Greyson LaLonde <greyson.r.lalonde@gmail.com> Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com>
This commit is contained in:
@@ -13,7 +13,7 @@ The Daytona sandbox tools give CrewAI agents access to isolated, ephemeral compu
|
||||
|
||||
- **`DaytonaExecTool`** — run any shell command inside a sandbox.
|
||||
- **`DaytonaPythonTool`** — execute a block of Python source code inside a sandbox.
|
||||
- **`DaytonaFileTool`** — read, write, append, list, delete, and inspect files inside a sandbox.
|
||||
- **`DaytonaFileTool`** — read, write, append, list, delete, and inspect files inside a sandbox; also supports `move`, `find` (content grep), `search` (filename glob), `chmod` (permissions), `replace` (bulk find-and-replace), and `exists`.
|
||||
|
||||
All three tools share the same sandbox lifecycle controls, so you can mix and match them while keeping state in a single persistent sandbox.
|
||||
|
||||
@@ -55,7 +55,7 @@ from crewai_tools import DaytonaPythonTool
|
||||
tool = DaytonaPythonTool()
|
||||
result = tool.run(code="print(sum(range(10)))")
|
||||
print(result)
|
||||
# {"exit_code": 0, "result": "45\n", "artifacts": None}
|
||||
# {"exit_code": 0, "result": "45\n", "artifacts": ExecutionArtifacts(stdout="45\n", charts=[])}
|
||||
```
|
||||
|
||||
### Multi-step shell session (persistent)
|
||||
@@ -63,17 +63,22 @@ print(result)
|
||||
```python Code
|
||||
from crewai_tools import DaytonaExecTool, DaytonaFileTool
|
||||
|
||||
# Create the persistent sandbox via the first tool, then attach the second
|
||||
# tool to it so both share state (installed packages, files, env vars).
|
||||
exec_tool = DaytonaExecTool(persistent=True)
|
||||
file_tool = DaytonaFileTool(persistent=True)
|
||||
|
||||
# Install a package, then write and run a script — all in the same sandbox
|
||||
exec_tool.run(command="pip install httpx -q")
|
||||
file_tool.run(action="write", path="/workspace/fetch.py", content="import httpx; print(httpx.get('https://httpbin.org/get').status_code)")
|
||||
exec_tool.run(command="python /workspace/fetch.py")
|
||||
file_tool = DaytonaFileTool(sandbox_id=exec_tool.active_sandbox_id)
|
||||
|
||||
file_tool.run(
|
||||
action="write",
|
||||
path="workspace/script.py",
|
||||
content="import httpx; print(f'httpx loaded, version {httpx.__version__}')",
|
||||
)
|
||||
exec_tool.run(command="python workspace/script.py")
|
||||
```
|
||||
|
||||
<Note>
|
||||
Each tool instance maintains its own persistent sandbox. To share **one** sandbox across two tools, create the first tool, grab its sandbox id via `tool._persistent_sandbox.id`, and pass it to the second tool via `sandbox_id=...`.
|
||||
By default, each tool with `persistent=True` lazily creates its **own** sandbox on first use. The pattern above shares a single sandbox across multiple tools by reading the first tool's `active_sandbox_id` after a `.run()` call and passing it to the others via `sandbox_id=...`. With `persistent=False` (the default), every `.run()` call gets a fresh sandbox that's deleted at the end of that call.
|
||||
</Note>
|
||||
|
||||
### Attach to an existing sandbox
|
||||
@@ -82,7 +87,7 @@ Each tool instance maintains its own persistent sandbox. To share **one** sandbo
|
||||
from crewai_tools import DaytonaExecTool
|
||||
|
||||
tool = DaytonaExecTool(sandbox_id="my-long-lived-sandbox")
|
||||
result = tool.run(command="ls /workspace")
|
||||
result = tool.run(command="ls workspace")
|
||||
```
|
||||
|
||||
### Custom sandbox parameters
|
||||
@@ -102,6 +107,41 @@ tool = DaytonaExecTool(
|
||||
)
|
||||
```
|
||||
|
||||
### Searching, moving, and modifying files
|
||||
|
||||
```python Code
|
||||
from crewai_tools import DaytonaFileTool
|
||||
|
||||
file_tool = DaytonaFileTool(persistent=True)
|
||||
|
||||
# Find every TODO in the source tree (grep file contents recursively)
|
||||
file_tool.run(action="find", path="workspace/src", pattern="TODO:")
|
||||
|
||||
# Find all Python files (glob match on filenames)
|
||||
file_tool.run(action="search", path="workspace", pattern="*.py")
|
||||
|
||||
# Make a script executable
|
||||
file_tool.run(action="chmod", path="workspace/run.sh", mode="755")
|
||||
|
||||
# Rename or move a file
|
||||
file_tool.run(
|
||||
action="move",
|
||||
path="workspace/draft.md",
|
||||
destination="workspace/final.md",
|
||||
)
|
||||
|
||||
# Bulk find-and-replace across multiple files
|
||||
file_tool.run(
|
||||
action="replace",
|
||||
paths=["workspace/src/a.py", "workspace/src/b.py"],
|
||||
pattern="old_function",
|
||||
replacement="new_function",
|
||||
)
|
||||
|
||||
# Quick existence check before a destructive op
|
||||
file_tool.run(action="exists", path="workspace/cache.db")
|
||||
```
|
||||
|
||||
### Agent integration
|
||||
|
||||
```python Code
|
||||
@@ -121,7 +161,7 @@ coder = Agent(
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Write a Python script that prints the first 10 Fibonacci numbers, save it to /workspace/fib.py, and run it.",
|
||||
description="Write a Python script that prints the first 10 Fibonacci numbers, save it to workspace/fib.py, and run it.",
|
||||
expected_output="The first 10 Fibonacci numbers printed to stdout.",
|
||||
agent=coder,
|
||||
)
|
||||
@@ -168,12 +208,22 @@ All three tools accept these parameters at initialization:
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `action` | `str` | ✓ | One of: `read`, `write`, `append`, `list`, `delete`, `mkdir`, `info`. |
|
||||
| `path` | `str` | ✓ | Absolute path inside the sandbox. |
|
||||
| `content` | `str \| None` | | Content to write or append. Required for `append`. |
|
||||
| `action` | `str` | ✓ | One of: `read`, `write`, `append`, `list`, `delete`, `mkdir`, `info`, `exists`, `move`, `find`, `search`, `chmod`, `replace`. |
|
||||
| `path` | `str \| None` | ✓ for all actions except `replace` | Absolute path inside the sandbox. |
|
||||
| `content` | `str \| None` | ✓ for `append` | Content to write or append. |
|
||||
| `binary` | `bool` | | If `True`, `content` is base64 on write; returns base64 on read. |
|
||||
| `recursive` | `bool` | | For `delete`: remove directories recursively. |
|
||||
| `mode` | `str` | | For `mkdir`: octal permission string (default `"0755"`). |
|
||||
| `mode` | `str \| None` | | For `mkdir`: octal permissions for the new directory (defaults to `"0755"`). For `chmod`: octal permissions to apply to the target. |
|
||||
| `destination` | `str \| None` | ✓ for `move` | Destination path for `move`. |
|
||||
| `pattern` | `str \| None` | ✓ for `find`, `search`, `replace` | For `find`: substring matched against file CONTENTS. For `search`: glob matched against file NAMES (e.g. `*.py`). For `replace`: text to replace inside files. |
|
||||
| `replacement` | `str \| None` | ✓ for `replace` | Replacement text for `pattern`. |
|
||||
| `paths` | `list[str] \| None` | ✓ for `replace` | List of file paths in which to replace text. |
|
||||
| `owner` | `str \| None` | | For `chmod`: new file owner. |
|
||||
| `group` | `str \| None` | | For `chmod`: new file group. |
|
||||
|
||||
<Note>
|
||||
For `chmod`, pass at least one of `mode`, `owner`, or `group` — any field left as `None` is left unchanged on the target.
|
||||
</Note>
|
||||
|
||||
<Tip>
|
||||
For files larger than a few KB, create the file first with `action="write"` and empty content, then send the body via multiple `action="append"` calls of ~4 KB each to stay within tool-call payload limits.
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"en/tools/ai-ml/langchaintool",
|
||||
"en/tools/ai-ml/ragtool",
|
||||
"en/tools/ai-ml/codeinterpretertool",
|
||||
"en/tools/ai-ml/daytona",
|
||||
"en/tools/ai-ml/e2bsandboxtools"
|
||||
]
|
||||
},
|
||||
@@ -764,6 +765,7 @@
|
||||
"en/tools/ai-ml/langchaintool",
|
||||
"en/tools/ai-ml/ragtool",
|
||||
"en/tools/ai-ml/codeinterpretertool",
|
||||
"en/tools/ai-ml/daytona",
|
||||
"en/tools/ai-ml/e2bsandboxtools"
|
||||
]
|
||||
},
|
||||
@@ -1724,8 +1726,8 @@
|
||||
"en/tools/ai-ml/langchaintool",
|
||||
"en/tools/ai-ml/ragtool",
|
||||
"en/tools/ai-ml/codeinterpretertool",
|
||||
"en/tools/ai-ml/e2bsandboxtools",
|
||||
"en/tools/ai-ml/daytona"
|
||||
"en/tools/ai-ml/daytona",
|
||||
"en/tools/ai-ml/e2bsandboxtools"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2205,8 +2207,8 @@
|
||||
"en/tools/ai-ml/langchaintool",
|
||||
"en/tools/ai-ml/ragtool",
|
||||
"en/tools/ai-ml/codeinterpretertool",
|
||||
"en/tools/ai-ml/e2bsandboxtools",
|
||||
"en/tools/ai-ml/daytona"
|
||||
"en/tools/ai-ml/daytona",
|
||||
"en/tools/ai-ml/e2bsandboxtools"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2686,8 +2688,8 @@
|
||||
"en/tools/ai-ml/langchaintool",
|
||||
"en/tools/ai-ml/ragtool",
|
||||
"en/tools/ai-ml/codeinterpretertool",
|
||||
"en/tools/ai-ml/e2bsandboxtools",
|
||||
"en/tools/ai-ml/daytona"
|
||||
"en/tools/ai-ml/daytona",
|
||||
"en/tools/ai-ml/e2bsandboxtools"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -3167,8 +3169,8 @@
|
||||
"en/tools/ai-ml/langchaintool",
|
||||
"en/tools/ai-ml/ragtool",
|
||||
"en/tools/ai-ml/codeinterpretertool",
|
||||
"en/tools/ai-ml/e2bsandboxtools",
|
||||
"en/tools/ai-ml/daytona"
|
||||
"en/tools/ai-ml/daytona",
|
||||
"en/tools/ai-ml/e2bsandboxtools"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -3647,8 +3649,8 @@
|
||||
"en/tools/ai-ml/langchaintool",
|
||||
"en/tools/ai-ml/ragtool",
|
||||
"en/tools/ai-ml/codeinterpretertool",
|
||||
"en/tools/ai-ml/e2bsandboxtools",
|
||||
"en/tools/ai-ml/daytona"
|
||||
"en/tools/ai-ml/daytona",
|
||||
"en/tools/ai-ml/e2bsandboxtools"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -4126,8 +4128,8 @@
|
||||
"en/tools/ai-ml/langchaintool",
|
||||
"en/tools/ai-ml/ragtool",
|
||||
"en/tools/ai-ml/codeinterpretertool",
|
||||
"en/tools/ai-ml/e2bsandboxtools",
|
||||
"en/tools/ai-ml/daytona"
|
||||
"en/tools/ai-ml/daytona",
|
||||
"en/tools/ai-ml/e2bsandboxtools"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -4605,8 +4607,8 @@
|
||||
"en/tools/ai-ml/langchaintool",
|
||||
"en/tools/ai-ml/ragtool",
|
||||
"en/tools/ai-ml/codeinterpretertool",
|
||||
"en/tools/ai-ml/e2bsandboxtools",
|
||||
"en/tools/ai-ml/daytona"
|
||||
"en/tools/ai-ml/daytona",
|
||||
"en/tools/ai-ml/e2bsandboxtools"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -5084,8 +5086,8 @@
|
||||
"en/tools/ai-ml/langchaintool",
|
||||
"en/tools/ai-ml/ragtool",
|
||||
"en/tools/ai-ml/codeinterpretertool",
|
||||
"en/tools/ai-ml/e2bsandboxtools",
|
||||
"en/tools/ai-ml/daytona"
|
||||
"en/tools/ai-ml/daytona",
|
||||
"en/tools/ai-ml/e2bsandboxtools"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -5565,8 +5567,8 @@
|
||||
"en/tools/ai-ml/langchaintool",
|
||||
"en/tools/ai-ml/ragtool",
|
||||
"en/tools/ai-ml/codeinterpretertool",
|
||||
"en/tools/ai-ml/e2bsandboxtools",
|
||||
"en/tools/ai-ml/daytona"
|
||||
"en/tools/ai-ml/daytona",
|
||||
"en/tools/ai-ml/e2bsandboxtools"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -6045,8 +6047,8 @@
|
||||
"en/tools/ai-ml/langchaintool",
|
||||
"en/tools/ai-ml/ragtool",
|
||||
"en/tools/ai-ml/codeinterpretertool",
|
||||
"en/tools/ai-ml/e2bsandboxtools",
|
||||
"en/tools/ai-ml/daytona"
|
||||
"en/tools/ai-ml/daytona",
|
||||
"en/tools/ai-ml/e2bsandboxtools"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ The Daytona sandbox tools give CrewAI agents access to isolated, ephemeral compu
|
||||
|
||||
- **`DaytonaExecTool`** — run any shell command inside a sandbox.
|
||||
- **`DaytonaPythonTool`** — execute a block of Python source code inside a sandbox.
|
||||
- **`DaytonaFileTool`** — read, write, append, list, delete, and inspect files inside a sandbox.
|
||||
- **`DaytonaFileTool`** — read, write, append, list, delete, and inspect files inside a sandbox; also supports `move`, `find` (content grep), `search` (filename glob), `chmod` (permissions), `replace` (bulk find-and-replace), and `exists`.
|
||||
|
||||
All three tools share the same sandbox lifecycle controls, so you can mix and match them while keeping state in a single persistent sandbox.
|
||||
|
||||
@@ -55,7 +55,7 @@ from crewai_tools import DaytonaPythonTool
|
||||
tool = DaytonaPythonTool()
|
||||
result = tool.run(code="print(sum(range(10)))")
|
||||
print(result)
|
||||
# {"exit_code": 0, "result": "45\n", "artifacts": None}
|
||||
# {"exit_code": 0, "result": "45\n", "artifacts": ExecutionArtifacts(stdout="45\n", charts=[])}
|
||||
```
|
||||
|
||||
### Multi-step shell session (persistent)
|
||||
@@ -63,17 +63,22 @@ print(result)
|
||||
```python Code
|
||||
from crewai_tools import DaytonaExecTool, DaytonaFileTool
|
||||
|
||||
# Create the persistent sandbox via the first tool, then attach the second
|
||||
# tool to it so both share state (installed packages, files, env vars).
|
||||
exec_tool = DaytonaExecTool(persistent=True)
|
||||
file_tool = DaytonaFileTool(persistent=True)
|
||||
|
||||
# Install a package, then write and run a script — all in the same sandbox
|
||||
exec_tool.run(command="pip install httpx -q")
|
||||
file_tool.run(action="write", path="/workspace/fetch.py", content="import httpx; print(httpx.get('https://httpbin.org/get').status_code)")
|
||||
exec_tool.run(command="python /workspace/fetch.py")
|
||||
file_tool = DaytonaFileTool(sandbox_id=exec_tool.active_sandbox_id)
|
||||
|
||||
file_tool.run(
|
||||
action="write",
|
||||
path="workspace/script.py",
|
||||
content="import httpx; print(f'httpx loaded, version {httpx.__version__}')",
|
||||
)
|
||||
exec_tool.run(command="python workspace/script.py")
|
||||
```
|
||||
|
||||
<Note>
|
||||
Each tool instance maintains its own persistent sandbox. To share **one** sandbox across two tools, create the first tool, grab its sandbox id via `tool._persistent_sandbox.id`, and pass it to the second tool via `sandbox_id=...`.
|
||||
By default, each tool with `persistent=True` lazily creates its **own** sandbox on first use. The pattern above shares a single sandbox across multiple tools by reading the first tool's `active_sandbox_id` after a `.run()` call and passing it to the others via `sandbox_id=...`. With `persistent=False` (the default), every `.run()` call gets a fresh sandbox that's deleted at the end of that call.
|
||||
</Note>
|
||||
|
||||
### Attach to an existing sandbox
|
||||
@@ -82,7 +87,7 @@ Each tool instance maintains its own persistent sandbox. To share **one** sandbo
|
||||
from crewai_tools import DaytonaExecTool
|
||||
|
||||
tool = DaytonaExecTool(sandbox_id="my-long-lived-sandbox")
|
||||
result = tool.run(command="ls /workspace")
|
||||
result = tool.run(command="ls workspace")
|
||||
```
|
||||
|
||||
### Custom sandbox parameters
|
||||
@@ -102,6 +107,41 @@ tool = DaytonaExecTool(
|
||||
)
|
||||
```
|
||||
|
||||
### Searching, moving, and modifying files
|
||||
|
||||
```python Code
|
||||
from crewai_tools import DaytonaFileTool
|
||||
|
||||
file_tool = DaytonaFileTool(persistent=True)
|
||||
|
||||
# Find every TODO in the source tree (grep file contents recursively)
|
||||
file_tool.run(action="find", path="workspace/src", pattern="TODO:")
|
||||
|
||||
# Find all Python files (glob match on filenames)
|
||||
file_tool.run(action="search", path="workspace", pattern="*.py")
|
||||
|
||||
# Make a script executable
|
||||
file_tool.run(action="chmod", path="workspace/run.sh", mode="755")
|
||||
|
||||
# Rename or move a file
|
||||
file_tool.run(
|
||||
action="move",
|
||||
path="workspace/draft.md",
|
||||
destination="workspace/final.md",
|
||||
)
|
||||
|
||||
# Bulk find-and-replace across multiple files
|
||||
file_tool.run(
|
||||
action="replace",
|
||||
paths=["workspace/src/a.py", "workspace/src/b.py"],
|
||||
pattern="old_function",
|
||||
replacement="new_function",
|
||||
)
|
||||
|
||||
# Quick existence check before a destructive op
|
||||
file_tool.run(action="exists", path="workspace/cache.db")
|
||||
```
|
||||
|
||||
### Agent integration
|
||||
|
||||
```python Code
|
||||
@@ -121,7 +161,7 @@ coder = Agent(
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Write a Python script that prints the first 10 Fibonacci numbers, save it to /workspace/fib.py, and run it.",
|
||||
description="Write a Python script that prints the first 10 Fibonacci numbers, save it to workspace/fib.py, and run it.",
|
||||
expected_output="The first 10 Fibonacci numbers printed to stdout.",
|
||||
agent=coder,
|
||||
)
|
||||
@@ -168,12 +208,22 @@ All three tools accept these parameters at initialization:
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `action` | `str` | ✓ | One of: `read`, `write`, `append`, `list`, `delete`, `mkdir`, `info`. |
|
||||
| `path` | `str` | ✓ | Absolute path inside the sandbox. |
|
||||
| `content` | `str \| None` | | Content to write or append. Required for `append`. |
|
||||
| `action` | `str` | ✓ | One of: `read`, `write`, `append`, `list`, `delete`, `mkdir`, `info`, `exists`, `move`, `find`, `search`, `chmod`, `replace`. |
|
||||
| `path` | `str \| None` | ✓ for all actions except `replace` | Absolute path inside the sandbox. |
|
||||
| `content` | `str \| None` | ✓ for `append` | Content to write or append. |
|
||||
| `binary` | `bool` | | If `True`, `content` is base64 on write; returns base64 on read. |
|
||||
| `recursive` | `bool` | | For `delete`: remove directories recursively. |
|
||||
| `mode` | `str` | | For `mkdir`: octal permission string (default `"0755"`). |
|
||||
| `mode` | `str \| None` | | For `mkdir`: octal permissions for the new directory (defaults to `"0755"`). For `chmod`: octal permissions to apply to the target. |
|
||||
| `destination` | `str \| None` | ✓ for `move` | Destination path for `move`. |
|
||||
| `pattern` | `str \| None` | ✓ for `find`, `search`, `replace` | For `find`: substring matched against file CONTENTS. For `search`: glob matched against file NAMES (e.g. `*.py`). For `replace`: text to replace inside files. |
|
||||
| `replacement` | `str \| None` | ✓ for `replace` | Replacement text for `pattern`. |
|
||||
| `paths` | `list[str] \| None` | ✓ for `replace` | List of file paths in which to replace text. |
|
||||
| `owner` | `str \| None` | | For `chmod`: new file owner. |
|
||||
| `group` | `str \| None` | | For `chmod`: new file group. |
|
||||
|
||||
<Note>
|
||||
For `chmod`, pass at least one of `mode`, `owner`, or `group` — any field left as `None` is left unchanged on the target.
|
||||
</Note>
|
||||
|
||||
<Tip>
|
||||
For files larger than a few KB, create the file first with `action="write"` and empty content, then send the body via multiple `action="append"` calls of ~4 KB each to stay within tool-call payload limits.
|
||||
|
||||
@@ -13,7 +13,7 @@ The Daytona sandbox tools give CrewAI agents access to isolated, ephemeral compu
|
||||
|
||||
- **`DaytonaExecTool`** — run any shell command inside a sandbox.
|
||||
- **`DaytonaPythonTool`** — execute a block of Python source code inside a sandbox.
|
||||
- **`DaytonaFileTool`** — read, write, append, list, delete, and inspect files inside a sandbox.
|
||||
- **`DaytonaFileTool`** — read, write, append, list, delete, and inspect files inside a sandbox; also supports `move`, `find` (content grep), `search` (filename glob), `chmod` (permissions), `replace` (bulk find-and-replace), and `exists`.
|
||||
|
||||
All three tools share the same sandbox lifecycle controls, so you can mix and match them while keeping state in a single persistent sandbox.
|
||||
|
||||
@@ -55,7 +55,7 @@ from crewai_tools import DaytonaPythonTool
|
||||
tool = DaytonaPythonTool()
|
||||
result = tool.run(code="print(sum(range(10)))")
|
||||
print(result)
|
||||
# {"exit_code": 0, "result": "45\n", "artifacts": None}
|
||||
# {"exit_code": 0, "result": "45\n", "artifacts": ExecutionArtifacts(stdout="45\n", charts=[])}
|
||||
```
|
||||
|
||||
### Multi-step shell session (persistent)
|
||||
@@ -63,17 +63,22 @@ print(result)
|
||||
```python Code
|
||||
from crewai_tools import DaytonaExecTool, DaytonaFileTool
|
||||
|
||||
# Create the persistent sandbox via the first tool, then attach the second
|
||||
# tool to it so both share state (installed packages, files, env vars).
|
||||
exec_tool = DaytonaExecTool(persistent=True)
|
||||
file_tool = DaytonaFileTool(persistent=True)
|
||||
|
||||
# Install a package, then write and run a script — all in the same sandbox
|
||||
exec_tool.run(command="pip install httpx -q")
|
||||
file_tool.run(action="write", path="/workspace/fetch.py", content="import httpx; print(httpx.get('https://httpbin.org/get').status_code)")
|
||||
exec_tool.run(command="python /workspace/fetch.py")
|
||||
file_tool = DaytonaFileTool(sandbox_id=exec_tool.active_sandbox_id)
|
||||
|
||||
file_tool.run(
|
||||
action="write",
|
||||
path="workspace/script.py",
|
||||
content="import httpx; print(f'httpx loaded, version {httpx.__version__}')",
|
||||
)
|
||||
exec_tool.run(command="python workspace/script.py")
|
||||
```
|
||||
|
||||
<Note>
|
||||
Each tool instance maintains its own persistent sandbox. To share **one** sandbox across two tools, create the first tool, grab its sandbox id via `tool._persistent_sandbox.id`, and pass it to the second tool via `sandbox_id=...`.
|
||||
By default, each tool with `persistent=True` lazily creates its **own** sandbox on first use. The pattern above shares a single sandbox across multiple tools by reading the first tool's `active_sandbox_id` after a `.run()` call and passing it to the others via `sandbox_id=...`. With `persistent=False` (the default), every `.run()` call gets a fresh sandbox that's deleted at the end of that call.
|
||||
</Note>
|
||||
|
||||
### Attach to an existing sandbox
|
||||
@@ -82,7 +87,7 @@ Each tool instance maintains its own persistent sandbox. To share **one** sandbo
|
||||
from crewai_tools import DaytonaExecTool
|
||||
|
||||
tool = DaytonaExecTool(sandbox_id="my-long-lived-sandbox")
|
||||
result = tool.run(command="ls /workspace")
|
||||
result = tool.run(command="ls workspace")
|
||||
```
|
||||
|
||||
### Custom sandbox parameters
|
||||
@@ -102,6 +107,41 @@ tool = DaytonaExecTool(
|
||||
)
|
||||
```
|
||||
|
||||
### Searching, moving, and modifying files
|
||||
|
||||
```python Code
|
||||
from crewai_tools import DaytonaFileTool
|
||||
|
||||
file_tool = DaytonaFileTool(persistent=True)
|
||||
|
||||
# Find every TODO in the source tree (grep file contents recursively)
|
||||
file_tool.run(action="find", path="workspace/src", pattern="TODO:")
|
||||
|
||||
# Find all Python files (glob match on filenames)
|
||||
file_tool.run(action="search", path="workspace", pattern="*.py")
|
||||
|
||||
# Make a script executable
|
||||
file_tool.run(action="chmod", path="workspace/run.sh", mode="755")
|
||||
|
||||
# Rename or move a file
|
||||
file_tool.run(
|
||||
action="move",
|
||||
path="workspace/draft.md",
|
||||
destination="workspace/final.md",
|
||||
)
|
||||
|
||||
# Bulk find-and-replace across multiple files
|
||||
file_tool.run(
|
||||
action="replace",
|
||||
paths=["workspace/src/a.py", "workspace/src/b.py"],
|
||||
pattern="old_function",
|
||||
replacement="new_function",
|
||||
)
|
||||
|
||||
# Quick existence check before a destructive op
|
||||
file_tool.run(action="exists", path="workspace/cache.db")
|
||||
```
|
||||
|
||||
### Agent integration
|
||||
|
||||
```python Code
|
||||
@@ -121,7 +161,7 @@ coder = Agent(
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Write a Python script that prints the first 10 Fibonacci numbers, save it to /workspace/fib.py, and run it.",
|
||||
description="Write a Python script that prints the first 10 Fibonacci numbers, save it to workspace/fib.py, and run it.",
|
||||
expected_output="The first 10 Fibonacci numbers printed to stdout.",
|
||||
agent=coder,
|
||||
)
|
||||
@@ -168,12 +208,22 @@ All three tools accept these parameters at initialization:
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `action` | `str` | ✓ | One of: `read`, `write`, `append`, `list`, `delete`, `mkdir`, `info`. |
|
||||
| `path` | `str` | ✓ | Absolute path inside the sandbox. |
|
||||
| `content` | `str \| None` | | Content to write or append. Required for `append`. |
|
||||
| `action` | `str` | ✓ | One of: `read`, `write`, `append`, `list`, `delete`, `mkdir`, `info`, `exists`, `move`, `find`, `search`, `chmod`, `replace`. |
|
||||
| `path` | `str \| None` | ✓ for all actions except `replace` | Absolute path inside the sandbox. |
|
||||
| `content` | `str \| None` | ✓ for `append` | Content to write or append. |
|
||||
| `binary` | `bool` | | If `True`, `content` is base64 on write; returns base64 on read. |
|
||||
| `recursive` | `bool` | | For `delete`: remove directories recursively. |
|
||||
| `mode` | `str` | | For `mkdir`: octal permission string (default `"0755"`). |
|
||||
| `mode` | `str \| None` | | For `mkdir`: octal permissions for the new directory (defaults to `"0755"`). For `chmod`: octal permissions to apply to the target. |
|
||||
| `destination` | `str \| None` | ✓ for `move` | Destination path for `move`. |
|
||||
| `pattern` | `str \| None` | ✓ for `find`, `search`, `replace` | For `find`: substring matched against file CONTENTS. For `search`: glob matched against file NAMES (e.g. `*.py`). For `replace`: text to replace inside files. |
|
||||
| `replacement` | `str \| None` | ✓ for `replace` | Replacement text for `pattern`. |
|
||||
| `paths` | `list[str] \| None` | ✓ for `replace` | List of file paths in which to replace text. |
|
||||
| `owner` | `str \| None` | | For `chmod`: new file owner. |
|
||||
| `group` | `str \| None` | | For `chmod`: new file group. |
|
||||
|
||||
<Note>
|
||||
For `chmod`, pass at least one of `mode`, `owner`, or `group` — any field left as `None` is left unchanged on the target.
|
||||
</Note>
|
||||
|
||||
<Tip>
|
||||
For files larger than a few KB, create the file first with `action="write"` and empty content, then send the body via multiple `action="append"` calls of ~4 KB each to stay within tool-call payload limits.
|
||||
|
||||
@@ -13,7 +13,7 @@ The Daytona sandbox tools give CrewAI agents access to isolated, ephemeral compu
|
||||
|
||||
- **`DaytonaExecTool`** — run any shell command inside a sandbox.
|
||||
- **`DaytonaPythonTool`** — execute a block of Python source code inside a sandbox.
|
||||
- **`DaytonaFileTool`** — read, write, append, list, delete, and inspect files inside a sandbox.
|
||||
- **`DaytonaFileTool`** — read, write, append, list, delete, and inspect files inside a sandbox; also supports `move`, `find` (content grep), `search` (filename glob), `chmod` (permissions), `replace` (bulk find-and-replace), and `exists`.
|
||||
|
||||
All three tools share the same sandbox lifecycle controls, so you can mix and match them while keeping state in a single persistent sandbox.
|
||||
|
||||
@@ -55,7 +55,7 @@ from crewai_tools import DaytonaPythonTool
|
||||
tool = DaytonaPythonTool()
|
||||
result = tool.run(code="print(sum(range(10)))")
|
||||
print(result)
|
||||
# {"exit_code": 0, "result": "45\n", "artifacts": None}
|
||||
# {"exit_code": 0, "result": "45\n", "artifacts": ExecutionArtifacts(stdout="45\n", charts=[])}
|
||||
```
|
||||
|
||||
### Multi-step shell session (persistent)
|
||||
@@ -63,17 +63,22 @@ print(result)
|
||||
```python Code
|
||||
from crewai_tools import DaytonaExecTool, DaytonaFileTool
|
||||
|
||||
# Create the persistent sandbox via the first tool, then attach the second
|
||||
# tool to it so both share state (installed packages, files, env vars).
|
||||
exec_tool = DaytonaExecTool(persistent=True)
|
||||
file_tool = DaytonaFileTool(persistent=True)
|
||||
|
||||
# Install a package, then write and run a script — all in the same sandbox
|
||||
exec_tool.run(command="pip install httpx -q")
|
||||
file_tool.run(action="write", path="/workspace/fetch.py", content="import httpx; print(httpx.get('https://httpbin.org/get').status_code)")
|
||||
exec_tool.run(command="python /workspace/fetch.py")
|
||||
file_tool = DaytonaFileTool(sandbox_id=exec_tool.active_sandbox_id)
|
||||
|
||||
file_tool.run(
|
||||
action="write",
|
||||
path="workspace/script.py",
|
||||
content="import httpx; print(f'httpx loaded, version {httpx.__version__}')",
|
||||
)
|
||||
exec_tool.run(command="python workspace/script.py")
|
||||
```
|
||||
|
||||
<Note>
|
||||
Each tool instance maintains its own persistent sandbox. To share **one** sandbox across two tools, create the first tool, grab its sandbox id via `tool._persistent_sandbox.id`, and pass it to the second tool via `sandbox_id=...`.
|
||||
By default, each tool with `persistent=True` lazily creates its **own** sandbox on first use. The pattern above shares a single sandbox across multiple tools by reading the first tool's `active_sandbox_id` after a `.run()` call and passing it to the others via `sandbox_id=...`. With `persistent=False` (the default), every `.run()` call gets a fresh sandbox that's deleted at the end of that call.
|
||||
</Note>
|
||||
|
||||
### Attach to an existing sandbox
|
||||
@@ -82,7 +87,7 @@ Each tool instance maintains its own persistent sandbox. To share **one** sandbo
|
||||
from crewai_tools import DaytonaExecTool
|
||||
|
||||
tool = DaytonaExecTool(sandbox_id="my-long-lived-sandbox")
|
||||
result = tool.run(command="ls /workspace")
|
||||
result = tool.run(command="ls workspace")
|
||||
```
|
||||
|
||||
### Custom sandbox parameters
|
||||
@@ -102,6 +107,41 @@ tool = DaytonaExecTool(
|
||||
)
|
||||
```
|
||||
|
||||
### Searching, moving, and modifying files
|
||||
|
||||
```python Code
|
||||
from crewai_tools import DaytonaFileTool
|
||||
|
||||
file_tool = DaytonaFileTool(persistent=True)
|
||||
|
||||
# Find every TODO in the source tree (grep file contents recursively)
|
||||
file_tool.run(action="find", path="workspace/src", pattern="TODO:")
|
||||
|
||||
# Find all Python files (glob match on filenames)
|
||||
file_tool.run(action="search", path="workspace", pattern="*.py")
|
||||
|
||||
# Make a script executable
|
||||
file_tool.run(action="chmod", path="workspace/run.sh", mode="755")
|
||||
|
||||
# Rename or move a file
|
||||
file_tool.run(
|
||||
action="move",
|
||||
path="workspace/draft.md",
|
||||
destination="workspace/final.md",
|
||||
)
|
||||
|
||||
# Bulk find-and-replace across multiple files
|
||||
file_tool.run(
|
||||
action="replace",
|
||||
paths=["workspace/src/a.py", "workspace/src/b.py"],
|
||||
pattern="old_function",
|
||||
replacement="new_function",
|
||||
)
|
||||
|
||||
# Quick existence check before a destructive op
|
||||
file_tool.run(action="exists", path="workspace/cache.db")
|
||||
```
|
||||
|
||||
### Agent integration
|
||||
|
||||
```python Code
|
||||
@@ -121,7 +161,7 @@ coder = Agent(
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Write a Python script that prints the first 10 Fibonacci numbers, save it to /workspace/fib.py, and run it.",
|
||||
description="Write a Python script that prints the first 10 Fibonacci numbers, save it to workspace/fib.py, and run it.",
|
||||
expected_output="The first 10 Fibonacci numbers printed to stdout.",
|
||||
agent=coder,
|
||||
)
|
||||
@@ -168,12 +208,22 @@ All three tools accept these parameters at initialization:
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `action` | `str` | ✓ | One of: `read`, `write`, `append`, `list`, `delete`, `mkdir`, `info`. |
|
||||
| `path` | `str` | ✓ | Absolute path inside the sandbox. |
|
||||
| `content` | `str \| None` | | Content to write or append. Required for `append`. |
|
||||
| `action` | `str` | ✓ | One of: `read`, `write`, `append`, `list`, `delete`, `mkdir`, `info`, `exists`, `move`, `find`, `search`, `chmod`, `replace`. |
|
||||
| `path` | `str \| None` | ✓ for all actions except `replace` | Absolute path inside the sandbox. |
|
||||
| `content` | `str \| None` | ✓ for `append` | Content to write or append. |
|
||||
| `binary` | `bool` | | If `True`, `content` is base64 on write; returns base64 on read. |
|
||||
| `recursive` | `bool` | | For `delete`: remove directories recursively. |
|
||||
| `mode` | `str` | | For `mkdir`: octal permission string (default `"0755"`). |
|
||||
| `mode` | `str \| None` | | For `mkdir`: octal permissions for the new directory (defaults to `"0755"`). For `chmod`: octal permissions to apply to the target. |
|
||||
| `destination` | `str \| None` | ✓ for `move` | Destination path for `move`. |
|
||||
| `pattern` | `str \| None` | ✓ for `find`, `search`, `replace` | For `find`: substring matched against file CONTENTS. For `search`: glob matched against file NAMES (e.g. `*.py`). For `replace`: text to replace inside files. |
|
||||
| `replacement` | `str \| None` | ✓ for `replace` | Replacement text for `pattern`. |
|
||||
| `paths` | `list[str] \| None` | ✓ for `replace` | List of file paths in which to replace text. |
|
||||
| `owner` | `str \| None` | | For `chmod`: new file owner. |
|
||||
| `group` | `str \| None` | | For `chmod`: new file group. |
|
||||
|
||||
<Note>
|
||||
For `chmod`, pass at least one of `mode`, `owner`, or `group` — any field left as `None` is left unchanged on the target.
|
||||
</Note>
|
||||
|
||||
<Tip>
|
||||
For files larger than a few KB, create the file first with `action="write"` and empty content, then send the body via multiple `action="append"` calls of ~4 KB each to stay within tool-call payload limits.
|
||||
|
||||
@@ -55,10 +55,11 @@ from crewai_tools import DaytonaExecTool, DaytonaFileTool
|
||||
exec_tool = DaytonaExecTool(persistent=True)
|
||||
file_tool = DaytonaFileTool(persistent=True)
|
||||
|
||||
# Agent writes a script, then runs it — both share the same sandbox instance
|
||||
# because they each keep their own persistent sandbox. If you need the *same*
|
||||
# sandbox across two tools, create one tool, grab the sandbox id via
|
||||
# `tool._persistent_sandbox.id`, and pass it to the other via `sandbox_id=...`.
|
||||
# Agent writes a script, then runs it — but each tool keeps its OWN persistent
|
||||
# sandbox. To share the *same* sandbox across two tools, create and use the
|
||||
# first tool, then read its `active_sandbox_id` and pass it to the second:
|
||||
# exec_tool.run(command="pip install httpx")
|
||||
# file_tool = DaytonaFileTool(sandbox_id=exec_tool.active_sandbox_id)
|
||||
```
|
||||
|
||||
### Attach to an existing sandbox
|
||||
@@ -99,9 +100,14 @@ tool = DaytonaExecTool(
|
||||
- `timeout: int | None` — seconds.
|
||||
|
||||
### `DaytonaFileTool`
|
||||
- `action: "read" | "write" | "list" | "delete" | "mkdir" | "info"`
|
||||
- `path: str` — absolute path inside the sandbox.
|
||||
- `content: str | None` — required for `write`.
|
||||
- `action`: one of `read`, `write`, `append`, `list`, `delete`, `mkdir`, `info`, `exists`, `move`, `find`, `search`, `chmod`, `replace`.
|
||||
- `path: str | None` — absolute path inside the sandbox. Required for all actions except `replace`.
|
||||
- `content: str | None` — required for `append`; optional for `write`.
|
||||
- `binary: bool` — if `True`, `content` is base64 on write / returned as base64 on read.
|
||||
- `recursive: bool` — for `delete`, removes directories recursively.
|
||||
- `mode: str` — for `mkdir`, octal permission string (default `"0755"`).
|
||||
- `mode: str | None` — for `mkdir` (defaults to `"0755"`) or for `chmod` (e.g. `"755"`).
|
||||
- `destination: str | None` — required for `move`.
|
||||
- `pattern: str | None` — required for `find` (content grep), `search` (filename glob), and `replace`.
|
||||
- `replacement: str | None` — required for `replace`.
|
||||
- `paths: list[str] | None` — required for `replace`; list of files to operate on.
|
||||
- `owner: str | None` / `group: str | None` — for `chmod`. Pass at least one of `mode`, `owner`, or `group`.
|
||||
|
||||
@@ -196,3 +196,27 @@ class DaytonaBaseTool(BaseTool):
|
||||
"the sandbox may need manual deletion.",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def active_sandbox_id(self) -> str | None:
|
||||
"""The id of the sandbox this tool is currently bound to, if any.
|
||||
|
||||
Returns:
|
||||
- the explicitly attached `sandbox_id`, if set at construction;
|
||||
- the id of the lazily-created persistent sandbox, once a call has
|
||||
triggered creation;
|
||||
- None for ephemeral mode (where no sandbox lives between calls).
|
||||
|
||||
Use this to share one sandbox across multiple tool instances:
|
||||
|
||||
exec_tool = DaytonaExecTool(persistent=True)
|
||||
exec_tool.run(command="pip install httpx")
|
||||
file_tool = DaytonaFileTool(sandbox_id=exec_tool.active_sandbox_id)
|
||||
"""
|
||||
if self.sandbox_id:
|
||||
return self.sandbox_id
|
||||
with self._lock:
|
||||
sandbox = self._persistent_sandbox
|
||||
if sandbox is None:
|
||||
return None
|
||||
return getattr(sandbox, "id", None)
|
||||
|
||||
@@ -4,6 +4,8 @@ import base64
|
||||
from builtins import type as type_
|
||||
import logging
|
||||
import posixpath
|
||||
import shlex
|
||||
import uuid
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
@@ -14,22 +16,54 @@ from crewai_tools.tools.daytona_sandbox_tool.daytona_base_tool import DaytonaBas
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
FileAction = Literal["read", "write", "append", "list", "delete", "mkdir", "info"]
|
||||
FileAction = Literal[
|
||||
"read",
|
||||
"write",
|
||||
"append",
|
||||
"list",
|
||||
"delete",
|
||||
"mkdir",
|
||||
"info",
|
||||
"exists",
|
||||
"move",
|
||||
"find",
|
||||
"search",
|
||||
"chmod",
|
||||
"replace",
|
||||
]
|
||||
|
||||
|
||||
class DaytonaFileToolSchema(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)."
|
||||
"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 whether a path exists); "
|
||||
"'move' (rename or relocate a file/dir; requires 'destination'); "
|
||||
"'find' (grep file CONTENTS recursively; requires 'pattern'); "
|
||||
"'search' (find files by NAME pattern; requires 'pattern'); "
|
||||
"'chmod' (change permissions/owner/group; pass at least one of "
|
||||
"'mode', 'owner', 'group'); "
|
||||
"'replace' (find-and-replace text across files; requires "
|
||||
"'paths', 'pattern', and 'replacement')."
|
||||
),
|
||||
)
|
||||
path: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Absolute path inside the sandbox. Required for all actions "
|
||||
"except 'replace' (which uses 'paths' instead)."
|
||||
),
|
||||
)
|
||||
path: str = Field(..., description="Absolute path inside the sandbox.")
|
||||
content: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
@@ -50,18 +84,78 @@ class DaytonaFileToolSchema(BaseModel):
|
||||
default=False,
|
||||
description="For action='delete': remove directories recursively.",
|
||||
)
|
||||
mode: str = Field(
|
||||
default="0755",
|
||||
description="For action='mkdir': octal permission string (default 0755).",
|
||||
mode: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Octal permission string. For 'mkdir' it sets the new directory "
|
||||
"permissions (defaults to '0755' if omitted). For 'chmod' it sets "
|
||||
"the target's mode (e.g. '755' to make a script executable). "
|
||||
"Ignored for other actions."
|
||||
),
|
||||
)
|
||||
destination: str | None = Field(
|
||||
default=None,
|
||||
description="For action='move': absolute destination path.",
|
||||
)
|
||||
pattern: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"For 'find': substring matched against file CONTENTS. "
|
||||
"For 'search': glob-style pattern matched against file NAMES "
|
||||
"(e.g. '*.py'). "
|
||||
"For 'replace': text to replace inside files."
|
||||
),
|
||||
)
|
||||
replacement: str | None = Field(
|
||||
default=None,
|
||||
description="For action='replace': replacement text for 'pattern'.",
|
||||
)
|
||||
paths: list[str] | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"For action='replace': list of absolute file paths in which to "
|
||||
"replace 'pattern' with 'replacement'."
|
||||
),
|
||||
)
|
||||
owner: str | None = Field(
|
||||
default=None,
|
||||
description="For action='chmod': new file owner (user name).",
|
||||
)
|
||||
group: str | None = Field(
|
||||
default=None,
|
||||
description="For action='chmod': new file group.",
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_action_args(self) -> DaytonaFileToolSchema:
|
||||
if self.action != "replace" and not self.path:
|
||||
raise ValueError(f"action={self.action!r} requires 'path'.")
|
||||
if self.action == "append" and self.content is None:
|
||||
raise ValueError(
|
||||
"action='append' requires 'content'. Pass the chunk to append "
|
||||
"in the 'content' field."
|
||||
)
|
||||
if self.action == "move" and not self.destination:
|
||||
raise ValueError("action='move' requires 'destination'.")
|
||||
if self.action == "find" and not self.pattern:
|
||||
raise ValueError(
|
||||
"action='find' requires 'pattern' (text to search for inside files)."
|
||||
)
|
||||
if self.action == "search" and not self.pattern:
|
||||
raise ValueError("action='search' requires 'pattern' (glob, e.g. '*.py').")
|
||||
if self.action == "chmod" and not (self.mode or self.owner or self.group):
|
||||
raise ValueError(
|
||||
"action='chmod' requires at least one of 'mode', 'owner', or 'group'."
|
||||
)
|
||||
if self.action == "replace":
|
||||
if not self.paths:
|
||||
raise ValueError(
|
||||
"action='replace' requires 'paths' (list of file paths)."
|
||||
)
|
||||
if not self.pattern:
|
||||
raise ValueError("action='replace' requires 'pattern'.")
|
||||
if self.replacement is None:
|
||||
raise ValueError("action='replace' requires 'replacement'.")
|
||||
return self
|
||||
|
||||
|
||||
@@ -75,9 +169,10 @@ class DaytonaFileTool(DaytonaBaseTool):
|
||||
|
||||
name: str = "Daytona Sandbox Files"
|
||||
description: str = (
|
||||
"Perform filesystem operations inside a Daytona sandbox: read a file, "
|
||||
"write content to a path, append content to an existing file, list a "
|
||||
"directory, delete a path, make a directory, or fetch file metadata. "
|
||||
"Perform filesystem operations inside a Daytona sandbox: read, "
|
||||
"write, append, list, delete, mkdir, info, exists, move, find "
|
||||
"(content grep), search (filename glob), chmod (permissions/owner/"
|
||||
"group), and replace (bulk find-and-replace across files). "
|
||||
"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."
|
||||
@@ -87,30 +182,79 @@ class DaytonaFileTool(DaytonaBaseTool):
|
||||
def _run(
|
||||
self,
|
||||
action: FileAction,
|
||||
path: str,
|
||||
path: str | None = None,
|
||||
content: str | None = None,
|
||||
binary: bool = False,
|
||||
recursive: bool = False,
|
||||
mode: str = "0755",
|
||||
mode: str | None = None,
|
||||
destination: str | None = None,
|
||||
pattern: str | None = None,
|
||||
replacement: str | None = None,
|
||||
paths: list[str] | None = None,
|
||||
owner: str | None = None,
|
||||
group: str | None = None,
|
||||
) -> Any:
|
||||
sandbox, should_delete = self._acquire_sandbox()
|
||||
try:
|
||||
if action == "read":
|
||||
if path is None:
|
||||
raise ValueError("action='read' requires 'path'")
|
||||
return self._read(sandbox, path, binary=binary)
|
||||
if action == "write":
|
||||
if path is None:
|
||||
raise ValueError("action='write' requires 'path'")
|
||||
return self._write(sandbox, path, content or "", binary=binary)
|
||||
if action == "append":
|
||||
if path is None:
|
||||
raise ValueError("action='append' requires 'path'")
|
||||
return self._append(sandbox, path, content or "", binary=binary)
|
||||
if action == "list":
|
||||
if path is None:
|
||||
raise ValueError("action='list' requires 'path'")
|
||||
return self._list(sandbox, path)
|
||||
if action == "delete":
|
||||
if path is None:
|
||||
raise ValueError("action='delete' requires 'path'")
|
||||
sandbox.fs.delete_file(path, recursive=recursive)
|
||||
return {"status": "deleted", "path": path}
|
||||
if action == "mkdir":
|
||||
sandbox.fs.create_folder(path, mode)
|
||||
return {"status": "created", "path": path, "mode": mode}
|
||||
if path is None:
|
||||
raise ValueError("action='mkdir' requires 'path'")
|
||||
mkdir_mode = mode or "0755"
|
||||
sandbox.fs.create_folder(path, mkdir_mode)
|
||||
return {"status": "created", "path": path, "mode": mkdir_mode}
|
||||
if action == "info":
|
||||
if path is None:
|
||||
raise ValueError("action='info' requires 'path'")
|
||||
return self._info(sandbox, path)
|
||||
if action == "exists":
|
||||
if path is None:
|
||||
raise ValueError("action='exists' requires 'path'")
|
||||
return self._exists(sandbox, path)
|
||||
if action == "move":
|
||||
if path is None or destination is None:
|
||||
raise ValueError("action='move' requires 'path' and 'destination'")
|
||||
sandbox.fs.move_files(path, destination)
|
||||
return {"status": "moved", "from": path, "to": destination}
|
||||
if action == "find":
|
||||
if path is None or pattern is None:
|
||||
raise ValueError("action='find' requires 'path' and 'pattern'")
|
||||
return self._find(sandbox, path, pattern)
|
||||
if action == "search":
|
||||
if path is None or pattern is None:
|
||||
raise ValueError("action='search' requires 'path' and 'pattern'")
|
||||
return self._search(sandbox, path, pattern)
|
||||
if action == "chmod":
|
||||
if path is None:
|
||||
raise ValueError("action='chmod' requires 'path'")
|
||||
return self._chmod(sandbox, path, mode=mode, owner=owner, group=group)
|
||||
if action == "replace":
|
||||
if paths is None or pattern is None or replacement is None:
|
||||
raise ValueError(
|
||||
"action='replace' requires 'paths', 'pattern', and "
|
||||
"'replacement'"
|
||||
)
|
||||
return self._replace(sandbox, paths, pattern, replacement)
|
||||
raise ValueError(f"Unknown action: {action}")
|
||||
finally:
|
||||
self._release_sandbox(sandbox, should_delete)
|
||||
@@ -146,17 +290,46 @@ class DaytonaFileTool(DaytonaBaseTool):
|
||||
) -> dict[str, Any]:
|
||||
chunk = base64.b64decode(content) if binary else content.encode("utf-8")
|
||||
self._ensure_parent_dir(sandbox, path)
|
||||
|
||||
# Server-side `cat >>` keeps this O(chunk_size) per call. The naive
|
||||
# download-concat-reupload alternative is O(N^2) in total transfer.
|
||||
# /tmp/ is on the sandbox's ephemeral filesystem, not the host.
|
||||
temp_path = f"/tmp/.crewai-append-{uuid.uuid4().hex}" # noqa: S108
|
||||
sandbox.fs.upload_file(chunk, temp_path)
|
||||
|
||||
quoted_temp = shlex.quote(temp_path)
|
||||
quoted_target = shlex.quote(path)
|
||||
response = sandbox.process.exec(
|
||||
f"cat {quoted_temp} >> {quoted_target}; "
|
||||
f"rc=$?; rm -f {quoted_temp}; exit $rc"
|
||||
)
|
||||
|
||||
exit_code = getattr(response, "exit_code", 0)
|
||||
if exit_code != 0:
|
||||
try:
|
||||
sandbox.fs.delete_file(temp_path)
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"Best-effort temp-file cleanup failed after append "
|
||||
"error; the file may need manual deletion.",
|
||||
exc_info=True,
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"append failed: exit_code={exit_code}, "
|
||||
f"output={getattr(response, 'result', '')!r}"
|
||||
)
|
||||
|
||||
try:
|
||||
existing: bytes = sandbox.fs.download_file(path)
|
||||
info = sandbox.fs.get_file_info(path)
|
||||
total_bytes = getattr(info, "size", None)
|
||||
except Exception:
|
||||
existing = b""
|
||||
payload = existing + chunk
|
||||
sandbox.fs.upload_file(payload, path)
|
||||
total_bytes = None
|
||||
|
||||
return {
|
||||
"status": "appended",
|
||||
"path": path,
|
||||
"appended_bytes": len(chunk),
|
||||
"total_bytes": len(payload),
|
||||
"total_bytes": total_bytes,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -190,6 +363,77 @@ class DaytonaFileTool(DaytonaBaseTool):
|
||||
def _info(self, sandbox: Any, path: str) -> dict[str, Any]:
|
||||
return self._file_info_to_dict(sandbox.fs.get_file_info(path))
|
||||
|
||||
def _exists(self, sandbox: Any, path: str) -> dict[str, Any]:
|
||||
try:
|
||||
info = sandbox.fs.get_file_info(path)
|
||||
except Exception:
|
||||
return {"path": path, "exists": False}
|
||||
return {
|
||||
"path": path,
|
||||
"exists": True,
|
||||
"is_dir": getattr(info, "is_dir", False),
|
||||
}
|
||||
|
||||
def _find(self, sandbox: Any, path: str, pattern: str) -> dict[str, Any]:
|
||||
matches = sandbox.fs.find_files(path, pattern)
|
||||
return {
|
||||
"path": path,
|
||||
"pattern": pattern,
|
||||
"matches": [
|
||||
{
|
||||
"file": getattr(m, "file", None),
|
||||
"line": getattr(m, "line", None),
|
||||
"content": getattr(m, "content", None),
|
||||
}
|
||||
for m in matches
|
||||
],
|
||||
}
|
||||
|
||||
def _search(self, sandbox: Any, path: str, pattern: str) -> dict[str, Any]:
|
||||
response = sandbox.fs.search_files(path, pattern)
|
||||
files = getattr(response, "files", None) or []
|
||||
return {"path": path, "pattern": pattern, "files": list(files)}
|
||||
|
||||
def _chmod(
|
||||
self,
|
||||
sandbox: Any,
|
||||
path: str,
|
||||
*,
|
||||
mode: str | None,
|
||||
owner: str | None,
|
||||
group: str | None,
|
||||
) -> dict[str, Any]:
|
||||
kwargs: dict[str, str] = {}
|
||||
if mode is not None:
|
||||
kwargs["mode"] = mode
|
||||
if owner is not None:
|
||||
kwargs["owner"] = owner
|
||||
if group is not None:
|
||||
kwargs["group"] = group
|
||||
sandbox.fs.set_file_permissions(path, **kwargs)
|
||||
return {"status": "permissions_set", "path": path, **kwargs}
|
||||
|
||||
def _replace(
|
||||
self,
|
||||
sandbox: Any,
|
||||
paths: list[str],
|
||||
pattern: str,
|
||||
replacement: str,
|
||||
) -> dict[str, Any]:
|
||||
results = sandbox.fs.replace_in_files(paths, pattern, replacement)
|
||||
return {
|
||||
"pattern": pattern,
|
||||
"replacement": replacement,
|
||||
"results": [
|
||||
{
|
||||
"file": getattr(r, "file", None),
|
||||
"success": getattr(r, "success", None),
|
||||
"error": getattr(r, "error", None),
|
||||
}
|
||||
for r in (results or [])
|
||||
],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _file_info_to_dict(info: Any) -> dict[str, Any]:
|
||||
fields = (
|
||||
|
||||
Reference in New Issue
Block a user