From 9f534d088ba24d7dc77a903d3ba74eead1fa01bb Mon Sep 17 00:00:00 2001 From: Trevor SANDY Date: Sun, 3 May 2026 18:20:40 +0200 Subject: [PATCH] OpenClaw use Docker setup.sh instead of openckaw_ctl --- .vscode/launch.json | 21 +- access/auto_config.sh | 49 +- openclaw_ctl | 1182 ----------------------------------------- suite_services.py | 522 +++++++++--------- 4 files changed, 338 insertions(+), 1436 deletions(-) delete mode 100644 openclaw_ctl diff --git a/.vscode/launch.json b/.vscode/launch.json index fc3ac20..fb734ea 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,16 +20,29 @@ "program": "${command:SelectScriptName}", "args": [] }, + { + "type": "bashdb", + "request": "launch", + "name": "Bash-Debug (openclaw_setup)", + "cwd": "${workspaceFolder}/openclaw", + "program": "${workspaceFolder}/openclaw/scripts/docker/setup.sh", + "env": { + "OPENCLAW_SANDBOX": "1", + "OPENCLAW_SKIP_ONBOARDING": "1", + "OPENCLAW_CONFIG_DIR": "/mnt/c/Users/Trevor/.openclaw", + "OPENCLAW_WORKSPACE_DIR": "/mnt/c/Users/Trevor/.openclaw/workspace" + } + }, { "type": "bashdb", "request": "launch", "name": "Bash-Debug (openclaw_ctl)", "cwd": "${workspaceFolder}", "program": "${workspaceFolder}/openclaw_ctl", - "args": ["--gateway", "--sandbox"], - //"args": ["--configuration", "--sandbox"] - //"args": ["--onboarding", "--sandbox"] - //"args": ["--setenv", "--sandbox"] + //"args": ["--gateway", "--sandbox"], + "args": ["--configuration", "--sandbox"], + //"args": ["--onboarding", "--sandbox"], + //"args": ["--setenv", "--sandbox"], }, { "name": "PowerShell Launch Current File", diff --git a/access/auto_config.sh b/access/auto_config.sh index 0172886..69d0a34 100644 --- a/access/auto_config.sh +++ b/access/auto_config.sh @@ -1,6 +1,6 @@ #!/bin/bash # Trevor SANDY -# Last Update April, 28 2026 +# Last Update May, 03 2026 # Copyright (C) 2026 by Trevor SANDY # # Auto-configure, with user prompts, self-hosted AI-Suite with Caddy/Nginx proxy and @@ -399,7 +399,7 @@ user_confirm='Default' config_mode="Interactive" up_ver="v1.1.0" up_bin='./access/url-parser' -yq_ver="v4.45.4" # v4.52.4 +yq_ver="v4.53.2" # v4.45.4 yq_bin='./access/yq' PLATFORM='unknown' @@ -1871,7 +1871,8 @@ openclaw_compose_path="./openclaw/docker-compose.yml" if [[ -f "$openclaw_compose_path" ]]; then log_info "${HEADER}Rebuild OpenClaw Services" #------------------------------------------- - # Rebuild OpenClaw services + # Rebuild OpenClaw services with service name and container names + log_info "${BODY}Rebuild services with service name and container names" # shellcheck disable=SC2016 openclaw_service_yaml=' { @@ -1901,6 +1902,44 @@ if [[ -f "$openclaw_compose_path" ]]; then ) ' update_yaml_file "$openclaw_service_yaml" "$openclaw_compose_path" + + # Add openclaw-gateway build args and set pull_policy for locally built image + if [[ -n ${AC_OPENCLAW_SANDBOX+x} ]]; then + log_info "${BODY}Add openclaw-gateway build args and set image pull_policy" + openclaw_service_yaml=' + .services."openclaw-gateway" |= ( + . as $orig | + # Rebuild in desired order, then merge original + { + "image": "${OPENCLAW_IMAGE:-openclaw:local}", + "pull_policy": "never" + } + * $orig + # Normalize/enrich build + | .build |= ( + (select(tag == "!!str") | { + "context": ., + "args": { + "OPENCLAW_INSTALL_DOCKER_CLI": "${OPENCLAW_INSTALL_DOCKER_CLI:-}" + } + }) + // + (. * { + "args": { + "OPENCLAW_INSTALL_DOCKER_CLI": "${OPENCLAW_INSTALL_DOCKER_CLI:-}" + } + }) + ) + ) | + .services."openclaw-cli" |= ( + { + "image": "openclaw:local", + "pull_policy": "never" + } * . + ) + ' + update_yaml_file "$openclaw_service_yaml" "$openclaw_compose_path" + fi fi log_info "${HEADER}Configure Proxy Service" @@ -3127,8 +3166,8 @@ PSH echo "$ps_payload" > "$posix_script_path" && ps_payload="" - log_debug "Hosts edit script: $win_script_path" - cat "$posix_script_path" + #log_debug "Hosts edit script: $win_script_path" + #cat "$posix_script_path" #----------------------------- # Elevation and Restricted check before PowerShell invocation diff --git a/openclaw_ctl b/openclaw_ctl deleted file mode 100644 index 087b54d..0000000 --- a/openclaw_ctl +++ /dev/null @@ -1,1182 +0,0 @@ -#!/usr/bin/env bash -# Trevor SANDY -# Last Update April, 30 2026 -# Copyright (C) 2026 by Trevor SANDY -# -# Change Updates -# https://github.com/openclaw/openclaw/blob/main/scripts/docker/setup.sh -# 490e6d6 - 30/04/2026 -# 66f4b52 - 28/04/2026 - -set -euo pipefail - -: "${APP_NAME:='AI-Suite'}" -: "${DEBUG_ON:=false}" -: "${PLATFORM:='unknown'}" -: "${PRIMARY_ARG:=''}" -: "${ENV_MODE:=''}" -: "${SILENT:=0}" - -# Reset BASH time counter -SECONDS=0 - -# Colors -SGR='' -END='' -# Core SGR support detection -if [[ "${SILENT:-0}" == 0 && -t 1 ]]; then - if colors=$(tput colors 2>/dev/null) && [[ "$colors" -ge 8 ]]; then - SGR=$'\033[' - END="${SGR}0m" - fi -fi -# Primitive style flags -BOLD='' -DIM='' -ITALIC='' -UNDERLINE='' -# Base colors -RED='' -GREEN='' -YELLOW='' -BLUE='' -MAGENTA='' -CYAN='' -WHITE='' -# Composite tokens -DIM_CYAN='' -BOLD_MAGENTA='' -ITALIC_RED_BG='' -UNDERLINE_YELLOW='' -# Semantic tokens -HEADER='' -BODY=' - ' -COLON=':' -APP="${APP_NAME}${COLON}" -ERROR="${APP} ERROR" -INFO="${APP} INFO" - -# Apply SGR layer once -if [[ -n "$SGR" ]]; then - BOLD='1;' - DIM='2;' - ITALIC='3;' - UNDERLINE='4;' - - RED="${SGR}31m" - GREEN="${SGR}32m" - YELLOW="${SGR}33m" - BLUE="${SGR}34m" - MAGENTA="${SGR}35m" - CYAN="${SGR}36m" - WHITE="${SGR}37m" - - DIM_CYAN="${SGR}${DIM}36m" - BOLD_MAGENTA="${SGR}${BOLD}95m" - ITALIC_RED_BG="${SGR}${ITALIC}41m" - UNDERLINE_YELLOW="${SGR}${UNDERLINE}93m" - - HEADER="${SGR}${BOLD}${UNDERLINE}92m" - BODY="${SGR}37m -${END} ${SGR}32m" - - COLON="${SGR}97m:" - APP="${SGR}${ITALIC}94m${APP_NAME}${COLON}${END}" -fi - -# Log level names -declare -A LOG_LEVEL_NAME=( - [NOTICE]='NOTICE' - [CRITICAL]='CRITICAL' - [ERROR]='ERROR' - [WARNING]='WARNING' - [DEBUG]='DEBUG' - [INFO]='INFO' -) - -# Log level styles -declare -A LOG_LEVEL_STYLE=( - [NOTICE]="${SGR}${BOLD}95m" # MAGENTA - [CRITICAL]="${SGR}${BOLD}41m" # RED_BG - [ERROR]="${SGR}${BOLD}91m" # RED - [WARNING]="${SGR}${BOLD}${UNDERLINE}93m" # YELLOW - [DEBUG]="${SGR}${BOLD}97m" # WHITE - [INFO]="${SGR}36m" # CYAN -) - -# Log message styles -declare -A LOG_MESSAGE_STYLE=( - [NOTICE]="$BOLD_MAGENTA" - [CRITICAL]="$ITALIC_RED_BG" - [ERROR]="$RED" - [WARNING]="$UNDERLINE_YELLOW" - [DEBUG]="$WHITE" - [INFO]="$DIM_CYAN" -) - -# Prefix cache (precomputed for speed) -log_header_prefix() { - local -n _ref=$1 - local level=$2 - local prefix - if [[ -n "$SGR" ]]; then - prefix="${APP} ${LOG_LEVEL_STYLE[$level]}${LOG_LEVEL_NAME[$level]}${END}" - else - prefix="${APP} ${LOG_LEVEL_NAME[$level]}" - fi - _ref=$prefix -} - -declare -A LOG_HEADER_PREFIX - -for level in "${!LOG_LEVEL_NAME[@]}"; do - log_header_prefix LOG_HEADER_PREFIX["$level"] "$level" -done - -LOG_TIMESTAMP="${LOG_TIMESTAMP:-false}" - -# Injection-aware logger -log() { - local level="$1"; shift - [[ "$level" == DEBUG && "$DEBUG_ON" != true ]] && return - - local header="${LOG_HEADER_PREFIX[$level]}" - local message="${LOG_MESSAGE_STYLE[$level]}" - - if [[ -n "$SGR" && "$*" == *$'\033['* ]]; then - message="${END}$*" - else - message="${message}$*" - fi - - if [[ "$LOG_TIMESTAMP" == true ]]; then - printf -v header '%(%Y-%m-%d %H:%M:%S)T %s' -1 "$header" - fi - - if [[ "$level" == CRITICAL || "$level" == ERROR ]]; then - printf '%s %s%s\n' "$header" "$message" "$END" >&2 - else - printf '%s %s%s\n' "$header" "$message" "$END" - fi -} - -# Semantic tokens -log_header_prefix ERROR 'ERROR' -log_header_prefix INFO 'INFO' - -# Wrappers -log_notice() { log NOTICE "$*"; } -log_critical() { log CRITICAL "$*"; } -log_error() { log ERROR "$*"; } -log_warning() { log WARNING "$*"; } -log_debug() { log DEBUG "$*"; } -log_info() { log INFO "$*"; } - -fail() { - log_critical "$*" - exit 1 -} - -strip_sgr() { - local line sgr=$'\033' - while IFS= read -r line; do - while [[ $line == *"${sgr}["*m* ]]; do - line="${line/${sgr}\[[0-79;]\{1,11\}m/}" - done - printf '%s\n' "$line" - done - # - The substitution regex explained: - # line original line - # / substitution delimeter '/' - # $sgr match the SGR escape sequence '\x1b' before the color or attribute code - # \[ matches the first open bracket - escape '\[' to distinguish from regex [ - # [0-79;]\{1,11\} matches '1 to 11' of any character in '012345679;' - escape the curly braces - # with '\{' to keep the shell from mangling them - # we have 11 times due to bold, dim, italic, underline and color * 2 plus reset * 1 - # m match the SGR escape sequence reset character 'm' - this trails the color code - # / substitution delimeter '/' -} - -# Real-time tee with SGR stripping -SCRIPT="${BASH_SOURCE[0]##*/}" -if [[ "$SCRIPT" == "${0##*/}" ]]; then - LOG_PATH="${AC_LOG_PATH:-$PWD}" - LOG="$LOG_PATH/$SCRIPT.log" - [[ -f "$LOG" && -r "$LOG" ]] && rm "$LOG" - # Strip SGR color sequence codes from output" - exec > >(tee >(strip_sgr >>"$LOG")) - exec 2> >(tee >(strip_sgr >>"$LOG") >&2) -fi - -# Capture elapsed execution time -# shellcheck disable=SC2329 -finish_elapsed_time() { - set +x - local ELAPSED - ELAPSED="$((SECONDS / 3600))hrs $(((SECONDS / 60) % 60))min $((SECONDS % 60))sec" - printf '%s\n' "${INFO} ${CYAN}Elapsed time:${END} ${GREEN}$ELAPSED${END}" - printf '%s\n' "${INFO} ${GREEN}-------------------------------------------${END}" -} - -completion='' -# shellcheck disable=SC2329 -finish () { - local header="${END}✅ ${HEADER}" - local action="Set .env" - local status="Completed" - case "$PRIMARY_ARG" in - --setenv) : ;; - --onboarding) action="Onboarding" ;; - --configuration) action="Configuration" ;; - --gateway) action="Gateway" ;; - *) action="Unknown" ;; - esac - case "$completion" in - Success!) : ;; - Partial!) - header="${END}⚠️ ${HEADER}" - status="Finished" - ;; - *) - header="${END}❌ ${SGR}${BOLD}${UNDERLINE}91m" - status="Terminated" - ;; - esac - log_info "${header}OpenClaw - $action $status" - #------------------------------------------- - finish_elapsed_time -} - -trap finish EXIT - -######################################## -# ENVIRONMENT -######################################## -log_info "${HEADER}OpenClaw - Environment" -#------------------------------------------- -detect_arch() { - local -n _ref=$1 - case $(uname -m) in - x86_64) _ref='amd64' ;; - aarch64 | arm64) _ref='arm64' ;; - armv7l) _ref='arm' ;; - i686 | i386) _ref='386' ;; - *) _ref='err' ;; - esac -} - -detect_os() { - local -n _ref=$1 - case $(uname | tr '[:upper:]' '[:lower:]') in - linux*) _ref='linux' ;; - darwin*) _ref='darwin' ;; - *) _ref='err' ;; - esac -} - -is_wsl() { - case "$(uname -r)" in - *icrosoft*WSL2 | *icrosoft*wsl2) return ;; - *icrosoft) fail "Microsoft WSL1 is not supported. Use WSL2 with 'wsl --set-version 2'" ;; - *) return 1 ;; - esac -} - -os='' -detect_os os -case "$os" in -linux*) - if is_wsl; then - PLATFORM="wsl" - else - PLATFORM="linux" - fi - ;; -darwin*) PLATFORM="mac" ;; -err) fail "Unsupported platform." ;; -esac - -arch='' -detect_arch arch -if [[ "$arch" == "err" ]]; then fail "Unsupported CPU architecture"; fi - -log_info "${BODY}OS:${END} ${WHITE}$os" -log_info "${BODY}PLATFORM:${END} ${WHITE}$PLATFORM" -log_info "${BODY}ARCHITECTURE:${END} ${WHITE}$arch" - -######################################## -# ROOT / CONFIG -######################################## -log_info "${HEADER}OpenClaw - Variables" -#------------------------------------------- -ROOT_DIR="$(pwd)" -OPENCLAW_DIR="" -log_info "${BODY}ROOT_DIR:${END} ${WHITE}$ROOT_DIR" -case "$(basename "$ROOT_DIR")" in - openclaw) - OPENCLAW_DIR="${ROOT_DIR}" - ROOT_DIR="$(cd "$ROOT_DIR/../" && pwd)" - ;; - ai-suite) - OPENCLAW_DIR="$(cd "$ROOT_DIR/openclaw" && pwd)" - ;; - *) fail "Must be run from 'openclaw' or 'ai-suite'." ;; -esac -log_info "${BODY}OPENCLAW_DIR:${END} ${WHITE}$OPENCLAW_DIR" -# shellcheck disable=SC1091 -source "$OPENCLAW_DIR/scripts/lib/docker-build.sh" - -COMPOSE_FILE="$OPENCLAW_DIR/docker-compose.yml" -EXTRA_COMPOSE_FILE="$OPENCLAW_DIR/docker-compose.extra.yml" -SANDBOX_COMPOSE_FILE="$OPENCLAW_DIR/docker-compose.sandbox.yml" -#PRIVATE_COMPOSE_FILE="$ROOT_DIR/docker-compose.override.private.yml" -#PUBLIC_COMPOSE_FILE="$ROOT_DIR/docker-compose.override.public.yml" - -IMAGE_NAME="${OPENCLAW_IMAGE:-ghcr.io/openclaw/openclaw:latest}" -EXTRA_MOUNTS="${OPENCLAW_EXTRA_MOUNTS:-}" -HOME_VOLUME_NAME="${OPENCLAW_HOME_VOLUME:-}" -DOCKER_SOCKET_PATH="${OPENCLAW_DOCKER_SOCKET:-}" -ENV_FILE="$OPENCLAW_DIR/.env" -RAW_SANDBOX_SETTING="${OPENCLAW_SANDBOX:-}" -SANDBOX_ENABLED="" -TIMEZONE="${OPENCLAW_TZ:-}" -RAW_SKIP_ONBOARDING="${OPENCLAW_SKIP_ONBOARDING:-}" -SKIP_ONBOARDING="" -PERMISSIONS="${PERMISSIONS:-}" - -HOME_DIR="$HOME" -[[ "$PLATFORM" == "wsl" ]] && \ -HOME_DIR="$(wslpath "$(cmd.exe /c "/dev/null)")" -OPENCLAW_CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$HOME_DIR/.openclaw}" -log_info "${BODY}OPENCLAW_CONFIG_DIR:${END} ${WHITE}$OPENCLAW_CONFIG_DIR" -OPENCLAW_WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$HOME_DIR/.openclaw/workspace}" -log_info "${BODY}OPENCLAW_WORKSPACE_DIR:${END} ${WHITE}$OPENCLAW_WORKSPACE_DIR" -OPENCLAW_GATEWAY_PORT="${OPENCLAW_GATEWAY_PORT:-18789}" -OPENCLAW_GATEWAY_BIND="${OPENCLAW_GATEWAY_BIND:-lan}" - -OPENCLAW_DOCKER_GID="" -OPENCLAW_INSTALL_DOCKER_CLI="" - -######################################## -# ARGUMENT PARSING (single primary arg) -######################################## - -if [[ $# -gt 0 ]]; then - log_info "${HEADER}OpenClaw - Arguments" - #------------------------------------------- - PRIMARY_ARG="$1" - log_info "${BODY}Primary Argument:${END} ${WHITE}$PRIMARY_ARG" - shift || true -fi - -case "$PRIMARY_ARG" in - --setenv) ENV_MODE="1" ;; - --onboarding|--configuration|--gateway) : ;; - *) echo -e "${ERROR} ${RED}Unknown primary argument.${END}" >&2 ;; -esac - -# Parse remaining flags -ENVIRONMENT="${ENVIRONMENT:-private}" -while [ $# -gt 0 ]; do - case "$1" in - --debug) DEBUG_ON="true" ;; - --sandbox) RAW_SANDBOX_SETTING="1" ;; - # --environment) - # shift - # if [[ $# -eq 0 ]]; then - # echo -e "${WARNING} ${YELLOW}--environment option requires 'public/private' argument.${END}" - # else - # ENVIRONMENT="$1" - # fi - # ;; - *) - echo -e "${NOTICE} Extra argument ignored: ${WHITE}$1${END}" >&2 - ;; - esac - shift -done - -######################################## -# UTILITIES -######################################## - -require_cmd() { - if ! command -v "$1" >/dev/null 2>&1; then - fail "Missing dependency: $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 -} - -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 -} - -run_docker_image() { - if [[ "$IMAGE_NAME" == "openclaw:local" ]]; then - log_info "${BLUE}==>${END} ${GREEN}Building Docker image:${END} ${WHITE}$IMAGE_NAME" - run_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 "$OPENCLAW_DIR/Dockerfile" \ - "$OPENCLAW_DIR" - else - log_info "${BLUE}==>${END} ${GREEN}Pulling Docker image:${END} ${WHITE}$IMAGE_NAME" - if ! docker pull "$IMAGE_NAME"; then - fail "Failed to pull image $IMAGE_NAME. Please check the image name and your access permissions." - 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. -# -# 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. -fix_permissions() { - log_info "" - log_info "${BLUE}==>${END} ${GREEN}Fixing data-directory permissions" - run_prestart_gateway --user root --entrypoint sh openclaw-gateway -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' - PERMISSIONS=1 -} - -sync_gateway_config() { - local allowed_origin_json="" - local current_allowed_origins="" - local batch_json="" - - if [[ "${OPENCLAW_GATEWAY_BIND}" != "loopback" ]]; then - allowed_origin_json="$(printf '["http://localhost:%s","http://127.0.0.1:%s"]' "$OPENCLAW_GATEWAY_PORT" "$OPENCLAW_GATEWAY_PORT")" - current_allowed_origins="$( - run_prestart_cli config get gateway.controlUi.allowedOrigins 2>/dev/null || true - )" - current_allowed_origins="${current_allowed_origins//$'\r'/}" - fi - - batch_json="$(printf '[{"path":"gateway.mode","value":"local"},{"path":"gateway.bind","value":"%s"}' "$OPENCLAW_GATEWAY_BIND")" - if [[ -n "$allowed_origin_json" ]]; then - if [[ -n "$current_allowed_origins" && "$current_allowed_origins" != "null" && "$current_allowed_origins" != "[]" ]]; then - log_info "${GREEN}Control UI allowlist already configured; leaving gateway.controlUi.allowedOrigins unchanged." - else - batch_json+=",{\"path\":\"gateway.controlUi.allowedOrigins\",\"value\":$allowed_origin_json}" - fi - fi - batch_json+="]" - - if ! run_prestart_cli config set --batch-json "$batch_json" >/dev/null; then - : #fail "Could not complete run configuration" - fi - log_info "${GREEN}Pinned gateway.mode=local and gateway.bind=${END}${WHITE}$OPENCLAW_GATEWAY_BIND for Docker setup." - if [[ -n "$allowed_origin_json" ]]; then - if [[ -z "$current_allowed_origins" || "$current_allowed_origins" == "null" || "$current_allowed_origins" == "[]" ]]; then - log_info "${GREEN}Set gateway.controlUi.allowedOrigins to $allowed_origin_json for non-loopback bind." - fi - fi -} - -######################################## -# QUERY MODE -######################################## - -is_env_mode() { - if is_truthy_value "$ENV_MODE"; then - log_info "${HEADER}OpenClaw - Set .env" - #------------------------------------------- - return 0 - fi - return 1 -} - -upsert_env() { - local file="$1" - shift - local -a keys=("$@") - local tmp - tmp="$(mktemp)" - # Use a delimited string instead of an associative array so the script - # works with Bash 3.2 (macOS default) which lacks `declare -A`. - local seen=" " - - if [[ -f "$file" ]]; then - while IFS= read -r line || [[ -n "$line" ]]; do - local key="${line%%=*}" - local replaced=false - for k in "${keys[@]}"; do - if [[ "$key" == "$k" ]]; then - printf '%s=%s\n' "$k" "${!k-}" >>"$tmp" - log_info "${BODY}$k:${END} ${WHITE}${!k-}" - 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" - log_info "${BODY}$k:${END} ${WHITE}${!k-}" - fi - done - - mv "$tmp" "$file" -} - -run_set_env() { - export OPENCLAW_CONFIG_DIR - export OPENCLAW_WORKSPACE_DIR - export OPENCLAW_DISABLE_BONJOUR="${OPENCLAW_DISABLE_BONJOUR:-}" - 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_DOCKER_SETUP=1 - export OPENCLAW_TZ="$TIMEZONE" - export COMPOSE_IGNORE_ORPHANS="${COMPOSE_IGNORE_ORPHANS:-'true'}" - export OTEL_EXPORTER_OTLP_ENDPOINT="${OTEL_EXPORTER_OTLP_ENDPOINT:-}" - export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-}" - export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="${OTEL_EXPORTER_OTLP_METRICS_ENDPOINT:-}" - export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT="${OTEL_EXPORTER_OTLP_LOGS_ENDPOINT:-}" - export OTEL_EXPORTER_OTLP_PROTOCOL="${OTEL_EXPORTER_OTLP_PROTOCOL:-}" - export OTEL_SERVICE_NAME="${OTEL_SERVICE_NAME:-}" - export OTEL_SEMCONV_STABILITY_OPT_IN="${OTEL_SEMCONV_STABILITY_OPT_IN:-}" - export OPENCLAW_OTEL_PRELOADED="${OPENCLAW_OTEL_PRELOADED:-}" - export OPENCLAW_SKIP_ONBOARDING="$SKIP_ONBOARDING" - - upsert_env "$ENV_FILE" \ - OPENCLAW_CONFIG_DIR \ - OPENCLAW_WORKSPACE_DIR \ - OPENCLAW_DISABLE_BONJOUR \ - OPENCLAW_GATEWAY_TOKEN \ - OPENCLAW_EXTRA_MOUNTS \ - OPENCLAW_HOME_VOLUME \ - OPENCLAW_DOCKER_APT_PACKAGES \ - OPENCLAW_EXTENSIONS \ - OPENCLAW_SANDBOX \ - OPENCLAW_DOCKER_SOCKET \ - OPENCLAW_DOCKER_GID \ - OPENCLAW_INSTALL_DOCKER_CLI \ - OPENCLAW_ALLOW_INSECURE_PRIVATE_WS \ - OPENCLAW_TZ \ - COMPOSE_IGNORE_ORPHANS \ - OTEL_EXPORTER_OTLP_ENDPOINT \ - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT \ - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT \ - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT \ - OTEL_EXPORTER_OTLP_PROTOCOL \ - OTEL_SERVICE_NAME \ - OTEL_SEMCONV_STABILITY_OPT_IN \ - OPENCLAW_OTEL_PRELOADED \ - OPENCLAW_SKIP_ONBOARDING - completion='Success!' -} - -######################################## -# EXEC MODE -######################################## - -# Dockerfile uses BuildKit-only syntax (RUN --mount=type=cache). Force -# BuildKit so hosts defaulting to the legacy builder do not fail. -run_docker_build() { - docker_build_exec "$@" -} - -run_prestart_gateway() { - docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps "$@" -} - -# During setup, avoid the shared-network openclaw-cli service because it -# requires the gateway container's network namespace to already exist. That -# creates a circular dependency for config writes that are needed before the -# gateway can start cleanly. -run_prestart_cli() { - run_prestart_gateway --entrypoint node openclaw-gateway \ - dist/index.js "$@" -} - -run_runtime_cli() { - local compose_scope="${1:-current}" - local deps_mode="${2:-with-deps}" - shift 2 - - local -a compose_args - local -a run_args=(run --rm) - - case "$compose_scope" in - current) compose_args=("${COMPOSE_ARGS[@]}") ;; - base) compose_args=("${BASE_COMPOSE_ARGS[@]}") ;; - *) fail "Unknown runtime CLI compose scope: $compose_scope" ;; - esac - - case "$deps_mode" in - with-deps) ;; - no-deps) run_args+=(--no-deps) ;; - *) fail "Unknown runtime CLI deps mode: $deps_mode" ;; - esac - - docker compose "${compose_args[@]}" "${run_args[@]}" openclaw-cli "$@" -} - -run_onboarding() { - if [[ -n "$SKIP_ONBOARDING" ]]; then - log_info "" - log_info "${BLUE}==>${END} ${GREEN}Skip onboarding (OPENCLAW_SKIP_ONBOARDING is set)" - return - fi - - if ! fix_permissions; then - log_error "Could not fix permissions for run onboarding." - fi - log_info "" - log_info "${BLUE}==>${END} ${GREEN}Onboarding (interactive)" - log_info "${GREEN}Docker setup pins Gateway mode to local." - log_info "${GREEN}Gateway runtime bind comes from OPENCLAW_GATEWAY_BIND (default: lan)." - log_info "${GREEN}Current runtime bind:${END} ${WHITE}$OPENCLAW_GATEWAY_BIND" - if is_truthy_value "$OPENCLAW_DISABLE_BONJOUR"; then - log_info "${GREEN}Bonjour/mDNS advertising:${END} ${WHITE}force disabled (OPENCLAW_DISABLE_BONJOUR=$OPENCLAW_DISABLE_BONJOUR)." - elif [[ -z "$OPENCLAW_DISABLE_BONJOUR" ]]; then - log_info "${GREEN}Bonjour/mDNS advertising:${END} ${WHITE}auto (disabled inside the Gateway container unless explicitly enabled)." - else - log_info "${GREEN}Bonjour/mDNS advertising:${END} ${WHITE}explicitly enabled (OPENCLAW_DISABLE_BONJOUR=$OPENCLAW_DISABLE_BONJOUR)." - fi - log_info "${GREEN}Gateway token:${END} ${WHITE}$OPENCLAW_GATEWAY_TOKEN" - log_info "${GREEN}Tailscale exposure:${END} ${WHITE}Off (use host-level tailnet/Tailscale setup separately)." - log_info "${GREEN}Install Gateway daemon:${END} ${WHITE}No (managed by Docker Compose)" - log_info "" - run_prestart_cli onboard --mode local --no-install-daemon -} - -run_configuration() { - [[ -z "$PERMISSIONS" ]] && \ - if ! fix_permissions; then - log_error "Could not fix permissions for run configuration." - fi - log_info "" - log_info "${BLUE}==>${END} ${GREEN}Docker gateway defaults" - sync_gateway_config - completion='Success!' -} - -run_gateway() { - [[ -z "$PERMISSIONS" ]] && \ - if ! fix_permissions; then - log_error "Could not fix permissions for run gateway." - fi - log_info "" - log_info "${BLUE}==>${END} ${GREEN}Provider setup (optional)" - log_info "${GREEN}WhatsApp (QR):" - log_info " ${WHITE}${COMPOSE_HINT} run --rm openclaw-cli channels login" - log_info "${GREEN}Telegram (bot token):" - log_info " ${WHITE}${COMPOSE_HINT} run --rm openclaw-cli channels add --channel telegram --token " - log_info "${GREEN}Discord (bot token):" - log_info " ${WHITE}${COMPOSE_HINT} run --rm openclaw-cli channels add --channel discord --token " - log_info "${GREEN}Docs:${END} ${WHITE}https://docs.openclaw.ai/channels" - log_info "" - log_info "${BLUE}==>${END} ${GREEN}Starting gateway" - docker compose "${COMPOSE_ARGS[@]}" up -d openclaw-gateway - - if [[ -n "$SANDBOX_ENABLED" ]]; then - enable_sandbox - fi - log_info "" - log_info "${GREEN}Gateway running with host port mapping." - log_info "${GREEN}Access from tailnet devices via the host's tailnet IP." - log_info "${GREEN}Config:${END} ${WHITE}$OPENCLAW_CONFIG_DIR" - log_info "${GREEN}Workspace:${END} ${WHITE}$OPENCLAW_WORKSPACE_DIR" - log_info "${GREEN}Token:${END} ${WHITE}$OPENCLAW_GATEWAY_TOKEN" - log_info "" - log_info "${GREEN}Commands:" - log_info " ${WHITE}${COMPOSE_HINT} logs -f openclaw-gateway" - log_info " ${WHITE}${COMPOSE_HINT} exec openclaw-gateway node dist/index.js health --token \"$OPENCLAW_GATEWAY_TOKEN\"" - completion='Success!' -} - -######################################## -# EXTRA COMPOSE -######################################## - -write_extra_compose() { - local home_volume="$1" - shift || true - - local mount - local gateway_home_mount="" - local gateway_config_mount="" - local gateway_workspace_mount="" - - cat >"$EXTRA_COMPOSE_FILE" <<'YAML' -services: - openclaw-gateway: - volumes: -YAML - - ######################################## - # Home + core mounts - ######################################## - - 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" - - # shellcheck disable=SC2129 - 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 - - ######################################## - # Extra mounts - ######################################## - - for mount in "$@"; do - [[ -z "$mount" ]] && continue - validate_mount_spec "$mount" - printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE" - done - - ######################################## - # CLI service (mirror mounts) - ######################################## - - cat >>"$EXTRA_COMPOSE_FILE" <<'YAML' - openclaw-cli: - volumes: -YAML - - if [[ -n "$home_volume" ]]; then - # shellcheck disable=SC2129 - 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 - [[ -z "$mount" ]] && continue - validate_mount_spec "$mount" - printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE" - done - - ######################################## - # Named volume declaration (if needed) - ######################################## - - if [[ -n "$home_volume" && "$home_volume" != *"/"* ]]; then - validate_named_volume "$home_volume" - - cat >>"$EXTRA_COMPOSE_FILE" <${END} ${WHITE}Sandbox setup" - - # Build sandbox image if Dockerfile.sandbox exists. - if [[ -f "$OPENCLAW_DIR/Dockerfile.sandbox" ]]; then - log_info "${GREEN}Building sandbox image:${END} ${WHITE}openclaw-sandbox:bookworm-slim" - run_docker_build \ - -t "openclaw-sandbox:bookworm-slim" \ - -f "$OPENCLAW_DIR/Dockerfile.sandbox" \ - "$OPENCLAW_DIR" - else - log_warning "${YELLOW}Dockerfile.sandbox not found in $OPENCLAW_DIR" >&2 - log_notice " Sandbox config will be applied but no sandbox image will be built." >&2 - log_notice " 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 - log_warning "${YELLOW}Docker CLI not found inside the container image." >&2 - log_notice " Sandbox requires Docker CLI. Rebuild with --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1" >&2 - log_notice " or use a local build (OPENCLAW_IMAGE=openclaw:local). Skipping sandbox setup." >&2 - SANDBOX_ENABLED="" - 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="$OPENCLAW_DIR/docker-compose.sandbox.yml" - cat >"$SANDBOX_COMPOSE_FILE" <>"$SANDBOX_COMPOSE_FILE" <${END} ${GREEN}Sandbox:${END} ${WHITE}added Docker socket mount" - else - log_warning "${YELLOW}OPENCLAW_SANDBOX enabled but Docker socket not found at $DOCKER_SOCKET_PATH." >&2 - log_notice " 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 ! run_runtime_cli current no-deps \ - config set agents.defaults.sandbox.mode "non-main" >/dev/null; then - log_warning "${YELLOW}Failed to set agents.defaults.sandbox.mode" >&2 - sandbox_config_ok=false - fi - if ! run_runtime_cli current no-deps \ - config set agents.defaults.sandbox.scope "agent" >/dev/null; then - log_warning "${YELLOW}Failed to set agents.defaults.sandbox.scope" >&2 - sandbox_config_ok=false - fi - if ! run_runtime_cli current no-deps \ - config set agents.defaults.sandbox.workspaceAccess "none" >/dev/null; then - log_warning "${YELLOW}Failed to set agents.defaults.sandbox.workspaceAccess" >&2 - sandbox_config_ok=false - fi - - if [[ "$sandbox_config_ok" == true ]]; then - log_info "${GREEN}Sandbox enabled:${END} ${WHITE}mode=non-main, scope=agent, workspaceAccess=none" - log_info "${GREEN}Docs:${END} ${WHITE}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 - log_warning "${YELLOW}Sandbox config was partially applied. Check errors above." >&2 - log_info " ${YELLOW}Skipping gateway restart to avoid exposing Docker socket without a full sandbox policy." >&2 - if ! run_runtime_cli base no-deps \ - config set agents.defaults.sandbox.mode "off" >/dev/null; then - log_warning "${YELLOW}Failed to roll back agents.defaults.sandbox.mode to off" >&2 - else - log_info "${MAGENTA}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 ! run_runtime_cli current with-deps \ - config set agents.defaults.sandbox.mode "off" >/dev/null; then - log_warning "${YELLOW}Failed to reset agents.defaults.sandbox.mode to off" >&2 - fi - if [[ -f "$OPENCLAW_DIR/docker-compose.sandbox.yml" ]]; then - rm -f "$OPENCLAW_DIR/docker-compose.sandbox.yml" - fi - fi -} - -######################################## -# STATE -######################################## - -require_cmd docker -if ! docker compose version >/dev/null 2>&1; then - fail "Docker Compose not available (try: docker compose version)" -fi - -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" - log_info "${BODY}Reusing gateway token from:${END} ${WHITE}$OPENCLAW_CONFIG_DIR/openclaw.json" - else - DOTENV_GATEWAY_TOKEN="$(read_env_gateway_token "$OPENCLAW_DIR/.env" || true)" - if [[ -n "$DOTENV_GATEWAY_TOKEN" ]]; then - OPENCLAW_GATEWAY_TOKEN="$DOTENV_GATEWAY_TOKEN" - log_info "${BODY}Reusing gateway token from:${END} ${WHITE}$OPENCLAW_DIR/.env" - elif command -v openssl >/dev/null 2>&1; then - log_info "${BODY}Generating gateway token from:${END} ${WHITE}openssl rand -hex 32" - OPENCLAW_GATEWAY_TOKEN="$(openssl rand -hex 32)" - else - log_info "${BODY}Generating gateway token from:${END} ${WHITE}Python secrets.token_hex(32)" - OPENCLAW_GATEWAY_TOKEN="$(python3 - <<'PY' -import secrets -print(secrets.token_hex(32)) -PY -)" - fi - fi -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 - -if is_truthy_value "$RAW_SKIP_ONBOARDING"; then - SKIP_ONBOARDING="1" -fi - -# Detect Docker socket GID for sandbox group_add. -if [[ -n "$SANDBOX_ENABLED" && -S "$DOCKER_SOCKET_PATH" ]]; then - OPENCLAW_DOCKER_GID="$(stat -c '%g' "$DOCKER_SOCKET_PATH" 2>/dev/null || echo "")" -fi - -# When sandbox is requested, ensure Docker CLI build arg is set for local builds. -# Docker socket mount is deferred until sandbox prerequisites are verified. -if [[ -n "$SANDBOX_ENABLED" ]]; then - if [[ -z "${OPENCLAW_INSTALL_DOCKER_CLI:-}" ]]; then - OPENCLAW_INSTALL_DOCKER_CLI=1 - fi -fi - -if is_env_mode; then - run_set_env - exit 0 -else - log_info "${HEADER}OpenClaw - Execution Mode" - #------------------------------------------- -fi - -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 [[ -n "$EXTRA_MOUNTS" ]]; then - if contains_disallowed_chars "$EXTRA_MOUNTS"; then - fail "OPENCLAW_EXTRA_MOUNTS cannot contain control characters." - fi -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 early 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" - -COMPOSE_FILES=("$COMPOSE_FILE") -COMPOSE_ARGS=("-p" "ai-suite") -# if [[ "$ENVIRONMENT" == "public" ]]; then -# COMPOSE_FILES+=("$PUBLIC_COMPOSE_FILE") -# else -# COMPOSE_FILES+=("$PRIVATE_COMPOSE_FILE") -# fi - -VALID_MOUNTS=() -if [[ -n "$EXTRA_MOUNTS" ]]; then - IFS=',' read -r -a mounts <<<"$EXTRA_MOUNTS" - for mount in "${mounts[@]}"; do - mount="${mount#"${mount%%[![:space:]]*}"}" - mount="${mount%"${mount##*[![:space:]]}"}" - if [[ -n "$mount" ]]; then - VALID_MOUNTS+=("$mount") - fi - done -fi - -if [[ -n "$HOME_VOLUME_NAME" || ${#VALID_MOUNTS[@]} -gt 0 ]]; then - # Bash 3.2 + nounset treats "${array[@]}" on an empty array as unbound. - if [[ ${#VALID_MOUNTS[@]} -gt 0 ]]; then - write_extra_compose "$HOME_VOLUME_NAME" "${VALID_MOUNTS[@]}" - else - write_extra_compose "$HOME_VOLUME_NAME" - fi - COMPOSE_FILES+=("$EXTRA_COMPOSE_FILE") -fi -for compose_file in "${COMPOSE_FILES[@]}"; do - COMPOSE_ARGS+=("-f" "$compose_file") -done -# Keep a base compose arg set without sandbox overlay so rollback paths can -# force a known-safe gateway service definition (no docker.sock mount). -BASE_COMPOSE_ARGS=("${COMPOSE_ARGS[@]}") -COMPOSE_HINT="docker compose" -for compose_file in "${COMPOSE_FILES[@]}"; do - COMPOSE_HINT+=" -f ${compose_file}" -done - -######################################## -# DISPATCHER -######################################## - -run_docker_image - -case "$PRIMARY_ARG" in - --onboarding|"") - run_onboarding - run_configuration - ;; - --configuration) - run_configuration - ;; - --gateway) - run_gateway - ;; -esac diff --git a/suite_services.py b/suite_services.py index 99086ed..2a6b431 100644 --- a/suite_services.py +++ b/suite_services.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ Trevor SANDY -Last Update April 29, 2026 +Last Update May 03, 2026 Copyright (c) 2025-Present by Trevor SANDY AI-Suite uses this script for the installation command that handles the AI-Suite @@ -68,6 +68,7 @@ import queue import re import requests import secrets +import shlex import shutil import subprocess import tarfile @@ -324,8 +325,8 @@ def run_command(cmd, cwd=None, re_raise=None): cwd=cwd, check=True ) - if result.returncode != 0 and result.stderr: - log.error(f"{result.stderr.strip()}") + # if result.returncode != 0: + # log.error(f"Command return code: {result.returncode}") except Exception as e: log.error(f"Exception: {e}.") if re_raise: @@ -1442,7 +1443,7 @@ def check_llama_cpp_model(operation, env_vars, using_hf): log.info(f"Using {llama} model: {model_name}...") return model_name -def check_llama_process(operation=None, env_vars={}): +def check_llama_process(operation=None, env_vars=None): """Check for Ollama/LLaMA.cpp (on host) and attempt to launch if not running.""" if not attempted_launch: log.info(f"Checking for {llama} process on host...") @@ -1494,6 +1495,9 @@ def check_llama_process(operation=None, env_vars={}): sys.exit(1) log.info(f"{llama} is not running...", extra=log_bright) if start_llama: + if not env_vars: + log.critical("The env_vars dictionary is empty - exiting...") + sys.exit(1) if llama_found: log.info(f"Attempting to launch {llama} on host...") llama_args = [] @@ -1586,10 +1590,19 @@ def clone_openclaw_repo(): os.chdir(repo_dir) git("sparse-checkout", "init", "--cone") git("sparse-checkout", "set", - "scripts/docker/setup.sh", - "scripts/lib/docker-build.sh", - "scripts/clawdock", - "docs" + "apps", + "assets", + "docs", + "extensions", + "packages", + "patches", + "qa", + "scripts", + "security", + "skills", + "src", + "ui", + "vendor" ) git("checkout", "main") else: @@ -1684,7 +1697,7 @@ def prepare_supabase_env(env_vars): built_env_vars['COMPOSE_IGNORE_ORPHANS'] = 'true' write_dotenv_file(env_file, built_env_vars) -def prepare_openclaw_env(cwd): +def prepare_openclaw_env(cwd, sandbox=True): """ Creates a .env file from env.example with required values set. """ @@ -1700,12 +1713,20 @@ def prepare_openclaw_env(cwd): bridge_port = 18790 gateway_bind = "lan" gateway_token = secrets.token_hex(32) # 32 bytes -> 64 hex chars - openclaw_image = "ghcr.io/openclaw/openclaw:latest" gateway_password = secrets.token_hex(16) # 16 bytes -> 32 hex chars openapi_key = "llamacpp-local" if llama_cpp else "ollama-local" cwd = "./openclaw" if not cwd else cwd example_path = os.path.join(cwd, ".env.example") output_path=os.path.join(cwd, ".env") + openclaw_image = "ghcr.io/openclaw/openclaw:latest" + openclaw_image_comment = "Use a remote image instead of building locally" + openclaw_docker_cli = 0 + openclaw_sandbox = 0 + if sandbox: + openclaw_sandbox = 1 + openclaw_docker_cli = 1 + openclaw_image = "openclaw:local" + openclaw_image_comment = "Build image locally for sandbox" add_home_dir = True add_config_dir = True add_config_path = True @@ -1715,18 +1736,11 @@ def prepare_openclaw_env(cwd): add_bridge_port = True add_gateway_bind = True add_gateway_token = True - add_remote_image = True + add_openclaw_image = True add_gateway_password = True add_openai_api_key = True - add_extra_mounts = True - add_home_volume = True add_sandbox = True - add_docker_socket = True - add_docker_gid = True - add_install_docker_cli = True - add_timezone = True - sandbox = False - update_env = False + add_docker_cli = True log.info(f"Writing .env file to {output_path}...") debug_style = LSHF.style(logging.WARNING) try: @@ -1782,15 +1796,29 @@ def prepare_openclaw_env(cwd): modified_lines.append(line) elif modified_line.startswith("OPENCLAW_GATEWAY_TOKEN="): add_gateway_password = False + gateway_password = None key_value = modified_line.split('=', 1) if len(key_value) == 2 and not key_value[1]: add_gateway_token = False modified_lines.append(f"OPENCLAW_GATEWAY_TOKEN={gateway_token}\n") - log.debug(f"OPENCLAW_GATEWAY_TOKEN={elide(gateway_token)}", extra=debug_style) + else: + modified_lines.append(line) + elif modified_line.startswith("OPENCLAW_SANDBOX="): + key_value = modified_line.split('=', 1) + if len(key_value) == 2 and key_value[1]: + add_sandbox = False + modified_lines.append(f"OPENCLAW_SANDBOX={openclaw_sandbox}\n") + else: + modified_lines.append(line) + elif modified_line.startswith("OPENCLAW_INSTALL_DOCKER_CLI="): + key_value = modified_line.split('=', 1) + if len(key_value) == 2 and key_value[1]: + add_docker_cli = False + modified_lines.append(f"OPENCLAW_INSTALL_DOCKER_CLI={openclaw_docker_cli}\n") else: modified_lines.append(line) elif modified_line.startswith("OPENCLAW_IMAGE="): - add_remote_image = False + add_openclaw_image = False key_value = modified_line.split('=', 1) if len(key_value) == 2 and not key_value[1]: modified_lines.append(f"OPENCLAW_IMAGE={openclaw_image}\n") @@ -1798,11 +1826,11 @@ def prepare_openclaw_env(cwd): modified_lines.append(line) elif modified_line.startswith("OPENCLAW_GATEWAY_PASSWORD="): add_gateway_token = False + gateway_token = None key_value = modified_line.split('=', 1) if len(key_value) == 2 and not key_value[1]: add_gateway_password = False modified_lines.append(f"OPENCLAW_GATEWAY_PASSWORD={gateway_password}\n") - log.debug(f"OPENCLAW_GATEWAY_PASSWORD={elide(gateway_password)}", extra=debug_style) else: modified_lines.append(line) elif modified_line.startswith("OPENAI_API_KEY="): @@ -1812,45 +1840,10 @@ def prepare_openclaw_env(cwd): modified_lines.append(f"OPENAI_API_KEY={openapi_key}\n") else: modified_lines.append(line) - elif modified_line.startswith("OPENCLAW_EXTRA_MOUNTS="): - key_value = modified_line.split('=', 1) - if len(key_value) == 2 and key_value[1]: - add_extra_mounts = False - modified_lines.append(line) - elif modified_line.startswith("OPENCLAW_HOME_VOLUME="): - key_value = modified_line.split('=', 1) - if len(key_value) == 2 and key_value[1]: - add_home_volume = False - modified_lines.append(line) - elif modified_line.startswith("OPENCLAW_SANDBOX="): - key_value = modified_line.split('=', 1) - if len(key_value) == 2 and key_value[1]: - add_sandbox = False - modified_lines.append(line) - elif modified_line.startswith("OPENCLAW_DOCKER_SOCKET="): - key_value = modified_line.split('=', 1) - if len(key_value) == 2 and key_value[1]: - add_docker_socket = False - modified_lines.append(line) - elif modified_line.startswith("OPENCLAW_DOCKER_GID=") and sandbox: - key_value = modified_line.split('=', 1) - if len(key_value) == 2 and key_value[1]: - add_docker_gid = False - modified_lines.append(line) - elif modified_line.startswith("OPENCLAW_INSTALL_DOCKER_CLI=") and sandbox: - key_value = modified_line.split('=', 1) - if len(key_value) == 2 and key_value[1]: - add_install_docker_cli = False - modified_lines.append(line) - elif modified_line.startswith("OPENCLAW_TZ="): - key_value = modified_line.split('=', 1) - if len(key_value) == 2 and key_value[1]: - add_timezone = False - modified_lines.append(line) else: modified_lines.append(line) default_paths = add_config_dir or add_workspace_dir - if add_remote_image or default_paths: + if add_openclaw_image or default_paths: lines = modified_lines modified_lines = [] section_header = False @@ -1860,13 +1853,13 @@ def prepare_openclaw_env(cwd): section_header = True default_path_insert = modified_line.startswith("# Optional path overrides ") auto_configure_settings = modified_line.startswith("# Model provider API keys ") - if section_header and add_remote_image: + if section_header and add_openclaw_image: section_header = False - add_remote_image = False + add_openclaw_image = False modified_lines.append("# " + "-" * 77 + "\n") - modified_lines.append("# Prebuilt Image\n") + modified_lines.append("# OpenClaw Image\n") modified_lines.append("# " + "-" * 77 + "\n") - modified_lines.append("# Use a remote image instead of building locally.\n") + modified_lines.append(f"# {openclaw_image_comment}.\n") modified_lines.append(f"OPENCLAW_IMAGE={openclaw_image}\n") modified_lines.append("\n") modified_lines.append(line) @@ -1880,8 +1873,8 @@ def prepare_openclaw_env(cwd): modified_lines.append(f"OPENCLAW_WORKSPACE_DIR={workspace_dir}\n") elif add_gateway_password and modified_line.startswith("# OPENCLAW_GATEWAY_PASSWORD="): add_gateway_password = False + gateway_token = None modified_lines.append(f"OPENCLAW_GATEWAY_PASSWORD={gateway_password}\n") - log.debug(f"OPENCLAW_GATEWAY_PASSWORD={elide(gateway_password)}", extra=debug_style) elif add_openai_api_key and modified_line.startswith("# OPENAI_API_KEY="): add_openai_api_key = False modified_lines.append(f"OPENAI_API_KEY={openapi_key}\n") @@ -1902,6 +1895,11 @@ def prepare_openclaw_env(cwd): section_header = False modified_lines.append("# Auto-configure settings\n") modified_lines.append("# " + "-" * 77 + "\n") + modified_lines.append("COMPOSE_IGNORE_ORPHANS=true\n") + if add_sandbox and sandbox: + modified_lines.append(f"OPENCLAW_SANDBOX={openclaw_sandbox}\n") + if add_docker_cli and sandbox: + modified_lines.append(f"OPENCLAW_INSTALL_DOCKER_CLI={openclaw_docker_cli}\n") if add_gateway_port: modified_lines.append(f"OPENCLAW_GATEWAY_PORT={gateway_port}\n") if add_bridge_port: @@ -1909,29 +1907,8 @@ def prepare_openclaw_env(cwd): if add_gateway_bind: modified_lines.append(f"OPENCLAW_GATEWAY_BIND={gateway_bind}\n") if add_gateway_token: + gateway_password = None modified_lines.append(f"OPENCLAW_GATEWAY_TOKEN={gateway_token}\n") - log.debug(f"OPENCLAW_GATEWAY_TOKEN={elide(gateway_token)}", extra=debug_style) - if add_extra_mounts: - update_env = True - modified_lines.append("OPENCLAW_EXTRA_MOUNTS=\n") - if add_home_volume: - update_env = True - modified_lines.append("OPENCLAW_HOME_VOLUME=\n") - if add_sandbox: - update_env = True - modified_lines.append("OPENCLAW_SANDBOX=\n") - if add_docker_socket: - update_env = True - modified_lines.append("OPENCLAW_DOCKER_SOCKET=\n") - if add_docker_gid: - update_env = True - modified_lines.append("OPENCLAW_DOCKER_GID=\n") - if add_install_docker_cli: - update_env = True - modified_lines.append("OPENCLAW_INSTALL_DOCKER_CLI=\n") - if add_timezone: - update_env = True - modified_lines.append("OPENCLAW_TZ=\n") modified_lines.append("\n") modified_lines.append("# " + "-" * 77 + "\n") modified_lines.append(line) @@ -1946,24 +1923,13 @@ def prepare_openclaw_env(cwd): except Exception as e: log.error(f"Exception: OpenClaw Setup: {e}") return False - if update_env: - oc_script = os.path.normpath("openclaw_ctl") - if not os.path.exists(oc_script): - log.error(f"OpenClaw setup script not found at {oc_script}") - return - cmd = ["bash", "-c"] - if system == 'Windows': - convert_line_endings(oc_script) - oc_script = oc_script.replace("\\", "/") - oc_script = "".join(["./", oc_script]) - if system == "Windows": - cmd = ["wsl", "-e"] + cmd - cmd_args = ["--sandbox"] - env_cmd = cmd + [ - f'{oc_script} --setenv {" ".join(cmd_args)}' - ] - run_command(env_cmd) + if gateway_token: + log.debug(f"OPENCLAW_GATEWAY_TOKEN={elide(gateway_token)}", extra=debug_style) + if gateway_password: + log.debug(f"OPENCLAW_GATEWAY_PASSWORD={elide(gateway_password)}", extra=debug_style) + log.debug(f"OPENCLAW_SANDBOX={openclaw_sandbox}", extra=debug_style) log.debug(f"OPENCLAW_IMAGE={openclaw_image}", extra=debug_style) + log.debug(f"OPENCLAW_INSTALL_DOCKER_CLI={openclaw_docker_cli}", extra=debug_style) log.debug(f"OPENCLAW_HOME={home_dir}", extra=debug_style) log.debug(f"OPENCLAW_CONFIG_DIR={config_dir}", extra=debug_style) log.debug(f"OPENCLAW_WORKSPACE_DIR={workspace_dir}", extra=debug_style) @@ -1972,10 +1938,13 @@ def prepare_openclaw_env(cwd): log.debug(f"OPENCLAW_GATEWAY_BIND={gateway_bind}", extra=debug_style) log.debug(f"OPENAI_API_KEY={openapi_key}", extra=debug_style) log.info(f".env file created at {output_path}", extra=log_bright) - + _openclaw_add_compose_updates() + _openclaw_inject_env_vars_logging() + # if sandbox: + # _openclaw_enable_sandbox() return True -def prepare_openclaw_config(cwd, env_vars): +def prepare_openclaw_config(cwd, env_vars, onboard_store): log.info("Starting OpenClaw config preparation...") src_path = pathlib.Path("./.openclaw.example.json") dst_dir = pathlib.Path.home() / ".openclaw" @@ -1987,18 +1956,20 @@ def prepare_openclaw_config(cwd, env_vars): config = json.load(f) cwd = "./openclaw" if not cwd else cwd oc_env_file=os.path.join(cwd, ".env") - oc_env_vars = get_dotenv_vars(oc_env_file) - if not oc_env_vars: + if not onboard_store['b']: + set_dotenv_var(oc_env_file, 'OPENCLAW_SKIP_ONBOARDING', '1', None) + oc_env : dict[str, str] = get_dotenv_vars(oc_env_file) + if not oc_env: log.error("No OpenClaw environment variables detected!") return if env_vars is None: log.error("The env_vars dictionary is empty!") return - token = oc_env_vars.get("OPENCLAW_GATEWAY_TOKEN") + token = oc_env.get("OPENCLAW_GATEWAY_TOKEN") log.info("Updating gateway token") config["gateway"]["auth"]["token"] = token - openapi_key = oc_env_vars.get("OPENAI_API_KEY") + openapi_key = oc_env.get("OPENAI_API_KEY") if llama_cpp: log.info("Configuring llama.cpp provider") @@ -2126,6 +2097,156 @@ def prepare_openclaw_config(cwd, env_vars): raise log.info("OpenClaw .json configuration complete") +def _openclaw_add_compose_updates(setup_path=None): + """ + Set compose project to ai-suite and update installed Docker CLI check. + """ + if setup_path is None: + setup_path = "./openclaw/scripts/docker/setup.sh" + path = pathlib.Path(setup_path) + if not path.exists(): + raise FileNotFoundError(f"{setup_path} not found") + try: + with open(path, "r", newline="\n") as f: + lines = f.readlines() + updated_lines: list[str] = [] + # Set compose project argument + compose_args = False + compose_project = 'COMPOSE_ARGS=("-p" "ai-suite")' + for line in lines: + stripped = line.lstrip() + if stripped.startswith(compose_project): + updated_lines = lines + compose_args = True + break + if stripped.startswith('COMPOSE_ARGS=()'): + updated_lines.append(f'{compose_project}\n') + compose_args = True + else: + updated_lines.append(line) + if compose_args: + log.info(f"Add compose project in {path}", extra=log_bright) + else: + log.error(f"COMPOSE_ARGS=() not found in {path}", extra=log_bright) + # Update installed Docker CLI check + lines = updated_lines + updated_lines = [] + check_cli = False + cli_check = 'if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --entrypoint docker openclaw-gateway --version' + cli_check_update = 'elif ! docker compose "${COMPOSE_ARGS[@]}" exec -T openclaw-gateway docker --version' + for line in lines: + stripped = line.lstrip() + if stripped.startswith(cli_check_update): + updated_lines = lines + check_cli = True + break + if stripped.startswith(cli_check): + check_cli = True + updated_lines.append(' if ! docker compose "${COMPOSE_ARGS[@]}" ps --status running | grep -q openclaw-gateway; then\n') + updated_lines.append(' echo "WARNING: openclaw-gateway is not running. Skipping sandbox setup." >&2\n') + updated_lines.append(' SANDBOX_ENABLED=""\n') + updated_lines.append(f' {cli_check_update} >/dev/null 2>&1; then\n') + else: + updated_lines.append(line) + if check_cli: + log.info(f"Update Docker CLI ckeck in {path}", extra=log_bright) + else: + log.error(f"Docker CLI ckeck not found in {path}") + with open(path, "w", newline="\n") as f: + f.writelines(updated_lines) + log.info(f"Perform compose updates in {path}", extra=log_bright) + except Exception as e: + log.error(f"Exception: OpenClaw compose updates: {e}") + +def _openclaw_inject_env_vars_logging(setup_path=None): + """ + Inject logging statements to display env vars. + """ + if setup_path is None: + setup_path = "./openclaw/scripts/docker/setup.sh" + path = pathlib.Path(setup_path) + if not path.exists(): + raise FileNotFoundError(f"{setup_path} not found") + try: + with open(path, "r", newline="\n") as f: + lines = f.readlines() + updated_lines: list[str] = [] + inject_env_logging = False + performed_injection = False + for line in lines: + stripped = line.lstrip() + if stripped.startswith('# Injected environment variable logging.'): + updated_lines = lines + performed_injection = True + break + if stripped.startswith('upsert_env() {'): + inject_env_logging = True + updated_lines.append(line) + continue + if inject_env_logging: + if stripped.startswith('tmp="$(mktemp)"'): + updated_lines.append(line) + updated_lines.append(" # Injected environment variable logging.\n") + updated_lines.append(' echo "==> OpenClaw environment variables:"\n') + updated_lines.append(' echo " - ROOT_DIR: ${ROOT_DIR:-}"\n') + updated_lines.append(' echo " - COMPOSE_FILE: ${COMPOSE_FILE:-}"\n') + updated_lines.append(' echo " - EXTRA_COMPOSE_FILE: ${EXTRA_COMPOSE_FILE:-}"\n') + continue + elif stripped.startswith('mv "$tmp" "$file"'): + updated_lines.append(line) + inject_env_logging = False + continue + if stripped.startswith('if [[ "$key" == "$k" ]]; then'): + updated_lines.append(line) + updated_lines.append(' echo " - $k: ${!k-}"\n') + performed_injection = True + elif stripped.startswith('if [[ "$seen" != *" $k "* ]]; then'): + updated_lines.append(line) + updated_lines.append(' echo " - $k: ${!k-}"\n') + performed_injection = True + else: + updated_lines.append(line) + else: + updated_lines.append(line) + with open(path, "w", newline="\n") as f: + f.writelines(updated_lines) + if performed_injection: + log.info(f"OpenClaw add env vars logging in {path}", extra=log_bright) + else: + log.warning(f"Function upsert_env not found - skip logging injection.") + except Exception as e: + log.error(f"Exception: OpenClaw env vars logging: {e}") + +def _openclaw_enable_sandbox(compose_path=None): + """ + Enable OpenClaw sandbox-related settings in a docker-compose.yml file by + uncommenting the required lines. + """ + if compose_path is None: + compose_path = "./openclaw/docker-compose.yml" + path = pathlib.Path(compose_path) + if not path.exists(): + raise FileNotFoundError(f"{compose_path} not found") + try: + with open(path, "r", newline="\n") as f: + lines = f.readlines() + updated_lines: list[str] = [] + for line in lines: + stripped = line.lstrip() + if stripped.startswith("# - /var/run/docker.sock:/var/run/docker.sock"): + updated_lines.append(line.replace("# ", "", 1)) + elif stripped.startswith("# group_add:"): + updated_lines.append(line.replace("# ", "", 1)) + elif stripped.startswith('# - "${DOCKER_GID:-999}"'): + updated_lines.append(line.replace("# ", "", 1)) + else: + updated_lines.append(line) + with open(path, "w", newline="\n") as f: + f.writelines(updated_lines) + log.info(f"Sandbox enabled in {path}", extra=log_bright) + except Exception as e: + log.error(f"Exception: OpenClaw enable sandbox: {e}") + def _openclaw_normalize(name: str): if not isinstance(name, str): raise TypeError("Model name must be a string") @@ -2365,139 +2486,42 @@ def start_open_webui_tools_filesystem(environment=None, build=False): compose_file = "open-webui/tools/servers/filesystem/compose.yaml" start_built_container(compose_file, environment, build) -def start_openclaw( - onboard_store, - environment=None, - build=False, - cwd=None, - *, - non_interactive=False, - on_failure="continue", # "continue" | "quit" - on_max="continue", # "continue" | "quit" - ): - """Start the OpenClaw services (using its compose file).""" +def start_openclaw(onboard_store, build=False, cwd=None, oc_sandbox=None): + """Start the OpenClaw services (using its setup file).""" log.info("Starting OpenClaw services...") - oc_script = os.path.normpath("openclaw_ctl") - if not os.path.exists(oc_script): - log.error(f"OpenClaw setup script not found at {oc_script}") + oc_dir = "openclaw" + oc_script = os.path.normpath("scripts/docker/setup.sh") + oc_path = os.path.normpath(os.path.join(oc_dir, oc_script)) + if not os.path.exists(oc_path): + log.error(f"OpenClaw setup script not found at {oc_path}") return cmd = ["bash", "-c"] if system == 'Windows': - convert_line_endings(oc_script) - oc_script = oc_script.replace("\\", "/") - oc_prefix = "../" if cwd else "./" - oc_script = "".join([oc_prefix, oc_script]) - if system == "Windows": cmd = ["wsl", "-e"] + cmd - cmd_args = ["--sandbox"] - if build: - cmd_args.append("--build") - if onboard_store['b']: - # --- Onboarding + Configuration loop --- - onboarding_cmd = cmd + [ - f'{oc_script} --onboarding {" ".join(cmd_args)}' - ] - _openclaw_run_with_retries( - onboarding_cmd, - cwd, - "Onboarding", - non_interactive=non_interactive, - on_failure=on_failure, - on_max=on_max, - ) - else: - # --- Configuration loop --- - config_cmd = cmd + [ - f'{oc_script} --configuration {" ".join(cmd_args)}' - ] - _openclaw_run_with_retries( - config_cmd, - cwd, - "Configuration", - non_interactive=non_interactive, - on_failure=on_failure, - on_max=on_max, - ) - # --- Gateway start --- - if environment == "public": - cmd_args.extend(["--environment", "public"]) - gateway_cmd = cmd + [ - f'{oc_script} --gateway {" ".join(cmd_args)}' - ] - run_command(gateway_cmd, cwd=cwd) - -def _openclaw_prompt_max(action_name, non_interactive, on_max): - """Prompt user after max attempts reached.""" - if non_interactive: - log.info( - f"{action_name} reached max attempts → non-interactive mode: auto '{on_max}'." - ) - return on_max - while True: - choice = input( - f"{action_name} reached max attempts. " - "What do you want to do? [c]ontinue / [q]uit: " - ).strip().lower() - - if choice in ("c", "continue"): - return "continue" - elif choice in ("q", "quit"): - return "quit" - -def _openclaw_prompt_retry(action_name, non_interactive, on_failure): - """Prompt user after a failed attempt (before max attempts).""" - if non_interactive: - log.info( - f"{action_name} failed → non-interactive mode: auto '{on_failure}'." - ) - return on_failure - while True: - choice = input( - f"{action_name} failed. What do you want to do? " - "[r]etry / [c]ontinue / [q]uit: " - ).strip().lower() - if choice in ("r", "retry"): - return "retry" - elif choice in ("c", "continue"): - return "continue" - elif choice in ("q", "quit"): - return "quit" - -def _openclaw_run_with_retries( - cmd, - cwd, - action_name, - max_attempts=3, - non_interactive=False, - on_failure="continue", # or "quit" - on_max="continue", # or "quit" - ): - """Run a command with controlled retry behavior for run_command that raises exceptions on failure.""" - attempts = 0 - while attempts < max_attempts: - try: - run_command(cmd, cwd=cwd, re_raise=True) - log.info(f"{action_name} completed successfully.") - return 0 # success → exit loop - except Exception as e: - attempts += 1 - log.warning(f"{action_name} attempt {attempts} failed {e}") - if attempts < max_attempts: - decision = _openclaw_prompt_retry(action_name, non_interactive, on_failure) - if decision == "retry": - continue - elif decision == "continue": - return 1 - elif decision == "quit": - raise SystemExit(f"{action_name} aborted by user.") - else: - log.error(f"{action_name} reached max attempts ({max_attempts}).") - decision = _openclaw_prompt_max(action_name, non_interactive, on_max) - if decision == "continue": - return 1 - elif decision == "quit": - raise SystemExit(f"{action_name} aborted after max attempts.") - return 1 # fallback, should not reach here + convert_line_endings(oc_path) + oc_script = oc_script.replace("\\", "/") + oc_env_file=os.path.join(oc_dir, ".env") + oc_env : dict[str, str] = get_dotenv_vars(oc_env_file) + if not oc_env: + log.error(f"OpenClaw .env file not found at {oc_env_file}") + env = {} + for key in ['OPENCLAW_IMAGE', 'OPENCLAW_CONFIG_DIR', 'OPENCLAW_WORKSPACE_DIR']: + val = oc_env.get(key) + if val is not None: + env[key] = val + if oc_sandbox: + for key in ['OPENCLAW_SANDBOX', 'OPENCLAW_INSTALL_DOCKER_CLI']: + val = oc_env.get(key) + if val is not None: + env[key] = val + if not onboard_store['b']: + val = oc_env.get('OPENCLAW_SKIP_ONBOARDING') + if val is not None: + env['OPENCLAW_SKIP_ONBOARDING'] = val + env_prefix = " ".join(f"{k}={shlex.quote(v)}" for k, v in env.items()) + cmd_str = f"{env_prefix} {oc_script}" if env_prefix else oc_script + oc_cmd = cmd + [cmd_str] + run_command(oc_cmd, cwd=cwd) def start_ai_suite(profile=None, environment=None, build=False): """Start the AI-Suite services (using its compose file) for the specified @@ -3158,13 +3182,15 @@ def docker_container_is_running(container): log.error(f"Exception: {e.stderr}.") return False -def display_service_endpoints(profile, supabase, openclaw, env_vars={}): +def display_service_endpoints(profile, supabase, openclaw, env_vars=None): """Display AI-Suite installation or operation status""" if not profile: log.error("Profile required to display service endpoints") return debug = log_level == logging.DEBUG + if env_vars is None: + env_vars = {} host = "localhost" if debug else env_vars.get('AC_DOMAIN', 'undefined') private = str(env_vars.get('AC_LOCAL')).lower() protocol = 'http' if private else 'https' @@ -3431,7 +3457,7 @@ def display_ac_env_vars(ac_env_vars): log.info(raw_msg, extra=env_var_style) log.info("="*60, extra=line_style) -def setup_ai_suite_ac_auto_config(prompt_store, onboard_store, env_vars:dict={}): +def setup_ai_suite_ac_auto_config(prompt_store, onboard_store, env_vars=None): """Setup env_vars for self-hosted AI-Suite with Caddy/Nginx proxy and Authelia 2FA identity and access management. """ @@ -3931,14 +3957,17 @@ def main(): # Setup OpenClaw repository if using OpenClaw openclaw = \ any(p for p in args.profile if p in ['openclaw', 'ai-all']) - ocwd = None + oc_cwd = None + oc_sandbox = None onboard_store = {'b':False} base_dir = pathlib.Path(__file__).parent if openclaw: if build: clone_openclaw_repo() - ocwd = os.path.join(base_dir, "openclaw") - prepare_openclaw_env(ocwd) + oc_cwd = os.path.join(base_dir, "openclaw") + env = os.getenv("OPENCLAW_SANDBOX") + oc_sandbox = True if env is None or int(env) == 1 else False + prepare_openclaw_env(oc_cwd, oc_sandbox) # Setup Open WebUI Functions and Tools Filesystem repository open_webui = \ @@ -4034,7 +4063,10 @@ def main(): ac_subdomains.append(profile) if ac_subdomains: ac_env_vars.append(f'AC_SUBDOMAINS="{" ".join(ac_subdomains)}"') - # Miscalleanous environment variables + # OpenClaw sandbox + if oc_sandbox: + ac_env_vars.append(f'AC_OPENCLAW_SANDBOX={str(True).lower()}') + # Miscellaneous environment variables ac_env_vars.append(f'AC_LLAMA={str(False).lower()}') ac_env_vars.append(f'AC_LLAMACPP={str(llama_cpp).lower()}') ac_env_vars.append(f'AC_SEARXNG={str(False).lower()}') @@ -4260,7 +4292,7 @@ def main(): # Setup OpenClaw configuration if openclaw: - prepare_openclaw_config(ocwd, env_vars) + prepare_openclaw_config(oc_cwd, env_vars, onboard_store) # Setup Open WebUI Functions and Tools Filesystem repos if open_webui: @@ -4289,11 +4321,11 @@ def main(): start_supabase(args.environment, build) # Give Supabase some time to initialize log.info("Waiting for Supabase to initialize...", extra=log_bright) - wait_with_progress(10) + wait_with_progress(5) # Start OpenClaw if openclaw: - start_openclaw(onboard_store, args.environment, build, ocwd) + start_openclaw(onboard_store, build, oc_cwd, oc_sandbox) # Give OpenClaw some time to initialize log.info("Waiting for OpenClaw to initialize...", extra=log_bright) wait_with_progress(5)