deepagents 0.3.9__py3-none-any.whl → 0.3.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepagents/__init__.py +3 -1
- deepagents/_version.py +3 -0
- deepagents/backends/__init__.py +2 -0
- deepagents/backends/composite.py +2 -2
- deepagents/backends/filesystem.py +13 -21
- deepagents/backends/local_shell.py +305 -0
- deepagents/backends/sandbox.py +357 -3
- deepagents/backends/utils.py +69 -24
- deepagents/middleware/filesystem.py +35 -9
- deepagents/middleware/skills.py +1 -1
- deepagents/middleware/subagents.py +23 -9
- deepagents/py.typed +0 -0
- deepagents-0.3.10.dist-info/METADATA +76 -0
- deepagents-0.3.10.dist-info/RECORD +25 -0
- deepagents-0.3.9.dist-info/METADATA +0 -527
- deepagents-0.3.9.dist-info/RECORD +0 -22
- {deepagents-0.3.9.dist-info → deepagents-0.3.10.dist-info}/WHEEL +0 -0
- {deepagents-0.3.9.dist-info → deepagents-0.3.10.dist-info}/top_level.txt +0 -0
deepagents/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Deep Agents package."""
|
|
2
2
|
|
|
3
|
+
from deepagents._version import __version__
|
|
3
4
|
from deepagents.graph import create_deep_agent
|
|
4
5
|
from deepagents.middleware.filesystem import FilesystemMiddleware
|
|
5
6
|
from deepagents.middleware.memory import MemoryMiddleware
|
|
@@ -11,5 +12,6 @@ __all__ = [
|
|
|
11
12
|
"MemoryMiddleware",
|
|
12
13
|
"SubAgent",
|
|
13
14
|
"SubAgentMiddleware",
|
|
15
|
+
"__version__",
|
|
14
16
|
"create_deep_agent",
|
|
15
17
|
]
|
deepagents/_version.py
ADDED
deepagents/backends/__init__.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from deepagents.backends.composite import CompositeBackend
|
|
4
4
|
from deepagents.backends.filesystem import FilesystemBackend
|
|
5
|
+
from deepagents.backends.local_shell import LocalShellBackend
|
|
5
6
|
from deepagents.backends.protocol import BackendProtocol
|
|
6
7
|
from deepagents.backends.state import StateBackend
|
|
7
8
|
from deepagents.backends.store import StoreBackend
|
|
@@ -10,6 +11,7 @@ __all__ = [
|
|
|
10
11
|
"BackendProtocol",
|
|
11
12
|
"CompositeBackend",
|
|
12
13
|
"FilesystemBackend",
|
|
14
|
+
"LocalShellBackend",
|
|
13
15
|
"StateBackend",
|
|
14
16
|
"StoreBackend",
|
|
15
17
|
]
|
deepagents/backends/composite.py
CHANGED
|
@@ -222,13 +222,13 @@ class CompositeBackend(BackendProtocol):
|
|
|
222
222
|
path: str | None = None,
|
|
223
223
|
glob: str | None = None,
|
|
224
224
|
) -> list[GrepMatch] | str:
|
|
225
|
-
"""Search files for
|
|
225
|
+
"""Search files for literal text pattern.
|
|
226
226
|
|
|
227
227
|
Routes to backends based on path: specific route searches one backend,
|
|
228
228
|
"/" or None searches all backends, otherwise searches default backend.
|
|
229
229
|
|
|
230
230
|
Args:
|
|
231
|
-
pattern:
|
|
231
|
+
pattern: Literal text to search for (NOT regex).
|
|
232
232
|
path: Directory to search. None searches all backends.
|
|
233
233
|
glob: Glob pattern to filter files (e.g., "*.py", "**/*.txt").
|
|
234
234
|
Filters by filename, not content.
|
|
@@ -388,25 +388,18 @@ class FilesystemBackend(BackendProtocol):
|
|
|
388
388
|
path: str | None = None,
|
|
389
389
|
glob: str | None = None,
|
|
390
390
|
) -> list[GrepMatch] | str:
|
|
391
|
-
"""Search for a
|
|
391
|
+
"""Search for a literal text pattern in files.
|
|
392
392
|
|
|
393
|
-
Uses ripgrep if available, falling back to Python
|
|
393
|
+
Uses ripgrep if available, falling back to Python search.
|
|
394
394
|
|
|
395
395
|
Args:
|
|
396
|
-
pattern:
|
|
396
|
+
pattern: Literal string to search for (NOT regex).
|
|
397
397
|
path: Directory or file path to search in. Defaults to current directory.
|
|
398
398
|
glob: Optional glob pattern to filter which files to search.
|
|
399
399
|
|
|
400
400
|
Returns:
|
|
401
401
|
List of GrepMatch dicts containing path, line number, and matched text.
|
|
402
|
-
Returns an error string if the regex pattern is invalid.
|
|
403
402
|
"""
|
|
404
|
-
# Validate regex
|
|
405
|
-
try:
|
|
406
|
-
re.compile(pattern)
|
|
407
|
-
except re.error as e:
|
|
408
|
-
return f"Invalid regex pattern: {e}"
|
|
409
|
-
|
|
410
403
|
# Resolve base path
|
|
411
404
|
try:
|
|
412
405
|
base_full = self._resolve_path(path or ".")
|
|
@@ -416,10 +409,11 @@ class FilesystemBackend(BackendProtocol):
|
|
|
416
409
|
if not base_full.exists():
|
|
417
410
|
return []
|
|
418
411
|
|
|
419
|
-
# Try ripgrep first
|
|
412
|
+
# Try ripgrep first (with -F flag for literal search)
|
|
420
413
|
results = self._ripgrep_search(pattern, base_full, glob)
|
|
421
414
|
if results is None:
|
|
422
|
-
|
|
415
|
+
# Python fallback needs escaped pattern for literal search
|
|
416
|
+
results = self._python_search(re.escape(pattern), base_full, glob)
|
|
423
417
|
|
|
424
418
|
matches: list[GrepMatch] = []
|
|
425
419
|
for fpath, items in results.items():
|
|
@@ -428,10 +422,10 @@ class FilesystemBackend(BackendProtocol):
|
|
|
428
422
|
return matches
|
|
429
423
|
|
|
430
424
|
def _ripgrep_search(self, pattern: str, base_full: Path, include_glob: str | None) -> dict[str, list[tuple[int, str]]] | None:
|
|
431
|
-
"""Search using ripgrep with
|
|
425
|
+
"""Search using ripgrep with fixed-string (literal) mode.
|
|
432
426
|
|
|
433
427
|
Args:
|
|
434
|
-
pattern:
|
|
428
|
+
pattern: Literal string to search for (unescaped).
|
|
435
429
|
base_full: Resolved base path to search in.
|
|
436
430
|
include_glob: Optional glob pattern to filter files.
|
|
437
431
|
|
|
@@ -439,7 +433,7 @@ class FilesystemBackend(BackendProtocol):
|
|
|
439
433
|
Dict mapping file paths to list of `(line_number, line_text)` tuples.
|
|
440
434
|
Returns `None` if ripgrep is unavailable or times out.
|
|
441
435
|
"""
|
|
442
|
-
cmd = ["rg", "--json"]
|
|
436
|
+
cmd = ["rg", "--json", "-F"] # -F enables fixed-string (literal) mode
|
|
443
437
|
if include_glob:
|
|
444
438
|
cmd.extend(["--glob", include_glob])
|
|
445
439
|
cmd.extend(["--", pattern, str(base_full)])
|
|
@@ -484,22 +478,20 @@ class FilesystemBackend(BackendProtocol):
|
|
|
484
478
|
return results
|
|
485
479
|
|
|
486
480
|
def _python_search(self, pattern: str, base_full: Path, include_glob: str | None) -> dict[str, list[tuple[int, str]]]:
|
|
487
|
-
"""Fallback search using Python
|
|
481
|
+
"""Fallback search using Python when ripgrep is unavailable.
|
|
488
482
|
|
|
489
483
|
Recursively searches files, respecting `max_file_size_bytes` limit.
|
|
490
484
|
|
|
491
485
|
Args:
|
|
492
|
-
pattern:
|
|
486
|
+
pattern: Escaped regex pattern (from re.escape) for literal search.
|
|
493
487
|
base_full: Resolved base path to search in.
|
|
494
488
|
include_glob: Optional glob pattern to filter files by name.
|
|
495
489
|
|
|
496
490
|
Returns:
|
|
497
491
|
Dict mapping file paths to list of `(line_number, line_text)` tuples.
|
|
498
492
|
"""
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
except re.error:
|
|
502
|
-
return {}
|
|
493
|
+
# Compile escaped pattern once for efficiency (used in loop)
|
|
494
|
+
regex = re.compile(pattern)
|
|
503
495
|
|
|
504
496
|
results: dict[str, list[tuple[int, str]]] = {}
|
|
505
497
|
root = base_full if base_full.is_dir() else base_full.parent
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""`LocalShellBackend`: Filesystem backend with unrestricted local shell execution.
|
|
2
|
+
|
|
3
|
+
This backend extends FilesystemBackend to add shell command execution on the local
|
|
4
|
+
host system. It provides NO sandboxing or isolation - all operations run directly
|
|
5
|
+
on the host machine with full system access.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import uuid
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from deepagents.backends.filesystem import FilesystemBackend
|
|
16
|
+
from deepagents.backends.protocol import ExecuteResponse, SandboxBackendProtocol
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LocalShellBackend(FilesystemBackend, SandboxBackendProtocol):
|
|
23
|
+
"""Filesystem backend with unrestricted local shell command execution.
|
|
24
|
+
|
|
25
|
+
This backend extends `FilesystemBackend` to add shell command execution
|
|
26
|
+
capabilities. Commands are executed directly on the host system without any
|
|
27
|
+
sandboxing, process isolation, or security restrictions.
|
|
28
|
+
|
|
29
|
+
!!! warning "Security Warning"
|
|
30
|
+
|
|
31
|
+
This backend grants agents BOTH direct filesystem access AND unrestricted
|
|
32
|
+
shell execution on your local machine. Use with extreme caution and only in
|
|
33
|
+
appropriate environments.
|
|
34
|
+
|
|
35
|
+
**Appropriate use cases:**
|
|
36
|
+
|
|
37
|
+
- Local development CLIs (coding assistants, development tools)
|
|
38
|
+
- Personal development environments where you trust the agent's code
|
|
39
|
+
- CI/CD pipelines with proper secret management (see security considerations)
|
|
40
|
+
|
|
41
|
+
**Inappropriate use cases:**
|
|
42
|
+
|
|
43
|
+
- Production environments (e.g., web servers, APIs, multi-tenant systems)
|
|
44
|
+
- Processing untrusted user input or executing untrusted code
|
|
45
|
+
|
|
46
|
+
Use `StateBackend`, `StoreBackend`, or extend `BaseSandbox` for production.
|
|
47
|
+
|
|
48
|
+
**Security risks:**
|
|
49
|
+
|
|
50
|
+
- Agents can execute **arbitrary shell commands** with your user's permissions
|
|
51
|
+
- Agents can read **any accessible file**, including secrets (API keys,
|
|
52
|
+
credentials, `.env` files, SSH keys, etc.)
|
|
53
|
+
- Combined with network tools, secrets may be exfiltrated via SSRF attacks
|
|
54
|
+
- File modifications and command execution are **permanent and irreversible**
|
|
55
|
+
- Agents can install packages, modify system files, spawn processes, etc.
|
|
56
|
+
- **No process isolation** - commands run directly on your host system
|
|
57
|
+
- **No resource limits** - commands can consume unlimited CPU, memory, disk
|
|
58
|
+
|
|
59
|
+
**Recommended safeguards:**
|
|
60
|
+
|
|
61
|
+
Since shell access is unrestricted and can bypass filesystem restrictions:
|
|
62
|
+
|
|
63
|
+
1. **Enable Human-in-the-Loop (HITL) middleware** to review and approve ALL
|
|
64
|
+
operations before execution. This is STRONGLY RECOMMENDED as your primary
|
|
65
|
+
safeguard when using this backend.
|
|
66
|
+
2. Run in dedicated development environments only - never on shared or
|
|
67
|
+
production systems
|
|
68
|
+
3. Never expose to untrusted users or allow execution of untrusted code
|
|
69
|
+
4. For production environments requiring code execution, extend `BaseSandbox`
|
|
70
|
+
to create a properly isolated backend (Docker containers, VMs, or other
|
|
71
|
+
sandboxed execution environments)
|
|
72
|
+
|
|
73
|
+
!!! note
|
|
74
|
+
|
|
75
|
+
`virtual_mode=True` and path-based restrictions provide NO security
|
|
76
|
+
with shell access enabled, since commands can access any path on the system
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
```python
|
|
80
|
+
from deepagents.backends import LocalShellBackend
|
|
81
|
+
|
|
82
|
+
# Create backend with explicit environment
|
|
83
|
+
backend = LocalShellBackend(root_dir="/home/user/project", env={"PATH": "/usr/bin:/bin"})
|
|
84
|
+
|
|
85
|
+
# Execute shell commands (runs directly on host)
|
|
86
|
+
result = backend.execute("ls -la")
|
|
87
|
+
print(result.output)
|
|
88
|
+
print(result.exit_code)
|
|
89
|
+
|
|
90
|
+
# Use filesystem operations (inherited from FilesystemBackend)
|
|
91
|
+
content = backend.read("/README.md")
|
|
92
|
+
backend.write("/output.txt", "Hello world")
|
|
93
|
+
|
|
94
|
+
# Inherit all environment variables
|
|
95
|
+
backend = LocalShellBackend(root_dir="/home/user/project", inherit_env=True)
|
|
96
|
+
```
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
root_dir: str | Path | None = None,
|
|
102
|
+
*,
|
|
103
|
+
virtual_mode: bool = False,
|
|
104
|
+
timeout: float = 120.0,
|
|
105
|
+
max_output_bytes: int = 100_000,
|
|
106
|
+
env: dict[str, str] | None = None,
|
|
107
|
+
inherit_env: bool = False,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Initialize local shell backend with filesystem access.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
root_dir: Working directory for both filesystem operations and shell commands.
|
|
113
|
+
|
|
114
|
+
- If not provided, defaults to the current working directory.
|
|
115
|
+
- Shell commands execute with this as their working directory.
|
|
116
|
+
- When `virtual_mode=False` (default): Paths are used as-is. Agents can
|
|
117
|
+
access any file using absolute paths or `..` sequences.
|
|
118
|
+
- When `virtual_mode=True`: Acts as a virtual root for filesystem operations.
|
|
119
|
+
Useful with `CompositeBackend` to support routing file operations across
|
|
120
|
+
different backend implementations. **Note:** This does NOT restrict shell
|
|
121
|
+
commands.
|
|
122
|
+
|
|
123
|
+
virtual_mode: Enable virtual path mode for filesystem operations.
|
|
124
|
+
|
|
125
|
+
When `True`, treats `root_dir` as a virtual root filesystem. All paths
|
|
126
|
+
are interpreted relative to `root_dir` (e.g., `/file.txt` maps to
|
|
127
|
+
`{root_dir}/file.txt`). Path traversal (`..`, `~`) is blocked.
|
|
128
|
+
|
|
129
|
+
**Primary use case:** Working with `CompositeBackend`, which routes
|
|
130
|
+
different path prefixes to different backends. Virtual mode allows the
|
|
131
|
+
CompositeBackend to strip route prefixes and pass normalized paths to
|
|
132
|
+
each backend, enabling file operations to work correctly across multiple
|
|
133
|
+
backend implementations.
|
|
134
|
+
|
|
135
|
+
**Important:** This only affects filesystem operations. Shell commands
|
|
136
|
+
executed via `execute()` are NOT restricted and can access any path.
|
|
137
|
+
|
|
138
|
+
timeout: Maximum time in seconds to wait for shell command execution.
|
|
139
|
+
Commands exceeding this timeout will be terminated. Defaults to 120 seconds.
|
|
140
|
+
|
|
141
|
+
max_output_bytes: Maximum number of bytes to capture from command output.
|
|
142
|
+
Output exceeding this limit will be truncated. Defaults to 100,000 bytes.
|
|
143
|
+
|
|
144
|
+
env: Environment variables for shell commands. If None, starts with an empty
|
|
145
|
+
environment (unless `inherit_env=True`).
|
|
146
|
+
|
|
147
|
+
inherit_env: Whether to inherit the parent process's environment variables.
|
|
148
|
+
When False (default), only variables in `env` dict are available.
|
|
149
|
+
When True, inherits all `os.environ` variables and applies `env` overrides.
|
|
150
|
+
"""
|
|
151
|
+
# Initialize parent FilesystemBackend
|
|
152
|
+
super().__init__(
|
|
153
|
+
root_dir=root_dir,
|
|
154
|
+
virtual_mode=virtual_mode,
|
|
155
|
+
max_file_size_mb=10,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Store execution parameters
|
|
159
|
+
self._timeout = timeout
|
|
160
|
+
self._max_output_bytes = max_output_bytes
|
|
161
|
+
|
|
162
|
+
# Build environment based on inherit_env setting
|
|
163
|
+
if inherit_env:
|
|
164
|
+
self._env = os.environ.copy()
|
|
165
|
+
if env is not None:
|
|
166
|
+
self._env.update(env)
|
|
167
|
+
else:
|
|
168
|
+
self._env = env if env is not None else {}
|
|
169
|
+
|
|
170
|
+
# Generate unique sandbox ID
|
|
171
|
+
self._sandbox_id = f"local-{uuid.uuid4().hex[:8]}"
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def id(self) -> str:
|
|
175
|
+
"""Unique identifier for this backend instance.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
String identifier in format "local-{random_hex}".
|
|
179
|
+
"""
|
|
180
|
+
return self._sandbox_id
|
|
181
|
+
|
|
182
|
+
def execute(
|
|
183
|
+
self,
|
|
184
|
+
command: str,
|
|
185
|
+
) -> ExecuteResponse:
|
|
186
|
+
r"""Execute a shell command directly on the host system.
|
|
187
|
+
|
|
188
|
+
!!! danger "Unrestricted Execution"
|
|
189
|
+
Commands are executed directly on your host system using `subprocess.run()`
|
|
190
|
+
with `shell=True`. There is **no sandboxing, isolation, or security
|
|
191
|
+
restrictions**. The command runs with your user's full permissions and can:
|
|
192
|
+
|
|
193
|
+
- Access any file on the filesystem (regardless of `virtual_mode`)
|
|
194
|
+
- Execute any program or script
|
|
195
|
+
- Make network connections
|
|
196
|
+
- Modify system configuration
|
|
197
|
+
- Spawn additional processes
|
|
198
|
+
- Install packages or modify dependencies
|
|
199
|
+
|
|
200
|
+
**Always use Human-in-the-Loop (HITL) middleware when using this method.**
|
|
201
|
+
|
|
202
|
+
The command is executed using the system shell (`/bin/sh` or equivalent) with
|
|
203
|
+
the working directory set to the backend's `root_dir`. Stdout and stderr are
|
|
204
|
+
combined into a single output stream.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
command: Shell command string to execute.
|
|
208
|
+
Examples: "python script.py", "ls -la", "grep pattern file.txt"
|
|
209
|
+
|
|
210
|
+
**Security:** This string is passed directly to the shell. Agents can
|
|
211
|
+
execute arbitrary commands including pipes, redirects, command
|
|
212
|
+
substitution, etc.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
ExecuteResponse containing:
|
|
216
|
+
- output: Combined stdout and stderr (stderr lines prefixed with [stderr])
|
|
217
|
+
- exit_code: Process exit code (0 for success, non-zero for failure)
|
|
218
|
+
- truncated: True if output was truncated due to size limits
|
|
219
|
+
|
|
220
|
+
Examples:
|
|
221
|
+
```python
|
|
222
|
+
# Run a simple command
|
|
223
|
+
result = backend.execute("echo hello")
|
|
224
|
+
assert result.output == "hello\\n"
|
|
225
|
+
assert result.exit_code == 0
|
|
226
|
+
|
|
227
|
+
# Handle errors
|
|
228
|
+
result = backend.execute("cat nonexistent.txt")
|
|
229
|
+
assert result.exit_code != 0
|
|
230
|
+
assert "[stderr]" in result.output
|
|
231
|
+
|
|
232
|
+
# Check for truncation
|
|
233
|
+
result = backend.execute("cat huge_file.txt")
|
|
234
|
+
if result.truncated:
|
|
235
|
+
print("Output was truncated")
|
|
236
|
+
|
|
237
|
+
# Commands run in root_dir, but can access any path
|
|
238
|
+
result = backend.execute("cat /etc/passwd") # Can read system files!
|
|
239
|
+
```
|
|
240
|
+
"""
|
|
241
|
+
if not command or not isinstance(command, str):
|
|
242
|
+
return ExecuteResponse(
|
|
243
|
+
output="Error: Command must be a non-empty string.",
|
|
244
|
+
exit_code=1,
|
|
245
|
+
truncated=False,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
result = subprocess.run( # noqa: S602
|
|
250
|
+
command,
|
|
251
|
+
check=False,
|
|
252
|
+
shell=True, # Intentional: designed for LLM-controlled shell execution
|
|
253
|
+
capture_output=True,
|
|
254
|
+
text=True,
|
|
255
|
+
timeout=self._timeout,
|
|
256
|
+
env=self._env,
|
|
257
|
+
cwd=str(self.cwd), # Use the root_dir from FilesystemBackend
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Combine stdout and stderr
|
|
261
|
+
# Prefix each stderr line with [stderr] for clear attribution.
|
|
262
|
+
# Example: "hello\n[stderr] error: file not found" # noqa: ERA001
|
|
263
|
+
output_parts = []
|
|
264
|
+
if result.stdout:
|
|
265
|
+
output_parts.append(result.stdout)
|
|
266
|
+
if result.stderr:
|
|
267
|
+
stderr_lines = result.stderr.strip().split("\n")
|
|
268
|
+
output_parts.extend(f"[stderr] {line}" for line in stderr_lines)
|
|
269
|
+
|
|
270
|
+
output = "\n".join(output_parts) if output_parts else "<no output>"
|
|
271
|
+
|
|
272
|
+
# Check for truncation
|
|
273
|
+
truncated = False
|
|
274
|
+
if len(output) > self._max_output_bytes:
|
|
275
|
+
output = output[: self._max_output_bytes]
|
|
276
|
+
output += f"\n\n... Output truncated at {self._max_output_bytes} bytes."
|
|
277
|
+
truncated = True
|
|
278
|
+
|
|
279
|
+
# Add exit code info if non-zero
|
|
280
|
+
if result.returncode != 0:
|
|
281
|
+
output = f"{output.rstrip()}\n\nExit code: {result.returncode}"
|
|
282
|
+
|
|
283
|
+
return ExecuteResponse(
|
|
284
|
+
output=output,
|
|
285
|
+
exit_code=result.returncode,
|
|
286
|
+
truncated=truncated,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
except subprocess.TimeoutExpired:
|
|
290
|
+
return ExecuteResponse(
|
|
291
|
+
output=f"Error: Command timed out after {self._timeout:.1f} seconds.",
|
|
292
|
+
exit_code=124, # Standard timeout exit code
|
|
293
|
+
truncated=False,
|
|
294
|
+
)
|
|
295
|
+
except Exception as e: # noqa: BLE001
|
|
296
|
+
# Broad exception catch is intentional: we want to catch all execution errors
|
|
297
|
+
# and return a consistent ExecuteResponse rather than propagating exceptions
|
|
298
|
+
return ExecuteResponse(
|
|
299
|
+
output=f"Error executing command: {e}",
|
|
300
|
+
exit_code=1,
|
|
301
|
+
truncated=False,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
__all__ = ["LocalShellBackend"]
|