Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
15 changes: 9 additions & 6 deletions src/agents/extensions/sandbox/blaxel/mounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from ....sandbox.materialization import MaterializedFile
from ....sandbox.session.base_sandbox_session import BaseSandboxSession
from ....sandbox.types import FileMode, Permissions
from ....sandbox.workspace_paths import sandbox_path_str

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -81,7 +82,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 +96,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 All @@ -105,7 +106,7 @@ async def teardown_for_snapshot(
) -> None:
_assert_blaxel_session(session)
_ = mount
await _unmount_bucket(session, str(path))
await _unmount_bucket(session, sandbox_path_str(path))

async def restore_after_snapshot(
self,
Expand All @@ -114,7 +115,7 @@ async def restore_after_snapshot(
path: Path,
) -> None:
_assert_blaxel_session(session)
config = _build_mount_config(mount, mount_path=str(path))
config = _build_mount_config(mount, mount_path=sandbox_path_str(path))
await _mount_bucket(session, config)

def build_docker_volume_driver_config(
Expand Down Expand Up @@ -606,7 +607,9 @@ def _resolve_config(
message="BlaxelDriveMountStrategy requires a BlaxelDriveMount entry",
context={"mount_type": mount.type},
)
mount_path = mount.drive_mount_path or str(mount._resolve_mount_path(session, dest))
mount_path = mount.drive_mount_path or sandbox_path_str(
mount._resolve_mount_path(session, dest)
)
return BlaxelDriveMountConfig(
drive_name=mount.drive_name,
mount_path=mount_path,
Expand All @@ -619,7 +622,7 @@ def _effective_mount_path(mount: Mount, fallback: Path) -> str:
"""Return the actual mount path, preferring ``drive_mount_path`` over the manifest path."""
if isinstance(mount, BlaxelDriveMount) and mount.drive_mount_path:
return mount.drive_mount_path
return str(fallback)
return sandbox_path_str(fallback)

@staticmethod
def _resolve_config_from_source(mount: Mount, mount_path: str) -> BlaxelDriveMountConfig:
Expand Down
33 changes: 17 additions & 16 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, sandbox_path_str

DEFAULT_BLAXEL_WORKSPACE_ROOT = "/workspace"
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -319,7 +320,7 @@ async def start(self) -> None:
# Ensure workspace root exists before BaseSandboxSession.start() materializes
# the manifest. Blaxel base images run as root and do not ship a pre-created
# workspace directory.
root = self.state.manifest.root
root = sandbox_path_str(self.state.manifest.root)
try:
await self._sandbox.process.exec(
{
Expand Down Expand Up @@ -368,7 +369,7 @@ async def mkdir(
if path == Path("/"):
return
try:
await self._sandbox.fs.mkdir(str(path))
await self._sandbox.fs.mkdir(sandbox_path_str(path))
except Exception as e:
raise WorkspaceArchiveWriteError(
path=path,
Expand All @@ -377,14 +378,14 @@ async def mkdir(
) from e

async def read(self, path: Path | str, *, user: str | User | None = None) -> io.IOBase:
path = Path(path)
error_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:
workspace_path = await self._validate_path_access(path)

try:
data: Any = await self._sandbox.fs.read_binary(str(workspace_path))
data: Any = await self._sandbox.fs.read_binary(sandbox_path_str(workspace_path))
if isinstance(data, str):
data = data.encode("utf-8")
return io.BytesIO(bytes(data))
Expand All @@ -397,8 +398,8 @@ async def read(self, path: Path | str, *, user: str | User | None = None) -> io.
status = first_arg.get("status")
error_str = str(e).lower()
if status == 404 or "not found" in error_str or "no such file" in error_str:
raise WorkspaceReadNotFoundError(path=path, cause=e) from e
raise WorkspaceArchiveReadError(path=path, cause=e) from e
raise WorkspaceReadNotFoundError(path=error_path, cause=e) from e
raise WorkspaceArchiveReadError(path=error_path, cause=e) from e

async def write(
self,
Expand All @@ -407,19 +408,19 @@ async def write(
*,
user: str | User | None = None,
) -> None:
path = Path(path)
error_path = posix_path_as_path(coerce_posix_path(path))
if user is not None:
await self._check_write_with_exec(path, user=user)

payload = data.read()
if isinstance(payload, str):
payload = payload.encode("utf-8")
if not isinstance(payload, bytes | bytearray):
raise WorkspaceWriteTypeError(path=path, actual_type=type(payload).__name__)
raise WorkspaceWriteTypeError(path=error_path, actual_type=type(payload).__name__)

workspace_path = await self._validate_path_access(path, for_write=True)
try:
await self._sandbox.fs.write_binary(str(workspace_path), bytes(payload))
await self._sandbox.fs.write_binary(sandbox_path_str(workspace_path), bytes(payload))
except Exception as e:
raise WorkspaceArchiveWriteError(path=workspace_path, cause=e) from e

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
Loading
Loading