From 5b7152839920e2c407b7f5761ccc222a0b9d5bcd Mon Sep 17 00:00:00 2001 From: mjoffre Date: Thu, 28 May 2026 00:13:15 +0000 Subject: [PATCH 1/4] feat: add update_network method for sandbox network configuration updates Add SandboxUpdateNetwork type and update_network class methods to both SandboxInstance (async) and SyncSandboxInstance (sync) following the existing update_metadata/update_ttl/update_lifecycle pattern. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/blaxel/core/sandbox/__init__.py | 2 ++ src/blaxel/core/sandbox/default/sandbox.py | 37 ++++++++++++++++++++++ src/blaxel/core/sandbox/sync/sandbox.py | 37 ++++++++++++++++++++++ src/blaxel/core/sandbox/types.py | 10 ++++++ 4 files changed, 86 insertions(+) diff --git a/src/blaxel/core/sandbox/__init__.py b/src/blaxel/core/sandbox/__init__.py index 02c217f..38e952d 100644 --- a/src/blaxel/core/sandbox/__init__.py +++ b/src/blaxel/core/sandbox/__init__.py @@ -38,6 +38,7 @@ SandboxConfiguration, SandboxCreateConfiguration, SandboxFilesystemFile, + SandboxUpdateNetwork, SessionCreateOptions, SessionWithToken, StreamHandle, @@ -59,6 +60,7 @@ "AsyncStreamHandle", "SandboxFilesystemFile", "CopyResponse", + "SandboxUpdateNetwork", "Sandbox", "SandboxFileSystem", "SandboxPreviews", diff --git a/src/blaxel/core/sandbox/default/sandbox.py b/src/blaxel/core/sandbox/default/sandbox.py index a069c67..f80a600 100644 --- a/src/blaxel/core/sandbox/default/sandbox.py +++ b/src/blaxel/core/sandbox/default/sandbox.py @@ -32,6 +32,7 @@ SandboxConfiguration, SandboxCreateConfiguration, SandboxUpdateMetadata, + SandboxUpdateNetwork, SessionWithToken, ) from .codegen import SandboxCodegen @@ -477,6 +478,42 @@ async def update_lifecycle( return cls(response) + @classmethod + async def update_network( + cls, sandbox_name: str, network: SandboxUpdateNetwork + ) -> "SandboxInstance": + """Update sandbox network configuration without recreating it. + + Args: + sandbox_name: The name of the sandbox to update + network: The new network configuration + + Returns: + A new SandboxInstance with updated network configuration + """ + sandbox_instance = await cls.get(sandbox_name) + sandbox = sandbox_instance.sandbox + + updated_sandbox = Sandbox.from_dict(sandbox.to_dict()) + if updated_sandbox.spec is None: + raise ValueError(f"Sandbox {sandbox_name} has invalid spec") + + if network.network is not None: + if isinstance(network.network, dict): + updated_sandbox.spec.network = SandboxNetworkModel.from_dict(network.network) + else: + updated_sandbox.spec.network = network.network + else: + updated_sandbox.spec.network = UNSET + + response = await update_sandbox( + sandbox_name=sandbox_name, + client=client, + body=updated_sandbox, + ) + + return cls(response) + @classmethod async def create_if_not_exists( cls, sandbox: Union[Sandbox, SandboxCreateConfiguration, Dict[str, Any]] diff --git a/src/blaxel/core/sandbox/sync/sandbox.py b/src/blaxel/core/sandbox/sync/sandbox.py index 9a76f50..cf64e2a 100644 --- a/src/blaxel/core/sandbox/sync/sandbox.py +++ b/src/blaxel/core/sandbox/sync/sandbox.py @@ -37,6 +37,7 @@ SandboxConfiguration, SandboxCreateConfiguration, SandboxUpdateMetadata, + SandboxUpdateNetwork, SessionWithToken, ) from .codegen import SyncSandboxCodegen @@ -392,6 +393,42 @@ def update_lifecycle( return cls(response) + @classmethod + def update_network( + cls, sandbox_name: str, network: SandboxUpdateNetwork + ) -> "SyncSandboxInstance": + """Update sandbox network configuration without recreating it. + + Args: + sandbox_name: The name of the sandbox to update + network: The new network configuration + + Returns: + A new SyncSandboxInstance with updated network configuration + """ + sandbox_instance = cls.get(sandbox_name) + sandbox = sandbox_instance.sandbox + + updated_sandbox = Sandbox.from_dict(sandbox.to_dict()) + if updated_sandbox.spec is None: + raise ValueError(f"Sandbox {sandbox_name} has invalid spec") + + if network.network is not None: + if isinstance(network.network, dict): + updated_sandbox.spec.network = SandboxNetworkModel.from_dict(network.network) + else: + updated_sandbox.spec.network = network.network + else: + updated_sandbox.spec.network = UNSET + + response = update_sandbox( + sandbox_name=sandbox_name, + client=client, + body=updated_sandbox, + ) + + return cls(response) + @classmethod def create_if_not_exists( cls, sandbox: Union[Sandbox, SandboxCreateConfiguration, Dict[str, Any]] diff --git a/src/blaxel/core/sandbox/types.py b/src/blaxel/core/sandbox/types.py index b43ed1f..22628d6 100644 --- a/src/blaxel/core/sandbox/types.py +++ b/src/blaxel/core/sandbox/types.py @@ -146,6 +146,16 @@ def __init__( self.display_name = display_name +class SandboxUpdateNetwork: + """Configuration for updating sandbox network configuration.""" + + def __init__( + self, + network: Union[SandboxNetwork, Dict[str, Any]] | None = None, + ): + self.network = network + + class SandboxCreateConfiguration: """Simplified configuration for creating sandboxes with default values.""" From 7e75d13132a9cc20e24f23cc5f92d8984278e717 Mon Sep 17 00:00:00 2001 From: mjoffre Date: Thu, 28 May 2026 00:22:38 +0000 Subject: [PATCH 2/4] test: add integration tests for update_network method Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../integration/core/sandbox/test_network.py | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 tests/integration/core/sandbox/test_network.py diff --git a/tests/integration/core/sandbox/test_network.py b/tests/integration/core/sandbox/test_network.py new file mode 100644 index 0000000..b22e502 --- /dev/null +++ b/tests/integration/core/sandbox/test_network.py @@ -0,0 +1,116 @@ +import pytest + +from blaxel.core import SandboxInstance +from blaxel.core.sandbox.types import SandboxUpdateNetwork +from tests.helpers import ( + default_image, + default_labels, + unique_name, +) + +# ============================================================================= +# Sandbox UpdateNetwork Tests +# ============================================================================= + + +@pytest.mark.asyncio(loop_scope="class") +class TestSandboxUpdateNetwork: + """Test sandbox update_network operations.""" + + async def test_updates_sandbox_network_with_allowed_domains(self): + """Test updating sandbox network with allowed domains.""" + name = unique_name("update-net-allow") + await SandboxInstance.create( + { + "name": name, + "image": default_image, + "labels": default_labels, + } + ) + + try: + updated = await SandboxInstance.update_network( + name, + SandboxUpdateNetwork( + network={"allowedDomains": ["api.openai.com", "api.stripe.com"]} + ), + ) + assert updated.spec.network is not None + assert "api.openai.com" in updated.spec.network.allowed_domains + assert "api.stripe.com" in updated.spec.network.allowed_domains + finally: + await SandboxInstance.delete(name) + + async def test_updates_sandbox_network_with_forbidden_domains(self): + """Test updating sandbox network with forbidden domains.""" + name = unique_name("update-net-forbid") + await SandboxInstance.create( + { + "name": name, + "image": default_image, + "labels": default_labels, + } + ) + + try: + updated = await SandboxInstance.update_network( + name, + SandboxUpdateNetwork( + network={"forbiddenDomains": ["*.malware.com", "evil.example.org"]} + ), + ) + assert updated.spec.network is not None + assert "*.malware.com" in updated.spec.network.forbidden_domains + assert "evil.example.org" in updated.spec.network.forbidden_domains + finally: + await SandboxInstance.delete(name) + + async def test_updates_sandbox_network_with_model_object(self): + """Test updating sandbox network using SandboxNetwork model object.""" + from blaxel.core.client.models import SandboxNetwork + + name = unique_name("update-net-model") + await SandboxInstance.create( + { + "name": name, + "image": default_image, + "labels": default_labels, + } + ) + + try: + network_config = SandboxNetwork( + allowed_domains=["api.example.com"], + ) + updated = await SandboxInstance.update_network( + name, + SandboxUpdateNetwork(network=network_config), + ) + assert updated.spec.network is not None + assert "api.example.com" in updated.spec.network.allowed_domains + finally: + await SandboxInstance.delete(name) + + async def test_clears_sandbox_network_config(self): + """Test clearing sandbox network configuration by passing network=None.""" + name = unique_name("update-net-clear") + await SandboxInstance.create( + { + "name": name, + "image": default_image, + "network": {"allowedDomains": ["api.example.com"]}, + "labels": default_labels, + } + ) + + try: + updated = await SandboxInstance.update_network( + name, + SandboxUpdateNetwork(network=None), + ) + # Network should be cleared (UNSET) + from blaxel.core.client.types import UNSET + + assert updated.spec.network is UNSET or updated.spec.network is None + finally: + await SandboxInstance.delete(name) From 7cdd77a668ade4cae5f5b150887f9ea8d3b23384 Mon Sep 17 00:00:00 2001 From: mjoffre Date: Thu, 28 May 2026 00:25:23 +0000 Subject: [PATCH 3/4] test: use httpbin.org domains in network integration tests Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../integration/core/sandbox/test_network.py | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/tests/integration/core/sandbox/test_network.py b/tests/integration/core/sandbox/test_network.py index b22e502..0352912 100644 --- a/tests/integration/core/sandbox/test_network.py +++ b/tests/integration/core/sandbox/test_network.py @@ -18,7 +18,7 @@ class TestSandboxUpdateNetwork: """Test sandbox update_network operations.""" async def test_updates_sandbox_network_with_allowed_domains(self): - """Test updating sandbox network with allowed domains.""" + """Test updating sandbox network with allowed domains using httpbin.""" name = unique_name("update-net-allow") await SandboxInstance.create( { @@ -31,18 +31,16 @@ async def test_updates_sandbox_network_with_allowed_domains(self): try: updated = await SandboxInstance.update_network( name, - SandboxUpdateNetwork( - network={"allowedDomains": ["api.openai.com", "api.stripe.com"]} - ), + SandboxUpdateNetwork(network={"allowedDomains": ["httpbin.org", "*.httpbin.org"]}), ) assert updated.spec.network is not None - assert "api.openai.com" in updated.spec.network.allowed_domains - assert "api.stripe.com" in updated.spec.network.allowed_domains + assert "httpbin.org" in updated.spec.network.allowed_domains + assert "*.httpbin.org" in updated.spec.network.allowed_domains finally: await SandboxInstance.delete(name) async def test_updates_sandbox_network_with_forbidden_domains(self): - """Test updating sandbox network with forbidden domains.""" + """Test updating sandbox network with forbidden domains using httpbin.""" name = unique_name("update-net-forbid") await SandboxInstance.create( { @@ -56,12 +54,12 @@ async def test_updates_sandbox_network_with_forbidden_domains(self): updated = await SandboxInstance.update_network( name, SandboxUpdateNetwork( - network={"forbiddenDomains": ["*.malware.com", "evil.example.org"]} + network={"forbiddenDomains": ["httpbin.org", "*.httpbin.org"]} ), ) assert updated.spec.network is not None - assert "*.malware.com" in updated.spec.network.forbidden_domains - assert "evil.example.org" in updated.spec.network.forbidden_domains + assert "httpbin.org" in updated.spec.network.forbidden_domains + assert "*.httpbin.org" in updated.spec.network.forbidden_domains finally: await SandboxInstance.delete(name) @@ -80,14 +78,14 @@ async def test_updates_sandbox_network_with_model_object(self): try: network_config = SandboxNetwork( - allowed_domains=["api.example.com"], + allowed_domains=["httpbin.org"], ) updated = await SandboxInstance.update_network( name, SandboxUpdateNetwork(network=network_config), ) assert updated.spec.network is not None - assert "api.example.com" in updated.spec.network.allowed_domains + assert "httpbin.org" in updated.spec.network.allowed_domains finally: await SandboxInstance.delete(name) @@ -98,7 +96,7 @@ async def test_clears_sandbox_network_config(self): { "name": name, "image": default_image, - "network": {"allowedDomains": ["api.example.com"]}, + "network": {"allowedDomains": ["httpbin.org"]}, "labels": default_labels, } ) @@ -108,7 +106,6 @@ async def test_clears_sandbox_network_config(self): name, SandboxUpdateNetwork(network=None), ) - # Network should be cleared (UNSET) from blaxel.core.client.types import UNSET assert updated.spec.network is UNSET or updated.spec.network is None From 8ebdf034e23a77b4bf3fbdd23d36a623a712e932 Mon Sep 17 00:00:00 2001 From: mjoffre Date: Thu, 28 May 2026 00:38:15 +0000 Subject: [PATCH 4/4] fix: use exact equality assertions to resolve CodeQL URL substring warnings Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tests/integration/core/sandbox/test_network.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/integration/core/sandbox/test_network.py b/tests/integration/core/sandbox/test_network.py index 0352912..6a06f4e 100644 --- a/tests/integration/core/sandbox/test_network.py +++ b/tests/integration/core/sandbox/test_network.py @@ -34,8 +34,7 @@ async def test_updates_sandbox_network_with_allowed_domains(self): SandboxUpdateNetwork(network={"allowedDomains": ["httpbin.org", "*.httpbin.org"]}), ) assert updated.spec.network is not None - assert "httpbin.org" in updated.spec.network.allowed_domains - assert "*.httpbin.org" in updated.spec.network.allowed_domains + assert set(updated.spec.network.allowed_domains) == {"httpbin.org", "*.httpbin.org"} finally: await SandboxInstance.delete(name) @@ -58,8 +57,7 @@ async def test_updates_sandbox_network_with_forbidden_domains(self): ), ) assert updated.spec.network is not None - assert "httpbin.org" in updated.spec.network.forbidden_domains - assert "*.httpbin.org" in updated.spec.network.forbidden_domains + assert set(updated.spec.network.forbidden_domains) == {"httpbin.org", "*.httpbin.org"} finally: await SandboxInstance.delete(name) @@ -85,7 +83,7 @@ async def test_updates_sandbox_network_with_model_object(self): SandboxUpdateNetwork(network=network_config), ) assert updated.spec.network is not None - assert "httpbin.org" in updated.spec.network.allowed_domains + assert updated.spec.network.allowed_domains == ["httpbin.org"] finally: await SandboxInstance.delete(name)