From 41d4ea692ac514b351b2bd6d2ba6ef47d451976f Mon Sep 17 00:00:00 2001 From: nick_h Date: Thu, 26 Mar 2026 14:59:11 -0400 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20improve=20install=20UX=20=E2=80=94?= =?UTF-8?q?=20Docker=20permission=20detection=20+=20headless=20DNS=20suppo?= =?UTF-8?q?rt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/dns/resolver.go | 16 ++++------------ internal/stack/stack.go | 13 ++++++++++--- obolup.sh | 33 +++++++++++++++++---------------- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/internal/dns/resolver.go b/internal/dns/resolver.go index ea69a730..3e3445ab 100644 --- a/internal/dns/resolver.go +++ b/internal/dns/resolver.go @@ -317,19 +317,15 @@ func removeMacOSResolver() { // --- Linux (NetworkManager dnsmasq plugin) --- // configureLinuxResolver sets up NM's dnsmasq plugin for *.obol.stack. +// Returns a non-fatal error when NetworkManager is unavailable (e.g. headless +// servers) — the caller falls back to /etc/hosts entries. func configureLinuxResolver() error { if configureNMDnsmasq() { return nil } - // NM not available — print instructions - fmt.Println("\nWildcard DNS for *.obol.stack requires NetworkManager with dnsmasq.") - fmt.Println("Install with:") - fmt.Println(" sudo apt install network-manager dnsmasq-base # Debian/Ubuntu") - fmt.Println(" sudo dnf install NetworkManager dnsmasq # Fedora/RHEL") - fmt.Println(" sudo pacman -S networkmanager dnsmasq # Arch") - fmt.Println("\nThen re-run: obol stack up") - return fmt.Errorf("NetworkManager required for wildcard DNS on Linux") + // NM not available — return quiet error; caller handles the fallback message. + return fmt.Errorf("NetworkManager not available (headless or server system)") } // hasNMDnsmasqConfig checks if the NM dnsmasq config for obol.stack exists. @@ -359,10 +355,6 @@ func configureNMDnsmasq() bool { // Check if dnsmasq binary is available (NM plugin requires it) if _, err := exec.LookPath("dnsmasq"); err != nil { - fmt.Println("Note: dnsmasq not found. Install it for wildcard DNS support:") - fmt.Println(" sudo apt install dnsmasq-base # Debian/Ubuntu") - fmt.Println(" sudo dnf install dnsmasq # Fedora/RHEL") - fmt.Println(" sudo pacman -S dnsmasq # Arch") return false } diff --git a/internal/stack/stack.go b/internal/stack/stack.go index b7e22101..e582f94c 100644 --- a/internal/stack/stack.go +++ b/internal/stack/stack.go @@ -285,13 +285,20 @@ func Up(cfg *config.Config, u *ui.UI) error { return err } - // Ensure DNS resolver is running for wildcard *.obol.stack + // Ensure *.obol.stack resolves to localhost. + // /etc/hosts is the primary mechanism (works everywhere, including headless). + // Wildcard DNS via NM dnsmasq (Linux) or /etc/resolver (macOS) is optional + // and only attempted as an enhancement for subdomain resolution. + if err := dns.EnsureHostsEntries(nil); err != nil { + u.Warnf("Could not update /etc/hosts for obol.stack: %v", err) + } if err := dns.EnsureRunning(); err != nil { u.Warnf("DNS resolver failed to start: %v", err) } else if err := dns.ConfigureSystemResolver(); err != nil { - u.Warnf("Failed to configure system DNS resolver: %v", err) + u.Dim(fmt.Sprintf(" Wildcard DNS not configured: %v", err)) + u.Dim(" obol.stack will resolve via /etc/hosts (subdomains added as needed)") } else { - u.Success("DNS resolver configured") + u.Success("DNS resolver configured (wildcard *.obol.stack)") } u.Blank() diff --git a/obolup.sh b/obolup.sh index dc148231..9989e388 100755 --- a/obolup.sh +++ b/obolup.sh @@ -123,8 +123,24 @@ check_docker() { return 1 fi - # Check if Docker daemon is running; try to start it automatically + # Check if Docker daemon is running and accessible if ! docker info >/dev/null 2>&1; then + # Distinguish permission errors from daemon-not-running + local docker_err + docker_err=$(docker info 2>&1) + + if echo "$docker_err" | grep -qi "permission denied"; then + log_error "Docker is installed but your user does not have permission to access it" + echo "" + echo " Add your user to the docker group and apply the change:" + echo " sudo usermod -aG docker \$USER" + echo " newgrp docker" + echo "" + log_dim " Then run this installer again." + echo "" + return 1 + fi + if [[ "$(uname -s)" == "Linux" ]]; then log_warn "Docker daemon is not running — attempting to start..." # Try systemd first (apt/yum installs), then snap @@ -183,21 +199,6 @@ check_docker() { log_warn "k3d may not work correctly with older Docker versions" fi - # Check if user can run Docker without sudo (Linux-specific) - if [[ "$(uname -s)" == "Linux" ]]; then - if ! docker ps >/dev/null 2>&1; then - log_warn "Current user cannot run Docker commands" - echo "" - echo "You may need to add your user to the docker group:" - echo " sudo usermod -aG docker \$USER" - echo " newgrp docker" - echo "" - echo "Or run commands with sudo." - echo "" - # Don't fail here - user might be running with sudo - fi - fi - # Check Docker networking (ensure bridge network works) if ! docker network ls >/dev/null 2>&1; then log_error "Docker networking is not functional" From 3cb7c4f0af8ff3da0a8fb2e767627978e7d22cd2 Mon Sep 17 00:00:00 2001 From: nick_h Date: Thu, 26 Mar 2026 15:04:46 -0400 Subject: [PATCH 2/7] fix: export OBOL_BIN_DIR to PATH during install so helm is found by bootstrap --- obolup.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/obolup.sh b/obolup.sh index 9989e388..5eb72ccf 100755 --- a/obolup.sh +++ b/obolup.sh @@ -1242,6 +1242,13 @@ install_dependencies() { log_info "Checking and installing dependencies..." echo "" + # Ensure OBOL_BIN_DIR is in PATH for the current process so that + # binaries installed here (helm, helmfile, etc.) are immediately + # available to each other and to later steps like `obol bootstrap`. + if ! echo "$PATH" | grep -q "$OBOL_BIN_DIR"; then + export PATH="$OBOL_BIN_DIR:$PATH" + fi + # Install each dependency install_kubectl || log_warn "kubectl installation failed (continuing...)" install_helm || log_warn "helm installation failed (continuing...)" @@ -1434,20 +1441,13 @@ add_to_profile() { # In interactive mode, asks user whether to auto-modify or show manual instructions. # In non-interactive mode (CI/CD), prints manual instructions only unless OBOL_MODIFY_PATH=yes. configure_path() { - # Check if OBOL_BIN_DIR is already in current PATH - if echo "$PATH" | grep -q "$OBOL_BIN_DIR"; then - log_success "OBOL_BIN_DIR already in PATH" - return 0 - fi - # Detect appropriate profile file local profile profile=$(detect_shell_profile) - # Check if already configured in detected profile + # Check if already persisted in the shell profile if [[ -f "$profile" ]] && grep -qF "$OBOL_BIN_DIR" "$profile" 2>/dev/null; then log_success "OBOL_BIN_DIR already configured in $profile" - log_info "Will be available in new shell sessions" return 0 fi From 0803c39356885725933308cd38d9595d048d972b Mon Sep 17 00:00:00 2001 From: nick_h Date: Thu, 26 Mar 2026 15:12:11 -0400 Subject: [PATCH 3/7] fix: check all prerequisites upfront before starting installation --- obolup.sh | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/obolup.sh b/obolup.sh index 5eb72ccf..a38d3d97 100755 --- a/obolup.sh +++ b/obolup.sh @@ -96,6 +96,49 @@ command_exists() { command -v "$1" >/dev/null 2>&1 } +# Check host prerequisites that the installer cannot provide itself. +# Binaries we download (helm, kubectl, k3d, etc.) are not checked here — +# only tools the user must install separately. +check_prerequisites() { + local missing=() + + # Node.js 22+ / npm — required for openclaw CLI (unless already installed) + local need_npm=true + if command_exists openclaw; then + local oc_version + oc_version=$(openclaw --version 2>/dev/null | tr -d '[:space:]' || echo "") + if [[ -n "$oc_version" ]] && version_ge "$oc_version" "$OPENCLAW_VERSION"; then + need_npm=false + fi + fi + + if [[ "$need_npm" == "true" ]]; then + if ! command_exists npm; then + missing+=("Node.js 22+ (npm) — required to install openclaw CLI") + else + local node_major + node_major=$(node --version 2>/dev/null | sed 's/v//' | cut -d. -f1) + if [[ -z "$node_major" ]] || [[ "$node_major" -lt 22 ]]; then + missing+=("Node.js 22+ (found: v${node_major:-none}) — required for openclaw CLI") + fi + fi + fi + + if [[ ${#missing[@]} -gt 0 ]]; then + log_error "Missing prerequisites:" + echo "" + for dep in "${missing[@]}"; do + echo " • $dep" + done + echo "" + log_dim " Install the above, then re-run obolup.sh" + echo "" + return 1 + fi + + return 0 +} + # Detect installation mode (install vs upgrade) detect_installation_mode() { if [[ -f "$OBOL_BIN_DIR/obol" ]]; then @@ -1691,6 +1734,11 @@ main() { fi echo "" + # Check host prerequisites (npm/Node.js, etc.) before starting installs + if ! check_prerequisites; then + exit 1 + fi + create_directories install_obol_binary copy_bootstrap_script From f4be5269197bae6d097ea0377a65068e02143c05 Mon Sep 17 00:00:00 2001 From: nick_h Date: Thu, 26 Mar 2026 15:46:09 -0400 Subject: [PATCH 4/7] fix: update openclaw version to 2026.3.24 (v2026.3.13-1 missing from npm) --- internal/openclaw/OPENCLAW_VERSION | 2 +- obolup.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/openclaw/OPENCLAW_VERSION b/internal/openclaw/OPENCLAW_VERSION index a4a00969..fb85ceb0 100644 --- a/internal/openclaw/OPENCLAW_VERSION +++ b/internal/openclaw/OPENCLAW_VERSION @@ -1,3 +1,3 @@ # renovate: datasource=github-releases depName=openclaw/openclaw # Pins the upstream OpenClaw version to build and publish. -v2026.3.13-1 +v2026.3.24 diff --git a/obolup.sh b/obolup.sh index a38d3d97..7d3abf13 100755 --- a/obolup.sh +++ b/obolup.sh @@ -62,7 +62,7 @@ readonly K9S_VERSION="0.50.18" readonly HELM_DIFF_VERSION="3.14.1" # Must match internal/openclaw/OPENCLAW_VERSION (without "v" prefix). # Tested by TestOpenClawVersionConsistency. -readonly OPENCLAW_VERSION="2026.3.13-1" +readonly OPENCLAW_VERSION="2026.3.24" # Repository URL for building from source readonly OBOL_REPO_URL="git@github.com:ObolNetwork/obol-stack.git" From 31141984b09b336adc81b0aec0b822e7408fa4a8 Mon Sep 17 00:00:00 2001 From: nick_h Date: Thu, 26 Mar 2026 15:55:36 -0400 Subject: [PATCH 5/7] fix: make wildcard DNS opt-in to prevent breaking host DNS resolution --- cmd/obol/bootstrap.go | 2 +- cmd/obol/main.go | 8 +++++++- internal/stack/stack.go | 26 ++++++++++++++------------ 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/cmd/obol/bootstrap.go b/cmd/obol/bootstrap.go index d74a6039..a8cc03ef 100644 --- a/cmd/obol/bootstrap.go +++ b/cmd/obol/bootstrap.go @@ -38,7 +38,7 @@ func bootstrapCommand(cfg *config.Config) *cli.Command { } // Step 2: Start stack - if err := stack.Up(cfg, u); err != nil { + if err := stack.Up(cfg, u, false); err != nil { return fmt.Errorf("bootstrap up failed: %w", err) } diff --git a/cmd/obol/main.go b/cmd/obol/main.go index e8d80385..3a4d9e25 100644 --- a/cmd/obol/main.go +++ b/cmd/obol/main.go @@ -159,8 +159,14 @@ GLOBAL OPTIONS:{{template "visibleFlagTemplate" .}}{{end}} { Name: "up", Usage: "Start the Obol Stack", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "wildcard-dns", + Usage: "Configure wildcard *.obol.stack DNS via NetworkManager/dnsmasq (Linux) or /etc/resolver (macOS)", + }, + }, Action: func(ctx context.Context, cmd *cli.Command) error { - return stack.Up(cfg, getUI(cmd)) + return stack.Up(cfg, getUI(cmd), cmd.Bool("wildcard-dns")) }, }, { diff --git a/internal/stack/stack.go b/internal/stack/stack.go index e582f94c..f9a8dc80 100644 --- a/internal/stack/stack.go +++ b/internal/stack/stack.go @@ -254,7 +254,7 @@ func dockerBridgeGatewayIP() (string, error) { } // Up starts the cluster using the configured backend -func Up(cfg *config.Config, u *ui.UI) error { +func Up(cfg *config.Config, u *ui.UI, wildcardDNS bool) error { stackID := getStackID(cfg) if stackID == "" { return fmt.Errorf("stack ID not found, run 'obol stack init' first") @@ -285,20 +285,22 @@ func Up(cfg *config.Config, u *ui.UI) error { return err } - // Ensure *.obol.stack resolves to localhost. - // /etc/hosts is the primary mechanism (works everywhere, including headless). - // Wildcard DNS via NM dnsmasq (Linux) or /etc/resolver (macOS) is optional - // and only attempted as an enhancement for subdomain resolution. + // Ensure obol.stack resolves to localhost via /etc/hosts (works everywhere). if err := dns.EnsureHostsEntries(nil); err != nil { u.Warnf("Could not update /etc/hosts for obol.stack: %v", err) } - if err := dns.EnsureRunning(); err != nil { - u.Warnf("DNS resolver failed to start: %v", err) - } else if err := dns.ConfigureSystemResolver(); err != nil { - u.Dim(fmt.Sprintf(" Wildcard DNS not configured: %v", err)) - u.Dim(" obol.stack will resolve via /etc/hosts (subdomains added as needed)") - } else { - u.Success("DNS resolver configured (wildcard *.obol.stack)") + + // Wildcard *.obol.stack DNS is opt-in (--wildcard-dns) because it + // modifies system DNS config (NetworkManager/resolv.conf on Linux, + // /etc/resolver on macOS) which can break host DNS resolution. + if wildcardDNS { + if err := dns.EnsureRunning(); err != nil { + u.Warnf("DNS resolver failed to start: %v", err) + } else if err := dns.ConfigureSystemResolver(); err != nil { + u.Warnf("Wildcard DNS configuration failed: %v", err) + } else { + u.Success("Wildcard DNS configured (*.obol.stack)") + } } u.Blank() From 9dd61eda5c8dc98528d4d06911c73e0b5ddbb12f Mon Sep 17 00:00:00 2001 From: nick_h Date: Thu, 26 Mar 2026 16:24:05 -0400 Subject: [PATCH 6/7] fix: fresh installs never got prompted for an API key. --- obolup.sh | 124 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 93 insertions(+), 31 deletions(-) diff --git a/obolup.sh b/obolup.sh index 7d3abf13..a887744f 100755 --- a/obolup.sh +++ b/obolup.sh @@ -1545,11 +1545,10 @@ configure_path() { # it so the subsequent obol bootstrap / stack up picks it up via autoConfigureLLM. check_agent_model_api_key() { local config_file="$HOME/.openclaw/openclaw.json" - [[ -f "$config_file" ]] || return 0 - # Extract agents.defaults.model.primary (e.g., "anthropic/claude-sonnet-4-6") + # Try to detect the model from an existing openclaw.json config. local primary_model="" - if command_exists python3; then + if [[ -f "$config_file" ]] && command_exists python3; then primary_model=$(python3 -c " import json, sys try: @@ -1559,47 +1558,110 @@ except: pass " 2>/dev/null) fi - [[ -n "$primary_model" ]] || return 0 - - # Determine provider and required env var + # Determine provider from config, or check for any pre-existing API keys + # in the environment even on fresh installs. local provider="" env_var="" provider_name="" - case "$primary_model" in - *claude*) provider="anthropic"; env_var="ANTHROPIC_API_KEY"; provider_name="Anthropic" ;; - gpt*|o1*|o3*|o4*) provider="openai"; env_var="OPENAI_API_KEY"; provider_name="OpenAI" ;; - *) return 0 ;; - esac + if [[ -n "$primary_model" ]]; then + case "$primary_model" in + *claude*) provider="anthropic"; env_var="ANTHROPIC_API_KEY"; provider_name="Anthropic" ;; + gpt*|o1*|o3*|o4*) provider="openai"; env_var="OPENAI_API_KEY"; provider_name="OpenAI" ;; + esac + fi - echo "" - if [[ -n "${!env_var:-}" ]]; then - log_success "$env_var detected for $primary_model" + # If we identified a provider from config, check if the key is already set. + if [[ -n "$provider" ]]; then + echo "" + if [[ -n "${!env_var:-}" ]]; then + log_success "$env_var detected for $primary_model" + return 0 + fi + + # Anthropic-specific fallback: Claude Code subscription token + if [[ "$provider" == "anthropic" && -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then + export ANTHROPIC_API_KEY="$CLAUDE_CODE_OAUTH_TOKEN" + log_success "Claude Code subscription detected (CLAUDE_CODE_OAUTH_TOKEN)" + return 0 + fi + + # Interactive: prompt for the specific key + if [[ -c /dev/tty ]]; then + log_info "Your agent uses $primary_model ($provider_name)" + echo "" + local api_key="" + read -r -p " $provider_name API key ($env_var): " api_key Date: Thu, 26 Mar 2026 16:36:50 -0400 Subject: [PATCH 7/7] Revert "fix: fresh installs never got prompted for an API key." This reverts commit 9dd61eda5c8dc98528d4d06911c73e0b5ddbb12f. --- obolup.sh | 124 ++++++++++++++---------------------------------------- 1 file changed, 31 insertions(+), 93 deletions(-) diff --git a/obolup.sh b/obolup.sh index a887744f..7d3abf13 100755 --- a/obolup.sh +++ b/obolup.sh @@ -1545,10 +1545,11 @@ configure_path() { # it so the subsequent obol bootstrap / stack up picks it up via autoConfigureLLM. check_agent_model_api_key() { local config_file="$HOME/.openclaw/openclaw.json" + [[ -f "$config_file" ]] || return 0 - # Try to detect the model from an existing openclaw.json config. + # Extract agents.defaults.model.primary (e.g., "anthropic/claude-sonnet-4-6") local primary_model="" - if [[ -f "$config_file" ]] && command_exists python3; then + if command_exists python3; then primary_model=$(python3 -c " import json, sys try: @@ -1558,110 +1559,47 @@ except: pass " 2>/dev/null) fi - # Determine provider from config, or check for any pre-existing API keys - # in the environment even on fresh installs. - local provider="" env_var="" provider_name="" - if [[ -n "$primary_model" ]]; then - case "$primary_model" in - *claude*) provider="anthropic"; env_var="ANTHROPIC_API_KEY"; provider_name="Anthropic" ;; - gpt*|o1*|o3*|o4*) provider="openai"; env_var="OPENAI_API_KEY"; provider_name="OpenAI" ;; - esac - fi + [[ -n "$primary_model" ]] || return 0 - # If we identified a provider from config, check if the key is already set. - if [[ -n "$provider" ]]; then - echo "" - if [[ -n "${!env_var:-}" ]]; then - log_success "$env_var detected for $primary_model" - return 0 - fi - - # Anthropic-specific fallback: Claude Code subscription token - if [[ "$provider" == "anthropic" && -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then - export ANTHROPIC_API_KEY="$CLAUDE_CODE_OAUTH_TOKEN" - log_success "Claude Code subscription detected (CLAUDE_CODE_OAUTH_TOKEN)" - return 0 - fi + # Determine provider and required env var + local provider="" env_var="" provider_name="" + case "$primary_model" in + *claude*) provider="anthropic"; env_var="ANTHROPIC_API_KEY"; provider_name="Anthropic" ;; + gpt*|o1*|o3*|o4*) provider="openai"; env_var="OPENAI_API_KEY"; provider_name="OpenAI" ;; + *) return 0 ;; + esac - # Interactive: prompt for the specific key - if [[ -c /dev/tty ]]; then - log_info "Your agent uses $primary_model ($provider_name)" - echo "" - local api_key="" - read -r -p " $provider_name API key ($env_var): " api_key