Skip to content

Conversation

@olesho
Copy link
Collaborator

@olesho olesho commented Sep 4, 2025

Summary by CodeRabbit

  • New Features

    • Unified EnvironmentConfig for AI Chat; providers and Settings UI now read API keys from localStorage, runtime, or build-time fallbacks and report key sources.
    • Frontend accepts runtime-injected API keys and persists them to localStorage when present.
  • Chores

    • Docker entrypoint script injects runtime config into frontend and starts nginx; added .env example, Docker Compose env vars for API keys, disabled default host-volume mount, and BuildKit/runtime notes.

@olesho olesho requested a review from tysonthomas9 September 4, 2025 23:12
@coderabbitai
Copy link

coderabbitai bot commented Sep 4, 2025

Walkthrough

Adds a centralized EnvironmentConfig for resolving API keys (localStorage → runtime-injected window → build-time → none), updates AI Chat providers and SettingsDialog to use it, and introduces Docker runtime injection via a new entrypoint plus compose/.env support and Dockerfile runtime ENTRYPOINT changes.

Changes

Cohort / File(s) Summary
Docker build & runtime
docker/Dockerfile, docker/docker-entrypoint.sh, docker/.env.example, docker/docker-compose.yml
Dockerfile: BuildKit notes; copies entrypoint, sets ENTRYPOINT to /docker-entrypoint.sh, makes it executable, exposes port 8000. Added docker/docker-entrypoint.sh which injects window.__RUNTIME_CONFIG__ (OPENAI/OPENROUTER/GROQ/LITELLM) into frontend JS and starts nginx. Added docker/.env.example with placeholder keys. docker-compose.yml: host-volume mount commented out and runtime env vars for the four API keys added (default empty).
New environment manager (AI Chat)
front_end/panels/ai_chat/core/EnvironmentConfig.ts
New singleton EnvironmentConfig that reads API keys from localStorage, runtime window config, and build-time config (priority order), exposes getApiKey/hasApiKey/getApiKeySource/getStorageKey/validateCredentials/getBuildInfo, and provides getEnvironmentConfig().
Build wiring
front_end/panels/ai_chat/BUILD.gn
Added core/EnvironmentConfig.ts to ai_chat sources so it is compiled and included in release metadata.
Provider integrations
front_end/panels/ai_chat/LLM/OpenAIProvider.ts, front_end/panels/ai_chat/LLM/OpenRouterProvider.ts, front_end/panels/ai_chat/LLM/GroqProvider.ts
Providers now use getEnvironmentConfig() and a private getApiKey() fallback chain; Authorization headers and credential validation/storage key retrieval use EnvironmentConfig. OpenRouter includes enhanced logging and vision-model detection fallback/caching. Public APIs unchanged.
Settings & entrypoint comments
front_end/panels/ai_chat/ui/SettingsDialog.ts, front_end/entrypoints/devtools_app/devtools_app.ts
SettingsDialog reads initial API key values from EnvironmentConfig (falling back to inputs/localStorage); model-selector param renamed to selectorType and assigned via data attribute. devtools_app.ts adds comments noting runtime config is injected into HTML head.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI as AI Chat UI
  participant Provider as LLM Provider
  participant Env as EnvironmentConfig
  participant LS as localStorage
  participant Win as window.__RUNTIME_CONFIG__
  participant Build as Build-Time Config
  participant API as LLM Service API

  User->>UI: Trigger request
  UI->>Provider: Use provider instance
  Provider->>Env: getApiKey(provider)
  alt Key in localStorage
    Env->>LS: read key
    LS-->>Env: apiKey
    Env-->>Provider: apiKey (source: localStorage)
  else Key in runtime
    Env->>Win: read runtime config
    Win-->>Env: apiKey
    Env-->>Provider: apiKey (source: runtime)
  else Key in build-time
    Env->>Build: read build config
    Build-->>Env: apiKey
    Env-->>Provider: apiKey (source: build-time)
  else No key
    Env-->>Provider: "" (source: none)
  end
  Provider->>API: HTTP request with Authorization: Bearer <apiKey>
  API-->>Provider: response/error
  Provider-->>UI: result/error
Loading
sequenceDiagram
  autonumber
  participant Dev as Developer
  participant DockerBuild as Docker Build
  participant Image as Image
  participant Compose as docker-compose (runtime)
  participant Nginx as nginx in container

  Dev->>DockerBuild: docker build (with BuildKit)
  DockerBuild->>Image: copy files, run npm build, embed build-time config
  Dev->>Compose: docker compose up (env vars passed)
  Compose->>Image: environment OPENAI_API_KEY, OPENROUTER_API_KEY, GROQ_API_KEY, LITELLM_API_KEY
  Image->>Nginx: on start, entrypoint injects window.__RUNTIME_CONFIG__ into frontend JS
  Nginx->>Clients: serve assets with runtime-injected keys
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I nibble on configs, keys in a row,
From local leaves, then runtime winds blow.
Build-time crumbs wait if none are found,
I tuck them in storage safe and sound.
Panels compiled; nginx hums — hop round! 🥕🐇

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/docker--api-keys

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link

claude bot commented Sep 4, 2025

Claude finished @olesho's task —— View job


PR Review: Pass API keys for docker build

Todo List:

  • Read repository setup instructions (CLAUDE.md)
  • Examine Docker configuration changes
  • Review TypeScript LLM provider changes
  • Analyze new EnvironmentConfig.ts file
  • Check for security concerns with API key handling
  • Assess code quality and best practices
  • Provide comprehensive feedback

🔒 Security Analysis

⚠️ Critical Security Issues

1. Hardcoded Demo API Keys in Production Code

  • Location: front_end/panels/ai_chat/core/EnvironmentConfig.ts:46-49
  • Issue: Demo API keys are hardcoded in the getBuildConfig() function
"apiKeys": {
    "openai": "sk-demo-openai",
    "openrouter": "sk-or-demo-openrouter", 
    "groq": "gsk_demo-groq",
    "litellm": "demo-litellm"
}
  • Risk: These demo keys could be accidentally used in production builds
  • Recommendation: Remove hardcoded keys and ensure this function returns DEFAULT_BUILD_CONFIG when no environment variables are available

2. API Keys in Docker Build Logs

  • Location: docker/Dockerfile:5-14
  • Issue: Build arguments may be visible in build logs and image history
  • Risk: Sensitive API keys could be exposed through docker history command
  • Recommendation: Use multi-stage builds with secrets mount or BuildKit secrets

✅ Security Best Practices Implemented

1. Proper Fallback Hierarchy

  • localStorage (user input) → build-time config → empty string
  • Prevents accidental exposure of sensitive data

2. Build-time vs Runtime Separation

  • API keys are injected at build time, not stored in final image
  • Docker compose properly handles environment variable passing

🏗️ Code Quality Assessment

✅ Strengths

1. Well-Structured Environment Config

  • Clean singleton pattern implementation
  • Comprehensive API key management with multiple fallback sources
  • Good separation of concerns

2. Consistent Integration Pattern

  • All LLM providers follow the same pattern for API key handling
  • Backward compatibility maintained with constructor parameters

3. Proper Error Handling

  • Comprehensive validation methods
  • Clear error messages for missing credentials

⚠️ Code Quality Issues

1. Recursive Function Call Bug

  • Location: front_end/panels/ai_chat/LLM/GroqProvider.ts:57-58
private getApiKey(): string {
    if (this.getApiKey() && this.getApiKey().trim() !== '') { // ❌ Infinite recursion
        return this.getApiKey().trim();
    }
}
  • Fix: Should be this.apiKey instead of this.getApiKey()

2. Inconsistent Validation Methods

  • Location: front_end/panels/ai_chat/LLM/OpenAIProvider.ts:544-560
  • Issue: OpenAIProvider doesn't use the new EnvironmentConfig for validation like other providers
  • Recommendation: Standardize to use this.envConfig.validateCredentials('openai')

3. Missing API Key Source in Response

  • Location: Multiple providers
  • Issue: Some validation responses don't include the source field consistently
  • Impact: Makes debugging credential issues more difficult

🚀 Performance Considerations

✅ Good Practices

  • Singleton pattern prevents multiple config instantiations
  • Caching implemented for vision model detection (OpenRouterProvider)
  • Lazy initialization of environment config

📝 Minor Optimizations

  • Debug logging could be made conditional to reduce overhead in production
  • Vision model cache could be shared across provider instances

🧪 Test Coverage

❌ Missing Test Coverage

  • No unit tests for the new EnvironmentConfig class
  • No tests for API key fallback hierarchy
  • No tests for validation methods
  • Docker build integration not tested

📋 Recommended Test Cases

  1. EnvironmentConfig fallback hierarchy testing
  2. API key validation edge cases
  3. Build-time configuration injection
  4. Provider credential validation consistency

📚 Documentation & Maintainability

✅ Good Documentation

  • Comprehensive JSDoc comments
  • Clear explanation of fallback hierarchy
  • Build process documented in Docker files

📝 Areas for Improvement

  • README should document how to pass API keys during Docker build
  • Environment variable naming conventions could be documented
  • Migration guide for existing users would be helpful

🛠️ Specific Recommendations

High Priority (Must Fix)

  1. Remove hardcoded demo keys from EnvironmentConfig.ts
  2. Fix infinite recursion bug in GroqProvider.ts:57-58
  3. Standardize validation across all providers to use EnvironmentConfig

Medium Priority (Should Fix)

  1. Use Docker BuildKit secrets for more secure API key handling
  2. Add unit tests for EnvironmentConfig class
  3. Implement consistent error handling across all providers

Low Priority (Nice to Have)

  1. Add migration documentation
  2. Implement conditional debug logging
  3. Share vision model cache across provider instances

✅ Overall Assessment

This PR successfully implements a robust API key management system for Docker builds. The core architecture is sound with good separation of concerns and proper fallback mechanisms. However, there are critical security issues (hardcoded demo keys) and a significant bug (infinite recursion) that must be addressed before merging.

Recommendation: Request changes to fix the critical issues, then this will be ready for merge.


Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
front_end/panels/ai_chat/LLM/OpenAIProvider.ts (2)

544-560: Delegate credential validation to EnvironmentConfig for consistency.

Other providers rely on env config; this one still reads localStorage directly.

-  validateCredentials(): {isValid: boolean, message: string, missingItems?: string[]} {
-    const storageKeys = this.getCredentialStorageKeys();
-    const apiKey = localStorage.getItem(storageKeys.apiKey!);
-    
-    if (!apiKey) {
-      return {
-        isValid: false,
-        message: 'OpenAI API key is required. Please add your API key in Settings.',
-        missingItems: ['API Key']
-      };
-    }
-    
-    return {
-      isValid: true,
-      message: 'OpenAI credentials are configured correctly.'
-    };
-  }
+  validateCredentials(): {isValid: boolean, message: string, missingItems?: string[]} {
+    return this.envConfig.validateCredentials('openai');
+  }

565-569: Use centralized storage key from EnvironmentConfig.

-  getCredentialStorageKeys(): {apiKey: string} {
-    return {
-      apiKey: 'ai_chat_api_key'
-    };
-  }
+  getCredentialStorageKeys(): {apiKey: string} {
+    return { apiKey: this.envConfig.getStorageKey('openai') };
+  }
front_end/panels/ai_chat/LLM/OpenRouterProvider.ts (1)

