|
| 1 | +#!/bin/bash |
| 2 | +# Baudbot Config — interactive secrets and configuration setup. |
| 3 | +# Writes to ~/.baudbot/.env (admin-owned). Deploy copies to agent runtime. |
| 4 | +# |
| 5 | +# Usage: baudbot config |
| 6 | +# sudo baudbot config (when run via install.sh) |
| 7 | +# |
| 8 | +# Can be re-run to update existing config. Existing values shown as defaults. |
| 9 | + |
| 10 | +set -euo pipefail |
| 11 | + |
| 12 | +# ── Formatting ─────────────────────────────────────────────────────────────── |
| 13 | + |
| 14 | +BOLD='\033[1m' |
| 15 | +DIM='\033[2m' |
| 16 | +GREEN='\033[0;32m' |
| 17 | +YELLOW='\033[0;33m' |
| 18 | +RED='\033[0;31m' |
| 19 | +CYAN='\033[0;36m' |
| 20 | +RESET='\033[0m' |
| 21 | + |
| 22 | +info() { echo -e "${BOLD}${GREEN}▸${RESET} $1"; } |
| 23 | +warn() { echo -e "${BOLD}${YELLOW}▸${RESET} $1"; } |
| 24 | +ask() { echo -en "${BOLD}${CYAN}?${RESET} $1"; } |
| 25 | +dim() { echo -e "${DIM}$1${RESET}"; } |
| 26 | + |
| 27 | +# ── Determine config directory ─────────────────────────────────────────────── |
| 28 | + |
| 29 | +# If run as root via sudo, write to the admin user's ~/.baudbot/ |
| 30 | +# If run as a normal user, write to their own ~/.baudbot/ |
| 31 | +# BAUDBOT_CONFIG_USER env var overrides detection (used by install.sh) |
| 32 | +if [ -n "${BAUDBOT_CONFIG_USER:-}" ]; then |
| 33 | + CONFIG_USER="$BAUDBOT_CONFIG_USER" |
| 34 | + CONFIG_HOME=$(getent passwd "$CONFIG_USER" | cut -d: -f6) |
| 35 | +elif [ "$(id -u)" -eq 0 ]; then |
| 36 | + CONFIG_USER="${SUDO_USER:-root}" |
| 37 | + if [ "$CONFIG_USER" = "root" ]; then |
| 38 | + echo "Run as: sudo baudbot config (not as root directly)" |
| 39 | + exit 1 |
| 40 | + fi |
| 41 | + CONFIG_HOME=$(getent passwd "$CONFIG_USER" | cut -d: -f6) |
| 42 | +else |
| 43 | + CONFIG_USER="$(whoami)" |
| 44 | + CONFIG_HOME="$HOME" |
| 45 | +fi |
| 46 | + |
| 47 | +CONFIG_DIR="$CONFIG_HOME/.baudbot" |
| 48 | +CONFIG_FILE="$CONFIG_DIR/.env" |
| 49 | + |
| 50 | +mkdir -p "$CONFIG_DIR" |
| 51 | +# Ensure owned by the admin user (not root) |
| 52 | +if [ "$(id -u)" -eq 0 ]; then |
| 53 | + chown "$CONFIG_USER:$CONFIG_USER" "$CONFIG_DIR" |
| 54 | +fi |
| 55 | + |
| 56 | +# ── Load existing config ───────────────────────────────────────────────────── |
| 57 | + |
| 58 | +declare -A ENV_VARS |
| 59 | +declare -A EXISTING |
| 60 | + |
| 61 | +if [ -f "$CONFIG_FILE" ]; then |
| 62 | + while IFS='=' read -r key value; do |
| 63 | + # Skip comments and empty lines |
| 64 | + [[ "$key" =~ ^#.*$ ]] && continue |
| 65 | + [ -z "$key" ] && continue |
| 66 | + EXISTING[$key]="$value" |
| 67 | + done < "$CONFIG_FILE" |
| 68 | +fi |
| 69 | + |
| 70 | +# ── Prompting ──────────────────────────────────────────────────────────────── |
| 71 | + |
| 72 | +# prompt_secret KEY "description" "url" [required] [prefix] |
| 73 | +# If an existing value is set, shows [****] and allows Enter to keep it. |
| 74 | +prompt_secret() { |
| 75 | + local key="$1" desc="$2" url="${3:-}" required="${4:-}" prefix="${5:-}" |
| 76 | + local label="" existing="${EXISTING[$key]:-}" |
| 77 | + |
| 78 | + if [ "$required" = "required" ]; then |
| 79 | + label="${RED}*${RESET} " |
| 80 | + fi |
| 81 | + |
| 82 | + if [ -n "$url" ]; then |
| 83 | + dim " $url" |
| 84 | + fi |
| 85 | + |
| 86 | + if [ -n "$existing" ]; then |
| 87 | + # Show masked existing value |
| 88 | + local masked="${existing:0:4}****" |
| 89 | + ask "${label}${desc} [${masked}]: " |
| 90 | + else |
| 91 | + ask "${label}${desc}: " |
| 92 | + fi |
| 93 | + read -r value |
| 94 | + |
| 95 | + # Empty input with existing value = keep existing |
| 96 | + if [ -z "$value" ] && [ -n "$existing" ]; then |
| 97 | + ENV_VARS[$key]="$existing" |
| 98 | + return |
| 99 | + fi |
| 100 | + |
| 101 | + # Validate prefix if provided |
| 102 | + if [ -n "$value" ] && [ -n "$prefix" ]; then |
| 103 | + local match=false |
| 104 | + IFS='|' read -ra prefixes <<< "$prefix" |
| 105 | + for p in "${prefixes[@]}"; do |
| 106 | + if [[ "$value" == "$p"* ]]; then |
| 107 | + match=true |
| 108 | + break |
| 109 | + fi |
| 110 | + done |
| 111 | + if [ "$match" = false ]; then |
| 112 | + warn "Expected prefix '${prefix}' — saved anyway" |
| 113 | + fi |
| 114 | + fi |
| 115 | + |
| 116 | + # Warn if required and empty |
| 117 | + if [ -z "$value" ] && [ "$required" = "required" ]; then |
| 118 | + warn "Skipped (required — agent won't fully work without this)" |
| 119 | + fi |
| 120 | + |
| 121 | + if [ -n "$value" ]; then |
| 122 | + ENV_VARS[$key]="$value" |
| 123 | + fi |
| 124 | +} |
| 125 | + |
| 126 | +# ── Collect secrets ────────────────────────────────────────────────────────── |
| 127 | + |
| 128 | +echo "" |
| 129 | +if [ -f "$CONFIG_FILE" ]; then |
| 130 | + echo -e "Updating config in ${BOLD}$CONFIG_FILE${RESET}" |
| 131 | + echo -e "Press ${BOLD}Enter${RESET} to keep existing values." |
| 132 | +else |
| 133 | + echo -e "Baudbot needs API keys to talk to services." |
| 134 | + echo -e "Press ${BOLD}Enter${RESET} to skip optional values." |
| 135 | +fi |
| 136 | +echo -e " ${DIM}$CONFIG_FILE${RESET}" |
| 137 | +echo "" |
| 138 | + |
| 139 | +# -- Required -- |
| 140 | +echo -e "${BOLD}Required${RESET} ${DIM}(agent won't start without these)${RESET}" |
| 141 | +echo "" |
| 142 | + |
| 143 | +echo -e "${BOLD}LLM provider${RESET} ${DIM}(set at least one)${RESET}" |
| 144 | +echo "" |
| 145 | + |
| 146 | +prompt_secret "ANTHROPIC_API_KEY" \ |
| 147 | + "Anthropic API key" \ |
| 148 | + "https://console.anthropic.com/settings/keys" \ |
| 149 | + "" \ |
| 150 | + "sk-ant-" |
| 151 | + |
| 152 | +prompt_secret "OPENAI_API_KEY" \ |
| 153 | + "OpenAI API key" \ |
| 154 | + "https://platform.openai.com/api-keys" \ |
| 155 | + "" \ |
| 156 | + "sk-" |
| 157 | + |
| 158 | +prompt_secret "GEMINI_API_KEY" \ |
| 159 | + "Google Gemini API key" \ |
| 160 | + "https://aistudio.google.com/apikey" |
| 161 | + |
| 162 | +prompt_secret "OPENCODE_ZEN_API_KEY" \ |
| 163 | + "OpenCode Zen API key (multi-provider router)" \ |
| 164 | + "https://opencode.ai" |
| 165 | + |
| 166 | +HAS_LLM_KEY=false |
| 167 | +for k in ANTHROPIC_API_KEY OPENAI_API_KEY GEMINI_API_KEY OPENCODE_ZEN_API_KEY; do |
| 168 | + if [ -n "${ENV_VARS[$k]:-}" ]; then HAS_LLM_KEY=true; break; fi |
| 169 | +done |
| 170 | +if [ "$HAS_LLM_KEY" = false ]; then |
| 171 | + warn "No LLM key set — agent needs at least one to work" |
| 172 | +fi |
| 173 | + |
| 174 | +echo "" |
| 175 | + |
| 176 | +prompt_secret "GITHUB_TOKEN" \ |
| 177 | + "GitHub personal access token" \ |
| 178 | + "https://github.com/settings/tokens" \ |
| 179 | + "required" \ |
| 180 | + "ghp_|github_pat_" |
| 181 | + |
| 182 | +prompt_secret "SLACK_BOT_TOKEN" \ |
| 183 | + "Slack bot token" \ |
| 184 | + "https://api.slack.com/apps → OAuth & Permissions" \ |
| 185 | + "required" \ |
| 186 | + "xoxb-" |
| 187 | + |
| 188 | +prompt_secret "SLACK_APP_TOKEN" \ |
| 189 | + "Slack app-level token (Socket Mode)" \ |
| 190 | + "https://api.slack.com/apps → Basic Information → App-Level Tokens" \ |
| 191 | + "required" \ |
| 192 | + "xapp-" |
| 193 | + |
| 194 | +prompt_secret "SLACK_ALLOWED_USERS" \ |
| 195 | + "Slack user IDs (comma-separated)" \ |
| 196 | + "Click your Slack profile → ··· → Copy member ID" \ |
| 197 | + "required" \ |
| 198 | + "U" |
| 199 | + |
| 200 | +echo "" |
| 201 | + |
| 202 | +# -- Optional -- |
| 203 | +echo -e "${BOLD}Optional${RESET} ${DIM}(press Enter to skip)${RESET}" |
| 204 | +echo "" |
| 205 | + |
| 206 | +prompt_secret "AGENTMAIL_API_KEY" \ |
| 207 | + "AgentMail API key" \ |
| 208 | + "https://app.agentmail.to" |
| 209 | + |
| 210 | +prompt_secret "BAUDBOT_EMAIL" \ |
| 211 | + "Agent email address (e.g. agent@agentmail.to)" |
| 212 | + |
| 213 | +if [ -n "${ENV_VARS[AGENTMAIL_API_KEY]:-}" ]; then |
| 214 | + prompt_secret "BAUDBOT_SECRET" \ |
| 215 | + "Email auth secret (or press Enter to auto-generate)" |
| 216 | + if [ -z "${ENV_VARS[BAUDBOT_SECRET]:-}" ]; then |
| 217 | + ENV_VARS[BAUDBOT_SECRET]="$(openssl rand -hex 32)" |
| 218 | + dim " Auto-generated: ${ENV_VARS[BAUDBOT_SECRET]}" |
| 219 | + fi |
| 220 | + |
| 221 | + prompt_secret "BAUDBOT_ALLOWED_EMAILS" \ |
| 222 | + "Allowed sender emails (comma-separated)" |
| 223 | +fi |
| 224 | + |
| 225 | +prompt_secret "SENTRY_AUTH_TOKEN" \ |
| 226 | + "Sentry API token" \ |
| 227 | + "https://sentry.io/settings/account/api/auth-tokens/" |
| 228 | + |
| 229 | +if [ -n "${ENV_VARS[SENTRY_AUTH_TOKEN]:-}" ]; then |
| 230 | + prompt_secret "SENTRY_ORG" "Sentry org slug" |
| 231 | + prompt_secret "SENTRY_CHANNEL_ID" "Slack channel ID for Sentry alerts" "" "" "C" |
| 232 | +fi |
| 233 | + |
| 234 | +prompt_secret "KERNEL_API_KEY" \ |
| 235 | + "Kernel cloud browser API key" \ |
| 236 | + "https://kernel.computer" |
| 237 | + |
| 238 | +# ── Auto-set values ────────────────────────────────────────────────────────── |
| 239 | + |
| 240 | +# These are set automatically based on the system state |
| 241 | +ENV_VARS[BAUDBOT_AGENT_USER]="baudbot_agent" |
| 242 | + |
| 243 | +if id baudbot_agent &>/dev/null; then |
| 244 | + ENV_VARS[BAUDBOT_AGENT_HOME]=$(getent passwd baudbot_agent | cut -d: -f6) |
| 245 | +else |
| 246 | + ENV_VARS[BAUDBOT_AGENT_HOME]="/home/baudbot_agent" |
| 247 | +fi |
| 248 | + |
| 249 | +# Source dir: resolve from this script's location, or keep existing |
| 250 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd || echo "")" |
| 251 | +if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/setup.sh" ]; then |
| 252 | + ENV_VARS[BAUDBOT_SOURCE_DIR]="$SCRIPT_DIR" |
| 253 | +elif [ -n "${EXISTING[BAUDBOT_SOURCE_DIR]:-}" ]; then |
| 254 | + ENV_VARS[BAUDBOT_SOURCE_DIR]="${EXISTING[BAUDBOT_SOURCE_DIR]}" |
| 255 | +fi |
| 256 | + |
| 257 | +# ── Write config ───────────────────────────────────────────────────────────── |
| 258 | + |
| 259 | +ENV_CONTENT="# Baudbot configuration |
| 260 | +# Generated by baudbot config on $(date -Iseconds) |
| 261 | +# Re-run: baudbot config |
| 262 | +# Deploy: baudbot deploy |
| 263 | +" |
| 264 | + |
| 265 | +ordered_keys=( |
| 266 | + ANTHROPIC_API_KEY |
| 267 | + OPENAI_API_KEY |
| 268 | + GEMINI_API_KEY |
| 269 | + OPENCODE_ZEN_API_KEY |
| 270 | + GITHUB_TOKEN |
| 271 | + SLACK_BOT_TOKEN |
| 272 | + SLACK_APP_TOKEN |
| 273 | + SLACK_ALLOWED_USERS |
| 274 | + AGENTMAIL_API_KEY |
| 275 | + BAUDBOT_EMAIL |
| 276 | + BAUDBOT_SECRET |
| 277 | + BAUDBOT_ALLOWED_EMAILS |
| 278 | + SENTRY_AUTH_TOKEN |
| 279 | + SENTRY_ORG |
| 280 | + SENTRY_CHANNEL_ID |
| 281 | + KERNEL_API_KEY |
| 282 | + BAUDBOT_AGENT_USER |
| 283 | + BAUDBOT_AGENT_HOME |
| 284 | + BAUDBOT_SOURCE_DIR |
| 285 | +) |
| 286 | + |
| 287 | +for key in "${ordered_keys[@]}"; do |
| 288 | + if [ -n "${ENV_VARS[$key]:-}" ]; then |
| 289 | + ENV_CONTENT+="${key}=${ENV_VARS[$key]}"$'\n' |
| 290 | + fi |
| 291 | +done |
| 292 | + |
| 293 | +echo "$ENV_CONTENT" > "$CONFIG_FILE" |
| 294 | +chmod 600 "$CONFIG_FILE" |
| 295 | +# Ensure owned by admin user |
| 296 | +if [ "$(id -u)" -eq 0 ]; then |
| 297 | + chown "$CONFIG_USER:$CONFIG_USER" "$CONFIG_FILE" |
| 298 | +fi |
| 299 | + |
| 300 | +VAR_COUNT=$(grep -c '=' "$CONFIG_FILE") |
| 301 | +info "Wrote $VAR_COUNT variables to $CONFIG_FILE" |
| 302 | +echo "" |
| 303 | +echo -e "Next: ${BOLD}sudo baudbot deploy${RESET} to push config to the agent" |
| 304 | +echo "" |
0 commit comments