deepagents 0.3.7a1__py3-none-any.whl → 0.3.9__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.
@@ -31,6 +31,39 @@ class FilesystemBackend(BackendProtocol):
31
31
  Files are accessed using their actual filesystem paths. Relative paths are
32
32
  resolved relative to the current working directory. Content is read/written
33
33
  as plain text, and metadata (timestamps) are derived from filesystem stats.
34
+
35
+ !!! warning "Security Warning"
36
+
37
+ This backend grants agents direct filesystem read/write access. Use with
38
+ caution and only in appropriate environments.
39
+
40
+ **Appropriate use cases:**
41
+
42
+ - Local development CLIs (coding assistants, development tools)
43
+ - CI/CD pipelines (see security considerations below)
44
+
45
+ **Inappropriate use cases:**
46
+
47
+ - Web servers or HTTP APIs - use `StateBackend`, `StoreBackend`, or
48
+ `SandboxBackend` instead
49
+
50
+ **Security risks:**
51
+
52
+ - Agents can read any accessible file, including secrets (API keys,
53
+ credentials, `.env` files)
54
+ - Combined with network tools, secrets may be exfiltrated via SSRF attacks
55
+ - File modifications are permanent and irreversible
56
+
57
+ **Recommended safeguards:**
58
+
59
+ 1. Enable Human-in-the-Loop (HITL) middleware to review sensitive operations
60
+ 2. Exclude secrets from accessible filesystem paths (especially in CI/CD)
61
+ 3. Use `SandboxBackend` for production environments requiring filesystem
62
+ interaction
63
+ 4. **Always** use `virtual_mode=True` with `root_dir` to enable path-based
64
+ access restrictions (blocks `..`, `~`, and absolute paths outside root).
65
+ Note that the default (`virtual_mode=False`) provides no security even with
66
+ `root_dir` set.
34
67
  """
35
68
 
36
69
  def __init__(
@@ -44,14 +77,29 @@ class FilesystemBackend(BackendProtocol):
44
77
  Args:
45
78
  root_dir: Optional root directory for file operations.
46
79
 
47
- If provided, all file paths will be resolved relative to this directory.
48
- If not provided, uses the current working directory.
49
- virtual_mode: Enables sandboxed operation where all paths are treated as
50
- virtual paths rooted at `root_dir`.
80
+ - If not provided, defaults to the current working directory.
81
+ - When `virtual_mode=False` (default): Only affects relative path
82
+ resolution. Provides **no security** - agents can access any file
83
+ using absolute paths or `..` sequences.
84
+ - When `virtual_mode=True`: All paths are restricted to this
85
+ directory with traversal protection enabled.
86
+
87
+ virtual_mode: Enable path-based access restrictions.
88
+
89
+ When `True`, all paths are treated as virtual paths anchored to
90
+ `root_dir`. Path traversal (`..`, `~`) is blocked and all resolved paths
91
+ are verified to remain within `root_dir`.
92
+
93
+ When `False` (default), **no security is provided**:
94
+
95
+ - Absolute paths (e.g., `/etc/passwd`) bypass `root_dir` entirely
96
+ - Relative paths with `..` can escape `root_dir`
97
+ - Agents have unrestricted filesystem access
98
+
99
+ **Security note:** `virtual_mode=True` provides path-based access
100
+ control, not process isolation. It restricts which files can be
101
+ accessed via paths, but does not sandbox the Python process itself.
51
102
 
52
- Path traversal (using `..` or `~`) is disallowed and all resolved paths
53
- must remain within the root directory. When `False` (default), absolute
54
- paths are allowed as-is and relative paths resolve under cwd.
55
103
  max_file_size_mb: Maximum file size in megabytes for operations like
56
104
  grep's Python fallback search.
57
105
 
@@ -46,12 +46,32 @@ for m in matches:
46
46
  print(json.dumps(result))
47
47
  " 2>/dev/null"""
48
48
 