159-173: Add request timeout and remove unnecessary quotes on header key.

Current fetches have no timeout; add AbortController. Also drop quotes on Authorization to satisfy lint.

-  private async makeAPIRequest(endpoint: string, payloadBody: any): Promise<any> {
+  private async makeAPIRequest(endpoint: string, payloadBody: any): Promise<any> {
     try {
       logger.debug('Making OpenRouter API request to:', endpoint);
-
-      const response = await fetch(endpoint, {
+      const controller = new AbortController();
+      const timeout = setTimeout(() => controller.abort(), 30000); // 30s
+      const response = await fetch(endpoint, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
-          'Authorization': `Bearer ${this.getApiKey()}`,
+          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
         },
-        body: JSON.stringify(payloadBody),
+        body: JSON.stringify(payloadBody),
+        signal: controller.signal,
       });
+      clearTimeout(timeout);

Also applies to: 167-170

🧹 Nitpick comments (16)
front_end/panels/ai_chat/LLM/OpenAIProvider.ts (3)

5-11: Fix import order per lint rule.

Place value imports before type-only imports to satisfy import/order.

-import type { LLMMessage, LLMResponse, LLMCallOptions, LLMProvider, ModelInfo, MessageContent } from './LLMTypes.js';
+import { getEnvironmentConfig } from '../core/EnvironmentConfig.js';
+import type { LLMMessage, LLMResponse, LLMCallOptions, LLMProvider, ModelInfo, MessageContent } from './LLMTypes.js';
-import { getEnvironmentConfig } from '../core/EnvironmentConfig.js';

52-68: API key fallback hierarchy — LGTM with one improvement.

Logic is sound. Add an early guard to avoid sending empty Bearer headers later.

   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');
+    return this.envConfig.getApiKey('openai')?.trim() ?? '';
   }

64-64: Remove trailing whitespace to satisfy lint.

There’s trailing space on this line; strip it to keep CI green.

front_end/panels/ai_chat/LLM/GroqProvider.ts (2)

10-11: Fix import order per lint rule.

Place value imports before type-only imports.

-import type { LLMMessage, LLMResponse, LLMCallOptions, LLMProvider, ModelInfo } from './LLMTypes.js';
+import { getEnvironmentConfig } from '../core/EnvironmentConfig.js';
+import type { LLMMessage, LLMResponse, LLMCallOptions, LLMProvider, ModelInfo } from './LLMTypes.js';
-import { getEnvironmentConfig } from '../core/EnvironmentConfig.js';

60-60: Remove trailing whitespace to satisfy lint.

Minor, but keeps lint clean.

docker/docker-compose.yml (1)

20-24: Clean up duplicated commented volumes block.

There are two commented “volumes” keys. Keep a single commented example to avoid confusion.

-    # volumes:
-      # For development: mount the built files directly (optional)
-      # Uncomment the lines below to use local files instead of container files
-      # volumes:
-      #   - ../out/Default/gen/front_end:/usr/share/nginx/html:ro
+    # For development: mount the built files directly (optional)
+    # volumes:
+    #   - ../out/Default/gen/front_end:/usr/share/nginx/html:ro
front_end/panels/ai_chat/LLM/OpenRouterProvider.ts (5)

10-10: Fix import order to satisfy lint (value imports before type-only).

-import type { LLMMessage, LLMResponse, LLMCallOptions, LLMProvider, ModelInfo } from './LLMTypes.js';
-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';
+import { getEnvironmentConfig } from '../core/EnvironmentConfig.js';
+import { LLMBaseProvider } from './LLMProvider.js';
+import { LLMRetryManager } from './LLMErrorHandler.js';
+import { LLMResponseParser } from './LLMResponseParser.js';
+import { createLogger } from '../core/Logger.js';
+import type { LLMMessage, LLMResponse, LLMCallOptions, LLMProvider, ModelInfo } from './LLMTypes.js';

297-301: Avoid logging full request payload (may contain user data).

Redact messages or log only metadata to prevent PII leakage.

-      logger.info('Request payload:', payloadBody);
+      logger.debug('Request payload meta:', { model: payloadBody?.model, hasTools: !!payloadBody?.tools, msgCount: payloadBody?.messages?.length });

180-182: Avoid logging full responses.

Responses can include sensitive content. Log minimal metadata.

-      logger.info('OpenRouter Response:', data);
+      logger.info('OpenRouter response meta:', {
+        hasChoices: Array.isArray(data?.choices),
+        usage: data?.usage ? { prompt: data.usage.prompt_tokens, completion: data.usage.completion_tokens } : undefined,
+      });

343-349: Remove quotes on header key to satisfy lint.

-        headers: {
-          'Authorization': `Bearer ${this.getApiKey()}`,
-        },
+        headers: {
+          Authorization: `Bearer ${this.getApiKey()}`,
+        },

Also applies to: 347-348


377-383: Remove quotes on header key to satisfy lint.

-        headers: {
-          'Authorization': `Bearer ${this.getApiKey()}`,
-        },
+        headers: {
+          Authorization: `Bearer ${this.getApiKey()}`,
+        },

Also applies to: 381-382

front_end/panels/ai_chat/core/EnvironmentConfig.ts (5)

56-56: Use default fallback to mark absence of build keys.

-const BUILD_CONFIG = getBuildConfig();
+const BUILD_CONFIG = getBuildConfig() || DEFAULT_BUILD_CONFIG;

18-31: Type cleanup (optional): simplify apiKeys typing to satisfy member-delimiter-style.

-interface BuildTimeConfig {
-  readonly apiKeys: {
-    readonly openai?: string;
-    readonly openrouter?: string;
-    readonly groq?: string;
-    readonly litellm?: string;
-  };
-  readonly buildTime: string;
-  readonly hasKeys: boolean;
-}
+type Provider = 'openai' | 'openrouter' | 'groq' | 'litellm';
+type APIKeys = Readonly<Partial<Record<Provider, string>>>;
+interface BuildTimeConfig {
+  readonly apiKeys: APIKeys;
+  readonly buildTime: string;
+  readonly hasKeys: boolean;
+}

81-137: General: trailing spaces and eol-last.

Several lines have trailing spaces; add final newline. Run formatter to fix.

-    return '';
+    return '';

(Apply project formatter to this file.)


215-225: Avoid leaking provider list in logs (optional).

Logging which providers have keys can be sensitive. Consider logging counts only.

-      availableProviders: BUILD_CONFIG ? Object.keys(BUILD_CONFIG.apiKeys) : []
+      availableProviders: BUILD_CONFIG ? Object.keys(BUILD_CONFIG.apiKeys).length ? ['<redacted>'] : [] : []

1-235: Architecture: keys must not live in the frontend.

Even if injected at build, any key used by browser-side fetch is public. Move third‑party calls behind a small server/proxy (or OAuth flow) and accept only user-supplied keys in localStorage for pure client builds.

Happy to draft a minimal Node/Cloudflare proxy with rate limiting and request signing, plus compose wiring.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c5b19b2 and 81b3c47.

📒 Files selected for processing (7)
  • docker/Dockerfile (1 hunks)
  • docker/docker-compose.yml (1 hunks)
  • front_end/panels/ai_chat/BUILD.gn (2 hunks)
  • front_end/panels/ai_chat/LLM/GroqProvider.ts (5 hunks)
  • front_end/panels/ai_chat/LLM/OpenAIProvider.ts (4 hunks)
  • front_end/panels/ai_chat/LLM/OpenRouterProvider.ts (7 hunks)
  • front_end/panels/ai_chat/core/EnvironmentConfig.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
PR: BrowserOperator/browser-operator-core#0
File: eval-server/nodejs/CLAUDE.md:0-0
Timestamp: 2025-08-29T23:08:07.342Z
Learning: Applies to eval-server/nodejs/src/config.js : Require OPENAI_API_KEY and PORT (default 8080) from environment configuration
Learnt from: CR
PR: BrowserOperator/browser-operator-core#0
File: eval-server/nodejs/CLAUDE.md:0-0
Timestamp: 2025-08-29T23:08:07.342Z
Learning: Applies to eval-server/nodejs/src/config.js : Centralize configuration via environment variables in src/config.js
📚 Learning: 2025-08-29T23:08:07.342Z
Learnt from: CR
PR: BrowserOperator/browser-operator-core#0
File: eval-server/nodejs/CLAUDE.md:0-0
Timestamp: 2025-08-29T23:08:07.342Z
Learning: Applies to eval-server/nodejs/src/config.js : Require OPENAI_API_KEY and PORT (default 8080) from environment configuration

Applied to files:

  • front_end/panels/ai_chat/LLM/OpenAIProvider.ts
📚 Learning: 2025-08-29T23:08:07.342Z
Learnt from: CR
PR: BrowserOperator/browser-operator-core#0
File: eval-server/nodejs/CLAUDE.md:0-0
Timestamp: 2025-08-29T23:08:07.342Z
Learning: Applies to eval-server/nodejs/src/evaluator.js : LLM evaluator must integrate with the OpenAI API to judge agent responses

Applied to files:

  • front_end/panels/ai_chat/LLM/OpenAIProvider.ts
🧬 Code graph analysis (4)
front_end/panels/ai_chat/LLM/OpenAIProvider.ts (1)
front_end/panels/ai_chat/core/EnvironmentConfig.ts (1)
  • getEnvironmentConfig (233-235)
front_end/panels/ai_chat/LLM/GroqProvider.ts (2)
front_end/panels/ai_chat/core/EnvironmentConfig.ts (1)
  • getEnvironmentConfig (233-235)
front_end/panels/ai_chat/ui/AIChatPanel.ts (2)
  • apiKey (1112-1143)
  • apiKey (1185-1204)
front_end/panels/ai_chat/core/EnvironmentConfig.ts (3)
front_end/panels/ai_chat/core/Logger.ts (1)
  • createLogger (316-318)
front_end/panels/ai_chat/core/Constants.ts (1)
  • STORAGE_KEYS (39-45)
front_end/panels/ai_chat/ui/AIChatPanel.ts (1)
  • localStorage (772-795)
front_end/panels/ai_chat/LLM/OpenRouterProvider.ts (2)
front_end/panels/ai_chat/core/EnvironmentConfig.ts (1)
  • getEnvironmentConfig (233-235)
front_end/panels/ai_chat/ui/AIChatPanel.ts (5)
  • logger (907-1000)
  • logger (1276-1329)
  • logger (1352-1379)
  • logger (1486-1499)
  • logger (1504-1507)
🪛 ESLint
front_end/panels/ai_chat/LLM/OpenAIProvider.ts

[error] 10-10: ../core/EnvironmentConfig.js import should occur before type import of ./LLMTypes.js

(import/order)


[error] 64-64: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)

front_end/panels/ai_chat/LLM/GroqProvider.ts

[error] 10-10: ../core/EnvironmentConfig.js import should occur before type import of ./LLMTypes.js

(import/order)


[error] 60-60: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 115-115: Unnecessarily quoted property 'Authorization' found.

(@stylistic/quote-props)


[error] 282-282: Unnecessarily quoted property 'Authorization' found.

(@stylistic/quote-props)

front_end/panels/ai_chat/core/EnvironmentConfig.ts

[error] 7-7: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 12-12: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 23-23: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 24-24: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 25-25: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 26-26: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 33-33: 'DEFAULT_BUILD_CONFIG' is assigned a value but never used.

(@typescript-eslint/no-unused-vars)


[error] 45-50: Unnecessarily quoted property 'apiKeys' found.

(@stylistic/quote-props)


[error] 45-45: Strings must use singlequote.

(@stylistic/quotes)


[error] 46-46: Unnecessarily quoted property 'openai' found.

(@stylistic/quote-props)


[error] 46-46: Strings must use singlequote.

(@stylistic/quotes)


[error] 46-46: Strings must use singlequote.

(@stylistic/quotes)


[error] 47-47: Unnecessarily quoted property 'openrouter' found.

(@stylistic/quote-props)


[error] 47-47: Strings must use singlequote.

(@stylistic/quotes)


[error] 47-47: Strings must use singlequote.

(@stylistic/quotes)


[error] 48-48: Unnecessarily quoted property 'groq' found.

(@stylistic/quote-props)


[error] 48-48: Strings must use singlequote.

(@stylistic/quotes)


[error] 48-48: Strings must use singlequote.

(@stylistic/quotes)


[error] 49-49: Unnecessarily quoted property 'litellm' found.

(@stylistic/quote-props)


[error] 49-49: Strings must use singlequote.

(@stylistic/quotes)


[error] 49-49: Strings must use singlequote.

(@stylistic/quotes)


[error] 51-51: Unnecessarily quoted property 'buildTime' found.

(@stylistic/quote-props)


[error] 51-51: Strings must use singlequote.

(@stylistic/quotes)


[error] 51-51: Strings must use singlequote.

(@stylistic/quotes)


[error] 52-52: Unnecessarily quoted property 'hasKeys' found.

(@stylistic/quote-props)


[error] 52-52: Strings must use singlequote.

(@stylistic/quotes)


[error] 77-77: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 109-109: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 114-114: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 122-122: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 141-141: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 151-151: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 158-158: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 162-162: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 166-166: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 172-172: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 182-182: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 187-187: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 188-188: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 189-189: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 190-190: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 194-194: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 202-202: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 212-212: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 216-216: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 217-217: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 218-218: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 230-230: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 235-235: Newline required at end of file but not found.

(@stylistic/eol-last)

front_end/panels/ai_chat/LLM/OpenRouterProvider.ts

[error] 10-10: ../core/EnvironmentConfig.js import should occur before type import of ./LLMTypes.js

(import/order)


[error] 81-81: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 167-167: Unnecessarily quoted property 'Authorization' found.

(@stylistic/quote-props)


[error] 347-347: Unnecessarily quoted property 'Authorization' found.

(@stylistic/quote-props)


[error] 381-381: Unnecessarily quoted property 'Authorization' found.

(@stylistic/quote-props)


[error] 646-646: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 651-651: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 672-672: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)

