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
28 changes: 28 additions & 0 deletions packages/prime-sandboxes/src/prime_sandboxes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class Sandbox(BaseModel):
gpu_type: Optional[str] = Field(None, alias="gpuType")
vm: bool = False
network_access: bool = Field(True, alias="networkAccess")
allowed_domains: List[str] = Field(default_factory=list, alias="allowedDomains")
blocked_domains: List[str] = Field(default_factory=list, alias="blockedDomains")
status: str
timeout_minutes: int = Field(..., alias="timeoutMinutes")
environment_vars: Optional[Dict[str, Any]] = Field(None, alias="environmentVars")
Expand Down Expand Up @@ -89,6 +91,8 @@ class CreateSandboxRequest(BaseModel):
gpu_type: Optional[str] = None
vm: bool = False
network_access: bool = True
allowed_domains: List[str] = Field(default_factory=list)
blocked_domains: List[str] = Field(default_factory=list)
timeout_minutes: int = 60
environment_vars: Optional[Dict[str, str]] = None
secrets: Optional[Dict[str, str]] = None
Expand Down Expand Up @@ -116,6 +120,30 @@ def validate_guaranteed(self) -> "CreateSandboxRequest":
raise ValueError("guaranteed is not supported for VM sandboxes")
return self

@model_validator(mode="after")
def validate_allowed_domains(self) -> "CreateSandboxRequest":
if self.allowed_domains:
if self.network_access:
raise ValueError(
"allowed_domains requires network_access=false "
"(it is an egress allowlist for restricted sandboxes)"
)
if self.vm:
raise ValueError("allowed_domains is not supported for VM sandboxes")
return self

@model_validator(mode="after")
def validate_blocked_domains(self) -> "CreateSandboxRequest":
if self.blocked_domains:
if not self.network_access:
raise ValueError(
"blocked_domains requires network_access=true "
"(it is an egress blocklist for unrestricted sandboxes)"
)
if self.vm:
raise ValueError("blocked_domains is not supported for VM sandboxes")
return self


class UpdateSandboxRequest(BaseModel):
"""Update sandbox request model"""
Expand Down
52 changes: 52 additions & 0 deletions packages/prime/src/prime_cli/commands/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ def _format_sandbox_for_details(sandbox: Sandbox) -> Dict[str, Any]:
"gpu_type": getattr(sandbox, "gpu_type", None),
"vm": sandbox.vm,
"network_access": sandbox.network_access,
"allowed_domains": getattr(sandbox, "allowed_domains", []) or [],
"blocked_domains": getattr(sandbox, "blocked_domains", []) or [],
"timeout_minutes": sandbox.timeout_minutes,
"labels": sandbox.labels,
"created_at": iso_timestamp(sandbox.created_at),
Expand Down Expand Up @@ -407,6 +409,10 @@ def get(
style="green" if sandbox_data["network_access"] else "yellow",
)
table.add_row("Network Access", network_display)
if sandbox_data.get("allowed_domains"):
table.add_row("Allowed Domains", ", ".join(sandbox_data["allowed_domains"]))
if sandbox_data.get("blocked_domains"):
table.add_row("Blocked Domains", ", ".join(sandbox_data["blocked_domains"]))
table.add_row("Timeout (minutes)", str(sandbox_data["timeout_minutes"]))

# Show labels
Expand Down Expand Up @@ -493,6 +499,24 @@ def create(
"--network-access/--no-network-access",
help="Allow outbound internet access (enabled by default)",
),
allowed_domains: Optional[List[str]] = typer.Option(
None,
"--allowed-domain",
help=(
"Egress domain allowlist for a restricted sandbox. "
"Wildcards like '*.example.com' are allowed. Requires "
"--no-network-access. Can be specified multiple times."
),
),
blocked_domains: Optional[List[str]] = typer.Option(
None,
"--blocked-domain",
help=(
"Egress domain blocklist for an unrestricted sandbox. "
"Wildcards like '*.example.com' are allowed. Requires "
"--network-access (the default). Can be specified multiple times."
),
),
timeout_minutes: int = typer.Option(60, help="Timeout in minutes"),
team_id: Optional[str] = typer.Option(
None, help="Team ID (uses config team_id if not specified)"
Expand Down Expand Up @@ -582,6 +606,28 @@ def create(
)
raise typer.Exit(1)

if allowed_domains:
if network_access:
console.print(
"[red]--allowed-domain requires --no-network-access.[/red] "
"It is an egress allowlist for restricted sandboxes."
)
raise typer.Exit(1)
if vm:
console.print("[red]--allowed-domain is not supported for VM sandboxes.[/red]")
raise typer.Exit(1)

if blocked_domains:
if not network_access:
console.print(
"[red]--blocked-domain requires --network-access.[/red] "
"It is an egress blocklist for unrestricted sandboxes."
)
raise typer.Exit(1)
if vm:
console.print("[red]--blocked-domain is not supported for VM sandboxes.[/red]")
raise typer.Exit(1)

if not docker_image:
console.print(
"[red]Docker image is required.[/red] Provide a DOCKER_IMAGE positional argument."
Expand Down Expand Up @@ -620,6 +666,8 @@ def create(
gpu_type=gpu_type,
vm=vm,
network_access=network_access,
allowed_domains=allowed_domains if allowed_domains else [],
blocked_domains=blocked_domains if blocked_domains else [],
timeout_minutes=timeout_minutes,
environment_vars=env_vars if env_vars else None,
secrets=secrets_vars if secrets_vars else None,
Expand All @@ -643,6 +691,10 @@ def create(
console.print(f"GPUs: {gpu_type} x{gpu_count}")
network_status = "[green]Enabled[/green]" if network_access else "[yellow]Disabled[/yellow]"
console.print(f"Network Access: {network_status}")
if allowed_domains:
console.print(f"Allowed Domains: {', '.join(allowed_domains)}")
if blocked_domains:
console.print(f"Blocked Domains: {', '.join(blocked_domains)}")
console.print(f"Timeout: {timeout_minutes} minutes")
console.print(f"Team: {team_id or 'Personal'}")
console.print(f"Region: {region or 'Backend default'}")
Expand Down