Spaces:
Running
Running
File size: 8,511 Bytes
ee68a9d 21859b2 ee68a9d 21859b2 ee68a9d 21859b2 e499dd1 020b203 e499dd1 805b3d2 020b203 805b3d2 e499dd1 020b203 805b3d2 e499dd1 21859b2 805b3d2 21859b2 e499dd1 21859b2 020b203 21859b2 020b203 21859b2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | """
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}")
|