🔇 Additional comments (5)
docker/Dockerfile (1)

4-15: Avoid baking API keys into the image or frontend bundle
Remove the global ENV blocks from docker/Dockerfile and either:

  • Use BuildKit secrets in a scoped RUN (e.g. RUN --mount=type=secret,id=openai_api_key … npm run build)
  • Or inline each ARG into the build command (RUN OPENAI_API_KEY="$OPENAI_API_KEY" npm run build)

Re-run your secrets scan against the actual frontend build output directory (e.g. dist/, build/, etc.) to confirm no keys are embedded.

front_end/panels/ai_chat/BUILD.gn (1)

31-31: Include EnvironmentConfig.ts — LGTM.

Adding EnvironmentConfig to both sources and _ai_chat_sources looks correct for build and metadata generation. No action needed.

Also applies to: 137-137

front_end/panels/ai_chat/LLM/OpenAIProvider.ts (1)

46-47: Singleton env config — LGTM.

Using a cached singleton is fine and avoids repeated instantiation.

front_end/panels/ai_chat/LLM/GroqProvider.ts (1)

42-43: Env-config delegation — LGTM.

validateCredentials() and storage key retrieval correctly delegate to EnvironmentConfig.

Also applies to: 454-465

front_end/panels/ai_chat/core/EnvironmentConfig.ts (1)

33-37: Ensure DEFAULT_BUILD_CONFIG is used or removed; clean up whitespace

  • DEFAULT_BUILD_CONFIG is still unused—either integrate it as the fallback config or delete its definition.
  • Remove trailing spaces from comments and code in EnvironmentConfig.ts.
  • Add a newline at the end of the file.

Likely an incorrect or invalid review comment.

Comment on lines 55 to 63
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');
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix infinite recursion in getApiKey().

this.getApiKey() calls itself repeatedly. Use this.apiKey.

   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
-    return this.envConfig.getApiKey('groq');
+    return this.envConfig.getApiKey('groq')?.trim() ?? '';
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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');
}
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('groq')?.trim() ?? '';
}
🧰 Tools
🪛 ESLint

[error] 60-60: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/LLM/GroqProvider.ts around lines 55 to 63,
getApiKey() currently calls this.getApiKey() causing infinite recursion; replace
those self-calls with the instance field this.apiKey (guarding for
null/undefined), e.g. check if this.apiKey is a non-empty string, return
this.apiKey.trim(), otherwise return this.envConfig.getApiKey('groq'); keep the
same priority order and trimming behavior.

Comment on lines 114 to 116
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
'Authorization': `Bearer ${this.getApiKey()}`,
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Unquote Authorization header and guard against empty key.

  • Style: no quotes needed around Authorization.
  • Behavior: avoid sending an empty Bearer header.
-          'Authorization': `Bearer ${this.getApiKey()}`,
+          Authorization: (() => {
+            const k = this.getApiKey();
+            if (!k) throw new Error('Groq API key is missing. Configure it in Settings.');
+            return `Bearer ${k}`;
+          })(),
-          'Authorization': `Bearer ${this.getApiKey()}`,
+          Authorization: (() => {
+            const k = this.getApiKey();
+            if (!k) throw new Error('Groq API key is missing. Configure it in Settings.');
+            return `Bearer ${k}`;
+          })(),

Also applies to: 281-283

🧰 Tools
🪛 ESLint

[error] 115-115: Unnecessarily quoted property 'Authorization' found.

(@stylistic/quote-props)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/LLM/GroqProvider.ts around lines 114-116 (and
similarly at 281-283), the Authorization header is written with unnecessary
quotes around the header name and may send an empty Bearer token; change it to
use an unquoted header name and build the header only after retrieving and
trimming the API key into a local variable (e.g., const key =
this.getApiKey()?.trim()), then if key is falsy either throw a clear error or
omit the Authorization header instead of sending "Bearer " with an empty token.
Ensure the header includes `Bearer ${key}` only when key is non-empty.

Comment on lines 321 to 323
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
Authorization: `Bearer ${this.getApiKey()}`,
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid sending empty Authorization header.

If no key is present, fail fast with a clear error instead of Authorization: Bearer .

-          Authorization: `Bearer ${this.getApiKey()}`,
+          ...(this.getApiKey()
+            ? { Authorization: `Bearer ${this.getApiKey()}` }
+            : (() => { throw new Error('OpenAI API key is missing. Configure it in Settings.'); })()),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
Authorization: `Bearer ${this.getApiKey()}`,
},
'Content-Type': 'application/json',
...(this.getApiKey()
? { Authorization: `Bearer ${this.getApiKey()}` }
: (() => { throw new Error('OpenAI API key is missing. Configure it in Settings.'); })()),
},
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/LLM/OpenAIProvider.ts around lines 321 to 323, the
code unconditionally sets Authorization: `Bearer ${this.getApiKey()}` which can
create an empty Authorization header; change this to fail fast when no API key
is present by checking this.getApiKey() before building headers and throw a
clear error (or return early) if it's missing, otherwise include the
Authorization header with the key; ensure headers only include Authorization
when a non-empty key exists.

@olesho olesho changed the title Pass API keys for docker build DRAFT: Pass API keys for docker build Sep 5, 2025
@claude
Copy link

claude bot commented Sep 6, 2025

Claude finished @olesho's task —— View job


PR Review: Pass API keys for docker build

Todo List:

  • Read repository setup instructions (CLAUDE.md)
  • Examine Docker configuration changes
  • Review TypeScript LLM provider changes
  • Analyze new EnvironmentConfig.ts file
  • Check for security concerns with API key handling
  • Assess code quality and best practices
  • Provide comprehensive feedback

🔒 Security Analysis

⚠️ Critical Security Issues

1. Exposed API Keys in Repository

  • Location: front_end/models/crux-manager/CrUXManager.ts:23
  • Issue: Google API key AIzaSyCCSOx25vrb5z0tbedCB3_JRzzbVW6Uwgw is hardcoded
  • Location: front_end/models/trace/handlers/NetworkRequestsHandler.test.ts:421
  • Issue: Firebase API key AIzaSyCGxLbbFQxH4BV1fY0RODlxTos9nJa2l_g is hardcoded
  • Risk: These keys are publicly visible and should be rotated immediately
  • Recommendation: Move to environment variables and rotate the exposed keys

2. Runtime API Key Injection Security Concerns

  • Location: docker/docker-entrypoint.sh:15-61
  • Issue: Script directly injects API keys into JavaScript files and logs their presence
  • Risk: Keys become visible in browser dev tools and container logs
  • Recommendation: Use secure runtime configuration or server-side proxy instead

✅ Security Best Practices Implemented

1. Proper Fallback Hierarchy

  • localStorage (user input) → runtime config → build-time config → empty string
  • Prevents accidental exposure of sensitive data

2. Build-time vs Runtime Separation

  • API keys are injected at runtime, not baked into image layers
  • Docker compose properly handles environment variable passing

🏗️ Code Quality Assessment

✅ Strengths

1. Well-Structured Environment Config

  • Clean singleton pattern implementation
  • Comprehensive API key management with multiple fallback sources
  • Good separation of concerns

2. Consistent Integration Pattern

  • All LLM providers follow the same pattern for API key handling
  • Backward compatibility maintained with constructor parameters

3. Proper Error Handling

  • Comprehensive validation methods
  • Clear error messages for missing credentials

⚠️ Code Quality Issues

1. Critical Bug - Infinite Recursion

  • Location: front_end/panels/ai_chat/LLM/GroqProvider.ts:57-58
private getApiKey(): string {
    if (this.getApiKey() && this.getApiKey().trim() !== '') { // ❌ Infinite recursion
        return this.getApiKey().trim();
    }
}
  • Fix: Should be this.apiKey instead of this.getApiKey()

2. Inconsistent Validation Methods

  • Location: front_end/panels/ai_chat/LLM/OpenAIProvider.ts:544-560
  • Issue: OpenAIProvider doesn't use the new EnvironmentConfig for validation like other providers
  • Recommendation: Standardize to use this.envConfig.validateCredentials('openai')

3. Missing localStorage Safety Guards

  • Location: front_end/panels/ai_chat/core/EnvironmentConfig.ts:147,254
  • Issue: Direct localStorage access without checking availability
  • Risk: Crashes in non-browser environments (SSR, tests)
  • Fix: Guard with typeof localStorage !== 'undefined'

🚀 Performance Considerations

✅ Good Practices

  • Singleton pattern prevents multiple config instantiations
  • Caching implemented for vision model detection (OpenRouterProvider)
  • Lazy initialization of environment config

