From c3c57931f9456d6446896a9c2c3ba59f5d940931 Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Wed, 11 Mar 2026 05:08:02 +0800 Subject: [PATCH 1/2] feat(openclaw): enhance example with configuration options and better defaults - Add environment variable support for server, image, timeout, token, port - Expand network policy to allow GitHub API access - Improve README with configuration tables and advanced usage - Add custom gateway port documentation - Add token truncation in log output for security --- examples/openclaw/README.md | 62 +++++++++++++++++++++++++++++++++++++ examples/openclaw/main.py | 42 +++++++++++++++++++------ 2 files changed, 94 insertions(+), 10 deletions(-) diff --git a/examples/openclaw/README.md b/examples/openclaw/README.md index bcafec73..09f773e0 100644 --- a/examples/openclaw/README.md +++ b/examples/openclaw/README.md @@ -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). @@ -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/) diff --git a/examples/openclaw/main.py b/examples/openclaw/main.py index 8ea664cc..52ab8b69 100644 --- a/examples/openclaw/main.py +++ b/examples/openclaw/main.py @@ -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 @@ -51,19 +63,24 @@ 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 @@ -71,11 +88,16 @@ def main() -> None: # 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__": From 6a9350ed79fb06fee9013eac7aa0f2c8250a9adc Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Wed, 11 Mar 2026 13:22:14 +0800 Subject: [PATCH 2/2] feat: support user-defined Docker networks - Change network_mode from Literal['host', 'bridge'] to str to allow user-defined network names - Remove startup-time validation that blocked custom networks - Update port-mapping logic to handle any non-host network mode - Update endpoint resolution to work with user-defined networks This enables OpenSandbox to work in Docker Compose setups with shared user-defined networks (e.g., app-net), allowing sandbox containers to communicate with other services like databases by hostname. --- server/src/config.py | 4 ++-- server/src/services/docker.py | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/server/src/config.py b/server/src/config.py index e1c1b088..ef4d6810 100644 --- a/server/src/config.py +++ b/server/src/config.py @@ -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, diff --git a/server/src/services/docker.py b/server/src/services/docker.py index 69015208..18f40cd3 100644 --- a/server/src/services/docker.py +++ b/server/src/services/docker.py @@ -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: @@ -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), @@ -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(