e0e362d700
SDK Tests / changes (push) Waiting to run
SDK Tests / Python SDK Tests (sandbox) (push) Blocked by required conditions
SDK Tests / CLI Quality (push) Blocked by required conditions
SDK Tests / CLI Tests (push) Blocked by required conditions
SDK Tests / Python SDK Quality (code-interpreter) (push) Blocked by required conditions
SDK Tests / Python SDK Quality (sandbox) (push) Blocked by required conditions
SDK Tests / Python SDK Tests (code-interpreter) (push) Blocked by required conditions
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Blocked by required conditions
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Blocked by required conditions
Deploy Docs Pages / build (push) Waiting to run
Deploy Docs Pages / deploy (push) Blocked by required conditions
Real E2E Tests / changes (push) Waiting to run
Real E2E Tests / Python E2E (docker bridge) (push) Blocked by required conditions
Real E2E Tests / Java E2E (docker bridge) (push) Blocked by required conditions
Real E2E Tests / JavaScript E2E (docker bridge) (push) Blocked by required conditions
Real E2E Tests / C# E2E (docker bridge) (push) Blocked by required conditions
Real E2E Tests / Go E2E (docker bridge) (push) Blocked by required conditions
Real E2E Tests / Real E2E CI (push) Blocked by required conditions
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Blocked by required conditions
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Blocked by required conditions
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Blocked by required conditions
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Blocked by required conditions
SDK Tests / Go SDK Quality And Tests (push) Blocked by required conditions
SDK Tests / SDK CI (push) Blocked by required conditions
541 lines
17 KiB
Markdown
541 lines
17 KiB
Markdown
# OpenSandbox SDK for Python
|
|
|
|
|
|
A Python SDK for low-level interaction with OpenSandbox. It provides capabilities to create, manage, and interact with secure sandbox environments, including executing shell commands, managing files, and monitoring resources.
|
|
|
|
## Installation
|
|
|
|
### pip
|
|
|
|
```bash
|
|
pip install opensandbox
|
|
```
|
|
|
|
### uv
|
|
|
|
```bash
|
|
uv add opensandbox
|
|
```
|
|
|
|
## Quick Start
|
|
|
|
The following example shows how to create a sandbox and execute a shell command.
|
|
|
|
> **Note**: Before running this example, ensure the OpenSandbox service is running. See the root [README.md](../../../README.md) for startup instructions.
|
|
|
|
```python
|
|
import asyncio
|
|
from opensandbox.sandbox import Sandbox
|
|
from opensandbox.config import ConnectionConfig
|
|
from opensandbox.exceptions import SandboxException
|
|
|
|
async def main():
|
|
# 1. Configure connection
|
|
config = ConnectionConfig(
|
|
domain="api.opensandbox.io",
|
|
api_key="your-api-key"
|
|
)
|
|
|
|
# 2. Create a Sandbox
|
|
try:
|
|
sandbox = await Sandbox.create(
|
|
"ubuntu",
|
|
connection_config=config
|
|
)
|
|
async with sandbox:
|
|
|
|
# 3. Execute a shell command
|
|
execution = await sandbox.commands.run("echo 'Hello Sandbox!'")
|
|
|
|
# 4. Print output
|
|
print(execution.logs.stdout[0].text)
|
|
|
|
# 5. Cleanup (sandbox.close() called automatically)
|
|
# Note: kill() must be called explicitly if you want to terminate the remote sandbox instance immediately
|
|
await sandbox.kill()
|
|
|
|
except SandboxException as e:
|
|
# Handle Sandbox specific exceptions
|
|
print(f"Sandbox Error: [{e.error.code}] {e.error.message}")
|
|
# Server logs can be correlated by this request id (if available)
|
|
print(f"Request ID: {e.request_id}")
|
|
except Exception as e:
|
|
print(f"Error: {e}")
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|
|
```
|
|
|
|
### Synchronous Quick Start
|
|
|
|
If you prefer a synchronous API, use `SandboxSync` / `SandboxManagerSync` and `ConnectionConfigSync`:
|
|
|
|
```python
|
|
from datetime import timedelta
|
|
|
|
import httpx
|
|
from opensandbox import SandboxSync
|
|
from opensandbox.config import ConnectionConfigSync
|
|
|
|
config = ConnectionConfigSync(
|
|
domain="api.opensandbox.io",
|
|
api_key="your-api-key",
|
|
request_timeout=timedelta(seconds=30),
|
|
transport=httpx.HTTPTransport(limits=httpx.Limits(max_connections=20)),
|
|
)
|
|
|
|
sandbox = SandboxSync.create("ubuntu", connection_config=config)
|
|
with sandbox:
|
|
execution = sandbox.commands.run("echo 'Hello Sandbox!'")
|
|
print(execution.logs.stdout[0].text)
|
|
sandbox.kill()
|
|
```
|
|
|
|
### Synchronous Sandbox Pool
|
|
|
|
`SandboxPoolSync` keeps a buffer of ready sandboxes to reduce acquire latency. The
|
|
pool API is synchronous and aligned with the Kotlin `SandboxPool` semantics: acquire
|
|
is allowed on any node, while replenish/shrink is gated by the store's primary lock.
|
|
|
|
```python
|
|
from datetime import timedelta
|
|
|
|
from opensandbox import (
|
|
AcquirePolicy,
|
|
InMemoryPoolStateStore,
|
|
PoolCreationSpec,
|
|
SandboxPoolSync,
|
|
)
|
|
from opensandbox.config import ConnectionConfigSync
|
|
|
|
pool = SandboxPoolSync(
|
|
pool_name="demo-pool",
|
|
owner_id="worker-1",
|
|
max_idle=2,
|
|
state_store=InMemoryPoolStateStore(), # single-process only
|
|
connection_config=ConnectionConfigSync(domain="api.opensandbox.io"),
|
|
creation_spec=PoolCreationSpec(image="ubuntu:22.04"),
|
|
reconcile_interval=timedelta(seconds=5),
|
|
)
|
|
|
|
pool.start()
|
|
try:
|
|
sandbox = pool.acquire(
|
|
sandbox_timeout=timedelta(minutes=30),
|
|
policy=AcquirePolicy.FAIL_FAST,
|
|
)
|
|
try:
|
|
result = sandbox.commands.run("echo pool-ok")
|
|
print(result.logs.stdout[0].text)
|
|
finally:
|
|
sandbox.kill()
|
|
sandbox.close()
|
|
finally:
|
|
pool.shutdown(graceful=True)
|
|
```
|
|
|
|
Use `SandboxPoolAsync` in asyncio applications so pool acquire, warmup, health
|
|
checks, and lifecycle calls do not block the event loop:
|
|
|
|
```python
|
|
from datetime import timedelta
|
|
|
|
from opensandbox import (
|
|
AcquirePolicy,
|
|
InMemoryAsyncPoolStateStore,
|
|
PoolCreationSpec,
|
|
SandboxPoolAsync,
|
|
)
|
|
from opensandbox.config import ConnectionConfig
|
|
|
|
pool = SandboxPoolAsync(
|
|
pool_name="demo-pool",
|
|
owner_id="worker-1",
|
|
max_idle=2,
|
|
state_store=InMemoryAsyncPoolStateStore(), # single-event-loop only
|
|
connection_config=ConnectionConfig(domain="api.opensandbox.io"),
|
|
creation_spec=PoolCreationSpec(image="ubuntu:22.04"),
|
|
)
|
|
|
|
await pool.start()
|
|
try:
|
|
sandbox = await pool.acquire(
|
|
sandbox_timeout=timedelta(minutes=30),
|
|
policy=AcquirePolicy.FAIL_FAST,
|
|
)
|
|
try:
|
|
result = await sandbox.commands.run("echo pool-ok")
|
|
print(result.logs.stdout[0].text)
|
|
finally:
|
|
await sandbox.kill()
|
|
await sandbox.close()
|
|
finally:
|
|
await pool.shutdown(graceful=True)
|
|
```
|
|
|
|
For Python production services with multiple processes or pods, use Redis-backed
|
|
pool state. Install the optional dependency:
|
|
|
|
```bash
|
|
pip install "opensandbox[pool-redis]"
|
|
```
|
|
|
|
Create and configure the Redis client yourself, then pass it to `RedisPoolStateStore`.
|
|
The store does not create or close Redis clients.
|
|
|
|
```python
|
|
import redis
|
|
|
|
from opensandbox import PoolCreationSpec, SandboxPoolSync
|
|
from opensandbox.config import ConnectionConfigSync
|
|
from opensandbox.pool_redis import RedisPoolStateStore
|
|
|
|
redis_client = redis.Redis.from_url(
|
|
"redis://user:password@redis.example.com:6379/0",
|
|
decode_responses=True,
|
|
)
|
|
|
|
pool = SandboxPoolSync(
|
|
pool_name="prod-pool",
|
|
owner_id="worker-1",
|
|
max_idle=10,
|
|
state_store=RedisPoolStateStore(redis_client, key_prefix="opensandbox:pool:prod"),
|
|
connection_config=ConnectionConfigSync(domain="api.opensandbox.io"),
|
|
creation_spec=PoolCreationSpec(image="ubuntu:22.04"),
|
|
primary_lock_ttl=timedelta(seconds=60),
|
|
)
|
|
```
|
|
|
|
For async pools, pass a `redis.asyncio` client to `AsyncRedisPoolStateStore`.
|
|
|
|
Notes:
|
|
|
|
- `InMemoryPoolStateStore` is for single-process development and tests. It is not
|
|
a process-wide or pod-wide pool for gunicorn, uvicorn workers, Celery, or Kubernetes.
|
|
- `max_idle` is the target/cap for ready idle sandboxes. It is not a global limit
|
|
on borrowed or directly-created sandboxes.
|
|
- For distributed deployment, all nodes in one logical pool must share the same
|
|
`key_prefix` and `pool_name`.
|
|
- Each running process should use a unique `owner_id`; it identifies the primary
|
|
lock owner and is not the pool identifier.
|
|
- All nodes sharing one pool must use the same creation and warmup definition. If
|
|
that definition changes, use a new `pool_name` or `key_prefix` and drain the old pool.
|
|
- `resize(max_idle)` can be called from any node. The call returns after the new
|
|
idle target is stored in the shared state store; the current primary applies
|
|
replenish or shrink work during periodic reconcile.
|
|
- Use `resize(0)` and wait for `snapshot().idle_count == 0` to drain a distributed
|
|
idle buffer. `release_all_idle()` is only a best-effort cleanup pass in distributed
|
|
mode because another primary may put new idle sandboxes concurrently unless the
|
|
shared target has already been reduced.
|
|
- Configure `primary_lock_ttl` greater than `warmup_ready_timeout` plus expected
|
|
warmup preparer time and buffer.
|
|
- Redis outages are surfaced as pool state store errors. The pool fails closed; it
|
|
does not bypass shared state.
|
|
|
|
## Usage Examples
|
|
|
|
### 1. Lifecycle Management
|
|
|
|
Manage the sandbox lifecycle, including renewal, pausing, and resuming.
|
|
|
|
```python
|
|
from datetime import timedelta
|
|
|
|
# Renew the sandbox
|
|
# This resets the expiration time to (current time + duration)
|
|
await sandbox.renew(timedelta(minutes=30))
|
|
|
|
# Pause execution (suspends all processes)
|
|
await sandbox.pause()
|
|
|
|
# Resume execution
|
|
sandbox = await Sandbox.resume(
|
|
sandbox_id=sandbox.id,
|
|
connection_config=config,
|
|
)
|
|
|
|
# Get current status
|
|
info = await sandbox.get_info()
|
|
print(f"State: {info.status.state}")
|
|
print(f"Expires: {info.expires_at}") # None when no automatic expiration is configured
|
|
```
|
|
|
|
Create a non-expiring sandbox by omitting `timeout`:
|
|
|
|
```python
|
|
manual = await Sandbox.create(
|
|
"ubuntu",
|
|
connection_config=config,
|
|
)
|
|
```
|
|
|
|
### 2. Custom Health Check
|
|
|
|
Define custom logic to determine if the sandbox is healthy. This overrides the default ping check.
|
|
|
|
```python
|
|
async def custom_health_check(sbx: Sandbox) -> bool:
|
|
try:
|
|
# 1. Get the external mapped address for port 80
|
|
endpoint = await sbx.get_endpoint(80)
|
|
|
|
# 2. Perform your connection check (e.g. HTTP request, Socket connect)
|
|
# return await check_connection(endpoint.endpoint)
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
sandbox = await Sandbox.create(
|
|
"nginx:latest",
|
|
connection_config=config,
|
|
health_check=custom_health_check # Custom check: Wait for port 80 to be accessible
|
|
)
|
|
```
|
|
|
|
### 3. Command Execution & Streaming
|
|
|
|
Execute commands and handle output streams in real-time.
|
|
|
|
```python
|
|
from opensandbox.models.execd import ExecutionHandlers, RunCommandOpts
|
|
|
|
# Define async handlers for streaming output
|
|
async def handle_stdout(msg):
|
|
print(f"STDOUT: {msg.text}")
|
|
|
|
async def handle_stderr(msg):
|
|
print(f"STDERR: {msg.text}")
|
|
|
|
async def handle_complete(complete):
|
|
print(f"Command finished in {complete.execution_time_in_millis}ms")
|
|
|
|
# Create handlers (all handlers must be async)
|
|
handlers = ExecutionHandlers(
|
|
on_stdout=handle_stdout,
|
|
on_stderr=handle_stderr,
|
|
on_execution_complete=handle_complete
|
|
)
|
|
|
|
# Execute command with handlers
|
|
result = await sandbox.commands.run(
|
|
"for i in {1..5}; do echo \"Count $i\"; sleep 0.5; done",
|
|
handlers=handlers
|
|
)
|
|
```
|
|
|
|
### 4. Comprehensive File Operations
|
|
|
|
Manage files and directories, including read, write, list, delete, and search.
|
|
|
|
```python
|
|
from opensandbox.models.filesystem import WriteEntry, SearchEntry
|
|
|
|
# 1. Write file
|
|
await sandbox.files.write_files([
|
|
WriteEntry(
|
|
path="/tmp/hello.txt",
|
|
data="Hello World",
|
|
mode=644
|
|
)
|
|
])
|
|
|
|
# 2. Read file
|
|
content = await sandbox.files.read_file("/tmp/hello.txt")
|
|
print(f"Content: {content}")
|
|
|
|
# 3. List/Search files
|
|
files = await sandbox.files.search(
|
|
SearchEntry(
|
|
path="/tmp",
|
|
pattern="*.txt"
|
|
)
|
|
)
|
|
for f in files:
|
|
print(f"Found: {f.path}")
|
|
|
|
# 4. Delete file
|
|
await sandbox.files.delete_files(["/tmp/hello.txt"])
|
|
```
|
|
|
|
### 5. Sandbox Management (Admin)
|
|
|
|
Use `SandboxManager` for administrative tasks and finding existing sandboxes.
|
|
|
|
```python
|
|
from opensandbox.manager import SandboxManager
|
|
from opensandbox.models.sandboxes import SandboxFilter
|
|
|
|
# Create manager using async context manager
|
|
async with await SandboxManager.create(connection_config=config) as manager:
|
|
|
|
# List running sandboxes
|
|
sandboxes = await manager.list_sandbox_infos(
|
|
SandboxFilter(
|
|
states=["RUNNING"],
|
|
page_size=10
|
|
)
|
|
)
|
|
|
|
for info in sandboxes.sandbox_infos:
|
|
print(f"Found sandbox: {info.id}")
|
|
# Perform admin actions
|
|
await manager.kill_sandbox(info.id)
|
|
```
|
|
|
|
## Configuration
|
|
|
|
### 1. Connection Configuration
|
|
|
|
The `ConnectionConfig` class manages API server connection settings.
|
|
|
|
| Parameter | Description | Default | Environment Variable |
|
|
| ----------------- | ------------------------------------------ | ---------------------------- | ---------------------- |
|
|
| `api_key` | API Key for authentication | Required | `OPEN_SANDBOX_API_KEY` |
|
|
| `domain` | The endpoint domain of the sandbox service | Required (or localhost:8080) | `OPEN_SANDBOX_DOMAIN` |
|
|
| `protocol` | HTTP protocol (http/https) | `http` | - |
|
|
| `request_timeout` | Timeout for API requests | 30 seconds | - |
|
|
| `debug` | Enable debug logging for HTTP requests | `False` | - |
|
|
| `headers` | Custom HTTP headers | Empty | - |
|
|
| `transport` | Shared httpx transport (pool/proxy/retry) | SDK-created per instance | - |
|
|
| `use_server_proxy` | Use sandbox server as proxy for execd/endpoint requests (e.g. when client cannot reach the sandbox directly) | `False` | - |
|
|
|
|
```python
|
|
from datetime import timedelta
|
|
|
|
# 1. Basic configuration
|
|
config = ConnectionConfig(
|
|
api_key="your-key",
|
|
domain="api.opensandbox.io",
|
|
request_timeout=timedelta(seconds=60)
|
|
)
|
|
|
|
# 2. Advanced: Custom headers and custom transport
|
|
# If you create many Sandbox instances, configuring a shared transport is recommended to optimize resource usage.
|
|
# SDK default keep-alive is 30 seconds for its own transports.
|
|
import httpx
|
|
|
|
config = ConnectionConfig(
|
|
api_key="your-key",
|
|
domain="api.opensandbox.io",
|
|
headers={
|
|
"X-Custom-Header": "value",
|
|
"X-Request-ID": "trace-123",
|
|
},
|
|
transport=httpx.AsyncHTTPTransport(
|
|
limits=httpx.Limits(
|
|
max_connections=100,
|
|
max_keepalive_connections=50,
|
|
keepalive_expiry=30.0,
|
|
)
|
|
),
|
|
)
|
|
|
|
# If you provide a custom transport, you are responsible for closing it:
|
|
# await config.transport.aclose()
|
|
```
|
|
|
|
### 2. Sandbox Creation Configuration
|
|
|
|
The `Sandbox.create()` allows configuring the sandbox environment.
|
|
|
|
| Parameter | Description | Default |
|
|
| --------------- | ---------------------------------------- | ------------------------------- |
|
|
| `image` | Docker image specification | Required |
|
|
| `timeout` | Automatic termination timeout | 10 minutes |
|
|
| `entrypoint` | Container entrypoint command | `["tail", "-f", "/dev/null"]` |
|
|
| `resource` | CPU and memory limits | `{"cpu": "1", "memory": "2Gi"}` |
|
|
| `env` | Environment variables | Empty |
|
|
| `metadata` | Custom metadata tags | Empty |
|
|
| `network_policy` | Optional outbound network policy (egress) | - |
|
|
| `credential_proxy` | Optional Credential Vault proxy startup settings | - |
|
|
| `ready_timeout` | Max time to wait for sandbox to be ready | 30 seconds |
|
|
|
|
Note: metadata keys under `opensandbox.io/` are reserved for system-managed
|
|
labels and will be rejected by the server.
|
|
|
|
```python
|
|
from datetime import timedelta
|
|
|
|
from opensandbox.models.sandboxes import NetworkPolicy, NetworkRule
|
|
|
|
sandbox = await Sandbox.create(
|
|
"python:3.11",
|
|
connection_config=config,
|
|
timeout=timedelta(minutes=30),
|
|
resource={"cpu": "2", "memory": "4Gi"},
|
|
env={"PYTHONPATH": "/app"},
|
|
metadata={"project": "demo"},
|
|
network_policy=NetworkPolicy(
|
|
defaultAction="deny",
|
|
egress=[NetworkRule(action="allow", target="pypi.org")],
|
|
),
|
|
)
|
|
```
|
|
|
|
### 3. Runtime Egress Policy Updates
|
|
|
|
Runtime egress policy reads and patches are sent directly to the sandbox egress sidecar.
|
|
The SDK first resolves the sandbox endpoint on port `18080`, then calls the sidecar `/policy` API.
|
|
|
|
Patch uses merge semantics:
|
|
- Incoming rules take priority over existing rules with the same `target`.
|
|
- Existing rules for other targets remain unchanged.
|
|
- Within a single patch payload, the first rule for a `target` wins.
|
|
- The current `defaultAction` is preserved.
|
|
|
|
```python
|
|
policy = await sandbox.get_egress_policy()
|
|
|
|
await sandbox.patch_egress_rules(
|
|
[
|
|
NetworkRule(action="allow", target="www.github.com"),
|
|
NetworkRule(action="deny", target="pypi.org"),
|
|
]
|
|
)
|
|
```
|
|
|
|
### 4. Credential Vault
|
|
|
|
Credential Vault injects outbound credentials from the egress sidecar while
|
|
keeping real secrets out of sandbox environment variables, commands, files, and
|
|
logs. Create the sandbox with `credential_proxy` enabled, then write credentials
|
|
and bindings through `sandbox.credential_vault`.
|
|
|
|
```python
|
|
from opensandbox.models.sandboxes import (
|
|
Credential,
|
|
CredentialBinding,
|
|
CredentialProxyConfig,
|
|
NetworkPolicy,
|
|
NetworkRule,
|
|
)
|
|
|
|
sandbox = await Sandbox.create(
|
|
"python:3.11",
|
|
connection_config=config,
|
|
network_policy=NetworkPolicy(
|
|
defaultAction="deny",
|
|
egress=[NetworkRule(action="allow", target="api.example.com")],
|
|
),
|
|
credential_proxy=CredentialProxyConfig(enabled=True),
|
|
)
|
|
|
|
await sandbox.credential_vault.create(
|
|
credentials=[Credential(name="api-token", source={"value": "<token>"})],
|
|
bindings=[
|
|
CredentialBinding(
|
|
name="api-token",
|
|
match={
|
|
"schemes": ["https"],
|
|
"hosts": ["api.example.com"],
|
|
"paths": ["/v1/*"],
|
|
},
|
|
auth={"type": "apiKey", "name": "x-api-key", "credential": "api-token"},
|
|
)
|
|
],
|
|
)
|
|
```
|
|
|
|
See [Credential Vault](../../../docs/guides/credential-vault.md) for auth types,
|
|
binding guidance, and Git/curl examples.
|