deepagents 0.3.8__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 +431 -24
- deepagents/backends/utils.py +69 -24
- deepagents/middleware/filesystem.py +482 -522
- deepagents/middleware/skills.py +1 -1
- deepagents/middleware/subagents.py +23 -9
- deepagents/middleware/summarization.py +9 -4
- 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.8.dist-info → deepagents-0.3.10.dist-info}/WHEEL +1 -1
- deepagents-0.3.8.dist-info/METADATA +0 -527
- deepagents-0.3.8.dist-info/RECORD +0 -22
- {deepagents-0.3.8.dist-info → deepagents-0.3.10.dist-info}/top_level.txt +0 -0
deepagents/backends/sandbox.py
CHANGED
|
@@ -3,14 +3,21 @@
|
|
|
3
3
|
This module provides a base class that implements all SandboxBackendProtocol
|
|
4
4
|
methods using shell commands executed via execute(). Concrete implementations
|
|
5
5
|
only need to implement the execute() method.
|
|
6
|
+
|
|
7
|
+
It also defines the SandboxProvider abstract base class for third-party SDK
|
|
8
|
+
implementations to manage sandbox lifecycle (list, create, delete).
|
|
6
9
|
"""
|
|
7
10
|
|
|
8
11
|
from __future__ import annotations
|
|
9
12
|
|
|
13
|
+
import asyncio
|
|
10
14
|
import base64
|
|
11
15
|
import json
|
|
12
16
|
import shlex
|
|
13
17
|
from abc import ABC, abstractmethod
|
|
18
|
+
from typing import Any, Generic, NotRequired, TypeVar
|
|
19
|
+
|
|
20
|
+
from typing_extensions import TypedDict
|
|
14
21
|
|
|
15
22
|
from deepagents.backends.protocol import (
|
|
16
23
|
EditResult,
|
|
@@ -23,6 +30,353 @@ from deepagents.backends.protocol import (
|
|
|
23
30
|
WriteResult,
|
|
24
31
|
)
|
|
25
32
|
|
|
33
|
+
# Type variable for provider-specific metadata
|
|
34
|
+
MetadataT = TypeVar("MetadataT", covariant=True)
|
|
35
|
+
"""Type variable for sandbox metadata.
|
|
36
|
+
|
|
37
|
+
Providers can define their own TypedDict to specify the structure of sandbox metadata,
|
|
38
|
+
enabling type-safe access to metadata fields.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
```python
|
|
42
|
+
class ProviderMetadata(TypedDict, total=False):
|
|
43
|
+
status: Literal["running", "stopped"]
|
|
44
|
+
created_at: str
|
|
45
|
+
template: str
|
|
46
|
+
|
|
47
|
+
class MyProvider(SandboxProvider[ProviderMetadata]):
|
|
48
|
+
def list(
|
|
49
|
+
self, *, cursor=None, **kwargs: Any
|
|
50
|
+
) -> SandboxListResponse[ProviderMetadata]:
|
|
51
|
+
# Extract kwargs as needed
|
|
52
|
+
status = kwargs.get("status")
|
|
53
|
+
...
|
|
54
|
+
```
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SandboxInfo(TypedDict, Generic[MetadataT]):
|
|
59
|
+
"""Metadata for a single sandbox instance.
|
|
60
|
+
|
|
61
|
+
This lightweight structure is returned from list operations and provides
|
|
62
|
+
basic information about a sandbox without requiring a full connection.
|
|
63
|
+
|
|
64
|
+
Type Parameters:
|
|
65
|
+
MetadataT: Type of the metadata field. Providers should define a TypedDict
|
|
66
|
+
for type-safe metadata access.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
sandbox_id: Unique identifier for the sandbox instance.
|
|
70
|
+
metadata: Optional provider-specific metadata (e.g., creation time, status,
|
|
71
|
+
resource limits, template information). Structure is provider-defined.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
```python
|
|
75
|
+
# Using default dict[str, Any]
|
|
76
|
+
info: SandboxInfo = {
|
|
77
|
+
"sandbox_id": "sb_abc123",
|
|
78
|
+
"metadata": {"status": "running", "created_at": "2024-01-15T10:30:00Z", "template": "python-3.11"},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Using typed metadata
|
|
83
|
+
class MyMetadata(TypedDict, total=False):
|
|
84
|
+
status: Literal["running", "stopped"]
|
|
85
|
+
created_at: str
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
typed_info: SandboxInfo[MyMetadata] = {
|
|
89
|
+
"sandbox_id": "sb_abc123",
|
|
90
|
+
"metadata": {"status": "running", "created_at": "2024-01-15T10:30:00Z"},
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
sandbox_id: str
|
|
96
|
+
metadata: NotRequired[MetadataT]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class SandboxListResponse(TypedDict, Generic[MetadataT]):
|
|
100
|
+
"""Paginated response from a sandbox list operation.
|
|
101
|
+
|
|
102
|
+
This structure supports cursor-based pagination for efficiently browsing
|
|
103
|
+
large collections of sandboxes.
|
|
104
|
+
|
|
105
|
+
Type Parameters:
|
|
106
|
+
MetadataT: Type of the metadata field in SandboxInfo items.
|
|
107
|
+
|
|
108
|
+
Attributes:
|
|
109
|
+
items: List of sandbox metadata objects for the current page.
|
|
110
|
+
cursor: Opaque continuation token for retrieving the next page.
|
|
111
|
+
None indicates no more pages available. Clients should treat this
|
|
112
|
+
as an opaque string and pass it to subsequent list() calls.
|
|
113
|
+
|
|
114
|
+
Example:
|
|
115
|
+
```python
|
|
116
|
+
response: SandboxListResponse[MyMetadata] = {
|
|
117
|
+
"items": [{"sandbox_id": "sb_001", "metadata": {"status": "running"}}, {"sandbox_id": "sb_002", "metadata": {"status": "stopped"}}],
|
|
118
|
+
"cursor": "eyJvZmZzZXQiOjEwMH0=",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Fetch next page
|
|
122
|
+
next_response = provider.list(cursor=response["cursor"])
|
|
123
|
+
```
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
items: list[SandboxInfo[MetadataT]]
|
|
127
|
+
cursor: str | None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class SandboxProvider(ABC, Generic[MetadataT]):
|
|
131
|
+
"""Abstract base class for third-party sandbox provider implementations.
|
|
132
|
+
|
|
133
|
+
Defines the lifecycle management interface for sandbox providers. Implementations
|
|
134
|
+
should integrate with their respective SDKs to provide standardized sandbox
|
|
135
|
+
lifecycle operations (list, get_or_create, delete).
|
|
136
|
+
|
|
137
|
+
Implementations can add provider-specific parameters as keyword-only arguments
|
|
138
|
+
with defaults, maintaining compatibility while providing type-safe APIs.
|
|
139
|
+
|
|
140
|
+
Sync/Async Convention: Following LangChain convention, providers should offer both
|
|
141
|
+
sync and async methods in the same namespace if possible (doesn't hurt performance)
|
|
142
|
+
(e.g., both `list()` and `alist()` in one class). The default async implementations
|
|
143
|
+
delegate to sync methods via a thread pool. Providers can override async methods to
|
|
144
|
+
provide optimized async implementations if needed.
|
|
145
|
+
|
|
146
|
+
Alternatively, if necessary for performance optimization, providers may split into
|
|
147
|
+
separate implementations (e.g., `MySyncProvider` and `MyAsyncProvider`). In this
|
|
148
|
+
case, unimplemented methods should raise NotImplementedError with clear guidance
|
|
149
|
+
(e.g., "This provider only supports async operations. Use 'await provider.alist()'
|
|
150
|
+
or switch to MySyncProvider for synchronous code").
|
|
151
|
+
|
|
152
|
+
Example Implementation:
|
|
153
|
+
```python
|
|
154
|
+
class CustomMetadata(TypedDict, total=False):
|
|
155
|
+
status: Literal["running", "stopped"]
|
|
156
|
+
template: str
|
|
157
|
+
created_at: str
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class CustomSandboxProvider(SandboxProvider[CustomMetadata]):
|
|
161
|
+
def list(
|
|
162
|
+
self, *, cursor=None, status: Literal["running", "stopped"] | None = None, template_id: str | None = None, **kwargs: Any
|
|
163
|
+
) -> SandboxListResponse[CustomMetadata]:
|
|
164
|
+
# Type-safe parameters with IDE autocomplete
|
|
165
|
+
# ... query provider API
|
|
166
|
+
return {"items": [...], "cursor": None}
|
|
167
|
+
|
|
168
|
+
def get_or_create(
|
|
169
|
+
self, *, sandbox_id=None, template_id: str = "default", timeout_minutes: int | None = None, **kwargs: Any
|
|
170
|
+
) -> SandboxBackendProtocol:
|
|
171
|
+
# Type-safe parameters with IDE autocomplete
|
|
172
|
+
return CustomSandbox(sandbox_id or self._create_new(), template_id)
|
|
173
|
+
|
|
174
|
+
def delete(self, *, sandbox_id: str, force: bool = False, **kwargs: Any) -> None:
|
|
175
|
+
# Implementation
|
|
176
|
+
self._client.delete(sandbox_id, force=force)
|
|
177
|
+
```
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
@abstractmethod
|
|
181
|
+
def list(
|
|
182
|
+
self,
|
|
183
|
+
*,
|
|
184
|
+
cursor: str | None = None,
|
|
185
|
+
**kwargs: Any,
|
|
186
|
+
) -> SandboxListResponse[MetadataT]:
|
|
187
|
+
"""List available sandboxes with optional filtering and pagination.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
cursor: Optional continuation token from a previous list() call.
|
|
191
|
+
Pass None to start from the beginning. The cursor is opaque
|
|
192
|
+
and provider-specific; clients should not parse or modify it.
|
|
193
|
+
**kwargs: Provider-specific filter parameters. Implementations should
|
|
194
|
+
expose these as named keyword-only parameters with defaults for
|
|
195
|
+
type safety. Common examples include status filters, creation time
|
|
196
|
+
ranges, template filters, or owner filters.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
SandboxListResponse containing:
|
|
200
|
+
- items: List of sandbox metadata for the current page
|
|
201
|
+
- cursor: Token for next page, or None if this is the last page
|
|
202
|
+
|
|
203
|
+
Example:
|
|
204
|
+
```python
|
|
205
|
+
# First page
|
|
206
|
+
response = provider.list()
|
|
207
|
+
for sandbox in response["items"]:
|
|
208
|
+
print(sandbox["sandbox_id"])
|
|
209
|
+
|
|
210
|
+
# Next page if available
|
|
211
|
+
if response["cursor"]:
|
|
212
|
+
next_response = provider.list(cursor=response["cursor"])
|
|
213
|
+
|
|
214
|
+
# With filters (if provider supports them)
|
|
215
|
+
running = provider.list(status="running")
|
|
216
|
+
```
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
@abstractmethod
|
|
220
|
+
def get_or_create(
|
|
221
|
+
self,
|
|
222
|
+
*,
|
|
223
|
+
sandbox_id: str | None = None,
|
|
224
|
+
**kwargs: Any,
|
|
225
|
+
) -> SandboxBackendProtocol:
|
|
226
|
+
"""Get an existing sandbox or create a new one.
|
|
227
|
+
|
|
228
|
+
This method retrieves a connection to an existing sandbox if sandbox_id
|
|
229
|
+
is provided, or creates a new sandbox instance if sandbox_id is None.
|
|
230
|
+
The returned object implements SandboxBackendProtocol and can be used
|
|
231
|
+
for all sandbox operations (execute, read, write, etc.).
|
|
232
|
+
|
|
233
|
+
Important: If a sandbox_id is provided but does not exist, this method
|
|
234
|
+
should raise an error rather than creating a new sandbox. Only when
|
|
235
|
+
sandbox_id is explicitly None should a new sandbox be created.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
sandbox_id: Unique identifier of an existing sandbox to retrieve.
|
|
239
|
+
If None, creates a new sandbox instance. The new sandbox's ID
|
|
240
|
+
can be accessed via the returned object's .id property.
|
|
241
|
+
If a non-None value is provided but the sandbox doesn't exist,
|
|
242
|
+
an error will be raised.
|
|
243
|
+
**kwargs: Provider-specific creation/connection parameters. Implementations
|
|
244
|
+
should expose these as named keyword-only parameters with defaults
|
|
245
|
+
for type safety. Common examples include template_id, resource limits,
|
|
246
|
+
environment variables, or timeout settings.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
An object implementing SandboxBackendProtocol that can execute
|
|
250
|
+
commands, read/write files, and perform other sandbox operations.
|
|
251
|
+
|
|
252
|
+
Raises:
|
|
253
|
+
Implementation-specific exceptions for errors such as:
|
|
254
|
+
- Sandbox not found (if sandbox_id provided but doesn't exist)
|
|
255
|
+
- Insufficient permissions
|
|
256
|
+
- Resource limits exceeded
|
|
257
|
+
- Invalid template or configuration
|
|
258
|
+
|
|
259
|
+
Example:
|
|
260
|
+
```python
|
|
261
|
+
# Create a new sandbox
|
|
262
|
+
sandbox = provider.get_or_create(sandbox_id=None, template_id="python-3.11", timeout_minutes=60)
|
|
263
|
+
print(sandbox.id) # "sb_new123"
|
|
264
|
+
|
|
265
|
+
# Reconnect to existing sandbox
|
|
266
|
+
existing = provider.get_or_create(sandbox_id="sb_new123")
|
|
267
|
+
|
|
268
|
+
# Use the sandbox
|
|
269
|
+
result = sandbox.execute("python --version")
|
|
270
|
+
print(result.output)
|
|
271
|
+
```
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
@abstractmethod
|
|
275
|
+
def delete(
|
|
276
|
+
self,
|
|
277
|
+
*,
|
|
278
|
+
sandbox_id: str,
|
|
279
|
+
**kwargs: Any,
|
|
280
|
+
) -> None:
|
|
281
|
+
"""Delete a sandbox instance.
|
|
282
|
+
|
|
283
|
+
This permanently destroys the sandbox and all its associated data.
|
|
284
|
+
The operation is typically irreversible.
|
|
285
|
+
|
|
286
|
+
Idempotency: This method should be idempotent - calling delete on a
|
|
287
|
+
non-existent sandbox should succeed without raising an error. This makes
|
|
288
|
+
cleanup code simpler and safe to retry.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
sandbox_id: Unique identifier of the sandbox to delete.
|
|
292
|
+
**kwargs: Provider-specific deletion options. Implementations should
|
|
293
|
+
expose these as named keyword-only parameters with defaults for
|
|
294
|
+
type safety. Common examples include force flags, grace periods,
|
|
295
|
+
or cleanup options.
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
Implementation-specific exceptions for errors such as:
|
|
299
|
+
- Insufficient permissions
|
|
300
|
+
- Sandbox is locked or in use
|
|
301
|
+
- Network or API errors
|
|
302
|
+
|
|
303
|
+
Example:
|
|
304
|
+
```python
|
|
305
|
+
# Simple deletion
|
|
306
|
+
provider.delete(sandbox_id="sb_123")
|
|
307
|
+
|
|
308
|
+
# Safe to call multiple times (idempotent)
|
|
309
|
+
provider.delete(sandbox_id="sb_123") # No error even if already deleted
|
|
310
|
+
|
|
311
|
+
# With options (if provider supports them)
|
|
312
|
+
provider.delete(sandbox_id="sb_456", force=True)
|
|
313
|
+
```
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
async def alist(
|
|
317
|
+
self,
|
|
318
|
+
*,
|
|
319
|
+
cursor: str | None = None,
|
|
320
|
+
**kwargs: Any,
|
|
321
|
+
) -> SandboxListResponse[MetadataT]:
|
|
322
|
+
"""Async version of list().
|
|
323
|
+
|
|
324
|
+
By default, runs the synchronous list() method in a thread pool.
|
|
325
|
+
Providers can override this for native async implementations.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
cursor: Optional continuation token from a previous list() call.
|
|
329
|
+
**kwargs: Provider-specific filter parameters.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
SandboxListResponse containing items and cursor for pagination.
|
|
333
|
+
"""
|
|
334
|
+
return await asyncio.to_thread(self.list, cursor=cursor, **kwargs)
|
|
335
|
+
|
|
336
|
+
async def aget_or_create(
|
|
337
|
+
self,
|
|
338
|
+
*,
|
|
339
|
+
sandbox_id: str | None = None,
|
|
340
|
+
**kwargs: Any,
|
|
341
|
+
) -> SandboxBackendProtocol:
|
|
342
|
+
"""Async version of get_or_create().
|
|
343
|
+
|
|
344
|
+
By default, runs the synchronous get_or_create() method in a thread pool.
|
|
345
|
+
Providers can override this for native async implementations.
|
|
346
|
+
|
|
347
|
+
Important: If a sandbox_id is provided but does not exist, this method
|
|
348
|
+
should raise an error rather than creating a new sandbox. Only when
|
|
349
|
+
sandbox_id is explicitly None should a new sandbox be created.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
sandbox_id: Unique identifier of an existing sandbox to retrieve.
|
|
353
|
+
If None, creates a new sandbox instance. If a non-None value
|
|
354
|
+
is provided but the sandbox doesn't exist, an error will be raised.
|
|
355
|
+
**kwargs: Provider-specific creation/connection parameters.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
An object implementing SandboxBackendProtocol.
|
|
359
|
+
"""
|
|
360
|
+
return await asyncio.to_thread(self.get_or_create, sandbox_id=sandbox_id, **kwargs)
|
|
361
|
+
|
|
362
|
+
async def adelete(
|
|
363
|
+
self,
|
|
364
|
+
*,
|
|
365
|
+
sandbox_id: str,
|
|
366
|
+
**kwargs: Any,
|
|
367
|
+
) -> None:
|
|
368
|
+
"""Async version of delete().
|
|
369
|
+
|
|
370
|
+
By default, runs the synchronous delete() method in a thread pool.
|
|
371
|
+
Providers can override this for native async implementations.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
sandbox_id: Unique identifier of the sandbox to delete.
|
|
375
|
+
**kwargs: Provider-specific deletion options.
|
|
376
|
+
"""
|
|
377
|
+
await asyncio.to_thread(self.delete, sandbox_id=sandbox_id, **kwargs)
|
|
378
|
+
|
|
379
|
+
|
|
26
380
|
_GLOB_COMMAND_TEMPLATE = """python3 -c "
|
|
27
381
|
import glob
|
|
28
382
|
import os
|
|
@@ -46,40 +400,82 @@ for m in matches:
|
|
|
46
400
|
print(json.dumps(result))
|
|
47
401
|
" 2>/dev/null"""
|
|
48
402
|
|
|
403
|
+
# Use heredoc to pass content via stdin to avoid ARG_MAX limits on large files.
|
|
404
|
+
# ARG_MAX limits the total size of command-line arguments.
|
|
405
|
+
# Previously, base64-encoded content was interpolated directly into the command
|
|
406
|
+
# string, which would fail for files larger than ~100KB after base64 expansion.
|
|
407
|
+
# Heredocs bypass this by passing data through stdin rather than as arguments.
|
|
408
|
+
# Stdin format: first line is base64-encoded file path, second line is base64-encoded content.
|
|
49
409
|
_WRITE_COMMAND_TEMPLATE = """python3 -c "
|
|
50
410
|
import os
|
|
51
411
|
import sys
|
|
52
412
|
import base64
|
|
413
|
+
import json
|
|
53
414
|
|
|
54
|
-
file_path
|
|
415
|
+
# Read JSON payload from stdin containing file_path and content (both base64-encoded)
|
|
416
|
+
payload_b64 = sys.stdin.read().strip()
|
|
417
|
+
if not payload_b64:
|
|
418
|
+
print('Error: No payload received for write operation', file=sys.stderr)
|
|
419
|
+
sys.exit(1)
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
payload = base64.b64decode(payload_b64).decode('utf-8')
|
|
423
|
+
data = json.loads(payload)
|
|
424
|
+
file_path = data['path']
|
|
425
|
+
content = base64.b64decode(data['content']).decode('utf-8')
|
|
426
|
+
except Exception as e:
|
|
427
|
+
print(f'Error: Failed to decode write payload: {{e}}', file=sys.stderr)
|
|
428
|
+
sys.exit(1)
|
|
55
429
|
|
|
56
430
|
# Check if file already exists (atomic with write)
|
|
57
431
|
if os.path.exists(file_path):
|
|
58
|
-
print(f'Error: File \\'{file_path}\\' already exists', file=sys.stderr)
|
|
432
|
+
print(f'Error: File \\'{{file_path}}\\' already exists', file=sys.stderr)
|
|
59
433
|
sys.exit(1)
|
|
60
434
|
|
|
61
435
|
# Create parent directory if needed
|
|
62
436
|
parent_dir = os.path.dirname(file_path) or '.'
|
|
63
437
|
os.makedirs(parent_dir, exist_ok=True)
|
|
64
438
|
|
|
65
|
-
# Decode and write content
|
|
66
|
-
content = base64.b64decode('{content_b64}').decode('utf-8')
|
|
67
439
|
with open(file_path, 'w') as f:
|
|
68
440
|
f.write(content)
|
|
69
|
-
"
|
|
70
|
-
|
|
441
|
+
" <<'__DEEPAGENTS_EOF__'
|
|
442
|
+
{payload_b64}
|
|
443
|
+
__DEEPAGENTS_EOF__"""
|
|
444
|
+
|
|
445
|
+
# Use heredoc to pass edit parameters via stdin to avoid ARG_MAX limits.
|
|
446
|
+
# Stdin format: base64-encoded JSON with {"path": str, "old": str, "new": str}.
|
|
447
|
+
# JSON bundles all parameters; base64 ensures safe transport of arbitrary content
|
|
448
|
+
# (special chars, newlines, etc.) through the heredoc without escaping issues.
|
|
71
449
|
_EDIT_COMMAND_TEMPLATE = """python3 -c "
|
|
72
450
|
import sys
|
|
73
451
|
import base64
|
|
452
|
+
import json
|
|
453
|
+
import os
|
|
454
|
+
|
|
455
|
+
# Read and decode JSON payload from stdin
|
|
456
|
+
payload_b64 = sys.stdin.read().strip()
|
|
457
|
+
if not payload_b64:
|
|
458
|
+
print('Error: No payload received for edit operation', file=sys.stderr)
|
|
459
|
+
sys.exit(4)
|
|
460
|
+
|
|
461
|
+
try:
|
|
462
|
+
payload = base64.b64decode(payload_b64).decode('utf-8')
|
|
463
|
+
data = json.loads(payload)
|
|
464
|
+
file_path = data['path']
|
|
465
|
+
old = data['old']
|
|
466
|
+
new = data['new']
|
|
467
|
+
except Exception as e:
|
|
468
|
+
print(f'Error: Failed to decode edit payload: {{e}}', file=sys.stderr)
|
|
469
|
+
sys.exit(4)
|
|
470
|
+
|
|
471
|
+
# Check if file exists
|
|
472
|
+
if not os.path.isfile(file_path):
|
|
473
|
+
sys.exit(3) # File not found
|
|
74
474
|
|
|
75
475
|
# Read file content
|
|
76
|
-
with open(
|
|
476
|
+
with open(file_path, 'r') as f:
|
|
77
477
|
text = f.read()
|
|
78
478
|
|
|
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
479
|
# Count occurrences
|
|
84
480
|
count = text.count(old)
|
|
85
481
|
|
|
@@ -96,11 +492,13 @@ else:
|
|
|
96
492
|
result = text.replace(old, new, 1)
|
|
97
493
|
|
|
98
494
|
# Write back to file
|
|
99
|
-
with open(
|
|
495
|
+
with open(file_path, 'w') as f:
|
|
100
496
|
f.write(result)
|
|
101
497
|
|
|
102
498
|
print(count)
|
|
103
|
-
"
|
|
499
|
+
" <<'__DEEPAGENTS_EOF__'
|
|
500
|
+
{payload_b64}
|
|
501
|
+
__DEEPAGENTS_EOF__"""
|
|
104
502
|
|
|
105
503
|
_READ_COMMAND_TEMPLATE = """python3 -c "
|
|
106
504
|
import os
|
|
@@ -221,11 +619,14 @@ except PermissionError:
|
|
|
221
619
|
content: str,
|
|
222
620
|
) -> WriteResult:
|
|
223
621
|
"""Create a new file. Returns WriteResult; error populated on failure."""
|
|
224
|
-
#
|
|
622
|
+
# Create JSON payload with file path and base64-encoded content
|
|
623
|
+
# This avoids shell injection via file_path and ARG_MAX limits on content
|
|
225
624
|
content_b64 = base64.b64encode(content.encode("utf-8")).decode("ascii")
|
|
625
|
+
payload = json.dumps({"path": file_path, "content": content_b64})
|
|
626
|
+
payload_b64 = base64.b64encode(payload.encode("utf-8")).decode("ascii")
|
|
226
627
|
|
|
227
628
|
# Single atomic check + write command
|
|
228
|
-
cmd = _WRITE_COMMAND_TEMPLATE.format(
|
|
629
|
+
cmd = _WRITE_COMMAND_TEMPLATE.format(payload_b64=payload_b64)
|
|
229
630
|
result = self.execute(cmd)
|
|
230
631
|
|
|
231
632
|
# Check for errors (exit code or error message in output)
|
|
@@ -244,23 +645,29 @@ except PermissionError:
|
|
|
244
645
|
replace_all: bool = False,
|
|
245
646
|
) -> EditResult:
|
|
246
647
|
"""Edit a file by replacing string occurrences. Returns EditResult."""
|
|
247
|
-
#
|
|
248
|
-
|
|
249
|
-
|
|
648
|
+
# Create JSON payload with file path, old string, and new string
|
|
649
|
+
# This avoids shell injection via file_path and ARG_MAX limits on strings
|
|
650
|
+
payload = json.dumps({"path": file_path, "old": old_string, "new": new_string})
|
|
651
|
+
payload_b64 = base64.b64encode(payload.encode("utf-8")).decode("ascii")
|
|
250
652
|
|
|
251
653
|
# Use template for string replacement
|
|
252
|
-
cmd = _EDIT_COMMAND_TEMPLATE.format(
|
|
654
|
+
cmd = _EDIT_COMMAND_TEMPLATE.format(payload_b64=payload_b64, replace_all=replace_all)
|
|
253
655
|
result = self.execute(cmd)
|
|
254
656
|
|
|
255
657
|
exit_code = result.exit_code
|
|
256
658
|
output = result.output.strip()
|
|
257
659
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
660
|
+
# Map exit codes to error messages
|
|
661
|
+
error_messages = {
|
|
662
|
+
1: f"Error: String not found in file: '{old_string}'",
|
|
663
|
+
2: f"Error: String '{old_string}' appears multiple times. Use replace_all=True to replace all occurrences.",
|
|
664
|
+
3: f"Error: File '{file_path}' not found",
|
|
665
|
+
4: f"Error: Failed to decode edit payload: {output}",
|
|
666
|
+
}
|
|
667
|
+
if exit_code in error_messages:
|
|
668
|
+
return EditResult(error=error_messages[exit_code])
|
|
262
669
|
if exit_code != 0:
|
|
263
|
-
return EditResult(error=f"Error
|
|
670
|
+
return EditResult(error=f"Error editing file (exit code {exit_code}): {output or 'Unknown error'}")
|
|
264
671
|
|
|
265
672
|
count = int(output)
|
|
266
673
|
# External storage - no files_update needed
|