Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,33 @@ jobs:
if: steps.changes.outputs.run != 'true'
run: echo "Skipping tests for non-code changes."

tests-windows:
runs-on: windows-latest
env:
OPENAI_API_KEY: fake-for-tests
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- name: Detect code changes
id: changes
shell: bash
run: ./.github/scripts/detect-changes.sh code "${{ github.event.pull_request.base.sha || github.event.before }}" "${{ github.sha }}"
- name: Setup uv
if: steps.changes.outputs.run == 'true'
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57
with:
enable-cache: true
python-version: "3.13"
- name: Install dependencies
if: steps.changes.outputs.run == 'true'
run: uv sync --all-extras --all-packages --group dev
- name: Run tests
if: steps.changes.outputs.run == 'true'
run: uv run pytest
- name: Skip tests
if: steps.changes.outputs.run != 'true'
run: echo "Skipping tests for non-code changes."

build-docs:
runs-on: ubuntu-latest
env:
Expand Down
4 changes: 2 additions & 2 deletions src/agents/extensions/sandbox/blaxel/mounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ async def activate(
_assert_blaxel_session(session)
_ = base_dir
mount_path = mount._resolve_mount_path(session, dest)
config = _build_mount_config(mount, mount_path=str(mount_path))
config = _build_mount_config(mount, mount_path=mount_path.as_posix())
await _mount_bucket(session, config)
return []

Expand All @@ -95,7 +95,7 @@ async def deactivate(
_assert_blaxel_session(session)
_ = base_dir
mount_path = mount._resolve_mount_path(session, dest)
await _unmount_bucket(session, str(mount_path))
await _unmount_bucket(session, mount_path.as_posix())

async def teardown_for_snapshot(
self,
Expand Down
19 changes: 10 additions & 9 deletions src/agents/extensions/sandbox/blaxel/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
retry_async,
)
from ....sandbox.util.tar_utils import UnsafeTarMemberError, validate_tar_bytes
from ....sandbox.workspace_paths import coerce_posix_path, posix_path_as_path

DEFAULT_BLAXEL_WORKSPACE_ROOT = "/workspace"
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -377,7 +378,7 @@ async def mkdir(
) from e

async def read(self, path: Path | str, *, user: str | User | None = None) -> io.IOBase:
path = Path(path)
path = posix_path_as_path(coerce_posix_path(path))
if user is not None:
workspace_path = await self._check_read_with_exec(path, user=user)
else:
Expand Down Expand Up @@ -407,7 +408,7 @@ async def write(
*,
user: str | User | None = None,
) -> None:
path = Path(path)
path = posix_path_as_path(coerce_posix_path(path))
if user is not None:
await self._check_write_with_exec(path, user=user)

Expand Down Expand Up @@ -525,11 +526,11 @@ def _tar_exclude_args(self) -> list[str]:
)
)
async def persist_workspace(self) -> io.IOBase:
root = Path(self.state.manifest.root)
root = self._workspace_root_path()
tar_path = f"/tmp/bl-persist-{self.state.session_id.hex}.tar"
excludes = " ".join(self._tar_exclude_args())
tar_cmd = (
f"tar {excludes} -C {shlex.quote(str(root))} -cf {shlex.quote(tar_path)} ."
f"tar {excludes} -C {shlex.quote(root.as_posix())} -cf {shlex.quote(tar_path)} ."
).strip()

unmounted_mounts: list[tuple[Mount, Path]] = []
Expand Down Expand Up @@ -596,7 +597,7 @@ async def persist_workspace(self) -> io.IOBase:
return io.BytesIO(raw)

