From a8cc6c011baa2cb220f606d8726af0dd582eca82 Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Mon, 18 May 2026 12:41:54 +0100 Subject: [PATCH] docs(container-gateway): fix Docker driver setup for containerized gateway The existing docs omitted or misstated several requirements when running the gateway as a container with the Docker compute driver: 1. OPENSHELL_GRPC_ENDPOINT is required. The Docker driver rejects startup if this env var is missing, but it was not mentioned. 2. The supervisor binary must be extracted to a host path before starting the gateway. The gateway validates the path at startup from inside the container, and the host Docker daemon uses the same path as a bind-mount source when creating sandbox containers. Extracting to a path inside the gateway container alone is insufficient. 3. Docker socket access requires adding the docker group. The gateway image runs as nvs:nvs (UID 1000) which does not have access to the Docker socket by default. 4. Port binding should remain 127.0.0.1. The Docker driver automatically binds the gateway to the bridge network interface (gateway_bind_addresses in the driver) so sandbox containers can reach it without exposing the port on 0.0.0.0. 5. The mTLS setup section was missing --server-san host.openshell.internal on generate-certs. Sandbox containers resolve host.openshell.internal to reach the gateway, so this SAN must be present in the server cert. The mTLS docker run was also missing --group-add docker, the supervisor binary mount, OPENSHELL_GRPC_ENDPOINT, and OPENSHELL_DOCKER_SUPERVISOR_BIN. Validated by deploying OpenShell on a Fedora Kinoite (bootc) system using the updated compose.yml. --- deploy/docker/docker-compose.yml | 137 ++++++++++++ deploy/docker/gateway.toml | 59 +++++ docs/about/container-gateway.mdx | 88 +++++++- docs/get-started/tutorials/docker-compose.mdx | 204 ++++++++++++++++++ docs/get-started/tutorials/index.mdx | 5 + 5 files changed, 487 insertions(+), 6 deletions(-) create mode 100644 deploy/docker/docker-compose.yml create mode 100644 deploy/docker/gateway.toml create mode 100644 docs/get-started/tutorials/docker-compose.mdx diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml new file mode 100644 index 000000000..5c25e7980 --- /dev/null +++ b/deploy/docker/docker-compose.yml @@ -0,0 +1,137 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# OpenShell gateway — docker-compose setup (Docker compute driver) +# +# Prerequisites: +# - Docker Desktop (Windows / macOS) or Docker Engine + Compose plugin (Linux) +# - The openshell CLI installed on your workstation +# +# Quick start: +# +# 1. Start the gateway: +# docker compose up -d +# +# 2. Register the gateway with the CLI (one-time): +# openshell gateway add openshell-docker \ +# --endpoint http://localhost:18080 \ +# --no-tls +# +# 3. Configure an AI provider (example: Anthropic): +# ANTHROPIC_API_KEY=sk-ant-... \ +# openshell provider create --type anthropic --from-existing +# +# 4. Create a sandboxed agent — Claude Code or OpenClaw: +# openshell sandbox create -- claude +# openshell sandbox create --from openclaw +# +# Sandbox containers are managed by the gateway, not by this Compose file. +# Each `openshell sandbox create` call launches a fresh container; the gateway +# tracks their lifecycle. +# +# Configuration: +# All gateway settings below use OPENSHELL_* environment variables. +# The gateway binary reads these directly, so no separate config file +# is required. See `docker run --rm ghcr.io/nvidia/openshell/gateway:latest +# --help` for the full list of supported variables. +# +# The dev image (ghcr.io/nvidia/openshell/gateway:dev) also supports a +# TOML configuration file via --config / OPENSHELL_GATEWAY_CONFIG. +# See gateway.toml in this directory for an equivalent TOML reference. +# +# Data directory note: +# /var/lib/openshell is bind-mounted at the SAME absolute path in both the +# host and the container. This is required so that the supervisor binary +# extracted from the supervisor image can be passed to Docker as a host-side +# bind-mount source when sandbox containers are created. Named volumes +# cannot be used here because Docker resolves bind-mount sources against the +# host filesystem, not the container filesystem. +# +# Linux note: +# host.docker.internal is not automatically added on Linux Docker. +# Add the following under the gateway service to enable it: +# extra_hosts: +# - "host.docker.internal:host-gateway" + +services: + gateway: + image: ghcr.io/nvidia/openshell/gateway:${IMAGE_TAG:-latest} + restart: unless-stopped + + # This setup is Docker-outside-of-Docker (DooD), not Docker-in-Docker (DinD). + # The gateway uses the host's Docker socket to create sibling containers on the + # host, rather than running a nested Docker daemon. DooD does NOT require + # --privileged; it only needs read/write access to /var/run/docker.sock. + # + # Run as UID 0 so the gateway can: + # - write the extracted supervisor binary to /var/lib/openshell + # - access /var/run/docker.sock (typically owned by root or the docker group) + # Distroless images have no /etc/passwd, so the numeric UID must be used. + # This is appropriate for local development. Production deployments + # should use a dedicated non-root UID with explicit docker-group membership. + user: "0" + + ports: + # gRPC / control-plane API (used by the openshell CLI and sandbox callbacks) + # The Docker driver injects host.openshell.internal: into sandbox + # containers as the callback endpoint. The gateway's internal port is 8080, so + # host port 8080 must be published at the same number so that + # host.openshell.internal:8080 routes to the gateway container. + - "${OPENSHELL_PORT:-8080}:8080" + # Health endpoint (GET /healthz, GET /readyz) + - "${OPENSHELL_HEALTH_PORT:-8081}:8081" + + volumes: + # Docker socket — lets the gateway create and manage sandbox containers. + - /var/run/docker.sock:/var/run/docker.sock + + # Data directory — must be a bind-mount with source == target so that + # paths written inside the container are resolvable by Docker when it + # creates sandbox containers (see note above). + # /var/lib/openshell is intentionally not namespaced to a sub-path + # (e.g. /var/lib/openshell/gateway): the path must match exactly on + # both the host and inside the container, and a single gateway per host + # is the expected topology. + - type: bind + source: /var/lib/openshell + target: /var/lib/openshell + bind: + create_host_path: true + + environment: + # ── Listener ─────────────────────────────────────────────────────────── + # bind-address is already set to 0.0.0.0 by the image's default CMD; + # these vars configure the remaining listener ports. + OPENSHELL_HEALTH_PORT: "8081" + + # ── Auth / TLS ────────────────────────────────────────────────────────── + OPENSHELL_DISABLE_TLS: "true" + + # ── Compute driver ───────────────────────────────────────────────────── + OPENSHELL_DRIVERS: "docker" + + # ── Sandbox defaults ──────────────────────────────────────────────────── + # Default image used when --from is not specified. + OPENSHELL_SANDBOX_IMAGE: "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" + + # ── Docker driver ─────────────────────────────────────────────────────── + # Supervisor image used to extract the openshell-sandbox binary on first + # start. The binary is cached in /var/lib/openshell and reused on + # subsequent starts. + OPENSHELL_DOCKER_SUPERVISOR_IMAGE: "ghcr.io/nvidia/openshell/supervisor:latest" + + # Address sandbox containers use to call back to this gateway. + # The Docker driver always substitutes host.openshell.internal and the + # gateway bind port into this URL, so only the scheme (http/https) is + # meaningful here. The gateway must be published on the same port number + # on the Docker host (port 8080 by default — see ports above). + OPENSHELL_GRPC_ENDPOINT: "http://host.openshell.internal:8080" + + # ── Persistence ──────────────────────────────────────────────────────── + OPENSHELL_DB_URL: "sqlite:/var/lib/openshell/gateway.db?mode=rwc" + + # ── XDG paths ────────────────────────────────────────────────────────── + # Point XDG data home at the bind-mounted data directory so the + # extracted supervisor binary lands at a host-resolvable path. + XDG_DATA_HOME: /var/lib/openshell + HOME: /var/lib/openshell diff --git a/deploy/docker/gateway.toml b/deploy/docker/gateway.toml new file mode 100644 index 000000000..e77e7b250 --- /dev/null +++ b/deploy/docker/gateway.toml @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# OpenShell gateway TOML configuration — Docker compute driver reference. +# +# This file is the TOML equivalent of the OPENSHELL_* environment variables +# set in docker-compose.yml. It requires the dev image or a release that +# includes RFC 0003 config-file support (post-0.0.42). +# +# To use this file with docker-compose, switch the image to the dev tag and +# replace the environment section with a config file mount: +# +# services: +# gateway: +# image: ghcr.io/nvidia/openshell/gateway:dev +# volumes: +# - type: bind +# source: ./gateway.toml +# target: /etc/openshell/gateway.toml +# read_only: true +# environment: +# OPENSHELL_GATEWAY_CONFIG: /etc/openshell/gateway.toml +# XDG_DATA_HOME: /var/lib/openshell +# HOME: /var/lib/openshell +# command: [] # clear the default CMD; config drives everything +# +# grpc_endpoint note: +# host.docker.internal is automatically resolvable from containers on +# Docker Desktop (Windows / macOS). On Linux, replace with the host IP +# or add extra_hosts: ["host.docker.internal:host-gateway"] to compose. + +[openshell] +version = 1 + +[openshell.gateway] +# Bind to loopback only. The Docker driver adds an extra listener on the +# bridge interface automatically so sandbox containers can reach the gateway. +bind_address = "127.0.0.1:8080" +health_bind_address = "127.0.0.1:8081" +log_level = "info" +compute_drivers = ["docker"] +disable_tls = true + +[openshell.drivers.docker] +# Default image pulled for `openshell sandbox create` without --from. +default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" +# Supervisor image from which the openshell-sandbox binary is extracted on +# first start. The binary is cached to XDG_DATA_HOME and reused on restart. +supervisor_image = "ghcr.io/nvidia/openshell/supervisor:latest" +# Only pull images that are not already cached locally. +image_pull_policy = "IfNotPresent" +# Prefix applied to sandbox container names. +sandbox_namespace = "openshell" +# Address sandbox containers use to call back to the gateway. +# The Docker driver replaces the host with host.openshell.internal and the +# port with the gateway's own bind port (8080). Only the scheme survives. +# The gateway must be published on port 8080 on the Docker host so that +# host.openshell.internal:8080 resolves to the gateway container. +grpc_endpoint = "http://host.openshell.internal:8080" diff --git a/docs/about/container-gateway.mdx b/docs/about/container-gateway.mdx index 4413f388e..2ccb189db 100644 --- a/docs/about/container-gateway.mdx +++ b/docs/about/container-gateway.mdx @@ -12,29 +12,69 @@ Use this approach when you want to run the OpenShell gateway as a container inst The gateway image is published at `ghcr.io/nvidia/openshell/gateway`. +## Prerequisites for the Docker Driver + +When the gateway runs as a container and creates Docker-backed sandboxes, the gateway container +communicates with the host Docker daemon via the mounted socket. This requires three things beyond +a basic `docker run`: + +1. **Docker socket access.** The gateway process must be able to read and write the Docker socket. + Add the `docker` group (or the GID of `/var/run/docker.sock`) so the socket is accessible + without running as root. + +2. **gRPC endpoint.** Sandbox containers call back to the gateway over the `OPENSHELL_GRPC_ENDPOINT` + address. Set this to `http://127.0.0.1:8080` so the driver knows the scheme and port. The + docker driver automatically binds the gateway to the bridge network interface so sandbox + containers can reach it — you do not need to expose the port on `0.0.0.0`. + +3. **Supervisor binary on the host.** The gateway bind-mounts the `openshell-sandbox` supervisor + binary into each sandbox container. Because bind-mount paths are resolved by the host Docker + daemon (not inside the gateway container), the binary must exist at a path on the host + filesystem. Extract it before starting the gateway and mount it at the same path. + ## Quick Start -This example runs the gateway locally with TLS disabled. It is suitable for development on a single machine. Binding to `127.0.0.1` prevents remote access without authentication. +Extract the supervisor binary to the host once, then start the gateway: + +```shell +mkdir -p ~/openshell/supervisor +docker create --name tmp-supervisor ghcr.io/nvidia/openshell/supervisor:latest +docker cp tmp-supervisor:/openshell-sandbox ~/openshell/supervisor/openshell-sandbox +docker rm tmp-supervisor +chmod +x ~/openshell/supervisor/openshell-sandbox +``` + +Start the gateway: ```shell docker run -d \ --name openshell-gateway \ --restart unless-stopped \ + --group-add docker \ -p 127.0.0.1:8080:8080 \ -v openshell-state:/var/openshell \ -v /var/run/docker.sock:/var/run/docker.sock \ + -v ~/openshell/supervisor/openshell-sandbox:~/openshell/supervisor/openshell-sandbox:ro \ -e OPENSHELL_DRIVERS=docker \ + -e OPENSHELL_GRPC_ENDPOINT=http://127.0.0.1:8080 \ + -e OPENSHELL_DOCKER_SUPERVISOR_BIN=~/openshell/supervisor/openshell-sandbox \ -e OPENSHELL_DB_URL=sqlite:/var/openshell/openshell.db \ -e OPENSHELL_DISABLE_TLS=true \ ghcr.io/nvidia/openshell/gateway:latest ``` -Register the gateway with the CLI: +Register the gateway with the CLI. If running on the same machine, use `--local`: ```shell openshell gateway add http://127.0.0.1:8080 --local --name local ``` +If registering from a different machine on the same network, use the host IP and `--remote`: + +```shell +openshell gateway add http://HOST_IP:8080 --remote --name remote +``` + Confirm the CLI can reach the gateway: ```shell @@ -42,7 +82,9 @@ openshell status ``` -Disabling TLS removes authentication. Binding to `127.0.0.1` limits access to the local machine. If you expose the port on `0.0.0.0`, enable mTLS to prevent unauthenticated access. +Disabling TLS removes authentication. This example binds to `127.0.0.1` so only local +connections are accepted. To accept remote connections, enable mTLS or restrict access with +a firewall rule. ## Full mTLS Setup @@ -58,7 +100,9 @@ docker run --rm \ -v "$HOME/.local/state/openshell:/home/openshell/.local/state/openshell" \ -v "$HOME/.config/openshell:/home/openshell/.config/openshell" \ ghcr.io/nvidia/openshell/gateway:latest \ - generate-certs --output-dir /home/openshell/.local/state/openshell/tls + generate-certs \ + --output-dir /home/openshell/.local/state/openshell/tls \ + --server-san host.openshell.internal ``` This writes the server and client certificates under `~/.local/state/openshell/tls/` and copies the client bundle to `~/.config/openshell/gateways/openshell/mtls/` so the CLI picks it up automatically. @@ -69,10 +113,14 @@ Start the gateway with mTLS enabled: docker run -d \ --name openshell-gateway \ --restart unless-stopped \ + --group-add docker \ -p 127.0.0.1:8080:8080 \ -v "$HOME/.local/state/openshell:/home/openshell/.local/state/openshell" \ -v /var/run/docker.sock:/var/run/docker.sock \ + -v ~/openshell/supervisor/openshell-sandbox:~/openshell/supervisor/openshell-sandbox:ro \ -e OPENSHELL_DRIVERS=docker \ + -e OPENSHELL_GRPC_ENDPOINT=https://127.0.0.1:8080 \ + -e OPENSHELL_DOCKER_SUPERVISOR_BIN=~/openshell/supervisor/openshell-sandbox \ -e OPENSHELL_DB_URL=sqlite:/home/openshell/.local/state/openshell/openshell.db \ -e OPENSHELL_TLS_CERT=/home/openshell/.local/state/openshell/tls/server/tls.crt \ -e OPENSHELL_TLS_KEY=/home/openshell/.local/state/openshell/tls/server/tls.key \ @@ -91,20 +139,41 @@ openshell gateway add https://127.0.0.1:8080 --local --name local ## Docker Compose -Save the following as `compose.yml`. This uses the TLS-disabled configuration bound to localhost, suitable for local development. +The following `compose.yml` runs the gateway with the Docker driver on an immutable OS or any +Docker host. It includes all required configuration for sandbox containers to call back to the +gateway. + +Before starting, extract the supervisor binary to a host path. The path must be the same on +both the host and inside the gateway container because the host Docker daemon uses it as a +bind-mount source when creating sandbox containers: + +```shell +mkdir -p ~/openshell/supervisor +docker create --name tmp-supervisor ghcr.io/nvidia/openshell/supervisor:latest +docker cp tmp-supervisor:/openshell-sandbox ~/openshell/supervisor/openshell-sandbox +docker rm tmp-supervisor +chmod +x ~/openshell/supervisor/openshell-sandbox +``` + +Save the following as `~/openshell/compose.yml`, substituting your home directory for `HOME`: ```yaml services: gateway: image: ghcr.io/nvidia/openshell/gateway:latest restart: unless-stopped + group_add: + - docker ports: - "127.0.0.1:8080:8080" volumes: - openshell-state:/var/openshell - /var/run/docker.sock:/var/run/docker.sock + - HOME/openshell/supervisor/openshell-sandbox:HOME/openshell/supervisor/openshell-sandbox:ro environment: OPENSHELL_DRIVERS: docker + OPENSHELL_GRPC_ENDPOINT: "http://127.0.0.1:8080" + OPENSHELL_DOCKER_SUPERVISOR_BIN: "HOME/openshell/supervisor/openshell-sandbox" OPENSHELL_DB_URL: "sqlite:/var/openshell/openshell.db" OPENSHELL_DISABLE_TLS: "true" @@ -118,12 +187,19 @@ Start the gateway: docker compose up -d ``` -Register the gateway with the CLI: +Register the gateway with the CLI. If registering from the same machine: ```shell openshell gateway add http://127.0.0.1:8080 --local --name local ``` +If registering from a different machine on the same network, replace `HOST_IP` with the +machine's LAN address: + +```shell +openshell gateway add http://HOST_IP:8080 --remote --name remote +``` + ## Using Podman Replace `docker` with `podman` in the commands above. Mount the Podman socket instead of the Docker socket and set the driver to `podman`: diff --git a/docs/get-started/tutorials/docker-compose.mdx b/docs/get-started/tutorials/docker-compose.mdx new file mode 100644 index 000000000..db836b44a --- /dev/null +++ b/docs/get-started/tutorials/docker-compose.mdx @@ -0,0 +1,204 @@ +--- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +title: "Run the Gateway with Docker Compose" +sidebar-title: "Docker Compose Setup" +slug: "get-started/tutorials/docker-compose" +description: "Run the OpenShell gateway as a Docker Compose service and create agent sandboxes including OpenClaw." +keywords: "Generative AI, Docker Compose, Gateway, Sandbox, OpenClaw, Docker, Installation" +--- + +This tutorial shows how to run the OpenShell gateway as a Docker Compose service on a Linux host or on a machine running Docker Desktop (Windows or macOS). + +After completing this tutorial you have: + +- An OpenShell gateway running as a Compose service. +- The `openshell` CLI registered against that gateway. +- An AI provider configured with your API key. +- A running OpenClaw sandbox. + +## Prerequisites + +- Docker Desktop (Windows or macOS) or Docker Engine with the Compose plugin (Linux). +- The `openshell` CLI installed on your workstation. See [Install the CLI](#install-the-cli) below. +- Port 8080 available on the host. + +## Compose files + +The Compose configuration lives at [`deploy/docker/`](https://github.com/NVIDIA/OpenShell/tree/main/deploy/docker) in the repository. + +| File | Purpose | +|---|---| +| `docker-compose.yml` | Gateway service, volumes, and environment variables | +| `gateway.toml` | TOML reference for release builds with config-file support | + +## Port note + +The Docker compute driver injects `host.openshell.internal:` into every sandbox container as its callback address. The gateway listens on port 8080 inside the container, so **port 8080 must be published at the same number on the Docker host**. Publishing it as a different host port (for example `18080:8080`) causes sandbox containers to call back to the wrong port and remain stuck in the `Provisioning` phase. + +If port 8080 is taken, change `OPENSHELL_SERVER_PORT` and update the port mapping to `:8080`, then set `OPENSHELL_PORT=` in an `.env` file. + +## Data directory + +The gateway extracts the `openshell-sandbox` supervisor binary from `ghcr.io/nvidia/openshell/supervisor:latest` on first start and caches it at: + +``` +/var/lib/openshell/openshell/docker-supervisor//openshell-sandbox +``` + +This path is used as a bind-mount source when Docker creates sandbox containers. +Docker resolves bind-mount sources against the **host filesystem**, not the container filesystem, so the data directory must be bind-mounted at the **same absolute path** in both the host and the container. + +The Compose file uses `/var/lib/openshell` for this purpose and sets `create_host_path: true` so Docker creates it on first run. + +## Start the gateway + +```shell +cd deploy/docker +docker compose up -d +``` + +Verify the gateway is healthy: + +```shell +curl -sf http://localhost:8080/healthz +``` + +## Install the CLI + +**Binary (recommended — macOS / Linux / WSL):** + +```shell +curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh +``` + +**From PyPI (any platform with [uv](https://docs.astral.sh/uv/)):** + +```shell +uv tool install -U openshell +``` + + +On Windows without WSL, install the CLI inside a WSL 2 distribution (for example AlmaLinux or Ubuntu) and run all `openshell` commands from that distribution. + + +## Register the gateway + +Run this once after the gateway starts: + +```shell +openshell gateway add http://localhost:8080 --name openshell-docker +``` + +Verify the connection: + +```shell +openshell status +``` + +The output should show `Status: Connected`. + +## Configure an AI provider + +Set your API key as an environment variable and create a provider: + + + + +```shell +ANTHROPIC_API_KEY=sk-ant-... \ + openshell provider create --name anthropic --type anthropic --from-existing +``` + + + + +```shell +OPENAI_API_KEY=sk-... \ + openshell provider create --name openai --type openai --from-existing +``` + + + + +Confirm the provider was stored: + +```shell +openshell provider list +``` + +## Pre-pull sandbox images (optional) + +Sandbox images are pulled automatically on first use, but the initial pull can take several minutes for large images. Pre-pull to avoid long waits at sandbox creation time: + +```shell +# Base image — includes Claude Code, OpenCode, Codex, and Copilot +docker pull ghcr.io/nvidia/openshell-community/sandboxes/base:latest + +# OpenClaw image +docker pull ghcr.io/nvidia/openshell-community/sandboxes/openclaw:latest +``` + +## Create a sandbox + + + + +```shell +openshell sandbox create --from openclaw +``` + +OpenClaw launches directly. The first run pulls the image if it is not cached. + + + + +```shell +openshell sandbox create -- claude +``` + + + + +```shell +openshell sandbox create -- opencode +``` + + + + +Wait for the phase to change from `Provisioning` to `Ready`: + +```shell +openshell sandbox list +``` + +Then connect: + +```shell +openshell sandbox connect +``` + +## Manage the gateway + +| Command | Purpose | +|---|---| +| `docker compose up -d` | Start or restart the gateway | +| `docker compose down` | Stop the gateway and remove the container | +| `docker compose logs -f` | Tail gateway logs | +| `docker compose pull` | Pull a new gateway image version | + +## Linux notes + +On Linux, `host.docker.internal` and `host.openshell.internal` are not automatically resolvable from containers. Add the following under the `gateway` service in `docker-compose.yml`: + +```yaml +extra_hosts: + - "host.docker.internal:host-gateway" + - "host.openshell.internal:host-gateway" +``` + +## Next steps + +- [First Network Policy](/get-started/tutorials/first-network-policy) — apply L7 policies to your sandbox. +- [GitHub Push Access](/get-started/tutorials/github-sandbox) — grant a sandbox scoped GitHub access. diff --git a/docs/get-started/tutorials/index.mdx b/docs/get-started/tutorials/index.mdx index c03e924f7..c6e6a3f60 100644 --- a/docs/get-started/tutorials/index.mdx +++ b/docs/get-started/tutorials/index.mdx @@ -12,6 +12,11 @@ Hands-on walkthroughs that teach OpenShell concepts by building real configurati + + +Run the OpenShell gateway as a Docker Compose service and create agent sandboxes including OpenClaw. + + Create a sandbox, observe default-deny networking, apply a read-only L7 policy, and inspect audit logs. No AI agent required.