Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/bootstrap-tests.yml
Original file line number Diff line number Diff line change
@@ -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
41 changes: 27 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -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.
Expand All @@ -98,23 +98,36 @@ The launchable is split into two stages:
On hosts named like `brev-<env_id>`, the launchable derives:

```text
OpenClaw: https://openclaw-<env_id>.apps.run.brev.nvidia.com/chat?session=main
code-server: https://code-server0-<env_id>.apps.run.brev.nvidia.com
OpenClaw: https://openclaw-<env_id>.gobrev.dev/chat?session=main
code-server: https://code-server-<env_id>.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 <instance> --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.
Expand All @@ -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`.
Expand All @@ -160,7 +173,7 @@ OpenClaw Gateway Started
========================

URL:
https://openclaw-<env_id>.apps.run.brev.nvidia.com/chat?session=main
https://openclaw-<env_id>.gobrev.dev/chat?session=main

API Token:
<token>
Expand All @@ -169,10 +182,10 @@ Hostname:
brev-<env_id>

Origin:
https://openclaw-<env_id>.apps.run.brev.nvidia.com
https://openclaw-<env_id>.gobrev.dev

code-server:
https://code-server0-<env_id>.apps.run.brev.nvidia.com
https://code-server-<env_id>.gobrev.dev
```

### Logs and State
Expand Down
98 changes: 87 additions & 11 deletions configure.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -115,14 +115,51 @@ 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

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"
Expand Down Expand Up @@ -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)"
Expand All @@ -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 <<EOF
["http://127.0.0.1:18789","http://localhost:18789","${origin}","${legacy_origin}"]
["http://127.0.0.1:18789","http://localhost:18789","${origin}","${previous_origin}","${legacy_origin}"]
EOF
)"
else
Expand All @@ -257,7 +299,9 @@ print_gateway_info() {

host_name="$(hostname 2>/dev/null || true)"
origin="$(derive_openclaw_origin)"
if [[ -n "$token" ]]; then
if url="$(get_dashboard_connection_url)"; then
:
elif [[ -n "$token" ]]; then
url="${origin}/#token=${token}"
else
url="${origin}/chat?session=main"
Expand All @@ -266,7 +310,7 @@ print_gateway_info() {
printf '\nOpenClaw Gateway Started\n'
printf '========================\n\n'
printf 'URL:\n%s\n\n' "$url"
printf 'API Token:\n%s\n\n' "${token:-Unavailable - review $OPENCLAW_STATE_DIR/gateway.log}"
printf 'API Token:\n%s\n\n' "${token:-Stored as a SecretRef; use the dashboard URL above. Generate a new dashboard link if it expires.}"
printf 'Hostname:\n%s\n\n' "${host_name:-unknown}"
printf 'Origin:\n%s\n' "$origin"
}
Expand Down Expand Up @@ -342,6 +386,36 @@ set_model_context_window() {
mv "$tmp_file" "$config_file"
}

ensure_config_compatible() {
local config_file backup_file
if openclaw config validate >/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

Expand Down Expand Up @@ -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"
Expand Down
Loading
Loading