diff --git a/.github/workflows/bootstrap-tests.yml b/.github/workflows/bootstrap-tests.yml new file mode 100644 index 0000000..eb12c3d --- /dev/null +++ b/.github/workflows/bootstrap-tests.yml @@ -0,0 +1,20 @@ +name: Bootstrap regression tests + +on: + pull_request: + push: + branches: [main, codex/openclaw-development-recovery] + +permissions: + contents: read + +jobs: + regression: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Validate shell syntax + run: bash -n launch.sh configure.sh tests/bootstrap-regression.sh + - name: Test bootstrap URL and restart behavior + run: bash tests/bootstrap-regression.sh diff --git a/README.md b/README.md index 7d3cc41..7960e59 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ To generate an API key, visit [build.nvidia.com/settings/api-keys](https://build ======================== URL: - https://openclaw-xxxxxxxx.apps.run.brev.nvidia.com/#token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + https://openclaw-xxxxxxxx.gobrev.dev/#token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx API Token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx @@ -47,10 +47,10 @@ To generate an API key, visit [build.nvidia.com/settings/api-keys](https://build brev-xxxxxxxx Origin: - https://openclaw-xxxxxxxx.apps.run.brev.nvidia.com + https://openclaw-xxxxxxxx.gobrev.dev ``` -1. Copy the full URL (looks like `https://openclaw-xxxxxxxx.apps.run.brev.nvidia.com/#token=xxx...`) and open it in a new tab. +1. Copy the full URL (looks like `https://openclaw-xxxxxxxx.gobrev.dev/#token=xxx...`) and open it in a new tab. 1. Click the "Connect" button to open the OpenClaw chat interface. You may need to click this button more than once. 1. Start chatting with the agent. @@ -75,8 +75,8 @@ The launchable is split into two stages: `launch.sh`: -1. Ensures Node.js 22 or newer is installed. -2. Installs the pinned OpenClaw npm package version `2026.3.13`, which corresponds to GitHub release [`v2026.3.13-1`](https://github.com/openclaw/openclaw/releases/tag/v2026.3.13-1). +1. Ensures a supported Node.js runtime is installed (`>=24.16.0 <25` or `>=26.1.0`); installs Node.js 24 LTS when needed. +2. Installs the pinned OpenClaw npm package version `2026.9.9`, which corresponds to the stable GitHub release [`v2026.9.9`](https://github.com/openclaw/openclaw/releases/tag/v2026.9.9). 3. Verifies the `openclaw` CLI is available. 4. Clones or refreshes `https://github.com/liveaverage/launch-openclaw.git` into `~/launch-openclaw` by default. 5. Installs `code-server`, the custom NV Theme, and the `fabiospampinato.vscode-terminals` extension. @@ -98,23 +98,36 @@ The launchable is split into two stages: On hosts named like `brev-`, the launchable derives: ```text -OpenClaw: https://openclaw-.apps.run.brev.nvidia.com/chat?session=main -code-server: https://code-server0-.apps.run.brev.nvidia.com +OpenClaw: https://openclaw-.gobrev.dev/chat?session=main +code-server: https://code-server-.gobrev.dev ``` If the hostname does not match the Brev naming pattern, it falls back to: ```text -OpenClaw: http://localhost:3000/chat?session=main +OpenClaw: http://localhost:18789/chat?session=main code-server: http://localhost:13337 ``` +The Brev console (or `brev ports ls --json`) is authoritative for +secure-link URLs. For custom service names or domains, set `OPENCLAW_ORIGIN` +and `CODE_SERVER_ORIGIN` to the exact HTTPS origins before running the scripts. +The Control UI retains the previous Brev and legacy LaunchPad origins alongside +the current origin; it does not enable wildcard origins or disable token auth. + +After configuration, re-running `launch.sh` restarts the gateway using the saved +credentials instead of displaying the first-run configuration prompt. This is +a bootstrap re-run guarantee, not an automatic gateway restart after a VM reboot. + ### Re-run Safety The bootstrap is designed to be safe to run multiple times: - It skips Node installation when a compatible version is already installed. - It keeps OpenClaw pinned to the npm package version resolved from `OPENCLAW_RELEASE_TAG` and reinstalls if another version is found on PATH. +- OpenClaw 2026.9.9 requires explicit reverse-proxy attribution. Set `OPENCLAW_TRUSTED_PROXY_IPS` to a JSON array of verified Brev proxy IPs (for example, `'["100.73.87.46"]'` on the tested Development route). Proxy addresses are deployment-specific; do not assume this example applies to another region or route. Only individual IPs are accepted, not CIDRs or wildcards. Ensure the proxy safely overwrites/rebuilds forwarded headers. Gateway token authentication remains enabled; this setting does not enable proxy-only authentication. +- Fresh releases may store the gateway token as a SecretRef. The configuration terminal uses OpenClaw's official dashboard bootstrap link and rewrites both its browser origin and WebSocket target to the Brev secure link. The temporary dashboard link can expire; rerun configuration to generate another. SecretRef objects are never presented as plaintext tokens or replaced simply because they are references. +- Before restarting an existing gateway, it validates the config and uses safe, non-interactive upstream Doctor migrations when required, retaining a private rollback copy. If Doctor cannot repair it (for example, an obsolete gateway lock blocks migration), bootstrap fails with the backup location rather than starting an incompatible gateway. Inspect and resolve the reported stale process/lock before retrying; do not remove locks belonging to a running gateway. - It refreshes the local `~/launch-openclaw` checkout if it already exists. - It skips the first-run configure terminal after both `~/.openclaw/.env` and `~/.openclaw/openclaw.json` exist. - It reuses a running gateway if a previously started process is still alive. @@ -123,19 +136,19 @@ The bootstrap is designed to be safe to run multiple times: Target a GitHub release tag: ```bash -OPENCLAW_RELEASE_TAG=v2026.3.24 ./launch.sh +OPENCLAW_RELEASE_TAG=v2026.9.9 ./launch.sh ``` For piped execution: ```bash -curl -fsSL https://raw.githubusercontent.com/liveaverage/launch-openclaw/refs/heads/main/launch.sh | OPENCLAW_RELEASE_TAG=v2026.3.24 bash +curl -fsSL https://raw.githubusercontent.com/liveaverage/launch-openclaw/refs/heads/main/launch.sh | OPENCLAW_RELEASE_TAG=v2026.9.9 bash ``` Override the npm package version directly if needed: ```bash -OPENCLAW_VERSION=2026.3.13 ./launch.sh +OPENCLAW_VERSION=2026.9.9 ./launch.sh ``` When `OPENCLAW_VERSION` is unset, `launch.sh` now resolves it from `OPENCLAW_RELEASE_TAG`. It first tries the exact tag without the leading `v`, then falls back to removing a trailing GitHub-only `-N` suffix such as `v2026.3.13-1 -> 2026.3.13`. @@ -160,7 +173,7 @@ OpenClaw Gateway Started ======================== URL: -https://openclaw-.apps.run.brev.nvidia.com/chat?session=main +https://openclaw-.gobrev.dev/chat?session=main API Token: @@ -169,10 +182,10 @@ Hostname: brev- Origin: -https://openclaw-.apps.run.brev.nvidia.com +https://openclaw-.gobrev.dev code-server: -https://code-server0-.apps.run.brev.nvidia.com +https://code-server-.gobrev.dev ``` ### Logs and State diff --git a/configure.sh b/configure.sh index 5a9c40d..1bd1112 100755 --- a/configure.sh +++ b/configure.sh @@ -74,9 +74,9 @@ get_gateway_token_from_config_file() { [[ -f "$config_file" ]] || return 1 if command -v jq >/dev/null 2>&1; then - token="$(jq -r '.gateway.auth.token // empty' "$config_file" 2>/dev/null | sed -n '1p')" || true + token="$(jq -r '.gateway.auth.token | select(type == "string")' "$config_file" 2>/dev/null | sed -n '1p')" || true else - token="$(node -e 'const fs=require("fs");const p=process.argv[1];const j=JSON.parse(fs.readFileSync(p,"utf8"));if(j?.gateway?.auth?.token)process.stdout.write(j.gateway.auth.token);' "$config_file" 2>/dev/null)" || true + token="$(node -e 'const fs=require("fs");const p=process.argv[1];const j=JSON.parse(fs.readFileSync(p,"utf8"));if(typeof j?.gateway?.auth?.token==="string")process.stdout.write(j.gateway.auth.token);' "$config_file" 2>/dev/null)" || true fi [[ -n "$token" && "$token" != "__OPENCLAW_REDACTED__" && "$token" != "null" ]] || return 1 @@ -115,7 +115,7 @@ get_gateway_token() { local status=$? set -e - if [[ "$status" -eq 0 && -n "$token" && "$token" != "null" && "$token" != "undefined" && "$token" != "__OPENCLAW_REDACTED__" ]]; then + if [[ "$status" -eq 0 && "$token" =~ ^[[:alnum:]_-]+$ && "$token" != "null" && "$token" != "undefined" && "$token" != "__OPENCLAW_REDACTED__" ]]; then printf '%s\n' "$token" return 0 fi @@ -123,6 +123,43 @@ get_gateway_token() { return 1 } +gateway_token_is_configured() { + [[ -n "${OPENCLAW_GATEWAY_TOKEN:-}" ]] && return 0 + node -e ' + const fs=require("fs"); + try { + const token=JSON.parse(fs.readFileSync(process.argv[1],"utf8"))?.gateway?.auth?.token; + if(typeof token==="string" ? token.trim().length : token && typeof token==="object" && token.source && token.id) process.exit(0); + } catch {} + process.exit(1); + ' "$HOME/.openclaw/openclaw.json" +} + +get_dashboard_connection_url() { + local details origin + origin="$(derive_openclaw_origin)" + details="$(openclaw dashboard --no-open --json 2>/dev/null)" || return 1 + printf '%s' "$details" | node -e ' + const fs=require("fs"); + try { + const details=JSON.parse(fs.readFileSync(0,"utf8")); + if(!details.ok) process.exit(1); + const generated=new URL(details.browserUrl || details.url); + const external=new URL(process.argv[1]); + const url=new URL(generated.pathname+generated.search+generated.hash,external); + const params=new URLSearchParams(url.hash.slice(1)); + if(params.has("gatewayUrl")) { + const gateway=new URL(external); + gateway.protocol=external.protocol==="https:" ? "wss:" : "ws:"; + gateway.pathname="/"; gateway.search=""; gateway.hash=""; + params.set("gatewayUrl",gateway.origin); + url.hash=params.toString(); + } + process.stdout.write(url.toString()); + } catch { process.exit(1); } + ' "$origin" +} + start_gateway() { local gateway_log="$1" local pid_file="$2" @@ -214,18 +251,22 @@ start_auto_approve_loop() { derive_openclaw_origin() { local host_name env_id + if [[ -n "${OPENCLAW_ORIGIN:-}" ]]; then + printf '%s\n' "${OPENCLAW_ORIGIN%/}" + return + fi host_name="$(hostname 2>/dev/null || true)" env_id="$(printf '%s\n' "$host_name" | sed -E 's/^brev-([[:alnum:]]+)$/\1/')" if [[ -n "$env_id" && "$env_id" != "$host_name" ]]; then - printf 'https://openclaw-%s.apps.run.brev.nvidia.com\n' "$env_id" + printf 'https://openclaw-%s.gobrev.dev\n' "$env_id" else - printf 'http://localhost:3000\n' + printf 'http://localhost:18789\n' fi } configure_control_ui_origin() { - local origin legacy_origin env_id host_name + local origin legacy_origin previous_origin env_id host_name local origins_json origin="$(derive_openclaw_origin)" @@ -235,8 +276,9 @@ configure_control_ui_origin() { # Include the legacy Launchpad-hosted Skybridge origin if [[ -n "$env_id" && "$env_id" != "$host_name" ]]; then legacy_origin="https://openclaw-${env_id}.stg.apps.launchpad.nvidia.com" + previous_origin="https://openclaw-${env_id}.apps.run.brev.nvidia.com" origins_json="$(cat </dev/null 2>&1; then + return + fi + + config_file="$HOME/.openclaw/openclaw.json" + backup_file="$(mktemp "$config_file.pre-migration.XXXXXX")" + chmod 600 "$backup_file" + (umask 077; cp "$config_file" "$backup_file") + chmod 600 "$backup_file" + log "Migrating legacy OpenClaw config; rollback copy: $backup_file" + # Safe, non-interactive upstream migrations only; never force a service repair. + openclaw doctor --fix --non-interactive || fail "Config migration failed. Original config retained at $backup_file; inspect doctor output before retrying." + openclaw config validate >/dev/null 2>&1 || fail "Migrated config is still invalid. Original config retained at $backup_file." +} + +configure_trusted_proxies() { + [[ -n "${OPENCLAW_TRUSTED_PROXY_IPS:-}" ]] || return 0 + node -e ' + const {isIP}=require("net"); + try { + const ips=JSON.parse(process.argv[1]); + if(!Array.isArray(ips) || !ips.length || ips.some(ip=>typeof ip!=="string" || !isIP(ip) || ip==="0.0.0.0" || ip==="::")) process.exit(1); + } catch { process.exit(1); } + ' "$OPENCLAW_TRUSTED_PROXY_IPS" || fail "OPENCLAW_TRUSTED_PROXY_IPS must be a JSON array of explicit proxy IPs, not wildcards or network ranges." + log "Configuring explicitly trusted reverse proxy IPs (gateway token authentication is unchanged)" + openclaw config set gateway.trustedProxies "$OPENCLAW_TRUSTED_PROXY_IPS" --strict-json +} + main() { local gateway_log approval_log pid_file token gateway_pid @@ -372,16 +446,18 @@ main() { load_saved_env fi + stop_existing_gateway "$pid_file" + ensure_config_compatible configure_control_ui_origin + configure_trusted_proxies token="$(get_gateway_token || true)" - if [[ -z "$token" ]]; then + if [[ -z "$token" ]] && ! gateway_token_is_configured; then log "No configured gateway token found; generating one with OpenClaw doctor" openclaw doctor --generate-gateway-token >/dev/null token="$(get_gateway_token || true)" fi - stop_existing_gateway "$pid_file" log "Starting OpenClaw gateway" gateway_pid="$(start_gateway "$gateway_log" "$pid_file")" log "Gateway is running with PID $gateway_pid" diff --git a/launch.sh b/launch.sh index 4c3710f..4e652f7 100755 --- a/launch.sh +++ b/launch.sh @@ -15,8 +15,8 @@ fi CODE_SERVER_VERSION="${CODE_SERVER_VERSION:-4.89.1}" CODE_SERVER_PORT="${CODE_SERVER_PORT:-13337}" -DEFAULT_OPENCLAW_VERSION="2026.3.13" -DEFAULT_OPENCLAW_RELEASE_TAG="v2026.3.13-1" +DEFAULT_OPENCLAW_VERSION="2026.9.9" +DEFAULT_OPENCLAW_RELEASE_TAG="v2026.9.9" OPENCLAW_VERSION="${OPENCLAW_VERSION:-}" OPENCLAW_RELEASE_TAG="${OPENCLAW_RELEASE_TAG:-$DEFAULT_OPENCLAW_RELEASE_TAG}" OPENCLAW_ENV_FILE="${OPENCLAW_ENV_FILE:-$HOME/.openclaw/.env}" @@ -203,46 +203,63 @@ is_openclaw_configured() { derive_openclaw_origin() { local host_name env_id + if [[ -n "${OPENCLAW_ORIGIN:-}" ]]; then + printf '%s\n' "${OPENCLAW_ORIGIN%/}" + return + fi host_name="$(hostname 2>/dev/null || true)" env_id="$(printf '%s\n' "$host_name" | sed -E 's/^brev-([[:alnum:]]+)$/\1/')" if [[ -n "$env_id" && "$env_id" != "$host_name" ]]; then - printf 'https://openclaw-%s.apps.run.brev.nvidia.com\n' "$env_id" + printf 'https://openclaw-%s.gobrev.dev\n' "$env_id" else - printf 'http://localhost:3000\n' + printf 'http://localhost:18789\n' fi } derive_code_server_origin() { local host_name env_id + if [[ -n "${CODE_SERVER_ORIGIN:-}" ]]; then + printf '%s\n' "${CODE_SERVER_ORIGIN%/}" + return + fi host_name="$(hostname 2>/dev/null || true)" env_id="$(printf '%s\n' "$host_name" | sed -E 's/^brev-([[:alnum:]]+)$/\1/')" if [[ -n "$env_id" && "$env_id" != "$host_name" ]]; then - printf 'https://code-server0-%s.apps.run.brev.nvidia.com\n' "$env_id" + printf 'https://code-server-%s.gobrev.dev\n' "$env_id" else printf 'http://localhost:%s\n' "$CODE_SERVER_PORT" fi } -ensure_node() { - local node_major - node_major="$(get_node_major)" +node_runtime_supported() { + local version major minor + version="$(node --version 2>/dev/null || true)" + [[ "$version" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]] || return 1 + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + # OpenClaw 2026.9.9 npm engines: >=24.16.0 <25 || >=26.1.0. + (( (major == 24 && minor >= 16) || (major == 26 && minor >= 1) || major > 26 )) +} - if [[ "$node_major" -ge 22 ]]; then - log "Node.js $(node --version) already satisfies the >=22 requirement" +ensure_node() { + if node_runtime_supported; then + log "Node.js $(node --version) satisfies the OpenClaw runtime requirement" return fi - log "Installing Node.js 22 from NodeSource" + log "Installing Node.js 24 LTS from NodeSource" require_cmd curl command -v apt-get >/dev/null 2>&1 || fail "This script currently supports Ubuntu/Debian environments with apt-get" run_as_root apt-get update run_as_root apt-get install -y ca-certificates curl gnupg - curl -fsSL https://deb.nodesource.com/setup_22.x | run_as_root bash - + curl -fsSL https://deb.nodesource.com/setup_24.x | run_as_root bash - run_as_root apt-get install -y nodejs + node_runtime_supported || fail "Node.js $(node --version) does not satisfy >=24.16.0 <25 or >=26.1.0" + log "Installed Node.js $(node --version)" } @@ -328,10 +345,22 @@ install_code_server_extensions() { run_as_root -H -u "$TARGET_USER" env HOME="$TARGET_HOME" code-server --install-extension fabiospampinato.vscode-terminals --force >/dev/null } +configure_environment_prefix() { + local name value + # Only non-secret onboarding options belong in the generated terminal command. + # The code-server service does not inherit the one-shot bootstrap environment. + printf 'env ' + for name in OPENCLAW_TRUSTED_PROXY_IPS OPENCLAW_ORIGIN OPENCLAW_BASE_URL OPENCLAW_MODEL OPENCLAW_CONTEXT_WINDOW; do + value="${!name:-}" + [[ -n "$value" ]] || continue + printf '%s=%q ' "$name" "$value" + done +} + configure_code_server() { local config_dir settings_dir settings_user_dir workspaces_dir workspace_path home_workspace_path local terminals_target code_server_origin terminal_cmd run_once_marker wrapper_cmd - local home_readme + local home_readme configure_env config_dir="$TARGET_HOME/.config/code-server" settings_dir="$TARGET_HOME/.local/share/code-server" @@ -343,12 +372,13 @@ configure_code_server() { terminals_target="$TARGET_HOME/.vscode/terminals.json" code_server_origin="$(derive_code_server_origin)" run_once_marker="$TARGET_HOME/.cache/openclaw-launchable/configure-ran" + configure_env="$(configure_environment_prefix)" log "Configuring code-server" run_as_root -u "$TARGET_USER" mkdir -p "$config_dir" "$settings_user_dir" "$workspaces_dir" "$TARGET_HOME/.vscode" "$TARGET_HOME/.cache/openclaw-launchable" run_as_root -u "$TARGET_USER" install -m 644 "$LAUNCH_REPO_DIR/README.md" "$home_readme" - wrapper_cmd="mkdir -p \"${TARGET_HOME}/.cache/openclaw-launchable\" && if [[ -f \"${run_once_marker}\" ]]; then printf 'OpenClaw configure autorun already ran. Opening a fresh login shell.\\n\\n'; source ~/.profile >/dev/null 2>&1 || true; source ~/.bashrc >/dev/null 2>&1 || true; exec bash -l; fi; cd $(printf '%q' "$LAUNCH_REPO_DIR") && bash $(printf '%q' "$LAUNCH_REPO_DIR/configure.sh") && touch \"${run_once_marker}\"; source ~/.profile >/dev/null 2>&1 || true; source ~/.bashrc >/dev/null 2>&1 || true; exec bash -l" + wrapper_cmd="mkdir -p \"${TARGET_HOME}/.cache/openclaw-launchable\" && if [[ -f \"${run_once_marker}\" ]]; then printf 'OpenClaw configure autorun already ran. Opening a fresh login shell.\\n\\n'; source ~/.profile >/dev/null 2>&1 || true; source ~/.bashrc >/dev/null 2>&1 || true; exec bash -l; fi; cd $(printf '%q' "$LAUNCH_REPO_DIR") && ${configure_env}bash $(printf '%q' "$LAUNCH_REPO_DIR/configure.sh") && touch \"${run_once_marker}\"; source ~/.profile >/dev/null 2>&1 || true; source ~/.bashrc >/dev/null 2>&1 || true; exec bash -l" terminal_cmd="$wrapper_cmd" run_as_root -u "$TARGET_USER" tee "$terminals_target" >/dev/null <= 22" + log "Step 1/6: Ensuring a supported Node.js runtime" ensure_node log "Confirmed Node version: $(node --version)" @@ -478,12 +508,13 @@ main() { enable_code_server_service if is_openclaw_configured; then - log "Step 5/6: OpenClaw is already configured" + log "Step 5/6: Starting the configured OpenClaw gateway" + bash "$LAUNCH_REPO_DIR/configure.sh" else log "Step 5/6: OpenClaw onboarding is deferred to configure.sh" + log "Step 6/6: Printing code-server access information" + print_configuration_pending fi - log "Step 6/6: Printing code-server access information" - print_configuration_pending } main "$@" diff --git a/tests/bootstrap-regression.sh b/tests/bootstrap-regression.sh new file mode 100644 index 0000000..b871a40 --- /dev/null +++ b/tests/bootstrap-regression.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bash +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +passes=0 + +for option in OPENCLAW_TRUSTED_PROXY_IPS OPENCLAW_ORIGIN OPENCLAW_BASE_URL OPENCLAW_MODEL OPENCLAW_CONTEXT_WINDOW; do + ( + eval "$(sed '$d' "$ROOT/launch.sh")" + unset OPENCLAW_TRUSTED_PROXY_IPS OPENCLAW_ORIGIN OPENCLAW_BASE_URL OPENCLAW_MODEL OPENCLAW_CONTEXT_WINDOW + value='literal spaces; $(exit 7) "quotes"' + printf -v "$option" '%s' "$value" + export NVIDIA_API_KEY=qa-secret-never-persist CUSTOM_API_KEY=qa-secret-never-persist OPENCLAW_GATEWAY_TOKEN=qa-secret-never-persist + prefix="$(configure_environment_prefix)" + [[ "$prefix" != *qa-secret* && "$prefix" != *API_KEY* && "$prefix" != *GATEWAY_TOKEN* ]] + actual="$(env -u "$option" bash -c "${prefix}printenv $option")" + [[ "$actual" == "$value" ]] + ) + passes=$((passes + 1)) +done + +( + eval "$(sed '$d' "$ROOT/launch.sh")" + unset OPENCLAW_TRUSTED_PROXY_IPS OPENCLAW_ORIGIN OPENCLAW_BASE_URL OPENCLAW_MODEL OPENCLAW_CONTEXT_WINDOW + [[ "$(configure_environment_prefix)" == 'env ' ]] +) +passes=$((passes + 1)) + +for runtime in v22.23.3 v24.15.9 v24.16.0 v24.21.0 v24.16.0-rc.1 v25.0.0 v26.0.9 v26.1.0 v27.0.0 unavailable; do + ( + eval "$(sed '$d' "$ROOT/launch.sh")" + node() { printf '%s\n' "$runtime"; } + case "$runtime" in + v24.16.0|v24.21.0|v26.1.0|v27.0.0) node_runtime_supported ;; + *) ! node_runtime_supported ;; + esac + ) + passes=$((passes + 1)) +done + +( + unset OPENCLAW_VERSION OPENCLAW_RELEASE_TAG + eval "$(sed '$d' "$ROOT/launch.sh")" + [[ "$DEFAULT_OPENCLAW_VERSION" == 2026.9.9 && "$OPENCLAW_RELEASE_TAG" == v2026.9.9 ]] +) +passes=$((passes + 1)) + +for migration in valid repaired unrepairable; do + ( + eval "$(sed '$d' "$ROOT/configure.sh")" + fixture="$(mktemp -d)" + trap 'rm -rf "$fixture"' EXIT + HOME="$fixture" + mkdir -p "$HOME/.openclaw" + cp "$ROOT/README.md" "$HOME/.openclaw/openclaw.json" + validates=0 + repairs=0 + openclaw() { + if [[ "$1 $2" == 'config validate' ]]; then + validates=$((validates + 1)) + [[ "$migration" == valid || ( "$migration" == repaired && "$validates" -gt 1 ) ]] + else + [[ "$*" == 'doctor --fix --non-interactive' ]] + repairs=$((repairs + 1)) + fi + } + if [[ "$migration" == unrepairable ]]; then + if (ensure_config_compatible); then + exit 1 + fi + else + ensure_config_compatible + if [[ "$migration" == valid ]]; then + [[ "$repairs" -eq 0 ]] + else + [[ "$repairs" -eq 1 && "$validates" -eq 2 ]] + fi + fi + if [[ "$migration" != valid ]]; then + for backup in "$HOME"/.openclaw/openclaw.json.pre-migration.*; do + cmp -s "$backup" "$HOME/.openclaw/openclaw.json" + [[ "$(ls -l "$backup" | awk '{print substr($1,1,10)}')" == '-rw-------' ]] + done + fi + ) + passes=$((passes + 1)) +done + +for proxies in '["100.73.87.46"]' '["127.0.0.1","::1"]' '["0.0.0.0/0"]' '["100.64.0.0/10"]' '["*"]' '["0.0.0.0"]' '["::"]' '[]' 'not-json'; do + ( + eval "$(sed '$d' "$ROOT/configure.sh")" + OPENCLAW_TRUSTED_PROXY_IPS="$proxies" + openclaw() { + [[ "$1 $2 $3" == 'config set gateway.trustedProxies' && "$4" == "$proxies" && "$5" == --strict-json ]] + } + case "$proxies" in + '["100.73.87.46"]'|'["127.0.0.1","::1"]') configure_trusted_proxies ;; + *) if (configure_trusted_proxies >/dev/null 2>&1); then exit 1; fi ;; + esac + ) + passes=$((passes + 1)) +done + +for credential in literal reference missing; do + ( + eval "$(sed '$d' "$ROOT/configure.sh")" + fixture="$(mktemp -d)" + trap 'rm -rf "$fixture"' EXIT + HOME="$fixture" + unset OPENCLAW_GATEWAY_TOKEN + mkdir -p "$HOME/.openclaw" + case "$credential" in + literal) printf '%s' '{"gateway":{"auth":{"token":"qa-fake-token"}}}' > "$HOME/.openclaw/openclaw.json" ;; + reference) printf '%s' '{"gateway":{"auth":{"token":{"source":"store","provider":"default","id":"gateway-token"}}}}' > "$HOME/.openclaw/openclaw.json" ;; + missing) printf '%s' '{"gateway":{"auth":{}}}' > "$HOME/.openclaw/openclaw.json" ;; + esac + if [[ "$credential" == missing ]]; then + ! gateway_token_is_configured + else + gateway_token_is_configured + fi + if [[ "$credential" == literal ]]; then + [[ "$(get_gateway_token_from_config_file)" == qa-fake-token ]] + else + ! get_gateway_token_from_config_file + fi + ) + passes=$((passes + 1)) +done + +( + eval "$(sed '$d' "$ROOT/configure.sh")" + hostname() { printf 'brev-u1x29ihj3\n'; } + openclaw() { + [[ "$*" == 'dashboard --no-open --json' ]] + printf '%s' '{"ok":true,"browserUrl":"http://127.0.0.1:18789/#bootstrapToken=qa-fake-bootstrap&bootstrapProfile=main&gatewayUrl=ws%3A%2F%2F127.0.0.1%3A18789"}' + } + url="$(get_dashboard_connection_url)" + node -e ' + const url=new URL(process.argv[1]); const params=new URLSearchParams(url.hash.slice(1)); + if(url.origin!=="https://openclaw-u1x29ihj3.gobrev.dev" || params.get("bootstrapToken")!=="qa-fake-bootstrap" || params.get("gatewayUrl")!=="wss://openclaw-u1x29ihj3.gobrev.dev") process.exit(1); + ' "$url" +) +passes=$((passes + 1)) + +check_origin() ( + local script="$1" function="$2" host="$3" expected="$4" override="${5:-}" + # Load function definitions without invoking the script's final main call. + eval "$(sed '$d' "$ROOT/$script")" + hostname() { printf '%s\n' "$host"; } + unset OPENCLAW_ORIGIN CODE_SERVER_ORIGIN + if [[ "$function" == derive_openclaw_origin ]]; then + OPENCLAW_ORIGIN="$override" + else + CODE_SERVER_ORIGIN="$override" + fi + [[ "$("$function")" == "$expected" ]] +) + +for script in launch.sh configure.sh; do + check_origin "$script" derive_openclaw_origin brev-u1x29ihj3 https://openclaw-u1x29ihj3.gobrev.dev + check_origin "$script" derive_openclaw_origin local-dev http://localhost:18789 + check_origin "$script" derive_openclaw_origin brev-u1x29ihj3 https://custom.example https://custom.example/ + passes=$((passes + 3)) +done +check_origin launch.sh derive_code_server_origin brev-u1x29ihj3 https://code-server-u1x29ihj3.gobrev.dev +check_origin launch.sh derive_code_server_origin local-dev http://localhost:13337 +check_origin launch.sh derive_code_server_origin brev-u1x29ihj3 https://editor.example https://editor.example/ +passes=$((passes + 3)) + +( + eval "$(sed '$d' "$ROOT/configure.sh")" + hostname() { printf 'brev-u1x29ihj3\n'; } + openclaw() { + if [[ "$3" == gateway.bind ]]; then + [[ "$4" == lan ]] + else + [[ "$3" == gateway.controlUi.allowedOrigins && "$5" == --strict-json ]] + node -e ' + const origins=JSON.parse(process.argv[1]); + for(const origin of ["https://openclaw-u1x29ihj3.gobrev.dev", "https://openclaw-u1x29ihj3.apps.run.brev.nvidia.com", "https://openclaw-u1x29ihj3.stg.apps.launchpad.nvidia.com"]) { + if(!origins.includes(origin)) process.exit(1); + } + if(origins.includes("*")) process.exit(1); + ' "$4" + fi + } + configure_control_ui_origin +) +passes=$((passes + 1)) + +for configured in yes no; do + ( + eval "$(sed '$d' "$ROOT/launch.sh")" + calls="" + for function in require_non_root require_cmd ensure_node ensure_openclaw_installed verify_openclaw_cli clone_or_refresh_launch_repo install_code_server install_code_server_extensions configure_code_server enable_code_server_service; do + eval "$function() { :; }" + done + node() { printf 'v24.21.0\n'; } + is_openclaw_configured() { [[ "$configured" == yes ]]; } + bash() { calls="${calls}gateway "; [[ "$1" == "$LAUNCH_REPO_DIR/configure.sh" ]]; } + print_configuration_pending() { calls="${calls}pending "; } + main + if [[ "$configured" == yes ]]; then + [[ "$calls" == 'gateway ' ]] + else + [[ "$calls" == 'pending ' ]] + fi + ) + passes=$((passes + 1)) +done + +printf 'PASS: %s bootstrap regression cases\n' "$passes"