refactor(scripts): move container setup entrypoints
This commit is contained in:
parent
3b79494cbf
commit
46ccbacbd9
3
.github/labeler.yml
vendored
3
.github/labeler.yml
vendored
@ -165,7 +165,10 @@
|
|||||||
- "Dockerfile.*"
|
- "Dockerfile.*"
|
||||||
- "docker-compose.yml"
|
- "docker-compose.yml"
|
||||||
- "docker-setup.sh"
|
- "docker-setup.sh"
|
||||||
|
- "setup-podman.sh"
|
||||||
- ".dockerignore"
|
- ".dockerignore"
|
||||||
|
- "scripts/docker/setup.sh"
|
||||||
|
- "scripts/podman/setup.sh"
|
||||||
- "scripts/**/*docker*"
|
- "scripts/**/*docker*"
|
||||||
- "scripts/**/Dockerfile*"
|
- "scripts/**/Dockerfile*"
|
||||||
- "scripts/sandbox-*.sh"
|
- "scripts/sandbox-*.sh"
|
||||||
|
|||||||
612
docker-setup.sh
612
docker-setup.sh
@ -2,615 +2,11 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
COMPOSE_FILE="$ROOT_DIR/docker-compose.yml"
|
SCRIPT_PATH="$ROOT_DIR/scripts/docker/setup.sh"
|
||||||
EXTRA_COMPOSE_FILE="$ROOT_DIR/docker-compose.extra.yml"
|
|
||||||
IMAGE_NAME="${OPENCLAW_IMAGE:-openclaw:local}"
|
|
||||||
EXTRA_MOUNTS="${OPENCLAW_EXTRA_MOUNTS:-}"
|
|
||||||
HOME_VOLUME_NAME="${OPENCLAW_HOME_VOLUME:-}"
|
|
||||||
RAW_SANDBOX_SETTING="${OPENCLAW_SANDBOX:-}"
|
|
||||||
SANDBOX_ENABLED=""
|
|
||||||
DOCKER_SOCKET_PATH="${OPENCLAW_DOCKER_SOCKET:-}"
|
|
||||||
TIMEZONE="${OPENCLAW_TZ:-}"
|
|
||||||
|
|
||||||
fail() {
|
if [[ ! -f "$SCRIPT_PATH" ]]; then
|
||||||
echo "ERROR: $*" >&2
|
echo "Docker setup script not found at $SCRIPT_PATH" >&2
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
require_cmd() {
|
|
||||||
if ! command -v "$1" >/dev/null 2>&1; then
|
|
||||||
echo "Missing dependency: $1" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
is_truthy_value() {
|
|
||||||
local raw="${1:-}"
|
|
||||||
raw="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
|
|
||||||
case "$raw" in
|
|
||||||
1 | true | yes | on) return 0 ;;
|
|
||||||
*) return 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
read_config_gateway_token() {
|
|
||||||
local config_path="$OPENCLAW_CONFIG_DIR/openclaw.json"
|
|
||||||
if [[ ! -f "$config_path" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if command -v python3 >/dev/null 2>&1; then
|
|
||||||
python3 - "$config_path" <<'PY'
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
|
|
||||||
path = sys.argv[1]
|
|
||||||
try:
|
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
|
||||||
cfg = json.load(f)
|
|
||||||
except Exception:
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
gateway = cfg.get("gateway")
|
|
||||||
if not isinstance(gateway, dict):
|
|
||||||
raise SystemExit(0)
|
|
||||||
auth = gateway.get("auth")
|
|
||||||
if not isinstance(auth, dict):
|
|
||||||
raise SystemExit(0)
|
|
||||||
token = auth.get("token")
|
|
||||||
if isinstance(token, str):
|
|
||||||
token = token.strip()
|
|
||||||
if token:
|
|
||||||
print(token)
|
|
||||||
PY
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if command -v node >/dev/null 2>&1; then
|
|
||||||
node - "$config_path" <<'NODE'
|
|
||||||
const fs = require("node:fs");
|
|
||||||
const configPath = process.argv[2];
|
|
||||||
try {
|
|
||||||
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
||||||
const token = cfg?.gateway?.auth?.token;
|
|
||||||
if (typeof token === "string" && token.trim().length > 0) {
|
|
||||||
process.stdout.write(token.trim());
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Keep docker-setup resilient when config parsing fails.
|
|
||||||
}
|
|
||||||
NODE
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
read_env_gateway_token() {
|
|
||||||
local env_path="$1"
|
|
||||||
local line=""
|
|
||||||
local token=""
|
|
||||||
if [[ ! -f "$env_path" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
||||||
line="${line%$'\r'}"
|
|
||||||
if [[ "$line" == OPENCLAW_GATEWAY_TOKEN=* ]]; then
|
|
||||||
token="${line#OPENCLAW_GATEWAY_TOKEN=}"
|
|
||||||
fi
|
|
||||||
done <"$env_path"
|
|
||||||
if [[ -n "$token" ]]; then
|
|
||||||
printf '%s' "$token"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_control_ui_allowed_origins() {
|
|
||||||
if [[ "${OPENCLAW_GATEWAY_BIND}" == "loopback" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
local allowed_origin_json
|
|
||||||
local current_allowed_origins
|
|
||||||
allowed_origin_json="$(printf '["http://127.0.0.1:%s"]' "$OPENCLAW_GATEWAY_PORT")"
|
|
||||||
current_allowed_origins="$(
|
|
||||||
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
|
|
||||||
config get gateway.controlUi.allowedOrigins 2>/dev/null || true
|
|
||||||
)"
|
|
||||||
current_allowed_origins="${current_allowed_origins//$'\r'/}"
|
|
||||||
|
|
||||||
if [[ -n "$current_allowed_origins" && "$current_allowed_origins" != "null" && "$current_allowed_origins" != "[]" ]]; then
|
|
||||||
echo "Control UI allowlist already configured; leaving gateway.controlUi.allowedOrigins unchanged."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
|
|
||||||
config set gateway.controlUi.allowedOrigins "$allowed_origin_json" --strict-json >/dev/null
|
|
||||||
echo "Set gateway.controlUi.allowedOrigins to $allowed_origin_json for non-loopback bind."
|
|
||||||
}
|
|
||||||
|
|
||||||
sync_gateway_mode_and_bind() {
|
|
||||||
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
|
|
||||||
config set gateway.mode local >/dev/null
|
|
||||||
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
|
|
||||||
config set gateway.bind "$OPENCLAW_GATEWAY_BIND" >/dev/null
|
|
||||||
echo "Pinned gateway.mode=local and gateway.bind=$OPENCLAW_GATEWAY_BIND for Docker setup."
|
|
||||||
}
|
|
||||||
|
|
||||||
contains_disallowed_chars() {
|
|
||||||
local value="$1"
|
|
||||||
[[ "$value" == *$'\n'* || "$value" == *$'\r'* || "$value" == *$'\t'* ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
is_valid_timezone() {
|
|
||||||
local value="$1"
|
|
||||||
[[ -e "/usr/share/zoneinfo/$value" && ! -d "/usr/share/zoneinfo/$value" ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_mount_path_value() {
|
|
||||||
local label="$1"
|
|
||||||
local value="$2"
|
|
||||||
if [[ -z "$value" ]]; then
|
|
||||||
fail "$label cannot be empty."
|
|
||||||
fi
|
|
||||||
if contains_disallowed_chars "$value"; then
|
|
||||||
fail "$label contains unsupported control characters."
|
|
||||||
fi
|
|
||||||
if [[ "$value" =~ [[:space:]] ]]; then
|
|
||||||
fail "$label cannot contain whitespace."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_named_volume() {
|
|
||||||
local value="$1"
|
|
||||||
if [[ ! "$value" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]]; then
|
|
||||||
fail "OPENCLAW_HOME_VOLUME must match [A-Za-z0-9][A-Za-z0-9_.-]* when using a named volume."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_mount_spec() {
|
|
||||||
local mount="$1"
|
|
||||||
if contains_disallowed_chars "$mount"; then
|
|
||||||
fail "OPENCLAW_EXTRA_MOUNTS entries cannot contain control characters."
|
|
||||||
fi
|
|
||||||
# Keep mount specs strict to avoid YAML structure injection.
|
|
||||||
# Expected format: source:target[:options]
|
|
||||||
if [[ ! "$mount" =~ ^[^[:space:],:]+:[^[:space:],:]+(:[^[:space:],:]+)?$ ]]; then
|
|
||||||
fail "Invalid mount format '$mount'. Expected source:target[:options] without spaces."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
require_cmd docker
|
|
||||||
if ! docker compose version >/dev/null 2>&1; then
|
|
||||||
echo "Docker Compose not available (try: docker compose version)" >&2
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "$DOCKER_SOCKET_PATH" && "${DOCKER_HOST:-}" == unix://* ]]; then
|
exec "$SCRIPT_PATH" "$@"
|
||||||
DOCKER_SOCKET_PATH="${DOCKER_HOST#unix://}"
|
|
||||||
fi
|
|
||||||
if [[ -z "$DOCKER_SOCKET_PATH" ]]; then
|
|
||||||
DOCKER_SOCKET_PATH="/var/run/docker.sock"
|
|
||||||
fi
|
|
||||||
if is_truthy_value "$RAW_SANDBOX_SETTING"; then
|
|
||||||
SANDBOX_ENABLED="1"
|
|
||||||
fi
|
|
||||||
|
|
||||||
OPENCLAW_CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}"
|
|
||||||
OPENCLAW_WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$HOME/.openclaw/workspace}"
|
|
||||||
|
|
||||||
validate_mount_path_value "OPENCLAW_CONFIG_DIR" "$OPENCLAW_CONFIG_DIR"
|
|
||||||
validate_mount_path_value "OPENCLAW_WORKSPACE_DIR" "$OPENCLAW_WORKSPACE_DIR"
|
|
||||||
if [[ -n "$HOME_VOLUME_NAME" ]]; then
|
|
||||||
if [[ "$HOME_VOLUME_NAME" == *"/"* ]]; then
|
|
||||||
validate_mount_path_value "OPENCLAW_HOME_VOLUME" "$HOME_VOLUME_NAME"
|
|
||||||
else
|
|
||||||
validate_named_volume "$HOME_VOLUME_NAME"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if contains_disallowed_chars "$EXTRA_MOUNTS"; then
|
|
||||||
fail "OPENCLAW_EXTRA_MOUNTS cannot contain control characters."
|
|
||||||
fi
|
|
||||||
if [[ -n "$SANDBOX_ENABLED" ]]; then
|
|
||||||
validate_mount_path_value "OPENCLAW_DOCKER_SOCKET" "$DOCKER_SOCKET_PATH"
|
|
||||||
fi
|
|
||||||
if [[ -n "$TIMEZONE" ]]; then
|
|
||||||
if contains_disallowed_chars "$TIMEZONE"; then
|
|
||||||
fail "OPENCLAW_TZ contains unsupported control characters."
|
|
||||||
fi
|
|
||||||
if [[ ! "$TIMEZONE" =~ ^[A-Za-z0-9/_+\-]+$ ]]; then
|
|
||||||
fail "OPENCLAW_TZ must be a valid IANA timezone string (e.g. Asia/Shanghai)."
|
|
||||||
fi
|
|
||||||
if ! is_valid_timezone "$TIMEZONE"; then
|
|
||||||
fail "OPENCLAW_TZ must match a timezone in /usr/share/zoneinfo (e.g. Asia/Shanghai)."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$OPENCLAW_CONFIG_DIR"
|
|
||||||
mkdir -p "$OPENCLAW_WORKSPACE_DIR"
|
|
||||||
# Seed directory tree eagerly so bind mounts work even on Docker Desktop/Windows
|
|
||||||
# where the container (even as root) cannot create new host subdirectories.
|
|
||||||
mkdir -p "$OPENCLAW_CONFIG_DIR/identity"
|
|
||||||
mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/agent"
|
|
||||||
mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/sessions"
|
|
||||||
|
|
||||||
export OPENCLAW_CONFIG_DIR
|
|
||||||
export OPENCLAW_WORKSPACE_DIR
|
|
||||||
export OPENCLAW_GATEWAY_PORT="${OPENCLAW_GATEWAY_PORT:-18789}"
|
|
||||||
export OPENCLAW_BRIDGE_PORT="${OPENCLAW_BRIDGE_PORT:-18790}"
|
|
||||||
export OPENCLAW_GATEWAY_BIND="${OPENCLAW_GATEWAY_BIND:-lan}"
|
|
||||||
export OPENCLAW_IMAGE="$IMAGE_NAME"
|
|
||||||
export OPENCLAW_DOCKER_APT_PACKAGES="${OPENCLAW_DOCKER_APT_PACKAGES:-}"
|
|
||||||
export OPENCLAW_EXTENSIONS="${OPENCLAW_EXTENSIONS:-}"
|
|
||||||
export OPENCLAW_EXTRA_MOUNTS="$EXTRA_MOUNTS"
|
|
||||||
export OPENCLAW_HOME_VOLUME="$HOME_VOLUME_NAME"
|
|
||||||
export OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="${OPENCLAW_ALLOW_INSECURE_PRIVATE_WS:-}"
|
|
||||||
export OPENCLAW_SANDBOX="$SANDBOX_ENABLED"
|
|
||||||
export OPENCLAW_DOCKER_SOCKET="$DOCKER_SOCKET_PATH"
|
|
||||||
export OPENCLAW_TZ="$TIMEZONE"
|
|
||||||
|
|
||||||
# Detect Docker socket GID for sandbox group_add.
|
|
||||||
DOCKER_GID=""
|
|
||||||
if [[ -n "$SANDBOX_ENABLED" && -S "$DOCKER_SOCKET_PATH" ]]; then
|
|
||||||
DOCKER_GID="$(stat -c '%g' "$DOCKER_SOCKET_PATH" 2>/dev/null || stat -f '%g' "$DOCKER_SOCKET_PATH" 2>/dev/null || echo "")"
|
|
||||||
fi
|
|
||||||
export DOCKER_GID
|
|
||||||
|
|
||||||
if [[ -z "${OPENCLAW_GATEWAY_TOKEN:-}" ]]; then
|
|
||||||
EXISTING_CONFIG_TOKEN="$(read_config_gateway_token || true)"
|
|
||||||
if [[ -n "$EXISTING_CONFIG_TOKEN" ]]; then
|
|
||||||
OPENCLAW_GATEWAY_TOKEN="$EXISTING_CONFIG_TOKEN"
|
|
||||||
echo "Reusing gateway token from $OPENCLAW_CONFIG_DIR/openclaw.json"
|
|
||||||
else
|
|
||||||
DOTENV_GATEWAY_TOKEN="$(read_env_gateway_token "$ROOT_DIR/.env" || true)"
|
|
||||||
if [[ -n "$DOTENV_GATEWAY_TOKEN" ]]; then
|
|
||||||
OPENCLAW_GATEWAY_TOKEN="$DOTENV_GATEWAY_TOKEN"
|
|
||||||
echo "Reusing gateway token from $ROOT_DIR/.env"
|
|
||||||
elif command -v openssl >/dev/null 2>&1; then
|
|
||||||
OPENCLAW_GATEWAY_TOKEN="$(openssl rand -hex 32)"
|
|
||||||
else
|
|
||||||
OPENCLAW_GATEWAY_TOKEN="$(python3 - <<'PY'
|
|
||||||
import secrets
|
|
||||||
print(secrets.token_hex(32))
|
|
||||||
PY
|
|
||||||
)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
export OPENCLAW_GATEWAY_TOKEN
|
|
||||||
|
|
||||||
COMPOSE_FILES=("$COMPOSE_FILE")
|
|
||||||
COMPOSE_ARGS=()
|
|
||||||
|
|
||||||
write_extra_compose() {
|
|
||||||
local home_volume="$1"
|
|
||||||
shift
|
|
||||||
local mount
|
|
||||||
local gateway_home_mount
|
|
||||||
local gateway_config_mount
|
|
||||||
local gateway_workspace_mount
|
|
||||||
|
|
||||||
cat >"$EXTRA_COMPOSE_FILE" <<'YAML'
|
|
||||||
services:
|
|
||||||
openclaw-gateway:
|
|
||||||
volumes:
|
|
||||||
YAML
|
|
||||||
|
|
||||||
if [[ -n "$home_volume" ]]; then
|
|
||||||
gateway_home_mount="${home_volume}:/home/node"
|
|
||||||
gateway_config_mount="${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw"
|
|
||||||
gateway_workspace_mount="${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace"
|
|
||||||
validate_mount_spec "$gateway_home_mount"
|
|
||||||
validate_mount_spec "$gateway_config_mount"
|
|
||||||
validate_mount_spec "$gateway_workspace_mount"
|
|
||||||
printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE"
|
|
||||||
printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE"
|
|
||||||
printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
for mount in "$@"; do
|
|
||||||
validate_mount_spec "$mount"
|
|
||||||
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
|
|
||||||
done
|
|
||||||
|
|
||||||
cat >>"$EXTRA_COMPOSE_FILE" <<'YAML'
|
|
||||||
openclaw-cli:
|
|
||||||
volumes:
|
|
||||||
YAML
|
|
||||||
|
|
||||||
if [[ -n "$home_volume" ]]; then
|
|
||||||
printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE"
|
|
||||||
printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE"
|
|
||||||
printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
for mount in "$@"; do
|
|
||||||
validate_mount_spec "$mount"
|
|
||||||
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -n "$home_volume" && "$home_volume" != *"/"* ]]; then
|
|
||||||
validate_named_volume "$home_volume"
|
|
||||||
cat >>"$EXTRA_COMPOSE_FILE" <<YAML
|
|
||||||
volumes:
|
|
||||||
${home_volume}:
|
|
||||||
YAML
|
|
||||||
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
|
|
||||||
export OPENCLAW_INSTALL_DOCKER_CLI=1
|
|
||||||
fi
|
|
||||||
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
|
|
||||||
|
|
||||||
ENV_FILE="$ROOT_DIR/.env"
|
|
||||||
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"
|
|
||||||
seen="$seen$k "
|
|
||||||
replaced=true
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [[ "$replaced" == false ]]; then
|
|
||||||
printf '%s\n' "$line" >>"$tmp"
|
|
||||||
fi
|
|
||||||
done <"$file"
|
|
||||||
fi
|
|
||||||
|
|
||||||
for k in "${keys[@]}"; do
|
|
||||||
if [[ "$seen" != *" $k "* ]]; then
|
|
||||||
printf '%s=%s\n' "$k" "${!k-}" >>"$tmp"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
mv "$tmp" "$file"
|
|
||||||
}
|
|
||||||
|
|
||||||
upsert_env "$ENV_FILE" \
|
|
||||||
OPENCLAW_CONFIG_DIR \
|
|
||||||
OPENCLAW_WORKSPACE_DIR \
|
|
||||||
OPENCLAW_GATEWAY_PORT \
|
|
||||||
OPENCLAW_BRIDGE_PORT \
|
|
||||||
OPENCLAW_GATEWAY_BIND \
|
|
||||||
OPENCLAW_GATEWAY_TOKEN \
|
|
||||||
OPENCLAW_IMAGE \
|
|
||||||
OPENCLAW_EXTRA_MOUNTS \
|
|
||||||
OPENCLAW_HOME_VOLUME \
|
|
||||||
OPENCLAW_DOCKER_APT_PACKAGES \
|
|
||||||
OPENCLAW_EXTENSIONS \
|
|
||||||
OPENCLAW_SANDBOX \
|
|
||||||
OPENCLAW_DOCKER_SOCKET \
|
|
||||||
DOCKER_GID \
|
|
||||||
OPENCLAW_INSTALL_DOCKER_CLI \
|
|
||||||
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS \
|
|
||||||
OPENCLAW_TZ
|
|
||||||
|
|
||||||
if [[ "$IMAGE_NAME" == "openclaw:local" ]]; then
|
|
||||||
echo "==> Building Docker image: $IMAGE_NAME"
|
|
||||||
docker build \
|
|
||||||
--build-arg "OPENCLAW_DOCKER_APT_PACKAGES=${OPENCLAW_DOCKER_APT_PACKAGES}" \
|
|
||||||
--build-arg "OPENCLAW_EXTENSIONS=${OPENCLAW_EXTENSIONS}" \
|
|
||||||
--build-arg "OPENCLAW_INSTALL_DOCKER_CLI=${OPENCLAW_INSTALL_DOCKER_CLI:-}" \
|
|
||||||
-t "$IMAGE_NAME" \
|
|
||||||
-f "$ROOT_DIR/Dockerfile" \
|
|
||||||
"$ROOT_DIR"
|
|
||||||
else
|
|
||||||
echo "==> Pulling Docker image: $IMAGE_NAME"
|
|
||||||
if ! docker pull "$IMAGE_NAME"; then
|
|
||||||
echo "ERROR: Failed to pull image $IMAGE_NAME. Please check the image name and your access permissions." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure bind-mounted data directories are writable by the container's `node`
|
|
||||||
# user (uid 1000). Host-created dirs inherit the host user's uid which may
|
|
||||||
# differ, causing EACCES when the container tries to mkdir/write.
|
|
||||||
# Running a brief root container to chown is the portable Docker idiom --
|
|
||||||
# it works regardless of the host uid and doesn't require host-side root.
|
|
||||||
echo ""
|
|
||||||
echo "==> Fixing data-directory permissions"
|
|
||||||
# Use -xdev to restrict chown to the config-dir mount only — without it,
|
|
||||||
# the recursive chown would cross into the workspace bind mount and rewrite
|
|
||||||
# ownership of all user project files on Linux hosts.
|
|
||||||
# After fixing the config dir, only the OpenClaw metadata subdirectory
|
|
||||||
# (.openclaw/) inside the workspace gets chowned, not the user's project files.
|
|
||||||
docker compose "${COMPOSE_ARGS[@]}" run --rm --user root --entrypoint sh openclaw-cli -c \
|
|
||||||
'find /home/node/.openclaw -xdev -exec chown node:node {} +; \
|
|
||||||
[ -d /home/node/.openclaw/workspace/.openclaw ] && chown -R node:node /home/node/.openclaw/workspace/.openclaw || true'
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "==> Onboarding (interactive)"
|
|
||||||
echo "Docker setup pins Gateway mode to local."
|
|
||||||
echo "Gateway runtime bind comes from OPENCLAW_GATEWAY_BIND (default: lan)."
|
|
||||||
echo "Current runtime bind: $OPENCLAW_GATEWAY_BIND"
|
|
||||||
echo "Gateway token: $OPENCLAW_GATEWAY_TOKEN"
|
|
||||||
echo "Tailscale exposure: Off (use host-level tailnet/Tailscale setup separately)."
|
|
||||||
echo "Install Gateway daemon: No (managed by Docker Compose)"
|
|
||||||
echo ""
|
|
||||||
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli onboard --mode local --no-install-daemon
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "==> Docker gateway defaults"
|
|
||||||
sync_gateway_mode_and_bind
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "==> Control UI origin allowlist"
|
|
||||||
ensure_control_ui_allowed_origins
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "==> Provider setup (optional)"
|
|
||||||
echo "WhatsApp (QR):"
|
|
||||||
echo " ${COMPOSE_HINT} run --rm openclaw-cli channels login"
|
|
||||||
echo "Telegram (bot token):"
|
|
||||||
echo " ${COMPOSE_HINT} run --rm openclaw-cli channels add --channel telegram --token <token>"
|
|
||||||
echo "Discord (bot token):"
|
|
||||||
echo " ${COMPOSE_HINT} run --rm openclaw-cli channels add --channel discord --token <token>"
|
|
||||||
echo "Docs: https://docs.openclaw.ai/channels"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "==> Starting gateway"
|
|
||||||
docker compose "${COMPOSE_ARGS[@]}" up -d openclaw-gateway
|
|
||||||
|
|
||||||
# --- Sandbox setup (opt-in via OPENCLAW_SANDBOX=1) ---
|
|
||||||
if [[ -n "$SANDBOX_ENABLED" ]]; then
|
|
||||||
echo ""
|
|
||||||
echo "==> Sandbox setup"
|
|
||||||
|
|
||||||
# Build sandbox image if Dockerfile.sandbox exists.
|
|
||||||
if [[ -f "$ROOT_DIR/Dockerfile.sandbox" ]]; then
|
|
||||||
echo "Building sandbox image: openclaw-sandbox:bookworm-slim"
|
|
||||||
docker build \
|
|
||||||
-t "openclaw-sandbox:bookworm-slim" \
|
|
||||||
-f "$ROOT_DIR/Dockerfile.sandbox" \
|
|
||||||
"$ROOT_DIR"
|
|
||||||
else
|
|
||||||
echo "WARNING: Dockerfile.sandbox not found in $ROOT_DIR" >&2
|
|
||||||
echo " Sandbox config will be applied but no sandbox image will be built." >&2
|
|
||||||
echo " Agent exec may fail if the configured sandbox image does not exist." >&2
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Defense-in-depth: verify Docker CLI in the running image before enabling
|
|
||||||
# sandbox. This avoids claiming sandbox is enabled when the image cannot
|
|
||||||
# launch sandbox containers.
|
|
||||||
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --entrypoint docker openclaw-gateway --version >/dev/null 2>&1; then
|
|
||||||
echo "WARNING: Docker CLI not found inside the container image." >&2
|
|
||||||
echo " Sandbox requires Docker CLI. Rebuild with --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1" >&2
|
|
||||||
echo " or use a local build (OPENCLAW_IMAGE=openclaw:local). Skipping sandbox setup." >&2
|
|
||||||
SANDBOX_ENABLED=""
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Apply sandbox config only if prerequisites are met.
|
|
||||||
if [[ -n "$SANDBOX_ENABLED" ]]; then
|
|
||||||
# Mount Docker socket via a dedicated compose overlay. This overlay is
|
|
||||||
# created only after sandbox prerequisites pass, so the socket is never
|
|
||||||
# exposed when sandbox cannot actually run.
|
|
||||||
if [[ -S "$DOCKER_SOCKET_PATH" ]]; then
|
|
||||||
SANDBOX_COMPOSE_FILE="$ROOT_DIR/docker-compose.sandbox.yml"
|
|
||||||
cat >"$SANDBOX_COMPOSE_FILE" <<YAML
|
|
||||||
services:
|
|
||||||
openclaw-gateway:
|
|
||||||
volumes:
|
|
||||||
- ${DOCKER_SOCKET_PATH}:/var/run/docker.sock
|
|
||||||
YAML
|
|
||||||
if [[ -n "${DOCKER_GID:-}" ]]; then
|
|
||||||
cat >>"$SANDBOX_COMPOSE_FILE" <<YAML
|
|
||||||
group_add:
|
|
||||||
- "${DOCKER_GID}"
|
|
||||||
YAML
|
|
||||||
fi
|
|
||||||
COMPOSE_ARGS+=("-f" "$SANDBOX_COMPOSE_FILE")
|
|
||||||
echo "==> Sandbox: added Docker socket mount"
|
|
||||||
else
|
|
||||||
echo "WARNING: OPENCLAW_SANDBOX enabled but Docker socket not found at $DOCKER_SOCKET_PATH." >&2
|
|
||||||
echo " Sandbox requires Docker socket access. Skipping sandbox setup." >&2
|
|
||||||
SANDBOX_ENABLED=""
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$SANDBOX_ENABLED" ]]; then
|
|
||||||
# Enable sandbox in OpenClaw config.
|
|
||||||
sandbox_config_ok=true
|
|
||||||
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
|
|
||||||
config set agents.defaults.sandbox.mode "non-main" >/dev/null; then
|
|
||||||
echo "WARNING: Failed to set agents.defaults.sandbox.mode" >&2
|
|
||||||
sandbox_config_ok=false
|
|
||||||
fi
|
|
||||||
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
|
|
||||||
config set agents.defaults.sandbox.scope "agent" >/dev/null; then
|
|
||||||
echo "WARNING: Failed to set agents.defaults.sandbox.scope" >&2
|
|
||||||
sandbox_config_ok=false
|
|
||||||
fi
|
|
||||||
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
|
|
||||||
config set agents.defaults.sandbox.workspaceAccess "none" >/dev/null; then
|
|
||||||
echo "WARNING: Failed to set agents.defaults.sandbox.workspaceAccess" >&2
|
|
||||||
sandbox_config_ok=false
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$sandbox_config_ok" == true ]]; then
|
|
||||||
echo "Sandbox enabled: mode=non-main, scope=agent, workspaceAccess=none"
|
|
||||||
echo "Docs: https://docs.openclaw.ai/gateway/sandboxing"
|
|
||||||
# Restart gateway with sandbox compose overlay to pick up socket mount + config.
|
|
||||||
docker compose "${COMPOSE_ARGS[@]}" up -d openclaw-gateway
|
|
||||||
else
|
|
||||||
echo "WARNING: Sandbox config was partially applied. Check errors above." >&2
|
|
||||||
echo " Skipping gateway restart to avoid exposing Docker socket without a full sandbox policy." >&2
|
|
||||||
if ! docker compose "${BASE_COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
|
|
||||||
config set agents.defaults.sandbox.mode "off" >/dev/null; then
|
|
||||||
echo "WARNING: Failed to roll back agents.defaults.sandbox.mode to off" >&2
|
|
||||||
else
|
|
||||||
echo "Sandbox mode rolled back to off due to partial sandbox config failure."
|
|
||||||
fi
|
|
||||||
if [[ -n "${SANDBOX_COMPOSE_FILE:-}" ]]; then
|
|
||||||
rm -f "$SANDBOX_COMPOSE_FILE"
|
|
||||||
fi
|
|
||||||
# Ensure gateway service definition is reset without sandbox overlay mount.
|
|
||||||
docker compose "${BASE_COMPOSE_ARGS[@]}" up -d --force-recreate openclaw-gateway
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# Keep reruns deterministic: if sandbox is not active for this run, reset
|
|
||||||
# persisted sandbox mode so future execs do not require docker.sock by stale
|
|
||||||
# config alone.
|
|
||||||
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
|
|
||||||
config set agents.defaults.sandbox.mode "off" >/dev/null; then
|
|
||||||
echo "WARNING: Failed to reset agents.defaults.sandbox.mode to off" >&2
|
|
||||||
fi
|
|
||||||
if [[ -f "$ROOT_DIR/docker-compose.sandbox.yml" ]]; then
|
|
||||||
rm -f "$ROOT_DIR/docker-compose.sandbox.yml"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Gateway running with host port mapping."
|
|
||||||
echo "Access from tailnet devices via the host's tailnet IP."
|
|
||||||
echo "Config: $OPENCLAW_CONFIG_DIR"
|
|
||||||
echo "Workspace: $OPENCLAW_WORKSPACE_DIR"
|
|
||||||
echo "Token: $OPENCLAW_GATEWAY_TOKEN"
|
|
||||||
echo ""
|
|
||||||
echo "Commands:"
|
|
||||||
echo " ${COMPOSE_HINT} logs -f openclaw-gateway"
|
|
||||||
echo " ${COMPOSE_HINT} exec openclaw-gateway node dist/index.js health --token \"$OPENCLAW_GATEWAY_TOKEN\""
|
|
||||||
|
|||||||
616
scripts/docker/setup.sh
Executable file
616
scripts/docker/setup.sh
Executable file
@ -0,0 +1,616 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
COMPOSE_FILE="$ROOT_DIR/docker-compose.yml"
|
||||||
|
EXTRA_COMPOSE_FILE="$ROOT_DIR/docker-compose.extra.yml"
|
||||||
|
IMAGE_NAME="${OPENCLAW_IMAGE:-openclaw:local}"
|
||||||
|
EXTRA_MOUNTS="${OPENCLAW_EXTRA_MOUNTS:-}"
|
||||||
|
HOME_VOLUME_NAME="${OPENCLAW_HOME_VOLUME:-}"
|
||||||
|
RAW_SANDBOX_SETTING="${OPENCLAW_SANDBOX:-}"
|
||||||
|
SANDBOX_ENABLED=""
|
||||||
|
DOCKER_SOCKET_PATH="${OPENCLAW_DOCKER_SOCKET:-}"
|
||||||
|
TIMEZONE="${OPENCLAW_TZ:-}"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "ERROR: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
if ! command -v "$1" >/dev/null 2>&1; then
|
||||||
|
echo "Missing dependency: $1" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
is_truthy_value() {
|
||||||
|
local raw="${1:-}"
|
||||||
|
raw="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
case "$raw" in
|
||||||
|
1 | true | yes | on) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
read_config_gateway_token() {
|
||||||
|
local config_path="$OPENCLAW_CONFIG_DIR/openclaw.json"
|
||||||
|
if [[ ! -f "$config_path" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
|
python3 - "$config_path" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
path = sys.argv[1]
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
cfg = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
gateway = cfg.get("gateway")
|
||||||
|
if not isinstance(gateway, dict):
|
||||||
|
raise SystemExit(0)
|
||||||
|
auth = gateway.get("auth")
|
||||||
|
if not isinstance(auth, dict):
|
||||||
|
raise SystemExit(0)
|
||||||
|
token = auth.get("token")
|
||||||
|
if isinstance(token, str):
|
||||||
|
token = token.strip()
|
||||||
|
if token:
|
||||||
|
print(token)
|
||||||
|
PY
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if command -v node >/dev/null 2>&1; then
|
||||||
|
node - "$config_path" <<'NODE'
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const configPath = process.argv[2];
|
||||||
|
try {
|
||||||
|
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
||||||
|
const token = cfg?.gateway?.auth?.token;
|
||||||
|
if (typeof token === "string" && token.trim().length > 0) {
|
||||||
|
process.stdout.write(token.trim());
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Keep docker-setup resilient when config parsing fails.
|
||||||
|
}
|
||||||
|
NODE
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
read_env_gateway_token() {
|
||||||
|
local env_path="$1"
|
||||||
|
local line=""
|
||||||
|
local token=""
|
||||||
|
if [[ ! -f "$env_path" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
line="${line%$'\r'}"
|
||||||
|
if [[ "$line" == OPENCLAW_GATEWAY_TOKEN=* ]]; then
|
||||||
|
token="${line#OPENCLAW_GATEWAY_TOKEN=}"
|
||||||
|
fi
|
||||||
|
done <"$env_path"
|
||||||
|
if [[ -n "$token" ]]; then
|
||||||
|
printf '%s' "$token"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_control_ui_allowed_origins() {
|
||||||
|
if [[ "${OPENCLAW_GATEWAY_BIND}" == "loopback" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local allowed_origin_json
|
||||||
|
local current_allowed_origins
|
||||||
|
allowed_origin_json="$(printf '["http://127.0.0.1:%s"]' "$OPENCLAW_GATEWAY_PORT")"
|
||||||
|
current_allowed_origins="$(
|
||||||
|
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
|
||||||
|
config get gateway.controlUi.allowedOrigins 2>/dev/null || true
|
||||||
|
)"
|
||||||
|
current_allowed_origins="${current_allowed_origins//$'\r'/}"
|
||||||
|
|
||||||
|
if [[ -n "$current_allowed_origins" && "$current_allowed_origins" != "null" && "$current_allowed_origins" != "[]" ]]; then
|
||||||
|
echo "Control UI allowlist already configured; leaving gateway.controlUi.allowedOrigins unchanged."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
|
||||||
|
config set gateway.controlUi.allowedOrigins "$allowed_origin_json" --strict-json >/dev/null
|
||||||
|
echo "Set gateway.controlUi.allowedOrigins to $allowed_origin_json for non-loopback bind."
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_gateway_mode_and_bind() {
|
||||||
|
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
|
||||||
|
config set gateway.mode local >/dev/null
|
||||||
|
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
|
||||||
|
config set gateway.bind "$OPENCLAW_GATEWAY_BIND" >/dev/null
|
||||||
|
echo "Pinned gateway.mode=local and gateway.bind=$OPENCLAW_GATEWAY_BIND for Docker setup."
|
||||||
|
}
|
||||||
|
|
||||||
|
contains_disallowed_chars() {
|
||||||
|
local value="$1"
|
||||||
|
[[ "$value" == *$'\n'* || "$value" == *$'\r'* || "$value" == *$'\t'* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
is_valid_timezone() {
|
||||||
|
local value="$1"
|
||||||
|
[[ -e "/usr/share/zoneinfo/$value" && ! -d "/usr/share/zoneinfo/$value" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_mount_path_value() {
|
||||||
|
local label="$1"
|
||||||
|
local value="$2"
|
||||||
|
if [[ -z "$value" ]]; then
|
||||||
|
fail "$label cannot be empty."
|
||||||
|
fi
|
||||||
|
if contains_disallowed_chars "$value"; then
|
||||||
|
fail "$label contains unsupported control characters."
|
||||||
|
fi
|
||||||
|
if [[ "$value" =~ [[:space:]] ]]; then
|
||||||
|
fail "$label cannot contain whitespace."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_named_volume() {
|
||||||
|
local value="$1"
|
||||||
|
if [[ ! "$value" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]]; then
|
||||||
|
fail "OPENCLAW_HOME_VOLUME must match [A-Za-z0-9][A-Za-z0-9_.-]* when using a named volume."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_mount_spec() {
|
||||||
|
local mount="$1"
|
||||||
|
if contains_disallowed_chars "$mount"; then
|
||||||
|
fail "OPENCLAW_EXTRA_MOUNTS entries cannot contain control characters."
|
||||||
|
fi
|
||||||
|
# Keep mount specs strict to avoid YAML structure injection.
|
||||||
|
# Expected format: source:target[:options]
|
||||||
|
if [[ ! "$mount" =~ ^[^[:space:],:]+:[^[:space:],:]+(:[^[:space:],:]+)?$ ]]; then
|
||||||
|
fail "Invalid mount format '$mount'. Expected source:target[:options] without spaces."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require_cmd docker
|
||||||
|
if ! docker compose version >/dev/null 2>&1; then
|
||||||
|
echo "Docker Compose not available (try: docker compose version)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$DOCKER_SOCKET_PATH" && "${DOCKER_HOST:-}" == unix://* ]]; then
|
||||||
|
DOCKER_SOCKET_PATH="${DOCKER_HOST#unix://}"
|
||||||
|
fi
|
||||||
|
if [[ -z "$DOCKER_SOCKET_PATH" ]]; then
|
||||||
|
DOCKER_SOCKET_PATH="/var/run/docker.sock"
|
||||||
|
fi
|
||||||
|
if is_truthy_value "$RAW_SANDBOX_SETTING"; then
|
||||||
|
SANDBOX_ENABLED="1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
OPENCLAW_CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}"
|
||||||
|
OPENCLAW_WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$HOME/.openclaw/workspace}"
|
||||||
|
|
||||||
|
validate_mount_path_value "OPENCLAW_CONFIG_DIR" "$OPENCLAW_CONFIG_DIR"
|
||||||
|
validate_mount_path_value "OPENCLAW_WORKSPACE_DIR" "$OPENCLAW_WORKSPACE_DIR"
|
||||||
|
if [[ -n "$HOME_VOLUME_NAME" ]]; then
|
||||||
|
if [[ "$HOME_VOLUME_NAME" == *"/"* ]]; then
|
||||||
|
validate_mount_path_value "OPENCLAW_HOME_VOLUME" "$HOME_VOLUME_NAME"
|
||||||
|
else
|
||||||
|
validate_named_volume "$HOME_VOLUME_NAME"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if contains_disallowed_chars "$EXTRA_MOUNTS"; then
|
||||||
|
fail "OPENCLAW_EXTRA_MOUNTS cannot contain control characters."
|
||||||
|
fi
|
||||||
|
if [[ -n "$SANDBOX_ENABLED" ]]; then
|
||||||
|
validate_mount_path_value "OPENCLAW_DOCKER_SOCKET" "$DOCKER_SOCKET_PATH"
|
||||||
|
fi
|
||||||
|
if [[ -n "$TIMEZONE" ]]; then
|
||||||
|
if contains_disallowed_chars "$TIMEZONE"; then
|
||||||
|
fail "OPENCLAW_TZ contains unsupported control characters."
|
||||||
|
fi
|
||||||
|
if [[ ! "$TIMEZONE" =~ ^[A-Za-z0-9/_+\-]+$ ]]; then
|
||||||
|
fail "OPENCLAW_TZ must be a valid IANA timezone string (e.g. Asia/Shanghai)."
|
||||||
|
fi
|
||||||
|
if ! is_valid_timezone "$TIMEZONE"; then
|
||||||
|
fail "OPENCLAW_TZ must match a timezone in /usr/share/zoneinfo (e.g. Asia/Shanghai)."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$OPENCLAW_CONFIG_DIR"
|
||||||
|
mkdir -p "$OPENCLAW_WORKSPACE_DIR"
|
||||||
|
# Seed directory tree eagerly so bind mounts work even on Docker Desktop/Windows
|
||||||
|
# where the container (even as root) cannot create new host subdirectories.
|
||||||
|
mkdir -p "$OPENCLAW_CONFIG_DIR/identity"
|
||||||
|
mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/agent"
|
||||||
|
mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/sessions"
|
||||||
|
|
||||||
|
export OPENCLAW_CONFIG_DIR
|
||||||
|
export OPENCLAW_WORKSPACE_DIR
|
||||||
|
export OPENCLAW_GATEWAY_PORT="${OPENCLAW_GATEWAY_PORT:-18789}"
|
||||||
|
export OPENCLAW_BRIDGE_PORT="${OPENCLAW_BRIDGE_PORT:-18790}"
|
||||||
|
export OPENCLAW_GATEWAY_BIND="${OPENCLAW_GATEWAY_BIND:-lan}"
|
||||||
|
export OPENCLAW_IMAGE="$IMAGE_NAME"
|
||||||
|
export OPENCLAW_DOCKER_APT_PACKAGES="${OPENCLAW_DOCKER_APT_PACKAGES:-}"
|
||||||
|
export OPENCLAW_EXTENSIONS="${OPENCLAW_EXTENSIONS:-}"
|
||||||
|
export OPENCLAW_EXTRA_MOUNTS="$EXTRA_MOUNTS"
|
||||||
|
export OPENCLAW_HOME_VOLUME="$HOME_VOLUME_NAME"
|
||||||
|
export OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="${OPENCLAW_ALLOW_INSECURE_PRIVATE_WS:-}"
|
||||||
|
export OPENCLAW_SANDBOX="$SANDBOX_ENABLED"
|
||||||
|
export OPENCLAW_DOCKER_SOCKET="$DOCKER_SOCKET_PATH"
|
||||||
|
export OPENCLAW_TZ="$TIMEZONE"
|
||||||
|
|
||||||
|
# Detect Docker socket GID for sandbox group_add.
|
||||||
|
DOCKER_GID=""
|
||||||
|
if [[ -n "$SANDBOX_ENABLED" && -S "$DOCKER_SOCKET_PATH" ]]; then
|
||||||
|
DOCKER_GID="$(stat -c '%g' "$DOCKER_SOCKET_PATH" 2>/dev/null || stat -f '%g' "$DOCKER_SOCKET_PATH" 2>/dev/null || echo "")"
|
||||||
|
fi
|
||||||
|
export DOCKER_GID
|
||||||
|
|
||||||
|
if [[ -z "${OPENCLAW_GATEWAY_TOKEN:-}" ]]; then
|
||||||
|
EXISTING_CONFIG_TOKEN="$(read_config_gateway_token || true)"
|
||||||
|
if [[ -n "$EXISTING_CONFIG_TOKEN" ]]; then
|
||||||
|
OPENCLAW_GATEWAY_TOKEN="$EXISTING_CONFIG_TOKEN"
|
||||||
|
echo "Reusing gateway token from $OPENCLAW_CONFIG_DIR/openclaw.json"
|
||||||
|
else
|
||||||
|
DOTENV_GATEWAY_TOKEN="$(read_env_gateway_token "$ROOT_DIR/.env" || true)"
|
||||||
|
if [[ -n "$DOTENV_GATEWAY_TOKEN" ]]; then
|
||||||
|
OPENCLAW_GATEWAY_TOKEN="$DOTENV_GATEWAY_TOKEN"
|
||||||
|
echo "Reusing gateway token from $ROOT_DIR/.env"
|
||||||
|
elif command -v openssl >/dev/null 2>&1; then
|
||||||
|
OPENCLAW_GATEWAY_TOKEN="$(openssl rand -hex 32)"
|
||||||
|
else
|
||||||
|
OPENCLAW_GATEWAY_TOKEN="$(python3 - <<'PY'
|
||||||
|
import secrets
|
||||||
|
print(secrets.token_hex(32))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
export OPENCLAW_GATEWAY_TOKEN
|
||||||
|
|
||||||
|
COMPOSE_FILES=("$COMPOSE_FILE")
|
||||||
|
COMPOSE_ARGS=()
|
||||||
|
|
||||||
|
write_extra_compose() {
|
||||||
|
local home_volume="$1"
|
||||||
|
shift
|
||||||
|
local mount
|
||||||
|
local gateway_home_mount
|
||||||
|
local gateway_config_mount
|
||||||
|
local gateway_workspace_mount
|
||||||
|
|
||||||
|
cat >"$EXTRA_COMPOSE_FILE" <<'YAML'
|
||||||
|
services:
|
||||||
|
openclaw-gateway:
|
||||||
|
volumes:
|
||||||
|
YAML
|
||||||
|
|
||||||
|
if [[ -n "$home_volume" ]]; then
|
||||||
|
gateway_home_mount="${home_volume}:/home/node"
|
||||||
|
gateway_config_mount="${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw"
|
||||||
|
gateway_workspace_mount="${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace"
|
||||||
|
validate_mount_spec "$gateway_home_mount"
|
||||||
|
validate_mount_spec "$gateway_config_mount"
|
||||||
|
validate_mount_spec "$gateway_workspace_mount"
|
||||||
|
printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE"
|
||||||
|
printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE"
|
||||||
|
printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for mount in "$@"; do
|
||||||
|
validate_mount_spec "$mount"
|
||||||
|
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
|
||||||
|
done
|
||||||
|
|
||||||
|
cat >>"$EXTRA_COMPOSE_FILE" <<'YAML'
|
||||||
|
openclaw-cli:
|
||||||
|
volumes:
|
||||||
|
YAML
|
||||||
|
|
||||||
|
if [[ -n "$home_volume" ]]; then
|
||||||
|
printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE"
|
||||||
|
printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE"
|
||||||
|
printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for mount in "$@"; do
|
||||||
|
validate_mount_spec "$mount"
|
||||||
|
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "$home_volume" && "$home_volume" != *"/"* ]]; then
|
||||||
|
validate_named_volume "$home_volume"
|
||||||
|
cat >>"$EXTRA_COMPOSE_FILE" <<YAML
|
||||||
|
volumes:
|
||||||
|
${home_volume}:
|
||||||
|
YAML
|
||||||
|
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
|
||||||
|
export OPENCLAW_INSTALL_DOCKER_CLI=1
|
||||||
|
fi
|
||||||
|
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
|
||||||
|
|
||||||
|
ENV_FILE="$ROOT_DIR/.env"
|
||||||
|
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"
|
||||||
|
seen="$seen$k "
|
||||||
|
replaced=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ "$replaced" == false ]]; then
|
||||||
|
printf '%s\n' "$line" >>"$tmp"
|
||||||
|
fi
|
||||||
|
done <"$file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for k in "${keys[@]}"; do
|
||||||
|
if [[ "$seen" != *" $k "* ]]; then
|
||||||
|
printf '%s=%s\n' "$k" "${!k-}" >>"$tmp"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
mv "$tmp" "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
upsert_env "$ENV_FILE" \
|
||||||
|
OPENCLAW_CONFIG_DIR \
|
||||||
|
OPENCLAW_WORKSPACE_DIR \
|
||||||
|
OPENCLAW_GATEWAY_PORT \
|
||||||
|
OPENCLAW_BRIDGE_PORT \
|
||||||
|
OPENCLAW_GATEWAY_BIND \
|
||||||
|
OPENCLAW_GATEWAY_TOKEN \
|
||||||
|
OPENCLAW_IMAGE \
|
||||||
|
OPENCLAW_EXTRA_MOUNTS \
|
||||||
|
OPENCLAW_HOME_VOLUME \
|
||||||
|
OPENCLAW_DOCKER_APT_PACKAGES \
|
||||||
|
OPENCLAW_EXTENSIONS \
|
||||||
|
OPENCLAW_SANDBOX \
|
||||||
|
OPENCLAW_DOCKER_SOCKET \
|
||||||
|
DOCKER_GID \
|
||||||
|
OPENCLAW_INSTALL_DOCKER_CLI \
|
||||||
|
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS \
|
||||||
|
OPENCLAW_TZ
|
||||||
|
|
||||||
|
if [[ "$IMAGE_NAME" == "openclaw:local" ]]; then
|
||||||
|
echo "==> Building Docker image: $IMAGE_NAME"
|
||||||
|
docker build \
|
||||||
|
--build-arg "OPENCLAW_DOCKER_APT_PACKAGES=${OPENCLAW_DOCKER_APT_PACKAGES}" \
|
||||||
|
--build-arg "OPENCLAW_EXTENSIONS=${OPENCLAW_EXTENSIONS}" \
|
||||||
|
--build-arg "OPENCLAW_INSTALL_DOCKER_CLI=${OPENCLAW_INSTALL_DOCKER_CLI:-}" \
|
||||||
|
-t "$IMAGE_NAME" \
|
||||||
|
-f "$ROOT_DIR/Dockerfile" \
|
||||||
|
"$ROOT_DIR"
|
||||||
|
else
|
||||||
|
echo "==> Pulling Docker image: $IMAGE_NAME"
|
||||||
|
if ! docker pull "$IMAGE_NAME"; then
|
||||||
|
echo "ERROR: Failed to pull image $IMAGE_NAME. Please check the image name and your access permissions." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure bind-mounted data directories are writable by the container's `node`
|
||||||
|
# user (uid 1000). Host-created dirs inherit the host user's uid which may
|
||||||
|
# differ, causing EACCES when the container tries to mkdir/write.
|
||||||
|
# Running a brief root container to chown is the portable Docker idiom --
|
||||||
|
# it works regardless of the host uid and doesn't require host-side root.
|
||||||
|
echo ""
|
||||||
|
echo "==> Fixing data-directory permissions"
|
||||||
|
# Use -xdev to restrict chown to the config-dir mount only — without it,
|
||||||
|
# the recursive chown would cross into the workspace bind mount and rewrite
|
||||||
|
# ownership of all user project files on Linux hosts.
|
||||||
|
# After fixing the config dir, only the OpenClaw metadata subdirectory
|
||||||
|
# (.openclaw/) inside the workspace gets chowned, not the user's project files.
|
||||||
|
docker compose "${COMPOSE_ARGS[@]}" run --rm --user root --entrypoint sh openclaw-cli -c \
|
||||||
|
'find /home/node/.openclaw -xdev -exec chown node:node {} +; \
|
||||||
|
[ -d /home/node/.openclaw/workspace/.openclaw ] && chown -R node:node /home/node/.openclaw/workspace/.openclaw || true'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "==> Onboarding (interactive)"
|
||||||
|
echo "Docker setup pins Gateway mode to local."
|
||||||
|
echo "Gateway runtime bind comes from OPENCLAW_GATEWAY_BIND (default: lan)."
|
||||||
|
echo "Current runtime bind: $OPENCLAW_GATEWAY_BIND"
|
||||||
|
echo "Gateway token: $OPENCLAW_GATEWAY_TOKEN"
|
||||||
|
echo "Tailscale exposure: Off (use host-level tailnet/Tailscale setup separately)."
|
||||||
|
echo "Install Gateway daemon: No (managed by Docker Compose)"
|
||||||
|
echo ""
|
||||||
|
docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli onboard --mode local --no-install-daemon
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "==> Docker gateway defaults"
|
||||||
|
sync_gateway_mode_and_bind
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "==> Control UI origin allowlist"
|
||||||
|
ensure_control_ui_allowed_origins
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "==> Provider setup (optional)"
|
||||||
|
echo "WhatsApp (QR):"
|
||||||
|
echo " ${COMPOSE_HINT} run --rm openclaw-cli channels login"
|
||||||
|
echo "Telegram (bot token):"
|
||||||
|
echo " ${COMPOSE_HINT} run --rm openclaw-cli channels add --channel telegram --token <token>"
|
||||||
|
echo "Discord (bot token):"
|
||||||
|
echo " ${COMPOSE_HINT} run --rm openclaw-cli channels add --channel discord --token <token>"
|
||||||
|
echo "Docs: https://docs.openclaw.ai/channels"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "==> Starting gateway"
|
||||||
|
docker compose "${COMPOSE_ARGS[@]}" up -d openclaw-gateway
|
||||||
|
|
||||||
|
# --- Sandbox setup (opt-in via OPENCLAW_SANDBOX=1) ---
|
||||||
|
if [[ -n "$SANDBOX_ENABLED" ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "==> Sandbox setup"
|
||||||
|
|
||||||
|
# Build sandbox image if Dockerfile.sandbox exists.
|
||||||
|
if [[ -f "$ROOT_DIR/Dockerfile.sandbox" ]]; then
|
||||||
|
echo "Building sandbox image: openclaw-sandbox:bookworm-slim"
|
||||||
|
docker build \
|
||||||
|
-t "openclaw-sandbox:bookworm-slim" \
|
||||||
|
-f "$ROOT_DIR/Dockerfile.sandbox" \
|
||||||
|
"$ROOT_DIR"
|
||||||
|
else
|
||||||
|
echo "WARNING: Dockerfile.sandbox not found in $ROOT_DIR" >&2
|
||||||
|
echo " Sandbox config will be applied but no sandbox image will be built." >&2
|
||||||
|
echo " Agent exec may fail if the configured sandbox image does not exist." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Defense-in-depth: verify Docker CLI in the running image before enabling
|
||||||
|
# sandbox. This avoids claiming sandbox is enabled when the image cannot
|
||||||
|
# launch sandbox containers.
|
||||||
|
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --entrypoint docker openclaw-gateway --version >/dev/null 2>&1; then
|
||||||
|
echo "WARNING: Docker CLI not found inside the container image." >&2
|
||||||
|
echo " Sandbox requires Docker CLI. Rebuild with --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1" >&2
|
||||||
|
echo " or use a local build (OPENCLAW_IMAGE=openclaw:local). Skipping sandbox setup." >&2
|
||||||
|
SANDBOX_ENABLED=""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Apply sandbox config only if prerequisites are met.
|
||||||
|
if [[ -n "$SANDBOX_ENABLED" ]]; then
|
||||||
|
# Mount Docker socket via a dedicated compose overlay. This overlay is
|
||||||
|
# created only after sandbox prerequisites pass, so the socket is never
|
||||||
|
# exposed when sandbox cannot actually run.
|
||||||
|
if [[ -S "$DOCKER_SOCKET_PATH" ]]; then
|
||||||
|
SANDBOX_COMPOSE_FILE="$ROOT_DIR/docker-compose.sandbox.yml"
|
||||||
|
cat >"$SANDBOX_COMPOSE_FILE" <<YAML
|
||||||
|
services:
|
||||||
|
openclaw-gateway:
|
||||||
|
volumes:
|
||||||
|
- ${DOCKER_SOCKET_PATH}:/var/run/docker.sock
|
||||||
|
YAML
|
||||||
|
if [[ -n "${DOCKER_GID:-}" ]]; then
|
||||||
|
cat >>"$SANDBOX_COMPOSE_FILE" <<YAML
|
||||||
|
group_add:
|
||||||
|
- "${DOCKER_GID}"
|
||||||
|
YAML
|
||||||
|
fi
|
||||||
|
COMPOSE_ARGS+=("-f" "$SANDBOX_COMPOSE_FILE")
|
||||||
|
echo "==> Sandbox: added Docker socket mount"
|
||||||
|
else
|
||||||
|
echo "WARNING: OPENCLAW_SANDBOX enabled but Docker socket not found at $DOCKER_SOCKET_PATH." >&2
|
||||||
|
echo " Sandbox requires Docker socket access. Skipping sandbox setup." >&2
|
||||||
|
SANDBOX_ENABLED=""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$SANDBOX_ENABLED" ]]; then
|
||||||
|
# Enable sandbox in OpenClaw config.
|
||||||
|
sandbox_config_ok=true
|
||||||
|
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
|
||||||
|
config set agents.defaults.sandbox.mode "non-main" >/dev/null; then
|
||||||
|
echo "WARNING: Failed to set agents.defaults.sandbox.mode" >&2
|
||||||
|
sandbox_config_ok=false
|
||||||
|
fi
|
||||||
|
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
|
||||||
|
config set agents.defaults.sandbox.scope "agent" >/dev/null; then
|
||||||
|
echo "WARNING: Failed to set agents.defaults.sandbox.scope" >&2
|
||||||
|
sandbox_config_ok=false
|
||||||
|
fi
|
||||||
|
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
|
||||||
|
config set agents.defaults.sandbox.workspaceAccess "none" >/dev/null; then
|
||||||
|
echo "WARNING: Failed to set agents.defaults.sandbox.workspaceAccess" >&2
|
||||||
|
sandbox_config_ok=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$sandbox_config_ok" == true ]]; then
|
||||||
|
echo "Sandbox enabled: mode=non-main, scope=agent, workspaceAccess=none"
|
||||||
|
echo "Docs: https://docs.openclaw.ai/gateway/sandboxing"
|
||||||
|
# Restart gateway with sandbox compose overlay to pick up socket mount + config.
|
||||||
|
docker compose "${COMPOSE_ARGS[@]}" up -d openclaw-gateway
|
||||||
|
else
|
||||||
|
echo "WARNING: Sandbox config was partially applied. Check errors above." >&2
|
||||||
|
echo " Skipping gateway restart to avoid exposing Docker socket without a full sandbox policy." >&2
|
||||||
|
if ! docker compose "${BASE_COMPOSE_ARGS[@]}" run --rm --no-deps openclaw-cli \
|
||||||
|
config set agents.defaults.sandbox.mode "off" >/dev/null; then
|
||||||
|
echo "WARNING: Failed to roll back agents.defaults.sandbox.mode to off" >&2
|
||||||
|
else
|
||||||
|
echo "Sandbox mode rolled back to off due to partial sandbox config failure."
|
||||||
|
fi
|
||||||
|
if [[ -n "${SANDBOX_COMPOSE_FILE:-}" ]]; then
|
||||||
|
rm -f "$SANDBOX_COMPOSE_FILE"
|
||||||
|
fi
|
||||||
|
# Ensure gateway service definition is reset without sandbox overlay mount.
|
||||||
|
docker compose "${BASE_COMPOSE_ARGS[@]}" up -d --force-recreate openclaw-gateway
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Keep reruns deterministic: if sandbox is not active for this run, reset
|
||||||
|
# persisted sandbox mode so future execs do not require docker.sock by stale
|
||||||
|
# config alone.
|
||||||
|
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \
|
||||||
|
config set agents.defaults.sandbox.mode "off" >/dev/null; then
|
||||||
|
echo "WARNING: Failed to reset agents.defaults.sandbox.mode to off" >&2
|
||||||
|
fi
|
||||||
|
if [[ -f "$ROOT_DIR/docker-compose.sandbox.yml" ]]; then
|
||||||
|
rm -f "$ROOT_DIR/docker-compose.sandbox.yml"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Gateway running with host port mapping."
|
||||||
|
echo "Access from tailnet devices via the host's tailnet IP."
|
||||||
|
echo "Config: $OPENCLAW_CONFIG_DIR"
|
||||||
|
echo "Workspace: $OPENCLAW_WORKSPACE_DIR"
|
||||||
|
echo "Token: $OPENCLAW_GATEWAY_TOKEN"
|
||||||
|
echo ""
|
||||||
|
echo "Commands:"
|
||||||
|
echo " ${COMPOSE_HINT} logs -f openclaw-gateway"
|
||||||
|
echo " ${COMPOSE_HINT} exec openclaw-gateway node dist/index.js health --token \"$OPENCLAW_GATEWAY_TOKEN\""
|
||||||
@ -1,5 +1,5 @@
|
|||||||
# OpenClaw gateway — Podman Quadlet (rootless)
|
# OpenClaw gateway — Podman Quadlet (rootless)
|
||||||
# Installed by setup-podman.sh into openclaw's ~/.config/containers/systemd/
|
# Installed by scripts/podman/setup.sh into openclaw's ~/.config/containers/systemd/
|
||||||
# {{OPENCLAW_HOME}} is replaced at install time.
|
# {{OPENCLAW_HOME}} is replaced at install time.
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
|
|||||||
312
scripts/podman/setup.sh
Executable file
312
scripts/podman/setup.sh
Executable file
@ -0,0 +1,312 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# One-time host setup for rootless OpenClaw in Podman: creates the openclaw
|
||||||
|
# user, builds the image, loads it into that user's Podman store, and installs
|
||||||
|
# the launch script. Run from repo root with sudo capability.
|
||||||
|
#
|
||||||
|
# Usage: ./scripts/podman/setup.sh [--quadlet|--container]
|
||||||
|
# --quadlet Install systemd Quadlet so the container runs as a user service
|
||||||
|
# --container Only install user + image + launch script; you start the container manually (default)
|
||||||
|
# Or set OPENCLAW_PODMAN_QUADLET=1 (or 0) to choose without a flag.
|
||||||
|
#
|
||||||
|
# After this, start the gateway manually:
|
||||||
|
# ./scripts/run-openclaw-podman.sh launch
|
||||||
|
# ./scripts/run-openclaw-podman.sh launch setup # onboarding wizard
|
||||||
|
# Or as the openclaw user: sudo -u openclaw /home/openclaw/run-openclaw-podman.sh
|
||||||
|
# If you used --quadlet, you can also: sudo systemctl --machine openclaw@ --user start openclaw.service
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
OPENCLAW_USER="${OPENCLAW_PODMAN_USER:-openclaw}"
|
||||||
|
REPO_PATH="${OPENCLAW_REPO_PATH:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||||
|
RUN_SCRIPT_SRC="$REPO_PATH/scripts/run-openclaw-podman.sh"
|
||||||
|
QUADLET_TEMPLATE="$REPO_PATH/scripts/podman/openclaw.container.in"
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
if ! command -v "$1" >/dev/null 2>&1; then
|
||||||
|
echo "Missing dependency: $1" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
is_writable_dir() {
|
||||||
|
local dir="$1"
|
||||||
|
[[ -n "$dir" && -d "$dir" && ! -L "$dir" && -w "$dir" && -x "$dir" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
is_safe_tmp_base() {
|
||||||
|
local dir="$1"
|
||||||
|
local mode=""
|
||||||
|
local owner=""
|
||||||
|
is_writable_dir "$dir" || return 1
|
||||||
|
mode="$(stat -Lc '%a' "$dir" 2>/dev/null || true)"
|
||||||
|
if [[ -n "$mode" ]]; then
|
||||||
|
local perm=$((8#$mode))
|
||||||
|
if (( (perm & 0022) != 0 && (perm & 01000) == 0 )); then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if is_root; then
|
||||||
|
owner="$(stat -Lc '%u' "$dir" 2>/dev/null || true)"
|
||||||
|
if [[ -n "$owner" && "$owner" != "0" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_image_tmp_dir() {
|
||||||
|
if ! is_root && is_safe_tmp_base "${TMPDIR:-}"; then
|
||||||
|
printf '%s' "$TMPDIR"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if is_safe_tmp_base "/var/tmp"; then
|
||||||
|
printf '%s' "/var/tmp"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if is_safe_tmp_base "/tmp"; then
|
||||||
|
printf '%s' "/tmp"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf '%s' "/tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_root() { [[ "$(id -u)" -eq 0 ]]; }
|
||||||
|
|
||||||
|
run_root() {
|
||||||
|
if is_root; then
|
||||||
|
"$@"
|
||||||
|
else
|
||||||
|
sudo "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_as_user() {
|
||||||
|
# When switching users, the caller's cwd may be inaccessible to the target
|
||||||
|
# user (e.g. a private home dir). Wrap in a subshell that cd's to a
|
||||||
|
# world-traversable directory so sudo/runuser don't fail with "cannot chdir".
|
||||||
|
# TODO: replace with fully rootless podman build to eliminate the need for
|
||||||
|
# user-switching entirely.
|
||||||
|
local user="$1"
|
||||||
|
shift
|
||||||
|
if command -v sudo >/dev/null 2>&1; then
|
||||||
|
( cd /tmp 2>/dev/null || cd /; sudo -u "$user" "$@" )
|
||||||
|
elif is_root && command -v runuser >/dev/null 2>&1; then
|
||||||
|
( cd /tmp 2>/dev/null || cd /; runuser -u "$user" -- "$@" )
|
||||||
|
else
|
||||||
|
echo "Need sudo (or root+runuser) to run commands as $user." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_as_openclaw() {
|
||||||
|
# Avoid root writes into $OPENCLAW_HOME (symlink/hardlink/TOCTOU footguns).
|
||||||
|
# Anything under the target user's home should be created/modified as that user.
|
||||||
|
run_as_user "$OPENCLAW_USER" env HOME="$OPENCLAW_HOME" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
escape_sed_replacement_pipe_delim() {
|
||||||
|
# Escape replacement metacharacters for sed "s|...|...|g" replacement text.
|
||||||
|
printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Quadlet: opt-in via --quadlet or OPENCLAW_PODMAN_QUADLET=1
|
||||||
|
INSTALL_QUADLET=false
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--quadlet) INSTALL_QUADLET=true ;;
|
||||||
|
--container) INSTALL_QUADLET=false ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [[ -n "${OPENCLAW_PODMAN_QUADLET:-}" ]]; then
|
||||||
|
case "${OPENCLAW_PODMAN_QUADLET,,}" in
|
||||||
|
1|yes|true) INSTALL_QUADLET=true ;;
|
||||||
|
0|no|false) INSTALL_QUADLET=false ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_cmd podman
|
||||||
|
if ! is_root; then
|
||||||
|
require_cmd sudo
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$REPO_PATH/Dockerfile" ]]; then
|
||||||
|
echo "Dockerfile not found at $REPO_PATH. Set OPENCLAW_REPO_PATH to the repo root." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$RUN_SCRIPT_SRC" ]]; then
|
||||||
|
echo "Launch script not found at $RUN_SCRIPT_SRC." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
generate_token_hex_32() {
|
||||||
|
if command -v openssl >/dev/null 2>&1; then
|
||||||
|
openssl rand -hex 32
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
|
python3 - <<'PY'
|
||||||
|
import secrets
|
||||||
|
print(secrets.token_hex(32))
|
||||||
|
PY
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if command -v od >/dev/null 2>&1; then
|
||||||
|
# 32 random bytes -> 64 lowercase hex chars
|
||||||
|
od -An -N32 -tx1 /dev/urandom | tr -d " \n"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "Missing dependency: need openssl or python3 (or od) to generate OPENCLAW_GATEWAY_TOKEN." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
user_exists() {
|
||||||
|
local user="$1"
|
||||||
|
if command -v getent >/dev/null 2>&1; then
|
||||||
|
getent passwd "$user" >/dev/null 2>&1 && return 0
|
||||||
|
fi
|
||||||
|
id -u "$user" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_user_home() {
|
||||||
|
local user="$1"
|
||||||
|
local home=""
|
||||||
|
if command -v getent >/dev/null 2>&1; then
|
||||||
|
home="$(getent passwd "$user" 2>/dev/null | cut -d: -f6 || true)"
|
||||||
|
fi
|
||||||
|
if [[ -z "$home" && -f /etc/passwd ]]; then
|
||||||
|
home="$(awk -F: -v u="$user" '$1==u {print $6}' /etc/passwd 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
if [[ -z "$home" ]]; then
|
||||||
|
home="/home/$user"
|
||||||
|
fi
|
||||||
|
printf '%s' "$home"
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_nologin_shell() {
|
||||||
|
for cand in /usr/sbin/nologin /sbin/nologin /usr/bin/nologin /bin/false; do
|
||||||
|
if [[ -x "$cand" ]]; then
|
||||||
|
printf '%s' "$cand"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
printf '%s' "/usr/sbin/nologin"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create openclaw user (non-login, with home) if missing
|
||||||
|
if ! user_exists "$OPENCLAW_USER"; then
|
||||||
|
NOLOGIN_SHELL="$(resolve_nologin_shell)"
|
||||||
|
echo "Creating user $OPENCLAW_USER ($NOLOGIN_SHELL, with home)..."
|
||||||
|
if command -v useradd >/dev/null 2>&1; then
|
||||||
|
run_root useradd -m -s "$NOLOGIN_SHELL" "$OPENCLAW_USER"
|
||||||
|
elif command -v adduser >/dev/null 2>&1; then
|
||||||
|
# Debian/Ubuntu: adduser supports --disabled-password/--gecos. Busybox adduser differs.
|
||||||
|
run_root adduser --disabled-password --gecos "" --shell "$NOLOGIN_SHELL" "$OPENCLAW_USER"
|
||||||
|
else
|
||||||
|
echo "Neither useradd nor adduser found, cannot create user $OPENCLAW_USER." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "User $OPENCLAW_USER already exists."
|
||||||
|
fi
|
||||||
|
|
||||||
|
OPENCLAW_HOME="$(resolve_user_home "$OPENCLAW_USER")"
|
||||||
|
OPENCLAW_UID="$(id -u "$OPENCLAW_USER" 2>/dev/null || true)"
|
||||||
|
OPENCLAW_CONFIG="$OPENCLAW_HOME/.openclaw"
|
||||||
|
LAUNCH_SCRIPT_DST="$OPENCLAW_HOME/run-openclaw-podman.sh"
|
||||||
|
|
||||||
|
# Prefer systemd user services (Quadlet) for production. Enable lingering early so rootless Podman can run
|
||||||
|
# without an interactive login.
|
||||||
|
if command -v loginctl &>/dev/null; then
|
||||||
|
run_root loginctl enable-linger "$OPENCLAW_USER" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [[ -n "${OPENCLAW_UID:-}" && -d /run/user ]] && command -v systemctl &>/dev/null; then
|
||||||
|
if [[ ! -d "/run/user/$OPENCLAW_UID" ]]; then
|
||||||
|
run_root install -d -m 700 -o "$OPENCLAW_UID" -g "$OPENCLAW_UID" "/run/user/$OPENCLAW_UID" || true
|
||||||
|
fi
|
||||||
|
run_root mkdir -p "/run/user/$OPENCLAW_UID/containers" || true
|
||||||
|
run_root chown "$OPENCLAW_UID:$OPENCLAW_UID" "/run/user/$OPENCLAW_UID/containers" || true
|
||||||
|
run_root chmod 700 "/run/user/$OPENCLAW_UID/containers" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir_user_dirs_as_openclaw() {
|
||||||
|
run_root install -d -m 700 -o "$OPENCLAW_UID" -g "$OPENCLAW_UID" "$OPENCLAW_HOME" "$OPENCLAW_CONFIG"
|
||||||
|
run_root install -d -m 700 -o "$OPENCLAW_UID" -g "$OPENCLAW_UID" "$OPENCLAW_CONFIG/workspace"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_subid_entry() {
|
||||||
|
local file="$1"
|
||||||
|
if [[ ! -f "$file" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
grep -q "^${OPENCLAW_USER}:" "$file" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! ensure_subid_entry /etc/subuid || ! ensure_subid_entry /etc/subgid; then
|
||||||
|
echo "WARNING: ${OPENCLAW_USER} may not have subuid/subgid ranges configured." >&2
|
||||||
|
echo "If rootless Podman fails, add 'openclaw:100000:65536' to both /etc/subuid and /etc/subgid." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir_user_dirs_as_openclaw
|
||||||
|
|
||||||
|
IMAGE_TMP_BASE="$(resolve_image_tmp_dir)"
|
||||||
|
echo "Using temp base for image export: $IMAGE_TMP_BASE"
|
||||||
|
IMAGE_TAR_DIR="$(mktemp -d "${IMAGE_TMP_BASE%/}/openclaw-podman-image.XXXXXX")"
|
||||||
|
chmod 700 "$IMAGE_TAR_DIR"
|
||||||
|
IMAGE_TAR="$IMAGE_TAR_DIR/openclaw-image.tar"
|
||||||
|
cleanup_image_tar() {
|
||||||
|
rm -rf "$IMAGE_TAR_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup_image_tar EXIT
|
||||||
|
|
||||||
|
BUILD_ARGS=()
|
||||||
|
if [[ -n "${OPENCLAW_DOCKER_APT_PACKAGES:-}" ]]; then
|
||||||
|
BUILD_ARGS+=(--build-arg "OPENCLAW_DOCKER_APT_PACKAGES=${OPENCLAW_DOCKER_APT_PACKAGES}")
|
||||||
|
fi
|
||||||
|
if [[ -n "${OPENCLAW_EXTENSIONS:-}" ]]; then
|
||||||
|
BUILD_ARGS+=(--build-arg "OPENCLAW_EXTENSIONS=${OPENCLAW_EXTENSIONS}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building image openclaw:local..."
|
||||||
|
podman build -t openclaw:local -f "$REPO_PATH/Dockerfile" "${BUILD_ARGS[@]}" "$REPO_PATH"
|
||||||
|
echo "Saving image to $IMAGE_TAR ..."
|
||||||
|
podman save -o "$IMAGE_TAR" openclaw:local
|
||||||
|
|
||||||
|
echo "Loading image into $OPENCLAW_USER Podman store..."
|
||||||
|
run_as_openclaw podman load -i "$IMAGE_TAR"
|
||||||
|
|
||||||
|
echo "Installing launch script to $LAUNCH_SCRIPT_DST ..."
|
||||||
|
run_root install -m 0755 -o "$OPENCLAW_UID" -g "$OPENCLAW_UID" "$RUN_SCRIPT_SRC" "$LAUNCH_SCRIPT_DST"
|
||||||
|
|
||||||
|
if [[ ! -f "$OPENCLAW_CONFIG/.env" ]]; then
|
||||||
|
TOKEN="$(generate_token_hex_32)"
|
||||||
|
run_as_openclaw sh -lc "umask 077 && printf '%s\n' 'OPENCLAW_GATEWAY_TOKEN=$TOKEN' > '$OPENCLAW_CONFIG/.env'"
|
||||||
|
echo "Generated OPENCLAW_GATEWAY_TOKEN and wrote it to $OPENCLAW_CONFIG/.env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$OPENCLAW_CONFIG/openclaw.json" ]]; then
|
||||||
|
run_as_openclaw sh -lc "umask 077 && cat > '$OPENCLAW_CONFIG/openclaw.json' <<'JSON'
|
||||||
|
{ \"gateway\": { \"mode\": \"local\" } }
|
||||||
|
JSON"
|
||||||
|
echo "Wrote minimal config to $OPENCLAW_CONFIG/openclaw.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$INSTALL_QUADLET" == true ]]; then
|
||||||
|
QUADLET_DIR="$OPENCLAW_HOME/.config/containers/systemd"
|
||||||
|
QUADLET_DST="$QUADLET_DIR/openclaw.container"
|
||||||
|
echo "Installing Quadlet to $QUADLET_DST ..."
|
||||||
|
run_as_openclaw mkdir -p "$QUADLET_DIR"
|
||||||
|
OPENCLAW_HOME_ESCAPED="$(escape_sed_replacement_pipe_delim "$OPENCLAW_HOME")"
|
||||||
|
sed "s|{{OPENCLAW_HOME}}|$OPENCLAW_HOME_ESCAPED|g" "$QUADLET_TEMPLATE" | \
|
||||||
|
run_as_openclaw sh -lc "cat > '$QUADLET_DST'"
|
||||||
|
run_as_openclaw chmod 0644 "$QUADLET_DST"
|
||||||
|
|
||||||
|
echo "Reloading and enabling user service..."
|
||||||
|
run_root systemctl --machine "${OPENCLAW_USER}@" --user daemon-reload
|
||||||
|
run_root systemctl --machine "${OPENCLAW_USER}@" --user enable --now openclaw.service
|
||||||
|
echo "Quadlet installed and service started."
|
||||||
|
else
|
||||||
|
echo "Container + launch script installed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Next:"
|
||||||
|
echo " ./scripts/run-openclaw-podman.sh launch"
|
||||||
|
echo " ./scripts/run-openclaw-podman.sh launch setup"
|
||||||
@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Rootless OpenClaw in Podman: run after one-time setup.
|
# Rootless OpenClaw in Podman: run after one-time setup.
|
||||||
#
|
#
|
||||||
# One-time setup (from repo root): ./setup-podman.sh
|
# One-time setup (from repo root): ./scripts/podman/setup.sh
|
||||||
# Then:
|
# Then:
|
||||||
# ./scripts/run-openclaw-podman.sh launch # Start gateway
|
# ./scripts/run-openclaw-podman.sh launch # Start gateway
|
||||||
# ./scripts/run-openclaw-podman.sh launch setup # Onboarding wizard
|
# ./scripts/run-openclaw-podman.sh launch setup # Onboarding wizard
|
||||||
@ -10,7 +10,7 @@
|
|||||||
# sudo -u openclaw /home/openclaw/run-openclaw-podman.sh
|
# sudo -u openclaw /home/openclaw/run-openclaw-podman.sh
|
||||||
# sudo -u openclaw /home/openclaw/run-openclaw-podman.sh setup
|
# sudo -u openclaw /home/openclaw/run-openclaw-podman.sh setup
|
||||||
#
|
#
|
||||||
# Legacy: "setup-host" delegates to ../setup-podman.sh
|
# Legacy: "setup-host" delegates to the Podman setup script
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@ -35,15 +35,19 @@ OPENCLAW_HOME="$(resolve_user_home "$OPENCLAW_USER")"
|
|||||||
OPENCLAW_UID="$(id -u "$OPENCLAW_USER" 2>/dev/null || true)"
|
OPENCLAW_UID="$(id -u "$OPENCLAW_USER" 2>/dev/null || true)"
|
||||||
LAUNCH_SCRIPT="$OPENCLAW_HOME/run-openclaw-podman.sh"
|
LAUNCH_SCRIPT="$OPENCLAW_HOME/run-openclaw-podman.sh"
|
||||||
|
|
||||||
# Legacy: setup-host → run setup-podman.sh
|
# Legacy: setup-host → run the Podman setup script
|
||||||
if [[ "${1:-}" == "setup-host" ]]; then
|
if [[ "${1:-}" == "setup-host" ]]; then
|
||||||
shift
|
shift
|
||||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
SETUP_PODMAN="$REPO_ROOT/scripts/podman/setup.sh"
|
||||||
|
if [[ -f "$SETUP_PODMAN" ]]; then
|
||||||
|
exec "$SETUP_PODMAN" "$@"
|
||||||
|
fi
|
||||||
SETUP_PODMAN="$REPO_ROOT/setup-podman.sh"
|
SETUP_PODMAN="$REPO_ROOT/setup-podman.sh"
|
||||||
if [[ -f "$SETUP_PODMAN" ]]; then
|
if [[ -f "$SETUP_PODMAN" ]]; then
|
||||||
exec "$SETUP_PODMAN" "$@"
|
exec "$SETUP_PODMAN" "$@"
|
||||||
fi
|
fi
|
||||||
echo "setup-podman.sh not found at $SETUP_PODMAN. Run from repo root: ./setup-podman.sh" >&2
|
echo "Podman setup script not found. Run from repo root: ./scripts/podman/setup.sh" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -228,4 +232,4 @@ podman run --pull="$PODMAN_PULL" -d --replace \
|
|||||||
|
|
||||||
echo "Container $CONTAINER_NAME started. Dashboard: http://127.0.0.1:${HOST_GATEWAY_PORT}/"
|
echo "Container $CONTAINER_NAME started. Dashboard: http://127.0.0.1:${HOST_GATEWAY_PORT}/"
|
||||||
echo "Logs: podman logs -f $CONTAINER_NAME"
|
echo "Logs: podman logs -f $CONTAINER_NAME"
|
||||||
echo "For auto-start/restarts, use: ./setup-podman.sh --quadlet (Quadlet + systemd user service)."
|
echo "For auto-start/restarts, use: ./scripts/podman/setup.sh --quadlet (Quadlet + systemd user service)."
|
||||||
|
|||||||
310
setup-podman.sh
310
setup-podman.sh
@ -1,312 +1,12 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# One-time host setup for rootless OpenClaw in Podman: creates the openclaw
|
|
||||||
# user, builds the image, loads it into that user's Podman store, and installs
|
|
||||||
# the launch script. Run from repo root with sudo capability.
|
|
||||||
#
|
|
||||||
# Usage: ./setup-podman.sh [--quadlet|--container]
|
|
||||||
# --quadlet Install systemd Quadlet so the container runs as a user service
|
|
||||||
# --container Only install user + image + launch script; you start the container manually (default)
|
|
||||||
# Or set OPENCLAW_PODMAN_QUADLET=1 (or 0) to choose without a flag.
|
|
||||||
#
|
|
||||||
# After this, start the gateway manually:
|
|
||||||
# ./scripts/run-openclaw-podman.sh launch
|
|
||||||
# ./scripts/run-openclaw-podman.sh launch setup # onboarding wizard
|
|
||||||
# Or as the openclaw user: sudo -u openclaw /home/openclaw/run-openclaw-podman.sh
|
|
||||||
# If you used --quadlet, you can also: sudo systemctl --machine openclaw@ --user start openclaw.service
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
OPENCLAW_USER="${OPENCLAW_PODMAN_USER:-openclaw}"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
REPO_PATH="${OPENCLAW_REPO_PATH:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
SCRIPT_PATH="$ROOT_DIR/scripts/podman/setup.sh"
|
||||||
RUN_SCRIPT_SRC="$REPO_PATH/scripts/run-openclaw-podman.sh"
|
|
||||||
QUADLET_TEMPLATE="$REPO_PATH/scripts/podman/openclaw.container.in"
|
|
||||||
|
|
||||||
require_cmd() {
|
if [[ ! -f "$SCRIPT_PATH" ]]; then
|
||||||
if ! command -v "$1" >/dev/null 2>&1; then
|
echo "Podman setup script not found at $SCRIPT_PATH" >&2
|
||||||
echo "Missing dependency: $1" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
is_writable_dir() {
|
|
||||||
local dir="$1"
|
|
||||||
[[ -n "$dir" && -d "$dir" && ! -L "$dir" && -w "$dir" && -x "$dir" ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
is_safe_tmp_base() {
|
|
||||||
local dir="$1"
|
|
||||||
local mode=""
|
|
||||||
local owner=""
|
|
||||||
is_writable_dir "$dir" || return 1
|
|
||||||
mode="$(stat -Lc '%a' "$dir" 2>/dev/null || true)"
|
|
||||||
if [[ -n "$mode" ]]; then
|
|
||||||
local perm=$((8#$mode))
|
|
||||||
if (( (perm & 0022) != 0 && (perm & 01000) == 0 )); then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if is_root; then
|
|
||||||
owner="$(stat -Lc '%u' "$dir" 2>/dev/null || true)"
|
|
||||||
if [[ -n "$owner" && "$owner" != "0" ]]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve_image_tmp_dir() {
|
|
||||||
if ! is_root && is_safe_tmp_base "${TMPDIR:-}"; then
|
|
||||||
printf '%s' "$TMPDIR"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if is_safe_tmp_base "/var/tmp"; then
|
|
||||||
printf '%s' "/var/tmp"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if is_safe_tmp_base "/tmp"; then
|
|
||||||
printf '%s' "/tmp"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
printf '%s' "/tmp"
|
|
||||||
}
|
|
||||||
|
|
||||||
is_root() { [[ "$(id -u)" -eq 0 ]]; }
|
|
||||||
|
|
||||||
run_root() {
|
|
||||||
if is_root; then
|
|
||||||
"$@"
|
|
||||||
else
|
|
||||||
sudo "$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
run_as_user() {
|
|
||||||
# When switching users, the caller's cwd may be inaccessible to the target
|
|
||||||
# user (e.g. a private home dir). Wrap in a subshell that cd's to a
|
|
||||||
# world-traversable directory so sudo/runuser don't fail with "cannot chdir".
|
|
||||||
# TODO: replace with fully rootless podman build to eliminate the need for
|
|
||||||
# user-switching entirely.
|
|
||||||
local user="$1"
|
|
||||||
shift
|
|
||||||
if command -v sudo >/dev/null 2>&1; then
|
|
||||||
( cd /tmp 2>/dev/null || cd /; sudo -u "$user" "$@" )
|
|
||||||
elif is_root && command -v runuser >/dev/null 2>&1; then
|
|
||||||
( cd /tmp 2>/dev/null || cd /; runuser -u "$user" -- "$@" )
|
|
||||||
else
|
|
||||||
echo "Need sudo (or root+runuser) to run commands as $user." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
run_as_openclaw() {
|
|
||||||
# Avoid root writes into $OPENCLAW_HOME (symlink/hardlink/TOCTOU footguns).
|
|
||||||
# Anything under the target user's home should be created/modified as that user.
|
|
||||||
run_as_user "$OPENCLAW_USER" env HOME="$OPENCLAW_HOME" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
escape_sed_replacement_pipe_delim() {
|
|
||||||
# Escape replacement metacharacters for sed "s|...|...|g" replacement text.
|
|
||||||
printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Quadlet: opt-in via --quadlet or OPENCLAW_PODMAN_QUADLET=1
|
|
||||||
INSTALL_QUADLET=false
|
|
||||||
for arg in "$@"; do
|
|
||||||
case "$arg" in
|
|
||||||
--quadlet) INSTALL_QUADLET=true ;;
|
|
||||||
--container) INSTALL_QUADLET=false ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
if [[ -n "${OPENCLAW_PODMAN_QUADLET:-}" ]]; then
|
|
||||||
case "${OPENCLAW_PODMAN_QUADLET,,}" in
|
|
||||||
1|yes|true) INSTALL_QUADLET=true ;;
|
|
||||||
0|no|false) INSTALL_QUADLET=false ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
require_cmd podman
|
|
||||||
if ! is_root; then
|
|
||||||
require_cmd sudo
|
|
||||||
fi
|
|
||||||
if [[ ! -f "$REPO_PATH/Dockerfile" ]]; then
|
|
||||||
echo "Dockerfile not found at $REPO_PATH. Set OPENCLAW_REPO_PATH to the repo root." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -f "$RUN_SCRIPT_SRC" ]]; then
|
|
||||||
echo "Launch script not found at $RUN_SCRIPT_SRC." >&2
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
generate_token_hex_32() {
|
exec "$SCRIPT_PATH" "$@"
|
||||||
if command -v openssl >/dev/null 2>&1; then
|
|
||||||
openssl rand -hex 32
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if command -v python3 >/dev/null 2>&1; then
|
|
||||||
python3 - <<'PY'
|
|
||||||
import secrets
|
|
||||||
print(secrets.token_hex(32))
|
|
||||||
PY
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if command -v od >/dev/null 2>&1; then
|
|
||||||
# 32 random bytes -> 64 lowercase hex chars
|
|
||||||
od -An -N32 -tx1 /dev/urandom | tr -d " \n"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
echo "Missing dependency: need openssl or python3 (or od) to generate OPENCLAW_GATEWAY_TOKEN." >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
user_exists() {
|
|
||||||
local user="$1"
|
|
||||||
if command -v getent >/dev/null 2>&1; then
|
|
||||||
getent passwd "$user" >/dev/null 2>&1 && return 0
|
|
||||||
fi
|
|
||||||
id -u "$user" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve_user_home() {
|
|
||||||
local user="$1"
|
|
||||||
local home=""
|
|
||||||
if command -v getent >/dev/null 2>&1; then
|
|
||||||
home="$(getent passwd "$user" 2>/dev/null | cut -d: -f6 || true)"
|
|
||||||
fi
|
|
||||||
if [[ -z "$home" && -f /etc/passwd ]]; then
|
|
||||||
home="$(awk -F: -v u="$user" '$1==u {print $6}' /etc/passwd 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
if [[ -z "$home" ]]; then
|
|
||||||
home="/home/$user"
|
|
||||||
fi
|
|
||||||
printf '%s' "$home"
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve_nologin_shell() {
|
|
||||||
for cand in /usr/sbin/nologin /sbin/nologin /usr/bin/nologin /bin/false; do
|
|
||||||
if [[ -x "$cand" ]]; then
|
|
||||||
printf '%s' "$cand"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
printf '%s' "/usr/sbin/nologin"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create openclaw user (non-login, with home) if missing
|
|
||||||
if ! user_exists "$OPENCLAW_USER"; then
|
|
||||||
NOLOGIN_SHELL="$(resolve_nologin_shell)"
|
|
||||||
echo "Creating user $OPENCLAW_USER ($NOLOGIN_SHELL, with home)..."
|
|
||||||
if command -v useradd >/dev/null 2>&1; then
|
|
||||||
run_root useradd -m -s "$NOLOGIN_SHELL" "$OPENCLAW_USER"
|
|
||||||
elif command -v adduser >/dev/null 2>&1; then
|
|
||||||
# Debian/Ubuntu: adduser supports --disabled-password/--gecos. Busybox adduser differs.
|
|
||||||
run_root adduser --disabled-password --gecos "" --shell "$NOLOGIN_SHELL" "$OPENCLAW_USER"
|
|
||||||
else
|
|
||||||
echo "Neither useradd nor adduser found, cannot create user $OPENCLAW_USER." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "User $OPENCLAW_USER already exists."
|
|
||||||
fi
|
|
||||||
|
|
||||||
OPENCLAW_HOME="$(resolve_user_home "$OPENCLAW_USER")"
|
|
||||||
OPENCLAW_UID="$(id -u "$OPENCLAW_USER" 2>/dev/null || true)"
|
|
||||||
OPENCLAW_CONFIG="$OPENCLAW_HOME/.openclaw"
|
|
||||||
LAUNCH_SCRIPT_DST="$OPENCLAW_HOME/run-openclaw-podman.sh"
|
|
||||||
|
|
||||||
# Prefer systemd user services (Quadlet) for production. Enable lingering early so rootless Podman can run
|
|
||||||
# without an interactive login.
|
|
||||||
if command -v loginctl &>/dev/null; then
|
|
||||||
run_root loginctl enable-linger "$OPENCLAW_USER" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
if [[ -n "${OPENCLAW_UID:-}" && -d /run/user ]] && command -v systemctl &>/dev/null; then
|
|
||||||
run_root systemctl start "user@${OPENCLAW_UID}.service" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Rootless Podman needs subuid/subgid for the run user
|
|
||||||
if ! grep -q "^${OPENCLAW_USER}:" /etc/subuid 2>/dev/null; then
|
|
||||||
echo "Warning: $OPENCLAW_USER has no subuid range. Rootless Podman may fail." >&2
|
|
||||||
echo " Add a line to /etc/subuid and /etc/subgid, e.g.: $OPENCLAW_USER:100000:65536" >&2
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Creating $OPENCLAW_CONFIG and workspace..."
|
|
||||||
run_as_openclaw mkdir -p "$OPENCLAW_CONFIG/workspace"
|
|
||||||
run_as_openclaw chmod 700 "$OPENCLAW_CONFIG" "$OPENCLAW_CONFIG/workspace" 2>/dev/null || true
|
|
||||||
|
|
||||||
ENV_FILE="$OPENCLAW_CONFIG/.env"
|
|
||||||
if run_as_openclaw test -f "$ENV_FILE"; then
|
|
||||||
if ! run_as_openclaw grep -q '^OPENCLAW_GATEWAY_TOKEN=' "$ENV_FILE" 2>/dev/null; then
|
|
||||||
TOKEN="$(generate_token_hex_32)"
|
|
||||||
printf 'OPENCLAW_GATEWAY_TOKEN=%s\n' "$TOKEN" | run_as_openclaw tee -a "$ENV_FILE" >/dev/null
|
|
||||||
echo "Added OPENCLAW_GATEWAY_TOKEN to $ENV_FILE."
|
|
||||||
fi
|
|
||||||
run_as_openclaw chmod 600 "$ENV_FILE" 2>/dev/null || true
|
|
||||||
else
|
|
||||||
TOKEN="$(generate_token_hex_32)"
|
|
||||||
printf 'OPENCLAW_GATEWAY_TOKEN=%s\n' "$TOKEN" | run_as_openclaw tee "$ENV_FILE" >/dev/null
|
|
||||||
run_as_openclaw chmod 600 "$ENV_FILE" 2>/dev/null || true
|
|
||||||
echo "Created $ENV_FILE with new token."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# The gateway refuses to start unless gateway.mode=local is set in config.
|
|
||||||
# Make first-run non-interactive; users can run the wizard later to configure channels/providers.
|
|
||||||
OPENCLAW_JSON="$OPENCLAW_CONFIG/openclaw.json"
|
|
||||||
if ! run_as_openclaw test -f "$OPENCLAW_JSON"; then
|
|
||||||
printf '%s\n' '{ gateway: { mode: "local" } }' | run_as_openclaw tee "$OPENCLAW_JSON" >/dev/null
|
|
||||||
run_as_openclaw chmod 600 "$OPENCLAW_JSON" 2>/dev/null || true
|
|
||||||
echo "Created $OPENCLAW_JSON (minimal gateway.mode=local)."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Building image from $REPO_PATH..."
|
|
||||||
BUILD_ARGS=()
|
|
||||||
[[ -n "${OPENCLAW_DOCKER_APT_PACKAGES:-}" ]] && BUILD_ARGS+=(--build-arg "OPENCLAW_DOCKER_APT_PACKAGES=${OPENCLAW_DOCKER_APT_PACKAGES}")
|
|
||||||
[[ -n "${OPENCLAW_EXTENSIONS:-}" ]] && BUILD_ARGS+=(--build-arg "OPENCLAW_EXTENSIONS=${OPENCLAW_EXTENSIONS}")
|
|
||||||
podman build ${BUILD_ARGS[@]+"${BUILD_ARGS[@]}"} -t openclaw:local -f "$REPO_PATH/Dockerfile" "$REPO_PATH"
|
|
||||||
|
|
||||||
echo "Loading image into $OPENCLAW_USER's Podman store..."
|
|
||||||
TMP_IMAGE_DIR="$(resolve_image_tmp_dir)"
|
|
||||||
echo "Using temporary image dir: $TMP_IMAGE_DIR"
|
|
||||||
TMP_STAGE_DIR="$(mktemp -d -p "$TMP_IMAGE_DIR" openclaw-image.XXXXXX)"
|
|
||||||
TMP_IMAGE="$TMP_STAGE_DIR/image.tar"
|
|
||||||
chmod 700 "$TMP_STAGE_DIR"
|
|
||||||
trap 'rm -rf "$TMP_STAGE_DIR"' EXIT
|
|
||||||
podman save openclaw:local -o "$TMP_IMAGE"
|
|
||||||
chmod 600 "$TMP_IMAGE"
|
|
||||||
# Stream the image into the target user's podman load so private temp directories
|
|
||||||
# do not need to be traversable by $OPENCLAW_USER.
|
|
||||||
cat "$TMP_IMAGE" | run_as_user "$OPENCLAW_USER" env HOME="$OPENCLAW_HOME" podman load
|
|
||||||
rm -rf "$TMP_STAGE_DIR"
|
|
||||||
trap - EXIT
|
|
||||||
|
|
||||||
echo "Copying launch script to $LAUNCH_SCRIPT_DST..."
|
|
||||||
run_root cat "$RUN_SCRIPT_SRC" | run_as_openclaw tee "$LAUNCH_SCRIPT_DST" >/dev/null
|
|
||||||
run_as_openclaw chmod 755 "$LAUNCH_SCRIPT_DST"
|
|
||||||
|
|
||||||
# Optionally install systemd quadlet for openclaw user (rootless Podman + systemd)
|
|
||||||
QUADLET_DIR="$OPENCLAW_HOME/.config/containers/systemd"
|
|
||||||
if [[ "$INSTALL_QUADLET" == true && -f "$QUADLET_TEMPLATE" ]]; then
|
|
||||||
echo "Installing systemd quadlet for $OPENCLAW_USER..."
|
|
||||||
run_as_openclaw mkdir -p "$QUADLET_DIR"
|
|
||||||
OPENCLAW_HOME_SED="$(escape_sed_replacement_pipe_delim "$OPENCLAW_HOME")"
|
|
||||||
sed "s|{{OPENCLAW_HOME}}|$OPENCLAW_HOME_SED|g" "$QUADLET_TEMPLATE" | run_as_openclaw tee "$QUADLET_DIR/openclaw.container" >/dev/null
|
|
||||||
run_as_openclaw chmod 700 "$OPENCLAW_HOME/.config" "$OPENCLAW_HOME/.config/containers" "$QUADLET_DIR" 2>/dev/null || true
|
|
||||||
run_as_openclaw chmod 600 "$QUADLET_DIR/openclaw.container" 2>/dev/null || true
|
|
||||||
if command -v systemctl &>/dev/null; then
|
|
||||||
run_root systemctl --machine "${OPENCLAW_USER}@" --user daemon-reload 2>/dev/null || true
|
|
||||||
run_root systemctl --machine "${OPENCLAW_USER}@" --user enable openclaw.service 2>/dev/null || true
|
|
||||||
run_root systemctl --machine "${OPENCLAW_USER}@" --user start openclaw.service 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Setup complete. Start the gateway:"
|
|
||||||
echo " $RUN_SCRIPT_SRC launch"
|
|
||||||
echo " $RUN_SCRIPT_SRC launch setup # onboarding wizard"
|
|
||||||
echo "Or as $OPENCLAW_USER (e.g. from cron):"
|
|
||||||
echo " sudo -u $OPENCLAW_USER $LAUNCH_SCRIPT_DST"
|
|
||||||
echo " sudo -u $OPENCLAW_USER $LAUNCH_SCRIPT_DST setup"
|
|
||||||
if [[ "$INSTALL_QUADLET" == true ]]; then
|
|
||||||
echo "Or use systemd (quadlet):"
|
|
||||||
echo " sudo systemctl --machine ${OPENCLAW_USER}@ --user start openclaw.service"
|
|
||||||
echo " sudo systemctl --machine ${OPENCLAW_USER}@ --user status openclaw.service"
|
|
||||||
else
|
|
||||||
echo "To install systemd quadlet later: $0 --quadlet"
|
|
||||||
fi
|
|
||||||
|
|||||||
@ -50,13 +50,14 @@ exit 0
|
|||||||
|
|
||||||
async function createDockerSetupSandbox(): Promise<DockerSetupSandbox> {
|
async function createDockerSetupSandbox(): Promise<DockerSetupSandbox> {
|
||||||
const rootDir = await mkdtemp(join(tmpdir(), "openclaw-docker-setup-"));
|
const rootDir = await mkdtemp(join(tmpdir(), "openclaw-docker-setup-"));
|
||||||
const scriptPath = join(rootDir, "docker-setup.sh");
|
const scriptPath = join(rootDir, "scripts", "docker", "setup.sh");
|
||||||
const dockerfilePath = join(rootDir, "Dockerfile");
|
const dockerfilePath = join(rootDir, "Dockerfile");
|
||||||
const composePath = join(rootDir, "docker-compose.yml");
|
const composePath = join(rootDir, "docker-compose.yml");
|
||||||
const binDir = join(rootDir, "bin");
|
const binDir = join(rootDir, "bin");
|
||||||
const logPath = join(rootDir, "docker-stub.log");
|
const logPath = join(rootDir, "docker-stub.log");
|
||||||
|
|
||||||
await copyFile(join(repoRoot, "docker-setup.sh"), scriptPath);
|
await mkdir(join(rootDir, "scripts", "docker"), { recursive: true });
|
||||||
|
await copyFile(join(repoRoot, "scripts", "docker", "setup.sh"), scriptPath);
|
||||||
await chmod(scriptPath, 0o755);
|
await chmod(scriptPath, 0o755);
|
||||||
await writeFile(dockerfilePath, "FROM scratch\n");
|
await writeFile(dockerfilePath, "FROM scratch\n");
|
||||||
await writeFile(
|
await writeFile(
|
||||||
@ -168,7 +169,7 @@ function resolveBashForCompatCheck(): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("docker-setup.sh", () => {
|
describe("scripts/docker/setup.sh", () => {
|
||||||
let sandbox: DockerSetupSandbox | null = null;
|
let sandbox: DockerSetupSandbox | null = null;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@ -439,7 +440,7 @@ describe("docker-setup.sh", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("avoids associative arrays so the script remains Bash 3.2-compatible", async () => {
|
it("avoids associative arrays so the script remains Bash 3.2-compatible", async () => {
|
||||||
const script = await readFile(join(repoRoot, "docker-setup.sh"), "utf8");
|
const script = await readFile(join(repoRoot, "scripts", "docker", "setup.sh"), "utf8");
|
||||||
expect(script).not.toMatch(/^\s*declare -A\b/m);
|
expect(script).not.toMatch(/^\s*declare -A\b/m);
|
||||||
|
|
||||||
const systemBash = resolveBashForCompatCheck();
|
const systemBash = resolveBashForCompatCheck();
|
||||||
@ -456,9 +457,13 @@ describe("docker-setup.sh", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const syntaxCheck = spawnSync(systemBash, ["-n", join(repoRoot, "docker-setup.sh")], {
|
const syntaxCheck = spawnSync(
|
||||||
encoding: "utf8",
|
systemBash,
|
||||||
});
|
["-n", join(repoRoot, "scripts", "docker", "setup.sh")],
|
||||||
|
{
|
||||||
|
encoding: "utf8",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
expect(syntaxCheck.status).toBe(0);
|
expect(syntaxCheck.status).toBe(0);
|
||||||
expect(syntaxCheck.stderr).not.toContain("declare: -A: invalid option");
|
expect(syntaxCheck.stderr).not.toContain("declare: -A: invalid option");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user