diff --git a/docs/README_zh.md b/docs/README_zh.md index 3afac11f..2ac708a6 100644 --- a/docs/README_zh.md +++ b/docs/README_zh.md @@ -164,6 +164,7 @@ OpenSandbox 提供了丰富的示例来演示不同场景下的沙箱使用方 - **[google-adk](../examples/google-adk/README.md)** - 使用 Google ADK 通过 OpenSandbox 工具读写文件并执行命令。 - **[nullclaw](../examples/nullclaw/README.md)** - 在沙箱中启动 Nullclaw Gateway。 - **[openclaw](../examples/openclaw/README_zh.md)** - 在沙箱中启动 OpenClaw Gateway。 +- **[zeroclaw](../examples/zeroclaw/README.md)** - 在沙箱中启动 ZeroClaw Gateway。 #### 🌐 浏览器与桌面环境 diff --git a/examples/README.md b/examples/README.md index 44780b5e..1eed2d1a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -17,6 +17,7 @@ Examples for common OpenSandbox use cases. Each subdirectory contains runnable c - Google ADK [**google-adk**](google-adk): Google ADK agent calling OpenSandbox tools - 🦞 [**nullclaw**](nullclaw): Launch a Nullclaw Gateway inside a sandbox - 🦞 [**openclaw**](openclaw): Run an OpenClaw Gateway inside a sandbox +- 🦞 [**zeroclaw**](zeroclaw): Run a ZeroClaw Gateway inside a sandbox - 🖥️ [**desktop**](desktop): Launch VNC desktop (Xvfb + x11vnc) for VNC client connections - Playwright [**playwright**](playwright): Launch headless browser (Playwright + Chromium) to scrape web content - VS Code [**vscode**](vscode): Launch code-server (VS Code Web) to provide browser access diff --git a/examples/zeroclaw/Dockerfile b/examples/zeroclaw/Dockerfile new file mode 100644 index 00000000..6e44b678 --- /dev/null +++ b/examples/zeroclaw/Dockerfile @@ -0,0 +1,26 @@ +# Copyright 2026 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM ghcr.io/zeroclaw-labs/zeroclaw:latest AS zeroclaw + +FROM debian:trixie-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends bash ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=zeroclaw /usr/local/bin/zeroclaw /usr/local/bin/zeroclaw + +ENTRYPOINT ["zeroclaw"] +CMD ["gateway"] diff --git a/examples/zeroclaw/README.md b/examples/zeroclaw/README.md new file mode 100644 index 00000000..608d4f6e --- /dev/null +++ b/examples/zeroclaw/README.md @@ -0,0 +1,90 @@ +# ZeroClaw Gateway Example + +Launch a [ZeroClaw](https://github.com/zeroclaw-labs/zeroclaw) Gateway inside an OpenSandbox instance and expose its HTTP endpoint. The script polls the gateway health check until it returns HTTP 200, then prints the reachable endpoint. + +This example uses a thin wrapper image built from the official ZeroClaw image. The wrapper adds the shell/runtime pieces that OpenSandbox's Docker bootstrap path expects, while still using the upstream `zeroclaw` binary. + +## Build the ZeroClaw Sandbox Image + +Build the local wrapper image: + +```shell +cd examples/zeroclaw +./build.sh +``` + +By default, this builds `opensandbox/zeroclaw:latest`. You can override the image name with `IMAGE=...` or `TAG=...`. + +## Start OpenSandbox server [local] + +The wrapper image copies the official ZeroClaw binary from `ghcr.io/zeroclaw-labs/zeroclaw:latest`. + +### Notes (Docker runtime requirement) + +The server uses `runtime.type = "docker"` by default, so it **must** be able to reach a running Docker daemon. + +- **Docker Desktop**: ensure Docker Desktop is running, then verify with `docker version`. +- **Colima (macOS)**: start it first (`colima start`) and export the socket before starting the server: + +```shell +export DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock" +``` + +Pre-pull the upstream ZeroClaw image if you want to warm the cache before building: + +```shell +docker pull ghcr.io/zeroclaw-labs/zeroclaw:latest +``` + +Start the OpenSandbox server (logs will stay in the terminal): + +```shell +uv pip install opensandbox-server +opensandbox-server init-config ~/.sandbox.toml --example docker +opensandbox-server +``` + +If you see errors like `FileNotFoundError: [Errno 2] No such file or directory` from `docker/transport/unixconn.py`, it usually means the Docker unix socket is missing or Docker is not running. + +## Create and Access the ZeroClaw Sandbox + +This example is hard-coded for a quick start: +- OpenSandbox server: `http://localhost:8080` +- Image: `opensandbox/zeroclaw:latest` +- Gateway port: `42617` +- Timeout: `3600s` +- Command: `zeroclaw gateway --host 0.0.0.0 --port 42617` +- Env: `ZEROCLAW_ALLOW_PUBLIC_BIND=true` + +Install dependencies from the project root: + +```shell +uv pip install opensandbox requests +``` + +Run the example: + +```shell +uv run python examples/zeroclaw/main.py +``` + +Or override the image name: + +```shell +export ZEROCLAW_SANDBOX_IMAGE=opensandbox/zeroclaw:latest +uv run python examples/zeroclaw/main.py +``` + +You should see output similar to: + +```text +Creating zeroclaw sandbox with image=opensandbox/zeroclaw:latest on OpenSandbox server http://localhost:8080... +[check] sandbox ready after 0.8s +Zeroclaw gateway started. Please refer to 127.0.0.1:56234/proxy/42617 +``` + +The endpoint printed at the end (for example, `127.0.0.1:56234/proxy/42617`) is the ZeroClaw Gateway address exposed from the sandbox. The readiness probe in this example is `GET /health`. + +## References +- [ZeroClaw](https://github.com/zeroclaw-labs/zeroclaw) +- [OpenSandbox Python SDK](https://pypi.org/project/opensandbox/) diff --git a/examples/zeroclaw/build.sh b/examples/zeroclaw/build.sh new file mode 100755 index 00000000..8b849992 --- /dev/null +++ b/examples/zeroclaw/build.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Copyright 2026 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -ex + +TAG=${TAG:-latest} +IMAGE=${IMAGE:-opensandbox/zeroclaw:${TAG}} + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +docker build -t "${IMAGE}" "${SCRIPT_DIR}" diff --git a/examples/zeroclaw/main.py b/examples/zeroclaw/main.py new file mode 100644 index 00000000..cdcfc506 --- /dev/null +++ b/examples/zeroclaw/main.py @@ -0,0 +1,73 @@ +# Copyright 2026 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import time +from datetime import timedelta + +import requests +from opensandbox import SandboxSync +from opensandbox.config import ConnectionConfigSync + + +def check_zeroclaw(sbx: SandboxSync) -> bool: + """ + Health check: poll zeroclaw gateway until it returns 200. + + Returns: + True when ready + False on timeout or any exception + """ + try: + endpoint = sbx.get_endpoint(42617) + start = time.perf_counter() + url = f"http://{endpoint.endpoint}/health" + for _ in range(150): # max for ~30s + try: + resp = requests.get(url, timeout=1) + if resp.status_code == 200: + elapsed = time.perf_counter() - start + print(f"[check] sandbox ready after {elapsed:.1f}s") + return True + except Exception: + pass + time.sleep(0.2) + return False + except Exception as exc: + print(f"[check] failed: {exc}") + return False + + +def main() -> None: + server = "http://localhost:8080" + image = os.getenv("ZEROCLAW_SANDBOX_IMAGE", "opensandbox/zeroclaw:latest") + timeout_seconds = 3600 # 1 hour + + print(f"Creating zeroclaw sandbox with image={image} on OpenSandbox server {server}...") + sandbox = SandboxSync.create( + image=image, + timeout=timedelta(seconds=timeout_seconds), + metadata={"example": "zeroclaw"}, + entrypoint=["zeroclaw", "gateway", "--host", "0.0.0.0", "--port", "42617"], + connection_config=ConnectionConfigSync(domain=server), + health_check=check_zeroclaw, + env={"ZEROCLAW_ALLOW_PUBLIC_BIND": "true"}, + ) + + endpoint = sandbox.get_endpoint(42617) + print(f"Zeroclaw gateway started. Please refer to {endpoint.endpoint}") + + +if __name__ == "__main__": + main()