langchain-agentcore-codeinterpreter 0.0.3__tar.gz → 0.0.5__tar.gz
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.
- {langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/Makefile +2 -2
- {langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/PKG-INFO +1 -1
- {langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/langchain_agentcore_codeinterpreter/sandbox.py +221 -14
- {langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/pyproject.toml +8 -2
- {langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/tests/integration_tests/sandbox/test_sandbox.py +60 -5
- langchain_agentcore_codeinterpreter-0.0.5/tests/unit_tests/test_sandbox.py +968 -0
- {langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/uv.lock +631 -617
- langchain_agentcore_codeinterpreter-0.0.3/tests/unit_tests/test_sandbox.py +0 -510
- {langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/.gitignore +0 -0
- {langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/LICENSE +0 -0
- {langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/README.md +0 -0
- {langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/langchain_agentcore_codeinterpreter/__init__.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/scripts/check_imports.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/tests/__init__.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/tests/integration_tests/__init__.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/tests/integration_tests/sandbox/__init__.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/tests/integration_tests/test_compile.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/tests/unit_tests/__init__.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/tests/unit_tests/test_imports.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/tests/unit_tests/test_stream_parsing.py +0 -0
{langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/Makefile
RENAMED
|
@@ -30,10 +30,10 @@ test: ## Run individual unit test: make test TEST_FILE=tests/unit_tests/test.py
|
|
|
30
30
|
env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY -u AWS_SESSION_TOKEN uv run --group test pytest --disable-socket --allow-unix-socket $(TEST_FILE)
|
|
31
31
|
|
|
32
32
|
integration_tests: ## Run all integration tests
|
|
33
|
-
uv run --group test --group test_integration pytest -n auto $(TEST_FILE)
|
|
33
|
+
uv run --group test --group test_integration pytest -n auto --benchmark-disable $(TEST_FILE)
|
|
34
34
|
|
|
35
35
|
integration_test: ## Run individual integration test: make integration_test TEST_FILE=tests/integration_tests/integ_test.py
|
|
36
|
-
uv run --group test --group test_integration pytest -n auto $(TEST_FILE)
|
|
36
|
+
uv run --group test --group test_integration pytest -n auto --benchmark-disable $(TEST_FILE)
|
|
37
37
|
|
|
38
38
|
coverage_tests: ## Run unit tests with coverage
|
|
39
39
|
env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY -u AWS_SESSION_TOKEN uv run --group test pytest --cov=langchain_agentcore_codeinterpreter --cov-report=html --cov-report=term-missing --cov-branch tests/unit_tests/
|
{langchain_agentcore_codeinterpreter-0.0.3 → langchain_agentcore_codeinterpreter-0.0.5}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langchain-agentcore-codeinterpreter
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: Amazon Bedrock AgentCore Code Interpreter sandbox integration for Deep Agents
|
|
5
5
|
Project-URL: Source Code, https://github.com/langchain-ai/langchain-aws/tree/main/libs/agentcore-codeinterpreter
|
|
6
6
|
Project-URL: Repository, https://github.com/langchain-ai/langchain-aws
|
|
@@ -14,6 +14,9 @@ from deepagents.backends.protocol import (
|
|
|
14
14
|
ExecuteResponse,
|
|
15
15
|
FileDownloadResponse,
|
|
16
16
|
FileUploadResponse,
|
|
17
|
+
GlobResult,
|
|
18
|
+
GrepResult,
|
|
19
|
+
LsResult,
|
|
17
20
|
ReadResult,
|
|
18
21
|
WriteResult,
|
|
19
22
|
)
|
|
@@ -167,6 +170,15 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
167
170
|
The caller is responsible for managing the interpreter lifecycle
|
|
168
171
|
(``start()`` / ``stop()``).
|
|
169
172
|
|
|
173
|
+
!!! note
|
|
174
|
+
|
|
175
|
+
When the sandbox working directory is not ``/`` (e.g.
|
|
176
|
+
``/opt/amazon/genesis1p-tools/var/``), paths must be resolved
|
|
177
|
+
against the real cwd before shell preflight commands and stripped of
|
|
178
|
+
the cwd prefix before the AgentCore ``writeFiles``/``readFiles`` APIs.
|
|
179
|
+
Pass the known cwd via the ``cwd`` constructor argument, or let the
|
|
180
|
+
sandbox detect it automatically on the first path operation via ``pwd``.
|
|
181
|
+
|
|
170
182
|
Example:
|
|
171
183
|
.. code-block:: python
|
|
172
184
|
|
|
@@ -185,26 +197,114 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
185
197
|
interpreter.stop()
|
|
186
198
|
"""
|
|
187
199
|
|
|
188
|
-
def __init__(
|
|
200
|
+
def __init__(
|
|
201
|
+
self,
|
|
202
|
+
*,
|
|
203
|
+
interpreter: CodeInterpreter,
|
|
204
|
+
cwd: str | None = None,
|
|
205
|
+
) -> None:
|
|
189
206
|
"""Create a backend wrapping an active CodeInterpreter session.
|
|
190
207
|
|
|
191
208
|
Args:
|
|
192
209
|
interpreter: A started :class:`CodeInterpreter` instance.
|
|
210
|
+
cwd: The sandbox working directory (e.g.
|
|
211
|
+
``"/opt/amazon/genesis1p-tools/var"``). When provided,
|
|
212
|
+
``write()`` uses it to resolve virtual paths to real absolute
|
|
213
|
+
paths and to strip the prefix before the AgentCore
|
|
214
|
+
``writeFiles`` API. When omitted, the cwd is detected
|
|
215
|
+
automatically on the first path operation via ``pwd``.
|
|
193
216
|
"""
|
|
194
217
|
self._interpreter = interpreter
|
|
218
|
+
self._cwd: str | None = cwd.rstrip("/") if cwd is not None else None
|
|
219
|
+
|
|
220
|
+
def _get_cwd(self) -> str:
|
|
221
|
+
"""Return the sandbox working directory, detecting it lazily if needed.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
The working directory with any trailing slash stripped.
|
|
225
|
+
"""
|
|
226
|
+
if self._cwd is None:
|
|
227
|
+
result = self.execute("pwd")
|
|
228
|
+
if result.exit_code != 0 or not result.output.strip():
|
|
229
|
+
raise RuntimeError(
|
|
230
|
+
f"Failed to detect sandbox working directory: "
|
|
231
|
+
f"exit_code={result.exit_code}, output={result.output!r}"
|
|
232
|
+
)
|
|
233
|
+
self._cwd = result.output.strip().rstrip("/")
|
|
234
|
+
return self._cwd
|
|
235
|
+
|
|
236
|
+
def _to_relative_path(self, path: str) -> str:
|
|
237
|
+
"""Strip the cwd prefix (or leading slashes) for AgentCore file APIs.
|
|
195
238
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
239
|
+
When the sandbox cwd is known and ``path`` starts with it, the cwd
|
|
240
|
+
prefix is removed so the AgentCore ``writeFiles``/``readFiles`` APIs
|
|
241
|
+
receive a cwd-relative path. For paths that do not start with the cwd,
|
|
242
|
+
the standard leading-slash stripping is applied.
|
|
199
243
|
|
|
200
244
|
Args:
|
|
201
245
|
path: File path (absolute or relative).
|
|
202
246
|
|
|
203
247
|
Returns:
|
|
204
|
-
|
|
248
|
+
Path relative to the sandbox cwd, with no leading ``/`` or ``./``.
|
|
205
249
|
"""
|
|
250
|
+
if self._cwd is not None:
|
|
251
|
+
cwd_prefix = self._cwd + "/"
|
|
252
|
+
if path.startswith(cwd_prefix):
|
|
253
|
+
return path[len(cwd_prefix) :]
|
|
206
254
|
return _normalize_relative_path(path)
|
|
207
255
|
|
|
256
|
+
def _to_absolute_path(self, path: str) -> str:
|
|
257
|
+
"""Resolve a path to a real absolute path under the sandbox cwd.
|
|
258
|
+
|
|
259
|
+
Paths already under the cwd are returned unchanged. Virtual paths
|
|
260
|
+
(e.g. ``/workspace/hello.py``) and relative paths are prepended with
|
|
261
|
+
the cwd so that shell commands (``makedirs``, etc.) operate on the
|
|
262
|
+
real filesystem location.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
path: File path to resolve.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Absolute path under the sandbox cwd.
|
|
269
|
+
"""
|
|
270
|
+
cwd = self._get_cwd()
|
|
271
|
+
if not cwd:
|
|
272
|
+
return path
|
|
273
|
+
if path.startswith(cwd + "/") or path == cwd:
|
|
274
|
+
return path
|
|
275
|
+
return cwd + "/" + path.lstrip("/")
|
|
276
|
+
|
|
277
|
+
def write(self, file_path: str, content: str) -> WriteResult:
|
|
278
|
+
"""Create a new file in the sandbox, failing if it already exists.
|
|
279
|
+
|
|
280
|
+
Normalizes ``file_path`` to a real absolute path under the sandbox cwd
|
|
281
|
+
before the preflight shell command (so ``makedirs`` operates on the
|
|
282
|
+
correct location) and before uploading (so the AgentCore ``writeFiles``
|
|
283
|
+
API receives a cwd-relative path rather than a doubled absolute path).
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
file_path: Destination path for the new file. May be a real
|
|
287
|
+
absolute path under the sandbox cwd, a virtual absolute path
|
|
288
|
+
(e.g. ``/workspace/hello.py``), or a relative path.
|
|
289
|
+
content: UTF-8 text content to write.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
``WriteResult`` with ``path`` set to the resolved absolute path on
|
|
293
|
+
success, or ``error`` on failure.
|
|
294
|
+
"""
|
|
295
|
+
abs_path = self._to_absolute_path(file_path)
|
|
296
|
+
preflight_error = self._write_preflight(abs_path)
|
|
297
|
+
if preflight_error is not None:
|
|
298
|
+
return preflight_error
|
|
299
|
+
responses = self.upload_files([(abs_path, content.encode("utf-8"))])
|
|
300
|
+
assert responses, "upload_files returned no responses"
|
|
301
|
+
response = responses[0]
|
|
302
|
+
if response.error:
|
|
303
|
+
return WriteResult(
|
|
304
|
+
error=f"Failed to write file '{abs_path}': {response.error}"
|
|
305
|
+
)
|
|
306
|
+
return WriteResult(path=abs_path)
|
|
307
|
+
|
|
208
308
|
def _invoke(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
209
309
|
"""Invoke the interpreter and eagerly consume the response stream.
|
|
210
310
|
|
|
@@ -311,19 +411,20 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
311
411
|
as the input paths.
|
|
312
412
|
"""
|
|
313
413
|
try:
|
|
414
|
+
self._get_cwd()
|
|
314
415
|
relative_paths = [self._to_relative_path(p) for p in paths]
|
|
315
416
|
response = self._invoke(
|
|
316
417
|
method="readFiles", params={"paths": relative_paths}
|
|
317
418
|
)
|
|
318
|
-
file_contents = _extract_files_from_stream(response,
|
|
419
|
+
file_contents = _extract_files_from_stream(response, relative_paths)
|
|
319
420
|
|
|
320
421
|
return [
|
|
321
422
|
FileDownloadResponse(
|
|
322
|
-
path=
|
|
323
|
-
content=file_contents.get(
|
|
324
|
-
error=None if
|
|
423
|
+
path=original,
|
|
424
|
+
content=file_contents.get(rel),
|
|
425
|
+
error=None if rel in file_contents else "file_not_found",
|
|
325
426
|
)
|
|
326
|
-
for
|
|
427
|
+
for original, rel in zip(paths, relative_paths)
|
|
327
428
|
]
|
|
328
429
|
except SessionExpiredError:
|
|
329
430
|
logger.error("AgentCore session expired while downloading files: %s", paths)
|
|
@@ -341,7 +442,7 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
341
442
|
def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
|
|
342
443
|
"""Upload files to the AgentCore sandbox.
|
|
343
444
|
|
|
344
|
-
Text files are sent directly; binary files are
|
|
445
|
+
Text files are sent directly; binary files are sent as raw blobs.
|
|
345
446
|
|
|
346
447
|
Args:
|
|
347
448
|
files: List of ``(path, content)`` tuples to upload.
|
|
@@ -350,7 +451,7 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
350
451
|
List of :class:`FileUploadResponse` objects in the same order
|
|
351
452
|
as the input files.
|
|
352
453
|
"""
|
|
353
|
-
file_list: list[dict[str, str]] = []
|
|
454
|
+
file_list: list[dict[str, str | bytes]] = []
|
|
354
455
|
|
|
355
456
|
for path, content in files:
|
|
356
457
|
rel_path = self._to_relative_path(path)
|
|
@@ -358,8 +459,7 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
358
459
|
text_content = content.decode("utf-8")
|
|
359
460
|
file_list.append({"path": rel_path, "text": text_content})
|
|
360
461
|
except UnicodeDecodeError:
|
|
361
|
-
|
|
362
|
-
file_list.append({"path": rel_path, "blob": encoded})
|
|
462
|
+
file_list.append({"path": rel_path, "blob": content})
|
|
363
463
|
|
|
364
464
|
try:
|
|
365
465
|
if file_list:
|
|
@@ -381,6 +481,53 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
381
481
|
for path, _ in files
|
|
382
482
|
]
|
|
383
483
|
|
|
484
|
+
def ls(self, path: str) -> LsResult:
|
|
485
|
+
"""List a directory, resolving ``path`` against the sandbox cwd."""
|
|
486
|
+
return super().ls(self._to_absolute_path(path))
|
|
487
|
+
|
|
488
|
+
def read(
|
|
489
|
+
self,
|
|
490
|
+
file_path: str,
|
|
491
|
+
offset: int = 0,
|
|
492
|
+
limit: int = 2000,
|
|
493
|
+
) -> ReadResult:
|
|
494
|
+
"""Read a file, resolving ``file_path`` against the sandbox cwd."""
|
|
495
|
+
return super().read(self._to_absolute_path(file_path), offset, limit)
|
|
496
|
+
|
|
497
|
+
def grep(
|
|
498
|
+
self,
|
|
499
|
+
pattern: str,
|
|
500
|
+
path: str | None = None,
|
|
501
|
+
glob: str | None = None,
|
|
502
|
+
) -> GrepResult:
|
|
503
|
+
"""Search file contents, resolving ``path`` against the sandbox cwd."""
|
|
504
|
+
resolved = self._to_absolute_path(path) if path else path
|
|
505
|
+
return super().grep(pattern, resolved, glob)
|
|
506
|
+
|
|
507
|
+
def glob(
|
|
508
|
+
self,
|
|
509
|
+
pattern: str,
|
|
510
|
+
path: str | None = None,
|
|
511
|
+
) -> GlobResult:
|
|
512
|
+
"""Match paths by glob, resolving ``path`` against the sandbox cwd."""
|
|
513
|
+
resolved = self._to_absolute_path(path) if path else path
|
|
514
|
+
return super().glob(pattern, resolved)
|
|
515
|
+
|
|
516
|
+
def edit(
|
|
517
|
+
self,
|
|
518
|
+
file_path: str,
|
|
519
|
+
old_string: str,
|
|
520
|
+
new_string: str,
|
|
521
|
+
replace_all: bool = False, # noqa: FBT001, FBT002
|
|
522
|
+
) -> EditResult:
|
|
523
|
+
"""Edit a file, resolving ``file_path`` against the sandbox cwd."""
|
|
524
|
+
return super().edit(
|
|
525
|
+
self._to_absolute_path(file_path),
|
|
526
|
+
old_string,
|
|
527
|
+
new_string,
|
|
528
|
+
replace_all,
|
|
529
|
+
)
|
|
530
|
+
|
|
384
531
|
# ------------------------------------------------------------------
|
|
385
532
|
# Async overrides — use a dedicated executor to avoid starving the
|
|
386
533
|
# default asyncio thread pool with long-running boto3 stream reads.
|
|
@@ -520,3 +667,63 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
520
667
|
_AGENTCORE_EXECUTOR,
|
|
521
668
|
lambda: self.download_files(paths),
|
|
522
669
|
)
|
|
670
|
+
|
|
671
|
+
async def als(self, path: str) -> LsResult:
|
|
672
|
+
"""Async version of :meth:`ls`.
|
|
673
|
+
|
|
674
|
+
Args:
|
|
675
|
+
path: Directory path to list, resolved against the sandbox cwd.
|
|
676
|
+
|
|
677
|
+
Returns:
|
|
678
|
+
``LsResult`` with directory entries on success or ``error`` on
|
|
679
|
+
failure.
|
|
680
|
+
"""
|
|
681
|
+
loop = asyncio.get_running_loop()
|
|
682
|
+
return await loop.run_in_executor(
|
|
683
|
+
_AGENTCORE_EXECUTOR,
|
|
684
|
+
lambda: self.ls(path),
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
async def agrep(
|
|
688
|
+
self,
|
|
689
|
+
pattern: str,
|
|
690
|
+
path: str | None = None,
|
|
691
|
+
glob: str | None = None,
|
|
692
|
+
) -> GrepResult:
|
|
693
|
+
"""Async version of :meth:`grep`.
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
pattern: Literal string to search for.
|
|
697
|
+
path: Directory or file to search in, resolved against the sandbox
|
|
698
|
+
cwd. When ``None``, the ``BaseSandbox`` default is used.
|
|
699
|
+
glob: Optional file-name glob to restrict the search.
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
``GrepResult`` with a list of matches or ``error`` on failure.
|
|
703
|
+
"""
|
|
704
|
+
loop = asyncio.get_running_loop()
|
|
705
|
+
return await loop.run_in_executor(
|
|
706
|
+
_AGENTCORE_EXECUTOR,
|
|
707
|
+
lambda: self.grep(pattern, path, glob),
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
async def aglob(
|
|
711
|
+
self,
|
|
712
|
+
pattern: str,
|
|
713
|
+
path: str | None = None,
|
|
714
|
+
) -> GlobResult:
|
|
715
|
+
"""Async version of :meth:`glob`.
|
|
716
|
+
|
|
717
|
+
Args:
|
|
718
|
+
pattern: Glob pattern to match.
|
|
719
|
+
path: Directory to search in, resolved against the sandbox cwd.
|
|
720
|
+
When ``None``, the ``BaseSandbox`` default is used.
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
``GlobResult`` with a list of matches or ``error`` on failure.
|
|
724
|
+
"""
|
|
725
|
+
loop = asyncio.get_running_loop()
|
|
726
|
+
return await loop.run_in_executor(
|
|
727
|
+
_AGENTCORE_EXECUTOR,
|
|
728
|
+
lambda: self.glob(pattern, path),
|
|
729
|
+
)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "langchain-agentcore-codeinterpreter"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.5"
|
|
8
8
|
description = "Amazon Bedrock AgentCore Code Interpreter sandbox integration for Deep Agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -46,7 +46,7 @@ dev = [
|
|
|
46
46
|
]
|
|
47
47
|
|
|
48
48
|
[tool.uv]
|
|
49
|
-
prerelease = "
|
|
49
|
+
prerelease = "if-necessary-or-explicit"
|
|
50
50
|
|
|
51
51
|
[tool.ruff.lint]
|
|
52
52
|
select = [
|
|
@@ -62,6 +62,12 @@ disallow_untyped_defs = "True"
|
|
|
62
62
|
|
|
63
63
|
[tool.pytest.ini_options]
|
|
64
64
|
addopts = "--strict-markers --strict-config --durations=5"
|
|
65
|
+
filterwarnings = [
|
|
66
|
+
# `bedrock-agentcore` (third-party) still uses class-based pydantic `Config`
|
|
67
|
+
# in `bedrock_agentcore.runtime.context.RequestContext`. Remove once upstream
|
|
68
|
+
# migrates to `ConfigDict`.
|
|
69
|
+
"ignore:Support for class-based .config. is deprecated:DeprecationWarning",
|
|
70
|
+
]
|
|
65
71
|
markers = [
|
|
66
72
|
"requires: mark tests as requiring a specific library",
|
|
67
73
|
"asyncio: mark tests as requiring asyncio",
|
|
@@ -82,20 +82,50 @@ class TestAgentCoreSandboxIntegration:
|
|
|
82
82
|
assert results[0].error is not None
|
|
83
83
|
|
|
84
84
|
def test_binary_roundtrip(self, sandbox: AgentCoreSandbox) -> None:
|
|
85
|
-
"""Binary content uploaded
|
|
86
|
-
import base64
|
|
87
|
-
|
|
85
|
+
"""Binary content uploaded as a blob should round-trip unchanged."""
|
|
88
86
|
content = b"\x00\x01\x02\xff\xfe\xfd"
|
|
89
87
|
sandbox.upload_files([("binary_test.bin", content)])
|
|
90
88
|
results = sandbox.download_files(["binary_test.bin"])
|
|
91
89
|
assert results[0].error is None
|
|
92
|
-
|
|
93
|
-
assert results[0].content in (content, base64.b64encode(content))
|
|
90
|
+
assert results[0].content == content
|
|
94
91
|
|
|
95
92
|
def test_session_id(self, sandbox: AgentCoreSandbox) -> None:
|
|
96
93
|
"""The sandbox should have a non-empty session ID."""
|
|
97
94
|
assert sandbox.id != ""
|
|
98
95
|
|
|
96
|
+
def test_read_plane_roundtrip_virtual_path(self, sandbox: AgentCoreSandbox) -> None:
|
|
97
|
+
"""Read plane round-trips a virtual path against a non-root cwd."""
|
|
98
|
+
vpath = "/workspace/readplane_roundtrip.txt"
|
|
99
|
+
vdir = "/workspace"
|
|
100
|
+
content = "needle in haystack\nsecond line\n"
|
|
101
|
+
|
|
102
|
+
write_result = sandbox.write(vpath, content)
|
|
103
|
+
assert write_result.error is None, write_result.error
|
|
104
|
+
|
|
105
|
+
read_result = sandbox.read(vpath)
|
|
106
|
+
assert read_result.error is None, read_result.error
|
|
107
|
+
assert read_result.file_data is not None
|
|
108
|
+
assert read_result.file_data["content"].strip() == content.strip()
|
|
109
|
+
|
|
110
|
+
ls_result = sandbox.ls(vdir)
|
|
111
|
+
assert ls_result.error is None, ls_result.error
|
|
112
|
+
assert ls_result.entries is not None
|
|
113
|
+
assert any("readplane_roundtrip.txt" in e["path"] for e in ls_result.entries)
|
|
114
|
+
|
|
115
|
+
grep_result = sandbox.grep("needle", vdir)
|
|
116
|
+
assert grep_result.error is None, grep_result.error
|
|
117
|
+
assert grep_result.matches
|
|
118
|
+
|
|
119
|
+
glob_result = sandbox.glob("*.txt", vdir)
|
|
120
|
+
assert glob_result.error is None, glob_result.error
|
|
121
|
+
assert glob_result.matches
|
|
122
|
+
|
|
123
|
+
edit_result = sandbox.edit(vpath, "needle", "pin")
|
|
124
|
+
assert edit_result.error is None, edit_result.error
|
|
125
|
+
after = sandbox.read(vpath)
|
|
126
|
+
assert after.file_data is not None
|
|
127
|
+
assert "pin in haystack" in after.file_data["content"]
|
|
128
|
+
|
|
99
129
|
# ------------------------------------------------------------------
|
|
100
130
|
# Async integration tests
|
|
101
131
|
# ------------------------------------------------------------------
|
|
@@ -135,3 +165,28 @@ class TestAgentCoreSandboxIntegration:
|
|
|
135
165
|
for i, result in enumerate(results):
|
|
136
166
|
assert result.exit_code == 0
|
|
137
167
|
assert f"concurrent-{i}" in result.output
|
|
168
|
+
|
|
169
|
+
def test_aread_plane_roundtrip_virtual_path(
|
|
170
|
+
self, sandbox: AgentCoreSandbox
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Async read plane round-trips a virtual path against a non-root cwd."""
|
|
173
|
+
vpath = "/workspace/areadplane_roundtrip.txt"
|
|
174
|
+
vdir = "/workspace"
|
|
175
|
+
|
|
176
|
+
async def run() -> None:
|
|
177
|
+
write_result = await sandbox.awrite(vpath, "async needle\n")
|
|
178
|
+
assert write_result.error is None, write_result.error
|
|
179
|
+
|
|
180
|
+
read_result = await sandbox.aread(vpath)
|
|
181
|
+
assert read_result.error is None, read_result.error
|
|
182
|
+
|
|
183
|
+
ls_result = await sandbox.als(vdir)
|
|
184
|
+
assert ls_result.error is None, ls_result.error
|
|
185
|
+
|
|
186
|
+
grep_result = await sandbox.agrep("needle", vdir)
|
|
187
|
+
assert grep_result.error is None, grep_result.error
|
|
188
|
+
|
|
189
|
+
glob_result = await sandbox.aglob("*.txt", vdir)
|
|
190
|
+
assert glob_result.error is None, glob_result.error
|
|
191
|
+
|
|
192
|
+
asyncio.run(run())
|