mirror of
https://github.com/trevorsandy/ai-suite.git
synced 2026-09-12 20:57:59 +02:00
1183 lines
37 KiB
Bash
1183 lines
37 KiB
Bash
#!/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 <distro> 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 "<nul set /p=%USERPROFILE%" 2>/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 <token>"
|
|
log_info "${GREEN}Discord (bot token):"
|
|
log_info " ${WHITE}${COMPOSE_HINT} run --rm openclaw-cli channels add --channel discord --token <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" <<YAML
|
|
volumes:
|
|
${home_volume}:
|
|
YAML
|
|
fi
|
|
}
|
|
|
|
########################################
|
|
# SANDBOX MODULE (ISOLATED + SAFE)
|
|
########################################
|
|
|
|
enable_sandbox() {
|
|
log_info ""
|
|
# --- Sandbox setup (opt-in via OPENCLAW_SANDBOX=1) ---
|
|
log_info "${BLUE}==>${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" <<YAML
|
|
services:
|
|
openclaw-gateway:
|
|
volumes:
|
|
- ${DOCKER_SOCKET_PATH}:/var/run/docker.sock
|
|
YAML
|
|
if [[ -n "${OPENCLAW_DOCKER_GID:-}" ]]; then
|
|
cat >>"$SANDBOX_COMPOSE_FILE" <<YAML
|
|
group_add:
|
|
- "${OPENCLAW_DOCKER_GID}"
|
|
YAML
|
|
fi
|
|
COMPOSE_ARGS+=("-f" "$SANDBOX_COMPOSE_FILE")
|
|
log_info "${BLUE}==>${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
|