Skip to content

Commit bb35c93

Browse files
authored
feat: support OAuth subscription login as alternative to API keys (#193)
1 parent 29ca8e0 commit bb35c93

File tree

7 files changed

+688
-61
lines changed

7 files changed

+688
-61
lines changed

bin/baudbot

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ usage() {
145145
echo " install Bootstrap install from GitHub (download script, then escalate)"
146146
echo " setup One-time system setup (user, deps, firewall, systemd; --experimental enables risky integrations)"
147147
echo " config Interactive secrets and config setup"
148+
echo " login Authenticate with an LLM subscription (OAuth)"
148149
echo " env Manage env vars and backend source (set/get/sync/backend)"
149150
echo " deploy Deploy source + config to agent runtime"
150151
echo " broker Slack broker commands (register workspace linkage)"
@@ -443,6 +444,33 @@ case "$COMMAND_NAME" in
443444
dispatch_registered_command "start" "$@"
444445
;;
445446

447+
login)
448+
require_root "login"
449+
BAUDBOT_AGENT_USER="${BAUDBOT_AGENT_USER:-baudbot_agent}"
450+
BAUDBOT_HOME="$(resolve_user_home "$BAUDBOT_AGENT_USER" 2>/dev/null || echo "/home/$BAUDBOT_AGENT_USER")"
451+
AUTH_JSON="$BAUDBOT_HOME/.pi/agent/auth.json"
452+
453+
NODE_BIN="$(resolve_node_bin || true)"
454+
if [ -z "$NODE_BIN" ]; then
455+
echo "❌ Could not find node runtime for OAuth login."
456+
exit 1
457+
fi
458+
459+
OAUTH_SCRIPT="$BAUDBOT_ROOT/bin/oauth-login.mjs"
460+
if [ ! -f "$OAUTH_SCRIPT" ]; then
461+
echo "❌ oauth-login.mjs not found at $OAUTH_SCRIPT"
462+
exit 1
463+
fi
464+
465+
"$NODE_BIN" "$OAUTH_SCRIPT" --auth-path "$AUTH_JSON" "$@"
466+
467+
# Fix ownership
468+
if [ -f "$AUTH_JSON" ] && id "$BAUDBOT_AGENT_USER" &>/dev/null; then
469+
chown "$BAUDBOT_AGENT_USER:$BAUDBOT_AGENT_USER" "$AUTH_JSON"
470+
chown "$BAUDBOT_AGENT_USER:$BAUDBOT_AGENT_USER" "$(dirname "$AUTH_JSON")" 2>/dev/null || true
471+
fi
472+
;;
473+
446474
setup)
447475
if [ "${1:-}" = "--slack-broker" ]; then
448476
shift

bin/config.sh

Lines changed: 144 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -361,62 +361,148 @@ fi
361361
echo -e "${BOLD}Required${RESET} ${DIM}(agent won't start without these)${RESET}"
362362
echo ""
363363

364-
# LLM provider picker
365-
echo -e "${BOLD}LLM provider${RESET}"
366-
LLM_CHOICE="$(ui_choose "Choose your primary LLM provider:" \
367-
"Anthropic" \
368-
"OpenAI" \
369-
"Gemini" \
370-
"OpenCode Zen")"
371-
372-
case "$LLM_CHOICE" in
373-
"Anthropic")
374-
prompt_secret "ANTHROPIC_API_KEY" \
375-
"Anthropic API key" \
376-
"https://console.anthropic.com/settings/keys" \
377-
"required" \
378-
"sk-ant-"
379-
;;
380-
"OpenAI")
381-
prompt_secret "OPENAI_API_KEY" \
382-
"OpenAI API key" \
383-
"https://platform.openai.com/api-keys" \
384-
"required" \
385-
"sk-"
386-
;;
387-
"Gemini")
388-
prompt_secret "GEMINI_API_KEY" \
389-
"Google Gemini API key" \
390-
"https://aistudio.google.com/apikey" \
391-
"required"
392-
;;
393-
"OpenCode Zen")
394-
prompt_secret "OPENCODE_ZEN_API_KEY" \
395-
"OpenCode Zen API key (multi-provider router)" \
396-
"https://opencode.ai" \
397-
"required"
398-
;;
399-
esac
364+
# LLM authentication tier
365+
echo -e "${BOLD}LLM authentication${RESET}"
366+
LLM_AUTH_TIER="$(ui_choose "How would you like to authenticate with your LLM?" \
367+
"API key" \
368+
"Subscription login (OAuth)")"
369+
370+
USED_SUBSCRIPTION_LOGIN=false
371+
372+
if [ "$LLM_AUTH_TIER" = "API key" ]; then
373+
# ── API key path ──
374+
echo ""
375+
LLM_CHOICE="$(ui_choose "Choose your primary LLM provider:" \
376+
"Anthropic" \
377+
"OpenAI" \
378+
"Gemini" \
379+
"OpenCode Zen")"
380+
381+
case "$LLM_CHOICE" in
382+
"Anthropic")
383+
prompt_secret "ANTHROPIC_API_KEY" \
384+
"Anthropic API key" \
385+
"https://console.anthropic.com/settings/keys" \
386+
"required" \
387+
"sk-ant-"
388+
;;
389+
"OpenAI")
390+
prompt_secret "OPENAI_API_KEY" \
391+
"OpenAI API key" \
392+
"https://platform.openai.com/api-keys" \
393+
"required" \
394+
"sk-"
395+
;;
396+
"Gemini")
397+
prompt_secret "GEMINI_API_KEY" \
398+
"Google Gemini API key" \
399+
"https://aistudio.google.com/apikey" \
400+
"required"
401+
;;
402+
"OpenCode Zen")
403+
prompt_secret "OPENCODE_ZEN_API_KEY" \
404+
"OpenCode Zen API key (multi-provider router)" \
405+
"https://opencode.ai" \
406+
"required"
407+
;;
408+
esac
400409

