1+ import asyncio
2+ import functools
3+ import time
14from typing import Dict , Optional , Union
25
36from ....exceptions import HyperbrowserError
47from ....models .sandbox import (
8+ CompleteSandboxImageBuildParams ,
59 CreateSandboxParams ,
10+ CreateSandboxImageBuildParams ,
611 SandboxDetail ,
712 SandboxExecParams ,
813 SandboxExposeParams ,
914 SandboxExposeResult ,
15+ SandboxImageBuild ,
16+ SandboxImageBuildCreateResult ,
17+ SandboxImageListParams ,
1018 SandboxImageListResponse ,
19+ SandboxImageInit ,
1120 SandboxListParams ,
1221 SandboxListResponse ,
1322 SandboxMemorySnapshotParams ,
1423 SandboxMemorySnapshotResult ,
24+ SandboxNetworkPolicy ,
25+ SandboxNetworkUpdateResult ,
1526 SandboxRuntimeSession ,
1627 SandboxSnapshotListParams ,
1728 SandboxSnapshotListResponse ,
3041 _copy_model ,
3142 _expires_within_buffer ,
3243)
44+ from ..sandboxes .image_build import (
45+ IMAGE_BUILD_SOURCE_PLATFORM ,
46+ build_docker_image_from_dockerfile ,
47+ is_terminal_image_build_status ,
48+ make_temp_docker_tag ,
49+ package_docker_image ,
50+ remove_docker_image ,
51+ upload_image_build_artifact ,
52+ )
3353from .sandboxes .sandbox_files import (
3454 DEFAULT_WATCH_TIMEOUT_MS ,
3555 SandboxFileWatchHandle ,
6787]
6888
6989
90+ async def _run_blocking (func , * args , ** kwargs ):
91+ loop = asyncio .get_running_loop ()
92+ call = functools .partial (func , * args , ** kwargs )
93+ return await loop .run_in_executor (None , call )
94+
95+
7096class SandboxHandle :
7197 def __init__ (self , service : "SandboxManager" , detail : SandboxDetail ):
7298 self ._service = service
@@ -130,6 +156,10 @@ def disk_mib(self):
130156 def exposed_ports (self ):
131157 return self ._detail .exposed_ports
132158
159+ @property
160+ def network (self ):
161+ return self ._detail .network
162+
133163 def to_dict (self ):
134164 return self ._detail .model_dump ()
135165
@@ -166,6 +196,25 @@ async def create_memory_snapshot(
166196 raise TypeError ("params must be a SandboxMemorySnapshotParams instance" )
167197 return await self ._service .create_memory_snapshot (self .id , normalized )
168198
199+ async def update_network (
200+ self ,
201+ policy : SandboxNetworkPolicy ,
202+ ) -> SandboxNetworkUpdateResult :
203+ if not isinstance (policy , SandboxNetworkPolicy ):
204+ raise TypeError ("policy must be a SandboxNetworkPolicy instance" )
205+ result = await self ._service .update_network (self .id , policy )
206+ self ._detail = self ._detail .model_copy (update = {"network" : result .network })
207+ return result
208+
209+ async def clear_network (self ) -> SandboxNetworkUpdateResult :
210+ return await self .update_network (
211+ SandboxNetworkPolicy (
212+ allow_internet_access = True ,
213+ allow_out = [],
214+ deny_out = [],
215+ )
216+ )
217+
169218 async def expose (self , params : SandboxExposeParams ) -> SandboxExposeResult :
170219 if not isinstance (params , SandboxExposeParams ):
171220 raise TypeError ("params must be a SandboxExposeParams instance" )
@@ -350,8 +399,16 @@ async def list(
350399 )
351400 return SandboxListResponse (** payload )
352401
353- async def list_images (self ) -> SandboxImageListResponse :
354- payload = await self ._request ("GET" , "/images" )
402+ async def list_images (
403+ self , params : SandboxImageListParams = SandboxImageListParams ()
404+ ) -> SandboxImageListResponse :
405+ if not isinstance (params , SandboxImageListParams ):
406+ raise TypeError ("params must be a SandboxImageListParams instance" )
407+ payload = await self ._request (
408+ "GET" ,
409+ "/images" ,
410+ params = params .model_dump (exclude_none = True , by_alias = True ),
411+ )
355412 return SandboxImageListResponse (** payload )
356413
357414 async def list_snapshots (
@@ -370,6 +427,170 @@ async def stop(self, sandbox_id: str) -> BasicResponse:
370427 payload = await self ._request ("PUT" , f"/sandbox/{ sandbox_id } /stop" )
371428 return BasicResponse (** payload )
372429
430+ async def create_image_build (
431+ self ,
432+ params : CreateSandboxImageBuildParams ,
433+ ) -> SandboxImageBuildCreateResult :
434+ if not isinstance (params , CreateSandboxImageBuildParams ):
435+ raise TypeError ("params must be a CreateSandboxImageBuildParams instance" )
436+ payload = await self ._request (
437+ "POST" ,
438+ "/images/builds" ,
439+ data = params .model_dump (exclude_none = True , by_alias = True ),
440+ )
441+ return SandboxImageBuildCreateResult (** payload )
442+
443+ async def get_image_build (self , build_id : str ) -> SandboxImageBuild :
444+ payload = await self ._request ("GET" , f"/images/builds/{ build_id } " )
445+ return SandboxImageBuild (** payload ["build" ])
446+
447+ async def complete_image_build (
448+ self ,
449+ build_id : str ,
450+ params : CompleteSandboxImageBuildParams ,
451+ ) -> SandboxImageBuild :
452+ if not isinstance (params , CompleteSandboxImageBuildParams ):
453+ raise TypeError ("params must be a CompleteSandboxImageBuildParams instance" )
454+ payload = await self ._request (
455+ "POST" ,
456+ f"/images/builds/{ build_id } /complete" ,
457+ data = params .model_dump (exclude_none = True , by_alias = True ),
458+ )
459+ return SandboxImageBuild (** payload ["build" ])
460+
461+ async def cancel_image_build (self , build_id : str ) -> SandboxImageBuild :
462+ payload = await self ._request ("POST" , f"/images/builds/{ build_id } /cancel" )
463+ return SandboxImageBuild (** payload ["build" ])
464+
465+ async def wait_for_image_build (
466+ self ,
467+ build_id : str ,
468+ * ,
469+ poll_interval : float = 3.0 ,
470+ timeout : Optional [float ] = 35 * 60 ,
471+ ) -> SandboxImageBuild :
472+ deadline = None if timeout is None else time .monotonic () + timeout
473+ while True :
474+ build = await self .get_image_build (build_id )
475+ if build .status == "completed" :
476+ return build
477+ if build .status == "failed" :
478+ message = build .error_message or "image build failed"
479+ if build .error_code :
480+ message = f"image build failed [{ build .error_code } ]: { message } "
481+ raise RuntimeError (message )
482+ if is_terminal_image_build_status (build .status ):
483+ raise RuntimeError (f"image build { build .status } " )
484+ if deadline is not None and time .monotonic () >= deadline :
485+ raise TimeoutError (f"timed out waiting for image build { build_id } " )
486+ await asyncio .sleep (poll_interval )
487+
488+ async def build_image_from_docker_image (
489+ self ,
490+ * ,
491+ docker_image : str ,
492+ image_name : str ,
493+ platform : str = IMAGE_BUILD_SOURCE_PLATFORM ,
494+ image_init : Optional [SandboxImageInit ] = None ,
495+ image_config_user : Optional [str ] = None ,
496+ wait : bool = True ,
497+ poll_interval : float = 3.0 ,
498+ wait_timeout : Optional [float ] = 35 * 60 ,
499+ temp_dir : Optional [str ] = None ,
500+ upload_timeout : Optional [float ] = None ,
501+ ) -> SandboxImageBuild :
502+ artifact = await _run_blocking (
503+ package_docker_image ,
504+ docker_image ,
505+ platform = platform ,
506+ temp_dir = temp_dir ,
507+ )
508+ try :
509+ create_result = await self .create_image_build (
510+ CreateSandboxImageBuildParams (
511+ image_name = image_name ,
512+ input_sha256 = artifact .sha256_hex ,
513+ input_size_bytes = artifact .size_bytes ,
514+ input_format = artifact .input_format ,
515+ source_platform = artifact .source_platform ,
516+ image_config_user = (
517+ image_config_user
518+ if image_config_user is not None
519+ else artifact .image_config_user
520+ ),
521+ image_init = image_init
522+ if image_init is not None
523+ else artifact .image_init ,
524+ )
525+ )
526+ await _run_blocking (
527+ upload_image_build_artifact ,
528+ create_result .upload ,
529+ artifact .path ,
530+ timeout = upload_timeout ,
531+ )
532+ build = await self .complete_image_build (
533+ create_result .build .id ,
534+ CompleteSandboxImageBuildParams (
535+ input_sha256 = artifact .sha256_hex ,
536+ input_size_bytes = artifact .size_bytes ,
537+ input_format = artifact .input_format ,
538+ ),
539+ )
540+ if wait :
541+ return await self .wait_for_image_build (
542+ build .id ,
543+ poll_interval = poll_interval ,
544+ timeout = wait_timeout ,
545+ )
546+ return build
547+ finally :
548+ artifact .cleanup ()
549+
550+ async def build_image_from_dockerfile (
551+ self ,
552+ * ,
553+ context_path ,
554+ image_name : str ,
555+ dockerfile = "Dockerfile" ,
556+ docker_tag : Optional [str ] = None ,
557+ platform : str = IMAGE_BUILD_SOURCE_PLATFORM ,
558+ build_args : Optional [Dict [str , str ]] = None ,
559+ image_init : Optional [SandboxImageInit ] = None ,
560+ image_config_user : Optional [str ] = None ,
561+ wait : bool = True ,
562+ poll_interval : float = 3.0 ,
563+ wait_timeout : Optional [float ] = 35 * 60 ,
564+ temp_dir : Optional [str ] = None ,
565+ upload_timeout : Optional [float ] = None ,
566+ ) -> SandboxImageBuild :
567+ tag = docker_tag or make_temp_docker_tag ()
568+ remove_tag = docker_tag is None
569+ try :
570+ await _run_blocking (
571+ build_docker_image_from_dockerfile ,
572+ context_path = context_path ,
573+ dockerfile = dockerfile ,
574+ tag = tag ,
575+ platform = platform ,
576+ build_args = build_args ,
577+ )
578+ return await self .build_image_from_docker_image (
579+ docker_image = tag ,
580+ image_name = image_name ,
581+ platform = platform ,
582+ image_init = image_init ,
583+ image_config_user = image_config_user ,
584+ wait = wait ,
585+ poll_interval = poll_interval ,
586+ wait_timeout = wait_timeout ,
587+ temp_dir = temp_dir ,
588+ upload_timeout = upload_timeout ,
589+ )
590+ finally :
591+ if remove_tag :
592+ await _run_blocking (remove_docker_image , tag )
593+
373594 async def get_runtime_session (self , sandbox_id : str ) -> SandboxRuntimeSession :
374595 detail = await self .get_detail (sandbox_id )
375596 session = SandboxHandle ._to_runtime_session (detail )
@@ -404,6 +625,20 @@ async def create_memory_snapshot(
404625 )
405626 return SandboxMemorySnapshotResult (** payload )
406627
628+ async def update_network (
629+ self ,
630+ sandbox_id : str ,
631+ policy : SandboxNetworkPolicy ,
632+ ) -> SandboxNetworkUpdateResult :
633+ if not isinstance (policy , SandboxNetworkPolicy ):
634+ raise TypeError ("policy must be a SandboxNetworkPolicy instance" )
635+ payload = await self ._request (
636+ "PUT" ,
637+ f"/sandbox/{ sandbox_id } /network" ,
638+ data = policy .model_dump (exclude_none = True , by_alias = True ),
639+ )
640+ return SandboxNetworkUpdateResult (** payload )
641+
407642 async def expose (
408643 self ,
409644 sandbox_id : str ,
0 commit comments