From 3c9beaea67bc3257b4483b24c86a9f254235ed78 Mon Sep 17 00:00:00 2001 From: Derin Works Claw Bot Date: Sun, 15 Mar 2026 21:45:14 -0400 Subject: [PATCH 1/4] fix: docker cli config set run without depending on gateway --- docker-setup.sh | 628 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 624 insertions(+), 4 deletions(-) diff --git a/docker-setup.sh b/docker-setup.sh index e8d6335bf42..ebedb91d40d 100755 --- a/docker-setup.sh +++ b/docker-setup.sh @@ -2,11 +2,631 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SCRIPT_PATH="$ROOT_DIR/scripts/docker/setup.sh" +COMPOSE_FILE="$ROOT_DIR/docker-compose.yml" +EXTRA_COMPOSE_FILE="$ROOT_DIR/docker-compose.extra.yml" +IMAGE_NAME="${OPENCLAW_IMAGE:-openclaw:local}" +EXTRA_MOUNTS="${OPENCLAW_EXTRA_MOUNTS:-}" +HOME_VOLUME_NAME="${OPENCLAW_HOME_VOLUME:-}" +RAW_SANDBOX_SETTING="${OPENCLAW_SANDBOX:-}" +SANDBOX_ENABLED="" +DOCKER_SOCKET_PATH="${OPENCLAW_DOCKER_SOCKET:-}" +TIMEZONE="${OPENCLAW_TZ:-}" -if [[ ! -f "$SCRIPT_PATH" ]]; then - echo "Docker setup script not found at $SCRIPT_PATH" >&2 +fail() { + echo "ERROR: $*" >&2 + exit 1 +} + +require_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Missing dependency: $1" >&2 + exit 1 + fi +} + +is_truthy_value() { + local raw="${1:-}" + raw="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')" + case "$raw" in + 1 | true | yes | on) return 0 ;; + *) return 1 ;; + esac +} + +read_config_gateway_token() { + local config_path="$OPENCLAW_CONFIG_DIR/openclaw.json" + if [[ ! -f "$config_path" ]]; then + return 0 + fi + if command -v python3 >/dev/null 2>&1; then + python3 - "$config_path" <<'PY' +import json +import sys + +path = sys.argv[1] +try: + with open(path, "r", encoding="utf-8") as f: + cfg = json.load(f) +except Exception: + raise SystemExit(0) + +gateway = cfg.get("gateway") +if not isinstance(gateway, dict): + raise SystemExit(0) +auth = gateway.get("auth") +if not isinstance(auth, dict): + raise SystemExit(0) +token = auth.get("token") +if isinstance(token, str): + token = token.strip() + if token: + print(token) +PY + return 0 + fi + if command -v node >/dev/null 2>&1; then + node - "$config_path" <<'NODE' +const fs = require("node:fs"); +const configPath = process.argv[2]; +try { + const cfg = JSON.parse(fs.readFileSync(configPath, "utf8")); + const token = cfg?.gateway?.auth?.token; + if (typeof token === "string" && token.trim().length > 0) { + process.stdout.write(token.trim()); + } +} catch { + // Keep docker-setup resilient when config parsing fails. +} +NODE + fi +} + +read_env_gateway_token() { + local env_path="$1" + local line="" + local token="" + if [[ ! -f "$env_path" ]]; then + return 0 + fi + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line%$'\r'}" + if [[ "$line" == OPENCLAW_GATEWAY_TOKEN=* ]]; then + token="${line#OPENCLAW_GATEWAY_TOKEN=}" + fi + done <"$env_path" + if [[ -n "$token" ]]; then + printf '%s' "$token" + fi +} + +ensure_control_ui_allowed_origins() { + if [[ "${OPENCLAW_GATEWAY_BIND}" == "loopback" ]]; then + return 0 + fi + + local allowed_origin_json + local current_allowed_origins + allowed_origin_json="$(printf '["http://127.0.0.1:%s"]' "$OPENCLAW_GATEWAY_PORT")" + current_allowed_origins="$( + docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \ + config get gateway.controlUi.allowedOrigins 2>/dev/null || true + )" + current_allowed_origins="${current_allowed_origins//$'\r'/}" + + if [[ -n "$current_allowed_origins" && "$current_allowed_origins" != "null" && "$current_allowed_origins" != "[]" ]]; then + echo "Control UI allowlist already configured; leaving gateway.controlUi.allowedOrigins unchanged." + return 0 + fi + + docker compose "${COMPOSE_ARGS[@]}" -f <(cat <<'YAML' +services: + openclaw-cli: + network_mode: bridge +YAML +) run --rm --no-deps openclaw-cli \ + config set gateway.controlUi.allowedOrigins "$allowed_origin_json" --strict-json >/dev/null + echo "Set gateway.controlUi.allowedOrigins to $allowed_origin_json for non-loopback bind." +} + +sync_gateway_mode_and_bind() { + docker compose "${COMPOSE_ARGS[@]}" -f <(cat <<'YAML' +services: + openclaw-cli: + network_mode: bridge +YAML +) run --rm --no-deps openclaw-cli \ + config set gateway.mode local >/dev/null + + docker compose "${COMPOSE_ARGS[@]}" -f <(cat <<'YAML' +services: + openclaw-cli: + network_mode: bridge +YAML +) run --rm --no-deps openclaw-cli \ + config set gateway.bind "$OPENCLAW_GATEWAY_BIND" >/dev/null + echo "Pinned gateway.mode=local and gateway.bind=$OPENCLAW_GATEWAY_BIND for Docker setup." +} + +contains_disallowed_chars() { + local value="$1" + [[ "$value" == *$'\n'* || "$value" == *$'\r'* || "$value" == *$'\t'* ]] +} + +is_valid_timezone() { + local value="$1" + [[ -e "/usr/share/zoneinfo/$value" && ! -d "/usr/share/zoneinfo/$value" ]] +} + +validate_mount_path_value() { + local label="$1" + local value="$2" + if [[ -z "$value" ]]; then + fail "$label cannot be empty." + fi + if contains_disallowed_chars "$value"; then + fail "$label contains unsupported control characters." + fi + if [[ "$value" =~ [[:space:]] ]]; then + fail "$label cannot contain whitespace." + fi +} + +validate_named_volume() { + local value="$1" + if [[ ! "$value" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]]; then + fail "OPENCLAW_HOME_VOLUME must match [A-Za-z0-9][A-Za-z0-9_.-]* when using a named volume." + fi +} + +validate_mount_spec() { + local mount="$1" + if contains_disallowed_chars "$mount"; then + fail "OPENCLAW_EXTRA_MOUNTS entries cannot contain control characters." + fi + # Keep mount specs strict to avoid YAML structure injection. + # Expected format: source:target[:options] + if [[ ! "$mount" =~ ^[^[:space:],:]+:[^[:space:],:]+(:[^[:space:],:]+)?$ ]]; then + fail "Invalid mount format '$mount'. Expected source:target[:options] without spaces." + fi +} + +require_cmd docker +if ! docker compose version >/dev/null 2>&1; then + echo "Docker Compose not available (try: docker compose version)" >&2 exit 1 fi -exec "$SCRIPT_PATH" "$@" +if [[ -z "$DOCKER_SOCKET_PATH" && "${DOCKER_HOST:-}" == unix://* ]]; then + DOCKER_SOCKET_PATH="${DOCKER_HOST#unix://}" +fi +if [[ -z "$DOCKER_SOCKET_PATH" ]]; then + DOCKER_SOCKET_PATH="/var/run/docker.sock" +fi +if is_truthy_value "$RAW_SANDBOX_SETTING"; then + SANDBOX_ENABLED="1" +fi + +OPENCLAW_CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}" +OPENCLAW_WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$HOME/.openclaw/workspace}" + +validate_mount_path_value "OPENCLAW_CONFIG_DIR" "$OPENCLAW_CONFIG_DIR" +validate_mount_path_value "OPENCLAW_WORKSPACE_DIR" "$OPENCLAW_WORKSPACE_DIR" +if [[ -n "$HOME_VOLUME_NAME" ]]; then + if [[ "$HOME_VOLUME_NAME" == *"/"* ]]; then + validate_mount_path_value "OPENCLAW_HOME_VOLUME" "$HOME_VOLUME_NAME" + else + validate_named_volume "$HOME_VOLUME_NAME" + fi +fi +if contains_disallowed_chars "$EXTRA_MOUNTS"; then + fail "OPENCLAW_EXTRA_MOUNTS cannot contain control characters." +fi +if [[ -n "$SANDBOX_ENABLED" ]]; then + validate_mount_path_value "OPENCLAW_DOCKER_SOCKET" "$DOCKER_SOCKET_PATH" +fi +if [[ -n "$TIMEZONE" ]]; then + if contains_disallowed_chars "$TIMEZONE"; then + fail "OPENCLAW_TZ contains unsupported control characters." + fi + if [[ ! "$TIMEZONE" =~ ^[A-Za-z0-9/_+\-]+$ ]]; then + fail "OPENCLAW_TZ must be a valid IANA timezone string (e.g. Asia/Shanghai)." + fi + if ! is_valid_timezone "$TIMEZONE"; then + fail "OPENCLAW_TZ must match a timezone in /usr/share/zoneinfo (e.g. Asia/Shanghai)." + fi +fi + +mkdir -p "$OPENCLAW_CONFIG_DIR" +mkdir -p "$OPENCLAW_WORKSPACE_DIR" +# Seed directory tree eagerly so bind mounts work even on Docker Desktop/Windows +# where the container (even as root) cannot create new host subdirectories. +mkdir -p "$OPENCLAW_CONFIG_DIR/identity" +mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/agent" +mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/sessions" + +export OPENCLAW_CONFIG_DIR +export OPENCLAW_WORKSPACE_DIR +export OPENCLAW_GATEWAY_PORT="${OPENCLAW_GATEWAY_PORT:-18789}" +export OPENCLAW_BRIDGE_PORT="${OPENCLAW_BRIDGE_PORT:-18790}" +export OPENCLAW_GATEWAY_BIND="${OPENCLAW_GATEWAY_BIND:-lan}" +export OPENCLAW_IMAGE="$IMAGE_NAME" +export OPENCLAW_DOCKER_APT_PACKAGES="${OPENCLAW_DOCKER_APT_PACKAGES:-}" +export OPENCLAW_EXTENSIONS="${OPENCLAW_EXTENSIONS:-}" +export OPENCLAW_EXTRA_MOUNTS="$EXTRA_MOUNTS" +export OPENCLAW_HOME_VOLUME="$HOME_VOLUME_NAME" +export OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="${OPENCLAW_ALLOW_INSECURE_PRIVATE_WS:-}" +export OPENCLAW_SANDBOX="$SANDBOX_ENABLED" +export OPENCLAW_DOCKER_SOCKET="$DOCKER_SOCKET_PATH" +export OPENCLAW_TZ="$TIMEZONE" + +# Detect Docker socket GID for sandbox group_add. +DOCKER_GID="" +if [[ -n "$SANDBOX_ENABLED" && -S "$DOCKER_SOCKET_PATH" ]]; then + DOCKER_GID="$(stat -c '%g' "$DOCKER_SOCKET_PATH" 2>/dev/null || stat -f '%g' "$DOCKER_SOCKET_PATH" 2>/dev/null || echo "")" +fi +export DOCKER_GID + +if [[ -z "${OPENCLAW_GATEWAY_TOKEN:-}" ]]; then + EXISTING_CONFIG_TOKEN="$(read_config_gateway_token || true)" + if [[ -n "$EXISTING_CONFIG_TOKEN" ]]; then + OPENCLAW_GATEWAY_TOKEN="$EXISTING_CONFIG_TOKEN" + echo "Reusing gateway token from $OPENCLAW_CONFIG_DIR/openclaw.json" + else + DOTENV_GATEWAY_TOKEN="$(read_env_gateway_token "$ROOT_DIR/.env" || true)" + if [[ -n "$DOTENV_GATEWAY_TOKEN" ]]; then + OPENCLAW_GATEWAY_TOKEN="$DOTENV_GATEWAY_TOKEN" + echo "Reusing gateway token from $ROOT_DIR/.env" + elif command -v openssl >/dev/null 2>&1; then + OPENCLAW_GATEWAY_TOKEN="$(openssl rand -hex 32)" + else + OPENCLAW_GATEWAY_TOKEN="$(python3 - <<'PY' +import secrets +print(secrets.token_hex(32)) +PY +)" + fi + fi +fi +export OPENCLAW_GATEWAY_TOKEN + +COMPOSE_FILES=("$COMPOSE_FILE") +COMPOSE_ARGS=() + +write_extra_compose() { + local home_volume="$1" + shift + local mount + local gateway_home_mount + local gateway_config_mount + local gateway_workspace_mount + + cat >"$EXTRA_COMPOSE_FILE" <<'YAML' +services: + openclaw-gateway: + volumes: +YAML + + if [[ -n "$home_volume" ]]; then + gateway_home_mount="${home_volume}:/home/node" + gateway_config_mount="${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw" + gateway_workspace_mount="${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace" + validate_mount_spec "$gateway_home_mount" + validate_mount_spec "$gateway_config_mount" + validate_mount_spec "$gateway_workspace_mount" + printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE" + printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE" + printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE" + fi + + for mount in "$@"; do + validate_mount_spec "$mount" + printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE" + done + + cat >>"$EXTRA_COMPOSE_FILE" <<'YAML' + openclaw-cli: + volumes: +YAML + + if [[ -n "$home_volume" ]]; then + printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE" + printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE" + printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE" + fi + + for mount in "$@"; do + validate_mount_spec "$mount" + printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE" + done + + if [[ -n "$home_volume" && "$home_volume" != *"/"* ]]; then + validate_named_volume "$home_volume" + cat >>"$EXTRA_COMPOSE_FILE" <>"$tmp" + seen="$seen$k " + replaced=true + break + fi + done + if [[ "$replaced" == false ]]; then + printf '%s\n' "$line" >>"$tmp" + fi + done <"$file" + fi + + for k in "${keys[@]}"; do + if [[ "$seen" != *" $k "* ]]; then + printf '%s=%s\n' "$k" "${!k-}" >>"$tmp" + fi + done + + mv "$tmp" "$file" +} + +upsert_env "$ENV_FILE" \ + OPENCLAW_CONFIG_DIR \ + OPENCLAW_WORKSPACE_DIR \ + OPENCLAW_GATEWAY_PORT \ + OPENCLAW_BRIDGE_PORT \ + OPENCLAW_GATEWAY_BIND \ + OPENCLAW_GATEWAY_TOKEN \ + OPENCLAW_IMAGE \ + OPENCLAW_EXTRA_MOUNTS \ + OPENCLAW_HOME_VOLUME \ + OPENCLAW_DOCKER_APT_PACKAGES \ + OPENCLAW_EXTENSIONS \ + OPENCLAW_SANDBOX \ + OPENCLAW_DOCKER_SOCKET \ + DOCKER_GID \ + OPENCLAW_INSTALL_DOCKER_CLI \ + OPENCLAW_ALLOW_INSECURE_PRIVATE_WS \ + OPENCLAW_TZ + +if [[ "$IMAGE_NAME" == "openclaw:local" ]]; then + echo "==> Building Docker image: $IMAGE_NAME" + docker build \ + --build-arg "OPENCLAW_DOCKER_APT_PACKAGES=${OPENCLAW_DOCKER_APT_PACKAGES}" \ + --build-arg "OPENCLAW_EXTENSIONS=${OPENCLAW_EXTENSIONS}" \ + --build-arg "OPENCLAW_INSTALL_DOCKER_CLI=${OPENCLAW_INSTALL_DOCKER_CLI:-}" \ + -t "$IMAGE_NAME" \ + -f "$ROOT_DIR/Dockerfile" \ + "$ROOT_DIR" +else + echo "==> Pulling Docker image: $IMAGE_NAME" + if ! docker pull "$IMAGE_NAME"; then + echo "ERROR: Failed to pull image $IMAGE_NAME. Please check the image name and your access permissions." >&2 + exit 1 + fi +fi + +# Ensure bind-mounted data directories are writable by the container's `node` +# user (uid 1000). Host-created dirs inherit the host user's uid which may +# differ, causing EACCES when the container tries to mkdir/write. +# Running a brief root container to chown is the portable Docker idiom -- +# it works regardless of the host uid and doesn't require host-side root. +echo "" +echo "==> Fixing data-directory permissions" +# Use -xdev to restrict chown to the config-dir mount only — without it, +# the recursive chown would cross into the workspace bind mount and rewrite +# ownership of all user project files on Linux hosts. +# After fixing the config dir, only the OpenClaw metadata subdirectory +# (.openclaw/) inside the workspace gets chowned, not the user's project files. +docker compose "${COMPOSE_ARGS[@]}" run --rm --user root --entrypoint sh openclaw-cli -c \ + 'find /home/node/.openclaw -xdev -exec chown node:node {} +; \ + [ -d /home/node/.openclaw/workspace/.openclaw ] && chown -R node:node /home/node/.openclaw/workspace/.openclaw || true' + +echo "" +echo "==> Onboarding (interactive)" +echo "Docker setup pins Gateway mode to local." +echo "Gateway runtime bind comes from OPENCLAW_GATEWAY_BIND (default: lan)." +echo "Current runtime bind: $OPENCLAW_GATEWAY_BIND" +echo "Gateway token: $OPENCLAW_GATEWAY_TOKEN" +echo "Tailscale exposure: Off (use host-level tailnet/Tailscale setup separately)." +echo "Install Gateway daemon: No (managed by Docker Compose)" +echo "" +docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli onboard --mode local --no-install-daemon + +echo "" +echo "==> Docker gateway defaults" +sync_gateway_mode_and_bind + +echo "" +echo "==> Control UI origin allowlist" +ensure_control_ui_allowed_origins + +echo "" +echo "==> Provider setup (optional)" +echo "WhatsApp (QR):" +echo " ${COMPOSE_HINT} run --rm openclaw-cli channels login" +echo "Telegram (bot token):" +echo " ${COMPOSE_HINT} run --rm openclaw-cli channels add --channel telegram --token " +echo "Discord (bot token):" +echo " ${COMPOSE_HINT} run --rm openclaw-cli channels add --channel discord --token " +echo "Docs: https://docs.openclaw.ai/channels" + +echo "" +echo "==> Starting gateway" +docker compose "${COMPOSE_ARGS[@]}" up -d openclaw-gateway + +# --- Sandbox setup (opt-in via OPENCLAW_SANDBOX=1) --- +if [[ -n "$SANDBOX_ENABLED" ]]; then + echo "" + echo "==> Sandbox setup" + + # Build sandbox image if Dockerfile.sandbox exists. + if [[ -f "$ROOT_DIR/Dockerfile.sandbox" ]]; then + echo "Building sandbox image: openclaw-sandbox:bookworm-slim" + docker build \ + -t "openclaw-sandbox:bookworm-slim" \ + -f "$ROOT_DIR/Dockerfile.sandbox" \ + "$ROOT_DIR" + else + echo "WARNING: Dockerfile.sandbox not found in $ROOT_DIR" >&2 + echo " Sandbox config will be applied but no sandbox image will be built." >&2 + echo " Agent exec may fail if the configured sandbox image does not exist." >&2 + fi + + # Defense-in-depth: verify Docker CLI in the running image before enabling + # sandbox. This avoids claiming sandbox is enabled when the image cannot + # launch sandbox containers. + if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --entrypoint docker openclaw-gateway --version >/dev/null 2>&1; then + echo "WARNING: Docker CLI not found inside the container image." >&2 + echo " Sandbox requires Docker CLI. Rebuild with --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1" >&2 + echo " or use a local build (OPENCLAW_IMAGE=openclaw:local). Skipping sandbox setup." >&2 + SANDBOX_ENABLED="" + fi +fi + +# Apply sandbox config only if prerequisites are met. +if [[ -n "$SANDBOX_ENABLED" ]]; then + # Mount Docker socket via a dedicated compose overlay. This overlay is + # created only after sandbox prerequisites pass, so the socket is never + # exposed when sandbox cannot actually run. + if [[ -S "$DOCKER_SOCKET_PATH" ]]; then + SANDBOX_COMPOSE_FILE="$ROOT_DIR/docker-compose.sandbox.yml" + cat >"$SANDBOX_COMPOSE_FILE" <>"$SANDBOX_COMPOSE_FILE" < Sandbox: added Docker socket mount" + else + echo "WARNING: OPENCLAW_SANDBOX enabled but Docker socket not found at $DOCKER_SOCKET_PATH." >&2 + echo " Sandbox requires Docker socket access. Skipping sandbox setup." >&2 + SANDBOX_ENABLED="" + fi +fi + +if [[ -n "$SANDBOX_ENABLED" ]]; then + # Enable sandbox in OpenClaw config. + sandbox_config_ok=true + if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \ + config set agents.defaults.sandbox.mode "non-main" >/dev/null; then + echo "WARNING: Failed to set agents.defaults.sandbox.mode" >&2 + sandbox_config_ok=false + fi + if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \ + config set agents.defaults.sandbox.scope "agent" >/dev/null; then + echo "WARNING: Failed to set agents.defaults.sandbox.scope" >&2 + sandbox_config_ok=false + fi + if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \ + config set agents.defaults.sandbox.workspaceAccess "none" >/dev/null; then + echo "WARNING: Failed to set agents.defaults.sandbox.workspaceAccess" >&2 + sandbox_config_ok=false + fi + + if [[ "$sandbox_config_ok" == true ]]; then + echo "Sandbox enabled: mode=non-main, scope=agent, workspaceAccess=none" + echo "Docs: https://docs.openclaw.ai/gateway/sandboxing" + # Restart gateway with sandbox compose overlay to pick up socket mount + config. + docker compose "${COMPOSE_ARGS[@]}" up -d openclaw-gateway + else + echo "WARNING: Sandbox config was partially applied. Check errors above." >&2 + echo " Skipping gateway restart to avoid exposing Docker socket without a full sandbox policy." >&2 + if ! docker compose "${BASE_COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \ + config set agents.defaults.sandbox.mode "off" >/dev/null; then + echo "WARNING: Failed to roll back agents.defaults.sandbox.mode to off" >&2 + else + echo "Sandbox mode rolled back to off due to partial sandbox config failure." + fi + if [[ -n "${SANDBOX_COMPOSE_FILE:-}" ]]; then + rm -f "$SANDBOX_COMPOSE_FILE" + fi + # Ensure gateway service definition is reset without sandbox overlay mount. + docker compose "${BASE_COMPOSE_ARGS[@]}" up -d --force-recreate openclaw-gateway + fi +else + # Keep reruns deterministic: if sandbox is not active for this run, reset + # persisted sandbox mode so future execs do not require docker.sock by stale + # config alone. + if ! docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \ + config set agents.defaults.sandbox.mode "off" >/dev/null; then + echo "WARNING: Failed to reset agents.defaults.sandbox.mode to off" >&2 + fi + if [[ -f "$ROOT_DIR/docker-compose.sandbox.yml" ]]; then + rm -f "$ROOT_DIR/docker-compose.sandbox.yml" + fi +fi + +echo "" +echo "Gateway running with host port mapping." +echo "Access from tailnet devices via the host's tailnet IP." +echo "Config: $OPENCLAW_CONFIG_DIR" +echo "Workspace: $OPENCLAW_WORKSPACE_DIR" +echo "Token: $OPENCLAW_GATEWAY_TOKEN" +echo "" +echo "Commands:" +echo " ${COMPOSE_HINT} logs -f openclaw-gateway" +echo " ${COMPOSE_HINT} exec openclaw-gateway node dist/index.js health --token \"$OPENCLAW_GATEWAY_TOKEN\"" From bf43094f237c6d853718baca1f13abfa80e4c866 Mon Sep 17 00:00:00 2001 From: Derin Works Claude Bot Date: Mon, 16 Mar 2026 16:22:26 -0400 Subject: [PATCH 2/4] fix: config get in ensure_control_ui_allowed_origins also needs --no-deps + bridge override; DRY up repeated inline YAML block --- docker-setup.sh | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/docker-setup.sh b/docker-setup.sh index ebedb91d40d..6ef9c25aa71 100755 --- a/docker-setup.sh +++ b/docker-setup.sh @@ -12,6 +12,16 @@ SANDBOX_ENABLED="" DOCKER_SOCKET_PATH="${OPENCLAW_DOCKER_SOCKET:-}" TIMEZONE="${OPENCLAW_TZ:-}" +# Temp file for the --no-deps + network_mode:bridge Compose override used by +# config read/write calls that must not start (or join) the gateway service. +BRIDGE_OVERRIDE_FILE="$(mktemp)" +trap 'rm -f "$BRIDGE_OVERRIDE_FILE"' EXIT +cat >"$BRIDGE_OVERRIDE_FILE" <<'YAML' +services: + openclaw-cli: + network_mode: bridge +YAML + fail() { echo "ERROR: $*" >&2 exit 1 @@ -108,7 +118,7 @@ ensure_control_ui_allowed_origins() { local current_allowed_origins allowed_origin_json="$(printf '["http://127.0.0.1:%s"]' "$OPENCLAW_GATEWAY_PORT")" current_allowed_origins="$( - docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \ + docker compose "${COMPOSE_ARGS[@]}" -f "$BRIDGE_OVERRIDE_FILE" run --rm --no-deps openclaw-cli \ config get gateway.controlUi.allowedOrigins 2>/dev/null || true )" current_allowed_origins="${current_allowed_origins//$'\r'/}" @@ -118,31 +128,16 @@ ensure_control_ui_allowed_origins() { return 0 fi - docker compose "${COMPOSE_ARGS[@]}" -f <(cat <<'YAML' -services: - openclaw-cli: - network_mode: bridge -YAML -) run --rm --no-deps openclaw-cli \ + docker compose "${COMPOSE_ARGS[@]}" -f "$BRIDGE_OVERRIDE_FILE" run --rm --no-deps openclaw-cli \ config set gateway.controlUi.allowedOrigins "$allowed_origin_json" --strict-json >/dev/null echo "Set gateway.controlUi.allowedOrigins to $allowed_origin_json for non-loopback bind." } sync_gateway_mode_and_bind() { - docker compose "${COMPOSE_ARGS[@]}" -f <(cat <<'YAML' -services: - openclaw-cli: - network_mode: bridge -YAML -) run --rm --no-deps openclaw-cli \ + docker compose "${COMPOSE_ARGS[@]}" -f "$BRIDGE_OVERRIDE_FILE" run --rm --no-deps openclaw-cli \ config set gateway.mode local >/dev/null - docker compose "${COMPOSE_ARGS[@]}" -f <(cat <<'YAML' -services: - openclaw-cli: - network_mode: bridge -YAML -) run --rm --no-deps openclaw-cli \ + docker compose "${COMPOSE_ARGS[@]}" -f "$BRIDGE_OVERRIDE_FILE" run --rm --no-deps openclaw-cli \ config set gateway.bind "$OPENCLAW_GATEWAY_BIND" >/dev/null echo "Pinned gateway.mode=local and gateway.bind=$OPENCLAW_GATEWAY_BIND for Docker setup." } From d0485064b9a53e771344e4a38e02f4b9193c04a7 Mon Sep 17 00:00:00 2001 From: Derin Works Claude Bot Date: Thu, 19 Mar 2026 21:23:07 -0400 Subject: [PATCH 3/4] fix: move docker setup changes to scripts/docker/setup.sh; restore docker-setup.sh thin wrapper Co-Authored-By: Claude Sonnet 4.6 --- docker-setup.sh | 623 +--------------------------------------- scripts/docker/setup.sh | 19 +- 2 files changed, 19 insertions(+), 623 deletions(-) diff --git a/docker-setup.sh b/docker-setup.sh index 6ef9c25aa71..e8d6335bf42 100755 --- a/docker-setup.sh +++ b/docker-setup.sh @@ -2,626 +2,11 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -COMPOSE_FILE="$ROOT_DIR/docker-compose.yml" -EXTRA_COMPOSE_FILE="$ROOT_DIR/docker-compose.extra.yml" -IMAGE_NAME="${OPENCLAW_IMAGE:-openclaw:local}" -EXTRA_MOUNTS="${OPENCLAW_EXTRA_MOUNTS:-}" -HOME_VOLUME_NAME="${OPENCLAW_HOME_VOLUME:-}" -RAW_SANDBOX_SETTING="${OPENCLAW_SANDBOX:-}" -SANDBOX_ENABLED="" -DOCKER_SOCKET_PATH="${OPENCLAW_DOCKER_SOCKET:-}" -TIMEZONE="${OPENCLAW_TZ:-}" +SCRIPT_PATH="$ROOT_DIR/scripts/docker/setup.sh" -# Temp file for the --no-deps + network_mode:bridge Compose override used by -# config read/write calls that must not start (or join) the gateway service. -BRIDGE_OVERRIDE_FILE="$(mktemp)" -trap 'rm -f "$BRIDGE_OVERRIDE_FILE"' EXIT -cat >"$BRIDGE_OVERRIDE_FILE" <<'YAML' -services: - openclaw-cli: - network_mode: bridge -YAML - -fail() { - echo "ERROR: $*" >&2 - exit 1 -} - -require_cmd() { - if ! command -v "$1" >/dev/null 2>&1; then - echo "Missing dependency: $1" >&2 - exit 1 - fi -} - -is_truthy_value() { - local raw="${1:-}" - raw="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')" - case "$raw" in - 1 | true | yes | on) return 0 ;; - *) return 1 ;; - esac -} - -read_config_gateway_token() { - local config_path="$OPENCLAW_CONFIG_DIR/openclaw.json" - if [[ ! -f "$config_path" ]]; then - return 0 - fi - if command -v python3 >/dev/null 2>&1; then - python3 - "$config_path" <<'PY' -import json -import sys - -path = sys.argv[1] -try: - with open(path, "r", encoding="utf-8") as f: - cfg = json.load(f) -except Exception: - raise SystemExit(0) - -gateway = cfg.get("gateway") -if not isinstance(gateway, dict): - raise SystemExit(0) -auth = gateway.get("auth") -if not isinstance(auth, dict): - raise SystemExit(0) -token = auth.get("token") -if isinstance(token, str): - token = token.strip() - if token: - print(token) -PY - return 0 - fi - if command -v node >/dev/null 2>&1; then - node - "$config_path" <<'NODE' -const fs = require("node:fs"); -const configPath = process.argv[2]; -try { - const cfg = JSON.parse(fs.readFileSync(configPath, "utf8")); - const token = cfg?.gateway?.auth?.token; - if (typeof token === "string" && token.trim().length > 0) { - process.stdout.write(token.trim()); - } -} catch { - // Keep docker-setup resilient when config parsing fails. -} -NODE - fi -} - -read_env_gateway_token() { - local env_path="$1" - local line="" - local token="" - if [[ ! -f "$env_path" ]]; then - return 0 - fi - while IFS= read -r line || [[ -n "$line" ]]; do - line="${line%$'\r'}" - if [[ "$line" == OPENCLAW_GATEWAY_TOKEN=* ]]; then - token="${line#OPENCLAW_GATEWAY_TOKEN=}" - fi - done <"$env_path" - if [[ -n "$token" ]]; then - printf '%s' "$token" - fi -} - -ensure_control_ui_allowed_origins() { - if [[ "${OPENCLAW_GATEWAY_BIND}" == "loopback" ]]; then - return 0 - fi - - local allowed_origin_json - local current_allowed_origins - allowed_origin_json="$(printf '["http://127.0.0.1:%s"]' "$OPENCLAW_GATEWAY_PORT")" - current_allowed_origins="$( - docker compose "${COMPOSE_ARGS[@]}" -f "$BRIDGE_OVERRIDE_FILE" run --rm --no-deps openclaw-cli \ - config get gateway.controlUi.allowedOrigins 2>/dev/null || true - )" - current_allowed_origins="${current_allowed_origins//$'\r'/}" - - if [[ -n "$current_allowed_origins" && "$current_allowed_origins" != "null" && "$current_allowed_origins" != "[]" ]]; then - echo "Control UI allowlist already configured; leaving gateway.controlUi.allowedOrigins unchanged." - return 0 - fi - - docker compose "${COMPOSE_ARGS[@]}" -f "$BRIDGE_OVERRIDE_FILE" run --rm --no-deps openclaw-cli \ - config set gateway.controlUi.allowedOrigins "$allowed_origin_json" --strict-json >/dev/null - echo "Set gateway.controlUi.allowedOrigins to $allowed_origin_json for non-loopback bind." -} - -sync_gateway_mode_and_bind() { - docker compose "${COMPOSE_ARGS[@]}" -f "$BRIDGE_OVERRIDE_FILE" run --rm --no-deps openclaw-cli \ - config set gateway.mode local >/dev/null - - docker compose "${COMPOSE_ARGS[@]}" -f "$BRIDGE_OVERRIDE_FILE" run --rm --no-deps openclaw-cli \ - config set gateway.bind "$OPENCLAW_GATEWAY_BIND" >/dev/null - echo "Pinned gateway.mode=local and gateway.bind=$OPENCLAW_GATEWAY_BIND for Docker setup." -} - -contains_disallowed_chars() { - local value="$1" - [[ "$value" == *$'\n'* || "$value" == *$'\r'* || "$value" == *$'\t'* ]] -} - -is_valid_timezone() { - local value="$1" - [[ -e "/usr/share/zoneinfo/$value" && ! -d "/usr/share/zoneinfo/$value" ]] -} - -validate_mount_path_value() { - local label="$1" - local value="$2" - if [[ -z "$value" ]]; then - fail "$label cannot be empty." - fi - if contains_disallowed_chars "$value"; then - fail "$label contains unsupported control characters." - fi - if [[ "$value" =~ [[:space:]] ]]; then - fail "$label cannot contain whitespace." - fi -} - -validate_named_volume() { - local value="$1" - if [[ ! "$value" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]]; then - fail "OPENCLAW_HOME_VOLUME must match [A-Za-z0-9][A-Za-z0-9_.-]* when using a named volume." - fi -} - -validate_mount_spec() { - local mount="$1" - if contains_disallowed_chars "$mount"; then - fail "OPENCLAW_EXTRA_MOUNTS entries cannot contain control characters." - fi - # Keep mount specs strict to avoid YAML structure injection. - # Expected format: source:target[:options] - if [[ ! "$mount" =~ ^[^[:space:],:]+:[^[:space:],:]+(:[^[:space:],:]+)?$ ]]; then - fail "Invalid mount format '$mount'. Expected source:target[:options] without spaces." - fi -} - -require_cmd docker -if ! docker compose version >/dev/null 2>&1; then - echo "Docker Compose not available (try: docker compose version)" >&2 +if [[ ! -f "$SCRIPT_PATH" ]]; then + echo "Docker setup script not found at $SCRIPT_PATH" >&2 exit 1 fi -if [[ -z "$DOCKER_SOCKET_PATH" && "${DOCKER_HOST:-}" == unix://* ]]; then - DOCKER_SOCKET_PATH="${DOCKER_HOST#unix://}" -fi -if [[ -z "$DOCKER_SOCKET_PATH" ]]; then - DOCKER_SOCKET_PATH="/var/run/docker.sock" -fi -if is_truthy_value "$RAW_SANDBOX_SETTING"; then - SANDBOX_ENABLED="1" -fi - -OPENCLAW_CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}" -OPENCLAW_WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$HOME/.openclaw/workspace}" - -validate_mount_path_value "OPENCLAW_CONFIG_DIR" "$OPENCLAW_CONFIG_DIR" -validate_mount_path_value "OPENCLAW_WORKSPACE_DIR" "$OPENCLAW_WORKSPACE_DIR" -if [[ -n "$HOME_VOLUME_NAME" ]]; then - if [[ "$HOME_VOLUME_NAME" == *"/"* ]]; then - validate_mount_path_value "OPENCLAW_HOME_VOLUME" "$HOME_VOLUME_NAME" - else - validate_named_volume "$HOME_VOLUME_NAME" - fi -fi -if contains_disallowed_chars "$EXTRA_MOUNTS"; then - fail "OPENCLAW_EXTRA_MOUNTS cannot contain control characters." -fi -if [[ -n "$SANDBOX_ENABLED" ]]; then - validate_mount_path_value "OPENCLAW_DOCKER_SOCKET" "$DOCKER_SOCKET_PATH" -fi -if [[ -n "$TIMEZONE" ]]; then - if contains_disallowed_chars "$TIMEZONE"; then - fail "OPENCLAW_TZ contains unsupported control characters." - fi - if [[ ! "$TIMEZONE" =~ ^[A-Za-z0-9/_+\-]+$ ]]; then - fail "OPENCLAW_TZ must be a valid IANA timezone string (e.g. Asia/Shanghai)." - fi - if ! is_valid_timezone "$TIMEZONE"; then - fail "OPENCLAW_TZ must match a timezone in /usr/share/zoneinfo (e.g. Asia/Shanghai)." - fi -fi - -mkdir -p "$OPENCLAW_CONFIG_DIR" -mkdir -p "$OPENCLAW_WORKSPACE_DIR" -# Seed directory tree eagerly so bind mounts work even on Docker Desktop/Windows -# where the container (even as root) cannot create new host subdirectories. -mkdir -p "$OPENCLAW_CONFIG_DIR/identity" -mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/agent" -mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/sessions" - -export OPENCLAW_CONFIG_DIR -export OPENCLAW_WORKSPACE_DIR -export OPENCLAW_GATEWAY_PORT="${OPENCLAW_GATEWAY_PORT:-18789}" -export OPENCLAW_BRIDGE_PORT="${OPENCLAW_BRIDGE_PORT:-18790}" -export OPENCLAW_GATEWAY_BIND="${OPENCLAW_GATEWAY_BIND:-lan}" -export OPENCLAW_IMAGE="$IMAGE_NAME" -export OPENCLAW_DOCKER_APT_PACKAGES="${OPENCLAW_DOCKER_APT_PACKAGES:-}" -export OPENCLAW_EXTENSIONS="${OPENCLAW_EXTENSIONS:-}" -export OPENCLAW_EXTRA_MOUNTS="$EXTRA_MOUNTS" -export OPENCLAW_HOME_VOLUME="$HOME_VOLUME_NAME" -export OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="${OPENCLAW_ALLOW_INSECURE_PRIVATE_WS:-}" -export OPENCLAW_SANDBOX="$SANDBOX_ENABLED" -export OPENCLAW_DOCKER_SOCKET="$DOCKER_SOCKET_PATH" -export OPENCLAW_TZ="$TIMEZONE" - -# Detect Docker socket GID for sandbox group_add. -DOCKER_GID="" -if [[ -n "$SANDBOX_ENABLED" && -S "$DOCKER_SOCKET_PATH" ]]; then - DOCKER_GID="$(stat -c '%g' "$DOCKER_SOCKET_PATH" 2>/dev/null || stat -f '%g' "$DOCKER_SOCKET_PATH" 2>/dev/null || echo "")" -fi -export DOCKER_GID - -if [[ -z "${OPENCLAW_GATEWAY_TOKEN:-}" ]]; then - EXISTING_CONFIG_TOKEN="$(read_config_gateway_token || true)" - if [[ -n "$EXISTING_CONFIG_TOKEN" ]]; then - OPENCLAW_GATEWAY_TOKEN="$EXISTING_CONFIG_TOKEN" - echo "Reusing gateway token from $OPENCLAW_CONFIG_DIR/openclaw.json" - else - DOTENV_GATEWAY_TOKEN="$(read_env_gateway_token "$ROOT_DIR/.env" || true)" - if [[ -n "$DOTENV_GATEWAY_TOKEN" ]]; then - OPENCLAW_GATEWAY_TOKEN="$DOTENV_GATEWAY_TOKEN" - echo "Reusing gateway token from $ROOT_DIR/.env" - elif command -v openssl >/dev/null 2>&1; then - OPENCLAW_GATEWAY_TOKEN="$(openssl rand -hex 32)" - else - OPENCLAW_GATEWAY_TOKEN="$(python3 - <<'PY' -import secrets -print(secrets.token_hex(32)) -PY -)" - fi - fi -fi -export OPENCLAW_GATEWAY_TOKEN - -COMPOSE_FILES=("$COMPOSE_FILE") -COMPOSE_ARGS=() - -write_extra_compose() { - local home_volume="$1" - shift - local mount - local gateway_home_mount - local gateway_config_mount - local gateway_workspace_mount - - cat >"$EXTRA_COMPOSE_FILE" <<'YAML' -services: - openclaw-gateway: - volumes: -YAML - - if [[ -n "$home_volume" ]]; then - gateway_home_mount="${home_volume}:/home/node" - gateway_config_mount="${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw" - gateway_workspace_mount="${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace" - validate_mount_spec "$gateway_home_mount" - validate_mount_spec "$gateway_config_mount" - validate_mount_spec "$gateway_workspace_mount" - printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE" - printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE" - printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE" - fi - - for mount in "$@"; do - validate_mount_spec "$mount" - printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE" - done - - cat >>"$EXTRA_COMPOSE_FILE" <<'YAML' - openclaw-cli: - volumes: -YAML - - if [[ -n "$home_volume" ]]; then - printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE" - printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE" - printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE" - fi - - for mount in "$@"; do - validate_mount_spec "$mount" - printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE" - done - - if [[ -n "$home_volume" && "$home_volume" != *"/"* ]]; then - validate_named_volume "$home_volume" - cat >>"$EXTRA_COMPOSE_FILE" <>"$tmp" - seen="$seen$k " - replaced=true - break - fi - done - if [[ "$replaced" == false ]]; then - printf '%s\n' "$line" >>"$tmp" - fi - done <"$file" - fi - - for k in "${keys[@]}"; do - if [[ "$seen" != *" $k "* ]]; then - printf '%s=%s\n' "$k" "${!k-}" >>"$tmp" - fi - done - - mv "$tmp" "$file" -} - -upsert_env "$ENV_FILE" \ - OPENCLAW_CONFIG_DIR \ - OPENCLAW_WORKSPACE_DIR \ - OPENCLAW_GATEWAY_PORT \ - OPENCLAW_BRIDGE_PORT \ - OPENCLAW_GATEWAY_BIND \ - OPENCLAW_GATEWAY_TOKEN \ - OPENCLAW_IMAGE \ - OPENCLAW_EXTRA_MOUNTS \ - OPENCLAW_HOME_VOLUME \ - OPENCLAW_DOCKER_APT_PACKAGES \ - OPENCLAW_EXTENSIONS \ - OPENCLAW_SANDBOX \ - OPENCLAW_DOCKER_SOCKET \ - DOCKER_GID \ - OPENCLAW_INSTALL_DOCKER_CLI \ - OPENCLAW_ALLOW_INSECURE_PRIVATE_WS \ - OPENCLAW_TZ - -if [[ "$IMAGE_NAME" == "openclaw:local" ]]; then - echo "==> Building Docker image: $IMAGE_NAME" - docker build \ - --build-arg "OPENCLAW_DOCKER_APT_PACKAGES=${OPENCLAW_DOCKER_APT_PACKAGES}" \ - --build-arg "OPENCLAW_EXTENSIONS=${OPENCLAW_EXTENSIONS}" \ - --build-arg "OPENCLAW_INSTALL_DOCKER_CLI=${OPENCLAW_INSTALL_DOCKER_CLI:-}" \ - -t "$IMAGE_NAME" \ - -f "$ROOT_DIR/Dockerfile" \ - "$ROOT_DIR" -else - echo "==> Pulling Docker image: $IMAGE_NAME" - if ! docker pull "$IMAGE_NAME"; then - echo "ERROR: Failed to pull image $IMAGE_NAME. Please check the image name and your access permissions." >&2 - exit 1 - fi -fi - -# Ensure bind-mounted data directories are writable by the container's `node` -# user (uid 1000). Host-created dirs inherit the host user's uid which may -# differ, causing EACCES when the container tries to mkdir/write. -# Running a brief root container to chown is the portable Docker idiom -- -# it works regardless of the host uid and doesn't require host-side root. -echo "" -echo "==> Fixing data-directory permissions" -# Use -xdev to restrict chown to the config-dir mount only — without it, -# the recursive chown would cross into the workspace bind mount and rewrite -# ownership of all user project files on Linux hosts. -# After fixing the config dir, only the OpenClaw metadata subdirectory -# (.openclaw/) inside the workspace gets chowned, not the user's project files. -docker compose "${COMPOSE_ARGS[@]}" run --rm --user root --entrypoint sh openclaw-cli -c \ - 'find /home/node/.openclaw -xdev -exec chown node:node {} +; \ - [ -d /home/node/.openclaw/workspace/.openclaw ] && chown -R node:node /home/node/.openclaw/workspace/.openclaw || true' - -echo "" -echo "==> Onboarding (interactive)" -echo "Docker setup pins Gateway mode to local." -echo "Gateway runtime bind comes from OPENCLAW_GATEWAY_BIND (default: lan)." -echo "Current runtime bind: $OPENCLAW_GATEWAY_BIND" -echo "Gateway token: $OPENCLAW_GATEWAY_TOKEN" -echo "Tailscale exposure: Off (use host-level tailnet/Tailscale setup separately)." -echo "Install Gateway daemon: No (managed by Docker Compose)" -echo "" -docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli onboard --mode local --no-install-daemon - -echo "" -echo "==> Docker gateway defaults" -sync_gateway_mode_and_bind - -echo "" -echo "==> Control UI origin allowlist" -ensure_control_ui_allowed_origins - -echo "" -echo "==> Provider setup (optional)" -echo "WhatsApp (QR):" -echo " ${COMPOSE_HINT} run --rm openclaw-cli channels login" -echo "Telegram (bot token):" -echo " ${COMPOSE_HINT} run --rm openclaw-cli channels add --channel telegram --token " -echo "Discord (bot token):" -echo " ${COMPOSE_HINT} run --rm openclaw-cli channels add --channel discord --token " -echo "Docs: https://docs.openclaw.ai/channels" - -echo "" -echo "==> Starting gateway" -docker compose "${COMPOSE_ARGS[@]}" up -d openclaw-gateway - -# --- Sandbox setup (opt-in via OPENCLAW_SANDBOX=1) --- -if [[ -n "$SANDBOX_ENABLED" ]]; then - echo "" - echo "==> Sandbox setup" - - # Build sandbox image if Dockerfile.sandbox exists. - if [[ -f "$ROOT_DIR/Dockerfile.sandbox" ]]; then - echo "Building sandbox image: openclaw-sandbox:bookworm-slim" - docker build \ - -t "openclaw-sandbox:bookworm-slim" \ - -f "$ROOT_DIR/Dockerfile.sandbox" \ - "$ROOT_DIR" - else - echo "WARNING: Dockerfile.sandbox not found in $ROOT_DIR" >&2 - echo " Sandbox config will be applied but no sandbox image will be built." >&2 - echo " Agent exec may fail if the configured sandbox image does not exist." >&2 - fi - - # Defense-in-depth: verify Docker CLI in the running image before enabling - # sandbox. This avoids claiming sandbox is enabled when the image cannot - # launch sandbox containers. - if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --entrypoint docker openclaw-gateway --version >/dev/null 2>&1; then - echo "WARNING: Docker CLI not found inside the container image." >&2 - echo " Sandbox requires Docker CLI. Rebuild with --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1" >&2 - echo " or use a local build (OPENCLAW_IMAGE=openclaw:local). Skipping sandbox setup." >&2 - SANDBOX_ENABLED="" - fi -fi - -# Apply sandbox config only if prerequisites are met. -if [[ -n "$SANDBOX_ENABLED" ]]; then - # Mount Docker socket via a dedicated compose overlay. This overlay is - # created only after sandbox prerequisites pass, so the socket is never - # exposed when sandbox cannot actually run. - if [[ -S "$DOCKER_SOCKET_PATH" ]]; then - SANDBOX_COMPOSE_FILE="$ROOT_DIR/docker-compose.sandbox.yml" - cat >"$SANDBOX_COMPOSE_FILE" <>"$SANDBOX_COMPOSE_FILE" < Sandbox: added Docker socket mount" - else - echo "WARNING: OPENCLAW_SANDBOX enabled but Docker socket not found at $DOCKER_SOCKET_PATH." >&2 - echo " Sandbox requires Docker socket access. Skipping sandbox setup." >&2 - SANDBOX_ENABLED="" - fi -fi - -if [[ -n "$SANDBOX_ENABLED" ]]; then - # Enable sandbox in OpenClaw config. - sandbox_config_ok=true - if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \ - config set agents.defaults.sandbox.mode "non-main" >/dev/null; then - echo "WARNING: Failed to set agents.defaults.sandbox.mode" >&2 - sandbox_config_ok=false - fi - if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \ - config set agents.defaults.sandbox.scope "agent" >/dev/null; then - echo "WARNING: Failed to set agents.defaults.sandbox.scope" >&2 - sandbox_config_ok=false - fi - if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \ - config set agents.defaults.sandbox.workspaceAccess "none" >/dev/null; then - echo "WARNING: Failed to set agents.defaults.sandbox.workspaceAccess" >&2 - sandbox_config_ok=false - fi - - if [[ "$sandbox_config_ok" == true ]]; then - echo "Sandbox enabled: mode=non-main, scope=agent, workspaceAccess=none" - echo "Docs: https://docs.openclaw.ai/gateway/sandboxing" - # Restart gateway with sandbox compose overlay to pick up socket mount + config. - docker compose "${COMPOSE_ARGS[@]}" up -d openclaw-gateway - else - echo "WARNING: Sandbox config was partially applied. Check errors above." >&2 - echo " Skipping gateway restart to avoid exposing Docker socket without a full sandbox policy." >&2 - if ! docker compose "${BASE_COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \ - config set agents.defaults.sandbox.mode "off" >/dev/null; then - echo "WARNING: Failed to roll back agents.defaults.sandbox.mode to off" >&2 - else - echo "Sandbox mode rolled back to off due to partial sandbox config failure." - fi - if [[ -n "${SANDBOX_COMPOSE_FILE:-}" ]]; then - rm -f "$SANDBOX_COMPOSE_FILE" - fi - # Ensure gateway service definition is reset without sandbox overlay mount. - docker compose "${BASE_COMPOSE_ARGS[@]}" up -d --force-recreate openclaw-gateway - fi -else - # Keep reruns deterministic: if sandbox is not active for this run, reset - # persisted sandbox mode so future execs do not require docker.sock by stale - # config alone. - if ! docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \ - config set agents.defaults.sandbox.mode "off" >/dev/null; then - echo "WARNING: Failed to reset agents.defaults.sandbox.mode to off" >&2 - fi - if [[ -f "$ROOT_DIR/docker-compose.sandbox.yml" ]]; then - rm -f "$ROOT_DIR/docker-compose.sandbox.yml" - fi -fi - -echo "" -echo "Gateway running with host port mapping." -echo "Access from tailnet devices via the host's tailnet IP." -echo "Config: $OPENCLAW_CONFIG_DIR" -echo "Workspace: $OPENCLAW_WORKSPACE_DIR" -echo "Token: $OPENCLAW_GATEWAY_TOKEN" -echo "" -echo "Commands:" -echo " ${COMPOSE_HINT} logs -f openclaw-gateway" -echo " ${COMPOSE_HINT} exec openclaw-gateway node dist/index.js health --token \"$OPENCLAW_GATEWAY_TOKEN\"" +exec "$SCRIPT_PATH" "$@" diff --git a/scripts/docker/setup.sh b/scripts/docker/setup.sh index cfa6fd4046e..398f013d134 100755 --- a/scripts/docker/setup.sh +++ b/scripts/docker/setup.sh @@ -12,6 +12,16 @@ SANDBOX_ENABLED="" DOCKER_SOCKET_PATH="${OPENCLAW_DOCKER_SOCKET:-}" TIMEZONE="${OPENCLAW_TZ:-}" +# Temp file for the --no-deps + network_mode:bridge Compose override used by +# config read/write calls that must not start (or join) the gateway service. +BRIDGE_OVERRIDE_FILE="$(mktemp)" +trap 'rm -f "$BRIDGE_OVERRIDE_FILE"' EXIT +cat >"$BRIDGE_OVERRIDE_FILE" <<'YAML' +services: + openclaw-cli: + network_mode: bridge +YAML + fail() { echo "ERROR: $*" >&2 exit 1 @@ -108,7 +118,7 @@ ensure_control_ui_allowed_origins() { local current_allowed_origins allowed_origin_json="$(printf '["http://127.0.0.1:%s"]' "$OPENCLAW_GATEWAY_PORT")" current_allowed_origins="$( - docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \ + docker compose "${COMPOSE_ARGS[@]}" -f "$BRIDGE_OVERRIDE_FILE" run --rm --no-deps openclaw-cli \ config get gateway.controlUi.allowedOrigins 2>/dev/null || true )" current_allowed_origins="${current_allowed_origins//$'\r'/}" @@ -118,15 +128,16 @@ ensure_control_ui_allowed_origins() { return 0 fi - docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \ + docker compose "${COMPOSE_ARGS[@]}" -f "$BRIDGE_OVERRIDE_FILE" run --rm --no-deps openclaw-cli \ config set gateway.controlUi.allowedOrigins "$allowed_origin_json" --strict-json >/dev/null echo "Set gateway.controlUi.allowedOrigins to $allowed_origin_json for non-loopback bind." } sync_gateway_mode_and_bind() { - docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \ + docker compose "${COMPOSE_ARGS[@]}" -f "$BRIDGE_OVERRIDE_FILE" run --rm --no-deps openclaw-cli \ config set gateway.mode local >/dev/null - docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \ + + docker compose "${COMPOSE_ARGS[@]}" -f "$BRIDGE_OVERRIDE_FILE" run --rm --no-deps openclaw-cli \ config set gateway.bind "$OPENCLAW_GATEWAY_BIND" >/dev/null echo "Pinned gateway.mode=local and gateway.bind=$OPENCLAW_GATEWAY_BIND for Docker setup." } From 40d03c6a38808796a0323cb58c720f7716613b4c Mon Sep 17 00:00:00 2001 From: Derin Works Claude Bot Date: Thu, 19 Mar 2026 21:54:29 -0400 Subject: [PATCH 4/4] fix(docker): seed full default Control UI origin set in ensure_control_ui_allowed_origins Mirrors buildDefaultControlUiAllowedOrigins: always includes both http://localhost: and http://127.0.0.1:, plus http://: when bind=custom. Co-Authored-By: Claude Sonnet 4.6 --- scripts/docker/setup.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/scripts/docker/setup.sh b/scripts/docker/setup.sh index 398f013d134..f6436c9ebca 100755 --- a/scripts/docker/setup.sh +++ b/scripts/docker/setup.sh @@ -116,7 +116,20 @@ ensure_control_ui_allowed_origins() { local allowed_origin_json local current_allowed_origins - allowed_origin_json="$(printf '["http://127.0.0.1:%s"]' "$OPENCLAW_GATEWAY_PORT")" + # Mirror buildDefaultControlUiAllowedOrigins: always seed both localhost and 127.0.0.1. + allowed_origin_json="$(printf '["http://localhost:%s","http://127.0.0.1:%s"]' "$OPENCLAW_GATEWAY_PORT" "$OPENCLAW_GATEWAY_PORT")" + if [[ "${OPENCLAW_GATEWAY_BIND}" == "custom" ]]; then + local custom_bind_host + custom_bind_host="$( + docker compose "${COMPOSE_ARGS[@]}" -f "$BRIDGE_OVERRIDE_FILE" run --rm --no-deps openclaw-cli \ + config get gateway.customBindHost 2>/dev/null || true + )" + custom_bind_host="${custom_bind_host//$'\r'/}" + if [[ -n "$custom_bind_host" && "$custom_bind_host" != "null" ]]; then + allowed_origin_json="$(printf '["http://localhost:%s","http://127.0.0.1:%s","http://%s:%s"]' \ + "$OPENCLAW_GATEWAY_PORT" "$OPENCLAW_GATEWAY_PORT" "$custom_bind_host" "$OPENCLAW_GATEWAY_PORT")" + fi + fi current_allowed_origins="$( docker compose "${COMPOSE_ARGS[@]}" -f "$BRIDGE_OVERRIDE_FILE" run --rm --no-deps openclaw-cli \ config get gateway.controlUi.allowedOrigins 2>/dev/null || true