- Rename src/secrets.py → src/credential_store.py (avoid stdlib conflict) - Enhanced /status command: uptime, tokens, cost, context window usage - Session metadata now tracks input/output tokens, cost, duration - _safe_env() changed from allowlist to blocklist approach - Better Claude CLI error logging Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
70 lines
1.8 KiB
Python
70 lines
1.8 KiB
Python
"""Echo Core secrets manager — keyring wrapper."""
|
|
|
|
import json
|
|
import keyring
|
|
|
|
SERVICE = "echo-core"
|
|
|
|
# Required secrets that should exist for full functionality
|
|
REQUIRED_SECRETS = ["discord_token"]
|
|
|
|
|
|
def set_secret(name: str, value: str) -> None:
|
|
"""Store a secret in the system keyring."""
|
|
keyring.set_password(SERVICE, name, value)
|
|
# Track secret name in the registry
|
|
names = _get_registry()
|
|
if name not in names:
|
|
names.append(name)
|
|
_save_registry(names)
|
|
|
|
|
|
def get_secret(name: str) -> str | None:
|
|
"""Retrieve a secret from keyring. Returns None if not found."""
|
|
return keyring.get_password(SERVICE, name)
|
|
|
|
|
|
def get_json_secret(name: str) -> dict | None:
|
|
"""Retrieve and JSON-deserialize a secret."""
|
|
raw = get_secret(name)
|
|
if raw is None:
|
|
return None
|
|
return json.loads(raw)
|
|
|
|
|
|
def delete_secret(name: str) -> bool:
|
|
"""Delete a secret. Returns True if existed."""
|
|
try:
|
|
keyring.delete_password(SERVICE, name)
|
|
except keyring.errors.PasswordDeleteError:
|
|
pass
|
|
names = _get_registry()
|
|
if name in names:
|
|
names.remove(name)
|
|
_save_registry(names)
|
|
return True
|
|
return False
|
|
|
|
|
|
def list_secrets() -> list[str]:
|
|
"""List all stored secret names."""
|
|
return _get_registry()
|
|
|
|
|
|
def check_secrets() -> dict[str, bool]:
|
|
"""Check which required secrets exist. Returns {name: exists}."""
|
|
return {name: get_secret(name) is not None for name in REQUIRED_SECRETS}
|
|
|
|
|
|
def _get_registry() -> list[str]:
|
|
"""Get list of secret names from keyring registry."""
|
|
raw = keyring.get_password(SERVICE, "_registry")
|
|
if raw is None:
|
|
return []
|
|
return json.loads(raw)
|
|
|
|
|
|
def _save_registry(names: list[str]) -> None:
|
|
"""Save secret names registry to keyring."""
|
|
keyring.set_password(SERVICE, "_registry", json.dumps(sorted(set(names))))
|