📝 Minor Optimizations

  • Debug logging could be made conditional to reduce overhead in production
  • Vision model cache could be shared across provider instances

🧪 Test Coverage

❌ Missing Test Coverage

  • No unit tests for the new EnvironmentConfig class
  • No tests for API key fallback hierarchy
  • No tests for validation methods
  • Docker build integration not tested

📋 Recommended Test Cases

  1. EnvironmentConfig fallback hierarchy testing
  2. API key validation edge cases
  3. Build-time configuration injection
  4. Provider credential validation consistency

📚 Documentation & Maintainability

✅ Good Documentation

  • Comprehensive JSDoc comments
  • Clear explanation of fallback hierarchy
  • Build process documented in Docker files

📝 Areas for Improvement

  • README should document how to pass API keys during Docker build
  • Environment variable naming conventions could be documented
  • Migration guide for existing users would be helpful

🛠️ Specific Recommendations

High Priority (Must Fix)

  1. Rotate exposed API keys found in CrUXManager.ts and NetworkRequestsHandler.test.ts
  2. Fix infinite recursion bug in GroqProvider.ts:57-58
  3. Add localStorage safety guards in EnvironmentConfig.ts
  4. Standardize validation across all providers to use EnvironmentConfig

Medium Priority (Should Fix)

  1. Use more secure API key injection method (avoid direct JS file modification)
  2. Add unit tests for EnvironmentConfig class
  3. Implement consistent error handling across all providers

Low Priority (Nice to Have)

  1. Add migration documentation
  2. Implement conditional debug logging
  3. Share vision model cache across provider instances

✅ Overall Assessment

This PR successfully implements a robust API key management system for Docker builds. The core architecture is sound with good separation of concerns and proper fallback mechanisms. However, there are critical security issues (exposed API keys in repository) and a significant bug (infinite recursion) that must be addressed before merging.

The Docker runtime injection approach, while functional, could be more secure. Consider using server-side proxy or OAuth for production deployments.

Recommendation: Request changes to fix the critical issues, then this will be ready for merge.


@olesho olesho changed the title DRAFT: Pass API keys for docker build Pass API keys for docker build Sep 6, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
front_end/panels/ai_chat/ui/SettingsDialog.ts (1)

604-617: Do not prefill runtime-injected API keys into inputs (they’ll be persisted to localStorage on Save).

Prefilling env-provided secrets into the inputs causes accidental persistence to localStorage when the user presses Save, defeating the purpose of passing keys at build/run time. Initialize inputs from localStorage only, show a “Using runtime-injected key” status if present, and gate persistence to “only if the user changed the field.”

Apply this diff here:

-const envConfig = getEnvironmentConfig();
-const settingsSavedApiKey = envConfig.getApiKey('openai');
+const envConfig = getEnvironmentConfig();
+const settingsSavedApiKey = localStorage.getItem('ai_chat_api_key') || '';
+const hasEnvOpenAIKey = !!envConfig.getApiKey('openai');
 ...
-settingsApiKeyInput.value = settingsSavedApiKey;
+settingsApiKeyInput.value = settingsSavedApiKey;
 ...
-const settingsApiKeyStatus = document.createElement('div');
+const settingsApiKeyStatus = document.createElement('div');
 settingsApiKeyStatus.className = 'settings-status';
 settingsApiKeyStatus.style.display = 'none';
 openaiSettingsSection.appendChild(settingsApiKeyStatus);
+
+// Inform the user we’re using an injected key without persisting it
+if (hasEnvOpenAIKey && !settingsSavedApiKey) {
+  settingsApiKeyStatus.textContent = 'Using runtime-injected key';
+  settingsApiKeyStatus.style.display = 'block';
+  settingsApiKeyStatus.style.backgroundColor = 'var(--color-background-elevation-1)';
+  settingsApiKeyStatus.style.color = 'var(--color-text-secondary)';
+}
+
+// Track initial for “persist only if changed”
+const initialOpenAIKey = settingsApiKeyInput.value;

And gate persistence in the Save handler (non-diff snippet – replicate the pattern for other providers):

// OpenAI
const newApiKey = settingsApiKeyInput.value.trim();
if (newApiKey !== initialOpenAIKey) {
  if (newApiKey) localStorage.setItem('ai_chat_api_key', newApiKey);
  else localStorage.removeItem('ai_chat_api_key');
}
♻️ Duplicate comments (5)
front_end/panels/ai_chat/core/EnvironmentConfig.ts (2)

252-269: Source detection should mirror the same guards.

Ensure consistent empty/SSR handling.