49
+ # Use heredoc to pass content via stdin to avoid ARG_MAX limits on large files.
50
+ # ARG_MAX limits the total size of command-line arguments.
51
+ # Previously, base64-encoded content was interpolated directly into the command
52
+ # string, which would fail for files larger than ~100KB after base64 expansion.
53
+ # Heredocs bypass this by passing data through stdin rather than as arguments.
54
+ # Stdin format: first line is base64-encoded file path, second line is base64-encoded content.
49
55
  _WRITE_COMMAND_TEMPLATE = """python3 -c "
50
56
  import os
51
57
  import sys
52
58
  import base64
59
+ import json
53
60
 
54
- file_path = '{file_path}'
61
+ # Read JSON payload from stdin containing file_path and content (both base64-encoded)
62
+ payload_b64 = sys.stdin.read().strip()
63
+ if not payload_b64:
64
+ print('Error: No payload received for write operation', file=sys.stderr)
65
+ sys.exit(1)
66
+
67
+ try:
68
+ payload = base64.b64decode(payload_b64).decode('utf-8')
69
+ data = json.loads(payload)
70
+ file_path = data['path']
71
+ content = base64.b64decode(data['content']).decode('utf-8')
72
+ except Exception as e:
73
+ print(f'Error: Failed to decode write payload: {e}', file=sys.stderr)
74
+ sys.exit(1)
55
75
 
56
76
  # Check if file already exists (atomic with write)
57
77
  if os.path.exists(file_path):
@@ -62,24 +82,46 @@ if os.path.exists(file_path):
62
82
  parent_dir = os.path.dirname(file_path) or '.'
63
83
  os.makedirs(parent_dir, exist_ok=True)
64
84
 
65
- # Decode and write content
66
- content = base64.b64decode('{content_b64}').decode('utf-8')
67
85
  with open(file_path, 'w') as f:
68
86
  f.write(content)
69
- " 2>&1"""
70
-
87
+ " <<'__DEEPAGENTS_EOF__'
88
+ {payload_b64}
89
+ __DEEPAGENTS_EOF__"""
90
+
91
+ # Use heredoc to pass edit parameters via stdin to avoid ARG_MAX limits.
92
+ # Stdin format: base64-encoded JSON with {"path": str, "old": str, "new": str}.
93
+ # JSON bundles all parameters; base64 ensures safe transport of arbitrary content
94
+ # (special chars, newlines, etc.) through the heredoc without escaping issues.
71
95
  _EDIT_COMMAND_TEMPLATE = """python3 -c "
72
96
  import sys
73
97
  import base64
98
+ import json
99
+ import os
100
+
101
+ # Read and decode JSON payload from stdin
102
+ payload_b64 = sys.stdin.read().strip()
103
+ if not payload_b64:
104
+ print('Error: No payload received for edit operation', file=sys.stderr)
105
+ sys.exit(4)
106
+
107
+ try:
108
+ payload = base64.b64decode(payload_b64).decode('utf-8')
109
+ data = json.loads(payload)
110
+ file_path = data['path']
111
+ old = data['old']
112
+ new = data['new']
113
+ except Exception as e:
114
+ print(f'Error: Failed to decode edit payload: {e}', file=sys.stderr)
115
+ sys.exit(4)
116
+
117
+ # Check if file exists
118
+ if not os.path.isfile(file_path):
119
+ sys.exit(3) # File not found
74
120
 
75
121
  # Read file content
76
- with open('{file_path}', 'r') as f:
122
+ with open(file_path, 'r') as f:
77
123
  text = f.read()
78
124
 
79
- # Decode base64-encoded strings
80
- old = base64.b64decode('{old_b64}').decode('utf-8')
81
- new = base64.b64decode('{new_b64}').decode('utf-8')
82
-
83
125
  # Count occurrences
84
126
  count = text.count(old)
85
127
 
@@ -96,11 +138,13 @@ else:
96
138
  result = text.replace(old, new, 1)
97
139
 
98
140
  # Write back to file
99
- with open('{file_path}', 'w') as f:
141
+ with open(file_path, 'w') as f:
100
142
  f.write(result)
101
143
 
102
144
  print(count)
103
- " 2>&1"""
145
+ " <<'__DEEPAGENTS_EOF__'
146
+ {payload_b64}
147
+ __DEEPAGENTS_EOF__"""
104
148
 
105
149
  _READ_COMMAND_TEMPLATE = """python3 -c "
106
150
  import os
@@ -221,11 +265,14 @@ except PermissionError:
221
265
  content: str,
222
266
  ) -> WriteResult:
223
267
  """Create a new file. Returns WriteResult; error populated on failure."""
224
- # Encode content as base64 to avoid any escaping issues
268
+ # Create JSON payload with file path and base64-encoded content
269
+ # This avoids shell injection via file_path and ARG_MAX limits on content
225
270
  content_b64 = base64.b64encode(content.encode("utf-8")).decode("ascii")
