refactor: move credentials from shell config to ~/.hive

This commit is contained in:
bryan
2026-02-27 15:55:08 -08:00
parent 3654c57f66
commit 09460b28bc
8 changed files with 277 additions and 140 deletions
+15
View File
@@ -42,6 +42,14 @@ For Vault integration:
from core.framework.credentials.vault import HashiCorpVaultStorage
"""
from .key_storage import (
delete_aden_api_key,
generate_and_save_credential_key,
load_aden_api_key,
load_credential_key,
save_aden_api_key,
save_credential_key,
)
from .models import (
CredentialDecryptionError,
CredentialError,
@@ -132,6 +140,13 @@ __all__ = [
"CredentialRefreshError",
"CredentialValidationError",
"CredentialDecryptionError",
# Key storage (bootstrap credentials)
"load_credential_key",
"save_credential_key",
"generate_and_save_credential_key",
"load_aden_api_key",
"save_aden_api_key",
"delete_aden_api_key",
# Validation
"ensure_credential_key_env",
"validate_agent_credentials",
+201
View File
@@ -0,0 +1,201 @@
"""
Dedicated file-based storage for bootstrap credentials.
HIVE_CREDENTIAL_KEY -> ~/.hive/secrets/credential_key (plain text, chmod 600)
ADEN_API_KEY -> ~/.hive/credentials/ (encrypted via EncryptedFileStorage)
Boot order:
1. load_credential_key() -- reads/generates the Fernet key, sets os.environ
2. load_aden_api_key() -- uses the encrypted store (which needs the key from step 1)
"""
from __future__ import annotations
import logging
import os
import stat
from pathlib import Path
logger = logging.getLogger(__name__)
CREDENTIAL_KEY_PATH = Path.home() / ".hive" / "secrets" / "credential_key"
CREDENTIAL_KEY_ENV_VAR = "HIVE_CREDENTIAL_KEY"
ADEN_CREDENTIAL_ID = "aden_api_key"
ADEN_ENV_VAR = "ADEN_API_KEY"
# ---------------------------------------------------------------------------
# HIVE_CREDENTIAL_KEY
# ---------------------------------------------------------------------------
def load_credential_key() -> str | None:
"""Load HIVE_CREDENTIAL_KEY with priority: env > file > shell config.
Sets ``os.environ["HIVE_CREDENTIAL_KEY"]`` as a side-effect when found.
Returns the key string, or ``None`` if unavailable everywhere.
"""
# 1. Already in environment (set by parent process, CI, Windows Registry, etc.)
key = os.environ.get(CREDENTIAL_KEY_ENV_VAR)
if key:
return key
# 2. Dedicated secrets file
key = _read_credential_key_file()
if key:
os.environ[CREDENTIAL_KEY_ENV_VAR] = key
return key
# 3. Shell config fallback (backward compat for old installs)
key = _read_from_shell_config(CREDENTIAL_KEY_ENV_VAR)
if key:
os.environ[CREDENTIAL_KEY_ENV_VAR] = key
return key
return None
def save_credential_key(key: str) -> Path:
"""Save HIVE_CREDENTIAL_KEY to ``~/.hive/secrets/credential_key``.
Creates parent dirs with mode 700, writes the file with mode 600.
Also sets ``os.environ["HIVE_CREDENTIAL_KEY"]``.
Returns:
The path that was written.
"""
path = CREDENTIAL_KEY_PATH
path.parent.mkdir(parents=True, exist_ok=True)
# Restrict the secrets directory itself
path.parent.chmod(stat.S_IRWXU) # 0o700
path.write_text(key)
path.chmod(stat.S_IRUSR | stat.S_IWUSR) # 0o600
os.environ[CREDENTIAL_KEY_ENV_VAR] = key
return path
def generate_and_save_credential_key() -> str:
"""Generate a new Fernet key and persist it to ``~/.hive/secrets/credential_key``.
Returns:
The generated key string.
"""
from cryptography.fernet import Fernet
key = Fernet.generate_key().decode()
save_credential_key(key)
return key
# ---------------------------------------------------------------------------
# ADEN_API_KEY
# ---------------------------------------------------------------------------
def load_aden_api_key() -> str | None:
"""Load ADEN_API_KEY with priority: env > encrypted store > shell config.
**Must** be called after ``load_credential_key()`` because the encrypted
store depends on HIVE_CREDENTIAL_KEY.
Sets ``os.environ["ADEN_API_KEY"]`` as a side-effect when found.
Returns the key string, or ``None`` if unavailable everywhere.
"""
# 1. Already in environment
key = os.environ.get(ADEN_ENV_VAR)
if key:
return key
# 2. Encrypted credential store
key = _read_aden_from_encrypted_store()
if key:
os.environ[ADEN_ENV_VAR] = key
return key
# 3. Shell config fallback (backward compat)
key = _read_from_shell_config(ADEN_ENV_VAR)
if key:
os.environ[ADEN_ENV_VAR] = key
return key
return None
def save_aden_api_key(key: str) -> None:
"""Save ADEN_API_KEY to the encrypted credential store.
Also sets ``os.environ["ADEN_API_KEY"]``.
"""
from pydantic import SecretStr
from .models import CredentialKey, CredentialObject
from .storage import EncryptedFileStorage
storage = EncryptedFileStorage()
cred = CredentialObject(
id=ADEN_CREDENTIAL_ID,
keys={"api_key": CredentialKey(name="api_key", value=SecretStr(key))},
)
storage.save(cred)
os.environ[ADEN_ENV_VAR] = key
def delete_aden_api_key() -> None:
"""Remove ADEN_API_KEY from the encrypted store and ``os.environ``."""
try:
from .storage import EncryptedFileStorage
storage = EncryptedFileStorage()
storage.delete(ADEN_CREDENTIAL_ID)
except Exception:
logger.debug("Could not delete %s from encrypted store", ADEN_CREDENTIAL_ID)
os.environ.pop(ADEN_ENV_VAR, None)
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _read_credential_key_file() -> str | None:
"""Read the credential key from ``~/.hive/secrets/credential_key``."""
try:
if CREDENTIAL_KEY_PATH.is_file():
value = CREDENTIAL_KEY_PATH.read_text().strip()
if value:
return value
except Exception:
logger.debug("Could not read %s", CREDENTIAL_KEY_PATH)
return None
def _read_from_shell_config(env_var: str) -> str | None:
"""Fallback: read an env var from ~/.zshrc or ~/.bashrc."""
try:
from aden_tools.credentials.shell_config import check_env_var_in_shell_config
found, value = check_env_var_in_shell_config(env_var)
if found and value:
return value
except ImportError:
pass
return None
def _read_aden_from_encrypted_store() -> str | None:
"""Try to load ADEN_API_KEY from the encrypted credential store."""
if not os.environ.get(CREDENTIAL_KEY_ENV_VAR):
return None
try:
from .storage import EncryptedFileStorage
storage = EncryptedFileStorage()
cred = storage.load(ADEN_CREDENTIAL_ID)
if cred:
return cred.get_key("api_key")
except Exception:
logger.debug("Could not load %s from encrypted store", ADEN_CREDENTIAL_ID)
return None
+10 -53
View File
@@ -256,57 +256,23 @@ class CredentialSetupSession:
def _ensure_credential_key(self) -> bool:
"""Ensure HIVE_CREDENTIAL_KEY is available for encrypted storage."""
if os.environ.get("HIVE_CREDENTIAL_KEY"):
from .key_storage import generate_and_save_credential_key, load_credential_key
if load_credential_key():
return True
# Try to load from shell config
try:
from aden_tools.credentials.shell_config import check_env_var_in_shell_config
found, value = check_env_var_in_shell_config("HIVE_CREDENTIAL_KEY")
if found and value:
os.environ["HIVE_CREDENTIAL_KEY"] = value
return True
except ImportError:
pass
# Generate a new key
self._print(f"{Colors.YELLOW}Initializing credential store...{Colors.NC}")
try:
from cryptography.fernet import Fernet
generated_key = Fernet.generate_key().decode()
os.environ["HIVE_CREDENTIAL_KEY"] = generated_key
# Save to shell config
self._save_key_to_shell_config(generated_key)
generate_and_save_credential_key()
self._print(
f"{Colors.GREEN}✓ Encryption key saved to ~/.hive/secrets/credential_key{Colors.NC}"
)
return True
except Exception as e:
self._print(f"{Colors.RED}Failed to initialize credential store: {e}{Colors.NC}")
return False
def _save_key_to_shell_config(self, key: str) -> None:
"""Save HIVE_CREDENTIAL_KEY to shell config."""
try:
from aden_tools.credentials.shell_config import (
add_env_var_to_shell_config,
)
success, config_path = add_env_var_to_shell_config(
"HIVE_CREDENTIAL_KEY",
key,
comment="Encryption key for Hive credential store",
)
if success:
self._print(f"{Colors.GREEN}✓ Encryption key saved to {config_path}{Colors.NC}")
except Exception:
# Fallback: just tell the user
self._print("\n")
self._print(
f"{Colors.YELLOW}Add this to your shell config (~/.zshrc or ~/.bashrc):{Colors.NC}"
)
self._print(f' export HIVE_CREDENTIAL_KEY="{key}"')
def _setup_single_credential(self, cred: MissingCredential) -> bool:
"""Set up a single credential. Returns True if configured."""
self._print(f"\n{Colors.CYAN}{'' * 60}{Colors.NC}")
@@ -444,19 +410,10 @@ class CredentialSetupSession:
self._print(f"{Colors.YELLOW}No key entered. Skipping.{Colors.NC}")
return False
os.environ["ADEN_API_KEY"] = aden_key
# Persist to encrypted store and set os.environ
from .key_storage import save_aden_api_key
# Save to shell config
try:
from aden_tools.credentials.shell_config import add_env_var_to_shell_config
add_env_var_to_shell_config(
"ADEN_API_KEY",
aden_key,
comment="Aden Platform API key",
)
except Exception:
pass
save_aden_api_key(aden_key)
# Sync from Aden
try:
+25 -22
View File
@@ -14,43 +14,46 @@ logger = logging.getLogger(__name__)
def ensure_credential_key_env() -> None:
"""Load credentials from shell config if not in environment.
"""Load bootstrap credentials into ``os.environ``.
The quickstart.sh and setup-credentials skill write API keys to ~/.zshrc
or ~/.bashrc. If the user hasn't sourced their config in the current shell,
this reads them directly so the runner (and any MCP subprocesses) can use them.
Priority chain for each credential:
1. ``os.environ`` (already set nothing to do)
2. Dedicated file storage (``~/.hive/secrets/`` or encrypted store)
3. Shell config fallback (``~/.zshrc`` / ``~/.bashrc``) for backward compat
Loads:
- HIVE_CREDENTIAL_KEY (encrypted credential store)
- ADEN_API_KEY (Aden OAuth sync)
- All LLM API keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, ZAI_API_KEY, etc.)
Boot order matters: HIVE_CREDENTIAL_KEY must load BEFORE ADEN_API_KEY
because the encrypted store depends on it.
Remaining LLM/tool API keys still load from shell config.
"""
from .key_storage import load_aden_api_key, load_credential_key
# Step 1: HIVE_CREDENTIAL_KEY (must come first — encrypted store depends on it)
load_credential_key()
# Step 2: ADEN_API_KEY (uses encrypted store, then shell config fallback)
load_aden_api_key()
# Step 3: Load remaining LLM/tool API keys from shell config
try:
from aden_tools.credentials.shell_config import check_env_var_in_shell_config
except ImportError:
return
# Core credentials that are always checked
env_vars_to_load = ["HIVE_CREDENTIAL_KEY", "ADEN_API_KEY"]
# Add all LLM/tool API keys from CREDENTIAL_SPECS
try:
from aden_tools.credentials import CREDENTIAL_SPECS
for spec in CREDENTIAL_SPECS.values():
if spec.env_var and spec.env_var not in env_vars_to_load:
env_vars_to_load.append(spec.env_var)
var_name = spec.env_var
if var_name and var_name not in ("HIVE_CREDENTIAL_KEY", "ADEN_API_KEY"):
if not os.environ.get(var_name):
found, value = check_env_var_in_shell_config(var_name)
if found and value:
os.environ[var_name] = value
logger.debug("Loaded %s from shell config", var_name)
except ImportError:
pass
for var_name in env_vars_to_load:
if os.environ.get(var_name):
continue
found, value = check_env_var_in_shell_config(var_name)
if found and value:
os.environ[var_name] = value
logger.debug("Loaded %s from shell config", var_name)
@dataclass
class CredentialStatus:
+6 -13
View File
@@ -139,25 +139,18 @@ def create_app(model: str | None = None) -> web.Application:
try:
from framework.credentials.validation import ensure_credential_key_env
# Load ALL credentials: HIVE_CREDENTIAL_KEY, ADEN_API_KEY, and LLM keys
ensure_credential_key_env()
# Ensure HIVE_CREDENTIAL_KEY exists and is persisted (same as TUI setup flow).
# ensure_credential_key_env() loads from shell config but won't generate a new
# key. Web-only users who never ran the TUI need one auto-generated + persisted
# so credentials survive server restarts.
# Auto-generate credential key for web-only users who never ran the TUI
if not os.environ.get("HIVE_CREDENTIAL_KEY"):
try:
from aden_tools.credentials.shell_config import add_env_var_to_shell_config
from cryptography.fernet import Fernet
from framework.credentials.key_storage import generate_and_save_credential_key
generated_key = Fernet.generate_key().decode()
os.environ["HIVE_CREDENTIAL_KEY"] = generated_key
add_env_var_to_shell_config(
"HIVE_CREDENTIAL_KEY",
generated_key,
comment="Encryption key for Hive credential store",
generate_and_save_credential_key()
logger.info(
"Generated and persisted HIVE_CREDENTIAL_KEY to ~/.hive/secrets/credential_key"
)
logger.info("Generated and persisted HIVE_CREDENTIAL_KEY to shell config")
except Exception as exc:
logger.warning("Could not auto-persist HIVE_CREDENTIAL_KEY: %s", exc)
+5 -20
View File
@@ -2,7 +2,6 @@
import asyncio
import logging
import os
from aiohttp import web
from pydantic import SecretStr
@@ -63,25 +62,15 @@ async def handle_save_credential(request: web.Request) -> web.Response:
if not credential_id or not keys or not isinstance(keys, dict):
return web.json_response({"error": "credential_id and keys are required"}, status=400)
# ADEN_API_KEY lives in env + shell config, not the encrypted store
# ADEN_API_KEY is stored in the encrypted store via key_storage module
if credential_id == "aden_api_key":
key = keys.get("api_key", "").strip()
if not key:
return web.json_response({"error": "api_key is required"}, status=400)
os.environ["ADEN_API_KEY"] = key
from framework.credentials.key_storage import save_aden_api_key
# Persist to shell config (best-effort, same pattern as TUI setup)
try:
from aden_tools.credentials.shell_config import add_env_var_to_shell_config
add_env_var_to_shell_config(
"ADEN_API_KEY",
key,
comment="Aden Platform API key",
)
except Exception as exc:
logger.warning("Could not persist ADEN_API_KEY to shell config: %s", exc)
save_aden_api_key(key)
# Immediately sync OAuth tokens from Aden (runs in executor because
# _presync_aden_tokens makes blocking HTTP calls to the Aden server).
@@ -111,13 +100,9 @@ async def handle_delete_credential(request: web.Request) -> web.Response:
credential_id = request.match_info["credential_id"]
if credential_id == "aden_api_key":
os.environ.pop("ADEN_API_KEY", None)
try:
from aden_tools.credentials.shell_config import remove_env_var_from_shell_config
from framework.credentials.key_storage import delete_aden_api_key
remove_env_var_from_shell_config("ADEN_API_KEY")
except Exception as exc:
logger.warning("Could not remove ADEN_API_KEY from shell config: %s", exc)
delete_aden_api_key()
return web.json_response({"deleted": True})
store = _get_store(request)
+2 -13
View File
@@ -160,20 +160,9 @@ class CredentialSetupScreen(ModalScreen[bool | None]):
aden_input = self.query_one("#key-aden", Input)
aden_key = aden_input.value.strip()
if aden_key:
os.environ["ADEN_API_KEY"] = aden_key
# Persist to shell config
try:
from aden_tools.credentials.shell_config import (
add_env_var_to_shell_config,
)
from framework.credentials.key_storage import save_aden_api_key
add_env_var_to_shell_config(
"ADEN_API_KEY",
aden_key,
comment="Aden Platform API key",
)
except Exception:
pass
save_aden_api_key(aden_key)
configured += 1 # ADEN_API_KEY itself counts as configured
# Run Aden sync for all Aden-backed creds (best-effort)
+13 -19
View File
@@ -1044,9 +1044,15 @@ echo ""
HIVE_CRED_DIR="$HOME/.hive/credentials"
# Check if HIVE_CREDENTIAL_KEY already exists (from env or shell rc)
HIVE_KEY_FILE="$HOME/.hive/secrets/credential_key"
# Check if HIVE_CREDENTIAL_KEY already exists (from env, file, or shell rc)
if [ -n "$HIVE_CREDENTIAL_KEY" ]; then
echo -e "${GREEN} ✓ HIVE_CREDENTIAL_KEY already set${NC}"
elif [ -f "$HIVE_KEY_FILE" ]; then
HIVE_CREDENTIAL_KEY=$(cat "$HIVE_KEY_FILE")
export HIVE_CREDENTIAL_KEY
echo -e "${GREEN} ✓ HIVE_CREDENTIAL_KEY loaded from $HIVE_KEY_FILE${NC}"
else
# Generate a new Fernet encryption key
echo -n " Generating encryption key... "
@@ -1059,13 +1065,14 @@ else
else
echo -e "${GREEN}ok${NC}"
# Save to shell rc file
echo "" >> "$SHELL_RC_FILE"
echo "# Encryption key for Hive credential store (~/.hive/credentials)" >> "$SHELL_RC_FILE"
echo "export HIVE_CREDENTIAL_KEY=\"$GENERATED_KEY\"" >> "$SHELL_RC_FILE"
# Save to dedicated secrets file (chmod 600)
mkdir -p "$(dirname "$HIVE_KEY_FILE")"
chmod 700 "$(dirname "$HIVE_KEY_FILE")"
echo -n "$GENERATED_KEY" > "$HIVE_KEY_FILE"
chmod 600 "$HIVE_KEY_FILE"
export HIVE_CREDENTIAL_KEY="$GENERATED_KEY"
echo -e "${GREEN} ✓ Encryption key saved to $SHELL_RC_FILE${NC}"
echo -e "${GREEN} ✓ Encryption key saved to $HIVE_KEY_FILE${NC}"
fi
fi
@@ -1263,22 +1270,9 @@ fi
if [ -n "$HIVE_CREDENTIAL_KEY" ]; then
echo -e "${BOLD}Credential Store:${NC}"
echo -e " ${GREEN}${NC} ${DIM}~/.hive/credentials/${NC} (encrypted)"
echo -e " ${DIM}Set up agent credentials with:${NC} ${CYAN}/setup-credentials${NC}"
echo ""
fi
echo -e "${BOLD}Build a New Agent (Claude):${NC}"
echo ""
echo -e " 1. Open Claude Code in this directory:"
echo -e " ${CYAN}claude${NC}"
echo ""
echo -e " 2. Build a new agent:"
echo -e " ${CYAN}/hive${NC}"
echo ""
echo -e " 3. Test an existing agent:"
echo -e " ${CYAN}/hive-test${NC}"
echo ""
# Show Codex instructions if available
if [ "$CODEX_AVAILABLE" = true ]; then
echo -e "${BOLD}Build a New Agent (Codex):${NC}"