Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 53 additions & 8 deletions cortex/api_key_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,32 @@
Detection order (highest priority first):
1. CORTEX_PROVIDER=ollama environment variable (for explicit Ollama mode)
2. API key environment variables: ANTHROPIC_API_KEY, OPENAI_API_KEY
3. Cached key location (~/.cortex/.api_key_cache)
4. Saved Ollama provider preference in ~/.cortex/.env (CORTEX_PROVIDER=ollama)
5. API keys in ~/.cortex/.env
6. ~/.config/anthropic/credentials.json (Claude CLI location)
7. ~/.config/openai/credentials.json
8. .env in current directory
3. Encrypted storage (~/.cortex/environments/cortex.json) - secure Fernet-encrypted storage
4. Cached key location (~/.cortex/.api_key_cache)
5. Saved Ollama provider preference in ~/.cortex/.env (CORTEX_PROVIDER=ollama)
6. API keys in ~/.cortex/.env
7. ~/.config/anthropic/credentials.json (Claude CLI location)
8. ~/.config/openai/credentials.json
9. .env in current directory

Implements caching to avoid repeated file checks, file locking for safe
concurrent access, and supports manual entry with optional saving to
~/.cortex/.env.
concurrent access, encrypted storage for sensitive keys, and supports
manual entry with optional saving.

"""

import fcntl
import json
import logging
import os
import re
from pathlib import Path
from typing import Optional

from cortex.branding import console, cx_print

logger = logging.getLogger(__name__)

# Constants
CORTEX_DIR = ".cortex"
CORTEX_ENV_FILE = ".env"
Expand Down Expand Up @@ -74,6 +78,14 @@ def detect(self) -> tuple[bool, str | None, str | None, str | None]:
"""
Auto-detect API key from common locations.

Detection order (highest priority first):
1. CORTEX_PROVIDER=ollama environment variable
2. API key environment variables (ANTHROPIC_API_KEY, OPENAI_API_KEY)
3. Encrypted storage (~/.cortex/environments/cortex.json)
4. Cached key location
5. Saved Ollama provider preference
6. File-based locations (.env files, credentials.json)

Returns:
Tuple of (found, key, provider, source)
- found: True if key was found
Expand All @@ -90,6 +102,11 @@ def detect(self) -> tuple[bool, str | None, str | None, str | None]:
if result:
return result

# Check encrypted storage (secure storage from first-run wizard)
result = self._check_encrypted_storage()
if result:
return result

# Check cached location
result = self._check_cached_key()
if result:
Expand All @@ -113,6 +130,34 @@ def _check_environment_api_keys(self) -> tuple[bool, str, str, str] | None:
return (True, value, provider, "environment")
return None

def _check_encrypted_storage(self) -> tuple[bool, str, str, str] | None:
"""Check for API keys in encrypted storage (~/.cortex/environments/cortex.json).

This is the secure storage location used by the first-run wizard.
Keys are stored encrypted using Fernet encryption.
"""
try:
from cortex.env_manager import get_env_manager

env_mgr = get_env_manager()

# Check for API keys in encrypted storage
for env_var, provider in ENV_VAR_PROVIDERS.items():
value = env_mgr.get_variable(app="cortex", key=env_var, decrypt=True)
if value:
# Set in environment for this session
os.environ[env_var] = value
logger.debug(f"Loaded {env_var} from encrypted storage")
return (True, value, provider, "encrypted storage (~/.cortex/environments/)")
except ImportError:
# cryptography not installed, skip encrypted storage check
logger.debug("cryptography not installed, skipping encrypted storage check")
except Exception as e:
# Log but don't fail - other detection methods may work
logger.debug(f"Could not check encrypted storage: {e}")

return None

def _check_saved_ollama_provider(self) -> tuple[bool, str, str, str] | None:
"""Check if Ollama was previously selected as the provider in config file."""
env_file = Path.home() / CORTEX_DIR / CORTEX_ENV_FILE
Expand Down
48 changes: 37 additions & 11 deletions cortex/first_run_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@
from pathlib import Path
from typing import Any

from cortex.env_manager import get_env_manager

logger = logging.getLogger(__name__)

# Application name for storing cortex API keys
CORTEX_APP_NAME = "cortex"


class WizardStep(Enum):
"""Steps in the first-run wizard."""
Expand Down Expand Up @@ -786,21 +791,42 @@ def _prompt(self, message: str, default: str = "") -> str:
return default

def _save_env_var(self, name: str, value: str):
"""Save environment variable to shell config."""
shell = os.environ.get("SHELL", "/bin/bash")
shell_name = os.path.basename(shell)
config_file = self._get_shell_config(shell_name)
"""Save environment variable securely using encrypted storage.

export_line = f'\nexport {name}="{value}"\n' # nosec - intentional user config storage
API keys are stored encrypted in ~/.cortex/environments/cortex.json
using Fernet encryption. The encryption key is stored in
~/.cortex/.env_key with restricted permissions (chmod 600).
"""
# Set for current session regardless of storage success
os.environ[name] = value

try:
with open(config_file, "a") as f:
f.write(export_line)

# Also set for current session
os.environ[name] = value
env_mgr = get_env_manager()

# Handle brand names correctly (e.g., "OpenAI" not "Openai")
provider_name_raw = name.replace("_API_KEY", "")
if provider_name_raw == "OPENAI":
provider_name_display = "OpenAI"
elif provider_name_raw == "ANTHROPIC":
provider_name_display = "Anthropic"
else:
provider_name_display = provider_name_raw.replace("_", " ").title()

env_mgr.set_variable(
app=CORTEX_APP_NAME,
key=name,
value=value,
encrypt=True,
description=f"API key for {provider_name_display}",
)
logger.info(f"Saved {name} to encrypted storage")
except ImportError:
logger.warning(
f"cryptography package not installed. {name} set for current session only. "
"Install cryptography for persistent encrypted storage: pip install cryptography"
)
except Exception as e:
logger.warning(f"Could not save env var: {e}")
logger.warning(f"Could not save env var to encrypted storage: {e}")


# Convenience functions
Expand Down
Loading