271
+ payload = json.dumps({"path": file_path, "content": content_b64})
272
+ payload_b64 = base64.b64encode(payload.encode("utf-8")).decode("ascii")
226
273
 
227
274
  # Single atomic check + write command
228
- cmd = _WRITE_COMMAND_TEMPLATE.format(file_path=file_path, content_b64=content_b64)
275
+ cmd = _WRITE_COMMAND_TEMPLATE.format(payload_b64=payload_b64)
229
276
  result = self.execute(cmd)
230
277
 
231
278
  # Check for errors (exit code or error message in output)
@@ -244,23 +291,29 @@ except PermissionError:
244
291
  replace_all: bool = False,
245
292
  ) -> EditResult:
246
293
  """Edit a file by replacing string occurrences. Returns EditResult."""
247
- # Encode strings as base64 to avoid any escaping issues
248
- old_b64 = base64.b64encode(old_string.encode("utf-8")).decode("ascii")
249
- new_b64 = base64.b64encode(new_string.encode("utf-8")).decode("ascii")
294
+ # Create JSON payload with file path, old string, and new string
295
+ # This avoids shell injection via file_path and ARG_MAX limits on strings
296
+ payload = json.dumps({"path": file_path, "old": old_string, "new": new_string})
297
+ payload_b64 = base64.b64encode(payload.encode("utf-8")).decode("ascii")
250
298
 
251
299
  # Use template for string replacement
252
- cmd = _EDIT_COMMAND_TEMPLATE.format(file_path=file_path, old_b64=old_b64, new_b64=new_b64, replace_all=replace_all)
300
+ cmd = _EDIT_COMMAND_TEMPLATE.format(payload_b64=payload_b64, replace_all=replace_all)
253
301
  result = self.execute(cmd)
254
302
 
255
303
  exit_code = result.exit_code
256
304
  output = result.output.strip()
257
305
 
258
- if exit_code == 1:
259
- return EditResult(error=f"Error: String not found in file: '{old_string}'")
260
- if exit_code == 2:
261
- return EditResult(error=f"Error: String '{old_string}' appears multiple times. Use replace_all=True to replace all occurrences.")
306
+ # Map exit codes to error messages
307
+ error_messages = {
308
+ 1: f"Error: String not found in file: '{old_string}'",
309
+ 2: f"Error: String '{old_string}' appears multiple times. Use replace_all=True to replace all occurrences.",
310
+ 3: f"Error: File '{file_path}' not found",
311
+ 4: f"Error: Failed to decode edit payload: {output}",
312
+ }
313
+ if exit_code in error_messages:
314
+ return EditResult(error=error_messages[exit_code])
262
315
  if exit_code != 0:
263
- return EditResult(error=f"Error: File '{file_path}' not found")
316
+ return EditResult(error=f"Error editing file (exit code {exit_code}): {output or 'Unknown error'}")
264
317
 
265
318
  count = int(output)
266
319
  # External storage - no files_update needed
deepagents/graph.py CHANGED
@@ -5,7 +5,6 @@ from typing import Any
5
5
 
6
6
  from langchain.agents import create_agent
7
7
  from langchain.agents.middleware import HumanInTheLoopMiddleware, InterruptOnConfig, TodoListMiddleware
8
- from langchain.agents.middleware.summarization import SummarizationMiddleware
9
8
  from langchain.agents.middleware.types import AgentMiddleware
10
9
  from langchain.agents.structured_output import ResponseFormat
11
10
  from langchain.chat_models import init_chat_model
@@ -26,6 +25,7 @@ from deepagents.middleware.memory import MemoryMiddleware
26
25
  from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware
27
26
  from deepagents.middleware.skills import SkillsMiddleware
28
27
  from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
28
+ from deepagents.middleware.summarization import SummarizationMiddleware
29
29
 
30
30
  BASE_AGENT_PROMPT = "In order to complete the objective that the user asks of you, you have access to a number of standard tools."
31
31
 
@@ -38,7 +38,7 @@ def get_default_model() -> ChatAnthropic:
38
38
  """
39
39
  return ChatAnthropic(
40
40
  model_name="claude-sonnet-4-5-20250929",
41
- max_tokens=20000,
41
+ max_tokens=20000, # type: ignore[call-arg]
42
42
  )
43
43
 
44
44
 
@@ -63,11 +63,14 @@ def create_deep_agent(
63
63
  ) -> CompiledStateGraph:
64
64
  """Create a deep agent.
