From 3e6662bfe918a7e25bc2282842050501e85e64fe Mon Sep 17 00:00:00 2001 From: null-crafter <163591881+null-crafter@users.noreply.github.com> Date: Wed, 15 Jul 2026 18:48:16 +0000 Subject: [PATCH 1/9] feat: add install.sh for curl-bash combo --- README.md | 21 + install.sh | 488 ++++++++++++++++++ .../seed_claw_mentor.cpython-314.pyc | Bin 0 -> 18468 bytes {examples => scripts}/seed_claw_mentor.py | 57 +- 4 files changed, 550 insertions(+), 16 deletions(-) create mode 100755 install.sh create mode 100644 scripts/__pycache__/seed_claw_mentor.cpython-314.pyc rename {examples => scripts}/seed_claw_mentor.py (88%) diff --git a/README.md b/README.md index 85240c4..dbbd8e3 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,27 @@ LLM Provider (Anthropic, OpenRouter, etc.) ## Quick Start +### Automated setup (one command) + +On a fresh Debian/Ubuntu server, [`install.sh`](install.sh) does the whole server side -- installs OpenClaw (or NemoClaw), Caddy with automatic TLS, the firewall, and the `iblai-openclaw-extensions` plugin, then prints the gateway token, an Ed25519 device key, and the exact API call to register the instance with ibl.ai. Pass `IBLAI_API_KEY` and it does the platform side too -- creating the claw instance and a mentor (wiring in the device key) via [`scripts/seed_claw_mentor.py`](scripts/seed_claw_mentor.py): + +```bash +# OpenClaw (default) +curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh \ + | DOMAIN=claw.example.com ANTHROPIC_API_KEY=sk-ant-... bash + +# NemoClaw +curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh \ + | CLAW_TYPE=nemoclaw DOMAIN=claw.example.com ANTHROPIC_API_KEY=sk-ant-... bash + +# ...and seed the ibl.ai platform in the same run (instance + mentor) +curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh \ + | DOMAIN=claw.example.com ANTHROPIC_API_KEY=sk-ant-... \ + IBLAI_API_KEY=your-platform-key IBLAI_ORG=your-org bash +``` + +Point `DOMAIN`'s DNS A record at the server and open ports 80/443 first. Optional vars: `MODEL`, `SANDBOX_NAME` (NemoClaw), `INSTALL_PLUGIN=no`, `SETUP_FIREWALL=no`, and for seeding `IBLAI_HOST` / `IBLAI_ORG` / `AGENT_NAME`. Re-running is safe -- it never regenerates an existing token or key. Prefer the manual steps below to understand each piece. + ### 1. Set up the server SSH into your VPS, install OpenClaw, configure Caddy for TLS, and start the gateway as a systemd service. diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..a1c9c34 --- /dev/null +++ b/install.sh @@ -0,0 +1,488 @@ +#!/usr/bin/env bash +# +# install.sh -- one-command server setup for a claw gateway + ibl.ai. +# +# Installs and wires up, on a fresh Debian/Ubuntu host: +# * OpenClaw (host systemd service) OR NemoClaw (Docker/OpenShell sandbox) +# * Caddy reverse proxy with automatic Let's Encrypt TLS +# * the host firewall (UFW) +# * the optional iblai-openclaw-extensions plugin +# * an Ed25519 device-identity keypair for the ibl.ai platform +# +# and prints the gateway token + keypair. If you pass IBLAI_API_KEY it also seeds +# the ibl.ai platform (claw instance + mentor + wiring, incl. the device key) via +# scripts/seed_claw_mentor.py; otherwise it prints the API call for you to run. +# +# Usage (OpenClaw, the default): +# curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh \ +# | DOMAIN=claw.example.com ANTHROPIC_API_KEY=sk-ant-... bash +# +# Usage (NemoClaw): +# curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh \ +# | CLAW_TYPE=nemoclaw DOMAIN=claw.example.com ANTHROPIC_API_KEY=sk-ant-... bash +# +# Config (environment variables): +# DOMAIN (required) hostname whose DNS A record points at this server +# ANTHROPIC_API_KEY (required for openclaw; nemoclaw collects it in its wizard) +# CLAW_TYPE openclaw (default) | nemoclaw +# MODEL default: anthropic/claude-sonnet-4-6 +# SANDBOX_NAME nemoclaw only, default: main +# INSTALL_PLUGIN yes (default) | no +# SETUP_FIREWALL yes (default) | no +# +# Platform seeding (optional; runs only when IBLAI_API_KEY is set): +# IBLAI_API_KEY ibl.ai platform API key -- presence enables seeding +# IBLAI_HOST default: https://base.manager.iblai.app +# IBLAI_ORG tenant/org slug, default: main +# IBLAI_USER_ID mentor owner, default: admin +# IBLAI_CLAW_TYPE platform claw_type, default: openclaw +# AGENT_NAME default: Claw Agent (also AGENT_DESCRIPTION, AGENT_ID, AGENT_CONFIG) +# +# Re-running is safe: it never regenerates an existing gateway token or device +# key (doing so would break platform pairing) and skips already-installed steps. +# +# Interactive bits (the NemoClaw onboarding wizard) read from /dev/tty, so the +# curl | bash form still works. See docs/server-setup.md and docs/nemoclaw-setup.md. +set -euo pipefail + +# ---- configuration (environment-driven) -------------------------------------- +CLAW_TYPE="${CLAW_TYPE:-openclaw}" +DOMAIN="${DOMAIN:-}" +ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-}" +MODEL="${MODEL:-anthropic/claude-sonnet-5}" +SANDBOX_NAME="${SANDBOX_NAME:-main}" +INSTALL_PLUGIN="${INSTALL_PLUGIN:-yes}" +SETUP_FIREWALL="${SETUP_FIREWALL:-yes}" + +# Platform seeding (optional -- only runs when IBLAI_API_KEY is non-empty). +IBLAI_API_KEY="${IBLAI_API_KEY:-}" +IBLAI_HOST="${IBLAI_HOST:-https://base.manager.iblai.app}" +IBLAI_ORG="${IBLAI_ORG:-main}" +IBLAI_USER_ID="${IBLAI_USER_ID:-admin}" +IBLAI_CLAW_TYPE="${IBLAI_CLAW_TYPE:-openclaw}" +AGENT_NAME="${AGENT_NAME:-Claw Agent}" +AGENT_DESCRIPTION="${AGENT_DESCRIPTION:-}" +AGENT_ID="${AGENT_ID:-main}" +AGENT_CONFIG="${AGENT_CONFIG:-}" +SEED_URL="${SEED_URL:-https://raw.githubusercontent.com/iblai/claw-setup/main/scripts/seed_claw_mentor.py}" + +GATEWAY_PORT=18789 +OPENCLAW_HOME="${HOME}/.openclaw" +DEVICE_KEY_PEM="${OPENCLAW_HOME}/platform_device_identity.pem" +PLUGIN_REPO="https://github.com/iblai/iblai-openclaw-extensions-plugin.git" +PLUGIN_DIR="/opt/iblai-openclaw-extensions" + +log() { printf '\033[1;32m==>\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; } +die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; } + +usage() { awk 'NR==1{next} /^#/{sub(/^#[[:space:]]?/,""); print; next} {exit}' "$0" 2>/dev/null || true; } + +# Run an interactive command attached to the real terminal even under curl|bash. +tty_run() { + if [ -e /dev/tty ]; then "$@" /dev/tty 2>&1; else "$@"; fi +} + +# Append an export to ~/.bashrc once (survives new SSH sessions). +persist_env() { + local key="$1" val="$2" + grep -q "^export ${key}=" "${HOME}/.bashrc" 2>/dev/null \ + || printf 'export %s=%s\n' "$key" "$val" >> "${HOME}/.bashrc" +} + +# Escape a PEM file into a JSON string body (no surrounding quotes). +json_escape_file() { + awk 'BEGIN{ORS=""} {gsub(/\\/,"\\\\"); gsub(/"/,"\\\""); print $0 "\\n"}' "$1" +} + +# ---- validation -------------------------------------------------------------- +case "${1:-}" in -h|--help) usage; exit 0 ;; esac +[ -n "$DOMAIN" ] || { usage; die "DOMAIN is required (e.g. DOMAIN=claw.example.com)"; } +case "$CLAW_TYPE" in openclaw|nemoclaw) ;; *) die "CLAW_TYPE must be 'openclaw' or 'nemoclaw'";; esac +[ "$(id -u)" -eq 0 ] || die "run as root (the setup guides assume root@server)" +command -v apt-get >/dev/null 2>&1 || die "this installer targets Debian/Ubuntu (apt not found)" +if [ "$CLAW_TYPE" = openclaw ] && [ -z "$ANTHROPIC_API_KEY" ]; then + die "ANTHROPIC_API_KEY is required for openclaw" +fi + +# ---- shared: Node.js 22 ------------------------------------------------------ +install_node() { + local major + major="$(node --version 2>/dev/null | sed 's/^v//; s/\..*//')" || true + if [ "${major:-0}" -ge 22 ] 2>/dev/null; then + log "Node.js $(node --version) already installed" + return + fi + log "Installing Node.js 22" + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - + apt-get install -y nodejs +} + +# ---- shared: Ed25519 device identity (for the platform side) ----------------- +gen_device_key() { + mkdir -p "$OPENCLAW_HOME" + if [ ! -f "$DEVICE_KEY_PEM" ]; then + log "Generating Ed25519 device-identity keypair for the platform" + openssl genpkey -algorithm ed25519 -out "$DEVICE_KEY_PEM" # PKCS8 -> BEGIN PRIVATE KEY + chmod 600 "$DEVICE_KEY_PEM" + else + log "Reusing existing device-identity keypair ($DEVICE_KEY_PEM)" + fi +} + +# ---- shared: Caddy + TLS ----------------------------------------------------- +install_caddy() { + if ! command -v caddy >/dev/null 2>&1; then + log "Installing Caddy" + apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \ + | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \ + | tee /etc/apt/sources.list.d/caddy-stable.list + apt-get update && apt-get install -y caddy + else + log "Caddy already installed" + fi + + # Caddyfile is fully derived from DOMAIN -- write it every run (back up any existing). + log "Writing /etc/caddy/Caddyfile for ${DOMAIN}" + [ -f /etc/caddy/Caddyfile ] && cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak + cat > /etc/caddy/Caddyfile </dev/null 2>&1 || apt-get install -y ufw + log "Configuring UFW (22, 80, 443)" + ufw allow outgoing + ufw allow 22/tcp + ufw allow 80/tcp + ufw allow 443/tcp + ufw --force enable +} + +# ---- OpenClaw ---------------------------------------------------------------- +openclaw_install() { + install_node + if ! command -v openclaw >/dev/null 2>&1; then + log "Installing OpenClaw" + npm install -g openclaw@latest + else + log "OpenClaw $(openclaw --version 2>/dev/null) already installed" + fi + + # Gateway token: reuse the persisted one if present, else mint a fresh one. + local env_file="${OPENCLAW_HOME}/gateway.systemd.env" + mkdir -p "$OPENCLAW_HOME" + if [ -f "$env_file" ]; then + OPENCLAW_GATEWAY_TOKEN="$(sed -n 's/^OPENCLAW_GATEWAY_TOKEN=//p' "$env_file" | head -1)" + fi + if [ -z "${OPENCLAW_GATEWAY_TOKEN:-}" ]; then + log "Generating gateway token" + OPENCLAW_GATEWAY_TOKEN="$(openssl rand -hex 32)" + else + log "Reusing existing gateway token" + fi + persist_env OPENCLAW_GATEWAY_TOKEN "$OPENCLAW_GATEWAY_TOKEN" + persist_env ANTHROPIC_API_KEY "$ANTHROPIC_API_KEY" + + # Full config up front -> skips the interactive onboarding wizard. + local ver + ver="$(openclaw --version 2>/dev/null | head -1 | tr -d '[:space:]')" + ver="${ver:-latest}" + if [ ! -f "${OPENCLAW_HOME}/openclaw.json" ]; then + log "Writing ${OPENCLAW_HOME}/openclaw.json (version=${ver}, model=${MODEL})" + cat > "${OPENCLAW_HOME}/openclaw.json" < "${HOME}/.config/systemd/user/openclaw-gateway.service.d/10-env-file.conf" <<'DROPIN' +[Service] +EnvironmentFile=-/root/.openclaw/gateway.systemd.env +DROPIN + printf 'OPENCLAW_GATEWAY_TOKEN=%s\n' "$OPENCLAW_GATEWAY_TOKEN" > "${OPENCLAW_HOME}/gateway.systemd.env" + chmod 600 "${OPENCLAW_HOME}/gateway.systemd.env" + + systemctl --user daemon-reload + systemctl --user restart openclaw-gateway + + openclaw_healthcheck +} + +openclaw_plugin() { + [ "$INSTALL_PLUGIN" = yes ] || { log "Skipping plugin (INSTALL_PLUGIN=$INSTALL_PLUGIN)"; return; } + if openclaw plugins list 2>/dev/null | grep -q 'iblai-openclaw-extensions'; then + log "iblai-openclaw-extensions plugin already installed" + return + fi + build_plugin + log "Installing + enabling iblai-openclaw-extensions" + openclaw plugins install "$PLUGIN_DIR" + openclaw plugins enable iblai-openclaw-extensions + systemctl --user restart openclaw-gateway +} + +openclaw_healthcheck() { + local code + code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${GATEWAY_PORT}/" || true)" + [ "$code" = 200 ] && log "Gateway healthy (127.0.0.1:${GATEWAY_PORT} -> 200)" \ + || warn "Gateway health check returned '${code}' (expected 200). Check: journalctl --user -u openclaw-gateway -n 50" +} + +# ---- NemoClaw ---------------------------------------------------------------- +nemoclaw_install() { + if ! command -v docker >/dev/null 2>&1; then + log "Installing Docker" + apt-get update && apt-get install -y docker.io + systemctl enable --now docker + fi + install_node + + # Origin allowlist is baked at onboard time -> must be set BEFORE the wizard runs. + export CHAT_UI_URL="https://${DOMAIN}" + persist_env CHAT_UI_URL "https://${DOMAIN}" + + if ! command -v nemoclaw >/dev/null 2>&1; then + log "Running the NVIDIA NemoClaw installer (interactive onboarding)" + tty_run bash -c "$(curl -fsSL https://www.nvidia.com/nemoclaw.sh)" + else + log "NemoClaw $(nemoclaw --version 2>/dev/null) already installed" + fi + + # Onboard a sandbox if the named one isn't up yet. + if ! nemoclaw "$SANDBOX_NAME" status >/dev/null 2>&1; then + log "Onboarding sandbox '${SANDBOX_NAME}' (interactive)" + ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" tty_run nemoclaw onboard + fi + + # Read the gateway token straight off the host. + OPENCLAW_GATEWAY_TOKEN="$(nemoclaw "$SANDBOX_NAME" gateway-token --quiet 2>/dev/null || true)" + [ -n "$OPENCLAW_GATEWAY_TOKEN" ] \ + || die "could not read gateway token for '${SANDBOX_NAME}'. Finish onboarding, then re-run." + + # Ensure the host<->sandbox forward exists, and survive reboots via systemd. + openshell forward start --background "127.0.0.1:${GATEWAY_PORT}" "$SANDBOX_NAME" 2>/dev/null || true + log "Installing nemoclaw-forward.service" + cat > /etc/systemd/system/nemoclaw-forward.service </dev/null | grep -q 'iblai-openclaw-extensions'; then + log "iblai-openclaw-extensions plugin already installed in sandbox" + return + fi + build_plugin + # Option A (runtime install, verified end to end). Copy the whole plugin -- not + # just dist/ -- and drop node_modules first (self-referential symlink trips the + # sandbox state-backup audit; the bundled dist/index.mjs is all that's needed). + local ct + ct="$(docker ps --format '{{.Names}}' | grep "openshell-${SANDBOX_NAME}" | head -1)" + [ -n "$ct" ] || die "could not find the openshell-${SANDBOX_NAME} container" + rm -rf "${PLUGIN_DIR}/node_modules" + docker exec "$ct" rm -rf /tmp/iblai-openclaw-extensions + docker cp "$PLUGIN_DIR" "$ct":/tmp/iblai-openclaw-extensions + docker exec "$ct" chmod -R a+rX /tmp/iblai-openclaw-extensions + log "Installing + enabling iblai-openclaw-extensions in the sandbox" + nemoclaw "$SANDBOX_NAME" exec --no-tty -- openclaw plugins install /tmp/iblai-openclaw-extensions + nemoclaw "$SANDBOX_NAME" exec --no-tty -- openclaw plugins enable iblai-openclaw-extensions + nemoclaw "$SANDBOX_NAME" restart +} + +nemoclaw_healthcheck() { + local code + code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${GATEWAY_PORT}/" || true)" + if [ "$code" = 200 ]; then + log "Gateway healthy (127.0.0.1:${GATEWAY_PORT} -> 200)" + else + warn "Gateway health check returned '${code}' (expected 200)." + warn "The forward port may differ; check: nemoclaw ${SANDBOX_NAME} dashboard-url" + fi +} + +# ---- shared: build the plugin bundle from source ----------------------------- +build_plugin() { + log "Building iblai-openclaw-extensions from source" + if [ ! -d "$PLUGIN_DIR/.git" ]; then + rm -rf "$PLUGIN_DIR" + git clone "$PLUGIN_REPO" "$PLUGIN_DIR" + fi + command -v pnpm >/dev/null 2>&1 || npm install -g pnpm + ( cd "$PLUGIN_DIR" && pnpm install && pnpm build ) +} + +# ---- optional: seed the ibl.ai platform -------------------------------------- +# Reuses scripts/seed_claw_mentor.py (the maintained platform-side flow) rather +# than reimplementing the API calls here. Runs the repo copy if present, else +# fetches it. Passes the gateway token + device key this run just produced. +seed_platform() { + log "Seeding ibl.ai platform: instance + mentor for ${DOMAIN}" + python3 -c 'import requests' >/dev/null 2>&1 || apt-get install -y python3-requests + + local seed="" + for c in "./scripts/seed_claw_mentor.py" \ + "$(dirname "${BASH_SOURCE[0]:-$0}")/scripts/seed_claw_mentor.py"; do + [ -f "$c" ] && { seed="$c"; break; } + done + if [ -z "$seed" ]; then + seed="$(mktemp /tmp/seed_claw_mentor.XXXXXX.py)" + curl -fsSL "$SEED_URL" -o "$seed" + fi + + local args=( + --api-key "$IBLAI_API_KEY" + --host "$IBLAI_HOST" + --tenant-key "$IBLAI_ORG" + --user-id "$IBLAI_USER_ID" + --agent-name "$AGENT_NAME" + --claw-name "$DOMAIN" + --claw-server-url "https://${DOMAIN}" + --claw-gateway-token "$OPENCLAW_GATEWAY_TOKEN" + --claw-type "$IBLAI_CLAW_TYPE" + --agent-id "$AGENT_ID" + --device-key-pem "$DEVICE_KEY_PEM" + ) + [ -n "$AGENT_DESCRIPTION" ] && args+=(--agent-description "$AGENT_DESCRIPTION") + [ -n "$AGENT_CONFIG" ] && args+=(--agent-config "$AGENT_CONFIG") + + python3 "$seed" "${args[@]}" +} + +# ---- final summary ----------------------------------------------------------- +print_summary() { + local server_url="https://${DOMAIN}" pem_json + + cat <, , : + + curl -X POST https:///api/ai-mentor/orgs//claw/instances/ \\ + -H "Authorization: Token " \\ + -H "Content-Type: application/json" \\ + -d '{ + "name": "${DOMAIN}", + "claw_type": "${IBLAI_CLAW_TYPE}", + "server_url": "${server_url}", + "gateway_token": "${OPENCLAW_GATEWAY_TOKEN}", + "connection_params": {"device_identity": {"private_key_pem": "${pem_json}"}} + }' + + Or re-run with IBLAI_API_KEY set to seed automatically. Full flow: + scripts/seed_claw_mentor.py and docs/platform-integration.md +REG + fi + + echo + echo " First platform config push mints a pending device pairing -- approve it once:" + if [ "$CLAW_TYPE" = openclaw ]; then + cat <<'NEXT' + openclaw devices list + openclaw devices approve --token "$OPENCLAW_GATEWAY_TOKEN" +NEXT + else + printf ' nemoclaw %s exec --no-tty -- openclaw devices list\n' "$SANDBOX_NAME" + printf ' nemoclaw %s exec --no-tty -- openclaw devices approve \n' "$SANDBOX_NAME" + fi + echo "============================================================================" +} + +# ---- main -------------------------------------------------------------------- +log "Setting up ${CLAW_TYPE} for ${DOMAIN}" +gen_device_key +if [ "$CLAW_TYPE" = openclaw ]; then + openclaw_install + install_caddy + setup_firewall + openclaw_plugin +else + nemoclaw_install + install_caddy + setup_firewall + nemoclaw_plugin +fi + +SEED_RESULT=skipped +if [ -n "$IBLAI_API_KEY" ]; then + if seed_platform; then SEED_RESULT=ok; else SEED_RESULT=failed; fi +fi + +print_summary diff --git a/scripts/__pycache__/seed_claw_mentor.cpython-314.pyc b/scripts/__pycache__/seed_claw_mentor.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..990294e0eba88cfc9c9f3b2add663534b982ac95 GIT binary patch literal 18468 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 dict: """POST /orgs/{org}/claw/instances/ — returns the created ClawInstance JSON.""" print("Create Claw Instance") @@ -84,6 +85,9 @@ def create_claw_instance( "server_url": server_url, "gateway_token": gateway_token, } + if connection_params: + # Ed25519 device identity, required for OpenClaw config push (missing → "missing scope"). + payload["connection_params"] = connection_params response = session.post(url, json=payload, timeout=60) response.raise_for_status() return response.json() @@ -193,6 +197,7 @@ def run( claw_type: str, agent_id: str, agent_config: dict, + connection_params: dict | None = None, ) -> int: """Execute the full create sequence. Returns 0 on success, 1 on failure.""" @@ -201,7 +206,7 @@ def run( try: claw_instance = create_claw_instance( session, host, tenant_key, claw_name, claw_server_url, - claw_gateway_token, claw_type, + claw_gateway_token, claw_type, connection_params, ) except requests.HTTPError as exc: print( @@ -323,16 +328,16 @@ def parse_args() -> argparse.Namespace: "--user-id", default=DUMMY_USER_ID, help="User id (mentor owner)" ) - # Mentor + # Agent parser.add_argument( - "--mentor-name", - default=DUMMY_MENTOR_NAME, - help="Display name for the new mentor", + "--agent-name", + default=DUMMY_AGENT_NAME, + help="Display name for the new agent", ) parser.add_argument( - "--mentor-description", - default=DUMMY_MENTOR_DESCRIPTION or None, - help="Short description for the mentor (optional)", + "--agent-description", + default=DUMMY_AGENT_DESCRIPTION or None, + help="Short description for the agent (optional)", ) # Claw instance @@ -354,9 +359,18 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--claw-type", default=DUMMY_CLAW_TYPE, - choices=["openclaw", "ironclaw"], + choices=["openclaw", "ironclaw", "nemoclaw"], help="Claw instance type (default: openclaw)", ) + parser.add_argument( + "--device-key-pem", + default=None, + help=( + "Path to an Ed25519 PKCS8 private-key PEM. Sent as " + "connection_params.device_identity.private_key_pem " + "(required for OpenClaw device identity / config push)." + ), + ) # Agent config parser.add_argument( @@ -398,6 +412,16 @@ def main() -> int: elif DUMMY_AGENT_CONFIG: agent_config = DUMMY_AGENT_CONFIG + connection_params: dict = {} + if args.device_key_pem: + try: + with open(args.device_key_pem, encoding="utf-8") as fh: + pem = fh.read() + except OSError as exc: + print(f"--device-key-pem could not be read: {exc}", file=sys.stderr) + return 2 + connection_params = {"device_identity": {"private_key_pem": pem}} + session = requests.Session() session.headers.update( { @@ -412,14 +436,15 @@ def main() -> int: host=host, tenant_key=args.tenant_key, user_id=args.user_id, - mentor_name=args.mentor_name, - mentor_description=args.mentor_description, + mentor_name=args.agent_name, + mentor_description=args.agent_description, claw_name=args.claw_name, claw_server_url=args.claw_server_url, claw_gateway_token=args.claw_gateway_token, claw_type=args.claw_type, agent_id=args.agent_id, agent_config=agent_config, + connection_params=connection_params or None, ) From 43f1cec59e8013ba03f4971ec1daec540c3a39b8 Mon Sep 17 00:00:00 2001 From: null-crafter <163591881+null-crafter@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:14:54 +0000 Subject: [PATCH 2/9] fix: allow env vars to be entered interactively --- README.md | 19 +++----- install.sh | 130 ++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 109 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index dbbd8e3..c0ee41b 100644 --- a/README.md +++ b/README.md @@ -74,24 +74,15 @@ LLM Provider (Anthropic, OpenRouter, etc.) ### Automated setup (one command) -On a fresh Debian/Ubuntu server, [`install.sh`](install.sh) does the whole server side -- installs OpenClaw (or NemoClaw), Caddy with automatic TLS, the firewall, and the `iblai-openclaw-extensions` plugin, then prints the gateway token, an Ed25519 device key, and the exact API call to register the instance with ibl.ai. Pass `IBLAI_API_KEY` and it does the platform side too -- creating the claw instance and a mentor (wiring in the device key) via [`scripts/seed_claw_mentor.py`](scripts/seed_claw_mentor.py): +On a fresh Debian/Ubuntu server, run [`install.sh`](install.sh) with no arguments: ```bash -# OpenClaw (default) -curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh \ - | DOMAIN=claw.example.com ANTHROPIC_API_KEY=sk-ant-... bash - -# NemoClaw -curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh \ - | CLAW_TYPE=nemoclaw DOMAIN=claw.example.com ANTHROPIC_API_KEY=sk-ant-... bash - -# ...and seed the ibl.ai platform in the same run (instance + mentor) -curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh \ - | DOMAIN=claw.example.com ANTHROPIC_API_KEY=sk-ant-... \ - IBLAI_API_KEY=your-platform-key IBLAI_ORG=your-org bash +curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh | bash ``` -Point `DOMAIN`'s DNS A record at the server and open ports 80/443 first. Optional vars: `MODEL`, `SANDBOX_NAME` (NemoClaw), `INSTALL_PLUGIN=no`, `SETUP_FIREWALL=no`, and for seeding `IBLAI_HOST` / `IBLAI_ORG` / `AGENT_NAME`. Re-running is safe -- it never regenerates an existing token or key. Prefer the manual steps below to understand each piece. +It prompts for the domain, API key, claw type, and whether to register on ibl.ai, then does the whole server side -- installs OpenClaw (or NemoClaw), Caddy with automatic TLS, the firewall, and the `iblai-openclaw-extensions` plugin -- and optionally the platform side (claw instance + mentor via [`scripts/seed_claw_mentor.py`](scripts/seed_claw_mentor.py), wiring in the device key). Finally it prints the gateway token, an Ed25519 device key, and the API call to register the instance. + +Point your domain's DNS A record at the server and open ports 80/443 first. Every prompt can be pre-set as an environment variable to skip it (`DOMAIN`, `ANTHROPIC_API_KEY`, `CLAW_TYPE=nemoclaw`, `MODEL`, `IBLAI_API_KEY`, `IBLAI_ORG`, ...) for unattended runs. Re-running is safe -- it never regenerates an existing token or key. Prefer the manual steps below to understand each piece. ### 1. Set up the server diff --git a/install.sh b/install.sh index a1c9c34..964f538 100755 --- a/install.sh +++ b/install.sh @@ -13,19 +13,20 @@ # the ibl.ai platform (claw instance + mentor + wiring, incl. the device key) via # scripts/seed_claw_mentor.py; otherwise it prints the API call for you to run. # -# Usage (OpenClaw, the default): -# curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh \ -# | DOMAIN=claw.example.com ANTHROPIC_API_KEY=sk-ant-... bash +# Usage (interactive -- prompts for domain, API key, etc.): +# curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh | bash # -# Usage (NemoClaw): +# Usage (non-interactive -- pre-set any variable to skip its prompt): # curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh \ -# | CLAW_TYPE=nemoclaw DOMAIN=claw.example.com ANTHROPIC_API_KEY=sk-ant-... bash +# | DOMAIN=claw.example.com ANTHROPIC_API_KEY=sk-ant-... bash +# # NemoClaw: add CLAW_TYPE=nemoclaw # -# Config (environment variables): +# Config (each is prompted if unset and a terminal is attached; otherwise the +# default shown is used, or the value is required): # DOMAIN (required) hostname whose DNS A record points at this server # ANTHROPIC_API_KEY (required for openclaw; nemoclaw collects it in its wizard) # CLAW_TYPE openclaw (default) | nemoclaw -# MODEL default: anthropic/claude-sonnet-4-6 +# MODEL default: anthropic/claude-sonnet-5 # SANDBOX_NAME nemoclaw only, default: main # INSTALL_PLUGIN yes (default) | no # SETUP_FIREWALL yes (default) | no @@ -41,28 +42,30 @@ # Re-running is safe: it never regenerates an existing gateway token or device # key (doing so would break platform pairing) and skips already-installed steps. # -# Interactive bits (the NemoClaw onboarding wizard) read from /dev/tty, so the -# curl | bash form still works. See docs/server-setup.md and docs/nemoclaw-setup.md. +# All prompts (and the NemoClaw onboarding wizard) read from /dev/tty, so the +# curl | bash form is fully interactive. See docs/server-setup.md and docs/nemoclaw-setup.md. set -euo pipefail -# ---- configuration (environment-driven) -------------------------------------- -CLAW_TYPE="${CLAW_TYPE:-openclaw}" +# ---- configuration ----------------------------------------------------------- +# Each value comes from the environment (pre-set to skip its prompt), an +# interactive prompt, or a default -- collect_config() below resolves them. +CLAW_TYPE="${CLAW_TYPE:-}" DOMAIN="${DOMAIN:-}" ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-}" -MODEL="${MODEL:-anthropic/claude-sonnet-5}" -SANDBOX_NAME="${SANDBOX_NAME:-main}" -INSTALL_PLUGIN="${INSTALL_PLUGIN:-yes}" -SETUP_FIREWALL="${SETUP_FIREWALL:-yes}" +MODEL="${MODEL:-}" +SANDBOX_NAME="${SANDBOX_NAME:-}" +INSTALL_PLUGIN="${INSTALL_PLUGIN:-}" +SETUP_FIREWALL="${SETUP_FIREWALL:-}" -# Platform seeding (optional -- only runs when IBLAI_API_KEY is non-empty). +# Platform seeding (optional -- runs when IBLAI_API_KEY ends up set). IBLAI_API_KEY="${IBLAI_API_KEY:-}" -IBLAI_HOST="${IBLAI_HOST:-https://base.manager.iblai.app}" -IBLAI_ORG="${IBLAI_ORG:-main}" -IBLAI_USER_ID="${IBLAI_USER_ID:-admin}" -IBLAI_CLAW_TYPE="${IBLAI_CLAW_TYPE:-openclaw}" -AGENT_NAME="${AGENT_NAME:-Claw Agent}" +IBLAI_HOST="${IBLAI_HOST:-}" +IBLAI_ORG="${IBLAI_ORG:-}" +IBLAI_USER_ID="${IBLAI_USER_ID:-}" +IBLAI_CLAW_TYPE="${IBLAI_CLAW_TYPE:-}" +AGENT_NAME="${AGENT_NAME:-}" AGENT_DESCRIPTION="${AGENT_DESCRIPTION:-}" -AGENT_ID="${AGENT_ID:-main}" +AGENT_ID="${AGENT_ID:-}" AGENT_CONFIG="${AGENT_CONFIG:-}" SEED_URL="${SEED_URL:-https://raw.githubusercontent.com/iblai/claw-setup/main/scripts/seed_claw_mentor.py}" @@ -78,9 +81,37 @@ die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; } usage() { awk 'NR==1{next} /^#/{sub(/^#[[:space:]]?/,""); print; next} {exit}' "$0" 2>/dev/null || true; } +# True when the controlling terminal is readable (works even under curl|bash). +have_tty() { (exec /dev/null; } + # Run an interactive command attached to the real terminal even under curl|bash. tty_run() { - if [ -e /dev/tty ]; then "$@" /dev/tty 2>&1; else "$@"; fi + if have_tty; then "$@" /dev/tty 2>&1; else "$@"; fi +} + +# prompt VAR "message" [default] [secret] -- keep an env-provided value, else ask +# on the terminal (blank input takes the default). A terminal is required (main +# checks have_tty up front and errors out otherwise). +prompt() { + local var="$1" msg="$2" def="${3-}" secret="${4-}" ans + [ -n "${!var-}" ] && return 0 + if [ -n "$secret" ]; then + printf '%s: ' "$msg" >/dev/tty + read -rs ans /dev/tty + elif [ -n "$def" ]; then + printf '%s [%s]: ' "$msg" "$def" >/dev/tty; read -r ans /dev/tty; read -r ans /dev/tty; read -r ans interactive prompt > default. +collect_config() { + log "Setup -- Enter accepts each [default]; pre-set env vars to skip prompts." + + prompt CLAW_TYPE "Claw type: openclaw or nemoclaw" openclaw + case "$CLAW_TYPE" in openclaw|nemoclaw) ;; *) die "CLAW_TYPE must be 'openclaw' or 'nemoclaw'";; esac + + prompt DOMAIN "Domain served over HTTPS (its DNS A record must already point here)" + + if [ "$CLAW_TYPE" = nemoclaw ]; then + prompt SANDBOX_NAME "NemoClaw sandbox name" main + prompt ANTHROPIC_API_KEY "Anthropic API key (blank = enter it in the NemoClaw wizard)" "" secret + else + prompt ANTHROPIC_API_KEY "Anthropic API key" "" secret + prompt MODEL "Primary model" anthropic/claude-sonnet-5 + fi + + prompt INSTALL_PLUGIN "Install the iblai-openclaw-extensions plugin? yes/no" yes + prompt SETUP_FIREWALL "Configure the UFW firewall (22/80/443)? yes/no" yes + + # Optional: register + seed the ibl.ai platform in the same run. + if [ -z "$IBLAI_API_KEY" ] && confirm "Register this instance on the ibl.ai platform now?" no; then + IBLAI_SEED=yes + fi + if [ -n "$IBLAI_API_KEY" ] || [ "${IBLAI_SEED:-no}" = yes ]; then + prompt IBLAI_API_KEY "ibl.ai platform API key" "" secret + prompt IBLAI_HOST "ibl.ai platform host" https://base.manager.iblai.app + prompt IBLAI_ORG "ibl.ai org / tenant slug" main + prompt AGENT_NAME "Agent display name (the mentor shown on the platform)" "Claw Agent" + fi + + # Defaults for anything not prompted or left blank. + MODEL="${MODEL:-anthropic/claude-sonnet-5}" + SANDBOX_NAME="${SANDBOX_NAME:-main}" + INSTALL_PLUGIN="${INSTALL_PLUGIN:-yes}" + SETUP_FIREWALL="${SETUP_FIREWALL:-yes}" + IBLAI_HOST="${IBLAI_HOST:-https://base.manager.iblai.app}" + IBLAI_ORG="${IBLAI_ORG:-main}" + IBLAI_USER_ID="${IBLAI_USER_ID:-admin}" + IBLAI_CLAW_TYPE="${IBLAI_CLAW_TYPE:-openclaw}" + AGENT_NAME="${AGENT_NAME:-Claw Agent}" + AGENT_ID="${AGENT_ID:-main}" +} + +# ---- checks, then gather config ---------------------------------------------- case "${1:-}" in -h|--help) usage; exit 0 ;; esac -[ -n "$DOMAIN" ] || { usage; die "DOMAIN is required (e.g. DOMAIN=claw.example.com)"; } -case "$CLAW_TYPE" in openclaw|nemoclaw) ;; *) die "CLAW_TYPE must be 'openclaw' or 'nemoclaw'";; esac +have_tty || die "no terminal available -- this installer is interactive; run it from a shell (e.g. over SSH)" [ "$(id -u)" -eq 0 ] || die "run as root (the setup guides assume root@server)" command -v apt-get >/dev/null 2>&1 || die "this installer targets Debian/Ubuntu (apt not found)" + +collect_config + +[ -n "$DOMAIN" ] || { usage; die "DOMAIN is required"; } if [ "$CLAW_TYPE" = openclaw ] && [ -z "$ANTHROPIC_API_KEY" ]; then die "ANTHROPIC_API_KEY is required for openclaw" fi From a45ec1cdc603f053df4e03ddf834363134fe7a03 Mon Sep 17 00:00:00 2001 From: null-crafter <163591881+null-crafter@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:37:44 +0000 Subject: [PATCH 3/9] fix: update prompt text when starting setup --- install.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 964f538..f03f5a8 100755 --- a/install.sh +++ b/install.sh @@ -126,9 +126,20 @@ json_escape_file() { awk 'BEGIN{ORS=""} {gsub(/\\/,"\\\\"); gsub(/"/,"\\\""); print $0 "\\n"}' "$1" } +# Pixel-art splash: "ibl.ai Claw Setup". +banner() { + printf '\n\033[1;32m' + cat <<'ART' ++-----------------------+ +| ibl.ai Claw Setup | ++-----------------------+ +ART + printf '\033[0m\n' +} + # Gather every user-facing setting: env value > interactive prompt > default. collect_config() { - log "Setup -- Enter accepts each [default]; pre-set env vars to skip prompts." + log "Setup" prompt CLAW_TYPE "Claw type: openclaw or nemoclaw" openclaw case "$CLAW_TYPE" in openclaw|nemoclaw) ;; *) die "CLAW_TYPE must be 'openclaw' or 'nemoclaw'";; esac From 7d59c7ffdc2960542eda03dbc6e872061e89cbc5 Mon Sep 17 00:00:00 2001 From: null-crafter <163591881+null-crafter@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:39:00 +0000 Subject: [PATCH 4/9] fix: print banner for setup --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index f03f5a8..e270250 100755 --- a/install.sh +++ b/install.sh @@ -139,7 +139,7 @@ ART # Gather every user-facing setting: env value > interactive prompt > default. collect_config() { - log "Setup" + banner prompt CLAW_TYPE "Claw type: openclaw or nemoclaw" openclaw case "$CLAW_TYPE" in openclaw|nemoclaw) ;; *) die "CLAW_TYPE must be 'openclaw' or 'nemoclaw'";; esac From 62a7cc8ec9d7804ff016559e2ed8f3e679b1e6c2 Mon Sep 17 00:00:00 2001 From: null-crafter <163591881+null-crafter@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:30:26 +0000 Subject: [PATCH 5/9] fix: added setup value caching and fixed ufw command --- install.sh | 105 ++++++++++++++++++++++++++++------------------------- 1 file changed, 56 insertions(+), 49 deletions(-) diff --git a/install.sh b/install.sh index e270250..b036a74 100755 --- a/install.sh +++ b/install.sh @@ -13,27 +13,29 @@ # the ibl.ai platform (claw instance + mentor + wiring, incl. the device key) via # scripts/seed_claw_mentor.py; otherwise it prints the API call for you to run. # -# Usage (interactive -- prompts for domain, API key, etc.): +# Usage (interactive -- prompts for domain, etc.): # curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh | bash # # Usage (non-interactive -- pre-set any variable to skip its prompt): # curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh \ -# | DOMAIN=claw.example.com ANTHROPIC_API_KEY=sk-ant-... bash +# | DOMAIN=claw.example.com bash # # NemoClaw: add CLAW_TYPE=nemoclaw # +# The LLM provider, API key, and model are chosen in OpenClaw's own onboarding +# (or the NemoClaw wizard) during setup, not here. +# # Config (each is prompted if unset and a terminal is attached; otherwise the -# default shown is used, or the value is required): +# default shown is used, or the value is required). Previous answers are cached +# in ~/.cache/iblai-claw-setup and offered as the defaults next time: # DOMAIN (required) hostname whose DNS A record points at this server -# ANTHROPIC_API_KEY (required for openclaw; nemoclaw collects it in its wizard) # CLAW_TYPE openclaw (default) | nemoclaw -# MODEL default: anthropic/claude-sonnet-5 # SANDBOX_NAME nemoclaw only, default: main # INSTALL_PLUGIN yes (default) | no # SETUP_FIREWALL yes (default) | no # # Platform seeding (optional; runs only when IBLAI_API_KEY is set): # IBLAI_API_KEY ibl.ai platform API key -- presence enables seeding -# IBLAI_HOST default: https://base.manager.iblai.app +# IBLAI_HOST env var only (not prompted); default: https://base.manager.iblai.app # IBLAI_ORG tenant/org slug, default: main # IBLAI_USER_ID mentor owner, default: admin # IBLAI_CLAW_TYPE platform claw_type, default: openclaw @@ -51,8 +53,6 @@ set -euo pipefail # interactive prompt, or a default -- collect_config() below resolves them. CLAW_TYPE="${CLAW_TYPE:-}" DOMAIN="${DOMAIN:-}" -ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-}" -MODEL="${MODEL:-}" SANDBOX_NAME="${SANDBOX_NAME:-}" INSTALL_PLUGIN="${INSTALL_PLUGIN:-}" SETUP_FIREWALL="${SETUP_FIREWALL:-}" @@ -75,6 +75,19 @@ DEVICE_KEY_PEM="${OPENCLAW_HOME}/platform_device_identity.pem" PLUGIN_REPO="https://github.com/iblai/iblai-openclaw-extensions-plugin.git" PLUGIN_DIR="/opt/iblai-openclaw-extensions" +# Cache previously entered (non-secret) answers and reuse them as prompt defaults. +CACHE_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/iblai-claw-setup" +CACHE_KEYS=(CLAW_TYPE DOMAIN SANDBOX_NAME INSTALL_PLUGIN SETUP_FIREWALL IBLAI_HOST IBLAI_ORG IBLAI_USER_ID IBLAI_CLAW_TYPE AGENT_NAME AGENT_ID) +declare -A CACHED=() +if [ -f "$CACHE_FILE" ]; then + while IFS='=' read -r _k _v; do [ -n "$_k" ] && CACHED["$_k"]="$_v"; done < "$CACHE_FILE" +fi +save_cache() { + mkdir -p "$(dirname "$CACHE_FILE")" + local k + for k in "${CACHE_KEYS[@]}"; do printf '%s=%s\n' "$k" "${!k}"; done > "$CACHE_FILE" +} + log() { printf '\033[1;32m==>\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; } die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; } @@ -95,6 +108,7 @@ tty_run() { prompt() { local var="$1" msg="$2" def="${3-}" secret="${4-}" ans [ -n "${!var-}" ] && return 0 + [ -n "${CACHED[$var]:-}" ] && def="${CACHED[$var]}" if [ -n "$secret" ]; then printf '%s: ' "$msg" >/dev/tty read -rs ans /dev/tty @@ -148,28 +162,23 @@ collect_config() { if [ "$CLAW_TYPE" = nemoclaw ]; then prompt SANDBOX_NAME "NemoClaw sandbox name" main - prompt ANTHROPIC_API_KEY "Anthropic API key (blank = enter it in the NemoClaw wizard)" "" secret - else - prompt ANTHROPIC_API_KEY "Anthropic API key" "" secret - prompt MODEL "Primary model" anthropic/claude-sonnet-5 fi prompt INSTALL_PLUGIN "Install the iblai-openclaw-extensions plugin? yes/no" yes prompt SETUP_FIREWALL "Configure the UFW firewall (22/80/443)? yes/no" yes - # Optional: register + seed the ibl.ai platform in the same run. + # Optional: register + seed the ibl.ai platform in the same run. The platform + # host is intentionally not prompted -- override it with IBLAI_HOST if needed. if [ -z "$IBLAI_API_KEY" ] && confirm "Register this instance on the ibl.ai platform now?" no; then IBLAI_SEED=yes fi if [ -n "$IBLAI_API_KEY" ] || [ "${IBLAI_SEED:-no}" = yes ]; then - prompt IBLAI_API_KEY "ibl.ai platform API key" "" secret - prompt IBLAI_HOST "ibl.ai platform host" https://base.manager.iblai.app + prompt IBLAI_API_KEY "ibl.ai platform API key" # echoed on purpose (not hidden) prompt IBLAI_ORG "ibl.ai org / tenant slug" main prompt AGENT_NAME "Agent display name (the mentor shown on the platform)" "Claw Agent" fi # Defaults for anything not prompted or left blank. - MODEL="${MODEL:-anthropic/claude-sonnet-5}" SANDBOX_NAME="${SANDBOX_NAME:-main}" INSTALL_PLUGIN="${INSTALL_PLUGIN:-yes}" SETUP_FIREWALL="${SETUP_FIREWALL:-yes}" @@ -179,6 +188,8 @@ collect_config() { IBLAI_CLAW_TYPE="${IBLAI_CLAW_TYPE:-openclaw}" AGENT_NAME="${AGENT_NAME:-Claw Agent}" AGENT_ID="${AGENT_ID:-main}" + + save_cache } # ---- checks, then gather config ---------------------------------------------- @@ -190,9 +201,6 @@ command -v apt-get >/dev/null 2>&1 || die "this installer targets Debian/Ubuntu collect_config [ -n "$DOMAIN" ] || { usage; die "DOMAIN is required"; } -if [ "$CLAW_TYPE" = openclaw ] && [ -z "$ANTHROPIC_API_KEY" ]; then - die "ANTHROPIC_API_KEY is required for openclaw" -fi # ---- shared: Node.js 22 ------------------------------------------------------ install_node() { @@ -253,7 +261,7 @@ setup_firewall() { [ "$SETUP_FIREWALL" = yes ] || { log "Skipping UFW (SETUP_FIREWALL=$SETUP_FIREWALL)"; return; } command -v ufw >/dev/null 2>&1 || apt-get install -y ufw log "Configuring UFW (22, 80, 443)" - ufw allow outgoing + ufw default allow outgoing ufw allow 22/tcp ufw allow 80/tcp ufw allow 443/tcp @@ -271,8 +279,10 @@ openclaw_install() { fi # Gateway token: reuse the persisted one if present, else mint a fresh one. + # Stored by reference in openclaw.json ("${OPENCLAW_GATEWAY_TOKEN}"); the real + # value is handed to the systemd user service via a chmod-600 EnvironmentFile. local env_file="${OPENCLAW_HOME}/gateway.systemd.env" - mkdir -p "$OPENCLAW_HOME" + mkdir -p "${OPENCLAW_HOME}/workspace" if [ -f "$env_file" ]; then OPENCLAW_GATEWAY_TOKEN="$(sed -n 's/^OPENCLAW_GATEWAY_TOKEN=//p' "$env_file" | head -1)" fi @@ -283,25 +293,14 @@ openclaw_install() { log "Reusing existing gateway token" fi persist_env OPENCLAW_GATEWAY_TOKEN "$OPENCLAW_GATEWAY_TOKEN" - persist_env ANTHROPIC_API_KEY "$ANTHROPIC_API_KEY" - # Full config up front -> skips the interactive onboarding wizard. - local ver - ver="$(openclaw --version 2>/dev/null | head -1 | tr -d '[:space:]')" - ver="${ver:-latest}" + # Pre-write only the gateway/session config. The LLM provider, API key, and model + # are deliberately left out so OpenClaw's onboarding collects them. if [ ! -f "${OPENCLAW_HOME}/openclaw.json" ]; then - log "Writing ${OPENCLAW_HOME}/openclaw.json (version=${ver}, model=${MODEL})" + log "Writing ${OPENCLAW_HOME}/openclaw.json (gateway config; provider + model set during onboarding)" cat > "${OPENCLAW_HOME}/openclaw.json" < "${HOME}/.config/systemd/user/openclaw-gateway.service.d/10-env-file.conf" <<'DROPIN' + # A user service does not inherit our shell env, so supply the token via an + # EnvironmentFile (chmod 600, keeps the secret out of the config file). + mkdir -p "${unit}.d" + cat > "${unit}.d/10-env-file.conf" < "${OPENCLAW_HOME}/gateway.systemd.env" - chmod 600 "${OPENCLAW_HOME}/gateway.systemd.env" + printf 'OPENCLAW_GATEWAY_TOKEN=%s\n' "$OPENCLAW_GATEWAY_TOKEN" > "$env_file" + chmod 600 "$env_file" systemctl --user daemon-reload systemctl --user restart openclaw-gateway @@ -353,6 +359,7 @@ openclaw_plugin() { systemctl --user restart openclaw-gateway } + openclaw_healthcheck() { local code code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${GATEWAY_PORT}/" || true)" @@ -382,8 +389,8 @@ nemoclaw_install() { # Onboard a sandbox if the named one isn't up yet. if ! nemoclaw "$SANDBOX_NAME" status >/dev/null 2>&1; then - log "Onboarding sandbox '${SANDBOX_NAME}' (interactive)" - ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" tty_run nemoclaw onboard + log "Onboarding sandbox '${SANDBOX_NAME}' (interactive; choose provider + enter API key here)" + tty_run nemoclaw onboard fi # Read the gateway token straight off the host. From 364ac39544ffe42d5321551c486ee992a6efbb95 Mon Sep 17 00:00:00 2001 From: null-crafter <163591881+null-crafter@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:29:09 +0000 Subject: [PATCH 6/9] fix: handle ubuntu unattended upgrade --- install.sh | 84 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 17 deletions(-) diff --git a/install.sh b/install.sh index b036a74..178e768 100755 --- a/install.sh +++ b/install.sh @@ -18,17 +18,20 @@ # # Usage (non-interactive -- pre-set any variable to skip its prompt): # curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh \ -# | DOMAIN=claw.example.com bash +# | DOMAIN=claw.example.com LLM_PROVIDER=anthropic LLM_API_KEY=sk-ant-... bash # # NemoClaw: add CLAW_TYPE=nemoclaw # -# The LLM provider, API key, and model are chosen in OpenClaw's own onboarding -# (or the NemoClaw wizard) during setup, not here. +# For OpenClaw the LLM provider + API key are collected here (its onboarding runs +# non-interactively). NemoClaw collects them in its own wizard. # # Config (each is prompted if unset and a terminal is attached; otherwise the # default shown is used, or the value is required). Previous answers are cached # in ~/.cache/iblai-claw-setup and offered as the defaults next time: # DOMAIN (required) hostname whose DNS A record points at this server # CLAW_TYPE openclaw (default) | nemoclaw +# LLM_PROVIDER openclaw only: anthropic (default) | openai | openrouter | google | groq | mistral | deepseek | xai +# LLM_API_KEY openclaw only: API key for the chosen provider (required) +# MODEL openclaw only: model id (default depends on provider) # SANDBOX_NAME nemoclaw only, default: main # INSTALL_PLUGIN yes (default) | no # SETUP_FIREWALL yes (default) | no @@ -53,6 +56,10 @@ set -euo pipefail # interactive prompt, or a default -- collect_config() below resolves them. CLAW_TYPE="${CLAW_TYPE:-}" DOMAIN="${DOMAIN:-}" +LLM_PROVIDER="${LLM_PROVIDER:-}" +LLM_API_KEY="${LLM_API_KEY:-}" +LLM_KEY_ENV="${LLM_KEY_ENV:-}" +MODEL="${MODEL:-}" SANDBOX_NAME="${SANDBOX_NAME:-}" INSTALL_PLUGIN="${INSTALL_PLUGIN:-}" SETUP_FIREWALL="${SETUP_FIREWALL:-}" @@ -77,7 +84,7 @@ PLUGIN_DIR="/opt/iblai-openclaw-extensions" # Cache previously entered (non-secret) answers and reuse them as prompt defaults. CACHE_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/iblai-claw-setup" -CACHE_KEYS=(CLAW_TYPE DOMAIN SANDBOX_NAME INSTALL_PLUGIN SETUP_FIREWALL IBLAI_HOST IBLAI_ORG IBLAI_USER_ID IBLAI_CLAW_TYPE AGENT_NAME AGENT_ID) +CACHE_KEYS=(CLAW_TYPE DOMAIN LLM_PROVIDER MODEL SANDBOX_NAME INSTALL_PLUGIN SETUP_FIREWALL IBLAI_HOST IBLAI_ORG IBLAI_USER_ID IBLAI_CLAW_TYPE AGENT_NAME AGENT_ID) declare -A CACHED=() if [ -f "$CACHE_FILE" ]; then while IFS='=' read -r _k _v; do [ -n "$_k" ] && CACHED["$_k"]="$_v"; done < "$CACHE_FILE" @@ -162,6 +169,24 @@ collect_config() { if [ "$CLAW_TYPE" = nemoclaw ]; then prompt SANDBOX_NAME "NemoClaw sandbox name" main + else + # OpenClaw onboarding runs non-interactively, so collect the LLM provider + key here. + prompt LLM_PROVIDER "LLM provider (anthropic, openai, openrouter, google, groq, mistral, deepseek, xai)" anthropic + local key_env model_default + case "$LLM_PROVIDER" in + anthropic) key_env=ANTHROPIC_API_KEY; model_default=anthropic/claude-sonnet-4-6 ;; + openai) key_env=OPENAI_API_KEY; model_default=openai/gpt-5 ;; + openrouter) key_env=OPENROUTER_API_KEY; model_default=openrouter/anthropic/claude-sonnet-4 ;; + google) key_env=GEMINI_API_KEY; model_default=google/gemini-2.5-pro ;; + groq) key_env=GROQ_API_KEY; model_default=groq/llama-3.3-70b-versatile ;; + mistral) key_env=MISTRAL_API_KEY; model_default=mistral/mistral-large-latest ;; + deepseek) key_env=DEEPSEEK_API_KEY; model_default=deepseek/deepseek-chat ;; + xai) key_env=XAI_API_KEY; model_default=xai/grok-4 ;; + *) die "unsupported LLM_PROVIDER '$LLM_PROVIDER' (anthropic|openai|openrouter|google|groq|mistral|deepseek|xai)" ;; + esac + LLM_KEY_ENV="$key_env" + prompt MODEL "Model id" "$model_default" + prompt LLM_API_KEY "${LLM_PROVIDER} API key" fi prompt INSTALL_PLUGIN "Install the iblai-openclaw-extensions plugin? yes/no" yes @@ -174,7 +199,7 @@ collect_config() { fi if [ -n "$IBLAI_API_KEY" ] || [ "${IBLAI_SEED:-no}" = yes ]; then prompt IBLAI_API_KEY "ibl.ai platform API key" # echoed on purpose (not hidden) - prompt IBLAI_ORG "ibl.ai org / tenant slug" main + prompt IBLAI_ORG "ibl.ai platform key" main prompt AGENT_NAME "Agent display name (the mentor shown on the platform)" "Claw Agent" fi @@ -201,6 +226,19 @@ command -v apt-get >/dev/null 2>&1 || die "this installer targets Debian/Ubuntu collect_config [ -n "$DOMAIN" ] || { usage; die "DOMAIN is required"; } +if [ "$CLAW_TYPE" = openclaw ] && [ -z "$LLM_API_KEY" ]; then + die "an LLM API key is required for openclaw (set LLM_API_KEY or enter it at the prompt)" +fi + +# ---- shared: system prep (runs first) ---------------------------------------- +system_prep() { + log "Disabling unattended-upgrades + apt timers (avoids apt lock contention)" + systemctl stop unattended-upgrades apt-daily.service apt-daily-upgrade.service >/dev/null 2>&1 || true + systemctl disable --now unattended-upgrades apt-daily.timer apt-daily-upgrade.timer >/dev/null 2>&1 || true + log "Installing base packages (Go)" + apt-get update + apt-get install -y golang-go +} # ---- shared: Node.js 22 ------------------------------------------------------ install_node() { @@ -293,14 +331,21 @@ openclaw_install() { log "Reusing existing gateway token" fi persist_env OPENCLAW_GATEWAY_TOKEN "$OPENCLAW_GATEWAY_TOKEN" + persist_env "$LLM_KEY_ENV" "$LLM_API_KEY" - # Pre-write only the gateway/session config. The LLM provider, API key, and model - # are deliberately left out so OpenClaw's onboarding collects them. + # Onboarding is non-interactive, so write the full config (provider + model + + # gateway) up front. The provider API key is read from $LLM_KEY_ENV at runtime. if [ ! -f "${OPENCLAW_HOME}/openclaw.json" ]; then - log "Writing ${OPENCLAW_HOME}/openclaw.json (gateway config; provider + model set during onboarding)" + log "Writing ${OPENCLAW_HOME}/openclaw.json (provider=${LLM_PROVIDER}, model=${MODEL})" cat > "${OPENCLAW_HOME}/openclaw.json" < "${unit}.d/10-env-file.conf" < "$env_file" + { + printf 'OPENCLAW_GATEWAY_TOKEN=%s\n' "$OPENCLAW_GATEWAY_TOKEN" + printf '%s=%s\n' "$LLM_KEY_ENV" "$LLM_API_KEY" + } > "$env_file" chmod 600 "$env_file" systemctl --user daemon-reload @@ -562,6 +611,7 @@ NEXT } # ---- main -------------------------------------------------------------------- +system_prep log "Setting up ${CLAW_TYPE} for ${DOMAIN}" gen_device_key if [ "$CLAW_TYPE" = openclaw ]; then From 0450b56aed04b7160387a3c6b612c35620b52ec9 Mon Sep 17 00:00:00 2001 From: null-crafter <163591881+null-crafter@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:17:36 +0000 Subject: [PATCH 7/9] fix: handle openclaw gateway token warnings --- install.sh | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/install.sh b/install.sh index 178e768..4d4cdc7 100755 --- a/install.sh +++ b/install.sh @@ -125,6 +125,7 @@ prompt() { printf '%s: ' "$msg" >/dev/tty; read -r ans Date: Fri, 17 Jul 2026 18:28:53 +0000 Subject: [PATCH 8/9] feat: add nemoclaw support --- README.md | 10 +++++++--- install.sh | 58 +++++++++++++++++++++++++----------------------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index c0ee41b..96e4f8a 100644 --- a/README.md +++ b/README.md @@ -74,15 +74,19 @@ LLM Provider (Anthropic, OpenRouter, etc.) ### Automated setup (one command) -On a fresh Debian/Ubuntu server, run [`install.sh`](install.sh) with no arguments: +On a fresh Debian/Ubuntu server, run [`install.sh`](install.sh). `HARNESS_TYPE` is the only variable you set (default `openclaw`); everything else is asked interactively: ```bash +# OpenClaw (default) curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh | bash + +# NemoClaw +curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh | HARNESS_TYPE=nemoclaw bash ``` -It prompts for the domain, API key, claw type, and whether to register on ibl.ai, then does the whole server side -- installs OpenClaw (or NemoClaw), Caddy with automatic TLS, the firewall, and the `iblai-openclaw-extensions` plugin -- and optionally the platform side (claw instance + mentor via [`scripts/seed_claw_mentor.py`](scripts/seed_claw_mentor.py), wiring in the device key). Finally it prints the gateway token, an Ed25519 device key, and the API call to register the instance. +It prompts for the domain, the LLM provider + API key (OpenClaw) or runs NemoClaw's own wizard, the sandbox / plugin / firewall choices, and whether to register on ibl.ai. Then it does the whole server side -- OpenClaw (or NemoClaw), Caddy with automatic TLS, the firewall, and the `iblai-openclaw-extensions` plugin -- and optionally the platform side (claw instance + mentor via [`scripts/seed_claw_mentor.py`](scripts/seed_claw_mentor.py), wiring in the device key). Finally it prints the gateway token, an Ed25519 device key, and the API call to register the instance. -Point your domain's DNS A record at the server and open ports 80/443 first. Every prompt can be pre-set as an environment variable to skip it (`DOMAIN`, `ANTHROPIC_API_KEY`, `CLAW_TYPE=nemoclaw`, `MODEL`, `IBLAI_API_KEY`, `IBLAI_ORG`, ...) for unattended runs. Re-running is safe -- it never regenerates an existing token or key. Prefer the manual steps below to understand each piece. +Point your domain's DNS A record at the server and open ports 80/443 first. Answers are cached in `~/.cache/iblai-claw-setup` and offered as defaults next time. Re-running is safe -- it never regenerates an existing token or key. Prefer the manual steps below to understand each piece. ### 1. Set up the server diff --git a/install.sh b/install.sh index 4d4cdc7..93c4676 100755 --- a/install.sh +++ b/install.sh @@ -13,28 +13,19 @@ # the ibl.ai platform (claw instance + mentor + wiring, incl. the device key) via # scripts/seed_claw_mentor.py; otherwise it prints the API call for you to run. # -# Usage (interactive -- prompts for domain, etc.): +# Usage -- HARNESS_TYPE is the only variable you ever set; everything else is asked +# interactively (and cached in ~/.cache/iblai-claw-setup for next time): # curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh | bash +# # OpenClaw (default) +# curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh | HARNESS_TYPE=nemoclaw bash +# # NemoClaw # -# Usage (non-interactive -- pre-set any variable to skip its prompt): -# curl -fsSL https://raw.githubusercontent.com/iblai/claw-setup/main/install.sh \ -# | DOMAIN=claw.example.com LLM_PROVIDER=anthropic LLM_API_KEY=sk-ant-... bash -# # NemoClaw: add CLAW_TYPE=nemoclaw -# -# For OpenClaw the LLM provider + API key are collected here (its onboarding runs -# non-interactively). NemoClaw collects them in its own wizard. -# -# Config (each is prompted if unset and a terminal is attached; otherwise the -# default shown is used, or the value is required). Previous answers are cached -# in ~/.cache/iblai-claw-setup and offered as the defaults next time: -# DOMAIN (required) hostname whose DNS A record points at this server -# CLAW_TYPE openclaw (default) | nemoclaw -# LLM_PROVIDER openclaw only: anthropic (default) | openai | openrouter | google | groq | mistral | deepseek | xai -# LLM_API_KEY openclaw only: API key for the chosen provider (required) -# MODEL openclaw only: model id (default depends on provider) -# SANDBOX_NAME nemoclaw only, default: main -# INSTALL_PLUGIN yes (default) | no -# SETUP_FIREWALL yes (default) | no +# You will be prompted for: +# - the domain served over HTTPS (its DNS A record must already point at this host) +# - OpenClaw: the LLM provider + API key + model (NemoClaw asks these in its own wizard) +# - NemoClaw: the sandbox name +# - whether to install the iblai-openclaw-extensions plugin, and to configure UFW +# - optionally, whether to register + seed this instance on the ibl.ai platform # # Platform seeding (optional; runs only when IBLAI_API_KEY is set): # IBLAI_API_KEY ibl.ai platform API key -- presence enables seeding @@ -54,7 +45,7 @@ set -euo pipefail # ---- configuration ----------------------------------------------------------- # Each value comes from the environment (pre-set to skip its prompt), an # interactive prompt, or a default -- collect_config() below resolves them. -CLAW_TYPE="${CLAW_TYPE:-}" +HARNESS_TYPE="${HARNESS_TYPE:-}" DOMAIN="${DOMAIN:-}" LLM_PROVIDER="${LLM_PROVIDER:-}" LLM_API_KEY="${LLM_API_KEY:-}" @@ -84,7 +75,7 @@ PLUGIN_DIR="/opt/iblai-openclaw-extensions" # Cache previously entered (non-secret) answers and reuse them as prompt defaults. CACHE_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/iblai-claw-setup" -CACHE_KEYS=(CLAW_TYPE DOMAIN LLM_PROVIDER MODEL SANDBOX_NAME INSTALL_PLUGIN SETUP_FIREWALL IBLAI_HOST IBLAI_ORG IBLAI_USER_ID IBLAI_CLAW_TYPE AGENT_NAME AGENT_ID) +CACHE_KEYS=(HARNESS_TYPE DOMAIN LLM_PROVIDER MODEL SANDBOX_NAME INSTALL_PLUGIN SETUP_FIREWALL IBLAI_HOST IBLAI_ORG IBLAI_USER_ID IBLAI_CLAW_TYPE AGENT_NAME AGENT_ID) declare -A CACHED=() if [ -f "$CACHE_FILE" ]; then while IFS='=' read -r _k _v; do [ -n "$_k" ] && CACHED["$_k"]="$_v"; done < "$CACHE_FILE" @@ -163,12 +154,12 @@ ART collect_config() { banner - prompt CLAW_TYPE "Claw type: openclaw or nemoclaw" openclaw - case "$CLAW_TYPE" in openclaw|nemoclaw) ;; *) die "CLAW_TYPE must be 'openclaw' or 'nemoclaw'";; esac + prompt HARNESS_TYPE "Harness type: openclaw or nemoclaw" openclaw + case "$HARNESS_TYPE" in openclaw|nemoclaw) ;; *) die "HARNESS_TYPE must be 'openclaw' or 'nemoclaw'";; esac prompt DOMAIN "Domain served over HTTPS (its DNS A record must already point here)" - if [ "$CLAW_TYPE" = nemoclaw ]; then + if [ "$HARNESS_TYPE" = nemoclaw ]; then prompt SANDBOX_NAME "NemoClaw sandbox name" main else # OpenClaw onboarding runs non-interactively, so collect the LLM provider + key here. @@ -227,7 +218,7 @@ command -v apt-get >/dev/null 2>&1 || die "this installer targets Debian/Ubuntu collect_config [ -n "$DOMAIN" ] || { usage; die "DOMAIN is required"; } -if [ "$CLAW_TYPE" = openclaw ] && [ -z "$LLM_API_KEY" ]; then +if [ "$HARNESS_TYPE" = openclaw ] && [ -z "$LLM_API_KEY" ]; then die "an LLM API key is required for openclaw (set LLM_API_KEY or enter it at the prompt)" fi @@ -433,6 +424,11 @@ nemoclaw_install() { export CHAT_UI_URL="https://${DOMAIN}" persist_env CHAT_UI_URL "https://${DOMAIN}" + # The wizard asks for a sandbox name and every later host command is keyed on it, + # so it must match $SANDBOX_NAME. Say so before the installer's own wizard can run. + log "IMPORTANT: when the wizard asks for the sandbox NAME, enter exactly: ${SANDBOX_NAME}" + log "(that name is used for the port-forward, plugin install, and device pairing)" + if ! command -v nemoclaw >/dev/null 2>&1; then log "Running the NVIDIA NemoClaw installer (interactive onboarding)" tty_run bash -c "$(curl -fsSL https://www.nvidia.com/nemoclaw.sh)" @@ -449,7 +445,7 @@ nemoclaw_install() { # Read the gateway token straight off the host. OPENCLAW_GATEWAY_TOKEN="$(nemoclaw "$SANDBOX_NAME" gateway-token --quiet 2>/dev/null || true)" [ -n "$OPENCLAW_GATEWAY_TOKEN" ] \ - || die "could not read gateway token for '${SANDBOX_NAME}'. Finish onboarding, then re-run." + || die "could not read the gateway token for sandbox '${SANDBOX_NAME}'. If you named it differently in the wizard, re-run with SANDBOX_NAME=." # Ensure the host<->sandbox forward exists, and survive reboots via systemd. openshell forward start --background "127.0.0.1:${GATEWAY_PORT}" "$SANDBOX_NAME" 2>/dev/null || true @@ -565,7 +561,7 @@ print_summary() { cat < --token "$OPENCLAW_GATEWAY_TOKEN" @@ -616,9 +612,9 @@ NEXT # ---- main -------------------------------------------------------------------- system_prep -log "Setting up ${CLAW_TYPE} for ${DOMAIN}" +log "Setting up ${HARNESS_TYPE} for ${DOMAIN}" gen_device_key -if [ "$CLAW_TYPE" = openclaw ]; then +if [ "$HARNESS_TYPE" = openclaw ]; then openclaw_install install_caddy setup_firewall From df21f7a00bfa16a060c07e5e6302350f4d8b433e Mon Sep 17 00:00:00 2001 From: null-crafter <163591881+null-crafter@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:13:14 +0000 Subject: [PATCH 9/9] fix: allow openshell ports in nemoclaw path --- install.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/install.sh b/install.sh index 93c4676..3e24d0f 100755 --- a/install.sh +++ b/install.sh @@ -295,6 +295,12 @@ setup_firewall() { ufw allow 22/tcp ufw allow 80/tcp ufw allow 443/tcp + if [ "$HARNESS_TYPE" = nemoclaw ]; then + # NemoClaw openshell gateway: sandbox bridge subnet -> host bridge gateway on 8080/tcp. + # 172.18.0.0/16 is the default openshell Docker bridge; adjust if yours differs. + log "Allowing NemoClaw openshell gateway (172.18.0.0/16 -> 172.18.0.1:8080/tcp)" + ufw allow from 172.18.0.0/16 to 172.18.0.1 port 8080 proto tcp + fi ufw --force enable }