-    const storageKey = STORAGE_KEYS[provider];
-    const localStorageKey = localStorage.getItem(storageKey);
+    const storageKey = STORAGE_KEYS[provider];
+    const localStorageKey =
+      typeof window !== 'undefined' && typeof window.localStorage !== 'undefined'
+        ? window.localStorage.getItem(storageKey)
+        : null;
@@
-    if (this.getRuntimeKey(provider)) {
+    if (this.getRuntimeKey(provider)) {
       return 'runtime';
     }
@@
-    if (BUILD_CONFIG?.apiKeys?.[provider]) {
+    if (typeof BUILD_CONFIG?.apiKeys?.[provider] === 'string' &&
+        BUILD_CONFIG.apiKeys[provider]!.trim() !== '') {
       return 'build-time';
     }

118-158: Guard localStorage for SSR/tests; avoid reference errors.

Direct localStorage access can throw when window is undefined.

-    for (const provider of providers) {
+    for (const provider of providers) {
       const storageKey = STORAGE_KEYS[provider];
-      const existingKey = localStorage.getItem(storageKey);
+      const existingKey =
+        typeof window !== 'undefined' && typeof window.localStorage !== 'undefined'
+          ? window.localStorage.getItem(storageKey)
+          : null;
@@
-          localStorage.setItem(storageKey, runtimeKey);
+          if (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined') {
+            window.localStorage.setItem(storageKey, runtimeKey);
+          }
front_end/panels/ai_chat/ui/SettingsDialog.ts (3)

715-721: Do not prefill LiteLLM API key from env; persist only on change.

Initialize from localStorage only and track initial value.

-const settingsSavedLiteLLMApiKey = envConfig.getApiKey('litellm');
+const settingsSavedLiteLLMApiKey = localStorage.getItem(LITELLM_API_KEY_STORAGE_KEY) || '';
 ...
 litellmApiKeyInput.value = settingsSavedLiteLLMApiKey;
+const initialLiteLLMApiKey = litellmApiKeyInput.value;

1271-1277: Do not prefill Groq API key from env; enable “Fetch” when env key exists; persist only on change.

Initialize from localStorage, compute env presence, track initial.

-const settingsSavedGroqApiKey = envConfig.getApiKey('groq');
+const settingsSavedGroqApiKey = localStorage.getItem(GROQ_API_KEY_STORAGE_KEY) || '';
+const hasEnvGroqKey = !!envConfig.getApiKey('groq');
 ...
 groqApiKeyInput.value = settingsSavedGroqApiKey;
+const initialGroqApiKey = groqApiKeyInput.value;

Also update button state and click fallback (non-diff, see Groq note at Line 529).


1457-1463: Do not prefill OpenRouter API key from env; enable “Fetch” when env key exists; persist only on change.

Initialize from localStorage, compute env presence, track initial.

-const settingsSavedOpenRouterApiKey = envConfig.getApiKey('openrouter');
+const settingsSavedOpenRouterApiKey = localStorage.getItem(OPENROUTER_API_KEY_STORAGE_KEY) || '';
+const hasEnvOpenRouterKey = !!envConfig.getApiKey('openrouter');
 ...
 openrouterApiKeyInput.value = settingsSavedOpenRouterApiKey;
+const initialOpenrouterApiKey = openrouterApiKeyInput.value;

Also update button state and click fallback (non-diff, see OpenRouter note at Line 548).

🧹 Nitpick comments (6)
docker/Dockerfile (3)

4-7: Optional: Document concrete BuildKit usage (secrets) for repeatability.

Add an example command (buildx/build) to avoid confusion when not using docker-compose.

 # Usage: DOCKER_BUILDKIT=1 docker-compose build
+# Or:
+#   docker buildx build --progress=plain .

54-58: Guard against path drift: COPY will create missing dirs silently.

If the upstream DevTools layout changes, these COPYs can land in unintended paths without failing the build. Add directory existence checks right before copying.

Apply after Line 53 (outside changed range):

# Fail fast if expected target dirs ever change upstream
RUN test -d front_end/panels/ai_chat/core \
 && test -d front_end/panels/ai_chat/ui \
 && test -d front_end/entrypoints/devtools_app

59-60: Build step duplication – consider removing the earlier npm run build.

You build at Line 47 and again at Line 60. Unless the first build is intentionally priming caches, drop it to cut build time.

docker/docker-entrypoint.sh (1)

33-37: Make localStorage persistence opt-in.

Persisting provider keys in localStorage increases exposure. Gate it behind an env flag (e.g., RUNTIME_PERSIST_KEYS=1).

-// Auto-save to localStorage\
-if (window.__RUNTIME_CONFIG__.OPENAI_API_KEY) localStorage.setItem("ai_chat_api_key", window.__RUNTIME_CONFIG__.OPENAI_API_KEY);\
+// Auto-save to localStorage (opt-in via RUNTIME_PERSIST_KEYS=1)\
+if (String('"${RUNTIME_PERSIST_KEYS-}"') === "1" && window.__RUNTIME_CONFIG__.OPENAI_API_KEY) localStorage.setItem("ai_chat_api_key", window.__RUNTIME_CONFIG__.OPENAI_API_KEY);\

Repeat similarly for the other three providers.

front_end/panels/ai_chat/core/EnvironmentConfig.ts (1)

51-58: Nit: narrow RuntimeConfig keys to exact strings only where used.

Current interface is fine; just ensure consumers rely on getRuntimeKey to avoid casing typos elsewhere.

front_end/panels/ai_chat/ui/SettingsDialog.ts (1)

11-11: Fix import order to satisfy ESLint (import/order).

Place EnvironmentConfig import before Logger (per hint).

 import * as i18n from '../../../core/i18n/i18n.js';
 import * as UI from '../../../ui/legacy/legacy.js';
 import { getEvaluationConfig, setEvaluationConfig, isEvaluationEnabled, connectToEvaluationService, disconnectFromEvaluationService, getEvaluationClientId, isEvaluationConnected } from '../common/EvaluationConfig.js';
-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 { getEnvironmentConfig } from '../core/EnvironmentConfig.js';
+import { createLogger } from '../core/Logger.js';
+import { LLMClient } from '../LLM/LLMClient.js';
+import { getTracingConfig, setTracingConfig, isTracingEnabled } from '../tracing/TracingConfig.js';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 81b3c47 and ded87fd.

📒 Files selected for processing (7)
  • docker/.env.example (1 hunks)
  • docker/Dockerfile (3 hunks)
  • docker/docker-compose.yml (1 hunks)
  • docker/docker-entrypoint.sh (1 hunks)
  • front_end/entrypoints/devtools_app/devtools_app.ts (1 hunks)
  • front_end/panels/ai_chat/core/EnvironmentConfig.ts (1 hunks)
  • front_end/panels/ai_chat/ui/SettingsDialog.ts (7 hunks)
✅ Files skipped from review due to trivial changes (2)
  • front_end/entrypoints/devtools_app/devtools_app.ts
  • docker/.env.example
🚧 Files skipped from review as they are similar to previous changes (1)
  • docker/docker-compose.yml
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
PR: BrowserOperator/browser-operator-core#0
File: eval-server/nodejs/CLAUDE.md:0-0
Timestamp: 2025-08-29T23:08:07.342Z
Learning: Applies to eval-server/nodejs/src/config.js : Centralize configuration via environment variables in src/config.js
Learnt from: CR
PR: BrowserOperator/browser-operator-core#0
File: eval-server/nodejs/CLAUDE.md:0-0
Timestamp: 2025-08-29T23:08:07.342Z
Learning: Applies to eval-server/nodejs/src/config.js : Require OPENAI_API_KEY and PORT (default 8080) from environment configuration
📚 Learning: 2025-08-29T23:08:07.342Z
Learnt from: CR
PR: BrowserOperator/browser-operator-core#0
File: eval-server/nodejs/CLAUDE.md:0-0
Timestamp: 2025-08-29T23:08:07.342Z
Learning: Applies to eval-server/nodejs/src/config.js : Require OPENAI_API_KEY and PORT (default 8080) from environment configuration

Applied to files:

  • front_end/panels/ai_chat/core/EnvironmentConfig.ts
📚 Learning: 2025-08-29T23:08:07.342Z
Learnt from: CR
PR: BrowserOperator/browser-operator-core#0
File: eval-server/nodejs/CLAUDE.md:0-0
Timestamp: 2025-08-29T23:08:07.342Z
Learning: Applies to eval-server/nodejs/src/config.js : Centralize configuration via environment variables in src/config.js

Applied to files:

  • front_end/panels/ai_chat/core/EnvironmentConfig.ts
🧬 Code graph analysis (2)
front_end/panels/ai_chat/core/EnvironmentConfig.ts (3)
front_end/panels/ai_chat/core/Logger.ts (1)
  • createLogger (316-318)
front_end/panels/ai_chat/core/Constants.ts (1)
  • STORAGE_KEYS (39-45)
front_end/panels/ai_chat/ui/AIChatPanel.ts (1)
  • localStorage (772-795)
front_end/panels/ai_chat/ui/SettingsDialog.ts (1)
front_end/panels/ai_chat/core/EnvironmentConfig.ts (1)
  • getEnvironmentConfig (334-336)
🪛 ESLint
front_end/panels/ai_chat/core/EnvironmentConfig.ts

[error] 7-7: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 11-11: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 13-13: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 27-27: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 28-28: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 29-29: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 30-30: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 68-68: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 69-69: Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free.

(@typescript-eslint/ban-ts-comment)


[error] 71-71: Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free.

(@typescript-eslint/ban-ts-comment)


[error] 73-73: Unexpected console statement. Only these console methods are allowed: assert, context, error, timeStamp, time, timeEnd, warn.

(no-console)


[error] 83-83: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 84-84: Unexpected console statement. Only these console methods are allowed: assert, context, error, timeStamp, time, timeEnd, warn.

(no-console)


[error] 105-105: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 117-117: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 120-120: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 148-148: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 172-172: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 179-179: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 196-196: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 202-202: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 210-210: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 238-238: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 248-248: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 255-255: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 259-259: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 263-263: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 267-267: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 273-273: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 283-283: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 288-288: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 289-289: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 290-290: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 291-291: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 295-295: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 303-303: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 313-313: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 317-317: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 318-318: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 319-319: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 331-331: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 336-336: Newline required at end of file but not found.

(@stylistic/eol-last)

front_end/panels/ai_chat/ui/SettingsDialog.ts

[error] 11-11: ../core/EnvironmentConfig.js import should occur before import of ../core/Logger.js

(import/order)

🔇 Additional comments (2)
docker/Dockerfile (1)

71-78: nginx port configuration validated
docker/nginx.conf is configured with listen 8000; (and IPv6), matching the EXPOSE 8000 instruction.

front_end/panels/ai_chat/core/EnvironmentConfig.ts (1)

36-47: Good: No build-time keys in source.

Returning DEFAULT_BUILD_CONFIG by default prevents shipping secrets. Keep it.

Comment on lines +18 to +41
# 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Make injection idempotent and safely escape API key values.

Current sed prepends on every start and doesn’t escape quotes/backslashes in env values. Add a marker/guard and escape values before injecting.

 set -e

 # Log configuration status to container logs
@@
-# 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
+# Escape for JSON-in-JS string literals
+escape_for_js() {
+  # escapes \ and " characters
+  # shellcheck disable=SC2001
+  printf '%s' "${1-}" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
+}
+OPENAI_API_KEY_JS="$(escape_for_js "${OPENAI_API_KEY-}")"
+OPENROUTER_API_KEY_JS="$(escape_for_js "${OPENROUTER_API_KEY-}")"
+GROQ_API_KEY_JS="$(escape_for_js "${GROQ_API_KEY-}")"
+LITELLM_API_KEY_JS="$(escape_for_js "${LITELLM_API_KEY-}")"
+INJECT_MARKER='/* RUNTIME_CONFIG: injected */'
+
+# Find and modify the main DevTools entry point files (idempotent)
+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\
+    if grep -q "__RUNTIME_CONFIG__" "$js_file"; then
+      echo "    ↷ Already injected, skipping"
+      continue
+    fi
+    # Inject a global configuration object at the beginning of the file
+    sed -i '1i\
+/* RUNTIME_CONFIG: injected */\
 // 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:-}"'",\
+  OPENAI_API_KEY: "'"${OPENAI_API_KEY_JS}"'",\
+  OPENROUTER_API_KEY: "'"${OPENROUTER_API_KEY_JS}"'",\
+  GROQ_API_KEY: "'"${GROQ_API_KEY_JS}"'",\
+  LITELLM_API_KEY: "'"${LITELLM_API_KEY_JS}"'",\
   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
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 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
set -e
# Log configuration status to container logs
# Escape for JSON-in-JS string literals
escape_for_js() {
# escapes \ and " characters
# shellcheck disable=SC2001
printf '%s' "${1-}" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
}
OPENAI_API_KEY_JS="$(escape_for_js "${OPENAI_API_KEY-}")"
OPENROUTER_API_KEY_JS="$(escape_for_js "${OPENROUTER_API_KEY-}")"
GROQ_API_KEY_JS="$(escape_for_js "${GROQ_API_KEY-}")"
LITELLM_API_KEY_JS="$(escape_for_js "${LITELLM_API_KEY-}")"
INJECT_MARKER='/* RUNTIME_CONFIG: injected */'
# Find and modify the main DevTools entry point files (idempotent)
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")"
if grep -q "__RUNTIME_CONFIG__" "$js_file"; then
echo " ↷ Already injected, skipping"
continue
fi
# Inject a global configuration object at the beginning of the file
sed -i '1i\
/* RUNTIME_CONFIG: injected */\
// Runtime API key injection\
window.__RUNTIME_CONFIG__ = {\
OPENAI_API_KEY: "'"${OPENAI_API_KEY_JS}"'",\
OPENROUTER_API_KEY: "'"${OPENROUTER_API_KEY_JS}"'",\
GROQ_API_KEY: "'"${GROQ_API_KEY_JS}"'",\
LITELLM_API_KEY: "'"${LITELLM_API_KEY_JS}"'",\
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
🤖 Prompt for AI Agents
In docker/docker-entrypoint.sh around lines 18-41, the sed-based prepend is not
idempotent and will inject every start and also fails to escape
quotes/backslashes in env values; update the script to first check for a unique
marker (e.g. /* RUNTIME-CONFIG-INJECTED */) in each target file and skip
injection if present, and build the injection payload from properly escaped
environment variables (escape backslashes and quotes/newlines or serialize as
JSON) into a temporary string/file before inserting so values are safe for
inclusion in JS; perform the insertion atomically (e.g. write payload to a temp
file and use awk/sed/printf to insert only when marker missing) to ensure
idempotence and correct escaping.

Comment on lines +43 to +61
# 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Apply the same idempotency/escaping to AI Chat panel files.

Mirror the guard and escaped values so we don't prepend twice.

-# Also inject into AI Chat panel files specifically
+# Also inject into AI Chat panel files specifically (idempotent)
 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\
+    if grep -q "__RUNTIME_CONFIG__" "$js_file"; then
+      echo "    ↷ Already injected, skipping"
+      continue
+    fi
+    sed -i '1i\
+/* RUNTIME_CONFIG: injected */\
 // 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:-}"'",\
+    OPENAI_API_KEY: "'"${OPENAI_API_KEY_JS}"'",\
+    OPENROUTER_API_KEY: "'"${OPENROUTER_API_KEY_JS}"'",\
+    GROQ_API_KEY: "'"${GROQ_API_KEY_JS}"'",\
+    LITELLM_API_KEY: "'"${LITELLM_API_KEY_JS}"'",\
     timestamp: "'"$(date -Iseconds)"'",\
     source: "docker-runtime"\
   };\
 }\
 ' "$js_file"
   fi
 done
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 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
# Also inject into AI Chat panel files specifically (idempotent)
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")"
if grep -q "__RUNTIME_CONFIG__" "$js_file"; then
echo " ↷ Already injected, skipping"
continue
fi
sed -i '1i\
/* RUNTIME_CONFIG: injected */\
// Runtime API key injection for AI Chat\
if (typeof window !== "undefined" && !window.__RUNTIME_CONFIG__) {\
window.__RUNTIME_CONFIG__ = {\
OPENAI_API_KEY: "'"${OPENAI_API_KEY_JS}"'",\
OPENROUTER_API_KEY: "'"${OPENROUTER_API_KEY_JS}"'",\
GROQ_API_KEY: "'"${GROQ_API_KEY_JS}"'",\
LITELLM_API_KEY: "'"${LITELLM_API_KEY_JS}"'",\
timestamp: "'"$(date -Iseconds)"'",\
source: "docker-runtime"\
};\
}\
' "$js_file"
fi
done
🤖 Prompt for AI Agents
In docker/docker-entrypoint.sh around lines 43 to 61, the AI Chat panel
injection lacks the idempotent guard and proper escaping used elsewhere so the
snippet can be prepended multiple times and variables may not be safely escaped;
update the sed insert to mirror the same guard (check typeof window !==
"undefined" && !window.__RUNTIME_CONFIG__) and inject the same escaped values
format for each API key and timestamp so the snippet is only added once and
variables are safely expanded; ensure the insertion matches the existing
injection logic (same guard, same escaped quoting) used for other panels to
prevent duplicate prepends and injection bugs.

Comment on lines 1 to 336
// 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. Runtime environment variables (Docker runtime injection)
* 3. Build-time environment variables (Docker build args, fallback)
* 4. Empty string (no configuration available)
*
* 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';

// 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 - replaced at Docker build for local/dev only.
// IMPORTANT: Do not commit real keys. The default returns no keys.
function getBuildConfig(): BuildTimeConfig {
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
*/
export type APIKeyProvider = 'openai' | 'openrouter' | 'groq' | 'litellm';

/**
* Storage keys for localStorage API keys
*/
const STORAGE_KEYS: Record<APIKeyProvider, string> = {
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 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) : []
});
this.debugLogged = true;
}
}

/**
* 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<APIKeyProvider, keyof RuntimeConfig> = {
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
*/
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. 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
*/
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();
}