401-
SELECTED_LLM_KEY=""
402-
case "$LLM_CHOICE" in
403-
"Anthropic") SELECTED_LLM_KEY="ANTHROPIC_API_KEY" ;;
404-
"OpenAI") SELECTED_LLM_KEY="OPENAI_API_KEY" ;;
405-
"Gemini") SELECTED_LLM_KEY="GEMINI_API_KEY" ;;
406-
"OpenCode Zen") SELECTED_LLM_KEY="OPENCODE_ZEN_API_KEY" ;;
407-
esac
410+
SELECTED_LLM_KEY=""
411+
case "$LLM_CHOICE" in
412+
"Anthropic") SELECTED_LLM_KEY="ANTHROPIC_API_KEY" ;;
413+
"OpenAI") SELECTED_LLM_KEY="OPENAI_API_KEY" ;;
414+
"Gemini") SELECTED_LLM_KEY="GEMINI_API_KEY" ;;
415+
"OpenCode Zen") SELECTED_LLM_KEY="OPENCODE_ZEN_API_KEY" ;;
416+
esac
408417

409-
if [ -z "${ENV_VARS[$SELECTED_LLM_KEY]:-}" ]; then
410-
echo "$SELECTED_LLM_KEY is required for selected provider '$LLM_CHOICE'"
411-
exit 1
412-
fi
418+
if [ -z "${ENV_VARS[$SELECTED_LLM_KEY]:-}" ]; then
419+
echo "$SELECTED_LLM_KEY is required for selected provider '$LLM_CHOICE'"
420+
exit 1
421+
fi
413422