65
65
 
66
- Deep agents require a LLM that supports tool calling.
66
+ !!! warning "Deep agents require a LLM that supports tool calling!"
67
67
 
68
- This agent will by default have access to a tool to write todos (`write_todos`),
69
- seven file and execution tools: `ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`, `execute`,
70
- and a tool to call subagents (`task`).
68
+ By default, this agent has access to the following tools:
69
+
70
+ - `write_todos`: manage a todo list
71
+ - `ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`: file operations
72
+ - `execute`: run shell commands
73
+ - `task`: call subagents
71
74
 
72
75
  The `execute` tool allows running shell commands if the backend implements `SandboxBackendProtocol`.
73
76
  For non-sandbox backends, the `execute` tool will return an error message.
@@ -82,10 +85,14 @@ def create_deep_agent(
82
85
 
83
86
  In addition to custom tools you provide, deep agents include built-in tools for planning,
84
87
  file management, and subagent spawning.
85
- system_prompt: The additional instructions the agent should have.
86
-
87
- Will go in the system prompt. Can be a string or a `SystemMessage`.
88
- middleware: Additional middleware to apply after standard middleware.
88
+ system_prompt: Custom system instructions to prepend before the base deep agent
89
+ prompt.
90
+
91
+ If a string, it's concatenated with the base prompt.
92
+ middleware: Additional middleware to apply after the standard middleware stack
93
+ (`TodoListMiddleware`, `FilesystemMiddleware`, `SubAgentMiddleware`,
94
+ `SummarizationMiddleware`, `AnthropicPromptCachingMiddleware`,
95
+ `PatchToolCallsMiddleware`).
89
96
  subagents: The subagents to use.
90
97
 
91
98
  Each subagent should be a `dict` with the following keys:
@@ -142,9 +149,17 @@ def create_deep_agent(
142
149
  ):
143
150
  trigger = ("fraction", 0.85)
144
151
  keep = ("fraction", 0.10)
152
+ truncate_args_settings = {
153
+ "trigger": ("fraction", 0.85),
154
+ "keep": ("fraction", 0.10),
155
+ }
145
156
  else:
146
157
  trigger = ("tokens", 170000)
147
158
  keep = ("messages", 6)
159
+ truncate_args_settings = {
160
+ "trigger": ("messages", 20),
161
+ "keep": ("messages", 20),
162
+ }
148
163
 
149
164
  # Build middleware stack for subagents (includes skills if provided)
150
165
  subagent_middleware: list[AgentMiddleware] = [
@@ -160,9 +175,11 @@ def create_deep_agent(
160
175
  FilesystemMiddleware(backend=backend),
161
176
  SummarizationMiddleware(
162
177
  model=model,
178
+ backend=backend,
163
179
  trigger=trigger,
164
180
  keep=keep,
165
181
  trim_tokens_to_summarize=None,
182
+ truncate_args_settings=truncate_args_settings,
166
183
  ),
167
184
  AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
168
185
  PatchToolCallsMiddleware(),
@@ -190,9 +207,11 @@ def create_deep_agent(
190
207
  ),
191
208
  SummarizationMiddleware(
192
209
  model=model,
210
+ backend=backend,
193
211
  trigger=trigger,
194
212
  keep=keep,
195
213
  trim_tokens_to_summarize=None,
214
+ truncate_args_settings=truncate_args_settings,
196
215
  ),
197
216
  AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
198
217
  PatchToolCallsMiddleware(),
@@ -1,9 +1,10 @@
1
- """Middleware for the DeepAgent."""
1
+ """Middleware for the agent."""
2
2
 
3
3
  from deepagents.middleware.filesystem import FilesystemMiddleware
4
4
  from deepagents.middleware.memory import MemoryMiddleware
5
5
  from deepagents.middleware.skills import SkillsMiddleware
6
6
  from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
7
+ from deepagents.middleware.summarization import SummarizationMiddleware
7
8
 
8
9
  __all__ = [
9
10
  "CompiledSubAgent",
@@ -12,4 +13,5 @@ __all__ = [
12
13
  "SkillsMiddleware",
13
14
  "SubAgent",
14
15
  "SubAgentMiddleware",
16
+ "SummarizationMiddleware",
15
17
  ]