// 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}`);
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', 'runtime', 'build-time', or 'none')
*/
getApiKeySource(provider: APIKeyProvider): 'localStorage' | 'runtime' | 'build-time' | 'none' {
const storageKey = STORAGE_KEYS[provider];
const localStorageKey = localStorage.getItem(storageKey);

if (localStorageKey && localStorageKey.trim() !== '') {
return 'localStorage';
}

if (this.getRuntimeKey(provider)) {
return 'runtime';
}

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Run lint autofix to clear trailing-space and no-console findings.

After the above changes, run the project’s lint/fix to keep the tree clean.


🏁 Script executed:

#!/bin/bash
# Show any remaining lints in the changed files
fd -a EnvironmentConfig.ts | xargs -r rg -n 'console\.log|@ts-ignore| +$' || true

Length of output: 572


Remove all console.log calls, @ts-ignore comments, and trailing spaces, then rerun lint autofix

  • Eliminate the console.log statements in getRuntimeConfig() (e.g. “Runtime config found” and “Runtime config not found”)
  • Remove both // @ts-ignore lines around __RUNTIME_CONFIG__
  • Strip any trailing whitespace throughout EnvironmentConfig.ts
🧰 Tools
🪛 ESLint

[error] 7-7: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 11-11: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 13-13: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 27-27: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 28-28: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 29-29: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 30-30: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 68-68: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 69-69: Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free.

(@typescript-eslint/ban-ts-comment)


[error] 71-71: Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free.

(@typescript-eslint/ban-ts-comment)


[error] 73-73: Unexpected console statement. Only these console methods are allowed: assert, context, error, timeStamp, time, timeEnd, warn.

(no-console)


[error] 83-83: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 84-84: Unexpected console statement. Only these console methods are allowed: assert, context, error, timeStamp, time, timeEnd, warn.

(no-console)


[error] 105-105: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 117-117: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 120-120: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 148-148: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 172-172: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 179-179: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 196-196: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 202-202: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 210-210: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 238-238: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 248-248: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 255-255: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 259-259: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 263-263: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 267-267: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 273-273: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 283-283: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 288-288: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 289-289: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 290-290: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 291-291: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 295-295: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 303-303: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 313-313: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 317-317: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 318-318: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 319-319: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 331-331: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 336-336: Newline required at end of file but not found.

(@stylistic/eol-last)

Comment on lines +63 to +86
// 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;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use logger instead of console, and prefer @ts-expect-error over @ts-ignore.

Aligns with lint rules and keeps logs consistent.

-  // @ts-ignore - __RUNTIME_CONFIG__ is injected by Docker entrypoint
+  // @ts-expect-error __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:', {
+    // @ts-expect-error
+    const config = window.__RUNTIME_CONFIG__ as RuntimeConfig;
+    logger.debug('Runtime config found:', {
@@
-  console.log('Runtime config not found on window object');
+  logger.debug('Runtime config not found on window object');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 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;
}
// Get runtime configuration if available (from Docker runtime injection)
function getRuntimeConfig(): RuntimeConfig | null {
if (typeof window === 'undefined') {
return null;
}
// @ts-expect-error __RUNTIME_CONFIG__ is injected by Docker entrypoint
if (window.__RUNTIME_CONFIG__) {
// @ts-expect-error
const config = window.__RUNTIME_CONFIG__ as RuntimeConfig;
logger.debug('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;
}
logger.debug('Runtime config not found on window object');
return null;
}
🧰 Tools
🪛 ESLint

[error] 68-68: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 69-69: Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free.

(@typescript-eslint/ban-ts-comment)


[error] 71-71: Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free.

(@typescript-eslint/ban-ts-comment)


[error] 73-73: Unexpected console statement. Only these console methods are allowed: assert, context, error, timeStamp, time, timeEnd, warn.

(no-console)


[error] 83-83: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 84-84: Unexpected console statement. Only these console methods are allowed: assert, context, error, timeStamp, time, timeEnd, warn.

(no-console)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/EnvironmentConfig.ts around lines 63 to 86,
replace the console.log calls with the project's logger (import it at top if
missing) and change the TypeScript directive comments from @ts-ignore to
@ts-expect-error; specifically, use logger.debug/info for the runtime-config
presence/absence messages and wrap the injected __RUNTIME_CONFIG__ access with
// @ts-expect-error instead of // @ts-ignore so the linter/type-checker
expectations are explicit.

Comment on lines +206 to +229
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();
}

// 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}`);
return BUILD_CONFIG.apiKeys[provider];
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Same SSR guard in getApiKey; also trim-check build-time values.

Prevents crashes and avoids returning whitespace-only keys.

-    const storageKey = STORAGE_KEYS[provider];
-    const localStorageKey = localStorage.getItem(storageKey);
+    const storageKey = STORAGE_KEYS[provider];
+    const localStorageKey =
+      typeof window !== 'undefined' && typeof window.localStorage !== 'undefined'
+        ? window.localStorage.getItem(storageKey)
+        : null;
@@
-      localStorage.setItem(storageKey, runtimeKey);
+      if (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined') {
+        window.localStorage.setItem(storageKey, runtimeKey);
+      }
       return runtimeKey;
     }
@@
-    if (BUILD_CONFIG?.apiKeys?.[provider]) {
+    if (typeof BUILD_CONFIG?.apiKeys?.[provider] === 'string' &&
+        BUILD_CONFIG.apiKeys[provider]!.trim() !== '') {
       logger.debug(`Using build-time API key for ${provider}`);
-      return BUILD_CONFIG.apiKeys[provider];
+      return BUILD_CONFIG.apiKeys[provider]!.trim();
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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();
}
// 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}`);
return BUILD_CONFIG.apiKeys[provider];
}
getApiKey(provider: APIKeyProvider): string {
// First check localStorage (highest priority)
const storageKey = STORAGE_KEYS[provider];
const localStorageKey =
typeof window !== 'undefined' && typeof window.localStorage !== 'undefined'
? window.localStorage.getItem(storageKey)
: null;
if (localStorageKey && localStorageKey.trim() !== '') {
logger.debug(`Using localStorage API key for ${provider}`);
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
if (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined') {
window.localStorage.setItem(storageKey, runtimeKey);
}
return runtimeKey;
}
// Fallback to build-time configuration
if (typeof BUILD_CONFIG?.apiKeys?.[provider] === 'string' &&
BUILD_CONFIG.apiKeys[provider]!.trim() !== '') {
logger.debug(`Using build-time API key for ${provider}`);
return BUILD_CONFIG.apiKeys[provider]!.trim();
}
🧰 Tools
🪛 ESLint

[error] 210-210: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/EnvironmentConfig.ts around lines 206 to 229,
add SSR-safe guards around all localStorage access (check typeof window !==
'undefined' && window.localStorage) before getItem/setItem to avoid server-side
crashes, ensure runtimeKey and build-time API key values are trimmed and
validated (only return non-empty trimmed strings), and when persisting
runtimeKey to localStorage save the trimmed value rather than the raw string.

Comment on lines +287 to +309
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
};
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Type bug: validateCredentials omits 'runtime' in source.

getApiKeySource can return 'runtime'; the narrower return type here will cause TS errors.

-  validateCredentials(provider: APIKeyProvider): {
+  validateCredentials(provider: APIKeyProvider): {
     isValid: boolean;
     message: string;
-    source?: 'localStorage' | 'build-time';
+    source?: 'localStorage' | 'runtime' | 'build-time';
     missingItems?: string[];
   } {
@@
-    return {
+    return {
       isValid: true,
       message: `${provider} credentials are configured correctly (source: ${source}).`,
-      source: source !== 'none' ? source : undefined
+      source: (source === 'none') ? undefined : source
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
};
}
validateCredentials(provider: APIKeyProvider): {
isValid: boolean;
message: string;
source?: 'localStorage' | 'runtime' | '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') ? undefined : source
};
}
🧰 Tools
🪛 ESLint

[error] 288-288: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 289-289: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 290-290: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 291-291: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 295-295: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 303-303: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/EnvironmentConfig.ts around lines 287 to 309,
the validateCredentials return type for source is too narrow and omits
'runtime', causing TypeScript errors when getApiKeySource returns 'runtime';
update the source type to include 'runtime' (e.g., source?: 'localStorage' |
'build-time' | 'runtime') so the returned object correctly types the value,
leaving the runtime check/assignment logic unchanged.

} 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');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Good fallback to env for Groq; initialize envConfig earlier and extend the same fallback to the manual “Fetch Models” UX.

  • Minor: define const envConfig = getEnvironmentConfig() before registering this listener to avoid any edge-case timing issues.
  • Essential (paired with the “don’t prefill secrets” fix below): make the Groq “Fetch Models” button usable when only a runtime-injected key exists by enabling on env presence and falling back in the click handler.

Example (non-diff) adjustments:

// enable/disable
const hasEnvGroqKey = !!envConfig.getApiKey('groq');
fetchGroqModelsButton.disabled = !(groqApiKeyInput.value.trim() || hasEnvGroqKey);
groqApiKeyInput.addEventListener('input', () => {
  fetchGroqModelsButton.disabled = !(groqApiKeyInput.value.trim() || hasEnvGroqKey);
});

// click handler fallback (inside fetchGroqModelsButton click listener)
const groqApiKey = groqApiKeyInput.value.trim() || envConfig.getApiKey('groq');
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/ui/SettingsDialog.ts around line 529, envConfig is
used too late and the Groq "Fetch Models" button doesn't enable when only a
runtime/injected env key exists; initialize const envConfig =
getEnvironmentConfig() before registering the listener, compute a hasEnvGroqKey
boolean, use that to set fetchGroqModelsButton.disabled initially and inside the
groqApiKeyInput input listener so the button enables when either the input has
text or the env key is present, and in the fetchGroqModelsButton click handler
fall back to envConfig.getApiKey('groq') when groqApiKeyInput.value.trim() is
empty.

} 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');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Same as Groq: keep OpenRouter usable with only runtime-injected keys.

Enable the button if an env key exists and fall back in the click handler.

Example (non-diff):

const hasEnvOpenRouterKey = !!envConfig.getApiKey('openrouter');
fetchOpenRouterModelsButton.disabled = !(openrouterApiKeyInput.value.trim() || hasEnvOpenRouterKey);
openrouterApiKeyInput.addEventListener('input', () => {
  fetchOpenRouterModelsButton.disabled = !(openrouterApiKeyInput.value.trim() || hasEnvOpenRouterKey);
});

// in click handler
const openrouterApiKey = openrouterApiKeyInput.value.trim() || envConfig.getApiKey('openrouter');
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/ui/SettingsDialog.ts around line 548, the current
logic only enables the OpenRouter model fetch button when the input contains a
key, which prevents using a runtime/injected env key; update the logic to treat
an env key as valid for enabling the button and keep the existing fallback in
the click handler. Specifically, compute a boolean hasEnvOpenRouterKey from
envConfig.getApiKey('openrouter'), use that or the trimmed input value to set
fetchOpenRouterModelsButton.disabled initially and inside the input event
listener, and leave the click handler to read the API key with the existing
fallback (trimmed input || envConfig.getApiKey('openrouter')).

@claude
Copy link

claude bot commented Sep 6, 2025

Claude finished @olesho's task —— View job


PR Review: Pass API keys for docker build

