From b83e97e8d926c6b8e428af7046d179cbfdb6fd89 Mon Sep 17 00:00:00 2001 From: 0pfleet Date: Tue, 10 Feb 2026 16:24:40 -0800 Subject: [PATCH 1/7] fix(api): load API keys from database at server/worker startup Previously, API keys configured via the webapp were stored in the database but GraphClient read them from environment variables at import time. This caused "The api_key client option must be set" errors even when keys were configured in the UI. This fix loads API keys from the database into environment variables during server and worker startup, bridging the gap between webapp settings and GraphClient initialization. - Server: Load keys after DB connected, before GraphClient.connect() - Worker: Load keys in startup() before any jobs run Co-Authored-By: Claude Opus 4.5 --- apps/api/src/sibyl/jobs/worker.py | 26 ++++++++++++++++++++++++++ apps/api/src/sibyl/main.py | 25 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/apps/api/src/sibyl/jobs/worker.py b/apps/api/src/sibyl/jobs/worker.py index 00efeb4..328a987 100644 --- a/apps/api/src/sibyl/jobs/worker.py +++ b/apps/api/src/sibyl/jobs/worker.py @@ -169,6 +169,8 @@ async def _cleanup_orphaned_agent_jobs() -> int: async def startup(ctx: dict[str, Any]) -> None: """Worker startup - initialize resources.""" + import os + from sibyl.banner import log_banner from sibyl_core.logging import configure_logging @@ -179,6 +181,30 @@ async def startup(ctx: dict[str, Any]) -> None: log.info("Job worker online") ctx["start_time"] = datetime.now(UTC) + # Load API keys from database into environment BEFORE any jobs use GraphClient + # This bridges the gap between webapp-configured settings (stored in DB) + # and CoreConfig (which reads from env vars at import time) + try: + from sibyl.services.settings import get_settings_service + + settings_svc = get_settings_service() + + # Load OpenAI key if not already set in environment + if not os.environ.get("OPENAI_API_KEY"): + openai_key = await settings_svc.get_openai_key() + if openai_key: + os.environ["OPENAI_API_KEY"] = openai_key + log.debug("Loaded OpenAI API key from database settings") + + # Load Anthropic key if not already set in environment + if not os.environ.get("ANTHROPIC_API_KEY"): + anthropic_key = await settings_svc.get_anthropic_key() + if anthropic_key: + os.environ["ANTHROPIC_API_KEY"] = anthropic_key + log.debug("Loaded Anthropic API key from database settings") + except Exception as e: + log.warning("Failed to load API keys from database", error=str(e)) + # Clean up stale working agents (from worker crashes) stale_marked = await _cleanup_stale_working_agents() if stale_marked: diff --git a/apps/api/src/sibyl/main.py b/apps/api/src/sibyl/main.py index ae46bcc..9c1934f 100644 --- a/apps/api/src/sibyl/main.py +++ b/apps/api/src/sibyl/main.py @@ -113,6 +113,31 @@ async def lifespan(_app: Starlette) -> "AsyncGenerator[None]": # noqa: PLR0915 except Exception as e: log.warning("Source recovery failed", error=str(e)) + # Load API keys from database into environment BEFORE GraphClient initializes + # This bridges the gap between webapp-configured settings (stored in DB) + # and CoreConfig (which reads from env vars at import time) + if db_connected: + try: + from sibyl.services.settings import get_settings_service + + settings_svc = get_settings_service() + + # Load OpenAI key if not already set in environment + if not os.environ.get("OPENAI_API_KEY"): + openai_key = await settings_svc.get_openai_key() + if openai_key: + os.environ["OPENAI_API_KEY"] = openai_key + log.debug("Loaded OpenAI API key from database settings") + + # Load Anthropic key if not already set in environment + if not os.environ.get("ANTHROPIC_API_KEY"): + anthropic_key = await settings_svc.get_anthropic_key() + if anthropic_key: + os.environ["ANTHROPIC_API_KEY"] = anthropic_key + log.debug("Loaded Anthropic API key from database settings") + except Exception as e: + log.warning("Failed to load API keys from database", error=str(e)) + try: from sibyl_core.graph.client import get_graph_client From 99af0ae9d978d56d625d7a12d02bf1a01b3cffbd Mon Sep 17 00:00:00 2001 From: 0pfleet Date: Tue, 10 Feb 2026 16:25:04 -0800 Subject: [PATCH 2/7] fix(api): hot-reload API keys when updated via webapp When users update API keys through the settings UI, the running server now immediately uses the new keys without requiring a restart: 1. Update environment variables with new key values 2. Reset GraphClient singleton so next connection uses new keys 3. Handle key deletion by clearing env vars and resetting client This ensures a seamless experience when rotating or updating API keys. Co-Authored-By: Claude Opus 4.5 --- apps/api/src/sibyl/api/routes/settings.py | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/apps/api/src/sibyl/api/routes/settings.py b/apps/api/src/sibyl/api/routes/settings.py index 35ecbd6..b4c6328 100644 --- a/apps/api/src/sibyl/api/routes/settings.py +++ b/apps/api/src/sibyl/api/routes/settings.py @@ -6,6 +6,8 @@ from __future__ import annotations +import os + import httpx import structlog from fastapi import APIRouter, Depends, HTTPException @@ -192,6 +194,10 @@ async def update_settings( description="OpenAI API key for embeddings and LLM operations", ) updated.append("openai_api_key") + # Update environment variable so running server uses new key immediately + # This bridges webapp settings to GraphClient which reads from env vars + os.environ["OPENAI_API_KEY"] = request.openai_api_key + log.info("Updated OpenAI API key in environment") else: log.warning("OpenAI key validation failed", error=error) @@ -208,9 +214,23 @@ async def update_settings( description="Anthropic API key for Claude models", ) updated.append("anthropic_api_key") + # Update environment variable so running server uses new key immediately + os.environ["ANTHROPIC_API_KEY"] = request.anthropic_api_key + log.info("Updated Anthropic API key in environment") else: log.warning("Anthropic key validation failed", error=error) + # If API keys were updated, reset the GraphClient so it reconnects with new keys + # The global singleton is reused, so existing connections would use stale keys + if updated: + try: + from sibyl_core.graph.client import reset_graph_client + + await reset_graph_client() + log.info("Reset GraphClient after API key update", keys=updated) + except Exception as e: + log.warning("Failed to reset GraphClient", error=str(e)) + return UpdateSettingsResponse(updated=updated, validation=validation) @@ -239,6 +259,19 @@ async def delete_setting( deleted = await service.delete(key) if deleted: + # Clear from environment and reset GraphClient if this was an API key + if key in ("openai_api_key", "anthropic_api_key"): + env_key = "OPENAI_API_KEY" if key == "openai_api_key" else "ANTHROPIC_API_KEY" + os.environ.pop(env_key, None) + + try: + from sibyl_core.graph.client import reset_graph_client + + await reset_graph_client() + log.info("Reset GraphClient after API key deletion", key=key) + except Exception as e: + log.warning("Failed to reset GraphClient", error=str(e)) + return DeleteSettingResponse( deleted=True, key=key, From e468e105d60ad5d8009ede34f58c9918aecba274 Mon Sep 17 00:00:00 2001 From: 0pfleet Date: Tue, 10 Feb 2026 16:25:14 -0800 Subject: [PATCH 3/7] fix(web): persist setup wizard step across tab switches Users often switch tabs during setup to copy API keys from provider dashboards. Previously, returning to Sibyl would reset the wizard to the Welcome step, losing their progress. Now the current step is saved to sessionStorage, so users return to the same step. Storage is cleared on completion. Co-Authored-By: Claude Opus 4.5 --- .../web/src/components/setup/setup-wizard.tsx | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/setup/setup-wizard.tsx b/apps/web/src/components/setup/setup-wizard.tsx index 57c2918..256ffc8 100644 --- a/apps/web/src/components/setup/setup-wizard.tsx +++ b/apps/web/src/components/setup/setup-wizard.tsx @@ -1,7 +1,7 @@ 'use client'; import { AnimatePresence, motion } from 'motion/react'; -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import type { SetupStatus } from '@/lib/api'; import { AdminAccountStep } from './steps/admin-account-step'; import { ApiKeysStep } from './steps/api-keys-step'; @@ -16,9 +16,30 @@ interface SetupWizardProps { } const STEPS: SetupStep[] = ['welcome', 'api-keys', 'admin', 'connect']; +const STEP_STORAGE_KEY = 'sibyl-setup-step'; + +function getStoredStep(): SetupStep { + if (typeof window === 'undefined') return 'welcome'; + const stored = sessionStorage.getItem(STEP_STORAGE_KEY); + if (stored && STEPS.includes(stored as SetupStep)) { + return stored as SetupStep; + } + return 'welcome'; +} export function SetupWizard({ initialStatus, onComplete }: SetupWizardProps) { - const [step, setStep] = useState('welcome'); + const [step, setStep] = useState(getStoredStep); + + // Persist step to sessionStorage so tab switches don't reset progress + useEffect(() => { + sessionStorage.setItem(STEP_STORAGE_KEY, step); + }, [step]); + + // Clear stored step on completion + const handleComplete = useCallback(() => { + sessionStorage.removeItem(STEP_STORAGE_KEY); + onComplete(); + }, [onComplete]); const currentIndex = STEPS.indexOf(step); const isLastStep = step === 'connect'; @@ -128,7 +149,7 @@ export function SetupWizard({ initialStatus, onComplete }: SetupWizardProps) { exit={{ opacity: 0 }} transition={{ duration: 0.3 }} > - + )} From f2835da28bccb70be1de9626493d97959cb12e54 Mon Sep 17 00:00:00 2001 From: 0pfleet Date: Tue, 10 Feb 2026 16:25:39 -0800 Subject: [PATCH 4/7] fix(web): improve API key setup UX with clear save feedback Improved the API keys setup step to provide clear feedback when keys are saved individually: - Show green "Saved" badge on configured keys (persists across tab switches) - Display progress banner: "OpenAI key saved! Now add your Anthropic key." - Update button text contextually: "Save Key" vs "Save Keys" - Show "Enter Anthropic Key" when only that key is missing (and vice versa) - Use configured state (from DB) instead of volatile validation state This helps users understand their progress when switching tabs to copy API keys from provider dashboards. Co-Authored-By: Claude Opus 4.5 --- .../components/setup/steps/api-keys-step.tsx | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/apps/web/src/components/setup/steps/api-keys-step.tsx b/apps/web/src/components/setup/steps/api-keys-step.tsx index 957227a..5b39b9c 100644 --- a/apps/web/src/components/setup/steps/api-keys-step.tsx +++ b/apps/web/src/components/setup/steps/api-keys-step.tsx @@ -46,7 +46,6 @@ export function ApiKeysStep({ initialStatus, onBack, onValidated }: ApiKeysStepP updateSettings.data?.validation?.anthropic_api_key?.valid ?? validation?.anthropic_valid ?? initialStatus?.anthropic_valid; - const bothValid = openaiValid === true && anthropicValid === true; // Validation errors const openaiError = @@ -92,10 +91,11 @@ export function ApiKeysStep({ initialStatus, onBack, onValidated }: ApiKeysStepP }, [revalidate, onValidated]); const handleContinue = useCallback(() => { - if (bothValid) { + // Both keys are configured (and were validated before being saved) + if (bothConfigured) { onValidated(true); } - }, [bothValid, onValidated]); + }, [bothConfigured, onValidated]); return (
@@ -155,6 +155,19 @@ export function ApiKeysStep({ initialStatus, onBack, onValidated }: ApiKeysStepP
)} + {/* Progress message - show when one key is saved but not both */} + {!bothConfigured && (openaiConfigured || anthropicConfigured) && ( +
+
+ +

+ {openaiConfigured && !anthropicConfigured && 'OpenAI key saved! Now add your Anthropic key below.'} + {anthropicConfigured && !openaiConfigured && 'Anthropic key saved! Now add your OpenAI key below.'} +

+
+
+ )} + {/* Help text */} {!bothConfigured && !hasKeyInput && (
@@ -198,7 +211,8 @@ export function ApiKeysStep({ initialStatus, onBack, onValidated }: ApiKeysStepP Back - {bothValid ? ( + {bothConfigured ? ( + // Both keys saved - show Continue - ) : bothConfigured ? ( - ) : ( + // Waiting for user to enter key(s) )}
@@ -287,22 +291,21 @@ function ApiKeyInput({ statusIcon = ; statusColor = 'text-sc-cyan'; statusText = 'Validating...'; - } else if (valid === true) { - statusIcon =