#!/bin/bash ############################################################################### # Generate a new remote agent instance from the template. # - Interactive mode: no flags, prompts for values. # - Non-interactive (ChatOps) mode: any flag (-a/-n/-p/-u/-k/-m/-t) enables it; # all required params must be provided, otherwise exits with JSON error. ############################################################################### set -e WORKSPACE="${WORKSPACE:-/root/.openclaw/workspace}" TEMPLATE_DIR="$WORKSPACE/remote-blueprints/template" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } log_ok() { echo -e "${GREEN}[OK]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_err() { echo -e "${RED}[ERROR]${NC} $1"; } # ---------- parse flags (non-interactive mode detection) ---------- NONINTERACTIVE=false AGENT_ID="" AGENT_NAME="" PROJECT_ID="" LLM_BASE_URL="" LLM_API_KEY="" LLM_MODEL_ID="" CONTROL_UI_TOKEN="" while getopts "a:n:p:u:k:m:t:" opt; do NONINTERACTIVE=true case "$opt" in a) AGENT_ID="$OPTARG" ;; n) AGENT_NAME="$OPTARG" ;; p) PROJECT_ID="$OPTARG" ;; u) LLM_BASE_URL="$OPTARG" ;; k) LLM_API_KEY="$OPTARG" ;; m) LLM_MODEL_ID="$OPTARG" ;; t) CONTROL_UI_TOKEN="$OPTARG" ;; *) ;; esac done shift $((OPTIND-1)) # ---------- interactive fallback (no flags at all) ---------- if [ "$NONINTERACTIVE" = false ]; then echo -n "AGENT_ID (required): " read -r AGENT_ID if [ -z "$AGENT_ID" ]; then log_err "AGENT_ID is required." exit 1 fi echo -n "AGENT_NAME (optional, default = AGENT_ID): " read -r AGENT_NAME echo -n "PROJECT_ID (optional, default = default): " read -r PROJECT_ID echo -n "LLM_BASE_URL (required): " read -r LLM_BASE_URL echo -n "LLM_API_KEY (required): " read -r LLM_API_KEY echo -n "LLM_MODEL_ID (required, e.g. qwen-max, gpt-4o): " read -r LLM_MODEL_ID echo -n "CONTROL_UI_TOKEN (optional, auto-generate if empty): " read -r CONTROL_UI_TOKEN else # ---------- strict non-interactive mode ---------- missing=() [ -z "$AGENT_ID" ] && missing+=("AGENT_ID") [ -z "$LLM_BASE_URL" ] && missing+=("LLM_BASE_URL") [ -z "$LLM_API_KEY" ] && missing+=("LLM_API_KEY") [ -z "$LLM_MODEL_ID" ] && missing+=("LLM_MODEL_ID") if [ ${#missing[@]} -ne 0 ]; then # JSON error for ChatOps callers printf '{"ok":false,"error":"missing_required_params","missing":[' >&2 for i in "${!missing[@]}"; do [ "$i" -ne 0 ] && printf ',' >&2 printf '"%s"' "${missing[$i]}" >&2 done printf ']}' >&2 echo >&2 exit 1 fi fi # Defaults for optional values [ -z "$AGENT_NAME" ] && AGENT_NAME="$AGENT_ID" [ -z "$PROJECT_ID" ] && PROJECT_ID="default" # ---------- validate AGENT_ID ---------- if ! echo "$AGENT_ID" | grep -qE '^[a-zA-Z0-9_-]+$'; then log_err "AGENT_ID must contain only letters, numbers, hyphens, and underscores." exit 1 fi # ---------- ensure template dir exists ---------- if [ ! -d "$TEMPLATE_DIR" ]; then log_err "Template not found: $TEMPLATE_DIR" exit 1 fi # ---------- auto-generate CONTROL_UI_TOKEN if empty ---------- if [ -z "$CONTROL_UI_TOKEN" ]; then if command -v openssl >/dev/null 2>&1; then CONTROL_UI_TOKEN=$(openssl rand -hex 24) log_info "Generated CONTROL_UI_TOKEN via openssl." else CONTROL_UI_TOKEN=$(head -c 32 /dev/urandom | base64 | tr -dc 'a-f0-9' | head -c 48) log_warn "openssl not found; CONTROL_UI_TOKEN generated via /dev/urandom fallback." fi fi INSTANCE_DIR="$WORKSPACE/remote-blueprints/$AGENT_ID" if [ -d "$INSTANCE_DIR" ]; then if [ "$NONINTERACTIVE" = true ]; then log_err "Instance already exists: $INSTANCE_DIR" exit 1 fi log_warn "Instance already exists: $INSTANCE_DIR" echo -n "Overwrite? (y/N): " read -r confirm if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then log_info "Aborted." exit 0 fi rm -rf "$INSTANCE_DIR" fi # ---------- copy template ---------- log_info "Copying template to $INSTANCE_DIR ..." mkdir -p "$INSTANCE_DIR" cp -r "$TEMPLATE_DIR"/* "$INSTANCE_DIR/" [ -f "$TEMPLATE_DIR/.env.tpl" ] && cp "$TEMPLATE_DIR/.env.tpl" "$INSTANCE_DIR/.env.tpl" log_ok "Copy done." # ---------- render templates (safe sed with # delimiter) ---------- escape_sed_val() { local v="$1" v="${v//\\/\\\\}" v="${v//&/\\&}" v="${v//\//\\/}" printf '%s' "$v" } AGENT_NAME_ESC=$(escape_sed_val "$AGENT_NAME") PROJECT_ID_ESC=$(escape_sed_val "$PROJECT_ID") LLM_BASE_URL_ESC=$(escape_sed_val "$LLM_BASE_URL") LLM_API_KEY_ESC=$(escape_sed_val "$LLM_API_KEY") LLM_MODEL_ID_ESC=$(escape_sed_val "$LLM_MODEL_ID") CONTROL_UI_TOKEN_ESC=$(escape_sed_val "$CONTROL_UI_TOKEN") # Render all *.tpl and .env.tpl files while IFS= read -r -d '' f; do log_info "Rendering $f ..." sed -i "s#{{AGENT_ID}}#$AGENT_ID#g" "$f" sed -i "s#{{AGENT_NAME}}#$AGENT_NAME_ESC#g" "$f" sed -i "s#{{PROJECT_ID}}#$PROJECT_ID_ESC#g" "$f" sed -i "s#{{LLM_BASE_URL}}#$LLM_BASE_URL_ESC#g" "$f" sed -i "s#{{LLM_API_KEY}}#$LLM_API_KEY_ESC#g" "$f" sed -i "s#{{LLM_MODEL_ID}}#$LLM_MODEL_ID_ESC#g" "$f" sed -i "s#{{CONTROL_UI_TOKEN}}#$CONTROL_UI_TOKEN_ESC#g" "$f" # Backward-compat: clean any legacy BAILIAN placeholders if present sed -i "s#{{BAILIAN_API_KEY}}#$LLM_API_KEY_ESC#g" "$f" || true done < <(find "$INSTANCE_DIR" -type f \( -name '*.tpl' -o -name '.env.tpl' \) -print0) # ---------- rename agents/{{AGENT_ID}}.json.tpl -> agents/.json.tpl (before generic .tpl removal) ---------- if [ -f "$INSTANCE_DIR/agents/{{AGENT_ID}}.json.tpl" ]; then mv "$INSTANCE_DIR/agents/{{AGENT_ID}}.json.tpl" "$INSTANCE_DIR/agents/$AGENT_ID.json.tpl" fi # ---------- rename all *.tpl to final filenames ---------- while IFS= read -r -d '' f; do base="${f%.tpl}" mv "$f" "$base" log_info "Renamed to $base" done < <(find "$INSTANCE_DIR" -type f -name '*.tpl' -print0) # .env.tpl -> .env if [ -f "$INSTANCE_DIR/.env.tpl" ]; then mv "$INSTANCE_DIR/.env.tpl" "$INSTANCE_DIR/.env" log_info "Renamed .env.tpl to .env" fi # ---------- generate deploy_to_target.sh ---------- DEPLOY_SCRIPT="$INSTANCE_DIR/deploy_to_target.sh" cat > "$DEPLOY_SCRIPT" << 'DEPLOYEOF' #!/bin/bash # Deploy this agent directory to a remote host. Usage: ./deploy_to_target.sh [SSH_USER] set -e TARGET_IP="${1:?Usage: $0 [SSH_USER]}" SSH_USER="${2:-root}" AGENT_ID="__AGENT_ID_PLACEHOLDER__" REMOTE_PATH="/opt/openclaw-remote/$AGENT_ID" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" if [ ! -f "$SCRIPT_DIR/docker-compose.yml" ]; then echo "[ERROR] docker-compose.yml not found in $SCRIPT_DIR" exit 1 fi echo "[INFO] Syncing $SCRIPT_DIR to ${SSH_USER}@${TARGET_IP}:$REMOTE_PATH ..." ssh "${SSH_USER}@${TARGET_IP}" "mkdir -p $REMOTE_PATH" (cd "$SCRIPT_DIR" && tar cf - .) | ssh "${SSH_USER}@${TARGET_IP}" "cd $REMOTE_PATH && tar xf -" echo "[INFO] Remote: docker compose down && docker compose up -d --build ..." ssh "${SSH_USER}@${TARGET_IP}" "cd $REMOTE_PATH && docker compose down 2>/dev/null || true && docker compose up -d --build" echo "[OK] Deploy done." DEPLOYEOF # inject actual AGENT_ID into deploy script sed -i "s#__AGENT_ID_PLACEHOLDER__#$AGENT_ID#g" "$DEPLOY_SCRIPT" chmod +x "$DEPLOY_SCRIPT" log_ok "Generated $DEPLOY_SCRIPT" log_ok "Instance ready: $INSTANCE_DIR" echo "[OK] CONTROL_UI_TOKEN: $CONTROL_UI_TOKEN" echo "Next: cd $INSTANCE_DIR && ./deploy_to_target.sh [SSH_USER]"