Todo List:

  • Read repository setup instructions (CLAUDE.md)
  • Examine Docker configuration changes
  • Review TypeScript LLM provider changes
  • Analyze new EnvironmentConfig.ts file
  • Check for security concerns with API key handling
  • Assess code quality and best practices
  • Provide comprehensive feedback

🔒 Security Analysis

⚠️ Critical Security Issues

1. Exposed API Keys in Repository

  • Location: front_end/models/crux-manager/CrUXManager.ts:23
  • Issue: Google API key AIzaSyCCSOx25vrb5z0tbedCB3_JRzzbVW6Uwgw is hardcoded
  • Location: front_end/models/trace/handlers/NetworkRequestsHandler.test.ts:421
  • Issue: Firebase API key AIzaSyCGxLbbFQxH4BV1fY0RODlxTos9nJa2l_g is hardcoded
  • Risk: These keys are publicly visible and should be rotated immediately
  • Recommendation: Move to environment variables and rotate the exposed keys

2. Runtime API Key Injection Security Concerns

  • Location: docker/docker-entrypoint.sh:15-61
  • Issue: Script directly injects API keys into JavaScript files and logs their presence
  • Risk: Keys become visible in browser dev tools and container logs
  • Recommendation: Use secure runtime configuration or server-side proxy instead

✅ Security Best Practices Implemented

1. Proper Fallback Hierarchy

  • localStorage (user input) → runtime config → build-time config → empty string
  • Prevents accidental exposure of sensitive data

2. Build-time vs Runtime Separation

  • API keys are injected at runtime, not baked into image layers
  • Docker compose properly handles environment variable passing

🏗️ Code Quality Assessment

✅ Strengths

1. Well-Structured Environment Config

  • Clean singleton pattern implementation
  • Comprehensive API key management with multiple fallback sources
  • Good separation of concerns

2. Consistent Integration Pattern

  • All LLM providers follow the same pattern for API key handling
  • Backward compatibility maintained with constructor parameters

3. Proper Error Handling

  • Comprehensive validation methods
  • Clear error messages for missing credentials

⚠️ Code Quality Issues

1. Missing localStorage Safety Guards

  • Location: front_end/panels/ai_chat/core/EnvironmentConfig.ts:147,254
  • Issue: Direct localStorage access without checking availability
  • Risk: Crashes in non-browser environments (SSR, tests)
  • Fix: Guard with typeof localStorage !== 'undefined'

2. Inconsistent Validation Methods

  • Location: front_end/panels/ai_chat/core/EnvironmentConfig.ts:291
  • Issue: Return type missing 'runtime' source option
  • Impact: TypeScript errors when runtime source is detected

3. Console.log Usage

  • Location: front_end/panels/ai_chat/core/EnvironmentConfig.ts:73,84
  • Issue: Using console.log instead of proper logging framework
  • Fix: Use existing logger infrastructure

🚀 Performance Considerations

✅ Good Practices

  • Singleton pattern prevents multiple config instantiations
  • Caching implemented for vision model detection (OpenRouterProvider)
  • Lazy initialization of environment config

📝 Minor Optimizations

  • Debug logging could be made conditional to reduce overhead in production
  • Vision model cache could be shared across provider instances

🧪 Test Coverage

❌ Missing Test Coverage

  • No unit tests for the new EnvironmentConfig class
  • No tests for API key fallback hierarchy
  • No tests for validation methods
  • Docker build integration not tested

📋 Recommended Test Cases

  1. EnvironmentConfig fallback hierarchy testing
  2. API key validation edge cases
  3. Build-time configuration injection
  4. Provider credential validation consistency

📚 Documentation & Maintainability

✅ Good Documentation

  • Comprehensive JSDoc comments
  • Clear explanation of fallback hierarchy
  • Build process documented in Docker files

📝 Areas for Improvement

  • README should document how to pass API keys during Docker build
  • Environment variable naming conventions could be documented
  • Migration guide for existing users would be helpful

🛠️ Specific Recommendations

High Priority (Must Fix)

  1. Rotate exposed API keys found in CrUXManager.ts and NetworkRequestsHandler.test.ts
  2. Add localStorage safety guards in EnvironmentConfig.ts
  3. Fix TypeScript return type for validateCredentials to include 'runtime'
  4. Use proper logging instead of console.log statements

Medium Priority (Should Fix)

  1. Use more secure API key injection method (avoid direct JS file modification)
  2. Add unit tests for EnvironmentConfig class
  3. Implement consistent error handling across all providers

Low Priority (Nice to Have)

  1. Add migration documentation
  2. Implement conditional debug logging
  3. Share vision model cache across provider instances

✅ Overall Assessment

This PR successfully implements a robust API key management system for Docker builds. The core architecture is sound with good separation of concerns and proper fallback mechanisms. However, there are critical security issues (exposed API keys in repository) that must be addressed before merging.

The Docker runtime injection approach, while functional, could be more secure. Consider using server-side proxy or OAuth for production deployments.

Recommendation: Request changes to fix the critical issues, then this will be ready for merge.


@tysonthomas9
Copy link
Collaborator

I see Claude Code and Code Rabbit is busy reviewing 🤣

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
front_end/panels/ai_chat/LLM/OpenRouterProvider.ts (2)

377-383: Same Authorization fix for fetchVisionModels().

-        headers: {
-          'Authorization': `Bearer ${this.getApiKey()}`,
-        },
+        headers: {
+          Authorization: (() => {
+            const k = this.getApiKey()?.trim();
+            if (!k) throw new Error('OpenRouter API key is missing. Configure it in Settings.');
+            return `Bearer ${k}`;
+          })(),
+        },

640-673: SSR-safe access to localStorage in validateCredentials; drop redundant key “presence” log.

-    const authMethod = localStorage.getItem('openrouter_auth_method');
-    const oauthToken = localStorage.getItem('openrouter_oauth_token');
+    const hasLS = typeof window !== 'undefined' && !!window.localStorage;
+    const authMethod = hasLS ? window.localStorage.getItem('openrouter_auth_method') : null;
+    const oauthToken = hasLS ? window.localStorage.getItem('openrouter_oauth_token') : null;
@@
-    logger.debug('- API key presence:', apiKey ? '<redacted>' : 'none');
♻️ Duplicate comments (6)
front_end/panels/ai_chat/core/EnvironmentConfig.ts (4)

206-234: SSR-safe localStorage and trimming in getApiKey; also trim build-time keys.

-    const storageKey = STORAGE_KEYS[provider];
-    const localStorageKey = localStorage.getItem(storageKey);
+    const storageKey = STORAGE_KEYS[provider];
+    const hasLS = typeof window !== 'undefined' && !!window.localStorage;
+    const localStorageKey = hasLS ? window.localStorage.getItem(storageKey) : null;
@@
-      // Also save to localStorage for future use
-      localStorage.setItem(storageKey, runtimeKey);
+      // Also save to localStorage for future use
+      if (hasLS) {
+        window.localStorage.setItem(storageKey, runtimeKey);
+      }
       return runtimeKey;
     }
