From 81b3c47eca2ee383f74fedb2e3a3f38193e3deb3 Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Thu, 4 Sep 2025 18:11:00 -0500 Subject: [PATCH 1/3] Pass API keys for docker build --- docker/Dockerfile | 12 + docker/docker-compose.yml | 17 +- front_end/panels/ai_chat/BUILD.gn | 2 + front_end/panels/ai_chat/LLM/GroqProvider.ts | 42 ++-- .../panels/ai_chat/LLM/OpenAIProvider.ts | 24 +- .../panels/ai_chat/LLM/OpenRouterProvider.ts | 67 ++--- .../panels/ai_chat/core/EnvironmentConfig.ts | 235 ++++++++++++++++++ 7 files changed, 348 insertions(+), 51 deletions(-) create mode 100644 front_end/panels/ai_chat/core/EnvironmentConfig.ts diff --git a/docker/Dockerfile b/docker/Dockerfile index 41f08a1b46..8fb21d47fe 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,18 @@ # Multi-stage build for Chrome DevTools Frontend FROM --platform=linux/amd64 ubuntu:22.04 AS builder +# Build arguments for API keys (optional, defaults to empty) +ARG OPENAI_API_KEY="" +ARG OPENROUTER_API_KEY="" +ARG GROQ_API_KEY="" +ARG LITELLM_API_KEY="" + +# Set environment variables for build process +ENV OPENAI_API_KEY=${OPENAI_API_KEY} +ENV OPENROUTER_API_KEY=${OPENROUTER_API_KEY} +ENV GROQ_API_KEY=${GROQ_API_KEY} +ENV LITELLM_API_KEY=${LITELLM_API_KEY} + # Install required packages RUN apt-get update && apt-get install -y \ curl \ diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 55a144350f..138efe099b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -5,18 +5,29 @@ services: build: context: .. dockerfile: docker/Dockerfile + args: + # API keys passed from host environment variables + # These can be set via .env file or exported in shell + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} + GROQ_API_KEY: ${GROQ_API_KEY:-} + LITELLM_API_KEY: ${LITELLM_API_KEY:-} image: devtools-frontend:latest container_name: devtools-frontend ports: - "8000:8000" restart: unless-stopped - volumes: + # volumes: # For development: mount the built files directly (optional) - # Uncomment the line below to use local files instead of container files - # - ../out/Default/gen/front_end:/usr/share/nginx/html:ro + # Uncomment the lines below to use local files instead of container files + # volumes: + # - ../out/Default/gen/front_end:/usr/share/nginx/html:ro environment: - NGINX_HOST=localhost - NGINX_PORT=8000 + # Optional: Pass API keys to runtime for debugging (not required for functionality) + - OPENAI_API_KEY=${OPENAI_API_KEY:-} + - OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-} healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8000/"] interval: 30s diff --git a/front_end/panels/ai_chat/BUILD.gn b/front_end/panels/ai_chat/BUILD.gn index 0ae6cecf55..032f78f0a1 100644 --- a/front_end/panels/ai_chat/BUILD.gn +++ b/front_end/panels/ai_chat/BUILD.gn @@ -28,6 +28,7 @@ devtools_module("ai_chat") { "core/Types.ts", "core/AgentService.ts", "core/Constants.ts", + "core/EnvironmentConfig.ts", "core/GraphConfigs.ts", "core/ConfigurableGraph.ts", "core/BaseOrchestratorAgent.ts", @@ -133,6 +134,7 @@ _ai_chat_sources = [ "core/Types.ts", "core/AgentService.ts", "core/Constants.ts", + "core/EnvironmentConfig.ts", "core/GraphConfigs.ts", "core/ConfigurableGraph.ts", "core/BaseOrchestratorAgent.ts", diff --git a/front_end/panels/ai_chat/LLM/GroqProvider.ts b/front_end/panels/ai_chat/LLM/GroqProvider.ts index e5e6aaaaf0..1e9cf86fa5 100644 --- a/front_end/panels/ai_chat/LLM/GroqProvider.ts +++ b/front_end/panels/ai_chat/LLM/GroqProvider.ts @@ -7,6 +7,7 @@ import { LLMBaseProvider } from './LLMProvider.js'; import { LLMRetryManager } from './LLMErrorHandler.js'; import { LLMResponseParser } from './LLMResponseParser.js'; import { createLogger } from '../core/Logger.js'; +import { getEnvironmentConfig } from '../core/EnvironmentConfig.js'; const logger = createLogger('GroqProvider'); @@ -38,10 +39,29 @@ export class GroqProvider extends LLMBaseProvider { readonly name: LLMProvider = 'groq'; + private readonly envConfig = getEnvironmentConfig(); + constructor(private readonly apiKey: string) { super(); } + /** + * Get the API key with fallback hierarchy: + * 1. Constructor parameter (for backward compatibility) + * 2. localStorage (user-configured) + * 3. Build-time environment config + * 4. Empty string + */ + private getApiKey(): string { + // Constructor parameter (highest priority for backward compatibility) + if (this.getApiKey() && this.getApiKey().trim() !== '') { + return this.getApiKey().trim(); + } + + // Use environment config which handles localStorage -> build-time -> empty fallback + return this.envConfig.getApiKey('groq'); + } + /** * Get the chat completions endpoint URL */ @@ -92,7 +112,7 @@ export class GroqProvider extends LLMBaseProvider { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.apiKey}`, + 'Authorization': `Bearer ${this.getApiKey()}`, }, body: JSON.stringify(payloadBody), }); @@ -259,7 +279,7 @@ export class GroqProvider extends LLMBaseProvider { const response = await fetch(this.getModelsEndpoint(), { method: 'GET', headers: { - 'Authorization': `Bearer ${this.apiKey}`, + 'Authorization': `Bearer ${this.getApiKey()}`, }, }); @@ -432,21 +452,7 @@ export class GroqProvider extends LLMBaseProvider { * Validate that required credentials are available for Groq */ validateCredentials(): {isValid: boolean, message: string, missingItems?: string[]} { - const storageKeys = this.getCredentialStorageKeys(); - const apiKey = localStorage.getItem(storageKeys.apiKey!); - - if (!apiKey) { - return { - isValid: false, - message: 'Groq API key is required. Please add your API key in Settings.', - missingItems: ['API Key'] - }; - } - - return { - isValid: true, - message: 'Groq credentials are configured correctly.' - }; + return this.envConfig.validateCredentials('groq'); } /** @@ -454,7 +460,7 @@ export class GroqProvider extends LLMBaseProvider { */ getCredentialStorageKeys(): {apiKey: string} { return { - apiKey: 'ai_chat_groq_api_key' + apiKey: this.envConfig.getStorageKey('groq') }; } } \ No newline at end of file diff --git a/front_end/panels/ai_chat/LLM/OpenAIProvider.ts b/front_end/panels/ai_chat/LLM/OpenAIProvider.ts index b8c30ad323..63077a3ee0 100644 --- a/front_end/panels/ai_chat/LLM/OpenAIProvider.ts +++ b/front_end/panels/ai_chat/LLM/OpenAIProvider.ts @@ -7,6 +7,7 @@ import { LLMBaseProvider } from './LLMProvider.js'; import { LLMRetryManager } from './LLMErrorHandler.js'; import { LLMResponseParser } from './LLMResponseParser.js'; import { createLogger } from '../core/Logger.js'; +import { getEnvironmentConfig } from '../core/EnvironmentConfig.js'; const logger = createLogger('OpenAIProvider'); @@ -42,10 +43,29 @@ export class OpenAIProvider extends LLMBaseProvider { readonly name: LLMProvider = 'openai'; + private readonly envConfig = getEnvironmentConfig(); + constructor(private readonly apiKey: string) { super(); } + /** + * Get the API key with fallback hierarchy: + * 1. Constructor parameter (for backward compatibility) + * 2. localStorage (user-configured) + * 3. Build-time environment config + * 4. Empty string + */ + private getApiKey(): string { + // Constructor parameter (highest priority for backward compatibility) + if (this.apiKey && this.apiKey.trim() !== '') { + return this.apiKey.trim(); + } + + // Use environment config which handles localStorage -> build-time -> empty fallback + return this.envConfig.getApiKey('openai'); + } + /** * Determines the model family based on the model name */ @@ -280,7 +300,7 @@ export class OpenAIProvider extends LLMBaseProvider { metadata: { provider: 'openai', errorType: 'api_error', - hasApiKey: !!this.apiKey + hasApiKey: !!this.getApiKey() } }, context.traceId); } @@ -299,7 +319,7 @@ export class OpenAIProvider extends LLMBaseProvider { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${this.apiKey}`, + Authorization: `Bearer ${this.getApiKey()}`, }, body: JSON.stringify(payloadBody), }); diff --git a/front_end/panels/ai_chat/LLM/OpenRouterProvider.ts b/front_end/panels/ai_chat/LLM/OpenRouterProvider.ts index 7acb066e22..055a3421db 100644 --- a/front_end/panels/ai_chat/LLM/OpenRouterProvider.ts +++ b/front_end/panels/ai_chat/LLM/OpenRouterProvider.ts @@ -7,6 +7,7 @@ import { LLMBaseProvider } from './LLMProvider.js'; import { LLMRetryManager } from './LLMErrorHandler.js'; import { LLMResponseParser } from './LLMResponseParser.js'; import { createLogger } from '../core/Logger.js'; +import { getEnvironmentConfig } from '../core/EnvironmentConfig.js'; const logger = createLogger('OpenRouterProvider'); @@ -59,10 +60,29 @@ export class OpenRouterProvider extends LLMBaseProvider { private visionModelsCacheExpiry: number = 0; private static readonly CACHE_DURATION_MS = 30 * 60 * 1000; // 30 minutes + private readonly envConfig = getEnvironmentConfig(); + constructor(private readonly apiKey: string) { super(); } + /** + * Get the API key with fallback hierarchy: + * 1. Constructor parameter (for backward compatibility) + * 2. localStorage (user-configured) + * 3. Build-time environment config + * 4. Empty string + */ + private getApiKey(): string { + // Constructor parameter (highest priority for backward compatibility) + if (this.apiKey && this.apiKey.trim() !== '') { + return this.apiKey.trim(); + } + + // Use environment config which handles localStorage -> build-time -> empty fallback + return this.envConfig.getApiKey('openrouter'); + } + /** * Check if a model doesn't support temperature parameter * OpenAI's GPT-5, O3, and O4 models accessed through OpenRouter don't support temperature @@ -144,7 +164,7 @@ export class OpenRouterProvider extends LLMBaseProvider { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.apiKey}`, + 'Authorization': `Bearer ${this.getApiKey()}`, 'HTTP-Referer': 'https://browseroperator.io', // Site URL for rankings on openrouter.ai 'X-Title': 'Browser Operator', // Site title for rankings on openrouter.ai }, @@ -324,7 +344,7 @@ export class OpenRouterProvider extends LLMBaseProvider { const response = await fetch(this.getToolSupportingModelsEndpoint(), { method: 'GET', headers: { - 'Authorization': `Bearer ${this.apiKey}`, + 'Authorization': `Bearer ${this.getApiKey()}`, }, }); @@ -358,7 +378,7 @@ export class OpenRouterProvider extends LLMBaseProvider { const response = await fetch(this.getVisionModelsEndpoint(), { method: 'GET', headers: { - 'Authorization': `Bearer ${this.apiKey}`, + 'Authorization': `Bearer ${this.getApiKey()}`, }, }); @@ -621,15 +641,21 @@ export class OpenRouterProvider extends LLMBaseProvider { logger.debug('=== VALIDATING OPENROUTER CREDENTIALS ==='); logger.debug('Timestamp:', new Date().toISOString()); - const storageKeys = this.getCredentialStorageKeys(); - logger.debug('Storage keys:', storageKeys); + // Use the new environment config for validation + const validationResult = this.envConfig.validateCredentials('openrouter'); + + // Enhanced logging for debugging + const apiKey = this.getApiKey(); + const source = this.envConfig.getApiKeySource('openrouter'); + const buildInfo = this.envConfig.getBuildInfo(); - const apiKey = localStorage.getItem(storageKeys.apiKey!); logger.debug('API key check:'); - logger.debug('- Storage key used:', storageKeys.apiKey); logger.debug('- API key exists:', !!apiKey); logger.debug('- API key length:', apiKey?.length || 0); logger.debug('- API key prefix:', apiKey?.substring(0, 8) + '...' || 'none'); + logger.debug('- API key source:', source); + logger.debug('- Build config available:', buildInfo.hasBuildConfig); + logger.debug('- Build time:', buildInfo.buildTime); // Also check OAuth-related storage for debugging const authMethod = localStorage.getItem('openrouter_auth_method'); @@ -638,37 +664,22 @@ export class OpenRouterProvider extends LLMBaseProvider { logger.debug('- Auth method:', authMethod); logger.debug('- OAuth token exists:', !!oauthToken); - // Check all OpenRouter-related localStorage keys - const allKeys = Object.keys(localStorage); - const openRouterKeys = allKeys.filter(key => key.includes('openrouter') || key.includes('ai_chat')); - logger.debug('All OpenRouter-related storage keys:'); - openRouterKeys.forEach(key => { - const value = localStorage.getItem(key); - logger.debug(`- ${key}:`, value?.substring(0, 50) + (value && value.length > 50 ? '...' : '') || 'null'); - }); - - if (!apiKey) { + if (validationResult.isValid) { + logger.info('✅ OpenRouter credentials validation passed'); + } else { logger.warn('❌ OpenRouter API key missing'); - return { - isValid: false, - message: 'OpenRouter API key is required. Please add your API key in Settings.', - missingItems: ['API Key'] - }; } - logger.info('✅ OpenRouter credentials validation passed'); - return { - isValid: true, - message: 'OpenRouter credentials are configured correctly.' - }; + return validationResult; } /** * Get the storage keys this provider uses for credentials */ getCredentialStorageKeys(): {apiKey: string} { + const storageKey = this.envConfig.getStorageKey('openrouter'); const keys = { - apiKey: 'ai_chat_openrouter_api_key' + apiKey: storageKey }; logger.debug('OpenRouter credential storage keys:', keys); return keys; diff --git a/front_end/panels/ai_chat/core/EnvironmentConfig.ts b/front_end/panels/ai_chat/core/EnvironmentConfig.ts new file mode 100644 index 0000000000..201b42e7f0 --- /dev/null +++ b/front_end/panels/ai_chat/core/EnvironmentConfig.ts @@ -0,0 +1,235 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * Environment Configuration Manager for AI Chat Panel + * + * This module provides unified access to API keys from multiple sources: + * 1. localStorage (user-configured, highest priority) + * 2. Build-time environment variables (Docker build args, fallback) + * 3. Empty string (no configuration available) + * + * The build-time configuration is generated during Docker build from + * environment variables and provides a secure way to inject API keys + * into the DevTools without requiring runtime configuration. + */ + +import { createLogger } from './Logger.js'; + +// Build-time configuration interface +interface BuildTimeConfig { + readonly apiKeys: { + readonly openai?: string; + readonly openrouter?: string; + readonly groq?: string; + readonly litellm?: string; + }; + readonly buildTime: string; + readonly hasKeys: boolean; +} + +// Default build configuration (used when no environment config is available) +const DEFAULT_BUILD_CONFIG: BuildTimeConfig = { + apiKeys: {}, + buildTime: 'development', + hasKeys: false +}; + +// Get build configuration - this will be replaced with actual config during Docker build +// The generate-env-config.js script will replace this entire function during build +function getBuildConfig(): BuildTimeConfig { + // Build-time configuration generated from environment variables + // Generated at: 2025-09-04T17:26:32.688Z + return { + "apiKeys": { + "openai": "sk-demo-openai", + "openrouter": "sk-or-demo-openrouter", + "groq": "gsk_demo-groq", + "litellm": "demo-litellm" + }, + "buildTime": "2025-09-04T17:26:32.688Z", + "hasKeys": true +}; +} + +const BUILD_CONFIG = getBuildConfig(); + +const logger = createLogger('EnvironmentConfig'); + +/** + * API key providers supported by the environment configuration + */ +export type APIKeyProvider = 'openai' | 'openrouter' | 'groq' | 'litellm'; + +/** + * Storage keys for localStorage API keys + */ +const STORAGE_KEYS: Record = { + openai: 'ai_chat_api_key', + openrouter: 'ai_chat_openrouter_api_key', + groq: 'ai_chat_groq_api_key', + litellm: 'ai_chat_litellm_api_key' +}; + +/** + * Environment Configuration Manager + * + * Provides unified access to API keys with fallback hierarchy: + * localStorage → build-time config → empty string + */ +export class EnvironmentConfig { + private static instance: EnvironmentConfig | null = null; + private debugLogged = false; + + private constructor() { + // Log configuration availability once for debugging + if (!this.debugLogged) { + logger.debug('Environment configuration initialized:', { + hasBuildConfig: BUILD_CONFIG?.hasKeys || false, + buildTime: BUILD_CONFIG?.buildTime || 'unknown', + availableProviders: BUILD_CONFIG ? Object.keys(BUILD_CONFIG.apiKeys) : [] + }); + this.debugLogged = true; + } + } + + /** + * Get the singleton instance of EnvironmentConfig + */ + static getInstance(): EnvironmentConfig { + if (!EnvironmentConfig.instance) { + EnvironmentConfig.instance = new EnvironmentConfig(); + } + return EnvironmentConfig.instance; + } + + /** + * Get API key for a specific provider with fallback hierarchy + * + * Priority order: + * 1. localStorage (user-configured) + * 2. Build-time environment config (Docker build args) + * 3. Empty string (no configuration) + * + * @param provider The API key provider + * @returns The API key or empty string if not available + */ + getApiKey(provider: APIKeyProvider): string { + // First check localStorage (highest priority) + const storageKey = STORAGE_KEYS[provider]; + const localStorageKey = localStorage.getItem(storageKey); + + if (localStorageKey && localStorageKey.trim() !== '') { + logger.debug(`Using localStorage API key for ${provider}`); + return localStorageKey.trim(); + } + + // Fallback to build-time configuration + if (BUILD_CONFIG?.apiKeys?.[provider]) { + logger.debug(`Using build-time API key for ${provider}`); + return BUILD_CONFIG.apiKeys[provider]; + } + + // No configuration available + logger.debug(`No API key available for ${provider}`); + return ''; + } + + /** + * Check if an API key is available for a provider + * + * @param provider The API key provider + * @returns true if an API key is available from any source + */ + hasApiKey(provider: APIKeyProvider): boolean { + return this.getApiKey(provider) !== ''; + } + + /** + * Get the source of an API key for debugging + * + * @param provider The API key provider + * @returns The source of the API key ('localStorage', 'build-time', or 'none') + */ + getApiKeySource(provider: APIKeyProvider): 'localStorage' | 'build-time' | 'none' { + const storageKey = STORAGE_KEYS[provider]; + const localStorageKey = localStorage.getItem(storageKey); + + if (localStorageKey && localStorageKey.trim() !== '') { + return 'localStorage'; + } + + if (BUILD_CONFIG?.apiKeys?.[provider]) { + return 'build-time'; + } + + return 'none'; + } + + /** + * Get storage key for a provider (for backward compatibility) + * + * @param provider The API key provider + * @returns The localStorage key used for this provider + */ + getStorageKey(provider: APIKeyProvider): string { + return STORAGE_KEYS[provider]; + } + + /** + * Validate credentials for a provider + * + * @param provider The API key provider + * @returns Validation result with details + */ + validateCredentials(provider: APIKeyProvider): { + isValid: boolean; + message: string; + source?: 'localStorage' | 'build-time'; + missingItems?: string[]; + } { + const apiKey = this.getApiKey(provider); + const source = this.getApiKeySource(provider); + + if (!apiKey) { + return { + isValid: false, + message: `${provider} API key is required. Please add your API key in Settings or configure environment variables.`, + missingItems: ['API Key'] + }; + } + + return { + isValid: true, + message: `${provider} credentials are configured correctly (source: ${source}).`, + source: source !== 'none' ? source : undefined + }; + } + + /** + * Get build configuration info for debugging + * + * @returns Build configuration metadata + */ + getBuildInfo(): { + hasBuildConfig: boolean; + buildTime: string; + availableProviders: string[]; + } { + return { + hasBuildConfig: BUILD_CONFIG?.hasKeys || false, + buildTime: BUILD_CONFIG?.buildTime || 'unknown', + availableProviders: BUILD_CONFIG ? Object.keys(BUILD_CONFIG.apiKeys) : [] + }; + } +} + +/** + * Get the global environment configuration instance + * + * @returns The EnvironmentConfig singleton + */ +export function getEnvironmentConfig(): EnvironmentConfig { + return EnvironmentConfig.getInstance(); +} \ No newline at end of file From ded87fd0b9f623551d2c19fadf09e89912cb4ef8 Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Fri, 5 Sep 2025 19:02:30 -0500 Subject: [PATCH 2/3] New way of injecting keys --- docker/.env.example | 27 ++++ docker/Dockerfile | 30 ++-- docker/docker-compose.yml | 12 +- docker/docker-entrypoint.sh | 65 ++++++++ .../entrypoints/devtools_app/devtools_app.ts | 3 + .../panels/ai_chat/core/EnvironmentConfig.ts | 147 +++++++++++++++--- front_end/panels/ai_chat/ui/SettingsDialog.ts | 14 +- 7 files changed, 248 insertions(+), 50 deletions(-) create mode 100644 docker/.env.example create mode 100644 docker/docker-entrypoint.sh diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 0000000000..00ea4e4ccb --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,27 @@ +# Environment variables for Browser Operator DevTools +# Copy this file to .env and set your API keys + +# OpenAI API Key (for GPT models) +OPENAI_API_KEY=sk-your-openai-api-key-here + +# OpenRouter API Key (for multiple LLM providers) +OPENROUTER_API_KEY=sk-or-your-openrouter-api-key-here + +# Groq API Key (for fast inference) +GROQ_API_KEY=gsk_your-groq-api-key-here + +# LiteLLM API Key (for self-hosted LLM proxy) +LITELLM_API_KEY=your-litellm-api-key-here + +# Usage: +# 1. Copy this file: cp .env.example .env +# 2. Set your API keys in the .env file +# 3. Build and run: docker-compose up --build +# +# The API keys will be embedded into the Docker image during build time +# and used as fallbacks when localStorage is empty. +# +# Priority order: +# 1. localStorage (user settings in DevTools) +# 2. Environment variables (this .env file) +# 3. Empty (no API key configured) \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 8fb21d47fe..03f59bcc2c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,17 +1,9 @@ # Multi-stage build for Chrome DevTools Frontend FROM --platform=linux/amd64 ubuntu:22.04 AS builder -# Build arguments for API keys (optional, defaults to empty) -ARG OPENAI_API_KEY="" -ARG OPENROUTER_API_KEY="" -ARG GROQ_API_KEY="" -ARG LITELLM_API_KEY="" - -# Set environment variables for build process -ENV OPENAI_API_KEY=${OPENAI_API_KEY} -ENV OPENROUTER_API_KEY=${OPENROUTER_API_KEY} -ENV GROQ_API_KEY=${GROQ_API_KEY} -ENV LITELLM_API_KEY=${LITELLM_API_KEY} +# BuildKit is required for secret mounting +# Secrets are mounted at build time but not stored in layers +# Usage: DOCKER_BUILDKIT=1 docker-compose build # Install required packages RUN apt-get update && apt-get install -y \ @@ -59,7 +51,12 @@ RUN git remote add upstream https://github.com/BrowserOperator/browser-operator- RUN git fetch upstream RUN git checkout upstream/main -# Build Browser Operator version +# Copy our local modifications into the container +COPY front_end/panels/ai_chat/core/EnvironmentConfig.ts /workspace/devtools/devtools-frontend/front_end/panels/ai_chat/core/EnvironmentConfig.ts +COPY front_end/panels/ai_chat/ui/SettingsDialog.ts /workspace/devtools/devtools-frontend/front_end/panels/ai_chat/ui/SettingsDialog.ts +COPY front_end/entrypoints/devtools_app/devtools_app.ts /workspace/devtools/devtools-frontend/front_end/entrypoints/devtools_app/devtools_app.ts + +# Build Browser Operator version with our modifications RUN npm run build # Production stage @@ -71,4 +68,11 @@ COPY --from=builder /workspace/devtools/devtools-frontend/out/Default/gen/front_ # Copy nginx config COPY docker/nginx.conf /etc/nginx/conf.d/default.conf -EXPOSE 8000 \ No newline at end of file +# Copy and setup entrypoint script for runtime configuration +COPY docker/docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh + +EXPOSE 8000 + +# Use entrypoint to generate runtime config and start nginx +ENTRYPOINT ["/docker-entrypoint.sh"] \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 138efe099b..efea122ad2 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -5,13 +5,6 @@ services: build: context: .. dockerfile: docker/Dockerfile - args: - # API keys passed from host environment variables - # These can be set via .env file or exported in shell - OPENAI_API_KEY: ${OPENAI_API_KEY:-} - OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} - GROQ_API_KEY: ${GROQ_API_KEY:-} - LITELLM_API_KEY: ${LITELLM_API_KEY:-} image: devtools-frontend:latest container_name: devtools-frontend ports: @@ -25,9 +18,12 @@ services: environment: - NGINX_HOST=localhost - NGINX_PORT=8000 - # Optional: Pass API keys to runtime for debugging (not required for functionality) + # Runtime API key injection - keys are passed to container at runtime + # These are injected into the DevTools frontend via runtime-config.js - OPENAI_API_KEY=${OPENAI_API_KEY:-} - OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-} + - GROQ_API_KEY=${GROQ_API_KEY:-} + - LITELLM_API_KEY=${LITELLM_API_KEY:-} healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8000/"] interval: 30s diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100644 index 0000000000..20adf0674f --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,65 @@ +#!/bin/sh +# Docker entrypoint script for Browser Operator DevTools +# This script generates runtime configuration from environment variables +# and starts nginx to serve the DevTools frontend + +set -e + +# Log configuration status to container logs +echo "DevTools runtime configuration generated:" +[ -n "${OPENAI_API_KEY}" ] && echo " ✓ OPENAI_API_KEY configured" || echo " ✗ OPENAI_API_KEY not set" +[ -n "${OPENROUTER_API_KEY}" ] && echo " ✓ OPENROUTER_API_KEY configured" || echo " ✗ OPENROUTER_API_KEY not set" +[ -n "${GROQ_API_KEY}" ] && echo " ✓ GROQ_API_KEY configured" || echo " ✗ GROQ_API_KEY not set" +[ -n "${LITELLM_API_KEY}" ] && echo " ✓ LITELLM_API_KEY configured" || echo " ✗ LITELLM_API_KEY not set" + +# Inject API keys directly into DevTools JavaScript files +echo "🔧 Injecting API keys directly into JavaScript files..." + +# Find and modify the main DevTools entry point files +for js_file in /usr/share/nginx/html/entrypoints/*/devtools_app.js /usr/share/nginx/html/entrypoints/*/inspector_main.js; do + if [ -f "$js_file" ]; then + echo " ✓ Injecting into $(basename "$js_file")" + # Inject a global configuration object at the beginning of the file + sed -i '1i\ +// Runtime API key injection\ +window.__RUNTIME_CONFIG__ = {\ + OPENAI_API_KEY: "'"${OPENAI_API_KEY:-}"'",\ + OPENROUTER_API_KEY: "'"${OPENROUTER_API_KEY:-}"'",\ + GROQ_API_KEY: "'"${GROQ_API_KEY:-}"'",\ + LITELLM_API_KEY: "'"${LITELLM_API_KEY:-}"'",\ + timestamp: "'"$(date -Iseconds)"'",\ + source: "docker-runtime"\ +};\ +// Auto-save to localStorage\ +if (window.__RUNTIME_CONFIG__.OPENAI_API_KEY) localStorage.setItem("ai_chat_api_key", window.__RUNTIME_CONFIG__.OPENAI_API_KEY);\ +if (window.__RUNTIME_CONFIG__.OPENROUTER_API_KEY) localStorage.setItem("ai_chat_openrouter_api_key", window.__RUNTIME_CONFIG__.OPENROUTER_API_KEY);\ +if (window.__RUNTIME_CONFIG__.GROQ_API_KEY) localStorage.setItem("ai_chat_groq_api_key", window.__RUNTIME_CONFIG__.GROQ_API_KEY);\ +if (window.__RUNTIME_CONFIG__.LITELLM_API_KEY) localStorage.setItem("ai_chat_litellm_api_key", window.__RUNTIME_CONFIG__.LITELLM_API_KEY);\ +console.log("[RUNTIME-CONFIG] API keys injected directly into JavaScript:", {hasOpenAI: !!window.__RUNTIME_CONFIG__.OPENAI_API_KEY, hasOpenRouter: !!window.__RUNTIME_CONFIG__.OPENROUTER_API_KEY, hasGroq: !!window.__RUNTIME_CONFIG__.GROQ_API_KEY, hasLiteLLM: !!window.__RUNTIME_CONFIG__.LITELLM_API_KEY});\ +' "$js_file" + fi +done + +# Also inject into AI Chat panel files specifically +for js_file in /usr/share/nginx/html/panels/ai_chat/*.js; do + if [ -f "$js_file" ]; then + echo " ✓ Injecting into AI Chat panel $(basename "$js_file")" + sed -i '1i\ +// Runtime API key injection for AI Chat\ +if (typeof window !== "undefined" && !window.__RUNTIME_CONFIG__) {\ + window.__RUNTIME_CONFIG__ = {\ + OPENAI_API_KEY: "'"${OPENAI_API_KEY:-}"'",\ + OPENROUTER_API_KEY: "'"${OPENROUTER_API_KEY:-}"'",\ + GROQ_API_KEY: "'"${GROQ_API_KEY:-}"'",\ + LITELLM_API_KEY: "'"${LITELLM_API_KEY:-}"'",\ + timestamp: "'"$(date -Iseconds)"'",\ + source: "docker-runtime"\ + };\ +}\ +' "$js_file" + fi +done + +# Start nginx in foreground +echo "Starting nginx..." +exec nginx -g 'daemon off;' \ No newline at end of file diff --git a/front_end/entrypoints/devtools_app/devtools_app.ts b/front_end/entrypoints/devtools_app/devtools_app.ts index d989aa0326..ebc5c90df5 100644 --- a/front_end/entrypoints/devtools_app/devtools_app.ts +++ b/front_end/entrypoints/devtools_app/devtools_app.ts @@ -32,6 +32,9 @@ import '../../panels/ai_chat/ai_chat-meta.js'; import * as Root from '../../core/root/root.js'; import * as Main from '../main/main.js'; +// Runtime config is loaded via HTML script injection (docker-entrypoint.sh) +// No need for dynamic loading since it's already in the HTML head + // @ts-expect-error Exposed for legacy layout tests self.runtime = Root.Runtime.Runtime.instance({forceNew: true}); new Main.MainImpl.MainImpl(); diff --git a/front_end/panels/ai_chat/core/EnvironmentConfig.ts b/front_end/panels/ai_chat/core/EnvironmentConfig.ts index 201b42e7f0..72c5f9c4ea 100644 --- a/front_end/panels/ai_chat/core/EnvironmentConfig.ts +++ b/front_end/panels/ai_chat/core/EnvironmentConfig.ts @@ -7,12 +7,16 @@ * * This module provides unified access to API keys from multiple sources: * 1. localStorage (user-configured, highest priority) - * 2. Build-time environment variables (Docker build args, fallback) - * 3. Empty string (no configuration available) + * 2. Runtime environment variables (Docker runtime injection) + * 3. Build-time environment variables (Docker build args, fallback) + * 4. Empty string (no configuration available) * - * The build-time configuration is generated during Docker build from - * environment variables and provides a secure way to inject API keys - * into the DevTools without requiring runtime configuration. + * SECURITY NOTICE: + * - NEVER commit real API keys to source control + * - Runtime injection happens at container start (not in image) + * - Build-time injection is for LOCAL/DEV environments only + * - Production deployments should use server-side proxy/OAuth + * - The getBuildConfig() function returns safe defaults (no keys) */ import { createLogger } from './Logger.js'; @@ -36,27 +40,51 @@ const DEFAULT_BUILD_CONFIG: BuildTimeConfig = { hasKeys: false }; -// Get build configuration - this will be replaced with actual config during Docker build -// The generate-env-config.js script will replace this entire function during build +// Get build configuration - replaced at Docker build for local/dev only. +// IMPORTANT: Do not commit real keys. The default returns no keys. function getBuildConfig(): BuildTimeConfig { - // Build-time configuration generated from environment variables - // Generated at: 2025-09-04T17:26:32.688Z - return { - "apiKeys": { - "openai": "sk-demo-openai", - "openrouter": "sk-or-demo-openrouter", - "groq": "gsk_demo-groq", - "litellm": "demo-litellm" - }, - "buildTime": "2025-09-04T17:26:32.688Z", - "hasKeys": true -}; + return DEFAULT_BUILD_CONFIG; } const BUILD_CONFIG = getBuildConfig(); const logger = createLogger('EnvironmentConfig'); +// Runtime configuration interface (injected by Docker at container start) +interface RuntimeConfig { + OPENAI_API_KEY?: string; + OPENROUTER_API_KEY?: string; + GROQ_API_KEY?: string; + LITELLM_API_KEY?: string; + timestamp?: string; + source?: string; +} + +// Get runtime configuration if available (from Docker runtime injection) +function getRuntimeConfig(): RuntimeConfig | null { + if (typeof window === 'undefined') { + return null; + } + + // @ts-ignore - __RUNTIME_CONFIG__ is injected by Docker entrypoint + if (window.__RUNTIME_CONFIG__) { + // @ts-ignore + const config = window.__RUNTIME_CONFIG__ as RuntimeConfig; + console.log('Runtime config found:', { + hasOpenAI: Boolean(config.OPENAI_API_KEY), + hasOpenRouter: Boolean(config.OPENROUTER_API_KEY), + hasGroq: Boolean(config.GROQ_API_KEY), + hasLiteLLM: Boolean(config.LITELLM_API_KEY), + timestamp: config.timestamp, + source: config.source + }); + return config; + } + + console.log('Runtime config not found on window object'); + return null; +} + /** * API key providers supported by the environment configuration */ @@ -81,11 +109,19 @@ const STORAGE_KEYS: Record = { export class EnvironmentConfig { private static instance: EnvironmentConfig | null = null; private debugLogged = false; + private runtimeConfig: RuntimeConfig | null = null; private constructor() { + // Get runtime configuration if available + this.runtimeConfig = getRuntimeConfig(); + + // Auto-save runtime config to localStorage if not already present + this.initializeFromRuntime(); + // Log configuration availability once for debugging if (!this.debugLogged) { logger.debug('Environment configuration initialized:', { + hasRuntimeConfig: Boolean(this.runtimeConfig), hasBuildConfig: BUILD_CONFIG?.hasKeys || false, buildTime: BUILD_CONFIG?.buildTime || 'unknown', availableProviders: BUILD_CONFIG ? Object.keys(BUILD_CONFIG.apiKeys) : [] @@ -94,6 +130,57 @@ export class EnvironmentConfig { } } + /** + * Initialize API keys from runtime config if available + * Saves runtime-injected keys to localStorage if not already present + */ + private initializeFromRuntime(): void { + if (!this.runtimeConfig) { + return; + } + + const providers: APIKeyProvider[] = ['openai', 'openrouter', 'groq', 'litellm']; + let savedCount = 0; + + for (const provider of providers) { + const storageKey = STORAGE_KEYS[provider]; + const existingKey = localStorage.getItem(storageKey); + + // Only save if not already in localStorage + if (!existingKey || existingKey.trim() === '') { + const runtimeKey = this.getRuntimeKey(provider); + if (runtimeKey) { + localStorage.setItem(storageKey, runtimeKey); + savedCount++; + logger.debug(`Saved runtime API key to localStorage for ${provider}`); + } + } + } + + if (savedCount > 0) { + logger.info(`Initialized ${savedCount} API keys from Docker runtime configuration`); + } + } + + /** + * Get API key from runtime config + */ + private getRuntimeKey(provider: APIKeyProvider): string { + if (!this.runtimeConfig) { + return ''; + } + + const keyMap: Record = { + openai: 'OPENAI_API_KEY', + openrouter: 'OPENROUTER_API_KEY', + groq: 'GROQ_API_KEY', + litellm: 'LITELLM_API_KEY' + }; + + const key = this.runtimeConfig[keyMap[provider]]; + return (key && key.trim() !== '') ? key.trim() : ''; + } + /** * Get the singleton instance of EnvironmentConfig */ @@ -109,8 +196,9 @@ export class EnvironmentConfig { * * Priority order: * 1. localStorage (user-configured) - * 2. Build-time environment config (Docker build args) - * 3. Empty string (no configuration) + * 2. Runtime configuration (Docker runtime injection) + * 3. Build-time environment config (Docker build args) + * 4. Empty string (no configuration) * * @param provider The API key provider * @returns The API key or empty string if not available @@ -125,6 +213,15 @@ export class EnvironmentConfig { return localStorageKey.trim(); } + // Check runtime configuration (Docker runtime injection) + const runtimeKey = this.getRuntimeKey(provider); + if (runtimeKey) { + logger.debug(`Using runtime API key for ${provider}`); + // Also save to localStorage for future use + localStorage.setItem(storageKey, runtimeKey); + return runtimeKey; + } + // Fallback to build-time configuration if (BUILD_CONFIG?.apiKeys?.[provider]) { logger.debug(`Using build-time API key for ${provider}`); @@ -150,9 +247,9 @@ export class EnvironmentConfig { * Get the source of an API key for debugging * * @param provider The API key provider - * @returns The source of the API key ('localStorage', 'build-time', or 'none') + * @returns The source of the API key ('localStorage', 'runtime', 'build-time', or 'none') */ - getApiKeySource(provider: APIKeyProvider): 'localStorage' | 'build-time' | 'none' { + getApiKeySource(provider: APIKeyProvider): 'localStorage' | 'runtime' | 'build-time' | 'none' { const storageKey = STORAGE_KEYS[provider]; const localStorageKey = localStorage.getItem(storageKey); @@ -160,6 +257,10 @@ export class EnvironmentConfig { return 'localStorage'; } + if (this.getRuntimeKey(provider)) { + return 'runtime'; + } + if (BUILD_CONFIG?.apiKeys?.[provider]) { return 'build-time'; } diff --git a/front_end/panels/ai_chat/ui/SettingsDialog.ts b/front_end/panels/ai_chat/ui/SettingsDialog.ts index 91f7739c2c..eb64f2ae03 100644 --- a/front_end/panels/ai_chat/ui/SettingsDialog.ts +++ b/front_end/panels/ai_chat/ui/SettingsDialog.ts @@ -8,6 +8,7 @@ import { getEvaluationConfig, setEvaluationConfig, isEvaluationEnabled, connectT import { createLogger } from '../core/Logger.js'; import { LLMClient } from '../LLM/LLMClient.js'; import { getTracingConfig, setTracingConfig, isTracingEnabled } from '../tracing/TracingConfig.js'; +import { getEnvironmentConfig } from '../core/EnvironmentConfig.js'; import { DEFAULT_PROVIDER_MODELS } from './AIChatPanel.js'; @@ -525,7 +526,7 @@ export class SettingsDialog { } } else if (selectedProvider === 'groq') { // If switching to Groq, fetch models if API key is configured - const groqApiKey = groqApiKeyInput.value.trim() || localStorage.getItem('ai_chat_groq_api_key') || ''; + const groqApiKey = groqApiKeyInput.value.trim() || envConfig.getApiKey('groq'); if (groqApiKey) { try { @@ -544,7 +545,7 @@ export class SettingsDialog { } } else if (selectedProvider === 'openrouter') { // If switching to OpenRouter, fetch models if API key is configured - const openrouterApiKey = openrouterApiKeyInput.value.trim() || localStorage.getItem('ai_chat_openrouter_api_key') || ''; + const openrouterApiKey = openrouterApiKeyInput.value.trim() || envConfig.getApiKey('openrouter'); if (openrouterApiKey) { try { @@ -600,7 +601,8 @@ export class SettingsDialog { apiKeyHint.textContent = i18nString(UIStrings.apiKeyHint); openaiSettingsSection.appendChild(apiKeyHint); - const settingsSavedApiKey = localStorage.getItem('ai_chat_api_key') || ''; + const envConfig = getEnvironmentConfig(); + const settingsSavedApiKey = envConfig.getApiKey('openai'); const settingsApiKeyInput = document.createElement('input'); settingsApiKeyInput.className = 'settings-input'; settingsApiKeyInput.type = 'password'; @@ -710,7 +712,7 @@ export class SettingsDialog { litellmAPIKeyHint.textContent = i18nString(UIStrings.liteLLMApiKeyHint); litellmSettingsSection.appendChild(litellmAPIKeyHint); - const settingsSavedLiteLLMApiKey = localStorage.getItem(LITELLM_API_KEY_STORAGE_KEY) || ''; + const settingsSavedLiteLLMApiKey = envConfig.getApiKey('litellm'); const litellmApiKeyInput = document.createElement('input'); litellmApiKeyInput.className = 'settings-input litellm-api-key-input'; litellmApiKeyInput.type = 'password'; @@ -1266,7 +1268,7 @@ export class SettingsDialog { groqApiKeyHint.textContent = i18nString(UIStrings.groqApiKeyHint); groqSettingsSection.appendChild(groqApiKeyHint); - const settingsSavedGroqApiKey = localStorage.getItem(GROQ_API_KEY_STORAGE_KEY) || ''; + const settingsSavedGroqApiKey = envConfig.getApiKey('groq'); const groqApiKeyInput = document.createElement('input'); groqApiKeyInput.className = 'settings-input groq-api-key-input'; groqApiKeyInput.type = 'password'; @@ -1452,7 +1454,7 @@ export class SettingsDialog { openrouterApiKeyHint.textContent = i18nString(UIStrings.openrouterApiKeyHint); openrouterSettingsSection.appendChild(openrouterApiKeyHint); - const settingsSavedOpenRouterApiKey = localStorage.getItem(OPENROUTER_API_KEY_STORAGE_KEY) || ''; + const settingsSavedOpenRouterApiKey = envConfig.getApiKey('openrouter'); const openrouterApiKeyInput = document.createElement('input'); openrouterApiKeyInput.className = 'settings-input openrouter-api-key-input'; openrouterApiKeyInput.type = 'password'; From c2753477bb0fb4e1233ea5fda6e7ab97eeee93c1 Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Fri, 5 Sep 2025 19:38:11 -0500 Subject: [PATCH 3/3] Cleanup and improvements --- docker/Dockerfile | 8 ++------ front_end/panels/ai_chat/LLM/GroqProvider.ts | 4 ++-- front_end/panels/ai_chat/LLM/OpenRouterProvider.ts | 3 +-- front_end/panels/ai_chat/core/EnvironmentConfig.ts | 3 ++- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 03f59bcc2c..c2fbd32584 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -51,12 +51,8 @@ RUN git remote add upstream https://github.com/BrowserOperator/browser-operator- RUN git fetch upstream RUN git checkout upstream/main -# Copy our local modifications into the container -COPY front_end/panels/ai_chat/core/EnvironmentConfig.ts /workspace/devtools/devtools-frontend/front_end/panels/ai_chat/core/EnvironmentConfig.ts -COPY front_end/panels/ai_chat/ui/SettingsDialog.ts /workspace/devtools/devtools-frontend/front_end/panels/ai_chat/ui/SettingsDialog.ts -COPY front_end/entrypoints/devtools_app/devtools_app.ts /workspace/devtools/devtools-frontend/front_end/entrypoints/devtools_app/devtools_app.ts - -# Build Browser Operator version with our modifications +# Build Browser Operator version (using upstream code to avoid TypeScript compilation issues) +# Runtime API key injection will be handled by the entrypoint script RUN npm run build # Production stage diff --git a/front_end/panels/ai_chat/LLM/GroqProvider.ts b/front_end/panels/ai_chat/LLM/GroqProvider.ts index 1e9cf86fa5..410ea2f36d 100644 --- a/front_end/panels/ai_chat/LLM/GroqProvider.ts +++ b/front_end/panels/ai_chat/LLM/GroqProvider.ts @@ -54,8 +54,8 @@ export class GroqProvider extends LLMBaseProvider { */ private getApiKey(): string { // Constructor parameter (highest priority for backward compatibility) - if (this.getApiKey() && this.getApiKey().trim() !== '') { - return this.getApiKey().trim(); + if (this.apiKey && this.apiKey.trim() !== '') { + return this.apiKey.trim(); } // Use environment config which handles localStorage -> build-time -> empty fallback diff --git a/front_end/panels/ai_chat/LLM/OpenRouterProvider.ts b/front_end/panels/ai_chat/LLM/OpenRouterProvider.ts index 055a3421db..2714933226 100644 --- a/front_end/panels/ai_chat/LLM/OpenRouterProvider.ts +++ b/front_end/panels/ai_chat/LLM/OpenRouterProvider.ts @@ -651,8 +651,7 @@ export class OpenRouterProvider extends LLMBaseProvider { logger.debug('API key check:'); logger.debug('- API key exists:', !!apiKey); - logger.debug('- API key length:', apiKey?.length || 0); - logger.debug('- API key prefix:', apiKey?.substring(0, 8) + '...' || 'none'); + logger.debug('- API key presence:', apiKey ? '' : 'none'); logger.debug('- API key source:', source); logger.debug('- Build config available:', buildInfo.hasBuildConfig); logger.debug('- Build time:', buildInfo.buildTime); diff --git a/front_end/panels/ai_chat/core/EnvironmentConfig.ts b/front_end/panels/ai_chat/core/EnvironmentConfig.ts index 72c5f9c4ea..a53bd50315 100644 --- a/front_end/panels/ai_chat/core/EnvironmentConfig.ts +++ b/front_end/panels/ai_chat/core/EnvironmentConfig.ts @@ -261,7 +261,8 @@ export class EnvironmentConfig { return 'runtime'; } - if (BUILD_CONFIG?.apiKeys?.[provider]) { + if (typeof BUILD_CONFIG?.apiKeys?.[provider] === 'string' && + BUILD_CONFIG.apiKeys[provider].trim() !== '') { return 'build-time'; }