414-
# Keep only selected provider key for deterministic config.
415-
for key in ANTHROPIC_API_KEY OPENAI_API_KEY GEMINI_API_KEY OPENCODE_ZEN_API_KEY; do
416-
if [ "$key" != "$SELECTED_LLM_KEY" ]; then
417-
unset "ENV_VARS[$key]"
423+
# Keep only selected provider key for deterministic config.
424+
for key in ANTHROPIC_API_KEY OPENAI_API_KEY GEMINI_API_KEY OPENCODE_ZEN_API_KEY; do
425+
if [ "$key" != "$SELECTED_LLM_KEY" ]; then
426+
unset "ENV_VARS[$key]"
427+
fi
428+
done
429+
430+
else
431+
# ── Subscription login (OAuth) path ──
432+
clear_keys ANTHROPIC_API_KEY OPENAI_API_KEY GEMINI_API_KEY OPENCODE_ZEN_API_KEY
433+
LLM_CHOICE="Subscription"
434+
SELECTED_LLM_KEY=""
435+
436+
# Resolve the agent home for auth.json
437+
BAUDBOT_HOME="${BAUDBOT_HOME:-/home/baudbot_agent}"
438+
AUTH_JSON="$BAUDBOT_HOME/.pi/agent/auth.json"
439+
440+
# Find Node.js for oauth-login.mjs
441+
OAUTH_SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/oauth-login.mjs"
442+
OAUTH_NODE_BIN=""
443+
if [ -f "$SCRIPT_DIR/../bin/lib/runtime-node.sh" ]; then
444+
# shellcheck source=bin/lib/runtime-node.sh
445+
source "$SCRIPT_DIR/../bin/lib/runtime-node.sh" 2>/dev/null || true
446+
OAUTH_NODE_BIN="$(bb_resolve_runtime_node_bin "$BAUDBOT_HOME" 2>/dev/null || true)"
447+
fi
448+
if [ -z "$OAUTH_NODE_BIN" ] || [ ! -x "$OAUTH_NODE_BIN" ]; then
449+
OAUTH_NODE_BIN="$(command -v node 2>/dev/null || true)"
450+
fi
451+
452+
if [ ! -f "$OAUTH_SCRIPT" ]; then
453+
echo "❌ oauth-login.mjs not found at $OAUTH_SCRIPT"
454+
exit 1
455+
fi
456+
if [ -z "$OAUTH_NODE_BIN" ]; then
457+
echo "❌ Node.js not found — required for OAuth login"
458+
exit 1
418459
fi
419-
done
460+
461+
# Check for existing OAuth credentials
462+
HAS_EXISTING_OAUTH=false
463+
EXISTING_OAUTH_PROVIDER=""
464+
if [ -f "$AUTH_JSON" ] && command -v jq &>/dev/null; then
465+
for op in "openai-codex" "anthropic"; do
466+
if jq -e --arg p "$op" '.[$p]' "$AUTH_JSON" &>/dev/null; then
467+
HAS_EXISTING_OAUTH=true
468+
EXISTING_OAUTH_PROVIDER="$op"
469+
break
470+
fi
471+
done
472+
fi
473+
474+
if [ "$HAS_EXISTING_OAUTH" = true ]; then
475+
info "Existing OAuth credentials found ($EXISTING_OAUTH_PROVIDER)."
476+
if ! ui_confirm "Re-authenticate with a different provider?" false; then
477+
info "Keeping existing OAuth credentials."
478+
USED_SUBSCRIPTION_LOGIN=true
479+
fi
480+
fi
481+
482+
if [ "$USED_SUBSCRIPTION_LOGIN" = false ]; then
483+
echo ""
484+
dim " This will open an OAuth flow. You'll get a URL to open in your browser."
485+
echo ""
486+
487+
# Run oauth-login.mjs interactively
488+
OAUTH_PROVIDER_ID=""
489+
if OAUTH_PROVIDER_ID=$("$OAUTH_NODE_BIN" "$OAUTH_SCRIPT" --auth-path "$AUTH_JSON"); then
490+
OAUTH_PROVIDER_ID="$(echo "$OAUTH_PROVIDER_ID" | tr -d '[:space:]')"
491+
info "✓ OAuth login complete ($OAUTH_PROVIDER_ID)"
492+
493+
# Fix ownership if running as root
494+
if [ "$(id -u)" -eq 0 ] && id baudbot_agent &>/dev/null; then
495+
chown baudbot_agent:baudbot_agent "$AUTH_JSON"
496+
# Also fix parent dirs
497+
chown baudbot_agent:baudbot_agent "$(dirname "$AUTH_JSON")" 2>/dev/null || true
498+
fi
499+
else
500+
echo "❌ OAuth login failed"
501+
exit 1
502+
fi
503+
USED_SUBSCRIPTION_LOGIN=true
504+
fi
505+
fi
420506

421507
echo ""
422508

@@ -655,7 +741,12 @@ VAR_COUNT=$(grep -c '=' "$CONFIG_FILE")
655741
info "Wrote $VAR_COUNT variables to $CONFIG_FILE"
656742
echo ""
657743
echo -e "${BOLD}Summary${RESET}"
658-
echo -e " LLM provider: ${BOLD}${LLM_CHOICE}${RESET}"
744+
echo -e " LLM auth: ${BOLD}${LLM_AUTH_TIER}${RESET}"
745+
if [ "$LLM_AUTH_TIER" = "API key" ]; then
746+
echo -e " LLM provider: ${BOLD}${LLM_CHOICE}${RESET}"
747+
else
748+
echo -e " LLM provider: ${BOLD}Subscription (OAuth via auth.json)${RESET}"
749+
fi
659750
echo -e " Slack mode: ${BOLD}${SLACK_CHOICE}${RESET}"
660751
if [ "$SLACK_CHOICE" = "Use baudbot.ai Slack integration (easy)" ]; then
661752
echo -e " ${DIM}Next: run 'sudo baudbot broker register' after install${RESET}"

