From dd89ac8f04abc3ef1fb4516f39ab7cd3d847dc91 Mon Sep 17 00:00:00 2001 From: Nim G Date: Fri, 6 Mar 2026 15:41:28 -0300 Subject: [PATCH 1/7] Add SSH agent support for deployment scripts --- CLAUDE.md | 3 ++- build/pre-deploy.mjs | 20 +++++++++++++++----- docs/TESTING.md | 14 ++++++++++++-- docs/VPS-SETUP-GUIDE.md | 9 ++++++++- playbooks/00-fresh-deploy-setup.md | 22 ++++++++++++++-------- playbooks/00-onboarding.md | 5 +++-- playbooks/02-base-setup.md | 5 ++++- playbooks/04-vps1-openclaw.md | 2 ++ playbooks/07-verification.md | 2 ++ playbooks/08b-pair-devices.md | 2 ++ playbooks/08c-deploy-report.md | 2 ++ playbooks/maintenance.md | 2 ++ scripts/health-check.sh | 8 ++++---- scripts/lib/logs-explorer/remote.ts | 16 +++++++++------- scripts/lib/resolve-gateway.sh | 9 ++++++--- scripts/lib/ssh.sh | 29 ++++++++++++++++++++++++----- scripts/logs-docker.sh | 3 ++- scripts/logs-explorer.sh | 12 ++++++------ scripts/logs-openclaw.sh | 3 ++- scripts/openclaw.sh | 3 ++- scripts/restart-gateway.sh | 15 +++++++++++---- scripts/restart-sandboxes.sh | 9 +++++---- scripts/ssh-agent.sh | 15 ++++++++------- scripts/ssh-openclaw.sh | 3 ++- scripts/ssh-vps.sh | 3 ++- scripts/start-browser.sh | 7 ++++--- scripts/sync-media.sh | 9 +++++---- scripts/tag-deploy.sh | 7 ++----- scripts/update-openclaw.sh | 13 +++++++------ scripts/update-sandbox-toolkit.sh | 11 ++++++----- scripts/update-sandboxes.sh | 5 +++-- 31 files changed, 178 insertions(+), 90 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4537721..fd37141 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -90,7 +90,8 @@ See [00-fresh-deploy-setup.md](playbooks/00-fresh-deploy-setup.md) § 0.7 for ex ```bash # After base setup, SSH as adminclaw (not ubuntu) -ssh -i -p @ +# If using an SSH agent, omit -i and rely on your normal ssh config. +ssh [-i ] -p @ # Run commands as openclaw sudo -u openclaw diff --git a/build/pre-deploy.mjs b/build/pre-deploy.mjs index 3082952..911be8b 100644 --- a/build/pre-deploy.mjs +++ b/build/pre-deploy.mjs @@ -197,19 +197,29 @@ async function queryVpsCapacity(env) { const ip = env.VPS_IP; const user = env.SSH_USER || "adminclaw"; const port = env.SSH_PORT || "222"; - const keyPath = env.SSH_KEY || "~/.ssh/vps1_openclaw_ed25519"; + const keyPath = env.SSH_KEY?.trim() || ""; + const identityAgent = env.SSH_IDENTITY_AGENT?.trim() || ""; if (!ip) fatal("VPS_IP not set in .env — cannot query VPS capacity for resource % resolution"); - const expandedKey = keyPath.replace(/^~/, process.env.HOME || ""); info(`Querying VPS capacity at ${user}@${ip}:${port}...`); const sshArgs = [ "-o", "StrictHostKeyChecking=accept-new", "-o", "ConnectTimeout=10", - "-i", expandedKey, "-p", port, `${user}@${ip}`, - "nproc && grep MemTotal /proc/meminfo | awk '{print $2}'" ]; + if (keyPath) { + sshArgs.push("-i", keyPath.replace(/^~/, process.env.HOME || "")); + } + if (identityAgent) { + sshArgs.push("-o", `IdentityAgent=${identityAgent.replace(/^~/, process.env.HOME || "")}`); + } + + sshArgs.push( + "-p", port, `${user}@${ip}`, + "nproc && grep MemTotal /proc/meminfo | awk '{print $2}'" + ); + const { stdout, stderr, exitCode } = await spawnAsync("ssh", sshArgs); if (exitCode !== 0) { @@ -421,7 +431,7 @@ function generateStackEnv(env, config, claws) { // Source: .env const envVars = [ - "VPS_IP", "SSH_KEY", "SSH_PORT", "SSH_USER", + "VPS_IP", "SSH_KEY", "SSH_IDENTITY_AGENT", "SSH_PORT", "SSH_USER", "HOSTALERT_TELEGRAM_BOT_TOKEN", "HOSTALERT_TELEGRAM_CHAT_ID", "CLOUDFLARE_API_TOKEN", "CLOUDFLARE_TUNNEL_TOKEN", ]; diff --git a/docs/TESTING.md b/docs/TESTING.md index efb003a..7b148d3 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -14,7 +14,8 @@ source deploy/host/source-config.sh This exports `ENV__*` vars (from `.env`) and `STACK__*` vars (from `stack.yml`). Key variables used in tests below: - `ENV__VPS_IP` - OpenClaw VPS -- `ENV__SSH_KEY` - SSH key location +- `ENV__SSH_KEY` - Optional SSH key location +- `ENV__SSH_IDENTITY_AGENT` - Optional SSH agent socket path - `ENV__SSH_USER` - SSH username (should be `adminclaw`) - `ENV__SSH_PORT` - SSH port (should be `222`) - `STACK__STACK__INSTALL_DIR` - VPS install base (default: `/home/openclaw`) @@ -174,7 +175,16 @@ For a rapid health check, run this single command. Source config first for varia ```bash source deploy/host/source-config.sh echo "=== VPS-1 Health ===" && \ -ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +if [ -n "${ENV__SSH_KEY:-}" ]; then + SSH_KEY_ARGS=(-i "${ENV__SSH_KEY}") +else + SSH_KEY_ARGS=() +fi +if [ -n "${ENV__SSH_IDENTITY_AGENT:-}" ]; then + SSH_KEY_ARGS+=(-o "IdentityAgent=${ENV__SSH_IDENTITY_AGENT}") +fi + +ssh "${SSH_KEY_ARGS[@]}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ "sudo -u openclaw bash -c 'cd ${INSTALL_DIR} && docker compose ps --format \"{{.Name}}: {{.Status}}\"' && \ echo && \ echo '=== Claw Instances ===' && \ diff --git a/docs/VPS-SETUP-GUIDE.md b/docs/VPS-SETUP-GUIDE.md index 341b22b..31471ee 100644 --- a/docs/VPS-SETUP-GUIDE.md +++ b/docs/VPS-SETUP-GUIDE.md @@ -100,7 +100,8 @@ Record it in `.env`: VPS_IP=x.x.x.x # SSH Configuration (required) -SSH_KEY=~/.ssh/vps1_openclaw_ed25519 # Path to your ssh key generated in Step 2 +SSH_KEY=~/.ssh/vps1_openclaw_ed25519 # Optional: path to your ssh key file +SSH_IDENTITY_AGENT= # Optional: ssh agent socket path if you use an agent instead of a key file SSH_USER=ubuntu # Initial user created by OVH, changed to adminclaw during hardening SSH_PORT=22 # Initial SSH port, changed to 222 during hardening ``` @@ -117,6 +118,8 @@ ssh-add ~/.ssh/vps1_openclaw_ed25519 # Test VPS-1 (OpenClaw) ssh -i ~/.ssh/vps1_openclaw_ed25519 ubuntu@ +# Or, if your key is already available through your SSH agent/config: +ssh ubuntu@ ``` On first connection, accept the host key fingerprint. @@ -187,9 +190,13 @@ ssh-add ~/.ssh/vps1_openclaw_ed25519 # SSH to OpenClaw VPS (before deployment - default port 22) ssh -i ~/.ssh/vps1_openclaw_ed25519 ubuntu@ +# Or with agent-based auth: +ssh ubuntu@ # After claude deployment and hardening - use port 222 and adminclaw user ssh -i ~/.ssh/vps1_openclaw_ed25519 -p 222 adminclaw@ +# Or with agent-based auth: +ssh -p 222 adminclaw@ ``` ### OVHCloud Control Panel Links diff --git a/playbooks/00-fresh-deploy-setup.md b/playbooks/00-fresh-deploy-setup.md index 3809fee..fb351f2 100644 --- a/playbooks/00-fresh-deploy-setup.md +++ b/playbooks/00-fresh-deploy-setup.md @@ -9,7 +9,7 @@ This playbook validates the configuration needed to deploy OpenClaw on a fresh U ## Prerequisites - A fresh Ubuntu VPS (>= 24.04) with root/sudo access -- An SSH key pair for VPS access +- SSH access to the VPS, either via a private key file or an SSH agent - A Cloudflare account with a domain - Cloudflare Tunnel token (`CLOUDFLARE_TUNNEL_TOKEN`, manual) OR Cloudflare API token (`CLOUDFLARE_API_TOKEN`, automated) - Cloudflare Access application protecting the domain @@ -47,7 +47,8 @@ echo "VPS_IP=${VPS_IP:-EMPTY}" && \ echo "CF_TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN:+SET}" && \ echo "CF_API_TOKEN=${CLOUDFLARE_API_TOKEN:+SET}" && \ echo "ADMIN_TELEGRAM_ID=${ADMIN_TELEGRAM_ID:-EMPTY}" && \ -echo "SSH_KEY=${SSH_KEY:-~/.ssh/vps1_openclaw_ed25519}" && \ +echo "SSH_KEY=${SSH_KEY:-EMPTY}" && \ +echo "SSH_IDENTITY_AGENT=${SSH_IDENTITY_AGENT:-EMPTY}" && \ grep '_TELEGRAM_BOT_TOKEN=' .env | grep -v '^#' && \ echo "=== stack.yml ===" && \ grep '^\s*domain:' stack.yml | head -1 && \ @@ -64,6 +65,7 @@ grep -A1 '^claws:' stack.yml | tail -n +2 | grep '^\s\+[a-z]' | sed 's/://;s/^\s 5. **`ADMIN_TELEGRAM_ID`** — Must be numeric. If empty: "Send a message to @userinfobot on Telegram to get your numeric user ID." 6. **Bot tokens** — Each claw name needs a matching `_TELEGRAM_BOT_TOKEN` line in `.env` (uppercased, hyphens→underscores). If missing: "Create a Telegram bot via @BotFather and paste the token. See `docs/TELEGRAM.md`." 7. **Claws** — The `claws` section lists claw names. Single claw = standard deploy. Multiple claws: inform user each gets its own container/domain. +8. **SSH auth** — At least one of `SSH_KEY` or `SSH_IDENTITY_AGENT` must be set. `SSH_KEY` is a file path; `SSH_IDENTITY_AGENT` is an agent socket path such as `~/.bitwarden-ssh-agent.sock`. ### If any fields are invalid or missing @@ -117,11 +119,13 @@ When `CF_API_TOKEN` is set, automate tunnel creation, route configuration, and D ## 0.3 SSH Check -1. Validate `SSH_KEY` exists on the local system (default: `~/.ssh/vps1_openclaw_ed25519`). -2. Test SSH connectivity using values from `.env` (`SSH_USER`, `SSH_PORT`): +1. If `SSH_KEY` is set, validate the file exists on the local system. +2. If `SSH_IDENTITY_AGENT` is set, validate the socket exists on the local system. +3. Test SSH connectivity using values from `.env` (`SSH_USER`, `SSH_PORT`): ```bash -ssh -i -o ConnectTimeout=10 -o BatchMode=yes -p @ echo "VPS OK" +ssh [ -i ] [ -o IdentityAgent= ] \ + -o ConnectTimeout=10 -o BatchMode=yes -p @ echo "VPS OK" ``` **If SSH fails — diagnose by error type:** @@ -152,7 +156,8 @@ Then retry the SSH test. > > - The key at `` wasn't added to the VPS during provisioning > - The key file doesn't exist — check: `ls -la ` -> - The SSH agent doesn't have the key loaded — try: `ssh-add `" +> - The SSH agent socket is wrong — check: `ls -l ` +> - The SSH agent doesn't have the key loaded — try: `ssh-add ` if you use a file-backed key" --- @@ -163,7 +168,8 @@ After SSH is confirmed working, query the VPS hardware to verify gateway contain ### Query VPS Resources ```bash -ssh -i -p @ "nproc && free -b | awk '/^Mem:/{print \$2}'" +ssh [ -i ] [ -o IdentityAgent= ] \ + -p @ "nproc && free -b | awk '/^Mem:/{print \$2}'" ``` This returns two lines: CPU count (e.g., `6`) and total memory in bytes (e.g., `11811160064`). @@ -349,7 +355,7 @@ A full deployment consumes significant context. To avoid mid-deploy compaction, ``` Read playbooks/04-vps1-openclaw.md §4.2 and execute the infrastructure setup. -SSH: ssh -i -p @ +SSH: ssh [ -i ] [ -o IdentityAgent= ] -p @ Log: Write detailed execution log (all commands, full output, errors, recovery steps) to .deploy-logs//04-infra-config.md Return: pass/fail. diff --git a/playbooks/00-onboarding.md b/playbooks/00-onboarding.md index eaee212..924782d 100644 --- a/playbooks/00-onboarding.md +++ b/playbooks/00-onboarding.md @@ -10,12 +10,13 @@ Triggered when the user says **"onboard"**. Walk through each configuration deci Before starting, verify that `install.sh` was run: -1. Check `.env` exists and has `VPS_IP`, `SSH_USER`, `SSH_KEY` populated (non-empty values) +1. Check `.env` exists and has `VPS_IP`, `SSH_USER`, and either `SSH_KEY` or `SSH_IDENTITY_AGENT` populated 2. Check `stack.yml` exists 3. Verify SSH connectivity: ```bash -ssh -i -o BatchMode=yes -o ConnectTimeout=5 -p @ echo "ok" +ssh [ -i ] [ -o IdentityAgent= ] \ + -o BatchMode=yes -o ConnectTimeout=5 -p @ echo "ok" ``` **If `.env` is missing or VPS fields are empty:** Tell the user to run `bash install.sh` first and stop here. diff --git a/playbooks/02-base-setup.md b/playbooks/02-base-setup.md index 9773374..c7123f0 100644 --- a/playbooks/02-base-setup.md +++ b/playbooks/02-base-setup.md @@ -31,12 +31,15 @@ This playbook configures: Config variables (read from `.env`): - `VPS_IP` - Public IP of VPS-1 -- `SSH_KEY` - Path to SSH private key +- `SSH_KEY` - Optional path to SSH private key +- `SSH_IDENTITY_AGENT` - Optional path to SSH agent socket - `SSH_USER` - Initial SSH user (e.g., ubuntu, root, debian — depends on provider) - `SSH_HARDENED_PORT` - Target SSH port for hardening (default: 222 if not set) - `CLOUDFLARE_TUNNEL_TOKEN` - Cloudflare Tunnel token - `HOSTNAME` - Optional, friendly hostname (replaces provider default) +> **SSH auth convention:** Examples below use `ssh -i ...` for brevity. If you use agent-based auth instead, omit `-i ` and add `-o IdentityAgent=` when needed. + ## Execution Order Complete sections 2.1–2.6 on VPS-1. diff --git a/playbooks/04-vps1-openclaw.md b/playbooks/04-vps1-openclaw.md index 28c6269..45a4c3c 100644 --- a/playbooks/04-vps1-openclaw.md +++ b/playbooks/04-vps1-openclaw.md @@ -33,6 +33,8 @@ Config values are read from `.env` and `stack.yml` (resolved by `npm run pre-dep - `defaults.install_dir` (`stack.yml`) - Base installation directory on VPS (default: `/home/openclaw`) - Per-claw overrides in `stack.yml` under `claws.` +> **SSH auth convention:** Commands below may show `ssh -i ${SSH_KEY} ...`. If the stack uses agent-based auth, omit `-i ${SSH_KEY}` and rely on your SSH config or add `-o IdentityAgent=${SSH_IDENTITY_AGENT}`. + --- ## 4.2 Infrastructure Setup diff --git a/playbooks/07-verification.md b/playbooks/07-verification.md index 490563a..708fa1f 100644 --- a/playbooks/07-verification.md +++ b/playbooks/07-verification.md @@ -28,6 +28,8 @@ This playbook verifies: - Workers deployed (01-workers.md) - VPS-1 rebooted after configuration +> **SSH auth convention:** Commands below may show `ssh -i ...`. If you use an SSH agent, omit `-i ` and use your normal SSH config or add `-o IdentityAgent=`. + ## Pre-Verification: Reboot VPS-1 Before running verification tests, reboot VPS-1 to ensure all configuration changes take effect cleanly (especially kernel parameters, SSH config, and systemd services). diff --git a/playbooks/08b-pair-devices.md b/playbooks/08b-pair-devices.md index 0496790..ca97dcb 100644 --- a/playbooks/08b-pair-devices.md +++ b/playbooks/08b-pair-devices.md @@ -8,6 +8,8 @@ Pair browser and Telegram devices with each claw's gateway. - Domain verified as protected by Cloudflare Access (during `00-fresh-deploy-setup.md`) - LLM proxy configured (optional but recommended — `08a-configure-llm-proxy.md`) +> **SSH auth convention:** Examples below may show `ssh -i ...`. If you use agent-based auth, omit `-i ` and rely on your SSH config or add `-o IdentityAgent=`. + --- ## Open the Claw URLs diff --git a/playbooks/08c-deploy-report.md b/playbooks/08c-deploy-report.md index 187f483..8c479c6 100644 --- a/playbooks/08c-deploy-report.md +++ b/playbooks/08c-deploy-report.md @@ -15,6 +15,8 @@ The report is saved to `.deploy-logs//08-deploy-report.md` (same time Collect the following values and present them in a single, neatly formatted report: +> **SSH auth convention:** Examples below may show `ssh -i ...`. If you use agent-based auth, omit `-i ` and rely on your SSH config or add `-o IdentityAgent=`. + ## Values to collect 1. **User passwords** — source `scripts/lib/source-config.sh` to get `ADMINCLAW_PASSWORD` and `OPENCLAW_PASSWORD`. These are auto-generated and persisted in `.env`. diff --git a/playbooks/maintenance.md b/playbooks/maintenance.md index a659270..8fb177b 100644 --- a/playbooks/maintenance.md +++ b/playbooks/maintenance.md @@ -180,6 +180,8 @@ sudo -u openclaw bash -c 'cd && docker compose up -d' #### SSH Keys +If your deployment uses agent-based auth, rotate the underlying key in your agent and update `SSH_IDENTITY_AGENT` only if the socket path changes. The file-based example below applies when `.env` uses `SSH_KEY`. + ```bash # 1. Generate new key pair (local machine) ssh-keygen -t ed25519 -f ~/.ssh/vps1_openclaw_ed25519_new diff --git a/scripts/health-check.sh b/scripts/health-check.sh index d210dbb..c75768b 100755 --- a/scripts/health-check.sh +++ b/scripts/health-check.sh @@ -9,6 +9,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/source-config.sh" +source "$SCRIPT_DIR/lib/ssh.sh" QUIET=false while [[ $# -gt 0 ]]; do @@ -36,7 +37,6 @@ while [[ $# -gt 0 ]]; do done export TERM=xterm-256color -SSH_CMD="ssh -i ${ENV__SSH_KEY} -p ${ENV__SSH_PORT} ${ENV__SSH_USER}@${ENV__VPS_IP}" FAILURES=0 log() { @@ -61,7 +61,7 @@ warn() { # --- SSH connectivity --- log "" log "Checking VPS connectivity..." -if ! $SSH_CMD "true" 2>/dev/null; then +if ! "${SSH_CMD[@]}" "$VPS" "true" 2>/dev/null; then fail "Cannot reach VPS at ${ENV__VPS_IP}:${ENV__SSH_PORT}" log "" log "$(printf '\033[31m%s check(s) failed.\033[0m')" "$FAILURES" @@ -97,7 +97,7 @@ CONTAINERS=("${CLAW_CONTAINERS[@]}" "${INFRA_CONTAINERS[@]}") log "" log "Checking Docker containers..." for CONTAINER in "${CONTAINERS[@]}"; do - STATUS=$($SSH_CMD "sudo docker inspect -f '{{.State.Status}}' $CONTAINER 2>/dev/null" 2>/dev/null || echo "not_found") + STATUS=$("${SSH_CMD[@]}" "$VPS" "sudo docker inspect -f '{{.State.Status}}' $CONTAINER 2>/dev/null" 2>/dev/null || echo "not_found") if [ "$STATUS" = "running" ]; then pass "$CONTAINER is running" @@ -110,7 +110,7 @@ for CONTAINER in "${CONTAINERS[@]}"; do fi # Check Docker healthcheck status if the container defines one - HEALTH=$($SSH_CMD "sudo docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' $CONTAINER 2>/dev/null" 2>/dev/null || echo "unknown") + HEALTH=$("${SSH_CMD[@]}" "$VPS" "sudo docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' $CONTAINER 2>/dev/null" 2>/dev/null || echo "unknown") case "$HEALTH" in healthy) pass "$CONTAINER healthcheck: healthy" ;; diff --git a/scripts/lib/logs-explorer/remote.ts b/scripts/lib/logs-explorer/remote.ts index 0f3e8f1..69c79a0 100644 --- a/scripts/lib/logs-explorer/remote.ts +++ b/scripts/lib/logs-explorer/remote.ts @@ -101,7 +101,8 @@ export function loadConfig(): Config { host: envVars.ENV__VPS_IP ?? '', port: envVars.ENV__SSH_PORT ?? '222', user: envVars.ENV__SSH_USER ?? 'adminclaw', - keyPath: expandHome(envVars.ENV__SSH_KEY ?? '~/.ssh/vps1_openclaw_ed25519'), + keyPath: envVars.ENV__SSH_KEY ? expandHome(envVars.ENV__SSH_KEY) : '', + identityAgent: envVars.ENV__SSH_IDENTITY_AGENT ? expandHome(envVars.ENV__SSH_IDENTITY_AGENT) : '', pythonScript: resolve(scriptDir, 'debug-sessions.py'), baseDir: '', // resolved by resolveInstance() llmLogPath: '', // resolved by resolveInstance() @@ -142,18 +143,19 @@ export async function resolveInstance(cfg: Config): Promise { } function sshBaseArgs(cfg: Config): string[] { - return [ + const args = [ 'ssh', - '-i', - cfg.keyPath, '-p', cfg.port, '-o', 'StrictHostKeyChecking=no', '-o', 'BatchMode=yes', - `${cfg.user}@${cfg.host}`, ] + if (cfg.keyPath) args.push('-i', cfg.keyPath) + if (cfg.identityAgent) args.push('-o', `IdentityAgent=${cfg.identityAgent}`) + args.push(`${cfg.user}@${cfg.host}`) + return args } export async function sshExec(cfg: Config, cmd: string): Promise { @@ -175,14 +177,14 @@ export async function uploadScript(cfg: Config): Promise { [ 'scp', '-q', - '-i', - cfg.keyPath, '-P', cfg.port, '-o', 'StrictHostKeyChecking=no', '-o', 'BatchMode=yes', + ...(cfg.keyPath ? ['-i', cfg.keyPath] : []), + ...(cfg.identityAgent ? ['-o', `IdentityAgent=${cfg.identityAgent}`] : []), cfg.pythonScript, `${cfg.user}@${cfg.host}:/tmp/debug-sessions.py`, ], diff --git a/scripts/lib/resolve-gateway.sh b/scripts/lib/resolve-gateway.sh index a155985..ece558c 100644 --- a/scripts/lib/resolve-gateway.sh +++ b/scripts/lib/resolve-gateway.sh @@ -9,10 +9,13 @@ # if exactly one, use it; if multiple, show interactive picker; # if zero, error with guidance. # -# Requires ENV__SSH_KEY, ENV__SSH_PORT, ENV__SSH_USER, ENV__VPS_IP to be set (from stack.env). +# Requires ENV__SSH_PORT, ENV__SSH_USER, ENV__VPS_IP (and optionally ENV__SSH_KEY) +# to be set from stack.env. # shellcheck source=select-claw.sh source "$(dirname "${BASH_SOURCE[0]}")/select-claw.sh" +# shellcheck source=ssh.sh +source "$(dirname "${BASH_SOURCE[0]}")/ssh.sh" resolve_gateway() { local instance="" @@ -44,8 +47,8 @@ resolve_gateway() { # Auto-detect: find running claw containers (match -openclaw- substring, exclude sandbox containers) local containers - containers=$(ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" -o ConnectTimeout=10 -o BatchMode=yes \ - "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + containers=$("${SSH_CMD[@]}" -o ConnectTimeout=10 -o BatchMode=yes \ + "$VPS" \ "sudo docker ps --format '{{.Names}}' --filter 'name=openclaw-'" 2>/dev/null \ | grep -v 'sbx-' \ || true) diff --git a/scripts/lib/ssh.sh b/scripts/lib/ssh.sh index 9862b21..f06ddf8 100644 --- a/scripts/lib/ssh.sh +++ b/scripts/lib/ssh.sh @@ -1,9 +1,28 @@ #!/usr/bin/env bash -# ssh.sh — Shared SSH and rsync helpers for scripts/. -# Source this after source-config.sh. Requires ENV__SSH_KEY, ENV__SSH_PORT, -# ENV__SSH_USER, ENV__VPS_IP from stack.env. +# ssh.sh — Shared SSH helpers for scripts/. +# Source this after source-config.sh. SSH_KEY is optional: if unset, ssh/scp +# fall back to the user's normal SSH config and agent behavior. -SSH_CMD="ssh -i ${ENV__SSH_KEY} -p ${ENV__SSH_PORT} -o StrictHostKeyChecking=accept-new" +SSH_ARGS=(-p "${ENV__SSH_PORT}" -o StrictHostKeyChecking=accept-new) +if [ -n "${ENV__SSH_KEY:-}" ]; then + SSH_ARGS=(-i "${ENV__SSH_KEY}" "${SSH_ARGS[@]}") +fi +if [ -n "${ENV__SSH_IDENTITY_AGENT:-}" ]; then + SSH_ARGS+=(-o "IdentityAgent=${ENV__SSH_IDENTITY_AGENT}") +fi + +SCP_ARGS=(-P "${ENV__SSH_PORT}" -o StrictHostKeyChecking=accept-new) +if [ -n "${ENV__SSH_KEY:-}" ]; then + SCP_ARGS=(-i "${ENV__SSH_KEY}" "${SCP_ARGS[@]}") +fi +if [ -n "${ENV__SSH_IDENTITY_AGENT:-}" ]; then + SCP_ARGS+=(-o "IdentityAgent=${ENV__SSH_IDENTITY_AGENT}") +fi + +SSH_CMD=(ssh "${SSH_ARGS[@]}") +SCP_CMD=(scp "${SCP_ARGS[@]}") +SSH_RSYNC_CMD=$(printf '%q ' ssh "${SSH_ARGS[@]}") +SSH_RSYNC_CMD=${SSH_RSYNC_CMD% } VPS="${ENV__SSH_USER}@${ENV__VPS_IP}" INSTALL_DIR="$STACK__STACK__INSTALL_DIR" @@ -12,7 +31,7 @@ INSTALL_DIR="$STACK__STACK__INSTALL_DIR" # Set RSYNC_EXTRA before calling to inject flags (e.g., RSYNC_EXTRA="--dry-run"). do_rsync() { rsync -avz ${RSYNC_EXTRA:-} \ - -e "${SSH_CMD}" \ + -e "${SSH_RSYNC_CMD}" \ --rsync-path='sudo rsync' \ "$@" } diff --git a/scripts/logs-docker.sh b/scripts/logs-docker.sh index 64c863f..6c060fc 100755 --- a/scripts/logs-docker.sh +++ b/scripts/logs-docker.sh @@ -10,6 +10,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/source-config.sh" +source "$SCRIPT_DIR/lib/ssh.sh" COMPOSE_DIR="${STACK__STACK__INSTALL_DIR}" COMPOSE_ARGS=("logs") @@ -25,7 +26,7 @@ fi printf "\033[32mStreaming logs from all containers on VPS-1 (%s)\033[0m\n" "$ENV__VPS_IP" -TERM=xterm-256color ssh -t -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +TERM=xterm-256color "${SSH_CMD[@]}" -t "$VPS" \ "sudo -u openclaw bash -c 'cd $COMPOSE_DIR && docker compose ${COMPOSE_ARGS[*]}'" # Alternate if multiple compose files: diff --git a/scripts/logs-explorer.sh b/scripts/logs-explorer.sh index 905e001..94abefa 100755 --- a/scripts/logs-explorer.sh +++ b/scripts/logs-explorer.sh @@ -72,19 +72,19 @@ fi source "$SCRIPT_DIR/lib/source-config.sh" source "$SCRIPT_DIR/lib/select-claw.sh" +source "$SCRIPT_DIR/lib/ssh.sh" # Common SSH/SCP options (scp uses -P for port, ssh uses -p) -SSH_COMMON=(-i "${ENV__SSH_KEY}" -o ConnectTimeout=10) -SSH_OPTS=("${SSH_COMMON[@]}" -p "${ENV__SSH_PORT}") -SCP_OPTS=("${SSH_COMMON[@]}" -P "${ENV__SSH_PORT}") +SSH_OPTS=("${SSH_ARGS[@]}" -o ConnectTimeout=10) +SCP_OPTS=("${SCP_ARGS[@]}" -o ConnectTimeout=10) # Resolve instance if not specified if [[ -z "$INSTANCE" ]]; then # adminclaw can't traverse /home/openclaw (750), so use sudo ls INSTANCES=$(ssh "${SSH_OPTS[@]}" -o BatchMode=yes \ - "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + "$VPS" \ "sudo ls -1 ${STACK__STACK__INSTALL_DIR}/instances/ 2>/dev/null | grep -v '^\\.'" 2>&1) || { - echo "Error: SSH connection failed. Check ENV__SSH_KEY, ENV__SSH_PORT, ENV__SSH_USER, ENV__VPS_IP in stack.env" >&2 + echo "Error: SSH connection failed. Check ENV__SSH_KEY/agent, ENV__SSH_PORT, ENV__SSH_USER, ENV__VPS_IP in stack.env" >&2 echo " ENV__SSH_USER=${ENV__SSH_USER} ENV__SSH_PORT=${ENV__SSH_PORT} ENV__VPS_IP=${ENV__VPS_IP}" >&2 exit 1 } @@ -109,7 +109,7 @@ if [[ ! -f "$PYTHON_SCRIPT" ]]; then fi # Copy script to VPS -if ! scp -q "${SCP_OPTS[@]}" "$PYTHON_SCRIPT" "${ENV__SSH_USER}@${ENV__VPS_IP}:${REMOTE_SCRIPT}"; then +if ! scp -q "${SCP_OPTS[@]}" "$PYTHON_SCRIPT" "${VPS}:${REMOTE_SCRIPT}"; then echo "Error: Failed to copy debug script to VPS" >&2 exit 1 fi diff --git a/scripts/logs-openclaw.sh b/scripts/logs-openclaw.sh index 29d1113..0cc33cb 100755 --- a/scripts/logs-openclaw.sh +++ b/scripts/logs-openclaw.sh @@ -11,6 +11,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/source-config.sh" +source "$SCRIPT_DIR/lib/ssh.sh" source "$SCRIPT_DIR/lib/resolve-gateway.sh" # Extract --instance before other args @@ -41,5 +42,5 @@ DOCKER_ARGS+=("$CONTAINER") printf "\033[32mStreaming logs from %s on VPS-1 (%s)\033[0m\n" "$CONTAINER" "$ENV__VPS_IP" -TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo docker ${DOCKER_ARGS[*]}" diff --git a/scripts/openclaw.sh b/scripts/openclaw.sh index 0ff5302..965dc06 100755 --- a/scripts/openclaw.sh +++ b/scripts/openclaw.sh @@ -16,6 +16,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/source-config.sh" +source "$SCRIPT_DIR/lib/ssh.sh" source "$SCRIPT_DIR/lib/resolve-gateway.sh" # Extract --instance before passing remaining args to openclaw @@ -42,5 +43,5 @@ GATEWAY=$(resolve_gateway ${INSTANCE_ARGS[@]+"${INSTANCE_ARGS[@]}"}) || exit 1 PROJECT_NAME="${STACK__STACK__PROJECT_NAME:-openclaw-stack}" INSTANCE_NAME="${GATEWAY#${PROJECT_NAME}-openclaw-}" -TERM=xterm-256color ssh -t -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +TERM=xterm-256color "${SSH_CMD[@]}" -t "$VPS" \ "openclaw --instance $INSTANCE_NAME ${REMAINING_ARGS[*]}" diff --git a/scripts/restart-gateway.sh b/scripts/restart-gateway.sh index a22c6c3..67818ce 100755 --- a/scripts/restart-gateway.sh +++ b/scripts/restart-gateway.sh @@ -12,25 +12,26 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/source-config.sh" +source "$SCRIPT_DIR/lib/ssh.sh" source "$SCRIPT_DIR/lib/resolve-gateway.sh" GATEWAY=$(resolve_gateway "$@") || exit 1 # Check gateway container exists -if ! ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +if ! "${SSH_CMD[@]}" "$VPS" \ "sudo docker inspect -f '{{.State.Running}}' $GATEWAY 2>/dev/null" | grep -q true; then echo "Error: $GATEWAY container is not running on VPS" >&2 exit 1 fi printf '\033[33mRestarting %s...\033[0m\n' "$GATEWAY" -TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo -u openclaw bash -c 'cd ${STACK__STACK__INSTALL_DIR} && docker compose restart $GATEWAY'" # Wait for gateway to be healthy printf '\033[33mWaiting for gateway to be healthy...\033[0m\n' for i in $(seq 1 30); do - STATUS=$(TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + STATUS=$(TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo docker inspect -f '{{.State.Health.Status}}' $GATEWAY 2>/dev/null" || echo "unknown") if [ "$STATUS" = "healthy" ]; then printf '\033[32mGateway is healthy.\033[0m\n' @@ -40,5 +41,11 @@ for i in $(seq 1 30); do done echo "Warning: gateway did not become healthy within 60s. Check logs with:" -echo " ssh -i ${ENV__SSH_KEY} -p ${ENV__SSH_PORT} ${ENV__SSH_USER}@${ENV__VPS_IP} 'sudo docker logs --tail 20 $GATEWAY'" +if [ -n "${ENV__SSH_KEY:-}" ]; then + echo " ssh -i ${ENV__SSH_KEY} -p ${ENV__SSH_PORT} ${ENV__SSH_USER}@${ENV__VPS_IP} 'sudo docker logs --tail 20 $GATEWAY'" +elif [ -n "${ENV__SSH_IDENTITY_AGENT:-}" ]; then + echo " ssh -o IdentityAgent=${ENV__SSH_IDENTITY_AGENT} -p ${ENV__SSH_PORT} ${ENV__SSH_USER}@${ENV__VPS_IP} 'sudo docker logs --tail 20 $GATEWAY'" +else + echo " ssh -p ${ENV__SSH_PORT} ${ENV__SSH_USER}@${ENV__VPS_IP} 'sudo docker logs --tail 20 $GATEWAY'" +fi exit 1 diff --git a/scripts/restart-sandboxes.sh b/scripts/restart-sandboxes.sh index f0115f4..5f03d4e 100755 --- a/scripts/restart-sandboxes.sh +++ b/scripts/restart-sandboxes.sh @@ -18,6 +18,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/source-config.sh" +source "$SCRIPT_DIR/lib/ssh.sh" source "$SCRIPT_DIR/lib/resolve-gateway.sh" ALL=false @@ -54,7 +55,7 @@ done GATEWAY=$(resolve_gateway ${INSTANCE_ARGS[@]+"${INSTANCE_ARGS[@]}"}) || exit 1 # Check gateway container is running -if ! ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +if ! "${SSH_CMD[@]}" "$VPS" \ "sudo docker inspect -f '{{.State.Running}}' $GATEWAY 2>/dev/null" | grep -q true; then echo "Error: $GATEWAY container is not running on VPS" >&2 exit 1 @@ -72,7 +73,7 @@ else FILTER="name=openclaw-sbx-" fi -CONTAINERS=$(TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +CONTAINERS=$(TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo docker exec $GATEWAY docker ps -a --filter '$FILTER' --format '{{.Names}}\t{{.Status}}\t{{.Image}}'" 2>/dev/null || true) # Filter out browser containers unless --all @@ -113,7 +114,7 @@ fi # Graceful stop (SIGTERM + 10s grace period) before removal. printf '\033[33mStopping sandbox containers...\033[0m\n' -TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo docker exec $GATEWAY docker stop $NAMES" 2>/dev/null || true # Use 'openclaw sandbox recreate' to remove containers AND clean the internal @@ -124,7 +125,7 @@ if [ "$ALL" = false ]; then RECREATE_FLAGS="--force" fi printf '\033[33mRemoving sandbox containers...\033[0m\n' -TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo docker exec --user node $GATEWAY openclaw sandbox recreate $RECREATE_FLAGS" echo "" diff --git a/scripts/ssh-agent.sh b/scripts/ssh-agent.sh index 70bd7e2..e0ffe30 100755 --- a/scripts/ssh-agent.sh +++ b/scripts/ssh-agent.sh @@ -15,6 +15,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/source-config.sh" +source "$SCRIPT_DIR/lib/ssh.sh" source "$SCRIPT_DIR/lib/resolve-gateway.sh" # Extract --instance before positional args @@ -36,12 +37,12 @@ MAX_WAIT=60 # seconds to wait for sandbox to appear # Helper: run a command on the VPS inside the gateway container as node gw_exec() { - TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo docker exec --user node $GATEWAY $*" } # Fetch agents list and sandbox status in one SSH call -COMBINED_JSON=$(TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +COMBINED_JSON=$(TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo docker exec --user node $GATEWAY sh -c ' echo \"===AGENTS===\" openclaw agents list --json 2>/dev/null @@ -155,7 +156,7 @@ if [[ "$CONTAINER_STATUS" == "none" ]]; then # Send a message to the agent to trigger sandbox creation. # The agent loop creates the sandbox container when a tool is needed (requires sandbox.mode = "all" per agent). # Stdout suppressed (agent response not needed); stderr preserved so errors are visible. - TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo docker exec --user node $GATEWAY openclaw agent --agent $AGENT --message ping --timeout 60" \ >/dev/null & AGENT_PID=$! @@ -202,15 +203,15 @@ fi # Start container if stopped — if it fails (stale registry entry), trigger recreation if [[ "$CONTAINER_STATUS" == "stopped" ]]; then printf '\033[33mContainer %s is stopped — starting...\033[0m\n' "$CONTAINER_NAME" - if ! ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + if ! "${SSH_CMD[@]}" "$VPS" \ "sudo docker exec $GATEWAY docker start $CONTAINER_NAME" >/dev/null 2>&1; then # Container was removed but registry still had an entry (e.g. after restart-sandboxes.sh). # Clean the stale entry and trigger fresh sandbox creation via agent message. printf '\033[33mContainer no longer exists — cleaning stale entry and recreating...\033[0m\n' - TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo docker exec --user node $GATEWAY openclaw sandbox recreate --all --force" >/dev/null 2>&1 || true - TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo docker exec --user node $GATEWAY openclaw agent --agent $AGENT --message ping --timeout 60" \ >/dev/null & AGENT_PID=$! @@ -249,5 +250,5 @@ if [[ "$CONTAINER_STATUS" == "stopped" ]]; then fi printf '\033[32mExec into %s (%s agent)\033[0m\n' "$CONTAINER_NAME" "$AGENT" -TERM=xterm-256color ssh -t -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +TERM=xterm-256color "${SSH_CMD[@]}" -t "$VPS" \ "sudo docker exec -it $GATEWAY docker exec -it -u 1000:1000 -w /workspace $CONTAINER_NAME bash" diff --git a/scripts/ssh-openclaw.sh b/scripts/ssh-openclaw.sh index ac022ca..62f8f4f 100755 --- a/scripts/ssh-openclaw.sh +++ b/scripts/ssh-openclaw.sh @@ -11,6 +11,7 @@ printf '\033[32mSSH into OpenClaw container \033[0m\n' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/source-config.sh" +source "$SCRIPT_DIR/lib/ssh.sh" source "$SCRIPT_DIR/lib/resolve-gateway.sh" GATEWAY=$(resolve_gateway "$@") || exit 1 @@ -18,5 +19,5 @@ GATEWAY=$(resolve_gateway "$@") || exit 1 printf '\033[32mStarting bash session in %s container \033[0m\n' "$GATEWAY" printf 'OpenClaw CLI:\033[33m openclaw \033[0m \n' printf 'Example: openclaw security audit --deep \n' -TERM=xterm-256color ssh -t -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +TERM=xterm-256color "${SSH_CMD[@]}" -t "$VPS" \ "sudo docker exec -it -u node $GATEWAY bash" diff --git a/scripts/ssh-vps.sh b/scripts/ssh-vps.sh index 1398ce0..09f2012 100755 --- a/scripts/ssh-vps.sh +++ b/scripts/ssh-vps.sh @@ -5,7 +5,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/source-config.sh" +source "$SCRIPT_DIR/lib/ssh.sh" printf "\033[32mSSH'ing into OpenClaw VPS as ${ENV__SSH_USER} \033[0m\n" # Set TERM to fix issues when running this script via ghostty -TERM=xterm-256color ssh -t -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" +TERM=xterm-256color "${SSH_CMD[@]}" -t "$VPS" diff --git a/scripts/start-browser.sh b/scripts/start-browser.sh index cc40959..73c49ff 100755 --- a/scripts/start-browser.sh +++ b/scripts/start-browser.sh @@ -17,6 +17,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/source-config.sh" +source "$SCRIPT_DIR/lib/ssh.sh" source "$SCRIPT_DIR/lib/resolve-gateway.sh" # Extract --instance before positional args @@ -35,13 +36,13 @@ MAX_WAIT=90 # seconds to wait for browser container # Helper: run a command inside the gateway container as node gw_exec() { - TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo docker exec --user node $GATEWAY $*" } # Helper: run a command inside the gateway container as root (for nested docker) gw_exec_root() { - TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo docker exec $GATEWAY $*" } @@ -121,7 +122,7 @@ if [[ -z "$BROWSER_CONTAINER" ]]; then printf '\033[33mNo existing browser container. Sending agent message to trigger creation...\033[0m\n' # Send a message that reliably triggers the browser tool - TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo docker exec --user node $GATEWAY openclaw agent --agent $AGENT \ --message 'Use the browser tool to navigate to about:blank. Do nothing else after that.' \ --timeout 90" >/dev/null 2>&1 & diff --git a/scripts/sync-media.sh b/scripts/sync-media.sh index 24b75ba..cde396d 100755 --- a/scripts/sync-media.sh +++ b/scripts/sync-media.sh @@ -10,6 +10,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/source-config.sh" +source "$SCRIPT_DIR/lib/ssh.sh" INSTANCE="" LOCAL_DIR="" @@ -28,7 +29,7 @@ while [[ $# -gt 0 ]]; do done LOCAL_DIR="${LOCAL_DIR:-$SCRIPT_DIR/../media}" -SSH_CMD="TERM=xterm-256color ssh -i ${ENV__SSH_KEY} -p ${ENV__SSH_PORT}" +SSH_RSYNC_CMD="TERM=xterm-256color ${SSH_RSYNC_CMD}" sync_instance() { local name="$1" @@ -38,7 +39,7 @@ sync_instance() { mkdir -p "$dest" echo "Syncing ${name} → $dest ..." rsync -avz --progress \ - -e "$SSH_CMD" \ + -e "$SSH_RSYNC_CMD" \ --rsync-path="sudo rsync" \ "${ENV__SSH_USER}@${ENV__VPS_IP}:${remote}" \ "$dest/" @@ -49,8 +50,8 @@ if [[ -n "$INSTANCE" ]]; then sync_instance "$INSTANCE" "$LOCAL_DIR/$INSTANCE" else # Discover all instances and sync each - INSTANCES=$(ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" -o ConnectTimeout=10 -o BatchMode=yes \ - "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + INSTANCES=$("${SSH_CMD[@]}" -o ConnectTimeout=10 -o BatchMode=yes \ + "$VPS" \ "sudo ls -1 ${STACK__STACK__INSTALL_DIR}/instances/ 2>/dev/null | grep -v '^\\.'" 2>&1) || { echo "Error: Could not list instances on VPS" >&2 exit 1 diff --git a/scripts/tag-deploy.sh b/scripts/tag-deploy.sh index 5bec7a5..8e68635 100755 --- a/scripts/tag-deploy.sh +++ b/scripts/tag-deploy.sh @@ -8,15 +8,12 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/source-config.sh" - -SSH_CMD="ssh -i ${ENV__SSH_KEY} -p ${ENV__SSH_PORT} -o StrictHostKeyChecking=accept-new" -VPS="${ENV__SSH_USER}@${ENV__VPS_IP}" -INSTALL_DIR="$STACK__STACK__INSTALL_DIR" +source "$SCRIPT_DIR/lib/ssh.sh" MSG="${1:-}" TAG="deploy-$(date +%Y%m%d-%H%M%S)" -${SSH_CMD} "${VPS}" "sudo -u openclaw bash -c 'cd ${INSTALL_DIR} && \ +"${SSH_CMD[@]}" "$VPS" "sudo -u openclaw bash -c 'cd ${INSTALL_DIR} && \ git tag -a \"${TAG}\" -m \"${MSG:-successful deploy}\"'" echo "Tagged: ${TAG}" diff --git a/scripts/update-openclaw.sh b/scripts/update-openclaw.sh index abfd528..631447e 100755 --- a/scripts/update-openclaw.sh +++ b/scripts/update-openclaw.sh @@ -11,6 +11,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/source-config.sh" +source "$SCRIPT_DIR/lib/ssh.sh" source "$SCRIPT_DIR/lib/resolve-gateway.sh" OPENCLAW_DIR="${STACK__STACK__INSTALL_DIR}/openclaw" @@ -39,22 +40,22 @@ printf '\033[32mUpdating OpenClaw on %s...\033[0m\n' "$ENV__VPS_IP" # Step 1: Pull upstream changes printf '\033[33m[1/4] Pulling upstream changes...\033[0m\n' -TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo -u openclaw bash -c 'cd $OPENCLAW_DIR && git pull'" # Step 2: Rebuild gateway image (stack-scoped: STACK__STACK__IMAGE from stack.env) printf '\033[33m[2/4] Building gateway image...\033[0m\n' -TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo -u openclaw ${STACK__STACK__INSTALL_DIR}/host/build-openclaw.sh" # Step 3: Recreate container(s) with new image (brief downtime) if [[ ${#INSTANCE_ARGS[@]} -gt 0 ]]; then printf '\033[33m[3/4] Recreating %s container...\033[0m\n' "$GATEWAY" - TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo -u openclaw bash -c 'cd ${STACK__STACK__INSTALL_DIR} && docker compose up -d $GATEWAY'" else printf '\033[33m[3/4] Recreating all gateway containers...\033[0m\n' - TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo -u openclaw bash -c 'cd ${STACK__STACK__INSTALL_DIR} && docker compose up -d'" fi @@ -63,7 +64,7 @@ printf '\033[33m[4/4] Waiting for %s to be healthy...\033[0m\n' "$GATEWAY" TIMEOUT=300 ELAPSED=0 while true; do - STATUS=$(TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + STATUS=$(TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo docker inspect -f '{{.State.Health.Status}}' $GATEWAY 2>/dev/null" 2>/dev/null || echo "unknown") if [ "$STATUS" = "healthy" ]; then break @@ -80,6 +81,6 @@ done # Show version echo "" -VERSION=$(TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +VERSION=$(TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "openclaw --instance $INSTANCE_NAME --version 2>/dev/null" || echo "(could not read version)") printf '\033[32mOpenClaw updated successfully. Version: %s\033[0m\n' "$VERSION" diff --git a/scripts/update-sandbox-toolkit.sh b/scripts/update-sandbox-toolkit.sh index 1b0f865..37b57c7 100755 --- a/scripts/update-sandbox-toolkit.sh +++ b/scripts/update-sandbox-toolkit.sh @@ -21,6 +21,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/source-config.sh" +source "$SCRIPT_DIR/lib/ssh.sh" source "$SCRIPT_DIR/lib/resolve-gateway.sh" SYNC_ONLY=false @@ -83,7 +84,7 @@ SYNC_REMOTE=( printf '\033[32mUpdating sandbox toolkit on %s...\033[0m\n' "$ENV__VPS_IP" # Check gateway container is running -if ! ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +if ! "${SSH_CMD[@]}" "$VPS" \ "sudo docker inspect -f '{{.State.Running}}' $GATEWAY 2>/dev/null" | grep -q true; then echo "Error: $GATEWAY container is not running on VPS" >&2 exit 1 @@ -112,7 +113,7 @@ for i in "${!SYNC_LOCAL[@]}"; do echo " [dry-run] Would sync $local_file -> $remote_path" else # Write as openclaw user directly — avoids temp files and permission issues - cat "$local_path" | ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + cat "$local_path" | "${SSH_CMD[@]}" "$VPS" \ "sudo -u openclaw tee $remote_path > /dev/null" echo " Synced $local_file" fi @@ -131,7 +132,7 @@ printf '\033[33m[%d/%d] Regenerating gateway shims...\033[0m\n' "$STEP" "$TOTAL_ if [ "$DRY_RUN" = true ]; then echo " [dry-run] Would regenerate shims via docker exec --user root" else - TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo docker exec -i --user root $GATEWAY sh" << 'SHIM_SCRIPT' TOOLKIT_CONFIG="/app/openclaw-stack/sandbox-toolkit.yaml" TOOLKIT_PARSER="/app/openclaw-stack/parse-toolkit.mjs" @@ -189,10 +190,10 @@ if [ "$DRY_RUN" = true ]; then fi if [ "$DRY_RUN" = true ]; then - TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + TERM=xterm-256color "${SSH_CMD[@]}" "$VPS" \ "sudo docker exec $GATEWAY /app/openclaw-stack/rebuild-sandboxes.sh $REBUILD_FLAGS" else - TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" -t "${ENV__SSH_USER}@${ENV__VPS_IP}" \ + TERM=xterm-256color "${SSH_CMD[@]}" -t "$VPS" \ "sudo docker exec $GATEWAY /app/openclaw-stack/rebuild-sandboxes.sh $REBUILD_FLAGS" fi diff --git a/scripts/update-sandboxes.sh b/scripts/update-sandboxes.sh index ca1f7fc..ddf0cb8 100755 --- a/scripts/update-sandboxes.sh +++ b/scripts/update-sandboxes.sh @@ -13,6 +13,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/source-config.sh" +source "$SCRIPT_DIR/lib/ssh.sh" source "$SCRIPT_DIR/lib/resolve-gateway.sh" # Pass through flags to rebuild-sandboxes.sh @@ -46,14 +47,14 @@ GATEWAY=$(resolve_gateway ${INSTANCE_ARGS[@]+"${INSTANCE_ARGS[@]}"}) || exit 1 printf '\033[32mRebuilding sandbox images on %s...\033[0m\n' "$ENV__VPS_IP" # Check gateway container is running -if ! ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +if ! "${SSH_CMD[@]}" "$VPS" \ "sudo docker inspect -f '{{.State.Running}}' $GATEWAY 2>/dev/null" | grep -q true; then echo "Error: $GATEWAY container is not running on VPS" >&2 exit 1 fi # Run rebuild-sandboxes.sh inside the running gateway container -TERM=xterm-256color ssh -i "${ENV__SSH_KEY}" -p "${ENV__SSH_PORT}" -t "${ENV__SSH_USER}@${ENV__VPS_IP}" \ +TERM=xterm-256color "${SSH_CMD[@]}" -t "$VPS" \ "sudo docker exec $GATEWAY /app/openclaw-stack/rebuild-sandboxes.sh $FLAGS" echo "" From 31637e549e8eefb753d466fbb2106f276c947b33 Mon Sep 17 00:00:00 2001 From: Nim G Date: Fri, 6 Mar 2026 15:42:26 -0300 Subject: [PATCH 2/7] Document SSH agent env option --- .env.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index f576b8b..516e54a 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,8 @@ HOSTNAME= SSH_USER=ubuntu # Initial SSH user — changed to adminclaw during hardening SSH_PORT=22 # Current SSH port — updated to SSH_HARDENED_PORT after hardening SSH_HARDENED_PORT=222 # Target SSH port for hardening (removed from .env after hardening completes) -SSH_KEY=~/.ssh/vps1_openclaw_ed25519 +SSH_KEY= # Optional: path to SSH private key file +SSH_IDENTITY_AGENT= # Optional: path to SSH agent socket (e.g. ~/.bitwarden-ssh-agent.sock) # ── STACK CONFIG ENV ───────────────────────────────────────────────── # The rest of the below vars are referenced in stack.yml.example From 5631861beaaba87d8907198c6c35983542af1633 Mon Sep 17 00:00:00 2001 From: Nim G Date: Fri, 6 Mar 2026 15:52:16 -0300 Subject: [PATCH 3/7] Fix SSH agent follow-up issues --- scripts/health-check.sh | 4 ++-- scripts/lib/logs-explorer/remote.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/health-check.sh b/scripts/health-check.sh index c75768b..3f625b4 100755 --- a/scripts/health-check.sh +++ b/scripts/health-check.sh @@ -125,7 +125,7 @@ done log "" log "Checking for recent container restarts..." for CONTAINER in "${CONTAINERS[@]}"; do - RESTART_COUNT=$($SSH_CMD "sudo docker inspect -f '{{.RestartCount}}' $CONTAINER 2>/dev/null" 2>/dev/null || echo "unknown") + RESTART_COUNT=$("${SSH_CMD[@]}" "$VPS" "sudo docker inspect -f '{{.RestartCount}}' $CONTAINER 2>/dev/null" 2>/dev/null || echo "unknown") if [ "$RESTART_COUNT" = "unknown" ]; then continue elif [ "$RESTART_COUNT" -gt 0 ] 2>/dev/null; then @@ -141,7 +141,7 @@ log "Checking OpenClaw gateway health..." for CLAW_CONTAINER in "${CLAW_CONTAINERS[@]}"; do INSTANCE_NAME="${CLAW_CONTAINER#${PROJECT_NAME}-openclaw-}" - HEALTH_OUTPUT=$($SSH_CMD "openclaw --instance $INSTANCE_NAME health 2>&1" 2>/dev/null) && HEALTH_EXIT=0 || HEALTH_EXIT=$? + HEALTH_OUTPUT=$("${SSH_CMD[@]}" "$VPS" "openclaw --instance $INSTANCE_NAME health 2>&1" 2>/dev/null) && HEALTH_EXIT=0 || HEALTH_EXIT=$? if [ "$HEALTH_EXIT" -eq 0 ]; then pass "openclaw health ($INSTANCE_NAME): OK" diff --git a/scripts/lib/logs-explorer/remote.ts b/scripts/lib/logs-explorer/remote.ts index 69c79a0..e4d7cb5 100644 --- a/scripts/lib/logs-explorer/remote.ts +++ b/scripts/lib/logs-explorer/remote.ts @@ -44,6 +44,7 @@ export interface Config { port: string user: string keyPath: string + identityAgent: string pythonScript: string baseDir: string llmLogPath: string From 2e1fd19498805f9f6a69ce3fa341b703dac55797 Mon Sep 17 00:00:00 2001 From: Nim G Date: Fri, 6 Mar 2026 15:55:00 -0300 Subject: [PATCH 4/7] Update resolve-gateway SSH comment --- scripts/lib/resolve-gateway.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/lib/resolve-gateway.sh b/scripts/lib/resolve-gateway.sh index ece558c..b9fd32d 100644 --- a/scripts/lib/resolve-gateway.sh +++ b/scripts/lib/resolve-gateway.sh @@ -9,8 +9,8 @@ # if exactly one, use it; if multiple, show interactive picker; # if zero, error with guidance. # -# Requires ENV__SSH_PORT, ENV__SSH_USER, ENV__VPS_IP (and optionally ENV__SSH_KEY) -# to be set from stack.env. +# Requires ENV__SSH_PORT, ENV__SSH_USER, ENV__VPS_IP, and optionally +# ENV__SSH_KEY / ENV__SSH_IDENTITY_AGENT from stack.env. # shellcheck source=select-claw.sh source "$(dirname "${BASH_SOURCE[0]}")/select-claw.sh" From 4e653bdbff555eded2a609e80a0149646106bc26 Mon Sep 17 00:00:00 2001 From: Nim G Date: Fri, 6 Mar 2026 16:07:57 -0300 Subject: [PATCH 5/7] Polish SSH agent docs and helper cleanup --- .env.example | 4 +++- docs/VPS-SETUP-GUIDE.md | 2 +- playbooks/00-fresh-deploy-setup.md | 2 +- playbooks/00-onboarding.md | 2 +- scripts/lib/ssh.sh | 1 - scripts/sync-media.sh | 2 ++ 6 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 516e54a..e6457ff 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,9 @@ HOSTNAME= SSH_USER=ubuntu # Initial SSH user — changed to adminclaw during hardening SSH_PORT=22 # Current SSH port — updated to SSH_HARDENED_PORT after hardening SSH_HARDENED_PORT=222 # Target SSH port for hardening (removed from .env after hardening completes) -SSH_KEY= # Optional: path to SSH private key file + +## Set SSH_KEY or SSH_IDENTITY_AGENT so scripts know how to reach the VPS. +SSH_KEY= # Optional: path to SSH private key file (e.g. ~/.ssh/vps1_openclaw_ed25519) SSH_IDENTITY_AGENT= # Optional: path to SSH agent socket (e.g. ~/.bitwarden-ssh-agent.sock) # ── STACK CONFIG ENV ───────────────────────────────────────────────── diff --git a/docs/VPS-SETUP-GUIDE.md b/docs/VPS-SETUP-GUIDE.md index 31471ee..dd210ed 100644 --- a/docs/VPS-SETUP-GUIDE.md +++ b/docs/VPS-SETUP-GUIDE.md @@ -99,7 +99,7 @@ Record it in `.env`: VPS_IP=x.x.x.x -# SSH Configuration (required) +# SSH Configuration (use SSH_KEY, SSH_IDENTITY_AGENT, or your normal ssh config) SSH_KEY=~/.ssh/vps1_openclaw_ed25519 # Optional: path to your ssh key file SSH_IDENTITY_AGENT= # Optional: ssh agent socket path if you use an agent instead of a key file SSH_USER=ubuntu # Initial user created by OVH, changed to adminclaw during hardening diff --git a/playbooks/00-fresh-deploy-setup.md b/playbooks/00-fresh-deploy-setup.md index fb351f2..b529bad 100644 --- a/playbooks/00-fresh-deploy-setup.md +++ b/playbooks/00-fresh-deploy-setup.md @@ -65,7 +65,7 @@ grep -A1 '^claws:' stack.yml | tail -n +2 | grep '^\s\+[a-z]' | sed 's/://;s/^\s 5. **`ADMIN_TELEGRAM_ID`** — Must be numeric. If empty: "Send a message to @userinfobot on Telegram to get your numeric user ID." 6. **Bot tokens** — Each claw name needs a matching `_TELEGRAM_BOT_TOKEN` line in `.env` (uppercased, hyphens→underscores). If missing: "Create a Telegram bot via @BotFather and paste the token. See `docs/TELEGRAM.md`." 7. **Claws** — The `claws` section lists claw names. Single claw = standard deploy. Multiple claws: inform user each gets its own container/domain. -8. **SSH auth** — At least one of `SSH_KEY` or `SSH_IDENTITY_AGENT` must be set. `SSH_KEY` is a file path; `SSH_IDENTITY_AGENT` is an agent socket path such as `~/.bitwarden-ssh-agent.sock`. +8. **SSH auth** — Use `SSH_KEY`, `SSH_IDENTITY_AGENT`, or a compatible local SSH config/agent setup. `SSH_KEY` is a file path; `SSH_IDENTITY_AGENT` is an agent socket path such as `~/.bitwarden-ssh-agent.sock`. If both env vars are empty, confirm your normal `ssh` config can reach the VPS without extra flags. ### If any fields are invalid or missing diff --git a/playbooks/00-onboarding.md b/playbooks/00-onboarding.md index 924782d..8dcf03d 100644 --- a/playbooks/00-onboarding.md +++ b/playbooks/00-onboarding.md @@ -10,7 +10,7 @@ Triggered when the user says **"onboard"**. Walk through each configuration deci Before starting, verify that `install.sh` was run: -1. Check `.env` exists and has `VPS_IP`, `SSH_USER`, and either `SSH_KEY` or `SSH_IDENTITY_AGENT` populated +1. Check `.env` exists and has `VPS_IP`, `SSH_USER`, and either SSH auth env vars populated or a compatible local SSH config/agent setup 2. Check `stack.yml` exists 3. Verify SSH connectivity: diff --git a/scripts/lib/ssh.sh b/scripts/lib/ssh.sh index f06ddf8..064b34e 100644 --- a/scripts/lib/ssh.sh +++ b/scripts/lib/ssh.sh @@ -20,7 +20,6 @@ if [ -n "${ENV__SSH_IDENTITY_AGENT:-}" ]; then fi SSH_CMD=(ssh "${SSH_ARGS[@]}") -SCP_CMD=(scp "${SCP_ARGS[@]}") SSH_RSYNC_CMD=$(printf '%q ' ssh "${SSH_ARGS[@]}") SSH_RSYNC_CMD=${SSH_RSYNC_CMD% } VPS="${ENV__SSH_USER}@${ENV__VPS_IP}" diff --git a/scripts/sync-media.sh b/scripts/sync-media.sh index cde396d..4379973 100755 --- a/scripts/sync-media.sh +++ b/scripts/sync-media.sh @@ -29,6 +29,8 @@ while [[ $# -gt 0 ]]; do done LOCAL_DIR="${LOCAL_DIR:-$SCRIPT_DIR/../media}" +# Prefix the remote shell command so rsync runs ssh with a real TERM; this is +# only used by the inline rsync invocation below, not by do_rsync() in ssh.sh. SSH_RSYNC_CMD="TERM=xterm-256color ${SSH_RSYNC_CMD}" sync_instance() { From 7da5716914ace1d758ff98a3cc8f413f675bc44e Mon Sep 17 00:00:00 2001 From: Nim G Date: Fri, 6 Mar 2026 17:17:40 -0300 Subject: [PATCH 6/7] Improve SSH agent auth handling --- playbooks/00-fresh-deploy-setup.md | 1 + playbooks/02-base-setup.md | 16 ++++++++++++++++ scripts/lib/logs-explorer/remote.ts | 4 ++-- scripts/lib/ssh.sh | 4 ++-- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/playbooks/00-fresh-deploy-setup.md b/playbooks/00-fresh-deploy-setup.md index b529bad..c89b6b0 100644 --- a/playbooks/00-fresh-deploy-setup.md +++ b/playbooks/00-fresh-deploy-setup.md @@ -326,6 +326,7 @@ After the user confirms, launch **01-workers and 02-base-setup as parallel subag - A command fails and the error requires user input to resolve - A playbook step explicitly says to wait for user input (e.g., a blocking error with multiple resolution paths) - **SSH verification (02-base-setup.md § 2.4 Step 3):** You MUST test SSH on port `` from the local machine and confirm it works before proceeding. This is a mandatory stop point — do not skip it during automated deployment. +- **Local SSH config update:** If the user connects via a `~/.ssh/config` host alias or agent-based SSH config, prompt them to update that local entry to port `` immediately after the local hardened-port test passes and before removing port 22. - **07-verification.md:** Run in the main context (not a subagent) so the user sees real-time progress and errors can be handled directly. By this point, all heavy steps have been offloaded to subagents and the context window has room. Report the summary table, then run `scripts/sync-workspaces.sh down --all` to pull back any files OpenClaw generated on first start, before proceeding to 08a-configure-llm-proxy.md. Normal informational output (progress updates, version notes, check results) should be reported inline without pausing. The first user interaction after confirmation should be device pairing in `08b-pair-devices.md`. diff --git a/playbooks/02-base-setup.md b/playbooks/02-base-setup.md index c7123f0..2891142 100644 --- a/playbooks/02-base-setup.md +++ b/playbooks/02-base-setup.md @@ -317,6 +317,22 @@ ssh -i -p adminclaw@ "echo 'Port # Changed from 22 during hardening` 3. Delete the `SSH_HARDENED_PORT=` line entirely +If the user relies on a local `~/.ssh/config` host alias or SSH agent-based config, this is also the safe moment to update that host entry to the hardened port. Prompt them explicitly before locking down port 22. + +Example `~/.ssh/config` update: + +```sshconfig +Host + HostName + User adminclaw + Port + IdentityAgent + IdentitiesOnly yes + PreferredAuthentications publickey +``` + +Require the user to confirm they updated their local SSH config entry, or that they intentionally do not use one, before continuing to remove port 22. + Then lock down SSH: ```bash diff --git a/scripts/lib/logs-explorer/remote.ts b/scripts/lib/logs-explorer/remote.ts index e4d7cb5..e254be7 100644 --- a/scripts/lib/logs-explorer/remote.ts +++ b/scripts/lib/logs-explorer/remote.ts @@ -154,7 +154,7 @@ function sshBaseArgs(cfg: Config): string[] { 'BatchMode=yes', ] if (cfg.keyPath) args.push('-i', cfg.keyPath) - if (cfg.identityAgent) args.push('-o', `IdentityAgent=${cfg.identityAgent}`) + if (cfg.identityAgent) args.push('-o', `IdentityAgent=${cfg.identityAgent}`, '-o', 'IdentitiesOnly=yes') args.push(`${cfg.user}@${cfg.host}`) return args } @@ -185,7 +185,7 @@ export async function uploadScript(cfg: Config): Promise { '-o', 'BatchMode=yes', ...(cfg.keyPath ? ['-i', cfg.keyPath] : []), - ...(cfg.identityAgent ? ['-o', `IdentityAgent=${cfg.identityAgent}`] : []), + ...(cfg.identityAgent ? ['-o', `IdentityAgent=${cfg.identityAgent}`, '-o', 'IdentitiesOnly=yes'] : []), cfg.pythonScript, `${cfg.user}@${cfg.host}:/tmp/debug-sessions.py`, ], diff --git a/scripts/lib/ssh.sh b/scripts/lib/ssh.sh index 064b34e..e444f70 100644 --- a/scripts/lib/ssh.sh +++ b/scripts/lib/ssh.sh @@ -8,7 +8,7 @@ if [ -n "${ENV__SSH_KEY:-}" ]; then SSH_ARGS=(-i "${ENV__SSH_KEY}" "${SSH_ARGS[@]}") fi if [ -n "${ENV__SSH_IDENTITY_AGENT:-}" ]; then - SSH_ARGS+=(-o "IdentityAgent=${ENV__SSH_IDENTITY_AGENT}") + SSH_ARGS+=(-o "IdentityAgent=${ENV__SSH_IDENTITY_AGENT}" -o "IdentitiesOnly=yes") fi SCP_ARGS=(-P "${ENV__SSH_PORT}" -o StrictHostKeyChecking=accept-new) @@ -16,7 +16,7 @@ if [ -n "${ENV__SSH_KEY:-}" ]; then SCP_ARGS=(-i "${ENV__SSH_KEY}" "${SCP_ARGS[@]}") fi if [ -n "${ENV__SSH_IDENTITY_AGENT:-}" ]; then - SCP_ARGS+=(-o "IdentityAgent=${ENV__SSH_IDENTITY_AGENT}") + SCP_ARGS+=(-o "IdentityAgent=${ENV__SSH_IDENTITY_AGENT}" -o "IdentitiesOnly=yes") fi SSH_CMD=(ssh "${SSH_ARGS[@]}") From 8fa05c313c902806ff46ec0f412a124165ce9563 Mon Sep 17 00:00:00 2001 From: Nim G Date: Sat, 7 Mar 2026 10:03:56 -0300 Subject: [PATCH 7/7] fix(ssh): remove IdentitiesOnly=yes when using SSH_IDENTITY_AGENT IdentitiesOnly=yes restricts auth to explicitly listed identity files. When SSH_KEY is unset, this causes auth failures because no IdentityFile is specified. Remove it so the agent can offer all loaded keys. --- scripts/lib/ssh.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/lib/ssh.sh b/scripts/lib/ssh.sh index e444f70..064b34e 100644 --- a/scripts/lib/ssh.sh +++ b/scripts/lib/ssh.sh @@ -8,7 +8,7 @@ if [ -n "${ENV__SSH_KEY:-}" ]; then SSH_ARGS=(-i "${ENV__SSH_KEY}" "${SSH_ARGS[@]}") fi if [ -n "${ENV__SSH_IDENTITY_AGENT:-}" ]; then - SSH_ARGS+=(-o "IdentityAgent=${ENV__SSH_IDENTITY_AGENT}" -o "IdentitiesOnly=yes") + SSH_ARGS+=(-o "IdentityAgent=${ENV__SSH_IDENTITY_AGENT}") fi SCP_ARGS=(-P "${ENV__SSH_PORT}" -o StrictHostKeyChecking=accept-new) @@ -16,7 +16,7 @@ if [ -n "${ENV__SSH_KEY:-}" ]; then SCP_ARGS=(-i "${ENV__SSH_KEY}" "${SCP_ARGS[@]}") fi if [ -n "${ENV__SSH_IDENTITY_AGENT:-}" ]; then - SCP_ARGS+=(-o "IdentityAgent=${ENV__SSH_IDENTITY_AGENT}" -o "IdentitiesOnly=yes") + SCP_ARGS+=(-o "IdentityAgent=${ENV__SSH_IDENTITY_AGENT}") fi SSH_CMD=(ssh "${SSH_ARGS[@]}")