async def hydrate_workspace(self, data: io.IOBase) -> None:
root = self.state.manifest.root
root = self._workspace_root_path()
tar_path = f"/tmp/bl-hydrate-{self.state.session_id.hex}.tar"
payload = data.read()
if isinstance(payload, str):
Expand All @@ -608,7 +609,7 @@ async def hydrate_workspace(self, data: io.IOBase) -> None:
validate_tar_bytes(bytes(payload))
except UnsafeTarMemberError as e:
raise WorkspaceArchiveWriteError(
path=Path(root),
path=root,
context={
"reason": "unsafe_or_invalid_tar",
"member": e.member,
Expand All @@ -623,12 +624,12 @@ async def hydrate_workspace(self, data: io.IOBase) -> None:
result = await self._exec_internal(
"sh",
"-c",
f"tar -C {shlex.quote(root)} -xf {shlex.quote(tar_path)}",
f"tar -C {shlex.quote(root.as_posix())} -xf {shlex.quote(tar_path)}",
timeout=self.state.timeouts.workspace_tar_s,
)
if result.exit_code != 0:
raise WorkspaceArchiveWriteError(
path=Path(root),
path=root,
context={
"reason": "tar_extract_failed",
"output": result.stderr.decode("utf-8", errors="replace"),
Expand All @@ -637,7 +638,7 @@ async def hydrate_workspace(self, data: io.IOBase) -> None:
except WorkspaceArchiveWriteError:
raise
except Exception as e:
raise WorkspaceArchiveWriteError(path=Path(root), cause=e) from e
raise WorkspaceArchiveWriteError(path=root, cause=e) from e
finally:
try:
await self._exec_internal(
Expand Down
29 changes: 17 additions & 12 deletions src/agents/extensions/sandbox/cloudflare/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
retry_async,
)
from ....sandbox.util.tar_utils import UnsafeTarMemberError, validate_tar_bytes
from ....sandbox.workspace_paths import coerce_posix_path, posix_path_as_path

_DEFAULT_EXEC_TIMEOUT_S = 30.0
_DEFAULT_REQUEST_TIMEOUT_S = 120.0
Expand Down Expand Up @@ -345,7 +346,9 @@ async def mount_bucket(
mount_path: Path | str,
options: dict[str, object],
) -> None:
workspace_path = await self._validate_path_access(mount_path, for_write=True)
workspace_path = await self._validate_path_access(
coerce_posix_path(mount_path).as_posix(), for_write=True
)
http = self._session()
url = self._url("mount")
payload = {
Expand Down Expand Up @@ -389,7 +392,9 @@ async def mount_bucket(
) from e

async def unmount_bucket(self, mount_path: Path | str) -> None:
workspace_path = await self._validate_path_access(mount_path, for_write=True)
workspace_path = await self._validate_path_access(
coerce_posix_path(mount_path).as_posix(), for_write=True
)
http = self._session()
url = self._url("unmount")
payload = {"mountPath": str(workspace_path)}
Expand Down Expand Up @@ -501,10 +506,10 @@ def _handle_event_payload(data: str) -> None:

async def _prepare_backend_workspace(self) -> None:
try:
root = Path(self.state.manifest.root)
await self._exec_internal("mkdir", "-p", "--", str(root))
root = self._workspace_root_path()
await self._exec_internal("mkdir", "-p", "--", root.as_posix())
except Exception as e:
raise WorkspaceStartError(path=Path(self.state.manifest.root), cause=e) from e
raise WorkspaceStartError(path=self._workspace_root_path(), cause=e) from e

async def _can_reuse_restorable_snapshot_workspace(self) -> bool:
if not self._workspace_state_preserved_on_start():
Expand All @@ -518,7 +523,7 @@ async def _can_reuse_restorable_snapshot_workspace(self) -> bool:
return await self._can_skip_snapshot_restore_on_resume(is_running=is_running)

async def _restore_snapshot_into_workspace_on_resume(self) -> None:
root = Path(self.state.manifest.root)
root = self._workspace_root_path()
detached_mounts: list[tuple[Any, Path]] = []
if self._restore_workspace_was_running:
for mount_entry, mount_path in self.state.manifest.ephemeral_mount_targets():
Expand Down Expand Up @@ -965,7 +970,7 @@ async def pty_terminate_all(self) -> None:
await self._terminate_pty_entry(entry)

async def read(self, path: Path | str, *, user: str | User | None = None) -> io.IOBase:
path = Path(path)
path = posix_path_as_path(coerce_posix_path(path))
if user is not None:
await self._check_read_with_exec(path, user=user)

Expand Down Expand Up @@ -1029,7 +1034,7 @@ async def write(
*,
user: str | User | None = None,
) -> None:
path = Path(path)
path = posix_path_as_path(coerce_posix_path(path))
if user is not None:
await self._check_write_with_exec(path, user=user)

Expand Down Expand Up @@ -1106,7 +1111,7 @@ async def running(self) -> bool:
or _is_transient_workspace_error(exc)
)
async def _persist_workspace_via_http(self) -> io.IOBase:
root = Path(self.state.manifest.root)
root = self._workspace_root_path()
skip = self._persist_workspace_skip_relpaths()
excludes_param = ",".join(
rel.as_posix().removeprefix("./")
Expand Down Expand Up @@ -1148,7 +1153,7 @@ async def _persist_workspace_via_http(self) -> io.IOBase:
or _is_transient_workspace_error(exc)
)
async def _hydrate_workspace_via_http(self, data: io.IOBase) -> None:
root = Path(self.state.manifest.root)
root = self._workspace_root_path()
raw = data.read()
if isinstance(raw, str):
raw = raw.encode("utf-8")
Expand Down Expand Up @@ -1199,7 +1204,7 @@ async def _hydrate_workspace_via_http(self, data: io.IOBase) -> None:
raise WorkspaceArchiveWriteError(path=root, cause=e) from e

async def persist_workspace(self) -> io.IOBase:
root = Path(self.state.manifest.root)
root = self._workspace_root_path()
unmounted_mounts: list[tuple[Any, Path]] = []
unmount_error: WorkspaceArchiveReadError | None = None
for mount_entry, mount_path in self.state.manifest.ephemeral_mount_targets():
Expand Down Expand Up @@ -1245,7 +1250,7 @@ async def persist_workspace(self) -> io.IOBase:
return persisted

async def hydrate_workspace(self, data: io.IOBase) -> None:
root = Path(self.state.manifest.root)
root = self._workspace_root_path()
unmounted_mounts: list[tuple[Any, Path]] = []
unmount_error: WorkspaceArchiveWriteError | None = None
for mount_entry, mount_path in self.state.manifest.ephemeral_mount_targets():
Expand Down
24 changes: 12 additions & 12 deletions src/agents/extensions/sandbox/daytona/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
retry_async,
)
from ....sandbox.util.tar_utils import UnsafeTarMemberError, validate_tar_bytes
from ....sandbox.workspace_paths import coerce_posix_path, posix_path_as_path

DEFAULT_DAYTONA_WORKSPACE_ROOT = "/home/daytona/workspace"
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -782,7 +783,7 @@ async def _terminate_pty_entry(self, entry: _DaytonaPtySessionEntry) -> None:
pass

async def read(self, path: Path | str, *, user: str | User | None = None) -> io.IOBase:
path = Path(path)
path = posix_path_as_path(coerce_posix_path(path))
if user is not None:
workspace_path = await self._check_read_with_exec(path, user=user)
else:
Expand All @@ -809,7 +810,7 @@ async def write(
*,
user: str | User | None = None,
) -> None:
path = Path(path)
path = posix_path_as_path(coerce_posix_path(path))
if user is not None:
await self._check_write_with_exec(path, user=user)

Expand Down Expand Up @@ -859,7 +860,6 @@ def _tar_exclude_args(self) -> list[str]:
)
)
async def _run_persist_workspace_command(self, tar_cmd: str, tar_path: str) -> bytes:
root = self.state.manifest.root
try:
envs = await self._resolved_envs()
result = await self._sandbox.process.exec(
Expand All @@ -869,7 +869,7 @@ async def _run_persist_workspace_command(self, tar_cmd: str, tar_path: str) -> b
)
if result.exit_code != 0:
raise WorkspaceArchiveReadError(
path=Path(root),
path=self._workspace_root_path(),
context={"reason": "tar_failed", "output": result.result or ""},
)
return cast(
Expand All @@ -882,7 +882,7 @@ async def _run_persist_workspace_command(self, tar_cmd: str, tar_path: str) -> b
except WorkspaceArchiveReadError:
raise
except Exception as e:
raise WorkspaceArchiveReadError(path=Path(root), cause=e) from e
raise WorkspaceArchiveReadError(path=self._workspace_root_path(), cause=e) from e

async def persist_workspace(self) -> io.IOBase:
def _error_context_summary(error: WorkspaceArchiveReadError) -> dict[str, str]:
Expand All @@ -892,11 +892,11 @@ def _error_context_summary(error: WorkspaceArchiveReadError) -> dict[str, str]:
summary["cause"] = str(error.cause)
return summary

root = Path(self.state.manifest.root)
root = self._workspace_root_path()
tar_path = f"/tmp/sandbox-persist-{self.state.session_id.hex}.tar"
excludes = " ".join(self._tar_exclude_args())
tar_cmd = (
f"tar {excludes} -C {shlex.quote(str(root))} -cf {shlex.quote(tar_path)} ."
f"tar {excludes} -C {shlex.quote(root.as_posix())} -cf {shlex.quote(tar_path)} ."
).strip()

unmounted_mounts: list[tuple[Mount, Path]] = []
Expand Down Expand Up @@ -964,7 +964,7 @@ def _error_context_summary(error: WorkspaceArchiveReadError) -> dict[str, str]:
return io.BytesIO(raw)

async def hydrate_workspace(self, data: io.IOBase) -> None:
root = self.state.manifest.root
root = self._workspace_root_path()
tar_path = f"/tmp/sandbox-hydrate-{self.state.session_id.hex}.tar"
payload = data.read()
if isinstance(payload, str):
Expand All @@ -976,7 +976,7 @@ async def hydrate_workspace(self, data: io.IOBase) -> None:
validate_tar_bytes(bytes(payload))
except UnsafeTarMemberError as e:
raise WorkspaceArchiveWriteError(
path=Path(root),
path=root,
context={
"reason": "unsafe_or_invalid_tar",
"member": e.member,
Expand All @@ -994,19 +994,19 @@ async def hydrate_workspace(self, data: io.IOBase) -> None:
timeout=self.state.timeouts.file_upload_s,
)
result = await self._sandbox.process.exec(
f"tar -C {shlex.quote(root)} -xf {shlex.quote(tar_path)}",
f"tar -C {shlex.quote(root.as_posix())} -xf {shlex.quote(tar_path)}",
env=envs or None,
timeout=self.state.timeouts.workspace_tar_s,
)
if result.exit_code != 0:
raise WorkspaceArchiveWriteError(
path=Path(root),
path=root,
context={"reason": "tar_extract_failed", "output": result.result or ""},
)
except WorkspaceArchiveWriteError:
raise
except Exception as e:
raise WorkspaceArchiveWriteError(path=Path(root), cause=e) from e
raise WorkspaceArchiveWriteError(path=root, cause=e) from e
finally:
try:
envs = await self._resolved_envs()
Expand Down
Loading
Loading