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
62 changes: 62 additions & 0 deletions examples/openclaw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,54 @@

Launch an [OpenClaw](https://github.com/openclaw/openclaw) Gateway inside an OpenSandbox instance and expose its HTTP endpoint. The script polls the gateway until it returns HTTP 200, then prints the reachable endpoint.

## Quick Start

```shell
# Install dependencies
uv pip install opensandbox requests

# Run with default settings
uv run python examples/openclaw/main.py
```

## Configuration Options

The example supports various environment variables for customization:

| Variable | Default | Description |
|----------|---------|-------------|
| `OPENCLAW_SERVER` | `http://localhost:8080` | OpenSandbox server address |
| `OPENCLAW_TOKEN` | `dummy-token-for-sandbox` | Gateway authentication token |
| `OPENCLAW_IMAGE` | `ghcr.io/openclaw/openclaw:latest` | Container image |
| `OPENCLAW_TIMEOUT` | `3600` | Sandbox timeout in seconds |

## Network Policy

By default, the sandbox denies all network access except `pypi.org` (for package installation). You can customize this in `main.py`:

```python
network_policy=NetworkPolicy(
defaultAction="deny",
egress=[
NetworkRule(action="allow", target="pypi.org"),
NetworkRule(action="allow", target="pypi.python.org"),
# Add more allowed targets
],
)
```

## Environment Variables for OpenClaw

Pass environment variables to the OpenClaw Gateway inside the sandbox:

```python
env={
"OPENCLAW_GATEWAY_TOKEN": token,
"OPENCLAW_MODEL": "claude-sonnet-4-20250514",
# Add more env vars as needed
},
```

## Start OpenSandbox server [local]

You can find the latest OpenClaw container image [here](https://github.com/openclaw/openclaw/pkgs/container/openclaw).
Expand Down Expand Up @@ -65,6 +113,20 @@ Openclaw started finished. Please refer to 127.0.0.1:56123

The endpoint printed at the end (e.g., `127.0.0.1:56123`) is the OpenClaw Gateway address exposed from the sandbox.

## Advanced: Custom Gateway Port

To use a custom port, modify the `entrypoint` in `main.py`:

```python
entrypoint=["node dist/index.js gateway --bind=lan --port 19999 --allow-unconfigured --verbose"],
```

Then update the port in the `get_endpoint()` call:

```python
endpoint = sandbox.get_endpoint(19999)
```

## References
- [OpenClaw](https://github.com/openclaw/openclaw)
- [OpenSandbox Python SDK](https://pypi.org/project/opensandbox/)
42 changes: 32 additions & 10 deletions examples/openclaw/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,28 @@
import requests


def check_openclaw(sbx: SandboxSync) -> bool:
# Configuration defaults - can be overridden via environment variables
DEFAULT_SERVER = os.getenv("OPENCLAW_SERVER", "http://localhost:8080")
DEFAULT_IMAGE = os.getenv("OPENCLAW_IMAGE", "ghcr.io/openclaw/openclaw:latest")
DEFAULT_TIMEOUT = int(os.getenv("OPENCLAW_TIMEOUT", "3600"))
DEFAULT_TOKEN = os.getenv("OPENCLAW_TOKEN", "dummy-token-for-sandbox")
DEFAULT_PORT = int(os.getenv("OPENCLAW_PORT", "18789"))


def check_openclaw(sbx: SandboxSync, port: int = DEFAULT_PORT) -> bool:
"""
Health check: poll openclaw until it returns 200.

Args:
sbx: SandboxSync instance
port: Gateway port to check

Returns:
True when ready
False on timeout or any exception
"""
try:
endpoint = sbx.get_endpoint(18789)
endpoint = sbx.get_endpoint(port)
start = time.perf_counter()
url = f"http://{endpoint.endpoint}"
for _ in range(150): # max for ~30s
Expand All @@ -51,31 +63,41 @@ def check_openclaw(sbx: SandboxSync) -> bool:


def main() -> None:
server = "http://localhost:8080"
image = "ghcr.io/openclaw/openclaw:latest"
timeout_seconds = 3600 # 1 hour
token = os.getenv("OPENCLAW_GATEWAY_TOKEN", "dummy-token-for-sandbox")
server = DEFAULT_SERVER
image = DEFAULT_IMAGE
timeout_seconds = DEFAULT_TIMEOUT
token = os.getenv("OPENCLAW_GATEWAY_TOKEN", DEFAULT_TOKEN)
port = DEFAULT_PORT

print(f"Creating openclaw sandbox with image={image} on OpenSandbox server {server}...")
print(f" Token: {token[:16]}..." if len(token) > 16 else f" Token: {token}")
print(f" Port: {port}")
print(f" Timeout: {timeout_seconds}s")

sandbox = SandboxSync.create(
image=image,
timeout=timedelta(seconds=timeout_seconds),
metadata={"example": "openclaw"},
entrypoint=["node dist/index.js gateway --bind=lan --port 18789 --allow-unconfigured --verbose"],
entrypoint=[f"node dist/index.js gateway --bind=lan --port {port} --allow-unconfigured --verbose"],
connection_config=ConnectionConfigSync(domain=server),
health_check=check_openclaw,
health_check=lambda sbx: check_openclaw(sbx, port),
# env for openclaw
env={
"OPENCLAW_GATEWAY_TOKEN": token
},
# use network policy to limit openclaw network accesses
network_policy=NetworkPolicy(
defaultAction="deny",
egress=[NetworkRule(action="allow", target="pypi.org")],
egress=[
NetworkRule(action="allow", target="pypi.org"),
NetworkRule(action="allow", target="pypi.python.org"),
NetworkRule(action="allow", target="github.com"),
NetworkRule(action="allow", target="api.github.com"),
],
),
)

endpoint = sandbox.get_endpoint(18789)
endpoint = sandbox.get_endpoint(port)
print(f"Openclaw started finished. Please refer to {endpoint.endpoint}")

if __name__ == "__main__":
Expand Down
4 changes: 2 additions & 2 deletions server/src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,9 +378,9 @@ def validate_secure_runtime(self) -> "SecureRuntimeConfig":
class DockerConfig(BaseModel):
"""Docker runtime specific settings."""

network_mode: Literal["host", "bridge"] = Field(
network_mode: str = Field(
default="host",
description="Docker network mode for sandbox containers (host, bridge, ...).",
description="Docker network mode for sandbox containers (host, bridge, or user-defined network name).",
)
api_timeout: Optional[int] = Field(
default=None,
Expand Down
7 changes: 3 additions & 4 deletions server/src/services/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,6 @@ def __init__(self, config: Optional[AppConfig] = None):

self.execd_image = runtime_config.execd_image
self.network_mode = (self.app_config.docker.network_mode or HOST_NETWORK_MODE).lower()
if self.network_mode not in {HOST_NETWORK_MODE, BRIDGE_NETWORK_MODE}:
raise ValueError(f"Unsupported Docker network_mode '{self.network_mode}'.")
self._execd_archive_cache: Optional[bytes] = None
self._api_timeout = self._resolve_api_timeout()
try:
Expand Down Expand Up @@ -861,7 +859,7 @@ def _provision_sandbox(
host_config_kwargs = self._base_host_config_kwargs(
mem_limit, nano_cpus, self.network_mode
)
if self.network_mode == BRIDGE_NETWORK_MODE:
if self.network_mode != HOST_NETWORK_MODE:
host_execd_port, host_http_port = self._allocate_distinct_host_ports()
port_bindings = {
"44772": ("0.0.0.0", host_execd_port),
Expand Down Expand Up @@ -1506,7 +1504,8 @@ def get_endpoint(self, sandbox_id: str, port: int, resolve_internal: bool = Fals
if self.network_mode == HOST_NETWORK_MODE:
return Endpoint(endpoint=f"{public_host}:{port}")

if self.network_mode == BRIDGE_NETWORK_MODE:
# For bridge or any user-defined network, use port bindings
if self.network_mode != HOST_NETWORK_MODE:
container = self._get_container_by_sandbox_id(sandbox_id)
labels = container.attrs.get("Config", {}).get("Labels") or {}
execd_host_port = self._parse_host_port_label(
Expand Down