Files
bot/docker-setup.sh
hanzo-dev 0fa6a33a93 Eradicate OpenClaw — resolve merge conflicts and finish the rebrand
Three coordinated things:

1. Resolve the committed upstream/main merge conflicts. 94 files were
   sitting with raw <<<<<<< HEAD ... >>>>>>> upstream/main blocks. Every
   conflict had HanzoBot/Bot on HEAD and OpenClaw on upstream. Resolved
   by keeping HEAD everywhere — 540 conflict blocks.

2. Rewrite remaining OpenClaw text refs.
     OPENCLAW   → HANZO_BOT
     OpenClaw   → HanzoBot          (PascalCase type identifiers)
     openclaw   → bot               (codebase convention — botToken,
                                    botUsername etc.; avoids invalid
                                    hyphenated JS identifiers)
     openclaw[_]/openclaw[A-Z] → bot[_]/bot[A-Z]   (compound)
     ai.openclaw.x → ai.hanzo.bot.x  (JVM package path)

3. Delete or rename openclaw-named file paths (116 of them). Dead
   duplicates with a Bot-named canonical from the partial migration:
   deleted. Otherwise renamed.
     ai/openclaw/**           → deleted (ai/hanzo/bot/** canonical)
     Sources/OpenClaw*/       → deleted (Sources/Bot* canonical)
     Tests/OpenClawIPCTests/  → deleted (Tests/BotIPCTests canonical)
     OpenClawKit/             → deleted (BotKit canonical)
     openclaw-tools.*.ts      → deleted (bot-tools.*.ts canonical)
     openclaw-root.ts etc.    → deleted (bot-root.ts canonical)
     types.openclaw.ts        → deleted (types.bot.ts canonical)
     extensions/*/openclaw.plugin.json → renamed hanzo-bot.plugin.json
     docs/start/openclaw.md   → renamed hanzo-bot.md (+ zh-CN)
     docs/assets/openclaw-*.png, whatsapp-openclaw*.jpg → deleted
     scripts/.../openclaw-*   → deleted (bot/hanzo-bot parallels)
     openclaw.mjs             → deleted (hanzo-bot.mjs is package.bin)

Then patched 65 broken JS/TS import strings where hanzo-bot had ended
up inside an import path ('./types.hanzo-bot.js' → './types.bot.js'
etc., since the on-disk filename uses the brand-neutral bot- prefix).

Pre-existing lint debt cleaned up to get oxlint --type-aware to 0/0:
removed the dead i18n test that referenced a path no longer existing;
defined the missing DIDConfig and WalletConfig types in types.base.ts
(they were imported but never declared); dropped unused imports;
collapsed redundant type assertion and a tautological meta.bot lookup
that the openclaw→bot rename made redundant.

The a2ui.bundle.js generated artifact is kept at its pre-rebrand
contents — it gets regenerated by 'pnpm canvas:a2ui:bundle' from
sources I cannot rebuild in this commit; the source side is clean.

Verified: 0 occurrences of openclaw (case-insensitive) in tree source
content, 0 file or directory paths with openclaw in the name; oxlint
--type-aware src test reports 0/0.
2026-05-11 17:56:29 -07:00

394 lines
13 KiB
Bash
Executable File

#!/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"
SANDBOX_COMPOSE_FILE="$ROOT_DIR/docker-compose.sandbox.yml"
IMAGE_NAME="${BOT_IMAGE:-bot:local}"
EXTRA_MOUNTS="${BOT_EXTRA_MOUNTS:-}"
HOME_VOLUME_NAME="${BOT_HOME_VOLUME:-}"
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Missing dependency: $1" >&2
exit 1
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
# --- Input validation ---
# Reject control characters (newlines, tabs) in BOT_EXTRA_MOUNTS to prevent
# YAML injection into the generated docker-compose.extra.yml overlay.
# Use tr to detect control chars portably (grep -P is not available on macOS).
if [[ -n "$EXTRA_MOUNTS" ]]; then
_cleaned="$(printf '%s' "$EXTRA_MOUNTS" | tr -d '[:cntrl:]')"
if [[ "$_cleaned" != "$EXTRA_MOUNTS" ]]; then
echo "BOT_EXTRA_MOUNTS cannot contain control characters" >&2
exit 1
fi
fi
# Validate each mount entry has the host:container colon-separated format.
if [[ -n "$EXTRA_MOUNTS" ]]; then
IFS=',' read -r -a _validate_mounts <<<"$EXTRA_MOUNTS"
for _vm in "${_validate_mounts[@]}"; do
_vm="${_vm#"${_vm%%[![:space:]]*}"}"
_vm="${_vm%"${_vm##*[![:space:]]}"}"
if [[ -n "$_vm" && "$_vm" != *":"* ]]; then
echo "Invalid mount format: '$_vm' (expected host:container)" >&2
exit 1
fi
done
fi
# Validate BOT_HOME_VOLUME matches Docker named volume pattern.
if [[ -n "$HOME_VOLUME_NAME" ]] && ! printf '%s' "$HOME_VOLUME_NAME" | grep -qE '^[a-zA-Z0-9][a-zA-Z0-9_.-]*$'; then
echo "BOT_HOME_VOLUME must match [a-zA-Z0-9][a-zA-Z0-9_.-]* (got '$HOME_VOLUME_NAME')" >&2
exit 1
fi
# --- Resolve sandbox flag ---
# Derive sandbox enabled state from BOT_SANDBOX env var.
# "0", "false", "no", and empty are treated as disabled.
SANDBOX_ENABLED=""
case "${BOT_SANDBOX:-}" in
1|true|yes) SANDBOX_ENABLED=1 ;;
esac
export BOT_SANDBOX="${BOT_SANDBOX:-}"
BOT_CONFIG_DIR="${BOT_CONFIG_DIR:-$HOME/.bot}"
BOT_WORKSPACE_DIR="${BOT_WORKSPACE_DIR:-$HOME/.hanzo/bot/workspace}"
mkdir -p "$BOT_CONFIG_DIR"
mkdir -p "$BOT_WORKSPACE_DIR"
# Pre-create identity directory so the CLI can write device auth files
# without hitting EACCES in the container.
mkdir -p "$BOT_CONFIG_DIR/identity"
# Pre-create agent data directories to avoid EACCES when the container
# tries to write session and agent data.
mkdir -p "$BOT_CONFIG_DIR/agents/main/agent"
mkdir -p "$BOT_CONFIG_DIR/agents/main/sessions"
export BOT_CONFIG_DIR
export BOT_WORKSPACE_DIR
export BOT_GATEWAY_PORT="${BOT_GATEWAY_PORT:-18789}"
export BOT_BRIDGE_PORT="${BOT_BRIDGE_PORT:-18790}"
export BOT_GATEWAY_BIND="${BOT_GATEWAY_BIND:-lan}"
export BOT_IMAGE="$IMAGE_NAME"
export BOT_DOCKER_APT_PACKAGES="${BOT_DOCKER_APT_PACKAGES:-}"
export BOT_EXTRA_MOUNTS="$EXTRA_MOUNTS"
export BOT_HOME_VOLUME="$HOME_VOLUME_NAME"
# Reuse gateway token from existing config if BOT_GATEWAY_TOKEN is not set.
if [[ -z "${BOT_GATEWAY_TOKEN:-}" ]]; then
config_file="$BOT_CONFIG_DIR/bot.json"
if [[ -f "$config_file" ]] && command -v python3 >/dev/null 2>&1; then
existing_token="$(python3 -c "
import json, sys
try:
cfg = json.load(open('$config_file'))
token = cfg.get('gateway', {}).get('auth', {}).get('token', '')
if token:
print(token, end='')
except Exception:
pass
" 2>/dev/null || true)"
if [[ -n "$existing_token" ]]; then
BOT_GATEWAY_TOKEN="$existing_token"
fi
fi
fi
if [[ -z "${BOT_GATEWAY_TOKEN:-}" ]]; then
if command -v openssl >/dev/null 2>&1; then
BOT_GATEWAY_TOKEN="$(openssl rand -hex 32)"
else
BOT_GATEWAY_TOKEN="$(python3 - <<'PY'
import secrets
print(secrets.token_hex(32))
PY
)"
fi
fi
export BOT_GATEWAY_TOKEN
COMPOSE_FILES=("$COMPOSE_FILE")
COMPOSE_ARGS=()
write_extra_compose() {
local home_volume="$1"
shift
local mount
cat >"$EXTRA_COMPOSE_FILE" <<'YAML'
services:
bot-gateway:
volumes:
YAML
if [[ -n "$home_volume" ]]; then
printf ' - %s:/home/node\n' "$home_volume" >>"$EXTRA_COMPOSE_FILE"
printf ' - %s:/home/node/.bot\n' "$BOT_CONFIG_DIR" >>"$EXTRA_COMPOSE_FILE"
printf ' - %s:/home/node/.hanzo/bot/workspace\n' "$BOT_WORKSPACE_DIR" >>"$EXTRA_COMPOSE_FILE"
fi
for mount in "$@"; do
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
done
cat >>"$EXTRA_COMPOSE_FILE" <<'YAML'
bot-cli:
volumes:
YAML
if [[ -n "$home_volume" ]]; then
printf ' - %s:/home/node\n' "$home_volume" >>"$EXTRA_COMPOSE_FILE"
printf ' - %s:/home/node/.bot\n' "$BOT_CONFIG_DIR" >>"$EXTRA_COMPOSE_FILE"
printf ' - %s:/home/node/.hanzo/bot/workspace\n' "$BOT_WORKSPACE_DIR" >>"$EXTRA_COMPOSE_FILE"
fi
for mount in "$@"; do
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
done
if [[ -n "$home_volume" && "$home_volume" != *"/"* ]]; then
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.
export BOT_INSTALL_DOCKER_CLI="${BOT_INSTALL_DOCKER_CLI:-}"
if [[ -n "$SANDBOX_ENABLED" ]]; then
if [[ -z "$BOT_INSTALL_DOCKER_CLI" ]]; then
export BOT_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" \
BOT_CONFIG_DIR \
BOT_WORKSPACE_DIR \
BOT_GATEWAY_PORT \
BOT_BRIDGE_PORT \
BOT_GATEWAY_BIND \
BOT_GATEWAY_TOKEN \
BOT_IMAGE \
BOT_EXTRA_MOUNTS \
BOT_HOME_VOLUME \
BOT_DOCKER_APT_PACKAGES \
BOT_SANDBOX
echo "==> Building Docker image: $IMAGE_NAME"
docker build \
--build-arg "BOT_DOCKER_APT_PACKAGES=${BOT_DOCKER_APT_PACKAGES}" \
--build-arg "BOT_INSTALL_DOCKER_CLI=${BOT_INSTALL_DOCKER_CLI}" \
-t "$IMAGE_NAME" \
-f "$ROOT_DIR/Dockerfile" \
"$ROOT_DIR"
# 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 Bot metadata subdirectory
# (.hanzo/bot/) inside the workspace gets chowned, not the user's project files.
docker compose "${COMPOSE_ARGS[@]}" run --rm --user root --entrypoint sh bot-cli -c \
'find /home/node/.bot -xdev -exec chown node:node {} +; \
[ -d /home/node/.hanzo/bot/workspace/.bot ] && chown -R node:node /home/node/.hanzo/bot/workspace/.bot || true'
echo ""
echo "==> Onboarding (interactive)"
echo "When prompted:"
echo " - Gateway bind: lan"
echo " - Gateway auth: token"
echo " - Gateway token: $BOT_GATEWAY_TOKEN"
echo " - Tailscale exposure: Off"
echo " - Install Gateway daemon: No"
echo ""
docker compose "${COMPOSE_ARGS[@]}" run --rm bot-cli onboard --mode local --no-install-daemon
# Apply gateway configuration defaults.
docker compose "${COMPOSE_ARGS[@]}" run --rm bot-cli config set gateway.mode local
docker compose "${COMPOSE_ARGS[@]}" run --rm bot-cli config set gateway.bind lan
# --- Sandbox configuration ---
GATEWAY_STARTED=""
if [[ -n "$SANDBOX_ENABLED" ]]; then
# Verify the Docker CLI is available inside the gateway image.
DOCKER_CLI_OK=""
if docker compose "${COMPOSE_ARGS[@]}" run --rm --entrypoint docker bot-gateway --version >/dev/null 2>&1; then
DOCKER_CLI_OK=1
fi
if [[ -z "$DOCKER_CLI_OK" ]]; then
echo "Sandbox requires Docker CLI inside the gateway image; skipping sandbox setup." >&2
SANDBOX_ENABLED=""
fi
fi
if [[ -n "$SANDBOX_ENABLED" ]]; then
# Start the gateway first so sandbox config writes can run against it.
docker compose "${COMPOSE_ARGS[@]}" up -d bot-gateway
# Write sandbox configuration via CLI config commands.
SANDBOX_CONFIG_OK=1
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps bot-cli config set agents.defaults.sandbox.mode non-main 2>/dev/null; then
echo "Failed to set agents.defaults.sandbox.mode" >&2
SANDBOX_CONFIG_OK=""
fi
if ! docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps bot-cli config set agents.defaults.sandbox.scope session 2>/dev/null; then
echo "Failed to set agents.defaults.sandbox.scope" >&2
SANDBOX_CONFIG_OK=""
fi
if [[ -z "$SANDBOX_CONFIG_OK" ]]; then
echo "Skipping gateway restart to avoid exposing Docker socket with incomplete sandbox config." >&2
# Reset sandbox mode to off since config is incomplete.
docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps bot-cli config set agents.defaults.sandbox.mode off 2>/dev/null || true
# Remove sandbox overlay to ensure no docker.sock mount remains.
rm -f "$SANDBOX_COMPOSE_FILE"
# Force-recreate gateway without the sandbox overlay.
docker compose "${BASE_COMPOSE_ARGS[@]}" up -d --force-recreate bot-gateway
GATEWAY_STARTED=1
else
# Generate sandbox compose overlay with docker.sock mount.
BOT_DOCKER_SOCKET="${BOT_DOCKER_SOCKET:-/var/run/docker.sock}"
cat >"$SANDBOX_COMPOSE_FILE" <<YAML
services:
bot-gateway:
volumes:
- ${BOT_DOCKER_SOCKET}:/var/run/docker.sock
YAML
COMPOSE_FILES+=("$SANDBOX_COMPOSE_FILE")
COMPOSE_ARGS=()
for compose_file in "${COMPOSE_FILES[@]}"; do
COMPOSE_ARGS+=("-f" "$compose_file")
done
docker compose "${COMPOSE_ARGS[@]}" up -d --force-recreate bot-gateway
GATEWAY_STARTED=1
fi
else
# When sandbox is not active, clean up stale sandbox state.
docker compose "${COMPOSE_ARGS[@]}" run --rm --no-deps bot-cli config set agents.defaults.sandbox.mode off 2>/dev/null || true
rm -f "$SANDBOX_COMPOSE_FILE"
fi
echo ""
echo "==> Provider setup (optional)"
echo "WhatsApp (QR):"
echo " ${COMPOSE_HINT} run --rm bot-cli channels login"
echo "Telegram (bot token):"
echo " ${COMPOSE_HINT} run --rm bot-cli channels add --channel telegram --token <token>"
echo "Discord (bot token):"
echo " ${COMPOSE_HINT} run --rm bot-cli channels add --channel discord --token <token>"
echo "Docs: https://docs.hanzo.bot/channels"
if [[ -z "$GATEWAY_STARTED" ]]; then
echo ""
echo "==> Starting gateway"
docker compose "${COMPOSE_ARGS[@]}" up -d bot-gateway
fi
echo ""
echo "Gateway running with host port mapping."
echo "Access from tailnet devices via the host's tailnet IP."
echo "Config: $BOT_CONFIG_DIR"
echo "Workspace: $BOT_WORKSPACE_DIR"
echo "Token: $BOT_GATEWAY_TOKEN"
echo ""
echo "Commands:"
echo " ${COMPOSE_HINT} logs -f bot-gateway"
echo " ${COMPOSE_HINT} exec bot-gateway node dist/index.js health --token \"$BOT_GATEWAY_TOKEN\""