bin/config.test.sh

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,50 +83,78 @@ echo "================="
8383
echo ""
8484

8585
# Test 1: Advanced Slack path writes socket-mode keys only
86+
# Input: 1=API key tier, 1=Anthropic, key, 2=advanced Slack, tokens, ...
8687
HOME1="$TMPDIR/advanced"
87-
run_config "$HOME1" '1\nsk-ant-test\n2\nxoxb-test\nxapp-test\n\nn\nn\n'
88+
run_config "$HOME1" '1\n1\nsk-ant-test\n2\nxoxb-test\nxapp-test\n\nn\nn\n'
8889
ENV1="$HOME1/.baudbot/.env"
8990
expect_file_contains "advanced path writes Anthropic key" "$ENV1" "ANTHROPIC_API_KEY=sk-ant-test"
9091
expect_file_contains "advanced path writes SLACK_BOT_TOKEN" "$ENV1" "SLACK_BOT_TOKEN=xoxb-test"
9192
expect_file_contains "advanced path writes SLACK_APP_TOKEN" "$ENV1" "SLACK_APP_TOKEN=xapp-test"
9293
expect_file_not_contains "advanced path does not write OPENAI key" "$ENV1" "OPENAI_API_KEY="
9394

9495
# Test 2: Easy Slack path avoids socket-mode keys
96+
# Input: 1=API key tier, 2=OpenAI, key, 1=easy Slack, ...
9597
HOME2="$TMPDIR/easy"
96-
run_config "$HOME2" '2\nsk-openai-test\n1\n\nn\nn\n'
98+
run_config "$HOME2" '1\n2\nsk-openai-test\n1\n\nn\nn\n'
9799
ENV2="$HOME2/.baudbot/.env"
98100
expect_file_contains "easy path writes OpenAI key" "$ENV2" "OPENAI_API_KEY=sk-openai-test"
99101
expect_file_not_contains "easy path omits SLACK_BOT_TOKEN" "$ENV2" "SLACK_BOT_TOKEN="
100102
expect_file_not_contains "easy path omits SLACK_APP_TOKEN" "$ENV2" "SLACK_APP_TOKEN="
101103

102104
# Test 3: Optional integration toggle prompts conditionally
105+
# Input: 1=API key tier, 3=Gemini, key, 2=advanced Slack, tokens, ..., y=kernel, key, n=sentry
103106
HOME3="$TMPDIR/kernel"
104-
run_config "$HOME3" '3\ngem-key\n2\nxoxb-test\nxapp-test\n\ny\nkernel-key\nn\n'
107+
run_config "$HOME3" '1\n3\ngem-key\n2\nxoxb-test\nxapp-test\n\ny\nkernel-key\nn\n'
105108
ENV3="$HOME3/.baudbot/.env"
106109
expect_file_contains "kernel enabled writes key" "$ENV3" "KERNEL_API_KEY=kernel-key"
107110
expect_file_not_contains "sentry skipped omits token" "$ENV3" "SENTRY_AUTH_TOKEN="
108111
expect_file_not_contains "email skipped omits AgentMail" "$ENV3" "AGENTMAIL_API_KEY="
109112

110113
# Test 4: Selected LLM key is required
114+
# Input: 1=API key tier, 1=Anthropic, empty key
111115
HOME4="$TMPDIR/missing-llm"
112-
expect_exit_nonzero "fails when selected provider key is missing" "$HOME4" '1\n\n'
116+
expect_exit_nonzero "fails when selected provider key is missing" "$HOME4" '1\n1\n\n'
113117

114118
# Test 5: Re-run preserves existing selected LLM key when input is blank
119+
# Input: 1=API key tier, 1=Anthropic, blank (keep existing), 1=easy Slack, ...
115120
HOME5="$TMPDIR/rerun-keep-llm"
116121
write_existing_env "$HOME5" 'ANTHROPIC_API_KEY=sk-ant-existing\n'
117-
run_config "$HOME5" '1\n\n1\n\nn\nn\n'
122+
run_config "$HOME5" '1\n1\n\n1\n\nn\nn\n'
118123
ENV5="$HOME5/.baudbot/.env"
119124
expect_file_contains "rerun keeps existing Anthropic key" "$ENV5" "ANTHROPIC_API_KEY=sk-ant-existing"
120125

