diff --git a/docs/ar/tools/ai-ml/daytona.mdx b/docs/ar/tools/ai-ml/daytona.mdx
index 9447c6a3f7..5c112b0d1e 100644
--- a/docs/ar/tools/ai-ml/daytona.mdx
+++ b/docs/ar/tools/ai-ml/daytona.mdx
@@ -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")
```
-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.
### 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. |
+
+
+For `chmod`, pass at least one of `mode`, `owner`, or `group` — any field left as `None` is left unchanged on the target.
+
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.
diff --git a/docs/docs.json b/docs/docs.json
index 4454aa5db3..60fe1b9dad 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -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"
]
},
{
diff --git a/docs/en/tools/ai-ml/daytona.mdx b/docs/en/tools/ai-ml/daytona.mdx
index 9447c6a3f7..5c112b0d1e 100644
--- a/docs/en/tools/ai-ml/daytona.mdx
+++ b/docs/en/tools/ai-ml/daytona.mdx
@@ -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")
```
-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.
### 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. |
+
+
+For `chmod`, pass at least one of `mode`, `owner`, or `group` — any field left as `None` is left unchanged on the target.
+
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.
diff --git a/docs/ko/tools/ai-ml/daytona.mdx b/docs/ko/tools/ai-ml/daytona.mdx
index 9447c6a3f7..5c112b0d1e 100644
--- a/docs/ko/tools/ai-ml/daytona.mdx
+++ b/docs/ko/tools/ai-ml/daytona.mdx
@@ -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")
```
-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.
### 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. |
+
+
+For `chmod`, pass at least one of `mode`, `owner`, or `group` — any field left as `None` is left unchanged on the target.
+
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.
diff --git a/docs/pt-BR/tools/ai-ml/daytona.mdx b/docs/pt-BR/tools/ai-ml/daytona.mdx
index 9447c6a3f7..5c112b0d1e 100644
--- a/docs/pt-BR/tools/ai-ml/daytona.mdx
+++ b/docs/pt-BR/tools/ai-ml/daytona.mdx
@@ -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")
```
-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.
### 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. |
+
+
+For `chmod`, pass at least one of `mode`, `owner`, or `group` — any field left as `None` is left unchanged on the target.
+
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.
diff --git a/lib/crewai-tools/src/crewai_tools/tools/daytona_sandbox_tool/README.md b/lib/crewai-tools/src/crewai_tools/tools/daytona_sandbox_tool/README.md
index a2365049e0..c5163884d6 100644
--- a/lib/crewai-tools/src/crewai_tools/tools/daytona_sandbox_tool/README.md
+++ b/lib/crewai-tools/src/crewai_tools/tools/daytona_sandbox_tool/README.md
@@ -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`.
diff --git a/lib/crewai-tools/src/crewai_tools/tools/daytona_sandbox_tool/daytona_base_tool.py b/lib/crewai-tools/src/crewai_tools/tools/daytona_sandbox_tool/daytona_base_tool.py
index b601e4309d..042245bf77 100644
--- a/lib/crewai-tools/src/crewai_tools/tools/daytona_sandbox_tool/daytona_base_tool.py
+++ b/lib/crewai-tools/src/crewai_tools/tools/daytona_sandbox_tool/daytona_base_tool.py
@@ -196,3 +196,27 @@ def close(self) -> None:
"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)
diff --git a/lib/crewai-tools/src/crewai_tools/tools/daytona_sandbox_tool/daytona_file_tool.py b/lib/crewai-tools/src/crewai_tools/tools/daytona_sandbox_tool/daytona_file_tool.py
index e019419b3e..da8ea77342 100644
--- a/lib/crewai-tools/src/crewai_tools/tools/daytona_sandbox_tool/daytona_file_tool.py
+++ b/lib/crewai-tools/src/crewai_tools/tools/daytona_sandbox_tool/daytona_file_tool.py
@@ -4,6 +4,8 @@
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 @@
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 @@ def _append(
) -> 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 @@ def _list(self, sandbox: Any, path: str) -> dict[str, Any]:
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 = (