Opencode-Cli / gateway /credentials_sync.py
Hermes Agent
fix(db): prevent WAL lock collisions, disable bundle regex patching, and add self-healing supervisor
e499dd1
Raw History Blame Contribute Delete
8.51 kB
"""
OpenCode Space — Automatic Credentials Synchronizer
===================================================
Automatically captures API keys and credentials entered in the OmniRoute Web UI
and persists them into:
1. Persistent Disk Vault (/data/omniroute/credentials_vault.json)
2. Hugging Face Space Secrets API (if HF_TOKEN with write permission is present)
3. Active Process Environment (os.environ)
"""
import os
import json
import logging
import urllib.request
import urllib.error
logger = logging.getLogger("CredentialsSync")
VAULT_PATH = "/data/omniroute/credentials_vault.json"
SPACE_ID = os.environ.get("SPACE_ID", "Jishnupg/Opencode-Cli")
PROVIDER_ENV_MAP = {
"openai": "OPENAI_API_KEY",
"anthropic": "ANTHROPIC_API_KEY",
"claude": "ANTHROPIC_API_KEY",
"gemini": "GEMINI_API_KEY",
"google": "GEMINI_API_KEY",
"antigravity": "ANTIGRAVITY_API_KEY",
"deepseek": "DEEPSEEK_API_KEY",
"groq": "GROQ_API_KEY",
"nvidia": "NVIDIA_API_KEY",
"nvidia-nim": "NVIDIA_API_KEY",
"huggingface": "HF_TOKEN",
"hf": "HF_TOKEN",
"openrouter": "OPENROUTER_API_KEY",
"mistral": "MISTRAL_API_KEY",
"cohere": "COHERE_API_KEY",
"moonshot": "MOONSHOT_API_KEY",
"kimi": "MOONSHOT_API_KEY",
"kimi-coding": "MOONSHOT_API_KEY",
"together": "TOGETHER_API_KEY",
"fireworks": "FIREWORKS_API_KEY",
"cerebras": "CEREBRAS_API_KEY",
"perplexity": "PERPLEXITY_API_KEY",
"xai": "XAI_API_KEY",
"grok": "XAI_API_KEY",
"github": "GITHUB_TOKEN",
}
def normalize_provider_name(name: str) -> str:
if not name:
return ""
clean = name.lower().strip()
for k, v in PROVIDER_ENV_MAP.items():
if k in clean:
return v
# Fallback: create a sanitized env var name
sanitized = "".join(c if c.isalnum() else "_" for c in name.upper())
return f"{sanitized}_API_KEY"
def save_to_local_vault(env_name: str, secret_val: str):
"""Saves key-value pairs into /data/omniroute/credentials_vault.json."""
if not env_name or not secret_val:
return
try:
os.makedirs(os.path.dirname(VAULT_PATH), exist_ok=True)
vault = {}
if os.path.exists(VAULT_PATH):
try:
with open(VAULT_PATH, "r", encoding="utf-8") as f:
vault = json.load(f)
except Exception:
vault = {}
vault[env_name] = secret_val
with open(VAULT_PATH, "w", encoding="utf-8") as f:
json.dump(vault, f, indent=2)
# Inject into active process environment
os.environ[env_name] = secret_val
logger.info(f"[VAULT] Saved {env_name} to {VAULT_PATH} & active os.environ")
except Exception as e:
logger.warning(f"[VAULT] Failed to save {env_name} to local vault: {e}")
def sync_secret_to_huggingface(secret_name: str, secret_val: str):
"""Syncs a secret to Hugging Face Space Secrets via REST API."""
token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_API_KEY")
if not token or not secret_name or not secret_val:
return False
url = f"https://huggingface.co/api/spaces/{SPACE_ID}/secrets"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"User-Agent": "OpenCode-Space-Vault/1.0"
}
payload = {
"key": secret_name,
"value": secret_val,
"description": "Auto-synced from OmniRoute Web UI"
}
try:
req = urllib.request.Request(url, data=json.dumps(payload).encode("utf-8"), headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=10) as resp:
if resp.status in (200, 201):
logger.info(f"[HF_SYNC] Successfully uploaded secret {secret_name} to Hugging Face Space Secrets!")
return True
except urllib.error.HTTPError as e:
# If already exists, try PUT or ignore
if e.code in (400, 409):
try:
req = urllib.request.Request(url, data=json.dumps(payload).encode("utf-8"), headers=headers, method="PUT")
with urllib.request.urlopen(req, timeout=10) as resp:
logger.info(f"[HF_SYNC] Updated secret {secret_name} in Hugging Face Space Secrets.")
return True
except Exception:
pass
logger.warning(f"[HF_SYNC] HF API returned status {e.code} while syncing {secret_name}: {e.read().decode('utf-8', 'ignore')}")
except Exception as e:
logger.warning(f"[HF_SYNC] Failed to sync {secret_name} to HF Space: {e}")
return False
def handle_captured_credential(provider_hint: str, api_key: str):
"""High-level dispatch to save locally and attempt HF cloud sync."""
if not api_key:
return
api_key_str = str(api_key).strip()
if len(api_key_str) < 5:
return
# Check if this is a structured JSON string (like Antigravity OAuth object)
if api_key_str.startswith("{") and api_key_str.endswith("}"):
try:
parsed = json.loads(api_key_str)
if isinstance(parsed, dict):
# Extract refresh token or access token if available
rt = parsed.get("refresh_token") or parsed.get("token") or parsed.get("access_token")
if rt and isinstance(rt, str):
env_name = normalize_provider_name(provider_hint)
save_to_local_vault(env_name, rt)
sync_secret_to_huggingface(env_name, rt)
except Exception:
pass
env_name = normalize_provider_name(provider_hint)
save_to_local_vault(env_name, api_key_str)
sync_secret_to_huggingface(env_name, api_key_str)
import threading
_sync_timer = None
_sync_lock = threading.Lock()
def _debounced_vault_backup():
try:
from vault_sync import backup_to_vault
backup_to_vault()
except Exception as e:
logger.warning(f"[PERSISTENCE] Debounced vault backup failed: {e}")
def schedule_vault_sync(delay_seconds: float = 15.0):
global _sync_timer
with _sync_lock:
if _sync_timer and _sync_timer.is_alive():
_sync_timer.cancel()
_sync_timer = threading.Timer(delay_seconds, _debounced_vault_backup)
_sync_timer.daemon = True
_sync_timer.start()
def flush_omniroute_db():
"""Safely inspects database in read-only mode and schedules debounced cloud backup."""
db_path = os.path.join(os.environ.get("DATA_DIR", "/data/omniroute"), "storage.sqlite")
if not os.path.exists(db_path) or os.path.getsize(db_path) == 0:
return
try:
sync_sqlite_credentials_to_vault(db_path)
schedule_vault_sync(15.0)
except Exception as e:
logger.warning(f"[PERSISTENCE] flush_omniroute_db error: {e}")
def sync_sqlite_credentials_to_vault(db_path=None):
"""Scans OmniRoute SQLite database in read-only mode for any newly configured providers and syncs them."""
import sqlite3
if not db_path:
db_path = os.path.join(os.environ.get("DATA_DIR", "/data/omniroute"), "storage.sqlite")
if not os.path.exists(db_path):
return
try:
# Open in read-only URI mode to prevent any WAL locking conflicts with Node.js
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=5.0)
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
tables = [t[0] for t in cursor.fetchall()]
# Check connections and provider tables
for tbl in ["connections", "provider_connections", "keys", "providers"]:
if tbl in tables:
cursor.execute(f"SELECT * FROM {tbl}")
rows = cursor.fetchall()
col_names = [d[0].lower() for d in cursor.description]
for r in rows:
row_dict = dict(zip(col_names, r))
provider = str(row_dict.get("provider_id") or row_dict.get("provider") or row_dict.get("name") or row_dict.get("id") or "")
key_val = str(row_dict.get("api_key") or row_dict.get("key") or row_dict.get("token") or row_dict.get("credentials") or "")
if provider and key_val and len(key_val) > 5 and not key_val.startswith("enc:"):
handle_captured_credential(provider, key_val)
conn.close()
except Exception as e:
logger.debug(f"[VAULT] SQLite scan skipped: {e}")