121126
# Test 6: Advanced Slack mode clears stale broker registration keys
127+
# Input: 1=API key tier, 2=OpenAI, key, 2=advanced Slack, tokens, ...
122128
HOME6="$TMPDIR/clear-broker"
123129
write_existing_env "$HOME6" 'OPENAI_API_KEY=sk-old\nSLACK_BROKER_URL=https://broker.example.com\nSLACK_BROKER_WORKSPACE_ID=T0123\nSLACK_BROKER_PUBLIC_KEY=abc\n'
124-
run_config "$HOME6" '2\nsk-openai-new\n2\nxoxb-new\nxapp-new\n\nn\nn\n'
130+
run_config "$HOME6" '1\n2\nsk-openai-new\n2\nxoxb-new\nxapp-new\n\nn\nn\n'
125131
ENV6="$HOME6/.baudbot/.env"
126132
expect_file_not_contains "advanced clears broker URL" "$ENV6" "SLACK_BROKER_URL="
127133
expect_file_not_contains "advanced clears broker workspace" "$ENV6" "SLACK_BROKER_WORKSPACE_ID="
128134
expect_file_contains "advanced retains socket bot token" "$ENV6" "SLACK_BOT_TOKEN=xoxb-new"
129135

136+
# Test 7: Subscription login tier with existing auth.json skips OAuth flow
137+
# Input: 2=Subscription tier, n=don't re-auth, 1=easy Slack, n=kernel, n=sentry
138+
HOME7="$TMPDIR/subscription"
139+
mkdir -p "$HOME7/.pi/agent"
140+
echo '{"anthropic":{"type":"oauth","access":"tok","refresh":"ref","expires":9999999999999}}' \
141+
> "$HOME7/.pi/agent/auth.json"
142+
config_user="$(id -un)"
143+
printf "%b" '2\nn\n1\n\nn\nn\n' \
144+
| HOME="$HOME7" BAUDBOT_HOME="$HOME7" BAUDBOT_CONFIG_USER="$config_user" BAUDBOT_TRY_INSTALL_GUM=0 \
145+
bash "$CONFIG_SCRIPT" >"$OUT_FILE" 2>"$ERR_FILE"
146+
ENV7="$HOME7/.baudbot/.env"
147+
expect_file_not_contains "subscription path omits ANTHROPIC_API_KEY" "$ENV7" "ANTHROPIC_API_KEY="
148+
expect_file_not_contains "subscription path omits OPENAI_API_KEY" "$ENV7" "OPENAI_API_KEY="
149+
# Verify subscription was detected in output
150+
if grep -q "Subscription" "$OUT_FILE"; then
151+
echo " PASS: subscription tier shown in summary"
152+
PASS=$((PASS + 1))
153+
else
154+
echo " FAIL: subscription tier shown in summary"
155+
FAIL=$((FAIL + 1))
156+
fi
157+
130158
echo ""
131159
echo "Results: $PASS passed, $FAIL failed"
132160

bin/doctor.sh

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,23 @@ if [ -f "$ENV_FILE" ]; then
188188
if [ "$VALID_LLM_COUNT" -gt 0 ]; then
189189
pass "at least one valid LLM API key is set"
190190
else
191-
fail "no valid LLM API key set (need ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, or OPENCODE_ZEN_API_KEY)"
191+
# Check auth.json for OAuth subscription credentials
192+
AUTH_JSON="$BAUDBOT_HOME/.pi/agent/auth.json"
193+
HAS_OAUTH=false
194+
OAUTH_PROVIDERS=""
195+
if [ -f "$AUTH_JSON" ] && command -v jq &>/dev/null; then
196+
for oauth_provider in "openai-codex" "anthropic" "google" "github-copilot"; do
197+
if jq -e --arg p "$oauth_provider" '.[$p]' "$AUTH_JSON" &>/dev/null; then
198+
HAS_OAUTH=true
199+
OAUTH_PROVIDERS="${OAUTH_PROVIDERS:+$OAUTH_PROVIDERS, }$oauth_provider"
200+
fi
201+
done
202+
fi
203+
if [ "$HAS_OAUTH" = true ]; then
204+
pass "OAuth subscription credentials found in auth.json ($OAUTH_PROVIDERS)"
205+
else
206+
fail "no valid LLM API key set (need ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, or OPENCODE_ZEN_API_KEY; or use: sudo baudbot login)"
207+
fi
192208
fi
193209

194210
read_first_env_value() {

0 commit comments

Comments
 (0)