From 64ac570670b3ad829b2e005b41d12912d3b17fad Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Tue, 21 Apr 2026 12:55:47 +0900 Subject: [PATCH] refactor: share sandbox tar exclude arg generation --- .../extensions/sandbox/blaxel/sandbox.py | 10 ++----- .../extensions/sandbox/daytona/sandbox.py | 10 ++----- src/agents/extensions/sandbox/e2b/sandbox.py | 10 ++----- src/agents/sandbox/session/tar_workspace.py | 18 ++++++++++++ tests/sandbox/test_tar_workspace.py | 28 +++++++++++++++++++ 5 files changed, 52 insertions(+), 24 deletions(-) create mode 100644 src/agents/sandbox/session/tar_workspace.py create mode 100644 tests/sandbox/test_tar_workspace.py diff --git a/src/agents/extensions/sandbox/blaxel/sandbox.py b/src/agents/extensions/sandbox/blaxel/sandbox.py index d4d27ffce5..e87cb38389 100644 --- a/src/agents/extensions/sandbox/blaxel/sandbox.py +++ b/src/agents/extensions/sandbox/blaxel/sandbox.py @@ -56,6 +56,7 @@ ) from ....sandbox.session.runtime_helpers import RESOLVE_WORKSPACE_PATH_HELPER, RuntimeHelperScript from ....sandbox.session.sandbox_client import BaseSandboxClient +from ....sandbox.session.tar_workspace import shell_tar_exclude_args from ....sandbox.snapshot import SnapshotBase, SnapshotSpec, resolve_snapshot from ....sandbox.types import ExecResult, ExposedPortEndpoint, User from ....sandbox.util.retry import ( @@ -510,14 +511,7 @@ async def running(self) -> bool: # -- workspace persistence ----------------------------------------------- def _tar_exclude_args(self) -> list[str]: - excludes: list[str] = [] - for rel in sorted(self._persist_workspace_skip_relpaths(), key=lambda p: p.as_posix()): - rel_posix = rel.as_posix().lstrip("/") - if not rel_posix or rel_posix in {".", "/"}: - continue - excludes.append(f"--exclude={shlex.quote(rel_posix)}") - excludes.append(f"--exclude={shlex.quote(f'./{rel_posix}')}") - return excludes + return shell_tar_exclude_args(self._persist_workspace_skip_relpaths()) @retry_async( retry_if=lambda exc, self: ( diff --git a/src/agents/extensions/sandbox/daytona/sandbox.py b/src/agents/extensions/sandbox/daytona/sandbox.py index 849362b8b6..541e11009c 100644 --- a/src/agents/extensions/sandbox/daytona/sandbox.py +++ b/src/agents/extensions/sandbox/daytona/sandbox.py @@ -55,6 +55,7 @@ ) from ....sandbox.session.runtime_helpers import RESOLVE_WORKSPACE_PATH_HELPER, RuntimeHelperScript from ....sandbox.session.sandbox_client import BaseSandboxClient, BaseSandboxClientOptions +from ....sandbox.session.tar_workspace import shell_tar_exclude_args from ....sandbox.snapshot import SnapshotBase, SnapshotSpec, resolve_snapshot from ....sandbox.types import ExecResult, ExposedPortEndpoint, User from ....sandbox.util.retry import ( @@ -878,14 +879,7 @@ async def running(self) -> bool: return False def _tar_exclude_args(self) -> list[str]: - excludes: list[str] = [] - for rel in sorted(self._persist_workspace_skip_relpaths(), key=lambda p: p.as_posix()): - rel_posix = rel.as_posix().lstrip("/") - if not rel_posix or rel_posix in {".", "/"}: - continue - excludes.append(f"--exclude={shlex.quote(rel_posix)}") - excludes.append(f"--exclude={shlex.quote(f'./{rel_posix}')}") - return excludes + return shell_tar_exclude_args(self._persist_workspace_skip_relpaths()) @retry_async( retry_if=lambda exc, self, tar_cmd, tar_path: ( diff --git a/src/agents/extensions/sandbox/e2b/sandbox.py b/src/agents/extensions/sandbox/e2b/sandbox.py index 0e8d92d1d6..aedf4c0471 100644 --- a/src/agents/extensions/sandbox/e2b/sandbox.py +++ b/src/agents/extensions/sandbox/e2b/sandbox.py @@ -63,6 +63,7 @@ ) from ....sandbox.session.runtime_helpers import RESOLVE_WORKSPACE_PATH_HELPER, RuntimeHelperScript from ....sandbox.session.sandbox_client import BaseSandboxClient, BaseSandboxClientOptions +from ....sandbox.session.tar_workspace import shell_tar_exclude_args from ....sandbox.snapshot import SnapshotBase, SnapshotSpec, resolve_snapshot from ....sandbox.types import ExecResult, ExposedPortEndpoint, User from ....sandbox.util.retry import ( @@ -1226,14 +1227,7 @@ async def _terminate_pty_entry(self, entry: _E2BPtyProcessEntry) -> None: pass def _tar_exclude_args(self) -> list[str]: - excludes: list[str] = [] - for rel in sorted(self._persist_workspace_skip_relpaths(), key=lambda p: p.as_posix()): - rel_posix = rel.as_posix().lstrip("/") - if not rel_posix or rel_posix in {".", "/"}: - continue - excludes.append(f"--exclude={shlex.quote(rel_posix)}") - excludes.append(f"--exclude={shlex.quote(f'./{rel_posix}')}") - return excludes + return shell_tar_exclude_args(self._persist_workspace_skip_relpaths()) @retry_async( retry_if=lambda exc, self, tar_cmd: ( diff --git a/src/agents/sandbox/session/tar_workspace.py b/src/agents/sandbox/session/tar_workspace.py new file mode 100644 index 0000000000..32229c59f7 --- /dev/null +++ b/src/agents/sandbox/session/tar_workspace.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +import shlex +from collections.abc import Iterable +from pathlib import Path + +__all__ = ["shell_tar_exclude_args"] + + +def shell_tar_exclude_args(skip_relpaths: Iterable[Path]) -> list[str]: + excludes: list[str] = [] + for rel in sorted(skip_relpaths, key=lambda p: p.as_posix()): + rel_posix = rel.as_posix().lstrip("/") + if not rel_posix or rel_posix in {".", "/"}: + continue + excludes.append(f"--exclude={shlex.quote(rel_posix)}") + excludes.append(f"--exclude={shlex.quote(f'./{rel_posix}')}") + return excludes diff --git a/tests/sandbox/test_tar_workspace.py b/tests/sandbox/test_tar_workspace.py new file mode 100644 index 0000000000..a2671f3257 --- /dev/null +++ b/tests/sandbox/test_tar_workspace.py @@ -0,0 +1,28 @@ +from pathlib import Path + +from agents.sandbox.session.tar_workspace import shell_tar_exclude_args + + +def test_shell_tar_exclude_args_skips_empty_and_dot_paths() -> None: + assert shell_tar_exclude_args([Path(""), Path("."), Path("/")]) == [] + + +def test_shell_tar_exclude_args_sorts_and_adds_plain_and_dot_prefixed_patterns() -> None: + assert shell_tar_exclude_args( + [ + Path("logs/events.jsonl"), + Path("cache dir/file.txt"), + ] + ) == [ + "--exclude='cache dir/file.txt'", + "--exclude='./cache dir/file.txt'", + "--exclude=logs/events.jsonl", + "--exclude=./logs/events.jsonl", + ] + + +def test_shell_tar_exclude_args_normalizes_absolute_paths() -> None: + assert shell_tar_exclude_args([Path("/tmp/workspace/cache")]) == [ + "--exclude=tmp/workspace/cache", + "--exclude=./tmp/workspace/cache", + ]