@@
-    if (BUILD_CONFIG?.apiKeys?.[provider]) {
+    if (typeof BUILD_CONFIG?.apiKeys?.[provider] === 'string' &&
+        BUILD_CONFIG.apiKeys[provider]!.trim() !== '') {
       logger.debug(`Using build-time API key for ${provider}`);
-      return BUILD_CONFIG.apiKeys[provider];
+      return BUILD_CONFIG.apiKeys[provider]!.trim();
     }

252-270: SSR guard in getApiKeySource for localStorage access; keep non-empty checks aligned.

-    const storageKey = STORAGE_KEYS[provider];
-    const localStorageKey = localStorage.getItem(storageKey);
+    const storageKey = STORAGE_KEYS[provider];
+    const localStorageKey =
+      (typeof window !== 'undefined' && !!window.localStorage)
+        ? window.localStorage.getItem(storageKey)
+        : null;

288-310: Type bug: validateCredentials omits 'runtime' from source union (TS mismatch).

-  validateCredentials(provider: APIKeyProvider): {
+  validateCredentials(provider: APIKeyProvider): {
     isValid: boolean;
     message: string;
-    source?: 'localStorage' | 'build-time';
+    source?: 'localStorage' | 'runtime' | 'build-time';
     missingItems?: string[];
   } {
@@
-    return {
+    return {
       isValid: true,
       message: `${provider} credentials are configured correctly (source: ${source}).`,
-      source: source !== 'none' ? source : undefined
+      source: (source === 'none') ? undefined : source
     };

63-86: Replace console.log and @ts-ignore with logger and @ts-expect-error (lint blocker).

Use the project logger and TS-safe directive to satisfy no-console and ban-ts-comment rules.

-  // @ts-ignore - __RUNTIME_CONFIG__ is injected by Docker entrypoint
+  // @ts-expect-error __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:', {
+    // @ts-expect-error
+    const config = window.__RUNTIME_CONFIG__ as RuntimeConfig;
+    logger.debug('Runtime config found:', {
@@
-  console.log('Runtime config not found on window object');
+  logger.debug('Runtime config not found on window object');
front_end/panels/ai_chat/LLM/GroqProvider.ts (2)

111-116: Don’t send an empty Bearer token; unquote header.

Build Authorization only with a non-empty key and fail fast otherwise.

-        headers: {
-          'Content-Type': 'application/json',
-          'Authorization': `Bearer ${this.getApiKey()}`,
-        },
+        headers: {
+          'Content-Type': 'application/json',
+          Authorization: (() => {
+            const k = this.getApiKey()?.trim();
+            if (!k) throw new Error('Groq API key is missing. Configure it in Settings.');
+            return `Bearer ${k}`;
+          })(),
+        },

279-283: Same fix for models request Authorization header.

-        headers: {
-          'Authorization': `Bearer ${this.getApiKey()}`,
-        },
+        headers: {
+          Authorization: (() => {
+            const k = this.getApiKey()?.trim();
+            if (!k) throw new Error('Groq API key is missing. Configure it in Settings.');
+            return `Bearer ${k}`;
+          })(),
+        },
🧹 Nitpick comments (4)
front_end/panels/ai_chat/core/EnvironmentConfig.ts (2)

106-108: Doc drift: update fallback order to include runtime.

- * Provides unified access to API keys with fallback hierarchy:
- * localStorage → build-time config → empty string
+ * Provides unified access to API keys with fallback hierarchy:
+ * localStorage → runtime (Docker) → build-time config → empty string

7-7: Clean up trailing whitespace and add missing EOF newline (lint).

Run lint autofix; remove trailing spaces and ensure a newline at EOF to unblock CI.

Also applies to: 11-11, 13-13, 68-68, 83-85, 105-105, 117-117, 120-120, 148-148, 172-172, 179-179, 196-196, 202-202, 210-210, 248-248, 255-255, 263-264, 268-268, 296-296, 304-304, 314-314, 332-332, 337-337

front_end/panels/ai_chat/LLM/GroqProvider.ts (1)

5-12: Import order nit: move value import before type-only import to satisfy lint.

-import type { LLMMessage, LLMResponse, LLMCallOptions, LLMProvider, ModelInfo } from './LLMTypes.js';
-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';
+import { getEnvironmentConfig } from '../core/EnvironmentConfig.js';
+import { LLMBaseProvider } from './LLMProvider.js';
+import { LLMRetryManager } from './LLMErrorHandler.js';
+import { LLMResponseParser } from './LLMResponseParser.js';
+import { createLogger } from '../core/Logger.js';
+import type { LLMMessage, LLMResponse, LLMCallOptions, LLMProvider, ModelInfo } from './LLMTypes.js';
front_end/panels/ai_chat/LLM/OpenRouterProvider.ts (1)

5-11: Import order nit: place value import before type-only import to satisfy lint.

-import type { LLMMessage, LLMResponse, LLMCallOptions, LLMProvider, ModelInfo } from './LLMTypes.js';
-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';
+import { getEnvironmentConfig } from '../core/EnvironmentConfig.js';
+import { LLMBaseProvider } from './LLMProvider.js';
+import { LLMRetryManager } from './LLMErrorHandler.js';
+import { LLMResponseParser } from './LLMResponseParser.js';
+import { createLogger } from '../core/Logger.js';
+import type { LLMMessage, LLMResponse, LLMCallOptions, LLMProvider, ModelInfo } from './LLMTypes.js';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ded87fd and c275347.

📒 Files selected for processing (4)
  • docker/Dockerfile (3 hunks)
  • front_end/panels/ai_chat/LLM/GroqProvider.ts (5 hunks)
  • front_end/panels/ai_chat/LLM/OpenRouterProvider.ts (7 hunks)
  • front_end/panels/ai_chat/core/EnvironmentConfig.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • docker/Dockerfile
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
PR: BrowserOperator/browser-operator-core#0
File: eval-server/nodejs/CLAUDE.md:0-0
Timestamp: 2025-08-29T23:08:07.342Z
Learning: Applies to eval-server/nodejs/src/config.js : Centralize configuration via environment variables in src/config.js
Learnt from: CR
PR: BrowserOperator/browser-operator-core#0
File: eval-server/nodejs/CLAUDE.md:0-0
Timestamp: 2025-08-29T23:08:07.342Z
Learning: Applies to eval-server/nodejs/src/config.js : Require OPENAI_API_KEY and PORT (default 8080) from environment configuration
📚 Learning: 2025-08-29T23:08:07.342Z
Learnt from: CR
PR: BrowserOperator/browser-operator-core#0
File: eval-server/nodejs/CLAUDE.md:0-0
Timestamp: 2025-08-29T23:08:07.342Z
Learning: Applies to eval-server/nodejs/src/config.js : Require OPENAI_API_KEY and PORT (default 8080) from environment configuration

Applied to files:

  • front_end/panels/ai_chat/core/EnvironmentConfig.ts
📚 Learning: 2025-08-29T23:08:07.342Z
Learnt from: CR
PR: BrowserOperator/browser-operator-core#0
File: eval-server/nodejs/CLAUDE.md:0-0
Timestamp: 2025-08-29T23:08:07.342Z
Learning: Applies to eval-server/nodejs/src/config.js : Centralize configuration via environment variables in src/config.js

Applied to files:

  • front_end/panels/ai_chat/core/EnvironmentConfig.ts
🧬 Code graph analysis (3)
front_end/panels/ai_chat/core/EnvironmentConfig.ts (3)
front_end/panels/ai_chat/core/Logger.ts (1)
  • createLogger (316-318)
front_end/panels/ai_chat/core/Constants.ts (1)
  • STORAGE_KEYS (39-45)
front_end/panels/ai_chat/ui/AIChatPanel.ts (1)
  • localStorage (772-795)
front_end/panels/ai_chat/LLM/GroqProvider.ts (2)
front_end/panels/ai_chat/core/EnvironmentConfig.ts (1)
  • getEnvironmentConfig (335-337)
front_end/panels/ai_chat/ui/AIChatPanel.ts (2)
  • apiKey (1112-1143)
  • apiKey (1185-1204)
front_end/panels/ai_chat/LLM/OpenRouterProvider.ts (1)
front_end/panels/ai_chat/core/EnvironmentConfig.ts (1)
  • getEnvironmentConfig (335-337)
🪛 ESLint
front_end/panels/ai_chat/core/EnvironmentConfig.ts

[error] 7-7: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 11-11: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 13-13: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 27-27: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 28-28: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 29-29: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 30-30: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 68-68: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 69-69: Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free.

(@typescript-eslint/ban-ts-comment)


[error] 71-71: Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free.

(@typescript-eslint/ban-ts-comment)


[error] 73-73: Unexpected console statement. Only these console methods are allowed: assert, context, error, timeStamp, time, timeEnd, warn.

(no-console)


[error] 83-83: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 84-84: Unexpected console statement. Only these console methods are allowed: assert, context, error, timeStamp, time, timeEnd, warn.

(no-console)


[error] 105-105: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 117-117: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 120-120: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 148-148: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 172-172: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 179-179: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 196-196: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 202-202: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 210-210: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 238-238: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 248-248: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 255-255: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 259-259: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 263-263: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 264-264: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 268-268: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 274-274: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 284-284: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 289-289: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 290-290: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 291-291: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 292-292: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 296-296: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 304-304: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 314-314: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 318-318: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 319-319: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 320-320: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 332-332: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 337-337: Newline required at end of file but not found.

(@stylistic/eol-last)

front_end/panels/ai_chat/LLM/GroqProvider.ts

[error] 10-10: ../core/EnvironmentConfig.js import should occur before type import of ./LLMTypes.js

(import/order)


[error] 60-60: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 115-115: Unnecessarily quoted property 'Authorization' found.

(@stylistic/quote-props)


[error] 282-282: Unnecessarily quoted property 'Authorization' found.

(@stylistic/quote-props)

front_end/panels/ai_chat/LLM/OpenRouterProvider.ts

[error] 10-10: ../core/EnvironmentConfig.js import should occur before type import of ./LLMTypes.js

(import/order)


[error] 81-81: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 167-167: Unnecessarily quoted property 'Authorization' found.

(@stylistic/quote-props)


[error] 347-347: Unnecessarily quoted property 'Authorization' found.

(@stylistic/quote-props)


[error] 381-381: Unnecessarily quoted property 'Authorization' found.

(@stylistic/quote-props)


[error] 646-646: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 651-651: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 671-671: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)

Comment on lines +145 to +158
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}`);
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard localStorage for SSR/tests in initializeFromRuntime (prevents ReferenceError).

Accessing localStorage unguarded will crash in non-browser contexts; also persist trimmed values.

-    for (const provider of providers) {
-      const storageKey = STORAGE_KEYS[provider];
-      const existingKey = localStorage.getItem(storageKey);
+    const hasLS = typeof window !== 'undefined' && !!window.localStorage;
+    for (const provider of providers) {
+      const storageKey = STORAGE_KEYS[provider];
+      const existingKey = hasLS ? window.localStorage.getItem(storageKey) : null;
@@
-      if (!existingKey || existingKey.trim() === '') {
+      if (!existingKey || existingKey.trim() === '') {
         const runtimeKey = this.getRuntimeKey(provider);
         if (runtimeKey) {
-          localStorage.setItem(storageKey, runtimeKey);
+          if (hasLS) {
+            window.localStorage.setItem(storageKey, runtimeKey.trim());
+          }
           savedCount++;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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}`);
}
}
}
// Guard against non-browser contexts (SSR/tests) for localStorage
const hasLS = typeof window !== 'undefined' && !!window.localStorage;
for (const provider of providers) {
const storageKey = STORAGE_KEYS[provider];
const existingKey = hasLS ? window.localStorage.getItem(storageKey) : null;
// Only save if not already in localStorage
if (!existingKey || existingKey.trim() === '') {
const runtimeKey = this.getRuntimeKey(provider);
if (runtimeKey) {
if (hasLS) {
window.localStorage.setItem(storageKey, runtimeKey.trim());
}
savedCount++;
logger.debug(`Saved runtime API key to localStorage for ${provider}`);
}
}
}
🧰 Tools
🪛 ESLint

[error] 148-148: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/EnvironmentConfig.ts around lines 145 to 158,
the loop unguardedly reads/writes localStorage which will throw in SSR/tests and
it saves untrimmed values; wrap access in a runtime check (e.g., ensure typeof
window !== 'undefined' && window.localStorage) and guard each getItem/setItem
behind that check, trim existingKey and runtimeKey before using them, and wrap
storage operations in a try/catch to swallow/storage-related errors so
initialization won't crash in non-browser environments.

Comment on lines 166 to 170
'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
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid empty Bearer token; unquote Authorization header.

-        headers: {
-          'Content-Type': 'application/json',
-          '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
-        },
+        headers: {
+          'Content-Type': 'application/json',
+          Authorization: (() => {
+            const k = this.getApiKey()?.trim();
+            if (!k) throw new Error('OpenRouter API key is missing. Configure it in Settings.');
+            return `Bearer ${k}`;
+          })(),
+          'HTTP-Referer': 'https://browseroperator.io',
+          'X-Title': 'Browser Operator',
+        },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'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
},
headers: {
'Content-Type': 'application/json',
Authorization: (() => {
const k = this.getApiKey()?.trim();
if (!k) throw new Error('OpenRouter API key is missing. Configure it in Settings.');
return `Bearer ${k}`;
})(),
'HTTP-Referer': 'https://browseroperator.io',
'X-Title': 'Browser Operator',
},
🧰 Tools
🪛 ESLint

[error] 167-167: Unnecessarily quoted property 'Authorization' found.

(@stylistic/quote-props)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/LLM/OpenRouterProvider.ts around lines 166-170, the
Authorization header currently risks sending an empty "Bearer " token and may be
improperly quoted; update the code to first read const apiKey =
this.getApiKey(), then only add the Authorization header when apiKey is truthy
(omit the header entirely if empty) and set it unquoted as Authorization:
`Bearer ${apiKey}` so no quoted string or empty bearer token is sent.

Comment on lines 344 to 349
const response = await fetch(this.getToolSupportingModelsEndpoint(), {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Authorization': `Bearer ${this.getApiKey()}`,
},
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Same Authorization fix for fetchModels().

-        headers: {
-          'Authorization': `Bearer ${this.getApiKey()}`,
-        },
+        headers: {
+          Authorization: (() => {
+            const k = this.getApiKey()?.trim();
+            if (!k) throw new Error('OpenRouter API key is missing. Configure it in Settings.');
+            return `Bearer ${k}`;
+          })(),
+        },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const response = await fetch(this.getToolSupportingModelsEndpoint(), {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Authorization': `Bearer ${this.getApiKey()}`,
},
});
const response = await fetch(this.getToolSupportingModelsEndpoint(), {
method: 'GET',
headers: {
Authorization: (() => {
const k = this.getApiKey()?.trim();
if (!k) throw new Error('OpenRouter API key is missing. Configure it in Settings.');
return `Bearer ${k}`;
})(),
},
});
🧰 Tools
🪛 ESLint

[error] 347-347: Unnecessarily quoted property 'Authorization' found.

(@stylistic/quote-props)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/LLM/OpenRouterProvider.ts around lines 344 to 349,
ensure fetchModels() uses the same Authorization header format as other calls:
include an Authorization header set to `Bearer ${this.getApiKey()}` when calling
the models endpoint; update the fetch call to add the headers object
(Authorization: `Bearer ${this.getApiKey()}`) so the API key is sent correctly
with the request.

@tysonthomas9
Copy link
Collaborator

@olesho Can you add documentation on the feature?

@olesho olesho closed this Sep 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants